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