praisonai-code 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- praisonai_code/__init__.py +17 -0
- praisonai_code/cli/__init__.py +12 -0
- praisonai_code/cli/_forward_shim.py +10 -0
- praisonai_code/cli/_paths.py +88 -0
- praisonai_code/cli/_warnings.py +58 -0
- praisonai_code/cli/app.py +757 -0
- praisonai_code/cli/approval_backend.py +272 -0
- praisonai_code/cli/branding.py +94 -0
- praisonai_code/cli/commands/__init__.py +114 -0
- praisonai_code/cli/commands/acp.py +80 -0
- praisonai_code/cli/commands/agent.py +116 -0
- praisonai_code/cli/commands/agents.py +80 -0
- praisonai_code/cli/commands/app.py +139 -0
- praisonai_code/cli/commands/attach.py +95 -0
- praisonai_code/cli/commands/audit.py +102 -0
- praisonai_code/cli/commands/auth.py +508 -0
- praisonai_code/cli/commands/batch.py +848 -0
- praisonai_code/cli/commands/benchmark.py +286 -0
- praisonai_code/cli/commands/browser.py +299 -0
- praisonai_code/cli/commands/call.py +45 -0
- praisonai_code/cli/commands/chat.py +332 -0
- praisonai_code/cli/commands/checkpoint.py +170 -0
- praisonai_code/cli/commands/code.py +276 -0
- praisonai_code/cli/commands/command.py +114 -0
- praisonai_code/cli/commands/commit.py +47 -0
- praisonai_code/cli/commands/completion.py +333 -0
- praisonai_code/cli/commands/config.py +681 -0
- praisonai_code/cli/commands/context.py +414 -0
- praisonai_code/cli/commands/daemon.py +203 -0
- praisonai_code/cli/commands/debug.py +142 -0
- praisonai_code/cli/commands/deploy.py +71 -0
- praisonai_code/cli/commands/diag.py +55 -0
- praisonai_code/cli/commands/docs.py +1575 -0
- praisonai_code/cli/commands/doctor.py +332 -0
- praisonai_code/cli/commands/endpoints.py +51 -0
- praisonai_code/cli/commands/environment.py +179 -0
- praisonai_code/cli/commands/eval.py +131 -0
- praisonai_code/cli/commands/examples.py +953 -0
- praisonai_code/cli/commands/flow.py +436 -0
- praisonai_code/cli/commands/github.py +752 -0
- praisonai_code/cli/commands/hooks.py +74 -0
- praisonai_code/cli/commands/init.py +174 -0
- praisonai_code/cli/commands/knowledge.py +440 -0
- praisonai_code/cli/commands/langextract.py +120 -0
- praisonai_code/cli/commands/langfuse.py +984 -0
- praisonai_code/cli/commands/loop.py +211 -0
- praisonai_code/cli/commands/lsp.py +112 -0
- praisonai_code/cli/commands/managed.py +659 -0
- praisonai_code/cli/commands/mcp.py +763 -0
- praisonai_code/cli/commands/memory.py +298 -0
- praisonai_code/cli/commands/models.py +264 -0
- praisonai_code/cli/commands/n8n.py +326 -0
- praisonai_code/cli/commands/obs.py +19 -0
- praisonai_code/cli/commands/package.py +76 -0
- praisonai_code/cli/commands/paths.py +106 -0
- praisonai_code/cli/commands/permissions.py +272 -0
- praisonai_code/cli/commands/plugins.py +609 -0
- praisonai_code/cli/commands/port.py +530 -0
- praisonai_code/cli/commands/profile.py +466 -0
- praisonai_code/cli/commands/publish.py +193 -0
- praisonai_code/cli/commands/rag.py +913 -0
- praisonai_code/cli/commands/realtime.py +52 -0
- praisonai_code/cli/commands/recipe.py +684 -0
- praisonai_code/cli/commands/registry.py +59 -0
- praisonai_code/cli/commands/replay.py +830 -0
- praisonai_code/cli/commands/research.py +49 -0
- praisonai_code/cli/commands/retrieval.py +377 -0
- praisonai_code/cli/commands/rules.py +71 -0
- praisonai_code/cli/commands/run.py +1573 -0
- praisonai_code/cli/commands/sandbox.py +371 -0
- praisonai_code/cli/commands/schedule.py +529 -0
- praisonai_code/cli/commands/serve.py +690 -0
- praisonai_code/cli/commands/session.py +450 -0
- praisonai_code/cli/commands/setup.py +174 -0
- praisonai_code/cli/commands/skills.py +545 -0
- praisonai_code/cli/commands/standardise.py +711 -0
- praisonai_code/cli/commands/templates.py +54 -0
- praisonai_code/cli/commands/test.py +558 -0
- praisonai_code/cli/commands/todo.py +74 -0
- praisonai_code/cli/commands/tools.py +205 -0
- praisonai_code/cli/commands/traces.py +145 -0
- praisonai_code/cli/commands/tracker.py +852 -0
- praisonai_code/cli/commands/train.py +613 -0
- praisonai_code/cli/commands/ui.py +172 -0
- praisonai_code/cli/commands/up.py +354 -0
- praisonai_code/cli/commands/validate.py +291 -0
- praisonai_code/cli/commands/version.py +101 -0
- praisonai_code/cli/commands/workflow.py +97 -0
- praisonai_code/cli/config_loader.py +437 -0
- praisonai_code/cli/configuration/__init__.py +27 -0
- praisonai_code/cli/configuration/config.schema.json +57 -0
- praisonai_code/cli/configuration/credentials.py +446 -0
- praisonai_code/cli/configuration/loader.py +364 -0
- praisonai_code/cli/configuration/model_resolver.py +161 -0
- praisonai_code/cli/configuration/oauth.py +389 -0
- praisonai_code/cli/configuration/paths.py +224 -0
- praisonai_code/cli/configuration/resolver.py +687 -0
- praisonai_code/cli/configuration/schema.py +317 -0
- praisonai_code/cli/execution/__init__.py +99 -0
- praisonai_code/cli/execution/core.py +208 -0
- praisonai_code/cli/execution/profiler.py +898 -0
- praisonai_code/cli/execution/request.py +85 -0
- praisonai_code/cli/execution/result.py +74 -0
- praisonai_code/cli/fallback_schema.py +416 -0
- praisonai_code/cli/features/__init__.py +278 -0
- praisonai_code/cli/features/_endpoint_registry.py +64 -0
- praisonai_code/cli/features/_search_registry.py +43 -0
- praisonai_code/cli/features/acp.py +236 -0
- praisonai_code/cli/features/action_orchestrator.py +576 -0
- praisonai_code/cli/features/agent_scheduler.py +773 -0
- praisonai_code/cli/features/agent_tools.py +603 -0
- praisonai_code/cli/features/agents.py +397 -0
- praisonai_code/cli/features/at_mentions.py +471 -0
- praisonai_code/cli/features/audit_cli.py +270 -0
- praisonai_code/cli/features/auto_memory.py +182 -0
- praisonai_code/cli/features/auto_mode.py +552 -0
- praisonai_code/cli/features/autonomy_mode.py +546 -0
- praisonai_code/cli/features/background.py +356 -0
- praisonai_code/cli/features/base.py +168 -0
- praisonai_code/cli/features/benchmark.py +1462 -0
- praisonai_code/cli/features/capabilities.py +1326 -0
- praisonai_code/cli/features/checkpoints.py +345 -0
- praisonai_code/cli/features/cli_profiler.py +335 -0
- praisonai_code/cli/features/code_intelligence.py +666 -0
- praisonai_code/cli/features/compaction.py +294 -0
- praisonai_code/cli/features/compare.py +534 -0
- praisonai_code/cli/features/config_hierarchy.py +366 -0
- praisonai_code/cli/features/context_manager.py +597 -0
- praisonai_code/cli/features/cost_tracker.py +514 -0
- praisonai_code/cli/features/csv_test_runner.py +736 -0
- praisonai_code/cli/features/custom_definitions.py +790 -0
- praisonai_code/cli/features/debug.py +810 -0
- praisonai_code/cli/features/deploy.py +605 -0
- praisonai_code/cli/features/diag.py +289 -0
- praisonai_code/cli/features/display_jsonl.py +173 -0
- praisonai_code/cli/features/doctor/__init__.py +63 -0
- praisonai_code/cli/features/doctor/checks/__init__.py +29 -0
- praisonai_code/cli/features/doctor/checks/acp_checks.py +220 -0
- praisonai_code/cli/features/doctor/checks/bot_checks.py +340 -0
- praisonai_code/cli/features/doctor/checks/config_checks.py +373 -0
- praisonai_code/cli/features/doctor/checks/db_checks.py +366 -0
- praisonai_code/cli/features/doctor/checks/env_checks.py +637 -0
- praisonai_code/cli/features/doctor/checks/gateway_checks.py +387 -0
- praisonai_code/cli/features/doctor/checks/lsp_checks.py +231 -0
- praisonai_code/cli/features/doctor/checks/mcp_checks.py +367 -0
- praisonai_code/cli/features/doctor/checks/memory_checks.py +268 -0
- praisonai_code/cli/features/doctor/checks/network_checks.py +251 -0
- praisonai_code/cli/features/doctor/checks/obs_checks.py +328 -0
- praisonai_code/cli/features/doctor/checks/packaging_checks.py +422 -0
- praisonai_code/cli/features/doctor/checks/performance_checks.py +235 -0
- praisonai_code/cli/features/doctor/checks/permissions_checks.py +259 -0
- praisonai_code/cli/features/doctor/checks/runtime_checks.py +650 -0
- praisonai_code/cli/features/doctor/checks/runtime_migration_checks.py +220 -0
- praisonai_code/cli/features/doctor/checks/selftest_checks.py +322 -0
- praisonai_code/cli/features/doctor/checks/serve_checks.py +426 -0
- praisonai_code/cli/features/doctor/checks/skills_checks.py +327 -0
- praisonai_code/cli/features/doctor/checks/tools_checks.py +371 -0
- praisonai_code/cli/features/doctor/engine.py +266 -0
- praisonai_code/cli/features/doctor/formatters.py +377 -0
- praisonai_code/cli/features/doctor/handler.py +564 -0
- praisonai_code/cli/features/doctor/models.py +276 -0
- praisonai_code/cli/features/doctor/registry.py +239 -0
- praisonai_code/cli/features/endpoints.py +1016 -0
- praisonai_code/cli/features/eval.py +559 -0
- praisonai_code/cli/features/examples.py +707 -0
- praisonai_code/cli/features/external_agents.py +231 -0
- praisonai_code/cli/features/fast_context.py +410 -0
- praisonai_code/cli/features/file_history.py +320 -0
- praisonai_code/cli/features/flow_display.py +566 -0
- praisonai_code/cli/features/git_attribution.py +159 -0
- praisonai_code/cli/features/git_integration.py +651 -0
- praisonai_code/cli/features/guardrail.py +171 -0
- praisonai_code/cli/features/handoff.py +252 -0
- praisonai_code/cli/features/hooks.py +583 -0
- praisonai_code/cli/features/hybrid_workflow.py +391 -0
- praisonai_code/cli/features/image.py +384 -0
- praisonai_code/cli/features/interactive_core_headless.py +450 -0
- praisonai_code/cli/features/interactive_runtime.py +600 -0
- praisonai_code/cli/features/interactive_test_harness.py +537 -0
- praisonai_code/cli/features/interactive_tools.py +428 -0
- praisonai_code/cli/features/interactive_tui.py +603 -0
- praisonai_code/cli/features/job_workflow.py +906 -0
- praisonai_code/cli/features/jobs.py +632 -0
- praisonai_code/cli/features/knowledge.py +531 -0
- praisonai_code/cli/features/knowledge_cli.py +438 -0
- praisonai_code/cli/features/lite.py +244 -0
- praisonai_code/cli/features/logs.py +200 -0
- praisonai_code/cli/features/lsp_cli.py +225 -0
- praisonai_code/cli/features/lsp_diagnostics.py +185 -0
- praisonai_code/cli/features/mcp.py +344 -0
- praisonai_code/cli/features/message_queue.py +587 -0
- praisonai_code/cli/features/metrics.py +210 -0
- praisonai_code/cli/features/migrate.py +1329 -0
- praisonai_code/cli/features/migration_flow.py +463 -0
- praisonai_code/cli/features/migration_spec.py +276 -0
- praisonai_code/cli/features/n8n.py +703 -0
- praisonai_code/cli/features/observability.py +293 -0
- praisonai_code/cli/features/ollama.py +361 -0
- praisonai_code/cli/features/output_modes.py +155 -0
- praisonai_code/cli/features/output_style.py +273 -0
- praisonai_code/cli/features/package.py +631 -0
- praisonai_code/cli/features/performance.py +308 -0
- praisonai_code/cli/features/persistence.py +636 -0
- praisonai_code/cli/features/profiler/__init__.py +81 -0
- praisonai_code/cli/features/profiler/core.py +558 -0
- praisonai_code/cli/features/profiler/optimizations.py +652 -0
- praisonai_code/cli/features/profiler/suite.py +386 -0
- praisonai_code/cli/features/queue/__init__.py +73 -0
- praisonai_code/cli/features/queue/manager.py +435 -0
- praisonai_code/cli/features/queue/models.py +289 -0
- praisonai_code/cli/features/queue/persistence.py +564 -0
- praisonai_code/cli/features/queue/scheduler.py +529 -0
- praisonai_code/cli/features/queue/worker.py +400 -0
- praisonai_code/cli/features/recipe.py +2187 -0
- praisonai_code/cli/features/recipe_creator.py +996 -0
- praisonai_code/cli/features/recipe_optimizer.py +1364 -0
- praisonai_code/cli/features/recipe_prompts.py +226 -0
- praisonai_code/cli/features/registry.py +229 -0
- praisonai_code/cli/features/repo_map.py +860 -0
- praisonai_code/cli/features/router.py +466 -0
- praisonai_code/cli/features/safe_shell.py +427 -0
- praisonai_code/cli/features/sandbox_cli.py +283 -0
- praisonai_code/cli/features/sandbox_executor.py +536 -0
- praisonai_code/cli/features/sdk_knowledge.py +500 -0
- praisonai_code/cli/features/session.py +222 -0
- praisonai_code/cli/features/session_checkpoints.py +208 -0
- praisonai_code/cli/features/setup/__init__.py +9 -0
- praisonai_code/cli/features/setup/handler.py +355 -0
- praisonai_code/cli/features/setup/templates.py +62 -0
- praisonai_code/cli/features/skills.py +940 -0
- praisonai_code/cli/features/slash_commands.py +692 -0
- praisonai_code/cli/features/telemetry.py +179 -0
- praisonai_code/cli/features/templates.py +1390 -0
- praisonai_code/cli/features/thinking.py +343 -0
- praisonai_code/cli/features/todo.py +334 -0
- praisonai_code/cli/features/tools.py +680 -0
- praisonai_code/cli/features/tui/__init__.py +83 -0
- praisonai_code/cli/features/tui/app.py +871 -0
- praisonai_code/cli/features/tui/cli.py +580 -0
- praisonai_code/cli/features/tui/config.py +150 -0
- praisonai_code/cli/features/tui/debug.py +526 -0
- praisonai_code/cli/features/tui/events.py +99 -0
- praisonai_code/cli/features/tui/mock_provider.py +328 -0
- praisonai_code/cli/features/tui/orchestrator.py +652 -0
- praisonai_code/cli/features/tui/screens/__init__.py +50 -0
- praisonai_code/cli/features/tui/screens/help.py +157 -0
- praisonai_code/cli/features/tui/screens/main.py +568 -0
- praisonai_code/cli/features/tui/screens/queue.py +174 -0
- praisonai_code/cli/features/tui/screens/session.py +124 -0
- praisonai_code/cli/features/tui/screens/settings.py +148 -0
- praisonai_code/cli/features/tui/session_store.py +198 -0
- praisonai_code/cli/features/tui/widgets/__init__.py +56 -0
- praisonai_code/cli/features/tui/widgets/chat.py +263 -0
- praisonai_code/cli/features/tui/widgets/command_popup.py +258 -0
- praisonai_code/cli/features/tui/widgets/composer.py +292 -0
- praisonai_code/cli/features/tui/widgets/file_popup.py +207 -0
- praisonai_code/cli/features/tui/widgets/queue_panel.py +223 -0
- praisonai_code/cli/features/tui/widgets/status.py +181 -0
- praisonai_code/cli/features/tui/widgets/tool_panel.py +307 -0
- praisonai_code/cli/features/wizard.py +289 -0
- praisonai_code/cli/features/workflow.py +802 -0
- praisonai_code/cli/features/yaml_utils.py +321 -0
- praisonai_code/cli/interactive/__init__.py +48 -0
- praisonai_code/cli/interactive/async_tui.py +1218 -0
- praisonai_code/cli/interactive/config.py +139 -0
- praisonai_code/cli/interactive/core.py +618 -0
- praisonai_code/cli/interactive/events.py +131 -0
- praisonai_code/cli/interactive/frontends/__init__.py +31 -0
- praisonai_code/cli/interactive/frontends/rich_frontend.py +462 -0
- praisonai_code/cli/interactive/frontends/textual_frontend.py +157 -0
- praisonai_code/cli/interactive/praison_io.py +502 -0
- praisonai_code/cli/interactive/repl.py +297 -0
- praisonai_code/cli/interactive/split_tui.py +456 -0
- praisonai_code/cli/interactive/tui_app.py +457 -0
- praisonai_code/cli/langfuse_client.py +360 -0
- praisonai_code/cli/main.py +7421 -0
- praisonai_code/cli/output/__init__.py +25 -0
- praisonai_code/cli/output/console.py +456 -0
- praisonai_code/cli/output/event_bridge.py +191 -0
- praisonai_code/cli/schedule_cli.py +54 -0
- praisonai_code/cli/schema_provider.py +23 -0
- praisonai_code/cli/session/__init__.py +16 -0
- praisonai_code/cli/session/resume.py +148 -0
- praisonai_code/cli/session/unified.py +548 -0
- praisonai_code/cli/state/__init__.py +31 -0
- praisonai_code/cli/state/identifiers.py +161 -0
- praisonai_code/cli/state/project_sessions.py +383 -0
- praisonai_code/cli/state/sessions.py +390 -0
- praisonai_code/cli/ui/__init__.py +160 -0
- praisonai_code/cli/ui/config.py +46 -0
- praisonai_code/cli/ui/events.py +61 -0
- praisonai_code/cli/ui/mg_backend.py +342 -0
- praisonai_code/cli/ui/plain.py +133 -0
- praisonai_code/cli/ui/rich_backend.py +162 -0
- praisonai_code/cli/unified_schema.py +655 -0
- praisonai_code/cli/utils/env_utils.py +126 -0
- praisonai_code/cli/utils/project.py +131 -0
- praisonai_code/cli_backends/__init__.py +73 -0
- praisonai_code/cli_backends/claude.py +373 -0
- praisonai_code/cli_backends/registry.py +113 -0
- praisonai_code/runtime/__init__.py +36 -0
- praisonai_code/runtime/__main__.py +81 -0
- praisonai_code/runtime/client.py +131 -0
- praisonai_code/runtime/descriptor.py +209 -0
- praisonai_code/runtime/server.py +356 -0
- praisonai_code-0.0.1.dist-info/METADATA +80 -0
- praisonai_code-0.0.1.dist-info/RECORD +309 -0
- praisonai_code-0.0.1.dist-info/WHEEL +5 -0
- praisonai_code-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Replay command group for PraisonAI CLI.
|
|
3
|
+
|
|
4
|
+
Provides context replay functionality:
|
|
5
|
+
- replay list: List available traces
|
|
6
|
+
- replay context <session_id>: Interactive replay of a trace
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="Context replay for debugging agent execution")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command("list")
|
|
17
|
+
def replay_list(
|
|
18
|
+
limit: int = typer.Option(
|
|
19
|
+
20,
|
|
20
|
+
"--limit",
|
|
21
|
+
"-n",
|
|
22
|
+
help="Maximum number of traces to show",
|
|
23
|
+
),
|
|
24
|
+
json_output: bool = typer.Option(
|
|
25
|
+
False,
|
|
26
|
+
"--json",
|
|
27
|
+
"-j",
|
|
28
|
+
help="Output as JSON",
|
|
29
|
+
),
|
|
30
|
+
):
|
|
31
|
+
"""List available context traces."""
|
|
32
|
+
from ..output.console import get_output_controller
|
|
33
|
+
|
|
34
|
+
output = get_output_controller()
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
from praisonai.replay import list_traces
|
|
38
|
+
traces = list_traces(limit=limit)
|
|
39
|
+
except ImportError:
|
|
40
|
+
output.print_error("Replay module not available")
|
|
41
|
+
raise typer.Exit(1)
|
|
42
|
+
|
|
43
|
+
if json_output or output.is_json_mode:
|
|
44
|
+
output.print_json({
|
|
45
|
+
"traces": [t.to_dict() for t in traces]
|
|
46
|
+
})
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
if not traces:
|
|
50
|
+
output.print_info("No traces found. Run agents with trace=True to create traces.")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
# Format as table
|
|
54
|
+
headers = ["Session ID", "Events", "Size", "Modified"]
|
|
55
|
+
rows = []
|
|
56
|
+
|
|
57
|
+
for trace in traces:
|
|
58
|
+
modified = trace.modified_at.strftime("%Y-%m-%d %H:%M")
|
|
59
|
+
size_kb = trace.size_bytes / 1024
|
|
60
|
+
size_str = f"{size_kb:.1f} KB" if size_kb >= 1 else f"{trace.size_bytes} B"
|
|
61
|
+
|
|
62
|
+
rows.append([
|
|
63
|
+
trace.session_id[:40] + "..." if len(trace.session_id) > 40 else trace.session_id,
|
|
64
|
+
str(trace.event_count),
|
|
65
|
+
size_str,
|
|
66
|
+
modified,
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
output.print_table(headers, rows, title="Available Context Traces")
|
|
70
|
+
output.print("\nUse 'praisonai replay context <session_id>' to replay a trace.")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command("context")
|
|
74
|
+
def replay_context(
|
|
75
|
+
session_id: str = typer.Argument(
|
|
76
|
+
...,
|
|
77
|
+
help="Session ID or path to trace file",
|
|
78
|
+
),
|
|
79
|
+
start: int = typer.Option(
|
|
80
|
+
0,
|
|
81
|
+
"--start",
|
|
82
|
+
"-s",
|
|
83
|
+
help="Start from event number (1-based)",
|
|
84
|
+
),
|
|
85
|
+
no_rich: bool = typer.Option(
|
|
86
|
+
False,
|
|
87
|
+
"--no-rich",
|
|
88
|
+
help="Disable Rich formatting",
|
|
89
|
+
),
|
|
90
|
+
dump: bool = typer.Option(
|
|
91
|
+
False,
|
|
92
|
+
"--dump",
|
|
93
|
+
"-d",
|
|
94
|
+
help="Dump all events non-interactively (for scripting)",
|
|
95
|
+
),
|
|
96
|
+
json_output: bool = typer.Option(
|
|
97
|
+
False,
|
|
98
|
+
"--json",
|
|
99
|
+
"-j",
|
|
100
|
+
help="Output as JSON (implies --dump)",
|
|
101
|
+
),
|
|
102
|
+
full: bool = typer.Option(
|
|
103
|
+
False,
|
|
104
|
+
"--full",
|
|
105
|
+
"-f",
|
|
106
|
+
help="Show full context content (messages, responses) without truncation",
|
|
107
|
+
),
|
|
108
|
+
stats: bool = typer.Option(
|
|
109
|
+
False,
|
|
110
|
+
"--stats",
|
|
111
|
+
help="Show session statistics (tokens, costs, duplicates)",
|
|
112
|
+
),
|
|
113
|
+
cost: bool = typer.Option(
|
|
114
|
+
False,
|
|
115
|
+
"--cost",
|
|
116
|
+
help="Show cost breakdown only",
|
|
117
|
+
),
|
|
118
|
+
):
|
|
119
|
+
"""Interactive replay of a context trace.
|
|
120
|
+
|
|
121
|
+
Use --dump for non-interactive output (useful for scripting/verification).
|
|
122
|
+
Use --json for machine-readable output.
|
|
123
|
+
Use --full to show complete context content at each stage.
|
|
124
|
+
Use --stats to show session statistics (tokens, costs, duplicates).
|
|
125
|
+
Use --cost to show cost breakdown only.
|
|
126
|
+
"""
|
|
127
|
+
from ..output.console import get_output_controller
|
|
128
|
+
|
|
129
|
+
output = get_output_controller()
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
from praisonai.replay import ContextTraceReader, ReplayPlayer
|
|
133
|
+
from praisonai.replay.analyzer import TraceAnalyzer, format_stats_output, format_cost_output
|
|
134
|
+
except ImportError:
|
|
135
|
+
output.print_error("Replay module not available")
|
|
136
|
+
raise typer.Exit(1)
|
|
137
|
+
|
|
138
|
+
# Load trace
|
|
139
|
+
reader = ContextTraceReader(session_id)
|
|
140
|
+
|
|
141
|
+
if not reader.exists:
|
|
142
|
+
output.print_error(f"Trace not found: {session_id}")
|
|
143
|
+
output.print_info("Use 'praisonai replay list' to see available traces.")
|
|
144
|
+
raise typer.Exit(1)
|
|
145
|
+
|
|
146
|
+
events = reader.get_all()
|
|
147
|
+
if not events:
|
|
148
|
+
output.print_error(f"No events in trace: {session_id}")
|
|
149
|
+
raise typer.Exit(1)
|
|
150
|
+
|
|
151
|
+
# Stats mode - show statistics only
|
|
152
|
+
if stats:
|
|
153
|
+
analyzer = TraceAnalyzer(events)
|
|
154
|
+
result = analyzer.analyze()
|
|
155
|
+
if json_output:
|
|
156
|
+
output.print_json(result.to_dict())
|
|
157
|
+
else:
|
|
158
|
+
output.print(format_stats_output(result))
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
# Cost mode - show cost breakdown only
|
|
162
|
+
if cost:
|
|
163
|
+
analyzer = TraceAnalyzer(events)
|
|
164
|
+
result = analyzer.analyze()
|
|
165
|
+
if json_output:
|
|
166
|
+
output.print_json({"cost": result.to_dict()["cost_stats"]})
|
|
167
|
+
else:
|
|
168
|
+
output.print(format_cost_output(result))
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
# JSON output implies dump mode
|
|
172
|
+
if json_output:
|
|
173
|
+
dump = True
|
|
174
|
+
|
|
175
|
+
# Non-interactive dump mode
|
|
176
|
+
if dump:
|
|
177
|
+
_dump_events(output, events, session_id, json_output, full)
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
output.print_success(f"Loaded {len(events)} events from: {session_id}")
|
|
181
|
+
|
|
182
|
+
# Create and run player with full mode support
|
|
183
|
+
player = ReplayPlayer(reader, use_rich=not no_rich, full_mode=full)
|
|
184
|
+
|
|
185
|
+
# Set start position if specified
|
|
186
|
+
if start > 0:
|
|
187
|
+
if player.goto(start - 1): # Convert to 0-based
|
|
188
|
+
output.print(f"Starting from event {start}")
|
|
189
|
+
else:
|
|
190
|
+
output.print_warning(f"Invalid start position. Valid range: 1-{len(events)}")
|
|
191
|
+
|
|
192
|
+
player.run()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _dump_events(output, events, session_id: str, json_output: bool, full: bool = False):
|
|
196
|
+
"""Dump all events non-interactively.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
output: Output controller
|
|
200
|
+
events: List of trace events
|
|
201
|
+
session_id: Session identifier
|
|
202
|
+
json_output: Whether to output as JSON
|
|
203
|
+
full: Whether to show full content without truncation
|
|
204
|
+
"""
|
|
205
|
+
# Import analyzer for stats
|
|
206
|
+
from praisonai.replay.analyzer import TraceAnalyzer
|
|
207
|
+
|
|
208
|
+
if json_output or output.is_json_mode:
|
|
209
|
+
# Include analysis in JSON output
|
|
210
|
+
analyzer = TraceAnalyzer(events)
|
|
211
|
+
result = analyzer.analyze()
|
|
212
|
+
output.print_json({
|
|
213
|
+
"session_id": session_id,
|
|
214
|
+
"total_events": len(events),
|
|
215
|
+
"analysis": result.to_dict(),
|
|
216
|
+
"events": [e.to_dict() if hasattr(e, 'to_dict') else e for e in events],
|
|
217
|
+
})
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
# Text output - formatted for readability
|
|
221
|
+
output.print(f"\n{'='*60}")
|
|
222
|
+
output.print(f" CONTEXT REPLAY: {session_id}")
|
|
223
|
+
output.print(f" Total Events: {len(events)}")
|
|
224
|
+
output.print(f"{'='*60}\n")
|
|
225
|
+
|
|
226
|
+
# Track running totals
|
|
227
|
+
running_prompt_tokens = 0
|
|
228
|
+
running_completion_tokens = 0
|
|
229
|
+
running_cost = 0.0
|
|
230
|
+
|
|
231
|
+
for i, event in enumerate(events):
|
|
232
|
+
# Get event details
|
|
233
|
+
if hasattr(event, 'event_type'):
|
|
234
|
+
et = event.event_type
|
|
235
|
+
if hasattr(et, 'value'):
|
|
236
|
+
et = et.value
|
|
237
|
+
else:
|
|
238
|
+
et = event.get('event_type', 'unknown')
|
|
239
|
+
|
|
240
|
+
agent_name = getattr(event, 'agent_name', None) or (event.get('agent_name') if isinstance(event, dict) else None)
|
|
241
|
+
seq = getattr(event, 'sequence_num', i) or (event.get('sequence_num', i) if isinstance(event, dict) else i)
|
|
242
|
+
branch_id = getattr(event, 'branch_id', None) or (event.get('branch_id') if isinstance(event, dict) else None)
|
|
243
|
+
|
|
244
|
+
# Get token/cost info
|
|
245
|
+
prompt_tokens = getattr(event, 'prompt_tokens', 0) or (event.get('prompt_tokens', 0) if isinstance(event, dict) else 0)
|
|
246
|
+
completion_tokens = getattr(event, 'completion_tokens', 0) or (event.get('completion_tokens', 0) if isinstance(event, dict) else 0)
|
|
247
|
+
cost_usd = getattr(event, 'cost_usd', 0) or (event.get('cost_usd', 0) if isinstance(event, dict) else 0)
|
|
248
|
+
|
|
249
|
+
# Update running totals
|
|
250
|
+
running_prompt_tokens += prompt_tokens
|
|
251
|
+
running_completion_tokens += completion_tokens
|
|
252
|
+
running_cost += cost_usd
|
|
253
|
+
|
|
254
|
+
# Format event header
|
|
255
|
+
header = f"[{seq + 1:3d}] {et.upper()}"
|
|
256
|
+
if agent_name:
|
|
257
|
+
header += f" ({agent_name})"
|
|
258
|
+
if branch_id:
|
|
259
|
+
header += f" [branch: {branch_id}]"
|
|
260
|
+
|
|
261
|
+
output.print(f"\n{'-'*50}")
|
|
262
|
+
output.print(header)
|
|
263
|
+
|
|
264
|
+
# Show token/cost info for LLM events
|
|
265
|
+
if prompt_tokens or completion_tokens:
|
|
266
|
+
output.print(f" prompt_tokens: {prompt_tokens:,}")
|
|
267
|
+
output.print(f" completion_tokens: {completion_tokens:,}")
|
|
268
|
+
if cost_usd:
|
|
269
|
+
output.print(f" cost: ${cost_usd:.6f}")
|
|
270
|
+
|
|
271
|
+
# Show event-specific data
|
|
272
|
+
data = getattr(event, 'data', None) or (event.get('data') if isinstance(event, dict) else None)
|
|
273
|
+
if data:
|
|
274
|
+
for key, value in data.items():
|
|
275
|
+
if value is not None:
|
|
276
|
+
value_str = str(value)
|
|
277
|
+
# Full mode: show complete content; otherwise truncate
|
|
278
|
+
if not full and len(value_str) > 100:
|
|
279
|
+
value_str = value_str[:100] + "..."
|
|
280
|
+
|
|
281
|
+
# For messages array in full mode, format nicely
|
|
282
|
+
if full and key == "messages" and isinstance(value, list):
|
|
283
|
+
output.print(f" {key}:")
|
|
284
|
+
for msg_idx, msg in enumerate(value):
|
|
285
|
+
role = msg.get("role", "unknown") if isinstance(msg, dict) else "unknown"
|
|
286
|
+
content = msg.get("content", "") if isinstance(msg, dict) else str(msg)
|
|
287
|
+
output.print(f" [{msg_idx + 1}] {role}:")
|
|
288
|
+
# Indent content lines
|
|
289
|
+
for line in str(content).split('\n')[:20]: # Limit lines in full mode
|
|
290
|
+
output.print(f" {line}")
|
|
291
|
+
if len(str(content).split('\n')) > 20:
|
|
292
|
+
output.print(f" ... ({len(str(content).split(chr(10)))} lines total)")
|
|
293
|
+
else:
|
|
294
|
+
output.print(f" {key}: {value_str}")
|
|
295
|
+
|
|
296
|
+
# Show token info if available (legacy field)
|
|
297
|
+
tokens_used = getattr(event, 'tokens_used', 0) or (event.get('tokens_used', 0) if isinstance(event, dict) else 0)
|
|
298
|
+
if tokens_used and not prompt_tokens and not completion_tokens:
|
|
299
|
+
output.print(f" tokens_used: {tokens_used}")
|
|
300
|
+
|
|
301
|
+
# Show summary with running totals
|
|
302
|
+
output.print(f"\n{'='*60}")
|
|
303
|
+
output.print(f" SESSION SUMMARY")
|
|
304
|
+
output.print(f"{'='*60}")
|
|
305
|
+
output.print(f" Total Events: {len(events)}")
|
|
306
|
+
output.print(f" Prompt Tokens: {running_prompt_tokens:,}")
|
|
307
|
+
output.print(f" Completion Tokens: {running_completion_tokens:,}")
|
|
308
|
+
output.print(f" Total Tokens: {running_prompt_tokens + running_completion_tokens:,}")
|
|
309
|
+
output.print(f" Total Cost: ${running_cost:.6f}")
|
|
310
|
+
|
|
311
|
+
# Show duplicate analysis if in full mode
|
|
312
|
+
if full:
|
|
313
|
+
analyzer = TraceAnalyzer(events)
|
|
314
|
+
result = analyzer.analyze()
|
|
315
|
+
if result.duplicates:
|
|
316
|
+
output.print(f"\n DUPLICATE CONTENT DETECTED:")
|
|
317
|
+
output.print(f" Duplicates Found: {result.duplicate_count}")
|
|
318
|
+
output.print(f" Wasted Tokens: {result.total_wasted_tokens:,}")
|
|
319
|
+
for dup in result.duplicates[:3]:
|
|
320
|
+
output.print(f" - Hash {dup.content_hash[:12]}... ({dup.duplicate_count}x, ~{dup.wasted_tokens:,} wasted)")
|
|
321
|
+
else:
|
|
322
|
+
output.print(f"\n CONTEXT EFFICIENCY: No duplicates detected ✓")
|
|
323
|
+
|
|
324
|
+
output.print(f"{'='*60}\n")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@app.command("show")
|
|
328
|
+
def replay_show(
|
|
329
|
+
session_id: str = typer.Argument(
|
|
330
|
+
...,
|
|
331
|
+
help="Session ID or path to trace file",
|
|
332
|
+
),
|
|
333
|
+
event_num: int = typer.Option(
|
|
334
|
+
None,
|
|
335
|
+
"--event",
|
|
336
|
+
"-e",
|
|
337
|
+
help="Show specific event number (1-based)",
|
|
338
|
+
),
|
|
339
|
+
agent: Optional[str] = typer.Option(
|
|
340
|
+
None,
|
|
341
|
+
"--agent",
|
|
342
|
+
"-a",
|
|
343
|
+
help="Filter by agent name",
|
|
344
|
+
),
|
|
345
|
+
event_type: Optional[str] = typer.Option(
|
|
346
|
+
None,
|
|
347
|
+
"--type",
|
|
348
|
+
"-t",
|
|
349
|
+
help="Filter by event type",
|
|
350
|
+
),
|
|
351
|
+
json_output: bool = typer.Option(
|
|
352
|
+
False,
|
|
353
|
+
"--json",
|
|
354
|
+
"-j",
|
|
355
|
+
help="Output as JSON",
|
|
356
|
+
),
|
|
357
|
+
):
|
|
358
|
+
"""Show trace events without interactive mode."""
|
|
359
|
+
from ..output.console import get_output_controller
|
|
360
|
+
|
|
361
|
+
output = get_output_controller()
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
from praisonai.replay import ContextTraceReader
|
|
365
|
+
except ImportError:
|
|
366
|
+
output.print_error("Replay module not available")
|
|
367
|
+
raise typer.Exit(1)
|
|
368
|
+
|
|
369
|
+
reader = ContextTraceReader(session_id)
|
|
370
|
+
|
|
371
|
+
if not reader.exists:
|
|
372
|
+
output.print_error(f"Trace not found: {session_id}")
|
|
373
|
+
raise typer.Exit(1)
|
|
374
|
+
|
|
375
|
+
# Get events with filters
|
|
376
|
+
if event_num is not None:
|
|
377
|
+
events = [reader[event_num - 1]] if 0 < event_num <= len(reader) else []
|
|
378
|
+
elif agent:
|
|
379
|
+
events = reader.get_by_agent(agent)
|
|
380
|
+
elif event_type:
|
|
381
|
+
events = reader.get_by_type(event_type)
|
|
382
|
+
else:
|
|
383
|
+
events = reader.get_all()
|
|
384
|
+
|
|
385
|
+
if json_output or output.is_json_mode:
|
|
386
|
+
output.print_json({
|
|
387
|
+
"session_id": session_id,
|
|
388
|
+
"total_events": len(reader),
|
|
389
|
+
"filtered_events": len(events),
|
|
390
|
+
"events": [e.to_dict() if hasattr(e, 'to_dict') else e for e in events],
|
|
391
|
+
})
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
if not events:
|
|
395
|
+
output.print_info("No events match the filter criteria.")
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
output.print(f"Session: {session_id}")
|
|
399
|
+
output.print(f"Showing {len(events)} of {len(reader)} events\n")
|
|
400
|
+
|
|
401
|
+
for i, event in enumerate(events):
|
|
402
|
+
# Get event details
|
|
403
|
+
if hasattr(event, 'event_type'):
|
|
404
|
+
et = event.event_type
|
|
405
|
+
if hasattr(et, 'value'):
|
|
406
|
+
et = et.value
|
|
407
|
+
else:
|
|
408
|
+
et = event.get('event_type', 'unknown')
|
|
409
|
+
|
|
410
|
+
agent_name = getattr(event, 'agent_name', None) or (event.get('agent_name') if isinstance(event, dict) else None)
|
|
411
|
+
seq = getattr(event, 'sequence_num', i) or (event.get('sequence_num', i) if isinstance(event, dict) else i)
|
|
412
|
+
|
|
413
|
+
line = f"[{seq + 1}] {et.upper()}"
|
|
414
|
+
if agent_name:
|
|
415
|
+
line += f" ({agent_name})"
|
|
416
|
+
|
|
417
|
+
output.print(line)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@app.command("delete")
|
|
421
|
+
def replay_delete(
|
|
422
|
+
session_id: str = typer.Argument(
|
|
423
|
+
...,
|
|
424
|
+
help="Session ID to delete",
|
|
425
|
+
),
|
|
426
|
+
force: bool = typer.Option(
|
|
427
|
+
False,
|
|
428
|
+
"--force",
|
|
429
|
+
"-f",
|
|
430
|
+
help="Skip confirmation",
|
|
431
|
+
),
|
|
432
|
+
):
|
|
433
|
+
"""Delete a context trace."""
|
|
434
|
+
from ..output.console import get_output_controller
|
|
435
|
+
|
|
436
|
+
output = get_output_controller()
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
from praisonai.replay import ContextTraceReader
|
|
440
|
+
except ImportError:
|
|
441
|
+
output.print_error("Replay module not available")
|
|
442
|
+
raise typer.Exit(1)
|
|
443
|
+
|
|
444
|
+
reader = ContextTraceReader(session_id)
|
|
445
|
+
|
|
446
|
+
if not reader.exists:
|
|
447
|
+
output.print_error(f"Trace not found: {session_id}")
|
|
448
|
+
raise typer.Exit(1)
|
|
449
|
+
|
|
450
|
+
if not force:
|
|
451
|
+
confirm = typer.confirm(f"Delete trace '{session_id}' ({len(reader)} events)?")
|
|
452
|
+
if not confirm:
|
|
453
|
+
output.print("Cancelled.")
|
|
454
|
+
raise typer.Exit(0)
|
|
455
|
+
|
|
456
|
+
try:
|
|
457
|
+
reader.path.unlink()
|
|
458
|
+
output.print_success(f"Deleted trace: {session_id}")
|
|
459
|
+
except Exception as e:
|
|
460
|
+
output.print_error(f"Failed to delete: {e}")
|
|
461
|
+
raise typer.Exit(1)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@app.command("flow")
|
|
465
|
+
def replay_flow(
|
|
466
|
+
session_id: str = typer.Argument(
|
|
467
|
+
...,
|
|
468
|
+
help="Session ID or path to trace file",
|
|
469
|
+
),
|
|
470
|
+
format: str = typer.Option(
|
|
471
|
+
"ascii",
|
|
472
|
+
"--format",
|
|
473
|
+
"-f",
|
|
474
|
+
help="Output format: ascii, json",
|
|
475
|
+
),
|
|
476
|
+
):
|
|
477
|
+
"""Visualize agent flow from a context trace."""
|
|
478
|
+
from ..output.console import get_output_controller
|
|
479
|
+
|
|
480
|
+
output = get_output_controller()
|
|
481
|
+
|
|
482
|
+
try:
|
|
483
|
+
from praisonai.replay import ContextTraceReader
|
|
484
|
+
except ImportError:
|
|
485
|
+
output.print_error("Replay module not available")
|
|
486
|
+
raise typer.Exit(1)
|
|
487
|
+
|
|
488
|
+
reader = ContextTraceReader(session_id)
|
|
489
|
+
|
|
490
|
+
if not reader.exists:
|
|
491
|
+
output.print_error(f"Trace not found: {session_id}")
|
|
492
|
+
raise typer.Exit(1)
|
|
493
|
+
|
|
494
|
+
events = reader.get_all()
|
|
495
|
+
if not events:
|
|
496
|
+
output.print_error(f"No events in trace: {session_id}")
|
|
497
|
+
raise typer.Exit(1)
|
|
498
|
+
|
|
499
|
+
# Build flow structure from events
|
|
500
|
+
flow_data = _build_flow_from_events(events)
|
|
501
|
+
|
|
502
|
+
if format == "json":
|
|
503
|
+
output.print_json(flow_data)
|
|
504
|
+
return
|
|
505
|
+
|
|
506
|
+
# ASCII visualization
|
|
507
|
+
_print_ascii_flow(output, flow_data, session_id)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _build_flow_from_events(events) -> dict:
|
|
511
|
+
"""Build flow structure from context events."""
|
|
512
|
+
agents = {}
|
|
513
|
+
handoffs = []
|
|
514
|
+
|
|
515
|
+
for event in events:
|
|
516
|
+
event_type = event.event_type
|
|
517
|
+
if hasattr(event_type, 'value'):
|
|
518
|
+
event_type = event_type.value
|
|
519
|
+
|
|
520
|
+
agent_name = event.agent_name
|
|
521
|
+
|
|
522
|
+
if event_type == "agent_start" and agent_name:
|
|
523
|
+
if agent_name not in agents:
|
|
524
|
+
agents[agent_name] = {
|
|
525
|
+
"name": agent_name,
|
|
526
|
+
"start_time": event.timestamp,
|
|
527
|
+
"end_time": None,
|
|
528
|
+
"messages": 0,
|
|
529
|
+
"tool_calls": [],
|
|
530
|
+
"llm_calls": 0,
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
elif event_type == "agent_end" and agent_name:
|
|
534
|
+
if agent_name in agents:
|
|
535
|
+
agents[agent_name]["end_time"] = event.timestamp
|
|
536
|
+
|
|
537
|
+
elif event_type == "agent_handoff":
|
|
538
|
+
handoffs.append({
|
|
539
|
+
"from": event.data.get("from_agent"),
|
|
540
|
+
"to": event.data.get("to_agent"),
|
|
541
|
+
"reason": event.data.get("reason"),
|
|
542
|
+
"timestamp": event.timestamp,
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
elif event_type == "message_added" and agent_name:
|
|
546
|
+
if agent_name in agents:
|
|
547
|
+
agents[agent_name]["messages"] += 1
|
|
548
|
+
|
|
549
|
+
elif event_type == "tool_call_start" and agent_name:
|
|
550
|
+
tool_name = event.data.get("tool_name", "unknown")
|
|
551
|
+
if agent_name in agents:
|
|
552
|
+
agents[agent_name]["tool_calls"].append(tool_name)
|
|
553
|
+
|
|
554
|
+
elif event_type == "llm_request" and agent_name:
|
|
555
|
+
if agent_name in agents:
|
|
556
|
+
agents[agent_name]["llm_calls"] += 1
|
|
557
|
+
|
|
558
|
+
# Determine agent order based on start times
|
|
559
|
+
agent_order = sorted(
|
|
560
|
+
agents.keys(),
|
|
561
|
+
key=lambda a: agents[a].get("start_time", 0) or 0
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
"session_id": events[0].session_id if events else "",
|
|
566
|
+
"agents": agents,
|
|
567
|
+
"agent_order": agent_order,
|
|
568
|
+
"handoffs": handoffs,
|
|
569
|
+
"total_events": len(events),
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _print_ascii_flow(output, flow_data: dict, session_id: str):
|
|
574
|
+
"""Print ASCII flow visualization."""
|
|
575
|
+
agents = flow_data["agents"]
|
|
576
|
+
agent_order = flow_data["agent_order"]
|
|
577
|
+
handoffs = flow_data["handoffs"]
|
|
578
|
+
|
|
579
|
+
output.print(f"\n{'='*60}")
|
|
580
|
+
output.print(f" AGENT FLOW: {session_id}")
|
|
581
|
+
output.print(f"{'='*60}\n")
|
|
582
|
+
|
|
583
|
+
if not agent_order:
|
|
584
|
+
output.print(" No agents found in trace.")
|
|
585
|
+
return
|
|
586
|
+
|
|
587
|
+
# Print agent boxes with connections
|
|
588
|
+
for i, agent_name in enumerate(agent_order):
|
|
589
|
+
agent = agents[agent_name]
|
|
590
|
+
|
|
591
|
+
# Agent box
|
|
592
|
+
box_width = 40
|
|
593
|
+
output.print(f" ┌{'─' * box_width}┐")
|
|
594
|
+
output.print(f" │ {'Agent: ' + agent_name:<{box_width-2}} │")
|
|
595
|
+
output.print(f" │{' ' * box_width}│")
|
|
596
|
+
output.print(f" │ Messages: {agent['messages']:<{box_width-15}} │")
|
|
597
|
+
output.print(f" │ LLM Calls: {agent['llm_calls']:<{box_width-16}} │")
|
|
598
|
+
|
|
599
|
+
if agent['tool_calls']:
|
|
600
|
+
tools_str = ", ".join(agent['tool_calls'][:3])
|
|
601
|
+
if len(agent['tool_calls']) > 3:
|
|
602
|
+
tools_str += f" (+{len(agent['tool_calls'])-3} more)"
|
|
603
|
+
output.print(f" │ Tools: {tools_str:<{box_width-13}} │")
|
|
604
|
+
|
|
605
|
+
output.print(f" └{'─' * box_width}┘")
|
|
606
|
+
|
|
607
|
+
# Connection to next agent
|
|
608
|
+
if i < len(agent_order) - 1:
|
|
609
|
+
next_agent = agent_order[i + 1]
|
|
610
|
+
|
|
611
|
+
# Find handoff reason
|
|
612
|
+
handoff_reason = None
|
|
613
|
+
for h in handoffs:
|
|
614
|
+
if h["from"] == agent_name and h["to"] == next_agent:
|
|
615
|
+
handoff_reason = h.get("reason")
|
|
616
|
+
break
|
|
617
|
+
|
|
618
|
+
output.print(" │")
|
|
619
|
+
output.print(" ▼")
|
|
620
|
+
if handoff_reason:
|
|
621
|
+
reason_short = handoff_reason[:35] + "..." if len(handoff_reason) > 35 else handoff_reason
|
|
622
|
+
output.print(f" [{reason_short}]")
|
|
623
|
+
output.print("")
|
|
624
|
+
|
|
625
|
+
# Summary
|
|
626
|
+
output.print("\n" + "─"*60)
|
|
627
|
+
output.print(" Summary:")
|
|
628
|
+
output.print(f" Agents: {len(agent_order)}")
|
|
629
|
+
output.print(f" Handoffs: {len(handoffs)}")
|
|
630
|
+
output.print(f" Total Events: {flow_data['total_events']}")
|
|
631
|
+
output.print("─"*60 + "\n")
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
@app.command("dashboard")
|
|
635
|
+
def replay_dashboard(
|
|
636
|
+
session_id: Optional[str] = typer.Argument(
|
|
637
|
+
None,
|
|
638
|
+
help="Session ID (optional, shows all recent if not specified)",
|
|
639
|
+
),
|
|
640
|
+
limit: int = typer.Option(
|
|
641
|
+
5,
|
|
642
|
+
"--limit",
|
|
643
|
+
"-n",
|
|
644
|
+
help="Number of recent sessions to analyze",
|
|
645
|
+
),
|
|
646
|
+
json_output: bool = typer.Option(
|
|
647
|
+
False,
|
|
648
|
+
"--json",
|
|
649
|
+
"-j",
|
|
650
|
+
help="Output as JSON",
|
|
651
|
+
),
|
|
652
|
+
):
|
|
653
|
+
"""Show context analytics dashboard with usage patterns.
|
|
654
|
+
|
|
655
|
+
Displays:
|
|
656
|
+
- Token usage by agent (bar chart)
|
|
657
|
+
- Cost breakdown by tool
|
|
658
|
+
- Duplicate content patterns
|
|
659
|
+
- Optimization recommendations
|
|
660
|
+
"""
|
|
661
|
+
from ..output.console import get_output_controller
|
|
662
|
+
|
|
663
|
+
output = get_output_controller()
|
|
664
|
+
|
|
665
|
+
try:
|
|
666
|
+
from praisonai.replay import list_traces, ContextTraceReader
|
|
667
|
+
from praisonai.replay.analyzer import TraceAnalyzer
|
|
668
|
+
except ImportError:
|
|
669
|
+
output.print_error("Replay module not available")
|
|
670
|
+
raise typer.Exit(1)
|
|
671
|
+
|
|
672
|
+
# Get traces to analyze
|
|
673
|
+
if session_id:
|
|
674
|
+
traces_to_analyze = [session_id]
|
|
675
|
+
else:
|
|
676
|
+
traces = list_traces(limit=limit)
|
|
677
|
+
if not traces:
|
|
678
|
+
output.print_info("No traces found. Run agents with trace=True to create traces.")
|
|
679
|
+
return
|
|
680
|
+
traces_to_analyze = [t.session_id for t in traces]
|
|
681
|
+
|
|
682
|
+
# Aggregate stats across sessions
|
|
683
|
+
all_stats = []
|
|
684
|
+
total_tokens = 0
|
|
685
|
+
total_cost = 0.0
|
|
686
|
+
total_duplicates = 0
|
|
687
|
+
total_wasted = 0
|
|
688
|
+
agent_tokens = {}
|
|
689
|
+
tool_costs = {}
|
|
690
|
+
|
|
691
|
+
for sid in traces_to_analyze:
|
|
692
|
+
reader = ContextTraceReader(sid)
|
|
693
|
+
if not reader.exists:
|
|
694
|
+
continue
|
|
695
|
+
|
|
696
|
+
events = reader.get_all()
|
|
697
|
+
if not events:
|
|
698
|
+
continue
|
|
699
|
+
|
|
700
|
+
analyzer = TraceAnalyzer(events)
|
|
701
|
+
result = analyzer.analyze()
|
|
702
|
+
all_stats.append(result)
|
|
703
|
+
|
|
704
|
+
# Aggregate using correct attribute paths
|
|
705
|
+
total_tokens += result.token_stats.total_tokens
|
|
706
|
+
total_cost += result.cost_stats.total_cost
|
|
707
|
+
total_duplicates += result.duplicate_count
|
|
708
|
+
total_wasted += result.total_wasted_tokens
|
|
709
|
+
|
|
710
|
+
# By agent - tokens is a dict with {prompt, completion, total}
|
|
711
|
+
for agent, tokens in result.token_stats.by_agent.items():
|
|
712
|
+
agent_total = tokens.get("total", 0) if isinstance(tokens, dict) else tokens
|
|
713
|
+
agent_tokens[agent] = agent_tokens.get(agent, 0) + agent_total
|
|
714
|
+
|
|
715
|
+
# By tool - cost is a dict with {count, cost}
|
|
716
|
+
for tool, cost_info in result.cost_stats.by_tool.items():
|
|
717
|
+
tool_cost = cost_info.get("cost", 0.0) if isinstance(cost_info, dict) else cost_info
|
|
718
|
+
tool_costs[tool] = tool_costs.get(tool, 0.0) + tool_cost
|
|
719
|
+
|
|
720
|
+
if json_output:
|
|
721
|
+
output.print_json({
|
|
722
|
+
"sessions_analyzed": len(all_stats),
|
|
723
|
+
"total_tokens": total_tokens,
|
|
724
|
+
"total_cost": total_cost,
|
|
725
|
+
"total_duplicates": total_duplicates,
|
|
726
|
+
"total_wasted_tokens": total_wasted,
|
|
727
|
+
"tokens_by_agent": agent_tokens,
|
|
728
|
+
"cost_by_tool": tool_costs,
|
|
729
|
+
})
|
|
730
|
+
return
|
|
731
|
+
|
|
732
|
+
# Display dashboard
|
|
733
|
+
output.print("\n" + "="*70)
|
|
734
|
+
output.print(" CONTEXT ANALYTICS DASHBOARD")
|
|
735
|
+
output.print("="*70)
|
|
736
|
+
output.print(f"\n Sessions Analyzed: {len(all_stats)}")
|
|
737
|
+
output.print(f" Total Tokens: {total_tokens:,}")
|
|
738
|
+
output.print(f" Total Cost: ${total_cost:.4f}")
|
|
739
|
+
|
|
740
|
+
# Token usage by agent (bar chart)
|
|
741
|
+
if agent_tokens:
|
|
742
|
+
output.print("\n TOKEN USAGE BY AGENT:")
|
|
743
|
+
output.print(" " + "-"*50)
|
|
744
|
+
max_tokens = max(agent_tokens.values()) if agent_tokens else 1
|
|
745
|
+
for agent, tokens in sorted(agent_tokens.items(), key=lambda x: -x[1]):
|
|
746
|
+
bar_len = int((tokens / max_tokens) * 30)
|
|
747
|
+
bar = "█" * bar_len
|
|
748
|
+
pct = (tokens / total_tokens * 100) if total_tokens > 0 else 0
|
|
749
|
+
output.print(f" {agent[:20]:<20} {bar:<30} {tokens:>10,} ({pct:.1f}%)")
|
|
750
|
+
|
|
751
|
+
# Cost by tool
|
|
752
|
+
if tool_costs:
|
|
753
|
+
output.print("\n COST BY TOOL:")
|
|
754
|
+
output.print(" " + "-"*50)
|
|
755
|
+
for tool, cost in sorted(tool_costs.items(), key=lambda x: -x[1]):
|
|
756
|
+
output.print(f" {tool:<25} ${cost:.4f}")
|
|
757
|
+
|
|
758
|
+
# Context efficiency
|
|
759
|
+
output.print("\n CONTEXT EFFICIENCY:")
|
|
760
|
+
output.print(" " + "-"*50)
|
|
761
|
+
if total_duplicates > 0:
|
|
762
|
+
waste_pct = (total_wasted / total_tokens * 100) if total_tokens > 0 else 0
|
|
763
|
+
output.print(f" ⚠️ Duplicates Found: {total_duplicates}")
|
|
764
|
+
output.print(f" ⚠️ Wasted Tokens: {total_wasted:,} ({waste_pct:.1f}%)")
|
|
765
|
+
output.print("\n RECOMMENDATIONS:")
|
|
766
|
+
output.print(" • Enable session-level deduplication in workflow")
|
|
767
|
+
output.print(" • Consider using context: {strategy: smart} in YAML")
|
|
768
|
+
output.print(" • Review tool outputs for redundant content")
|
|
769
|
+
else:
|
|
770
|
+
output.print(" ✅ Context is optimized! No duplicates detected.")
|
|
771
|
+
|
|
772
|
+
output.print("\n" + "="*70 + "\n")
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
@app.command("cleanup")
|
|
776
|
+
def replay_cleanup(
|
|
777
|
+
max_age_days: int = typer.Option(
|
|
778
|
+
7,
|
|
779
|
+
"--max-age",
|
|
780
|
+
help="Delete traces older than N days",
|
|
781
|
+
),
|
|
782
|
+
max_size_mb: int = typer.Option(
|
|
783
|
+
100,
|
|
784
|
+
"--max-size",
|
|
785
|
+
help="Delete oldest traces if total size exceeds N MB",
|
|
786
|
+
),
|
|
787
|
+
dry_run: bool = typer.Option(
|
|
788
|
+
False,
|
|
789
|
+
"--dry-run",
|
|
790
|
+
help="Show what would be deleted without deleting",
|
|
791
|
+
),
|
|
792
|
+
):
|
|
793
|
+
"""Clean up old context traces."""
|
|
794
|
+
from ..output.console import get_output_controller
|
|
795
|
+
|
|
796
|
+
output = get_output_controller()
|
|
797
|
+
|
|
798
|
+
try:
|
|
799
|
+
from praisonai.replay.storage import cleanup_old_traces, list_traces
|
|
800
|
+
except ImportError:
|
|
801
|
+
output.print_error("Replay module not available")
|
|
802
|
+
raise typer.Exit(1)
|
|
803
|
+
|
|
804
|
+
if dry_run:
|
|
805
|
+
from datetime import datetime
|
|
806
|
+
traces = list_traces(limit=10000)
|
|
807
|
+
now = datetime.now()
|
|
808
|
+
|
|
809
|
+
to_delete = []
|
|
810
|
+
for trace in traces:
|
|
811
|
+
age_days = (now - trace.modified_at).days
|
|
812
|
+
if age_days > max_age_days:
|
|
813
|
+
to_delete.append(trace)
|
|
814
|
+
|
|
815
|
+
if to_delete:
|
|
816
|
+
output.print(f"Would delete {len(to_delete)} traces older than {max_age_days} days:")
|
|
817
|
+
for t in to_delete[:10]:
|
|
818
|
+
output.print(f" - {t.session_id}")
|
|
819
|
+
if len(to_delete) > 10:
|
|
820
|
+
output.print(f" ... and {len(to_delete) - 10} more")
|
|
821
|
+
else:
|
|
822
|
+
output.print("No traces would be deleted.")
|
|
823
|
+
return
|
|
824
|
+
|
|
825
|
+
deleted = cleanup_old_traces(max_age_days=max_age_days, max_size_mb=max_size_mb)
|
|
826
|
+
|
|
827
|
+
if deleted > 0:
|
|
828
|
+
output.print_success(f"Deleted {deleted} old traces.")
|
|
829
|
+
else:
|
|
830
|
+
output.print_info("No traces needed cleanup.")
|