Skip to content

Commit 2141840

Browse files
authored
Merge pull request #20 from Daylily-Informatics/feature/gui-bugfixes-and-enhancements
GUI Bugfixes and Enhancements for Rank 4/5 Testing
2 parents ceb4a0b + acdff01 commit 2141840

File tree

8 files changed

+605
-92
lines changed

8 files changed

+605
-92
lines changed

client/gui.py

Lines changed: 180 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -207,14 +207,17 @@ def _agent_state_table():
207207
def _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

239243
def _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")
9871018
async 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")
11571311
async 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:

client/static/js/memory-panel.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,13 @@ class MemoryPanel {
5454
try {
5555
const params = new URLSearchParams({ limit: this.options.limit });
5656
if (this.filter.kind && this.filter.kind !== 'ALL') params.append('kind', this.filter.kind);
57-
if (this.filter.speaker_id) params.append('speaker_id', this.filter.speaker_id);
57+
// Handle "Unknown Speaker" special filter
58+
if (this.filter.speaker_id === '__UNKNOWN__') {
59+
params.append('speaker_id', '__UNKNOWN__');
60+
} else if (this.filter.speaker_id) {
61+
params.append('speaker_id', this.filter.speaker_id);
62+
}
63+
// Note: When speaker_id is null/empty, we get ALL memories (fixed behavior)
5864
const resp = await fetch(`/api/memories?${params}`);
5965
const data = await resp.json();
6066
this.memories = data.memories || [];
@@ -89,16 +95,22 @@ class MemoryPanel {
8995
const kindClass = kind.toLowerCase().replace(/_/g, '-');
9096
const speaker = m.speaker_id || m.meta?.speaker_id || '';
9197
const importance = m.importance || m.meta?.importance;
98+
const sessionId = m.session_id || m.meta?.session_id || '';
99+
const source = m.meta?.source || '';
92100
const ts = m.ts ? new Date(m.ts).toLocaleString() : '';
93101
return `
94102
<div class="memory-item ${kindClass}" onclick="memoryPanel.selectMemory('${m.sk || ''}')">
95103
<div class="memory-header">
96104
<span class="memory-kind-badge ${kindClass}">${kind}</span>
97105
${importance ? `<span class="importance-badge">${importance}</span>` : ''}
98106
${speaker ? `<span class="speaker-badge">👤 ${speaker}</span>` : ''}
107+
${source ? `<span class="source-badge" style="background:#e0e0e0;padding:1px 6px;border-radius:8px;font-size:0.7em;">${source}</span>` : ''}
99108
</div>
100109
<div class="memory-text">${m.text || ''}</div>
101-
${ts ? `<div class="memory-ts">${ts}</div>` : ''}
110+
<div class="memory-footer" style="display:flex;gap:12px;font-size:0.8em;color:#888;margin-top:4px;">
111+
${ts ? `<span>${ts}</span>` : ''}
112+
${sessionId ? `<span>Session: ${sessionId.substring(0, 12)}...</span>` : ''}
113+
</div>
102114
</div>
103115
`;
104116
}).join('');

0 commit comments

Comments
 (0)