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,904 @@
|
|
|
1
|
+
# parrot/tools/openapi_toolkit.py
|
|
2
|
+
"""
|
|
3
|
+
OpenAPIToolkit - Dynamic toolkit that exposes OpenAPI services as tools.
|
|
4
|
+
|
|
5
|
+
IMPROVEMENTS IN THIS VERSION:
|
|
6
|
+
- Uses prance for robust OpenAPI parsing and reference resolution
|
|
7
|
+
- Inline schema refs via prance (no manual recursion)
|
|
8
|
+
- Support for application/x-www-form-urlencoded
|
|
9
|
+
- Optimized schemas for single-operation specs (cleaner LLM experience)
|
|
10
|
+
|
|
11
|
+
This toolkit automatically converts OpenAPI specifications into callable tools,
|
|
12
|
+
allowing LLMs to interact with REST APIs without manual tool definition.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
toolkit = OpenAPIToolkit(
|
|
16
|
+
spec="https://petstore3.swagger.io/api/v3/openapi.json",
|
|
17
|
+
service="petstore"
|
|
18
|
+
)
|
|
19
|
+
tools = toolkit.get_tools()
|
|
20
|
+
# Creates tools like: petstore_get_pet, petstore_post_pet, etc.
|
|
21
|
+
"""
|
|
22
|
+
from typing import Dict, List, Any, Optional, Union
|
|
23
|
+
import contextlib
|
|
24
|
+
import re
|
|
25
|
+
import json
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from urllib.parse import urlparse
|
|
28
|
+
import yaml
|
|
29
|
+
import httpx
|
|
30
|
+
from pydantic import BaseModel, Field, create_model
|
|
31
|
+
from navconfig.logging import logging
|
|
32
|
+
|
|
33
|
+
# Use prance for OpenAPI parsing with reference resolution
|
|
34
|
+
try:
|
|
35
|
+
from prance import ResolvingParser
|
|
36
|
+
PRANCE_AVAILABLE = True
|
|
37
|
+
except ImportError:
|
|
38
|
+
PRANCE_AVAILABLE = False
|
|
39
|
+
|
|
40
|
+
from ..interfaces.http import HTTPService
|
|
41
|
+
from .toolkit import AbstractToolkit
|
|
42
|
+
from .abstract import ToolResult
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class OpenAPIToolkit(AbstractToolkit):
|
|
46
|
+
"""
|
|
47
|
+
Toolkit that dynamically generates tools from OpenAPI specifications.
|
|
48
|
+
|
|
49
|
+
This toolkit:
|
|
50
|
+
- Uses prance for robust OpenAPI 3.x parsing (JSON/YAML, local or remote)
|
|
51
|
+
- Automatically resolves ALL $ref references (internal and external)
|
|
52
|
+
- Creates one tool per operation with naming: {service}_{method}_{path}
|
|
53
|
+
- Handles path parameters, query parameters, and request bodies
|
|
54
|
+
- Supports multiple content types: application/json, application/x-www-form-urlencoded
|
|
55
|
+
- Optimizes schemas for single-operation specs (cleaner for LLMs)
|
|
56
|
+
- Supports multiple authentication methods (API keys, Bearer tokens, Basic auth)
|
|
57
|
+
|
|
58
|
+
The tools are generated dynamically and integrated with HTTPService
|
|
59
|
+
for robust HTTP handling with retry logic, proxy support, etc.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
spec: Union[str, Dict[str, Any]],
|
|
65
|
+
service: str,
|
|
66
|
+
base_url: Optional[str] = None,
|
|
67
|
+
api_key: Optional[str] = None,
|
|
68
|
+
auth_type: str = "bearer", # "bearer", "apikey", "basic"
|
|
69
|
+
auth_header: str = "Authorization",
|
|
70
|
+
api_key_location: str = "header", # "header", "query"
|
|
71
|
+
api_key_name: str = "api_key",
|
|
72
|
+
credentials: Optional[Dict[str, str]] = None,
|
|
73
|
+
use_proxy: bool = False,
|
|
74
|
+
timeout: int = 30,
|
|
75
|
+
debug: bool = False,
|
|
76
|
+
**kwargs
|
|
77
|
+
):
|
|
78
|
+
"""
|
|
79
|
+
Initialize OpenAPI toolkit.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
spec: OpenAPI spec as JSON string, YAML string, URL, dict, or file path
|
|
83
|
+
service: Service name used as prefix for tool names (e.g., "petstore")
|
|
84
|
+
base_url: Override base URL from spec
|
|
85
|
+
api_key: API key for authentication
|
|
86
|
+
auth_type: Authentication type ("bearer", "apikey", "basic")
|
|
87
|
+
auth_header: Header name for authentication (default: "Authorization")
|
|
88
|
+
api_key_location: Where to put API key ("header" or "query")
|
|
89
|
+
api_key_name: Name of API key parameter (for query params)
|
|
90
|
+
credentials: Alternative credentials dict (username/password for basic auth)
|
|
91
|
+
use_proxy: Enable proxy usage
|
|
92
|
+
timeout: Request timeout in seconds
|
|
93
|
+
debug: Enable debug logging
|
|
94
|
+
**kwargs: Additional toolkit configuration
|
|
95
|
+
"""
|
|
96
|
+
super().__init__(**kwargs)
|
|
97
|
+
|
|
98
|
+
self.service = service
|
|
99
|
+
self.debug = debug
|
|
100
|
+
self.logger = logging.getLogger(f'Parrot.Tools.OpenAPIToolkit.{service}')
|
|
101
|
+
|
|
102
|
+
# Load and parse OpenAPI spec with prance (auto-resolves all $refs)
|
|
103
|
+
self.raw_spec = self._load_spec_with_prance(spec)
|
|
104
|
+
self.spec = self.raw_spec # Already resolved by prance
|
|
105
|
+
|
|
106
|
+
# Extract base URL
|
|
107
|
+
self.base_url = base_url or self._extract_base_url()
|
|
108
|
+
if not self.base_url:
|
|
109
|
+
raise ValueError("No base URL found in spec and none provided")
|
|
110
|
+
|
|
111
|
+
# Validate base URL is absolute
|
|
112
|
+
if self.base_url.startswith('/'):
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"Base URL '{self.base_url}' is relative. "
|
|
115
|
+
"Please provide an absolute base_url parameter or ensure the OpenAPI spec "
|
|
116
|
+
"contains an absolute server URL. "
|
|
117
|
+
f"Example: OpenAPIToolkit(spec=..., service='{service}', "
|
|
118
|
+
f"base_url='https://example.com{self.base_url}')"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Ensure base URL has protocol
|
|
122
|
+
parsed_base = urlparse(self.base_url)
|
|
123
|
+
if not parsed_base.scheme:
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"Base URL '{self.base_url}' is missing protocol (http:// or https://). "
|
|
126
|
+
"Please provide a complete base_url parameter."
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Setup authentication
|
|
130
|
+
self.api_key = api_key
|
|
131
|
+
self.auth_type = auth_type
|
|
132
|
+
self.auth_header = auth_header
|
|
133
|
+
self.api_key_location = api_key_location
|
|
134
|
+
self.api_key_name = api_key_name
|
|
135
|
+
|
|
136
|
+
# Prepare credentials and headers for HTTPService
|
|
137
|
+
creds = credentials or {}
|
|
138
|
+
headers = {}
|
|
139
|
+
|
|
140
|
+
if api_key:
|
|
141
|
+
if auth_type == "bearer":
|
|
142
|
+
creds['token'] = api_key
|
|
143
|
+
elif auth_type == "apikey":
|
|
144
|
+
if api_key_location == "header":
|
|
145
|
+
headers[auth_header] = api_key
|
|
146
|
+
# For query params, we'll add it per request
|
|
147
|
+
else:
|
|
148
|
+
creds['apikey'] = api_key
|
|
149
|
+
|
|
150
|
+
# Initialize HTTPService
|
|
151
|
+
self.http_service = HTTPService(
|
|
152
|
+
accept='application/json',
|
|
153
|
+
headers=headers,
|
|
154
|
+
credentials=creds,
|
|
155
|
+
use_proxy=use_proxy,
|
|
156
|
+
timeout=timeout,
|
|
157
|
+
debug=debug,
|
|
158
|
+
**kwargs
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Parse operations from spec
|
|
162
|
+
self.operations = self._parse_operations()
|
|
163
|
+
|
|
164
|
+
# OPTIMIZATION 3: Detect if this is a single-operation spec
|
|
165
|
+
self.is_single_operation = len(self.operations) == 1
|
|
166
|
+
|
|
167
|
+
if self.debug:
|
|
168
|
+
self.logger.debug(
|
|
169
|
+
f"Loaded {len(self.operations)} operations. "
|
|
170
|
+
f"Single operation mode: {self.is_single_operation}"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Generate tools dynamically
|
|
174
|
+
self._generate_dynamic_methods()
|
|
175
|
+
|
|
176
|
+
def _load_spec_with_prance(self, spec: Union[str, Dict[str, Any]]) -> Dict[str, Any]:
|
|
177
|
+
"""
|
|
178
|
+
Load OpenAPI specification using prance for automatic reference resolution.
|
|
179
|
+
|
|
180
|
+
prance automatically:
|
|
181
|
+
- Resolves ALL $ref references (internal and external)
|
|
182
|
+
- Validates OpenAPI spec
|
|
183
|
+
- Handles YAML and JSON
|
|
184
|
+
- Supports local files and URLs
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
spec: URL, file path, JSON/YAML string, or dict
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Fully resolved specification as dictionary (NEVER returns None)
|
|
191
|
+
"""
|
|
192
|
+
# Track source URL for relative URL resolution
|
|
193
|
+
self._spec_source_url = None
|
|
194
|
+
|
|
195
|
+
# If already a dict, use prance to resolve it
|
|
196
|
+
if isinstance(spec, dict):
|
|
197
|
+
if PRANCE_AVAILABLE:
|
|
198
|
+
try:
|
|
199
|
+
# Create temporary file for prance to parse
|
|
200
|
+
import tempfile
|
|
201
|
+
temp_path = None
|
|
202
|
+
try:
|
|
203
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
|
204
|
+
json.dump(spec, f)
|
|
205
|
+
temp_path = f.name
|
|
206
|
+
|
|
207
|
+
parser = ResolvingParser(temp_path, lazy=True, strict=False)
|
|
208
|
+
resolved_spec = parser.specification
|
|
209
|
+
|
|
210
|
+
# Clean up temp file
|
|
211
|
+
if temp_path:
|
|
212
|
+
Path(temp_path).unlink(missing_ok=True)
|
|
213
|
+
|
|
214
|
+
# Ensure we have a valid spec
|
|
215
|
+
if resolved_spec and isinstance(resolved_spec, dict):
|
|
216
|
+
return resolved_spec
|
|
217
|
+
else:
|
|
218
|
+
self.logger.warning("prance returned invalid spec, using original dict")
|
|
219
|
+
return spec
|
|
220
|
+
|
|
221
|
+
except Exception as inner_e:
|
|
222
|
+
# Clean up temp file on error
|
|
223
|
+
if temp_path:
|
|
224
|
+
with contextlib.suppress(Exception):
|
|
225
|
+
Path(temp_path).unlink(missing_ok=True)
|
|
226
|
+
raise inner_e
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
self.logger.warning(f"prance parsing failed, using dict as-is: {e}")
|
|
230
|
+
# ALWAYS return the original spec as fallback
|
|
231
|
+
return spec
|
|
232
|
+
else:
|
|
233
|
+
# Prance not available, return original dict
|
|
234
|
+
return spec
|
|
235
|
+
|
|
236
|
+
# Check if it's a URL
|
|
237
|
+
parsed = urlparse(spec)
|
|
238
|
+
if parsed.scheme in ('http', 'https'):
|
|
239
|
+
if self.debug:
|
|
240
|
+
self.logger.debug(f"Loading spec from URL: {spec}")
|
|
241
|
+
self._spec_source_url = spec
|
|
242
|
+
|
|
243
|
+
if PRANCE_AVAILABLE:
|
|
244
|
+
# Let prance handle the URL directly
|
|
245
|
+
try:
|
|
246
|
+
parser = ResolvingParser(spec, lazy=True, strict=False)
|
|
247
|
+
resolved_spec = parser.specification
|
|
248
|
+
if resolved_spec and isinstance(resolved_spec, dict):
|
|
249
|
+
return resolved_spec
|
|
250
|
+
except Exception as e:
|
|
251
|
+
self.logger.warning(f"prance URL parsing failed: {e}")
|
|
252
|
+
# Fall through to manual loading
|
|
253
|
+
|
|
254
|
+
# Fallback: manual download and parse
|
|
255
|
+
response = httpx.get(spec, timeout=30)
|
|
256
|
+
response.raise_for_status()
|
|
257
|
+
content = response.text
|
|
258
|
+
|
|
259
|
+
# Parse manually
|
|
260
|
+
try:
|
|
261
|
+
loaded = yaml.safe_load(content)
|
|
262
|
+
if loaded and isinstance(loaded, dict):
|
|
263
|
+
return loaded
|
|
264
|
+
except Exception:
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
# Try JSON
|
|
268
|
+
loaded = json.loads(content)
|
|
269
|
+
if loaded and isinstance(loaded, dict):
|
|
270
|
+
return loaded
|
|
271
|
+
|
|
272
|
+
raise ValueError(f"Failed to parse spec from URL: {spec}")
|
|
273
|
+
|
|
274
|
+
# Check if it's a file path
|
|
275
|
+
elif Path(spec).exists():
|
|
276
|
+
if self.debug:
|
|
277
|
+
self.logger.debug(f"Loading spec from file: {spec}")
|
|
278
|
+
|
|
279
|
+
if PRANCE_AVAILABLE:
|
|
280
|
+
try:
|
|
281
|
+
parser = ResolvingParser(str(spec), lazy=True, strict=False)
|
|
282
|
+
resolved_spec = parser.specification
|
|
283
|
+
if resolved_spec and isinstance(resolved_spec, dict):
|
|
284
|
+
return resolved_spec
|
|
285
|
+
except Exception as e:
|
|
286
|
+
self.logger.warning(f"prance file parsing failed: {e}")
|
|
287
|
+
# Fall through to manual loading
|
|
288
|
+
|
|
289
|
+
# Fallback: manual loading
|
|
290
|
+
with open(spec, 'r', encoding='utf-8') as f:
|
|
291
|
+
content = f.read()
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
loaded = yaml.safe_load(content)
|
|
295
|
+
if loaded and isinstance(loaded, dict):
|
|
296
|
+
return loaded
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
# Try JSON
|
|
301
|
+
loaded = json.loads(content)
|
|
302
|
+
if loaded and isinstance(loaded, dict):
|
|
303
|
+
return loaded
|
|
304
|
+
|
|
305
|
+
raise ValueError(f"Failed to parse spec from file: {spec}")
|
|
306
|
+
|
|
307
|
+
# Otherwise, treat as string content
|
|
308
|
+
else:
|
|
309
|
+
if self.debug:
|
|
310
|
+
self.logger.debug("Parsing spec from string")
|
|
311
|
+
|
|
312
|
+
# Try YAML first (superset of JSON)
|
|
313
|
+
try:
|
|
314
|
+
loaded = yaml.safe_load(spec)
|
|
315
|
+
if loaded and isinstance(loaded, dict):
|
|
316
|
+
return loaded
|
|
317
|
+
except Exception:
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
# Try JSON
|
|
321
|
+
try:
|
|
322
|
+
loaded = json.loads(spec)
|
|
323
|
+
if loaded and isinstance(loaded, dict):
|
|
324
|
+
return loaded
|
|
325
|
+
except Exception:
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
raise ValueError(
|
|
329
|
+
"Failed to parse spec from string"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def _extract_base_url(self) -> Optional[str]:
|
|
333
|
+
"""Extract base URL from OpenAPI servers section."""
|
|
334
|
+
servers = self.spec.get('servers', [])
|
|
335
|
+
if servers and len(servers) > 0:
|
|
336
|
+
server_url = servers[0].get('url', '')
|
|
337
|
+
|
|
338
|
+
# Handle server variables if present
|
|
339
|
+
variables = servers[0].get('variables', {})
|
|
340
|
+
for var_name, var_config in variables.items():
|
|
341
|
+
default_value = var_config.get('default', '')
|
|
342
|
+
server_url = server_url.replace(f'{{{var_name}}}', default_value)
|
|
343
|
+
|
|
344
|
+
# If server URL is relative (starts with /), construct absolute URL
|
|
345
|
+
# from the spec source if it was loaded from URL
|
|
346
|
+
if server_url.startswith('/') and hasattr(self, '_spec_source_url') and self._spec_source_url:
|
|
347
|
+
parsed = urlparse(self._spec_source_url)
|
|
348
|
+
base = f"{parsed.scheme}://{parsed.netloc}"
|
|
349
|
+
server_url = base + server_url
|
|
350
|
+
|
|
351
|
+
return server_url
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
def _parse_operations(self) -> List[Dict[str, Any]]:
|
|
355
|
+
"""
|
|
356
|
+
Parse OpenAPI paths into operation definitions.
|
|
357
|
+
|
|
358
|
+
Each operation includes:
|
|
359
|
+
- operation_id: Unique identifier
|
|
360
|
+
- path: API path
|
|
361
|
+
- method: HTTP method
|
|
362
|
+
- parameters: Path, query, and header params
|
|
363
|
+
- request_body: Body schema with content type info
|
|
364
|
+
- description/summary: Operation description
|
|
365
|
+
"""
|
|
366
|
+
operations = []
|
|
367
|
+
|
|
368
|
+
for path, path_item in self.spec.get('paths', {}).items():
|
|
369
|
+
# HTTP methods
|
|
370
|
+
for method in ['get', 'post', 'put', 'patch', 'delete', 'head', 'options']:
|
|
371
|
+
if method not in path_item:
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
operation_spec = path_item[method]
|
|
375
|
+
|
|
376
|
+
# Generate operation ID if not present
|
|
377
|
+
operation_id = operation_spec.get(
|
|
378
|
+
'operationId',
|
|
379
|
+
f"{method}_{path.replace('/', '_').replace('{', '').replace('}', '')}"
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Parse parameters
|
|
383
|
+
parameters = {
|
|
384
|
+
'path': [],
|
|
385
|
+
'query': [],
|
|
386
|
+
'header': [],
|
|
387
|
+
'cookie': []
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
for param in operation_spec.get('parameters', []):
|
|
391
|
+
param_in = param.get('in', 'query')
|
|
392
|
+
if param_in in parameters:
|
|
393
|
+
parameters[param_in].append(param)
|
|
394
|
+
|
|
395
|
+
# IMPROVEMENT 2: Parse request body with content type detection
|
|
396
|
+
request_body = None
|
|
397
|
+
if 'requestBody' in operation_spec:
|
|
398
|
+
request_body_spec = operation_spec['requestBody']
|
|
399
|
+
content = request_body_spec.get('content', {})
|
|
400
|
+
|
|
401
|
+
# Detect content type - prioritize JSON, then form-urlencoded
|
|
402
|
+
content_type = None
|
|
403
|
+
schema = None
|
|
404
|
+
|
|
405
|
+
if 'application/json' in content:
|
|
406
|
+
content_type = 'application/json'
|
|
407
|
+
schema = content['application/json'].get('schema', {})
|
|
408
|
+
elif 'application/x-www-form-urlencoded' in content:
|
|
409
|
+
content_type = 'application/x-www-form-urlencoded'
|
|
410
|
+
schema = content['application/x-www-form-urlencoded'].get('schema', {})
|
|
411
|
+
elif content:
|
|
412
|
+
# Fallback to first available content type
|
|
413
|
+
content_type = list(content.keys())[0]
|
|
414
|
+
schema = content[content_type].get('schema', {})
|
|
415
|
+
|
|
416
|
+
if schema:
|
|
417
|
+
request_body = {
|
|
418
|
+
'schema': schema,
|
|
419
|
+
'required': request_body_spec.get('required', False),
|
|
420
|
+
'description': request_body_spec.get('description', ''),
|
|
421
|
+
'content_type': content_type # Track content type
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
operations.append({
|
|
425
|
+
'operation_id': operation_id,
|
|
426
|
+
'path': path,
|
|
427
|
+
'method': method.upper(),
|
|
428
|
+
'parameters': parameters,
|
|
429
|
+
'request_body': request_body,
|
|
430
|
+
'summary': operation_spec.get('summary', ''),
|
|
431
|
+
'description': operation_spec.get('description', ''),
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
return operations
|
|
435
|
+
|
|
436
|
+
def _normalize_path_for_method_name(self, path: str) -> str:
|
|
437
|
+
"""
|
|
438
|
+
Normalize path for method name.
|
|
439
|
+
|
|
440
|
+
Examples:
|
|
441
|
+
/pet/{petId} -> pet_petid
|
|
442
|
+
/store/inventory -> store_inventory
|
|
443
|
+
/user/login -> user_login
|
|
444
|
+
"""
|
|
445
|
+
# Remove leading/trailing slashes
|
|
446
|
+
path = path.strip('/')
|
|
447
|
+
|
|
448
|
+
# Replace path parameters {petId} with just petid
|
|
449
|
+
path = re.sub(r'\{([^}]+)\}', r'\1', path)
|
|
450
|
+
|
|
451
|
+
# Replace slashes and special chars with underscores
|
|
452
|
+
path = re.sub(r'[^a-zA-Z0-9]+', '_', path)
|
|
453
|
+
|
|
454
|
+
# Convert to lowercase
|
|
455
|
+
path = path.lower()
|
|
456
|
+
|
|
457
|
+
return path
|
|
458
|
+
|
|
459
|
+
def _resolve_schema_ref(self, schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
460
|
+
"""
|
|
461
|
+
Manually resolve $ref if present.
|
|
462
|
+
|
|
463
|
+
This is a fallback for when prance fails to resolve references.
|
|
464
|
+
Works with internal references only (#/components/schemas/...).
|
|
465
|
+
"""
|
|
466
|
+
if '$ref' not in schema:
|
|
467
|
+
return schema
|
|
468
|
+
|
|
469
|
+
ref_path = schema['$ref']
|
|
470
|
+
if not ref_path.startswith('#/'):
|
|
471
|
+
# External refs not supported in this fallback
|
|
472
|
+
return schema
|
|
473
|
+
|
|
474
|
+
# Navigate the reference path
|
|
475
|
+
parts = ref_path[2:].split('/') # Remove '#/' prefix
|
|
476
|
+
resolved = self.spec
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
for part in parts:
|
|
480
|
+
resolved = resolved[part]
|
|
481
|
+
|
|
482
|
+
# Recursively resolve nested refs
|
|
483
|
+
return self._resolve_schema_ref(resolved)
|
|
484
|
+
except (KeyError, TypeError):
|
|
485
|
+
# If resolution fails, return original
|
|
486
|
+
return schema
|
|
487
|
+
|
|
488
|
+
def _create_pydantic_schema(
|
|
489
|
+
self,
|
|
490
|
+
operation: Dict[str, Any]
|
|
491
|
+
) -> type[BaseModel]:
|
|
492
|
+
"""
|
|
493
|
+
Create Pydantic model for operation parameters.
|
|
494
|
+
|
|
495
|
+
IMPROVEMENT 3: For single-operation specs, create minimal schema
|
|
496
|
+
without redundant path/method fields for better LLM experience.
|
|
497
|
+
"""
|
|
498
|
+
fields = {}
|
|
499
|
+
|
|
500
|
+
# OPTIMIZATION: Skip path/method fields for single-operation specs
|
|
501
|
+
skip_meta_fields = self.is_single_operation
|
|
502
|
+
|
|
503
|
+
# Add path parameters (always required)
|
|
504
|
+
for param in operation['parameters'].get('path', []):
|
|
505
|
+
field_type = self._openapi_type_to_python(param['schema'])
|
|
506
|
+
field_info = Field(
|
|
507
|
+
description=param.get('description', f"Path parameter: {param['name']}")
|
|
508
|
+
)
|
|
509
|
+
fields[param['name']] = (field_type, field_info)
|
|
510
|
+
|
|
511
|
+
# Add query parameters
|
|
512
|
+
for param in operation['parameters'].get('query', []):
|
|
513
|
+
field_type = self._openapi_type_to_python(param['schema'])
|
|
514
|
+
if is_required := param.get('required', False):
|
|
515
|
+
field_info = Field(
|
|
516
|
+
description=param.get('description', f"Query parameter: {param['name']}")
|
|
517
|
+
)
|
|
518
|
+
fields[param['name']] = (field_type, field_info)
|
|
519
|
+
else:
|
|
520
|
+
field_info = Field(
|
|
521
|
+
default=None,
|
|
522
|
+
description=param.get('description', f"Query parameter: {param['name']}")
|
|
523
|
+
)
|
|
524
|
+
fields[param['name']] = (Optional[field_type], field_info)
|
|
525
|
+
|
|
526
|
+
# Add header parameters (usually optional)
|
|
527
|
+
for param in operation['parameters'].get('header', []):
|
|
528
|
+
field_type = self._openapi_type_to_python(param['schema'])
|
|
529
|
+
if is_required := param.get('required', False):
|
|
530
|
+
field_info = Field(
|
|
531
|
+
description=param.get('description', f"Header parameter: {param['name']}")
|
|
532
|
+
)
|
|
533
|
+
fields[param['name']] = (field_type, field_info)
|
|
534
|
+
else:
|
|
535
|
+
field_info = Field(
|
|
536
|
+
default=None,
|
|
537
|
+
description=param.get('description', f"Header parameter: {param['name']}")
|
|
538
|
+
)
|
|
539
|
+
fields[param['name']] = (Optional[field_type], field_info)
|
|
540
|
+
|
|
541
|
+
# Add request body fields
|
|
542
|
+
if operation['request_body']:
|
|
543
|
+
schema = operation['request_body']['schema']
|
|
544
|
+
|
|
545
|
+
# CRITICAL FIX: Manually resolve $ref if prance failed
|
|
546
|
+
# This ensures we always get the actual schema with type and properties
|
|
547
|
+
schema = self._resolve_schema_ref(schema)
|
|
548
|
+
|
|
549
|
+
# If request body is a single object, flatten its properties
|
|
550
|
+
if schema.get('type') == 'object' and 'properties' in schema:
|
|
551
|
+
for field_name, field_schema in schema['properties'].items():
|
|
552
|
+
field_type = self._openapi_type_to_python(field_schema)
|
|
553
|
+
required = field_name in schema.get('required', [])
|
|
554
|
+
|
|
555
|
+
if required:
|
|
556
|
+
field_info = Field(
|
|
557
|
+
description=field_schema.get('description', f"Body parameter: {field_name}")
|
|
558
|
+
)
|
|
559
|
+
fields[field_name] = (field_type, field_info)
|
|
560
|
+
else:
|
|
561
|
+
field_info = Field(
|
|
562
|
+
default=None,
|
|
563
|
+
description=field_schema.get('description', f"Body parameter: {field_name}")
|
|
564
|
+
)
|
|
565
|
+
fields[field_name] = (Optional[field_type], field_info)
|
|
566
|
+
else:
|
|
567
|
+
# For non-object bodies, create a single 'body' field
|
|
568
|
+
field_type = self._openapi_type_to_python(schema)
|
|
569
|
+
if is_required := operation['request_body'].get('required', False):
|
|
570
|
+
field_info = Field(
|
|
571
|
+
description=operation['request_body'].get('description', 'Request body')
|
|
572
|
+
)
|
|
573
|
+
fields['body'] = (field_type, field_info)
|
|
574
|
+
else:
|
|
575
|
+
field_info = Field(
|
|
576
|
+
default=None,
|
|
577
|
+
description=operation['request_body'].get('description', 'Request body')
|
|
578
|
+
)
|
|
579
|
+
fields['body'] = (Optional[field_type], field_info)
|
|
580
|
+
|
|
581
|
+
# Create dynamic model
|
|
582
|
+
model_name = f"{operation['operation_id']}_Schema"
|
|
583
|
+
|
|
584
|
+
# If no fields, create empty schema
|
|
585
|
+
if not fields:
|
|
586
|
+
return create_model(model_name)
|
|
587
|
+
|
|
588
|
+
return create_model(model_name, **fields)
|
|
589
|
+
|
|
590
|
+
def _openapi_type_to_python(self, schema: Dict[str, Any]) -> type:
|
|
591
|
+
"""
|
|
592
|
+
Convert OpenAPI schema type to Python type.
|
|
593
|
+
|
|
594
|
+
Args:
|
|
595
|
+
schema: OpenAPI schema definition
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
Corresponding Python type
|
|
599
|
+
"""
|
|
600
|
+
schema_type = schema.get('type', 'string')
|
|
601
|
+
schema_format = schema.get('format')
|
|
602
|
+
|
|
603
|
+
# Handle arrays
|
|
604
|
+
if schema_type == 'array':
|
|
605
|
+
items_schema = schema.get('items', {'type': 'string'})
|
|
606
|
+
item_type = self._openapi_type_to_python(items_schema)
|
|
607
|
+
return List[item_type]
|
|
608
|
+
|
|
609
|
+
# Handle objects (as dict)
|
|
610
|
+
if schema_type == 'object':
|
|
611
|
+
return Dict[str, Any]
|
|
612
|
+
|
|
613
|
+
# Handle primitive types
|
|
614
|
+
type_mapping = {
|
|
615
|
+
'string': str,
|
|
616
|
+
'integer': int,
|
|
617
|
+
'number': float,
|
|
618
|
+
'boolean': bool,
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
# Consider format for more specific types
|
|
622
|
+
if schema_type == 'integer' and schema_format == 'int64':
|
|
623
|
+
return int
|
|
624
|
+
if schema_type == 'number' and schema_format == 'float':
|
|
625
|
+
return float
|
|
626
|
+
|
|
627
|
+
return type_mapping.get(schema_type, str)
|
|
628
|
+
|
|
629
|
+
def _generate_dynamic_methods(self):
|
|
630
|
+
"""
|
|
631
|
+
Generate dynamic async methods for each operation.
|
|
632
|
+
|
|
633
|
+
This is the magic that converts OpenAPI operations into toolkit methods.
|
|
634
|
+
Each method becomes a tool automatically via AbstractToolkit.get_tools().
|
|
635
|
+
"""
|
|
636
|
+
for operation in self.operations:
|
|
637
|
+
# Generate method name: {service}_{method}_{normalized_path}
|
|
638
|
+
method_name = self._create_method_name(operation)
|
|
639
|
+
|
|
640
|
+
# Create the async method
|
|
641
|
+
async_method = self._create_operation_method(operation)
|
|
642
|
+
|
|
643
|
+
# Create and attach Pydantic schema for argument validation
|
|
644
|
+
pydantic_schema = self._create_pydantic_schema(operation)
|
|
645
|
+
async_method._args_schema = pydantic_schema
|
|
646
|
+
|
|
647
|
+
# Bind method to instance
|
|
648
|
+
bound_method = async_method.__get__(self, self.__class__)
|
|
649
|
+
setattr(self, method_name, bound_method)
|
|
650
|
+
|
|
651
|
+
if self.debug:
|
|
652
|
+
self.logger.debug(
|
|
653
|
+
f"Created tool method: {method_name} "
|
|
654
|
+
f"for {operation['method']} {operation['path']}"
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
def _create_method_name(self, operation: Dict[str, Any]) -> str:
|
|
658
|
+
"""
|
|
659
|
+
Create method name following convention: {service}_{method}_{path}
|
|
660
|
+
|
|
661
|
+
Examples:
|
|
662
|
+
petstore_get_pet_petid
|
|
663
|
+
petstore_post_pet
|
|
664
|
+
petstore_get_store_inventory
|
|
665
|
+
"""
|
|
666
|
+
method = operation['method'].lower()
|
|
667
|
+
path = self._normalize_path_for_method_name(operation['path'])
|
|
668
|
+
|
|
669
|
+
# Combine with service prefix
|
|
670
|
+
method_name = f"{self.service}_{method}_{path}"
|
|
671
|
+
|
|
672
|
+
# Clean up multiple underscores
|
|
673
|
+
method_name = re.sub(r'_+', '_', method_name)
|
|
674
|
+
|
|
675
|
+
# Remove trailing underscores
|
|
676
|
+
method_name = method_name.strip('_')
|
|
677
|
+
|
|
678
|
+
return method_name
|
|
679
|
+
|
|
680
|
+
def _create_operation_method(self, operation: Dict[str, Any]):
|
|
681
|
+
"""
|
|
682
|
+
Create an async method that executes the OpenAPI operation.
|
|
683
|
+
|
|
684
|
+
This method will be called when the LLM uses the tool.
|
|
685
|
+
"""
|
|
686
|
+
# Create the implementation
|
|
687
|
+
async def operation_method(self_ref, **kwargs) -> Dict[str, Any]:
|
|
688
|
+
"""
|
|
689
|
+
Execute OpenAPI operation.
|
|
690
|
+
|
|
691
|
+
This docstring will be dynamically set for each operation.
|
|
692
|
+
"""
|
|
693
|
+
try:
|
|
694
|
+
# Build URL with path parameters
|
|
695
|
+
url = self_ref._build_operation_url(operation, kwargs)
|
|
696
|
+
|
|
697
|
+
# Separate query parameters
|
|
698
|
+
query_params = self_ref._extract_query_params(operation, kwargs)
|
|
699
|
+
|
|
700
|
+
# Extract header parameters
|
|
701
|
+
header_params = self_ref._extract_header_params(operation, kwargs)
|
|
702
|
+
|
|
703
|
+
# IMPROVEMENT 2: Build request body with content type handling
|
|
704
|
+
body_data, content_type = self_ref._extract_body_data(operation, kwargs)
|
|
705
|
+
|
|
706
|
+
# Make request
|
|
707
|
+
method = operation['method']
|
|
708
|
+
|
|
709
|
+
if self_ref.debug:
|
|
710
|
+
self_ref.logger.debug(
|
|
711
|
+
f"Executing {method} {url} with "
|
|
712
|
+
f"params={query_params}, headers={header_params}, "
|
|
713
|
+
f"body={body_data}, content_type={content_type}"
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
# Prepare request headers
|
|
717
|
+
request_headers = header_params.copy() if header_params else {}
|
|
718
|
+
|
|
719
|
+
# Determine content type and prepare request body
|
|
720
|
+
use_json = True
|
|
721
|
+
if method in ['POST', 'PUT', 'PATCH'] and body_data:
|
|
722
|
+
if content_type == 'application/x-www-form-urlencoded':
|
|
723
|
+
# Send as form data
|
|
724
|
+
use_json = False
|
|
725
|
+
request_headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
726
|
+
|
|
727
|
+
# Create request kwargs
|
|
728
|
+
request_kwargs = {
|
|
729
|
+
'url': url,
|
|
730
|
+
'method': method,
|
|
731
|
+
'params': query_params,
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
# Only add headers if we have any
|
|
735
|
+
if request_headers:
|
|
736
|
+
request_kwargs['headers'] = request_headers
|
|
737
|
+
|
|
738
|
+
# Add body data for POST/PUT/PATCH
|
|
739
|
+
if method in ['POST', 'PUT', 'PATCH'] and body_data:
|
|
740
|
+
request_kwargs['use_json'] = use_json
|
|
741
|
+
request_kwargs['data'] = body_data
|
|
742
|
+
|
|
743
|
+
# Execute request via HTTPService
|
|
744
|
+
result, error = await self_ref.http_service.request(
|
|
745
|
+
**request_kwargs,
|
|
746
|
+
full_response=False,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
if error:
|
|
750
|
+
return ToolResult(
|
|
751
|
+
status="error",
|
|
752
|
+
result=None,
|
|
753
|
+
error=str(error),
|
|
754
|
+
metadata={
|
|
755
|
+
'operation_id': operation['operation_id'],
|
|
756
|
+
'method': method,
|
|
757
|
+
'url': url,
|
|
758
|
+
}
|
|
759
|
+
).model_dump()
|
|
760
|
+
|
|
761
|
+
return ToolResult(
|
|
762
|
+
status="success",
|
|
763
|
+
result=result,
|
|
764
|
+
metadata={
|
|
765
|
+
'operation_id': operation['operation_id'],
|
|
766
|
+
'method': method,
|
|
767
|
+
'url': url,
|
|
768
|
+
}
|
|
769
|
+
).model_dump()
|
|
770
|
+
|
|
771
|
+
except Exception as e:
|
|
772
|
+
self_ref.logger.error(f"Error executing operation: {e}", exc_info=True)
|
|
773
|
+
return ToolResult(
|
|
774
|
+
status="error",
|
|
775
|
+
result=None,
|
|
776
|
+
error=str(e),
|
|
777
|
+
metadata={'operation_id': operation['operation_id']}
|
|
778
|
+
).model_dump()
|
|
779
|
+
|
|
780
|
+
# Set dynamic docstring
|
|
781
|
+
description = (
|
|
782
|
+
operation.get('description') or
|
|
783
|
+
operation.get('summary', '') or
|
|
784
|
+
f"{operation['method']} {operation['path']}"
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
operation_method.__doc__ = f"{description}\n\nOperation: {operation['operation_id']}"
|
|
788
|
+
operation_method.__name__ = self._create_method_name(operation)
|
|
789
|
+
|
|
790
|
+
# Store operation for later reference
|
|
791
|
+
operation_method._operation = operation
|
|
792
|
+
|
|
793
|
+
return operation_method
|
|
794
|
+
|
|
795
|
+
def _build_operation_url(
|
|
796
|
+
self,
|
|
797
|
+
operation: Dict[str, Any],
|
|
798
|
+
params: Dict[str, Any]
|
|
799
|
+
) -> str:
|
|
800
|
+
"""
|
|
801
|
+
Build complete URL with path parameters substituted.
|
|
802
|
+
|
|
803
|
+
Args:
|
|
804
|
+
operation: Operation definition
|
|
805
|
+
params: Parameters provided by LLM
|
|
806
|
+
|
|
807
|
+
Returns:
|
|
808
|
+
Complete URL with path params substituted
|
|
809
|
+
"""
|
|
810
|
+
path = operation['path']
|
|
811
|
+
|
|
812
|
+
# Substitute path parameters
|
|
813
|
+
for param in operation['parameters'].get('path', []):
|
|
814
|
+
param_name = param['name']
|
|
815
|
+
if param_name in params:
|
|
816
|
+
placeholder = f"{{{param_name}}}"
|
|
817
|
+
path = path.replace(placeholder, str(params[param_name]))
|
|
818
|
+
|
|
819
|
+
# Combine with base URL
|
|
820
|
+
return f"{self.base_url.rstrip('/')}/{path.lstrip('/')}"
|
|
821
|
+
|
|
822
|
+
def _extract_query_params(
|
|
823
|
+
self,
|
|
824
|
+
operation: Dict[str, Any],
|
|
825
|
+
params: Dict[str, Any]
|
|
826
|
+
) -> Dict[str, Any]:
|
|
827
|
+
"""Extract query parameters from provided params."""
|
|
828
|
+
query_params = {}
|
|
829
|
+
|
|
830
|
+
for param in operation['parameters'].get('query', []):
|
|
831
|
+
param_name = param['name']
|
|
832
|
+
if param_name in params and params[param_name] is not None:
|
|
833
|
+
query_params[param_name] = params[param_name]
|
|
834
|
+
|
|
835
|
+
# Add API key if configured for query params
|
|
836
|
+
if self.api_key and self.api_key_location == "query":
|
|
837
|
+
query_params[self.api_key_name] = self.api_key
|
|
838
|
+
|
|
839
|
+
return query_params
|
|
840
|
+
|
|
841
|
+
def _extract_header_params(
|
|
842
|
+
self,
|
|
843
|
+
operation: Dict[str, Any],
|
|
844
|
+
params: Dict[str, Any]
|
|
845
|
+
) -> Dict[str, str]:
|
|
846
|
+
"""Extract header parameters from provided params."""
|
|
847
|
+
header_params = {}
|
|
848
|
+
|
|
849
|
+
for param in operation['parameters'].get('header', []):
|
|
850
|
+
param_name = param['name']
|
|
851
|
+
if param_name in params and params[param_name] is not None:
|
|
852
|
+
header_params[param_name] = str(params[param_name])
|
|
853
|
+
|
|
854
|
+
return header_params
|
|
855
|
+
|
|
856
|
+
def _extract_body_data(
|
|
857
|
+
self,
|
|
858
|
+
operation: Dict[str, Any],
|
|
859
|
+
params: Dict[str, Any]
|
|
860
|
+
) -> tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
861
|
+
"""
|
|
862
|
+
Extract request body data from provided params.
|
|
863
|
+
|
|
864
|
+
IMPROVEMENT 2: Returns tuple of (body_data, content_type) to handle
|
|
865
|
+
different content types like application/json and application/x-www-form-urlencoded
|
|
866
|
+
|
|
867
|
+
Returns:
|
|
868
|
+
Tuple of (body_data, content_type)
|
|
869
|
+
"""
|
|
870
|
+
if not operation['request_body']:
|
|
871
|
+
return None, None
|
|
872
|
+
|
|
873
|
+
# Get schema and content type
|
|
874
|
+
schema = operation['request_body']['schema']
|
|
875
|
+
content_type = operation['request_body'].get('content_type', 'application/json')
|
|
876
|
+
|
|
877
|
+
# If schema is an object with properties, extract those fields
|
|
878
|
+
if schema.get('type') == 'object' and 'properties' in schema:
|
|
879
|
+
body = {
|
|
880
|
+
prop_name: params[prop_name]
|
|
881
|
+
for prop_name in schema['properties'].keys()
|
|
882
|
+
if prop_name in params and params[prop_name] is not None
|
|
883
|
+
}
|
|
884
|
+
return body or None, content_type
|
|
885
|
+
|
|
886
|
+
# Otherwise, look for a 'body' parameter
|
|
887
|
+
if 'body' in params:
|
|
888
|
+
return params['body'], content_type
|
|
889
|
+
|
|
890
|
+
# Fallback: use all non-path, non-query, and non-header params
|
|
891
|
+
path_params = {p['name'] for p in operation['parameters'].get('path', [])}
|
|
892
|
+
query_params = {p['name'] for p in operation['parameters'].get('query', [])}
|
|
893
|
+
header_params = {p['name'] for p in operation['parameters'].get('header', [])}
|
|
894
|
+
|
|
895
|
+
body = {
|
|
896
|
+
key: value
|
|
897
|
+
for key, value in params.items()
|
|
898
|
+
if key not in path_params
|
|
899
|
+
and key not in query_params
|
|
900
|
+
and key not in header_params
|
|
901
|
+
and value is not None
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
return body or None, content_type
|