Compare commits
25 Commits
tm-track-i
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ceef062426 | ||
|
|
e44d778e0e | ||
|
|
5645cbb22e | ||
|
|
8f022be106 | ||
|
|
6a19b0a279 | ||
|
|
641c336b2c | ||
|
|
22f1812824 | ||
|
|
9edbf89b68 | ||
|
|
685f7b9af1 | ||
|
|
595fdfb6e7 | ||
|
|
8f99fa76ed | ||
|
|
17e3fcbe1f | ||
|
|
b858d705a8 | ||
|
|
d60f7b360d | ||
|
|
6050a7b3e0 | ||
|
|
46846bcace | ||
|
|
d71e91e8f2 | ||
|
|
9a212b8e29 | ||
|
|
67953b3a6a | ||
|
|
a760923c50 | ||
|
|
1c4f44af80 | ||
|
|
09014215a9 | ||
|
|
0ccc155457 | ||
|
|
8945457883 | ||
|
|
b787d7e591 |
1
.gitignore
vendored
@@ -26,3 +26,4 @@ plan.md
|
||||
conceptual_plan.md
|
||||
build_image
|
||||
chromadb-*.lock
|
||||
.claude
|
||||
|
||||
@@ -120,6 +120,8 @@ HEADERS_TO_FILTER = {
|
||||
"accept-encoding": "ACCEPT-ENCODING-XXX",
|
||||
"x-amzn-requestid": "X-AMZN-REQUESTID-XXX",
|
||||
"x-amzn-RequestId": "X-AMZN-REQUESTID-XXX",
|
||||
"x-a2a-notification-token": "X-A2A-NOTIFICATION-TOKEN-XXX",
|
||||
"x-a2a-version": "X-A2A-VERSION-XXX",
|
||||
}
|
||||
|
||||
|
||||
|
||||
152
docs/docs.json
@@ -61,7 +61,9 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "Welcome",
|
||||
"pages": ["index"]
|
||||
"pages": [
|
||||
"index"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -71,7 +73,11 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "Get Started",
|
||||
"pages": ["en/introduction", "en/installation", "en/quickstart"]
|
||||
"pages": [
|
||||
"en/introduction",
|
||||
"en/installation",
|
||||
"en/quickstart"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Guides",
|
||||
@@ -79,17 +85,23 @@
|
||||
{
|
||||
"group": "Strategy",
|
||||
"icon": "compass",
|
||||
"pages": ["en/guides/concepts/evaluating-use-cases"]
|
||||
"pages": [
|
||||
"en/guides/concepts/evaluating-use-cases"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Agents",
|
||||
"icon": "user",
|
||||
"pages": ["en/guides/agents/crafting-effective-agents"]
|
||||
"pages": [
|
||||
"en/guides/agents/crafting-effective-agents"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Crews",
|
||||
"icon": "users",
|
||||
"pages": ["en/guides/crews/first-crew"]
|
||||
"pages": [
|
||||
"en/guides/crews/first-crew"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Flows",
|
||||
@@ -279,6 +291,7 @@
|
||||
"en/observability/arize-phoenix",
|
||||
"en/observability/braintrust",
|
||||
"en/observability/datadog",
|
||||
"en/observability/galileo",
|
||||
"en/observability/langdb",
|
||||
"en/observability/langfuse",
|
||||
"en/observability/langtrace",
|
||||
@@ -324,7 +337,9 @@
|
||||
},
|
||||
{
|
||||
"group": "Telemetry",
|
||||
"pages": ["en/telemetry"]
|
||||
"pages": [
|
||||
"en/telemetry"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -334,7 +349,9 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "Getting Started",
|
||||
"pages": ["en/enterprise/introduction"]
|
||||
"pages": [
|
||||
"en/enterprise/introduction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Build",
|
||||
@@ -343,7 +360,8 @@
|
||||
"en/enterprise/features/crew-studio",
|
||||
"en/enterprise/features/marketplace",
|
||||
"en/enterprise/features/agent-repositories",
|
||||
"en/enterprise/features/tools-and-integrations"
|
||||
"en/enterprise/features/tools-and-integrations",
|
||||
"en/enterprise/features/pii-trace-redactions"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -411,7 +429,8 @@
|
||||
"group": "How-To Guides",
|
||||
"pages": [
|
||||
"en/enterprise/guides/build-crew",
|
||||
"en/enterprise/guides/deploy-crew",
|
||||
"en/enterprise/guides/prepare-for-deployment",
|
||||
"en/enterprise/guides/deploy-to-amp",
|
||||
"en/enterprise/guides/kickoff-crew",
|
||||
"en/enterprise/guides/update-crew",
|
||||
"en/enterprise/guides/enable-crew-studio",
|
||||
@@ -426,7 +445,9 @@
|
||||
},
|
||||
{
|
||||
"group": "Resources",
|
||||
"pages": ["en/enterprise/resources/frequently-asked-questions"]
|
||||
"pages": [
|
||||
"en/enterprise/resources/frequently-asked-questions"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -452,7 +473,10 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "Examples",
|
||||
"pages": ["en/examples/example", "en/examples/cookbooks"]
|
||||
"pages": [
|
||||
"en/examples/example",
|
||||
"en/examples/cookbooks"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -462,7 +486,9 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "Release Notes",
|
||||
"pages": ["en/changelog"]
|
||||
"pages": [
|
||||
"en/changelog"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -501,7 +527,9 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "Bem-vindo",
|
||||
"pages": ["pt-BR/index"]
|
||||
"pages": [
|
||||
"pt-BR/index"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -523,17 +551,23 @@
|
||||
{
|
||||
"group": "Estratégia",
|
||||
"icon": "compass",
|
||||
"pages": ["pt-BR/guides/concepts/evaluating-use-cases"]
|
||||
"pages": [
|
||||
"pt-BR/guides/concepts/evaluating-use-cases"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Agentes",
|
||||
"icon": "user",
|
||||
"pages": ["pt-BR/guides/agents/crafting-effective-agents"]
|
||||
"pages": [
|
||||
"pt-BR/guides/agents/crafting-effective-agents"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Crews",
|
||||
"icon": "users",
|
||||
"pages": ["pt-BR/guides/crews/first-crew"]
|
||||
"pages": [
|
||||
"pt-BR/guides/crews/first-crew"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Flows",
|
||||
@@ -710,6 +744,7 @@
|
||||
"pt-BR/observability/arize-phoenix",
|
||||
"pt-BR/observability/braintrust",
|
||||
"pt-BR/observability/datadog",
|
||||
"pt-BR/observability/galileo",
|
||||
"pt-BR/observability/langdb",
|
||||
"pt-BR/observability/langfuse",
|
||||
"pt-BR/observability/langtrace",
|
||||
@@ -754,7 +789,9 @@
|
||||
},
|
||||
{
|
||||
"group": "Telemetria",
|
||||
"pages": ["pt-BR/telemetry"]
|
||||
"pages": [
|
||||
"pt-BR/telemetry"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -764,7 +801,9 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "Começando",
|
||||
"pages": ["pt-BR/enterprise/introduction"]
|
||||
"pages": [
|
||||
"pt-BR/enterprise/introduction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Construir",
|
||||
@@ -773,7 +812,8 @@
|
||||
"pt-BR/enterprise/features/crew-studio",
|
||||
"pt-BR/enterprise/features/marketplace",
|
||||
"pt-BR/enterprise/features/agent-repositories",
|
||||
"pt-BR/enterprise/features/tools-and-integrations"
|
||||
"pt-BR/enterprise/features/tools-and-integrations",
|
||||
"pt-BR/enterprise/features/pii-trace-redactions"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -825,7 +865,8 @@
|
||||
"group": "Guias",
|
||||
"pages": [
|
||||
"pt-BR/enterprise/guides/build-crew",
|
||||
"pt-BR/enterprise/guides/deploy-crew",
|
||||
"pt-BR/enterprise/guides/prepare-for-deployment",
|
||||
"pt-BR/enterprise/guides/deploy-to-amp",
|
||||
"pt-BR/enterprise/guides/kickoff-crew",
|
||||
"pt-BR/enterprise/guides/update-crew",
|
||||
"pt-BR/enterprise/guides/enable-crew-studio",
|
||||
@@ -883,7 +924,10 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "Exemplos",
|
||||
"pages": ["pt-BR/examples/example", "pt-BR/examples/cookbooks"]
|
||||
"pages": [
|
||||
"pt-BR/examples/example",
|
||||
"pt-BR/examples/cookbooks"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -893,7 +937,9 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "Notas de Versão",
|
||||
"pages": ["pt-BR/changelog"]
|
||||
"pages": [
|
||||
"pt-BR/changelog"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -932,7 +978,9 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "환영합니다",
|
||||
"pages": ["ko/index"]
|
||||
"pages": [
|
||||
"ko/index"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -942,7 +990,11 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "시작 안내",
|
||||
"pages": ["ko/introduction", "ko/installation", "ko/quickstart"]
|
||||
"pages": [
|
||||
"ko/introduction",
|
||||
"ko/installation",
|
||||
"ko/quickstart"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "가이드",
|
||||
@@ -950,17 +1002,23 @@
|
||||
{
|
||||
"group": "전략",
|
||||
"icon": "compass",
|
||||
"pages": ["ko/guides/concepts/evaluating-use-cases"]
|
||||
"pages": [
|
||||
"ko/guides/concepts/evaluating-use-cases"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "에이전트 (Agents)",
|
||||
"icon": "user",
|
||||
"pages": ["ko/guides/agents/crafting-effective-agents"]
|
||||
"pages": [
|
||||
"ko/guides/agents/crafting-effective-agents"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "크루 (Crews)",
|
||||
"icon": "users",
|
||||
"pages": ["ko/guides/crews/first-crew"]
|
||||
"pages": [
|
||||
"ko/guides/crews/first-crew"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "플로우 (Flows)",
|
||||
@@ -1149,6 +1207,7 @@
|
||||
"ko/observability/arize-phoenix",
|
||||
"ko/observability/braintrust",
|
||||
"ko/observability/datadog",
|
||||
"ko/observability/galileo",
|
||||
"ko/observability/langdb",
|
||||
"ko/observability/langfuse",
|
||||
"ko/observability/langtrace",
|
||||
@@ -1193,7 +1252,9 @@
|
||||
},
|
||||
{
|
||||
"group": "Telemetry",
|
||||
"pages": ["ko/telemetry"]
|
||||
"pages": [
|
||||
"ko/telemetry"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1203,7 +1264,9 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "시작 안내",
|
||||
"pages": ["ko/enterprise/introduction"]
|
||||
"pages": [
|
||||
"ko/enterprise/introduction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "빌드",
|
||||
@@ -1212,7 +1275,8 @@
|
||||
"ko/enterprise/features/crew-studio",
|
||||
"ko/enterprise/features/marketplace",
|
||||
"ko/enterprise/features/agent-repositories",
|
||||
"ko/enterprise/features/tools-and-integrations"
|
||||
"ko/enterprise/features/tools-and-integrations",
|
||||
"ko/enterprise/features/pii-trace-redactions"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1264,7 +1328,8 @@
|
||||
"group": "How-To Guides",
|
||||
"pages": [
|
||||
"ko/enterprise/guides/build-crew",
|
||||
"ko/enterprise/guides/deploy-crew",
|
||||
"ko/enterprise/guides/prepare-for-deployment",
|
||||
"ko/enterprise/guides/deploy-to-amp",
|
||||
"ko/enterprise/guides/kickoff-crew",
|
||||
"ko/enterprise/guides/update-crew",
|
||||
"ko/enterprise/guides/enable-crew-studio",
|
||||
@@ -1294,7 +1359,9 @@
|
||||
},
|
||||
{
|
||||
"group": "학습 자원",
|
||||
"pages": ["ko/enterprise/resources/frequently-asked-questions"]
|
||||
"pages": [
|
||||
"ko/enterprise/resources/frequently-asked-questions"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1320,7 +1387,10 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "예시",
|
||||
"pages": ["ko/examples/example", "ko/examples/cookbooks"]
|
||||
"pages": [
|
||||
"ko/examples/example",
|
||||
"ko/examples/cookbooks"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1330,7 +1400,9 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "릴리스 노트",
|
||||
"pages": ["ko/changelog"]
|
||||
"pages": [
|
||||
"ko/changelog"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1445,6 +1517,18 @@
|
||||
"source": "/enterprise/:path*",
|
||||
"destination": "/en/enterprise/:path*"
|
||||
},
|
||||
{
|
||||
"source": "/en/enterprise/guides/deploy-crew",
|
||||
"destination": "/en/enterprise/guides/deploy-to-amp"
|
||||
},
|
||||
{
|
||||
"source": "/ko/enterprise/guides/deploy-crew",
|
||||
"destination": "/ko/enterprise/guides/deploy-to-amp"
|
||||
},
|
||||
{
|
||||
"source": "/pt-BR/enterprise/guides/deploy-crew",
|
||||
"destination": "/pt-BR/enterprise/guides/deploy-to-amp"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/:path*",
|
||||
"destination": "/en/api-reference/:path*"
|
||||
|
||||
@@ -4,6 +4,516 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Jan 08, 2026">
|
||||
## v1.8.0
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.8.0)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add native async chain for a2a
|
||||
- Add a2a update mechanisms (poll/stream/push) with handlers and config
|
||||
- Introduce global flow configuration for human-in-the-loop feedback
|
||||
- Add streaming tool call events and fix provider ID tracking
|
||||
- Introduce production-ready Flows and Crews architecture
|
||||
- Add HITL for Flows
|
||||
- Improve EventListener and TraceCollectionListener for enhanced event handling
|
||||
|
||||
### Bug Fixes
|
||||
- Handle missing a2a dependency as optional
|
||||
- Correct error fetching for WorkOS login polling
|
||||
- Fix wrong trigger name in sample documentation
|
||||
|
||||
### Documentation
|
||||
- Update webhook-streaming documentation
|
||||
- Adjust AOP to AMP documentation language
|
||||
|
||||
### Contributors
|
||||
@Vidit-Ostwal, @greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @lucasgomide, @mplachta
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Dec 19, 2025">
|
||||
## v1.7.2
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.7.2)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Bug Fixes
|
||||
- Resolve connection issues
|
||||
|
||||
### Documentation
|
||||
- Update api-reference/status docs page
|
||||
|
||||
### Contributors
|
||||
@greysonlalonde, @heitorado, @lorenzejay, @lucasgomide
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Dec 16, 2025">
|
||||
## v1.7.1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.7.1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Improvements
|
||||
- Add `--no-commit` flag to bump command
|
||||
- Use JSON schema for tool argument serialization
|
||||
|
||||
### Bug Fixes
|
||||
- Fix error message display from response when tool repository login fails
|
||||
- Fix graceful termination of future when executing a task asynchronously
|
||||
- Fix task ordering by adding index
|
||||
- Fix platform compatibility checks for Windows signals
|
||||
- Fix RPM controller timer to prevent process hang
|
||||
- Fix token usage recording and validate response model on stream
|
||||
|
||||
### Documentation
|
||||
- Add translated documentation for async
|
||||
- Add documentation for AOP Deploy API
|
||||
- Add documentation for the agent handler connector
|
||||
- Add documentation on native async
|
||||
|
||||
### Contributors
|
||||
@Llamrei, @dragosmc, @gilfeig, @greysonlalonde, @heitorado, @lorenzejay, @mattatcha, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Dec 09, 2025">
|
||||
## v1.7.0
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.7.0)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add async flow kickoff
|
||||
- Add async crew support
|
||||
- Add async task support
|
||||
- Add async knowledge support
|
||||
- Add async memory support
|
||||
- Add async support for tools and agent executor; improve typing and docs
|
||||
- Implement a2a extensions API and async agent card caching; fix task propagation & streaming
|
||||
- Add native async tool support
|
||||
- Add async llm support
|
||||
- Create sys event types and handler
|
||||
|
||||
### Bug Fixes
|
||||
- Fix issue to ensure nonetypes are not passed to otel
|
||||
- Fix deadlock in token store file operations
|
||||
- Fix to ensure otel span is closed
|
||||
- Use HuggingFaceEmbeddingFunction for embeddings, update keys and add tests
|
||||
- Fix to ensure supports_tools is true for all supported anthropic models
|
||||
- Ensure hooks work with lite agents flows
|
||||
|
||||
### Contributors
|
||||
@greysonlalonde, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Nov 29, 2025">
|
||||
## v1.6.1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.6.1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Bug Fixes
|
||||
- Fix ChatCompletionsClient call to ensure proper functionality
|
||||
- Ensure async methods are executable for annotations
|
||||
- Fix parameters in RagTool.add, add typing, and tests
|
||||
- Remove invalid parameter from SSE client
|
||||
- Erase 'oauth2_extra' setting on 'crewai config reset' command
|
||||
|
||||
### Refactoring
|
||||
- Enhance model validation and provider inference in LLM class
|
||||
|
||||
### Contributors
|
||||
@Vidit-Ostwal, @greysonlalonde, @heitorado, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Nov 25, 2025">
|
||||
## v1.6.0
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.6.0)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add streaming result support to flows and crews
|
||||
- Add gemini-3-pro-preview
|
||||
- Support CLI login with Entra ID
|
||||
- Add Merge Agent Handler tool
|
||||
- Enhance flow event state management
|
||||
|
||||
### Bug Fixes
|
||||
- Ensure custom rag store persist path is set if passed
|
||||
- Ensure fuzzy returns are more strict and show type warning
|
||||
- Re-add openai response_format parameter and add test
|
||||
- Fix rag tool embeddings configuration
|
||||
- Ensure flow execution start panel is not shown on plot
|
||||
|
||||
### Documentation
|
||||
- Update references from AMP to AOP in documentation
|
||||
- Update AMP to AOP
|
||||
|
||||
### Contributors
|
||||
@Vidit-Ostwal, @gilfeig, @greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @markmcd
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Nov 22, 2025">
|
||||
## v0.203.2
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/0.203.2)
|
||||
|
||||
## What's Changed
|
||||
|
||||
- Hotfix version bump from 0.203.1 to 0.203.2
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Nov 16, 2025">
|
||||
## v1.5.0
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.5.0)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add a2a trust remote completion status flag
|
||||
- Fetch and store more data about Okta authorization server
|
||||
- Implement before and after LLM call hooks in CrewAgentExecutor
|
||||
- Expose messages to TaskOutput and LiteAgentOutputs
|
||||
- Enhance schema description of QdrantVectorSearchTool
|
||||
|
||||
### Bug Fixes
|
||||
- Ensure tracing instrumentation flags are correctly applied
|
||||
- Fix custom tool documentation links and add Mintlify broken links action
|
||||
|
||||
### Documentation
|
||||
- Enhance task guardrail documentation with LLM-based validation support
|
||||
|
||||
### Contributors
|
||||
@danielfsbarreto, @greysonlalonde, @heitorado, @lorenzejay, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Nov 07, 2025">
|
||||
## v1.4.1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.4.1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Bug Fixes
|
||||
- Fix handling of agent max iterations
|
||||
- Resolve routing issues for LLM model syntax to respected providers
|
||||
|
||||
### Contributors
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Nov 07, 2025">
|
||||
## v1.4.0
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.4.0)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add support for non-AST plot routes
|
||||
- Implement first-class support for MCP
|
||||
- Add Pydantic validation dunder to BaseInterceptor
|
||||
- Add support for LLM message interceptor hooks
|
||||
- Cache i18n prompts for efficient use
|
||||
- Enhance QdrantVectorSearchTool
|
||||
|
||||
### Bug Fixes
|
||||
- Fix issues with keeping stopwords updated
|
||||
- Resolve unpickleable values in flow state
|
||||
- Ensure lite agents course-correct on validation errors
|
||||
- Fix callback argument hashing to ensure caching works
|
||||
- Allow adding RAG source content from valid URLs
|
||||
- Make plot node selection smoother
|
||||
- Fix duplicating document IDs for knowledge
|
||||
|
||||
### Refactoring
|
||||
- Improve MCP tool execution handling with concurrent futures
|
||||
- Simplify flow handling, typing, and logging; update UI and tests
|
||||
- Refactor stop word management to a property
|
||||
|
||||
### Documentation
|
||||
- Migrate embedder to embedding_model and require vectordb across tool docs; add provider examples (en/ko/pt-BR)
|
||||
|
||||
### Contributors
|
||||
@danielfsbarreto, @greysonlalonde, @lorenzejay, @lucasgomide, @tonykipkemboi
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Nov 01, 2025">
|
||||
## v1.3.0
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.3.0)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Refactor flow handling, typing, and logging
|
||||
- Enhance QdrantVectorSearchTool
|
||||
|
||||
### Bug Fixes
|
||||
- Fix Firecrawl tools and add tests
|
||||
- Refactor use_stop_words to property and add check for stop words
|
||||
|
||||
### Documentation
|
||||
- Migrate embedder to embedding_model and require vectordb across tool docs
|
||||
- Add provider examples in English, Korean, and Portuguese
|
||||
|
||||
### Refactoring
|
||||
- Improve flow handling and UI updates
|
||||
|
||||
### Contributors
|
||||
@danielfsbarreto, @greysonlalonde, @lorenzejay, @lucasgomide, @tonykipkemboi
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Oct 27, 2025">
|
||||
## v1.2.1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.2.1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add support for Datadog integration
|
||||
- Support apps and mcps in liteagent
|
||||
|
||||
### Documentation
|
||||
- Describe mandatory environment variable for calling Platform tools for each integration
|
||||
- Add Datadog integration documentation
|
||||
|
||||
### Contributors
|
||||
@barieom, @lorenzejay, @lucasgomide, @sabrenner
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Oct 24, 2025">
|
||||
## v1.2.0
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.2.0)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Bug Fixes
|
||||
- Update default LLM model and improve error logging in LLM utilities
|
||||
- Change flow visualization directory and method inspection
|
||||
|
||||
### Dropping Unused
|
||||
- Remove aisuite
|
||||
|
||||
### Contributors
|
||||
@greysonlalonde, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Oct 21, 2025">
|
||||
## v1.1.0
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.1.0)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Enhance InternalInstructor to support multiple LLM providers
|
||||
- Implement mypy plugin base
|
||||
- Improve QdrantVectorSearchTool
|
||||
|
||||
### Bug Fixes
|
||||
- Correct broken integration documentation links
|
||||
- Fix double trace call and add types
|
||||
- Pin template versions to latest
|
||||
|
||||
### Documentation
|
||||
- Update LLM integration details and examples
|
||||
|
||||
### Refactoring
|
||||
- Improve CrewBase typing
|
||||
|
||||
### Contributors
|
||||
@cwarre33, @danielfsbarreto, @greysonlalonde, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Oct 20, 2025">
|
||||
## v1.0.0
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Bump versions to 1.0.0
|
||||
- Enhance knowledge and guardrail event handling in Agent class
|
||||
- Inject tool repository credentials in crewai run command
|
||||
|
||||
### Bug Fixes
|
||||
- Preserve nested condition structure in Flow decorators
|
||||
- Add standard print parameters to Printer.print method
|
||||
- Fix errors when there is no input() available
|
||||
- Add a leeway of 10s when decoding JWT
|
||||
- Revert bad cron schedule
|
||||
- Correct cron schedule to run every 5 days at specific dates
|
||||
- Use system PATH for Docker binary instead of hardcoded path
|
||||
- Add CodeQL configuration to properly exclude template directories
|
||||
|
||||
### Documentation
|
||||
- Update security policy for vulnerability reporting
|
||||
- Add guide for capturing telemetry logs in CrewAI AMP
|
||||
- Add missing /resume files
|
||||
- Clarify webhook URL parameter in HITL workflows
|
||||
|
||||
### Contributors
|
||||
@Vidit-Ostwal, @greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @lucasgomide, @mplachta, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Oct 18, 2025">
|
||||
## v1.0.0b3 (Pre-release)
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0b3)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Enhance task guardrail functionality and validation
|
||||
- Improve support for importing native SDK
|
||||
- Add Azure native tests
|
||||
- Enhance BedrockCompletion class with advanced features
|
||||
- Enhance GeminiCompletion class with client parameter support
|
||||
- Enhance AnthropicCompletion class with additional client parameters
|
||||
|
||||
### Bug Fixes
|
||||
- Preserve nested condition structure in Flow decorators
|
||||
- Add standard print parameters to Printer.print method
|
||||
- Remove stdout prints and improve test determinism
|
||||
|
||||
### Refactoring
|
||||
- Convert project module to metaclass with full typing
|
||||
|
||||
### Contributors
|
||||
@greysonlalonde, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Oct 16, 2025">
|
||||
## v1.0.0b2 (Pre-release)
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0b2)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Enhance OpenAICompletion class with additional client parameters
|
||||
- Improve event bus thread safety and async support
|
||||
- Inject tool repository credentials in crewai run command
|
||||
|
||||
### Bug Fixes
|
||||
- Fix issue where it errors out if there is no input() available
|
||||
- Add a leeway of 10s when decoding JWT
|
||||
- Fix copying and adding NOT_SPECIFIED check in task.py
|
||||
|
||||
### Documentation
|
||||
- Ensure CREWAI_PLATFORM_INTEGRATION_TOKEN is mentioned in documentation
|
||||
- Update triggers documentation
|
||||
|
||||
### Contributors
|
||||
@Vidit-Ostwal, @greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @lucasgomide
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Oct 14, 2025">
|
||||
## v1.0.0b1 (Pre-release)
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0b1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Enhance OpenAICompletion class with additional client parameters
|
||||
- Improve event bus thread safety and async support
|
||||
- Implement Bedrock LLM integration
|
||||
|
||||
### Bug Fixes
|
||||
- Fix issue with missing input() availability
|
||||
- Resolve JWT decoding error by adding a leeway of 10 seconds
|
||||
- Inject tool repository credentials in crewai run command
|
||||
- Fix copy and add NOT_SPECIFIED check in task.py
|
||||
|
||||
### Documentation
|
||||
- Ensure CREWAI_PLATFORM_INTEGRATION_TOKEN is mentioned in documentation
|
||||
- Update triggers documentation
|
||||
|
||||
### Contributors
|
||||
@Vidit-Ostwal, @greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @lucasgomide
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Oct 13, 2025">
|
||||
## v0.203.1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/0.203.1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Core Improvements & Fixes
|
||||
- Fixed injection of tool repository credentials into the `crewai run` command
|
||||
- Added a 10-second leeway when decoding JWTs to reduce token validation errors
|
||||
- Corrected (then reverted) cron schedule fix intended to run jobs every 5 days at specific dates
|
||||
|
||||
### Documentation & Guides
|
||||
- Updated security policy to clarify the process for vulnerability reporting
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Oct 09, 2025">
|
||||
## v1.0.0a4 (Pre-release)
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0a4)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Enhance knowledge and guardrail event handling in Agent class
|
||||
- Introduce trigger listing and execution commands for local development
|
||||
- Update documentation with new approach to consume Platform Actions
|
||||
- Add guide for capturing telemetry logs in CrewAI AMP
|
||||
|
||||
### Bug Fixes
|
||||
- Revert bad cron schedule
|
||||
- Correct cron schedule to run every 5 days at specific dates
|
||||
- Remove duplicate line and add explicit environment variable
|
||||
- Resolve linting errors across the codebase
|
||||
- Replace print statements with logger in agent and memory handling
|
||||
- Use system PATH for Docker binary instead of hardcoded path
|
||||
- Allow failed PyPI publish
|
||||
- Match tag and release title, ignore devtools build for PyPI
|
||||
|
||||
### Documentation
|
||||
- Update security policy for vulnerability reporting
|
||||
- Add missing /resume files
|
||||
- Clarify webhook URL parameter in HITL workflows
|
||||
|
||||
### Contributors
|
||||
@Vidit-Ostwal, @greysonlalonde, @lorenzejay, @lucasgomide, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Sep 30, 2025">
|
||||
## v1.0.0a1
|
||||
|
||||
|
||||
@@ -574,6 +574,10 @@ When you run this Flow, the output will change based on the random boolean value
|
||||
|
||||
### Human in the Loop (human feedback)
|
||||
|
||||
<Note>
|
||||
The `@human_feedback` decorator requires **CrewAI version 1.8.0 or higher**.
|
||||
</Note>
|
||||
|
||||
The `@human_feedback` decorator enables human-in-the-loop workflows by pausing flow execution to collect feedback from a human. This is useful for approval gates, quality review, and decision points that require human judgment.
|
||||
|
||||
```python Code
|
||||
|
||||
342
docs/en/enterprise/features/pii-trace-redactions.mdx
Normal file
@@ -0,0 +1,342 @@
|
||||
---
|
||||
title: PII Redaction for Traces
|
||||
description: "Automatically redact sensitive data from crew and flow execution traces"
|
||||
icon: "lock"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
PII Redaction is a CrewAI AMP feature that automatically detects and masks Personally Identifiable Information (PII) in your crew and flow execution traces. This ensures sensitive data like credit card numbers, social security numbers, email addresses, and names are not exposed in your CrewAI AMP traces. You can also create custom recognizers to protect organization-specific data.
|
||||
|
||||
|
||||
<Info>
|
||||
PII Redaction is available on the Enterprise plan.
|
||||
Deployment must be version 1.8.0 or higher.
|
||||
</Info>
|
||||
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
|
||||
## Why PII Redaction Matters
|
||||
|
||||
When running AI agents in production, sensitive information often flows through your crews:
|
||||
|
||||
- Customer data from CRM integrations
|
||||
- Financial information from payment processors
|
||||
- Personal details from form submissions
|
||||
- Internal employee data
|
||||
|
||||
Without proper redaction, this data appears in traces, making compliance with regulations like GDPR, HIPAA, and PCI-DSS challenging. PII Redaction solves this by automatically masking sensitive data before it's stored in traces.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Detect** - Scan trace event data for known PII patterns
|
||||
2. **Classify** - Identify the type of sensitive data (credit card, SSN, email, etc.)
|
||||
3. **Mask/Redact** - Replace the sensitive data with masked values based on your configuration
|
||||
|
||||
```
|
||||
Original: "Contact john.doe@company.com or call 555-123-4567"
|
||||
Redacted: "Contact <EMAIL_ADDRESS> or call <PHONE_NUMBER>"
|
||||
```
|
||||
|
||||
## Enabling PII Redaction
|
||||
|
||||
<Info>
|
||||
You must be on the Enterprise plan and your deployment must be version 1.8.0 or higher to use this feature.
|
||||
</Info>
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to Crew Settings">
|
||||
In the CrewAI AMP dashboard, select your deployed crew and go to one of your deployments/automations, then navigate to **Settings** → **PII Protection**.
|
||||
</Step>
|
||||
|
||||
<Step title="Enable PII Protection">
|
||||
Toggle on **PII Redaction for Traces**. This will enable automatic scanning and redaction of trace data.
|
||||
|
||||
<Info>
|
||||
You need to manually enable PII Redaction for each deployment.
|
||||
</Info>
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Configure Entity Types">
|
||||
Select which types of PII to detect and redact. Each entity can be individually enabled or disabled.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Save">
|
||||
Save your configuration. PII redaction will be active on all subsequent crew executions, no redeployment is needed.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Supported Entity Types
|
||||
|
||||
CrewAI supports the following PII entity types, organized by category.
|
||||
|
||||
### Global Entities
|
||||
|
||||
| Entity | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `CREDIT_CARD` | Credit/debit card numbers | "4111-1111-1111-1111" |
|
||||
| `CRYPTO` | Cryptocurrency wallet addresses | "bc1qxy2kgd..." |
|
||||
| `DATE_TIME` | Dates and times | "January 15, 2024" |
|
||||
| `EMAIL_ADDRESS` | Email addresses | "john@example.com" |
|
||||
| `IBAN_CODE` | International bank account numbers | "DE89 3704 0044 0532 0130 00" |
|
||||
| `IP_ADDRESS` | IPv4 and IPv6 addresses | "192.168.1.1" |
|
||||
| `LOCATION` | Geographic locations | "New York City" |
|
||||
| `MEDICAL_LICENSE` | Medical license numbers | "MD12345" |
|
||||
| `NRP` | Nationalities, religious, or political groups | - |
|
||||
| `PERSON` | Personal names | "John Doe" |
|
||||
| `PHONE_NUMBER` | Phone numbers in various formats | "+1 (555) 123-4567" |
|
||||
| `URL` | Web URLs | "https://example.com" |
|
||||
|
||||
### US-Specific Entities
|
||||
|
||||
| Entity | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `US_BANK_NUMBER` | US Bank account numbers | "1234567890" |
|
||||
| `US_DRIVER_LICENSE` | US Driver's license numbers | "D1234567" |
|
||||
| `US_ITIN` | Individual Taxpayer ID | "900-70-0000" |
|
||||
| `US_PASSPORT` | US Passport numbers | "123456789" |
|
||||
| `US_SSN` | Social Security Numbers | "123-45-6789" |
|
||||
|
||||
## Redaction Actions
|
||||
|
||||
For each enabled entity, you can configure how the data is redacted:
|
||||
|
||||
| Action | Description | Example Output |
|
||||
|--------|-------------|----------------|
|
||||
| `mask` | Replace with the entity type label | `<CREDIT_CARD>` |
|
||||
| `redact` | Completely remove the text | *(empty)* |
|
||||
|
||||
## Custom Recognizers
|
||||
|
||||
In addition to built-in entities, you can create **custom recognizers** to detect organization-specific PII patterns.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
### Recognizer Types
|
||||
|
||||
You have two options for custom recognizers:
|
||||
|
||||
| Type | Best For | Example Use Case |
|
||||
|------|----------|------------------|
|
||||
| **Pattern-based (Regex)** | Structured data with predictable formats | Salary amounts, employee IDs, project codes |
|
||||
| **Deny-list** | Exact string matches | Company names, internal codenames, specific terms |
|
||||
|
||||
### Creating a Custom Recognizer
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to Custom Recognizers">
|
||||
Go to your Organization **Settings** → **Organization** → **Add Recognizer**.
|
||||
</Step>
|
||||
|
||||
<Step title="Configure the Recognizer">
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Configure the following fields:
|
||||
- **Name**: A descriptive name for the recognizer
|
||||
- **Entity Type**: The entity label that will appear in redacted output (e.g., `EMPLOYEE_ID`, `SALARY`)
|
||||
- **Type**: Choose between Regex Pattern or Deny List
|
||||
- **Pattern/Values**: Regex pattern or list of strings to match
|
||||
- **Confidence Threshold**: Minimum score (0.0-1.0) required for a match to trigger redaction. Higher values (e.g., 0.8) reduce false positives but may miss some matches. Lower values (e.g., 0.5) catch more matches but may over-redact. Default is 0.8.
|
||||
- **Context Words** (optional): Words that increase detection confidence when found nearby
|
||||
</Step>
|
||||
|
||||
<Step title="Save">
|
||||
Save the recognizer. It will be available to enable on your deployments.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Understanding Entity Types
|
||||
|
||||
The **Entity Type** determines how matched content appears in redacted traces:
|
||||
|
||||
```
|
||||
Entity Type: SALARY
|
||||
Pattern: salary:\s*\$\s*\d+
|
||||
Input: "Employee salary: $50,000"
|
||||
Output: "Employee <SALARY>"
|
||||
```
|
||||
|
||||
### Using Context Words
|
||||
|
||||
Context words improve accuracy by increasing confidence when specific terms appear near the matched pattern:
|
||||
|
||||
```
|
||||
Context Words: "project", "code", "internal"
|
||||
Entity Type: PROJECT_CODE
|
||||
Pattern: PRJ-\d{4}
|
||||
```
|
||||
|
||||
When "project" or "code" appears near "PRJ-1234", the recognizer has higher confidence it's a true match, reducing false positives.
|
||||
|
||||
|
||||
## Viewing Redacted Traces
|
||||
|
||||
Once PII redaction is enabled, your traces will show redacted values in place of sensitive data:
|
||||
|
||||
```
|
||||
Task Output: "Customer <PERSON> placed order #12345.
|
||||
Contact email: <EMAIL_ADDRESS>, phone: <PHONE_NUMBER>.
|
||||
Payment processed for card ending in <CREDIT_CARD>."
|
||||
```
|
||||
|
||||
Redacted values are clearly marked with angle brackets and the entity type label (e.g., `<EMAIL_ADDRESS>`), making it easy to understand what data was protected while still allowing you to debug and monitor crew behavior.
|
||||
|
||||
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
<Steps>
|
||||
<Step title="Enable Only Needed Entities">
|
||||
Each enabled entity adds processing overhead. Only enable entities relevant to your data.
|
||||
</Step>
|
||||
|
||||
<Step title="Use Specific Patterns">
|
||||
For custom recognizers, use specific patterns to reduce false positives and improve performance. Regex patterns are best when identifying specific patterns in the traces such as salary, employee id, project code, etc. Deny-list recognizers are best when identifying exact strings in the traces such as company names, internal codenames, etc.
|
||||
</Step>
|
||||
|
||||
<Step title="Leverage Context Words">
|
||||
Context words improve accuracy by only triggering detection when surrounding text matches.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<Accordion title="PII Not Being Redacted">
|
||||
**Possible Causes:**
|
||||
- Entity type not enabled in configuration
|
||||
- Pattern doesn't match the data format
|
||||
- Custom recognizer has syntax errors
|
||||
|
||||
**Solutions:**
|
||||
- Verify entity is enabled in Settings → Security
|
||||
- Test regex patterns with sample data
|
||||
- Check logs for configuration errors
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Too Much Data Being Redacted">
|
||||
**Possible Causes:**
|
||||
- Overly broad entity types enabled (e.g., `DATE_TIME` catches dates everywhere)
|
||||
- Custom recognizer patterns are too general
|
||||
|
||||
**Solutions:**
|
||||
- Disable entities that cause false positives
|
||||
- Make custom patterns more specific
|
||||
- Add context words to improve accuracy
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Performance Issues">
|
||||
**Possible Causes:**
|
||||
- Too many entities enabled
|
||||
- NLP-based entities (`PERSON`, `LOCATION`, `NRP`) are computationally expensive as they use machine learning models
|
||||
|
||||
**Solutions:**
|
||||
- Only enable entities you actually need
|
||||
- Consider using pattern-based alternatives where possible
|
||||
- Monitor trace processing times in the dashboard
|
||||
</Accordion>
|
||||
|
||||
---
|
||||
|
||||
## Practical Example: Salary Pattern Matching
|
||||
|
||||
This example demonstrates how to create a custom recognizer to detect and mask salary information in your traces.
|
||||
|
||||
### Use Case
|
||||
|
||||
Your crew processes employee or financial data that includes salary information in formats like:
|
||||
- `salary: $50,000`
|
||||
- `salary: $125,000.00`
|
||||
- `salary:$1,500.50`
|
||||
|
||||
You want to automatically mask these values to protect sensitive compensation data.
|
||||
|
||||
### Configuration
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Name** | `SALARY` |
|
||||
| **Entity Type** | `SALARY` |
|
||||
| **Type** | Regex Pattern |
|
||||
| **Regex Pattern** | `salary:\s*\$\s*\d{1,3}(,\d{3})*(\.\d{2})?` |
|
||||
| **Action** | Mask |
|
||||
| **Confidence Threshold** | `0.8` |
|
||||
| **Context Words** | `salary, compensation, pay, wage, income` |
|
||||
|
||||
### Regex Pattern Breakdown
|
||||
|
||||
| Pattern Component | Meaning |
|
||||
|-------------------|---------|
|
||||
| `salary:` | Matches the literal text "salary:" |
|
||||
| `\s*` | Matches zero or more whitespace characters |
|
||||
| `\$` | Matches the dollar sign (escaped) |
|
||||
| `\s*` | Matches zero or more whitespace characters after $ |
|
||||
| `\d{1,3}` | Matches 1-3 digits (e.g., "1", "50", "125") |
|
||||
| `(,\d{3})*` | Matches comma-separated thousands (e.g., ",000", ",500,000") |
|
||||
| `(\.\d{2})?` | Optionally matches cents (e.g., ".00", ".50") |
|
||||
|
||||
### Example Results
|
||||
|
||||
```
|
||||
Original: "Employee record shows salary: $125,000.00 annually"
|
||||
Redacted: "Employee record shows <SALARY> annually"
|
||||
|
||||
Original: "Base salary:$50,000 with bonus potential"
|
||||
Redacted: "Base <SALARY> with bonus potential"
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Adding context words like "salary", "compensation", "pay", "wage", and "income" helps increase detection confidence when these terms appear near the matched pattern, reducing false positives.
|
||||
</Tip>
|
||||
|
||||
### Enable the Recognizer for Your Deployments
|
||||
|
||||
<Warning>
|
||||
Creating a custom recognizer at the organization level does not automatically enable it for your deployments. You must manually enable each recognizer for every deployment where you want it applied.
|
||||
</Warning>
|
||||
|
||||
After creating your custom recognizer, enable it for each deployment:
|
||||
|
||||
<Steps>
|
||||
<Step title="Navigate to Your Deployment">
|
||||
Go to your deployment/automation and open **Settings** → **PII Protection**.
|
||||
</Step>
|
||||
|
||||
<Step title="Select Custom Recognizers">
|
||||
Under **Mask Recognizers**, you'll see your organization-defined recognizers. Check the box next to the recognizers you want to enable.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Save Configuration">
|
||||
Save your changes. The recognizer will be active on all subsequent executions for this deployment.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Info>
|
||||
Repeat this process for each deployment where you need the custom recognizer. This gives you granular control over which recognizers are active in different environments (e.g., development vs. production).
|
||||
</Info>
|
||||
@@ -55,7 +55,7 @@ Each webhook sends a list of events:
|
||||
}
|
||||
```
|
||||
|
||||
The `data` object structure varies by event type. Refer to the [event list](https://github.com/crewAIInc/crewAI/tree/main/src/crewai/utilities/events) on GitHub.
|
||||
The `data` object structure varies by event type. Refer to the [event list](https://github.com/crewAIInc/crewAI/tree/main/lib/crewai/src/crewai/events/types) on GitHub.
|
||||
|
||||
As requests are sent over HTTP, the order of events can't be guaranteed. If you need ordering, use the `timestamp` field.
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
title: "Deploy Crew"
|
||||
description: "Deploying a Crew on CrewAI AMP"
|
||||
title: "Deploy to AMP"
|
||||
description: "Deploy your Crew or Flow to CrewAI AMP"
|
||||
icon: "rocket"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Note>
|
||||
After creating a crew locally or through Crew Studio, the next step is
|
||||
After creating a Crew or Flow locally (or through Crew Studio), the next step is
|
||||
deploying it to the CrewAI AMP platform. This guide covers multiple deployment
|
||||
methods to help you choose the best approach for your workflow.
|
||||
</Note>
|
||||
@@ -14,19 +14,26 @@ mode: "wide"
|
||||
## Prerequisites
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Ready for Deployment" icon="users">
|
||||
You should have a working crew either built locally or created through Crew
|
||||
Studio
|
||||
<Card title="Project Ready for Deployment" icon="check-circle">
|
||||
You should have a working Crew or Flow that runs successfully locally.
|
||||
Follow our [preparation guide](/en/enterprise/guides/prepare-for-deployment) to verify your project structure.
|
||||
</Card>
|
||||
<Card title="GitHub Repository" icon="github">
|
||||
Your crew code should be in a GitHub repository (for GitHub integration
|
||||
Your code should be in a GitHub repository (for GitHub integration
|
||||
method)
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<Info>
|
||||
**Crews vs Flows**: Both project types can be deployed as "automations" on CrewAI AMP.
|
||||
The deployment process is the same, but they have different project structures.
|
||||
See [Prepare for Deployment](/en/enterprise/guides/prepare-for-deployment) for details.
|
||||
</Info>
|
||||
|
||||
## Option 1: Deploy Using CrewAI CLI
|
||||
|
||||
The CLI provides the fastest way to deploy locally developed crews to the Enterprise platform.
|
||||
The CLI provides the fastest way to deploy locally developed Crews or Flows to the AMP platform.
|
||||
The CLI automatically detects your project type from `pyproject.toml` and builds accordingly.
|
||||
|
||||
<Steps>
|
||||
<Step title="Install CrewAI CLI">
|
||||
@@ -128,7 +135,7 @@ crewai deploy remove <deployment_id>
|
||||
|
||||
## Option 2: Deploy Directly via Web Interface
|
||||
|
||||
You can also deploy your crews directly through the CrewAI AMP web interface by connecting your GitHub account. This approach doesn't require using the CLI on your local machine.
|
||||
You can also deploy your Crews or Flows directly through the CrewAI AMP web interface by connecting your GitHub account. This approach doesn't require using the CLI on your local machine. The platform automatically detects your project type and handles the build appropriately.
|
||||
|
||||
<Steps>
|
||||
|
||||
@@ -282,68 +289,7 @@ For automated deployments in CI/CD pipelines, you can use the CrewAI API to trig
|
||||
|
||||
</Steps>
|
||||
|
||||
## ⚠️ Environment Variable Security Requirements
|
||||
|
||||
<Warning>
|
||||
**Important**: CrewAI AMP has security restrictions on environment variable
|
||||
names that can cause deployment failures if not followed.
|
||||
</Warning>
|
||||
|
||||
### Blocked Environment Variable Patterns
|
||||
|
||||
For security reasons, the following environment variable naming patterns are **automatically filtered** and will cause deployment issues:
|
||||
|
||||
**Blocked Patterns:**
|
||||
|
||||
- Variables ending with `_TOKEN` (e.g., `MY_API_TOKEN`)
|
||||
- Variables ending with `_PASSWORD` (e.g., `DB_PASSWORD`)
|
||||
- Variables ending with `_SECRET` (e.g., `API_SECRET`)
|
||||
- Variables ending with `_KEY` in certain contexts
|
||||
|
||||
**Specific Blocked Variables:**
|
||||
|
||||
- `GITHUB_USER`, `GITHUB_TOKEN`
|
||||
- `AWS_REGION`, `AWS_DEFAULT_REGION`
|
||||
- Various internal CrewAI system variables
|
||||
|
||||
### Allowed Exceptions
|
||||
|
||||
Some variables are explicitly allowed despite matching blocked patterns:
|
||||
|
||||
- `AZURE_AD_TOKEN`
|
||||
- `AZURE_OPENAI_AD_TOKEN`
|
||||
- `ENTERPRISE_ACTION_TOKEN`
|
||||
- `CREWAI_ENTEPRISE_TOOLS_TOKEN`
|
||||
|
||||
### How to Fix Naming Issues
|
||||
|
||||
If your deployment fails due to environment variable restrictions:
|
||||
|
||||
```bash
|
||||
# ❌ These will cause deployment failures
|
||||
OPENAI_TOKEN=sk-...
|
||||
DATABASE_PASSWORD=mypassword
|
||||
API_SECRET=secret123
|
||||
|
||||
# ✅ Use these naming patterns instead
|
||||
OPENAI_API_KEY=sk-...
|
||||
DATABASE_CREDENTIALS=mypassword
|
||||
API_CONFIG=secret123
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Use standard naming conventions**: `PROVIDER_API_KEY` instead of `PROVIDER_TOKEN`
|
||||
2. **Test locally first**: Ensure your crew works with the renamed variables
|
||||
3. **Update your code**: Change any references to the old variable names
|
||||
4. **Document changes**: Keep track of renamed variables for your team
|
||||
|
||||
<Tip>
|
||||
If you encounter deployment failures with cryptic environment variable errors,
|
||||
check your variable names against these patterns first.
|
||||
</Tip>
|
||||
|
||||
### Interact with Your Deployed Crew
|
||||
## Interact with Your Deployed Automation
|
||||
|
||||
Once deployment is complete, you can access your crew through:
|
||||
|
||||
@@ -387,7 +333,108 @@ The Enterprise platform also offers:
|
||||
- **Custom Tools Repository**: Create, share, and install tools
|
||||
- **Crew Studio**: Build crews through a chat interface without writing code
|
||||
|
||||
## Troubleshooting Deployment Failures
|
||||
|
||||
If your deployment fails, check these common issues:
|
||||
|
||||
### Build Failures
|
||||
|
||||
#### Missing uv.lock File
|
||||
|
||||
**Symptom**: Build fails early with dependency resolution errors
|
||||
|
||||
**Solution**: Generate and commit the lock file:
|
||||
|
||||
```bash
|
||||
uv lock
|
||||
git add uv.lock
|
||||
git commit -m "Add uv.lock for deployment"
|
||||
git push
|
||||
```
|
||||
|
||||
<Warning>
|
||||
The `uv.lock` file is required for all deployments. Without it, the platform
|
||||
cannot reliably install your dependencies.
|
||||
</Warning>
|
||||
|
||||
#### Wrong Project Structure
|
||||
|
||||
**Symptom**: "Could not find entry point" or "Module not found" errors
|
||||
|
||||
**Solution**: Verify your project matches the expected structure:
|
||||
|
||||
- **Both Crews and Flows**: Must have entry point at `src/project_name/main.py`
|
||||
- **Crews**: Use a `run()` function as entry point
|
||||
- **Flows**: Use a `kickoff()` function as entry point
|
||||
|
||||
See [Prepare for Deployment](/en/enterprise/guides/prepare-for-deployment) for detailed structure diagrams.
|
||||
|
||||
#### Missing CrewBase Decorator
|
||||
|
||||
**Symptom**: "Crew not found", "Config not found", or agent/task configuration errors
|
||||
|
||||
**Solution**: Ensure **all** crew classes use the `@CrewBase` decorator:
|
||||
|
||||
```python
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
|
||||
@CrewBase # This decorator is REQUIRED
|
||||
class YourCrew():
|
||||
"""Your crew description"""
|
||||
|
||||
@agent
|
||||
def my_agent(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['my_agent'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# ... rest of crew definition
|
||||
```
|
||||
|
||||
<Info>
|
||||
This applies to standalone Crews AND crews embedded inside Flow projects.
|
||||
Every crew class needs the decorator.
|
||||
</Info>
|
||||
|
||||
#### Incorrect pyproject.toml Type
|
||||
|
||||
**Symptom**: Build succeeds but runtime fails, or unexpected behavior
|
||||
|
||||
**Solution**: Verify the `[tool.crewai]` section matches your project type:
|
||||
|
||||
```toml
|
||||
# For Crew projects:
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
|
||||
# For Flow projects:
|
||||
[tool.crewai]
|
||||
type = "flow"
|
||||
```
|
||||
|
||||
### Runtime Failures
|
||||
|
||||
#### LLM Connection Failures
|
||||
|
||||
**Symptom**: API key errors, "model not found", or authentication failures
|
||||
|
||||
**Solution**:
|
||||
1. Verify your LLM provider's API key is correctly set in environment variables
|
||||
2. Ensure the environment variable names match what your code expects
|
||||
3. Test locally with the exact same environment variables before deploying
|
||||
|
||||
#### Crew Execution Errors
|
||||
|
||||
**Symptom**: Crew starts but fails during execution
|
||||
|
||||
**Solution**:
|
||||
1. Check the execution logs in the AMP dashboard (Traces tab)
|
||||
2. Verify all tools have required API keys configured
|
||||
3. Ensure agent configurations in `agents.yaml` are valid
|
||||
4. Check task configurations in `tasks.yaml` for syntax errors
|
||||
|
||||
<Card title="Need Help?" icon="headset" href="mailto:support@crewai.com">
|
||||
Contact our support team for assistance with deployment issues or questions
|
||||
about the Enterprise platform.
|
||||
about the AMP platform.
|
||||
</Card>
|
||||
305
docs/en/enterprise/guides/prepare-for-deployment.mdx
Normal file
@@ -0,0 +1,305 @@
|
||||
---
|
||||
title: "Prepare for Deployment"
|
||||
description: "Ensure your Crew or Flow is ready for deployment to CrewAI AMP"
|
||||
icon: "clipboard-check"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Before deploying to CrewAI AMP, it's crucial to verify your project is correctly structured.
|
||||
Both Crews and Flows can be deployed as "automations," but they have different project structures
|
||||
and requirements that must be met for successful deployment.
|
||||
</Note>
|
||||
|
||||
## Understanding Automations
|
||||
|
||||
In CrewAI AMP, **automations** is the umbrella term for deployable Agentic AI projects. An automation can be either:
|
||||
|
||||
- **A Crew**: A standalone team of AI agents working together on tasks
|
||||
- **A Flow**: An orchestrated workflow that can combine multiple crews, direct LLM calls, and procedural logic
|
||||
|
||||
Understanding which type you're deploying is essential because they have different project structures and entry points.
|
||||
|
||||
## Crews vs Flows: Key Differences
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Projects" icon="users">
|
||||
Standalone AI agent teams with `crew.py` defining agents and tasks. Best for focused, collaborative tasks.
|
||||
</Card>
|
||||
<Card title="Flow Projects" icon="diagram-project">
|
||||
Orchestrated workflows with embedded crews in a `crews/` folder. Best for complex, multi-stage processes.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
| Aspect | Crew | Flow |
|
||||
|--------|------|------|
|
||||
| **Project structure** | `src/project_name/` with `crew.py` | `src/project_name/` with `crews/` folder |
|
||||
| **Main logic location** | `src/project_name/crew.py` | `src/project_name/main.py` (Flow class) |
|
||||
| **Entry point function** | `run()` in `main.py` | `kickoff()` in `main.py` |
|
||||
| **pyproject.toml type** | `type = "crew"` | `type = "flow"` |
|
||||
| **CLI create command** | `crewai create crew name` | `crewai create flow name` |
|
||||
| **Config location** | `src/project_name/config/` | `src/project_name/crews/crew_name/config/` |
|
||||
| **Can contain other crews** | No | Yes (in `crews/` folder) |
|
||||
|
||||
## Project Structure Reference
|
||||
|
||||
### Crew Project Structure
|
||||
|
||||
When you run `crewai create crew my_crew`, you get this structure:
|
||||
|
||||
```
|
||||
my_crew/
|
||||
├── .gitignore
|
||||
├── pyproject.toml # Must have type = "crew"
|
||||
├── README.md
|
||||
├── .env
|
||||
├── uv.lock # REQUIRED for deployment
|
||||
└── src/
|
||||
└── my_crew/
|
||||
├── __init__.py
|
||||
├── main.py # Entry point with run() function
|
||||
├── crew.py # Crew class with @CrewBase decorator
|
||||
├── tools/
|
||||
│ ├── custom_tool.py
|
||||
│ └── __init__.py
|
||||
└── config/
|
||||
├── agents.yaml # Agent definitions
|
||||
└── tasks.yaml # Task definitions
|
||||
```
|
||||
|
||||
<Warning>
|
||||
The nested `src/project_name/` structure is critical for Crews.
|
||||
Placing files at the wrong level will cause deployment failures.
|
||||
</Warning>
|
||||
|
||||
### Flow Project Structure
|
||||
|
||||
When you run `crewai create flow my_flow`, you get this structure:
|
||||
|
||||
```
|
||||
my_flow/
|
||||
├── .gitignore
|
||||
├── pyproject.toml # Must have type = "flow"
|
||||
├── README.md
|
||||
├── .env
|
||||
├── uv.lock # REQUIRED for deployment
|
||||
└── src/
|
||||
└── my_flow/
|
||||
├── __init__.py
|
||||
├── main.py # Entry point with kickoff() function + Flow class
|
||||
├── crews/ # Embedded crews folder
|
||||
│ └── poem_crew/
|
||||
│ ├── __init__.py
|
||||
│ ├── poem_crew.py # Crew with @CrewBase decorator
|
||||
│ └── config/
|
||||
│ ├── agents.yaml
|
||||
│ └── tasks.yaml
|
||||
└── tools/
|
||||
├── __init__.py
|
||||
└── custom_tool.py
|
||||
```
|
||||
|
||||
<Info>
|
||||
Both Crews and Flows use the `src/project_name/` structure.
|
||||
The key difference is that Flows have a `crews/` folder for embedded crews,
|
||||
while Crews have `crew.py` directly in the project folder.
|
||||
</Info>
|
||||
|
||||
## Pre-Deployment Checklist
|
||||
|
||||
Use this checklist to verify your project is ready for deployment.
|
||||
|
||||
### 1. Verify pyproject.toml Configuration
|
||||
|
||||
Your `pyproject.toml` must include the correct `[tool.crewai]` section:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="For Crews">
|
||||
```toml
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="For Flows">
|
||||
```toml
|
||||
[tool.crewai]
|
||||
type = "flow"
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Warning>
|
||||
If the `type` doesn't match your project structure, the build will fail or
|
||||
the automation won't run correctly.
|
||||
</Warning>
|
||||
|
||||
### 2. Ensure uv.lock File Exists
|
||||
|
||||
CrewAI uses `uv` for dependency management. The `uv.lock` file ensures reproducible builds and is **required** for deployment.
|
||||
|
||||
```bash
|
||||
# Generate or update the lock file
|
||||
uv lock
|
||||
|
||||
# Verify it exists
|
||||
ls -la uv.lock
|
||||
```
|
||||
|
||||
If the file doesn't exist, run `uv lock` and commit it to your repository:
|
||||
|
||||
```bash
|
||||
uv lock
|
||||
git add uv.lock
|
||||
git commit -m "Add uv.lock for deployment"
|
||||
git push
|
||||
```
|
||||
|
||||
### 3. Validate CrewBase Decorator Usage
|
||||
|
||||
**Every crew class must use the `@CrewBase` decorator.** This applies to:
|
||||
|
||||
- Standalone crew projects
|
||||
- Crews embedded inside Flow projects
|
||||
|
||||
```python
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
|
||||
@CrewBase # This decorator is REQUIRED
|
||||
class MyCrew():
|
||||
"""My crew description"""
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
@agent
|
||||
def my_agent(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['my_agent'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@task
|
||||
def my_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['my_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
|
||||
<Warning>
|
||||
If you forget the `@CrewBase` decorator, your deployment will fail with
|
||||
errors about missing agents or tasks configurations.
|
||||
</Warning>
|
||||
|
||||
### 4. Check Project Entry Points
|
||||
|
||||
Both Crews and Flows have their entry point in `src/project_name/main.py`:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="For Crews">
|
||||
The entry point uses a `run()` function:
|
||||
|
||||
```python
|
||||
# src/my_crew/main.py
|
||||
from my_crew.crew import MyCrew
|
||||
|
||||
def run():
|
||||
"""Run the crew."""
|
||||
inputs = {'topic': 'AI in Healthcare'}
|
||||
result = MyCrew().crew().kickoff(inputs=inputs)
|
||||
return result
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="For Flows">
|
||||
The entry point uses a `kickoff()` function with a Flow class:
|
||||
|
||||
```python
|
||||
# src/my_flow/main.py
|
||||
from crewai.flow import Flow, listen, start
|
||||
from my_flow.crews.poem_crew.poem_crew import PoemCrew
|
||||
|
||||
class MyFlow(Flow):
|
||||
@start()
|
||||
def begin(self):
|
||||
# Flow logic here
|
||||
result = PoemCrew().crew().kickoff(inputs={...})
|
||||
return result
|
||||
|
||||
def kickoff():
|
||||
"""Run the flow."""
|
||||
MyFlow().kickoff()
|
||||
|
||||
if __name__ == "__main__":
|
||||
kickoff()
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### 5. Prepare Environment Variables
|
||||
|
||||
Before deployment, ensure you have:
|
||||
|
||||
1. **LLM API keys** ready (OpenAI, Anthropic, Google, etc.)
|
||||
2. **Tool API keys** if using external tools (Serper, etc.)
|
||||
|
||||
<Tip>
|
||||
Test your project locally with the same environment variables before deploying
|
||||
to catch configuration issues early.
|
||||
</Tip>
|
||||
|
||||
## Quick Validation Commands
|
||||
|
||||
Run these commands from your project root to quickly verify your setup:
|
||||
|
||||
```bash
|
||||
# 1. Check project type in pyproject.toml
|
||||
grep -A2 "\[tool.crewai\]" pyproject.toml
|
||||
|
||||
# 2. Verify uv.lock exists
|
||||
ls -la uv.lock || echo "ERROR: uv.lock missing! Run 'uv lock'"
|
||||
|
||||
# 3. Verify src/ structure exists
|
||||
ls -la src/*/main.py 2>/dev/null || echo "No main.py found in src/"
|
||||
|
||||
# 4. For Crews - verify crew.py exists
|
||||
ls -la src/*/crew.py 2>/dev/null || echo "No crew.py (expected for Crews)"
|
||||
|
||||
# 5. For Flows - verify crews/ folder exists
|
||||
ls -la src/*/crews/ 2>/dev/null || echo "No crews/ folder (expected for Flows)"
|
||||
|
||||
# 6. Check for CrewBase usage
|
||||
grep -r "@CrewBase" . --include="*.py"
|
||||
```
|
||||
|
||||
## Common Setup Mistakes
|
||||
|
||||
| Mistake | Symptom | Fix |
|
||||
|---------|---------|-----|
|
||||
| Missing `uv.lock` | Build fails during dependency resolution | Run `uv lock` and commit |
|
||||
| Wrong `type` in pyproject.toml | Build succeeds but runtime fails | Change to correct type |
|
||||
| Missing `@CrewBase` decorator | "Config not found" errors | Add decorator to all crew classes |
|
||||
| Files at root instead of `src/` | Entry point not found | Move to `src/project_name/` |
|
||||
| Missing `run()` or `kickoff()` | Cannot start automation | Add correct entry function |
|
||||
|
||||
## Next Steps
|
||||
|
||||
Once your project passes all checklist items, you're ready to deploy:
|
||||
|
||||
<Card title="Deploy to AMP" icon="rocket" href="/en/enterprise/guides/deploy-to-amp">
|
||||
Follow the deployment guide to deploy your Crew or Flow to CrewAI AMP using
|
||||
the CLI, web interface, or CI/CD integration.
|
||||
</Card>
|
||||
@@ -1,43 +1,48 @@
|
||||
---
|
||||
title: Agent-to-Agent (A2A) Protocol
|
||||
description: Enable CrewAI agents to delegate tasks to remote A2A-compliant agents for specialized handling
|
||||
description: Agents delegate tasks to remote A2A agents and/or operate as A2A-compliant server agents.
|
||||
icon: network-wired
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## A2A Agent Delegation
|
||||
|
||||
CrewAI supports the Agent-to-Agent (A2A) protocol, allowing agents to delegate tasks to remote specialized agents. The agent's LLM automatically decides whether to handle a task directly or delegate to an A2A agent based on the task requirements.
|
||||
|
||||
<Note>
|
||||
A2A delegation requires the `a2a-sdk` package. Install with: `uv add 'crewai[a2a]'` or `pip install 'crewai[a2a]'`
|
||||
</Note>
|
||||
CrewAI treats [A2A protocol](https://a2a-protocol.org/latest/) as a first-class delegation primitive, enabling agents to delegate tasks, request information, and collaborate with remote agents, as well as act as A2A-compliant server agents.
|
||||
In client mode, agents autonomously choose between local execution and remote delegation based on task requirements.
|
||||
|
||||
## How It Works
|
||||
|
||||
When an agent is configured with A2A capabilities:
|
||||
|
||||
1. The LLM analyzes each task
|
||||
1. The Agent analyzes each task
|
||||
2. It decides to either:
|
||||
- Handle the task directly using its own capabilities
|
||||
- Delegate to a remote A2A agent for specialized handling
|
||||
3. If delegating, the agent communicates with the remote A2A agent through the protocol
|
||||
4. Results are returned to the CrewAI workflow
|
||||
|
||||
<Note>
|
||||
A2A delegation requires the `a2a-sdk` package. Install with: `uv add 'crewai[a2a]'` or `pip install 'crewai[a2a]'`
|
||||
</Note>
|
||||
|
||||
## Basic Configuration
|
||||
|
||||
<Warning>
|
||||
`crewai.a2a.config.A2AConfig` is deprecated and will be removed in v2.0.0. Use `A2AClientConfig` for connecting to remote agents and/or `A2AServerConfig` for exposing agents as servers.
|
||||
</Warning>
|
||||
|
||||
Configure an agent for A2A delegation by setting the `a2a` parameter:
|
||||
|
||||
```python Code
|
||||
from crewai import Agent, Crew, Task
|
||||
from crewai.a2a import A2AConfig
|
||||
from crewai.a2a import A2AClientConfig
|
||||
|
||||
agent = Agent(
|
||||
role="Research Coordinator",
|
||||
goal="Coordinate research tasks efficiently",
|
||||
backstory="Expert at delegating to specialized research agents",
|
||||
llm="gpt-4o",
|
||||
a2a=A2AConfig(
|
||||
a2a=A2AClientConfig(
|
||||
endpoint="https://example.com/.well-known/agent-card.json",
|
||||
timeout=120,
|
||||
max_turns=10
|
||||
@@ -54,9 +59,9 @@ crew = Crew(agents=[agent], tasks=[task], verbose=True)
|
||||
result = crew.kickoff()
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
## Client Configuration Options
|
||||
|
||||
The `A2AConfig` class accepts the following parameters:
|
||||
The `A2AClientConfig` class accepts the following parameters:
|
||||
|
||||
<ParamField path="endpoint" type="str" required>
|
||||
The A2A agent endpoint URL (typically points to `.well-known/agent-card.json`)
|
||||
@@ -87,14 +92,38 @@ The `A2AConfig` class accepts the following parameters:
|
||||
When `True`, returns the A2A agent's result directly when it signals completion. When `False`, allows the server agent to review the result and potentially continue the conversation.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="updates" type="UpdateConfig" default="StreamingConfig()">
|
||||
Update mechanism for receiving task status. Options: `StreamingConfig`, `PollingConfig`, or `PushNotificationConfig`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="transport_protocol" type="Literal['JSONRPC', 'GRPC', 'HTTP+JSON']" default="JSONRPC">
|
||||
Transport protocol for A2A communication. Options: `JSONRPC` (default), `GRPC`, or `HTTP+JSON`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="accepted_output_modes" type="list[str]" default='["application/json"]'>
|
||||
Media types the client can accept in responses.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="supported_transports" type="list[str]" default='["JSONRPC"]'>
|
||||
Ordered list of transport protocols the client supports.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="use_client_preference" type="bool" default="False">
|
||||
Whether to prioritize client transport preferences over server.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="extensions" type="list[str]" default="[]">
|
||||
Extension URIs the client supports.
|
||||
</ParamField>
|
||||
|
||||
## Authentication
|
||||
|
||||
For A2A agents that require authentication, use one of the provided auth schemes:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Bearer Token">
|
||||
```python Code
|
||||
from crewai.a2a import A2AConfig
|
||||
```python bearer_token_auth.py lines
|
||||
from crewai.a2a import A2AClientConfig
|
||||
from crewai.a2a.auth import BearerTokenAuth
|
||||
|
||||
agent = Agent(
|
||||
@@ -102,18 +131,18 @@ agent = Agent(
|
||||
goal="Coordinate tasks with secured agents",
|
||||
backstory="Manages secure agent communications",
|
||||
llm="gpt-4o",
|
||||
a2a=A2AConfig(
|
||||
a2a=A2AClientConfig(
|
||||
endpoint="https://secure-agent.example.com/.well-known/agent-card.json",
|
||||
auth=BearerTokenAuth(token="your-bearer-token"),
|
||||
timeout=120
|
||||
)
|
||||
)
|
||||
```
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="API Key">
|
||||
```python Code
|
||||
from crewai.a2a import A2AConfig
|
||||
```python api_key_auth.py lines
|
||||
from crewai.a2a import A2AClientConfig
|
||||
from crewai.a2a.auth import APIKeyAuth
|
||||
|
||||
agent = Agent(
|
||||
@@ -121,7 +150,7 @@ agent = Agent(
|
||||
goal="Coordinate with API-based agents",
|
||||
backstory="Manages API-authenticated communications",
|
||||
llm="gpt-4o",
|
||||
a2a=A2AConfig(
|
||||
a2a=A2AClientConfig(
|
||||
endpoint="https://api-agent.example.com/.well-known/agent-card.json",
|
||||
auth=APIKeyAuth(
|
||||
api_key="your-api-key",
|
||||
@@ -131,12 +160,12 @@ agent = Agent(
|
||||
timeout=120
|
||||
)
|
||||
)
|
||||
```
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="OAuth2">
|
||||
```python Code
|
||||
from crewai.a2a import A2AConfig
|
||||
```python oauth2_auth.py lines
|
||||
from crewai.a2a import A2AClientConfig
|
||||
from crewai.a2a.auth import OAuth2ClientCredentials
|
||||
|
||||
agent = Agent(
|
||||
@@ -144,7 +173,7 @@ agent = Agent(
|
||||
goal="Coordinate with OAuth-secured agents",
|
||||
backstory="Manages OAuth-authenticated communications",
|
||||
llm="gpt-4o",
|
||||
a2a=A2AConfig(
|
||||
a2a=A2AClientConfig(
|
||||
endpoint="https://oauth-agent.example.com/.well-known/agent-card.json",
|
||||
auth=OAuth2ClientCredentials(
|
||||
token_url="https://auth.example.com/oauth/token",
|
||||
@@ -155,12 +184,12 @@ agent = Agent(
|
||||
timeout=120
|
||||
)
|
||||
)
|
||||
```
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="HTTP Basic">
|
||||
```python Code
|
||||
from crewai.a2a import A2AConfig
|
||||
```python http_basic_auth.py lines
|
||||
from crewai.a2a import A2AClientConfig
|
||||
from crewai.a2a.auth import HTTPBasicAuth
|
||||
|
||||
agent = Agent(
|
||||
@@ -168,7 +197,7 @@ agent = Agent(
|
||||
goal="Coordinate with basic auth agents",
|
||||
backstory="Manages basic authentication communications",
|
||||
llm="gpt-4o",
|
||||
a2a=A2AConfig(
|
||||
a2a=A2AClientConfig(
|
||||
endpoint="https://basic-agent.example.com/.well-known/agent-card.json",
|
||||
auth=HTTPBasicAuth(
|
||||
username="your-username",
|
||||
@@ -177,7 +206,7 @@ agent = Agent(
|
||||
timeout=120
|
||||
)
|
||||
)
|
||||
```
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -186,7 +215,7 @@ agent = Agent(
|
||||
Configure multiple A2A agents for delegation by passing a list:
|
||||
|
||||
```python Code
|
||||
from crewai.a2a import A2AConfig
|
||||
from crewai.a2a import A2AClientConfig
|
||||
from crewai.a2a.auth import BearerTokenAuth
|
||||
|
||||
agent = Agent(
|
||||
@@ -195,11 +224,11 @@ agent = Agent(
|
||||
backstory="Expert at delegating to the right specialist",
|
||||
llm="gpt-4o",
|
||||
a2a=[
|
||||
A2AConfig(
|
||||
A2AClientConfig(
|
||||
endpoint="https://research.example.com/.well-known/agent-card.json",
|
||||
timeout=120
|
||||
),
|
||||
A2AConfig(
|
||||
A2AClientConfig(
|
||||
endpoint="https://data.example.com/.well-known/agent-card.json",
|
||||
auth=BearerTokenAuth(token="data-token"),
|
||||
timeout=90
|
||||
@@ -215,7 +244,7 @@ The LLM will automatically choose which A2A agent to delegate to based on the ta
|
||||
Control how agent connection failures are handled using the `fail_fast` parameter:
|
||||
|
||||
```python Code
|
||||
from crewai.a2a import A2AConfig
|
||||
from crewai.a2a import A2AClientConfig
|
||||
|
||||
# Fail immediately on connection errors (default)
|
||||
agent = Agent(
|
||||
@@ -223,7 +252,7 @@ agent = Agent(
|
||||
goal="Coordinate research tasks",
|
||||
backstory="Expert at delegation",
|
||||
llm="gpt-4o",
|
||||
a2a=A2AConfig(
|
||||
a2a=A2AClientConfig(
|
||||
endpoint="https://research.example.com/.well-known/agent-card.json",
|
||||
fail_fast=True
|
||||
)
|
||||
@@ -236,11 +265,11 @@ agent = Agent(
|
||||
backstory="Expert at working with available resources",
|
||||
llm="gpt-4o",
|
||||
a2a=[
|
||||
A2AConfig(
|
||||
A2AClientConfig(
|
||||
endpoint="https://primary.example.com/.well-known/agent-card.json",
|
||||
fail_fast=False
|
||||
),
|
||||
A2AConfig(
|
||||
A2AClientConfig(
|
||||
endpoint="https://backup.example.com/.well-known/agent-card.json",
|
||||
fail_fast=False
|
||||
)
|
||||
@@ -253,6 +282,192 @@ When `fail_fast=False`:
|
||||
- If all agents fail, the LLM receives a notice about unavailable agents and handles the task directly
|
||||
- Connection errors are captured and included in the context for better decision-making
|
||||
|
||||
## Update Mechanisms
|
||||
|
||||
Control how your agent receives task status updates from remote A2A agents:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Streaming (Default)">
|
||||
```python streaming_config.py lines
|
||||
from crewai.a2a import A2AClientConfig
|
||||
from crewai.a2a.updates import StreamingConfig
|
||||
|
||||
agent = Agent(
|
||||
role="Research Coordinator",
|
||||
goal="Coordinate research tasks",
|
||||
backstory="Expert at delegation",
|
||||
llm="gpt-4o",
|
||||
a2a=A2AClientConfig(
|
||||
endpoint="https://research.example.com/.well-known/agent-card.json",
|
||||
updates=StreamingConfig()
|
||||
)
|
||||
)
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Polling">
|
||||
```python polling_config.py lines
|
||||
from crewai.a2a import A2AClientConfig
|
||||
from crewai.a2a.updates import PollingConfig
|
||||
|
||||
agent = Agent(
|
||||
role="Research Coordinator",
|
||||
goal="Coordinate research tasks",
|
||||
backstory="Expert at delegation",
|
||||
llm="gpt-4o",
|
||||
a2a=A2AClientConfig(
|
||||
endpoint="https://research.example.com/.well-known/agent-card.json",
|
||||
updates=PollingConfig(
|
||||
interval=2.0,
|
||||
timeout=300.0,
|
||||
max_polls=100
|
||||
)
|
||||
)
|
||||
)
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Push Notifications">
|
||||
```python push_notifications_config.py lines
|
||||
from crewai.a2a import A2AClientConfig
|
||||
from crewai.a2a.updates import PushNotificationConfig
|
||||
|
||||
agent = Agent(
|
||||
role="Research Coordinator",
|
||||
goal="Coordinate research tasks",
|
||||
backstory="Expert at delegation",
|
||||
llm="gpt-4o",
|
||||
a2a=A2AClientConfig(
|
||||
endpoint="https://research.example.com/.well-known/agent-card.json",
|
||||
updates=PushNotificationConfig(
|
||||
url="{base_url}/a2a/callback",
|
||||
token="your-validation-token",
|
||||
timeout=300.0
|
||||
)
|
||||
)
|
||||
)
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Exposing Agents as A2A Servers
|
||||
|
||||
You can expose your CrewAI agents as A2A-compliant servers, allowing other A2A clients to delegate tasks to them.
|
||||
|
||||
### Server Configuration
|
||||
|
||||
Add an `A2AServerConfig` to your agent to enable server capabilities:
|
||||
|
||||
```python a2a_server_agent.py lines
|
||||
from crewai import Agent
|
||||
from crewai.a2a import A2AServerConfig
|
||||
|
||||
agent = Agent(
|
||||
role="Data Analyst",
|
||||
goal="Analyze datasets and provide insights",
|
||||
backstory="Expert data scientist with statistical analysis skills",
|
||||
llm="gpt-4o",
|
||||
a2a=A2AServerConfig(url="https://your-server.com")
|
||||
)
|
||||
```
|
||||
|
||||
### Server Configuration Options
|
||||
|
||||
<ParamField path="name" type="str" default="None">
|
||||
Human-readable name for the agent. Defaults to the agent's role if not provided.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="description" type="str" default="None">
|
||||
Human-readable description. Defaults to the agent's goal and backstory if not provided.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="version" type="str" default="1.0.0">
|
||||
Version string for the agent card.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="skills" type="list[AgentSkill]" default="[]">
|
||||
List of agent skills. Auto-generated from agent tools if not provided.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="capabilities" type="AgentCapabilities" default="AgentCapabilities(streaming=True, push_notifications=False)">
|
||||
Declaration of optional capabilities supported by the agent.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="default_input_modes" type="list[str]" default='["text/plain", "application/json"]'>
|
||||
Supported input MIME types.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="default_output_modes" type="list[str]" default='["text/plain", "application/json"]'>
|
||||
Supported output MIME types.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="url" type="str" default="None">
|
||||
Preferred endpoint URL. If set, overrides the URL passed to `to_agent_card()`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="preferred_transport" type="Literal['JSONRPC', 'GRPC', 'HTTP+JSON']" default="JSONRPC">
|
||||
Transport protocol for the preferred endpoint.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="protocol_version" type="str" default="0.3">
|
||||
A2A protocol version this agent supports.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="provider" type="AgentProvider" default="None">
|
||||
Information about the agent's service provider.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="documentation_url" type="str" default="None">
|
||||
URL to the agent's documentation.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="icon_url" type="str" default="None">
|
||||
URL to an icon for the agent.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="additional_interfaces" type="list[AgentInterface]" default="[]">
|
||||
Additional supported interfaces (transport and URL combinations).
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="security" type="list[dict[str, list[str]]]" default="[]">
|
||||
Security requirement objects for all agent interactions.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="security_schemes" type="dict[str, SecurityScheme]" default="{}">
|
||||
Security schemes available to authorize requests.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="supports_authenticated_extended_card" type="bool" default="False">
|
||||
Whether agent provides extended card to authenticated users.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="signatures" type="list[AgentCardSignature]" default="[]">
|
||||
JSON Web Signatures for the AgentCard.
|
||||
</ParamField>
|
||||
|
||||
### Combined Client and Server
|
||||
|
||||
An agent can act as both client and server by providing both configurations:
|
||||
|
||||
```python Code
|
||||
from crewai import Agent
|
||||
from crewai.a2a import A2AClientConfig, A2AServerConfig
|
||||
|
||||
agent = Agent(
|
||||
role="Research Coordinator",
|
||||
goal="Coordinate research and serve analysis requests",
|
||||
backstory="Expert at delegation and analysis",
|
||||
llm="gpt-4o",
|
||||
a2a=[
|
||||
A2AClientConfig(
|
||||
endpoint="https://specialist.example.com/.well-known/agent-card.json",
|
||||
timeout=120
|
||||
),
|
||||
A2AServerConfig(url="https://your-server.com")
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
<CardGroup cols={2}>
|
||||
|
||||
@@ -7,6 +7,10 @@ mode: "wide"
|
||||
|
||||
## Overview
|
||||
|
||||
<Note>
|
||||
The `@human_feedback` decorator requires **CrewAI version 1.8.0 or higher**. Make sure to update your installation before using this feature.
|
||||
</Note>
|
||||
|
||||
The `@human_feedback` decorator enables human-in-the-loop (HITL) workflows directly within CrewAI Flows. It allows you to pause flow execution, present output to a human for review, collect their feedback, and optionally route to different listeners based on the feedback outcome.
|
||||
|
||||
This is particularly valuable for:
|
||||
|
||||
@@ -11,10 +11,10 @@ Human-in-the-Loop (HITL) is a powerful approach that combines artificial intelli
|
||||
|
||||
CrewAI offers two main approaches for implementing human-in-the-loop workflows:
|
||||
|
||||
| Approach | Best For | Integration |
|
||||
|----------|----------|-------------|
|
||||
| **Flow-based** (`@human_feedback` decorator) | Local development, console-based review, synchronous workflows | [Human Feedback in Flows](/en/learn/human-feedback-in-flows) |
|
||||
| **Webhook-based** (Enterprise) | Production deployments, async workflows, external integrations (Slack, Teams, etc.) | This guide |
|
||||
| Approach | Best For | Integration | Version |
|
||||
|----------|----------|-------------|---------|
|
||||
| **Flow-based** (`@human_feedback` decorator) | Local development, console-based review, synchronous workflows | [Human Feedback in Flows](/en/learn/human-feedback-in-flows) | **1.8.0+** |
|
||||
| **Webhook-based** (Enterprise) | Production deployments, async workflows, external integrations (Slack, Teams, etc.) | This guide | - |
|
||||
|
||||
<Tip>
|
||||
If you're building flows and want to add human review steps with routing based on feedback, check out the [Human Feedback in Flows](/en/learn/human-feedback-in-flows) guide for the `@human_feedback` decorator.
|
||||
|
||||
115
docs/en/observability/galileo.mdx
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: Galileo
|
||||
description: Galileo integration for CrewAI tracing and evaluation
|
||||
icon: telescope
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This guide demonstrates how to integrate **Galileo** with **CrewAI**
|
||||
for comprehensive tracing and Evaluation Engineering.
|
||||
By the end of this guide, you will be able to trace your CrewAI agents,
|
||||
monitor their performance, and evaluate their behaviour with
|
||||
Galileo's powerful observability platform.
|
||||
|
||||
> **What is Galileo?** [Galileo](https://galileo.ai) is AI evaluation and observability
|
||||
platform that delivers end-to-end tracing, evaluation,
|
||||
and monitoring for AI applications. It enables teams to capture ground truth,
|
||||
create robust guardrails, and run systematic experiments with
|
||||
built-in experiment tracking and performance analytics—ensuring reliability,
|
||||
transparency, and continuous improvement across the AI lifecycle.
|
||||
|
||||
## Getting started
|
||||
|
||||
This tutorial follows the [CrewAI quickstart](/en/quickstart) and shows how to add
|
||||
Galileo's [CrewAIEventListener](https://v2docs.galileo.ai/sdk-api/python/reference/handlers/crewai/handler),
|
||||
an event handler.
|
||||
For more information, see Galileo’s
|
||||
[Add Galileo to a CrewAI Application](https://v2docs.galileo.ai/how-to-guides/third-party-integrations/add-galileo-to-crewai/add-galileo-to-crewai)
|
||||
how-to guide.
|
||||
|
||||
> **Note** This tutorial assumes you have completed the [CrewAI quickstart](/en/quickstart).
|
||||
If you want a completed comprehensive example, see the Galileo
|
||||
[CrewAI sdk-example repo](https://github.com/rungalileo/sdk-examples/tree/main/python/agent/crew-ai).
|
||||
|
||||
### Step 1: Install dependencies
|
||||
|
||||
Install the required dependencies for your app.
|
||||
Create a virtual environment using your preferred method,
|
||||
then install dependencies inside that environment using your
|
||||
preferred tool:
|
||||
|
||||
```bash
|
||||
uv add galileo
|
||||
```
|
||||
|
||||
### Step 2: Add to the .env file from the [CrewAI quickstart](/en/quickstart)
|
||||
|
||||
```bash
|
||||
# Your Galileo API key
|
||||
GALILEO_API_KEY="your-galileo-api-key"
|
||||
|
||||
# Your Galileo project name
|
||||
GALILEO_PROJECT="your-galileo-project-name"
|
||||
|
||||
# The name of the Log stream you want to use for logging
|
||||
GALILEO_LOG_STREAM="your-galileo-log-stream "
|
||||
```
|
||||
|
||||
### Step 3: Add the Galileo event listener
|
||||
|
||||
To enable logging with Galileo, you need to create an instance of the `CrewAIEventListener`.
|
||||
Import the Galileo CrewAI handler package by
|
||||
adding the following code at the top of your main.py file:
|
||||
|
||||
```python
|
||||
from galileo.handlers.crewai.handler import CrewAIEventListener
|
||||
```
|
||||
|
||||
At the start of your run function, create the event listener:
|
||||
|
||||
```python
|
||||
def run():
|
||||
# Create the event listener
|
||||
CrewAIEventListener()
|
||||
# The rest of your existing code goes here
|
||||
```
|
||||
|
||||
When you create the listener instance, it is automatically
|
||||
registered with CrewAI.
|
||||
|
||||
### Step 4: Run your crew
|
||||
|
||||
Run your crew with the CrewAI CLI:
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
|
||||
### Step 5: View the traces in Galileo
|
||||
|
||||
Once your crew has finished, the traces will be flushed and appear in Galileo.
|
||||
|
||||

|
||||
|
||||
## Understanding the Galileo Integration
|
||||
|
||||
Galileo integrates with CrewAI by registering an event listener
|
||||
that captures Crew execution events (e.g., agent actions, tool calls, model responses)
|
||||
and forwards them to Galileo for observability and evaluation.
|
||||
|
||||
### Understanding the event listener
|
||||
|
||||
Creating a `CrewAIEventListener()` instance is all that’s
|
||||
required to enable Galileo for a CrewAI run. When instantiated, the listener:
|
||||
|
||||
- Automatically registers itself with CrewAI
|
||||
- Reads Galileo configuration from environment variables
|
||||
- Logs all run data to the Galileo project and log stream specified by
|
||||
`GALILEO_PROJECT` and `GALILEO_LOG_STREAM`
|
||||
|
||||
No additional configuration or code changes are required.
|
||||
All data from this run is logged to the Galileo project and
|
||||
log stream specified by your environment configuration
|
||||
(for example, GALILEO_PROJECT and GALILEO_LOG_STREAM).
|
||||
BIN
docs/images/enterprise/pii_mask_custom_recognizer_salary.png
Normal file
|
After Width: | Height: | Size: 906 KiB |
BIN
docs/images/enterprise/pii_mask_recognizer.png
Normal file
|
After Width: | Height: | Size: 200 KiB |
BIN
docs/images/enterprise/pii_mask_recognizer_create.png
Normal file
|
After Width: | Height: | Size: 865 KiB |
BIN
docs/images/enterprise/pii_mask_recognizer_deployment_tab.png
Normal file
|
After Width: | Height: | Size: 1021 KiB |
BIN
docs/images/enterprise/pii_mask_recognizer_enable.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 116 KiB |
BIN
docs/images/enterprise/pii_mask_recognizer_trace_example.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
docs/images/enterprise/pii_mask_recognizers_options.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
docs/images/galileo-trace-veiw.png
Normal file
|
After Width: | Height: | Size: 239 KiB |
@@ -4,6 +4,545 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 1월 8일">
|
||||
## v1.8.0
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.8.0)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- a2a를 위한 네이티브 비동기 체인 추가
|
||||
- 핸들러 및 설정과 함께 a2a 업데이트 메커니즘(poll/stream/push) 추가
|
||||
- 휴먼 인 더 루프 피드백을 위한 전역 흐름 설정 도입
|
||||
- 스트리밍 도구 호출 이벤트 추가 및 프로바이더 ID 추적 수정
|
||||
- 프로덕션 준비된 Flows 및 Crews 아키텍처 도입
|
||||
- Flows를 위한 HITL 추가
|
||||
- 향상된 이벤트 처리를 위한 EventListener 및 TraceCollectionListener 개선
|
||||
|
||||
### 버그 수정
|
||||
- 누락된 a2a 종속성을 선택적으로 처리
|
||||
- WorkOS 로그인 폴링을 위한 오류 가져오기 수정
|
||||
- 샘플 문서의 잘못된 트리거 이름 수정
|
||||
|
||||
### 문서
|
||||
- 웹훅 스트리밍 문서 업데이트
|
||||
- AOP에서 AMP로 문서 언어 조정
|
||||
|
||||
### 기여자
|
||||
@Vidit-Ostwal, @greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @lucasgomide, @mplachta
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 12월 19일">
|
||||
## v1.7.2
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.7.2)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 버그 수정
|
||||
- 연결 문제 해결
|
||||
|
||||
### 문서
|
||||
- api-reference/status 문서 페이지 업데이트
|
||||
|
||||
### 기여자
|
||||
@greysonlalonde, @heitorado, @lorenzejay, @lucasgomide
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 12월 16일">
|
||||
## v1.7.1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.7.1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 개선 사항
|
||||
- bump 명령에 `--no-commit` 플래그 추가
|
||||
- 도구 인수 직렬화에 JSON 스키마 사용
|
||||
|
||||
### 버그 수정
|
||||
- 도구 저장소 로그인 실패 시 응답에서 오류 메시지 표시 수정
|
||||
- 비동기 작업 실행 시 future의 정상적인 종료 수정
|
||||
- 인덱스를 추가하여 작업 순서 수정
|
||||
- Windows 신호에 대한 플랫폼 호환성 검사 수정
|
||||
- 프로세스 중단을 방지하기 위한 RPM 컨트롤러 타이머 수정
|
||||
- 토큰 사용량 기록 수정 및 스트림에서 응답 모델 검증
|
||||
|
||||
### 문서
|
||||
- 비동기에 대한 번역된 문서 추가
|
||||
- AOP Deploy API 문서 추가
|
||||
- 에이전트 핸들러 커넥터 문서 추가
|
||||
- 네이티브 비동기 문서 추가
|
||||
|
||||
### 기여자
|
||||
@Llamrei, @dragosmc, @gilfeig, @greysonlalonde, @heitorado, @lorenzejay, @mattatcha, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 12월 9일">
|
||||
## v1.7.0
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.7.0)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 비동기 흐름 킥오프 추가
|
||||
- 비동기 크루 지원 추가
|
||||
- 비동기 작업 지원 추가
|
||||
- 비동기 지식 지원 추가
|
||||
- 비동기 메모리 지원 추가
|
||||
- 도구 및 에이전트 실행기에 대한 비동기 지원 추가; 타입 및 문서 개선
|
||||
- a2a 확장 API 및 비동기 에이전트 카드 캐싱 구현; 작업 전파 및 스트리밍 수정
|
||||
- 네이티브 비동기 도구 지원 추가
|
||||
- 비동기 llm 지원 추가
|
||||
- sys 이벤트 유형 및 핸들러 생성
|
||||
|
||||
### 버그 수정
|
||||
- nonetypes가 otel에 전달되지 않도록 보장하는 문제 수정
|
||||
- 토큰 저장소 파일 작업의 교착 상태 수정
|
||||
- otel span이 닫히도록 보장하는 수정
|
||||
- 임베딩에 HuggingFaceEmbeddingFunction 사용, 키 업데이트 및 테스트 추가
|
||||
- 모든 지원되는 anthropic 모델에 대해 supports_tools가 true인지 확인
|
||||
- 라이트 에이전트 흐름에서 훅이 작동하도록 보장
|
||||
|
||||
### 기여자
|
||||
@greysonlalonde, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 11월 29일">
|
||||
## v1.6.1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.6.1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 버그 수정
|
||||
- ChatCompletionsClient 호출이 제대로 작동하도록 수정
|
||||
- 어노테이션에 대해 비동기 메서드가 실행 가능하도록 보장
|
||||
- RagTool.add의 매개변수 수정, 타입 및 테스트 추가
|
||||
- SSE 클라이언트에서 잘못된 매개변수 제거
|
||||
- 'crewai config reset' 명령에서 'oauth2_extra' 설정 삭제
|
||||
|
||||
### 리팩토링
|
||||
- LLM 클래스에서 모델 검증 및 프로바이더 추론 향상
|
||||
|
||||
### 기여자
|
||||
@Vidit-Ostwal, @greysonlalonde, @heitorado, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 11월 25일">
|
||||
## v1.6.0
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.6.0)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 흐름 및 크루에 스트리밍 결과 지원 추가
|
||||
- gemini-3-pro-preview 추가
|
||||
- Entra ID를 사용한 CLI 로그인 지원
|
||||
- Merge Agent Handler 도구 추가
|
||||
- 흐름 이벤트 상태 관리 향상
|
||||
|
||||
### 버그 수정
|
||||
- 사용자 지정 rag 저장소 지속 경로가 전달된 경우 설정되도록 보장
|
||||
- 퍼지 반환이 더 엄격하고 타입 경고를 표시하도록 보장
|
||||
- openai response_format 매개변수 다시 추가 및 테스트 추가
|
||||
- rag 도구 임베딩 설정 수정
|
||||
- 플롯에서 흐름 실행 시작 패널이 표시되지 않도록 보장
|
||||
|
||||
### 문서
|
||||
- 문서에서 AMP에서 AOP로 참조 업데이트
|
||||
- AMP에서 AOP로 업데이트
|
||||
|
||||
### 기여자
|
||||
@Vidit-Ostwal, @gilfeig, @greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @markmcd
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 11월 22일">
|
||||
## v0.203.2
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/0.203.2)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
- 0.203.1에서 0.203.2로 핫픽스 버전 범프
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 11월 16일">
|
||||
## v1.5.0
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.5.0)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- a2a 신뢰 원격 완료 상태 플래그 추가
|
||||
- Okta 인증 서버에 대한 더 많은 데이터 가져오기 및 저장
|
||||
- CrewAgentExecutor에서 LLM 호출 전후 훅 구현
|
||||
- TaskOutput 및 LiteAgentOutputs에 메시지 노출
|
||||
- QdrantVectorSearchTool의 스키마 설명 향상
|
||||
|
||||
### 버그 수정
|
||||
- 추적 인스트루멘테이션 플래그가 올바르게 적용되도록 보장
|
||||
- 사용자 정의 도구 문서 링크 수정 및 Mintlify 깨진 링크 작업 추가
|
||||
|
||||
### 문서
|
||||
- LLM 기반 검증 지원으로 작업 가드레일 문서 향상
|
||||
|
||||
### 기여자
|
||||
@danielfsbarreto, @greysonlalonde, @heitorado, @lorenzejay, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 11월 7일">
|
||||
## v1.4.1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.4.1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 버그 수정
|
||||
- 에이전트 최대 반복 처리 수정
|
||||
- LLM 모델 구문에 대한 라우팅 문제를 해당 프로바이더로 해결
|
||||
|
||||
### 기여자
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 11월 7일">
|
||||
## v1.4.0
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.4.0)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 비AST 플롯 경로 지원 추가
|
||||
- MCP에 대한 일급 지원 구현
|
||||
- BaseInterceptor에 Pydantic 검증 던더 추가
|
||||
- LLM 메시지 인터셉터 훅 지원 추가
|
||||
- 효율적인 사용을 위한 i18n 프롬프트 캐싱
|
||||
- QdrantVectorSearchTool 향상
|
||||
|
||||
### 버그 수정
|
||||
- stopwords 업데이트 유지 관련 문제 수정
|
||||
- 흐름 상태에서 피클할 수 없는 값 해결
|
||||
- 라이트 에이전트가 검증 오류 시 수정되도록 보장
|
||||
- 캐싱이 작동하도록 콜백 인수 해싱 수정
|
||||
- 유효한 URL에서 RAG 소스 콘텐츠 추가 허용
|
||||
- 플롯 노드 선택을 더 부드럽게 만듦
|
||||
- 지식에 대한 중복 문서 ID 수정
|
||||
|
||||
### 리팩토링
|
||||
- concurrent futures로 MCP 도구 실행 처리 개선
|
||||
- 흐름 처리, 타입 및 로깅 단순화; UI 및 테스트 업데이트
|
||||
- 중지 단어 관리를 속성으로 리팩토링
|
||||
|
||||
### 문서
|
||||
- embedder를 embedding_model로 마이그레이션하고 도구 문서 전체에 vectordb 필요; 프로바이더 예제 추가 (en/ko/pt-BR)
|
||||
|
||||
### 기여자
|
||||
@danielfsbarreto, @greysonlalonde, @lorenzejay, @lucasgomide, @tonykipkemboi
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 11월 1일">
|
||||
## v1.3.0
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.3.0)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 흐름 처리, 타입 및 로깅 리팩토링
|
||||
- QdrantVectorSearchTool 향상
|
||||
|
||||
### 버그 수정
|
||||
- Firecrawl 도구 수정 및 테스트 추가
|
||||
- use_stop_words를 속성으로 리팩토링하고 중지 단어 확인 추가
|
||||
|
||||
### 문서
|
||||
- embedder를 embedding_model로 마이그레이션하고 도구 문서 전체에 vectordb 필요
|
||||
- 영어, 한국어 및 포르투갈어로 프로바이더 예제 추가
|
||||
|
||||
### 리팩토링
|
||||
- 흐름 처리 및 UI 업데이트 개선
|
||||
|
||||
### 기여자
|
||||
@danielfsbarreto, @greysonlalonde, @lorenzejay, @lucasgomide, @tonykipkemboi
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 10월 27일">
|
||||
## v1.2.1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.2.1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- Datadog 통합 지원 추가
|
||||
- liteagent에서 apps 및 mcps 지원
|
||||
|
||||
### 문서
|
||||
- 각 통합에 대해 Platform 도구를 호출하기 위한 필수 환경 변수 설명
|
||||
- Datadog 통합 문서 추가
|
||||
|
||||
### 기여자
|
||||
@barieom, @lorenzejay, @lucasgomide, @sabrenner
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 10월 24일">
|
||||
## v1.2.0
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.2.0)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 버그 수정
|
||||
- 기본 LLM 모델 업데이트 및 LLM 유틸리티의 오류 로깅 개선
|
||||
- 흐름 시각화 디렉토리 및 메서드 검사 변경
|
||||
|
||||
### 사용되지 않는 항목 삭제
|
||||
- aisuite 제거
|
||||
|
||||
### 기여자
|
||||
@greysonlalonde, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 10월 21일">
|
||||
## v1.1.0
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.1.0)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- InternalInstructor를 향상하여 여러 LLM 프로바이더 지원
|
||||
- mypy 플러그인 기반 구현
|
||||
- QdrantVectorSearchTool 개선
|
||||
|
||||
### 버그 수정
|
||||
- 깨진 통합 문서 링크 수정
|
||||
- 이중 추적 호출 수정 및 타입 추가
|
||||
- 템플릿 버전을 최신으로 고정
|
||||
|
||||
### 문서
|
||||
- LLM 통합 세부 정보 및 예제 업데이트
|
||||
|
||||
### 리팩토링
|
||||
- CrewBase 타이핑 개선
|
||||
|
||||
### 기여자
|
||||
@cwarre33, @danielfsbarreto, @greysonlalonde, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 10월 20일">
|
||||
## v1.0.0
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 버전을 1.0.0으로 범프
|
||||
- Agent 클래스에서 지식 및 가드레일 이벤트 처리 향상
|
||||
- crewai run 명령에 도구 저장소 자격 증명 주입
|
||||
|
||||
### 버그 수정
|
||||
- Flow 데코레이터에서 중첩된 조건 구조 유지
|
||||
- Printer.print 메서드에 표준 인쇄 매개변수 추가
|
||||
- input()을 사용할 수 없을 때 오류 수정
|
||||
- JWT 디코딩 시 10초 여유 추가
|
||||
- 잘못된 cron 일정 되돌리기
|
||||
- 특정 날짜에 5일마다 실행되도록 cron 일정 수정
|
||||
- 하드코딩된 경로 대신 Docker 바이너리에 시스템 PATH 사용
|
||||
- 템플릿 디렉토리를 올바르게 제외하기 위한 CodeQL 구성 추가
|
||||
|
||||
### 문서
|
||||
- 취약점 보고를 위한 보안 정책 업데이트
|
||||
- CrewAI AMP에서 텔레메트리 로그 캡처 가이드 추가
|
||||
- 누락된 /resume 파일 추가
|
||||
- HITL 워크플로에서 웹훅 URL 매개변수 명확화
|
||||
|
||||
### 기여자
|
||||
@Vidit-Ostwal, @greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @lucasgomide, @mplachta, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 10월 18일">
|
||||
## v1.0.0b3 (프리릴리스)
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0b3)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 작업 가드레일 기능 및 검증 향상
|
||||
- 네이티브 SDK 가져오기 지원 개선
|
||||
- Azure 네이티브 테스트 추가
|
||||
- 고급 기능으로 BedrockCompletion 클래스 향상
|
||||
- 클라이언트 매개변수 지원으로 GeminiCompletion 클래스 향상
|
||||
- 추가 클라이언트 매개변수로 AnthropicCompletion 클래스 향상
|
||||
|
||||
### 버그 수정
|
||||
- Flow 데코레이터에서 중첩된 조건 구조 유지
|
||||
- Printer.print 메서드에 표준 인쇄 매개변수 추가
|
||||
- stdout 인쇄 제거 및 테스트 결정론 개선
|
||||
|
||||
### 리팩토링
|
||||
- 전체 타이핑을 포함한 메타클래스로 프로젝트 모듈 변환
|
||||
|
||||
### 기여자
|
||||
@greysonlalonde, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 10월 16일">
|
||||
## v1.0.0b2 (프리릴리스)
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0b2)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 추가 클라이언트 매개변수로 OpenAICompletion 클래스 향상
|
||||
- 이벤트 버스 스레드 안전성 및 비동기 지원 개선
|
||||
- crewai run 명령에 도구 저장소 자격 증명 주입
|
||||
|
||||
### 버그 수정
|
||||
- input()을 사용할 수 없을 때 오류가 발생하는 문제 수정
|
||||
- JWT 디코딩 시 10초 여유 추가
|
||||
- task.py에서 복사 및 NOT_SPECIFIED 확인 수정
|
||||
|
||||
### 문서
|
||||
- 문서에서 CREWAI_PLATFORM_INTEGRATION_TOKEN이 언급되도록 보장
|
||||
- 트리거 문서 업데이트
|
||||
|
||||
### 기여자
|
||||
@Vidit-Ostwal, @greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @lucasgomide
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 10월 14일">
|
||||
## v1.0.0b1 (프리릴리스)
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0b1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 추가 클라이언트 매개변수로 OpenAICompletion 클래스 향상
|
||||
- 이벤트 버스 스레드 안전성 및 비동기 지원 개선
|
||||
- Bedrock LLM 통합 구현
|
||||
|
||||
### 버그 수정
|
||||
- 누락된 input() 가용성 문제 수정
|
||||
- 10초 여유를 추가하여 JWT 디코딩 오류 해결
|
||||
- crewai run 명령에 도구 저장소 자격 증명 주입
|
||||
- task.py에서 복사 및 NOT_SPECIFIED 확인 수정
|
||||
|
||||
### 문서
|
||||
- 문서에서 CREWAI_PLATFORM_INTEGRATION_TOKEN이 언급되도록 보장
|
||||
- 트리거 문서 업데이트
|
||||
|
||||
### 기여자
|
||||
@Vidit-Ostwal, @greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @lucasgomide
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 10월 13일">
|
||||
## v0.203.1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/0.203.1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 핵심 개선 및 수정
|
||||
- `crewai run` 명령에 도구 저장소 자격 증명 주입 수정
|
||||
- 토큰 검증 오류를 줄이기 위해 JWT 디코딩 시 10초 여유 추가
|
||||
- 특정 날짜에 5일마다 작업을 실행하도록 의도된 cron 일정 수정(이후 되돌림)
|
||||
|
||||
### 문서 및 가이드
|
||||
- 취약점 보고 프로세스를 명확히 하기 위해 보안 정책 업데이트
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 10월 9일">
|
||||
## v1.0.0a4 (프리릴리스)
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0a4)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- Agent 클래스에서 지식 및 가드레일 이벤트 처리 향상
|
||||
- 로컬 개발을 위한 트리거 목록 및 실행 명령 도입
|
||||
- Platform Actions을 소비하는 새로운 접근 방식으로 문서 업데이트
|
||||
- CrewAI AMP에서 텔레메트리 로그 캡처 가이드 추가
|
||||
|
||||
### 버그 수정
|
||||
- 잘못된 cron 일정 되돌리기
|
||||
- 특정 날짜에 5일마다 실행되도록 cron 일정 수정
|
||||
- 중복 행 제거 및 명시적 환경 변수 추가
|
||||
|
||||
### 기여자
|
||||
@greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @lucasgomide, @mplachta, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 10월 7일">
|
||||
## v1.0.0a3 (프리릴리스)
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0a3)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 플랫폼 작업에 대한 에이전트 지원 추가
|
||||
- 코드 실행기 도구에 인터프리터 인수 추가
|
||||
- 플랫폼 앱 실행에 대한 직접 지원
|
||||
|
||||
### 문서
|
||||
- 플랫폼 작업 문서 추가
|
||||
- MCP 문서에 stdio 및 sse 전송 유형 추가
|
||||
- AWS 모델 목록 업데이트
|
||||
|
||||
### 기여자
|
||||
@greysonlalonde, @heitorado, @lorenzejay, @lucasgomide
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 10월 3일">
|
||||
## v1.0.0a2 (프리릴리스)
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0a2)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 핵심 개선 및 수정
|
||||
- 모노레포를 위한 CI 업데이트
|
||||
- 기본 Anthropic 모델을 claude-sonnet-4-20250514로 업데이트
|
||||
- 모델 업데이트에 대한 테스트 수정
|
||||
|
||||
### 기여자
|
||||
@greysonlalonde, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2025년 9월 30일">
|
||||
## v1.0.0a1
|
||||
|
||||
|
||||
@@ -567,6 +567,10 @@ Fourth method running
|
||||
|
||||
### Human in the Loop (인간 피드백)
|
||||
|
||||
<Note>
|
||||
`@human_feedback` 데코레이터는 **CrewAI 버전 1.8.0 이상**이 필요합니다.
|
||||
</Note>
|
||||
|
||||
`@human_feedback` 데코레이터는 인간의 피드백을 수집하기 위해 플로우 실행을 일시 중지하는 human-in-the-loop 워크플로우를 가능하게 합니다. 이는 승인 게이트, 품질 검토, 인간의 판단이 필요한 결정 지점에 유용합니다.
|
||||
|
||||
```python Code
|
||||
|
||||
@@ -128,7 +128,7 @@ Flow를 배포할 때 다음을 고려하세요:
|
||||
### CrewAI Enterprise
|
||||
Flow를 배포하는 가장 쉬운 방법은 CrewAI Enterprise를 사용하는 것입니다. 인프라, 인증 및 모니터링을 대신 처리합니다.
|
||||
|
||||
시작하려면 [배포 가이드](/ko/enterprise/guides/deploy-crew)를 확인하세요.
|
||||
시작하려면 [배포 가이드](/ko/enterprise/guides/deploy-to-amp)를 확인하세요.
|
||||
|
||||
```bash
|
||||
crewai deploy create
|
||||
|
||||
@@ -91,7 +91,7 @@ Git 없이 빠르게 배포 — 프로젝트 ZIP 패키지를 업로드하세요
|
||||
## 관련 문서
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="크루 배포" href="/ko/enterprise/guides/deploy-crew" icon="rocket">
|
||||
<Card title="크루 배포" href="/ko/enterprise/guides/deploy-to-amp" icon="rocket">
|
||||
GitHub 또는 ZIP 파일로 크루 배포
|
||||
</Card>
|
||||
<Card title="자동화 트리거" href="/ko/enterprise/guides/automation-triggers" icon="trigger">
|
||||
|
||||
@@ -79,7 +79,7 @@ Crew Studio는 자연어와 시각적 워크플로 에디터로 처음부터 자
|
||||
<Card title="크루 빌드" href="/ko/enterprise/guides/build-crew" icon="paintbrush">
|
||||
크루를 빌드하세요.
|
||||
</Card>
|
||||
<Card title="크루 배포" href="/ko/enterprise/guides/deploy-crew" icon="rocket">
|
||||
<Card title="크루 배포" href="/ko/enterprise/guides/deploy-to-amp" icon="rocket">
|
||||
GitHub 또는 ZIP 파일로 크루 배포.
|
||||
</Card>
|
||||
<Card title="React 컴포넌트 내보내기" href="/ko/enterprise/guides/react-component-export" icon="download">
|
||||
|
||||
342
docs/ko/enterprise/features/pii-trace-redactions.mdx
Normal file
@@ -0,0 +1,342 @@
|
||||
---
|
||||
title: 트레이스용 PII 삭제
|
||||
description: "크루 및 플로우 실행 트레이스에서 민감한 데이터를 자동으로 삭제합니다"
|
||||
icon: "lock"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
PII 삭제는 크루 및 플로우 실행 트레이스에서 개인 식별 정보(PII)를 자동으로 감지하고 마스킹하는 CrewAI AMP 기능입니다. 이를 통해 신용카드 번호, 주민등록번호, 이메일 주소, 이름과 같은 민감한 데이터가 CrewAI AMP 트레이스에 노출되지 않도록 보장합니다. 또한 조직별 데이터를 보호하기 위해 커스텀 인식기를 생성할 수 있습니다.
|
||||
|
||||
|
||||
<Info>
|
||||
PII 삭제는 Enterprise 플랜에서 사용 가능합니다.
|
||||
배포 버전은 1.8.0 이상이어야 합니다.
|
||||
</Info>
|
||||
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
|
||||
## PII 삭제가 중요한 이유
|
||||
|
||||
프로덕션 환경에서 AI 에이전트를 실행할 때, 민감한 정보가 종종 크루를 통해 흐릅니다:
|
||||
|
||||
- CRM 통합의 고객 데이터
|
||||
- 결제 처리업체의 금융 정보
|
||||
- 양식 제출의 개인 정보
|
||||
- 내부 직원 데이터
|
||||
|
||||
적절한 삭제 없이는 이 데이터가 트레이스에 나타나, GDPR, HIPAA, PCI-DSS와 같은 규정 준수가 어려워집니다. PII 삭제는 트레이스에 저장되기 전에 민감한 데이터를 자동으로 마스킹하여 이 문제를 해결합니다.
|
||||
|
||||
## 작동 방식
|
||||
|
||||
1. **감지** - 알려진 PII 패턴에 대해 트레이스 이벤트 데이터를 스캔
|
||||
2. **분류** - 민감한 데이터 유형 식별 (신용카드, SSN, 이메일 등)
|
||||
3. **마스킹/삭제** - 구성에 따라 민감한 데이터를 마스킹된 값으로 대체
|
||||
|
||||
```
|
||||
원본: "john.doe@company.com으로 연락하거나 555-123-4567로 전화하세요"
|
||||
삭제됨: "<EMAIL_ADDRESS>로 연락하거나 <PHONE_NUMBER>로 전화하세요"
|
||||
```
|
||||
|
||||
## PII 삭제 활성화
|
||||
|
||||
<Info>
|
||||
이 기능을 사용하려면 Enterprise 플랜이어야 하며 배포 버전이 1.8.0 이상이어야 합니다.
|
||||
</Info>
|
||||
|
||||
<Steps>
|
||||
<Step title="크루 설정으로 이동">
|
||||
CrewAI AMP 대시보드에서 배포된 크루를 선택하고 배포/자동화 중 하나로 이동한 다음 **Settings** → **PII Protection**으로 이동합니다.
|
||||
</Step>
|
||||
|
||||
<Step title="PII 보호 활성화">
|
||||
**PII Redaction for Traces**를 토글하여 활성화합니다. 이렇게 하면 트레이스 데이터의 자동 스캔 및 삭제가 활성화됩니다.
|
||||
|
||||
<Info>
|
||||
각 배포에 대해 PII 삭제를 수동으로 활성화해야 합니다.
|
||||
</Info>
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="엔티티 유형 구성">
|
||||
감지하고 삭제할 PII 유형을 선택합니다. 각 엔티티는 개별적으로 활성화하거나 비활성화할 수 있습니다.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="저장">
|
||||
구성을 저장합니다. PII 삭제는 이후 모든 크루 실행에서 활성화되며, 재배포가 필요하지 않습니다.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 지원되는 엔티티 유형
|
||||
|
||||
CrewAI는 카테고리별로 구성된 다음 PII 엔티티 유형을 지원합니다.
|
||||
|
||||
### 글로벌 엔티티
|
||||
|
||||
| 엔티티 | 설명 | 예시 |
|
||||
|--------|------|------|
|
||||
| `CREDIT_CARD` | 신용/직불 카드 번호 | "4111-1111-1111-1111" |
|
||||
| `CRYPTO` | 암호화폐 지갑 주소 | "bc1qxy2kgd..." |
|
||||
| `DATE_TIME` | 날짜 및 시간 | "2024년 1월 15일" |
|
||||
| `EMAIL_ADDRESS` | 이메일 주소 | "john@example.com" |
|
||||
| `IBAN_CODE` | 국제 은행 계좌 번호 | "DE89 3704 0044 0532 0130 00" |
|
||||
| `IP_ADDRESS` | IPv4 및 IPv6 주소 | "192.168.1.1" |
|
||||
| `LOCATION` | 지리적 위치 | "뉴욕시" |
|
||||
| `MEDICAL_LICENSE` | 의료 면허 번호 | "MD12345" |
|
||||
| `NRP` | 국적, 종교 또는 정치 그룹 | - |
|
||||
| `PERSON` | 개인 이름 | "홍길동" |
|
||||
| `PHONE_NUMBER` | 다양한 형식의 전화번호 | "+82 (10) 1234-5678" |
|
||||
| `URL` | 웹 URL | "https://example.com" |
|
||||
|
||||
### 미국 특정 엔티티
|
||||
|
||||
| 엔티티 | 설명 | 예시 |
|
||||
|--------|------|------|
|
||||
| `US_BANK_NUMBER` | 미국 은행 계좌 번호 | "1234567890" |
|
||||
| `US_DRIVER_LICENSE` | 미국 운전면허 번호 | "D1234567" |
|
||||
| `US_ITIN` | 개인 납세자 번호 | "900-70-0000" |
|
||||
| `US_PASSPORT` | 미국 여권 번호 | "123456789" |
|
||||
| `US_SSN` | 사회보장번호 | "123-45-6789" |
|
||||
|
||||
## 삭제 작업
|
||||
|
||||
활성화된 각 엔티티에 대해 데이터가 삭제되는 방식을 구성할 수 있습니다:
|
||||
|
||||
| 작업 | 설명 | 출력 예시 |
|
||||
|------|------|----------|
|
||||
| `mask` | 엔티티 유형 레이블로 대체 | `<CREDIT_CARD>` |
|
||||
| `redact` | 텍스트를 완전히 제거 | *(비어있음)* |
|
||||
|
||||
## 커스텀 인식기
|
||||
|
||||
기본 제공 엔티티 외에도 조직별 PII 패턴을 감지하기 위한 **커스텀 인식기**를 생성할 수 있습니다.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
### 인식기 유형
|
||||
|
||||
커스텀 인식기에는 두 가지 옵션이 있습니다:
|
||||
|
||||
| 유형 | 적합한 용도 | 사용 사례 예시 |
|
||||
|------|------------|---------------|
|
||||
| **패턴 기반 (Regex)** | 예측 가능한 형식의 구조화된 데이터 | 급여 금액, 직원 ID, 프로젝트 코드 |
|
||||
| **거부 목록** | 정확한 문자열 매칭 | 회사명, 내부 코드명, 특정 용어 |
|
||||
|
||||
### 커스텀 인식기 생성
|
||||
|
||||
<Steps>
|
||||
<Step title="커스텀 인식기로 이동">
|
||||
조직 **Settings** → **Organization** → **Add Recognizer**로 이동합니다.
|
||||
</Step>
|
||||
|
||||
<Step title="인식기 구성">
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
다음 필드를 구성합니다:
|
||||
- **Name**: 인식기의 설명적 이름
|
||||
- **Entity Type**: 삭제된 출력에 나타날 엔티티 레이블 (예: `EMPLOYEE_ID`, `SALARY`)
|
||||
- **Type**: Regex 패턴 또는 거부 목록 중 선택
|
||||
- **Pattern/Values**: 매칭할 Regex 패턴 또는 문자열 목록
|
||||
- **Confidence Threshold**: 삭제를 트리거하는 데 필요한 최소 점수 (0.0-1.0). 높은 값 (예: 0.8)은 거짓 양성을 줄이지만 일부 매치를 놓칠 수 있습니다. 낮은 값 (예: 0.5)은 더 많은 매치를 잡지만 과도하게 삭제할 수 있습니다. 기본값은 0.8입니다.
|
||||
- **Context Words** (선택사항): 근처에서 발견될 때 감지 신뢰도를 높이는 단어
|
||||
</Step>
|
||||
|
||||
<Step title="저장">
|
||||
인식기를 저장합니다. 배포에서 활성화할 수 있게 됩니다.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### 엔티티 유형 이해하기
|
||||
|
||||
**Entity Type**은 매칭된 콘텐츠가 삭제된 트레이스에 어떻게 나타나는지 결정합니다:
|
||||
|
||||
```
|
||||
Entity Type: SALARY
|
||||
Pattern: salary:\s*\$\s*\d+
|
||||
입력: "직원 급여: $50,000"
|
||||
출력: "직원 <SALARY>"
|
||||
```
|
||||
|
||||
### 컨텍스트 단어 사용
|
||||
|
||||
컨텍스트 단어는 매칭된 패턴 근처에 특정 용어가 나타날 때 신뢰도를 높여 정확도를 향상시킵니다:
|
||||
|
||||
```
|
||||
Context Words: "project", "code", "internal"
|
||||
Entity Type: PROJECT_CODE
|
||||
Pattern: PRJ-\d{4}
|
||||
```
|
||||
|
||||
"project" 또는 "code"가 "PRJ-1234" 근처에 나타나면, 인식기는 그것이 진정한 매치라는 확신이 높아져 거짓 양성을 줄입니다.
|
||||
|
||||
|
||||
## 삭제된 트레이스 보기
|
||||
|
||||
PII 삭제가 활성화되면, 트레이스에서 민감한 데이터 대신 삭제된 값이 표시됩니다:
|
||||
|
||||
```
|
||||
Task Output: "고객 <PERSON>이 주문 #12345를 했습니다.
|
||||
연락처 이메일: <EMAIL_ADDRESS>, 전화: <PHONE_NUMBER>.
|
||||
<CREDIT_CARD>로 끝나는 카드로 결제가 처리되었습니다."
|
||||
```
|
||||
|
||||
삭제된 값은 꺾쇠 괄호와 엔티티 유형 레이블 (예: `<EMAIL_ADDRESS>`)로 명확하게 표시되어, 어떤 데이터가 보호되었는지 쉽게 이해할 수 있으면서도 크루 동작을 디버그하고 모니터링할 수 있습니다.
|
||||
|
||||
|
||||
|
||||
## 모범 사례
|
||||
|
||||
### 성능 고려사항
|
||||
|
||||
<Steps>
|
||||
<Step title="필요한 엔티티만 활성화">
|
||||
활성화된 각 엔티티는 처리 오버헤드를 추가합니다. 데이터와 관련된 엔티티만 활성화하세요.
|
||||
</Step>
|
||||
|
||||
<Step title="구체적인 패턴 사용">
|
||||
커스텀 인식기의 경우 거짓 양성을 줄이고 성능을 향상시키기 위해 구체적인 패턴을 사용하세요. Regex 패턴은 급여, 직원 ID, 프로젝트 코드 등 특정 패턴을 식별할 때 가장 적합합니다. 거부 목록 인식기는 회사명, 내부 코드명 등 정확한 문자열을 식별할 때 가장 적합합니다.
|
||||
</Step>
|
||||
|
||||
<Step title="컨텍스트 단어 활용">
|
||||
컨텍스트 단어는 주변 텍스트가 매칭될 때만 감지를 트리거하여 정확도를 향상시킵니다.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 문제 해결
|
||||
|
||||
<Accordion title="PII가 삭제되지 않음">
|
||||
**가능한 원인:**
|
||||
- 구성에서 엔티티 유형이 활성화되지 않음
|
||||
- 패턴이 데이터 형식과 매치되지 않음
|
||||
- 커스텀 인식기에 구문 오류가 있음
|
||||
|
||||
**해결책:**
|
||||
- Settings → Security에서 엔티티가 활성화되어 있는지 확인
|
||||
- 샘플 데이터로 regex 패턴 테스트
|
||||
- 구성 오류에 대한 로그 확인
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="너무 많은 데이터가 삭제됨">
|
||||
**가능한 원인:**
|
||||
- 너무 광범위한 엔티티 유형이 활성화됨 (예: `DATE_TIME`이 모든 곳의 날짜를 잡음)
|
||||
- 커스텀 인식기 패턴이 너무 일반적임
|
||||
|
||||
**해결책:**
|
||||
- 거짓 양성을 유발하는 엔티티 비활성화
|
||||
- 커스텀 패턴을 더 구체적으로 만들기
|
||||
- 정확도 향상을 위해 컨텍스트 단어 추가
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="성능 문제">
|
||||
**가능한 원인:**
|
||||
- 너무 많은 엔티티가 활성화됨
|
||||
- NLP 기반 엔티티 (`PERSON`, `LOCATION`, `NRP`)는 머신러닝 모델을 사용하므로 계산 비용이 높음
|
||||
|
||||
**해결책:**
|
||||
- 실제로 필요한 엔티티만 활성화
|
||||
- 가능한 경우 패턴 기반 대안 고려
|
||||
- 대시보드에서 트레이스 처리 시간 모니터링
|
||||
</Accordion>
|
||||
|
||||
---
|
||||
|
||||
## 실제 예시: 급여 패턴 매칭
|
||||
|
||||
이 예시는 트레이스에서 급여 정보를 감지하고 마스킹하는 커스텀 인식기를 생성하는 방법을 보여줍니다.
|
||||
|
||||
### 사용 사례
|
||||
|
||||
크루가 다음과 같은 형식의 급여 정보가 포함된 직원 또는 재무 데이터를 처리합니다:
|
||||
- `salary: $50,000`
|
||||
- `salary: $125,000.00`
|
||||
- `salary:$1,500.50`
|
||||
|
||||
민감한 보상 데이터를 보호하기 위해 이러한 값을 자동으로 마스킹하려고 합니다.
|
||||
|
||||
### 구성
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
| 필드 | 값 |
|
||||
|------|-----|
|
||||
| **Name** | `SALARY` |
|
||||
| **Entity Type** | `SALARY` |
|
||||
| **Type** | Regex Pattern |
|
||||
| **Regex Pattern** | `salary:\s*\$\s*\d{1,3}(,\d{3})*(\.\d{2})?` |
|
||||
| **Action** | Mask |
|
||||
| **Confidence Threshold** | `0.8` |
|
||||
| **Context Words** | `salary, compensation, pay, wage, income` |
|
||||
|
||||
### Regex 패턴 분석
|
||||
|
||||
| 패턴 구성요소 | 의미 |
|
||||
|--------------|------|
|
||||
| `salary:` | 리터럴 텍스트 "salary:" 매치 |
|
||||
| `\s*` | 0개 이상의 공백 문자 매치 |
|
||||
| `\$` | 달러 기호 매치 (이스케이프) |
|
||||
| `\s*` | $ 뒤의 0개 이상의 공백 문자 매치 |
|
||||
| `\d{1,3}` | 1-3자리 숫자 매치 (예: "1", "50", "125") |
|
||||
| `(,\d{3})*` | 쉼표로 구분된 천 단위 매치 (예: ",000", ",500,000") |
|
||||
| `(\.\d{2})?` | 선택적으로 센트 매치 (예: ".00", ".50") |
|
||||
|
||||
### 결과 예시
|
||||
|
||||
```
|
||||
원본: "직원 기록에 salary: $125,000.00 연봉이 표시됩니다"
|
||||
삭제됨: "직원 기록에 <SALARY> 연봉이 표시됩니다"
|
||||
|
||||
원본: "기본 salary:$50,000에 보너스 가능성"
|
||||
삭제됨: "기본 <SALARY>에 보너스 가능성"
|
||||
```
|
||||
|
||||
<Tip>
|
||||
"salary", "compensation", "pay", "wage", "income"과 같은 컨텍스트 단어를 추가하면 이러한 용어가 매칭된 패턴 근처에 나타날 때 감지 신뢰도가 높아져 거짓 양성을 줄입니다.
|
||||
</Tip>
|
||||
|
||||
### 배포에서 인식기 활성화
|
||||
|
||||
<Warning>
|
||||
조직 수준에서 커스텀 인식기를 생성해도 배포에 자동으로 활성화되지 않습니다. 적용하려는 모든 배포에 대해 각 인식기를 수동으로 활성화해야 합니다.
|
||||
</Warning>
|
||||
|
||||
커스텀 인식기를 생성한 후, 각 배포에서 활성화합니다:
|
||||
|
||||
<Steps>
|
||||
<Step title="배포로 이동">
|
||||
배포/자동화로 이동하여 **Settings** → **PII Protection**을 엽니다.
|
||||
</Step>
|
||||
|
||||
<Step title="커스텀 인식기 선택">
|
||||
**Mask Recognizers** 아래에서 조직에서 정의한 인식기를 볼 수 있습니다. 활성화하려는 인식기 옆의 체크박스를 선택합니다.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="구성 저장">
|
||||
변경 사항을 저장합니다. 인식기는 이 배포의 모든 후속 실행에서 활성화됩니다.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Info>
|
||||
커스텀 인식기가 필요한 각 배포에서 이 프로세스를 반복합니다. 이를 통해 다양한 환경 (예: 개발 vs. 프로덕션)에서 어떤 인식기가 활성화되는지 세밀하게 제어할 수 있습니다.
|
||||
</Info>
|
||||
@@ -1,305 +0,0 @@
|
||||
---
|
||||
title: "Crew 배포"
|
||||
description: "CrewAI 엔터프라이즈에서 Crew 배포하기"
|
||||
icon: "rocket"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Note>
|
||||
로컬에서 또는 Crew Studio를 통해 crew를 생성한 후, 다음 단계는 이를 CrewAI AMP
|
||||
플랫폼에 배포하는 것입니다. 본 가이드에서는 다양한 배포 방법을 다루며,
|
||||
여러분의 워크플로우에 가장 적합한 방식을 선택할 수 있도록 안내합니다.
|
||||
</Note>
|
||||
|
||||
## 사전 준비 사항
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="배포 준비가 된 Crew" icon="users">
|
||||
작동 중인 crew가 로컬에서 빌드되었거나 Crew Studio를 통해 생성되어 있어야
|
||||
합니다.
|
||||
</Card>
|
||||
<Card title="GitHub 저장소" icon="github">
|
||||
crew 코드가 GitHub 저장소에 있어야 합니다(GitHub 연동 방식의 경우).
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 옵션 1: CrewAI CLI를 사용한 배포
|
||||
|
||||
CLI는 로컬에서 개발된 crew를 Enterprise 플랫폼에 가장 빠르게 배포할 수 있는 방법을 제공합니다.
|
||||
|
||||
<Steps>
|
||||
<Step title="CrewAI CLI 설치">
|
||||
아직 설치하지 않았다면 CrewAI CLI를 설치하세요:
|
||||
|
||||
```bash
|
||||
pip install crewai[tools]
|
||||
```
|
||||
|
||||
<Tip>
|
||||
CLI는 기본 CrewAI 패키지에 포함되어 있지만, `[tools]` 추가 옵션을 사용하면 모든 배포 종속성을 함께 설치할 수 있습니다.
|
||||
</Tip>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Enterprise 플랫폼에 인증">
|
||||
먼저, CrewAI AMP 플랫폼에 CLI를 인증해야 합니다:
|
||||
|
||||
```bash
|
||||
# 이미 CrewAI AMP 계정이 있거나 새로 생성하고 싶을 때:
|
||||
crewai login
|
||||
```
|
||||
|
||||
위 명령어를 실행하면 CLI가 다음을 진행합니다:
|
||||
1. URL과 고유 기기 코드를 표시합니다
|
||||
2. 브라우저를 열어 인증 페이지로 이동합니다
|
||||
3. 기기 확인을 요청합니다
|
||||
4. 인증 과정을 완료합니다
|
||||
|
||||
인증이 성공적으로 완료되면 터미널에 확인 메시지가 표시됩니다!
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="배포 생성">
|
||||
|
||||
프로젝트 디렉터리에서 다음 명령어를 실행하세요:
|
||||
|
||||
```bash
|
||||
crewai deploy create
|
||||
```
|
||||
|
||||
이 명령어는 다음을 수행합니다:
|
||||
1. GitHub 저장소 정보를 감지합니다
|
||||
2. 로컬 `.env` 파일의 환경 변수를 식별합니다
|
||||
3. 이러한 변수를 Enterprise 플랫폼으로 안전하게 전송합니다
|
||||
4. 고유 식별자가 부여된 새 배포를 만듭니다
|
||||
|
||||
성공적으로 생성되면 다음과 같은 메시지가 표시됩니다:
|
||||
```shell
|
||||
Deployment created successfully!
|
||||
Name: your_project_name
|
||||
Deployment ID: 01234567-89ab-cdef-0123-456789abcdef
|
||||
Current Status: Deploy Enqueued
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="배포 진행 상황 모니터링">
|
||||
|
||||
다음 명령어로 배포 상태를 추적할 수 있습니다:
|
||||
|
||||
```bash
|
||||
crewai deploy status
|
||||
```
|
||||
|
||||
빌드 과정의 상세 로그가 필요하다면:
|
||||
|
||||
```bash
|
||||
crewai deploy logs
|
||||
```
|
||||
|
||||
<Tip>
|
||||
첫 배포는 컨테이너 이미지를 빌드하므로 일반적으로 10~15분 정도 소요됩니다. 이후 배포는 훨씬 빠릅니다.
|
||||
</Tip>
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 추가 CLI 명령어
|
||||
|
||||
CrewAI CLI는 배포를 관리하기 위한 여러 명령어를 제공합니다:
|
||||
|
||||
```bash
|
||||
# 모든 배포 목록 확인
|
||||
crewai deploy list
|
||||
|
||||
# 배포 상태 확인
|
||||
crewai deploy status
|
||||
|
||||
# 배포 로그 보기
|
||||
crewai deploy logs
|
||||
|
||||
# 코드 변경 후 업데이트 푸시
|
||||
crewai deploy push
|
||||
|
||||
# 배포 삭제
|
||||
crewai deploy remove <deployment_id>
|
||||
```
|
||||
|
||||
## 옵션 2: 웹 인터페이스를 통한 직접 배포
|
||||
|
||||
GitHub 계정을 연결하여 CrewAI AMP 웹 인터페이스를 통해 crews를 직접 배포할 수도 있습니다. 이 방법은 로컬 머신에서 CLI를 사용할 필요가 없습니다.
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="GitHub로 푸시하기">
|
||||
|
||||
crew를 GitHub 저장소에 푸시해야 합니다. 아직 crew를 만들지 않았다면, [이 튜토리얼](/ko/quickstart)을 따라할 수 있습니다.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="GitHub를 CrewAI AOP에 연결하기">
|
||||
|
||||
1. [CrewAI AMP](https://app.crewai.com)에 로그인합니다.
|
||||
2. "Connect GitHub" 버튼을 클릭합니다.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="저장소 선택하기">
|
||||
|
||||
GitHub 계정을 연결한 후 배포할 저장소를 선택할 수 있습니다:
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="환경 변수 설정하기">
|
||||
|
||||
배포 전에, LLM 제공업체 또는 기타 서비스에 연결할 환경 변수를 설정해야 합니다:
|
||||
|
||||
1. 변수를 개별적으로 또는 일괄적으로 추가할 수 있습니다.
|
||||
2. 환경 변수는 `KEY=VALUE` 형식(한 줄에 하나씩)으로 입력합니다.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Crew 배포하기">
|
||||
|
||||
1. "Deploy" 버튼을 클릭하여 배포 프로세스를 시작합니다.
|
||||
2. 진행 바를 통해 진행 상황을 모니터링할 수 있습니다.
|
||||
3. 첫 번째 배포에는 일반적으로 약 10-15분 정도 소요되며, 이후 배포는 더 빠릅니다.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
배포가 완료되면 다음을 확인할 수 있습니다:
|
||||
- crew의 고유 URL
|
||||
- crew API를 보호할 Bearer 토큰
|
||||
- 배포를 삭제해야 하는 경우 "Delete" 버튼
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## ⚠️ 환경 변수 보안 요구사항
|
||||
|
||||
<Warning>
|
||||
**중요**: CrewAI AOP는 환경 변수 이름에 대한 보안 제한이 있으며, 이를 따르지
|
||||
않을 경우 배포가 실패할 수 있습니다.
|
||||
</Warning>
|
||||
|
||||
### 차단된 환경 변수 패턴
|
||||
|
||||
보안상의 이유로, 다음과 같은 환경 변수 명명 패턴은 **자동으로 필터링**되며 배포에 문제가 발생할 수 있습니다:
|
||||
|
||||
**차단된 패턴:**
|
||||
|
||||
- `_TOKEN`으로 끝나는 변수 (예: `MY_API_TOKEN`)
|
||||
- `_PASSWORD`로 끝나는 변수 (예: `DB_PASSWORD`)
|
||||
- `_SECRET`로 끝나는 변수 (예: `API_SECRET`)
|
||||
- 특정 상황에서 `_KEY`로 끝나는 변수
|
||||
|
||||
**특정 차단 변수:**
|
||||
|
||||
- `GITHUB_USER`, `GITHUB_TOKEN`
|
||||
- `AWS_REGION`, `AWS_DEFAULT_REGION`
|
||||
- 다양한 내부 CrewAI 시스템 변수
|
||||
|
||||
### 허용된 예외
|
||||
|
||||
일부 변수는 차단된 패턴과 일치하더라도 명시적으로 허용됩니다:
|
||||
|
||||
- `AZURE_AD_TOKEN`
|
||||
- `AZURE_OPENAI_AD_TOKEN`
|
||||
- `ENTERPRISE_ACTION_TOKEN`
|
||||
- `CREWAI_ENTEPRISE_TOOLS_TOKEN`
|
||||
|
||||
### 네이밍 문제 해결 방법
|
||||
|
||||
환경 변수 제한으로 인해 배포가 실패하는 경우:
|
||||
|
||||
```bash
|
||||
# ❌ 이러한 이름은 배포 실패를 초래합니다
|
||||
OPENAI_TOKEN=sk-...
|
||||
DATABASE_PASSWORD=mypassword
|
||||
API_SECRET=secret123
|
||||
|
||||
# ✅ 대신 다음과 같은 네이밍 패턴을 사용하세요
|
||||
OPENAI_API_KEY=sk-...
|
||||
DATABASE_CREDENTIALS=mypassword
|
||||
API_CONFIG=secret123
|
||||
```
|
||||
|
||||
### 모범 사례
|
||||
|
||||
1. **표준 명명 규칙 사용**: `PROVIDER_TOKEN` 대신 `PROVIDER_API_KEY` 사용
|
||||
2. **먼저 로컬에서 테스트**: crew가 이름이 변경된 변수로 제대로 동작하는지 확인
|
||||
3. **코드 업데이트**: 이전 변수 이름을 참조하는 부분을 모두 변경
|
||||
4. **변경 내용 문서화**: 팀을 위해 이름이 변경된 변수를 기록
|
||||
|
||||
<Tip>
|
||||
배포 실패 시, 환경 변수 에러 메시지가 난해하다면 먼저 변수 이름이 이 패턴을
|
||||
따르는지 확인하세요.
|
||||
</Tip>
|
||||
|
||||
### 배포된 Crew와 상호작용하기
|
||||
|
||||
배포가 완료되면 다음을 통해 crew에 접근할 수 있습니다:
|
||||
|
||||
1. **REST API**: 플랫폼에서 아래의 주요 경로가 포함된 고유한 HTTPS 엔드포인트를 생성합니다:
|
||||
|
||||
- `/inputs`: 필요한 입력 파라미터 목록
|
||||
- `/kickoff`: 제공된 입력값으로 실행 시작
|
||||
- `/status/{kickoff_id}`: 실행 상태 확인
|
||||
|
||||
2. **웹 인터페이스**: [app.crewai.com](https://app.crewai.com)에 방문하여 다음을 확인할 수 있습니다:
|
||||
- **Status 탭**: 배포 정보, API 엔드포인트 세부 정보 및 인증 토큰 확인
|
||||
- **Run 탭**: crew 구조의 시각적 표현
|
||||
- **Executions 탭**: 모든 실행 내역
|
||||
- **Metrics 탭**: 성능 분석
|
||||
- **Traces 탭**: 상세 실행 인사이트
|
||||
|
||||
### 실행 트리거하기
|
||||
|
||||
Enterprise 대시보드에서 다음 작업을 수행할 수 있습니다:
|
||||
|
||||
1. crew 이름을 클릭하여 상세 정보를 엽니다
|
||||
2. 관리 인터페이스에서 "Trigger Crew"를 선택합니다
|
||||
3. 나타나는 모달에 필요한 입력값을 입력합니다
|
||||
4. 파이프라인을 따라 실행의 진행 상황을 모니터링합니다
|
||||
|
||||
### 모니터링 및 분석
|
||||
|
||||
Enterprise 플랫폼은 포괄적인 가시성 기능을 제공합니다:
|
||||
|
||||
- **실행 관리**: 활성 및 완료된 실행 추적
|
||||
- **트레이스**: 각 실행의 상세 분해
|
||||
- **메트릭**: 토큰 사용량, 실행 시간, 비용
|
||||
- **타임라인 보기**: 작업 시퀀스의 시각적 표현
|
||||
|
||||
### 고급 기능
|
||||
|
||||
Enterprise 플랫폼은 또한 다음을 제공합니다:
|
||||
|
||||
- **환경 변수 관리**: API 키를 안전하게 저장 및 관리
|
||||
- **LLM 연결**: 다양한 LLM 공급자와의 통합 구성
|
||||
- **Custom Tools Repository**: 도구 생성, 공유 및 설치
|
||||
- **Crew Studio**: 코드를 작성하지 않고 채팅 인터페이스를 통해 crew 빌드
|
||||
|
||||
<Card
|
||||
title="도움이 필요하신가요?"
|
||||
icon="headset"
|
||||
href="mailto:support@crewai.com"
|
||||
>
|
||||
Enterprise 플랫폼의 배포 문제 또는 문의 사항이 있으시면 지원팀에 연락해
|
||||
주십시오.
|
||||
</Card>
|
||||
438
docs/ko/enterprise/guides/deploy-to-amp.mdx
Normal file
@@ -0,0 +1,438 @@
|
||||
---
|
||||
title: "AMP에 배포하기"
|
||||
description: "Crew 또는 Flow를 CrewAI AMP에 배포하기"
|
||||
icon: "rocket"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Note>
|
||||
로컬에서 또는 Crew Studio를 통해 Crew나 Flow를 생성한 후, 다음 단계는 이를 CrewAI AMP
|
||||
플랫폼에 배포하는 것입니다. 본 가이드에서는 다양한 배포 방법을 다루며,
|
||||
여러분의 워크플로우에 가장 적합한 방식을 선택할 수 있도록 안내합니다.
|
||||
</Note>
|
||||
|
||||
## 사전 준비 사항
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="배포 준비가 완료된 프로젝트" icon="check-circle">
|
||||
로컬에서 성공적으로 실행되는 Crew 또는 Flow가 있어야 합니다.
|
||||
[배포 준비 가이드](/ko/enterprise/guides/prepare-for-deployment)를 따라 프로젝트 구조를 확인하세요.
|
||||
</Card>
|
||||
<Card title="GitHub 저장소" icon="github">
|
||||
코드가 GitHub 저장소에 있어야 합니다(GitHub 연동 방식의 경우).
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<Info>
|
||||
**Crews vs Flows**: 두 프로젝트 유형 모두 CrewAI AMP에서 "자동화"로 배포할 수 있습니다.
|
||||
배포 과정은 동일하지만, 프로젝트 구조가 다릅니다.
|
||||
자세한 내용은 [배포 준비하기](/ko/enterprise/guides/prepare-for-deployment)를 참조하세요.
|
||||
</Info>
|
||||
|
||||
## 옵션 1: CrewAI CLI를 사용한 배포
|
||||
|
||||
CLI는 로컬에서 개발된 Crew 또는 Flow를 AMP 플랫폼에 가장 빠르게 배포할 수 있는 방법을 제공합니다.
|
||||
CLI는 `pyproject.toml`에서 프로젝트 유형을 자동으로 감지하고 그에 맞게 빌드합니다.
|
||||
|
||||
<Steps>
|
||||
<Step title="CrewAI CLI 설치">
|
||||
아직 설치하지 않았다면 CrewAI CLI를 설치하세요:
|
||||
|
||||
```bash
|
||||
pip install crewai[tools]
|
||||
```
|
||||
|
||||
<Tip>
|
||||
CLI는 기본 CrewAI 패키지에 포함되어 있지만, `[tools]` 추가 옵션을 사용하면 모든 배포 종속성을 함께 설치할 수 있습니다.
|
||||
</Tip>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Enterprise 플랫폼에 인증">
|
||||
먼저, CrewAI AMP 플랫폼에 CLI를 인증해야 합니다:
|
||||
|
||||
```bash
|
||||
# 이미 CrewAI AMP 계정이 있거나 새로 생성하고 싶을 때:
|
||||
crewai login
|
||||
```
|
||||
|
||||
위 명령어를 실행하면 CLI가 다음을 진행합니다:
|
||||
1. URL과 고유 기기 코드를 표시합니다
|
||||
2. 브라우저를 열어 인증 페이지로 이동합니다
|
||||
3. 기기 확인을 요청합니다
|
||||
4. 인증 과정을 완료합니다
|
||||
|
||||
인증이 성공적으로 완료되면 터미널에 확인 메시지가 표시됩니다!
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="배포 생성">
|
||||
|
||||
프로젝트 디렉터리에서 다음 명령어를 실행하세요:
|
||||
|
||||
```bash
|
||||
crewai deploy create
|
||||
```
|
||||
|
||||
이 명령어는 다음을 수행합니다:
|
||||
1. GitHub 저장소 정보를 감지합니다
|
||||
2. 로컬 `.env` 파일의 환경 변수를 식별합니다
|
||||
3. 이러한 변수를 Enterprise 플랫폼으로 안전하게 전송합니다
|
||||
4. 고유 식별자가 부여된 새 배포를 만듭니다
|
||||
|
||||
성공적으로 생성되면 다음과 같은 메시지가 표시됩니다:
|
||||
```shell
|
||||
Deployment created successfully!
|
||||
Name: your_project_name
|
||||
Deployment ID: 01234567-89ab-cdef-0123-456789abcdef
|
||||
Current Status: Deploy Enqueued
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="배포 진행 상황 모니터링">
|
||||
|
||||
다음 명령어로 배포 상태를 추적할 수 있습니다:
|
||||
|
||||
```bash
|
||||
crewai deploy status
|
||||
```
|
||||
|
||||
빌드 과정의 상세 로그가 필요하다면:
|
||||
|
||||
```bash
|
||||
crewai deploy logs
|
||||
```
|
||||
|
||||
<Tip>
|
||||
첫 배포는 컨테이너 이미지를 빌드하므로 일반적으로 10~15분 정도 소요됩니다. 이후 배포는 훨씬 빠릅니다.
|
||||
</Tip>
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 추가 CLI 명령어
|
||||
|
||||
CrewAI CLI는 배포를 관리하기 위한 여러 명령어를 제공합니다:
|
||||
|
||||
```bash
|
||||
# 모든 배포 목록 확인
|
||||
crewai deploy list
|
||||
|
||||
# 배포 상태 확인
|
||||
crewai deploy status
|
||||
|
||||
# 배포 로그 보기
|
||||
crewai deploy logs
|
||||
|
||||
# 코드 변경 후 업데이트 푸시
|
||||
crewai deploy push
|
||||
|
||||
# 배포 삭제
|
||||
crewai deploy remove <deployment_id>
|
||||
```
|
||||
|
||||
## 옵션 2: 웹 인터페이스를 통한 직접 배포
|
||||
|
||||
GitHub 계정을 연결하여 CrewAI AMP 웹 인터페이스를 통해 Crew 또는 Flow를 직접 배포할 수도 있습니다. 이 방법은 로컬 머신에서 CLI를 사용할 필요가 없습니다. 플랫폼은 자동으로 프로젝트 유형을 감지하고 적절하게 빌드를 처리합니다.
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="GitHub로 푸시하기">
|
||||
|
||||
Crew를 GitHub 저장소에 푸시해야 합니다. 아직 Crew를 만들지 않았다면, [이 튜토리얼](/ko/quickstart)을 따라할 수 있습니다.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="GitHub를 CrewAI AMP에 연결하기">
|
||||
|
||||
1. [CrewAI AMP](https://app.crewai.com)에 로그인합니다.
|
||||
2. "Connect GitHub" 버튼을 클릭합니다.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="저장소 선택하기">
|
||||
|
||||
GitHub 계정을 연결한 후 배포할 저장소를 선택할 수 있습니다:
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="환경 변수 설정하기">
|
||||
|
||||
배포 전에, LLM 제공업체 또는 기타 서비스에 연결할 환경 변수를 설정해야 합니다:
|
||||
|
||||
1. 변수를 개별적으로 또는 일괄적으로 추가할 수 있습니다.
|
||||
2. 환경 변수는 `KEY=VALUE` 형식(한 줄에 하나씩)으로 입력합니다.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Crew 배포하기">
|
||||
|
||||
1. "Deploy" 버튼을 클릭하여 배포 프로세스를 시작합니다.
|
||||
2. 진행 바를 통해 진행 상황을 모니터링할 수 있습니다.
|
||||
3. 첫 번째 배포에는 일반적으로 약 10-15분 정도 소요되며, 이후 배포는 더 빠릅니다.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
배포가 완료되면 다음을 확인할 수 있습니다:
|
||||
- Crew의 고유 URL
|
||||
- Crew API를 보호할 Bearer 토큰
|
||||
- 배포를 삭제해야 하는 경우 "Delete" 버튼
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## 옵션 3: API를 통한 재배포 (CI/CD 통합)
|
||||
|
||||
CI/CD 파이프라인에서 자동화된 배포를 위해 CrewAI API를 사용하여 기존 crew의 재배포를 트리거할 수 있습니다. 이 방법은 GitHub Actions, Jenkins 또는 기타 자동화 워크플로우에 특히 유용합니다.
|
||||
|
||||
<Steps>
|
||||
<Step title="개인 액세스 토큰 발급">
|
||||
|
||||
CrewAI AMP 계정 설정에서 API 토큰을 생성합니다:
|
||||
|
||||
1. [app.crewai.com](https://app.crewai.com)으로 이동합니다
|
||||
2. **Settings** → **Account** → **Personal Access Token**을 클릭합니다
|
||||
3. 새 토큰을 생성하고 안전하게 복사합니다
|
||||
4. 이 토큰을 CI/CD 시스템의 시크릿으로 저장합니다
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Automation UUID 찾기">
|
||||
|
||||
배포된 crew의 고유 식별자를 찾습니다:
|
||||
|
||||
1. CrewAI AMP 대시보드에서 **Automations**로 이동합니다
|
||||
2. 기존 automation/crew를 선택합니다
|
||||
3. **Additional Details**를 클릭합니다
|
||||
4. **UUID**를 복사합니다 - 이것이 특정 crew 배포를 식별합니다
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="API를 통한 재배포 트리거">
|
||||
|
||||
Deploy API 엔드포인트를 사용하여 재배포를 트리거합니다:
|
||||
|
||||
```bash
|
||||
curl -i -X POST \
|
||||
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN" \
|
||||
https://app.crewai.com/crewai_plus/api/v1/crews/YOUR-AUTOMATION-UUID/deploy
|
||||
|
||||
# HTTP/2 200
|
||||
# content-type: application/json
|
||||
#
|
||||
# {
|
||||
# "uuid": "your-automation-uuid",
|
||||
# "status": "Deploy Enqueued",
|
||||
# "public_url": "https://your-crew-deployment.crewai.com",
|
||||
# "token": "your-bearer-token"
|
||||
# }
|
||||
```
|
||||
|
||||
<Info>
|
||||
Git에 연결되어 처음 생성된 automation의 경우, API가 재배포 전에 자동으로 저장소에서 최신 변경 사항을 가져옵니다.
|
||||
</Info>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="GitHub Actions 통합 예시">
|
||||
|
||||
더 복잡한 배포 트리거가 있는 GitHub Actions 워크플로우 예시입니다:
|
||||
|
||||
```yaml
|
||||
name: Deploy CrewAI Automation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
types: [ labeled ]
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
||||
(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy')) ||
|
||||
(github.event_name == 'release')
|
||||
steps:
|
||||
- name: Trigger CrewAI Redeployment
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer ${{ secrets.CREWAI_PAT }}" \
|
||||
https://app.crewai.com/crewai_plus/api/v1/crews/${{ secrets.CREWAI_AUTOMATION_UUID }}/deploy
|
||||
```
|
||||
|
||||
<Tip>
|
||||
`CREWAI_PAT`와 `CREWAI_AUTOMATION_UUID`를 저장소 시크릿으로 추가하세요. PR 배포의 경우 "deploy" 라벨을 추가하여 워크플로우를 트리거합니다.
|
||||
</Tip>
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## 배포된 Automation과 상호작용하기
|
||||
|
||||
배포가 완료되면 다음을 통해 crew에 접근할 수 있습니다:
|
||||
|
||||
1. **REST API**: 플랫폼에서 아래의 주요 경로가 포함된 고유한 HTTPS 엔드포인트를 생성합니다:
|
||||
|
||||
- `/inputs`: 필요한 입력 파라미터 목록
|
||||
- `/kickoff`: 제공된 입력값으로 실행 시작
|
||||
- `/status/{kickoff_id}`: 실행 상태 확인
|
||||
|
||||
2. **웹 인터페이스**: [app.crewai.com](https://app.crewai.com)에 방문하여 다음을 확인할 수 있습니다:
|
||||
- **Status 탭**: 배포 정보, API 엔드포인트 세부 정보 및 인증 토큰 확인
|
||||
- **Run 탭**: Crew 구조의 시각적 표현
|
||||
- **Executions 탭**: 모든 실행 내역
|
||||
- **Metrics 탭**: 성능 분석
|
||||
- **Traces 탭**: 상세 실행 인사이트
|
||||
|
||||
### 실행 트리거하기
|
||||
|
||||
Enterprise 대시보드에서 다음 작업을 수행할 수 있습니다:
|
||||
|
||||
1. Crew 이름을 클릭하여 상세 정보를 엽니다
|
||||
2. 관리 인터페이스에서 "Trigger Crew"를 선택합니다
|
||||
3. 나타나는 모달에 필요한 입력값을 입력합니다
|
||||
4. 파이프라인을 따라 실행의 진행 상황을 모니터링합니다
|
||||
|
||||
### 모니터링 및 분석
|
||||
|
||||
Enterprise 플랫폼은 포괄적인 가시성 기능을 제공합니다:
|
||||
|
||||
- **실행 관리**: 활성 및 완료된 실행 추적
|
||||
- **트레이스**: 각 실행의 상세 분해
|
||||
- **메트릭**: 토큰 사용량, 실행 시간, 비용
|
||||
- **타임라인 보기**: 작업 시퀀스의 시각적 표현
|
||||
|
||||
### 고급 기능
|
||||
|
||||
Enterprise 플랫폼은 또한 다음을 제공합니다:
|
||||
|
||||
- **환경 변수 관리**: API 키를 안전하게 저장 및 관리
|
||||
- **LLM 연결**: 다양한 LLM 공급자와의 통합 구성
|
||||
- **Custom Tools Repository**: 도구 생성, 공유 및 설치
|
||||
- **Crew Studio**: 코드를 작성하지 않고 채팅 인터페이스를 통해 crew 빌드
|
||||
|
||||
## 배포 실패 문제 해결
|
||||
|
||||
배포가 실패하면 다음과 같은 일반적인 문제를 확인하세요:
|
||||
|
||||
### 빌드 실패
|
||||
|
||||
#### uv.lock 파일 누락
|
||||
|
||||
**증상**: 의존성 해결 오류와 함께 빌드 초기에 실패
|
||||
|
||||
**해결책**: lock 파일을 생성하고 커밋합니다:
|
||||
|
||||
```bash
|
||||
uv lock
|
||||
git add uv.lock
|
||||
git commit -m "Add uv.lock for deployment"
|
||||
git push
|
||||
```
|
||||
|
||||
<Warning>
|
||||
`uv.lock` 파일은 모든 배포에 필수입니다. 이 파일이 없으면 플랫폼에서
|
||||
의존성을 안정적으로 설치할 수 없습니다.
|
||||
</Warning>
|
||||
|
||||
#### 잘못된 프로젝트 구조
|
||||
|
||||
**증상**: "Could not find entry point" 또는 "Module not found" 오류
|
||||
|
||||
**해결책**: 프로젝트가 예상 구조와 일치하는지 확인합니다:
|
||||
|
||||
- **Crews와 Flows 모두**: 진입점이 `src/project_name/main.py`에 있어야 합니다
|
||||
- **Crews**: 진입점으로 `run()` 함수 사용
|
||||
- **Flows**: 진입점으로 `kickoff()` 함수 사용
|
||||
|
||||
자세한 구조 다이어그램은 [배포 준비하기](/ko/enterprise/guides/prepare-for-deployment)를 참조하세요.
|
||||
|
||||
#### CrewBase 데코레이터 누락
|
||||
|
||||
**증상**: "Crew not found", "Config not found" 또는 agent/task 구성 오류
|
||||
|
||||
**해결책**: **모든** crew 클래스가 `@CrewBase` 데코레이터를 사용하는지 확인합니다:
|
||||
|
||||
```python
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
|
||||
@CrewBase # 이 데코레이터는 필수입니다
|
||||
class YourCrew():
|
||||
"""Crew 설명"""
|
||||
|
||||
@agent
|
||||
def my_agent(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['my_agent'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# ... 나머지 crew 정의
|
||||
```
|
||||
|
||||
<Info>
|
||||
이것은 독립 실행형 Crews와 Flow 프로젝트 내에 포함된 crews 모두에 적용됩니다.
|
||||
모든 crew 클래스에 데코레이터가 필요합니다.
|
||||
</Info>
|
||||
|
||||
#### 잘못된 pyproject.toml 타입
|
||||
|
||||
**증상**: 빌드는 성공하지만 런타임에서 실패하거나 예상치 못한 동작
|
||||
|
||||
**해결책**: `[tool.crewai]` 섹션이 프로젝트 유형과 일치하는지 확인합니다:
|
||||
|
||||
```toml
|
||||
# Crew 프로젝트의 경우:
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
|
||||
# Flow 프로젝트의 경우:
|
||||
[tool.crewai]
|
||||
type = "flow"
|
||||
```
|
||||
|
||||
### 런타임 실패
|
||||
|
||||
#### LLM 연결 실패
|
||||
|
||||
**증상**: API 키 오류, "model not found" 또는 인증 실패
|
||||
|
||||
**해결책**:
|
||||
1. LLM 제공업체의 API 키가 환경 변수에 올바르게 설정되어 있는지 확인합니다
|
||||
2. 환경 변수 이름이 코드에서 예상하는 것과 일치하는지 확인합니다
|
||||
3. 배포 전에 동일한 환경 변수로 로컬에서 테스트합니다
|
||||
|
||||
#### Crew 실행 오류
|
||||
|
||||
**증상**: Crew가 시작되지만 실행 중에 실패
|
||||
|
||||
**해결책**:
|
||||
1. AMP 대시보드에서 실행 로그를 확인합니다 (Traces 탭)
|
||||
2. 모든 도구에 필요한 API 키가 구성되어 있는지 확인합니다
|
||||
3. `agents.yaml`의 agent 구성이 유효한지 확인합니다
|
||||
4. `tasks.yaml`의 task 구성에 구문 오류가 없는지 확인합니다
|
||||
|
||||
<Card title="도움이 필요하신가요?" icon="headset" href="mailto:support@crewai.com">
|
||||
배포 문제 또는 AMP 플랫폼에 대한 문의 사항이 있으시면 지원팀에 연락해 주세요.
|
||||
</Card>
|
||||
305
docs/ko/enterprise/guides/prepare-for-deployment.mdx
Normal file
@@ -0,0 +1,305 @@
|
||||
---
|
||||
title: "배포 준비하기"
|
||||
description: "Crew 또는 Flow가 CrewAI AMP에 배포될 준비가 되었는지 확인하기"
|
||||
icon: "clipboard-check"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Note>
|
||||
CrewAI AMP에 배포하기 전에, 프로젝트가 올바르게 구성되어 있는지 확인하는 것이 중요합니다.
|
||||
Crews와 Flows 모두 "자동화"로 배포할 수 있지만, 성공적인 배포를 위해 충족해야 하는
|
||||
서로 다른 프로젝트 구조와 요구 사항이 있습니다.
|
||||
</Note>
|
||||
|
||||
## 자동화 이해하기
|
||||
|
||||
CrewAI AMP에서 **자동화(automations)**는 배포 가능한 Agentic AI 프로젝트의 총칭입니다. 자동화는 다음 중 하나일 수 있습니다:
|
||||
|
||||
- **Crew**: 작업을 함께 수행하는 AI 에이전트들의 독립 실행형 팀
|
||||
- **Flow**: 여러 crew, 직접 LLM 호출 및 절차적 로직을 결합할 수 있는 오케스트레이션된 워크플로우
|
||||
|
||||
배포하는 유형을 이해하는 것은 프로젝트 구조와 진입점이 다르기 때문에 필수적입니다.
|
||||
|
||||
## Crews vs Flows: 주요 차이점
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew 프로젝트" icon="users">
|
||||
에이전트와 작업을 정의하는 `crew.py`가 있는 독립 실행형 AI 에이전트 팀. 집중적이고 협업적인 작업에 적합합니다.
|
||||
</Card>
|
||||
<Card title="Flow 프로젝트" icon="diagram-project">
|
||||
`crews/` 폴더에 포함된 crew가 있는 오케스트레이션된 워크플로우. 복잡한 다단계 프로세스에 적합합니다.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
| 측면 | Crew | Flow |
|
||||
|------|------|------|
|
||||
| **프로젝트 구조** | `crew.py`가 있는 `src/project_name/` | `crews/` 폴더가 있는 `src/project_name/` |
|
||||
| **메인 로직 위치** | `src/project_name/crew.py` | `src/project_name/main.py` (Flow 클래스) |
|
||||
| **진입점 함수** | `main.py`의 `run()` | `main.py`의 `kickoff()` |
|
||||
| **pyproject.toml 타입** | `type = "crew"` | `type = "flow"` |
|
||||
| **CLI 생성 명령어** | `crewai create crew name` | `crewai create flow name` |
|
||||
| **설정 위치** | `src/project_name/config/` | `src/project_name/crews/crew_name/config/` |
|
||||
| **다른 crew 포함 가능** | 아니오 | 예 (`crews/` 폴더 내) |
|
||||
|
||||
## 프로젝트 구조 참조
|
||||
|
||||
### Crew 프로젝트 구조
|
||||
|
||||
`crewai create crew my_crew`를 실행하면 다음 구조를 얻습니다:
|
||||
|
||||
```
|
||||
my_crew/
|
||||
├── .gitignore
|
||||
├── pyproject.toml # type = "crew"여야 함
|
||||
├── README.md
|
||||
├── .env
|
||||
├── uv.lock # 배포에 필수
|
||||
└── src/
|
||||
└── my_crew/
|
||||
├── __init__.py
|
||||
├── main.py # run() 함수가 있는 진입점
|
||||
├── crew.py # @CrewBase 데코레이터가 있는 Crew 클래스
|
||||
├── tools/
|
||||
│ ├── custom_tool.py
|
||||
│ └── __init__.py
|
||||
└── config/
|
||||
├── agents.yaml # 에이전트 정의
|
||||
└── tasks.yaml # 작업 정의
|
||||
```
|
||||
|
||||
<Warning>
|
||||
중첩된 `src/project_name/` 구조는 Crews에 매우 중요합니다.
|
||||
잘못된 레벨에 파일을 배치하면 배포 실패의 원인이 됩니다.
|
||||
</Warning>
|
||||
|
||||
### Flow 프로젝트 구조
|
||||
|
||||
`crewai create flow my_flow`를 실행하면 다음 구조를 얻습니다:
|
||||
|
||||
```
|
||||
my_flow/
|
||||
├── .gitignore
|
||||
├── pyproject.toml # type = "flow"여야 함
|
||||
├── README.md
|
||||
├── .env
|
||||
├── uv.lock # 배포에 필수
|
||||
└── src/
|
||||
└── my_flow/
|
||||
├── __init__.py
|
||||
├── main.py # kickoff() 함수 + Flow 클래스가 있는 진입점
|
||||
├── crews/ # 포함된 crews 폴더
|
||||
│ └── poem_crew/
|
||||
│ ├── __init__.py
|
||||
│ ├── poem_crew.py # @CrewBase 데코레이터가 있는 Crew
|
||||
│ └── config/
|
||||
│ ├── agents.yaml
|
||||
│ └── tasks.yaml
|
||||
└── tools/
|
||||
├── __init__.py
|
||||
└── custom_tool.py
|
||||
```
|
||||
|
||||
<Info>
|
||||
Crews와 Flows 모두 `src/project_name/` 구조를 사용합니다.
|
||||
핵심 차이점은 Flows는 포함된 crews를 위한 `crews/` 폴더가 있고,
|
||||
Crews는 프로젝트 폴더에 직접 `crew.py`가 있다는 것입니다.
|
||||
</Info>
|
||||
|
||||
## 배포 전 체크리스트
|
||||
|
||||
이 체크리스트를 사용하여 프로젝트가 배포 준비가 되었는지 확인하세요.
|
||||
|
||||
### 1. pyproject.toml 설정 확인
|
||||
|
||||
`pyproject.toml`에 올바른 `[tool.crewai]` 섹션이 포함되어야 합니다:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Crews의 경우">
|
||||
```toml
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Flows의 경우">
|
||||
```toml
|
||||
[tool.crewai]
|
||||
type = "flow"
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Warning>
|
||||
`type`이 프로젝트 구조와 일치하지 않으면 빌드가 실패하거나
|
||||
자동화가 올바르게 실행되지 않습니다.
|
||||
</Warning>
|
||||
|
||||
### 2. uv.lock 파일 존재 확인
|
||||
|
||||
CrewAI는 의존성 관리를 위해 `uv`를 사용합니다. `uv.lock` 파일은 재현 가능한 빌드를 보장하며 배포에 **필수**입니다.
|
||||
|
||||
```bash
|
||||
# lock 파일 생성 또는 업데이트
|
||||
uv lock
|
||||
|
||||
# 존재 여부 확인
|
||||
ls -la uv.lock
|
||||
```
|
||||
|
||||
파일이 존재하지 않으면 `uv lock`을 실행하고 저장소에 커밋하세요:
|
||||
|
||||
```bash
|
||||
uv lock
|
||||
git add uv.lock
|
||||
git commit -m "Add uv.lock for deployment"
|
||||
git push
|
||||
```
|
||||
|
||||
### 3. CrewBase 데코레이터 사용 확인
|
||||
|
||||
**모든 crew 클래스는 `@CrewBase` 데코레이터를 사용해야 합니다.** 이것은 다음에 적용됩니다:
|
||||
|
||||
- 독립 실행형 crew 프로젝트
|
||||
- Flow 프로젝트 내에 포함된 crews
|
||||
|
||||
```python
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
|
||||
@CrewBase # 이 데코레이터는 필수입니다
|
||||
class MyCrew():
|
||||
"""내 crew 설명"""
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
@agent
|
||||
def my_agent(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['my_agent'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@task
|
||||
def my_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['my_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
|
||||
<Warning>
|
||||
`@CrewBase` 데코레이터를 잊으면 에이전트나 작업 구성이 누락되었다는
|
||||
오류와 함께 배포가 실패합니다.
|
||||
</Warning>
|
||||
|
||||
### 4. 프로젝트 진입점 확인
|
||||
|
||||
Crews와 Flows 모두 `src/project_name/main.py`에 진입점이 있습니다:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Crews의 경우">
|
||||
진입점은 `run()` 함수를 사용합니다:
|
||||
|
||||
```python
|
||||
# src/my_crew/main.py
|
||||
from my_crew.crew import MyCrew
|
||||
|
||||
def run():
|
||||
"""crew를 실행합니다."""
|
||||
inputs = {'topic': 'AI in Healthcare'}
|
||||
result = MyCrew().crew().kickoff(inputs=inputs)
|
||||
return result
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Flows의 경우">
|
||||
진입점은 Flow 클래스와 함께 `kickoff()` 함수를 사용합니다:
|
||||
|
||||
```python
|
||||
# src/my_flow/main.py
|
||||
from crewai.flow import Flow, listen, start
|
||||
from my_flow.crews.poem_crew.poem_crew import PoemCrew
|
||||
|
||||
class MyFlow(Flow):
|
||||
@start()
|
||||
def begin(self):
|
||||
# Flow 로직
|
||||
result = PoemCrew().crew().kickoff(inputs={...})
|
||||
return result
|
||||
|
||||
def kickoff():
|
||||
"""flow를 실행합니다."""
|
||||
MyFlow().kickoff()
|
||||
|
||||
if __name__ == "__main__":
|
||||
kickoff()
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### 5. 환경 변수 준비
|
||||
|
||||
배포 전에 다음을 준비해야 합니다:
|
||||
|
||||
1. **LLM API 키** (OpenAI, Anthropic, Google 등)
|
||||
2. **도구 API 키** - 외부 도구를 사용하는 경우 (Serper 등)
|
||||
|
||||
<Tip>
|
||||
구성 문제를 조기에 발견하기 위해 배포 전에 동일한 환경 변수로
|
||||
로컬에서 프로젝트를 테스트하세요.
|
||||
</Tip>
|
||||
|
||||
## 빠른 검증 명령어
|
||||
|
||||
프로젝트 루트에서 다음 명령어를 실행하여 설정을 빠르게 확인하세요:
|
||||
|
||||
```bash
|
||||
# 1. pyproject.toml에서 프로젝트 타입 확인
|
||||
grep -A2 "\[tool.crewai\]" pyproject.toml
|
||||
|
||||
# 2. uv.lock 존재 확인
|
||||
ls -la uv.lock || echo "오류: uv.lock이 없습니다! 'uv lock'을 실행하세요"
|
||||
|
||||
# 3. src/ 구조 존재 확인
|
||||
ls -la src/*/main.py 2>/dev/null || echo "src/에서 main.py를 찾을 수 없습니다"
|
||||
|
||||
# 4. Crews의 경우 - crew.py 존재 확인
|
||||
ls -la src/*/crew.py 2>/dev/null || echo "crew.py가 없습니다 (Crews에서 예상됨)"
|
||||
|
||||
# 5. Flows의 경우 - crews/ 폴더 존재 확인
|
||||
ls -la src/*/crews/ 2>/dev/null || echo "crews/ 폴더가 없습니다 (Flows에서 예상됨)"
|
||||
|
||||
# 6. CrewBase 사용 확인
|
||||
grep -r "@CrewBase" . --include="*.py"
|
||||
```
|
||||
|
||||
## 일반적인 설정 실수
|
||||
|
||||
| 실수 | 증상 | 해결 방법 |
|
||||
|------|------|----------|
|
||||
| `uv.lock` 누락 | 의존성 해결 중 빌드 실패 | `uv lock` 실행 후 커밋 |
|
||||
| pyproject.toml의 잘못된 `type` | 빌드 성공하지만 런타임 실패 | 올바른 타입으로 변경 |
|
||||
| `@CrewBase` 데코레이터 누락 | "Config not found" 오류 | 모든 crew 클래스에 데코레이터 추가 |
|
||||
| `src/` 대신 루트에 파일 배치 | 진입점을 찾을 수 없음 | `src/project_name/`으로 이동 |
|
||||
| `run()` 또는 `kickoff()` 누락 | 자동화를 시작할 수 없음 | 올바른 진입 함수 추가 |
|
||||
|
||||
## 다음 단계
|
||||
|
||||
프로젝트가 모든 체크리스트 항목을 통과하면 배포할 준비가 된 것입니다:
|
||||
|
||||
<Card title="AMP에 배포하기" icon="rocket" href="/ko/enterprise/guides/deploy-to-amp">
|
||||
CLI, 웹 인터페이스 또는 CI/CD 통합을 사용하여 Crew 또는 Flow를 CrewAI AMP에
|
||||
배포하려면 배포 가이드를 따르세요.
|
||||
</Card>
|
||||
@@ -79,7 +79,7 @@ CrewAI AOP는 오픈 소스 프레임워크의 강력함에 프로덕션 배포,
|
||||
<Card
|
||||
title="Crew 배포"
|
||||
icon="rocket"
|
||||
href="/ko/enterprise/guides/deploy-crew"
|
||||
href="/ko/enterprise/guides/deploy-to-amp"
|
||||
>
|
||||
Crew 배포
|
||||
</Card>
|
||||
@@ -96,4 +96,4 @@ CrewAI AOP는 오픈 소스 프레임워크의 강력함에 프로덕션 배포,
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
자세한 안내를 원하시면 [배포 가이드](/ko/enterprise/guides/deploy-crew)를 확인하거나 아래 버튼을 클릭해 시작하세요.
|
||||
자세한 안내를 원하시면 [배포 가이드](/ko/enterprise/guides/deploy-to-amp)를 확인하거나 아래 버튼을 클릭해 시작하세요.
|
||||
|
||||
@@ -7,6 +7,10 @@ mode: "wide"
|
||||
|
||||
## 개요
|
||||
|
||||
<Note>
|
||||
`@human_feedback` 데코레이터는 **CrewAI 버전 1.8.0 이상**이 필요합니다. 이 기능을 사용하기 전에 설치를 업데이트하세요.
|
||||
</Note>
|
||||
|
||||
`@human_feedback` 데코레이터는 CrewAI Flow 내에서 직접 human-in-the-loop(HITL) 워크플로우를 가능하게 합니다. Flow 실행을 일시 중지하고, 인간에게 검토를 위해 출력을 제시하고, 피드백을 수집하고, 선택적으로 피드백 결과에 따라 다른 리스너로 라우팅할 수 있습니다.
|
||||
|
||||
이는 특히 다음과 같은 경우에 유용합니다:
|
||||
|
||||
@@ -5,9 +5,22 @@ icon: "user-check"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
휴먼 인 더 루프(HITL, Human-in-the-Loop)는 인공지능과 인간의 전문 지식을 결합하여 의사결정을 강화하고 작업 결과를 향상시키는 강력한 접근 방식입니다. 이 가이드에서는 CrewAI 내에서 HITL을 구현하는 방법을 안내합니다.
|
||||
휴먼 인 더 루프(HITL, Human-in-the-Loop)는 인공지능과 인간의 전문 지식을 결합하여 의사결정을 강화하고 작업 결과를 향상시키는 강력한 접근 방식입니다. CrewAI는 필요에 따라 HITL을 구현하는 여러 가지 방법을 제공합니다.
|
||||
|
||||
## HITL 워크플로우 설정
|
||||
## HITL 접근 방식 선택
|
||||
|
||||
CrewAI는 human-in-the-loop 워크플로우를 구현하기 위한 두 가지 주요 접근 방식을 제공합니다:
|
||||
|
||||
| 접근 방식 | 적합한 용도 | 통합 | 버전 |
|
||||
|----------|----------|-------------|---------|
|
||||
| **Flow 기반** (`@human_feedback` 데코레이터) | 로컬 개발, 콘솔 기반 검토, 동기식 워크플로우 | [Flow에서 인간 피드백](/ko/learn/human-feedback-in-flows) | **1.8.0+** |
|
||||
| **Webhook 기반** (Enterprise) | 프로덕션 배포, 비동기 워크플로우, 외부 통합 (Slack, Teams 등) | 이 가이드 | - |
|
||||
|
||||
<Tip>
|
||||
Flow를 구축하면서 피드백을 기반으로 라우팅하는 인간 검토 단계를 추가하려면 `@human_feedback` 데코레이터에 대한 [Flow에서 인간 피드백](/ko/learn/human-feedback-in-flows) 가이드를 참조하세요.
|
||||
</Tip>
|
||||
|
||||
## Webhook 기반 HITL 워크플로우 설정
|
||||
|
||||
<Steps>
|
||||
<Step title="작업 구성">
|
||||
|
||||
115
docs/ko/observability/galileo.mdx
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: Galileo 갈릴레오
|
||||
description: CrewAI 추적 및 평가를 위한 Galileo 통합
|
||||
icon: telescope
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
이 가이드는 **Galileo**를 **CrewAI**와 통합하는 방법을 보여줍니다.
|
||||
포괄적인 추적 및 평가 엔지니어링을 위한 것입니다.
|
||||
이 가이드가 끝나면 CrewAI 에이전트를 추적할 수 있게 됩니다.
|
||||
성과를 모니터링하고 행동을 평가합니다.
|
||||
Galileo의 강력한 관측 플랫폼.
|
||||
|
||||
> **갈릴레오(Galileo)란 무엇인가요?**[Galileo](https://galileo.ai/)는 AI 평가 및 관찰 가능성입니다.
|
||||
엔드투엔드 추적, 평가,
|
||||
AI 애플리케이션 모니터링. 이를 통해 팀은 실제 사실을 포착할 수 있습니다.
|
||||
견고한 가드레일을 만들고 체계적인 실험을 실행하세요.
|
||||
내장된 실험 추적 및 성능 분석으로 신뢰성 보장
|
||||
AI 수명주기 전반에 걸쳐 투명성과 지속적인 개선을 제공합니다.
|
||||
|
||||
## 시작하기
|
||||
|
||||
이 튜토리얼은 [CrewAI 빠른 시작](/ko/quickstart.mdx)을 따르며 추가하는 방법을 보여줍니다.
|
||||
갈릴레오의 [CrewAIEventListener](https://v2docs.galileo.ai/sdk-api/python/reference/handlers/crewai/handler),
|
||||
이벤트 핸들러.
|
||||
자세한 내용은 갈릴레오 문서를 참고하세요.
|
||||
[CrewAI 애플리케이션에 Galileo 추가](https://v2docs.galileo.ai/how-to-guides/third-party-integrations/add-galileo-to-crewai/add-galileo-to-crewai)
|
||||
방법 안내.
|
||||
|
||||
> **참고**이 튜토리얼에서는 [CrewAI 빠른 시작](/ko/quickstart.mdx)을 완료했다고 가정합니다.
|
||||
완전한 포괄적인 예제를 원한다면 Galileo
|
||||
[CrewAI SDK 예제 저장소](https://github.com/rungalileo/sdk-examples/tree/main/python/agent/crew-ai).
|
||||
|
||||
### 1단계: 종속성 설치
|
||||
|
||||
앱에 필요한 종속성을 설치합니다.
|
||||
원하는 방법으로 가상 환경을 생성하고,
|
||||
그런 다음 다음을 사용하여 해당 환경 내에 종속성을 설치하십시오.
|
||||
선호하는 도구:
|
||||
|
||||
```bash
|
||||
uv add galileo
|
||||
```
|
||||
|
||||
### 2단계: [CrewAI 빠른 시작](/ko/quickstart.mdx)에서 .env 파일에 추가
|
||||
|
||||
```bash
|
||||
# Your Galileo API key
|
||||
GALILEO_API_KEY="your-galileo-api-key"
|
||||
|
||||
# Your Galileo project name
|
||||
GALILEO_PROJECT="your-galileo-project-name"
|
||||
|
||||
# The name of the Log stream you want to use for logging
|
||||
GALILEO_LOG_STREAM="your-galileo-log-stream "
|
||||
```
|
||||
|
||||
### 3단계: Galileo 이벤트 리스너 추가
|
||||
|
||||
Galileo로 로깅을 활성화하려면 `CrewAIEventListener`의 인스턴스를 생성해야 합니다.
|
||||
다음을 통해 Galileo CrewAI 핸들러 패키지를 가져옵니다.
|
||||
main.py 파일 상단에 다음 코드를 추가하세요.
|
||||
|
||||
```python
|
||||
from galileo.handlers.crewai.handler import CrewAIEventListener
|
||||
```
|
||||
|
||||
실행 함수 시작 시 이벤트 리스너를 생성합니다.
|
||||
|
||||
```python
|
||||
def run():
|
||||
# Create the event listener
|
||||
CrewAIEventListener()
|
||||
# The rest of your existing code goes here
|
||||
```
|
||||
|
||||
리스너 인스턴스를 생성하면 자동으로
|
||||
CrewAI에 등록되었습니다.
|
||||
|
||||
### 4단계: Crew Agent 실행
|
||||
|
||||
CrewAI CLI를 사용하여 Crew Agent를 실행하세요.
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
|
||||
### 5단계: Galileo에서 추적 보기
|
||||
|
||||
승무원 에이전트가 완료되면 흔적이 플러시되어 Galileo에 나타납니다.
|
||||
|
||||

|
||||
|
||||
## 갈릴레오 통합 이해
|
||||
|
||||
Galileo는 이벤트 리스너를 등록하여 CrewAI와 통합됩니다.
|
||||
승무원 실행 이벤트(예: 에이전트 작업, 도구 호출, 모델 응답)를 캡처합니다.
|
||||
관찰 가능성과 평가를 위해 이를 갈릴레오에 전달합니다.
|
||||
|
||||
### 이벤트 리스너 이해
|
||||
|
||||
`CrewAIEventListener()` 인스턴스를 생성하는 것이 전부입니다.
|
||||
CrewAI 실행을 위해 Galileo를 활성화하는 데 필요합니다. 인스턴스화되면 리스너는 다음을 수행합니다.
|
||||
|
||||
-CrewAI에 자동으로 등록됩니다.
|
||||
-환경 변수에서 Galileo 구성을 읽습니다.
|
||||
-모든 실행 데이터를 Galileo 프로젝트 및 다음에서 지정한 로그 스트림에 기록합니다.
|
||||
`GALILEO_PROJECT` 및 `GALILEO_LOG_STREAM`
|
||||
|
||||
추가 구성이나 코드 변경이 필요하지 않습니다.
|
||||
이 실행의 모든 데이터는 Galileo 프로젝트에 기록되며
|
||||
환경 구성에 따라 지정된 로그 스트림
|
||||
(예: GALILEO_PROJECT 및 GALILEO_LOG_STREAM)
|
||||
@@ -4,6 +4,545 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="08 jan 2026">
|
||||
## v1.8.0
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.8.0)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Adicionar cadeia async nativa para a2a
|
||||
- Adicionar mecanismos de atualização a2a (poll/stream/push) com handlers e config
|
||||
- Introduzir configuração global de fluxo para feedback human-in-the-loop
|
||||
- Adicionar eventos de chamada de ferramenta em streaming e corrigir rastreamento de ID do provedor
|
||||
- Introduzir arquitetura de Flows e Crews pronta para produção
|
||||
- Adicionar HITL para Flows
|
||||
- Melhorar EventListener e TraceCollectionListener para melhor tratamento de eventos
|
||||
|
||||
### Correções de Bugs
|
||||
- Tratar dependência a2a ausente como opcional
|
||||
- Corrigir busca de erro para polling de login WorkOS
|
||||
- Corrigir nome de trigger errado na documentação de exemplo
|
||||
|
||||
### Documentação
|
||||
- Atualizar documentação de webhook-streaming
|
||||
- Ajustar linguagem da documentação de AOP para AMP
|
||||
|
||||
### Contribuidores
|
||||
@Vidit-Ostwal, @greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @lucasgomide, @mplachta
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="19 dez 2025">
|
||||
## v1.7.2
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.7.2)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Correções de Bugs
|
||||
- Resolver problemas de conexão
|
||||
|
||||
### Documentação
|
||||
- Atualizar página de documentação api-reference/status
|
||||
|
||||
### Contribuidores
|
||||
@greysonlalonde, @heitorado, @lorenzejay, @lucasgomide
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="16 dez 2025">
|
||||
## v1.7.1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.7.1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Melhorias
|
||||
- Adicionar flag `--no-commit` ao comando bump
|
||||
- Usar schema JSON para serialização de argumentos de ferramenta
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir exibição de mensagem de erro da resposta quando login do repositório de ferramentas falha
|
||||
- Corrigir terminação graciosa de future ao executar tarefa assincronamente
|
||||
- Corrigir ordenação de tarefas adicionando índice
|
||||
- Corrigir verificações de compatibilidade de plataforma para sinais Windows
|
||||
- Corrigir timer do controlador RPM para evitar travamento do processo
|
||||
- Corrigir registro de uso de tokens e validar modelo de resposta em stream
|
||||
|
||||
### Documentação
|
||||
- Adicionar documentação traduzida para async
|
||||
- Adicionar documentação para API Deploy AOP
|
||||
- Adicionar documentação para o conector agent handler
|
||||
- Adicionar documentação sobre async nativo
|
||||
|
||||
### Contribuidores
|
||||
@Llamrei, @dragosmc, @gilfeig, @greysonlalonde, @heitorado, @lorenzejay, @mattatcha, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="09 dez 2025">
|
||||
## v1.7.0
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.7.0)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Adicionar kickoff de fluxo async
|
||||
- Adicionar suporte a crew async
|
||||
- Adicionar suporte a tarefa async
|
||||
- Adicionar suporte a conhecimento async
|
||||
- Adicionar suporte a memória async
|
||||
- Adicionar suporte async para ferramentas e executor de agente; melhorar tipagem e docs
|
||||
- Implementar API de extensões a2a e cache de cartão de agente async; corrigir propagação de tarefas e streaming
|
||||
- Adicionar suporte a ferramenta async nativa
|
||||
- Adicionar suporte a llm async
|
||||
- Criar tipos de eventos sys e handler
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir problema para garantir que nonetypes não sejam passados para otel
|
||||
- Corrigir deadlock em operações de arquivo do armazenamento de tokens
|
||||
- Corrigir para garantir que span otel seja fechado
|
||||
- Usar HuggingFaceEmbeddingFunction para embeddings, atualizar chaves e adicionar testes
|
||||
- Corrigir para garantir que supports_tools seja true para todos os modelos anthropic suportados
|
||||
- Garantir que hooks funcionem com fluxos de lite agents
|
||||
|
||||
### Contribuidores
|
||||
@greysonlalonde, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="29 nov 2025">
|
||||
## v1.6.1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.6.1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir chamada ChatCompletionsClient para garantir funcionamento adequado
|
||||
- Garantir que métodos async sejam executáveis para anotações
|
||||
- Corrigir parâmetros em RagTool.add, adicionar tipagem e testes
|
||||
- Remover parâmetro inválido do cliente SSE
|
||||
- Apagar configuração 'oauth2_extra' no comando 'crewai config reset'
|
||||
|
||||
### Refatoração
|
||||
- Aprimorar validação de modelo e inferência de provedor na classe LLM
|
||||
|
||||
### Contribuidores
|
||||
@Vidit-Ostwal, @greysonlalonde, @heitorado, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="25 nov 2025">
|
||||
## v1.6.0
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.6.0)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Adicionar suporte a resultado de streaming para fluxos e crews
|
||||
- Adicionar gemini-3-pro-preview
|
||||
- Suportar login CLI com Entra ID
|
||||
- Adicionar ferramenta Merge Agent Handler
|
||||
- Aprimorar gerenciamento de estado de eventos de fluxo
|
||||
|
||||
### Correções de Bugs
|
||||
- Garantir que caminho de persistência de armazenamento rag personalizado seja definido se passado
|
||||
- Garantir que retornos fuzzy sejam mais estritos e mostrem aviso de tipo
|
||||
- Re-adicionar parâmetro response_format do openai e adicionar teste
|
||||
- Corrigir configuração de embeddings da ferramenta rag
|
||||
- Garantir que painel de início de execução de fluxo não seja mostrado no plot
|
||||
|
||||
### Documentação
|
||||
- Atualizar referências de AMP para AOP na documentação
|
||||
- Atualizar AMP para AOP
|
||||
|
||||
### Contribuidores
|
||||
@Vidit-Ostwal, @gilfeig, @greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @markmcd
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="22 nov 2025">
|
||||
## v0.203.2
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/0.203.2)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
- Bump de versão hotfix de 0.203.1 para 0.203.2
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="16 nov 2025">
|
||||
## v1.5.0
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.5.0)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Adicionar flag de status de conclusão remota de confiança a2a
|
||||
- Buscar e armazenar mais dados sobre servidor de autorização Okta
|
||||
- Implementar hooks antes e depois de chamadas LLM no CrewAgentExecutor
|
||||
- Expor mensagens para TaskOutput e LiteAgentOutputs
|
||||
- Aprimorar descrição de schema do QdrantVectorSearchTool
|
||||
|
||||
### Correções de Bugs
|
||||
- Garantir que flags de instrumentação de rastreamento sejam aplicadas corretamente
|
||||
- Corrigir links de documentação de ferramentas personalizadas e adicionar ação de links quebrados do Mintlify
|
||||
|
||||
### Documentação
|
||||
- Aprimorar documentação de guardrail de tarefa com suporte a validação baseada em LLM
|
||||
|
||||
### Contribuidores
|
||||
@danielfsbarreto, @greysonlalonde, @heitorado, @lorenzejay, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="07 nov 2025">
|
||||
## v1.4.1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.4.1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir tratamento de iterações máximas do agente
|
||||
- Resolver problemas de roteamento para sintaxe de modelo LLM para provedores respeitados
|
||||
|
||||
### Contribuidores
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="07 nov 2025">
|
||||
## v1.4.0
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.4.0)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Adicionar suporte para rotas de plot não-AST
|
||||
- Implementar suporte de primeira classe para MCP
|
||||
- Adicionar dunder de validação Pydantic ao BaseInterceptor
|
||||
- Adicionar suporte para hooks de interceptor de mensagem LLM
|
||||
- Cache de prompts i18n para uso eficiente
|
||||
- Aprimorar QdrantVectorSearchTool
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir problemas para manter stopwords atualizadas
|
||||
- Resolver valores não pickleable no estado de fluxo
|
||||
- Garantir que lite agents corrijam curso em erros de validação
|
||||
- Corrigir hash de argumento de callback para garantir que cache funcione
|
||||
- Permitir adicionar conteúdo de fonte RAG de URLs válidas
|
||||
- Tornar seleção de nó de plot mais suave
|
||||
- Corrigir IDs de documento duplicados para conhecimento
|
||||
|
||||
### Refatoração
|
||||
- Melhorar tratamento de execução de ferramenta MCP com concurrent futures
|
||||
- Simplificar tratamento de fluxo, tipagem e logging; atualizar UI e testes
|
||||
- Refatorar gerenciamento de stop word para propriedade
|
||||
|
||||
### Documentação
|
||||
- Migrar embedder para embedding_model e exigir vectordb em documentação de ferramentas; adicionar exemplos de provedor (en/ko/pt-BR)
|
||||
|
||||
### Contribuidores
|
||||
@danielfsbarreto, @greysonlalonde, @lorenzejay, @lucasgomide, @tonykipkemboi
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="01 nov 2025">
|
||||
## v1.3.0
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.3.0)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Refatorar tratamento de fluxo, tipagem e logging
|
||||
- Aprimorar QdrantVectorSearchTool
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir ferramentas Firecrawl e adicionar testes
|
||||
- Refatorar use_stop_words para propriedade e adicionar verificação para stop words
|
||||
|
||||
### Documentação
|
||||
- Migrar embedder para embedding_model e exigir vectordb em documentação de ferramentas
|
||||
- Adicionar exemplos de provedor em Inglês, Coreano e Português
|
||||
|
||||
### Refatoração
|
||||
- Melhorar tratamento de fluxo e atualizações de UI
|
||||
|
||||
### Contribuidores
|
||||
@danielfsbarreto, @greysonlalonde, @lorenzejay, @lucasgomide, @tonykipkemboi
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="27 out 2025">
|
||||
## v1.2.1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.2.1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Adicionar suporte para integração Datadog
|
||||
- Suportar apps e mcps em liteagent
|
||||
|
||||
### Documentação
|
||||
- Descrever variável de ambiente obrigatória para chamar ferramentas Platform para cada integração
|
||||
- Adicionar documentação de integração Datadog
|
||||
|
||||
### Contribuidores
|
||||
@barieom, @lorenzejay, @lucasgomide, @sabrenner
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="24 out 2025">
|
||||
## v1.2.0
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.2.0)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Correções de Bugs
|
||||
- Atualizar modelo LLM padrão e melhorar logging de erros em utilitários LLM
|
||||
- Alterar diretório de visualização de fluxo e inspeção de método
|
||||
|
||||
### Removendo Não Utilizados
|
||||
- Remover aisuite
|
||||
|
||||
### Contribuidores
|
||||
@greysonlalonde, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="21 out 2025">
|
||||
## v1.1.0
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.1.0)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Aprimorar InternalInstructor para suportar múltiplos provedores LLM
|
||||
- Implementar base de plugin mypy
|
||||
- Melhorar QdrantVectorSearchTool
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir links de documentação de integração quebrados
|
||||
- Corrigir chamada de trace dupla e adicionar tipos
|
||||
- Fixar versões de template para mais recente
|
||||
|
||||
### Documentação
|
||||
- Atualizar detalhes e exemplos de integração LLM
|
||||
|
||||
### Refatoração
|
||||
- Melhorar tipagem do CrewBase
|
||||
|
||||
### Contribuidores
|
||||
@cwarre33, @danielfsbarreto, @greysonlalonde, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="20 out 2025">
|
||||
## v1.0.0
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Bump de versões para 1.0.0
|
||||
- Aprimorar tratamento de eventos de conhecimento e guardrail na classe Agent
|
||||
- Injetar credenciais do repositório de ferramentas no comando crewai run
|
||||
|
||||
### Correções de Bugs
|
||||
- Preservar estrutura de condição aninhada em decoradores Flow
|
||||
- Adicionar parâmetros de print padrão ao método Printer.print
|
||||
- Corrigir erros quando não há input() disponível
|
||||
- Adicionar margem de 10s ao decodificar JWT
|
||||
- Reverter agenda cron ruim
|
||||
- Corrigir agenda cron para executar a cada 5 dias em datas específicas
|
||||
- Usar PATH do sistema para binário Docker em vez de caminho hardcoded
|
||||
- Adicionar configuração CodeQL para excluir corretamente diretórios de template
|
||||
|
||||
### Documentação
|
||||
- Atualizar política de segurança para relatório de vulnerabilidade
|
||||
- Adicionar guia para capturar logs de telemetria no CrewAI AMP
|
||||
- Adicionar arquivos /resume ausentes
|
||||
- Esclarecer parâmetro de URL de webhook em workflows HITL
|
||||
|
||||
### Contribuidores
|
||||
@Vidit-Ostwal, @greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @lucasgomide, @mplachta, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="18 out 2025">
|
||||
## v1.0.0b3 (Pré-lançamento)
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0b3)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Aprimorar funcionalidade e validação de guardrail de tarefa
|
||||
- Melhorar suporte para importar SDK nativo
|
||||
- Adicionar testes nativos Azure
|
||||
- Aprimorar classe BedrockCompletion com funcionalidades avançadas
|
||||
- Aprimorar classe GeminiCompletion com suporte a parâmetro de cliente
|
||||
- Aprimorar classe AnthropicCompletion com parâmetros de cliente adicionais
|
||||
|
||||
### Correções de Bugs
|
||||
- Preservar estrutura de condição aninhada em decoradores Flow
|
||||
- Adicionar parâmetros de print padrão ao método Printer.print
|
||||
- Remover prints stdout e melhorar determinismo de teste
|
||||
|
||||
### Refatoração
|
||||
- Converter módulo de projeto para metaclasse com tipagem completa
|
||||
|
||||
### Contribuidores
|
||||
@greysonlalonde, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="16 out 2025">
|
||||
## v1.0.0b2 (Pré-lançamento)
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0b2)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Aprimorar classe OpenAICompletion com parâmetros de cliente adicionais
|
||||
- Melhorar segurança de thread do event bus e suporte async
|
||||
- Injetar credenciais do repositório de ferramentas no comando crewai run
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir problema onde ocorre erro se não houver input() disponível
|
||||
- Adicionar margem de 10s ao decodificar JWT
|
||||
- Corrigir cópia e adicionar verificação NOT_SPECIFIED em task.py
|
||||
|
||||
### Documentação
|
||||
- Garantir que CREWAI_PLATFORM_INTEGRATION_TOKEN seja mencionado na documentação
|
||||
- Atualizar documentação de triggers
|
||||
|
||||
### Contribuidores
|
||||
@Vidit-Ostwal, @greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @lucasgomide
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="14 out 2025">
|
||||
## v1.0.0b1 (Pré-lançamento)
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0b1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Aprimorar classe OpenAICompletion com parâmetros de cliente adicionais
|
||||
- Melhorar segurança de thread do event bus e suporte async
|
||||
- Implementar integração Bedrock LLM
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir problema com disponibilidade de input() ausente
|
||||
- Resolver erro de decodificação JWT adicionando margem de 10 segundos
|
||||
- Injetar credenciais do repositório de ferramentas no comando crewai run
|
||||
- Corrigir cópia e adicionar verificação NOT_SPECIFIED em task.py
|
||||
|
||||
### Documentação
|
||||
- Garantir que CREWAI_PLATFORM_INTEGRATION_TOKEN seja mencionado na documentação
|
||||
- Atualizar documentação de triggers
|
||||
|
||||
### Contribuidores
|
||||
@Vidit-Ostwal, @greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @lucasgomide
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="13 out 2025">
|
||||
## v0.203.1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/0.203.1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Melhorias e Correções do Núcleo
|
||||
- Corrigida injeção de credenciais do repositório de ferramentas no comando `crewai run`
|
||||
- Adicionada margem de 10 segundos ao decodificar JWTs para reduzir erros de validação de token
|
||||
- Corrigida (depois revertida) correção de agenda cron destinada a executar jobs a cada 5 dias em datas específicas
|
||||
|
||||
### Documentação e Guias
|
||||
- Atualizada política de segurança para esclarecer o processo de relatório de vulnerabilidade
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="09 out 2025">
|
||||
## v1.0.0a4 (Pré-lançamento)
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0a4)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Aprimorar tratamento de eventos de conhecimento e guardrail na classe Agent
|
||||
- Introduzir comandos de listagem e execução de trigger para desenvolvimento local
|
||||
- Atualizar documentação com nova abordagem para consumir Platform Actions
|
||||
- Adicionar guia para capturar logs de telemetria no CrewAI AMP
|
||||
|
||||
### Correções de Bugs
|
||||
- Reverter agenda cron ruim
|
||||
- Corrigir agenda cron para executar a cada 5 dias em datas específicas
|
||||
- Remover linha duplicada e adicionar variável de ambiente explícita
|
||||
|
||||
### Contribuidores
|
||||
@greysonlalonde, @heitorado, @joaomdmoura, @lorenzejay, @lucasgomide, @mplachta, @theCyberTech
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="07 out 2025">
|
||||
## v1.0.0a3 (Pré-lançamento)
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0a3)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Adicionar suporte a agente para ações de plataforma
|
||||
- Adicionar argumento de interpretador para ferramenta de execução de código
|
||||
- Suporte direto para execução de apps de plataforma
|
||||
|
||||
### Documentação
|
||||
- Adicionar documentação de ações de plataforma
|
||||
- Adicionar tipos de transporte stdio e sse à documentação MCP
|
||||
- Atualizar lista de modelos AWS
|
||||
|
||||
### Contribuidores
|
||||
@greysonlalonde, @heitorado, @lorenzejay, @lucasgomide
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="03 out 2025">
|
||||
## v1.0.0a2 (Pré-lançamento)
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.0.0a2)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Melhorias e Correções do Núcleo
|
||||
- Atualizações de CI para monorepo
|
||||
- Atualizar modelo Anthropic padrão para claude-sonnet-4-20250514
|
||||
- Corrigir testes para atualização de modelo
|
||||
|
||||
### Contribuidores
|
||||
@greysonlalonde, @lorenzejay
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="30 set 2025">
|
||||
## v1.0.0a1
|
||||
|
||||
|
||||
@@ -309,6 +309,10 @@ Ao executar esse Flow, a saída será diferente dependendo do valor booleano ale
|
||||
|
||||
### Human in the Loop (feedback humano)
|
||||
|
||||
<Note>
|
||||
O decorador `@human_feedback` requer **CrewAI versão 1.8.0 ou superior**.
|
||||
</Note>
|
||||
|
||||
O decorador `@human_feedback` permite fluxos de trabalho human-in-the-loop, pausando a execução do flow para coletar feedback de um humano. Isso é útil para portões de aprovação, revisão de qualidade e pontos de decisão que requerem julgamento humano.
|
||||
|
||||
```python Code
|
||||
|
||||
@@ -128,7 +128,7 @@ Ao implantar seu Flow, considere o seguinte:
|
||||
### CrewAI Enterprise
|
||||
A maneira mais fácil de implantar seu Flow é usando o CrewAI Enterprise. Ele lida com a infraestrutura, autenticação e monitoramento para você.
|
||||
|
||||
Confira o [Guia de Implantação](/pt-BR/enterprise/guides/deploy-crew) para começar.
|
||||
Confira o [Guia de Implantação](/pt-BR/enterprise/guides/deploy-to-amp) para começar.
|
||||
|
||||
```bash
|
||||
crewai deploy create
|
||||
|
||||
@@ -91,7 +91,7 @@ Após implantar, você pode ver os detalhes da automação e usar o menu **Optio
|
||||
## Relacionados
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Implantar um Crew" href="/pt-BR/enterprise/guides/deploy-crew" icon="rocket">
|
||||
<Card title="Implantar um Crew" href="/pt-BR/enterprise/guides/deploy-to-amp" icon="rocket">
|
||||
Implante um Crew via GitHub ou arquivo ZIP.
|
||||
</Card>
|
||||
<Card title="Gatilhos de Automação" href="/pt-BR/enterprise/guides/automation-triggers" icon="trigger">
|
||||
|
||||
@@ -79,7 +79,7 @@ Após publicar, você pode visualizar os detalhes da automação e usar o menu *
|
||||
<Card title="Criar um Crew" href="/pt-BR/enterprise/guides/build-crew" icon="paintbrush">
|
||||
Crie um Crew.
|
||||
</Card>
|
||||
<Card title="Implantar um Crew" href="/pt-BR/enterprise/guides/deploy-crew" icon="rocket">
|
||||
<Card title="Implantar um Crew" href="/pt-BR/enterprise/guides/deploy-to-amp" icon="rocket">
|
||||
Implante um Crew via GitHub ou ZIP.
|
||||
</Card>
|
||||
<Card title="Exportar um Componente React" href="/pt-BR/enterprise/guides/react-component-export" icon="download">
|
||||
|
||||
342
docs/pt-BR/enterprise/features/pii-trace-redactions.mdx
Normal file
@@ -0,0 +1,342 @@
|
||||
---
|
||||
title: Redação de PII para Traces
|
||||
description: "Redija automaticamente dados sensíveis de traces de execução de crews e flows"
|
||||
icon: "lock"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Visão Geral
|
||||
|
||||
A Redação de PII é um recurso do CrewAI AMP que detecta e mascara automaticamente Informações de Identificação Pessoal (PII) nos traces de execução de crews e flows. Isso garante que dados sensíveis como números de cartão de crédito, CPF, endereços de e-mail e nomes não sejam expostos nos traces do CrewAI AMP. Você também pode criar reconhecedores personalizados para proteger dados específicos da sua organização.
|
||||
|
||||
|
||||
<Info>
|
||||
A Redação de PII está disponível no plano Enterprise.
|
||||
A implantação deve ser versão 1.8.0 ou superior.
|
||||
</Info>
|
||||
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
|
||||
## Por Que a Redação de PII é Importante
|
||||
|
||||
Ao executar agentes de IA em produção, informações sensíveis frequentemente fluem através das suas crews:
|
||||
|
||||
- Dados de clientes de integrações CRM
|
||||
- Informações financeiras de processadores de pagamento
|
||||
- Detalhes pessoais de envios de formulários
|
||||
- Dados internos de funcionários
|
||||
|
||||
Sem a redação adequada, esses dados aparecem nos traces, tornando a conformidade com regulamentações como LGPD, HIPAA e PCI-DSS desafiadora. A Redação de PII resolve isso mascarando automaticamente dados sensíveis antes de serem armazenados nos traces.
|
||||
|
||||
## Como Funciona
|
||||
|
||||
1. **Detectar** - Escanear dados de eventos de trace para padrões de PII conhecidos
|
||||
2. **Classificar** - Identificar o tipo de dado sensível (cartão de crédito, CPF, e-mail, etc.)
|
||||
3. **Mascarar/Redigir** - Substituir os dados sensíveis por valores mascarados com base na sua configuração
|
||||
|
||||
```
|
||||
Original: "Entre em contato com john.doe@company.com ou ligue para 555-123-4567"
|
||||
Redigido: "Entre em contato com <EMAIL_ADDRESS> ou ligue para <PHONE_NUMBER>"
|
||||
```
|
||||
|
||||
## Habilitando a Redação de PII
|
||||
|
||||
<Info>
|
||||
Você deve estar no plano Enterprise e sua implantação deve ser versão 1.8.0 ou superior para usar este recurso.
|
||||
</Info>
|
||||
|
||||
<Steps>
|
||||
<Step title="Navegue até Configurações da Crew">
|
||||
No painel do CrewAI AMP, selecione sua crew implantada e vá para uma de suas implantações/automações, depois navegue até **Settings** → **PII Protection**.
|
||||
</Step>
|
||||
|
||||
<Step title="Habilitar Proteção PII">
|
||||
Ative **PII Redaction for Traces**. Isso habilitará a varredura automática e redação de dados de trace.
|
||||
|
||||
<Info>
|
||||
Você precisa habilitar manualmente a Redação de PII para cada implantação.
|
||||
</Info>
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Configurar Tipos de Entidade">
|
||||
Selecione quais tipos de PII detectar e redigir. Cada entidade pode ser habilitada ou desabilitada individualmente.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Salvar">
|
||||
Salve sua configuração. A redação de PII estará ativa em todas as execuções subsequentes da crew, sem necessidade de reimplantação.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Tipos de Entidade Suportados
|
||||
|
||||
O CrewAI suporta os seguintes tipos de entidade PII, organizados por categoria.
|
||||
|
||||
### Entidades Globais
|
||||
|
||||
| Entidade | Descrição | Exemplo |
|
||||
|----------|-----------|---------|
|
||||
| `CREDIT_CARD` | Números de cartão de crédito/débito | "4111-1111-1111-1111" |
|
||||
| `CRYPTO` | Endereços de carteira de criptomoedas | "bc1qxy2kgd..." |
|
||||
| `DATE_TIME` | Datas e horários | "15 de janeiro de 2024" |
|
||||
| `EMAIL_ADDRESS` | Endereços de e-mail | "john@example.com" |
|
||||
| `IBAN_CODE` | Números de conta bancária internacional | "DE89 3704 0044 0532 0130 00" |
|
||||
| `IP_ADDRESS` | Endereços IPv4 e IPv6 | "192.168.1.1" |
|
||||
| `LOCATION` | Localizações geográficas | "São Paulo" |
|
||||
| `MEDICAL_LICENSE` | Números de licença médica | "CRM12345" |
|
||||
| `NRP` | Nacionalidades, grupos religiosos ou políticos | - |
|
||||
| `PERSON` | Nomes pessoais | "João Silva" |
|
||||
| `PHONE_NUMBER` | Números de telefone em vários formatos | "+55 (11) 98765-4321" |
|
||||
| `URL` | URLs da web | "https://example.com" |
|
||||
|
||||
### Entidades Específicas dos EUA
|
||||
|
||||
| Entidade | Descrição | Exemplo |
|
||||
|----------|-----------|---------|
|
||||
| `US_BANK_NUMBER` | Números de conta bancária dos EUA | "1234567890" |
|
||||
| `US_DRIVER_LICENSE` | Números de carteira de motorista dos EUA | "D1234567" |
|
||||
| `US_ITIN` | Número de Identificação de Contribuinte Individual | "900-70-0000" |
|
||||
| `US_PASSPORT` | Números de passaporte dos EUA | "123456789" |
|
||||
| `US_SSN` | Números de Seguro Social | "123-45-6789" |
|
||||
|
||||
## Ações de Redação
|
||||
|
||||
Para cada entidade habilitada, você pode configurar como os dados são redigidos:
|
||||
|
||||
| Ação | Descrição | Exemplo de Saída |
|
||||
|------|-----------|------------------|
|
||||
| `mask` | Substituir pelo rótulo do tipo de entidade | `<CREDIT_CARD>` |
|
||||
| `redact` | Remover completamente o texto | *(vazio)* |
|
||||
|
||||
## Reconhecedores Personalizados
|
||||
|
||||
Além das entidades integradas, você pode criar **reconhecedores personalizados** para detectar padrões de PII específicos da sua organização.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
### Tipos de Reconhecedores
|
||||
|
||||
Você tem duas opções para reconhecedores personalizados:
|
||||
|
||||
| Tipo | Melhor Para | Exemplo de Caso de Uso |
|
||||
|------|-------------|------------------------|
|
||||
| **Baseado em Padrão (Regex)** | Dados estruturados com formatos previsíveis | Valores de salário, IDs de funcionários, códigos de projeto |
|
||||
| **Lista de Negação** | Correspondências exatas de strings | Nomes de empresas, codinomes internos, termos específicos |
|
||||
|
||||
### Criando um Reconhecedor Personalizado
|
||||
|
||||
<Steps>
|
||||
<Step title="Navegue até Reconhecedores Personalizados">
|
||||
Vá para **Settings** da Organização → **Organization** → **Add Recognizer**.
|
||||
</Step>
|
||||
|
||||
<Step title="Configure o Reconhecedor">
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Configure os seguintes campos:
|
||||
- **Name**: Um nome descritivo para o reconhecedor
|
||||
- **Entity Type**: O rótulo da entidade que aparecerá na saída redigida (ex.: `EMPLOYEE_ID`, `SALARY`)
|
||||
- **Type**: Escolha entre Padrão Regex ou Lista de Negação
|
||||
- **Pattern/Values**: Padrão regex ou lista de strings para corresponder
|
||||
- **Confidence Threshold**: Pontuação mínima (0.0-1.0) necessária para uma correspondência acionar a redação. Valores mais altos (ex.: 0.8) reduzem falsos positivos, mas podem perder algumas correspondências. Valores mais baixos (ex.: 0.5) capturam mais correspondências, mas podem redigir em excesso. O padrão é 0.8.
|
||||
- **Context Words** (opcional): Palavras que aumentam a confiança de detecção quando encontradas próximas
|
||||
</Step>
|
||||
|
||||
<Step title="Salvar">
|
||||
Salve o reconhecedor. Ele estará disponível para habilitar em suas implantações.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Entendendo os Tipos de Entidade
|
||||
|
||||
O **Entity Type** determina como o conteúdo correspondido aparece nos traces redigidos:
|
||||
|
||||
```
|
||||
Entity Type: SALARY
|
||||
Pattern: salary:\s*\$\s*\d+
|
||||
Entrada: "Salário do funcionário: $50,000"
|
||||
Saída: "Salário do funcionário <SALARY>"
|
||||
```
|
||||
|
||||
### Usando Palavras de Contexto
|
||||
|
||||
Palavras de contexto melhoram a precisão aumentando a confiança quando termos específicos aparecem próximos ao padrão correspondido:
|
||||
|
||||
```
|
||||
Context Words: "project", "code", "internal"
|
||||
Entity Type: PROJECT_CODE
|
||||
Pattern: PRJ-\d{4}
|
||||
```
|
||||
|
||||
Quando "project" ou "code" aparece próximo a "PRJ-1234", o reconhecedor tem maior confiança de que é uma correspondência verdadeira, reduzindo falsos positivos.
|
||||
|
||||
|
||||
## Visualizando Traces Redigidos
|
||||
|
||||
Uma vez que a redação de PII está habilitada, seus traces mostrarão valores redigidos no lugar de dados sensíveis:
|
||||
|
||||
```
|
||||
Task Output: "Cliente <PERSON> fez o pedido #12345.
|
||||
E-mail de contato: <EMAIL_ADDRESS>, telefone: <PHONE_NUMBER>.
|
||||
Pagamento processado para cartão terminando em <CREDIT_CARD>."
|
||||
```
|
||||
|
||||
Os valores redigidos são claramente marcados com colchetes angulares e o rótulo do tipo de entidade (ex.: `<EMAIL_ADDRESS>`), facilitando entender quais dados foram protegidos enquanto ainda permite depurar e monitorar o comportamento da crew.
|
||||
|
||||
|
||||
|
||||
## Melhores Práticas
|
||||
|
||||
### Considerações de Desempenho
|
||||
|
||||
<Steps>
|
||||
<Step title="Habilite Apenas Entidades Necessárias">
|
||||
Cada entidade habilitada adiciona sobrecarga de processamento. Habilite apenas entidades relevantes para seus dados.
|
||||
</Step>
|
||||
|
||||
<Step title="Use Padrões Específicos">
|
||||
Para reconhecedores personalizados, use padrões específicos para reduzir falsos positivos e melhorar o desempenho. Padrões regex são melhores para identificar padrões específicos nos traces como salário, ID de funcionário, código de projeto, etc. Reconhecedores de lista de negação são melhores para identificar strings exatas nos traces como nomes de empresas, codinomes internos, etc.
|
||||
</Step>
|
||||
|
||||
<Step title="Aproveite Palavras de Contexto">
|
||||
Palavras de contexto melhoram a precisão acionando a detecção apenas quando o texto circundante corresponde.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Solução de Problemas
|
||||
|
||||
<Accordion title="PII Não Está Sendo Redigido">
|
||||
**Possíveis Causas:**
|
||||
- Tipo de entidade não habilitado na configuração
|
||||
- Padrão não corresponde ao formato dos dados
|
||||
- Reconhecedor personalizado tem erros de sintaxe
|
||||
|
||||
**Soluções:**
|
||||
- Verifique se a entidade está habilitada em Settings → Security
|
||||
- Teste padrões regex com dados de amostra
|
||||
- Verifique logs para erros de configuração
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Muitos Dados Estão Sendo Redigidos">
|
||||
**Possíveis Causas:**
|
||||
- Tipos de entidade muito amplos habilitados (ex.: `DATE_TIME` captura datas em todos os lugares)
|
||||
- Padrões de reconhecedor personalizado são muito gerais
|
||||
|
||||
**Soluções:**
|
||||
- Desabilite entidades que causam falsos positivos
|
||||
- Torne padrões personalizados mais específicos
|
||||
- Adicione palavras de contexto para melhorar a precisão
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Problemas de Desempenho">
|
||||
**Possíveis Causas:**
|
||||
- Muitas entidades habilitadas
|
||||
- Entidades baseadas em NLP (`PERSON`, `LOCATION`, `NRP`) são computacionalmente caras pois usam modelos de machine learning
|
||||
|
||||
**Soluções:**
|
||||
- Habilite apenas entidades que você realmente precisa
|
||||
- Considere usar alternativas baseadas em padrão quando possível
|
||||
- Monitore tempos de processamento de trace no painel
|
||||
</Accordion>
|
||||
|
||||
---
|
||||
|
||||
## Exemplo Prático: Correspondência de Padrão de Salário
|
||||
|
||||
Este exemplo demonstra como criar um reconhecedor personalizado para detectar e mascarar informações de salário em seus traces.
|
||||
|
||||
### Caso de Uso
|
||||
|
||||
Sua crew processa dados de funcionários ou financeiros que incluem informações de salário em formatos como:
|
||||
- `salary: $50,000`
|
||||
- `salary: $125,000.00`
|
||||
- `salary:$1,500.50`
|
||||
|
||||
Você deseja mascarar automaticamente esses valores para proteger dados sensíveis de remuneração.
|
||||
|
||||
### Configuração
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **Name** | `SALARY` |
|
||||
| **Entity Type** | `SALARY` |
|
||||
| **Type** | Regex Pattern |
|
||||
| **Regex Pattern** | `salary:\s*\$\s*\d{1,3}(,\d{3})*(\.\d{2})?` |
|
||||
| **Action** | Mask |
|
||||
| **Confidence Threshold** | `0.8` |
|
||||
| **Context Words** | `salary, compensation, pay, wage, income` |
|
||||
|
||||
### Análise do Padrão Regex
|
||||
|
||||
| Componente do Padrão | Significado |
|
||||
|----------------------|-------------|
|
||||
| `salary:` | Corresponde ao texto literal "salary:" |
|
||||
| `\s*` | Corresponde a zero ou mais caracteres de espaço em branco |
|
||||
| `\$` | Corresponde ao sinal de dólar (escapado) |
|
||||
| `\s*` | Corresponde a zero ou mais caracteres de espaço em branco após $ |
|
||||
| `\d{1,3}` | Corresponde a 1-3 dígitos (ex.: "1", "50", "125") |
|
||||
| `(,\d{3})*` | Corresponde a milhares separados por vírgula (ex.: ",000", ",500,000") |
|
||||
| `(\.\d{2})?` | Opcionalmente corresponde a centavos (ex.: ".00", ".50") |
|
||||
|
||||
### Resultados de Exemplo
|
||||
|
||||
```
|
||||
Original: "Registro do funcionário mostra salary: $125,000.00 anualmente"
|
||||
Redigido: "Registro do funcionário mostra <SALARY> anualmente"
|
||||
|
||||
Original: "Salário base salary:$50,000 com potencial de bônus"
|
||||
Redigido: "Salário base <SALARY> com potencial de bônus"
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Adicionar palavras de contexto como "salary", "compensation", "pay", "wage" e "income" ajuda a aumentar a confiança de detecção quando esses termos aparecem próximos ao padrão correspondido, reduzindo falsos positivos.
|
||||
</Tip>
|
||||
|
||||
### Habilite o Reconhecedor para Suas Implantações
|
||||
|
||||
<Warning>
|
||||
Criar um reconhecedor personalizado no nível da organização não o habilita automaticamente para suas implantações. Você deve habilitar manualmente cada reconhecedor para cada implantação onde deseja aplicá-lo.
|
||||
</Warning>
|
||||
|
||||
Após criar seu reconhecedor personalizado, habilite-o para cada implantação:
|
||||
|
||||
<Steps>
|
||||
<Step title="Navegue até Sua Implantação">
|
||||
Vá para sua implantação/automação e abra **Settings** → **PII Protection**.
|
||||
</Step>
|
||||
|
||||
<Step title="Selecione Reconhecedores Personalizados">
|
||||
Em **Mask Recognizers**, você verá os reconhecedores definidos pela sua organização. Marque a caixa ao lado dos reconhecedores que deseja habilitar.
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Salvar Configuração">
|
||||
Salve suas alterações. O reconhecedor estará ativo em todas as execuções subsequentes para esta implantação.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Info>
|
||||
Repita este processo para cada implantação onde você precisa do reconhecedor personalizado. Isso oferece controle granular sobre quais reconhecedores estão ativos em diferentes ambientes (ex.: desenvolvimento vs. produção).
|
||||
</Info>
|
||||
@@ -1,304 +0,0 @@
|
||||
---
|
||||
title: "Deploy Crew"
|
||||
description: "Implantando um Crew na CrewAI AMP"
|
||||
icon: "rocket"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Depois de criar um crew localmente ou pelo Crew Studio, o próximo passo é
|
||||
implantá-lo na plataforma CrewAI AMP. Este guia cobre múltiplos métodos de
|
||||
implantação para ajudá-lo a escolher a melhor abordagem para o seu fluxo de
|
||||
trabalho.
|
||||
</Note>
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Crew Pronto para Implantação" icon="users">
|
||||
Você deve ter um crew funcional, criado localmente ou pelo Crew Studio
|
||||
</Card>
|
||||
<Card title="Repositório GitHub" icon="github">
|
||||
O código do seu crew deve estar em um repositório do GitHub (para o método
|
||||
de integração com GitHub)
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Opção 1: Implantar Usando o CrewAI CLI
|
||||
|
||||
A CLI fornece a maneira mais rápida de implantar crews desenvolvidos localmente na plataforma Enterprise.
|
||||
|
||||
<Steps>
|
||||
<Step title="Instale o CrewAI CLI">
|
||||
Se ainda não tiver, instale o CrewAI CLI:
|
||||
|
||||
```bash
|
||||
pip install crewai[tools]
|
||||
```
|
||||
|
||||
<Tip>
|
||||
A CLI vem com o pacote principal CrewAI, mas o extra `[tools]` garante todas as dependências de implantação.
|
||||
</Tip>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Autentique-se na Plataforma Enterprise">
|
||||
Primeiro, você precisa autenticar sua CLI com a plataforma CrewAI AMP:
|
||||
|
||||
```bash
|
||||
# Se já possui uma conta CrewAI AMP, ou deseja criar uma:
|
||||
crewai login
|
||||
```
|
||||
|
||||
Ao executar qualquer um dos comandos, a CLI irá:
|
||||
1. Exibir uma URL e um código de dispositivo único
|
||||
2. Abrir seu navegador para a página de autenticação
|
||||
3. Solicitar a confirmação do dispositivo
|
||||
4. Completar o processo de autenticação
|
||||
|
||||
Após a autenticação bem-sucedida, você verá uma mensagem de confirmação no terminal!
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Criar uma Implantação">
|
||||
|
||||
No diretório do seu projeto, execute:
|
||||
|
||||
```bash
|
||||
crewai deploy create
|
||||
```
|
||||
|
||||
Este comando irá:
|
||||
1. Detectar informações do seu repositório GitHub
|
||||
2. Identificar variáveis de ambiente no seu arquivo `.env` local
|
||||
3. Transferir essas variáveis com segurança para a plataforma Enterprise
|
||||
4. Criar uma nova implantação com um identificador único
|
||||
|
||||
Com a criação bem-sucedida, você verá uma mensagem como:
|
||||
```shell
|
||||
Deployment created successfully!
|
||||
Name: your_project_name
|
||||
Deployment ID: 01234567-89ab-cdef-0123-456789abcdef
|
||||
Current Status: Deploy Enqueued
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Acompanhe o Progresso da Implantação">
|
||||
|
||||
Acompanhe o status da implantação com:
|
||||
|
||||
```bash
|
||||
crewai deploy status
|
||||
```
|
||||
|
||||
Para ver logs detalhados do processo de build:
|
||||
|
||||
```bash
|
||||
crewai deploy logs
|
||||
```
|
||||
|
||||
<Tip>
|
||||
A primeira implantação normalmente leva de 10 a 15 minutos, pois as imagens dos containers são construídas. As próximas implantações são bem mais rápidas.
|
||||
</Tip>
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Comandos Adicionais da CLI
|
||||
|
||||
O CrewAI CLI oferece vários comandos para gerenciar suas implantações:
|
||||
|
||||
```bash
|
||||
# Liste todas as suas implantações
|
||||
crewai deploy list
|
||||
|
||||
# Consulte o status de uma implantação
|
||||
crewai deploy status
|
||||
|
||||
# Veja os logs da implantação
|
||||
crewai deploy logs
|
||||
|
||||
# Envie atualizações após alterações no código
|
||||
crewai deploy push
|
||||
|
||||
# Remova uma implantação
|
||||
crewai deploy remove <deployment_id>
|
||||
```
|
||||
|
||||
## Opção 2: Implantar Diretamente pela Interface Web
|
||||
|
||||
Você também pode implantar seus crews diretamente pela interface web da CrewAI AMP conectando sua conta do GitHub. Esta abordagem não requer utilizar a CLI na sua máquina local.
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Enviar no GitHub">
|
||||
|
||||
Você precisa subir seu crew para um repositório do GitHub. Caso ainda não tenha criado um crew, você pode [seguir este tutorial](/pt-BR/quickstart).
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Conectando o GitHub ao CrewAI AMP">
|
||||
|
||||
1. Faça login em [CrewAI AMP](https://app.crewai.com)
|
||||
2. Clique no botão "Connect GitHub"
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Selecionar o Repositório">
|
||||
|
||||
Após conectar sua conta GitHub, você poderá selecionar qual repositório deseja implantar:
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Definir as Variáveis de Ambiente">
|
||||
|
||||
Antes de implantar, você precisará configurar as variáveis de ambiente para conectar ao seu provedor de LLM ou outros serviços:
|
||||
|
||||
1. Você pode adicionar variáveis individualmente ou em lote
|
||||
2. Digite suas variáveis no formato `KEY=VALUE` (uma por linha)
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Implante Seu Crew">
|
||||
|
||||
1. Clique no botão "Deploy" para iniciar o processo de implantação
|
||||
2. Você pode monitorar o progresso pela barra de progresso
|
||||
3. A primeira implantação geralmente demora de 10 a 15 minutos; as próximas serão mais rápidas
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Após a conclusão, você verá:
|
||||
- A URL exclusiva do seu crew
|
||||
- Um Bearer token para proteger sua API crew
|
||||
- Um botão "Delete" caso precise remover a implantação
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## ⚠️ Requisitos de Segurança para Variáveis de Ambiente
|
||||
|
||||
<Warning>
|
||||
**Importante**: A CrewAI AMP possui restrições de segurança sobre os nomes de
|
||||
variáveis de ambiente que podem causar falha na implantação caso não sejam
|
||||
seguidas.
|
||||
</Warning>
|
||||
|
||||
### Padrões de Variáveis de Ambiente Bloqueados
|
||||
|
||||
Por motivos de segurança, os seguintes padrões de nome de variável de ambiente são **automaticamente filtrados** e causarão problemas de implantação:
|
||||
|
||||
**Padrões Bloqueados:**
|
||||
|
||||
- Variáveis terminando em `_TOKEN` (ex: `MY_API_TOKEN`)
|
||||
- Variáveis terminando em `_PASSWORD` (ex: `DB_PASSWORD`)
|
||||
- Variáveis terminando em `_SECRET` (ex: `API_SECRET`)
|
||||
- Variáveis terminando em `_KEY` em certos contextos
|
||||
|
||||
**Variáveis Bloqueadas Específicas:**
|
||||
|
||||
- `GITHUB_USER`, `GITHUB_TOKEN`
|
||||
- `AWS_REGION`, `AWS_DEFAULT_REGION`
|
||||
- Diversas variáveis internas do sistema CrewAI
|
||||
|
||||
### Exceções Permitidas
|
||||
|
||||
Algumas variáveis são explicitamente permitidas mesmo coincidindo com os padrões bloqueados:
|
||||
|
||||
- `AZURE_AD_TOKEN`
|
||||
- `AZURE_OPENAI_AD_TOKEN`
|
||||
- `ENTERPRISE_ACTION_TOKEN`
|
||||
- `CREWAI_ENTEPRISE_TOOLS_TOKEN`
|
||||
|
||||
### Como Corrigir Problemas de Nomeação
|
||||
|
||||
Se sua implantação falhar devido a restrições de variáveis de ambiente:
|
||||
|
||||
```bash
|
||||
# ❌ Estas irão causar falhas na implantação
|
||||
OPENAI_TOKEN=sk-...
|
||||
DATABASE_PASSWORD=mysenha
|
||||
API_SECRET=segredo123
|
||||
|
||||
# ✅ Utilize estes padrões de nomeação
|
||||
OPENAI_API_KEY=sk-...
|
||||
DATABASE_CREDENTIALS=mysenha
|
||||
API_CONFIG=segredo123
|
||||
```
|
||||
|
||||
### Melhores Práticas
|
||||
|
||||
1. **Use convenções padrão de nomenclatura**: `PROVIDER_API_KEY` em vez de `PROVIDER_TOKEN`
|
||||
2. **Teste localmente primeiro**: Certifique-se de que seu crew funciona com as variáveis renomeadas
|
||||
3. **Atualize seu código**: Altere todas as referências aos nomes antigos das variáveis
|
||||
4. **Documente as mudanças**: Mantenha registro das variáveis renomeadas para seu time
|
||||
|
||||
<Tip>
|
||||
Se você se deparar com falhas de implantação com erros enigmáticos de
|
||||
variáveis de ambiente, confira primeiro os nomes das variáveis em relação a
|
||||
esses padrões.
|
||||
</Tip>
|
||||
|
||||
### Interaja com Seu Crew Implantado
|
||||
|
||||
Após a implantação, você pode acessar seu crew por meio de:
|
||||
|
||||
1. **REST API**: A plataforma gera um endpoint HTTPS exclusivo com estas rotas principais:
|
||||
|
||||
- `/inputs`: Lista os parâmetros de entrada requeridos
|
||||
- `/kickoff`: Inicia uma execução com os inputs fornecidos
|
||||
- `/status/{kickoff_id}`: Consulta o status da execução
|
||||
|
||||
2. **Interface Web**: Acesse [app.crewai.com](https://app.crewai.com) para visualizar:
|
||||
- **Aba Status**: Informações da implantação, detalhes do endpoint da API e token de autenticação
|
||||
- **Aba Run**: Visualização da estrutura do seu crew
|
||||
- **Aba Executions**: Histórico de todas as execuções
|
||||
- **Aba Metrics**: Análises de desempenho
|
||||
- **Aba Traces**: Insights detalhados das execuções
|
||||
|
||||
### Dispare uma Execução
|
||||
|
||||
No dashboard Enterprise, você pode:
|
||||
|
||||
1. Clicar no nome do seu crew para abrir seus detalhes
|
||||
2. Selecionar "Trigger Crew" na interface de gerenciamento
|
||||
3. Inserir os inputs necessários no modal exibido
|
||||
4. Monitorar o progresso à medida que a execução avança pelo pipeline
|
||||
|
||||
### Monitoramento e Análises
|
||||
|
||||
A plataforma Enterprise oferece recursos abrangentes de observabilidade:
|
||||
|
||||
- **Gestão das Execuções**: Acompanhe execuções ativas e concluídas
|
||||
- **Traces**: Quebra detalhada de cada execução
|
||||
- **Métricas**: Uso de tokens, tempos de execução e custos
|
||||
- **Visualização em Linha do Tempo**: Representação visual das sequências de tarefas
|
||||
|
||||
### Funcionalidades Avançadas
|
||||
|
||||
A plataforma Enterprise também oferece:
|
||||
|
||||
- **Gerenciamento de Variáveis de Ambiente**: Armazene e gerencie com segurança as chaves de API
|
||||
- **Conexões com LLM**: Configure integrações com diversos provedores de LLM
|
||||
- **Repositório Custom Tools**: Crie, compartilhe e instale ferramentas
|
||||
- **Crew Studio**: Monte crews via interface de chat sem escrever código
|
||||
|
||||
<Card title="Precisa de Ajuda?" icon="headset" href="mailto:support@crewai.com">
|
||||
Entre em contato com nossa equipe de suporte para ajuda com questões de
|
||||
implantação ou dúvidas sobre a plataforma Enterprise.
|
||||
</Card>
|
||||
439
docs/pt-BR/enterprise/guides/deploy-to-amp.mdx
Normal file
@@ -0,0 +1,439 @@
|
||||
---
|
||||
title: "Deploy para AMP"
|
||||
description: "Implante seu Crew ou Flow no CrewAI AMP"
|
||||
icon: "rocket"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Depois de criar um Crew ou Flow localmente (ou pelo Crew Studio), o próximo passo é
|
||||
implantá-lo na plataforma CrewAI AMP. Este guia cobre múltiplos métodos de
|
||||
implantação para ajudá-lo a escolher a melhor abordagem para o seu fluxo de trabalho.
|
||||
</Note>
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Projeto Pronto para Implantação" icon="check-circle">
|
||||
Você deve ter um Crew ou Flow funcionando localmente com sucesso.
|
||||
Siga nosso [guia de preparação](/pt-BR/enterprise/guides/prepare-for-deployment) para verificar a estrutura do seu projeto.
|
||||
</Card>
|
||||
<Card title="Repositório GitHub" icon="github">
|
||||
Seu código deve estar em um repositório do GitHub (para o método de integração com GitHub).
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<Info>
|
||||
**Crews vs Flows**: Ambos os tipos de projeto podem ser implantados como "automações" no CrewAI AMP.
|
||||
O processo de implantação é o mesmo, mas eles têm estruturas de projeto diferentes.
|
||||
Veja [Preparar para Implantação](/pt-BR/enterprise/guides/prepare-for-deployment) para detalhes.
|
||||
</Info>
|
||||
|
||||
## Opção 1: Implantar Usando o CrewAI CLI
|
||||
|
||||
A CLI fornece a maneira mais rápida de implantar Crews ou Flows desenvolvidos localmente na plataforma AMP.
|
||||
A CLI detecta automaticamente o tipo do seu projeto a partir do `pyproject.toml` e faz o build adequadamente.
|
||||
|
||||
<Steps>
|
||||
<Step title="Instale o CrewAI CLI">
|
||||
Se ainda não tiver, instale o CrewAI CLI:
|
||||
|
||||
```bash
|
||||
pip install crewai[tools]
|
||||
```
|
||||
|
||||
<Tip>
|
||||
A CLI vem com o pacote principal CrewAI, mas o extra `[tools]` garante todas as dependências de implantação.
|
||||
</Tip>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Autentique-se na Plataforma Enterprise">
|
||||
Primeiro, você precisa autenticar sua CLI com a plataforma CrewAI AMP:
|
||||
|
||||
```bash
|
||||
# Se já possui uma conta CrewAI AMP, ou deseja criar uma:
|
||||
crewai login
|
||||
```
|
||||
|
||||
Ao executar qualquer um dos comandos, a CLI irá:
|
||||
1. Exibir uma URL e um código de dispositivo único
|
||||
2. Abrir seu navegador para a página de autenticação
|
||||
3. Solicitar a confirmação do dispositivo
|
||||
4. Completar o processo de autenticação
|
||||
|
||||
Após a autenticação bem-sucedida, você verá uma mensagem de confirmação no terminal!
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Criar uma Implantação">
|
||||
|
||||
No diretório do seu projeto, execute:
|
||||
|
||||
```bash
|
||||
crewai deploy create
|
||||
```
|
||||
|
||||
Este comando irá:
|
||||
1. Detectar informações do seu repositório GitHub
|
||||
2. Identificar variáveis de ambiente no seu arquivo `.env` local
|
||||
3. Transferir essas variáveis com segurança para a plataforma Enterprise
|
||||
4. Criar uma nova implantação com um identificador único
|
||||
|
||||
Com a criação bem-sucedida, você verá uma mensagem como:
|
||||
```shell
|
||||
Deployment created successfully!
|
||||
Name: your_project_name
|
||||
Deployment ID: 01234567-89ab-cdef-0123-456789abcdef
|
||||
Current Status: Deploy Enqueued
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Acompanhe o Progresso da Implantação">
|
||||
|
||||
Acompanhe o status da implantação com:
|
||||
|
||||
```bash
|
||||
crewai deploy status
|
||||
```
|
||||
|
||||
Para ver logs detalhados do processo de build:
|
||||
|
||||
```bash
|
||||
crewai deploy logs
|
||||
```
|
||||
|
||||
<Tip>
|
||||
A primeira implantação normalmente leva de 10 a 15 minutos, pois as imagens dos containers são construídas. As próximas implantações são bem mais rápidas.
|
||||
</Tip>
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Comandos Adicionais da CLI
|
||||
|
||||
O CrewAI CLI oferece vários comandos para gerenciar suas implantações:
|
||||
|
||||
```bash
|
||||
# Liste todas as suas implantações
|
||||
crewai deploy list
|
||||
|
||||
# Consulte o status de uma implantação
|
||||
crewai deploy status
|
||||
|
||||
# Veja os logs da implantação
|
||||
crewai deploy logs
|
||||
|
||||
# Envie atualizações após alterações no código
|
||||
crewai deploy push
|
||||
|
||||
# Remova uma implantação
|
||||
crewai deploy remove <deployment_id>
|
||||
```
|
||||
|
||||
## Opção 2: Implantar Diretamente pela Interface Web
|
||||
|
||||
Você também pode implantar seus Crews ou Flows diretamente pela interface web do CrewAI AMP conectando sua conta do GitHub. Esta abordagem não requer utilizar a CLI na sua máquina local. A plataforma detecta automaticamente o tipo do seu projeto e trata o build adequadamente.
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Enviar para o GitHub">
|
||||
|
||||
Você precisa enviar seu crew para um repositório do GitHub. Caso ainda não tenha criado um crew, você pode [seguir este tutorial](/pt-BR/quickstart).
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Conectando o GitHub ao CrewAI AMP">
|
||||
|
||||
1. Faça login em [CrewAI AMP](https://app.crewai.com)
|
||||
2. Clique no botão "Connect GitHub"
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Selecionar o Repositório">
|
||||
|
||||
Após conectar sua conta GitHub, você poderá selecionar qual repositório deseja implantar:
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Definir as Variáveis de Ambiente">
|
||||
|
||||
Antes de implantar, você precisará configurar as variáveis de ambiente para conectar ao seu provedor de LLM ou outros serviços:
|
||||
|
||||
1. Você pode adicionar variáveis individualmente ou em lote
|
||||
2. Digite suas variáveis no formato `KEY=VALUE` (uma por linha)
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Implante Seu Crew">
|
||||
|
||||
1. Clique no botão "Deploy" para iniciar o processo de implantação
|
||||
2. Você pode monitorar o progresso pela barra de progresso
|
||||
3. A primeira implantação geralmente demora de 10 a 15 minutos; as próximas serão mais rápidas
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
|
||||
Após a conclusão, você verá:
|
||||
- A URL exclusiva do seu crew
|
||||
- Um Bearer token para proteger sua API crew
|
||||
- Um botão "Delete" caso precise remover a implantação
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Opção 3: Reimplantar Usando API (Integração CI/CD)
|
||||
|
||||
Para implantações automatizadas em pipelines CI/CD, você pode usar a API do CrewAI para acionar reimplantações de crews existentes. Isso é particularmente útil para GitHub Actions, Jenkins ou outros workflows de automação.
|
||||
|
||||
<Steps>
|
||||
<Step title="Obtenha Seu Token de Acesso Pessoal">
|
||||
|
||||
Navegue até as configurações da sua conta CrewAI AMP para gerar um token de API:
|
||||
|
||||
1. Acesse [app.crewai.com](https://app.crewai.com)
|
||||
2. Clique em **Settings** → **Account** → **Personal Access Token**
|
||||
3. Gere um novo token e copie-o com segurança
|
||||
4. Armazene este token como um secret no seu sistema CI/CD
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Encontre o UUID da Sua Automação">
|
||||
|
||||
Localize o identificador único do seu crew implantado:
|
||||
|
||||
1. Acesse **Automations** no seu dashboard CrewAI AMP
|
||||
2. Selecione sua automação/crew existente
|
||||
3. Clique em **Additional Details**
|
||||
4. Copie o **UUID** - este identifica sua implantação específica do crew
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Acione a Reimplantação via API">
|
||||
|
||||
Use o endpoint da API de Deploy para acionar uma reimplantação:
|
||||
|
||||
```bash
|
||||
curl -i -X POST \
|
||||
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN" \
|
||||
https://app.crewai.com/crewai_plus/api/v1/crews/YOUR-AUTOMATION-UUID/deploy
|
||||
|
||||
# HTTP/2 200
|
||||
# content-type: application/json
|
||||
#
|
||||
# {
|
||||
# "uuid": "your-automation-uuid",
|
||||
# "status": "Deploy Enqueued",
|
||||
# "public_url": "https://your-crew-deployment.crewai.com",
|
||||
# "token": "your-bearer-token"
|
||||
# }
|
||||
```
|
||||
|
||||
<Info>
|
||||
Se sua automação foi criada originalmente conectada ao Git, a API automaticamente puxará as últimas alterações do seu repositório antes de reimplantar.
|
||||
</Info>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Exemplo de Integração com GitHub Actions">
|
||||
|
||||
Aqui está um workflow do GitHub Actions com gatilhos de implantação mais complexos:
|
||||
|
||||
```yaml
|
||||
name: Deploy CrewAI Automation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
types: [ labeled ]
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
||||
(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy')) ||
|
||||
(github.event_name == 'release')
|
||||
steps:
|
||||
- name: Trigger CrewAI Redeployment
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer ${{ secrets.CREWAI_PAT }}" \
|
||||
https://app.crewai.com/crewai_plus/api/v1/crews/${{ secrets.CREWAI_AUTOMATION_UUID }}/deploy
|
||||
```
|
||||
|
||||
<Tip>
|
||||
Adicione `CREWAI_PAT` e `CREWAI_AUTOMATION_UUID` como secrets do repositório. Para implantações de PR, adicione um label "deploy" para acionar o workflow.
|
||||
</Tip>
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Interaja com Sua Automação Implantada
|
||||
|
||||
Após a implantação, você pode acessar seu crew através de:
|
||||
|
||||
1. **REST API**: A plataforma gera um endpoint HTTPS exclusivo com estas rotas principais:
|
||||
|
||||
- `/inputs`: Lista os parâmetros de entrada requeridos
|
||||
- `/kickoff`: Inicia uma execução com os inputs fornecidos
|
||||
- `/status/{kickoff_id}`: Consulta o status da execução
|
||||
|
||||
2. **Interface Web**: Acesse [app.crewai.com](https://app.crewai.com) para visualizar:
|
||||
- **Aba Status**: Informações da implantação, detalhes do endpoint da API e token de autenticação
|
||||
- **Aba Run**: Visualização da estrutura do seu crew
|
||||
- **Aba Executions**: Histórico de todas as execuções
|
||||
- **Aba Metrics**: Análises de desempenho
|
||||
- **Aba Traces**: Insights detalhados das execuções
|
||||
|
||||
### Dispare uma Execução
|
||||
|
||||
No dashboard Enterprise, você pode:
|
||||
|
||||
1. Clicar no nome do seu crew para abrir seus detalhes
|
||||
2. Selecionar "Trigger Crew" na interface de gerenciamento
|
||||
3. Inserir os inputs necessários no modal exibido
|
||||
4. Monitorar o progresso à medida que a execução avança pelo pipeline
|
||||
|
||||
### Monitoramento e Análises
|
||||
|
||||
A plataforma Enterprise oferece recursos abrangentes de observabilidade:
|
||||
|
||||
- **Gestão das Execuções**: Acompanhe execuções ativas e concluídas
|
||||
- **Traces**: Quebra detalhada de cada execução
|
||||
- **Métricas**: Uso de tokens, tempos de execução e custos
|
||||
- **Visualização em Linha do Tempo**: Representação visual das sequências de tarefas
|
||||
|
||||
### Funcionalidades Avançadas
|
||||
|
||||
A plataforma Enterprise também oferece:
|
||||
|
||||
- **Gerenciamento de Variáveis de Ambiente**: Armazene e gerencie com segurança as chaves de API
|
||||
- **Conexões com LLM**: Configure integrações com diversos provedores de LLM
|
||||
- **Repositório Custom Tools**: Crie, compartilhe e instale ferramentas
|
||||
- **Crew Studio**: Monte crews via interface de chat sem escrever código
|
||||
|
||||
## Solução de Problemas em Falhas de Implantação
|
||||
|
||||
Se sua implantação falhar, verifique estes problemas comuns:
|
||||
|
||||
### Falhas de Build
|
||||
|
||||
#### Arquivo uv.lock Ausente
|
||||
|
||||
**Sintoma**: Build falha no início com erros de resolução de dependências
|
||||
|
||||
**Solução**: Gere e faça commit do arquivo lock:
|
||||
|
||||
```bash
|
||||
uv lock
|
||||
git add uv.lock
|
||||
git commit -m "Add uv.lock for deployment"
|
||||
git push
|
||||
```
|
||||
|
||||
<Warning>
|
||||
O arquivo `uv.lock` é obrigatório para todas as implantações. Sem ele, a plataforma
|
||||
não consegue instalar suas dependências de forma confiável.
|
||||
</Warning>
|
||||
|
||||
#### Estrutura de Projeto Incorreta
|
||||
|
||||
**Sintoma**: Erros "Could not find entry point" ou "Module not found"
|
||||
|
||||
**Solução**: Verifique se seu projeto corresponde à estrutura esperada:
|
||||
|
||||
- **Tanto Crews quanto Flows**: Devem ter ponto de entrada em `src/project_name/main.py`
|
||||
- **Crews**: Usam uma função `run()` como ponto de entrada
|
||||
- **Flows**: Usam uma função `kickoff()` como ponto de entrada
|
||||
|
||||
Veja [Preparar para Implantação](/pt-BR/enterprise/guides/prepare-for-deployment) para diagramas de estrutura detalhados.
|
||||
|
||||
#### Decorador CrewBase Ausente
|
||||
|
||||
**Sintoma**: Erros "Crew not found", "Config not found" ou erros de configuração de agent/task
|
||||
|
||||
**Solução**: Certifique-se de que **todas** as classes crew usam o decorador `@CrewBase`:
|
||||
|
||||
```python
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
|
||||
@CrewBase # Este decorador é OBRIGATÓRIO
|
||||
class YourCrew():
|
||||
"""Descrição do seu crew"""
|
||||
|
||||
@agent
|
||||
def my_agent(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['my_agent'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# ... resto da definição do crew
|
||||
```
|
||||
|
||||
<Info>
|
||||
Isso se aplica a Crews independentes E crews embutidos dentro de projetos Flow.
|
||||
Toda classe crew precisa do decorador.
|
||||
</Info>
|
||||
|
||||
#### Tipo Incorreto no pyproject.toml
|
||||
|
||||
**Sintoma**: Build tem sucesso mas falha em runtime, ou comportamento inesperado
|
||||
|
||||
**Solução**: Verifique se a seção `[tool.crewai]` corresponde ao tipo do seu projeto:
|
||||
|
||||
```toml
|
||||
# Para projetos Crew:
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
|
||||
# Para projetos Flow:
|
||||
[tool.crewai]
|
||||
type = "flow"
|
||||
```
|
||||
|
||||
### Falhas de Runtime
|
||||
|
||||
#### Falhas de Conexão com LLM
|
||||
|
||||
**Sintoma**: Erros de chave API, "model not found" ou falhas de autenticação
|
||||
|
||||
**Solução**:
|
||||
1. Verifique se a chave API do seu provedor LLM está corretamente definida nas variáveis de ambiente
|
||||
2. Certifique-se de que os nomes das variáveis de ambiente correspondem ao que seu código espera
|
||||
3. Teste localmente com exatamente as mesmas variáveis de ambiente antes de implantar
|
||||
|
||||
#### Erros de Execução do Crew
|
||||
|
||||
**Sintoma**: Crew inicia mas falha durante a execução
|
||||
|
||||
**Solução**:
|
||||
1. Verifique os logs de execução no dashboard AMP (aba Traces)
|
||||
2. Verifique se todas as ferramentas têm as chaves API necessárias configuradas
|
||||
3. Certifique-se de que as configurações de agents em `agents.yaml` são válidas
|
||||
4. Verifique se há erros de sintaxe nas configurações de tasks em `tasks.yaml`
|
||||
|
||||
<Card title="Precisa de Ajuda?" icon="headset" href="mailto:support@crewai.com">
|
||||
Entre em contato com nossa equipe de suporte para ajuda com questões de
|
||||
implantação ou dúvidas sobre a plataforma AMP.
|
||||
</Card>
|
||||
305
docs/pt-BR/enterprise/guides/prepare-for-deployment.mdx
Normal file
@@ -0,0 +1,305 @@
|
||||
---
|
||||
title: "Preparar para Implantação"
|
||||
description: "Certifique-se de que seu Crew ou Flow está pronto para implantação no CrewAI AMP"
|
||||
icon: "clipboard-check"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Antes de implantar no CrewAI AMP, é crucial verificar se seu projeto está estruturado corretamente.
|
||||
Tanto Crews quanto Flows podem ser implantados como "automações", mas eles têm estruturas de projeto
|
||||
e requisitos diferentes que devem ser atendidos para uma implantação bem-sucedida.
|
||||
</Note>
|
||||
|
||||
## Entendendo Automações
|
||||
|
||||
No CrewAI AMP, **automações** é o termo geral para projetos de IA Agêntica implantáveis. Uma automação pode ser:
|
||||
|
||||
- **Um Crew**: Uma equipe independente de agentes de IA trabalhando juntos em tarefas
|
||||
- **Um Flow**: Um workflow orquestrado que pode combinar múltiplos crews, chamadas diretas de LLM e lógica procedural
|
||||
|
||||
Entender qual tipo você está implantando é essencial porque eles têm estruturas de projeto e pontos de entrada diferentes.
|
||||
|
||||
## Crews vs Flows: Principais Diferenças
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Projetos Crew" icon="users">
|
||||
Equipes de agentes de IA independentes com `crew.py` definindo agentes e tarefas. Ideal para tarefas focadas e colaborativas.
|
||||
</Card>
|
||||
<Card title="Projetos Flow" icon="diagram-project">
|
||||
Workflows orquestrados com crews embutidos em uma pasta `crews/`. Ideal para processos complexos de múltiplas etapas.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
| Aspecto | Crew | Flow |
|
||||
|---------|------|------|
|
||||
| **Estrutura do projeto** | `src/project_name/` com `crew.py` | `src/project_name/` com pasta `crews/` |
|
||||
| **Localização da lógica principal** | `src/project_name/crew.py` | `src/project_name/main.py` (classe Flow) |
|
||||
| **Função de ponto de entrada** | `run()` em `main.py` | `kickoff()` em `main.py` |
|
||||
| **Tipo no pyproject.toml** | `type = "crew"` | `type = "flow"` |
|
||||
| **Comando CLI de criação** | `crewai create crew name` | `crewai create flow name` |
|
||||
| **Localização da configuração** | `src/project_name/config/` | `src/project_name/crews/crew_name/config/` |
|
||||
| **Pode conter outros crews** | Não | Sim (na pasta `crews/`) |
|
||||
|
||||
## Referência de Estrutura de Projeto
|
||||
|
||||
### Estrutura de Projeto Crew
|
||||
|
||||
Quando você executa `crewai create crew my_crew`, você obtém esta estrutura:
|
||||
|
||||
```
|
||||
my_crew/
|
||||
├── .gitignore
|
||||
├── pyproject.toml # Deve ter type = "crew"
|
||||
├── README.md
|
||||
├── .env
|
||||
├── uv.lock # OBRIGATÓRIO para implantação
|
||||
└── src/
|
||||
└── my_crew/
|
||||
├── __init__.py
|
||||
├── main.py # Ponto de entrada com função run()
|
||||
├── crew.py # Classe Crew com decorador @CrewBase
|
||||
├── tools/
|
||||
│ ├── custom_tool.py
|
||||
│ └── __init__.py
|
||||
└── config/
|
||||
├── agents.yaml # Definições de agentes
|
||||
└── tasks.yaml # Definições de tarefas
|
||||
```
|
||||
|
||||
<Warning>
|
||||
A estrutura aninhada `src/project_name/` é crítica para Crews.
|
||||
Colocar arquivos no nível errado causará falhas na implantação.
|
||||
</Warning>
|
||||
|
||||
### Estrutura de Projeto Flow
|
||||
|
||||
Quando você executa `crewai create flow my_flow`, você obtém esta estrutura:
|
||||
|
||||
```
|
||||
my_flow/
|
||||
├── .gitignore
|
||||
├── pyproject.toml # Deve ter type = "flow"
|
||||
├── README.md
|
||||
├── .env
|
||||
├── uv.lock # OBRIGATÓRIO para implantação
|
||||
└── src/
|
||||
└── my_flow/
|
||||
├── __init__.py
|
||||
├── main.py # Ponto de entrada com função kickoff() + classe Flow
|
||||
├── crews/ # Pasta de crews embutidos
|
||||
│ └── poem_crew/
|
||||
│ ├── __init__.py
|
||||
│ ├── poem_crew.py # Crew com decorador @CrewBase
|
||||
│ └── config/
|
||||
│ ├── agents.yaml
|
||||
│ └── tasks.yaml
|
||||
└── tools/
|
||||
├── __init__.py
|
||||
└── custom_tool.py
|
||||
```
|
||||
|
||||
<Info>
|
||||
Tanto Crews quanto Flows usam a estrutura `src/project_name/`.
|
||||
A diferença chave é que Flows têm uma pasta `crews/` para crews embutidos,
|
||||
enquanto Crews têm `crew.py` diretamente na pasta do projeto.
|
||||
</Info>
|
||||
|
||||
## Checklist Pré-Implantação
|
||||
|
||||
Use este checklist para verificar se seu projeto está pronto para implantação.
|
||||
|
||||
### 1. Verificar Configuração do pyproject.toml
|
||||
|
||||
Seu `pyproject.toml` deve incluir a seção `[tool.crewai]` correta:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Para Crews">
|
||||
```toml
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Para Flows">
|
||||
```toml
|
||||
[tool.crewai]
|
||||
type = "flow"
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Warning>
|
||||
Se o `type` não corresponder à estrutura do seu projeto, o build falhará ou
|
||||
a automação não funcionará corretamente.
|
||||
</Warning>
|
||||
|
||||
### 2. Garantir que o Arquivo uv.lock Existe
|
||||
|
||||
CrewAI usa `uv` para gerenciamento de dependências. O arquivo `uv.lock` garante builds reproduzíveis e é **obrigatório** para implantação.
|
||||
|
||||
```bash
|
||||
# Gerar ou atualizar o arquivo lock
|
||||
uv lock
|
||||
|
||||
# Verificar se existe
|
||||
ls -la uv.lock
|
||||
```
|
||||
|
||||
Se o arquivo não existir, execute `uv lock` e faça commit no seu repositório:
|
||||
|
||||
```bash
|
||||
uv lock
|
||||
git add uv.lock
|
||||
git commit -m "Add uv.lock for deployment"
|
||||
git push
|
||||
```
|
||||
|
||||
### 3. Validar Uso do Decorador CrewBase
|
||||
|
||||
**Toda classe crew deve usar o decorador `@CrewBase`.** Isso se aplica a:
|
||||
|
||||
- Projetos crew independentes
|
||||
- Crews embutidos dentro de projetos Flow
|
||||
|
||||
```python
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.project import CrewBase, agent, crew, task
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import List
|
||||
|
||||
@CrewBase # Este decorador é OBRIGATÓRIO
|
||||
class MyCrew():
|
||||
"""Descrição do meu crew"""
|
||||
|
||||
agents: List[BaseAgent]
|
||||
tasks: List[Task]
|
||||
|
||||
@agent
|
||||
def my_agent(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config['my_agent'], # type: ignore[index]
|
||||
verbose=True
|
||||
)
|
||||
|
||||
@task
|
||||
def my_task(self) -> Task:
|
||||
return Task(
|
||||
config=self.tasks_config['my_task'] # type: ignore[index]
|
||||
)
|
||||
|
||||
@crew
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=self.agents,
|
||||
tasks=self.tasks,
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Se você esquecer o decorador `@CrewBase`, sua implantação falhará com
|
||||
erros sobre configurações de agents ou tasks ausentes.
|
||||
</Warning>
|
||||
|
||||
### 4. Verificar Pontos de Entrada do Projeto
|
||||
|
||||
Tanto Crews quanto Flows têm seu ponto de entrada em `src/project_name/main.py`:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Para Crews">
|
||||
O ponto de entrada usa uma função `run()`:
|
||||
|
||||
```python
|
||||
# src/my_crew/main.py
|
||||
from my_crew.crew import MyCrew
|
||||
|
||||
def run():
|
||||
"""Executa o crew."""
|
||||
inputs = {'topic': 'AI in Healthcare'}
|
||||
result = MyCrew().crew().kickoff(inputs=inputs)
|
||||
return result
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Para Flows">
|
||||
O ponto de entrada usa uma função `kickoff()` com uma classe Flow:
|
||||
|
||||
```python
|
||||
# src/my_flow/main.py
|
||||
from crewai.flow import Flow, listen, start
|
||||
from my_flow.crews.poem_crew.poem_crew import PoemCrew
|
||||
|
||||
class MyFlow(Flow):
|
||||
@start()
|
||||
def begin(self):
|
||||
# Lógica do Flow aqui
|
||||
result = PoemCrew().crew().kickoff(inputs={...})
|
||||
return result
|
||||
|
||||
def kickoff():
|
||||
"""Executa o flow."""
|
||||
MyFlow().kickoff()
|
||||
|
||||
if __name__ == "__main__":
|
||||
kickoff()
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### 5. Preparar Variáveis de Ambiente
|
||||
|
||||
Antes da implantação, certifique-se de ter:
|
||||
|
||||
1. **Chaves de API de LLM** prontas (OpenAI, Anthropic, Google, etc.)
|
||||
2. **Chaves de API de ferramentas** se estiver usando ferramentas externas (Serper, etc.)
|
||||
|
||||
<Tip>
|
||||
Teste seu projeto localmente com as mesmas variáveis de ambiente antes de implantar
|
||||
para detectar problemas de configuração antecipadamente.
|
||||
</Tip>
|
||||
|
||||
## Comandos de Validação Rápida
|
||||
|
||||
Execute estes comandos a partir da raiz do seu projeto para verificar rapidamente sua configuração:
|
||||
|
||||
```bash
|
||||
# 1. Verificar tipo do projeto no pyproject.toml
|
||||
grep -A2 "\[tool.crewai\]" pyproject.toml
|
||||
|
||||
# 2. Verificar se uv.lock existe
|
||||
ls -la uv.lock || echo "ERRO: uv.lock ausente! Execute 'uv lock'"
|
||||
|
||||
# 3. Verificar se estrutura src/ existe
|
||||
ls -la src/*/main.py 2>/dev/null || echo "Nenhum main.py encontrado em src/"
|
||||
|
||||
# 4. Para Crews - verificar se crew.py existe
|
||||
ls -la src/*/crew.py 2>/dev/null || echo "Nenhum crew.py (esperado para Crews)"
|
||||
|
||||
# 5. Para Flows - verificar se pasta crews/ existe
|
||||
ls -la src/*/crews/ 2>/dev/null || echo "Nenhuma pasta crews/ (esperado para Flows)"
|
||||
|
||||
# 6. Verificar uso do CrewBase
|
||||
grep -r "@CrewBase" . --include="*.py"
|
||||
```
|
||||
|
||||
## Erros Comuns de Configuração
|
||||
|
||||
| Erro | Sintoma | Correção |
|
||||
|------|---------|----------|
|
||||
| `uv.lock` ausente | Build falha durante resolução de dependências | Execute `uv lock` e faça commit |
|
||||
| `type` errado no pyproject.toml | Build bem-sucedido mas falha em runtime | Altere para o tipo correto |
|
||||
| Decorador `@CrewBase` ausente | Erros "Config not found" | Adicione decorador a todas as classes crew |
|
||||
| Arquivos na raiz ao invés de `src/` | Ponto de entrada não encontrado | Mova para `src/project_name/` |
|
||||
| `run()` ou `kickoff()` ausente | Não é possível iniciar automação | Adicione a função de entrada correta |
|
||||
|
||||
## Próximos Passos
|
||||
|
||||
Uma vez que seu projeto passar por todos os itens do checklist, você está pronto para implantar:
|
||||
|
||||
<Card title="Deploy para AMP" icon="rocket" href="/pt-BR/enterprise/guides/deploy-to-amp">
|
||||
Siga o guia de implantação para implantar seu Crew ou Flow no CrewAI AMP usando
|
||||
a CLI, interface web ou integração CI/CD.
|
||||
</Card>
|
||||
@@ -82,7 +82,7 @@ CrewAI AMP expande o poder do framework open-source com funcionalidades projetad
|
||||
<Card
|
||||
title="Implantar Crew"
|
||||
icon="rocket"
|
||||
href="/pt-BR/enterprise/guides/deploy-crew"
|
||||
href="/pt-BR/enterprise/guides/deploy-to-amp"
|
||||
>
|
||||
Implantar Crew
|
||||
</Card>
|
||||
@@ -92,11 +92,11 @@ CrewAI AMP expande o poder do framework open-source com funcionalidades projetad
|
||||
<Card
|
||||
title="Acesso via API"
|
||||
icon="code"
|
||||
href="/pt-BR/enterprise/guides/deploy-crew"
|
||||
href="/pt-BR/enterprise/guides/kickoff-crew"
|
||||
>
|
||||
Usar a API do Crew
|
||||
</Card>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
Para instruções detalhadas, consulte nosso [guia de implantação](/pt-BR/enterprise/guides/deploy-crew) ou clique no botão abaixo para começar.
|
||||
Para instruções detalhadas, consulte nosso [guia de implantação](/pt-BR/enterprise/guides/deploy-to-amp) ou clique no botão abaixo para começar.
|
||||
|
||||
@@ -7,6 +7,10 @@ mode: "wide"
|
||||
|
||||
## Visão Geral
|
||||
|
||||
<Note>
|
||||
O decorador `@human_feedback` requer **CrewAI versão 1.8.0 ou superior**. Certifique-se de atualizar sua instalação antes de usar este recurso.
|
||||
</Note>
|
||||
|
||||
O decorador `@human_feedback` permite fluxos de trabalho human-in-the-loop (HITL) diretamente nos CrewAI Flows. Ele permite pausar a execução do flow, apresentar a saída para um humano revisar, coletar seu feedback e, opcionalmente, rotear para diferentes listeners com base no resultado do feedback.
|
||||
|
||||
Isso é particularmente valioso para:
|
||||
|
||||
@@ -5,9 +5,22 @@ icon: "user-check"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
Human-in-the-Loop (HITL) é uma abordagem poderosa que combina a inteligência artificial com a experiência humana para aprimorar a tomada de decisões e melhorar os resultados das tarefas. Este guia mostra como implementar HITL dentro da CrewAI.
|
||||
Human-in-the-Loop (HITL) é uma abordagem poderosa que combina a inteligência artificial com a experiência humana para aprimorar a tomada de decisões e melhorar os resultados das tarefas. CrewAI oferece várias maneiras de implementar HITL dependendo das suas necessidades.
|
||||
|
||||
## Configurando Workflows HITL
|
||||
## Escolhendo Sua Abordagem HITL
|
||||
|
||||
CrewAI oferece duas abordagens principais para implementar workflows human-in-the-loop:
|
||||
|
||||
| Abordagem | Melhor Para | Integração | Versão |
|
||||
|----------|----------|-------------|---------|
|
||||
| **Baseada em Flow** (decorador `@human_feedback`) | Desenvolvimento local, revisão via console, workflows síncronos | [Feedback Humano em Flows](/pt-BR/learn/human-feedback-in-flows) | **1.8.0+** |
|
||||
| **Baseada em Webhook** (Enterprise) | Deployments em produção, workflows assíncronos, integrações externas (Slack, Teams, etc.) | Este guia | - |
|
||||
|
||||
<Tip>
|
||||
Se você está construindo flows e deseja adicionar etapas de revisão humana com roteamento baseado em feedback, confira o guia [Feedback Humano em Flows](/pt-BR/learn/human-feedback-in-flows) para o decorador `@human_feedback`.
|
||||
</Tip>
|
||||
|
||||
## Configurando Workflows HITL Baseados em Webhook
|
||||
|
||||
<Steps>
|
||||
<Step title="Configure sua Tarefa">
|
||||
|
||||
115
docs/pt-BR/observability/galileo.mdx
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: Galileo Galileu
|
||||
description: Integração Galileo para rastreamento e avaliação CrewAI
|
||||
icon: telescope
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Visão geral
|
||||
|
||||
Este guia demonstra como integrar o **Galileo**com o **CrewAI**
|
||||
para rastreamento abrangente e engenharia de avaliação.
|
||||
Ao final deste guia, você será capaz de rastrear seus agentes CrewAI,
|
||||
monitorar seu desempenho e avaliar seu comportamento com
|
||||
A poderosa plataforma de observabilidade do Galileo.
|
||||
|
||||
> **O que é Galileo?**[Galileo](https://galileo.ai/) é avaliação e observabilidade de IA
|
||||
plataforma que oferece rastreamento, avaliação e
|
||||
e monitoramento de aplicações de IA. Ele permite que as equipes capturem a verdade,
|
||||
criar grades de proteção robustas e realizar experimentos sistemáticos com
|
||||
rastreamento de experimentos integrado e análise de desempenho -garantindo confiabilidade,
|
||||
transparência e melhoria contínua em todo o ciclo de vida da IA.
|
||||
|
||||
## Primeiros passos
|
||||
|
||||
Este tutorial segue o [CrewAI Quickstart](pt-BR/quickstart) e mostra como adicionar
|
||||
[CrewAIEventListener] do Galileo(https://v2docs.galileo.ai/sdk-api/python/reference/handlers/crewai/handler),
|
||||
um manipulador de eventos.
|
||||
Para mais informações, consulte Galileu
|
||||
[Adicionar Galileo a um aplicativo CrewAI](https://v2docs.galileo.ai/how-to-guides/third-party-integrations/add-galileo-to-crewai/add-galileo-to-crewai)
|
||||
guia prático.
|
||||
|
||||
> **Observação**Este tutorial pressupõe que você concluiu o [CrewAI Quickstart](pt-BR/quickstart).
|
||||
Se você quiser um exemplo completo e abrangente, consulte o Galileo
|
||||
[Repositório de exemplo SDK da CrewAI](https://github.com/rungalileo/sdk-examples/tree/main/python/agent/crew-ai).
|
||||
|
||||
### Etapa 1: instalar dependências
|
||||
|
||||
Instale as dependências necessárias para seu aplicativo.
|
||||
Crie um ambiente virtual usando seu método preferido,
|
||||
em seguida, instale dependências dentro desse ambiente usando seu
|
||||
ferramenta preferida:
|
||||
|
||||
```bash
|
||||
uv add galileo
|
||||
```
|
||||
|
||||
### Etapa 2: adicione ao arquivo .env do [CrewAI Quickstart](/pt-BR/quickstart)
|
||||
|
||||
```bash
|
||||
# Your Galileo API key
|
||||
GALILEO_API_KEY="your-galileo-api-key"
|
||||
|
||||
# Your Galileo project name
|
||||
GALILEO_PROJECT="your-galileo-project-name"
|
||||
|
||||
# The name of the Log stream you want to use for logging
|
||||
GALILEO_LOG_STREAM="your-galileo-log-stream "
|
||||
```
|
||||
|
||||
### Etapa 3: adicionar o ouvinte de eventos Galileo
|
||||
|
||||
Para habilitar o registro com Galileo, você precisa criar uma instância do `CrewAIEventListener`.
|
||||
Importe o pacote manipulador Galileo CrewAI por
|
||||
adicionando o seguinte código no topo do seu arquivo main.py:
|
||||
|
||||
```python
|
||||
from galileo.handlers.crewai.handler import CrewAIEventListener
|
||||
```
|
||||
|
||||
No início da sua função run, crie o ouvinte de evento:
|
||||
|
||||
```python
|
||||
def run():
|
||||
# Create the event listener
|
||||
CrewAIEventListener()
|
||||
# The rest of your existing code goes here
|
||||
```
|
||||
|
||||
Quando você cria a instância do listener, ela é automaticamente
|
||||
registrado na CrewAI.
|
||||
|
||||
### Etapa 4: administre sua Crew
|
||||
|
||||
Administre sua Crew com o CrewAI CLI:
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
|
||||
### Passo 5: Visualize os traços no Galileo
|
||||
|
||||
Assim que sua tripulação terminar, os rastros serão eliminados e aparecerão no Galileo.
|
||||
|
||||

|
||||
|
||||
## Compreendendo a integração do Galileo
|
||||
|
||||
Galileo se integra ao CrewAI registrando um ouvinte de evento
|
||||
que captura eventos de execução da tripulação (por exemplo, ações do agente, chamadas de ferramentas, respostas do modelo)
|
||||
e os encaminha ao Galileo para observabilidade e avaliação.
|
||||
|
||||
### Compreendendo o ouvinte de eventos
|
||||
|
||||
Criar uma instância `CrewAIEventListener()` é tudo o que você precisa
|
||||
necessário para habilitar o Galileo para uma execução do CrewAI. Quando instanciado, o ouvinte:
|
||||
|
||||
-Registra-se automaticamente no CrewAI
|
||||
-Lê a configuração do Galileo a partir de variáveis de ambiente
|
||||
-Registra todos os dados de execução no projeto Galileo e fluxo de log especificado por
|
||||
`GALILEO_PROJECT` e `GALILEO_LOG_STREAM`
|
||||
|
||||
Nenhuma configuração adicional ou alterações de código são necessárias.
|
||||
Todos os dados desta execução são registados no projecto Galileo e
|
||||
fluxo de log especificado pela configuração do seu ambiente
|
||||
(por exemplo, GALILEO_PROJECT e GALILEO_LOG_STREAM).
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests~=2.32.5",
|
||||
"docker~=7.1.0",
|
||||
"crewai==1.7.2",
|
||||
"crewai==1.8.1",
|
||||
"lancedb~=0.5.4",
|
||||
"tiktoken~=0.8.0",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
|
||||
@@ -291,4 +291,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.7.2"
|
||||
__version__ = "1.8.1"
|
||||
|
||||
@@ -49,7 +49,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.7.2",
|
||||
"crewai-tools==1.8.1",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
|
||||
@@ -40,7 +40,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.7.2"
|
||||
__version__ = "1.8.1"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
"""Agent-to-Agent (A2A) protocol communication module for CrewAI."""
|
||||
|
||||
from crewai.a2a.config import A2AConfig
|
||||
from crewai.a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
|
||||
|
||||
|
||||
__all__ = ["A2AConfig"]
|
||||
__all__ = [
|
||||
"A2AClientConfig",
|
||||
"A2AConfig",
|
||||
"A2AServerConfig",
|
||||
]
|
||||
|
||||
@@ -5,46 +5,75 @@ This module is separate from experimental.a2a to avoid circular imports.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
from importlib.metadata import version
|
||||
from typing import Any, ClassVar, Literal
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
BeforeValidator,
|
||||
Field,
|
||||
HttpUrl,
|
||||
TypeAdapter,
|
||||
)
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from typing_extensions import deprecated
|
||||
|
||||
from crewai.a2a.auth.schemas import AuthScheme
|
||||
from crewai.a2a.types import TransportType, Url
|
||||
|
||||
|
||||
http_url_adapter = TypeAdapter(HttpUrl)
|
||||
try:
|
||||
from a2a.types import (
|
||||
AgentCapabilities,
|
||||
AgentCardSignature,
|
||||
AgentInterface,
|
||||
AgentProvider,
|
||||
AgentSkill,
|
||||
SecurityScheme,
|
||||
)
|
||||
|
||||
Url = Annotated[
|
||||
str,
|
||||
BeforeValidator(
|
||||
lambda value: str(http_url_adapter.validate_python(value, strict=True))
|
||||
),
|
||||
]
|
||||
from crewai.a2a.updates import UpdateConfig
|
||||
except ImportError:
|
||||
UpdateConfig = Any
|
||||
AgentCapabilities = Any
|
||||
AgentCardSignature = Any
|
||||
AgentInterface = Any
|
||||
AgentProvider = Any
|
||||
SecurityScheme = Any
|
||||
AgentSkill = Any
|
||||
UpdateConfig = Any # type: ignore[misc,assignment]
|
||||
|
||||
|
||||
def _get_default_update_config() -> UpdateConfig:
|
||||
from crewai.a2a.updates import StreamingConfig
|
||||
|
||||
return StreamingConfig()
|
||||
|
||||
|
||||
@deprecated(
|
||||
"""
|
||||
`crewai.a2a.config.A2AConfig` is deprecated and will be removed in v2.0.0,
|
||||
use `crewai.a2a.config.A2AClientConfig` or `crewai.a2a.config.A2AServerConfig` instead.
|
||||
""",
|
||||
category=FutureWarning,
|
||||
)
|
||||
class A2AConfig(BaseModel):
|
||||
"""Configuration for A2A protocol integration.
|
||||
|
||||
Deprecated:
|
||||
Use A2AClientConfig instead. This class will be removed in a future version.
|
||||
|
||||
Attributes:
|
||||
endpoint: A2A agent endpoint URL.
|
||||
auth: Authentication scheme (Bearer, OAuth2, API Key, HTTP Basic/Digest).
|
||||
timeout: Request timeout in seconds (default: 120).
|
||||
max_turns: Maximum conversation turns with A2A agent (default: 10).
|
||||
auth: Authentication scheme.
|
||||
timeout: Request timeout in seconds.
|
||||
max_turns: Maximum conversation turns with A2A agent.
|
||||
response_model: Optional Pydantic model for structured A2A agent responses.
|
||||
fail_fast: If True, raise error when agent unreachable; if False, skip and continue (default: True).
|
||||
trust_remote_completion_status: If True, return A2A agent's result directly when status is "completed"; if False, always ask server agent to respond (default: False).
|
||||
fail_fast: If True, raise error when agent unreachable; if False, skip and continue.
|
||||
trust_remote_completion_status: If True, return A2A agent's result directly when completed.
|
||||
updates: Update mechanism config.
|
||||
transport_protocol: A2A transport protocol (grpc, jsonrpc, http+json).
|
||||
"""
|
||||
|
||||
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
|
||||
|
||||
endpoint: Url = Field(description="A2A agent endpoint URL")
|
||||
auth: AuthScheme | None = Field(
|
||||
default=None,
|
||||
description="Authentication scheme (Bearer, OAuth2, API Key, HTTP Basic/Digest)",
|
||||
description="Authentication scheme",
|
||||
)
|
||||
timeout: int = Field(default=120, description="Request timeout in seconds")
|
||||
max_turns: int = Field(
|
||||
@@ -52,13 +81,194 @@ class A2AConfig(BaseModel):
|
||||
)
|
||||
response_model: type[BaseModel] | None = Field(
|
||||
default=None,
|
||||
description="Optional Pydantic model for structured A2A agent responses. When specified, the A2A agent is expected to return JSON matching this schema.",
|
||||
description="Optional Pydantic model for structured A2A agent responses",
|
||||
)
|
||||
fail_fast: bool = Field(
|
||||
default=True,
|
||||
description="If True, raise an error immediately when the A2A agent is unreachable. If False, skip the A2A agent and continue execution.",
|
||||
description="If True, raise error when agent unreachable; if False, skip",
|
||||
)
|
||||
trust_remote_completion_status: bool = Field(
|
||||
default=False,
|
||||
description='If True, return the A2A agent\'s result directly when status is "completed" without asking the server agent to respond. If False, always ask the server agent to respond, allowing it to potentially delegate again.',
|
||||
description="If True, return A2A result directly when completed",
|
||||
)
|
||||
updates: UpdateConfig = Field(
|
||||
default_factory=_get_default_update_config,
|
||||
description="Update mechanism config",
|
||||
)
|
||||
transport_protocol: Literal["JSONRPC", "GRPC", "HTTP+JSON"] = Field(
|
||||
default="JSONRPC",
|
||||
description="Specified mode of A2A transport protocol",
|
||||
)
|
||||
|
||||
|
||||
class A2AClientConfig(BaseModel):
|
||||
"""Configuration for connecting to remote A2A agents.
|
||||
|
||||
Attributes:
|
||||
endpoint: A2A agent endpoint URL.
|
||||
auth: Authentication scheme.
|
||||
timeout: Request timeout in seconds.
|
||||
max_turns: Maximum conversation turns with A2A agent.
|
||||
response_model: Optional Pydantic model for structured A2A agent responses.
|
||||
fail_fast: If True, raise error when agent unreachable; if False, skip and continue.
|
||||
trust_remote_completion_status: If True, return A2A agent's result directly when completed.
|
||||
updates: Update mechanism config.
|
||||
accepted_output_modes: Media types the client can accept in responses.
|
||||
supported_transports: Ordered list of transport protocols the client supports.
|
||||
use_client_preference: Whether to prioritize client transport preferences over server.
|
||||
extensions: Extension URIs the client supports.
|
||||
"""
|
||||
|
||||
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
|
||||
|
||||
endpoint: Url = Field(description="A2A agent endpoint URL")
|
||||
auth: AuthScheme | None = Field(
|
||||
default=None,
|
||||
description="Authentication scheme",
|
||||
)
|
||||
timeout: int = Field(default=120, description="Request timeout in seconds")
|
||||
max_turns: int = Field(
|
||||
default=10, description="Maximum conversation turns with A2A agent"
|
||||
)
|
||||
response_model: type[BaseModel] | None = Field(
|
||||
default=None,
|
||||
description="Optional Pydantic model for structured A2A agent responses",
|
||||
)
|
||||
fail_fast: bool = Field(
|
||||
default=True,
|
||||
description="If True, raise error when agent unreachable; if False, skip",
|
||||
)
|
||||
trust_remote_completion_status: bool = Field(
|
||||
default=False,
|
||||
description="If True, return A2A result directly when completed",
|
||||
)
|
||||
updates: UpdateConfig = Field(
|
||||
default_factory=_get_default_update_config,
|
||||
description="Update mechanism config",
|
||||
)
|
||||
accepted_output_modes: list[str] = Field(
|
||||
default_factory=lambda: ["application/json"],
|
||||
description="Media types the client can accept in responses",
|
||||
)
|
||||
supported_transports: list[str] = Field(
|
||||
default_factory=lambda: ["JSONRPC"],
|
||||
description="Ordered list of transport protocols the client supports",
|
||||
)
|
||||
use_client_preference: bool = Field(
|
||||
default=False,
|
||||
description="Whether to prioritize client transport preferences over server",
|
||||
)
|
||||
extensions: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="Extension URIs the client supports",
|
||||
)
|
||||
transport_protocol: Literal["JSONRPC", "GRPC", "HTTP+JSON"] = Field(
|
||||
default="JSONRPC",
|
||||
description="Specified mode of A2A transport protocol",
|
||||
)
|
||||
|
||||
|
||||
class A2AServerConfig(BaseModel):
|
||||
"""Configuration for exposing a Crew or Agent as an A2A server.
|
||||
|
||||
All fields correspond to A2A AgentCard fields. Fields like name, description,
|
||||
and skills can be auto-derived from the Crew/Agent if not provided.
|
||||
|
||||
Attributes:
|
||||
name: Human-readable name for the agent.
|
||||
description: Human-readable description of the agent.
|
||||
version: Version string for the agent card.
|
||||
skills: List of agent skills/capabilities.
|
||||
default_input_modes: Default supported input MIME types.
|
||||
default_output_modes: Default supported output MIME types.
|
||||
capabilities: Declaration of optional capabilities.
|
||||
preferred_transport: Transport protocol for the preferred endpoint.
|
||||
protocol_version: A2A protocol version this agent supports.
|
||||
provider: Information about the agent's service provider.
|
||||
documentation_url: URL to the agent's documentation.
|
||||
icon_url: URL to an icon for the agent.
|
||||
additional_interfaces: Additional supported interfaces.
|
||||
security: Security requirement objects for all interactions.
|
||||
security_schemes: Security schemes available to authorize requests.
|
||||
supports_authenticated_extended_card: Whether agent provides extended card to authenticated users.
|
||||
url: Preferred endpoint URL for the agent.
|
||||
signatures: JSON Web Signatures for the AgentCard.
|
||||
"""
|
||||
|
||||
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
|
||||
|
||||
name: str | None = Field(
|
||||
default=None,
|
||||
description="Human-readable name for the agent. Auto-derived from Crew/Agent if not provided.",
|
||||
)
|
||||
description: str | None = Field(
|
||||
default=None,
|
||||
description="Human-readable description of the agent. Auto-derived from Crew/Agent if not provided.",
|
||||
)
|
||||
version: str = Field(
|
||||
default="1.0.0",
|
||||
description="Version string for the agent card",
|
||||
)
|
||||
skills: list[AgentSkill] = Field(
|
||||
default_factory=list,
|
||||
description="List of agent skills. Auto-derived from tasks/tools if not provided.",
|
||||
)
|
||||
default_input_modes: list[str] = Field(
|
||||
default_factory=lambda: ["text/plain", "application/json"],
|
||||
description="Default supported input MIME types",
|
||||
)
|
||||
default_output_modes: list[str] = Field(
|
||||
default_factory=lambda: ["text/plain", "application/json"],
|
||||
description="Default supported output MIME types",
|
||||
)
|
||||
capabilities: AgentCapabilities = Field(
|
||||
default_factory=lambda: AgentCapabilities(
|
||||
streaming=True,
|
||||
push_notifications=False,
|
||||
),
|
||||
description="Declaration of optional capabilities supported by the agent",
|
||||
)
|
||||
preferred_transport: TransportType = Field(
|
||||
default="JSONRPC",
|
||||
description="Transport protocol for the preferred endpoint",
|
||||
)
|
||||
protocol_version: str = Field(
|
||||
default_factory=lambda: version("a2a-sdk"),
|
||||
description="A2A protocol version this agent supports",
|
||||
)
|
||||
provider: AgentProvider | None = Field(
|
||||
default=None,
|
||||
description="Information about the agent's service provider",
|
||||
)
|
||||
documentation_url: Url | None = Field(
|
||||
default=None,
|
||||
description="URL to the agent's documentation",
|
||||
)
|
||||
icon_url: Url | None = Field(
|
||||
default=None,
|
||||
description="URL to an icon for the agent",
|
||||
)
|
||||
additional_interfaces: list[AgentInterface] = Field(
|
||||
default_factory=list,
|
||||
description="Additional supported interfaces (transport and URL combinations)",
|
||||
)
|
||||
security: list[dict[str, list[str]]] = Field(
|
||||
default_factory=list,
|
||||
description="Security requirement objects for all agent interactions",
|
||||
)
|
||||
security_schemes: dict[str, SecurityScheme] = Field(
|
||||
default_factory=dict,
|
||||
description="Security schemes available to authorize requests",
|
||||
)
|
||||
supports_authenticated_extended_card: bool = Field(
|
||||
default=False,
|
||||
description="Whether agent provides extended card to authenticated users",
|
||||
)
|
||||
url: Url | None = Field(
|
||||
default=None,
|
||||
description="Preferred endpoint URL for the agent. Set at runtime if not provided.",
|
||||
)
|
||||
signatures: list[AgentCardSignature] = Field(
|
||||
default_factory=list,
|
||||
description="JSON Web Signatures for the AgentCard",
|
||||
)
|
||||
|
||||
7
lib/crewai/src/crewai/a2a/errors.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""A2A protocol error types."""
|
||||
|
||||
from a2a.client.errors import A2AClientTimeoutError
|
||||
|
||||
|
||||
class A2APollingTimeoutError(A2AClientTimeoutError):
|
||||
"""Raised when polling exceeds the configured timeout."""
|
||||
470
lib/crewai/src/crewai/a2a/task_helpers.py
Normal file
@@ -0,0 +1,470 @@
|
||||
"""Helper functions for processing A2A task results."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import TYPE_CHECKING, Any, TypedDict
|
||||
import uuid
|
||||
|
||||
from a2a.client.errors import A2AClientHTTPError
|
||||
from a2a.types import (
|
||||
AgentCard,
|
||||
Message,
|
||||
Part,
|
||||
Role,
|
||||
Task,
|
||||
TaskArtifactUpdateEvent,
|
||||
TaskState,
|
||||
TaskStatusUpdateEvent,
|
||||
TextPart,
|
||||
)
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.a2a_events import (
|
||||
A2AConnectionErrorEvent,
|
||||
A2AResponseReceivedEvent,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from a2a.types import Task as A2ATask
|
||||
|
||||
SendMessageEvent = (
|
||||
tuple[Task, TaskStatusUpdateEvent | TaskArtifactUpdateEvent | None] | Message
|
||||
)
|
||||
|
||||
|
||||
TERMINAL_STATES: frozenset[TaskState] = frozenset(
|
||||
{
|
||||
TaskState.completed,
|
||||
TaskState.failed,
|
||||
TaskState.rejected,
|
||||
TaskState.canceled,
|
||||
}
|
||||
)
|
||||
|
||||
ACTIONABLE_STATES: frozenset[TaskState] = frozenset(
|
||||
{
|
||||
TaskState.input_required,
|
||||
TaskState.auth_required,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class TaskStateResult(TypedDict):
|
||||
"""Result dictionary from processing A2A task state."""
|
||||
|
||||
status: TaskState
|
||||
history: list[Message]
|
||||
result: NotRequired[str]
|
||||
error: NotRequired[str]
|
||||
agent_card: NotRequired[dict[str, Any]]
|
||||
a2a_agent_name: NotRequired[str | None]
|
||||
|
||||
|
||||
def extract_task_result_parts(a2a_task: A2ATask) -> list[str]:
|
||||
"""Extract result parts from A2A task status message, history, and artifacts.
|
||||
|
||||
Args:
|
||||
a2a_task: A2A Task object with status, history, and artifacts
|
||||
|
||||
Returns:
|
||||
List of result text parts
|
||||
"""
|
||||
result_parts: list[str] = []
|
||||
|
||||
if a2a_task.status and a2a_task.status.message:
|
||||
msg = a2a_task.status.message
|
||||
result_parts.extend(
|
||||
part.root.text for part in msg.parts if part.root.kind == "text"
|
||||
)
|
||||
|
||||
if not result_parts and a2a_task.history:
|
||||
for history_msg in reversed(a2a_task.history):
|
||||
if history_msg.role == Role.agent:
|
||||
result_parts.extend(
|
||||
part.root.text
|
||||
for part in history_msg.parts
|
||||
if part.root.kind == "text"
|
||||
)
|
||||
break
|
||||
|
||||
if a2a_task.artifacts:
|
||||
result_parts.extend(
|
||||
part.root.text
|
||||
for artifact in a2a_task.artifacts
|
||||
for part in artifact.parts
|
||||
if part.root.kind == "text"
|
||||
)
|
||||
|
||||
return result_parts
|
||||
|
||||
|
||||
def extract_error_message(a2a_task: A2ATask, default: str) -> str:
|
||||
"""Extract error message from A2A task.
|
||||
|
||||
Args:
|
||||
a2a_task: A2A Task object
|
||||
default: Default message if no error found
|
||||
|
||||
Returns:
|
||||
Error message string
|
||||
"""
|
||||
if a2a_task.status and a2a_task.status.message:
|
||||
msg = a2a_task.status.message
|
||||
if msg:
|
||||
for part in msg.parts:
|
||||
if part.root.kind == "text":
|
||||
return str(part.root.text)
|
||||
return str(msg)
|
||||
|
||||
if a2a_task.history:
|
||||
for history_msg in reversed(a2a_task.history):
|
||||
for part in history_msg.parts:
|
||||
if part.root.kind == "text":
|
||||
return str(part.root.text)
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def process_task_state(
|
||||
a2a_task: A2ATask,
|
||||
new_messages: list[Message],
|
||||
agent_card: AgentCard,
|
||||
turn_number: int,
|
||||
is_multiturn: bool,
|
||||
agent_role: str | None,
|
||||
result_parts: list[str] | None = None,
|
||||
endpoint: str | None = None,
|
||||
a2a_agent_name: str | None = None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
is_final: bool = True,
|
||||
) -> TaskStateResult | None:
|
||||
"""Process A2A task state and return result dictionary.
|
||||
|
||||
Shared logic for both polling and streaming handlers.
|
||||
|
||||
Args:
|
||||
a2a_task: The A2A task to process.
|
||||
new_messages: List to collect messages (modified in place).
|
||||
agent_card: The agent card.
|
||||
turn_number: Current turn number.
|
||||
is_multiturn: Whether multi-turn conversation.
|
||||
agent_role: Agent role for logging.
|
||||
result_parts: Accumulated result parts (streaming passes accumulated,
|
||||
polling passes None to extract from task).
|
||||
endpoint: A2A agent endpoint URL.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
from_task: Optional CrewAI Task for event metadata.
|
||||
from_agent: Optional CrewAI Agent for event metadata.
|
||||
is_final: Whether this is the final response in the stream.
|
||||
|
||||
Returns:
|
||||
Result dictionary if terminal/actionable state, None otherwise.
|
||||
"""
|
||||
if result_parts is None:
|
||||
result_parts = []
|
||||
|
||||
if a2a_task.status.state == TaskState.completed:
|
||||
if not result_parts:
|
||||
extracted_parts = extract_task_result_parts(a2a_task)
|
||||
result_parts.extend(extracted_parts)
|
||||
if a2a_task.history:
|
||||
new_messages.extend(a2a_task.history)
|
||||
|
||||
response_text = " ".join(result_parts) if result_parts else ""
|
||||
message_id = None
|
||||
if a2a_task.status and a2a_task.status.message:
|
||||
message_id = a2a_task.status.message.message_id
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AResponseReceivedEvent(
|
||||
response=response_text,
|
||||
turn_number=turn_number,
|
||||
context_id=a2a_task.context_id,
|
||||
message_id=message_id,
|
||||
is_multiturn=is_multiturn,
|
||||
status="completed",
|
||||
final=is_final,
|
||||
agent_role=agent_role,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
|
||||
return TaskStateResult(
|
||||
status=TaskState.completed,
|
||||
agent_card=agent_card.model_dump(exclude_none=True),
|
||||
result=response_text,
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
if a2a_task.status.state == TaskState.input_required:
|
||||
if a2a_task.history:
|
||||
new_messages.extend(a2a_task.history)
|
||||
|
||||
response_text = extract_error_message(a2a_task, "Additional input required")
|
||||
if response_text and not a2a_task.history:
|
||||
agent_message = Message(
|
||||
role=Role.agent,
|
||||
message_id=str(uuid.uuid4()),
|
||||
parts=[Part(root=TextPart(text=response_text))],
|
||||
context_id=a2a_task.context_id,
|
||||
task_id=a2a_task.id,
|
||||
)
|
||||
new_messages.append(agent_message)
|
||||
|
||||
input_message_id = None
|
||||
if a2a_task.status and a2a_task.status.message:
|
||||
input_message_id = a2a_task.status.message.message_id
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AResponseReceivedEvent(
|
||||
response=response_text,
|
||||
turn_number=turn_number,
|
||||
context_id=a2a_task.context_id,
|
||||
message_id=input_message_id,
|
||||
is_multiturn=is_multiturn,
|
||||
status="input_required",
|
||||
final=is_final,
|
||||
agent_role=agent_role,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
|
||||
return TaskStateResult(
|
||||
status=TaskState.input_required,
|
||||
error=response_text,
|
||||
history=new_messages,
|
||||
agent_card=agent_card.model_dump(exclude_none=True),
|
||||
)
|
||||
|
||||
if a2a_task.status.state in {TaskState.failed, TaskState.rejected}:
|
||||
error_msg = extract_error_message(a2a_task, "Task failed without error message")
|
||||
if a2a_task.history:
|
||||
new_messages.extend(a2a_task.history)
|
||||
return TaskStateResult(
|
||||
status=TaskState.failed,
|
||||
error=error_msg,
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
if a2a_task.status.state == TaskState.auth_required:
|
||||
error_msg = extract_error_message(a2a_task, "Authentication required")
|
||||
return TaskStateResult(
|
||||
status=TaskState.auth_required,
|
||||
error=error_msg,
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
if a2a_task.status.state == TaskState.canceled:
|
||||
error_msg = extract_error_message(a2a_task, "Task was canceled")
|
||||
return TaskStateResult(
|
||||
status=TaskState.canceled,
|
||||
error=error_msg,
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def send_message_and_get_task_id(
|
||||
event_stream: AsyncIterator[SendMessageEvent],
|
||||
new_messages: list[Message],
|
||||
agent_card: AgentCard,
|
||||
turn_number: int,
|
||||
is_multiturn: bool,
|
||||
agent_role: str | None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
endpoint: str | None = None,
|
||||
a2a_agent_name: str | None = None,
|
||||
context_id: str | None = None,
|
||||
) -> str | TaskStateResult:
|
||||
"""Send message and process initial response.
|
||||
|
||||
Handles the common pattern of sending a message and either:
|
||||
- Getting an immediate Message response (task completed synchronously)
|
||||
- Getting a Task that needs polling/waiting for completion
|
||||
|
||||
Args:
|
||||
event_stream: Async iterator from client.send_message()
|
||||
new_messages: List to collect messages (modified in place)
|
||||
agent_card: The agent card
|
||||
turn_number: Current turn number
|
||||
is_multiturn: Whether multi-turn conversation
|
||||
agent_role: Agent role for logging
|
||||
from_task: Optional CrewAI Task object for event metadata.
|
||||
from_agent: Optional CrewAI Agent object for event metadata.
|
||||
endpoint: Optional A2A endpoint URL.
|
||||
a2a_agent_name: Optional A2A agent name.
|
||||
context_id: Optional A2A context ID for correlation.
|
||||
|
||||
Returns:
|
||||
Task ID string if agent needs polling/waiting, or TaskStateResult if done.
|
||||
"""
|
||||
try:
|
||||
async for event in event_stream:
|
||||
if isinstance(event, Message):
|
||||
new_messages.append(event)
|
||||
result_parts = [
|
||||
part.root.text for part in event.parts if part.root.kind == "text"
|
||||
]
|
||||
response_text = " ".join(result_parts) if result_parts else ""
|
||||
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AResponseReceivedEvent(
|
||||
response=response_text,
|
||||
turn_number=turn_number,
|
||||
context_id=event.context_id,
|
||||
message_id=event.message_id,
|
||||
is_multiturn=is_multiturn,
|
||||
status="completed",
|
||||
final=True,
|
||||
agent_role=agent_role,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
|
||||
return TaskStateResult(
|
||||
status=TaskState.completed,
|
||||
result=response_text,
|
||||
history=new_messages,
|
||||
agent_card=agent_card.model_dump(exclude_none=True),
|
||||
)
|
||||
|
||||
if isinstance(event, tuple):
|
||||
a2a_task, _ = event
|
||||
|
||||
if a2a_task.status.state in TERMINAL_STATES | ACTIONABLE_STATES:
|
||||
result = process_task_state(
|
||||
a2a_task=a2a_task,
|
||||
new_messages=new_messages,
|
||||
agent_card=agent_card,
|
||||
turn_number=turn_number,
|
||||
is_multiturn=is_multiturn,
|
||||
agent_role=agent_role,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
)
|
||||
if result:
|
||||
return result
|
||||
|
||||
return a2a_task.id
|
||||
|
||||
return TaskStateResult(
|
||||
status=TaskState.failed,
|
||||
error="No task ID received from initial message",
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
except A2AClientHTTPError as e:
|
||||
error_msg = f"HTTP Error {e.status_code}: {e!s}"
|
||||
|
||||
error_message = Message(
|
||||
role=Role.agent,
|
||||
message_id=str(uuid.uuid4()),
|
||||
parts=[Part(root=TextPart(text=error_msg))],
|
||||
context_id=context_id,
|
||||
)
|
||||
new_messages.append(error_message)
|
||||
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AConnectionErrorEvent(
|
||||
endpoint=endpoint or "",
|
||||
error=str(e),
|
||||
error_type="http_error",
|
||||
status_code=e.status_code,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
operation="send_message",
|
||||
context_id=context_id,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AResponseReceivedEvent(
|
||||
response=error_msg,
|
||||
turn_number=turn_number,
|
||||
context_id=context_id,
|
||||
is_multiturn=is_multiturn,
|
||||
status="failed",
|
||||
final=True,
|
||||
agent_role=agent_role,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
return TaskStateResult(
|
||||
status=TaskState.failed,
|
||||
error=error_msg,
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error during send_message: {e!s}"
|
||||
|
||||
error_message = Message(
|
||||
role=Role.agent,
|
||||
message_id=str(uuid.uuid4()),
|
||||
parts=[Part(root=TextPart(text=error_msg))],
|
||||
context_id=context_id,
|
||||
)
|
||||
new_messages.append(error_message)
|
||||
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AConnectionErrorEvent(
|
||||
endpoint=endpoint or "",
|
||||
error=str(e),
|
||||
error_type="unexpected_error",
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
operation="send_message",
|
||||
context_id=context_id,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AResponseReceivedEvent(
|
||||
response=error_msg,
|
||||
turn_number=turn_number,
|
||||
context_id=context_id,
|
||||
is_multiturn=is_multiturn,
|
||||
status="failed",
|
||||
final=True,
|
||||
agent_role=agent_role,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
return TaskStateResult(
|
||||
status=TaskState.failed,
|
||||
error=error_msg,
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
finally:
|
||||
aclose = getattr(event_stream, "aclose", None)
|
||||
if aclose:
|
||||
await aclose()
|
||||
@@ -27,3 +27,14 @@ UNAVAILABLE_AGENTS_NOTICE_TEMPLATE: Final[Template] = Template(
|
||||
" $unavailable_agents"
|
||||
"\n</A2A_AGENTS_STATUS>\n"
|
||||
)
|
||||
REMOTE_AGENT_COMPLETED_NOTICE: Final[str] = """
|
||||
<REMOTE_AGENT_STATUS>
|
||||
STATUS: COMPLETED
|
||||
The remote agent has finished processing your request. Their response is in the conversation history above.
|
||||
You MUST now:
|
||||
1. Extract the answer from the conversation history
|
||||
2. Set is_a2a=false
|
||||
3. Return the answer as your final message
|
||||
DO NOT send another request - the task is already done.
|
||||
</REMOTE_AGENT_STATUS>
|
||||
"""
|
||||
|
||||
@@ -1,9 +1,41 @@
|
||||
"""Type definitions for A2A protocol message parts."""
|
||||
|
||||
from typing import Any, Literal, Protocol, TypedDict, runtime_checkable
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import (
|
||||
Annotated,
|
||||
Any,
|
||||
Literal,
|
||||
Protocol,
|
||||
TypedDict,
|
||||
runtime_checkable,
|
||||
)
|
||||
|
||||
from pydantic import BeforeValidator, HttpUrl, TypeAdapter
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
from crewai.a2a.updates import (
|
||||
PollingConfig,
|
||||
PollingHandler,
|
||||
PushNotificationConfig,
|
||||
PushNotificationHandler,
|
||||
StreamingConfig,
|
||||
StreamingHandler,
|
||||
UpdateConfig,
|
||||
)
|
||||
|
||||
|
||||
TransportType = Literal["JSONRPC", "GRPC", "HTTP+JSON"]
|
||||
|
||||
http_url_adapter: TypeAdapter[HttpUrl] = TypeAdapter(HttpUrl)
|
||||
|
||||
Url = Annotated[
|
||||
str,
|
||||
BeforeValidator(
|
||||
lambda value: str(http_url_adapter.validate_python(value, strict=True))
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class AgentResponseProtocol(Protocol):
|
||||
@@ -36,3 +68,16 @@ class PartsDict(TypedDict):
|
||||
|
||||
text: str
|
||||
metadata: NotRequired[PartsMetadataDict]
|
||||
|
||||
|
||||
PollingHandlerType = type[PollingHandler]
|
||||
StreamingHandlerType = type[StreamingHandler]
|
||||
PushNotificationHandlerType = type[PushNotificationHandler]
|
||||
|
||||
HandlerType = PollingHandlerType | StreamingHandlerType | PushNotificationHandlerType
|
||||
|
||||
HANDLER_REGISTRY: dict[type[UpdateConfig], HandlerType] = {
|
||||
PollingConfig: PollingHandler,
|
||||
StreamingConfig: StreamingHandler,
|
||||
PushNotificationConfig: PushNotificationHandler,
|
||||
}
|
||||
|
||||
35
lib/crewai/src/crewai/a2a/updates/__init__.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""A2A update mechanism configuration types."""
|
||||
|
||||
from crewai.a2a.updates.base import (
|
||||
BaseHandlerKwargs,
|
||||
PollingHandlerKwargs,
|
||||
PushNotificationHandlerKwargs,
|
||||
PushNotificationResultStore,
|
||||
StreamingHandlerKwargs,
|
||||
UpdateHandler,
|
||||
)
|
||||
from crewai.a2a.updates.polling.config import PollingConfig
|
||||
from crewai.a2a.updates.polling.handler import PollingHandler
|
||||
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
|
||||
from crewai.a2a.updates.push_notifications.handler import PushNotificationHandler
|
||||
from crewai.a2a.updates.streaming.config import StreamingConfig
|
||||
from crewai.a2a.updates.streaming.handler import StreamingHandler
|
||||
|
||||
|
||||
UpdateConfig = PollingConfig | StreamingConfig | PushNotificationConfig
|
||||
|
||||
__all__ = [
|
||||
"BaseHandlerKwargs",
|
||||
"PollingConfig",
|
||||
"PollingHandler",
|
||||
"PollingHandlerKwargs",
|
||||
"PushNotificationConfig",
|
||||
"PushNotificationHandler",
|
||||
"PushNotificationHandlerKwargs",
|
||||
"PushNotificationResultStore",
|
||||
"StreamingConfig",
|
||||
"StreamingHandler",
|
||||
"StreamingHandlerKwargs",
|
||||
"UpdateConfig",
|
||||
"UpdateHandler",
|
||||
]
|
||||
132
lib/crewai/src/crewai/a2a/updates/base.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""Base types for A2A update mechanism handlers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Protocol, TypedDict
|
||||
|
||||
from pydantic import GetCoreSchemaHandler
|
||||
from pydantic_core import CoreSchema, core_schema
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from a2a.client import Client
|
||||
from a2a.types import AgentCard, Message, Task
|
||||
|
||||
from crewai.a2a.task_helpers import TaskStateResult
|
||||
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
|
||||
|
||||
|
||||
class BaseHandlerKwargs(TypedDict, total=False):
|
||||
"""Base kwargs shared by all handlers."""
|
||||
|
||||
turn_number: int
|
||||
is_multiturn: bool
|
||||
agent_role: str | None
|
||||
context_id: str | None
|
||||
task_id: str | None
|
||||
endpoint: str | None
|
||||
agent_branch: Any
|
||||
a2a_agent_name: str | None
|
||||
from_task: Any
|
||||
from_agent: Any
|
||||
|
||||
|
||||
class PollingHandlerKwargs(BaseHandlerKwargs, total=False):
|
||||
"""Kwargs for polling handler."""
|
||||
|
||||
polling_interval: float
|
||||
polling_timeout: float
|
||||
history_length: int
|
||||
max_polls: int | None
|
||||
|
||||
|
||||
class StreamingHandlerKwargs(BaseHandlerKwargs, total=False):
|
||||
"""Kwargs for streaming handler."""
|
||||
|
||||
|
||||
class PushNotificationHandlerKwargs(BaseHandlerKwargs, total=False):
|
||||
"""Kwargs for push notification handler."""
|
||||
|
||||
config: PushNotificationConfig
|
||||
result_store: PushNotificationResultStore
|
||||
polling_timeout: float
|
||||
polling_interval: float
|
||||
|
||||
|
||||
class PushNotificationResultStore(Protocol):
|
||||
"""Protocol for storing and retrieving push notification results.
|
||||
|
||||
This protocol defines the interface for a result store that the
|
||||
PushNotificationHandler uses to wait for task completion.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls,
|
||||
source_type: Any,
|
||||
handler: GetCoreSchemaHandler,
|
||||
) -> CoreSchema:
|
||||
return core_schema.any_schema()
|
||||
|
||||
async def wait_for_result(
|
||||
self,
|
||||
task_id: str,
|
||||
timeout: float,
|
||||
poll_interval: float = 1.0,
|
||||
) -> Task | None:
|
||||
"""Wait for a task result to be available.
|
||||
|
||||
Args:
|
||||
task_id: The task ID to wait for.
|
||||
timeout: Max seconds to wait before returning None.
|
||||
poll_interval: Seconds between polling attempts.
|
||||
|
||||
Returns:
|
||||
The completed Task object, or None if timeout.
|
||||
"""
|
||||
...
|
||||
|
||||
async def get_result(self, task_id: str) -> Task | None:
|
||||
"""Get a task result if available.
|
||||
|
||||
Args:
|
||||
task_id: The task ID to retrieve.
|
||||
|
||||
Returns:
|
||||
The Task object if available, None otherwise.
|
||||
"""
|
||||
...
|
||||
|
||||
async def store_result(self, task: Task) -> None:
|
||||
"""Store a task result.
|
||||
|
||||
Args:
|
||||
task: The Task object to store.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class UpdateHandler(Protocol):
|
||||
"""Protocol for A2A update mechanism handlers."""
|
||||
|
||||
@staticmethod
|
||||
async def execute(
|
||||
client: Client,
|
||||
message: Message,
|
||||
new_messages: list[Message],
|
||||
agent_card: AgentCard,
|
||||
**kwargs: Any,
|
||||
) -> TaskStateResult:
|
||||
"""Execute the update mechanism and return result.
|
||||
|
||||
Args:
|
||||
client: A2A client instance.
|
||||
message: Message to send.
|
||||
new_messages: List to collect messages (modified in place).
|
||||
agent_card: The agent card.
|
||||
**kwargs: Additional handler-specific parameters.
|
||||
|
||||
Returns:
|
||||
Result dictionary with status, result/error, and history.
|
||||
"""
|
||||
...
|
||||
1
lib/crewai/src/crewai/a2a/updates/polling/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Polling update mechanism module."""
|
||||
25
lib/crewai/src/crewai/a2a/updates/polling/config.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""Polling update mechanism configuration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class PollingConfig(BaseModel):
|
||||
"""Configuration for polling-based task updates.
|
||||
|
||||
Attributes:
|
||||
interval: Seconds between poll attempts.
|
||||
timeout: Max seconds to poll before raising timeout error.
|
||||
max_polls: Max number of poll attempts.
|
||||
history_length: Number of messages to retrieve per poll.
|
||||
"""
|
||||
|
||||
interval: float = Field(
|
||||
default=2.0, gt=0, description="Seconds between poll attempts"
|
||||
)
|
||||
timeout: float | None = Field(default=None, gt=0, description="Max seconds to poll")
|
||||
max_polls: int | None = Field(default=None, gt=0, description="Max poll attempts")
|
||||
history_length: int = Field(
|
||||
default=100, gt=0, description="Messages to retrieve per poll"
|
||||
)
|
||||
359
lib/crewai/src/crewai/a2a/updates/polling/handler.py
Normal file
@@ -0,0 +1,359 @@
|
||||
"""Polling update mechanism handler."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import uuid
|
||||
|
||||
from a2a.client import Client
|
||||
from a2a.client.errors import A2AClientHTTPError
|
||||
from a2a.types import (
|
||||
AgentCard,
|
||||
Message,
|
||||
Part,
|
||||
Role,
|
||||
TaskQueryParams,
|
||||
TaskState,
|
||||
TextPart,
|
||||
)
|
||||
from typing_extensions import Unpack
|
||||
|
||||
from crewai.a2a.errors import A2APollingTimeoutError
|
||||
from crewai.a2a.task_helpers import (
|
||||
ACTIONABLE_STATES,
|
||||
TERMINAL_STATES,
|
||||
TaskStateResult,
|
||||
process_task_state,
|
||||
send_message_and_get_task_id,
|
||||
)
|
||||
from crewai.a2a.updates.base import PollingHandlerKwargs
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.a2a_events import (
|
||||
A2AConnectionErrorEvent,
|
||||
A2APollingStartedEvent,
|
||||
A2APollingStatusEvent,
|
||||
A2AResponseReceivedEvent,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from a2a.types import Task as A2ATask
|
||||
|
||||
|
||||
async def _poll_task_until_complete(
|
||||
client: Client,
|
||||
task_id: str,
|
||||
polling_interval: float,
|
||||
polling_timeout: float,
|
||||
agent_branch: Any | None = None,
|
||||
history_length: int = 100,
|
||||
max_polls: int | None = None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
context_id: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
a2a_agent_name: str | None = None,
|
||||
) -> A2ATask:
|
||||
"""Poll task status until terminal state reached.
|
||||
|
||||
Args:
|
||||
client: A2A client instance.
|
||||
task_id: Task ID to poll.
|
||||
polling_interval: Seconds between poll attempts.
|
||||
polling_timeout: Max seconds before timeout.
|
||||
agent_branch: Agent tree branch for logging.
|
||||
history_length: Number of messages to retrieve per poll.
|
||||
max_polls: Max number of poll attempts (None = unlimited).
|
||||
from_task: Optional CrewAI Task object for event metadata.
|
||||
from_agent: Optional CrewAI Agent object for event metadata.
|
||||
context_id: A2A context ID for correlation.
|
||||
endpoint: A2A agent endpoint URL.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
|
||||
Returns:
|
||||
Final task object in terminal state.
|
||||
|
||||
Raises:
|
||||
A2APollingTimeoutError: If polling exceeds timeout or max_polls.
|
||||
"""
|
||||
start_time = time.monotonic()
|
||||
poll_count = 0
|
||||
|
||||
while True:
|
||||
poll_count += 1
|
||||
task = await client.get_task(
|
||||
TaskQueryParams(id=task_id, history_length=history_length)
|
||||
)
|
||||
|
||||
elapsed = time.monotonic() - start_time
|
||||
effective_context_id = task.context_id or context_id
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2APollingStatusEvent(
|
||||
task_id=task_id,
|
||||
context_id=effective_context_id,
|
||||
state=str(task.status.state.value) if task.status.state else "unknown",
|
||||
elapsed_seconds=elapsed,
|
||||
poll_count=poll_count,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
|
||||
if task.status.state in TERMINAL_STATES | ACTIONABLE_STATES:
|
||||
return task
|
||||
|
||||
if elapsed > polling_timeout:
|
||||
raise A2APollingTimeoutError(
|
||||
f"Polling timeout after {polling_timeout}s ({poll_count} polls)"
|
||||
)
|
||||
|
||||
if max_polls and poll_count >= max_polls:
|
||||
raise A2APollingTimeoutError(
|
||||
f"Max polls ({max_polls}) exceeded after {elapsed:.1f}s"
|
||||
)
|
||||
|
||||
await asyncio.sleep(polling_interval)
|
||||
|
||||
|
||||
class PollingHandler:
|
||||
"""Polling-based update handler."""
|
||||
|
||||
@staticmethod
|
||||
async def execute(
|
||||
client: Client,
|
||||
message: Message,
|
||||
new_messages: list[Message],
|
||||
agent_card: AgentCard,
|
||||
**kwargs: Unpack[PollingHandlerKwargs],
|
||||
) -> TaskStateResult:
|
||||
"""Execute A2A delegation using polling for updates.
|
||||
|
||||
Args:
|
||||
client: A2A client instance.
|
||||
message: Message to send.
|
||||
new_messages: List to collect messages.
|
||||
agent_card: The agent card.
|
||||
**kwargs: Polling-specific parameters.
|
||||
|
||||
Returns:
|
||||
Dictionary with status, result/error, and history.
|
||||
"""
|
||||
polling_interval = kwargs.get("polling_interval", 2.0)
|
||||
polling_timeout = kwargs.get("polling_timeout", 300.0)
|
||||
endpoint = kwargs.get("endpoint", "")
|
||||
agent_branch = kwargs.get("agent_branch")
|
||||
turn_number = kwargs.get("turn_number", 0)
|
||||
is_multiturn = kwargs.get("is_multiturn", False)
|
||||
agent_role = kwargs.get("agent_role")
|
||||
history_length = kwargs.get("history_length", 100)
|
||||
max_polls = kwargs.get("max_polls")
|
||||
context_id = kwargs.get("context_id")
|
||||
task_id = kwargs.get("task_id")
|
||||
a2a_agent_name = kwargs.get("a2a_agent_name")
|
||||
from_task = kwargs.get("from_task")
|
||||
from_agent = kwargs.get("from_agent")
|
||||
|
||||
try:
|
||||
result_or_task_id = await send_message_and_get_task_id(
|
||||
event_stream=client.send_message(message),
|
||||
new_messages=new_messages,
|
||||
agent_card=agent_card,
|
||||
turn_number=turn_number,
|
||||
is_multiturn=is_multiturn,
|
||||
agent_role=agent_role,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
context_id=context_id,
|
||||
)
|
||||
|
||||
if not isinstance(result_or_task_id, str):
|
||||
return result_or_task_id
|
||||
|
||||
task_id = result_or_task_id
|
||||
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2APollingStartedEvent(
|
||||
task_id=task_id,
|
||||
context_id=context_id,
|
||||
polling_interval=polling_interval,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
|
||||
final_task = await _poll_task_until_complete(
|
||||
client=client,
|
||||
task_id=task_id,
|
||||
polling_interval=polling_interval,
|
||||
polling_timeout=polling_timeout,
|
||||
agent_branch=agent_branch,
|
||||
history_length=history_length,
|
||||
max_polls=max_polls,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
context_id=context_id,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
)
|
||||
|
||||
result = process_task_state(
|
||||
a2a_task=final_task,
|
||||
new_messages=new_messages,
|
||||
agent_card=agent_card,
|
||||
turn_number=turn_number,
|
||||
is_multiturn=is_multiturn,
|
||||
agent_role=agent_role,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
)
|
||||
if result:
|
||||
return result
|
||||
|
||||
return TaskStateResult(
|
||||
status=TaskState.failed,
|
||||
error=f"Unexpected task state: {final_task.status.state}",
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
except A2APollingTimeoutError as e:
|
||||
error_msg = str(e)
|
||||
|
||||
error_message = Message(
|
||||
role=Role.agent,
|
||||
message_id=str(uuid.uuid4()),
|
||||
parts=[Part(root=TextPart(text=error_msg))],
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
)
|
||||
new_messages.append(error_message)
|
||||
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AResponseReceivedEvent(
|
||||
response=error_msg,
|
||||
turn_number=turn_number,
|
||||
context_id=context_id,
|
||||
is_multiturn=is_multiturn,
|
||||
status="failed",
|
||||
final=True,
|
||||
agent_role=agent_role,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
return TaskStateResult(
|
||||
status=TaskState.failed,
|
||||
error=error_msg,
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
except A2AClientHTTPError as e:
|
||||
error_msg = f"HTTP Error {e.status_code}: {e!s}"
|
||||
|
||||
error_message = Message(
|
||||
role=Role.agent,
|
||||
message_id=str(uuid.uuid4()),
|
||||
parts=[Part(root=TextPart(text=error_msg))],
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
)
|
||||
new_messages.append(error_message)
|
||||
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AConnectionErrorEvent(
|
||||
endpoint=endpoint,
|
||||
error=str(e),
|
||||
error_type="http_error",
|
||||
status_code=e.status_code,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
operation="polling",
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AResponseReceivedEvent(
|
||||
response=error_msg,
|
||||
turn_number=turn_number,
|
||||
context_id=context_id,
|
||||
is_multiturn=is_multiturn,
|
||||
status="failed",
|
||||
final=True,
|
||||
agent_role=agent_role,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
return TaskStateResult(
|
||||
status=TaskState.failed,
|
||||
error=error_msg,
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error during polling: {e!s}"
|
||||
|
||||
error_message = Message(
|
||||
role=Role.agent,
|
||||
message_id=str(uuid.uuid4()),
|
||||
parts=[Part(root=TextPart(text=error_msg))],
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
)
|
||||
new_messages.append(error_message)
|
||||
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AConnectionErrorEvent(
|
||||
endpoint=endpoint or "",
|
||||
error=str(e),
|
||||
error_type="unexpected_error",
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
operation="polling",
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AResponseReceivedEvent(
|
||||
response=error_msg,
|
||||
turn_number=turn_number,
|
||||
context_id=context_id,
|
||||
is_multiturn=is_multiturn,
|
||||
status="failed",
|
||||
final=True,
|
||||
agent_role=agent_role,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
return TaskStateResult(
|
||||
status=TaskState.failed,
|
||||
error=error_msg,
|
||||
history=new_messages,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""Push notification update mechanism module."""
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Push notification update mechanism configuration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from a2a.types import PushNotificationAuthenticationInfo
|
||||
from pydantic import AnyHttpUrl, BaseModel, Field
|
||||
|
||||
from crewai.a2a.updates.base import PushNotificationResultStore
|
||||
|
||||
|
||||
class PushNotificationConfig(BaseModel):
|
||||
"""Configuration for webhook-based task updates.
|
||||
|
||||
Attributes:
|
||||
url: Callback URL where agent sends push notifications.
|
||||
id: Unique identifier for this config.
|
||||
token: Token to validate incoming notifications.
|
||||
authentication: Auth info for agent to use when calling webhook.
|
||||
timeout: Max seconds to wait for task completion.
|
||||
interval: Seconds between result polling attempts.
|
||||
result_store: Store for receiving push notification results.
|
||||
"""
|
||||
|
||||
url: AnyHttpUrl = Field(description="Callback URL for push notifications")
|
||||
id: str | None = Field(default=None, description="Unique config identifier")
|
||||
token: str | None = Field(default=None, description="Validation token")
|
||||
authentication: PushNotificationAuthenticationInfo | None = Field(
|
||||
default=None, description="Auth info for agent to use when calling webhook"
|
||||
)
|
||||
timeout: float | None = Field(
|
||||
default=300.0, gt=0, description="Max seconds to wait for task completion"
|
||||
)
|
||||
interval: float = Field(
|
||||
default=2.0, gt=0, description="Seconds between result polling attempts"
|
||||
)
|
||||
result_store: PushNotificationResultStore | None = Field(
|
||||
default=None, description="Result store for push notification handling"
|
||||
)
|
||||
362
lib/crewai/src/crewai/a2a/updates/push_notifications/handler.py
Normal file
@@ -0,0 +1,362 @@
|
||||
"""Push notification (webhook) update mechanism handler."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import uuid
|
||||
|
||||
from a2a.client import Client
|
||||
from a2a.client.errors import A2AClientHTTPError
|
||||
from a2a.types import (
|
||||
AgentCard,
|
||||
Message,
|
||||
Part,
|
||||
Role,
|
||||
TaskState,
|
||||
TextPart,
|
||||
)
|
||||
from typing_extensions import Unpack
|
||||
|
||||
from crewai.a2a.task_helpers import (
|
||||
TaskStateResult,
|
||||
process_task_state,
|
||||
send_message_and_get_task_id,
|
||||
)
|
||||
from crewai.a2a.updates.base import (
|
||||
PushNotificationHandlerKwargs,
|
||||
PushNotificationResultStore,
|
||||
)
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.a2a_events import (
|
||||
A2AConnectionErrorEvent,
|
||||
A2APushNotificationRegisteredEvent,
|
||||
A2APushNotificationTimeoutEvent,
|
||||
A2AResponseReceivedEvent,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from a2a.types import Task as A2ATask
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _wait_for_push_result(
|
||||
task_id: str,
|
||||
result_store: PushNotificationResultStore,
|
||||
timeout: float,
|
||||
poll_interval: float,
|
||||
agent_branch: Any | None = None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
context_id: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
a2a_agent_name: str | None = None,
|
||||
) -> A2ATask | None:
|
||||
"""Wait for push notification result.
|
||||
|
||||
Args:
|
||||
task_id: Task ID to wait for.
|
||||
result_store: Store to retrieve results from.
|
||||
timeout: Max seconds to wait.
|
||||
poll_interval: Seconds between polling attempts.
|
||||
agent_branch: Agent tree branch for logging.
|
||||
from_task: Optional CrewAI Task object for event metadata.
|
||||
from_agent: Optional CrewAI Agent object for event metadata.
|
||||
context_id: A2A context ID for correlation.
|
||||
endpoint: A2A agent endpoint URL.
|
||||
a2a_agent_name: Name of the A2A agent.
|
||||
|
||||
Returns:
|
||||
Final task object, or None if timeout.
|
||||
"""
|
||||
task = await result_store.wait_for_result(
|
||||
task_id=task_id,
|
||||
timeout=timeout,
|
||||
poll_interval=poll_interval,
|
||||
)
|
||||
|
||||
if task is None:
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2APushNotificationTimeoutEvent(
|
||||
task_id=task_id,
|
||||
context_id=context_id,
|
||||
timeout_seconds=timeout,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
|
||||
class PushNotificationHandler:
|
||||
"""Push notification (webhook) based update handler."""
|
||||
|
||||
@staticmethod
|
||||
async def execute(
|
||||
client: Client,
|
||||
message: Message,
|
||||
new_messages: list[Message],
|
||||
agent_card: AgentCard,
|
||||
**kwargs: Unpack[PushNotificationHandlerKwargs],
|
||||
) -> TaskStateResult:
|
||||
"""Execute A2A delegation using push notifications for updates.
|
||||
|
||||
Args:
|
||||
client: A2A client instance.
|
||||
message: Message to send.
|
||||
new_messages: List to collect messages.
|
||||
agent_card: The agent card.
|
||||
**kwargs: Push notification-specific parameters.
|
||||
|
||||
Returns:
|
||||
Dictionary with status, result/error, and history.
|
||||
|
||||
Raises:
|
||||
ValueError: If result_store or config not provided.
|
||||
"""
|
||||
config = kwargs.get("config")
|
||||
result_store = kwargs.get("result_store")
|
||||
polling_timeout = kwargs.get("polling_timeout", 300.0)
|
||||
polling_interval = kwargs.get("polling_interval", 2.0)
|
||||
agent_branch = kwargs.get("agent_branch")
|
||||
turn_number = kwargs.get("turn_number", 0)
|
||||
is_multiturn = kwargs.get("is_multiturn", False)
|
||||
agent_role = kwargs.get("agent_role")
|
||||
context_id = kwargs.get("context_id")
|
||||
task_id = kwargs.get("task_id")
|
||||
endpoint = kwargs.get("endpoint")
|
||||
a2a_agent_name = kwargs.get("a2a_agent_name")
|
||||
from_task = kwargs.get("from_task")
|
||||
from_agent = kwargs.get("from_agent")
|
||||
|
||||
if config is None:
|
||||
error_msg = (
|
||||
"PushNotificationConfig is required for push notification handler"
|
||||
)
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AConnectionErrorEvent(
|
||||
endpoint=endpoint or "",
|
||||
error=error_msg,
|
||||
error_type="configuration_error",
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
operation="push_notification",
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
return TaskStateResult(
|
||||
status=TaskState.failed,
|
||||
error=error_msg,
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
if result_store is None:
|
||||
error_msg = (
|
||||
"PushNotificationResultStore is required for push notification handler"
|
||||
)
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AConnectionErrorEvent(
|
||||
endpoint=endpoint or "",
|
||||
error=error_msg,
|
||||
error_type="configuration_error",
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
operation="push_notification",
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
return TaskStateResult(
|
||||
status=TaskState.failed,
|
||||
error=error_msg,
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
try:
|
||||
result_or_task_id = await send_message_and_get_task_id(
|
||||
event_stream=client.send_message(message),
|
||||
new_messages=new_messages,
|
||||
agent_card=agent_card,
|
||||
turn_number=turn_number,
|
||||
is_multiturn=is_multiturn,
|
||||
agent_role=agent_role,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
context_id=context_id,
|
||||
)
|
||||
|
||||
if not isinstance(result_or_task_id, str):
|
||||
return result_or_task_id
|
||||
|
||||
task_id = result_or_task_id
|
||||
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2APushNotificationRegisteredEvent(
|
||||
task_id=task_id,
|
||||
context_id=context_id,
|
||||
callback_url=str(config.url),
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"Push notification callback for task %s configured at %s (via initial request)",
|
||||
task_id,
|
||||
config.url,
|
||||
)
|
||||
|
||||
final_task = await _wait_for_push_result(
|
||||
task_id=task_id,
|
||||
result_store=result_store,
|
||||
timeout=polling_timeout,
|
||||
poll_interval=polling_interval,
|
||||
agent_branch=agent_branch,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
context_id=context_id,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
)
|
||||
|
||||
if final_task is None:
|
||||
return TaskStateResult(
|
||||
status=TaskState.failed,
|
||||
error=f"Push notification timeout after {polling_timeout}s",
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
result = process_task_state(
|
||||
a2a_task=final_task,
|
||||
new_messages=new_messages,
|
||||
agent_card=agent_card,
|
||||
turn_number=turn_number,
|
||||
is_multiturn=is_multiturn,
|
||||
agent_role=agent_role,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
)
|
||||
if result:
|
||||
return result
|
||||
|
||||
return TaskStateResult(
|
||||
status=TaskState.failed,
|
||||
error=f"Unexpected task state: {final_task.status.state}",
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
except A2AClientHTTPError as e:
|
||||
error_msg = f"HTTP Error {e.status_code}: {e!s}"
|
||||
|
||||
error_message = Message(
|
||||
role=Role.agent,
|
||||
message_id=str(uuid.uuid4()),
|
||||
parts=[Part(root=TextPart(text=error_msg))],
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
)
|
||||
new_messages.append(error_message)
|
||||
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AConnectionErrorEvent(
|
||||
endpoint=endpoint or "",
|
||||
error=str(e),
|
||||
error_type="http_error",
|
||||
status_code=e.status_code,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
operation="push_notification",
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AResponseReceivedEvent(
|
||||
response=error_msg,
|
||||
turn_number=turn_number,
|
||||
context_id=context_id,
|
||||
is_multiturn=is_multiturn,
|
||||
status="failed",
|
||||
final=True,
|
||||
agent_role=agent_role,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
return TaskStateResult(
|
||||
status=TaskState.failed,
|
||||
error=error_msg,
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error during push notification: {e!s}"
|
||||
|
||||
error_message = Message(
|
||||
role=Role.agent,
|
||||
message_id=str(uuid.uuid4()),
|
||||
parts=[Part(root=TextPart(text=error_msg))],
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
)
|
||||
new_messages.append(error_message)
|
||||
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AConnectionErrorEvent(
|
||||
endpoint=endpoint or "",
|
||||
error=str(e),
|
||||
error_type="unexpected_error",
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
operation="push_notification",
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AResponseReceivedEvent(
|
||||
response=error_msg,
|
||||
turn_number=turn_number,
|
||||
context_id=context_id,
|
||||
is_multiturn=is_multiturn,
|
||||
status="failed",
|
||||
final=True,
|
||||
agent_role=agent_role,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
return TaskStateResult(
|
||||
status=TaskState.failed,
|
||||
error=error_msg,
|
||||
history=new_messages,
|
||||
)
|
||||
1
lib/crewai/src/crewai/a2a/updates/streaming/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Streaming update mechanism module."""
|
||||
9
lib/crewai/src/crewai/a2a/updates/streaming/config.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Streaming update mechanism configuration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class StreamingConfig(BaseModel):
|
||||
"""Configuration for SSE-based task updates."""
|
||||
324
lib/crewai/src/crewai/a2a/updates/streaming/handler.py
Normal file
@@ -0,0 +1,324 @@
|
||||
"""Streaming (SSE) update mechanism handler."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from a2a.client import Client
|
||||
from a2a.client.errors import A2AClientHTTPError
|
||||
from a2a.types import (
|
||||
AgentCard,
|
||||
Message,
|
||||
Part,
|
||||
Role,
|
||||
TaskArtifactUpdateEvent,
|
||||
TaskState,
|
||||
TaskStatusUpdateEvent,
|
||||
TextPart,
|
||||
)
|
||||
from typing_extensions import Unpack
|
||||
|
||||
from crewai.a2a.task_helpers import (
|
||||
ACTIONABLE_STATES,
|
||||
TERMINAL_STATES,
|
||||
TaskStateResult,
|
||||
process_task_state,
|
||||
)
|
||||
from crewai.a2a.updates.base import StreamingHandlerKwargs
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.a2a_events import (
|
||||
A2AArtifactReceivedEvent,
|
||||
A2AConnectionErrorEvent,
|
||||
A2AResponseReceivedEvent,
|
||||
A2AStreamingChunkEvent,
|
||||
A2AStreamingStartedEvent,
|
||||
)
|
||||
|
||||
|
||||
class StreamingHandler:
|
||||
"""SSE streaming-based update handler."""
|
||||
|
||||
@staticmethod
|
||||
async def execute(
|
||||
client: Client,
|
||||
message: Message,
|
||||
new_messages: list[Message],
|
||||
agent_card: AgentCard,
|
||||
**kwargs: Unpack[StreamingHandlerKwargs],
|
||||
) -> TaskStateResult:
|
||||
"""Execute A2A delegation using SSE streaming for updates.
|
||||
|
||||
Args:
|
||||
client: A2A client instance.
|
||||
message: Message to send.
|
||||
new_messages: List to collect messages.
|
||||
agent_card: The agent card.
|
||||
**kwargs: Streaming-specific parameters.
|
||||
|
||||
Returns:
|
||||
Dictionary with status, result/error, and history.
|
||||
"""
|
||||
context_id = kwargs.get("context_id")
|
||||
task_id = kwargs.get("task_id")
|
||||
turn_number = kwargs.get("turn_number", 0)
|
||||
is_multiturn = kwargs.get("is_multiturn", False)
|
||||
agent_role = kwargs.get("agent_role")
|
||||
endpoint = kwargs.get("endpoint")
|
||||
a2a_agent_name = kwargs.get("a2a_agent_name")
|
||||
from_task = kwargs.get("from_task")
|
||||
from_agent = kwargs.get("from_agent")
|
||||
agent_branch = kwargs.get("agent_branch")
|
||||
|
||||
result_parts: list[str] = []
|
||||
final_result: TaskStateResult | None = None
|
||||
event_stream = client.send_message(message)
|
||||
chunk_index = 0
|
||||
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AStreamingStartedEvent(
|
||||
task_id=task_id,
|
||||
context_id=context_id,
|
||||
endpoint=endpoint or "",
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
turn_number=turn_number,
|
||||
is_multiturn=is_multiturn,
|
||||
agent_role=agent_role,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
async for event in event_stream:
|
||||
if isinstance(event, Message):
|
||||
new_messages.append(event)
|
||||
message_context_id = event.context_id or context_id
|
||||
for part in event.parts:
|
||||
if part.root.kind == "text":
|
||||
text = part.root.text
|
||||
result_parts.append(text)
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AStreamingChunkEvent(
|
||||
task_id=event.task_id or task_id,
|
||||
context_id=message_context_id,
|
||||
chunk=text,
|
||||
chunk_index=chunk_index,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
turn_number=turn_number,
|
||||
is_multiturn=is_multiturn,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
chunk_index += 1
|
||||
|
||||
elif isinstance(event, tuple):
|
||||
a2a_task, update = event
|
||||
|
||||
if isinstance(update, TaskArtifactUpdateEvent):
|
||||
artifact = update.artifact
|
||||
result_parts.extend(
|
||||
part.root.text
|
||||
for part in artifact.parts
|
||||
if part.root.kind == "text"
|
||||
)
|
||||
artifact_size = None
|
||||
if artifact.parts:
|
||||
artifact_size = sum(
|
||||
len(p.root.text.encode("utf-8"))
|
||||
if p.root.kind == "text"
|
||||
else len(getattr(p.root, "data", b""))
|
||||
for p in artifact.parts
|
||||
)
|
||||
effective_context_id = a2a_task.context_id or context_id
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AArtifactReceivedEvent(
|
||||
task_id=a2a_task.id,
|
||||
artifact_id=artifact.artifact_id,
|
||||
artifact_name=artifact.name,
|
||||
artifact_description=artifact.description,
|
||||
mime_type=artifact.parts[0].root.kind
|
||||
if artifact.parts
|
||||
else None,
|
||||
size_bytes=artifact_size,
|
||||
append=update.append or False,
|
||||
last_chunk=update.last_chunk or False,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
context_id=effective_context_id,
|
||||
turn_number=turn_number,
|
||||
is_multiturn=is_multiturn,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
|
||||
is_final_update = False
|
||||
if isinstance(update, TaskStatusUpdateEvent):
|
||||
is_final_update = update.final
|
||||
if (
|
||||
update.status
|
||||
and update.status.message
|
||||
and update.status.message.parts
|
||||
):
|
||||
result_parts.extend(
|
||||
part.root.text
|
||||
for part in update.status.message.parts
|
||||
if part.root.kind == "text" and part.root.text
|
||||
)
|
||||
|
||||
if (
|
||||
not is_final_update
|
||||
and a2a_task.status.state
|
||||
not in TERMINAL_STATES | ACTIONABLE_STATES
|
||||
):
|
||||
continue
|
||||
|
||||
final_result = process_task_state(
|
||||
a2a_task=a2a_task,
|
||||
new_messages=new_messages,
|
||||
agent_card=agent_card,
|
||||
turn_number=turn_number,
|
||||
is_multiturn=is_multiturn,
|
||||
agent_role=agent_role,
|
||||
result_parts=result_parts,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
is_final=is_final_update,
|
||||
)
|
||||
if final_result:
|
||||
break
|
||||
|
||||
except A2AClientHTTPError as e:
|
||||
error_msg = f"HTTP Error {e.status_code}: {e!s}"
|
||||
|
||||
error_message = Message(
|
||||
role=Role.agent,
|
||||
message_id=str(uuid.uuid4()),
|
||||
parts=[Part(root=TextPart(text=error_msg))],
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
)
|
||||
new_messages.append(error_message)
|
||||
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AConnectionErrorEvent(
|
||||
endpoint=endpoint or "",
|
||||
error=str(e),
|
||||
error_type="http_error",
|
||||
status_code=e.status_code,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
operation="streaming",
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AResponseReceivedEvent(
|
||||
response=error_msg,
|
||||
turn_number=turn_number,
|
||||
context_id=context_id,
|
||||
is_multiturn=is_multiturn,
|
||||
status="failed",
|
||||
final=True,
|
||||
agent_role=agent_role,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
return TaskStateResult(
|
||||
status=TaskState.failed,
|
||||
error=error_msg,
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error during streaming: {e!s}"
|
||||
|
||||
error_message = Message(
|
||||
role=Role.agent,
|
||||
message_id=str(uuid.uuid4()),
|
||||
parts=[Part(root=TextPart(text=error_msg))],
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
)
|
||||
new_messages.append(error_message)
|
||||
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AConnectionErrorEvent(
|
||||
endpoint=endpoint or "",
|
||||
error=str(e),
|
||||
error_type="unexpected_error",
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
operation="streaming",
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AResponseReceivedEvent(
|
||||
response=error_msg,
|
||||
turn_number=turn_number,
|
||||
context_id=context_id,
|
||||
is_multiturn=is_multiturn,
|
||||
status="failed",
|
||||
final=True,
|
||||
agent_role=agent_role,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
return TaskStateResult(
|
||||
status=TaskState.failed,
|
||||
error=error_msg,
|
||||
history=new_messages,
|
||||
)
|
||||
|
||||
finally:
|
||||
aclose = getattr(event_stream, "aclose", None)
|
||||
if aclose:
|
||||
try:
|
||||
await aclose()
|
||||
except Exception as close_error:
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AConnectionErrorEvent(
|
||||
endpoint=endpoint or "",
|
||||
error=str(close_error),
|
||||
error_type="stream_close_error",
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
operation="stream_close",
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
|
||||
if final_result:
|
||||
return final_result
|
||||
|
||||
return TaskStateResult(
|
||||
status=TaskState.completed,
|
||||
result=" ".join(result_parts) if result_parts else "",
|
||||
history=new_messages,
|
||||
agent_card=agent_card.model_dump(exclude_none=True),
|
||||
)
|
||||
@@ -1,832 +0,0 @@
|
||||
"""Utility functions for A2A (Agent-to-Agent) protocol delegation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, MutableMapping
|
||||
from contextlib import asynccontextmanager
|
||||
from functools import lru_cache
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import uuid
|
||||
|
||||
from a2a.client import Client, ClientConfig, ClientFactory
|
||||
from a2a.client.errors import A2AClientHTTPError
|
||||
from a2a.types import (
|
||||
AgentCard,
|
||||
Message,
|
||||
Part,
|
||||
Role,
|
||||
TaskArtifactUpdateEvent,
|
||||
TaskState,
|
||||
TaskStatusUpdateEvent,
|
||||
TextPart,
|
||||
TransportProtocol,
|
||||
)
|
||||
from aiocache import cached # type: ignore[import-untyped]
|
||||
from aiocache.serializers import PickleSerializer # type: ignore[import-untyped]
|
||||
import httpx
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
|
||||
from crewai.a2a.auth.schemas import APIKeyAuth, HTTPDigestAuth
|
||||
from crewai.a2a.auth.utils import (
|
||||
_auth_store,
|
||||
configure_auth_client,
|
||||
retry_on_401,
|
||||
validate_auth_against_agent_card,
|
||||
)
|
||||
from crewai.a2a.config import A2AConfig
|
||||
from crewai.a2a.types import PartsDict, PartsMetadataDict
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.a2a_events import (
|
||||
A2AConversationStartedEvent,
|
||||
A2ADelegationCompletedEvent,
|
||||
A2ADelegationStartedEvent,
|
||||
A2AMessageSentEvent,
|
||||
A2AResponseReceivedEvent,
|
||||
)
|
||||
from crewai.types.utils import create_literals_from_strings
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from a2a.types import Message, Task as A2ATask
|
||||
|
||||
from crewai.a2a.auth.schemas import AuthScheme
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def _fetch_agent_card_cached(
|
||||
endpoint: str,
|
||||
auth_hash: int,
|
||||
timeout: int,
|
||||
_ttl_hash: int,
|
||||
) -> AgentCard:
|
||||
"""Cached version of fetch_agent_card with auth support.
|
||||
|
||||
Args:
|
||||
endpoint: A2A agent endpoint URL
|
||||
auth_hash: Hash of the auth object
|
||||
timeout: Request timeout
|
||||
_ttl_hash: Time-based hash for cache invalidation
|
||||
|
||||
Returns:
|
||||
Cached AgentCard
|
||||
"""
|
||||
auth = _auth_store.get(auth_hash)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
return loop.run_until_complete(
|
||||
_fetch_agent_card_async(endpoint=endpoint, auth=auth, timeout=timeout)
|
||||
)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
def fetch_agent_card(
|
||||
endpoint: str,
|
||||
auth: AuthScheme | None = None,
|
||||
timeout: int = 30,
|
||||
use_cache: bool = True,
|
||||
cache_ttl: int = 300,
|
||||
) -> AgentCard:
|
||||
"""Fetch AgentCard from an A2A endpoint with optional caching.
|
||||
|
||||
Args:
|
||||
endpoint: A2A agent endpoint URL (AgentCard URL)
|
||||
auth: Optional AuthScheme for authentication
|
||||
timeout: Request timeout in seconds
|
||||
use_cache: Whether to use caching (default True)
|
||||
cache_ttl: Cache TTL in seconds (default 300 = 5 minutes)
|
||||
|
||||
Returns:
|
||||
AgentCard object with agent capabilities and skills
|
||||
|
||||
Raises:
|
||||
httpx.HTTPStatusError: If the request fails
|
||||
A2AClientHTTPError: If authentication fails
|
||||
"""
|
||||
if use_cache:
|
||||
if auth:
|
||||
auth_data = auth.model_dump_json(
|
||||
exclude={
|
||||
"_access_token",
|
||||
"_token_expires_at",
|
||||
"_refresh_token",
|
||||
"_authorization_callback",
|
||||
}
|
||||
)
|
||||
auth_hash = hash((type(auth).__name__, auth_data))
|
||||
else:
|
||||
auth_hash = 0
|
||||
_auth_store[auth_hash] = auth
|
||||
ttl_hash = int(time.time() // cache_ttl)
|
||||
return _fetch_agent_card_cached(endpoint, auth_hash, timeout, ttl_hash)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
return loop.run_until_complete(
|
||||
_fetch_agent_card_async(endpoint=endpoint, auth=auth, timeout=timeout)
|
||||
)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
@cached(ttl=300, serializer=PickleSerializer()) # type: ignore[untyped-decorator]
|
||||
async def _fetch_agent_card_async_cached(
|
||||
endpoint: str,
|
||||
auth_hash: int,
|
||||
timeout: int,
|
||||
) -> AgentCard:
|
||||
"""Cached async implementation of AgentCard fetching.
|
||||
|
||||
Args:
|
||||
endpoint: A2A agent endpoint URL
|
||||
auth_hash: Hash of the auth object
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
Cached AgentCard object
|
||||
"""
|
||||
auth = _auth_store.get(auth_hash)
|
||||
return await _fetch_agent_card_async(endpoint=endpoint, auth=auth, timeout=timeout)
|
||||
|
||||
|
||||
async def _fetch_agent_card_async(
|
||||
endpoint: str,
|
||||
auth: AuthScheme | None,
|
||||
timeout: int,
|
||||
) -> AgentCard:
|
||||
"""Async implementation of AgentCard fetching.
|
||||
|
||||
Args:
|
||||
endpoint: A2A agent endpoint URL
|
||||
auth: Optional AuthScheme for authentication
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
AgentCard object
|
||||
"""
|
||||
if "/.well-known/agent-card.json" in endpoint:
|
||||
base_url = endpoint.replace("/.well-known/agent-card.json", "")
|
||||
agent_card_path = "/.well-known/agent-card.json"
|
||||
else:
|
||||
url_parts = endpoint.split("/", 3)
|
||||
base_url = f"{url_parts[0]}//{url_parts[2]}"
|
||||
agent_card_path = f"/{url_parts[3]}" if len(url_parts) > 3 else "/"
|
||||
|
||||
headers: MutableMapping[str, str] = {}
|
||||
if auth:
|
||||
async with httpx.AsyncClient(timeout=timeout) as temp_auth_client:
|
||||
if isinstance(auth, (HTTPDigestAuth, APIKeyAuth)):
|
||||
configure_auth_client(auth, temp_auth_client)
|
||||
headers = await auth.apply_auth(temp_auth_client, {})
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout, headers=headers) as temp_client:
|
||||
if auth and isinstance(auth, (HTTPDigestAuth, APIKeyAuth)):
|
||||
configure_auth_client(auth, temp_client)
|
||||
|
||||
agent_card_url = f"{base_url}{agent_card_path}"
|
||||
|
||||
async def _fetch_agent_card_request() -> httpx.Response:
|
||||
return await temp_client.get(agent_card_url)
|
||||
|
||||
try:
|
||||
response = await retry_on_401(
|
||||
request_func=_fetch_agent_card_request,
|
||||
auth_scheme=auth,
|
||||
client=temp_client,
|
||||
headers=temp_client.headers,
|
||||
max_retries=2,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
return AgentCard.model_validate(response.json())
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
if e.response.status_code == 401:
|
||||
error_details = ["Authentication failed"]
|
||||
www_auth = e.response.headers.get("WWW-Authenticate")
|
||||
if www_auth:
|
||||
error_details.append(f"WWW-Authenticate: {www_auth}")
|
||||
if not auth:
|
||||
error_details.append("No auth scheme provided")
|
||||
msg = " | ".join(error_details)
|
||||
raise A2AClientHTTPError(401, msg) from e
|
||||
raise
|
||||
|
||||
|
||||
def execute_a2a_delegation(
|
||||
endpoint: str,
|
||||
auth: AuthScheme | None,
|
||||
timeout: int,
|
||||
task_description: str,
|
||||
context: str | None = None,
|
||||
context_id: str | None = None,
|
||||
task_id: str | None = None,
|
||||
reference_task_ids: list[str] | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
extensions: dict[str, Any] | None = None,
|
||||
conversation_history: list[Message] | None = None,
|
||||
agent_id: str | None = None,
|
||||
agent_role: Role | None = None,
|
||||
agent_branch: Any | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
turn_number: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Execute a task delegation to a remote A2A agent with multi-turn support.
|
||||
|
||||
Handles:
|
||||
- AgentCard discovery
|
||||
- Authentication setup
|
||||
- Message creation and sending
|
||||
- Response parsing
|
||||
- Multi-turn conversations
|
||||
|
||||
Args:
|
||||
endpoint: A2A agent endpoint URL (AgentCard URL)
|
||||
auth: Optional AuthScheme for authentication (Bearer, OAuth2, API Key, HTTP Basic/Digest)
|
||||
timeout: Request timeout in seconds
|
||||
task_description: The task to delegate
|
||||
context: Optional context information
|
||||
context_id: Context ID for correlating messages/tasks
|
||||
task_id: Specific task identifier
|
||||
reference_task_ids: List of related task IDs
|
||||
metadata: Additional metadata (external_id, request_id, etc.)
|
||||
extensions: Protocol extensions for custom fields
|
||||
conversation_history: Previous Message objects from conversation
|
||||
agent_id: Agent identifier for logging
|
||||
agent_role: Role of the CrewAI agent delegating the task
|
||||
agent_branch: Optional agent tree branch for logging
|
||||
response_model: Optional Pydantic model for structured outputs
|
||||
turn_number: Optional turn number for multi-turn conversations
|
||||
|
||||
Returns:
|
||||
Dictionary with:
|
||||
- status: "completed", "input_required", "failed", etc.
|
||||
- result: Result string (if completed)
|
||||
- error: Error message (if failed)
|
||||
- history: List of new Message objects from this exchange
|
||||
|
||||
Raises:
|
||||
ImportError: If a2a-sdk is not installed
|
||||
"""
|
||||
is_multiturn = bool(conversation_history and len(conversation_history) > 0)
|
||||
if turn_number is None:
|
||||
turn_number = (
|
||||
len([m for m in (conversation_history or []) if m.role == Role.user]) + 1
|
||||
)
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2ADelegationStartedEvent(
|
||||
endpoint=endpoint,
|
||||
task_description=task_description,
|
||||
agent_id=agent_id,
|
||||
is_multiturn=is_multiturn,
|
||||
turn_number=turn_number,
|
||||
),
|
||||
)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
result = loop.run_until_complete(
|
||||
_execute_a2a_delegation_async(
|
||||
endpoint=endpoint,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
task_description=task_description,
|
||||
context=context,
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
reference_task_ids=reference_task_ids,
|
||||
metadata=metadata,
|
||||
extensions=extensions,
|
||||
conversation_history=conversation_history or [],
|
||||
is_multiturn=is_multiturn,
|
||||
turn_number=turn_number,
|
||||
agent_branch=agent_branch,
|
||||
agent_id=agent_id,
|
||||
agent_role=agent_role,
|
||||
response_model=response_model,
|
||||
)
|
||||
)
|
||||
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2ADelegationCompletedEvent(
|
||||
status=result["status"],
|
||||
result=result.get("result"),
|
||||
error=result.get("error"),
|
||||
is_multiturn=is_multiturn,
|
||||
),
|
||||
)
|
||||
|
||||
return result
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
async def _execute_a2a_delegation_async(
|
||||
endpoint: str,
|
||||
auth: AuthScheme | None,
|
||||
timeout: int,
|
||||
task_description: str,
|
||||
context: str | None,
|
||||
context_id: str | None,
|
||||
task_id: str | None,
|
||||
reference_task_ids: list[str] | None,
|
||||
metadata: dict[str, Any] | None,
|
||||
extensions: dict[str, Any] | None,
|
||||
conversation_history: list[Message],
|
||||
is_multiturn: bool = False,
|
||||
turn_number: int = 1,
|
||||
agent_branch: Any | None = None,
|
||||
agent_id: str | None = None,
|
||||
agent_role: str | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Async implementation of A2A delegation with multi-turn support.
|
||||
|
||||
Args:
|
||||
endpoint: A2A agent endpoint URL
|
||||
auth: Optional AuthScheme for authentication
|
||||
timeout: Request timeout in seconds
|
||||
task_description: Task to delegate
|
||||
context: Optional context
|
||||
context_id: Context ID for correlation
|
||||
task_id: Specific task identifier
|
||||
reference_task_ids: Related task IDs
|
||||
metadata: Additional metadata
|
||||
extensions: Protocol extensions
|
||||
conversation_history: Previous Message objects
|
||||
is_multiturn: Whether this is a multi-turn conversation
|
||||
turn_number: Current turn number
|
||||
agent_branch: Agent tree branch for logging
|
||||
agent_id: Agent identifier for logging
|
||||
agent_role: Agent role for logging
|
||||
response_model: Optional Pydantic model for structured outputs
|
||||
|
||||
Returns:
|
||||
Dictionary with status, result/error, and new history
|
||||
"""
|
||||
if auth:
|
||||
auth_data = auth.model_dump_json(
|
||||
exclude={
|
||||
"_access_token",
|
||||
"_token_expires_at",
|
||||
"_refresh_token",
|
||||
"_authorization_callback",
|
||||
}
|
||||
)
|
||||
auth_hash = hash((type(auth).__name__, auth_data))
|
||||
else:
|
||||
auth_hash = 0
|
||||
_auth_store[auth_hash] = auth
|
||||
agent_card = await _fetch_agent_card_async_cached(
|
||||
endpoint=endpoint, auth_hash=auth_hash, timeout=timeout
|
||||
)
|
||||
|
||||
validate_auth_against_agent_card(agent_card, auth)
|
||||
|
||||
headers: MutableMapping[str, str] = {}
|
||||
if auth:
|
||||
async with httpx.AsyncClient(timeout=timeout) as temp_auth_client:
|
||||
if isinstance(auth, (HTTPDigestAuth, APIKeyAuth)):
|
||||
configure_auth_client(auth, temp_auth_client)
|
||||
headers = await auth.apply_auth(temp_auth_client, {})
|
||||
|
||||
a2a_agent_name = None
|
||||
if agent_card.name:
|
||||
a2a_agent_name = agent_card.name
|
||||
|
||||
if turn_number == 1:
|
||||
agent_id_for_event = agent_id or endpoint
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AConversationStartedEvent(
|
||||
agent_id=agent_id_for_event,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
),
|
||||
)
|
||||
|
||||
message_parts = []
|
||||
|
||||
if context:
|
||||
message_parts.append(f"Context:\n{context}\n\n")
|
||||
message_parts.append(f"{task_description}")
|
||||
message_text = "".join(message_parts)
|
||||
|
||||
if is_multiturn and conversation_history and not task_id:
|
||||
if first_task_id := conversation_history[0].task_id:
|
||||
task_id = first_task_id
|
||||
|
||||
parts: PartsDict = {"text": message_text}
|
||||
if response_model:
|
||||
parts.update(
|
||||
{
|
||||
"metadata": PartsMetadataDict(
|
||||
mimeType="application/json",
|
||||
schema=response_model.model_json_schema(),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
message = Message(
|
||||
role=Role.user,
|
||||
message_id=str(uuid.uuid4()),
|
||||
parts=[Part(root=TextPart(**parts))],
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
reference_task_ids=reference_task_ids,
|
||||
metadata=metadata,
|
||||
extensions=extensions,
|
||||
)
|
||||
|
||||
transport_protocol = TransportProtocol("JSONRPC")
|
||||
new_messages: list[Message] = [*conversation_history, message]
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AMessageSentEvent(
|
||||
message=message_text,
|
||||
turn_number=turn_number,
|
||||
is_multiturn=is_multiturn,
|
||||
agent_role=agent_role,
|
||||
),
|
||||
)
|
||||
|
||||
async with _create_a2a_client(
|
||||
agent_card=agent_card,
|
||||
transport_protocol=transport_protocol,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
streaming=True,
|
||||
auth=auth,
|
||||
) as client:
|
||||
result_parts: list[str] = []
|
||||
final_result: dict[str, Any] | None = None
|
||||
event_stream = client.send_message(message)
|
||||
|
||||
try:
|
||||
async for event in event_stream:
|
||||
if isinstance(event, Message):
|
||||
new_messages.append(event)
|
||||
for part in event.parts:
|
||||
if part.root.kind == "text":
|
||||
text = part.root.text
|
||||
result_parts.append(text)
|
||||
|
||||
elif isinstance(event, tuple):
|
||||
a2a_task, update = event
|
||||
|
||||
if isinstance(update, TaskArtifactUpdateEvent):
|
||||
artifact = update.artifact
|
||||
result_parts.extend(
|
||||
part.root.text
|
||||
for part in artifact.parts
|
||||
if part.root.kind == "text"
|
||||
)
|
||||
|
||||
is_final_update = False
|
||||
if isinstance(update, TaskStatusUpdateEvent):
|
||||
is_final_update = update.final
|
||||
|
||||
if not is_final_update and a2a_task.status.state not in [
|
||||
TaskState.completed,
|
||||
TaskState.input_required,
|
||||
TaskState.failed,
|
||||
TaskState.rejected,
|
||||
TaskState.auth_required,
|
||||
TaskState.canceled,
|
||||
]:
|
||||
continue
|
||||
|
||||
if a2a_task.status.state == TaskState.completed:
|
||||
extracted_parts = _extract_task_result_parts(a2a_task)
|
||||
result_parts.extend(extracted_parts)
|
||||
if a2a_task.history:
|
||||
new_messages.extend(a2a_task.history)
|
||||
|
||||
response_text = " ".join(result_parts) if result_parts else ""
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AResponseReceivedEvent(
|
||||
response=response_text,
|
||||
turn_number=turn_number,
|
||||
is_multiturn=is_multiturn,
|
||||
status="completed",
|
||||
agent_role=agent_role,
|
||||
),
|
||||
)
|
||||
|
||||
final_result = {
|
||||
"status": "completed",
|
||||
"result": response_text,
|
||||
"history": new_messages,
|
||||
"agent_card": agent_card,
|
||||
}
|
||||
break
|
||||
|
||||
if a2a_task.status.state == TaskState.input_required:
|
||||
if a2a_task.history:
|
||||
new_messages.extend(a2a_task.history)
|
||||
|
||||
response_text = _extract_error_message(
|
||||
a2a_task, "Additional input required"
|
||||
)
|
||||
if response_text and not a2a_task.history:
|
||||
agent_message = Message(
|
||||
role=Role.agent,
|
||||
message_id=str(uuid.uuid4()),
|
||||
parts=[Part(root=TextPart(text=response_text))],
|
||||
context_id=a2a_task.context_id
|
||||
if hasattr(a2a_task, "context_id")
|
||||
else None,
|
||||
task_id=a2a_task.task_id
|
||||
if hasattr(a2a_task, "task_id")
|
||||
else None,
|
||||
)
|
||||
new_messages.append(agent_message)
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AResponseReceivedEvent(
|
||||
response=response_text,
|
||||
turn_number=turn_number,
|
||||
is_multiturn=is_multiturn,
|
||||
status="input_required",
|
||||
agent_role=agent_role,
|
||||
),
|
||||
)
|
||||
|
||||
final_result = {
|
||||
"status": "input_required",
|
||||
"error": response_text,
|
||||
"history": new_messages,
|
||||
"agent_card": agent_card,
|
||||
}
|
||||
break
|
||||
|
||||
if a2a_task.status.state in [TaskState.failed, TaskState.rejected]:
|
||||
error_msg = _extract_error_message(
|
||||
a2a_task, "Task failed without error message"
|
||||
)
|
||||
if a2a_task.history:
|
||||
new_messages.extend(a2a_task.history)
|
||||
final_result = {
|
||||
"status": "failed",
|
||||
"error": error_msg,
|
||||
"history": new_messages,
|
||||
}
|
||||
break
|
||||
|
||||
if a2a_task.status.state == TaskState.auth_required:
|
||||
error_msg = _extract_error_message(
|
||||
a2a_task, "Authentication required"
|
||||
)
|
||||
final_result = {
|
||||
"status": "auth_required",
|
||||
"error": error_msg,
|
||||
"history": new_messages,
|
||||
}
|
||||
break
|
||||
|
||||
if a2a_task.status.state == TaskState.canceled:
|
||||
error_msg = _extract_error_message(
|
||||
a2a_task, "Task was canceled"
|
||||
)
|
||||
final_result = {
|
||||
"status": "canceled",
|
||||
"error": error_msg,
|
||||
"history": new_messages,
|
||||
}
|
||||
break
|
||||
except Exception as e:
|
||||
if isinstance(e, A2AClientHTTPError):
|
||||
error_msg = f"HTTP Error {e.status_code}: {e!s}"
|
||||
|
||||
error_message = Message(
|
||||
role=Role.agent,
|
||||
message_id=str(uuid.uuid4()),
|
||||
parts=[Part(root=TextPart(text=error_msg))],
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
)
|
||||
new_messages.append(error_message)
|
||||
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AResponseReceivedEvent(
|
||||
response=error_msg,
|
||||
turn_number=turn_number,
|
||||
is_multiturn=is_multiturn,
|
||||
status="failed",
|
||||
agent_role=agent_role,
|
||||
),
|
||||
)
|
||||
return {
|
||||
"status": "failed",
|
||||
"error": error_msg,
|
||||
"history": new_messages,
|
||||
}
|
||||
|
||||
current_exception: Exception | BaseException | None = e
|
||||
while current_exception:
|
||||
if hasattr(current_exception, "response"):
|
||||
response = current_exception.response
|
||||
if hasattr(response, "text"):
|
||||
break
|
||||
if current_exception and hasattr(current_exception, "__cause__"):
|
||||
current_exception = current_exception.__cause__
|
||||
raise
|
||||
finally:
|
||||
if hasattr(event_stream, "aclose"):
|
||||
await event_stream.aclose()
|
||||
|
||||
if final_result:
|
||||
return final_result
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
"result": " ".join(result_parts) if result_parts else "",
|
||||
"history": new_messages,
|
||||
}
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _create_a2a_client(
|
||||
agent_card: AgentCard,
|
||||
transport_protocol: TransportProtocol,
|
||||
timeout: int,
|
||||
headers: MutableMapping[str, str],
|
||||
streaming: bool,
|
||||
auth: AuthScheme | None = None,
|
||||
) -> AsyncIterator[Client]:
|
||||
"""Create and configure an A2A client.
|
||||
|
||||
Args:
|
||||
agent_card: The A2A agent card
|
||||
transport_protocol: Transport protocol to use
|
||||
timeout: Request timeout in seconds
|
||||
headers: HTTP headers (already with auth applied)
|
||||
streaming: Enable streaming responses
|
||||
auth: Optional AuthScheme for client configuration
|
||||
|
||||
Yields:
|
||||
Configured A2A client instance
|
||||
"""
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
) as httpx_client:
|
||||
if auth and isinstance(auth, (HTTPDigestAuth, APIKeyAuth)):
|
||||
configure_auth_client(auth, httpx_client)
|
||||
|
||||
config = ClientConfig(
|
||||
httpx_client=httpx_client,
|
||||
supported_transports=[str(transport_protocol.value)],
|
||||
streaming=streaming,
|
||||
accepted_output_modes=["application/json"],
|
||||
)
|
||||
|
||||
factory = ClientFactory(config)
|
||||
client = factory.create(agent_card)
|
||||
yield client
|
||||
|
||||
|
||||
def _extract_task_result_parts(a2a_task: A2ATask) -> list[str]:
|
||||
"""Extract result parts from A2A task history and artifacts.
|
||||
|
||||
Args:
|
||||
a2a_task: A2A Task object with history and artifacts
|
||||
|
||||
Returns:
|
||||
List of result text parts
|
||||
"""
|
||||
|
||||
result_parts: list[str] = []
|
||||
|
||||
if a2a_task.history:
|
||||
for history_msg in reversed(a2a_task.history):
|
||||
if history_msg.role == Role.agent:
|
||||
result_parts.extend(
|
||||
part.root.text
|
||||
for part in history_msg.parts
|
||||
if part.root.kind == "text"
|
||||
)
|
||||
break
|
||||
|
||||
if a2a_task.artifacts:
|
||||
result_parts.extend(
|
||||
part.root.text
|
||||
for artifact in a2a_task.artifacts
|
||||
for part in artifact.parts
|
||||
if part.root.kind == "text"
|
||||
)
|
||||
|
||||
return result_parts
|
||||
|
||||
|
||||
def _extract_error_message(a2a_task: A2ATask, default: str) -> str:
|
||||
"""Extract error message from A2A task.
|
||||
|
||||
Args:
|
||||
a2a_task: A2A Task object
|
||||
default: Default message if no error found
|
||||
|
||||
Returns:
|
||||
Error message string
|
||||
"""
|
||||
if a2a_task.status and a2a_task.status.message:
|
||||
msg = a2a_task.status.message
|
||||
if msg:
|
||||
for part in msg.parts:
|
||||
if part.root.kind == "text":
|
||||
return str(part.root.text)
|
||||
return str(msg)
|
||||
|
||||
if a2a_task.history:
|
||||
for history_msg in reversed(a2a_task.history):
|
||||
for part in history_msg.parts:
|
||||
if part.root.kind == "text":
|
||||
return str(part.root.text)
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def create_agent_response_model(agent_ids: tuple[str, ...]) -> type[BaseModel]:
|
||||
"""Create a dynamic AgentResponse model with Literal types for agent IDs.
|
||||
|
||||
Args:
|
||||
agent_ids: List of available A2A agent IDs
|
||||
|
||||
Returns:
|
||||
Dynamically created Pydantic model with Literal-constrained a2a_ids field
|
||||
"""
|
||||
|
||||
DynamicLiteral = create_literals_from_strings(agent_ids) # noqa: N806
|
||||
|
||||
return create_model(
|
||||
"AgentResponse",
|
||||
a2a_ids=(
|
||||
tuple[DynamicLiteral, ...], # type: ignore[valid-type]
|
||||
Field(
|
||||
default_factory=tuple,
|
||||
max_length=len(agent_ids),
|
||||
description="A2A agent IDs to delegate to.",
|
||||
),
|
||||
),
|
||||
message=(
|
||||
str,
|
||||
Field(
|
||||
description="The message content. If is_a2a=true, this is sent to the A2A agent. If is_a2a=false, this is your final answer ending the conversation."
|
||||
),
|
||||
),
|
||||
is_a2a=(
|
||||
bool,
|
||||
Field(
|
||||
description="Set to true to continue the conversation by sending this message to the A2A agent and awaiting their response. Set to false ONLY when you are completely done and providing your final answer (not when asking questions)."
|
||||
),
|
||||
),
|
||||
__base__=BaseModel,
|
||||
)
|
||||
|
||||
|
||||
def extract_a2a_agent_ids_from_config(
|
||||
a2a_config: list[A2AConfig] | A2AConfig | None,
|
||||
) -> tuple[list[A2AConfig], tuple[str, ...]]:
|
||||
"""Extract A2A agent IDs from A2A configuration.
|
||||
|
||||
Args:
|
||||
a2a_config: A2A configuration
|
||||
|
||||
Returns:
|
||||
List of A2A agent IDs
|
||||
"""
|
||||
if a2a_config is None:
|
||||
return [], ()
|
||||
|
||||
if isinstance(a2a_config, A2AConfig):
|
||||
a2a_agents = [a2a_config]
|
||||
else:
|
||||
a2a_agents = a2a_config
|
||||
return a2a_agents, tuple(config.endpoint for config in a2a_agents)
|
||||
|
||||
|
||||
def get_a2a_agents_and_response_model(
|
||||
a2a_config: list[A2AConfig] | A2AConfig | None,
|
||||
) -> tuple[list[A2AConfig], type[BaseModel]]:
|
||||
"""Get A2A agent IDs and response model.
|
||||
|
||||
Args:
|
||||
a2a_config: A2A configuration
|
||||
|
||||
Returns:
|
||||
Tuple of A2A agent IDs and response model
|
||||
"""
|
||||
a2a_agents, agent_ids = extract_a2a_agent_ids_from_config(a2a_config=a2a_config)
|
||||
|
||||
return a2a_agents, create_agent_response_model(agent_ids)
|
||||
1
lib/crewai/src/crewai/a2a/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""A2A utility modules for client operations."""
|
||||
513
lib/crewai/src/crewai/a2a/utils/agent_card.py
Normal file
@@ -0,0 +1,513 @@
|
||||
"""AgentCard utilities for A2A client and server operations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import MutableMapping
|
||||
from functools import lru_cache
|
||||
import time
|
||||
from types import MethodType
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from a2a.client.errors import A2AClientHTTPError
|
||||
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
|
||||
from aiocache import cached # type: ignore[import-untyped]
|
||||
from aiocache.serializers import PickleSerializer # type: ignore[import-untyped]
|
||||
import httpx
|
||||
|
||||
from crewai.a2a.auth.schemas import APIKeyAuth, HTTPDigestAuth
|
||||
from crewai.a2a.auth.utils import (
|
||||
_auth_store,
|
||||
configure_auth_client,
|
||||
retry_on_401,
|
||||
)
|
||||
from crewai.a2a.config import A2AServerConfig
|
||||
from crewai.crew import Crew
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.a2a_events import (
|
||||
A2AAgentCardFetchedEvent,
|
||||
A2AAuthenticationFailedEvent,
|
||||
A2AConnectionErrorEvent,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.a2a.auth.schemas import AuthScheme
|
||||
from crewai.agent import Agent
|
||||
from crewai.task import Task
|
||||
|
||||
|
||||
def _get_server_config(agent: Agent) -> A2AServerConfig | None:
|
||||
"""Get A2AServerConfig from an agent's a2a configuration.
|
||||
|
||||
Args:
|
||||
agent: The Agent instance to check.
|
||||
|
||||
Returns:
|
||||
A2AServerConfig if present, None otherwise.
|
||||
"""
|
||||
if agent.a2a is None:
|
||||
return None
|
||||
if isinstance(agent.a2a, A2AServerConfig):
|
||||
return agent.a2a
|
||||
if isinstance(agent.a2a, list):
|
||||
for config in agent.a2a:
|
||||
if isinstance(config, A2AServerConfig):
|
||||
return config
|
||||
return None
|
||||
|
||||
|
||||
def fetch_agent_card(
|
||||
endpoint: str,
|
||||
auth: AuthScheme | None = None,
|
||||
timeout: int = 30,
|
||||
use_cache: bool = True,
|
||||
cache_ttl: int = 300,
|
||||
) -> AgentCard:
|
||||
"""Fetch AgentCard from an A2A endpoint with optional caching.
|
||||
|
||||
Args:
|
||||
endpoint: A2A agent endpoint URL (AgentCard URL).
|
||||
auth: Optional AuthScheme for authentication.
|
||||
timeout: Request timeout in seconds.
|
||||
use_cache: Whether to use caching (default True).
|
||||
cache_ttl: Cache TTL in seconds (default 300 = 5 minutes).
|
||||
|
||||
Returns:
|
||||
AgentCard object with agent capabilities and skills.
|
||||
|
||||
Raises:
|
||||
httpx.HTTPStatusError: If the request fails.
|
||||
A2AClientHTTPError: If authentication fails.
|
||||
"""
|
||||
if use_cache:
|
||||
if auth:
|
||||
auth_data = auth.model_dump_json(
|
||||
exclude={
|
||||
"_access_token",
|
||||
"_token_expires_at",
|
||||
"_refresh_token",
|
||||
"_authorization_callback",
|
||||
}
|
||||
)
|
||||
auth_hash = hash((type(auth).__name__, auth_data))
|
||||
else:
|
||||
auth_hash = 0
|
||||
_auth_store[auth_hash] = auth
|
||||
ttl_hash = int(time.time() // cache_ttl)
|
||||
return _fetch_agent_card_cached(endpoint, auth_hash, timeout, ttl_hash)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
return loop.run_until_complete(
|
||||
afetch_agent_card(endpoint=endpoint, auth=auth, timeout=timeout)
|
||||
)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
async def afetch_agent_card(
|
||||
endpoint: str,
|
||||
auth: AuthScheme | None = None,
|
||||
timeout: int = 30,
|
||||
use_cache: bool = True,
|
||||
) -> AgentCard:
|
||||
"""Fetch AgentCard from an A2A endpoint asynchronously.
|
||||
|
||||
Native async implementation. Use this when running in an async context.
|
||||
|
||||
Args:
|
||||
endpoint: A2A agent endpoint URL (AgentCard URL).
|
||||
auth: Optional AuthScheme for authentication.
|
||||
timeout: Request timeout in seconds.
|
||||
use_cache: Whether to use caching (default True).
|
||||
|
||||
Returns:
|
||||
AgentCard object with agent capabilities and skills.
|
||||
|
||||
Raises:
|
||||
httpx.HTTPStatusError: If the request fails.
|
||||
A2AClientHTTPError: If authentication fails.
|
||||
"""
|
||||
if use_cache:
|
||||
if auth:
|
||||
auth_data = auth.model_dump_json(
|
||||
exclude={
|
||||
"_access_token",
|
||||
"_token_expires_at",
|
||||
"_refresh_token",
|
||||
"_authorization_callback",
|
||||
}
|
||||
)
|
||||
auth_hash = hash((type(auth).__name__, auth_data))
|
||||
else:
|
||||
auth_hash = 0
|
||||
_auth_store[auth_hash] = auth
|
||||
agent_card: AgentCard = await _afetch_agent_card_cached(
|
||||
endpoint, auth_hash, timeout
|
||||
)
|
||||
return agent_card
|
||||
|
||||
return await _afetch_agent_card_impl(endpoint=endpoint, auth=auth, timeout=timeout)
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def _fetch_agent_card_cached(
|
||||
endpoint: str,
|
||||
auth_hash: int,
|
||||
timeout: int,
|
||||
_ttl_hash: int,
|
||||
) -> AgentCard:
|
||||
"""Cached sync version of fetch_agent_card."""
|
||||
auth = _auth_store.get(auth_hash)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
return loop.run_until_complete(
|
||||
_afetch_agent_card_impl(endpoint=endpoint, auth=auth, timeout=timeout)
|
||||
)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
@cached(ttl=300, serializer=PickleSerializer()) # type: ignore[untyped-decorator]
|
||||
async def _afetch_agent_card_cached(
|
||||
endpoint: str,
|
||||
auth_hash: int,
|
||||
timeout: int,
|
||||
) -> AgentCard:
|
||||
"""Cached async implementation of AgentCard fetching."""
|
||||
auth = _auth_store.get(auth_hash)
|
||||
return await _afetch_agent_card_impl(endpoint=endpoint, auth=auth, timeout=timeout)
|
||||
|
||||
|
||||
async def _afetch_agent_card_impl(
|
||||
endpoint: str,
|
||||
auth: AuthScheme | None,
|
||||
timeout: int,
|
||||
) -> AgentCard:
|
||||
"""Internal async implementation of AgentCard fetching."""
|
||||
start_time = time.perf_counter()
|
||||
|
||||
if "/.well-known/agent-card.json" in endpoint:
|
||||
base_url = endpoint.replace("/.well-known/agent-card.json", "")
|
||||
agent_card_path = "/.well-known/agent-card.json"
|
||||
else:
|
||||
url_parts = endpoint.split("/", 3)
|
||||
base_url = f"{url_parts[0]}//{url_parts[2]}"
|
||||
agent_card_path = f"/{url_parts[3]}" if len(url_parts) > 3 else "/"
|
||||
|
||||
headers: MutableMapping[str, str] = {}
|
||||
if auth:
|
||||
async with httpx.AsyncClient(timeout=timeout) as temp_auth_client:
|
||||
if isinstance(auth, (HTTPDigestAuth, APIKeyAuth)):
|
||||
configure_auth_client(auth, temp_auth_client)
|
||||
headers = await auth.apply_auth(temp_auth_client, {})
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout, headers=headers) as temp_client:
|
||||
if auth and isinstance(auth, (HTTPDigestAuth, APIKeyAuth)):
|
||||
configure_auth_client(auth, temp_client)
|
||||
|
||||
agent_card_url = f"{base_url}{agent_card_path}"
|
||||
|
||||
async def _fetch_agent_card_request() -> httpx.Response:
|
||||
return await temp_client.get(agent_card_url)
|
||||
|
||||
try:
|
||||
response = await retry_on_401(
|
||||
request_func=_fetch_agent_card_request,
|
||||
auth_scheme=auth,
|
||||
client=temp_client,
|
||||
headers=temp_client.headers,
|
||||
max_retries=2,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
agent_card = AgentCard.model_validate(response.json())
|
||||
fetch_time_ms = (time.perf_counter() - start_time) * 1000
|
||||
agent_card_dict = agent_card.model_dump(exclude_none=True)
|
||||
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AAgentCardFetchedEvent(
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=agent_card.name,
|
||||
agent_card=agent_card_dict,
|
||||
protocol_version=agent_card.protocol_version,
|
||||
provider=agent_card_dict.get("provider"),
|
||||
cached=False,
|
||||
fetch_time_ms=fetch_time_ms,
|
||||
),
|
||||
)
|
||||
|
||||
return agent_card
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
||||
response_body = e.response.text[:1000] if e.response.text else None
|
||||
|
||||
if e.response.status_code == 401:
|
||||
error_details = ["Authentication failed"]
|
||||
www_auth = e.response.headers.get("WWW-Authenticate")
|
||||
if www_auth:
|
||||
error_details.append(f"WWW-Authenticate: {www_auth}")
|
||||
if not auth:
|
||||
error_details.append("No auth scheme provided")
|
||||
msg = " | ".join(error_details)
|
||||
|
||||
auth_type = type(auth).__name__ if auth else None
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AAuthenticationFailedEvent(
|
||||
endpoint=endpoint,
|
||||
auth_type=auth_type,
|
||||
error=msg,
|
||||
status_code=401,
|
||||
metadata={
|
||||
"elapsed_ms": elapsed_ms,
|
||||
"response_body": response_body,
|
||||
"www_authenticate": www_auth,
|
||||
"request_url": str(e.request.url),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
raise A2AClientHTTPError(401, msg) from e
|
||||
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AConnectionErrorEvent(
|
||||
endpoint=endpoint,
|
||||
error=str(e),
|
||||
error_type="http_error",
|
||||
status_code=e.response.status_code,
|
||||
operation="fetch_agent_card",
|
||||
metadata={
|
||||
"elapsed_ms": elapsed_ms,
|
||||
"response_body": response_body,
|
||||
"request_url": str(e.request.url),
|
||||
},
|
||||
),
|
||||
)
|
||||
raise
|
||||
|
||||
except httpx.TimeoutException as e:
|
||||
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AConnectionErrorEvent(
|
||||
endpoint=endpoint,
|
||||
error=str(e),
|
||||
error_type="timeout",
|
||||
operation="fetch_agent_card",
|
||||
metadata={
|
||||
"elapsed_ms": elapsed_ms,
|
||||
"timeout_config": timeout,
|
||||
"request_url": str(e.request.url) if e.request else None,
|
||||
},
|
||||
),
|
||||
)
|
||||
raise
|
||||
|
||||
except httpx.ConnectError as e:
|
||||
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AConnectionErrorEvent(
|
||||
endpoint=endpoint,
|
||||
error=str(e),
|
||||
error_type="connection_error",
|
||||
operation="fetch_agent_card",
|
||||
metadata={
|
||||
"elapsed_ms": elapsed_ms,
|
||||
"request_url": str(e.request.url) if e.request else None,
|
||||
},
|
||||
),
|
||||
)
|
||||
raise
|
||||
|
||||
except httpx.RequestError as e:
|
||||
elapsed_ms = (time.perf_counter() - start_time) * 1000
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AConnectionErrorEvent(
|
||||
endpoint=endpoint,
|
||||
error=str(e),
|
||||
error_type="request_error",
|
||||
operation="fetch_agent_card",
|
||||
metadata={
|
||||
"elapsed_ms": elapsed_ms,
|
||||
"request_url": str(e.request.url) if e.request else None,
|
||||
},
|
||||
),
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
def _task_to_skill(task: Task) -> AgentSkill:
|
||||
"""Convert a CrewAI Task to an A2A AgentSkill.
|
||||
|
||||
Args:
|
||||
task: The CrewAI Task to convert.
|
||||
|
||||
Returns:
|
||||
AgentSkill representing the task's capability.
|
||||
"""
|
||||
task_name = task.name or task.description[:50]
|
||||
task_id = task_name.lower().replace(" ", "_")
|
||||
|
||||
tags: list[str] = []
|
||||
if task.agent:
|
||||
tags.append(task.agent.role.lower().replace(" ", "-"))
|
||||
|
||||
return AgentSkill(
|
||||
id=task_id,
|
||||
name=task_name,
|
||||
description=task.description,
|
||||
tags=tags,
|
||||
examples=[task.expected_output] if task.expected_output else None,
|
||||
)
|
||||
|
||||
|
||||
def _tool_to_skill(tool_name: str, tool_description: str) -> AgentSkill:
|
||||
"""Convert an Agent's tool to an A2A AgentSkill.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool.
|
||||
tool_description: Description of what the tool does.
|
||||
|
||||
Returns:
|
||||
AgentSkill representing the tool's capability.
|
||||
"""
|
||||
tool_id = tool_name.lower().replace(" ", "_")
|
||||
|
||||
return AgentSkill(
|
||||
id=tool_id,
|
||||
name=tool_name,
|
||||
description=tool_description,
|
||||
tags=[tool_name.lower().replace(" ", "-")],
|
||||
)
|
||||
|
||||
|
||||
def _crew_to_agent_card(crew: Crew, url: str) -> AgentCard:
|
||||
"""Generate an A2A AgentCard from a Crew instance.
|
||||
|
||||
Args:
|
||||
crew: The Crew instance to generate a card for.
|
||||
url: The base URL where this crew will be exposed.
|
||||
|
||||
Returns:
|
||||
AgentCard describing the crew's capabilities.
|
||||
"""
|
||||
crew_name = getattr(crew, "name", None) or crew.__class__.__name__
|
||||
|
||||
description_parts: list[str] = []
|
||||
crew_description = getattr(crew, "description", None)
|
||||
if crew_description:
|
||||
description_parts.append(crew_description)
|
||||
else:
|
||||
agent_roles = [agent.role for agent in crew.agents]
|
||||
description_parts.append(
|
||||
f"A crew of {len(crew.agents)} agents: {', '.join(agent_roles)}"
|
||||
)
|
||||
|
||||
skills = [_task_to_skill(task) for task in crew.tasks]
|
||||
|
||||
return AgentCard(
|
||||
name=crew_name,
|
||||
description=" ".join(description_parts),
|
||||
url=url,
|
||||
version="1.0.0",
|
||||
capabilities=AgentCapabilities(
|
||||
streaming=True,
|
||||
push_notifications=True,
|
||||
),
|
||||
default_input_modes=["text/plain", "application/json"],
|
||||
default_output_modes=["text/plain", "application/json"],
|
||||
skills=skills,
|
||||
)
|
||||
|
||||
|
||||
def _agent_to_agent_card(agent: Agent, url: str) -> AgentCard:
|
||||
"""Generate an A2A AgentCard from an Agent instance.
|
||||
|
||||
Uses A2AServerConfig values when available, falling back to agent properties.
|
||||
|
||||
Args:
|
||||
agent: The Agent instance to generate a card for.
|
||||
url: The base URL where this agent will be exposed.
|
||||
|
||||
Returns:
|
||||
AgentCard describing the agent's capabilities.
|
||||
"""
|
||||
server_config = _get_server_config(agent) or A2AServerConfig()
|
||||
|
||||
name = server_config.name or agent.role
|
||||
|
||||
description_parts = [agent.goal]
|
||||
if agent.backstory:
|
||||
description_parts.append(agent.backstory)
|
||||
description = server_config.description or " ".join(description_parts)
|
||||
|
||||
skills: list[AgentSkill] = (
|
||||
server_config.skills.copy() if server_config.skills else []
|
||||
)
|
||||
|
||||
if not skills:
|
||||
if agent.tools:
|
||||
for tool in agent.tools:
|
||||
tool_name = getattr(tool, "name", None) or tool.__class__.__name__
|
||||
tool_desc = getattr(tool, "description", None) or f"Tool: {tool_name}"
|
||||
skills.append(_tool_to_skill(tool_name, tool_desc))
|
||||
|
||||
if not skills:
|
||||
skills.append(
|
||||
AgentSkill(
|
||||
id=agent.role.lower().replace(" ", "_"),
|
||||
name=agent.role,
|
||||
description=agent.goal,
|
||||
tags=[agent.role.lower().replace(" ", "-")],
|
||||
)
|
||||
)
|
||||
|
||||
return AgentCard(
|
||||
name=name,
|
||||
description=description,
|
||||
url=server_config.url or url,
|
||||
version=server_config.version,
|
||||
capabilities=server_config.capabilities,
|
||||
default_input_modes=server_config.default_input_modes,
|
||||
default_output_modes=server_config.default_output_modes,
|
||||
skills=skills,
|
||||
protocol_version=server_config.protocol_version,
|
||||
provider=server_config.provider,
|
||||
documentation_url=server_config.documentation_url,
|
||||
icon_url=server_config.icon_url,
|
||||
additional_interfaces=server_config.additional_interfaces,
|
||||
security=server_config.security,
|
||||
security_schemes=server_config.security_schemes,
|
||||
supports_authenticated_extended_card=server_config.supports_authenticated_extended_card,
|
||||
signatures=server_config.signatures,
|
||||
)
|
||||
|
||||
|
||||
def inject_a2a_server_methods(agent: Agent) -> None:
|
||||
"""Inject A2A server methods onto an Agent instance.
|
||||
|
||||
Adds a `to_agent_card(url: str) -> AgentCard` method to the agent
|
||||
that generates an A2A-compliant AgentCard.
|
||||
|
||||
Only injects if the agent has an A2AServerConfig.
|
||||
|
||||
Args:
|
||||
agent: The Agent instance to inject methods onto.
|
||||
"""
|
||||
if _get_server_config(agent) is None:
|
||||
return
|
||||
|
||||
def _to_agent_card(self: Agent, url: str) -> AgentCard:
|
||||
return _agent_to_agent_card(self, url)
|
||||
|
||||
object.__setattr__(agent, "to_agent_card", MethodType(_to_agent_card, agent))
|
||||
575
lib/crewai/src/crewai/a2a/utils/delegation.py
Normal file
@@ -0,0 +1,575 @@
|
||||
"""A2A delegation utilities for executing tasks on remote agents."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, MutableMapping
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
import uuid
|
||||
|
||||
from a2a.client import Client, ClientConfig, ClientFactory
|
||||
from a2a.types import (
|
||||
AgentCard,
|
||||
Message,
|
||||
Part,
|
||||
PushNotificationConfig as A2APushNotificationConfig,
|
||||
Role,
|
||||
TextPart,
|
||||
)
|
||||
import httpx
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai.a2a.auth.schemas import APIKeyAuth, HTTPDigestAuth
|
||||
from crewai.a2a.auth.utils import (
|
||||
_auth_store,
|
||||
configure_auth_client,
|
||||
validate_auth_against_agent_card,
|
||||
)
|
||||
from crewai.a2a.task_helpers import TaskStateResult
|
||||
from crewai.a2a.types import (
|
||||
HANDLER_REGISTRY,
|
||||
HandlerType,
|
||||
PartsDict,
|
||||
PartsMetadataDict,
|
||||
)
|
||||
from crewai.a2a.updates import (
|
||||
PollingConfig,
|
||||
PushNotificationConfig,
|
||||
StreamingHandler,
|
||||
UpdateConfig,
|
||||
)
|
||||
from crewai.a2a.utils.agent_card import _afetch_agent_card_cached
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.a2a_events import (
|
||||
A2AConversationStartedEvent,
|
||||
A2ADelegationCompletedEvent,
|
||||
A2ADelegationStartedEvent,
|
||||
A2AMessageSentEvent,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from a2a.types import Message
|
||||
|
||||
from crewai.a2a.auth.schemas import AuthScheme
|
||||
|
||||
|
||||
def get_handler(config: UpdateConfig | None) -> HandlerType:
|
||||
"""Get the handler class for a given update config.
|
||||
|
||||
Args:
|
||||
config: Update mechanism configuration.
|
||||
|
||||
Returns:
|
||||
Handler class for the config type, defaults to StreamingHandler.
|
||||
"""
|
||||
if config is None:
|
||||
return StreamingHandler
|
||||
return HANDLER_REGISTRY.get(type(config), StreamingHandler)
|
||||
|
||||
|
||||
def execute_a2a_delegation(
|
||||
endpoint: str,
|
||||
transport_protocol: Literal["JSONRPC", "GRPC", "HTTP+JSON"],
|
||||
auth: AuthScheme | None,
|
||||
timeout: int,
|
||||
task_description: str,
|
||||
context: str | None = None,
|
||||
context_id: str | None = None,
|
||||
task_id: str | None = None,
|
||||
reference_task_ids: list[str] | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
extensions: dict[str, Any] | None = None,
|
||||
conversation_history: list[Message] | None = None,
|
||||
agent_id: str | None = None,
|
||||
agent_role: Role | None = None,
|
||||
agent_branch: Any | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
turn_number: int | None = None,
|
||||
updates: UpdateConfig | None = None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
skill_id: str | None = None,
|
||||
) -> TaskStateResult:
|
||||
"""Execute a task delegation to a remote A2A agent synchronously.
|
||||
|
||||
This is the sync wrapper around aexecute_a2a_delegation. For async contexts,
|
||||
use aexecute_a2a_delegation directly.
|
||||
|
||||
Args:
|
||||
endpoint: A2A agent endpoint URL (AgentCard URL)
|
||||
transport_protocol: Optional A2A transport protocol (grpc, jsonrpc, http+json)
|
||||
auth: Optional AuthScheme for authentication (Bearer, OAuth2, API Key, HTTP Basic/Digest)
|
||||
timeout: Request timeout in seconds
|
||||
task_description: The task to delegate
|
||||
context: Optional context information
|
||||
context_id: Context ID for correlating messages/tasks
|
||||
task_id: Specific task identifier
|
||||
reference_task_ids: List of related task IDs
|
||||
metadata: Additional metadata (external_id, request_id, etc.)
|
||||
extensions: Protocol extensions for custom fields
|
||||
conversation_history: Previous Message objects from conversation
|
||||
agent_id: Agent identifier for logging
|
||||
agent_role: Role of the CrewAI agent delegating the task
|
||||
agent_branch: Optional agent tree branch for logging
|
||||
response_model: Optional Pydantic model for structured outputs
|
||||
turn_number: Optional turn number for multi-turn conversations
|
||||
endpoint: A2A agent endpoint URL.
|
||||
auth: Optional AuthScheme for authentication.
|
||||
timeout: Request timeout in seconds.
|
||||
task_description: The task to delegate.
|
||||
context: Optional context information.
|
||||
context_id: Context ID for correlating messages/tasks.
|
||||
task_id: Specific task identifier.
|
||||
reference_task_ids: List of related task IDs.
|
||||
metadata: Additional metadata.
|
||||
extensions: Protocol extensions for custom fields.
|
||||
conversation_history: Previous Message objects from conversation.
|
||||
agent_id: Agent identifier for logging.
|
||||
agent_role: Role of the CrewAI agent delegating the task.
|
||||
agent_branch: Optional agent tree branch for logging.
|
||||
response_model: Optional Pydantic model for structured outputs.
|
||||
turn_number: Optional turn number for multi-turn conversations.
|
||||
updates: Update mechanism config from A2AConfig.updates.
|
||||
from_task: Optional CrewAI Task object for event metadata.
|
||||
from_agent: Optional CrewAI Agent object for event metadata.
|
||||
skill_id: Optional skill ID to target a specific agent capability.
|
||||
|
||||
Returns:
|
||||
TaskStateResult with status, result/error, history, and agent_card.
|
||||
"""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
return loop.run_until_complete(
|
||||
aexecute_a2a_delegation(
|
||||
endpoint=endpoint,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
task_description=task_description,
|
||||
context=context,
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
reference_task_ids=reference_task_ids,
|
||||
metadata=metadata,
|
||||
extensions=extensions,
|
||||
conversation_history=conversation_history,
|
||||
agent_id=agent_id,
|
||||
agent_role=agent_role,
|
||||
agent_branch=agent_branch,
|
||||
response_model=response_model,
|
||||
transport_protocol=transport_protocol,
|
||||
turn_number=turn_number,
|
||||
updates=updates,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
skill_id=skill_id,
|
||||
)
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
async def aexecute_a2a_delegation(
|
||||
endpoint: str,
|
||||
transport_protocol: Literal["JSONRPC", "GRPC", "HTTP+JSON"],
|
||||
auth: AuthScheme | None,
|
||||
timeout: int,
|
||||
task_description: str,
|
||||
context: str | None = None,
|
||||
context_id: str | None = None,
|
||||
task_id: str | None = None,
|
||||
reference_task_ids: list[str] | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
extensions: dict[str, Any] | None = None,
|
||||
conversation_history: list[Message] | None = None,
|
||||
agent_id: str | None = None,
|
||||
agent_role: Role | None = None,
|
||||
agent_branch: Any | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
turn_number: int | None = None,
|
||||
updates: UpdateConfig | None = None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
skill_id: str | None = None,
|
||||
) -> TaskStateResult:
|
||||
"""Execute a task delegation to a remote A2A agent asynchronously.
|
||||
|
||||
Native async implementation with multi-turn support. Use this when running
|
||||
in an async context (e.g., with Crew.akickoff() or agent.aexecute_task()).
|
||||
|
||||
Args:
|
||||
endpoint: A2A agent endpoint URL
|
||||
transport_protocol: Optional A2A transport protocol (grpc, jsonrpc, http+json)
|
||||
auth: Optional AuthScheme for authentication
|
||||
timeout: Request timeout in seconds
|
||||
task_description: Task to delegate
|
||||
context: Optional context
|
||||
context_id: Context ID for correlation
|
||||
task_id: Specific task identifier
|
||||
reference_task_ids: Related task IDs
|
||||
metadata: Additional metadata
|
||||
extensions: Protocol extensions
|
||||
conversation_history: Previous Message objects
|
||||
turn_number: Current turn number
|
||||
agent_branch: Agent tree branch for logging
|
||||
agent_id: Agent identifier for logging
|
||||
agent_role: Agent role for logging
|
||||
response_model: Optional Pydantic model for structured outputs
|
||||
endpoint: A2A agent endpoint URL.
|
||||
auth: Optional AuthScheme for authentication.
|
||||
timeout: Request timeout in seconds.
|
||||
task_description: The task to delegate.
|
||||
context: Optional context information.
|
||||
context_id: Context ID for correlating messages/tasks.
|
||||
task_id: Specific task identifier.
|
||||
reference_task_ids: List of related task IDs.
|
||||
metadata: Additional metadata.
|
||||
extensions: Protocol extensions for custom fields.
|
||||
conversation_history: Previous Message objects from conversation.
|
||||
agent_id: Agent identifier for logging.
|
||||
agent_role: Role of the CrewAI agent delegating the task.
|
||||
agent_branch: Optional agent tree branch for logging.
|
||||
response_model: Optional Pydantic model for structured outputs.
|
||||
turn_number: Optional turn number for multi-turn conversations.
|
||||
updates: Update mechanism config from A2AConfig.updates.
|
||||
from_task: Optional CrewAI Task object for event metadata.
|
||||
from_agent: Optional CrewAI Agent object for event metadata.
|
||||
skill_id: Optional skill ID to target a specific agent capability.
|
||||
|
||||
Returns:
|
||||
TaskStateResult with status, result/error, history, and agent_card.
|
||||
"""
|
||||
if conversation_history is None:
|
||||
conversation_history = []
|
||||
|
||||
is_multiturn = len(conversation_history) > 0
|
||||
if turn_number is None:
|
||||
turn_number = len([m for m in conversation_history if m.role == Role.user]) + 1
|
||||
|
||||
result = await _aexecute_a2a_delegation_impl(
|
||||
endpoint=endpoint,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
task_description=task_description,
|
||||
context=context,
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
reference_task_ids=reference_task_ids,
|
||||
metadata=metadata,
|
||||
extensions=extensions,
|
||||
conversation_history=conversation_history,
|
||||
is_multiturn=is_multiturn,
|
||||
turn_number=turn_number,
|
||||
agent_branch=agent_branch,
|
||||
agent_id=agent_id,
|
||||
agent_role=agent_role,
|
||||
response_model=response_model,
|
||||
updates=updates,
|
||||
transport_protocol=transport_protocol,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
skill_id=skill_id,
|
||||
)
|
||||
|
||||
agent_card_data: dict[str, Any] = result.get("agent_card") or {}
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2ADelegationCompletedEvent(
|
||||
status=result["status"],
|
||||
result=result.get("result"),
|
||||
error=result.get("error"),
|
||||
context_id=context_id,
|
||||
is_multiturn=is_multiturn,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=result.get("a2a_agent_name"),
|
||||
agent_card=agent_card_data,
|
||||
provider=agent_card_data.get("provider"),
|
||||
metadata=metadata,
|
||||
extensions=list(extensions.keys()) if extensions else None,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def _aexecute_a2a_delegation_impl(
|
||||
endpoint: str,
|
||||
transport_protocol: Literal["JSONRPC", "GRPC", "HTTP+JSON"],
|
||||
auth: AuthScheme | None,
|
||||
timeout: int,
|
||||
task_description: str,
|
||||
context: str | None,
|
||||
context_id: str | None,
|
||||
task_id: str | None,
|
||||
reference_task_ids: list[str] | None,
|
||||
metadata: dict[str, Any] | None,
|
||||
extensions: dict[str, Any] | None,
|
||||
conversation_history: list[Message],
|
||||
is_multiturn: bool,
|
||||
turn_number: int,
|
||||
agent_branch: Any | None,
|
||||
agent_id: str | None,
|
||||
agent_role: str | None,
|
||||
response_model: type[BaseModel] | None,
|
||||
updates: UpdateConfig | None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
skill_id: str | None = None,
|
||||
) -> TaskStateResult:
|
||||
"""Internal async implementation of A2A delegation."""
|
||||
if auth:
|
||||
auth_data = auth.model_dump_json(
|
||||
exclude={
|
||||
"_access_token",
|
||||
"_token_expires_at",
|
||||
"_refresh_token",
|
||||
"_authorization_callback",
|
||||
}
|
||||
)
|
||||
auth_hash = hash((type(auth).__name__, auth_data))
|
||||
else:
|
||||
auth_hash = 0
|
||||
_auth_store[auth_hash] = auth
|
||||
agent_card = await _afetch_agent_card_cached(
|
||||
endpoint=endpoint, auth_hash=auth_hash, timeout=timeout
|
||||
)
|
||||
|
||||
validate_auth_against_agent_card(agent_card, auth)
|
||||
|
||||
headers: MutableMapping[str, str] = {}
|
||||
if auth:
|
||||
async with httpx.AsyncClient(timeout=timeout) as temp_auth_client:
|
||||
if isinstance(auth, (HTTPDigestAuth, APIKeyAuth)):
|
||||
configure_auth_client(auth, temp_auth_client)
|
||||
headers = await auth.apply_auth(temp_auth_client, {})
|
||||
|
||||
a2a_agent_name = None
|
||||
if agent_card.name:
|
||||
a2a_agent_name = agent_card.name
|
||||
|
||||
agent_card_dict = agent_card.model_dump(exclude_none=True)
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2ADelegationStartedEvent(
|
||||
endpoint=endpoint,
|
||||
task_description=task_description,
|
||||
agent_id=agent_id or endpoint,
|
||||
context_id=context_id,
|
||||
is_multiturn=is_multiturn,
|
||||
turn_number=turn_number,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
agent_card=agent_card_dict,
|
||||
protocol_version=agent_card.protocol_version,
|
||||
provider=agent_card_dict.get("provider"),
|
||||
skill_id=skill_id,
|
||||
metadata=metadata,
|
||||
extensions=list(extensions.keys()) if extensions else None,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
|
||||
if turn_number == 1:
|
||||
agent_id_for_event = agent_id or endpoint
|
||||
crewai_event_bus.emit(
|
||||
agent_branch,
|
||||
A2AConversationStartedEvent(
|
||||
agent_id=agent_id_for_event,
|
||||
endpoint=endpoint,
|
||||
context_id=context_id,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
agent_card=agent_card_dict,
|
||||
protocol_version=agent_card.protocol_version,
|
||||
provider=agent_card_dict.get("provider"),
|
||||
skill_id=skill_id,
|
||||
reference_task_ids=reference_task_ids,
|
||||
metadata=metadata,
|
||||
extensions=list(extensions.keys()) if extensions else None,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
|
||||
message_parts = []
|
||||
|
||||
if context:
|
||||
message_parts.append(f"Context:\n{context}\n\n")
|
||||
message_parts.append(f"{task_description}")
|
||||
message_text = "".join(message_parts)
|
||||
|
||||
if is_multiturn and conversation_history and not task_id:
|
||||
if first_task_id := conversation_history[0].task_id:
|
||||
task_id = first_task_id
|
||||
|
||||
parts: PartsDict = {"text": message_text}
|
||||
if response_model:
|
||||
parts.update(
|
||||
{
|
||||
"metadata": PartsMetadataDict(
|
||||
mimeType="application/json",
|
||||
schema=response_model.model_json_schema(),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
message_metadata = metadata.copy() if metadata else {}
|
||||
if skill_id:
|
||||
message_metadata["skill_id"] = skill_id
|
||||
|
||||
message = Message(
|
||||
role=Role.user,
|
||||
message_id=str(uuid.uuid4()),
|
||||
parts=[Part(root=TextPart(**parts))],
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
reference_task_ids=reference_task_ids,
|
||||
metadata=message_metadata if message_metadata else None,
|
||||
extensions=extensions,
|
||||
)
|
||||
|
||||
new_messages: list[Message] = [*conversation_history, message]
|
||||
crewai_event_bus.emit(
|
||||
None,
|
||||
A2AMessageSentEvent(
|
||||
message=message_text,
|
||||
turn_number=turn_number,
|
||||
context_id=context_id,
|
||||
message_id=message.message_id,
|
||||
is_multiturn=is_multiturn,
|
||||
agent_role=agent_role,
|
||||
endpoint=endpoint,
|
||||
a2a_agent_name=a2a_agent_name,
|
||||
skill_id=skill_id,
|
||||
metadata=message_metadata if message_metadata else None,
|
||||
extensions=list(extensions.keys()) if extensions else None,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
|
||||
handler = get_handler(updates)
|
||||
use_polling = isinstance(updates, PollingConfig)
|
||||
|
||||
handler_kwargs: dict[str, Any] = {
|
||||
"turn_number": turn_number,
|
||||
"is_multiturn": is_multiturn,
|
||||
"agent_role": agent_role,
|
||||
"context_id": context_id,
|
||||
"task_id": task_id,
|
||||
"endpoint": endpoint,
|
||||
"agent_branch": agent_branch,
|
||||
"a2a_agent_name": a2a_agent_name,
|
||||
"from_task": from_task,
|
||||
"from_agent": from_agent,
|
||||
}
|
||||
|
||||
if isinstance(updates, PollingConfig):
|
||||
handler_kwargs.update(
|
||||
{
|
||||
"polling_interval": updates.interval,
|
||||
"polling_timeout": updates.timeout or float(timeout),
|
||||
"history_length": updates.history_length,
|
||||
"max_polls": updates.max_polls,
|
||||
}
|
||||
)
|
||||
elif isinstance(updates, PushNotificationConfig):
|
||||
handler_kwargs.update(
|
||||
{
|
||||
"config": updates,
|
||||
"result_store": updates.result_store,
|
||||
"polling_timeout": updates.timeout or float(timeout),
|
||||
"polling_interval": updates.interval,
|
||||
}
|
||||
)
|
||||
|
||||
push_config_for_client = (
|
||||
updates if isinstance(updates, PushNotificationConfig) else None
|
||||
)
|
||||
|
||||
use_streaming = not use_polling and push_config_for_client is None
|
||||
|
||||
async with _create_a2a_client(
|
||||
agent_card=agent_card,
|
||||
transport_protocol=transport_protocol,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
streaming=use_streaming,
|
||||
auth=auth,
|
||||
use_polling=use_polling,
|
||||
push_notification_config=push_config_for_client,
|
||||
) as client:
|
||||
result = await handler.execute(
|
||||
client=client,
|
||||
message=message,
|
||||
new_messages=new_messages,
|
||||
agent_card=agent_card,
|
||||
**handler_kwargs,
|
||||
)
|
||||
result["a2a_agent_name"] = a2a_agent_name
|
||||
result["agent_card"] = agent_card.model_dump(exclude_none=True)
|
||||
return result
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _create_a2a_client(
|
||||
agent_card: AgentCard,
|
||||
transport_protocol: Literal["JSONRPC", "GRPC", "HTTP+JSON"],
|
||||
timeout: int,
|
||||
headers: MutableMapping[str, str],
|
||||
streaming: bool,
|
||||
auth: AuthScheme | None = None,
|
||||
use_polling: bool = False,
|
||||
push_notification_config: PushNotificationConfig | None = None,
|
||||
) -> AsyncIterator[Client]:
|
||||
"""Create and configure an A2A client.
|
||||
|
||||
Args:
|
||||
agent_card: The A2A agent card.
|
||||
transport_protocol: Transport protocol to use.
|
||||
timeout: Request timeout in seconds.
|
||||
headers: HTTP headers (already with auth applied).
|
||||
streaming: Enable streaming responses.
|
||||
auth: Optional AuthScheme for client configuration.
|
||||
use_polling: Enable polling mode.
|
||||
push_notification_config: Optional push notification config.
|
||||
|
||||
Yields:
|
||||
Configured A2A client instance.
|
||||
"""
|
||||
async with httpx.AsyncClient(
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
) as httpx_client:
|
||||
if auth and isinstance(auth, (HTTPDigestAuth, APIKeyAuth)):
|
||||
configure_auth_client(auth, httpx_client)
|
||||
|
||||
push_configs: list[A2APushNotificationConfig] = []
|
||||
if push_notification_config is not None:
|
||||
push_configs.append(
|
||||
A2APushNotificationConfig(
|
||||
url=str(push_notification_config.url),
|
||||
id=push_notification_config.id,
|
||||
token=push_notification_config.token,
|
||||
authentication=push_notification_config.authentication,
|
||||
)
|
||||
)
|
||||
|
||||
config = ClientConfig(
|
||||
httpx_client=httpx_client,
|
||||
supported_transports=[transport_protocol],
|
||||
streaming=streaming and not use_polling,
|
||||
polling=use_polling,
|
||||
accepted_output_modes=["application/json"],
|
||||
push_notification_configs=push_configs,
|
||||
)
|
||||
|
||||
factory = ClientFactory(config)
|
||||
client = factory.create(agent_card)
|
||||
yield client
|
||||
101
lib/crewai/src/crewai/a2a/utils/response_model.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""Response model utilities for A2A agent interactions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypeAlias
|
||||
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
|
||||
from crewai.a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
|
||||
from crewai.types.utils import create_literals_from_strings
|
||||
|
||||
|
||||
A2AConfigTypes: TypeAlias = A2AConfig | A2AServerConfig | A2AClientConfig
|
||||
A2AClientConfigTypes: TypeAlias = A2AConfig | A2AClientConfig
|
||||
|
||||
|
||||
def create_agent_response_model(agent_ids: tuple[str, ...]) -> type[BaseModel] | None:
|
||||
"""Create a dynamic AgentResponse model with Literal types for agent IDs.
|
||||
|
||||
Args:
|
||||
agent_ids: List of available A2A agent IDs.
|
||||
|
||||
Returns:
|
||||
Dynamically created Pydantic model with Literal-constrained a2a_ids field,
|
||||
or None if agent_ids is empty.
|
||||
"""
|
||||
if not agent_ids:
|
||||
return None
|
||||
|
||||
DynamicLiteral = create_literals_from_strings(agent_ids) # noqa: N806
|
||||
|
||||
return create_model(
|
||||
"AgentResponse",
|
||||
a2a_ids=(
|
||||
tuple[DynamicLiteral, ...], # type: ignore[valid-type]
|
||||
Field(
|
||||
default_factory=tuple,
|
||||
max_length=len(agent_ids),
|
||||
description="A2A agent IDs to delegate to.",
|
||||
),
|
||||
),
|
||||
message=(
|
||||
str,
|
||||
Field(
|
||||
description="The message content. If is_a2a=true, this is sent to the A2A agent. If is_a2a=false, this is your final answer ending the conversation."
|
||||
),
|
||||
),
|
||||
is_a2a=(
|
||||
bool,
|
||||
Field(
|
||||
description="Set to false when the remote agent has answered your question - extract their answer and return it as your final message. Set to true ONLY if you need to ask a NEW, DIFFERENT question. NEVER repeat the same request - if the conversation history shows the agent already answered, set is_a2a=false immediately."
|
||||
),
|
||||
),
|
||||
__base__=BaseModel,
|
||||
)
|
||||
|
||||
|
||||
def extract_a2a_agent_ids_from_config(
|
||||
a2a_config: list[A2AConfigTypes] | A2AConfigTypes | None,
|
||||
) -> tuple[list[A2AClientConfigTypes], tuple[str, ...]]:
|
||||
"""Extract A2A agent IDs from A2A configuration.
|
||||
|
||||
Filters out A2AServerConfig since it doesn't have an endpoint for delegation.
|
||||
|
||||
Args:
|
||||
a2a_config: A2A configuration (any type).
|
||||
|
||||
Returns:
|
||||
Tuple of client A2A configs list and agent endpoint IDs.
|
||||
"""
|
||||
if a2a_config is None:
|
||||
return [], ()
|
||||
|
||||
configs: list[A2AConfigTypes]
|
||||
if isinstance(a2a_config, (A2AConfig, A2AClientConfig, A2AServerConfig)):
|
||||
configs = [a2a_config]
|
||||
else:
|
||||
configs = a2a_config
|
||||
|
||||
# Filter to only client configs (those with endpoint)
|
||||
client_configs: list[A2AClientConfigTypes] = [
|
||||
config for config in configs if isinstance(config, (A2AConfig, A2AClientConfig))
|
||||
]
|
||||
|
||||
return client_configs, tuple(config.endpoint for config in client_configs)
|
||||
|
||||
|
||||
def get_a2a_agents_and_response_model(
|
||||
a2a_config: list[A2AConfigTypes] | A2AConfigTypes | None,
|
||||
) -> tuple[list[A2AClientConfigTypes], type[BaseModel] | None]:
|
||||
"""Get A2A agent configs and response model.
|
||||
|
||||
Args:
|
||||
a2a_config: A2A configuration (any type).
|
||||
|
||||
Returns:
|
||||
Tuple of client A2A configs and response model.
|
||||
"""
|
||||
a2a_agents, agent_ids = extract_a2a_agent_ids_from_config(a2a_config=a2a_config)
|
||||
|
||||
return a2a_agents, create_agent_response_model(agent_ids)
|
||||
399
lib/crewai/src/crewai/a2a/utils/task.py
Normal file
@@ -0,0 +1,399 @@
|
||||
"""A2A task utilities for server-side task management."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
from collections.abc import Callable, Coroutine
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from a2a.server.agent_execution import RequestContext
|
||||
from a2a.server.events import EventQueue
|
||||
from a2a.types import (
|
||||
InternalError,
|
||||
InvalidParamsError,
|
||||
Message,
|
||||
Task as A2ATask,
|
||||
TaskState,
|
||||
TaskStatus,
|
||||
TaskStatusUpdateEvent,
|
||||
)
|
||||
from a2a.utils import new_agent_text_message, new_text_artifact
|
||||
from a2a.utils.errors import ServerError
|
||||
from aiocache import SimpleMemoryCache, caches # type: ignore[import-untyped]
|
||||
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.a2a_events import (
|
||||
A2AServerTaskCanceledEvent,
|
||||
A2AServerTaskCompletedEvent,
|
||||
A2AServerTaskFailedEvent,
|
||||
A2AServerTaskStartedEvent,
|
||||
)
|
||||
from crewai.task import Task
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agent import Agent
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
P = ParamSpec("P")
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def _parse_redis_url(url: str) -> dict[str, Any]:
|
||||
"""Parse a Redis URL into aiocache configuration.
|
||||
|
||||
Args:
|
||||
url: Redis connection URL (e.g., redis://localhost:6379/0).
|
||||
|
||||
Returns:
|
||||
Configuration dict for aiocache.RedisCache.
|
||||
"""
|
||||
|
||||
parsed = urlparse(url)
|
||||
config: dict[str, Any] = {
|
||||
"cache": "aiocache.RedisCache",
|
||||
"endpoint": parsed.hostname or "localhost",
|
||||
"port": parsed.port or 6379,
|
||||
}
|
||||
if parsed.path and parsed.path != "/":
|
||||
try:
|
||||
config["db"] = int(parsed.path.lstrip("/"))
|
||||
except ValueError:
|
||||
pass
|
||||
if parsed.password:
|
||||
config["password"] = parsed.password
|
||||
return config
|
||||
|
||||
|
||||
_redis_url = os.environ.get("REDIS_URL")
|
||||
|
||||
caches.set_config(
|
||||
{
|
||||
"default": _parse_redis_url(_redis_url)
|
||||
if _redis_url
|
||||
else {
|
||||
"cache": "aiocache.SimpleMemoryCache",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def cancellable(
|
||||
fn: Callable[P, Coroutine[Any, Any, T]],
|
||||
) -> Callable[P, Coroutine[Any, Any, T]]:
|
||||
"""Decorator that enables cancellation for A2A task execution.
|
||||
|
||||
Runs a cancellation watcher concurrently with the wrapped function.
|
||||
When a cancel event is published, the execution is cancelled.
|
||||
|
||||
Args:
|
||||
fn: The async function to wrap.
|
||||
|
||||
Returns:
|
||||
Wrapped function with cancellation support.
|
||||
"""
|
||||
|
||||
@wraps(fn)
|
||||
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
||||
"""Wrap function with cancellation monitoring."""
|
||||
context: RequestContext | None = None
|
||||
for arg in args:
|
||||
if isinstance(arg, RequestContext):
|
||||
context = arg
|
||||
break
|
||||
if context is None:
|
||||
context = cast(RequestContext | None, kwargs.get("context"))
|
||||
|
||||
if context is None:
|
||||
return await fn(*args, **kwargs)
|
||||
|
||||
task_id = context.task_id
|
||||
cache = caches.get("default")
|
||||
|
||||
async def poll_for_cancel() -> bool:
|
||||
"""Poll cache for cancellation flag."""
|
||||
while True:
|
||||
if await cache.get(f"cancel:{task_id}"):
|
||||
return True
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def watch_for_cancel() -> bool:
|
||||
"""Watch for cancellation events via pub/sub or polling."""
|
||||
if isinstance(cache, SimpleMemoryCache):
|
||||
return await poll_for_cancel()
|
||||
|
||||
try:
|
||||
client = cache.client
|
||||
pubsub = client.pubsub()
|
||||
await pubsub.subscribe(f"cancel:{task_id}")
|
||||
async for message in pubsub.listen():
|
||||
if message["type"] == "message":
|
||||
return True
|
||||
except (OSError, ConnectionError) as e:
|
||||
logger.warning("Cancel watcher error for task_id=%s: %s", task_id, e)
|
||||
return await poll_for_cancel()
|
||||
return False
|
||||
|
||||
execute_task = asyncio.create_task(fn(*args, **kwargs))
|
||||
cancel_watch = asyncio.create_task(watch_for_cancel())
|
||||
|
||||
try:
|
||||
done, _ = await asyncio.wait(
|
||||
[execute_task, cancel_watch],
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
|
||||
if cancel_watch in done:
|
||||
execute_task.cancel()
|
||||
try:
|
||||
await execute_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
raise asyncio.CancelledError(f"Task {task_id} was cancelled")
|
||||
cancel_watch.cancel()
|
||||
return execute_task.result()
|
||||
finally:
|
||||
await cache.delete(f"cancel:{task_id}")
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@cancellable
|
||||
async def execute(
|
||||
agent: Agent,
|
||||
context: RequestContext,
|
||||
event_queue: EventQueue,
|
||||
) -> None:
|
||||
"""Execute an A2A task using a CrewAI agent.
|
||||
|
||||
Args:
|
||||
agent: The CrewAI agent to execute the task.
|
||||
context: The A2A request context containing the user's message.
|
||||
event_queue: The event queue for sending responses back.
|
||||
|
||||
TODOs:
|
||||
* need to impl both of structured output and file inputs, depends on `file_inputs` for
|
||||
`crewai.task.Task`, pass the below two to Task. both utils in `a2a.utils.parts`
|
||||
* structured outputs ingestion, `structured_inputs = get_data_parts(parts=context.message.parts)`
|
||||
* file inputs ingestion, `file_inputs = get_file_parts(parts=context.message.parts)`
|
||||
"""
|
||||
|
||||
user_message = context.get_user_input()
|
||||
task_id = context.task_id
|
||||
context_id = context.context_id
|
||||
if task_id is None or context_id is None:
|
||||
msg = "task_id and context_id are required"
|
||||
crewai_event_bus.emit(
|
||||
agent,
|
||||
A2AServerTaskFailedEvent(
|
||||
task_id="",
|
||||
context_id="",
|
||||
error=msg,
|
||||
from_agent=agent,
|
||||
),
|
||||
)
|
||||
raise ServerError(InvalidParamsError(message=msg)) from None
|
||||
|
||||
task = Task(
|
||||
description=user_message,
|
||||
expected_output="Response to the user's request",
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
crewai_event_bus.emit(
|
||||
agent,
|
||||
A2AServerTaskStartedEvent(
|
||||
task_id=task_id,
|
||||
context_id=context_id,
|
||||
from_task=task,
|
||||
from_agent=agent,
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
result = await agent.aexecute_task(task=task, tools=agent.tools)
|
||||
result_str = str(result)
|
||||
history: list[Message] = [context.message] if context.message else []
|
||||
history.append(new_agent_text_message(result_str, context_id, task_id))
|
||||
await event_queue.enqueue_event(
|
||||
A2ATask(
|
||||
id=task_id,
|
||||
context_id=context_id,
|
||||
status=TaskStatus(state=TaskState.input_required),
|
||||
artifacts=[new_text_artifact(result_str, f"result_{task_id}")],
|
||||
history=history,
|
||||
)
|
||||
)
|
||||
crewai_event_bus.emit(
|
||||
agent,
|
||||
A2AServerTaskCompletedEvent(
|
||||
task_id=task_id,
|
||||
context_id=context_id,
|
||||
result=str(result),
|
||||
from_task=task,
|
||||
from_agent=agent,
|
||||
),
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
crewai_event_bus.emit(
|
||||
agent,
|
||||
A2AServerTaskCanceledEvent(
|
||||
task_id=task_id,
|
||||
context_id=context_id,
|
||||
from_task=task,
|
||||
from_agent=agent,
|
||||
),
|
||||
)
|
||||
raise
|
||||
except Exception as e:
|
||||
crewai_event_bus.emit(
|
||||
agent,
|
||||
A2AServerTaskFailedEvent(
|
||||
task_id=task_id,
|
||||
context_id=context_id,
|
||||
error=str(e),
|
||||
from_task=task,
|
||||
from_agent=agent,
|
||||
),
|
||||
)
|
||||
raise ServerError(
|
||||
error=InternalError(message=f"Task execution failed: {e}")
|
||||
) from e
|
||||
|
||||
|
||||
async def cancel(
|
||||
context: RequestContext,
|
||||
event_queue: EventQueue,
|
||||
) -> A2ATask | None:
|
||||
"""Cancel an A2A task.
|
||||
|
||||
Publishes a cancel event that the cancellable decorator listens for.
|
||||
|
||||
Args:
|
||||
context: The A2A request context containing task information.
|
||||
event_queue: The event queue for sending the cancellation status.
|
||||
|
||||
Returns:
|
||||
The canceled task with updated status.
|
||||
"""
|
||||
task_id = context.task_id
|
||||
context_id = context.context_id
|
||||
if task_id is None or context_id is None:
|
||||
raise ServerError(InvalidParamsError(message="task_id and context_id required"))
|
||||
|
||||
if context.current_task and context.current_task.status.state in (
|
||||
TaskState.completed,
|
||||
TaskState.failed,
|
||||
TaskState.canceled,
|
||||
):
|
||||
return context.current_task
|
||||
|
||||
cache = caches.get("default")
|
||||
|
||||
await cache.set(f"cancel:{task_id}", True, ttl=3600)
|
||||
if not isinstance(cache, SimpleMemoryCache):
|
||||
await cache.client.publish(f"cancel:{task_id}", "cancel")
|
||||
|
||||
await event_queue.enqueue_event(
|
||||
TaskStatusUpdateEvent(
|
||||
task_id=task_id,
|
||||
context_id=context_id,
|
||||
status=TaskStatus(state=TaskState.canceled),
|
||||
final=True,
|
||||
)
|
||||
)
|
||||
|
||||
if context.current_task:
|
||||
context.current_task.status = TaskStatus(state=TaskState.canceled)
|
||||
return context.current_task
|
||||
return None
|
||||
|
||||
|
||||
def list_tasks(
|
||||
tasks: list[A2ATask],
|
||||
context_id: str | None = None,
|
||||
status: TaskState | None = None,
|
||||
status_timestamp_after: datetime | None = None,
|
||||
page_size: int = 50,
|
||||
page_token: str | None = None,
|
||||
history_length: int | None = None,
|
||||
include_artifacts: bool = False,
|
||||
) -> tuple[list[A2ATask], str | None, int]:
|
||||
"""Filter and paginate A2A tasks.
|
||||
|
||||
Provides filtering by context, status, and timestamp, along with
|
||||
cursor-based pagination. This is a pure utility function that operates
|
||||
on an in-memory list of tasks - storage retrieval is handled separately.
|
||||
|
||||
Args:
|
||||
tasks: All tasks to filter.
|
||||
context_id: Filter by context ID to get tasks in a conversation.
|
||||
status: Filter by task state (e.g., completed, working).
|
||||
status_timestamp_after: Filter to tasks updated after this time.
|
||||
page_size: Maximum tasks per page (default 50).
|
||||
page_token: Base64-encoded cursor from previous response.
|
||||
history_length: Limit history messages per task (None = full history).
|
||||
include_artifacts: Whether to include task artifacts (default False).
|
||||
|
||||
Returns:
|
||||
Tuple of (filtered_tasks, next_page_token, total_count).
|
||||
- filtered_tasks: Tasks matching filters, paginated and trimmed.
|
||||
- next_page_token: Token for next page, or None if no more pages.
|
||||
- total_count: Total number of tasks matching filters (before pagination).
|
||||
"""
|
||||
filtered: list[A2ATask] = []
|
||||
for task in tasks:
|
||||
if context_id and task.context_id != context_id:
|
||||
continue
|
||||
if status and task.status.state != status:
|
||||
continue
|
||||
if status_timestamp_after and task.status.timestamp:
|
||||
ts = datetime.fromisoformat(task.status.timestamp.replace("Z", "+00:00"))
|
||||
if ts <= status_timestamp_after:
|
||||
continue
|
||||
filtered.append(task)
|
||||
|
||||
def get_timestamp(t: A2ATask) -> datetime:
|
||||
"""Extract timestamp from task status for sorting."""
|
||||
if t.status.timestamp is None:
|
||||
return datetime.min
|
||||
return datetime.fromisoformat(t.status.timestamp.replace("Z", "+00:00"))
|
||||
|
||||
filtered.sort(key=get_timestamp, reverse=True)
|
||||
total = len(filtered)
|
||||
|
||||
start = 0
|
||||
if page_token:
|
||||
try:
|
||||
cursor_id = base64.b64decode(page_token).decode()
|
||||
for idx, task in enumerate(filtered):
|
||||
if task.id == cursor_id:
|
||||
start = idx + 1
|
||||
break
|
||||
except (ValueError, UnicodeDecodeError):
|
||||
pass
|
||||
|
||||
page = filtered[start : start + page_size]
|
||||
|
||||
result: list[A2ATask] = []
|
||||
for task in page:
|
||||
task = task.model_copy(deep=True)
|
||||
if history_length is not None and task.history:
|
||||
task.history = task.history[-history_length:]
|
||||
if not include_artifacts:
|
||||
task.artifacts = None
|
||||
result.append(task)
|
||||
|
||||
next_token: str | None = None
|
||||
if result and len(result) == page_size:
|
||||
next_token = base64.b64encode(result[-1].id.encode()).decode()
|
||||
|
||||
return result, next_token, total
|
||||
@@ -17,7 +17,6 @@ from urllib.parse import urlparse
|
||||
from pydantic import BaseModel, Field, InstanceOf, PrivateAttr, model_validator
|
||||
from typing_extensions import Self
|
||||
|
||||
from crewai.a2a.config import A2AConfig
|
||||
from crewai.agent.utils import (
|
||||
ahandle_knowledge_retrieval,
|
||||
apply_training_data,
|
||||
@@ -73,11 +72,19 @@ from crewai.utilities.constants import TRAINED_AGENTS_DATA_FILE, TRAINING_DATA_F
|
||||
from crewai.utilities.converter import Converter
|
||||
from crewai.utilities.guardrail_types import GuardrailType
|
||||
from crewai.utilities.llm_utils import create_llm
|
||||
from crewai.utilities.prompts import Prompts
|
||||
from crewai.utilities.prompts import Prompts, StandardPromptResult, SystemPromptResult
|
||||
from crewai.utilities.token_counter_callback import TokenCalcHandler
|
||||
from crewai.utilities.training_handler import CrewTrainingHandler
|
||||
|
||||
|
||||
try:
|
||||
from crewai.a2a.config import A2AClientConfig, A2AConfig, A2AServerConfig
|
||||
except ImportError:
|
||||
A2AClientConfig = Any
|
||||
A2AConfig = Any
|
||||
A2AServerConfig = Any
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai_tools import CodeInterpreterTool
|
||||
|
||||
@@ -218,9 +225,18 @@ class Agent(BaseAgent):
|
||||
guardrail_max_retries: int = Field(
|
||||
default=3, description="Maximum number of retries when guardrail fails"
|
||||
)
|
||||
a2a: list[A2AConfig] | A2AConfig | None = Field(
|
||||
a2a: (
|
||||
list[A2AConfig | A2AServerConfig | A2AClientConfig]
|
||||
| A2AConfig
|
||||
| A2AServerConfig
|
||||
| A2AClientConfig
|
||||
| None
|
||||
) = Field(
|
||||
default=None,
|
||||
description="A2A (Agent-to-Agent) configuration for delegating tasks to remote agents. Can be a single A2AConfig or a dict mapping agent IDs to configs.",
|
||||
description="""
|
||||
A2A (Agent-to-Agent) configuration for delegating tasks to remote agents.
|
||||
Can be a single A2AConfig/A2AClientConfig/A2AServerConfig, or a list of any number of A2AConfig/A2AClientConfig with a single A2AServerConfig.
|
||||
""",
|
||||
)
|
||||
executor_class: type[CrewAgentExecutor] | type[CrewAgentExecutorFlow] = Field(
|
||||
default=CrewAgentExecutor,
|
||||
@@ -733,7 +749,7 @@ class Agent(BaseAgent):
|
||||
if self.agent_executor is not None:
|
||||
self._update_executor_parameters(
|
||||
task=task,
|
||||
tools=parsed_tools,
|
||||
tools=parsed_tools, # type: ignore[arg-type]
|
||||
raw_tools=raw_tools,
|
||||
prompt=prompt,
|
||||
stop_words=stop_words,
|
||||
@@ -742,7 +758,7 @@ class Agent(BaseAgent):
|
||||
else:
|
||||
self.agent_executor = self.executor_class(
|
||||
llm=cast(BaseLLM, self.llm),
|
||||
task=task,
|
||||
task=task, # type: ignore[arg-type]
|
||||
i18n=self.i18n,
|
||||
agent=self,
|
||||
crew=self.crew,
|
||||
@@ -765,11 +781,11 @@ class Agent(BaseAgent):
|
||||
def _update_executor_parameters(
|
||||
self,
|
||||
task: Task | None,
|
||||
tools: list,
|
||||
tools: list[BaseTool],
|
||||
raw_tools: list[BaseTool],
|
||||
prompt: dict,
|
||||
prompt: SystemPromptResult | StandardPromptResult,
|
||||
stop_words: list[str],
|
||||
rpm_limit_fn: Callable | None,
|
||||
rpm_limit_fn: Callable | None, # type: ignore[type-arg]
|
||||
) -> None:
|
||||
"""Update executor parameters without recreating instance.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from collections.abc import Callable
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
from pydantic import BaseModel, GetCoreSchemaHandler
|
||||
from pydantic import BaseModel, GetCoreSchemaHandler, ValidationError
|
||||
from pydantic_core import CoreSchema, core_schema
|
||||
|
||||
from crewai.agents.agent_builder.base_agent_executor_mixin import CrewAgentExecutorMixin
|
||||
@@ -244,7 +244,20 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
||||
response_model=self.response_model,
|
||||
executor_context=self,
|
||||
)
|
||||
formatted_answer = process_llm_response(answer, self.use_stop_words) # type: ignore[assignment]
|
||||
if self.response_model is not None:
|
||||
try:
|
||||
self.response_model.model_validate_json(answer)
|
||||
formatted_answer = AgentFinish(
|
||||
thought="",
|
||||
output=answer,
|
||||
text=answer,
|
||||
)
|
||||
except ValidationError:
|
||||
formatted_answer = process_llm_response(
|
||||
answer, self.use_stop_words
|
||||
) # type: ignore[assignment]
|
||||
else:
|
||||
formatted_answer = process_llm_response(answer, self.use_stop_words) # type: ignore[assignment]
|
||||
|
||||
if isinstance(formatted_answer, AgentAction):
|
||||
# Extract agent fingerprint if available
|
||||
@@ -278,7 +291,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
||||
)
|
||||
|
||||
self._invoke_step_callback(formatted_answer) # type: ignore[arg-type]
|
||||
self._append_message(formatted_answer.text) # type: ignore[union-attr,attr-defined]
|
||||
self._append_message(formatted_answer.text) # type: ignore[union-attr]
|
||||
|
||||
except OutputParserError as e:
|
||||
formatted_answer = handle_output_parser_exception( # type: ignore[assignment]
|
||||
@@ -398,7 +411,21 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
||||
response_model=self.response_model,
|
||||
executor_context=self,
|
||||
)
|
||||
formatted_answer = process_llm_response(answer, self.use_stop_words) # type: ignore[assignment]
|
||||
|
||||
if self.response_model is not None:
|
||||
try:
|
||||
self.response_model.model_validate_json(answer)
|
||||
formatted_answer = AgentFinish(
|
||||
thought="",
|
||||
output=answer,
|
||||
text=answer,
|
||||
)
|
||||
except ValidationError:
|
||||
formatted_answer = process_llm_response(
|
||||
answer, self.use_stop_words
|
||||
) # type: ignore[assignment]
|
||||
else:
|
||||
formatted_answer = process_llm_response(answer, self.use_stop_words) # type: ignore[assignment]
|
||||
|
||||
if isinstance(formatted_answer, AgentAction):
|
||||
fingerprint_context = {}
|
||||
@@ -431,7 +458,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
||||
)
|
||||
|
||||
self._invoke_step_callback(formatted_answer) # type: ignore[arg-type]
|
||||
self._append_message(formatted_answer.text) # type: ignore[union-attr,attr-defined]
|
||||
self._append_message(formatted_answer.text) # type: ignore[union-attr]
|
||||
|
||||
except OutputParserError as e:
|
||||
formatted_answer = handle_output_parser_exception( # type: ignore[assignment]
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from crewai.cli.authentication.providers.base_provider import BaseProvider
|
||||
|
||||
|
||||
class KeycloakProvider(BaseProvider):
|
||||
def get_authorize_url(self) -> str:
|
||||
return f"{self._oauth2_base_url()}/realms/{self.settings.extra.get('realm')}/protocol/openid-connect/auth/device"
|
||||
|
||||
def get_token_url(self) -> str:
|
||||
return f"{self._oauth2_base_url()}/realms/{self.settings.extra.get('realm')}/protocol/openid-connect/token"
|
||||
|
||||
def get_jwks_url(self) -> str:
|
||||
return f"{self._oauth2_base_url()}/realms/{self.settings.extra.get('realm')}/protocol/openid-connect/certs"
|
||||
|
||||
def get_issuer(self) -> str:
|
||||
return f"{self._oauth2_base_url()}/realms/{self.settings.extra.get('realm')}"
|
||||
|
||||
def get_audience(self) -> str:
|
||||
return self.settings.audience or "no-audience-provided"
|
||||
|
||||
def get_client_id(self) -> str:
|
||||
if self.settings.client_id is None:
|
||||
raise ValueError(
|
||||
"Client ID is required. Please set it in the configuration."
|
||||
)
|
||||
return self.settings.client_id
|
||||
|
||||
def get_required_fields(self) -> list[str]:
|
||||
return ["realm"]
|
||||
|
||||
def _oauth2_base_url(self) -> str:
|
||||
domain = self.settings.domain.removeprefix("https://").removeprefix("http://")
|
||||
return f"https://{domain}"
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.7.2"
|
||||
"crewai[tools]==1.8.1"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.7.2"
|
||||
"crewai[tools]==1.8.1"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -10,7 +10,7 @@ This module provides the event infrastructure that allows users to:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from crewai.events.base_event_listener import BaseEventListener
|
||||
from crewai.events.depends import Depends
|
||||
@@ -34,6 +34,8 @@ from crewai.events.types.flow_events import (
|
||||
FlowFinishedEvent,
|
||||
FlowPlotEvent,
|
||||
FlowStartedEvent,
|
||||
HumanFeedbackReceivedEvent,
|
||||
HumanFeedbackRequestedEvent,
|
||||
MethodExecutionFailedEvent,
|
||||
MethodExecutionFinishedEvent,
|
||||
MethodExecutionStartedEvent,
|
||||
@@ -145,6 +147,8 @@ __all__ = [
|
||||
"FlowFinishedEvent",
|
||||
"FlowPlotEvent",
|
||||
"FlowStartedEvent",
|
||||
"HumanFeedbackReceivedEvent",
|
||||
"HumanFeedbackRequestedEvent",
|
||||
"KnowledgeQueryCompletedEvent",
|
||||
"KnowledgeQueryFailedEvent",
|
||||
"KnowledgeQueryStartedEvent",
|
||||
@@ -205,7 +209,7 @@ _AGENT_EVENT_MAPPING = {
|
||||
}
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
def __getattr__(name: str) -> Any:
|
||||
"""Lazy import for agent events to avoid circular imports."""
|
||||
if name in _AGENT_EVENT_MAPPING:
|
||||
import importlib
|
||||
|
||||
@@ -13,6 +13,8 @@ from crewai.events.types.a2a_events import (
|
||||
A2ADelegationCompletedEvent,
|
||||
A2ADelegationStartedEvent,
|
||||
A2AMessageSentEvent,
|
||||
A2APollingStartedEvent,
|
||||
A2APollingStatusEvent,
|
||||
A2AResponseReceivedEvent,
|
||||
)
|
||||
from crewai.events.types.agent_events import (
|
||||
@@ -37,6 +39,8 @@ from crewai.events.types.flow_events import (
|
||||
FlowFinishedEvent,
|
||||
FlowPausedEvent,
|
||||
FlowStartedEvent,
|
||||
HumanFeedbackReceivedEvent,
|
||||
HumanFeedbackRequestedEvent,
|
||||
MethodExecutionFailedEvent,
|
||||
MethodExecutionFinishedEvent,
|
||||
MethodExecutionPausedEvent,
|
||||
@@ -67,7 +71,6 @@ from crewai.events.types.mcp_events import (
|
||||
MCPConnectionCompletedEvent,
|
||||
MCPConnectionFailedEvent,
|
||||
MCPConnectionStartedEvent,
|
||||
MCPToolExecutionCompletedEvent,
|
||||
MCPToolExecutionFailedEvent,
|
||||
MCPToolExecutionStartedEvent,
|
||||
)
|
||||
@@ -206,10 +209,9 @@ class EventListener(BaseEventListener):
|
||||
@crewai_event_bus.on(TaskCompletedEvent)
|
||||
def on_task_completed(source: Any, event: TaskCompletedEvent) -> None:
|
||||
# Handle telemetry
|
||||
span = self.execution_spans.get(source)
|
||||
span = self.execution_spans.pop(source, None)
|
||||
if span:
|
||||
self._telemetry.task_ended(span, source, source.agent.crew)
|
||||
self.execution_spans[source] = None
|
||||
|
||||
# Pass task name if it exists
|
||||
task_name = get_task_name(source)
|
||||
@@ -219,11 +221,10 @@ class EventListener(BaseEventListener):
|
||||
|
||||
@crewai_event_bus.on(TaskFailedEvent)
|
||||
def on_task_failed(source: Any, event: TaskFailedEvent) -> None:
|
||||
span = self.execution_spans.get(source)
|
||||
span = self.execution_spans.pop(source, None)
|
||||
if span:
|
||||
if source.agent and source.agent.crew:
|
||||
self._telemetry.task_ended(span, source, source.agent.crew)
|
||||
self.execution_spans[source] = None
|
||||
|
||||
# Pass task name if it exists
|
||||
task_name = get_task_name(source)
|
||||
@@ -329,6 +330,33 @@ class EventListener(BaseEventListener):
|
||||
"paused",
|
||||
)
|
||||
|
||||
# ----------- HUMAN FEEDBACK EVENTS -----------
|
||||
@crewai_event_bus.on(HumanFeedbackRequestedEvent)
|
||||
def on_human_feedback_requested(
|
||||
_: Any, event: HumanFeedbackRequestedEvent
|
||||
) -> None:
|
||||
"""Handle human feedback requested event."""
|
||||
has_routing = event.emit is not None and len(event.emit) > 0
|
||||
self._telemetry.human_feedback_span(
|
||||
event_type="requested",
|
||||
has_routing=has_routing,
|
||||
num_outcomes=len(event.emit) if event.emit else 0,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(HumanFeedbackReceivedEvent)
|
||||
def on_human_feedback_received(
|
||||
_: Any, event: HumanFeedbackReceivedEvent
|
||||
) -> None:
|
||||
"""Handle human feedback received event."""
|
||||
has_routing = event.outcome is not None
|
||||
self._telemetry.human_feedback_span(
|
||||
event_type="received",
|
||||
has_routing=has_routing,
|
||||
num_outcomes=0,
|
||||
feedback_provided=bool(event.feedback and event.feedback.strip()),
|
||||
outcome=event.outcome,
|
||||
)
|
||||
|
||||
# ----------- TOOL USAGE EVENTS -----------
|
||||
@crewai_event_bus.on(ToolUsageStartedEvent)
|
||||
def on_tool_usage_started(source: Any, event: ToolUsageStartedEvent) -> None:
|
||||
@@ -580,6 +608,23 @@ class EventListener(BaseEventListener):
|
||||
event.total_turns,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(A2APollingStartedEvent)
|
||||
def on_a2a_polling_started(_: Any, event: A2APollingStartedEvent) -> None:
|
||||
self.formatter.handle_a2a_polling_started(
|
||||
event.task_id,
|
||||
event.polling_interval,
|
||||
event.endpoint,
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(A2APollingStatusEvent)
|
||||
def on_a2a_polling_status(_: Any, event: A2APollingStatusEvent) -> None:
|
||||
self.formatter.handle_a2a_polling_status(
|
||||
event.task_id,
|
||||
event.state,
|
||||
event.elapsed_seconds,
|
||||
event.poll_count,
|
||||
)
|
||||
|
||||
# ----------- MCP EVENTS -----------
|
||||
|
||||
@crewai_event_bus.on(MCPConnectionStartedEvent)
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
from crewai.events.types.a2a_events import (
|
||||
A2AAgentCardFetchedEvent,
|
||||
A2AArtifactReceivedEvent,
|
||||
A2AAuthenticationFailedEvent,
|
||||
A2AConnectionErrorEvent,
|
||||
A2AConversationCompletedEvent,
|
||||
A2AConversationStartedEvent,
|
||||
A2ADelegationCompletedEvent,
|
||||
A2ADelegationStartedEvent,
|
||||
A2AMessageSentEvent,
|
||||
A2AParallelDelegationCompletedEvent,
|
||||
A2AParallelDelegationStartedEvent,
|
||||
A2APollingStartedEvent,
|
||||
A2APollingStatusEvent,
|
||||
A2APushNotificationReceivedEvent,
|
||||
A2APushNotificationRegisteredEvent,
|
||||
A2APushNotificationSentEvent,
|
||||
A2APushNotificationTimeoutEvent,
|
||||
A2AResponseReceivedEvent,
|
||||
A2AServerTaskCanceledEvent,
|
||||
A2AServerTaskCompletedEvent,
|
||||
A2AServerTaskFailedEvent,
|
||||
A2AServerTaskStartedEvent,
|
||||
A2AStreamingChunkEvent,
|
||||
A2AStreamingStartedEvent,
|
||||
)
|
||||
from crewai.events.types.agent_events import (
|
||||
AgentExecutionCompletedEvent,
|
||||
AgentExecutionErrorEvent,
|
||||
@@ -76,7 +102,31 @@ from crewai.events.types.tool_usage_events import (
|
||||
|
||||
|
||||
EventTypes = (
|
||||
CrewKickoffStartedEvent
|
||||
A2AAgentCardFetchedEvent
|
||||
| A2AArtifactReceivedEvent
|
||||
| A2AAuthenticationFailedEvent
|
||||
| A2AConnectionErrorEvent
|
||||
| A2AConversationCompletedEvent
|
||||
| A2AConversationStartedEvent
|
||||
| A2ADelegationCompletedEvent
|
||||
| A2ADelegationStartedEvent
|
||||
| A2AMessageSentEvent
|
||||
| A2APollingStartedEvent
|
||||
| A2APollingStatusEvent
|
||||
| A2APushNotificationReceivedEvent
|
||||
| A2APushNotificationRegisteredEvent
|
||||
| A2APushNotificationSentEvent
|
||||
| A2APushNotificationTimeoutEvent
|
||||
| A2AResponseReceivedEvent
|
||||
| A2AServerTaskCanceledEvent
|
||||
| A2AServerTaskCompletedEvent
|
||||
| A2AServerTaskFailedEvent
|
||||
| A2AServerTaskStartedEvent
|
||||
| A2AStreamingChunkEvent
|
||||
| A2AStreamingStartedEvent
|
||||
| A2AParallelDelegationStartedEvent
|
||||
| A2AParallelDelegationCompletedEvent
|
||||
| CrewKickoffStartedEvent
|
||||
| CrewKickoffCompletedEvent
|
||||
| CrewKickoffFailedEvent
|
||||
| CrewTestStartedEvent
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Trace collection listener for orchestrating trace collection."""
|
||||
|
||||
import os
|
||||
from typing import Any, ClassVar, cast
|
||||
from typing import Any, ClassVar
|
||||
import uuid
|
||||
|
||||
from typing_extensions import Self
|
||||
@@ -18,6 +18,32 @@ from crewai.events.listeners.tracing.types import TraceEvent
|
||||
from crewai.events.listeners.tracing.utils import (
|
||||
safe_serialize_to_dict,
|
||||
)
|
||||
from crewai.events.types.a2a_events import (
|
||||
A2AAgentCardFetchedEvent,
|
||||
A2AArtifactReceivedEvent,
|
||||
A2AAuthenticationFailedEvent,
|
||||
A2AConnectionErrorEvent,
|
||||
A2AConversationCompletedEvent,
|
||||
A2AConversationStartedEvent,
|
||||
A2ADelegationCompletedEvent,
|
||||
A2ADelegationStartedEvent,
|
||||
A2AMessageSentEvent,
|
||||
A2AParallelDelegationCompletedEvent,
|
||||
A2AParallelDelegationStartedEvent,
|
||||
A2APollingStartedEvent,
|
||||
A2APollingStatusEvent,
|
||||
A2APushNotificationReceivedEvent,
|
||||
A2APushNotificationRegisteredEvent,
|
||||
A2APushNotificationSentEvent,
|
||||
A2APushNotificationTimeoutEvent,
|
||||
A2AResponseReceivedEvent,
|
||||
A2AServerTaskCanceledEvent,
|
||||
A2AServerTaskCompletedEvent,
|
||||
A2AServerTaskFailedEvent,
|
||||
A2AServerTaskStartedEvent,
|
||||
A2AStreamingChunkEvent,
|
||||
A2AStreamingStartedEvent,
|
||||
)
|
||||
from crewai.events.types.agent_events import (
|
||||
AgentExecutionCompletedEvent,
|
||||
AgentExecutionErrorEvent,
|
||||
@@ -105,7 +131,7 @@ class TraceCollectionListener(BaseEventListener):
|
||||
"""Create or return singleton instance."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cast(Self, cls._instance)
|
||||
return cls._instance
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -160,6 +186,7 @@ class TraceCollectionListener(BaseEventListener):
|
||||
self._register_flow_event_handlers(crewai_event_bus)
|
||||
self._register_context_event_handlers(crewai_event_bus)
|
||||
self._register_action_event_handlers(crewai_event_bus)
|
||||
self._register_a2a_event_handlers(crewai_event_bus)
|
||||
self._register_system_event_handlers(crewai_event_bus)
|
||||
|
||||
self._listeners_setup = True
|
||||
@@ -439,6 +466,147 @@ class TraceCollectionListener(BaseEventListener):
|
||||
) -> None:
|
||||
self._handle_action_event("knowledge_query_failed", source, event)
|
||||
|
||||
def _register_a2a_event_handlers(self, event_bus: CrewAIEventsBus) -> None:
|
||||
"""Register handlers for A2A (Agent-to-Agent) events."""
|
||||
|
||||
@event_bus.on(A2ADelegationStartedEvent)
|
||||
def on_a2a_delegation_started(
|
||||
source: Any, event: A2ADelegationStartedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_delegation_started", source, event)
|
||||
|
||||
@event_bus.on(A2ADelegationCompletedEvent)
|
||||
def on_a2a_delegation_completed(
|
||||
source: Any, event: A2ADelegationCompletedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_delegation_completed", source, event)
|
||||
|
||||
@event_bus.on(A2AConversationStartedEvent)
|
||||
def on_a2a_conversation_started(
|
||||
source: Any, event: A2AConversationStartedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_conversation_started", source, event)
|
||||
|
||||
@event_bus.on(A2AMessageSentEvent)
|
||||
def on_a2a_message_sent(source: Any, event: A2AMessageSentEvent) -> None:
|
||||
self._handle_action_event("a2a_message_sent", source, event)
|
||||
|
||||
@event_bus.on(A2AResponseReceivedEvent)
|
||||
def on_a2a_response_received(
|
||||
source: Any, event: A2AResponseReceivedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_response_received", source, event)
|
||||
|
||||
@event_bus.on(A2AConversationCompletedEvent)
|
||||
def on_a2a_conversation_completed(
|
||||
source: Any, event: A2AConversationCompletedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_conversation_completed", source, event)
|
||||
|
||||
@event_bus.on(A2APollingStartedEvent)
|
||||
def on_a2a_polling_started(source: Any, event: A2APollingStartedEvent) -> None:
|
||||
self._handle_action_event("a2a_polling_started", source, event)
|
||||
|
||||
@event_bus.on(A2APollingStatusEvent)
|
||||
def on_a2a_polling_status(source: Any, event: A2APollingStatusEvent) -> None:
|
||||
self._handle_action_event("a2a_polling_status", source, event)
|
||||
|
||||
@event_bus.on(A2APushNotificationRegisteredEvent)
|
||||
def on_a2a_push_notification_registered(
|
||||
source: Any, event: A2APushNotificationRegisteredEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_push_notification_registered", source, event)
|
||||
|
||||
@event_bus.on(A2APushNotificationReceivedEvent)
|
||||
def on_a2a_push_notification_received(
|
||||
source: Any, event: A2APushNotificationReceivedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_push_notification_received", source, event)
|
||||
|
||||
@event_bus.on(A2APushNotificationSentEvent)
|
||||
def on_a2a_push_notification_sent(
|
||||
source: Any, event: A2APushNotificationSentEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_push_notification_sent", source, event)
|
||||
|
||||
@event_bus.on(A2APushNotificationTimeoutEvent)
|
||||
def on_a2a_push_notification_timeout(
|
||||
source: Any, event: A2APushNotificationTimeoutEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_push_notification_timeout", source, event)
|
||||
|
||||
@event_bus.on(A2AStreamingStartedEvent)
|
||||
def on_a2a_streaming_started(
|
||||
source: Any, event: A2AStreamingStartedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_streaming_started", source, event)
|
||||
|
||||
@event_bus.on(A2AStreamingChunkEvent)
|
||||
def on_a2a_streaming_chunk(source: Any, event: A2AStreamingChunkEvent) -> None:
|
||||
self._handle_action_event("a2a_streaming_chunk", source, event)
|
||||
|
||||
@event_bus.on(A2AAgentCardFetchedEvent)
|
||||
def on_a2a_agent_card_fetched(
|
||||
source: Any, event: A2AAgentCardFetchedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_agent_card_fetched", source, event)
|
||||
|
||||
@event_bus.on(A2AAuthenticationFailedEvent)
|
||||
def on_a2a_authentication_failed(
|
||||
source: Any, event: A2AAuthenticationFailedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_authentication_failed", source, event)
|
||||
|
||||
@event_bus.on(A2AArtifactReceivedEvent)
|
||||
def on_a2a_artifact_received(
|
||||
source: Any, event: A2AArtifactReceivedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_artifact_received", source, event)
|
||||
|
||||
@event_bus.on(A2AConnectionErrorEvent)
|
||||
def on_a2a_connection_error(
|
||||
source: Any, event: A2AConnectionErrorEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_connection_error", source, event)
|
||||
|
||||
@event_bus.on(A2AServerTaskStartedEvent)
|
||||
def on_a2a_server_task_started(
|
||||
source: Any, event: A2AServerTaskStartedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_server_task_started", source, event)
|
||||
|
||||
@event_bus.on(A2AServerTaskCompletedEvent)
|
||||
def on_a2a_server_task_completed(
|
||||
source: Any, event: A2AServerTaskCompletedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_server_task_completed", source, event)
|
||||
|
||||
@event_bus.on(A2AServerTaskCanceledEvent)
|
||||
def on_a2a_server_task_canceled(
|
||||
source: Any, event: A2AServerTaskCanceledEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_server_task_canceled", source, event)
|
||||
|
||||
@event_bus.on(A2AServerTaskFailedEvent)
|
||||
def on_a2a_server_task_failed(
|
||||
source: Any, event: A2AServerTaskFailedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_server_task_failed", source, event)
|
||||
|
||||
@event_bus.on(A2AParallelDelegationStartedEvent)
|
||||
def on_a2a_parallel_delegation_started(
|
||||
source: Any, event: A2AParallelDelegationStartedEvent
|
||||
) -> None:
|
||||
self._handle_action_event("a2a_parallel_delegation_started", source, event)
|
||||
|
||||
@event_bus.on(A2AParallelDelegationCompletedEvent)
|
||||
def on_a2a_parallel_delegation_completed(
|
||||
source: Any, event: A2AParallelDelegationCompletedEvent
|
||||
) -> None:
|
||||
self._handle_action_event(
|
||||
"a2a_parallel_delegation_completed", source, event
|
||||
)
|
||||
|
||||
def _register_system_event_handlers(self, event_bus: CrewAIEventsBus) -> None:
|
||||
"""Register handlers for system signal events (SIGTERM, SIGINT, etc.)."""
|
||||
|
||||
@@ -570,10 +738,15 @@ class TraceCollectionListener(BaseEventListener):
|
||||
if event_type not in self.complex_events:
|
||||
return safe_serialize_to_dict(event)
|
||||
if event_type == "task_started":
|
||||
task_name = event.task.name or event.task.description
|
||||
task_display_name = (
|
||||
task_name[:80] + "..." if len(task_name) > 80 else task_name
|
||||
)
|
||||
return {
|
||||
"task_description": event.task.description,
|
||||
"expected_output": event.task.expected_output,
|
||||
"task_name": event.task.name or event.task.description,
|
||||
"task_name": task_name,
|
||||
"task_display_name": task_display_name,
|
||||
"context": event.context,
|
||||
"agent_role": source.agent.role,
|
||||
"task_id": str(event.task.id),
|
||||
|
||||
@@ -4,68 +4,120 @@ This module defines events emitted during A2A protocol delegation,
|
||||
including both single-turn and multiturn conversation flows.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import model_validator
|
||||
|
||||
from crewai.events.base_events import BaseEvent
|
||||
|
||||
|
||||
class A2AEventBase(BaseEvent):
|
||||
"""Base class for A2A events with task/agent context."""
|
||||
|
||||
from_task: Any | None = None
|
||||
from_agent: Any | None = None
|
||||
from_task: Any = None
|
||||
from_agent: Any = None
|
||||
|
||||
def __init__(self, **data):
|
||||
"""Initialize A2A event, extracting task and agent metadata."""
|
||||
if data.get("from_task"):
|
||||
task = data["from_task"]
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def extract_task_and_agent_metadata(cls, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Extract task and agent metadata before validation."""
|
||||
if task := data.get("from_task"):
|
||||
data["task_id"] = str(task.id)
|
||||
data["task_name"] = task.name or task.description
|
||||
data.setdefault("source_fingerprint", str(task.id))
|
||||
data.setdefault("source_type", "task")
|
||||
data.setdefault(
|
||||
"fingerprint_metadata",
|
||||
{
|
||||
"task_id": str(task.id),
|
||||
"task_name": task.name or task.description,
|
||||
},
|
||||
)
|
||||
data["from_task"] = None
|
||||
|
||||
if data.get("from_agent"):
|
||||
agent = data["from_agent"]
|
||||
if agent := data.get("from_agent"):
|
||||
data["agent_id"] = str(agent.id)
|
||||
data["agent_role"] = agent.role
|
||||
data.setdefault("source_fingerprint", str(agent.id))
|
||||
data.setdefault("source_type", "agent")
|
||||
data.setdefault(
|
||||
"fingerprint_metadata",
|
||||
{
|
||||
"agent_id": str(agent.id),
|
||||
"agent_role": agent.role,
|
||||
},
|
||||
)
|
||||
data["from_agent"] = None
|
||||
|
||||
super().__init__(**data)
|
||||
return data
|
||||
|
||||
|
||||
class A2ADelegationStartedEvent(A2AEventBase):
|
||||
"""Event emitted when A2A delegation starts.
|
||||
|
||||
Attributes:
|
||||
endpoint: A2A agent endpoint URL (AgentCard URL)
|
||||
task_description: Task being delegated to the A2A agent
|
||||
agent_id: A2A agent identifier
|
||||
is_multiturn: Whether this is part of a multiturn conversation
|
||||
turn_number: Current turn number (1-indexed, 1 for single-turn)
|
||||
endpoint: A2A agent endpoint URL (AgentCard URL).
|
||||
task_description: Task being delegated to the A2A agent.
|
||||
agent_id: A2A agent identifier.
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
is_multiturn: Whether this is part of a multiturn conversation.
|
||||
turn_number: Current turn number (1-indexed, 1 for single-turn).
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
agent_card: Full A2A agent card metadata.
|
||||
protocol_version: A2A protocol version being used.
|
||||
provider: Agent provider/organization info from agent card.
|
||||
skill_id: ID of the specific skill being invoked.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
extensions: List of A2A extension URIs in use.
|
||||
"""
|
||||
|
||||
type: str = "a2a_delegation_started"
|
||||
endpoint: str
|
||||
task_description: str
|
||||
agent_id: str
|
||||
context_id: str | None = None
|
||||
is_multiturn: bool = False
|
||||
turn_number: int = 1
|
||||
a2a_agent_name: str | None = None
|
||||
agent_card: dict[str, Any] | None = None
|
||||
protocol_version: str | None = None
|
||||
provider: dict[str, Any] | None = None
|
||||
skill_id: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
extensions: list[str] | None = None
|
||||
|
||||
|
||||
class A2ADelegationCompletedEvent(A2AEventBase):
|
||||
"""Event emitted when A2A delegation completes.
|
||||
|
||||
Attributes:
|
||||
status: Completion status (completed, input_required, failed, etc.)
|
||||
result: Result message if status is completed
|
||||
error: Error/response message (error for failed, response for input_required)
|
||||
is_multiturn: Whether this is part of a multiturn conversation
|
||||
status: Completion status (completed, input_required, failed, etc.).
|
||||
result: Result message if status is completed.
|
||||
error: Error/response message (error for failed, response for input_required).
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
is_multiturn: Whether this is part of a multiturn conversation.
|
||||
endpoint: A2A agent endpoint URL.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
agent_card: Full A2A agent card metadata.
|
||||
provider: Agent provider/organization info from agent card.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
extensions: List of A2A extension URIs in use.
|
||||
"""
|
||||
|
||||
type: str = "a2a_delegation_completed"
|
||||
status: str
|
||||
result: str | None = None
|
||||
error: str | None = None
|
||||
context_id: str | None = None
|
||||
is_multiturn: bool = False
|
||||
endpoint: str | None = None
|
||||
a2a_agent_name: str | None = None
|
||||
agent_card: dict[str, Any] | None = None
|
||||
provider: dict[str, Any] | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
extensions: list[str] | None = None
|
||||
|
||||
|
||||
class A2AConversationStartedEvent(A2AEventBase):
|
||||
@@ -75,51 +127,95 @@ class A2AConversationStartedEvent(A2AEventBase):
|
||||
before the first message exchange.
|
||||
|
||||
Attributes:
|
||||
agent_id: A2A agent identifier
|
||||
endpoint: A2A agent endpoint URL
|
||||
a2a_agent_name: Name of the A2A agent from agent card
|
||||
agent_id: A2A agent identifier.
|
||||
endpoint: A2A agent endpoint URL.
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
agent_card: Full A2A agent card metadata.
|
||||
protocol_version: A2A protocol version being used.
|
||||
provider: Agent provider/organization info from agent card.
|
||||
skill_id: ID of the specific skill being invoked.
|
||||
reference_task_ids: Related task IDs for context.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
extensions: List of A2A extension URIs in use.
|
||||
"""
|
||||
|
||||
type: str = "a2a_conversation_started"
|
||||
agent_id: str
|
||||
endpoint: str
|
||||
context_id: str | None = None
|
||||
a2a_agent_name: str | None = None
|
||||
agent_card: dict[str, Any] | None = None
|
||||
protocol_version: str | None = None
|
||||
provider: dict[str, Any] | None = None
|
||||
skill_id: str | None = None
|
||||
reference_task_ids: list[str] | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
extensions: list[str] | None = None
|
||||
|
||||
|
||||
class A2AMessageSentEvent(A2AEventBase):
|
||||
"""Event emitted when a message is sent to the A2A agent.
|
||||
|
||||
Attributes:
|
||||
message: Message content sent to the A2A agent
|
||||
turn_number: Current turn number (1-indexed)
|
||||
is_multiturn: Whether this is part of a multiturn conversation
|
||||
agent_role: Role of the CrewAI agent sending the message
|
||||
message: Message content sent to the A2A agent.
|
||||
turn_number: Current turn number (1-indexed).
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
message_id: Unique A2A message identifier.
|
||||
is_multiturn: Whether this is part of a multiturn conversation.
|
||||
agent_role: Role of the CrewAI agent sending the message.
|
||||
endpoint: A2A agent endpoint URL.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
skill_id: ID of the specific skill being invoked.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
extensions: List of A2A extension URIs in use.
|
||||
"""
|
||||
|
||||
type: str = "a2a_message_sent"
|
||||
message: str
|
||||
turn_number: int
|
||||
context_id: str | None = None
|
||||
message_id: str | None = None
|
||||
is_multiturn: bool = False
|
||||
agent_role: str | None = None
|
||||
endpoint: str | None = None
|
||||
a2a_agent_name: str | None = None
|
||||
skill_id: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
extensions: list[str] | None = None
|
||||
|
||||
|
||||
class A2AResponseReceivedEvent(A2AEventBase):
|
||||
"""Event emitted when a response is received from the A2A agent.
|
||||
|
||||
Attributes:
|
||||
response: Response content from the A2A agent
|
||||
turn_number: Current turn number (1-indexed)
|
||||
is_multiturn: Whether this is part of a multiturn conversation
|
||||
status: Response status (input_required, completed, etc.)
|
||||
agent_role: Role of the CrewAI agent (for display)
|
||||
response: Response content from the A2A agent.
|
||||
turn_number: Current turn number (1-indexed).
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
message_id: Unique A2A message identifier.
|
||||
is_multiturn: Whether this is part of a multiturn conversation.
|
||||
status: Response status (input_required, completed, etc.).
|
||||
final: Whether this is the final response in the stream.
|
||||
agent_role: Role of the CrewAI agent (for display).
|
||||
endpoint: A2A agent endpoint URL.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
extensions: List of A2A extension URIs in use.
|
||||
"""
|
||||
|
||||
type: str = "a2a_response_received"
|
||||
response: str
|
||||
turn_number: int
|
||||
context_id: str | None = None
|
||||
message_id: str | None = None
|
||||
is_multiturn: bool = False
|
||||
status: str
|
||||
final: bool = False
|
||||
agent_role: str | None = None
|
||||
endpoint: str | None = None
|
||||
a2a_agent_name: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
extensions: list[str] | None = None
|
||||
|
||||
|
||||
class A2AConversationCompletedEvent(A2AEventBase):
|
||||
@@ -128,14 +224,433 @@ class A2AConversationCompletedEvent(A2AEventBase):
|
||||
This is emitted once at the end of a multiturn conversation.
|
||||
|
||||
Attributes:
|
||||
status: Final status (completed, failed, etc.)
|
||||
final_result: Final result if completed successfully
|
||||
error: Error message if failed
|
||||
total_turns: Total number of turns in the conversation
|
||||
status: Final status (completed, failed, etc.).
|
||||
final_result: Final result if completed successfully.
|
||||
error: Error message if failed.
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
total_turns: Total number of turns in the conversation.
|
||||
endpoint: A2A agent endpoint URL.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
agent_card: Full A2A agent card metadata.
|
||||
reference_task_ids: Related task IDs for context.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
extensions: List of A2A extension URIs in use.
|
||||
"""
|
||||
|
||||
type: str = "a2a_conversation_completed"
|
||||
status: Literal["completed", "failed"]
|
||||
final_result: str | None = None
|
||||
error: str | None = None
|
||||
context_id: str | None = None
|
||||
total_turns: int
|
||||
endpoint: str | None = None
|
||||
a2a_agent_name: str | None = None
|
||||
agent_card: dict[str, Any] | None = None
|
||||
reference_task_ids: list[str] | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
extensions: list[str] | None = None
|
||||
|
||||
|
||||
class A2APollingStartedEvent(A2AEventBase):
|
||||
"""Event emitted when polling mode begins for A2A delegation.
|
||||
|
||||
Attributes:
|
||||
task_id: A2A task ID being polled.
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
polling_interval: Seconds between poll attempts.
|
||||
endpoint: A2A agent endpoint URL.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
"""
|
||||
|
||||
type: str = "a2a_polling_started"
|
||||
task_id: str
|
||||
context_id: str | None = None
|
||||
polling_interval: float
|
||||
endpoint: str
|
||||
a2a_agent_name: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class A2APollingStatusEvent(A2AEventBase):
|
||||
"""Event emitted on each polling iteration.
|
||||
|
||||
Attributes:
|
||||
task_id: A2A task ID being polled.
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
state: Current task state from remote agent.
|
||||
elapsed_seconds: Time since polling started.
|
||||
poll_count: Number of polls completed.
|
||||
endpoint: A2A agent endpoint URL.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
"""
|
||||
|
||||
type: str = "a2a_polling_status"
|
||||
task_id: str
|
||||
context_id: str | None = None
|
||||
state: str
|
||||
elapsed_seconds: float
|
||||
poll_count: int
|
||||
endpoint: str | None = None
|
||||
a2a_agent_name: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class A2APushNotificationRegisteredEvent(A2AEventBase):
|
||||
"""Event emitted when push notification callback is registered.
|
||||
|
||||
Attributes:
|
||||
task_id: A2A task ID for which callback is registered.
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
callback_url: URL where agent will send push notifications.
|
||||
endpoint: A2A agent endpoint URL.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
"""
|
||||
|
||||
type: str = "a2a_push_notification_registered"
|
||||
task_id: str
|
||||
context_id: str | None = None
|
||||
callback_url: str
|
||||
endpoint: str | None = None
|
||||
a2a_agent_name: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class A2APushNotificationReceivedEvent(A2AEventBase):
|
||||
"""Event emitted when a push notification is received.
|
||||
|
||||
This event should be emitted by the user's webhook handler when it receives
|
||||
a push notification from the remote A2A agent, before calling
|
||||
`result_store.store_result()`.
|
||||
|
||||
Attributes:
|
||||
task_id: A2A task ID from the notification.
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
state: Current task state from the notification.
|
||||
endpoint: A2A agent endpoint URL.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
"""
|
||||
|
||||
type: str = "a2a_push_notification_received"
|
||||
task_id: str
|
||||
context_id: str | None = None
|
||||
state: str
|
||||
endpoint: str | None = None
|
||||
a2a_agent_name: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class A2APushNotificationSentEvent(A2AEventBase):
|
||||
"""Event emitted when a push notification is sent to a callback URL.
|
||||
|
||||
Emitted by the A2A server when it sends a task status update to the
|
||||
client's registered push notification callback URL.
|
||||
|
||||
Attributes:
|
||||
task_id: A2A task ID being notified.
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
callback_url: URL the notification was sent to.
|
||||
state: Task state being reported.
|
||||
success: Whether the notification was successfully delivered.
|
||||
error: Error message if delivery failed.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
"""
|
||||
|
||||
type: str = "a2a_push_notification_sent"
|
||||
task_id: str
|
||||
context_id: str | None = None
|
||||
callback_url: str
|
||||
state: str
|
||||
success: bool = True
|
||||
error: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class A2APushNotificationTimeoutEvent(A2AEventBase):
|
||||
"""Event emitted when push notification wait times out.
|
||||
|
||||
Attributes:
|
||||
task_id: A2A task ID that timed out.
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
timeout_seconds: Timeout duration in seconds.
|
||||
endpoint: A2A agent endpoint URL.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
"""
|
||||
|
||||
type: str = "a2a_push_notification_timeout"
|
||||
task_id: str
|
||||
context_id: str | None = None
|
||||
timeout_seconds: float
|
||||
endpoint: str | None = None
|
||||
a2a_agent_name: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class A2AStreamingStartedEvent(A2AEventBase):
|
||||
"""Event emitted when streaming mode begins for A2A delegation.
|
||||
|
||||
Attributes:
|
||||
task_id: A2A task ID for the streaming session.
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
endpoint: A2A agent endpoint URL.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
turn_number: Current turn number (1-indexed).
|
||||
is_multiturn: Whether this is part of a multiturn conversation.
|
||||
agent_role: Role of the CrewAI agent.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
extensions: List of A2A extension URIs in use.
|
||||
"""
|
||||
|
||||
type: str = "a2a_streaming_started"
|
||||
task_id: str | None = None
|
||||
context_id: str | None = None
|
||||
endpoint: str
|
||||
a2a_agent_name: str | None = None
|
||||
turn_number: int = 1
|
||||
is_multiturn: bool = False
|
||||
agent_role: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
extensions: list[str] | None = None
|
||||
|
||||
|
||||
class A2AStreamingChunkEvent(A2AEventBase):
|
||||
"""Event emitted when a streaming chunk is received.
|
||||
|
||||
Attributes:
|
||||
task_id: A2A task ID for the streaming session.
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
chunk: The text content of the chunk.
|
||||
chunk_index: Index of this chunk in the stream (0-indexed).
|
||||
final: Whether this is the final chunk in the stream.
|
||||
endpoint: A2A agent endpoint URL.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
turn_number: Current turn number (1-indexed).
|
||||
is_multiturn: Whether this is part of a multiturn conversation.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
extensions: List of A2A extension URIs in use.
|
||||
"""
|
||||
|
||||
type: str = "a2a_streaming_chunk"
|
||||
task_id: str | None = None
|
||||
context_id: str | None = None
|
||||
chunk: str
|
||||
chunk_index: int
|
||||
final: bool = False
|
||||
endpoint: str | None = None
|
||||
a2a_agent_name: str | None = None
|
||||
turn_number: int = 1
|
||||
is_multiturn: bool = False
|
||||
metadata: dict[str, Any] | None = None
|
||||
extensions: list[str] | None = None
|
||||
|
||||
|
||||
class A2AAgentCardFetchedEvent(A2AEventBase):
|
||||
"""Event emitted when an agent card is successfully fetched.
|
||||
|
||||
Attributes:
|
||||
endpoint: A2A agent endpoint URL.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
agent_card: Full A2A agent card metadata.
|
||||
protocol_version: A2A protocol version from agent card.
|
||||
provider: Agent provider/organization info from agent card.
|
||||
cached: Whether the agent card was retrieved from cache.
|
||||
fetch_time_ms: Time taken to fetch the agent card in milliseconds.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
"""
|
||||
|
||||
type: str = "a2a_agent_card_fetched"
|
||||
endpoint: str
|
||||
a2a_agent_name: str | None = None
|
||||
agent_card: dict[str, Any] | None = None
|
||||
protocol_version: str | None = None
|
||||
provider: dict[str, Any] | None = None
|
||||
cached: bool = False
|
||||
fetch_time_ms: float | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class A2AAuthenticationFailedEvent(A2AEventBase):
|
||||
"""Event emitted when authentication to an A2A agent fails.
|
||||
|
||||
Attributes:
|
||||
endpoint: A2A agent endpoint URL.
|
||||
auth_type: Type of authentication attempted (e.g., bearer, oauth2, api_key).
|
||||
error: Error message describing the failure.
|
||||
status_code: HTTP status code if applicable.
|
||||
a2a_agent_name: Name of the A2A agent if known.
|
||||
protocol_version: A2A protocol version being used.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
"""
|
||||
|
||||
type: str = "a2a_authentication_failed"
|
||||
endpoint: str
|
||||
auth_type: str | None = None
|
||||
error: str
|
||||
status_code: int | None = None
|
||||
a2a_agent_name: str | None = None
|
||||
protocol_version: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class A2AArtifactReceivedEvent(A2AEventBase):
|
||||
"""Event emitted when an artifact is received from a remote A2A agent.
|
||||
|
||||
Attributes:
|
||||
task_id: A2A task ID the artifact belongs to.
|
||||
artifact_id: Unique identifier for the artifact.
|
||||
artifact_name: Name of the artifact.
|
||||
artifact_description: Purpose description of the artifact.
|
||||
mime_type: MIME type of the artifact content.
|
||||
size_bytes: Size of the artifact in bytes.
|
||||
append: Whether content should be appended to existing artifact.
|
||||
last_chunk: Whether this is the final chunk of the artifact.
|
||||
endpoint: A2A agent endpoint URL.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
context_id: Context ID for correlation.
|
||||
turn_number: Current turn number (1-indexed).
|
||||
is_multiturn: Whether this is part of a multiturn conversation.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
extensions: List of A2A extension URIs in use.
|
||||
"""
|
||||
|
||||
type: str = "a2a_artifact_received"
|
||||
task_id: str
|
||||
artifact_id: str
|
||||
artifact_name: str | None = None
|
||||
artifact_description: str | None = None
|
||||
mime_type: str | None = None
|
||||
size_bytes: int | None = None
|
||||
append: bool = False
|
||||
last_chunk: bool = False
|
||||
endpoint: str | None = None
|
||||
a2a_agent_name: str | None = None
|
||||
context_id: str | None = None
|
||||
turn_number: int = 1
|
||||
is_multiturn: bool = False
|
||||
metadata: dict[str, Any] | None = None
|
||||
extensions: list[str] | None = None
|
||||
|
||||
|
||||
class A2AConnectionErrorEvent(A2AEventBase):
|
||||
"""Event emitted when a connection error occurs during A2A communication.
|
||||
|
||||
Attributes:
|
||||
endpoint: A2A agent endpoint URL.
|
||||
error: Error message describing the connection failure.
|
||||
error_type: Type of error (e.g., timeout, connection_refused, dns_error).
|
||||
status_code: HTTP status code if applicable.
|
||||
a2a_agent_name: Name of the A2A agent from agent card.
|
||||
operation: The operation being attempted when error occurred.
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
task_id: A2A task ID if applicable.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
"""
|
||||
|
||||
type: str = "a2a_connection_error"
|
||||
endpoint: str
|
||||
error: str
|
||||
error_type: str | None = None
|
||||
status_code: int | None = None
|
||||
a2a_agent_name: str | None = None
|
||||
operation: str | None = None
|
||||
context_id: str | None = None
|
||||
task_id: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class A2AServerTaskStartedEvent(A2AEventBase):
|
||||
"""Event emitted when an A2A server task execution starts.
|
||||
|
||||
Attributes:
|
||||
task_id: A2A task ID for this execution.
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
"""
|
||||
|
||||
type: str = "a2a_server_task_started"
|
||||
task_id: str
|
||||
context_id: str
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class A2AServerTaskCompletedEvent(A2AEventBase):
|
||||
"""Event emitted when an A2A server task execution completes.
|
||||
|
||||
Attributes:
|
||||
task_id: A2A task ID for this execution.
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
result: The task result.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
"""
|
||||
|
||||
type: str = "a2a_server_task_completed"
|
||||
task_id: str
|
||||
context_id: str
|
||||
result: str
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class A2AServerTaskCanceledEvent(A2AEventBase):
|
||||
"""Event emitted when an A2A server task execution is canceled.
|
||||
|
||||
Attributes:
|
||||
task_id: A2A task ID for this execution.
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
"""
|
||||
|
||||
type: str = "a2a_server_task_canceled"
|
||||
task_id: str
|
||||
context_id: str
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class A2AServerTaskFailedEvent(A2AEventBase):
|
||||
"""Event emitted when an A2A server task execution fails.
|
||||
|
||||
Attributes:
|
||||
task_id: A2A task ID for this execution.
|
||||
context_id: A2A context ID grouping related tasks.
|
||||
error: Error message describing the failure.
|
||||
metadata: Custom A2A metadata key-value pairs.
|
||||
"""
|
||||
|
||||
type: str = "a2a_server_task_failed"
|
||||
task_id: str
|
||||
context_id: str
|
||||
error: str
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class A2AParallelDelegationStartedEvent(A2AEventBase):
|
||||
"""Event emitted when parallel delegation to multiple A2A agents begins.
|
||||
|
||||
Attributes:
|
||||
endpoints: List of A2A agent endpoints being delegated to.
|
||||
task_description: Description of the task being delegated.
|
||||
"""
|
||||
|
||||
type: str = "a2a_parallel_delegation_started"
|
||||
endpoints: list[str]
|
||||
task_description: str
|
||||
|
||||
|
||||
class A2AParallelDelegationCompletedEvent(A2AEventBase):
|
||||
"""Event emitted when parallel delegation to multiple A2A agents completes.
|
||||
|
||||
Attributes:
|
||||
endpoints: List of A2A agent endpoints that were delegated to.
|
||||
success_count: Number of successful delegations.
|
||||
failure_count: Number of failed delegations.
|
||||
results: Summary of results from each agent.
|
||||
"""
|
||||
|
||||
type: str = "a2a_parallel_delegation_completed"
|
||||
endpoints: list[str]
|
||||
success_count: int
|
||||
failure_count: int
|
||||
results: dict[str, str] | None = None
|
||||
|
||||
@@ -114,7 +114,6 @@ To enable tracing, do any one of these:
|
||||
New streaming sessions will be created on-demand when needed.
|
||||
This method exists for API compatibility with HITL callers.
|
||||
"""
|
||||
pass
|
||||
|
||||
def print_panel(
|
||||
self, content: Text, title: str, style: str = "blue", is_flow: bool = False
|
||||
@@ -1417,3 +1416,49 @@ To enable tracing, do any one of these:
|
||||
panel = self.create_panel(content, "❌ MCP Tool Failed", "red")
|
||||
self.print(panel)
|
||||
self.print()
|
||||
|
||||
def handle_a2a_polling_started(
|
||||
self,
|
||||
task_id: str,
|
||||
polling_interval: float,
|
||||
endpoint: str,
|
||||
) -> None:
|
||||
"""Handle A2A polling started event with panel display."""
|
||||
content = Text()
|
||||
content.append("A2A Polling Started\n", style="cyan bold")
|
||||
content.append("Task ID: ", style="white")
|
||||
content.append(f"{task_id[:8]}...\n", style="cyan")
|
||||
content.append("Interval: ", style="white")
|
||||
content.append(f"{polling_interval}s\n", style="cyan")
|
||||
|
||||
self.print_panel(content, "⏳ A2A Polling", "cyan")
|
||||
|
||||
def handle_a2a_polling_status(
|
||||
self,
|
||||
task_id: str,
|
||||
state: str,
|
||||
elapsed_seconds: float,
|
||||
poll_count: int,
|
||||
) -> None:
|
||||
"""Handle A2A polling status event with panel display."""
|
||||
if state == "completed":
|
||||
style = "green"
|
||||
status_indicator = "✓"
|
||||
elif state == "failed":
|
||||
style = "red"
|
||||
status_indicator = "✗"
|
||||
elif state == "working":
|
||||
style = "yellow"
|
||||
status_indicator = "⋯"
|
||||
else:
|
||||
style = "cyan"
|
||||
status_indicator = "•"
|
||||
|
||||
content = Text()
|
||||
content.append(f"Poll #{poll_count}\n", style=f"{style} bold")
|
||||
content.append("Status: ", style="white")
|
||||
content.append(f"{status_indicator} {state}\n", style=style)
|
||||
content.append("Elapsed: ", style="white")
|
||||
content.append(f"{elapsed_seconds:.1f}s\n", style=style)
|
||||
|
||||
self.print_panel(content, f"📊 A2A Poll #{poll_count}", style)
|
||||
|
||||
@@ -5,6 +5,7 @@ from crewai.flow.async_feedback import (
|
||||
PendingFeedbackContext,
|
||||
)
|
||||
from crewai.flow.flow import Flow, and_, listen, or_, router, start
|
||||
from crewai.flow.flow_config import flow_config
|
||||
from crewai.flow.human_feedback import HumanFeedbackResult, human_feedback
|
||||
from crewai.flow.persistence import persist
|
||||
from crewai.flow.visualization import (
|
||||
@@ -24,6 +25,7 @@ __all__ = [
|
||||
"PendingFeedbackContext",
|
||||
"and_",
|
||||
"build_flow_structure",
|
||||
"flow_config",
|
||||
"human_feedback",
|
||||
"listen",
|
||||
"or_",
|
||||
|
||||
@@ -1203,7 +1203,13 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
result = self.kickoff(inputs=inputs)
|
||||
result_holder.append(result)
|
||||
except Exception as e:
|
||||
signal_error(state, e)
|
||||
# HumanFeedbackPending is expected control flow, not an error
|
||||
from crewai.flow.async_feedback.types import HumanFeedbackPending
|
||||
|
||||
if isinstance(e, HumanFeedbackPending):
|
||||
result_holder.append(e)
|
||||
else:
|
||||
signal_error(state, e)
|
||||
finally:
|
||||
self.stream = True
|
||||
signal_end(state)
|
||||
@@ -1258,7 +1264,13 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
result = await self.kickoff_async(inputs=inputs)
|
||||
result_holder.append(result)
|
||||
except Exception as e:
|
||||
signal_error(state, e, is_async=True)
|
||||
# HumanFeedbackPending is expected control flow, not an error
|
||||
from crewai.flow.async_feedback.types import HumanFeedbackPending
|
||||
|
||||
if isinstance(e, HumanFeedbackPending):
|
||||
result_holder.append(e)
|
||||
else:
|
||||
signal_error(state, e, is_async=True)
|
||||
finally:
|
||||
self.stream = True
|
||||
signal_end(state, is_async=True)
|
||||
@@ -1590,29 +1602,45 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
if not self.suppress_flow_events:
|
||||
# Check if this is a HumanFeedbackPending exception (paused, not failed)
|
||||
from crewai.flow.async_feedback.types import HumanFeedbackPending
|
||||
# Check if this is a HumanFeedbackPending exception (paused, not failed)
|
||||
from crewai.flow.async_feedback.types import HumanFeedbackPending
|
||||
|
||||
if isinstance(e, HumanFeedbackPending):
|
||||
# Auto-save pending feedback (create default persistence if needed)
|
||||
if self._persistence is None:
|
||||
from crewai.flow.persistence import SQLiteFlowPersistence
|
||||
if isinstance(e, HumanFeedbackPending):
|
||||
# Auto-save pending feedback (create default persistence if needed)
|
||||
if self._persistence is None:
|
||||
from crewai.flow.persistence import SQLiteFlowPersistence
|
||||
|
||||
self._persistence = SQLiteFlowPersistence()
|
||||
self._persistence = SQLiteFlowPersistence()
|
||||
|
||||
# Regular failure
|
||||
future = crewai_event_bus.emit(
|
||||
self,
|
||||
MethodExecutionFailedEvent(
|
||||
type="method_execution_failed",
|
||||
method_name=method_name,
|
||||
flow_name=self.name or self.__class__.__name__,
|
||||
error=e,
|
||||
),
|
||||
)
|
||||
if future:
|
||||
self._event_futures.append(future)
|
||||
# Emit paused event (not failed)
|
||||
if not self.suppress_flow_events:
|
||||
future = crewai_event_bus.emit(
|
||||
self,
|
||||
MethodExecutionPausedEvent(
|
||||
type="method_execution_paused",
|
||||
method_name=method_name,
|
||||
flow_name=self.name or self.__class__.__name__,
|
||||
state=self._copy_and_serialize_state(),
|
||||
flow_id=e.context.flow_id,
|
||||
message=e.context.message,
|
||||
emit=e.context.emit,
|
||||
),
|
||||
)
|
||||
if future:
|
||||
self._event_futures.append(future)
|
||||
elif not self.suppress_flow_events:
|
||||
# Regular failure - emit failed event
|
||||
future = crewai_event_bus.emit(
|
||||
self,
|
||||
MethodExecutionFailedEvent(
|
||||
type="method_execution_failed",
|
||||
method_name=method_name,
|
||||
flow_name=self.name or self.__class__.__name__,
|
||||
error=e,
|
||||
),
|
||||
)
|
||||
if future:
|
||||
self._event_futures.append(future)
|
||||
raise e
|
||||
|
||||
def _copy_and_serialize_state(self) -> dict[str, Any]:
|
||||
|
||||
39
lib/crewai/src/crewai/flow/flow_config.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Global Flow configuration.
|
||||
|
||||
This module provides a singleton configuration object that can be used to
|
||||
customize Flow behavior at runtime.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.flow.async_feedback.types import HumanFeedbackProvider
|
||||
|
||||
|
||||
class FlowConfig:
|
||||
"""Global configuration for Flow execution.
|
||||
|
||||
Attributes:
|
||||
hitl_provider: The human-in-the-loop feedback provider.
|
||||
Defaults to None (uses console input).
|
||||
Can be overridden by deployments at startup.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._hitl_provider: HumanFeedbackProvider | None = None
|
||||
|
||||
@property
|
||||
def hitl_provider(self) -> Any:
|
||||
"""Get the configured HITL provider."""
|
||||
return self._hitl_provider
|
||||
|
||||
@hitl_provider.setter
|
||||
def hitl_provider(self, provider: Any) -> None:
|
||||
"""Set the HITL provider."""
|
||||
self._hitl_provider = provider
|
||||
|
||||
|
||||
# Singleton instance
|
||||
flow_config = FlowConfig()
|
||||
@@ -1,4 +1,5 @@
|
||||
import inspect
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field, InstanceOf, model_validator
|
||||
from typing_extensions import Self
|
||||
@@ -14,14 +15,14 @@ class FlowTrackable(BaseModel):
|
||||
inspecting the call stack.
|
||||
"""
|
||||
|
||||
parent_flow: InstanceOf[Flow] | None = Field(
|
||||
parent_flow: InstanceOf[Flow[Any]] | None = Field(
|
||||
default=None,
|
||||
description="The parent flow of the instance, if it was created inside a flow.",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_parent_flow(self) -> Self:
|
||||
max_depth = 5
|
||||
max_depth = 8
|
||||
frame = inspect.currentframe()
|
||||
|
||||
try:
|
||||
|
||||
@@ -283,11 +283,18 @@ def human_feedback(
|
||||
llm=llm if isinstance(llm, str) else None,
|
||||
)
|
||||
|
||||
if provider is not None:
|
||||
# Use custom provider (may raise HumanFeedbackPending)
|
||||
return provider.request_feedback(context, flow_instance)
|
||||
# Determine effective provider:
|
||||
effective_provider = provider
|
||||
if effective_provider is None:
|
||||
from crewai.flow.flow_config import flow_config
|
||||
|
||||
effective_provider = flow_config.hitl_provider
|
||||
|
||||
if effective_provider is not None:
|
||||
# Use provider (may raise HumanFeedbackPending for async providers)
|
||||
return effective_provider.request_feedback(context, flow_instance)
|
||||
else:
|
||||
# Use default console input
|
||||
# Use default console input (local development)
|
||||
return flow_instance._request_human_feedback(
|
||||
message=message,
|
||||
output=method_output,
|
||||
|
||||
@@ -925,11 +925,12 @@ class LLM(BaseLLM):
|
||||
except Exception as e:
|
||||
logging.debug(f"Error checking for tool calls: {e}")
|
||||
|
||||
# Track token usage and log callbacks if available in streaming mode
|
||||
if usage_info:
|
||||
self._track_token_usage_internal(usage_info)
|
||||
self._handle_streaming_callbacks(callbacks, usage_info, last_chunk)
|
||||
|
||||
if not tool_calls or not available_functions:
|
||||
# Track token usage and log callbacks if available in streaming mode
|
||||
if usage_info:
|
||||
self._track_token_usage_internal(usage_info)
|
||||
self._handle_streaming_callbacks(callbacks, usage_info, last_chunk)
|
||||
|
||||
if response_model and self.is_litellm:
|
||||
instructor_instance = InternalInstructor(
|
||||
@@ -962,12 +963,7 @@ class LLM(BaseLLM):
|
||||
if tool_result is not None:
|
||||
return tool_result
|
||||
|
||||
# --- 10) Track token usage and log callbacks if available in streaming mode
|
||||
if usage_info:
|
||||
self._track_token_usage_internal(usage_info)
|
||||
self._handle_streaming_callbacks(callbacks, usage_info, last_chunk)
|
||||
|
||||
# --- 11) Emit completion event and return response
|
||||
# --- 10) Emit completion event and return response
|
||||
self._handle_emit_call_events(
|
||||
response=full_response,
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
@@ -1148,6 +1144,10 @@ class LLM(BaseLLM):
|
||||
if response_model:
|
||||
params["response_model"] = response_model
|
||||
response = litellm.completion(**params)
|
||||
|
||||
if hasattr(response,"usage") and not isinstance(response.usage, type) and response.usage:
|
||||
usage_info = response.usage
|
||||
self._track_token_usage_internal(usage_info)
|
||||
|
||||
except ContextWindowExceededError as e:
|
||||
# Convert litellm's context window error to our own exception type
|
||||
@@ -1185,8 +1185,6 @@ class LLM(BaseLLM):
|
||||
start_time=0,
|
||||
end_time=0,
|
||||
)
|
||||
self._track_token_usage_internal(usage_info)
|
||||
|
||||
# --- 4) Check for tool calls
|
||||
tool_calls = getattr(response_message, "tool_calls", [])
|
||||
|
||||
@@ -1275,6 +1273,10 @@ class LLM(BaseLLM):
|
||||
params["response_model"] = response_model
|
||||
response = await litellm.acompletion(**params)
|
||||
|
||||
if hasattr(response,"usage") and not isinstance(response.usage, type) and response.usage:
|
||||
usage_info = response.usage
|
||||
self._track_token_usage_internal(usage_info)
|
||||
|
||||
except ContextWindowExceededError as e:
|
||||
raise LLMContextLengthExceededError(str(e)) from e
|
||||
|
||||
@@ -1361,6 +1363,7 @@ class LLM(BaseLLM):
|
||||
"""
|
||||
full_response = ""
|
||||
chunk_count = 0
|
||||
|
||||
usage_info = None
|
||||
|
||||
accumulated_tool_args: defaultdict[int, AccumulatedToolArgs] = defaultdict(
|
||||
@@ -1446,6 +1449,9 @@ class LLM(BaseLLM):
|
||||
end_time=0,
|
||||
)
|
||||
|
||||
if usage_info:
|
||||
self._track_token_usage_internal(usage_info)
|
||||
|
||||
if accumulated_tool_args and available_functions:
|
||||
# Convert accumulated tool args to ChatCompletionDeltaToolCall objects
|
||||
tool_calls_list: list[ChatCompletionDeltaToolCall] = [
|
||||
|
||||
@@ -443,7 +443,7 @@ class AzureCompletion(BaseLLM):
|
||||
params["presence_penalty"] = self.presence_penalty
|
||||
if self.max_tokens is not None:
|
||||
params["max_tokens"] = self.max_tokens
|
||||
if self.stop:
|
||||
if self.stop and self.supports_stop_words():
|
||||
params["stop"] = self.stop
|
||||
|
||||
# Handle tools/functions for Azure OpenAI models
|
||||
@@ -931,8 +931,28 @@ class AzureCompletion(BaseLLM):
|
||||
return self.is_openai_model
|
||||
|
||||
def supports_stop_words(self) -> bool:
|
||||
"""Check if the model supports stop words."""
|
||||
return True # Most Azure models support stop sequences
|
||||
"""Check if the model supports stop words.
|
||||
|
||||
Models using the Responses API (GPT-5 family, o-series reasoning models,
|
||||
computer-use-preview) do not support stop sequences.
|
||||
See: https://learn.microsoft.com/en-us/azure/ai-foundry/foundry-models/concepts/models-sold-directly-by-azure
|
||||
"""
|
||||
model_lower = self.model.lower() if self.model else ""
|
||||
|
||||
if "gpt-5" in model_lower:
|
||||
return False
|
||||
|
||||
o_series_models = ["o1", "o3", "o4", "o1-mini", "o3-mini", "o4-mini"]
|
||||
|
||||
responses_api_models = ["computer-use-preview"]
|
||||
|
||||
unsupported_stop_models = o_series_models + responses_api_models
|
||||
|
||||
for unsupported in unsupported_stop_models:
|
||||
if unsupported in model_lower:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_context_window_size(self) -> int:
|
||||
"""Get the context window size for the model."""
|
||||
|
||||
@@ -969,3 +969,35 @@ class Telemetry:
|
||||
close_span(span)
|
||||
|
||||
self._safe_telemetry_operation(_operation)
|
||||
|
||||
def human_feedback_span(
|
||||
self,
|
||||
event_type: str,
|
||||
has_routing: bool,
|
||||
num_outcomes: int = 0,
|
||||
feedback_provided: bool | None = None,
|
||||
outcome: str | None = None,
|
||||
) -> None:
|
||||
"""Records human feedback feature usage.
|
||||
|
||||
Args:
|
||||
event_type: Type of event - "requested" or "received".
|
||||
has_routing: Whether emit options were configured for routing.
|
||||
num_outcomes: Number of possible outcomes if routing is used.
|
||||
feedback_provided: Whether user provided feedback or skipped (None if requested).
|
||||
outcome: The collapsed outcome string if routing was used.
|
||||
"""
|
||||
|
||||
def _operation() -> None:
|
||||
tracer = trace.get_tracer("crewai.telemetry")
|
||||
span = tracer.start_span("Human Feedback")
|
||||
self._add_attribute(span, "event_type", event_type)
|
||||
self._add_attribute(span, "has_routing", has_routing)
|
||||
self._add_attribute(span, "num_outcomes", num_outcomes)
|
||||
if feedback_provided is not None:
|
||||
self._add_attribute(span, "feedback_provided", feedback_provided)
|
||||
if outcome is not None:
|
||||
self._add_attribute(span, "outcome", outcome)
|
||||
close_span(span)
|
||||
|
||||
self._safe_telemetry_operation(_operation)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"""Utilities for creating and manipulating types."""
|
||||
|
||||
from typing import Annotated, Final, Literal
|
||||
|
||||
from typing_extensions import TypeAliasType
|
||||
from typing import Annotated, Final, Literal, cast
|
||||
|
||||
|
||||
_DYNAMIC_LITERAL_ALIAS: Final[Literal["DynamicLiteral"]] = "DynamicLiteral"
|
||||
@@ -20,6 +18,11 @@ def create_literals_from_strings(
|
||||
|
||||
Returns:
|
||||
Literal type for each A2A agent ID
|
||||
|
||||
Raises:
|
||||
ValueError: If values is empty (Literal requires at least one value)
|
||||
"""
|
||||
unique_values: tuple[str, ...] = tuple(dict.fromkeys(values))
|
||||
return Literal.__getitem__(unique_values)
|
||||
if not unique_values:
|
||||
raise ValueError("Cannot create Literal type from empty values")
|
||||
return cast(type, Literal.__getitem__(unique_values))
|
||||
|
||||
323
lib/crewai/tests/a2a/test_a2a_integration.py
Normal file
@@ -0,0 +1,323 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from a2a.client import ClientFactory
|
||||
from a2a.types import AgentCard, Message, Part, Role, TaskState, TextPart
|
||||
|
||||
from crewai.a2a.updates.polling.handler import PollingHandler
|
||||
from crewai.a2a.updates.streaming.handler import StreamingHandler
|
||||
|
||||
|
||||
A2A_TEST_ENDPOINT = os.getenv("A2A_TEST_ENDPOINT", "http://localhost:9999")
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def a2a_client():
|
||||
"""Create A2A client for test server."""
|
||||
client = await ClientFactory.connect(A2A_TEST_ENDPOINT)
|
||||
yield client
|
||||
await client.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_message() -> Message:
|
||||
"""Create a simple test message."""
|
||||
return Message(
|
||||
role=Role.user,
|
||||
parts=[Part(root=TextPart(text="What is 2 + 2?"))],
|
||||
message_id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def agent_card(a2a_client) -> AgentCard:
|
||||
"""Fetch the real agent card from the server."""
|
||||
return await a2a_client.get_card()
|
||||
|
||||
|
||||
class TestA2AAgentCardFetching:
|
||||
"""Integration tests for agent card fetching."""
|
||||
|
||||
@pytest.mark.vcr()
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_agent_card(self, a2a_client) -> None:
|
||||
"""Test fetching an agent card from the server."""
|
||||
card = await a2a_client.get_card()
|
||||
|
||||
assert card is not None
|
||||
assert card.name == "GPT Assistant"
|
||||
assert card.url is not None
|
||||
assert card.capabilities is not None
|
||||
assert card.capabilities.streaming is True
|
||||
|
||||
|
||||
class TestA2APollingIntegration:
|
||||
"""Integration tests for A2A polling handler."""
|
||||
|
||||
@pytest.mark.vcr()
|
||||
@pytest.mark.asyncio
|
||||
async def test_polling_completes_task(
|
||||
self,
|
||||
a2a_client,
|
||||
test_message: Message,
|
||||
agent_card: AgentCard,
|
||||
) -> None:
|
||||
"""Test that polling handler completes a task successfully."""
|
||||
new_messages: list[Message] = []
|
||||
|
||||
result = await PollingHandler.execute(
|
||||
client=a2a_client,
|
||||
message=test_message,
|
||||
new_messages=new_messages,
|
||||
agent_card=agent_card,
|
||||
polling_interval=0.5,
|
||||
polling_timeout=30.0,
|
||||
)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["status"] == TaskState.completed
|
||||
assert result.get("result") is not None
|
||||
assert "4" in result["result"]
|
||||
|
||||
|
||||
class TestA2AStreamingIntegration:
|
||||
"""Integration tests for A2A streaming handler."""
|
||||
|
||||
@pytest.mark.vcr()
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_completes_task(
|
||||
self,
|
||||
a2a_client,
|
||||
test_message: Message,
|
||||
agent_card: AgentCard,
|
||||
) -> None:
|
||||
"""Test that streaming handler completes a task successfully."""
|
||||
new_messages: list[Message] = []
|
||||
|
||||
result = await StreamingHandler.execute(
|
||||
client=a2a_client,
|
||||
message=test_message,
|
||||
new_messages=new_messages,
|
||||
agent_card=agent_card,
|
||||
)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert result["status"] == TaskState.completed
|
||||
assert result.get("result") is not None
|
||||
|
||||
|
||||
class TestA2ATaskOperations:
|
||||
"""Integration tests for task operations."""
|
||||
|
||||
@pytest.mark.vcr()
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_message_and_get_response(
|
||||
self,
|
||||
a2a_client,
|
||||
test_message: Message,
|
||||
) -> None:
|
||||
"""Test sending a message and getting a response."""
|
||||
from a2a.types import Task
|
||||
|
||||
final_task: Task | None = None
|
||||
async for event in a2a_client.send_message(test_message):
|
||||
if isinstance(event, tuple) and len(event) >= 1:
|
||||
task, _ = event
|
||||
if isinstance(task, Task):
|
||||
final_task = task
|
||||
|
||||
assert final_task is not None
|
||||
assert final_task.id is not None
|
||||
assert final_task.status is not None
|
||||
assert final_task.status.state == TaskState.completed
|
||||
|
||||
|
||||
class TestA2APushNotificationHandler:
|
||||
"""Tests for push notification handler.
|
||||
|
||||
These tests use mocks for the result store since webhook callbacks
|
||||
are incoming requests that can't be recorded with VCR.
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_agent_card(self) -> AgentCard:
|
||||
"""Create a minimal valid agent card for testing."""
|
||||
from a2a.types import AgentCapabilities
|
||||
|
||||
return AgentCard(
|
||||
name="Test Agent",
|
||||
description="Test agent for push notification tests",
|
||||
url="http://localhost:9999",
|
||||
version="1.0.0",
|
||||
capabilities=AgentCapabilities(streaming=True, push_notifications=True),
|
||||
default_input_modes=["text"],
|
||||
default_output_modes=["text"],
|
||||
skills=[],
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_task(self) -> "Task":
|
||||
"""Create a minimal valid task for testing."""
|
||||
from a2a.types import Task, TaskStatus
|
||||
|
||||
return Task(
|
||||
id="task-123",
|
||||
context_id="ctx-123",
|
||||
status=TaskStatus(state=TaskState.working),
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_push_handler_waits_for_result(
|
||||
self,
|
||||
mock_agent_card: AgentCard,
|
||||
mock_task,
|
||||
) -> None:
|
||||
"""Test that push handler waits for result from store."""
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from a2a.types import Task, TaskStatus
|
||||
from pydantic import AnyHttpUrl
|
||||
|
||||
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
|
||||
from crewai.a2a.updates.push_notifications.handler import PushNotificationHandler
|
||||
|
||||
completed_task = Task(
|
||||
id="task-123",
|
||||
context_id="ctx-123",
|
||||
status=TaskStatus(state=TaskState.completed),
|
||||
history=[],
|
||||
)
|
||||
|
||||
mock_store = MagicMock()
|
||||
mock_store.wait_for_result = AsyncMock(return_value=completed_task)
|
||||
|
||||
async def mock_send_message(*args, **kwargs):
|
||||
yield (mock_task, None)
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.send_message = mock_send_message
|
||||
|
||||
config = PushNotificationConfig(
|
||||
url=AnyHttpUrl("http://localhost:8080/a2a/callback"),
|
||||
token="secret-token",
|
||||
result_store=mock_store,
|
||||
)
|
||||
|
||||
test_msg = Message(
|
||||
role=Role.user,
|
||||
parts=[Part(root=TextPart(text="What is 2+2?"))],
|
||||
message_id="msg-001",
|
||||
)
|
||||
|
||||
new_messages: list[Message] = []
|
||||
|
||||
result = await PushNotificationHandler.execute(
|
||||
client=mock_client,
|
||||
message=test_msg,
|
||||
new_messages=new_messages,
|
||||
agent_card=mock_agent_card,
|
||||
config=config,
|
||||
result_store=mock_store,
|
||||
polling_timeout=30.0,
|
||||
polling_interval=1.0,
|
||||
)
|
||||
|
||||
mock_store.wait_for_result.assert_called_once_with(
|
||||
task_id="task-123",
|
||||
timeout=30.0,
|
||||
poll_interval=1.0,
|
||||
)
|
||||
|
||||
assert result["status"] == TaskState.completed
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_push_handler_returns_failure_on_timeout(
|
||||
self,
|
||||
mock_agent_card: AgentCard,
|
||||
) -> None:
|
||||
"""Test that push handler returns failure when result store times out."""
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from a2a.types import Task, TaskStatus
|
||||
from pydantic import AnyHttpUrl
|
||||
|
||||
from crewai.a2a.updates.push_notifications.config import PushNotificationConfig
|
||||
from crewai.a2a.updates.push_notifications.handler import PushNotificationHandler
|
||||
|
||||
mock_store = MagicMock()
|
||||
mock_store.wait_for_result = AsyncMock(return_value=None)
|
||||
|
||||
working_task = Task(
|
||||
id="task-456",
|
||||
context_id="ctx-456",
|
||||
status=TaskStatus(state=TaskState.working),
|
||||
)
|
||||
|
||||
async def mock_send_message(*args, **kwargs):
|
||||
yield (working_task, None)
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.send_message = mock_send_message
|
||||
|
||||
config = PushNotificationConfig(
|
||||
url=AnyHttpUrl("http://localhost:8080/a2a/callback"),
|
||||
token="token",
|
||||
result_store=mock_store,
|
||||
)
|
||||
|
||||
test_msg = Message(
|
||||
role=Role.user,
|
||||
parts=[Part(root=TextPart(text="test"))],
|
||||
message_id="msg-002",
|
||||
)
|
||||
|
||||
new_messages: list[Message] = []
|
||||
|
||||
result = await PushNotificationHandler.execute(
|
||||
client=mock_client,
|
||||
message=test_msg,
|
||||
new_messages=new_messages,
|
||||
agent_card=mock_agent_card,
|
||||
config=config,
|
||||
result_store=mock_store,
|
||||
polling_timeout=5.0,
|
||||
polling_interval=0.5,
|
||||
)
|
||||
|
||||
assert result["status"] == TaskState.failed
|
||||
assert "timeout" in result.get("error", "").lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_push_handler_requires_config(
|
||||
self,
|
||||
mock_agent_card: AgentCard,
|
||||
) -> None:
|
||||
"""Test that push handler fails gracefully without config."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from crewai.a2a.updates.push_notifications.handler import PushNotificationHandler
|
||||
|
||||
mock_client = MagicMock()
|
||||
|
||||
test_msg = Message(
|
||||
role=Role.user,
|
||||
parts=[Part(root=TextPart(text="test"))],
|
||||
message_id="msg-003",
|
||||
)
|
||||
|
||||
new_messages: list[Message] = []
|
||||
|
||||
result = await PushNotificationHandler.execute(
|
||||
client=mock_client,
|
||||
message=test_msg,
|
||||
new_messages=new_messages,
|
||||
agent_card=mock_agent_card,
|
||||
)
|
||||
|
||||
assert result["status"] == TaskState.failed
|
||||
assert "config" in result.get("error", "").lower()
|
||||
325
lib/crewai/tests/a2a/utils/test_agent_card.py
Normal file
@@ -0,0 +1,325 @@
|
||||
"""Tests for A2A agent card utilities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from a2a.types import AgentCard, AgentSkill
|
||||
|
||||
from crewai import Agent
|
||||
from crewai.a2a.config import A2AClientConfig, A2AServerConfig
|
||||
from crewai.a2a.utils.agent_card import inject_a2a_server_methods
|
||||
|
||||
|
||||
class TestInjectA2AServerMethods:
|
||||
"""Tests for inject_a2a_server_methods function."""
|
||||
|
||||
def test_agent_with_server_config_gets_to_agent_card_method(self) -> None:
|
||||
"""Agent with A2AServerConfig should have to_agent_card method injected."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
a2a=A2AServerConfig(),
|
||||
)
|
||||
|
||||
assert hasattr(agent, "to_agent_card")
|
||||
assert callable(agent.to_agent_card)
|
||||
|
||||
def test_agent_without_server_config_no_injection(self) -> None:
|
||||
"""Agent without A2AServerConfig should not get to_agent_card method."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
a2a=A2AClientConfig(endpoint="http://example.com"),
|
||||
)
|
||||
|
||||
assert not hasattr(agent, "to_agent_card")
|
||||
|
||||
def test_agent_without_a2a_no_injection(self) -> None:
|
||||
"""Agent without any a2a config should not get to_agent_card method."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
)
|
||||
|
||||
assert not hasattr(agent, "to_agent_card")
|
||||
|
||||
def test_agent_with_mixed_configs_gets_injection(self) -> None:
|
||||
"""Agent with list containing A2AServerConfig should get to_agent_card."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
a2a=[
|
||||
A2AClientConfig(endpoint="http://example.com"),
|
||||
A2AServerConfig(name="My Agent"),
|
||||
],
|
||||
)
|
||||
|
||||
assert hasattr(agent, "to_agent_card")
|
||||
assert callable(agent.to_agent_card)
|
||||
|
||||
def test_manual_injection_on_plain_agent(self) -> None:
|
||||
"""inject_a2a_server_methods should work when called manually."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
)
|
||||
# Manually set server config and inject
|
||||
object.__setattr__(agent, "a2a", A2AServerConfig())
|
||||
inject_a2a_server_methods(agent)
|
||||
|
||||
assert hasattr(agent, "to_agent_card")
|
||||
assert callable(agent.to_agent_card)
|
||||
|
||||
|
||||
class TestToAgentCard:
|
||||
"""Tests for the injected to_agent_card method."""
|
||||
|
||||
def test_returns_agent_card(self) -> None:
|
||||
"""to_agent_card should return an AgentCard instance."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
a2a=A2AServerConfig(),
|
||||
)
|
||||
|
||||
card = agent.to_agent_card("http://localhost:8000")
|
||||
|
||||
assert isinstance(card, AgentCard)
|
||||
|
||||
def test_uses_agent_role_as_name(self) -> None:
|
||||
"""AgentCard name should default to agent role."""
|
||||
agent = Agent(
|
||||
role="Data Analyst",
|
||||
goal="Analyze data",
|
||||
backstory="Expert analyst",
|
||||
a2a=A2AServerConfig(),
|
||||
)
|
||||
|
||||
card = agent.to_agent_card("http://localhost:8000")
|
||||
|
||||
assert card.name == "Data Analyst"
|
||||
|
||||
def test_uses_server_config_name(self) -> None:
|
||||
"""AgentCard name should prefer A2AServerConfig.name over role."""
|
||||
agent = Agent(
|
||||
role="Data Analyst",
|
||||
goal="Analyze data",
|
||||
backstory="Expert analyst",
|
||||
a2a=A2AServerConfig(name="Custom Agent Name"),
|
||||
)
|
||||
|
||||
card = agent.to_agent_card("http://localhost:8000")
|
||||
|
||||
assert card.name == "Custom Agent Name"
|
||||
|
||||
def test_uses_goal_as_description(self) -> None:
|
||||
"""AgentCard description should include agent goal."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Accomplish important tasks",
|
||||
backstory="Has extensive experience",
|
||||
a2a=A2AServerConfig(),
|
||||
)
|
||||
|
||||
card = agent.to_agent_card("http://localhost:8000")
|
||||
|
||||
assert "Accomplish important tasks" in card.description
|
||||
|
||||
def test_uses_server_config_description(self) -> None:
|
||||
"""AgentCard description should prefer A2AServerConfig.description."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Accomplish important tasks",
|
||||
backstory="Has extensive experience",
|
||||
a2a=A2AServerConfig(description="Custom description"),
|
||||
)
|
||||
|
||||
card = agent.to_agent_card("http://localhost:8000")
|
||||
|
||||
assert card.description == "Custom description"
|
||||
|
||||
def test_uses_provided_url(self) -> None:
|
||||
"""AgentCard url should use the provided URL."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
a2a=A2AServerConfig(),
|
||||
)
|
||||
|
||||
card = agent.to_agent_card("http://my-server.com:9000")
|
||||
|
||||
assert card.url == "http://my-server.com:9000"
|
||||
|
||||
def test_uses_server_config_url(self) -> None:
|
||||
"""AgentCard url should prefer A2AServerConfig.url over provided URL."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
a2a=A2AServerConfig(url="http://configured-url.com"),
|
||||
)
|
||||
|
||||
card = agent.to_agent_card("http://fallback-url.com")
|
||||
|
||||
assert card.url == "http://configured-url.com/"
|
||||
|
||||
def test_generates_default_skill(self) -> None:
|
||||
"""AgentCard should have at least one skill based on agent role."""
|
||||
agent = Agent(
|
||||
role="Research Assistant",
|
||||
goal="Help with research",
|
||||
backstory="Skilled researcher",
|
||||
a2a=A2AServerConfig(),
|
||||
)
|
||||
|
||||
card = agent.to_agent_card("http://localhost:8000")
|
||||
|
||||
assert len(card.skills) >= 1
|
||||
skill = card.skills[0]
|
||||
assert skill.name == "Research Assistant"
|
||||
assert skill.description == "Help with research"
|
||||
|
||||
def test_uses_server_config_skills(self) -> None:
|
||||
"""AgentCard skills should prefer A2AServerConfig.skills."""
|
||||
custom_skill = AgentSkill(
|
||||
id="custom-skill",
|
||||
name="Custom Skill",
|
||||
description="A custom skill",
|
||||
tags=["custom"],
|
||||
)
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
a2a=A2AServerConfig(skills=[custom_skill]),
|
||||
)
|
||||
|
||||
card = agent.to_agent_card("http://localhost:8000")
|
||||
|
||||
assert len(card.skills) == 1
|
||||
assert card.skills[0].id == "custom-skill"
|
||||
assert card.skills[0].name == "Custom Skill"
|
||||
|
||||
def test_includes_custom_version(self) -> None:
|
||||
"""AgentCard should include version from A2AServerConfig."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
a2a=A2AServerConfig(version="2.0.0"),
|
||||
)
|
||||
|
||||
card = agent.to_agent_card("http://localhost:8000")
|
||||
|
||||
assert card.version == "2.0.0"
|
||||
|
||||
def test_default_version(self) -> None:
|
||||
"""AgentCard should have default version 1.0.0."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
a2a=A2AServerConfig(),
|
||||
)
|
||||
|
||||
card = agent.to_agent_card("http://localhost:8000")
|
||||
|
||||
assert card.version == "1.0.0"
|
||||
|
||||
|
||||
class TestAgentCardJsonStructure:
|
||||
"""Tests for the JSON structure of AgentCard."""
|
||||
|
||||
def test_json_has_required_fields(self) -> None:
|
||||
"""AgentCard JSON should contain all required A2A protocol fields."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
a2a=A2AServerConfig(),
|
||||
)
|
||||
|
||||
card = agent.to_agent_card("http://localhost:8000")
|
||||
json_data = card.model_dump()
|
||||
|
||||
assert "name" in json_data
|
||||
assert "description" in json_data
|
||||
assert "url" in json_data
|
||||
assert "version" in json_data
|
||||
assert "skills" in json_data
|
||||
assert "capabilities" in json_data
|
||||
assert "defaultInputModes" in json_data
|
||||
assert "defaultOutputModes" in json_data
|
||||
|
||||
def test_json_skills_structure(self) -> None:
|
||||
"""Each skill in JSON should have required fields."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
a2a=A2AServerConfig(),
|
||||
)
|
||||
|
||||
card = agent.to_agent_card("http://localhost:8000")
|
||||
json_data = card.model_dump()
|
||||
|
||||
assert len(json_data["skills"]) >= 1
|
||||
skill = json_data["skills"][0]
|
||||
assert "id" in skill
|
||||
assert "name" in skill
|
||||
assert "description" in skill
|
||||
assert "tags" in skill
|
||||
|
||||
def test_json_capabilities_structure(self) -> None:
|
||||
"""Capabilities in JSON should have expected fields."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
a2a=A2AServerConfig(),
|
||||
)
|
||||
|
||||
card = agent.to_agent_card("http://localhost:8000")
|
||||
json_data = card.model_dump()
|
||||
|
||||
capabilities = json_data["capabilities"]
|
||||
assert "streaming" in capabilities
|
||||
assert "pushNotifications" in capabilities
|
||||
|
||||
def test_json_serializable(self) -> None:
|
||||
"""AgentCard should be JSON serializable."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
a2a=A2AServerConfig(),
|
||||
)
|
||||
|
||||
card = agent.to_agent_card("http://localhost:8000")
|
||||
json_str = card.model_dump_json()
|
||||
|
||||
assert isinstance(json_str, str)
|
||||
assert "Test Agent" in json_str
|
||||
assert "http://localhost:8000" in json_str
|
||||
|
||||
def test_json_excludes_none_values(self) -> None:
|
||||
"""AgentCard JSON with exclude_none should omit None fields."""
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
a2a=A2AServerConfig(),
|
||||
)
|
||||
|
||||
card = agent.to_agent_card("http://localhost:8000")
|
||||
json_data = card.model_dump(exclude_none=True)
|
||||
|
||||
assert "provider" not in json_data
|
||||
assert "documentationUrl" not in json_data
|
||||
assert "iconUrl" not in json_data
|
||||
374
lib/crewai/tests/a2a/utils/test_task.py
Normal file
@@ -0,0 +1,374 @@
|
||||
"""Tests for A2A task utilities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from a2a.server.agent_execution import RequestContext
|
||||
from a2a.server.events import EventQueue
|
||||
from a2a.types import Message, Task as A2ATask, TaskState, TaskStatus
|
||||
|
||||
from crewai.a2a.utils.task import cancel, cancellable, execute
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_agent() -> MagicMock:
|
||||
"""Create a mock CrewAI agent."""
|
||||
agent = MagicMock()
|
||||
agent.role = "Test Agent"
|
||||
agent.tools = []
|
||||
agent.aexecute_task = AsyncMock(return_value="Task completed successfully")
|
||||
return agent
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_task(mock_context: MagicMock) -> MagicMock:
|
||||
"""Create a mock Task."""
|
||||
task = MagicMock()
|
||||
task.id = mock_context.task_id
|
||||
task.name = "Mock Task"
|
||||
task.description = "Mock task description"
|
||||
return task
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_context() -> MagicMock:
|
||||
"""Create a mock RequestContext."""
|
||||
context = MagicMock(spec=RequestContext)
|
||||
context.task_id = "test-task-123"
|
||||
context.context_id = "test-context-456"
|
||||
context.get_user_input.return_value = "Test user message"
|
||||
context.message = MagicMock(spec=Message)
|
||||
context.current_task = None
|
||||
return context
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_event_queue() -> AsyncMock:
|
||||
"""Create a mock EventQueue."""
|
||||
queue = AsyncMock(spec=EventQueue)
|
||||
queue.enqueue_event = AsyncMock()
|
||||
return queue
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(autouse=True)
|
||||
async def clear_cache(mock_context: MagicMock) -> None:
|
||||
"""Clear cancel flag from cache before each test."""
|
||||
from aiocache import caches
|
||||
|
||||
cache = caches.get("default")
|
||||
await cache.delete(f"cancel:{mock_context.task_id}")
|
||||
|
||||
|
||||
class TestCancellableDecorator:
|
||||
"""Tests for the cancellable decorator."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_executes_function_without_context(self) -> None:
|
||||
"""Function executes normally when no RequestContext is provided."""
|
||||
call_count = 0
|
||||
|
||||
@cancellable
|
||||
async def my_func(value: int) -> int:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return value * 2
|
||||
|
||||
result = await my_func(5)
|
||||
|
||||
assert result == 10
|
||||
assert call_count == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_executes_function_with_context(self, mock_context: MagicMock) -> None:
|
||||
"""Function executes normally with RequestContext when not cancelled."""
|
||||
@cancellable
|
||||
async def my_func(context: RequestContext) -> str:
|
||||
await asyncio.sleep(0.01)
|
||||
return "completed"
|
||||
|
||||
result = await my_func(mock_context)
|
||||
|
||||
assert result == "completed"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancellation_raises_cancelled_error(
|
||||
self, mock_context: MagicMock
|
||||
) -> None:
|
||||
"""Function raises CancelledError when cancel flag is set."""
|
||||
from aiocache import caches
|
||||
|
||||
cache = caches.get("default")
|
||||
|
||||
@cancellable
|
||||
async def slow_func(context: RequestContext) -> str:
|
||||
await asyncio.sleep(1.0)
|
||||
return "should not reach"
|
||||
|
||||
await cache.set(f"cancel:{mock_context.task_id}", True)
|
||||
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await slow_func(mock_context)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cleanup_removes_cancel_flag(self, mock_context: MagicMock) -> None:
|
||||
"""Cancel flag is cleaned up after execution."""
|
||||
from aiocache import caches
|
||||
|
||||
cache = caches.get("default")
|
||||
|
||||
@cancellable
|
||||
async def quick_func(context: RequestContext) -> str:
|
||||
return "done"
|
||||
|
||||
await quick_func(mock_context)
|
||||
|
||||
flag = await cache.get(f"cancel:{mock_context.task_id}")
|
||||
assert flag is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_extracts_context_from_kwargs(self, mock_context: MagicMock) -> None:
|
||||
"""Context can be passed as keyword argument."""
|
||||
@cancellable
|
||||
async def my_func(value: int, context: RequestContext | None = None) -> int:
|
||||
return value + 1
|
||||
|
||||
result = await my_func(10, context=mock_context)
|
||||
|
||||
assert result == 11
|
||||
|
||||
|
||||
class TestExecute:
|
||||
"""Tests for the execute function."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_successful_execution(
|
||||
self,
|
||||
mock_agent: MagicMock,
|
||||
mock_context: MagicMock,
|
||||
mock_event_queue: AsyncMock,
|
||||
mock_task: MagicMock,
|
||||
) -> None:
|
||||
"""Execute completes successfully and enqueues completed task."""
|
||||
with (
|
||||
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
|
||||
patch("crewai.a2a.utils.task.crewai_event_bus") as mock_bus,
|
||||
):
|
||||
await execute(mock_agent, mock_context, mock_event_queue)
|
||||
|
||||
mock_agent.aexecute_task.assert_called_once()
|
||||
mock_event_queue.enqueue_event.assert_called_once()
|
||||
assert mock_bus.emit.call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_emits_started_event(
|
||||
self,
|
||||
mock_agent: MagicMock,
|
||||
mock_context: MagicMock,
|
||||
mock_event_queue: AsyncMock,
|
||||
mock_task: MagicMock,
|
||||
) -> None:
|
||||
"""Execute emits A2AServerTaskStartedEvent."""
|
||||
with (
|
||||
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
|
||||
patch("crewai.a2a.utils.task.crewai_event_bus") as mock_bus,
|
||||
):
|
||||
await execute(mock_agent, mock_context, mock_event_queue)
|
||||
|
||||
first_call = mock_bus.emit.call_args_list[0]
|
||||
event = first_call[0][1]
|
||||
|
||||
assert event.type == "a2a_server_task_started"
|
||||
assert event.task_id == mock_context.task_id
|
||||
assert event.context_id == mock_context.context_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_emits_completed_event(
|
||||
self,
|
||||
mock_agent: MagicMock,
|
||||
mock_context: MagicMock,
|
||||
mock_event_queue: AsyncMock,
|
||||
mock_task: MagicMock,
|
||||
) -> None:
|
||||
"""Execute emits A2AServerTaskCompletedEvent on success."""
|
||||
with (
|
||||
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
|
||||
patch("crewai.a2a.utils.task.crewai_event_bus") as mock_bus,
|
||||
):
|
||||
await execute(mock_agent, mock_context, mock_event_queue)
|
||||
|
||||
second_call = mock_bus.emit.call_args_list[1]
|
||||
event = second_call[0][1]
|
||||
|
||||
assert event.type == "a2a_server_task_completed"
|
||||
assert event.task_id == mock_context.task_id
|
||||
assert event.result == "Task completed successfully"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_emits_failed_event_on_exception(
|
||||
self,
|
||||
mock_agent: MagicMock,
|
||||
mock_context: MagicMock,
|
||||
mock_event_queue: AsyncMock,
|
||||
mock_task: MagicMock,
|
||||
) -> None:
|
||||
"""Execute emits A2AServerTaskFailedEvent on exception."""
|
||||
mock_agent.aexecute_task = AsyncMock(side_effect=ValueError("Test error"))
|
||||
|
||||
with (
|
||||
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
|
||||
patch("crewai.a2a.utils.task.crewai_event_bus") as mock_bus,
|
||||
):
|
||||
with pytest.raises(Exception):
|
||||
await execute(mock_agent, mock_context, mock_event_queue)
|
||||
|
||||
failed_call = mock_bus.emit.call_args_list[1]
|
||||
event = failed_call[0][1]
|
||||
|
||||
assert event.type == "a2a_server_task_failed"
|
||||
assert "Test error" in event.error
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_emits_canceled_event_on_cancellation(
|
||||
self,
|
||||
mock_agent: MagicMock,
|
||||
mock_context: MagicMock,
|
||||
mock_event_queue: AsyncMock,
|
||||
mock_task: MagicMock,
|
||||
) -> None:
|
||||
"""Execute emits A2AServerTaskCanceledEvent on CancelledError."""
|
||||
mock_agent.aexecute_task = AsyncMock(side_effect=asyncio.CancelledError())
|
||||
|
||||
with (
|
||||
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
|
||||
patch("crewai.a2a.utils.task.crewai_event_bus") as mock_bus,
|
||||
):
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await execute(mock_agent, mock_context, mock_event_queue)
|
||||
|
||||
canceled_call = mock_bus.emit.call_args_list[1]
|
||||
event = canceled_call[0][1]
|
||||
|
||||
assert event.type == "a2a_server_task_canceled"
|
||||
assert event.task_id == mock_context.task_id
|
||||
|
||||
|
||||
class TestCancel:
|
||||
"""Tests for the cancel function."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sets_cancel_flag_in_cache(
|
||||
self,
|
||||
mock_context: MagicMock,
|
||||
mock_event_queue: AsyncMock,
|
||||
) -> None:
|
||||
"""Cancel sets the cancel flag in cache."""
|
||||
from aiocache import caches
|
||||
|
||||
cache = caches.get("default")
|
||||
|
||||
await cancel(mock_context, mock_event_queue)
|
||||
|
||||
flag = await cache.get(f"cancel:{mock_context.task_id}")
|
||||
assert flag is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_enqueues_task_status_update_event(
|
||||
self,
|
||||
mock_context: MagicMock,
|
||||
mock_event_queue: AsyncMock,
|
||||
) -> None:
|
||||
"""Cancel enqueues TaskStatusUpdateEvent with canceled state."""
|
||||
await cancel(mock_context, mock_event_queue)
|
||||
|
||||
mock_event_queue.enqueue_event.assert_called_once()
|
||||
event = mock_event_queue.enqueue_event.call_args[0][0]
|
||||
|
||||
assert event.task_id == mock_context.task_id
|
||||
assert event.context_id == mock_context.context_id
|
||||
assert event.status.state == TaskState.canceled
|
||||
assert event.final is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_none_when_no_current_task(
|
||||
self,
|
||||
mock_context: MagicMock,
|
||||
mock_event_queue: AsyncMock,
|
||||
) -> None:
|
||||
"""Cancel returns None when context has no current_task."""
|
||||
mock_context.current_task = None
|
||||
|
||||
result = await cancel(mock_context, mock_event_queue)
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_updated_task_when_current_task_exists(
|
||||
self,
|
||||
mock_context: MagicMock,
|
||||
mock_event_queue: AsyncMock,
|
||||
) -> None:
|
||||
"""Cancel returns updated task when context has current_task."""
|
||||
current_task = MagicMock(spec=A2ATask)
|
||||
current_task.status = TaskStatus(state=TaskState.working)
|
||||
mock_context.current_task = current_task
|
||||
|
||||
result = await cancel(mock_context, mock_event_queue)
|
||||
|
||||
assert result is current_task
|
||||
assert result.status.state == TaskState.canceled
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cleanup_after_cancel(
|
||||
self,
|
||||
mock_context: MagicMock,
|
||||
mock_event_queue: AsyncMock,
|
||||
) -> None:
|
||||
"""Cancel flag persists for cancellable decorator to detect."""
|
||||
from aiocache import caches
|
||||
|
||||
cache = caches.get("default")
|
||||
|
||||
await cancel(mock_context, mock_event_queue)
|
||||
|
||||
flag = await cache.get(f"cancel:{mock_context.task_id}")
|
||||
assert flag is True
|
||||
|
||||
await cache.delete(f"cancel:{mock_context.task_id}")
|
||||
|
||||
|
||||
class TestExecuteAndCancelIntegration:
|
||||
"""Integration tests for execute and cancel working together."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_stops_running_execute(
|
||||
self,
|
||||
mock_agent: MagicMock,
|
||||
mock_context: MagicMock,
|
||||
mock_event_queue: AsyncMock,
|
||||
mock_task: MagicMock,
|
||||
) -> None:
|
||||
"""Calling cancel stops a running execute."""
|
||||
async def slow_task(**kwargs: Any) -> str:
|
||||
await asyncio.sleep(2.0)
|
||||
return "should not complete"
|
||||
|
||||
mock_agent.aexecute_task = slow_task
|
||||
|
||||
with (
|
||||
patch("crewai.a2a.utils.task.Task", return_value=mock_task),
|
||||
patch("crewai.a2a.utils.task.crewai_event_bus"),
|
||||
):
|
||||
execute_task = asyncio.create_task(
|
||||
execute(mock_agent, mock_context, mock_event_queue)
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
await cancel(mock_context, mock_event_queue)
|
||||
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await execute_task
|
||||
@@ -14,6 +14,16 @@ except ImportError:
|
||||
A2A_SDK_INSTALLED = False
|
||||
|
||||
|
||||
def _create_mock_agent_card(name: str = "Test", url: str = "http://test-endpoint.com/"):
|
||||
"""Create a mock agent card with proper model_dump behavior."""
|
||||
mock_card = MagicMock()
|
||||
mock_card.name = name
|
||||
mock_card.url = url
|
||||
mock_card.model_dump.return_value = {"name": name, "url": url}
|
||||
mock_card.model_dump_json.return_value = f'{{"name": "{name}", "url": "{url}"}}'
|
||||
return mock_card
|
||||
|
||||
|
||||
@pytest.mark.skipif(not A2A_SDK_INSTALLED, reason="Requires a2a-sdk to be installed")
|
||||
def test_trust_remote_completion_status_true_returns_directly():
|
||||
"""When trust_remote_completion_status=True and A2A returns completed, return result directly."""
|
||||
@@ -44,8 +54,7 @@ def test_trust_remote_completion_status_true_returns_directly():
|
||||
patch("crewai.a2a.wrapper.execute_a2a_delegation") as mock_execute,
|
||||
patch("crewai.a2a.wrapper._fetch_agent_cards_concurrently") as mock_fetch,
|
||||
):
|
||||
mock_card = MagicMock()
|
||||
mock_card.name = "Test"
|
||||
mock_card = _create_mock_agent_card()
|
||||
mock_fetch.return_value = ({"http://test-endpoint.com/": mock_card}, {})
|
||||
|
||||
# A2A returns completed
|
||||
@@ -110,8 +119,7 @@ def test_trust_remote_completion_status_false_continues_conversation():
|
||||
patch("crewai.a2a.wrapper.execute_a2a_delegation") as mock_execute,
|
||||
patch("crewai.a2a.wrapper._fetch_agent_cards_concurrently") as mock_fetch,
|
||||
):
|
||||
mock_card = MagicMock()
|
||||
mock_card.name = "Test"
|
||||
mock_card = _create_mock_agent_card()
|
||||
mock_fetch.return_value = ({"http://test-endpoint.com/": mock_card}, {})
|
||||
|
||||
# A2A returns completed
|
||||
|
||||