ai-parrot 0.17.2__cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentui/.prettierrc +15 -0
- agentui/QUICKSTART.md +272 -0
- agentui/README.md +59 -0
- agentui/env.example +16 -0
- agentui/jsconfig.json +14 -0
- agentui/package-lock.json +4242 -0
- agentui/package.json +34 -0
- agentui/scripts/postinstall/apply-patches.mjs +260 -0
- agentui/src/app.css +61 -0
- agentui/src/app.d.ts +13 -0
- agentui/src/app.html +12 -0
- agentui/src/components/LoadingSpinner.svelte +64 -0
- agentui/src/components/ThemeSwitcher.svelte +159 -0
- agentui/src/components/index.js +4 -0
- agentui/src/lib/api/bots.ts +60 -0
- agentui/src/lib/api/chat.ts +22 -0
- agentui/src/lib/api/http.ts +25 -0
- agentui/src/lib/components/BotCard.svelte +33 -0
- agentui/src/lib/components/ChatBubble.svelte +63 -0
- agentui/src/lib/components/Toast.svelte +21 -0
- agentui/src/lib/config.ts +20 -0
- agentui/src/lib/stores/auth.svelte.ts +73 -0
- agentui/src/lib/stores/theme.svelte.js +64 -0
- agentui/src/lib/stores/toast.svelte.ts +31 -0
- agentui/src/lib/utils/conversation.ts +39 -0
- agentui/src/routes/+layout.svelte +20 -0
- agentui/src/routes/+page.svelte +232 -0
- agentui/src/routes/login/+page.svelte +200 -0
- agentui/src/routes/talk/[agentId]/+page.svelte +297 -0
- agentui/src/routes/talk/[agentId]/+page.ts +7 -0
- agentui/static/README.md +1 -0
- agentui/svelte.config.js +11 -0
- agentui/tailwind.config.ts +53 -0
- agentui/tsconfig.json +3 -0
- agentui/vite.config.ts +10 -0
- ai_parrot-0.17.2.dist-info/METADATA +472 -0
- ai_parrot-0.17.2.dist-info/RECORD +535 -0
- ai_parrot-0.17.2.dist-info/WHEEL +6 -0
- ai_parrot-0.17.2.dist-info/entry_points.txt +2 -0
- ai_parrot-0.17.2.dist-info/licenses/LICENSE +21 -0
- ai_parrot-0.17.2.dist-info/top_level.txt +6 -0
- crew-builder/.prettierrc +15 -0
- crew-builder/QUICKSTART.md +259 -0
- crew-builder/README.md +113 -0
- crew-builder/env.example +17 -0
- crew-builder/jsconfig.json +14 -0
- crew-builder/package-lock.json +4182 -0
- crew-builder/package.json +37 -0
- crew-builder/scripts/postinstall/apply-patches.mjs +260 -0
- crew-builder/src/app.css +62 -0
- crew-builder/src/app.d.ts +13 -0
- crew-builder/src/app.html +12 -0
- crew-builder/src/components/LoadingSpinner.svelte +64 -0
- crew-builder/src/components/ThemeSwitcher.svelte +149 -0
- crew-builder/src/components/index.js +9 -0
- crew-builder/src/lib/api/bots.ts +60 -0
- crew-builder/src/lib/api/chat.ts +80 -0
- crew-builder/src/lib/api/client.ts +56 -0
- crew-builder/src/lib/api/crew/crew.ts +136 -0
- crew-builder/src/lib/api/index.ts +5 -0
- crew-builder/src/lib/api/o365/auth.ts +65 -0
- crew-builder/src/lib/auth/auth.ts +54 -0
- crew-builder/src/lib/components/AgentNode.svelte +43 -0
- crew-builder/src/lib/components/BotCard.svelte +33 -0
- crew-builder/src/lib/components/ChatBubble.svelte +67 -0
- crew-builder/src/lib/components/ConfigPanel.svelte +278 -0
- crew-builder/src/lib/components/JsonTreeNode.svelte +76 -0
- crew-builder/src/lib/components/JsonViewer.svelte +24 -0
- crew-builder/src/lib/components/MarkdownEditor.svelte +48 -0
- crew-builder/src/lib/components/ThemeToggle.svelte +36 -0
- crew-builder/src/lib/components/Toast.svelte +67 -0
- crew-builder/src/lib/components/Toolbar.svelte +157 -0
- crew-builder/src/lib/components/index.ts +10 -0
- crew-builder/src/lib/config.ts +8 -0
- crew-builder/src/lib/stores/auth.svelte.ts +228 -0
- crew-builder/src/lib/stores/crewStore.ts +369 -0
- crew-builder/src/lib/stores/theme.svelte.js +145 -0
- crew-builder/src/lib/stores/toast.svelte.ts +69 -0
- crew-builder/src/lib/utils/conversation.ts +39 -0
- crew-builder/src/lib/utils/markdown.ts +122 -0
- crew-builder/src/lib/utils/talkHistory.ts +47 -0
- crew-builder/src/routes/+layout.svelte +20 -0
- crew-builder/src/routes/+page.svelte +539 -0
- crew-builder/src/routes/agents/+page.svelte +247 -0
- crew-builder/src/routes/agents/[agentId]/+page.svelte +288 -0
- crew-builder/src/routes/agents/[agentId]/+page.ts +7 -0
- crew-builder/src/routes/builder/+page.svelte +204 -0
- crew-builder/src/routes/crew/ask/+page.svelte +1052 -0
- crew-builder/src/routes/crew/ask/+page.ts +1 -0
- crew-builder/src/routes/integrations/o365/+page.svelte +304 -0
- crew-builder/src/routes/login/+page.svelte +197 -0
- crew-builder/src/routes/talk/[agentId]/+page.svelte +487 -0
- crew-builder/src/routes/talk/[agentId]/+page.ts +7 -0
- crew-builder/static/README.md +1 -0
- crew-builder/svelte.config.js +11 -0
- crew-builder/tailwind.config.ts +53 -0
- crew-builder/tsconfig.json +3 -0
- crew-builder/vite.config.ts +10 -0
- mcp_servers/calculator_server.py +309 -0
- parrot/__init__.py +27 -0
- parrot/__pycache__/__init__.cpython-310.pyc +0 -0
- parrot/__pycache__/version.cpython-310.pyc +0 -0
- parrot/_version.py +34 -0
- parrot/a2a/__init__.py +48 -0
- parrot/a2a/client.py +658 -0
- parrot/a2a/discovery.py +89 -0
- parrot/a2a/mixin.py +257 -0
- parrot/a2a/models.py +376 -0
- parrot/a2a/server.py +770 -0
- parrot/agents/__init__.py +29 -0
- parrot/bots/__init__.py +12 -0
- parrot/bots/a2a_agent.py +19 -0
- parrot/bots/abstract.py +3139 -0
- parrot/bots/agent.py +1129 -0
- parrot/bots/basic.py +9 -0
- parrot/bots/chatbot.py +669 -0
- parrot/bots/data.py +1618 -0
- parrot/bots/database/__init__.py +5 -0
- parrot/bots/database/abstract.py +3071 -0
- parrot/bots/database/cache.py +286 -0
- parrot/bots/database/models.py +468 -0
- parrot/bots/database/prompts.py +154 -0
- parrot/bots/database/retries.py +98 -0
- parrot/bots/database/router.py +269 -0
- parrot/bots/database/sql.py +41 -0
- parrot/bots/db/__init__.py +6 -0
- parrot/bots/db/abstract.py +556 -0
- parrot/bots/db/bigquery.py +602 -0
- parrot/bots/db/cache.py +85 -0
- parrot/bots/db/documentdb.py +668 -0
- parrot/bots/db/elastic.py +1014 -0
- parrot/bots/db/influx.py +898 -0
- parrot/bots/db/mock.py +96 -0
- parrot/bots/db/multi.py +783 -0
- parrot/bots/db/prompts.py +185 -0
- parrot/bots/db/sql.py +1255 -0
- parrot/bots/db/tools.py +212 -0
- parrot/bots/document.py +680 -0
- parrot/bots/hrbot.py +15 -0
- parrot/bots/kb.py +170 -0
- parrot/bots/mcp.py +36 -0
- parrot/bots/orchestration/README.md +463 -0
- parrot/bots/orchestration/__init__.py +1 -0
- parrot/bots/orchestration/agent.py +155 -0
- parrot/bots/orchestration/crew.py +3330 -0
- parrot/bots/orchestration/fsm.py +1179 -0
- parrot/bots/orchestration/hr.py +434 -0
- parrot/bots/orchestration/storage/__init__.py +4 -0
- parrot/bots/orchestration/storage/memory.py +100 -0
- parrot/bots/orchestration/storage/mixin.py +119 -0
- parrot/bots/orchestration/verify.py +202 -0
- parrot/bots/product.py +204 -0
- parrot/bots/prompts/__init__.py +96 -0
- parrot/bots/prompts/agents.py +155 -0
- parrot/bots/prompts/data.py +216 -0
- parrot/bots/prompts/output_generation.py +8 -0
- parrot/bots/scraper/__init__.py +3 -0
- parrot/bots/scraper/models.py +122 -0
- parrot/bots/scraper/scraper.py +1173 -0
- parrot/bots/scraper/templates.py +115 -0
- parrot/bots/stores/__init__.py +5 -0
- parrot/bots/stores/local.py +172 -0
- parrot/bots/webdev.py +81 -0
- parrot/cli.py +17 -0
- parrot/clients/__init__.py +16 -0
- parrot/clients/base.py +1491 -0
- parrot/clients/claude.py +1191 -0
- parrot/clients/factory.py +129 -0
- parrot/clients/google.py +4567 -0
- parrot/clients/gpt.py +1975 -0
- parrot/clients/grok.py +432 -0
- parrot/clients/groq.py +986 -0
- parrot/clients/hf.py +582 -0
- parrot/clients/models.py +18 -0
- parrot/conf.py +395 -0
- parrot/embeddings/__init__.py +9 -0
- parrot/embeddings/base.py +157 -0
- parrot/embeddings/google.py +98 -0
- parrot/embeddings/huggingface.py +74 -0
- parrot/embeddings/openai.py +84 -0
- parrot/embeddings/processor.py +88 -0
- parrot/exceptions.c +13868 -0
- parrot/exceptions.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/exceptions.pxd +22 -0
- parrot/exceptions.pxi +15 -0
- parrot/exceptions.pyx +44 -0
- parrot/generators/__init__.py +29 -0
- parrot/generators/base.py +200 -0
- parrot/generators/html.py +293 -0
- parrot/generators/react.py +205 -0
- parrot/generators/streamlit.py +203 -0
- parrot/generators/template.py +105 -0
- parrot/handlers/__init__.py +4 -0
- parrot/handlers/agent.py +861 -0
- parrot/handlers/agents/__init__.py +1 -0
- parrot/handlers/agents/abstract.py +900 -0
- parrot/handlers/bots.py +338 -0
- parrot/handlers/chat.py +915 -0
- parrot/handlers/creation.sql +192 -0
- parrot/handlers/crew/ARCHITECTURE.md +362 -0
- parrot/handlers/crew/README_BOTMANAGER_PERSISTENCE.md +303 -0
- parrot/handlers/crew/README_REDIS_PERSISTENCE.md +366 -0
- parrot/handlers/crew/__init__.py +0 -0
- parrot/handlers/crew/handler.py +801 -0
- parrot/handlers/crew/models.py +229 -0
- parrot/handlers/crew/redis_persistence.py +523 -0
- parrot/handlers/jobs/__init__.py +10 -0
- parrot/handlers/jobs/job.py +384 -0
- parrot/handlers/jobs/mixin.py +627 -0
- parrot/handlers/jobs/models.py +115 -0
- parrot/handlers/jobs/worker.py +31 -0
- parrot/handlers/models.py +596 -0
- parrot/handlers/o365_auth.py +105 -0
- parrot/handlers/stream.py +337 -0
- parrot/interfaces/__init__.py +6 -0
- parrot/interfaces/aws.py +143 -0
- parrot/interfaces/credentials.py +113 -0
- parrot/interfaces/database.py +27 -0
- parrot/interfaces/google.py +1123 -0
- parrot/interfaces/hierarchy.py +1227 -0
- parrot/interfaces/http.py +651 -0
- parrot/interfaces/images/__init__.py +0 -0
- parrot/interfaces/images/plugins/__init__.py +24 -0
- parrot/interfaces/images/plugins/abstract.py +58 -0
- parrot/interfaces/images/plugins/analisys.py +148 -0
- parrot/interfaces/images/plugins/classify.py +150 -0
- parrot/interfaces/images/plugins/classifybase.py +182 -0
- parrot/interfaces/images/plugins/detect.py +150 -0
- parrot/interfaces/images/plugins/exif.py +1103 -0
- parrot/interfaces/images/plugins/hash.py +52 -0
- parrot/interfaces/images/plugins/vision.py +104 -0
- parrot/interfaces/images/plugins/yolo.py +66 -0
- parrot/interfaces/images/plugins/zerodetect.py +197 -0
- parrot/interfaces/o365.py +978 -0
- parrot/interfaces/onedrive.py +822 -0
- parrot/interfaces/sharepoint.py +1435 -0
- parrot/interfaces/soap.py +257 -0
- parrot/loaders/__init__.py +8 -0
- parrot/loaders/abstract.py +1131 -0
- parrot/loaders/audio.py +199 -0
- parrot/loaders/basepdf.py +53 -0
- parrot/loaders/basevideo.py +1568 -0
- parrot/loaders/csv.py +409 -0
- parrot/loaders/docx.py +116 -0
- parrot/loaders/epubloader.py +316 -0
- parrot/loaders/excel.py +199 -0
- parrot/loaders/factory.py +55 -0
- parrot/loaders/files/__init__.py +0 -0
- parrot/loaders/files/abstract.py +39 -0
- parrot/loaders/files/html.py +26 -0
- parrot/loaders/files/text.py +63 -0
- parrot/loaders/html.py +152 -0
- parrot/loaders/markdown.py +442 -0
- parrot/loaders/pdf.py +373 -0
- parrot/loaders/pdfmark.py +320 -0
- parrot/loaders/pdftables.py +506 -0
- parrot/loaders/ppt.py +476 -0
- parrot/loaders/qa.py +63 -0
- parrot/loaders/splitters/__init__.py +10 -0
- parrot/loaders/splitters/base.py +138 -0
- parrot/loaders/splitters/md.py +228 -0
- parrot/loaders/splitters/token.py +143 -0
- parrot/loaders/txt.py +26 -0
- parrot/loaders/video.py +89 -0
- parrot/loaders/videolocal.py +218 -0
- parrot/loaders/videounderstanding.py +377 -0
- parrot/loaders/vimeo.py +167 -0
- parrot/loaders/web.py +599 -0
- parrot/loaders/youtube.py +504 -0
- parrot/manager/__init__.py +5 -0
- parrot/manager/manager.py +1030 -0
- parrot/mcp/__init__.py +28 -0
- parrot/mcp/adapter.py +105 -0
- parrot/mcp/cli.py +174 -0
- parrot/mcp/client.py +119 -0
- parrot/mcp/config.py +75 -0
- parrot/mcp/integration.py +842 -0
- parrot/mcp/oauth.py +933 -0
- parrot/mcp/server.py +225 -0
- parrot/mcp/transports/__init__.py +3 -0
- parrot/mcp/transports/base.py +279 -0
- parrot/mcp/transports/grpc_session.py +163 -0
- parrot/mcp/transports/http.py +312 -0
- parrot/mcp/transports/mcp.proto +108 -0
- parrot/mcp/transports/quic.py +1082 -0
- parrot/mcp/transports/sse.py +330 -0
- parrot/mcp/transports/stdio.py +309 -0
- parrot/mcp/transports/unix.py +395 -0
- parrot/mcp/transports/websocket.py +547 -0
- parrot/memory/__init__.py +16 -0
- parrot/memory/abstract.py +209 -0
- parrot/memory/agent.py +32 -0
- parrot/memory/cache.py +175 -0
- parrot/memory/core.py +555 -0
- parrot/memory/file.py +153 -0
- parrot/memory/mem.py +131 -0
- parrot/memory/redis.py +613 -0
- parrot/models/__init__.py +46 -0
- parrot/models/basic.py +118 -0
- parrot/models/compliance.py +208 -0
- parrot/models/crew.py +395 -0
- parrot/models/detections.py +654 -0
- parrot/models/generation.py +85 -0
- parrot/models/google.py +223 -0
- parrot/models/groq.py +23 -0
- parrot/models/openai.py +30 -0
- parrot/models/outputs.py +285 -0
- parrot/models/responses.py +938 -0
- parrot/notifications/__init__.py +743 -0
- parrot/openapi/__init__.py +3 -0
- parrot/openapi/components.yaml +641 -0
- parrot/openapi/config.py +322 -0
- parrot/outputs/__init__.py +32 -0
- parrot/outputs/formats/__init__.py +108 -0
- parrot/outputs/formats/altair.py +359 -0
- parrot/outputs/formats/application.py +122 -0
- parrot/outputs/formats/base.py +351 -0
- parrot/outputs/formats/bokeh.py +356 -0
- parrot/outputs/formats/card.py +424 -0
- parrot/outputs/formats/chart.py +436 -0
- parrot/outputs/formats/d3.py +255 -0
- parrot/outputs/formats/echarts.py +310 -0
- parrot/outputs/formats/generators/__init__.py +0 -0
- parrot/outputs/formats/generators/abstract.py +61 -0
- parrot/outputs/formats/generators/panel.py +145 -0
- parrot/outputs/formats/generators/streamlit.py +86 -0
- parrot/outputs/formats/generators/terminal.py +63 -0
- parrot/outputs/formats/holoviews.py +310 -0
- parrot/outputs/formats/html.py +147 -0
- parrot/outputs/formats/jinja2.py +46 -0
- parrot/outputs/formats/json.py +87 -0
- parrot/outputs/formats/map.py +933 -0
- parrot/outputs/formats/markdown.py +172 -0
- parrot/outputs/formats/matplotlib.py +237 -0
- parrot/outputs/formats/mixins/__init__.py +0 -0
- parrot/outputs/formats/mixins/emaps.py +855 -0
- parrot/outputs/formats/plotly.py +341 -0
- parrot/outputs/formats/seaborn.py +310 -0
- parrot/outputs/formats/table.py +397 -0
- parrot/outputs/formats/template_report.py +138 -0
- parrot/outputs/formats/yaml.py +125 -0
- parrot/outputs/formatter.py +152 -0
- parrot/outputs/templates/__init__.py +95 -0
- parrot/pipelines/__init__.py +0 -0
- parrot/pipelines/abstract.py +210 -0
- parrot/pipelines/detector.py +124 -0
- parrot/pipelines/models.py +90 -0
- parrot/pipelines/planogram.py +3002 -0
- parrot/pipelines/table.sql +97 -0
- parrot/plugins/__init__.py +106 -0
- parrot/plugins/importer.py +80 -0
- parrot/py.typed +0 -0
- parrot/registry/__init__.py +18 -0
- parrot/registry/registry.py +594 -0
- parrot/scheduler/__init__.py +1189 -0
- parrot/scheduler/models.py +60 -0
- parrot/security/__init__.py +16 -0
- parrot/security/prompt_injection.py +268 -0
- parrot/security/security_events.sql +25 -0
- parrot/services/__init__.py +1 -0
- parrot/services/mcp/__init__.py +8 -0
- parrot/services/mcp/config.py +13 -0
- parrot/services/mcp/server.py +295 -0
- parrot/services/o365_remote_auth.py +235 -0
- parrot/stores/__init__.py +7 -0
- parrot/stores/abstract.py +352 -0
- parrot/stores/arango.py +1090 -0
- parrot/stores/bigquery.py +1377 -0
- parrot/stores/cache.py +106 -0
- parrot/stores/empty.py +10 -0
- parrot/stores/faiss_store.py +1157 -0
- parrot/stores/kb/__init__.py +9 -0
- parrot/stores/kb/abstract.py +68 -0
- parrot/stores/kb/cache.py +165 -0
- parrot/stores/kb/doc.py +325 -0
- parrot/stores/kb/hierarchy.py +346 -0
- parrot/stores/kb/local.py +457 -0
- parrot/stores/kb/prompt.py +28 -0
- parrot/stores/kb/redis.py +659 -0
- parrot/stores/kb/store.py +115 -0
- parrot/stores/kb/user.py +374 -0
- parrot/stores/models.py +59 -0
- parrot/stores/pgvector.py +3 -0
- parrot/stores/postgres.py +2853 -0
- parrot/stores/utils/__init__.py +0 -0
- parrot/stores/utils/chunking.py +197 -0
- parrot/telemetry/__init__.py +3 -0
- parrot/telemetry/mixin.py +111 -0
- parrot/template/__init__.py +3 -0
- parrot/template/engine.py +259 -0
- parrot/tools/__init__.py +23 -0
- parrot/tools/abstract.py +644 -0
- parrot/tools/agent.py +363 -0
- parrot/tools/arangodbsearch.py +537 -0
- parrot/tools/arxiv_tool.py +188 -0
- parrot/tools/calculator/__init__.py +3 -0
- parrot/tools/calculator/operations/__init__.py +38 -0
- parrot/tools/calculator/operations/calculus.py +80 -0
- parrot/tools/calculator/operations/statistics.py +76 -0
- parrot/tools/calculator/tool.py +150 -0
- parrot/tools/cloudwatch.py +988 -0
- parrot/tools/codeinterpreter/__init__.py +127 -0
- parrot/tools/codeinterpreter/executor.py +371 -0
- parrot/tools/codeinterpreter/internals.py +473 -0
- parrot/tools/codeinterpreter/models.py +643 -0
- parrot/tools/codeinterpreter/prompts.py +224 -0
- parrot/tools/codeinterpreter/tool.py +664 -0
- parrot/tools/company_info/__init__.py +6 -0
- parrot/tools/company_info/tool.py +1138 -0
- parrot/tools/correlationanalysis.py +437 -0
- parrot/tools/database/abstract.py +286 -0
- parrot/tools/database/bq.py +115 -0
- parrot/tools/database/cache.py +284 -0
- parrot/tools/database/models.py +95 -0
- parrot/tools/database/pg.py +343 -0
- parrot/tools/databasequery.py +1159 -0
- parrot/tools/db.py +1800 -0
- parrot/tools/ddgo.py +370 -0
- parrot/tools/decorators.py +271 -0
- parrot/tools/dftohtml.py +282 -0
- parrot/tools/document.py +549 -0
- parrot/tools/ecs.py +819 -0
- parrot/tools/edareport.py +368 -0
- parrot/tools/elasticsearch.py +1049 -0
- parrot/tools/employees.py +462 -0
- parrot/tools/epson/__init__.py +96 -0
- parrot/tools/excel.py +683 -0
- parrot/tools/file/__init__.py +13 -0
- parrot/tools/file/abstract.py +76 -0
- parrot/tools/file/gcs.py +378 -0
- parrot/tools/file/local.py +284 -0
- parrot/tools/file/s3.py +511 -0
- parrot/tools/file/tmp.py +309 -0
- parrot/tools/file/tool.py +501 -0
- parrot/tools/file_reader.py +129 -0
- parrot/tools/flowtask/__init__.py +19 -0
- parrot/tools/flowtask/tool.py +761 -0
- parrot/tools/gittoolkit.py +508 -0
- parrot/tools/google/__init__.py +18 -0
- parrot/tools/google/base.py +169 -0
- parrot/tools/google/tools.py +1251 -0
- parrot/tools/googlelocation.py +5 -0
- parrot/tools/googleroutes.py +5 -0
- parrot/tools/googlesearch.py +5 -0
- parrot/tools/googlesitesearch.py +5 -0
- parrot/tools/googlevoice.py +2 -0
- parrot/tools/gvoice.py +695 -0
- parrot/tools/ibisworld/README.md +225 -0
- parrot/tools/ibisworld/__init__.py +11 -0
- parrot/tools/ibisworld/tool.py +366 -0
- parrot/tools/jiratoolkit.py +1718 -0
- parrot/tools/manager.py +1098 -0
- parrot/tools/math.py +152 -0
- parrot/tools/metadata.py +476 -0
- parrot/tools/msteams.py +1621 -0
- parrot/tools/msword.py +635 -0
- parrot/tools/multidb.py +580 -0
- parrot/tools/multistoresearch.py +369 -0
- parrot/tools/networkninja.py +167 -0
- parrot/tools/nextstop/__init__.py +4 -0
- parrot/tools/nextstop/base.py +286 -0
- parrot/tools/nextstop/employee.py +733 -0
- parrot/tools/nextstop/store.py +462 -0
- parrot/tools/notification.py +435 -0
- parrot/tools/o365/__init__.py +42 -0
- parrot/tools/o365/base.py +295 -0
- parrot/tools/o365/bundle.py +522 -0
- parrot/tools/o365/events.py +554 -0
- parrot/tools/o365/mail.py +992 -0
- parrot/tools/o365/onedrive.py +497 -0
- parrot/tools/o365/sharepoint.py +641 -0
- parrot/tools/openapi_toolkit.py +904 -0
- parrot/tools/openweather.py +527 -0
- parrot/tools/pdfprint.py +1001 -0
- parrot/tools/powerbi.py +518 -0
- parrot/tools/powerpoint.py +1113 -0
- parrot/tools/pricestool.py +146 -0
- parrot/tools/products/__init__.py +246 -0
- parrot/tools/prophet_tool.py +171 -0
- parrot/tools/pythonpandas.py +630 -0
- parrot/tools/pythonrepl.py +910 -0
- parrot/tools/qsource.py +436 -0
- parrot/tools/querytoolkit.py +395 -0
- parrot/tools/quickeda.py +827 -0
- parrot/tools/resttool.py +553 -0
- parrot/tools/retail/__init__.py +0 -0
- parrot/tools/retail/bby.py +528 -0
- parrot/tools/sandboxtool.py +703 -0
- parrot/tools/sassie/__init__.py +352 -0
- parrot/tools/scraping/__init__.py +7 -0
- parrot/tools/scraping/docs/select.md +466 -0
- parrot/tools/scraping/documentation.md +1278 -0
- parrot/tools/scraping/driver.py +436 -0
- parrot/tools/scraping/models.py +576 -0
- parrot/tools/scraping/options.py +85 -0
- parrot/tools/scraping/orchestrator.py +517 -0
- parrot/tools/scraping/readme.md +740 -0
- parrot/tools/scraping/tool.py +3115 -0
- parrot/tools/seasonaldetection.py +642 -0
- parrot/tools/shell_tool/__init__.py +5 -0
- parrot/tools/shell_tool/actions.py +408 -0
- parrot/tools/shell_tool/engine.py +155 -0
- parrot/tools/shell_tool/models.py +322 -0
- parrot/tools/shell_tool/tool.py +442 -0
- parrot/tools/site_search.py +214 -0
- parrot/tools/textfile.py +418 -0
- parrot/tools/think.py +378 -0
- parrot/tools/toolkit.py +298 -0
- parrot/tools/webapp_tool.py +187 -0
- parrot/tools/whatif.py +1279 -0
- parrot/tools/workday/MULTI_WSDL_EXAMPLE.md +249 -0
- parrot/tools/workday/__init__.py +6 -0
- parrot/tools/workday/models.py +1389 -0
- parrot/tools/workday/tool.py +1293 -0
- parrot/tools/yfinance_tool.py +306 -0
- parrot/tools/zipcode.py +217 -0
- parrot/utils/__init__.py +2 -0
- parrot/utils/helpers.py +73 -0
- parrot/utils/parsers/__init__.py +5 -0
- parrot/utils/parsers/toml.c +12078 -0
- parrot/utils/parsers/toml.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/utils/parsers/toml.pyx +21 -0
- parrot/utils/toml.py +11 -0
- parrot/utils/types.cpp +20936 -0
- parrot/utils/types.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/utils/types.pyx +213 -0
- parrot/utils/uv.py +11 -0
- parrot/version.py +10 -0
- parrot/yaml-rs/Cargo.lock +350 -0
- parrot/yaml-rs/Cargo.toml +19 -0
- parrot/yaml-rs/pyproject.toml +19 -0
- parrot/yaml-rs/python/yaml_rs/__init__.py +81 -0
- parrot/yaml-rs/src/lib.rs +222 -0
- requirements/docker-compose.yml +24 -0
- requirements/requirements-dev.txt +21 -0
|
@@ -0,0 +1,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
|
+
}
|