Skip to content

Conversation

@lebaudantoine
Copy link
Collaborator

/!\ not ready for merge, example of my researches for @GuittenyMartin.

The agent outputs the following data structure :

{
  "events": [
    {
      "participant_id": "da8d39ff-3b1c-4e8d-9a70-c630c9871bcb",
      "type": "participant_connected",
      "timestamp": "2025-10-09T21:52:33.969950"
    },
    {
      "participant_id": "da8d39ff-3b1c-4e8d-9a70-c630c9871bcb",
      "type": "speech_start",
      "timestamp": "2025-10-09T21:52:36.039456"
    },
    {
      "participant_id": "da8d39ff-3b1c-4e8d-9a70-c630c9871bcb",
      "type": "speech_end",
      "timestamp": "2025-10-09T21:52:36.589114"
    },
    {
      "participant_id": "da8d39ff-3b1c-4e8d-9a70-c630c9871bcb",
      "type": "speech_start",
      "timestamp": "2025-10-09T21:52:38.887518"
    },
    {
      "participant_id": "da8d39ff-3b1c-4e8d-9a70-c630c9871bcb",
      "type": "speech_end",
      "timestamp": "2025-10-09T21:52:39.438141"
    },
    {
      "participant_id": "da8d39ff-3b1c-4e8d-9a70-c630c9871bcb",
      "type": "participant_disconnected",
      "timestamp": "2025-10-09T21:52:43.223255"
    }
  ],
  "participants": [
    {
      "participantId": "da8d39ff-3b1c-4e8d-9a70-c630c9871bcb",
      "name": "antoine lebaud"
    }
  ]
}

We could extend this data structure to capture even more event or other types of data.
Ex : when someone mutes its mic., raises hand, send a chat message.

@lebaudantoine
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Oct 9, 2025

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link

coderabbitai bot commented Oct 9, 2025

Walkthrough

Adds a new Python agent (src/agents/metadata_extractor.py) implementing per-participant VAD (VADAgent), event model (MetadataEvent), orchestration and persistence to S3-compatible storage (MetadataAgent), plus entrypoint and prewarm. Adds minio==7.2.15 to src/agents/pyproject.toml. Updates Helm values to provide AWS S3-compatible env vars, run the metadata_extractor start command, and change the SSL certificate mount path. Adjusts agents Dockerfile CMD to invoke multi_user_transcriber.py.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Pre-merge checks

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title “Metadata agent” succinctly reflects the main change of introducing a metadata extraction agent without unnecessary details or noise, and aligns with the primary focus of the pull request.
Description Check ✅ Passed The description illustrates the output JSON structure produced by the new metadata agent and notes potential extensions, directly reflecting the changes introduced in the pull request.
Docstring Coverage ✅ Passed Docstring coverage is 80.95% which is sufficient. The required threshold is 80.00%.

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

@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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4353db4 and 87dd081.

📒 Files selected for processing (4)
  • src/agents/Dockerfile (1 hunks)
  • src/agents/metadata_extractor.py (1 hunks)
  • src/agents/pyproject.toml (1 hunks)
  • src/helm/env.d/dev-keycloak/values.meet.yaml.gotmpl (1 hunks)

Comment on lines 36 to 37
AGENT_NAME = os.getenv("ROOM_METADATA_AGENT_NAME", "metadata-extractor")

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove unused AGENT_NAME constant.

Line 36 defines AGENT_NAME, but it is never referenced. Please drop it to avoid shipping dead code.

🤖 Prompt for AI Agents
In src/agents/metadata_extractor.py around lines 36 to 37, remove the unused
AGENT_NAME constant declaration (AGENT_NAME =
os.getenv("ROOM_METADATA_AGENT_NAME", "metadata-extractor")) to eliminate dead
code; ensure no other references exist to that name (search file/project) before
deleting, then run tests/lint to confirm there are no remaining usages or
import-order changes required.

Comment on lines 96 to 107
def __init__(self, ctx: JobContext):
"""Initialize metadata agent."""
self.minio_client = Minio(
endpoint=os.getenv("AWS_S3_ENDPOINT_URL"),
access_key=os.getenv("AWS_S3_ACCESS_KEY_ID"),
secret_key=os.getenv("AWS_S3_SECRET_ACCESS_KEY"),
secure=os.getenv("AWS_S3_SECURE_ACCESS", "False").lower() == "true",
)

# todo - raise error if none
self.bucket_name = os.getenv("AWS_STORAGE_BUCKET_NAME")

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Raise when S3 endpoint or bucket env vars are missing.

Lines 98-107 build the MinIO client before verifying the required configuration. If AWS_S3_ENDPOINT_URL or AWS_STORAGE_BUCKET_NAME is absent (the TODO on Line 105), the agent survives initialization but crashes at shutdown when save() calls put_object, making the failure harder to diagnose. Fail fast by validating these env vars upfront.

-        self.minio_client = Minio(
-            endpoint=os.getenv("AWS_S3_ENDPOINT_URL"),
+        endpoint = os.getenv("AWS_S3_ENDPOINT_URL")
+        if not endpoint:
+            raise RuntimeError(
+                "AWS_S3_ENDPOINT_URL is required for metadata persistence."
+            )
+
+        self.bucket_name = os.getenv("AWS_STORAGE_BUCKET_NAME")
+        if not self.bucket_name:
+            raise RuntimeError(
+                "AWS_STORAGE_BUCKET_NAME is required for metadata persistence."
+            )
+
+        self.minio_client = Minio(
+            endpoint=endpoint,
             access_key=os.getenv("AWS_S3_ACCESS_KEY_ID"),
             secret_key=os.getenv("AWS_S3_SECRET_ACCESS_KEY"),
             secure=os.getenv("AWS_S3_SECURE_ACCESS", "False").lower() == "true",
         )
-
-        # todo - raise error if none
-        self.bucket_name = os.getenv("AWS_STORAGE_BUCKET_NAME")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def __init__(self, ctx: JobContext):
"""Initialize metadata agent."""
self.minio_client = Minio(
endpoint=os.getenv("AWS_S3_ENDPOINT_URL"),
access_key=os.getenv("AWS_S3_ACCESS_KEY_ID"),
secret_key=os.getenv("AWS_S3_SECRET_ACCESS_KEY"),
secure=os.getenv("AWS_S3_SECURE_ACCESS", "False").lower() == "true",
)
# todo - raise error if none
self.bucket_name = os.getenv("AWS_STORAGE_BUCKET_NAME")
def __init__(self, ctx: JobContext):
"""Initialize metadata agent."""
endpoint = os.getenv("AWS_S3_ENDPOINT_URL")
if not endpoint:
raise RuntimeError(
"AWS_S3_ENDPOINT_URL is required for metadata persistence."
)
self.bucket_name = os.getenv("AWS_STORAGE_BUCKET_NAME")
if not self.bucket_name:
raise RuntimeError(
"AWS_STORAGE_BUCKET_NAME is required for metadata persistence."
)
self.minio_client = Minio(
endpoint=endpoint,
access_key=os.getenv("AWS_S3_ACCESS_KEY_ID"),
secret_key=os.getenv("AWS_S3_SECRET_ACCESS_KEY"),
secure=os.getenv("AWS_S3_SECURE_ACCESS", "False").lower() == "true",
)
🤖 Prompt for AI Agents
In src/agents/metadata_extractor.py around lines 96 to 107, the Minio client and
bucket_name are created without validating required environment variables; add
upfront validation to fail fast: check that AWS_S3_ENDPOINT_URL and
AWS_STORAGE_BUCKET_NAME (and optionally
AWS_S3_ACCESS_KEY_ID/AWS_S3_SECRET_ACCESS_KEY if required) are present and raise
a clear exception (ValueError or custom) if missing before constructing Minio,
so initialization fails immediately with an explanatory message rather than
crashing later during save().

@lebaudantoine
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Oct 13, 2025

✅ Actions performed

Full review triggered.

Copy link

@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: 5

♻️ Duplicate comments (2)
src/agents/metadata_extractor.py (2)

38-38: AGENT_NAME is used, not dead code.

Contrary to a previous review comment, AGENT_NAME is actively used on line 345 (to construct agent_identity) and line 381 (as a WorkerOptions parameter). The constant should remain.


100-110: Missing environment variable validation causes delayed failures.

Lines 102-110 construct the MinIO client and retrieve the bucket name without validating that required environment variables are present. If AWS_S3_ENDPOINT_URL or AWS_STORAGE_BUCKET_NAME is missing, the agent initializes successfully but crashes later during save() (line 179), making the failure harder to diagnose. The TODO comment on line 109 acknowledges this gap.

Fail fast by validating these variables upfront.

Apply this diff to add validation:

+        endpoint = os.getenv("AWS_S3_ENDPOINT_URL")
+        if not endpoint:
+            raise ValueError("AWS_S3_ENDPOINT_URL is required for metadata persistence")
+
+        bucket_name = os.getenv("AWS_STORAGE_BUCKET_NAME")
+        if not bucket_name:
+            raise ValueError("AWS_STORAGE_BUCKET_NAME is required for metadata persistence")
+
         self.minio_client = Minio(
-            endpoint=os.getenv("AWS_S3_ENDPOINT_URL"),
+            endpoint=endpoint,
             access_key=os.getenv("AWS_S3_ACCESS_KEY_ID"),
             secret_key=os.getenv("AWS_S3_SECRET_ACCESS_KEY"),
             secure=os.getenv("AWS_S3_SECURE_ACCESS", "False").lower() == "true",
         )
 
-        # todo - raise error if none
-        self.bucket_name = os.getenv("AWS_STORAGE_BUCKET_NAME")
+        self.bucket_name = bucket_name
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4353db4 and 98fba52.

📒 Files selected for processing (4)
  • src/agents/Dockerfile (1 hunks)
  • src/agents/metadata_extractor.py (1 hunks)
  • src/agents/pyproject.toml (1 hunks)
  • src/helm/env.d/dev-keycloak/values.meet.yaml.gotmpl (1 hunks)
🔇 Additional comments (3)
src/helm/env.d/dev-keycloak/values.meet.yaml.gotmpl (1)

284-285: Verify the certificate mount path change.

The certificate mount path was changed from the Python certifi path to /usr/lib/ssl/cert.pem. Ensure this path is recognized by the MinIO client and other components for SSL/TLS verification. If this change is intentional, consider documenting the rationale.

src/agents/metadata_extractor.py (2)

342-367: LGTM!

The job request handler correctly prevents duplicate agents by querying the LiveKit API for existing agents with the same identity before accepting. Error handling and logging are appropriate.


321-339: LGTM!

The entrypoint function correctly initializes the metadata agent, connects to the room with audio-only subscription, processes existing participants, and registers a cleanup callback. The prewarm function appropriately preloads the VAD model to reduce cold-start latency.

Also applies to: 370-372

COPY . .

CMD ["python", "multi-user-transcriber.py", "start"]
CMD ["python", "multi_user_transcriber.py", "start"]
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inconsistent startup command: Dockerfile vs. Helm configuration.

The Dockerfile invokes multi_user_transcriber.py start, but the Helm values (src/helm/env.d/dev-keycloak/values.meet.yaml.gotmpl, lines 276-279) override this with metadata_extractor.py start. This creates confusion about which script is the intended entrypoint for the metadata extraction agent.

For consistency and clarity, align the Dockerfile CMD with the intended agent script.

If metadata_extractor.py is the correct entrypoint, apply this diff:

-CMD ["python", "multi_user_transcriber.py", "start"]
+CMD ["python", "metadata_extractor.py", "start"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CMD ["python", "multi_user_transcriber.py", "start"]
CMD ["python", "metadata_extractor.py", "start"]
🤖 Prompt for AI Agents
In src/agents/Dockerfile around line 24, the Dockerfile CMD currently starts the
wrong script ("multi_user_transcriber.py start") which conflicts with the Helm
values that expect "metadata_extractor.py start"; update the Dockerfile CMD to
invoke "metadata_extractor.py start" so the container entrypoint matches the
Helm configuration and avoid startup inconsistencies.

Comment on lines +68 to +89
async def on_enter(self) -> None:
"""Initialize VAD monitoring for this participant."""

@self.session.on("user_state_changed")
def on_user_state(event):
timestamp = datetime.now(timezone.utc)

if event.new_state == "speaking":
event = MetadataEvent(
participant_id=self.participant_identity,
type="speech_start",
timestamp=timestamp,
)
self.events.append(event)

elif event.old_state == "speaking":
event = MetadataEvent(
participant_id=self.participant_identity,
type="speech_end",
timestamp=timestamp,
)
self.events.append(event)
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Variable shadowing in VAD state change handler.

Lines 76 and 84 create MetadataEvent instances named event, which shadows the callback parameter event (line 72). While the code functions correctly, this shadowing reduces clarity.

Apply this diff to eliminate shadowing:

         @self.session.on("user_state_changed")
         def on_user_state(event):
             timestamp = datetime.now(timezone.utc)
 
             if event.new_state == "speaking":
-                event = MetadataEvent(
+                speech_start = MetadataEvent(
                     participant_id=self.participant_identity,
                     type="speech_start",
                     timestamp=timestamp,
                 )
-                self.events.append(event)
+                self.events.append(speech_start)
 
             elif event.old_state == "speaking":
-                event = MetadataEvent(
+                speech_end = MetadataEvent(
                     participant_id=self.participant_identity,
                     type="speech_end",
                     timestamp=timestamp,
                 )
-                self.events.append(event)
+                self.events.append(speech_end)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def on_enter(self) -> None:
"""Initialize VAD monitoring for this participant."""
@self.session.on("user_state_changed")
def on_user_state(event):
timestamp = datetime.now(timezone.utc)
if event.new_state == "speaking":
event = MetadataEvent(
participant_id=self.participant_identity,
type="speech_start",
timestamp=timestamp,
)
self.events.append(event)
elif event.old_state == "speaking":
event = MetadataEvent(
participant_id=self.participant_identity,
type="speech_end",
timestamp=timestamp,
)
self.events.append(event)
async def on_enter(self) -> None:
"""Initialize VAD monitoring for this participant."""
@self.session.on("user_state_changed")
def on_user_state(event):
timestamp = datetime.now(timezone.utc)
if event.new_state == "speaking":
speech_start = MetadataEvent(
participant_id=self.participant_identity,
type="speech_start",
timestamp=timestamp,
)
self.events.append(speech_start)
elif event.old_state == "speaking":
speech_end = MetadataEvent(
participant_id=self.participant_identity,
type="speech_end",
timestamp=timestamp,
)
self.events.append(speech_end)
🤖 Prompt for AI Agents
In src/agents/metadata_extractor.py around lines 68 to 89, the callback
parameter event is being shadowed by local variables named event when creating
MetadataEvent instances; rename those local variables (e.g., metadata_event or
mevt) so they do not overwrite the callback parameter, and update the subsequent
self.events.append(...) calls to append the new variable name to preserve
clarity and avoid shadowing.

Comment on lines 178 to 192
try:
self.minio_client.put_object(
self.bucket_name,
object_name,
stream,
length=len(data),
content_type="application/json",
)
logger.info(
"Uploaded speaker meeting metadata",
)
except S3Error:
logger.exception(
"Failed to upload meeting metadata",
)
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Broaden exception handling to prevent uncaught errors.

Line 189 catches only S3Error, but other exceptions (e.g., network errors, credential issues, or SDK bugs) may occur during the upload. Uncaught exceptions could crash the agent during cleanup.

Apply this diff to catch all exceptions:

         try:
             self.minio_client.put_object(
                 self.bucket_name,
                 object_name,
                 stream,
                 length=len(data),
                 content_type="application/json",
             )
             logger.info(
                 "Uploaded speaker meeting metadata",
             )
-        except S3Error:
+        except Exception:
             logger.exception(
                 "Failed to upload meeting metadata",
             )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
self.minio_client.put_object(
self.bucket_name,
object_name,
stream,
length=len(data),
content_type="application/json",
)
logger.info(
"Uploaded speaker meeting metadata",
)
except S3Error:
logger.exception(
"Failed to upload meeting metadata",
)
try:
self.minio_client.put_object(
self.bucket_name,
object_name,
stream,
length=len(data),
content_type="application/json",
)
logger.info(
"Uploaded speaker meeting metadata",
)
except Exception:
logger.exception(
"Failed to upload meeting metadata",
)
🤖 Prompt for AI Agents
In src/agents/metadata_extractor.py around lines 178 to 192, the current upload
block only catches S3Error which can leave other failures uncaught; change the
exception handling to catch all exceptions (except Exception as e) so
network/credential/SDK errors are handled, and log the full exception/traceback
(use logger.exception with a descriptive message) to avoid crashing during
cleanup while keeping the existing success log; do not re-raise so the agent can
continue cleanup.

"livekit-plugins-silero==1.2.6",
"python-dotenv==1.1.1"
"python-dotenv==1.1.1",
"minio==7.2.15"
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

Verify the minio package version for validity and security.

Since my knowledge cutoff is outdated, please verify that minio==7.2.15 is a valid, current, and secure version.

Run the following script to check for the latest version and any security advisories:


🏁 Script executed:

#!/bin/bash
# Description: Check PyPI for latest minio version and GitHub security advisories.

# Check PyPI for latest version
echo "=== Latest minio version on PyPI ==="
curl -s https://pypi.org/pypi/minio/json | jq -r '.info.version'

# Check for security advisories
echo -e "\n=== Security advisories for minio (Python) ==="
gh api graphql -f query='
{
  securityVulnerabilities(first: 10, ecosystem: PIP, package: "minio") {
    nodes {
      advisory {
        summary
        severity
        publishedAt
      }
      vulnerableVersionRange
      firstPatchedVersion {
        identifier
      }
    }
  }
}'

Length of output: 141


Upgrade minio dependency to 7.2.18
No security advisories were found; bump to the latest patch for bug fixes.

🤖 Prompt for AI Agents
In src/agents/pyproject.toml at line 11, the minio dependency is pinned to
"minio==7.2.15"; update this entry to "minio==7.2.18" to bump to the latest
patch release, then regenerate/update the lockfile (e.g., run your dependency
tool's lock/install command) to ensure the new version is reflected in the
lockfile and CI.

LIVEKIT_API_KEY: {{ $key }}
{{- end }}
{{- end }}
AWS_S3_ENDPOINT_URL: minio.meet.svc.cluster.local:9000
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inconsistent S3 endpoint URL format.

Line 265 specifies AWS_S3_ENDPOINT_URL: minio.meet.svc.cluster.local:9000 without a protocol prefix, while other sections (lines 60, 151, 187, 224) use http://minio.meet.svc.cluster.local:9000. The MinIO Python client may require an explicit protocol.

Apply this diff to ensure consistency and prevent connection errors:

-    AWS_S3_ENDPOINT_URL: minio.meet.svc.cluster.local:9000
+    AWS_S3_ENDPOINT_URL: http://minio.meet.svc.cluster.local:9000
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
AWS_S3_ENDPOINT_URL: minio.meet.svc.cluster.local:9000
AWS_S3_ENDPOINT_URL: http://minio.meet.svc.cluster.local:9000
🤖 Prompt for AI Agents
In src/helm/env.d/dev-keycloak/values.meet.yaml.gotmpl around line 265, the
AWS_S3_ENDPOINT_URL is missing the protocol (currently
"minio.meet.svc.cluster.local:9000") while other entries use "http://...";
update this value to include the explicit protocol (e.g.
"http://minio.meet.svc.cluster.local:9000") to match the other entries and
ensure MinIO clients receive a full URL.

@sonarqubecloud
Copy link

GuittenyMartin pushed a commit that referenced this pull request Oct 17, 2025
Copy the metadata agent from "Metadata agent #741" branch
to have the VAD agent and the right output format for metadatas
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants