Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,112 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.3.0] — 2026-05-07

Maximum-integration release for Yandex Dialogs platform features (Phases
0–2 of `docs/NLU_RESEARCH.md`). Six commits delivered on
`feat/platform-integration` and merged via PR
[#18](https://github.com/trudenboy/ma-provider-yandex-alice/pull/18).

### Added

- **Platform NLU consumption (Phase 0).** Read the rest of the Yandex
Dialogs request envelope:
- `meta.interfaces.screen` gates `buttons` emission so voice-only
surfaces (Mini, Pro) get the same ordinal-based prompt without
button payload.
- `request.markup.dangerous_context` short-circuits with a generic
"Не понял команду" + `end_session=true`; flagged content never
lands in `mass.music.search`.
- `request.nlu.entities[YANDEX.NUMBER]` feeds a new
`volume_relative` `ParsedControl` action: «прибавь на 20» / «убавь
5» / «на 15 громче» reads current volume, applies signed delta,
clamps `[0, 100]`, dispatches `cmd_volume_set`.
- `request.original_utterance` logged alongside the normalised
`command` for misclassification post-mortems (DEBUG only).

- **Response polish for screened surfaces (Phase 1).**
- `card` parameter plumbed through `_yandex_response` (BigImage /
ItemsList / ImageGallery shapes documented; emission deferred to
Phase 1.5 — needs separate image-upload infrastructure).
- Suggestion buttons (Следующая / Пауза / Громче / Тише) appended
to play- and control-success responses on screened surfaces.
- `provider/tts_dictionary.py` carries ~26 single-word foreign
artist transliterations (Metallica → мет+аллика, Coldplay →
к+олдплей, …) plus 16 multi-word phrases (Iron Maiden, Pink
Floyd, …); `_tts_for` now matches both Latin and Cyrillic words
so foreign band names get pronounced correctly while `text`
stays clean.
- `voice_continuation` opt-in toggle (`CONF_DIALOG_VOICE_CONTINUATION`,
default off): when enabled, play- and control-success responses
keep the conversation open. `стоп / останови / выключи` always
close the session.

- **Custom-intent grammar (Phase 2).** Eleven grammars declared on the
skill and dispatched at runtime via `request.nlu.intents`:
- `control.{pause, resume, next, previous, stop, volume_up,
volume_down, shuffle_on, shuffle_off, now_playing}`
- `play.my_wave`
- Each carries `positiveTests` for the dev-console "Протестировать"
button and uses `%lemma` directives to absorb morphology.
- Yandex's built-in `YANDEX.REJECT` (cancel pending prompt) and
`YANDEX.HELP` (contextual hint) are unlocked automatically once
any custom grammar is declared and now have runtime handlers.
- Regex parsers (`parse_command` / `parse_control`) remain as the
fallback when `request.nlu.intents` is empty — purely additive
coverage, no regression risk.
- Bumps `ya-dialogs-api==2.1.0` for the new `IntentDraft` API and
`set_intents` diff-based sync.

- **Root `CLAUDE.md`** aligned with upstream Music Assistant
`CLAUDE.md` — Sphinx-style docstrings, sync workflow, network-input
validation contract, debugging notes.

### Fixed

- **Webhook handler error handling**: post-auth dispatch is now wrapped
in `try / except` so a parser / resolver / MA-dispatch raise surfaces
as a Russian fallback ("Что-то пошло не так. Попробуй ещё раз.")
instead of HTTP 500 → Alice silence. Flagged in upstream
[music-assistant/server#3843](https://github.com/music-assistant/server/pull/3843)
by [@chrisuthe](https://github.com/chrisuthe).
- **Docstring style**: six existing Google-style docstrings (`Args:` /
`Raises:` / `Returns:`) converted to Sphinx-style (`:param:` /
`:raises:` / `:returns:`) per the upstream `CLAUDE.md` convention.
Flagged in the same upstream review.

### Fixed (review on PR [#18](https://github.com/trudenboy/ma-provider-yandex-alice/pull/18))

- **Logs no longer leak flagged content.** When
`request.markup.dangerous_context=true`, the structured "Webhook recv"
DEBUG log was still emitting the `command` and `original_utterance`
fields *before* the refusal branch ran. Both are now redacted to
`<redacted: dangerous_context>` so flagged phrases never reach
`$HOME/.musicassistant/musicassistant.log`. Found by Copilot
([#18 thread](https://github.com/trudenboy/ma-provider-yandex-alice/pull/18#discussion_r3204562269)).
- **`volume_relative` magnitude clamp accepts zero.** Previously
`max(1, …)` silently promoted "прибавь на 0" to a +1 bump. The
clamp is now `max(0, …)` so the parsed delta matches the spoken
number — `0` becomes a no-op rather than an unwanted volume change.
Found by Copilot
([#18 thread](https://github.com/trudenboy/ma-provider-yandex-alice/pull/18#discussion_r3204562328)).
- **`CONF_DIALOG_VOICE_CONTINUATION` comment accuracy.** The doc-comment
promised that "спасибо" closes the session via the `stop` control
intent, but `parse_control` does not match it. Comment corrected to
the actual matched phrases: «стоп / останови / выключи / выключи
музыку». Found by Copilot
([#18 thread](https://github.com/trudenboy/ma-provider-yandex-alice/pull/18#discussion_r3204562358)).

### Internal

- 466 unit tests (was 411). Coverage spans every new code path
including the dangerous-content log redaction, zero-magnitude
volume parse, suggestion-button gating, voice-continuation toggle,
platform-intent dispatch, REJECT / HELP handlers, and the
webhook-error-recovery fallback.
- `pyproject.toml`: `codespell` ignores `sting` (the artist Стинг in
`tts_dictionary.py`, not a typo of `string`).

## [1.2.3] — 2026-05-07

### Fixed
Expand Down
130 changes: 130 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# CLAUDE.md

Yandex Alice voice-skill provider for Music Assistant. Source repo for the
`yandex_alice` plugin provider that lives at
`music_assistant/providers/yandex_alice/` upstream — code is authored here
and synced to `music-assistant/server` via `ma-provider-tools`.

This file aligns with the upstream Music Assistant `CLAUDE.md` so that
provider code authored locally is shaped exactly like provider code in
the upstream tree (Sphinx docstrings, Behaviour rules, branching).

## Behaviour

- NEVER automatically reply on GitHub (PRs, issues, discussions) without
explicit consent from the developer.

## Layout

- `provider/` — plugin source (mirrored to `music_assistant/providers/yandex_alice/` on sync)
- `tests/` — pytest suite (mirrored to `tests/providers/yandex_alice/`)
- `docs/` — research notes (`NLU_RESEARCH.md`, `VOICE_UX_RESEARCH.md`, `VOICE_COMMANDS.md`); not synced
- `provider/manifest.json` — provider metadata + runtime requirements
- `pyproject.toml` — dev-time deps + lint config; not synced

## Development Commands

- `.venv/bin/python -m pytest tests/` — run all tests
- `.venv/bin/python -m pytest tests/test_dialogs.py -k <pattern>` — single file / pattern
- `.venv/bin/python -m ruff check provider/ tests/` — lint
- `.venv/bin/python -m ruff format provider/ tests/` — auto-format
- `.venv/bin/python -m mypy provider/` — type check (strict mode)
- `pre-commit run --all-files` — full pre-commit gate

Always run lint + tests + mypy before committing. Pre-commit hooks
mirror these checks plus gitleaks. CI runs `ruff format --check`, so
pushing without `ruff format` is the most common red build.

## Code Style

### Comments

Only use comments to explain complex, multi-line blocks of code. Do not
comment obvious operations.

### Docstring Format

Use Sphinx-style docstrings with `:param:` / `:returns:` / `:raises:`
syntax. For simple functions, a single-line docstring is fine.

Don't explain inner workings of the code in the docstrings (use inline
comments for that if/when needed). The docstring should provide clarity
to the **caller** of the function/method, not explain how it works
technically/internally.

```python
def my_function(param1: str, param2: int, param3: bool = False) -> str:
"""
Brief one-line description of the function.

:param param1: Description of what param1 is used for.
:param param2: Description of what param2 is used for.
:param param3: Description of what param3 is used for.
"""
```

Do **not** use Google-style (`Args:`) or bullet-style (`- param:`)
docstrings. AI assistants tend to generate Google-style by default —
explicitly steer them to Sphinx, and rewrite anything that slips
through.

### Provider style

- Match the layering of `provider/dialogs*.py`: webhook handler →
parsers (`dialogs_nlu.py`, `dialogs_control.py`, `dialogs_grammar.py`)
→ resolvers (`dialogs_player.py`). Keep network I/O in the handler;
parsers and resolvers are pure or take `mass: MusicAssistant` as a
dependency.
- Public-network inputs (URLs, hostnames, host headers) MUST go through
`is_public_https_url` from `provider/url_helpers.py` — both
`build_backend_uri` and the webhook probe rejected this in code
review (PR #3843, v1.2.2 fix). Never gate on scheme alone.
- `from __future__ import annotations` at the top of every Python file.

## Branching and PRs

- Default branch: `dev`. All work-in-progress PRs target `dev`.
- Long-lived feature branches: `feat/<topic>` (e.g. `feat/platform-integration`).
Merge to `dev` once the feature lands.
- Versioned bugfixes go through `dev` too; tags / releases happen on
`dev` after sync to upstream completes.

## Sync to upstream

`ma-provider-tools` runs the sync workflow that propagates `provider/`
and `tests/` from this repo into `music-assistant/server` under their
canonical paths. Do not edit files inside `music-assistant/server/`
directly — changes there are overwritten on the next sync.

CI in upstream `music-assistant/server` is the moderation gate; review
threads (e.g. PR #3843) drive bug fixes here, then a re-sync clears
them upstream. The CHANGELOG entries in this repo are the source of
truth for what landed.

## Debugging

- Music Assistant data: `$HOME/.musicassistant/`
- MA logs: `$HOME/.musicassistant/musicassistant.log` (current),
`musicassistant.log.1` etc. for older rotated logs
- MA database: `$HOME/.musicassistant/library.db` — query via `sqlite3`.
**Only execute SELECT queries** — never write to a live database.
- Webhook traffic during local testing: tail the MA log filtered by
`Webhook recv:` (the structured DEBUG line emitted on every Yandex
request). Bumping the dialog logger to DEBUG via
`python -m music_assistant --log-level debug` is enough.

## Other notes

- The plugin reuses Yandex Passport cookies via `ya-passport-auth` and
the `app-store-api` REST surface via `ya-dialogs-api`. Both packages
are owned by this same author; bump versions in `pyproject.toml` +
`provider/manifest.json` together.
- Tests never make live Yandex calls. Mock `aiohttp.ClientSession` per
the pattern in `tests/test_auto_create.py` if a new test needs HTTP.
- Webhook handler error handling (PR #3843 review thread): the
post-auth dispatch is wrapped in `try / except` (`_handle_webhook` →
`_handle_authenticated_request`) so a parse / dispatch error surfaces
as a Russian "что-то пошло не так" reply instead of HTTP 500 → Alice
silence. Keep this guarantee intact when modifying the handler — any
new branch should also satisfy the
`test_unexpected_inner_exception_returns_graceful_fallback` test.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.2.3
1.3.0
5 changes: 2 additions & 3 deletions provider/auth_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,9 +346,8 @@ async def perform_device_auth(
any other channel results in a popup the frontend isn't listening
for, so it never appears.

Raises:
LoginFailed: the Device Flow timed out, was rejected by
Yandex, or another Passport-level error escaped.
:raises LoginFailed: the Device Flow timed out, was rejected by
Yandex, or another Passport-level error escaped.
"""
if not session_id:
raise LoginFailed(
Expand Down
3 changes: 1 addition & 2 deletions provider/auth_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ async def cached_authenticated_session(x_token: str) -> AsyncIterator[aiohttp.Cl
``refresh_passport_cookies`` propagates so the caller can clear the
cached token and start a fresh Device Flow on the next click.

Raises:
ValueError: ``x_token`` is empty.
:raises ValueError: ``x_token`` is empty.
"""
if not x_token:
msg = "x_token is empty — cached authenticator requires an existing token"
Expand Down
2 changes: 2 additions & 0 deletions provider/auto_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@

from .auth_session import cached_authenticated_session, make_cached_authenticator
from .constants import DIALOG_CHANNEL
from .dialogs_grammar import build_grammar
from .skill_logo import load_skill_logo_bytes

if TYPE_CHECKING:
Expand Down Expand Up @@ -258,6 +259,7 @@ async def _run_pipeline(
description=description,
structured_examples=structured_examples,
activation_phrases=activation_phrases,
intents=build_grammar(),
logo_bytes=load_skill_logo_bytes(),
creator_factory=_make_logging_creator_factory(),
)
Expand Down
2 changes: 2 additions & 0 deletions provider/auto_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

from .auth_session import make_cached_authenticator
from .constants import DIALOG_CHANNEL
from .dialogs_grammar import build_grammar

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -110,6 +111,7 @@ async def run_auto_update(
description=description,
structured_examples=structured_examples,
activation_phrases=activation_phrases,
intents=build_grammar(),
voice=voice,
)
except InvalidCredentialsError as exc:
Expand Down
10 changes: 10 additions & 0 deletions provider/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@
# CONF_INSTANCE_NAME field.
CONF_USE_DIFFERENT_INSTANCE_NAME = "use_different_instance_name"

# Toggle: keep the conversation open after a play / control success (P1.4).
# Default OFF — historical voice-UX where the skill ends the session and
# the user re-says "Алиса, попроси <name>" for the next command. ON keeps
# `end_session=false` after success so follow-ups skip the activation
# preamble at the cost of a "skill is listening" indicator on screened
# surfaces. Explicit "стоп / останови / выключи / выключи музыку" still
# end the session via the existing `stop` control intent (matched by
# `parse_control` patterns in `dialogs_control.py`).
CONF_DIALOG_VOICE_CONTINUATION = "dialog_voice_continuation"

# Yandex Dialogs catalog voice options (TTS), passed to draft payload.
# Wire values + display names extracted live from the dev console
# (https://dialogs.yandex.ru/developer → skill → Голос dropdown) on
Expand Down
5 changes: 2 additions & 3 deletions provider/dialog_skill_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,8 @@ def build_backend_uri(base_url: str, webhook_secret: str) -> str:
can't reach (e.g. ``https://192.168.1.10`` or ``https://localhost``)
and the user would only discover the failure once moderation finishes.

Raises:
ValueError: ``base_url`` is empty / not a public HTTPS URL, or
``webhook_secret`` is empty.
:raises ValueError: ``base_url`` is empty / not a public HTTPS URL,
or ``webhook_secret`` is empty.
"""
base = (base_url or "").strip().rstrip("/")
if not base:
Expand Down
Loading
Loading