Skip to content

Anam avatar plugin#445

Open
dangusev wants to merge 10 commits intomainfrom
feat/anam-avatar-plugin
Open

Anam avatar plugin#445
dangusev wants to merge 10 commits intomainfrom
feat/anam-avatar-plugin

Conversation

@dangusev
Copy link
Copy Markdown
Collaborator

@dangusev dangusev commented Mar 24, 2026

An implementation of Anam avatar plugin

  • Example - see plugins/anam/example/anam_avatar_example.py
  • To run:
    • get an account at https://anam.ai/
    • Create api key and set ANAM_API_KEY with it
    • Get an ID of some avatar (not persona_id) and set ANAM_AVATAR_ID with it

Summary by CodeRabbit

  • New Features

    • Added Anam Avatar plugin for real-time synchronized avatar video/audio with configurable resolution and timeouts; available via optional extra anam and included in all-plugins.
  • Documentation

    • Added README with install instructions, env var requirements, quick-start, options, and high-level workflow.
  • Examples

    • Added runnable example demonstrating agent wiring, LLM/TTS/STT setup, and joining a call.
  • Tests

    • Added unit tests for initialization, track publishing, and agent attachment.
  • Chores

    • Registered plugin in workspace configuration.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cdf1764e-af53-4d0d-9258-d89eaec547e1

📥 Commits

Reviewing files that changed from the base of the PR and between 954edce and 7e98bd3.

📒 Files selected for processing (2)
  • plugins/anam/example/anam_avatar_example.py
  • plugins/anam/example/pyproject.toml
✅ Files skipped from review due to trivial changes (1)
  • plugins/anam/example/pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugins/anam/example/anam_avatar_example.py

📝 Walkthrough

Walkthrough

Adds a new Anam Avatar plugin package and workspace entry, implements AnamAvatarPublisher to stream agent audio/video to Anam, includes docs, an example CLI agent, tests, and exposes the plugin via optional extras. (50 words)

Changes

Cohort / File(s) Summary
Workspace & root extras
pyproject.toml, agents-core/pyproject.toml
Register vision-agents-plugins-anam as a workspace-provided package (plugins/anam) and add anam = ["vision-agents-plugins-anam"] to project.optional-dependencies; include it in the all-plugins extra.
Plugin package manifest
plugins/anam/pyproject.toml
New Hatch project manifest for vision-agents-plugins-anam with runtime deps (vision-agents, anam>=0.3.0,<0.4), VCS versioning, packaging, and dev deps.
Public API export
plugins/anam/vision_agents/plugins/anam/__init__.py
Expose AnamAvatarPublisher at package level via __all__.
Plugin implementation
plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py
Add AnamAvatarPublisher: credential handling (env/params), AnamClient lifecycle, connection/session readiness handling, background receivers for incoming video/audio frames, event subscriptions from Agent (TTS/realtime audio, turn starts), resampling and serialized outbound audio sending (24kHz mono target), interruption logic, and async shutdown.
Docs & example
plugins/anam/README.md, plugins/anam/example/anam_avatar_example.py, plugins/anam/example/pyproject.toml
Add README describing install, env vars, options, how-it-works; add example CLI script wiring Agent + LLM/TTS/STT and registering AnamAvatarPublisher; add example project manifest.
Tests
plugins/anam/tests/test_anam_plugin.py
New pytest suite: helper factory, DummyAgent, tests for init/name, missing credential errors, returned track types/resolution, and attach_agent subscriber registration.
Package example manifest
plugins/anam/pyproject.toml
Add package manifest for plugin source, build config, and dev tools. (new file)

Sequence Diagram(s)

sequenceDiagram
    participant Agent as Agent
    participant Publisher as AnamAvatarPublisher
    participant Anam as AnamClient
    participant Tracks as Audio/Video Tracks

    Agent->>Publisher: attach_agent()
    Publisher->>Publisher: subscribe to TTS & realtime audio events

    Publisher->>Anam: connect() / create session
    Anam-->>Publisher: connection_established / session_ready
    Publisher->>Tracks: start receiver tasks

    Agent->>Publisher: emit TTS/realtime audio chunk
    Publisher->>Publisher: resample to 24kHz mono (serialize)
    Publisher->>Anam: send_audio_input_chunk()

    Anam-->>Tracks: video_frames() / audio_frames()
    Tracks->>Agent: published frames delivered

    Agent->>Publisher: other participant turn starts
    Publisher->>Anam: interrupt()

    Agent->>Publisher: finish()
    Publisher->>Anam: close()
    Publisher->>Tracks: stop & cancel tasks
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

The face I borrow breathes in the dark, a small machine
that learns the cadence of the throat and imitates regret.
I feed it syllables, it returns a glassy mouth of light;
the sound comes back precise and colder than my own.
In the hush the avatar keeps the time I cannot keep.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Anam avatar plugin' accurately describes the main change—adding a new plugin for Anam avatar functionality across multiple files and directories.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/anam-avatar-plugin

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py (3)

218-221: Background tasks lack exception handling callbacks.

If _audio_receiver or _video_receiver raises an unhandled exception, it will be silently lost. Consider adding a done callback to log exceptions, similar to patterns used elsewhere in the codebase.

♻️ Suggested improvement
+    def _task_done_callback(self, task: asyncio.Task) -> None:
+        if not task.cancelled() and task.exception() is not None:
+            logger.exception(
+                "Background task failed", exc_info=task.exception()
+            )
+
     async def _connect(self) -> None:
         # ... existing code ...
         if self._audio_receiver_task is None:
             self._audio_receiver_task = asyncio.create_task(self._audio_receiver())
+            self._audio_receiver_task.add_done_callback(self._task_done_callback)
         if self._video_receiver_task is None:
             self._video_receiver_task = asyncio.create_task(self._video_receiver())
+            self._video_receiver_task.add_done_callback(self._task_done_callback)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py` around
lines 218 - 221, The background tasks created for _audio_receiver and
_video_receiver are started without done callbacks so their exceptions can be
lost; update the task creation in the block that assigns
self._audio_receiver_task and self._video_receiver_task to attach a done
callback (e.g., a helper like _log_task_exceptions or an inline callback) which
checks task.exception() and logs any exception via the existing logger, ensuring
both asyncio.create_task(self._audio_receiver()) and
asyncio.create_task(self._video_receiver()) register that callback so errors
aren’t silently dropped.

248-257: Clearing _connected event in finally may cause issues on reconnection.

If the connection succeeds but a subsequent operation fails, clearing the event in finally means a reconnection attempt would need to wait for the CONNECTION_ESTABLISHED event again, which may never fire if already connected.

Consider clearing only on timeout or explicit disconnect.

♻️ Suggested fix
     async def _wait_connected(self) -> None:
         try:
             await asyncio.wait_for(
                 self._connected.wait(), timeout=self._connect_timeout
             )
         except asyncio.TimeoutError:
             logger.error("Timed out waiting for Anam connection to be established")
+            self._connected.clear()
             raise
-        finally:
-            self._connected.clear()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py` around
lines 248 - 257, The current _wait_connected method clears the self._connected
event in a finally block which can wipe the connected flag even when connection
succeeded; change it to only clear the event on timeout or explicit disconnect:
remove the finally self._connected.clear(), move the clear into the
asyncio.TimeoutError except block (call self._connected.clear() there before
re-raising), and ensure any disconnect logic (e.g., an explicit disconnect
handler or method like _handle_disconnect/_on_disconnect) is responsible for
clearing self._connected when the connection actually drops so reconnection
waits behave correctly.

259-268: Same concern for _session_ready event clearing.

The finally block clears _session_ready unconditionally, which could cause the same reconnection issue as _wait_connected.

♻️ Suggested fix
     async def _wait_session_ready(self) -> None:
         try:
             await asyncio.wait_for(
                 self._session_ready.wait(), timeout=self._session_ready_timeout
             )
         except asyncio.TimeoutError:
             logger.error("Timed out waiting for Anam session to get ready")
+            self._session_ready.clear()
             raise
-        finally:
-            self._session_ready.clear()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py` around
lines 259 - 268, The finally block in _wait_session_ready unconditionally clears
the asyncio.Event _session_ready causing spurious reconnection behavior; change
it so the event is only cleared after a successful wait: remove the
unconditional finally clear and instead call self._session_ready.clear()
immediately after the await in _wait_session_ready (inside the try after await
completes), leaving the except asyncio.TimeoutError handler to log and re-raise
without clearing the event; reference _wait_session_ready and _session_ready to
locate the change.
plugins/anam/README.md (1)

1-3: Incomplete README documentation.

The README contains only a placeholder TODO. Consider documenting the plugin's purpose, configuration options (e.g., ANAM_API_KEY, ANAM_AVATAR_ID environment variables), and a basic usage example before merging.

Would you like me to draft documentation content based on the implementation in anam_avatar_publisher.py?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/anam/README.md` around lines 1 - 3, The README currently has a TODO;
update plugins/anam/README.md to document the plugin purpose, required
configuration and usage: explain what the Anam Avatar Plugin does, list required
environment variables (e.g., ANAM_API_KEY, ANAM_AVATAR_ID) and any optional
settings, and provide a minimal usage example showing how to invoke the
functionality exposed by anam_avatar_publisher.py (reference the main
function(s) or CLI entry in anam_avatar_publisher.py and demonstrate expected
inputs/outputs and error handling). Keep it concise, include mention of
authentication, where to find logs or returned avatar artifacts, and any runtime
prerequisites.
plugins/anam/example/anam_avatar_example.py (1)

2-2: Use modern type annotation syntax.

Per coding guidelines, prefer dict[str, T] over Dict. Also, Any is discouraged in type annotations.

♻️ Suggested fix
-from typing import Any, Dict
+from typing import Any

And update line 22:

-    async def get_weather(location: str) -> Dict[str, Any]:
+    async def get_weather(location: str) -> dict[str, object]:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/anam/example/anam_avatar_example.py` at line 2, Replace the legacy
typing imports and Any usage: remove "from typing import Any, Dict" and use the
built-in generic alias (e.g., dict[str, ...]) instead, and replace the vague Any
with a concrete type (or object if truly generic); update the type annotation
used at line 22 to use dict[str, X] (where X is a specific type like str, int,
or object) instead of Dict or Any so the code follows modern annotation syntax
and avoids Any.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugins/anam/example/anam_avatar_example.py`:
- Line 47: Fix the typo in the comment string "Have the agent join the
call/roomz" by replacing "roomz" with "room" so the comment reads "Have the
agent join the call/room"; update the comment where it appears (search for the
exact text "Have the agent join the call/roomz") to ensure correct spelling.

In `@plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py`:
- Around line 137-138: The except block currently catches Exception broadly when
calling self._exit_stack.aclose() in the close() path of AnamAvatarPublisher,
which violates the guideline to catch specific exceptions; inspect what context
managers are registered in self._exit_stack and identify the concrete exceptions
they may raise (for example: asyncio.CancelledError, OSError, RuntimeError, or
any SDK-specific exceptions from the Anam SDK) and replace the bare except
Exception with explicit except clauses for those expected exceptions (e.g.,
except asyncio.CancelledError: and except OSError: and except AnamSdkError:),
preserving the logger.warning("Failed to close Anam avatar publisher",
exc_info=True) behavior for each specific except branch and leaving other
unexpected exceptions to propagate.
- Around line 230-239: The _on_connection_closed callback currently calls await
self.close(), which can re-enter because close() triggers _exit_stack.aclose()
and fires the CONNECTION_CLOSED event again; remove the direct call to close()
from _on_connection_closed and instead update internal state (e.g., set a
_connected or _closed boolean) and log/emit any non-reentrant notifications so
the application-level lifecycle can perform shutdown; also make close()
idempotent by adding a _closed guard (check/set self._closed at start of
close()) so concurrent/duplicate calls are safe and no recursive await occurs
from _on_connection_closed or event handlers.

---

Nitpick comments:
In `@plugins/anam/example/anam_avatar_example.py`:
- Line 2: Replace the legacy typing imports and Any usage: remove "from typing
import Any, Dict" and use the built-in generic alias (e.g., dict[str, ...])
instead, and replace the vague Any with a concrete type (or object if truly
generic); update the type annotation used at line 22 to use dict[str, X] (where
X is a specific type like str, int, or object) instead of Dict or Any so the
code follows modern annotation syntax and avoids Any.

In `@plugins/anam/README.md`:
- Around line 1-3: The README currently has a TODO; update
plugins/anam/README.md to document the plugin purpose, required configuration
and usage: explain what the Anam Avatar Plugin does, list required environment
variables (e.g., ANAM_API_KEY, ANAM_AVATAR_ID) and any optional settings, and
provide a minimal usage example showing how to invoke the functionality exposed
by anam_avatar_publisher.py (reference the main function(s) or CLI entry in
anam_avatar_publisher.py and demonstrate expected inputs/outputs and error
handling). Keep it concise, include mention of authentication, where to find
logs or returned avatar artifacts, and any runtime prerequisites.

In `@plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py`:
- Around line 218-221: The background tasks created for _audio_receiver and
_video_receiver are started without done callbacks so their exceptions can be
lost; update the task creation in the block that assigns
self._audio_receiver_task and self._video_receiver_task to attach a done
callback (e.g., a helper like _log_task_exceptions or an inline callback) which
checks task.exception() and logs any exception via the existing logger, ensuring
both asyncio.create_task(self._audio_receiver()) and
asyncio.create_task(self._video_receiver()) register that callback so errors
aren’t silently dropped.
- Around line 248-257: The current _wait_connected method clears the
self._connected event in a finally block which can wipe the connected flag even
when connection succeeded; change it to only clear the event on timeout or
explicit disconnect: remove the finally self._connected.clear(), move the clear
into the asyncio.TimeoutError except block (call self._connected.clear() there
before re-raising), and ensure any disconnect logic (e.g., an explicit
disconnect handler or method like _handle_disconnect/_on_disconnect) is
responsible for clearing self._connected when the connection actually drops so
reconnection waits behave correctly.
- Around line 259-268: The finally block in _wait_session_ready unconditionally
clears the asyncio.Event _session_ready causing spurious reconnection behavior;
change it so the event is only cleared after a successful wait: remove the
unconditional finally clear and instead call self._session_ready.clear()
immediately after the await in _wait_session_ready (inside the try after await
completes), leaving the except asyncio.TimeoutError handler to log and re-raise
without clearing the event; reference _wait_session_ready and _session_ready to
locate the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f6da3e73-d7dc-4c82-9d24-97cc8dd50423

📥 Commits

Reviewing files that changed from the base of the PR and between ee9db10 and c997eb3.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • agents-core/pyproject.toml
  • plugins/anam/README.md
  • plugins/anam/example/anam_avatar_example.py
  • plugins/anam/py.typed
  • plugins/anam/pyproject.toml
  • plugins/anam/tests/test_anam_plugin.py
  • plugins/anam/vision_agents/plugins/anam/__init__.py
  • plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py
  • pyproject.toml

@dangusev dangusev force-pushed the feat/anam-avatar-plugin branch 3 times, most recently from 62bd38b to ee84704 Compare March 25, 2026 15:25
@dangusev dangusev marked this pull request as ready for review March 25, 2026 18:19
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
plugins/anam/tests/test_anam_plugin.py (1)

14-19: Consider adding type annotation to **overrides.

For completeness with the coding guidelines requiring type annotations everywhere:

♻️ Proposed refinement
-def _make_publisher(**overrides) -> AnamAvatarPublisher:
+def _make_publisher(**overrides: str | int | None) -> AnamAvatarPublisher:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/anam/tests/test_anam_plugin.py` around lines 14 - 19, Add a type
annotation for the variadic kwargs in _make_publisher: change the signature to
annotate **overrides as Dict[str, Any] (or dict[str, Any] for py3.9+) and import
Any and Dict from typing if not already present; keep the rest of the function
and the call to AnamAvatarPublisher the same so the merged kwargs remain
type-checked.
plugins/anam/example/anam_avatar_example.py (2)

18-25: Add short Google-style docstrings to public functions.

The functions setup_llm, create_agent, and join_call are public entry points lacking docstrings. Add concise Google-style docstrings with Args and Returns sections where applicable.

Proposed diff
 def setup_llm(model: str = "gemini-3.1-flash-lite-preview") -> gemini.LLM:
+    """Create an LLM client and register example weather tool.
+
+    Args:
+        model: Gemini model name.
+
+    Returns:
+        Configured Gemini LLM instance.
+    """
     llm = gemini.LLM(model)
@@
 async def create_agent(**kwargs) -> Agent:
+    """Create and configure an avatar-enabled agent.
+
+    Returns:
+        Configured agent instance.
+    """
     llm = setup_llm()
@@
 async def join_call(agent: Agent, call_type: str, call_id: str, **kwargs) -> None:
+    """Join a call and run a short starter interaction.
+
+    Args:
+        agent: Agent instance.
+        call_type: Call type.
+        call_id: Call identifier.
+    """
     call = await agent.create_call(call_type, call_id)

Also applies to lines 28-41 and 44-53.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/anam/example/anam_avatar_example.py` around lines 18 - 25, Add
concise Google-style docstrings to the public functions setup_llm, create_agent,
and join_call: for each function (setup_llm, create_agent, join_call) add a
one-line summary, an Args section listing parameters and types (e.g., model: str
for setup_llm), and a Returns section describing the return type (e.g.,
gemini.LLM for setup_llm or Agent/None as appropriate). Keep docstrings short,
placed immediately under each def, and use Google-style formatting with
parameter types and brief descriptions.

28-28: Add type annotations to variadic kwargs.

Both callback signatures accept **kwargs but lack type annotations. Per the coding guideline to "use type annotations everywhere with modern syntax," add **kwargs: object to both functions:

Proposed diff
-async def create_agent(**kwargs) -> Agent:
+async def create_agent(**kwargs: object) -> Agent:
-async def join_call(agent: Agent, call_type: str, call_id: str, **kwargs) -> None:
+async def join_call(agent: Agent, call_type: str, call_id: str, **kwargs: object) -> None:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/anam/example/anam_avatar_example.py` at line 28, The functions using
variadic keyword args (e.g., create_agent and the related callback functions in
this file) currently declare **kwargs without type annotations; update their
signatures to annotate the kwargs as **kwargs: object to comply with the
project’s modern type-annotation guideline (replace occurrences of **kwargs with
**kwargs: object in create_agent and the other callback function signatures).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugins/anam/example/anam_avatar_example.py`:
- Around line 2-3: Remove the unused legacy typing import "Any" from the import
statement "from typing import Any, Dict" and update the type annotation that
currently uses "Dict" (the dict type at the top-level annotation on line where
Dict is used) to the modern built-in generic syntax "dict[str, object]" instead
of "Dict[str, Any]"; after this change you can drop the typing import entirely
(or only import names still needed) so the module uses Python's built-in
generics and no longer relies on Any.

In `@plugins/anam/tests/test_anam_plugin.py`:
- Around line 46-49: The test test_init_custom_resolution currently only asserts
the track type; update it to also verify the custom resolution by calling
_make_publisher(width=640, height=480), obtaining track via
publish_video_track(), asserting isinstance(track, QueuedVideoTrack) and then
asserting the track's reported dimensions match 640x480 (e.g., check attributes
or a settings method such as track.width and track.height or
track.get_settings()['width']/['height'] as appropriate) so the test confirms
the resolution was applied.

---

Nitpick comments:
In `@plugins/anam/example/anam_avatar_example.py`:
- Around line 18-25: Add concise Google-style docstrings to the public functions
setup_llm, create_agent, and join_call: for each function (setup_llm,
create_agent, join_call) add a one-line summary, an Args section listing
parameters and types (e.g., model: str for setup_llm), and a Returns section
describing the return type (e.g., gemini.LLM for setup_llm or Agent/None as
appropriate). Keep docstrings short, placed immediately under each def, and use
Google-style formatting with parameter types and brief descriptions.
- Line 28: The functions using variadic keyword args (e.g., create_agent and the
related callback functions in this file) currently declare **kwargs without type
annotations; update their signatures to annotate the kwargs as **kwargs: object
to comply with the project’s modern type-annotation guideline (replace
occurrences of **kwargs with **kwargs: object in create_agent and the other
callback function signatures).

In `@plugins/anam/tests/test_anam_plugin.py`:
- Around line 14-19: Add a type annotation for the variadic kwargs in
_make_publisher: change the signature to annotate **overrides as Dict[str, Any]
(or dict[str, Any] for py3.9+) and import Any and Dict from typing if not
already present; keep the rest of the function and the call to
AnamAvatarPublisher the same so the merged kwargs remain type-checked.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7532a06b-467f-483d-bb2f-11d57f9f123e

📥 Commits

Reviewing files that changed from the base of the PR and between c997eb3 and b344830.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • agents-core/pyproject.toml
  • plugins/anam/README.md
  • plugins/anam/example/anam_avatar_example.py
  • plugins/anam/py.typed
  • plugins/anam/pyproject.toml
  • plugins/anam/tests/test_anam_plugin.py
  • plugins/anam/vision_agents/plugins/anam/__init__.py
  • plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py
  • pyproject.toml
✅ Files skipped from review due to trivial changes (4)
  • agents-core/pyproject.toml
  • plugins/anam/README.md
  • pyproject.toml
  • plugins/anam/pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugins/anam/vision_agents/plugins/anam/init.py
  • plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py

@dangusev dangusev requested review from Nash0x7E2 and aliev March 25, 2026 18:50
@dangusev dangusev force-pushed the feat/anam-avatar-plugin branch from 35dfcab to 954edce Compare March 27, 2026 14:59
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py (1)

232-239: ⚠️ Potential issue | 🟠 Major

Clear stale publisher state when the connection closes.

This callback only logs, so _real_session, _audio_input_stream, and the receiver task refs remain populated after an unexpected disconnect. Later audio/interrupt handlers will still target a dead session, and _connect() cannot establish a new one because it short-circuits once _real_session is set.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py` around
lines 232 - 239, In _on_connection_closed, clear the stale publisher state so
future reconnects and handlers don't target a dead session: reset
self._real_session to None, set self._audio_input_stream to None, and
cancel/await and clear the receiver task reference (e.g., self._receiver_task)
if it exists; do this inside the existing async _on_connection_closed method
(before/after the log as appropriate) so that _connect() will not short-circuit
on a leftover _real_session and audio/interrupt handlers won't use dead streams.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py`:
- Around line 47-48: Validate that constructor width and height are even
integers in the QueuedVideoTrack __init__ (the parameters width and height
shown) because the placeholder frame is generated from raw constructor sizes and
odd dimensions produce an odd first frame; if either dimension is not an even
positive int, raise a ValueError with a clear message (e.g., "width must be a
positive even integer") and mirror the same validation for the other
constructor(s) referenced around lines 66-68 so all entry points that create
placeholder frames enforce even dimensions; keep add_frame normalization as-is
but ensure __init__ prevents invalid sizes up front.
- Around line 220-223: The current background tasks created for
_audio_receiver() and _video_receiver() can fail silently; replace the bare
asyncio.create_task calls so failures are observed—either use the standardized
helper _run_tool_in_background(self._audio_receiver(), ...) and
_run_tool_in_background(self._video_receiver(), ...) or attach an
add_done_callback to each created task that logs exceptions and runs cleanup
(e.g., task.add_done_callback(lambda t: _log_and_cleanup_bg_task(t,
"audio_receiver"))). Ensure you reference and update the members
_audio_receiver_task and _video_receiver_task so they store the returned wrapped
task from _run_tool_in_background or the task with the done-callback attached.
- Around line 126-142: The close() method must stop the QueuedVideoTrack so its
recv() no longer serves the last frame; locate the video track instance used by
this publisher (e.g. the QueuedVideoTrack stored on the class such as
self._video_track or similar) and call its stop() before or when
cancelling/awaiting video tasks (referencing QueuedVideoTrack.recv and
self._video_receiver_task). Add a safe nil-check (if track is not None) and call
track.stop() (catch/log exceptions if needed) so downstream consumers are not
left with a frozen avatar after close().

---

Duplicate comments:
In `@plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py`:
- Around line 232-239: In _on_connection_closed, clear the stale publisher state
so future reconnects and handlers don't target a dead session: reset
self._real_session to None, set self._audio_input_stream to None, and
cancel/await and clear the receiver task reference (e.g., self._receiver_task)
if it exists; do this inside the existing async _on_connection_closed method
(before/after the log as appropriate) so that _connect() will not short-circuit
on a leftover _real_session and audio/interrupt handlers won't use dead streams.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 30062fb1-bc6f-485f-853b-7c038bfe3b12

📥 Commits

Reviewing files that changed from the base of the PR and between 35dfcab and 954edce.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • agents-core/pyproject.toml
  • plugins/anam/README.md
  • plugins/anam/example/anam_avatar_example.py
  • plugins/anam/py.typed
  • plugins/anam/pyproject.toml
  • plugins/anam/tests/test_anam_plugin.py
  • plugins/anam/vision_agents/plugins/anam/__init__.py
  • plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py
  • pyproject.toml
✅ Files skipped from review due to trivial changes (6)
  • plugins/anam/vision_agents/plugins/anam/init.py
  • pyproject.toml
  • agents-core/pyproject.toml
  • plugins/anam/pyproject.toml
  • plugins/anam/README.md
  • plugins/anam/example/anam_avatar_example.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugins/anam/tests/test_anam_plugin.py

Comment on lines +47 to +48
width: int = 1920,
height: int = 1080,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate custom video dimensions in __init__.

QueuedVideoTrack only normalizes frames passed through add_frame(). Its placeholder frame is created from the raw constructor size, so an odd custom width or height can still emit an odd first frame before avatar video arrives.

🛠️ Suggested validation
         avatar_id = avatar_id or os.getenv("ANAM_AVATAR_ID")
         if not avatar_id:
             raise ValueError("Anam avatar ID not provided")
+        if width <= 0 or height <= 0:
+            raise ValueError("width and height must be positive")
+        if width % 2 or height % 2:
+            raise ValueError("width and height must be even")

As per coding guidelines, "Raise ValueError with a descriptive message for invalid constructor arguments. Prefer custom domain exceptions over generic ones".

Also applies to: 66-68

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py` around
lines 47 - 48, Validate that constructor width and height are even integers in
the QueuedVideoTrack __init__ (the parameters width and height shown) because
the placeholder frame is generated from raw constructor sizes and odd dimensions
produce an odd first frame; if either dimension is not an even positive int,
raise a ValueError with a clear message (e.g., "width must be a positive even
integer") and mirror the same validation for the other constructor(s) referenced
around lines 66-68 so all entry points that create placeholder frames enforce
even dimensions; keep add_frame normalization as-is but ensure __init__ prevents
invalid sizes up front.

Comment on lines +126 to +142
async def close(self) -> None:
"""
Close the Anam avatar publisher, cancel audio & video processing tasks
and release resources.
"""
if self._audio_receiver_task is not None:
await cancel_and_wait(self._audio_receiver_task)
if self._video_receiver_task is not None:
await cancel_and_wait(self._video_receiver_task)

try:
await self._exit_stack.aclose()
await self._client.close()
except Exception:
logger.warning("Failed to close Anam avatar publisher", exc_info=True)
finally:
logger.debug("Anam avatar publisher closed")
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stop the video track during shutdown.

close() tears down the session, but QueuedVideoTrack.recv() keeps serving the last frame until stop() is called. Without that, downstream consumers can stay pinned on a frozen avatar after the publisher is closed.

🛠️ Suggested fix
     async def close(self) -> None:
         """
         Close the Anam avatar publisher, cancel audio & video processing tasks
         and release resources.
         """
+        self._video_track.stop()
         if self._audio_receiver_task is not None:
             await cancel_and_wait(self._audio_receiver_task)
         if self._video_receiver_task is not None:
             await cancel_and_wait(self._video_receiver_task)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py` around
lines 126 - 142, The close() method must stop the QueuedVideoTrack so its recv()
no longer serves the last frame; locate the video track instance used by this
publisher (e.g. the QueuedVideoTrack stored on the class such as
self._video_track or similar) and call its stop() before or when
cancelling/awaiting video tasks (referencing QueuedVideoTrack.recv and
self._video_receiver_task). Add a safe nil-check (if track is not None) and call
track.stop() (catch/log exceptions if needed) so downstream consumers are not
left with a frozen avatar after close().

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here @dangusev

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +220 to +223
if self._audio_receiver_task is None:
self._audio_receiver_task = asyncio.create_task(self._audio_receiver())
if self._video_receiver_task is None:
self._video_receiver_task = asyncio.create_task(self._video_receiver())
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Observe receiver task failures instead of letting them die silently.

If _audio_receiver() or _video_receiver() raises, the task just stays failed in the background and media forwarding stops with no immediate log or cleanup. Please attach a done callback or use the shared background-task pattern so failures are surfaced as soon as they happen.

Based on learnings, _run_tool_in_background is being standardized across realtime plugins specifically so add_done_callback-based exception logging handles background task failures uniformly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/anam/vision_agents/plugins/anam/anam_avatar_publisher.py` around
lines 220 - 223, The current background tasks created for _audio_receiver() and
_video_receiver() can fail silently; replace the bare asyncio.create_task calls
so failures are observed—either use the standardized helper
_run_tool_in_background(self._audio_receiver(), ...) and
_run_tool_in_background(self._video_receiver(), ...) or attach an
add_done_callback to each created task that logs exceptions and runs cleanup
(e.g., task.add_done_callback(lambda t: _log_and_cleanup_bg_task(t,
"audio_receiver"))). Ensure you reference and update the members
_audio_receiver_task and _video_receiver_task so they store the returned wrapped
task from _run_tool_in_background or the task with the done-callback attached.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dangusev any thoughts on this one?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +220 to +223
if self._audio_receiver_task is None:
self._audio_receiver_task = asyncio.create_task(self._audio_receiver())
if self._video_receiver_task is None:
self._video_receiver_task = asyncio.create_task(self._video_receiver())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dangusev any thoughts on this one?

Comment on lines +126 to +142
async def close(self) -> None:
"""
Close the Anam avatar publisher, cancel audio & video processing tasks
and release resources.
"""
if self._audio_receiver_task is not None:
await cancel_and_wait(self._audio_receiver_task)
if self._video_receiver_task is not None:
await cancel_and_wait(self._video_receiver_task)

try:
await self._exit_stack.aclose()
await self._client.close()
except Exception:
logger.warning("Failed to close Anam avatar publisher", exc_info=True)
finally:
logger.debug("Anam avatar publisher closed")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here @dangusev

@@ -0,0 +1,104 @@
# Anam Avatar Plugin for Vision Agents

Add real-time interactive avatar video to your AI agents using Anam's avatar API.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Link to their website please

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants