Sync from GitHub via hub-sync
Browse files- app/app.py +96 -2
- app/ckan_agent.py +31 -1
app/app.py
CHANGED
|
@@ -356,7 +356,7 @@ def _retrieval_stream_prefix(prompt: str, endpoint: str) -> str:
|
|
| 356 |
progress_refs = ", ".join(f"progress{index}" for index in range(1, 13))
|
| 357 |
return "\n".join(
|
| 358 |
[
|
| 359 |
-
"root = Card([header, suggestion, progress, candidates, callout, followups])",
|
| 360 |
'header = CardHeader("Finding dataset", "Searching CKAN and inspecting candidates")',
|
| 361 |
f'context = TextContent({_json_arg_safe(f"Request: {prompt} | Endpoint: {endpoint}")}, "small")',
|
| 362 |
f'progress = ListBlock([{progress_refs}], "number")',
|
|
@@ -378,6 +378,10 @@ def _agent_stream_suffix(result: AgentResult, progress_count: int) -> str:
|
|
| 378 |
lines = []
|
| 379 |
for step in range(progress_count + 1, 13):
|
| 380 |
lines.append(f'progress{step} = ListItem("waiting", "")')
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
columns = list(rows[0].keys())
|
| 382 |
for index, column in enumerate(columns):
|
| 383 |
lines.append(f'candidate_col{index + 1} = Col({_json_arg_safe(column)}, {_json_arg_safe([row.get(column) for row in rows])}, "string")')
|
|
@@ -400,6 +404,93 @@ def _agent_stream_suffix(result: AgentResult, progress_count: int) -> str:
|
|
| 400 |
return "\n".join(lines)
|
| 401 |
|
| 402 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
def _json_arg_safe(value: Any) -> str:
|
| 404 |
return json.dumps(value, ensure_ascii=False, default=str)
|
| 405 |
|
|
@@ -456,7 +547,10 @@ async def _stream_retrieval_workflow_response(
|
|
| 456 |
if item["type"] == "event":
|
| 457 |
event: AgentEvent = item["event"]
|
| 458 |
progress_count += 1
|
| 459 |
-
|
|
|
|
|
|
|
|
|
|
| 460 |
if progress_count <= 12:
|
| 461 |
line = f"progress{progress_count} = ListItem({_json_arg_safe(event.type)}, {_json_arg_safe(event.detail)})"
|
| 462 |
chunks.append(line)
|
|
|
|
| 356 |
progress_refs = ", ".join(f"progress{index}" for index in range(1, 13))
|
| 357 |
return "\n".join(
|
| 358 |
[
|
| 359 |
+
"root = Card([header, suggestion, progress, tool_details, candidates, callout, followups])",
|
| 360 |
'header = CardHeader("Finding dataset", "Searching CKAN and inspecting candidates")',
|
| 361 |
f'context = TextContent({_json_arg_safe(f"Request: {prompt} | Endpoint: {endpoint}")}, "small")',
|
| 362 |
f'progress = ListBlock([{progress_refs}], "number")',
|
|
|
|
| 378 |
lines = []
|
| 379 |
for step in range(progress_count + 1, 13):
|
| 380 |
lines.append(f'progress{step} = ListItem("waiting", "")')
|
| 381 |
+
tool_rows = _agent_tool_detail_rows(result.events)
|
| 382 |
+
for index, column in enumerate(["step", "event", "tool", "detail", "payload"]):
|
| 383 |
+
lines.append(f'tool_col{index + 1} = Col({_json_arg_safe(column)}, {_json_arg_safe([row[column] for row in tool_rows])}, "string")')
|
| 384 |
+
lines.append('tool_details = Table([tool_col1, tool_col2, tool_col3, tool_col4, tool_col5])')
|
| 385 |
columns = list(rows[0].keys())
|
| 386 |
for index, column in enumerate(columns):
|
| 387 |
lines.append(f'candidate_col{index + 1} = Col({_json_arg_safe(column)}, {_json_arg_safe([row.get(column) for row in rows])}, "string")')
|
|
|
|
| 404 |
return "\n".join(lines)
|
| 405 |
|
| 406 |
|
| 407 |
+
def _agent_tool_detail_rows(events: list[AgentEvent]) -> list[dict[str, str]]:
|
| 408 |
+
rows = []
|
| 409 |
+
for index, event in enumerate(events, start=1):
|
| 410 |
+
if event.type not in {"model_action", "tool_call", "tool_result", "selection", "retry", "error"}:
|
| 411 |
+
continue
|
| 412 |
+
rows.append(
|
| 413 |
+
{
|
| 414 |
+
"step": str(index),
|
| 415 |
+
"event": event.type,
|
| 416 |
+
"tool": _agent_event_tool_name(event),
|
| 417 |
+
"detail": _shorten(event.detail, 140),
|
| 418 |
+
"payload": _shorten(_agent_event_payload(event), 220),
|
| 419 |
+
}
|
| 420 |
+
)
|
| 421 |
+
if not rows:
|
| 422 |
+
rows.append({"step": "-", "event": "waiting", "tool": "-", "detail": "No tool calls yet.", "payload": ""})
|
| 423 |
+
return rows[:16]
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
def _agent_event_tool_name(event: AgentEvent) -> str:
|
| 427 |
+
data = event.data or {}
|
| 428 |
+
action = data.get("action")
|
| 429 |
+
if isinstance(action, dict):
|
| 430 |
+
return str(action.get("action") or action.get("tool") or event.type)
|
| 431 |
+
tool_result = data.get("tool_result")
|
| 432 |
+
if isinstance(tool_result, dict):
|
| 433 |
+
return str(tool_result.get("tool") or event.type)
|
| 434 |
+
resource = data.get("resource")
|
| 435 |
+
if isinstance(resource, dict):
|
| 436 |
+
return str(resource.get("format") or event.type)
|
| 437 |
+
return event.type
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
def _agent_event_payload(event: AgentEvent) -> str:
|
| 441 |
+
data = event.data or {}
|
| 442 |
+
action = data.get("action")
|
| 443 |
+
if isinstance(action, dict):
|
| 444 |
+
args = action.get("args")
|
| 445 |
+
confidence = action.get("confidence")
|
| 446 |
+
source = action.get("source")
|
| 447 |
+
return json.dumps({"args": args, "confidence": confidence, "source": source}, ensure_ascii=False, default=str)
|
| 448 |
+
tool_result = data.get("tool_result")
|
| 449 |
+
if isinstance(tool_result, dict):
|
| 450 |
+
summary = {
|
| 451 |
+
"ok": tool_result.get("ok"),
|
| 452 |
+
"summary": tool_result.get("summary"),
|
| 453 |
+
"data": _compact_tool_result_data(tool_result.get("data")),
|
| 454 |
+
"error": tool_result.get("error") or "",
|
| 455 |
+
}
|
| 456 |
+
return json.dumps(summary, ensure_ascii=False, default=str)
|
| 457 |
+
resource = data.get("resource")
|
| 458 |
+
if isinstance(resource, dict):
|
| 459 |
+
return json.dumps(
|
| 460 |
+
{
|
| 461 |
+
"package": resource.get("package_title"),
|
| 462 |
+
"resource": resource.get("name"),
|
| 463 |
+
"format": resource.get("format"),
|
| 464 |
+
"url": resource.get("url"),
|
| 465 |
+
},
|
| 466 |
+
ensure_ascii=False,
|
| 467 |
+
default=str,
|
| 468 |
+
)
|
| 469 |
+
if data:
|
| 470 |
+
return json.dumps(data, ensure_ascii=False, default=str)
|
| 471 |
+
return ""
|
| 472 |
+
|
| 473 |
+
|
| 474 |
+
def _compact_tool_result_data(data: Any) -> Any:
|
| 475 |
+
if not isinstance(data, dict):
|
| 476 |
+
return data
|
| 477 |
+
compact: dict[str, Any] = {}
|
| 478 |
+
for key, value in data.items():
|
| 479 |
+
if isinstance(value, list):
|
| 480 |
+
compact[key] = value[:5]
|
| 481 |
+
compact[f"{key}_count"] = len(value)
|
| 482 |
+
else:
|
| 483 |
+
compact[key] = value
|
| 484 |
+
return compact
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
def _shorten(value: Any, max_chars: int) -> str:
|
| 488 |
+
text = str(value)
|
| 489 |
+
if len(text) <= max_chars:
|
| 490 |
+
return text
|
| 491 |
+
return text[: max_chars - 1].rstrip() + "…"
|
| 492 |
+
|
| 493 |
+
|
| 494 |
def _json_arg_safe(value: Any) -> str:
|
| 495 |
return json.dumps(value, ensure_ascii=False, default=str)
|
| 496 |
|
|
|
|
| 547 |
if item["type"] == "event":
|
| 548 |
event: AgentEvent = item["event"]
|
| 549 |
progress_count += 1
|
| 550 |
+
trace_event = {"name": event.type, "detail": event.detail}
|
| 551 |
+
if event.data:
|
| 552 |
+
trace_event["data"] = event.data
|
| 553 |
+
trace_events.append(trace_event)
|
| 554 |
if progress_count <= 12:
|
| 555 |
line = f"progress{progress_count} = ListItem({_json_arg_safe(event.type)}, {_json_arg_safe(event.detail)})"
|
| 556 |
chunks.append(line)
|
app/ckan_agent.py
CHANGED
|
@@ -125,6 +125,7 @@ def run_ckan_agent(
|
|
| 125 |
valid_action, error = validate_action(action, session)
|
| 126 |
if error:
|
| 127 |
_record_event(session, AgentEvent("retry", error, {"action": asdict(action)}), on_event)
|
|
|
|
| 128 |
valid_action = fallback_action(session)
|
| 129 |
_record_event(session, AgentEvent("model_action", f"{valid_action.action}: {valid_action.reason}", {"action": asdict(valid_action)}), on_event)
|
| 130 |
|
|
@@ -220,6 +221,8 @@ def validate_action(action: AgentAction, session: AgentSession) -> tuple[AgentAc
|
|
| 220 |
rows = int(action.args.get("rows", 10) or 10)
|
| 221 |
action.args["rows"] = max(1, min(rows, 25))
|
| 222 |
if action.action in {"group_list", "organization_list"}:
|
|
|
|
|
|
|
| 223 |
rows = int(action.args.get("rows", 10) or 10)
|
| 224 |
action.args["rows"] = max(1, min(rows, 25))
|
| 225 |
if action.action == "package_show":
|
|
@@ -431,7 +434,34 @@ def _append_tool_result(session: AgentSession, action: AgentAction, result: Tool
|
|
| 431 |
session.messages.append(
|
| 432 |
{
|
| 433 |
"role": "user",
|
| 434 |
-
"content": "Tool observation:\n" + json.dumps(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
}
|
| 436 |
)
|
| 437 |
|
|
|
|
| 125 |
valid_action, error = validate_action(action, session)
|
| 126 |
if error:
|
| 127 |
_record_event(session, AgentEvent("retry", error, {"action": asdict(action)}), on_event)
|
| 128 |
+
_append_validation_error(session, action, error)
|
| 129 |
valid_action = fallback_action(session)
|
| 130 |
_record_event(session, AgentEvent("model_action", f"{valid_action.action}: {valid_action.reason}", {"action": asdict(valid_action)}), on_event)
|
| 131 |
|
|
|
|
| 221 |
rows = int(action.args.get("rows", 10) or 10)
|
| 222 |
action.args["rows"] = max(1, min(rows, 25))
|
| 223 |
if action.action in {"group_list", "organization_list"}:
|
| 224 |
+
if action.action in session.catalog_tools_run:
|
| 225 |
+
return action, f"{action.action} already ran in this run; choose tag_search, package_search, package_show, or ask_clarification instead."
|
| 226 |
rows = int(action.args.get("rows", 10) or 10)
|
| 227 |
action.args["rows"] = max(1, min(rows, 25))
|
| 228 |
if action.action == "package_show":
|
|
|
|
| 434 |
session.messages.append(
|
| 435 |
{
|
| 436 |
"role": "user",
|
| 437 |
+
"content": "Tool observation:\n" + json.dumps(
|
| 438 |
+
{
|
| 439 |
+
"action": asdict(action),
|
| 440 |
+
"result": asdict(result),
|
| 441 |
+
"catalog_tools_already_run": sorted(session.catalog_tools_run),
|
| 442 |
+
"instruction": "Do not repeat catalog discovery tools that already ran. Choose the next useful tool.",
|
| 443 |
+
},
|
| 444 |
+
ensure_ascii=False,
|
| 445 |
+
default=str,
|
| 446 |
+
),
|
| 447 |
+
}
|
| 448 |
+
)
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
def _append_validation_error(session: AgentSession, action: AgentAction, error: str) -> None:
|
| 452 |
+
session.messages.append(
|
| 453 |
+
{
|
| 454 |
+
"role": "user",
|
| 455 |
+
"content": "Rejected model action:\n" + json.dumps(
|
| 456 |
+
{
|
| 457 |
+
"action": asdict(action),
|
| 458 |
+
"error": error,
|
| 459 |
+
"catalog_tools_already_run": sorted(session.catalog_tools_run),
|
| 460 |
+
"instruction": "Choose a different valid action. Do not repeat rejected actions.",
|
| 461 |
+
},
|
| 462 |
+
ensure_ascii=False,
|
| 463 |
+
default=str,
|
| 464 |
+
),
|
| 465 |
}
|
| 466 |
)
|
| 467 |
|