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,1227 @@
1
+ """Utilities for managing the employee hierarchy stored in ArangoDB."""
2
+ from __future__ import annotations
3
+ import asyncio
4
+ from typing import List, Dict, Optional, Any, TypeVar, ParamSpec
5
+ import logging
6
+ from dataclasses import dataclass
7
+ from xmlrpc import client
8
+ # from arango import ArangoClient
9
+ from arangoasync import ArangoClient
10
+ from arangoasync.auth import Auth
11
+ from asyncdb import AsyncDB
12
+ from ..conf import default_dsn, EMPLOYEES_TABLE
13
+ from ..memory.cache import CacheMixin, cached_query
14
+
15
+ P = ParamSpec('P')
16
+ T = TypeVar('T')
17
+
18
+
19
+ logging.getLogger('arangoasync').setLevel(logging.WARNING)
20
+
21
+
22
+ @dataclass
23
+ class Employee:
24
+ """Employee Information"""
25
+ employee_id: str
26
+ associate_oid: str
27
+ first_name: str
28
+ last_name: str
29
+ display_name: str
30
+ email: str
31
+ job_code: str
32
+ position_id: str
33
+ department: str
34
+ program: str
35
+ reports_to: Optional[str]
36
+
37
+ class EmployeeHierarchyManager(CacheMixin):
38
+ """
39
+ Hierarchy Manager using ArangoDB to store employees and their reporting structure.
40
+ It supports importing from PostgreSQL, inserting individual employees,
41
+ and performing hierarchical queries like finding superiors, subordinates, and colleagues.
42
+
43
+ Attributes:
44
+ arango_host (str): Hostname for ArangoDB server.
45
+ arango_port (int): Port for ArangoDB server.
46
+ db_name (str): Name of the ArangoDB database to use.
47
+ username (str): Username for ArangoDB authentication.
48
+ password (str): Password for ArangoDB authentication.
49
+ employees_collection (str): Name of the collection for employee vertices.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ arango_host='localhost',
55
+ arango_port=8529,
56
+ db_name='company_db',
57
+ username='root',
58
+ password='',
59
+ **kwargs
60
+ ):
61
+ super().__init__(**kwargs)
62
+ # ArangoDB connection
63
+ self.client = ArangoClient(
64
+ hosts=f'http://{arango_host}:{arango_port}'
65
+ )
66
+ self.auth = Auth(
67
+ username=username,
68
+ password=password
69
+ )
70
+ self._username = username
71
+ self._password = password
72
+ self._database = db_name
73
+ self.sys_db = None
74
+ self.db = None
75
+ # Nombres de colecciones
76
+ self.employees_collection = kwargs.get('employees_collection', 'employees')
77
+ self.reports_to_collection = kwargs.get('reports_to_collection', 'reports_to')
78
+ self.graph_name = kwargs.get('graph_name', 'org_hierarchy')
79
+ self._primary_key = kwargs.get('primary_key', 'employee_id')
80
+
81
+ # postgreSQL connection:
82
+ self.pg_client = AsyncDB('pg', dsn=default_dsn)
83
+ # postgreSQL employees table:
84
+ self.employees_table = kwargs.get(
85
+ 'pg_employees_table',
86
+ EMPLOYEES_TABLE
87
+ )
88
+
89
+ async def __aenter__(self):
90
+ await self.connection()
91
+ return self
92
+
93
+ async def __aexit__(self, exc_type, exc_value, traceback):
94
+ if self.client:
95
+ try:
96
+ await self.client.close()
97
+ except Exception as e:
98
+ print(f"Error closing ArangoDB client: {e}")
99
+
100
+ async def connection(self):
101
+ """
102
+ Async context manager for ArangoDB connection
103
+ """
104
+ # Connect to "_system" database as root user.
105
+ self.sys_db = await self.client.db("_system", auth=self.auth)
106
+ if not await self.sys_db.has_database(self._database):
107
+ await self.sys_db.create_database(self._database)
108
+
109
+ # Connect To database:
110
+ self.db = await self.client.db(self._database, auth=self.auth)
111
+ return self.db
112
+
113
+ async def _setup_collections(self):
114
+ """
115
+ Creates the Collection and Graph structure in ArangoDB if they do not exist.
116
+ """
117
+ # 1. Create Employees collection (vertices)
118
+ if not await self.db.has_collection(self.employees_collection):
119
+ await self.db.create_collection(self.employees_collection)
120
+ print(f"✓ Collection '{self.employees_collection}' created")
121
+
122
+ # 2. Create ReportsTo collection (edges)
123
+ if not await self.db.has_collection(self.reports_to_collection):
124
+ await self.db.create_collection(self.reports_to_collection, edge=True)
125
+ print(f"✓ Collection of edges '{self.reports_to_collection}' created")
126
+
127
+ # 3. Create the graph
128
+ if not await self.db.has_graph(self.graph_name):
129
+ graph = await self.db.create_graph(self.graph_name)
130
+
131
+ # Define Graph Edge Definitions
132
+ await graph.create_edge_definition(
133
+ edge_collection=self.reports_to_collection,
134
+ from_vertex_collections=[self.employees_collection],
135
+ to_vertex_collections=[self.employees_collection]
136
+ )
137
+ print(f"✓ Graph '{self.graph_name}' created")
138
+
139
+ # 4. Create indexes to optimize searches
140
+ employees = self.db.collection(self.employees_collection)
141
+ await self._ensure_index(employees, [self._primary_key], unique=True)
142
+ await self._ensure_index(employees, ['department', 'program'], unique=False)
143
+ await self._ensure_index(employees, ['position_id'], unique=False)
144
+ await self._ensure_index(employees, ['associate_oid'], unique=False)
145
+
146
+ async def _ensure_index(self, collection, fields: List[str], unique: bool = False):
147
+ """
148
+ Ensures an index exists. If it doesn't, creates it.
149
+ If it exists but with different properties, drops and recreates it.
150
+
151
+ Args:
152
+ collection: The ArangoDB collection
153
+ fields: List of field names for the index
154
+ unique: Whether the index should be unique
155
+ """
156
+ existing_indexes = await collection.indexes()
157
+
158
+ # Check if an index with these exact fields already exists
159
+ for idx in existing_indexes:
160
+ # Skip the primary index (_key)
161
+ if idx['type'] == 'primary':
162
+ continue
163
+
164
+ # Check if this index has the same fields
165
+ if idx['fields'] == fields:
166
+ # Check if uniqueness matches
167
+ if idx.get('unique', False) == unique:
168
+ print(f"✓ Index on {fields} already exists")
169
+ return
170
+ else:
171
+ # Index exists but with different uniqueness - drop it
172
+ print(f"⚠ Dropping existing index on {fields} (uniqueness mismatch)")
173
+ try:
174
+ await collection.delete_index(idx['id'])
175
+ except Exception as e:
176
+ print(f"Warning: Could not drop index {idx['id']}: {e}")
177
+ break
178
+
179
+ # Create an index:
180
+ try:
181
+ await collection.add_index(
182
+ type="persistent",
183
+ fields=fields,
184
+ options={"unique": unique}
185
+ )
186
+ except Exception as e:
187
+ print(f"⚠ Could not create persistent index on {fields}: {e}")
188
+
189
+ # Create a hash index
190
+ try:
191
+ await collection.add_hash_index(fields=fields, unique=unique)
192
+ unique_str = "unique " if unique else ""
193
+ print(f"✓ {unique_str}Index on {fields} created")
194
+ except Exception as e:
195
+ print(f"⚠ Could not create index on {fields}: {e}")
196
+
197
+ async def drop_all_indexes(self):
198
+ """
199
+ Drop all user-defined indexes from the employees collection.
200
+ Useful for troubleshooting or resetting the collection.
201
+ """
202
+ employees = self.db.collection(self.employees_collection)
203
+ existing_indexes = await employees.indexes()
204
+
205
+ dropped_count = 0
206
+ for idx in existing_indexes:
207
+ # Skip the primary index (_key) - it cannot be dropped
208
+ if idx['type'] == 'primary':
209
+ continue
210
+
211
+ try:
212
+ await employees.delete_index(idx['id'])
213
+ print(f"✓ Dropped index: {idx['fields']}")
214
+ dropped_count += 1
215
+ except Exception as e:
216
+ print(f"⚠ Could not drop index {idx['id']}: {e}")
217
+
218
+ print(f"✓ Dropped {dropped_count} indexes")
219
+ return dropped_count
220
+
221
+ async def import_from_postgres(self):
222
+ """
223
+ Import employees from PostgreSQL
224
+
225
+ Args:
226
+ pg_conn_string: Connection string for PostgreSQL
227
+ e.g. "dbname=mydb user=user password=pass host=localhost"
228
+ """
229
+ query = f"""
230
+ SELECT
231
+ associate_id as employee_id,
232
+ associate_oid,
233
+ first_name,
234
+ last_name,
235
+ display_name,
236
+ job_code,
237
+ position_id,
238
+ corporate_email as email,
239
+ department,
240
+ reports_to_associate_id as reports_to,
241
+ region as program
242
+ FROM {self.employees_table}
243
+ WHERE status = 'Active'
244
+ ORDER BY reports_to_associate_id NULLS FIRST
245
+ """
246
+ async with await self.pg_client.connection() as conn: # pylint: disable=E1101 # noqa
247
+ employees_data = await conn.fetchall(query)
248
+
249
+ # cleanup collection before import
250
+ await self.truncate_hierarchy()
251
+
252
+ employees_collection = self.db.collection(self.employees_collection)
253
+ reports_to_collection = self.db.collection(self.reports_to_collection)
254
+
255
+ # Mapping Associate OID to ArangoDB _id
256
+ oid_to_id = {}
257
+ # First Step: insert employees
258
+ for row in employees_data:
259
+ # Clean whitespace from IDs
260
+ _id = row.get(self._primary_key, 'employee_id').strip()
261
+ if isinstance(_id, str):
262
+ _id = _id.strip()
263
+
264
+ reports_to = row['reports_to']
265
+ if isinstance(reports_to, str):
266
+ reports_to = reports_to.strip() if reports_to else None
267
+
268
+ employee_doc = {
269
+ '_key': _id, # Employee_id is the primary key
270
+ self._primary_key: _id,
271
+ 'associate_oid': row['associate_oid'],
272
+ 'first_name': row['first_name'],
273
+ 'last_name': row['last_name'],
274
+ 'display_name': row['display_name'],
275
+ 'email': row['email'],
276
+ 'job_code': row['job_code'],
277
+ 'position_id': row['position_id'],
278
+ 'department': row['department'],
279
+ 'program': row['program'],
280
+ 'reports_to': reports_to
281
+ }
282
+
283
+ result = await employees_collection.insert(
284
+ employee_doc,
285
+ overwrite=True
286
+ )
287
+ oid_to_id[_id] = result['_id']
288
+
289
+ print(f"✓ {len(employees_data)} Employees inserted")
290
+
291
+ # Second pass: create edges (reports_to relationships)
292
+ edges_created = 0
293
+ skipped_edges = 0
294
+ missing_bosses = set()
295
+
296
+ for row in employees_data:
297
+ # Clean whitespace from IDs (consistent with first pass)
298
+ _id = row.get(self._primary_key, 'employee_id').strip()
299
+ if isinstance(_id, str):
300
+ _id = _id.strip()
301
+
302
+ reports_to = row['reports_to']
303
+ if isinstance(reports_to, str):
304
+ reports_to = reports_to.strip() if reports_to else None
305
+
306
+ if reports_to: # If has a boss
307
+ # Verify that the boss exists in the database
308
+ if reports_to not in oid_to_id:
309
+ skipped_edges += 1
310
+ missing_bosses.add(reports_to)
311
+ continue
312
+
313
+ edge_doc = {
314
+ '_from': oid_to_id[_id], # Employee
315
+ '_to': oid_to_id[reports_to], # His boss
316
+ }
317
+
318
+ await reports_to_collection.insert(edge_doc)
319
+ edges_created += 1
320
+
321
+ print(f"✓ {edges_created} 'reports_to' edges created")
322
+ print(
323
+ f"✓ {await self.db.collection(name=self.reports_to_collection).count()} total 'reports_to' edges"
324
+ )
325
+
326
+ if skipped_edges > 0:
327
+ print(f"⚠ {skipped_edges} edges skipped (boss not found)")
328
+ print(f"⚠ Missing boss IDs: {missing_bosses}")
329
+
330
+ # Setup collections and graph
331
+ await self._setup_collections()
332
+
333
+ async def truncate_hierarchy(self) -> None:
334
+ """
335
+ Truncate employees and reports_to collections.
336
+
337
+ This:
338
+ - Deletes all employee vertices.
339
+ - Deletes all reports_to edges.
340
+ - Keeps:
341
+ - Collections
342
+ - Indexes
343
+ - Graph definition
344
+
345
+ Use this when you want a clean reload from PostgreSQL.
346
+ """
347
+ # Truncate edges first (good practice to avoid dangling edges mid-operation)
348
+ if await self.db.has_collection(self.reports_to_collection):
349
+ edges = self.db.collection(self.reports_to_collection)
350
+ await edges.truncate()
351
+
352
+ if await self.db.has_collection(self.employees_collection):
353
+ employees = self.db.collection(self.employees_collection)
354
+ await employees.truncate()
355
+
356
+ print("✓ Hierarchy data truncated (employees + reports_to)")
357
+
358
+
359
+ async def insert_employee(self, employee: Employee) -> str:
360
+ """
361
+ Insert an individual employee
362
+ """
363
+ employees_collection = self.db.collection(name=self.employees_collection)
364
+ reports_to_collection = self.db.collection(name=self.reports_to_collection)
365
+
366
+ # Insert employee
367
+ employee_doc = {
368
+ '_key': employee.employee_id,
369
+ self._primary_key: employee.employee_id,
370
+ 'associate_oid': employee.associate_oid,
371
+ 'first_name': employee.first_name,
372
+ 'last_name': employee.last_name,
373
+ 'display_name': employee.display_name,
374
+ 'email': employee.email,
375
+ 'job_code': employee.job_code,
376
+ 'position_id': employee.position_id,
377
+ 'department': employee.department,
378
+ 'program': employee.program,
379
+ 'reports_to': employee.reports_to
380
+ }
381
+
382
+ result = await employees_collection.insert(employee_doc, overwrite=True)
383
+ employee_id = result['_id']
384
+
385
+ # Crear arista si reporta a alguien
386
+ if employee.reports_to:
387
+ boss_id = f"{self.employees_collection}/{employee.reports_to}"
388
+
389
+ edge_doc = {
390
+ '_from': employee_id,
391
+ '_to': boss_id
392
+ }
393
+
394
+ await reports_to_collection.insert(edge_doc)
395
+
396
+ return employee_id
397
+
398
+ # ============= Hierarchical Queries =============
399
+ # @cached_query("does_report_to", ttl=3600)
400
+ async def does_report_to(self, employee_oid: str, boss_oid: str, limit: int = 1) -> bool:
401
+ """
402
+ Check if employee_oid reports directly or indirectly to boss_oid.
403
+
404
+ Args:
405
+ employee_oid: Associate OID of the employee
406
+ boss_oid: Associate OID of the boss
407
+
408
+ Returns:
409
+ True if employee reports to boss, False otherwise
410
+ """
411
+ query = """
412
+ FOR v, e, p IN 1..10 OUTBOUND
413
+ CONCAT(@collection, '/', @employee_oid)
414
+ GRAPH @graph_name
415
+ FILTER v.employee_id == @boss_oid
416
+ LIMIT @limit
417
+ RETURN true
418
+ """
419
+ cursor = await self.db.aql.execute(
420
+ query,
421
+ bind_vars={
422
+ 'collection': self.employees_collection,
423
+ 'employee_oid': employee_oid,
424
+ 'boss_oid': boss_oid,
425
+ 'graph_name': self.graph_name,
426
+ 'limit': limit
427
+ }
428
+ )
429
+ async with cursor:
430
+ results = [doc async for doc in cursor]
431
+ return len(results) > 0
432
+
433
+ # @cached_query("get_all_superiors", ttl=3600)
434
+ async def get_all_superiors(self, employee_oid: str) -> List[Dict]:
435
+ """
436
+ Return all superiors of an employee up to the CEO.
437
+
438
+ Returns:
439
+ List ordered from direct boss to CEO
440
+ """
441
+ query = """
442
+ FOR v, e, p IN 1..10 OUTBOUND
443
+ CONCAT(@collection, '/', @employee_oid)
444
+ GRAPH @graph_name
445
+ RETURN {
446
+ employee_id: v.employee_id,
447
+ associate_oid: v.associate_oid,
448
+ display_name: v.display_name,
449
+ department: v.department,
450
+ program: v.program,
451
+ level: LENGTH(p.edges)
452
+ }
453
+ """
454
+ cursor = await self.db.aql.execute(
455
+ query,
456
+ bind_vars={
457
+ 'collection': self.employees_collection,
458
+ 'employee_oid': employee_oid,
459
+ 'graph_name': self.graph_name
460
+ }
461
+ )
462
+ async with cursor:
463
+ results = [doc async for doc in cursor]
464
+ return results
465
+
466
+ @cached_query("get_direct_reports", ttl=3600)
467
+ async def get_direct_reports(self, boss_oid: str) -> List[Dict]:
468
+ """
469
+ Return direct reports of a boss
470
+ """
471
+ query = """
472
+ FOR v, e, p IN 1..1 INBOUND
473
+ CONCAT(@collection, '/', @boss_oid)
474
+ GRAPH @graph_name
475
+ RETURN {
476
+ employee_id: v.employee_id,
477
+ associate_oid: v.associate_oid,
478
+ display_name: v.display_name,
479
+ department: v.department,
480
+ program: v.program
481
+ }
482
+ """
483
+
484
+ cursor = await self.db.aql.execute(
485
+ query,
486
+ bind_vars={
487
+ 'collection': self.employees_collection,
488
+ 'boss_oid': boss_oid,
489
+ 'graph_name': self.graph_name
490
+ }
491
+ )
492
+ async with cursor:
493
+ results = [doc async for doc in cursor]
494
+ return results
495
+
496
+ # @cached_query("get_all_subordinates", ttl=3600)
497
+ async def get_all_subordinates(self, boss_oid: str, max_depth: int = 10) -> List[Dict]:
498
+ """
499
+ Return all subordinates (direct and indirect) of a boss
500
+ """
501
+ query = """
502
+ FOR v, e, p IN 1..@max_depth INBOUND
503
+ CONCAT(@collection, '/', @boss_oid)
504
+ GRAPH @graph_name
505
+ RETURN {
506
+ employee_id: v.employee_id,
507
+ associate_oid: v.associate_oid,
508
+ display_name: v.display_name,
509
+ department: v.department,
510
+ program: v.program,
511
+ level: LENGTH(p.edges)
512
+ }
513
+ """
514
+
515
+ cursor = await self.db.aql.execute(
516
+ query,
517
+ bind_vars={
518
+ 'collection': self.employees_collection,
519
+ 'boss_oid': boss_oid,
520
+ 'max_depth': max_depth,
521
+ 'graph_name': self.graph_name
522
+ }
523
+ )
524
+ async with cursor:
525
+ results = [doc async for doc in cursor]
526
+ return results
527
+
528
+ # @cached_query("get_org_chart", ttl=3600)
529
+ async def get_org_chart(self, root_oid: Optional[str] = None) -> Dict:
530
+ """
531
+ Build the complete org chart as a hierarchical tree
532
+
533
+ Args:
534
+ root_oid: If specified, builds the tree from that node
535
+ If None, searches for the CEO (node without boss)
536
+
537
+ Returns:
538
+ Hierarchical tree as a list of dictionaries
539
+ """
540
+ # If no root is specified, search for the CEO
541
+ if root_oid is None:
542
+ query_ceo = """
543
+ FOR emp IN @@collection
544
+ FILTER LENGTH(FOR v IN 1..1 OUTBOUND emp._id GRAPH @graph_name RETURN 1) == 0
545
+ LIMIT 1
546
+ RETURN emp.employee_id
547
+ """
548
+ cursor = await self.db.aql.execute(
549
+ query_ceo,
550
+ bind_vars={
551
+ '@collection': self.employees_collection,
552
+ 'graph_name': self.graph_name
553
+ }
554
+ )
555
+ async with cursor:
556
+ results = [doc async for doc in cursor]
557
+ if results:
558
+ root_oid = results[0]
559
+ else:
560
+ return {}
561
+ # Build the tree from the root_oid recursively
562
+ query = """
563
+ FOR v, e, p IN 0..10 INBOUND
564
+ CONCAT(@collection, '/', @root_oid)
565
+ GRAPH @graph_name
566
+ RETURN {
567
+ employee_id: v.employee_id,
568
+ associate_oid: v.associate_oid,
569
+ display_name: v.display_name,
570
+ department: v.department,
571
+ program: v.program,
572
+ level: LENGTH(p.edges),
573
+ path: p.vertices[*].employee_id
574
+ }
575
+ """
576
+ cursor = await self.db.aql.execute(
577
+ query,
578
+ bind_vars={
579
+ 'collection': self.employees_collection,
580
+ 'root_oid': root_oid,
581
+ 'graph_name': self.graph_name
582
+ }
583
+ )
584
+ async with cursor:
585
+ results = [doc async for doc in cursor]
586
+
587
+ return results
588
+
589
+ @cached_query("get_colleagues", ttl=3600)
590
+ async def get_colleagues(self, employee_oid: str) -> List[Dict[str, Any]]:
591
+ """
592
+ Return colleagues (employees who share the same boss)
593
+
594
+ Args:
595
+ employee_oid: Associate OID of the employee
596
+
597
+ Returns:
598
+ List of colleagues
599
+ """
600
+ query = """
601
+ FOR boss IN 1..1 OUTBOUND
602
+ CONCAT(@collection, '/', @employee_oid)
603
+ GRAPH @graph_name
604
+
605
+ FOR colleague IN 1..1 INBOUND
606
+ boss._id
607
+ GRAPH @graph_name
608
+ FILTER colleague.employee_id != @employee_oid
609
+ RETURN {
610
+ employee_id: colleague.employee_id,
611
+ associate_oid: colleague.associate_oid,
612
+ display_name: colleague.display_name,
613
+ department: colleague.department,
614
+ program: colleague.program
615
+ }
616
+ """
617
+
618
+ cursor = await self.db.aql.execute(
619
+ query,
620
+ bind_vars={
621
+ 'collection': self.employees_collection,
622
+ 'employee_oid': employee_oid,
623
+ 'graph_name': self.graph_name
624
+ }
625
+ )
626
+ async with cursor:
627
+ results = [doc async for doc in cursor]
628
+
629
+ return results
630
+
631
+ @cached_query("get_employee_info", ttl=7200) # Cache por 2 horas
632
+ async def get_employee_info(self, employee_oid: str) -> Optional[Dict]:
633
+ """
634
+ Get detailed information about an employee.
635
+
636
+ Args:
637
+ employee_oid: Employee ID (associate_oid)
638
+
639
+ Returns:
640
+ Dict with employee information or None if not found
641
+ {
642
+
643
+ 'employee_id': str,
644
+ 'associate_oid': str,
645
+ 'display_name': str,
646
+ 'first_name': str,
647
+ 'last_name': str,
648
+ 'email': str,
649
+ 'department': str,
650
+ 'program': str,
651
+ 'position_id': str,
652
+ 'job_code': str
653
+ }
654
+
655
+ Example:
656
+ ```python
657
+ manager = EmployeeHierarchyManager(...)
658
+
659
+ # First call - query ArangoDB
660
+ info = await manager.get_employee_info('E003')
661
+
662
+ # Second call - from Redis cache
663
+ info = await manager.get_employee_info('E003') # ⚡
664
+ ```
665
+ """
666
+ query = """
667
+ FOR emp IN @@collection
668
+ FILTER emp.employee_id == @employee_oid
669
+ LIMIT 1
670
+ RETURN {
671
+ employee_id: emp.employee_id,
672
+ associate_oid: emp.associate_oid,
673
+ display_name: emp.display_name,
674
+ first_name: emp.first_name,
675
+ last_name: emp.last_name,
676
+ email: emp.email,
677
+ department: emp.department,
678
+ program: emp.program,
679
+ position_id: emp.position_id,
680
+ job_code: emp.job_code
681
+ }
682
+ """
683
+
684
+ cursor = await self.db.aql.execute(
685
+ query,
686
+ bind_vars={
687
+ '@collection': self.employees_collection,
688
+ 'employee_oid': employee_oid
689
+ }
690
+ )
691
+ async with cursor:
692
+ results = [doc async for doc in cursor]
693
+
694
+ return results[0] if results else None
695
+
696
+ async def get_department_context(self, employee_oid: str) -> Dict:
697
+ """
698
+ Get a summary of the employee's department context, including
699
+ superiors, colleagues, direct reports, and all subordinates.
700
+ """
701
+ # 1. Get employee info (async)
702
+ employee_info = await self.get_employee_info(employee_oid)
703
+
704
+ if not employee_info:
705
+ # Employee not found
706
+ return {
707
+ 'employee': {'employee_id': employee_oid},
708
+ 'reports_to_chain': [],
709
+ 'colleagues': [],
710
+ 'manages': [],
711
+ 'all_subordinates': [],
712
+ 'department': 'Unknown',
713
+ 'program': 'Unknown',
714
+ 'total_subordinates': 0,
715
+ 'direct_reports_count': 0,
716
+ 'colleagues_count': 0,
717
+ 'reporting_levels': 0
718
+ }
719
+
720
+ # 2. Get superiors (async)
721
+ superiors = await self.get_all_superiors(employee_oid)
722
+
723
+ # 3. Get colleagues (async)
724
+ colleagues = await self.get_colleagues(employee_oid)
725
+
726
+ # 4. Get direct reports (async)
727
+ direct_reports = await self.get_direct_reports(employee_oid)
728
+
729
+ # 5. Get all subordinates (async)
730
+ all_subordinates = await self.get_all_subordinates(employee_oid)
731
+
732
+ return {
733
+ 'employee': {
734
+ 'employee_id': employee_info['employee_id'],
735
+ 'associate_oid': employee_info['associate_oid'],
736
+ 'display_name': employee_info['display_name'],
737
+ 'email': employee_info.get('email'),
738
+ 'position_id': employee_info.get('position_id')
739
+ },
740
+
741
+ 'reports_to_chain': [
742
+ f"{s['display_name']} ({s['department']} - {s['program']})"
743
+ for s in superiors
744
+ ],
745
+
746
+ 'colleagues': [c['display_name'] for c in colleagues],
747
+ 'manages': [r['display_name'] for r in direct_reports],
748
+ 'all_subordinates': all_subordinates,
749
+
750
+ # Usar department/program del empleado directamente
751
+ 'department': employee_info['department'],
752
+ 'program': employee_info['program'],
753
+
754
+ # Stats
755
+ 'total_subordinates': len(all_subordinates),
756
+ 'direct_reports_count': len(direct_reports),
757
+ 'colleagues_count': len(colleagues),
758
+ 'reporting_levels': len(superiors)
759
+ }
760
+
761
+ async def are_in_same_department(self, employee1: str, employee2: str) -> bool:
762
+ """
763
+ Check if two employees are in the same department (broader than colleagues).
764
+
765
+ Args:
766
+ employee1: First employee's ID
767
+ employee2: Second employee's ID
768
+
769
+ Returns:
770
+ True if in same department, False otherwise
771
+ """
772
+ query = """
773
+ LET emp1 = DOCUMENT(CONCAT(@collection, '/emp_', @emp1))
774
+ LET emp2 = DOCUMENT(CONCAT(@collection, '/emp_', @emp2))
775
+
776
+ RETURN {
777
+ same_department: emp1.department == emp2.department,
778
+ same_program: emp1.program == emp2.program,
779
+ employee1: {
780
+ name: emp1.display_name,
781
+ department: emp1.department,
782
+ program: emp1.program
783
+ },
784
+ employee2: {
785
+ name: emp2.display_name,
786
+ department: emp2.department,
787
+ program: emp2.program
788
+ }
789
+ }
790
+ """
791
+
792
+ cursor = await self.db.aql.execute(
793
+ query,
794
+ bind_vars={
795
+ 'collection': self.employees_collection,
796
+ 'emp1': employee1,
797
+ 'emp2': employee2
798
+ }
799
+ )
800
+ async with cursor:
801
+ results = [doc async for doc in cursor]
802
+ result = results[0] if results else {}
803
+ return result.get('same_department', False)
804
+
805
+ async def get_team_members(
806
+ self,
807
+ manager_id: str,
808
+ include_all_levels: bool = False
809
+ ) -> List[Dict[str, Any]]:
810
+ """
811
+ Get all team members under a manager.
812
+
813
+ Args:
814
+ manager_id: Manager's ID
815
+ include_all_levels: If True, include all subordinates recursively.
816
+ If False, only direct reports.
817
+
818
+ Returns:
819
+ List of team member information
820
+ """
821
+ depth = "1..99" if include_all_levels else "1..1"
822
+
823
+ query = f"""
824
+ FOR member, e, p IN {depth} INBOUND CONCAT(@collection, '/emp_', @manager_id)
825
+ GRAPH @graph_name
826
+ RETURN {{
827
+ employee_id: member.employee_id,
828
+ display_name: member.display_name,
829
+ department: member.department,
830
+ program: member.program,
831
+ associate_oid: member.associate_oid,
832
+ level: LENGTH(p.edges),
833
+ reports_directly: LENGTH(p.edges) == 1
834
+ }}
835
+ """
836
+
837
+ cursor = await self.db.aql.execute(
838
+ query,
839
+ bind_vars={
840
+ 'collection': self.employees_collection,
841
+ 'manager_id': manager_id,
842
+ 'graph_name': self.graph_name
843
+ }
844
+ )
845
+ async with cursor:
846
+ results = [doc async for doc in cursor]
847
+
848
+ return results
849
+
850
+ async def are_colleagues(self, employee1: str, employee2: str) -> bool:
851
+ """
852
+ Check if two employees are colleagues (same boss, same level).
853
+
854
+ Two employees are considered colleagues if:
855
+ 1. They have the same direct manager
856
+ 2. They are at the same hierarchical level
857
+ 3. They are not the same person
858
+
859
+ Args:
860
+ employee1: First employee's ID
861
+ employee2: Second employee's ID
862
+
863
+ Returns:
864
+ True if they are colleagues, False otherwise
865
+ """
866
+ if employee1 == employee2:
867
+ return False # Same person cannot be their own colleague
868
+
869
+ # Method 1: Check if they have the same direct boss
870
+ query = """
871
+ // Find the direct boss of employee1
872
+ LET boss1 = (
873
+ FOR v IN 1..1 OUTBOUND CONCAT(@collection, '/', @emp1)
874
+ GRAPH @graph_name
875
+ RETURN v._key
876
+ )
877
+
878
+ // Find the direct boss of employee2
879
+ LET boss2 = (
880
+ FOR v IN 1..1 OUTBOUND CONCAT(@collection, '/', @emp2)
881
+ GRAPH @graph_name
882
+ RETURN v._key
883
+ )
884
+
885
+ // Check if they have the same boss
886
+ RETURN {
887
+ employee1_boss: boss1[0],
888
+ employee2_boss: boss2[0],
889
+ same_boss: boss1[0] == boss2[0] AND boss1[0] != null,
890
+ are_colleagues: boss1[0] == boss2[0] AND boss1[0] != null
891
+ }
892
+ """
893
+
894
+ cursor = await self.db.aql.execute(
895
+ query,
896
+ bind_vars={
897
+ 'collection': self.employees_collection,
898
+ 'emp1': employee1,
899
+ 'emp2': employee2,
900
+ 'graph_name': self.graph_name
901
+ }
902
+ )
903
+ async with cursor:
904
+ results = [doc async for doc in cursor]
905
+ result = results[0] if results else {}
906
+ return result.get('are_colleagues', False)
907
+
908
+ async def is_manager(self, employee_oid: str) -> bool:
909
+ """
910
+ Check if the given employee is a manager (has direct reports).
911
+
912
+ Args:
913
+ employee_oid: Employee ID to check
914
+
915
+ Returns:
916
+ True if the employee is a manager, False otherwise
917
+ """
918
+ query = """
919
+ FOR v IN 1..1 INBOUND
920
+ CONCAT(@collection, '/', @employee_oid)
921
+ GRAPH @graph_name
922
+ LIMIT 1
923
+ RETURN true
924
+ """
925
+
926
+ cursor = await self.db.aql.execute(
927
+ query,
928
+ bind_vars={
929
+ 'collection': self.employees_collection,
930
+ 'employee_oid': employee_oid,
931
+ 'graph_name': self.graph_name
932
+ }
933
+ )
934
+ async with cursor:
935
+ results = [doc async for doc in cursor]
936
+ return len(results) > 0
937
+
938
+ async def get_closest_common_boss(self, employee1: str, employee2: str) -> Optional[Dict]:
939
+ """
940
+ Find the closest common boss between two employees.
941
+
942
+ Args:
943
+ employee1: First employee's ID
944
+ employee2: Second employee's ID
945
+
946
+ Returns:
947
+ Dict with common boss information or None if not found
948
+ """
949
+ query = """
950
+ LET paths1 = (
951
+ FOR v, e, p IN 1..10 OUTBOUND
952
+ CONCAT(@collection, '/', @employee1)
953
+ GRAPH @graph_name
954
+ RETURN {boss: v, path: p}
955
+ )
956
+
957
+ LET paths2 = (
958
+ FOR v, e, p IN 1..10 OUTBOUND
959
+ CONCAT(@collection, '/', @employee2)
960
+ GRAPH @graph_name
961
+ RETURN {boss: v, path: p}
962
+ )
963
+
964
+ FOR p1 IN paths1
965
+ FOR p2 IN paths2
966
+ FILTER p1.boss._key == p2.boss._key
967
+ SORT LENGTH(p1.path.edges) + LENGTH(p2.path.edges) ASC
968
+ LIMIT 1
969
+ RETURN {
970
+ employee_id: p1.boss.employee_id,
971
+ associate_oid: p1.boss.associate_oid,
972
+ display_name: p1.boss.display_name,
973
+ department: p1.boss.department,
974
+ program: p1.boss.program
975
+ }
976
+ """
977
+
978
+ cursor = await self.db.aql.execute(
979
+ query,
980
+ bind_vars={
981
+ 'collection': self.employees_collection,
982
+ 'employee1': employee1,
983
+ 'employee2': employee2,
984
+ 'graph_name': self.graph_name
985
+ }
986
+ )
987
+ async with cursor:
988
+ results = [doc async for doc in cursor]
989
+ return results[0] if results else None
990
+
991
+ async def is_boss_of(
992
+ self,
993
+ employee_oid: str,
994
+ boss_oid: str,
995
+ direct_only: bool = False
996
+ ) -> Dict[str, Any]:
997
+ """
998
+ Check if boss_oid is a boss (direct or indirect) of employee_oid.
999
+
1000
+ Args:
1001
+ employee_oid: Employee's ID
1002
+ boss_oid: Boss's ID
1003
+ direct_only: If True, check only direct reporting (level 1)
1004
+ If False, check any level in hierarchy
1005
+
1006
+ Returns:
1007
+ Dict with relationship details:
1008
+ {
1009
+ 'is_manager': bool,
1010
+ 'is_direct_manager': bool,
1011
+ 'level': int (0 if not manager, 1 for direct, 2+ for indirect),
1012
+ 'path': list of employee IDs from employee to manager
1013
+ }
1014
+ """
1015
+ if employee_oid == boss_oid:
1016
+ return {
1017
+ 'is_manager': False,
1018
+ 'is_direct_manager': False,
1019
+ 'level': 0,
1020
+ 'path': [],
1021
+ 'relationship': 'same_person'
1022
+ }
1023
+
1024
+ depth = "1..1" if direct_only else "1..99"
1025
+
1026
+ query = f"""
1027
+ // Find path from employee to potential manager
1028
+ FOR v, e, p IN {depth} OUTBOUND CONCAT(@collection, '/', @employee_oid)
1029
+ GRAPH @graph_name
1030
+ FILTER v._key == @boss_oid OR v.employee_id == @boss_oid
1031
+ LIMIT 1
1032
+ RETURN {{
1033
+ found: true,
1034
+ level: LENGTH(p.edges),
1035
+ path: p.vertices[*].employee_id,
1036
+ manager_name: v.display_name,
1037
+ employee_name: DOCUMENT(CONCAT(@collection, '/', @employee_oid)).display_name
1038
+ }}
1039
+ """
1040
+ cursor = await self.db.aql.execute(
1041
+ query,
1042
+ bind_vars={
1043
+ 'collection': self.employees_collection,
1044
+ 'employee_oid': employee_oid,
1045
+ 'boss_oid': boss_oid,
1046
+ 'graph_name': self.graph_name
1047
+ }
1048
+ )
1049
+ async with cursor:
1050
+ results = [doc async for doc in cursor]
1051
+ if not results:
1052
+ return {
1053
+ 'is_manager': False,
1054
+ 'is_direct_manager': False,
1055
+ 'level': 0,
1056
+ 'path': [],
1057
+ 'relationship': 'not_manager'
1058
+ }
1059
+ result = results[0]
1060
+ level = result['level']
1061
+ return {
1062
+ 'is_manager': True,
1063
+ 'is_direct_manager': level == 1,
1064
+ 'level': level,
1065
+ 'path': result['path'],
1066
+ 'relationship': 'direct_manager' if level == 1 else f'manager_level_{level}',
1067
+ 'manager_name': result['manager_name'],
1068
+ 'employee_name': result['employee_name']
1069
+ }
1070
+
1071
+ async def is_subordinate(
1072
+ self,
1073
+ employee_oid: str,
1074
+ manager_oid: str,
1075
+ direct_only: bool = False
1076
+ ) -> Dict[str, Any]:
1077
+ """
1078
+ Check if employee_oid is a subordinate of manager_oid.
1079
+ This is the inverse of is_boss_of().
1080
+
1081
+ Args:
1082
+ employee_oid: Employee's ID
1083
+ manager_oid: Potential manager's ID
1084
+ direct_only: If True, check only direct reporting
1085
+
1086
+ Returns:
1087
+ Dict with relationship details
1088
+ """
1089
+ # This is just the inverse of is_boss_of
1090
+ return await self.is_boss_of(employee_oid, manager_oid, direct_only)
1091
+
1092
+ async def get_relationship(
1093
+ self,
1094
+ employee1: str,
1095
+ employee2: str
1096
+ ) -> Dict[str, Any]:
1097
+ """
1098
+ Get the complete relationship between two employees.
1099
+
1100
+ Args:
1101
+ employee1: First employee's ID
1102
+ employee2: Second employee's ID
1103
+
1104
+ Returns:
1105
+ Comprehensive relationship information
1106
+ """
1107
+ if employee1 == employee2:
1108
+ return {
1109
+ 'relationship': 'same_person',
1110
+ 'employee1_id': employee1,
1111
+ 'employee2_id': employee2
1112
+ }
1113
+
1114
+ # Check all possible relationships in parallel
1115
+ results = await asyncio.gather(
1116
+ self.is_boss_of(employee1, employee2),
1117
+ self.is_boss_of(employee2, employee1),
1118
+ self.are_colleagues(employee1, employee2),
1119
+ self.are_in_same_department(employee1, employee2),
1120
+ return_exceptions=True
1121
+ )
1122
+
1123
+ emp1_manages_emp2 = {'is_manager': False} if isinstance(results[0], Exception) else results[0]
1124
+ emp2_manages_emp1 = {'is_manager': False} if isinstance(results[1], Exception) else results[1]
1125
+ are_colleagues = False if isinstance(results[2], Exception) else results[2]
1126
+ same_department = False if isinstance(results[3], Exception) else results[3]
1127
+
1128
+ # Determine primary relationship
1129
+ if emp1_manages_emp2['is_manager']:
1130
+ primary = 'manager_subordinate'
1131
+ details = {
1132
+ 'manager': employee1,
1133
+ 'subordinate': employee2,
1134
+ 'level': emp1_manages_emp2['level'],
1135
+ 'is_direct': emp1_manages_emp2['is_direct_manager']
1136
+ }
1137
+ elif emp2_manages_emp1['is_manager']:
1138
+ primary = 'subordinate_manager'
1139
+ details = {
1140
+ 'manager': employee2,
1141
+ 'subordinate': employee1,
1142
+ 'level': emp2_manages_emp1['level'],
1143
+ 'is_direct': emp2_manages_emp1['is_direct_manager']
1144
+ }
1145
+ elif are_colleagues:
1146
+ primary = 'colleagues'
1147
+ details = {'same_boss': True}
1148
+ elif same_department:
1149
+ primary = 'same_department'
1150
+ details = {'department_colleagues': True}
1151
+ else:
1152
+ primary = 'no_direct_relationship'
1153
+ details = {}
1154
+
1155
+ return {
1156
+ 'relationship': primary,
1157
+ 'employee1_id': employee1,
1158
+ 'employee2_id': employee2,
1159
+ 'details': details,
1160
+ 'are_colleagues': are_colleagues,
1161
+ 'same_department': same_department,
1162
+ 'emp1_manages_emp2': emp1_manages_emp2['is_manager'],
1163
+ 'emp2_manages_emp1': emp2_manages_emp1['is_manager']
1164
+ }
1165
+
1166
+ async def check_management_chain(
1167
+ self,
1168
+ employee_id: str,
1169
+ target_manager_id: str
1170
+ ) -> Dict[str, Any]:
1171
+ """
1172
+ Check if target_manager_id is anywhere in employee's management chain.
1173
+ Returns the complete path and level if found.
1174
+
1175
+ Args:
1176
+ employee_id: Employee's ID
1177
+ target_manager_id: Manager to search for in chain
1178
+
1179
+ Returns:
1180
+ Dict with chain details
1181
+ """
1182
+ query = """
1183
+ // Get all managers in the chain
1184
+ FOR v, e, p IN 1..99 OUTBOUND CONCAT(@collection, '/', @employee_id)
1185
+ GRAPH @graph_name
1186
+ OPTIONS {bfs: false} // Use DFS to get the path
1187
+ FILTER v._key == @target_manager OR v.employee_id == @target_manager
1188
+ LIMIT 1
1189
+ RETURN {
1190
+ found: true,
1191
+ level: LENGTH(p.edges),
1192
+ chain: (
1193
+ FOR vertex IN p.vertices
1194
+ RETURN {
1195
+ id: vertex.employee_id,
1196
+ name: vertex.display_name,
1197
+ department: vertex.department
1198
+ }
1199
+ )
1200
+ }
1201
+ """
1202
+
1203
+ cursor = await self.db.aql.execute(
1204
+ query,
1205
+ bind_vars={
1206
+ 'collection': self.employees_collection,
1207
+ 'employee_id': employee_id,
1208
+ 'target_manager': target_manager_id,
1209
+ 'graph_name': self.graph_name
1210
+ }
1211
+ )
1212
+
1213
+ async with cursor:
1214
+ results = [doc async for doc in cursor]
1215
+
1216
+ if results:
1217
+ return {
1218
+ 'in_chain': True,
1219
+ **results[0]
1220
+ }
1221
+ else:
1222
+ return {
1223
+ 'in_chain': False,
1224
+ 'found': False,
1225
+ 'level': 0,
1226
+ 'chain': []
1227
+ }