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
@@ -0,0 +1,613 @@
1
+ """
2
+ Recipe Registry HTTP Server
3
+
4
+ Provides a local HTTP server for recipe registry operations.
5
+ Supports:
6
+ - GET /healthz - Health check
7
+ - GET /v1/recipes - List recipes (pagination)
8
+ - GET /v1/recipes/{name} - Get recipe info (all versions)
9
+ - GET /v1/recipes/{name}/{version} - Get specific version info
10
+ - GET /v1/recipes/{name}/{version}/download - Download bundle
11
+ - POST /v1/recipes/{name}/{version} - Publish bundle (multipart)
12
+ - DELETE /v1/recipes/{name}/{version} - Delete version
13
+ - GET /v1/search?q=... - Search recipes
14
+
15
+ Usage:
16
+ from praisonai.recipe.server import create_app, run_server
17
+
18
+ # Run server
19
+ run_server(host="127.0.0.1", port=7777)
20
+
21
+ # Or get ASGI app for custom deployment
22
+ app = create_app(registry_path="~/.praison/registry")
23
+ """
24
+
25
+ import json
26
+ import hashlib
27
+ import os
28
+ import re
29
+ import tempfile
30
+ from pathlib import Path
31
+ from typing import Any, Callable, Dict, List, Optional, Tuple
32
+ from urllib.parse import parse_qs, unquote
33
+
34
+ from .registry import (
35
+ LocalRegistry,
36
+ RegistryError,
37
+ RecipeNotFoundError,
38
+ RecipeExistsError,
39
+ RegistryAuthError,
40
+ DEFAULT_REGISTRY_PATH,
41
+ DEFAULT_REGISTRY_PORT,
42
+ _calculate_checksum,
43
+ _get_timestamp,
44
+ )
45
+
46
+
47
+ class RegistryServer:
48
+ """
49
+ Simple HTTP server for recipe registry.
50
+
51
+ Uses only stdlib for minimal dependencies.
52
+ For production, consider using with uvicorn/gunicorn.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ registry_path: Optional[Path] = None,
58
+ token: Optional[str] = None,
59
+ read_only: bool = False,
60
+ ):
61
+ """
62
+ Initialize registry server.
63
+
64
+ Args:
65
+ registry_path: Path to registry directory
66
+ token: Required token for write operations (optional)
67
+ read_only: If True, disable all write operations
68
+ """
69
+ self.registry = LocalRegistry(registry_path)
70
+ self.token = token or os.environ.get("PRAISONAI_REGISTRY_TOKEN")
71
+ self.read_only = read_only
72
+ self._routes: List[Tuple[str, str, Callable]] = []
73
+ self._setup_routes()
74
+
75
+ def _setup_routes(self):
76
+ """Setup URL routes."""
77
+ self._routes = [
78
+ ("GET", r"^/healthz$", self._handle_health),
79
+ ("GET", r"^/v1/recipes$", self._handle_list_recipes),
80
+ ("GET", r"^/v1/recipes/([^/]+)$", self._handle_get_recipe),
81
+ ("GET", r"^/v1/recipes/([^/]+)/([^/]+)$", self._handle_get_version),
82
+ ("GET", r"^/v1/recipes/([^/]+)/([^/]+)/download$", self._handle_download),
83
+ ("POST", r"^/v1/recipes/([^/]+)/([^/]+)$", self._handle_publish),
84
+ ("DELETE", r"^/v1/recipes/([^/]+)/([^/]+)$", self._handle_delete_version),
85
+ ("DELETE", r"^/v1/recipes/([^/]+)$", self._handle_delete_recipe),
86
+ ("GET", r"^/v1/search$", self._handle_search),
87
+ ]
88
+
89
+ def _check_auth(self, headers: Dict[str, str]) -> bool:
90
+ """Check if request is authorized for write operations."""
91
+ if not self.token:
92
+ return True
93
+
94
+ auth = headers.get("Authorization", "")
95
+ if auth.startswith("Bearer "):
96
+ return auth[7:] == self.token
97
+ return False
98
+
99
+ def _json_response(
100
+ self,
101
+ data: Any,
102
+ status: int = 200,
103
+ headers: Optional[Dict[str, str]] = None,
104
+ ) -> Tuple[int, Dict[str, str], bytes]:
105
+ """Create JSON response."""
106
+ resp_headers = {
107
+ "Content-Type": "application/json",
108
+ "X-Registry-Version": "1.0",
109
+ }
110
+ if headers:
111
+ resp_headers.update(headers)
112
+
113
+ body = json.dumps(data, indent=2).encode("utf-8")
114
+ return status, resp_headers, body
115
+
116
+ def _error_response(
117
+ self,
118
+ message: str,
119
+ status: int = 400,
120
+ code: Optional[str] = None,
121
+ ) -> Tuple[int, Dict[str, str], bytes]:
122
+ """Create error response."""
123
+ return self._json_response({
124
+ "ok": False,
125
+ "error": message,
126
+ "code": code or "error",
127
+ }, status=status)
128
+
129
+ def _file_response(
130
+ self,
131
+ file_path: Path,
132
+ filename: str,
133
+ ) -> Tuple[int, Dict[str, str], bytes]:
134
+ """Create file download response."""
135
+ with open(file_path, "rb") as f:
136
+ content = f.read()
137
+
138
+ checksum = hashlib.sha256(content).hexdigest()
139
+
140
+ headers = {
141
+ "Content-Type": "application/gzip",
142
+ "Content-Disposition": f'attachment; filename="{filename}"',
143
+ "Content-Length": str(len(content)),
144
+ "ETag": f'"{checksum[:16]}"',
145
+ "X-Checksum-SHA256": checksum,
146
+ }
147
+
148
+ return 200, headers, content
149
+
150
+ def _parse_multipart(
151
+ self,
152
+ body: bytes,
153
+ content_type: str,
154
+ ) -> Dict[str, Any]:
155
+ """Parse multipart form data."""
156
+ # Extract boundary
157
+ boundary = None
158
+ for part in content_type.split(";"):
159
+ part = part.strip()
160
+ if part.startswith("boundary="):
161
+ boundary = part[9:].strip('"')
162
+ break
163
+
164
+ if not boundary:
165
+ raise ValueError("Missing boundary in multipart data")
166
+
167
+ result = {"fields": {}, "files": {}}
168
+ boundary_bytes = f"--{boundary}".encode()
169
+
170
+ parts = body.split(boundary_bytes)
171
+ for part in parts[1:]: # Skip preamble
172
+ if part.startswith(b"--"):
173
+ break # End marker
174
+
175
+ # Split headers and content
176
+ if b"\r\n\r\n" in part:
177
+ header_section, content = part.split(b"\r\n\r\n", 1)
178
+ else:
179
+ continue
180
+
181
+ # Parse headers
182
+ headers = {}
183
+ for line in header_section.decode("utf-8", errors="ignore").split("\r\n"):
184
+ if ":" in line:
185
+ key, value = line.split(":", 1)
186
+ headers[key.strip().lower()] = value.strip()
187
+
188
+ # Get field name and filename
189
+ disposition = headers.get("content-disposition", "")
190
+ name = None
191
+ filename = None
192
+
193
+ for item in disposition.split(";"):
194
+ item = item.strip()
195
+ if item.startswith("name="):
196
+ name = item[5:].strip('"')
197
+ elif item.startswith("filename="):
198
+ filename = item[9:].strip('"')
199
+
200
+ if not name:
201
+ continue
202
+
203
+ # Remove trailing \r\n
204
+ content = content.rstrip(b"\r\n")
205
+
206
+ if filename:
207
+ result["files"][name] = {
208
+ "filename": filename,
209
+ "content": content,
210
+ "content_type": headers.get("content-type", "application/octet-stream"),
211
+ }
212
+ else:
213
+ result["fields"][name] = content.decode("utf-8", errors="ignore")
214
+
215
+ return result
216
+
217
+ def handle_request(
218
+ self,
219
+ method: str,
220
+ path: str,
221
+ headers: Dict[str, str],
222
+ body: bytes,
223
+ query_string: str = "",
224
+ ) -> Tuple[int, Dict[str, str], bytes]:
225
+ """
226
+ Handle HTTP request.
227
+
228
+ Args:
229
+ method: HTTP method
230
+ path: URL path
231
+ headers: Request headers
232
+ body: Request body
233
+ query_string: Query string
234
+
235
+ Returns:
236
+ Tuple of (status_code, headers, body)
237
+ """
238
+ # Parse query string
239
+ query = parse_qs(query_string)
240
+
241
+ # Find matching route
242
+ for route_method, pattern, handler in self._routes:
243
+ if method != route_method:
244
+ continue
245
+
246
+ match = re.match(pattern, path)
247
+ if match:
248
+ try:
249
+ return handler(
250
+ headers=headers,
251
+ body=body,
252
+ query=query,
253
+ path_params=match.groups(),
254
+ )
255
+ except RecipeNotFoundError as e:
256
+ return self._error_response(str(e), status=404, code="not_found")
257
+ except RecipeExistsError as e:
258
+ return self._error_response(str(e), status=409, code="conflict")
259
+ except RegistryAuthError as e:
260
+ return self._error_response(str(e), status=401, code="auth_error")
261
+ except RegistryError as e:
262
+ return self._error_response(str(e), status=400, code="registry_error")
263
+ except Exception as e:
264
+ return self._error_response(f"Internal error: {e}", status=500, code="internal_error")
265
+
266
+ return self._error_response("Not found", status=404, code="not_found")
267
+
268
+ def _handle_health(self, **kwargs) -> Tuple[int, Dict[str, str], bytes]:
269
+ """Handle GET /healthz."""
270
+ return self._json_response({
271
+ "ok": True,
272
+ "status": "healthy",
273
+ "timestamp": _get_timestamp(),
274
+ "read_only": self.read_only,
275
+ "auth_required": bool(self.token),
276
+ })
277
+
278
+ def _handle_list_recipes(
279
+ self,
280
+ query: Dict[str, List[str]],
281
+ **kwargs,
282
+ ) -> Tuple[int, Dict[str, str], bytes]:
283
+ """Handle GET /v1/recipes."""
284
+ page = int(query.get("page", ["1"])[0])
285
+ per_page = min(int(query.get("per_page", ["50"])[0]), 100)
286
+ tags = query.get("tags", [""])[0].split(",") if query.get("tags") else None
287
+
288
+ if tags and tags[0] == "":
289
+ tags = None
290
+
291
+ recipes = self.registry.list_recipes(tags=tags)
292
+
293
+ # Paginate
294
+ start = (page - 1) * per_page
295
+ end = start + per_page
296
+ paginated = recipes[start:end]
297
+
298
+ return self._json_response({
299
+ "ok": True,
300
+ "recipes": paginated,
301
+ "total": len(recipes),
302
+ "page": page,
303
+ "per_page": per_page,
304
+ "has_more": end < len(recipes),
305
+ })
306
+
307
+ def _handle_get_recipe(
308
+ self,
309
+ path_params: Tuple[str, ...],
310
+ **kwargs,
311
+ ) -> Tuple[int, Dict[str, str], bytes]:
312
+ """Handle GET /v1/recipes/{name}."""
313
+ name = unquote(path_params[0])
314
+
315
+ versions = self.registry.get_versions(name)
316
+ info = self.registry.get_info(name)
317
+
318
+ return self._json_response({
319
+ "ok": True,
320
+ "name": name,
321
+ "versions": versions,
322
+ "latest": info.get("version"),
323
+ "description": info.get("description", ""),
324
+ "tags": info.get("tags", []),
325
+ })
326
+
327
+ def _handle_get_version(
328
+ self,
329
+ path_params: Tuple[str, ...],
330
+ **kwargs,
331
+ ) -> Tuple[int, Dict[str, str], bytes]:
332
+ """Handle GET /v1/recipes/{name}/{version}."""
333
+ name = unquote(path_params[0])
334
+ version = unquote(path_params[1])
335
+
336
+ info = self.registry.get_info(name, version)
337
+
338
+ return self._json_response({
339
+ "ok": True,
340
+ **info,
341
+ })
342
+
343
+ def _handle_download(
344
+ self,
345
+ path_params: Tuple[str, ...],
346
+ headers: Dict[str, str],
347
+ **kwargs,
348
+ ) -> Tuple[int, Dict[str, str], bytes]:
349
+ """Handle GET /v1/recipes/{name}/{version}/download."""
350
+ name = unquote(path_params[0])
351
+ version = unquote(path_params[1])
352
+
353
+ # Get bundle path
354
+ bundle_name = f"{name}-{version}.praison"
355
+ bundle_path = self.registry.recipes_path / name / version / bundle_name
356
+
357
+ if not bundle_path.exists():
358
+ raise RecipeNotFoundError(f"Bundle not found: {name}@{version}")
359
+
360
+ # Check ETag for caching
361
+ checksum = _calculate_checksum(bundle_path)
362
+ etag = f'"{checksum[:16]}"'
363
+
364
+ if_none_match = headers.get("If-None-Match", "")
365
+ if if_none_match == etag:
366
+ return 304, {"ETag": etag}, b""
367
+
368
+ return self._file_response(bundle_path, bundle_name)
369
+
370
+ def _handle_publish(
371
+ self,
372
+ path_params: Tuple[str, ...],
373
+ headers: Dict[str, str],
374
+ body: bytes,
375
+ **kwargs,
376
+ ) -> Tuple[int, Dict[str, str], bytes]:
377
+ """Handle POST /v1/recipes/{name}/{version}."""
378
+ if self.read_only:
379
+ return self._error_response("Registry is read-only", status=403, code="read_only")
380
+
381
+ if not self._check_auth(headers):
382
+ return self._error_response("Authentication required", status=401, code="auth_required")
383
+
384
+ name = unquote(path_params[0])
385
+ version = unquote(path_params[1])
386
+
387
+ # Parse multipart data
388
+ content_type = headers.get("Content-Type", "")
389
+ if not content_type.startswith("multipart/form-data"):
390
+ return self._error_response("Expected multipart/form-data", status=400)
391
+
392
+ try:
393
+ data = self._parse_multipart(body, content_type)
394
+ except ValueError as e:
395
+ return self._error_response(f"Invalid multipart data: {e}", status=400)
396
+
397
+ if "bundle" not in data["files"]:
398
+ return self._error_response("Missing bundle file", status=400)
399
+
400
+ force = data["fields"].get("force", "false").lower() == "true"
401
+ bundle_content = data["files"]["bundle"]["content"]
402
+
403
+ # Save to temp file and publish
404
+ with tempfile.NamedTemporaryFile(suffix=".praison", delete=False) as tmp:
405
+ tmp.write(bundle_content)
406
+ tmp_path = Path(tmp.name)
407
+
408
+ try:
409
+ result = self.registry.publish(tmp_path, force=force)
410
+
411
+ # Verify name/version match
412
+ if result["name"] != name or result["version"] != version:
413
+ # Rollback
414
+ self.registry.delete(result["name"], result["version"])
415
+ return self._error_response(
416
+ f"Bundle name/version ({result['name']}@{result['version']}) "
417
+ f"doesn't match URL ({name}@{version})",
418
+ status=400,
419
+ )
420
+
421
+ return self._json_response({
422
+ "ok": True,
423
+ **result,
424
+ }, status=201)
425
+ finally:
426
+ tmp_path.unlink(missing_ok=True)
427
+
428
+ def _handle_delete_version(
429
+ self,
430
+ path_params: Tuple[str, ...],
431
+ headers: Dict[str, str],
432
+ **kwargs,
433
+ ) -> Tuple[int, Dict[str, str], bytes]:
434
+ """Handle DELETE /v1/recipes/{name}/{version}."""
435
+ if self.read_only:
436
+ return self._error_response("Registry is read-only", status=403, code="read_only")
437
+
438
+ if not self._check_auth(headers):
439
+ return self._error_response("Authentication required", status=401, code="auth_required")
440
+
441
+ name = unquote(path_params[0])
442
+ version = unquote(path_params[1])
443
+
444
+ self.registry.delete(name, version)
445
+
446
+ return self._json_response({
447
+ "ok": True,
448
+ "deleted": f"{name}@{version}",
449
+ })
450
+
451
+ def _handle_delete_recipe(
452
+ self,
453
+ path_params: Tuple[str, ...],
454
+ headers: Dict[str, str],
455
+ **kwargs,
456
+ ) -> Tuple[int, Dict[str, str], bytes]:
457
+ """Handle DELETE /v1/recipes/{name}."""
458
+ if self.read_only:
459
+ return self._error_response("Registry is read-only", status=403, code="read_only")
460
+
461
+ if not self._check_auth(headers):
462
+ return self._error_response("Authentication required", status=401, code="auth_required")
463
+
464
+ name = unquote(path_params[0])
465
+
466
+ self.registry.delete(name)
467
+
468
+ return self._json_response({
469
+ "ok": True,
470
+ "deleted": name,
471
+ })
472
+
473
+ def _handle_search(
474
+ self,
475
+ query: Dict[str, List[str]],
476
+ **kwargs,
477
+ ) -> Tuple[int, Dict[str, str], bytes]:
478
+ """Handle GET /v1/search."""
479
+ q = query.get("q", [""])[0]
480
+ tags = query.get("tags", [""])[0].split(",") if query.get("tags") else None
481
+
482
+ if tags and tags[0] == "":
483
+ tags = None
484
+
485
+ if not q:
486
+ return self._error_response("Query parameter 'q' required", status=400)
487
+
488
+ results = self.registry.search(q, tags=tags)
489
+
490
+ return self._json_response({
491
+ "ok": True,
492
+ "query": q,
493
+ "results": results,
494
+ "count": len(results),
495
+ })
496
+
497
+
498
+ def create_wsgi_app(
499
+ registry_path: Optional[Path] = None,
500
+ token: Optional[str] = None,
501
+ read_only: bool = False,
502
+ ) -> Callable:
503
+ """
504
+ Create WSGI application for the registry server.
505
+
506
+ Args:
507
+ registry_path: Path to registry directory
508
+ token: Required token for write operations
509
+ read_only: If True, disable write operations
510
+
511
+ Returns:
512
+ WSGI application callable
513
+ """
514
+ server = RegistryServer(
515
+ registry_path=registry_path,
516
+ token=token,
517
+ read_only=read_only,
518
+ )
519
+
520
+ def app(environ, start_response):
521
+ method = environ.get("REQUEST_METHOD", "GET")
522
+ path = environ.get("PATH_INFO", "/")
523
+ query_string = environ.get("QUERY_STRING", "")
524
+
525
+ # Read headers
526
+ headers = {}
527
+ for key, value in environ.items():
528
+ if key.startswith("HTTP_"):
529
+ header_name = key[5:].replace("_", "-").title()
530
+ headers[header_name] = value
531
+ headers["Content-Type"] = environ.get("CONTENT_TYPE", "")
532
+
533
+ # Read body
534
+ try:
535
+ content_length = int(environ.get("CONTENT_LENGTH", 0))
536
+ except (ValueError, TypeError):
537
+ content_length = 0
538
+
539
+ body = environ["wsgi.input"].read(content_length) if content_length > 0 else b""
540
+
541
+ # Handle request
542
+ status_code, resp_headers, resp_body = server.handle_request(
543
+ method=method,
544
+ path=path,
545
+ headers=headers,
546
+ body=body,
547
+ query_string=query_string,
548
+ )
549
+
550
+ # Send response
551
+ status = f"{status_code} {'OK' if status_code < 400 else 'Error'}"
552
+ response_headers = [(k, v) for k, v in resp_headers.items()]
553
+ start_response(status, response_headers)
554
+
555
+ return [resp_body]
556
+
557
+ return app
558
+
559
+
560
+ def run_server(
561
+ host: str = "127.0.0.1",
562
+ port: int = DEFAULT_REGISTRY_PORT,
563
+ registry_path: Optional[Path] = None,
564
+ token: Optional[str] = None,
565
+ read_only: bool = False,
566
+ ):
567
+ """
568
+ Run the registry server using stdlib wsgiref.
569
+
570
+ Args:
571
+ host: Host to bind to
572
+ port: Port to bind to
573
+ registry_path: Path to registry directory
574
+ token: Required token for write operations
575
+ read_only: If True, disable write operations
576
+ """
577
+ from wsgiref.simple_server import make_server, WSGIRequestHandler
578
+
579
+ # Custom handler to suppress logs unless verbose
580
+ class QuietHandler(WSGIRequestHandler):
581
+ def log_message(self, format, *args):
582
+ pass # Suppress default logging
583
+
584
+ app = create_wsgi_app(
585
+ registry_path=registry_path,
586
+ token=token,
587
+ read_only=read_only,
588
+ )
589
+
590
+ server = make_server(host, port, app, handler_class=QuietHandler)
591
+
592
+ print(f"Recipe Registry Server running on http://{host}:{port}")
593
+ print(f"Registry path: {registry_path or DEFAULT_REGISTRY_PATH}")
594
+ if token:
595
+ print("Authentication: enabled (token required for writes)")
596
+ if read_only:
597
+ print("Mode: read-only")
598
+ print("\nEndpoints:")
599
+ print(" GET /healthz - Health check")
600
+ print(" GET /v1/recipes - List recipes")
601
+ print(" GET /v1/recipes/{name} - Get recipe info")
602
+ print(" GET /v1/recipes/{name}/{version} - Get version info")
603
+ print(" GET /v1/recipes/{name}/{version}/download - Download bundle")
604
+ print(" POST /v1/recipes/{name}/{version} - Publish bundle")
605
+ print(" DELETE /v1/recipes/{name}/{version} - Delete version")
606
+ print(" GET /v1/search?q=... - Search recipes")
607
+ print("\nPress Ctrl+C to stop")
608
+
609
+ try:
610
+ server.serve_forever()
611
+ except KeyboardInterrupt:
612
+ print("\nShutting down...")
613
+ server.shutdown()
@@ -0,0 +1,45 @@
1
+ """
2
+ PraisonAI Scheduler Module
3
+
4
+ This module provides scheduling capabilities for running agents and deployments
5
+ at regular intervals, enabling 24/7 autonomous operations.
6
+
7
+ Components:
8
+ - ScheduleParser: Parse schedule expressions (hourly, daily, */30m, etc.)
9
+ - ExecutorInterface: Abstract interface for executors
10
+ - AgentScheduler: Schedule agent execution at regular intervals
11
+ - DeploymentScheduler: Schedule deployment operations
12
+ """
13
+
14
+ from .base import ScheduleParser, ExecutorInterface, PraisonAgentExecutor
15
+
16
+ __all__ = [
17
+ 'ScheduleParser',
18
+ 'ExecutorInterface',
19
+ 'PraisonAgentExecutor',
20
+ ]
21
+
22
+ # Lazy imports for modules that will be created later
23
+ def __getattr__(name):
24
+ if name == 'AgentScheduler':
25
+ from .agent_scheduler import AgentScheduler
26
+ return AgentScheduler
27
+ elif name == 'create_agent_scheduler':
28
+ from .agent_scheduler import create_agent_scheduler
29
+ return create_agent_scheduler
30
+ elif name == 'create_scheduler':
31
+ # Return a factory function that creates a mock scheduler for testing
32
+ def _create_scheduler(provider='gcp', **kwargs):
33
+ from .agent_scheduler import AgentScheduler
34
+ # Create a mock agent and task for testing
35
+ class MockAgent:
36
+ pass
37
+ return AgentScheduler(MockAgent(), "test task")
38
+ return _create_scheduler
39
+ elif name == 'DeploymentScheduler':
40
+ from .agent_scheduler import AgentScheduler
41
+ return AgentScheduler
42
+ elif name == 'create_deployment_scheduler':
43
+ from .agent_scheduler import create_agent_scheduler
44
+ return create_agent_scheduler
45
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")