PraisonAI 3.0.0__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 (393) hide show
  1. praisonai/__init__.py +54 -0
  2. praisonai/__main__.py +15 -0
  3. praisonai/acp/__init__.py +54 -0
  4. praisonai/acp/config.py +159 -0
  5. praisonai/acp/server.py +587 -0
  6. praisonai/acp/session.py +219 -0
  7. praisonai/adapters/__init__.py +50 -0
  8. praisonai/adapters/readers.py +395 -0
  9. praisonai/adapters/rerankers.py +315 -0
  10. praisonai/adapters/retrievers.py +394 -0
  11. praisonai/adapters/vector_stores.py +409 -0
  12. praisonai/agent_scheduler.py +337 -0
  13. praisonai/agents_generator.py +903 -0
  14. praisonai/api/call.py +292 -0
  15. praisonai/auto.py +1197 -0
  16. praisonai/capabilities/__init__.py +275 -0
  17. praisonai/capabilities/a2a.py +140 -0
  18. praisonai/capabilities/assistants.py +283 -0
  19. praisonai/capabilities/audio.py +320 -0
  20. praisonai/capabilities/batches.py +469 -0
  21. praisonai/capabilities/completions.py +336 -0
  22. praisonai/capabilities/container_files.py +155 -0
  23. praisonai/capabilities/containers.py +93 -0
  24. praisonai/capabilities/embeddings.py +158 -0
  25. praisonai/capabilities/files.py +467 -0
  26. praisonai/capabilities/fine_tuning.py +293 -0
  27. praisonai/capabilities/guardrails.py +182 -0
  28. praisonai/capabilities/images.py +330 -0
  29. praisonai/capabilities/mcp.py +190 -0
  30. praisonai/capabilities/messages.py +270 -0
  31. praisonai/capabilities/moderations.py +154 -0
  32. praisonai/capabilities/ocr.py +217 -0
  33. praisonai/capabilities/passthrough.py +204 -0
  34. praisonai/capabilities/rag.py +207 -0
  35. praisonai/capabilities/realtime.py +160 -0
  36. praisonai/capabilities/rerank.py +165 -0
  37. praisonai/capabilities/responses.py +266 -0
  38. praisonai/capabilities/search.py +109 -0
  39. praisonai/capabilities/skills.py +133 -0
  40. praisonai/capabilities/vector_store_files.py +334 -0
  41. praisonai/capabilities/vector_stores.py +304 -0
  42. praisonai/capabilities/videos.py +141 -0
  43. praisonai/chainlit_ui.py +304 -0
  44. praisonai/chat/__init__.py +106 -0
  45. praisonai/chat/app.py +125 -0
  46. praisonai/cli/__init__.py +26 -0
  47. praisonai/cli/app.py +213 -0
  48. praisonai/cli/commands/__init__.py +75 -0
  49. praisonai/cli/commands/acp.py +70 -0
  50. praisonai/cli/commands/completion.py +333 -0
  51. praisonai/cli/commands/config.py +166 -0
  52. praisonai/cli/commands/debug.py +142 -0
  53. praisonai/cli/commands/diag.py +55 -0
  54. praisonai/cli/commands/doctor.py +166 -0
  55. praisonai/cli/commands/environment.py +179 -0
  56. praisonai/cli/commands/lsp.py +112 -0
  57. praisonai/cli/commands/mcp.py +210 -0
  58. praisonai/cli/commands/profile.py +457 -0
  59. praisonai/cli/commands/run.py +228 -0
  60. praisonai/cli/commands/schedule.py +150 -0
  61. praisonai/cli/commands/serve.py +97 -0
  62. praisonai/cli/commands/session.py +212 -0
  63. praisonai/cli/commands/traces.py +145 -0
  64. praisonai/cli/commands/version.py +101 -0
  65. praisonai/cli/configuration/__init__.py +18 -0
  66. praisonai/cli/configuration/loader.py +353 -0
  67. praisonai/cli/configuration/paths.py +114 -0
  68. praisonai/cli/configuration/schema.py +164 -0
  69. praisonai/cli/features/__init__.py +268 -0
  70. praisonai/cli/features/acp.py +236 -0
  71. praisonai/cli/features/action_orchestrator.py +546 -0
  72. praisonai/cli/features/agent_scheduler.py +773 -0
  73. praisonai/cli/features/agent_tools.py +474 -0
  74. praisonai/cli/features/agents.py +375 -0
  75. praisonai/cli/features/at_mentions.py +471 -0
  76. praisonai/cli/features/auto_memory.py +182 -0
  77. praisonai/cli/features/autonomy_mode.py +490 -0
  78. praisonai/cli/features/background.py +356 -0
  79. praisonai/cli/features/base.py +168 -0
  80. praisonai/cli/features/capabilities.py +1326 -0
  81. praisonai/cli/features/checkpoints.py +338 -0
  82. praisonai/cli/features/code_intelligence.py +652 -0
  83. praisonai/cli/features/compaction.py +294 -0
  84. praisonai/cli/features/compare.py +534 -0
  85. praisonai/cli/features/cost_tracker.py +514 -0
  86. praisonai/cli/features/debug.py +810 -0
  87. praisonai/cli/features/deploy.py +517 -0
  88. praisonai/cli/features/diag.py +289 -0
  89. praisonai/cli/features/doctor/__init__.py +63 -0
  90. praisonai/cli/features/doctor/checks/__init__.py +24 -0
  91. praisonai/cli/features/doctor/checks/acp_checks.py +240 -0
  92. praisonai/cli/features/doctor/checks/config_checks.py +366 -0
  93. praisonai/cli/features/doctor/checks/db_checks.py +366 -0
  94. praisonai/cli/features/doctor/checks/env_checks.py +543 -0
  95. praisonai/cli/features/doctor/checks/lsp_checks.py +199 -0
  96. praisonai/cli/features/doctor/checks/mcp_checks.py +349 -0
  97. praisonai/cli/features/doctor/checks/memory_checks.py +268 -0
  98. praisonai/cli/features/doctor/checks/network_checks.py +251 -0
  99. praisonai/cli/features/doctor/checks/obs_checks.py +328 -0
  100. praisonai/cli/features/doctor/checks/performance_checks.py +235 -0
  101. praisonai/cli/features/doctor/checks/permissions_checks.py +259 -0
  102. praisonai/cli/features/doctor/checks/selftest_checks.py +322 -0
  103. praisonai/cli/features/doctor/checks/serve_checks.py +426 -0
  104. praisonai/cli/features/doctor/checks/skills_checks.py +231 -0
  105. praisonai/cli/features/doctor/checks/tools_checks.py +371 -0
  106. praisonai/cli/features/doctor/engine.py +266 -0
  107. praisonai/cli/features/doctor/formatters.py +310 -0
  108. praisonai/cli/features/doctor/handler.py +397 -0
  109. praisonai/cli/features/doctor/models.py +264 -0
  110. praisonai/cli/features/doctor/registry.py +239 -0
  111. praisonai/cli/features/endpoints.py +1019 -0
  112. praisonai/cli/features/eval.py +560 -0
  113. praisonai/cli/features/external_agents.py +231 -0
  114. praisonai/cli/features/fast_context.py +410 -0
  115. praisonai/cli/features/flow_display.py +566 -0
  116. praisonai/cli/features/git_integration.py +651 -0
  117. praisonai/cli/features/guardrail.py +171 -0
  118. praisonai/cli/features/handoff.py +185 -0
  119. praisonai/cli/features/hooks.py +583 -0
  120. praisonai/cli/features/image.py +384 -0
  121. praisonai/cli/features/interactive_runtime.py +585 -0
  122. praisonai/cli/features/interactive_tools.py +380 -0
  123. praisonai/cli/features/interactive_tui.py +603 -0
  124. praisonai/cli/features/jobs.py +632 -0
  125. praisonai/cli/features/knowledge.py +531 -0
  126. praisonai/cli/features/lite.py +244 -0
  127. praisonai/cli/features/lsp_cli.py +225 -0
  128. praisonai/cli/features/mcp.py +169 -0
  129. praisonai/cli/features/message_queue.py +587 -0
  130. praisonai/cli/features/metrics.py +211 -0
  131. praisonai/cli/features/n8n.py +673 -0
  132. praisonai/cli/features/observability.py +293 -0
  133. praisonai/cli/features/ollama.py +361 -0
  134. praisonai/cli/features/output_style.py +273 -0
  135. praisonai/cli/features/package.py +631 -0
  136. praisonai/cli/features/performance.py +308 -0
  137. praisonai/cli/features/persistence.py +636 -0
  138. praisonai/cli/features/profile.py +226 -0
  139. praisonai/cli/features/profiler/__init__.py +81 -0
  140. praisonai/cli/features/profiler/core.py +558 -0
  141. praisonai/cli/features/profiler/optimizations.py +652 -0
  142. praisonai/cli/features/profiler/suite.py +386 -0
  143. praisonai/cli/features/profiling.py +350 -0
  144. praisonai/cli/features/queue/__init__.py +73 -0
  145. praisonai/cli/features/queue/manager.py +395 -0
  146. praisonai/cli/features/queue/models.py +286 -0
  147. praisonai/cli/features/queue/persistence.py +564 -0
  148. praisonai/cli/features/queue/scheduler.py +484 -0
  149. praisonai/cli/features/queue/worker.py +372 -0
  150. praisonai/cli/features/recipe.py +1723 -0
  151. praisonai/cli/features/recipes.py +449 -0
  152. praisonai/cli/features/registry.py +229 -0
  153. praisonai/cli/features/repo_map.py +860 -0
  154. praisonai/cli/features/router.py +466 -0
  155. praisonai/cli/features/sandbox_executor.py +515 -0
  156. praisonai/cli/features/serve.py +829 -0
  157. praisonai/cli/features/session.py +222 -0
  158. praisonai/cli/features/skills.py +856 -0
  159. praisonai/cli/features/slash_commands.py +650 -0
  160. praisonai/cli/features/telemetry.py +179 -0
  161. praisonai/cli/features/templates.py +1384 -0
  162. praisonai/cli/features/thinking.py +305 -0
  163. praisonai/cli/features/todo.py +334 -0
  164. praisonai/cli/features/tools.py +680 -0
  165. praisonai/cli/features/tui/__init__.py +83 -0
  166. praisonai/cli/features/tui/app.py +580 -0
  167. praisonai/cli/features/tui/cli.py +566 -0
  168. praisonai/cli/features/tui/debug.py +511 -0
  169. praisonai/cli/features/tui/events.py +99 -0
  170. praisonai/cli/features/tui/mock_provider.py +328 -0
  171. praisonai/cli/features/tui/orchestrator.py +652 -0
  172. praisonai/cli/features/tui/screens/__init__.py +50 -0
  173. praisonai/cli/features/tui/screens/main.py +245 -0
  174. praisonai/cli/features/tui/screens/queue.py +174 -0
  175. praisonai/cli/features/tui/screens/session.py +124 -0
  176. praisonai/cli/features/tui/screens/settings.py +148 -0
  177. praisonai/cli/features/tui/widgets/__init__.py +56 -0
  178. praisonai/cli/features/tui/widgets/chat.py +261 -0
  179. praisonai/cli/features/tui/widgets/composer.py +224 -0
  180. praisonai/cli/features/tui/widgets/queue_panel.py +200 -0
  181. praisonai/cli/features/tui/widgets/status.py +167 -0
  182. praisonai/cli/features/tui/widgets/tool_panel.py +248 -0
  183. praisonai/cli/features/workflow.py +720 -0
  184. praisonai/cli/legacy.py +236 -0
  185. praisonai/cli/main.py +5559 -0
  186. praisonai/cli/schedule_cli.py +54 -0
  187. praisonai/cli/state/__init__.py +31 -0
  188. praisonai/cli/state/identifiers.py +161 -0
  189. praisonai/cli/state/sessions.py +313 -0
  190. praisonai/code/__init__.py +93 -0
  191. praisonai/code/agent_tools.py +344 -0
  192. praisonai/code/diff/__init__.py +21 -0
  193. praisonai/code/diff/diff_strategy.py +432 -0
  194. praisonai/code/tools/__init__.py +27 -0
  195. praisonai/code/tools/apply_diff.py +221 -0
  196. praisonai/code/tools/execute_command.py +275 -0
  197. praisonai/code/tools/list_files.py +274 -0
  198. praisonai/code/tools/read_file.py +206 -0
  199. praisonai/code/tools/search_replace.py +248 -0
  200. praisonai/code/tools/write_file.py +217 -0
  201. praisonai/code/utils/__init__.py +46 -0
  202. praisonai/code/utils/file_utils.py +307 -0
  203. praisonai/code/utils/ignore_utils.py +308 -0
  204. praisonai/code/utils/text_utils.py +276 -0
  205. praisonai/db/__init__.py +64 -0
  206. praisonai/db/adapter.py +531 -0
  207. praisonai/deploy/__init__.py +62 -0
  208. praisonai/deploy/api.py +231 -0
  209. praisonai/deploy/docker.py +454 -0
  210. praisonai/deploy/doctor.py +367 -0
  211. praisonai/deploy/main.py +327 -0
  212. praisonai/deploy/models.py +179 -0
  213. praisonai/deploy/providers/__init__.py +33 -0
  214. praisonai/deploy/providers/aws.py +331 -0
  215. praisonai/deploy/providers/azure.py +358 -0
  216. praisonai/deploy/providers/base.py +101 -0
  217. praisonai/deploy/providers/gcp.py +314 -0
  218. praisonai/deploy/schema.py +208 -0
  219. praisonai/deploy.py +185 -0
  220. praisonai/endpoints/__init__.py +53 -0
  221. praisonai/endpoints/a2u_server.py +410 -0
  222. praisonai/endpoints/discovery.py +165 -0
  223. praisonai/endpoints/providers/__init__.py +28 -0
  224. praisonai/endpoints/providers/a2a.py +253 -0
  225. praisonai/endpoints/providers/a2u.py +208 -0
  226. praisonai/endpoints/providers/agents_api.py +171 -0
  227. praisonai/endpoints/providers/base.py +231 -0
  228. praisonai/endpoints/providers/mcp.py +263 -0
  229. praisonai/endpoints/providers/recipe.py +206 -0
  230. praisonai/endpoints/providers/tools_mcp.py +150 -0
  231. praisonai/endpoints/registry.py +131 -0
  232. praisonai/endpoints/server.py +161 -0
  233. praisonai/inbuilt_tools/__init__.py +24 -0
  234. praisonai/inbuilt_tools/autogen_tools.py +117 -0
  235. praisonai/inc/__init__.py +2 -0
  236. praisonai/inc/config.py +96 -0
  237. praisonai/inc/models.py +155 -0
  238. praisonai/integrations/__init__.py +56 -0
  239. praisonai/integrations/base.py +303 -0
  240. praisonai/integrations/claude_code.py +270 -0
  241. praisonai/integrations/codex_cli.py +255 -0
  242. praisonai/integrations/cursor_cli.py +195 -0
  243. praisonai/integrations/gemini_cli.py +222 -0
  244. praisonai/jobs/__init__.py +67 -0
  245. praisonai/jobs/executor.py +425 -0
  246. praisonai/jobs/models.py +230 -0
  247. praisonai/jobs/router.py +314 -0
  248. praisonai/jobs/server.py +186 -0
  249. praisonai/jobs/store.py +203 -0
  250. praisonai/llm/__init__.py +66 -0
  251. praisonai/llm/registry.py +382 -0
  252. praisonai/mcp_server/__init__.py +152 -0
  253. praisonai/mcp_server/adapters/__init__.py +74 -0
  254. praisonai/mcp_server/adapters/agents.py +128 -0
  255. praisonai/mcp_server/adapters/capabilities.py +168 -0
  256. praisonai/mcp_server/adapters/cli_tools.py +568 -0
  257. praisonai/mcp_server/adapters/extended_capabilities.py +462 -0
  258. praisonai/mcp_server/adapters/knowledge.py +93 -0
  259. praisonai/mcp_server/adapters/memory.py +104 -0
  260. praisonai/mcp_server/adapters/prompts.py +306 -0
  261. praisonai/mcp_server/adapters/resources.py +124 -0
  262. praisonai/mcp_server/adapters/tools_bridge.py +280 -0
  263. praisonai/mcp_server/auth/__init__.py +48 -0
  264. praisonai/mcp_server/auth/api_key.py +291 -0
  265. praisonai/mcp_server/auth/oauth.py +460 -0
  266. praisonai/mcp_server/auth/oidc.py +289 -0
  267. praisonai/mcp_server/auth/scopes.py +260 -0
  268. praisonai/mcp_server/cli.py +852 -0
  269. praisonai/mcp_server/elicitation.py +445 -0
  270. praisonai/mcp_server/icons.py +302 -0
  271. praisonai/mcp_server/recipe_adapter.py +573 -0
  272. praisonai/mcp_server/recipe_cli.py +824 -0
  273. praisonai/mcp_server/registry.py +703 -0
  274. praisonai/mcp_server/sampling.py +422 -0
  275. praisonai/mcp_server/server.py +490 -0
  276. praisonai/mcp_server/tasks.py +443 -0
  277. praisonai/mcp_server/transports/__init__.py +18 -0
  278. praisonai/mcp_server/transports/http_stream.py +376 -0
  279. praisonai/mcp_server/transports/stdio.py +132 -0
  280. praisonai/persistence/__init__.py +84 -0
  281. praisonai/persistence/config.py +238 -0
  282. praisonai/persistence/conversation/__init__.py +25 -0
  283. praisonai/persistence/conversation/async_mysql.py +427 -0
  284. praisonai/persistence/conversation/async_postgres.py +410 -0
  285. praisonai/persistence/conversation/async_sqlite.py +371 -0
  286. praisonai/persistence/conversation/base.py +151 -0
  287. praisonai/persistence/conversation/json_store.py +250 -0
  288. praisonai/persistence/conversation/mysql.py +387 -0
  289. praisonai/persistence/conversation/postgres.py +401 -0
  290. praisonai/persistence/conversation/singlestore.py +240 -0
  291. praisonai/persistence/conversation/sqlite.py +341 -0
  292. praisonai/persistence/conversation/supabase.py +203 -0
  293. praisonai/persistence/conversation/surrealdb.py +287 -0
  294. praisonai/persistence/factory.py +301 -0
  295. praisonai/persistence/hooks/__init__.py +18 -0
  296. praisonai/persistence/hooks/agent_hooks.py +297 -0
  297. praisonai/persistence/knowledge/__init__.py +26 -0
  298. praisonai/persistence/knowledge/base.py +144 -0
  299. praisonai/persistence/knowledge/cassandra.py +232 -0
  300. praisonai/persistence/knowledge/chroma.py +295 -0
  301. praisonai/persistence/knowledge/clickhouse.py +242 -0
  302. praisonai/persistence/knowledge/cosmosdb_vector.py +438 -0
  303. praisonai/persistence/knowledge/couchbase.py +286 -0
  304. praisonai/persistence/knowledge/lancedb.py +216 -0
  305. praisonai/persistence/knowledge/langchain_adapter.py +291 -0
  306. praisonai/persistence/knowledge/lightrag_adapter.py +212 -0
  307. praisonai/persistence/knowledge/llamaindex_adapter.py +256 -0
  308. praisonai/persistence/knowledge/milvus.py +277 -0
  309. praisonai/persistence/knowledge/mongodb_vector.py +306 -0
  310. praisonai/persistence/knowledge/pgvector.py +335 -0
  311. praisonai/persistence/knowledge/pinecone.py +253 -0
  312. praisonai/persistence/knowledge/qdrant.py +301 -0
  313. praisonai/persistence/knowledge/redis_vector.py +291 -0
  314. praisonai/persistence/knowledge/singlestore_vector.py +299 -0
  315. praisonai/persistence/knowledge/surrealdb_vector.py +309 -0
  316. praisonai/persistence/knowledge/upstash_vector.py +266 -0
  317. praisonai/persistence/knowledge/weaviate.py +223 -0
  318. praisonai/persistence/migrations/__init__.py +10 -0
  319. praisonai/persistence/migrations/manager.py +251 -0
  320. praisonai/persistence/orchestrator.py +406 -0
  321. praisonai/persistence/state/__init__.py +21 -0
  322. praisonai/persistence/state/async_mongodb.py +200 -0
  323. praisonai/persistence/state/base.py +107 -0
  324. praisonai/persistence/state/dynamodb.py +226 -0
  325. praisonai/persistence/state/firestore.py +175 -0
  326. praisonai/persistence/state/gcs.py +155 -0
  327. praisonai/persistence/state/memory.py +245 -0
  328. praisonai/persistence/state/mongodb.py +158 -0
  329. praisonai/persistence/state/redis.py +190 -0
  330. praisonai/persistence/state/upstash.py +144 -0
  331. praisonai/persistence/tests/__init__.py +3 -0
  332. praisonai/persistence/tests/test_all_backends.py +633 -0
  333. praisonai/profiler.py +1214 -0
  334. praisonai/recipe/__init__.py +134 -0
  335. praisonai/recipe/bridge.py +278 -0
  336. praisonai/recipe/core.py +893 -0
  337. praisonai/recipe/exceptions.py +54 -0
  338. praisonai/recipe/history.py +402 -0
  339. praisonai/recipe/models.py +266 -0
  340. praisonai/recipe/operations.py +440 -0
  341. praisonai/recipe/policy.py +422 -0
  342. praisonai/recipe/registry.py +849 -0
  343. praisonai/recipe/runtime.py +214 -0
  344. praisonai/recipe/security.py +711 -0
  345. praisonai/recipe/serve.py +859 -0
  346. praisonai/recipe/server.py +613 -0
  347. praisonai/scheduler/__init__.py +45 -0
  348. praisonai/scheduler/agent_scheduler.py +552 -0
  349. praisonai/scheduler/base.py +124 -0
  350. praisonai/scheduler/daemon_manager.py +225 -0
  351. praisonai/scheduler/state_manager.py +155 -0
  352. praisonai/scheduler/yaml_loader.py +193 -0
  353. praisonai/scheduler.py +194 -0
  354. praisonai/setup/__init__.py +1 -0
  355. praisonai/setup/build.py +21 -0
  356. praisonai/setup/post_install.py +23 -0
  357. praisonai/setup/setup_conda_env.py +25 -0
  358. praisonai/setup.py +16 -0
  359. praisonai/templates/__init__.py +116 -0
  360. praisonai/templates/cache.py +364 -0
  361. praisonai/templates/dependency_checker.py +358 -0
  362. praisonai/templates/discovery.py +391 -0
  363. praisonai/templates/loader.py +564 -0
  364. praisonai/templates/registry.py +511 -0
  365. praisonai/templates/resolver.py +206 -0
  366. praisonai/templates/security.py +327 -0
  367. praisonai/templates/tool_override.py +498 -0
  368. praisonai/templates/tools_doctor.py +256 -0
  369. praisonai/test.py +105 -0
  370. praisonai/train.py +562 -0
  371. praisonai/train_vision.py +306 -0
  372. praisonai/ui/agents.py +824 -0
  373. praisonai/ui/callbacks.py +57 -0
  374. praisonai/ui/chainlit_compat.py +246 -0
  375. praisonai/ui/chat.py +532 -0
  376. praisonai/ui/code.py +717 -0
  377. praisonai/ui/colab.py +474 -0
  378. praisonai/ui/colab_chainlit.py +81 -0
  379. praisonai/ui/components/aicoder.py +284 -0
  380. praisonai/ui/context.py +283 -0
  381. praisonai/ui/database_config.py +56 -0
  382. praisonai/ui/db.py +294 -0
  383. praisonai/ui/realtime.py +488 -0
  384. praisonai/ui/realtimeclient/__init__.py +756 -0
  385. praisonai/ui/realtimeclient/tools.py +242 -0
  386. praisonai/ui/sql_alchemy.py +710 -0
  387. praisonai/upload_vision.py +140 -0
  388. praisonai/version.py +1 -0
  389. praisonai-3.0.0.dist-info/METADATA +3493 -0
  390. praisonai-3.0.0.dist-info/RECORD +393 -0
  391. praisonai-3.0.0.dist-info/WHEEL +5 -0
  392. praisonai-3.0.0.dist-info/entry_points.txt +4 -0
  393. praisonai-3.0.0.dist-info/top_level.txt +1 -0
praisonai/profiler.py ADDED
@@ -0,0 +1,1214 @@
1
+ """
2
+ PraisonAI Profiler Module
3
+
4
+ Standardized profiling for performance monitoring across praisonai and praisonai-agents.
5
+
6
+ Features:
7
+ - Import timing
8
+ - Function execution timing
9
+ - Flow tracking
10
+ - File/module usage tracking
11
+ - Memory usage (tracemalloc)
12
+ - API call profiling (wall-clock time)
13
+ - Streaming profiling (TTFT, total time)
14
+ - Statistics (p50, p95, p99)
15
+ - cProfile integration
16
+ - Flamegraph generation
17
+ - Line-level profiling
18
+ - JSON/HTML export
19
+
20
+ Usage:
21
+ from praisonai.profiler import Profiler, profile, profile_imports
22
+
23
+ # Profile a function
24
+ @profile
25
+ def my_function():
26
+ pass
27
+
28
+ # Profile a block
29
+ with Profiler.block("my_operation"):
30
+ do_something()
31
+
32
+ # Profile API calls
33
+ with Profiler.api_call("https://api.example.com") as call:
34
+ response = requests.get(...)
35
+
36
+ # Profile streaming
37
+ with Profiler.streaming("chat") as tracker:
38
+ tracker.first_token()
39
+ for chunk in stream:
40
+ tracker.chunk()
41
+
42
+ # Profile imports
43
+ with profile_imports():
44
+ import heavy_module
45
+
46
+ # Get report with statistics
47
+ Profiler.report()
48
+ stats = Profiler.get_statistics()
49
+
50
+ # Export
51
+ Profiler.export_json()
52
+ Profiler.export_html()
53
+ """
54
+
55
+ import time
56
+ import functools
57
+ import threading
58
+ import sys
59
+ import os
60
+ import json
61
+ import tracemalloc
62
+ import cProfile
63
+ import pstats
64
+ import io
65
+ import statistics
66
+ from dataclasses import dataclass, field, asdict
67
+ from typing import Dict, List, Optional, Callable, Any
68
+ from contextlib import contextmanager, asynccontextmanager
69
+
70
+
71
+ # ============================================================================
72
+ # Data Classes
73
+ # ============================================================================
74
+
75
+ @dataclass
76
+ class TimingRecord:
77
+ """Record of a single timing measurement."""
78
+ name: str
79
+ duration_ms: float
80
+ category: str = "function"
81
+ file: str = ""
82
+ line: int = 0
83
+ timestamp: float = field(default_factory=time.time)
84
+
85
+
86
+ @dataclass
87
+ class APICallRecord:
88
+ """Record of an API/HTTP call."""
89
+ endpoint: str
90
+ method: str
91
+ duration_ms: float
92
+ status_code: int = 0
93
+ request_size: int = 0
94
+ response_size: int = 0
95
+ timestamp: float = field(default_factory=time.time)
96
+
97
+
98
+ @dataclass
99
+ class StreamingRecord:
100
+ """Record of streaming operation (LLM responses)."""
101
+ name: str
102
+ ttft_ms: float # Time to first token
103
+ total_ms: float
104
+ chunk_count: int = 0
105
+ total_tokens: int = 0
106
+ timestamp: float = field(default_factory=time.time)
107
+
108
+
109
+ @dataclass
110
+ class MemoryRecord:
111
+ """Record of memory usage."""
112
+ name: str
113
+ current_kb: float
114
+ peak_kb: float
115
+ timestamp: float = field(default_factory=time.time)
116
+
117
+
118
+ @dataclass
119
+ class ImportRecord:
120
+ """Record of a module import."""
121
+ module: str
122
+ duration_ms: float
123
+ parent: str = ""
124
+ timestamp: float = field(default_factory=time.time)
125
+
126
+
127
+ @dataclass
128
+ class FlowRecord:
129
+ """Record of execution flow."""
130
+ step: int
131
+ name: str
132
+ file: str
133
+ line: int
134
+ duration_ms: float
135
+ timestamp: float = field(default_factory=time.time)
136
+
137
+
138
+ # ============================================================================
139
+ # Streaming Tracker
140
+ # ============================================================================
141
+
142
+ class StreamingTracker:
143
+ """
144
+ Track streaming operations (LLM responses).
145
+
146
+ Usage:
147
+ tracker = StreamingTracker("chat")
148
+ tracker.start()
149
+ tracker.first_token() # Mark TTFT
150
+ for chunk in stream:
151
+ tracker.chunk()
152
+ tracker.end(total_tokens=100)
153
+ """
154
+
155
+ def __init__(self, name: str):
156
+ self.name = name
157
+ self._start_time: Optional[float] = None
158
+ self._first_token_time: Optional[float] = None
159
+ self._end_time: Optional[float] = None
160
+ self._chunk_count: int = 0
161
+ self._total_tokens: int = 0
162
+
163
+ def start(self) -> None:
164
+ """Start tracking."""
165
+ self._start_time = time.perf_counter()
166
+
167
+ def first_token(self) -> None:
168
+ """Mark time to first token."""
169
+ if self._first_token_time is None:
170
+ self._first_token_time = time.perf_counter()
171
+
172
+ def chunk(self) -> None:
173
+ """Record a chunk received."""
174
+ self._chunk_count += 1
175
+
176
+ def end(self, total_tokens: int = 0) -> None:
177
+ """End tracking and record to Profiler."""
178
+ self._end_time = time.perf_counter()
179
+ self._total_tokens = total_tokens
180
+
181
+ if self._start_time is not None:
182
+ ttft_ms = 0.0
183
+ if self._first_token_time is not None:
184
+ ttft_ms = (self._first_token_time - self._start_time) * 1000
185
+
186
+ total_ms = (self._end_time - self._start_time) * 1000
187
+
188
+ Profiler.record_streaming(
189
+ name=self.name,
190
+ ttft_ms=ttft_ms,
191
+ total_ms=total_ms,
192
+ chunk_count=self._chunk_count,
193
+ total_tokens=self._total_tokens
194
+ )
195
+
196
+ @property
197
+ def ttft_ms(self) -> float:
198
+ """Get time to first token in ms."""
199
+ if self._start_time and self._first_token_time:
200
+ return (self._first_token_time - self._start_time) * 1000
201
+ return 0.0
202
+
203
+ @property
204
+ def elapsed_ms(self) -> float:
205
+ """Get elapsed time in ms."""
206
+ if self._start_time:
207
+ end = self._end_time or time.perf_counter()
208
+ return (end - self._start_time) * 1000
209
+ return 0.0
210
+
211
+
212
+ # ============================================================================
213
+ # Profiler Class
214
+ # ============================================================================
215
+
216
+ class Profiler:
217
+ """
218
+ Centralized profiler for performance monitoring.
219
+
220
+ Thread-safe singleton pattern for global access.
221
+
222
+ Features:
223
+ - Function/block timing
224
+ - API call profiling (wall-clock)
225
+ - Streaming profiling (TTFT)
226
+ - Memory profiling
227
+ - Import timing
228
+ - Statistics (p50, p95, p99)
229
+ - cProfile integration
230
+ - Export (JSON, HTML)
231
+ """
232
+
233
+ _instance: Optional['Profiler'] = None
234
+ _lock = threading.Lock()
235
+
236
+ # Class-level storage
237
+ _timings: List[TimingRecord] = []
238
+ _imports: List[ImportRecord] = []
239
+ _flow: List[FlowRecord] = []
240
+ _api_calls: List[APICallRecord] = []
241
+ _streaming: List[StreamingRecord] = []
242
+ _memory: List[MemoryRecord] = []
243
+ _enabled: bool = False
244
+ _flow_step: int = 0
245
+ _files_accessed: Dict[str, int] = {}
246
+ _line_profile_data: Dict[str, Any] = {}
247
+ _cprofile_stats: List[Dict[str, Any]] = []
248
+
249
+ def __new__(cls):
250
+ if cls._instance is None:
251
+ with cls._lock:
252
+ if cls._instance is None:
253
+ cls._instance = super().__new__(cls)
254
+ return cls._instance
255
+
256
+ @classmethod
257
+ def enable(cls) -> None:
258
+ """Enable profiling."""
259
+ cls._enabled = True
260
+
261
+ @classmethod
262
+ def disable(cls) -> None:
263
+ """Disable profiling."""
264
+ cls._enabled = False
265
+
266
+ @classmethod
267
+ def is_enabled(cls) -> bool:
268
+ """Check if profiling is enabled."""
269
+ return cls._enabled or os.environ.get('PRAISONAI_PROFILE', '').lower() in ('1', 'true', 'yes')
270
+
271
+ @classmethod
272
+ def clear(cls) -> None:
273
+ """Clear all profiling data."""
274
+ with cls._lock:
275
+ cls._timings.clear()
276
+ cls._imports.clear()
277
+ cls._flow.clear()
278
+ cls._api_calls.clear()
279
+ cls._streaming.clear()
280
+ cls._memory.clear()
281
+ cls._flow_step = 0
282
+ cls._files_accessed.clear()
283
+ cls._line_profile_data.clear()
284
+ cls._cprofile_stats.clear()
285
+
286
+ @classmethod
287
+ def record_timing(cls, name: str, duration_ms: float, category: str = "function",
288
+ file: str = "", line: int = 0) -> None:
289
+ """Record a timing measurement."""
290
+ if not cls.is_enabled():
291
+ return
292
+
293
+ with cls._lock:
294
+ cls._timings.append(TimingRecord(
295
+ name=name,
296
+ duration_ms=duration_ms,
297
+ category=category,
298
+ file=file,
299
+ line=line
300
+ ))
301
+
302
+ # Track file access
303
+ if file:
304
+ cls._files_accessed[file] = cls._files_accessed.get(file, 0) + 1
305
+
306
+ @classmethod
307
+ def record_import(cls, module: str, duration_ms: float, parent: str = "") -> None:
308
+ """Record an import timing."""
309
+ if not cls.is_enabled():
310
+ return
311
+
312
+ with cls._lock:
313
+ cls._imports.append(ImportRecord(
314
+ module=module,
315
+ duration_ms=duration_ms,
316
+ parent=parent
317
+ ))
318
+
319
+ @classmethod
320
+ def record_flow(cls, name: str, duration_ms: float, file: str = "", line: int = 0) -> None:
321
+ """Record a flow step."""
322
+ if not cls.is_enabled():
323
+ return
324
+
325
+ with cls._lock:
326
+ cls._flow_step += 1
327
+ cls._flow.append(FlowRecord(
328
+ step=cls._flow_step,
329
+ name=name,
330
+ file=file,
331
+ line=line,
332
+ duration_ms=duration_ms
333
+ ))
334
+
335
+ @classmethod
336
+ @contextmanager
337
+ def block(cls, name: str, category: str = "block"):
338
+ """Context manager for profiling a block of code."""
339
+ start = time.time()
340
+ frame = sys._getframe(2) if hasattr(sys, '_getframe') else None
341
+ file = frame.f_code.co_filename if frame else ""
342
+ line = frame.f_lineno if frame else 0
343
+
344
+ try:
345
+ yield
346
+ finally:
347
+ duration_ms = (time.time() - start) * 1000
348
+ cls.record_timing(name, duration_ms, category, file, line)
349
+ cls.record_flow(name, duration_ms, file, line)
350
+
351
+ @classmethod
352
+ def get_timings(cls, category: Optional[str] = None) -> List[TimingRecord]:
353
+ """Get timing records, optionally filtered by category."""
354
+ with cls._lock:
355
+ if category:
356
+ return [t for t in cls._timings if t.category == category]
357
+ return cls._timings.copy()
358
+
359
+ @classmethod
360
+ def get_imports(cls, min_duration_ms: float = 0) -> List[ImportRecord]:
361
+ """Get import records, optionally filtered by minimum duration."""
362
+ with cls._lock:
363
+ if min_duration_ms > 0:
364
+ return [i for i in cls._imports if i.duration_ms >= min_duration_ms]
365
+ return cls._imports.copy()
366
+
367
+ @classmethod
368
+ def get_flow(cls) -> List[FlowRecord]:
369
+ """Get flow records."""
370
+ with cls._lock:
371
+ return cls._flow.copy()
372
+
373
+ @classmethod
374
+ def get_files_accessed(cls) -> Dict[str, int]:
375
+ """Get files accessed with counts."""
376
+ with cls._lock:
377
+ return cls._files_accessed.copy()
378
+
379
+ @classmethod
380
+ def get_summary(cls) -> Dict[str, Any]:
381
+ """Get profiling summary."""
382
+ with cls._lock:
383
+ total_time = sum(t.duration_ms for t in cls._timings)
384
+ import_time = sum(i.duration_ms for i in cls._imports)
385
+
386
+ # Group by category
387
+ by_category: Dict[str, float] = {}
388
+ for t in cls._timings:
389
+ by_category[t.category] = by_category.get(t.category, 0) + t.duration_ms
390
+
391
+ # Top slowest
392
+ slowest = sorted(cls._timings, key=lambda x: x.duration_ms, reverse=True)[:10]
393
+ slowest_imports = sorted(cls._imports, key=lambda x: x.duration_ms, reverse=True)[:10]
394
+
395
+ return {
396
+ 'total_time_ms': total_time,
397
+ 'import_time_ms': import_time,
398
+ 'timing_count': len(cls._timings),
399
+ 'import_count': len(cls._imports),
400
+ 'flow_steps': len(cls._flow),
401
+ 'files_accessed': len(cls._files_accessed),
402
+ 'by_category': by_category,
403
+ 'slowest_operations': [(s.name, s.duration_ms) for s in slowest],
404
+ 'slowest_imports': [(s.module, s.duration_ms) for s in slowest_imports],
405
+ }
406
+
407
+ @classmethod
408
+ def report(cls, output: str = "console") -> str:
409
+ """Generate and output profiling report."""
410
+ summary = cls.get_summary()
411
+
412
+ lines = [
413
+ "=" * 60,
414
+ "PraisonAI Profiling Report",
415
+ "=" * 60,
416
+ "",
417
+ f"Total Time: {summary['total_time_ms']:.2f}ms",
418
+ f"Import Time: {summary['import_time_ms']:.2f}ms",
419
+ f"Timing Records: {summary['timing_count']}",
420
+ f"Import Records: {summary['import_count']}",
421
+ f"Flow Steps: {summary['flow_steps']}",
422
+ f"Files Accessed: {summary['files_accessed']}",
423
+ "",
424
+ "By Category:",
425
+ ]
426
+
427
+ for cat, time_ms in summary['by_category'].items():
428
+ lines.append(f" {cat}: {time_ms:.2f}ms")
429
+
430
+ lines.extend([
431
+ "",
432
+ "Slowest Operations:",
433
+ ])
434
+ for name, time_ms in summary['slowest_operations']:
435
+ lines.append(f" {name}: {time_ms:.2f}ms")
436
+
437
+ lines.extend([
438
+ "",
439
+ "Slowest Imports:",
440
+ ])
441
+ for module, time_ms in summary['slowest_imports']:
442
+ lines.append(f" {module}: {time_ms:.2f}ms")
443
+
444
+ lines.append("=" * 60)
445
+
446
+ report_text = "\n".join(lines)
447
+
448
+ if output == "console":
449
+ print(report_text)
450
+
451
+ return report_text
452
+
453
+ # ========================================================================
454
+ # API Call Profiling
455
+ # ========================================================================
456
+
457
+ @classmethod
458
+ def record_api_call(cls, endpoint: str, method: str, duration_ms: float,
459
+ status_code: int = 0, request_size: int = 0,
460
+ response_size: int = 0) -> None:
461
+ """Record an API/HTTP call timing."""
462
+ if not cls.is_enabled():
463
+ return
464
+
465
+ with cls._lock:
466
+ cls._api_calls.append(APICallRecord(
467
+ endpoint=endpoint,
468
+ method=method,
469
+ duration_ms=duration_ms,
470
+ status_code=status_code,
471
+ request_size=request_size,
472
+ response_size=response_size
473
+ ))
474
+
475
+ @classmethod
476
+ def get_api_calls(cls) -> List[APICallRecord]:
477
+ """Get API call records."""
478
+ with cls._lock:
479
+ return cls._api_calls.copy()
480
+
481
+ @classmethod
482
+ @contextmanager
483
+ def api_call(cls, endpoint: str, method: str = "GET"):
484
+ """Context manager for profiling API calls."""
485
+ start = time.perf_counter()
486
+ call_info = {'status_code': 0, 'request_size': 0, 'response_size': 0}
487
+
488
+ try:
489
+ yield call_info
490
+ finally:
491
+ duration_ms = (time.perf_counter() - start) * 1000
492
+ cls.record_api_call(
493
+ endpoint=endpoint,
494
+ method=method,
495
+ duration_ms=duration_ms,
496
+ status_code=call_info.get('status_code', 0),
497
+ request_size=call_info.get('request_size', 0),
498
+ response_size=call_info.get('response_size', 0)
499
+ )
500
+
501
+ # ========================================================================
502
+ # Streaming Profiling
503
+ # ========================================================================
504
+
505
+ @classmethod
506
+ def record_streaming(cls, name: str, ttft_ms: float, total_ms: float,
507
+ chunk_count: int = 0, total_tokens: int = 0) -> None:
508
+ """Record streaming metrics."""
509
+ if not cls.is_enabled():
510
+ return
511
+
512
+ with cls._lock:
513
+ cls._streaming.append(StreamingRecord(
514
+ name=name,
515
+ ttft_ms=ttft_ms,
516
+ total_ms=total_ms,
517
+ chunk_count=chunk_count,
518
+ total_tokens=total_tokens
519
+ ))
520
+
521
+ @classmethod
522
+ def get_streaming_records(cls) -> List[StreamingRecord]:
523
+ """Get streaming records."""
524
+ with cls._lock:
525
+ return cls._streaming.copy()
526
+
527
+ @classmethod
528
+ @contextmanager
529
+ def streaming(cls, name: str):
530
+ """Context manager for profiling streaming operations."""
531
+ tracker = StreamingTracker(name)
532
+ tracker.start()
533
+ try:
534
+ yield tracker
535
+ finally:
536
+ tracker.end()
537
+
538
+ @classmethod
539
+ @asynccontextmanager
540
+ async def streaming_async(cls, name: str):
541
+ """Async context manager for profiling streaming operations."""
542
+ tracker = StreamingTracker(name)
543
+ tracker.start()
544
+ try:
545
+ yield tracker
546
+ finally:
547
+ tracker.end()
548
+
549
+ # ========================================================================
550
+ # Memory Profiling
551
+ # ========================================================================
552
+
553
+ @classmethod
554
+ def record_memory(cls, name: str, current_kb: float, peak_kb: float) -> None:
555
+ """Record memory usage."""
556
+ if not cls.is_enabled():
557
+ return
558
+
559
+ with cls._lock:
560
+ cls._memory.append(MemoryRecord(
561
+ name=name,
562
+ current_kb=current_kb,
563
+ peak_kb=peak_kb
564
+ ))
565
+
566
+ @classmethod
567
+ def get_memory_records(cls) -> List[MemoryRecord]:
568
+ """Get memory records."""
569
+ with cls._lock:
570
+ return cls._memory.copy()
571
+
572
+ @classmethod
573
+ @contextmanager
574
+ def memory(cls, name: str):
575
+ """Context manager for profiling memory usage."""
576
+ tracemalloc.start()
577
+ try:
578
+ yield
579
+ finally:
580
+ current, peak = tracemalloc.get_traced_memory()
581
+ tracemalloc.stop()
582
+ cls.record_memory(name, current / 1024, peak / 1024)
583
+
584
+ @classmethod
585
+ def memory_snapshot(cls) -> Dict[str, float]:
586
+ """Take a memory snapshot."""
587
+ tracemalloc.start()
588
+ current, peak = tracemalloc.get_traced_memory()
589
+ tracemalloc.stop()
590
+ return {
591
+ 'current_kb': current / 1024,
592
+ 'peak_kb': peak / 1024
593
+ }
594
+
595
+ # ========================================================================
596
+ # Statistics
597
+ # ========================================================================
598
+
599
+ @classmethod
600
+ def get_statistics(cls, category: Optional[str] = None) -> Dict[str, float]:
601
+ """
602
+ Get statistical analysis of timing data.
603
+
604
+ Returns p50, p95, p99, mean, std_dev, min, max.
605
+ """
606
+ with cls._lock:
607
+ if category:
608
+ durations = [t.duration_ms for t in cls._timings if t.category == category]
609
+ else:
610
+ durations = [t.duration_ms for t in cls._timings]
611
+
612
+ if not durations:
613
+ return {
614
+ 'p50': 0.0, 'p95': 0.0, 'p99': 0.0,
615
+ 'mean': 0.0, 'std_dev': 0.0, 'min': 0.0, 'max': 0.0,
616
+ 'count': 0
617
+ }
618
+
619
+ sorted_durations = sorted(durations)
620
+ n = len(sorted_durations)
621
+
622
+ def percentile(p: float) -> float:
623
+ idx = int(n * p / 100)
624
+ return sorted_durations[min(idx, n - 1)]
625
+
626
+ mean = statistics.mean(durations)
627
+ std_dev = statistics.stdev(durations) if n > 1 else 0.0
628
+
629
+ return {
630
+ 'p50': percentile(50),
631
+ 'p95': percentile(95),
632
+ 'p99': percentile(99),
633
+ 'mean': mean,
634
+ 'std_dev': std_dev,
635
+ 'min': min(durations),
636
+ 'max': max(durations),
637
+ 'count': n
638
+ }
639
+
640
+ # ========================================================================
641
+ # cProfile Integration
642
+ # ========================================================================
643
+
644
+ @classmethod
645
+ @contextmanager
646
+ def cprofile(cls, name: str):
647
+ """Context manager for cProfile profiling."""
648
+ pr = cProfile.Profile()
649
+ pr.enable()
650
+
651
+ try:
652
+ yield pr
653
+ finally:
654
+ pr.disable()
655
+
656
+ # Store stats
657
+ s = io.StringIO()
658
+ ps = pstats.Stats(pr, stream=s).sort_stats('cumulative')
659
+ ps.print_stats(30)
660
+
661
+ with cls._lock:
662
+ cls._cprofile_stats.append({
663
+ 'name': name,
664
+ 'stats': s.getvalue(),
665
+ 'total_calls': ps.total_calls if hasattr(ps, 'total_calls') else 0,
666
+ 'timestamp': time.time()
667
+ })
668
+
669
+ @classmethod
670
+ def get_cprofile_stats(cls) -> List[Dict[str, Any]]:
671
+ """Get cProfile statistics."""
672
+ with cls._lock:
673
+ return cls._cprofile_stats.copy()
674
+
675
+ # ========================================================================
676
+ # Line-Level Profiling
677
+ # ========================================================================
678
+
679
+ @classmethod
680
+ def get_line_profile_data(cls) -> Dict[str, Any]:
681
+ """Get line-level profiling data."""
682
+ with cls._lock:
683
+ return cls._line_profile_data.copy()
684
+
685
+ @classmethod
686
+ def set_line_profile_data(cls, func_name: str, data: Any) -> None:
687
+ """Store line-level profiling data."""
688
+ if not cls.is_enabled():
689
+ return
690
+ with cls._lock:
691
+ cls._line_profile_data[func_name] = data
692
+
693
+ # ========================================================================
694
+ # Flamegraph
695
+ # ========================================================================
696
+
697
+ @classmethod
698
+ def get_flamegraph_data(cls) -> List[Dict[str, Any]]:
699
+ """
700
+ Generate flamegraph-compatible data from flow records.
701
+
702
+ Returns list of {name, value, children} for flamegraph visualization.
703
+ """
704
+ with cls._lock:
705
+ # Convert flow records to flamegraph format
706
+ data = []
707
+ for record in cls._flow:
708
+ data.append({
709
+ 'name': record.name,
710
+ 'value': record.duration_ms,
711
+ 'file': record.file,
712
+ 'line': record.line
713
+ })
714
+ return data
715
+
716
+ @classmethod
717
+ def export_flamegraph(cls, filepath: str) -> None:
718
+ """
719
+ Export flamegraph to SVG file.
720
+
721
+ Note: Requires flamegraph data. For full flamegraph support,
722
+ use py-spy: py-spy record -o profile.svg -- python script.py
723
+ """
724
+ data = cls.get_flamegraph_data()
725
+
726
+ # Generate simple SVG flamegraph
727
+ svg_content = cls._generate_simple_flamegraph_svg(data)
728
+
729
+ with open(filepath, 'w') as f:
730
+ f.write(svg_content)
731
+
732
+ @classmethod
733
+ def _generate_simple_flamegraph_svg(cls, data: List[Dict[str, Any]]) -> str:
734
+ """Generate a simple SVG flamegraph."""
735
+ if not data:
736
+ return '<svg xmlns="http://www.w3.org/2000/svg" width="800" height="100"><text x="10" y="50">No profiling data</text></svg>'
737
+
738
+ total_time = sum(d['value'] for d in data)
739
+ if total_time == 0:
740
+ total_time = 1
741
+
742
+ width = 800
743
+ height = max(100, len(data) * 25 + 50)
744
+
745
+ svg_parts = [
746
+ f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}">',
747
+ '<style>rect:hover { opacity: 0.8; } text { font-family: monospace; font-size: 12px; }</style>',
748
+ f'<text x="10" y="20">PraisonAI Profiling Flamegraph (Total: {total_time:.2f}ms)</text>'
749
+ ]
750
+
751
+ y = 40
752
+ for item in sorted(data, key=lambda x: x['value'], reverse=True)[:20]:
753
+ bar_width = max(10, (item['value'] / total_time) * (width - 20))
754
+ color = f'hsl({int(item["value"] / total_time * 120)}, 70%, 50%)'
755
+
756
+ svg_parts.append(
757
+ f'<rect x="10" y="{y}" width="{bar_width}" height="20" fill="{color}" />'
758
+ )
759
+ svg_parts.append(
760
+ f'<text x="15" y="{y + 15}">{item["name"]}: {item["value"]:.2f}ms</text>'
761
+ )
762
+ y += 25
763
+
764
+ svg_parts.append('</svg>')
765
+ return '\n'.join(svg_parts)
766
+
767
+ # ========================================================================
768
+ # Export Functions
769
+ # ========================================================================
770
+
771
+ @classmethod
772
+ def export_json(cls) -> str:
773
+ """Export profiling data as JSON."""
774
+ # Get summary and stats first (they acquire their own locks)
775
+ summary = cls.get_summary()
776
+ stats = cls.get_statistics()
777
+
778
+ with cls._lock:
779
+ data = {
780
+ 'summary': summary,
781
+ 'statistics': stats,
782
+ 'timings': [asdict(t) for t in cls._timings],
783
+ 'api_calls': [asdict(a) for a in cls._api_calls],
784
+ 'streaming': [asdict(s) for s in cls._streaming],
785
+ 'memory': [asdict(m) for m in cls._memory],
786
+ 'imports': [asdict(i) for i in cls._imports],
787
+ 'flow': [asdict(f) for f in cls._flow]
788
+ }
789
+ return json.dumps(data, indent=2, default=str)
790
+
791
+ @classmethod
792
+ def export_html(cls) -> str:
793
+ """Export profiling data as HTML report."""
794
+ summary = cls.get_summary()
795
+ stats = cls.get_statistics()
796
+
797
+ html = f'''<!DOCTYPE html>
798
+ <html>
799
+ <head>
800
+ <title>PraisonAI Profiling Report</title>
801
+ <style>
802
+ body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 20px; }}
803
+ h1 {{ color: #333; }}
804
+ h2 {{ color: #666; border-bottom: 1px solid #ddd; padding-bottom: 5px; }}
805
+ table {{ border-collapse: collapse; width: 100%; margin: 10px 0; }}
806
+ th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
807
+ th {{ background-color: #f5f5f5; }}
808
+ .metric {{ display: inline-block; margin: 10px; padding: 15px; background: #f9f9f9; border-radius: 5px; }}
809
+ .metric-value {{ font-size: 24px; font-weight: bold; color: #0066cc; }}
810
+ .metric-label {{ font-size: 12px; color: #666; }}
811
+ </style>
812
+ </head>
813
+ <body>
814
+ <h1>PraisonAI Profiling Report</h1>
815
+
816
+ <h2>Summary</h2>
817
+ <div class="metric">
818
+ <div class="metric-value">{summary['total_time_ms']:.2f}ms</div>
819
+ <div class="metric-label">Total Time</div>
820
+ </div>
821
+ <div class="metric">
822
+ <div class="metric-value">{summary['timing_count']}</div>
823
+ <div class="metric-label">Operations</div>
824
+ </div>
825
+ <div class="metric">
826
+ <div class="metric-value">{len(cls._api_calls)}</div>
827
+ <div class="metric-label">API Calls</div>
828
+ </div>
829
+ <div class="metric">
830
+ <div class="metric-value">{len(cls._streaming)}</div>
831
+ <div class="metric-label">Streams</div>
832
+ </div>
833
+
834
+ <h2>Statistics</h2>
835
+ <table>
836
+ <tr><th>Metric</th><th>Value</th></tr>
837
+ <tr><td>P50 (Median)</td><td>{stats['p50']:.2f}ms</td></tr>
838
+ <tr><td>P95</td><td>{stats['p95']:.2f}ms</td></tr>
839
+ <tr><td>P99</td><td>{stats['p99']:.2f}ms</td></tr>
840
+ <tr><td>Mean</td><td>{stats['mean']:.2f}ms</td></tr>
841
+ <tr><td>Std Dev</td><td>{stats['std_dev']:.2f}ms</td></tr>
842
+ <tr><td>Min</td><td>{stats['min']:.2f}ms</td></tr>
843
+ <tr><td>Max</td><td>{stats['max']:.2f}ms</td></tr>
844
+ </table>
845
+
846
+ <h2>Slowest Operations</h2>
847
+ <table>
848
+ <tr><th>Operation</th><th>Duration (ms)</th></tr>
849
+ {''.join(f"<tr><td>{name}</td><td>{dur:.2f}</td></tr>" for name, dur in summary['slowest_operations'])}
850
+ </table>
851
+
852
+ <h2>API Calls</h2>
853
+ <table>
854
+ <tr><th>Endpoint</th><th>Method</th><th>Duration (ms)</th><th>Status</th></tr>
855
+ {''.join(f"<tr><td>{a.endpoint}</td><td>{a.method}</td><td>{a.duration_ms:.2f}</td><td>{a.status_code}</td></tr>" for a in cls._api_calls[:20])}
856
+ </table>
857
+
858
+ <h2>Streaming</h2>
859
+ <table>
860
+ <tr><th>Name</th><th>TTFT (ms)</th><th>Total (ms)</th><th>Chunks</th><th>Tokens</th></tr>
861
+ {''.join(f"<tr><td>{s.name}</td><td>{s.ttft_ms:.2f}</td><td>{s.total_ms:.2f}</td><td>{s.chunk_count}</td><td>{s.total_tokens}</td></tr>" for s in cls._streaming[:20])}
862
+ </table>
863
+ </body>
864
+ </html>'''
865
+ return html
866
+
867
+ @classmethod
868
+ def export_to_file(cls, filepath: str, format: str = "json") -> None:
869
+ """Export profiling data to file."""
870
+ if format == "json":
871
+ content = cls.export_json()
872
+ elif format == "html":
873
+ content = cls.export_html()
874
+ else:
875
+ raise ValueError(f"Unknown format: {format}")
876
+
877
+ with open(filepath, 'w') as f:
878
+ f.write(content)
879
+
880
+
881
+ # ============================================================================
882
+ # Decorators
883
+ # ============================================================================
884
+
885
+ def profile(func: Optional[Callable] = None, *, category: str = "function"):
886
+ """
887
+ Decorator to profile a function.
888
+
889
+ Usage:
890
+ @profile
891
+ def my_function():
892
+ pass
893
+
894
+ @profile(category="api")
895
+ def api_call():
896
+ pass
897
+ """
898
+ def decorator(fn: Callable) -> Callable:
899
+ @functools.wraps(fn)
900
+ def wrapper(*args, **kwargs):
901
+ if not Profiler.is_enabled():
902
+ return fn(*args, **kwargs)
903
+
904
+ start = time.time()
905
+ try:
906
+ return fn(*args, **kwargs)
907
+ finally:
908
+ duration_ms = (time.time() - start) * 1000
909
+ file = fn.__code__.co_filename if hasattr(fn, '__code__') else ""
910
+ line = fn.__code__.co_firstlineno if hasattr(fn, '__code__') else 0
911
+ Profiler.record_timing(fn.__name__, duration_ms, category, file, line)
912
+ Profiler.record_flow(fn.__name__, duration_ms, file, line)
913
+
914
+ return wrapper
915
+
916
+ if func is not None:
917
+ return decorator(func)
918
+ return decorator
919
+
920
+
921
+ def profile_async(func: Optional[Callable] = None, *, category: str = "async"):
922
+ """
923
+ Decorator to profile an async function.
924
+ """
925
+ def decorator(fn: Callable) -> Callable:
926
+ @functools.wraps(fn)
927
+ async def wrapper(*args, **kwargs):
928
+ if not Profiler.is_enabled():
929
+ return await fn(*args, **kwargs)
930
+
931
+ start = time.time()
932
+ try:
933
+ return await fn(*args, **kwargs)
934
+ finally:
935
+ duration_ms = (time.time() - start) * 1000
936
+ file = fn.__code__.co_filename if hasattr(fn, '__code__') else ""
937
+ line = fn.__code__.co_firstlineno if hasattr(fn, '__code__') else 0
938
+ Profiler.record_timing(fn.__name__, duration_ms, category, file, line)
939
+ Profiler.record_flow(fn.__name__, duration_ms, file, line)
940
+
941
+ return wrapper
942
+
943
+ if func is not None:
944
+ return decorator(func)
945
+ return decorator
946
+
947
+
948
+ # ============================================================================
949
+ # Import Profiling
950
+ # ============================================================================
951
+
952
+ class ImportProfiler:
953
+ """
954
+ Context manager to profile imports.
955
+
956
+ Usage:
957
+ with profile_imports() as profiler:
958
+ import heavy_module
959
+
960
+ print(profiler.get_imports())
961
+ """
962
+
963
+ def __init__(self):
964
+ self._original_import = None
965
+ self._imports: List[ImportRecord] = []
966
+
967
+ def __enter__(self):
968
+ import builtins
969
+ self._original_import = builtins.__import__
970
+
971
+ def profiled_import(name, globals=None, locals=None, fromlist=(), level=0):
972
+ start = time.time()
973
+ try:
974
+ return self._original_import(name, globals, locals, fromlist, level)
975
+ finally:
976
+ duration_ms = (time.time() - start) * 1000
977
+ if duration_ms > 1: # Only record imports > 1ms
978
+ record = ImportRecord(module=name, duration_ms=duration_ms)
979
+ self._imports.append(record)
980
+ Profiler.record_import(name, duration_ms)
981
+
982
+ builtins.__import__ = profiled_import
983
+ return self
984
+
985
+ def __exit__(self, exc_type, exc_val, exc_tb):
986
+ import builtins
987
+ builtins.__import__ = self._original_import
988
+ return False
989
+
990
+ def get_imports(self, min_duration_ms: float = 0) -> List[ImportRecord]:
991
+ """Get recorded imports."""
992
+ if min_duration_ms > 0:
993
+ return [i for i in self._imports if i.duration_ms >= min_duration_ms]
994
+ return self._imports.copy()
995
+
996
+ def get_slowest(self, n: int = 10) -> List[ImportRecord]:
997
+ """Get N slowest imports."""
998
+ return sorted(self._imports, key=lambda x: x.duration_ms, reverse=True)[:n]
999
+
1000
+
1001
+ def profile_imports():
1002
+ """Create an import profiler context manager."""
1003
+ return ImportProfiler()
1004
+
1005
+
1006
+ # ============================================================================
1007
+ # API Profiling Decorators
1008
+ # ============================================================================
1009
+
1010
+ def profile_api(func: Optional[Callable] = None, *, endpoint: str = ""):
1011
+ """
1012
+ Decorator to profile a function as an API call.
1013
+
1014
+ Usage:
1015
+ @profile_api(endpoint="openai/chat")
1016
+ def call_openai():
1017
+ pass
1018
+ """
1019
+ def decorator(fn: Callable) -> Callable:
1020
+ @functools.wraps(fn)
1021
+ def wrapper(*args, **kwargs):
1022
+ if not Profiler.is_enabled():
1023
+ return fn(*args, **kwargs)
1024
+
1025
+ ep = endpoint or fn.__name__
1026
+ start = time.perf_counter()
1027
+ try:
1028
+ return fn(*args, **kwargs)
1029
+ finally:
1030
+ duration_ms = (time.perf_counter() - start) * 1000
1031
+ Profiler.record_api_call(ep, "CALL", duration_ms)
1032
+
1033
+ return wrapper
1034
+
1035
+ if func is not None:
1036
+ return decorator(func)
1037
+ return decorator
1038
+
1039
+
1040
+ def profile_api_async(func: Optional[Callable] = None, *, endpoint: str = ""):
1041
+ """
1042
+ Decorator to profile an async function as an API call.
1043
+ """
1044
+ def decorator(fn: Callable) -> Callable:
1045
+ @functools.wraps(fn)
1046
+ async def wrapper(*args, **kwargs):
1047
+ if not Profiler.is_enabled():
1048
+ return await fn(*args, **kwargs)
1049
+
1050
+ ep = endpoint or fn.__name__
1051
+ start = time.perf_counter()
1052
+ try:
1053
+ return await fn(*args, **kwargs)
1054
+ finally:
1055
+ duration_ms = (time.perf_counter() - start) * 1000
1056
+ Profiler.record_api_call(ep, "CALL", duration_ms)
1057
+
1058
+ return wrapper
1059
+
1060
+ if func is not None:
1061
+ return decorator(func)
1062
+ return decorator
1063
+
1064
+
1065
+ # ============================================================================
1066
+ # cProfile Decorator
1067
+ # ============================================================================
1068
+
1069
+ def profile_detailed(func: Optional[Callable] = None):
1070
+ """
1071
+ Decorator for detailed cProfile profiling.
1072
+
1073
+ Usage:
1074
+ @profile_detailed
1075
+ def heavy_computation():
1076
+ pass
1077
+ """
1078
+ def decorator(fn: Callable) -> Callable:
1079
+ @functools.wraps(fn)
1080
+ def wrapper(*args, **kwargs):
1081
+ if not Profiler.is_enabled():
1082
+ return fn(*args, **kwargs)
1083
+
1084
+ pr = cProfile.Profile()
1085
+ pr.enable()
1086
+ try:
1087
+ return fn(*args, **kwargs)
1088
+ finally:
1089
+ pr.disable()
1090
+ s = io.StringIO()
1091
+ ps = pstats.Stats(pr, stream=s).sort_stats('cumulative')
1092
+ ps.print_stats(20)
1093
+ Profiler._cprofile_stats.append({
1094
+ 'name': fn.__name__,
1095
+ 'stats': s.getvalue(),
1096
+ 'timestamp': time.time()
1097
+ })
1098
+
1099
+ return wrapper
1100
+
1101
+ if func is not None:
1102
+ return decorator(func)
1103
+ return decorator
1104
+
1105
+
1106
+ # ============================================================================
1107
+ # Line-Level Profiling Decorator
1108
+ # ============================================================================
1109
+
1110
+ def profile_lines(func: Optional[Callable] = None):
1111
+ """
1112
+ Decorator for line-level profiling.
1113
+
1114
+ Note: Requires line_profiler package for full functionality.
1115
+ Falls back to basic timing if not available.
1116
+
1117
+ Usage:
1118
+ @profile_lines
1119
+ def my_function():
1120
+ pass
1121
+ """
1122
+ def decorator(fn: Callable) -> Callable:
1123
+ @functools.wraps(fn)
1124
+ def wrapper(*args, **kwargs):
1125
+ if not Profiler.is_enabled():
1126
+ return fn(*args, **kwargs)
1127
+
1128
+ # Try to use line_profiler if available
1129
+ try:
1130
+ from line_profiler import LineProfiler
1131
+ lp = LineProfiler()
1132
+ lp.add_function(fn)
1133
+ lp.enable()
1134
+ try:
1135
+ result = fn(*args, **kwargs)
1136
+ finally:
1137
+ lp.disable()
1138
+ s = io.StringIO()
1139
+ lp.print_stats(stream=s)
1140
+ Profiler.set_line_profile_data(fn.__name__, s.getvalue())
1141
+ return result
1142
+ except ImportError:
1143
+ # Fallback to basic timing
1144
+ start = time.perf_counter()
1145
+ try:
1146
+ return fn(*args, **kwargs)
1147
+ finally:
1148
+ duration_ms = (time.perf_counter() - start) * 1000
1149
+ Profiler.set_line_profile_data(fn.__name__, {
1150
+ 'note': 'line_profiler not installed',
1151
+ 'total_ms': duration_ms
1152
+ })
1153
+
1154
+ return wrapper
1155
+
1156
+ if func is not None:
1157
+ return decorator(func)
1158
+ return decorator
1159
+
1160
+
1161
+ # ============================================================================
1162
+ # Quick Profiling Functions
1163
+ # ============================================================================
1164
+
1165
+ def time_import(module_name: str) -> float:
1166
+ """
1167
+ Time how long it takes to import a module.
1168
+
1169
+ Returns duration in milliseconds.
1170
+ """
1171
+ start = time.time()
1172
+ __import__(module_name)
1173
+ return (time.time() - start) * 1000
1174
+
1175
+
1176
+ def check_module_available(module_name: str) -> bool:
1177
+ """
1178
+ Check if a module is available without importing it.
1179
+
1180
+ Uses importlib.util.find_spec which is fast.
1181
+ """
1182
+ import importlib.util
1183
+ return importlib.util.find_spec(module_name) is not None
1184
+
1185
+
1186
+ # ============================================================================
1187
+ # Exports
1188
+ # ============================================================================
1189
+
1190
+ __all__ = [
1191
+ # Core
1192
+ 'Profiler',
1193
+ 'StreamingTracker',
1194
+ # Decorators
1195
+ 'profile',
1196
+ 'profile_async',
1197
+ 'profile_api',
1198
+ 'profile_api_async',
1199
+ 'profile_detailed',
1200
+ 'profile_lines',
1201
+ # Import profiling
1202
+ 'profile_imports',
1203
+ 'ImportProfiler',
1204
+ # Utilities
1205
+ 'time_import',
1206
+ 'check_module_available',
1207
+ # Data classes
1208
+ 'TimingRecord',
1209
+ 'ImportRecord',
1210
+ 'FlowRecord',
1211
+ 'APICallRecord',
1212
+ 'StreamingRecord',
1213
+ 'MemoryRecord',
1214
+ ]