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,1462 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive Benchmarking CLI for PraisonAI.
|
|
3
|
+
|
|
4
|
+
Provides deep profiling and comparison across all execution paths:
|
|
5
|
+
- OpenAI SDK (baseline)
|
|
6
|
+
- PraisonAI Agent (Python SDK)
|
|
7
|
+
- PraisonAI CLI (fast path)
|
|
8
|
+
- PraisonAI CLI with profiling
|
|
9
|
+
- PraisonAI Workflow (single agent)
|
|
10
|
+
- PraisonAI Workflow (multi-agent)
|
|
11
|
+
- PraisonAI via LiteLLM
|
|
12
|
+
- LiteLLM standalone
|
|
13
|
+
|
|
14
|
+
Usage: praisonai benchmark profile "What is 2+2?"
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import json
|
|
20
|
+
import time
|
|
21
|
+
import subprocess
|
|
22
|
+
import statistics
|
|
23
|
+
import cProfile
|
|
24
|
+
import pstats
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from .base import FlagHandler
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Deep profiling constants
|
|
33
|
+
MAX_FUNCTION_STATS = 1000
|
|
34
|
+
MAX_CALL_GRAPH_EDGES = 5000
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class PhaseTimings:
|
|
39
|
+
"""Timing breakdown for execution phases."""
|
|
40
|
+
import_ms: float = 0.0
|
|
41
|
+
init_ms: float = 0.0
|
|
42
|
+
network_ms: float = 0.0
|
|
43
|
+
exec_ms: float = 0.0
|
|
44
|
+
render_ms: float = 0.0
|
|
45
|
+
total_ms: float = 0.0
|
|
46
|
+
ttft_ms: float = 0.0 # Time to first token
|
|
47
|
+
subprocess_ms: float = 0.0 # CLI subprocess overhead
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class FunctionStat:
|
|
52
|
+
"""Statistics for a single function from cProfile."""
|
|
53
|
+
name: str
|
|
54
|
+
file: str
|
|
55
|
+
line: int
|
|
56
|
+
calls: int
|
|
57
|
+
total_time_ms: float # Self time
|
|
58
|
+
cumulative_time_ms: float # Cumulative time
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
61
|
+
return {
|
|
62
|
+
"name": self.name,
|
|
63
|
+
"file": self.file,
|
|
64
|
+
"line": self.line,
|
|
65
|
+
"calls": self.calls,
|
|
66
|
+
"total_time_ms": self.total_time_ms,
|
|
67
|
+
"cumulative_time_ms": self.cumulative_time_ms,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class CallGraphData:
|
|
73
|
+
"""Call graph data (callers and callees)."""
|
|
74
|
+
callers: Dict[str, List[str]] = field(default_factory=dict)
|
|
75
|
+
callees: Dict[str, List[str]] = field(default_factory=dict)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def edge_count(self) -> int:
|
|
79
|
+
return sum(len(v) for v in self.callers.values())
|
|
80
|
+
|
|
81
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
82
|
+
return {
|
|
83
|
+
"callers": self.callers,
|
|
84
|
+
"callees": self.callees,
|
|
85
|
+
"edge_count": self.edge_count,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class ModuleBreakdown:
|
|
91
|
+
"""Module/file breakdown for deep profile visibility."""
|
|
92
|
+
praisonai_modules: List[Tuple[str, float]] = field(default_factory=list) # (file, cumulative_ms)
|
|
93
|
+
agent_modules: List[Tuple[str, float]] = field(default_factory=list)
|
|
94
|
+
network_modules: List[Tuple[str, float]] = field(default_factory=list)
|
|
95
|
+
third_party_modules: List[Tuple[str, float]] = field(default_factory=list)
|
|
96
|
+
|
|
97
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
98
|
+
return {
|
|
99
|
+
"praisonai": [{"file": f, "cumulative_ms": t} for f, t in self.praisonai_modules[:20]],
|
|
100
|
+
"agent": [{"file": f, "cumulative_ms": t} for f, t in self.agent_modules[:20]],
|
|
101
|
+
"network": [{"file": f, "cumulative_ms": t} for f, t in self.network_modules[:20]],
|
|
102
|
+
"third_party": [{"file": f, "cumulative_ms": t} for f, t in self.third_party_modules[:20]],
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class DeepProfileData:
|
|
108
|
+
"""Deep profiling data from cProfile."""
|
|
109
|
+
functions: List[FunctionStat] = field(default_factory=list)
|
|
110
|
+
call_graph: Optional[CallGraphData] = None
|
|
111
|
+
module_breakdown: Optional[ModuleBreakdown] = None
|
|
112
|
+
|
|
113
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
114
|
+
result = {
|
|
115
|
+
"functions": [f.to_dict() for f in self.functions],
|
|
116
|
+
}
|
|
117
|
+
if self.call_graph:
|
|
118
|
+
result["call_graph"] = self.call_graph.to_dict()
|
|
119
|
+
if self.module_breakdown:
|
|
120
|
+
result["module_breakdown"] = self.module_breakdown.to_dict()
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class BenchmarkRun:
|
|
126
|
+
"""Single benchmark run result."""
|
|
127
|
+
path_name: str
|
|
128
|
+
iteration: int
|
|
129
|
+
is_cold: bool
|
|
130
|
+
timings: PhaseTimings
|
|
131
|
+
memory_mb: float = 0.0
|
|
132
|
+
success: bool = True
|
|
133
|
+
error: str = ""
|
|
134
|
+
response_preview: str = ""
|
|
135
|
+
deep_profile: Optional[DeepProfileData] = None # Deep profiling data
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class BenchmarkResult:
|
|
140
|
+
"""Aggregated benchmark result for a path."""
|
|
141
|
+
path_name: str
|
|
142
|
+
runs: List[BenchmarkRun] = field(default_factory=list)
|
|
143
|
+
|
|
144
|
+
# Aggregated stats
|
|
145
|
+
mean_total_ms: float = 0.0
|
|
146
|
+
min_total_ms: float = 0.0
|
|
147
|
+
max_total_ms: float = 0.0
|
|
148
|
+
std_total_ms: float = 0.0
|
|
149
|
+
|
|
150
|
+
mean_import_ms: float = 0.0
|
|
151
|
+
mean_init_ms: float = 0.0
|
|
152
|
+
mean_network_ms: float = 0.0
|
|
153
|
+
|
|
154
|
+
cold_total_ms: float = 0.0
|
|
155
|
+
warm_total_ms: float = 0.0
|
|
156
|
+
|
|
157
|
+
delta_vs_sdk_ms: float = 0.0
|
|
158
|
+
|
|
159
|
+
# Aggregated deep profile data (when --deep is used)
|
|
160
|
+
aggregated_functions: Optional[List[FunctionStat]] = None
|
|
161
|
+
aggregated_call_graph: Optional[CallGraphData] = None
|
|
162
|
+
aggregated_module_breakdown: Optional[ModuleBreakdown] = None
|
|
163
|
+
|
|
164
|
+
def compute_stats(self, sdk_baseline_ms: float = 0.0):
|
|
165
|
+
"""Compute aggregated statistics from runs."""
|
|
166
|
+
if not self.runs:
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
totals = [r.timings.total_ms for r in self.runs if r.success]
|
|
170
|
+
if not totals:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
self.mean_total_ms = statistics.mean(totals)
|
|
174
|
+
self.min_total_ms = min(totals)
|
|
175
|
+
self.max_total_ms = max(totals)
|
|
176
|
+
self.std_total_ms = statistics.stdev(totals) if len(totals) > 1 else 0.0
|
|
177
|
+
|
|
178
|
+
self.mean_import_ms = statistics.mean([r.timings.import_ms for r in self.runs if r.success])
|
|
179
|
+
self.mean_init_ms = statistics.mean([r.timings.init_ms for r in self.runs if r.success])
|
|
180
|
+
self.mean_network_ms = statistics.mean([r.timings.network_ms for r in self.runs if r.success])
|
|
181
|
+
|
|
182
|
+
cold_runs = [r.timings.total_ms for r in self.runs if r.is_cold and r.success]
|
|
183
|
+
warm_runs = [r.timings.total_ms for r in self.runs if not r.is_cold and r.success]
|
|
184
|
+
|
|
185
|
+
self.cold_total_ms = statistics.mean(cold_runs) if cold_runs else 0.0
|
|
186
|
+
self.warm_total_ms = statistics.mean(warm_runs) if warm_runs else 0.0
|
|
187
|
+
|
|
188
|
+
self.delta_vs_sdk_ms = self.mean_total_ms - sdk_baseline_ms
|
|
189
|
+
|
|
190
|
+
# Aggregate deep profile data if present
|
|
191
|
+
self._aggregate_deep_profiles()
|
|
192
|
+
|
|
193
|
+
def _aggregate_deep_profiles(self):
|
|
194
|
+
"""Aggregate deep profile data across all runs."""
|
|
195
|
+
runs_with_deep = [r for r in self.runs if r.success and r.deep_profile]
|
|
196
|
+
if not runs_with_deep:
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
# Aggregate function stats by averaging times and summing calls
|
|
200
|
+
func_aggregates: Dict[str, Dict[str, Any]] = {}
|
|
201
|
+
|
|
202
|
+
for run in runs_with_deep:
|
|
203
|
+
for func in run.deep_profile.functions:
|
|
204
|
+
key = f"{func.name}:{func.file}:{func.line}"
|
|
205
|
+
if key not in func_aggregates:
|
|
206
|
+
func_aggregates[key] = {
|
|
207
|
+
"name": func.name,
|
|
208
|
+
"file": func.file,
|
|
209
|
+
"line": func.line,
|
|
210
|
+
"calls_list": [],
|
|
211
|
+
"total_time_list": [],
|
|
212
|
+
"cumulative_time_list": [],
|
|
213
|
+
}
|
|
214
|
+
func_aggregates[key]["calls_list"].append(func.calls)
|
|
215
|
+
func_aggregates[key]["total_time_list"].append(func.total_time_ms)
|
|
216
|
+
func_aggregates[key]["cumulative_time_list"].append(func.cumulative_time_ms)
|
|
217
|
+
|
|
218
|
+
# Build aggregated function list
|
|
219
|
+
aggregated = []
|
|
220
|
+
for key, data in func_aggregates.items():
|
|
221
|
+
aggregated.append(FunctionStat(
|
|
222
|
+
name=data["name"],
|
|
223
|
+
file=data["file"],
|
|
224
|
+
line=data["line"],
|
|
225
|
+
calls=int(statistics.mean(data["calls_list"])),
|
|
226
|
+
total_time_ms=statistics.mean(data["total_time_list"]),
|
|
227
|
+
cumulative_time_ms=statistics.mean(data["cumulative_time_list"]),
|
|
228
|
+
))
|
|
229
|
+
|
|
230
|
+
# Sort by cumulative time and limit
|
|
231
|
+
aggregated.sort(key=lambda x: x.cumulative_time_ms, reverse=True)
|
|
232
|
+
self.aggregated_functions = aggregated[:100]
|
|
233
|
+
|
|
234
|
+
# Use call graph from first run (structure is consistent)
|
|
235
|
+
if runs_with_deep[0].deep_profile.call_graph:
|
|
236
|
+
self.aggregated_call_graph = runs_with_deep[0].deep_profile.call_graph
|
|
237
|
+
|
|
238
|
+
# Use module breakdown from first run
|
|
239
|
+
if runs_with_deep[0].deep_profile.module_breakdown:
|
|
240
|
+
self.aggregated_module_breakdown = runs_with_deep[0].deep_profile.module_breakdown
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass
|
|
244
|
+
class BenchmarkReport:
|
|
245
|
+
"""Complete benchmark report."""
|
|
246
|
+
timestamp: str
|
|
247
|
+
prompt: str
|
|
248
|
+
iterations: int
|
|
249
|
+
results: Dict[str, BenchmarkResult] = field(default_factory=dict)
|
|
250
|
+
sdk_baseline_ms: float = 0.0
|
|
251
|
+
|
|
252
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
253
|
+
"""Convert to dictionary for JSON serialization."""
|
|
254
|
+
results_dict = {}
|
|
255
|
+
for name, r in self.results.items():
|
|
256
|
+
result_data = {
|
|
257
|
+
"path_name": r.path_name,
|
|
258
|
+
"mean_total_ms": r.mean_total_ms,
|
|
259
|
+
"min_total_ms": r.min_total_ms,
|
|
260
|
+
"max_total_ms": r.max_total_ms,
|
|
261
|
+
"std_total_ms": r.std_total_ms,
|
|
262
|
+
"mean_import_ms": r.mean_import_ms,
|
|
263
|
+
"mean_init_ms": r.mean_init_ms,
|
|
264
|
+
"mean_network_ms": r.mean_network_ms,
|
|
265
|
+
"cold_total_ms": r.cold_total_ms,
|
|
266
|
+
"warm_total_ms": r.warm_total_ms,
|
|
267
|
+
"delta_vs_sdk_ms": r.delta_vs_sdk_ms,
|
|
268
|
+
"runs": [
|
|
269
|
+
{
|
|
270
|
+
"iteration": run.iteration,
|
|
271
|
+
"is_cold": run.is_cold,
|
|
272
|
+
"total_ms": run.timings.total_ms,
|
|
273
|
+
"import_ms": run.timings.import_ms,
|
|
274
|
+
"init_ms": run.timings.init_ms,
|
|
275
|
+
"network_ms": run.timings.network_ms,
|
|
276
|
+
"memory_mb": run.memory_mb,
|
|
277
|
+
"success": run.success,
|
|
278
|
+
**({"deep_profile": run.deep_profile.to_dict()} if run.deep_profile else {})
|
|
279
|
+
}
|
|
280
|
+
for run in r.runs
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
# Add aggregated deep profile data if present
|
|
285
|
+
if r.aggregated_functions:
|
|
286
|
+
result_data["functions"] = [f.to_dict() for f in r.aggregated_functions]
|
|
287
|
+
if r.aggregated_call_graph:
|
|
288
|
+
result_data["call_graph"] = r.aggregated_call_graph.to_dict()
|
|
289
|
+
if r.aggregated_module_breakdown:
|
|
290
|
+
result_data["module_breakdown"] = r.aggregated_module_breakdown.to_dict()
|
|
291
|
+
|
|
292
|
+
results_dict[name] = result_data
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
"timestamp": self.timestamp,
|
|
296
|
+
"prompt": self.prompt,
|
|
297
|
+
"iterations": self.iterations,
|
|
298
|
+
"sdk_baseline_ms": self.sdk_baseline_ms,
|
|
299
|
+
"results": results_dict
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class BenchmarkHandler(FlagHandler):
|
|
304
|
+
"""
|
|
305
|
+
Comprehensive benchmarking handler for PraisonAI.
|
|
306
|
+
|
|
307
|
+
Commands:
|
|
308
|
+
praisonai benchmark profile [query] - Run full benchmark suite
|
|
309
|
+
praisonai benchmark sdk [query] - Benchmark OpenAI SDK only
|
|
310
|
+
praisonai benchmark agent [query] - Benchmark Agent only
|
|
311
|
+
praisonai benchmark cli [query] - Benchmark CLI only
|
|
312
|
+
praisonai benchmark compare [query] - Quick comparison table
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
DEFAULT_PROMPT = "Hi"
|
|
316
|
+
DEFAULT_ITERATIONS = 3
|
|
317
|
+
DEFAULT_MODEL = "gpt-4o-mini"
|
|
318
|
+
|
|
319
|
+
# Benchmark paths
|
|
320
|
+
PATHS = [
|
|
321
|
+
"openai_sdk",
|
|
322
|
+
"praisonai_agent",
|
|
323
|
+
"praisonai_cli",
|
|
324
|
+
"praisonai_cli_profile",
|
|
325
|
+
"praisonai_workflow_single",
|
|
326
|
+
"praisonai_workflow_multi",
|
|
327
|
+
"praisonai_litellm",
|
|
328
|
+
"litellm_standalone",
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def feature_name(self) -> str:
|
|
333
|
+
return "benchmark"
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def flag_name(self) -> str:
|
|
337
|
+
return "benchmark"
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def flag_help(self) -> str:
|
|
341
|
+
return "Run comprehensive performance benchmarks"
|
|
342
|
+
|
|
343
|
+
def check_dependencies(self) -> Tuple[bool, str]:
|
|
344
|
+
"""Check if benchmarking is available."""
|
|
345
|
+
return True, ""
|
|
346
|
+
|
|
347
|
+
def execute(self, *args, **kwargs) -> Any:
|
|
348
|
+
"""Execute the benchmark command."""
|
|
349
|
+
return self.handle(*args, **kwargs)
|
|
350
|
+
|
|
351
|
+
def _get_project_root(self) -> str:
|
|
352
|
+
"""Get the project root directory."""
|
|
353
|
+
# Navigate up from this file to find the project root
|
|
354
|
+
current = Path(__file__).resolve()
|
|
355
|
+
# Go up to praisonai-package
|
|
356
|
+
for _ in range(6):
|
|
357
|
+
current = current.parent
|
|
358
|
+
if (current / "pyproject.toml").exists():
|
|
359
|
+
return str(current)
|
|
360
|
+
return os.getcwd()
|
|
361
|
+
|
|
362
|
+
def _run_subprocess_benchmark(self, code: str, cwd: str = None) -> Tuple[Dict[str, float], str, str]:
|
|
363
|
+
"""
|
|
364
|
+
Run a benchmark in a subprocess and return timing data.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Tuple of (timings_dict, stdout, stderr)
|
|
368
|
+
"""
|
|
369
|
+
if cwd is None:
|
|
370
|
+
cwd = self._get_project_root()
|
|
371
|
+
|
|
372
|
+
t0 = time.perf_counter()
|
|
373
|
+
result = subprocess.run(
|
|
374
|
+
[sys.executable, "-c", code],
|
|
375
|
+
capture_output=True,
|
|
376
|
+
text=True,
|
|
377
|
+
cwd=cwd,
|
|
378
|
+
env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"},
|
|
379
|
+
timeout=120
|
|
380
|
+
)
|
|
381
|
+
subprocess_time = (time.perf_counter() - t0) * 1000
|
|
382
|
+
|
|
383
|
+
timings = {"subprocess_ms": subprocess_time}
|
|
384
|
+
|
|
385
|
+
# Parse JSON output from the benchmark code
|
|
386
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
387
|
+
try:
|
|
388
|
+
# Look for JSON in the output
|
|
389
|
+
lines = result.stdout.strip().split("\n")
|
|
390
|
+
for line in lines:
|
|
391
|
+
if line.startswith("{") and line.endswith("}"):
|
|
392
|
+
data = json.loads(line)
|
|
393
|
+
timings.update(data)
|
|
394
|
+
break
|
|
395
|
+
except json.JSONDecodeError:
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
return timings, result.stdout, result.stderr
|
|
399
|
+
|
|
400
|
+
def _extract_function_stats(self, profiler: cProfile.Profile, limit: int = 30,
|
|
401
|
+
sort_by: str = "cumulative") -> List[FunctionStat]:
|
|
402
|
+
"""Extract function statistics from cProfile."""
|
|
403
|
+
stats = pstats.Stats(profiler)
|
|
404
|
+
stats.sort_stats(sort_by)
|
|
405
|
+
|
|
406
|
+
function_stats = []
|
|
407
|
+
for (filename, line, name), (cc, nc, tt, ct, callers) in stats.stats.items():
|
|
408
|
+
if len(function_stats) >= MAX_FUNCTION_STATS:
|
|
409
|
+
break
|
|
410
|
+
|
|
411
|
+
function_stats.append(FunctionStat(
|
|
412
|
+
name=name,
|
|
413
|
+
file=filename,
|
|
414
|
+
line=line,
|
|
415
|
+
calls=nc,
|
|
416
|
+
total_time_ms=tt * 1000,
|
|
417
|
+
cumulative_time_ms=ct * 1000,
|
|
418
|
+
))
|
|
419
|
+
|
|
420
|
+
function_stats.sort(key=lambda x: x.cumulative_time_ms, reverse=True)
|
|
421
|
+
return function_stats[:limit]
|
|
422
|
+
|
|
423
|
+
def _extract_call_graph(self, profiler: cProfile.Profile) -> CallGraphData:
|
|
424
|
+
"""Extract call graph from cProfile."""
|
|
425
|
+
stats = pstats.Stats(profiler)
|
|
426
|
+
|
|
427
|
+
callers: Dict[str, List[str]] = {}
|
|
428
|
+
callees: Dict[str, List[str]] = {}
|
|
429
|
+
edge_count = 0
|
|
430
|
+
|
|
431
|
+
for (filename, line, name), (cc, nc, tt, ct, caller_dict) in stats.stats.items():
|
|
432
|
+
if edge_count >= MAX_CALL_GRAPH_EDGES:
|
|
433
|
+
break
|
|
434
|
+
|
|
435
|
+
callee_key = f"{name}:{filename}:{line}"
|
|
436
|
+
|
|
437
|
+
for (caller_file, caller_line, caller_name), _ in caller_dict.items():
|
|
438
|
+
if edge_count >= MAX_CALL_GRAPH_EDGES:
|
|
439
|
+
break
|
|
440
|
+
|
|
441
|
+
caller_key = f"{caller_name}:{caller_file}:{caller_line}"
|
|
442
|
+
|
|
443
|
+
if callee_key not in callers:
|
|
444
|
+
callers[callee_key] = []
|
|
445
|
+
if caller_key not in callers[callee_key]:
|
|
446
|
+
callers[callee_key].append(caller_key)
|
|
447
|
+
|
|
448
|
+
if caller_key not in callees:
|
|
449
|
+
callees[caller_key] = []
|
|
450
|
+
if callee_key not in callees[caller_key]:
|
|
451
|
+
callees[caller_key].append(callee_key)
|
|
452
|
+
|
|
453
|
+
edge_count += 1
|
|
454
|
+
|
|
455
|
+
return CallGraphData(callers=callers, callees=callees)
|
|
456
|
+
|
|
457
|
+
def _extract_module_breakdown(self, functions: List[FunctionStat]) -> ModuleBreakdown:
|
|
458
|
+
"""Extract module breakdown from function stats."""
|
|
459
|
+
praisonai_modules: Dict[str, float] = {}
|
|
460
|
+
agent_modules: Dict[str, float] = {}
|
|
461
|
+
network_modules: Dict[str, float] = {}
|
|
462
|
+
third_party_modules: Dict[str, float] = {}
|
|
463
|
+
|
|
464
|
+
for func in functions:
|
|
465
|
+
file_path = func.file.lower()
|
|
466
|
+
cumul = func.cumulative_time_ms
|
|
467
|
+
|
|
468
|
+
if "praisonai" in file_path and "agents" not in file_path:
|
|
469
|
+
praisonai_modules[func.file] = praisonai_modules.get(func.file, 0) + cumul
|
|
470
|
+
elif "praisonaiagents" in file_path or ("praisonai" in file_path and "agents" in file_path):
|
|
471
|
+
agent_modules[func.file] = agent_modules.get(func.file, 0) + cumul
|
|
472
|
+
elif any(x in file_path for x in ["httpx", "httpcore", "urllib", "requests", "aiohttp", "openai"]):
|
|
473
|
+
network_modules[func.file] = network_modules.get(func.file, 0) + cumul
|
|
474
|
+
elif "site-packages" in file_path or not file_path.startswith("/"):
|
|
475
|
+
third_party_modules[func.file] = third_party_modules.get(func.file, 0) + cumul
|
|
476
|
+
|
|
477
|
+
def to_sorted_list(d: Dict[str, float]) -> List[Tuple[str, float]]:
|
|
478
|
+
return sorted(d.items(), key=lambda x: x[1], reverse=True)[:20]
|
|
479
|
+
|
|
480
|
+
return ModuleBreakdown(
|
|
481
|
+
praisonai_modules=to_sorted_list(praisonai_modules),
|
|
482
|
+
agent_modules=to_sorted_list(agent_modules),
|
|
483
|
+
network_modules=to_sorted_list(network_modules),
|
|
484
|
+
third_party_modules=to_sorted_list(third_party_modules),
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
def _run_with_deep_profile(self, func, limit: int = 30) -> Tuple[Any, DeepProfileData]:
|
|
488
|
+
"""Run a function with deep cProfile profiling."""
|
|
489
|
+
profiler = cProfile.Profile()
|
|
490
|
+
profiler.enable()
|
|
491
|
+
|
|
492
|
+
try:
|
|
493
|
+
result = func()
|
|
494
|
+
finally:
|
|
495
|
+
profiler.disable()
|
|
496
|
+
|
|
497
|
+
# Extract deep profile data
|
|
498
|
+
functions = self._extract_function_stats(profiler, limit=limit)
|
|
499
|
+
call_graph = self._extract_call_graph(profiler)
|
|
500
|
+
module_breakdown = self._extract_module_breakdown(functions)
|
|
501
|
+
|
|
502
|
+
deep_profile = DeepProfileData(
|
|
503
|
+
functions=functions,
|
|
504
|
+
call_graph=call_graph,
|
|
505
|
+
module_breakdown=module_breakdown,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
return result, deep_profile
|
|
509
|
+
|
|
510
|
+
def benchmark_praisonai_agent_deep(self, prompt: str, iteration: int, is_cold: bool, limit: int = 30) -> BenchmarkRun:
|
|
511
|
+
"""Benchmark PraisonAI Agent with deep cProfile profiling (in-process)."""
|
|
512
|
+
t0 = time.perf_counter()
|
|
513
|
+
|
|
514
|
+
# Import timing
|
|
515
|
+
try:
|
|
516
|
+
from praisonaiagents import Agent
|
|
517
|
+
except ImportError:
|
|
518
|
+
return BenchmarkRun(
|
|
519
|
+
path_name="praisonai_agent",
|
|
520
|
+
iteration=iteration,
|
|
521
|
+
is_cold=is_cold,
|
|
522
|
+
timings=PhaseTimings(),
|
|
523
|
+
success=False,
|
|
524
|
+
error="praisonaiagents not installed"
|
|
525
|
+
)
|
|
526
|
+
t_import = time.perf_counter()
|
|
527
|
+
import_ms = (t_import - t0) * 1000
|
|
528
|
+
|
|
529
|
+
# Run with deep profiling
|
|
530
|
+
profiler = cProfile.Profile()
|
|
531
|
+
profiler.enable()
|
|
532
|
+
|
|
533
|
+
try:
|
|
534
|
+
agent = Agent(instructions="You are helpful", llm=self.DEFAULT_MODEL, output="minimal")
|
|
535
|
+
t_init = time.perf_counter()
|
|
536
|
+
init_ms = (t_init - t_import) * 1000
|
|
537
|
+
|
|
538
|
+
result = agent.start(prompt)
|
|
539
|
+
t_network = time.perf_counter()
|
|
540
|
+
network_ms = (t_network - t_init) * 1000
|
|
541
|
+
except Exception as e:
|
|
542
|
+
profiler.disable()
|
|
543
|
+
return BenchmarkRun(
|
|
544
|
+
path_name="praisonai_agent",
|
|
545
|
+
iteration=iteration,
|
|
546
|
+
is_cold=is_cold,
|
|
547
|
+
timings=PhaseTimings(),
|
|
548
|
+
success=False,
|
|
549
|
+
error=str(e)
|
|
550
|
+
)
|
|
551
|
+
finally:
|
|
552
|
+
profiler.disable()
|
|
553
|
+
|
|
554
|
+
total_ms = (t_network - t0) * 1000
|
|
555
|
+
|
|
556
|
+
# Extract deep profile data
|
|
557
|
+
functions = self._extract_function_stats(profiler, limit=limit)
|
|
558
|
+
call_graph = self._extract_call_graph(profiler)
|
|
559
|
+
module_breakdown = self._extract_module_breakdown(functions)
|
|
560
|
+
|
|
561
|
+
deep_profile = DeepProfileData(
|
|
562
|
+
functions=functions,
|
|
563
|
+
call_graph=call_graph,
|
|
564
|
+
module_breakdown=module_breakdown,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
return BenchmarkRun(
|
|
568
|
+
path_name="praisonai_agent",
|
|
569
|
+
iteration=iteration,
|
|
570
|
+
is_cold=is_cold,
|
|
571
|
+
timings=PhaseTimings(
|
|
572
|
+
import_ms=import_ms,
|
|
573
|
+
init_ms=init_ms,
|
|
574
|
+
network_ms=network_ms,
|
|
575
|
+
total_ms=total_ms,
|
|
576
|
+
),
|
|
577
|
+
success=True,
|
|
578
|
+
response_preview=str(result)[:50] if result else "",
|
|
579
|
+
deep_profile=deep_profile,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
def benchmark_openai_sdk_deep(self, prompt: str, iteration: int, is_cold: bool, limit: int = 30) -> BenchmarkRun:
|
|
583
|
+
"""Benchmark OpenAI SDK with deep cProfile profiling (in-process)."""
|
|
584
|
+
t0 = time.perf_counter()
|
|
585
|
+
|
|
586
|
+
# Import timing
|
|
587
|
+
try:
|
|
588
|
+
import openai
|
|
589
|
+
except ImportError:
|
|
590
|
+
return BenchmarkRun(
|
|
591
|
+
path_name="openai_sdk",
|
|
592
|
+
iteration=iteration,
|
|
593
|
+
is_cold=is_cold,
|
|
594
|
+
timings=PhaseTimings(),
|
|
595
|
+
success=False,
|
|
596
|
+
error="openai not installed"
|
|
597
|
+
)
|
|
598
|
+
t_import = time.perf_counter()
|
|
599
|
+
import_ms = (t_import - t0) * 1000
|
|
600
|
+
|
|
601
|
+
# Run with deep profiling
|
|
602
|
+
profiler = cProfile.Profile()
|
|
603
|
+
profiler.enable()
|
|
604
|
+
|
|
605
|
+
try:
|
|
606
|
+
client = openai.OpenAI()
|
|
607
|
+
t_init = time.perf_counter()
|
|
608
|
+
init_ms = (t_init - t_import) * 1000
|
|
609
|
+
|
|
610
|
+
response = client.chat.completions.create(
|
|
611
|
+
model=self.DEFAULT_MODEL,
|
|
612
|
+
messages=[{"role": "user", "content": prompt}],
|
|
613
|
+
max_tokens=10
|
|
614
|
+
)
|
|
615
|
+
result = response.choices[0].message.content
|
|
616
|
+
t_network = time.perf_counter()
|
|
617
|
+
network_ms = (t_network - t_init) * 1000
|
|
618
|
+
except Exception as e:
|
|
619
|
+
profiler.disable()
|
|
620
|
+
return BenchmarkRun(
|
|
621
|
+
path_name="openai_sdk",
|
|
622
|
+
iteration=iteration,
|
|
623
|
+
is_cold=is_cold,
|
|
624
|
+
timings=PhaseTimings(),
|
|
625
|
+
success=False,
|
|
626
|
+
error=str(e)
|
|
627
|
+
)
|
|
628
|
+
finally:
|
|
629
|
+
profiler.disable()
|
|
630
|
+
|
|
631
|
+
total_ms = (t_network - t0) * 1000
|
|
632
|
+
|
|
633
|
+
# Extract deep profile data
|
|
634
|
+
functions = self._extract_function_stats(profiler, limit=limit)
|
|
635
|
+
call_graph = self._extract_call_graph(profiler)
|
|
636
|
+
module_breakdown = self._extract_module_breakdown(functions)
|
|
637
|
+
|
|
638
|
+
deep_profile = DeepProfileData(
|
|
639
|
+
functions=functions,
|
|
640
|
+
call_graph=call_graph,
|
|
641
|
+
module_breakdown=module_breakdown,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
return BenchmarkRun(
|
|
645
|
+
path_name="openai_sdk",
|
|
646
|
+
iteration=iteration,
|
|
647
|
+
is_cold=is_cold,
|
|
648
|
+
timings=PhaseTimings(
|
|
649
|
+
import_ms=import_ms,
|
|
650
|
+
init_ms=init_ms,
|
|
651
|
+
network_ms=network_ms,
|
|
652
|
+
total_ms=total_ms,
|
|
653
|
+
),
|
|
654
|
+
success=True,
|
|
655
|
+
response_preview=str(result)[:50] if result else "",
|
|
656
|
+
deep_profile=deep_profile,
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
def benchmark_openai_sdk(self, prompt: str, iteration: int, is_cold: bool) -> BenchmarkRun:
|
|
660
|
+
"""Benchmark raw OpenAI SDK."""
|
|
661
|
+
code = f'''
|
|
662
|
+
import time
|
|
663
|
+
import json
|
|
664
|
+
import os
|
|
665
|
+
|
|
666
|
+
t0 = time.perf_counter()
|
|
667
|
+
import openai
|
|
668
|
+
t_import = time.perf_counter()
|
|
669
|
+
|
|
670
|
+
client = openai.OpenAI()
|
|
671
|
+
t_init = time.perf_counter()
|
|
672
|
+
|
|
673
|
+
response = client.chat.completions.create(
|
|
674
|
+
model="{self.DEFAULT_MODEL}",
|
|
675
|
+
messages=[{{"role": "user", "content": "{prompt}"}}],
|
|
676
|
+
max_tokens=10
|
|
677
|
+
)
|
|
678
|
+
t_network = time.perf_counter()
|
|
679
|
+
|
|
680
|
+
result = response.choices[0].message.content
|
|
681
|
+
t_end = time.perf_counter()
|
|
682
|
+
|
|
683
|
+
print(json.dumps({{
|
|
684
|
+
"import_ms": (t_import - t0) * 1000,
|
|
685
|
+
"init_ms": (t_init - t_import) * 1000,
|
|
686
|
+
"network_ms": (t_network - t_init) * 1000,
|
|
687
|
+
"exec_ms": (t_end - t_network) * 1000,
|
|
688
|
+
"total_ms": (t_end - t0) * 1000,
|
|
689
|
+
"response": result[:50]
|
|
690
|
+
}}))
|
|
691
|
+
'''
|
|
692
|
+
timings, stdout, stderr = self._run_subprocess_benchmark(code)
|
|
693
|
+
|
|
694
|
+
return BenchmarkRun(
|
|
695
|
+
path_name="openai_sdk",
|
|
696
|
+
iteration=iteration,
|
|
697
|
+
is_cold=is_cold,
|
|
698
|
+
timings=PhaseTimings(
|
|
699
|
+
import_ms=timings.get("import_ms", 0),
|
|
700
|
+
init_ms=timings.get("init_ms", 0),
|
|
701
|
+
network_ms=timings.get("network_ms", 0),
|
|
702
|
+
exec_ms=timings.get("exec_ms", 0),
|
|
703
|
+
total_ms=timings.get("total_ms", 0),
|
|
704
|
+
subprocess_ms=timings.get("subprocess_ms", 0),
|
|
705
|
+
),
|
|
706
|
+
success="total_ms" in timings,
|
|
707
|
+
error=stderr if "total_ms" not in timings else "",
|
|
708
|
+
response_preview=timings.get("response", "")[:50]
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
def benchmark_praisonai_agent(self, prompt: str, iteration: int, is_cold: bool) -> BenchmarkRun:
|
|
712
|
+
"""Benchmark PraisonAI Agent (Python SDK)."""
|
|
713
|
+
code = f'''
|
|
714
|
+
import time
|
|
715
|
+
import json
|
|
716
|
+
import sys
|
|
717
|
+
sys.path.insert(0, "{self._get_project_root()}/src/praisonai")
|
|
718
|
+
|
|
719
|
+
t0 = time.perf_counter()
|
|
720
|
+
from praisonaiagents import Agent
|
|
721
|
+
t_import = time.perf_counter()
|
|
722
|
+
|
|
723
|
+
agent = Agent(instructions="You are helpful", llm="{self.DEFAULT_MODEL}", output="minimal")
|
|
724
|
+
t_init = time.perf_counter()
|
|
725
|
+
|
|
726
|
+
result = agent.start("{prompt}")
|
|
727
|
+
t_network = time.perf_counter()
|
|
728
|
+
|
|
729
|
+
print(json.dumps({{
|
|
730
|
+
"import_ms": (t_import - t0) * 1000,
|
|
731
|
+
"init_ms": (t_init - t_import) * 1000,
|
|
732
|
+
"network_ms": (t_network - t_init) * 1000,
|
|
733
|
+
"total_ms": (t_network - t0) * 1000,
|
|
734
|
+
"response": str(result)[:50] if result else ""
|
|
735
|
+
}}))
|
|
736
|
+
'''
|
|
737
|
+
timings, stdout, stderr = self._run_subprocess_benchmark(code)
|
|
738
|
+
|
|
739
|
+
return BenchmarkRun(
|
|
740
|
+
path_name="praisonai_agent",
|
|
741
|
+
iteration=iteration,
|
|
742
|
+
is_cold=is_cold,
|
|
743
|
+
timings=PhaseTimings(
|
|
744
|
+
import_ms=timings.get("import_ms", 0),
|
|
745
|
+
init_ms=timings.get("init_ms", 0),
|
|
746
|
+
network_ms=timings.get("network_ms", 0),
|
|
747
|
+
total_ms=timings.get("total_ms", 0),
|
|
748
|
+
subprocess_ms=timings.get("subprocess_ms", 0),
|
|
749
|
+
),
|
|
750
|
+
success="total_ms" in timings,
|
|
751
|
+
error=stderr if "total_ms" not in timings else "",
|
|
752
|
+
response_preview=timings.get("response", "")[:50]
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
def benchmark_praisonai_cli(self, prompt: str, iteration: int, is_cold: bool, with_profile: bool = False) -> BenchmarkRun:
|
|
756
|
+
"""Benchmark PraisonAI CLI."""
|
|
757
|
+
t0 = time.perf_counter()
|
|
758
|
+
|
|
759
|
+
cmd = ["praisonai", prompt, "--llm", self.DEFAULT_MODEL]
|
|
760
|
+
if with_profile:
|
|
761
|
+
cmd.append("--profile")
|
|
762
|
+
|
|
763
|
+
result = subprocess.run(
|
|
764
|
+
cmd,
|
|
765
|
+
capture_output=True,
|
|
766
|
+
text=True,
|
|
767
|
+
cwd=self._get_project_root(),
|
|
768
|
+
timeout=120
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
total_ms = (time.perf_counter() - t0) * 1000
|
|
772
|
+
|
|
773
|
+
return BenchmarkRun(
|
|
774
|
+
path_name="praisonai_cli_profile" if with_profile else "praisonai_cli",
|
|
775
|
+
iteration=iteration,
|
|
776
|
+
is_cold=is_cold,
|
|
777
|
+
timings=PhaseTimings(
|
|
778
|
+
total_ms=total_ms,
|
|
779
|
+
subprocess_ms=total_ms,
|
|
780
|
+
),
|
|
781
|
+
success=result.returncode == 0,
|
|
782
|
+
error=result.stderr[:200] if result.returncode != 0 else "",
|
|
783
|
+
response_preview=result.stdout[:50] if result.stdout else ""
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
def benchmark_praisonai_workflow_single(self, prompt: str, iteration: int, is_cold: bool) -> BenchmarkRun:
|
|
787
|
+
"""Benchmark PraisonAI Workflow with single agent."""
|
|
788
|
+
code = f'''
|
|
789
|
+
import time
|
|
790
|
+
import json
|
|
791
|
+
import sys
|
|
792
|
+
sys.path.insert(0, "{self._get_project_root()}/src/praisonai")
|
|
793
|
+
|
|
794
|
+
t0 = time.perf_counter()
|
|
795
|
+
from praisonaiagents import Agent, Task, AgentTeam
|
|
796
|
+
t_import = time.perf_counter()
|
|
797
|
+
|
|
798
|
+
agent = Agent(name="Helper", instructions="You are helpful", llm="{self.DEFAULT_MODEL}", output="minimal")
|
|
799
|
+
task = Task(name="respond", description="{prompt}", expected_output="response", agent=agent)
|
|
800
|
+
agents = AgentTeam(agents=[agent], tasks=[task], verbose=0)
|
|
801
|
+
t_init = time.perf_counter()
|
|
802
|
+
|
|
803
|
+
result = agents.start()
|
|
804
|
+
t_network = time.perf_counter()
|
|
805
|
+
|
|
806
|
+
print(json.dumps({{
|
|
807
|
+
"import_ms": (t_import - t0) * 1000,
|
|
808
|
+
"init_ms": (t_init - t_import) * 1000,
|
|
809
|
+
"network_ms": (t_network - t_init) * 1000,
|
|
810
|
+
"total_ms": (t_network - t0) * 1000,
|
|
811
|
+
"response": str(result)[:50] if result else ""
|
|
812
|
+
}}))
|
|
813
|
+
'''
|
|
814
|
+
timings, stdout, stderr = self._run_subprocess_benchmark(code)
|
|
815
|
+
|
|
816
|
+
return BenchmarkRun(
|
|
817
|
+
path_name="praisonai_workflow_single",
|
|
818
|
+
iteration=iteration,
|
|
819
|
+
is_cold=is_cold,
|
|
820
|
+
timings=PhaseTimings(
|
|
821
|
+
import_ms=timings.get("import_ms", 0),
|
|
822
|
+
init_ms=timings.get("init_ms", 0),
|
|
823
|
+
network_ms=timings.get("network_ms", 0),
|
|
824
|
+
total_ms=timings.get("total_ms", 0),
|
|
825
|
+
subprocess_ms=timings.get("subprocess_ms", 0),
|
|
826
|
+
),
|
|
827
|
+
success="total_ms" in timings,
|
|
828
|
+
error=stderr[:200] if "total_ms" not in timings else "",
|
|
829
|
+
response_preview=timings.get("response", "")[:50]
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
def benchmark_praisonai_workflow_multi(self, prompt: str, iteration: int, is_cold: bool) -> BenchmarkRun:
|
|
833
|
+
"""Benchmark PraisonAI Workflow with multiple agents."""
|
|
834
|
+
code = f'''
|
|
835
|
+
import time
|
|
836
|
+
import json
|
|
837
|
+
import sys
|
|
838
|
+
sys.path.insert(0, "{self._get_project_root()}/src/praisonai")
|
|
839
|
+
|
|
840
|
+
t0 = time.perf_counter()
|
|
841
|
+
from praisonaiagents import Agent, Task, AgentTeam
|
|
842
|
+
t_import = time.perf_counter()
|
|
843
|
+
|
|
844
|
+
agent1 = Agent(name="Analyzer", instructions="Analyze the request", llm="{self.DEFAULT_MODEL}", output="minimal")
|
|
845
|
+
agent2 = Agent(name="Responder", instructions="Provide a response", llm="{self.DEFAULT_MODEL}", output="minimal")
|
|
846
|
+
task1 = Task(name="analyze", description="Analyze: {prompt}", expected_output="analysis", agent=agent1)
|
|
847
|
+
task2 = Task(name="respond", description="Respond based on analysis", expected_output="response", agent=agent2)
|
|
848
|
+
agents = AgentTeam(agents=[agent1, agent2], tasks=[task1, task2], verbose=0)
|
|
849
|
+
t_init = time.perf_counter()
|
|
850
|
+
|
|
851
|
+
result = agents.start()
|
|
852
|
+
t_network = time.perf_counter()
|
|
853
|
+
|
|
854
|
+
print(json.dumps({{
|
|
855
|
+
"import_ms": (t_import - t0) * 1000,
|
|
856
|
+
"init_ms": (t_init - t_import) * 1000,
|
|
857
|
+
"network_ms": (t_network - t_init) * 1000,
|
|
858
|
+
"total_ms": (t_network - t0) * 1000,
|
|
859
|
+
"response": str(result)[:50] if result else ""
|
|
860
|
+
}}))
|
|
861
|
+
'''
|
|
862
|
+
timings, stdout, stderr = self._run_subprocess_benchmark(code)
|
|
863
|
+
|
|
864
|
+
return BenchmarkRun(
|
|
865
|
+
path_name="praisonai_workflow_multi",
|
|
866
|
+
iteration=iteration,
|
|
867
|
+
is_cold=is_cold,
|
|
868
|
+
timings=PhaseTimings(
|
|
869
|
+
import_ms=timings.get("import_ms", 0),
|
|
870
|
+
init_ms=timings.get("init_ms", 0),
|
|
871
|
+
network_ms=timings.get("network_ms", 0),
|
|
872
|
+
total_ms=timings.get("total_ms", 0),
|
|
873
|
+
subprocess_ms=timings.get("subprocess_ms", 0),
|
|
874
|
+
),
|
|
875
|
+
success="total_ms" in timings,
|
|
876
|
+
error=stderr[:200] if "total_ms" not in timings else "",
|
|
877
|
+
response_preview=timings.get("response", "")[:50]
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
def benchmark_praisonai_litellm(self, prompt: str, iteration: int, is_cold: bool) -> BenchmarkRun:
|
|
881
|
+
"""Benchmark PraisonAI Agent using LiteLLM path (openai/ prefix)."""
|
|
882
|
+
code = f'''
|
|
883
|
+
import time
|
|
884
|
+
import json
|
|
885
|
+
import sys
|
|
886
|
+
sys.path.insert(0, "{self._get_project_root()}/src/praisonai")
|
|
887
|
+
|
|
888
|
+
t0 = time.perf_counter()
|
|
889
|
+
from praisonaiagents import Agent
|
|
890
|
+
t_import = time.perf_counter()
|
|
891
|
+
|
|
892
|
+
agent = Agent(instructions="You are helpful", llm="openai/{self.DEFAULT_MODEL}", output="minimal")
|
|
893
|
+
t_init = time.perf_counter()
|
|
894
|
+
|
|
895
|
+
result = agent.start("{prompt}")
|
|
896
|
+
t_network = time.perf_counter()
|
|
897
|
+
|
|
898
|
+
print(json.dumps({{
|
|
899
|
+
"import_ms": (t_import - t0) * 1000,
|
|
900
|
+
"init_ms": (t_init - t_import) * 1000,
|
|
901
|
+
"network_ms": (t_network - t_init) * 1000,
|
|
902
|
+
"total_ms": (t_network - t0) * 1000,
|
|
903
|
+
"response": str(result)[:50] if result else ""
|
|
904
|
+
}}))
|
|
905
|
+
'''
|
|
906
|
+
timings, stdout, stderr = self._run_subprocess_benchmark(code)
|
|
907
|
+
|
|
908
|
+
return BenchmarkRun(
|
|
909
|
+
path_name="praisonai_litellm",
|
|
910
|
+
iteration=iteration,
|
|
911
|
+
is_cold=is_cold,
|
|
912
|
+
timings=PhaseTimings(
|
|
913
|
+
import_ms=timings.get("import_ms", 0),
|
|
914
|
+
init_ms=timings.get("init_ms", 0),
|
|
915
|
+
network_ms=timings.get("network_ms", 0),
|
|
916
|
+
total_ms=timings.get("total_ms", 0),
|
|
917
|
+
subprocess_ms=timings.get("subprocess_ms", 0),
|
|
918
|
+
),
|
|
919
|
+
success="total_ms" in timings,
|
|
920
|
+
error=stderr[:200] if "total_ms" not in timings else "",
|
|
921
|
+
response_preview=timings.get("response", "")[:50]
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
def benchmark_litellm_standalone(self, prompt: str, iteration: int, is_cold: bool) -> BenchmarkRun:
|
|
925
|
+
"""Benchmark LiteLLM standalone (raw)."""
|
|
926
|
+
code = f'''
|
|
927
|
+
import time
|
|
928
|
+
import json
|
|
929
|
+
import os
|
|
930
|
+
|
|
931
|
+
t0 = time.perf_counter()
|
|
932
|
+
import litellm
|
|
933
|
+
t_import = time.perf_counter()
|
|
934
|
+
|
|
935
|
+
# Configure
|
|
936
|
+
litellm.drop_params = True
|
|
937
|
+
t_init = time.perf_counter()
|
|
938
|
+
|
|
939
|
+
response = litellm.completion(
|
|
940
|
+
model="{self.DEFAULT_MODEL}",
|
|
941
|
+
messages=[{{"role": "user", "content": "{prompt}"}}],
|
|
942
|
+
max_tokens=10
|
|
943
|
+
)
|
|
944
|
+
t_network = time.perf_counter()
|
|
945
|
+
|
|
946
|
+
result = response.choices[0].message.content
|
|
947
|
+
t_end = time.perf_counter()
|
|
948
|
+
|
|
949
|
+
print(json.dumps({{
|
|
950
|
+
"import_ms": (t_import - t0) * 1000,
|
|
951
|
+
"init_ms": (t_init - t_import) * 1000,
|
|
952
|
+
"network_ms": (t_network - t_init) * 1000,
|
|
953
|
+
"exec_ms": (t_end - t_network) * 1000,
|
|
954
|
+
"total_ms": (t_end - t0) * 1000,
|
|
955
|
+
"response": result[:50] if result else ""
|
|
956
|
+
}}))
|
|
957
|
+
'''
|
|
958
|
+
timings, stdout, stderr = self._run_subprocess_benchmark(code)
|
|
959
|
+
|
|
960
|
+
return BenchmarkRun(
|
|
961
|
+
path_name="litellm_standalone",
|
|
962
|
+
iteration=iteration,
|
|
963
|
+
is_cold=is_cold,
|
|
964
|
+
timings=PhaseTimings(
|
|
965
|
+
import_ms=timings.get("import_ms", 0),
|
|
966
|
+
init_ms=timings.get("init_ms", 0),
|
|
967
|
+
network_ms=timings.get("network_ms", 0),
|
|
968
|
+
exec_ms=timings.get("exec_ms", 0),
|
|
969
|
+
total_ms=timings.get("total_ms", 0),
|
|
970
|
+
subprocess_ms=timings.get("subprocess_ms", 0),
|
|
971
|
+
),
|
|
972
|
+
success="total_ms" in timings,
|
|
973
|
+
error=stderr[:200] if "total_ms" not in timings else "",
|
|
974
|
+
response_preview=timings.get("response", "")[:50]
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
def create_timeline_diagram(self, result: BenchmarkResult) -> str:
|
|
978
|
+
"""Create ASCII timeline diagram for a benchmark result."""
|
|
979
|
+
if not result.runs:
|
|
980
|
+
return "No data"
|
|
981
|
+
|
|
982
|
+
# Use mean values
|
|
983
|
+
import_ms = result.mean_import_ms
|
|
984
|
+
init_ms = result.mean_init_ms
|
|
985
|
+
network_ms = result.mean_network_ms
|
|
986
|
+
total_ms = result.mean_total_ms
|
|
987
|
+
|
|
988
|
+
# Calculate other time
|
|
989
|
+
other_ms = max(0, total_ms - import_ms - init_ms - network_ms)
|
|
990
|
+
|
|
991
|
+
# Scale to 60 chars width
|
|
992
|
+
if total_ms <= 0:
|
|
993
|
+
return "No timing data"
|
|
994
|
+
|
|
995
|
+
scale = 50.0 / total_ms
|
|
996
|
+
|
|
997
|
+
def bar_width(ms):
|
|
998
|
+
return max(1, int(ms * scale))
|
|
999
|
+
|
|
1000
|
+
import_w = bar_width(import_ms)
|
|
1001
|
+
init_w = bar_width(init_ms)
|
|
1002
|
+
network_w = bar_width(network_ms)
|
|
1003
|
+
other_w = bar_width(other_ms)
|
|
1004
|
+
|
|
1005
|
+
# Build diagram
|
|
1006
|
+
lines = []
|
|
1007
|
+
|
|
1008
|
+
# Top line
|
|
1009
|
+
total_w = import_w + init_w + network_w + other_w
|
|
1010
|
+
lines.append(f"ENTER {'─' * total_w}► RESPONSE")
|
|
1011
|
+
|
|
1012
|
+
# Phase boxes
|
|
1013
|
+
phases = []
|
|
1014
|
+
if import_ms > 0:
|
|
1015
|
+
phases.append(("import", import_ms, import_w))
|
|
1016
|
+
if init_ms > 0:
|
|
1017
|
+
phases.append(("init", init_ms, init_w))
|
|
1018
|
+
if network_ms > 0:
|
|
1019
|
+
phases.append(("network", network_ms, network_w))
|
|
1020
|
+
if other_ms > 10:
|
|
1021
|
+
phases.append(("other", other_ms, other_w))
|
|
1022
|
+
|
|
1023
|
+
if phases:
|
|
1024
|
+
# Phase names line
|
|
1025
|
+
name_line = " "
|
|
1026
|
+
for name, ms, w in phases:
|
|
1027
|
+
name_line += "│" + name.center(w - 1)
|
|
1028
|
+
name_line += "│"
|
|
1029
|
+
lines.append(name_line)
|
|
1030
|
+
|
|
1031
|
+
# Phase times line
|
|
1032
|
+
time_line = " "
|
|
1033
|
+
for name, ms, w in phases:
|
|
1034
|
+
time_str = f"{ms:.0f}ms"
|
|
1035
|
+
time_line += "│" + time_str.center(w - 1)
|
|
1036
|
+
time_line += "│"
|
|
1037
|
+
lines.append(time_line)
|
|
1038
|
+
|
|
1039
|
+
# Bottom line
|
|
1040
|
+
bottom_line = " "
|
|
1041
|
+
for i, (name, ms, w) in enumerate(phases):
|
|
1042
|
+
if i == 0:
|
|
1043
|
+
bottom_line += "└" + "─" * (w - 1)
|
|
1044
|
+
else:
|
|
1045
|
+
bottom_line += "┴" + "─" * (w - 1)
|
|
1046
|
+
bottom_line += "┘"
|
|
1047
|
+
lines.append(bottom_line)
|
|
1048
|
+
|
|
1049
|
+
# Total line
|
|
1050
|
+
lines.append(f"{' ' * 40}TOTAL: {total_ms:.0f}ms")
|
|
1051
|
+
|
|
1052
|
+
return "\n".join(lines)
|
|
1053
|
+
|
|
1054
|
+
def create_comparison_table(self, report: BenchmarkReport) -> str:
|
|
1055
|
+
"""Create master ASCII comparison table."""
|
|
1056
|
+
lines = []
|
|
1057
|
+
|
|
1058
|
+
# Header
|
|
1059
|
+
lines.append("+" + "-" * 30 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 12 + "+")
|
|
1060
|
+
lines.append(f"| {'Path':<28} | {'Import':>8} | {'Init':>8} | {'Network':>8} | {'Total':>8} | {'Δ SDK':>10} |")
|
|
1061
|
+
lines.append("+" + "-" * 30 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 12 + "+")
|
|
1062
|
+
|
|
1063
|
+
# Sort by total time
|
|
1064
|
+
sorted_results = sorted(report.results.values(), key=lambda r: r.mean_total_ms)
|
|
1065
|
+
|
|
1066
|
+
for result in sorted_results:
|
|
1067
|
+
delta_str = f"{result.delta_vs_sdk_ms:+.0f}ms" if result.delta_vs_sdk_ms != 0 else "baseline"
|
|
1068
|
+
lines.append(
|
|
1069
|
+
f"| {result.path_name:<28} | "
|
|
1070
|
+
f"{result.mean_import_ms:>6.0f}ms | "
|
|
1071
|
+
f"{result.mean_init_ms:>6.0f}ms | "
|
|
1072
|
+
f"{result.mean_network_ms:>6.0f}ms | "
|
|
1073
|
+
f"{result.mean_total_ms:>6.0f}ms | "
|
|
1074
|
+
f"{delta_str:>10} |"
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
lines.append("+" + "-" * 30 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 12 + "+")
|
|
1078
|
+
|
|
1079
|
+
return "\n".join(lines)
|
|
1080
|
+
|
|
1081
|
+
def create_variance_table(self, report: BenchmarkReport) -> str:
|
|
1082
|
+
"""Create variance analysis table."""
|
|
1083
|
+
lines = []
|
|
1084
|
+
|
|
1085
|
+
lines.append("\n## Variance Analysis")
|
|
1086
|
+
lines.append("+" + "-" * 30 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 12 + "+")
|
|
1087
|
+
lines.append(f"| {'Path':<28} | {'Mean':>8} | {'Min':>8} | {'Max':>8} | {'StdDev':>8} | {'Cold/Warm':>10} |")
|
|
1088
|
+
lines.append("+" + "-" * 30 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 12 + "+")
|
|
1089
|
+
|
|
1090
|
+
for name, result in sorted(report.results.items(), key=lambda x: x[1].mean_total_ms):
|
|
1091
|
+
cold_warm = f"{result.cold_total_ms:.0f}/{result.warm_total_ms:.0f}" if result.cold_total_ms > 0 else "N/A"
|
|
1092
|
+
lines.append(
|
|
1093
|
+
f"| {name:<28} | "
|
|
1094
|
+
f"{result.mean_total_ms:>6.0f}ms | "
|
|
1095
|
+
f"{result.min_total_ms:>6.0f}ms | "
|
|
1096
|
+
f"{result.max_total_ms:>6.0f}ms | "
|
|
1097
|
+
f"{result.std_total_ms:>6.0f}ms | "
|
|
1098
|
+
f"{cold_warm:>10} |"
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
lines.append("+" + "-" * 30 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 10 + "+" + "-" * 12 + "+")
|
|
1102
|
+
|
|
1103
|
+
return "\n".join(lines)
|
|
1104
|
+
|
|
1105
|
+
def classify_overhead(self, report: BenchmarkReport) -> str:
|
|
1106
|
+
"""Classify overhead by category."""
|
|
1107
|
+
lines = []
|
|
1108
|
+
|
|
1109
|
+
sdk_result = report.results.get("openai_sdk")
|
|
1110
|
+
if not sdk_result:
|
|
1111
|
+
return "No SDK baseline for comparison"
|
|
1112
|
+
|
|
1113
|
+
lines.append("\n## Overhead Classification")
|
|
1114
|
+
lines.append("-" * 60)
|
|
1115
|
+
|
|
1116
|
+
for name, result in report.results.items():
|
|
1117
|
+
if name == "openai_sdk":
|
|
1118
|
+
continue
|
|
1119
|
+
|
|
1120
|
+
total_overhead = result.delta_vs_sdk_ms
|
|
1121
|
+
|
|
1122
|
+
# Classify overhead
|
|
1123
|
+
network_variance = abs(result.mean_network_ms - sdk_result.mean_network_ms)
|
|
1124
|
+
import_overhead = result.mean_import_ms - sdk_result.mean_import_ms
|
|
1125
|
+
init_overhead = result.mean_init_ms - sdk_result.mean_init_ms
|
|
1126
|
+
|
|
1127
|
+
lines.append(f"\n{name}:")
|
|
1128
|
+
lines.append(f" Total overhead: {total_overhead:+.0f}ms")
|
|
1129
|
+
lines.append(f" - Network variance (unavoidable): ~{network_variance:.0f}ms")
|
|
1130
|
+
lines.append(f" - Import overhead: {import_overhead:+.0f}ms")
|
|
1131
|
+
lines.append(f" - Init overhead: {init_overhead:+.0f}ms")
|
|
1132
|
+
|
|
1133
|
+
# Categorize
|
|
1134
|
+
if "cli" in name.lower():
|
|
1135
|
+
lines.append(f" - CLI subprocess overhead: ~100-300ms (estimated)")
|
|
1136
|
+
if "litellm" in name.lower():
|
|
1137
|
+
lines.append(f" - LiteLLM wrapper overhead: included in init")
|
|
1138
|
+
if "workflow" in name.lower():
|
|
1139
|
+
lines.append(f" - Workflow orchestration: included in init")
|
|
1140
|
+
|
|
1141
|
+
return "\n".join(lines)
|
|
1142
|
+
|
|
1143
|
+
def create_deep_profile_output(self, result: BenchmarkResult, limit: int = 30) -> str:
|
|
1144
|
+
"""Create deep profile text output for a benchmark result."""
|
|
1145
|
+
lines = []
|
|
1146
|
+
|
|
1147
|
+
if not result.aggregated_functions:
|
|
1148
|
+
return ""
|
|
1149
|
+
|
|
1150
|
+
lines.append("\n## Deep Profile: Top Functions by Cumulative Time")
|
|
1151
|
+
lines.append("-" * 80)
|
|
1152
|
+
lines.append(f"{'Function':<45} {'Calls':>8} {'Self (ms)':>12} {'Cumul (ms)':>12}")
|
|
1153
|
+
lines.append("-" * 80)
|
|
1154
|
+
|
|
1155
|
+
for func in result.aggregated_functions[:limit]:
|
|
1156
|
+
name = func.name[:43] if len(func.name) > 43 else func.name
|
|
1157
|
+
lines.append(f"{name:<45} {func.calls:>8} {func.total_time_ms:>12.2f} {func.cumulative_time_ms:>12.2f}")
|
|
1158
|
+
|
|
1159
|
+
lines.append("-" * 80)
|
|
1160
|
+
|
|
1161
|
+
# Top functions by self time
|
|
1162
|
+
by_self = sorted(result.aggregated_functions, key=lambda x: x.total_time_ms, reverse=True)[:10]
|
|
1163
|
+
lines.append("\n## Top Functions by Self Time")
|
|
1164
|
+
lines.append("-" * 80)
|
|
1165
|
+
lines.append(f"{'Function':<45} {'Calls':>8} {'Self (ms)':>12} {'Cumul (ms)':>12}")
|
|
1166
|
+
lines.append("-" * 80)
|
|
1167
|
+
|
|
1168
|
+
for func in by_self:
|
|
1169
|
+
name = func.name[:43] if len(func.name) > 43 else func.name
|
|
1170
|
+
lines.append(f"{name:<45} {func.calls:>8} {func.total_time_ms:>12.2f} {func.cumulative_time_ms:>12.2f}")
|
|
1171
|
+
|
|
1172
|
+
lines.append("-" * 80)
|
|
1173
|
+
|
|
1174
|
+
# Module breakdown
|
|
1175
|
+
if result.aggregated_module_breakdown:
|
|
1176
|
+
mb = result.aggregated_module_breakdown
|
|
1177
|
+
lines.append("\n## Module Breakdown (by cumulative time)")
|
|
1178
|
+
lines.append("-" * 60)
|
|
1179
|
+
|
|
1180
|
+
if mb.praisonai_modules:
|
|
1181
|
+
lines.append("\nPraisonAI CLI Modules:")
|
|
1182
|
+
for file, cumul in mb.praisonai_modules[:5]:
|
|
1183
|
+
short_file = "..." + file[-50:] if len(file) > 50 else file
|
|
1184
|
+
lines.append(f" {short_file:<55} {cumul:>10.2f}ms")
|
|
1185
|
+
|
|
1186
|
+
if mb.agent_modules:
|
|
1187
|
+
lines.append("\nPraisonAI Agent Modules:")
|
|
1188
|
+
for file, cumul in mb.agent_modules[:5]:
|
|
1189
|
+
short_file = "..." + file[-50:] if len(file) > 50 else file
|
|
1190
|
+
lines.append(f" {short_file:<55} {cumul:>10.2f}ms")
|
|
1191
|
+
|
|
1192
|
+
if mb.network_modules:
|
|
1193
|
+
lines.append("\nNetwork Modules:")
|
|
1194
|
+
for file, cumul in mb.network_modules[:5]:
|
|
1195
|
+
short_file = "..." + file[-50:] if len(file) > 50 else file
|
|
1196
|
+
lines.append(f" {short_file:<55} {cumul:>10.2f}ms")
|
|
1197
|
+
|
|
1198
|
+
if mb.third_party_modules:
|
|
1199
|
+
lines.append("\nThird-Party Modules:")
|
|
1200
|
+
for file, cumul in mb.third_party_modules[:5]:
|
|
1201
|
+
short_file = "..." + file[-50:] if len(file) > 50 else file
|
|
1202
|
+
lines.append(f" {short_file:<55} {cumul:>10.2f}ms")
|
|
1203
|
+
|
|
1204
|
+
# Call graph summary
|
|
1205
|
+
if result.aggregated_call_graph:
|
|
1206
|
+
cg = result.aggregated_call_graph
|
|
1207
|
+
lines.append(f"\n## Call Graph: {cg.edge_count} edges")
|
|
1208
|
+
|
|
1209
|
+
return "\n".join(lines)
|
|
1210
|
+
|
|
1211
|
+
def run_full_benchmark(self, prompt: str = None, iterations: int = None,
|
|
1212
|
+
paths: List[str] = None, verbose: bool = True,
|
|
1213
|
+
deep: bool = False, limit: int = 30) -> BenchmarkReport:
|
|
1214
|
+
"""
|
|
1215
|
+
Run full benchmark suite.
|
|
1216
|
+
|
|
1217
|
+
Args:
|
|
1218
|
+
prompt: Query to benchmark (default: "Hi")
|
|
1219
|
+
iterations: Number of iterations per path (default: 3)
|
|
1220
|
+
paths: List of paths to benchmark (default: all)
|
|
1221
|
+
verbose: Print progress
|
|
1222
|
+
deep: Enable deep cProfile profiling
|
|
1223
|
+
limit: Number of top functions to show in deep profile
|
|
1224
|
+
|
|
1225
|
+
Returns:
|
|
1226
|
+
BenchmarkReport with all results
|
|
1227
|
+
"""
|
|
1228
|
+
prompt = prompt or self.DEFAULT_PROMPT
|
|
1229
|
+
iterations = iterations or self.DEFAULT_ITERATIONS
|
|
1230
|
+
paths = paths or self.PATHS
|
|
1231
|
+
|
|
1232
|
+
from datetime import datetime
|
|
1233
|
+
|
|
1234
|
+
report = BenchmarkReport(
|
|
1235
|
+
timestamp=datetime.utcnow().isoformat() + "Z",
|
|
1236
|
+
prompt=prompt,
|
|
1237
|
+
iterations=iterations,
|
|
1238
|
+
)
|
|
1239
|
+
|
|
1240
|
+
if verbose:
|
|
1241
|
+
print("=" * 70)
|
|
1242
|
+
print("PraisonAI Comprehensive Benchmark Suite")
|
|
1243
|
+
print("=" * 70)
|
|
1244
|
+
print(f"Prompt: \"{prompt}\"")
|
|
1245
|
+
print(f"Iterations: {iterations}")
|
|
1246
|
+
print(f"Paths: {len(paths)}")
|
|
1247
|
+
if deep:
|
|
1248
|
+
print(f"Deep Profiling: ENABLED (limit={limit})")
|
|
1249
|
+
print("=" * 70)
|
|
1250
|
+
|
|
1251
|
+
# Benchmark each path - use deep profiling methods when deep=True
|
|
1252
|
+
if deep:
|
|
1253
|
+
# Deep profiling uses in-process methods for supported paths
|
|
1254
|
+
benchmark_methods = {
|
|
1255
|
+
"openai_sdk": lambda p, i, c: self.benchmark_openai_sdk_deep(p, i, c, limit=limit),
|
|
1256
|
+
"praisonai_agent": lambda p, i, c: self.benchmark_praisonai_agent_deep(p, i, c, limit=limit),
|
|
1257
|
+
"praisonai_cli": lambda p, i, c: self.benchmark_praisonai_cli(p, i, c, with_profile=False),
|
|
1258
|
+
"praisonai_cli_profile": lambda p, i, c: self.benchmark_praisonai_cli(p, i, c, with_profile=True),
|
|
1259
|
+
"praisonai_workflow_single": self.benchmark_praisonai_workflow_single,
|
|
1260
|
+
"praisonai_workflow_multi": self.benchmark_praisonai_workflow_multi,
|
|
1261
|
+
"praisonai_litellm": self.benchmark_praisonai_litellm,
|
|
1262
|
+
"litellm_standalone": self.benchmark_litellm_standalone,
|
|
1263
|
+
}
|
|
1264
|
+
else:
|
|
1265
|
+
benchmark_methods = {
|
|
1266
|
+
"openai_sdk": self.benchmark_openai_sdk,
|
|
1267
|
+
"praisonai_agent": self.benchmark_praisonai_agent,
|
|
1268
|
+
"praisonai_cli": lambda p, i, c: self.benchmark_praisonai_cli(p, i, c, with_profile=False),
|
|
1269
|
+
"praisonai_cli_profile": lambda p, i, c: self.benchmark_praisonai_cli(p, i, c, with_profile=True),
|
|
1270
|
+
"praisonai_workflow_single": self.benchmark_praisonai_workflow_single,
|
|
1271
|
+
"praisonai_workflow_multi": self.benchmark_praisonai_workflow_multi,
|
|
1272
|
+
"praisonai_litellm": self.benchmark_praisonai_litellm,
|
|
1273
|
+
"litellm_standalone": self.benchmark_litellm_standalone,
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
for path_name in paths:
|
|
1277
|
+
if path_name not in benchmark_methods:
|
|
1278
|
+
if verbose:
|
|
1279
|
+
print(f"\n⚠ Unknown path: {path_name}")
|
|
1280
|
+
continue
|
|
1281
|
+
|
|
1282
|
+
if verbose:
|
|
1283
|
+
deep_indicator = " [DEEP]" if deep and path_name in ["openai_sdk", "praisonai_agent"] else ""
|
|
1284
|
+
print(f"\n📊 Benchmarking: {path_name}{deep_indicator}")
|
|
1285
|
+
|
|
1286
|
+
result = BenchmarkResult(path_name=path_name)
|
|
1287
|
+
method = benchmark_methods[path_name]
|
|
1288
|
+
|
|
1289
|
+
for i in range(iterations):
|
|
1290
|
+
is_cold = (i == 0)
|
|
1291
|
+
if verbose:
|
|
1292
|
+
print(f" Run {i + 1}/{iterations} ({'cold' if is_cold else 'warm'})...", end=" ", flush=True)
|
|
1293
|
+
|
|
1294
|
+
try:
|
|
1295
|
+
run = method(prompt, i + 1, is_cold)
|
|
1296
|
+
result.runs.append(run)
|
|
1297
|
+
|
|
1298
|
+
if verbose:
|
|
1299
|
+
if run.success:
|
|
1300
|
+
print(f"{run.timings.total_ms:.0f}ms")
|
|
1301
|
+
else:
|
|
1302
|
+
print(f"FAILED: {run.error[:50]}")
|
|
1303
|
+
except Exception as e:
|
|
1304
|
+
if verbose:
|
|
1305
|
+
print(f"ERROR: {e}")
|
|
1306
|
+
result.runs.append(BenchmarkRun(
|
|
1307
|
+
path_name=path_name,
|
|
1308
|
+
iteration=i + 1,
|
|
1309
|
+
is_cold=is_cold,
|
|
1310
|
+
timings=PhaseTimings(),
|
|
1311
|
+
success=False,
|
|
1312
|
+
error=str(e)
|
|
1313
|
+
))
|
|
1314
|
+
|
|
1315
|
+
report.results[path_name] = result
|
|
1316
|
+
|
|
1317
|
+
# Compute stats
|
|
1318
|
+
sdk_baseline = 0.0
|
|
1319
|
+
if "openai_sdk" in report.results:
|
|
1320
|
+
report.results["openai_sdk"].compute_stats(0.0)
|
|
1321
|
+
sdk_baseline = report.results["openai_sdk"].mean_total_ms
|
|
1322
|
+
report.sdk_baseline_ms = sdk_baseline
|
|
1323
|
+
|
|
1324
|
+
for name, result in report.results.items():
|
|
1325
|
+
result.compute_stats(sdk_baseline)
|
|
1326
|
+
|
|
1327
|
+
return report
|
|
1328
|
+
|
|
1329
|
+
def print_report(self, report: BenchmarkReport, deep: bool = False, limit: int = 30):
|
|
1330
|
+
"""Print full benchmark report."""
|
|
1331
|
+
print("\n" + "=" * 70)
|
|
1332
|
+
print("BENCHMARK RESULTS")
|
|
1333
|
+
print("=" * 70)
|
|
1334
|
+
|
|
1335
|
+
# Timeline diagrams for each path
|
|
1336
|
+
print("\n## Timeline Diagrams")
|
|
1337
|
+
print("-" * 70)
|
|
1338
|
+
|
|
1339
|
+
for name, result in sorted(report.results.items(), key=lambda x: x[1].mean_total_ms):
|
|
1340
|
+
print(f"\n### {name}")
|
|
1341
|
+
print(self.create_timeline_diagram(result))
|
|
1342
|
+
|
|
1343
|
+
# Comparison table
|
|
1344
|
+
print("\n" + "=" * 70)
|
|
1345
|
+
print("## Master Comparison Table")
|
|
1346
|
+
print(self.create_comparison_table(report))
|
|
1347
|
+
|
|
1348
|
+
# Variance analysis
|
|
1349
|
+
print(self.create_variance_table(report))
|
|
1350
|
+
|
|
1351
|
+
# Overhead classification
|
|
1352
|
+
print(self.classify_overhead(report))
|
|
1353
|
+
|
|
1354
|
+
# Deep profile output (if enabled and data available)
|
|
1355
|
+
if deep:
|
|
1356
|
+
for name, result in sorted(report.results.items(), key=lambda x: x[1].mean_total_ms):
|
|
1357
|
+
if result.aggregated_functions:
|
|
1358
|
+
print(f"\n{'=' * 70}")
|
|
1359
|
+
print(f"DEEP PROFILE: {name}")
|
|
1360
|
+
print("=" * 70)
|
|
1361
|
+
print(self.create_deep_profile_output(result, limit=limit))
|
|
1362
|
+
|
|
1363
|
+
print("\n" + "=" * 70)
|
|
1364
|
+
|
|
1365
|
+
def handle(self, subcommand: str = "profile", prompt: str = None,
|
|
1366
|
+
iterations: int = None, output_format: str = "text",
|
|
1367
|
+
output_file: str = None, **kwargs) -> Any:
|
|
1368
|
+
"""
|
|
1369
|
+
Handle benchmark subcommands.
|
|
1370
|
+
|
|
1371
|
+
Args:
|
|
1372
|
+
subcommand: One of profile, sdk, agent, cli, compare
|
|
1373
|
+
prompt: Query to benchmark
|
|
1374
|
+
iterations: Number of iterations
|
|
1375
|
+
output_format: text or json
|
|
1376
|
+
output_file: Path to save results
|
|
1377
|
+
|
|
1378
|
+
Returns:
|
|
1379
|
+
BenchmarkReport
|
|
1380
|
+
"""
|
|
1381
|
+
prompt = prompt or self.DEFAULT_PROMPT
|
|
1382
|
+
iterations = iterations or self.DEFAULT_ITERATIONS
|
|
1383
|
+
|
|
1384
|
+
if subcommand == "profile":
|
|
1385
|
+
# Full benchmark
|
|
1386
|
+
report = self.run_full_benchmark(prompt, iterations)
|
|
1387
|
+
|
|
1388
|
+
if output_format == "json":
|
|
1389
|
+
output = json.dumps(report.to_dict(), indent=2)
|
|
1390
|
+
if output_file:
|
|
1391
|
+
with open(output_file, "w") as f:
|
|
1392
|
+
f.write(output)
|
|
1393
|
+
print(f"Results saved to {output_file}")
|
|
1394
|
+
else:
|
|
1395
|
+
print(output)
|
|
1396
|
+
else:
|
|
1397
|
+
self.print_report(report)
|
|
1398
|
+
if output_file:
|
|
1399
|
+
with open(output_file, "w") as f:
|
|
1400
|
+
f.write(json.dumps(report.to_dict(), indent=2))
|
|
1401
|
+
print(f"\nJSON results saved to {output_file}")
|
|
1402
|
+
|
|
1403
|
+
return report
|
|
1404
|
+
|
|
1405
|
+
elif subcommand == "sdk":
|
|
1406
|
+
# SDK only
|
|
1407
|
+
report = self.run_full_benchmark(prompt, iterations, paths=["openai_sdk"])
|
|
1408
|
+
self.print_report(report)
|
|
1409
|
+
return report
|
|
1410
|
+
|
|
1411
|
+
elif subcommand == "agent":
|
|
1412
|
+
# Agent only
|
|
1413
|
+
report = self.run_full_benchmark(prompt, iterations, paths=["openai_sdk", "praisonai_agent"])
|
|
1414
|
+
self.print_report(report)
|
|
1415
|
+
return report
|
|
1416
|
+
|
|
1417
|
+
elif subcommand == "cli":
|
|
1418
|
+
# CLI only
|
|
1419
|
+
report = self.run_full_benchmark(prompt, iterations, paths=["openai_sdk", "praisonai_cli", "praisonai_cli_profile"])
|
|
1420
|
+
self.print_report(report)
|
|
1421
|
+
return report
|
|
1422
|
+
|
|
1423
|
+
elif subcommand == "compare":
|
|
1424
|
+
# Quick comparison (fewer iterations)
|
|
1425
|
+
report = self.run_full_benchmark(prompt, iterations=2,
|
|
1426
|
+
paths=["openai_sdk", "praisonai_agent", "praisonai_cli", "litellm_standalone"])
|
|
1427
|
+
print("\n" + self.create_comparison_table(report))
|
|
1428
|
+
return report
|
|
1429
|
+
|
|
1430
|
+
else:
|
|
1431
|
+
print(f"Unknown subcommand: {subcommand}")
|
|
1432
|
+
print("Available: profile, sdk, agent, cli, compare")
|
|
1433
|
+
return None
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
def run_benchmark_command(args) -> int:
|
|
1437
|
+
"""
|
|
1438
|
+
Run benchmark command from CLI args.
|
|
1439
|
+
|
|
1440
|
+
Args:
|
|
1441
|
+
args: Parsed CLI arguments
|
|
1442
|
+
|
|
1443
|
+
Returns:
|
|
1444
|
+
Exit code (0 for success)
|
|
1445
|
+
"""
|
|
1446
|
+
handler = BenchmarkHandler()
|
|
1447
|
+
|
|
1448
|
+
subcommand = getattr(args, 'benchmark_command', 'profile')
|
|
1449
|
+
prompt = getattr(args, 'benchmark_prompt', None)
|
|
1450
|
+
iterations = getattr(args, 'benchmark_iterations', None)
|
|
1451
|
+
output_format = getattr(args, 'benchmark_format', 'text')
|
|
1452
|
+
output_file = getattr(args, 'benchmark_output', None)
|
|
1453
|
+
|
|
1454
|
+
result = handler.handle(
|
|
1455
|
+
subcommand=subcommand,
|
|
1456
|
+
prompt=prompt,
|
|
1457
|
+
iterations=iterations,
|
|
1458
|
+
output_format=output_format,
|
|
1459
|
+
output_file=output_file
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
return 0 if result else 1
|