Skip to content

Commit 4dae56c

Browse files
committed
feat(scripts): add agentic search JSONL traces
1 parent 5afc2f3 commit 4dae56c

1 file changed

Lines changed: 106 additions & 43 deletions

File tree

scripts/agentic_search_stream.py

Lines changed: 106 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import json
1414
import sys
1515
import time
16+
from pathlib import Path
1617

1718
import httpx
1819

@@ -149,76 +150,138 @@ def render_error(event: dict) -> None:
149150
}
150151

151152

153+
class JsonlTraceWriter:
154+
"""Write agentic search stream events as replayable JSONL records."""
155+
156+
def __init__(self, path: str, request: dict) -> None:
157+
self.path = Path(path)
158+
self.path.parent.mkdir(parents=True, exist_ok=True)
159+
self.file = self.path.open("w", encoding="utf-8")
160+
self.started_at = time.time()
161+
self.sequence = 0
162+
self.write(
163+
{
164+
"type": "metadata",
165+
"schema_version": 1,
166+
"request": request,
167+
"started_at": self.started_at,
168+
}
169+
)
170+
171+
def write(self, payload: dict) -> None:
172+
self.file.write(json.dumps(payload, ensure_ascii=False) + "\n")
173+
self.file.flush()
174+
175+
def write_event(self, event: dict) -> None:
176+
self.sequence += 1
177+
self.write(
178+
{
179+
"type": "event",
180+
"sequence": self.sequence,
181+
"elapsed_ms": round((time.time() - self.started_at) * 1000),
182+
"event": event,
183+
}
184+
)
185+
186+
def close(self, elapsed_s: float) -> None:
187+
self.write(
188+
{
189+
"type": "summary",
190+
"event_count": self.sequence,
191+
"elapsed_s": round(elapsed_s, 3),
192+
}
193+
)
194+
self.file.close()
195+
196+
152197
def main():
153198
parser = argparse.ArgumentParser(description="AgenticSearch streaming search viewer")
154199
parser.add_argument("collection_id", help="Collection readable ID")
155200
parser.add_argument("query", help="Search query")
156201
parser.add_argument("--host", default="http://localhost:8001", help="API host")
157202
parser.add_argument("--filter", default=None, help="Filter JSON string")
158203
parser.add_argument("--mode", default="agentic", choices=["agentic", "direct"])
204+
parser.add_argument(
205+
"--jsonl",
206+
default=None,
207+
help="Write raw stream events to a JSONL trace for replay/eval analysis",
208+
)
159209
args = parser.parse_args()
160210

161211
url = f"{args.host}/collections/{args.collection_id}/agentic-search/stream"
162212
body = {"query": args.query, "mode": args.mode}
163213
if args.filter:
164214
body["filter"] = json.loads(args.filter)
165215

216+
trace_writer = JsonlTraceWriter(args.jsonl, body) if args.jsonl else None
217+
166218
print(f"{'─' * 60}")
167219
print(f" {bold('Agentic Search')}")
168220
print(f" {dim('Collection:')} {args.collection_id}")
169221
print(f" {dim('Query:')} {args.query}")
170222
if args.filter:
171223
print(f" {dim('Filter:')} {args.filter}")
172224
print(f" {dim('Mode:')} {args.mode}")
225+
if trace_writer:
226+
print(f" {dim('JSONL trace:')} {trace_writer.path}")
173227
print(f"{'─' * 60}")
174228

175229
start = time.monotonic()
176230

177-
with httpx.stream(
178-
"POST",
179-
url,
180-
json=body,
181-
headers={"Content-Type": "application/json"},
182-
timeout=120.0,
183-
) as response:
184-
if response.status_code != 200:
185-
print(red(f"\nHTTP {response.status_code}"))
186-
print(response.read().decode())
187-
sys.exit(1)
188-
189-
buffer = ""
190-
for chunk in response.iter_text():
191-
buffer += chunk
192-
while "\n\n" in buffer:
193-
message, buffer = buffer.split("\n\n", 1)
194-
message = message.strip()
195-
if not message:
196-
continue
197-
198-
# Parse SSE data line
199-
if message.startswith("data: "):
200-
data_str = message[6:]
201-
elif message.startswith("data:"):
202-
data_str = message[5:]
203-
else:
204-
continue
205-
206-
try:
207-
event = json.loads(data_str)
208-
except json.JSONDecodeError:
209-
print(dim(f" [raw] {data_str}"))
210-
continue
211-
212-
event_type = event.get("type", "unknown")
213-
renderer = RENDERERS.get(event_type)
214-
if renderer:
215-
renderer(event)
216-
else:
217-
print(dim(f" [unknown event] {json.dumps(event, indent=2)}"))
218-
219-
elapsed = time.monotonic() - start
231+
try:
232+
with httpx.stream(
233+
"POST",
234+
url,
235+
json=body,
236+
headers={"Content-Type": "application/json"},
237+
timeout=120.0,
238+
) as response:
239+
if response.status_code != 200:
240+
print(red(f"\nHTTP {response.status_code}"))
241+
print(response.read().decode())
242+
sys.exit(1)
243+
244+
buffer = ""
245+
for chunk in response.iter_text():
246+
buffer += chunk
247+
while "\n\n" in buffer:
248+
message, buffer = buffer.split("\n\n", 1)
249+
message = message.strip()
250+
if not message:
251+
continue
252+
253+
# Parse SSE data line
254+
if message.startswith("data: "):
255+
data_str = message[6:]
256+
elif message.startswith("data:"):
257+
data_str = message[5:]
258+
else:
259+
continue
260+
261+
try:
262+
event = json.loads(data_str)
263+
except json.JSONDecodeError:
264+
print(dim(f" [raw] {data_str}"))
265+
continue
266+
267+
if trace_writer:
268+
trace_writer.write_event(event)
269+
270+
event_type = event.get("type", "unknown")
271+
renderer = RENDERERS.get(event_type)
272+
if renderer:
273+
renderer(event)
274+
else:
275+
print(dim(f" [unknown event] {json.dumps(event, indent=2)}"))
276+
finally:
277+
elapsed = time.monotonic() - start
278+
if trace_writer:
279+
trace_writer.close(elapsed)
280+
220281
print(f"\n{'─' * 60}")
221282
print(f" {dim(f'Total time: {elapsed:.1f}s')}")
283+
if trace_writer:
284+
print(f" {dim(f'JSONL trace written to {trace_writer.path}')}")
222285
print(f"{'─' * 60}\n")
223286

224287

0 commit comments

Comments
 (0)