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.
Files changed (535) hide show
  1. agentui/.prettierrc +15 -0
  2. agentui/QUICKSTART.md +272 -0
  3. agentui/README.md +59 -0
  4. agentui/env.example +16 -0
  5. agentui/jsconfig.json +14 -0
  6. agentui/package-lock.json +4242 -0
  7. agentui/package.json +34 -0
  8. agentui/scripts/postinstall/apply-patches.mjs +260 -0
  9. agentui/src/app.css +61 -0
  10. agentui/src/app.d.ts +13 -0
  11. agentui/src/app.html +12 -0
  12. agentui/src/components/LoadingSpinner.svelte +64 -0
  13. agentui/src/components/ThemeSwitcher.svelte +159 -0
  14. agentui/src/components/index.js +4 -0
  15. agentui/src/lib/api/bots.ts +60 -0
  16. agentui/src/lib/api/chat.ts +22 -0
  17. agentui/src/lib/api/http.ts +25 -0
  18. agentui/src/lib/components/BotCard.svelte +33 -0
  19. agentui/src/lib/components/ChatBubble.svelte +63 -0
  20. agentui/src/lib/components/Toast.svelte +21 -0
  21. agentui/src/lib/config.ts +20 -0
  22. agentui/src/lib/stores/auth.svelte.ts +73 -0
  23. agentui/src/lib/stores/theme.svelte.js +64 -0
  24. agentui/src/lib/stores/toast.svelte.ts +31 -0
  25. agentui/src/lib/utils/conversation.ts +39 -0
  26. agentui/src/routes/+layout.svelte +20 -0
  27. agentui/src/routes/+page.svelte +232 -0
  28. agentui/src/routes/login/+page.svelte +200 -0
  29. agentui/src/routes/talk/[agentId]/+page.svelte +297 -0
  30. agentui/src/routes/talk/[agentId]/+page.ts +7 -0
  31. agentui/static/README.md +1 -0
  32. agentui/svelte.config.js +11 -0
  33. agentui/tailwind.config.ts +53 -0
  34. agentui/tsconfig.json +3 -0
  35. agentui/vite.config.ts +10 -0
  36. ai_parrot-0.17.2.dist-info/METADATA +472 -0
  37. ai_parrot-0.17.2.dist-info/RECORD +535 -0
  38. ai_parrot-0.17.2.dist-info/WHEEL +6 -0
  39. ai_parrot-0.17.2.dist-info/entry_points.txt +2 -0
  40. ai_parrot-0.17.2.dist-info/licenses/LICENSE +21 -0
  41. ai_parrot-0.17.2.dist-info/top_level.txt +6 -0
  42. crew-builder/.prettierrc +15 -0
  43. crew-builder/QUICKSTART.md +259 -0
  44. crew-builder/README.md +113 -0
  45. crew-builder/env.example +17 -0
  46. crew-builder/jsconfig.json +14 -0
  47. crew-builder/package-lock.json +4182 -0
  48. crew-builder/package.json +37 -0
  49. crew-builder/scripts/postinstall/apply-patches.mjs +260 -0
  50. crew-builder/src/app.css +62 -0
  51. crew-builder/src/app.d.ts +13 -0
  52. crew-builder/src/app.html +12 -0
  53. crew-builder/src/components/LoadingSpinner.svelte +64 -0
  54. crew-builder/src/components/ThemeSwitcher.svelte +149 -0
  55. crew-builder/src/components/index.js +9 -0
  56. crew-builder/src/lib/api/bots.ts +60 -0
  57. crew-builder/src/lib/api/chat.ts +80 -0
  58. crew-builder/src/lib/api/client.ts +56 -0
  59. crew-builder/src/lib/api/crew/crew.ts +136 -0
  60. crew-builder/src/lib/api/index.ts +5 -0
  61. crew-builder/src/lib/api/o365/auth.ts +65 -0
  62. crew-builder/src/lib/auth/auth.ts +54 -0
  63. crew-builder/src/lib/components/AgentNode.svelte +43 -0
  64. crew-builder/src/lib/components/BotCard.svelte +33 -0
  65. crew-builder/src/lib/components/ChatBubble.svelte +67 -0
  66. crew-builder/src/lib/components/ConfigPanel.svelte +278 -0
  67. crew-builder/src/lib/components/JsonTreeNode.svelte +76 -0
  68. crew-builder/src/lib/components/JsonViewer.svelte +24 -0
  69. crew-builder/src/lib/components/MarkdownEditor.svelte +48 -0
  70. crew-builder/src/lib/components/ThemeToggle.svelte +36 -0
  71. crew-builder/src/lib/components/Toast.svelte +67 -0
  72. crew-builder/src/lib/components/Toolbar.svelte +157 -0
  73. crew-builder/src/lib/components/index.ts +10 -0
  74. crew-builder/src/lib/config.ts +8 -0
  75. crew-builder/src/lib/stores/auth.svelte.ts +228 -0
  76. crew-builder/src/lib/stores/crewStore.ts +369 -0
  77. crew-builder/src/lib/stores/theme.svelte.js +145 -0
  78. crew-builder/src/lib/stores/toast.svelte.ts +69 -0
  79. crew-builder/src/lib/utils/conversation.ts +39 -0
  80. crew-builder/src/lib/utils/markdown.ts +122 -0
  81. crew-builder/src/lib/utils/talkHistory.ts +47 -0
  82. crew-builder/src/routes/+layout.svelte +20 -0
  83. crew-builder/src/routes/+page.svelte +539 -0
  84. crew-builder/src/routes/agents/+page.svelte +247 -0
  85. crew-builder/src/routes/agents/[agentId]/+page.svelte +288 -0
  86. crew-builder/src/routes/agents/[agentId]/+page.ts +7 -0
  87. crew-builder/src/routes/builder/+page.svelte +204 -0
  88. crew-builder/src/routes/crew/ask/+page.svelte +1052 -0
  89. crew-builder/src/routes/crew/ask/+page.ts +1 -0
  90. crew-builder/src/routes/integrations/o365/+page.svelte +304 -0
  91. crew-builder/src/routes/login/+page.svelte +197 -0
  92. crew-builder/src/routes/talk/[agentId]/+page.svelte +487 -0
  93. crew-builder/src/routes/talk/[agentId]/+page.ts +7 -0
  94. crew-builder/static/README.md +1 -0
  95. crew-builder/svelte.config.js +11 -0
  96. crew-builder/tailwind.config.ts +53 -0
  97. crew-builder/tsconfig.json +3 -0
  98. crew-builder/vite.config.ts +10 -0
  99. mcp_servers/calculator_server.py +309 -0
  100. parrot/__init__.py +27 -0
  101. parrot/__pycache__/__init__.cpython-310.pyc +0 -0
  102. parrot/__pycache__/version.cpython-310.pyc +0 -0
  103. parrot/_version.py +34 -0
  104. parrot/a2a/__init__.py +48 -0
  105. parrot/a2a/client.py +658 -0
  106. parrot/a2a/discovery.py +89 -0
  107. parrot/a2a/mixin.py +257 -0
  108. parrot/a2a/models.py +376 -0
  109. parrot/a2a/server.py +770 -0
  110. parrot/agents/__init__.py +29 -0
  111. parrot/bots/__init__.py +12 -0
  112. parrot/bots/a2a_agent.py +19 -0
  113. parrot/bots/abstract.py +3139 -0
  114. parrot/bots/agent.py +1129 -0
  115. parrot/bots/basic.py +9 -0
  116. parrot/bots/chatbot.py +669 -0
  117. parrot/bots/data.py +1618 -0
  118. parrot/bots/database/__init__.py +5 -0
  119. parrot/bots/database/abstract.py +3071 -0
  120. parrot/bots/database/cache.py +286 -0
  121. parrot/bots/database/models.py +468 -0
  122. parrot/bots/database/prompts.py +154 -0
  123. parrot/bots/database/retries.py +98 -0
  124. parrot/bots/database/router.py +269 -0
  125. parrot/bots/database/sql.py +41 -0
  126. parrot/bots/db/__init__.py +6 -0
  127. parrot/bots/db/abstract.py +556 -0
  128. parrot/bots/db/bigquery.py +602 -0
  129. parrot/bots/db/cache.py +85 -0
  130. parrot/bots/db/documentdb.py +668 -0
  131. parrot/bots/db/elastic.py +1014 -0
  132. parrot/bots/db/influx.py +898 -0
  133. parrot/bots/db/mock.py +96 -0
  134. parrot/bots/db/multi.py +783 -0
  135. parrot/bots/db/prompts.py +185 -0
  136. parrot/bots/db/sql.py +1255 -0
  137. parrot/bots/db/tools.py +212 -0
  138. parrot/bots/document.py +680 -0
  139. parrot/bots/hrbot.py +15 -0
  140. parrot/bots/kb.py +170 -0
  141. parrot/bots/mcp.py +36 -0
  142. parrot/bots/orchestration/README.md +463 -0
  143. parrot/bots/orchestration/__init__.py +1 -0
  144. parrot/bots/orchestration/agent.py +155 -0
  145. parrot/bots/orchestration/crew.py +3330 -0
  146. parrot/bots/orchestration/fsm.py +1179 -0
  147. parrot/bots/orchestration/hr.py +434 -0
  148. parrot/bots/orchestration/storage/__init__.py +4 -0
  149. parrot/bots/orchestration/storage/memory.py +100 -0
  150. parrot/bots/orchestration/storage/mixin.py +119 -0
  151. parrot/bots/orchestration/verify.py +202 -0
  152. parrot/bots/product.py +204 -0
  153. parrot/bots/prompts/__init__.py +96 -0
  154. parrot/bots/prompts/agents.py +155 -0
  155. parrot/bots/prompts/data.py +216 -0
  156. parrot/bots/prompts/output_generation.py +8 -0
  157. parrot/bots/scraper/__init__.py +3 -0
  158. parrot/bots/scraper/models.py +122 -0
  159. parrot/bots/scraper/scraper.py +1173 -0
  160. parrot/bots/scraper/templates.py +115 -0
  161. parrot/bots/stores/__init__.py +5 -0
  162. parrot/bots/stores/local.py +172 -0
  163. parrot/bots/webdev.py +81 -0
  164. parrot/cli.py +17 -0
  165. parrot/clients/__init__.py +16 -0
  166. parrot/clients/base.py +1491 -0
  167. parrot/clients/claude.py +1191 -0
  168. parrot/clients/factory.py +129 -0
  169. parrot/clients/google.py +4567 -0
  170. parrot/clients/gpt.py +1975 -0
  171. parrot/clients/grok.py +432 -0
  172. parrot/clients/groq.py +986 -0
  173. parrot/clients/hf.py +582 -0
  174. parrot/clients/models.py +18 -0
  175. parrot/conf.py +395 -0
  176. parrot/embeddings/__init__.py +9 -0
  177. parrot/embeddings/base.py +157 -0
  178. parrot/embeddings/google.py +98 -0
  179. parrot/embeddings/huggingface.py +74 -0
  180. parrot/embeddings/openai.py +84 -0
  181. parrot/embeddings/processor.py +88 -0
  182. parrot/exceptions.c +13868 -0
  183. parrot/exceptions.cpython-310-x86_64-linux-gnu.so +0 -0
  184. parrot/exceptions.pxd +22 -0
  185. parrot/exceptions.pxi +15 -0
  186. parrot/exceptions.pyx +44 -0
  187. parrot/generators/__init__.py +29 -0
  188. parrot/generators/base.py +200 -0
  189. parrot/generators/html.py +293 -0
  190. parrot/generators/react.py +205 -0
  191. parrot/generators/streamlit.py +203 -0
  192. parrot/generators/template.py +105 -0
  193. parrot/handlers/__init__.py +4 -0
  194. parrot/handlers/agent.py +861 -0
  195. parrot/handlers/agents/__init__.py +1 -0
  196. parrot/handlers/agents/abstract.py +900 -0
  197. parrot/handlers/bots.py +338 -0
  198. parrot/handlers/chat.py +915 -0
  199. parrot/handlers/creation.sql +192 -0
  200. parrot/handlers/crew/ARCHITECTURE.md +362 -0
  201. parrot/handlers/crew/README_BOTMANAGER_PERSISTENCE.md +303 -0
  202. parrot/handlers/crew/README_REDIS_PERSISTENCE.md +366 -0
  203. parrot/handlers/crew/__init__.py +0 -0
  204. parrot/handlers/crew/handler.py +801 -0
  205. parrot/handlers/crew/models.py +229 -0
  206. parrot/handlers/crew/redis_persistence.py +523 -0
  207. parrot/handlers/jobs/__init__.py +10 -0
  208. parrot/handlers/jobs/job.py +384 -0
  209. parrot/handlers/jobs/mixin.py +627 -0
  210. parrot/handlers/jobs/models.py +115 -0
  211. parrot/handlers/jobs/worker.py +31 -0
  212. parrot/handlers/models.py +596 -0
  213. parrot/handlers/o365_auth.py +105 -0
  214. parrot/handlers/stream.py +337 -0
  215. parrot/interfaces/__init__.py +6 -0
  216. parrot/interfaces/aws.py +143 -0
  217. parrot/interfaces/credentials.py +113 -0
  218. parrot/interfaces/database.py +27 -0
  219. parrot/interfaces/google.py +1123 -0
  220. parrot/interfaces/hierarchy.py +1227 -0
  221. parrot/interfaces/http.py +651 -0
  222. parrot/interfaces/images/__init__.py +0 -0
  223. parrot/interfaces/images/plugins/__init__.py +24 -0
  224. parrot/interfaces/images/plugins/abstract.py +58 -0
  225. parrot/interfaces/images/plugins/analisys.py +148 -0
  226. parrot/interfaces/images/plugins/classify.py +150 -0
  227. parrot/interfaces/images/plugins/classifybase.py +182 -0
  228. parrot/interfaces/images/plugins/detect.py +150 -0
  229. parrot/interfaces/images/plugins/exif.py +1103 -0
  230. parrot/interfaces/images/plugins/hash.py +52 -0
  231. parrot/interfaces/images/plugins/vision.py +104 -0
  232. parrot/interfaces/images/plugins/yolo.py +66 -0
  233. parrot/interfaces/images/plugins/zerodetect.py +197 -0
  234. parrot/interfaces/o365.py +978 -0
  235. parrot/interfaces/onedrive.py +822 -0
  236. parrot/interfaces/sharepoint.py +1435 -0
  237. parrot/interfaces/soap.py +257 -0
  238. parrot/loaders/__init__.py +8 -0
  239. parrot/loaders/abstract.py +1131 -0
  240. parrot/loaders/audio.py +199 -0
  241. parrot/loaders/basepdf.py +53 -0
  242. parrot/loaders/basevideo.py +1568 -0
  243. parrot/loaders/csv.py +409 -0
  244. parrot/loaders/docx.py +116 -0
  245. parrot/loaders/epubloader.py +316 -0
  246. parrot/loaders/excel.py +199 -0
  247. parrot/loaders/factory.py +55 -0
  248. parrot/loaders/files/__init__.py +0 -0
  249. parrot/loaders/files/abstract.py +39 -0
  250. parrot/loaders/files/html.py +26 -0
  251. parrot/loaders/files/text.py +63 -0
  252. parrot/loaders/html.py +152 -0
  253. parrot/loaders/markdown.py +442 -0
  254. parrot/loaders/pdf.py +373 -0
  255. parrot/loaders/pdfmark.py +320 -0
  256. parrot/loaders/pdftables.py +506 -0
  257. parrot/loaders/ppt.py +476 -0
  258. parrot/loaders/qa.py +63 -0
  259. parrot/loaders/splitters/__init__.py +10 -0
  260. parrot/loaders/splitters/base.py +138 -0
  261. parrot/loaders/splitters/md.py +228 -0
  262. parrot/loaders/splitters/token.py +143 -0
  263. parrot/loaders/txt.py +26 -0
  264. parrot/loaders/video.py +89 -0
  265. parrot/loaders/videolocal.py +218 -0
  266. parrot/loaders/videounderstanding.py +377 -0
  267. parrot/loaders/vimeo.py +167 -0
  268. parrot/loaders/web.py +599 -0
  269. parrot/loaders/youtube.py +504 -0
  270. parrot/manager/__init__.py +5 -0
  271. parrot/manager/manager.py +1030 -0
  272. parrot/mcp/__init__.py +28 -0
  273. parrot/mcp/adapter.py +105 -0
  274. parrot/mcp/cli.py +174 -0
  275. parrot/mcp/client.py +119 -0
  276. parrot/mcp/config.py +75 -0
  277. parrot/mcp/integration.py +842 -0
  278. parrot/mcp/oauth.py +933 -0
  279. parrot/mcp/server.py +225 -0
  280. parrot/mcp/transports/__init__.py +3 -0
  281. parrot/mcp/transports/base.py +279 -0
  282. parrot/mcp/transports/grpc_session.py +163 -0
  283. parrot/mcp/transports/http.py +312 -0
  284. parrot/mcp/transports/mcp.proto +108 -0
  285. parrot/mcp/transports/quic.py +1082 -0
  286. parrot/mcp/transports/sse.py +330 -0
  287. parrot/mcp/transports/stdio.py +309 -0
  288. parrot/mcp/transports/unix.py +395 -0
  289. parrot/mcp/transports/websocket.py +547 -0
  290. parrot/memory/__init__.py +16 -0
  291. parrot/memory/abstract.py +209 -0
  292. parrot/memory/agent.py +32 -0
  293. parrot/memory/cache.py +175 -0
  294. parrot/memory/core.py +555 -0
  295. parrot/memory/file.py +153 -0
  296. parrot/memory/mem.py +131 -0
  297. parrot/memory/redis.py +613 -0
  298. parrot/models/__init__.py +46 -0
  299. parrot/models/basic.py +118 -0
  300. parrot/models/compliance.py +208 -0
  301. parrot/models/crew.py +395 -0
  302. parrot/models/detections.py +654 -0
  303. parrot/models/generation.py +85 -0
  304. parrot/models/google.py +223 -0
  305. parrot/models/groq.py +23 -0
  306. parrot/models/openai.py +30 -0
  307. parrot/models/outputs.py +285 -0
  308. parrot/models/responses.py +938 -0
  309. parrot/notifications/__init__.py +743 -0
  310. parrot/openapi/__init__.py +3 -0
  311. parrot/openapi/components.yaml +641 -0
  312. parrot/openapi/config.py +322 -0
  313. parrot/outputs/__init__.py +32 -0
  314. parrot/outputs/formats/__init__.py +108 -0
  315. parrot/outputs/formats/altair.py +359 -0
  316. parrot/outputs/formats/application.py +122 -0
  317. parrot/outputs/formats/base.py +351 -0
  318. parrot/outputs/formats/bokeh.py +356 -0
  319. parrot/outputs/formats/card.py +424 -0
  320. parrot/outputs/formats/chart.py +436 -0
  321. parrot/outputs/formats/d3.py +255 -0
  322. parrot/outputs/formats/echarts.py +310 -0
  323. parrot/outputs/formats/generators/__init__.py +0 -0
  324. parrot/outputs/formats/generators/abstract.py +61 -0
  325. parrot/outputs/formats/generators/panel.py +145 -0
  326. parrot/outputs/formats/generators/streamlit.py +86 -0
  327. parrot/outputs/formats/generators/terminal.py +63 -0
  328. parrot/outputs/formats/holoviews.py +310 -0
  329. parrot/outputs/formats/html.py +147 -0
  330. parrot/outputs/formats/jinja2.py +46 -0
  331. parrot/outputs/formats/json.py +87 -0
  332. parrot/outputs/formats/map.py +933 -0
  333. parrot/outputs/formats/markdown.py +172 -0
  334. parrot/outputs/formats/matplotlib.py +237 -0
  335. parrot/outputs/formats/mixins/__init__.py +0 -0
  336. parrot/outputs/formats/mixins/emaps.py +855 -0
  337. parrot/outputs/formats/plotly.py +341 -0
  338. parrot/outputs/formats/seaborn.py +310 -0
  339. parrot/outputs/formats/table.py +397 -0
  340. parrot/outputs/formats/template_report.py +138 -0
  341. parrot/outputs/formats/yaml.py +125 -0
  342. parrot/outputs/formatter.py +152 -0
  343. parrot/outputs/templates/__init__.py +95 -0
  344. parrot/pipelines/__init__.py +0 -0
  345. parrot/pipelines/abstract.py +210 -0
  346. parrot/pipelines/detector.py +124 -0
  347. parrot/pipelines/models.py +90 -0
  348. parrot/pipelines/planogram.py +3002 -0
  349. parrot/pipelines/table.sql +97 -0
  350. parrot/plugins/__init__.py +106 -0
  351. parrot/plugins/importer.py +80 -0
  352. parrot/py.typed +0 -0
  353. parrot/registry/__init__.py +18 -0
  354. parrot/registry/registry.py +594 -0
  355. parrot/scheduler/__init__.py +1189 -0
  356. parrot/scheduler/models.py +60 -0
  357. parrot/security/__init__.py +16 -0
  358. parrot/security/prompt_injection.py +268 -0
  359. parrot/security/security_events.sql +25 -0
  360. parrot/services/__init__.py +1 -0
  361. parrot/services/mcp/__init__.py +8 -0
  362. parrot/services/mcp/config.py +13 -0
  363. parrot/services/mcp/server.py +295 -0
  364. parrot/services/o365_remote_auth.py +235 -0
  365. parrot/stores/__init__.py +7 -0
  366. parrot/stores/abstract.py +352 -0
  367. parrot/stores/arango.py +1090 -0
  368. parrot/stores/bigquery.py +1377 -0
  369. parrot/stores/cache.py +106 -0
  370. parrot/stores/empty.py +10 -0
  371. parrot/stores/faiss_store.py +1157 -0
  372. parrot/stores/kb/__init__.py +9 -0
  373. parrot/stores/kb/abstract.py +68 -0
  374. parrot/stores/kb/cache.py +165 -0
  375. parrot/stores/kb/doc.py +325 -0
  376. parrot/stores/kb/hierarchy.py +346 -0
  377. parrot/stores/kb/local.py +457 -0
  378. parrot/stores/kb/prompt.py +28 -0
  379. parrot/stores/kb/redis.py +659 -0
  380. parrot/stores/kb/store.py +115 -0
  381. parrot/stores/kb/user.py +374 -0
  382. parrot/stores/models.py +59 -0
  383. parrot/stores/pgvector.py +3 -0
  384. parrot/stores/postgres.py +2853 -0
  385. parrot/stores/utils/__init__.py +0 -0
  386. parrot/stores/utils/chunking.py +197 -0
  387. parrot/telemetry/__init__.py +3 -0
  388. parrot/telemetry/mixin.py +111 -0
  389. parrot/template/__init__.py +3 -0
  390. parrot/template/engine.py +259 -0
  391. parrot/tools/__init__.py +23 -0
  392. parrot/tools/abstract.py +644 -0
  393. parrot/tools/agent.py +363 -0
  394. parrot/tools/arangodbsearch.py +537 -0
  395. parrot/tools/arxiv_tool.py +188 -0
  396. parrot/tools/calculator/__init__.py +3 -0
  397. parrot/tools/calculator/operations/__init__.py +38 -0
  398. parrot/tools/calculator/operations/calculus.py +80 -0
  399. parrot/tools/calculator/operations/statistics.py +76 -0
  400. parrot/tools/calculator/tool.py +150 -0
  401. parrot/tools/cloudwatch.py +988 -0
  402. parrot/tools/codeinterpreter/__init__.py +127 -0
  403. parrot/tools/codeinterpreter/executor.py +371 -0
  404. parrot/tools/codeinterpreter/internals.py +473 -0
  405. parrot/tools/codeinterpreter/models.py +643 -0
  406. parrot/tools/codeinterpreter/prompts.py +224 -0
  407. parrot/tools/codeinterpreter/tool.py +664 -0
  408. parrot/tools/company_info/__init__.py +6 -0
  409. parrot/tools/company_info/tool.py +1138 -0
  410. parrot/tools/correlationanalysis.py +437 -0
  411. parrot/tools/database/abstract.py +286 -0
  412. parrot/tools/database/bq.py +115 -0
  413. parrot/tools/database/cache.py +284 -0
  414. parrot/tools/database/models.py +95 -0
  415. parrot/tools/database/pg.py +343 -0
  416. parrot/tools/databasequery.py +1159 -0
  417. parrot/tools/db.py +1800 -0
  418. parrot/tools/ddgo.py +370 -0
  419. parrot/tools/decorators.py +271 -0
  420. parrot/tools/dftohtml.py +282 -0
  421. parrot/tools/document.py +549 -0
  422. parrot/tools/ecs.py +819 -0
  423. parrot/tools/edareport.py +368 -0
  424. parrot/tools/elasticsearch.py +1049 -0
  425. parrot/tools/employees.py +462 -0
  426. parrot/tools/epson/__init__.py +96 -0
  427. parrot/tools/excel.py +683 -0
  428. parrot/tools/file/__init__.py +13 -0
  429. parrot/tools/file/abstract.py +76 -0
  430. parrot/tools/file/gcs.py +378 -0
  431. parrot/tools/file/local.py +284 -0
  432. parrot/tools/file/s3.py +511 -0
  433. parrot/tools/file/tmp.py +309 -0
  434. parrot/tools/file/tool.py +501 -0
  435. parrot/tools/file_reader.py +129 -0
  436. parrot/tools/flowtask/__init__.py +19 -0
  437. parrot/tools/flowtask/tool.py +761 -0
  438. parrot/tools/gittoolkit.py +508 -0
  439. parrot/tools/google/__init__.py +18 -0
  440. parrot/tools/google/base.py +169 -0
  441. parrot/tools/google/tools.py +1251 -0
  442. parrot/tools/googlelocation.py +5 -0
  443. parrot/tools/googleroutes.py +5 -0
  444. parrot/tools/googlesearch.py +5 -0
  445. parrot/tools/googlesitesearch.py +5 -0
  446. parrot/tools/googlevoice.py +2 -0
  447. parrot/tools/gvoice.py +695 -0
  448. parrot/tools/ibisworld/README.md +225 -0
  449. parrot/tools/ibisworld/__init__.py +11 -0
  450. parrot/tools/ibisworld/tool.py +366 -0
  451. parrot/tools/jiratoolkit.py +1718 -0
  452. parrot/tools/manager.py +1098 -0
  453. parrot/tools/math.py +152 -0
  454. parrot/tools/metadata.py +476 -0
  455. parrot/tools/msteams.py +1621 -0
  456. parrot/tools/msword.py +635 -0
  457. parrot/tools/multidb.py +580 -0
  458. parrot/tools/multistoresearch.py +369 -0
  459. parrot/tools/networkninja.py +167 -0
  460. parrot/tools/nextstop/__init__.py +4 -0
  461. parrot/tools/nextstop/base.py +286 -0
  462. parrot/tools/nextstop/employee.py +733 -0
  463. parrot/tools/nextstop/store.py +462 -0
  464. parrot/tools/notification.py +435 -0
  465. parrot/tools/o365/__init__.py +42 -0
  466. parrot/tools/o365/base.py +295 -0
  467. parrot/tools/o365/bundle.py +522 -0
  468. parrot/tools/o365/events.py +554 -0
  469. parrot/tools/o365/mail.py +992 -0
  470. parrot/tools/o365/onedrive.py +497 -0
  471. parrot/tools/o365/sharepoint.py +641 -0
  472. parrot/tools/openapi_toolkit.py +904 -0
  473. parrot/tools/openweather.py +527 -0
  474. parrot/tools/pdfprint.py +1001 -0
  475. parrot/tools/powerbi.py +518 -0
  476. parrot/tools/powerpoint.py +1113 -0
  477. parrot/tools/pricestool.py +146 -0
  478. parrot/tools/products/__init__.py +246 -0
  479. parrot/tools/prophet_tool.py +171 -0
  480. parrot/tools/pythonpandas.py +630 -0
  481. parrot/tools/pythonrepl.py +910 -0
  482. parrot/tools/qsource.py +436 -0
  483. parrot/tools/querytoolkit.py +395 -0
  484. parrot/tools/quickeda.py +827 -0
  485. parrot/tools/resttool.py +553 -0
  486. parrot/tools/retail/__init__.py +0 -0
  487. parrot/tools/retail/bby.py +528 -0
  488. parrot/tools/sandboxtool.py +703 -0
  489. parrot/tools/sassie/__init__.py +352 -0
  490. parrot/tools/scraping/__init__.py +7 -0
  491. parrot/tools/scraping/docs/select.md +466 -0
  492. parrot/tools/scraping/documentation.md +1278 -0
  493. parrot/tools/scraping/driver.py +436 -0
  494. parrot/tools/scraping/models.py +576 -0
  495. parrot/tools/scraping/options.py +85 -0
  496. parrot/tools/scraping/orchestrator.py +517 -0
  497. parrot/tools/scraping/readme.md +740 -0
  498. parrot/tools/scraping/tool.py +3115 -0
  499. parrot/tools/seasonaldetection.py +642 -0
  500. parrot/tools/shell_tool/__init__.py +5 -0
  501. parrot/tools/shell_tool/actions.py +408 -0
  502. parrot/tools/shell_tool/engine.py +155 -0
  503. parrot/tools/shell_tool/models.py +322 -0
  504. parrot/tools/shell_tool/tool.py +442 -0
  505. parrot/tools/site_search.py +214 -0
  506. parrot/tools/textfile.py +418 -0
  507. parrot/tools/think.py +378 -0
  508. parrot/tools/toolkit.py +298 -0
  509. parrot/tools/webapp_tool.py +187 -0
  510. parrot/tools/whatif.py +1279 -0
  511. parrot/tools/workday/MULTI_WSDL_EXAMPLE.md +249 -0
  512. parrot/tools/workday/__init__.py +6 -0
  513. parrot/tools/workday/models.py +1389 -0
  514. parrot/tools/workday/tool.py +1293 -0
  515. parrot/tools/yfinance_tool.py +306 -0
  516. parrot/tools/zipcode.py +217 -0
  517. parrot/utils/__init__.py +2 -0
  518. parrot/utils/helpers.py +73 -0
  519. parrot/utils/parsers/__init__.py +5 -0
  520. parrot/utils/parsers/toml.c +12078 -0
  521. parrot/utils/parsers/toml.cpython-310-x86_64-linux-gnu.so +0 -0
  522. parrot/utils/parsers/toml.pyx +21 -0
  523. parrot/utils/toml.py +11 -0
  524. parrot/utils/types.cpp +20936 -0
  525. parrot/utils/types.cpython-310-x86_64-linux-gnu.so +0 -0
  526. parrot/utils/types.pyx +213 -0
  527. parrot/utils/uv.py +11 -0
  528. parrot/version.py +10 -0
  529. parrot/yaml-rs/Cargo.lock +350 -0
  530. parrot/yaml-rs/Cargo.toml +19 -0
  531. parrot/yaml-rs/pyproject.toml +19 -0
  532. parrot/yaml-rs/python/yaml_rs/__init__.py +81 -0
  533. parrot/yaml-rs/src/lib.rs +222 -0
  534. requirements/docker-compose.yml +24 -0
  535. 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