Compare commits
31 Commits
v1.0.0a2
...
gl/chore/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6081809c76 | ||
|
|
30a4b712a3 | ||
|
|
8465350f1d | ||
|
|
2a23bc604c | ||
|
|
714f8a8940 | ||
|
|
f0fb349ddf | ||
|
|
bf2e2a42da | ||
|
|
e029de2863 | ||
|
|
814c962196 | ||
|
|
6492852a0c | ||
|
|
fecf7e9a83 | ||
|
|
6bc8818ae9 | ||
|
|
620df71763 | ||
|
|
7d6324dfa3 | ||
|
|
541eec0639 | ||
|
|
2ebb2e845f | ||
|
|
7b550ebfe8 | ||
|
|
29919c2d81 | ||
|
|
b71c88814f | ||
|
|
cb8bcfe214 | ||
|
|
13a514f8be | ||
|
|
316b1cea69 | ||
|
|
6f2e39c0dd | ||
|
|
8d93361cb3 | ||
|
|
54ec245d84 | ||
|
|
f589ab9b80 | ||
|
|
fadb59e0f0 | ||
|
|
1a60848425 | ||
|
|
0135163040 | ||
|
|
dac5d6d664 | ||
|
|
f0f94f2540 |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 33 KiB |
21
.github/codeql/codeql-config.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: "CodeQL Config"
|
||||
|
||||
paths-ignore:
|
||||
# Ignore template files - these are boilerplate code that shouldn't be analyzed
|
||||
- "src/crewai/cli/templates/**"
|
||||
# Ignore test cassettes - these are test fixtures/recordings
|
||||
- "tests/cassettes/**"
|
||||
# Ignore cache and build artifacts
|
||||
- ".cache/**"
|
||||
# Ignore documentation build artifacts
|
||||
- "docs/.cache/**"
|
||||
|
||||
paths:
|
||||
# Include all Python source code
|
||||
- "src/**"
|
||||
# Include tests (but exclude cassettes)
|
||||
- "tests/**"
|
||||
|
||||
# Configure specific queries or packs if needed
|
||||
# queries:
|
||||
# - uses: security-and-quality
|
||||
63
.github/security.md
vendored
@@ -1,27 +1,50 @@
|
||||
## CrewAI Security Vulnerability Reporting Policy
|
||||
## CrewAI Security Policy
|
||||
|
||||
CrewAI prioritizes the security of our software products, services, and GitHub repositories. To promptly address vulnerabilities, follow these steps for reporting security issues:
|
||||
We are committed to protecting the confidentiality, integrity, and availability of the CrewAI ecosystem. This policy explains how to report potential vulnerabilities and what you can expect from us when you do.
|
||||
|
||||
### Reporting Process
|
||||
Do **not** report vulnerabilities via public GitHub issues.
|
||||
### Scope
|
||||
|
||||
Email all vulnerability reports directly to:
|
||||
**security@crewai.com**
|
||||
We welcome reports for vulnerabilities that could impact:
|
||||
|
||||
### Required Information
|
||||
To help us quickly validate and remediate the issue, your report must include:
|
||||
- CrewAI-maintained source code and repositories
|
||||
- CrewAI-operated infrastructure and services
|
||||
- Official CrewAI releases, packages, and distributions
|
||||
|
||||
- **Vulnerability Type:** Clearly state the vulnerability type (e.g., SQL injection, XSS, privilege escalation).
|
||||
- **Affected Source Code:** Provide full file paths and direct URLs (branch, tag, or commit).
|
||||
- **Reproduction Steps:** Include detailed, step-by-step instructions. Screenshots are recommended.
|
||||
- **Special Configuration:** Document any special settings or configurations required to reproduce.
|
||||
- **Proof-of-Concept (PoC):** Provide exploit or PoC code (if available).
|
||||
- **Impact Assessment:** Clearly explain the severity and potential exploitation scenarios.
|
||||
Issues affecting clearly unaffiliated third-party services or user-generated content are out of scope, unless you can demonstrate a direct impact on CrewAI systems or customers.
|
||||
|
||||
### Our Response
|
||||
- We will acknowledge receipt of your report promptly via your provided email.
|
||||
- Confirmed vulnerabilities will receive priority remediation based on severity.
|
||||
- Patches will be released as swiftly as possible following verification.
|
||||
### How to Report
|
||||
|
||||
### Reward Notice
|
||||
Currently, we do not offer a bug bounty program. Rewards, if issued, are discretionary.
|
||||
- **Please do not** disclose vulnerabilities via public GitHub issues, pull requests, or social media.
|
||||
- Email detailed reports to **security@crewai.com** with the subject line `Security Report`.
|
||||
- If you need to share large files or sensitive artifacts, mention it in your email and we will coordinate a secure transfer method.
|
||||
|
||||
### What to Include
|
||||
|
||||
Providing comprehensive information enables us to validate the issue quickly:
|
||||
|
||||
- **Vulnerability overview** — a concise description and classification (e.g., RCE, privilege escalation)
|
||||
- **Affected components** — repository, branch, tag, or deployed service along with relevant file paths or endpoints
|
||||
- **Reproduction steps** — detailed, step-by-step instructions; include logs, screenshots, or screen recordings when helpful
|
||||
- **Proof-of-concept** — exploit details or code that demonstrates the impact (if available)
|
||||
- **Impact analysis** — severity assessment, potential exploitation scenarios, and any prerequisites or special configurations
|
||||
|
||||
### Our Commitment
|
||||
|
||||
- **Acknowledgement:** We aim to acknowledge your report within two business days.
|
||||
- **Communication:** We will keep you informed about triage results, remediation progress, and planned release timelines.
|
||||
- **Resolution:** Confirmed vulnerabilities will be prioritized based on severity and fixed as quickly as possible.
|
||||
- **Recognition:** We currently do not run a bug bounty program; any rewards or recognition are issued at CrewAI's discretion.
|
||||
|
||||
### Coordinated Disclosure
|
||||
|
||||
We ask that you allow us a reasonable window to investigate and remediate confirmed issues before any public disclosure. We will coordinate publication timelines with you whenever possible.
|
||||
|
||||
### Safe Harbor
|
||||
|
||||
We will not pursue or support legal action against individuals who, in good faith:
|
||||
|
||||
- Follow this policy and refrain from violating any applicable laws
|
||||
- Avoid privacy violations, data destruction, or service disruption
|
||||
- Limit testing to systems in scope and respect rate limits and terms of service
|
||||
|
||||
If you are unsure whether your testing is covered, please contact us at **security@crewai.com** before proceeding.
|
||||
|
||||
2
.github/workflows/build-uv-cache.yml
vendored
@@ -7,6 +7,8 @@ on:
|
||||
paths:
|
||||
- "uv.lock"
|
||||
- "pyproject.toml"
|
||||
schedule:
|
||||
- cron: "0 0 */5 * *" # Run every 5 days at midnight UTC to prevent cache expiration
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
1
.github/workflows/codeql.yml
vendored
@@ -73,6 +73,7 @@ jobs:
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
1737
crewAI.excalidraw
@@ -397,6 +397,7 @@
|
||||
"en/enterprise/guides/kickoff-crew",
|
||||
"en/enterprise/guides/update-crew",
|
||||
"en/enterprise/guides/enable-crew-studio",
|
||||
"en/enterprise/guides/capture_telemetry_logs",
|
||||
"en/enterprise/guides/azure-openai-setup",
|
||||
"en/enterprise/guides/tool-repository",
|
||||
"en/enterprise/guides/react-component-export",
|
||||
@@ -421,6 +422,7 @@
|
||||
"en/api-reference/introduction",
|
||||
"en/api-reference/inputs",
|
||||
"en/api-reference/kickoff",
|
||||
"en/api-reference/resume",
|
||||
"en/api-reference/status"
|
||||
]
|
||||
}
|
||||
@@ -827,6 +829,7 @@
|
||||
"pt-BR/api-reference/introduction",
|
||||
"pt-BR/api-reference/inputs",
|
||||
"pt-BR/api-reference/kickoff",
|
||||
"pt-BR/api-reference/resume",
|
||||
"pt-BR/api-reference/status"
|
||||
]
|
||||
}
|
||||
@@ -1239,6 +1242,7 @@
|
||||
"ko/api-reference/introduction",
|
||||
"ko/api-reference/inputs",
|
||||
"ko/api-reference/kickoff",
|
||||
"ko/api-reference/resume",
|
||||
"ko/api-reference/status"
|
||||
]
|
||||
}
|
||||
|
||||
6
docs/en/api-reference/resume.mdx
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: "POST /resume"
|
||||
description: "Resume crew execution with human feedback"
|
||||
openapi: "/enterprise-api.en.yaml POST /resume"
|
||||
mode: "wide"
|
||||
---
|
||||
35
docs/en/enterprise/guides/capture_telemetry_logs.mdx
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: "Open Telemetry Logs"
|
||||
description: "Understand how to capture telemetry logs from your CrewAI AMP deployments"
|
||||
icon: "magnifying-glass-chart"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
CrewAI AMP provides a powerful way to capture telemetry logs from your deployments. This allows you to monitor the performance of your agents and workflows, and to debug issues that may arise.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="ENTERPRISE OTEL SETUP enabled" icon="users">
|
||||
Your organization should have ENTERPRISE OTEL SETUP enabled
|
||||
</Card>
|
||||
<Card title="OTEL collector setup" icon="server">
|
||||
Your organization should have an OTEL collector setup or a provider like Datadog log intake setup
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
|
||||
## How to capture telemetry logs
|
||||
|
||||
1. Go to settings/organization tab
|
||||
2. Configure your OTEL collector setup
|
||||
3. Save
|
||||
|
||||
|
||||
|
||||
Example to setup OTEL log collection capture to Datadog.
|
||||
|
||||
|
||||
<Frame>
|
||||

|
||||
</Frame>
|
||||
@@ -40,6 +40,28 @@ Human-In-The-Loop (HITL) is a powerful approach that combines artificial intelli
|
||||
<Frame>
|
||||
<img src="/images/enterprise/crew-resume-endpoint.png" alt="Crew Resume Endpoint" />
|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**Critical: Webhook URLs Must Be Provided Again**:
|
||||
You **must** provide the same webhook URLs (`taskWebhookUrl`, `stepWebhookUrl`, `crewWebhookUrl`) in the resume call that you used in the kickoff call. Webhook configurations are **NOT** automatically carried over from kickoff - they must be explicitly included in the resume request to continue receiving notifications for task completion, agent steps, and crew completion.
|
||||
</Warning>
|
||||
|
||||
Example resume call with webhooks:
|
||||
```bash
|
||||
curl -X POST {BASE_URL}/resume \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"execution_id": "abcd1234-5678-90ef-ghij-klmnopqrstuv",
|
||||
"task_id": "research_task",
|
||||
"human_feedback": "Great work! Please add more details.",
|
||||
"is_approve": true,
|
||||
"taskWebhookUrl": "https://your-server.com/webhooks/task",
|
||||
"stepWebhookUrl": "https://your-server.com/webhooks/step",
|
||||
"crewWebhookUrl": "https://your-server.com/webhooks/crew"
|
||||
}'
|
||||
```
|
||||
|
||||
<Warning>
|
||||
**Feedback Impact on Task Execution**:
|
||||
It's crucial to exercise care when providing feedback, as the entire feedback content will be incorporated as additional context for further task executions.
|
||||
@@ -76,4 +98,4 @@ HITL workflows are particularly valuable for:
|
||||
- Complex decision-making scenarios
|
||||
- Sensitive or high-stakes operations
|
||||
- Creative tasks requiring human judgment
|
||||
- Compliance and regulatory reviews
|
||||
- Compliance and regulatory reviews
|
||||
|
||||
@@ -79,6 +79,28 @@ Human-in-the-Loop (HITL) is a powerful approach that combines artificial intelli
|
||||
<Frame>
|
||||
<img src="/images/enterprise/crew-resume-endpoint.png" alt="Crew Resume Endpoint" />
|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**Critical: Webhook URLs Must Be Provided Again**:
|
||||
You **must** provide the same webhook URLs (`taskWebhookUrl`, `stepWebhookUrl`, `crewWebhookUrl`) in the resume call that you used in the kickoff call. Webhook configurations are **NOT** automatically carried over from kickoff - they must be explicitly included in the resume request to continue receiving notifications for task completion, agent steps, and crew completion.
|
||||
</Warning>
|
||||
|
||||
Example resume call with webhooks:
|
||||
```bash
|
||||
curl -X POST {BASE_URL}/resume \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"execution_id": "abcd1234-5678-90ef-ghij-klmnopqrstuv",
|
||||
"task_id": "research_task",
|
||||
"human_feedback": "Great work! Please add more details.",
|
||||
"is_approve": true,
|
||||
"taskWebhookUrl": "https://your-server.com/webhooks/task",
|
||||
"stepWebhookUrl": "https://your-server.com/webhooks/step",
|
||||
"crewWebhookUrl": "https://your-server.com/webhooks/crew"
|
||||
}'
|
||||
```
|
||||
|
||||
<Warning>
|
||||
**Feedback Impact on Task Execution**:
|
||||
It's crucial to exercise care when providing feedback, as the entire feedback content will be incorporated as additional context for further task executions.
|
||||
|
||||
@@ -276,6 +276,134 @@ paths:
|
||||
'500':
|
||||
$ref: '#/components/responses/ServerError'
|
||||
|
||||
/resume:
|
||||
post:
|
||||
summary: Resume Crew Execution with Human Feedback
|
||||
description: |
|
||||
**📋 Reference Example Only** - *This shows the request format. To test with your actual crew, copy the cURL example and replace the URL + token with your real values.*
|
||||
|
||||
Resume a paused crew execution with human feedback for Human-in-the-Loop (HITL) workflows.
|
||||
When a task with `human_input=True` completes, the crew execution pauses and waits for human feedback.
|
||||
|
||||
**IMPORTANT**: You must provide the same webhook URLs (`taskWebhookUrl`, `stepWebhookUrl`, `crewWebhookUrl`)
|
||||
that were used in the original kickoff call. Webhook configurations are NOT automatically carried over -
|
||||
they must be explicitly provided in the resume request to continue receiving notifications.
|
||||
operationId: resumeCrewExecution
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- execution_id
|
||||
- task_id
|
||||
- human_feedback
|
||||
- is_approve
|
||||
properties:
|
||||
execution_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: The unique identifier for the crew execution (from kickoff)
|
||||
example: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
|
||||
task_id:
|
||||
type: string
|
||||
description: The ID of the task that requires human feedback
|
||||
example: "research_task"
|
||||
human_feedback:
|
||||
type: string
|
||||
description: Your feedback on the task output. This will be incorporated as additional context for subsequent task executions.
|
||||
example: "Great research! Please add more details about recent developments in the field."
|
||||
is_approve:
|
||||
type: boolean
|
||||
description: "Whether you approve the task output: true = positive feedback (continue), false = negative feedback (retry task)"
|
||||
example: true
|
||||
taskWebhookUrl:
|
||||
type: string
|
||||
format: uri
|
||||
description: Callback URL executed after each task completion. MUST be provided to continue receiving task notifications.
|
||||
example: "https://your-server.com/webhooks/task"
|
||||
stepWebhookUrl:
|
||||
type: string
|
||||
format: uri
|
||||
description: Callback URL executed after each agent thought/action. MUST be provided to continue receiving step notifications.
|
||||
example: "https://your-server.com/webhooks/step"
|
||||
crewWebhookUrl:
|
||||
type: string
|
||||
format: uri
|
||||
description: Callback URL executed when the crew execution completes. MUST be provided to receive completion notification.
|
||||
example: "https://your-server.com/webhooks/crew"
|
||||
examples:
|
||||
approve_and_continue:
|
||||
summary: Approve task and continue execution
|
||||
value:
|
||||
execution_id: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
|
||||
task_id: "research_task"
|
||||
human_feedback: "Excellent research! Proceed to the next task."
|
||||
is_approve: true
|
||||
taskWebhookUrl: "https://api.example.com/webhooks/task"
|
||||
stepWebhookUrl: "https://api.example.com/webhooks/step"
|
||||
crewWebhookUrl: "https://api.example.com/webhooks/crew"
|
||||
request_revision:
|
||||
summary: Request task revision with feedback
|
||||
value:
|
||||
execution_id: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
|
||||
task_id: "analysis_task"
|
||||
human_feedback: "Please include more quantitative data and cite your sources."
|
||||
is_approve: false
|
||||
taskWebhookUrl: "https://api.example.com/webhooks/task"
|
||||
crewWebhookUrl: "https://api.example.com/webhooks/crew"
|
||||
responses:
|
||||
'200':
|
||||
description: Execution resumed successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: ["resumed", "retrying", "completed"]
|
||||
description: Status of the resumed execution
|
||||
example: "resumed"
|
||||
message:
|
||||
type: string
|
||||
description: Human-readable message about the resume operation
|
||||
example: "Execution resumed successfully"
|
||||
examples:
|
||||
resumed:
|
||||
summary: Execution resumed with positive feedback
|
||||
value:
|
||||
status: "resumed"
|
||||
message: "Execution resumed successfully"
|
||||
retrying:
|
||||
summary: Task will be retried with negative feedback
|
||||
value:
|
||||
status: "retrying"
|
||||
message: "Task will be retried with your feedback"
|
||||
'400':
|
||||
description: Invalid request body or execution not in pending state
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
example:
|
||||
error: "Invalid Request"
|
||||
message: "Execution is not in pending human input state"
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'404':
|
||||
description: Execution ID or Task ID not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
example:
|
||||
error: "Not Found"
|
||||
message: "Execution ID not found"
|
||||
'500':
|
||||
$ref: '#/components/responses/ServerError'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
|
||||
@@ -276,6 +276,134 @@ paths:
|
||||
'500':
|
||||
$ref: '#/components/responses/ServerError'
|
||||
|
||||
/resume:
|
||||
post:
|
||||
summary: Resume Crew Execution with Human Feedback
|
||||
description: |
|
||||
**📋 Reference Example Only** - *This shows the request format. To test with your actual crew, copy the cURL example and replace the URL + token with your real values.*
|
||||
|
||||
Resume a paused crew execution with human feedback for Human-in-the-Loop (HITL) workflows.
|
||||
When a task with `human_input=True` completes, the crew execution pauses and waits for human feedback.
|
||||
|
||||
**IMPORTANT**: You must provide the same webhook URLs (`taskWebhookUrl`, `stepWebhookUrl`, `crewWebhookUrl`)
|
||||
that were used in the original kickoff call. Webhook configurations are NOT automatically carried over -
|
||||
they must be explicitly provided in the resume request to continue receiving notifications.
|
||||
operationId: resumeCrewExecution
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- execution_id
|
||||
- task_id
|
||||
- human_feedback
|
||||
- is_approve
|
||||
properties:
|
||||
execution_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: The unique identifier for the crew execution (from kickoff)
|
||||
example: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
|
||||
task_id:
|
||||
type: string
|
||||
description: The ID of the task that requires human feedback
|
||||
example: "research_task"
|
||||
human_feedback:
|
||||
type: string
|
||||
description: Your feedback on the task output. This will be incorporated as additional context for subsequent task executions.
|
||||
example: "Great research! Please add more details about recent developments in the field."
|
||||
is_approve:
|
||||
type: boolean
|
||||
description: "Whether you approve the task output: true = positive feedback (continue), false = negative feedback (retry task)"
|
||||
example: true
|
||||
taskWebhookUrl:
|
||||
type: string
|
||||
format: uri
|
||||
description: Callback URL executed after each task completion. MUST be provided to continue receiving task notifications.
|
||||
example: "https://your-server.com/webhooks/task"
|
||||
stepWebhookUrl:
|
||||
type: string
|
||||
format: uri
|
||||
description: Callback URL executed after each agent thought/action. MUST be provided to continue receiving step notifications.
|
||||
example: "https://your-server.com/webhooks/step"
|
||||
crewWebhookUrl:
|
||||
type: string
|
||||
format: uri
|
||||
description: Callback URL executed when the crew execution completes. MUST be provided to receive completion notification.
|
||||
example: "https://your-server.com/webhooks/crew"
|
||||
examples:
|
||||
approve_and_continue:
|
||||
summary: Approve task and continue execution
|
||||
value:
|
||||
execution_id: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
|
||||
task_id: "research_task"
|
||||
human_feedback: "Excellent research! Proceed to the next task."
|
||||
is_approve: true
|
||||
taskWebhookUrl: "https://api.example.com/webhooks/task"
|
||||
stepWebhookUrl: "https://api.example.com/webhooks/step"
|
||||
crewWebhookUrl: "https://api.example.com/webhooks/crew"
|
||||
request_revision:
|
||||
summary: Request task revision with feedback
|
||||
value:
|
||||
execution_id: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
|
||||
task_id: "analysis_task"
|
||||
human_feedback: "Please include more quantitative data and cite your sources."
|
||||
is_approve: false
|
||||
taskWebhookUrl: "https://api.example.com/webhooks/task"
|
||||
crewWebhookUrl: "https://api.example.com/webhooks/crew"
|
||||
responses:
|
||||
'200':
|
||||
description: Execution resumed successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: ["resumed", "retrying", "completed"]
|
||||
description: Status of the resumed execution
|
||||
example: "resumed"
|
||||
message:
|
||||
type: string
|
||||
description: Human-readable message about the resume operation
|
||||
example: "Execution resumed successfully"
|
||||
examples:
|
||||
resumed:
|
||||
summary: Execution resumed with positive feedback
|
||||
value:
|
||||
status: "resumed"
|
||||
message: "Execution resumed successfully"
|
||||
retrying:
|
||||
summary: Task will be retried with negative feedback
|
||||
value:
|
||||
status: "retrying"
|
||||
message: "Task will be retried with your feedback"
|
||||
'400':
|
||||
description: Invalid request body or execution not in pending state
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
example:
|
||||
error: "Invalid Request"
|
||||
message: "Execution is not in pending human input state"
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'404':
|
||||
description: Execution ID or Task ID not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
example:
|
||||
error: "Not Found"
|
||||
message: "Execution ID not found"
|
||||
'500':
|
||||
$ref: '#/components/responses/ServerError'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
|
||||
@@ -120,6 +120,134 @@ paths:
|
||||
'500':
|
||||
$ref: '#/components/responses/ServerError'
|
||||
|
||||
/resume:
|
||||
post:
|
||||
summary: Resume Crew Execution with Human Feedback
|
||||
description: |
|
||||
**📋 Reference Example Only** - *This shows the request format. To test with your actual crew, copy the cURL example and replace the URL + token with your real values.*
|
||||
|
||||
Resume a paused crew execution with human feedback for Human-in-the-Loop (HITL) workflows.
|
||||
When a task with `human_input=True` completes, the crew execution pauses and waits for human feedback.
|
||||
|
||||
**IMPORTANT**: You must provide the same webhook URLs (`taskWebhookUrl`, `stepWebhookUrl`, `crewWebhookUrl`)
|
||||
that were used in the original kickoff call. Webhook configurations are NOT automatically carried over -
|
||||
they must be explicitly provided in the resume request to continue receiving notifications.
|
||||
operationId: resumeCrewExecution
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- execution_id
|
||||
- task_id
|
||||
- human_feedback
|
||||
- is_approve
|
||||
properties:
|
||||
execution_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: The unique identifier for the crew execution (from kickoff)
|
||||
example: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
|
||||
task_id:
|
||||
type: string
|
||||
description: The ID of the task that requires human feedback
|
||||
example: "research_task"
|
||||
human_feedback:
|
||||
type: string
|
||||
description: Your feedback on the task output. This will be incorporated as additional context for subsequent task executions.
|
||||
example: "Great research! Please add more details about recent developments in the field."
|
||||
is_approve:
|
||||
type: boolean
|
||||
description: "Whether you approve the task output: true = positive feedback (continue), false = negative feedback (retry task)"
|
||||
example: true
|
||||
taskWebhookUrl:
|
||||
type: string
|
||||
format: uri
|
||||
description: Callback URL executed after each task completion. MUST be provided to continue receiving task notifications.
|
||||
example: "https://your-server.com/webhooks/task"
|
||||
stepWebhookUrl:
|
||||
type: string
|
||||
format: uri
|
||||
description: Callback URL executed after each agent thought/action. MUST be provided to continue receiving step notifications.
|
||||
example: "https://your-server.com/webhooks/step"
|
||||
crewWebhookUrl:
|
||||
type: string
|
||||
format: uri
|
||||
description: Callback URL executed when the crew execution completes. MUST be provided to receive completion notification.
|
||||
example: "https://your-server.com/webhooks/crew"
|
||||
examples:
|
||||
approve_and_continue:
|
||||
summary: Approve task and continue execution
|
||||
value:
|
||||
execution_id: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
|
||||
task_id: "research_task"
|
||||
human_feedback: "Excellent research! Proceed to the next task."
|
||||
is_approve: true
|
||||
taskWebhookUrl: "https://api.example.com/webhooks/task"
|
||||
stepWebhookUrl: "https://api.example.com/webhooks/step"
|
||||
crewWebhookUrl: "https://api.example.com/webhooks/crew"
|
||||
request_revision:
|
||||
summary: Request task revision with feedback
|
||||
value:
|
||||
execution_id: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
|
||||
task_id: "analysis_task"
|
||||
human_feedback: "Please include more quantitative data and cite your sources."
|
||||
is_approve: false
|
||||
taskWebhookUrl: "https://api.example.com/webhooks/task"
|
||||
crewWebhookUrl: "https://api.example.com/webhooks/crew"
|
||||
responses:
|
||||
'200':
|
||||
description: Execution resumed successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: ["resumed", "retrying", "completed"]
|
||||
description: Status of the resumed execution
|
||||
example: "resumed"
|
||||
message:
|
||||
type: string
|
||||
description: Human-readable message about the resume operation
|
||||
example: "Execution resumed successfully"
|
||||
examples:
|
||||
resumed:
|
||||
summary: Execution resumed with positive feedback
|
||||
value:
|
||||
status: "resumed"
|
||||
message: "Execution resumed successfully"
|
||||
retrying:
|
||||
summary: Task will be retried with negative feedback
|
||||
value:
|
||||
status: "retrying"
|
||||
message: "Task will be retried with your feedback"
|
||||
'400':
|
||||
description: Invalid request body or execution not in pending state
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
example:
|
||||
error: "Invalid Request"
|
||||
message: "Execution is not in pending human input state"
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'404':
|
||||
description: Execution ID or Task ID not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
example:
|
||||
error: "Not Found"
|
||||
message: "Execution ID not found"
|
||||
'500':
|
||||
$ref: '#/components/responses/ServerError'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
|
||||
@@ -156,6 +156,134 @@ paths:
|
||||
'500':
|
||||
$ref: '#/components/responses/ServerError'
|
||||
|
||||
/resume:
|
||||
post:
|
||||
summary: Resume Crew Execution with Human Feedback
|
||||
description: |
|
||||
**📋 Reference Example Only** - *This shows the request format. To test with your actual crew, copy the cURL example and replace the URL + token with your real values.*
|
||||
|
||||
Resume a paused crew execution with human feedback for Human-in-the-Loop (HITL) workflows.
|
||||
When a task with `human_input=True` completes, the crew execution pauses and waits for human feedback.
|
||||
|
||||
**IMPORTANT**: You must provide the same webhook URLs (`taskWebhookUrl`, `stepWebhookUrl`, `crewWebhookUrl`)
|
||||
that were used in the original kickoff call. Webhook configurations are NOT automatically carried over -
|
||||
they must be explicitly provided in the resume request to continue receiving notifications.
|
||||
operationId: resumeCrewExecution
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- execution_id
|
||||
- task_id
|
||||
- human_feedback
|
||||
- is_approve
|
||||
properties:
|
||||
execution_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: The unique identifier for the crew execution (from kickoff)
|
||||
example: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
|
||||
task_id:
|
||||
type: string
|
||||
description: The ID of the task that requires human feedback
|
||||
example: "research_task"
|
||||
human_feedback:
|
||||
type: string
|
||||
description: Your feedback on the task output. This will be incorporated as additional context for subsequent task executions.
|
||||
example: "Great research! Please add more details about recent developments in the field."
|
||||
is_approve:
|
||||
type: boolean
|
||||
description: "Whether you approve the task output: true = positive feedback (continue), false = negative feedback (retry task)"
|
||||
example: true
|
||||
taskWebhookUrl:
|
||||
type: string
|
||||
format: uri
|
||||
description: Callback URL executed after each task completion. MUST be provided to continue receiving task notifications.
|
||||
example: "https://your-server.com/webhooks/task"
|
||||
stepWebhookUrl:
|
||||
type: string
|
||||
format: uri
|
||||
description: Callback URL executed after each agent thought/action. MUST be provided to continue receiving step notifications.
|
||||
example: "https://your-server.com/webhooks/step"
|
||||
crewWebhookUrl:
|
||||
type: string
|
||||
format: uri
|
||||
description: Callback URL executed when the crew execution completes. MUST be provided to receive completion notification.
|
||||
example: "https://your-server.com/webhooks/crew"
|
||||
examples:
|
||||
approve_and_continue:
|
||||
summary: Approve task and continue execution
|
||||
value:
|
||||
execution_id: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
|
||||
task_id: "research_task"
|
||||
human_feedback: "Excellent research! Proceed to the next task."
|
||||
is_approve: true
|
||||
taskWebhookUrl: "https://api.example.com/webhooks/task"
|
||||
stepWebhookUrl: "https://api.example.com/webhooks/step"
|
||||
crewWebhookUrl: "https://api.example.com/webhooks/crew"
|
||||
request_revision:
|
||||
summary: Request task revision with feedback
|
||||
value:
|
||||
execution_id: "abcd1234-5678-90ef-ghij-klmnopqrstuv"
|
||||
task_id: "analysis_task"
|
||||
human_feedback: "Please include more quantitative data and cite your sources."
|
||||
is_approve: false
|
||||
taskWebhookUrl: "https://api.example.com/webhooks/task"
|
||||
crewWebhookUrl: "https://api.example.com/webhooks/crew"
|
||||
responses:
|
||||
'200':
|
||||
description: Execution resumed successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: ["resumed", "retrying", "completed"]
|
||||
description: Status of the resumed execution
|
||||
example: "resumed"
|
||||
message:
|
||||
type: string
|
||||
description: Human-readable message about the resume operation
|
||||
example: "Execution resumed successfully"
|
||||
examples:
|
||||
resumed:
|
||||
summary: Execution resumed with positive feedback
|
||||
value:
|
||||
status: "resumed"
|
||||
message: "Execution resumed successfully"
|
||||
retrying:
|
||||
summary: Task will be retried with negative feedback
|
||||
value:
|
||||
status: "retrying"
|
||||
message: "Task will be retried with your feedback"
|
||||
'400':
|
||||
description: Invalid request body or execution not in pending state
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
example:
|
||||
error: "Invalid Request"
|
||||
message: "Execution is not in pending human input state"
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'404':
|
||||
description: Execution ID or Task ID not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
example:
|
||||
error: "Not Found"
|
||||
message: "Execution ID not found"
|
||||
'500':
|
||||
$ref: '#/components/responses/ServerError'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
|
||||
BIN
docs/images/crewai-otel-export.png
Normal file
|
After Width: | Height: | Size: 317 KiB |
6
docs/ko/api-reference/resume.mdx
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: "POST /resume"
|
||||
description: "인간 피드백으로 crew 실행 재개"
|
||||
openapi: "/enterprise-api.ko.yaml POST /resume"
|
||||
mode: "wide"
|
||||
---
|
||||
@@ -40,6 +40,28 @@ mode: "wide"
|
||||
<Frame>
|
||||
<img src="/images/enterprise/crew-resume-endpoint.png" alt="Crew Resume Endpoint" />
|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**중요: Webhook URL을 다시 제공해야 합니다**:
|
||||
kickoff 호출에서 사용한 것과 동일한 webhook URL(`taskWebhookUrl`, `stepWebhookUrl`, `crewWebhookUrl`)을 resume 호출에서 **반드시** 제공해야 합니다. Webhook 설정은 kickoff에서 자동으로 전달되지 **않으므로**, 작업 완료, 에이전트 단계, crew 완료에 대한 알림을 계속 받으려면 resume 요청에 명시적으로 포함해야 합니다.
|
||||
</Warning>
|
||||
|
||||
Webhook을 포함한 resume 호출 예시:
|
||||
```bash
|
||||
curl -X POST {BASE_URL}/resume \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"execution_id": "abcd1234-5678-90ef-ghij-klmnopqrstuv",
|
||||
"task_id": "research_task",
|
||||
"human_feedback": "훌륭한 작업입니다! 더 자세한 내용을 추가해주세요.",
|
||||
"is_approve": true,
|
||||
"taskWebhookUrl": "https://your-server.com/webhooks/task",
|
||||
"stepWebhookUrl": "https://your-server.com/webhooks/step",
|
||||
"crewWebhookUrl": "https://your-server.com/webhooks/crew"
|
||||
}'
|
||||
```
|
||||
|
||||
<Warning>
|
||||
**피드백이 작업 실행에 미치는 영향**:
|
||||
피드백 전체 내용이 이후 작업 실행을 위한 추가 컨텍스트로 통합되므로 피드백 제공 시 신중함이 매우 중요합니다.
|
||||
@@ -76,4 +98,4 @@ HITL 워크플로우는 특히 다음과 같은 경우에 유용합니다:
|
||||
- 복잡한 의사 결정 시나리오
|
||||
- 민감하거나 위험도가 높은 작업
|
||||
- 인간의 판단이 필요한 창의적 작업
|
||||
- 준수 및 규제 검토
|
||||
- 준수 및 규제 검토
|
||||
|
||||
@@ -40,6 +40,28 @@ mode: "wide"
|
||||
<Frame>
|
||||
<img src="/images/enterprise/crew-resume-endpoint.png" alt="Crew Resume Endpoint" />
|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**중요: Webhook URL을 다시 제공해야 합니다**:
|
||||
kickoff 호출에서 사용한 것과 동일한 webhook URL(`taskWebhookUrl`, `stepWebhookUrl`, `crewWebhookUrl`)을 resume 호출에서 **반드시** 제공해야 합니다. Webhook 설정은 kickoff에서 자동으로 전달되지 **않으므로**, 작업 완료, 에이전트 단계, crew 완료에 대한 알림을 계속 받으려면 resume 요청에 명시적으로 포함해야 합니다.
|
||||
</Warning>
|
||||
|
||||
Webhook을 포함한 resume 호출 예시:
|
||||
```bash
|
||||
curl -X POST {BASE_URL}/resume \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"execution_id": "abcd1234-5678-90ef-ghij-klmnopqrstuv",
|
||||
"task_id": "research_task",
|
||||
"human_feedback": "훌륭한 작업입니다! 더 자세한 내용을 추가해주세요.",
|
||||
"is_approve": true,
|
||||
"taskWebhookUrl": "https://your-server.com/webhooks/task",
|
||||
"stepWebhookUrl": "https://your-server.com/webhooks/step",
|
||||
"crewWebhookUrl": "https://your-server.com/webhooks/crew"
|
||||
}'
|
||||
```
|
||||
|
||||
<Warning>
|
||||
**피드백이 작업 실행에 미치는 영향**:
|
||||
피드백의 전체 내용이 추가 컨텍스트로서 이후 작업 실행에 통합되므로, 피드백 제공 시 신중을 기하는 것이 매우 중요합니다.
|
||||
@@ -76,4 +98,4 @@ HITL 워크플로우는 다음과 같은 경우에 특히 유용합니다:
|
||||
- 복잡한 의사결정 시나리오
|
||||
- 민감하거나 고위험 작업
|
||||
- 인간의 판단이 필요한 창의적 과제
|
||||
- 컴플라이언스 및 규제 검토
|
||||
- 컴플라이언스 및 규제 검토
|
||||
|
||||
6
docs/pt-BR/api-reference/resume.mdx
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: "POST /resume"
|
||||
description: "Retomar execução do crew com feedback humano"
|
||||
openapi: "/enterprise-api.pt-BR.yaml POST /resume"
|
||||
mode: "wide"
|
||||
---
|
||||
@@ -40,6 +40,28 @@ Human-In-The-Loop (HITL) é uma abordagem poderosa que combina inteligência art
|
||||
<Frame>
|
||||
<img src="/images/enterprise/crew-resume-endpoint.png" alt="Crew Resume Endpoint" />
|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**Crítico: URLs de Webhook Devem Ser Fornecidas Novamente**:
|
||||
Você **deve** fornecer as mesmas URLs de webhook (`taskWebhookUrl`, `stepWebhookUrl`, `crewWebhookUrl`) na chamada de resume que você usou na chamada de kickoff. As configurações de webhook **NÃO** são automaticamente transferidas do kickoff - elas devem ser explicitamente incluídas na solicitação de resume para continuar recebendo notificações de conclusão de tarefa, etapas do agente e conclusão do crew.
|
||||
</Warning>
|
||||
|
||||
Exemplo de chamada resume com webhooks:
|
||||
```bash
|
||||
curl -X POST {BASE_URL}/resume \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"execution_id": "abcd1234-5678-90ef-ghij-klmnopqrstuv",
|
||||
"task_id": "research_task",
|
||||
"human_feedback": "Ótimo trabalho! Por favor, adicione mais detalhes.",
|
||||
"is_approve": true,
|
||||
"taskWebhookUrl": "https://your-server.com/webhooks/task",
|
||||
"stepWebhookUrl": "https://your-server.com/webhooks/step",
|
||||
"crewWebhookUrl": "https://your-server.com/webhooks/crew"
|
||||
}'
|
||||
```
|
||||
|
||||
<Warning>
|
||||
**Impacto do Feedback na Execução da Tarefa**:
|
||||
É crucial ter cuidado ao fornecer o feedback, pois todo o conteúdo do feedback será incorporado como contexto adicional para as próximas execuções da tarefa.
|
||||
@@ -76,4 +98,4 @@ Workflows HITL são particularmente valiosos para:
|
||||
- Cenários de tomada de decisão complexa
|
||||
- Operações sensíveis ou de alto risco
|
||||
- Tarefas criativas que exigem julgamento humano
|
||||
- Revisões de conformidade e regulatórias
|
||||
- Revisões de conformidade e regulatórias
|
||||
|
||||
@@ -40,6 +40,28 @@ Human-in-the-Loop (HITL) é uma abordagem poderosa que combina a inteligência a
|
||||
<Frame>
|
||||
<img src="/images/enterprise/crew-resume-endpoint.png" alt="Endpoint de Retomada Crew" />
|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
**Crítico: URLs de Webhook Devem Ser Fornecidas Novamente**:
|
||||
Você **deve** fornecer as mesmas URLs de webhook (`taskWebhookUrl`, `stepWebhookUrl`, `crewWebhookUrl`) na chamada de resume que você usou na chamada de kickoff. As configurações de webhook **NÃO** são automaticamente transferidas do kickoff - elas devem ser explicitamente incluídas na solicitação de resume para continuar recebendo notificações de conclusão de tarefa, etapas do agente e conclusão do crew.
|
||||
</Warning>
|
||||
|
||||
Exemplo de chamada resume com webhooks:
|
||||
```bash
|
||||
curl -X POST {BASE_URL}/resume \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"execution_id": "abcd1234-5678-90ef-ghij-klmnopqrstuv",
|
||||
"task_id": "research_task",
|
||||
"human_feedback": "Ótimo trabalho! Por favor, adicione mais detalhes.",
|
||||
"is_approve": true,
|
||||
"taskWebhookUrl": "https://your-server.com/webhooks/task",
|
||||
"stepWebhookUrl": "https://your-server.com/webhooks/step",
|
||||
"crewWebhookUrl": "https://your-server.com/webhooks/crew"
|
||||
}'
|
||||
```
|
||||
|
||||
<Warning>
|
||||
**Impacto do Feedback na Execução da Tarefa**:
|
||||
É fundamental ter cuidado ao fornecer feedback, pois todo o conteúdo do feedback será incorporado como contexto adicional para execuções futuras da tarefa.
|
||||
@@ -76,4 +98,4 @@ Workflows HITL são particularmente valiosos para:
|
||||
- Cenários de tomada de decisão complexa
|
||||
- Operações sensíveis ou de alto risco
|
||||
- Tarefas criativas que requerem julgamento humano
|
||||
- Revisões de conformidade e regulamentação
|
||||
- Revisões de conformidade e regulamentação
|
||||
|
||||
@@ -49,7 +49,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools>=0.74.0",
|
||||
"crewai-tools>=0.76.0",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
|
||||
@@ -40,7 +40,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "0.201.1"
|
||||
__version__ = "0.203.1"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ from crewai.utilities.converter import generate_model_description
|
||||
from crewai.utilities.llm_utils import create_llm
|
||||
from crewai.utilities.token_counter_callback import TokenCalcHandler
|
||||
from crewai.utilities.training_handler import CrewTrainingHandler
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
class Agent(BaseAgent):
|
||||
@@ -174,7 +175,7 @@ class Agent(BaseAgent):
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
def validate_from_repository(cls, v): # noqa: N805
|
||||
def validate_from_repository(cls, v): # noqa: N805
|
||||
if v is not None and (from_repository := v.get("from_repository")):
|
||||
return load_agent_from_repository(from_repository) | v
|
||||
return v
|
||||
@@ -347,15 +348,16 @@ class Agent(BaseAgent):
|
||||
)
|
||||
|
||||
if self.knowledge or (self.crew and self.crew.knowledge):
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=KnowledgeRetrievalStartedEvent(
|
||||
agent=self,
|
||||
),
|
||||
)
|
||||
try:
|
||||
self.knowledge_search_query = self._get_knowledge_search_query(
|
||||
task_prompt
|
||||
task_prompt, task
|
||||
)
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=KnowledgeRetrievalStartedEvent(
|
||||
from_task=task,
|
||||
from_agent=self,
|
||||
),
|
||||
)
|
||||
if self.knowledge_search_query:
|
||||
# Quering agent specific knowledge
|
||||
@@ -385,7 +387,8 @@ class Agent(BaseAgent):
|
||||
self,
|
||||
event=KnowledgeRetrievalCompletedEvent(
|
||||
query=self.knowledge_search_query,
|
||||
agent=self,
|
||||
from_task=task,
|
||||
from_agent=self,
|
||||
retrieved_knowledge=(
|
||||
(self.agent_knowledge_context or "")
|
||||
+ (
|
||||
@@ -403,8 +406,9 @@ class Agent(BaseAgent):
|
||||
self,
|
||||
event=KnowledgeSearchQueryFailedEvent(
|
||||
query=self.knowledge_search_query or "",
|
||||
agent=self,
|
||||
error=str(e),
|
||||
from_task=task,
|
||||
from_agent=self,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -702,7 +706,7 @@ class Agent(BaseAgent):
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
["/usr/bin/docker", "info"],
|
||||
["docker", "info"], # noqa: S607
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
@@ -728,13 +732,14 @@ class Agent(BaseAgent):
|
||||
def set_fingerprint(self, fingerprint: Fingerprint):
|
||||
self.security_config.fingerprint = fingerprint
|
||||
|
||||
def _get_knowledge_search_query(self, task_prompt: str) -> str | None:
|
||||
def _get_knowledge_search_query(self, task_prompt: str, task: Task) -> str | None:
|
||||
"""Generate a search query for the knowledge base based on the task description."""
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=KnowledgeQueryStartedEvent(
|
||||
task_prompt=task_prompt,
|
||||
agent=self,
|
||||
from_task=task,
|
||||
from_agent=self,
|
||||
),
|
||||
)
|
||||
query = self.i18n.slice("knowledge_search_query").format(
|
||||
@@ -749,8 +754,9 @@ class Agent(BaseAgent):
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=KnowledgeQueryFailedEvent(
|
||||
agent=self,
|
||||
error="LLM is not compatible with knowledge search queries",
|
||||
from_task=task,
|
||||
from_agent=self,
|
||||
),
|
||||
)
|
||||
return None
|
||||
@@ -769,7 +775,8 @@ class Agent(BaseAgent):
|
||||
self,
|
||||
event=KnowledgeQueryCompletedEvent(
|
||||
query=query,
|
||||
agent=self,
|
||||
from_task=task,
|
||||
from_agent=self,
|
||||
),
|
||||
)
|
||||
return rewritten_query
|
||||
@@ -777,15 +784,16 @@ class Agent(BaseAgent):
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=KnowledgeQueryFailedEvent(
|
||||
agent=self,
|
||||
error=str(e),
|
||||
from_task=task,
|
||||
from_agent=self,
|
||||
),
|
||||
)
|
||||
return None
|
||||
|
||||
def kickoff(
|
||||
self,
|
||||
messages: str | list[dict[str, str]],
|
||||
messages: str | list[LLMMessage],
|
||||
response_format: type[Any] | None = None,
|
||||
) -> LiteAgentOutput:
|
||||
"""
|
||||
@@ -825,7 +833,7 @@ class Agent(BaseAgent):
|
||||
|
||||
async def kickoff_async(
|
||||
self,
|
||||
messages: str | list[dict[str, str]],
|
||||
messages: str | list[LLMMessage],
|
||||
response_format: type[Any] | None = None,
|
||||
) -> LiteAgentOutput:
|
||||
"""
|
||||
@@ -855,6 +863,7 @@ class Agent(BaseAgent):
|
||||
response_format=response_format,
|
||||
i18n=self.i18n,
|
||||
original_agent=self,
|
||||
guardrail=self.guardrail,
|
||||
)
|
||||
|
||||
return await lite_agent.kickoff_async(messages)
|
||||
|
||||
@@ -30,6 +30,7 @@ def validate_jwt_token(
|
||||
algorithms=["RS256"],
|
||||
audience=audience,
|
||||
issuer=issuer,
|
||||
leeway=10.0,
|
||||
options={
|
||||
"verify_signature": True,
|
||||
"verify_exp": True,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import os
|
||||
import subprocess
|
||||
from enum import Enum
|
||||
|
||||
import click
|
||||
from packaging import version
|
||||
|
||||
from crewai.cli.utils import read_toml
|
||||
from crewai.cli.utils import build_env_with_tool_repository_credentials, read_toml
|
||||
from crewai.cli.version import get_crewai_version
|
||||
|
||||
|
||||
@@ -55,8 +56,22 @@ def execute_command(crew_type: CrewType) -> None:
|
||||
"""
|
||||
command = ["uv", "run", "kickoff" if crew_type == CrewType.FLOW else "run_crew"]
|
||||
|
||||
env = os.environ.copy()
|
||||
try:
|
||||
subprocess.run(command, capture_output=False, text=True, check=True) # noqa: S603
|
||||
pyproject_data = read_toml()
|
||||
sources = pyproject_data.get("tool", {}).get("uv", {}).get("sources", {})
|
||||
|
||||
for source_config in sources.values():
|
||||
if isinstance(source_config, dict):
|
||||
index = source_config.get("index")
|
||||
if index:
|
||||
index_env = build_env_with_tool_repository_credentials(index)
|
||||
env.update(index_env)
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
try:
|
||||
subprocess.run(command, capture_output=False, text=True, check=True, env=env) # noqa: S603
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
handle_error(e, crew_type)
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.201.1,<1.0.0"
|
||||
"crewai[tools]>=0.203.1,<1.0.0"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.201.1,<1.0.0",
|
||||
"crewai[tools]>=0.203.1,<1.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.201.1"
|
||||
"crewai[tools]>=0.203.1"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -32,6 +32,13 @@ from crewai.events.types.flow_events import (
|
||||
MethodExecutionFinishedEvent,
|
||||
MethodExecutionStartedEvent,
|
||||
)
|
||||
from crewai.events.types.knowledge_events import (
|
||||
KnowledgeQueryCompletedEvent,
|
||||
KnowledgeQueryFailedEvent,
|
||||
KnowledgeQueryStartedEvent,
|
||||
KnowledgeRetrievalCompletedEvent,
|
||||
KnowledgeRetrievalStartedEvent,
|
||||
)
|
||||
from crewai.events.types.llm_events import (
|
||||
LLMCallCompletedEvent,
|
||||
LLMCallFailedEvent,
|
||||
@@ -310,6 +317,26 @@ class TraceCollectionListener(BaseEventListener):
|
||||
def on_agent_reasoning_failed(source, event):
|
||||
self._handle_action_event("agent_reasoning_failed", source, event)
|
||||
|
||||
@event_bus.on(KnowledgeRetrievalStartedEvent)
|
||||
def on_knowledge_retrieval_started(source, event):
|
||||
self._handle_action_event("knowledge_retrieval_started", source, event)
|
||||
|
||||
@event_bus.on(KnowledgeRetrievalCompletedEvent)
|
||||
def on_knowledge_retrieval_completed(source, event):
|
||||
self._handle_action_event("knowledge_retrieval_completed", source, event)
|
||||
|
||||
@event_bus.on(KnowledgeQueryStartedEvent)
|
||||
def on_knowledge_query_started(source, event):
|
||||
self._handle_action_event("knowledge_query_started", source, event)
|
||||
|
||||
@event_bus.on(KnowledgeQueryCompletedEvent)
|
||||
def on_knowledge_query_completed(source, event):
|
||||
self._handle_action_event("knowledge_query_completed", source, event)
|
||||
|
||||
@event_bus.on(KnowledgeQueryFailedEvent)
|
||||
def on_knowledge_query_failed(source, event):
|
||||
self._handle_action_event("knowledge_query_failed", source, event)
|
||||
|
||||
def _initialize_crew_batch(self, source: Any, event: Any):
|
||||
"""Initialize trace batch"""
|
||||
user_context = self._get_user_context()
|
||||
|
||||
@@ -358,7 +358,8 @@ def prompt_user_for_trace_viewing(timeout_seconds: int = 20) -> bool:
|
||||
try:
|
||||
response = input().strip().lower()
|
||||
result[0] = response in ["y", "yes"]
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
except (EOFError, KeyboardInterrupt, OSError, LookupError):
|
||||
# Handle all input-related errors silently
|
||||
result[0] = False
|
||||
|
||||
input_thread = threading.Thread(target=get_input, daemon=True)
|
||||
@@ -371,6 +372,7 @@ def prompt_user_for_trace_viewing(timeout_seconds: int = 20) -> bool:
|
||||
return result[0]
|
||||
|
||||
except Exception:
|
||||
# Suppress any warnings or errors and assume "no"
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -1,51 +1,60 @@
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from typing import Any
|
||||
|
||||
from crewai.events.base_events import BaseEvent
|
||||
|
||||
|
||||
class KnowledgeRetrievalStartedEvent(BaseEvent):
|
||||
class KnowledgeEventBase(BaseEvent):
|
||||
task_id: str | None = None
|
||||
task_name: str | None = None
|
||||
from_task: Any | None = None
|
||||
from_agent: Any | None = None
|
||||
agent_role: str | None = None
|
||||
agent_id: str | None = None
|
||||
|
||||
def __init__(self, **data):
|
||||
super().__init__(**data)
|
||||
self._set_agent_params(data)
|
||||
self._set_task_params(data)
|
||||
|
||||
|
||||
class KnowledgeRetrievalStartedEvent(KnowledgeEventBase):
|
||||
"""Event emitted when a knowledge retrieval is started."""
|
||||
|
||||
type: str = "knowledge_search_query_started"
|
||||
agent: BaseAgent
|
||||
|
||||
|
||||
class KnowledgeRetrievalCompletedEvent(BaseEvent):
|
||||
class KnowledgeRetrievalCompletedEvent(KnowledgeEventBase):
|
||||
"""Event emitted when a knowledge retrieval is completed."""
|
||||
|
||||
query: str
|
||||
type: str = "knowledge_search_query_completed"
|
||||
agent: BaseAgent
|
||||
retrieved_knowledge: str
|
||||
|
||||
|
||||
class KnowledgeQueryStartedEvent(BaseEvent):
|
||||
class KnowledgeQueryStartedEvent(KnowledgeEventBase):
|
||||
"""Event emitted when a knowledge query is started."""
|
||||
|
||||
task_prompt: str
|
||||
type: str = "knowledge_query_started"
|
||||
agent: BaseAgent
|
||||
|
||||
|
||||
class KnowledgeQueryFailedEvent(BaseEvent):
|
||||
class KnowledgeQueryFailedEvent(KnowledgeEventBase):
|
||||
"""Event emitted when a knowledge query fails."""
|
||||
|
||||
type: str = "knowledge_query_failed"
|
||||
agent: BaseAgent
|
||||
error: str
|
||||
|
||||
|
||||
class KnowledgeQueryCompletedEvent(BaseEvent):
|
||||
class KnowledgeQueryCompletedEvent(KnowledgeEventBase):
|
||||
"""Event emitted when a knowledge query is completed."""
|
||||
|
||||
query: str
|
||||
type: str = "knowledge_query_completed"
|
||||
agent: BaseAgent
|
||||
|
||||
|
||||
class KnowledgeSearchQueryFailedEvent(BaseEvent):
|
||||
class KnowledgeSearchQueryFailedEvent(KnowledgeEventBase):
|
||||
"""Event emitted when a knowledge search query fails."""
|
||||
|
||||
query: str
|
||||
type: str = "knowledge_search_query_failed"
|
||||
agent: BaseAgent
|
||||
error: str
|
||||
|
||||
@@ -5,7 +5,21 @@ from typing import Any
|
||||
from crewai.events.base_events import BaseEvent
|
||||
|
||||
|
||||
class LLMGuardrailStartedEvent(BaseEvent):
|
||||
class LLMGuardrailBaseEvent(BaseEvent):
|
||||
task_id: str | None = None
|
||||
task_name: str | None = None
|
||||
from_task: Any | None = None
|
||||
from_agent: Any | None = None
|
||||
agent_role: str | None = None
|
||||
agent_id: str | None = None
|
||||
|
||||
def __init__(self, **data):
|
||||
super().__init__(**data)
|
||||
self._set_agent_params(data)
|
||||
self._set_task_params(data)
|
||||
|
||||
|
||||
class LLMGuardrailStartedEvent(LLMGuardrailBaseEvent):
|
||||
"""Event emitted when a guardrail task starts
|
||||
|
||||
Attributes:
|
||||
@@ -29,7 +43,7 @@ class LLMGuardrailStartedEvent(BaseEvent):
|
||||
self.guardrail = getsource(self.guardrail).strip()
|
||||
|
||||
|
||||
class LLMGuardrailCompletedEvent(BaseEvent):
|
||||
class LLMGuardrailCompletedEvent(LLMGuardrailBaseEvent):
|
||||
"""Event emitted when a guardrail task completes
|
||||
|
||||
Attributes:
|
||||
@@ -44,3 +58,16 @@ class LLMGuardrailCompletedEvent(BaseEvent):
|
||||
result: Any
|
||||
error: str | None = None
|
||||
retry_count: int
|
||||
|
||||
|
||||
class LLMGuardrailFailedEvent(LLMGuardrailBaseEvent):
|
||||
"""Event emitted when a guardrail task fails
|
||||
|
||||
Attributes:
|
||||
error: The error message
|
||||
retry_count: The number of times the guardrail has been retried
|
||||
"""
|
||||
|
||||
type: str = "llm_guardrail_failed"
|
||||
error: str
|
||||
retry_count: int
|
||||
|
||||
@@ -1377,7 +1377,7 @@ class ConsoleFormatter:
|
||||
if isinstance(formatted_answer, AgentAction):
|
||||
thought = re.sub(r"\n+", "\n", formatted_answer.thought)
|
||||
formatted_json = json.dumps(
|
||||
formatted_answer.tool_input,
|
||||
json.loads(formatted_answer.tool_input),
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import uuid
|
||||
from collections.abc import Callable
|
||||
from typing import (
|
||||
Any,
|
||||
Literal,
|
||||
cast,
|
||||
get_args,
|
||||
get_origin,
|
||||
@@ -62,6 +63,7 @@ from crewai.utilities.llm_utils import create_llm
|
||||
from crewai.utilities.printer import Printer
|
||||
from crewai.utilities.token_counter_callback import TokenCalcHandler
|
||||
from crewai.utilities.tool_utils import execute_tool_and_check_finality
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
class LiteAgentOutput(BaseModel):
|
||||
@@ -180,7 +182,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
_token_process: TokenProcess = PrivateAttr(default_factory=TokenProcess)
|
||||
_cache_handler: CacheHandler = PrivateAttr(default_factory=CacheHandler)
|
||||
_key: str = PrivateAttr(default_factory=lambda: str(uuid.uuid4()))
|
||||
_messages: list[dict[str, str]] = PrivateAttr(default_factory=list)
|
||||
_messages: list[LLMMessage] = PrivateAttr(default_factory=list)
|
||||
_iterations: int = PrivateAttr(default=0)
|
||||
_printer: Printer = PrivateAttr(default_factory=Printer)
|
||||
_guardrail: Callable | None = PrivateAttr(default=None)
|
||||
@@ -219,7 +221,6 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
raise TypeError(
|
||||
f"Guardrail requires LLM instance of type BaseLLM, got {type(self.llm).__name__}"
|
||||
)
|
||||
|
||||
self._guardrail = LLMGuardrail(description=self.guardrail, llm=self.llm)
|
||||
|
||||
return self
|
||||
@@ -276,7 +277,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
"""Return the original role for compatibility with tool interfaces."""
|
||||
return self.role
|
||||
|
||||
def kickoff(self, messages: str | list[dict[str, str]]) -> LiteAgentOutput:
|
||||
def kickoff(self, messages: str | list[LLMMessage]) -> LiteAgentOutput:
|
||||
"""
|
||||
Execute the agent with the given messages.
|
||||
|
||||
@@ -368,6 +369,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
guardrail=self._guardrail,
|
||||
retry_count=self._guardrail_retry_count,
|
||||
event_source=self,
|
||||
from_agent=self,
|
||||
)
|
||||
|
||||
if not guardrail_result.success:
|
||||
@@ -414,9 +416,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
|
||||
return output
|
||||
|
||||
async def kickoff_async(
|
||||
self, messages: str | list[dict[str, str]]
|
||||
) -> LiteAgentOutput:
|
||||
async def kickoff_async(self, messages: str | list[LLMMessage]) -> LiteAgentOutput:
|
||||
"""
|
||||
Execute the agent asynchronously with the given messages.
|
||||
|
||||
@@ -461,9 +461,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
|
||||
return base_prompt
|
||||
|
||||
def _format_messages(
|
||||
self, messages: str | list[dict[str, str]]
|
||||
) -> list[dict[str, str]]:
|
||||
def _format_messages(self, messages: str | list[LLMMessage]) -> list[LLMMessage]:
|
||||
"""Format messages for the LLM."""
|
||||
if isinstance(messages, str):
|
||||
messages = [{"role": "user", "content": messages}]
|
||||
@@ -471,7 +469,9 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
system_prompt = self._get_default_system_prompt()
|
||||
|
||||
# Add system message at the beginning
|
||||
formatted_messages = [{"role": "system", "content": system_prompt}]
|
||||
formatted_messages: list[LLMMessage] = [
|
||||
{"role": "system", "content": system_prompt}
|
||||
]
|
||||
|
||||
# Add the rest of the messages
|
||||
formatted_messages.extend(messages)
|
||||
@@ -583,6 +583,8 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
),
|
||||
)
|
||||
|
||||
def _append_message(self, text: str, role: str = "assistant") -> None:
|
||||
def _append_message(
|
||||
self, text: str, role: Literal["user", "assistant", "system"] = "assistant"
|
||||
) -> None:
|
||||
"""Append a message to the message list with the given role."""
|
||||
self._messages.append(format_message_for_llm(text, role=role))
|
||||
self._messages.append(cast(LLMMessage, format_message_for_llm(text, role=role)))
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from .annotations import (
|
||||
"""Project package for CrewAI."""
|
||||
|
||||
from crewai.project.annotations import (
|
||||
after_kickoff,
|
||||
agent,
|
||||
before_kickoff,
|
||||
@@ -11,19 +13,19 @@ from .annotations import (
|
||||
task,
|
||||
tool,
|
||||
)
|
||||
from .crew_base import CrewBase
|
||||
from crewai.project.crew_base import CrewBase
|
||||
|
||||
__all__ = [
|
||||
"CrewBase",
|
||||
"after_kickoff",
|
||||
"agent",
|
||||
"before_kickoff",
|
||||
"cache_handler",
|
||||
"callback",
|
||||
"crew",
|
||||
"task",
|
||||
"llm",
|
||||
"output_json",
|
||||
"output_pydantic",
|
||||
"task",
|
||||
"tool",
|
||||
"callback",
|
||||
"CrewBase",
|
||||
"llm",
|
||||
"cache_handler",
|
||||
"before_kickoff",
|
||||
"after_kickoff",
|
||||
]
|
||||
|
||||
@@ -1,97 +1,192 @@
|
||||
from functools import wraps
|
||||
from typing import Callable
|
||||
|
||||
from crewai import Crew
|
||||
from crewai.project.utils import memoize
|
||||
|
||||
"""Decorators for defining crew components and their behaviors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
def before_kickoff(func):
|
||||
"""Marks a method to execute before crew kickoff."""
|
||||
func.is_before_kickoff = True
|
||||
return func
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Concatenate, ParamSpec, TypeVar
|
||||
|
||||
from crewai.project.utils import memoize
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai import Agent, Crew, Task
|
||||
|
||||
from crewai.project.wrappers import (
|
||||
AfterKickoffMethod,
|
||||
AgentMethod,
|
||||
BeforeKickoffMethod,
|
||||
CacheHandlerMethod,
|
||||
CallbackMethod,
|
||||
CrewInstance,
|
||||
LLMMethod,
|
||||
OutputJsonClass,
|
||||
OutputPydanticClass,
|
||||
TaskMethod,
|
||||
TaskResultT,
|
||||
ToolMethod,
|
||||
)
|
||||
|
||||
P = ParamSpec("P")
|
||||
P2 = ParamSpec("P2")
|
||||
R = TypeVar("R")
|
||||
R2 = TypeVar("R2")
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def after_kickoff(func):
|
||||
"""Marks a method to execute after crew kickoff."""
|
||||
func.is_after_kickoff = True
|
||||
return func
|
||||
def before_kickoff(meth: Callable[P, R]) -> BeforeKickoffMethod[P, R]:
|
||||
"""Marks a method to execute before crew kickoff.
|
||||
|
||||
Args:
|
||||
meth: The method to mark.
|
||||
|
||||
Returns:
|
||||
A wrapped method marked for before kickoff execution.
|
||||
"""
|
||||
return BeforeKickoffMethod(meth)
|
||||
|
||||
|
||||
def task(func):
|
||||
"""Marks a method as a crew task."""
|
||||
func.is_task = True
|
||||
def after_kickoff(meth: Callable[P, R]) -> AfterKickoffMethod[P, R]:
|
||||
"""Marks a method to execute after crew kickoff.
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
result = func(*args, **kwargs)
|
||||
if not result.name:
|
||||
result.name = func.__name__
|
||||
return result
|
||||
Args:
|
||||
meth: The method to mark.
|
||||
|
||||
return memoize(wrapper)
|
||||
Returns:
|
||||
A wrapped method marked for after kickoff execution.
|
||||
"""
|
||||
return AfterKickoffMethod(meth)
|
||||
|
||||
|
||||
def agent(func):
|
||||
"""Marks a method as a crew agent."""
|
||||
func.is_agent = True
|
||||
func = memoize(func)
|
||||
return func
|
||||
def task(meth: Callable[P, TaskResultT]) -> TaskMethod[P, TaskResultT]:
|
||||
"""Marks a method as a crew task.
|
||||
|
||||
Args:
|
||||
meth: The method to mark.
|
||||
|
||||
Returns:
|
||||
A wrapped method marked as a task with memoization.
|
||||
"""
|
||||
return TaskMethod(memoize(meth))
|
||||
|
||||
|
||||
def llm(func):
|
||||
"""Marks a method as an LLM provider."""
|
||||
func.is_llm = True
|
||||
func = memoize(func)
|
||||
return func
|
||||
def agent(meth: Callable[P, R]) -> AgentMethod[P, R]:
|
||||
"""Marks a method as a crew agent.
|
||||
|
||||
Args:
|
||||
meth: The method to mark.
|
||||
|
||||
Returns:
|
||||
A wrapped method marked as an agent with memoization.
|
||||
"""
|
||||
return AgentMethod(memoize(meth))
|
||||
|
||||
|
||||
def output_json(cls):
|
||||
"""Marks a class as JSON output format."""
|
||||
cls.is_output_json = True
|
||||
return cls
|
||||
def llm(meth: Callable[P, R]) -> LLMMethod[P, R]:
|
||||
"""Marks a method as an LLM provider.
|
||||
|
||||
Args:
|
||||
meth: The method to mark.
|
||||
|
||||
Returns:
|
||||
A wrapped method marked as an LLM provider with memoization.
|
||||
"""
|
||||
return LLMMethod(memoize(meth))
|
||||
|
||||
|
||||
def output_pydantic(cls):
|
||||
"""Marks a class as Pydantic output format."""
|
||||
cls.is_output_pydantic = True
|
||||
return cls
|
||||
def output_json(cls: type[T]) -> OutputJsonClass[T]:
|
||||
"""Marks a class as JSON output format.
|
||||
|
||||
Args:
|
||||
cls: The class to mark.
|
||||
|
||||
Returns:
|
||||
A wrapped class marked as JSON output format.
|
||||
"""
|
||||
return OutputJsonClass(cls)
|
||||
|
||||
|
||||
def tool(func):
|
||||
"""Marks a method as a crew tool."""
|
||||
func.is_tool = True
|
||||
return memoize(func)
|
||||
def output_pydantic(cls: type[T]) -> OutputPydanticClass[T]:
|
||||
"""Marks a class as Pydantic output format.
|
||||
|
||||
Args:
|
||||
cls: The class to mark.
|
||||
|
||||
Returns:
|
||||
A wrapped class marked as Pydantic output format.
|
||||
"""
|
||||
return OutputPydanticClass(cls)
|
||||
|
||||
|
||||
def callback(func):
|
||||
"""Marks a method as a crew callback."""
|
||||
func.is_callback = True
|
||||
return memoize(func)
|
||||
def tool(meth: Callable[P, R]) -> ToolMethod[P, R]:
|
||||
"""Marks a method as a crew tool.
|
||||
|
||||
Args:
|
||||
meth: The method to mark.
|
||||
|
||||
Returns:
|
||||
A wrapped method marked as a tool with memoization.
|
||||
"""
|
||||
return ToolMethod(memoize(meth))
|
||||
|
||||
|
||||
def cache_handler(func):
|
||||
"""Marks a method as a cache handler."""
|
||||
func.is_cache_handler = True
|
||||
return memoize(func)
|
||||
def callback(meth: Callable[P, R]) -> CallbackMethod[P, R]:
|
||||
"""Marks a method as a crew callback.
|
||||
|
||||
Args:
|
||||
meth: The method to mark.
|
||||
|
||||
Returns:
|
||||
A wrapped method marked as a callback with memoization.
|
||||
"""
|
||||
return CallbackMethod(memoize(meth))
|
||||
|
||||
|
||||
def crew(func) -> Callable[..., Crew]:
|
||||
"""Marks a method as the main crew execution point."""
|
||||
def cache_handler(meth: Callable[P, R]) -> CacheHandlerMethod[P, R]:
|
||||
"""Marks a method as a cache handler.
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs) -> Crew:
|
||||
instantiated_tasks = []
|
||||
instantiated_agents = []
|
||||
agent_roles = set()
|
||||
Args:
|
||||
meth: The method to mark.
|
||||
|
||||
Returns:
|
||||
A wrapped method marked as a cache handler with memoization.
|
||||
"""
|
||||
return CacheHandlerMethod(memoize(meth))
|
||||
|
||||
|
||||
def crew(
|
||||
meth: Callable[Concatenate[CrewInstance, P], Crew],
|
||||
) -> Callable[Concatenate[CrewInstance, P], Crew]:
|
||||
"""Marks a method as the main crew execution point.
|
||||
|
||||
Args:
|
||||
meth: The method to mark as crew execution point.
|
||||
|
||||
Returns:
|
||||
A wrapped method that instantiates tasks and agents before execution.
|
||||
"""
|
||||
|
||||
@wraps(meth)
|
||||
def wrapper(self: CrewInstance, *args: P.args, **kwargs: P.kwargs) -> Crew:
|
||||
"""Wrapper that sets up crew before calling the decorated method.
|
||||
|
||||
Args:
|
||||
self: The crew class instance.
|
||||
*args: Additional positional arguments.
|
||||
**kwargs: Keyword arguments to pass to the method.
|
||||
|
||||
Returns:
|
||||
The configured Crew instance with callbacks attached.
|
||||
"""
|
||||
instantiated_tasks: list[Task] = []
|
||||
instantiated_agents: list[Agent] = []
|
||||
agent_roles: set[str] = set()
|
||||
|
||||
# Use the preserved task and agent information
|
||||
tasks = self._original_tasks.items()
|
||||
agents = self._original_agents.items()
|
||||
tasks = self.__crew_metadata__["original_tasks"].items()
|
||||
agents = self.__crew_metadata__["original_agents"].items()
|
||||
|
||||
# Instantiate tasks in order
|
||||
for task_name, task_method in tasks:
|
||||
for _, task_method in tasks:
|
||||
task_instance = task_method(self)
|
||||
instantiated_tasks.append(task_instance)
|
||||
agent_instance = getattr(task_instance, "agent", None)
|
||||
@@ -100,7 +195,7 @@ def crew(func) -> Callable[..., Crew]:
|
||||
agent_roles.add(agent_instance.role)
|
||||
|
||||
# Instantiate agents not included by tasks
|
||||
for agent_name, agent_method in agents:
|
||||
for _, agent_method in agents:
|
||||
agent_instance = agent_method(self)
|
||||
if agent_instance.role not in agent_roles:
|
||||
instantiated_agents.append(agent_instance)
|
||||
@@ -109,19 +204,44 @@ def crew(func) -> Callable[..., Crew]:
|
||||
self.agents = instantiated_agents
|
||||
self.tasks = instantiated_tasks
|
||||
|
||||
crew = func(self, *args, **kwargs)
|
||||
crew_instance = meth(self, *args, **kwargs)
|
||||
|
||||
def callback_wrapper(callback, instance):
|
||||
def wrapper(*args, **kwargs):
|
||||
return callback(instance, *args, **kwargs)
|
||||
def callback_wrapper(
|
||||
hook: Callable[Concatenate[CrewInstance, P2], R2], instance: CrewInstance
|
||||
) -> Callable[P2, R2]:
|
||||
"""Bind a hook callback to an instance.
|
||||
|
||||
return wrapper
|
||||
Args:
|
||||
hook: The callback hook to bind.
|
||||
instance: The instance to bind to.
|
||||
|
||||
for _, callback in self._before_kickoff.items():
|
||||
crew.before_kickoff_callbacks.append(callback_wrapper(callback, self))
|
||||
for _, callback in self._after_kickoff.items():
|
||||
crew.after_kickoff_callbacks.append(callback_wrapper(callback, self))
|
||||
Returns:
|
||||
A bound callback function.
|
||||
"""
|
||||
|
||||
return crew
|
||||
def bound_callback(*cb_args: P2.args, **cb_kwargs: P2.kwargs) -> R2:
|
||||
"""Execute the bound callback.
|
||||
|
||||
Args:
|
||||
*cb_args: Positional arguments for the callback.
|
||||
**cb_kwargs: Keyword arguments for the callback.
|
||||
|
||||
Returns:
|
||||
The result of the callback execution.
|
||||
"""
|
||||
return hook(instance, *cb_args, **cb_kwargs)
|
||||
|
||||
return bound_callback
|
||||
|
||||
for hook_callback in self.__crew_metadata__["before_kickoff"].values():
|
||||
crew_instance.before_kickoff_callbacks.append(
|
||||
callback_wrapper(hook_callback, self)
|
||||
)
|
||||
for hook_callback in self.__crew_metadata__["after_kickoff"].values():
|
||||
crew_instance.after_kickoff_callbacks.append(
|
||||
callback_wrapper(hook_callback, self)
|
||||
)
|
||||
|
||||
return crew_instance
|
||||
|
||||
return memoize(wrapper)
|
||||
|
||||
@@ -1,298 +1,631 @@
|
||||
"""Base metaclass for creating crew classes with configuration and method management."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeVar, cast
|
||||
from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeGuard, TypeVar, cast
|
||||
|
||||
import yaml
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from crewai.project.wrappers import CrewClass, CrewMetadata
|
||||
from crewai.tools import BaseTool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai import Agent, Task
|
||||
from crewai.agents.cache.cache_handler import CacheHandler
|
||||
from crewai.crews.crew_output import CrewOutput
|
||||
from crewai.project.wrappers import (
|
||||
CrewInstance,
|
||||
OutputJsonClass,
|
||||
OutputPydanticClass,
|
||||
)
|
||||
from crewai.tasks.task_output import TaskOutput
|
||||
|
||||
|
||||
class AgentConfig(TypedDict, total=False):
|
||||
"""Type definition for agent configuration dictionary.
|
||||
|
||||
All fields are optional as they come from YAML configuration files.
|
||||
Fields can be either string references (from YAML) or actual instances (after processing).
|
||||
"""
|
||||
|
||||
# Core agent attributes (from BaseAgent)
|
||||
role: str
|
||||
goal: str
|
||||
backstory: str
|
||||
cache: bool
|
||||
verbose: bool
|
||||
max_rpm: int
|
||||
allow_delegation: bool
|
||||
max_iter: int
|
||||
max_tokens: int
|
||||
callbacks: list[str]
|
||||
|
||||
# LLM configuration
|
||||
llm: str
|
||||
function_calling_llm: str
|
||||
use_system_prompt: bool
|
||||
|
||||
# Template configuration
|
||||
system_template: str
|
||||
prompt_template: str
|
||||
response_template: str
|
||||
|
||||
# Tools and handlers (can be string references or instances)
|
||||
tools: list[str] | list[BaseTool]
|
||||
step_callback: str
|
||||
cache_handler: str | CacheHandler
|
||||
|
||||
# Code execution
|
||||
allow_code_execution: bool
|
||||
code_execution_mode: Literal["safe", "unsafe"]
|
||||
|
||||
# Context and performance
|
||||
respect_context_window: bool
|
||||
max_retry_limit: int
|
||||
|
||||
# Multimodal and reasoning
|
||||
multimodal: bool
|
||||
reasoning: bool
|
||||
max_reasoning_attempts: int
|
||||
|
||||
# Knowledge configuration
|
||||
knowledge_sources: list[str] | list[Any]
|
||||
knowledge_storage: str | Any
|
||||
knowledge_config: dict[str, Any]
|
||||
embedder: dict[str, Any]
|
||||
agent_knowledge_context: str
|
||||
crew_knowledge_context: str
|
||||
knowledge_search_query: str
|
||||
|
||||
# Misc configuration
|
||||
inject_date: bool
|
||||
date_format: str
|
||||
from_repository: str
|
||||
guardrail: Callable[[Any], tuple[bool, Any]] | str
|
||||
guardrail_max_retries: int
|
||||
|
||||
|
||||
class TaskConfig(TypedDict, total=False):
|
||||
"""Type definition for task configuration dictionary.
|
||||
|
||||
All fields are optional as they come from YAML configuration files.
|
||||
Fields can be either string references (from YAML) or actual instances (after processing).
|
||||
"""
|
||||
|
||||
# Core task attributes
|
||||
name: str
|
||||
description: str
|
||||
expected_output: str
|
||||
|
||||
# Agent and context
|
||||
agent: str
|
||||
context: list[str]
|
||||
|
||||
# Tools and callbacks (can be string references or instances)
|
||||
tools: list[str] | list[BaseTool]
|
||||
callback: str
|
||||
callbacks: list[str]
|
||||
|
||||
# Output configuration
|
||||
output_json: str
|
||||
output_pydantic: str
|
||||
output_file: str
|
||||
create_directory: bool
|
||||
|
||||
# Execution configuration
|
||||
async_execution: bool
|
||||
human_input: bool
|
||||
markdown: bool
|
||||
|
||||
# Guardrail configuration
|
||||
guardrail: Callable[[TaskOutput], tuple[bool, Any]] | str
|
||||
guardrail_max_retries: int
|
||||
|
||||
# Misc configuration
|
||||
allow_crewai_trigger_context: bool
|
||||
|
||||
|
||||
load_dotenv()
|
||||
|
||||
T = TypeVar("T", bound=type)
|
||||
|
||||
"""Base decorator for creating crew classes with configuration and function management."""
|
||||
CallableT = TypeVar("CallableT", bound=Callable[..., Any])
|
||||
|
||||
|
||||
def CrewBase(cls: T) -> T: # noqa: N802
|
||||
"""Wraps a class with crew functionality and configuration management."""
|
||||
def _set_base_directory(cls: type[CrewClass]) -> None:
|
||||
"""Set the base directory for the crew class.
|
||||
|
||||
class WrappedClass(cls): # type: ignore
|
||||
is_crew_class: bool = True # type: ignore
|
||||
Args:
|
||||
cls: Crew class to configure.
|
||||
"""
|
||||
try:
|
||||
cls.base_directory = Path(inspect.getfile(cls)).parent
|
||||
except (TypeError, OSError):
|
||||
cls.base_directory = Path.cwd()
|
||||
|
||||
# Get the directory of the class being decorated
|
||||
base_directory = Path(inspect.getfile(cls)).parent
|
||||
|
||||
original_agents_config_path = getattr(
|
||||
cls, "agents_config", "config/agents.yaml"
|
||||
def _set_config_paths(cls: type[CrewClass]) -> None:
|
||||
"""Set the configuration file paths for the crew class.
|
||||
|
||||
Args:
|
||||
cls: Crew class to configure.
|
||||
"""
|
||||
cls.original_agents_config_path = getattr(
|
||||
cls, "agents_config", "config/agents.yaml"
|
||||
)
|
||||
cls.original_tasks_config_path = getattr(cls, "tasks_config", "config/tasks.yaml")
|
||||
|
||||
|
||||
def _set_mcp_params(cls: type[CrewClass]) -> None:
|
||||
"""Set the MCP server parameters for the crew class.
|
||||
|
||||
Args:
|
||||
cls: Crew class to configure.
|
||||
"""
|
||||
cls.mcp_server_params = getattr(cls, "mcp_server_params", None)
|
||||
cls.mcp_connect_timeout = getattr(cls, "mcp_connect_timeout", 30)
|
||||
|
||||
|
||||
def _is_string_list(value: list[str] | list[BaseTool]) -> TypeGuard[list[str]]:
|
||||
"""Type guard to check if list contains strings rather than BaseTool instances.
|
||||
|
||||
Args:
|
||||
value: List that may contain strings or BaseTool instances.
|
||||
|
||||
Returns:
|
||||
True if all elements are strings, False otherwise.
|
||||
"""
|
||||
return all(isinstance(item, str) for item in value)
|
||||
|
||||
|
||||
def _is_string_value(value: str | CacheHandler) -> TypeGuard[str]:
|
||||
"""Type guard to check if value is a string rather than a CacheHandler instance.
|
||||
|
||||
Args:
|
||||
value: Value that may be a string or CacheHandler instance.
|
||||
|
||||
Returns:
|
||||
True if value is a string, False otherwise.
|
||||
"""
|
||||
return isinstance(value, str)
|
||||
|
||||
|
||||
class CrewBaseMeta(type):
|
||||
"""Metaclass that adds crew functionality to classes."""
|
||||
|
||||
def __new__(
|
||||
mcs,
|
||||
name: str,
|
||||
bases: tuple[type, ...],
|
||||
namespace: dict[str, Any],
|
||||
**kwargs: Any,
|
||||
) -> type[CrewClass]:
|
||||
"""Create crew class with configuration and method injection.
|
||||
|
||||
Args:
|
||||
name: Class name.
|
||||
bases: Base classes.
|
||||
namespace: Class namespace dictionary.
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
New crew class with injected methods and attributes.
|
||||
"""
|
||||
cls = cast(
|
||||
type[CrewClass], cast(object, super().__new__(mcs, name, bases, namespace))
|
||||
)
|
||||
original_tasks_config_path = getattr(cls, "tasks_config", "config/tasks.yaml")
|
||||
|
||||
mcp_server_params: Any = getattr(cls, "mcp_server_params", None)
|
||||
mcp_connect_timeout: int = getattr(cls, "mcp_connect_timeout", 30)
|
||||
cls.is_crew_class = True
|
||||
cls._crew_name = name
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.load_configurations()
|
||||
self.map_all_agent_variables()
|
||||
self.map_all_task_variables()
|
||||
# Preserve all decorated functions
|
||||
self._original_functions = {
|
||||
name: method
|
||||
for name, method in cls.__dict__.items()
|
||||
if any(
|
||||
hasattr(method, attr)
|
||||
for attr in [
|
||||
"is_task",
|
||||
"is_agent",
|
||||
"is_before_kickoff",
|
||||
"is_after_kickoff",
|
||||
"is_kickoff",
|
||||
]
|
||||
)
|
||||
}
|
||||
# Store specific function types
|
||||
self._original_tasks = self._filter_functions(
|
||||
self._original_functions, "is_task"
|
||||
)
|
||||
self._original_agents = self._filter_functions(
|
||||
self._original_functions, "is_agent"
|
||||
)
|
||||
self._before_kickoff = self._filter_functions(
|
||||
self._original_functions, "is_before_kickoff"
|
||||
)
|
||||
self._after_kickoff = self._filter_functions(
|
||||
self._original_functions, "is_after_kickoff"
|
||||
)
|
||||
self._kickoff = self._filter_functions(
|
||||
self._original_functions, "is_kickoff"
|
||||
)
|
||||
for setup_fn in _CLASS_SETUP_FUNCTIONS:
|
||||
setup_fn(cls)
|
||||
|
||||
# Add close mcp server method to after kickoff
|
||||
bound_method = self._create_close_mcp_server_method()
|
||||
self._after_kickoff['_close_mcp_server'] = bound_method
|
||||
for method in _METHODS_TO_INJECT:
|
||||
setattr(cls, method.__name__, method)
|
||||
|
||||
def _create_close_mcp_server_method(self):
|
||||
def _close_mcp_server(self, instance, outputs):
|
||||
adapter = getattr(self, '_mcp_server_adapter', None)
|
||||
if adapter is not None:
|
||||
try:
|
||||
adapter.stop()
|
||||
except Exception as e:
|
||||
logging.warning(f"Error stopping MCP server: {e}")
|
||||
return outputs
|
||||
return cls
|
||||
|
||||
_close_mcp_server.is_after_kickoff = True
|
||||
def __call__(cls, *args: Any, **kwargs: Any) -> CrewInstance:
|
||||
"""Intercept instance creation to initialize crew functionality.
|
||||
|
||||
import types
|
||||
return types.MethodType(_close_mcp_server, self)
|
||||
Args:
|
||||
*args: Positional arguments for instance creation.
|
||||
**kwargs: Keyword arguments for instance creation.
|
||||
|
||||
def get_mcp_tools(self, *tool_names: list[str]) -> list[BaseTool]:
|
||||
if not self.mcp_server_params:
|
||||
return []
|
||||
Returns:
|
||||
Initialized crew instance.
|
||||
"""
|
||||
instance: CrewInstance = super().__call__(*args, **kwargs)
|
||||
CrewBaseMeta._initialize_crew_instance(instance, cls)
|
||||
return instance
|
||||
|
||||
from crewai_tools import MCPServerAdapter # type: ignore[import-untyped]
|
||||
@staticmethod
|
||||
def _initialize_crew_instance(instance: CrewInstance, cls: type) -> None:
|
||||
"""Initialize crew instance attributes and load configurations.
|
||||
|
||||
adapter = getattr(self, '_mcp_server_adapter', None)
|
||||
if not adapter:
|
||||
self._mcp_server_adapter = MCPServerAdapter(
|
||||
self.mcp_server_params,
|
||||
connect_timeout=self.mcp_connect_timeout
|
||||
)
|
||||
Args:
|
||||
instance: Crew instance to initialize.
|
||||
cls: Crew class type.
|
||||
"""
|
||||
instance._mcp_server_adapter = None
|
||||
instance.load_configurations()
|
||||
instance._all_methods = _get_all_methods(instance)
|
||||
instance.map_all_agent_variables()
|
||||
instance.map_all_task_variables()
|
||||
|
||||
return self._mcp_server_adapter.tools.filter_by_names(tool_names or None)
|
||||
|
||||
|
||||
def load_configurations(self):
|
||||
"""Load agent and task configurations from YAML files."""
|
||||
if isinstance(self.original_agents_config_path, str):
|
||||
agents_config_path = (
|
||||
self.base_directory / self.original_agents_config_path
|
||||
)
|
||||
try:
|
||||
self.agents_config = self.load_yaml(agents_config_path)
|
||||
except FileNotFoundError:
|
||||
logging.warning(
|
||||
f"Agent config file not found at {agents_config_path}. "
|
||||
"Proceeding with empty agent configurations."
|
||||
)
|
||||
self.agents_config = {}
|
||||
else:
|
||||
logging.warning(
|
||||
"No agent configuration path provided. Proceeding with empty agent configurations."
|
||||
)
|
||||
self.agents_config = {}
|
||||
|
||||
if isinstance(self.original_tasks_config_path, str):
|
||||
tasks_config_path = (
|
||||
self.base_directory / self.original_tasks_config_path
|
||||
)
|
||||
try:
|
||||
self.tasks_config = self.load_yaml(tasks_config_path)
|
||||
except FileNotFoundError:
|
||||
logging.warning(
|
||||
f"Task config file not found at {tasks_config_path}. "
|
||||
"Proceeding with empty task configurations."
|
||||
)
|
||||
self.tasks_config = {}
|
||||
else:
|
||||
logging.warning(
|
||||
"No task configuration path provided. Proceeding with empty task configurations."
|
||||
)
|
||||
self.tasks_config = {}
|
||||
|
||||
@staticmethod
|
||||
def load_yaml(config_path: Path):
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as file:
|
||||
return yaml.safe_load(file)
|
||||
except FileNotFoundError:
|
||||
print(f"File not found: {config_path}")
|
||||
raise
|
||||
|
||||
def _get_all_functions(self):
|
||||
return {
|
||||
name: getattr(self, name)
|
||||
for name in dir(self)
|
||||
if callable(getattr(self, name))
|
||||
}
|
||||
|
||||
def _filter_functions(
|
||||
self, functions: dict[str, Callable], attribute: str
|
||||
) -> dict[str, Callable]:
|
||||
return {
|
||||
name: func
|
||||
for name, func in functions.items()
|
||||
if hasattr(func, attribute)
|
||||
}
|
||||
|
||||
def map_all_agent_variables(self) -> None:
|
||||
all_functions = self._get_all_functions()
|
||||
llms = self._filter_functions(all_functions, "is_llm")
|
||||
tool_functions = self._filter_functions(all_functions, "is_tool")
|
||||
cache_handler_functions = self._filter_functions(
|
||||
all_functions, "is_cache_handler"
|
||||
)
|
||||
callbacks = self._filter_functions(all_functions, "is_callback")
|
||||
|
||||
for agent_name, agent_info in self.agents_config.items():
|
||||
self._map_agent_variables(
|
||||
agent_name,
|
||||
agent_info,
|
||||
llms,
|
||||
tool_functions,
|
||||
cache_handler_functions,
|
||||
callbacks,
|
||||
)
|
||||
|
||||
def _map_agent_variables(
|
||||
self,
|
||||
agent_name: str,
|
||||
agent_info: dict[str, Any],
|
||||
llms: dict[str, Callable],
|
||||
tool_functions: dict[str, Callable],
|
||||
cache_handler_functions: dict[str, Callable],
|
||||
callbacks: dict[str, Callable],
|
||||
) -> None:
|
||||
if llm := agent_info.get("llm"):
|
||||
try:
|
||||
self.agents_config[agent_name]["llm"] = llms[llm]()
|
||||
except KeyError:
|
||||
self.agents_config[agent_name]["llm"] = llm
|
||||
|
||||
if tools := agent_info.get("tools"):
|
||||
self.agents_config[agent_name]["tools"] = [
|
||||
tool_functions[tool]() for tool in tools
|
||||
original_methods = {
|
||||
name: method
|
||||
for name, method in cls.__dict__.items()
|
||||
if any(
|
||||
hasattr(method, attr)
|
||||
for attr in [
|
||||
"is_task",
|
||||
"is_agent",
|
||||
"is_before_kickoff",
|
||||
"is_after_kickoff",
|
||||
"is_kickoff",
|
||||
]
|
||||
|
||||
if function_calling_llm := agent_info.get("function_calling_llm"):
|
||||
try:
|
||||
self.agents_config[agent_name]["function_calling_llm"] = llms[function_calling_llm]()
|
||||
except KeyError:
|
||||
self.agents_config[agent_name]["function_calling_llm"] = function_calling_llm
|
||||
|
||||
if step_callback := agent_info.get("step_callback"):
|
||||
self.agents_config[agent_name]["step_callback"] = callbacks[
|
||||
step_callback
|
||||
]()
|
||||
|
||||
if cache_handler := agent_info.get("cache_handler"):
|
||||
self.agents_config[agent_name]["cache_handler"] = (
|
||||
cache_handler_functions[cache_handler]()
|
||||
)
|
||||
|
||||
def map_all_task_variables(self) -> None:
|
||||
all_functions = self._get_all_functions()
|
||||
agents = self._filter_functions(all_functions, "is_agent")
|
||||
tasks = self._filter_functions(all_functions, "is_task")
|
||||
output_json_functions = self._filter_functions(
|
||||
all_functions, "is_output_json"
|
||||
)
|
||||
tool_functions = self._filter_functions(all_functions, "is_tool")
|
||||
callback_functions = self._filter_functions(all_functions, "is_callback")
|
||||
output_pydantic_functions = self._filter_functions(
|
||||
all_functions, "is_output_pydantic"
|
||||
}
|
||||
|
||||
after_kickoff_callbacks = _filter_methods(original_methods, "is_after_kickoff")
|
||||
after_kickoff_callbacks["close_mcp_server"] = instance.close_mcp_server
|
||||
|
||||
instance.__crew_metadata__ = CrewMetadata(
|
||||
original_methods=original_methods,
|
||||
original_tasks=_filter_methods(original_methods, "is_task"),
|
||||
original_agents=_filter_methods(original_methods, "is_agent"),
|
||||
before_kickoff=_filter_methods(original_methods, "is_before_kickoff"),
|
||||
after_kickoff=after_kickoff_callbacks,
|
||||
kickoff=_filter_methods(original_methods, "is_kickoff"),
|
||||
)
|
||||
|
||||
|
||||
def close_mcp_server(
|
||||
self: CrewInstance, _instance: CrewInstance, outputs: CrewOutput
|
||||
) -> CrewOutput:
|
||||
"""Stop MCP server adapter and return outputs.
|
||||
|
||||
Args:
|
||||
self: Crew instance with MCP server adapter.
|
||||
_instance: Crew instance (unused, required by callback signature).
|
||||
outputs: Crew execution outputs.
|
||||
|
||||
Returns:
|
||||
Unmodified crew outputs.
|
||||
"""
|
||||
if self._mcp_server_adapter is not None:
|
||||
try:
|
||||
self._mcp_server_adapter.stop()
|
||||
except Exception as e:
|
||||
logging.warning(f"Error stopping MCP server: {e}")
|
||||
return outputs
|
||||
|
||||
|
||||
def get_mcp_tools(self: CrewInstance, *tool_names: str) -> list[BaseTool]:
|
||||
"""Get MCP tools filtered by name.
|
||||
|
||||
Args:
|
||||
self: Crew instance with MCP server configuration.
|
||||
*tool_names: Optional tool names to filter by.
|
||||
|
||||
Returns:
|
||||
List of filtered MCP tools, or empty list if no MCP server configured.
|
||||
"""
|
||||
if not self.mcp_server_params:
|
||||
return []
|
||||
|
||||
from crewai_tools import MCPServerAdapter # type: ignore[import-untyped]
|
||||
|
||||
if self._mcp_server_adapter is None:
|
||||
self._mcp_server_adapter = MCPServerAdapter(
|
||||
self.mcp_server_params, connect_timeout=self.mcp_connect_timeout
|
||||
)
|
||||
|
||||
return self._mcp_server_adapter.tools.filter_by_names(tool_names or None)
|
||||
|
||||
|
||||
def _load_config(
|
||||
self: CrewInstance, config_path: str | None, config_type: Literal["agent", "task"]
|
||||
) -> dict[str, Any]:
|
||||
"""Load YAML config file or return empty dict if not found.
|
||||
|
||||
Args:
|
||||
self: Crew instance with base directory and load_yaml method.
|
||||
config_path: Relative path to config file.
|
||||
config_type: Config type for logging, either "agent" or "task".
|
||||
|
||||
Returns:
|
||||
Config dictionary or empty dict.
|
||||
"""
|
||||
if isinstance(config_path, str):
|
||||
full_path = self.base_directory / config_path
|
||||
try:
|
||||
return self.load_yaml(full_path)
|
||||
except FileNotFoundError:
|
||||
logging.warning(
|
||||
f"{config_type.capitalize()} config file not found at {full_path}. "
|
||||
f"Proceeding with empty {config_type} configurations."
|
||||
)
|
||||
return {}
|
||||
else:
|
||||
logging.warning(
|
||||
f"No {config_type} configuration path provided. "
|
||||
f"Proceeding with empty {config_type} configurations."
|
||||
)
|
||||
return {}
|
||||
|
||||
for task_name, task_info in self.tasks_config.items():
|
||||
self._map_task_variables(
|
||||
task_name,
|
||||
task_info,
|
||||
agents,
|
||||
tasks,
|
||||
output_json_functions,
|
||||
tool_functions,
|
||||
callback_functions,
|
||||
output_pydantic_functions,
|
||||
)
|
||||
|
||||
def _map_task_variables(
|
||||
self,
|
||||
task_name: str,
|
||||
task_info: dict[str, Any],
|
||||
agents: dict[str, Callable],
|
||||
tasks: dict[str, Callable],
|
||||
output_json_functions: dict[str, Callable],
|
||||
tool_functions: dict[str, Callable],
|
||||
callback_functions: dict[str, Callable],
|
||||
output_pydantic_functions: dict[str, Callable],
|
||||
) -> None:
|
||||
if context_list := task_info.get("context"):
|
||||
self.tasks_config[task_name]["context"] = [
|
||||
tasks[context_task_name]() for context_task_name in context_list
|
||||
]
|
||||
def load_configurations(self: CrewInstance) -> None:
|
||||
"""Load agent and task YAML configurations.
|
||||
|
||||
if tools := task_info.get("tools"):
|
||||
self.tasks_config[task_name]["tools"] = [
|
||||
tool_functions[tool]() for tool in tools
|
||||
]
|
||||
Args:
|
||||
self: Crew instance with configuration paths.
|
||||
"""
|
||||
self.agents_config = self._load_config(self.original_agents_config_path, "agent")
|
||||
self.tasks_config = self._load_config(self.original_tasks_config_path, "task")
|
||||
|
||||
if agent_name := task_info.get("agent"):
|
||||
self.tasks_config[task_name]["agent"] = agents[agent_name]()
|
||||
|
||||
if output_json := task_info.get("output_json"):
|
||||
self.tasks_config[task_name]["output_json"] = output_json_functions[
|
||||
output_json
|
||||
]
|
||||
def load_yaml(config_path: Path) -> dict[str, Any]:
|
||||
"""Load and parse YAML configuration file.
|
||||
|
||||
if output_pydantic := task_info.get("output_pydantic"):
|
||||
self.tasks_config[task_name]["output_pydantic"] = (
|
||||
output_pydantic_functions[output_pydantic]
|
||||
)
|
||||
Args:
|
||||
config_path: Path to YAML configuration file.
|
||||
|
||||
if callbacks := task_info.get("callbacks"):
|
||||
self.tasks_config[task_name]["callbacks"] = [
|
||||
callback_functions[callback]() for callback in callbacks
|
||||
]
|
||||
Returns:
|
||||
Parsed YAML content as a dictionary. Returns empty dict if file is empty.
|
||||
|
||||
if guardrail := task_info.get("guardrail"):
|
||||
self.tasks_config[task_name]["guardrail"] = guardrail
|
||||
Raises:
|
||||
FileNotFoundError: If config file does not exist.
|
||||
"""
|
||||
try:
|
||||
with open(config_path, encoding="utf-8") as file:
|
||||
content = yaml.safe_load(file)
|
||||
return content if isinstance(content, dict) else {}
|
||||
except FileNotFoundError:
|
||||
logging.warning(f"File not found: {config_path}")
|
||||
raise
|
||||
|
||||
# Include base class (qual)name in the wrapper class (qual)name.
|
||||
WrappedClass.__name__ = CrewBase.__name__ + "(" + cls.__name__ + ")"
|
||||
WrappedClass.__qualname__ = CrewBase.__qualname__ + "(" + cls.__name__ + ")"
|
||||
WrappedClass._crew_name = cls.__name__
|
||||
|
||||
return cast(T, WrappedClass)
|
||||
def _get_all_methods(self: CrewInstance) -> dict[str, Callable[..., Any]]:
|
||||
"""Return all non-dunder callable attributes (methods).
|
||||
|
||||
Args:
|
||||
self: Instance to inspect for callable attributes.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping method names to bound method objects.
|
||||
"""
|
||||
return {
|
||||
name: getattr(self, name)
|
||||
for name in dir(self)
|
||||
if not (name.startswith("__") and name.endswith("__"))
|
||||
and callable(getattr(self, name, None))
|
||||
}
|
||||
|
||||
|
||||
def _filter_methods(
|
||||
methods: dict[str, CallableT], attribute: str
|
||||
) -> dict[str, CallableT]:
|
||||
"""Filter methods by attribute presence, preserving exact callable types.
|
||||
|
||||
Args:
|
||||
methods: Dictionary of methods to filter.
|
||||
attribute: Attribute name to check for.
|
||||
|
||||
Returns:
|
||||
Dictionary containing only methods with the specified attribute.
|
||||
The return type matches the input callable type exactly.
|
||||
"""
|
||||
return {
|
||||
name: method for name, method in methods.items() if hasattr(method, attribute)
|
||||
}
|
||||
|
||||
|
||||
def map_all_agent_variables(self: CrewInstance) -> None:
|
||||
"""Map agent configuration variables to callable instances.
|
||||
|
||||
Args:
|
||||
self: Crew instance with agent configurations to map.
|
||||
"""
|
||||
llms = _filter_methods(self._all_methods, "is_llm")
|
||||
tool_functions = _filter_methods(self._all_methods, "is_tool")
|
||||
cache_handler_functions = _filter_methods(self._all_methods, "is_cache_handler")
|
||||
callbacks = _filter_methods(self._all_methods, "is_callback")
|
||||
|
||||
for agent_name, agent_info in self.agents_config.items():
|
||||
self._map_agent_variables(
|
||||
agent_name=agent_name,
|
||||
agent_info=agent_info,
|
||||
llms=llms,
|
||||
tool_functions=tool_functions,
|
||||
cache_handler_functions=cache_handler_functions,
|
||||
callbacks=callbacks,
|
||||
)
|
||||
|
||||
|
||||
def _map_agent_variables(
|
||||
self: CrewInstance,
|
||||
agent_name: str,
|
||||
agent_info: AgentConfig,
|
||||
llms: dict[str, Callable[[], Any]],
|
||||
tool_functions: dict[str, Callable[[], BaseTool]],
|
||||
cache_handler_functions: dict[str, Callable[[], Any]],
|
||||
callbacks: dict[str, Callable[..., Any]],
|
||||
) -> None:
|
||||
"""Resolve and map variables for a single agent.
|
||||
|
||||
Args:
|
||||
self: Crew instance with agent configurations.
|
||||
agent_name: Name of agent to configure.
|
||||
agent_info: Agent configuration dictionary with optional fields.
|
||||
llms: Dictionary mapping names to LLM factory functions.
|
||||
tool_functions: Dictionary mapping names to tool factory functions.
|
||||
cache_handler_functions: Dictionary mapping names to cache handler factory functions.
|
||||
callbacks: Dictionary of available callbacks.
|
||||
"""
|
||||
if llm := agent_info.get("llm"):
|
||||
factory = llms.get(llm)
|
||||
self.agents_config[agent_name]["llm"] = factory() if factory else llm
|
||||
|
||||
if tools := agent_info.get("tools"):
|
||||
if _is_string_list(tools):
|
||||
self.agents_config[agent_name]["tools"] = [
|
||||
tool_functions[tool]() for tool in tools
|
||||
]
|
||||
|
||||
if function_calling_llm := agent_info.get("function_calling_llm"):
|
||||
factory = llms.get(function_calling_llm)
|
||||
self.agents_config[agent_name]["function_calling_llm"] = (
|
||||
factory() if factory else function_calling_llm
|
||||
)
|
||||
|
||||
if step_callback := agent_info.get("step_callback"):
|
||||
self.agents_config[agent_name]["step_callback"] = callbacks[step_callback]()
|
||||
|
||||
if cache_handler := agent_info.get("cache_handler"):
|
||||
if _is_string_value(cache_handler):
|
||||
self.agents_config[agent_name]["cache_handler"] = cache_handler_functions[
|
||||
cache_handler
|
||||
]()
|
||||
|
||||
|
||||
def map_all_task_variables(self: CrewInstance) -> None:
|
||||
"""Map task configuration variables to callable instances.
|
||||
|
||||
Args:
|
||||
self: Crew instance with task configurations to map.
|
||||
"""
|
||||
agents = _filter_methods(self._all_methods, "is_agent")
|
||||
tasks = _filter_methods(self._all_methods, "is_task")
|
||||
output_json_functions = _filter_methods(self._all_methods, "is_output_json")
|
||||
tool_functions = _filter_methods(self._all_methods, "is_tool")
|
||||
callback_functions = _filter_methods(self._all_methods, "is_callback")
|
||||
output_pydantic_functions = _filter_methods(self._all_methods, "is_output_pydantic")
|
||||
|
||||
for task_name, task_info in self.tasks_config.items():
|
||||
self._map_task_variables(
|
||||
task_name=task_name,
|
||||
task_info=task_info,
|
||||
agents=agents,
|
||||
tasks=tasks,
|
||||
output_json_functions=output_json_functions,
|
||||
tool_functions=tool_functions,
|
||||
callback_functions=callback_functions,
|
||||
output_pydantic_functions=output_pydantic_functions,
|
||||
)
|
||||
|
||||
|
||||
def _map_task_variables(
|
||||
self: CrewInstance,
|
||||
task_name: str,
|
||||
task_info: TaskConfig,
|
||||
agents: dict[str, Callable[[], Agent]],
|
||||
tasks: dict[str, Callable[[], Task]],
|
||||
output_json_functions: dict[str, OutputJsonClass[Any]],
|
||||
tool_functions: dict[str, Callable[[], BaseTool]],
|
||||
callback_functions: dict[str, Callable[..., Any]],
|
||||
output_pydantic_functions: dict[str, OutputPydanticClass[Any]],
|
||||
) -> None:
|
||||
"""Resolve and map variables for a single task.
|
||||
|
||||
Args:
|
||||
self: Crew instance with task configurations.
|
||||
task_name: Name of task to configure.
|
||||
task_info: Task configuration dictionary with optional fields.
|
||||
agents: Dictionary mapping names to agent factory functions.
|
||||
tasks: Dictionary mapping names to task factory functions.
|
||||
output_json_functions: Dictionary of JSON output class wrappers.
|
||||
tool_functions: Dictionary mapping names to tool factory functions.
|
||||
callback_functions: Dictionary of available callbacks.
|
||||
output_pydantic_functions: Dictionary of Pydantic output class wrappers.
|
||||
"""
|
||||
if context_list := task_info.get("context"):
|
||||
self.tasks_config[task_name]["context"] = [
|
||||
tasks[context_task_name]() for context_task_name in context_list
|
||||
]
|
||||
|
||||
if tools := task_info.get("tools"):
|
||||
if _is_string_list(tools):
|
||||
self.tasks_config[task_name]["tools"] = [
|
||||
tool_functions[tool]() for tool in tools
|
||||
]
|
||||
|
||||
if agent_name := task_info.get("agent"):
|
||||
self.tasks_config[task_name]["agent"] = agents[agent_name]()
|
||||
|
||||
if output_json := task_info.get("output_json"):
|
||||
self.tasks_config[task_name]["output_json"] = output_json_functions[output_json]
|
||||
|
||||
if output_pydantic := task_info.get("output_pydantic"):
|
||||
self.tasks_config[task_name]["output_pydantic"] = output_pydantic_functions[
|
||||
output_pydantic
|
||||
]
|
||||
|
||||
if callbacks := task_info.get("callbacks"):
|
||||
self.tasks_config[task_name]["callbacks"] = [
|
||||
callback_functions[callback]() for callback in callbacks
|
||||
]
|
||||
|
||||
if guardrail := task_info.get("guardrail"):
|
||||
self.tasks_config[task_name]["guardrail"] = guardrail
|
||||
|
||||
|
||||
_CLASS_SETUP_FUNCTIONS: tuple[Callable[[type[CrewClass]], None], ...] = (
|
||||
_set_base_directory,
|
||||
_set_config_paths,
|
||||
_set_mcp_params,
|
||||
)
|
||||
|
||||
_METHODS_TO_INJECT = (
|
||||
close_mcp_server,
|
||||
get_mcp_tools,
|
||||
_load_config,
|
||||
load_configurations,
|
||||
staticmethod(load_yaml),
|
||||
map_all_agent_variables,
|
||||
_map_agent_variables,
|
||||
map_all_task_variables,
|
||||
_map_task_variables,
|
||||
)
|
||||
|
||||
|
||||
class _CrewBaseType(type):
|
||||
"""Metaclass for CrewBase that makes it callable as a decorator."""
|
||||
|
||||
def __call__(cls, decorated_cls: type) -> type[CrewClass]:
|
||||
"""Apply CrewBaseMeta to the decorated class.
|
||||
|
||||
Args:
|
||||
decorated_cls: Class to transform with CrewBaseMeta metaclass.
|
||||
|
||||
Returns:
|
||||
New class with CrewBaseMeta metaclass applied.
|
||||
"""
|
||||
__name = str(decorated_cls.__name__)
|
||||
__bases = tuple(decorated_cls.__bases__)
|
||||
__dict = {
|
||||
key: value
|
||||
for key, value in decorated_cls.__dict__.items()
|
||||
if key not in ("__dict__", "__weakref__")
|
||||
}
|
||||
for slot in __dict.get("__slots__", tuple()):
|
||||
__dict.pop(slot, None)
|
||||
__dict["__metaclass__"] = CrewBaseMeta
|
||||
return cast(type[CrewClass], CrewBaseMeta(__name, __bases, __dict))
|
||||
|
||||
|
||||
class CrewBase(metaclass=_CrewBaseType):
|
||||
"""Class decorator that applies CrewBaseMeta metaclass.
|
||||
|
||||
Applies CrewBaseMeta metaclass to a class via decorator syntax rather than
|
||||
explicit metaclass declaration. Use as @CrewBase instead of
|
||||
class Foo(metaclass=CrewBaseMeta).
|
||||
|
||||
Note:
|
||||
Reference: https://stackoverflow.com/questions/11091609/setting-a-class-metaclass-using-a-decorator
|
||||
"""
|
||||
|
||||