Compare commits

..

7 Commits

Author SHA1 Message Date
Greyson LaLonde
661646b3f5 chore: update test assumption 2026-01-16 11:09:44 -05:00
Greyson LaLonde
3f7e76a9d3 fix: ensure loop close 2026-01-16 03:41:23 -05:00
Greyson LaLonde
4573fc95c4 feat: register traces 2026-01-16 03:40:50 -05:00
Greyson LaLonde
a675327ffa feat: add pass additional data to a2a events 2026-01-16 03:37:09 -05:00
Greyson LaLonde
b7434af0ce feat: add additional a2a events; emissions 2026-01-15 19:06:58 -05:00
Heitor Carvalho
e44d778e0e feat: keycloak sso provider support (#4241)
Some checks failed
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Notify Downstream / notify-downstream (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
2026-01-15 15:38:40 -03:00
nicoferdi96
5645cbb22e CrewAI AMP Deployment Guidelines (#4205)
* doc changes for better deplyment guidelines and checklist

* chore: remove .claude folder from version control

The .claude folder contains local Claude Code skills and configuration
that should not be tracked in the repository. Already in .gitignore.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Better project structure for flows

* docs.json updated structure

* Ko and Pt traslations for deploying guidelines to AMP

* fix broken links

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Greyson LaLonde <greyson.r.lalonde@gmail.com>
2026-01-15 16:32:20 +01:00
34 changed files with 3862 additions and 842 deletions

1
.gitignore vendored
View File

@@ -26,3 +26,4 @@ plan.md
conceptual_plan.md
build_image
chromadb-*.lock
.claude

View File

@@ -429,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",
@@ -864,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",
@@ -1326,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",
@@ -1514,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*"

View File

@@ -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>

View 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>

View File

@@ -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

View File

@@ -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">

View File

@@ -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">

View File

@@ -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>
![Connect GitHub Button](/images/enterprise/connect-github.png)
</Frame>
</Step>
<Step title="저장소 선택하기">
GitHub 계정을 연결한 후 배포할 저장소를 선택할 수 있습니다:
<Frame>
![Select Repository](/images/enterprise/select-repo.png)
</Frame>
</Step>
<Step title="환경 변수 설정하기">
배포 전에, LLM 제공업체 또는 기타 서비스에 연결할 환경 변수를 설정해야 합니다:
1. 변수를 개별적으로 또는 일괄적으로 추가할 수 있습니다.
2. 환경 변수는 `KEY=VALUE` 형식(한 줄에 하나씩)으로 입력합니다.
<Frame>
![Set Environment Variables](/images/enterprise/set-env-variables.png)
</Frame>
</Step>
<Step title="Crew 배포하기">
1. "Deploy" 버튼을 클릭하여 배포 프로세스를 시작합니다.
2. 진행 바를 통해 진행 상황을 모니터링할 수 있습니다.
3. 첫 번째 배포에는 일반적으로 약 10-15분 정도 소요되며, 이후 배포는 더 빠릅니다.
<Frame>
![Deploy Progress](/images/enterprise/deploy-progress.png)
</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>

View 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>
![Connect GitHub Button](/images/enterprise/connect-github.png)
</Frame>
</Step>
<Step title="저장소 선택하기">
GitHub 계정을 연결한 후 배포할 저장소를 선택할 수 있습니다:
<Frame>
![Select Repository](/images/enterprise/select-repo.png)
</Frame>
</Step>
<Step title="환경 변수 설정하기">
배포 전에, LLM 제공업체 또는 기타 서비스에 연결할 환경 변수를 설정해야 합니다:
1. 변수를 개별적으로 또는 일괄적으로 추가할 수 있습니다.
2. 환경 변수는 `KEY=VALUE` 형식(한 줄에 하나씩)으로 입력합니다.
<Frame>
![Set Environment Variables](/images/enterprise/set-env-variables.png)
</Frame>
</Step>
<Step title="Crew 배포하기">
1. "Deploy" 버튼을 클릭하여 배포 프로세스를 시작합니다.
2. 진행 바를 통해 진행 상황을 모니터링할 수 있습니다.
3. 첫 번째 배포에는 일반적으로 약 10-15분 정도 소요되며, 이후 배포는 더 빠릅니다.
<Frame>
![Deploy Progress](/images/enterprise/deploy-progress.png)
</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>

View 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>

View File

@@ -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)를 확인하거나 아래 버튼을 클릭해 시작하세요.

View File

@@ -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

View File

@@ -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">

View File

@@ -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">

View File

@@ -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>
![Botão Connect GitHub](/images/enterprise/connect-github.png)
</Frame>
</Step>
<Step title="Selecionar o Repositório">
Após conectar sua conta GitHub, você poderá selecionar qual repositório deseja implantar:
<Frame>
![Selecionar Repositório](/images/enterprise/select-repo.png)
</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>
![Definir Variáveis de Ambiente](/images/enterprise/set-env-variables.png)
</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>
![Progresso da Implantação](/images/enterprise/deploy-progress.png)
</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>

View 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>
![Botão Connect GitHub](/images/enterprise/connect-github.png)
</Frame>
</Step>
<Step title="Selecionar o Repositório">
Após conectar sua conta GitHub, você poderá selecionar qual repositório deseja implantar:
<Frame>
![Selecionar Repositório](/images/enterprise/select-repo.png)
</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>
![Definir Variáveis de Ambiente](/images/enterprise/set-env-variables.png)
</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>
![Progresso da Implantação](/images/enterprise/deploy-progress.png)
</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>

View 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>

View File

@@ -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.

View File

@@ -3,9 +3,10 @@
from __future__ import annotations
from collections.abc import AsyncIterator
from typing import TYPE_CHECKING, TypedDict
from typing import TYPE_CHECKING, Any, TypedDict
import uuid
from a2a.client.errors import A2AClientHTTPError
from a2a.types import (
AgentCard,
Message,
@@ -20,7 +21,10 @@ from a2a.types import (
from typing_extensions import NotRequired
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import A2AResponseReceivedEvent
from crewai.events.types.a2a_events import (
A2AConnectionErrorEvent,
A2AResponseReceivedEvent,
)
if TYPE_CHECKING:
@@ -55,7 +59,8 @@ class TaskStateResult(TypedDict):
history: list[Message]
result: NotRequired[str]
error: NotRequired[str]
agent_card: NotRequired[AgentCard]
agent_card: NotRequired[dict[str, Any]]
a2a_agent_name: NotRequired[str | None]
def extract_task_result_parts(a2a_task: A2ATask) -> list[str]:
@@ -131,50 +136,69 @@ def process_task_state(
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
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)
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
Result dictionary if terminal/actionable state, None otherwise.
"""
should_extract = result_parts is None
if result_parts is None:
result_parts = []
if a2a_task.status.state == TaskState.completed:
if should_extract:
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,
agent_card=agent_card.model_dump(exclude_none=True),
result=response_text,
history=new_messages,
)
@@ -194,14 +218,24 @@ def process_task_state(
)
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,
),
)
@@ -209,7 +243,7 @@ def process_task_state(
status=TaskState.input_required,
error=response_text,
history=new_messages,
agent_card=agent_card,
agent_card=agent_card.model_dump(exclude_none=True),
)
if a2a_task.status.state in {TaskState.failed, TaskState.rejected}:
@@ -248,6 +282,11 @@ async def send_message_and_get_task_id(
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.
@@ -262,6 +301,11 @@ async def send_message_and_get_task_id(
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.
@@ -280,9 +324,16 @@ async def send_message_and_get_task_id(
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,
),
)
@@ -290,7 +341,7 @@ async def send_message_and_get_task_id(
status=TaskState.completed,
result=response_text,
history=new_messages,
agent_card=agent_card,
agent_card=agent_card.model_dump(exclude_none=True),
)
if isinstance(event, tuple):
@@ -304,6 +355,10 @@ async def send_message_and_get_task_id(
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
@@ -316,6 +371,99 @@ async def send_message_and_get_task_id(
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:

View File

@@ -22,6 +22,13 @@ class BaseHandlerKwargs(TypedDict, total=False):
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):
@@ -29,8 +36,6 @@ class PollingHandlerKwargs(BaseHandlerKwargs, total=False):
polling_interval: float
polling_timeout: float
endpoint: str
agent_branch: Any
history_length: int
max_polls: int | None
@@ -38,9 +43,6 @@ class PollingHandlerKwargs(BaseHandlerKwargs, total=False):
class StreamingHandlerKwargs(BaseHandlerKwargs, total=False):
"""Kwargs for streaming handler."""
context_id: str | None
task_id: str | None
class PushNotificationHandlerKwargs(BaseHandlerKwargs, total=False):
"""Kwargs for push notification handler."""
@@ -49,7 +51,6 @@ class PushNotificationHandlerKwargs(BaseHandlerKwargs, total=False):
result_store: PushNotificationResultStore
polling_timeout: float
polling_interval: float
agent_branch: Any
class PushNotificationResultStore(Protocol):

View File

@@ -31,6 +31,7 @@ from crewai.a2a.task_helpers import (
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,
@@ -49,23 +50,33 @@ async def _poll_task_until_complete(
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)
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
Final task object in terminal state.
Raises:
A2APollingTimeoutError: If polling exceeds timeout or max_polls
A2APollingTimeoutError: If polling exceeds timeout or max_polls.
"""
start_time = time.monotonic()
poll_count = 0
@@ -77,13 +88,19 @@ async def _poll_task_until_complete(
)
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,
),
)
@@ -137,6 +154,9 @@ class PollingHandler:
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(
@@ -146,6 +166,11 @@ class PollingHandler:
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):
@@ -157,8 +182,12 @@ class PollingHandler:
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,
),
)
@@ -170,6 +199,11 @@ class PollingHandler:
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(
@@ -179,6 +213,10 @@ class PollingHandler:
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
@@ -206,9 +244,15 @@ class PollingHandler:
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(
@@ -229,14 +273,83 @@ class PollingHandler:
)
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(

View File

@@ -29,6 +29,7 @@ from crewai.a2a.updates.base import (
)
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import (
A2AConnectionErrorEvent,
A2APushNotificationRegisteredEvent,
A2APushNotificationTimeoutEvent,
A2AResponseReceivedEvent,
@@ -48,6 +49,11 @@ async def _wait_for_push_result(
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.
@@ -57,6 +63,11 @@ async def _wait_for_push_result(
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.
@@ -72,7 +83,12 @@ async def _wait_for_push_result(
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,
),
)
@@ -115,18 +131,56 @@ class PushNotificationHandler:
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="PushNotificationConfig is required for push notification handler",
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="PushNotificationResultStore is required for push notification handler",
error=error_msg,
history=new_messages,
)
@@ -138,6 +192,11 @@ class PushNotificationHandler:
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):
@@ -149,7 +208,12 @@ class PushNotificationHandler:
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,
),
)
@@ -165,6 +229,11 @@ class PushNotificationHandler:
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:
@@ -181,6 +250,10 @@ class PushNotificationHandler:
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
@@ -203,14 +276,83 @@ class PushNotificationHandler:
)
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(

View File

@@ -26,7 +26,13 @@ from crewai.a2a.task_helpers import (
)
from crewai.a2a.updates.base import StreamingHandlerKwargs
from crewai.events.event_bus import crewai_event_bus
from crewai.events.types.a2a_events import A2AResponseReceivedEvent
from crewai.events.types.a2a_events import (
A2AArtifactReceivedEvent,
A2AConnectionErrorEvent,
A2AResponseReceivedEvent,
A2AStreamingChunkEvent,
A2AStreamingStartedEvent,
)
class StreamingHandler:
@@ -57,19 +63,57 @@ class StreamingHandler:
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
@@ -81,10 +125,51 @@ class StreamingHandler:
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
@@ -101,6 +186,11 @@ class StreamingHandler:
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
@@ -118,13 +208,82 @@ class StreamingHandler:
new_messages.append(error_message)
crewai_event_bus.emit(
None,
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(
@@ -136,7 +295,23 @@ class StreamingHandler:
finally:
aclose = getattr(event_stream, "aclose", None)
if aclose:
await 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
@@ -145,5 +320,5 @@ class StreamingHandler:
status=TaskState.completed,
result=" ".join(result_parts) if result_parts else "",
history=new_messages,
agent_card=agent_card,
agent_card=agent_card.model_dump(exclude_none=True),
)

View File

@@ -23,6 +23,12 @@ from crewai.a2a.auth.utils import (
)
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:
@@ -183,6 +189,8 @@ async def _afetch_agent_card_impl(
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"
@@ -217,7 +225,24 @@ async def _afetch_agent_card_impl(
)
response.raise_for_status()
return AgentCard.model_validate(response.json())
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:
if e.response.status_code == 401:
@@ -228,7 +253,66 @@ async def _afetch_agent_card_impl(
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,
),
)
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",
),
)
raise
except httpx.TimeoutException as e:
crewai_event_bus.emit(
None,
A2AConnectionErrorEvent(
endpoint=endpoint,
error=str(e),
error_type="timeout",
operation="fetch_agent_card",
),
)
raise
except httpx.ConnectError as e:
crewai_event_bus.emit(
None,
A2AConnectionErrorEvent(
endpoint=endpoint,
error=str(e),
error_type="connection_error",
operation="fetch_agent_card",
),
)
raise
except httpx.RequestError as e:
crewai_event_bus.emit(
None,
A2AConnectionErrorEvent(
endpoint=endpoint,
error=str(e),
error_type="request_error",
operation="fetch_agent_card",
),
)
raise

View File

@@ -88,6 +88,9 @@ def execute_a2a_delegation(
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.
@@ -129,6 +132,9 @@ def execute_a2a_delegation(
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.
@@ -156,10 +162,16 @@ def execute_a2a_delegation(
transport_protocol=transport_protocol,
turn_number=turn_number,
updates=updates,
from_task=from_task,
from_agent=from_agent,
skill_id=skill_id,
)
)
finally:
loop.close()
try:
loop.run_until_complete(loop.shutdown_asyncgens())
finally:
loop.close()
async def aexecute_a2a_delegation(
@@ -181,6 +193,9 @@ async def aexecute_a2a_delegation(
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.
@@ -222,6 +237,9 @@ async def aexecute_a2a_delegation(
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.
@@ -233,17 +251,6 @@ async def aexecute_a2a_delegation(
if turn_number is None:
turn_number = len([m for m in conversation_history 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,
),
)
result = await _aexecute_a2a_delegation_impl(
endpoint=endpoint,
auth=auth,
@@ -264,15 +271,28 @@ async def aexecute_a2a_delegation(
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,
),
)
@@ -299,6 +319,9 @@ async def _aexecute_a2a_delegation_impl(
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:
@@ -331,6 +354,28 @@ async def _aexecute_a2a_delegation_impl(
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(
@@ -338,7 +383,17 @@ async def _aexecute_a2a_delegation_impl(
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,
),
)
@@ -364,6 +419,10 @@ async def _aexecute_a2a_delegation_impl(
}
)
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()),
@@ -371,7 +430,7 @@ async def _aexecute_a2a_delegation_impl(
context_id=context_id,
task_id=task_id,
reference_task_ids=reference_task_ids,
metadata=metadata,
metadata=message_metadata if message_metadata else None,
extensions=extensions,
)
@@ -381,8 +440,17 @@ async def _aexecute_a2a_delegation_impl(
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,
),
)
@@ -397,6 +465,9 @@ async def _aexecute_a2a_delegation_impl(
"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):
@@ -434,13 +505,16 @@ async def _aexecute_a2a_delegation_impl(
use_polling=use_polling,
push_notification_config=push_config_for_client,
) as client:
return await handler.execute(
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

View File

@@ -3,11 +3,14 @@
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
@@ -45,7 +48,14 @@ T = TypeVar("T")
def _parse_redis_url(url: str) -> dict[str, Any]:
from urllib.parse import urlparse
"""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] = {
@@ -127,7 +137,7 @@ def cancellable(
async for message in pubsub.listen():
if message["type"] == "message":
return True
except Exception as e:
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
@@ -183,7 +193,12 @@ async def execute(
msg = "task_id and context_id are required"
crewai_event_bus.emit(
agent,
A2AServerTaskFailedEvent(a2a_task_id="", a2a_context_id="", error=msg),
A2AServerTaskFailedEvent(
task_id="",
context_id="",
error=msg,
from_agent=agent,
),
)
raise ServerError(InvalidParamsError(message=msg)) from None
@@ -195,7 +210,12 @@ async def execute(
crewai_event_bus.emit(
agent,
A2AServerTaskStartedEvent(a2a_task_id=task_id, a2a_context_id=context_id),
A2AServerTaskStartedEvent(
task_id=task_id,
context_id=context_id,
from_task=task,
from_agent=agent,
),
)
try:
@@ -215,20 +235,33 @@ async def execute(
crewai_event_bus.emit(
agent,
A2AServerTaskCompletedEvent(
a2a_task_id=task_id, a2a_context_id=context_id, result=str(result)
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(a2a_task_id=task_id, a2a_context_id=context_id),
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(
a2a_task_id=task_id, a2a_context_id=context_id, error=str(e)
task_id=task_id,
context_id=context_id,
error=str(e),
from_task=task,
from_agent=agent,
),
)
raise ServerError(
@@ -282,3 +315,85 @@ async def cancel(
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

View File

@@ -6,9 +6,10 @@ Wraps agent classes with A2A delegation capabilities.
from __future__ import annotations
import asyncio
from collections.abc import Callable, Coroutine
from collections.abc import Callable, Coroutine, Mapping
from concurrent.futures import ThreadPoolExecutor, as_completed
from functools import wraps
import json
from types import MethodType
from typing import TYPE_CHECKING, Any
@@ -189,7 +190,7 @@ def _execute_task_with_a2a(
a2a_agents: list[A2AConfig | A2AClientConfig],
original_fn: Callable[..., str],
task: Task,
agent_response_model: type[BaseModel],
agent_response_model: type[BaseModel] | None,
context: str | None,
tools: list[BaseTool] | None,
extension_registry: ExtensionRegistry,
@@ -277,7 +278,7 @@ def _execute_task_with_a2a(
def _augment_prompt_with_a2a(
a2a_agents: list[A2AConfig | A2AClientConfig],
task_description: str,
agent_cards: dict[str, AgentCard],
agent_cards: Mapping[str, AgentCard | dict[str, Any]],
conversation_history: list[Message] | None = None,
turn_num: int = 0,
max_turns: int | None = None,
@@ -309,7 +310,15 @@ def _augment_prompt_with_a2a(
for config in a2a_agents:
if config.endpoint in agent_cards:
card = agent_cards[config.endpoint]
agents_text += f"\n{card.model_dump_json(indent=2, exclude_none=True, include={'description', 'url', 'skills'})}\n"
if isinstance(card, dict):
filtered = {
k: v
for k, v in card.items()
if k in {"description", "url", "skills"} and v is not None
}
agents_text += f"\n{json.dumps(filtered, indent=2)}\n"
else:
agents_text += f"\n{card.model_dump_json(indent=2, exclude_none=True, include={'description', 'url', 'skills'})}\n"
failed_agents = failed_agents or {}
if failed_agents:
@@ -377,7 +386,7 @@ IMPORTANT: You have the ability to delegate this task to remote A2A agents.
def _parse_agent_response(
raw_result: str | dict[str, Any], agent_response_model: type[BaseModel]
raw_result: str | dict[str, Any], agent_response_model: type[BaseModel] | None
) -> BaseModel | str | dict[str, Any]:
"""Parse LLM output as AgentResponse or return raw agent response."""
if agent_response_model:
@@ -394,6 +403,11 @@ def _parse_agent_response(
def _handle_max_turns_exceeded(
conversation_history: list[Message],
max_turns: int,
from_task: Any | None = None,
from_agent: Any | None = None,
endpoint: str | None = None,
a2a_agent_name: str | None = None,
agent_card: dict[str, Any] | None = None,
) -> str:
"""Handle the case when max turns is exceeded.
@@ -421,6 +435,11 @@ def _handle_max_turns_exceeded(
final_result=final_message,
error=None,
total_turns=max_turns,
from_task=from_task,
from_agent=from_agent,
endpoint=endpoint,
a2a_agent_name=a2a_agent_name,
agent_card=agent_card,
),
)
return final_message
@@ -432,6 +451,11 @@ def _handle_max_turns_exceeded(
final_result=None,
error=f"Conversation exceeded maximum turns ({max_turns})",
total_turns=max_turns,
from_task=from_task,
from_agent=from_agent,
endpoint=endpoint,
a2a_agent_name=a2a_agent_name,
agent_card=agent_card,
),
)
raise Exception(f"A2A conversation exceeded maximum turns ({max_turns})")
@@ -442,7 +466,12 @@ def _process_response_result(
disable_structured_output: bool,
turn_num: int,
agent_role: str,
agent_response_model: type[BaseModel],
agent_response_model: type[BaseModel] | None,
from_task: Any | None = None,
from_agent: Any | None = None,
endpoint: str | None = None,
a2a_agent_name: str | None = None,
agent_card: dict[str, Any] | None = None,
) -> tuple[str | None, str | None]:
"""Process LLM response and determine next action.
@@ -461,6 +490,10 @@ def _process_response_result(
turn_number=final_turn_number,
is_multiturn=True,
agent_role=agent_role,
from_task=from_task,
from_agent=from_agent,
endpoint=endpoint,
a2a_agent_name=a2a_agent_name,
),
)
crewai_event_bus.emit(
@@ -470,6 +503,11 @@ def _process_response_result(
final_result=result_text,
error=None,
total_turns=final_turn_number,
from_task=from_task,
from_agent=from_agent,
endpoint=endpoint,
a2a_agent_name=a2a_agent_name,
agent_card=agent_card,
),
)
return result_text, None
@@ -490,6 +528,10 @@ def _process_response_result(
turn_number=final_turn_number,
is_multiturn=True,
agent_role=agent_role,
from_task=from_task,
from_agent=from_agent,
endpoint=endpoint,
a2a_agent_name=a2a_agent_name,
),
)
crewai_event_bus.emit(
@@ -499,6 +541,11 @@ def _process_response_result(
final_result=str(llm_response.message),
error=None,
total_turns=final_turn_number,
from_task=from_task,
from_agent=from_agent,
endpoint=endpoint,
a2a_agent_name=a2a_agent_name,
agent_card=agent_card,
),
)
return str(llm_response.message), None
@@ -510,13 +557,15 @@ def _process_response_result(
def _prepare_agent_cards_dict(
a2a_result: TaskStateResult,
agent_id: str,
agent_cards: dict[str, AgentCard] | None,
) -> dict[str, AgentCard]:
agent_cards: Mapping[str, AgentCard | dict[str, Any]] | None,
) -> dict[str, AgentCard | dict[str, Any]]:
"""Prepare agent cards dictionary from result and existing cards.
Shared logic for both sync and async response handlers.
"""
agent_cards_dict = agent_cards or {}
agent_cards_dict: dict[str, AgentCard | dict[str, Any]] = (
dict(agent_cards) if agent_cards else {}
)
if "agent_card" in a2a_result and agent_id not in agent_cards_dict:
agent_cards_dict[agent_id] = a2a_result["agent_card"]
return agent_cards_dict
@@ -529,7 +578,7 @@ def _prepare_delegation_context(
original_task_description: str | None,
) -> tuple[
list[A2AConfig | A2AClientConfig],
type[BaseModel],
type[BaseModel] | None,
str,
str,
A2AConfig | A2AClientConfig,
@@ -598,6 +647,11 @@ def _handle_task_completion(
reference_task_ids: list[str],
agent_config: A2AConfig | A2AClientConfig,
turn_num: int,
from_task: Any | None = None,
from_agent: Any | None = None,
endpoint: str | None = None,
a2a_agent_name: str | None = None,
agent_card: dict[str, Any] | None = None,
) -> tuple[str | None, str | None, list[str]]:
"""Handle task completion state including reference task updates.
@@ -624,6 +678,11 @@ def _handle_task_completion(
final_result=result_text,
error=None,
total_turns=final_turn_number,
from_task=from_task,
from_agent=from_agent,
endpoint=endpoint,
a2a_agent_name=a2a_agent_name,
agent_card=agent_card,
),
)
return str(result_text), task_id_config, reference_task_ids
@@ -645,8 +704,11 @@ def _handle_agent_response_and_continue(
original_fn: Callable[..., str],
context: str | None,
tools: list[BaseTool] | None,
agent_response_model: type[BaseModel],
agent_response_model: type[BaseModel] | None,
remote_task_completed: bool = False,
endpoint: str | None = None,
a2a_agent_name: str | None = None,
agent_card: dict[str, Any] | None = None,
) -> tuple[str | None, str | None]:
"""Handle A2A result and get CrewAI agent's response.
@@ -698,6 +760,11 @@ def _handle_agent_response_and_continue(
turn_num=turn_num,
agent_role=self.role,
agent_response_model=agent_response_model,
from_task=task,
from_agent=self,
endpoint=endpoint,
a2a_agent_name=a2a_agent_name,
agent_card=agent_card,
)
@@ -750,6 +817,12 @@ def _delegate_to_a2a(
conversation_history: list[Message] = []
current_agent_card = agent_cards.get(agent_id) if agent_cards else None
current_agent_card_dict = (
current_agent_card.model_dump() if current_agent_card else None
)
current_a2a_agent_name = current_agent_card.name if current_agent_card else None
try:
for turn_num in range(max_turns):
console_formatter = getattr(crewai_event_bus, "_console", None)
@@ -777,6 +850,8 @@ def _delegate_to_a2a(
turn_number=turn_num + 1,
updates=agent_config.updates,
transport_protocol=agent_config.transport_protocol,
from_task=task,
from_agent=self,
)
conversation_history = a2a_result.get("history", [])
@@ -797,6 +872,11 @@ def _delegate_to_a2a(
reference_task_ids,
agent_config,
turn_num,
from_task=task,
from_agent=self,
endpoint=agent_config.endpoint,
a2a_agent_name=current_a2a_agent_name,
agent_card=current_agent_card_dict,
)
)
if trusted_result is not None:
@@ -818,6 +898,9 @@ def _delegate_to_a2a(
tools=tools,
agent_response_model=agent_response_model,
remote_task_completed=(a2a_result["status"] == TaskState.completed),
endpoint=agent_config.endpoint,
a2a_agent_name=current_a2a_agent_name,
agent_card=current_agent_card_dict,
)
if final_result is not None:
@@ -846,6 +929,9 @@ def _delegate_to_a2a(
tools=tools,
agent_response_model=agent_response_model,
remote_task_completed=False,
endpoint=agent_config.endpoint,
a2a_agent_name=current_a2a_agent_name,
agent_card=current_agent_card_dict,
)
if final_result is not None:
@@ -862,11 +948,24 @@ def _delegate_to_a2a(
final_result=None,
error=error_msg,
total_turns=turn_num + 1,
from_task=task,
from_agent=self,
endpoint=agent_config.endpoint,
a2a_agent_name=current_a2a_agent_name,
agent_card=current_agent_card_dict,
),
)
return f"A2A delegation failed: {error_msg}"
return _handle_max_turns_exceeded(conversation_history, max_turns)
return _handle_max_turns_exceeded(
conversation_history,
max_turns,
from_task=task,
from_agent=self,
endpoint=agent_config.endpoint,
a2a_agent_name=current_a2a_agent_name,
agent_card=current_agent_card_dict,
)
finally:
task.description = original_task_description
@@ -916,7 +1015,7 @@ async def _aexecute_task_with_a2a(
a2a_agents: list[A2AConfig | A2AClientConfig],
original_fn: Callable[..., Coroutine[Any, Any, str]],
task: Task,
agent_response_model: type[BaseModel],
agent_response_model: type[BaseModel] | None,
context: str | None,
tools: list[BaseTool] | None,
extension_registry: ExtensionRegistry,
@@ -1001,8 +1100,11 @@ async def _ahandle_agent_response_and_continue(
original_fn: Callable[..., Coroutine[Any, Any, str]],
context: str | None,
tools: list[BaseTool] | None,
agent_response_model: type[BaseModel],
agent_response_model: type[BaseModel] | None,
remote_task_completed: bool = False,
endpoint: str | None = None,
a2a_agent_name: str | None = None,
agent_card: dict[str, Any] | None = None,
) -> tuple[str | None, str | None]:
"""Async version of _handle_agent_response_and_continue."""
agent_cards_dict = _prepare_agent_cards_dict(a2a_result, agent_id, agent_cards)
@@ -1032,6 +1134,11 @@ async def _ahandle_agent_response_and_continue(
turn_num=turn_num,
agent_role=self.role,
agent_response_model=agent_response_model,
from_task=task,
from_agent=self,
endpoint=endpoint,
a2a_agent_name=a2a_agent_name,
agent_card=agent_card,
)
@@ -1066,6 +1173,12 @@ async def _adelegate_to_a2a(
conversation_history: list[Message] = []
current_agent_card = agent_cards.get(agent_id) if agent_cards else None
current_agent_card_dict = (
current_agent_card.model_dump() if current_agent_card else None
)
current_a2a_agent_name = current_agent_card.name if current_agent_card else None
try:
for turn_num in range(max_turns):
console_formatter = getattr(crewai_event_bus, "_console", None)
@@ -1093,6 +1206,8 @@ async def _adelegate_to_a2a(
turn_number=turn_num + 1,
transport_protocol=agent_config.transport_protocol,
updates=agent_config.updates,
from_task=task,
from_agent=self,
)
conversation_history = a2a_result.get("history", [])
@@ -1113,6 +1228,11 @@ async def _adelegate_to_a2a(
reference_task_ids,
agent_config,
turn_num,
from_task=task,
from_agent=self,
endpoint=agent_config.endpoint,
a2a_agent_name=current_a2a_agent_name,
agent_card=current_agent_card_dict,
)
)
if trusted_result is not None:
@@ -1134,6 +1254,9 @@ async def _adelegate_to_a2a(
tools=tools,
agent_response_model=agent_response_model,
remote_task_completed=(a2a_result["status"] == TaskState.completed),
endpoint=agent_config.endpoint,
a2a_agent_name=current_a2a_agent_name,
agent_card=current_agent_card_dict,
)
if final_result is not None:
@@ -1161,6 +1284,9 @@ async def _adelegate_to_a2a(
context=context,
tools=tools,
agent_response_model=agent_response_model,
endpoint=agent_config.endpoint,
a2a_agent_name=current_a2a_agent_name,
agent_card=current_agent_card_dict,
)
if final_result is not None:
@@ -1177,11 +1303,24 @@ async def _adelegate_to_a2a(
final_result=None,
error=error_msg,
total_turns=turn_num + 1,
from_task=task,
from_agent=self,
endpoint=agent_config.endpoint,
a2a_agent_name=current_a2a_agent_name,
agent_card=current_agent_card_dict,
),
)
return f"A2A delegation failed: {error_msg}"
return _handle_max_turns_exceeded(conversation_history, max_turns)
return _handle_max_turns_exceeded(
conversation_history,
max_turns,
from_task=task,
from_agent=self,
endpoint=agent_config.endpoint,
a2a_agent_name=current_a2a_agent_name,
agent_card=current_agent_card_dict,
)
finally:
task.description = original_task_description

View File

@@ -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}"

View File

@@ -1,19 +1,28 @@
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,
@@ -93,7 +102,11 @@ from crewai.events.types.tool_usage_events import (
EventTypes = (
A2AConversationCompletedEvent
A2AAgentCardFetchedEvent
| A2AArtifactReceivedEvent
| A2AAuthenticationFailedEvent
| A2AConnectionErrorEvent
| A2AConversationCompletedEvent
| A2AConversationStartedEvent
| A2ADelegationCompletedEvent
| A2ADelegationStartedEvent
@@ -102,12 +115,17 @@ EventTypes = (
| A2APollingStatusEvent
| A2APushNotificationReceivedEvent
| A2APushNotificationRegisteredEvent
| A2APushNotificationSentEvent
| A2APushNotificationTimeoutEvent
| A2AResponseReceivedEvent
| A2AServerTaskCanceledEvent
| A2AServerTaskCompletedEvent
| A2AServerTaskFailedEvent
| A2AServerTaskStartedEvent
| A2AStreamingChunkEvent
| A2AStreamingStartedEvent
| A2AParallelDelegationStartedEvent
| A2AParallelDelegationCompletedEvent
| CrewKickoffStartedEvent
| CrewKickoffCompletedEvent
| CrewKickoffFailedEvent

View File

@@ -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),

View File

@@ -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: Any) -> None:
"""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,119 +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
polling_interval: Seconds between poll attempts
endpoint: A2A agent endpoint URL
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
state: Current task state from remote agent
elapsed_seconds: Time since polling started
poll_count: Number of polls completed
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
callback_url: URL where agent will send push notifications
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
state: Current task state from the notification
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
timeout_seconds: Timeout duration in seconds
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."""
"""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"
a2a_task_id: str
a2a_context_id: str
task_id: str
context_id: str
metadata: dict[str, Any] | None = None
class A2AServerTaskCompletedEvent(A2AEventBase):
"""Event emitted when an A2A server task execution completes."""
"""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"
a2a_task_id: str
a2a_context_id: str
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."""
"""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"
a2a_task_id: str
a2a_context_id: str
task_id: str
context_id: str
metadata: dict[str, Any] | None = None
class A2AServerTaskFailedEvent(A2AEventBase):
"""Event emitted when an A2A server task execution fails."""
"""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"
a2a_task_id: str
a2a_context_id: str
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

View File

@@ -26,9 +26,13 @@ def mock_agent() -> MagicMock:
@pytest.fixture
def mock_task() -> MagicMock:
def mock_task(mock_context: MagicMock) -> MagicMock:
"""Create a mock Task."""
return MagicMock()
task = MagicMock()
task.id = mock_context.task_id
task.name = "Mock Task"
task.description = "Mock task description"
return task
@pytest.fixture
@@ -179,8 +183,8 @@ class TestExecute:
event = first_call[0][1]
assert event.type == "a2a_server_task_started"
assert event.a2a_task_id == mock_context.task_id
assert event.a2a_context_id == mock_context.context_id
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(
@@ -201,7 +205,7 @@ class TestExecute:
event = second_call[0][1]
assert event.type == "a2a_server_task_completed"
assert event.a2a_task_id == mock_context.task_id
assert event.task_id == mock_context.task_id
assert event.result == "Task completed successfully"
@pytest.mark.asyncio
@@ -250,7 +254,7 @@ class TestExecute:
event = canceled_call[0][1]
assert event.type == "a2a_server_task_canceled"
assert event.a2a_task_id == mock_context.task_id
assert event.task_id == mock_context.task_id
class TestCancel:

View File

@@ -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

View File

@@ -0,0 +1,138 @@
import pytest
from crewai.cli.authentication.main import Oauth2Settings
from crewai.cli.authentication.providers.keycloak import KeycloakProvider
class TestKeycloakProvider:
@pytest.fixture(autouse=True)
def setup_method(self):
self.valid_settings = Oauth2Settings(
provider="keycloak",
domain="keycloak.example.com",
client_id="test-client-id",
audience="test-audience",
extra={
"realm": "test-realm"
}
)
self.provider = KeycloakProvider(self.valid_settings)
def test_initialization_with_valid_settings(self):
provider = KeycloakProvider(self.valid_settings)
assert provider.settings == self.valid_settings
assert provider.settings.provider == "keycloak"
assert provider.settings.domain == "keycloak.example.com"
assert provider.settings.client_id == "test-client-id"
assert provider.settings.audience == "test-audience"
assert provider.settings.extra.get("realm") == "test-realm"
def test_get_authorize_url(self):
expected_url = "https://keycloak.example.com/realms/test-realm/protocol/openid-connect/auth/device"
assert self.provider.get_authorize_url() == expected_url
def test_get_authorize_url_with_different_domain(self):
settings = Oauth2Settings(
provider="keycloak",
domain="auth.company.com",
client_id="test-client",
audience="test-audience",
extra={
"realm": "my-realm"
}
)
provider = KeycloakProvider(settings)
expected_url = "https://auth.company.com/realms/my-realm/protocol/openid-connect/auth/device"
assert provider.get_authorize_url() == expected_url
def test_get_token_url(self):
expected_url = "https://keycloak.example.com/realms/test-realm/protocol/openid-connect/token"
assert self.provider.get_token_url() == expected_url
def test_get_token_url_with_different_domain(self):
settings = Oauth2Settings(
provider="keycloak",
domain="sso.enterprise.com",
client_id="test-client",
audience="test-audience",
extra={
"realm": "enterprise-realm"
}
)
provider = KeycloakProvider(settings)
expected_url = "https://sso.enterprise.com/realms/enterprise-realm/protocol/openid-connect/token"
assert provider.get_token_url() == expected_url
def test_get_jwks_url(self):
expected_url = "https://keycloak.example.com/realms/test-realm/protocol/openid-connect/certs"
assert self.provider.get_jwks_url() == expected_url
def test_get_jwks_url_with_different_domain(self):
settings = Oauth2Settings(
provider="keycloak",
domain="identity.org",
client_id="test-client",
audience="test-audience",
extra={
"realm": "org-realm"
}
)
provider = KeycloakProvider(settings)
expected_url = "https://identity.org/realms/org-realm/protocol/openid-connect/certs"
assert provider.get_jwks_url() == expected_url
def test_get_issuer(self):
expected_issuer = "https://keycloak.example.com/realms/test-realm"
assert self.provider.get_issuer() == expected_issuer
def test_get_issuer_with_different_domain(self):
settings = Oauth2Settings(
provider="keycloak",
domain="login.myapp.io",
client_id="test-client",
audience="test-audience",
extra={
"realm": "app-realm"
}
)
provider = KeycloakProvider(settings)
expected_issuer = "https://login.myapp.io/realms/app-realm"
assert provider.get_issuer() == expected_issuer
def test_get_audience(self):
assert self.provider.get_audience() == "test-audience"
def test_get_client_id(self):
assert self.provider.get_client_id() == "test-client-id"
def test_get_required_fields(self):
assert self.provider.get_required_fields() == ["realm"]
def test_oauth2_base_url(self):
assert self.provider._oauth2_base_url() == "https://keycloak.example.com"
def test_oauth2_base_url_strips_https_prefix(self):
settings = Oauth2Settings(
provider="keycloak",
domain="https://keycloak.example.com",
client_id="test-client-id",
audience="test-audience",
extra={
"realm": "test-realm"
}
)
provider = KeycloakProvider(settings)
assert provider._oauth2_base_url() == "https://keycloak.example.com"
def test_oauth2_base_url_strips_http_prefix(self):
settings = Oauth2Settings(
provider="keycloak",
domain="http://keycloak.example.com",
client_id="test-client-id",
audience="test-audience",
extra={
"realm": "test-realm"
}
)
provider = KeycloakProvider(settings)
assert provider._oauth2_base_url() == "https://keycloak.example.com"