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,933 @@
|
|
|
1
|
+
from typing import Any, Optional, Tuple, Dict, Union, List
|
|
2
|
+
import re
|
|
3
|
+
import uuid
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import html
|
|
7
|
+
import folium
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from .chart import BaseChart
|
|
10
|
+
from . import register_renderer
|
|
11
|
+
from ...models.outputs import OutputMode
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import geopandas as gpd
|
|
15
|
+
GEOPANDAS_AVAILABLE = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
GEOPANDAS_AVAILABLE = False
|
|
18
|
+
gpd = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
FOLIUM_SYSTEM_PROMPT = """FOLIUM MAP OUTPUT MODE:
|
|
22
|
+
When user request a MAP, generate an interactive map using Folium by extracting and using geographic information from the available data.
|
|
23
|
+
|
|
24
|
+
MAP GENERATION STRATEGY:
|
|
25
|
+
1. First, create a filtered dataframe with the data you need (e.g., `map_data = df[...]`).
|
|
26
|
+
2. The tool will confirm the variable creation.
|
|
27
|
+
3. THEN, in the same or next turn, generate the Folium code referencing `map_data`.
|
|
28
|
+
4. DO NOT print the content of `map_data`.
|
|
29
|
+
5. ALWAYS return the map as Python code in a markdown block (```python).
|
|
30
|
+
|
|
31
|
+
🚨 CRITICAL REQUIREMENTS: 🚨
|
|
32
|
+
|
|
33
|
+
1. **ALWAYS analyze the available data for geographic information**:
|
|
34
|
+
- Look for columns with: latitude, longitude, lat, lon, coordinates
|
|
35
|
+
- Look for location columns: city, state, country, address, location, place
|
|
36
|
+
- Look for postal codes or zip codes that can be geocoded
|
|
37
|
+
|
|
38
|
+
2. **Extract coordinates**:
|
|
39
|
+
- If you have lat/lon columns, use them directly
|
|
40
|
+
- If you have addresses/cities, you MUST extract or infer coordinates
|
|
41
|
+
- For well-known cities, use their standard coordinates
|
|
42
|
+
- Example: "Miami, Florida" → [25.7617, -80.1918]
|
|
43
|
+
|
|
44
|
+
3. **Validate coordinates before using them**:
|
|
45
|
+
- NEVER use coordinates (0.0, 0.0) - these are invalid placeholders
|
|
46
|
+
- For invalid/missing coordinates:
|
|
47
|
+
* If you have an address, try to infer approximate location
|
|
48
|
+
* If location is in same city as other valid points, estimate nearby
|
|
49
|
+
* If no inference possible, SKIP the marker entirely
|
|
50
|
+
- Coordinate sanity checks:
|
|
51
|
+
* Latitude must be between -90 and 90
|
|
52
|
+
* Longitude must be between -180 and 180
|
|
53
|
+
* For US locations: lat ~25-50, lon ~-125 to -65
|
|
54
|
+
|
|
55
|
+
4. **Return Python code in markdown block** (```python):
|
|
56
|
+
- Import folium at the top
|
|
57
|
+
- Store map in variable: 'm', 'map', 'folium_map', or 'my_map'
|
|
58
|
+
- Add ALL relevant data points as markers (except invalid ones)
|
|
59
|
+
- Include popups with useful information
|
|
60
|
+
- DO NOT call map.save() or display methods
|
|
61
|
+
|
|
62
|
+
5. **Map configuration**:
|
|
63
|
+
- Center map on the average/median of all VALID locations
|
|
64
|
+
- Set appropriate zoom level (10-14 for cities, 5-8 for regions)
|
|
65
|
+
- Use descriptive popups and tooltips
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
BASIC EXAMPLE:
|
|
69
|
+
```python
|
|
70
|
+
import folium
|
|
71
|
+
|
|
72
|
+
# Create base map with correct coordinate order
|
|
73
|
+
m = folium.Map(
|
|
74
|
+
location=[40.7128, -74.0060], # [latitude, longitude]
|
|
75
|
+
zoom_start=12,
|
|
76
|
+
tiles='OpenStreetMap'
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Add marker with correct coordinate order
|
|
80
|
+
folium.Marker(
|
|
81
|
+
location=[40.7128, -74.0060], # [latitude, longitude]
|
|
82
|
+
popup='New York City',
|
|
83
|
+
tooltip='Click for info',
|
|
84
|
+
icon=folium.Icon(color='red', icon='info-sign')
|
|
85
|
+
).add_to(m)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
MULTIPLE MARKERS EXAMPLE:
|
|
89
|
+
```python
|
|
90
|
+
import folium
|
|
91
|
+
import pandas as pd
|
|
92
|
+
|
|
93
|
+
# Sample data with lat/lon columns
|
|
94
|
+
stores = pd.DataFrame({
|
|
95
|
+
'name': ['Store A', 'Store B', 'Store C'],
|
|
96
|
+
'latitude': [40.7128, 34.0522, 41.8781], # lat column
|
|
97
|
+
'longitude': [-74.0060, -118.2437, -87.6298] # lon column
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
# Calculate center from data
|
|
101
|
+
center_lat = stores['latitude'].median()
|
|
102
|
+
center_lon = stores['longitude'].median()
|
|
103
|
+
|
|
104
|
+
# Create map centered on data
|
|
105
|
+
m = folium.Map(
|
|
106
|
+
location=[center_lat, center_lon], # ALWAYS [lat, lon]
|
|
107
|
+
zoom_start=5
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Add markers - iterate with correct order
|
|
111
|
+
for idx, row in stores.iterrows():
|
|
112
|
+
folium.Marker(
|
|
113
|
+
location=[row['latitude'], row['longitude']], # [lat, lon]
|
|
114
|
+
popup=f"Store: {row['name']}",
|
|
115
|
+
tooltip=row['name']
|
|
116
|
+
).add_to(m)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
COORDINATE VALIDATION FUNCTION (OPTIONAL):
|
|
120
|
+
```python
|
|
121
|
+
def validate_coords(lat, lon, name=""):
|
|
122
|
+
\"\"\"Validate lat/lon are in correct ranges.\"\"\"
|
|
123
|
+
if not (-90 <= lat <= 90):
|
|
124
|
+
print(f"⚠️ Invalid latitude for {name}: {lat}")
|
|
125
|
+
return False
|
|
126
|
+
if not (-180 <= lon <= 180):
|
|
127
|
+
print(f"⚠️ Invalid longitude for {name}: {lon}")
|
|
128
|
+
return False
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
# Use before adding markers:
|
|
132
|
+
if validate_coords(lat, lon, store_name):
|
|
133
|
+
folium.Marker(location=[lat, lon], ...).add_to(m)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
DATA MODE (when DataFrame is provided):
|
|
137
|
+
If a DataFrame is provided with geographic data, return it as-is or with minimal processing.
|
|
138
|
+
The system will automatically combine it with GeoJSON to create choropleth maps.
|
|
139
|
+
Ensure the DataFrame has columns that can join with GeoJSON properties.
|
|
140
|
+
|
|
141
|
+
ADVANCED FEATURES:
|
|
142
|
+
- For heatmaps: use folium.plugins.HeatMap
|
|
143
|
+
- For polylines: use folium.PolyLine (coordinates in [lat, lon] order!)
|
|
144
|
+
- For circles: folium.Circle(location=[lat, lon], radius=...)
|
|
145
|
+
- For custom tiles: ALWAYS include attribution parameter
|
|
146
|
+
Example: folium.TileLayer('Stamen Terrain', attr='Map tiles by Stamen Design').add_to(m)
|
|
147
|
+
- Use clear, informative popups and tooltips
|
|
148
|
+
|
|
149
|
+
COMMON MISTAKES TO AVOID:
|
|
150
|
+
❌ Using [lon, lat] order instead of [lat, lon]
|
|
151
|
+
❌ Forgetting to calculate map center from data
|
|
152
|
+
❌ Using fixed zoom level that doesn't fit the data
|
|
153
|
+
❌ Not validating coordinate ranges
|
|
154
|
+
❌ Swapping latitude and longitude column references
|
|
155
|
+
|
|
156
|
+
FINAL CHECKLIST BEFORE RETURNING CODE:
|
|
157
|
+
1. ✓ All folium.Map() calls use [latitude, longitude] order
|
|
158
|
+
2. ✓ All folium.Marker() calls use [latitude, longitude] order
|
|
159
|
+
3. ✓ All coordinate arrays/lists use [latitude, longitude] order
|
|
160
|
+
4. ✓ Map center is calculated from actual marker positions
|
|
161
|
+
5. ✓ Zoom level is appropriate for geographic spread
|
|
162
|
+
6. ✓ No longitude values in the latitude position
|
|
163
|
+
7. ✓ No latitude values in the longitude position
|
|
164
|
+
|
|
165
|
+
Remember: LATITUDE FIRST, LONGITUDE SECOND. Always [lat, lon], never [lon, lat]!
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
FOLIUM_DATA_PROMPT = """FOLIUM DATA MODE:
|
|
170
|
+
You are generating data for a choropleth map.
|
|
171
|
+
|
|
172
|
+
REQUIREMENTS:
|
|
173
|
+
1. Return a pandas DataFrame with geographic data
|
|
174
|
+
2. Include a column that matches GeoJSON property keys (e.g., 'state', 'country', 'region_id')
|
|
175
|
+
3. Include numeric columns for visualization (e.g., 'population', 'value', 'score')
|
|
176
|
+
4. Data should be clean and ready for visualization
|
|
177
|
+
|
|
178
|
+
EXAMPLE OUTPUT (as Python code that creates DataFrame):
|
|
179
|
+
```python
|
|
180
|
+
import pandas as pd
|
|
181
|
+
|
|
182
|
+
data = pd.DataFrame({
|
|
183
|
+
'state': ['California', 'Texas', 'Florida', 'New York'],
|
|
184
|
+
'population': [39538223, 29145505, 21538187, 20201249],
|
|
185
|
+
'gdp': [3.4, 2.1, 1.2, 1.9]
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@register_renderer(OutputMode.MAP, system_prompt=FOLIUM_SYSTEM_PROMPT)
|
|
192
|
+
class FoliumRenderer(BaseChart):
|
|
193
|
+
"""Renderer for Folium maps with support for DataFrames and GeoJSON"""
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def get_expected_content_type(cls) -> type:
|
|
197
|
+
"""
|
|
198
|
+
This renderer can work with both string (code) and DataFrame (data).
|
|
199
|
+
We'll handle both in the render method.
|
|
200
|
+
"""
|
|
201
|
+
return Union[str, pd.DataFrame] if GEOPANDAS_AVAILABLE else str
|
|
202
|
+
|
|
203
|
+
def _is_valid_latitude(self, value: Any) -> bool:
|
|
204
|
+
"""Check if value is a valid latitude (-90 to 90)."""
|
|
205
|
+
return isinstance(value, (int, float)) and -90 <= value <= 90
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _is_valid_longitude(self, value: Any) -> bool:
|
|
209
|
+
"""Check if value is a valid longitude (-180 to 180)."""
|
|
210
|
+
return isinstance(value, (int, float)) and -180 <= value <= 180
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _detect_coordinate_swap(self, lat: float, lon: float) -> bool:
|
|
214
|
+
"""
|
|
215
|
+
Detect if coordinates are likely swapped using multiple heuristics.
|
|
216
|
+
|
|
217
|
+
Returns True if coordinates appear to be swapped.
|
|
218
|
+
"""
|
|
219
|
+
# Basic validation - both must be numeric
|
|
220
|
+
if not (isinstance(lat, (int, float)) and isinstance(lon, (int, float))):
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
# Check 1: Basic range check
|
|
224
|
+
lat_in_lat_range = -90 <= lat <= 90
|
|
225
|
+
lon_in_lon_range = -180 <= lon <= 180
|
|
226
|
+
lat_in_lon_range = -180 <= lat <= 180
|
|
227
|
+
lon_in_lat_range = -90 <= lon <= 90
|
|
228
|
+
|
|
229
|
+
# If current order is invalid but swapped would be valid
|
|
230
|
+
if not (lat_in_lat_range and lon_in_lon_range):
|
|
231
|
+
if lat_in_lon_range and lon_in_lat_range:
|
|
232
|
+
return True # Definitely swapped
|
|
233
|
+
|
|
234
|
+
# Check 2: Magnitude heuristic for common locations
|
|
235
|
+
# Most inhabited locations: lat magnitude < 70, lon can be larger
|
|
236
|
+
if abs(lat) > 90:
|
|
237
|
+
return True # Invalid latitude, must be swapped
|
|
238
|
+
|
|
239
|
+
# Check 3: Sign heuristic for Western Hemisphere (Americas, especially US)
|
|
240
|
+
# For US/Americas: latitude should be positive (20-50), longitude negative (-60 to -180)
|
|
241
|
+
if lat < 0 and lon > 0:
|
|
242
|
+
# Negative latitude, positive longitude = likely Southern Hemisphere or swapped
|
|
243
|
+
# Check if swapping would make sense for US/Americas
|
|
244
|
+
if -130 <= lat <= -60 and 20 <= lon <= 50:
|
|
245
|
+
print(f" 📍 Detected likely swap (US coordinates): [{lat}, {lon}] → [{lon}, {lat}]")
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
# Check 4: Extreme latitude with moderate longitude
|
|
249
|
+
# If latitude is very high/low (near poles) but longitude is moderate, might be swapped
|
|
250
|
+
if abs(lat) > 75 and abs(lon) < 75:
|
|
251
|
+
# Check if swapping makes more sense
|
|
252
|
+
if abs(lon) > 10 and abs(lat) > abs(lon):
|
|
253
|
+
# Probably swapped (unless actually near poles)
|
|
254
|
+
if not (85 <= abs(lat) <= 90): # Not actually at poles
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
# Check 5: Florida-specific heuristic
|
|
258
|
+
# Florida: lat 24.5-31 (positive), lon -80 to -87 (negative)
|
|
259
|
+
if -90 <= lat <= -75 and 20 <= lon <= 35:
|
|
260
|
+
print(f" 🌴 Detected Florida coordinates swap: [{lat}, {lon}] → [{lon}, {lat}]")
|
|
261
|
+
return True
|
|
262
|
+
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _normalize_location(self, location: Any) -> Tuple[Any, bool]:
|
|
267
|
+
"""
|
|
268
|
+
Ensure coordinates are in [lat, lon] order and within valid ranges.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
(normalized_location, was_swapped)
|
|
272
|
+
"""
|
|
273
|
+
if not isinstance(location, (list, tuple)) or len(location) < 2:
|
|
274
|
+
return location, False
|
|
275
|
+
|
|
276
|
+
lat, lon = location[0], location[1]
|
|
277
|
+
|
|
278
|
+
# Skip if not numeric
|
|
279
|
+
if not (isinstance(lat, (int, float)) and isinstance(lon, (int, float))):
|
|
280
|
+
return location, False
|
|
281
|
+
|
|
282
|
+
# Check if coordinates appear to be swapped
|
|
283
|
+
if self._detect_coordinate_swap(lat, lon):
|
|
284
|
+
# Swap coordinates
|
|
285
|
+
fixed_location = [lon, lat, *location[2:]]
|
|
286
|
+
return fixed_location, True
|
|
287
|
+
|
|
288
|
+
# Check if coordinates are valid as-is
|
|
289
|
+
if self._is_valid_latitude(lat) and self._is_valid_longitude(lon):
|
|
290
|
+
return list(location), False
|
|
291
|
+
|
|
292
|
+
# Invalid coordinates but swapping doesn't help
|
|
293
|
+
print(f" ⚠️ Invalid coordinates (can't auto-fix): [{lat}, {lon}]")
|
|
294
|
+
return list(location), False
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _prepare_map_coordinates(self, map_obj: Any) -> None:
|
|
298
|
+
"""
|
|
299
|
+
Normalize marker coordinates and recenter the map.
|
|
300
|
+
|
|
301
|
+
Improvements:
|
|
302
|
+
- Better coordinate validation
|
|
303
|
+
- More detailed logging
|
|
304
|
+
- Robust center calculation
|
|
305
|
+
"""
|
|
306
|
+
coordinates: List[Tuple[float, float]] = []
|
|
307
|
+
swaps = 0
|
|
308
|
+
invalid = 0
|
|
309
|
+
total_markers = 0
|
|
310
|
+
|
|
311
|
+
print("\n📍 Validating map coordinates...")
|
|
312
|
+
|
|
313
|
+
for child in getattr(map_obj, '_children', {}).values():
|
|
314
|
+
location = getattr(child, 'location', None)
|
|
315
|
+
if location is None:
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
total_markers += 1
|
|
319
|
+
original_location = location.copy() if isinstance(location, list) else list(location)
|
|
320
|
+
|
|
321
|
+
fixed_location, was_swapped = self._normalize_location(location)
|
|
322
|
+
|
|
323
|
+
if was_swapped:
|
|
324
|
+
setattr(child, 'location', fixed_location)
|
|
325
|
+
swaps += 1
|
|
326
|
+
print(f" ✓ Fixed: {original_location} → {fixed_location}")
|
|
327
|
+
|
|
328
|
+
# Collect valid coordinates for centering
|
|
329
|
+
if isinstance(fixed_location, (list, tuple)) and len(fixed_location) >= 2:
|
|
330
|
+
first, second = fixed_location[0], fixed_location[1]
|
|
331
|
+
if self._is_valid_latitude(first) and self._is_valid_longitude(second):
|
|
332
|
+
coordinates.append((first, second))
|
|
333
|
+
else:
|
|
334
|
+
invalid += 1
|
|
335
|
+
print(f" ⚠️ Invalid coordinates (skipping): {fixed_location}")
|
|
336
|
+
|
|
337
|
+
# Update map center based on valid coordinates
|
|
338
|
+
if coordinates:
|
|
339
|
+
lats = pd.Series([lat for lat, _ in coordinates])
|
|
340
|
+
lons = pd.Series([lon for _, lon in coordinates])
|
|
341
|
+
|
|
342
|
+
new_center = [float(lats.median()), float(lons.median())]
|
|
343
|
+
old_center = map_obj.location
|
|
344
|
+
|
|
345
|
+
map_obj.location = new_center
|
|
346
|
+
|
|
347
|
+
print(f"\n📊 Coordinate validation summary:")
|
|
348
|
+
print(f" Total markers: {total_markers}")
|
|
349
|
+
print(f" Valid coordinates: {len(coordinates)}")
|
|
350
|
+
print(f" Swapped and fixed: {swaps}")
|
|
351
|
+
print(f" Invalid (skipped): {invalid}")
|
|
352
|
+
print(f" Map center: {old_center} → {new_center}")
|
|
353
|
+
|
|
354
|
+
# Suggest appropriate zoom level based on coordinate spread
|
|
355
|
+
lat_range = float(lats.max() - lats.min())
|
|
356
|
+
lon_range = float(lons.max() - lons.min())
|
|
357
|
+
max_range = max(lat_range, lon_range)
|
|
358
|
+
|
|
359
|
+
if max_range < 0.1:
|
|
360
|
+
suggested_zoom = 13 # Very tight cluster
|
|
361
|
+
elif max_range < 1:
|
|
362
|
+
suggested_zoom = 10 # City level
|
|
363
|
+
elif max_range < 5:
|
|
364
|
+
suggested_zoom = 7 # Regional
|
|
365
|
+
elif max_range < 20:
|
|
366
|
+
suggested_zoom = 5 # Multi-state
|
|
367
|
+
else:
|
|
368
|
+
suggested_zoom = 3 # Continental
|
|
369
|
+
|
|
370
|
+
# Update zoom if it seems wrong
|
|
371
|
+
current_zoom = map_obj.options.get('zoom', map_obj.options.get('zoom_start', 10))
|
|
372
|
+
if abs(current_zoom - suggested_zoom) > 3:
|
|
373
|
+
print(f" 💡 Suggested zoom: {suggested_zoom} (current: {current_zoom})")
|
|
374
|
+
|
|
375
|
+
else:
|
|
376
|
+
print(f" ⚠️ No valid coordinates found among {total_markers} markers")
|
|
377
|
+
|
|
378
|
+
print() # Empty line for readability
|
|
379
|
+
|
|
380
|
+
def execute_code(
|
|
381
|
+
self,
|
|
382
|
+
code: str,
|
|
383
|
+
pandas_tool: Any = None,
|
|
384
|
+
execution_state: Optional[Dict[str, Any]] = None,
|
|
385
|
+
**kwargs,
|
|
386
|
+
) -> Tuple[Any, Optional[str]]:
|
|
387
|
+
"""Execute Folium map code and return map object."""
|
|
388
|
+
extra_namespace = None
|
|
389
|
+
if pandas_tool is None:
|
|
390
|
+
try:
|
|
391
|
+
import folium
|
|
392
|
+
extra_namespace = {'folium': folium}
|
|
393
|
+
except ImportError:
|
|
394
|
+
return None, "folium library not available"
|
|
395
|
+
|
|
396
|
+
context, error = super().execute_code(
|
|
397
|
+
code,
|
|
398
|
+
pandas_tool=pandas_tool,
|
|
399
|
+
execution_state=execution_state,
|
|
400
|
+
extra_namespace=extra_namespace,
|
|
401
|
+
**kwargs,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
if error:
|
|
405
|
+
return None, error
|
|
406
|
+
|
|
407
|
+
if not context:
|
|
408
|
+
return None, "Execution context was empty"
|
|
409
|
+
|
|
410
|
+
# Debug: print all variables in context
|
|
411
|
+
# print(f"CONTEXT KEYS: {list(context.keys())}")
|
|
412
|
+
|
|
413
|
+
# Try to find map object
|
|
414
|
+
map_obj = None
|
|
415
|
+
for var_name in ['m', 'map', 'folium_map', 'my_map']:
|
|
416
|
+
if var_name in context:
|
|
417
|
+
obj = context[var_name]
|
|
418
|
+
print(f"Found variable '{var_name}': {type(obj)}")
|
|
419
|
+
# Check if it's a folium Map
|
|
420
|
+
if hasattr(obj, '_name') and hasattr(obj, 'location'):
|
|
421
|
+
map_obj = obj
|
|
422
|
+
break
|
|
423
|
+
|
|
424
|
+
# If still None, try to find any folium.Map object
|
|
425
|
+
if map_obj is None:
|
|
426
|
+
for var_name, obj in context.items():
|
|
427
|
+
if var_name.startswith('_'):
|
|
428
|
+
continue
|
|
429
|
+
# Check if it's a folium Map by class name
|
|
430
|
+
if obj.__class__.__name__ == 'Map' and 'folium' in obj.__class__.__module__:
|
|
431
|
+
print(f"Found folium Map in variable '{var_name}'")
|
|
432
|
+
map_obj = obj
|
|
433
|
+
break
|
|
434
|
+
|
|
435
|
+
# Handle DataFrame case (for data mode)
|
|
436
|
+
if map_obj is None:
|
|
437
|
+
for var_name in ['data', 'df']:
|
|
438
|
+
if var_name in context and isinstance(context[var_name], pd.DataFrame):
|
|
439
|
+
return context[var_name], None
|
|
440
|
+
|
|
441
|
+
if map_obj is None:
|
|
442
|
+
# Provide helpful error message
|
|
443
|
+
available_vars = [k for k in context.keys() if not k.startswith('_')]
|
|
444
|
+
return None, (
|
|
445
|
+
f"Code must define a folium Map variable (m, map, folium_map, or my_map). "
|
|
446
|
+
f"Available variables: {', '.join(available_vars)}"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
return map_obj, None
|
|
450
|
+
|
|
451
|
+
def _create_choropleth_map(
|
|
452
|
+
self,
|
|
453
|
+
data: pd.DataFrame,
|
|
454
|
+
geojson_path: str,
|
|
455
|
+
key_on: str,
|
|
456
|
+
columns: Tuple[str, str],
|
|
457
|
+
**kwargs
|
|
458
|
+
) -> Any:
|
|
459
|
+
"""Create a choropleth map from DataFrame and GeoJSON."""
|
|
460
|
+
if not GEOPANDAS_AVAILABLE:
|
|
461
|
+
raise ImportError("geopandas is required for choropleth maps")
|
|
462
|
+
|
|
463
|
+
if isinstance(geojson_path, (str, Path)):
|
|
464
|
+
gdf = gpd.read_file(geojson_path)
|
|
465
|
+
else:
|
|
466
|
+
gdf = geojson_path
|
|
467
|
+
|
|
468
|
+
center = kwargs.get('center')
|
|
469
|
+
if center is None:
|
|
470
|
+
bounds = gdf.total_bounds
|
|
471
|
+
center = [(bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2]
|
|
472
|
+
|
|
473
|
+
m = folium.Map(
|
|
474
|
+
location=center,
|
|
475
|
+
zoom_start=kwargs.get('zoom_start', 6),
|
|
476
|
+
tiles=kwargs.get('tiles', 'OpenStreetMap')
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
folium.Choropleth(
|
|
480
|
+
geo_data=gdf,
|
|
481
|
+
name='choropleth',
|
|
482
|
+
data=data,
|
|
483
|
+
columns=columns,
|
|
484
|
+
key_on=key_on,
|
|
485
|
+
fill_color=kwargs.get('fill_color', 'YlOrRd'),
|
|
486
|
+
fill_opacity=kwargs.get('fill_opacity', 0.7),
|
|
487
|
+
line_opacity=kwargs.get('line_opacity', 0.2),
|
|
488
|
+
legend_name=kwargs.get('legend_name', columns[1]),
|
|
489
|
+
highlight=kwargs.get('highlight', True)
|
|
490
|
+
).add_to(m)
|
|
491
|
+
|
|
492
|
+
if kwargs.get('layer_control', True):
|
|
493
|
+
folium.LayerControl().add_to(m)
|
|
494
|
+
|
|
495
|
+
if kwargs.get('add_tooltips', True):
|
|
496
|
+
self._add_choropleth_tooltips(m, gdf, data, columns, key_on)
|
|
497
|
+
|
|
498
|
+
return m
|
|
499
|
+
|
|
500
|
+
def _add_choropleth_tooltips(
|
|
501
|
+
self,
|
|
502
|
+
map_obj: Any,
|
|
503
|
+
gdf: gpd.GeoDataFrame,
|
|
504
|
+
data: pd.DataFrame,
|
|
505
|
+
columns: Tuple[str, str],
|
|
506
|
+
key_on: str
|
|
507
|
+
):
|
|
508
|
+
"""Add interactive tooltips to choropleth map."""
|
|
509
|
+
property_name = key_on.split('.')[-1]
|
|
510
|
+
|
|
511
|
+
gdf_with_data = gdf.merge(
|
|
512
|
+
data,
|
|
513
|
+
left_on=property_name,
|
|
514
|
+
right_on=columns[0],
|
|
515
|
+
how='left'
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
folium.GeoJson(
|
|
519
|
+
gdf_with_data,
|
|
520
|
+
style_function=lambda x: {
|
|
521
|
+
'fillColor': 'transparent',
|
|
522
|
+
'color': 'transparent',
|
|
523
|
+
'weight': 0
|
|
524
|
+
},
|
|
525
|
+
tooltip=folium.GeoJsonTooltip(
|
|
526
|
+
fields=[property_name, columns[1]],
|
|
527
|
+
aliases=[property_name.capitalize(), columns[1].capitalize()],
|
|
528
|
+
localize=True
|
|
529
|
+
)
|
|
530
|
+
).add_to(map_obj)
|
|
531
|
+
|
|
532
|
+
def _extract_head_resources(self, full_html: str) -> str:
|
|
533
|
+
"""
|
|
534
|
+
Extracts scripts and styles from the <head> of the Folium HTML.
|
|
535
|
+
This allows us to pass them to BaseChart to include in the header.
|
|
536
|
+
"""
|
|
537
|
+
head_match = re.search(r'<head[^>]*>(.*?)</head>', full_html, re.DOTALL)
|
|
538
|
+
if not head_match:
|
|
539
|
+
return ""
|
|
540
|
+
|
|
541
|
+
content = head_match[1]
|
|
542
|
+
|
|
543
|
+
# Capture full script/style/link tags to avoid malformed HTML fragments
|
|
544
|
+
resources: List[str] = []
|
|
545
|
+
for pattern in [
|
|
546
|
+
r'<script[^>]*>.*?</script>',
|
|
547
|
+
r'<style[^>]*>.*?</style>',
|
|
548
|
+
r'<link[^>]*?>',
|
|
549
|
+
]:
|
|
550
|
+
resources.extend(re.findall(pattern, content, re.DOTALL))
|
|
551
|
+
|
|
552
|
+
return '\n'.join(resources)
|
|
553
|
+
|
|
554
|
+
def _render_chart_content(self, chart_obj: Any, **kwargs) -> str:
|
|
555
|
+
"""
|
|
556
|
+
Render Folium map content (Body + Inline Scripts).
|
|
557
|
+
This implements the abstract method from BaseChart.
|
|
558
|
+
|
|
559
|
+
Note: we keep the original Folium-generated map ID to ensure all
|
|
560
|
+
associated styles and scripts continue to reference the same element.
|
|
561
|
+
This prevents broken layouts where the map container ends up with no
|
|
562
|
+
height because the IDs in the head and body fall out of sync.
|
|
563
|
+
"""
|
|
564
|
+
# Render the map to a complete HTML string
|
|
565
|
+
full_html = chart_obj.get_root().render()
|
|
566
|
+
|
|
567
|
+
# Extract the body content (divs and inline scripts)
|
|
568
|
+
# We use the same logic as before, but now strictly for the body
|
|
569
|
+
explanation = kwargs.get('explanation')
|
|
570
|
+
explanation_block = self._build_explanation_section(explanation)
|
|
571
|
+
return f"{explanation_block}{self._extract_map_content(full_html)}"
|
|
572
|
+
|
|
573
|
+
def to_html(
|
|
574
|
+
self,
|
|
575
|
+
chart_obj: Any,
|
|
576
|
+
mode: str = 'partial',
|
|
577
|
+
**kwargs
|
|
578
|
+
) -> str:
|
|
579
|
+
"""
|
|
580
|
+
Convert Folium map to HTML using BaseChart's standard pipeline.
|
|
581
|
+
"""
|
|
582
|
+
# 1. Generate the full Folium HTML internally to get resources
|
|
583
|
+
full_html = chart_obj.get_root().render()
|
|
584
|
+
|
|
585
|
+
# 2. Extract the CDN links and CSS from the head
|
|
586
|
+
extra_head = self._extract_head_resources(full_html)
|
|
587
|
+
|
|
588
|
+
# 3. Pass to parent to use standard template
|
|
589
|
+
# Note: parent calls self._render_chart_content internally
|
|
590
|
+
return super().to_html(
|
|
591
|
+
chart_obj,
|
|
592
|
+
mode=mode,
|
|
593
|
+
extra_head=extra_head, # Inject Folium JS/CSS here
|
|
594
|
+
icon='🗺️',
|
|
595
|
+
**kwargs
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
@staticmethod
|
|
599
|
+
def _extract_map_content(full_html: str, map_id: Optional[str] = None) -> str:
|
|
600
|
+
"""
|
|
601
|
+
Extract map content (Divs + Script) from full Folium HTML.
|
|
602
|
+
|
|
603
|
+
We intentionally keep the original Folium-generated map ID unless a
|
|
604
|
+
custom ID is provided. This avoids mismatches between IDs referenced in
|
|
605
|
+
<head> resources (styles/scripts) and the body content that would
|
|
606
|
+
otherwise leave the map container with no height.
|
|
607
|
+
"""
|
|
608
|
+
# 1. Extract Custom Styles
|
|
609
|
+
styles = []
|
|
610
|
+
for style_match in re.finditer(r'<style[^>]*>(.*?)</style>', full_html, re.DOTALL):
|
|
611
|
+
styles.append(style_match.group(0))
|
|
612
|
+
|
|
613
|
+
# 2. Find the map div
|
|
614
|
+
div_pattern = r'<div[^>]*id="(map_[^"]*)"[^>]*>.*?</div>'
|
|
615
|
+
div_match = re.search(div_pattern, full_html, re.DOTALL)
|
|
616
|
+
|
|
617
|
+
if div_match:
|
|
618
|
+
original_id = div_match[1]
|
|
619
|
+
map_id = map_id or original_id
|
|
620
|
+
|
|
621
|
+
# Obtenemos el HTML crudo del div
|
|
622
|
+
map_div = div_match[0]
|
|
623
|
+
|
|
624
|
+
# --- PASO 1: Actualizar el ID ---
|
|
625
|
+
map_div = map_div.replace(f'id="{original_id}"', f'id="{map_id}"')
|
|
626
|
+
|
|
627
|
+
# --- PASO 2: Inyectar Altura Fija (La solución al problema) ---
|
|
628
|
+
# Definimos la altura deseada
|
|
629
|
+
fixed_height_style = "height: 600px; min-height: 600px;"
|
|
630
|
+
|
|
631
|
+
# Intentamos reemplazar la altura porcentual que genera Folium (ej: height: 100.0%;)
|
|
632
|
+
# Usamos Regex para ser flexibles con espacios o decimales
|
|
633
|
+
map_div, num_subs = re.subn(
|
|
634
|
+
r'height:\s*100(\.0)?%;',
|
|
635
|
+
fixed_height_style,
|
|
636
|
+
map_div
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
# Si el regex no encontró nada (ej: Folium cambió formato), inyectamos el estilo a la fuerza
|
|
640
|
+
if num_subs == 0:
|
|
641
|
+
if 'style="' in map_div:
|
|
642
|
+
# Agregamos al principio del estilo existente con !important por si acaso
|
|
643
|
+
map_div = map_div.replace('style="', f'style="{fixed_height_style} ')
|
|
644
|
+
else:
|
|
645
|
+
# Si no hay estilo, creamos uno
|
|
646
|
+
map_div = map_div.replace('<div', f'<div style="{fixed_height_style}"')
|
|
647
|
+
|
|
648
|
+
# 3. Extract Inline Scripts
|
|
649
|
+
inline_scripts = []
|
|
650
|
+
for script_match in re.finditer(r'<script[^>]*>(.*?)</script>', full_html, re.DOTALL):
|
|
651
|
+
opening_tag = script_match.group(0)
|
|
652
|
+
script_content = script_match.group(1)
|
|
653
|
+
|
|
654
|
+
if 'src=' not in opening_tag and script_content.strip():
|
|
655
|
+
updated_script = script_content.replace(f'"{original_id}"', f'"{map_id}"')
|
|
656
|
+
updated_script = updated_script.replace(f"'{original_id}'", f"'{map_id}'")
|
|
657
|
+
inline_scripts.append(updated_script)
|
|
658
|
+
else:
|
|
659
|
+
map_id = map_id or f'folium-map-{uuid.uuid4().hex[:8]}'
|
|
660
|
+
# Fallback en caso de error de regex general
|
|
661
|
+
map_div = f'<div id="{map_id}" style="width: 100%; height: 600px;">Map Rendering Error</div>'
|
|
662
|
+
inline_scripts = []
|
|
663
|
+
|
|
664
|
+
# 4. Combine with proper newlines
|
|
665
|
+
parts = []
|
|
666
|
+
|
|
667
|
+
# Add styles with separation
|
|
668
|
+
if styles:
|
|
669
|
+
parts.extend(styles)
|
|
670
|
+
parts.append('') # Extra newline after styles
|
|
671
|
+
|
|
672
|
+
# Add map div
|
|
673
|
+
parts.append(map_div)
|
|
674
|
+
parts.append('') # Extra newline after div
|
|
675
|
+
|
|
676
|
+
# Add scripts with proper formatting
|
|
677
|
+
if inline_scripts:
|
|
678
|
+
parts.append('<script>')
|
|
679
|
+
parts.append('') # Newline after opening tag
|
|
680
|
+
parts.extend(inline_scripts)
|
|
681
|
+
parts.append('') # Newline before closing tag
|
|
682
|
+
parts.append('</script>')
|
|
683
|
+
|
|
684
|
+
# Join with double newlines for readability
|
|
685
|
+
return '\n\n'.join(parts)
|
|
686
|
+
|
|
687
|
+
@staticmethod
|
|
688
|
+
def _build_explanation_section(explanation: Optional[str]) -> str:
|
|
689
|
+
"""Build a collapsible explanation section to show above the map."""
|
|
690
|
+
if not explanation:
|
|
691
|
+
return ""
|
|
692
|
+
|
|
693
|
+
escaped_explanation = html.escape(str(explanation))
|
|
694
|
+
|
|
695
|
+
return '''
|
|
696
|
+
<style>
|
|
697
|
+
.ap-map-explanation {margin-bottom: 16px;}
|
|
698
|
+
.ap-map-explanation details {border: 1px solid #e0e0e0; border-radius: 6px; overflow: hidden; background: #ffffff;}
|
|
699
|
+
.ap-map-explanation summary {background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); color: #fff; padding: 12px 16px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; font-weight: 600; user-select: none;}
|
|
700
|
+
.ap-map-explanation .ap-toggle-icon {transition: transform 0.3s ease;}
|
|
701
|
+
.ap-map-explanation details[open] .ap-toggle-icon {transform: rotate(90deg);}
|
|
702
|
+
.ap-map-explanation .ap-explanation-content {padding: 12px 16px; background: #f8fafc; color: #1f2937;}
|
|
703
|
+
.ap-map-explanation p {margin: 0; line-height: 1.6;}
|
|
704
|
+
</style>
|
|
705
|
+
<div class="ap-map-explanation">
|
|
706
|
+
<details>
|
|
707
|
+
<summary>
|
|
708
|
+
<span>📝 Explicación del mapa</span>
|
|
709
|
+
<span class="ap-toggle-icon">▶</span>
|
|
710
|
+
</summary>
|
|
711
|
+
<div class="ap-explanation-content">
|
|
712
|
+
<p>{escaped_explanation}</p>
|
|
713
|
+
</div>
|
|
714
|
+
</details>
|
|
715
|
+
</div>
|
|
716
|
+
'''.format(escaped_explanation=escaped_explanation)
|
|
717
|
+
|
|
718
|
+
@staticmethod
|
|
719
|
+
def _is_latitude(value: Any) -> bool:
|
|
720
|
+
return isinstance(value, (int, float)) and -90 <= value <= 90
|
|
721
|
+
|
|
722
|
+
@staticmethod
|
|
723
|
+
def _is_longitude(value: Any) -> bool:
|
|
724
|
+
return isinstance(value, (int, float)) and -180 <= value <= 180
|
|
725
|
+
|
|
726
|
+
def _normalize_location(self, location: Any) -> Tuple[Any, bool]:
|
|
727
|
+
"""Ensure coordinates are in [lat, lon] order and within valid ranges."""
|
|
728
|
+
if not isinstance(location, (list, tuple)) or len(location) < 2:
|
|
729
|
+
return location, False
|
|
730
|
+
|
|
731
|
+
lat, lon = location[0], location[1]
|
|
732
|
+
if not isinstance(lat, (int, float)) or not isinstance(lon, (int, float)):
|
|
733
|
+
return location, False
|
|
734
|
+
|
|
735
|
+
lat_first_valid = self._is_latitude(lat) and self._is_longitude(lon)
|
|
736
|
+
lon_first_valid = self._is_latitude(lon) and self._is_longitude(lat)
|
|
737
|
+
|
|
738
|
+
# Detect clear reversals or polar misplacements
|
|
739
|
+
if not lat_first_valid and lon_first_valid:
|
|
740
|
+
return [lon, lat, *location[2:]], True
|
|
741
|
+
|
|
742
|
+
# Heuristic: if latitude magnitude is extreme while longitude is moderate, swap
|
|
743
|
+
if lat_first_valid and abs(lat) > 75 and abs(lon) < 75 and lon_first_valid:
|
|
744
|
+
return [lon, lat, *location[2:]], True
|
|
745
|
+
|
|
746
|
+
return list(location), False
|
|
747
|
+
|
|
748
|
+
def _prepare_map_coordinates(self, map_obj: Any) -> None:
|
|
749
|
+
"""Normalize marker coordinates and recenter the map."""
|
|
750
|
+
coordinates: List[Tuple[float, float]] = []
|
|
751
|
+
swaps = 0
|
|
752
|
+
|
|
753
|
+
for child in getattr(map_obj, '_children', {}).values():
|
|
754
|
+
location = getattr(child, 'location', None)
|
|
755
|
+
fixed_location, swapped = self._normalize_location(location)
|
|
756
|
+
if swapped:
|
|
757
|
+
setattr(child, 'location', fixed_location)
|
|
758
|
+
swaps += 1
|
|
759
|
+
if isinstance(fixed_location, (list, tuple)) and len(fixed_location) >= 2:
|
|
760
|
+
first, second = fixed_location[0], fixed_location[1]
|
|
761
|
+
if self._is_latitude(first) and self._is_longitude(second):
|
|
762
|
+
coordinates.append((first, second))
|
|
763
|
+
|
|
764
|
+
if coordinates:
|
|
765
|
+
lats = pd.Series([lat for lat, _ in coordinates])
|
|
766
|
+
lons = pd.Series([lon for _, lon in coordinates])
|
|
767
|
+
map_obj.location = [float(lats.median()), float(lons.median())]
|
|
768
|
+
if swaps:
|
|
769
|
+
print(f"Corrected {swaps} marker coordinate pairs to [lat, lon] order.")
|
|
770
|
+
|
|
771
|
+
def to_json(self, chart_obj: Any) -> Optional[Dict]:
|
|
772
|
+
"""Export map metadata as JSON."""
|
|
773
|
+
try:
|
|
774
|
+
return {
|
|
775
|
+
'center': chart_obj.location,
|
|
776
|
+
'zoom': chart_obj.options.get('zoom_start', chart_obj.options.get('zoom', 10)),
|
|
777
|
+
'tiles': chart_obj.tiles if hasattr(chart_obj, 'tiles') else 'OpenStreetMap',
|
|
778
|
+
'type': 'folium_map'
|
|
779
|
+
}
|
|
780
|
+
except Exception as e:
|
|
781
|
+
return {'error': str(e)}
|
|
782
|
+
|
|
783
|
+
async def render(
|
|
784
|
+
self,
|
|
785
|
+
response: Any,
|
|
786
|
+
theme: str = 'monokai',
|
|
787
|
+
environment: str = 'html',
|
|
788
|
+
include_code: bool = False,
|
|
789
|
+
html_mode: str = 'partial',
|
|
790
|
+
**kwargs
|
|
791
|
+
) -> Tuple[Any, Optional[Any]]:
|
|
792
|
+
"""
|
|
793
|
+
Render Folium map.
|
|
794
|
+
|
|
795
|
+
CRITICAL: Always returns (code, html) tuple
|
|
796
|
+
- First return (code): Python code string for response.output
|
|
797
|
+
- Second return (html): HTML content for response.response
|
|
798
|
+
"""
|
|
799
|
+
explanation = getattr(response, 'explanation', None)
|
|
800
|
+
|
|
801
|
+
# 1. Extract Code - Try response.code first, fallback to content extraction
|
|
802
|
+
code = None
|
|
803
|
+
try:
|
|
804
|
+
code = getattr(response, 'code', None)
|
|
805
|
+
except Exception:
|
|
806
|
+
pass
|
|
807
|
+
|
|
808
|
+
# Fallback: extract from content if code is not available
|
|
809
|
+
if not code:
|
|
810
|
+
try:
|
|
811
|
+
content = self._get_content(response)
|
|
812
|
+
code = self._extract_code(content)
|
|
813
|
+
except Exception:
|
|
814
|
+
pass
|
|
815
|
+
|
|
816
|
+
# 2. Extract DataFrame - Try response.data first, then check content
|
|
817
|
+
dataframe = None
|
|
818
|
+
try:
|
|
819
|
+
dataframe = getattr(response, 'data', None)
|
|
820
|
+
if dataframe is not None and not isinstance(dataframe, pd.DataFrame):
|
|
821
|
+
dataframe = None
|
|
822
|
+
except Exception:
|
|
823
|
+
pass
|
|
824
|
+
|
|
825
|
+
# Fallback: check if content is a DataFrame
|
|
826
|
+
if dataframe is None:
|
|
827
|
+
try:
|
|
828
|
+
content = self._get_content(response)
|
|
829
|
+
if isinstance(content, pd.DataFrame):
|
|
830
|
+
dataframe = content
|
|
831
|
+
except Exception:
|
|
832
|
+
pass
|
|
833
|
+
|
|
834
|
+
output_format = kwargs.get('output_format', environment)
|
|
835
|
+
geojson_path = kwargs.get('geojson_path') or kwargs.get('geojson')
|
|
836
|
+
|
|
837
|
+
# --- DATA MODE (DataFrame + GeoJSON) ---
|
|
838
|
+
if GEOPANDAS_AVAILABLE and dataframe is not None and geojson_path:
|
|
839
|
+
try:
|
|
840
|
+
key_on = kwargs.get('key_on', 'feature.properties.name')
|
|
841
|
+
join_column = kwargs.get('join_column', dataframe.columns[0])
|
|
842
|
+
value_column = kwargs.get('value_column', dataframe.columns[1])
|
|
843
|
+
|
|
844
|
+
map_obj = self._create_choropleth_map(
|
|
845
|
+
data=dataframe,
|
|
846
|
+
geojson_path=geojson_path,
|
|
847
|
+
key_on=key_on,
|
|
848
|
+
columns=(join_column, value_column),
|
|
849
|
+
**kwargs
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
# Use to_html (which now uses super().to_html)
|
|
853
|
+
html_output = self.to_html(
|
|
854
|
+
map_obj,
|
|
855
|
+
mode=html_mode,
|
|
856
|
+
include_code=False,
|
|
857
|
+
title=kwargs.get('title', 'Choropleth Map'),
|
|
858
|
+
explanation=explanation,
|
|
859
|
+
**kwargs
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
# CRITICAL: Always return (code_string, html)
|
|
863
|
+
data_info = f"# Choropleth map with {len(dataframe)} regions"
|
|
864
|
+
return data_info, html_output
|
|
865
|
+
|
|
866
|
+
except Exception as e:
|
|
867
|
+
error_msg = f"Error creating choropleth: {str(e)}"
|
|
868
|
+
error_html = self._render_error(error_msg, code or "", theme)
|
|
869
|
+
# CRITICAL: Return code first, then error HTML
|
|
870
|
+
return code or f"# {error_msg}", error_html
|
|
871
|
+
|
|
872
|
+
# --- CODE MODE ---
|
|
873
|
+
if not code:
|
|
874
|
+
error_msg = "No map code found in response"
|
|
875
|
+
error_html = f"<div class='error'>{error_msg}</div>"
|
|
876
|
+
# CRITICAL: Return error message as code, error HTML as second value
|
|
877
|
+
return f"# {error_msg}", error_html
|
|
878
|
+
|
|
879
|
+
# Validate code completeness - check if it actually creates a map
|
|
880
|
+
if 'folium.Map' not in code and 'folium_map' not in code and 'm = ' not in code and 'map = ' not in code:
|
|
881
|
+
warning_msg = "Warning: Code appears incomplete - no map creation detected"
|
|
882
|
+
print(f"⚠️ {warning_msg}")
|
|
883
|
+
print(f"CODE PREVIEW: {code[:200]}...")
|
|
884
|
+
# Continue execution anyway - maybe the map is created differently
|
|
885
|
+
|
|
886
|
+
# Execute code
|
|
887
|
+
result_obj, error = self.execute_code(
|
|
888
|
+
code,
|
|
889
|
+
pandas_tool=kwargs.pop('pandas_tool', None),
|
|
890
|
+
execution_state=kwargs.pop('execution_state', None),
|
|
891
|
+
**kwargs,
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
if error:
|
|
895
|
+
error_html = self._render_error(error, code, theme)
|
|
896
|
+
# CRITICAL: Always return original code first, error HTML second
|
|
897
|
+
return code, error_html
|
|
898
|
+
|
|
899
|
+
# Handle if result is a DataFrame (data mode without GeoJSON)
|
|
900
|
+
if isinstance(result_obj, pd.DataFrame):
|
|
901
|
+
# Return code and DataFrame info
|
|
902
|
+
df_info = f"<div>DataFrame with {len(result_obj)} rows and {len(result_obj.columns)} columns</div>"
|
|
903
|
+
return code, df_info
|
|
904
|
+
|
|
905
|
+
# Result is a Folium map object
|
|
906
|
+
map_obj = result_obj
|
|
907
|
+
|
|
908
|
+
# Normalize coordinates and center based on available markers
|
|
909
|
+
self._prepare_map_coordinates(map_obj)
|
|
910
|
+
|
|
911
|
+
# Handle Jupyter/Notebook Environment
|
|
912
|
+
if output_format in {'jupyter', 'notebook', 'ipython', 'colab'}:
|
|
913
|
+
# For Jupyter, return code and map object
|
|
914
|
+
return code, map_obj
|
|
915
|
+
|
|
916
|
+
# Generate HTML for Web/Terminal
|
|
917
|
+
html_output = self.to_html(
|
|
918
|
+
map_obj,
|
|
919
|
+
mode=html_mode,
|
|
920
|
+
include_code=include_code,
|
|
921
|
+
code=code,
|
|
922
|
+
theme=theme,
|
|
923
|
+
title=kwargs.get('title', 'Folium Map'),
|
|
924
|
+
explanation=explanation,
|
|
925
|
+
**kwargs
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
# Return based on output format
|
|
929
|
+
if output_format == 'json':
|
|
930
|
+
return code, self.to_json(map_obj)
|
|
931
|
+
|
|
932
|
+
# Default: Always return (code_string, html_string)
|
|
933
|
+
return code, html_output
|