@@ -207,14 +207,17 @@ def _agent_state_table():
207207def _load_sessions_from_db() -> list[dict[str, str]]:
208208 table = _agent_state_table()
209209 if not table or not STATE.selected_agent_id:
210+ logging.debug("_load_sessions_from_db: no table (%s) or agent_id (%s)", table, STATE.selected_agent_id)
210211 return []
211212
212213 pk = f"AGENT#{STATE.selected_agent_id}"
214+ logging.info("Loading sessions from DynamoDB: pk=%s, table=%s", pk, STATE.selected_table)
213215 try:
214216 resp = table.query(
215217 KeyConditionExpression=Key("pk").eq(pk) & Key("sk").begins_with("SESSION#"),
216218 Limit=200,
217219 )
220+ logging.info("Sessions query returned %d items", len(resp.get("Items", [])))
218221 except Exception as e:
219222 logging.error("Failed to list sessions from DynamoDB: %s", e)
220223 return []
@@ -233,13 +236,14 @@ def _load_sessions_from_db() -> list[dict[str, str]]:
233236 )
234237
235238 sessions.sort(key=lambda s: s.get("created") or "", reverse=True)
239+ logging.info("Loaded %d sessions from DynamoDB", len(sessions))
236240 return sessions
237241
238242
239243def _persist_session_to_db(name: str, description: str) -> None:
240244 table = _agent_state_table()
241245 if not table or not STATE.selected_agent_id:
242- logging.warning("Cannot persist session; missing table or agent id." )
246+ logging.warning("Cannot persist session; missing table (%s) or agent id (%s).", STATE.selected_table, STATE.selected_agent_id )
243247 return
244248
245249 pk = f"AGENT#{STATE.selected_agent_id}"
@@ -250,6 +254,7 @@ def _persist_session_to_db(name: str, description: str) -> None:
250254 existing = table.get_item(Key={"pk": pk, "sk": sk}).get("Item")
251255 if existing and existing.get("created"):
252256 created = existing["created"]
257+ logging.info("Session %s already exists, preserving created timestamp", name)
253258 except Exception as e:
254259 logging.warning("Failed to read existing session (soft): %s", e)
255260
@@ -265,6 +270,7 @@ def _persist_session_to_db(name: str, description: str) -> None:
265270
266271 try:
267272 table.put_item(Item=item)
273+ logging.info("Session %s persisted to DynamoDB (pk=%s, sk=%s)", name, pk, sk)
268274 except Exception as e:
269275 logging.error("Failed to persist session %s: %s", name, e)
270276
@@ -983,6 +989,31 @@ def memories_page(request: Request):
983989 return templates.TemplateResponse("memories.html", {"request": request, "state": STATE})
984990
985991
992+ @app.get("/stack_outputs")
993+ def stack_outputs_page(request: Request):
994+ """Stack outputs and connectivity testing page."""
995+ prefix = _normalized_stack_prefix()
996+ stacks_available, _, _, _ = _list_stacks_by_status(prefix)
997+
998+ # Get outputs for selected stack
999+ outputs = []
1000+ if STATE.selected_stack:
1001+ try:
1002+ cf = cf_client()
1003+ resp = cf.describe_stacks(StackName=STATE.selected_stack)
1004+ stack_data = resp.get("Stacks", [{}])[0]
1005+ outputs = stack_data.get("Outputs", [])
1006+ except Exception as e:
1007+ logging.error("Failed to get stack outputs: %s", e)
1008+
1009+ return templates.TemplateResponse("stack_outputs.html", {
1010+ "request": request,
1011+ "state": STATE,
1012+ "stacks": stacks_available,
1013+ "outputs": outputs,
1014+ })
1015+
1016+
9861017@app.post("/api/send_message")
9871018async def send_message(request: Request):
9881019 if not STATE.selected_endpoint:
@@ -1087,6 +1118,85 @@ def _effective_region() -> str:
10871118 )
10881119
10891120
1121+ # ─────────────────────────────────────────────────────────────────────────────
1122+ # Connectivity Testing Endpoints
1123+ # ─────────────────────────────────────────────────────────────────────────────
1124+
1125+ @app.get("/api/test_broker")
1126+ async def test_broker():
1127+ """Test connectivity to the broker endpoint."""
1128+ if not STATE.selected_endpoint:
1129+ return {"success": False, "message": "No broker endpoint configured"}
1130+
1131+ try:
1132+ import httpx
1133+ async with httpx.AsyncClient(timeout=10.0) as client:
1134+ # Send a minimal test request
1135+ resp = await client.post(
1136+ STATE.selected_endpoint,
1137+ json={"transcript": "ping", "channel": "test"},
1138+ headers={"Content-Type": "application/json"},
1139+ )
1140+ if resp.status_code in (200, 201, 400, 401, 403):
1141+ return {"success": True, "message": f"Broker reachable (HTTP {resp.status_code})"}
1142+ return {"success": False, "message": f"Unexpected status: {resp.status_code}"}
1143+ except Exception as e:
1144+ return {"success": False, "message": f"Connection failed: {str(e)}"}
1145+
1146+
1147+ @app.get("/api/test_dynamo")
1148+ async def test_dynamo():
1149+ """Test connectivity to DynamoDB table."""
1150+ if not STATE.selected_table:
1151+ return {"success": False, "message": "No DynamoDB table configured"}
1152+
1153+ try:
1154+ table = _agent_state_table()
1155+ if not table:
1156+ return {"success": False, "message": "Could not connect to table"}
1157+
1158+ # Try a simple describe operation
1159+ resp = table.meta.client.describe_table(TableName=STATE.selected_table)
1160+ status = resp.get("Table", {}).get("TableStatus", "UNKNOWN")
1161+ item_count = resp.get("Table", {}).get("ItemCount", 0)
1162+ return {"success": True, "message": f"Table status: {status}, Items: {item_count}"}
1163+ except Exception as e:
1164+ return {"success": False, "message": f"Connection failed: {str(e)}"}
1165+
1166+
1167+ @app.get("/api/test_s3")
1168+ async def test_s3():
1169+ """Test connectivity to S3 bucket."""
1170+ if not STATE.selected_bucket:
1171+ return {"success": False, "message": "No S3 bucket configured"}
1172+
1173+ try:
1174+ s3 = boto_sess().client("s3")
1175+ # Try to list a few objects
1176+ resp = s3.list_objects_v2(Bucket=STATE.selected_bucket, MaxKeys=1)
1177+ return {"success": True, "message": f"Bucket accessible, {resp.get('KeyCount', 0)} objects found"}
1178+ except Exception as e:
1179+ return {"success": False, "message": f"Connection failed: {str(e)}"}
1180+
1181+
1182+ @app.post("/api/test_endpoint")
1183+ async def test_endpoint(request: Request):
1184+ """Test connectivity to an arbitrary endpoint."""
1185+ data = await request.json()
1186+ url = data.get("url", "").strip()
1187+
1188+ if not url:
1189+ return {"success": False, "message": "No URL provided"}
1190+
1191+ try:
1192+ import httpx
1193+ async with httpx.AsyncClient(timeout=10.0) as client:
1194+ resp = await client.get(url)
1195+ return {"success": True, "message": f"HTTP {resp.status_code}"}
1196+ except Exception as e:
1197+ return {"success": False, "message": f"Failed: {str(e)}"}
1198+
1199+
10901200# ─────────────────────────────────────────────────────────────────────────────
10911201# Memory and Speaker API Endpoints (for Rank 4/5 features)
10921202# ─────────────────────────────────────────────────────────────────────────────
@@ -1100,43 +1210,48 @@ async def get_memories(
11001210 """Retrieve memories from the agent state table."""
11011211 table = _agent_state_table()
11021212 if not table:
1213+ logging.warning("get_memories: No table configured (table=%s)", STATE.selected_table)
11031214 return {"memories": [], "error": "No table configured"}
11041215
11051216 agent_id = STATE.selected_agent_id or "agent1"
1217+ logging.info("get_memories: agent_id=%s, speaker_id=%s, kind=%s, limit=%d", agent_id, speaker_id, kind, limit)
11061218
11071219 try:
11081220 # Query main agent partition
11091221 pk = f"AGENT#{agent_id}"
11101222 resp = table.query(
11111223 KeyConditionExpression=Key("pk").eq(pk),
11121224 ScanIndexForward=False,
1113- Limit=limit * 2 , # Fetch extra for filtering
1225+ Limit=limit * 3 , # Fetch extra for filtering
11141226 )
11151227 items = resp.get("Items", [])
1116-
1117- # Also query speaker-specific partition if speaker_id provided
1118- if speaker_id:
1119- speaker_pk = f"AGENT#{agent_id}#SPEAKER#{speaker_id}"
1120- speaker_resp = table.query(
1121- KeyConditionExpression=Key("pk").eq(speaker_pk),
1122- ScanIndexForward=False,
1123- Limit=limit,
1124- )
1125- items.extend(speaker_resp.get("Items", []))
1228+ logging.info("get_memories: Query returned %d items from pk=%s", len(items), pk)
11261229
11271230 # Filter to memories only
11281231 memories = [i for i in items if "#MEMORY#" in i.get("sk", "") or i.get("item_type") == "MEMORY"]
1232+ logging.info("get_memories: Filtered to %d memories", len(memories))
11291233
11301234 # Filter by kind if specified
11311235 if kind and kind.upper() != "ALL":
11321236 memories = [m for m in memories if m.get("kind") == kind.upper()]
1237+ logging.info("get_memories: After kind filter (%s): %d memories", kind, len(memories))
11331238
1134- # Filter by speaker_id if specified (in meta)
1135- if speaker_id:
1239+ # Filter by speaker_id if specified
1240+ # Special case: __UNKNOWN__ means filter for memories with no speaker_id
1241+ if speaker_id == "__UNKNOWN__":
1242+ memories = [
1243+ m for m in memories
1244+ if not m.get("speaker_id") and not m.get("meta", {}).get("speaker_id")
1245+ ]
1246+ logging.info("get_memories: After unknown speaker filter: %d memories", len(memories))
1247+ elif speaker_id:
1248+ # Filter for specific speaker
11361249 memories = [
11371250 m for m in memories
11381251 if m.get("speaker_id") == speaker_id or m.get("meta", {}).get("speaker_id") == speaker_id
11391252 ]
1253+ logging.info("get_memories: After speaker filter (%s): %d memories", speaker_id, len(memories))
1254+ # If speaker_id is None/empty, return ALL memories (no filtering)
11401255
11411256 # Sort by timestamp and limit
11421257 memories.sort(key=lambda x: x.get("ts", ""), reverse=True)
@@ -1153,38 +1268,85 @@ async def get_memories(
11531268 return {"memories": [], "error": str(e)}
11541269
11551270
1271+ @app.get("/api/memory_stats")
1272+ async def get_memory_stats():
1273+ """Get memory statistics from the agent state table."""
1274+ table = _agent_state_table()
1275+ if not table:
1276+ return {"total": 0, "by_kind": {}, "error": "No table configured"}
1277+
1278+ agent_id = STATE.selected_agent_id or "agent1"
1279+
1280+ try:
1281+ # Query all memories from main agent partition
1282+ pk = f"AGENT#{agent_id}"
1283+ resp = table.query(
1284+ KeyConditionExpression=Key("pk").eq(pk),
1285+ ScanIndexForward=False,
1286+ Limit=500, # Get enough for stats
1287+ )
1288+ items = resp.get("Items", [])
1289+
1290+ # Filter to memories only
1291+ memories = [i for i in items if "#MEMORY#" in i.get("sk", "") or i.get("item_type") == "MEMORY"]
1292+
1293+ # Count by kind
1294+ by_kind: dict[str, int] = {}
1295+ for m in memories:
1296+ k = m.get("kind", "UNKNOWN")
1297+ by_kind[k] = by_kind.get(k, 0) + 1
1298+
1299+ return {
1300+ "total": len(memories),
1301+ "by_kind": by_kind,
1302+ "agent_id": agent_id,
1303+ }
1304+
1305+ except ClientError as e:
1306+ logging.error("get_memory_stats failed: %s", e)
1307+ return {"total": 0, "by_kind": {}, "error": str(e)}
1308+
1309+
11561310@app.get("/api/speakers")
11571311async def list_speakers():
11581312 """List all known speakers for the current agent."""
11591313 table = _agent_state_table()
11601314 if not table:
1161- return {"speakers": [], "error": "No table configured"}
1315+ logging.warning("list_speakers: No table configured (table=%s)", STATE.selected_table)
1316+ return {"speakers": [], "error": "No table configured", "debug": {"table": STATE.selected_table}}
11621317
11631318 agent_id = STATE.selected_agent_id or "agent1"
11641319
11651320 try:
11661321 # Query VOICE partition for speakers
11671322 pk = f"AGENT#{agent_id}#VOICE"
1323+ logging.info("list_speakers: Querying pk=%s from table=%s", pk, STATE.selected_table)
11681324 resp = table.query(
11691325 KeyConditionExpression=Key("pk").eq(pk),
11701326 Limit=100,
11711327 )
11721328
1329+ items = resp.get("Items", [])
1330+ logging.info("list_speakers: Query returned %d items", len(items))
1331+
11731332 speakers = []
1174- for item in resp.get("Items", []) :
1175- speakers.append( {
1333+ for item in items :
1334+ speaker = {
11761335 "speaker_id": item.get("sk") or item.get("speaker_id"),
11771336 "speaker_name": item.get("speaker_name"),
11781337 "first_seen": item.get("first_seen_ts"),
11791338 "last_seen": item.get("last_seen_ts"),
11801339 "interaction_count": item.get("interaction_count", 0),
11811340 "enrollment_status": item.get("enrollment_status", "unknown"),
1182- })
1341+ }
1342+ speakers.append(speaker)
1343+ logging.debug("list_speakers: Found speaker %s (%s)", speaker["speaker_id"], speaker["speaker_name"])
11831344
11841345 return {
11851346 "speakers": speakers,
11861347 "total": len(speakers),
11871348 "agent_id": agent_id,
1349+ "debug": {"pk": pk, "table": STATE.selected_table},
11881350 }
11891351
11901352 except ClientError as e:
0 commit comments