ai-parrot 0.17.2__cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentui/.prettierrc +15 -0
- agentui/QUICKSTART.md +272 -0
- agentui/README.md +59 -0
- agentui/env.example +16 -0
- agentui/jsconfig.json +14 -0
- agentui/package-lock.json +4242 -0
- agentui/package.json +34 -0
- agentui/scripts/postinstall/apply-patches.mjs +260 -0
- agentui/src/app.css +61 -0
- agentui/src/app.d.ts +13 -0
- agentui/src/app.html +12 -0
- agentui/src/components/LoadingSpinner.svelte +64 -0
- agentui/src/components/ThemeSwitcher.svelte +159 -0
- agentui/src/components/index.js +4 -0
- agentui/src/lib/api/bots.ts +60 -0
- agentui/src/lib/api/chat.ts +22 -0
- agentui/src/lib/api/http.ts +25 -0
- agentui/src/lib/components/BotCard.svelte +33 -0
- agentui/src/lib/components/ChatBubble.svelte +63 -0
- agentui/src/lib/components/Toast.svelte +21 -0
- agentui/src/lib/config.ts +20 -0
- agentui/src/lib/stores/auth.svelte.ts +73 -0
- agentui/src/lib/stores/theme.svelte.js +64 -0
- agentui/src/lib/stores/toast.svelte.ts +31 -0
- agentui/src/lib/utils/conversation.ts +39 -0
- agentui/src/routes/+layout.svelte +20 -0
- agentui/src/routes/+page.svelte +232 -0
- agentui/src/routes/login/+page.svelte +200 -0
- agentui/src/routes/talk/[agentId]/+page.svelte +297 -0
- agentui/src/routes/talk/[agentId]/+page.ts +7 -0
- agentui/static/README.md +1 -0
- agentui/svelte.config.js +11 -0
- agentui/tailwind.config.ts +53 -0
- agentui/tsconfig.json +3 -0
- agentui/vite.config.ts +10 -0
- ai_parrot-0.17.2.dist-info/METADATA +472 -0
- ai_parrot-0.17.2.dist-info/RECORD +535 -0
- ai_parrot-0.17.2.dist-info/WHEEL +6 -0
- ai_parrot-0.17.2.dist-info/entry_points.txt +2 -0
- ai_parrot-0.17.2.dist-info/licenses/LICENSE +21 -0
- ai_parrot-0.17.2.dist-info/top_level.txt +6 -0
- crew-builder/.prettierrc +15 -0
- crew-builder/QUICKSTART.md +259 -0
- crew-builder/README.md +113 -0
- crew-builder/env.example +17 -0
- crew-builder/jsconfig.json +14 -0
- crew-builder/package-lock.json +4182 -0
- crew-builder/package.json +37 -0
- crew-builder/scripts/postinstall/apply-patches.mjs +260 -0
- crew-builder/src/app.css +62 -0
- crew-builder/src/app.d.ts +13 -0
- crew-builder/src/app.html +12 -0
- crew-builder/src/components/LoadingSpinner.svelte +64 -0
- crew-builder/src/components/ThemeSwitcher.svelte +149 -0
- crew-builder/src/components/index.js +9 -0
- crew-builder/src/lib/api/bots.ts +60 -0
- crew-builder/src/lib/api/chat.ts +80 -0
- crew-builder/src/lib/api/client.ts +56 -0
- crew-builder/src/lib/api/crew/crew.ts +136 -0
- crew-builder/src/lib/api/index.ts +5 -0
- crew-builder/src/lib/api/o365/auth.ts +65 -0
- crew-builder/src/lib/auth/auth.ts +54 -0
- crew-builder/src/lib/components/AgentNode.svelte +43 -0
- crew-builder/src/lib/components/BotCard.svelte +33 -0
- crew-builder/src/lib/components/ChatBubble.svelte +67 -0
- crew-builder/src/lib/components/ConfigPanel.svelte +278 -0
- crew-builder/src/lib/components/JsonTreeNode.svelte +76 -0
- crew-builder/src/lib/components/JsonViewer.svelte +24 -0
- crew-builder/src/lib/components/MarkdownEditor.svelte +48 -0
- crew-builder/src/lib/components/ThemeToggle.svelte +36 -0
- crew-builder/src/lib/components/Toast.svelte +67 -0
- crew-builder/src/lib/components/Toolbar.svelte +157 -0
- crew-builder/src/lib/components/index.ts +10 -0
- crew-builder/src/lib/config.ts +8 -0
- crew-builder/src/lib/stores/auth.svelte.ts +228 -0
- crew-builder/src/lib/stores/crewStore.ts +369 -0
- crew-builder/src/lib/stores/theme.svelte.js +145 -0
- crew-builder/src/lib/stores/toast.svelte.ts +69 -0
- crew-builder/src/lib/utils/conversation.ts +39 -0
- crew-builder/src/lib/utils/markdown.ts +122 -0
- crew-builder/src/lib/utils/talkHistory.ts +47 -0
- crew-builder/src/routes/+layout.svelte +20 -0
- crew-builder/src/routes/+page.svelte +539 -0
- crew-builder/src/routes/agents/+page.svelte +247 -0
- crew-builder/src/routes/agents/[agentId]/+page.svelte +288 -0
- crew-builder/src/routes/agents/[agentId]/+page.ts +7 -0
- crew-builder/src/routes/builder/+page.svelte +204 -0
- crew-builder/src/routes/crew/ask/+page.svelte +1052 -0
- crew-builder/src/routes/crew/ask/+page.ts +1 -0
- crew-builder/src/routes/integrations/o365/+page.svelte +304 -0
- crew-builder/src/routes/login/+page.svelte +197 -0
- crew-builder/src/routes/talk/[agentId]/+page.svelte +487 -0
- crew-builder/src/routes/talk/[agentId]/+page.ts +7 -0
- crew-builder/static/README.md +1 -0
- crew-builder/svelte.config.js +11 -0
- crew-builder/tailwind.config.ts +53 -0
- crew-builder/tsconfig.json +3 -0
- crew-builder/vite.config.ts +10 -0
- mcp_servers/calculator_server.py +309 -0
- parrot/__init__.py +27 -0
- parrot/__pycache__/__init__.cpython-310.pyc +0 -0
- parrot/__pycache__/version.cpython-310.pyc +0 -0
- parrot/_version.py +34 -0
- parrot/a2a/__init__.py +48 -0
- parrot/a2a/client.py +658 -0
- parrot/a2a/discovery.py +89 -0
- parrot/a2a/mixin.py +257 -0
- parrot/a2a/models.py +376 -0
- parrot/a2a/server.py +770 -0
- parrot/agents/__init__.py +29 -0
- parrot/bots/__init__.py +12 -0
- parrot/bots/a2a_agent.py +19 -0
- parrot/bots/abstract.py +3139 -0
- parrot/bots/agent.py +1129 -0
- parrot/bots/basic.py +9 -0
- parrot/bots/chatbot.py +669 -0
- parrot/bots/data.py +1618 -0
- parrot/bots/database/__init__.py +5 -0
- parrot/bots/database/abstract.py +3071 -0
- parrot/bots/database/cache.py +286 -0
- parrot/bots/database/models.py +468 -0
- parrot/bots/database/prompts.py +154 -0
- parrot/bots/database/retries.py +98 -0
- parrot/bots/database/router.py +269 -0
- parrot/bots/database/sql.py +41 -0
- parrot/bots/db/__init__.py +6 -0
- parrot/bots/db/abstract.py +556 -0
- parrot/bots/db/bigquery.py +602 -0
- parrot/bots/db/cache.py +85 -0
- parrot/bots/db/documentdb.py +668 -0
- parrot/bots/db/elastic.py +1014 -0
- parrot/bots/db/influx.py +898 -0
- parrot/bots/db/mock.py +96 -0
- parrot/bots/db/multi.py +783 -0
- parrot/bots/db/prompts.py +185 -0
- parrot/bots/db/sql.py +1255 -0
- parrot/bots/db/tools.py +212 -0
- parrot/bots/document.py +680 -0
- parrot/bots/hrbot.py +15 -0
- parrot/bots/kb.py +170 -0
- parrot/bots/mcp.py +36 -0
- parrot/bots/orchestration/README.md +463 -0
- parrot/bots/orchestration/__init__.py +1 -0
- parrot/bots/orchestration/agent.py +155 -0
- parrot/bots/orchestration/crew.py +3330 -0
- parrot/bots/orchestration/fsm.py +1179 -0
- parrot/bots/orchestration/hr.py +434 -0
- parrot/bots/orchestration/storage/__init__.py +4 -0
- parrot/bots/orchestration/storage/memory.py +100 -0
- parrot/bots/orchestration/storage/mixin.py +119 -0
- parrot/bots/orchestration/verify.py +202 -0
- parrot/bots/product.py +204 -0
- parrot/bots/prompts/__init__.py +96 -0
- parrot/bots/prompts/agents.py +155 -0
- parrot/bots/prompts/data.py +216 -0
- parrot/bots/prompts/output_generation.py +8 -0
- parrot/bots/scraper/__init__.py +3 -0
- parrot/bots/scraper/models.py +122 -0
- parrot/bots/scraper/scraper.py +1173 -0
- parrot/bots/scraper/templates.py +115 -0
- parrot/bots/stores/__init__.py +5 -0
- parrot/bots/stores/local.py +172 -0
- parrot/bots/webdev.py +81 -0
- parrot/cli.py +17 -0
- parrot/clients/__init__.py +16 -0
- parrot/clients/base.py +1491 -0
- parrot/clients/claude.py +1191 -0
- parrot/clients/factory.py +129 -0
- parrot/clients/google.py +4567 -0
- parrot/clients/gpt.py +1975 -0
- parrot/clients/grok.py +432 -0
- parrot/clients/groq.py +986 -0
- parrot/clients/hf.py +582 -0
- parrot/clients/models.py +18 -0
- parrot/conf.py +395 -0
- parrot/embeddings/__init__.py +9 -0
- parrot/embeddings/base.py +157 -0
- parrot/embeddings/google.py +98 -0
- parrot/embeddings/huggingface.py +74 -0
- parrot/embeddings/openai.py +84 -0
- parrot/embeddings/processor.py +88 -0
- parrot/exceptions.c +13868 -0
- parrot/exceptions.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/exceptions.pxd +22 -0
- parrot/exceptions.pxi +15 -0
- parrot/exceptions.pyx +44 -0
- parrot/generators/__init__.py +29 -0
- parrot/generators/base.py +200 -0
- parrot/generators/html.py +293 -0
- parrot/generators/react.py +205 -0
- parrot/generators/streamlit.py +203 -0
- parrot/generators/template.py +105 -0
- parrot/handlers/__init__.py +4 -0
- parrot/handlers/agent.py +861 -0
- parrot/handlers/agents/__init__.py +1 -0
- parrot/handlers/agents/abstract.py +900 -0
- parrot/handlers/bots.py +338 -0
- parrot/handlers/chat.py +915 -0
- parrot/handlers/creation.sql +192 -0
- parrot/handlers/crew/ARCHITECTURE.md +362 -0
- parrot/handlers/crew/README_BOTMANAGER_PERSISTENCE.md +303 -0
- parrot/handlers/crew/README_REDIS_PERSISTENCE.md +366 -0
- parrot/handlers/crew/__init__.py +0 -0
- parrot/handlers/crew/handler.py +801 -0
- parrot/handlers/crew/models.py +229 -0
- parrot/handlers/crew/redis_persistence.py +523 -0
- parrot/handlers/jobs/__init__.py +10 -0
- parrot/handlers/jobs/job.py +384 -0
- parrot/handlers/jobs/mixin.py +627 -0
- parrot/handlers/jobs/models.py +115 -0
- parrot/handlers/jobs/worker.py +31 -0
- parrot/handlers/models.py +596 -0
- parrot/handlers/o365_auth.py +105 -0
- parrot/handlers/stream.py +337 -0
- parrot/interfaces/__init__.py +6 -0
- parrot/interfaces/aws.py +143 -0
- parrot/interfaces/credentials.py +113 -0
- parrot/interfaces/database.py +27 -0
- parrot/interfaces/google.py +1123 -0
- parrot/interfaces/hierarchy.py +1227 -0
- parrot/interfaces/http.py +651 -0
- parrot/interfaces/images/__init__.py +0 -0
- parrot/interfaces/images/plugins/__init__.py +24 -0
- parrot/interfaces/images/plugins/abstract.py +58 -0
- parrot/interfaces/images/plugins/analisys.py +148 -0
- parrot/interfaces/images/plugins/classify.py +150 -0
- parrot/interfaces/images/plugins/classifybase.py +182 -0
- parrot/interfaces/images/plugins/detect.py +150 -0
- parrot/interfaces/images/plugins/exif.py +1103 -0
- parrot/interfaces/images/plugins/hash.py +52 -0
- parrot/interfaces/images/plugins/vision.py +104 -0
- parrot/interfaces/images/plugins/yolo.py +66 -0
- parrot/interfaces/images/plugins/zerodetect.py +197 -0
- parrot/interfaces/o365.py +978 -0
- parrot/interfaces/onedrive.py +822 -0
- parrot/interfaces/sharepoint.py +1435 -0
- parrot/interfaces/soap.py +257 -0
- parrot/loaders/__init__.py +8 -0
- parrot/loaders/abstract.py +1131 -0
- parrot/loaders/audio.py +199 -0
- parrot/loaders/basepdf.py +53 -0
- parrot/loaders/basevideo.py +1568 -0
- parrot/loaders/csv.py +409 -0
- parrot/loaders/docx.py +116 -0
- parrot/loaders/epubloader.py +316 -0
- parrot/loaders/excel.py +199 -0
- parrot/loaders/factory.py +55 -0
- parrot/loaders/files/__init__.py +0 -0
- parrot/loaders/files/abstract.py +39 -0
- parrot/loaders/files/html.py +26 -0
- parrot/loaders/files/text.py +63 -0
- parrot/loaders/html.py +152 -0
- parrot/loaders/markdown.py +442 -0
- parrot/loaders/pdf.py +373 -0
- parrot/loaders/pdfmark.py +320 -0
- parrot/loaders/pdftables.py +506 -0
- parrot/loaders/ppt.py +476 -0
- parrot/loaders/qa.py +63 -0
- parrot/loaders/splitters/__init__.py +10 -0
- parrot/loaders/splitters/base.py +138 -0
- parrot/loaders/splitters/md.py +228 -0
- parrot/loaders/splitters/token.py +143 -0
- parrot/loaders/txt.py +26 -0
- parrot/loaders/video.py +89 -0
- parrot/loaders/videolocal.py +218 -0
- parrot/loaders/videounderstanding.py +377 -0
- parrot/loaders/vimeo.py +167 -0
- parrot/loaders/web.py +599 -0
- parrot/loaders/youtube.py +504 -0
- parrot/manager/__init__.py +5 -0
- parrot/manager/manager.py +1030 -0
- parrot/mcp/__init__.py +28 -0
- parrot/mcp/adapter.py +105 -0
- parrot/mcp/cli.py +174 -0
- parrot/mcp/client.py +119 -0
- parrot/mcp/config.py +75 -0
- parrot/mcp/integration.py +842 -0
- parrot/mcp/oauth.py +933 -0
- parrot/mcp/server.py +225 -0
- parrot/mcp/transports/__init__.py +3 -0
- parrot/mcp/transports/base.py +279 -0
- parrot/mcp/transports/grpc_session.py +163 -0
- parrot/mcp/transports/http.py +312 -0
- parrot/mcp/transports/mcp.proto +108 -0
- parrot/mcp/transports/quic.py +1082 -0
- parrot/mcp/transports/sse.py +330 -0
- parrot/mcp/transports/stdio.py +309 -0
- parrot/mcp/transports/unix.py +395 -0
- parrot/mcp/transports/websocket.py +547 -0
- parrot/memory/__init__.py +16 -0
- parrot/memory/abstract.py +209 -0
- parrot/memory/agent.py +32 -0
- parrot/memory/cache.py +175 -0
- parrot/memory/core.py +555 -0
- parrot/memory/file.py +153 -0
- parrot/memory/mem.py +131 -0
- parrot/memory/redis.py +613 -0
- parrot/models/__init__.py +46 -0
- parrot/models/basic.py +118 -0
- parrot/models/compliance.py +208 -0
- parrot/models/crew.py +395 -0
- parrot/models/detections.py +654 -0
- parrot/models/generation.py +85 -0
- parrot/models/google.py +223 -0
- parrot/models/groq.py +23 -0
- parrot/models/openai.py +30 -0
- parrot/models/outputs.py +285 -0
- parrot/models/responses.py +938 -0
- parrot/notifications/__init__.py +743 -0
- parrot/openapi/__init__.py +3 -0
- parrot/openapi/components.yaml +641 -0
- parrot/openapi/config.py +322 -0
- parrot/outputs/__init__.py +32 -0
- parrot/outputs/formats/__init__.py +108 -0
- parrot/outputs/formats/altair.py +359 -0
- parrot/outputs/formats/application.py +122 -0
- parrot/outputs/formats/base.py +351 -0
- parrot/outputs/formats/bokeh.py +356 -0
- parrot/outputs/formats/card.py +424 -0
- parrot/outputs/formats/chart.py +436 -0
- parrot/outputs/formats/d3.py +255 -0
- parrot/outputs/formats/echarts.py +310 -0
- parrot/outputs/formats/generators/__init__.py +0 -0
- parrot/outputs/formats/generators/abstract.py +61 -0
- parrot/outputs/formats/generators/panel.py +145 -0
- parrot/outputs/formats/generators/streamlit.py +86 -0
- parrot/outputs/formats/generators/terminal.py +63 -0
- parrot/outputs/formats/holoviews.py +310 -0
- parrot/outputs/formats/html.py +147 -0
- parrot/outputs/formats/jinja2.py +46 -0
- parrot/outputs/formats/json.py +87 -0
- parrot/outputs/formats/map.py +933 -0
- parrot/outputs/formats/markdown.py +172 -0
- parrot/outputs/formats/matplotlib.py +237 -0
- parrot/outputs/formats/mixins/__init__.py +0 -0
- parrot/outputs/formats/mixins/emaps.py +855 -0
- parrot/outputs/formats/plotly.py +341 -0
- parrot/outputs/formats/seaborn.py +310 -0
- parrot/outputs/formats/table.py +397 -0
- parrot/outputs/formats/template_report.py +138 -0
- parrot/outputs/formats/yaml.py +125 -0
- parrot/outputs/formatter.py +152 -0
- parrot/outputs/templates/__init__.py +95 -0
- parrot/pipelines/__init__.py +0 -0
- parrot/pipelines/abstract.py +210 -0
- parrot/pipelines/detector.py +124 -0
- parrot/pipelines/models.py +90 -0
- parrot/pipelines/planogram.py +3002 -0
- parrot/pipelines/table.sql +97 -0
- parrot/plugins/__init__.py +106 -0
- parrot/plugins/importer.py +80 -0
- parrot/py.typed +0 -0
- parrot/registry/__init__.py +18 -0
- parrot/registry/registry.py +594 -0
- parrot/scheduler/__init__.py +1189 -0
- parrot/scheduler/models.py +60 -0
- parrot/security/__init__.py +16 -0
- parrot/security/prompt_injection.py +268 -0
- parrot/security/security_events.sql +25 -0
- parrot/services/__init__.py +1 -0
- parrot/services/mcp/__init__.py +8 -0
- parrot/services/mcp/config.py +13 -0
- parrot/services/mcp/server.py +295 -0
- parrot/services/o365_remote_auth.py +235 -0
- parrot/stores/__init__.py +7 -0
- parrot/stores/abstract.py +352 -0
- parrot/stores/arango.py +1090 -0
- parrot/stores/bigquery.py +1377 -0
- parrot/stores/cache.py +106 -0
- parrot/stores/empty.py +10 -0
- parrot/stores/faiss_store.py +1157 -0
- parrot/stores/kb/__init__.py +9 -0
- parrot/stores/kb/abstract.py +68 -0
- parrot/stores/kb/cache.py +165 -0
- parrot/stores/kb/doc.py +325 -0
- parrot/stores/kb/hierarchy.py +346 -0
- parrot/stores/kb/local.py +457 -0
- parrot/stores/kb/prompt.py +28 -0
- parrot/stores/kb/redis.py +659 -0
- parrot/stores/kb/store.py +115 -0
- parrot/stores/kb/user.py +374 -0
- parrot/stores/models.py +59 -0
- parrot/stores/pgvector.py +3 -0
- parrot/stores/postgres.py +2853 -0
- parrot/stores/utils/__init__.py +0 -0
- parrot/stores/utils/chunking.py +197 -0
- parrot/telemetry/__init__.py +3 -0
- parrot/telemetry/mixin.py +111 -0
- parrot/template/__init__.py +3 -0
- parrot/template/engine.py +259 -0
- parrot/tools/__init__.py +23 -0
- parrot/tools/abstract.py +644 -0
- parrot/tools/agent.py +363 -0
- parrot/tools/arangodbsearch.py +537 -0
- parrot/tools/arxiv_tool.py +188 -0
- parrot/tools/calculator/__init__.py +3 -0
- parrot/tools/calculator/operations/__init__.py +38 -0
- parrot/tools/calculator/operations/calculus.py +80 -0
- parrot/tools/calculator/operations/statistics.py +76 -0
- parrot/tools/calculator/tool.py +150 -0
- parrot/tools/cloudwatch.py +988 -0
- parrot/tools/codeinterpreter/__init__.py +127 -0
- parrot/tools/codeinterpreter/executor.py +371 -0
- parrot/tools/codeinterpreter/internals.py +473 -0
- parrot/tools/codeinterpreter/models.py +643 -0
- parrot/tools/codeinterpreter/prompts.py +224 -0
- parrot/tools/codeinterpreter/tool.py +664 -0
- parrot/tools/company_info/__init__.py +6 -0
- parrot/tools/company_info/tool.py +1138 -0
- parrot/tools/correlationanalysis.py +437 -0
- parrot/tools/database/abstract.py +286 -0
- parrot/tools/database/bq.py +115 -0
- parrot/tools/database/cache.py +284 -0
- parrot/tools/database/models.py +95 -0
- parrot/tools/database/pg.py +343 -0
- parrot/tools/databasequery.py +1159 -0
- parrot/tools/db.py +1800 -0
- parrot/tools/ddgo.py +370 -0
- parrot/tools/decorators.py +271 -0
- parrot/tools/dftohtml.py +282 -0
- parrot/tools/document.py +549 -0
- parrot/tools/ecs.py +819 -0
- parrot/tools/edareport.py +368 -0
- parrot/tools/elasticsearch.py +1049 -0
- parrot/tools/employees.py +462 -0
- parrot/tools/epson/__init__.py +96 -0
- parrot/tools/excel.py +683 -0
- parrot/tools/file/__init__.py +13 -0
- parrot/tools/file/abstract.py +76 -0
- parrot/tools/file/gcs.py +378 -0
- parrot/tools/file/local.py +284 -0
- parrot/tools/file/s3.py +511 -0
- parrot/tools/file/tmp.py +309 -0
- parrot/tools/file/tool.py +501 -0
- parrot/tools/file_reader.py +129 -0
- parrot/tools/flowtask/__init__.py +19 -0
- parrot/tools/flowtask/tool.py +761 -0
- parrot/tools/gittoolkit.py +508 -0
- parrot/tools/google/__init__.py +18 -0
- parrot/tools/google/base.py +169 -0
- parrot/tools/google/tools.py +1251 -0
- parrot/tools/googlelocation.py +5 -0
- parrot/tools/googleroutes.py +5 -0
- parrot/tools/googlesearch.py +5 -0
- parrot/tools/googlesitesearch.py +5 -0
- parrot/tools/googlevoice.py +2 -0
- parrot/tools/gvoice.py +695 -0
- parrot/tools/ibisworld/README.md +225 -0
- parrot/tools/ibisworld/__init__.py +11 -0
- parrot/tools/ibisworld/tool.py +366 -0
- parrot/tools/jiratoolkit.py +1718 -0
- parrot/tools/manager.py +1098 -0
- parrot/tools/math.py +152 -0
- parrot/tools/metadata.py +476 -0
- parrot/tools/msteams.py +1621 -0
- parrot/tools/msword.py +635 -0
- parrot/tools/multidb.py +580 -0
- parrot/tools/multistoresearch.py +369 -0
- parrot/tools/networkninja.py +167 -0
- parrot/tools/nextstop/__init__.py +4 -0
- parrot/tools/nextstop/base.py +286 -0
- parrot/tools/nextstop/employee.py +733 -0
- parrot/tools/nextstop/store.py +462 -0
- parrot/tools/notification.py +435 -0
- parrot/tools/o365/__init__.py +42 -0
- parrot/tools/o365/base.py +295 -0
- parrot/tools/o365/bundle.py +522 -0
- parrot/tools/o365/events.py +554 -0
- parrot/tools/o365/mail.py +992 -0
- parrot/tools/o365/onedrive.py +497 -0
- parrot/tools/o365/sharepoint.py +641 -0
- parrot/tools/openapi_toolkit.py +904 -0
- parrot/tools/openweather.py +527 -0
- parrot/tools/pdfprint.py +1001 -0
- parrot/tools/powerbi.py +518 -0
- parrot/tools/powerpoint.py +1113 -0
- parrot/tools/pricestool.py +146 -0
- parrot/tools/products/__init__.py +246 -0
- parrot/tools/prophet_tool.py +171 -0
- parrot/tools/pythonpandas.py +630 -0
- parrot/tools/pythonrepl.py +910 -0
- parrot/tools/qsource.py +436 -0
- parrot/tools/querytoolkit.py +395 -0
- parrot/tools/quickeda.py +827 -0
- parrot/tools/resttool.py +553 -0
- parrot/tools/retail/__init__.py +0 -0
- parrot/tools/retail/bby.py +528 -0
- parrot/tools/sandboxtool.py +703 -0
- parrot/tools/sassie/__init__.py +352 -0
- parrot/tools/scraping/__init__.py +7 -0
- parrot/tools/scraping/docs/select.md +466 -0
- parrot/tools/scraping/documentation.md +1278 -0
- parrot/tools/scraping/driver.py +436 -0
- parrot/tools/scraping/models.py +576 -0
- parrot/tools/scraping/options.py +85 -0
- parrot/tools/scraping/orchestrator.py +517 -0
- parrot/tools/scraping/readme.md +740 -0
- parrot/tools/scraping/tool.py +3115 -0
- parrot/tools/seasonaldetection.py +642 -0
- parrot/tools/shell_tool/__init__.py +5 -0
- parrot/tools/shell_tool/actions.py +408 -0
- parrot/tools/shell_tool/engine.py +155 -0
- parrot/tools/shell_tool/models.py +322 -0
- parrot/tools/shell_tool/tool.py +442 -0
- parrot/tools/site_search.py +214 -0
- parrot/tools/textfile.py +418 -0
- parrot/tools/think.py +378 -0
- parrot/tools/toolkit.py +298 -0
- parrot/tools/webapp_tool.py +187 -0
- parrot/tools/whatif.py +1279 -0
- parrot/tools/workday/MULTI_WSDL_EXAMPLE.md +249 -0
- parrot/tools/workday/__init__.py +6 -0
- parrot/tools/workday/models.py +1389 -0
- parrot/tools/workday/tool.py +1293 -0
- parrot/tools/yfinance_tool.py +306 -0
- parrot/tools/zipcode.py +217 -0
- parrot/utils/__init__.py +2 -0
- parrot/utils/helpers.py +73 -0
- parrot/utils/parsers/__init__.py +5 -0
- parrot/utils/parsers/toml.c +12078 -0
- parrot/utils/parsers/toml.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/utils/parsers/toml.pyx +21 -0
- parrot/utils/toml.py +11 -0
- parrot/utils/types.cpp +20936 -0
- parrot/utils/types.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/utils/types.pyx +213 -0
- parrot/utils/uv.py +11 -0
- parrot/version.py +10 -0
- parrot/yaml-rs/Cargo.lock +350 -0
- parrot/yaml-rs/Cargo.toml +19 -0
- parrot/yaml-rs/pyproject.toml +19 -0
- parrot/yaml-rs/python/yaml_rs/__init__.py +81 -0
- parrot/yaml-rs/src/lib.rs +222 -0
- requirements/docker-compose.yml +24 -0
- requirements/requirements-dev.txt +21 -0
parrot/tools/whatif.py
ADDED
|
@@ -0,0 +1,1279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
What-If Scenario Analysis Tool for AI-Parrot
|
|
3
|
+
Supports derived metrics, constraints, and optimization
|
|
4
|
+
"""
|
|
5
|
+
from typing import Dict, List, Optional, Tuple, Type
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import numpy as np
|
|
11
|
+
import traceback
|
|
12
|
+
from .abstract import AbstractTool, ToolResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ===== Enums =====
|
|
16
|
+
|
|
17
|
+
class ObjectiveType(Enum):
|
|
18
|
+
"""Type of optimization objective"""
|
|
19
|
+
MINIMIZE = "minimize"
|
|
20
|
+
MAXIMIZE = "maximize"
|
|
21
|
+
TARGET = "target"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConstraintType(Enum):
|
|
25
|
+
"""Type of constraint"""
|
|
26
|
+
MAX_CHANGE = "max_change" # Don't change more than X%
|
|
27
|
+
MIN_VALUE = "min_value" # Keep above X
|
|
28
|
+
MAX_VALUE = "max_value" # Keep below X
|
|
29
|
+
RATIO = "ratio" # Keep ratio between metrics
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ===== Core Data Classes =====
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Objective:
|
|
36
|
+
"""Defines an optimization objective"""
|
|
37
|
+
metric: str
|
|
38
|
+
type: ObjectiveType
|
|
39
|
+
target_value: Optional[float] = None
|
|
40
|
+
weight: float = 1.0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class Constraint:
|
|
45
|
+
"""Defines a constraint"""
|
|
46
|
+
metric: str
|
|
47
|
+
type: ConstraintType
|
|
48
|
+
value: float
|
|
49
|
+
reference_metric: Optional[str] = None # For ratio constraints
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Action:
|
|
54
|
+
"""Defines a possible action"""
|
|
55
|
+
name: str
|
|
56
|
+
column: str
|
|
57
|
+
operation: str # 'exclude', 'scale', 'set', 'scale_proportional'
|
|
58
|
+
value: any
|
|
59
|
+
cost: float = 0.0
|
|
60
|
+
affects_derived: bool = False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class ScenarioResult:
|
|
65
|
+
"""Result of an optimized scenario"""
|
|
66
|
+
scenario_name: str
|
|
67
|
+
base_df: pd.DataFrame
|
|
68
|
+
result_df: pd.DataFrame
|
|
69
|
+
actions: List[Action]
|
|
70
|
+
optimizer: 'ScenarioOptimizer'
|
|
71
|
+
calculator: 'MetricsCalculator'
|
|
72
|
+
|
|
73
|
+
def compare(self) -> Dict:
|
|
74
|
+
"""Compare scenario with baseline"""
|
|
75
|
+
metrics = self.optimizer.evaluate_scenario(self.result_df)
|
|
76
|
+
|
|
77
|
+
comparison = {
|
|
78
|
+
'scenario_name': self.scenario_name,
|
|
79
|
+
'actions_taken': [
|
|
80
|
+
{
|
|
81
|
+
'action': action.name,
|
|
82
|
+
'description': f"{action.operation} {action.column} = {action.value}"
|
|
83
|
+
}
|
|
84
|
+
for action in self.actions
|
|
85
|
+
],
|
|
86
|
+
'metrics': metrics,
|
|
87
|
+
'summary': {
|
|
88
|
+
'total_actions': len(self.actions),
|
|
89
|
+
'total_cost': sum(a.cost for a in self.actions)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return comparison
|
|
94
|
+
|
|
95
|
+
def visualize(self) -> str:
|
|
96
|
+
"""Generate visual summary of the scenario"""
|
|
97
|
+
comparison = self.compare()
|
|
98
|
+
|
|
99
|
+
output = [f"\n{'='*70}"]
|
|
100
|
+
output.append(f"Scenario: {self.scenario_name}")
|
|
101
|
+
output.append(f"{'='*70}\n")
|
|
102
|
+
|
|
103
|
+
output.append("Actions Taken:")
|
|
104
|
+
if self.actions:
|
|
105
|
+
for i, action_info in enumerate(comparison['actions_taken'], 1):
|
|
106
|
+
output.append(f" {i}. {action_info['description']}")
|
|
107
|
+
else:
|
|
108
|
+
output.append(" No actions needed - current state meets objectives")
|
|
109
|
+
|
|
110
|
+
output.append("\nMetric Changes:")
|
|
111
|
+
output.append(f"{'Metric':<20} {'Baseline':>15} {'Scenario':>15} {'Change':>15} {'% Change':>12}")
|
|
112
|
+
output.append("-" * 80)
|
|
113
|
+
|
|
114
|
+
for metric, data in comparison['metrics'].items():
|
|
115
|
+
base_value = data['value'] - data['change']
|
|
116
|
+
output.append(
|
|
117
|
+
f"{metric:<20} {base_value:>15.2f} "
|
|
118
|
+
f"{data['value']:>15.2f} {data['change']:>15.2f} "
|
|
119
|
+
f"{data['pct_change']:>11.2f}%"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Add derived metrics info if any
|
|
123
|
+
derived_metrics = [
|
|
124
|
+
m for m in comparison['metrics'].keys() if m in self.calculator.formulas
|
|
125
|
+
]
|
|
126
|
+
if derived_metrics:
|
|
127
|
+
output.append(f"\nDerived Metrics: {', '.join(derived_metrics)}")
|
|
128
|
+
|
|
129
|
+
return "\n".join(output)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ===== Pydantic Schemas for Tool Input =====
|
|
133
|
+
|
|
134
|
+
class DerivedMetric(BaseModel):
|
|
135
|
+
"""Calculated/derived metric"""
|
|
136
|
+
name: str = Field(description="Name of derived metric (e.g., 'revenue_per_visit')")
|
|
137
|
+
formula: str = Field(description="Formula as string (e.g., 'revenue / visits')")
|
|
138
|
+
description: Optional[str] = Field(None, description="Description of what it represents")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class WhatIfObjective(BaseModel):
|
|
142
|
+
"""Objective for scenario optimization"""
|
|
143
|
+
type: str = Field(description="Type: minimize, maximize, or target")
|
|
144
|
+
metric: str = Field(description="Column/metric name (can be derived)")
|
|
145
|
+
target_value: Optional[float] = None
|
|
146
|
+
weight: float = 1.0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class WhatIfConstraint(BaseModel):
|
|
150
|
+
"""Constraint for scenario"""
|
|
151
|
+
type: str = Field(description="Type: max_change, min_value, max_value, or ratio")
|
|
152
|
+
metric: str
|
|
153
|
+
value: float
|
|
154
|
+
reference_metric: Optional[str] = None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class WhatIfAction(BaseModel):
|
|
158
|
+
"""Possible action to take"""
|
|
159
|
+
type: str = Field(description="Type: close_region, exclude_values, adjust_metric, set_value, scale_proportional")
|
|
160
|
+
target: str
|
|
161
|
+
parameters: Dict = Field(default_factory=dict)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class WhatIfInput(BaseModel):
|
|
165
|
+
"""Input schema for WhatIfTool"""
|
|
166
|
+
scenario_description: str
|
|
167
|
+
df_name: Optional[str] = None
|
|
168
|
+
objectives: List[WhatIfObjective] = Field(default_factory=list)
|
|
169
|
+
constraints: List[WhatIfConstraint] = Field(default_factory=list)
|
|
170
|
+
possible_actions: List[WhatIfAction]
|
|
171
|
+
derived_metrics: List[DerivedMetric] = Field(
|
|
172
|
+
default_factory=list,
|
|
173
|
+
description="Calculated metrics from existing columns"
|
|
174
|
+
)
|
|
175
|
+
max_actions: int = 5
|
|
176
|
+
algorithm: str = "greedy" # greedy or genetic
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ===== Metrics Calculator =====
|
|
180
|
+
|
|
181
|
+
class MetricsCalculator:
|
|
182
|
+
"""Calculates derived metrics on DataFrames"""
|
|
183
|
+
|
|
184
|
+
def __init__(self):
|
|
185
|
+
self.formulas: Dict[str, str] = {}
|
|
186
|
+
self.descriptions: Dict[str, str] = {}
|
|
187
|
+
|
|
188
|
+
def register_metric(self, name: str, formula: str, description: str = ""):
|
|
189
|
+
"""Register a derived metric"""
|
|
190
|
+
self.formulas[name] = formula
|
|
191
|
+
self.descriptions[name] = description
|
|
192
|
+
|
|
193
|
+
def calculate(self, df: pd.DataFrame, metric_name: str) -> pd.Series:
|
|
194
|
+
"""Calculate a derived metric"""
|
|
195
|
+
if metric_name not in self.formulas:
|
|
196
|
+
# If not derived, return column directly
|
|
197
|
+
if metric_name in df.columns:
|
|
198
|
+
return df[metric_name]
|
|
199
|
+
raise ValueError(f"Metric '{metric_name}' not found in DataFrame or formulas")
|
|
200
|
+
|
|
201
|
+
formula = self.formulas[metric_name]
|
|
202
|
+
|
|
203
|
+
# Evaluate formula safely
|
|
204
|
+
# Create safe context with DataFrame columns
|
|
205
|
+
context = {col: df[col] for col in df.columns}
|
|
206
|
+
context['np'] = np # Allow numpy functions
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
result = eval(formula, {"__builtins__": {}}, context)
|
|
210
|
+
return pd.Series(result, index=df.index)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
raise ValueError(f"Error calculating '{metric_name}': {str(e)}")
|
|
213
|
+
|
|
214
|
+
def add_to_dataframe(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
215
|
+
"""Add all derived metrics to DataFrame"""
|
|
216
|
+
df_copy = df.copy()
|
|
217
|
+
for metric_name in self.formulas:
|
|
218
|
+
df_copy[metric_name] = self.calculate(df, metric_name)
|
|
219
|
+
return df_copy
|
|
220
|
+
|
|
221
|
+
def get_base_value(self, df: pd.DataFrame, metric_name: str) -> float:
|
|
222
|
+
"""Get total value of a metric (derived or not)"""
|
|
223
|
+
if metric_name in df.columns:
|
|
224
|
+
return df[metric_name].sum()
|
|
225
|
+
|
|
226
|
+
series = self.calculate(df, metric_name)
|
|
227
|
+
return series.sum()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ===== Scenario Optimizer =====
|
|
231
|
+
|
|
232
|
+
class ScenarioOptimizer:
|
|
233
|
+
"""Optimizer with support for derived metrics"""
|
|
234
|
+
|
|
235
|
+
def __init__(self, base_df: pd.DataFrame, calculator: MetricsCalculator):
|
|
236
|
+
self.base_df = base_df.copy()
|
|
237
|
+
self.calculator = calculator
|
|
238
|
+
|
|
239
|
+
# Calculate base metrics (including derived)
|
|
240
|
+
self.base_with_derived = calculator.add_to_dataframe(base_df)
|
|
241
|
+
self.base_metrics = {}
|
|
242
|
+
|
|
243
|
+
for col in self.base_with_derived.columns:
|
|
244
|
+
if pd.api.types.is_numeric_dtype(self.base_with_derived[col]):
|
|
245
|
+
self.base_metrics[col] = {
|
|
246
|
+
'sum': self.base_with_derived[col].sum(),
|
|
247
|
+
'mean': self.base_with_derived[col].mean(),
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
def evaluate_scenario(self, df: pd.DataFrame) -> Dict[str, Dict]:
|
|
251
|
+
"""Evaluate metrics of a scenario (including derived)"""
|
|
252
|
+
# Add derived metrics
|
|
253
|
+
df_with_derived = self.calculator.add_to_dataframe(df)
|
|
254
|
+
|
|
255
|
+
scenario_metrics = {}
|
|
256
|
+
for col in df_with_derived.columns:
|
|
257
|
+
if pd.api.types.is_numeric_dtype(df_with_derived[col]):
|
|
258
|
+
base_sum = self.base_metrics.get(col, {}).get('sum', 0)
|
|
259
|
+
scenario_sum = df_with_derived[col].sum()
|
|
260
|
+
|
|
261
|
+
scenario_metrics[col] = {
|
|
262
|
+
'value': scenario_sum,
|
|
263
|
+
'change': scenario_sum - base_sum,
|
|
264
|
+
'pct_change': (
|
|
265
|
+
(scenario_sum - base_sum) / base_sum * 100
|
|
266
|
+
) if base_sum != 0 else 0
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return scenario_metrics
|
|
270
|
+
|
|
271
|
+
def check_constraints(
|
|
272
|
+
self,
|
|
273
|
+
df: pd.DataFrame,
|
|
274
|
+
constraints: List[Constraint]
|
|
275
|
+
) -> Tuple[bool, List[str]]:
|
|
276
|
+
"""Check if scenario meets constraints"""
|
|
277
|
+
violations = []
|
|
278
|
+
scenario_metrics = self.evaluate_scenario(df)
|
|
279
|
+
|
|
280
|
+
for constraint in constraints:
|
|
281
|
+
metric_data = scenario_metrics.get(constraint.metric)
|
|
282
|
+
if not metric_data:
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
if constraint.type == ConstraintType.MAX_CHANGE:
|
|
286
|
+
if abs(metric_data['pct_change']) > constraint.value:
|
|
287
|
+
violations.append(
|
|
288
|
+
f"{constraint.metric} changed by {metric_data['pct_change']:.2f}%, "
|
|
289
|
+
f"exceeds limit of {constraint.value}%"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
elif constraint.type == ConstraintType.MIN_VALUE:
|
|
293
|
+
if metric_data['value'] < constraint.value:
|
|
294
|
+
violations.append(
|
|
295
|
+
f"{constraint.metric} = {metric_data['value']:.2f}, "
|
|
296
|
+
f"below minimum of {constraint.value}"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
elif constraint.type == ConstraintType.MAX_VALUE:
|
|
300
|
+
if metric_data['value'] > constraint.value:
|
|
301
|
+
violations.append(
|
|
302
|
+
f"{constraint.metric} = {metric_data['value']:.2f}, "
|
|
303
|
+
f"exceeds maximum of {constraint.value}"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
elif constraint.type == ConstraintType.RATIO:
|
|
307
|
+
if constraint.reference_metric:
|
|
308
|
+
ref_data = scenario_metrics.get(constraint.reference_metric)
|
|
309
|
+
if ref_data and ref_data['value'] != 0:
|
|
310
|
+
ratio = metric_data['value'] / ref_data['value']
|
|
311
|
+
if ratio > constraint.value:
|
|
312
|
+
violations.append(
|
|
313
|
+
f"Ratio {constraint.metric}/{constraint.reference_metric} = {ratio:.2f}, "
|
|
314
|
+
f"exceeds {constraint.value}"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return len(violations) == 0, violations
|
|
318
|
+
|
|
319
|
+
def objective_function(
|
|
320
|
+
self,
|
|
321
|
+
df: pd.DataFrame,
|
|
322
|
+
objectives: List[Objective]
|
|
323
|
+
) -> float:
|
|
324
|
+
"""Calculate objective function value"""
|
|
325
|
+
scenario_metrics = self.evaluate_scenario(df)
|
|
326
|
+
total_score = 0.0
|
|
327
|
+
|
|
328
|
+
for obj in objectives:
|
|
329
|
+
metric_data = scenario_metrics.get(obj.metric)
|
|
330
|
+
if not metric_data:
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
value = metric_data['value']
|
|
334
|
+
|
|
335
|
+
if obj.type == ObjectiveType.MINIMIZE:
|
|
336
|
+
score = -value # Negative because we minimize
|
|
337
|
+
elif obj.type == ObjectiveType.MAXIMIZE:
|
|
338
|
+
score = value
|
|
339
|
+
elif obj.type == ObjectiveType.TARGET:
|
|
340
|
+
score = -abs(value - obj.target_value) # Penalize deviation
|
|
341
|
+
|
|
342
|
+
total_score += score * obj.weight
|
|
343
|
+
|
|
344
|
+
return total_score
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# ===== What-If DSL =====
|
|
348
|
+
|
|
349
|
+
class WhatIfDSL:
|
|
350
|
+
"""Domain Specific Language for What-If analysis with optimization"""
|
|
351
|
+
|
|
352
|
+
def __init__(self, df: pd.DataFrame, name: str = "scenario"):
|
|
353
|
+
self.df = df.copy()
|
|
354
|
+
self.base_df = df.copy()
|
|
355
|
+
self.name = name
|
|
356
|
+
|
|
357
|
+
# Calculator for derived metrics
|
|
358
|
+
self.calculator = MetricsCalculator()
|
|
359
|
+
self.optimizer = None # Initialize after registering metrics
|
|
360
|
+
|
|
361
|
+
self.objectives: List[Objective] = []
|
|
362
|
+
self.constraints: List[Constraint] = []
|
|
363
|
+
self.possible_actions: List[Action] = []
|
|
364
|
+
self.applied_actions: List[Action] = []
|
|
365
|
+
|
|
366
|
+
def register_derived_metric(self, name: str, formula: str, description: str = ""):
|
|
367
|
+
"""Register a derived metric"""
|
|
368
|
+
self.calculator.register_metric(name, formula, description)
|
|
369
|
+
return self
|
|
370
|
+
|
|
371
|
+
def initialize_optimizer(self):
|
|
372
|
+
"""Initialize optimizer after registering metrics"""
|
|
373
|
+
if self.optimizer is None:
|
|
374
|
+
self.optimizer = ScenarioOptimizer(self.base_df, self.calculator)
|
|
375
|
+
return self
|
|
376
|
+
|
|
377
|
+
# ===== Objective Definition =====
|
|
378
|
+
|
|
379
|
+
def minimize(self, metric: str, weight: float = 1.0) -> 'WhatIfDSL':
|
|
380
|
+
"""Minimize a metric"""
|
|
381
|
+
self.objectives.append(
|
|
382
|
+
Objective(metric=metric, type=ObjectiveType.MINIMIZE, weight=weight)
|
|
383
|
+
)
|
|
384
|
+
return self
|
|
385
|
+
|
|
386
|
+
def maximize(self, metric: str, weight: float = 1.0) -> 'WhatIfDSL':
|
|
387
|
+
"""Maximize a metric"""
|
|
388
|
+
self.objectives.append(
|
|
389
|
+
Objective(metric=metric, type=ObjectiveType.MAXIMIZE, weight=weight)
|
|
390
|
+
)
|
|
391
|
+
return self
|
|
392
|
+
|
|
393
|
+
def target(self, metric: str, value: float, weight: float = 1.0) -> 'WhatIfDSL':
|
|
394
|
+
"""Reach a target value"""
|
|
395
|
+
self.objectives.append(
|
|
396
|
+
Objective(
|
|
397
|
+
metric=metric,
|
|
398
|
+
type=ObjectiveType.TARGET,
|
|
399
|
+
target_value=value,
|
|
400
|
+
weight=weight
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
return self
|
|
404
|
+
|
|
405
|
+
# ===== Constraint Definition =====
|
|
406
|
+
|
|
407
|
+
def constrain_change(self, metric: str, max_pct: float) -> 'WhatIfDSL':
|
|
408
|
+
"""Constraint: metric cannot change more than X%"""
|
|
409
|
+
self.constraints.append(
|
|
410
|
+
Constraint(metric=metric, type=ConstraintType.MAX_CHANGE, value=max_pct)
|
|
411
|
+
)
|
|
412
|
+
return self
|
|
413
|
+
|
|
414
|
+
def constrain_min(self, metric: str, min_value: float) -> 'WhatIfDSL':
|
|
415
|
+
"""Constraint: metric must stay above X"""
|
|
416
|
+
self.constraints.append(
|
|
417
|
+
Constraint(metric=metric, type=ConstraintType.MIN_VALUE, value=min_value)
|
|
418
|
+
)
|
|
419
|
+
return self
|
|
420
|
+
|
|
421
|
+
def constrain_max(self, metric: str, max_value: float) -> 'WhatIfDSL':
|
|
422
|
+
"""Constraint: metric must stay below X"""
|
|
423
|
+
self.constraints.append(
|
|
424
|
+
Constraint(metric=metric, type=ConstraintType.MAX_VALUE, value=max_value)
|
|
425
|
+
)
|
|
426
|
+
return self
|
|
427
|
+
|
|
428
|
+
def constrain_ratio(self, metric: str, reference: str, max_ratio: float) -> 'WhatIfDSL':
|
|
429
|
+
"""Constraint: ratio between two metrics"""
|
|
430
|
+
self.constraints.append(
|
|
431
|
+
Constraint(
|
|
432
|
+
metric=metric,
|
|
433
|
+
type=ConstraintType.RATIO,
|
|
434
|
+
value=max_ratio,
|
|
435
|
+
reference_metric=reference
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
return self
|
|
439
|
+
|
|
440
|
+
# ===== Possible Actions Definition =====
|
|
441
|
+
|
|
442
|
+
def can_close_regions(self, regions: Optional[List[str]] = None) -> 'WhatIfDSL':
|
|
443
|
+
"""Define that regions can be closed"""
|
|
444
|
+
if regions is None:
|
|
445
|
+
if 'region' in self.df.columns:
|
|
446
|
+
regions = self.df['region'].unique().tolist()
|
|
447
|
+
else:
|
|
448
|
+
return self
|
|
449
|
+
|
|
450
|
+
for region in regions:
|
|
451
|
+
self.possible_actions.append(
|
|
452
|
+
Action(
|
|
453
|
+
name=f"close_{region}",
|
|
454
|
+
column="region",
|
|
455
|
+
operation="exclude",
|
|
456
|
+
value=region,
|
|
457
|
+
cost=1.0 # Cost of closing a region
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
return self
|
|
461
|
+
|
|
462
|
+
def can_exclude_values(
|
|
463
|
+
self,
|
|
464
|
+
column: str,
|
|
465
|
+
values: Optional[List[str]] = None
|
|
466
|
+
) -> 'WhatIfDSL':
|
|
467
|
+
"""Define that specific values can be excluded from a column (generic version of can_close_regions)"""
|
|
468
|
+
if values is None:
|
|
469
|
+
if column in self.df.columns:
|
|
470
|
+
values = self.df[column].unique().tolist()
|
|
471
|
+
else:
|
|
472
|
+
return self
|
|
473
|
+
|
|
474
|
+
for value in values:
|
|
475
|
+
self.possible_actions.append(
|
|
476
|
+
Action(
|
|
477
|
+
name=f"exclude_{column}_{value}",
|
|
478
|
+
column=column,
|
|
479
|
+
operation="exclude",
|
|
480
|
+
value=value,
|
|
481
|
+
cost=1.0 # Cost of excluding a value
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
return self
|
|
485
|
+
|
|
486
|
+
def can_adjust_metric(
|
|
487
|
+
self,
|
|
488
|
+
metric: str,
|
|
489
|
+
min_pct: float = -50,
|
|
490
|
+
max_pct: float = 50,
|
|
491
|
+
by_region: bool = False
|
|
492
|
+
) -> 'WhatIfDSL':
|
|
493
|
+
"""Define that a metric can be adjusted"""
|
|
494
|
+
if by_region and 'region' in self.df.columns:
|
|
495
|
+
regions = self.df['region'].unique()
|
|
496
|
+
for region in regions:
|
|
497
|
+
for pct in np.linspace(min_pct, max_pct, 10):
|
|
498
|
+
if pct != 0:
|
|
499
|
+
self.possible_actions.append(
|
|
500
|
+
Action(
|
|
501
|
+
name=f"adjust_{metric}_{region}_{pct:.0f}pct",
|
|
502
|
+
column=metric,
|
|
503
|
+
operation="scale_region",
|
|
504
|
+
value={'region': region, 'scale': 1 + pct / 100},
|
|
505
|
+
cost=abs(pct) / 100 # Cost proportional to change
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
else:
|
|
509
|
+
for pct in np.linspace(min_pct, max_pct, 10):
|
|
510
|
+
if pct != 0:
|
|
511
|
+
self.possible_actions.append(
|
|
512
|
+
Action(
|
|
513
|
+
name=f"adjust_{metric}_{pct:.0f}pct",
|
|
514
|
+
column=metric,
|
|
515
|
+
operation="scale",
|
|
516
|
+
value=1 + pct / 100,
|
|
517
|
+
cost=abs(pct) / 100
|
|
518
|
+
)
|
|
519
|
+
)
|
|
520
|
+
return self
|
|
521
|
+
|
|
522
|
+
def can_scale_proportional(
|
|
523
|
+
self,
|
|
524
|
+
base_column: str,
|
|
525
|
+
affected_columns: List[str],
|
|
526
|
+
min_pct: float = -50,
|
|
527
|
+
max_pct: float = 100,
|
|
528
|
+
by_region: bool = False
|
|
529
|
+
) -> 'WhatIfDSL':
|
|
530
|
+
"""
|
|
531
|
+
Allow scaling a base metric and adjust others proportionally.
|
|
532
|
+
|
|
533
|
+
Example: Increase 'visits' and have 'revenue' and 'expenses' scale
|
|
534
|
+
according to revenue_per_visit and expenses_per_visit.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
base_column: Base column to scale (e.g., 'visits')
|
|
538
|
+
affected_columns: Columns that adjust proportionally (e.g., ['revenue', 'expenses'])
|
|
539
|
+
min_pct: Minimum % change
|
|
540
|
+
max_pct: Maximum % change
|
|
541
|
+
by_region: Whether to apply by region
|
|
542
|
+
"""
|
|
543
|
+
if by_region and 'region' in self.df.columns:
|
|
544
|
+
regions = self.df['region'].unique()
|
|
545
|
+
for region in regions:
|
|
546
|
+
for pct in np.linspace(min_pct, max_pct, 10):
|
|
547
|
+
if pct != 0:
|
|
548
|
+
self.possible_actions.append(
|
|
549
|
+
Action(
|
|
550
|
+
name=f"scale_{base_column}_{region}_{pct:.0f}pct",
|
|
551
|
+
column=base_column,
|
|
552
|
+
operation="scale_proportional_region",
|
|
553
|
+
value={
|
|
554
|
+
'region': region,
|
|
555
|
+
'scale': 1 + pct / 100,
|
|
556
|
+
'affected': affected_columns
|
|
557
|
+
},
|
|
558
|
+
cost=abs(pct) / 50,
|
|
559
|
+
affects_derived=True
|
|
560
|
+
)
|
|
561
|
+
)
|
|
562
|
+
else:
|
|
563
|
+
for pct in np.linspace(min_pct, max_pct, 10):
|
|
564
|
+
if pct != 0:
|
|
565
|
+
self.possible_actions.append(
|
|
566
|
+
Action(
|
|
567
|
+
name=f"scale_{base_column}_{pct:.0f}pct",
|
|
568
|
+
column=base_column,
|
|
569
|
+
operation="scale_proportional",
|
|
570
|
+
value={
|
|
571
|
+
'scale': 1 + pct / 100,
|
|
572
|
+
'affected': affected_columns
|
|
573
|
+
},
|
|
574
|
+
cost=abs(pct) / 50,
|
|
575
|
+
affects_derived=True
|
|
576
|
+
)
|
|
577
|
+
)
|
|
578
|
+
return self
|
|
579
|
+
|
|
580
|
+
# ===== Apply Actions =====
|
|
581
|
+
|
|
582
|
+
def _apply_action(self, action: Action, df: Optional[pd.DataFrame] = None) -> pd.DataFrame:
|
|
583
|
+
"""Apply an action to the dataframe"""
|
|
584
|
+
df = self.df.copy() if df is None else df.copy()
|
|
585
|
+
|
|
586
|
+
if action.operation == "exclude":
|
|
587
|
+
df = df[df[action.column] != action.value]
|
|
588
|
+
|
|
589
|
+
elif action.operation == "scale":
|
|
590
|
+
df[action.column] = df[action.column].astype(float)
|
|
591
|
+
df[action.column] = df[action.column] * action.value
|
|
592
|
+
|
|
593
|
+
elif action.operation == "scale_region":
|
|
594
|
+
region = action.value['region']
|
|
595
|
+
scale = action.value['scale']
|
|
596
|
+
mask = df['region'] == region
|
|
597
|
+
# Convert column to float first to avoid dtype warnings
|
|
598
|
+
df[action.column] = df[action.column].astype(float)
|
|
599
|
+
df.loc[mask, action.column] = df.loc[mask, action.column] * scale
|
|
600
|
+
|
|
601
|
+
elif action.operation == "scale_proportional":
|
|
602
|
+
# Scale base column
|
|
603
|
+
scale = action.value['scale']
|
|
604
|
+
df[action.column] = df[action.column].astype(float)
|
|
605
|
+
df[action.column] = df[action.column] * scale
|
|
606
|
+
|
|
607
|
+
# Calculate derived metrics before the change
|
|
608
|
+
df_with_derived = self.calculator.add_to_dataframe(self.base_df)
|
|
609
|
+
|
|
610
|
+
# Adjust affected columns proportionally
|
|
611
|
+
for affected_col in action.value['affected']:
|
|
612
|
+
# Look for related derived metric (e.g., revenue_per_visit)
|
|
613
|
+
derived_metric = f"{affected_col}_per_{action.column}"
|
|
614
|
+
|
|
615
|
+
if derived_metric in self.calculator.formulas:
|
|
616
|
+
# Calculate value per base unit
|
|
617
|
+
per_unit = df_with_derived[derived_metric].values
|
|
618
|
+
# Apply to new base column values
|
|
619
|
+
df[affected_col] = df[action.column].values * per_unit
|
|
620
|
+
|
|
621
|
+
elif action.operation == "scale_proportional_region":
|
|
622
|
+
region = action.value['region']
|
|
623
|
+
scale = action.value['scale']
|
|
624
|
+
mask = df['region'] == region
|
|
625
|
+
|
|
626
|
+
# Scale base column in region
|
|
627
|
+
# Convert column to float first to avoid dtype warnings
|
|
628
|
+
df[action.column] = df[action.column].astype(float)
|
|
629
|
+
df.loc[mask, action.column] = df.loc[mask, action.column] * scale
|
|
630
|
+
|
|
631
|
+
# Calculate derived metrics
|
|
632
|
+
df_with_derived = self.calculator.add_to_dataframe(self.base_df)
|
|
633
|
+
|
|
634
|
+
# Adjust affected columns in region
|
|
635
|
+
for affected_col in action.value['affected']:
|
|
636
|
+
derived_metric = f"{affected_col}_per_{action.column}"
|
|
637
|
+
|
|
638
|
+
if derived_metric in self.calculator.formulas:
|
|
639
|
+
per_unit = df_with_derived.loc[mask, derived_metric].values
|
|
640
|
+
df.loc[mask, affected_col] = df.loc[mask, action.column].values * per_unit
|
|
641
|
+
|
|
642
|
+
elif action.operation == "set_value":
|
|
643
|
+
df[action.column] = action.value
|
|
644
|
+
|
|
645
|
+
return df
|
|
646
|
+
|
|
647
|
+
# ===== Optimization =====
|
|
648
|
+
|
|
649
|
+
def solve(
|
|
650
|
+
self,
|
|
651
|
+
max_actions: int = 5,
|
|
652
|
+
algorithm: str = "greedy"
|
|
653
|
+
) -> ScenarioResult:
|
|
654
|
+
"""
|
|
655
|
+
Find best combination of actions that meets constraints.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
max_actions: Maximum number of actions to take
|
|
659
|
+
algorithm: 'greedy' or 'genetic'
|
|
660
|
+
"""
|
|
661
|
+
|
|
662
|
+
if algorithm == "greedy":
|
|
663
|
+
return self._solve_greedy(max_actions)
|
|
664
|
+
elif algorithm == "genetic":
|
|
665
|
+
return self._solve_genetic(max_actions)
|
|
666
|
+
else:
|
|
667
|
+
raise ValueError(f"Unknown algorithm: {algorithm}")
|
|
668
|
+
|
|
669
|
+
def _solve_greedy(self, max_actions: int) -> ScenarioResult:
|
|
670
|
+
"""Greedy algorithm: evaluate actions one by one"""
|
|
671
|
+
# SPECIAL CASE: If no objectives, just apply actions directly
|
|
672
|
+
if len(self.objectives) == 0 and len(self.constraints) == 0:
|
|
673
|
+
selected_actions = []
|
|
674
|
+
current_df = self.df.copy()
|
|
675
|
+
|
|
676
|
+
for action in self.possible_actions[:max_actions]:
|
|
677
|
+
test_df = self._apply_action(action, current_df)
|
|
678
|
+
if not test_df.empty:
|
|
679
|
+
selected_actions.append(action)
|
|
680
|
+
current_df = test_df
|
|
681
|
+
if len(selected_actions) >= max_actions:
|
|
682
|
+
break
|
|
683
|
+
|
|
684
|
+
self.applied_actions = selected_actions
|
|
685
|
+
return ScenarioResult(
|
|
686
|
+
scenario_name=self.name,
|
|
687
|
+
base_df=self.base_df,
|
|
688
|
+
result_df=current_df,
|
|
689
|
+
actions=selected_actions,
|
|
690
|
+
optimizer=self.optimizer,
|
|
691
|
+
calculator=self.calculator
|
|
692
|
+
)
|
|
693
|
+
best_df = self.df.copy()
|
|
694
|
+
best_score = self.optimizer.objective_function(best_df, self.objectives)
|
|
695
|
+
selected_actions = []
|
|
696
|
+
|
|
697
|
+
for _ in range(max_actions):
|
|
698
|
+
best_action = None
|
|
699
|
+
best_action_score = best_score
|
|
700
|
+
best_action_df = None
|
|
701
|
+
|
|
702
|
+
# Try each possible action
|
|
703
|
+
for action in self.possible_actions:
|
|
704
|
+
if action in selected_actions:
|
|
705
|
+
continue
|
|
706
|
+
|
|
707
|
+
# Apply action
|
|
708
|
+
test_df = self._apply_action(action, best_df)
|
|
709
|
+
|
|
710
|
+
# Check constraints
|
|
711
|
+
valid, violations = self.optimizer.check_constraints(
|
|
712
|
+
test_df, self.constraints
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
if not valid:
|
|
716
|
+
continue
|
|
717
|
+
|
|
718
|
+
# Calculate score
|
|
719
|
+
score = self.optimizer.objective_function(test_df, self.objectives)
|
|
720
|
+
score -= action.cost * 10 # Penalize by action cost
|
|
721
|
+
|
|
722
|
+
if score > best_action_score:
|
|
723
|
+
best_action = action
|
|
724
|
+
best_action_score = score
|
|
725
|
+
best_action_df = test_df
|
|
726
|
+
|
|
727
|
+
# If we found an improvement, apply it
|
|
728
|
+
if best_action:
|
|
729
|
+
selected_actions.append(best_action)
|
|
730
|
+
best_df = best_action_df
|
|
731
|
+
best_score = best_action_score
|
|
732
|
+
else:
|
|
733
|
+
break # No more improvements possible
|
|
734
|
+
|
|
735
|
+
self.applied_actions = selected_actions
|
|
736
|
+
return ScenarioResult(
|
|
737
|
+
scenario_name=self.name,
|
|
738
|
+
base_df=self.base_df,
|
|
739
|
+
result_df=best_df,
|
|
740
|
+
actions=selected_actions,
|
|
741
|
+
optimizer=self.optimizer,
|
|
742
|
+
calculator=self.calculator
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
def _solve_genetic(self, max_actions: int) -> ScenarioResult:
|
|
746
|
+
"""Genetic algorithm to explore solution space"""
|
|
747
|
+
from itertools import combinations
|
|
748
|
+
|
|
749
|
+
best_score = float('-inf')
|
|
750
|
+
best_actions = []
|
|
751
|
+
best_df = self.df.copy()
|
|
752
|
+
|
|
753
|
+
# Explore combinations of actions
|
|
754
|
+
for r in range(1, min(max_actions + 1, len(self.possible_actions) + 1)):
|
|
755
|
+
for action_combo in combinations(self.possible_actions, r):
|
|
756
|
+
# Apply combination of actions
|
|
757
|
+
test_df = self.base_df.copy()
|
|
758
|
+
for action in action_combo:
|
|
759
|
+
test_df = self._apply_action(action, test_df)
|
|
760
|
+
|
|
761
|
+
# Check constraints
|
|
762
|
+
valid, violations = self.optimizer.check_constraints(
|
|
763
|
+
test_df, self.constraints
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
if not valid:
|
|
767
|
+
continue
|
|
768
|
+
|
|
769
|
+
# Calculate score
|
|
770
|
+
score = self.optimizer.objective_function(test_df, self.objectives)
|
|
771
|
+
score -= sum(a.cost for a in action_combo) * 10
|
|
772
|
+
|
|
773
|
+
if score > best_score:
|
|
774
|
+
best_score = score
|
|
775
|
+
best_actions = list(action_combo)
|
|
776
|
+
best_df = test_df
|
|
777
|
+
|
|
778
|
+
self.applied_actions = best_actions
|
|
779
|
+
return ScenarioResult(
|
|
780
|
+
scenario_name=self.name,
|
|
781
|
+
base_df=self.base_df,
|
|
782
|
+
result_df=best_df,
|
|
783
|
+
actions=best_actions,
|
|
784
|
+
optimizer=self.optimizer,
|
|
785
|
+
calculator=self.calculator
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
# ===== What-If Tool Implementation =====
|
|
790
|
+
|
|
791
|
+
class WhatIfTool(AbstractTool):
|
|
792
|
+
"""
|
|
793
|
+
What-If Analysis Tool with support for derived metrics and optimization.
|
|
794
|
+
|
|
795
|
+
Allows LLM to execute hypothetical scenarios on DataFrames,
|
|
796
|
+
optimize metrics under constraints, and compare results.
|
|
797
|
+
"""
|
|
798
|
+
args_schema: Type[BaseModel] = WhatIfInput
|
|
799
|
+
|
|
800
|
+
def __init__(self):
|
|
801
|
+
super().__init__(
|
|
802
|
+
name="whatif_scenario",
|
|
803
|
+
description=self._get_description()
|
|
804
|
+
)
|
|
805
|
+
self.scenarios_cache: Dict[str, ScenarioResult] = {}
|
|
806
|
+
self._parent_agent = None # Reference to PandasAgent
|
|
807
|
+
|
|
808
|
+
def _get_description(self) -> str:
|
|
809
|
+
return """
|
|
810
|
+
Execute what-if scenario analysis on DataFrames with optimization and derived metrics support.
|
|
811
|
+
|
|
812
|
+
This tool allows you to:
|
|
813
|
+
- Test hypothetical scenarios (e.g., "what if we close region X?")
|
|
814
|
+
- Optimize metrics under constraints (e.g., "reduce expenses without revenue dropping >5%")
|
|
815
|
+
- Handle derived metrics (e.g., revenue_per_visit, expenses_per_visit)
|
|
816
|
+
- Simulate proportional changes (e.g., "what if we increase visits by 20%?")
|
|
817
|
+
|
|
818
|
+
DERIVED METRICS:
|
|
819
|
+
You can define calculated metrics using formulas:
|
|
820
|
+
- revenue_per_visit = revenue / visits
|
|
821
|
+
- expenses_per_visit = expenses / visits
|
|
822
|
+
- profit_margin = (revenue - expenses) / revenue
|
|
823
|
+
- cost_per_employee = expenses / headcount
|
|
824
|
+
|
|
825
|
+
These metrics are automatically recalculated when base columns change.
|
|
826
|
+
|
|
827
|
+
PROPORTIONAL SCALING:
|
|
828
|
+
When you scale a base metric (like 'visits'), you can specify affected columns
|
|
829
|
+
that should scale proportionally based on derived metrics.
|
|
830
|
+
|
|
831
|
+
Example: "What if we increase visits by 20%?"
|
|
832
|
+
- visits increases by 20%
|
|
833
|
+
- revenue = visits * revenue_per_visit (automatically adjusted)
|
|
834
|
+
- expenses = visits * expenses_per_visit (automatically adjusted)
|
|
835
|
+
|
|
836
|
+
TRIGGER PATTERNS:
|
|
837
|
+
- "What if we close region X?" or "What if we close project Y?"
|
|
838
|
+
- "What if we exclude department Z?"
|
|
839
|
+
- "What if we reduce expenses to Y?"
|
|
840
|
+
- "What if we increase visits by Z%?"
|
|
841
|
+
- "How can I reduce costs without affecting revenue?"
|
|
842
|
+
- "Find the best way to maximize profit"
|
|
843
|
+
|
|
844
|
+
COMMON SCENARIOS:
|
|
845
|
+
|
|
846
|
+
1. Simple Impact Analysis:
|
|
847
|
+
"What if we close the North region?" or "What if we close the Belkin project?"
|
|
848
|
+
→ Removes entity from the specified column, shows impact on all metrics
|
|
849
|
+
|
|
850
|
+
2. Constraint Optimization:
|
|
851
|
+
"Reduce expenses to 500k without revenue dropping more than 5%"
|
|
852
|
+
→ Finds optimal actions to hit target while respecting constraints
|
|
853
|
+
|
|
854
|
+
3. Proportional Changes:
|
|
855
|
+
"What if we increase visits by 30%?"
|
|
856
|
+
→ Scales visits and adjusts revenue/expenses proportionally
|
|
857
|
+
|
|
858
|
+
4. Multi-Objective:
|
|
859
|
+
"Maximize profit while keeping headcount above 100"
|
|
860
|
+
→ Optimizes multiple goals with constraints
|
|
861
|
+
|
|
862
|
+
IMPORTANT:
|
|
863
|
+
- Always define derived_metrics when dealing with per-unit calculations
|
|
864
|
+
- Use scale_proportional actions for scenarios involving rate-based changes
|
|
865
|
+
- Constraints are hard limits - scenarios violating them are rejected
|
|
866
|
+
- Objectives can have weights (higher = more important)
|
|
867
|
+
""".strip()
|
|
868
|
+
|
|
869
|
+
def set_parent_agent(self, agent):
|
|
870
|
+
"""Set reference to parent PandasAgent"""
|
|
871
|
+
self._parent_agent = agent
|
|
872
|
+
|
|
873
|
+
def get_input_schema(self) -> type[BaseModel]:
|
|
874
|
+
return WhatIfInput
|
|
875
|
+
|
|
876
|
+
async def _execute(self, **kwargs) -> ToolResult:
|
|
877
|
+
"""Execute what-if analysis - FIXED VERSION"""
|
|
878
|
+
|
|
879
|
+
self.logger.debug(
|
|
880
|
+
f"WhatIfTool kwargs keys: {list(kwargs.keys())}"
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
# Validate input
|
|
884
|
+
try:
|
|
885
|
+
input_data = WhatIfInput(**kwargs)
|
|
886
|
+
self.logger.info(f" Input validated: {input_data.scenario_description}")
|
|
887
|
+
except Exception as e:
|
|
888
|
+
self.logger.error(f" Input validation failed: {str(e)}")
|
|
889
|
+
return ToolResult(
|
|
890
|
+
success=False,
|
|
891
|
+
result={},
|
|
892
|
+
error=f"Invalid input: {str(e)}"
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
# Check parent agent
|
|
896
|
+
if not self._parent_agent:
|
|
897
|
+
self.logger.error(" Parent agent not set!")
|
|
898
|
+
return ToolResult(
|
|
899
|
+
success=False,
|
|
900
|
+
result={},
|
|
901
|
+
error="Tool not initialized with parent agent"
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
# CRITICAL FIX: Access dataframes correctly
|
|
905
|
+
if not hasattr(self._parent_agent, 'dataframes'):
|
|
906
|
+
return ToolResult(
|
|
907
|
+
success=False,
|
|
908
|
+
result={},
|
|
909
|
+
error="Parent agent missing 'dataframes' attribute"
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
self.logger.info(
|
|
913
|
+
f":: Available DataFrames: {list(self._parent_agent.dataframes.keys())}"
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
df = None
|
|
917
|
+
if input_data.df_name:
|
|
918
|
+
df = self._parent_agent.dataframes.get(input_data.df_name)
|
|
919
|
+
if df is None:
|
|
920
|
+
self.logger.error(f" DataFrame '{input_data.df_name}' not found")
|
|
921
|
+
return ToolResult(
|
|
922
|
+
success=False,
|
|
923
|
+
result={},
|
|
924
|
+
error=f"DataFrame '{input_data.df_name}' not found. Available: {list(self._parent_agent.dataframes.keys())}"
|
|
925
|
+
)
|
|
926
|
+
else:
|
|
927
|
+
# Get first DataFrame
|
|
928
|
+
if self._parent_agent.dataframes:
|
|
929
|
+
df_name = list(self._parent_agent.dataframes.keys())[0]
|
|
930
|
+
df = self._parent_agent.dataframes[df_name]
|
|
931
|
+
self.logger.info(f" Using first DataFrame: {df_name}")
|
|
932
|
+
else:
|
|
933
|
+
self.logger.error(" No DataFrames loaded!")
|
|
934
|
+
return ToolResult(
|
|
935
|
+
success=False,
|
|
936
|
+
result={},
|
|
937
|
+
error="No DataFrames loaded"
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
if df is None or df.empty:
|
|
941
|
+
self.logger.error(" DataFrame is None or empty")
|
|
942
|
+
return ToolResult(
|
|
943
|
+
success=False,
|
|
944
|
+
result={},
|
|
945
|
+
error="DataFrame is empty"
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
self.logger.info(f" DataFrame shape: {df.shape}, columns: {list(df.columns)[:5]}...")
|
|
949
|
+
|
|
950
|
+
try:
|
|
951
|
+
# Build DSL
|
|
952
|
+
dsl = WhatIfDSL(df, name=input_data.scenario_description)
|
|
953
|
+
|
|
954
|
+
# Register derived metrics
|
|
955
|
+
for derived in input_data.derived_metrics:
|
|
956
|
+
dsl.register_derived_metric(derived.name, derived.formula, derived.description or "")
|
|
957
|
+
self.logger.info(f" Registered {len(input_data.derived_metrics)} derived metrics")
|
|
958
|
+
|
|
959
|
+
# Initialize optimizer
|
|
960
|
+
dsl.initialize_optimizer()
|
|
961
|
+
self.logger.info(" Optimizer initialized")
|
|
962
|
+
|
|
963
|
+
# Configure objectives
|
|
964
|
+
for obj in input_data.objectives:
|
|
965
|
+
obj_type = obj.type.lower()
|
|
966
|
+
if obj_type == "minimize":
|
|
967
|
+
dsl.minimize(obj.metric, weight=obj.weight)
|
|
968
|
+
elif obj_type == "maximize":
|
|
969
|
+
dsl.maximize(obj.metric, weight=obj.weight)
|
|
970
|
+
elif obj_type == "target":
|
|
971
|
+
dsl.target(obj.metric, obj.target_value, weight=obj.weight)
|
|
972
|
+
self.logger.info(f" Configured {len(input_data.objectives)} objectives")
|
|
973
|
+
|
|
974
|
+
# Configure constraints
|
|
975
|
+
for constraint in input_data.constraints:
|
|
976
|
+
const_type = constraint.type.lower()
|
|
977
|
+
if const_type == "max_change":
|
|
978
|
+
dsl.constrain_change(constraint.metric, constraint.value)
|
|
979
|
+
elif const_type == "min_value":
|
|
980
|
+
dsl.constrain_min(constraint.metric, constraint.value)
|
|
981
|
+
elif const_type == "max_value":
|
|
982
|
+
dsl.constrain_max(constraint.metric, constraint.value)
|
|
983
|
+
elif const_type == "ratio":
|
|
984
|
+
dsl.constrain_ratio(constraint.metric, constraint.reference_metric, constraint.value)
|
|
985
|
+
self.logger.info(f" Configured {len(input_data.constraints)} constraints")
|
|
986
|
+
|
|
987
|
+
# Configure possible actions
|
|
988
|
+
for action in input_data.possible_actions:
|
|
989
|
+
action_type = action.type.lower()
|
|
990
|
+
|
|
991
|
+
if action_type == "close_region":
|
|
992
|
+
regions = action.parameters.get("regions")
|
|
993
|
+
dsl.can_close_regions(regions)
|
|
994
|
+
|
|
995
|
+
elif action_type == "exclude_values":
|
|
996
|
+
column = action.parameters.get("column", action.target)
|
|
997
|
+
values = action.parameters.get("values")
|
|
998
|
+
dsl.can_exclude_values(column, values)
|
|
999
|
+
|
|
1000
|
+
elif action_type == "adjust_metric":
|
|
1001
|
+
dsl.can_adjust_metric(
|
|
1002
|
+
metric=action.target,
|
|
1003
|
+
min_pct=action.parameters.get("min_pct", -50),
|
|
1004
|
+
max_pct=action.parameters.get("max_pct", 50),
|
|
1005
|
+
by_region=action.parameters.get("by_region", False)
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
elif action_type == "scale_proportional":
|
|
1009
|
+
dsl.can_scale_proportional(
|
|
1010
|
+
base_column=action.target,
|
|
1011
|
+
affected_columns=action.parameters.get("affected_columns", []),
|
|
1012
|
+
min_pct=action.parameters.get("min_pct", -50),
|
|
1013
|
+
max_pct=action.parameters.get("max_pct", 100),
|
|
1014
|
+
by_region=action.parameters.get("by_region", False)
|
|
1015
|
+
)
|
|
1016
|
+
self.logger.info(f" Configured {len(input_data.possible_actions)} possible actions")
|
|
1017
|
+
|
|
1018
|
+
# Solve scenario
|
|
1019
|
+
self.logger.info(f" Solving with {input_data.algorithm} algorithm...")
|
|
1020
|
+
result = dsl.solve(
|
|
1021
|
+
max_actions=input_data.max_actions,
|
|
1022
|
+
algorithm=input_data.algorithm
|
|
1023
|
+
)
|
|
1024
|
+
self.logger.info(f" Solved! {len(result.actions)} actions applied")
|
|
1025
|
+
|
|
1026
|
+
# Cache result
|
|
1027
|
+
scenario_id = f"scenario_{len(self.scenarios_cache) + 1}"
|
|
1028
|
+
self.scenarios_cache[scenario_id] = result
|
|
1029
|
+
|
|
1030
|
+
# Prepare result
|
|
1031
|
+
comparison = result.compare()
|
|
1032
|
+
|
|
1033
|
+
# create the comparison table:
|
|
1034
|
+
comparison_table = self._create_comparison_table(result)
|
|
1035
|
+
# Build response - CRITICAL: Always return ToolResult with result field
|
|
1036
|
+
response_data = {
|
|
1037
|
+
"scenario_id": scenario_id,
|
|
1038
|
+
"scenario_name": input_data.scenario_description,
|
|
1039
|
+
"visualization": result.visualize(),
|
|
1040
|
+
"actions_count": len(result.actions),
|
|
1041
|
+
"metrics_changed": list(comparison['metrics'].keys()),
|
|
1042
|
+
"comparison": comparison,
|
|
1043
|
+
"comparison_table": comparison_table,
|
|
1044
|
+
"actions_applied": [
|
|
1045
|
+
{
|
|
1046
|
+
"action": a.name,
|
|
1047
|
+
"description": self._describe_action(a),
|
|
1048
|
+
"cost": a.cost
|
|
1049
|
+
}
|
|
1050
|
+
for a in result.actions
|
|
1051
|
+
],
|
|
1052
|
+
"summary": f"{len(result.actions)} actions applied",
|
|
1053
|
+
"baseline_summary": self._summarize_df(result.base_df),
|
|
1054
|
+
"scenario_summary": self._summarize_df(result.result_df),
|
|
1055
|
+
"verdict": self._generate_veredict(result)
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
return ToolResult(
|
|
1059
|
+
success=True,
|
|
1060
|
+
result=response_data
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
except Exception as e:
|
|
1064
|
+
self.logger.error(
|
|
1065
|
+
f"Error executing scenario: {e} :\n{traceback.format_exc()}"
|
|
1066
|
+
)
|
|
1067
|
+
return ToolResult(
|
|
1068
|
+
success=False,
|
|
1069
|
+
result={},
|
|
1070
|
+
error=f"Execution error: {str(e)}",
|
|
1071
|
+
metadata={"traceback": traceback.format_exc()}
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
def _create_comparison_table(self, result: ScenarioResult) -> str:
|
|
1075
|
+
"""Create comparison table in markdown format"""
|
|
1076
|
+
comparison = result.compare()
|
|
1077
|
+
|
|
1078
|
+
lines = [
|
|
1079
|
+
"| Metric | Baseline | Scenario | Change | % Change |",
|
|
1080
|
+
"|--------|----------|----------|--------|----------|"
|
|
1081
|
+
]
|
|
1082
|
+
|
|
1083
|
+
for metric, data in comparison['metrics'].items():
|
|
1084
|
+
baseline = data['value'] - data['change']
|
|
1085
|
+
scenario = data['value']
|
|
1086
|
+
change = data['change']
|
|
1087
|
+
pct = data['pct_change']
|
|
1088
|
+
|
|
1089
|
+
lines.append(
|
|
1090
|
+
f"| {metric} | {baseline:,.2f} | {scenario:,.2f} | "
|
|
1091
|
+
f"{change:+,.2f} | {pct:+.2f}% |"
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
return "\n".join(lines)
|
|
1095
|
+
|
|
1096
|
+
def _describe_action(self, action: Action) -> str:
|
|
1097
|
+
"""Generate readable description of an action"""
|
|
1098
|
+
if action.operation == "exclude":
|
|
1099
|
+
return f"Close/Remove {action.value} from {action.column}"
|
|
1100
|
+
elif action.operation == "scale":
|
|
1101
|
+
pct = (action.value - 1) * 100
|
|
1102
|
+
return f"Adjust {action.column} by {pct:+.1f}%"
|
|
1103
|
+
elif action.operation == "scale_region":
|
|
1104
|
+
region = action.value['region']
|
|
1105
|
+
pct = (action.value['scale'] - 1) * 100
|
|
1106
|
+
return f"Adjust {action.column} in {region} by {pct:+.1f}%"
|
|
1107
|
+
elif action.operation in ["scale_proportional", "scale_proportional_region"]:
|
|
1108
|
+
pct = (action.value['scale'] - 1) * 100
|
|
1109
|
+
if 'region' in action.value:
|
|
1110
|
+
region = action.value['region']
|
|
1111
|
+
affected = ", ".join(action.value['affected'])
|
|
1112
|
+
return f"Scale {action.column} by {pct:+.1f}% in {region} (affects: {affected})"
|
|
1113
|
+
else:
|
|
1114
|
+
affected = ", ".join(action.value['affected'])
|
|
1115
|
+
return f"Scale {action.column} by {pct:+.1f}% (affects: {affected})"
|
|
1116
|
+
return action.name
|
|
1117
|
+
|
|
1118
|
+
def _generate_veredict(self, result: ScenarioResult) -> str:
|
|
1119
|
+
"""Generate verdict about the scenario"""
|
|
1120
|
+
comparison = result.compare()
|
|
1121
|
+
|
|
1122
|
+
verdicts = []
|
|
1123
|
+
|
|
1124
|
+
# Analyze significant changes
|
|
1125
|
+
for metric, data in comparison['metrics'].items():
|
|
1126
|
+
pct = data['pct_change']
|
|
1127
|
+
if abs(pct) > 10:
|
|
1128
|
+
direction = "increased" if pct > 0 else "decreased"
|
|
1129
|
+
verdicts.append(
|
|
1130
|
+
f"⚠️ {metric} {direction} by {abs(pct):.1f}%"
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
if not verdicts:
|
|
1134
|
+
verdicts.append("✅ Minor changes, scenario is viable")
|
|
1135
|
+
|
|
1136
|
+
return " | ".join(verdicts)
|
|
1137
|
+
|
|
1138
|
+
def _summarize_df(self, df: pd.DataFrame) -> Dict:
|
|
1139
|
+
"""Resume un DataFrame"""
|
|
1140
|
+
summary = {
|
|
1141
|
+
"row_count": len(df),
|
|
1142
|
+
"metrics": {}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
for col in df.columns:
|
|
1146
|
+
if pd.api.types.is_numeric_dtype(df[col]):
|
|
1147
|
+
summary["metrics"][col] = {
|
|
1148
|
+
"sum": float(df[col].sum()),
|
|
1149
|
+
"mean": float(df[col].mean()),
|
|
1150
|
+
"min": float(df[col].min()),
|
|
1151
|
+
"max": float(df[col].max())
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
return summary
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
# ===== System Prompt for LLM =====
|
|
1158
|
+
|
|
1159
|
+
WHATIF_SYSTEM_PROMPT = """
|
|
1160
|
+
## What-If Scenario Analysis
|
|
1161
|
+
|
|
1162
|
+
You have access to a powerful `whatif_scenario` tool for analyzing hypothetical scenarios on DataFrames.
|
|
1163
|
+
|
|
1164
|
+
**When to use it:**
|
|
1165
|
+
- User asks "what if..." questions
|
|
1166
|
+
- User wants to understand impact of changes
|
|
1167
|
+
- User needs to optimize metrics under constraints
|
|
1168
|
+
- User asks how to achieve a goal (e.g., "how can I reduce X without affecting Y?")
|
|
1169
|
+
|
|
1170
|
+
**Trigger patterns:**
|
|
1171
|
+
- "What if we [action]?"
|
|
1172
|
+
- "What happens if [condition]?"
|
|
1173
|
+
- "How can I [objective] without [constraint]?"
|
|
1174
|
+
- "What's the impact of [action]?"
|
|
1175
|
+
- "Show me a scenario where [condition]"
|
|
1176
|
+
- "Find the best way to [objective]"
|
|
1177
|
+
|
|
1178
|
+
**Example Usage:**
|
|
1179
|
+
|
|
1180
|
+
User: "What if we close the North region?"
|
|
1181
|
+
→ Tool call:
|
|
1182
|
+
{
|
|
1183
|
+
"scenario_description": "close_north_region",
|
|
1184
|
+
"objectives": [],
|
|
1185
|
+
"constraints": [],
|
|
1186
|
+
"possible_actions": [
|
|
1187
|
+
{
|
|
1188
|
+
"type": "close_region",
|
|
1189
|
+
"target": "North",
|
|
1190
|
+
"parameters": {"regions": ["North"]}
|
|
1191
|
+
}
|
|
1192
|
+
],
|
|
1193
|
+
"derived_metrics": [],
|
|
1194
|
+
"max_actions": 1
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
User: "What if we increase visits by 30%? How does that affect revenue and expenses?"
|
|
1198
|
+
→ Tool call:
|
|
1199
|
+
{
|
|
1200
|
+
"scenario_description": "increase_visits_30pct",
|
|
1201
|
+
"objectives": [],
|
|
1202
|
+
"constraints": [],
|
|
1203
|
+
"possible_actions": [
|
|
1204
|
+
{
|
|
1205
|
+
"type": "scale_proportional",
|
|
1206
|
+
"target": "visits",
|
|
1207
|
+
"parameters": {
|
|
1208
|
+
"min_pct": 30,
|
|
1209
|
+
"max_pct": 30,
|
|
1210
|
+
"affected_columns": ["revenue", "expenses"],
|
|
1211
|
+
"by_region": false
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
],
|
|
1215
|
+
"derived_metrics": [
|
|
1216
|
+
{"name": "revenue_per_visit", "formula": "revenue / visits"},
|
|
1217
|
+
{"name": "expenses_per_visit", "formula": "expenses / visits"}
|
|
1218
|
+
],
|
|
1219
|
+
"max_actions": 1
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
User: "How can I reduce expenses to 500k without revenue dropping more than 5%?"
|
|
1223
|
+
→ Tool call:
|
|
1224
|
+
{
|
|
1225
|
+
"scenario_description": "reduce_expenses_preserve_revenue",
|
|
1226
|
+
"objectives": [
|
|
1227
|
+
{"type": "target", "metric": "expenses", "target_value": 500000, "weight": 2.0}
|
|
1228
|
+
],
|
|
1229
|
+
"constraints": [
|
|
1230
|
+
{"type": "max_change", "metric": "revenue", "value": 5.0}
|
|
1231
|
+
],
|
|
1232
|
+
"possible_actions": [
|
|
1233
|
+
{
|
|
1234
|
+
"type": "close_region",
|
|
1235
|
+
"target": "regions",
|
|
1236
|
+
"parameters": {}
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
"type": "adjust_metric",
|
|
1240
|
+
"target": "expenses",
|
|
1241
|
+
"parameters": {"min_pct": -40, "max_pct": 0, "by_region": true}
|
|
1242
|
+
}
|
|
1243
|
+
],
|
|
1244
|
+
"derived_metrics": [],
|
|
1245
|
+
"max_actions": 3,
|
|
1246
|
+
"algorithm": "greedy"
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
**After executing:**
|
|
1250
|
+
1. Present the comparison table clearly
|
|
1251
|
+
2. Explain the actions taken
|
|
1252
|
+
3. Highlight significant changes
|
|
1253
|
+
4. Note if constraints were satisfied
|
|
1254
|
+
5. Offer to explore alternative scenarios
|
|
1255
|
+
"""
|
|
1256
|
+
|
|
1257
|
+
# ===== Integration Helper for PandasAgent =====
|
|
1258
|
+
|
|
1259
|
+
def integrate_whatif_tool(agent) -> WhatIfTool:
|
|
1260
|
+
"""
|
|
1261
|
+
Integrate WhatIfTool into an existing PandasAgent.
|
|
1262
|
+
|
|
1263
|
+
Args:
|
|
1264
|
+
agent: Instance of PandasAgent
|
|
1265
|
+
|
|
1266
|
+
Returns:
|
|
1267
|
+
The WhatIfTool instance (for reference)
|
|
1268
|
+
"""
|
|
1269
|
+
# Create and register the tool
|
|
1270
|
+
whatif_tool = WhatIfTool()
|
|
1271
|
+
whatif_tool.set_parent_agent(agent)
|
|
1272
|
+
agent.tool_manager.register_tool(whatif_tool)
|
|
1273
|
+
|
|
1274
|
+
# Add system prompt enhancement
|
|
1275
|
+
current_prompt = agent.system_prompt_template or ""
|
|
1276
|
+
if "What-If Scenario Analysis" not in current_prompt:
|
|
1277
|
+
agent.system_prompt_template = current_prompt + "\n\n" + WHATIF_SYSTEM_PROMPT
|
|
1278
|
+
|
|
1279
|
+
return whatif_tool
|