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.
- agentui/.prettierrc +15 -0
- agentui/QUICKSTART.md +272 -0
- agentui/README.md +59 -0
- agentui/env.example +16 -0
- agentui/jsconfig.json +14 -0
- agentui/package-lock.json +4242 -0
- agentui/package.json +34 -0
- agentui/scripts/postinstall/apply-patches.mjs +260 -0
- agentui/src/app.css +61 -0
- agentui/src/app.d.ts +13 -0
- agentui/src/app.html +12 -0
- agentui/src/components/LoadingSpinner.svelte +64 -0
- agentui/src/components/ThemeSwitcher.svelte +159 -0
- agentui/src/components/index.js +4 -0
- agentui/src/lib/api/bots.ts +60 -0
- agentui/src/lib/api/chat.ts +22 -0
- agentui/src/lib/api/http.ts +25 -0
- agentui/src/lib/components/BotCard.svelte +33 -0
- agentui/src/lib/components/ChatBubble.svelte +63 -0
- agentui/src/lib/components/Toast.svelte +21 -0
- agentui/src/lib/config.ts +20 -0
- agentui/src/lib/stores/auth.svelte.ts +73 -0
- agentui/src/lib/stores/theme.svelte.js +64 -0
- agentui/src/lib/stores/toast.svelte.ts +31 -0
- agentui/src/lib/utils/conversation.ts +39 -0
- agentui/src/routes/+layout.svelte +20 -0
- agentui/src/routes/+page.svelte +232 -0
- agentui/src/routes/login/+page.svelte +200 -0
- agentui/src/routes/talk/[agentId]/+page.svelte +297 -0
- agentui/src/routes/talk/[agentId]/+page.ts +7 -0
- agentui/static/README.md +1 -0
- agentui/svelte.config.js +11 -0
- agentui/tailwind.config.ts +53 -0
- agentui/tsconfig.json +3 -0
- agentui/vite.config.ts +10 -0
- ai_parrot-0.17.2.dist-info/METADATA +472 -0
- ai_parrot-0.17.2.dist-info/RECORD +535 -0
- ai_parrot-0.17.2.dist-info/WHEEL +6 -0
- ai_parrot-0.17.2.dist-info/entry_points.txt +2 -0
- ai_parrot-0.17.2.dist-info/licenses/LICENSE +21 -0
- ai_parrot-0.17.2.dist-info/top_level.txt +6 -0
- crew-builder/.prettierrc +15 -0
- crew-builder/QUICKSTART.md +259 -0
- crew-builder/README.md +113 -0
- crew-builder/env.example +17 -0
- crew-builder/jsconfig.json +14 -0
- crew-builder/package-lock.json +4182 -0
- crew-builder/package.json +37 -0
- crew-builder/scripts/postinstall/apply-patches.mjs +260 -0
- crew-builder/src/app.css +62 -0
- crew-builder/src/app.d.ts +13 -0
- crew-builder/src/app.html +12 -0
- crew-builder/src/components/LoadingSpinner.svelte +64 -0
- crew-builder/src/components/ThemeSwitcher.svelte +149 -0
- crew-builder/src/components/index.js +9 -0
- crew-builder/src/lib/api/bots.ts +60 -0
- crew-builder/src/lib/api/chat.ts +80 -0
- crew-builder/src/lib/api/client.ts +56 -0
- crew-builder/src/lib/api/crew/crew.ts +136 -0
- crew-builder/src/lib/api/index.ts +5 -0
- crew-builder/src/lib/api/o365/auth.ts +65 -0
- crew-builder/src/lib/auth/auth.ts +54 -0
- crew-builder/src/lib/components/AgentNode.svelte +43 -0
- crew-builder/src/lib/components/BotCard.svelte +33 -0
- crew-builder/src/lib/components/ChatBubble.svelte +67 -0
- crew-builder/src/lib/components/ConfigPanel.svelte +278 -0
- crew-builder/src/lib/components/JsonTreeNode.svelte +76 -0
- crew-builder/src/lib/components/JsonViewer.svelte +24 -0
- crew-builder/src/lib/components/MarkdownEditor.svelte +48 -0
- crew-builder/src/lib/components/ThemeToggle.svelte +36 -0
- crew-builder/src/lib/components/Toast.svelte +67 -0
- crew-builder/src/lib/components/Toolbar.svelte +157 -0
- crew-builder/src/lib/components/index.ts +10 -0
- crew-builder/src/lib/config.ts +8 -0
- crew-builder/src/lib/stores/auth.svelte.ts +228 -0
- crew-builder/src/lib/stores/crewStore.ts +369 -0
- crew-builder/src/lib/stores/theme.svelte.js +145 -0
- crew-builder/src/lib/stores/toast.svelte.ts +69 -0
- crew-builder/src/lib/utils/conversation.ts +39 -0
- crew-builder/src/lib/utils/markdown.ts +122 -0
- crew-builder/src/lib/utils/talkHistory.ts +47 -0
- crew-builder/src/routes/+layout.svelte +20 -0
- crew-builder/src/routes/+page.svelte +539 -0
- crew-builder/src/routes/agents/+page.svelte +247 -0
- crew-builder/src/routes/agents/[agentId]/+page.svelte +288 -0
- crew-builder/src/routes/agents/[agentId]/+page.ts +7 -0
- crew-builder/src/routes/builder/+page.svelte +204 -0
- crew-builder/src/routes/crew/ask/+page.svelte +1052 -0
- crew-builder/src/routes/crew/ask/+page.ts +1 -0
- crew-builder/src/routes/integrations/o365/+page.svelte +304 -0
- crew-builder/src/routes/login/+page.svelte +197 -0
- crew-builder/src/routes/talk/[agentId]/+page.svelte +487 -0
- crew-builder/src/routes/talk/[agentId]/+page.ts +7 -0
- crew-builder/static/README.md +1 -0
- crew-builder/svelte.config.js +11 -0
- crew-builder/tailwind.config.ts +53 -0
- crew-builder/tsconfig.json +3 -0
- crew-builder/vite.config.ts +10 -0
- mcp_servers/calculator_server.py +309 -0
- parrot/__init__.py +27 -0
- parrot/__pycache__/__init__.cpython-310.pyc +0 -0
- parrot/__pycache__/version.cpython-310.pyc +0 -0
- parrot/_version.py +34 -0
- parrot/a2a/__init__.py +48 -0
- parrot/a2a/client.py +658 -0
- parrot/a2a/discovery.py +89 -0
- parrot/a2a/mixin.py +257 -0
- parrot/a2a/models.py +376 -0
- parrot/a2a/server.py +770 -0
- parrot/agents/__init__.py +29 -0
- parrot/bots/__init__.py +12 -0
- parrot/bots/a2a_agent.py +19 -0
- parrot/bots/abstract.py +3139 -0
- parrot/bots/agent.py +1129 -0
- parrot/bots/basic.py +9 -0
- parrot/bots/chatbot.py +669 -0
- parrot/bots/data.py +1618 -0
- parrot/bots/database/__init__.py +5 -0
- parrot/bots/database/abstract.py +3071 -0
- parrot/bots/database/cache.py +286 -0
- parrot/bots/database/models.py +468 -0
- parrot/bots/database/prompts.py +154 -0
- parrot/bots/database/retries.py +98 -0
- parrot/bots/database/router.py +269 -0
- parrot/bots/database/sql.py +41 -0
- parrot/bots/db/__init__.py +6 -0
- parrot/bots/db/abstract.py +556 -0
- parrot/bots/db/bigquery.py +602 -0
- parrot/bots/db/cache.py +85 -0
- parrot/bots/db/documentdb.py +668 -0
- parrot/bots/db/elastic.py +1014 -0
- parrot/bots/db/influx.py +898 -0
- parrot/bots/db/mock.py +96 -0
- parrot/bots/db/multi.py +783 -0
- parrot/bots/db/prompts.py +185 -0
- parrot/bots/db/sql.py +1255 -0
- parrot/bots/db/tools.py +212 -0
- parrot/bots/document.py +680 -0
- parrot/bots/hrbot.py +15 -0
- parrot/bots/kb.py +170 -0
- parrot/bots/mcp.py +36 -0
- parrot/bots/orchestration/README.md +463 -0
- parrot/bots/orchestration/__init__.py +1 -0
- parrot/bots/orchestration/agent.py +155 -0
- parrot/bots/orchestration/crew.py +3330 -0
- parrot/bots/orchestration/fsm.py +1179 -0
- parrot/bots/orchestration/hr.py +434 -0
- parrot/bots/orchestration/storage/__init__.py +4 -0
- parrot/bots/orchestration/storage/memory.py +100 -0
- parrot/bots/orchestration/storage/mixin.py +119 -0
- parrot/bots/orchestration/verify.py +202 -0
- parrot/bots/product.py +204 -0
- parrot/bots/prompts/__init__.py +96 -0
- parrot/bots/prompts/agents.py +155 -0
- parrot/bots/prompts/data.py +216 -0
- parrot/bots/prompts/output_generation.py +8 -0
- parrot/bots/scraper/__init__.py +3 -0
- parrot/bots/scraper/models.py +122 -0
- parrot/bots/scraper/scraper.py +1173 -0
- parrot/bots/scraper/templates.py +115 -0
- parrot/bots/stores/__init__.py +5 -0
- parrot/bots/stores/local.py +172 -0
- parrot/bots/webdev.py +81 -0
- parrot/cli.py +17 -0
- parrot/clients/__init__.py +16 -0
- parrot/clients/base.py +1491 -0
- parrot/clients/claude.py +1191 -0
- parrot/clients/factory.py +129 -0
- parrot/clients/google.py +4567 -0
- parrot/clients/gpt.py +1975 -0
- parrot/clients/grok.py +432 -0
- parrot/clients/groq.py +986 -0
- parrot/clients/hf.py +582 -0
- parrot/clients/models.py +18 -0
- parrot/conf.py +395 -0
- parrot/embeddings/__init__.py +9 -0
- parrot/embeddings/base.py +157 -0
- parrot/embeddings/google.py +98 -0
- parrot/embeddings/huggingface.py +74 -0
- parrot/embeddings/openai.py +84 -0
- parrot/embeddings/processor.py +88 -0
- parrot/exceptions.c +13868 -0
- parrot/exceptions.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/exceptions.pxd +22 -0
- parrot/exceptions.pxi +15 -0
- parrot/exceptions.pyx +44 -0
- parrot/generators/__init__.py +29 -0
- parrot/generators/base.py +200 -0
- parrot/generators/html.py +293 -0
- parrot/generators/react.py +205 -0
- parrot/generators/streamlit.py +203 -0
- parrot/generators/template.py +105 -0
- parrot/handlers/__init__.py +4 -0
- parrot/handlers/agent.py +861 -0
- parrot/handlers/agents/__init__.py +1 -0
- parrot/handlers/agents/abstract.py +900 -0
- parrot/handlers/bots.py +338 -0
- parrot/handlers/chat.py +915 -0
- parrot/handlers/creation.sql +192 -0
- parrot/handlers/crew/ARCHITECTURE.md +362 -0
- parrot/handlers/crew/README_BOTMANAGER_PERSISTENCE.md +303 -0
- parrot/handlers/crew/README_REDIS_PERSISTENCE.md +366 -0
- parrot/handlers/crew/__init__.py +0 -0
- parrot/handlers/crew/handler.py +801 -0
- parrot/handlers/crew/models.py +229 -0
- parrot/handlers/crew/redis_persistence.py +523 -0
- parrot/handlers/jobs/__init__.py +10 -0
- parrot/handlers/jobs/job.py +384 -0
- parrot/handlers/jobs/mixin.py +627 -0
- parrot/handlers/jobs/models.py +115 -0
- parrot/handlers/jobs/worker.py +31 -0
- parrot/handlers/models.py +596 -0
- parrot/handlers/o365_auth.py +105 -0
- parrot/handlers/stream.py +337 -0
- parrot/interfaces/__init__.py +6 -0
- parrot/interfaces/aws.py +143 -0
- parrot/interfaces/credentials.py +113 -0
- parrot/interfaces/database.py +27 -0
- parrot/interfaces/google.py +1123 -0
- parrot/interfaces/hierarchy.py +1227 -0
- parrot/interfaces/http.py +651 -0
- parrot/interfaces/images/__init__.py +0 -0
- parrot/interfaces/images/plugins/__init__.py +24 -0
- parrot/interfaces/images/plugins/abstract.py +58 -0
- parrot/interfaces/images/plugins/analisys.py +148 -0
- parrot/interfaces/images/plugins/classify.py +150 -0
- parrot/interfaces/images/plugins/classifybase.py +182 -0
- parrot/interfaces/images/plugins/detect.py +150 -0
- parrot/interfaces/images/plugins/exif.py +1103 -0
- parrot/interfaces/images/plugins/hash.py +52 -0
- parrot/interfaces/images/plugins/vision.py +104 -0
- parrot/interfaces/images/plugins/yolo.py +66 -0
- parrot/interfaces/images/plugins/zerodetect.py +197 -0
- parrot/interfaces/o365.py +978 -0
- parrot/interfaces/onedrive.py +822 -0
- parrot/interfaces/sharepoint.py +1435 -0
- parrot/interfaces/soap.py +257 -0
- parrot/loaders/__init__.py +8 -0
- parrot/loaders/abstract.py +1131 -0
- parrot/loaders/audio.py +199 -0
- parrot/loaders/basepdf.py +53 -0
- parrot/loaders/basevideo.py +1568 -0
- parrot/loaders/csv.py +409 -0
- parrot/loaders/docx.py +116 -0
- parrot/loaders/epubloader.py +316 -0
- parrot/loaders/excel.py +199 -0
- parrot/loaders/factory.py +55 -0
- parrot/loaders/files/__init__.py +0 -0
- parrot/loaders/files/abstract.py +39 -0
- parrot/loaders/files/html.py +26 -0
- parrot/loaders/files/text.py +63 -0
- parrot/loaders/html.py +152 -0
- parrot/loaders/markdown.py +442 -0
- parrot/loaders/pdf.py +373 -0
- parrot/loaders/pdfmark.py +320 -0
- parrot/loaders/pdftables.py +506 -0
- parrot/loaders/ppt.py +476 -0
- parrot/loaders/qa.py +63 -0
- parrot/loaders/splitters/__init__.py +10 -0
- parrot/loaders/splitters/base.py +138 -0
- parrot/loaders/splitters/md.py +228 -0
- parrot/loaders/splitters/token.py +143 -0
- parrot/loaders/txt.py +26 -0
- parrot/loaders/video.py +89 -0
- parrot/loaders/videolocal.py +218 -0
- parrot/loaders/videounderstanding.py +377 -0
- parrot/loaders/vimeo.py +167 -0
- parrot/loaders/web.py +599 -0
- parrot/loaders/youtube.py +504 -0
- parrot/manager/__init__.py +5 -0
- parrot/manager/manager.py +1030 -0
- parrot/mcp/__init__.py +28 -0
- parrot/mcp/adapter.py +105 -0
- parrot/mcp/cli.py +174 -0
- parrot/mcp/client.py +119 -0
- parrot/mcp/config.py +75 -0
- parrot/mcp/integration.py +842 -0
- parrot/mcp/oauth.py +933 -0
- parrot/mcp/server.py +225 -0
- parrot/mcp/transports/__init__.py +3 -0
- parrot/mcp/transports/base.py +279 -0
- parrot/mcp/transports/grpc_session.py +163 -0
- parrot/mcp/transports/http.py +312 -0
- parrot/mcp/transports/mcp.proto +108 -0
- parrot/mcp/transports/quic.py +1082 -0
- parrot/mcp/transports/sse.py +330 -0
- parrot/mcp/transports/stdio.py +309 -0
- parrot/mcp/transports/unix.py +395 -0
- parrot/mcp/transports/websocket.py +547 -0
- parrot/memory/__init__.py +16 -0
- parrot/memory/abstract.py +209 -0
- parrot/memory/agent.py +32 -0
- parrot/memory/cache.py +175 -0
- parrot/memory/core.py +555 -0
- parrot/memory/file.py +153 -0
- parrot/memory/mem.py +131 -0
- parrot/memory/redis.py +613 -0
- parrot/models/__init__.py +46 -0
- parrot/models/basic.py +118 -0
- parrot/models/compliance.py +208 -0
- parrot/models/crew.py +395 -0
- parrot/models/detections.py +654 -0
- parrot/models/generation.py +85 -0
- parrot/models/google.py +223 -0
- parrot/models/groq.py +23 -0
- parrot/models/openai.py +30 -0
- parrot/models/outputs.py +285 -0
- parrot/models/responses.py +938 -0
- parrot/notifications/__init__.py +743 -0
- parrot/openapi/__init__.py +3 -0
- parrot/openapi/components.yaml +641 -0
- parrot/openapi/config.py +322 -0
- parrot/outputs/__init__.py +32 -0
- parrot/outputs/formats/__init__.py +108 -0
- parrot/outputs/formats/altair.py +359 -0
- parrot/outputs/formats/application.py +122 -0
- parrot/outputs/formats/base.py +351 -0
- parrot/outputs/formats/bokeh.py +356 -0
- parrot/outputs/formats/card.py +424 -0
- parrot/outputs/formats/chart.py +436 -0
- parrot/outputs/formats/d3.py +255 -0
- parrot/outputs/formats/echarts.py +310 -0
- parrot/outputs/formats/generators/__init__.py +0 -0
- parrot/outputs/formats/generators/abstract.py +61 -0
- parrot/outputs/formats/generators/panel.py +145 -0
- parrot/outputs/formats/generators/streamlit.py +86 -0
- parrot/outputs/formats/generators/terminal.py +63 -0
- parrot/outputs/formats/holoviews.py +310 -0
- parrot/outputs/formats/html.py +147 -0
- parrot/outputs/formats/jinja2.py +46 -0
- parrot/outputs/formats/json.py +87 -0
- parrot/outputs/formats/map.py +933 -0
- parrot/outputs/formats/markdown.py +172 -0
- parrot/outputs/formats/matplotlib.py +237 -0
- parrot/outputs/formats/mixins/__init__.py +0 -0
- parrot/outputs/formats/mixins/emaps.py +855 -0
- parrot/outputs/formats/plotly.py +341 -0
- parrot/outputs/formats/seaborn.py +310 -0
- parrot/outputs/formats/table.py +397 -0
- parrot/outputs/formats/template_report.py +138 -0
- parrot/outputs/formats/yaml.py +125 -0
- parrot/outputs/formatter.py +152 -0
- parrot/outputs/templates/__init__.py +95 -0
- parrot/pipelines/__init__.py +0 -0
- parrot/pipelines/abstract.py +210 -0
- parrot/pipelines/detector.py +124 -0
- parrot/pipelines/models.py +90 -0
- parrot/pipelines/planogram.py +3002 -0
- parrot/pipelines/table.sql +97 -0
- parrot/plugins/__init__.py +106 -0
- parrot/plugins/importer.py +80 -0
- parrot/py.typed +0 -0
- parrot/registry/__init__.py +18 -0
- parrot/registry/registry.py +594 -0
- parrot/scheduler/__init__.py +1189 -0
- parrot/scheduler/models.py +60 -0
- parrot/security/__init__.py +16 -0
- parrot/security/prompt_injection.py +268 -0
- parrot/security/security_events.sql +25 -0
- parrot/services/__init__.py +1 -0
- parrot/services/mcp/__init__.py +8 -0
- parrot/services/mcp/config.py +13 -0
- parrot/services/mcp/server.py +295 -0
- parrot/services/o365_remote_auth.py +235 -0
- parrot/stores/__init__.py +7 -0
- parrot/stores/abstract.py +352 -0
- parrot/stores/arango.py +1090 -0
- parrot/stores/bigquery.py +1377 -0
- parrot/stores/cache.py +106 -0
- parrot/stores/empty.py +10 -0
- parrot/stores/faiss_store.py +1157 -0
- parrot/stores/kb/__init__.py +9 -0
- parrot/stores/kb/abstract.py +68 -0
- parrot/stores/kb/cache.py +165 -0
- parrot/stores/kb/doc.py +325 -0
- parrot/stores/kb/hierarchy.py +346 -0
- parrot/stores/kb/local.py +457 -0
- parrot/stores/kb/prompt.py +28 -0
- parrot/stores/kb/redis.py +659 -0
- parrot/stores/kb/store.py +115 -0
- parrot/stores/kb/user.py +374 -0
- parrot/stores/models.py +59 -0
- parrot/stores/pgvector.py +3 -0
- parrot/stores/postgres.py +2853 -0
- parrot/stores/utils/__init__.py +0 -0
- parrot/stores/utils/chunking.py +197 -0
- parrot/telemetry/__init__.py +3 -0
- parrot/telemetry/mixin.py +111 -0
- parrot/template/__init__.py +3 -0
- parrot/template/engine.py +259 -0
- parrot/tools/__init__.py +23 -0
- parrot/tools/abstract.py +644 -0
- parrot/tools/agent.py +363 -0
- parrot/tools/arangodbsearch.py +537 -0
- parrot/tools/arxiv_tool.py +188 -0
- parrot/tools/calculator/__init__.py +3 -0
- parrot/tools/calculator/operations/__init__.py +38 -0
- parrot/tools/calculator/operations/calculus.py +80 -0
- parrot/tools/calculator/operations/statistics.py +76 -0
- parrot/tools/calculator/tool.py +150 -0
- parrot/tools/cloudwatch.py +988 -0
- parrot/tools/codeinterpreter/__init__.py +127 -0
- parrot/tools/codeinterpreter/executor.py +371 -0
- parrot/tools/codeinterpreter/internals.py +473 -0
- parrot/tools/codeinterpreter/models.py +643 -0
- parrot/tools/codeinterpreter/prompts.py +224 -0
- parrot/tools/codeinterpreter/tool.py +664 -0
- parrot/tools/company_info/__init__.py +6 -0
- parrot/tools/company_info/tool.py +1138 -0
- parrot/tools/correlationanalysis.py +437 -0
- parrot/tools/database/abstract.py +286 -0
- parrot/tools/database/bq.py +115 -0
- parrot/tools/database/cache.py +284 -0
- parrot/tools/database/models.py +95 -0
- parrot/tools/database/pg.py +343 -0
- parrot/tools/databasequery.py +1159 -0
- parrot/tools/db.py +1800 -0
- parrot/tools/ddgo.py +370 -0
- parrot/tools/decorators.py +271 -0
- parrot/tools/dftohtml.py +282 -0
- parrot/tools/document.py +549 -0
- parrot/tools/ecs.py +819 -0
- parrot/tools/edareport.py +368 -0
- parrot/tools/elasticsearch.py +1049 -0
- parrot/tools/employees.py +462 -0
- parrot/tools/epson/__init__.py +96 -0
- parrot/tools/excel.py +683 -0
- parrot/tools/file/__init__.py +13 -0
- parrot/tools/file/abstract.py +76 -0
- parrot/tools/file/gcs.py +378 -0
- parrot/tools/file/local.py +284 -0
- parrot/tools/file/s3.py +511 -0
- parrot/tools/file/tmp.py +309 -0
- parrot/tools/file/tool.py +501 -0
- parrot/tools/file_reader.py +129 -0
- parrot/tools/flowtask/__init__.py +19 -0
- parrot/tools/flowtask/tool.py +761 -0
- parrot/tools/gittoolkit.py +508 -0
- parrot/tools/google/__init__.py +18 -0
- parrot/tools/google/base.py +169 -0
- parrot/tools/google/tools.py +1251 -0
- parrot/tools/googlelocation.py +5 -0
- parrot/tools/googleroutes.py +5 -0
- parrot/tools/googlesearch.py +5 -0
- parrot/tools/googlesitesearch.py +5 -0
- parrot/tools/googlevoice.py +2 -0
- parrot/tools/gvoice.py +695 -0
- parrot/tools/ibisworld/README.md +225 -0
- parrot/tools/ibisworld/__init__.py +11 -0
- parrot/tools/ibisworld/tool.py +366 -0
- parrot/tools/jiratoolkit.py +1718 -0
- parrot/tools/manager.py +1098 -0
- parrot/tools/math.py +152 -0
- parrot/tools/metadata.py +476 -0
- parrot/tools/msteams.py +1621 -0
- parrot/tools/msword.py +635 -0
- parrot/tools/multidb.py +580 -0
- parrot/tools/multistoresearch.py +369 -0
- parrot/tools/networkninja.py +167 -0
- parrot/tools/nextstop/__init__.py +4 -0
- parrot/tools/nextstop/base.py +286 -0
- parrot/tools/nextstop/employee.py +733 -0
- parrot/tools/nextstop/store.py +462 -0
- parrot/tools/notification.py +435 -0
- parrot/tools/o365/__init__.py +42 -0
- parrot/tools/o365/base.py +295 -0
- parrot/tools/o365/bundle.py +522 -0
- parrot/tools/o365/events.py +554 -0
- parrot/tools/o365/mail.py +992 -0
- parrot/tools/o365/onedrive.py +497 -0
- parrot/tools/o365/sharepoint.py +641 -0
- parrot/tools/openapi_toolkit.py +904 -0
- parrot/tools/openweather.py +527 -0
- parrot/tools/pdfprint.py +1001 -0
- parrot/tools/powerbi.py +518 -0
- parrot/tools/powerpoint.py +1113 -0
- parrot/tools/pricestool.py +146 -0
- parrot/tools/products/__init__.py +246 -0
- parrot/tools/prophet_tool.py +171 -0
- parrot/tools/pythonpandas.py +630 -0
- parrot/tools/pythonrepl.py +910 -0
- parrot/tools/qsource.py +436 -0
- parrot/tools/querytoolkit.py +395 -0
- parrot/tools/quickeda.py +827 -0
- parrot/tools/resttool.py +553 -0
- parrot/tools/retail/__init__.py +0 -0
- parrot/tools/retail/bby.py +528 -0
- parrot/tools/sandboxtool.py +703 -0
- parrot/tools/sassie/__init__.py +352 -0
- parrot/tools/scraping/__init__.py +7 -0
- parrot/tools/scraping/docs/select.md +466 -0
- parrot/tools/scraping/documentation.md +1278 -0
- parrot/tools/scraping/driver.py +436 -0
- parrot/tools/scraping/models.py +576 -0
- parrot/tools/scraping/options.py +85 -0
- parrot/tools/scraping/orchestrator.py +517 -0
- parrot/tools/scraping/readme.md +740 -0
- parrot/tools/scraping/tool.py +3115 -0
- parrot/tools/seasonaldetection.py +642 -0
- parrot/tools/shell_tool/__init__.py +5 -0
- parrot/tools/shell_tool/actions.py +408 -0
- parrot/tools/shell_tool/engine.py +155 -0
- parrot/tools/shell_tool/models.py +322 -0
- parrot/tools/shell_tool/tool.py +442 -0
- parrot/tools/site_search.py +214 -0
- parrot/tools/textfile.py +418 -0
- parrot/tools/think.py +378 -0
- parrot/tools/toolkit.py +298 -0
- parrot/tools/webapp_tool.py +187 -0
- parrot/tools/whatif.py +1279 -0
- parrot/tools/workday/MULTI_WSDL_EXAMPLE.md +249 -0
- parrot/tools/workday/__init__.py +6 -0
- parrot/tools/workday/models.py +1389 -0
- parrot/tools/workday/tool.py +1293 -0
- parrot/tools/yfinance_tool.py +306 -0
- parrot/tools/zipcode.py +217 -0
- parrot/utils/__init__.py +2 -0
- parrot/utils/helpers.py +73 -0
- parrot/utils/parsers/__init__.py +5 -0
- parrot/utils/parsers/toml.c +12078 -0
- parrot/utils/parsers/toml.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/utils/parsers/toml.pyx +21 -0
- parrot/utils/toml.py +11 -0
- parrot/utils/types.cpp +20936 -0
- parrot/utils/types.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/utils/types.pyx +213 -0
- parrot/utils/uv.py +11 -0
- parrot/version.py +10 -0
- parrot/yaml-rs/Cargo.lock +350 -0
- parrot/yaml-rs/Cargo.toml +19 -0
- parrot/yaml-rs/pyproject.toml +19 -0
- parrot/yaml-rs/python/yaml_rs/__init__.py +81 -0
- parrot/yaml-rs/src/lib.rs +222 -0
- requirements/docker-compose.yml +24 -0
- 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)
|