From eb6b4ef5c6b83c8d616a5afbf7dcbbf04a074d5a Mon Sep 17 00:00:00 2001 From: sebvanleuven Date: Tue, 17 Feb 2026 12:46:28 +0000 Subject: [PATCH 1/4] move end_sequence after TTS to main loop to make the requirement more clear --- examples/avatar_audio_passthrough.py | 1 + examples/utils.py | 1 - uv.lock | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/avatar_audio_passthrough.py b/examples/avatar_audio_passthrough.py index 663d3c8..38a470b 100644 --- a/examples/avatar_audio_passthrough.py +++ b/examples/avatar_audio_passthrough.py @@ -85,6 +85,7 @@ async def interactive_loop(session, display: VideoDisplay) -> None: AgentAudioInputConfig(encoding="pcm_s16le", sample_rate=24000, channels=1) ) await send_audio_file_chunked(agent, wav_path) + await agent.end_sequence() else: print(f"❌ File not found: {wav_file}") diff --git a/examples/utils.py b/examples/utils.py index a3827c3..94179e4 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -117,7 +117,6 @@ async def send_audio_file_chunked( # Small delay between chunks await asyncio.sleep(0.01) - await agent.end_sequence() print(f"✅ Sent {chunk_count} audio chunks from {wav_file_path.name}") diff --git a/uv.lock b/uv.lock index 1915a2e..1200a81 100644 --- a/uv.lock +++ b/uv.lock @@ -188,7 +188,7 @@ wheels = [ [[package]] name = "anam" -version = "0.2.0a2" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "aiohttp" }, From 14f303ca1e57e4e57747ea21d4d746bb83bdb8f1 Mon Sep 17 00:00:00 2001 From: sebvanleuven Date: Tue, 17 Feb 2026 12:50:51 +0000 Subject: [PATCH 2/4] add comment to make end_sequence() clear --- examples/avatar_audio_passthrough.py | 2 ++ examples/persona_interactive_video.py | 16 +++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/examples/avatar_audio_passthrough.py b/examples/avatar_audio_passthrough.py index 38a470b..0dc96f5 100644 --- a/examples/avatar_audio_passthrough.py +++ b/examples/avatar_audio_passthrough.py @@ -85,6 +85,8 @@ async def interactive_loop(session, display: VideoDisplay) -> None: AgentAudioInputConfig(encoding="pcm_s16le", sample_rate=24000, channels=1) ) await send_audio_file_chunked(agent, wav_path) + # When TTS audio is finished, signal end of sequence to the backend + # to let the avatar go back to listening mode await agent.end_sequence() else: print(f"❌ File not found: {wav_file}") diff --git a/examples/persona_interactive_video.py b/examples/persona_interactive_video.py index f4fe615..86f8b1e 100644 --- a/examples/persona_interactive_video.py +++ b/examples/persona_interactive_video.py @@ -18,7 +18,7 @@ export ANAM_AVATAR_ID="your-avatar-id" export ANAM_VOICE_ID="your-voice-id" export ANAM_LLM_ID="your-llm-id" - export ANAM_AVATAR_MODEL="model-name" # optional, e.g. "cara-3" + export ANAM_AVATAR_MODEL="model-name" # optional, e.g. "cara-3" uv run --extra display python examples/persona_interactive_video.py """ @@ -79,7 +79,9 @@ async def interactive_loop(session, display: VideoDisplay) -> None: print("Available commands:") print(" m - Send text message (user input for the conversation.)") print(" t - Send talk command (bypasses LLM and sends text to TTS) using REST API)") - print(" ts - Send talk stream (bypasses LLM and sends text to TTS) using WebSocket (lower latency)") + print( + " ts - Send talk stream (bypasses LLM and sends text to TTS) using WebSocket (lower latency)" + ) print(" i - Interrupt current audio") print(" c - Toggle live captions. Default: disabled") print(" h - Toggle conversation history at session end. Default: disabled.") @@ -128,7 +130,9 @@ async def interactive_loop(session, display: VideoDisplay) -> None: elif command == "t" or command == "ts": # Get the rest of the input as the talk (stream) command if len(parts) < 2: - print("❌ Please provide talk (stream) command. Usage: t|ts ") + print( + "❌ Please provide talk (stream) command. Usage: t|ts " + ) continue message_text = " ".join(parts[1:]) try: @@ -281,12 +285,14 @@ def main() -> None: avatar_id = os.environ.get("ANAM_AVATAR_ID", "").strip().strip('"') voice_id = os.environ.get("ANAM_VOICE_ID", "").strip().strip('"') avatar_model = os.environ.get("ANAM_AVATAR_MODEL") - llm_id = os.environ.get("ANAM_LLM_ID","").strip().strip('"') + llm_id = os.environ.get("ANAM_LLM_ID", "").strip().strip('"') api_base_url = os.environ.get("ANAM_API_BASE_URL", "https://api.anam.ai").strip().strip('"') if not api_key or not avatar_id or not voice_id: # These are required for an ephemeral persona configuration. - raise ValueError("Set ANAM_API_KEY, ANAM_AVATAR_ID, ANAM_LLM_ID and ANAM_VOICE_ID environment variables") + raise ValueError( + "Set ANAM_API_KEY, ANAM_AVATAR_ID, ANAM_LLM_ID and ANAM_VOICE_ID environment variables" + ) system_prompt = "You are a helpful and creative assistant. Respond in a conversational tone with short sentences and do not use special characters or emojis. Start you first message with 'Hello developer, Welcome to Anam. What can I help you with today?'" From 63a6f2160d406a8cf03e5d1584b7cc2a43013174 Mon Sep 17 00:00:00 2001 From: sebvanleuven Date: Wed, 18 Feb 2026 13:56:29 +0000 Subject: [PATCH 3/4] add sessionOptions to allow disabling session recordings --- src/anam/__init__.py | 4 ++++ src/anam/_api.py | 14 ++++++-------- src/anam/client.py | 8 ++++++-- src/anam/types.py | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/anam/__init__.py b/src/anam/__init__.py index 8a9d7bb..fe42d13 100644 --- a/src/anam/__init__.py +++ b/src/anam/__init__.py @@ -62,6 +62,8 @@ async def consume_audio(): MessageRole, MessageStreamEvent, PersonaConfig, + SessionOptions, + SessionReplayOptions, ) __all__ = [ @@ -79,6 +81,8 @@ async def consume_audio(): "MessageRole", "MessageStreamEvent", "PersonaConfig", + "SessionOptions", + "SessionReplayOptions", "VideoFrame", # Errors "AnamError", diff --git a/src/anam/_api.py b/src/anam/_api.py index 07e935b..5086324 100644 --- a/src/anam/_api.py +++ b/src/anam/_api.py @@ -7,7 +7,7 @@ from ._version import __version__ from .errors import AnamError, AuthenticationError, ErrorCode, SessionError -from .types import ClientOptions, PersonaConfig, SessionInfo +from .types import ClientOptions, PersonaConfig, SessionInfo, SessionOptions logger = logging.getLogger(__name__) @@ -39,12 +39,12 @@ def _api_url(self) -> str: """Get the full API URL.""" return f"{self._base_url}/{self._api_version}" - async def get_session_token(self, persona_config: PersonaConfig) -> str: + async def get_session_token(self, persona_config: PersonaConfig, session_options: SessionOptions) -> str: """Get a session token using the API key. Args: persona_config: The persona configuration to use. - + session_options: Session options (optional). Returns: The session token string. @@ -62,6 +62,7 @@ async def get_session_token(self, persona_config: PersonaConfig) -> str: body = { "clientLabel": client_label, "personaConfig": persona_config.to_dict(), + "sessionOptions": session_options.to_dict(), } logger.debug("Requesting session token from %s", url) @@ -98,7 +99,7 @@ async def get_session_token(self, persona_config: PersonaConfig) -> str: async def start_session( self, persona_config: PersonaConfig, - session_options: dict[str, Any] | None = None, + session_options: SessionOptions, ) -> SessionInfo: """Start a new streaming session. @@ -114,7 +115,7 @@ async def start_session( """ # Get session token if we don't have one if not self._session_token: - await self.get_session_token(persona_config) + await self.get_session_token(persona_config, session_options) url = f"{self._api_url}/engine/session" headers = { @@ -122,11 +123,8 @@ async def start_session( "Authorization": f"Bearer {self._session_token}", } body: dict[str, Any] = { - "personaConfig": persona_config.to_dict(), "clientMetadata": CLIENT_METADATA, } - if session_options: - body["sessionOptions"] = session_options logger.debug("Starting session at %s", url) diff --git a/src/anam/client.py b/src/anam/client.py index e9a64b0..0a9ccab 100644 --- a/src/anam/client.py +++ b/src/anam/client.py @@ -24,6 +24,7 @@ MessageStreamEvent, PersonaConfig, SessionInfo, + SessionOptions, ) logger = logging.getLogger(__name__) @@ -215,15 +216,17 @@ def connect(self) -> "_SessionContextManager": """ return _SessionContextManager(self) - async def connect_async(self) -> "Session": + async def connect_async(self, session_options: SessionOptions = SessionOptions()) -> "Session": """Connect to Anam and start streaming (without context manager). + Args: + session_options: Session options (default: SessionOptions(enable_session_replay=True)). + Returns: A Session object for interacting with the avatar. Note: You must call session.close() when done. - Prefer using `async with client.connect()` instead. """ if self.is_streaming: raise SessionError("Already connected. Call close() first.") @@ -238,6 +241,7 @@ async def connect_async(self) -> "Session": self._session_info = await self._api_client.start_session( persona_config=self._persona_config, + session_options=session_options, ) # Create streaming client with callbacks diff --git a/src/anam/types.py b/src/anam/types.py index efe897a..1206252 100644 --- a/src/anam/types.py +++ b/src/anam/types.py @@ -100,6 +100,39 @@ def to_dict(self) -> dict[str, Any]: result["enableAudioPassthrough"] = self.enable_audio_passthrough return result +@dataclass +class SessionReplayOptions: + """Session replay options. Maps to anam-lab sessionReplay schema. + + Args: + enable_session_replay: If True (default), session is recorded. Set False to disable. + """ + + enable_session_replay: bool = True + + def to_dict(self) -> dict[str, Any]: + return {"enableSessionReplay": self.enable_session_replay} + + +@dataclass +class SessionOptions: + """Configuration for an Anam session. + + Args: + enable_session_replay: If True (default), session is recorded. Set False to disable. + """ + + enable_session_replay: bool = True + + def __post_init__(self) -> None: + self._session_replay = SessionReplayOptions( + enable_session_replay=self.enable_session_replay + ) + + def to_dict(self) -> dict[str, Any]: + result: dict[str, Any] = {} + result["sessionReplay"] = self._session_replay.to_dict() + return result @dataclass class ClientOptions: From ae68287e17e0eaddb5b1c46c8a35163d4454ea80 Mon Sep 17 00:00:00 2001 From: sebvanleuven Date: Wed, 18 Feb 2026 14:36:15 +0000 Subject: [PATCH 4/4] linter split lines --- src/anam/_api.py | 4 +++- src/anam/types.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/anam/_api.py b/src/anam/_api.py index 5086324..c3c05ba 100644 --- a/src/anam/_api.py +++ b/src/anam/_api.py @@ -39,7 +39,9 @@ def _api_url(self) -> str: """Get the full API URL.""" return f"{self._base_url}/{self._api_version}" - async def get_session_token(self, persona_config: PersonaConfig, session_options: SessionOptions) -> str: + async def get_session_token( + self, persona_config: PersonaConfig, session_options: SessionOptions + ) -> str: """Get a session token using the API key. Args: diff --git a/src/anam/types.py b/src/anam/types.py index 1206252..6ab936d 100644 --- a/src/anam/types.py +++ b/src/anam/types.py @@ -100,6 +100,7 @@ def to_dict(self) -> dict[str, Any]: result["enableAudioPassthrough"] = self.enable_audio_passthrough return result + @dataclass class SessionReplayOptions: """Session replay options. Maps to anam-lab sessionReplay schema. @@ -134,6 +135,7 @@ def to_dict(self) -> dict[str, Any]: result["sessionReplay"] = self._session_replay.to_dict() return result + @dataclass class ClientOptions: """Optional configuration for AnamClient.