mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-03-13 23:38:17 +00:00
Compare commits
63 Commits
lorenze/im
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88fd859c26 | ||
|
|
3413f2e671 | ||
|
|
326ec15d54 | ||
|
|
c5a8fef118 | ||
|
|
b7af26ff60 | ||
|
|
48eb7c6937 | ||
|
|
d8e38f2f0b | ||
|
|
542afe61a8 | ||
|
|
8a5b3bc237 | ||
|
|
534f0707ca | ||
|
|
0046f9a96f | ||
|
|
e72a80be6e | ||
|
|
7cffcab84a | ||
|
|
f070ce8abd | ||
|
|
d9f6e2222f | ||
|
|
adef605410 | ||
|
|
cd42bcf035 | ||
|
|
bc45a7fbe3 | ||
|
|
87759cdb14 | ||
|
|
059cb93aeb | ||
|
|
cebc52694e | ||
|
|
53df41989a | ||
|
|
ea70976a5d | ||
|
|
3cc6516ae5 | ||
|
|
ad82e52d39 | ||
|
|
9336702ebc | ||
|
|
030f6d6c43 | ||
|
|
95d51db29f | ||
|
|
a8f51419f6 | ||
|
|
e7f17d2284 | ||
|
|
5d0811258f | ||
|
|
7972192d55 | ||
|
|
b3f8a42321 | ||
|
|
21224f2bc5 | ||
|
|
b76022c1e7 | ||
|
|
1ac5801578 | ||
|
|
c00a348837 | ||
|
|
6c8c6c8e12 | ||
|
|
3899910aa9 | ||
|
|
757a435ee3 | ||
|
|
8bfdb188f7 | ||
|
|
1bdb9496a3 | ||
|
|
979aa26c3d | ||
|
|
514c082882 | ||
|
|
c9e8068578 | ||
|
|
df2778f08b | ||
|
|
d8fea2518d | ||
|
|
d259150d8d | ||
|
|
c4a328c9d5 | ||
|
|
373abbb6b7 | ||
|
|
86d3ee022d | ||
|
|
09e3b81ca3 | ||
|
|
b6d8ce5c55 | ||
|
|
b371f97a2f | ||
|
|
017189db78 | ||
|
|
02d911494f | ||
|
|
8102d0a6ca | ||
|
|
ee374d01de | ||
|
|
9914e51199 | ||
|
|
2dbb83ae31 | ||
|
|
7377e1aa26 | ||
|
|
51754899a2 | ||
|
|
71b4f8402a |
@@ -21,7 +21,6 @@ OPENROUTER_API_KEY=fake-openrouter-key
|
||||
AWS_ACCESS_KEY_ID=fake-aws-access-key
|
||||
AWS_SECRET_ACCESS_KEY=fake-aws-secret-key
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_REGION_NAME=us-east-1
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Azure OpenAI Configuration
|
||||
|
||||
127
.github/workflows/nightly.yml
vendored
Normal file
127
.github/workflows/nightly.yml
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
name: Nightly Canary Release
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * *' # daily at 6am UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check for new commits
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
has_changes: ${{ steps.check.outputs.has_changes }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check for commits in last 24h
|
||||
id: check
|
||||
run: |
|
||||
RECENT=$(git log --since="24 hours ago" --oneline | head -1)
|
||||
if [ -n "$RECENT" ]; then
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build:
|
||||
name: Build nightly packages
|
||||
needs: check
|
||||
if: needs.check.outputs.has_changes == 'true' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
|
||||
- name: Stamp nightly versions
|
||||
run: |
|
||||
DATE=$(date +%Y%m%d)
|
||||
for init_file in \
|
||||
lib/crewai/src/crewai/__init__.py \
|
||||
lib/crewai-tools/src/crewai_tools/__init__.py \
|
||||
lib/crewai-files/src/crewai_files/__init__.py; do
|
||||
CURRENT=$(python -c "
|
||||
import re
|
||||
text = open('$init_file').read()
|
||||
print(re.search(r'__version__\s*=\s*\"(.*?)\"\s*$', text, re.MULTILINE).group(1))
|
||||
")
|
||||
NIGHTLY="${CURRENT}.dev${DATE}"
|
||||
sed -i "s/__version__ = .*/__version__ = \"${NIGHTLY}\"/" "$init_file"
|
||||
echo "$init_file: $CURRENT -> $NIGHTLY"
|
||||
done
|
||||
|
||||
# Update cross-package dependency pins to nightly versions
|
||||
sed -i "s/\"crewai-tools==[^\"]*\"/\"crewai-tools==${NIGHTLY}\"/" lib/crewai/pyproject.toml
|
||||
sed -i "s/\"crewai==[^\"]*\"/\"crewai==${NIGHTLY}\"/" lib/crewai-tools/pyproject.toml
|
||||
echo "Updated cross-package dependency pins to ${NIGHTLY}"
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
uv build --all-packages
|
||||
rm dist/.gitignore
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
publish:
|
||||
name: Publish nightly to PyPI
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/crewai
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
version: "0.8.4"
|
||||
python-version: "3.12"
|
||||
enable-cache: false
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
|
||||
- name: Publish to PyPI
|
||||
env:
|
||||
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
||||
run: |
|
||||
failed=0
|
||||
for package in dist/*; do
|
||||
if [[ "$package" == *"crewai_devtools"* ]]; then
|
||||
echo "Skipping private package: $package"
|
||||
continue
|
||||
fi
|
||||
echo "Publishing $package"
|
||||
if ! uv publish "$package"; then
|
||||
echo "Failed to publish $package"
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
if [ $failed -eq 1 ]; then
|
||||
echo "Some packages failed to publish"
|
||||
exit 1
|
||||
fi
|
||||
76
.github/workflows/publish.yml
vendored
76
.github/workflows/publish.yml
vendored
@@ -1,8 +1,6 @@
|
||||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [deployment-tests-passed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
@@ -20,11 +18,8 @@ jobs:
|
||||
- name: Determine release tag
|
||||
id: release
|
||||
run: |
|
||||
# Priority: workflow_dispatch input > repository_dispatch payload > default branch
|
||||
if [ -n "${{ inputs.release_tag }}" ]; then
|
||||
echo "tag=${{ inputs.release_tag }}" >> $GITHUB_OUTPUT
|
||||
elif [ -n "${{ github.event.client_payload.release_tag }}" ]; then
|
||||
echo "tag=${{ github.event.client_payload.release_tag }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "tag=" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
@@ -64,6 +59,8 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.release_tag || github.ref }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
@@ -98,3 +95,72 @@ jobs:
|
||||
echo "Some packages failed to publish"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build Slack payload
|
||||
if: success()
|
||||
id: slack
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
run: |
|
||||
payload=$(uv run python -c "
|
||||
import json, re, subprocess, sys
|
||||
|
||||
with open('lib/crewai/src/crewai/__init__.py') as f:
|
||||
m = re.search(r\"__version__\s*=\s*[\\\"']([^\\\"']+)\", f.read())
|
||||
version = m.group(1) if m else 'unknown'
|
||||
|
||||
import os
|
||||
tag = os.environ.get('RELEASE_TAG') or version
|
||||
|
||||
try:
|
||||
r = subprocess.run(['gh','release','view',tag,'--json','body','-q','.body'],
|
||||
capture_output=True, text=True, check=True)
|
||||
body = r.stdout.strip()
|
||||
except Exception:
|
||||
body = ''
|
||||
|
||||
blocks = [
|
||||
{'type':'section','text':{'type':'mrkdwn',
|
||||
'text':f':rocket: \`crewai v{version}\` published to PyPI'}},
|
||||
{'type':'section','text':{'type':'mrkdwn',
|
||||
'text':f'<https://pypi.org/project/crewai/{version}/|View on PyPI> · <https://github.com/crewAIInc/crewAI/releases/tag/{tag}|Release notes>'}},
|
||||
{'type':'divider'},
|
||||
]
|
||||
|
||||
if body:
|
||||
heading, items = '', []
|
||||
for line in body.split('\n'):
|
||||
line = line.strip()
|
||||
if not line: continue
|
||||
hm = re.match(r'^#{2,3}\s+(.*)', line)
|
||||
if hm:
|
||||
if heading and items:
|
||||
skip = heading in ('What\\'s Changed','') or 'Contributors' in heading
|
||||
if not skip:
|
||||
txt = f'*{heading}*\n' + '\n'.join(f'• {i}' for i in items)
|
||||
blocks.append({'type':'section','text':{'type':'mrkdwn','text':txt}})
|
||||
heading, items = hm.group(1), []
|
||||
elif line.startswith('- ') or line.startswith('* '):
|
||||
items.append(re.sub(r'\*\*([^*]*)\*\*', r'*\1*', line[2:]))
|
||||
if heading and items:
|
||||
skip = heading in ('What\\'s Changed','') or 'Contributors' in heading
|
||||
if not skip:
|
||||
txt = f'*{heading}*\n' + '\n'.join(f'• {i}' for i in items)
|
||||
blocks.append({'type':'section','text':{'type':'mrkdwn','text':txt}})
|
||||
|
||||
blocks.append({'type':'divider'})
|
||||
blocks.append({'type':'section','text':{'type':'mrkdwn',
|
||||
'text':f'\`\`\`uv add \"crewai[tools]=={version}\"\`\`\`'}})
|
||||
|
||||
print(json.dumps({'blocks':blocks}))
|
||||
")
|
||||
echo "payload=$payload" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Notify Slack
|
||||
if: success()
|
||||
uses: slackapi/slack-github-action@v2.1.0
|
||||
with:
|
||||
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
webhook-type: incoming-webhook
|
||||
payload: ${{ steps.slack.outputs.payload }}
|
||||
|
||||
18
.github/workflows/trigger-deployment-tests.yml
vendored
18
.github/workflows/trigger-deployment-tests.yml
vendored
@@ -1,18 +0,0 @@
|
||||
name: Trigger Deployment Tests
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
trigger:
|
||||
name: Trigger deployment tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger deployment tests
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.CREWAI_DEPLOYMENTS_PAT }}
|
||||
repository: ${{ secrets.CREWAI_DEPLOYMENTS_REPOSITORY }}
|
||||
event-type: crewai-release
|
||||
client-payload: '{"release_tag": "${{ github.event.release.tag_name }}", "release_name": "${{ github.event.release.name }}"}'
|
||||
@@ -12,6 +12,7 @@ from dotenv import load_dotenv
|
||||
import pytest
|
||||
from vcr.request import Request # type: ignore[import-untyped]
|
||||
|
||||
|
||||
try:
|
||||
import vcr.stubs.httpx_stubs as httpx_stubs # type: ignore[import-untyped]
|
||||
except ModuleNotFoundError:
|
||||
|
||||
3607
docs/docs.json
3607
docs/docs.json
File diff suppressed because it is too large
Load Diff
@@ -1,12 +0,0 @@
|
||||
---
|
||||
title: "Agents: Examples"
|
||||
description: "Runnable examples for robust agent configuration and execution."
|
||||
icon: "rocket-launch"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Example links
|
||||
|
||||
- [/en/guides/agents/crafting-effective-agents](/en/guides/agents/crafting-effective-agents)
|
||||
- [/en/learn/customizing-agents](/en/learn/customizing-agents)
|
||||
- [/en/learn/coding-agents](/en/learn/coding-agents)
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
title: "Agents: Concepts"
|
||||
description: "Agent role contracts, task boundaries, and decision criteria for robust agent behavior."
|
||||
icon: "user"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## When to use
|
||||
|
||||
- You need specialized behavior with explicit role and goal.
|
||||
- You need tool-enabled execution under constraints.
|
||||
|
||||
## When not to use
|
||||
|
||||
- Static transformations are enough without model reasoning.
|
||||
- Task can be solved by deterministic code only.
|
||||
|
||||
## Core decisions
|
||||
|
||||
| Decision | Choose this when |
|
||||
|---|---|
|
||||
| Single agent | Narrow scope, low coordination needs |
|
||||
| Multi-agent crew | Distinct expertise and review loops needed |
|
||||
| Tool-enabled agent | Model needs external actions or data |
|
||||
|
||||
## Canonical links
|
||||
|
||||
- Reference: [/en/ai/agents/reference](/en/ai/agents/reference)
|
||||
- Patterns: [/en/ai/agents/patterns](/en/ai/agents/patterns)
|
||||
- Troubleshooting: [/en/ai/agents/troubleshooting](/en/ai/agents/troubleshooting)
|
||||
- Examples: [/en/ai/agents/examples](/en/ai/agents/examples)
|
||||
- Existing docs: [/en/concepts/agents](/en/concepts/agents)
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
title: "Agents: Patterns"
|
||||
description: "Practical agent patterns for role design, tool boundaries, and reliable outputs."
|
||||
icon: "diagram-project"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Patterns
|
||||
|
||||
1. Role + reviewer pair
|
||||
- One agent drafts, one agent validates.
|
||||
|
||||
2. Tool-bounded agent
|
||||
- Restrict tool list to minimal action set.
|
||||
|
||||
3. Structured output agent
|
||||
- Force JSON or schema output for automation pipelines.
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
title: "Agents: Reference"
|
||||
description: "Reference for agent fields, prompt contracts, tool usage, and output constraints."
|
||||
icon: "book"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Agent contract
|
||||
|
||||
- `role`: stable operating identity
|
||||
- `goal`: measurable completion objective
|
||||
- `backstory`: bounded style and context
|
||||
- `tools`: allowed action surface
|
||||
|
||||
## Output contract
|
||||
|
||||
- Prefer structured outputs for machine workflows.
|
||||
- Define failure behavior for missing tool data.
|
||||
|
||||
## Canonical source
|
||||
|
||||
Primary API details live in [/en/concepts/agents](/en/concepts/agents).
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
title: "Agents: Troubleshooting"
|
||||
description: "Diagnose and fix common agent reliability and instruction-following failures."
|
||||
icon: "circle-exclamation"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Common issues
|
||||
|
||||
- Hallucinated tool results: require tool-call evidence in output.
|
||||
- Prompt drift: tighten role and success criteria.
|
||||
- Verbose but low-signal output: enforce concise schema output.
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
title: "Crews: Examples"
|
||||
description: "Runnable crew examples for sequential and hierarchical execution."
|
||||
icon: "rocket-launch"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Example links
|
||||
|
||||
- [/en/guides/crews/first-crew](/en/guides/crews/first-crew)
|
||||
- [/en/learn/sequential-process](/en/learn/sequential-process)
|
||||
- [/en/learn/hierarchical-process](/en/learn/hierarchical-process)
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
title: "Crews: Concepts"
|
||||
description: "When to use crews, process selection, delegation boundaries, and collaboration strategy."
|
||||
icon: "users"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## When to use
|
||||
|
||||
- You need multiple agents with specialized roles.
|
||||
- You need staged execution and reviewer loops.
|
||||
|
||||
## Process decision table
|
||||
|
||||
| Process | Best for |
|
||||
|---|---|
|
||||
| Sequential | Linear pipelines and deterministic ordering |
|
||||
| Hierarchical | Manager-controlled planning and delegation |
|
||||
|
||||
## Canonical links
|
||||
|
||||
- Reference: [/en/ai/crews/reference](/en/ai/crews/reference)
|
||||
- Patterns: [/en/ai/crews/patterns](/en/ai/crews/patterns)
|
||||
- Troubleshooting: [/en/ai/crews/troubleshooting](/en/ai/crews/troubleshooting)
|
||||
- Examples: [/en/ai/crews/examples](/en/ai/crews/examples)
|
||||
- Existing docs: [/en/concepts/crews](/en/concepts/crews)
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
title: "Crews: Patterns"
|
||||
description: "Production crew patterns for decomposition, review loops, and hybrid orchestration with Flows."
|
||||
icon: "diagram-project"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Patterns
|
||||
|
||||
1. Researcher + writer + reviewer
|
||||
2. Manager-directed hierarchical crew
|
||||
3. Flow-orchestrated multi-crew pipeline
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
title: "Crews: Reference"
|
||||
description: "Reference for crew composition, process semantics, task context passing, and execution modes."
|
||||
icon: "book"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Crew contract
|
||||
|
||||
- `agents`: available executors
|
||||
- `tasks`: work units with expected output
|
||||
- `process`: ordering and delegation semantics
|
||||
|
||||
## Runtime
|
||||
|
||||
- `kickoff()` for synchronous runs
|
||||
- `kickoff_async()` for async execution
|
||||
|
||||
## Canonical source
|
||||
|
||||
Primary API details live in [/en/concepts/crews](/en/concepts/crews).
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
title: "Crews: Troubleshooting"
|
||||
description: "Common multi-agent coordination failures and practical fixes."
|
||||
icon: "circle-exclamation"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Common issues
|
||||
|
||||
- Agents overlap on responsibilities: tighten role boundaries.
|
||||
- Output inconsistency: standardize expected outputs per task.
|
||||
- Slow runs: reduce unnecessary handoffs and model size.
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
title: "Flows: Examples"
|
||||
description: "Runnable end-to-end examples for production flow orchestration."
|
||||
icon: "rocket-launch"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Canonical examples
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Flowstate Chat History" icon="comments" href="/en/learn/flowstate-chat-history">
|
||||
Persistent chat history with summary compaction and memory scope.
|
||||
</Card>
|
||||
<Card title="Flows Concepts Example" icon="arrow-progress" href="/en/concepts/flows">
|
||||
Full API and feature-oriented flow examples, including routers and persistence.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
title: "Flows: Concepts"
|
||||
description: "When to use Flows, when not to use them, and key design constraints for production orchestration."
|
||||
icon: "arrow-progress"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## When to use
|
||||
|
||||
- You need deterministic orchestration, branching, and resumable execution.
|
||||
- You need explicit state transitions across steps.
|
||||
- You need persistence, routing, and event-driven control.
|
||||
|
||||
## When not to use
|
||||
|
||||
- A single prompt/response interaction is enough.
|
||||
- You only need one agent call without orchestration logic.
|
||||
|
||||
## Core decisions
|
||||
|
||||
| Decision | Choose this when |
|
||||
|---|---|
|
||||
| Unstructured state | Fast prototyping, highly dynamic fields |
|
||||
| Structured state | Stable contracts, team development, type safety |
|
||||
| `@persist()` | Long-running workflows and recovery requirements |
|
||||
| Router labels | Deterministic branch handling |
|
||||
|
||||
## Canonical links
|
||||
|
||||
- Reference: [/en/ai/flows/reference](/en/ai/flows/reference)
|
||||
- Patterns: [/en/ai/flows/patterns](/en/ai/flows/patterns)
|
||||
- Troubleshooting: [/en/ai/flows/troubleshooting](/en/ai/flows/troubleshooting)
|
||||
- Examples: [/en/ai/flows/examples](/en/ai/flows/examples)
|
||||
|
||||
## Existing docs
|
||||
|
||||
- [/en/concepts/flows](/en/concepts/flows)
|
||||
- [/en/guides/flows/mastering-flow-state](/en/guides/flows/mastering-flow-state)
|
||||
- [/en/learn/flowstate-chat-history](/en/learn/flowstate-chat-history)
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
title: "Flows: Patterns"
|
||||
description: "Production flow patterns: triage routing, flowstate chat history, and human-in-the-loop checkpoints."
|
||||
icon: "diagram-project"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Recommended patterns
|
||||
|
||||
1. Triage router flow
|
||||
- Inputs: normalized request payload
|
||||
- Output: deterministic route label + action
|
||||
- Reference: [/en/concepts/flows](/en/concepts/flows)
|
||||
|
||||
2. Flowstate chat history
|
||||
- Inputs: `session_id`, `last_user_message`
|
||||
- Output: assistant reply + compact context state
|
||||
- Reference: [/en/learn/flowstate-chat-history](/en/learn/flowstate-chat-history)
|
||||
|
||||
3. Human feedback gates
|
||||
- Inputs: generated artifact + reviewer feedback
|
||||
- Output: approved/rejected/revision path
|
||||
- Reference: [/en/learn/human-feedback-in-flows](/en/learn/human-feedback-in-flows)
|
||||
|
||||
## Pattern requirements
|
||||
|
||||
- declare explicit input schema
|
||||
- define expected output shape
|
||||
- list failure modes and retries
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
title: "Flows: Reference"
|
||||
description: "API-oriented reference for Flow decorators, lifecycle semantics, state, routing, and persistence."
|
||||
icon: "book"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Decorators
|
||||
|
||||
- `@start()` entrypoint, optional conditional trigger
|
||||
- `@listen(...)` downstream method subscription
|
||||
- `@router(...)` label-based deterministic routing
|
||||
- `@persist()` automatic state persistence checkpoints
|
||||
|
||||
## Runtime contracts
|
||||
|
||||
- `kickoff(inputs=...)` initializes or updates run inputs.
|
||||
- final output is the value from the last completed method.
|
||||
- `self.state` always has an auto-generated `id`.
|
||||
|
||||
## State contracts
|
||||
|
||||
- Use typed state for durable workflows.
|
||||
- Keep control fields explicit (`route`, `status`, `retry_count`).
|
||||
- Avoid storing unbounded raw transcripts in state.
|
||||
|
||||
## Resume and recovery
|
||||
|
||||
- Use persistence for recoverable runs.
|
||||
- Keep idempotent step logic for safe retries.
|
||||
|
||||
## Canonical source
|
||||
|
||||
Primary API details live in [/en/concepts/flows](/en/concepts/flows).
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
title: "Flows: Troubleshooting"
|
||||
description: "Common flow failures, causes, and fixes for state, routing, persistence, and resumption."
|
||||
icon: "circle-exclamation"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Common issues
|
||||
|
||||
### Branch did not trigger
|
||||
|
||||
- Cause: router label mismatch.
|
||||
- Fix: align returned label with `@listen("label")` exactly.
|
||||
|
||||
### State fields missing
|
||||
|
||||
- Cause: untyped dynamic writes or missing inputs.
|
||||
- Fix: switch to typed state and validate required fields at `@start()`.
|
||||
|
||||
### Context window blow-up
|
||||
|
||||
- Cause: raw message accumulation.
|
||||
- Fix: use sliding window + summary compaction pattern.
|
||||
|
||||
### Resume behavior inconsistent
|
||||
|
||||
- Cause: non-idempotent side effects in retried steps.
|
||||
- Fix: make side-effecting calls idempotent and record execution markers in state.
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
title: "LLMs: Examples"
|
||||
description: "Concrete examples for model setup, routing, and output-control patterns."
|
||||
icon: "rocket-launch"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Example links
|
||||
|
||||
- [/en/concepts/llms](/en/concepts/llms)
|
||||
- [/en/learn/llm-connections](/en/learn/llm-connections)
|
||||
- [/en/learn/custom-llm](/en/learn/custom-llm)
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
title: "LLMs: Concepts"
|
||||
description: "Model selection strategy, cost-quality tradeoffs, and reliability posture for CrewAI systems."
|
||||
icon: "microchip-ai"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## When to use advanced LLM configuration
|
||||
|
||||
- You need predictable quality, latency, and cost control.
|
||||
- You need model routing by task type.
|
||||
|
||||
## Core decisions
|
||||
|
||||
| Decision | Choose this when |
|
||||
|---|---|
|
||||
| Single model | Small systems with uniform task profile |
|
||||
| Routed models | Mixed workloads with different quality/cost needs |
|
||||
| Structured output | Automation pipelines and strict parsing needs |
|
||||
|
||||
## Canonical links
|
||||
|
||||
- Reference: [/en/ai/llms/reference](/en/ai/llms/reference)
|
||||
- Patterns: [/en/ai/llms/patterns](/en/ai/llms/patterns)
|
||||
- Troubleshooting: [/en/ai/llms/troubleshooting](/en/ai/llms/troubleshooting)
|
||||
- Examples: [/en/ai/llms/examples](/en/ai/llms/examples)
|
||||
- Existing docs: [/en/concepts/llms](/en/concepts/llms)
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
title: "LLMs: Patterns"
|
||||
description: "Model routing, reliability defaults, and structured outputs for production AI workflows."
|
||||
icon: "diagram-project"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Patterns
|
||||
|
||||
1. Role-based model routing
|
||||
2. Reliability defaults (`timeout`, `max_retries`, low temperature)
|
||||
3. JSON-first outputs for machine consumption
|
||||
4. Responses API for multi-turn reasoning flows
|
||||
|
||||
## Reference
|
||||
|
||||
- [/en/concepts/llms#production-llm-patterns](/en/concepts/llms#production-llm-patterns)
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
title: "LLMs: Reference"
|
||||
description: "Provider-agnostic LLM configuration reference for CrewAI projects."
|
||||
icon: "book"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Common parameters
|
||||
|
||||
- `model`
|
||||
- `temperature`
|
||||
- `max_tokens`
|
||||
- `timeout`
|
||||
- `max_retries`
|
||||
- `response_format`
|
||||
|
||||
## Contract guidance
|
||||
|
||||
- Set low temperature for extraction/classification.
|
||||
- Use structured outputs for downstream automation.
|
||||
- Set explicit timeout and retry policy for production.
|
||||
|
||||
## Canonical source
|
||||
|
||||
Primary API details live in [/en/concepts/llms](/en/concepts/llms).
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
title: "LLMs: Troubleshooting"
|
||||
description: "Fix common model behavior failures: drift, latency spikes, malformed output, and cost overruns."
|
||||
icon: "circle-exclamation"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Common issues
|
||||
|
||||
- Malformed JSON: enforce `response_format` and validate at boundary.
|
||||
- Latency spikes: route heavy tasks to smaller models when acceptable.
|
||||
- Cost growth: add budget-aware model routing and truncation rules.
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
title: "Memory: Examples"
|
||||
description: "Runnable examples for scoped storage and semantic retrieval in CrewAI."
|
||||
icon: "rocket-launch"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Example links
|
||||
|
||||
- [/en/concepts/memory](/en/concepts/memory)
|
||||
- [/en/learn/flowstate-chat-history](/en/learn/flowstate-chat-history)
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
title: "Memory: Concepts"
|
||||
description: "Designing recall systems with scope boundaries and state-vs-memory separation."
|
||||
icon: "database"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## When to use memory
|
||||
|
||||
- You need semantic recall across runs.
|
||||
- You need long-term context outside immediate flow state.
|
||||
|
||||
## When to use state instead
|
||||
|
||||
- Data is only needed for current control flow.
|
||||
- Data must remain deterministic and explicit per step.
|
||||
|
||||
## Canonical links
|
||||
|
||||
- Reference: [/en/ai/memory/reference](/en/ai/memory/reference)
|
||||
- Patterns: [/en/ai/memory/patterns](/en/ai/memory/patterns)
|
||||
- Troubleshooting: [/en/ai/memory/troubleshooting](/en/ai/memory/troubleshooting)
|
||||
- Examples: [/en/ai/memory/examples](/en/ai/memory/examples)
|
||||
- Existing docs: [/en/concepts/memory](/en/concepts/memory)
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
title: "Memory: Patterns"
|
||||
description: "Practical memory patterns for session recall, scoped retrieval, and hybrid flow-state designs."
|
||||
icon: "diagram-project"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Patterns
|
||||
|
||||
1. Session-scoped recall (`/chat/{session_id}`)
|
||||
2. Project-scoped knowledge (`/project/{project_id}`)
|
||||
3. Hybrid pattern: flow state for control, memory for long-tail context
|
||||
|
||||
## Reference
|
||||
|
||||
- [/en/learn/flowstate-chat-history](/en/learn/flowstate-chat-history)
|
||||
- [/en/guides/flows/mastering-flow-state](/en/guides/flows/mastering-flow-state)
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
title: "Memory: Reference"
|
||||
description: "Reference for remember/recall contracts, scopes, and retrieval tuning."
|
||||
icon: "book"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## API surface
|
||||
|
||||
- `remember(content, scope=...)`
|
||||
- `recall(query, limit=...)`
|
||||
- `extract_memories(text)`
|
||||
- `scope(path)` and `subscope(name)`
|
||||
|
||||
## Scope rules
|
||||
|
||||
- use `/{entity_type}/{identifier}` paths
|
||||
- keep hierarchy shallow
|
||||
- isolate sessions by stable identifiers
|
||||
|
||||
## Canonical source
|
||||
|
||||
Primary API details live in [/en/concepts/memory](/en/concepts/memory).
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
title: "Memory: Troubleshooting"
|
||||
description: "Diagnose poor recall quality, scope leakage, and stale memory retrieval."
|
||||
icon: "circle-exclamation"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Common issues
|
||||
|
||||
- Irrelevant recall: tighten scopes and query wording.
|
||||
- Missing recall: check scope path and recency weighting.
|
||||
- Scope leakage: avoid shared broad scopes for unrelated workflows.
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
title: "AI-First Documentation"
|
||||
description: "Canonical, agent-optimized documentation map for Flows, Agents, Crews, LLMs, Memory, and Tools."
|
||||
icon: "sitemap"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
This section is the canonical map for AI agents and developers.
|
||||
|
||||
Use it when you need:
|
||||
- one source of truth per domain
|
||||
- predictable page structure
|
||||
- runnable patterns with explicit inputs and outputs
|
||||
|
||||
## Domain Packs
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Flows" icon="arrow-progress" href="/en/ai/flows/index">
|
||||
State, routing, persistence, resume, and orchestration lifecycle.
|
||||
</Card>
|
||||
<Card title="Agents" icon="user" href="/en/ai/agents/index">
|
||||
Agent contracts, tool boundaries, prompt roles, and output discipline.
|
||||
</Card>
|
||||
<Card title="Crews" icon="users" href="/en/ai/crews/index">
|
||||
Multi-agent execution, process choice, delegation, and coordination.
|
||||
</Card>
|
||||
<Card title="LLMs" icon="microchip-ai" href="/en/ai/llms/index">
|
||||
Model configuration contracts, routing, reliability defaults, and providers.
|
||||
</Card>
|
||||
<Card title="Memory" icon="database" href="/en/ai/memory/index">
|
||||
Retrieval semantics, scope design, and state-vs-memory architecture.
|
||||
</Card>
|
||||
<Card title="Tools" icon="wrench" href="/en/ai/tools/index">
|
||||
Tool safety, schema contracts, retries, and integration patterns.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Writing Contract
|
||||
|
||||
Every domain follows the same structure:
|
||||
1. Concepts (`index`)
|
||||
2. Reference (`reference`)
|
||||
3. Patterns (`patterns`)
|
||||
4. Troubleshooting (`troubleshooting`)
|
||||
5. Examples (`examples`)
|
||||
|
||||
## Deprecation Policy
|
||||
|
||||
When a page is replaced:
|
||||
- keep a redirect for the old URL
|
||||
- keep one canonical destination
|
||||
- avoid duplicated conceptual prose
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
title: "Tools: Examples"
|
||||
description: "Practical examples for tool-driven agents and crews."
|
||||
icon: "rocket-launch"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Example links
|
||||
|
||||
- [/en/tools/overview](/en/tools/overview)
|
||||
- [/en/learn/create-custom-tools](/en/learn/create-custom-tools)
|
||||
- [/en/learn/tool-hooks](/en/learn/tool-hooks)
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
title: "Tools: Concepts"
|
||||
description: "Tool selection strategy, safety boundaries, and reliability rules for agentic execution."
|
||||
icon: "wrench"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## When to use tools
|
||||
|
||||
- Agents need external data or side effects.
|
||||
- Deterministic systems must be integrated into agent workflows.
|
||||
|
||||
## Tool safety rules
|
||||
|
||||
- define clear input schemas
|
||||
- validate outputs before downstream use
|
||||
- isolate privileged tools behind policy checks
|
||||
|
||||
## Canonical links
|
||||
|
||||
- Reference: [/en/ai/tools/reference](/en/ai/tools/reference)
|
||||
- Patterns: [/en/ai/tools/patterns](/en/ai/tools/patterns)
|
||||
- Troubleshooting: [/en/ai/tools/troubleshooting](/en/ai/tools/troubleshooting)
|
||||
- Examples: [/en/ai/tools/examples](/en/ai/tools/examples)
|
||||
- Existing docs: [/en/concepts/tools](/en/concepts/tools)
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
title: "Tools: Patterns"
|
||||
description: "Tool execution patterns for retrieval, action safety, and response grounding."
|
||||
icon: "diagram-project"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Patterns
|
||||
|
||||
1. Read-first then write pattern
|
||||
2. Validation gate before side effects
|
||||
3. Fallback tool chains for degraded mode
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
title: "Tools: Reference"
|
||||
description: "Reference for tool invocation contracts, argument schemas, and runtime safeguards."
|
||||
icon: "book"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Tool contract
|
||||
|
||||
- deterministic input schema
|
||||
- stable output schema
|
||||
- explicit error behavior
|
||||
|
||||
## Runtime safeguards
|
||||
|
||||
- timeout and retry policy
|
||||
- idempotency for side effects
|
||||
- validation before commit
|
||||
|
||||
## Canonical source
|
||||
|
||||
Primary API details live in [/en/concepts/tools](/en/concepts/tools).
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
title: "Tools: Troubleshooting"
|
||||
description: "Common tool-call failures and fixes for schema mismatch, retries, and side effects."
|
||||
icon: "circle-exclamation"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Common issues
|
||||
|
||||
- Schema mismatch: align tool args with declared model output schema.
|
||||
- Repeated side effects: add idempotency keys.
|
||||
- Tool timeouts: define retries with bounded backoff.
|
||||
@@ -4,6 +4,195 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Mar 13, 2026">
|
||||
## v1.10.2rc1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.10.2rc1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add release command and trigger PyPI publish
|
||||
|
||||
### Bug Fixes
|
||||
- Fix cross-process and thread-safe locking to unprotected I/O
|
||||
- Propagate contextvars across all thread and executor boundaries
|
||||
- Propagate ContextVars into async task threads
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.10.2a1
|
||||
|
||||
## Contributors
|
||||
|
||||
@danglies007, @greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 11, 2026">
|
||||
## v1.10.2a1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.10.2a1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add support for tool search, saving tokens, and dynamically injecting appropriate tools during execution for Anthropics.
|
||||
- Introduce more Brave Search tools.
|
||||
- Create action for nightly releases.
|
||||
|
||||
### Bug Fixes
|
||||
- Fix LockException under concurrent multi-process execution.
|
||||
- Resolve issues with grouping parallel tool results in a single user message.
|
||||
- Address MCP tools resolutions and eliminate all shared mutable connections.
|
||||
- Update LLM parameter handling in the human_feedback function.
|
||||
- Add missing list/dict methods to LockedListProxy and LockedDictProxy.
|
||||
- Propagate contextvars context to parallel tool call threads.
|
||||
- Bump gitpython dependency to >=3.1.41 to resolve CVE path traversal vulnerability.
|
||||
|
||||
### Refactoring
|
||||
- Refactor memory classes to be serializable.
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.10.1.
|
||||
|
||||
## Contributors
|
||||
|
||||
@akaKuruma, @github-actions[bot], @giulio-leone, @greysonlalonde, @joaomdmoura, @jonathansampson, @lorenzejay, @lucasgomide, @mattatcha
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 04, 2026">
|
||||
## v1.10.1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.10.1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Upgrade Gemini GenAI
|
||||
|
||||
### Bug Fixes
|
||||
- Adjust executor listener value to avoid recursion
|
||||
- Group parallel function response parts in a single Content object in Gemini
|
||||
- Surface thought output from thinking models in Gemini
|
||||
- Load MCP and platform tools when agent tools are None
|
||||
- Support Jupyter environments with running event loops in A2A
|
||||
- Use anonymous ID for ephemeral traces
|
||||
- Conditionally pass plus header
|
||||
- Skip signal handler registration in non-main threads for telemetry
|
||||
- Inject tool errors as observations and resolve name collisions
|
||||
- Upgrade pypdf from 4.x to 6.7.4 to resolve Dependabot alerts
|
||||
- Resolve critical and high Dependabot security alerts
|
||||
|
||||
### Documentation
|
||||
- Sync Composio tool documentation across locales
|
||||
|
||||
## Contributors
|
||||
|
||||
@giulio-leone, @greysonlalonde, @haxzie, @joaomdmoura, @lorenzejay, @mattatcha, @mplachta, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Feb 27, 2026">
|
||||
## v1.10.1a1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.10.1a1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Implement asynchronous invocation support in step callback methods
|
||||
- Implement lazy loading for heavy dependencies in Memory module
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.10.0
|
||||
|
||||
### Refactoring
|
||||
- Refactor step callback methods to support asynchronous invocation
|
||||
- Refactor to implement lazy loading for heavy dependencies in Memory module
|
||||
|
||||
### Bug Fixes
|
||||
- Fix branch for release notes
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Feb 27, 2026">
|
||||
## v1.10.1a1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.10.1a1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Refactoring
|
||||
- Refactor step callback methods to support asynchronous invocation
|
||||
- Implement lazy loading for heavy dependencies in Memory module
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.10.0
|
||||
|
||||
### Bug Fixes
|
||||
- Make branch for release notes
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Feb 26, 2026">
|
||||
## v1.10.0
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.10.0)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Enhance MCP tool resolution and related events
|
||||
- Update lancedb version and add lance-namespace packages
|
||||
- Enhance JSON argument parsing and validation in CrewAgentExecutor and BaseTool
|
||||
- Migrate CLI HTTP client from requests to httpx
|
||||
- Add versioned documentation
|
||||
- Add yanked detection for version notes
|
||||
- Implement user input handling in Flows
|
||||
- Enhance HITL self-loop functionality in human feedback integration tests
|
||||
- Add started_event_id and set in eventbus
|
||||
- Auto update tools.specs
|
||||
|
||||
### Bug Fixes
|
||||
- Validate tool kwargs even when empty to prevent cryptic TypeError
|
||||
- Preserve null types in tool parameter schemas for LLM
|
||||
- Map output_pydantic/output_json to native structured output
|
||||
- Ensure callbacks are ran/awaited if promise
|
||||
- Capture method name in exception context
|
||||
- Preserve enum type in router result; improve types
|
||||
- Fix cyclic flows silently breaking when persistence ID is passed in inputs
|
||||
- Correct CLI flag format from --skip-provider to --skip_provider
|
||||
- Ensure OpenAI tool call stream is finalized
|
||||
- Resolve complex schema $ref pointers in MCP tools
|
||||
- Enforce additionalProperties=false in schemas
|
||||
- Reject reserved script names for crew folders
|
||||
- Resolve race condition in guardrail event emission test
|
||||
|
||||
### Documentation
|
||||
- Add litellm dependency note for non-native LLM providers
|
||||
- Clarify NL2SQL security model and hardening guidance
|
||||
- Add 96 missing actions across 9 integrations
|
||||
|
||||
### Refactoring
|
||||
- Refactor crew to provider
|
||||
- Extract HITL to provider pattern
|
||||
- Improve hook typing and registration
|
||||
|
||||
## Contributors
|
||||
|
||||
@dependabot[bot], @github-actions[bot], @github-code-quality[bot], @greysonlalonde, @heitorado, @hobostay, @joaomdmoura, @johnvan7, @jonathansampson, @lorenzejay, @lucasgomide, @mattatcha, @mplachta, @nicoferdi96, @theCyberTech, @thiagomoretto, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Jan 26, 2026">
|
||||
## v1.9.0
|
||||
|
||||
|
||||
@@ -23,17 +23,6 @@ In the CrewAI framework, an `Agent` is an autonomous unit that can:
|
||||
at creating content.
|
||||
</Tip>
|
||||
|
||||
## When to Use Agents
|
||||
|
||||
- You need role-specific reasoning and decision-making.
|
||||
- You need tool-enabled execution with delegated responsibilities.
|
||||
- You need reusable behavioral units across tasks and crews.
|
||||
|
||||
## When Not to Use Agents
|
||||
|
||||
- Deterministic business logic in plain code is sufficient.
|
||||
- A static transformation without reasoning is sufficient.
|
||||
|
||||
<Note type="info" title="Enterprise Enhancement: Visual Agent Builder">
|
||||
CrewAI AMP includes a Visual Agent Builder that simplifies agent creation and configuration without writing code. Design your agents visually and test them in real-time.
|
||||
|
||||
|
||||
@@ -9,17 +9,6 @@ mode: "wide"
|
||||
|
||||
A crew in crewAI represents a collaborative group of agents working together to achieve a set of tasks. Each crew defines the strategy for task execution, agent collaboration, and the overall workflow.
|
||||
|
||||
## When to Use Crews
|
||||
|
||||
- You need multiple specialized agents collaborating on a shared outcome.
|
||||
- You need process-level orchestration (`sequential` or `hierarchical`).
|
||||
- You need task-level handoffs and context propagation.
|
||||
|
||||
## When Not to Use Crews
|
||||
|
||||
- A single agent can complete the work end-to-end.
|
||||
- You do not need multi-step task decomposition.
|
||||
|
||||
## Crew Attributes
|
||||
|
||||
| Attribute | Parameters | Description |
|
||||
@@ -428,17 +417,3 @@ crewai replay -t <task_id>
|
||||
```
|
||||
|
||||
These commands let you replay from your latest kickoff tasks, still retaining context from previously executed tasks.
|
||||
|
||||
## Common Failure Modes
|
||||
|
||||
### Agents overlap responsibilities
|
||||
- Cause: role/goal definitions are too broad.
|
||||
- Fix: tighten role boundaries and task ownership.
|
||||
|
||||
### Hierarchical runs stall or degrade
|
||||
- Cause: weak manager configuration or unclear delegation criteria.
|
||||
- Fix: define a stronger manager objective and explicit completion criteria.
|
||||
|
||||
### Crew outputs are inconsistent
|
||||
- Cause: expected outputs are underspecified across tasks.
|
||||
- Fix: enforce structured outputs and stronger task contracts.
|
||||
|
||||
@@ -19,121 +19,82 @@ Flows allow you to create structured, event-driven workflows. They provide a sea
|
||||
|
||||
4. **Flexible Control Flow**: Implement conditional logic, loops, and branching within your workflows.
|
||||
|
||||
## When to Use Flows
|
||||
|
||||
- You need deterministic orchestration and branching logic.
|
||||
- You need explicit state transitions across multiple steps.
|
||||
- You need resumable workflows with persistence.
|
||||
- You need to combine crews, direct model calls, and Python logic in one runtime.
|
||||
|
||||
## When Not to Use Flows
|
||||
|
||||
- A single prompt/response call is sufficient.
|
||||
- A single crew kickoff with no orchestration logic is sufficient.
|
||||
- You do not need stateful multi-step execution.
|
||||
|
||||
## Getting Started
|
||||
|
||||
The example below shows a realistic Flow for support-ticket triage. It demonstrates features teams use in production: typed state, routing, memory access, and persistence.
|
||||
Let's create a simple Flow where you will use OpenAI to generate a random city in one task and then use that city to generate a fun fact in another task.
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, listen, router, start
|
||||
from crewai.flow.persistence import persist
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from dotenv import load_dotenv
|
||||
from litellm import completion
|
||||
|
||||
|
||||
class SupportTriageState(BaseModel):
|
||||
ticket_id: str = ""
|
||||
customer_tier: str = "standard" # standard | enterprise
|
||||
issue: str = ""
|
||||
urgency: str = "normal"
|
||||
route: str = ""
|
||||
draft_reply: str = ""
|
||||
internal_notes: list[str] = Field(default_factory=list)
|
||||
class ExampleFlow(Flow):
|
||||
model = "gpt-4o-mini"
|
||||
|
||||
|
||||
@persist()
|
||||
class SupportTriageFlow(Flow[SupportTriageState]):
|
||||
@start()
|
||||
def ingest_ticket(self):
|
||||
# kickoff(inputs={...}) is merged into typed state fields
|
||||
print(f"Flow State ID: {self.state.id}")
|
||||
def generate_city(self):
|
||||
print("Starting flow")
|
||||
# Each flow state automatically gets a unique ID
|
||||
print(f"Flow State ID: {self.state['id']}")
|
||||
|
||||
self.remember(
|
||||
f"Ticket {self.state.ticket_id}: {self.state.issue}",
|
||||
scope=f"/support/{self.state.ticket_id}",
|
||||
response = completion(
|
||||
model=self.model,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Return the name of a random city in the world.",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
issue = self.state.issue.lower()
|
||||
if "security" in issue or "breach" in issue:
|
||||
self.state.urgency = "critical"
|
||||
elif self.state.customer_tier == "enterprise":
|
||||
self.state.urgency = "high"
|
||||
else:
|
||||
self.state.urgency = "normal"
|
||||
random_city = response["choices"][0]["message"]["content"]
|
||||
# Store the city in our state
|
||||
self.state["city"] = random_city
|
||||
print(f"Random City: {random_city}")
|
||||
|
||||
return self.state.issue
|
||||
return random_city
|
||||
|
||||
@router(ingest_ticket)
|
||||
def route_ticket(self):
|
||||
issue = self.state.issue.lower()
|
||||
if "security" in issue or "breach" in issue:
|
||||
self.state.route = "security"
|
||||
return "security_review"
|
||||
if self.state.customer_tier == "enterprise" or self.state.urgency == "high":
|
||||
self.state.route = "priority"
|
||||
return "priority_queue"
|
||||
self.state.route = "standard"
|
||||
return "standard_queue"
|
||||
|
||||
@listen("security_review")
|
||||
def handle_security(self):
|
||||
self.state.internal_notes.append("Escalated to Security Incident Response")
|
||||
self.state.draft_reply = (
|
||||
"We have escalated your case to our security team and will update you shortly."
|
||||
@listen(generate_city)
|
||||
def generate_fun_fact(self, random_city):
|
||||
response = completion(
|
||||
model=self.model,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Tell me a fun fact about {random_city}",
|
||||
},
|
||||
],
|
||||
)
|
||||
return self.state.draft_reply
|
||||
|
||||
@listen("priority_queue")
|
||||
def handle_priority(self):
|
||||
history = self.recall("SLA commitments for enterprise support", limit=2)
|
||||
self.state.internal_notes.append(
|
||||
f"Loaded {len(history)} memory hits for priority handling"
|
||||
)
|
||||
self.state.draft_reply = (
|
||||
"Your ticket has been prioritized and assigned to a senior support engineer."
|
||||
)
|
||||
return self.state.draft_reply
|
||||
|
||||
@listen("standard_queue")
|
||||
def handle_standard(self):
|
||||
self.state.internal_notes.append("Routed to standard support queue")
|
||||
self.state.draft_reply = "Thanks for reporting this. Our team will follow up soon."
|
||||
return self.state.draft_reply
|
||||
fun_fact = response["choices"][0]["message"]["content"]
|
||||
# Store the fun fact in our state
|
||||
self.state["fun_fact"] = fun_fact
|
||||
return fun_fact
|
||||
|
||||
|
||||
flow = SupportTriageFlow()
|
||||
flow.plot("support_triage_flow")
|
||||
result = flow.kickoff(
|
||||
inputs={
|
||||
"ticket_id": "TCK-1024",
|
||||
"customer_tier": "enterprise",
|
||||
"issue": "Cannot access SSO after enabling new policy",
|
||||
}
|
||||
)
|
||||
print("Final reply:", result)
|
||||
print("Route:", flow.state.route)
|
||||
print("Notes:", flow.state.internal_notes)
|
||||
|
||||
flow = ExampleFlow()
|
||||
flow.plot()
|
||||
result = flow.kickoff()
|
||||
|
||||
print(f"Generated fun fact: {result}")
|
||||
```
|
||||

|
||||
In this example, one flow demonstrates several core features together:
|
||||
1. `@start()` initializes and normalizes state for downstream steps.
|
||||
2. `@router()` performs deterministic branching into labeled routes.
|
||||
3. Route listeners implement lane-specific behavior (`security`, `priority`, `standard`).
|
||||
4. `@persist()` keeps the flow state recoverable between runs.
|
||||
5. Built-in memory methods (`remember`, `recall`) add durable context beyond a single method call.
|
||||
In the above example, we have created a simple Flow that generates a random city using OpenAI and then generates a fun fact about that city. The Flow consists of two tasks: `generate_city` and `generate_fun_fact`. The `generate_city` task is the starting point of the Flow, and the `generate_fun_fact` task listens for the output of the `generate_city` task.
|
||||
|
||||
This pattern mirrors typical production workflows where request classification, policy-aware routing, and auditable state all happen in one orchestrated flow.
|
||||
Each Flow instance automatically receives a unique identifier (UUID) in its state, which helps track and manage flow executions. The state can also store additional data (like the generated city and fun fact) that persists throughout the flow's execution.
|
||||
|
||||
When you run the Flow, it will:
|
||||
1. Generate a unique ID for the flow state
|
||||
2. Generate a random city and store it in the state
|
||||
3. Generate a fun fact about that city and store it in the state
|
||||
4. Print the results to the console
|
||||
|
||||
The state's unique ID and stored data can be useful for tracking flow executions and maintaining context between tasks.
|
||||
|
||||
**Note:** Ensure you have set up your `.env` file to store your `OPENAI_API_KEY`. This key is necessary for authenticating requests to the OpenAI API.
|
||||
|
||||
### @start()
|
||||
|
||||
@@ -156,15 +117,15 @@ The `@listen()` decorator can be used in several ways:
|
||||
1. **Listening to a Method by Name**: You can pass the name of the method you want to listen to as a string. When that method completes, the listener method will be triggered.
|
||||
|
||||
```python Code
|
||||
@listen("upstream_method")
|
||||
def downstream_method(self, upstream_result):
|
||||
@listen("generate_city")
|
||||
def generate_fun_fact(self, random_city):
|
||||
# Implementation
|
||||
```
|
||||
|
||||
2. **Listening to a Method Directly**: You can pass the method itself. When that method completes, the listener method will be triggered.
|
||||
```python Code
|
||||
@listen(upstream_method)
|
||||
def downstream_method(self, upstream_result):
|
||||
@listen(generate_city)
|
||||
def generate_fun_fact(self, random_city):
|
||||
# Implementation
|
||||
```
|
||||
|
||||
@@ -780,17 +741,201 @@ This example demonstrates several key features of using Agents in flows:
|
||||
|
||||
3. **Tool Integration**: Agents can use tools (like `WebsiteSearchTool`) to enhance their capabilities.
|
||||
|
||||
## Multi-Crew Flows and Plotting
|
||||
## Adding Crews to Flows
|
||||
|
||||
Detailed build walkthroughs and project scaffolding are documented in guide pages to keep this concepts page focused.
|
||||
Creating a flow with multiple crews in CrewAI is straightforward.
|
||||
|
||||
- Build your first flow: [/en/guides/flows/first-flow](/en/guides/flows/first-flow)
|
||||
- Master state and persistence: [/en/guides/flows/mastering-flow-state](/en/guides/flows/mastering-flow-state)
|
||||
- Real-world chat-state pattern: [/en/learn/flowstate-chat-history](/en/learn/flowstate-chat-history)
|
||||
You can generate a new CrewAI project that includes all the scaffolding needed to create a flow with multiple crews by running the following command:
|
||||
|
||||
For visualization:
|
||||
- Use `flow.plot("my_flow_plot")` in code, or
|
||||
- Use `crewai flow plot` in CLI projects.
|
||||
```bash
|
||||
crewai create flow name_of_flow
|
||||
```
|
||||
|
||||
This command will generate a new CrewAI project with the necessary folder structure. The generated project includes a prebuilt crew called `poem_crew` that is already working. You can use this crew as a template by copying, pasting, and editing it to create other crews.
|
||||
|
||||
### Folder Structure
|
||||
|
||||
After running the `crewai create flow name_of_flow` command, you will see a folder structure similar to the following:
|
||||
|
||||
| Directory/File | Description |
|
||||
| :--------------------- | :----------------------------------------------------------------- |
|
||||
| `name_of_flow/` | Root directory for the flow. |
|
||||
| ├── `crews/` | Contains directories for specific crews. |
|
||||
| │ └── `poem_crew/` | Directory for the "poem_crew" with its configurations and scripts. |
|
||||
| │ ├── `config/` | Configuration files directory for the "poem_crew". |
|
||||
| │ │ ├── `agents.yaml` | YAML file defining the agents for "poem_crew". |
|
||||
| │ │ └── `tasks.yaml` | YAML file defining the tasks for "poem_crew". |
|
||||
| │ ├── `poem_crew.py` | Script for "poem_crew" functionality. |
|
||||
| ├── `tools/` | Directory for additional tools used in the flow. |
|
||||
| │ └── `custom_tool.py` | Custom tool implementation. |
|
||||
| ├── `main.py` | Main script for running the flow. |
|
||||
| ├── `README.md` | Project description and instructions. |
|
||||
| ├── `pyproject.toml` | Configuration file for project dependencies and settings. |
|
||||
| └── `.gitignore` | Specifies files and directories to ignore in version control. |
|
||||
|
||||
### Building Your Crews
|
||||
|
||||
In the `crews` folder, you can define multiple crews. Each crew will have its own folder containing configuration files and the crew definition file. For example, the `poem_crew` folder contains:
|
||||
|
||||
- `config/agents.yaml`: Defines the agents for the crew.
|
||||
- `config/tasks.yaml`: Defines the tasks for the crew.
|
||||
- `poem_crew.py`: Contains the crew definition, including agents, tasks, and the crew itself.
|
||||
|
||||
You can copy, paste, and edit the `poem_crew` to create other crews.
|
||||
|
||||
### Connecting Crews in `main.py`
|
||||
|
||||
The `main.py` file is where you create your flow and connect the crews together. You can define your flow by using the `Flow` class and the decorators `@start` and `@listen` to specify the flow of execution.
|
||||
|
||||
Here's an example of how you can connect the `poem_crew` in the `main.py` file:
|
||||
|
||||
```python Code
|
||||
#!/usr/bin/env python
|
||||
from random import randint
|
||||
|
||||
from pydantic import BaseModel
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from .crews.poem_crew.poem_crew import PoemCrew
|
||||
|
||||
class PoemState(BaseModel):
|
||||
sentence_count: int = 1
|
||||
poem: str = ""
|
||||
|
||||
class PoemFlow(Flow[PoemState]):
|
||||
|
||||
@start()
|
||||
def generate_sentence_count(self):
|
||||
print("Generating sentence count")
|
||||
self.state.sentence_count = randint(1, 5)
|
||||
|
||||
@listen(generate_sentence_count)
|
||||
def generate_poem(self):
|
||||
print("Generating poem")
|
||||
result = PoemCrew().crew().kickoff(inputs={"sentence_count": self.state.sentence_count})
|
||||
|
||||
print("Poem generated", result.raw)
|
||||
self.state.poem = result.raw
|
||||
|
||||
@listen(generate_poem)
|
||||
def save_poem(self):
|
||||
print("Saving poem")
|
||||
with open("poem.txt", "w") as f:
|
||||
f.write(self.state.poem)
|
||||
|
||||
def kickoff():
|
||||
poem_flow = PoemFlow()
|
||||
poem_flow.kickoff()
|
||||
|
||||
|
||||
def plot():
|
||||
poem_flow = PoemFlow()
|
||||
poem_flow.plot("PoemFlowPlot")
|
||||
|
||||
if __name__ == "__main__":
|
||||
kickoff()
|
||||
plot()
|
||||
```
|
||||
|
||||
In this example, the `PoemFlow` class defines a flow that generates a sentence count, uses the `PoemCrew` to generate a poem, and then saves the poem to a file. The flow is kicked off by calling the `kickoff()` method. The PoemFlowPlot will be generated by `plot()` method.
|
||||
|
||||

|
||||
|
||||
### Running the Flow
|
||||
|
||||
(Optional) Before running the flow, you can install the dependencies by running:
|
||||
|
||||
```bash
|
||||
crewai install
|
||||
```
|
||||
|
||||
Once all of the dependencies are installed, you need to activate the virtual environment by running:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
After activating the virtual environment, you can run the flow by executing one of the following commands:
|
||||
|
||||
```bash
|
||||
crewai flow kickoff
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
uv run kickoff
|
||||
```
|
||||
|
||||
The flow will execute, and you should see the output in the console.
|
||||
|
||||
## Plot Flows
|
||||
|
||||
Visualizing your AI workflows can provide valuable insights into the structure and execution paths of your flows. CrewAI offers a powerful visualization tool that allows you to generate interactive plots of your flows, making it easier to understand and optimize your AI workflows.
|
||||
|
||||
### What are Plots?
|
||||
|
||||
Plots in CrewAI are graphical representations of your AI workflows. They display the various tasks, their connections, and the flow of data between them. This visualization helps in understanding the sequence of operations, identifying bottlenecks, and ensuring that the workflow logic aligns with your expectations.
|
||||
|
||||
### How to Generate a Plot
|
||||
|
||||
CrewAI provides two convenient methods to generate plots of your flows:
|
||||
|
||||
#### Option 1: Using the `plot()` Method
|
||||
|
||||
If you are working directly with a flow instance, you can generate a plot by calling the `plot()` method on your flow object. This method will create an HTML file containing the interactive plot of your flow.
|
||||
|
||||
```python Code
|
||||
# Assuming you have a flow instance
|
||||
flow.plot("my_flow_plot")
|
||||
```
|
||||
|
||||
This will generate a file named `my_flow_plot.html` in your current directory. You can open this file in a web browser to view the interactive plot.
|
||||
|
||||
#### Option 2: Using the Command Line
|
||||
|
||||
If you are working within a structured CrewAI project, you can generate a plot using the command line. This is particularly useful for larger projects where you want to visualize the entire flow setup.
|
||||
|
||||
```bash
|
||||
crewai flow plot
|
||||
```
|
||||
|
||||
This command will generate an HTML file with the plot of your flow, similar to the `plot()` method. The file will be saved in your project directory, and you can open it in a web browser to explore the flow.
|
||||
|
||||
### Understanding the Plot
|
||||
|
||||
The generated plot will display nodes representing the tasks in your flow, with directed edges indicating the flow of execution. The plot is interactive, allowing you to zoom in and out, and hover over nodes to see additional details.
|
||||
|
||||
By visualizing your flows, you can gain a clearer understanding of the workflow's structure, making it easier to debug, optimize, and communicate your AI processes to others.
|
||||
|
||||
### Conclusion
|
||||
|
||||
Plotting your flows is a powerful feature of CrewAI that enhances your ability to design and manage complex AI workflows. Whether you choose to use the `plot()` method or the command line, generating plots will provide you with a visual representation of your workflows, aiding in both development and presentation.
|
||||
|
||||
## Next Steps
|
||||
|
||||
If you're interested in exploring additional examples of flows, we have a variety of recommendations in our examples repository. Here are four specific flow examples, each showcasing unique use cases to help you match your current problem type to a specific example:
|
||||
|
||||
1. **Email Auto Responder Flow**: This example demonstrates an infinite loop where a background job continually runs to automate email responses. It's a great use case for tasks that need to be performed repeatedly without manual intervention. [View Example](https://github.com/crewAIInc/crewAI-examples/tree/main/email_auto_responder_flow)
|
||||
|
||||
2. **Lead Score Flow**: This flow showcases adding human-in-the-loop feedback and handling different conditional branches using the router. It's an excellent example of how to incorporate dynamic decision-making and human oversight into your workflows. [View Example](https://github.com/crewAIInc/crewAI-examples/tree/main/lead-score-flow)
|
||||
|
||||
3. **Write a Book Flow**: This example excels at chaining multiple crews together, where the output of one crew is used by another. Specifically, one crew outlines an entire book, and another crew generates chapters based on the outline. Eventually, everything is connected to produce a complete book. This flow is perfect for complex, multi-step processes that require coordination between different tasks. [View Example](https://github.com/crewAIInc/crewAI-examples/tree/main/write_a_book_with_flows)
|
||||
|
||||
4. **Meeting Assistant Flow**: This flow demonstrates how to broadcast one event to trigger multiple follow-up actions. For instance, after a meeting is completed, the flow can update a Trello board, send a Slack message, and save the results. It's a great example of handling multiple outcomes from a single event, making it ideal for comprehensive task management and notification systems. [View Example](https://github.com/crewAIInc/crewAI-examples/tree/main/meeting_assistant_flow)
|
||||
|
||||
By exploring these examples, you can gain insights into how to leverage CrewAI Flows for various use cases, from automating repetitive tasks to managing complex, multi-step processes with dynamic decision-making and human feedback.
|
||||
|
||||
Also, check out our YouTube video on how to use flows in CrewAI below!
|
||||
|
||||
<iframe
|
||||
className="w-full aspect-video rounded-xl"
|
||||
src="https://www.youtube.com/embed/MTb5my6VOT8"
|
||||
title="CrewAI Flows overview"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
|
||||
## Running Flows
|
||||
|
||||
@@ -801,7 +946,7 @@ There are two ways to run a flow:
|
||||
You can run a flow programmatically by creating an instance of your flow class and calling the `kickoff()` method:
|
||||
|
||||
```python
|
||||
flow = SupportTriageFlow()
|
||||
flow = ExampleFlow()
|
||||
result = flow.kickoff()
|
||||
```
|
||||
|
||||
@@ -920,21 +1065,3 @@ crewai flow kickoff
|
||||
```
|
||||
|
||||
However, the `crewai run` command is now the preferred method as it works for both crews and flows.
|
||||
|
||||
## Common Failure Modes
|
||||
|
||||
### Router branch not firing
|
||||
- Cause: returned label does not match a `@listen("label")` value.
|
||||
- Fix: align router return strings with listener labels exactly.
|
||||
|
||||
### State fields missing at runtime
|
||||
- Cause: untyped dynamic fields or missing kickoff inputs.
|
||||
- Fix: use typed state and validate required fields in `@start()`.
|
||||
|
||||
### Prompt/token growth over time
|
||||
- Cause: appending unbounded message history in state.
|
||||
- Fix: apply sliding-window state and summary compaction patterns.
|
||||
|
||||
### Non-idempotent retries
|
||||
- Cause: side effects executed on retried steps.
|
||||
- Fix: add idempotency keys/markers to state and guard external writes.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -156,7 +156,6 @@ class ResearchFlow(Flow):
|
||||
```
|
||||
|
||||
See the [Flows documentation](/concepts/flows) for more on memory in Flows.
|
||||
For a production-style conversational pattern that combines Flow state and memory, see [Flowstate Chat History](/en/learn/flowstate-chat-history).
|
||||
|
||||
|
||||
## Hierarchical Scopes
|
||||
|
||||
@@ -10,17 +10,6 @@ mode: "wide"
|
||||
The planning feature in CrewAI allows you to add planning capability to your crew. When enabled, before each Crew iteration,
|
||||
all Crew information is sent to an AgentPlanner that will plan the tasks step by step, and this plan will be added to each task description.
|
||||
|
||||
## When to Use Planning
|
||||
|
||||
- Tasks require multi-step decomposition before execution.
|
||||
- You need more consistent execution quality on complex tasks.
|
||||
- You want transparent planning traces in crew runs.
|
||||
|
||||
## When Not to Use Planning
|
||||
|
||||
- Tasks are simple and deterministic.
|
||||
- Latency and token budget are strict and planning overhead is not justified.
|
||||
|
||||
### Using the Planning Feature
|
||||
|
||||
Getting started with the planning feature is very easy, the only step required is to add `planning=True` to your Crew:
|
||||
@@ -42,7 +31,7 @@ my_crew = Crew(
|
||||
From this point on, your crew will have planning enabled, and the tasks will be planned before each iteration.
|
||||
|
||||
<Warning>
|
||||
Planning model defaults can vary by version and environment. To avoid implicit provider dependencies, set `planning_llm` explicitly in your crew configuration.
|
||||
When planning is enabled, crewAI will use `gpt-4o-mini` as the default LLM for planning, which requires a valid OpenAI API key. Since your agents might be using different LLMs, this could cause confusion if you don't have an OpenAI API key configured or if you're experiencing unexpected behavior related to LLM API calls.
|
||||
</Warning>
|
||||
|
||||
#### Planning LLM
|
||||
@@ -163,14 +152,4 @@ A list with 10 bullet points of the most relevant information about AI LLMs.
|
||||
**Expected Output:**
|
||||
A fully fledged report with the main topics, each with a full section of information. Formatted as markdown without '```'.
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Common Failure Modes
|
||||
|
||||
### Planning adds cost/latency without quality gains
|
||||
- Cause: planning enabled for simple tasks.
|
||||
- Fix: disable `planning` for straightforward pipelines.
|
||||
|
||||
### Unexpected provider authentication errors
|
||||
- Cause: implicit planner model/provider assumptions.
|
||||
- Fix: set `planning_llm` explicitly and ensure matching credentials are configured.
|
||||
</CodeGroup>
|
||||
@@ -12,20 +12,11 @@ mode: "wide"
|
||||
These processes ensure tasks are distributed and executed efficiently, in alignment with a predefined strategy.
|
||||
</Tip>
|
||||
|
||||
## When to Use Each Process
|
||||
|
||||
- Use `sequential` when task order is fixed and outputs feed directly into the next task.
|
||||
- Use `hierarchical` when you need a manager to delegate and validate work dynamically.
|
||||
|
||||
## When Not to Use Hierarchical
|
||||
|
||||
- You do not need dynamic delegation.
|
||||
- You cannot provide a reliable `manager_llm` or `manager_agent`.
|
||||
|
||||
## Process Implementations
|
||||
|
||||
- **Sequential**: Executes tasks sequentially, ensuring tasks are completed in an orderly progression.
|
||||
- **Hierarchical**: Organizes tasks in a managerial hierarchy, where tasks are delegated and executed based on a structured chain of command. A manager language model (`manager_llm`) or a custom manager agent (`manager_agent`) must be specified in the crew to enable the hierarchical process, facilitating the creation and management of tasks by the manager.
|
||||
- **Consensual Process (Planned)**: Aiming for collaborative decision-making among agents on task execution, this process type introduces a democratic approach to task management within CrewAI. It is planned for future development and is not currently implemented in the codebase.
|
||||
|
||||
## The Role of Processes in Teamwork
|
||||
Processes enable individual agents to operate as a cohesive unit, streamlining their efforts to achieve common objectives with efficiency and coherence.
|
||||
@@ -68,17 +59,9 @@ Emulates a corporate hierarchy, CrewAI allows specifying a custom manager agent
|
||||
|
||||
## Process Class: Detailed Overview
|
||||
|
||||
The `Process` class is implemented as an enumeration (`Enum`), ensuring type safety and restricting process values to the defined types (`sequential`, `hierarchical`).
|
||||
The `Process` class is implemented as an enumeration (`Enum`), ensuring type safety and restricting process values to the defined types (`sequential`, `hierarchical`). The consensual process is planned for future inclusion, emphasizing our commitment to continuous development and innovation.
|
||||
|
||||
## Conclusion
|
||||
|
||||
The structured collaboration facilitated by processes within CrewAI is crucial for enabling systematic teamwork among agents.
|
||||
## Common Failure Modes
|
||||
|
||||
### Hierarchical process fails at startup
|
||||
- Cause: missing `manager_llm` or `manager_agent`.
|
||||
- Fix: provide one of them explicitly in crew configuration.
|
||||
|
||||
### Sequential process produces weak outputs
|
||||
- Cause: task boundaries/context are underspecified.
|
||||
- Fix: improve task descriptions, expected outputs, and task context chaining.
|
||||
This documentation has been updated to reflect the latest features, enhancements, and the planned integration of the Consensual Process, ensuring users have access to the most current and comprehensive information.
|
||||
@@ -9,20 +9,9 @@ mode: "wide"
|
||||
|
||||
Testing is a crucial part of the development process, and it is essential to ensure that your crew is performing as expected. With crewAI, you can easily test your crew and evaluate its performance using the built-in testing capabilities.
|
||||
|
||||
## When to Use Testing
|
||||
|
||||
- Before promoting a crew to production.
|
||||
- After changing prompts, tools, or model configurations.
|
||||
- When benchmarking quality/cost/latency tradeoffs.
|
||||
|
||||
## When Not to Rely on Testing Alone
|
||||
|
||||
- For safety-critical deployments without human review gates.
|
||||
- When test datasets are too small or unrepresentative.
|
||||
|
||||
### Using the Testing Feature
|
||||
|
||||
Use the CLI command `crewai test` to run repeated crew executions and compare outputs across iterations. The parameters are `n_iterations` and `model`, which are optional and default to `2` and `gpt-4o-mini`.
|
||||
We added the CLI command `crewai test` to make it easy to test your crew. This command will run your crew for a specified number of iterations and provide detailed performance metrics. The parameters are `n_iterations` and `model`, which are optional and default to 2 and `gpt-4o-mini` respectively. For now, the only provider available is OpenAI.
|
||||
|
||||
```bash
|
||||
crewai test
|
||||
@@ -58,13 +47,3 @@ A table of scores at the end will show the performance of the crew in terms of t
|
||||
| Execution Time (s) | 126 | 145 | **135** | | |
|
||||
|
||||
The example above shows the test results for two runs of the crew with two tasks, with the average total score for each task and the crew as a whole.
|
||||
|
||||
## Common Failure Modes
|
||||
|
||||
### Scores fluctuate too much between runs
|
||||
- Cause: high sampling randomness or unstable prompts.
|
||||
- Fix: lower temperature and tighten output constraints.
|
||||
|
||||
### Good test scores but poor production quality
|
||||
- Cause: test prompts do not match real workload.
|
||||
- Fix: build a representative test set from real production inputs.
|
||||
|
||||
@@ -10,17 +10,6 @@ mode: "wide"
|
||||
CrewAI tools empower agents with capabilities ranging from web searching and data analysis to collaboration and delegating tasks among coworkers.
|
||||
This documentation outlines how to create, integrate, and leverage these tools within the CrewAI framework, including a new focus on collaboration tools.
|
||||
|
||||
## When to Use Tools
|
||||
|
||||
- Agents need external data or side effects.
|
||||
- You need deterministic actions wrapped in reusable interfaces.
|
||||
- You need to connect APIs, files, databases, or browser actions into agent workflows.
|
||||
|
||||
## When Not to Use Tools
|
||||
|
||||
- The task can be solved entirely from prompt context.
|
||||
- The external side effect cannot be made safe or idempotent.
|
||||
|
||||
## What is a Tool?
|
||||
|
||||
A tool in CrewAI is a skill or function that agents can utilize to perform various actions.
|
||||
@@ -296,17 +285,3 @@ writer1 = Agent(
|
||||
Tools are pivotal in extending the capabilities of CrewAI agents, enabling them to undertake a broad spectrum of tasks and collaborate effectively.
|
||||
When building solutions with CrewAI, leverage both custom and existing tools to empower your agents and enhance the AI ecosystem. Consider utilizing error handling,
|
||||
caching mechanisms, and the flexibility of tool arguments to optimize your agents' performance and capabilities.
|
||||
|
||||
## Common Failure Modes
|
||||
|
||||
### Tool schema mismatch
|
||||
- Cause: model-generated arguments do not match tool signature.
|
||||
- Fix: tighten tool descriptions and validate input schemas.
|
||||
|
||||
### Repeated side effects
|
||||
- Cause: retries trigger duplicate writes/actions.
|
||||
- Fix: add idempotency keys and deduplication checks in tool logic.
|
||||
|
||||
### Tool timeouts under load
|
||||
- Cause: unbounded retries or slow external services.
|
||||
- Fix: set explicit timeout/retry policy and graceful fallbacks.
|
||||
|
||||
@@ -177,6 +177,11 @@ You need to push your crew to a GitHub repository. If you haven't created a crew
|
||||

|
||||
</Frame>
|
||||
|
||||
<Info>
|
||||
Using private Python packages? You'll need to add your registry credentials here too.
|
||||
See [Private Package Registries](/en/enterprise/guides/private-package-registry) for the required variables.
|
||||
</Info>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Deploy Your Crew">
|
||||
|
||||
@@ -256,6 +256,12 @@ Before deployment, ensure you have:
|
||||
1. **LLM API keys** ready (OpenAI, Anthropic, Google, etc.)
|
||||
2. **Tool API keys** if using external tools (Serper, etc.)
|
||||
|
||||
<Info>
|
||||
If your project depends on packages from a **private PyPI registry**, you'll also need to configure
|
||||
registry authentication credentials as environment variables. See the
|
||||
[Private Package Registries](/en/enterprise/guides/private-package-registry) guide for details.
|
||||
</Info>
|
||||
|
||||
<Tip>
|
||||
Test your project locally with the same environment variables before deploying
|
||||
to catch configuration issues early.
|
||||
|
||||
263
docs/en/enterprise/guides/private-package-registry.mdx
Normal file
263
docs/en/enterprise/guides/private-package-registry.mdx
Normal file
@@ -0,0 +1,263 @@
|
||||
---
|
||||
title: "Private Package Registries"
|
||||
description: "Install private Python packages from authenticated PyPI registries in CrewAI AMP"
|
||||
icon: "lock"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Note>
|
||||
This guide covers how to configure your CrewAI project to install Python packages
|
||||
from private PyPI registries (Azure DevOps Artifacts, GitHub Packages, GitLab, AWS CodeArtifact, etc.)
|
||||
when deploying to CrewAI AMP.
|
||||
</Note>
|
||||
|
||||
## When You Need This
|
||||
|
||||
If your project depends on internal or proprietary Python packages hosted on a private registry
|
||||
rather than the public PyPI, you'll need to:
|
||||
|
||||
1. Tell UV **where** to find the package (an index URL)
|
||||
2. Tell UV **which** packages come from that index (a source mapping)
|
||||
3. Provide **credentials** so UV can authenticate during install
|
||||
|
||||
CrewAI AMP uses [UV](https://docs.astral.sh/uv/) for dependency resolution and installation.
|
||||
UV supports authenticated private registries through `pyproject.toml` configuration combined
|
||||
with environment variables for credentials.
|
||||
|
||||
## Step 1: Configure pyproject.toml
|
||||
|
||||
Three pieces work together in your `pyproject.toml`:
|
||||
|
||||
### 1a. Declare the dependency
|
||||
|
||||
Add the private package to your `[project.dependencies]` like any other dependency:
|
||||
|
||||
```toml
|
||||
[project]
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.100.1,<1.0.0",
|
||||
"my-private-package>=1.2.0",
|
||||
]
|
||||
```
|
||||
|
||||
### 1b. Define the index
|
||||
|
||||
Register your private registry as a named index under `[[tool.uv.index]]`:
|
||||
|
||||
```toml
|
||||
[[tool.uv.index]]
|
||||
name = "my-private-registry"
|
||||
url = "https://pkgs.dev.azure.com/my-org/_packaging/my-feed/pypi/simple/"
|
||||
explicit = true
|
||||
```
|
||||
|
||||
<Info>
|
||||
The `name` field is important — UV uses it to construct the environment variable names
|
||||
for authentication (see [Step 2](#step-2-set-authentication-credentials) below).
|
||||
|
||||
Setting `explicit = true` means UV won't search this index for every package — only the
|
||||
ones you explicitly map to it in `[tool.uv.sources]`. This avoids unnecessary queries
|
||||
against your private registry and protects against dependency confusion attacks.
|
||||
</Info>
|
||||
|
||||
### 1c. Map the package to the index
|
||||
|
||||
Tell UV which packages should be resolved from your private index using `[tool.uv.sources]`:
|
||||
|
||||
```toml
|
||||
[tool.uv.sources]
|
||||
my-private-package = { index = "my-private-registry" }
|
||||
```
|
||||
|
||||
### Complete example
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "my-crew-project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.10,<=3.13"
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.100.1,<1.0.0",
|
||||
"my-private-package>=1.2.0",
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "my-private-registry"
|
||||
url = "https://pkgs.dev.azure.com/my-org/_packaging/my-feed/pypi/simple/"
|
||||
explicit = true
|
||||
|
||||
[tool.uv.sources]
|
||||
my-private-package = { index = "my-private-registry" }
|
||||
```
|
||||
|
||||
After updating `pyproject.toml`, regenerate your lock file:
|
||||
|
||||
```bash
|
||||
uv lock
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Always commit the updated `uv.lock` along with your `pyproject.toml` changes.
|
||||
The lock file is required for deployment — see [Prepare for Deployment](/en/enterprise/guides/prepare-for-deployment).
|
||||
</Warning>
|
||||
|
||||
## Step 2: Set Authentication Credentials
|
||||
|
||||
UV authenticates against private indexes using environment variables that follow a naming convention
|
||||
based on the index name you defined in `pyproject.toml`:
|
||||
|
||||
```
|
||||
UV_INDEX_{UPPER_NAME}_USERNAME
|
||||
UV_INDEX_{UPPER_NAME}_PASSWORD
|
||||
```
|
||||
|
||||
Where `{UPPER_NAME}` is your index name converted to **uppercase** with **hyphens replaced by underscores**.
|
||||
|
||||
For example, an index named `my-private-registry` uses:
|
||||
|
||||
| Variable | Value |
|
||||
|----------|-------|
|
||||
| `UV_INDEX_MY_PRIVATE_REGISTRY_USERNAME` | Your registry username or token name |
|
||||
| `UV_INDEX_MY_PRIVATE_REGISTRY_PASSWORD` | Your registry password or token/PAT |
|
||||
|
||||
<Warning>
|
||||
These environment variables **must** be added via the CrewAI AMP **Environment Variables** settings —
|
||||
either globally or at the deployment level. They cannot be set in `.env` files or hardcoded in your project.
|
||||
|
||||
See [Setting Environment Variables in AMP](#setting-environment-variables-in-amp) below.
|
||||
</Warning>
|
||||
|
||||
## Registry Provider Reference
|
||||
|
||||
The table below shows the index URL format and credential values for common registry providers.
|
||||
Replace placeholder values with your actual organization and feed details.
|
||||
|
||||
| Provider | Index URL | Username | Password |
|
||||
|----------|-----------|----------|----------|
|
||||
| **Azure DevOps Artifacts** | `https://pkgs.dev.azure.com/{org}/_packaging/{feed}/pypi/simple/` | Any non-empty string (e.g. `token`) | Personal Access Token (PAT) with Packaging Read scope |
|
||||
| **GitHub Packages** | `https://pypi.pkg.github.com/{owner}/simple/` | GitHub username | Personal Access Token (classic) with `read:packages` scope |
|
||||
| **GitLab Package Registry** | `https://gitlab.com/api/v4/projects/{project_id}/packages/pypi/simple/` | `__token__` | Project or Personal Access Token with `read_api` scope |
|
||||
| **AWS CodeArtifact** | Use the URL from `aws codeartifact get-repository-endpoint` | `aws` | Token from `aws codeartifact get-authorization-token` |
|
||||
| **Google Artifact Registry** | `https://{region}-python.pkg.dev/{project}/{repo}/simple/` | `_json_key_base64` | Base64-encoded service account key |
|
||||
| **JFrog Artifactory** | `https://{instance}.jfrog.io/artifactory/api/pypi/{repo}/simple/` | Username or email | API key or identity token |
|
||||
| **Self-hosted (devpi, Nexus, etc.)** | Your registry's simple API URL | Registry username | Registry password |
|
||||
|
||||
<Tip>
|
||||
For **AWS CodeArtifact**, the authorization token expires periodically.
|
||||
You'll need to refresh the `UV_INDEX_*_PASSWORD` value when it expires.
|
||||
Consider automating this in your CI/CD pipeline.
|
||||
</Tip>
|
||||
|
||||
## Setting Environment Variables in AMP
|
||||
|
||||
Private registry credentials must be configured as environment variables in CrewAI AMP.
|
||||
You have two options:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Web Interface">
|
||||
1. Log in to [CrewAI AMP](https://app.crewai.com)
|
||||
2. Navigate to your automation
|
||||
3. Open the **Environment Variables** tab
|
||||
4. Add each variable (`UV_INDEX_*_USERNAME` and `UV_INDEX_*_PASSWORD`) with its value
|
||||
|
||||
See the [Deploy to AMP — Set Environment Variables](/en/enterprise/guides/deploy-to-amp#set-environment-variables) step for details.
|
||||
</Tab>
|
||||
<Tab title="CLI Deployment">
|
||||
Add the variables to your local `.env` file before running `crewai deploy create`.
|
||||
The CLI will securely transfer them to the platform:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
OPENAI_API_KEY=sk-...
|
||||
UV_INDEX_MY_PRIVATE_REGISTRY_USERNAME=token
|
||||
UV_INDEX_MY_PRIVATE_REGISTRY_PASSWORD=your-pat-here
|
||||
```
|
||||
|
||||
```bash
|
||||
crewai deploy create
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Warning>
|
||||
**Never** commit credentials to your repository. Use AMP environment variables for all secrets.
|
||||
The `.env` file should be listed in `.gitignore`.
|
||||
</Warning>
|
||||
|
||||
To update credentials on an existing deployment, see [Update Your Crew — Environment Variables](/en/enterprise/guides/update-crew).
|
||||
|
||||
## How It All Fits Together
|
||||
|
||||
When CrewAI AMP builds your automation, the resolution flow works like this:
|
||||
|
||||
<Steps>
|
||||
<Step title="Build starts">
|
||||
AMP pulls your repository and reads `pyproject.toml` and `uv.lock`.
|
||||
</Step>
|
||||
<Step title="UV resolves dependencies">
|
||||
UV reads `[tool.uv.sources]` to determine which index each package should come from.
|
||||
</Step>
|
||||
<Step title="UV authenticates">
|
||||
For each private index, UV looks up `UV_INDEX_{NAME}_USERNAME` and `UV_INDEX_{NAME}_PASSWORD`
|
||||
from the environment variables you configured in AMP.
|
||||
</Step>
|
||||
<Step title="Packages install">
|
||||
UV downloads and installs all packages — both public (from PyPI) and private (from your registry).
|
||||
</Step>
|
||||
<Step title="Automation runs">
|
||||
Your crew or flow starts with all dependencies available.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication Errors During Build
|
||||
|
||||
**Symptom**: Build fails with `401 Unauthorized` or `403 Forbidden` when resolving a private package.
|
||||
|
||||
**Check**:
|
||||
- The `UV_INDEX_*` environment variable names match your index name exactly (uppercased, hyphens → underscores)
|
||||
- Credentials are set in AMP environment variables, not just in a local `.env`
|
||||
- Your token/PAT has the required read permissions for the package feed
|
||||
- The token hasn't expired (especially relevant for AWS CodeArtifact)
|
||||
|
||||
### Package Not Found
|
||||
|
||||
**Symptom**: `No matching distribution found for my-private-package`.
|
||||
|
||||
**Check**:
|
||||
- The index URL in `pyproject.toml` ends with `/simple/`
|
||||
- The `[tool.uv.sources]` entry maps the correct package name to the correct index name
|
||||
- The package is actually published to your private registry
|
||||
- Run `uv lock` locally with the same credentials to verify resolution works
|
||||
|
||||
### Lock File Conflicts
|
||||
|
||||
**Symptom**: `uv lock` fails or produces unexpected results after adding a private index.
|
||||
|
||||
**Solution**: Set the credentials locally and regenerate:
|
||||
|
||||
```bash
|
||||
export UV_INDEX_MY_PRIVATE_REGISTRY_USERNAME=token
|
||||
export UV_INDEX_MY_PRIVATE_REGISTRY_PASSWORD=your-pat
|
||||
uv lock
|
||||
```
|
||||
|
||||
Then commit the updated `uv.lock`.
|
||||
|
||||
## Related Guides
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Prepare for Deployment" icon="clipboard-check" href="/en/enterprise/guides/prepare-for-deployment">
|
||||
Verify project structure and dependencies before deploying.
|
||||
</Card>
|
||||
<Card title="Deploy to AMP" icon="rocket" href="/en/enterprise/guides/deploy-to-amp">
|
||||
Deploy your crew or flow and configure environment variables.
|
||||
</Card>
|
||||
<Card title="Update Your Crew" icon="arrows-rotate" href="/en/enterprise/guides/update-crew">
|
||||
Update environment variables and push changes to a running deployment.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -8,10 +8,6 @@ mode: "wide"
|
||||
## Quickstarts & Demos
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Flowstate Chat History" icon="comments" href="/en/learn/flowstate-chat-history">
|
||||
Manage chat sessions with sliding-window history, summary compaction, and persisted Flow state.
|
||||
</Card>
|
||||
|
||||
<Card title="Collaboration" icon="people-arrows" href="https://github.com/crewAIInc/crewAI-quickstarts/blob/main/Collaboration/crewai_collaboration.ipynb">
|
||||
Coordinate multiple agents on shared tasks. Includes notebook with end-to-end collaboration pattern.
|
||||
</Card>
|
||||
|
||||
@@ -34,10 +34,6 @@ mode: "wide"
|
||||
## Flows
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Flowstate Chat History" icon="comments" href="/en/learn/flowstate-chat-history">
|
||||
Stateful chat pattern with compacted context and persisted session state.
|
||||
</Card>
|
||||
|
||||
<Card title="Content Creator Flow" icon="pen" href="https://github.com/crewAIInc/crewAI-examples/tree/main/flows/content_creator_flow">
|
||||
Multi‑crew content generation with routing.
|
||||
</Card>
|
||||
|
||||
@@ -47,23 +47,6 @@ CrewAI offers two ways to manage state in your flows:
|
||||
|
||||
Let's examine each approach in detail.
|
||||
|
||||
### Flow State vs Memory: When to use each
|
||||
|
||||
Both features keep context, but they solve different problems.
|
||||
|
||||
| Dimension | Flow State (`self.state`) | Memory (`self.remember` / `self.recall`) |
|
||||
|---|---|---|
|
||||
| Primary purpose | Track execution and deterministic workflow data | Store and retrieve semantic knowledge across interactions |
|
||||
| Data shape | Explicit fields (dict/Pydantic model) | Text records with inferred scopes and ranked recall |
|
||||
| Typical lifetime | Current flow run (or persisted checkpoints) | Long-term knowledge over many runs |
|
||||
| Access pattern | Direct reads/writes (`self.state.field`) | Query-based retrieval (`self.recall("...")`) |
|
||||
| Best for | Routing flags, counters, intermediate outputs, chat window | Durable facts, prior outcomes, reusable context |
|
||||
| Chat use | Recent turns + running summary + control flags | Long-tail memory outside context window |
|
||||
|
||||
Practical rule:
|
||||
- Use **state** for what your control flow depends on right now.
|
||||
- Use **memory** for what you may want to retrieve later by meaning.
|
||||
|
||||
## Unstructured State Management
|
||||
|
||||
Unstructured state uses a dictionary-like approach, offering flexibility and simplicity for straightforward applications.
|
||||
|
||||
518
docs/en/guides/migration/migrating-from-langgraph.mdx
Normal file
518
docs/en/guides/migration/migrating-from-langgraph.mdx
Normal file
@@ -0,0 +1,518 @@
|
||||
---
|
||||
title: "Moving from LangGraph to CrewAI: A Practical Guide for Engineers"
|
||||
description: If you already have built with LangGraph, learn how to quickly port your projects to CrewAI
|
||||
icon: switch
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
You've built agents with LangGraph. You've wrestled with `StateGraph`, wired up conditional edges, and debugged state dictionaries at 2 AM. It works — but somewhere along the way, you started wondering if there's a better path to production.
|
||||
|
||||
There is. **CrewAI Flows** gives you the same power — event-driven orchestration, conditional routing, shared state — with dramatically less boilerplate and a mental model that maps cleanly to how you actually think about multi-step AI workflows.
|
||||
|
||||
This article walks through the core concepts side by side, shows real code comparisons, and demonstrates why CrewAI Flows is the framework you'll want to reach for next.
|
||||
|
||||
---
|
||||
|
||||
## The Mental Model Shift
|
||||
|
||||
LangGraph asks you to think in **graphs**: nodes, edges, and state dictionaries. Every workflow is a directed graph where you explicitly wire transitions between computation steps. It's powerful, but the abstraction carries overhead — especially when your workflow is fundamentally sequential with a few decision points.
|
||||
|
||||
CrewAI Flows asks you to think in **events**: methods that start things, methods that listen for results, and methods that route execution. The topology of your workflow emerges from decorator annotations rather than explicit graph construction. This isn't just syntactic sugar — it changes how you design, read, and maintain your pipelines.
|
||||
|
||||
Here's the core mapping:
|
||||
|
||||
| LangGraph Concept | CrewAI Flows Equivalent |
|
||||
| --- | --- |
|
||||
| `StateGraph` class | `Flow` class |
|
||||
| `add_node()` | Methods decorated with `@start`, `@listen` |
|
||||
| `add_edge()` / `add_conditional_edges()` | `@listen()` / `@router()` decorators |
|
||||
| `TypedDict` state | Pydantic `BaseModel` state |
|
||||
| `START` / `END` constants | `@start()` decorator / natural method return |
|
||||
| `graph.compile()` | `flow.kickoff()` |
|
||||
| Checkpointer / persistence | Built-in memory (LanceDB-backed) |
|
||||
|
||||
Let's see what this looks like in practice.
|
||||
|
||||
---
|
||||
|
||||
## Demo 1: A Simple Sequential Pipeline
|
||||
|
||||
Imagine you're building a pipeline that takes a topic, researches it, writes a summary, and formats the output. Here's how each framework handles it.
|
||||
|
||||
### LangGraph Approach
|
||||
|
||||
```python
|
||||
from typing import TypedDict
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
|
||||
class ResearchState(TypedDict):
|
||||
topic: str
|
||||
raw_research: str
|
||||
summary: str
|
||||
formatted_output: str
|
||||
|
||||
def research_topic(state: ResearchState) -> dict:
|
||||
# Call an LLM or search API
|
||||
result = llm.invoke(f"Research the topic: {state['topic']}")
|
||||
return {"raw_research": result}
|
||||
|
||||
def write_summary(state: ResearchState) -> dict:
|
||||
result = llm.invoke(
|
||||
f"Summarize this research:\n{state['raw_research']}"
|
||||
)
|
||||
return {"summary": result}
|
||||
|
||||
def format_output(state: ResearchState) -> dict:
|
||||
result = llm.invoke(
|
||||
f"Format this summary as a polished article section:\n{state['summary']}"
|
||||
)
|
||||
return {"formatted_output": result}
|
||||
|
||||
# Build the graph
|
||||
graph = StateGraph(ResearchState)
|
||||
graph.add_node("research", research_topic)
|
||||
graph.add_node("summarize", write_summary)
|
||||
graph.add_node("format", format_output)
|
||||
|
||||
graph.add_edge(START, "research")
|
||||
graph.add_edge("research", "summarize")
|
||||
graph.add_edge("summarize", "format")
|
||||
graph.add_edge("format", END)
|
||||
|
||||
# Compile and run
|
||||
app = graph.compile()
|
||||
result = app.invoke({"topic": "quantum computing advances in 2026"})
|
||||
print(result["formatted_output"])
|
||||
```
|
||||
|
||||
You define functions, register them as nodes, and manually wire every transition. For a simple sequence like this, there's a lot of ceremony.
|
||||
|
||||
### CrewAI Flows Approach
|
||||
|
||||
```python
|
||||
from crewai import LLM, Agent, Crew, Process, Task
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class ResearchState(BaseModel):
|
||||
topic: str = ""
|
||||
raw_research: str = ""
|
||||
summary: str = ""
|
||||
formatted_output: str = ""
|
||||
|
||||
class ResearchFlow(Flow[ResearchState]):
|
||||
@start()
|
||||
def research_topic(self):
|
||||
# Option 1: Direct LLM call
|
||||
result = llm.call(f"Research the topic: {self.state.topic}")
|
||||
self.state.raw_research = result
|
||||
return result
|
||||
|
||||
@listen(research_topic)
|
||||
def write_summary(self, research_output):
|
||||
# Option 2: A single agent
|
||||
summarizer = Agent(
|
||||
role="Research Summarizer",
|
||||
goal="Produce concise, accurate summaries of research content",
|
||||
backstory="You are an expert at distilling complex research into clear, "
|
||||
"digestible summaries.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
result = summarizer.kickoff(
|
||||
f"Summarize this research:\n{self.state.raw_research}"
|
||||
)
|
||||
self.state.summary = str(result)
|
||||
return self.state.summary
|
||||
|
||||
@listen(write_summary)
|
||||
def format_output(self, summary_output):
|
||||
# Option 3: a complete crew (with one or more agents)
|
||||
formatter = Agent(
|
||||
role="Content Formatter",
|
||||
goal="Transform research summaries into polished, publication-ready article sections",
|
||||
backstory="You are a skilled editor with expertise in structuring and "
|
||||
"presenting technical content for a general audience.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
format_task = Task(
|
||||
description=f"Format this summary as a polished article section:\n{self.state.summary}",
|
||||
expected_output="A well-structured, polished article section ready for publication.",
|
||||
agent=formatter,
|
||||
)
|
||||
crew = Crew(
|
||||
agents=[formatter],
|
||||
tasks=[format_task],
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
result = crew.kickoff()
|
||||
self.state.formatted_output = str(result)
|
||||
return self.state.formatted_output
|
||||
|
||||
# Run the flow
|
||||
flow = ResearchFlow()
|
||||
flow.state.topic = "quantum computing advances in 2026"
|
||||
result = flow.kickoff()
|
||||
print(flow.state.formatted_output)
|
||||
|
||||
```
|
||||
|
||||
Notice what's different: no graph construction, no edge wiring, no compile step. The execution order is declared right where the logic lives. `@start()` marks the entry point, and `@listen(method_name)` chains steps together. The state is a proper Pydantic model with type safety, validation, and IDE auto-completion.
|
||||
|
||||
---
|
||||
|
||||
## Demo 2: Conditional Routing
|
||||
|
||||
This is where things get interesting. Say you're building a content pipeline that routes to different processing paths based on the type of content detected.
|
||||
|
||||
### LangGraph Approach
|
||||
|
||||
```python
|
||||
from typing import TypedDict, Literal
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
|
||||
class ContentState(TypedDict):
|
||||
input_text: str
|
||||
content_type: str
|
||||
result: str
|
||||
|
||||
def classify_content(state: ContentState) -> dict:
|
||||
content_type = llm.invoke(
|
||||
f"Classify this content as 'technical', 'creative', or 'business':\n{state['input_text']}"
|
||||
)
|
||||
return {"content_type": content_type.strip().lower()}
|
||||
|
||||
def process_technical(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as technical doc:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
def process_creative(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as creative writing:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
def process_business(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as business content:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
# Routing function
|
||||
def route_content(state: ContentState) -> Literal["technical", "creative", "business"]:
|
||||
return state["content_type"]
|
||||
|
||||
# Build the graph
|
||||
graph = StateGraph(ContentState)
|
||||
graph.add_node("classify", classify_content)
|
||||
graph.add_node("technical", process_technical)
|
||||
graph.add_node("creative", process_creative)
|
||||
graph.add_node("business", process_business)
|
||||
|
||||
graph.add_edge(START, "classify")
|
||||
graph.add_conditional_edges(
|
||||
"classify",
|
||||
route_content,
|
||||
{
|
||||
"technical": "technical",
|
||||
"creative": "creative",
|
||||
"business": "business",
|
||||
}
|
||||
)
|
||||
graph.add_edge("technical", END)
|
||||
graph.add_edge("creative", END)
|
||||
graph.add_edge("business", END)
|
||||
|
||||
app = graph.compile()
|
||||
result = app.invoke({"input_text": "Explain how TCP handshakes work"})
|
||||
```
|
||||
|
||||
You need a separate routing function, explicit conditional edge mapping, and termination edges for every branch. The routing logic is decoupled from the node that produces the routing decision.
|
||||
|
||||
### CrewAI Flows Approach
|
||||
|
||||
```python
|
||||
from crewai import LLM, Agent
|
||||
from crewai.flow.flow import Flow, listen, router, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class ContentState(BaseModel):
|
||||
input_text: str = ""
|
||||
content_type: str = ""
|
||||
result: str = ""
|
||||
|
||||
class ContentFlow(Flow[ContentState]):
|
||||
@start()
|
||||
def classify_content(self):
|
||||
self.state.content_type = (
|
||||
llm.call(
|
||||
f"Classify this content as 'technical', 'creative', or 'business':\n"
|
||||
f"{self.state.input_text}"
|
||||
)
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
return self.state.content_type
|
||||
|
||||
@router(classify_content)
|
||||
def route_content(self, classification):
|
||||
if classification == "technical":
|
||||
return "process_technical"
|
||||
elif classification == "creative":
|
||||
return "process_creative"
|
||||
else:
|
||||
return "process_business"
|
||||
|
||||
@listen("process_technical")
|
||||
def handle_technical(self):
|
||||
agent = Agent(
|
||||
role="Technical Writer",
|
||||
goal="Produce clear, accurate technical documentation",
|
||||
backstory="You are an expert technical writer who specializes in "
|
||||
"explaining complex technical concepts precisely.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as technical doc:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
@listen("process_creative")
|
||||
def handle_creative(self):
|
||||
agent = Agent(
|
||||
role="Creative Writer",
|
||||
goal="Craft engaging and imaginative creative content",
|
||||
backstory="You are a talented creative writer with a flair for "
|
||||
"compelling storytelling and vivid expression.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as creative writing:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
@listen("process_business")
|
||||
def handle_business(self):
|
||||
agent = Agent(
|
||||
role="Business Writer",
|
||||
goal="Produce professional, results-oriented business content",
|
||||
backstory="You are an experienced business writer who communicates "
|
||||
"strategy and value clearly to professional audiences.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as business content:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
flow = ContentFlow()
|
||||
flow.state.input_text = "Explain how TCP handshakes work"
|
||||
flow.kickoff()
|
||||
print(flow.state.result)
|
||||
|
||||
```
|
||||
|
||||
The `@router()` decorator turns a method into a decision point. It returns a string that matches a listener — no mapping dictionaries, no separate routing functions. The branching logic reads like a Python `if` statement because it *is* one.
|
||||
|
||||
---
|
||||
|
||||
## Demo 3: Integrating AI Agent Crews into Flows
|
||||
|
||||
Here's where CrewAI's real power shines. Flows aren't just for chaining LLM calls — they orchestrate full **Crews** of autonomous agents. This is something LangGraph simply doesn't have a native equivalent for.
|
||||
|
||||
```python
|
||||
from crewai import Agent, Task, Crew
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ArticleState(BaseModel):
|
||||
topic: str = ""
|
||||
research: str = ""
|
||||
draft: str = ""
|
||||
final_article: str = ""
|
||||
|
||||
class ArticleFlow(Flow[ArticleState]):
|
||||
|
||||
@start()
|
||||
def run_research_crew(self):
|
||||
"""A full Crew of agents handles research."""
|
||||
researcher = Agent(
|
||||
role="Senior Research Analyst",
|
||||
goal=f"Produce comprehensive research on: {self.state.topic}",
|
||||
backstory="You're a veteran analyst known for thorough, "
|
||||
"well-sourced research reports.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
research_task = Task(
|
||||
description=f"Research '{self.state.topic}' thoroughly. "
|
||||
"Cover key trends, data points, and expert opinions.",
|
||||
expected_output="A detailed research brief with sources.",
|
||||
agent=researcher
|
||||
)
|
||||
|
||||
crew = Crew(agents=[researcher], tasks=[research_task])
|
||||
result = crew.kickoff()
|
||||
self.state.research = result.raw
|
||||
return result.raw
|
||||
|
||||
@listen(run_research_crew)
|
||||
def run_writing_crew(self, research_output):
|
||||
"""A different Crew handles writing."""
|
||||
writer = Agent(
|
||||
role="Technical Writer",
|
||||
goal="Write a compelling article based on provided research.",
|
||||
backstory="You turn complex research into engaging, clear prose.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
editor = Agent(
|
||||
role="Senior Editor",
|
||||
goal="Review and polish articles for publication quality.",
|
||||
backstory="20 years of editorial experience at top tech publications.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
write_task = Task(
|
||||
description=f"Write an article based on this research:\n{self.state.research}",
|
||||
expected_output="A well-structured draft article.",
|
||||
agent=writer
|
||||
)
|
||||
|
||||
edit_task = Task(
|
||||
description="Review, fact-check, and polish the draft article.",
|
||||
expected_output="A publication-ready article.",
|
||||
agent=editor
|
||||
)
|
||||
|
||||
crew = Crew(agents=[writer, editor], tasks=[write_task, edit_task])
|
||||
result = crew.kickoff()
|
||||
self.state.final_article = result.raw
|
||||
return result.raw
|
||||
|
||||
# Run the full pipeline
|
||||
flow = ArticleFlow()
|
||||
flow.state.topic = "The Future of Edge AI"
|
||||
flow.kickoff()
|
||||
print(flow.state.final_article)
|
||||
```
|
||||
|
||||
This is the key insight: **Flows provide the orchestration layer, and Crews provide the intelligence layer.** Each step in a Flow can spin up a full team of collaborating agents, each with their own roles, goals, and tools. You get structured, predictable control flow *and* autonomous agent collaboration — the best of both worlds.
|
||||
|
||||
In LangGraph, achieving something similar means manually implementing agent communication protocols, tool-calling loops, and delegation logic inside your node functions. It's possible, but it's plumbing you're building from scratch every time.
|
||||
|
||||
---
|
||||
|
||||
## Demo 4: Parallel Execution and Synchronization
|
||||
|
||||
Real-world pipelines often need to fan out work and join the results. CrewAI Flows handles this elegantly with `and_` and `or_` operators.
|
||||
|
||||
```python
|
||||
from crewai import LLM
|
||||
from crewai.flow.flow import Flow, and_, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class AnalysisState(BaseModel):
|
||||
topic: str = ""
|
||||
market_data: str = ""
|
||||
tech_analysis: str = ""
|
||||
competitor_intel: str = ""
|
||||
final_report: str = ""
|
||||
|
||||
class ParallelAnalysisFlow(Flow[AnalysisState]):
|
||||
@start()
|
||||
def start_method(self):
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def gather_market_data(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def run_tech_analysis(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def gather_competitor_intel(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(and_(gather_market_data, run_tech_analysis, gather_competitor_intel))
|
||||
def synthesize_report(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
flow = ParallelAnalysisFlow()
|
||||
flow.state.topic = "AI-powered developer tools"
|
||||
flow.kickoff()
|
||||
|
||||
```
|
||||
|
||||
Multiple `@start()` decorators fire in parallel. The `and_()` combinator on the `@listen` decorator ensures `synthesize_report` only executes after *all three* upstream methods complete. There's also `or_()` for when you want to proceed as soon as *any* upstream task finishes.
|
||||
|
||||
In LangGraph, you'd need to build a fan-out/fan-in pattern with parallel branches, a synchronization node, and careful state merging — all wired explicitly through edges.
|
||||
|
||||
---
|
||||
|
||||
## Why CrewAI Flows for Production
|
||||
|
||||
Beyond cleaner syntax, Flows deliver several production-critical advantages:
|
||||
|
||||
**Built-in state persistence.** Flow state is backed by LanceDB, meaning your workflows can survive crashes, be resumed, and accumulate knowledge across runs. LangGraph requires you to configure a separate checkpointer.
|
||||
|
||||
**Type-safe state management.** Pydantic models give you validation, serialization, and IDE support out of the box. LangGraph's `TypedDict` states don't validate at runtime.
|
||||
|
||||
**First-class agent orchestration.** Crews are a native primitive. You define agents with roles, goals, backstories, and tools — and they collaborate autonomously within the structured envelope of a Flow. No need to reinvent multi-agent coordination.
|
||||
|
||||
**Simpler mental model.** Decorators declare intent. `@start` means "begin here." `@listen(x)` means "run after x." `@router(x)` means "decide where to go after x." The code reads like the workflow it describes.
|
||||
|
||||
**CLI integration.** Run flows with `crewai run`. No separate compilation step, no graph serialization. Your Flow is a Python class, and it runs like one.
|
||||
|
||||
---
|
||||
|
||||
## Migration Cheat Sheet
|
||||
|
||||
If you're sitting on a LangGraph codebase and want to move to CrewAI Flows, here's a practical conversion guide:
|
||||
|
||||
1. **Map your state.** Convert your `TypedDict` to a Pydantic `BaseModel`. Add default values for all fields.
|
||||
2. **Convert nodes to methods.** Each `add_node` function becomes a method on your `Flow` subclass. Replace `state["field"]` reads with `self.state.field`.
|
||||
3. **Replace edges with decorators.** Your `add_edge(START, "first_node")` becomes `@start()` on the first method. Sequential `add_edge("a", "b")` becomes `@listen(a)` on method `b`.
|
||||
4. **Replace conditional edges with `@router`.** Your routing function and `add_conditional_edges()` mapping become a single `@router()` method that returns a route string.
|
||||
5. **Replace compile + invoke with kickoff.** Drop `graph.compile()`. Call `flow.kickoff()` instead.
|
||||
6. **Consider where Crews fit.** Any node where you have complex multi-step agent logic is a candidate for extraction into a Crew. This is where you'll see the biggest quality improvement.
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
Install CrewAI and scaffold a new Flow project:
|
||||
|
||||
```bash
|
||||
pip install crewai
|
||||
crewai create flow my_first_flow
|
||||
cd my_first_flow
|
||||
```
|
||||
|
||||
This generates a project structure with a ready-to-edit Flow class, configuration files, and a `pyproject.toml` with `type = "flow"` already set. Run it with:
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
|
||||
From there, add your agents, wire up your listeners, and ship it.
|
||||
|
||||
---
|
||||
|
||||
## Final Thoughts
|
||||
|
||||
LangGraph taught the ecosystem that AI workflows need structure. That was an important lesson. But CrewAI Flows takes that lesson and delivers it in a form that's faster to write, easier to read, and more powerful in production — especially when your workflows involve multiple collaborating agents.
|
||||
|
||||
If you're building anything beyond a single-agent chain, give Flows a serious look. The decorator-driven model, native Crew integration, and built-in state management mean you'll spend less time on plumbing and more time on the problems that matter.
|
||||
|
||||
Start with `crewai create flow`. You won't look back.
|
||||
@@ -27,11 +27,8 @@ mode: "wide"
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12, justifyContent: 'center' }}>
|
||||
<a className="button button-primary" href="/en/installation">Install</a>
|
||||
<a className="button" href="/en/quickstart">Quickstart</a>
|
||||
<a className="button" href="/en/guides/crews/first-crew">First Crew</a>
|
||||
<a className="button" href="/en/guides/flows/first-flow">First Flow</a>
|
||||
<a className="button" href="/en/concepts/llms">LLM Setup</a>
|
||||
<a className="button button-primary" href="/en/quickstart">Get started</a>
|
||||
<a className="button" href="/en/changelog">View changelog</a>
|
||||
<a className="button" href="/en/api-reference/introduction">API Reference</a>
|
||||
</div>
|
||||
|
||||
@@ -39,49 +36,17 @@ mode: "wide"
|
||||
|
||||
<div style={{ marginTop: 32 }} />
|
||||
|
||||
## Start in 3 steps
|
||||
## Get started
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="1) Install" href="/en/installation" icon="wrench">
|
||||
<Card title="Introduction" href="/en/introduction" icon="sparkles">
|
||||
Overview of CrewAI concepts, architecture, and what you can build with agents, crews, and flows.
|
||||
</Card>
|
||||
<Card title="Installation" href="/en/installation" icon="wrench">
|
||||
Install via `uv`, configure API keys, and set up the CLI for local development.
|
||||
</Card>
|
||||
<Card title="2) Run Quickstart" href="/en/quickstart" icon="rocket">
|
||||
Launch your first working crew with a minimal project and iterate from there.
|
||||
</Card>
|
||||
<Card title="3) Pick a path" href="/en/ai/overview" icon="sitemap">
|
||||
Continue with canonical domain packs for Flows, Agents, Crews, LLMs, Memory, and Tools.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Most-used pages
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="First Crew" href="/en/guides/crews/first-crew" icon="users">
|
||||
Build a production-style crew with role/task configuration and execution flow.
|
||||
</Card>
|
||||
<Card title="First Flow" href="/en/guides/flows/first-flow" icon="arrow-progress">
|
||||
Build event-driven orchestration with state, listeners, and routing.
|
||||
</Card>
|
||||
<Card title="Flowstate Chat History" href="/en/learn/flowstate-chat-history" icon="comments">
|
||||
Stateful chat history pattern with persistence and summary compaction.
|
||||
</Card>
|
||||
<Card title="Agents" href="/en/concepts/agents" icon="user">
|
||||
Agent role design, tool boundaries, and output contracts.
|
||||
</Card>
|
||||
<Card title="Crews" href="/en/concepts/crews" icon="users-gear">
|
||||
Multi-agent collaboration patterns and process semantics.
|
||||
</Card>
|
||||
<Card title="Flows" href="/en/concepts/flows" icon="code-branch">
|
||||
Deterministic orchestration, state lifecycle, persistence, and resume.
|
||||
</Card>
|
||||
<Card title="LLMs" href="/en/concepts/llms" icon="microchip-ai">
|
||||
Model setup, provider config, routing patterns, and reliability defaults.
|
||||
</Card>
|
||||
<Card title="Memory" href="/en/concepts/memory" icon="database">
|
||||
Semantic recall, scope strategy, and state-vs-memory architecture.
|
||||
</Card>
|
||||
<Card title="Tools" href="/en/tools/overview" icon="wrench">
|
||||
Tool categories, integration surfaces, and practical usage patterns.
|
||||
<Card title="Quickstart" href="/en/quickstart" icon="rocket">
|
||||
Spin up your first crew in minutes. Learn the core runtime, project layout, and dev loop.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -125,11 +90,7 @@ mode: "wide"
|
||||
</CardGroup>
|
||||
|
||||
<Callout title="Explore real-world patterns" icon="github">
|
||||
Browse the <a href="/en/examples/cookbooks">examples and cookbooks</a> for end-to-end reference implementations across agents, flows, and enterprise automations. For a practical conversational pattern, start with <a href="/en/learn/flowstate-chat-history">Flowstate Chat History</a>.
|
||||
</Callout>
|
||||
|
||||
<Callout title="AI-First Docs" icon="sitemap">
|
||||
Use the <a href="/en/ai/overview">AI-First Documentation map</a> for canonical domain packs across Flows, Agents, Crews, LLMs, Memory, and Tools.
|
||||
Browse the <a href="/en/examples/cookbooks">examples and cookbooks</a> for end-to-end reference implementations across agents, flows, and enterprise automations.
|
||||
</Callout>
|
||||
|
||||
## Stay connected
|
||||
|
||||
@@ -16,52 +16,6 @@ It empowers developers to build production-ready multi-agent systems by combinin
|
||||
|
||||
With over 100,000 developers certified through our community courses, CrewAI is the standard for enterprise-ready AI automation.
|
||||
|
||||
## Start Here
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Install" href="/en/installation" icon="wrench">
|
||||
Set up CrewAI, configure API keys, and prepare your local environment.
|
||||
</Card>
|
||||
<Card title="Quickstart" href="/en/quickstart" icon="rocket">
|
||||
Run your first working crew with a minimal setup.
|
||||
</Card>
|
||||
<Card title="First Crew" href="/en/guides/crews/first-crew" icon="users-gear">
|
||||
Build a production-style crew with roles, tasks, and execution flow.
|
||||
</Card>
|
||||
<Card title="First Flow" href="/en/guides/flows/first-flow" icon="arrow-progress">
|
||||
Build event-driven orchestration with state, listeners, and routers.
|
||||
</Card>
|
||||
<Card title="LLM Setup" href="/en/concepts/llms" icon="microchip-ai">
|
||||
Configure providers, models, and reliability defaults.
|
||||
</Card>
|
||||
<Card title="API Reference" href="/en/api-reference/introduction" icon="book">
|
||||
Use kickoff, resume, and status endpoints for production integrations.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Most-used Docs
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Agents" href="/en/concepts/agents" icon="user">
|
||||
Role design, tool boundaries, and output contracts.
|
||||
</Card>
|
||||
<Card title="Crews" href="/en/concepts/crews" icon="users">
|
||||
Multi-agent coordination and process choices.
|
||||
</Card>
|
||||
<Card title="Flows" href="/en/concepts/flows" icon="code-branch">
|
||||
Deterministic orchestration, state, persistence, and resume.
|
||||
</Card>
|
||||
<Card title="Memory" href="/en/concepts/memory" icon="database">
|
||||
Scope strategy and semantic recall across runs.
|
||||
</Card>
|
||||
<Card title="Flowstate Chat History" href="/en/learn/flowstate-chat-history" icon="comments">
|
||||
Stateful chat context with summary compaction and persistence.
|
||||
</Card>
|
||||
<Card title="AI-First Docs Map" href="/en/ai/overview" icon="sitemap">
|
||||
Canonical domain packs for Flows, Agents, Crews, LLMs, Memory, and Tools.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## The CrewAI Architecture
|
||||
|
||||
CrewAI's architecture is designed to balance autonomy with control.
|
||||
@@ -176,7 +130,7 @@ For any production-ready application, **start with a Flow**.
|
||||
<Card
|
||||
title="Quick Start"
|
||||
icon="bolt"
|
||||
href="/en/quickstart"
|
||||
href="en/quickstart"
|
||||
>
|
||||
Follow our quickstart guide to create your first CrewAI agent and get hands-on experience.
|
||||
</Card>
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
---
|
||||
title: "Flowstate Chat History"
|
||||
description: "Build a stateful chat workflow that keeps context compact, persistent, and production-friendly."
|
||||
icon: "comments"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This guide shows a practical pattern for managing LLM chat history with Flow state:
|
||||
|
||||
- Keep recent turns in a sliding window
|
||||
- Summarize older turns into a compact running summary
|
||||
- Persist state automatically with `@persist()`
|
||||
- Keep optional long-term recall using Flow memory
|
||||
|
||||
## Why this pattern works
|
||||
|
||||
Naively appending every message to prompts causes token bloat and unstable behavior over long sessions. A better approach is:
|
||||
|
||||
1. Keep only the most recent turns in `state.messages`
|
||||
2. Move older turns into `state.running_summary`
|
||||
3. Build prompts from `running_summary + recent messages`
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. CrewAI installed and configured
|
||||
2. API key configured for your model provider
|
||||
3. Basic familiarity with Flow decorators (`@start`, `@listen`)
|
||||
|
||||
## Step 1: Define typed chat state
|
||||
|
||||
```python Code
|
||||
from typing import Dict, List
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ChatSessionState(BaseModel):
|
||||
session_id: str = "demo-session"
|
||||
running_summary: str = ""
|
||||
messages: List[Dict[str, str]] = Field(default_factory=list)
|
||||
max_recent_messages: int = 8
|
||||
last_user_message: str = ""
|
||||
assistant_reply: str = ""
|
||||
turn_count: int = 0
|
||||
```
|
||||
|
||||
## Step 2: Build the Flow
|
||||
|
||||
```python Code
|
||||
from crewai.flow.flow import Flow, start, listen
|
||||
from crewai.flow.persistence import persist
|
||||
from litellm import completion
|
||||
|
||||
|
||||
@persist()
|
||||
class ChatHistoryFlow(Flow[ChatSessionState]):
|
||||
model = "gpt-4o-mini"
|
||||
|
||||
@start()
|
||||
def capture_user_message(self):
|
||||
self.state.last_user_message = self.state.last_user_message.strip()
|
||||
self.state.messages.append(
|
||||
{"role": "user", "content": self.state.last_user_message}
|
||||
)
|
||||
self.state.turn_count += 1
|
||||
return self.state.last_user_message
|
||||
|
||||
@listen(capture_user_message)
|
||||
def compact_old_history(self, _):
|
||||
if len(self.state.messages) <= self.state.max_recent_messages:
|
||||
return "no_compaction"
|
||||
|
||||
overflow = self.state.messages[:-self.state.max_recent_messages]
|
||||
self.state.messages = self.state.messages[-self.state.max_recent_messages :]
|
||||
overflow_text = "\n".join(
|
||||
f"{m['role']}: {m['content']}" for m in overflow
|
||||
)
|
||||
|
||||
summary_prompt = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Summarize old chat turns into short bullet points. Preserve facts, constraints, and decisions.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f"Existing summary:\n{self.state.running_summary or '(empty)'}\n\n"
|
||||
f"New old turns:\n{overflow_text}"
|
||||
),
|
||||
},
|
||||
]
|
||||
summary_response = completion(model=self.model, messages=summary_prompt)
|
||||
self.state.running_summary = summary_response["choices"][0]["message"]["content"]
|
||||
return "compacted"
|
||||
|
||||
@listen(compact_old_history)
|
||||
def generate_reply(self, _):
|
||||
system_context = (
|
||||
"You are a helpful assistant.\n"
|
||||
f"Conversation summary so far:\n{self.state.running_summary or '(none)'}"
|
||||
)
|
||||
|
||||
response = completion(
|
||||
model=self.model,
|
||||
messages=[{"role": "system", "content": system_context}, *self.state.messages],
|
||||
)
|
||||
answer = response["choices"][0]["message"]["content"]
|
||||
|
||||
self.state.assistant_reply = answer
|
||||
self.state.messages.append({"role": "assistant", "content": answer})
|
||||
|
||||
# Optional: store key turns in long-term memory for later recall
|
||||
self.remember(
|
||||
f"Session {self.state.session_id} turn {self.state.turn_count}: "
|
||||
f"user={self.state.last_user_message} assistant={answer}",
|
||||
scope=f"/chat/{self.state.session_id}",
|
||||
)
|
||||
return answer
|
||||
```
|
||||
|
||||
## Step 3: Run it
|
||||
|
||||
```python Code
|
||||
flow = ChatHistoryFlow()
|
||||
|
||||
first = flow.kickoff(
|
||||
inputs={
|
||||
"session_id": "customer-42",
|
||||
"last_user_message": "I need help choosing a pricing plan for a 10-person team.",
|
||||
}
|
||||
)
|
||||
print("Assistant:", first)
|
||||
|
||||
second = flow.kickoff(
|
||||
inputs={
|
||||
"last_user_message": "We also need SSO and audit logs. What do you recommend now?",
|
||||
}
|
||||
)
|
||||
print("Assistant:", second)
|
||||
print("Turns:", flow.state.turn_count)
|
||||
print("Recent messages:", len(flow.state.messages))
|
||||
```
|
||||
|
||||
## Expected output (shape)
|
||||
|
||||
```text Output
|
||||
Assistant: ...initial recommendation...
|
||||
Assistant: ...updated recommendation with SSO and audit-log requirements...
|
||||
Turns: 2
|
||||
Recent messages: 4
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If replies ignore earlier context:
|
||||
increase `max_recent_messages` and ensure `running_summary` is included in the system context.
|
||||
- If prompts become too large:
|
||||
lower `max_recent_messages` and summarize more aggressively.
|
||||
- If sessions collide:
|
||||
provide a stable `session_id` and isolate memory scope with `/chat/{session_id}`.
|
||||
|
||||
## Next steps
|
||||
|
||||
- Add tool calls for account lookup or product catalog retrieval
|
||||
- Route to human review for high-risk decisions
|
||||
- Add structured output to capture recommendations in machine-readable JSON
|
||||
@@ -7,7 +7,7 @@ mode: "wide"
|
||||
|
||||
## Connect CrewAI to LLMs
|
||||
|
||||
CrewAI uses LiteLLM to connect to a wide variety of Language Models (LLMs). This integration provides extensive versatility, allowing you to use models from numerous providers with a simple, unified interface.
|
||||
CrewAI connects to LLMs through native SDK integrations for the most popular providers (OpenAI, Anthropic, Google Gemini, Azure, and AWS Bedrock), and uses LiteLLM as a flexible fallback for all other providers.
|
||||
|
||||
<Note>
|
||||
By default, CrewAI uses the `gpt-4o-mini` model. This is determined by the `OPENAI_MODEL_NAME` environment variable, which defaults to "gpt-4o-mini" if not set.
|
||||
@@ -41,6 +41,14 @@ LiteLLM supports a wide range of providers, including but not limited to:
|
||||
|
||||
For a complete and up-to-date list of supported providers, please refer to the [LiteLLM Providers documentation](https://docs.litellm.ai/docs/providers).
|
||||
|
||||
<Info>
|
||||
To use any provider not covered by a native integration, add LiteLLM as a dependency to your project:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
Native providers (OpenAI, Anthropic, Google Gemini, Azure, AWS Bedrock) use their own SDK extras — see the [Provider Configuration Examples](/en/concepts/llms#provider-configuration-examples).
|
||||
</Info>
|
||||
|
||||
## Changing the LLM
|
||||
|
||||
To use a different LLM with your CrewAI agents, you have several options:
|
||||
|
||||
@@ -35,7 +35,7 @@ Visit [app.crewai.com](https://app.crewai.com) and create your free account. Thi
|
||||
If you haven't already, install CrewAI with the CLI tools:
|
||||
|
||||
```bash
|
||||
uv add crewai[tools]
|
||||
uv add 'crewai[tools]'
|
||||
```
|
||||
|
||||
Then authenticate your CLI with your CrewAI AMP account:
|
||||
|
||||
@@ -18,77 +18,46 @@ Composio is an integration platform that allows you to connect your AI agents to
|
||||
To incorporate Composio tools into your project, follow the instructions below:
|
||||
|
||||
```shell
|
||||
pip install composio-crewai
|
||||
pip install composio composio-crewai
|
||||
pip install crewai
|
||||
```
|
||||
|
||||
After the installation is complete, either run `composio login` or export your composio API key as `COMPOSIO_API_KEY`. Get your Composio API key from [here](https://app.composio.dev)
|
||||
After the installation is complete, set your Composio API key as `COMPOSIO_API_KEY`. Get your Composio API key from [here](https://platform.composio.dev)
|
||||
|
||||
## Example
|
||||
|
||||
The following example demonstrates how to initialize the tool and execute a github action:
|
||||
|
||||
1. Initialize Composio toolset
|
||||
1. Initialize Composio with CrewAI Provider
|
||||
|
||||
```python Code
|
||||
from composio_crewai import ComposioToolSet, App, Action
|
||||
from composio_crewai import ComposioProvider
|
||||
from composio import Composio
|
||||
from crewai import Agent, Task, Crew
|
||||
|
||||
toolset = ComposioToolSet()
|
||||
composio = Composio(provider=ComposioProvider())
|
||||
```
|
||||
|
||||
2. Connect your GitHub account
|
||||
2. Create a new Composio Session and retrieve the tools
|
||||
<CodeGroup>
|
||||
```shell CLI
|
||||
composio add github
|
||||
```
|
||||
```python Code
|
||||
request = toolset.initiate_connection(app=App.GITHUB)
|
||||
print(f"Open this URL to authenticate: {request.redirectUrl}")
|
||||
```python
|
||||
session = composio.create(
|
||||
user_id="your-user-id",
|
||||
toolkits=["gmail", "github"] # optional, default is all toolkits
|
||||
)
|
||||
tools = session.tools()
|
||||
```
|
||||
Read more about sessions and user management [here](https://docs.composio.dev/docs/configuring-sessions)
|
||||
</CodeGroup>
|
||||
|
||||
3. Get Tools
|
||||
3. Authenticating users manually
|
||||
|
||||
- Retrieving all the tools from an app (not recommended for production):
|
||||
Composio automatically authenticates the users during the agent chat session. However, you can also authenticate the user manually by calling the `authorize` method.
|
||||
```python Code
|
||||
tools = toolset.get_tools(apps=[App.GITHUB])
|
||||
connection_request = session.authorize("github")
|
||||
print(f"Open this URL to authenticate: {connection_request.redirect_url}")
|
||||
```
|
||||
|
||||
- Filtering tools based on tags:
|
||||
```python Code
|
||||
tag = "users"
|
||||
|
||||
filtered_action_enums = toolset.find_actions_by_tags(
|
||||
App.GITHUB,
|
||||
tags=[tag],
|
||||
)
|
||||
|
||||
tools = toolset.get_tools(actions=filtered_action_enums)
|
||||
```
|
||||
|
||||
- Filtering tools based on use case:
|
||||
```python Code
|
||||
use_case = "Star a repository on GitHub"
|
||||
|
||||
filtered_action_enums = toolset.find_actions_by_use_case(
|
||||
App.GITHUB, use_case=use_case, advanced=False
|
||||
)
|
||||
|
||||
tools = toolset.get_tools(actions=filtered_action_enums)
|
||||
```
|
||||
<Tip>Set `advanced` to True to get actions for complex use cases</Tip>
|
||||
|
||||
- Using specific tools:
|
||||
|
||||
In this demo, we will use the `GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER` action from the GitHub app.
|
||||
```python Code
|
||||
tools = toolset.get_tools(
|
||||
actions=[Action.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER]
|
||||
)
|
||||
```
|
||||
Learn more about filtering actions [here](https://docs.composio.dev/patterns/tools/use-tools/use-specific-actions)
|
||||
|
||||
4. Define agent
|
||||
|
||||
```python Code
|
||||
@@ -116,4 +85,4 @@ crew = Crew(agents=[crewai_agent], tasks=[task])
|
||||
crew.kickoff()
|
||||
```
|
||||
|
||||
* More detailed list of tools can be found [here](https://app.composio.dev)
|
||||
* More detailed list of tools can be found [here](https://docs.composio.dev/toolkits)
|
||||
|
||||
@@ -1,97 +1,316 @@
|
||||
---
|
||||
title: Brave Search
|
||||
description: The `BraveSearchTool` is designed to search the internet using the Brave Search API.
|
||||
title: Brave Search Tools
|
||||
description: A suite of tools for querying the Brave Search API — covering web, news, image, and video search.
|
||||
icon: searchengin
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
# `BraveSearchTool`
|
||||
# Brave Search Tools
|
||||
|
||||
## Description
|
||||
|
||||
This tool is designed to perform web searches using the Brave Search API. It allows you to search the internet with a specified query and retrieve relevant results. The tool supports customizable result counts and country-specific searches.
|
||||
CrewAI offers a family of Brave Search tools, each targeting a specific [Brave Search API](https://brave.com/search/api/) endpoint.
|
||||
Rather than a single catch-all tool, you can pick exactly the tool that matches the kind of results your agent needs:
|
||||
|
||||
| Tool | Endpoint | Use case |
|
||||
| --- | --- | --- |
|
||||
| `BraveWebSearchTool` | Web Search | General web results, snippets, and URLs |
|
||||
| `BraveNewsSearchTool` | News Search | Recent news articles and headlines |
|
||||
| `BraveImageSearchTool` | Image Search | Image results with dimensions and source URLs |
|
||||
| `BraveVideoSearchTool` | Video Search | Video results from across the web |
|
||||
| `BraveLocalPOIsTool` | Local POIs | Find points of interest (e.g., restaurants) |
|
||||
| `BraveLocalPOIsDescriptionTool` | Local POIs | Retrieve AI-generated location descriptions |
|
||||
| `BraveLLMContextTool` | LLM Context | Pre-extracted web content optimized for AI agents, LLM grounding, and RAG pipelines. |
|
||||
|
||||
All tools share a common base class (`BraveSearchToolBase`) that provides consistent behavior — rate limiting, automatic retries on `429` responses, header and parameter validation, and optional file saving.
|
||||
|
||||
<Note>
|
||||
The older `BraveSearchTool` class is still available for backwards compatibility, but it is considered **legacy** and will not receive the same level of attention going forward. We recommend migrating to the specific tools listed above, which offer richer configuration and a more focused interface.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
While many tools (e.g., _BraveWebSearchTool_, _BraveNewsSearchTool_, _BraveImageSearchTool_, and _BraveVideoSearchTool_) can be used with a free Brave Search API subscription/plan, some parameters (e.g., `enable_snippets`) and tools (e.g., _BraveLocalPOIsTool_ and _BraveLocalPOIsDescriptionTool_) require a paid plan. Consult your subscription plan's capabilities for clarification.
|
||||
</Note>
|
||||
|
||||
## Installation
|
||||
|
||||
To incorporate this tool into your project, follow the installation instructions below:
|
||||
|
||||
```shell
|
||||
pip install 'crewai[tools]'
|
||||
```
|
||||
|
||||
## Steps to Get Started
|
||||
## Getting Started
|
||||
|
||||
To effectively use the `BraveSearchTool`, follow these steps:
|
||||
1. **Install the package** — confirm that `crewai[tools]` is installed in your Python environment.
|
||||
2. **Get an API key** — sign up at [api-dashboard.search.brave.com/login](https://api-dashboard.search.brave.com/login) to generate a key.
|
||||
3. **Set the environment variable** — store your key as `BRAVE_API_KEY`, or pass it directly via the `api_key` parameter.
|
||||
|
||||
1. **Package Installation**: Confirm that the `crewai[tools]` package is installed in your Python environment.
|
||||
2. **API Key Acquisition**: Acquire a Brave Search API key at https://api.search.brave.com/app/keys (sign in to generate a key).
|
||||
3. **Environment Configuration**: Store your obtained API key in an environment variable named `BRAVE_API_KEY` to facilitate its use by the tool.
|
||||
## Quick Examples
|
||||
|
||||
## Example
|
||||
|
||||
The following example demonstrates how to initialize the tool and execute a search with a given query:
|
||||
### Web Search
|
||||
|
||||
```python Code
|
||||
from crewai_tools import BraveSearchTool
|
||||
from crewai_tools import BraveWebSearchTool
|
||||
|
||||
# Initialize the tool for internet searching capabilities
|
||||
tool = BraveSearchTool()
|
||||
|
||||
# Execute a search
|
||||
results = tool.run(search_query="CrewAI agent framework")
|
||||
tool = BraveWebSearchTool()
|
||||
results = tool.run(q="CrewAI agent framework")
|
||||
print(results)
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
The `BraveSearchTool` accepts the following parameters:
|
||||
|
||||
- **search_query**: Mandatory. The search query you want to use to search the internet.
|
||||
- **country**: Optional. Specify the country for the search results. Default is empty string.
|
||||
- **n_results**: Optional. Number of search results to return. Default is `10`.
|
||||
- **save_file**: Optional. Whether to save the search results to a file. Default is `False`.
|
||||
|
||||
## Example with Parameters
|
||||
|
||||
Here is an example demonstrating how to use the tool with additional parameters:
|
||||
### News Search
|
||||
|
||||
```python Code
|
||||
from crewai_tools import BraveSearchTool
|
||||
from crewai_tools import BraveNewsSearchTool
|
||||
|
||||
# Initialize the tool with custom parameters
|
||||
tool = BraveSearchTool(
|
||||
country="US",
|
||||
n_results=5,
|
||||
save_file=True
|
||||
tool = BraveNewsSearchTool()
|
||||
results = tool.run(q="latest AI breakthroughs")
|
||||
print(results)
|
||||
```
|
||||
|
||||
### Image Search
|
||||
|
||||
```python Code
|
||||
from crewai_tools import BraveImageSearchTool
|
||||
|
||||
tool = BraveImageSearchTool()
|
||||
results = tool.run(q="northern lights photography")
|
||||
print(results)
|
||||
```
|
||||
|
||||
### Video Search
|
||||
|
||||
```python Code
|
||||
from crewai_tools import BraveVideoSearchTool
|
||||
|
||||
tool = BraveVideoSearchTool()
|
||||
results = tool.run(q="how to build AI agents")
|
||||
print(results)
|
||||
```
|
||||
|
||||
### Location POI Descriptions
|
||||
|
||||
```python Code
|
||||
from crewai_tools import (
|
||||
BraveWebSearchTool,
|
||||
BraveLocalPOIsDescriptionTool,
|
||||
)
|
||||
|
||||
# Execute a search
|
||||
results = tool.run(search_query="Latest AI developments")
|
||||
print(results)
|
||||
web_search = BraveWebSearchTool(raw=True)
|
||||
poi_details = BraveLocalPOIsDescriptionTool()
|
||||
|
||||
results = web_search.run(q="italian restaurants in pensacola, florida")
|
||||
|
||||
if "locations" in results:
|
||||
location_ids = [ loc["id"] for loc in results["locations"]["results"] ]
|
||||
if location_ids:
|
||||
descriptions = poi_details.run(ids=location_ids)
|
||||
print(descriptions)
|
||||
```
|
||||
|
||||
## Common Constructor Parameters
|
||||
|
||||
Every Brave Search tool accepts the following parameters at initialization:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `api_key` | `str \| None` | `None` | Brave API key. Falls back to the `BRAVE_API_KEY` environment variable. |
|
||||
| `headers` | `dict \| None` | `None` | Additional HTTP headers to send with every request (e.g., `api-version`, geolocation headers). |
|
||||
| `requests_per_second` | `float` | `1.0` | Maximum request rate. The tool will sleep between calls to stay within this limit. |
|
||||
| `save_file` | `bool` | `False` | When `True`, each response is written to a timestamped `.txt` file. |
|
||||
| `raw` | `bool` | `False` | When `True`, the full API JSON response is returned without any refinement. |
|
||||
| `timeout` | `int` | `30` | HTTP request timeout in seconds. |
|
||||
| `country` | `str \| None` | `None` | Legacy shorthand for geo-targeting (e.g., `"US"`). Prefer using the `country` query parameter directly. |
|
||||
| `n_results` | `int` | `10` | Legacy shorthand for result count. Prefer using the `count` query parameter directly. |
|
||||
|
||||
<Warning>
|
||||
The `country` and `n_results` constructor parameters exist for backwards compatibility. They are applied as defaults when the corresponding query parameters (`country`, `count`) are not provided at call time. For new code, we recommend passing `country` and `count` directly as query parameters instead.
|
||||
</Warning>
|
||||
|
||||
## Query Parameters
|
||||
|
||||
Each tool validates its query parameters against a Pydantic schema before sending the request.
|
||||
The parameters vary slightly per endpoint — here is a summary of the most commonly used ones:
|
||||
|
||||
### BraveWebSearchTool
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `q` | **(required)** Search query string (max 400 chars). |
|
||||
| `country` | Two-letter country code for geo-targeting (e.g., `"US"`). |
|
||||
| `search_lang` | Two-letter language code for results (e.g., `"en"`). |
|
||||
| `count` | Max number of results to return (1–20). |
|
||||
| `offset` | Skip the first N pages of results (0–9). |
|
||||
| `safesearch` | Content filter: `"off"`, `"moderate"`, or `"strict"`. |
|
||||
| `freshness` | Recency filter: `"pd"` (past day), `"pw"` (past week), `"pm"` (past month), `"py"` (past year), or a date range like `"2025-01-01to2025-06-01"`. |
|
||||
| `extra_snippets` | Include up to 5 additional text snippets per result. |
|
||||
| `goggles` | Brave Goggles URL(s) and/or source for custom re-ranking. |
|
||||
|
||||
For the complete parameter and header reference, see the [Brave Web Search API documentation](https://api-dashboard.search.brave.com/api-reference/web/search/get).
|
||||
|
||||
### BraveNewsSearchTool
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `q` | **(required)** Search query string (max 400 chars). |
|
||||
| `country` | Two-letter country code for geo-targeting. |
|
||||
| `search_lang` | Two-letter language code for results. |
|
||||
| `count` | Max number of results to return (1–50). |
|
||||
| `offset` | Skip the first N pages of results (0–9). |
|
||||
| `safesearch` | Content filter: `"off"`, `"moderate"`, or `"strict"`. |
|
||||
| `freshness` | Recency filter (same options as Web Search). |
|
||||
| `goggles` | Brave Goggles URL(s) and/or source for custom re-ranking. |
|
||||
|
||||
For the complete parameter and header reference, see the [Brave News Search API documentation](https://api-dashboard.search.brave.com/api-reference/news/news_search/get).
|
||||
|
||||
### BraveImageSearchTool
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `q` | **(required)** Search query string (max 400 chars). |
|
||||
| `country` | Two-letter country code for geo-targeting. |
|
||||
| `search_lang` | Two-letter language code for results. |
|
||||
| `count` | Max number of results to return (1–200). |
|
||||
| `safesearch` | Content filter: `"off"` or `"strict"`. |
|
||||
| `spellcheck` | Attempt to correct spelling errors in the query. |
|
||||
|
||||
For the complete parameter and header reference, see the [Brave Image Search API documentation](https://api-dashboard.search.brave.com/api-reference/images/image_search).
|
||||
|
||||
### BraveVideoSearchTool
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `q` | **(required)** Search query string (max 400 chars). |
|
||||
| `country` | Two-letter country code for geo-targeting. |
|
||||
| `search_lang` | Two-letter language code for results. |
|
||||
| `count` | Max number of results to return (1–50). |
|
||||
| `offset` | Skip the first N pages of results (0–9). |
|
||||
| `safesearch` | Content filter: `"off"`, `"moderate"`, or `"strict"`. |
|
||||
| `freshness` | Recency filter (same options as Web Search). |
|
||||
|
||||
For the complete parameter and header reference, see the [Brave Video Search API documentation](https://api-dashboard.search.brave.com/api-reference/videos/video_search/get).
|
||||
|
||||
### BraveLocalPOIsTool
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `ids` | **(required)** A list of unique identifiers for the desired locations. |
|
||||
| `search_lang` | Two-letter language code for results. |
|
||||
|
||||
For the complete parameter and header reference, see [Brave Local POIs API documentation](https://api-dashboard.search.brave.com/api-reference/web/local_pois).
|
||||
|
||||
### BraveLocalPOIsDescriptionTool
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `ids` | **(required)** A list of unique identifiers for the desired locations. |
|
||||
|
||||
For the complete parameter and header reference, see [Brave POI Descriptions API documentation](https://api-dashboard.search.brave.com/api-reference/web/poi_descriptions).
|
||||
|
||||
## Custom Headers
|
||||
|
||||
All tools support custom HTTP request headers. The Web Search tool, for example, accepts geolocation headers for location-aware results:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import BraveWebSearchTool
|
||||
|
||||
tool = BraveWebSearchTool(
|
||||
headers={
|
||||
"x-loc-lat": "37.7749",
|
||||
"x-loc-long": "-122.4194",
|
||||
"x-loc-city": "San Francisco",
|
||||
"x-loc-state": "CA",
|
||||
"x-loc-country": "US",
|
||||
}
|
||||
)
|
||||
|
||||
results = tool.run(q="best coffee shops nearby")
|
||||
```
|
||||
|
||||
You can also update headers after initialization using the `set_headers()` method:
|
||||
|
||||
```python Code
|
||||
tool.set_headers({"api-version": "2025-01-01"})
|
||||
```
|
||||
|
||||
## Raw Mode
|
||||
|
||||
By default, each tool refines the API response into a concise list of results. If you need the full, unprocessed API response, enable raw mode:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import BraveWebSearchTool
|
||||
|
||||
tool = BraveWebSearchTool(raw=True)
|
||||
full_response = tool.run(q="Brave Search API")
|
||||
```
|
||||
|
||||
## Agent Integration Example
|
||||
|
||||
Here's how to integrate the `BraveSearchTool` with a CrewAI agent:
|
||||
Here's how to equip a CrewAI agent with multiple Brave Search tools:
|
||||
|
||||
```python Code
|
||||
from crewai import Agent
|
||||
from crewai.project import agent
|
||||
from crewai_tools import BraveSearchTool
|
||||
from crewai_tools import BraveWebSearchTool, BraveNewsSearchTool
|
||||
|
||||
# Initialize the tool
|
||||
brave_search_tool = BraveSearchTool()
|
||||
web_search = BraveWebSearchTool()
|
||||
news_search = BraveNewsSearchTool()
|
||||
|
||||
# Define an agent with the BraveSearchTool
|
||||
@agent
|
||||
def researcher(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config["researcher"],
|
||||
allow_delegation=False,
|
||||
tools=[brave_search_tool]
|
||||
tools=[web_search, news_search],
|
||||
)
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
Combining multiple parameters for a targeted search:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import BraveWebSearchTool
|
||||
|
||||
tool = BraveWebSearchTool(
|
||||
requests_per_second=0.5, # conservative rate limit
|
||||
save_file=True,
|
||||
)
|
||||
|
||||
results = tool.run(
|
||||
q="artificial intelligence news",
|
||||
country="US",
|
||||
search_lang="en",
|
||||
count=5,
|
||||
freshness="pm", # past month only
|
||||
extra_snippets=True,
|
||||
)
|
||||
print(results)
|
||||
```
|
||||
|
||||
## Migrating from `BraveSearchTool` (Legacy)
|
||||
|
||||
If you are currently using `BraveSearchTool`, switching to the new tools is straightforward:
|
||||
|
||||
```python Code
|
||||
# Before (legacy)
|
||||
from crewai_tools import BraveSearchTool
|
||||
|
||||
tool = BraveSearchTool(country="US", n_results=5, save_file=True)
|
||||
results = tool.run(search_query="AI agents")
|
||||
|
||||
# After (recommended)
|
||||
from crewai_tools import BraveWebSearchTool
|
||||
|
||||
tool = BraveWebSearchTool(save_file=True)
|
||||
results = tool.run(q="AI agents", country="US", count=5)
|
||||
```
|
||||
|
||||
Key differences:
|
||||
- **Import**: Use `BraveWebSearchTool` (or the news/image/video variant) instead of `BraveSearchTool`.
|
||||
- **Query parameter**: Use `q` instead of `search_query`. (Both `search_query` and `query` are still accepted for convenience, but `q` is the preferred parameter.)
|
||||
- **Result count**: Pass `count` as a query parameter instead of `n_results` at init time.
|
||||
- **Country**: Pass `country` as a query parameter instead of at init time.
|
||||
- **API key**: Can now be passed directly via `api_key=` in addition to the `BRAVE_API_KEY` environment variable.
|
||||
- **Rate limiting**: Configurable via `requests_per_second` with automatic retry on `429` responses.
|
||||
|
||||
## Conclusion
|
||||
|
||||
By integrating the `BraveSearchTool` into Python projects, users gain the ability to conduct real-time, relevant searches across the internet directly from their applications. The tool provides a simple interface to the powerful Brave Search API, making it easy to retrieve and process search results programmatically. By adhering to the setup and usage guidelines provided, incorporating this tool into projects is streamlined and straightforward.
|
||||
The Brave Search tool suite gives your CrewAI agents flexible, endpoint-specific access to the Brave Search API. Whether you need web pages, breaking news, images, or videos, there is a dedicated tool with validated parameters and built-in resilience. Pick the tool that fits your use case, and refer to the [Brave Search API documentation](https://brave.com/search/api/) for the full details on available parameters and response formats.
|
||||
|
||||
@@ -4,6 +4,195 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 3월 13일">
|
||||
## v1.10.2rc1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.10.2rc1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 릴리스 명령 추가 및 PyPI 게시 트리거
|
||||
|
||||
### 버그 수정
|
||||
- 보호되지 않은 I/O에 대한 프로세스 간 및 스레드 안전 잠금 수정
|
||||
- 모든 스레드 및 실행기 경계를 넘는 contextvars 전파
|
||||
- async 작업 스레드로 ContextVars 전파
|
||||
|
||||
### 문서
|
||||
- v1.10.2a1에 대한 변경 로그 및 버전 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@danglies007, @greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 11일">
|
||||
## v1.10.2a1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.10.2a1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- Anthropics에 대한 도구 검색 지원 추가, 토큰 저장, 실행 중 적절한 도구를 동적으로 주입하는 기능 추가.
|
||||
- 더 많은 Brave Search 도구 도입.
|
||||
- 야간 릴리스를 위한 액션 생성.
|
||||
|
||||
### 버그 수정
|
||||
- 동시 다중 프로세스 실행 중 LockException 수정.
|
||||
- 단일 사용자 메시지에서 병렬 도구 결과 그룹화 문제 해결.
|
||||
- MCP 도구 해상도 문제 해결 및 모든 공유 가변 연결 제거.
|
||||
- human_feedback 함수에서 LLM 매개변수 처리 업데이트.
|
||||
- LockedListProxy 및 LockedDictProxy에 누락된 list/dict 메서드 추가.
|
||||
- 병렬 도구 호출 스레드에 contextvars 컨텍스트 전파.
|
||||
- CVE 경로 탐색 취약점을 해결하기 위해 gitpython 의존성을 >=3.1.41로 업데이트.
|
||||
|
||||
### 리팩토링
|
||||
- 메모리 클래스를 직렬화 가능하도록 리팩토링.
|
||||
|
||||
### 문서
|
||||
- v1.10.1에 대한 변경 로그 및 버전 업데이트.
|
||||
|
||||
## 기여자
|
||||
|
||||
@akaKuruma, @github-actions[bot], @giulio-leone, @greysonlalonde, @joaomdmoura, @jonathansampson, @lorenzejay, @lucasgomide, @mattatcha
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 4일">
|
||||
## v1.10.1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.10.1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- Gemini GenAI 업그레이드
|
||||
|
||||
### 버그 수정
|
||||
- 재귀를 피하기 위해 실행기 리스너 값을 조정
|
||||
- Gemini에서 병렬 함수 응답 부분을 단일 Content 객체로 그룹화
|
||||
- Gemini에서 사고 모델의 사고 출력을 표시
|
||||
- 에이전트 도구가 None일 때 MCP 및 플랫폼 도구 로드
|
||||
- A2A에서 실행 이벤트 루프가 있는 Jupyter 환경 지원
|
||||
- 일시적인 추적을 위해 익명 ID 사용
|
||||
- 조건부로 플러스 헤더 전달
|
||||
- 원격 측정을 위해 비주 스레드에서 신호 처리기 등록 건너뛰기
|
||||
- 도구 오류를 관찰로 주입하고 이름 충돌 해결
|
||||
- Dependabot 경고를 해결하기 위해 pypdf를 4.x에서 6.7.4로 업그레이드
|
||||
- 심각 및 높은 Dependabot 보안 경고 해결
|
||||
|
||||
### 문서
|
||||
- Composio 도구 문서를 지역별로 동기화
|
||||
|
||||
## 기여자
|
||||
|
||||
@giulio-leone, @greysonlalonde, @haxzie, @joaomdmoura, @lorenzejay, @mattatcha, @mplachta, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 2월 27일">
|
||||
## v1.10.1a1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.10.1a1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 단계 콜백 메서드에서 비동기 호출 지원 구현
|
||||
- 메모리 모듈의 무거운 의존성에 대한 지연 로딩 구현
|
||||
|
||||
### 문서
|
||||
- v1.10.0에 대한 변경 로그 및 버전 업데이트
|
||||
|
||||
### 리팩토링
|
||||
- 비동기 호출을 지원하기 위해 단계 콜백 메서드 리팩토링
|
||||
- 메모리 모듈의 무거운 의존성에 대한 지연 로딩을 구현하기 위해 리팩토링
|
||||
|
||||
### 버그 수정
|
||||
- 릴리스 노트의 분기 수정
|
||||
|
||||
## 기여자
|
||||
|
||||
@greysonlalonde, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 2월 27일">
|
||||
## v1.10.1a1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.10.1a1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 리팩토링
|
||||
- 비동기 호출을 지원하기 위해 단계 콜백 메서드 리팩토링
|
||||
- 메모리 모듈의 무거운 의존성에 대해 지연 로딩 구현
|
||||
|
||||
### 문서화
|
||||
- v1.10.0에 대한 변경 로그 및 버전 업데이트
|
||||
|
||||
### 버그 수정
|
||||
- 릴리스 노트를 위한 브랜치 생성
|
||||
|
||||
## 기여자
|
||||
|
||||
@greysonlalonde, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 2월 26일">
|
||||
## v1.10.0
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.10.0)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- MCP 도구 해상도 및 관련 이벤트 개선
|
||||
- lancedb 버전 업데이트 및 lance-namespace 패키지 추가
|
||||
- CrewAgentExecutor 및 BaseTool에서 JSON 인수 파싱 및 검증 개선
|
||||
- CLI HTTP 클라이언트를 requests에서 httpx로 마이그레이션
|
||||
- 버전화된 문서 추가
|
||||
- 버전 노트에 대한 yanked 감지 추가
|
||||
- Flows에서 사용자 입력 처리 구현
|
||||
- 인간 피드백 통합 테스트에서 HITL 자기 루프 기능 개선
|
||||
- eventbus에 started_event_id 추가 및 설정
|
||||
- tools.specs 자동 업데이트
|
||||
|
||||
### 버그 수정
|
||||
- 빈 경우에도 도구 kwargs를 검증하여 모호한 TypeError 방지
|
||||
- LLM을 위한 도구 매개변수 스키마에서 null 타입 유지
|
||||
- output_pydantic/output_json을 네이티브 구조화된 출력으로 매핑
|
||||
- 약속이 있는 경우 콜백이 실행/대기되도록 보장
|
||||
- 예외 컨텍스트에서 메서드 이름 캡처
|
||||
- 라우터 결과에서 enum 타입 유지; 타입 개선
|
||||
- 입력으로 지속성 ID가 전달될 때 조용히 깨지는 순환 흐름 수정
|
||||
- CLI 플래그 형식을 --skip-provider에서 --skip_provider로 수정
|
||||
- OpenAI 도구 호출 스트림이 완료되도록 보장
|
||||
- MCP 도구에서 복잡한 스키마 $ref 포인터 해결
|
||||
- 스키마에서 additionalProperties=false 강제 적용
|
||||
- 크루 폴더에 대해 예약된 스크립트 이름 거부
|
||||
- 가드레일 이벤트 방출 테스트에서 경쟁 조건 해결
|
||||
|
||||
### 문서
|
||||
- 비네이티브 LLM 공급자를 위한 litellm 종속성 노트 추가
|
||||
- NL2SQL 보안 모델 및 강화 지침 명확화
|
||||
- 9개 통합에서 96개의 누락된 작업 추가
|
||||
|
||||
### 리팩토링
|
||||
- crew를 provider로 리팩토링
|
||||
- HITL을 provider 패턴으로 추출
|
||||
- 훅 타이핑 및 등록 개선
|
||||
|
||||
## 기여자
|
||||
|
||||
@dependabot[bot], @github-actions[bot], @github-code-quality[bot], @greysonlalonde, @heitorado, @hobostay, @joaomdmoura, @johnvan7, @jonathansampson, @lorenzejay, @lucasgomide, @mattatcha, @mplachta, @nicoferdi96, @theCyberTech, @thiagomoretto, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 1월 26일">
|
||||
## v1.9.0
|
||||
|
||||
|
||||
@@ -105,6 +105,15 @@ CrewAI 코드 내에는 사용할 모델을 지정할 수 있는 여러 위치
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Info>
|
||||
CrewAI는 OpenAI, Anthropic, Google (Gemini API), Azure, AWS Bedrock에 대해 네이티브 SDK 통합을 제공합니다 — 제공자별 extras(예: `uv add "crewai[openai]"`) 외에 추가 설치가 필요하지 않습니다.
|
||||
|
||||
그 외 모든 제공자는 **LiteLLM**을 통해 지원됩니다. 이를 사용하려면 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Info>
|
||||
|
||||
## 공급자 구성 예시
|
||||
|
||||
CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양한 LLM 공급자를 지원합니다.
|
||||
@@ -214,6 +223,11 @@ CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양
|
||||
| `meta_llama/Llama-4-Maverick-17B-128E-Instruct-FP8` | 128k | 4028 | 텍스트, 이미지 | 텍스트 |
|
||||
| `meta_llama/Llama-3.3-70B-Instruct` | 128k | 4028 | 텍스트 | 텍스트 |
|
||||
| `meta_llama/Llama-3.3-8B-Instruct` | 128k | 4028 | 텍스트 | 텍스트 |
|
||||
|
||||
**참고:** 이 제공자는 LiteLLM을 사용합니다. 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Anthropic">
|
||||
@@ -354,6 +368,11 @@ CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양
|
||||
| gemini-1.5-flash | 1M 토큰 | 밸런스 잡힌 멀티모달 모델, 대부분의 작업에 적합 |
|
||||
| gemini-1.5-flash-8B | 1M 토큰 | 가장 빠르고, 비용 효율적, 고빈도 작업에 적합 |
|
||||
| gemini-1.5-pro | 2M 토큰 | 최고의 성능, 논리적 추론, 코딩, 창의적 협업 등 다양한 추론 작업에 적합 |
|
||||
|
||||
**참고:** 이 제공자는 LiteLLM을 사용합니다. 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Azure">
|
||||
@@ -439,6 +458,11 @@ CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양
|
||||
model="sagemaker/<my-endpoint>"
|
||||
)
|
||||
```
|
||||
|
||||
**참고:** 이 제공자는 LiteLLM을 사용합니다. 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Mistral">
|
||||
@@ -454,6 +478,11 @@ CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양
|
||||
temperature=0.7
|
||||
)
|
||||
```
|
||||
|
||||
**참고:** 이 제공자는 LiteLLM을 사용합니다. 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Nvidia NIM">
|
||||
@@ -540,6 +569,11 @@ CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양
|
||||
| rakuten/rakutenai-7b-instruct | 1,024 토큰 | 언어 이해, 추론, 텍스트 생성이 탁월한 최첨단 LLM |
|
||||
| rakuten/rakutenai-7b-chat | 1,024 토큰 | 언어 이해, 추론, 텍스트 생성이 탁월한 최첨단 LLM |
|
||||
| baichuan-inc/baichuan2-13b-chat | 4,096 토큰 | 중국어 및 영어 대화, 코딩, 수학, 지시 따르기, 퀴즈 풀이 지원 |
|
||||
|
||||
**참고:** 이 제공자는 LiteLLM을 사용합니다. 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Local NVIDIA NIM Deployed using WSL2">
|
||||
@@ -580,6 +614,11 @@ CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
**참고:** 이 제공자는 LiteLLM을 사용합니다. 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Groq">
|
||||
@@ -601,6 +640,11 @@ CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양
|
||||
| Llama 3.1 70B/8B| 131,072 토큰 | 고성능, 대용량 문맥 작업 |
|
||||
| Llama 3.2 Series| 8,192 토큰 | 범용 작업 |
|
||||
| Mixtral 8x7B | 32,768 토큰 | 성능과 문맥의 균형 |
|
||||
|
||||
**참고:** 이 제공자는 LiteLLM을 사용합니다. 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="IBM watsonx.ai">
|
||||
@@ -623,6 +667,11 @@ CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양
|
||||
base_url="https://api.watsonx.ai/v1"
|
||||
)
|
||||
```
|
||||
|
||||
**참고:** 이 제공자는 LiteLLM을 사용합니다. 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Ollama (Local LLMs)">
|
||||
@@ -636,6 +685,11 @@ CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양
|
||||
base_url="http://localhost:11434"
|
||||
)
|
||||
```
|
||||
|
||||
**참고:** 이 제공자는 LiteLLM을 사용합니다. 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Fireworks AI">
|
||||
@@ -651,6 +705,11 @@ CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양
|
||||
temperature=0.7
|
||||
)
|
||||
```
|
||||
|
||||
**참고:** 이 제공자는 LiteLLM을 사용합니다. 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Perplexity AI">
|
||||
@@ -666,6 +725,11 @@ CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양
|
||||
base_url="https://api.perplexity.ai/"
|
||||
)
|
||||
```
|
||||
|
||||
**참고:** 이 제공자는 LiteLLM을 사용합니다. 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Hugging Face">
|
||||
@@ -680,6 +744,11 @@ CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양
|
||||
model="huggingface/meta-llama/Meta-Llama-3.1-8B-Instruct"
|
||||
)
|
||||
```
|
||||
|
||||
**참고:** 이 제공자는 LiteLLM을 사용합니다. 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="SambaNova">
|
||||
@@ -703,6 +772,11 @@ CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양
|
||||
| Llama 3.2 Series| 8,192 토큰 | 범용, 멀티모달 작업 |
|
||||
| Llama 3.3 70B | 최대 131,072 토큰 | 고성능, 높은 출력 품질 |
|
||||
| Qwen2 familly | 8,192 토큰 | 고성능, 높은 출력 품질 |
|
||||
|
||||
**참고:** 이 제공자는 LiteLLM을 사용합니다. 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Cerebras">
|
||||
@@ -728,6 +802,11 @@ CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양
|
||||
- 속도와 품질의 우수한 밸런스
|
||||
- 긴 컨텍스트 윈도우 지원
|
||||
</Info>
|
||||
|
||||
**참고:** 이 제공자는 LiteLLM을 사용합니다. 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Open Router">
|
||||
@@ -750,6 +829,11 @@ CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양
|
||||
- openrouter/deepseek/deepseek-r1
|
||||
- openrouter/deepseek/deepseek-chat
|
||||
</Info>
|
||||
|
||||
**참고:** 이 제공자는 LiteLLM을 사용합니다. 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Nebius AI Studio">
|
||||
@@ -772,6 +856,11 @@ CrewAI는 고유한 기능, 인증 방법, 모델 역량을 제공하는 다양
|
||||
- 경쟁력 있는 가격
|
||||
- 속도와 품질의 우수한 밸런스
|
||||
</Info>
|
||||
|
||||
**참고:** 이 제공자는 LiteLLM을 사용합니다. 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
|
||||
@@ -176,6 +176,11 @@ Crew를 GitHub 저장소에 푸시해야 합니다. 아직 Crew를 만들지 않
|
||||

|
||||
</Frame>
|
||||
|
||||
<Info>
|
||||
프라이빗 Python 패키지를 사용하시나요? 여기에 레지스트리 자격 증명도 추가해야 합니다.
|
||||
필요한 변수는 [프라이빗 패키지 레지스트리](/ko/enterprise/guides/private-package-registry)를 참조하세요.
|
||||
</Info>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Crew 배포하기">
|
||||
|
||||
@@ -256,6 +256,12 @@ Crews와 Flows 모두 `src/project_name/main.py`에 진입점이 있습니다:
|
||||
1. **LLM API 키** (OpenAI, Anthropic, Google 등)
|
||||
2. **도구 API 키** - 외부 도구를 사용하는 경우 (Serper 등)
|
||||
|
||||
<Info>
|
||||
프로젝트가 **프라이빗 PyPI 레지스트리**의 패키지에 의존하는 경우, 레지스트리 인증 자격 증명도
|
||||
환경 변수로 구성해야 합니다. 자세한 내용은
|
||||
[프라이빗 패키지 레지스트리](/ko/enterprise/guides/private-package-registry) 가이드를 참조하세요.
|
||||
</Info>
|
||||
|
||||
<Tip>
|
||||
구성 문제를 조기에 발견하기 위해 배포 전에 동일한 환경 변수로
|
||||
로컬에서 프로젝트를 테스트하세요.
|
||||
|
||||
261
docs/ko/enterprise/guides/private-package-registry.mdx
Normal file
261
docs/ko/enterprise/guides/private-package-registry.mdx
Normal file
@@ -0,0 +1,261 @@
|
||||
---
|
||||
title: "프라이빗 패키지 레지스트리"
|
||||
description: "CrewAI AMP에서 인증된 PyPI 레지스트리의 프라이빗 Python 패키지 설치하기"
|
||||
icon: "lock"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Note>
|
||||
이 가이드는 CrewAI AMP에 배포할 때 프라이빗 PyPI 레지스트리(Azure DevOps Artifacts, GitHub Packages,
|
||||
GitLab, AWS CodeArtifact 등)에서 Python 패키지를 설치하도록 CrewAI 프로젝트를 구성하는 방법을 다룹니다.
|
||||
</Note>
|
||||
|
||||
## 이 가이드가 필요한 경우
|
||||
|
||||
프로젝트가 공개 PyPI가 아닌 프라이빗 레지스트리에 호스팅된 내부 또는 독점 Python 패키지에
|
||||
의존하는 경우, 다음을 수행해야 합니다:
|
||||
|
||||
1. UV에 패키지를 **어디서** 찾을지 알려줍니다 (index URL)
|
||||
2. UV에 **어떤** 패키지가 해당 index에서 오는지 알려줍니다 (source 매핑)
|
||||
3. UV가 설치 중에 인증할 수 있도록 **자격 증명**을 제공합니다
|
||||
|
||||
CrewAI AMP는 의존성 해결 및 설치에 [UV](https://docs.astral.sh/uv/)를 사용합니다.
|
||||
UV는 `pyproject.toml` 구성과 자격 증명용 환경 변수를 결합하여 인증된 프라이빗 레지스트리를 지원합니다.
|
||||
|
||||
## 1단계: pyproject.toml 구성
|
||||
|
||||
`pyproject.toml`에서 세 가지 요소가 함께 작동합니다:
|
||||
|
||||
### 1a. 의존성 선언
|
||||
|
||||
프라이빗 패키지를 다른 의존성과 마찬가지로 `[project.dependencies]`에 추가합니다:
|
||||
|
||||
```toml
|
||||
[project]
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.100.1,<1.0.0",
|
||||
"my-private-package>=1.2.0",
|
||||
]
|
||||
```
|
||||
|
||||
### 1b. index 정의
|
||||
|
||||
프라이빗 레지스트리를 `[[tool.uv.index]]` 아래에 명명된 index로 등록합니다:
|
||||
|
||||
```toml
|
||||
[[tool.uv.index]]
|
||||
name = "my-private-registry"
|
||||
url = "https://pkgs.dev.azure.com/my-org/_packaging/my-feed/pypi/simple/"
|
||||
explicit = true
|
||||
```
|
||||
|
||||
<Info>
|
||||
`name` 필드는 중요합니다 — UV는 이를 사용하여 인증을 위한 환경 변수 이름을
|
||||
구성합니다 (아래 [2단계](#2단계-인증-자격-증명-설정)를 참조하세요).
|
||||
|
||||
`explicit = true`를 설정하면 UV가 모든 패키지에 대해 이 index를 검색하지 않습니다 —
|
||||
`[tool.uv.sources]`에서 명시적으로 매핑한 패키지만 검색합니다. 이렇게 하면 프라이빗
|
||||
레지스트리에 대한 불필요한 쿼리를 방지하고 의존성 혼동 공격을 차단할 수 있습니다.
|
||||
</Info>
|
||||
|
||||
### 1c. 패키지를 index에 매핑
|
||||
|
||||
`[tool.uv.sources]`를 사용하여 프라이빗 index에서 해결해야 할 패키지를 UV에 알려줍니다:
|
||||
|
||||
```toml
|
||||
[tool.uv.sources]
|
||||
my-private-package = { index = "my-private-registry" }
|
||||
```
|
||||
|
||||
### 전체 예시
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "my-crew-project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.10,<=3.13"
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.100.1,<1.0.0",
|
||||
"my-private-package>=1.2.0",
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "my-private-registry"
|
||||
url = "https://pkgs.dev.azure.com/my-org/_packaging/my-feed/pypi/simple/"
|
||||
explicit = true
|
||||
|
||||
[tool.uv.sources]
|
||||
my-private-package = { index = "my-private-registry" }
|
||||
```
|
||||
|
||||
`pyproject.toml`을 업데이트한 후 lock 파일을 다시 생성합니다:
|
||||
|
||||
```bash
|
||||
uv lock
|
||||
```
|
||||
|
||||
<Warning>
|
||||
업데이트된 `uv.lock`을 항상 `pyproject.toml` 변경 사항과 함께 커밋하세요.
|
||||
lock 파일은 배포에 필수입니다 — [배포 준비하기](/ko/enterprise/guides/prepare-for-deployment)를 참조하세요.
|
||||
</Warning>
|
||||
|
||||
## 2단계: 인증 자격 증명 설정
|
||||
|
||||
UV는 `pyproject.toml`에서 정의한 index 이름을 기반으로 한 명명 규칙을 따르는
|
||||
환경 변수를 사용하여 프라이빗 index에 인증합니다:
|
||||
|
||||
```
|
||||
UV_INDEX_{UPPER_NAME}_USERNAME
|
||||
UV_INDEX_{UPPER_NAME}_PASSWORD
|
||||
```
|
||||
|
||||
여기서 `{UPPER_NAME}`은 index 이름을 **대문자**로 변환하고 **하이픈을 언더스코어로 대체**한 것입니다.
|
||||
|
||||
예를 들어, `my-private-registry`라는 이름의 index는 다음을 사용합니다:
|
||||
|
||||
| 변수 | 값 |
|
||||
|------|-----|
|
||||
| `UV_INDEX_MY_PRIVATE_REGISTRY_USERNAME` | 레지스트리 사용자 이름 또는 토큰 이름 |
|
||||
| `UV_INDEX_MY_PRIVATE_REGISTRY_PASSWORD` | 레지스트리 비밀번호 또는 토큰/PAT |
|
||||
|
||||
<Warning>
|
||||
이 환경 변수는 CrewAI AMP **환경 변수** 설정을 통해 **반드시** 추가해야 합니다 —
|
||||
전역적으로 또는 배포 수준에서. `.env` 파일에 설정하거나 프로젝트에 하드코딩할 수 없습니다.
|
||||
|
||||
아래 [AMP에서 환경 변수 설정](#amp에서-환경-변수-설정)을 참조하세요.
|
||||
</Warning>
|
||||
|
||||
## 레지스트리 제공업체 참조
|
||||
|
||||
아래 표는 일반적인 레지스트리 제공업체의 index URL 형식과 자격 증명 값을 보여줍니다.
|
||||
자리 표시자 값을 실제 조직 및 피드 세부 정보로 대체하세요.
|
||||
|
||||
| 제공업체 | Index URL | 사용자 이름 | 비밀번호 |
|
||||
|---------|-----------|-----------|---------|
|
||||
| **Azure DevOps Artifacts** | `https://pkgs.dev.azure.com/{org}/_packaging/{feed}/pypi/simple/` | 비어 있지 않은 임의의 문자열 (예: `token`) | Packaging Read 범위의 Personal Access Token (PAT) |
|
||||
| **GitHub Packages** | `https://pypi.pkg.github.com/{owner}/simple/` | GitHub 사용자 이름 | `read:packages` 범위의 Personal Access Token (classic) |
|
||||
| **GitLab Package Registry** | `https://gitlab.com/api/v4/projects/{project_id}/packages/pypi/simple/` | `__token__` | `read_api` 범위의 Project 또는 Personal Access Token |
|
||||
| **AWS CodeArtifact** | `aws codeartifact get-repository-endpoint`의 URL 사용 | `aws` | `aws codeartifact get-authorization-token`의 토큰 |
|
||||
| **Google Artifact Registry** | `https://{region}-python.pkg.dev/{project}/{repo}/simple/` | `_json_key_base64` | Base64로 인코딩된 서비스 계정 키 |
|
||||
| **JFrog Artifactory** | `https://{instance}.jfrog.io/artifactory/api/pypi/{repo}/simple/` | 사용자 이름 또는 이메일 | API 키 또는 ID 토큰 |
|
||||
| **자체 호스팅 (devpi, Nexus 등)** | 레지스트리의 simple API URL | 레지스트리 사용자 이름 | 레지스트리 비밀번호 |
|
||||
|
||||
<Tip>
|
||||
**AWS CodeArtifact**의 경우 인증 토큰이 주기적으로 만료됩니다.
|
||||
만료되면 `UV_INDEX_*_PASSWORD` 값을 갱신해야 합니다.
|
||||
CI/CD 파이프라인에서 이를 자동화하는 것을 고려하세요.
|
||||
</Tip>
|
||||
|
||||
## AMP에서 환경 변수 설정
|
||||
|
||||
프라이빗 레지스트리 자격 증명은 CrewAI AMP에서 환경 변수로 구성해야 합니다.
|
||||
두 가지 옵션이 있습니다:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="웹 인터페이스">
|
||||
1. [CrewAI AMP](https://app.crewai.com)에 로그인합니다
|
||||
2. 자동화로 이동합니다
|
||||
3. **Environment Variables** 탭을 엽니다
|
||||
4. 각 변수 (`UV_INDEX_*_USERNAME` 및 `UV_INDEX_*_PASSWORD`)에 값을 추가합니다
|
||||
|
||||
자세한 내용은 [AMP에 배포하기 — 환경 변수 설정하기](/ko/enterprise/guides/deploy-to-amp#환경-변수-설정하기) 단계를 참조하세요.
|
||||
</Tab>
|
||||
<Tab title="CLI 배포">
|
||||
`crewai deploy create`를 실행하기 전에 로컬 `.env` 파일에 변수를 추가합니다.
|
||||
CLI가 이를 안전하게 플랫폼으로 전송합니다:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
OPENAI_API_KEY=sk-...
|
||||
UV_INDEX_MY_PRIVATE_REGISTRY_USERNAME=token
|
||||
UV_INDEX_MY_PRIVATE_REGISTRY_PASSWORD=your-pat-here
|
||||
```
|
||||
|
||||
```bash
|
||||
crewai deploy create
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Warning>
|
||||
자격 증명을 저장소에 **절대** 커밋하지 마세요. 모든 비밀 정보에는 AMP 환경 변수를 사용하세요.
|
||||
`.env` 파일은 `.gitignore`에 포함되어야 합니다.
|
||||
</Warning>
|
||||
|
||||
기존 배포의 자격 증명을 업데이트하려면 [Crew 업데이트하기 — 환경 변수](/ko/enterprise/guides/update-crew)를 참조하세요.
|
||||
|
||||
## 전체 동작 흐름
|
||||
|
||||
CrewAI AMP가 자동화를 빌드할 때, 해결 흐름은 다음과 같이 작동합니다:
|
||||
|
||||
<Steps>
|
||||
<Step title="빌드 시작">
|
||||
AMP가 저장소를 가져오고 `pyproject.toml`과 `uv.lock`을 읽습니다.
|
||||
</Step>
|
||||
<Step title="UV가 의존성 해결">
|
||||
UV가 `[tool.uv.sources]`를 읽어 각 패키지가 어떤 index에서 와야 하는지 결정합니다.
|
||||
</Step>
|
||||
<Step title="UV가 인증">
|
||||
각 프라이빗 index에 대해 UV가 AMP에서 구성한 환경 변수에서
|
||||
`UV_INDEX_{NAME}_USERNAME`과 `UV_INDEX_{NAME}_PASSWORD`를 조회합니다.
|
||||
</Step>
|
||||
<Step title="패키지 설치">
|
||||
UV가 공개(PyPI) 및 프라이빗(레지스트리) 패키지를 모두 다운로드하고 설치합니다.
|
||||
</Step>
|
||||
<Step title="자동화 실행">
|
||||
모든 의존성이 사용 가능한 상태에서 crew 또는 flow가 시작됩니다.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 문제 해결
|
||||
|
||||
### 빌드 중 인증 오류
|
||||
|
||||
**증상**: 프라이빗 패키지를 해결할 때 `401 Unauthorized` 또는 `403 Forbidden`으로 빌드가 실패합니다.
|
||||
|
||||
**확인사항**:
|
||||
- `UV_INDEX_*` 환경 변수 이름이 index 이름과 정확히 일치하는지 확인합니다 (대문자, 하이픈 -> 언더스코어)
|
||||
- 자격 증명이 로컬 `.env`뿐만 아니라 AMP 환경 변수에 설정되어 있는지 확인합니다
|
||||
- 토큰/PAT에 패키지 피드에 필요한 읽기 권한이 있는지 확인합니다
|
||||
- 토큰이 만료되지 않았는지 확인합니다 (특히 AWS CodeArtifact의 경우)
|
||||
|
||||
### 패키지를 찾을 수 없음
|
||||
|
||||
**증상**: `No matching distribution found for my-private-package`.
|
||||
|
||||
**확인사항**:
|
||||
- `pyproject.toml`의 index URL이 `/simple/`로 끝나는지 확인합니다
|
||||
- `[tool.uv.sources]` 항목이 올바른 패키지 이름을 올바른 index 이름에 매핑하는지 확인합니다
|
||||
- 패키지가 실제로 프라이빗 레지스트리에 게시되어 있는지 확인합니다
|
||||
- 동일한 자격 증명으로 로컬에서 `uv lock`을 실행하여 해결이 작동하는지 확인합니다
|
||||
|
||||
### Lock 파일 충돌
|
||||
|
||||
**증상**: 프라이빗 index를 추가한 후 `uv lock`이 실패하거나 예상치 못한 결과를 생성합니다.
|
||||
|
||||
**해결책**: 로컬에서 자격 증명을 설정하고 다시 생성합니다:
|
||||
|
||||
```bash
|
||||
export UV_INDEX_MY_PRIVATE_REGISTRY_USERNAME=token
|
||||
export UV_INDEX_MY_PRIVATE_REGISTRY_PASSWORD=your-pat
|
||||
uv lock
|
||||
```
|
||||
|
||||
그런 다음 업데이트된 `uv.lock`을 커밋합니다.
|
||||
|
||||
## 관련 가이드
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="배포 준비하기" icon="clipboard-check" href="/ko/enterprise/guides/prepare-for-deployment">
|
||||
배포 전에 프로젝트 구조와 의존성을 확인합니다.
|
||||
</Card>
|
||||
<Card title="AMP에 배포하기" icon="rocket" href="/ko/enterprise/guides/deploy-to-amp">
|
||||
crew 또는 flow를 배포하고 환경 변수를 구성합니다.
|
||||
</Card>
|
||||
<Card title="Crew 업데이트하기" icon="arrows-rotate" href="/ko/enterprise/guides/update-crew">
|
||||
환경 변수를 업데이트하고 실행 중인 배포에 변경 사항을 푸시합니다.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
518
docs/ko/guides/migration/migrating-from-langgraph.mdx
Normal file
518
docs/ko/guides/migration/migrating-from-langgraph.mdx
Normal file
@@ -0,0 +1,518 @@
|
||||
---
|
||||
title: "LangGraph에서 CrewAI로 옮기기: 엔지니어를 위한 실전 가이드"
|
||||
description: LangGraph로 이미 구축했다면, 프로젝트를 CrewAI로 빠르게 옮기는 방법을 알아보세요
|
||||
icon: switch
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
LangGraph로 에이전트를 구축해 왔습니다. `StateGraph`와 씨름하고, 조건부 에지를 연결하고, 새벽 2시에 상태 딕셔너리를 디버깅해 본 적도 있죠. 동작은 하지만 — 어느 순간부터 프로덕션으로 가는 더 나은 길이 없을까 고민하게 됩니다.
|
||||
|
||||
있습니다. **CrewAI Flows**는 이벤트 기반 오케스트레이션, 조건부 라우팅, 공유 상태라는 동일한 힘을 훨씬 적은 보일러플레이트와 실제로 다단계 AI 워크플로우를 생각하는 방식에 잘 맞는 정신적 모델로 제공합니다.
|
||||
|
||||
이 글은 핵심 개념을 나란히 비교하고 실제 코드 비교를 보여주며, 다음으로 손이 갈 프레임워크가 왜 CrewAI Flows인지 설명합니다.
|
||||
|
||||
---
|
||||
|
||||
## 정신적 모델의 전환
|
||||
|
||||
LangGraph는 **그래프**로 생각하라고 요구합니다: 노드, 에지, 그리고 상태 딕셔너리. 모든 워크플로우는 계산 단계 사이의 전이를 명시적으로 연결하는 방향 그래프입니다. 강력하지만, 특히 워크플로우가 몇 개의 결정 지점이 있는 순차적 흐름일 때 이 추상화는 오버헤드를 가져옵니다.
|
||||
|
||||
CrewAI Flows는 **이벤트**로 생각하라고 요구합니다: 시작하는 메서드, 결과를 듣는 메서드, 실행을 라우팅하는 메서드. 워크플로우의 토폴로지는 명시적 그래프 구성 대신 데코레이터 어노테이션에서 드러납니다. 이것은 단순한 문법 설탕이 아니라 — 파이프라인을 설계하고 읽고 유지하는 방식을 바꿉니다.
|
||||
|
||||
핵심 매핑은 다음과 같습니다:
|
||||
|
||||
| LangGraph 개념 | CrewAI Flows 대응 |
|
||||
| --- | --- |
|
||||
| `StateGraph` class | `Flow` class |
|
||||
| `add_node()` | Methods decorated with `@start`, `@listen` |
|
||||
| `add_edge()` / `add_conditional_edges()` | `@listen()` / `@router()` decorators |
|
||||
| `TypedDict` state | Pydantic `BaseModel` state |
|
||||
| `START` / `END` constants | `@start()` decorator / natural method return |
|
||||
| `graph.compile()` | `flow.kickoff()` |
|
||||
| Checkpointer / persistence | Built-in memory (LanceDB-backed) |
|
||||
|
||||
실제로 어떻게 보이는지 살펴보겠습니다.
|
||||
|
||||
---
|
||||
|
||||
## 데모 1: 간단한 순차 파이프라인
|
||||
|
||||
주제를 받아 조사하고, 요약을 작성한 뒤, 결과를 포맷팅하는 파이프라인을 만든다고 해봅시다. 각 프레임워크는 이렇게 처리합니다.
|
||||
|
||||
### LangGraph 방식
|
||||
|
||||
```python
|
||||
from typing import TypedDict
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
|
||||
class ResearchState(TypedDict):
|
||||
topic: str
|
||||
raw_research: str
|
||||
summary: str
|
||||
formatted_output: str
|
||||
|
||||
def research_topic(state: ResearchState) -> dict:
|
||||
# Call an LLM or search API
|
||||
result = llm.invoke(f"Research the topic: {state['topic']}")
|
||||
return {"raw_research": result}
|
||||
|
||||
def write_summary(state: ResearchState) -> dict:
|
||||
result = llm.invoke(
|
||||
f"Summarize this research:\n{state['raw_research']}"
|
||||
)
|
||||
return {"summary": result}
|
||||
|
||||
def format_output(state: ResearchState) -> dict:
|
||||
result = llm.invoke(
|
||||
f"Format this summary as a polished article section:\n{state['summary']}"
|
||||
)
|
||||
return {"formatted_output": result}
|
||||
|
||||
# Build the graph
|
||||
graph = StateGraph(ResearchState)
|
||||
graph.add_node("research", research_topic)
|
||||
graph.add_node("summarize", write_summary)
|
||||
graph.add_node("format", format_output)
|
||||
|
||||
graph.add_edge(START, "research")
|
||||
graph.add_edge("research", "summarize")
|
||||
graph.add_edge("summarize", "format")
|
||||
graph.add_edge("format", END)
|
||||
|
||||
# Compile and run
|
||||
app = graph.compile()
|
||||
result = app.invoke({"topic": "quantum computing advances in 2026"})
|
||||
print(result["formatted_output"])
|
||||
```
|
||||
|
||||
함수를 정의하고 노드로 등록한 다음, 모든 전이를 수동으로 연결합니다. 이렇게 단순한 순서인데도 의례처럼 해야 할 작업이 많습니다.
|
||||
|
||||
### CrewAI Flows 방식
|
||||
|
||||
```python
|
||||
from crewai import LLM, Agent, Crew, Process, Task
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class ResearchState(BaseModel):
|
||||
topic: str = ""
|
||||
raw_research: str = ""
|
||||
summary: str = ""
|
||||
formatted_output: str = ""
|
||||
|
||||
class ResearchFlow(Flow[ResearchState]):
|
||||
@start()
|
||||
def research_topic(self):
|
||||
# Option 1: Direct LLM call
|
||||
result = llm.call(f"Research the topic: {self.state.topic}")
|
||||
self.state.raw_research = result
|
||||
return result
|
||||
|
||||
@listen(research_topic)
|
||||
def write_summary(self, research_output):
|
||||
# Option 2: A single agent
|
||||
summarizer = Agent(
|
||||
role="Research Summarizer",
|
||||
goal="Produce concise, accurate summaries of research content",
|
||||
backstory="You are an expert at distilling complex research into clear, "
|
||||
"digestible summaries.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
result = summarizer.kickoff(
|
||||
f"Summarize this research:\n{self.state.raw_research}"
|
||||
)
|
||||
self.state.summary = str(result)
|
||||
return self.state.summary
|
||||
|
||||
@listen(write_summary)
|
||||
def format_output(self, summary_output):
|
||||
# Option 3: a complete crew (with one or more agents)
|
||||
formatter = Agent(
|
||||
role="Content Formatter",
|
||||
goal="Transform research summaries into polished, publication-ready article sections",
|
||||
backstory="You are a skilled editor with expertise in structuring and "
|
||||
"presenting technical content for a general audience.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
format_task = Task(
|
||||
description=f"Format this summary as a polished article section:\n{self.state.summary}",
|
||||
expected_output="A well-structured, polished article section ready for publication.",
|
||||
agent=formatter,
|
||||
)
|
||||
crew = Crew(
|
||||
agents=[formatter],
|
||||
tasks=[format_task],
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
result = crew.kickoff()
|
||||
self.state.formatted_output = str(result)
|
||||
return self.state.formatted_output
|
||||
|
||||
# Run the flow
|
||||
flow = ResearchFlow()
|
||||
flow.state.topic = "quantum computing advances in 2026"
|
||||
result = flow.kickoff()
|
||||
print(flow.state.formatted_output)
|
||||
|
||||
```
|
||||
|
||||
눈에 띄는 차이점이 있습니다: 그래프 구성 없음, 에지 연결 없음, 컴파일 단계 없음. 실행 순서는 로직이 있는 곳에서 바로 선언됩니다. `@start()`는 진입점을 표시하고, `@listen(method_name)`은 단계들을 연결합니다. 상태는 타입 안전성, 검증, IDE 자동 완성까지 제공하는 제대로 된 Pydantic 모델입니다.
|
||||
|
||||
---
|
||||
|
||||
## 데모 2: 조건부 라우팅
|
||||
|
||||
여기서 흥미로워집니다. 콘텐츠 유형에 따라 서로 다른 처리 경로로 라우팅하는 파이프라인을 만든다고 해봅시다.
|
||||
|
||||
### LangGraph 방식
|
||||
|
||||
```python
|
||||
from typing import TypedDict, Literal
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
|
||||
class ContentState(TypedDict):
|
||||
input_text: str
|
||||
content_type: str
|
||||
result: str
|
||||
|
||||
def classify_content(state: ContentState) -> dict:
|
||||
content_type = llm.invoke(
|
||||
f"Classify this content as 'technical', 'creative', or 'business':\n{state['input_text']}"
|
||||
)
|
||||
return {"content_type": content_type.strip().lower()}
|
||||
|
||||
def process_technical(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as technical doc:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
def process_creative(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as creative writing:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
def process_business(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as business content:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
# Routing function
|
||||
def route_content(state: ContentState) -> Literal["technical", "creative", "business"]:
|
||||
return state["content_type"]
|
||||
|
||||
# Build the graph
|
||||
graph = StateGraph(ContentState)
|
||||
graph.add_node("classify", classify_content)
|
||||
graph.add_node("technical", process_technical)
|
||||
graph.add_node("creative", process_creative)
|
||||
graph.add_node("business", process_business)
|
||||
|
||||
graph.add_edge(START, "classify")
|
||||
graph.add_conditional_edges(
|
||||
"classify",
|
||||
route_content,
|
||||
{
|
||||
"technical": "technical",
|
||||
"creative": "creative",
|
||||
"business": "business",
|
||||
}
|
||||
)
|
||||
graph.add_edge("technical", END)
|
||||
graph.add_edge("creative", END)
|
||||
graph.add_edge("business", END)
|
||||
|
||||
app = graph.compile()
|
||||
result = app.invoke({"input_text": "Explain how TCP handshakes work"})
|
||||
```
|
||||
|
||||
별도의 라우팅 함수, 명시적 조건부 에지 매핑, 그리고 모든 분기에 대한 종료 에지가 필요합니다. 라우팅 결정 로직이 그 결정을 만들어 내는 노드와 분리됩니다.
|
||||
|
||||
### CrewAI Flows 방식
|
||||
|
||||
```python
|
||||
from crewai import LLM, Agent
|
||||
from crewai.flow.flow import Flow, listen, router, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class ContentState(BaseModel):
|
||||
input_text: str = ""
|
||||
content_type: str = ""
|
||||
result: str = ""
|
||||
|
||||
class ContentFlow(Flow[ContentState]):
|
||||
@start()
|
||||
def classify_content(self):
|
||||
self.state.content_type = (
|
||||
llm.call(
|
||||
f"Classify this content as 'technical', 'creative', or 'business':\n"
|
||||
f"{self.state.input_text}"
|
||||
)
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
return self.state.content_type
|
||||
|
||||
@router(classify_content)
|
||||
def route_content(self, classification):
|
||||
if classification == "technical":
|
||||
return "process_technical"
|
||||
elif classification == "creative":
|
||||
return "process_creative"
|
||||
else:
|
||||
return "process_business"
|
||||
|
||||
@listen("process_technical")
|
||||
def handle_technical(self):
|
||||
agent = Agent(
|
||||
role="Technical Writer",
|
||||
goal="Produce clear, accurate technical documentation",
|
||||
backstory="You are an expert technical writer who specializes in "
|
||||
"explaining complex technical concepts precisely.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as technical doc:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
@listen("process_creative")
|
||||
def handle_creative(self):
|
||||
agent = Agent(
|
||||
role="Creative Writer",
|
||||
goal="Craft engaging and imaginative creative content",
|
||||
backstory="You are a talented creative writer with a flair for "
|
||||
"compelling storytelling and vivid expression.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as creative writing:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
@listen("process_business")
|
||||
def handle_business(self):
|
||||
agent = Agent(
|
||||
role="Business Writer",
|
||||
goal="Produce professional, results-oriented business content",
|
||||
backstory="You are an experienced business writer who communicates "
|
||||
"strategy and value clearly to professional audiences.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as business content:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
flow = ContentFlow()
|
||||
flow.state.input_text = "Explain how TCP handshakes work"
|
||||
flow.kickoff()
|
||||
print(flow.state.result)
|
||||
|
||||
```
|
||||
|
||||
`@router()` 데코레이터는 메서드를 결정 지점으로 만듭니다. 리스너와 매칭되는 문자열을 반환하므로, 매핑 딕셔너리도, 별도의 라우팅 함수도 필요 없습니다. 분기 로직이 Python `if` 문처럼 읽히는 이유는, 실제로 `if` 문이기 때문입니다.
|
||||
|
||||
---
|
||||
|
||||
## 데모 3: AI 에이전트 Crew를 Flow에 통합하기
|
||||
|
||||
여기서 CrewAI의 진짜 힘이 드러납니다. Flows는 LLM 호출을 연결하는 것에 그치지 않고 자율적인 에이전트 **Crew** 전체를 오케스트레이션합니다. 이는 LangGraph에 기본으로 대응되는 개념이 없습니다.
|
||||
|
||||
```python
|
||||
from crewai import Agent, Task, Crew
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ArticleState(BaseModel):
|
||||
topic: str = ""
|
||||
research: str = ""
|
||||
draft: str = ""
|
||||
final_article: str = ""
|
||||
|
||||
class ArticleFlow(Flow[ArticleState]):
|
||||
|
||||
@start()
|
||||
def run_research_crew(self):
|
||||
"""A full Crew of agents handles research."""
|
||||
researcher = Agent(
|
||||
role="Senior Research Analyst",
|
||||
goal=f"Produce comprehensive research on: {self.state.topic}",
|
||||
backstory="You're a veteran analyst known for thorough, "
|
||||
"well-sourced research reports.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
research_task = Task(
|
||||
description=f"Research '{self.state.topic}' thoroughly. "
|
||||
"Cover key trends, data points, and expert opinions.",
|
||||
expected_output="A detailed research brief with sources.",
|
||||
agent=researcher
|
||||
)
|
||||
|
||||
crew = Crew(agents=[researcher], tasks=[research_task])
|
||||
result = crew.kickoff()
|
||||
self.state.research = result.raw
|
||||
return result.raw
|
||||
|
||||
@listen(run_research_crew)
|
||||
def run_writing_crew(self, research_output):
|
||||
"""A different Crew handles writing."""
|
||||
writer = Agent(
|
||||
role="Technical Writer",
|
||||
goal="Write a compelling article based on provided research.",
|
||||
backstory="You turn complex research into engaging, clear prose.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
editor = Agent(
|
||||
role="Senior Editor",
|
||||
goal="Review and polish articles for publication quality.",
|
||||
backstory="20 years of editorial experience at top tech publications.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
write_task = Task(
|
||||
description=f"Write an article based on this research:\n{self.state.research}",
|
||||
expected_output="A well-structured draft article.",
|
||||
agent=writer
|
||||
)
|
||||
|
||||
edit_task = Task(
|
||||
description="Review, fact-check, and polish the draft article.",
|
||||
expected_output="A publication-ready article.",
|
||||
agent=editor
|
||||
)
|
||||
|
||||
crew = Crew(agents=[writer, editor], tasks=[write_task, edit_task])
|
||||
result = crew.kickoff()
|
||||
self.state.final_article = result.raw
|
||||
return result.raw
|
||||
|
||||
# Run the full pipeline
|
||||
flow = ArticleFlow()
|
||||
flow.state.topic = "The Future of Edge AI"
|
||||
flow.kickoff()
|
||||
print(flow.state.final_article)
|
||||
```
|
||||
|
||||
핵심 인사이트는 다음과 같습니다: **Flows는 오케스트레이션 레이어를, Crews는 지능 레이어를 제공합니다.** Flow의 각 단계는 각자의 역할, 목표, 도구를 가진 협업 에이전트 팀을 띄울 수 있습니다. 구조화되고 예측 가능한 제어 흐름 *그리고* 자율적 에이전트 협업 — 두 세계의 장점을 모두 얻습니다.
|
||||
|
||||
LangGraph에서 비슷한 것을 하려면 노드 함수 안에 에이전트 통신 프로토콜, 도구 호출 루프, 위임 로직을 직접 구현해야 합니다. 가능하긴 하지만, 매번 처음부터 배관을 만드는 셈입니다.
|
||||
|
||||
---
|
||||
|
||||
## 데모 4: 병렬 실행과 동기화
|
||||
|
||||
실제 파이프라인은 종종 작업을 병렬로 분기하고 결과를 합쳐야 합니다. CrewAI Flows는 `and_`와 `or_` 연산자로 이를 우아하게 처리합니다.
|
||||
|
||||
```python
|
||||
from crewai import LLM
|
||||
from crewai.flow.flow import Flow, and_, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class AnalysisState(BaseModel):
|
||||
topic: str = ""
|
||||
market_data: str = ""
|
||||
tech_analysis: str = ""
|
||||
competitor_intel: str = ""
|
||||
final_report: str = ""
|
||||
|
||||
class ParallelAnalysisFlow(Flow[AnalysisState]):
|
||||
@start()
|
||||
def start_method(self):
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def gather_market_data(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def run_tech_analysis(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def gather_competitor_intel(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(and_(gather_market_data, run_tech_analysis, gather_competitor_intel))
|
||||
def synthesize_report(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
flow = ParallelAnalysisFlow()
|
||||
flow.state.topic = "AI-powered developer tools"
|
||||
flow.kickoff()
|
||||
|
||||
```
|
||||
|
||||
여러 `@start()` 데코레이터는 병렬로 실행됩니다. `@listen` 데코레이터의 `and_()` 결합자는 `synthesize_report`가 *세 가지* 상위 메서드가 모두 완료된 뒤에만 실행되도록 보장합니다. *어떤* 상위 작업이든 끝나는 즉시 진행하고 싶다면 `or_()`도 사용할 수 있습니다.
|
||||
|
||||
LangGraph에서는 병렬 분기, 동기화 노드, 신중한 상태 병합이 포함된 fan-out/fan-in 패턴을 만들어야 하며 — 모든 것을 에지로 명시적으로 연결해야 합니다.
|
||||
|
||||
---
|
||||
|
||||
## 프로덕션에서 CrewAI Flows를 쓰는 이유
|
||||
|
||||
깔끔한 문법을 넘어, Flows는 여러 프로덕션 핵심 이점을 제공합니다:
|
||||
|
||||
**내장 상태 지속성.** Flow 상태는 LanceDB에 의해 백업되므로 워크플로우가 크래시에서 살아남고, 재개될 수 있으며, 실행 간에 지식을 축적할 수 있습니다. LangGraph는 별도의 체크포인터를 구성해야 합니다.
|
||||
|
||||
**타입 안전한 상태 관리.** Pydantic 모델은 즉시 검증, 직렬화, IDE 지원을 제공합니다. LangGraph의 `TypedDict` 상태는 런타임 검증을 하지 않습니다.
|
||||
|
||||
**일급 에이전트 오케스트레이션.** Crews는 기본 프리미티브입니다. 역할, 목표, 배경, 도구를 가진 에이전트를 정의하고, Flow의 구조적 틀 안에서 자율적으로 협업하게 합니다. 다중 에이전트 조율을 다시 만들 필요가 없습니다.
|
||||
|
||||
**더 단순한 정신적 모델.** 데코레이터는 의도를 선언합니다. `@start`는 "여기서 시작", `@listen(x)`는 "x 이후 실행", `@router(x)`는 "x 이후 어디로 갈지 결정"을 의미합니다. 코드는 자신이 설명하는 워크플로우처럼 읽힙니다.
|
||||
|
||||
**CLI 통합.** `crewai run`으로 Flows를 실행합니다. 별도의 컴파일 단계나 그래프 직렬화가 없습니다. Flow는 Python 클래스이며, 그대로 실행됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 치트 시트
|
||||
|
||||
LangGraph 코드베이스를 CrewAI Flows로 옮기고 싶다면, 다음의 실전 변환 가이드를 참고하세요:
|
||||
|
||||
1. **상태를 매핑하세요.** `TypedDict`를 Pydantic `BaseModel`로 변환하고 모든 필드에 기본값을 추가하세요.
|
||||
2. **노드를 메서드로 변환하세요.** 각 `add_node` 함수는 `Flow` 서브클래스의 메서드가 됩니다. `state["field"]` 읽기는 `self.state.field`로 바꾸세요.
|
||||
3. **에지를 데코레이터로 교체하세요.** `add_edge(START, "first_node")`는 첫 메서드의 `@start()`가 됩니다. 순차적인 `add_edge("a", "b")`는 `b` 메서드의 `@listen(a)`가 됩니다.
|
||||
4. **조건부 에지는 `@router`로 교체하세요.** 라우팅 함수와 `add_conditional_edges()` 매핑은 하나의 `@router()` 메서드로 통합하고, 라우트 문자열을 반환하세요.
|
||||
5. **compile + invoke를 kickoff으로 교체하세요.** `graph.compile()`를 제거하고 `flow.kickoff()`를 호출하세요.
|
||||
6. **Crew가 들어갈 지점을 고려하세요.** 복잡한 다단계 에이전트 로직이 있는 노드는 Crew로 분리할 후보입니다. 이 부분에서 가장 큰 품질 향상을 체감할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
## 시작하기
|
||||
|
||||
CrewAI를 설치하고 새 Flow 프로젝트를 스캐폴딩하세요:
|
||||
|
||||
```bash
|
||||
pip install crewai
|
||||
crewai create flow my_first_flow
|
||||
cd my_first_flow
|
||||
```
|
||||
|
||||
이렇게 하면 바로 편집 가능한 Flow 클래스, 설정 파일, 그리고 `type = "flow"`가 이미 설정된 `pyproject.toml`이 포함된 프로젝트 구조가 생성됩니다. 다음으로 실행하세요:
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
|
||||
그 다음부터는 에이전트를 추가하고 리스너를 연결한 뒤, 배포하면 됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 마무리
|
||||
|
||||
LangGraph는 AI 워크플로우에 구조가 필요하다는 사실을 생태계에 일깨워 주었습니다. 중요한 교훈이었습니다. 하지만 CrewAI Flows는 그 교훈을 더 빠르게 쓰고, 더 쉽게 읽으며, 프로덕션에서 더 강력한 형태로 제공합니다 — 특히 워크플로우에 여러 에이전트의 협업이 포함될 때 그렇습니다.
|
||||
|
||||
단일 에이전트 체인을 넘는 무엇인가를 만들고 있다면, Flows를 진지하게 검토해 보세요. 데코레이터 기반 모델, Crews의 네이티브 통합, 내장 상태 관리를 통해 배관 작업에 쓰는 시간을 줄이고, 중요한 문제에 더 많은 시간을 쓸 수 있습니다.
|
||||
|
||||
`crewai create flow`로 시작하세요. 후회하지 않을 겁니다.
|
||||
@@ -7,7 +7,7 @@ mode: "wide"
|
||||
|
||||
## CrewAI를 LLM에 연결하기
|
||||
|
||||
CrewAI는 LiteLLM을 사용하여 다양한 언어 모델(LLM)에 연결합니다. 이 통합은 높은 다양성을 제공하여, 여러 공급자의 모델을 간단하고 통합된 인터페이스로 사용할 수 있게 해줍니다.
|
||||
CrewAI는 가장 인기 있는 제공자(OpenAI, Anthropic, Google Gemini, Azure, AWS Bedrock)에 대해 네이티브 SDK 통합을 통해 LLM에 연결하며, 그 외 모든 제공자에 대해서는 LiteLLM을 유연한 폴백으로 사용합니다.
|
||||
|
||||
<Note>
|
||||
기본적으로 CrewAI는 `gpt-4o-mini` 모델을 사용합니다. 이는 `OPENAI_MODEL_NAME` 환경 변수에 의해 결정되며, 설정되지 않은 경우 기본값은 "gpt-4o-mini"입니다.
|
||||
@@ -41,6 +41,14 @@ LiteLLM은 다음을 포함하되 이에 국한되지 않는 다양한 프로바
|
||||
|
||||
지원되는 프로바이더의 전체 및 최신 목록은 [LiteLLM 프로바이더 문서](https://docs.litellm.ai/docs/providers)를 참조하세요.
|
||||
|
||||
<Info>
|
||||
네이티브 통합에서 지원하지 않는 제공자를 사용하려면 LiteLLM을 프로젝트에 의존성으로 추가하세요:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
네이티브 제공자(OpenAI, Anthropic, Google Gemini, Azure, AWS Bedrock)는 자체 SDK extras를 사용합니다 — [공급자 구성 예시](/ko/concepts/llms#공급자-구성-예시)를 참조하세요.
|
||||
</Info>
|
||||
|
||||
## LLM 변경하기
|
||||
|
||||
CrewAI agent에서 다른 LLM을 사용하려면 여러 가지 방법이 있습니다:
|
||||
|
||||
@@ -35,7 +35,7 @@ crewai login
|
||||
아직 설치하지 않았다면 CLI 도구와 함께 CrewAI를 설치하세요:
|
||||
|
||||
```bash
|
||||
uv add crewai[tools]
|
||||
uv add 'crewai[tools]'
|
||||
```
|
||||
|
||||
그런 다음 CrewAI AMP 계정으로 CLI를 인증하세요:
|
||||
|
||||
@@ -18,77 +18,46 @@ Composio는 AI 에이전트를 250개 이상의 도구와 연결할 수 있는
|
||||
Composio 도구를 프로젝트에 통합하려면 아래 지침을 따르세요:
|
||||
|
||||
```shell
|
||||
pip install composio-crewai
|
||||
pip install composio composio-crewai
|
||||
pip install crewai
|
||||
```
|
||||
|
||||
설치가 완료된 후, `composio login`을 실행하거나 Composio API 키를 `COMPOSIO_API_KEY`로 export하세요. Composio API 키는 [여기](https://app.composio.dev)에서 받을 수 있습니다.
|
||||
설치가 완료되면 Composio API 키를 `COMPOSIO_API_KEY`로 설정하세요. Composio API 키는 [여기](https://platform.composio.dev)에서 받을 수 있습니다.
|
||||
|
||||
## 예시
|
||||
|
||||
다음 예시는 도구를 초기화하고 github action을 실행하는 방법을 보여줍니다:
|
||||
다음 예시는 도구를 초기화하고 GitHub 액션을 실행하는 방법을 보여줍니다:
|
||||
|
||||
1. Composio 도구 세트 초기화
|
||||
1. CrewAI Provider와 함께 Composio 초기화
|
||||
|
||||
```python Code
|
||||
from composio_crewai import ComposioToolSet, App, Action
|
||||
from composio_crewai import ComposioProvider
|
||||
from composio import Composio
|
||||
from crewai import Agent, Task, Crew
|
||||
|
||||
toolset = ComposioToolSet()
|
||||
composio = Composio(provider=ComposioProvider())
|
||||
```
|
||||
|
||||
2. GitHub 계정 연결
|
||||
2. 새 Composio 세션을 만들고 도구 가져오기
|
||||
<CodeGroup>
|
||||
```shell CLI
|
||||
composio add github
|
||||
```
|
||||
```python Code
|
||||
request = toolset.initiate_connection(app=App.GITHUB)
|
||||
print(f"Open this URL to authenticate: {request.redirectUrl}")
|
||||
```python
|
||||
session = composio.create(
|
||||
user_id="your-user-id",
|
||||
toolkits=["gmail", "github"] # optional, default is all toolkits
|
||||
)
|
||||
tools = session.tools()
|
||||
```
|
||||
세션 및 사용자 관리에 대한 자세한 내용은 [여기](https://docs.composio.dev/docs/configuring-sessions)를 참고하세요.
|
||||
</CodeGroup>
|
||||
|
||||
3. 도구 가져오기
|
||||
3. 사용자 수동 인증하기
|
||||
|
||||
- 앱에서 모든 도구를 가져오기 (프로덕션 환경에서는 권장하지 않음):
|
||||
Composio는 에이전트 채팅 세션 중에 사용자를 자동으로 인증합니다. 하지만 `authorize` 메서드를 호출해 사용자를 수동으로 인증할 수도 있습니다.
|
||||
```python Code
|
||||
tools = toolset.get_tools(apps=[App.GITHUB])
|
||||
connection_request = session.authorize("github")
|
||||
print(f"Open this URL to authenticate: {connection_request.redirect_url}")
|
||||
```
|
||||
|
||||
- 태그를 기반으로 도구 필터링:
|
||||
```python Code
|
||||
tag = "users"
|
||||
|
||||
filtered_action_enums = toolset.find_actions_by_tags(
|
||||
App.GITHUB,
|
||||
tags=[tag],
|
||||
)
|
||||
|
||||
tools = toolset.get_tools(actions=filtered_action_enums)
|
||||
```
|
||||
|
||||
- 사용 사례를 기반으로 도구 필터링:
|
||||
```python Code
|
||||
use_case = "Star a repository on GitHub"
|
||||
|
||||
filtered_action_enums = toolset.find_actions_by_use_case(
|
||||
App.GITHUB, use_case=use_case, advanced=False
|
||||
)
|
||||
|
||||
tools = toolset.get_tools(actions=filtered_action_enums)
|
||||
```
|
||||
<Tip>`advanced`를 True로 설정하면 복잡한 사용 사례를 위한 액션을 가져올 수 있습니다</Tip>
|
||||
|
||||
- 특정 도구 사용하기:
|
||||
|
||||
이 데모에서는 GitHub 앱의 `GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER` 액션을 사용합니다.
|
||||
```python Code
|
||||
tools = toolset.get_tools(
|
||||
actions=[Action.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER]
|
||||
)
|
||||
```
|
||||
액션 필터링에 대해 더 자세한 내용을 보려면 [여기](https://docs.composio.dev/patterns/tools/use-tools/use-specific-actions)를 참고하세요.
|
||||
|
||||
4. 에이전트 정의
|
||||
|
||||
```python Code
|
||||
@@ -116,4 +85,4 @@ crew = Crew(agents=[crewai_agent], tasks=[task])
|
||||
crew.kickoff()
|
||||
```
|
||||
|
||||
* 더욱 자세한 도구 리스트는 [여기](https://app.composio.dev)에서 확인하실 수 있습니다.
|
||||
* 더욱 자세한 도구 목록은 [여기](https://docs.composio.dev/toolkits)에서 확인할 수 있습니다.
|
||||
@@ -4,6 +4,195 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="13 mar 2026">
|
||||
## v1.10.2rc1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.10.2rc1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Adicionar comando de lançamento e acionar publicação no PyPI
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir bloqueio seguro entre processos e threads para I/O não protegido
|
||||
- Propagar contextvars através de todos os limites de thread e executor
|
||||
- Propagar ContextVars para threads de tarefas assíncronas
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.10.2a1
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@danglies007, @greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="11 mar 2026">
|
||||
## v1.10.2a1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.10.2a1)
|
||||
|
||||
## O que mudou
|
||||
|
||||
### Recursos
|
||||
- Adicionar suporte para busca de ferramentas, salvamento de tokens e injeção dinâmica de ferramentas apropriadas durante a execução para Anthropics.
|
||||
- Introduzir mais ferramentas de Busca Brave.
|
||||
- Criar ação para lançamentos noturnos.
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir LockException durante a execução concorrente de múltiplos processos.
|
||||
- Resolver problemas com a agrupação de resultados de ferramentas paralelas em uma única mensagem de usuário.
|
||||
- Abordar resoluções de ferramentas MCP e eliminar todas as conexões mutáveis compartilhadas.
|
||||
- Atualizar o manuseio de parâmetros LLM na função human_feedback.
|
||||
- Adicionar métodos de lista/dicionário ausentes a LockedListProxy e LockedDictProxy.
|
||||
- Propagar o contexto de contextvars para as threads de chamada de ferramentas paralelas.
|
||||
- Atualizar a dependência gitpython para >=3.1.41 para resolver a vulnerabilidade de travessia de diretórios CVE.
|
||||
|
||||
### Refatoração
|
||||
- Refatorar classes de memória para serem serializáveis.
|
||||
|
||||
### Documentação
|
||||
- Atualizar o changelog e a versão para v1.10.1.
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@akaKuruma, @github-actions[bot], @giulio-leone, @greysonlalonde, @joaomdmoura, @jonathansampson, @lorenzejay, @lucasgomide, @mattatcha
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="04 mar 2026">
|
||||
## v1.10.1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.10.1)
|
||||
|
||||
## O que mudou
|
||||
|
||||
### Recursos
|
||||
- Atualizar Gemini GenAI
|
||||
|
||||
### Correções de Bugs
|
||||
- Ajustar o valor do listener do executor para evitar recursão
|
||||
- Agrupar partes da resposta da função paralela em um único objeto Content no Gemini
|
||||
- Exibir a saída de pensamento dos modelos de pensamento no Gemini
|
||||
- Carregar ferramentas MCP e da plataforma quando as ferramentas do agente forem None
|
||||
- Suportar ambientes Jupyter com loops de eventos em A2A
|
||||
- Usar ID anônimo para rastreamentos efêmeros
|
||||
- Passar condicionalmente o cabeçalho plus
|
||||
- Ignorar o registro do manipulador de sinal em threads não principais para telemetria
|
||||
- Injetar erros de ferramentas como observações e resolver colisões de nomes
|
||||
- Atualizar pypdf de 4.x para 6.7.4 para resolver alertas do Dependabot
|
||||
- Resolver alertas de segurança críticos e altos do Dependabot
|
||||
|
||||
### Documentação
|
||||
- Sincronizar a documentação da ferramenta Composio entre locais
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@giulio-leone, @greysonlalonde, @haxzie, @joaomdmoura, @lorenzejay, @mattatcha, @mplachta, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="27 fev 2026">
|
||||
## v1.10.1a1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.10.1a1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Funcionalidades
|
||||
- Implementar suporte a invocação assíncrona em métodos de callback de etapas
|
||||
- Implementar carregamento sob demanda para dependências pesadas no módulo de Memória
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.10.0
|
||||
|
||||
### Refatoração
|
||||
- Refatorar métodos de callback de etapas para suportar invocação assíncrona
|
||||
- Refatorar para implementar carregamento sob demanda para dependências pesadas no módulo de Memória
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir branch para notas de lançamento
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@greysonlalonde, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="27 fev 2026">
|
||||
## v1.10.1a1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.10.1a1)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Refatoração
|
||||
- Refatorar métodos de callback de etapas para suportar invocação assíncrona
|
||||
- Implementar carregamento sob demanda para dependências pesadas no módulo de Memória
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.10.0
|
||||
|
||||
### Correções de Bugs
|
||||
- Criar branch para notas de lançamento
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@greysonlalonde, @joaomdmoura
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="26 fev 2026">
|
||||
## v1.10.0
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.10.0)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Recursos
|
||||
- Aprimorar a resolução da ferramenta MCP e eventos relacionados
|
||||
- Atualizar a versão do lancedb e adicionar pacotes lance-namespace
|
||||
- Aprimorar a análise e validação de argumentos JSON no CrewAgentExecutor e BaseTool
|
||||
- Migrar o cliente HTTP da CLI de requests para httpx
|
||||
- Adicionar documentação versionada
|
||||
- Adicionar detecção de versões removidas para notas de versão
|
||||
- Implementar tratamento de entrada do usuário em Flows
|
||||
- Aprimorar a funcionalidade de auto-loop HITL nos testes de integração de feedback humano
|
||||
- Adicionar started_event_id e definir no eventbus
|
||||
- Atualizar automaticamente tools.specs
|
||||
|
||||
### Correções de Bugs
|
||||
- Validar kwargs da ferramenta mesmo quando vazios para evitar TypeError crípticos
|
||||
- Preservar tipos nulos nos esquemas de parâmetros da ferramenta para LLM
|
||||
- Mapear output_pydantic/output_json para saída estruturada nativa
|
||||
- Garantir que callbacks sejam executados/aguardados se forem promessas
|
||||
- Capturar o nome do método no contexto da exceção
|
||||
- Preservar tipo enum no resultado do roteador; melhorar tipos
|
||||
- Corrigir fluxos cíclicos que quebram silenciosamente quando o ID de persistência é passado nas entradas
|
||||
- Corrigir o formato da flag da CLI de --skip-provider para --skip_provider
|
||||
- Garantir que o fluxo de chamada da ferramenta OpenAI seja finalizado
|
||||
- Resolver ponteiros $ref de esquema complexos nas ferramentas MCP
|
||||
- Impor additionalProperties=false nos esquemas
|
||||
- Rejeitar nomes de scripts reservados para pastas de equipe
|
||||
- Resolver condição de corrida no teste de emissão de eventos de guardrail
|
||||
|
||||
### Documentação
|
||||
- Adicionar nota de dependência litellm para provedores de LLM não nativos
|
||||
- Esclarecer o modelo de segurança NL2SQL e orientações de fortalecimento
|
||||
- Adicionar 96 ações ausentes em 9 integrações
|
||||
|
||||
### Refatoração
|
||||
- Refatorar crew para provider
|
||||
- Extrair HITL para padrão de provider
|
||||
- Melhorar tipagem e registro de hooks
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@dependabot[bot], @github-actions[bot], @github-code-quality[bot], @greysonlalonde, @heitorado, @hobostay, @joaomdmoura, @johnvan7, @jonathansampson, @lorenzejay, @lucasgomide, @mattatcha, @mplachta, @nicoferdi96, @theCyberTech, @thiagomoretto, @vinibrsl
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="26 jan 2026">
|
||||
## v1.9.0
|
||||
|
||||
|
||||
@@ -105,6 +105,15 @@ Existem diferentes locais no código do CrewAI onde você pode especificar o mod
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Info>
|
||||
O CrewAI oferece integrações nativas via SDK para OpenAI, Anthropic, Google (Gemini API), Azure e AWS Bedrock — sem necessidade de instalação extra além dos extras específicos do provedor (ex.: `uv add "crewai[openai]"`).
|
||||
|
||||
Todos os outros provedores são alimentados pelo **LiteLLM**. Se você planeja usar algum deles, adicione-o como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Info>
|
||||
|
||||
## Exemplos de Configuração de Provedores
|
||||
|
||||
O CrewAI suporta uma grande variedade de provedores de LLM, cada um com recursos, métodos de autenticação e capacidades de modelo únicos.
|
||||
@@ -214,6 +223,11 @@ Nesta seção, você encontrará exemplos detalhados que ajudam a selecionar, co
|
||||
| `meta_llama/Llama-4-Maverick-17B-128E-Instruct-FP8` | 128k | 4028 | Texto, Imagem | Texto |
|
||||
| `meta_llama/Llama-3.3-70B-Instruct` | 128k | 4028 | Texto | Texto |
|
||||
| `meta_llama/Llama-3.3-8B-Instruct` | 128k | 4028 | Texto | Texto |
|
||||
|
||||
**Nota:** Este provedor usa o LiteLLM. Adicione-o como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Anthropic">
|
||||
@@ -354,6 +368,11 @@ Nesta seção, você encontrará exemplos detalhados que ajudam a selecionar, co
|
||||
| gemini-1.5-flash | 1M tokens | Modelo multimodal equilibrado, bom para maioria das tarefas |
|
||||
| gemini-1.5-flash-8B | 1M tokens | Mais rápido, mais eficiente em custo, adequado para tarefas de alta frequência |
|
||||
| gemini-1.5-pro | 2M tokens | Melhor desempenho para uma ampla variedade de tarefas de raciocínio, incluindo lógica, codificação e colaboração criativa |
|
||||
|
||||
**Nota:** Este provedor usa o LiteLLM. Adicione-o como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Azure">
|
||||
@@ -438,6 +457,11 @@ Nesta seção, você encontrará exemplos detalhados que ajudam a selecionar, co
|
||||
model="sagemaker/<my-endpoint>"
|
||||
)
|
||||
```
|
||||
|
||||
**Nota:** Este provedor usa o LiteLLM. Adicione-o como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Mistral">
|
||||
@@ -453,6 +477,11 @@ Nesta seção, você encontrará exemplos detalhados que ajudam a selecionar, co
|
||||
temperature=0.7
|
||||
)
|
||||
```
|
||||
|
||||
**Nota:** Este provedor usa o LiteLLM. Adicione-o como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Nvidia NIM">
|
||||
@@ -539,6 +568,11 @@ Nesta seção, você encontrará exemplos detalhados que ajudam a selecionar, co
|
||||
| rakuten/rakutenai-7b-instruct | 1.024 tokens | LLM topo de linha, compreensão, raciocínio e geração textual.|
|
||||
| rakuten/rakutenai-7b-chat | 1.024 tokens | LLM topo de linha, compreensão, raciocínio e geração textual.|
|
||||
| baichuan-inc/baichuan2-13b-chat | 4.096 tokens | Suporte a chat em chinês/inglês, programação, matemática, seguir instruções, resolver quizzes.|
|
||||
|
||||
**Nota:** Este provedor usa o LiteLLM. Adicione-o como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Local NVIDIA NIM Deployed using WSL2">
|
||||
@@ -579,6 +613,11 @@ Nesta seção, você encontrará exemplos detalhados que ajudam a selecionar, co
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
**Nota:** Este provedor usa o LiteLLM. Adicione-o como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Groq">
|
||||
@@ -600,6 +639,11 @@ Nesta seção, você encontrará exemplos detalhados que ajudam a selecionar, co
|
||||
| Llama 3.1 70B/8B | 131.072 tokens | Alta performance e tarefas de contexto grande|
|
||||
| Llama 3.2 Série | 8.192 tokens | Tarefas gerais |
|
||||
| Mixtral 8x7B | 32.768 tokens | Equilíbrio entre performance e contexto |
|
||||
|
||||
**Nota:** Este provedor usa o LiteLLM. Adicione-o como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="IBM watsonx.ai">
|
||||
@@ -622,6 +666,11 @@ Nesta seção, você encontrará exemplos detalhados que ajudam a selecionar, co
|
||||
base_url="https://api.watsonx.ai/v1"
|
||||
)
|
||||
```
|
||||
|
||||
**Nota:** Este provedor usa o LiteLLM. Adicione-o como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Ollama (LLMs Locais)">
|
||||
@@ -635,6 +684,11 @@ Nesta seção, você encontrará exemplos detalhados que ajudam a selecionar, co
|
||||
base_url="http://localhost:11434"
|
||||
)
|
||||
```
|
||||
|
||||
**Nota:** Este provedor usa o LiteLLM. Adicione-o como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Fireworks AI">
|
||||
@@ -650,6 +704,11 @@ Nesta seção, você encontrará exemplos detalhados que ajudam a selecionar, co
|
||||
temperature=0.7
|
||||
)
|
||||
```
|
||||
|
||||
**Nota:** Este provedor usa o LiteLLM. Adicione-o como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Perplexity AI">
|
||||
@@ -665,6 +724,11 @@ Nesta seção, você encontrará exemplos detalhados que ajudam a selecionar, co
|
||||
base_url="https://api.perplexity.ai/"
|
||||
)
|
||||
```
|
||||
|
||||
**Nota:** Este provedor usa o LiteLLM. Adicione-o como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Hugging Face">
|
||||
@@ -679,6 +743,11 @@ Nesta seção, você encontrará exemplos detalhados que ajudam a selecionar, co
|
||||
model="huggingface/meta-llama/Meta-Llama-3.1-8B-Instruct"
|
||||
)
|
||||
```
|
||||
|
||||
**Nota:** Este provedor usa o LiteLLM. Adicione-o como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="SambaNova">
|
||||
@@ -702,6 +771,11 @@ Nesta seção, você encontrará exemplos detalhados que ajudam a selecionar, co
|
||||
| Llama 3.2 Série | 8.192 tokens | Tarefas gerais e multimodais |
|
||||
| Llama 3.3 70B | Até 131.072 tokens | Desempenho e qualidade de saída elevada |
|
||||
| Família Qwen2 | 8.192 tokens | Desempenho e qualidade de saída elevada |
|
||||
|
||||
**Nota:** Este provedor usa o LiteLLM. Adicione-o como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Cerebras">
|
||||
@@ -727,6 +801,11 @@ Nesta seção, você encontrará exemplos detalhados que ajudam a selecionar, co
|
||||
- Equilíbrio entre velocidade e qualidade
|
||||
- Suporte a longas janelas de contexto
|
||||
</Info>
|
||||
|
||||
**Nota:** Este provedor usa o LiteLLM. Adicione-o como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Open Router">
|
||||
@@ -749,6 +828,11 @@ Nesta seção, você encontrará exemplos detalhados que ajudam a selecionar, co
|
||||
- openrouter/deepseek/deepseek-r1
|
||||
- openrouter/deepseek/deepseek-chat
|
||||
</Info>
|
||||
|
||||
**Nota:** Este provedor usa o LiteLLM. Adicione-o como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
|
||||
@@ -176,6 +176,11 @@ Você precisa enviar seu crew para um repositório do GitHub. Caso ainda não te
|
||||

|
||||
</Frame>
|
||||
|
||||
<Info>
|
||||
Usando pacotes Python privados? Você também precisará adicionar suas credenciais de registro aqui.
|
||||
Consulte [Registros de Pacotes Privados](/pt-BR/enterprise/guides/private-package-registry) para as variáveis necessárias.
|
||||
</Info>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Implante Seu Crew">
|
||||
|
||||
@@ -256,6 +256,12 @@ Antes da implantação, certifique-se de ter:
|
||||
1. **Chaves de API de LLM** prontas (OpenAI, Anthropic, Google, etc.)
|
||||
2. **Chaves de API de ferramentas** se estiver usando ferramentas externas (Serper, etc.)
|
||||
|
||||
<Info>
|
||||
Se seu projeto depende de pacotes de um **registro PyPI privado**, você também precisará configurar
|
||||
credenciais de autenticação do registro como variáveis de ambiente. Consulte o guia
|
||||
[Registros de Pacotes Privados](/pt-BR/enterprise/guides/private-package-registry) para mais detalhes.
|
||||
</Info>
|
||||
|
||||
<Tip>
|
||||
Teste seu projeto localmente com as mesmas variáveis de ambiente antes de implantar
|
||||
para detectar problemas de configuração antecipadamente.
|
||||
|
||||
263
docs/pt-BR/enterprise/guides/private-package-registry.mdx
Normal file
263
docs/pt-BR/enterprise/guides/private-package-registry.mdx
Normal file
@@ -0,0 +1,263 @@
|
||||
---
|
||||
title: "Registros de Pacotes Privados"
|
||||
description: "Instale pacotes Python privados de registros PyPI autenticados no CrewAI AMP"
|
||||
icon: "lock"
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Este guia aborda como configurar seu projeto CrewAI para instalar pacotes Python
|
||||
de registros PyPI privados (Azure DevOps Artifacts, GitHub Packages, GitLab, AWS CodeArtifact, etc.)
|
||||
ao implantar no CrewAI AMP.
|
||||
</Note>
|
||||
|
||||
## Quando Você Precisa Disso
|
||||
|
||||
Se seu projeto depende de pacotes Python internos ou proprietários hospedados em um registro privado
|
||||
em vez do PyPI público, você precisará:
|
||||
|
||||
1. Informar ao UV **onde** encontrar o pacote (uma URL de index)
|
||||
2. Informar ao UV **quais** pacotes vêm desse index (um mapeamento de source)
|
||||
3. Fornecer **credenciais** para que o UV possa autenticar durante a instalação
|
||||
|
||||
O CrewAI AMP usa [UV](https://docs.astral.sh/uv/) para resolução e instalação de dependências.
|
||||
O UV suporta registros privados autenticados por meio da configuração do `pyproject.toml` combinada
|
||||
com variáveis de ambiente para credenciais.
|
||||
|
||||
## Passo 1: Configurar o pyproject.toml
|
||||
|
||||
Três elementos trabalham juntos no seu `pyproject.toml`:
|
||||
|
||||
### 1a. Declarar a dependência
|
||||
|
||||
Adicione o pacote privado ao seu `[project.dependencies]` como qualquer outra dependência:
|
||||
|
||||
```toml
|
||||
[project]
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.100.1,<1.0.0",
|
||||
"my-private-package>=1.2.0",
|
||||
]
|
||||
```
|
||||
|
||||
### 1b. Definir o index
|
||||
|
||||
Registre seu registro privado como um index nomeado em `[[tool.uv.index]]`:
|
||||
|
||||
```toml
|
||||
[[tool.uv.index]]
|
||||
name = "my-private-registry"
|
||||
url = "https://pkgs.dev.azure.com/my-org/_packaging/my-feed/pypi/simple/"
|
||||
explicit = true
|
||||
```
|
||||
|
||||
<Info>
|
||||
O campo `name` é importante — o UV o utiliza para construir os nomes das variáveis de ambiente
|
||||
para autenticação (veja o [Passo 2](#passo-2-configurar-credenciais-de-autenticação) abaixo).
|
||||
|
||||
Definir `explicit = true` significa que o UV não consultará esse index para todos os pacotes — apenas
|
||||
os que você mapear explicitamente em `[tool.uv.sources]`. Isso evita consultas desnecessárias
|
||||
ao seu registro privado e protege contra ataques de confusão de dependências.
|
||||
</Info>
|
||||
|
||||
### 1c. Mapear o pacote para o index
|
||||
|
||||
Informe ao UV quais pacotes devem ser resolvidos a partir do seu index privado usando `[tool.uv.sources]`:
|
||||
|
||||
```toml
|
||||
[tool.uv.sources]
|
||||
my-private-package = { index = "my-private-registry" }
|
||||
```
|
||||
|
||||
### Exemplo completo
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "my-crew-project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.10,<=3.13"
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.100.1,<1.0.0",
|
||||
"my-private-package>=1.2.0",
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "my-private-registry"
|
||||
url = "https://pkgs.dev.azure.com/my-org/_packaging/my-feed/pypi/simple/"
|
||||
explicit = true
|
||||
|
||||
[tool.uv.sources]
|
||||
my-private-package = { index = "my-private-registry" }
|
||||
```
|
||||
|
||||
Após atualizar o `pyproject.toml`, regenere seu arquivo lock:
|
||||
|
||||
```bash
|
||||
uv lock
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Sempre faça commit do `uv.lock` atualizado junto com as alterações no `pyproject.toml`.
|
||||
O arquivo lock é obrigatório para implantação — veja [Preparar para Implantação](/pt-BR/enterprise/guides/prepare-for-deployment).
|
||||
</Warning>
|
||||
|
||||
## Passo 2: Configurar Credenciais de Autenticação
|
||||
|
||||
O UV autentica em indexes privados usando variáveis de ambiente que seguem uma convenção de nomenclatura
|
||||
baseada no nome do index que você definiu no `pyproject.toml`:
|
||||
|
||||
```
|
||||
UV_INDEX_{UPPER_NAME}_USERNAME
|
||||
UV_INDEX_{UPPER_NAME}_PASSWORD
|
||||
```
|
||||
|
||||
Onde `{UPPER_NAME}` é o nome do seu index convertido para **maiúsculas** com **hifens substituídos por underscores**.
|
||||
|
||||
Por exemplo, um index chamado `my-private-registry` usa:
|
||||
|
||||
| Variável | Valor |
|
||||
|----------|-------|
|
||||
| `UV_INDEX_MY_PRIVATE_REGISTRY_USERNAME` | Seu nome de usuário ou nome do token do registro |
|
||||
| `UV_INDEX_MY_PRIVATE_REGISTRY_PASSWORD` | Sua senha ou token/PAT do registro |
|
||||
|
||||
<Warning>
|
||||
Essas variáveis de ambiente **devem** ser adicionadas pelas configurações de **Variáveis de Ambiente** do CrewAI AMP —
|
||||
globalmente ou no nível da implantação. Elas não podem ser definidas em arquivos `.env` ou codificadas no seu projeto.
|
||||
|
||||
Veja [Configurar Variáveis de Ambiente no AMP](#configurar-variáveis-de-ambiente-no-amp) abaixo.
|
||||
</Warning>
|
||||
|
||||
## Referência de Provedores de Registro
|
||||
|
||||
A tabela abaixo mostra o formato da URL de index e os valores de credenciais para provedores de registro comuns.
|
||||
Substitua os valores de exemplo pelos detalhes reais da sua organização e feed.
|
||||
|
||||
| Provedor | URL do Index | Usuário | Senha |
|
||||
|----------|-------------|---------|-------|
|
||||
| **Azure DevOps Artifacts** | `https://pkgs.dev.azure.com/{org}/_packaging/{feed}/pypi/simple/` | Qualquer string não vazia (ex: `token`) | Personal Access Token (PAT) com escopo Packaging Read |
|
||||
| **GitHub Packages** | `https://pypi.pkg.github.com/{owner}/simple/` | Nome de usuário do GitHub | Personal Access Token (classic) com escopo `read:packages` |
|
||||
| **GitLab Package Registry** | `https://gitlab.com/api/v4/projects/{project_id}/packages/pypi/simple/` | `__token__` | Project ou Personal Access Token com escopo `read_api` |
|
||||
| **AWS CodeArtifact** | Use a URL de `aws codeartifact get-repository-endpoint` | `aws` | Token de `aws codeartifact get-authorization-token` |
|
||||
| **Google Artifact Registry** | `https://{region}-python.pkg.dev/{project}/{repo}/simple/` | `_json_key_base64` | Chave de conta de serviço codificada em Base64 |
|
||||
| **JFrog Artifactory** | `https://{instance}.jfrog.io/artifactory/api/pypi/{repo}/simple/` | Nome de usuário ou email | Chave API ou token de identidade |
|
||||
| **Auto-hospedado (devpi, Nexus, etc.)** | URL da API simple do seu registro | Nome de usuário do registro | Senha do registro |
|
||||
|
||||
<Tip>
|
||||
Para **AWS CodeArtifact**, o token de autorização expira periodicamente.
|
||||
Você precisará atualizar o valor de `UV_INDEX_*_PASSWORD` quando ele expirar.
|
||||
Considere automatizar isso no seu pipeline de CI/CD.
|
||||
</Tip>
|
||||
|
||||
## Configurar Variáveis de Ambiente no AMP
|
||||
|
||||
As credenciais do registro privado devem ser configuradas como variáveis de ambiente no CrewAI AMP.
|
||||
Você tem duas opções:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Interface Web">
|
||||
1. Faça login no [CrewAI AMP](https://app.crewai.com)
|
||||
2. Navegue até sua automação
|
||||
3. Abra a aba **Environment Variables**
|
||||
4. Adicione cada variável (`UV_INDEX_*_USERNAME` e `UV_INDEX_*_PASSWORD`) com seu valor
|
||||
|
||||
Veja o passo [Deploy para AMP — Definir Variáveis de Ambiente](/pt-BR/enterprise/guides/deploy-to-amp#definir-as-variáveis-de-ambiente) para detalhes.
|
||||
</Tab>
|
||||
<Tab title="Implantação via CLI">
|
||||
Adicione as variáveis ao seu arquivo `.env` local antes de executar `crewai deploy create`.
|
||||
A CLI as transferirá com segurança para a plataforma:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
OPENAI_API_KEY=sk-...
|
||||
UV_INDEX_MY_PRIVATE_REGISTRY_USERNAME=token
|
||||
UV_INDEX_MY_PRIVATE_REGISTRY_PASSWORD=your-pat-here
|
||||
```
|
||||
|
||||
```bash
|
||||
crewai deploy create
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Warning>
|
||||
**Nunca** faça commit de credenciais no seu repositório. Use variáveis de ambiente do AMP para todos os segredos.
|
||||
O arquivo `.env` deve estar listado no `.gitignore`.
|
||||
</Warning>
|
||||
|
||||
Para atualizar credenciais em uma implantação existente, veja [Atualizar Seu Crew — Variáveis de Ambiente](/pt-BR/enterprise/guides/update-crew).
|
||||
|
||||
## Como Tudo se Conecta
|
||||
|
||||
Quando o CrewAI AMP faz o build da sua automação, o fluxo de resolução funciona assim:
|
||||
|
||||
<Steps>
|
||||
<Step title="Build inicia">
|
||||
O AMP busca seu repositório e lê o `pyproject.toml` e o `uv.lock`.
|
||||
</Step>
|
||||
<Step title="UV resolve dependências">
|
||||
O UV lê `[tool.uv.sources]` para determinar de qual index cada pacote deve vir.
|
||||
</Step>
|
||||
<Step title="UV autentica">
|
||||
Para cada index privado, o UV busca `UV_INDEX_{NAME}_USERNAME` e `UV_INDEX_{NAME}_PASSWORD`
|
||||
nas variáveis de ambiente que você configurou no AMP.
|
||||
</Step>
|
||||
<Step title="Pacotes são instalados">
|
||||
O UV baixa e instala todos os pacotes — tanto públicos (do PyPI) quanto privados (do seu registro).
|
||||
</Step>
|
||||
<Step title="Automação executa">
|
||||
Seu crew ou flow inicia com todas as dependências disponíveis.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Solução de Problemas
|
||||
|
||||
### Erros de Autenticação Durante o Build
|
||||
|
||||
**Sintoma**: Build falha com `401 Unauthorized` ou `403 Forbidden` ao resolver um pacote privado.
|
||||
|
||||
**Verifique**:
|
||||
- Os nomes das variáveis de ambiente `UV_INDEX_*` correspondem exatamente ao nome do seu index (maiúsculas, hifens -> underscores)
|
||||
- As credenciais estão definidas nas variáveis de ambiente do AMP, não apenas em um `.env` local
|
||||
- Seu token/PAT tem as permissões de leitura necessárias para o feed de pacotes
|
||||
- O token não expirou (especialmente relevante para AWS CodeArtifact)
|
||||
|
||||
### Pacote Não Encontrado
|
||||
|
||||
**Sintoma**: `No matching distribution found for my-private-package`.
|
||||
|
||||
**Verifique**:
|
||||
- A URL do index no `pyproject.toml` termina com `/simple/`
|
||||
- A entrada `[tool.uv.sources]` mapeia o nome correto do pacote para o nome correto do index
|
||||
- O pacote está realmente publicado no seu registro privado
|
||||
- Execute `uv lock` localmente com as mesmas credenciais para verificar se a resolução funciona
|
||||
|
||||
### Conflitos no Arquivo Lock
|
||||
|
||||
**Sintoma**: `uv lock` falha ou produz resultados inesperados após adicionar um index privado.
|
||||
|
||||
**Solução**: Defina as credenciais localmente e regenere:
|
||||
|
||||
```bash
|
||||
export UV_INDEX_MY_PRIVATE_REGISTRY_USERNAME=token
|
||||
export UV_INDEX_MY_PRIVATE_REGISTRY_PASSWORD=your-pat
|
||||
uv lock
|
||||
```
|
||||
|
||||
Em seguida, faça commit do `uv.lock` atualizado.
|
||||
|
||||
## Guias Relacionados
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Preparar para Implantação" icon="clipboard-check" href="/pt-BR/enterprise/guides/prepare-for-deployment">
|
||||
Verifique a estrutura do projeto e as dependências antes de implantar.
|
||||
</Card>
|
||||
<Card title="Deploy para AMP" icon="rocket" href="/pt-BR/enterprise/guides/deploy-to-amp">
|
||||
Implante seu crew ou flow e configure variáveis de ambiente.
|
||||
</Card>
|
||||
<Card title="Atualizar Seu Crew" icon="arrows-rotate" href="/pt-BR/enterprise/guides/update-crew">
|
||||
Atualize variáveis de ambiente e envie alterações para uma implantação em execução.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
518
docs/pt-BR/guides/migration/migrating-from-langgraph.mdx
Normal file
518
docs/pt-BR/guides/migration/migrating-from-langgraph.mdx
Normal file
@@ -0,0 +1,518 @@
|
||||
---
|
||||
title: "Migrando do LangGraph para o CrewAI: um guia prático para engenheiros"
|
||||
description: Se você já construiu com LangGraph, saiba como portar rapidamente seus projetos para o CrewAI
|
||||
icon: switch
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
Você construiu agentes com LangGraph. Já lutou com o `StateGraph`, ligou arestas condicionais e depurou dicionários de estado às 2 da manhã. Funciona — mas, em algum momento, você começou a se perguntar se existe um caminho melhor para produção.
|
||||
|
||||
Existe. **CrewAI Flows** entrega o mesmo poder — orquestração orientada a eventos, roteamento condicional, estado compartilhado — com muito menos boilerplate e um modelo mental que se alinha a como você realmente pensa sobre fluxos de trabalho de IA em múltiplas etapas.
|
||||
|
||||
Este artigo apresenta os conceitos principais lado a lado, mostra comparações reais de código e demonstra por que o CrewAI Flows é o framework que você vai querer usar a seguir.
|
||||
|
||||
---
|
||||
|
||||
## A Mudança de Modelo Mental
|
||||
|
||||
LangGraph pede que você pense em **grafos**: nós, arestas e dicionários de estado. Todo workflow é um grafo direcionado em que você conecta explicitamente as transições entre as etapas de computação. É poderoso, mas a abstração traz overhead — especialmente quando o seu fluxo é fundamentalmente sequencial com alguns pontos de decisão.
|
||||
|
||||
CrewAI Flows pede que você pense em **eventos**: métodos que iniciam, métodos que escutam resultados e métodos que roteiam a execução. A topologia do workflow emerge de anotações com decorators, em vez de construção explícita do grafo. Isso não é apenas açúcar sintático — muda como você projeta, lê e mantém seus pipelines.
|
||||
|
||||
Veja o mapeamento principal:
|
||||
|
||||
| Conceito no LangGraph | Equivalente no CrewAI Flows |
|
||||
| --- | --- |
|
||||
| `StateGraph` class | `Flow` class |
|
||||
| `add_node()` | Methods decorated with `@start`, `@listen` |
|
||||
| `add_edge()` / `add_conditional_edges()` | `@listen()` / `@router()` decorators |
|
||||
| `TypedDict` state | Pydantic `BaseModel` state |
|
||||
| `START` / `END` constants | `@start()` decorator / natural method return |
|
||||
| `graph.compile()` | `flow.kickoff()` |
|
||||
| Checkpointer / persistence | Built-in memory (LanceDB-backed) |
|
||||
|
||||
Vamos ver como isso fica na prática.
|
||||
|
||||
---
|
||||
|
||||
## Demo 1: Um Pipeline Sequencial Simples
|
||||
|
||||
Imagine que você está construindo um pipeline que recebe um tema, pesquisa, escreve um resumo e formata a saída. Veja como cada framework lida com isso.
|
||||
|
||||
### Abordagem com LangGraph
|
||||
|
||||
```python
|
||||
from typing import TypedDict
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
|
||||
class ResearchState(TypedDict):
|
||||
topic: str
|
||||
raw_research: str
|
||||
summary: str
|
||||
formatted_output: str
|
||||
|
||||
def research_topic(state: ResearchState) -> dict:
|
||||
# Call an LLM or search API
|
||||
result = llm.invoke(f"Research the topic: {state['topic']}")
|
||||
return {"raw_research": result}
|
||||
|
||||
def write_summary(state: ResearchState) -> dict:
|
||||
result = llm.invoke(
|
||||
f"Summarize this research:\n{state['raw_research']}"
|
||||
)
|
||||
return {"summary": result}
|
||||
|
||||
def format_output(state: ResearchState) -> dict:
|
||||
result = llm.invoke(
|
||||
f"Format this summary as a polished article section:\n{state['summary']}"
|
||||
)
|
||||
return {"formatted_output": result}
|
||||
|
||||
# Build the graph
|
||||
graph = StateGraph(ResearchState)
|
||||
graph.add_node("research", research_topic)
|
||||
graph.add_node("summarize", write_summary)
|
||||
graph.add_node("format", format_output)
|
||||
|
||||
graph.add_edge(START, "research")
|
||||
graph.add_edge("research", "summarize")
|
||||
graph.add_edge("summarize", "format")
|
||||
graph.add_edge("format", END)
|
||||
|
||||
# Compile and run
|
||||
app = graph.compile()
|
||||
result = app.invoke({"topic": "quantum computing advances in 2026"})
|
||||
print(result["formatted_output"])
|
||||
```
|
||||
|
||||
Você define funções, registra-as como nós e conecta manualmente cada transição. Para uma sequência simples como essa, há muita cerimônia.
|
||||
|
||||
### Abordagem com CrewAI Flows
|
||||
|
||||
```python
|
||||
from crewai import LLM, Agent, Crew, Process, Task
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class ResearchState(BaseModel):
|
||||
topic: str = ""
|
||||
raw_research: str = ""
|
||||
summary: str = ""
|
||||
formatted_output: str = ""
|
||||
|
||||
class ResearchFlow(Flow[ResearchState]):
|
||||
@start()
|
||||
def research_topic(self):
|
||||
# Option 1: Direct LLM call
|
||||
result = llm.call(f"Research the topic: {self.state.topic}")
|
||||
self.state.raw_research = result
|
||||
return result
|
||||
|
||||
@listen(research_topic)
|
||||
def write_summary(self, research_output):
|
||||
# Option 2: A single agent
|
||||
summarizer = Agent(
|
||||
role="Research Summarizer",
|
||||
goal="Produce concise, accurate summaries of research content",
|
||||
backstory="You are an expert at distilling complex research into clear, "
|
||||
"digestible summaries.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
result = summarizer.kickoff(
|
||||
f"Summarize this research:\n{self.state.raw_research}"
|
||||
)
|
||||
self.state.summary = str(result)
|
||||
return self.state.summary
|
||||
|
||||
@listen(write_summary)
|
||||
def format_output(self, summary_output):
|
||||
# Option 3: a complete crew (with one or more agents)
|
||||
formatter = Agent(
|
||||
role="Content Formatter",
|
||||
goal="Transform research summaries into polished, publication-ready article sections",
|
||||
backstory="You are a skilled editor with expertise in structuring and "
|
||||
"presenting technical content for a general audience.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
format_task = Task(
|
||||
description=f"Format this summary as a polished article section:\n{self.state.summary}",
|
||||
expected_output="A well-structured, polished article section ready for publication.",
|
||||
agent=formatter,
|
||||
)
|
||||
crew = Crew(
|
||||
agents=[formatter],
|
||||
tasks=[format_task],
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
result = crew.kickoff()
|
||||
self.state.formatted_output = str(result)
|
||||
return self.state.formatted_output
|
||||
|
||||
# Run the flow
|
||||
flow = ResearchFlow()
|
||||
flow.state.topic = "quantum computing advances in 2026"
|
||||
result = flow.kickoff()
|
||||
print(flow.state.formatted_output)
|
||||
|
||||
```
|
||||
|
||||
Repare a diferença: nada de construção de grafo, de ligação de arestas, nem de etapa de compilação. A ordem de execução é declarada exatamente onde a lógica vive. `@start()` marca o ponto de entrada, e `@listen(method_name)` encadeia as etapas. O estado é um modelo Pydantic de verdade, com segurança de tipos, validação e auto-complete na IDE.
|
||||
|
||||
---
|
||||
|
||||
## Demo 2: Roteamento Condicional
|
||||
|
||||
Aqui é que fica interessante. Digamos que você está construindo um pipeline de conteúdo que roteia para diferentes caminhos de processamento com base no tipo de conteúdo detectado.
|
||||
|
||||
### Abordagem com LangGraph
|
||||
|
||||
```python
|
||||
from typing import TypedDict, Literal
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
|
||||
class ContentState(TypedDict):
|
||||
input_text: str
|
||||
content_type: str
|
||||
result: str
|
||||
|
||||
def classify_content(state: ContentState) -> dict:
|
||||
content_type = llm.invoke(
|
||||
f"Classify this content as 'technical', 'creative', or 'business':\n{state['input_text']}"
|
||||
)
|
||||
return {"content_type": content_type.strip().lower()}
|
||||
|
||||
def process_technical(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as technical doc:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
def process_creative(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as creative writing:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
def process_business(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as business content:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
# Routing function
|
||||
def route_content(state: ContentState) -> Literal["technical", "creative", "business"]:
|
||||
return state["content_type"]
|
||||
|
||||
# Build the graph
|
||||
graph = StateGraph(ContentState)
|
||||
graph.add_node("classify", classify_content)
|
||||
graph.add_node("technical", process_technical)
|
||||
graph.add_node("creative", process_creative)
|
||||
graph.add_node("business", process_business)
|
||||
|
||||
graph.add_edge(START, "classify")
|
||||
graph.add_conditional_edges(
|
||||
"classify",
|
||||
route_content,
|
||||
{
|
||||
"technical": "technical",
|
||||
"creative": "creative",
|
||||
"business": "business",
|
||||
}
|
||||
)
|
||||
graph.add_edge("technical", END)
|
||||
graph.add_edge("creative", END)
|
||||
graph.add_edge("business", END)
|
||||
|
||||
app = graph.compile()
|
||||
result = app.invoke({"input_text": "Explain how TCP handshakes work"})
|
||||
```
|
||||
|
||||
Você precisa de uma função de roteamento separada, de um mapeamento explícito de arestas condicionais e de arestas de término para cada ramificação. A lógica de roteamento fica desacoplada do nó que produz a decisão.
|
||||
|
||||
### Abordagem com CrewAI Flows
|
||||
|
||||
```python
|
||||
from crewai import LLM, Agent
|
||||
from crewai.flow.flow import Flow, listen, router, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class ContentState(BaseModel):
|
||||
input_text: str = ""
|
||||
content_type: str = ""
|
||||
result: str = ""
|
||||
|
||||
class ContentFlow(Flow[ContentState]):
|
||||
@start()
|
||||
def classify_content(self):
|
||||
self.state.content_type = (
|
||||
llm.call(
|
||||
f"Classify this content as 'technical', 'creative', or 'business':\n"
|
||||
f"{self.state.input_text}"
|
||||
)
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
return self.state.content_type
|
||||
|
||||
@router(classify_content)
|
||||
def route_content(self, classification):
|
||||
if classification == "technical":
|
||||
return "process_technical"
|
||||
elif classification == "creative":
|
||||
return "process_creative"
|
||||
else:
|
||||
return "process_business"
|
||||
|
||||
@listen("process_technical")
|
||||
def handle_technical(self):
|
||||
agent = Agent(
|
||||
role="Technical Writer",
|
||||
goal="Produce clear, accurate technical documentation",
|
||||
backstory="You are an expert technical writer who specializes in "
|
||||
"explaining complex technical concepts precisely.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as technical doc:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
@listen("process_creative")
|
||||
def handle_creative(self):
|
||||
agent = Agent(
|
||||
role="Creative Writer",
|
||||
goal="Craft engaging and imaginative creative content",
|
||||
backstory="You are a talented creative writer with a flair for "
|
||||
"compelling storytelling and vivid expression.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as creative writing:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
@listen("process_business")
|
||||
def handle_business(self):
|
||||
agent = Agent(
|
||||
role="Business Writer",
|
||||
goal="Produce professional, results-oriented business content",
|
||||
backstory="You are an experienced business writer who communicates "
|
||||
"strategy and value clearly to professional audiences.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as business content:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
flow = ContentFlow()
|
||||
flow.state.input_text = "Explain how TCP handshakes work"
|
||||
flow.kickoff()
|
||||
print(flow.state.result)
|
||||
|
||||
```
|
||||
|
||||
O decorator `@router()` transforma um método em um ponto de decisão. Ele retorna uma string que corresponde a um listener — sem dicionários de mapeamento, sem funções de roteamento separadas. A lógica de ramificação parece um `if` em Python porque *é* um.
|
||||
|
||||
---
|
||||
|
||||
## Demo 3: Integrando Crews de Agentes de IA em Flows
|
||||
|
||||
É aqui que o verdadeiro poder do CrewAI aparece. Flows não servem apenas para encadear chamadas de LLM — elas orquestram **Crews** completas de agentes autônomos. Isso é algo para o qual o LangGraph simplesmente não tem um equivalente nativo.
|
||||
|
||||
```python
|
||||
from crewai import Agent, Task, Crew
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ArticleState(BaseModel):
|
||||
topic: str = ""
|
||||
research: str = ""
|
||||
draft: str = ""
|
||||
final_article: str = ""
|
||||
|
||||
class ArticleFlow(Flow[ArticleState]):
|
||||
|
||||
@start()
|
||||
def run_research_crew(self):
|
||||
"""A full Crew of agents handles research."""
|
||||
researcher = Agent(
|
||||
role="Senior Research Analyst",
|
||||
goal=f"Produce comprehensive research on: {self.state.topic}",
|
||||
backstory="You're a veteran analyst known for thorough, "
|
||||
"well-sourced research reports.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
research_task = Task(
|
||||
description=f"Research '{self.state.topic}' thoroughly. "
|
||||
"Cover key trends, data points, and expert opinions.",
|
||||
expected_output="A detailed research brief with sources.",
|
||||
agent=researcher
|
||||
)
|
||||
|
||||
crew = Crew(agents=[researcher], tasks=[research_task])
|
||||
result = crew.kickoff()
|
||||
self.state.research = result.raw
|
||||
return result.raw
|
||||
|
||||
@listen(run_research_crew)
|
||||
def run_writing_crew(self, research_output):
|
||||
"""A different Crew handles writing."""
|
||||
writer = Agent(
|
||||
role="Technical Writer",
|
||||
goal="Write a compelling article based on provided research.",
|
||||
backstory="You turn complex research into engaging, clear prose.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
editor = Agent(
|
||||
role="Senior Editor",
|
||||
goal="Review and polish articles for publication quality.",
|
||||
backstory="20 years of editorial experience at top tech publications.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
write_task = Task(
|
||||
description=f"Write an article based on this research:\n{self.state.research}",
|
||||
expected_output="A well-structured draft article.",
|
||||
agent=writer
|
||||
)
|
||||
|
||||
edit_task = Task(
|
||||
description="Review, fact-check, and polish the draft article.",
|
||||
expected_output="A publication-ready article.",
|
||||
agent=editor
|
||||
)
|
||||
|
||||
crew = Crew(agents=[writer, editor], tasks=[write_task, edit_task])
|
||||
result = crew.kickoff()
|
||||
self.state.final_article = result.raw
|
||||
return result.raw
|
||||
|
||||
# Run the full pipeline
|
||||
flow = ArticleFlow()
|
||||
flow.state.topic = "The Future of Edge AI"
|
||||
flow.kickoff()
|
||||
print(flow.state.final_article)
|
||||
```
|
||||
|
||||
Este é o insight-chave: **Flows fornecem a camada de orquestração, e Crews fornecem a camada de inteligência.** Cada etapa em um Flow pode subir uma equipe completa de agentes colaborativos, cada um com seus próprios papéis, objetivos e ferramentas. Você obtém fluxo de controle estruturado e previsível *e* colaboração autônoma de agentes — o melhor dos dois mundos.
|
||||
|
||||
No LangGraph, alcançar algo similar significa implementar manualmente protocolos de comunicação entre agentes, loops de chamada de ferramentas e lógica de delegação dentro das funções dos nós. É possível, mas é encanamento que você constrói do zero todas as vezes.
|
||||
|
||||
---
|
||||
|
||||
## Demo 4: Execução Paralela e Sincronização
|
||||
|
||||
Pipelines do mundo real frequentemente precisam dividir o trabalho e juntar os resultados. O CrewAI Flows lida com isso de forma elegante com os operadores `and_` e `or_`.
|
||||
|
||||
```python
|
||||
from crewai import LLM
|
||||
from crewai.flow.flow import Flow, and_, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class AnalysisState(BaseModel):
|
||||
topic: str = ""
|
||||
market_data: str = ""
|
||||
tech_analysis: str = ""
|
||||
competitor_intel: str = ""
|
||||
final_report: str = ""
|
||||
|
||||
class ParallelAnalysisFlow(Flow[AnalysisState]):
|
||||
@start()
|
||||
def start_method(self):
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def gather_market_data(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def run_tech_analysis(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def gather_competitor_intel(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(and_(gather_market_data, run_tech_analysis, gather_competitor_intel))
|
||||
def synthesize_report(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
flow = ParallelAnalysisFlow()
|
||||
flow.state.topic = "AI-powered developer tools"
|
||||
flow.kickoff()
|
||||
|
||||
```
|
||||
|
||||
Vários decorators `@start()` disparam em paralelo. O combinador `and_()` no decorator `@listen` garante que `synthesize_report` só execute depois que *todos os três* métodos upstream forem concluídos. Também existe `or_()` para quando você quer prosseguir assim que *qualquer* tarefa upstream terminar.
|
||||
|
||||
No LangGraph, você precisaria construir um padrão fan-out/fan-in com ramificações paralelas, um nó de sincronização e uma mesclagem de estado cuidadosa — tudo conectado explicitamente por arestas.
|
||||
|
||||
---
|
||||
|
||||
## Por que CrewAI Flows em Produção
|
||||
|
||||
Além de uma sintaxe mais limpa, Flows entrega várias vantagens críticas para produção:
|
||||
|
||||
**Persistência de estado integrada.** O estado do Flow é respaldado pelo LanceDB, o que significa que seus workflows podem sobreviver a falhas, ser retomados e acumular conhecimento entre execuções. No LangGraph, você precisa configurar um checkpointer separado.
|
||||
|
||||
**Gerenciamento de estado com segurança de tipos.** Modelos Pydantic oferecem validação, serialização e suporte de IDE prontos para uso. Estados `TypedDict` do LangGraph não validam em runtime.
|
||||
|
||||
**Orquestração de agentes de primeira classe.** Crews são um primitivo nativo. Você define agentes com papéis, objetivos, histórias e ferramentas — e eles colaboram de forma autônoma dentro do envelope estruturado de um Flow. Não é preciso reinventar a coordenação multiagente.
|
||||
|
||||
**Modelo mental mais simples.** Decorators declaram intenção. `@start` significa "comece aqui". `@listen(x)` significa "execute depois de x". `@router(x)` significa "decida para onde ir depois de x". O código lê como o workflow que ele descreve.
|
||||
|
||||
**Integração com CLI.** Execute flows com `crewai run`. Sem etapa de compilação separada, sem serialização de grafo. Seu Flow é uma classe Python, e ele roda como tal.
|
||||
|
||||
---
|
||||
|
||||
## Cheat Sheet de Migração
|
||||
|
||||
Se você está com uma base de código LangGraph e quer migrar para o CrewAI Flows, aqui vai um guia prático de conversão:
|
||||
|
||||
1. **Mapeie seu estado.** Converta seu `TypedDict` para um `BaseModel` do Pydantic. Adicione valores padrão para todos os campos.
|
||||
2. **Converta nós em métodos.** Cada função de `add_node` vira um método na sua subclasse de `Flow`. Substitua leituras `state["field"]` por `self.state.field`.
|
||||
3. **Substitua arestas por decorators.** `add_edge(START, "first_node")` vira `@start()` no primeiro método. A sequência `add_edge("a", "b")` vira `@listen(a)` no método `b`.
|
||||
4. **Substitua arestas condicionais por `@router`.** A função de roteamento e o mapeamento do `add_conditional_edges()` viram um único método `@router()` que retorna a string de rota.
|
||||
5. **Troque compile + invoke por kickoff.** Remova `graph.compile()`. Chame `flow.kickoff()`.
|
||||
6. **Considere onde as Crews se encaixam.** Qualquer nó com lógica complexa de agentes em múltiplas etapas é um candidato a extração para uma Crew. É aqui que você verá a maior melhoria de qualidade.
|
||||
|
||||
---
|
||||
|
||||
## Primeiros Passos
|
||||
|
||||
Instale o CrewAI e crie o scaffold de um novo projeto Flow:
|
||||
|
||||
```bash
|
||||
pip install crewai
|
||||
crewai create flow my_first_flow
|
||||
cd my_first_flow
|
||||
```
|
||||
|
||||
Isso gera uma estrutura de projeto com uma classe Flow pronta para edição, arquivos de configuração e um `pyproject.toml` com `type = "flow"` já definido. Execute com:
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
|
||||
A partir daí, adicione seus agentes, conecte seus listeners e publique.
|
||||
|
||||
---
|
||||
|
||||
## Considerações Finais
|
||||
|
||||
O LangGraph ensinou ao ecossistema que workflows de IA precisam de estrutura. Essa foi uma lição importante. Mas o CrewAI Flows pega essa lição e a entrega de um jeito mais rápido de escrever, mais fácil de ler e mais poderoso em produção — especialmente quando seus workflows envolvem múltiplos agentes colaborando.
|
||||
|
||||
Se você está construindo algo além de uma cadeia de agente único, dê uma olhada séria no Flows. O modelo baseado em decorators, a integração nativa com Crews e o gerenciamento de estado embutido significam menos tempo com encanamento e mais tempo nos problemas que importam.
|
||||
|
||||
Comece com `crewai create flow`. Você não vai olhar para trás.
|
||||
@@ -7,7 +7,7 @@ mode: "wide"
|
||||
|
||||
## Conecte o CrewAI a LLMs
|
||||
|
||||
O CrewAI utiliza o LiteLLM para conectar-se a uma grande variedade de Modelos de Linguagem (LLMs). Essa integração proporciona grande versatilidade, permitindo que você utilize modelos de inúmeros provedores por meio de uma interface simples e unificada.
|
||||
O CrewAI conecta-se a LLMs por meio de integrações nativas via SDK para os provedores mais populares (OpenAI, Anthropic, Google Gemini, Azure e AWS Bedrock), e usa o LiteLLM como alternativa flexível para todos os demais provedores.
|
||||
|
||||
<Note>
|
||||
Por padrão, o CrewAI usa o modelo `gpt-4o-mini`. Isso é determinado pela variável de ambiente `OPENAI_MODEL_NAME`, que tem como padrão "gpt-4o-mini" se não for definida.
|
||||
@@ -40,6 +40,14 @@ O LiteLLM oferece suporte a uma ampla gama de provedores, incluindo, mas não se
|
||||
|
||||
Para uma lista completa e sempre atualizada dos provedores suportados, consulte a [documentação de Provedores do LiteLLM](https://docs.litellm.ai/docs/providers).
|
||||
|
||||
<Info>
|
||||
Para usar qualquer provedor não coberto por uma integração nativa, adicione o LiteLLM como dependência ao seu projeto:
|
||||
```bash
|
||||
uv add 'crewai[litellm]'
|
||||
```
|
||||
Provedores nativos (OpenAI, Anthropic, Google Gemini, Azure, AWS Bedrock) usam seus próprios extras de SDK — consulte os [Exemplos de Configuração de Provedores](/pt-BR/concepts/llms#exemplos-de-configuração-de-provedores).
|
||||
</Info>
|
||||
|
||||
## Alterando a LLM
|
||||
|
||||
Para utilizar uma LLM diferente com seus agentes CrewAI, você tem várias opções:
|
||||
|
||||
@@ -11,84 +11,53 @@ mode: "wide"
|
||||
Composio é uma plataforma de integração que permite conectar seus agentes de IA a mais de 250 ferramentas. Os principais recursos incluem:
|
||||
|
||||
- **Autenticação de Nível Empresarial**: Suporte integrado para OAuth, Chaves de API, JWT com atualização automática de token
|
||||
- **Observabilidade Completa**: Logs detalhados de uso das ferramentas, registros de execução, e muito mais
|
||||
- **Observabilidade Completa**: Logs detalhados de uso das ferramentas, carimbos de data/hora de execução e muito mais
|
||||
|
||||
## Instalação
|
||||
|
||||
Para incorporar as ferramentas Composio em seu projeto, siga as instruções abaixo:
|
||||
|
||||
```shell
|
||||
pip install composio-crewai
|
||||
pip install composio composio-crewai
|
||||
pip install crewai
|
||||
```
|
||||
|
||||
Após a conclusão da instalação, execute `composio login` ou exporte sua chave de API do composio como `COMPOSIO_API_KEY`. Obtenha sua chave de API Composio [aqui](https://app.composio.dev)
|
||||
Após concluir a instalação, defina sua chave de API do Composio como `COMPOSIO_API_KEY`. Obtenha sua chave de API do Composio [aqui](https://platform.composio.dev)
|
||||
|
||||
## Exemplo
|
||||
|
||||
O exemplo a seguir demonstra como inicializar a ferramenta e executar uma ação do github:
|
||||
O exemplo a seguir demonstra como inicializar a ferramenta e executar uma ação do GitHub:
|
||||
|
||||
1. Inicialize o conjunto de ferramentas Composio
|
||||
1. Inicialize o Composio com o Provider do CrewAI
|
||||
|
||||
```python Code
|
||||
from composio_crewai import ComposioToolSet, App, Action
|
||||
from composio_crewai import ComposioProvider
|
||||
from composio import Composio
|
||||
from crewai import Agent, Task, Crew
|
||||
|
||||
toolset = ComposioToolSet()
|
||||
composio = Composio(provider=ComposioProvider())
|
||||
```
|
||||
|
||||
2. Conecte sua conta do GitHub
|
||||
2. Crie uma nova sessão Composio e recupere as ferramentas
|
||||
<CodeGroup>
|
||||
```shell CLI
|
||||
composio add github
|
||||
```
|
||||
```python Code
|
||||
request = toolset.initiate_connection(app=App.GITHUB)
|
||||
print(f"Open this URL to authenticate: {request.redirectUrl}")
|
||||
```python
|
||||
session = composio.create(
|
||||
user_id="your-user-id",
|
||||
toolkits=["gmail", "github"] # optional, default is all toolkits
|
||||
)
|
||||
tools = session.tools()
|
||||
```
|
||||
Leia mais sobre sessões e gerenciamento de usuários [aqui](https://docs.composio.dev/docs/configuring-sessions)
|
||||
</CodeGroup>
|
||||
|
||||
3. Obtenha ferramentas
|
||||
3. Autenticação manual dos usuários
|
||||
|
||||
- Recuperando todas as ferramentas de um app (não recomendado em produção):
|
||||
O Composio autentica automaticamente os usuários durante a sessão de chat do agente. No entanto, você também pode autenticar o usuário manualmente chamando o método `authorize`.
|
||||
```python Code
|
||||
tools = toolset.get_tools(apps=[App.GITHUB])
|
||||
connection_request = session.authorize("github")
|
||||
print(f"Open this URL to authenticate: {connection_request.redirect_url}")
|
||||
```
|
||||
|
||||
- Filtrando ferramentas com base em tags:
|
||||
```python Code
|
||||
tag = "users"
|
||||
|
||||
filtered_action_enums = toolset.find_actions_by_tags(
|
||||
App.GITHUB,
|
||||
tags=[tag],
|
||||
)
|
||||
|
||||
tools = toolset.get_tools(actions=filtered_action_enums)
|
||||
```
|
||||
|
||||
- Filtrando ferramentas com base no caso de uso:
|
||||
```python Code
|
||||
use_case = "Star a repository on GitHub"
|
||||
|
||||
filtered_action_enums = toolset.find_actions_by_use_case(
|
||||
App.GITHUB, use_case=use_case, advanced=False
|
||||
)
|
||||
|
||||
tools = toolset.get_tools(actions=filtered_action_enums)
|
||||
```
|
||||
<Tip>Defina `advanced` como True para obter ações para casos de uso complexos</Tip>
|
||||
|
||||
- Usando ferramentas específicas:
|
||||
|
||||
Neste exemplo, usaremos a ação `GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER` do app GitHub.
|
||||
```python Code
|
||||
tools = toolset.get_tools(
|
||||
actions=[Action.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER]
|
||||
)
|
||||
```
|
||||
Saiba mais sobre como filtrar ações [aqui](https://docs.composio.dev/patterns/tools/use-tools/use-specific-actions)
|
||||
|
||||
4. Defina o agente
|
||||
|
||||
```python Code
|
||||
@@ -116,4 +85,4 @@ crew = Crew(agents=[crewai_agent], tasks=[task])
|
||||
crew.kickoff()
|
||||
```
|
||||
|
||||
* Uma lista mais detalhada de ferramentas pode ser encontrada [aqui](https://app.composio.dev)
|
||||
* Uma lista mais detalhada de ferramentas pode ser encontrada [aqui](https://docs.composio.dev/toolkits)
|
||||
@@ -8,8 +8,8 @@ authors = [
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"Pillow~=10.4.0",
|
||||
"pypdf~=4.0.0",
|
||||
"Pillow~=12.1.1",
|
||||
"pypdf~=6.7.5",
|
||||
"python-magic>=0.4.27",
|
||||
"aiocache~=0.12.3",
|
||||
"aiofiles~=24.1.0",
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.9.3"
|
||||
__version__ = "1.10.2rc1"
|
||||
|
||||
@@ -8,12 +8,10 @@ authors = [
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"lancedb~=0.5.4",
|
||||
"pytube~=15.0.0",
|
||||
"requests~=2.32.5",
|
||||
"docker~=7.1.0",
|
||||
"crewai==1.9.3",
|
||||
"lancedb~=0.5.4",
|
||||
"crewai==1.10.2rc1",
|
||||
"tiktoken~=0.8.0",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
@@ -110,7 +108,7 @@ stagehand = [
|
||||
"stagehand>=0.4.1",
|
||||
]
|
||||
github = [
|
||||
"gitpython==3.1.38",
|
||||
"gitpython>=3.1.41,<4",
|
||||
"PyGithub==1.59.1",
|
||||
]
|
||||
rag = [
|
||||
|
||||
@@ -10,7 +10,18 @@ from crewai_tools.aws.s3.writer_tool import S3WriterTool
|
||||
from crewai_tools.tools.ai_mind_tool.ai_mind_tool import AIMindTool
|
||||
from crewai_tools.tools.apify_actors_tool.apify_actors_tool import ApifyActorsTool
|
||||
from crewai_tools.tools.arxiv_paper_tool.arxiv_paper_tool import ArxivPaperTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_image_tool import BraveImageSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_llm_context_tool import (
|
||||
BraveLLMContextTool,
|
||||
)
|
||||
from crewai_tools.tools.brave_search_tool.brave_local_pois_tool import (
|
||||
BraveLocalPOIsDescriptionTool,
|
||||
BraveLocalPOIsTool,
|
||||
)
|
||||
from crewai_tools.tools.brave_search_tool.brave_news_tool import BraveNewsSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_search_tool import BraveSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_video_tool import BraveVideoSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_web_tool import BraveWebSearchTool
|
||||
from crewai_tools.tools.brightdata_tool.brightdata_dataset import (
|
||||
BrightDataDatasetTool,
|
||||
)
|
||||
@@ -200,7 +211,14 @@ __all__ = [
|
||||
"ArxivPaperTool",
|
||||
"BedrockInvokeAgentTool",
|
||||
"BedrockKBRetrieverTool",
|
||||
"BraveImageSearchTool",
|
||||
"BraveLLMContextTool",
|
||||
"BraveLocalPOIsDescriptionTool",
|
||||
"BraveLocalPOIsTool",
|
||||
"BraveNewsSearchTool",
|
||||
"BraveSearchTool",
|
||||
"BraveVideoSearchTool",
|
||||
"BraveWebSearchTool",
|
||||
"BrightDataDatasetTool",
|
||||
"BrightDataSearchTool",
|
||||
"BrightDataWebUnlockerTool",
|
||||
@@ -291,4 +309,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.9.3"
|
||||
__version__ = "1.10.2rc1"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from collections.abc import Callable
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from crewai.utilities.lock_store import lock as store_lock
|
||||
from lancedb import ( # type: ignore[import-untyped]
|
||||
DBConnection as LanceDBConnection,
|
||||
connect as lancedb_connect,
|
||||
@@ -33,21 +35,24 @@ class LanceDBAdapter(Adapter):
|
||||
|
||||
_db: LanceDBConnection = PrivateAttr()
|
||||
_table: LanceDBTable = PrivateAttr()
|
||||
_lock_name: str = PrivateAttr(default="")
|
||||
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
self._db = lancedb_connect(self.uri)
|
||||
self._table = self._db.open_table(self.table_name)
|
||||
self._lock_name = f"lancedb:{os.path.realpath(str(self.uri))}"
|
||||
|
||||
super().model_post_init(__context)
|
||||
|
||||
def query(self, question: str) -> str: # type: ignore[override]
|
||||
query = self.embedding_function([question])[0]
|
||||
results = (
|
||||
self._table.search(query, vector_column_name=self.vector_column_name)
|
||||
.limit(self.top_k)
|
||||
.select([self.text_column_name])
|
||||
.to_list()
|
||||
)
|
||||
with store_lock(self._lock_name):
|
||||
results = (
|
||||
self._table.search(query, vector_column_name=self.vector_column_name)
|
||||
.limit(self.top_k)
|
||||
.select([self.text_column_name])
|
||||
.to_list()
|
||||
)
|
||||
values = [result[self.text_column_name] for result in results]
|
||||
return "\n".join(values)
|
||||
|
||||
@@ -56,4 +61,5 @@ class LanceDBAdapter(Adapter):
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self._table.add(*args, **kwargs)
|
||||
with store_lock(self._lock_name):
|
||||
self._table.add(*args, **kwargs)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextvars
|
||||
import logging
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
@@ -18,6 +21,9 @@ class BrowserSessionManager:
|
||||
This class maintains separate browser sessions for different threads,
|
||||
enabling concurrent usage of browsers in multi-threaded environments.
|
||||
Browsers are created lazily only when needed by tools.
|
||||
|
||||
Uses per-key events to serialize creation for the same thread_id without
|
||||
blocking unrelated callers or wasting resources on duplicate sessions.
|
||||
"""
|
||||
|
||||
def __init__(self, region: str = "us-west-2"):
|
||||
@@ -27,8 +33,10 @@ class BrowserSessionManager:
|
||||
region: AWS region for browser client
|
||||
"""
|
||||
self.region = region
|
||||
self._lock = threading.Lock()
|
||||
self._async_sessions: dict[str, tuple[BrowserClient, AsyncBrowser]] = {}
|
||||
self._sync_sessions: dict[str, tuple[BrowserClient, SyncBrowser]] = {}
|
||||
self._creating: dict[str, threading.Event] = {}
|
||||
|
||||
async def get_async_browser(self, thread_id: str) -> AsyncBrowser:
|
||||
"""Get or create an async browser for the specified thread.
|
||||
@@ -39,10 +47,29 @@ class BrowserSessionManager:
|
||||
Returns:
|
||||
An async browser instance specific to the thread
|
||||
"""
|
||||
if thread_id in self._async_sessions:
|
||||
return self._async_sessions[thread_id][1]
|
||||
loop = asyncio.get_event_loop()
|
||||
while True:
|
||||
with self._lock:
|
||||
if thread_id in self._async_sessions:
|
||||
return self._async_sessions[thread_id][1]
|
||||
if thread_id not in self._creating:
|
||||
self._creating[thread_id] = threading.Event()
|
||||
break
|
||||
event = self._creating[thread_id]
|
||||
ctx = contextvars.copy_context()
|
||||
await loop.run_in_executor(None, ctx.run, event.wait)
|
||||
|
||||
return await self._create_async_browser_session(thread_id)
|
||||
try:
|
||||
browser_client, browser = await self._create_async_browser_session(
|
||||
thread_id
|
||||
)
|
||||
with self._lock:
|
||||
self._async_sessions[thread_id] = (browser_client, browser)
|
||||
return browser
|
||||
finally:
|
||||
with self._lock:
|
||||
evt = self._creating.pop(thread_id)
|
||||
evt.set()
|
||||
|
||||
def get_sync_browser(self, thread_id: str) -> SyncBrowser:
|
||||
"""Get or create a sync browser for the specified thread.
|
||||
@@ -53,19 +80,33 @@ class BrowserSessionManager:
|
||||
Returns:
|
||||
A sync browser instance specific to the thread
|
||||
"""
|
||||
if thread_id in self._sync_sessions:
|
||||
return self._sync_sessions[thread_id][1]
|
||||
while True:
|
||||
with self._lock:
|
||||
if thread_id in self._sync_sessions:
|
||||
return self._sync_sessions[thread_id][1]
|
||||
if thread_id not in self._creating:
|
||||
self._creating[thread_id] = threading.Event()
|
||||
break
|
||||
event = self._creating[thread_id]
|
||||
event.wait()
|
||||
|
||||
return self._create_sync_browser_session(thread_id)
|
||||
try:
|
||||
return self._create_sync_browser_session(thread_id)
|
||||
finally:
|
||||
with self._lock:
|
||||
evt = self._creating.pop(thread_id)
|
||||
evt.set()
|
||||
|
||||
async def _create_async_browser_session(self, thread_id: str) -> AsyncBrowser:
|
||||
async def _create_async_browser_session(
|
||||
self, thread_id: str
|
||||
) -> tuple[BrowserClient, AsyncBrowser]:
|
||||
"""Create a new async browser session for the specified thread.
|
||||
|
||||
Args:
|
||||
thread_id: Unique identifier for the thread
|
||||
|
||||
Returns:
|
||||
The newly created async browser instance
|
||||
Tuple of (BrowserClient, AsyncBrowser).
|
||||
|
||||
Raises:
|
||||
Exception: If browser session creation fails
|
||||
@@ -75,10 +116,8 @@ class BrowserSessionManager:
|
||||
browser_client = BrowserClient(region=self.region)
|
||||
|
||||
try:
|
||||
# Start browser session
|
||||
browser_client.start()
|
||||
|
||||
# Get WebSocket connection info
|
||||
ws_url, headers = browser_client.generate_ws_headers()
|
||||
|
||||
logger.info(
|
||||
@@ -87,7 +126,6 @@ class BrowserSessionManager:
|
||||
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
# Connect to browser using Playwright
|
||||
playwright = await async_playwright().start()
|
||||
browser = await playwright.chromium.connect_over_cdp(
|
||||
endpoint_url=ws_url, headers=headers, timeout=30000
|
||||
@@ -96,17 +134,13 @@ class BrowserSessionManager:
|
||||
f"Successfully connected to async browser for thread {thread_id}"
|
||||
)
|
||||
|
||||
# Store session resources
|
||||
self._async_sessions[thread_id] = (browser_client, browser)
|
||||
|
||||
return browser
|
||||
return browser_client, browser
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to create async browser session for thread {thread_id}: {e}"
|
||||
)
|
||||
|
||||
# Clean up resources if session creation fails
|
||||
if browser_client:
|
||||
try:
|
||||
browser_client.stop()
|
||||
@@ -132,10 +166,8 @@ class BrowserSessionManager:
|
||||
browser_client = BrowserClient(region=self.region)
|
||||
|
||||
try:
|
||||
# Start browser session
|
||||
browser_client.start()
|
||||
|
||||
# Get WebSocket connection info
|
||||
ws_url, headers = browser_client.generate_ws_headers()
|
||||
|
||||
logger.info(
|
||||
@@ -144,7 +176,6 @@ class BrowserSessionManager:
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
# Connect to browser using Playwright
|
||||
playwright = sync_playwright().start()
|
||||
browser = playwright.chromium.connect_over_cdp(
|
||||
endpoint_url=ws_url, headers=headers, timeout=30000
|
||||
@@ -153,8 +184,8 @@ class BrowserSessionManager:
|
||||
f"Successfully connected to sync browser for thread {thread_id}"
|
||||
)
|
||||
|
||||
# Store session resources
|
||||
self._sync_sessions[thread_id] = (browser_client, browser)
|
||||
with self._lock:
|
||||
self._sync_sessions[thread_id] = (browser_client, browser)
|
||||
|
||||
return browser
|
||||
|
||||
@@ -163,7 +194,6 @@ class BrowserSessionManager:
|
||||
f"Failed to create sync browser session for thread {thread_id}: {e}"
|
||||
)
|
||||
|
||||
# Clean up resources if session creation fails
|
||||
if browser_client:
|
||||
try:
|
||||
browser_client.stop()
|
||||
@@ -178,13 +208,13 @@ class BrowserSessionManager:
|
||||
Args:
|
||||
thread_id: Unique identifier for the thread
|
||||
"""
|
||||
if thread_id not in self._async_sessions:
|
||||
logger.warning(f"No async browser session found for thread {thread_id}")
|
||||
return
|
||||
with self._lock:
|
||||
if thread_id not in self._async_sessions:
|
||||
logger.warning(f"No async browser session found for thread {thread_id}")
|
||||
return
|
||||
|
||||
browser_client, browser = self._async_sessions[thread_id]
|
||||
browser_client, browser = self._async_sessions.pop(thread_id)
|
||||
|
||||
# Close browser
|
||||
if browser:
|
||||
try:
|
||||
await browser.close()
|
||||
@@ -193,7 +223,6 @@ class BrowserSessionManager:
|
||||
f"Error closing async browser for thread {thread_id}: {e}"
|
||||
)
|
||||
|
||||
# Stop browser client
|
||||
if browser_client:
|
||||
try:
|
||||
browser_client.stop()
|
||||
@@ -202,8 +231,6 @@ class BrowserSessionManager:
|
||||
f"Error stopping browser client for thread {thread_id}: {e}"
|
||||
)
|
||||
|
||||
# Remove session from dictionary
|
||||
del self._async_sessions[thread_id]
|
||||
logger.info(f"Async browser session cleaned up for thread {thread_id}")
|
||||
|
||||
def close_sync_browser(self, thread_id: str) -> None:
|
||||
@@ -212,13 +239,13 @@ class BrowserSessionManager:
|
||||
Args:
|
||||
thread_id: Unique identifier for the thread
|
||||
"""
|
||||
if thread_id not in self._sync_sessions:
|
||||
logger.warning(f"No sync browser session found for thread {thread_id}")
|
||||
return
|
||||
with self._lock:
|
||||
if thread_id not in self._sync_sessions:
|
||||
logger.warning(f"No sync browser session found for thread {thread_id}")
|
||||
return
|
||||
|
||||
browser_client, browser = self._sync_sessions[thread_id]
|
||||
browser_client, browser = self._sync_sessions.pop(thread_id)
|
||||
|
||||
# Close browser
|
||||
if browser:
|
||||
try:
|
||||
browser.close()
|
||||
@@ -227,7 +254,6 @@ class BrowserSessionManager:
|
||||
f"Error closing sync browser for thread {thread_id}: {e}"
|
||||
)
|
||||
|
||||
# Stop browser client
|
||||
if browser_client:
|
||||
try:
|
||||
browser_client.stop()
|
||||
@@ -236,19 +262,17 @@ class BrowserSessionManager:
|
||||
f"Error stopping browser client for thread {thread_id}: {e}"
|
||||
)
|
||||
|
||||
# Remove session from dictionary
|
||||
del self._sync_sessions[thread_id]
|
||||
logger.info(f"Sync browser session cleaned up for thread {thread_id}")
|
||||
|
||||
async def close_all_browsers(self) -> None:
|
||||
"""Close all browser sessions."""
|
||||
# Close all async browsers
|
||||
async_thread_ids = list(self._async_sessions.keys())
|
||||
with self._lock:
|
||||
async_thread_ids = list(self._async_sessions.keys())
|
||||
sync_thread_ids = list(self._sync_sessions.keys())
|
||||
|
||||
for thread_id in async_thread_ids:
|
||||
await self.close_async_browser(thread_id)
|
||||
|
||||
# Close all sync browsers
|
||||
sync_thread_ids = list(self._sync_sessions.keys())
|
||||
for thread_id in sync_thread_ids:
|
||||
self.close_sync_browser(thread_id)
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import chromadb
|
||||
from crewai.utilities.lock_store import lock as store_lock
|
||||
from pydantic import BaseModel, Field, PrivateAttr
|
||||
|
||||
from crewai_tools.rag.base_loader import BaseLoader
|
||||
@@ -38,22 +40,32 @@ class RAG(Adapter):
|
||||
_client: Any = PrivateAttr()
|
||||
_collection: Any = PrivateAttr()
|
||||
_embedding_service: EmbeddingService = PrivateAttr()
|
||||
_lock_name: str = PrivateAttr(default="")
|
||||
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
try:
|
||||
if self.persist_directory:
|
||||
self._client = chromadb.PersistentClient(path=self.persist_directory)
|
||||
else:
|
||||
self._client = chromadb.Client()
|
||||
|
||||
self._collection = self._client.get_or_create_collection(
|
||||
name=self.collection_name,
|
||||
metadata={
|
||||
"hnsw:space": "cosine",
|
||||
"description": "CrewAI Knowledge Base",
|
||||
},
|
||||
self._lock_name = (
|
||||
f"chromadb:{os.path.realpath(self.persist_directory)}"
|
||||
if self.persist_directory
|
||||
else "chromadb:ephemeral"
|
||||
)
|
||||
|
||||
with store_lock(self._lock_name):
|
||||
if self.persist_directory:
|
||||
self._client = chromadb.PersistentClient(
|
||||
path=self.persist_directory
|
||||
)
|
||||
else:
|
||||
self._client = chromadb.Client()
|
||||
|
||||
self._collection = self._client.get_or_create_collection(
|
||||
name=self.collection_name,
|
||||
metadata={
|
||||
"hnsw:space": "cosine",
|
||||
"description": "CrewAI Knowledge Base",
|
||||
},
|
||||
)
|
||||
|
||||
self._embedding_service = EmbeddingService(
|
||||
provider=self.embedding_provider,
|
||||
model=self.embedding_model,
|
||||
@@ -87,29 +99,8 @@ class RAG(Adapter):
|
||||
loader_result = loader.load(source_content)
|
||||
doc_id = loader_result.doc_id
|
||||
|
||||
existing_doc = self._collection.get(
|
||||
where={"source": source_content.source_ref}, limit=1
|
||||
)
|
||||
existing_doc_id = (
|
||||
existing_doc and existing_doc["metadatas"][0]["doc_id"]
|
||||
if existing_doc["metadatas"]
|
||||
else None
|
||||
)
|
||||
|
||||
if existing_doc_id == doc_id:
|
||||
logger.warning(
|
||||
f"Document with source {loader_result.source} already exists"
|
||||
)
|
||||
return
|
||||
|
||||
# Document with same source ref does exists but the content has changed, deleting the oldest reference
|
||||
if existing_doc_id and existing_doc_id != loader_result.doc_id:
|
||||
logger.warning(f"Deleting old document with doc_id {existing_doc_id}")
|
||||
self._collection.delete(where={"doc_id": existing_doc_id})
|
||||
|
||||
documents = []
|
||||
|
||||
chunks = chunker.chunk(loader_result.content)
|
||||
documents = []
|
||||
for i, chunk in enumerate(chunks):
|
||||
doc_metadata = (metadata or {}).copy()
|
||||
doc_metadata["chunk_index"] = i
|
||||
@@ -136,7 +127,6 @@ class RAG(Adapter):
|
||||
|
||||
ids = [doc.id for doc in documents]
|
||||
metadatas = []
|
||||
|
||||
for doc in documents:
|
||||
doc_metadata = doc.metadata.copy()
|
||||
doc_metadata.update(
|
||||
@@ -148,27 +138,48 @@ class RAG(Adapter):
|
||||
)
|
||||
metadatas.append(doc_metadata)
|
||||
|
||||
try:
|
||||
self._collection.add(
|
||||
ids=ids,
|
||||
embeddings=embeddings,
|
||||
documents=contents,
|
||||
metadatas=metadatas,
|
||||
with store_lock(self._lock_name):
|
||||
existing_doc = self._collection.get(
|
||||
where={"source": source_content.source_ref}, limit=1
|
||||
)
|
||||
logger.info(f"Added {len(documents)} documents to knowledge base")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add documents to ChromaDB: {e}")
|
||||
existing_doc_id = (
|
||||
existing_doc and existing_doc["metadatas"][0]["doc_id"]
|
||||
if existing_doc["metadatas"]
|
||||
else None
|
||||
)
|
||||
|
||||
if existing_doc_id == doc_id:
|
||||
logger.warning(
|
||||
f"Document with source {loader_result.source} already exists"
|
||||
)
|
||||
return
|
||||
|
||||
if existing_doc_id and existing_doc_id != loader_result.doc_id:
|
||||
logger.warning(f"Deleting old document with doc_id {existing_doc_id}")
|
||||
self._collection.delete(where={"doc_id": existing_doc_id})
|
||||
|
||||
try:
|
||||
self._collection.add(
|
||||
ids=ids,
|
||||
embeddings=embeddings,
|
||||
documents=contents,
|
||||
metadatas=metadatas,
|
||||
)
|
||||
logger.info(f"Added {len(documents)} documents to knowledge base")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add documents to ChromaDB: {e}")
|
||||
|
||||
def query(self, question: str, where: dict[str, Any] | None = None) -> str: # type: ignore
|
||||
try:
|
||||
question_embedding = self._embedding_service.embed_text(question)
|
||||
|
||||
results = self._collection.query(
|
||||
query_embeddings=[question_embedding],
|
||||
n_results=self.top_k,
|
||||
where=where,
|
||||
include=["documents", "metadatas", "distances"],
|
||||
)
|
||||
with store_lock(self._lock_name):
|
||||
results = self._collection.query(
|
||||
query_embeddings=[question_embedding],
|
||||
n_results=self.top_k,
|
||||
where=where,
|
||||
include=["documents", "metadatas", "distances"],
|
||||
)
|
||||
|
||||
if (
|
||||
not results
|
||||
@@ -201,7 +212,8 @@ class RAG(Adapter):
|
||||
|
||||
def delete_collection(self) -> None:
|
||||
try:
|
||||
self._client.delete_collection(self.collection_name)
|
||||
with store_lock(self._lock_name):
|
||||
self._client.delete_collection(self.collection_name)
|
||||
logger.info(f"Deleted collection: {self.collection_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete collection: {e}")
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
from crewai_tools.tools.ai_mind_tool.ai_mind_tool import AIMindTool
|
||||
from crewai_tools.tools.apify_actors_tool.apify_actors_tool import ApifyActorsTool
|
||||
from crewai_tools.tools.arxiv_paper_tool.arxiv_paper_tool import ArxivPaperTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_image_tool import BraveImageSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_llm_context_tool import (
|
||||
BraveLLMContextTool,
|
||||
)
|
||||
from crewai_tools.tools.brave_search_tool.brave_local_pois_tool import (
|
||||
BraveLocalPOIsDescriptionTool,
|
||||
BraveLocalPOIsTool,
|
||||
)
|
||||
from crewai_tools.tools.brave_search_tool.brave_news_tool import BraveNewsSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_search_tool import BraveSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_video_tool import BraveVideoSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_web_tool import BraveWebSearchTool
|
||||
from crewai_tools.tools.brightdata_tool import (
|
||||
BrightDataDatasetTool,
|
||||
BrightDataSearchTool,
|
||||
@@ -185,7 +196,14 @@ __all__ = [
|
||||
"AIMindTool",
|
||||
"ApifyActorsTool",
|
||||
"ArxivPaperTool",
|
||||
"BraveImageSearchTool",
|
||||
"BraveLLMContextTool",
|
||||
"BraveLocalPOIsDescriptionTool",
|
||||
"BraveLocalPOIsTool",
|
||||
"BraveNewsSearchTool",
|
||||
"BraveSearchTool",
|
||||
"BraveVideoSearchTool",
|
||||
"BraveWebSearchTool",
|
||||
"BrightDataDatasetTool",
|
||||
"BrightDataSearchTool",
|
||||
"BrightDataWebUnlockerTool",
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from crewai.tools import BaseTool, EnvVar
|
||||
from pydantic import BaseModel, Field
|
||||
import requests
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Brave API error codes that indicate non-retryable quota/usage exhaustion.
|
||||
_QUOTA_CODES = frozenset({"QUOTA_LIMITED", "USAGE_LIMIT_EXCEEDED"})
|
||||
|
||||
|
||||
def _save_results_to_file(content: str) -> None:
|
||||
"""Saves the search results to a file."""
|
||||
filename = f"search_results_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.txt"
|
||||
with open(filename, "w") as file:
|
||||
file.write(content)
|
||||
|
||||
|
||||
def _parse_error_body(resp: requests.Response) -> dict[str, Any] | None:
|
||||
"""Extract the structured "error" object from a Brave API error response."""
|
||||
try:
|
||||
body = resp.json()
|
||||
error = body.get("error")
|
||||
return error if isinstance(error, dict) else None
|
||||
except (ValueError, KeyError):
|
||||
return None
|
||||
|
||||
|
||||
def _raise_for_error(resp: requests.Response) -> None:
|
||||
"""Brave Search API error responses contain helpful JSON payloads"""
|
||||
status = resp.status_code
|
||||
try:
|
||||
body = json.dumps(resp.json())
|
||||
except (ValueError, KeyError):
|
||||
body = resp.text[:500]
|
||||
|
||||
raise RuntimeError(f"Brave Search API error (HTTP {status}): {body}")
|
||||
|
||||
|
||||
def _is_retryable(resp: requests.Response) -> bool:
|
||||
"""Return True for transient failures that are worth retrying.
|
||||
|
||||
* 429 + RATE_LIMITED — the per-second sliding window is full.
|
||||
* 5xx — transient server-side errors.
|
||||
|
||||
Quota exhaustion (QUOTA_LIMITED, USAGE_LIMIT_EXCEEDED) is
|
||||
explicitly excluded: retrying will never succeed until the billing
|
||||
period resets.
|
||||
"""
|
||||
if resp.status_code == 429:
|
||||
error = _parse_error_body(resp) or {}
|
||||
return error.get("code") not in _QUOTA_CODES
|
||||
return 500 <= resp.status_code < 600
|
||||
|
||||
|
||||
def _retry_delay(resp: requests.Response, attempt: int) -> float:
|
||||
"""Compute wait time before the next retry attempt.
|
||||
|
||||
Prefers the server-supplied Retry-After header when available;
|
||||
falls back to exponential backoff (1s, 2s, 4s, ...).
|
||||
"""
|
||||
retry_after = resp.headers.get("Retry-After")
|
||||
if retry_after is not None:
|
||||
try:
|
||||
return max(0.0, float(retry_after))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return float(2**attempt)
|
||||
|
||||
|
||||
class BraveSearchToolBase(BaseTool, ABC):
|
||||
"""
|
||||
Base class for Brave Search API interactions.
|
||||
|
||||
Individual tool subclasses must provide the following:
|
||||
- search_url
|
||||
- header_schema (pydantic model)
|
||||
- args_schema (pydantic model)
|
||||
- _refine_payload() -> dict[str, Any]
|
||||
"""
|
||||
|
||||
search_url: str
|
||||
raw: bool = False
|
||||
args_schema: type[BaseModel]
|
||||
header_schema: type[BaseModel]
|
||||
|
||||
# Tool options (legacy parameters)
|
||||
country: str | None = None
|
||||
save_file: bool = False
|
||||
n_results: int = 10
|
||||
|
||||
env_vars: list[EnvVar] = Field(
|
||||
default_factory=lambda: [
|
||||
EnvVar(
|
||||
name="BRAVE_API_KEY",
|
||||
description="API key for Brave Search",
|
||||
required=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key: str | None = None,
|
||||
headers: dict[str, Any] | None = None,
|
||||
requests_per_second: float = 1.0,
|
||||
save_file: bool = False,
|
||||
raw: bool = False,
|
||||
timeout: int = 30,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._api_key = api_key or os.environ.get("BRAVE_API_KEY")
|
||||
if not self._api_key:
|
||||
raise ValueError("BRAVE_API_KEY environment variable is required")
|
||||
|
||||
self.raw = bool(raw)
|
||||
self._timeout = int(timeout)
|
||||
self.save_file = bool(save_file)
|
||||
self._requests_per_second = float(requests_per_second)
|
||||
self._headers = self._build_and_validate_headers(headers or {})
|
||||
# Per-instance rate limiting: each instance has its own clock and lock.
|
||||
# Total process rate is the sum of limits of instances you create.
|
||||
self._last_request_time: float = 0
|
||||
self._rate_limit_lock = threading.Lock()
|
||||
|
||||
@property
|
||||
def api_key(self) -> str:
|
||||
return self._api_key
|
||||
|
||||
@property
|
||||
def headers(self) -> dict[str, Any]:
|
||||
return self._headers
|
||||
|
||||
def set_headers(self, headers: dict[str, Any]) -> BraveSearchToolBase:
|
||||
merged = {**self._headers, **{k.lower(): v for k, v in headers.items()}}
|
||||
self._headers = self._build_and_validate_headers(merged)
|
||||
return self
|
||||
|
||||
def _build_and_validate_headers(self, headers: dict[str, Any]) -> dict[str, Any]:
|
||||
normalized = {k.lower(): v for k, v in headers.items()}
|
||||
normalized.setdefault("x-subscription-token", self._api_key)
|
||||
normalized.setdefault("accept", "application/json")
|
||||
|
||||
try:
|
||||
self.header_schema(**normalized)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid headers: {e}") from e
|
||||
|
||||
return normalized
|
||||
|
||||
def _rate_limit(self) -> None:
|
||||
"""Enforce minimum interval between requests for this instance. Thread-safe."""
|
||||
if self._requests_per_second <= 0:
|
||||
return
|
||||
|
||||
min_interval = 1.0 / self._requests_per_second
|
||||
with self._rate_limit_lock:
|
||||
now = time.time()
|
||||
next_allowed = self._last_request_time + min_interval
|
||||
if now < next_allowed:
|
||||
time.sleep(next_allowed - now)
|
||||
now = time.time()
|
||||
self._last_request_time = now
|
||||
|
||||
def _make_request(
|
||||
self, params: dict[str, Any], *, _max_retries: int = 3
|
||||
) -> dict[str, Any]:
|
||||
"""Execute an HTTP GET against the Brave Search API with retry logic."""
|
||||
last_resp: requests.Response | None = None
|
||||
|
||||
# Retry the request up to _max_retries times
|
||||
for attempt in range(_max_retries):
|
||||
self._rate_limit()
|
||||
|
||||
# Make the request
|
||||
try:
|
||||
resp = requests.get(
|
||||
self.search_url,
|
||||
headers=self._headers,
|
||||
params=params,
|
||||
timeout=self._timeout,
|
||||
)
|
||||
except requests.ConnectionError as exc:
|
||||
raise RuntimeError(
|
||||
f"Brave Search API connection failed: {exc}"
|
||||
) from exc
|
||||
except requests.Timeout as exc:
|
||||
raise RuntimeError(
|
||||
f"Brave Search API request timed out after {self._timeout}s: {exc}"
|
||||
) from exc
|
||||
|
||||
# Log the rate limit headers and request details
|
||||
logger.debug(
|
||||
"Brave Search API request: %s %s -> %d",
|
||||
"GET",
|
||||
resp.url,
|
||||
resp.status_code,
|
||||
)
|
||||
|
||||
# Response was OK, return the JSON body
|
||||
if resp.ok:
|
||||
try:
|
||||
return resp.json()
|
||||
except ValueError as exc:
|
||||
raise RuntimeError(
|
||||
f"Brave Search API returned invalid JSON (HTTP {resp.status_code}): {exc}"
|
||||
) from exc
|
||||
|
||||
# Response was not OK, but is retryable
|
||||
# (e.g., 429 Too Many Requests, 500 Internal Server Error)
|
||||
if _is_retryable(resp) and attempt < _max_retries - 1:
|
||||
delay = _retry_delay(resp, attempt)
|
||||
logger.warning(
|
||||
"Brave Search API returned %d. Retrying in %.1fs (attempt %d/%d)",
|
||||
resp.status_code,
|
||||
delay,
|
||||
attempt + 1,
|
||||
_max_retries,
|
||||
)
|
||||
time.sleep(delay)
|
||||
last_resp = resp
|
||||
continue
|
||||
|
||||
# Response was not OK, nor was it retryable
|
||||
# (e.g., 422 Unprocessable Entity, 400 Bad Request (OPTION_NOT_IN_PLAN))
|
||||
_raise_for_error(resp)
|
||||
|
||||
# All retries exhausted
|
||||
_raise_for_error(last_resp or resp) # type: ignore[possibly-undefined]
|
||||
return {} # unreachable (here to satisfy the type checker and linter)
|
||||
|
||||
def _run(self, q: str | None = None, **params: Any) -> Any:
|
||||
# Allow positional usage: tool.run("latest Brave browser features")
|
||||
if q is not None:
|
||||
params["q"] = q
|
||||
|
||||
params = self._common_payload_refinement(params)
|
||||
|
||||
# Validate only schema fields
|
||||
schema_keys = self.args_schema.model_fields
|
||||
payload_in = {k: v for k, v in params.items() if k in schema_keys}
|
||||
|
||||
try:
|
||||
validated = self.args_schema(**payload_in)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid parameters: {e}") from e
|
||||
|
||||
# The subclass may have additional refinements to apply to the payload, such as goggles or other parameters
|
||||
payload = self._refine_request_payload(validated.model_dump(exclude_none=True))
|
||||
response = self._make_request(payload)
|
||||
|
||||
if not self.raw:
|
||||
response = self._refine_response(response)
|
||||
|
||||
if self.save_file:
|
||||
_save_results_to_file(json.dumps(response, indent=2))
|
||||
|
||||
return response
|
||||
|
||||
@abstractmethod
|
||||
def _refine_request_payload(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Subclass must implement: transform validated params dict into API request params."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def _refine_response(self, response: dict[str, Any]) -> Any:
|
||||
"""Subclass must implement: transform response dict into a more useful format."""
|
||||
raise NotImplementedError
|
||||
|
||||
_EMPTY_VALUES: ClassVar[tuple[None, str, str, list[Any]]] = (None, "", "null", [])
|
||||
|
||||
def _common_payload_refinement(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Common payload refinement for all tools."""
|
||||
# crewAI's schema pipeline (ensure_all_properties_required in
|
||||
# pydantic_schema_utils.py) marks every property as required so
|
||||
# that OpenAI strict-mode structured outputs work correctly.
|
||||
# The side-effect is that the LLM fills in *every* parameter —
|
||||
# even truly optional ones — using placeholder values such as
|
||||
# None, "", "null", or []. Only optional fields are affected,
|
||||
# so we limit the check to those.
|
||||
fields = self.args_schema.model_fields
|
||||
params = {
|
||||
k: v
|
||||
for k, v in params.items()
|
||||
# Permit custom and required fields, and fields with non-empty values
|
||||
if k not in fields or fields[k].is_required() or v not in self._EMPTY_VALUES
|
||||
}
|
||||
|
||||
# Make sure params has "q" for query instead of "query" or "search_query"
|
||||
query = params.get("query") or params.get("search_query")
|
||||
if query is not None and "q" not in params:
|
||||
params["q"] = query
|
||||
params.pop("query", None)
|
||||
params.pop("search_query", None)
|
||||
|
||||
# If "count" was not explicitly provided, use n_results
|
||||
# (only when the schema actually supports a "count" field)
|
||||
if "count" in self.args_schema.model_fields:
|
||||
if "count" not in params and self.n_results is not None:
|
||||
params["count"] = self.n_results
|
||||
|
||||
# If "country" was not explicitly provided, but self.country is set, use it
|
||||
# (only when the schema actually supports a "country" field)
|
||||
if "country" in self.args_schema.model_fields:
|
||||
if "country" not in params and self.country is not None:
|
||||
params["country"] = self.country
|
||||
|
||||
return params
|
||||
@@ -0,0 +1,42 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai_tools.tools.brave_search_tool.base import BraveSearchToolBase
|
||||
from crewai_tools.tools.brave_search_tool.schemas import (
|
||||
ImageSearchHeaders,
|
||||
ImageSearchParams,
|
||||
)
|
||||
|
||||
|
||||
class BraveImageSearchTool(BraveSearchToolBase):
|
||||
"""A tool that performs image searches using the Brave Search API."""
|
||||
|
||||
name: str = "Brave Image Search"
|
||||
args_schema: type[BaseModel] = ImageSearchParams
|
||||
header_schema: type[BaseModel] = ImageSearchHeaders
|
||||
|
||||
description: str = (
|
||||
"A tool that performs image searches using the Brave Search API. "
|
||||
"Results are returned as structured JSON data."
|
||||
)
|
||||
|
||||
search_url: str = "https://api.search.brave.com/res/v1/images/search"
|
||||
|
||||
def _refine_request_payload(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return params
|
||||
|
||||
def _refine_response(self, response: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
# Make the response more concise, and easier to consume
|
||||
results = response.get("results", [])
|
||||
return [
|
||||
{
|
||||
"title": result.get("title"),
|
||||
"url": result.get("properties", {}).get("url"),
|
||||
"dimensions": f"{w}x{h}"
|
||||
if (w := result.get("properties", {}).get("width"))
|
||||
and (h := result.get("properties", {}).get("height"))
|
||||
else None,
|
||||
}
|
||||
for result in results
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai_tools.tools.brave_search_tool.base import BraveSearchToolBase
|
||||
from crewai_tools.tools.brave_search_tool.response_types import LLMContext
|
||||
from crewai_tools.tools.brave_search_tool.schemas import (
|
||||
LLMContextHeaders,
|
||||
LLMContextParams,
|
||||
)
|
||||
|
||||
|
||||
class BraveLLMContextTool(BraveSearchToolBase):
|
||||
"""A tool that retrieves context for LLM usage from the Brave Search API."""
|
||||
|
||||
name: str = "Brave LLM Context"
|
||||
args_schema: type[BaseModel] = LLMContextParams
|
||||
header_schema: type[BaseModel] = LLMContextHeaders
|
||||
|
||||
description: str = (
|
||||
"A tool that retrieves context for LLM usage from the Brave Search API. "
|
||||
"Results are returned as structured JSON data."
|
||||
)
|
||||
|
||||
search_url: str = "https://api.search.brave.com/res/v1/llm/context"
|
||||
|
||||
def _refine_request_payload(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return params
|
||||
|
||||
def _refine_response(self, response: LLMContext.Response) -> LLMContext.Response:
|
||||
"""The LLM Context response schema is fairly simple. Return as is."""
|
||||
return response
|
||||
@@ -0,0 +1,109 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai_tools.tools.brave_search_tool.base import BraveSearchToolBase
|
||||
from crewai_tools.tools.brave_search_tool.response_types import LocalPOIs
|
||||
from crewai_tools.tools.brave_search_tool.schemas import (
|
||||
LocalPOIsDescriptionHeaders,
|
||||
LocalPOIsDescriptionParams,
|
||||
LocalPOIsHeaders,
|
||||
LocalPOIsParams,
|
||||
)
|
||||
|
||||
|
||||
DayOpeningHours = LocalPOIs.DayOpeningHours
|
||||
OpeningHours = LocalPOIs.OpeningHours
|
||||
LocationResult = LocalPOIs.LocationResult
|
||||
LocalPOIsResponse = LocalPOIs.Response
|
||||
|
||||
|
||||
def _flatten_slots(slots: list[DayOpeningHours]) -> list[dict[str, str]]:
|
||||
"""Convert a list of DayOpeningHours dicts into simplified entries."""
|
||||
return [
|
||||
{
|
||||
"day": slot["full_name"].lower(),
|
||||
"opens": slot["opens"],
|
||||
"closes": slot["closes"],
|
||||
}
|
||||
for slot in slots
|
||||
]
|
||||
|
||||
|
||||
def _simplify_opening_hours(result: LocationResult) -> list[dict[str, str]] | None:
|
||||
"""Collapse opening_hours into a flat list of {day, opens, closes} dicts."""
|
||||
hours = result.get("opening_hours")
|
||||
if not hours:
|
||||
return None
|
||||
|
||||
entries: list[dict[str, str]] = []
|
||||
|
||||
current = hours.get("current_day")
|
||||
if current:
|
||||
entries.extend(_flatten_slots(current))
|
||||
|
||||
days = hours.get("days")
|
||||
if days:
|
||||
for day_slots in days:
|
||||
entries.extend(_flatten_slots(day_slots))
|
||||
|
||||
return entries or None
|
||||
|
||||
|
||||
class BraveLocalPOIsTool(BraveSearchToolBase):
|
||||
"""A tool that retrieves local POIs using the Brave Search API."""
|
||||
|
||||
name: str = "Brave Local POIs"
|
||||
args_schema: type[BaseModel] = LocalPOIsParams
|
||||
header_schema: type[BaseModel] = LocalPOIsHeaders
|
||||
description: str = (
|
||||
"A tool that retrieves local POIs using the Brave Search API. "
|
||||
"Results are returned as structured JSON data."
|
||||
)
|
||||
search_url: str = "https://api.search.brave.com/res/v1/local/pois"
|
||||
|
||||
def _refine_request_payload(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return params
|
||||
|
||||
def _refine_response(self, response: LocalPOIsResponse) -> list[dict[str, Any]]:
|
||||
results = response.get("results", [])
|
||||
return [
|
||||
{
|
||||
"title": result.get("title"),
|
||||
"url": result.get("url"),
|
||||
"description": result.get("description"),
|
||||
"address": result.get("postal_address", {}).get("displayAddress"),
|
||||
"contact": result.get("contact", {}).get("telephone")
|
||||
or result.get("contact", {}).get("email")
|
||||
or None,
|
||||
"opening_hours": _simplify_opening_hours(result),
|
||||
}
|
||||
for result in results
|
||||
]
|
||||
|
||||
|
||||
class BraveLocalPOIsDescriptionTool(BraveSearchToolBase):
|
||||
"""A tool that retrieves AI-generated descriptions for local POIs using the Brave Search API."""
|
||||
|
||||
name: str = "Brave Local POI Descriptions"
|
||||
args_schema: type[BaseModel] = LocalPOIsDescriptionParams
|
||||
header_schema: type[BaseModel] = LocalPOIsDescriptionHeaders
|
||||
description: str = (
|
||||
"A tool that retrieves AI-generated descriptions for local POIs using the Brave Search API. "
|
||||
"Results are returned as structured JSON data."
|
||||
)
|
||||
search_url: str = "https://api.search.brave.com/res/v1/local/descriptions"
|
||||
|
||||
def _refine_request_payload(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return params
|
||||
|
||||
def _refine_response(self, response: LocalPOIsResponse) -> list[dict[str, Any]]:
|
||||
# Make the response more concise, and easier to consume
|
||||
results = response.get("results", [])
|
||||
return [
|
||||
{
|
||||
"id": result.get("id"),
|
||||
"description": result.get("description"),
|
||||
}
|
||||
for result in results
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai_tools.tools.brave_search_tool.base import BraveSearchToolBase
|
||||
from crewai_tools.tools.brave_search_tool.schemas import (
|
||||
NewsSearchHeaders,
|
||||
NewsSearchParams,
|
||||
)
|
||||
|
||||
|
||||
class BraveNewsSearchTool(BraveSearchToolBase):
|
||||
"""A tool that performs news searches using the Brave Search API."""
|
||||
|
||||
name: str = "Brave News Search"
|
||||
args_schema: type[BaseModel] = NewsSearchParams
|
||||
header_schema: type[BaseModel] = NewsSearchHeaders
|
||||
|
||||
description: str = (
|
||||
"A tool that performs news searches using the Brave Search API. "
|
||||
"Results are returned as structured JSON data."
|
||||
)
|
||||
|
||||
search_url: str = "https://api.search.brave.com/res/v1/news/search"
|
||||
|
||||
def _refine_request_payload(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return params
|
||||
|
||||
def _refine_response(self, response: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
# Make the response more concise, and easier to consume
|
||||
results = response.get("results", [])
|
||||
return [
|
||||
{
|
||||
"url": result.get("url"),
|
||||
"title": result.get("title"),
|
||||
"description": result.get("description"),
|
||||
}
|
||||
for result in results
|
||||
]
|
||||
@@ -1,4 +1,3 @@
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
@@ -10,16 +9,13 @@ from pydantic import BaseModel, Field
|
||||
from pydantic.types import StringConstraints
|
||||
import requests
|
||||
|
||||
from crewai_tools.tools.brave_search_tool.base import _save_results_to_file
|
||||
from crewai_tools.tools.brave_search_tool.schemas import WebSearchParams
|
||||
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def _save_results_to_file(content: str) -> None:
|
||||
"""Saves the search results to a file."""
|
||||
filename = f"search_results_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.txt"
|
||||
with open(filename, "w") as file:
|
||||
file.write(content)
|
||||
|
||||
|
||||
FreshnessPreset = Literal["pd", "pw", "pm", "py"]
|
||||
FreshnessRange = Annotated[
|
||||
str, StringConstraints(pattern=r"^\d{4}-\d{2}-\d{2}to\d{4}-\d{2}-\d{2}$")
|
||||
@@ -28,51 +24,6 @@ Freshness = FreshnessPreset | FreshnessRange
|
||||
SafeSearch = Literal["off", "moderate", "strict"]
|
||||
|
||||
|
||||
class BraveSearchToolSchema(BaseModel):
|
||||
"""Input for BraveSearchTool"""
|
||||
|
||||
query: str = Field(..., description="Search query to perform")
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description="Country code for geo-targeting (e.g., 'US', 'BR').",
|
||||
)
|
||||
search_language: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the search results (e.g., 'en', 'es').",
|
||||
)
|
||||
count: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of results to return. Actual number may be less.",
|
||||
)
|
||||
offset: int | None = Field(
|
||||
default=None, description="Skip the first N result sets/pages. Max is 9."
|
||||
)
|
||||
safesearch: SafeSearch | None = Field(
|
||||
default=None,
|
||||
description="Filter out explicit content. Options: off/moderate/strict",
|
||||
)
|
||||
spellcheck: bool | None = Field(
|
||||
default=None,
|
||||
description="Attempt to correct spelling errors in the search query.",
|
||||
)
|
||||
freshness: Freshness | None = Field(
|
||||
default=None,
|
||||
description="Enforce freshness of results. Options: pd/pw/pm/py, or YYYY-MM-DDtoYYYY-MM-DD",
|
||||
)
|
||||
text_decorations: bool | None = Field(
|
||||
default=None,
|
||||
description="Include markup to highlight search terms in the results.",
|
||||
)
|
||||
extra_snippets: bool | None = Field(
|
||||
default=None,
|
||||
description="Include up to 5 text snippets for each page if possible.",
|
||||
)
|
||||
operators: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to apply search operators (e.g., site:example.com).",
|
||||
)
|
||||
|
||||
|
||||
# TODO: Extend support to additional endpoints (e.g., /images, /news, etc.)
|
||||
class BraveSearchTool(BaseTool):
|
||||
"""A tool that performs web searches using the Brave Search API."""
|
||||
@@ -82,7 +33,7 @@ class BraveSearchTool(BaseTool):
|
||||
"A tool that performs web searches using the Brave Search API. "
|
||||
"Results are returned as structured JSON data."
|
||||
)
|
||||
args_schema: type[BaseModel] = BraveSearchToolSchema
|
||||
args_schema: type[BaseModel] = WebSearchParams
|
||||
search_url: str = "https://api.search.brave.com/res/v1/web/search"
|
||||
n_results: int = 10
|
||||
save_file: bool = False
|
||||
@@ -119,8 +70,8 @@ class BraveSearchTool(BaseTool):
|
||||
|
||||
# Construct and send the request
|
||||
try:
|
||||
# Maintain both "search_query" and "query" for backwards compatibility
|
||||
query = kwargs.get("search_query") or kwargs.get("query")
|
||||
# Fallback to "query" or "search_query" for backwards compatibility
|
||||
query = kwargs.get("q") or kwargs.get("query") or kwargs.get("search_query")
|
||||
if not query:
|
||||
raise ValueError("Query is required")
|
||||
|
||||
@@ -129,8 +80,11 @@ class BraveSearchTool(BaseTool):
|
||||
if country := kwargs.get("country"):
|
||||
payload["country"] = country
|
||||
|
||||
if search_language := kwargs.get("search_language"):
|
||||
payload["search_language"] = search_language
|
||||
# Fallback to "search_language" for backwards compatibility
|
||||
if search_lang := kwargs.get("search_lang") or kwargs.get(
|
||||
"search_language"
|
||||
):
|
||||
payload["search_lang"] = search_lang
|
||||
|
||||
# Fallback to deprecated n_results parameter if no count is provided
|
||||
count = kwargs.get("count")
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai_tools.tools.brave_search_tool.base import BraveSearchToolBase
|
||||
from crewai_tools.tools.brave_search_tool.schemas import (
|
||||
VideoSearchHeaders,
|
||||
VideoSearchParams,
|
||||
)
|
||||
|
||||
|
||||
class BraveVideoSearchTool(BraveSearchToolBase):
|
||||
"""A tool that performs video searches using the Brave Search API."""
|
||||
|
||||
name: str = "Brave Video Search"
|
||||
args_schema: type[BaseModel] = VideoSearchParams
|
||||
header_schema: type[BaseModel] = VideoSearchHeaders
|
||||
|
||||
description: str = (
|
||||
"A tool that performs video searches using the Brave Search API. "
|
||||
"Results are returned as structured JSON data."
|
||||
)
|
||||
|
||||
search_url: str = "https://api.search.brave.com/res/v1/videos/search"
|
||||
|
||||
def _refine_request_payload(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return params
|
||||
|
||||
def _refine_response(self, response: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
# Make the response more concise, and easier to consume
|
||||
results = response.get("results", [])
|
||||
return [
|
||||
{
|
||||
"url": result.get("url"),
|
||||
"title": result.get("title"),
|
||||
"description": result.get("description"),
|
||||
}
|
||||
for result in results
|
||||
]
|
||||
@@ -0,0 +1,45 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai_tools.tools.brave_search_tool.base import BraveSearchToolBase
|
||||
from crewai_tools.tools.brave_search_tool.schemas import (
|
||||
WebSearchHeaders,
|
||||
WebSearchParams,
|
||||
)
|
||||
|
||||
|
||||
class BraveWebSearchTool(BraveSearchToolBase):
|
||||
"""A tool that performs web searches using the Brave Search API."""
|
||||
|
||||
name: str = "Brave Web Search"
|
||||
args_schema: type[BaseModel] = WebSearchParams
|
||||
header_schema: type[BaseModel] = WebSearchHeaders
|
||||
|
||||
description: str = (
|
||||
"A tool that performs web searches using the Brave Search API. "
|
||||
"Results are returned as structured JSON data."
|
||||
)
|
||||
|
||||
search_url: str = "https://api.search.brave.com/res/v1/web/search"
|
||||
|
||||
def _refine_request_payload(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return params
|
||||
|
||||
def _refine_response(self, response: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
results = response.get("web", {}).get("results", [])
|
||||
refined = []
|
||||
for result in results:
|
||||
snippets = result.get("extra_snippets") or []
|
||||
if not snippets:
|
||||
desc = result.get("description")
|
||||
if desc:
|
||||
snippets = [desc]
|
||||
refined.append(
|
||||
{
|
||||
"url": result.get("url"),
|
||||
"title": result.get("title"),
|
||||
"snippets": snippets,
|
||||
}
|
||||
)
|
||||
return refined
|
||||
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
|
||||
class LocalPOIs:
|
||||
class PostalAddress(TypedDict, total=False):
|
||||
type: Literal["PostalAddress"]
|
||||
country: str
|
||||
postalCode: str
|
||||
streetAddress: str
|
||||
addressRegion: str
|
||||
addressLocality: str
|
||||
displayAddress: str
|
||||
|
||||
class DayOpeningHours(TypedDict):
|
||||
abbr_name: str
|
||||
full_name: str
|
||||
opens: str
|
||||
closes: str
|
||||
|
||||
class OpeningHours(TypedDict, total=False):
|
||||
current_day: list[LocalPOIs.DayOpeningHours]
|
||||
days: list[list[LocalPOIs.DayOpeningHours]]
|
||||
|
||||
class LocationResult(TypedDict, total=False):
|
||||
provider_url: str
|
||||
title: str
|
||||
url: str
|
||||
id: str | None
|
||||
opening_hours: LocalPOIs.OpeningHours | None
|
||||
postal_address: LocalPOIs.PostalAddress | None
|
||||
|
||||
class Response(TypedDict, total=False):
|
||||
type: Literal["local_pois"]
|
||||
results: list[LocalPOIs.LocationResult]
|
||||
|
||||
|
||||
class LLMContext:
|
||||
class LLMContextItem(TypedDict, total=False):
|
||||
snippets: list[str]
|
||||
title: str
|
||||
url: str
|
||||
|
||||
class LLMContextMapItem(TypedDict, total=False):
|
||||
name: str
|
||||
snippets: list[str]
|
||||
title: str
|
||||
url: str
|
||||
|
||||
class LLMContextPOIItem(TypedDict, total=False):
|
||||
name: str
|
||||
snippets: list[str]
|
||||
title: str
|
||||
url: str
|
||||
|
||||
class Grounding(TypedDict, total=False):
|
||||
generic: list[LLMContext.LLMContextItem]
|
||||
poi: LLMContext.LLMContextPOIItem
|
||||
map: list[LLMContext.LLMContextMapItem]
|
||||
|
||||
class Sources(TypedDict, total=False):
|
||||
pass
|
||||
|
||||
class Response(TypedDict, total=False):
|
||||
grounding: LLMContext.Grounding
|
||||
sources: LLMContext.Sources
|
||||
@@ -0,0 +1,525 @@
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic.types import StringConstraints
|
||||
|
||||
|
||||
# Common types
|
||||
Units = Literal["metric", "imperial"]
|
||||
SafeSearch = Literal["off", "moderate", "strict"]
|
||||
Freshness = (
|
||||
Literal["pd", "pw", "pm", "py"]
|
||||
| Annotated[
|
||||
str, StringConstraints(pattern=r"^\d{4}-\d{2}-\d{2}to\d{4}-\d{2}-\d{2}$")
|
||||
]
|
||||
)
|
||||
ResultFilter = list[
|
||||
Literal[
|
||||
"discussions",
|
||||
"faq",
|
||||
"infobox",
|
||||
"news",
|
||||
"query",
|
||||
"summarizer",
|
||||
"videos",
|
||||
"web",
|
||||
"locations",
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
class LLMContextParams(BaseModel):
|
||||
"""Parameters for Brave LLM Context endpoint."""
|
||||
|
||||
q: str = Field(
|
||||
description="Search query to perform",
|
||||
min_length=1,
|
||||
max_length=400,
|
||||
)
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description="Country code for geo-targeting (e.g., 'US', 'BR').",
|
||||
pattern=r"^[A-Z]{2}$",
|
||||
)
|
||||
search_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the search results (e.g., 'en', 'es').",
|
||||
pattern=r"^[a-z]{2}$",
|
||||
)
|
||||
count: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of results to return. Actual number may be less.",
|
||||
ge=1,
|
||||
le=50,
|
||||
)
|
||||
maximum_number_of_urls: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of URLs to include in the context.",
|
||||
ge=1,
|
||||
le=50,
|
||||
)
|
||||
maximum_number_of_tokens: int | None = Field(
|
||||
default=None,
|
||||
description="The approximate maximum number of tokens to include in the context.",
|
||||
ge=1,
|
||||
le=32768,
|
||||
)
|
||||
maximum_number_of_snippets: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of different snippets to include in the context.",
|
||||
ge=1,
|
||||
le=100,
|
||||
)
|
||||
context_threshold_mode: (
|
||||
Literal["disabled", "strict", "lenient", "balanced"] | None
|
||||
) = Field(
|
||||
default=None,
|
||||
description="The mode to use for the context thresholding.",
|
||||
)
|
||||
maximum_number_of_tokens_per_url: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of tokens to include for each URL in the context.",
|
||||
ge=1,
|
||||
le=8192,
|
||||
)
|
||||
maximum_number_of_snippets_per_url: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of snippets to include per URL.",
|
||||
ge=1,
|
||||
le=100,
|
||||
)
|
||||
goggles: str | list[str] | None = Field(
|
||||
default=None,
|
||||
description="Goggles act as a custom re-ranking mechanism. Goggle source or URLs.",
|
||||
)
|
||||
enable_local: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to enable local recall. Not setting this value means auto-detect and uses local recall if any of the localization headers are provided.",
|
||||
)
|
||||
|
||||
|
||||
class WebSearchParams(BaseModel):
|
||||
"""Parameters for Brave Web Search endpoint."""
|
||||
|
||||
q: str = Field(
|
||||
description="Search query to perform",
|
||||
min_length=1,
|
||||
max_length=400,
|
||||
)
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description="Country code for geo-targeting (e.g., 'US', 'BR').",
|
||||
pattern=r"^[A-Z]{2}$",
|
||||
)
|
||||
search_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the search results (e.g., 'en', 'es').",
|
||||
pattern=r"^[a-z]{2}$",
|
||||
)
|
||||
ui_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the user interface (e.g., 'en-US', 'es-AR').",
|
||||
pattern=r"^[a-z]{2}-[A-Z]{2}$",
|
||||
)
|
||||
count: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of results to return. Actual number may be less.",
|
||||
ge=1,
|
||||
le=20,
|
||||
)
|
||||
offset: int | None = Field(
|
||||
default=None,
|
||||
description="Skip the first N result sets/pages. Max is 9.",
|
||||
ge=0,
|
||||
le=9,
|
||||
)
|
||||
safesearch: Literal["off", "moderate", "strict"] | None = Field(
|
||||
default=None,
|
||||
description="Filter out explicit content. Options: off/moderate/strict",
|
||||
)
|
||||
spellcheck: bool | None = Field(
|
||||
default=None,
|
||||
description="Attempt to correct spelling errors in the search query.",
|
||||
)
|
||||
freshness: Freshness | None = Field(
|
||||
default=None,
|
||||
description="Enforce freshness of results. Options: pd/pw/pm/py, or YYYY-MM-DDtoYYYY-MM-DD",
|
||||
)
|
||||
text_decorations: bool | None = Field(
|
||||
default=None,
|
||||
description="Include markup to highlight search terms in the results.",
|
||||
)
|
||||
extra_snippets: bool | None = Field(
|
||||
default=None,
|
||||
description="Include up to 5 text snippets for each page if possible.",
|
||||
)
|
||||
result_filter: ResultFilter | None = Field(
|
||||
default=None,
|
||||
description="Filter the results by type. Options: discussions/faq/infobox/news/query/summarizer/videos/web/locations. Note: The `count` parameter is applied only to the `web` results.",
|
||||
)
|
||||
units: Units | None = Field(
|
||||
default=None,
|
||||
description="The units to use for the results. Options: metric/imperial",
|
||||
)
|
||||
goggles: str | list[str] | None = Field(
|
||||
default=None,
|
||||
description="Goggles act as a custom re-ranking mechanism. Goggle source or URLs.",
|
||||
)
|
||||
summary: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to generate a summarizer ID for the results.",
|
||||
)
|
||||
enable_rich_callback: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to enable rich callbacks for the results. Requires Pro level subscription.",
|
||||
)
|
||||
include_fetch_metadata: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to include fetch metadata (e.g., last fetch time) in the results.",
|
||||
)
|
||||
operators: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to apply search operators (e.g., site:example.com).",
|
||||
)
|
||||
|
||||
|
||||
class LocalPOIsParams(BaseModel):
|
||||
"""Parameters for Brave Local POIs endpoint."""
|
||||
|
||||
ids: list[str] = Field(
|
||||
description="List of POI IDs to retrieve. Maximum of 20. IDs are valid for 8 hours.",
|
||||
min_length=1,
|
||||
max_length=20,
|
||||
)
|
||||
search_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the search results (e.g., 'en', 'es').",
|
||||
pattern=r"^[a-z]{2}$",
|
||||
)
|
||||
ui_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the user interface (e.g., 'en-US', 'es-AR').",
|
||||
pattern=r"^[a-z]{2}-[A-Z]{2}$",
|
||||
)
|
||||
units: Units | None = Field(
|
||||
default=None,
|
||||
description="The units to use for the results. Options: metric/imperial",
|
||||
)
|
||||
|
||||
|
||||
class LocalPOIsDescriptionParams(BaseModel):
|
||||
"""Parameters for Brave Local POI Descriptions endpoint."""
|
||||
|
||||
ids: list[str] = Field(
|
||||
description="List of POI IDs to retrieve. Maximum of 20. IDs are valid for 8 hours.",
|
||||
min_length=1,
|
||||
max_length=20,
|
||||
)
|
||||
|
||||
|
||||
class ImageSearchParams(BaseModel):
|
||||
"""Parameters for Brave Image Search endpoint."""
|
||||
|
||||
q: str = Field(
|
||||
description="Search query to perform",
|
||||
min_length=1,
|
||||
max_length=400,
|
||||
)
|
||||
search_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the search results (e.g., 'en', 'es').",
|
||||
pattern=r"^[a-z]{2}$",
|
||||
)
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description="Country code for geo-targeting (e.g., 'US', 'BR').",
|
||||
pattern=r"^[A-Z]{2}$",
|
||||
)
|
||||
safesearch: Literal["off", "strict"] | None = Field(
|
||||
default=None,
|
||||
description="Filter out explicit content. Default is strict.",
|
||||
)
|
||||
count: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of results to return.",
|
||||
ge=1,
|
||||
le=200,
|
||||
)
|
||||
spellcheck: bool | None = Field(
|
||||
default=None,
|
||||
description="Attempt to correct spelling errors in the search query.",
|
||||
)
|
||||
|
||||
|
||||
class VideoSearchParams(BaseModel):
|
||||
"""Parameters for Brave Video Search endpoint."""
|
||||
|
||||
q: str = Field(
|
||||
description="Search query to perform",
|
||||
min_length=1,
|
||||
max_length=400,
|
||||
)
|
||||
search_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the search results (e.g., 'en', 'es').",
|
||||
pattern=r"^[a-z]{2}$",
|
||||
)
|
||||
ui_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the user interface (e.g., 'en-US', 'es-AR').",
|
||||
pattern=r"^[a-z]{2}-[A-Z]{2}$",
|
||||
)
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description="Country code for geo-targeting (e.g., 'US', 'BR').",
|
||||
pattern=r"^[A-Z]{2}$",
|
||||
)
|
||||
safesearch: SafeSearch | None = Field(
|
||||
default=None,
|
||||
description="Filter out explicit content. Options: off/moderate/strict",
|
||||
)
|
||||
count: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of results to return.",
|
||||
ge=1,
|
||||
le=50,
|
||||
)
|
||||
offset: int | None = Field(
|
||||
default=None,
|
||||
description="Skip the first N result sets/pages. Max is 9.",
|
||||
ge=0,
|
||||
le=9,
|
||||
)
|
||||
spellcheck: bool | None = Field(
|
||||
default=None,
|
||||
description="Attempt to correct spelling errors in the search query.",
|
||||
)
|
||||
freshness: Freshness | None = Field(
|
||||
default=None,
|
||||
description="Enforce freshness of results. Options: pd/pw/pm/py, or YYYY-MM-DDtoYYYY-MM-DD",
|
||||
)
|
||||
include_fetch_metadata: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to include fetch metadata (e.g., last fetch time) in the results.",
|
||||
)
|
||||
operators: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to apply search operators (e.g., site:example.com).",
|
||||
)
|
||||
|
||||
|
||||
class NewsSearchParams(BaseModel):
|
||||
"""Parameters for Brave News Search endpoint."""
|
||||
|
||||
q: str = Field(
|
||||
description="Search query to perform",
|
||||
min_length=1,
|
||||
max_length=400,
|
||||
)
|
||||
search_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the search results (e.g., 'en', 'es').",
|
||||
pattern=r"^[a-z]{2}$",
|
||||
)
|
||||
ui_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the user interface (e.g., 'en-US', 'es-AR').",
|
||||
pattern=r"^[a-z]{2}-[A-Z]{2}$",
|
||||
)
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description="Country code for geo-targeting (e.g., 'US', 'BR').",
|
||||
pattern=r"^[A-Z]{2}$",
|
||||
)
|
||||
safesearch: Literal["off", "moderate", "strict"] | None = Field(
|
||||
default=None,
|
||||
description="Filter out explicit content. Options: off/moderate/strict",
|
||||
)
|
||||
count: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of results to return.",
|
||||
ge=1,
|
||||
le=50,
|
||||
)
|
||||
offset: int | None = Field(
|
||||
default=None,
|
||||
description="Skip the first N result sets/pages. Max is 9.",
|
||||
ge=0,
|
||||
le=9,
|
||||
)
|
||||
spellcheck: bool | None = Field(
|
||||
default=None,
|
||||
description="Attempt to correct spelling errors in the search query.",
|
||||
)
|
||||
freshness: Freshness | None = Field(
|
||||
default=None,
|
||||
description="Enforce freshness of results. Options: pd/pw/pm/py, or YYYY-MM-DDtoYYYY-MM-DD",
|
||||
)
|
||||
extra_snippets: bool | None = Field(
|
||||
default=None,
|
||||
description="Include up to 5 text snippets for each page if possible.",
|
||||
)
|
||||
goggles: str | list[str] | None = Field(
|
||||
default=None,
|
||||
description="Goggles act as a custom re-ranking mechanism. Goggle source or URLs.",
|
||||
)
|
||||
include_fetch_metadata: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to include fetch metadata in the results.",
|
||||
)
|
||||
operators: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to apply search operators (e.g., site:example.com).",
|
||||
)
|
||||
|
||||
|
||||
class BaseSearchHeaders(BaseModel):
|
||||
"""Common headers for Brave Search endpoints."""
|
||||
|
||||
x_subscription_token: str = Field(
|
||||
alias="x-subscription-token",
|
||||
description="API key for Brave Search",
|
||||
)
|
||||
api_version: str | None = Field(
|
||||
alias="api-version",
|
||||
default=None,
|
||||
description="API version to use. Default is latest available.",
|
||||
pattern=r"^\d{4}-\d{2}-\d{2}$", # YYYY-MM-DD
|
||||
)
|
||||
accept: Literal["application/json"] | Literal["*/*"] | None = Field(
|
||||
default=None,
|
||||
description="Accept header for the request.",
|
||||
)
|
||||
cache_control: Literal["no-cache"] | None = Field(
|
||||
alias="cache-control",
|
||||
default=None,
|
||||
description="Cache control header for the request.",
|
||||
)
|
||||
user_agent: str | None = Field(
|
||||
alias="user-agent",
|
||||
default=None,
|
||||
description="User agent for the request.",
|
||||
)
|
||||
|
||||
|
||||
class LLMContextHeaders(BaseSearchHeaders):
|
||||
"""Headers for Brave LLM Context endpoint."""
|
||||
|
||||
x_loc_lat: float | None = Field(
|
||||
alias="x-loc-lat",
|
||||
default=None,
|
||||
description="Latitude of the user's location.",
|
||||
ge=-90.0,
|
||||
le=90.0,
|
||||
)
|
||||
x_loc_long: float | None = Field(
|
||||
alias="x-loc-long",
|
||||
default=None,
|
||||
description="Longitude of the user's location.",
|
||||
ge=-180.0,
|
||||
le=180.0,
|
||||
)
|
||||
x_loc_city: str | None = Field(
|
||||
alias="x-loc-city",
|
||||
default=None,
|
||||
description="City of the user's location.",
|
||||
)
|
||||
x_loc_state: str | None = Field(
|
||||
alias="x-loc-state",
|
||||
default=None,
|
||||
description="State of the user's location.",
|
||||
)
|
||||
x_loc_state_name: str | None = Field(
|
||||
alias="x-loc-state-name",
|
||||
default=None,
|
||||
description="Name of the state of the user's location.",
|
||||
)
|
||||
x_loc_country: str | None = Field(
|
||||
alias="x-loc-country",
|
||||
default=None,
|
||||
description="The ISO 3166-1 alpha-2 country code of the user's location.",
|
||||
)
|
||||
|
||||
|
||||
class LocalPOIsHeaders(BaseSearchHeaders):
|
||||
"""Headers for Brave Local POIs endpoint."""
|
||||
|
||||
x_loc_lat: float | None = Field(
|
||||
alias="x-loc-lat",
|
||||
default=None,
|
||||
description="Latitude of the user's location.",
|
||||
ge=-90.0,
|
||||
le=90.0,
|
||||
)
|
||||
x_loc_long: float | None = Field(
|
||||
alias="x-loc-long",
|
||||
default=None,
|
||||
description="Longitude of the user's location.",
|
||||
ge=-180.0,
|
||||
le=180.0,
|
||||
)
|
||||
|
||||
|
||||
class LocalPOIsDescriptionHeaders(BaseSearchHeaders):
|
||||
"""Headers for Brave Local POI Descriptions endpoint."""
|
||||
|
||||
|
||||
class VideoSearchHeaders(BaseSearchHeaders):
|
||||
"""Headers for Brave Video Search endpoint."""
|
||||
|
||||
|
||||
class ImageSearchHeaders(BaseSearchHeaders):
|
||||
"""Headers for Brave Image Search endpoint."""
|
||||
|
||||
|
||||
class NewsSearchHeaders(BaseSearchHeaders):
|
||||
"""Headers for Brave News Search endpoint."""
|
||||
|
||||
|
||||
class WebSearchHeaders(BaseSearchHeaders):
|
||||
"""Headers for Brave Web Search endpoint."""
|
||||
|
||||
x_loc_lat: float | None = Field(
|
||||
alias="x-loc-lat",
|
||||
default=None,
|
||||
description="Latitude of the user's location.",
|
||||
ge=-90.0,
|
||||
le=90.0,
|
||||
)
|
||||
x_loc_long: float | None = Field(
|
||||
alias="x-loc-long",
|
||||
default=None,
|
||||
description="Longitude of the user's location.",
|
||||
ge=-180.0,
|
||||
le=180.0,
|
||||
)
|
||||
x_loc_timezone: str | None = Field(
|
||||
alias="x-loc-timezone",
|
||||
default=None,
|
||||
description="Timezone of the user's location.",
|
||||
)
|
||||
x_loc_city: str | None = Field(
|
||||
alias="x-loc-city",
|
||||
default=None,
|
||||
description="City of the user's location.",
|
||||
)
|
||||
x_loc_state: str | None = Field(
|
||||
alias="x-loc-state",
|
||||
default=None,
|
||||
description="State of the user's location.",
|
||||
)
|
||||
x_loc_state_name: str | None = Field(
|
||||
alias="x-loc-state-name",
|
||||
default=None,
|
||||
description="Name of the state of the user's location.",
|
||||
)
|
||||
x_loc_country: str | None = Field(
|
||||
alias="x-loc-country",
|
||||
default=None,
|
||||
description="The ISO 3166-1 alpha-2 country code of the user's location.",
|
||||
)
|
||||
x_loc_postal_code: str | None = Field(
|
||||
alias="x-loc-postal-code",
|
||||
default=None,
|
||||
description="The postal code of the user's location.",
|
||||
)
|
||||
@@ -30,9 +30,8 @@ class FileWriterTool(BaseTool):
|
||||
|
||||
def _run(self, **kwargs: Any) -> str:
|
||||
try:
|
||||
# Create the directory if it doesn't exist
|
||||
if kwargs.get("directory") and not os.path.exists(kwargs["directory"]):
|
||||
os.makedirs(kwargs["directory"])
|
||||
if kwargs.get("directory"):
|
||||
os.makedirs(kwargs["directory"], exist_ok=True)
|
||||
|
||||
# Construct the full path
|
||||
filepath = os.path.join(kwargs.get("directory") or "", kwargs["filename"])
|
||||
|
||||
@@ -99,8 +99,8 @@ class FileCompressorTool(BaseTool):
|
||||
def _prepare_output(output_path: str, overwrite: bool) -> bool:
|
||||
"""Ensures output path is ready for writing."""
|
||||
output_dir = os.path.dirname(output_path)
|
||||
if output_dir and not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
if output_dir:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
if os.path.exists(output_path) and not overwrite:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -18,7 +18,6 @@ class MergeAgentHandlerToolError(Exception):
|
||||
"""Base exception for Merge Agent Handler tool errors."""
|
||||
|
||||
|
||||
|
||||
class MergeAgentHandlerTool(BaseTool):
|
||||
"""
|
||||
Wrapper for Merge Agent Handler tools.
|
||||
@@ -174,7 +173,7 @@ class MergeAgentHandlerTool(BaseTool):
|
||||
>>> tool = MergeAgentHandlerTool.from_tool_name(
|
||||
... tool_name="linear__create_issue",
|
||||
... tool_pack_id="134e0111-0f67-44f6-98f0-597000290bb3",
|
||||
... registered_user_id="91b2b905-e866-40c8-8be2-efe53827a0aa"
|
||||
... registered_user_id="91b2b905-e866-40c8-8be2-efe53827a0aa",
|
||||
... )
|
||||
"""
|
||||
# Create an empty args schema model (proper BaseModel subclass)
|
||||
@@ -210,7 +209,10 @@ class MergeAgentHandlerTool(BaseTool):
|
||||
if "parameters" in tool_schema:
|
||||
try:
|
||||
params = tool_schema["parameters"]
|
||||
if params.get("type") == "object" and "properties" in params:
|
||||
if (
|
||||
params.get("type") == "object"
|
||||
and "properties" in params
|
||||
):
|
||||
# Build field definitions for Pydantic
|
||||
fields = {}
|
||||
properties = params["properties"]
|
||||
@@ -298,7 +300,7 @@ class MergeAgentHandlerTool(BaseTool):
|
||||
>>> tools = MergeAgentHandlerTool.from_tool_pack(
|
||||
... tool_pack_id="134e0111-0f67-44f6-98f0-597000290bb3",
|
||||
... registered_user_id="91b2b905-e866-40c8-8be2-efe53827a0aa",
|
||||
... tool_names=["linear__create_issue", "linear__get_issues"]
|
||||
... tool_names=["linear__create_issue", "linear__get_issues"],
|
||||
... )
|
||||
"""
|
||||
# Create a temporary instance to fetch the tool list
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
|
||||
from crewai import Agent, Crew, Task
|
||||
from multion_tool import MultiOnTool # type: ignore[import-not-found]
|
||||
from multion_tool import MultiOnTool # type: ignore[import-not-found]
|
||||
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "Your Key"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user