ai-parrot 0.17.2__cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.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 (535) hide show
  1. agentui/.prettierrc +15 -0
  2. agentui/QUICKSTART.md +272 -0
  3. agentui/README.md +59 -0
  4. agentui/env.example +16 -0
  5. agentui/jsconfig.json +14 -0
  6. agentui/package-lock.json +4242 -0
  7. agentui/package.json +34 -0
  8. agentui/scripts/postinstall/apply-patches.mjs +260 -0
  9. agentui/src/app.css +61 -0
  10. agentui/src/app.d.ts +13 -0
  11. agentui/src/app.html +12 -0
  12. agentui/src/components/LoadingSpinner.svelte +64 -0
  13. agentui/src/components/ThemeSwitcher.svelte +159 -0
  14. agentui/src/components/index.js +4 -0
  15. agentui/src/lib/api/bots.ts +60 -0
  16. agentui/src/lib/api/chat.ts +22 -0
  17. agentui/src/lib/api/http.ts +25 -0
  18. agentui/src/lib/components/BotCard.svelte +33 -0
  19. agentui/src/lib/components/ChatBubble.svelte +63 -0
  20. agentui/src/lib/components/Toast.svelte +21 -0
  21. agentui/src/lib/config.ts +20 -0
  22. agentui/src/lib/stores/auth.svelte.ts +73 -0
  23. agentui/src/lib/stores/theme.svelte.js +64 -0
  24. agentui/src/lib/stores/toast.svelte.ts +31 -0
  25. agentui/src/lib/utils/conversation.ts +39 -0
  26. agentui/src/routes/+layout.svelte +20 -0
  27. agentui/src/routes/+page.svelte +232 -0
  28. agentui/src/routes/login/+page.svelte +200 -0
  29. agentui/src/routes/talk/[agentId]/+page.svelte +297 -0
  30. agentui/src/routes/talk/[agentId]/+page.ts +7 -0
  31. agentui/static/README.md +1 -0
  32. agentui/svelte.config.js +11 -0
  33. agentui/tailwind.config.ts +53 -0
  34. agentui/tsconfig.json +3 -0
  35. agentui/vite.config.ts +10 -0
  36. ai_parrot-0.17.2.dist-info/METADATA +472 -0
  37. ai_parrot-0.17.2.dist-info/RECORD +535 -0
  38. ai_parrot-0.17.2.dist-info/WHEEL +6 -0
  39. ai_parrot-0.17.2.dist-info/entry_points.txt +2 -0
  40. ai_parrot-0.17.2.dist-info/licenses/LICENSE +21 -0
  41. ai_parrot-0.17.2.dist-info/top_level.txt +6 -0
  42. crew-builder/.prettierrc +15 -0
  43. crew-builder/QUICKSTART.md +259 -0
  44. crew-builder/README.md +113 -0
  45. crew-builder/env.example +17 -0
  46. crew-builder/jsconfig.json +14 -0
  47. crew-builder/package-lock.json +4182 -0
  48. crew-builder/package.json +37 -0
  49. crew-builder/scripts/postinstall/apply-patches.mjs +260 -0
  50. crew-builder/src/app.css +62 -0
  51. crew-builder/src/app.d.ts +13 -0
  52. crew-builder/src/app.html +12 -0
  53. crew-builder/src/components/LoadingSpinner.svelte +64 -0
  54. crew-builder/src/components/ThemeSwitcher.svelte +149 -0
  55. crew-builder/src/components/index.js +9 -0
  56. crew-builder/src/lib/api/bots.ts +60 -0
  57. crew-builder/src/lib/api/chat.ts +80 -0
  58. crew-builder/src/lib/api/client.ts +56 -0
  59. crew-builder/src/lib/api/crew/crew.ts +136 -0
  60. crew-builder/src/lib/api/index.ts +5 -0
  61. crew-builder/src/lib/api/o365/auth.ts +65 -0
  62. crew-builder/src/lib/auth/auth.ts +54 -0
  63. crew-builder/src/lib/components/AgentNode.svelte +43 -0
  64. crew-builder/src/lib/components/BotCard.svelte +33 -0
  65. crew-builder/src/lib/components/ChatBubble.svelte +67 -0
  66. crew-builder/src/lib/components/ConfigPanel.svelte +278 -0
  67. crew-builder/src/lib/components/JsonTreeNode.svelte +76 -0
  68. crew-builder/src/lib/components/JsonViewer.svelte +24 -0
  69. crew-builder/src/lib/components/MarkdownEditor.svelte +48 -0
  70. crew-builder/src/lib/components/ThemeToggle.svelte +36 -0
  71. crew-builder/src/lib/components/Toast.svelte +67 -0
  72. crew-builder/src/lib/components/Toolbar.svelte +157 -0
  73. crew-builder/src/lib/components/index.ts +10 -0
  74. crew-builder/src/lib/config.ts +8 -0
  75. crew-builder/src/lib/stores/auth.svelte.ts +228 -0
  76. crew-builder/src/lib/stores/crewStore.ts +369 -0
  77. crew-builder/src/lib/stores/theme.svelte.js +145 -0
  78. crew-builder/src/lib/stores/toast.svelte.ts +69 -0
  79. crew-builder/src/lib/utils/conversation.ts +39 -0
  80. crew-builder/src/lib/utils/markdown.ts +122 -0
  81. crew-builder/src/lib/utils/talkHistory.ts +47 -0
  82. crew-builder/src/routes/+layout.svelte +20 -0
  83. crew-builder/src/routes/+page.svelte +539 -0
  84. crew-builder/src/routes/agents/+page.svelte +247 -0
  85. crew-builder/src/routes/agents/[agentId]/+page.svelte +288 -0
  86. crew-builder/src/routes/agents/[agentId]/+page.ts +7 -0
  87. crew-builder/src/routes/builder/+page.svelte +204 -0
  88. crew-builder/src/routes/crew/ask/+page.svelte +1052 -0
  89. crew-builder/src/routes/crew/ask/+page.ts +1 -0
  90. crew-builder/src/routes/integrations/o365/+page.svelte +304 -0
  91. crew-builder/src/routes/login/+page.svelte +197 -0
  92. crew-builder/src/routes/talk/[agentId]/+page.svelte +487 -0
  93. crew-builder/src/routes/talk/[agentId]/+page.ts +7 -0
  94. crew-builder/static/README.md +1 -0
  95. crew-builder/svelte.config.js +11 -0
  96. crew-builder/tailwind.config.ts +53 -0
  97. crew-builder/tsconfig.json +3 -0
  98. crew-builder/vite.config.ts +10 -0
  99. mcp_servers/calculator_server.py +309 -0
  100. parrot/__init__.py +27 -0
  101. parrot/__pycache__/__init__.cpython-310.pyc +0 -0
  102. parrot/__pycache__/version.cpython-310.pyc +0 -0
  103. parrot/_version.py +34 -0
  104. parrot/a2a/__init__.py +48 -0
  105. parrot/a2a/client.py +658 -0
  106. parrot/a2a/discovery.py +89 -0
  107. parrot/a2a/mixin.py +257 -0
  108. parrot/a2a/models.py +376 -0
  109. parrot/a2a/server.py +770 -0
  110. parrot/agents/__init__.py +29 -0
  111. parrot/bots/__init__.py +12 -0
  112. parrot/bots/a2a_agent.py +19 -0
  113. parrot/bots/abstract.py +3139 -0
  114. parrot/bots/agent.py +1129 -0
  115. parrot/bots/basic.py +9 -0
  116. parrot/bots/chatbot.py +669 -0
  117. parrot/bots/data.py +1618 -0
  118. parrot/bots/database/__init__.py +5 -0
  119. parrot/bots/database/abstract.py +3071 -0
  120. parrot/bots/database/cache.py +286 -0
  121. parrot/bots/database/models.py +468 -0
  122. parrot/bots/database/prompts.py +154 -0
  123. parrot/bots/database/retries.py +98 -0
  124. parrot/bots/database/router.py +269 -0
  125. parrot/bots/database/sql.py +41 -0
  126. parrot/bots/db/__init__.py +6 -0
  127. parrot/bots/db/abstract.py +556 -0
  128. parrot/bots/db/bigquery.py +602 -0
  129. parrot/bots/db/cache.py +85 -0
  130. parrot/bots/db/documentdb.py +668 -0
  131. parrot/bots/db/elastic.py +1014 -0
  132. parrot/bots/db/influx.py +898 -0
  133. parrot/bots/db/mock.py +96 -0
  134. parrot/bots/db/multi.py +783 -0
  135. parrot/bots/db/prompts.py +185 -0
  136. parrot/bots/db/sql.py +1255 -0
  137. parrot/bots/db/tools.py +212 -0
  138. parrot/bots/document.py +680 -0
  139. parrot/bots/hrbot.py +15 -0
  140. parrot/bots/kb.py +170 -0
  141. parrot/bots/mcp.py +36 -0
  142. parrot/bots/orchestration/README.md +463 -0
  143. parrot/bots/orchestration/__init__.py +1 -0
  144. parrot/bots/orchestration/agent.py +155 -0
  145. parrot/bots/orchestration/crew.py +3330 -0
  146. parrot/bots/orchestration/fsm.py +1179 -0
  147. parrot/bots/orchestration/hr.py +434 -0
  148. parrot/bots/orchestration/storage/__init__.py +4 -0
  149. parrot/bots/orchestration/storage/memory.py +100 -0
  150. parrot/bots/orchestration/storage/mixin.py +119 -0
  151. parrot/bots/orchestration/verify.py +202 -0
  152. parrot/bots/product.py +204 -0
  153. parrot/bots/prompts/__init__.py +96 -0
  154. parrot/bots/prompts/agents.py +155 -0
  155. parrot/bots/prompts/data.py +216 -0
  156. parrot/bots/prompts/output_generation.py +8 -0
  157. parrot/bots/scraper/__init__.py +3 -0
  158. parrot/bots/scraper/models.py +122 -0
  159. parrot/bots/scraper/scraper.py +1173 -0
  160. parrot/bots/scraper/templates.py +115 -0
  161. parrot/bots/stores/__init__.py +5 -0
  162. parrot/bots/stores/local.py +172 -0
  163. parrot/bots/webdev.py +81 -0
  164. parrot/cli.py +17 -0
  165. parrot/clients/__init__.py +16 -0
  166. parrot/clients/base.py +1491 -0
  167. parrot/clients/claude.py +1191 -0
  168. parrot/clients/factory.py +129 -0
  169. parrot/clients/google.py +4567 -0
  170. parrot/clients/gpt.py +1975 -0
  171. parrot/clients/grok.py +432 -0
  172. parrot/clients/groq.py +986 -0
  173. parrot/clients/hf.py +582 -0
  174. parrot/clients/models.py +18 -0
  175. parrot/conf.py +395 -0
  176. parrot/embeddings/__init__.py +9 -0
  177. parrot/embeddings/base.py +157 -0
  178. parrot/embeddings/google.py +98 -0
  179. parrot/embeddings/huggingface.py +74 -0
  180. parrot/embeddings/openai.py +84 -0
  181. parrot/embeddings/processor.py +88 -0
  182. parrot/exceptions.c +13868 -0
  183. parrot/exceptions.cpython-310-x86_64-linux-gnu.so +0 -0
  184. parrot/exceptions.pxd +22 -0
  185. parrot/exceptions.pxi +15 -0
  186. parrot/exceptions.pyx +44 -0
  187. parrot/generators/__init__.py +29 -0
  188. parrot/generators/base.py +200 -0
  189. parrot/generators/html.py +293 -0
  190. parrot/generators/react.py +205 -0
  191. parrot/generators/streamlit.py +203 -0
  192. parrot/generators/template.py +105 -0
  193. parrot/handlers/__init__.py +4 -0
  194. parrot/handlers/agent.py +861 -0
  195. parrot/handlers/agents/__init__.py +1 -0
  196. parrot/handlers/agents/abstract.py +900 -0
  197. parrot/handlers/bots.py +338 -0
  198. parrot/handlers/chat.py +915 -0
  199. parrot/handlers/creation.sql +192 -0
  200. parrot/handlers/crew/ARCHITECTURE.md +362 -0
  201. parrot/handlers/crew/README_BOTMANAGER_PERSISTENCE.md +303 -0
  202. parrot/handlers/crew/README_REDIS_PERSISTENCE.md +366 -0
  203. parrot/handlers/crew/__init__.py +0 -0
  204. parrot/handlers/crew/handler.py +801 -0
  205. parrot/handlers/crew/models.py +229 -0
  206. parrot/handlers/crew/redis_persistence.py +523 -0
  207. parrot/handlers/jobs/__init__.py +10 -0
  208. parrot/handlers/jobs/job.py +384 -0
  209. parrot/handlers/jobs/mixin.py +627 -0
  210. parrot/handlers/jobs/models.py +115 -0
  211. parrot/handlers/jobs/worker.py +31 -0
  212. parrot/handlers/models.py +596 -0
  213. parrot/handlers/o365_auth.py +105 -0
  214. parrot/handlers/stream.py +337 -0
  215. parrot/interfaces/__init__.py +6 -0
  216. parrot/interfaces/aws.py +143 -0
  217. parrot/interfaces/credentials.py +113 -0
  218. parrot/interfaces/database.py +27 -0
  219. parrot/interfaces/google.py +1123 -0
  220. parrot/interfaces/hierarchy.py +1227 -0
  221. parrot/interfaces/http.py +651 -0
  222. parrot/interfaces/images/__init__.py +0 -0
  223. parrot/interfaces/images/plugins/__init__.py +24 -0
  224. parrot/interfaces/images/plugins/abstract.py +58 -0
  225. parrot/interfaces/images/plugins/analisys.py +148 -0
  226. parrot/interfaces/images/plugins/classify.py +150 -0
  227. parrot/interfaces/images/plugins/classifybase.py +182 -0
  228. parrot/interfaces/images/plugins/detect.py +150 -0
  229. parrot/interfaces/images/plugins/exif.py +1103 -0
  230. parrot/interfaces/images/plugins/hash.py +52 -0
  231. parrot/interfaces/images/plugins/vision.py +104 -0
  232. parrot/interfaces/images/plugins/yolo.py +66 -0
  233. parrot/interfaces/images/plugins/zerodetect.py +197 -0
  234. parrot/interfaces/o365.py +978 -0
  235. parrot/interfaces/onedrive.py +822 -0
  236. parrot/interfaces/sharepoint.py +1435 -0
  237. parrot/interfaces/soap.py +257 -0
  238. parrot/loaders/__init__.py +8 -0
  239. parrot/loaders/abstract.py +1131 -0
  240. parrot/loaders/audio.py +199 -0
  241. parrot/loaders/basepdf.py +53 -0
  242. parrot/loaders/basevideo.py +1568 -0
  243. parrot/loaders/csv.py +409 -0
  244. parrot/loaders/docx.py +116 -0
  245. parrot/loaders/epubloader.py +316 -0
  246. parrot/loaders/excel.py +199 -0
  247. parrot/loaders/factory.py +55 -0
  248. parrot/loaders/files/__init__.py +0 -0
  249. parrot/loaders/files/abstract.py +39 -0
  250. parrot/loaders/files/html.py +26 -0
  251. parrot/loaders/files/text.py +63 -0
  252. parrot/loaders/html.py +152 -0
  253. parrot/loaders/markdown.py +442 -0
  254. parrot/loaders/pdf.py +373 -0
  255. parrot/loaders/pdfmark.py +320 -0
  256. parrot/loaders/pdftables.py +506 -0
  257. parrot/loaders/ppt.py +476 -0
  258. parrot/loaders/qa.py +63 -0
  259. parrot/loaders/splitters/__init__.py +10 -0
  260. parrot/loaders/splitters/base.py +138 -0
  261. parrot/loaders/splitters/md.py +228 -0
  262. parrot/loaders/splitters/token.py +143 -0
  263. parrot/loaders/txt.py +26 -0
  264. parrot/loaders/video.py +89 -0
  265. parrot/loaders/videolocal.py +218 -0
  266. parrot/loaders/videounderstanding.py +377 -0
  267. parrot/loaders/vimeo.py +167 -0
  268. parrot/loaders/web.py +599 -0
  269. parrot/loaders/youtube.py +504 -0
  270. parrot/manager/__init__.py +5 -0
  271. parrot/manager/manager.py +1030 -0
  272. parrot/mcp/__init__.py +28 -0
  273. parrot/mcp/adapter.py +105 -0
  274. parrot/mcp/cli.py +174 -0
  275. parrot/mcp/client.py +119 -0
  276. parrot/mcp/config.py +75 -0
  277. parrot/mcp/integration.py +842 -0
  278. parrot/mcp/oauth.py +933 -0
  279. parrot/mcp/server.py +225 -0
  280. parrot/mcp/transports/__init__.py +3 -0
  281. parrot/mcp/transports/base.py +279 -0
  282. parrot/mcp/transports/grpc_session.py +163 -0
  283. parrot/mcp/transports/http.py +312 -0
  284. parrot/mcp/transports/mcp.proto +108 -0
  285. parrot/mcp/transports/quic.py +1082 -0
  286. parrot/mcp/transports/sse.py +330 -0
  287. parrot/mcp/transports/stdio.py +309 -0
  288. parrot/mcp/transports/unix.py +395 -0
  289. parrot/mcp/transports/websocket.py +547 -0
  290. parrot/memory/__init__.py +16 -0
  291. parrot/memory/abstract.py +209 -0
  292. parrot/memory/agent.py +32 -0
  293. parrot/memory/cache.py +175 -0
  294. parrot/memory/core.py +555 -0
  295. parrot/memory/file.py +153 -0
  296. parrot/memory/mem.py +131 -0
  297. parrot/memory/redis.py +613 -0
  298. parrot/models/__init__.py +46 -0
  299. parrot/models/basic.py +118 -0
  300. parrot/models/compliance.py +208 -0
  301. parrot/models/crew.py +395 -0
  302. parrot/models/detections.py +654 -0
  303. parrot/models/generation.py +85 -0
  304. parrot/models/google.py +223 -0
  305. parrot/models/groq.py +23 -0
  306. parrot/models/openai.py +30 -0
  307. parrot/models/outputs.py +285 -0
  308. parrot/models/responses.py +938 -0
  309. parrot/notifications/__init__.py +743 -0
  310. parrot/openapi/__init__.py +3 -0
  311. parrot/openapi/components.yaml +641 -0
  312. parrot/openapi/config.py +322 -0
  313. parrot/outputs/__init__.py +32 -0
  314. parrot/outputs/formats/__init__.py +108 -0
  315. parrot/outputs/formats/altair.py +359 -0
  316. parrot/outputs/formats/application.py +122 -0
  317. parrot/outputs/formats/base.py +351 -0
  318. parrot/outputs/formats/bokeh.py +356 -0
  319. parrot/outputs/formats/card.py +424 -0
  320. parrot/outputs/formats/chart.py +436 -0
  321. parrot/outputs/formats/d3.py +255 -0
  322. parrot/outputs/formats/echarts.py +310 -0
  323. parrot/outputs/formats/generators/__init__.py +0 -0
  324. parrot/outputs/formats/generators/abstract.py +61 -0
  325. parrot/outputs/formats/generators/panel.py +145 -0
  326. parrot/outputs/formats/generators/streamlit.py +86 -0
  327. parrot/outputs/formats/generators/terminal.py +63 -0
  328. parrot/outputs/formats/holoviews.py +310 -0
  329. parrot/outputs/formats/html.py +147 -0
  330. parrot/outputs/formats/jinja2.py +46 -0
  331. parrot/outputs/formats/json.py +87 -0
  332. parrot/outputs/formats/map.py +933 -0
  333. parrot/outputs/formats/markdown.py +172 -0
  334. parrot/outputs/formats/matplotlib.py +237 -0
  335. parrot/outputs/formats/mixins/__init__.py +0 -0
  336. parrot/outputs/formats/mixins/emaps.py +855 -0
  337. parrot/outputs/formats/plotly.py +341 -0
  338. parrot/outputs/formats/seaborn.py +310 -0
  339. parrot/outputs/formats/table.py +397 -0
  340. parrot/outputs/formats/template_report.py +138 -0
  341. parrot/outputs/formats/yaml.py +125 -0
  342. parrot/outputs/formatter.py +152 -0
  343. parrot/outputs/templates/__init__.py +95 -0
  344. parrot/pipelines/__init__.py +0 -0
  345. parrot/pipelines/abstract.py +210 -0
  346. parrot/pipelines/detector.py +124 -0
  347. parrot/pipelines/models.py +90 -0
  348. parrot/pipelines/planogram.py +3002 -0
  349. parrot/pipelines/table.sql +97 -0
  350. parrot/plugins/__init__.py +106 -0
  351. parrot/plugins/importer.py +80 -0
  352. parrot/py.typed +0 -0
  353. parrot/registry/__init__.py +18 -0
  354. parrot/registry/registry.py +594 -0
  355. parrot/scheduler/__init__.py +1189 -0
  356. parrot/scheduler/models.py +60 -0
  357. parrot/security/__init__.py +16 -0
  358. parrot/security/prompt_injection.py +268 -0
  359. parrot/security/security_events.sql +25 -0
  360. parrot/services/__init__.py +1 -0
  361. parrot/services/mcp/__init__.py +8 -0
  362. parrot/services/mcp/config.py +13 -0
  363. parrot/services/mcp/server.py +295 -0
  364. parrot/services/o365_remote_auth.py +235 -0
  365. parrot/stores/__init__.py +7 -0
  366. parrot/stores/abstract.py +352 -0
  367. parrot/stores/arango.py +1090 -0
  368. parrot/stores/bigquery.py +1377 -0
  369. parrot/stores/cache.py +106 -0
  370. parrot/stores/empty.py +10 -0
  371. parrot/stores/faiss_store.py +1157 -0
  372. parrot/stores/kb/__init__.py +9 -0
  373. parrot/stores/kb/abstract.py +68 -0
  374. parrot/stores/kb/cache.py +165 -0
  375. parrot/stores/kb/doc.py +325 -0
  376. parrot/stores/kb/hierarchy.py +346 -0
  377. parrot/stores/kb/local.py +457 -0
  378. parrot/stores/kb/prompt.py +28 -0
  379. parrot/stores/kb/redis.py +659 -0
  380. parrot/stores/kb/store.py +115 -0
  381. parrot/stores/kb/user.py +374 -0
  382. parrot/stores/models.py +59 -0
  383. parrot/stores/pgvector.py +3 -0
  384. parrot/stores/postgres.py +2853 -0
  385. parrot/stores/utils/__init__.py +0 -0
  386. parrot/stores/utils/chunking.py +197 -0
  387. parrot/telemetry/__init__.py +3 -0
  388. parrot/telemetry/mixin.py +111 -0
  389. parrot/template/__init__.py +3 -0
  390. parrot/template/engine.py +259 -0
  391. parrot/tools/__init__.py +23 -0
  392. parrot/tools/abstract.py +644 -0
  393. parrot/tools/agent.py +363 -0
  394. parrot/tools/arangodbsearch.py +537 -0
  395. parrot/tools/arxiv_tool.py +188 -0
  396. parrot/tools/calculator/__init__.py +3 -0
  397. parrot/tools/calculator/operations/__init__.py +38 -0
  398. parrot/tools/calculator/operations/calculus.py +80 -0
  399. parrot/tools/calculator/operations/statistics.py +76 -0
  400. parrot/tools/calculator/tool.py +150 -0
  401. parrot/tools/cloudwatch.py +988 -0
  402. parrot/tools/codeinterpreter/__init__.py +127 -0
  403. parrot/tools/codeinterpreter/executor.py +371 -0
  404. parrot/tools/codeinterpreter/internals.py +473 -0
  405. parrot/tools/codeinterpreter/models.py +643 -0
  406. parrot/tools/codeinterpreter/prompts.py +224 -0
  407. parrot/tools/codeinterpreter/tool.py +664 -0
  408. parrot/tools/company_info/__init__.py +6 -0
  409. parrot/tools/company_info/tool.py +1138 -0
  410. parrot/tools/correlationanalysis.py +437 -0
  411. parrot/tools/database/abstract.py +286 -0
  412. parrot/tools/database/bq.py +115 -0
  413. parrot/tools/database/cache.py +284 -0
  414. parrot/tools/database/models.py +95 -0
  415. parrot/tools/database/pg.py +343 -0
  416. parrot/tools/databasequery.py +1159 -0
  417. parrot/tools/db.py +1800 -0
  418. parrot/tools/ddgo.py +370 -0
  419. parrot/tools/decorators.py +271 -0
  420. parrot/tools/dftohtml.py +282 -0
  421. parrot/tools/document.py +549 -0
  422. parrot/tools/ecs.py +819 -0
  423. parrot/tools/edareport.py +368 -0
  424. parrot/tools/elasticsearch.py +1049 -0
  425. parrot/tools/employees.py +462 -0
  426. parrot/tools/epson/__init__.py +96 -0
  427. parrot/tools/excel.py +683 -0
  428. parrot/tools/file/__init__.py +13 -0
  429. parrot/tools/file/abstract.py +76 -0
  430. parrot/tools/file/gcs.py +378 -0
  431. parrot/tools/file/local.py +284 -0
  432. parrot/tools/file/s3.py +511 -0
  433. parrot/tools/file/tmp.py +309 -0
  434. parrot/tools/file/tool.py +501 -0
  435. parrot/tools/file_reader.py +129 -0
  436. parrot/tools/flowtask/__init__.py +19 -0
  437. parrot/tools/flowtask/tool.py +761 -0
  438. parrot/tools/gittoolkit.py +508 -0
  439. parrot/tools/google/__init__.py +18 -0
  440. parrot/tools/google/base.py +169 -0
  441. parrot/tools/google/tools.py +1251 -0
  442. parrot/tools/googlelocation.py +5 -0
  443. parrot/tools/googleroutes.py +5 -0
  444. parrot/tools/googlesearch.py +5 -0
  445. parrot/tools/googlesitesearch.py +5 -0
  446. parrot/tools/googlevoice.py +2 -0
  447. parrot/tools/gvoice.py +695 -0
  448. parrot/tools/ibisworld/README.md +225 -0
  449. parrot/tools/ibisworld/__init__.py +11 -0
  450. parrot/tools/ibisworld/tool.py +366 -0
  451. parrot/tools/jiratoolkit.py +1718 -0
  452. parrot/tools/manager.py +1098 -0
  453. parrot/tools/math.py +152 -0
  454. parrot/tools/metadata.py +476 -0
  455. parrot/tools/msteams.py +1621 -0
  456. parrot/tools/msword.py +635 -0
  457. parrot/tools/multidb.py +580 -0
  458. parrot/tools/multistoresearch.py +369 -0
  459. parrot/tools/networkninja.py +167 -0
  460. parrot/tools/nextstop/__init__.py +4 -0
  461. parrot/tools/nextstop/base.py +286 -0
  462. parrot/tools/nextstop/employee.py +733 -0
  463. parrot/tools/nextstop/store.py +462 -0
  464. parrot/tools/notification.py +435 -0
  465. parrot/tools/o365/__init__.py +42 -0
  466. parrot/tools/o365/base.py +295 -0
  467. parrot/tools/o365/bundle.py +522 -0
  468. parrot/tools/o365/events.py +554 -0
  469. parrot/tools/o365/mail.py +992 -0
  470. parrot/tools/o365/onedrive.py +497 -0
  471. parrot/tools/o365/sharepoint.py +641 -0
  472. parrot/tools/openapi_toolkit.py +904 -0
  473. parrot/tools/openweather.py +527 -0
  474. parrot/tools/pdfprint.py +1001 -0
  475. parrot/tools/powerbi.py +518 -0
  476. parrot/tools/powerpoint.py +1113 -0
  477. parrot/tools/pricestool.py +146 -0
  478. parrot/tools/products/__init__.py +246 -0
  479. parrot/tools/prophet_tool.py +171 -0
  480. parrot/tools/pythonpandas.py +630 -0
  481. parrot/tools/pythonrepl.py +910 -0
  482. parrot/tools/qsource.py +436 -0
  483. parrot/tools/querytoolkit.py +395 -0
  484. parrot/tools/quickeda.py +827 -0
  485. parrot/tools/resttool.py +553 -0
  486. parrot/tools/retail/__init__.py +0 -0
  487. parrot/tools/retail/bby.py +528 -0
  488. parrot/tools/sandboxtool.py +703 -0
  489. parrot/tools/sassie/__init__.py +352 -0
  490. parrot/tools/scraping/__init__.py +7 -0
  491. parrot/tools/scraping/docs/select.md +466 -0
  492. parrot/tools/scraping/documentation.md +1278 -0
  493. parrot/tools/scraping/driver.py +436 -0
  494. parrot/tools/scraping/models.py +576 -0
  495. parrot/tools/scraping/options.py +85 -0
  496. parrot/tools/scraping/orchestrator.py +517 -0
  497. parrot/tools/scraping/readme.md +740 -0
  498. parrot/tools/scraping/tool.py +3115 -0
  499. parrot/tools/seasonaldetection.py +642 -0
  500. parrot/tools/shell_tool/__init__.py +5 -0
  501. parrot/tools/shell_tool/actions.py +408 -0
  502. parrot/tools/shell_tool/engine.py +155 -0
  503. parrot/tools/shell_tool/models.py +322 -0
  504. parrot/tools/shell_tool/tool.py +442 -0
  505. parrot/tools/site_search.py +214 -0
  506. parrot/tools/textfile.py +418 -0
  507. parrot/tools/think.py +378 -0
  508. parrot/tools/toolkit.py +298 -0
  509. parrot/tools/webapp_tool.py +187 -0
  510. parrot/tools/whatif.py +1279 -0
  511. parrot/tools/workday/MULTI_WSDL_EXAMPLE.md +249 -0
  512. parrot/tools/workday/__init__.py +6 -0
  513. parrot/tools/workday/models.py +1389 -0
  514. parrot/tools/workday/tool.py +1293 -0
  515. parrot/tools/yfinance_tool.py +306 -0
  516. parrot/tools/zipcode.py +217 -0
  517. parrot/utils/__init__.py +2 -0
  518. parrot/utils/helpers.py +73 -0
  519. parrot/utils/parsers/__init__.py +5 -0
  520. parrot/utils/parsers/toml.c +12078 -0
  521. parrot/utils/parsers/toml.cpython-310-x86_64-linux-gnu.so +0 -0
  522. parrot/utils/parsers/toml.pyx +21 -0
  523. parrot/utils/toml.py +11 -0
  524. parrot/utils/types.cpp +20936 -0
  525. parrot/utils/types.cpython-310-x86_64-linux-gnu.so +0 -0
  526. parrot/utils/types.pyx +213 -0
  527. parrot/utils/uv.py +11 -0
  528. parrot/version.py +10 -0
  529. parrot/yaml-rs/Cargo.lock +350 -0
  530. parrot/yaml-rs/Cargo.toml +19 -0
  531. parrot/yaml-rs/pyproject.toml +19 -0
  532. parrot/yaml-rs/python/yaml_rs/__init__.py +81 -0
  533. parrot/yaml-rs/src/lib.rs +222 -0
  534. requirements/docker-compose.yml +24 -0
  535. requirements/requirements-dev.txt +21 -0
@@ -0,0 +1,1189 @@
1
+ """
2
+ Agent Scheduler Module for AI-Parrot.
3
+
4
+ This module provides scheduling capabilities for agents using APScheduler,
5
+ allowing agents to execute operations at specified intervals.
6
+ """
7
+ import asyncio
8
+ import contextlib
9
+ import inspect
10
+ import json
11
+ from typing import Any, Dict, Optional, Callable, List, Tuple, Set
12
+ from datetime import datetime
13
+ import uuid
14
+ from enum import Enum
15
+ from functools import wraps
16
+ from aiohttp import web
17
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
18
+ from apscheduler.jobstores.memory import MemoryJobStore
19
+ from apscheduler.jobstores.redis import RedisJobStore
20
+ from apscheduler.executors.asyncio import AsyncIOExecutor
21
+ from apscheduler.triggers.cron import CronTrigger
22
+ from apscheduler.triggers.interval import IntervalTrigger
23
+ from apscheduler.triggers.date import DateTrigger
24
+ from apscheduler.events import (
25
+ EVENT_JOB_ADDED,
26
+ EVENT_JOB_ERROR,
27
+ EVENT_JOB_EXECUTED,
28
+ EVENT_JOB_MAX_INSTANCES,
29
+ EVENT_JOB_MISSED,
30
+ EVENT_JOB_SUBMITTED,
31
+ EVENT_SCHEDULER_SHUTDOWN,
32
+ EVENT_SCHEDULER_STARTED,
33
+ JobExecutionEvent,
34
+ )
35
+ from apscheduler.jobstores.base import JobLookupError
36
+ from navconfig.logging import logging
37
+ from asyncdb import AsyncDB
38
+ from navigator.conf import CACHE_HOST, CACHE_PORT
39
+ from navigator.connections import PostgresPool
40
+ from querysource.conf import default_dsn
41
+ from .models import AgentSchedule
42
+ from ..notifications import NotificationMixin
43
+ from ..conf import ENVIRONMENT
44
+
45
+
46
+ # disable logging of APScheduler
47
+ logging.getLogger("apscheduler").setLevel(logging.WARNING)
48
+
49
+
50
+ # Database Model for Scheduler
51
+ class ScheduleType(Enum):
52
+ """Schedule execution types."""
53
+ ONCE = "once"
54
+ DAILY = "daily"
55
+ WEEKLY = "weekly"
56
+ MONTHLY = "monthly"
57
+ INTERVAL = "interval"
58
+ CRON = "cron"
59
+ CRONTAB = "crontab" # using crontab-syntax (supported by APScheduler)
60
+
61
+
62
+ # Decorator for scheduling agent methods
63
+ def schedule(
64
+ schedule_type: ScheduleType = ScheduleType.DAILY,
65
+ **schedule_config
66
+ ):
67
+ """
68
+ Decorator to mark agent methods for scheduling.
69
+
70
+ Usage:
71
+ @schedule(schedule_type=ScheduleType.DAILY, hour=9, minute=0)
72
+ async def generate_daily_report(self):
73
+ ...
74
+
75
+ @schedule(schedule_type=ScheduleType.INTERVAL, hours=2)
76
+ async def check_updates(self):
77
+ ...
78
+ """
79
+ def decorator(func: Callable) -> Callable:
80
+ @wraps(func)
81
+ async def wrapper(*args, **kwargs):
82
+ return await func(*args, **kwargs)
83
+
84
+ # Add scheduling metadata to the function
85
+ wrapper._schedule_config = {
86
+ 'schedule_type': schedule_type.value,
87
+ 'schedule_config': schedule_config,
88
+ 'method_name': func.__name__
89
+ }
90
+ return wrapper
91
+ return decorator
92
+
93
+
94
+ class _SchedulerNotification(NotificationMixin):
95
+ """Helper to reuse notification mixin capabilities."""
96
+
97
+ def __init__(self, logger):
98
+ self.logger = logger
99
+
100
+
101
+ class AgentSchedulerManager:
102
+ """
103
+ Manager for scheduling agent operations using APScheduler.
104
+
105
+ This manager handles:
106
+ - Loading schedules from database on startup
107
+ - Adding/removing schedules dynamically
108
+ - Executing scheduled agent operations
109
+ - Safe restart of scheduler
110
+ """
111
+
112
+ def __init__(self, bot_manager=None):
113
+ self.logger = logging.getLogger('Parrot.Scheduler')
114
+ self.bot_manager = bot_manager
115
+ self.app: Optional[web.Application] = None
116
+ self.db: Optional[AsyncDB] = None
117
+ self._pool: Optional[AsyncDB] = None # Database connection pool
118
+ self._job_context: Dict[str, Dict[str, Any]] = {}
119
+ self._pending_success_tasks: Set[asyncio.Task] = set()
120
+
121
+ # Configure APScheduler with AsyncIO
122
+ jobstores = {
123
+ 'default': MemoryJobStore(),
124
+ "redis": RedisJobStore(
125
+ db=6,
126
+ jobs_key="apscheduler.jobs",
127
+ run_times_key="apscheduler.run_times",
128
+ host=CACHE_HOST,
129
+ port=CACHE_PORT,
130
+ ),
131
+ }
132
+ executors = {
133
+ 'default': AsyncIOExecutor()
134
+ }
135
+ job_defaults = {
136
+ 'coalesce': True, # Combine multiple missed runs into one
137
+ 'max_instances': 2, # Maximum concurrent instances of each job
138
+ 'misfire_grace_time': 300 # 5 minutes grace period
139
+ }
140
+
141
+ self.scheduler = AsyncIOScheduler(
142
+ jobstores=jobstores,
143
+ executors=executors,
144
+ job_defaults=job_defaults,
145
+ timezone='UTC'
146
+ )
147
+
148
+ def _prepare_call_arguments(
149
+ self,
150
+ method: Callable,
151
+ prompt: Optional[Any],
152
+ metadata: Optional[Dict[str, Any]],
153
+ *,
154
+ is_crew: bool,
155
+ method_name: Optional[str]
156
+ ) -> Tuple[List[Any], Dict[str, Any]]:
157
+ """Build positional and keyword arguments for method execution."""
158
+ call_kwargs: Dict[str, Any] = dict(metadata or {})
159
+ call_args: List[Any] = []
160
+
161
+ if prompt is None:
162
+ return call_args, call_kwargs
163
+
164
+ assigned_prompt = False
165
+
166
+ if is_crew:
167
+ crew_prompt_map = {
168
+ 'run_flow': 'initial_task',
169
+ 'run_loop': 'initial_task',
170
+ 'run_sequential': 'query',
171
+ 'run_parallel': 'tasks',
172
+ }
173
+ if (param_name := crew_prompt_map.get(method_name or '')):
174
+ if param_name == 'tasks':
175
+ if param_name not in call_kwargs and isinstance(prompt, list):
176
+ call_kwargs[param_name] = prompt
177
+ assigned_prompt = True
178
+ elif param_name not in call_kwargs:
179
+ call_kwargs[param_name] = prompt
180
+ assigned_prompt = True
181
+
182
+ if not assigned_prompt:
183
+ call_args, call_kwargs = self._apply_prompt_signature(
184
+ method,
185
+ call_args,
186
+ call_kwargs,
187
+ prompt
188
+ )
189
+
190
+ return call_args, call_kwargs
191
+
192
+ def _apply_prompt_signature(
193
+ self,
194
+ method: Callable,
195
+ call_args: List[Any],
196
+ call_kwargs: Dict[str, Any],
197
+ prompt: Any
198
+ ) -> Tuple[List[Any], Dict[str, Any]]:
199
+ """Inject prompt into call signature when possible."""
200
+ try:
201
+ signature = inspect.signature(method)
202
+ except (TypeError, ValueError):
203
+ return call_args, call_kwargs
204
+
205
+ positional_params = [
206
+ param
207
+ for param in signature.parameters.values()
208
+ if param.kind in (
209
+ inspect.Parameter.POSITIONAL_ONLY,
210
+ inspect.Parameter.POSITIONAL_OR_KEYWORD
211
+ )
212
+ ]
213
+
214
+ if positional_params:
215
+ first_param = positional_params[0]
216
+ call_kwargs.setdefault(first_param.name, prompt)
217
+ return call_args, call_kwargs
218
+
219
+ if any(
220
+ param.kind == inspect.Parameter.VAR_POSITIONAL
221
+ for param in signature.parameters.values()
222
+ ):
223
+ call_args.append(prompt)
224
+ return call_args, call_kwargs
225
+
226
+ if any(
227
+ param.kind == inspect.Parameter.VAR_KEYWORD
228
+ for param in signature.parameters.values()
229
+ ):
230
+ call_kwargs.setdefault('prompt', prompt)
231
+
232
+ return call_args, call_kwargs
233
+
234
+ def define_listeners(self):
235
+ # Asyncio Scheduler
236
+ self.scheduler.add_listener(
237
+ self.scheduler_status,
238
+ EVENT_SCHEDULER_STARTED
239
+ )
240
+ self.scheduler.add_listener(
241
+ self.scheduler_shutdown,
242
+ EVENT_SCHEDULER_SHUTDOWN
243
+ )
244
+ self.scheduler.add_listener(self.job_success, EVENT_JOB_EXECUTED)
245
+ self.scheduler.add_listener(self.job_status, EVENT_JOB_ERROR | EVENT_JOB_MISSED)
246
+ # a new job was added:
247
+ self.scheduler.add_listener(self.job_added, EVENT_JOB_ADDED)
248
+
249
+ def scheduler_status(self, event):
250
+ print(event)
251
+ self.logger.debug(f"[{ENVIRONMENT} - NAV Scheduler] :: Started.")
252
+ self.logger.notice(
253
+ f"[{ENVIRONMENT} - NAV Scheduler] START time is: {datetime.now()}"
254
+ )
255
+
256
+ def scheduler_shutdown(self, event):
257
+ self.logger.notice(
258
+ f"[{ENVIRONMENT}] Scheduler {event} Stopped at: {datetime.now()}"
259
+ )
260
+
261
+ def job_added(self, event: JobExecutionEvent, *args, **kwargs):
262
+ with contextlib.suppress(Exception):
263
+ job = self.scheduler.get_job(event.job_id)
264
+ job_name = job.name
265
+ # TODO: using to check if tasks were added
266
+ self.logger.info(
267
+ f"Job Added: {job_name} with args: {args!s}/{kwargs!r}"
268
+ )
269
+
270
+ def job_status(self, event: JobExecutionEvent):
271
+ """React on Error events from scheduler.
272
+
273
+ :param apscheduler.events.JobExecutionEvent event: job execution event.
274
+
275
+ TODO: add the reschedule_job
276
+ scheduler = sched.scheduler #it returns the native apscheduler instance
277
+ scheduler.reschedule_job('my_job_id', trigger='cron', minute='*/5')
278
+
279
+ """
280
+ job_id = event.job_id
281
+ self._job_context.pop(str(job_id), None)
282
+ job = self.scheduler.get_job(job_id)
283
+ job_name = job.name
284
+ scheduled = event.scheduled_run_time
285
+ stack = event.traceback
286
+ if event.code == EVENT_JOB_MISSED:
287
+ self.logger.warning(
288
+ f"[{ENVIRONMENT} - NAV Scheduler] Job {job_name} \
289
+ was missed for scheduled run at {scheduled}"
290
+ )
291
+ message = f"⚠️ :: [{ENVIRONMENT} - NAV Scheduler] Job {job_name} was missed \
292
+ for scheduled run at {scheduled}"
293
+ elif event.code == EVENT_JOB_ERROR:
294
+ self.logger.error(
295
+ f"[{ENVIRONMENT} - NAV Scheduler] Job {job_name} scheduled at \
296
+ {scheduled!s} failed with Exception: {event.exception!s}"
297
+ )
298
+ message = f"🛑 :: [{ENVIRONMENT} - NAV Scheduler] Job **{job_name}** \
299
+ scheduled at {scheduled!s} failed with Error {event.exception!s}"
300
+ if stack:
301
+ self.logger.exception(
302
+ f"[{ENVIRONMENT} - NAV Scheduler] Job {job_name} id: {job_id!s} \
303
+ StackTrace: {stack!s}"
304
+ )
305
+ message = f"🛑 :: [{ENVIRONMENT} - NAV Scheduler] Job \
306
+ **{job_name}**:**{job_id!s}** failed with Exception {event.exception!s}"
307
+ # send a Notification error from Scheduler
308
+ elif event.code == EVENT_JOB_MAX_INSTANCES:
309
+ self.logger.exception(
310
+ f"[{ENVIRONMENT} - Scheduler] Job {job_name} could not be submitted \
311
+ Maximum number of running instances was reached."
312
+ )
313
+ message = f"⚠️ :: [{ENVIRONMENT} - NAV Scheduler] Job **{job_name}** was \
314
+ missed for scheduled run at {scheduled}"
315
+ else:
316
+ # will be an exception
317
+ message = f"🛑 :: [{ENVIRONMENT} - NAV Scheduler] Job \
318
+ {job_name}:{job_id!s} failed with Exception {stack!s}"
319
+ # send a Notification Exception from Scheduler
320
+ # self._send_notification(message)
321
+
322
+ def job_success(self, event: JobExecutionEvent):
323
+ """Job Success.
324
+
325
+ Event when a Job was executed successfully.
326
+
327
+ :param apscheduler.events.JobExecutionEvent event: job execution event
328
+ """
329
+ job_id = event.job_id
330
+ try:
331
+ job = self.scheduler.get_job(job_id)
332
+ except JobLookupError as err:
333
+ self.logger.warning(f"Error found a Job with ID: {err}")
334
+ return False
335
+ job_name = job.name
336
+ self.logger.info(
337
+ f"[Scheduler - {ENVIRONMENT}]: {job_name} with id {event.job_id!s} \
338
+ was queued/executed successfully @ {event.scheduled_run_time!s}"
339
+ )
340
+
341
+ job_kwargs = getattr(job, "kwargs", {}) or {}
342
+ schedule_id = str(job_kwargs.get('schedule_id', event.job_id))
343
+ context = self._job_context.pop(schedule_id, {})
344
+
345
+ if 'agent_name' in context:
346
+ agent_name = context['agent_name']
347
+ else:
348
+ agent_name = job_kwargs.get('agent_name', job_name)
349
+
350
+ if 'success_callback' in context:
351
+ success_callback = context['success_callback']
352
+ else:
353
+ success_callback = job_kwargs.get('success_callback')
354
+
355
+ if 'send_result' in context:
356
+ send_result = context['send_result']
357
+ else:
358
+ send_result = job_kwargs.get('send_result')
359
+
360
+ result = getattr(event, 'retval', None)
361
+
362
+ if not schedule_id:
363
+ self.logger.debug(
364
+ "Job %s executed successfully but no schedule_id was found in context",
365
+ job_id,
366
+ )
367
+ return True
368
+
369
+ task = asyncio.create_task(
370
+ self._process_job_success(
371
+ schedule_id,
372
+ agent_name,
373
+ result,
374
+ success_callback,
375
+ send_result if isinstance(send_result, dict) else send_result,
376
+ )
377
+ )
378
+ self._pending_success_tasks.add(task)
379
+ task.add_done_callback(self._pending_success_tasks.discard)
380
+ return True
381
+
382
+ async def _execute_agent_job(
383
+ self,
384
+ schedule_id: str,
385
+ agent_name: str,
386
+ prompt: Optional[str] = None,
387
+ method_name: Optional[str] = None,
388
+ metadata: Optional[Dict] = None,
389
+ *,
390
+ is_crew: bool = False,
391
+ success_callback: Optional[Callable] = None,
392
+ send_result: Optional[Dict[str, Any]] = None
393
+ ):
394
+ """
395
+ Execute a scheduled agent operation.
396
+
397
+ Args:
398
+ schedule_id: Unique identifier for this schedule
399
+ agent_name: Name of the agent to execute
400
+ prompt: Optional prompt to send to the agent
401
+ method_name: Optional public method to call on the agent
402
+ metadata: Additional metadata for execution context
403
+ """
404
+ try:
405
+ self.logger.info(
406
+ f"Executing scheduled job {schedule_id} for agent {agent_name}"
407
+ )
408
+
409
+ if not self.bot_manager:
410
+ raise RuntimeError("Bot manager not available")
411
+
412
+ call_metadata: Dict[str, Any] = dict(metadata or {})
413
+
414
+ metadata_send_result = call_metadata.pop('send_result', None)
415
+ send_result_config = (
416
+ send_result
417
+ if send_result is not None
418
+ else metadata_send_result
419
+ )
420
+
421
+ metadata_success_callback = call_metadata.pop('success_callback', None)
422
+ if success_callback is None and callable(metadata_success_callback):
423
+ success_callback = metadata_success_callback
424
+
425
+ metadata_is_crew = call_metadata.pop('is_crew', None)
426
+ if metadata_is_crew is not None:
427
+ is_crew = bool(is_crew or metadata_is_crew)
428
+
429
+ agent: Any = None
430
+ if is_crew:
431
+ if (crew_entry := self.bot_manager.get_crew(agent_name)):
432
+ agent = crew_entry[0]
433
+ else:
434
+ raise ValueError(f"Crew {agent_name} not found")
435
+ elif not (agent := self.bot_manager._bots.get(agent_name)):
436
+ agent = await self.bot_manager.registry.get_instance(agent_name)
437
+ if not agent:
438
+ raise ValueError(
439
+ f"Agent {agent_name} not found"
440
+ )
441
+
442
+ if method_name:
443
+ if not hasattr(agent, method_name):
444
+ raise AttributeError(
445
+ f"Agent {agent_name} has no method {method_name}"
446
+ )
447
+ method = getattr(agent, method_name)
448
+ if not callable(method):
449
+ raise TypeError(f"{method_name} is not callable")
450
+
451
+ call_args, call_kwargs = self._prepare_call_arguments(
452
+ method,
453
+ prompt,
454
+ call_metadata,
455
+ is_crew=is_crew,
456
+ method_name=method_name,
457
+ )
458
+ result = await method(*call_args, **call_kwargs)
459
+ elif prompt is not None:
460
+ result = await agent.chat(prompt)
461
+ else:
462
+ raise ValueError(
463
+ "Either prompt or method_name must be provided"
464
+ )
465
+
466
+ send_result_payload = (
467
+ dict(send_result_config)
468
+ if isinstance(send_result_config, dict)
469
+ else send_result_config
470
+ )
471
+
472
+ self._job_context[str(schedule_id)] = {
473
+ 'schedule_id': str(schedule_id),
474
+ 'agent_name': agent_name,
475
+ 'success_callback': success_callback,
476
+ 'send_result': send_result_payload,
477
+ }
478
+
479
+ self.logger.info(
480
+ f"Successfully executed job {schedule_id} for agent {agent_name}"
481
+ )
482
+
483
+ return result
484
+
485
+ except Exception as e:
486
+ self.logger.error(
487
+ f"Error executing scheduled job {schedule_id}: {e}",
488
+ exc_info=True
489
+ )
490
+ self._job_context.pop(str(schedule_id), None)
491
+ await self._update_schedule_run(schedule_id, success=False, error=str(e))
492
+ raise
493
+
494
+ async def _handle_job_success(
495
+ self,
496
+ schedule_id: str,
497
+ agent_name: str,
498
+ result: Any,
499
+ success_callback: Optional[Callable],
500
+ send_result: Optional[Dict[str, Any]],
501
+ ) -> None:
502
+ """Execute success callback or fallback notification."""
503
+ if success_callback:
504
+ callback_result = success_callback(result)
505
+ if inspect.isawaitable(callback_result):
506
+ await callback_result
507
+ return
508
+
509
+ if not send_result:
510
+ return
511
+
512
+ await self._send_result_email(schedule_id, agent_name, result, send_result)
513
+
514
+ async def _send_result_email(
515
+ self,
516
+ schedule_id: str,
517
+ agent_name: str,
518
+ result: Any,
519
+ send_result: Dict[str, Any],
520
+ ) -> None:
521
+ """Send job result via email using the notification system."""
522
+ if not isinstance(send_result, dict):
523
+ self.logger.warning(
524
+ "send_result configuration for schedule %s is not a dictionary", schedule_id
525
+ )
526
+ return
527
+
528
+ recipients = (
529
+ send_result.get('recipients')
530
+ or send_result.get('emails')
531
+ or send_result.get('email')
532
+ or send_result.get('to')
533
+ )
534
+
535
+ if not recipients:
536
+ self.logger.warning(
537
+ "send_result for schedule %s is missing recipients", schedule_id
538
+ )
539
+ return
540
+
541
+ subject = send_result.get(
542
+ 'subject',
543
+ f"Scheduled job {agent_name} completed",
544
+ )
545
+
546
+ message = send_result.get(
547
+ 'message',
548
+ f"Job {agent_name} ({schedule_id}) completed successfully.",
549
+ )
550
+
551
+ if (include_result := send_result.get('include_result', True)):
552
+ if (formatted_result := self._format_result(result)):
553
+ message = f"{message}\n\nResult:\n{formatted_result}"
554
+
555
+ template = send_result.get('template')
556
+ report = send_result.get('report')
557
+
558
+ reserved_keys = {
559
+ 'recipients',
560
+ 'emails',
561
+ 'email',
562
+ 'to',
563
+ 'subject',
564
+ 'message',
565
+ 'include_result',
566
+ 'template',
567
+ 'report',
568
+ }
569
+
570
+ extra_kwargs = {
571
+ key: value
572
+ for key, value in send_result.items()
573
+ if key not in reserved_keys
574
+ }
575
+
576
+ notifier = _SchedulerNotification(self.logger)
577
+ await notifier.send_email(
578
+ message=message,
579
+ recipients=recipients,
580
+ subject=subject,
581
+ report=report,
582
+ template=template,
583
+ **extra_kwargs,
584
+ )
585
+
586
+ async def _process_job_success(
587
+ self,
588
+ schedule_id: str,
589
+ agent_name: str,
590
+ result: Any,
591
+ success_callback: Optional[Callable],
592
+ send_result: Optional[Dict[str, Any]],
593
+ ) -> None:
594
+ """Finalize processing for successful job executions."""
595
+ try:
596
+ await self._update_schedule_run(schedule_id, success=True)
597
+ except Exception as update_error: # pragma: no cover - safety net
598
+ self.logger.error(
599
+ "Failed to update schedule run for job %s: %s",
600
+ schedule_id,
601
+ update_error,
602
+ exc_info=True,
603
+ )
604
+
605
+ try:
606
+ await self._handle_job_success(
607
+ schedule_id,
608
+ agent_name,
609
+ result,
610
+ success_callback,
611
+ send_result,
612
+ )
613
+ except Exception as callback_error: # pragma: no cover - safety net
614
+ self.logger.error(
615
+ "Error executing success callback for job %s: %s",
616
+ schedule_id,
617
+ callback_error,
618
+ exc_info=True,
619
+ )
620
+
621
+ def _format_result(self, result: Any) -> str:
622
+ """Format execution result for notifications."""
623
+ if result is None:
624
+ return ''
625
+
626
+ if isinstance(result, (str, int, float, bool)):
627
+ return str(result)
628
+
629
+ if hasattr(result, 'model_dump'):
630
+ with contextlib.suppress(Exception):
631
+ return json.dumps(result.model_dump(), indent=2, default=str)
632
+
633
+ if hasattr(result, 'dict'):
634
+ with contextlib.suppress(Exception):
635
+ return json.dumps(result.dict(), indent=2, default=str)
636
+
637
+ try:
638
+ return json.dumps(result, indent=2, default=str)
639
+ except TypeError:
640
+ return str(result)
641
+
642
+ async def _update_schedule_run(
643
+ self,
644
+ schedule_id: str,
645
+ success: bool = True,
646
+ error: Optional[str] = None
647
+ ):
648
+ """Update schedule record after execution."""
649
+ try:
650
+ async with await self._pool.acquire() as conn: # pylint: disable=no-member # noqa
651
+ AgentSchedule.Meta.connection = conn
652
+ schedule = AgentSchedule.get(schedule_id=schedule_id)
653
+
654
+ schedule.last_run = datetime.now()
655
+ schedule.run_count += 1
656
+
657
+ if error:
658
+ if not schedule.metadata:
659
+ schedule.metadata = {}
660
+ schedule.metadata['last_error'] = error
661
+ schedule.metadata['last_error_time'] = datetime.now().isoformat()
662
+
663
+ await schedule.update()
664
+
665
+ except Exception as e:
666
+ self.logger.error(f"Failed to update schedule run: {e}")
667
+
668
+ def _create_trigger(self, schedule_type: str, config: Dict[str, Any]):
669
+ """
670
+ Create APScheduler trigger based on schedule type and configuration.
671
+
672
+ Args:
673
+ schedule_type: Type of schedule (daily, weekly, monthly, interval, cron)
674
+ config: Configuration dictionary for the trigger
675
+
676
+ Returns:
677
+ APScheduler trigger instance
678
+ """
679
+ schedule_type = schedule_type.lower()
680
+
681
+ if schedule_type == ScheduleType.ONCE.value:
682
+ run_date = config.get('run_date', datetime.now())
683
+ return DateTrigger(run_date=run_date)
684
+
685
+ elif schedule_type == ScheduleType.DAILY.value:
686
+ hour = config.get('hour', 0)
687
+ minute = config.get('minute', 0)
688
+ return CronTrigger(hour=hour, minute=minute)
689
+
690
+ elif schedule_type == ScheduleType.WEEKLY.value:
691
+ day_of_week = config.get('day_of_week', 'mon')
692
+ hour = config.get('hour', 0)
693
+ minute = config.get('minute', 0)
694
+ return CronTrigger(day_of_week=day_of_week, hour=hour, minute=minute)
695
+
696
+ elif schedule_type == ScheduleType.MONTHLY.value:
697
+ day = config.get('day', 1)
698
+ hour = config.get('hour', 0)
699
+ minute = config.get('minute', 0)
700
+ return CronTrigger(day=day, hour=hour, minute=minute)
701
+
702
+ elif schedule_type == ScheduleType.INTERVAL.value:
703
+ return IntervalTrigger(
704
+ weeks=config.get('weeks', 0),
705
+ days=config.get('days', 0),
706
+ hours=config.get('hours', 0),
707
+ minutes=config.get('minutes', 0),
708
+ seconds=config.get('seconds', 0)
709
+ )
710
+
711
+ elif schedule_type == ScheduleType.CRON.value:
712
+ # Full cron expression support
713
+ return CronTrigger(**config)
714
+
715
+ elif schedule_type == ScheduleType.CRONTAB.value:
716
+ # Support for crontab syntax (same as cron but more user-friendly)
717
+ return CronTrigger.from_crontab(**config, timezone='UTC')
718
+
719
+ else:
720
+ raise ValueError(
721
+ f"Unsupported schedule type: {schedule_type}"
722
+ )
723
+
724
+ async def add_schedule(
725
+ self,
726
+ agent_name: str,
727
+ schedule_type: str,
728
+ schedule_config: Dict[str, Any],
729
+ prompt: Optional[str] = None,
730
+ method_name: Optional[str] = None,
731
+ created_by: Optional[int] = None,
732
+ created_email: Optional[str] = None,
733
+ metadata: Optional[Dict] = None,
734
+ agent_id: Optional[str] = None,
735
+ *,
736
+ is_crew: bool = False,
737
+ send_result: Optional[Dict[str, Any]] = None,
738
+ success_callback: Optional[Callable] = None
739
+ ) -> AgentSchedule:
740
+ """
741
+ Add a new schedule to both database and APScheduler.
742
+
743
+ Args:
744
+ agent_name: Name of the agent
745
+ schedule_type: Type of schedule
746
+ schedule_config: Configuration for the schedule
747
+ prompt: Optional prompt to execute
748
+ method_name: Optional method name to call
749
+ created_by: User ID who created the schedule
750
+ created_email: Email of creator
751
+ metadata: Additional metadata passed to execution method
752
+ agent_id: Optional agent ID
753
+ is_crew: Whether the scheduled target is a crew
754
+ send_result: Optional configuration to email execution results
755
+ success_callback: Optional coroutine/function executed after success
756
+
757
+ Returns:
758
+ Created AgentSchedule instance
759
+ """
760
+ # Validate agent exists
761
+ if self.bot_manager:
762
+ if is_crew:
763
+ crew_entry = self.bot_manager.get_crew(agent_name)
764
+ if not crew_entry:
765
+ raise ValueError(f"Crew {agent_name} not found")
766
+ _, crew_def = crew_entry
767
+ if not agent_id:
768
+ agent_id = getattr(crew_def, 'crew_id', agent_name)
769
+ else:
770
+ agent = self.bot_manager._bots.get(
771
+ agent_name
772
+ ) or await self.bot_manager.registry.get_instance(agent_name)
773
+ if not agent:
774
+ raise ValueError(f"Agent {agent_name} not found")
775
+
776
+ if not agent_id:
777
+ agent_id = getattr(agent, 'chatbot_id', agent_name)
778
+
779
+ # Create database record
780
+ async with await self._pool.acquire() as conn: # pylint: disable=no-member # noqa
781
+ # TODO> create the bind method: AgentSchedule.bind(conn)
782
+ AgentSchedule.Meta.connection = conn
783
+ try:
784
+ schedule = AgentSchedule(
785
+ agent_id=agent_id or agent_name,
786
+ agent_name=agent_name,
787
+ prompt=prompt,
788
+ method_name=method_name,
789
+ schedule_type=schedule_type,
790
+ schedule_config=schedule_config,
791
+ created_by=created_by,
792
+ created_email=created_email,
793
+ metadata=dict(metadata or {}),
794
+ is_crew=is_crew,
795
+ send_result=dict(send_result or {}),
796
+ )
797
+ await schedule.save()
798
+ except Exception as e:
799
+ self.logger.error(f"Error saving schedule object: {e}")
800
+ raise
801
+
802
+ # Add to APScheduler
803
+ try:
804
+ trigger = self._create_trigger(schedule_type, schedule_config)
805
+
806
+ job = self.scheduler.add_job(
807
+ self._execute_agent_job,
808
+ trigger=trigger,
809
+ id=str(schedule.schedule_id),
810
+ name=f"{agent_name}_{schedule_type}",
811
+ kwargs={
812
+ 'schedule_id': str(schedule.schedule_id),
813
+ 'agent_name': agent_name,
814
+ 'prompt': prompt,
815
+ 'method_name': method_name,
816
+ 'metadata': dict(metadata or {}),
817
+ 'is_crew': is_crew,
818
+ 'success_callback': success_callback,
819
+ 'send_result': dict(send_result or {}),
820
+ },
821
+ replace_existing=True
822
+ )
823
+
824
+ # Update next run time
825
+ if job.next_run_time:
826
+ schedule.next_run = job.next_run_time
827
+ await schedule.update()
828
+
829
+ self.logger.info(
830
+ f"Added schedule {schedule.schedule_id} for agent {agent_name}"
831
+ )
832
+
833
+ except Exception as e:
834
+ # Rollback database record
835
+ await schedule.delete()
836
+ raise RuntimeError(
837
+ f"Failed to add schedule to jobstore: {e}"
838
+ ) from e
839
+
840
+ return schedule
841
+
842
+ async def remove_schedule(self, schedule_id: str):
843
+ """Remove a schedule from both database and APScheduler."""
844
+ try:
845
+ # Remove from APScheduler
846
+ self.scheduler.remove_job(schedule_id)
847
+
848
+ # Remove from database
849
+ async with await self._pool.acquire() as conn: # pylint: disable=no-member # noqa
850
+ AgentSchedule.Meta.connection = conn
851
+ schedule = await AgentSchedule.get(schedule_id=uuid.UUID(schedule_id))
852
+ await schedule.delete()
853
+
854
+ self.logger.info(
855
+ f"Removed schedule {schedule_id}"
856
+ )
857
+
858
+ except Exception as e:
859
+ self.logger.error(f"Error removing schedule {schedule_id}: {e}")
860
+ raise
861
+
862
+ async def load_schedules_from_db(self):
863
+ """Load all enabled schedules from database and add to APScheduler."""
864
+ try:
865
+ # Query all enabled schedules
866
+ query = """
867
+ SELECT * FROM navigator.agents_scheduler
868
+ WHERE enabled = TRUE
869
+ ORDER BY created_at
870
+ """
871
+ async with await self._pool.acquire() as conn: # pylint: disable=no-member # noqa
872
+ AgentSchedule.Meta.connection = conn
873
+ results, error = await conn.query(query)
874
+ if error:
875
+ self.logger.warning(f"Error querying schedules: {error}")
876
+ return
877
+
878
+ loaded = 0
879
+ failed = 0
880
+
881
+ for record in results:
882
+ try:
883
+ schedule_data = AgentSchedule(**record)
884
+ trigger = self._create_trigger(
885
+ schedule_data.schedule_type,
886
+ schedule_data.schedule_config
887
+ )
888
+
889
+ self.scheduler.add_job(
890
+ self._execute_agent_job,
891
+ trigger=trigger,
892
+ id=str(schedule_data.schedule_id),
893
+ name=f"{schedule_data.agent_name}_{schedule_data.schedule_type}",
894
+ kwargs={
895
+ 'schedule_id': str(schedule_data.schedule_id),
896
+ 'agent_name': schedule_data.agent_name,
897
+ 'prompt': schedule_data.prompt,
898
+ 'method_name': schedule_data.method_name,
899
+ 'metadata': dict(schedule_data.metadata or {}),
900
+ 'is_crew': schedule_data.is_crew,
901
+ 'send_result': dict(schedule_data.send_result or {}),
902
+ },
903
+ replace_existing=True
904
+ )
905
+
906
+ loaded += 1
907
+
908
+ except Exception as e:
909
+ failed += 1
910
+ self.logger.error(
911
+ f"Failed to load schedule {record.get('schedule_id')}: {e}"
912
+ )
913
+
914
+ self.logger.notice(
915
+ f"Loaded {loaded} schedules from database ({failed} failed)"
916
+ )
917
+
918
+ except Exception as e:
919
+ self.logger.error(f"Error loading schedules from database: {e}")
920
+ raise
921
+
922
+ async def restart_scheduler(self):
923
+ """Safely restart the scheduler."""
924
+ try:
925
+ self.logger.info("Restarting scheduler...")
926
+
927
+ if self.scheduler.running:
928
+ self.scheduler.shutdown(wait=True)
929
+
930
+ # Reload schedules from database
931
+ await self.load_schedules_from_db()
932
+
933
+ # Start scheduler
934
+ self.scheduler.start()
935
+
936
+ self.logger.notice("Scheduler restarted successfully")
937
+
938
+ except Exception as e:
939
+ self.logger.error(f"Error restarting scheduler: {e}")
940
+ raise
941
+
942
+ def setup(self, app: web.Application) -> web.Application:
943
+ """
944
+ Setup scheduler with aiohttp application.
945
+
946
+ Similar to BotManager setup pattern.
947
+ """
948
+ # Database Pool:
949
+ self.db = PostgresPool(
950
+ dsn=default_dsn,
951
+ name="Parrot.Scheduler",
952
+ startup=self.on_startup,
953
+ shutdown=self.on_shutdown
954
+ )
955
+ self.db.configure(app, register="agentdb")
956
+ self.app = app
957
+
958
+ # Add to app
959
+ self.app['scheduler_manager'] = self
960
+
961
+ # Configure routes
962
+ router = self.app.router
963
+ router.add_view(
964
+ '/api/v1/parrot/scheduler/schedules',
965
+ SchedulerHandler
966
+ )
967
+ router.add_view(
968
+ '/api/v1/parrot/scheduler/schedules/{schedule_id}',
969
+ SchedulerHandler
970
+ )
971
+ router.add_post(
972
+ '/api/v1/parrot/scheduler/restart',
973
+ self.restart_handler
974
+ )
975
+
976
+ return self.app
977
+
978
+ async def on_startup(self, app: web.Application, conn: Callable):
979
+ """Initialize scheduler on app startup."""
980
+ self.logger.notice("Starting Agent Scheduler...")
981
+ try:
982
+ self._pool = conn
983
+ except Exception as e:
984
+ self.logger.error(
985
+ f"Failed to get database connection pool: {e}"
986
+ )
987
+ self._pool = app['agentdb']
988
+
989
+ # Load schedules from database
990
+ await self.load_schedules_from_db()
991
+
992
+ # Start scheduler
993
+ self.scheduler.start()
994
+
995
+ self.logger.notice(
996
+ "Agent Scheduler started successfully"
997
+ )
998
+
999
+ async def on_shutdown(self, app: web.Application, conn: Callable):
1000
+ """Cleanup on app shutdown."""
1001
+ self.logger.info("Shutting down Agent Scheduler...")
1002
+
1003
+ if self.scheduler.running:
1004
+ self.scheduler.shutdown(wait=True)
1005
+
1006
+ self.logger.notice("Agent Scheduler shut down")
1007
+
1008
+ async def restart_handler(self, request: web.Request):
1009
+ """HTTP endpoint to restart scheduler."""
1010
+ try:
1011
+ await self.restart_scheduler()
1012
+ return web.json_response({
1013
+ 'status': 'success',
1014
+ 'message': 'Scheduler restarted successfully'
1015
+ })
1016
+ except Exception as e:
1017
+ return web.json_response({
1018
+ 'status': 'error',
1019
+ 'message': str(e)
1020
+ }, status=500)
1021
+
1022
+
1023
+ class SchedulerHandler(web.View):
1024
+ """HTTP handler for schedule management."""
1025
+
1026
+ async def get(self):
1027
+ """Get schedule(s)."""
1028
+ scheduler_manager = self.request.app.get('scheduler_manager')
1029
+ schedule_id = self.request.match_info.get('schedule_id')
1030
+
1031
+ try:
1032
+ if schedule_id:
1033
+ # Get specific schedule
1034
+ async with await self._pool.acquire() as conn: # pylint: disable=no-member # noqa
1035
+ AgentSchedule.Meta.connection = conn
1036
+ schedule = await AgentSchedule.get(schedule_id=uuid.UUID(schedule_id))
1037
+
1038
+ # Get job info from scheduler
1039
+ job = scheduler_manager.scheduler.get_job(schedule_id)
1040
+ job_info = {
1041
+ 'next_run': job.next_run_time.isoformat() if job and job.next_run_time else None,
1042
+ 'pending': job is not None
1043
+ }
1044
+
1045
+ return web.json_response({
1046
+ 'schedule': dict(schedule),
1047
+ 'job': job_info
1048
+ })
1049
+ else:
1050
+ # List all schedules
1051
+ async with await self._pool.acquire() as conn: # pylint: disable=no-member # noqa
1052
+ AgentSchedule.Meta.connection = conn
1053
+ results = await AgentSchedule.all()
1054
+
1055
+ return web.json_response({
1056
+ 'schedules': [dict(r) for r in results],
1057
+ 'count': len(results)
1058
+ })
1059
+
1060
+ except Exception as e:
1061
+ return web.json_response({
1062
+ 'status': 'error',
1063
+ 'message': str(e)
1064
+ }, status=500)
1065
+
1066
+ async def post(self):
1067
+ """Create new schedule."""
1068
+ scheduler_manager = self.request.app.get('scheduler_manager')
1069
+
1070
+ try:
1071
+ data = await self.request.json()
1072
+
1073
+ # Extract session info
1074
+ session = await self.request.app.get('session_manager').get_session(
1075
+ self.request
1076
+ )
1077
+ created_by = session.get('user_id')
1078
+ created_email = session.get('email')
1079
+
1080
+ schedule = await scheduler_manager.add_schedule(
1081
+ agent_name=data['agent_name'],
1082
+ schedule_type=data['schedule_type'],
1083
+ schedule_config=data['schedule_config'],
1084
+ prompt=data.get('prompt'),
1085
+ method_name=data.get('method_name'),
1086
+ created_by=created_by,
1087
+ created_email=created_email,
1088
+ metadata=data.get('metadata', {}),
1089
+ is_crew=data.get('is_crew', False),
1090
+ send_result=data.get('send_result'),
1091
+ )
1092
+
1093
+ return web.json_response({
1094
+ 'status': 'success',
1095
+ 'schedule': dict(schedule)
1096
+ }, status=201)
1097
+
1098
+ except Exception as e:
1099
+ return web.json_response({
1100
+ 'status': 'error',
1101
+ 'message': str(e)
1102
+ }, status=500)
1103
+
1104
+ async def delete(self):
1105
+ """Delete schedule."""
1106
+ scheduler_manager = self.request.app.get('scheduler_manager')
1107
+ schedule_id = self.request.match_info.get('schedule_id')
1108
+
1109
+ if not schedule_id:
1110
+ return web.json_response({
1111
+ 'status': 'error',
1112
+ 'message': 'schedule_id required'
1113
+ }, status=400)
1114
+
1115
+ try:
1116
+ await scheduler_manager.remove_schedule(schedule_id)
1117
+
1118
+ return web.json_response({
1119
+ 'status': 'success',
1120
+ 'message': f'Schedule {schedule_id} deleted'
1121
+ })
1122
+
1123
+ except Exception as e:
1124
+ return web.json_response({
1125
+ 'status': 'error',
1126
+ 'message': str(e)
1127
+ }, status=500)
1128
+
1129
+ async def patch(self):
1130
+ """Update schedule (enable/disable)."""
1131
+ schedule_id = self.request.match_info.get('schedule_id')
1132
+
1133
+ if not schedule_id:
1134
+ return web.json_response({
1135
+ 'status': 'error',
1136
+ 'message': 'schedule_id required'
1137
+ }, status=400)
1138
+
1139
+ try:
1140
+ data = await self.request.json()
1141
+
1142
+ async with await self._pool.acquire() as conn: # pylint: disable=no-member # noqa
1143
+ AgentSchedule.Meta.connection = conn
1144
+ schedule = await AgentSchedule.get(schedule_id=uuid.UUID(schedule_id))
1145
+
1146
+ # Update fields
1147
+ if 'enabled' in data:
1148
+ schedule.enabled = data['enabled']
1149
+
1150
+ schedule.updated_at = datetime.now()
1151
+ await schedule.update()
1152
+
1153
+ # If disabled, remove from scheduler
1154
+ scheduler_manager = self.request.app.get('scheduler_manager')
1155
+ if not schedule.enabled:
1156
+ scheduler_manager.scheduler.remove_job(schedule_id)
1157
+ else:
1158
+ # Re-add to scheduler
1159
+ trigger = scheduler_manager._create_trigger(
1160
+ schedule.schedule_type,
1161
+ schedule.schedule_config
1162
+ )
1163
+ scheduler_manager.scheduler.add_job(
1164
+ scheduler_manager._execute_agent_job,
1165
+ trigger=trigger,
1166
+ id=schedule_id,
1167
+ name=f"{schedule.agent_name}_{schedule.schedule_type}",
1168
+ kwargs={
1169
+ 'schedule_id': schedule_id,
1170
+ 'agent_name': schedule.agent_name,
1171
+ 'prompt': schedule.prompt,
1172
+ 'method_name': schedule.method_name,
1173
+ 'metadata': dict(schedule.metadata or {}),
1174
+ 'is_crew': schedule.is_crew,
1175
+ 'send_result': dict(schedule.send_result or {}),
1176
+ },
1177
+ replace_existing=True
1178
+ )
1179
+
1180
+ return web.json_response({
1181
+ 'status': 'success',
1182
+ 'schedule': dict(schedule)
1183
+ })
1184
+
1185
+ except Exception as e:
1186
+ return web.json_response({
1187
+ 'status': 'error',
1188
+ 'message': str(e)
1189
+ }, status=500)