@@ -51,9 +51,14 @@ def _secure_log_filename(name: str) -> str:
5151 _HAS_TRANSCRIBE_STREAMING = False
5252
5353
54+ from fastapi .staticfiles import StaticFiles
55+
5456app = FastAPI (title = "marvain — Agent Manager" )
5557templates = Jinja2Templates (directory = "client/templates" )
5658
59+ # Mount static files directory
60+ app .mount ("/static" , StaticFiles (directory = "client/static" ), name = "static" )
61+
5762logging .basicConfig (level = logging .INFO , format = "%(asctime)s [%(levelname)s] %(message)s" )
5863
5964SAMCONFIG_PATH = Path ("samconfig.toml" )
@@ -966,6 +971,18 @@ def chat_page(request: Request):
966971 return templates .TemplateResponse ("chat.html" , {"request" : request , "state" : STATE , "stack_name" : label })
967972
968973
974+ @app .get ("/speakers" )
975+ def speakers_page (request : Request ):
976+ """Speaker management page."""
977+ return templates .TemplateResponse ("speakers.html" , {"request" : request , "state" : STATE })
978+
979+
980+ @app .get ("/memories" )
981+ def memories_page (request : Request ):
982+ """Memory visualization page."""
983+ return templates .TemplateResponse ("memories.html" , {"request" : request , "state" : STATE })
984+
985+
969986@app .post ("/api/send_message" )
970987async def send_message (request : Request ):
971988 if not STATE .selected_endpoint :
@@ -1264,6 +1281,135 @@ async def get_conversation_context(session_id: Optional[str] = None):
12641281 return {"context" : [], "error" : str (e )}
12651282
12661283
1284+ @app .post ("/api/speakers/enroll" )
1285+ async def enroll_speaker (request : Request ):
1286+ """Enroll a new speaker with voice samples."""
1287+ from fastapi import UploadFile
1288+ import io
1289+
1290+ table = _agent_state_table ()
1291+ if not table :
1292+ return JSONResponse (content = {"error" : "No table configured" }, status_code = 400 )
1293+
1294+ agent_id = STATE .selected_agent_id or "agent1"
1295+
1296+ try :
1297+ form = await request .form ()
1298+ speaker_name = form .get ("speaker_name" , "" ).strip ()
1299+ if not speaker_name :
1300+ return JSONResponse (content = {"error" : "Speaker name is required" }, status_code = 400 )
1301+
1302+ # Get audio files
1303+ audio_files = form .getlist ("audio_files" )
1304+ voice_samples = []
1305+
1306+ for i , audio_file in enumerate (audio_files ):
1307+ if hasattr (audio_file , "read" ):
1308+ content = await audio_file .read ()
1309+ # Store as base64 (for simplicity; in production, upload to S3)
1310+ import base64
1311+ voice_samples .append ({
1312+ "index" : i ,
1313+ "size" : len (content ),
1314+ "data_b64" : base64 .b64encode (content ).decode ("utf-8" )[:1000 ], # Truncate for storage
1315+ })
1316+
1317+ # Generate speaker ID
1318+ speaker_id = f"spk_{ speaker_name .lower ().replace (' ' , '_' )} _{ uuid4 ().hex [:8 ]} "
1319+
1320+ # Store in DynamoDB
1321+ pk = f"AGENT#{ agent_id } #VOICE"
1322+ item = {
1323+ "pk" : pk ,
1324+ "sk" : speaker_id ,
1325+ "speaker_id" : speaker_id ,
1326+ "speaker_name" : speaker_name ,
1327+ "enrollment_status" : "enrolled" ,
1328+ "first_seen_ts" : datetime .utcnow ().isoformat (),
1329+ "last_seen_ts" : datetime .utcnow ().isoformat (),
1330+ "interaction_count" : 0 ,
1331+ "voice_samples" : voice_samples ,
1332+ }
1333+ table .put_item (Item = item )
1334+
1335+ return {
1336+ "success" : True ,
1337+ "speaker_id" : speaker_id ,
1338+ "speaker_name" : speaker_name ,
1339+ "samples_count" : len (voice_samples ),
1340+ }
1341+
1342+ except Exception as e :
1343+ logging .error ("enroll_speaker failed: %s" , e )
1344+ return JSONResponse (content = {"error" : str (e )}, status_code = 500 )
1345+
1346+
1347+ @app .get ("/api/tool_status" )
1348+ async def get_tool_status ():
1349+ """Get status of recent tool executions from the agent state."""
1350+ table = _agent_state_table ()
1351+ if not table :
1352+ return {"tools" : [], "error" : "No table configured" }
1353+
1354+ agent_id = STATE .selected_agent_id or "agent1"
1355+
1356+ try :
1357+ pk = f"AGENT#{ agent_id } "
1358+ resp = table .query (
1359+ KeyConditionExpression = Key ("pk" ).eq (pk ),
1360+ ScanIndexForward = False ,
1361+ Limit = 50 ,
1362+ )
1363+
1364+ # Filter for events that include tool executions
1365+ tool_events = []
1366+ for item in resp .get ("Items" , []):
1367+ payload = item .get ("payload" , {})
1368+ if isinstance (payload , str ):
1369+ try :
1370+ payload = json .loads (payload )
1371+ except Exception :
1372+ payload = {}
1373+
1374+ # Check if this event has tool info
1375+ if payload .get ("tool_calls" ) or item .get ("source" ) == "tool" :
1376+ tool_events .append ({
1377+ "ts" : item .get ("ts" ),
1378+ "session_id" : item .get ("session_id" ),
1379+ "tool_calls" : payload .get ("tool_calls" , []),
1380+ "iterations" : payload .get ("iterations" , 1 ),
1381+ })
1382+
1383+ return {
1384+ "tools" : tool_events [:20 ],
1385+ "total" : len (tool_events ),
1386+ "agent_id" : agent_id ,
1387+ }
1388+
1389+ except ClientError as e :
1390+ logging .error ("get_tool_status failed: %s" , e )
1391+ return {"tools" : [], "error" : str (e )}
1392+
1393+
1394+ @app .delete ("/api/speakers/{speaker_id}" )
1395+ async def delete_speaker (speaker_id : str ):
1396+ """Delete a speaker from the registry."""
1397+ table = _agent_state_table ()
1398+ if not table :
1399+ return JSONResponse (content = {"error" : "No table configured" }, status_code = 400 )
1400+
1401+ agent_id = STATE .selected_agent_id or "agent1"
1402+
1403+ try :
1404+ pk = f"AGENT#{ agent_id } #VOICE"
1405+ table .delete_item (Key = {"pk" : pk , "sk" : speaker_id })
1406+ return {"success" : True , "speaker_id" : speaker_id }
1407+
1408+ except ClientError as e :
1409+ logging .error ("delete_speaker failed: %s" , e )
1410+ return JSONResponse (content = {"error" : str (e )}, status_code = 500 )
1411+
1412+
12671413# ─────────────────────────────────────────────────────────────────────────────
12681414
12691415
0 commit comments