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,58 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from PIL import Image
|
|
4
|
+
from navconfig.logging import logging
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ImagePlugin(ABC):
|
|
8
|
+
"""
|
|
9
|
+
ImagePlugin is a base class for image processing plugins.
|
|
10
|
+
It provides a common interface for image processing tasks.
|
|
11
|
+
Subclasses should implement the `analyze` method to define
|
|
12
|
+
the specific image processing logic.
|
|
13
|
+
"""
|
|
14
|
+
column_name: str = "image_info"
|
|
15
|
+
|
|
16
|
+
def __init__(self, *args, **kwargs):
|
|
17
|
+
"""
|
|
18
|
+
Initialize the ImagePlugin with an optional image path.
|
|
19
|
+
|
|
20
|
+
:param image: Path to the image file.
|
|
21
|
+
"""
|
|
22
|
+
self.column_name = kwargs.get("column_name", self.column_name)
|
|
23
|
+
self.logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
async def analyze(self, image: Image.Image, **kwargs) -> Any:
|
|
27
|
+
"""
|
|
28
|
+
Analyze the image and perform the desired processing.
|
|
29
|
+
|
|
30
|
+
:param image: Image Bytes opened with PIL Image.open
|
|
31
|
+
"""
|
|
32
|
+
raise NotImplementedError(
|
|
33
|
+
"Image Plugin must implement analyze() method."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
async def __aenter__(self):
|
|
37
|
+
if hasattr(self, "open"):
|
|
38
|
+
await self.open()
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
42
|
+
if hasattr(self, "close"):
|
|
43
|
+
await self.close()
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
async def start(self):
|
|
47
|
+
"""
|
|
48
|
+
Start the plugin. This method can be overridden by subclasses
|
|
49
|
+
to perform any initialization or setup tasks.
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
async def dispose(self):
|
|
55
|
+
"""
|
|
56
|
+
Dispose of the plugin resources.
|
|
57
|
+
"""
|
|
58
|
+
return self
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Union, Dict, List
|
|
3
|
+
import yaml
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from PIL import Image
|
|
6
|
+
from .abstract import ImagePlugin
|
|
7
|
+
from ....clients.google import GoogleModel, GoogleGenAIClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AnalysisPlugin(ImagePlugin):
|
|
11
|
+
"""Plugin for analyzing images."""
|
|
12
|
+
column_name: str = "image_features"
|
|
13
|
+
|
|
14
|
+
def __init__(self, *args, **kwargs):
|
|
15
|
+
super().__init__(*args, **kwargs)
|
|
16
|
+
self._model_name: str = kwargs.get(
|
|
17
|
+
"model_name", GoogleModel.GEMINI_2_5_FLASH.value
|
|
18
|
+
)
|
|
19
|
+
self.prompt_path = kwargs.get("prompt_path", Path.cwd() / "prompts")
|
|
20
|
+
self.prompt_file: Union[str, Path] = kwargs.get(
|
|
21
|
+
"prompt_file", "default_analysis_prompt.json"
|
|
22
|
+
)
|
|
23
|
+
self._structured_outputs: Dict[str, BaseModel] = kwargs.get(
|
|
24
|
+
"structured_outputs", {}
|
|
25
|
+
)
|
|
26
|
+
self.filter_by: List[str] = kwargs.get(
|
|
27
|
+
"filter_by", ["Boxes on Floor"]
|
|
28
|
+
)
|
|
29
|
+
self.filter_column: str = kwargs.get(
|
|
30
|
+
"filter_column", "category"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def _load_model(self, model_name: str) -> BaseModel:
|
|
34
|
+
""" Load the classification or categorization model based on the provided model name.
|
|
35
|
+
This method uses importlib to dynamically import the model class.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
module_path, class_name = model_name.rsplit('.', 1)
|
|
39
|
+
module = __import__(module_path, fromlist=[class_name])
|
|
40
|
+
return getattr(module, class_name)
|
|
41
|
+
except (ImportError, AttributeError) as e:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"Failed to load categorization model: {model_name}. Error: {e}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
async def start(self, **kwargs):
|
|
47
|
+
"""Initialize the plugin and load the prompt."""
|
|
48
|
+
if isinstance(self.prompt_file, str):
|
|
49
|
+
self.prompt_file = Path(self.prompt_file)
|
|
50
|
+
if not self.prompt_file.is_absolute():
|
|
51
|
+
self.prompt_file = self.prompt_path.joinpath(self.prompt_file)
|
|
52
|
+
if not self.prompt_file.exists():
|
|
53
|
+
raise FileNotFoundError(
|
|
54
|
+
f"Prompt file {self.prompt_file} does not exist."
|
|
55
|
+
)
|
|
56
|
+
# From the prompt File, load the prompt content:
|
|
57
|
+
content = self.prompt_file.read_text()
|
|
58
|
+
# open from YAML file:
|
|
59
|
+
try:
|
|
60
|
+
self.prompt = yaml.safe_load(content).get('analysis_configs', {})
|
|
61
|
+
except Exception as e:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"Failed to decode YAML from prompt file {self.prompt_file}. Error: {e}"
|
|
64
|
+
)
|
|
65
|
+
# Iterate over all prompts and load the models:
|
|
66
|
+
for key, value in self.prompt.items():
|
|
67
|
+
structured_output = value.get("structured_output", None)
|
|
68
|
+
if structured_output is None:
|
|
69
|
+
self.logger.warning(
|
|
70
|
+
f"No structured output defined for {key} in prompt file."
|
|
71
|
+
)
|
|
72
|
+
continue
|
|
73
|
+
try:
|
|
74
|
+
self._structured_outputs[key] = self._load_model(structured_output)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"Failed to load structured output model for {key}. Error: {e}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def _extract_analysis_results(self, result) -> dict:
|
|
81
|
+
"""
|
|
82
|
+
Extract analysis results from AIMessage object and return as dictionary.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
result: AIMessage object with structured_output containing InkWallAnalysis
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
dict: Dictionary with analysis data
|
|
89
|
+
"""
|
|
90
|
+
if hasattr(result, 'output') and result.output:
|
|
91
|
+
analysis_result = result.output
|
|
92
|
+
# Convert Pydantic model to dictionary
|
|
93
|
+
return analysis_result.dict()
|
|
94
|
+
else:
|
|
95
|
+
raise ValueError("No output found in the result object.")
|
|
96
|
+
|
|
97
|
+
async def analyze(self, image: Union[Path, Image.Image], **kwargs) -> dict:
|
|
98
|
+
"""
|
|
99
|
+
Analyze the ink wall image and perform structured analysis.
|
|
100
|
+
|
|
101
|
+
:param image: Image Bytes opened with PIL Image.open
|
|
102
|
+
:return: A dictionary containing the updated detections column.
|
|
103
|
+
"""
|
|
104
|
+
row = kwargs.get("row", None)
|
|
105
|
+
# Check filter condition
|
|
106
|
+
if hasattr(self, 'filter_column') and hasattr(self, 'filter_by'):
|
|
107
|
+
if row[self.filter_column] not in self.filter_by:
|
|
108
|
+
self.logger.info(
|
|
109
|
+
f"Skipping for analysis for row {row.name} with category {row[self.filter_column]}"
|
|
110
|
+
)
|
|
111
|
+
return None
|
|
112
|
+
image_classification = row[self.filter_column]
|
|
113
|
+
if not image_classification:
|
|
114
|
+
self.logger.warning(
|
|
115
|
+
f"Row {row.name} has no valid category for analysis."
|
|
116
|
+
)
|
|
117
|
+
return None
|
|
118
|
+
structured_output = self._structured_outputs.get(image_classification, None)
|
|
119
|
+
if structured_output is None:
|
|
120
|
+
self.logger.error(
|
|
121
|
+
f"No structured output defined for image classification: {image_classification}"
|
|
122
|
+
)
|
|
123
|
+
return None
|
|
124
|
+
# Perform analysis based on the image classification
|
|
125
|
+
try:
|
|
126
|
+
async with GoogleGenAIClient() as client:
|
|
127
|
+
_result = await client.ask_to_image(
|
|
128
|
+
image=image,
|
|
129
|
+
prompt=self.prompt[image_classification]['prompt'],
|
|
130
|
+
structured_output=structured_output,
|
|
131
|
+
model=self._model_name
|
|
132
|
+
)
|
|
133
|
+
if _result and isinstance(_result.output, structured_output):
|
|
134
|
+
content = self._extract_analysis_results(_result)
|
|
135
|
+
return {
|
|
136
|
+
"analysis:": image_classification,
|
|
137
|
+
**content
|
|
138
|
+
}
|
|
139
|
+
else:
|
|
140
|
+
self.logger.error(
|
|
141
|
+
f"Unexpected output format for {image_classification} analysis."
|
|
142
|
+
)
|
|
143
|
+
return None
|
|
144
|
+
except Exception as e:
|
|
145
|
+
self.logger.error(
|
|
146
|
+
f"Error during analysis for {image_classification}: {e}"
|
|
147
|
+
)
|
|
148
|
+
return None
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List, Union, Optional
|
|
3
|
+
from enum import Enum, EnumMeta
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
from PIL import Image
|
|
6
|
+
from .abstract import ImagePlugin
|
|
7
|
+
from ....clients.google import GoogleModel, GoogleGenAIClient
|
|
8
|
+
|
|
9
|
+
DEFAULT_PROMPT = """
|
|
10
|
+
You are an expert in retail image analysis. Your task is to classify the provided image into one of the following categories.
|
|
11
|
+
Please read the definitions carefully and choose the single best fit.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def is_model_class(cls) -> bool:
|
|
15
|
+
return isinstance(cls, type) and issubclass(cls, BaseModel)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_enum_class(cls) -> bool:
|
|
19
|
+
return isinstance(cls, type) and issubclass(cls, Enum)
|
|
20
|
+
|
|
21
|
+
class ImageCategory(str, Enum):
|
|
22
|
+
"""Enumeration for retail image categories."""
|
|
23
|
+
INK_WALL = "Ink Wall"
|
|
24
|
+
FRONT_OF_STORE = "Front of Store"
|
|
25
|
+
SHELVES_WITH_PRODUCTS = "Shelves with Products"
|
|
26
|
+
BOXES_ON_FLOOR = "Boxes on Floor"
|
|
27
|
+
MERCHANDISING_ENDCAP = "Merchandising Endcap"
|
|
28
|
+
OTHER = "Other"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ImageClassification(BaseModel):
|
|
32
|
+
"""Schema for classifying a retail image."""
|
|
33
|
+
category: ImageCategory = Field(
|
|
34
|
+
...,
|
|
35
|
+
description="The best-fitting category for the image based on the provided definitions."
|
|
36
|
+
)
|
|
37
|
+
confidence_score: float = Field(
|
|
38
|
+
..., ge=0.0, le=1.0,
|
|
39
|
+
description="The model's confidence in its classification, from 0.0 to 1.0."
|
|
40
|
+
)
|
|
41
|
+
reasoning: str = Field(
|
|
42
|
+
...,
|
|
43
|
+
description="A brief explanation for why the image was assigned to this category."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ClassificationPlugin(ImagePlugin):
|
|
48
|
+
"""
|
|
49
|
+
ClassificationPlugin is a plugin for performing image classification.
|
|
50
|
+
Uses Gemini 2.5 multimodal model for image classification tasks.
|
|
51
|
+
"""
|
|
52
|
+
column_name: str = "image_classifications"
|
|
53
|
+
|
|
54
|
+
def __init__(self, *args, **kwargs):
|
|
55
|
+
super().__init__(*args, **kwargs)
|
|
56
|
+
self._model_name: str = kwargs.get(
|
|
57
|
+
"model_name", GoogleModel.GEMINI_2_5_FLASH.value
|
|
58
|
+
)
|
|
59
|
+
self.prompt: List[str] = kwargs.get("prompt", DEFAULT_PROMPT)
|
|
60
|
+
self.confidence: float = kwargs.get("confidence", 0.5)
|
|
61
|
+
self._classification_model = kwargs.get(
|
|
62
|
+
"classification_model", self._load_classification_model(
|
|
63
|
+
ImageClassification
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
self._category_model = kwargs.get(
|
|
67
|
+
"category_model", self._load_category_model(
|
|
68
|
+
ImageCategory
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _load_model(self, model_name: str) -> BaseModel:
|
|
73
|
+
""" Load the classification or categorization model based on the provided model name.
|
|
74
|
+
This method uses importlib to dynamically import the model class.
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
module_path, class_name = model_name.rsplit('.', 1)
|
|
78
|
+
module = __import__(module_path, fromlist=[class_name])
|
|
79
|
+
return getattr(module, class_name)
|
|
80
|
+
except (ImportError, AttributeError) as e:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Failed to load categorization model: {model_name}. Error: {e}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def _load_category_model(self, model_name: Union[str, Enum]) -> Enum:
|
|
86
|
+
"""
|
|
87
|
+
Load the categorization model based on the provided model name.
|
|
88
|
+
|
|
89
|
+
Category Model is a BaseModel that defines the structure of the categorization result.
|
|
90
|
+
model_name will be the python path where the model is stored.
|
|
91
|
+
for example, resources.models.categorization_models.ImageCategory
|
|
92
|
+
uses importlib to dynamically import the model class.
|
|
93
|
+
"""
|
|
94
|
+
if is_enum_class(model_name):
|
|
95
|
+
# Already a Enum instance, return it directly
|
|
96
|
+
return model_name
|
|
97
|
+
elif isinstance(model_name, str):
|
|
98
|
+
# Attempt to import the model class dynamically
|
|
99
|
+
return self._load_model(model_name)
|
|
100
|
+
else:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
"Category model_name must be a string or a Enum instance."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def _load_classification_model(self, model_name: Union[str, BaseModel]) -> BaseModel:
|
|
106
|
+
"""
|
|
107
|
+
Load the classification model based on the provided model name.
|
|
108
|
+
"""
|
|
109
|
+
if is_model_class(model_name):
|
|
110
|
+
# Already a BaseModel instance, return it directly
|
|
111
|
+
return model_name
|
|
112
|
+
elif isinstance(model_name, str):
|
|
113
|
+
# Attempt to import the model class dynamically
|
|
114
|
+
return self._load_model(model_name)
|
|
115
|
+
else:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
"Classification model_name must be a string or a BaseModel instance."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
async def analyze(self, image: Union[Path, Image.Image], **kwargs) -> dict:
|
|
121
|
+
"""
|
|
122
|
+
Analyze the image and classify it into a retail category.
|
|
123
|
+
|
|
124
|
+
:param image: Image Bytes opened with PIL Image.open
|
|
125
|
+
:return: A dictionary containing the classification result.
|
|
126
|
+
"""
|
|
127
|
+
async with GoogleGenAIClient() as client:
|
|
128
|
+
_result = await client.ask_to_image(
|
|
129
|
+
image=image,
|
|
130
|
+
prompt=self.prompt,
|
|
131
|
+
structured_output=self._classification_model,
|
|
132
|
+
model=self._model_name
|
|
133
|
+
)
|
|
134
|
+
if _result and isinstance(_result.output, self._classification_model):
|
|
135
|
+
result = _result.output
|
|
136
|
+
# evaluate if confidence_score is above the threshold
|
|
137
|
+
if result.confidence_score < self.confidence:
|
|
138
|
+
self.logger.warning(
|
|
139
|
+
f"Classification confidence {result.confidence_score} "
|
|
140
|
+
f"is below the threshold {self.confidence}. "
|
|
141
|
+
"Returning None."
|
|
142
|
+
)
|
|
143
|
+
return None
|
|
144
|
+
# If the model returns a valid classification result
|
|
145
|
+
return result.dict()
|
|
146
|
+
else:
|
|
147
|
+
self.logger.error(
|
|
148
|
+
"The model did not return a valid classification result."
|
|
149
|
+
)
|
|
150
|
+
return None
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List, Optional, Union
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from .abstract import ImagePlugin
|
|
6
|
+
from ....clients.google import GoogleModel
|
|
7
|
+
from ....models import ObjectDetectionResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_model_class(cls) -> bool:
|
|
11
|
+
return isinstance(cls, type) and issubclass(cls, BaseModel)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_PROMPT = ''
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ClassifyBase(ImagePlugin):
|
|
18
|
+
"""
|
|
19
|
+
ClassifyBase is an Abstract base class for performing image classification.
|
|
20
|
+
Uses Gemini 2.5 multimodal model for image classification tasks.
|
|
21
|
+
"""
|
|
22
|
+
column_name: str = "detections"
|
|
23
|
+
|
|
24
|
+
def __init__(self, *args, **kwargs):
|
|
25
|
+
super().__init__(*args, **kwargs)
|
|
26
|
+
self._model_name: str = kwargs.get(
|
|
27
|
+
"model_name", GoogleModel.GEMINI_2_5_FLASH.value
|
|
28
|
+
)
|
|
29
|
+
model = kwargs.get(
|
|
30
|
+
"detection_model",
|
|
31
|
+
ObjectDetectionResult
|
|
32
|
+
)
|
|
33
|
+
self.reference_image: Optional[Path] = kwargs.get("reference_image", None)
|
|
34
|
+
self._detection_model: Optional[BaseModel] = self._load_model(model)
|
|
35
|
+
self.prompt: List[str] = kwargs.get("prompt", DEFAULT_PROMPT)
|
|
36
|
+
self.filter_by: List[str] = kwargs.get(
|
|
37
|
+
"filter_by", ["Boxes on Floor"]
|
|
38
|
+
)
|
|
39
|
+
# Modified: filter_column can be None to disable filtering
|
|
40
|
+
self.filter_column: Optional[str] = kwargs.get("filter_column", None)
|
|
41
|
+
|
|
42
|
+
async def start(self, **kwargs):
|
|
43
|
+
if isinstance(self.reference_image, str):
|
|
44
|
+
self.reference_image = Path(self.reference_image)
|
|
45
|
+
if self.reference_image and not self.reference_image.is_absolute():
|
|
46
|
+
self.reference_image = Path.cwd() / self.reference_image
|
|
47
|
+
if self.reference_image and not self.reference_image.exists():
|
|
48
|
+
self.logger.warning(
|
|
49
|
+
f"Reference image {self.reference_image} does not exist. "
|
|
50
|
+
"Classification may not work as expected."
|
|
51
|
+
)
|
|
52
|
+
self.reference_image = None
|
|
53
|
+
|
|
54
|
+
def _load_model(self, model_name: str) -> BaseModel:
|
|
55
|
+
""" Load the classification or categorization model based on the provided model name.
|
|
56
|
+
This method uses importlib to dynamically import the model class.
|
|
57
|
+
"""
|
|
58
|
+
if is_model_class(model_name):
|
|
59
|
+
# Already a BaseModel instance, return it directly
|
|
60
|
+
return model_name
|
|
61
|
+
try:
|
|
62
|
+
module_path, class_name = model_name.rsplit('.', 1)
|
|
63
|
+
module = __import__(module_path, fromlist=[class_name])
|
|
64
|
+
return getattr(module, class_name)
|
|
65
|
+
except (ImportError, AttributeError) as e:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"Failed to load categorization model: {model_name}. Error: {e}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _is_valid_filter_value(self, value):
|
|
71
|
+
"""Check if a filter value is valid (not NA/NaN/None)."""
|
|
72
|
+
if pd.isna(value):
|
|
73
|
+
return False
|
|
74
|
+
if value is None:
|
|
75
|
+
return False
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
def _should_apply_filter(self) -> bool:
|
|
79
|
+
"""
|
|
80
|
+
Determine if filtering should be applied based on filter_column.
|
|
81
|
+
Returns False if filter_column is None, True otherwise.
|
|
82
|
+
"""
|
|
83
|
+
return self.filter_column is not None
|
|
84
|
+
|
|
85
|
+
def _filter_dataset(self, dataset: pd.DataFrame) -> pd.DataFrame:
|
|
86
|
+
"""
|
|
87
|
+
Apply filtering to the dataset if filter_column is specified.
|
|
88
|
+
If filter_column is None, return the entire dataset unchanged.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
dataset: Input DataFrame to potentially filter
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Filtered DataFrame or original dataset if no filtering should be applied
|
|
95
|
+
"""
|
|
96
|
+
if not self._should_apply_filter():
|
|
97
|
+
self.logger.debug(
|
|
98
|
+
"Filter column is None - processing entire dataset without filtering"
|
|
99
|
+
)
|
|
100
|
+
return dataset
|
|
101
|
+
|
|
102
|
+
if self.filter_column not in dataset.columns:
|
|
103
|
+
self.logger.warning(
|
|
104
|
+
f"Filter column '{self.filter_column}' not found in dataset. "
|
|
105
|
+
"Processing entire dataset."
|
|
106
|
+
)
|
|
107
|
+
return dataset
|
|
108
|
+
|
|
109
|
+
# Apply filtering logic
|
|
110
|
+
if not self.filter_by:
|
|
111
|
+
self.logger.warning("filter_by is empty - processing entire dataset")
|
|
112
|
+
return dataset
|
|
113
|
+
|
|
114
|
+
# Filter the dataset based on filter_by values in filter_column
|
|
115
|
+
mask = dataset[self.filter_column].apply(
|
|
116
|
+
lambda x: self._is_valid_filter_value(x) and x in self.filter_by
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
filtered_dataset = dataset[mask].copy()
|
|
120
|
+
|
|
121
|
+
self.logger.info(
|
|
122
|
+
f"Filtered dataset from {len(dataset)} to {len(filtered_dataset)} rows "
|
|
123
|
+
f"using filter_column='{self.filter_column}' with values {self.filter_by}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return filtered_dataset
|
|
127
|
+
|
|
128
|
+
async def process_dataset(self, dataset: pd.DataFrame) -> pd.DataFrame:
|
|
129
|
+
"""
|
|
130
|
+
Process the dataset with optional filtering.
|
|
131
|
+
This method should be implemented by subclasses to handle the actual classification.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
dataset: Input DataFrame
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Processed DataFrame with classification results
|
|
138
|
+
"""
|
|
139
|
+
# Apply filtering (or not, depending on filter_column)
|
|
140
|
+
filtered_dataset = self._filter_dataset(dataset)
|
|
141
|
+
|
|
142
|
+
# Placeholder for actual classification logic
|
|
143
|
+
# Subclasses should override this method
|
|
144
|
+
processed_dataset = await self._classify_images(filtered_dataset)
|
|
145
|
+
|
|
146
|
+
return processed_dataset
|
|
147
|
+
|
|
148
|
+
async def _classify_images(self, dataset: pd.DataFrame) -> pd.DataFrame:
|
|
149
|
+
"""
|
|
150
|
+
Perform the actual image classification.
|
|
151
|
+
This method should be implemented by subclasses.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
dataset: Filtered (or unfiltered) DataFrame to classify
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
DataFrame with classification results
|
|
158
|
+
"""
|
|
159
|
+
raise NotImplementedError("Subclasses must implement _classify_images method")
|
|
160
|
+
|
|
161
|
+
def configure_filtering(
|
|
162
|
+
self,
|
|
163
|
+
filter_column: Optional[str] = None,
|
|
164
|
+
filter_by: Optional[List[str]] = None
|
|
165
|
+
) -> None:
|
|
166
|
+
"""
|
|
167
|
+
Dynamically configure filtering parameters.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
filter_column: Column to filter by. Set to None to disable filtering.
|
|
171
|
+
filter_by: Values to filter by. Only used if filter_column is not None.
|
|
172
|
+
"""
|
|
173
|
+
if filter_column is not None:
|
|
174
|
+
self.filter_column = filter_column
|
|
175
|
+
|
|
176
|
+
if filter_by is not None:
|
|
177
|
+
self.filter_by = filter_by
|
|
178
|
+
|
|
179
|
+
self.logger.info(
|
|
180
|
+
f"Filtering configured: filter_column='{self.filter_column}', "
|
|
181
|
+
f"filter_by={self.filter_by if self.filter_column else 'N/A (filtering disabled)'}"
|
|
182
|
+
)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Union
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from PIL import Image
|
|
5
|
+
from ....clients.google import GoogleGenAIClient
|
|
6
|
+
from .classifybase import ClassifyBase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_model_class(cls) -> bool:
|
|
10
|
+
return isinstance(cls, type) and issubclass(cls, BaseModel)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULT_PROMPT = """
|
|
14
|
+
Analyze this retail image to identify Epson and competitor products.
|
|
15
|
+
|
|
16
|
+
TARGET BRANDS: Epson, HP, Canon, Brother, Lexmark, Xerox, Ricoh, Kyocera, Sharp
|
|
17
|
+
|
|
18
|
+
TASK:
|
|
19
|
+
1. Count ALL visible products (boxed and unboxed)
|
|
20
|
+
2. Identify each product's brand, type, and model (if visible)
|
|
21
|
+
3. Provide approximate location as normalized coordinates (0.0 to 1.0)
|
|
22
|
+
|
|
23
|
+
IMPORTANT:
|
|
24
|
+
- Focus on printers, scanners, ink cartridges, toner, and related products
|
|
25
|
+
- Include products that are partially visible
|
|
26
|
+
- Use confidence scores (0.0 to 1.0) for each detection
|
|
27
|
+
- Provide bounding boxes as [x1, y1, x2, y2] where (0,0) is top-left, (1,1) is bottom-right
|
|
28
|
+
|
|
29
|
+
Respond with a complete JSON object following the specified schema.
|
|
30
|
+
""" # noqa
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DetectionPlugin(ClassifyBase):
|
|
34
|
+
"""
|
|
35
|
+
DetectionPlugin is a plugin for performing image detection.
|
|
36
|
+
Uses Gemini 2.5 multimodal model for image detection tasks.
|
|
37
|
+
"""
|
|
38
|
+
column_name: str = "detections"
|
|
39
|
+
section_name: str = "detections"
|
|
40
|
+
|
|
41
|
+
def __init__(self, *args, **kwargs):
|
|
42
|
+
super().__init__(*args, **kwargs)
|
|
43
|
+
self.section_name = kwargs.get("section_name", self.section_name)
|
|
44
|
+
|
|
45
|
+
def _extract_detection_results(self, result) -> dict:
|
|
46
|
+
"""
|
|
47
|
+
Extract detection results from AIMessage object and return as dictionary.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
result: AIMessage object with structured_output containing ObjectDetectionResult
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
dict: Dictionary with 'analysis' and 'detections' keys
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
# Check if we have structured output
|
|
57
|
+
print(hasattr(result, 'structured_output'), hasattr(result, 'output'))
|
|
58
|
+
if hasattr(result, 'output') and result.output:
|
|
59
|
+
detection_result = result.output
|
|
60
|
+
|
|
61
|
+
# Convert BoundingBox objects to dictionaries
|
|
62
|
+
detections = []
|
|
63
|
+
for bbox in detection_result.detections:
|
|
64
|
+
detection_dict = {
|
|
65
|
+
"object_id": bbox.object_id,
|
|
66
|
+
"brand": bbox.brand,
|
|
67
|
+
"model": bbox.model,
|
|
68
|
+
"product_type": bbox.product_type,
|
|
69
|
+
"description": bbox.description,
|
|
70
|
+
"confidence": bbox.confidence,
|
|
71
|
+
"bbox": bbox.bbox # Already a list [x1, y1, x2, y2]
|
|
72
|
+
}
|
|
73
|
+
detections.append(detection_dict)
|
|
74
|
+
|
|
75
|
+
# Create the final result dictionary
|
|
76
|
+
detected = False
|
|
77
|
+
if detection_result.total_count > 0:
|
|
78
|
+
detected = True
|
|
79
|
+
r = {
|
|
80
|
+
"analysis": detection_result.analysis,
|
|
81
|
+
"total_count": detection_result.total_count, # Include this for completeness
|
|
82
|
+
"detected": detected,
|
|
83
|
+
"detections": detections
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return r
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
# Fallback if no structured output
|
|
90
|
+
return {
|
|
91
|
+
"analysis": "No structured output available",
|
|
92
|
+
"total_count": 0,
|
|
93
|
+
"detections": []
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async def analyze(self, image: Union[Path, Image.Image], **kwargs) -> dict:
|
|
97
|
+
"""
|
|
98
|
+
Analyze the image and classify it into a retail category.
|
|
99
|
+
|
|
100
|
+
:param image: Image Bytes opened with PIL Image.open
|
|
101
|
+
:return: A dictionary containing the classification result.
|
|
102
|
+
"""
|
|
103
|
+
row = kwargs.get("row", None)
|
|
104
|
+
detections_column = kwargs.get(self.column_name, None)
|
|
105
|
+
if detections_column is None:
|
|
106
|
+
detections_column = {}
|
|
107
|
+
else:
|
|
108
|
+
# Make a copy to avoid modifying original
|
|
109
|
+
detections_column = detections_column.copy()
|
|
110
|
+
if self.section_name in detections_column:
|
|
111
|
+
del detections_column[self.section_name]
|
|
112
|
+
|
|
113
|
+
if getattr(self, 'filter_column', None) and getattr(self, 'filter_by', None):
|
|
114
|
+
filter_value = row[self.filter_column] if self.filter_column in row else None
|
|
115
|
+
# Check if filter value is valid and not NA
|
|
116
|
+
if not self._is_valid_filter_value(filter_value):
|
|
117
|
+
self.logger.info(
|
|
118
|
+
f"Skipping detection for row {row.name} - filter column '{self.filter_column}' has NA/invalid value"
|
|
119
|
+
)
|
|
120
|
+
return detections_column
|
|
121
|
+
# Now safe to do the comparison
|
|
122
|
+
if filter_value not in self.filter_by:
|
|
123
|
+
self.logger.info(
|
|
124
|
+
f"Skipping detection for row {row.name} with category {filter_value}"
|
|
125
|
+
)
|
|
126
|
+
return detections_column
|
|
127
|
+
# Open the image
|
|
128
|
+
async with GoogleGenAIClient() as client:
|
|
129
|
+
_result = await client.ask_to_image(
|
|
130
|
+
image=image,
|
|
131
|
+
reference_images=[self.reference_image] if self.reference_image else None,
|
|
132
|
+
prompt=self.prompt,
|
|
133
|
+
structured_output=self._detection_model,
|
|
134
|
+
model=self._model_name
|
|
135
|
+
)
|
|
136
|
+
if _result:
|
|
137
|
+
detections = self._extract_detection_results(_result)
|
|
138
|
+
self.logger.info(f"Successfully detected {detections['total_count']} products")
|
|
139
|
+
self.logger.debug(
|
|
140
|
+
f"Analysis: {detections['analysis'][:100]}..."
|
|
141
|
+
)
|
|
142
|
+
detections_column[self.section_name] = detections
|
|
143
|
+
row[self.column_name] = detections_column
|
|
144
|
+
# Return the updated detections column
|
|
145
|
+
return detections_column
|
|
146
|
+
else:
|
|
147
|
+
self.logger.error(
|
|
148
|
+
"The model did not return a valid Detection result."
|
|
149
|
+
)
|
|
150
|
+
return detections_column
|