Skip to content

Commit 491e328

Browse files
authored
Merge pull request #49 from mezonai/feature/record_Agent_session
add logic interview
2 parents ac0643c + bcf8078 commit 491e328

6 files changed

Lines changed: 357 additions & 4 deletions

File tree

Architect_MultiClient_Server/orchestrator_service/api/dispatch_api.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from pydantic import BaseModel
1414
from orchestrator_service.auth.verify_account import authenticate_account
1515
from orchestrator_service.services.livekit_client import get_livekit_service
16+
from orchestrator_service.services.interview_queue import get_interview_queue
1617

1718
router = APIRouter()
1819

@@ -70,7 +71,8 @@ class AccountModel(BaseModel):
7071
class DispatchRequestModel(BaseModel):
7172
account: AccountModel
7273
room_name: str
73-
74+
type: Optional[str] = "normal"
75+
metadata: dict = {}
7476

7577
async def ensure_dispatch(room_name: str) -> Dict[str, Any]:
7678
"""
@@ -172,6 +174,20 @@ async def api_create_dispatch(body: DispatchRequestModel) -> Dict[str, Any]:
172174
"""Create a dispatch for the specified room."""
173175
await verify_account(body.account.dict())
174176

177+
if(body.type == "interview"):
178+
try:
179+
# For interview dispatches, add to interview queue
180+
interview_id = body.metadata.get("interview_id")
181+
room_name = body.room_name
182+
183+
if interview_id:
184+
interview_queue = get_interview_queue()
185+
interview_queue.add_by_room_name(room_name, interview_id)
186+
else:
187+
raise HTTPException(status_code=400, detail="interview_id is required in metadata for interview dispatch")
188+
189+
except Exception as e:
190+
raise HTTPException(status_code=400, detail=f"Invalid metadata for interview dispatch: {str(e)}")
175191
result = await ensure_dispatch(body.room_name)
176192
if result["status"] == DispatchStatus.ERROR:
177193
raise HTTPException(status_code=500, detail=result["message"])

Architect_MultiClient_Server/orchestrator_service/api/room_registry_api.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from orchestrator_service.services.room_registry import get_room_registry
1313
from orchestrator_service.services.livekit_client import get_livekit_service
1414
from orchestrator_service.services.transcription_service import TranscriptionService
15+
from orchestrator_service.services.interview_queue import get_interview_queue
1516
from orchestrator_service.auth.transcript_auth import verify_api_key
1617

1718
# Import để có thể access egress_service
@@ -62,15 +63,17 @@ async def register_room(
6263
"""
6364

6465
registry = get_room_registry()
66+
interview_queue = get_interview_queue()
6567
stt_room_id = None
6668
tracks_started = 0
6769

6870
# 1. Start room in STT service FIRST
6971
try:
7072
stt_response = await transcription_service.start_room(request.room_name)
71-
if stt_response.get("success"):
72-
stt_room_id = stt_response.get("room_id")
73-
logger.info(f"✅ Room '{request.room_name}' started in STT service")
73+
if stt_response:
74+
if stt_response.get("success"):
75+
stt_room_id = stt_response.get("room_id")
76+
logger.info(f"✅ Room '{request.room_name}' started in STT service")
7477
else:
7578
logger.warning(f"⚠️ Failed to start room in STT service")
7679
except Exception as e:
@@ -83,6 +86,12 @@ async def register_room(
8386
detail=f"Room '{request.room_name}' is already registered"
8487
)
8588

89+
# 2.5. Check if this room is in interview queue and update mapping
90+
if stt_room_id and interview_queue.get_interview_id(request.room_name):
91+
# Update from room_name to room_id
92+
interview_queue.update_to_room_id(request.room_name, stt_room_id)
93+
logger.info(f"✅ Interview mapping updated: room_name='{request.room_name}' → room_id='{stt_room_id}'")
94+
8695
# 3. Start recording for existing tracks (best effort)
8796
try:
8897
livekit_service = get_livekit_service()

Architect_MultiClient_Server/orchestrator_service/api/summary_api.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"""
22
Internal API endpoints for room summary
33
"""
4+
import asyncio
45
from fastapi import APIRouter, HTTPException, Body, Depends, Header, Query
56
from pydantic import BaseModel
67
from orchestrator_service.services.summary_service import get_summary_service
8+
from orchestrator_service.services.interview_queue import get_interview_queue
9+
from orchestrator_service.services.interview_webhook_service import get_interview_webhook_service
710
from orchestrator_service.config.application_config import get_config
811
from datetime import datetime
912
from typing import Optional
@@ -32,7 +35,22 @@ async def generate_room_summary(request: SummaryRequest = Body(...)):
3235
"""
3336
Internal endpoint to generate a summary for a room.
3437
Input: {"room_id": "..."}
38+
39+
If the room is associated with an interview, also sends track data to interview webhook.
3540
"""
41+
42+
# Check if this room is associated with an interview
43+
interview_queue = get_interview_queue()
44+
interview_id = interview_queue.get_interview_id(request.room_id)
45+
46+
if interview_id:
47+
# Send interview data to webhook asynchronously
48+
webhook_service = get_interview_webhook_service()
49+
asyncio.create_task(
50+
webhook_service.send_interview_data(interview_id, request.room_id)
51+
)
52+
interview_queue.remove(request.room_id) # Optionally remove from queue after processing
53+
3654
service = get_summary_service()
3755
result = await service.generate_summary(request.room_id)
3856

Architect_MultiClient_Server/orchestrator_service/config/application_config.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,35 @@ def validate(self) -> bool:
239239
return True
240240

241241

242+
# ============================================================================
243+
# Interview Configuration
244+
# ============================================================================
245+
246+
@dataclass
247+
class InterviewConfig:
248+
"""Configuration for interview webhook service"""
249+
webhook_url: str = ""
250+
webhook_api_key: str = ""
251+
timeout: float = 30.0
252+
enabled: bool = False
253+
254+
@classmethod
255+
def from_env(cls) -> 'InterviewConfig':
256+
"""Create Interview config from environment variables"""
257+
return cls(
258+
webhook_url=os.getenv('INTERVIEW_WEBHOOK_URL', ''),
259+
webhook_api_key=os.getenv('INTERVIEW_WEBHOOK_API_KEY', ''),
260+
timeout=float(os.getenv('INTERVIEW_WEBHOOK_TIMEOUT', '30.0')),
261+
enabled=os.getenv('INTERVIEW_ENABLED', 'false').lower() == 'true',
262+
)
263+
264+
def validate(self) -> bool:
265+
"""Validate interview configuration"""
266+
if self.enabled and not self.webhook_url:
267+
return False
268+
return True
269+
270+
242271
# ============================================================================
243272
# Main Application Configuration (Singleton)
244273
# ============================================================================
@@ -268,6 +297,7 @@ def __init__(self):
268297
self.logger = LoggerConfig.from_env()
269298
self.minio = MinIOConfig.from_env()
270299
self.llm = LLMConfig.from_env()
300+
self.interview = InterviewConfig.from_env()
271301

272302
self._initialized = True
273303
self._validate_all()
@@ -278,6 +308,8 @@ def _validate_all(self):
278308
raise ValueError("Invalid LiveKit configuration")
279309
if not self.minio.validate():
280310
raise ValueError("Invalid MinIO configuration")
311+
if not self.interview.validate():
312+
raise ValueError("Invalid Interview configuration")
281313

282314
@classmethod
283315
def get_instance(cls) -> 'Config':
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""
2+
Interview Queue Service - Singleton for managing interview room mappings
3+
"""
4+
from typing import Dict, Optional
5+
from orchestrator_service.utils.logger import get_logger
6+
7+
logger = get_logger(__name__)
8+
9+
10+
class InterviewQueue:
11+
"""
12+
Singleton service to manage interview room mappings.
13+
14+
Lifecycle:
15+
1. When interview dispatch is created: {room_name: interview_id}
16+
2. When room is registered: update to {room_id: interview_id}
17+
3. When summary is generated: retrieve interview_id by room_id and send to webhook
18+
"""
19+
20+
_instance = None
21+
22+
def __new__(cls):
23+
if cls._instance is None:
24+
cls._instance = super().__new__(cls)
25+
cls._instance._initialized = False
26+
return cls._instance
27+
28+
def __init__(self):
29+
if self._initialized:
30+
return
31+
32+
# Store mappings: key can be room_name or room_id, value is interview_id
33+
self._queue: Dict[str, str] = {}
34+
self._initialized = True
35+
logger.info("InterviewQueue initialized")
36+
37+
def add_by_room_name(self, room_name: str, interview_id: str) -> bool:
38+
"""
39+
Add interview mapping by room_name.
40+
Called when dispatch is created with interview type.
41+
42+
Args:
43+
room_name: LiveKit room name
44+
interview_id: Interview identifier
45+
46+
Returns:
47+
True if added successfully
48+
"""
49+
if not room_name or not interview_id:
50+
logger.warning(f"Invalid parameters: room_name={room_name}, interview_id={interview_id}")
51+
return False
52+
53+
self._queue[room_name] = interview_id
54+
logger.info(f"✅ Interview added: room_name='{room_name}' → interview_id='{interview_id}'")
55+
return True
56+
57+
def update_to_room_id(self, room_name: str, room_id: str) -> bool:
58+
"""
59+
Update mapping from room_name to room_id when room is registered.
60+
61+
Args:
62+
room_name: LiveKit room name
63+
room_id: MongoDB room _id
64+
65+
Returns:
66+
True if updated successfully, False if room_name not found
67+
"""
68+
if room_name not in self._queue:
69+
logger.debug(f"Room '{room_name}' not in interview queue, skipping update")
70+
return False
71+
72+
interview_id = self._queue[room_name]
73+
74+
# Remove old room_name mapping
75+
del self._queue[room_name]
76+
77+
# Add new room_id mapping
78+
self._queue[room_id] = interview_id
79+
80+
logger.info(f"✅ Interview mapping updated: room_name='{room_name}' → room_id='{room_id}', interview_id='{interview_id}'")
81+
return True
82+
83+
def get_interview_id(self, key: str) -> Optional[str]:
84+
"""
85+
Get interview_id by room_name or room_id.
86+
87+
Args:
88+
key: Either room_name or room_id
89+
90+
Returns:
91+
interview_id if found, None otherwise
92+
"""
93+
return self._queue.get(key)
94+
95+
def remove(self, key: str) -> bool:
96+
"""
97+
Remove interview mapping.
98+
99+
Args:
100+
key: Either room_name or room_id
101+
102+
Returns:
103+
True if removed, False if not found
104+
"""
105+
if key in self._queue:
106+
interview_id = self._queue.pop(key)
107+
logger.info(f"🗑️ Interview mapping removed: key='{key}', interview_id='{interview_id}'")
108+
return True
109+
return False
110+
111+
def clear(self):
112+
"""Clear all interview mappings."""
113+
count = len(self._queue)
114+
self._queue.clear()
115+
logger.info(f"🗑️ Interview queue cleared: {count} mappings removed")
116+
117+
def list_all(self) -> Dict[str, str]:
118+
"""Get all interview mappings."""
119+
return self._queue.copy()
120+
121+
def count(self) -> int:
122+
"""Get count of interview mappings."""
123+
return len(self._queue)
124+
125+
126+
def get_interview_queue() -> InterviewQueue:
127+
"""Get the singleton interview queue instance."""
128+
return InterviewQueue()

0 commit comments

Comments
 (0)