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,1179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Crew with Finite State Machine Orchestration
|
|
3
|
+
==================================================
|
|
4
|
+
Enhanced workflow orchestration using python-statemachine for complex agent flows
|
|
5
|
+
with conditional transitions, error handling, and state-based execution control.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- State-based agent lifecycle (idle → ready → running → completed/failed)
|
|
9
|
+
- Conditional transitions with custom predicates
|
|
10
|
+
- Error recovery with fallback agents
|
|
11
|
+
- Dynamic prompt building based on context and dependencies
|
|
12
|
+
- Shared results and context across agents
|
|
13
|
+
- On-success and on-error routing
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
from typing import (
|
|
17
|
+
List, Dict, Any, Union, Optional, Set, Callable, Iterable, Awaitable
|
|
18
|
+
)
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from enum import Enum
|
|
21
|
+
import asyncio
|
|
22
|
+
import uuid
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from statemachine import State, StateMachine
|
|
25
|
+
from navconfig.logging import logging
|
|
26
|
+
|
|
27
|
+
from ..agent import BasicAgent
|
|
28
|
+
from ..abstract import AbstractBot
|
|
29
|
+
from ...tools.manager import ToolManager
|
|
30
|
+
from ...tools.agent import AgentContext
|
|
31
|
+
from ...models.responses import AIMessage, AgentResponse
|
|
32
|
+
from ...models.crew import (
|
|
33
|
+
CrewResult,
|
|
34
|
+
AgentExecutionInfo,
|
|
35
|
+
build_agent_metadata,
|
|
36
|
+
determine_run_status,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Type aliases for better readability
|
|
41
|
+
AgentRef = Union[str, BasicAgent, AbstractBot]
|
|
42
|
+
DependencyResults = Dict[str, str]
|
|
43
|
+
PromptBuilder = Callable[[AgentContext, DependencyResults], Union[str, Awaitable[str]]]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TransitionCondition(str, Enum):
|
|
47
|
+
"""Predefined transition conditions."""
|
|
48
|
+
ON_SUCCESS = "on_success"
|
|
49
|
+
ON_ERROR = "on_error"
|
|
50
|
+
ON_TIMEOUT = "on_timeout"
|
|
51
|
+
ON_CONDITION = "on_condition" # Custom condition
|
|
52
|
+
ALWAYS = "always" # Unconditional transition
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AgentTaskMachine(StateMachine):
|
|
56
|
+
"""
|
|
57
|
+
Finite state machine describing the lifecycle of an agent execution.
|
|
58
|
+
|
|
59
|
+
States:
|
|
60
|
+
idle: Agent is created but not scheduled
|
|
61
|
+
ready: All dependencies satisfied, ready to execute
|
|
62
|
+
running: Agent is currently executing
|
|
63
|
+
completed: Agent finished successfully
|
|
64
|
+
failed: Agent execution failed
|
|
65
|
+
blocked: Agent cannot proceed (missing dependencies or resources)
|
|
66
|
+
|
|
67
|
+
Transitions:
|
|
68
|
+
schedule: idle → ready (when dependencies are met)
|
|
69
|
+
start: ready → running (begin execution)
|
|
70
|
+
succeed: running → completed (successful completion)
|
|
71
|
+
fail: running/ready/idle → failed (error occurred)
|
|
72
|
+
block: idle/ready → blocked (dependencies not met)
|
|
73
|
+
unblock: blocked → ready (dependencies now satisfied)
|
|
74
|
+
retry: failed → ready (retry after failure)
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
idle = State("idle", initial=True)
|
|
78
|
+
ready = State("ready")
|
|
79
|
+
running = State("running")
|
|
80
|
+
completed = State("completed", final=True)
|
|
81
|
+
failed = State("failed")
|
|
82
|
+
blocked = State("blocked")
|
|
83
|
+
|
|
84
|
+
# Primary transitions
|
|
85
|
+
schedule = idle.to(ready)
|
|
86
|
+
start = ready.to(running)
|
|
87
|
+
succeed = running.to(completed)
|
|
88
|
+
fail = running.to(failed) | ready.to(failed) | idle.to(failed)
|
|
89
|
+
block = idle.to(blocked) | ready.to(blocked)
|
|
90
|
+
unblock = blocked.to(ready)
|
|
91
|
+
retry = failed.to(ready)
|
|
92
|
+
|
|
93
|
+
def __init__(self, agent_name: str, **kwargs):
|
|
94
|
+
self.agent_name = agent_name
|
|
95
|
+
super().__init__(**kwargs)
|
|
96
|
+
|
|
97
|
+
def on_enter_running(self):
|
|
98
|
+
"""Called when entering running state."""
|
|
99
|
+
logging.debug(f"Agent {self.agent_name} started execution")
|
|
100
|
+
|
|
101
|
+
def on_enter_completed(self):
|
|
102
|
+
"""Called when entering completed state."""
|
|
103
|
+
logging.info(f"Agent {self.agent_name} completed successfully")
|
|
104
|
+
|
|
105
|
+
def on_enter_failed(self):
|
|
106
|
+
"""Called when entering failed state."""
|
|
107
|
+
logging.error(f"Agent {self.agent_name} execution failed")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class FlowTransition:
|
|
112
|
+
"""
|
|
113
|
+
Represents a transition from one agent to another with conditions.
|
|
114
|
+
|
|
115
|
+
A transition defines:
|
|
116
|
+
- What triggers it (condition)
|
|
117
|
+
- Where it goes (target agents)
|
|
118
|
+
- How to prepare the input (instruction/prompt_builder)
|
|
119
|
+
- What to do on success/failure
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
source: str # Source agent name
|
|
123
|
+
targets: Set[str] # Target agent names
|
|
124
|
+
condition: TransitionCondition = TransitionCondition.ON_SUCCESS
|
|
125
|
+
instruction: Optional[str] = None
|
|
126
|
+
prompt_builder: Optional[PromptBuilder] = None
|
|
127
|
+
predicate: Optional[Callable[[Any], Union[bool, Awaitable[bool]]]] = None
|
|
128
|
+
priority: int = 0 # Higher priority transitions are evaluated first
|
|
129
|
+
metadata: Optional[AgentExecutionInfo] = None
|
|
130
|
+
|
|
131
|
+
async def should_activate(self, result: Any, error: Optional[Exception] = None) -> bool:
|
|
132
|
+
"""
|
|
133
|
+
Determine if this transition should be activated based on the condition.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
result: The result from the source agent
|
|
137
|
+
error: Any error that occurred during source agent execution
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True if the transition should be activated
|
|
141
|
+
"""
|
|
142
|
+
if self.condition == TransitionCondition.ALWAYS:
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
if self.condition == TransitionCondition.ON_SUCCESS:
|
|
146
|
+
return error is None
|
|
147
|
+
|
|
148
|
+
if self.condition == TransitionCondition.ON_ERROR:
|
|
149
|
+
return error is not None
|
|
150
|
+
|
|
151
|
+
if self.condition == TransitionCondition.ON_CONDITION and self.predicate:
|
|
152
|
+
pred_result = self.predicate(result)
|
|
153
|
+
if asyncio.iscoroutine(pred_result):
|
|
154
|
+
return await pred_result
|
|
155
|
+
return bool(pred_result)
|
|
156
|
+
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
async def build_prompt(
|
|
160
|
+
self,
|
|
161
|
+
context: AgentContext,
|
|
162
|
+
dependencies: DependencyResults
|
|
163
|
+
) -> str:
|
|
164
|
+
"""
|
|
165
|
+
Build the prompt for target agents using the prompt_builder or instruction.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
context: The execution context
|
|
169
|
+
dependencies: Results from dependency agents
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The constructed prompt string
|
|
173
|
+
"""
|
|
174
|
+
if self.prompt_builder:
|
|
175
|
+
prompt = self.prompt_builder(context, dependencies)
|
|
176
|
+
return await prompt if asyncio.iscoroutine(prompt) else prompt
|
|
177
|
+
|
|
178
|
+
if self.instruction:
|
|
179
|
+
return self.instruction
|
|
180
|
+
|
|
181
|
+
# Default: use original query with dependency context
|
|
182
|
+
parts = [f"Task: {context.original_query}"]
|
|
183
|
+
|
|
184
|
+
if dependencies:
|
|
185
|
+
parts.append("\nContext from previous agents:")
|
|
186
|
+
for agent_name, result in dependencies.items():
|
|
187
|
+
parts.extend((f"\n--- {agent_name} ---", str(result)))
|
|
188
|
+
|
|
189
|
+
return "\n".join(parts)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass
|
|
193
|
+
class FlowNode:
|
|
194
|
+
"""
|
|
195
|
+
Represents an agent in the FSM-based workflow.
|
|
196
|
+
|
|
197
|
+
A FlowNode wraps an agent with:
|
|
198
|
+
- State machine for lifecycle management
|
|
199
|
+
- Dependencies on other agents
|
|
200
|
+
- Transitions to other agents
|
|
201
|
+
- Execution metadata and results
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
agent: Union[BasicAgent, AbstractBot]
|
|
205
|
+
fsm: AgentTaskMachine
|
|
206
|
+
dependencies: Set[str] = field(default_factory=set)
|
|
207
|
+
outgoing_transitions: List[FlowTransition] = field(default_factory=list)
|
|
208
|
+
result: Optional[Any] = None
|
|
209
|
+
response: Optional[Any] = None
|
|
210
|
+
error: Optional[Exception] = None
|
|
211
|
+
execution_time: float = 0.0
|
|
212
|
+
started_at: Optional[datetime] = None
|
|
213
|
+
completed_at: Optional[datetime] = None
|
|
214
|
+
retry_count: int = 0
|
|
215
|
+
max_retries: int = 3
|
|
216
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
217
|
+
agent_info: Optional[AgentExecutionInfo] = None
|
|
218
|
+
transitions_processed: bool = False # Track if transitions have been activated
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def name(self) -> str:
|
|
222
|
+
"""Get the agent name."""
|
|
223
|
+
return self.agent.name
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def is_terminal(self) -> bool:
|
|
227
|
+
"""Check if this node has no outgoing transitions."""
|
|
228
|
+
return len(self.outgoing_transitions) == 0
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def can_retry(self) -> bool:
|
|
232
|
+
"""Check if this node can be retried."""
|
|
233
|
+
return self.retry_count < self.max_retries and self.fsm.current_state == self.fsm.failed
|
|
234
|
+
|
|
235
|
+
def add_transition(self, transition: FlowTransition) -> None:
|
|
236
|
+
"""Add an outgoing transition from this node."""
|
|
237
|
+
self.outgoing_transitions.append(transition)
|
|
238
|
+
# Sort by priority (descending)
|
|
239
|
+
self.outgoing_transitions.sort(key=lambda t: t.priority, reverse=True)
|
|
240
|
+
|
|
241
|
+
async def get_active_transitions(
|
|
242
|
+
self,
|
|
243
|
+
error: Optional[Exception] = None
|
|
244
|
+
) -> List[FlowTransition]:
|
|
245
|
+
"""
|
|
246
|
+
Get all transitions that should be activated based on current result/error.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
List of transitions that match their activation conditions
|
|
250
|
+
"""
|
|
251
|
+
active = []
|
|
252
|
+
for transition in self.outgoing_transitions:
|
|
253
|
+
if await transition.should_activate(self.result, error):
|
|
254
|
+
active.append(transition)
|
|
255
|
+
return active
|
|
256
|
+
|
|
257
|
+
async def execute(self, prompt:str, ctx: Dict[str, Any]) -> Any:
|
|
258
|
+
"""Execute the agent with context from previous agents."""
|
|
259
|
+
return await self.agent.ask(
|
|
260
|
+
question=prompt,
|
|
261
|
+
**ctx
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class AgentsFlow:
|
|
266
|
+
"""
|
|
267
|
+
Enhanced Agent Crew with Finite State Machine orchestration.
|
|
268
|
+
|
|
269
|
+
This implementation provides sophisticated workflow control with:
|
|
270
|
+
- State-based agent lifecycle management
|
|
271
|
+
- Conditional transitions (on_success, on_error, custom conditions)
|
|
272
|
+
- Error recovery and retry logic
|
|
273
|
+
- Dynamic prompt building
|
|
274
|
+
- Parallel execution where possible
|
|
275
|
+
- Detailed execution tracking and logging
|
|
276
|
+
|
|
277
|
+
Example:
|
|
278
|
+
>>> crew = AgentsFlow(name="ResearchCrew")
|
|
279
|
+
>>>
|
|
280
|
+
>>> # Add agents
|
|
281
|
+
>>> researcher = crew.add_agent(research_agent)
|
|
282
|
+
>>> analyzer = crew.add_agent(analysis_agent)
|
|
283
|
+
>>> writer = crew.add_agent(writer_agent)
|
|
284
|
+
>>> error_handler = crew.add_agent(recovery_agent)
|
|
285
|
+
>>>
|
|
286
|
+
>>> # Define flow with conditional transitions
|
|
287
|
+
>>> crew.task_flow(
|
|
288
|
+
... source=researcher,
|
|
289
|
+
... targets=analyzer,
|
|
290
|
+
... instruction="Analyze the research findings"
|
|
291
|
+
... )
|
|
292
|
+
>>>
|
|
293
|
+
>>> # Add error handling
|
|
294
|
+
>>> crew.task_flow(
|
|
295
|
+
... source=analyzer,
|
|
296
|
+
... targets=error_handler,
|
|
297
|
+
... condition=TransitionCondition.ON_ERROR
|
|
298
|
+
... )
|
|
299
|
+
>>>
|
|
300
|
+
>>> # Execute workflow
|
|
301
|
+
>>> result = await crew.run_flow("Research AI trends in 2025")
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
def __init__(
|
|
305
|
+
self,
|
|
306
|
+
name: str = "AgentsFlow",
|
|
307
|
+
agents: Optional[List[Union[BasicAgent, AbstractBot]]] = None,
|
|
308
|
+
shared_tool_manager: Optional[ToolManager] = None,
|
|
309
|
+
max_parallel_tasks: int = 10,
|
|
310
|
+
default_max_retries: int = 3,
|
|
311
|
+
execution_timeout: Optional[float] = None,
|
|
312
|
+
truncation_length: Optional[int] = None,
|
|
313
|
+
):
|
|
314
|
+
"""
|
|
315
|
+
Initialize the FSM-based Agent Crew.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
name: Name of the crew
|
|
319
|
+
agents: List of agents to add initially
|
|
320
|
+
shared_tool_manager: Shared tool manager for all agents
|
|
321
|
+
max_parallel_tasks: Maximum concurrent agent executions
|
|
322
|
+
default_max_retries: Default retry count for failed agents
|
|
323
|
+
execution_timeout: Maximum time (seconds) for workflow execution
|
|
324
|
+
"""
|
|
325
|
+
self.name = name
|
|
326
|
+
self.nodes: Dict[str, FlowNode] = {}
|
|
327
|
+
self.shared_tool_manager = shared_tool_manager or ToolManager()
|
|
328
|
+
self.max_parallel_tasks = max_parallel_tasks
|
|
329
|
+
self.default_max_retries = default_max_retries
|
|
330
|
+
self.execution_timeout = execution_timeout
|
|
331
|
+
self.logger = logging.getLogger(f"parrot.crews.fsm.{self.name}")
|
|
332
|
+
self.semaphore = asyncio.Semaphore(max_parallel_tasks)
|
|
333
|
+
self.truncation_length = truncation_length or 200
|
|
334
|
+
# Execution tracking
|
|
335
|
+
self.execution_log: List[Dict[str, Any]] = []
|
|
336
|
+
self.current_context: Optional[AgentContext] = None
|
|
337
|
+
self._agent_locks: Dict[int, asyncio.Lock] = {}
|
|
338
|
+
|
|
339
|
+
# Add initial agents
|
|
340
|
+
if agents:
|
|
341
|
+
for agent in agents:
|
|
342
|
+
self.add_agent(agent)
|
|
343
|
+
|
|
344
|
+
def add_agent(
|
|
345
|
+
self,
|
|
346
|
+
agent: Union[BasicAgent, AbstractBot],
|
|
347
|
+
agent_id: Optional[str] = None,
|
|
348
|
+
max_retries: Optional[int] = None
|
|
349
|
+
) -> FlowNode:
|
|
350
|
+
"""
|
|
351
|
+
Add an agent to the crew and return its FlowNode.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
agent: The agent to add
|
|
355
|
+
agent_id: Optional custom ID (defaults to agent.name)
|
|
356
|
+
max_retries: Maximum retry attempts for this agent
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
The created FlowNode for this agent
|
|
360
|
+
"""
|
|
361
|
+
agent_id = agent_id or agent.name
|
|
362
|
+
|
|
363
|
+
if agent_id in self.nodes:
|
|
364
|
+
self.logger.warning(f"Agent '{agent_id}' already exists, skipping")
|
|
365
|
+
return self.nodes[agent_id]
|
|
366
|
+
|
|
367
|
+
# Create FSM for this agent
|
|
368
|
+
fsm = AgentTaskMachine(agent_name=agent_id)
|
|
369
|
+
|
|
370
|
+
# Create FlowNode
|
|
371
|
+
node = FlowNode(
|
|
372
|
+
agent=agent,
|
|
373
|
+
fsm=fsm,
|
|
374
|
+
max_retries=max_retries or self.default_max_retries
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
self.nodes[agent_id] = node
|
|
378
|
+
|
|
379
|
+
# Share tools with new agent
|
|
380
|
+
if self.shared_tool_manager:
|
|
381
|
+
for tool_name in self.shared_tool_manager.list_tools():
|
|
382
|
+
tool = self.shared_tool_manager.get_tool(tool_name)
|
|
383
|
+
if tool and not agent.tool_manager.get_tool(tool_name):
|
|
384
|
+
agent.tool_manager.add_tool(tool, tool_name)
|
|
385
|
+
|
|
386
|
+
self.logger.info(f"Added agent '{agent_id}' to crew")
|
|
387
|
+
return node
|
|
388
|
+
|
|
389
|
+
def _resolve_agent_ref(self, ref: AgentRef) -> str:
|
|
390
|
+
"""Convert an AgentRef to an agent name string."""
|
|
391
|
+
return ref if isinstance(ref, str) else ref.name
|
|
392
|
+
|
|
393
|
+
def task_flow(
|
|
394
|
+
self,
|
|
395
|
+
source: AgentRef,
|
|
396
|
+
targets: Optional[Union[AgentRef, Iterable[AgentRef]]] = None,
|
|
397
|
+
*,
|
|
398
|
+
condition: TransitionCondition = TransitionCondition.ON_SUCCESS,
|
|
399
|
+
instruction: Optional[str] = None,
|
|
400
|
+
prompt_builder: Optional[PromptBuilder] = None,
|
|
401
|
+
predicate: Optional[Callable[[Any], Union[bool, Awaitable[bool]]]] = None,
|
|
402
|
+
priority: int = 0,
|
|
403
|
+
**metadata
|
|
404
|
+
) -> FlowNode:
|
|
405
|
+
"""
|
|
406
|
+
Define transitions from source agent to target agent(s).
|
|
407
|
+
|
|
408
|
+
This method builds the workflow graph by defining how agents connect
|
|
409
|
+
and under what conditions. It supports:
|
|
410
|
+
- Unconditional transitions (always execute targets after source)
|
|
411
|
+
- Success-based transitions (only execute targets if source succeeds)
|
|
412
|
+
- Error-based transitions (only execute targets if source fails)
|
|
413
|
+
- Custom conditional transitions (with predicate function)
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
source: Source agent (agent object, name, or FlowNode)
|
|
417
|
+
targets: Target agent(s) to transition to (None for terminal node)
|
|
418
|
+
condition: When to activate this transition
|
|
419
|
+
instruction: Static instruction/prompt for target agents
|
|
420
|
+
prompt_builder: Dynamic prompt builder function
|
|
421
|
+
predicate: Custom condition predicate for ON_CONDITION transitions
|
|
422
|
+
priority: Transition priority (higher = evaluated first)
|
|
423
|
+
**metadata: Additional metadata for this transition
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
The source FlowNode for method chaining
|
|
427
|
+
|
|
428
|
+
Examples:
|
|
429
|
+
# Simple success-based transition
|
|
430
|
+
crew.task_flow(researcher, analyzer)
|
|
431
|
+
|
|
432
|
+
# Error handling transition
|
|
433
|
+
crew.task_flow(
|
|
434
|
+
analyzer,
|
|
435
|
+
error_handler,
|
|
436
|
+
condition=TransitionCondition.ON_ERROR,
|
|
437
|
+
instruction="Handle the error and retry"
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Conditional branching with predicate
|
|
441
|
+
crew.task_flow(
|
|
442
|
+
classifier,
|
|
443
|
+
[processor_a, processor_b],
|
|
444
|
+
condition=TransitionCondition.ON_CONDITION,
|
|
445
|
+
predicate=lambda result: "category_a" in result.lower()
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Dynamic prompt building
|
|
449
|
+
def build_analysis_prompt(ctx, deps):
|
|
450
|
+
research = deps.get('researcher', '')
|
|
451
|
+
return f"Analyze this research in detail:\n{research}"
|
|
452
|
+
|
|
453
|
+
crew.task_flow(
|
|
454
|
+
researcher,
|
|
455
|
+
analyzer,
|
|
456
|
+
prompt_builder=build_analysis_prompt
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# Method chaining for complex flows
|
|
460
|
+
crew.task_flow(start, process).task_flow(process, analyze).task_flow(analyze, end)
|
|
461
|
+
"""
|
|
462
|
+
source_name = self._resolve_agent_ref(source)
|
|
463
|
+
|
|
464
|
+
if source_name not in self.nodes:
|
|
465
|
+
raise ValueError(f"Source agent '{source_name}' not found in crew")
|
|
466
|
+
|
|
467
|
+
source_node = self.nodes[source_name]
|
|
468
|
+
|
|
469
|
+
# Handle terminal node (no targets)
|
|
470
|
+
if targets is None:
|
|
471
|
+
self.logger.info(f"Agent '{source_name}' is a terminal node")
|
|
472
|
+
return source_node
|
|
473
|
+
|
|
474
|
+
# Normalize targets to a list
|
|
475
|
+
if not isinstance(targets, (list, tuple, set)):
|
|
476
|
+
targets = [targets]
|
|
477
|
+
|
|
478
|
+
target_names = {self._resolve_agent_ref(t) for t in targets}
|
|
479
|
+
|
|
480
|
+
# Validate all targets exist
|
|
481
|
+
for target_name in target_names:
|
|
482
|
+
if target_name not in self.nodes:
|
|
483
|
+
raise ValueError(f"Target agent '{target_name}' not found in crew")
|
|
484
|
+
|
|
485
|
+
# Create transition
|
|
486
|
+
transition = FlowTransition(
|
|
487
|
+
source=source_name,
|
|
488
|
+
targets=target_names,
|
|
489
|
+
condition=condition,
|
|
490
|
+
instruction=instruction,
|
|
491
|
+
prompt_builder=prompt_builder,
|
|
492
|
+
predicate=predicate,
|
|
493
|
+
priority=priority,
|
|
494
|
+
metadata=metadata
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
source_node.add_transition(transition)
|
|
498
|
+
|
|
499
|
+
# Update dependencies in target nodes
|
|
500
|
+
for target_name in target_names:
|
|
501
|
+
target_node = self.nodes[target_name]
|
|
502
|
+
|
|
503
|
+
if self._would_create_cycle(source_name, target_name):
|
|
504
|
+
self.logger.debug(
|
|
505
|
+
"Skipping dependency %s → %s to avoid circular dependency",
|
|
506
|
+
source_name,
|
|
507
|
+
target_name
|
|
508
|
+
)
|
|
509
|
+
continue
|
|
510
|
+
|
|
511
|
+
target_node.dependencies.add(source_name)
|
|
512
|
+
|
|
513
|
+
self.logger.info(
|
|
514
|
+
f"Added {condition.value} transition: {source_name} → {target_names}"
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
return source_node
|
|
518
|
+
|
|
519
|
+
def on_success(
|
|
520
|
+
self,
|
|
521
|
+
source: AgentRef,
|
|
522
|
+
targets: Union[AgentRef, Iterable[AgentRef]],
|
|
523
|
+
**kwargs
|
|
524
|
+
) -> FlowNode:
|
|
525
|
+
"""Convenience method for ON_SUCCESS transitions."""
|
|
526
|
+
return self.task_flow(
|
|
527
|
+
source,
|
|
528
|
+
targets,
|
|
529
|
+
condition=TransitionCondition.ON_SUCCESS,
|
|
530
|
+
**kwargs
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
def on_error(
|
|
534
|
+
self,
|
|
535
|
+
source: AgentRef,
|
|
536
|
+
targets: Union[AgentRef, Iterable[AgentRef]],
|
|
537
|
+
**kwargs
|
|
538
|
+
) -> FlowNode:
|
|
539
|
+
"""Convenience method for ON_ERROR transitions."""
|
|
540
|
+
return self.task_flow(
|
|
541
|
+
source,
|
|
542
|
+
targets,
|
|
543
|
+
condition=TransitionCondition.ON_ERROR,
|
|
544
|
+
**kwargs
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
def on_condition(
|
|
548
|
+
self,
|
|
549
|
+
source: AgentRef,
|
|
550
|
+
targets: Union[AgentRef, Iterable[AgentRef]],
|
|
551
|
+
predicate: Callable[[Any], Union[bool, Awaitable[bool]]],
|
|
552
|
+
**kwargs
|
|
553
|
+
) -> FlowNode:
|
|
554
|
+
"""Convenience method for ON_CONDITION transitions."""
|
|
555
|
+
return self.task_flow(
|
|
556
|
+
source,
|
|
557
|
+
targets,
|
|
558
|
+
condition=TransitionCondition.ON_CONDITION,
|
|
559
|
+
predicate=predicate,
|
|
560
|
+
**kwargs
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
async def run_flow(
|
|
564
|
+
self,
|
|
565
|
+
initial_task: str,
|
|
566
|
+
entry_point: Optional[AgentRef] = None,
|
|
567
|
+
user_id: Optional[str] = None,
|
|
568
|
+
session_id: Optional[str] = None,
|
|
569
|
+
max_iterations: int = 100,
|
|
570
|
+
**shared_data
|
|
571
|
+
) -> CrewResult:
|
|
572
|
+
"""
|
|
573
|
+
Execute the workflow starting from the entry point.
|
|
574
|
+
|
|
575
|
+
The workflow execution follows these steps:
|
|
576
|
+
1. Initialize all agents to idle state
|
|
577
|
+
2. Schedule entry point agent(s) to ready state
|
|
578
|
+
3. Execute ready agents (respecting max_parallel_tasks)
|
|
579
|
+
4. Evaluate transitions based on results/errors
|
|
580
|
+
5. Schedule next agents based on activated transitions
|
|
581
|
+
6. Repeat until all terminal nodes complete or max_iterations reached
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
initial_task: The initial task/prompt for the workflow
|
|
585
|
+
entry_point: Starting agent(s) (defaults to agents with no dependencies)
|
|
586
|
+
user_id: User identifier for tracking
|
|
587
|
+
session_id: Session identifier
|
|
588
|
+
max_iterations: Maximum execution rounds (safety limit)
|
|
589
|
+
**shared_data: Additional shared data for all agents
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
CrewResult: Standardized execution payload containing outputs,
|
|
593
|
+
metadata, and execution logs.
|
|
594
|
+
|
|
595
|
+
Raises:
|
|
596
|
+
RuntimeError: If workflow gets stuck or exceeds max_iterations
|
|
597
|
+
TimeoutError: If execution_timeout is exceeded
|
|
598
|
+
"""
|
|
599
|
+
session_id = session_id or str(uuid.uuid4())
|
|
600
|
+
user_id = user_id or 'crew_user'
|
|
601
|
+
|
|
602
|
+
# Initialize execution context
|
|
603
|
+
self.current_context = AgentContext(
|
|
604
|
+
user_id=user_id,
|
|
605
|
+
session_id=session_id,
|
|
606
|
+
original_query=initial_task,
|
|
607
|
+
shared_data=shared_data,
|
|
608
|
+
agent_results={}
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
self.execution_log = []
|
|
612
|
+
start_time = asyncio.get_event_loop().time()
|
|
613
|
+
|
|
614
|
+
# Reset all agents to idle state by creating new FSM instances
|
|
615
|
+
for node in self.nodes.values():
|
|
616
|
+
# Create a new FSM instance to reset to initial state
|
|
617
|
+
node.fsm = AgentTaskMachine(agent_name=node.agent.name)
|
|
618
|
+
node.result = None
|
|
619
|
+
node.response = None
|
|
620
|
+
node.error = None
|
|
621
|
+
node.retry_count = 0
|
|
622
|
+
node.transitions_processed = False # Reset transition processing flag
|
|
623
|
+
node.metadata = None
|
|
624
|
+
|
|
625
|
+
# Determine entry points
|
|
626
|
+
entry_agents = self._get_entry_agents(entry_point)
|
|
627
|
+
|
|
628
|
+
if not entry_agents:
|
|
629
|
+
raise ValueError("No entry point agents found. Specify entry_point or add agents with no dependencies.")
|
|
630
|
+
|
|
631
|
+
self.logger.info(f"Starting workflow with entry points: {entry_agents}")
|
|
632
|
+
|
|
633
|
+
# Schedule entry point agents
|
|
634
|
+
for agent_name in entry_agents:
|
|
635
|
+
node = self.nodes[agent_name]
|
|
636
|
+
node.fsm.schedule()
|
|
637
|
+
|
|
638
|
+
# Main execution loop
|
|
639
|
+
iteration = 0
|
|
640
|
+
while iteration < max_iterations:
|
|
641
|
+
# Check timeout
|
|
642
|
+
if self.execution_timeout:
|
|
643
|
+
elapsed = asyncio.get_event_loop().time() - start_time
|
|
644
|
+
if elapsed > self.execution_timeout:
|
|
645
|
+
raise TimeoutError(
|
|
646
|
+
f"Workflow execution exceeded timeout of {self.execution_timeout}s"
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
# Get agents ready to execute
|
|
650
|
+
ready_agents = self._get_ready_agents()
|
|
651
|
+
|
|
652
|
+
if not ready_agents:
|
|
653
|
+
# Check if we're done
|
|
654
|
+
if self._is_workflow_complete():
|
|
655
|
+
break
|
|
656
|
+
|
|
657
|
+
# Check if we're stuck
|
|
658
|
+
if not self._has_active_agents():
|
|
659
|
+
raise RuntimeError(
|
|
660
|
+
f"Workflow is stuck at iteration {iteration}. "
|
|
661
|
+
f"No ready agents and no active agents. "
|
|
662
|
+
f"This may indicate missing transitions or unsatisfied dependencies."
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
# Wait for active agents
|
|
666
|
+
await asyncio.sleep(0.1)
|
|
667
|
+
iteration += 1
|
|
668
|
+
continue
|
|
669
|
+
|
|
670
|
+
# Execute ready agents in parallel
|
|
671
|
+
await self._execute_agents_parallel(ready_agents)
|
|
672
|
+
|
|
673
|
+
# Process completed agents and activate transitions
|
|
674
|
+
await self._process_transitions()
|
|
675
|
+
|
|
676
|
+
iteration += 1
|
|
677
|
+
|
|
678
|
+
if iteration >= max_iterations:
|
|
679
|
+
raise RuntimeError(
|
|
680
|
+
f"Workflow exceeded max iterations ({max_iterations}). "
|
|
681
|
+
f"Possible infinite loop or very complex workflow."
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
end_time = asyncio.get_event_loop().time()
|
|
685
|
+
|
|
686
|
+
agent_ids: List[str] = []
|
|
687
|
+
for entry in self.execution_log:
|
|
688
|
+
agent_name = entry.get("agent_id") or entry.get("agent_name")
|
|
689
|
+
if agent_name and agent_name not in agent_ids:
|
|
690
|
+
agent_ids.append(agent_name)
|
|
691
|
+
|
|
692
|
+
for agent_name in self.nodes:
|
|
693
|
+
if agent_name not in agent_ids:
|
|
694
|
+
agent_ids.append(agent_name)
|
|
695
|
+
|
|
696
|
+
responses: Dict[str, Any] = {}
|
|
697
|
+
agents_info: List[AgentExecutionInfo] = []
|
|
698
|
+
results_payload: List[Any] = []
|
|
699
|
+
errors: Dict[str, str] = {}
|
|
700
|
+
last_output: Optional[Any] = None
|
|
701
|
+
|
|
702
|
+
for agent_name in agent_ids:
|
|
703
|
+
node = self.nodes.get(agent_name)
|
|
704
|
+
if not node:
|
|
705
|
+
continue
|
|
706
|
+
|
|
707
|
+
if node.result is not None:
|
|
708
|
+
results_payload.append(node.result)
|
|
709
|
+
responses[agent_name] = node.response
|
|
710
|
+
metadata = (
|
|
711
|
+
node.metadata
|
|
712
|
+
if isinstance(node.metadata, AgentExecutionInfo)
|
|
713
|
+
else build_agent_metadata(
|
|
714
|
+
agent_name,
|
|
715
|
+
node.agent,
|
|
716
|
+
node.response,
|
|
717
|
+
node.result,
|
|
718
|
+
node.execution_time,
|
|
719
|
+
'completed'
|
|
720
|
+
)
|
|
721
|
+
)
|
|
722
|
+
agents_info.append(metadata)
|
|
723
|
+
last_output = node.result
|
|
724
|
+
else:
|
|
725
|
+
results_payload.append(node.result)
|
|
726
|
+
responses[agent_name] = node.response
|
|
727
|
+
status_value = 'failed' if node.error is not None else 'pending'
|
|
728
|
+
error_message = str(node.error) if node.error else None
|
|
729
|
+
if error_message:
|
|
730
|
+
errors[agent_name] = error_message
|
|
731
|
+
metadata = (
|
|
732
|
+
node.metadata
|
|
733
|
+
if isinstance(node.metadata, AgentExecutionInfo)
|
|
734
|
+
else build_agent_metadata(
|
|
735
|
+
agent_name,
|
|
736
|
+
node.agent,
|
|
737
|
+
node.response,
|
|
738
|
+
node.result,
|
|
739
|
+
node.execution_time,
|
|
740
|
+
status_value,
|
|
741
|
+
error_message
|
|
742
|
+
)
|
|
743
|
+
)
|
|
744
|
+
agents_info.append(metadata)
|
|
745
|
+
|
|
746
|
+
success_count = sum(info.status == 'completed' for info in agents_info)
|
|
747
|
+
failure_count = sum(info.status == 'failed' for info in agents_info)
|
|
748
|
+
status = determine_run_status(success_count, failure_count)
|
|
749
|
+
|
|
750
|
+
# Get final output from terminal nodes
|
|
751
|
+
terminal_results = [
|
|
752
|
+
node.result
|
|
753
|
+
for node in self.nodes.values()
|
|
754
|
+
if node.is_terminal and node.fsm.current_state == node.fsm.completed
|
|
755
|
+
]
|
|
756
|
+
final_output = terminal_results[-1] if terminal_results else ''
|
|
757
|
+
|
|
758
|
+
return CrewResult(
|
|
759
|
+
output=final_output or last_output,
|
|
760
|
+
response=responses,
|
|
761
|
+
results=results_payload,
|
|
762
|
+
agent_ids=agent_ids,
|
|
763
|
+
agents=agents_info,
|
|
764
|
+
errors=errors,
|
|
765
|
+
execution_log=self.execution_log,
|
|
766
|
+
total_time=end_time - start_time,
|
|
767
|
+
status=status,
|
|
768
|
+
metadata={
|
|
769
|
+
'mode': 'fsm',
|
|
770
|
+
'iterations': iteration,
|
|
771
|
+
'completed': success_count,
|
|
772
|
+
'failed': failure_count
|
|
773
|
+
}
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _get_entry_agents(self, entry_point: Optional[AgentRef]) -> Set[str]:
|
|
778
|
+
"""Determine which agents should be entry points."""
|
|
779
|
+
if entry_point:
|
|
780
|
+
if isinstance(entry_point, (list, tuple, set)):
|
|
781
|
+
return {self._resolve_agent_ref(e) for e in entry_point}
|
|
782
|
+
return {self._resolve_agent_ref(entry_point)}
|
|
783
|
+
|
|
784
|
+
# Find agents with no dependencies
|
|
785
|
+
return {
|
|
786
|
+
name for name, node in self.nodes.items()
|
|
787
|
+
if not node.dependencies
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
def _get_ready_agents(self) -> Set[str]:
|
|
791
|
+
"""Get all agents in ready state."""
|
|
792
|
+
return {
|
|
793
|
+
name for name, node in self.nodes.items()
|
|
794
|
+
if node.fsm.current_state == node.fsm.ready
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
def _has_active_agents(self) -> bool:
|
|
798
|
+
"""Check if any agents are currently running."""
|
|
799
|
+
return any(
|
|
800
|
+
node.fsm.current_state == node.fsm.running
|
|
801
|
+
for node in self.nodes.values()
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
def _truncate_text(self, text: Optional[str], *, enabled: bool = True) -> str:
|
|
805
|
+
"""Truncate text using configured length."""
|
|
806
|
+
if text is None or not enabled:
|
|
807
|
+
return text or ""
|
|
808
|
+
|
|
809
|
+
if self.truncation_length is None or self.truncation_length <= 0:
|
|
810
|
+
return text
|
|
811
|
+
|
|
812
|
+
if len(text) <= self.truncation_length:
|
|
813
|
+
return text
|
|
814
|
+
|
|
815
|
+
return f"{text[:self.truncation_length]}..."
|
|
816
|
+
|
|
817
|
+
def _is_workflow_complete(self) -> bool:
|
|
818
|
+
"""Check if all terminal nodes have completed or failed (without retries)."""
|
|
819
|
+
terminal_nodes = [
|
|
820
|
+
node for node in self.nodes.values() if node.is_terminal
|
|
821
|
+
]
|
|
822
|
+
|
|
823
|
+
if terminal_nodes:
|
|
824
|
+
# Terminal nodes are complete if they're in completed state OR
|
|
825
|
+
# in failed state with no retries remaining
|
|
826
|
+
return all(
|
|
827
|
+
node.fsm.current_state == node.fsm.completed or
|
|
828
|
+
(node.fsm.current_state == node.fsm.failed and not node.can_retry)
|
|
829
|
+
for node in terminal_nodes
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
# If no terminal nodes defined, check if all nodes are done
|
|
833
|
+
return all(
|
|
834
|
+
node.fsm.current_state == node.fsm.completed or
|
|
835
|
+
(node.fsm.current_state == node.fsm.failed and not node.can_retry)
|
|
836
|
+
for node in self.nodes.values()
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
async def _execute_agents_parallel(self, agent_names: Set[str]) -> None:
|
|
840
|
+
"""Execute multiple agents in parallel."""
|
|
841
|
+
tasks = []
|
|
842
|
+
|
|
843
|
+
for agent_name in agent_names:
|
|
844
|
+
node = self.nodes[agent_name]
|
|
845
|
+
node.fsm.start() # Transition to running state
|
|
846
|
+
tasks.append(self._execute_single_agent(agent_name))
|
|
847
|
+
|
|
848
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
849
|
+
|
|
850
|
+
async def _execute_single_agent(self, agent_name: str) -> None:
|
|
851
|
+
"""Execute a single agent and update its state."""
|
|
852
|
+
node = self.nodes[agent_name]
|
|
853
|
+
node.started_at = datetime.now()
|
|
854
|
+
|
|
855
|
+
try:
|
|
856
|
+
# Ensure agent is configured
|
|
857
|
+
await self._ensure_agent_ready(node.agent)
|
|
858
|
+
|
|
859
|
+
# Build prompt based on dependencies
|
|
860
|
+
prompt = await self._build_agent_prompt(node)
|
|
861
|
+
|
|
862
|
+
# Execute with semaphore for rate limiting
|
|
863
|
+
async with self.semaphore:
|
|
864
|
+
start_time = asyncio.get_event_loop().time()
|
|
865
|
+
|
|
866
|
+
response = await node.execute(
|
|
867
|
+
prompt=prompt,
|
|
868
|
+
ctx={
|
|
869
|
+
"session_id": self.current_context.session_id,
|
|
870
|
+
"user_id": self.current_context.user_id,
|
|
871
|
+
**self.current_context.shared_data
|
|
872
|
+
}
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
end_time = asyncio.get_event_loop().time()
|
|
876
|
+
node.execution_time = end_time - start_time
|
|
877
|
+
|
|
878
|
+
# Extract result
|
|
879
|
+
result = self._extract_result(response)
|
|
880
|
+
node.result = result
|
|
881
|
+
node.response = response
|
|
882
|
+
node.completed_at = datetime.now()
|
|
883
|
+
# Build agent execution info
|
|
884
|
+
node.agent_info = build_agent_metadata(
|
|
885
|
+
agent_id=agent_name,
|
|
886
|
+
agent=node.agent,
|
|
887
|
+
response=response,
|
|
888
|
+
output=result,
|
|
889
|
+
execution_time=node.execution_time,
|
|
890
|
+
status='completed',
|
|
891
|
+
error=None
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
# Store in context
|
|
895
|
+
self.current_context.agent_results[agent_name] = result
|
|
896
|
+
|
|
897
|
+
# Transition to completed
|
|
898
|
+
node.fsm.succeed()
|
|
899
|
+
|
|
900
|
+
# Log execution
|
|
901
|
+
self.execution_log.append({
|
|
902
|
+
"agent_id": agent_name,
|
|
903
|
+
"agent_name": agent_name,
|
|
904
|
+
"state": "completed",
|
|
905
|
+
"execution_time": node.execution_time,
|
|
906
|
+
"input": self._truncate_text(prompt),
|
|
907
|
+
"output": self._truncate_text(result),
|
|
908
|
+
"started_at": node.started_at.isoformat(),
|
|
909
|
+
"completed_at": node.completed_at.isoformat(),
|
|
910
|
+
"result_length": len(str(result)),
|
|
911
|
+
"success": True
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
self.logger.info(
|
|
915
|
+
f"Agent '{agent_name}' completed in {node.execution_time:.2f}s"
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
except Exception as e:
|
|
919
|
+
node.error = e
|
|
920
|
+
node.completed_at = datetime.now()
|
|
921
|
+
node.fsm.fail()
|
|
922
|
+
node.response = None
|
|
923
|
+
# Build agent execution info for failure
|
|
924
|
+
node.agent_info = build_agent_metadata(
|
|
925
|
+
agent_id=agent_name,
|
|
926
|
+
agent=node.agent,
|
|
927
|
+
response=None,
|
|
928
|
+
output=None,
|
|
929
|
+
execution_time=node.execution_time or 0.0,
|
|
930
|
+
status='failed',
|
|
931
|
+
error=str(e)
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
self.execution_log.append({
|
|
935
|
+
"agent_id": agent_name,
|
|
936
|
+
"agent_name": agent_name,
|
|
937
|
+
"state": "failed",
|
|
938
|
+
"error": str(e),
|
|
939
|
+
"started_at": node.started_at.isoformat() if node.started_at else None,
|
|
940
|
+
"completed_at": node.completed_at.isoformat(),
|
|
941
|
+
"retry_count": node.retry_count,
|
|
942
|
+
"success": False
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
self.logger.error(
|
|
946
|
+
f"Agent '{agent_name}' failed: {e}",
|
|
947
|
+
exc_info=True
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
async def _process_transitions(self) -> None:
|
|
951
|
+
"""Process transitions for all completed/failed agents."""
|
|
952
|
+
for agent_name, node in self.nodes.items():
|
|
953
|
+
# Only process nodes that just completed or failed AND haven't been processed yet
|
|
954
|
+
if node.fsm.current_state not in [node.fsm.completed, node.fsm.failed]:
|
|
955
|
+
continue
|
|
956
|
+
|
|
957
|
+
# Skip if transitions already processed
|
|
958
|
+
if node.transitions_processed:
|
|
959
|
+
continue
|
|
960
|
+
|
|
961
|
+
# Get active transitions
|
|
962
|
+
error = node.error if node.fsm.current_state == node.fsm.failed else None
|
|
963
|
+
active_transitions = await node.get_active_transitions(error)
|
|
964
|
+
|
|
965
|
+
if not active_transitions:
|
|
966
|
+
# Check for retry on failure
|
|
967
|
+
if node.fsm.current_state == node.fsm.failed and node.can_retry:
|
|
968
|
+
self.logger.info(
|
|
969
|
+
f"Retrying agent '{agent_name}' (attempt {node.retry_count + 1}/{node.max_retries})"
|
|
970
|
+
)
|
|
971
|
+
node.retry_count += 1
|
|
972
|
+
node.fsm.retry()
|
|
973
|
+
# Don't mark as processed - allow retry to execute
|
|
974
|
+
node.transitions_processed = False
|
|
975
|
+
else:
|
|
976
|
+
# Mark as processed if no transitions and no retry
|
|
977
|
+
node.transitions_processed = True
|
|
978
|
+
continue
|
|
979
|
+
|
|
980
|
+
# Activate transitions
|
|
981
|
+
transition_activated = False
|
|
982
|
+
for transition in active_transitions:
|
|
983
|
+
if await self._activate_transition(transition):
|
|
984
|
+
transition_activated = True
|
|
985
|
+
|
|
986
|
+
# Mark transitions as processed only when activation succeeded
|
|
987
|
+
node.transitions_processed = transition_activated
|
|
988
|
+
|
|
989
|
+
async def _activate_transition(self, transition: FlowTransition) -> bool:
|
|
990
|
+
"""Activate a transition and schedule target agents.
|
|
991
|
+
|
|
992
|
+
Returns:
|
|
993
|
+
True if at least one target agent was scheduled or reactivated.
|
|
994
|
+
"""
|
|
995
|
+
scheduled_any = False
|
|
996
|
+
|
|
997
|
+
for target_name in transition.targets:
|
|
998
|
+
target_node = self.nodes[target_name]
|
|
999
|
+
scheduled = False
|
|
1000
|
+
|
|
1001
|
+
if not self._are_dependencies_satisfied(target_node):
|
|
1002
|
+
self.logger.debug(
|
|
1003
|
+
"Dependencies for '%s' not yet satisfied after transition from '%s'",
|
|
1004
|
+
target_name,
|
|
1005
|
+
transition.source
|
|
1006
|
+
)
|
|
1007
|
+
continue
|
|
1008
|
+
|
|
1009
|
+
if target_node.fsm.current_state == target_node.fsm.idle:
|
|
1010
|
+
target_node.fsm.schedule()
|
|
1011
|
+
scheduled = True
|
|
1012
|
+
elif target_node.fsm.current_state == target_node.fsm.blocked:
|
|
1013
|
+
target_node.fsm.unblock()
|
|
1014
|
+
scheduled = True
|
|
1015
|
+
elif target_node.fsm.current_state == target_node.fsm.failed and target_node.can_retry:
|
|
1016
|
+
target_node.fsm.retry()
|
|
1017
|
+
scheduled = True
|
|
1018
|
+
elif target_node.fsm.current_state == target_node.fsm.ready:
|
|
1019
|
+
scheduled = True
|
|
1020
|
+
|
|
1021
|
+
if scheduled:
|
|
1022
|
+
self.logger.debug(
|
|
1023
|
+
f"Scheduled agent '{target_name}' via transition from '{transition.source}'"
|
|
1024
|
+
)
|
|
1025
|
+
scheduled_any = True
|
|
1026
|
+
|
|
1027
|
+
return scheduled_any
|
|
1028
|
+
|
|
1029
|
+
def _would_create_cycle(self, source_name: str, target_name: str) -> bool:
|
|
1030
|
+
"""Check if adding a dependency would introduce a cycle."""
|
|
1031
|
+
if source_name == target_name:
|
|
1032
|
+
return True
|
|
1033
|
+
|
|
1034
|
+
visited = set()
|
|
1035
|
+
stack = [source_name]
|
|
1036
|
+
|
|
1037
|
+
while stack:
|
|
1038
|
+
current = stack.pop()
|
|
1039
|
+
if current == target_name:
|
|
1040
|
+
return True
|
|
1041
|
+
if current in visited:
|
|
1042
|
+
continue
|
|
1043
|
+
visited.add(current)
|
|
1044
|
+
|
|
1045
|
+
current_node = self.nodes.get(current)
|
|
1046
|
+
if not current_node:
|
|
1047
|
+
continue
|
|
1048
|
+
|
|
1049
|
+
stack.extend(current_node.dependencies)
|
|
1050
|
+
|
|
1051
|
+
return False
|
|
1052
|
+
|
|
1053
|
+
def _are_dependencies_satisfied(self, node: FlowNode) -> bool:
|
|
1054
|
+
"""Check if all dependencies for a node are satisfied."""
|
|
1055
|
+
for dep_name in node.dependencies:
|
|
1056
|
+
dep_node = self.nodes[dep_name]
|
|
1057
|
+
if dep_node.fsm.current_state != dep_node.fsm.completed:
|
|
1058
|
+
return False
|
|
1059
|
+
return True
|
|
1060
|
+
|
|
1061
|
+
async def _build_agent_prompt(self, node: FlowNode) -> str:
|
|
1062
|
+
"""Build the prompt for an agent based on its dependencies and transitions."""
|
|
1063
|
+
# Gather results from dependencies
|
|
1064
|
+
dependencies = {}
|
|
1065
|
+
for dep_name in node.dependencies:
|
|
1066
|
+
dep_node = self.nodes[dep_name]
|
|
1067
|
+
if dep_node.result is not None:
|
|
1068
|
+
dependencies[dep_name] = dep_node.result
|
|
1069
|
+
|
|
1070
|
+
# Find the transition that activated this agent
|
|
1071
|
+
activating_transition = None
|
|
1072
|
+
for dep_name in node.dependencies:
|
|
1073
|
+
dep_node = self.nodes[dep_name]
|
|
1074
|
+
for transition in dep_node.outgoing_transitions:
|
|
1075
|
+
if node.name in transition.targets:
|
|
1076
|
+
activating_transition = transition
|
|
1077
|
+
break
|
|
1078
|
+
if activating_transition:
|
|
1079
|
+
break
|
|
1080
|
+
|
|
1081
|
+
# Use transition's prompt builder if available
|
|
1082
|
+
if activating_transition:
|
|
1083
|
+
return await activating_transition.build_prompt(
|
|
1084
|
+
self.current_context,
|
|
1085
|
+
dependencies
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
# Default prompt building
|
|
1089
|
+
if not dependencies:
|
|
1090
|
+
return self.current_context.original_query
|
|
1091
|
+
|
|
1092
|
+
parts = [
|
|
1093
|
+
f"Task: {self.current_context.original_query}\n",
|
|
1094
|
+
"\nContext from previous agents:",
|
|
1095
|
+
]
|
|
1096
|
+
|
|
1097
|
+
for dep_name, result in dependencies.items():
|
|
1098
|
+
parts.extend((f"\n--- {dep_name} ---", str(result)))
|
|
1099
|
+
|
|
1100
|
+
return "\n".join(parts)
|
|
1101
|
+
|
|
1102
|
+
async def _ensure_agent_ready(self, agent: Union[BasicAgent, AbstractBot]) -> None:
|
|
1103
|
+
"""Ensure agent is configured before execution."""
|
|
1104
|
+
if hasattr(agent, "is_configured") and agent.is_configured:
|
|
1105
|
+
return
|
|
1106
|
+
|
|
1107
|
+
agent_id = id(agent)
|
|
1108
|
+
lock = self._agent_locks.get(agent_id)
|
|
1109
|
+
if lock is None:
|
|
1110
|
+
lock = asyncio.Lock()
|
|
1111
|
+
self._agent_locks[agent_id] = lock
|
|
1112
|
+
|
|
1113
|
+
async with lock:
|
|
1114
|
+
if not (hasattr(agent, "is_configured") and agent.is_configured):
|
|
1115
|
+
self.logger.info(f"Auto-configuring agent '{agent.name}'")
|
|
1116
|
+
await agent.configure()
|
|
1117
|
+
|
|
1118
|
+
def _extract_result(self, response: Any) -> str:
|
|
1119
|
+
"""Extract result string from response."""
|
|
1120
|
+
if isinstance(response, (AIMessage, AgentResponse)) or hasattr(response, 'content'):
|
|
1121
|
+
return response.content
|
|
1122
|
+
return str(response)
|
|
1123
|
+
|
|
1124
|
+
def visualize_workflow(self, format: str = "mermaid") -> str:
|
|
1125
|
+
"""
|
|
1126
|
+
Generate a visual representation of the workflow.
|
|
1127
|
+
|
|
1128
|
+
Args:
|
|
1129
|
+
format: Output format ("mermaid" for Mermaid diagrams)
|
|
1130
|
+
|
|
1131
|
+
Returns:
|
|
1132
|
+
String representation of the workflow diagram
|
|
1133
|
+
"""
|
|
1134
|
+
if format == "mermaid":
|
|
1135
|
+
lines = ["graph TD"]
|
|
1136
|
+
|
|
1137
|
+
for agent_name, node in self.nodes.items():
|
|
1138
|
+
# Node style based on state
|
|
1139
|
+
if node.fsm.current_state == node.fsm.completed:
|
|
1140
|
+
lines.append(f" {agent_name}[{agent_name}]:::completed")
|
|
1141
|
+
elif node.fsm.current_state == node.fsm.failed:
|
|
1142
|
+
lines.append(f" {agent_name}[{agent_name}]:::failed")
|
|
1143
|
+
elif node.fsm.current_state == node.fsm.running:
|
|
1144
|
+
lines.append(f" {agent_name}[{agent_name}]:::running")
|
|
1145
|
+
else:
|
|
1146
|
+
lines.append(f" {agent_name}[{agent_name}]")
|
|
1147
|
+
|
|
1148
|
+
# Transitions
|
|
1149
|
+
for transition in node.outgoing_transitions:
|
|
1150
|
+
for target in transition.targets:
|
|
1151
|
+
arrow_label = transition.condition.value
|
|
1152
|
+
lines.append(f" {agent_name} -->|{arrow_label}| {target}")
|
|
1153
|
+
|
|
1154
|
+
# Styles
|
|
1155
|
+
lines.extend([
|
|
1156
|
+
" classDef completed fill:#90EE90",
|
|
1157
|
+
" classDef failed fill:#FFB6C1",
|
|
1158
|
+
" classDef running fill:#87CEEB"
|
|
1159
|
+
])
|
|
1160
|
+
|
|
1161
|
+
return "\n".join(lines)
|
|
1162
|
+
|
|
1163
|
+
raise ValueError(f"Unsupported format: {format}")
|
|
1164
|
+
|
|
1165
|
+
def get_workflow_stats(self) -> Dict[str, Any]:
|
|
1166
|
+
"""Get statistics about the workflow."""
|
|
1167
|
+
total_nodes = len(self.nodes)
|
|
1168
|
+
states_count = {}
|
|
1169
|
+
|
|
1170
|
+
for node in self.nodes.values():
|
|
1171
|
+
state_name = node.fsm.current_state.name
|
|
1172
|
+
states_count[state_name] = states_count.get(state_name, 0) + 1
|
|
1173
|
+
|
|
1174
|
+
return {
|
|
1175
|
+
"total_agents": total_nodes,
|
|
1176
|
+
"states": states_count,
|
|
1177
|
+
"execution_log_entries": len(self.execution_log),
|
|
1178
|
+
"has_context": self.current_context is not None
|
|
1179
|
+
}
|