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,60 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional, Callable, List
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import uuid
|
|
4
|
+
from asyncdb.models import Model, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AgentSchedule(Model):
|
|
8
|
+
"""
|
|
9
|
+
Database model for storing agent schedules.
|
|
10
|
+
|
|
11
|
+
SQL Table Creation:
|
|
12
|
+
CREATE TABLE IF NOT EXISTS navigator.agents_scheduler (
|
|
13
|
+
schedule_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
14
|
+
agent_id VARCHAR NOT NULL,
|
|
15
|
+
agent_name VARCHAR NOT NULL,
|
|
16
|
+
prompt TEXT,
|
|
17
|
+
method_name VARCHAR,
|
|
18
|
+
schedule_type VARCHAR NOT NULL,
|
|
19
|
+
schedule_config JSONB NOT NULL,
|
|
20
|
+
enabled BOOLEAN DEFAULT TRUE,
|
|
21
|
+
created_by INTEGER,
|
|
22
|
+
created_email VARCHAR,
|
|
23
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
24
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
25
|
+
last_run TIMESTAMP WITH TIME ZONE,
|
|
26
|
+
next_run TIMESTAMP WITH TIME ZONE,
|
|
27
|
+
run_count INTEGER DEFAULT 0,
|
|
28
|
+
metadata JSONB DEFAULT '{}'::JSONB,
|
|
29
|
+
is_crew BOOLEAN DEFAULT FALSE,
|
|
30
|
+
send_result JSONB DEFAULT '{}'::JSONB
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE INDEX idx_agents_scheduler_enabled ON navigator.agents_scheduler(enabled);
|
|
34
|
+
CREATE INDEX idx_agents_scheduler_agent ON navigator.agents_scheduler(agent_name);
|
|
35
|
+
"""
|
|
36
|
+
schedule_id: uuid.UUID = Field(primary_key=True, default_factory=uuid.uuid4)
|
|
37
|
+
agent_id: str = Field(required=True)
|
|
38
|
+
agent_name: str = Field(required=True)
|
|
39
|
+
prompt: Optional[str] = Field(required=False)
|
|
40
|
+
method_name: Optional[str] = Field(required=False)
|
|
41
|
+
schedule_type: str = Field(required=True)
|
|
42
|
+
schedule_config: dict = Field(required=True, default_factory=dict)
|
|
43
|
+
enabled: bool = Field(required=False, default=True)
|
|
44
|
+
created_by: Optional[int] = Field(required=False)
|
|
45
|
+
created_email: Optional[str] = Field(required=False)
|
|
46
|
+
created_at: datetime = Field(required=False, default_factory=datetime.now)
|
|
47
|
+
updated_at: datetime = Field(required=False, default_factory=datetime.now)
|
|
48
|
+
last_run: Optional[datetime] = Field(required=False)
|
|
49
|
+
next_run: Optional[datetime] = Field(required=False)
|
|
50
|
+
run_count: int = Field(required=False, default=0)
|
|
51
|
+
metadata: dict = Field(required=False, default_factory=dict)
|
|
52
|
+
is_crew: bool = Field(required=False, default=False)
|
|
53
|
+
send_result: dict = Field(required=False, default_factory=dict)
|
|
54
|
+
|
|
55
|
+
class Meta:
|
|
56
|
+
driver = 'pg'
|
|
57
|
+
name = "agents_scheduler"
|
|
58
|
+
schema = "navigator"
|
|
59
|
+
strict = True
|
|
60
|
+
frozen = False
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Security utilities for AI-Parrot.
|
|
3
|
+
"""
|
|
4
|
+
from .prompt_injection import (
|
|
5
|
+
PromptInjectionDetector,
|
|
6
|
+
SecurityEventLogger,
|
|
7
|
+
ThreatLevel,
|
|
8
|
+
PromptInjectionException
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'PromptInjectionDetector',
|
|
13
|
+
'SecurityEventLogger',
|
|
14
|
+
'ThreatLevel',
|
|
15
|
+
'PromptInjectionException'
|
|
16
|
+
]
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt Injection Detection and Protection.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Optional, Dict, Any, List, Tuple
|
|
5
|
+
from enum import Enum
|
|
6
|
+
import re
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from navconfig.logging import logging
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ThreatLevel(Enum):
|
|
12
|
+
"""Severity levels for detected threats."""
|
|
13
|
+
LOW = "low"
|
|
14
|
+
MEDIUM = "medium"
|
|
15
|
+
HIGH = "high"
|
|
16
|
+
CRITICAL = "critical"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PromptInjectionException(Exception):
|
|
20
|
+
"""Raised when a critical prompt injection is detected in strict mode."""
|
|
21
|
+
def __init__(self, message: str, threats: List[Dict], original_input: str):
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.threats = threats
|
|
24
|
+
self.original_input = original_input
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PromptInjectionDetector:
|
|
28
|
+
"""
|
|
29
|
+
Detects and mitigates prompt injection attempts in user questions.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# CRITICAL: Direct instruction override attempts
|
|
33
|
+
CRITICAL_PATTERNS = [
|
|
34
|
+
(
|
|
35
|
+
re.compile(r'ignore\s+(all\s+)?(previous|above|prior)\s+instructions?', re.IGNORECASE),
|
|
36
|
+
"Direct instruction override"
|
|
37
|
+
),
|
|
38
|
+
(
|
|
39
|
+
re.compile(r'forget\s+(everything|all|your\s+(instructions?|rules?|prompt))', re.IGNORECASE),
|
|
40
|
+
"Memory wipe attempt"
|
|
41
|
+
),
|
|
42
|
+
(
|
|
43
|
+
re.compile(r'you\s+are\s+now\s+(a|an|no\s+longer)', re.IGNORECASE),
|
|
44
|
+
"Role hijacking"
|
|
45
|
+
),
|
|
46
|
+
(
|
|
47
|
+
re.compile(r'disregard\s+(all\s+)?(previous|above|prior|your)', re.IGNORECASE),
|
|
48
|
+
"Disregard command"
|
|
49
|
+
),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
# HIGH: System impersonation
|
|
53
|
+
HIGH_PATTERNS = [
|
|
54
|
+
(
|
|
55
|
+
re.compile(r'system\s*:\s*', re.IGNORECASE),
|
|
56
|
+
"System role impersonation"
|
|
57
|
+
),
|
|
58
|
+
(
|
|
59
|
+
re.compile(r'<\s*/?system\s*>', re.IGNORECASE),
|
|
60
|
+
"System tag injection"
|
|
61
|
+
),
|
|
62
|
+
(
|
|
63
|
+
re.compile(r'\[SYSTEM\]|\(SYSTEM\)|【SYSTEM】', re.IGNORECASE),
|
|
64
|
+
"System marker injection"
|
|
65
|
+
),
|
|
66
|
+
(
|
|
67
|
+
re.compile(r'new\s+instructions?:\s*', re.IGNORECASE),
|
|
68
|
+
"Instruction replacement"
|
|
69
|
+
),
|
|
70
|
+
(
|
|
71
|
+
re.compile(r'assistant\s*:\s*ignore', re.IGNORECASE),
|
|
72
|
+
"Assistant role injection"
|
|
73
|
+
),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
# MEDIUM: Prompt extraction attempts
|
|
77
|
+
MEDIUM_PATTERNS = [
|
|
78
|
+
(
|
|
79
|
+
re.compile(r'(reveal|show|print|display|output)\s+(your\s+)?(prompt|instructions|system\s+message)', re.IGNORECASE),
|
|
80
|
+
"Prompt extraction attempt"
|
|
81
|
+
),
|
|
82
|
+
(
|
|
83
|
+
re.compile(r'what\s+(is|are)\s+your\s+(original\s+)?(instructions?|prompt|rules?)', re.IGNORECASE),
|
|
84
|
+
"Instruction disclosure request"
|
|
85
|
+
),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
def __init__(self, logger: Optional[logging.Logger] = None):
|
|
89
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
90
|
+
|
|
91
|
+
def detect_threats(self, text: str) -> List[Dict[str, Any]]:
|
|
92
|
+
"""
|
|
93
|
+
Scan text for prompt injection patterns.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List of detected threats with details
|
|
97
|
+
"""
|
|
98
|
+
if not text or not isinstance(text, str):
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
threats = []
|
|
102
|
+
|
|
103
|
+
# Check CRITICAL patterns
|
|
104
|
+
for pattern, description in self.CRITICAL_PATTERNS:
|
|
105
|
+
if match := pattern.search(text):
|
|
106
|
+
threats.append({
|
|
107
|
+
'level': ThreatLevel.CRITICAL,
|
|
108
|
+
'pattern': pattern.pattern,
|
|
109
|
+
'description': description,
|
|
110
|
+
'matched_text': match.group(0),
|
|
111
|
+
'position': match.span()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
# Check HIGH patterns
|
|
115
|
+
for pattern, description in self.HIGH_PATTERNS:
|
|
116
|
+
if match := pattern.search(text):
|
|
117
|
+
threats.append({
|
|
118
|
+
'level': ThreatLevel.HIGH,
|
|
119
|
+
'pattern': pattern.pattern,
|
|
120
|
+
'description': description,
|
|
121
|
+
'matched_text': match.group(0),
|
|
122
|
+
'position': match.span()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
# Check MEDIUM patterns
|
|
126
|
+
for pattern, description in self.MEDIUM_PATTERNS:
|
|
127
|
+
if match := pattern.search(text):
|
|
128
|
+
threats.append({
|
|
129
|
+
'level': ThreatLevel.MEDIUM,
|
|
130
|
+
'pattern': pattern.pattern,
|
|
131
|
+
'description': description,
|
|
132
|
+
'matched_text': match.group(0),
|
|
133
|
+
'position': match.span()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
return threats
|
|
137
|
+
|
|
138
|
+
def sanitize(
|
|
139
|
+
self,
|
|
140
|
+
text: str,
|
|
141
|
+
strict: bool = True,
|
|
142
|
+
replacement: str = "[FILTERED_CONTENT]"
|
|
143
|
+
) -> Tuple[str, List[Dict[str, Any]]]:
|
|
144
|
+
"""
|
|
145
|
+
Sanitize text by replacing detected patterns.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
text: Input text to sanitize
|
|
149
|
+
strict: If True, replace patterns; if False, only detect
|
|
150
|
+
replacement: Text to use for replacements
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Tuple of (sanitized_text, detected_threats)
|
|
154
|
+
"""
|
|
155
|
+
threats = self.detect_threats(text)
|
|
156
|
+
|
|
157
|
+
if not strict or not threats:
|
|
158
|
+
return text, threats
|
|
159
|
+
|
|
160
|
+
sanitized = text
|
|
161
|
+
|
|
162
|
+
# In strict mode, replace CRITICAL and HIGH patterns
|
|
163
|
+
for pattern, _ in self.CRITICAL_PATTERNS + self.HIGH_PATTERNS:
|
|
164
|
+
sanitized = pattern.sub(replacement, sanitized)
|
|
165
|
+
|
|
166
|
+
return sanitized, threats
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class SecurityEventLogger:
|
|
170
|
+
"""
|
|
171
|
+
Logs security events with session tracking.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def __init__(self, db_pool=None, logger: Optional[logging.Logger] = None):
|
|
175
|
+
self.db_pool = db_pool
|
|
176
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
177
|
+
|
|
178
|
+
async def log_injection_attempt(
|
|
179
|
+
self,
|
|
180
|
+
user_id: str,
|
|
181
|
+
session_id: str,
|
|
182
|
+
chatbot_id: str,
|
|
183
|
+
threats: List[Dict[str, Any]],
|
|
184
|
+
original_input: str,
|
|
185
|
+
sanitized_input: str,
|
|
186
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
187
|
+
):
|
|
188
|
+
"""
|
|
189
|
+
Log a detected prompt injection attempt.
|
|
190
|
+
"""
|
|
191
|
+
max_severity = max(
|
|
192
|
+
(t['level'] for t in threats),
|
|
193
|
+
default=ThreatLevel.LOW
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Always log to application logger
|
|
197
|
+
for threat in threats:
|
|
198
|
+
self.logger.warning(
|
|
199
|
+
f"🔒 SECURITY: Prompt injection detected | "
|
|
200
|
+
f"Severity: {threat['level'].value.upper()} | "
|
|
201
|
+
f"User: {user_id} | Session: {session_id} | "
|
|
202
|
+
f"Bot: {chatbot_id} | "
|
|
203
|
+
f"Type: {threat['description']} | "
|
|
204
|
+
f"Pattern: '{threat['matched_text']}'"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Log to database if pool available
|
|
208
|
+
if self.db_pool:
|
|
209
|
+
await self._log_to_database(
|
|
210
|
+
user_id=user_id,
|
|
211
|
+
session_id=session_id,
|
|
212
|
+
chatbot_id=chatbot_id,
|
|
213
|
+
threats=threats,
|
|
214
|
+
original_input=original_input,
|
|
215
|
+
sanitized_input=sanitized_input,
|
|
216
|
+
max_severity=max_severity,
|
|
217
|
+
metadata=metadata or {}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
async def _log_to_database(
|
|
221
|
+
self,
|
|
222
|
+
user_id: str,
|
|
223
|
+
session_id: str,
|
|
224
|
+
chatbot_id: str,
|
|
225
|
+
threats: List[Dict[str, Any]],
|
|
226
|
+
original_input: str,
|
|
227
|
+
sanitized_input: str,
|
|
228
|
+
max_severity: ThreatLevel,
|
|
229
|
+
metadata: Dict[str, Any]
|
|
230
|
+
):
|
|
231
|
+
"""Store security event in database."""
|
|
232
|
+
try:
|
|
233
|
+
query = """
|
|
234
|
+
INSERT INTO navigator.security_events (
|
|
235
|
+
event_type,
|
|
236
|
+
user_id,
|
|
237
|
+
session_id,
|
|
238
|
+
chatbot_id,
|
|
239
|
+
severity,
|
|
240
|
+
threat_details,
|
|
241
|
+
original_input,
|
|
242
|
+
sanitized_input,
|
|
243
|
+
metadata,
|
|
244
|
+
created_at
|
|
245
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
async with await self.db_pool.acquire() as conn:
|
|
249
|
+
await conn.execute(
|
|
250
|
+
query,
|
|
251
|
+
'prompt_injection',
|
|
252
|
+
user_id,
|
|
253
|
+
session_id,
|
|
254
|
+
chatbot_id,
|
|
255
|
+
max_severity.value,
|
|
256
|
+
threats, # PostgreSQL JSONB
|
|
257
|
+
original_input[:2000], # Truncate for storage
|
|
258
|
+
sanitized_input[:2000],
|
|
259
|
+
metadata,
|
|
260
|
+
datetime.now(timezone.utc),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
self.logger.info(
|
|
264
|
+
f"Security event logged to database for user {user_id}"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
except Exception as e:
|
|
268
|
+
self.logger.error(f"Failed to log security event to database: {e}")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- Security events tracking table
|
|
2
|
+
CREATE TABLE IF NOT EXISTS security_events (
|
|
3
|
+
event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4
|
+
event_type VARCHAR(50) NOT NULL,
|
|
5
|
+
user_id VARCHAR(255) NOT NULL,
|
|
6
|
+
session_id VARCHAR(255) NOT NULL,
|
|
7
|
+
chatbot_id VARCHAR(255),
|
|
8
|
+
severity VARCHAR(20) NOT NULL,
|
|
9
|
+
threat_details JSONB NOT NULL,
|
|
10
|
+
original_input TEXT,
|
|
11
|
+
sanitized_input TEXT,
|
|
12
|
+
metadata JSONB DEFAULT '{}',
|
|
13
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
-- Indexes for security monitoring
|
|
17
|
+
CREATE INDEX idx_security_events_user_id ON security_events(user_id);
|
|
18
|
+
CREATE INDEX idx_security_events_session_id ON security_events(session_id);
|
|
19
|
+
CREATE INDEX idx_security_events_severity ON security_events(severity);
|
|
20
|
+
CREATE INDEX idx_security_events_created_at ON security_events(created_at DESC);
|
|
21
|
+
CREATE INDEX idx_security_events_chatbot ON security_events(chatbot_id);
|
|
22
|
+
|
|
23
|
+
-- Index for monitoring critical/high severity events
|
|
24
|
+
CREATE INDEX idx_security_events_high_severity ON security_events(severity, created_at DESC)
|
|
25
|
+
WHERE severity IN ('critical', 'high');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Parrot service helpers."""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class TransportConfig:
|
|
6
|
+
"""Configuration for a single transport."""
|
|
7
|
+
transport: str # "stdio" or "http" or "sse" or "unix"
|
|
8
|
+
enabled: bool = True
|
|
9
|
+
host: Optional[str] = None # Only for HTTP
|
|
10
|
+
port: Optional[int] = None # Only for HTTP
|
|
11
|
+
url: Optional[str] = None # Only for HTTP/SSE transport
|
|
12
|
+
name_suffix: Optional[str] = None # e.g., "local" or "remote"
|
|
13
|
+
socket_path: Optional[str] = None # Only for UNIX socket transport
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Utilities for starting the MCP server inside the aiohttp application."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Dict, List, Optional, Union
|
|
4
|
+
import asyncio
|
|
5
|
+
import contextlib
|
|
6
|
+
import inspect
|
|
7
|
+
from importlib import import_module
|
|
8
|
+
from aiohttp import web
|
|
9
|
+
from navconfig.logging import logging
|
|
10
|
+
from ...conf import (
|
|
11
|
+
MCP_SERVER_DESCRIPTION,
|
|
12
|
+
MCP_SERVER_HOST,
|
|
13
|
+
MCP_SERVER_LOG_LEVEL,
|
|
14
|
+
MCP_SERVER_NAME,
|
|
15
|
+
MCP_SERVER_PORT,
|
|
16
|
+
MCP_SERVER_TRANSPORT,
|
|
17
|
+
MCP_STARTED_TOOLS,
|
|
18
|
+
)
|
|
19
|
+
from ...mcp.server import MCPServer, MCPServerConfig, HttpMCPServer, SseMCPServer
|
|
20
|
+
from ...tools.abstract import AbstractTool
|
|
21
|
+
from ...tools.toolkit import AbstractToolkit
|
|
22
|
+
from .config import TransportConfig
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ParrotMCPServer:
|
|
26
|
+
"""Manage lifecycle of multiple MCP servers (multi-transport) attached to an aiohttp app."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
transports: Optional[Union[str, List[str], Dict[str, TransportConfig]]] = None,
|
|
32
|
+
host: Optional[str] = None,
|
|
33
|
+
port: Optional[int] = None,
|
|
34
|
+
tools: Optional[Dict[str, str]] = None,
|
|
35
|
+
name: Optional[str] = None,
|
|
36
|
+
description: Optional[str] = None,
|
|
37
|
+
log_level: Optional[str] = None,
|
|
38
|
+
allow_anonymous: bool = True,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.name = name or MCP_SERVER_NAME
|
|
41
|
+
self.description = description or MCP_SERVER_DESCRIPTION
|
|
42
|
+
self.log_level = log_level or MCP_SERVER_LOG_LEVEL
|
|
43
|
+
self.tools_config = tools if isinstance(tools, dict) else MCP_STARTED_TOOLS
|
|
44
|
+
self.toolkit_instance = tools if isinstance(tools, AbstractToolkit) else None
|
|
45
|
+
self.allow_anonymous = allow_anonymous
|
|
46
|
+
# Parse transport configuration
|
|
47
|
+
self.transport_configs = self._parse_transports(
|
|
48
|
+
transports, host, port
|
|
49
|
+
)
|
|
50
|
+
# Multiple servers, one per transport
|
|
51
|
+
self.servers: Dict[str, MCPServer] = {}
|
|
52
|
+
self._server_tasks: Dict[str, asyncio.Task] = {}
|
|
53
|
+
self.logger = logging.getLogger("Parrot.MCPServer")
|
|
54
|
+
self.app: Optional[web.Application] = None
|
|
55
|
+
|
|
56
|
+
# Multiple servers, one per transport
|
|
57
|
+
self.servers: Dict[str, MCPServer] = {}
|
|
58
|
+
self._server_tasks: Dict[str, asyncio.Task] = {}
|
|
59
|
+
|
|
60
|
+
def _parse_transports(
|
|
61
|
+
self,
|
|
62
|
+
transports: Optional[Union[str, List[str], Dict[str, TransportConfig]]],
|
|
63
|
+
default_host: Optional[str],
|
|
64
|
+
default_port: Optional[int],
|
|
65
|
+
) -> Dict[str, TransportConfig]:
|
|
66
|
+
"""Parse transport configuration into normalized format."""
|
|
67
|
+
if transports is None:
|
|
68
|
+
# Default: HTTP only
|
|
69
|
+
transports = MCP_SERVER_TRANSPORT or "http"
|
|
70
|
+
|
|
71
|
+
configs = {}
|
|
72
|
+
|
|
73
|
+
if isinstance(transports, str):
|
|
74
|
+
# Single transport string: "stdio" or "http"
|
|
75
|
+
transport = transports.lower()
|
|
76
|
+
host = default_host or MCP_SERVER_HOST if transport in {"http", "sse"} else None
|
|
77
|
+
port = default_port or MCP_SERVER_PORT if transport in {"http", "sse"} else None
|
|
78
|
+
configs[transport] = TransportConfig(
|
|
79
|
+
transport=transport,
|
|
80
|
+
host=host,
|
|
81
|
+
port=port,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
elif isinstance(transports, list):
|
|
85
|
+
# List of transport names: ["stdio", "http"]
|
|
86
|
+
for i, transport in enumerate(transports):
|
|
87
|
+
transport = transport.lower()
|
|
88
|
+
is_http_like = transport in {"http", "sse"}
|
|
89
|
+
port = ((default_port or MCP_SERVER_PORT) + i) if is_http_like else None
|
|
90
|
+
configs[transport] = TransportConfig(
|
|
91
|
+
transport=transport,
|
|
92
|
+
host=(default_host or MCP_SERVER_HOST) if is_http_like else None,
|
|
93
|
+
port=port,
|
|
94
|
+
name_suffix=f"{i}" if len(transports) > 1 else None,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
elif isinstance(transports, dict):
|
|
98
|
+
# Full control: {"stdio": TransportConfig(...), "http": TransportConfig(...)}
|
|
99
|
+
configs = transports
|
|
100
|
+
|
|
101
|
+
return configs
|
|
102
|
+
|
|
103
|
+
def setup(self, app: web.Application) -> None:
|
|
104
|
+
"""Register lifecycle hooks inside the aiohttp application."""
|
|
105
|
+
self.app = app
|
|
106
|
+
app["parrot_mcp_server"] = self
|
|
107
|
+
app.on_startup.append(self.on_startup)
|
|
108
|
+
app.on_shutdown.append(self.on_shutdown)
|
|
109
|
+
self.logger.notice(
|
|
110
|
+
":: ParrotMCPServer registered for aiohttp app lifecycle management"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
async def on_startup(self, app: web.Application) -> None: # pylint: disable=unused-argument
|
|
114
|
+
"""Start the MCP server once aiohttp finishes bootstrapping."""
|
|
115
|
+
tools = await self._load_configured_tools()
|
|
116
|
+
if not tools:
|
|
117
|
+
self.logger.info("No MCP tools configured to start")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
# Start a server for each enabled transport
|
|
121
|
+
for transport_key, config in self.transport_configs.items():
|
|
122
|
+
if not config.enabled:
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
server_name = self.name
|
|
126
|
+
if config.name_suffix:
|
|
127
|
+
server_name = f"{self.name}-{config.name_suffix}"
|
|
128
|
+
|
|
129
|
+
mcp_config = MCPServerConfig(
|
|
130
|
+
name=server_name,
|
|
131
|
+
description=self.description,
|
|
132
|
+
transport=config.transport,
|
|
133
|
+
host=config.host,
|
|
134
|
+
port=config.port,
|
|
135
|
+
log_level=self.log_level,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Ensure MCP endpoints bypass the auth middleware (uses AuthHandler.exclude_list)
|
|
139
|
+
self._add_auth_exclusions(
|
|
140
|
+
[
|
|
141
|
+
mcp_config.base_path,
|
|
142
|
+
f"{mcp_config.base_path.rstrip('/')}/events",
|
|
143
|
+
f"{mcp_config.base_path.rstrip('/')}/.well-known/oauth-authorization-server",
|
|
144
|
+
f"{mcp_config.base_path.rstrip('/')}/oauth/register",
|
|
145
|
+
f"{mcp_config.base_path.rstrip('/')}/oauth/authorize",
|
|
146
|
+
f"{mcp_config.base_path.rstrip('/')}/oauth/token",
|
|
147
|
+
]
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if config.transport == "stdio":
|
|
151
|
+
server = MCPServer(mcp_config)
|
|
152
|
+
server.register_tools(tools)
|
|
153
|
+
self.servers[transport_key] = server
|
|
154
|
+
|
|
155
|
+
start_coro = server.start()
|
|
156
|
+
self._server_tasks[transport_key] = asyncio.create_task(start_coro)
|
|
157
|
+
self.logger.info(f"Spawned stdio MCP server task: {server_name}")
|
|
158
|
+
|
|
159
|
+
elif config.transport in {"http", "sse"}:
|
|
160
|
+
# Launch HTTP/SSE MCP server using existing aiohttp app
|
|
161
|
+
if config.transport == "sse":
|
|
162
|
+
server = SseMCPServer(mcp_config, parent_app=app)
|
|
163
|
+
else:
|
|
164
|
+
server = HttpMCPServer(mcp_config, parent_app=app)
|
|
165
|
+
server.register_tools(tools)
|
|
166
|
+
self.servers[transport_key] = server
|
|
167
|
+
|
|
168
|
+
# Solo registra rutas, no crea nueva app
|
|
169
|
+
await server.start()
|
|
170
|
+
|
|
171
|
+
self.logger.info(
|
|
172
|
+
f"MCP server '{server_name}' routes registered on existing app"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
async def on_shutdown(self, app: web.Application) -> None: # pylint: disable=unused-argument
|
|
176
|
+
"""Stop the MCP server when aiohttp starts shutting down."""
|
|
177
|
+
shutdown_errors = []
|
|
178
|
+
|
|
179
|
+
# Stop all servers
|
|
180
|
+
for transport_key, server in self.servers.items():
|
|
181
|
+
try:
|
|
182
|
+
await server.stop()
|
|
183
|
+
self.logger.info(f"Stopped MCP server: {transport_key}")
|
|
184
|
+
except Exception as exc:
|
|
185
|
+
shutdown_errors.append((transport_key, exc))
|
|
186
|
+
self.logger.error(f"Failed stopping MCP server {transport_key}: {exc}")
|
|
187
|
+
|
|
188
|
+
# Cancel stdio tasks
|
|
189
|
+
for transport_key, task in self._server_tasks.items():
|
|
190
|
+
if not task.done():
|
|
191
|
+
task.cancel()
|
|
192
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
193
|
+
await task
|
|
194
|
+
|
|
195
|
+
self._server_tasks.clear()
|
|
196
|
+
self.servers.clear()
|
|
197
|
+
|
|
198
|
+
if shutdown_errors:
|
|
199
|
+
self.logger.warning(
|
|
200
|
+
f"Shutdown completed with {len(shutdown_errors)} errors"
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
self.logger.info("All MCP servers shutdown complete")
|
|
204
|
+
|
|
205
|
+
def _add_auth_exclusions(self, paths: List[str]) -> None:
|
|
206
|
+
"""Register paths that should bypass the Auth middleware (if present)."""
|
|
207
|
+
if not self.allow_anonymous:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
if not self.app:
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
auth_handler = self.app.get("auth")
|
|
214
|
+
if not auth_handler:
|
|
215
|
+
self.logger.debug("Auth handler not found; skipping auth exclusions for MCP")
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
for path in paths:
|
|
219
|
+
auth_handler.add_exclude_list(path)
|
|
220
|
+
# Also add glob-style pattern to cover subpaths
|
|
221
|
+
if not path.endswith("*"):
|
|
222
|
+
auth_handler.add_exclude_list(f"{path}*")
|
|
223
|
+
self.logger.info("Registered MCP paths as auth exclusions: %s", paths)
|
|
224
|
+
|
|
225
|
+
async def _load_configured_tools(self) -> List[AbstractTool]:
|
|
226
|
+
"""Instantiate every tool declared in configuration."""
|
|
227
|
+
loaded: List[AbstractTool] = []
|
|
228
|
+
|
|
229
|
+
if not self.tools_config:
|
|
230
|
+
return loaded
|
|
231
|
+
|
|
232
|
+
for class_name, module_path in self.tools_config.items():
|
|
233
|
+
if isinstance(module_path, (AbstractTool)):
|
|
234
|
+
loaded.append(module_path)
|
|
235
|
+
continue
|
|
236
|
+
elif isinstance(module_path, AbstractToolkit):
|
|
237
|
+
toolkit = module_path
|
|
238
|
+
await self._maybe_start_toolkit(toolkit, class_name)
|
|
239
|
+
loaded.extend(toolkit.get_tools())
|
|
240
|
+
continue
|
|
241
|
+
try:
|
|
242
|
+
module = import_module(module_path)
|
|
243
|
+
tool_cls = getattr(module, class_name)
|
|
244
|
+
except (ImportError, AttributeError) as exc:
|
|
245
|
+
self.logger.error(
|
|
246
|
+
"Unable to import MCP tool %s from %s: %s",
|
|
247
|
+
class_name,
|
|
248
|
+
module_path,
|
|
249
|
+
exc,
|
|
250
|
+
)
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
instances = await self._initialize_tool(tool_cls, class_name)
|
|
254
|
+
if not instances:
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
loaded.extend(instances)
|
|
258
|
+
|
|
259
|
+
self.logger.info("Loaded %s MCP tools", len(loaded))
|
|
260
|
+
return loaded
|
|
261
|
+
|
|
262
|
+
async def _initialize_tool(
|
|
263
|
+
self,
|
|
264
|
+
tool_cls,
|
|
265
|
+
class_name: str,
|
|
266
|
+
) -> List[AbstractTool]:
|
|
267
|
+
"""Instantiate either a toolkit or an individual AbstractTool."""
|
|
268
|
+
try:
|
|
269
|
+
instance = tool_cls()
|
|
270
|
+
except Exception as exc:
|
|
271
|
+
self.logger.error("Unable to instantiate %s: %s", class_name, exc)
|
|
272
|
+
return []
|
|
273
|
+
|
|
274
|
+
if isinstance(instance, AbstractToolkit):
|
|
275
|
+
await self._maybe_start_toolkit(instance, class_name)
|
|
276
|
+
return list(instance.get_tools())
|
|
277
|
+
|
|
278
|
+
if isinstance(instance, AbstractTool):
|
|
279
|
+
return [instance]
|
|
280
|
+
|
|
281
|
+
self.logger.warning(
|
|
282
|
+
"Configured MCP entry %s is neither an AbstractTool nor AbstractToolkit",
|
|
283
|
+
class_name,
|
|
284
|
+
)
|
|
285
|
+
return []
|
|
286
|
+
|
|
287
|
+
async def _maybe_start_toolkit(self, toolkit: AbstractToolkit, class_name: str) -> None:
|
|
288
|
+
"""Call toolkit.start() when available."""
|
|
289
|
+
try:
|
|
290
|
+
result = toolkit.start()
|
|
291
|
+
if inspect.isawaitable(result):
|
|
292
|
+
await result
|
|
293
|
+
self.logger.debug("Toolkit %s started", class_name)
|
|
294
|
+
except Exception as exc: # pragma: no cover - logging path
|
|
295
|
+
self.logger.error("Toolkit %s failed during startup: %s", class_name, exc)
|