/
/
/
1"""
2Trimmed response dataclasses used in tool replies.
3
4Tools that need to return a Music Assistant entity use these light-weight shapes
5to keep payloads small for LLM context windows. Resources, by contrast, return
6the full ``music_assistant_models`` types directly because clients usually
7expect a complete object when they fetch a URI.
8"""
9
10from __future__ import annotations
11
12from dataclasses import dataclass, field
13from typing import Any
14
15
16@dataclass
17class TrackBrief:
18 """A track summary for tool responses."""
19
20 uri: str
21 name: str
22 artists: list[str] = field(default_factory=list)
23 album: str | None = None
24 duration: int | None = None
25 disc_number: int | None = None
26 track_number: int | None = None
27
28
29@dataclass
30class AlbumTracksResult:
31 """An album summary plus its track listing in disc/track order."""
32
33 album: AlbumBrief
34 tracks: list[TrackBrief] = field(default_factory=list)
35
36
37@dataclass
38class ArtistAlbumsResult:
39 """An artist summary plus their album discography."""
40
41 artist: ArtistBrief
42 albums: list[AlbumBrief] = field(default_factory=list)
43
44
45@dataclass
46class AlbumBrief:
47 """An album summary for tool responses."""
48
49 uri: str
50 name: str
51 artist: str | None = None
52 year: int | None = None
53
54
55@dataclass
56class ArtistBrief:
57 """An artist summary for tool responses."""
58
59 uri: str
60 name: str
61
62
63@dataclass
64class PlaylistBrief:
65 """A playlist summary for tool responses."""
66
67 uri: str
68 name: str
69 track_count: int | None = None
70 owner: str | None = None
71
72
73@dataclass
74class RadioBrief:
75 """A radio summary for tool responses."""
76
77 uri: str
78 name: str
79 description: str | None = None
80
81
82@dataclass
83class PlayerBrief:
84 """A player summary for tool responses."""
85
86 player_id: str
87 name: str
88 state: str
89 volume_level: int | None = None
90 powered: bool = True
91 current_item: str | None = None
92 available: bool = True
93 enabled: bool = True
94 needs_setup: bool = False
95 active_group: str | None = None
96 synced_to: str | None = None
97 volume_muted: bool | None = None
98 group_volume: int | None = None
99 group_volume_muted: bool | None = None
100 external_source: str | None = None
101
102
103@dataclass
104class QueueItemBrief:
105 """A queue item summary."""
106
107 item_id: str
108 name: str
109 index: int
110 duration: int | None = None
111 artists: list[str] = field(default_factory=list)
112
113
114@dataclass
115class QueueBrief:
116 """
117 A queue summary for tool responses.
118
119 ``item_count`` is ``None`` when the upstream queue object exposes neither
120 a canonical total nor an items-count field â better to say "unknown"
121 than to silently return the truncated lookahead length, which would
122 under-report a non-empty queue as ``0``.
123 """
124
125 queue_id: str
126 current_index: int | None
127 item_count: int | None
128 shuffle: bool
129 repeat: str
130 items: list[QueueItemBrief] = field(default_factory=list)
131 available: bool = True
132 index_in_buffer: int | None = None
133 next_insertable_index: int | None = None
134 items_start_index: int = 0
135
136
137@dataclass
138class RemoveFromQueueResult:
139 """
140 Per-item outcome of a ``remove_item`` call.
141
142 Every requested ``item_id`` lands in exactly one bucket, so the caller
143 always learns the fate of the full batch â including rows deleted before
144 a later id turned out to be stale.
145 """
146
147 removed: list[str] = field(default_factory=list)
148 skipped_played: list[str] = field(default_factory=list)
149 skipped_buffered: list[str] = field(default_factory=list)
150 not_found: list[str] = field(default_factory=list)
151
152
153@dataclass
154class AddToQueueResult:
155 """Confirmation of a successful ``add_to_queue`` call."""
156
157 item_id: str
158 uri: str
159 name: str
160 option: str
161 index: int | None = None
162
163
164@dataclass
165class RecommendationFolderBrief:
166 """One curated recommendation row (e.g. "Mood: Focus"), without its items."""
167
168 name: str
169 provider: str
170 item_id: str
171
172
173@dataclass
174class RecommendationItemBrief:
175 """One item inside a recommendation row."""
176
177 uri: str
178 name: str
179 media_type: str | None = None
180
181
182# ---- Debug namespace response dataclasses (spec 0005) ----
183
184
185@dataclass(frozen=True, kw_only=True)
186class PlayerInspect:
187 """Raw mirror of a Player dataclass with state.* surfaced separately."""
188
189 player_id: str
190 raw: dict[str, Any]
191 state: dict[str, Any]
192 truncated: bool
193
194
195@dataclass(frozen=True, kw_only=True)
196class QueueInspect:
197 """Raw mirror of a PlayerQueue with current_item resolved."""
198
199 queue_id: str
200 raw: dict[str, Any]
201 current_item: dict[str, Any] | None
202 truncated: bool
203
204
205@dataclass(frozen=True, kw_only=True)
206class ProviderInspect:
207 """Raw mirror of a runtime Provider object + its manifest."""
208
209 instance_id: str
210 raw: dict[str, Any]
211 manifest: dict[str, Any]
212 truncated: bool
213
214
215@dataclass(frozen=True, kw_only=True)
216class LogLine:
217 """One parsed record from musicassistant.log (continuation lines joined into message)."""
218
219 timestamp: str | None
220 level: str | None
221 component: str | None
222 message: str
223
224
225@dataclass(frozen=True, kw_only=True)
226class LogTailResult:
227 """Result of debug_tail_log."""
228
229 log_path: str
230 lines: list[LogLine]
231 bytes_scanned: int
232 truncated: bool
233 has_more: bool = False
234 response_truncated: bool = False
235 next_call_hint: str | None = None
236
237
238@dataclass(frozen=True, kw_only=True)
239class ComponentCount:
240 """Record count for one log component."""
241
242 component: str
243 count: int
244
245
246@dataclass(frozen=True, kw_only=True)
247class LogStatsResult:
248 """Result of debug_log_stats."""
249
250 log_path: str
251 window_seconds: int | None
252 total_records: int
253 level_counts: dict[str, int]
254 top_components: list[ComponentCount]
255 first_timestamp: str | None
256 last_timestamp: str | None
257 bytes_scanned: int
258 truncated: bool
259
260
261@dataclass(frozen=True, kw_only=True)
262class EventRecord:
263 """One MA event captured into the ring buffer."""
264
265 timestamp: str
266 event_type: str
267 object_id: str | None
268 data: Any
269
270
271@dataclass(frozen=True, kw_only=True)
272class EventSnapshot:
273 """Result of debug_recent_events."""
274
275 events: list[EventRecord]
276 buffer_capacity: int
277 total_seen: int
278
279
280@dataclass(frozen=True, kw_only=True)
281class EventBufferStats:
282 """Result of debug_event_buffer_stats."""
283
284 capacity: int
285 current_size: int
286 total_seen: int
287 dropped: int
288 subscribed_since: str | None
289 by_type: dict[str, int]
290
291
292@dataclass(frozen=True, kw_only=True)
293class ProviderSummary:
294 """One row of debug_list_providers."""
295
296 instance_id: str
297 domain: str
298 type: str
299 name: str
300 available: bool
301 last_error: str | None
302
303
304@dataclass(frozen=True, kw_only=True)
305class ProviderList:
306 """Result of debug_list_providers."""
307
308 providers: list[ProviderSummary]
309
310
311@dataclass(frozen=True, kw_only=True)
312class ConfigValueDump:
313 """One value from a provider ConfigEntry dump (SECURE_STRING already masked upstream)."""
314
315 key: str
316 type: str
317 value: Any
318
319
320@dataclass(frozen=True, kw_only=True)
321class ProviderConfigDump:
322 """Result of debug_inspect_provider_config."""
323
324 instance_id: str
325 domain: str
326 values: list[ConfigValueDump]
327 truncated: bool
328
329
330@dataclass(frozen=True, kw_only=True)
331class RouteEntry:
332 """One row of debug_list_webserver_routes."""
333
334 method: str
335 path: str
336 registered_by: str | None
337
338
339@dataclass(frozen=True, kw_only=True)
340class RouteList:
341 """Result of debug_list_webserver_routes."""
342
343 routes: list[RouteEntry]
344
345
346@dataclass(frozen=True, kw_only=True)
347class PackageVersions:
348 """Result of debug_list_package_versions."""
349
350 packages: dict[str, str]
351
352
353@dataclass(frozen=True, kw_only=True)
354class ReloadResult:
355 """Result of debug_reload_provider."""
356
357 instance_id: str
358 duration_ms: float
359 new_available: bool
360 last_error: str | None
361
362
363@dataclass(frozen=True, kw_only=True)
364class HealthSummary:
365 """Result of debug_health_summary â the LLM agent's triage entry point."""
366
367 providers_loaded: int
368 providers_disabled: int
369 providers_error: int
370 providers_error_details: list[ProviderSummary]
371 queues_total: int
372 queues_with_active_playback: int
373 queues_with_errors: int
374 events_per_min_by_type: dict[str, float] | None
375 log_errors_last_5min: int | None
376 disabled_capabilities: list[str]
377
378
379# ---- Config namespace response dataclasses (spec 0006) ----
380
381
382@dataclass(frozen=True, kw_only=True)
383class ConfigTarget:
384 """One configurable target (provider, core controller, or player)."""
385
386 target_type: str
387 target_id: str
388 domain: str
389 name: str
390 enabled: bool
391
392
393@dataclass(frozen=True, kw_only=True)
394class ConfigTargetList:
395 """Result of config_list_targets."""
396
397 providers: list[ConfigTarget]
398 core: list[ConfigTarget]
399 players: list[ConfigTarget]
400
401
402@dataclass(frozen=True, kw_only=True)
403class CoreConfigDump:
404 """Result of config_get_core."""
405
406 domain: str
407 values: list[ConfigValueDump]
408 truncated: bool
409
410
411@dataclass(frozen=True, kw_only=True)
412class PlayerConfigDump:
413 """Result of config_get_player."""
414
415 player_id: str
416 provider: str
417 values: list[ConfigValueDump]
418 truncated: bool
419
420
421@dataclass(frozen=True, kw_only=True)
422class ConfigEntryDump:
423 """One editable ConfigEntry definition + current value."""
424
425 key: str
426 type: str
427 label: str | None
428 default_value: Any
429 required: bool
430 description: str | None
431 options: list[Any] | None
432 range: tuple[int, int] | None
433 advanced: bool
434 hidden: bool
435 requires_reload: bool
436 depends_on: str | None
437 action: str | None
438 current_value: Any
439
440
441@dataclass(frozen=True, kw_only=True)
442class ConfigEntryList:
443 """Result of config_get_entries."""
444
445 target_type: str
446 target_id: str
447 entries: list[ConfigEntryDump]
448 truncated: bool
449
450
451@dataclass(frozen=True, kw_only=True)
452class DSPConfigDump:
453 """Result of config_get_dsp â mirrors music_assistant_models.dsp.DSPConfig."""
454
455 player_id: str
456 enabled: bool
457 input_gain: float
458 output_gain: float
459 filters: list[dict[str, Any]]
460
461
462@dataclass(frozen=True, kw_only=True)
463class ValueChange:
464 """One key's before/after in a config diff (secrets masked both sides)."""
465
466 key: str
467 before: Any
468 after: Any
469 secret: bool
470
471
472@dataclass(frozen=True, kw_only=True)
473class DiffResult:
474 """A dry-run config diff."""
475
476 target_type: str
477 target_id: str
478 changes: list[ValueChange]
479
480
481@dataclass(frozen=True, kw_only=True)
482class SetValueResult:
483 """Result of config_set_*_value."""
484
485 target_type: str
486 target_id: str
487 key: str
488 applied: bool
489 requires_reload: bool
490 audit_log_id: str
491 diff: DiffResult | None
492
493
494@dataclass(frozen=True, kw_only=True)
495class SaveResult:
496 """Result of config_save_* (bulk)."""
497
498 target_type: str
499 target_id: str
500 applied: bool
501 changes: list[ValueChange]
502 requires_reload: bool
503 audit_log_id: str
504 diff: DiffResult | None
505
506
507@dataclass(frozen=True, kw_only=True)
508class ActionResult:
509 """Result of config_trigger_provider_action."""
510
511 instance_id: str
512 action_key: str
513 new_entries: list[ConfigEntryDump]
514 extra_data: dict[str, Any]
515 audit_log_id: str
516