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
parrot/a2a/server.py
ADDED
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
# parrot/a2a/server.py
|
|
2
|
+
"""
|
|
3
|
+
A2A Server - Wraps an AI-Parrot Agent as an A2A-compliant HTTP service.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
from typing import Dict, List, Optional, Any, TYPE_CHECKING, Union
|
|
7
|
+
import uuid
|
|
8
|
+
import json
|
|
9
|
+
import contextlib
|
|
10
|
+
import asyncio
|
|
11
|
+
from aiohttp import web
|
|
12
|
+
from navconfig.logging import logging
|
|
13
|
+
from .models import (
|
|
14
|
+
AgentCard,
|
|
15
|
+
AgentSkill,
|
|
16
|
+
AgentCapabilities,
|
|
17
|
+
Task,
|
|
18
|
+
TaskState,
|
|
19
|
+
TaskStatus,
|
|
20
|
+
Message,
|
|
21
|
+
Part,
|
|
22
|
+
Artifact,
|
|
23
|
+
Role,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from ..bots.abstract import AbstractBot
|
|
28
|
+
from ..tools.abstract import AbstractTool
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class A2AServer:
|
|
32
|
+
"""
|
|
33
|
+
Wraps an AI-Parrot Agent (BasicAgent/AbstractBot) as an A2A HTTP service.
|
|
34
|
+
|
|
35
|
+
This server exposes your existing agent via the A2A protocol, automatically
|
|
36
|
+
generating the AgentCard from the agent's properties and tools.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
from parrot.bots import Agent
|
|
40
|
+
from parrot.a2a import A2AServer
|
|
41
|
+
|
|
42
|
+
# Create your agent as usual
|
|
43
|
+
agent = Agent(
|
|
44
|
+
name="CustomerSupport",
|
|
45
|
+
llm="anthropic:claude-sonnet-4-20250514",
|
|
46
|
+
tools=[QueryCustomersTool(), CreateTicketTool()]
|
|
47
|
+
)
|
|
48
|
+
await agent.configure()
|
|
49
|
+
|
|
50
|
+
# Wrap it as A2A service
|
|
51
|
+
a2a = A2AServer(agent)
|
|
52
|
+
|
|
53
|
+
# Mount on your aiohttp app
|
|
54
|
+
app = web.Application()
|
|
55
|
+
a2a.setup(app)
|
|
56
|
+
|
|
57
|
+
# Agent is now accessible at:
|
|
58
|
+
# - GET /.well-known/agent.json (discovery)
|
|
59
|
+
# - POST /a2a/message/send (send message)
|
|
60
|
+
# - POST /a2a/message/stream (streaming)
|
|
61
|
+
# - GET /a2a/tasks/{id} (get task)
|
|
62
|
+
# etc.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
agent: "AbstractBot",
|
|
68
|
+
*,
|
|
69
|
+
base_path: str = "/a2a",
|
|
70
|
+
version: str = "1.0.0",
|
|
71
|
+
capabilities: Optional[AgentCapabilities] = None,
|
|
72
|
+
extra_skills: Optional[List[AgentSkill]] = None,
|
|
73
|
+
tags: Optional[List[str]] = None,
|
|
74
|
+
):
|
|
75
|
+
"""
|
|
76
|
+
Initialize A2A server wrapper.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
agent: The AI-Parrot agent to expose (BasicAgent, etc.)
|
|
80
|
+
base_path: URL prefix for A2A endpoints (default: /a2a)
|
|
81
|
+
version: Version string for the AgentCard
|
|
82
|
+
capabilities: Override auto-detected capabilities
|
|
83
|
+
extra_skills: Additional skills beyond auto-discovered tools
|
|
84
|
+
tags: Tags for the AgentCard
|
|
85
|
+
"""
|
|
86
|
+
self.agent = agent
|
|
87
|
+
self.base_path = base_path.rstrip("/")
|
|
88
|
+
self.version = version
|
|
89
|
+
self.capabilities = capabilities or AgentCapabilities(streaming=True)
|
|
90
|
+
self.extra_skills = extra_skills or []
|
|
91
|
+
self.tags = tags or []
|
|
92
|
+
|
|
93
|
+
# Runtime state
|
|
94
|
+
self._tasks: Dict[str, Task] = {}
|
|
95
|
+
self._app: Optional[web.Application] = None
|
|
96
|
+
self._url: Optional[str] = None
|
|
97
|
+
self._agent_card: Optional[AgentCard] = None
|
|
98
|
+
|
|
99
|
+
self.logger = logging.getLogger(f"A2A.{agent.name}")
|
|
100
|
+
|
|
101
|
+
def setup(self, app: web.Application, url: Optional[str] = None) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Register A2A routes on an aiohttp application.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
app: The aiohttp Application
|
|
107
|
+
url: Public URL where this agent is accessible (for AgentCard)
|
|
108
|
+
"""
|
|
109
|
+
self._app = app
|
|
110
|
+
self._url = url
|
|
111
|
+
|
|
112
|
+
# Store reference in app
|
|
113
|
+
app[f"a2a_server_{self.agent.name}"] = self
|
|
114
|
+
|
|
115
|
+
# Well-known agent card endpoint
|
|
116
|
+
app.router.add_get("/.well-known/agent.json", self._handle_agent_card)
|
|
117
|
+
|
|
118
|
+
# A2A HTTP+JSON Binding endpoints
|
|
119
|
+
app.router.add_post(f"{self.base_path}/message/send", self._handle_send_message)
|
|
120
|
+
app.router.add_post(f"{self.base_path}/message/stream", self._handle_stream_message)
|
|
121
|
+
app.router.add_get(f"{self.base_path}/tasks/{{task_id}}", self._handle_get_task)
|
|
122
|
+
app.router.add_get(f"{self.base_path}/tasks", self._handle_list_tasks)
|
|
123
|
+
app.router.add_post(f"{self.base_path}/tasks/{{task_id}}/cancel", self._handle_cancel_task)
|
|
124
|
+
app.router.add_get(f"{self.base_path}/tasks/{{task_id}}/subscribe", self._handle_subscribe)
|
|
125
|
+
|
|
126
|
+
# JSON-RPC binding (alternative)
|
|
127
|
+
app.router.add_post(f"{self.base_path}/rpc", self._handle_jsonrpc)
|
|
128
|
+
|
|
129
|
+
self.logger.info(
|
|
130
|
+
f"A2A server mounted for agent '{self.agent.name}' at {self.base_path}"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# ─────────────────────────────────────────────────────────────
|
|
134
|
+
# AgentCard Generation (from Agent properties)
|
|
135
|
+
# ─────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
def get_agent_card(self) -> AgentCard:
|
|
138
|
+
"""Generate AgentCard from the wrapped agent's properties."""
|
|
139
|
+
if self._agent_card:
|
|
140
|
+
return self._agent_card
|
|
141
|
+
|
|
142
|
+
# Build skills from agent's tools
|
|
143
|
+
skills = self._build_skills_from_tools()
|
|
144
|
+
skills.extend(self.extra_skills)
|
|
145
|
+
|
|
146
|
+
# Add a default "chat" skill if no tools
|
|
147
|
+
if not skills:
|
|
148
|
+
skills.append(AgentSkill(
|
|
149
|
+
id="chat",
|
|
150
|
+
name="Chat",
|
|
151
|
+
description=f"Have a conversation with {self.agent.name}",
|
|
152
|
+
tags=["conversation", "chat"],
|
|
153
|
+
))
|
|
154
|
+
|
|
155
|
+
# Build description from agent properties
|
|
156
|
+
description_parts = []
|
|
157
|
+
if hasattr(self.agent, 'description') and self.agent.description:
|
|
158
|
+
description_parts.append(self.agent.description)
|
|
159
|
+
if hasattr(self.agent, 'role') and self.agent.role:
|
|
160
|
+
description_parts.append(f"Role: {self.agent.role}")
|
|
161
|
+
if hasattr(self.agent, 'goal') and self.agent.goal:
|
|
162
|
+
description_parts.append(f"Goal: {self.agent.goal}")
|
|
163
|
+
|
|
164
|
+
description = " | ".join(description_parts) if description_parts else f"AI Agent: {self.agent.name}"
|
|
165
|
+
|
|
166
|
+
self._agent_card = AgentCard(
|
|
167
|
+
name=self.agent.name,
|
|
168
|
+
description=description,
|
|
169
|
+
version=self.version,
|
|
170
|
+
url=self._url,
|
|
171
|
+
skills=skills,
|
|
172
|
+
capabilities=self.capabilities,
|
|
173
|
+
tags=self.tags or getattr(self.agent, 'tags', []),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return self._agent_card
|
|
177
|
+
|
|
178
|
+
def _build_skills_from_tools(self) -> List[AgentSkill]:
|
|
179
|
+
"""Convert agent's tools to A2A skills."""
|
|
180
|
+
skills = []
|
|
181
|
+
|
|
182
|
+
# Get tools from tool_manager if available
|
|
183
|
+
if hasattr(self.agent, 'tool_manager'):
|
|
184
|
+
tools = self.agent.tool_manager.list_tools()
|
|
185
|
+
for tool_name in tools:
|
|
186
|
+
if tool := self.agent.tool_manager.get_tool(tool_name):
|
|
187
|
+
if skill := self._tool_to_skill(tool):
|
|
188
|
+
skills.append(skill)
|
|
189
|
+
|
|
190
|
+
# Also check direct tools attribute
|
|
191
|
+
elif hasattr(self.agent, 'tools') and self.agent.tools:
|
|
192
|
+
for tool in self.agent.tools:
|
|
193
|
+
if skill := self._tool_to_skill(tool):
|
|
194
|
+
skills.append(skill)
|
|
195
|
+
|
|
196
|
+
return skills
|
|
197
|
+
|
|
198
|
+
def _tool_to_skill(self, tool: "AbstractTool") -> Optional[AgentSkill]:
|
|
199
|
+
"""Convert an AbstractTool to an AgentSkill."""
|
|
200
|
+
try:
|
|
201
|
+
name = getattr(tool, 'name', None)
|
|
202
|
+
if not name:
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
description = getattr(tool, 'description', f"Tool: {name}")
|
|
206
|
+
|
|
207
|
+
# Try to get input schema from args_schema (Pydantic model)
|
|
208
|
+
input_schema = None
|
|
209
|
+
if hasattr(tool, 'args_schema') and tool.args_schema:
|
|
210
|
+
with contextlib.suppress(Exception):
|
|
211
|
+
input_schema = tool.args_schema.model_json_schema()
|
|
212
|
+
|
|
213
|
+
# Get tags if available
|
|
214
|
+
tags = getattr(tool, 'tags', [])
|
|
215
|
+
if isinstance(tags, str):
|
|
216
|
+
tags = [tags]
|
|
217
|
+
|
|
218
|
+
return AgentSkill(
|
|
219
|
+
id=name,
|
|
220
|
+
name=name.replace("_", " ").title(),
|
|
221
|
+
description=description,
|
|
222
|
+
tags=list(tags),
|
|
223
|
+
input_schema=input_schema,
|
|
224
|
+
)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
self.logger.warning(f"Could not convert tool to skill: {e}")
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
# ─────────────────────────────────────────────────────────────
|
|
230
|
+
# Core Message Processing (delegates to Agent)
|
|
231
|
+
# ─────────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
async def process_message(self, message: Message) -> Task:
|
|
234
|
+
"""
|
|
235
|
+
Process an A2A message by delegating to the wrapped agent.
|
|
236
|
+
"""
|
|
237
|
+
task = Task.create(context_id=message.context_id)
|
|
238
|
+
task.history.append(message)
|
|
239
|
+
self._tasks[task.id] = task
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
task.working(f"Processing with {self.agent.name}...")
|
|
243
|
+
|
|
244
|
+
# Extract the question/input from message
|
|
245
|
+
question = message.get_text()
|
|
246
|
+
data = message.get_data()
|
|
247
|
+
|
|
248
|
+
# If structured data with skill/tool request
|
|
249
|
+
if data and "skill" in data:
|
|
250
|
+
response = await self._invoke_skill(data["skill"], data.get("params", {}))
|
|
251
|
+
elif data and "tool" in data:
|
|
252
|
+
response = await self._invoke_tool(data["tool"], data.get("params", {}))
|
|
253
|
+
else:
|
|
254
|
+
# Default: use agent's ask/chat method
|
|
255
|
+
response = await self._ask_agent(question, message)
|
|
256
|
+
|
|
257
|
+
task.complete(response)
|
|
258
|
+
|
|
259
|
+
except Exception as e:
|
|
260
|
+
self.logger.error(f"Error processing message: {e}", exc_info=True)
|
|
261
|
+
task.fail(str(e))
|
|
262
|
+
|
|
263
|
+
return task
|
|
264
|
+
|
|
265
|
+
async def _ask_agent(self, question: str, message: Message) -> Any:
|
|
266
|
+
"""Delegate question to agent's ask/chat method."""
|
|
267
|
+
# Prepare kwargs for the agent
|
|
268
|
+
kwargs = self._build_ask_kwargs(message)
|
|
269
|
+
|
|
270
|
+
# Pass context_id as session_id if available
|
|
271
|
+
if message.context_id:
|
|
272
|
+
kwargs["session_id"] = message.context_id
|
|
273
|
+
|
|
274
|
+
# Use ask() method (most compatible)
|
|
275
|
+
if hasattr(self.agent, 'ask'):
|
|
276
|
+
response = await self.agent.ask(question, **kwargs)
|
|
277
|
+
elif hasattr(self.agent, 'chat'):
|
|
278
|
+
response = await self.agent.chat(question, **kwargs)
|
|
279
|
+
elif hasattr(self.agent, 'conversation'):
|
|
280
|
+
response = await self.agent.conversation(question, **kwargs)
|
|
281
|
+
else:
|
|
282
|
+
raise NotImplementedError(
|
|
283
|
+
f"Agent {self.agent.name} doesn't have ask/chat/conversation method"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return response
|
|
287
|
+
|
|
288
|
+
async def _invoke_skill(self, skill_id: str, params: Dict[str, Any]) -> Any:
|
|
289
|
+
"""Invoke a specific skill (tool) by ID."""
|
|
290
|
+
return await self._invoke_tool(skill_id, params)
|
|
291
|
+
|
|
292
|
+
async def _invoke_tool(self, tool_name: str, params: Dict[str, Any]) -> Any:
|
|
293
|
+
"""Invoke a specific tool by name."""
|
|
294
|
+
tool = None
|
|
295
|
+
|
|
296
|
+
# Try tool_manager first
|
|
297
|
+
if hasattr(self.agent, 'tool_manager'):
|
|
298
|
+
tool = self.agent.tool_manager.get_tool(tool_name)
|
|
299
|
+
|
|
300
|
+
# Try direct tools list
|
|
301
|
+
if not tool and hasattr(self.agent, 'tools'):
|
|
302
|
+
for t in self.agent.tools:
|
|
303
|
+
if getattr(t, 'name', None) == tool_name:
|
|
304
|
+
tool = t
|
|
305
|
+
break
|
|
306
|
+
|
|
307
|
+
if not tool:
|
|
308
|
+
raise ValueError(f"Tool/skill '{tool_name}' not found")
|
|
309
|
+
|
|
310
|
+
# Execute the tool
|
|
311
|
+
if hasattr(tool, '_execute'):
|
|
312
|
+
result = await tool._execute(**params)
|
|
313
|
+
elif hasattr(tool, 'run'):
|
|
314
|
+
result = await tool.run(**params)
|
|
315
|
+
elif hasattr(tool, '_arun'):
|
|
316
|
+
result = await tool._arun(**params)
|
|
317
|
+
else:
|
|
318
|
+
raise NotImplementedError(f"Tool {tool_name} has no executable method")
|
|
319
|
+
|
|
320
|
+
return result
|
|
321
|
+
|
|
322
|
+
# ─────────────────────────────────────────────────────────────
|
|
323
|
+
# HTTP Handlers
|
|
324
|
+
# ─────────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
async def _handle_agent_card(self, request: web.Request) -> web.Response:
|
|
327
|
+
"""GET /.well-known/agent.json"""
|
|
328
|
+
card = self.get_agent_card()
|
|
329
|
+
return web.json_response(card.to_dict())
|
|
330
|
+
|
|
331
|
+
async def _handle_send_message(self, request: web.Request) -> web.Response:
|
|
332
|
+
"""POST /a2a/message/send"""
|
|
333
|
+
try:
|
|
334
|
+
data = await request.json()
|
|
335
|
+
message = Message.from_dict(data.get("message", {}))
|
|
336
|
+
config = data.get("configuration", {})
|
|
337
|
+
|
|
338
|
+
task = await self.process_message(message)
|
|
339
|
+
|
|
340
|
+
# If blocking mode, wait for completion (already done in process_message)
|
|
341
|
+
# but if we had async processing, we'd wait here
|
|
342
|
+
|
|
343
|
+
return web.json_response(task.to_dict())
|
|
344
|
+
|
|
345
|
+
except json.JSONDecodeError:
|
|
346
|
+
return web.json_response(
|
|
347
|
+
{"error": {"code": "InvalidJSON", "message": "Invalid JSON body"}},
|
|
348
|
+
status=400
|
|
349
|
+
)
|
|
350
|
+
except Exception as e:
|
|
351
|
+
self.logger.error(f"Error in send_message: {e}", exc_info=True)
|
|
352
|
+
return web.json_response(
|
|
353
|
+
{"error": {"code": "InternalError", "message": str(e)}},
|
|
354
|
+
status=500
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
async def _handle_stream_message(self, request: web.Request) -> web.StreamResponse:
|
|
358
|
+
"""POST /a2a/message/stream - SSE streaming response."""
|
|
359
|
+
response = web.StreamResponse(
|
|
360
|
+
headers={
|
|
361
|
+
"Content-Type": "text/event-stream",
|
|
362
|
+
"Cache-Control": "no-cache",
|
|
363
|
+
"Connection": "keep-alive",
|
|
364
|
+
}
|
|
365
|
+
)
|
|
366
|
+
await response.prepare(request)
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
data = await request.json()
|
|
370
|
+
message = Message.from_dict(data.get("message", {}))
|
|
371
|
+
|
|
372
|
+
# Create task
|
|
373
|
+
task = Task.create(context_id=message.context_id)
|
|
374
|
+
task.history.append(message)
|
|
375
|
+
self._tasks[task.id] = task
|
|
376
|
+
|
|
377
|
+
# Send initial task
|
|
378
|
+
await self._send_sse(response, {"task": task.to_dict()})
|
|
379
|
+
|
|
380
|
+
# Send working status
|
|
381
|
+
task.working(f"Processing with {self.agent.name}...")
|
|
382
|
+
await self._send_sse(response, {
|
|
383
|
+
"statusUpdate": {
|
|
384
|
+
"taskId": task.id,
|
|
385
|
+
"contextId": task.context_id,
|
|
386
|
+
"status": {"state": "working"},
|
|
387
|
+
"final": False
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
# Process with streaming
|
|
392
|
+
try:
|
|
393
|
+
question = message.get_text()
|
|
394
|
+
|
|
395
|
+
# Try to use streaming method
|
|
396
|
+
if hasattr(self.agent, 'ask_stream'):
|
|
397
|
+
await self._stream_with_ask_stream(response, task, question, message)
|
|
398
|
+
else:
|
|
399
|
+
# Fallback to non-streaming
|
|
400
|
+
await self._stream_fallback(response, task, question, message)
|
|
401
|
+
|
|
402
|
+
except Exception as e:
|
|
403
|
+
self.logger.error(f"Error in streaming: {e}", exc_info=True)
|
|
404
|
+
task.fail(str(e))
|
|
405
|
+
await self._send_sse(response, {
|
|
406
|
+
"statusUpdate": {
|
|
407
|
+
"taskId": task.id,
|
|
408
|
+
"contextId": task.context_id,
|
|
409
|
+
"status": {
|
|
410
|
+
"state": "failed",
|
|
411
|
+
"message": {"role": "agent", "parts": [{"text": str(e)}]}
|
|
412
|
+
},
|
|
413
|
+
"final": True
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
except Exception as e:
|
|
418
|
+
self.logger.error(f"Error setting up stream: {e}", exc_info=True)
|
|
419
|
+
await self._send_sse(response, {"error": {"message": str(e)}})
|
|
420
|
+
|
|
421
|
+
await response.write_eof()
|
|
422
|
+
return response
|
|
423
|
+
|
|
424
|
+
async def _stream_with_ask_stream(
|
|
425
|
+
self,
|
|
426
|
+
response: web.StreamResponse,
|
|
427
|
+
task: Task,
|
|
428
|
+
question: str,
|
|
429
|
+
message: Message
|
|
430
|
+
) -> None:
|
|
431
|
+
"""Stream using agent's ask_stream method with light buffering."""
|
|
432
|
+
kwargs = {}
|
|
433
|
+
if message.context_id:
|
|
434
|
+
kwargs["session_id"] = message.context_id
|
|
435
|
+
|
|
436
|
+
collected_text = []
|
|
437
|
+
artifact_id = str(uuid.uuid4())
|
|
438
|
+
|
|
439
|
+
# Light buffering - balance between responsiveness and efficiency
|
|
440
|
+
buffer = []
|
|
441
|
+
buffer_size = 0
|
|
442
|
+
MIN_CHUNK_SIZE = 15 # ~3-4 words
|
|
443
|
+
MAX_BUFFER_TIME = 0.1 # 100ms max wait
|
|
444
|
+
last_flush = asyncio.get_event_loop().time()
|
|
445
|
+
|
|
446
|
+
async def flush_buffer():
|
|
447
|
+
nonlocal buffer, buffer_size, last_flush
|
|
448
|
+
if buffer:
|
|
449
|
+
chunk_text = "".join(buffer)
|
|
450
|
+
collected_text.append(chunk_text)
|
|
451
|
+
|
|
452
|
+
await self._send_sse(response, {
|
|
453
|
+
"artifactUpdate": {
|
|
454
|
+
"taskId": task.id,
|
|
455
|
+
"contextId": task.context_id,
|
|
456
|
+
"artifact": {
|
|
457
|
+
"artifactId": artifact_id,
|
|
458
|
+
"name": "response",
|
|
459
|
+
"parts": [{"text": chunk_text}]
|
|
460
|
+
},
|
|
461
|
+
"append": len(collected_text) > 1,
|
|
462
|
+
"lastChunk": False
|
|
463
|
+
}
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
buffer = []
|
|
467
|
+
buffer_size = 0
|
|
468
|
+
last_flush = asyncio.get_event_loop().time()
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
async for chunk in self.agent.ask_stream(question, **kwargs):
|
|
472
|
+
chunk_text = self._extract_chunk_text(chunk)
|
|
473
|
+
|
|
474
|
+
if chunk_text:
|
|
475
|
+
buffer.append(chunk_text)
|
|
476
|
+
buffer_size += len(chunk_text)
|
|
477
|
+
|
|
478
|
+
current_time = asyncio.get_event_loop().time()
|
|
479
|
+
time_since_flush = current_time - last_flush
|
|
480
|
+
|
|
481
|
+
# Flush on size OR time threshold
|
|
482
|
+
if buffer_size >= MIN_CHUNK_SIZE or time_since_flush >= MAX_BUFFER_TIME:
|
|
483
|
+
await flush_buffer()
|
|
484
|
+
|
|
485
|
+
# Flush remaining
|
|
486
|
+
await flush_buffer()
|
|
487
|
+
|
|
488
|
+
# Final artifact with complete text
|
|
489
|
+
full_text = "".join(collected_text)
|
|
490
|
+
artifact = Artifact(
|
|
491
|
+
artifact_id=artifact_id,
|
|
492
|
+
name="response",
|
|
493
|
+
parts=[Part.from_text(full_text)]
|
|
494
|
+
)
|
|
495
|
+
task.artifacts.append(artifact)
|
|
496
|
+
|
|
497
|
+
await self._send_sse(response, {
|
|
498
|
+
"artifactUpdate": {
|
|
499
|
+
"taskId": task.id,
|
|
500
|
+
"contextId": task.context_id,
|
|
501
|
+
"artifact": artifact.to_dict(),
|
|
502
|
+
"append": False,
|
|
503
|
+
"lastChunk": True
|
|
504
|
+
}
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
task.status = TaskStatus(state=TaskState.COMPLETED)
|
|
508
|
+
await self._send_sse(response, {
|
|
509
|
+
"statusUpdate": {
|
|
510
|
+
"taskId": task.id,
|
|
511
|
+
"contextId": task.context_id,
|
|
512
|
+
"status": {"state": "completed"},
|
|
513
|
+
"final": True
|
|
514
|
+
}
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
except Exception as e:
|
|
518
|
+
self.logger.error(f"Streaming error: {e}", exc_info=True)
|
|
519
|
+
raise
|
|
520
|
+
|
|
521
|
+
async def _stream_fallback(
|
|
522
|
+
self,
|
|
523
|
+
response: web.StreamResponse,
|
|
524
|
+
task: Task,
|
|
525
|
+
question: str,
|
|
526
|
+
message: Message
|
|
527
|
+
) -> None:
|
|
528
|
+
"""Fallback when streaming is not available - use regular ask."""
|
|
529
|
+
result = await self._ask_agent(question, message)
|
|
530
|
+
|
|
531
|
+
# Send artifact
|
|
532
|
+
artifact = Artifact.from_response(result)
|
|
533
|
+
task.artifacts.append(artifact)
|
|
534
|
+
await self._send_sse(response, {
|
|
535
|
+
"artifactUpdate": {
|
|
536
|
+
"taskId": task.id,
|
|
537
|
+
"contextId": task.context_id,
|
|
538
|
+
"artifact": artifact.to_dict(),
|
|
539
|
+
"lastChunk": True
|
|
540
|
+
}
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
# Send completed
|
|
544
|
+
task.status = TaskStatus(state=TaskState.COMPLETED)
|
|
545
|
+
await self._send_sse(response, {
|
|
546
|
+
"statusUpdate": {
|
|
547
|
+
"taskId": task.id,
|
|
548
|
+
"contextId": task.context_id,
|
|
549
|
+
"status": {"state": "completed"},
|
|
550
|
+
"final": True
|
|
551
|
+
}
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
def _build_ask_kwargs(self, message: Message) -> Dict[str, Any]:
|
|
555
|
+
"""Build kwargs for ask/ask_stream methods."""
|
|
556
|
+
kwargs = {}
|
|
557
|
+
|
|
558
|
+
# Get max_tokens from agent
|
|
559
|
+
max_tokens = getattr(self.agent, 'max_tokens', None)
|
|
560
|
+
if max_tokens is None and hasattr(self.agent, '_llm') and self.agent._llm:
|
|
561
|
+
max_tokens = getattr(self.agent._llm, 'max_tokens', None)
|
|
562
|
+
kwargs["max_tokens"] = max_tokens or 4096
|
|
563
|
+
|
|
564
|
+
# Pass context_id as session_id
|
|
565
|
+
if message.context_id:
|
|
566
|
+
kwargs["session_id"] = message.context_id
|
|
567
|
+
|
|
568
|
+
return kwargs
|
|
569
|
+
|
|
570
|
+
def _extract_chunk_text(self, chunk: Any) -> Optional[str]:
|
|
571
|
+
"""Extract text content from a stream chunk."""
|
|
572
|
+
if chunk is None:
|
|
573
|
+
return None
|
|
574
|
+
|
|
575
|
+
# String chunk
|
|
576
|
+
if isinstance(chunk, str):
|
|
577
|
+
return chunk
|
|
578
|
+
|
|
579
|
+
# AIMessage or similar response object
|
|
580
|
+
if hasattr(chunk, 'content'):
|
|
581
|
+
return chunk.content
|
|
582
|
+
if hasattr(chunk, 'text'):
|
|
583
|
+
return chunk.text
|
|
584
|
+
if hasattr(chunk, 'delta'):
|
|
585
|
+
delta = chunk.delta
|
|
586
|
+
if hasattr(delta, 'text'):
|
|
587
|
+
return delta.text
|
|
588
|
+
if hasattr(delta, 'content'):
|
|
589
|
+
return delta.content
|
|
590
|
+
|
|
591
|
+
# Dict chunk
|
|
592
|
+
if isinstance(chunk, dict):
|
|
593
|
+
return chunk.get('text') or chunk.get('content') or chunk.get('delta', {}).get('text')
|
|
594
|
+
|
|
595
|
+
# Fallback
|
|
596
|
+
return str(chunk) if chunk else None
|
|
597
|
+
|
|
598
|
+
async def _send_sse(self, response: web.StreamResponse, data: Dict[str, Any]):
|
|
599
|
+
"""Send SSE event."""
|
|
600
|
+
await response.write(f"data: {json.dumps(data)}\n\n".encode())
|
|
601
|
+
|
|
602
|
+
async def _handle_get_task(self, request: web.Request) -> web.Response:
|
|
603
|
+
"""GET /a2a/tasks/{task_id}"""
|
|
604
|
+
task_id = request.match_info["task_id"]
|
|
605
|
+
if task := self._tasks.get(task_id):
|
|
606
|
+
return web.json_response(task.to_dict())
|
|
607
|
+
|
|
608
|
+
return web.json_response(
|
|
609
|
+
{"error": {"code": "TaskNotFoundError", "message": f"Task {task_id} not found"}},
|
|
610
|
+
status=404
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
async def _handle_list_tasks(self, request: web.Request) -> web.Response:
|
|
614
|
+
"""GET /a2a/tasks"""
|
|
615
|
+
context_id = request.query.get("contextId")
|
|
616
|
+
state = request.query.get("status")
|
|
617
|
+
page_size = int(request.query.get("pageSize", 50))
|
|
618
|
+
|
|
619
|
+
tasks = list(self._tasks.values())
|
|
620
|
+
|
|
621
|
+
if context_id:
|
|
622
|
+
tasks = [t for t in tasks if t.context_id == context_id]
|
|
623
|
+
if state:
|
|
624
|
+
tasks = [t for t in tasks if t.status.state.value == state]
|
|
625
|
+
|
|
626
|
+
tasks = tasks[:page_size]
|
|
627
|
+
|
|
628
|
+
return web.json_response({
|
|
629
|
+
"tasks": [t.to_dict() for t in tasks],
|
|
630
|
+
"totalSize": len(tasks),
|
|
631
|
+
"pageSize": page_size,
|
|
632
|
+
"nextPageToken": ""
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
async def _handle_cancel_task(self, request: web.Request) -> web.Response:
|
|
636
|
+
"""POST /a2a/tasks/{task_id}/cancel"""
|
|
637
|
+
task_id = request.match_info["task_id"]
|
|
638
|
+
task = self._tasks.get(task_id)
|
|
639
|
+
|
|
640
|
+
if not task:
|
|
641
|
+
return web.json_response(
|
|
642
|
+
{"error": {"code": "TaskNotFoundError"}},
|
|
643
|
+
status=404
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
terminal_states = {TaskState.COMPLETED, TaskState.FAILED, TaskState.CANCELLED}
|
|
647
|
+
if task.status.state in terminal_states:
|
|
648
|
+
return web.json_response(
|
|
649
|
+
{"error": {"code": "TaskNotCancelableError"}},
|
|
650
|
+
status=400
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
task.status = TaskStatus(state=TaskState.CANCELLED)
|
|
654
|
+
return web.json_response(task.to_dict())
|
|
655
|
+
|
|
656
|
+
async def _handle_subscribe(self, request: web.Request) -> web.StreamResponse:
|
|
657
|
+
"""GET /a2a/tasks/{task_id}/subscribe"""
|
|
658
|
+
task_id = request.match_info["task_id"]
|
|
659
|
+
task = self._tasks.get(task_id)
|
|
660
|
+
|
|
661
|
+
if not task:
|
|
662
|
+
return web.json_response(
|
|
663
|
+
{"error": {"code": "TaskNotFoundError"}},
|
|
664
|
+
status=404
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
response = web.StreamResponse(
|
|
668
|
+
headers={"Content-Type": "text/event-stream"}
|
|
669
|
+
)
|
|
670
|
+
await response.prepare(request)
|
|
671
|
+
|
|
672
|
+
# Send current state
|
|
673
|
+
await self._send_sse(response, {"task": task.to_dict()})
|
|
674
|
+
|
|
675
|
+
# For now, just close (in production, would subscribe to updates)
|
|
676
|
+
await response.write_eof()
|
|
677
|
+
return response
|
|
678
|
+
|
|
679
|
+
async def _handle_jsonrpc(self, request: web.Request) -> web.Response:
|
|
680
|
+
"""POST /a2a/rpc - JSON-RPC 2.0 binding."""
|
|
681
|
+
data = await request.json()
|
|
682
|
+
method = data.get("method")
|
|
683
|
+
params = data.get("params", {})
|
|
684
|
+
req_id = data.get("id")
|
|
685
|
+
|
|
686
|
+
try:
|
|
687
|
+
if method == "message/send":
|
|
688
|
+
message = Message.from_dict(params.get("message", {}))
|
|
689
|
+
task = await self.process_message(message)
|
|
690
|
+
result = task.to_dict()
|
|
691
|
+
elif method == "tasks/get":
|
|
692
|
+
task = self._tasks.get(params.get("id"))
|
|
693
|
+
result = task.to_dict() if task else None
|
|
694
|
+
elif method == "tasks/list":
|
|
695
|
+
tasks = list(self._tasks.values())
|
|
696
|
+
result = {"tasks": [t.to_dict() for t in tasks]}
|
|
697
|
+
else:
|
|
698
|
+
return web.json_response({
|
|
699
|
+
"jsonrpc": "2.0",
|
|
700
|
+
"id": req_id,
|
|
701
|
+
"error": {"code": -32601, "message": f"Method not found: {method}"}
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
return web.json_response({
|
|
705
|
+
"jsonrpc": "2.0",
|
|
706
|
+
"id": req_id,
|
|
707
|
+
"result": result
|
|
708
|
+
})
|
|
709
|
+
except Exception as e:
|
|
710
|
+
return web.json_response({
|
|
711
|
+
"jsonrpc": "2.0",
|
|
712
|
+
"id": req_id,
|
|
713
|
+
"error": {"code": -32603, "message": str(e)}
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
class A2AEnabledMixin:
|
|
718
|
+
"""
|
|
719
|
+
Mixin to add A2A server capabilities to an agent class.
|
|
720
|
+
|
|
721
|
+
Similar to MCPEnabledMixin, this adds A2A methods directly to your agent.
|
|
722
|
+
|
|
723
|
+
Example:
|
|
724
|
+
class MyAgent(A2AEnabledMixin, BasicAgent):
|
|
725
|
+
pass
|
|
726
|
+
|
|
727
|
+
agent = MyAgent(name="test", llm="openai:gpt-4")
|
|
728
|
+
await agent.configure()
|
|
729
|
+
|
|
730
|
+
# Start A2A server
|
|
731
|
+
app = web.Application()
|
|
732
|
+
agent.setup_a2a(app, url="https://my-agent.example.com")
|
|
733
|
+
"""
|
|
734
|
+
|
|
735
|
+
_a2a_server: Optional[A2AServer] = None
|
|
736
|
+
|
|
737
|
+
def setup_a2a(
|
|
738
|
+
self,
|
|
739
|
+
app: web.Application,
|
|
740
|
+
url: Optional[str] = None,
|
|
741
|
+
base_path: str = "/a2a",
|
|
742
|
+
**kwargs
|
|
743
|
+
) -> A2AServer:
|
|
744
|
+
"""
|
|
745
|
+
Setup A2A server for this agent.
|
|
746
|
+
|
|
747
|
+
Args:
|
|
748
|
+
app: aiohttp Application to mount routes on
|
|
749
|
+
url: Public URL for AgentCard
|
|
750
|
+
base_path: URL prefix for A2A endpoints
|
|
751
|
+
**kwargs: Additional A2AServer options
|
|
752
|
+
|
|
753
|
+
Returns:
|
|
754
|
+
The A2AServer instance
|
|
755
|
+
"""
|
|
756
|
+
self._a2a_server = A2AServer(
|
|
757
|
+
self,
|
|
758
|
+
base_path=base_path,
|
|
759
|
+
**kwargs
|
|
760
|
+
)
|
|
761
|
+
self._a2a_server.setup(app, url=url)
|
|
762
|
+
return self._a2a_server
|
|
763
|
+
|
|
764
|
+
def get_a2a_server(self) -> Optional[A2AServer]:
|
|
765
|
+
"""Get the A2A server instance if setup."""
|
|
766
|
+
return self._a2a_server
|
|
767
|
+
|
|
768
|
+
def get_agent_card(self) -> Optional[AgentCard]:
|
|
769
|
+
"""Get the AgentCard if A2A is setup."""
|
|
770
|
+
return self._a2a_server.get_agent_card() if self._a2a_server else None
|