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,1251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Migrated Google Tools using the AbstractTool framework.
|
|
3
|
+
"""
|
|
4
|
+
import asyncio
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import string
|
|
10
|
+
import tempfile
|
|
11
|
+
import aiohttp
|
|
12
|
+
import orjson
|
|
13
|
+
from pydantic import BaseModel, Field, field_validator
|
|
14
|
+
from googleapiclient.discovery import build
|
|
15
|
+
from navconfig import config
|
|
16
|
+
from markitdown import MarkItDown
|
|
17
|
+
from ...conf import GOOGLE_API_KEY
|
|
18
|
+
from ..abstract import AbstractTool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Schema definitions
|
|
22
|
+
class GoogleSearchArgs(BaseModel):
|
|
23
|
+
"""Arguments schema for Google Search Tool."""
|
|
24
|
+
query: str = Field(description="Search query")
|
|
25
|
+
max_results: int = Field(default=5, ge=1, le=50, description="Maximum number of results to return")
|
|
26
|
+
preview: bool = Field(default=False, description="If True, fetch full page content for each result")
|
|
27
|
+
preview_method: str = Field(default="aiohttp", description="Method to use for preview: 'aiohttp' or 'selenium'")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GoogleSiteSearchArgs(BaseModel):
|
|
31
|
+
"""Arguments schema for Google Site Search Tool."""
|
|
32
|
+
query: str = Field(description="Search query")
|
|
33
|
+
site: str = Field(description="Site to search within (e.g., 'example.com')")
|
|
34
|
+
max_results: int = Field(default=5, ge=1, le=50, description="Maximum number of results to return")
|
|
35
|
+
preview: bool = Field(default=False, description="If True, fetch full page content for each result")
|
|
36
|
+
preview_method: str = Field(default="aiohttp", description="Method to use for preview: 'aiohttp' or 'selenium'")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GoogleLocationArgs(BaseModel):
|
|
40
|
+
"""Arguments schema for Google Location Finder."""
|
|
41
|
+
address: str = Field(description="Complete address to geocode")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GoogleRouteArgs(BaseModel):
|
|
45
|
+
"""Arguments schema for Google Route Search."""
|
|
46
|
+
origin: str = Field(description="Origin address or coordinates")
|
|
47
|
+
destination: str = Field(description="Destination address or coordinates")
|
|
48
|
+
waypoints: Optional[List[str]] = Field(default=None, description="Optional waypoints between origin and destination")
|
|
49
|
+
travel_mode: str = Field(default="DRIVE", description="Travel mode: DRIVE, WALK, BICYCLE, TRANSIT")
|
|
50
|
+
routing_preference: str = Field(default="TRAFFIC_AWARE", description="Routing preference")
|
|
51
|
+
optimize_waypoints: bool = Field(default=False, description="Whether to optimize waypoint order")
|
|
52
|
+
departure_time: Optional[str] = Field(default=None, description="Departure time in ISO format")
|
|
53
|
+
include_static_map: bool = Field(default=False, description="Whether to include a static map URL")
|
|
54
|
+
include_interactive_map: bool = Field(default=False, description="Whether to generate an interactive HTML map")
|
|
55
|
+
map_size: str = Field(
|
|
56
|
+
default="640x640",
|
|
57
|
+
description="Map size for static map in format 'widthxheight' (e.g., '640x640')"
|
|
58
|
+
)
|
|
59
|
+
map_scale: int = Field(default=2, description="Map scale factor")
|
|
60
|
+
map_type: str = Field(default="roadmap", description="Map type: roadmap, satellite, terrain, hybrid")
|
|
61
|
+
auto_zoom: bool = Field(default=True, description="Automatically calculate zoom based on route distance")
|
|
62
|
+
zoom: int = Field(default=8, description="Manual zoom level (used when auto_zoom=False)")
|
|
63
|
+
|
|
64
|
+
@field_validator('map_size')
|
|
65
|
+
@classmethod
|
|
66
|
+
def validate_map_size(cls, v):
|
|
67
|
+
"""Validate map_size format."""
|
|
68
|
+
if not isinstance(v, str):
|
|
69
|
+
raise ValueError('map_size must be a string')
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
parts = v.split('x')
|
|
73
|
+
if len(parts) != 2:
|
|
74
|
+
raise ValueError('map_size must be in format "widthxheight"')
|
|
75
|
+
|
|
76
|
+
width, height = int(parts[0]), int(parts[1])
|
|
77
|
+
if width <= 0 or height <= 0:
|
|
78
|
+
raise ValueError('map_size dimensions must be positive')
|
|
79
|
+
|
|
80
|
+
return v
|
|
81
|
+
except ValueError as e:
|
|
82
|
+
raise ValueError(f'Invalid map_size format: {e}')
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def map_width(self) -> int:
|
|
86
|
+
"""Get map width from map_size string."""
|
|
87
|
+
return int(self.map_size.split('x')[0]) # pylint: disable=E1101
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def map_height(self) -> int:
|
|
91
|
+
"""Get map height from map_size string."""
|
|
92
|
+
return int(self.map_size.split('x')[1]) # pylint: disable=E1101
|
|
93
|
+
|
|
94
|
+
def get_map_size_tuple(self) -> tuple:
|
|
95
|
+
"""Get map_size as tuple."""
|
|
96
|
+
return (self.map_width, self.map_height)
|
|
97
|
+
|
|
98
|
+
def get_map_size_list(self) -> List[int]:
|
|
99
|
+
"""Get map_size as list."""
|
|
100
|
+
return [self.map_width, self.map_height]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class GooglePlaceReviewsArgs(BaseModel):
|
|
104
|
+
"""Arguments schema for Google Place Reviews tool."""
|
|
105
|
+
|
|
106
|
+
place_id: str = Field(description="Google Place identifier")
|
|
107
|
+
language: Optional[str] = Field(
|
|
108
|
+
default=None,
|
|
109
|
+
description="Optional language code for the returned reviews",
|
|
110
|
+
)
|
|
111
|
+
reviews_limit: Optional[int] = Field(
|
|
112
|
+
default=None,
|
|
113
|
+
ge=1,
|
|
114
|
+
le=10,
|
|
115
|
+
description="Optional limit for the number of reviews to return",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class GoogleTrafficArgs(BaseModel):
|
|
120
|
+
"""Arguments schema for Google Place traffic tool."""
|
|
121
|
+
|
|
122
|
+
place_id: str = Field(description="Google Place identifier")
|
|
123
|
+
language: Optional[str] = Field(
|
|
124
|
+
default=None,
|
|
125
|
+
description="Optional language code for the returned place details",
|
|
126
|
+
)
|
|
127
|
+
include_popular_times: bool = Field(
|
|
128
|
+
default=True,
|
|
129
|
+
description="Fetch Google popular times data to estimate traffic",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Google Search Tool
|
|
133
|
+
class GooglePlacesBaseTool(AbstractTool):
|
|
134
|
+
"""Shared helpers for Google Places based tools."""
|
|
135
|
+
|
|
136
|
+
base_url: str = "https://maps.googleapis.com/maps/api/place/details/json"
|
|
137
|
+
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
api_key: Optional[str] = None,
|
|
141
|
+
request_timeout: int = 30,
|
|
142
|
+
**kwargs,
|
|
143
|
+
):
|
|
144
|
+
super().__init__(**kwargs)
|
|
145
|
+
self.api_key = api_key or GOOGLE_API_KEY
|
|
146
|
+
if not self.api_key:
|
|
147
|
+
raise ValueError("Google API key is required for Google Places tools")
|
|
148
|
+
self.request_timeout = request_timeout
|
|
149
|
+
|
|
150
|
+
async def _fetch_place_details(
|
|
151
|
+
self,
|
|
152
|
+
place_id: str,
|
|
153
|
+
fields: str,
|
|
154
|
+
language: Optional[str] = None,
|
|
155
|
+
) -> Dict[str, Any]:
|
|
156
|
+
params = {
|
|
157
|
+
"placeid": place_id,
|
|
158
|
+
"key": self.api_key,
|
|
159
|
+
"fields": fields,
|
|
160
|
+
}
|
|
161
|
+
if language:
|
|
162
|
+
params["language"] = language
|
|
163
|
+
|
|
164
|
+
timeout = aiohttp.ClientTimeout(total=self.request_timeout)
|
|
165
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
166
|
+
async with session.get(self.base_url, params=params) as response:
|
|
167
|
+
payload = await response.json(content_type=None)
|
|
168
|
+
payload.setdefault("http_status", response.status)
|
|
169
|
+
return payload
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class GoogleSearchTool(AbstractTool):
|
|
173
|
+
"""Enhanced Google Search tool with content preview capabilities."""
|
|
174
|
+
|
|
175
|
+
name = "GoogleSearchTool"
|
|
176
|
+
description = "Search the web using Google Custom Search API with optional content preview"
|
|
177
|
+
args_schema = GoogleSearchArgs
|
|
178
|
+
|
|
179
|
+
def __init__(self, **kwargs):
|
|
180
|
+
super().__init__(**kwargs)
|
|
181
|
+
self.cse_id = config.get('GOOGLE_SEARCH_ENGINE_ID')
|
|
182
|
+
self.search_key = config.get('GOOGLE_SEARCH_API_KEY')
|
|
183
|
+
|
|
184
|
+
async def _fetch_page_content(self, url: str, method: str = "aiohttp") -> str:
|
|
185
|
+
"""Fetch full page content using specified method."""
|
|
186
|
+
if method == "aiohttp":
|
|
187
|
+
return await self._fetch_with_aiohttp(url)
|
|
188
|
+
elif method == "selenium":
|
|
189
|
+
return await self._fetch_with_selenium(url)
|
|
190
|
+
else:
|
|
191
|
+
raise ValueError(f"Unknown preview method: {method}")
|
|
192
|
+
|
|
193
|
+
async def _fetch_with_aiohttp(self, url: str) -> str:
|
|
194
|
+
"""Fetch page content using aiohttp."""
|
|
195
|
+
try:
|
|
196
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
197
|
+
headers = {
|
|
198
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
199
|
+
}
|
|
200
|
+
async with aiohttp.ClientSession(timeout=timeout, headers=headers) as session:
|
|
201
|
+
async with session.get(url) as response:
|
|
202
|
+
if response.status == 200:
|
|
203
|
+
# Check if content is a PDF
|
|
204
|
+
content_type = response.headers.get('Content-Type', '').lower()
|
|
205
|
+
is_pdf = url.lower().endswith('.pdf') or 'application/pdf' in content_type
|
|
206
|
+
|
|
207
|
+
if is_pdf:
|
|
208
|
+
# Use markitdown for PDF content extraction
|
|
209
|
+
try:
|
|
210
|
+
# Download PDF content to a temporary file
|
|
211
|
+
pdf_content = await response.read()
|
|
212
|
+
with tempfile.NamedTemporaryFile(mode='wb', suffix='.pdf', delete=False) as tmp_file:
|
|
213
|
+
tmp_file.write(pdf_content)
|
|
214
|
+
tmp_file_path = tmp_file.name
|
|
215
|
+
|
|
216
|
+
# Extract content using markitdown
|
|
217
|
+
markitdown = MarkItDown()
|
|
218
|
+
result = markitdown.convert(tmp_file_path)
|
|
219
|
+
|
|
220
|
+
# Clean up temporary file
|
|
221
|
+
Path(tmp_file_path).unlink(missing_ok=True)
|
|
222
|
+
|
|
223
|
+
# Return extracted text content (limited size)
|
|
224
|
+
return result.text_content[:5000] if result.text_content else "PDF content could not be extracted"
|
|
225
|
+
except Exception as pdf_error:
|
|
226
|
+
return f"Error extracting PDF content: {str(pdf_error)}"
|
|
227
|
+
else:
|
|
228
|
+
# Regular text/HTML content
|
|
229
|
+
content = await response.text()
|
|
230
|
+
# Basic HTML content extraction (you might want to use BeautifulSoup here)
|
|
231
|
+
return content[:5000] # Limit content size
|
|
232
|
+
else:
|
|
233
|
+
return f"Error: HTTP {response.status}"
|
|
234
|
+
except Exception as e:
|
|
235
|
+
return f"Error fetching content: {str(e)}"
|
|
236
|
+
|
|
237
|
+
async def _fetch_with_selenium(self, url: str) -> str:
|
|
238
|
+
"""Fetch page content using Selenium (placeholder implementation)."""
|
|
239
|
+
# Note: This would require selenium and a webdriver
|
|
240
|
+
# Implementation would depend on your selenium setup
|
|
241
|
+
return "Selenium implementation not yet available"
|
|
242
|
+
|
|
243
|
+
async def _execute(self, **kwargs) -> Dict[str, Any]:
|
|
244
|
+
"""Execute Google search with optional content preview."""
|
|
245
|
+
query = kwargs['query']
|
|
246
|
+
max_results = kwargs['max_results']
|
|
247
|
+
preview = kwargs['preview']
|
|
248
|
+
preview_method = kwargs['preview_method']
|
|
249
|
+
|
|
250
|
+
# Build search service
|
|
251
|
+
service = build("customsearch", "v1", developerKey=self.search_key)
|
|
252
|
+
|
|
253
|
+
# Execute search
|
|
254
|
+
res = service.cse().list( # pylint: disable=E1101 # noqa
|
|
255
|
+
q=query,
|
|
256
|
+
cx=self.cse_id,
|
|
257
|
+
num=max_results
|
|
258
|
+
).execute()
|
|
259
|
+
|
|
260
|
+
results = []
|
|
261
|
+
for item in res.get('items', []):
|
|
262
|
+
result_item = {
|
|
263
|
+
'title': item['title'],
|
|
264
|
+
'link': item['link'],
|
|
265
|
+
'snippet': item['snippet'],
|
|
266
|
+
'description': item['snippet']
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# Add full content if preview is requested
|
|
270
|
+
if preview:
|
|
271
|
+
self.logger.info(f"Fetching preview for: {item['link']}")
|
|
272
|
+
content = await self._fetch_page_content(item['link'], preview_method)
|
|
273
|
+
result_item['full_content'] = content
|
|
274
|
+
|
|
275
|
+
results.append(result_item)
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
'query': query,
|
|
279
|
+
'total_results': len(results),
|
|
280
|
+
'results': results
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# Google Site Search Tool
|
|
285
|
+
class GoogleSiteSearchTool(GoogleSearchTool):
|
|
286
|
+
"""Google Site Search tool - extends GoogleSearchTool with site restriction."""
|
|
287
|
+
|
|
288
|
+
name = "google_site_search"
|
|
289
|
+
description = "Search within a specific site using Google Custom Search API"
|
|
290
|
+
args_schema = GoogleSiteSearchArgs
|
|
291
|
+
|
|
292
|
+
async def _execute(self, **kwargs) -> Dict[str, Any]:
|
|
293
|
+
"""Execute site-specific Google search."""
|
|
294
|
+
query = kwargs['query']
|
|
295
|
+
site = kwargs['site']
|
|
296
|
+
# Modify query to include site restriction
|
|
297
|
+
site_query = f"{query} site:{site}"
|
|
298
|
+
|
|
299
|
+
# Use parent class execution with modified query
|
|
300
|
+
modified_kwargs = kwargs.copy()
|
|
301
|
+
modified_kwargs['query'] = site_query
|
|
302
|
+
|
|
303
|
+
result = await super()._execute(**modified_kwargs)
|
|
304
|
+
result['original_query'] = query
|
|
305
|
+
result['site'] = site
|
|
306
|
+
result['search_query'] = site_query
|
|
307
|
+
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# Google Location Finder Tool
|
|
312
|
+
class GoogleLocationTool(AbstractTool):
|
|
313
|
+
"""Google Geocoding tool for location information."""
|
|
314
|
+
|
|
315
|
+
name = "google_location_finder"
|
|
316
|
+
description = "Find location information using Google Geocoding API"
|
|
317
|
+
args_schema = GoogleLocationArgs
|
|
318
|
+
|
|
319
|
+
def __init__(self, **kwargs):
|
|
320
|
+
super().__init__(**kwargs)
|
|
321
|
+
self.google_key = kwargs.get('api_key', GOOGLE_API_KEY)
|
|
322
|
+
self.base_url = "https://maps.googleapis.com/maps/api/geocode/json"
|
|
323
|
+
|
|
324
|
+
def _extract_location_components(self, data: Dict) -> Dict[str, Optional[str]]:
|
|
325
|
+
"""Extract location components from geocoding response."""
|
|
326
|
+
city = state = state_code = zipcode = country = country_code = None
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
for component in data.get('address_components', []):
|
|
330
|
+
types = component.get('types', [])
|
|
331
|
+
|
|
332
|
+
if 'locality' in types:
|
|
333
|
+
city = component['long_name']
|
|
334
|
+
elif 'administrative_area_level_1' in types:
|
|
335
|
+
state_code = component['short_name']
|
|
336
|
+
state = component['long_name']
|
|
337
|
+
elif 'postal_code' in types:
|
|
338
|
+
zipcode = component['long_name']
|
|
339
|
+
elif 'country' in types:
|
|
340
|
+
country = component['long_name']
|
|
341
|
+
country_code = component['short_name']
|
|
342
|
+
except Exception as e:
|
|
343
|
+
self.logger.error(f"Error extracting location components: {e}")
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
'city': city,
|
|
347
|
+
'state': state,
|
|
348
|
+
'state_code': state_code,
|
|
349
|
+
'zipcode': zipcode,
|
|
350
|
+
'country': country,
|
|
351
|
+
'country_code': country_code
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async def _execute(self, **kwargs) -> Dict[str, Any]:
|
|
355
|
+
"""Execute geocoding request."""
|
|
356
|
+
address = kwargs['address']
|
|
357
|
+
|
|
358
|
+
params = {
|
|
359
|
+
"address": address,
|
|
360
|
+
"key": self.google_key
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
364
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
365
|
+
async with session.get(self.base_url, params=params) as response:
|
|
366
|
+
if response.status != 200:
|
|
367
|
+
raise Exception(f"HTTP {response.status}: {await response.text()}")
|
|
368
|
+
|
|
369
|
+
result = await response.json()
|
|
370
|
+
|
|
371
|
+
if result['status'] != 'OK':
|
|
372
|
+
return {
|
|
373
|
+
'status': result['status'],
|
|
374
|
+
'error': result.get('error_message', 'Unknown error'),
|
|
375
|
+
'results': []
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
# Process results into tabular format
|
|
379
|
+
processed_results = []
|
|
380
|
+
for location in result['results']:
|
|
381
|
+
components = self._extract_location_components(location)
|
|
382
|
+
geometry = location.get('geometry', {})
|
|
383
|
+
location_data = geometry.get('location', {})
|
|
384
|
+
|
|
385
|
+
processed_result = {
|
|
386
|
+
'formatted_address': location.get('formatted_address'),
|
|
387
|
+
'latitude': location_data.get('lat'),
|
|
388
|
+
'longitude': location_data.get('lng'),
|
|
389
|
+
'place_id': location.get('place_id'),
|
|
390
|
+
'location_type': geometry.get('location_type'),
|
|
391
|
+
'city': components['city'],
|
|
392
|
+
'state': components['state'],
|
|
393
|
+
'state_code': components['state_code'],
|
|
394
|
+
'zipcode': components['zipcode'],
|
|
395
|
+
'country': components['country'],
|
|
396
|
+
'country_code': components['country_code'],
|
|
397
|
+
'types': location.get('types', [])
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
# Add viewport if available
|
|
401
|
+
if 'viewport' in geometry:
|
|
402
|
+
viewport = geometry['viewport']
|
|
403
|
+
processed_result.update({
|
|
404
|
+
'viewport_northeast_lat': viewport.get('northeast', {}).get('lat'),
|
|
405
|
+
'viewport_northeast_lng': viewport.get('northeast', {}).get('lng'),
|
|
406
|
+
'viewport_southwest_lat': viewport.get('southwest', {}).get('lat'),
|
|
407
|
+
'viewport_southwest_lng': viewport.get('southwest', {}).get('lng')
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
processed_results.append(processed_result)
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
'status': result['status'],
|
|
414
|
+
'query': address,
|
|
415
|
+
'results_count': len(processed_results),
|
|
416
|
+
'results': processed_results,
|
|
417
|
+
'raw_response': result # Include original response for reference
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class GoogleReviewsTool(GooglePlacesBaseTool):
|
|
422
|
+
"""Retrieve reviews, rating, and metadata for a Google Place."""
|
|
423
|
+
|
|
424
|
+
name = "google_place_reviews"
|
|
425
|
+
description = "Extract reviews and rating details for a Google Place via the Places Details API"
|
|
426
|
+
args_schema = GooglePlaceReviewsArgs
|
|
427
|
+
|
|
428
|
+
async def _execute(self, **kwargs) -> Dict[str, Any]:
|
|
429
|
+
place_id = kwargs['place_id']
|
|
430
|
+
language = kwargs.get('language')
|
|
431
|
+
reviews_limit = kwargs.get('reviews_limit')
|
|
432
|
+
|
|
433
|
+
fields = "rating,reviews,user_ratings_total,name"
|
|
434
|
+
response = await self._fetch_place_details(
|
|
435
|
+
place_id=place_id,
|
|
436
|
+
fields=fields,
|
|
437
|
+
language=language,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
status = response.get('status', 'UNKNOWN')
|
|
441
|
+
if status != 'OK':
|
|
442
|
+
return {
|
|
443
|
+
'status': status,
|
|
444
|
+
'place_id': place_id,
|
|
445
|
+
'error_message': response.get('error_message'),
|
|
446
|
+
'http_status': response.get('http_status'),
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
result = response.get('result', {})
|
|
450
|
+
reviews: List[Dict[str, Any]] = result.get('reviews', []) or []
|
|
451
|
+
|
|
452
|
+
if reviews_limit is not None:
|
|
453
|
+
reviews = reviews[:reviews_limit]
|
|
454
|
+
|
|
455
|
+
simplified_reviews = [
|
|
456
|
+
{
|
|
457
|
+
'author_name': review.get('author_name'),
|
|
458
|
+
'author_url': review.get('author_url'),
|
|
459
|
+
'language': review.get('language'),
|
|
460
|
+
'profile_photo_url': review.get('profile_photo_url'),
|
|
461
|
+
'rating': review.get('rating'),
|
|
462
|
+
'relative_time_description': review.get('relative_time_description'),
|
|
463
|
+
'text': review.get('text'),
|
|
464
|
+
'time': review.get('time'),
|
|
465
|
+
}
|
|
466
|
+
for review in reviews
|
|
467
|
+
]
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
'status': status,
|
|
471
|
+
'place_id': place_id,
|
|
472
|
+
'name': result.get('name'),
|
|
473
|
+
'rating': result.get('rating'),
|
|
474
|
+
'user_ratings_total': result.get('user_ratings_total'),
|
|
475
|
+
'reviews_returned': len(simplified_reviews),
|
|
476
|
+
'reviews': simplified_reviews,
|
|
477
|
+
'raw_response': response,
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
class GoogleTrafficTool(GooglePlacesBaseTool):
|
|
482
|
+
"""Retrieve Google popular times data to estimate venue traffic."""
|
|
483
|
+
|
|
484
|
+
name = "google_place_traffic"
|
|
485
|
+
description = "Extract current popularity and popular times for a Google Place"
|
|
486
|
+
args_schema = GoogleTrafficArgs
|
|
487
|
+
|
|
488
|
+
day_mapping = {
|
|
489
|
+
"1": "Monday",
|
|
490
|
+
"2": "Tuesday",
|
|
491
|
+
"3": "Wednesday",
|
|
492
|
+
"4": "Thursday",
|
|
493
|
+
"5": "Friday",
|
|
494
|
+
"6": "Saturday",
|
|
495
|
+
"7": "Sunday",
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async def _execute(self, **kwargs) -> Dict[str, Any]:
|
|
499
|
+
place_id = kwargs['place_id']
|
|
500
|
+
language = kwargs.get('language')
|
|
501
|
+
include_popular_times = kwargs['include_popular_times']
|
|
502
|
+
|
|
503
|
+
fields = (
|
|
504
|
+
"name,place_id,address_components,formatted_address,geometry,types,"
|
|
505
|
+
"vicinity,rating,user_ratings_total"
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
response = await self._fetch_place_details(
|
|
509
|
+
place_id=place_id,
|
|
510
|
+
fields=fields,
|
|
511
|
+
language=language,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
status = response.get('status', 'UNKNOWN')
|
|
515
|
+
if status != 'OK':
|
|
516
|
+
return {
|
|
517
|
+
'status': status,
|
|
518
|
+
'place_id': place_id,
|
|
519
|
+
'error_message': response.get('error_message'),
|
|
520
|
+
'http_status': response.get('http_status'),
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
place_info = response.get('result', {})
|
|
524
|
+
|
|
525
|
+
structured_popular_times: Optional[Dict[str, Dict[str, Dict[str, Any]]]] = None
|
|
526
|
+
traffic_schedule: Optional[Dict[str, Dict[str, Dict[str, Any]]]] = None
|
|
527
|
+
|
|
528
|
+
if include_popular_times:
|
|
529
|
+
address_parts = [
|
|
530
|
+
place_info.get('name'),
|
|
531
|
+
place_info.get('formatted_address') or place_info.get('vicinity'),
|
|
532
|
+
]
|
|
533
|
+
address = ', '.join([part for part in address_parts if part])
|
|
534
|
+
try:
|
|
535
|
+
search_data = await self._make_google_search(address) if address else None
|
|
536
|
+
except ValueError as exc:
|
|
537
|
+
self.logger.warning(f"Google popular times search failed: {exc}")
|
|
538
|
+
search_data = None
|
|
539
|
+
|
|
540
|
+
if search_data:
|
|
541
|
+
self._get_populartimes(place_info, search_data)
|
|
542
|
+
popular_times_raw = place_info.get('popular_times')
|
|
543
|
+
|
|
544
|
+
structured_popular_times = self._normalize_popular_times(popular_times_raw)
|
|
545
|
+
if structured_popular_times:
|
|
546
|
+
try:
|
|
547
|
+
traffic_schedule = self.convert_populartimes(structured_popular_times)
|
|
548
|
+
except Exception as exc:
|
|
549
|
+
self.logger.error(f"Error formatting traffic data: {exc}")
|
|
550
|
+
traffic_schedule = None
|
|
551
|
+
|
|
552
|
+
result_payload = {
|
|
553
|
+
'status': status,
|
|
554
|
+
'place_id': place_id,
|
|
555
|
+
'name': place_info.get('name'),
|
|
556
|
+
'formatted_address': place_info.get('formatted_address') or place_info.get('vicinity'),
|
|
557
|
+
'address_components': place_info.get('address_components'),
|
|
558
|
+
'geometry': place_info.get('geometry'),
|
|
559
|
+
'types': place_info.get('types'),
|
|
560
|
+
'rating': place_info.get('rating'),
|
|
561
|
+
'rating_n': place_info.get('rating_n'),
|
|
562
|
+
'user_ratings_total': place_info.get('user_ratings_total'),
|
|
563
|
+
'current_popularity': place_info.get('current_popularity'),
|
|
564
|
+
'popular_times': structured_popular_times,
|
|
565
|
+
'traffic': traffic_schedule,
|
|
566
|
+
'time_spent': place_info.get('time_spent'),
|
|
567
|
+
'raw_response': response,
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return result_payload
|
|
571
|
+
|
|
572
|
+
def _normalize_popular_times(
|
|
573
|
+
self,
|
|
574
|
+
popular_times: Optional[Any],
|
|
575
|
+
) -> Optional[Dict[str, Dict[str, Dict[str, Any]]]]:
|
|
576
|
+
if not popular_times:
|
|
577
|
+
return None
|
|
578
|
+
|
|
579
|
+
normalized: Dict[str, Dict[str, Dict[str, Any]]] = {}
|
|
580
|
+
|
|
581
|
+
if isinstance(popular_times, dict):
|
|
582
|
+
iterator = popular_times.items()
|
|
583
|
+
else:
|
|
584
|
+
iterator = []
|
|
585
|
+
for entry in popular_times:
|
|
586
|
+
if isinstance(entry, (list, tuple)) and len(entry) >= 2:
|
|
587
|
+
day_key = str(entry[0])
|
|
588
|
+
iterator.append((day_key, entry[1]))
|
|
589
|
+
|
|
590
|
+
for day_key, entries in iterator:
|
|
591
|
+
day_data: Dict[str, Dict[str, Any]] = {}
|
|
592
|
+
if isinstance(entries, dict):
|
|
593
|
+
for hour_key, value in entries.items():
|
|
594
|
+
if isinstance(value, dict):
|
|
595
|
+
hour_str = str(hour_key)
|
|
596
|
+
day_data[hour_str] = {
|
|
597
|
+
'hour': value.get('hour', int(hour_str) if hour_str.isdigit() else value.get('hour')),
|
|
598
|
+
'human_hour': value.get('human_hour'),
|
|
599
|
+
'traffic': value.get('traffic'),
|
|
600
|
+
'traffic_status': value.get('traffic_status'),
|
|
601
|
+
}
|
|
602
|
+
elif isinstance(value, (list, tuple)) and len(value) >= 5:
|
|
603
|
+
hour_str = str(value[0])
|
|
604
|
+
day_data[hour_str] = {
|
|
605
|
+
'human_hour': value[4],
|
|
606
|
+
'traffic': value[1],
|
|
607
|
+
'traffic_status': value[2],
|
|
608
|
+
}
|
|
609
|
+
elif isinstance(entries, list):
|
|
610
|
+
for value in entries:
|
|
611
|
+
if isinstance(value, (list, tuple)) and len(value) >= 5:
|
|
612
|
+
hour_str = str(value[0])
|
|
613
|
+
day_data[hour_str] = {
|
|
614
|
+
'human_hour': value[4],
|
|
615
|
+
'traffic': value[1],
|
|
616
|
+
'traffic_status': value[2],
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if day_data:
|
|
620
|
+
normalized[str(day_key)] = day_data
|
|
621
|
+
|
|
622
|
+
return normalized or None
|
|
623
|
+
|
|
624
|
+
def convert_populartimes(
|
|
625
|
+
self,
|
|
626
|
+
popular_times: Dict[str, Dict[str, Dict[str, Any]]]
|
|
627
|
+
) -> Dict[str, Dict[str, Dict[str, Any]]]:
|
|
628
|
+
converted_data: Dict[str, Dict[str, Dict[str, Any]]] = {}
|
|
629
|
+
|
|
630
|
+
for day_number, hours in popular_times.items():
|
|
631
|
+
day_name = self.day_mapping.get(str(day_number), "Unknown")
|
|
632
|
+
converted_data[day_name] = {}
|
|
633
|
+
|
|
634
|
+
for hour, data in hours.items():
|
|
635
|
+
time_str = f"{int(hour):02}:00:00" if str(hour).isdigit() else str(hour)
|
|
636
|
+
converted_data[day_name][time_str] = {
|
|
637
|
+
'hour': data.get('hour', int(hour) if str(hour).isdigit() else hour),
|
|
638
|
+
'human_hour': data.get('human_hour'),
|
|
639
|
+
'traffic': data.get('traffic'),
|
|
640
|
+
'traffic_status': data.get('traffic_status'),
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return converted_data
|
|
644
|
+
|
|
645
|
+
@staticmethod
|
|
646
|
+
def index_get(array: Any, *argv: int) -> Optional[Any]:
|
|
647
|
+
try:
|
|
648
|
+
for index in argv:
|
|
649
|
+
array = array[index]
|
|
650
|
+
return array
|
|
651
|
+
except (IndexError, TypeError):
|
|
652
|
+
return None
|
|
653
|
+
|
|
654
|
+
def _get_populartimes(self, place_info: Dict[str, Any], data: Any) -> None:
|
|
655
|
+
info = self.index_get(data, 0, 1, 0, 14)
|
|
656
|
+
rating = self.index_get(info, 4, 7)
|
|
657
|
+
rating_n = self.index_get(info, 4, 8)
|
|
658
|
+
popular_times = self.index_get(info, 84, 0)
|
|
659
|
+
current_popularity = self.index_get(info, 84, 7, 1)
|
|
660
|
+
time_spent = self.index_get(info, 117, 0)
|
|
661
|
+
|
|
662
|
+
if time_spent:
|
|
663
|
+
time_spent_str = str(time_spent).lower()
|
|
664
|
+
nums = [
|
|
665
|
+
float(value)
|
|
666
|
+
for value in re.findall(r'\d*\.\d+|\d+', time_spent_str.replace(',', '.'))
|
|
667
|
+
]
|
|
668
|
+
contains_min = 'min' in time_spent_str
|
|
669
|
+
contains_hour = 'hour' in time_spent_str or 'hr' in time_spent_str
|
|
670
|
+
parsed_time: Optional[List[int]] = None
|
|
671
|
+
|
|
672
|
+
if contains_min and contains_hour and len(nums) >= 2:
|
|
673
|
+
parsed_time = [int(nums[0]), int(nums[1] * 60)]
|
|
674
|
+
elif contains_hour and nums:
|
|
675
|
+
upper = nums[0] if len(nums) == 1 else nums[1]
|
|
676
|
+
parsed_time = [int(nums[0] * 60), int(upper * 60)]
|
|
677
|
+
elif contains_min and nums:
|
|
678
|
+
upper = nums[0] if len(nums) == 1 else nums[1]
|
|
679
|
+
parsed_time = [int(nums[0]), int(upper)]
|
|
680
|
+
|
|
681
|
+
time_spent = parsed_time if parsed_time is not None else time_spent
|
|
682
|
+
|
|
683
|
+
place_info.update(
|
|
684
|
+
**{
|
|
685
|
+
'rating': rating if rating is not None else place_info.get('rating'),
|
|
686
|
+
'rating_n': rating_n,
|
|
687
|
+
'current_popularity': current_popularity,
|
|
688
|
+
'popular_times': popular_times,
|
|
689
|
+
'time_spent': time_spent,
|
|
690
|
+
}
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
async def _make_google_search(self, query_string: str) -> Optional[Any]:
|
|
694
|
+
params_url = {
|
|
695
|
+
"tbm": "map",
|
|
696
|
+
"tch": 1,
|
|
697
|
+
"hl": "en",
|
|
698
|
+
"q": urllib.parse.quote_plus(query_string),
|
|
699
|
+
"pb": "!4m12!1m3!1d4005.9771522653964!2d-122.42072974863942!3d37.8077459796541!2m3!1f0!2f0!3f0!3m2!1i1125!2i976"
|
|
700
|
+
"!4f13.1!7i20!10b1!12m6!2m3!5m1!6e2!20e3!10b1!16b1!19m3!2m2!1i392!2i106!20m61!2m2!1i203!2i100!3m2!2i4!5b1"
|
|
701
|
+
"!6m6!1m2!1i86!2i86!1m2!1i408!2i200!7m46!1m3!1e1!2b0!3e3!1m3!1e2!2b1!3e2!1m3!1e2!2b0!3e3!1m3!1e3!2b0!3e3!"
|
|
702
|
+
"1m3!1e4!2b0!3e3!1m3!1e8!2b0!3e3!1m3!1e3!2b1!3e2!1m3!1e9!2b1!3e2!1m3!1e10!2b0!3e3!1m3!1e10!2b1!3e2!1m3!1e"
|
|
703
|
+
"10!2b0!3e4!2b1!4b1!9b0!22m6!1sa9fVWea_MsX8adX8j8AE%3A1!2zMWk6Mix0OjExODg3LGU6MSxwOmE5ZlZXZWFfTXNYOGFkWDh"
|
|
704
|
+
"qOEFFOjE!7e81!12e3!17sa9fVWea_MsX8adX8j8AE%3A564!18e15!24m15!2b1!5m4!2b1!3b1!5b1!6b1!10m1!8e3!17b1!24b1!"
|
|
705
|
+
"25b1!26b1!30m1!2b1!36b1!26m3!2m2!1i80!2i92!30m28!1m6!1m2!1i0!2i0!2m2!1i458!2i976!1m6!1m2!1i1075!2i0!2m2!"
|
|
706
|
+
"1i1125!2i976!1m6!1m2!1i0!2i0!2m2!1i1125!2i20!1m6!1m2!1i0!2i956!2m2!1i1125!2i976!37m1!1e81!42b1!47m0!49m1"
|
|
707
|
+
"!3b1"
|
|
708
|
+
}
|
|
709
|
+
search_url = "https://www.google.com/search?" + "&".join(
|
|
710
|
+
f"{key}={value}" for key, value in params_url.items()
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
headers = {
|
|
714
|
+
'User-Agent': (
|
|
715
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
|
|
716
|
+
'AppleWebKit/537.36 (KHTML, like Gecko) '
|
|
717
|
+
'Chrome/120.0.0.0 Safari/537.36'
|
|
718
|
+
)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
timeout = aiohttp.ClientTimeout(total=self.request_timeout)
|
|
722
|
+
async with aiohttp.ClientSession(timeout=timeout, headers=headers) as session:
|
|
723
|
+
async with session.get(search_url) as response:
|
|
724
|
+
raw_content = await response.read()
|
|
725
|
+
if response.status != 200:
|
|
726
|
+
raise ValueError(f"HTTP {response.status}: Unable to fetch Google search results")
|
|
727
|
+
|
|
728
|
+
await asyncio.sleep(0.5)
|
|
729
|
+
|
|
730
|
+
if not raw_content:
|
|
731
|
+
raise ValueError("Empty response from Google Search")
|
|
732
|
+
|
|
733
|
+
result = raw_content.decode('utf-8', errors='ignore')
|
|
734
|
+
data = result.split('/*""*/')[0].strip()
|
|
735
|
+
if not data:
|
|
736
|
+
raise ValueError("Empty response from Google Search")
|
|
737
|
+
|
|
738
|
+
jend = data.rfind("}")
|
|
739
|
+
if jend >= 0:
|
|
740
|
+
data = data[:jend + 1]
|
|
741
|
+
|
|
742
|
+
parsed = orjson.loads(data)
|
|
743
|
+
payload = parsed.get('d')
|
|
744
|
+
if not payload:
|
|
745
|
+
return None
|
|
746
|
+
|
|
747
|
+
return orjson.loads(payload[4:])
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
class GoogleRoutesTool(AbstractTool):
|
|
751
|
+
"""Google Routes tool using the new Routes API v2."""
|
|
752
|
+
|
|
753
|
+
name = "google_routes"
|
|
754
|
+
description = "Find routes using Google Routes API v2 with waypoint optimization"
|
|
755
|
+
args_schema = GoogleRouteArgs
|
|
756
|
+
|
|
757
|
+
def __init__(self, **kwargs):
|
|
758
|
+
super().__init__(**kwargs)
|
|
759
|
+
self.google_key = kwargs.get('api_key', GOOGLE_API_KEY)
|
|
760
|
+
self.base_url = "https://routes.googleapis.com/directions/v2:computeRoutes"
|
|
761
|
+
|
|
762
|
+
def _default_output_dir(self) -> Optional[Path]:
|
|
763
|
+
"""Get the default output directory for this tool type."""
|
|
764
|
+
return self.static_dir / "route_maps" if self.static_dir else None
|
|
765
|
+
|
|
766
|
+
def _create_location_object(self, location: str) -> Dict[str, Any]:
|
|
767
|
+
"""Create location object for Routes API."""
|
|
768
|
+
try:
|
|
769
|
+
if ',' in location:
|
|
770
|
+
parts = location.strip().split(',')
|
|
771
|
+
if len(parts) == 2:
|
|
772
|
+
lat, lng = map(float, parts)
|
|
773
|
+
return {
|
|
774
|
+
"location": {
|
|
775
|
+
"latLng": {
|
|
776
|
+
"latitude": lat,
|
|
777
|
+
"longitude": lng
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
except ValueError:
|
|
782
|
+
pass
|
|
783
|
+
|
|
784
|
+
return {"address": location}
|
|
785
|
+
|
|
786
|
+
def _calculate_optimal_zoom(self, distance_miles: float, viewport: Dict = None) -> int:
|
|
787
|
+
"""Calculate optimal zoom level based on route distance."""
|
|
788
|
+
if viewport:
|
|
789
|
+
ne_lat = viewport.get('northeast', {}).get('lat', 0)
|
|
790
|
+
ne_lng = viewport.get('northeast', {}).get('lng', 0)
|
|
791
|
+
sw_lat = viewport.get('southwest', {}).get('lat', 0)
|
|
792
|
+
sw_lng = viewport.get('southwest', {}).get('lng', 0)
|
|
793
|
+
|
|
794
|
+
lat_span = abs(ne_lat - sw_lat)
|
|
795
|
+
lng_span = abs(ne_lng - sw_lng)
|
|
796
|
+
max_span = max(lat_span, lng_span)
|
|
797
|
+
|
|
798
|
+
if max_span >= 10:
|
|
799
|
+
return 6
|
|
800
|
+
elif max_span >= 5:
|
|
801
|
+
return 7
|
|
802
|
+
elif max_span >= 2:
|
|
803
|
+
return 8
|
|
804
|
+
elif max_span >= 1:
|
|
805
|
+
return 9
|
|
806
|
+
elif max_span >= 0.5:
|
|
807
|
+
return 10
|
|
808
|
+
elif max_span >= 0.25:
|
|
809
|
+
return 11
|
|
810
|
+
elif max_span >= 0.1:
|
|
811
|
+
return 12
|
|
812
|
+
elif max_span >= 0.05:
|
|
813
|
+
return 13
|
|
814
|
+
else: return 14
|
|
815
|
+
|
|
816
|
+
if distance_miles >= 500:
|
|
817
|
+
return 6
|
|
818
|
+
elif distance_miles >= 200:
|
|
819
|
+
return 7
|
|
820
|
+
elif distance_miles >= 100:
|
|
821
|
+
return 8
|
|
822
|
+
elif distance_miles >= 50:
|
|
823
|
+
return 9
|
|
824
|
+
elif distance_miles >= 25:
|
|
825
|
+
return 10
|
|
826
|
+
elif distance_miles >= 10:
|
|
827
|
+
return 11
|
|
828
|
+
elif distance_miles >= 5:
|
|
829
|
+
return 12
|
|
830
|
+
elif distance_miles >= 2:
|
|
831
|
+
return 13
|
|
832
|
+
else: return 14
|
|
833
|
+
|
|
834
|
+
def _get_gradient_colors(self, num_colors: int, start_color: str = "0x0000FF", end_color: str = "0xFF0000") -> List[str]:
|
|
835
|
+
"""Generate gradient colors for waypoint markers."""
|
|
836
|
+
if num_colors <= 1:
|
|
837
|
+
return [start_color]
|
|
838
|
+
|
|
839
|
+
start_rgb = tuple(int(start_color[2:][i:i+2], 16) for i in (0, 2, 4))
|
|
840
|
+
end_rgb = tuple(int(end_color[2:][i:i+2], 16) for i in (0, 2, 4))
|
|
841
|
+
|
|
842
|
+
colors = []
|
|
843
|
+
for i in range(num_colors):
|
|
844
|
+
ratio = i / (num_colors - 1)
|
|
845
|
+
r = int(start_rgb[0] + ratio * (end_rgb[0] - start_rgb[0]))
|
|
846
|
+
g = int(start_rgb[1] + ratio * (end_rgb[1] - start_rgb[1]))
|
|
847
|
+
b = int(start_rgb[2] + ratio * (end_rgb[2] - start_rgb[2]))
|
|
848
|
+
colors.append(f"0x{r:02x}{g:02x}{b:02x}")
|
|
849
|
+
|
|
850
|
+
return colors
|
|
851
|
+
|
|
852
|
+
async def _extract_coordinates_from_location(self, location: str) -> Tuple[float, float]:
|
|
853
|
+
"""Extract coordinates from location string or geocode address."""
|
|
854
|
+
try:
|
|
855
|
+
if ',' in location:
|
|
856
|
+
parts = location.strip().split(',')
|
|
857
|
+
if len(parts) == 2:
|
|
858
|
+
lat, lng = map(float, parts)
|
|
859
|
+
return (lat, lng)
|
|
860
|
+
except ValueError:
|
|
861
|
+
pass
|
|
862
|
+
|
|
863
|
+
try:
|
|
864
|
+
geocoder = GoogleLocationTool(api_key=self.google_key)
|
|
865
|
+
result = await geocoder.execute(address=location)
|
|
866
|
+
if result.status == "success" and result.result['results']:
|
|
867
|
+
first_result = result.result['results'][0]
|
|
868
|
+
lat = first_result['latitude']
|
|
869
|
+
lng = first_result['longitude']
|
|
870
|
+
if lat is not None and lng is not None:
|
|
871
|
+
return (lat, lng)
|
|
872
|
+
except Exception as e:
|
|
873
|
+
self.logger.warning(f"Failed to geocode {location}: {e}")
|
|
874
|
+
|
|
875
|
+
return (0.0, 0.0)
|
|
876
|
+
|
|
877
|
+
async def _generate_static_map_url(self, route_data: Dict, coordinates_cache: Dict, args: Dict) -> str:
|
|
878
|
+
"""Generate Google Static Maps URL for the route."""
|
|
879
|
+
base_url = "https://maps.googleapis.com/maps/api/staticmap"
|
|
880
|
+
|
|
881
|
+
route = route_data['routes'][0]
|
|
882
|
+
encoded_polyline = route.get('polyline', {}).get('encodedPolyline', '')
|
|
883
|
+
|
|
884
|
+
origin_coords = coordinates_cache['origin']
|
|
885
|
+
dest_coords = coordinates_cache['destination']
|
|
886
|
+
waypoint_coords_list = coordinates_cache['waypoints']
|
|
887
|
+
|
|
888
|
+
markers = []
|
|
889
|
+
markers.append(f"markers=color:green|label:O|{origin_coords[0]},{origin_coords[1]}")
|
|
890
|
+
markers.append(f"markers=color:red|label:D|{dest_coords[0]},{dest_coords[1]}")
|
|
891
|
+
|
|
892
|
+
if waypoint_coords_list:
|
|
893
|
+
colors = self._get_gradient_colors(len(waypoint_coords_list))
|
|
894
|
+
alpha_labels = string.ascii_uppercase
|
|
895
|
+
|
|
896
|
+
for i, coords in enumerate(waypoint_coords_list):
|
|
897
|
+
if i < len(colors) and i < len(alpha_labels):
|
|
898
|
+
color = colors[i].replace('0x', '')
|
|
899
|
+
label = alpha_labels[i]
|
|
900
|
+
markers.append(f"markers=color:0x{color}|size:mid|label:{label}|{coords[0]},{coords[1]}")
|
|
901
|
+
|
|
902
|
+
map_size = args['map_size']
|
|
903
|
+
|
|
904
|
+
if args.get('auto_zoom', True):
|
|
905
|
+
viewport = route.get('viewport')
|
|
906
|
+
distance_miles = args.get('total_distance_miles', 0)
|
|
907
|
+
zoom_level = self._calculate_optimal_zoom(distance_miles, viewport)
|
|
908
|
+
self.logger.info(f"Auto-calculated zoom level: {zoom_level} for distance: {distance_miles} miles")
|
|
909
|
+
else:
|
|
910
|
+
zoom_level = args['zoom']
|
|
911
|
+
|
|
912
|
+
params = {
|
|
913
|
+
"size": f"{map_size}",
|
|
914
|
+
"scale": args['map_scale'],
|
|
915
|
+
"maptype": args['map_type'],
|
|
916
|
+
"zoom": zoom_level,
|
|
917
|
+
"language": "en",
|
|
918
|
+
"key": self.google_key
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if encoded_polyline:
|
|
922
|
+
params["path"] = f"enc:{encoded_polyline}"
|
|
923
|
+
|
|
924
|
+
query_string = urllib.parse.urlencode(params)
|
|
925
|
+
markers_string = '&'.join(markers)
|
|
926
|
+
|
|
927
|
+
return f"{base_url}?{query_string}&{markers_string}"
|
|
928
|
+
|
|
929
|
+
def _generate_interactive_html_map(self, route_data: Dict, coordinates_cache: Dict, args: Dict) -> str:
|
|
930
|
+
"""Generate an interactive HTML map using Google Maps JavaScript API."""
|
|
931
|
+
route = route_data['routes'][0]
|
|
932
|
+
encoded_polyline = route.get('polyline', {}).get('encodedPolyline', '')
|
|
933
|
+
|
|
934
|
+
self.logger.info(f"Generating interactive map with polyline length: {len(encoded_polyline)} chars")
|
|
935
|
+
self.logger.info(f"Polyline sample: {encoded_polyline[:100]}...")
|
|
936
|
+
|
|
937
|
+
origin_coords = coordinates_cache['origin']
|
|
938
|
+
dest_coords = coordinates_cache['destination']
|
|
939
|
+
waypoint_coords = coordinates_cache['waypoints']
|
|
940
|
+
|
|
941
|
+
self.logger.info(f"Origin coords: {origin_coords}, Dest coords: {dest_coords}, Waypoints: {waypoint_coords}")
|
|
942
|
+
|
|
943
|
+
valid_coords = [coord for coord in [origin_coords, dest_coords] + waypoint_coords if coord != (0.0, 0.0)]
|
|
944
|
+
|
|
945
|
+
if valid_coords:
|
|
946
|
+
all_lats = [coord[0] for coord in valid_coords]
|
|
947
|
+
all_lngs = [coord[1] for coord in valid_coords]
|
|
948
|
+
center_lat = sum(all_lats) / len(all_lats)
|
|
949
|
+
center_lng = sum(all_lngs) / len(all_lngs)
|
|
950
|
+
else:
|
|
951
|
+
center_lat, center_lng = 37.7749, -122.4194
|
|
952
|
+
|
|
953
|
+
viewport = route.get('viewport', {})
|
|
954
|
+
distance_miles = args.get('total_distance_miles', 0)
|
|
955
|
+
zoom_level = self._calculate_optimal_zoom(distance_miles, viewport)
|
|
956
|
+
|
|
957
|
+
waypoint_markers_js = ""
|
|
958
|
+
if waypoint_coords:
|
|
959
|
+
alpha_labels = string.ascii_uppercase
|
|
960
|
+
for i, (lat, lng) in enumerate(waypoint_coords):
|
|
961
|
+
if i < len(alpha_labels):
|
|
962
|
+
label = alpha_labels[i]
|
|
963
|
+
waypoint_markers_js += f"""
|
|
964
|
+
new google.maps.Marker({{
|
|
965
|
+
position: {{lat: {lat}, lng: {lng}}},
|
|
966
|
+
map: map,
|
|
967
|
+
title: 'Waypoint {label}',
|
|
968
|
+
label: '{label}',
|
|
969
|
+
icon: {{
|
|
970
|
+
url: 'https://maps.google.com/mapfiles/ms/icons/blue-dot.png'
|
|
971
|
+
}}
|
|
972
|
+
}});
|
|
973
|
+
"""
|
|
974
|
+
|
|
975
|
+
polyline_js = ""
|
|
976
|
+
if encoded_polyline and len(encoded_polyline) > 10:
|
|
977
|
+
|
|
978
|
+
# FIX: Escape backslashes for the JavaScript string literal.
|
|
979
|
+
js_safe_polyline = encoded_polyline.replace('\\', '\\\\')
|
|
980
|
+
|
|
981
|
+
polyline_js = f"""
|
|
982
|
+
try {{
|
|
983
|
+
console.log('Decoding polyline: {js_safe_polyline[:50]}...');
|
|
984
|
+
const decodedPath = google.maps.geometry.encoding.decodePath('{js_safe_polyline}');
|
|
985
|
+
console.log('Decoded path points:', decodedPath.length);
|
|
986
|
+
console.log('First few points:', decodedPath.slice(0, 3));
|
|
987
|
+
|
|
988
|
+
const routePath = new google.maps.Polyline({{
|
|
989
|
+
path: decodedPath,
|
|
990
|
+
geodesic: false,
|
|
991
|
+
strokeColor: '#4285F4',
|
|
992
|
+
strokeOpacity: 0.8,
|
|
993
|
+
strokeWeight: 6
|
|
994
|
+
}});
|
|
995
|
+
|
|
996
|
+
routePath.setMap(map);
|
|
997
|
+
|
|
998
|
+
const bounds = new google.maps.LatLngBounds();
|
|
999
|
+
decodedPath.forEach(function(point) {{
|
|
1000
|
+
bounds.extend(point);
|
|
1001
|
+
}});
|
|
1002
|
+
map.fitBounds(bounds);
|
|
1003
|
+
|
|
1004
|
+
console.log('Route polyline added successfully');
|
|
1005
|
+
}} catch (error) {{
|
|
1006
|
+
console.error('Error decoding polyline:', error);
|
|
1007
|
+
console.log('Falling back to marker bounds');
|
|
1008
|
+
const bounds = new google.maps.LatLngBounds();
|
|
1009
|
+
bounds.extend({{lat: {origin_coords[0]}, lng: {origin_coords[1]}}});
|
|
1010
|
+
bounds.extend({{lat: {dest_coords[0]}, lng: {dest_coords[1]}}});"""
|
|
1011
|
+
|
|
1012
|
+
for lat, lng in waypoint_coords:
|
|
1013
|
+
polyline_js += f"""
|
|
1014
|
+
bounds.extend({{lat: {lat}, lng: {lng}}});"""
|
|
1015
|
+
|
|
1016
|
+
polyline_js += """
|
|
1017
|
+
map.fitBounds(bounds);
|
|
1018
|
+
}
|
|
1019
|
+
"""
|
|
1020
|
+
else:
|
|
1021
|
+
self.logger.warning(f"No valid polyline found, using marker bounds only")
|
|
1022
|
+
polyline_js = f"""
|
|
1023
|
+
console.log('No valid polyline, fitting to markers');
|
|
1024
|
+
const bounds = new google.maps.LatLngBounds();
|
|
1025
|
+
bounds.extend({{lat: {origin_coords[0]}, lng: {origin_coords[1]}}});
|
|
1026
|
+
bounds.extend({{lat: {dest_coords[0]}, lng: {dest_coords[1]}}});"""
|
|
1027
|
+
|
|
1028
|
+
for lat, lng in waypoint_coords:
|
|
1029
|
+
polyline_js += f"""
|
|
1030
|
+
bounds.extend({{lat: {lat}, lng: {lng}}});"""
|
|
1031
|
+
|
|
1032
|
+
polyline_js += """
|
|
1033
|
+
map.fitBounds(bounds);
|
|
1034
|
+
"""
|
|
1035
|
+
|
|
1036
|
+
html_content = f"""
|
|
1037
|
+
<!DOCTYPE html>
|
|
1038
|
+
<html>
|
|
1039
|
+
<head>
|
|
1040
|
+
<title>Route Map</title>
|
|
1041
|
+
<style>
|
|
1042
|
+
#map {{ height: 600px; width: 100%; }}
|
|
1043
|
+
.info-panel {{ padding: 20px; background: #f5f5f5; margin: 10px; border-radius: 8px; font-family: Arial, sans-serif; }}
|
|
1044
|
+
.route-info {{ display: flex; gap: 20px; flex-wrap: wrap; }}
|
|
1045
|
+
.info-item {{ background: white; padding: 10px; border-radius: 4px; }}
|
|
1046
|
+
</style>
|
|
1047
|
+
</head>
|
|
1048
|
+
<body>
|
|
1049
|
+
<div class="info-panel">
|
|
1050
|
+
<h2>Route Information</h2>
|
|
1051
|
+
<div class="route-info">
|
|
1052
|
+
<div class="info-item">
|
|
1053
|
+
<strong>Distance:</strong> {args.get('total_distance_formatted', 'N/A')}
|
|
1054
|
+
</div>
|
|
1055
|
+
<div class="info-item">
|
|
1056
|
+
<strong>Duration:</strong> {args.get('total_duration_formatted', 'N/A')}
|
|
1057
|
+
</div>
|
|
1058
|
+
<div class="info-item">
|
|
1059
|
+
<strong>Travel Mode:</strong> {args.get('travel_mode', 'N/A')}
|
|
1060
|
+
</div>
|
|
1061
|
+
</div>
|
|
1062
|
+
</div>
|
|
1063
|
+
<div id="map"></div>
|
|
1064
|
+
<script>
|
|
1065
|
+
function initMap() {{
|
|
1066
|
+
const map = new google.maps.Map(document.getElementById("map"), {{
|
|
1067
|
+
zoom: {zoom_level},
|
|
1068
|
+
center: {{lat: {center_lat}, lng: {center_lng}}},
|
|
1069
|
+
mapTypeId: '{args.get('map_type', 'roadmap')}'
|
|
1070
|
+
}});
|
|
1071
|
+
new google.maps.Marker({{
|
|
1072
|
+
position: {{lat: {origin_coords[0]}, lng: {origin_coords[1]}}},
|
|
1073
|
+
map: map,
|
|
1074
|
+
title: 'Origin',
|
|
1075
|
+
label: 'O',
|
|
1076
|
+
icon: {{ url: 'https://maps.google.com/mapfiles/ms/icons/green-dot.png' }}
|
|
1077
|
+
}});
|
|
1078
|
+
new google.maps.Marker({{
|
|
1079
|
+
position: {{lat: {dest_coords[0]}, lng: {dest_coords[1]}}},
|
|
1080
|
+
map: map,
|
|
1081
|
+
title: 'Destination',
|
|
1082
|
+
label: 'D',
|
|
1083
|
+
icon: {{ url: 'https://maps.google.com/mapfiles/ms/icons/red-dot.png' }}
|
|
1084
|
+
}});
|
|
1085
|
+
{waypoint_markers_js}
|
|
1086
|
+
{polyline_js}
|
|
1087
|
+
}}
|
|
1088
|
+
window.initMap = initMap;
|
|
1089
|
+
</script>
|
|
1090
|
+
<script async defer
|
|
1091
|
+
src="https://maps.googleapis.com/maps/api/js?key={self.google_key}&libraries=geometry&callback=initMap">
|
|
1092
|
+
</script>
|
|
1093
|
+
</body>
|
|
1094
|
+
</html>
|
|
1095
|
+
"""
|
|
1096
|
+
|
|
1097
|
+
return html_content
|
|
1098
|
+
|
|
1099
|
+
async def _execute(self, **kwargs) -> Dict[str, Any]:
|
|
1100
|
+
"""Execute route calculation using Google Routes API v2."""
|
|
1101
|
+
origin = kwargs['origin']
|
|
1102
|
+
destination = kwargs['destination']
|
|
1103
|
+
waypoints = kwargs.get('waypoints', [])
|
|
1104
|
+
travel_mode = kwargs['travel_mode']
|
|
1105
|
+
routing_preference = kwargs['routing_preference']
|
|
1106
|
+
optimize_waypoints = kwargs['optimize_waypoints']
|
|
1107
|
+
departure_time = kwargs.get('departure_time')
|
|
1108
|
+
include_static_map = kwargs['include_static_map']
|
|
1109
|
+
include_interactive_map = kwargs['include_interactive_map']
|
|
1110
|
+
|
|
1111
|
+
# Build request data
|
|
1112
|
+
data = {
|
|
1113
|
+
"origin": self._create_location_object(origin),
|
|
1114
|
+
"destination": self._create_location_object(destination),
|
|
1115
|
+
"travelMode": travel_mode,
|
|
1116
|
+
"routingPreference": routing_preference,
|
|
1117
|
+
"computeAlternativeRoutes": False,
|
|
1118
|
+
"optimizeWaypointOrder": optimize_waypoints,
|
|
1119
|
+
"routeModifiers": {
|
|
1120
|
+
"avoidTolls": False,
|
|
1121
|
+
"avoidHighways": False,
|
|
1122
|
+
"avoidFerries": False
|
|
1123
|
+
},
|
|
1124
|
+
"languageCode": "en-US",
|
|
1125
|
+
"units": "IMPERIAL"
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if waypoints:
|
|
1129
|
+
data['intermediates'] = [self._create_location_object(wp) for wp in waypoints]
|
|
1130
|
+
|
|
1131
|
+
if departure_time:
|
|
1132
|
+
data['departureTime'] = departure_time
|
|
1133
|
+
|
|
1134
|
+
headers = {
|
|
1135
|
+
"Content-Type": "application/json",
|
|
1136
|
+
"X-Goog-Api-Key": self.google_key,
|
|
1137
|
+
"X-Goog-FieldMask": "routes.legs,routes.duration,routes.staticDuration,routes.distanceMeters,routes.polyline,routes.optimizedIntermediateWaypointIndex,routes.description,routes.warnings,routes.viewport,routes.travelAdvisory,routes.localizedValues"
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
# Make API request
|
|
1141
|
+
timeout = aiohttp.ClientTimeout(total=60)
|
|
1142
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
1143
|
+
async with session.post(self.base_url, json=data, headers=headers) as response:
|
|
1144
|
+
if response.status != 200:
|
|
1145
|
+
error_data = await response.json()
|
|
1146
|
+
raise Exception(f"Routes API error: {error_data}")
|
|
1147
|
+
|
|
1148
|
+
result = await response.json()
|
|
1149
|
+
|
|
1150
|
+
if not result or 'routes' not in result or not result['routes']:
|
|
1151
|
+
raise Exception("No routes found in API response")
|
|
1152
|
+
|
|
1153
|
+
# Process route data
|
|
1154
|
+
route = result['routes'][0]
|
|
1155
|
+
|
|
1156
|
+
total_duration_seconds = 0
|
|
1157
|
+
static_duration_seconds = 0
|
|
1158
|
+
total_distance_meters = 0
|
|
1159
|
+
route_instructions = []
|
|
1160
|
+
|
|
1161
|
+
for i, leg in enumerate(route['legs']):
|
|
1162
|
+
duration_str = leg.get('duration', '0s')
|
|
1163
|
+
leg_duration = int(duration_str.rstrip('s')) if duration_str else 0
|
|
1164
|
+
total_duration_seconds += leg_duration
|
|
1165
|
+
|
|
1166
|
+
static_duration_str = leg.get('staticDuration', '0s')
|
|
1167
|
+
static_duration_seconds += int(static_duration_str.rstrip('s'))
|
|
1168
|
+
|
|
1169
|
+
distance_meters = leg.get('distanceMeters', 0)
|
|
1170
|
+
total_distance_meters += distance_meters
|
|
1171
|
+
|
|
1172
|
+
distance_miles = distance_meters / 1609.34
|
|
1173
|
+
route_instructions.append(f"Leg {i+1}: Continue for {distance_miles:.1f} miles")
|
|
1174
|
+
|
|
1175
|
+
total_duration_minutes = total_duration_seconds / 60
|
|
1176
|
+
static_duration_minutes = static_duration_seconds / 60
|
|
1177
|
+
total_distance_miles = total_distance_meters / 1609.34
|
|
1178
|
+
|
|
1179
|
+
waypoint_order = route.get('optimizedIntermediateWaypointIndex', [])
|
|
1180
|
+
|
|
1181
|
+
# Extract coordinates once for map generation
|
|
1182
|
+
self.logger.info("Extracting coordinates for map generation...")
|
|
1183
|
+
coordinates_cache = {
|
|
1184
|
+
'origin': await self._extract_coordinates_from_location(origin),
|
|
1185
|
+
'destination': await self._extract_coordinates_from_location(destination),
|
|
1186
|
+
'waypoints': []
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if waypoints:
|
|
1190
|
+
for waypoint in waypoints:
|
|
1191
|
+
wp_coords = await self._extract_coordinates_from_location(waypoint)
|
|
1192
|
+
coordinates_cache['waypoints'].append(wp_coords)
|
|
1193
|
+
|
|
1194
|
+
self.logger.info(f"Cached coordinates - Origin: {coordinates_cache['origin']}, Dest: {coordinates_cache['destination']}, Waypoints: {coordinates_cache['waypoints']}")
|
|
1195
|
+
|
|
1196
|
+
# Build response
|
|
1197
|
+
response_data = {
|
|
1198
|
+
'origin': origin,
|
|
1199
|
+
'destination': destination,
|
|
1200
|
+
'waypoints': waypoints,
|
|
1201
|
+
'optimized_waypoint_order': waypoint_order,
|
|
1202
|
+
'route_instructions': route_instructions,
|
|
1203
|
+
'total_duration_minutes': total_duration_minutes,
|
|
1204
|
+
'static_duration_minutes': static_duration_minutes,
|
|
1205
|
+
'total_distance_miles': total_distance_miles,
|
|
1206
|
+
'total_duration_formatted': f"{total_duration_minutes:.2f} minutes",
|
|
1207
|
+
'total_distance_formatted': f"{total_distance_miles:.2f} miles",
|
|
1208
|
+
'encoded_polyline': route.get('polyline', {}).get('encodedPolyline'),
|
|
1209
|
+
'raw_response': result
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
# Prepare args for map generation
|
|
1213
|
+
map_args = kwargs.copy()
|
|
1214
|
+
map_args['total_distance_miles'] = total_distance_miles
|
|
1215
|
+
map_args['total_duration_formatted'] = response_data['total_duration_formatted']
|
|
1216
|
+
map_args['total_distance_formatted'] = response_data['total_distance_formatted']
|
|
1217
|
+
|
|
1218
|
+
# Generate static map if requested
|
|
1219
|
+
if include_static_map:
|
|
1220
|
+
response_data['static_map_url'] = await self._generate_static_map_url(result, coordinates_cache, map_args)
|
|
1221
|
+
|
|
1222
|
+
# Generate interactive map if requested
|
|
1223
|
+
if include_interactive_map:
|
|
1224
|
+
html_map = self._generate_interactive_html_map(result, coordinates_cache, map_args)
|
|
1225
|
+
|
|
1226
|
+
if self.output_dir:
|
|
1227
|
+
filename = self.generate_filename("route_map", "html", include_timestamp=True)
|
|
1228
|
+
html_file_path = self.output_dir / filename
|
|
1229
|
+
|
|
1230
|
+
with open(html_file_path, 'w', encoding='utf-8') as f:
|
|
1231
|
+
f.write(html_map)
|
|
1232
|
+
|
|
1233
|
+
response_data['interactive_map_file'] = str(html_file_path)
|
|
1234
|
+
response_data['interactive_map_url'] = self.to_static_url(html_file_path)
|
|
1235
|
+
|
|
1236
|
+
response_data['interactive_map_html'] = html_map
|
|
1237
|
+
|
|
1238
|
+
# Also include static map URL when interactive map is requested
|
|
1239
|
+
if not include_static_map:
|
|
1240
|
+
response_data['static_map_url'] = await self._generate_static_map_url(result, coordinates_cache, map_args)
|
|
1241
|
+
|
|
1242
|
+
return response_data
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
# Export all tools
|
|
1246
|
+
__all__ = [
|
|
1247
|
+
'GoogleSearchTool',
|
|
1248
|
+
'GoogleSiteSearchTool',
|
|
1249
|
+
'GoogleLocationTool',
|
|
1250
|
+
'GoogleRoutesTool'
|
|
1251
|
+
]
|