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.
Files changed (309) hide show
  1. praisonai_code/__init__.py +17 -0
  2. praisonai_code/cli/__init__.py +12 -0
  3. praisonai_code/cli/_forward_shim.py +10 -0
  4. praisonai_code/cli/_paths.py +88 -0
  5. praisonai_code/cli/_warnings.py +58 -0
  6. praisonai_code/cli/app.py +757 -0
  7. praisonai_code/cli/approval_backend.py +272 -0
  8. praisonai_code/cli/branding.py +94 -0
  9. praisonai_code/cli/commands/__init__.py +114 -0
  10. praisonai_code/cli/commands/acp.py +80 -0
  11. praisonai_code/cli/commands/agent.py +116 -0
  12. praisonai_code/cli/commands/agents.py +80 -0
  13. praisonai_code/cli/commands/app.py +139 -0
  14. praisonai_code/cli/commands/attach.py +95 -0
  15. praisonai_code/cli/commands/audit.py +102 -0
  16. praisonai_code/cli/commands/auth.py +508 -0
  17. praisonai_code/cli/commands/batch.py +848 -0
  18. praisonai_code/cli/commands/benchmark.py +286 -0
  19. praisonai_code/cli/commands/browser.py +299 -0
  20. praisonai_code/cli/commands/call.py +45 -0
  21. praisonai_code/cli/commands/chat.py +332 -0
  22. praisonai_code/cli/commands/checkpoint.py +170 -0
  23. praisonai_code/cli/commands/code.py +276 -0
  24. praisonai_code/cli/commands/command.py +114 -0
  25. praisonai_code/cli/commands/commit.py +47 -0
  26. praisonai_code/cli/commands/completion.py +333 -0
  27. praisonai_code/cli/commands/config.py +681 -0
  28. praisonai_code/cli/commands/context.py +414 -0
  29. praisonai_code/cli/commands/daemon.py +203 -0
  30. praisonai_code/cli/commands/debug.py +142 -0
  31. praisonai_code/cli/commands/deploy.py +71 -0
  32. praisonai_code/cli/commands/diag.py +55 -0
  33. praisonai_code/cli/commands/docs.py +1575 -0
  34. praisonai_code/cli/commands/doctor.py +332 -0
  35. praisonai_code/cli/commands/endpoints.py +51 -0
  36. praisonai_code/cli/commands/environment.py +179 -0
  37. praisonai_code/cli/commands/eval.py +131 -0
  38. praisonai_code/cli/commands/examples.py +953 -0
  39. praisonai_code/cli/commands/flow.py +436 -0
  40. praisonai_code/cli/commands/github.py +752 -0
  41. praisonai_code/cli/commands/hooks.py +74 -0
  42. praisonai_code/cli/commands/init.py +174 -0
  43. praisonai_code/cli/commands/knowledge.py +440 -0
  44. praisonai_code/cli/commands/langextract.py +120 -0
  45. praisonai_code/cli/commands/langfuse.py +984 -0
  46. praisonai_code/cli/commands/loop.py +211 -0
  47. praisonai_code/cli/commands/lsp.py +112 -0
  48. praisonai_code/cli/commands/managed.py +659 -0
  49. praisonai_code/cli/commands/mcp.py +763 -0
  50. praisonai_code/cli/commands/memory.py +298 -0
  51. praisonai_code/cli/commands/models.py +264 -0
  52. praisonai_code/cli/commands/n8n.py +326 -0
  53. praisonai_code/cli/commands/obs.py +19 -0
  54. praisonai_code/cli/commands/package.py +76 -0
  55. praisonai_code/cli/commands/paths.py +106 -0
  56. praisonai_code/cli/commands/permissions.py +272 -0
  57. praisonai_code/cli/commands/plugins.py +609 -0
  58. praisonai_code/cli/commands/port.py +530 -0
  59. praisonai_code/cli/commands/profile.py +466 -0
  60. praisonai_code/cli/commands/publish.py +193 -0
  61. praisonai_code/cli/commands/rag.py +913 -0
  62. praisonai_code/cli/commands/realtime.py +52 -0
  63. praisonai_code/cli/commands/recipe.py +684 -0
  64. praisonai_code/cli/commands/registry.py +59 -0
  65. praisonai_code/cli/commands/replay.py +830 -0
  66. praisonai_code/cli/commands/research.py +49 -0
  67. praisonai_code/cli/commands/retrieval.py +377 -0
  68. praisonai_code/cli/commands/rules.py +71 -0
  69. praisonai_code/cli/commands/run.py +1573 -0
  70. praisonai_code/cli/commands/sandbox.py +371 -0
  71. praisonai_code/cli/commands/schedule.py +529 -0
  72. praisonai_code/cli/commands/serve.py +690 -0
  73. praisonai_code/cli/commands/session.py +450 -0
  74. praisonai_code/cli/commands/setup.py +174 -0
  75. praisonai_code/cli/commands/skills.py +545 -0
  76. praisonai_code/cli/commands/standardise.py +711 -0
  77. praisonai_code/cli/commands/templates.py +54 -0
  78. praisonai_code/cli/commands/test.py +558 -0
  79. praisonai_code/cli/commands/todo.py +74 -0
  80. praisonai_code/cli/commands/tools.py +205 -0
  81. praisonai_code/cli/commands/traces.py +145 -0
  82. praisonai_code/cli/commands/tracker.py +852 -0
  83. praisonai_code/cli/commands/train.py +613 -0
  84. praisonai_code/cli/commands/ui.py +172 -0
  85. praisonai_code/cli/commands/up.py +354 -0
  86. praisonai_code/cli/commands/validate.py +291 -0
  87. praisonai_code/cli/commands/version.py +101 -0
  88. praisonai_code/cli/commands/workflow.py +97 -0
  89. praisonai_code/cli/config_loader.py +437 -0
  90. praisonai_code/cli/configuration/__init__.py +27 -0
  91. praisonai_code/cli/configuration/config.schema.json +57 -0
  92. praisonai_code/cli/configuration/credentials.py +446 -0
  93. praisonai_code/cli/configuration/loader.py +364 -0
  94. praisonai_code/cli/configuration/model_resolver.py +161 -0
  95. praisonai_code/cli/configuration/oauth.py +389 -0
  96. praisonai_code/cli/configuration/paths.py +224 -0
  97. praisonai_code/cli/configuration/resolver.py +687 -0
  98. praisonai_code/cli/configuration/schema.py +317 -0
  99. praisonai_code/cli/execution/__init__.py +99 -0
  100. praisonai_code/cli/execution/core.py +208 -0
  101. praisonai_code/cli/execution/profiler.py +898 -0
  102. praisonai_code/cli/execution/request.py +85 -0
  103. praisonai_code/cli/execution/result.py +74 -0
  104. praisonai_code/cli/fallback_schema.py +416 -0
  105. praisonai_code/cli/features/__init__.py +278 -0
  106. praisonai_code/cli/features/_endpoint_registry.py +64 -0
  107. praisonai_code/cli/features/_search_registry.py +43 -0
  108. praisonai_code/cli/features/acp.py +236 -0
  109. praisonai_code/cli/features/action_orchestrator.py +576 -0
  110. praisonai_code/cli/features/agent_scheduler.py +773 -0
  111. praisonai_code/cli/features/agent_tools.py +603 -0
  112. praisonai_code/cli/features/agents.py +397 -0
  113. praisonai_code/cli/features/at_mentions.py +471 -0
  114. praisonai_code/cli/features/audit_cli.py +270 -0
  115. praisonai_code/cli/features/auto_memory.py +182 -0
  116. praisonai_code/cli/features/auto_mode.py +552 -0
  117. praisonai_code/cli/features/autonomy_mode.py +546 -0
  118. praisonai_code/cli/features/background.py +356 -0
  119. praisonai_code/cli/features/base.py +168 -0
  120. praisonai_code/cli/features/benchmark.py +1462 -0
  121. praisonai_code/cli/features/capabilities.py +1326 -0
  122. praisonai_code/cli/features/checkpoints.py +345 -0
  123. praisonai_code/cli/features/cli_profiler.py +335 -0
  124. praisonai_code/cli/features/code_intelligence.py +666 -0
  125. praisonai_code/cli/features/compaction.py +294 -0
  126. praisonai_code/cli/features/compare.py +534 -0
  127. praisonai_code/cli/features/config_hierarchy.py +366 -0
  128. praisonai_code/cli/features/context_manager.py +597 -0
  129. praisonai_code/cli/features/cost_tracker.py +514 -0
  130. praisonai_code/cli/features/csv_test_runner.py +736 -0
  131. praisonai_code/cli/features/custom_definitions.py +790 -0
  132. praisonai_code/cli/features/debug.py +810 -0
  133. praisonai_code/cli/features/deploy.py +605 -0
  134. praisonai_code/cli/features/diag.py +289 -0
  135. praisonai_code/cli/features/display_jsonl.py +173 -0
  136. praisonai_code/cli/features/doctor/__init__.py +63 -0
  137. praisonai_code/cli/features/doctor/checks/__init__.py +29 -0
  138. praisonai_code/cli/features/doctor/checks/acp_checks.py +220 -0
  139. praisonai_code/cli/features/doctor/checks/bot_checks.py +340 -0
  140. praisonai_code/cli/features/doctor/checks/config_checks.py +373 -0
  141. praisonai_code/cli/features/doctor/checks/db_checks.py +366 -0
  142. praisonai_code/cli/features/doctor/checks/env_checks.py +637 -0
  143. praisonai_code/cli/features/doctor/checks/gateway_checks.py +387 -0
  144. praisonai_code/cli/features/doctor/checks/lsp_checks.py +231 -0
  145. praisonai_code/cli/features/doctor/checks/mcp_checks.py +367 -0
  146. praisonai_code/cli/features/doctor/checks/memory_checks.py +268 -0
  147. praisonai_code/cli/features/doctor/checks/network_checks.py +251 -0
  148. praisonai_code/cli/features/doctor/checks/obs_checks.py +328 -0
  149. praisonai_code/cli/features/doctor/checks/packaging_checks.py +422 -0
  150. praisonai_code/cli/features/doctor/checks/performance_checks.py +235 -0
  151. praisonai_code/cli/features/doctor/checks/permissions_checks.py +259 -0
  152. praisonai_code/cli/features/doctor/checks/runtime_checks.py +650 -0
  153. praisonai_code/cli/features/doctor/checks/runtime_migration_checks.py +220 -0
  154. praisonai_code/cli/features/doctor/checks/selftest_checks.py +322 -0
  155. praisonai_code/cli/features/doctor/checks/serve_checks.py +426 -0
  156. praisonai_code/cli/features/doctor/checks/skills_checks.py +327 -0
  157. praisonai_code/cli/features/doctor/checks/tools_checks.py +371 -0
  158. praisonai_code/cli/features/doctor/engine.py +266 -0
  159. praisonai_code/cli/features/doctor/formatters.py +377 -0
  160. praisonai_code/cli/features/doctor/handler.py +564 -0
  161. praisonai_code/cli/features/doctor/models.py +276 -0
  162. praisonai_code/cli/features/doctor/registry.py +239 -0
  163. praisonai_code/cli/features/endpoints.py +1016 -0
  164. praisonai_code/cli/features/eval.py +559 -0
  165. praisonai_code/cli/features/examples.py +707 -0
  166. praisonai_code/cli/features/external_agents.py +231 -0
  167. praisonai_code/cli/features/fast_context.py +410 -0
  168. praisonai_code/cli/features/file_history.py +320 -0
  169. praisonai_code/cli/features/flow_display.py +566 -0
  170. praisonai_code/cli/features/git_attribution.py +159 -0
  171. praisonai_code/cli/features/git_integration.py +651 -0
  172. praisonai_code/cli/features/guardrail.py +171 -0
  173. praisonai_code/cli/features/handoff.py +252 -0
  174. praisonai_code/cli/features/hooks.py +583 -0
  175. praisonai_code/cli/features/hybrid_workflow.py +391 -0
  176. praisonai_code/cli/features/image.py +384 -0
  177. praisonai_code/cli/features/interactive_core_headless.py +450 -0
  178. praisonai_code/cli/features/interactive_runtime.py +600 -0
  179. praisonai_code/cli/features/interactive_test_harness.py +537 -0
  180. praisonai_code/cli/features/interactive_tools.py +428 -0
  181. praisonai_code/cli/features/interactive_tui.py +603 -0
  182. praisonai_code/cli/features/job_workflow.py +906 -0
  183. praisonai_code/cli/features/jobs.py +632 -0
  184. praisonai_code/cli/features/knowledge.py +531 -0
  185. praisonai_code/cli/features/knowledge_cli.py +438 -0
  186. praisonai_code/cli/features/lite.py +244 -0
  187. praisonai_code/cli/features/logs.py +200 -0
  188. praisonai_code/cli/features/lsp_cli.py +225 -0
  189. praisonai_code/cli/features/lsp_diagnostics.py +185 -0
  190. praisonai_code/cli/features/mcp.py +344 -0
  191. praisonai_code/cli/features/message_queue.py +587 -0
  192. praisonai_code/cli/features/metrics.py +210 -0
  193. praisonai_code/cli/features/migrate.py +1329 -0
  194. praisonai_code/cli/features/migration_flow.py +463 -0
  195. praisonai_code/cli/features/migration_spec.py +276 -0
  196. praisonai_code/cli/features/n8n.py +703 -0
  197. praisonai_code/cli/features/observability.py +293 -0
  198. praisonai_code/cli/features/ollama.py +361 -0
  199. praisonai_code/cli/features/output_modes.py +155 -0
  200. praisonai_code/cli/features/output_style.py +273 -0
  201. praisonai_code/cli/features/package.py +631 -0
  202. praisonai_code/cli/features/performance.py +308 -0
  203. praisonai_code/cli/features/persistence.py +636 -0
  204. praisonai_code/cli/features/profiler/__init__.py +81 -0
  205. praisonai_code/cli/features/profiler/core.py +558 -0
  206. praisonai_code/cli/features/profiler/optimizations.py +652 -0
  207. praisonai_code/cli/features/profiler/suite.py +386 -0
  208. praisonai_code/cli/features/queue/__init__.py +73 -0
  209. praisonai_code/cli/features/queue/manager.py +435 -0
  210. praisonai_code/cli/features/queue/models.py +289 -0
  211. praisonai_code/cli/features/queue/persistence.py +564 -0
  212. praisonai_code/cli/features/queue/scheduler.py +529 -0
  213. praisonai_code/cli/features/queue/worker.py +400 -0
  214. praisonai_code/cli/features/recipe.py +2187 -0
  215. praisonai_code/cli/features/recipe_creator.py +996 -0
  216. praisonai_code/cli/features/recipe_optimizer.py +1364 -0
  217. praisonai_code/cli/features/recipe_prompts.py +226 -0
  218. praisonai_code/cli/features/registry.py +229 -0
  219. praisonai_code/cli/features/repo_map.py +860 -0
  220. praisonai_code/cli/features/router.py +466 -0
  221. praisonai_code/cli/features/safe_shell.py +427 -0
  222. praisonai_code/cli/features/sandbox_cli.py +283 -0
  223. praisonai_code/cli/features/sandbox_executor.py +536 -0
  224. praisonai_code/cli/features/sdk_knowledge.py +500 -0
  225. praisonai_code/cli/features/session.py +222 -0
  226. praisonai_code/cli/features/session_checkpoints.py +208 -0
  227. praisonai_code/cli/features/setup/__init__.py +9 -0
  228. praisonai_code/cli/features/setup/handler.py +355 -0
  229. praisonai_code/cli/features/setup/templates.py +62 -0
  230. praisonai_code/cli/features/skills.py +940 -0
  231. praisonai_code/cli/features/slash_commands.py +692 -0
  232. praisonai_code/cli/features/telemetry.py +179 -0
  233. praisonai_code/cli/features/templates.py +1390 -0
  234. praisonai_code/cli/features/thinking.py +343 -0
  235. praisonai_code/cli/features/todo.py +334 -0
  236. praisonai_code/cli/features/tools.py +680 -0
  237. praisonai_code/cli/features/tui/__init__.py +83 -0
  238. praisonai_code/cli/features/tui/app.py +871 -0
  239. praisonai_code/cli/features/tui/cli.py +580 -0
  240. praisonai_code/cli/features/tui/config.py +150 -0
  241. praisonai_code/cli/features/tui/debug.py +526 -0
  242. praisonai_code/cli/features/tui/events.py +99 -0
  243. praisonai_code/cli/features/tui/mock_provider.py +328 -0
  244. praisonai_code/cli/features/tui/orchestrator.py +652 -0
  245. praisonai_code/cli/features/tui/screens/__init__.py +50 -0
  246. praisonai_code/cli/features/tui/screens/help.py +157 -0
  247. praisonai_code/cli/features/tui/screens/main.py +568 -0
  248. praisonai_code/cli/features/tui/screens/queue.py +174 -0
  249. praisonai_code/cli/features/tui/screens/session.py +124 -0
  250. praisonai_code/cli/features/tui/screens/settings.py +148 -0
  251. praisonai_code/cli/features/tui/session_store.py +198 -0
  252. praisonai_code/cli/features/tui/widgets/__init__.py +56 -0
  253. praisonai_code/cli/features/tui/widgets/chat.py +263 -0
  254. praisonai_code/cli/features/tui/widgets/command_popup.py +258 -0
  255. praisonai_code/cli/features/tui/widgets/composer.py +292 -0
  256. praisonai_code/cli/features/tui/widgets/file_popup.py +207 -0
  257. praisonai_code/cli/features/tui/widgets/queue_panel.py +223 -0
  258. praisonai_code/cli/features/tui/widgets/status.py +181 -0
  259. praisonai_code/cli/features/tui/widgets/tool_panel.py +307 -0
  260. praisonai_code/cli/features/wizard.py +289 -0
  261. praisonai_code/cli/features/workflow.py +802 -0
  262. praisonai_code/cli/features/yaml_utils.py +321 -0
  263. praisonai_code/cli/interactive/__init__.py +48 -0
  264. praisonai_code/cli/interactive/async_tui.py +1218 -0
  265. praisonai_code/cli/interactive/config.py +139 -0
  266. praisonai_code/cli/interactive/core.py +618 -0
  267. praisonai_code/cli/interactive/events.py +131 -0
  268. praisonai_code/cli/interactive/frontends/__init__.py +31 -0
  269. praisonai_code/cli/interactive/frontends/rich_frontend.py +462 -0
  270. praisonai_code/cli/interactive/frontends/textual_frontend.py +157 -0
  271. praisonai_code/cli/interactive/praison_io.py +502 -0
  272. praisonai_code/cli/interactive/repl.py +297 -0
  273. praisonai_code/cli/interactive/split_tui.py +456 -0
  274. praisonai_code/cli/interactive/tui_app.py +457 -0
  275. praisonai_code/cli/langfuse_client.py +360 -0
  276. praisonai_code/cli/main.py +7421 -0
  277. praisonai_code/cli/output/__init__.py +25 -0
  278. praisonai_code/cli/output/console.py +456 -0
  279. praisonai_code/cli/output/event_bridge.py +191 -0
  280. praisonai_code/cli/schedule_cli.py +54 -0
  281. praisonai_code/cli/schema_provider.py +23 -0
  282. praisonai_code/cli/session/__init__.py +16 -0
  283. praisonai_code/cli/session/resume.py +148 -0
  284. praisonai_code/cli/session/unified.py +548 -0
  285. praisonai_code/cli/state/__init__.py +31 -0
  286. praisonai_code/cli/state/identifiers.py +161 -0
  287. praisonai_code/cli/state/project_sessions.py +383 -0
  288. praisonai_code/cli/state/sessions.py +390 -0
  289. praisonai_code/cli/ui/__init__.py +160 -0
  290. praisonai_code/cli/ui/config.py +46 -0
  291. praisonai_code/cli/ui/events.py +61 -0
  292. praisonai_code/cli/ui/mg_backend.py +342 -0
  293. praisonai_code/cli/ui/plain.py +133 -0
  294. praisonai_code/cli/ui/rich_backend.py +162 -0
  295. praisonai_code/cli/unified_schema.py +655 -0
  296. praisonai_code/cli/utils/env_utils.py +126 -0
  297. praisonai_code/cli/utils/project.py +131 -0
  298. praisonai_code/cli_backends/__init__.py +73 -0
  299. praisonai_code/cli_backends/claude.py +373 -0
  300. praisonai_code/cli_backends/registry.py +113 -0
  301. praisonai_code/runtime/__init__.py +36 -0
  302. praisonai_code/runtime/__main__.py +81 -0
  303. praisonai_code/runtime/client.py +131 -0
  304. praisonai_code/runtime/descriptor.py +209 -0
  305. praisonai_code/runtime/server.py +356 -0
  306. praisonai_code-0.0.1.dist-info/METADATA +80 -0
  307. praisonai_code-0.0.1.dist-info/RECORD +309 -0
  308. praisonai_code-0.0.1.dist-info/WHEEL +5 -0
  309. 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