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,900 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Tuple, Union, List, Dict, Any, Optional, Callable, Sequence, Awaitable
|
|
4
|
+
import functools
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
import tempfile
|
|
7
|
+
import aiofiles
|
|
8
|
+
# Parrot:
|
|
9
|
+
from aiohttp import web
|
|
10
|
+
from datamodel.parsers.json import json_encoder # noqa pylint: disable=E0611
|
|
11
|
+
# AsyncDB:
|
|
12
|
+
from asyncdb import AsyncDB
|
|
13
|
+
# Requirements from Notify API:
|
|
14
|
+
from notify import Notify # para envio local
|
|
15
|
+
from notify.providers.teams import Teams
|
|
16
|
+
from notify.server import NotifyClient # envio a traves de los workers
|
|
17
|
+
from notify.models import Actor, Chat, TeamsCard, TeamsChannel
|
|
18
|
+
from notify.conf import NOTIFY_REDIS, NOTIFY_WORKER_STREAM, NOTIFY_CHANNEL
|
|
19
|
+
# Navigator:
|
|
20
|
+
from navconfig import config, BASE_DIR
|
|
21
|
+
from navconfig.logging import logging
|
|
22
|
+
from navigator_session import get_session
|
|
23
|
+
# Auth
|
|
24
|
+
from navigator_auth.decorators import (
|
|
25
|
+
is_authenticated,
|
|
26
|
+
)
|
|
27
|
+
from navigator_auth.conf import AUTH_SESSION_OBJECT
|
|
28
|
+
# Tasker:
|
|
29
|
+
from navigator.background import (
|
|
30
|
+
BackgroundService,
|
|
31
|
+
TaskWrapper,
|
|
32
|
+
JobRecord
|
|
33
|
+
)
|
|
34
|
+
from navigator.services.ws import WebSocketManager
|
|
35
|
+
from navigator.applications.base import BaseApplication # pylint: disable=E0611
|
|
36
|
+
from navigator.views import BaseView
|
|
37
|
+
from navigator.responses import JSONResponse
|
|
38
|
+
from navigator.types import WebApp # pylint: disable=E0611
|
|
39
|
+
from navigator.conf import CACHE_URL, default_dsn
|
|
40
|
+
# Parrot:
|
|
41
|
+
from ...bots.agent import BasicAgent
|
|
42
|
+
from ...tools.abstract import AbstractTool
|
|
43
|
+
from ...models.responses import AgentResponse, AIMessage
|
|
44
|
+
from ...clients.gpt import OpenAIClient
|
|
45
|
+
from ...clients.claude import ClaudeClient
|
|
46
|
+
from ...conf import STATIC_DIR, AGENTS_BOTS_PROMPT_DIR, AGENTS_DIR
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RedisWriter:
|
|
50
|
+
"""RedisWriter class."""
|
|
51
|
+
def __init__(self):
|
|
52
|
+
self.conn = AsyncDB('redis', dsn=CACHE_URL)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def redis(self):
|
|
56
|
+
"""Get Redis Connection."""
|
|
57
|
+
return self.conn
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class JobWSManager(WebSocketManager):
|
|
61
|
+
"""
|
|
62
|
+
Extends the generic WebSocketManager with one helper that sends
|
|
63
|
+
a direct message to the user owning a finished job.
|
|
64
|
+
"""
|
|
65
|
+
async def notify_job_done(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
user_id: int | str,
|
|
69
|
+
job_id: str,
|
|
70
|
+
status: str,
|
|
71
|
+
result: Optional[Any] = None,
|
|
72
|
+
error: Optional[str] = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Push a JSON message to every open WS belonging to `user_id`.
|
|
76
|
+
"""
|
|
77
|
+
payload = {
|
|
78
|
+
"type": "job_status",
|
|
79
|
+
"job_id": job_id,
|
|
80
|
+
"status": status, # "done" / "failed"
|
|
81
|
+
"result": result,
|
|
82
|
+
"error": error,
|
|
83
|
+
}
|
|
84
|
+
message = json_encoder(payload)
|
|
85
|
+
delivered = 0
|
|
86
|
+
for ws, info in list(self.clients.items()):
|
|
87
|
+
if ws.closed:
|
|
88
|
+
continue
|
|
89
|
+
if info.get("user_id") == str(user_id):
|
|
90
|
+
await ws.send_str(message)
|
|
91
|
+
delivered += 1
|
|
92
|
+
|
|
93
|
+
if delivered == 0:
|
|
94
|
+
self.logger.debug(
|
|
95
|
+
"No active WS for user_id=%s when job %s finished", user_id, job_id
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def auth_groups(
|
|
99
|
+
allowed: Sequence[str]
|
|
100
|
+
) -> Callable[[Callable[..., Awaitable]], Callable[..., Awaitable]]:
|
|
101
|
+
"""Ensure the request is authenticated *and* the user belongs
|
|
102
|
+
to at least one of `allowed` groups.
|
|
103
|
+
"""
|
|
104
|
+
def decorator(fn: Union[Any, Any]) -> Any:
|
|
105
|
+
# 1️⃣ first wrap the target function with the base auth check
|
|
106
|
+
fn = AgentHandler.service_auth(fn)
|
|
107
|
+
@functools.wraps(fn)
|
|
108
|
+
async def wrapper(self, *args, **kwargs):
|
|
109
|
+
# At this point `service_auth` has already:
|
|
110
|
+
# * verified the session
|
|
111
|
+
# * populated `self._session` and `self._superuser`
|
|
112
|
+
if self._superuser:
|
|
113
|
+
# If the user is a superuser, skip group checks
|
|
114
|
+
return await fn(self, *args, **kwargs)
|
|
115
|
+
# Now add the group check
|
|
116
|
+
user_groups = set(self._session.get("groups", []))
|
|
117
|
+
if not user_groups.intersection(allowed):
|
|
118
|
+
self.error(
|
|
119
|
+
response={
|
|
120
|
+
"error": "Forbidden",
|
|
121
|
+
"message": f"User lacks required group(s): {allowed}"
|
|
122
|
+
},
|
|
123
|
+
status=403
|
|
124
|
+
)
|
|
125
|
+
return await fn(self, *args, **kwargs)
|
|
126
|
+
return wrapper
|
|
127
|
+
return decorator
|
|
128
|
+
|
|
129
|
+
def auth_by_attribute(
|
|
130
|
+
allowed: Sequence[str],
|
|
131
|
+
attribute: str = 'job_code'
|
|
132
|
+
) -> Callable[[Callable[..., Awaitable]], Callable[..., Awaitable]]:
|
|
133
|
+
"""Ensure the request is authenticated *and* the user belongs
|
|
134
|
+
to at least one of `allowed` Job Codes.
|
|
135
|
+
"""
|
|
136
|
+
def decorator(fn: Union[Any, Any]) -> Any:
|
|
137
|
+
# 1️⃣ first wrap the target function with the base auth check
|
|
138
|
+
fn = AgentHandler.service_auth(fn)
|
|
139
|
+
@functools.wraps(fn)
|
|
140
|
+
async def wrapper(self, *args, **kwargs):
|
|
141
|
+
# At this point `service_auth` has already:
|
|
142
|
+
# * verified the session
|
|
143
|
+
# * populated `self._session` and `self._superuser`
|
|
144
|
+
if self._superuser:
|
|
145
|
+
# If the user is a superuser, skip job code checks
|
|
146
|
+
return await fn(self, *args, **kwargs)
|
|
147
|
+
# Now add the jobcode check
|
|
148
|
+
userinfo = self._session.get(AUTH_SESSION_OBJECT, {})
|
|
149
|
+
attr = userinfo.get(attribute, None)
|
|
150
|
+
if not attr:
|
|
151
|
+
self.error(
|
|
152
|
+
response={
|
|
153
|
+
"error": "Forbidden",
|
|
154
|
+
"message": f"User does not have a valid {attribute}."
|
|
155
|
+
},
|
|
156
|
+
status=403
|
|
157
|
+
)
|
|
158
|
+
if not attr in allowed:
|
|
159
|
+
self.error(
|
|
160
|
+
response={
|
|
161
|
+
"error": "Forbidden",
|
|
162
|
+
"message": f"User lacks required attribute(s) ({attribute})."
|
|
163
|
+
},
|
|
164
|
+
status=403
|
|
165
|
+
)
|
|
166
|
+
return await fn(self, *args, **kwargs)
|
|
167
|
+
return wrapper
|
|
168
|
+
return decorator
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@is_authenticated()
|
|
172
|
+
class AgentHandler(BaseView):
|
|
173
|
+
"""Abstract class for chatbot/agent handlers.
|
|
174
|
+
|
|
175
|
+
Provide a complete abstraction for exposing AI Agents as a REST API.
|
|
176
|
+
"""
|
|
177
|
+
app: web.Application = None
|
|
178
|
+
agent_name: str = "NextStop"
|
|
179
|
+
agent_id: str = "nextstop"
|
|
180
|
+
_tools: List[AbstractTool] = []
|
|
181
|
+
_agent: BasicAgent = None
|
|
182
|
+
_use_llm: str = 'google'
|
|
183
|
+
_use_model: str = 'gemini-2.5-pro'
|
|
184
|
+
# signals
|
|
185
|
+
on_startup: Optional[Callable] = None
|
|
186
|
+
on_shutdown: Optional[Callable] = None
|
|
187
|
+
on_cleanup: Optional[Callable] = None
|
|
188
|
+
|
|
189
|
+
# Define base routes - can be overridden in subclasses
|
|
190
|
+
base_route: str = None # e.g., "/api/v1/agent/{agent_name}"
|
|
191
|
+
additional_routes: List[Dict[str, Any]] = [] # Custom routes
|
|
192
|
+
_agent_class: type = BasicAgent # Default agent class
|
|
193
|
+
_agent_response: type = AgentResponse # Default response type
|
|
194
|
+
|
|
195
|
+
def __init__(
|
|
196
|
+
self,
|
|
197
|
+
request: web.Request = None,
|
|
198
|
+
*args,
|
|
199
|
+
app: web.Application = None,
|
|
200
|
+
**kwargs
|
|
201
|
+
):
|
|
202
|
+
if request is not None:
|
|
203
|
+
super().__init__(request, *args, **kwargs)
|
|
204
|
+
self.logger = logging.getLogger(
|
|
205
|
+
f"{self.__class__.__module__}.{self.__class__.__name__}"
|
|
206
|
+
)
|
|
207
|
+
self.redis = RedisWriter()
|
|
208
|
+
self.gcs = None # GCS Manager
|
|
209
|
+
self.s3 = None # S3 Manager
|
|
210
|
+
# Session and User ID
|
|
211
|
+
self._session: Optional[Dict[str, Any]] = None
|
|
212
|
+
self._userid: Optional[str] = None
|
|
213
|
+
# Temporal Agent Uploader
|
|
214
|
+
self.temp_dir = self.create_temp_directory()
|
|
215
|
+
self.app = app
|
|
216
|
+
self._program: str = kwargs.pop('program_slug', 'parrot') # Default program slug
|
|
217
|
+
|
|
218
|
+
def set_program(self, program_slug: str) -> None:
|
|
219
|
+
"""Set the program slug for the agent."""
|
|
220
|
+
self._program = program_slug
|
|
221
|
+
|
|
222
|
+
def setup(
|
|
223
|
+
self,
|
|
224
|
+
app: Union[WebApp, web.Application],
|
|
225
|
+
route: List[Dict[Any, str]] = None
|
|
226
|
+
) -> None:
|
|
227
|
+
"""Setup the handler with the application and route.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
app (Union[WebApp, web.Application]): The web application instance.
|
|
231
|
+
route (List[Dict[Any, str]]): The route configuration.
|
|
232
|
+
"""
|
|
233
|
+
if isinstance(app, BaseApplication):
|
|
234
|
+
app = app.get_app()
|
|
235
|
+
elif isinstance(app, WebApp):
|
|
236
|
+
app = app # register the app into the Extension
|
|
237
|
+
else:
|
|
238
|
+
raise TypeError(
|
|
239
|
+
"Expected app to be an instance of BaseApplication."
|
|
240
|
+
)
|
|
241
|
+
self.app = app
|
|
242
|
+
# Register the main view class route
|
|
243
|
+
if route:
|
|
244
|
+
self.app.router.add_view(route, self.__class__)
|
|
245
|
+
elif self.base_route:
|
|
246
|
+
self.app.router.add_view(self.base_route, self.__class__)
|
|
247
|
+
|
|
248
|
+
# And register any additional custom routes
|
|
249
|
+
self._register_additional_routes()
|
|
250
|
+
|
|
251
|
+
# Tasker: Background Task Manager (if not already registered):
|
|
252
|
+
if 'background_service' not in app:
|
|
253
|
+
BackgroundService(
|
|
254
|
+
app=self.app,
|
|
255
|
+
max_workers=10,
|
|
256
|
+
queue_size=10,
|
|
257
|
+
tracker_type='redis', # Use 'redis' for Redis-based tracking
|
|
258
|
+
service_name=f"{self.agent_name}_tasker"
|
|
259
|
+
)
|
|
260
|
+
# Tool definition:
|
|
261
|
+
self.define_tools()
|
|
262
|
+
|
|
263
|
+
# Startup and shutdown callbacks
|
|
264
|
+
app.on_startup.append(self.create_agent)
|
|
265
|
+
# Register Signals:
|
|
266
|
+
if callable(self.on_startup):
|
|
267
|
+
app.on_startup.append(self.on_startup)
|
|
268
|
+
if callable(self.on_shutdown):
|
|
269
|
+
app.on_shutdown.append(self.on_shutdown)
|
|
270
|
+
if callable(self.on_cleanup):
|
|
271
|
+
app.on_cleanup.append(self.on_cleanup)
|
|
272
|
+
|
|
273
|
+
async def create_agent(self, app: web.Application):
|
|
274
|
+
self.logger.info("Starting up agent handler...")
|
|
275
|
+
# Initialize the agent
|
|
276
|
+
await self._create_agent(app)
|
|
277
|
+
|
|
278
|
+
def define_tools(self):
|
|
279
|
+
"""Define additional tools for the agent."""
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
def db_connection(
|
|
283
|
+
self,
|
|
284
|
+
driver: str = 'pg',
|
|
285
|
+
dsn: str = None,
|
|
286
|
+
credentials: dict = None
|
|
287
|
+
) -> AsyncDB:
|
|
288
|
+
"""Return a database connection."""
|
|
289
|
+
if not dsn:
|
|
290
|
+
dsn = config.get(f'{driver.upper()}_DSN', fallback=default_dsn)
|
|
291
|
+
if not dsn and credentials:
|
|
292
|
+
dsn = credentials.get('dsn', default_dsn)
|
|
293
|
+
if not dsn:
|
|
294
|
+
raise ValueError(
|
|
295
|
+
f"DSN for {driver} is not provided."
|
|
296
|
+
)
|
|
297
|
+
return AsyncDB(driver, dsn=dsn, credentials=credentials)
|
|
298
|
+
|
|
299
|
+
async def register_background_task(
|
|
300
|
+
self,
|
|
301
|
+
task: Callable[..., Awaitable],
|
|
302
|
+
request: web.Request = None,
|
|
303
|
+
done_callback: Optional[Callable[..., Awaitable]] = None,
|
|
304
|
+
*args,
|
|
305
|
+
**kwargs
|
|
306
|
+
) -> JobRecord:
|
|
307
|
+
"""Register a background task with the BackgroundService.
|
|
308
|
+
Add an optional task wrapper to handle the task execution.
|
|
309
|
+
Args:
|
|
310
|
+
task (Callable[..., Awaitable]): The task to be executed.
|
|
311
|
+
request (web.Request, optional): The request object. Defaults to None.
|
|
312
|
+
done_callback (Optional[Callable[..., Awaitable]], optional):
|
|
313
|
+
A callback to be called when the task is done. Defaults to None.
|
|
314
|
+
*args: Positional arguments to pass to the task.
|
|
315
|
+
**kwargs: Keyword arguments to pass to the task.
|
|
316
|
+
Returns:
|
|
317
|
+
JobRecord: The job record containing the task ID and other details.
|
|
318
|
+
Raises:
|
|
319
|
+
RuntimeError: If the request is not available.
|
|
320
|
+
"""
|
|
321
|
+
if not request:
|
|
322
|
+
request = self.request
|
|
323
|
+
if not request:
|
|
324
|
+
raise RuntimeError("Request is not available.")
|
|
325
|
+
# Get the BackgroundService instance
|
|
326
|
+
service: BackgroundService = request.app['background_service']
|
|
327
|
+
# Create a TaskWrapper instance
|
|
328
|
+
task = TaskWrapper(
|
|
329
|
+
*args,
|
|
330
|
+
fn=task,
|
|
331
|
+
logger=self.logger,
|
|
332
|
+
**kwargs
|
|
333
|
+
)
|
|
334
|
+
task.add_callback(done_callback)
|
|
335
|
+
# Register the task with the service
|
|
336
|
+
job = await service.submit(task)
|
|
337
|
+
self.logger.notice(
|
|
338
|
+
f"Registered background task: {task!r} with ID: {job.task_id}"
|
|
339
|
+
)
|
|
340
|
+
return job
|
|
341
|
+
|
|
342
|
+
async def find_jobs(self, request: web.Request) -> web.Response:
|
|
343
|
+
"""Return Jobs by User."""
|
|
344
|
+
# Get the BackgroundService instance
|
|
345
|
+
service: BackgroundService = request.app['background_service']
|
|
346
|
+
# get service tracker:
|
|
347
|
+
tracker = service.tracker
|
|
348
|
+
session = await self.get_user_session()
|
|
349
|
+
userid = self.get_userid(session=session)
|
|
350
|
+
if not userid:
|
|
351
|
+
return JSONResponse(
|
|
352
|
+
content=None,
|
|
353
|
+
headers={"x-message": "User ID not found in session."},
|
|
354
|
+
status=401
|
|
355
|
+
)
|
|
356
|
+
search = {
|
|
357
|
+
'user_id': userid,
|
|
358
|
+
'agent_name': self.agent_name,
|
|
359
|
+
}
|
|
360
|
+
result = await tracker.find_jobs(
|
|
361
|
+
attrs=search
|
|
362
|
+
)
|
|
363
|
+
if not result:
|
|
364
|
+
return JSONResponse(
|
|
365
|
+
content=None,
|
|
366
|
+
headers={"x-message": "No jobs found for this user."},
|
|
367
|
+
status=204
|
|
368
|
+
)
|
|
369
|
+
return JSONResponse(result)
|
|
370
|
+
|
|
371
|
+
def _register_additional_routes(self):
|
|
372
|
+
"""Register additional custom routes defined in the class."""
|
|
373
|
+
for route_config in self.additional_routes:
|
|
374
|
+
method = route_config.get('method', 'GET').upper()
|
|
375
|
+
path = route_config['path']
|
|
376
|
+
handler_name = route_config['handler']
|
|
377
|
+
|
|
378
|
+
# Get the handler method from the class
|
|
379
|
+
handler_method = getattr(self.__class__, handler_name)
|
|
380
|
+
|
|
381
|
+
# Create a wrapper that instantiates the class and calls the method
|
|
382
|
+
async def route_wrapper(request, handler_method=handler_method):
|
|
383
|
+
instance = self.__class__(request)
|
|
384
|
+
return await handler_method(instance, request)
|
|
385
|
+
|
|
386
|
+
# Add the route to the router
|
|
387
|
+
self.app.router.add_route(method, path, route_wrapper)
|
|
388
|
+
|
|
389
|
+
async def get_task_status(self, task_id: str, request: web.Request = None) -> JSONResponse:
|
|
390
|
+
"""Get the status of a background task by its ID."""
|
|
391
|
+
req = request or self.request
|
|
392
|
+
if not req:
|
|
393
|
+
raise RuntimeError("Request is not available.")
|
|
394
|
+
service: BackgroundService = req.app['background_service']
|
|
395
|
+
try:
|
|
396
|
+
job = await service.record(task_id)
|
|
397
|
+
if job:
|
|
398
|
+
return JSONResponse(
|
|
399
|
+
{
|
|
400
|
+
"task_id": job.task_id,
|
|
401
|
+
"status": job.status,
|
|
402
|
+
"result": job.result,
|
|
403
|
+
"created_at": job.created_at,
|
|
404
|
+
"started_at": job.started_at,
|
|
405
|
+
"error": job.error,
|
|
406
|
+
"stacktrace": job.stacktrace,
|
|
407
|
+
"attributes": job.attributes,
|
|
408
|
+
"finished_at": job.finished_at,
|
|
409
|
+
"name": job.name,
|
|
410
|
+
# "job": job
|
|
411
|
+
}
|
|
412
|
+
)
|
|
413
|
+
else:
|
|
414
|
+
return JSONResponse(
|
|
415
|
+
{"message": "Task not found"},
|
|
416
|
+
status=404
|
|
417
|
+
)
|
|
418
|
+
except Exception as e:
|
|
419
|
+
return JSONResponse(
|
|
420
|
+
{"error": str(e)},
|
|
421
|
+
status=500
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
def add_route(self, method: str, path: str, handler: str):
|
|
425
|
+
"""Instance method to add custom routes."""
|
|
426
|
+
if not hasattr(self, 'additional_routes'):
|
|
427
|
+
self.additional_routes = []
|
|
428
|
+
self.additional_routes.append({
|
|
429
|
+
'method': method,
|
|
430
|
+
'path': path,
|
|
431
|
+
'handler': handler
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
def create_temp_directory(self, name: str = 'documents'):
|
|
435
|
+
"""Create the temporary directory for saving Agent Documents."""
|
|
436
|
+
tmp_dir = tempfile.TemporaryDirectory()
|
|
437
|
+
# Create the "documents" subdirectory inside the temporary directory
|
|
438
|
+
p_dir = Path(tmp_dir.name).joinpath(self.agent_name, name)
|
|
439
|
+
p_dir.mkdir(parents=True, exist_ok=True)
|
|
440
|
+
return p_dir
|
|
441
|
+
|
|
442
|
+
async def get_user_session(self):
|
|
443
|
+
"""Return the user session from the request."""
|
|
444
|
+
# TODO: Add ABAC Support.
|
|
445
|
+
if not self.request:
|
|
446
|
+
raise RuntimeError("Request is not available.")
|
|
447
|
+
try:
|
|
448
|
+
session = self.request.session
|
|
449
|
+
except AttributeError:
|
|
450
|
+
session = await get_session(self.request)
|
|
451
|
+
if not session:
|
|
452
|
+
self.error(
|
|
453
|
+
response={'message': 'Session not found'},
|
|
454
|
+
status=401
|
|
455
|
+
)
|
|
456
|
+
if not session:
|
|
457
|
+
self.error(
|
|
458
|
+
response={
|
|
459
|
+
"error": "Unauthorized",
|
|
460
|
+
"message": "Hint: maybe need to login and pass Authorization token."
|
|
461
|
+
},
|
|
462
|
+
status=403
|
|
463
|
+
)
|
|
464
|
+
return session
|
|
465
|
+
|
|
466
|
+
def get_userid(
|
|
467
|
+
self,
|
|
468
|
+
session: Optional[Dict[str, Any]] = None,
|
|
469
|
+
idx: str = 'user_id'
|
|
470
|
+
) -> Optional[str]:
|
|
471
|
+
"""Return the user ID from the session."""
|
|
472
|
+
if not session:
|
|
473
|
+
session = self._session
|
|
474
|
+
if not session:
|
|
475
|
+
return None
|
|
476
|
+
if AUTH_SESSION_OBJECT in session:
|
|
477
|
+
return session[AUTH_SESSION_OBJECT][idx]
|
|
478
|
+
return session.get(idx, None)
|
|
479
|
+
|
|
480
|
+
@staticmethod
|
|
481
|
+
def service_auth(fn: Union[Any, Any]) -> Any:
|
|
482
|
+
"""Decorator to ensure the service is authenticated."""
|
|
483
|
+
async def wrapper(self, *args, **kwargs):
|
|
484
|
+
session = await self.get_user_session()
|
|
485
|
+
if not session:
|
|
486
|
+
self.error(
|
|
487
|
+
response={
|
|
488
|
+
"error": "Unauthorized",
|
|
489
|
+
"message": "Hint: maybe need to login and pass Authorization token."
|
|
490
|
+
},
|
|
491
|
+
status=403
|
|
492
|
+
)
|
|
493
|
+
# define in-request variables for session and userid
|
|
494
|
+
self._userid = self.get_userid(session)
|
|
495
|
+
self._session = session
|
|
496
|
+
# extract other user information as groups, programs and username:
|
|
497
|
+
userinfo = session.get(AUTH_SESSION_OBJECT, {})
|
|
498
|
+
self._session['email'] = userinfo.get('email', None)
|
|
499
|
+
self._session['username'] = userinfo.get('username', None)
|
|
500
|
+
self._session['programs'] = userinfo.get('programs', [])
|
|
501
|
+
self._session['groups'] = userinfo.get('groups', [])
|
|
502
|
+
self._superuser = userinfo.get('superuser', False)
|
|
503
|
+
self._session['is_superuser'] = self._superuser
|
|
504
|
+
# Set the session in the request for further use
|
|
505
|
+
## Calling post-authorization Model:
|
|
506
|
+
await self._post_auth(self, *args, **kwargs)
|
|
507
|
+
return await fn(self, *args, **kwargs)
|
|
508
|
+
return wrapper
|
|
509
|
+
|
|
510
|
+
async def _post_auth(self, *args, **kwargs):
|
|
511
|
+
"""Post-authorization Model."""
|
|
512
|
+
return True
|
|
513
|
+
|
|
514
|
+
def get_agent(self) -> Any:
|
|
515
|
+
"""Return the agent instance."""
|
|
516
|
+
return self._agent
|
|
517
|
+
|
|
518
|
+
def _create_actors(self, recipients: Union[List[dict], dict] = None) -> List:
|
|
519
|
+
if isinstance(recipients, dict):
|
|
520
|
+
recipients = [recipients]
|
|
521
|
+
if not recipients:
|
|
522
|
+
return self.error(
|
|
523
|
+
{'message': 'Recipients are required'},
|
|
524
|
+
status=400
|
|
525
|
+
)
|
|
526
|
+
rcpts = []
|
|
527
|
+
for recipient in recipients:
|
|
528
|
+
name = recipient.get('name', 'Navigator')
|
|
529
|
+
email = recipient.get('address')
|
|
530
|
+
if not email:
|
|
531
|
+
return self.error(
|
|
532
|
+
{'message': 'Address is required'},
|
|
533
|
+
status=400
|
|
534
|
+
)
|
|
535
|
+
rcpt = Actor(**{
|
|
536
|
+
"name": name,
|
|
537
|
+
"account": {
|
|
538
|
+
"address": email
|
|
539
|
+
}
|
|
540
|
+
})
|
|
541
|
+
rcpts.append(rcpt)
|
|
542
|
+
return rcpts
|
|
543
|
+
|
|
544
|
+
async def send_notification(
|
|
545
|
+
self,
|
|
546
|
+
content: str,
|
|
547
|
+
provider: str = 'telegram',
|
|
548
|
+
recipients: Union[List[dict], dict] = None,
|
|
549
|
+
**kwargs
|
|
550
|
+
) -> Any:
|
|
551
|
+
"""Return the notification provider instance."""
|
|
552
|
+
provider = kwargs.get('provider', provider).lower()
|
|
553
|
+
response = []
|
|
554
|
+
if provider == 'telegram':
|
|
555
|
+
sender = Notify(provider)
|
|
556
|
+
chat_id = kwargs.get('chat_id', config.get('TELEGRAM_CHAT_ID'))
|
|
557
|
+
chat = Chat(
|
|
558
|
+
chat_id=chat_id
|
|
559
|
+
)
|
|
560
|
+
async with sender as message: # pylint: disable=E1701 # noqa: E501
|
|
561
|
+
result = await message.send(
|
|
562
|
+
recipient=chat,
|
|
563
|
+
**kwargs
|
|
564
|
+
)
|
|
565
|
+
for r in result:
|
|
566
|
+
res = {
|
|
567
|
+
"provider": provider,
|
|
568
|
+
"message_id": r.message_id,
|
|
569
|
+
"title": r.chat.title,
|
|
570
|
+
}
|
|
571
|
+
response.append(res)
|
|
572
|
+
elif provider == 'email':
|
|
573
|
+
rcpts = self._create_actors(recipients)
|
|
574
|
+
credentials = {
|
|
575
|
+
"hostname": config.get('smtp_host'),
|
|
576
|
+
"port": config.get('smtp_port'),
|
|
577
|
+
"username": config.get('smtp_host_user'),
|
|
578
|
+
"password": config.get('smtp_host_password')
|
|
579
|
+
}
|
|
580
|
+
sender = Notify(provider, **credentials)
|
|
581
|
+
async with sender as message: # pylint: disable=E1701 # noqa: E501
|
|
582
|
+
result = await message.send(
|
|
583
|
+
recipient=rcpts,
|
|
584
|
+
**kwargs,
|
|
585
|
+
**credentials
|
|
586
|
+
)
|
|
587
|
+
for r in result:
|
|
588
|
+
res = {
|
|
589
|
+
"provider": provider,
|
|
590
|
+
"status": r[1],
|
|
591
|
+
}
|
|
592
|
+
response.append(res)
|
|
593
|
+
elif provider == 'teams':
|
|
594
|
+
# Support for private messages:
|
|
595
|
+
sender = Teams(as_user=True)
|
|
596
|
+
if recipients:
|
|
597
|
+
rcpts = self._create_actors(recipients)
|
|
598
|
+
else:
|
|
599
|
+
# by Teams Channel
|
|
600
|
+
default_teams_id = config.get('MS_TEAMS_DEFAULT_TEAMS_ID')
|
|
601
|
+
default_chat_id = config.get('MS_TEAMS_DEFAULT_CHANNEL_ID')
|
|
602
|
+
teams_id = kwargs.pop('teams_id', default_teams_id)
|
|
603
|
+
chat_id = kwargs.pop('chat_id', default_chat_id)
|
|
604
|
+
rcpts = TeamsChannel(
|
|
605
|
+
name="General",
|
|
606
|
+
team_id=teams_id,
|
|
607
|
+
channel_id=chat_id
|
|
608
|
+
)
|
|
609
|
+
card = TeamsCard(
|
|
610
|
+
title=kwargs.get('title'),
|
|
611
|
+
summary=kwargs.get('summary'),
|
|
612
|
+
text=kwargs.get('message'),
|
|
613
|
+
sections=kwargs.get('sections', [])
|
|
614
|
+
)
|
|
615
|
+
async with sender as message: # pylint: disable=E1701 # noqa: E501
|
|
616
|
+
result = await message.send(
|
|
617
|
+
recipient=rcpts,
|
|
618
|
+
message=card
|
|
619
|
+
)
|
|
620
|
+
for r in result:
|
|
621
|
+
res = {
|
|
622
|
+
"message_id": r['id'],
|
|
623
|
+
"webUrl": r['webUrl']
|
|
624
|
+
}
|
|
625
|
+
response.append(res)
|
|
626
|
+
elif provider == 'ses':
|
|
627
|
+
credentials = {
|
|
628
|
+
"aws_access_key_id": config.get('AWS_ACCESS_KEY_ID'),
|
|
629
|
+
"aws_secret_access_key": config.get('AWS_SECRET_ACCESS_KEY'),
|
|
630
|
+
"aws_region_name": config.get('AWS_REGION_NAME'),
|
|
631
|
+
"sender_email": config.get('SENDER_EMAIL')
|
|
632
|
+
}
|
|
633
|
+
message = {
|
|
634
|
+
"provider": "ses",
|
|
635
|
+
"message": content,
|
|
636
|
+
"template": 'email_applied.html',
|
|
637
|
+
**credentials,
|
|
638
|
+
}
|
|
639
|
+
async with NotifyClient(
|
|
640
|
+
redis_url=NOTIFY_REDIS
|
|
641
|
+
) as client:
|
|
642
|
+
# Stream but using Wrapper:
|
|
643
|
+
await client.stream(
|
|
644
|
+
message,
|
|
645
|
+
stream=NOTIFY_WORKER_STREAM,
|
|
646
|
+
use_wrapper=True
|
|
647
|
+
)
|
|
648
|
+
elif provider == 'mail':
|
|
649
|
+
rcpts = self._create_actors(recipients)
|
|
650
|
+
name = kwargs.pop('name', 'Navigator')
|
|
651
|
+
email = kwargs.pop('address')
|
|
652
|
+
message = {
|
|
653
|
+
"provider": 'email',
|
|
654
|
+
"recipient": {
|
|
655
|
+
"name": name,
|
|
656
|
+
"account": {
|
|
657
|
+
"address": email
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
"message": 'Congratulations!',
|
|
661
|
+
"template": 'email_applied.html'
|
|
662
|
+
**kwargs
|
|
663
|
+
}
|
|
664
|
+
async with NotifyClient(
|
|
665
|
+
redis_url=NOTIFY_REDIS
|
|
666
|
+
) as client:
|
|
667
|
+
for recipient in rcpts:
|
|
668
|
+
message['recipient'] = [recipient]
|
|
669
|
+
await client.publish(
|
|
670
|
+
message,
|
|
671
|
+
channel=NOTIFY_CHANNEL,
|
|
672
|
+
use_wrapper=True
|
|
673
|
+
)
|
|
674
|
+
response = "Message sent"
|
|
675
|
+
else:
|
|
676
|
+
payload = {
|
|
677
|
+
"provider": provider,
|
|
678
|
+
**kwargs
|
|
679
|
+
}
|
|
680
|
+
# Create a NotifyClient instance
|
|
681
|
+
async with NotifyClient(redis_url=NOTIFY_REDIS) as client:
|
|
682
|
+
for recipient in recipients:
|
|
683
|
+
payload['recipient'] = [recipient]
|
|
684
|
+
# Stream but using Wrapper:
|
|
685
|
+
await client.stream(
|
|
686
|
+
payload,
|
|
687
|
+
stream=NOTIFY_WORKER_STREAM,
|
|
688
|
+
use_wrapper=True
|
|
689
|
+
)
|
|
690
|
+
return response
|
|
691
|
+
|
|
692
|
+
async def _handle_uploads(
|
|
693
|
+
self,
|
|
694
|
+
key: str,
|
|
695
|
+
ext: str = '.csv',
|
|
696
|
+
mime_type: str = 'text/csv',
|
|
697
|
+
preserve_filenames: bool = True,
|
|
698
|
+
as_bytes: bool = False
|
|
699
|
+
) -> Tuple[List[dict], dict]:
|
|
700
|
+
"""handle file uploads."""
|
|
701
|
+
# Check if Content-Type is correctly set
|
|
702
|
+
content_type = self.request.headers.get('Content-Type', '')
|
|
703
|
+
if 'multipart/form-data' not in content_type:
|
|
704
|
+
raise web.HTTPUnsupportedMediaType(
|
|
705
|
+
text='Invalid Content-Type. Use multipart/form-data',
|
|
706
|
+
content_type='application/json'
|
|
707
|
+
)
|
|
708
|
+
form_data = {} # return any other form data, if exists.
|
|
709
|
+
uploaded_files_info = []
|
|
710
|
+
try:
|
|
711
|
+
reader = await self.request.multipart()
|
|
712
|
+
except KeyError:
|
|
713
|
+
raise FileNotFoundError(
|
|
714
|
+
"No files found in the request. Please upload a file."
|
|
715
|
+
)
|
|
716
|
+
# Process each part of the multipart request
|
|
717
|
+
async for part in reader:
|
|
718
|
+
if part.filename:
|
|
719
|
+
if key and part.name != key:
|
|
720
|
+
continue
|
|
721
|
+
# Create a temporary file for each uploaded file
|
|
722
|
+
file_ext = Path(part.filename).suffix or ext
|
|
723
|
+
if preserve_filenames:
|
|
724
|
+
# Use the original filename and save in the temp directory
|
|
725
|
+
temp_file_path = Path(tempfile.gettempdir()) / part.filename
|
|
726
|
+
else:
|
|
727
|
+
with tempfile.NamedTemporaryFile(
|
|
728
|
+
delete=False,
|
|
729
|
+
dir=tempfile.gettempdir(),
|
|
730
|
+
suffix=file_ext
|
|
731
|
+
) as temp_file:
|
|
732
|
+
temp_file_path = Path(temp_file.name)
|
|
733
|
+
# save as a byte stream if required
|
|
734
|
+
file_content = None
|
|
735
|
+
if as_bytes:
|
|
736
|
+
# Read the file content as bytes
|
|
737
|
+
file_content = BytesIO()
|
|
738
|
+
while True:
|
|
739
|
+
chunk = await part.read_chunk()
|
|
740
|
+
if not chunk:
|
|
741
|
+
break
|
|
742
|
+
file_content.write(chunk)
|
|
743
|
+
# Write the bytes to the temp file
|
|
744
|
+
async with aiofiles.open(temp_file_path, 'wb') as f:
|
|
745
|
+
await f.write(file_content.getvalue())
|
|
746
|
+
else:
|
|
747
|
+
# Write the file content
|
|
748
|
+
with temp_file_path.open("wb") as f:
|
|
749
|
+
while True:
|
|
750
|
+
chunk = await part.read_chunk()
|
|
751
|
+
if not chunk:
|
|
752
|
+
break
|
|
753
|
+
f.write(chunk)
|
|
754
|
+
# Get Content-Type header
|
|
755
|
+
mime_type = part.headers.get('Content-Type', mime_type)
|
|
756
|
+
# Store file information
|
|
757
|
+
file_info = {
|
|
758
|
+
'filename': part.filename,
|
|
759
|
+
'path': str(temp_file_path),
|
|
760
|
+
'content_type': mime_type,
|
|
761
|
+
'size': temp_file_path.stat().st_size
|
|
762
|
+
}
|
|
763
|
+
if file_content is not None:
|
|
764
|
+
file_info['content'] = file_content
|
|
765
|
+
uploaded_files_info.append(file_info)
|
|
766
|
+
else:
|
|
767
|
+
# If it's a form field, add it to the dictionary
|
|
768
|
+
form_field_name = part.name
|
|
769
|
+
form_field_value = await part.text()
|
|
770
|
+
form_data[form_field_name] = form_field_value
|
|
771
|
+
# Check if any files were uploaded
|
|
772
|
+
if not uploaded_files_info:
|
|
773
|
+
raise FileNotFoundError(
|
|
774
|
+
"No files found in the request. Please upload a file."
|
|
775
|
+
)
|
|
776
|
+
# Return the list of uploaded files and any other form data
|
|
777
|
+
return uploaded_files_info, form_data
|
|
778
|
+
|
|
779
|
+
async def _create_agent(
|
|
780
|
+
self,
|
|
781
|
+
app: web.Application
|
|
782
|
+
) -> BasicAgent:
|
|
783
|
+
"""Create and configure a BasicAgent instance."""
|
|
784
|
+
try:
|
|
785
|
+
print('AGENTE > ', self._use_llm, self._use_model)
|
|
786
|
+
agent = self._agent_class(
|
|
787
|
+
name=self.agent_name,
|
|
788
|
+
tools=self._tools,
|
|
789
|
+
llm=self._use_llm,
|
|
790
|
+
model=self._use_model
|
|
791
|
+
)
|
|
792
|
+
print('AGENTE 2 > ', agent)
|
|
793
|
+
agent.set_response(self._agent_response)
|
|
794
|
+
await agent.configure()
|
|
795
|
+
# define the main agent:
|
|
796
|
+
self._agent = agent
|
|
797
|
+
# Store the agent in the application context
|
|
798
|
+
app[self.agent_id] = agent
|
|
799
|
+
self.logger.info(
|
|
800
|
+
f"Agent {self.agent_name} created and configured successfully."
|
|
801
|
+
)
|
|
802
|
+
return agent
|
|
803
|
+
except Exception as e:
|
|
804
|
+
raise RuntimeError(
|
|
805
|
+
f"Failed to create agent {self.agent_name}: {str(e)}"
|
|
806
|
+
) from e
|
|
807
|
+
|
|
808
|
+
async def open_prompt(self, prompt_file: str = None) -> str:
|
|
809
|
+
"""
|
|
810
|
+
Opens a prompt file and returns its content.
|
|
811
|
+
"""
|
|
812
|
+
if not prompt_file:
|
|
813
|
+
raise ValueError("No prompt file specified.")
|
|
814
|
+
file = AGENTS_DIR.joinpath(self.agent_id, 'prompts', prompt_file)
|
|
815
|
+
try:
|
|
816
|
+
async with aiofiles.open(file, 'r') as f:
|
|
817
|
+
content = await f.read()
|
|
818
|
+
return content
|
|
819
|
+
except Exception as e:
|
|
820
|
+
raise RuntimeError(
|
|
821
|
+
f"Failed to read prompt file {prompt_file}: {e}"
|
|
822
|
+
) from e
|
|
823
|
+
|
|
824
|
+
async def ask_agent(
|
|
825
|
+
self,
|
|
826
|
+
query: str = None,
|
|
827
|
+
prompt_file: str = None,
|
|
828
|
+
*args,
|
|
829
|
+
**kwargs
|
|
830
|
+
) -> Tuple[AgentResponse, AIMessage]:
|
|
831
|
+
"""
|
|
832
|
+
Asks the agent a question and returns the response.
|
|
833
|
+
"""
|
|
834
|
+
if not self._agent:
|
|
835
|
+
agent = self.request.app[self.agent_id]
|
|
836
|
+
else:
|
|
837
|
+
agent = self._agent
|
|
838
|
+
if not agent:
|
|
839
|
+
raise RuntimeError(
|
|
840
|
+
f"Agent {self.agent_name} is not initialized or not found."
|
|
841
|
+
)
|
|
842
|
+
userid = self._userid if self._userid else self.request.session.get('user_id', None)
|
|
843
|
+
if not userid:
|
|
844
|
+
raise RuntimeError(
|
|
845
|
+
"User ID is not set in the session."
|
|
846
|
+
)
|
|
847
|
+
if not agent:
|
|
848
|
+
raise RuntimeError(
|
|
849
|
+
f"Agent {self.agent_name} is not initialized or not found."
|
|
850
|
+
)
|
|
851
|
+
# query:
|
|
852
|
+
if not query:
|
|
853
|
+
# extract the query from the prompt file if provided:
|
|
854
|
+
if prompt_file:
|
|
855
|
+
query = await self.open_prompt(prompt_file)
|
|
856
|
+
elif hasattr(self.request, 'query') and 'query' in self.request.query:
|
|
857
|
+
query = self.request.query.get('query', None)
|
|
858
|
+
elif hasattr(self.request, 'json'):
|
|
859
|
+
data = await self.request.json()
|
|
860
|
+
query = data.get('query', None)
|
|
861
|
+
elif hasattr(self.request, 'data'):
|
|
862
|
+
data = await self.request.data()
|
|
863
|
+
query = data.get('query', None)
|
|
864
|
+
elif hasattr(self.request, 'text'):
|
|
865
|
+
query = self.request.text
|
|
866
|
+
else:
|
|
867
|
+
raise ValueError(
|
|
868
|
+
"No query provided and no prompt file specified."
|
|
869
|
+
)
|
|
870
|
+
if not query:
|
|
871
|
+
raise ValueError(
|
|
872
|
+
"No query provided or found in the request."
|
|
873
|
+
)
|
|
874
|
+
try:
|
|
875
|
+
response = await agent.conversation(
|
|
876
|
+
question=query,
|
|
877
|
+
max_tokens=8192
|
|
878
|
+
)
|
|
879
|
+
if isinstance(response, Exception):
|
|
880
|
+
raise response
|
|
881
|
+
except Exception as e:
|
|
882
|
+
print(f"Error invoking agent: {e}")
|
|
883
|
+
raise RuntimeError(
|
|
884
|
+
f"Failed to generate report due to an error in the agent invocation: {e}"
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
# Create the response object
|
|
888
|
+
final_report = response.output.strip()
|
|
889
|
+
# when final report is made, then generate the transcript, pdf and podcast:
|
|
890
|
+
response_data = self._agent_response(
|
|
891
|
+
user_id=str(userid),
|
|
892
|
+
agent_name=self.agent_name,
|
|
893
|
+
attributes=kwargs.pop('attributes', {}),
|
|
894
|
+
data=final_report,
|
|
895
|
+
status="success",
|
|
896
|
+
created_at=datetime.now(),
|
|
897
|
+
output=response.output,
|
|
898
|
+
**kwargs
|
|
899
|
+
)
|
|
900
|
+
return response_data, response
|