-
Notifications
You must be signed in to change notification settings - Fork 91
Metadata agent #741
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
base: main
Are you sure you want to change the base?
Metadata agent #741
Changes from 3 commits
698aef8
969435f
87dd081
8f6ffc7
27fe0d8
33a6d2f
4a29169
067cabf
b87dfc5
98fba52
d8ed15e
7665de1
6b9ffc0
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,314 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Metadata agent that extracts metadata from active room.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import uuid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from dataclasses import asdict, dataclass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from datetime import datetime | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from io import BytesIO | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import List | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from dotenv import load_dotenv | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from livekit import rtc | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from livekit.agents import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Agent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AgentSession, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AutoSubscribe, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JobContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JobProcess, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RoomInputOptions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RoomIO, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RoomOutputOptions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WorkerOptions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cli, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utils, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from livekit.plugins import silero | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from minio import Minio | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from minio.error import S3Error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| load_dotenv() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger = logging.getLogger("metadata-extractor") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AGENT_NAME = os.getenv("ROOM_METADATA_AGENT_NAME", "metadata-extractor") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @dataclass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class MetadataEvent: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Wip.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| participant_id: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: str | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp: datetime | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def to_dict(self) -> dict: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Return a JSON-serializable dictionary representation of the event.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data = asdict(self) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data["timestamp"] = self.timestamp.isoformat() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class VADAgent(Agent): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Agent that monitors voice activity for a specific participant.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self, participant_identity: str, events: List): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Wip.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super().__init__( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| instructions="not-needed", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.participant_identity = participant_identity | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.events = events | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class MetadataAgent: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Monitor and manage real-time metadata extraction from meeting rooms. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Oversees VAD (Voice Activity Detection) and participant metadata streams | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| to track and analyze real-time events, coordinating data collection across | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| participants for insights like speaking activity and engagement. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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.""" | |
| 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().
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.
🧹 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.
| 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.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,8 @@ dependencies = [ | |
| "livekit-agents==1.2.6", | ||
| "livekit-plugins-deepgram==1.2.6", | ||
| "livekit-plugins-silero==1.2.6", | ||
| "python-dotenv==1.1.1" | ||
| "python-dotenv==1.1.1", | ||
| "minio==7.2.15" | ||
|
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. 🧹 Nitpick | 🔵 Trivial 🧩 Analysis chainVerify 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 🤖 Prompt for AI Agents |
||
| ] | ||
|
|
||
| [project.optional-dependencies] | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -262,12 +262,22 @@ agents: | |||||
| LIVEKIT_API_KEY: {{ $key }} | ||||||
| {{- end }} | ||||||
| {{- end }} | ||||||
| AWS_S3_ENDPOINT_URL: minio.meet.svc.cluster.local:9000 | ||||||
|
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. Inconsistent S3 endpoint URL format. Line 265 specifies 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
Suggested change
🤖 Prompt for AI Agents |
||||||
| AWS_S3_ACCESS_KEY_ID: meet | ||||||
| AWS_S3_SECRET_ACCESS_KEY: password | ||||||
| AWS_S3_SECURE_ACCESS: False | ||||||
| AWS_STORAGE_BUCKET_NAME: meet-media-storage | ||||||
|
|
||||||
| image: | ||||||
| repository: localhost:5001/meet-agents | ||||||
| pullPolicy: Always | ||||||
| tag: "latest" | ||||||
|
|
||||||
| command: | ||||||
| - "python" | ||||||
| - "metadata_extractor.py" | ||||||
| - "start" | ||||||
|
|
||||||
| # Extra volume mounts to manage our local custom CA and avoid to disable ssl | ||||||
| extraVolumeMounts: | ||||||
| - name: certs | ||||||
|
|
||||||
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.
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 withmetadata_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.pyis the correct entrypoint, apply this diff:📝 Committable suggestion
🤖 Prompt for AI Agents