-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add director notes support (initial config + runtime cues) #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -3,7 +3,9 @@ | |||||
| from __future__ import annotations | ||||||
|
|
||||||
| import asyncio | ||||||
| import json | ||||||
| import logging | ||||||
| import math | ||||||
| import uuid | ||||||
| from collections.abc import AsyncIterator | ||||||
| from typing import Any, Awaitable, Callable, TypeVar | ||||||
|
|
@@ -531,9 +533,6 @@ async def send_message(self, content: str) -> None: | |||||
| Raises: | ||||||
| SessionError: If not connected or if LLM is not available. | ||||||
| """ | ||||||
| if not self._client._streaming_client: | ||||||
| raise SessionError("Not connected") | ||||||
|
|
||||||
| # Validate that LLM is available for processing messages | ||||||
| persona_config = self._get_persona_config() | ||||||
|
|
||||||
|
|
@@ -545,13 +544,7 @@ async def send_message(self, content: str) -> None: | |||||
| "Persona ID and LLM ID are not set, messages will not be processed by the backend." | ||||||
| ) | ||||||
|
|
||||||
| # Wait for data channel to be ready | ||||||
| streaming = self._client._streaming_client | ||||||
| if not getattr(streaming, "_data_channel_open", False): | ||||||
| logger.debug("Waiting for data channel to open...") | ||||||
| if not await streaming.wait_for_data_channel(timeout=10.0): | ||||||
| raise SessionError("Data channel did not open in time") | ||||||
|
|
||||||
| streaming = await self._ensure_data_channel_open() | ||||||
| streaming.send_user_message(content) | ||||||
|
|
||||||
| async def interrupt(self) -> None: | ||||||
|
|
@@ -560,17 +553,57 @@ async def interrupt(self) -> None: | |||||
| Raises: | ||||||
| SessionError: If not connected. | ||||||
| """ | ||||||
| streaming = await self._ensure_data_channel_open() | ||||||
| streaming.send_interrupt() | ||||||
|
|
||||||
| async def send_director_note_cue( | ||||||
| self, | ||||||
| tag: str, | ||||||
| *, | ||||||
| at_seconds: float | None = None, | ||||||
| in_seconds: float | None = None, | ||||||
| ) -> None: | ||||||
| """Send a director-note cue over the active session data channel. | ||||||
|
|
||||||
| Args: | ||||||
| tag: Director-note cue tag (for example ``"curious"``). | ||||||
| at_seconds: Optional turn-relative cue time in seconds. | ||||||
| in_seconds: Optional delay relative to receipt time in seconds. | ||||||
|
|
||||||
| Raises: | ||||||
| SessionError: If not connected, data channel is unavailable, or send fails. | ||||||
| ValueError: If a provided timing value is not finite. | ||||||
| """ | ||||||
| for field_name, value in (("at_seconds", at_seconds), ("in_seconds", in_seconds)): | ||||||
| if value is not None and not math.isfinite(value): | ||||||
| raise ValueError(f"{field_name} must be a finite number") | ||||||
|
|
||||||
| payload: dict[str, Any] = { | ||||||
| "message_type": "director_note_cue", | ||||||
| "cue": {"tag": tag}, | ||||||
| } | ||||||
|
|
||||||
| if at_seconds is not None: | ||||||
| payload["at_seconds"] = at_seconds | ||||||
|
|
||||||
| if in_seconds is not None: | ||||||
| payload["in_seconds"] = in_seconds | ||||||
|
|
||||||
| streaming = await self._ensure_data_channel_open() | ||||||
| if not streaming.send_data_message(json.dumps(payload)): | ||||||
| raise SessionError("Failed to send director note cue over data channel") | ||||||
|
|
||||||
| async def _ensure_data_channel_open(self): | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Static type checking will fail on this new helper because Prompt for AI agents
Suggested change
|
||||||
| """Return the streaming client once the data channel is ready.""" | ||||||
| if not self._client._streaming_client: | ||||||
| raise SessionError("Not connected") | ||||||
|
|
||||||
| # Wait for data channel to be ready (same as send_message) | ||||||
| streaming = self._client._streaming_client | ||||||
| if not getattr(streaming, "_data_channel_open", False): | ||||||
| logger.debug("Waiting for data channel to open...") | ||||||
| if not await streaming.wait_for_data_channel(timeout=10.0): | ||||||
| raise SessionError("Data channel did not open in time") | ||||||
|
|
||||||
| streaming.send_interrupt() | ||||||
| return streaming | ||||||
|
|
||||||
| def create_talk_stream(self, correlation_id: str | None = None) -> TalkMessageStream: | ||||||
| """Create a talk message stream for sending text chunks to TTS. | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,6 +47,34 @@ class MessageRole(str, Enum): | |
| SYSTEM = "system" | ||
|
|
||
|
|
||
| @dataclass | ||
| class DirectorNotes: | ||
| """Director-notes settings applied at session start via ``PersonaConfig``. | ||
|
|
||
| These fields are optional and serialized only when set. | ||
| """ | ||
|
|
||
| custom_style_prompt: str | None = None | ||
| preset_style: str | None = None | ||
| idling_prompt: str | None = None | ||
| overlay_model_name_on_output: bool | None = None | ||
| expressivity: float | None = None | ||
|
|
||
| def to_dict(self) -> dict[str, Any]: | ||
| result: dict[str, Any] = {} | ||
| if self.custom_style_prompt is not None: | ||
| result["customStylePrompt"] = self.custom_style_prompt | ||
| if self.preset_style is not None: | ||
| result["presetStyle"] = self.preset_style | ||
| if self.idling_prompt is not None: | ||
| result["idlingPrompt"] = self.idling_prompt | ||
| if self.overlay_model_name_on_output is not None: | ||
| result["overlayModelNameOnOutput"] = self.overlay_model_name_on_output | ||
| if self.expressivity is not None: | ||
| result["expressivity"] = self.expressivity | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Session startup can still send invalid JSON when Prompt for AI agents |
||
| return result | ||
|
|
||
|
|
||
| @dataclass | ||
| class PersonaConfig: | ||
| """Configuration for an Anam persona. | ||
|
|
@@ -62,6 +90,7 @@ class PersonaConfig: | |
| llm_id: LLM model to use (optional). Set to 'CUSTOMER_CLIENT_V1' to disable | ||
| Anam's default brain for custom LLM integration. | ||
| max_session_length_seconds: Maximum session duration (optional). | ||
| director_notes: Optional director-notes defaults applied at session start. | ||
| enable_audio_passthrough: If True, bypasses Anam's orchestration layer | ||
| and allows to ingest TTS audio directly through the socket. | ||
| """ | ||
|
|
@@ -75,6 +104,7 @@ class PersonaConfig: | |
| language_code: str | None = None | ||
| llm_id: str | None = None | ||
| max_session_length_seconds: int | None = None | ||
| director_notes: DirectorNotes | None = None | ||
| enable_audio_passthrough: bool | None = False | ||
|
|
||
| def to_dict(self) -> dict[str, Any]: | ||
|
|
@@ -98,6 +128,10 @@ def to_dict(self) -> dict[str, Any]: | |
| result["llmId"] = self.llm_id | ||
| if self.max_session_length_seconds is not None: | ||
| result["maxSessionLengthSeconds"] = self.max_session_length_seconds | ||
| if self.director_notes is not None: | ||
| director_notes = self.director_notes.to_dict() | ||
| if director_notes: | ||
| result["directorNotes"] = director_notes | ||
| if self.enable_audio_passthrough is not None: | ||
| result["enableAudioPassthrough"] = self.enable_audio_passthrough | ||
| return result | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: The markdown heading for
Runtime Director Note Cuesuses#####(h5), but since it's a peer section to#### Initial Director Notes, it should use####(h4). Using#####makes it appear as a sub-subsection ofInitialrather than a sibling section under### Director Notes.Prompt for AI agents