-
Notifications
You must be signed in to change notification settings - Fork 915
Expand file tree
/
Copy pathagent_demo_helpers.py
More file actions
349 lines (298 loc) · 12.1 KB
/
Copy pathagent_demo_helpers.py
File metadata and controls
349 lines (298 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
"""Agent Demo Helpers (v1 API)
Shared utilities for agent demo scripts (search_agent_demo, coding_agent_demo, etc.).
Provides:
- AgentDemoRunner: stateful helper for v1 API calls (send messages, fetch, search)
- Print helpers: stateless formatters for various memory types
"""
import uuid
from typing import List, Optional
import httpx
from common_utils.datetime_utils import get_now_with_timezone
DEFAULT_BASE_URL = "http://localhost:1995"
# ==================== Print Helpers ====================
def print_separator(text: str = ""):
if text:
print(f"\n{'='*60}")
print(f"{text}")
print('=' * 60)
else:
print('-' * 60)
def print_episodic_memories(memories: list):
"""Print episodic memories."""
if not memories:
print(" (none)")
return
for i, m in enumerate(memories, 1):
print(f"\n [{i}] {m.get('summary') or m.get('episode') or 'N/A'}")
if m.get("keywords"):
print(f" Keywords : {', '.join(m['keywords'])}")
if m.get("timestamp"):
print(f" Time : {m['timestamp']}")
def print_event_logs(memories: list):
"""Print event log memories (atomic facts)."""
if not memories:
print(" (none)")
return
for i, m in enumerate(memories, 1):
print(f"\n [{i}] {m.get('atomic_fact', 'N/A')}")
if m.get("timestamp"):
print(f" Time : {m['timestamp']}")
def print_foresights(memories: list):
"""Print foresight memories."""
if not memories:
print(" (none)")
return
for i, m in enumerate(memories, 1):
content = m.get("content") or m.get("foresight") or "N/A"
print(f"\n [{i}] {content}")
validity = " ~ ".join(filter(None, [m.get("start_time"), m.get("end_time")]))
if validity:
print(f" Validity : {validity}")
if m.get("evidence"):
print(f" Evidence : {m['evidence']}")
def print_agent_cases(memories: list):
"""Print agent experience memories."""
if not memories:
print(" (none)")
return
for i, exp in enumerate(memories, 1):
print(f"\n [{i}] {exp.get('task_intent', 'N/A')}")
print(f" Parent : {exp.get('parent_id', 'N/A')}")
approach = exp.get("approach", "")
if approach:
print(f" Approach : {approach}")
if exp.get("quality_score") is not None:
print(f" Quality : {exp['quality_score']}")
def print_agent_skills(memories: list):
"""Print agent skills."""
if not memories:
print(" (none)")
return
for i, m in enumerate(memories, 1):
print(f"\n [{i}] {m.get('name') or 'Unnamed'}")
if m.get("description"):
print(f" Description: {m['description']}")
print(f" Content : {m.get('content', 'N/A')}")
print(f" Confidence : {m.get('confidence', 0):.2f}")
print(f" Cluster : {m.get('cluster_id', 'N/A')}")
def print_search_case_results(hits: list):
"""Print search results for agent_case."""
if not hits:
print(" (no results)")
return
for i, h in enumerate(hits, 1):
score = h.get("score", 0.0)
task_intent = h.get("task_intent") or ""
print(f"\n [{i}] score={score:.4f}")
print(f" Intent : {task_intent}")
def print_search_skill_results(hits: list):
"""Print search results for agent_skill."""
if not hits:
print(" (no results)")
return
for i, h in enumerate(hits, 1):
score = h.get("score", 0.0)
name = h.get("name") or "Unnamed"
content = h.get("content") or ""
print(f"\n [{i}] score={score:.4f} {name}")
print(f" {content}")
if h.get("description"):
print(f" Description: {h['description']}")
print(f" Confidence : {h.get('confidence', 0.0):.2f}")
# Memory type -> (label, printer) mapping for fetch step
MEMORY_TYPE_PRINTERS = [
("episodic_memory", "Episodic Memory", print_episodic_memories),
("agent_case", "Agent Case", print_agent_cases),
("agent_skill", "Agent Skill", print_agent_skills),
]
# ==================== AgentDemoRunner ====================
class AgentDemoRunner:
"""Stateful helper for running agent demo scripts (v1 API).
Encapsulates user/session config and provides v1 API call methods.
Each demo creates its own runner with unique session_id.
Usage:
runner = AgentDemoRunner(
session_prefix="search_agent_demo",
user_id="demo_user",
)
await runner.send_agent_message(msg, 0, flush=True)
"""
def __init__(
self,
session_prefix: str = "agent_demo",
user_id: str = "demo_user",
msg_prefix: str = "agent_msg",
base_url: str = DEFAULT_BASE_URL,
# Kept for backward compat — old demos pass these but they are unused in v1
group_id_prefix: str = "",
group_name: str = "",
description: str = "", # noqa: ARG002
tags: Optional[List[str]] = None, # noqa: ARG002
):
self.run_id = uuid.uuid4().hex[:8]
self.session_id = f"{session_prefix}_{self.run_id}"
# v1 auto-generates group_id from user_id, but demos may want to reference it
self.group_id = f"{group_id_prefix or session_prefix}_{self.run_id}"
self.group_name = group_name
self.msg_prefix = msg_prefix
self.user_id = user_id
self.base_url = base_url
self.agent_url = f"{base_url}/api/v1/memories/agent"
self.flush_url = f"{base_url}/api/v1/memories/agent/flush"
self.get_url = f"{base_url}/api/v1/memories/get"
self.search_url = f"{base_url}/api/v1/memories/search"
async def save_conversation_meta(self):
"""No-op in v1 — conversation meta is auto-created.
Kept for backward compatibility with existing demo scripts.
"""
print(f" v1 API: group auto-registered (user_id={self.user_id}, session={self.session_id})")
async def send_agent_message(
self, msg: dict, msg_index: int, flush: bool = False
) -> bool:
"""Send a single agent message via POST /api/v1/memories/agent."""
now = get_now_with_timezone()
timestamp_ms = int(now.timestamp() * 1000)
role = msg.get("role", "user")
sender_id = self.user_id if role == "user" else "assistant"
message_item = {
"message_id": f"{self.msg_prefix}_{self.run_id}_{msg_index:03d}",
"sender_id": sender_id,
"sender_name": sender_id,
"role": role,
"timestamp": timestamp_ms,
"content": msg.get("content") or "",
}
if msg.get("tool_calls"):
message_item["tool_calls"] = msg["tool_calls"]
if msg.get("tool_call_id"):
message_item["tool_call_id"] = msg["tool_call_id"]
payload = {
"user_id": self.user_id,
"session_id": self.session_id,
"messages": [message_item],
}
try:
async with httpx.AsyncClient(timeout=500.0) as client:
resp = await client.post(self.agent_url, json=payload)
resp.raise_for_status()
result = resp.json()
data = result.get("data", {})
status = data.get("status", "")
role_label = f"[{role}]".ljust(12)
raw_content = msg.get("content")
if isinstance(raw_content, list):
content_preview = (raw_content[0].get("text", "") if raw_content else "")[:50]
else:
content_preview = (raw_content or "(tool_calls)")[:50]
if status == "extracted":
print(f" {role_label} {content_preview} -> Extracted memories")
else:
print(f" {role_label} {content_preview}")
# Handle flush after message if requested
if flush:
await self._flush()
return True
except httpx.ConnectError:
print(f" Cannot connect to API server ({self.base_url})")
print(f" Please start first: uv run python src/run.py")
return False
except Exception as e:
print(f" Error: {e}")
return False
async def _flush(self) -> bool:
"""Trigger flush via POST /api/v1/memories/agent/flush."""
payload = {
"user_id": self.user_id,
"session_id": self.session_id,
}
try:
async with httpx.AsyncClient(timeout=500.0) as client:
resp = await client.post(self.flush_url, json=payload)
resp.raise_for_status()
result = resp.json()
data = result.get("data", {})
if data.get("status") == "extracted":
print(f" [flush] -> Extracted memories")
else:
print(f" [flush] -> {data.get('status', 'done')}")
return True
except Exception as e:
print(f" Flush error: {e}")
return False
async def fetch_memories(self, memory_type: str) -> list:
"""Fetch memories via POST /api/v1/memories/get."""
payload = {
"memory_type": memory_type,
"page": 1,
"page_size": 20,
"rank_by": "timestamp",
"rank_order": "desc",
"filters": {
"user_id": self.user_id,
},
}
try:
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.post(self.get_url, json=payload)
resp.raise_for_status()
result = resp.json()
data = result.get("data", {})
# v1 GetMemResponse has typed arrays: episodes, profiles, agent_cases, agent_skills
if memory_type == "episodic_memory":
return data.get("episodes", [])
elif memory_type == "profile":
return data.get("profiles", [])
elif memory_type == "agent_case":
return data.get("agent_cases", [])
elif memory_type == "agent_skill":
return data.get("agent_skills", [])
else:
return []
except Exception as e:
print(f" [{memory_type}] Fetch error: {e}")
return []
async def search_memories(
self,
query: str,
memory_type: str,
top_k: int = 5,
method: str = "hybrid",
) -> list | dict:
"""Search memories via POST /api/v1/memories/search.
Args:
query: Search query text.
memory_type: One of "agent_memory", "episodic_memory", "profile", "raw_message".
top_k: Max results.
method: Retrieval method.
Returns:
For agent_memory: dict with "cases" and "skills" lists.
For other types: list of results.
"""
payload = {
"query": query,
"method": method,
"memory_types": [memory_type],
"top_k": top_k,
"filters": {
"user_id": self.user_id,
},
}
try:
async with httpx.AsyncClient(timeout=120.0) as client:
resp = await client.post(self.search_url, json=payload)
resp.raise_for_status()
result = resp.json()
data = result.get("data", {})
if memory_type == "agent_memory":
agent_mem = data.get("agent_memory") or {}
return {
"cases": agent_mem.get("cases", []),
"skills": agent_mem.get("skills", []),
}
elif memory_type == "episodic_memory":
return data.get("episodes", [])
else:
return data.get("memories", [])
except Exception as e:
print(f" Search error: {e}")
return [] if memory_type != "agent_memory" else {"cases": [], "skills": []}