mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-04-24 03:42:38 +00:00
Compare commits
8 Commits
devin/1753
...
lg-tool-in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c5558bc13 | ||
|
|
c978c4f495 | ||
|
|
fab7c8504a | ||
|
|
ae9907c8e7 | ||
|
|
3836ba50be | ||
|
|
63f7d75b34 | ||
|
|
c212dc2155 | ||
|
|
e18174de19 |
18
.github/workflows/tests.yml
vendored
18
.github/workflows/tests.yml
vendored
@@ -37,9 +37,25 @@ jobs:
|
||||
- name: Install the project
|
||||
run: uv sync --dev --all-extras
|
||||
|
||||
- name: Install SQLite with FTS5 support
|
||||
run: |
|
||||
# WORKAROUND: GitHub Actions' Ubuntu runner uses SQLite without FTS5 support compiled in.
|
||||
# This is a temporary fix until the runner includes SQLite with FTS5 or Python's sqlite3
|
||||
# module is compiled with FTS5 support by default.
|
||||
# TODO: Remove this workaround once GitHub Actions runners include SQLite FTS5 support
|
||||
|
||||
# Install pysqlite3-binary which has FTS5 support
|
||||
uv pip install pysqlite3-binary
|
||||
# Create a sitecustomize.py to override sqlite3 with pysqlite3
|
||||
mkdir -p .pytest_sqlite_override
|
||||
echo "import sys; import pysqlite3; sys.modules['sqlite3'] = pysqlite3" > .pytest_sqlite_override/sitecustomize.py
|
||||
# Test FTS5 availability
|
||||
PYTHONPATH=.pytest_sqlite_override uv run python -c "import sqlite3; print(f'SQLite version: {sqlite3.sqlite_version}')"
|
||||
PYTHONPATH=.pytest_sqlite_override uv run python -c "import sqlite3; conn = sqlite3.connect(':memory:'); conn.execute('CREATE VIRTUAL TABLE test USING fts5(content)'); print('FTS5 module available')"
|
||||
|
||||
- name: Run tests (group ${{ matrix.group }} of 8)
|
||||
run: |
|
||||
uv run pytest \
|
||||
PYTHONPATH=.pytest_sqlite_override uv run pytest \
|
||||
--block-network \
|
||||
--timeout=30 \
|
||||
-vv \
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -26,5 +26,4 @@ test_flow.html
|
||||
crewairules.mdc
|
||||
plan.md
|
||||
conceptual_plan.md
|
||||
build_image
|
||||
chromadb-*.lock
|
||||
build_image
|
||||
@@ -32,6 +32,11 @@
|
||||
"href": "https://chatgpt.com/g/g-qqTuUWsBY-crewai-assistant",
|
||||
"icon": "robot"
|
||||
},
|
||||
{
|
||||
"anchor": "Get Help",
|
||||
"href": "mailto:support@crewai.com",
|
||||
"icon": "headset"
|
||||
},
|
||||
{
|
||||
"anchor": "Releases",
|
||||
"href": "https://github.com/crewAIInc/crewAI/releases",
|
||||
@@ -161,9 +166,7 @@
|
||||
"en/tools/search-research/websitesearchtool",
|
||||
"en/tools/search-research/codedocssearchtool",
|
||||
"en/tools/search-research/youtubechannelsearchtool",
|
||||
"en/tools/search-research/youtubevideosearchtool",
|
||||
"en/tools/search-research/tavilysearchtool",
|
||||
"en/tools/search-research/tavilyextractortool"
|
||||
"en/tools/search-research/youtubevideosearchtool"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -367,6 +370,11 @@
|
||||
"href": "https://chatgpt.com/g/g-qqTuUWsBY-crewai-assistant",
|
||||
"icon": "robot"
|
||||
},
|
||||
{
|
||||
"anchor": "Obter Ajuda",
|
||||
"href": "mailto:support@crewai.com",
|
||||
"icon": "headset"
|
||||
},
|
||||
{
|
||||
"anchor": "Lançamentos",
|
||||
"href": "https://github.com/crewAIInc/crewAI/releases",
|
||||
|
||||
@@ -712,7 +712,7 @@ crew = Crew(
|
||||
memory_config={
|
||||
"provider": "mem0",
|
||||
"config": {"user_id": "john"},
|
||||
"user_memory": {} # DEPRECATED: Will be removed in version 0.156.0 or on 2025-08-04, use external_memory instead
|
||||
"user_memory": {} # Required - triggers user memory initialization
|
||||
},
|
||||
process=Process.sequential,
|
||||
verbose=True
|
||||
|
||||
@@ -54,11 +54,10 @@ crew = Crew(
|
||||
| **Markdown** _(optional)_ | `markdown` | `Optional[bool]` | Whether the task should instruct the agent to return the final answer formatted in Markdown. Defaults to False. |
|
||||
| **Config** _(optional)_ | `config` | `Optional[Dict[str, Any]]` | Task-specific configuration parameters. |
|
||||
| **Output File** _(optional)_ | `output_file` | `Optional[str]` | File path for storing the task output. |
|
||||
| **Create Directory** _(optional)_ | `create_directory` | `Optional[bool]` | Whether to create the directory for output_file if it doesn't exist. Defaults to True. |
|
||||
| **Output JSON** _(optional)_ | `output_json` | `Optional[Type[BaseModel]]` | A Pydantic model to structure the JSON output. |
|
||||
| **Output Pydantic** _(optional)_ | `output_pydantic` | `Optional[Type[BaseModel]]` | A Pydantic model for task output. |
|
||||
| **Callback** _(optional)_ | `callback` | `Optional[Any]` | Function/object to be executed after task completion. |
|
||||
| **Guardrail** _(optional)_ | `guardrail` | `Optional[Callable]` | Function to validate task output before proceeding to next task. |
|
||||
| **Guardrail** _(optional)_ | `guardrail` | `Optional[Union[Callable, str]]` | Function or string description to validate task output before proceeding to next task. |
|
||||
|
||||
## Creating Tasks
|
||||
|
||||
@@ -88,6 +87,7 @@ research_task:
|
||||
expected_output: >
|
||||
A list with 10 bullet points of the most relevant information about {topic}
|
||||
agent: researcher
|
||||
guardrail: ensure each bullet contains a minimum of 100 words
|
||||
|
||||
reporting_task:
|
||||
description: >
|
||||
@@ -334,7 +334,9 @@ Task guardrails provide a way to validate and transform task outputs before they
|
||||
are passed to the next task. This feature helps ensure data quality and provides
|
||||
feedback to agents when their output doesn't meet specific criteria.
|
||||
|
||||
Guardrails are implemented as Python functions that contain custom validation logic, giving you complete control over the validation process and ensuring reliable, deterministic results.
|
||||
**Guardrails can be defined in two ways:**
|
||||
1. **Function-based guardrails**: Python functions that implement custom validation logic
|
||||
2. **String-based guardrails**: Natural language descriptions that are automatically converted to LLM-powered validation
|
||||
|
||||
### Function-Based Guardrails
|
||||
|
||||
@@ -376,7 +378,82 @@ blog_task = Task(
|
||||
- On success: it returns a tuple of `(bool, Any)`. For example: `(True, validated_result)`
|
||||
- On Failure: it returns a tuple of `(bool, str)`. For example: `(False, "Error message explain the failure")`
|
||||
|
||||
### String-Based Guardrails
|
||||
|
||||
String-based guardrails allow you to describe validation criteria in natural language. When you provide a string instead of a function, CrewAI automatically converts it to an `LLMGuardrail` that uses an AI agent to validate the task output.
|
||||
|
||||
#### Using String Guardrails in Python
|
||||
|
||||
```python Code
|
||||
from crewai import Task
|
||||
|
||||
# Simple string-based guardrail
|
||||
blog_task = Task(
|
||||
description="Write a blog post about AI",
|
||||
expected_output="A blog post under 200 words",
|
||||
agent=blog_agent,
|
||||
guardrail="Ensure the blog post is under 200 words and includes practical examples"
|
||||
)
|
||||
|
||||
# More complex validation criteria
|
||||
research_task = Task(
|
||||
description="Research AI trends for 2025",
|
||||
expected_output="A comprehensive research report",
|
||||
agent=research_agent,
|
||||
guardrail="Ensure each finding includes a credible source and is backed by recent data from 2024-2025"
|
||||
)
|
||||
```
|
||||
|
||||
#### Using String Guardrails in YAML
|
||||
|
||||
```yaml
|
||||
research_task:
|
||||
description: Research the latest AI developments
|
||||
expected_output: A list of 10 bullet points about AI
|
||||
agent: researcher
|
||||
guardrail: ensure each bullet contains a minimum of 100 words
|
||||
|
||||
validation_task:
|
||||
description: Validate the research findings
|
||||
expected_output: A validation report
|
||||
agent: validator
|
||||
guardrail: confirm all sources are from reputable publications and published within the last 2 years
|
||||
```
|
||||
|
||||
#### How String Guardrails Work
|
||||
|
||||
When you provide a string guardrail, CrewAI automatically:
|
||||
1. Creates an `LLMGuardrail` instance using the string as validation criteria
|
||||
2. Uses the task's agent LLM to power the validation
|
||||
3. Creates a temporary validation agent that checks the output against your criteria
|
||||
4. Returns detailed feedback if validation fails
|
||||
|
||||
This approach is ideal when you want to use natural language to describe validation rules without writing custom validation functions.
|
||||
|
||||
### LLMGuardrail Class
|
||||
|
||||
The `LLMGuardrail` class is the underlying mechanism that powers string-based guardrails. You can also use it directly for more advanced control:
|
||||
|
||||
```python Code
|
||||
from crewai import Task
|
||||
from crewai.tasks.llm_guardrail import LLMGuardrail
|
||||
from crewai.llm import LLM
|
||||
|
||||
# Create a custom LLMGuardrail with specific LLM
|
||||
custom_guardrail = LLMGuardrail(
|
||||
description="Ensure the response contains exactly 5 bullet points with proper citations",
|
||||
llm=LLM(model="gpt-4o-mini")
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Research AI safety measures",
|
||||
expected_output="A detailed analysis with bullet points",
|
||||
agent=research_agent,
|
||||
guardrail=custom_guardrail
|
||||
)
|
||||
```
|
||||
|
||||
**Note**: When you use a string guardrail, CrewAI automatically creates an `LLMGuardrail` instance using your task's agent LLM. Using `LLMGuardrail` directly gives you more control over the validation process and LLM selection.
|
||||
|
||||
### Error Handling Best Practices
|
||||
|
||||
@@ -804,87 +881,21 @@ These validations help in maintaining the consistency and reliability of task ex
|
||||
|
||||
## Creating Directories when Saving Files
|
||||
|
||||
The `create_directory` parameter controls whether CrewAI should automatically create directories when saving task outputs to files. This feature is particularly useful for organizing outputs and ensuring that file paths are correctly structured, especially when working with complex project hierarchies.
|
||||
|
||||
### Default Behavior
|
||||
|
||||
By default, `create_directory=True`, which means CrewAI will automatically create any missing directories in the output file path:
|
||||
You can now specify if a task should create directories when saving its output to a file. This is particularly useful for organizing outputs and ensuring that file paths are correctly structured.
|
||||
|
||||
```python Code
|
||||
# Default behavior - directories are created automatically
|
||||
report_task = Task(
|
||||
description='Generate a comprehensive market analysis report',
|
||||
expected_output='A detailed market analysis with charts and insights',
|
||||
agent=analyst_agent,
|
||||
output_file='reports/2025/market_analysis.md', # Creates 'reports/2025/' if it doesn't exist
|
||||
markdown=True
|
||||
# ...
|
||||
|
||||
save_output_task = Task(
|
||||
description='Save the summarized AI news to a file',
|
||||
expected_output='File saved successfully',
|
||||
agent=research_agent,
|
||||
tools=[file_save_tool],
|
||||
output_file='outputs/ai_news_summary.txt',
|
||||
create_directory=True
|
||||
)
|
||||
```
|
||||
|
||||
### Disabling Directory Creation
|
||||
|
||||
If you want to prevent automatic directory creation and ensure that the directory already exists, set `create_directory=False`:
|
||||
|
||||
```python Code
|
||||
# Strict mode - directory must already exist
|
||||
strict_output_task = Task(
|
||||
description='Save critical data that requires existing infrastructure',
|
||||
expected_output='Data saved to pre-configured location',
|
||||
agent=data_agent,
|
||||
output_file='secure/vault/critical_data.json',
|
||||
create_directory=False # Will raise RuntimeError if 'secure/vault/' doesn't exist
|
||||
)
|
||||
```
|
||||
|
||||
### YAML Configuration
|
||||
|
||||
You can also configure this behavior in your YAML task definitions:
|
||||
|
||||
```yaml tasks.yaml
|
||||
analysis_task:
|
||||
description: >
|
||||
Generate quarterly financial analysis
|
||||
expected_output: >
|
||||
A comprehensive financial report with quarterly insights
|
||||
agent: financial_analyst
|
||||
output_file: reports/quarterly/q4_2024_analysis.pdf
|
||||
create_directory: true # Automatically create 'reports/quarterly/' directory
|
||||
|
||||
audit_task:
|
||||
description: >
|
||||
Perform compliance audit and save to existing audit directory
|
||||
expected_output: >
|
||||
A compliance audit report
|
||||
agent: auditor
|
||||
output_file: audit/compliance_report.md
|
||||
create_directory: false # Directory must already exist
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
**Automatic Directory Creation (`create_directory=True`):**
|
||||
- Development and prototyping environments
|
||||
- Dynamic report generation with date-based folders
|
||||
- Automated workflows where directory structure may vary
|
||||
- Multi-tenant applications with user-specific folders
|
||||
|
||||
**Manual Directory Management (`create_directory=False`):**
|
||||
- Production environments with strict file system controls
|
||||
- Security-sensitive applications where directories must be pre-configured
|
||||
- Systems with specific permission requirements
|
||||
- Compliance environments where directory creation is audited
|
||||
|
||||
### Error Handling
|
||||
|
||||
When `create_directory=False` and the directory doesn't exist, CrewAI will raise a `RuntimeError`:
|
||||
|
||||
```python Code
|
||||
try:
|
||||
result = crew.kickoff()
|
||||
except RuntimeError as e:
|
||||
# Handle missing directory error
|
||||
print(f"Directory creation failed: {e}")
|
||||
# Create directory manually or use fallback location
|
||||
#...
|
||||
```
|
||||
|
||||
Check out the video below to see how to use structured outputs in CrewAI:
|
||||
|
||||
@@ -10,6 +10,8 @@ Neatlogs helps you **see what your agent did**, **why**, and **share it**.
|
||||
|
||||
It captures every step: thoughts, tool calls, responses, evaluations. No raw logs. Just clear, structured traces. Great for debugging and collaboration.
|
||||
|
||||
---
|
||||
|
||||
## Why use Neatlogs?
|
||||
|
||||
CrewAI agents use multiple tools and reasoning steps. When something goes wrong, you need context — not just errors.
|
||||
@@ -35,6 +37,8 @@ The best UX to view a CrewAI trace. Post comments anywhere you want. Use AI to d
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
## Core Features
|
||||
|
||||
- **Trace Viewer**: Track thoughts, tools, and decisions in sequence
|
||||
@@ -45,6 +49,8 @@ The best UX to view a CrewAI trace. Post comments anywhere you want. Use AI to d
|
||||
- **Ask the Trace (AI)**: Chat with your trace using Neatlogs AI bot
|
||||
- **Public Sharing**: Publish trace links to your community
|
||||
|
||||
---
|
||||
|
||||
## Quick Setup with CrewAI
|
||||
|
||||
<Steps>
|
||||
@@ -55,7 +61,7 @@ The best UX to view a CrewAI trace. Post comments anywhere you want. Use AI to d
|
||||
```bash
|
||||
pip install neatlogs
|
||||
```
|
||||
(Latest version 0.8.0, Python 3.8+; MIT license)
|
||||
(Latest version 0.8.0, Python 3.8+; MIT license) :contentReference[oaicite:1]{index=1}
|
||||
</Step>
|
||||
<Step title="Initialize Neatlogs">
|
||||
Before starting Crew agents, add:
|
||||
@@ -70,18 +76,18 @@ The best UX to view a CrewAI trace. Post comments anywhere you want. Use AI to d
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Under the Hood
|
||||
|
||||
According to GitHub, Neatlogs:
|
||||
|
||||
- Captures thoughts, tool calls, responses, errors, and token stats
|
||||
- Supports AI-powered task generation and robust evaluation workflows
|
||||
- Captures thoughts, tool calls, responses, errors, and token stats :contentReference[oaicite:2]{index=2}
|
||||
- Supports AI-powered task generation and robust evaluation workflows :contentReference[oaicite:3]{index=3}
|
||||
|
||||
All with just two lines of code.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Watch It Work
|
||||
|
||||
@@ -107,7 +113,7 @@ All with just two lines of code.
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Links & Support
|
||||
|
||||
@@ -115,9 +121,9 @@ All with just two lines of code.
|
||||
- 🔐 [Dashboard & API Key](https://app.neatlogs.com/)
|
||||
- 🐦 [Follow on Twitter](https://twitter.com/neatlogs)
|
||||
- 📧 Contact: hello@neatlogs.com
|
||||
- 🛠 [GitHub SDK](https://github.com/NeatLogs/neatlogs)
|
||||
|
||||
- 🛠 [GitHub SDK](https://github.com/NeatLogs/neatlogs) :contentReference[oaicite:4]{index=4}
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
|
||||
@@ -44,14 +44,6 @@ These tools enable your agents to search the web, research topics, and find info
|
||||
<Card title="YouTube Video Search" icon="play" href="/en/tools/search-research/youtubevideosearchtool">
|
||||
Find and analyze YouTube videos by topic, keyword, or criteria.
|
||||
</Card>
|
||||
|
||||
<Card title="Tavily Search Tool" icon="magnifying-glass" href="/en/tools/search-research/tavilysearchtool">
|
||||
Comprehensive web search using Tavily's AI-powered search API.
|
||||
</Card>
|
||||
|
||||
<Card title="Tavily Extractor Tool" icon="file-text" href="/en/tools/search-research/tavilyextractortool">
|
||||
Extract structured content from web pages using the Tavily API.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## **Common Use Cases**
|
||||
@@ -63,19 +55,17 @@ These tools enable your agents to search the web, research topics, and find info
|
||||
- **Academic Research**: Find scholarly articles and technical papers
|
||||
|
||||
```python
|
||||
from crewai_tools import SerperDevTool, GitHubSearchTool, YoutubeVideoSearchTool, TavilySearchTool, TavilyExtractorTool
|
||||
from crewai_tools import SerperDevTool, GitHubSearchTool, YoutubeVideoSearchTool
|
||||
|
||||
# Create research tools
|
||||
web_search = SerperDevTool()
|
||||
code_search = GitHubSearchTool()
|
||||
video_research = YoutubeVideoSearchTool()
|
||||
tavily_search = TavilySearchTool()
|
||||
content_extractor = TavilyExtractorTool()
|
||||
|
||||
# Add to your agent
|
||||
agent = Agent(
|
||||
role="Research Analyst",
|
||||
tools=[web_search, code_search, video_research, tavily_search, content_extractor],
|
||||
tools=[web_search, code_search, video_research],
|
||||
goal="Gather comprehensive information on any topic"
|
||||
)
|
||||
```
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
---
|
||||
title: "Tavily Extractor Tool"
|
||||
description: "Extract structured content from web pages using the Tavily API"
|
||||
icon: "file-text"
|
||||
---
|
||||
|
||||
The `TavilyExtractorTool` allows CrewAI agents to extract structured content from web pages using the Tavily API. It can process single URLs or lists of URLs and provides options for controlling the extraction depth and including images.
|
||||
|
||||
## Installation
|
||||
|
||||
To use the `TavilyExtractorTool`, you need to install the `tavily-python` library:
|
||||
|
||||
```shell
|
||||
pip install 'crewai[tools]' tavily-python
|
||||
```
|
||||
|
||||
You also need to set your Tavily API key as an environment variable:
|
||||
|
||||
```bash
|
||||
export TAVILY_API_KEY='your-tavily-api-key'
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
|
||||
Here's how to initialize and use the `TavilyExtractorTool` within a CrewAI agent:
|
||||
|
||||
```python
|
||||
import os
|
||||
from crewai import Agent, Task, Crew
|
||||
from crewai_tools import TavilyExtractorTool
|
||||
|
||||
# Ensure TAVILY_API_KEY is set in your environment
|
||||
# os.environ["TAVILY_API_KEY"] = "YOUR_API_KEY"
|
||||
|
||||
# Initialize the tool
|
||||
tavily_tool = TavilyExtractorTool()
|
||||
|
||||
# Create an agent that uses the tool
|
||||
extractor_agent = Agent(
|
||||
role='Web Content Extractor',
|
||||
goal='Extract key information from specified web pages',
|
||||
backstory='You are an expert at extracting relevant content from websites using the Tavily API.',
|
||||
tools=[tavily_tool],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Define a task for the agent
|
||||
extract_task = Task(
|
||||
description='Extract the main content from the URL https://example.com using basic extraction depth.',
|
||||
expected_output='A JSON string containing the extracted content from the URL.',
|
||||
agent=extractor_agent
|
||||
)
|
||||
|
||||
# Create and run the crew
|
||||
crew = Crew(
|
||||
agents=[extractor_agent],
|
||||
tasks=[extract_task],
|
||||
verbose=2
|
||||
)
|
||||
|
||||
result = crew.kickoff()
|
||||
print(result)
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The `TavilyExtractorTool` accepts the following arguments:
|
||||
|
||||
- `urls` (Union[List[str], str]): **Required**. A single URL string or a list of URL strings to extract data from.
|
||||
- `include_images` (Optional[bool]): Whether to include images in the extraction results. Defaults to `False`.
|
||||
- `extract_depth` (Literal["basic", "advanced"]): The depth of extraction. Use `"basic"` for faster, surface-level extraction or `"advanced"` for more comprehensive extraction. Defaults to `"basic"`.
|
||||
- `timeout` (int): The maximum time in seconds to wait for the extraction request to complete. Defaults to `60`.
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Multiple URLs with Advanced Extraction
|
||||
|
||||
```python
|
||||
# Example with multiple URLs and advanced extraction
|
||||
multi_extract_task = Task(
|
||||
description='Extract content from https://example.com and https://anotherexample.org using advanced extraction.',
|
||||
expected_output='A JSON string containing the extracted content from both URLs.',
|
||||
agent=extractor_agent
|
||||
)
|
||||
|
||||
# Configure the tool with custom parameters
|
||||
custom_extractor = TavilyExtractorTool(
|
||||
extract_depth='advanced',
|
||||
include_images=True,
|
||||
timeout=120
|
||||
)
|
||||
|
||||
agent_with_custom_tool = Agent(
|
||||
role="Advanced Content Extractor",
|
||||
goal="Extract comprehensive content with images",
|
||||
tools=[custom_extractor]
|
||||
)
|
||||
```
|
||||
|
||||
### Tool Parameters
|
||||
|
||||
You can customize the tool's behavior by setting parameters during initialization:
|
||||
|
||||
```python
|
||||
# Initialize with custom configuration
|
||||
extractor_tool = TavilyExtractorTool(
|
||||
extract_depth='advanced', # More comprehensive extraction
|
||||
include_images=True, # Include image results
|
||||
timeout=90 # Custom timeout
|
||||
)
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Single or Multiple URLs**: Extract content from one URL or process multiple URLs in a single request
|
||||
- **Configurable Depth**: Choose between basic (fast) and advanced (comprehensive) extraction modes
|
||||
- **Image Support**: Optionally include images in the extraction results
|
||||
- **Structured Output**: Returns well-formatted JSON containing the extracted content
|
||||
- **Error Handling**: Robust handling of network timeouts and extraction errors
|
||||
|
||||
## Response Format
|
||||
|
||||
The tool returns a JSON string representing the structured data extracted from the provided URL(s). The exact structure depends on the content of the pages and the `extract_depth` used.
|
||||
|
||||
Common response elements include:
|
||||
- **Title**: The page title
|
||||
- **Content**: Main text content of the page
|
||||
- **Images**: Image URLs and metadata (when `include_images=True`)
|
||||
- **Metadata**: Additional page information like author, description, etc.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Content Analysis**: Extract and analyze content from competitor websites
|
||||
- **Research**: Gather structured data from multiple sources for analysis
|
||||
- **Content Migration**: Extract content from existing websites for migration
|
||||
- **Monitoring**: Regular extraction of content for change detection
|
||||
- **Data Collection**: Systematic extraction of information from web sources
|
||||
|
||||
Refer to the [Tavily API documentation](https://docs.tavily.com/docs/tavily-api/python-sdk#extract) for detailed information about the response structure and available options.
|
||||
@@ -1,122 +0,0 @@
|
||||
---
|
||||
title: "Tavily Search Tool"
|
||||
description: "Perform comprehensive web searches using the Tavily Search API"
|
||||
icon: "magnifying-glass"
|
||||
---
|
||||
|
||||
The `TavilySearchTool` provides an interface to the Tavily Search API, enabling CrewAI agents to perform comprehensive web searches. It allows for specifying search depth, topics, time ranges, included/excluded domains, and whether to include direct answers, raw content, or images in the results.
|
||||
|
||||
## Installation
|
||||
|
||||
To use the `TavilySearchTool`, you need to install the `tavily-python` library:
|
||||
|
||||
```shell
|
||||
pip install 'crewai[tools]' tavily-python
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Ensure your Tavily API key is set as an environment variable:
|
||||
|
||||
```bash
|
||||
export TAVILY_API_KEY='your_tavily_api_key'
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
|
||||
Here's how to initialize and use the `TavilySearchTool` within a CrewAI agent:
|
||||
|
||||
```python
|
||||
import os
|
||||
from crewai import Agent, Task, Crew
|
||||
from crewai_tools import TavilySearchTool
|
||||
|
||||
# Ensure the TAVILY_API_KEY environment variable is set
|
||||
# os.environ["TAVILY_API_KEY"] = "YOUR_TAVILY_API_KEY"
|
||||
|
||||
# Initialize the tool
|
||||
tavily_tool = TavilySearchTool()
|
||||
|
||||
# Create an agent that uses the tool
|
||||
researcher = Agent(
|
||||
role='Market Researcher',
|
||||
goal='Find information about the latest AI trends',
|
||||
backstory='An expert market researcher specializing in technology.',
|
||||
tools=[tavily_tool],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Create a task for the agent
|
||||
research_task = Task(
|
||||
description='Search for the top 3 AI trends in 2024.',
|
||||
expected_output='A JSON report summarizing the top 3 AI trends found.',
|
||||
agent=researcher
|
||||
)
|
||||
|
||||
# Form the crew and kick it off
|
||||
crew = Crew(
|
||||
agents=[researcher],
|
||||
tasks=[research_task],
|
||||
verbose=2
|
||||
)
|
||||
|
||||
result = crew.kickoff()
|
||||
print(result)
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The `TavilySearchTool` accepts the following arguments during initialization or when calling the `run` method:
|
||||
|
||||
- `query` (str): **Required**. The search query string.
|
||||
- `search_depth` (Literal["basic", "advanced"], optional): The depth of the search. Defaults to `"basic"`.
|
||||
- `topic` (Literal["general", "news", "finance"], optional): The topic to focus the search on. Defaults to `"general"`.
|
||||
- `time_range` (Literal["day", "week", "month", "year"], optional): The time range for the search. Defaults to `None`.
|
||||
- `days` (int, optional): The number of days to search back. Relevant if `time_range` is not set. Defaults to `7`.
|
||||
- `max_results` (int, optional): The maximum number of search results to return. Defaults to `5`.
|
||||
- `include_domains` (Sequence[str], optional): A list of domains to prioritize in the search. Defaults to `None`.
|
||||
- `exclude_domains` (Sequence[str], optional): A list of domains to exclude from the search. Defaults to `None`.
|
||||
- `include_answer` (Union[bool, Literal["basic", "advanced"]], optional): Whether to include a direct answer synthesized from the search results. Defaults to `False`.
|
||||
- `include_raw_content` (bool, optional): Whether to include the raw HTML content of the searched pages. Defaults to `False`.
|
||||
- `include_images` (bool, optional): Whether to include image results. Defaults to `False`.
|
||||
- `timeout` (int, optional): The request timeout in seconds. Defaults to `60`.
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
You can configure the tool with custom parameters:
|
||||
|
||||
```python
|
||||
# Example: Initialize with specific parameters
|
||||
custom_tavily_tool = TavilySearchTool(
|
||||
search_depth='advanced',
|
||||
max_results=10,
|
||||
include_answer=True
|
||||
)
|
||||
|
||||
# The agent will use these defaults
|
||||
agent_with_custom_tool = Agent(
|
||||
role="Advanced Researcher",
|
||||
goal="Conduct detailed research with comprehensive results",
|
||||
tools=[custom_tavily_tool]
|
||||
)
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Comprehensive Search**: Access to Tavily's powerful search index
|
||||
- **Configurable Depth**: Choose between basic and advanced search modes
|
||||
- **Topic Filtering**: Focus searches on general, news, or finance topics
|
||||
- **Time Range Control**: Limit results to specific time periods
|
||||
- **Domain Control**: Include or exclude specific domains
|
||||
- **Direct Answers**: Get synthesized answers from search results
|
||||
- **Content Filtering**: Prevent context window issues with automatic content truncation
|
||||
|
||||
## Response Format
|
||||
|
||||
The tool returns search results as a JSON string containing:
|
||||
- Search results with titles, URLs, and content snippets
|
||||
- Optional direct answers to queries
|
||||
- Optional image results
|
||||
- Optional raw HTML content (when enabled)
|
||||
|
||||
Content for each result is automatically truncated to prevent context window issues while maintaining the most relevant information.
|
||||
@@ -54,11 +54,10 @@ crew = Crew(
|
||||
| **Markdown** _(opcional)_ | `markdown` | `Optional[bool]` | Se a tarefa deve instruir o agente a retornar a resposta final formatada em Markdown. O padrão é False. |
|
||||
| **Config** _(opcional)_ | `config` | `Optional[Dict[str, Any]]` | Parâmetros de configuração específicos da tarefa. |
|
||||
| **Arquivo de Saída** _(opcional)_| `output_file` | `Optional[str]` | Caminho do arquivo para armazenar a saída da tarefa. |
|
||||
| **Criar Diretório** _(opcional)_ | `create_directory` | `Optional[bool]` | Se deve criar o diretório para output_file caso não exista. O padrão é True. |
|
||||
| **Saída JSON** _(opcional)_ | `output_json` | `Optional[Type[BaseModel]]` | Um modelo Pydantic para estruturar a saída em JSON. |
|
||||
| **Output Pydantic** _(opcional)_ | `output_pydantic` | `Optional[Type[BaseModel]]` | Um modelo Pydantic para a saída da tarefa. |
|
||||
| **Callback** _(opcional)_ | `callback` | `Optional[Any]` | Função/objeto a ser executado após a conclusão da tarefa. |
|
||||
| **Guardrail** _(opcional)_ | `guardrail` | `Optional[Callable]` | Função para validar a saída da tarefa antes de prosseguir para a próxima tarefa. |
|
||||
| **Guardrail** _(opcional)_ | `guardrail` | `Optional[Union[Callable, str]]` | Função ou descrição em string para validar a saída da tarefa antes de prosseguir para a próxima tarefa. |
|
||||
|
||||
## Criando Tarefas
|
||||
|
||||
@@ -88,6 +87,7 @@ research_task:
|
||||
expected_output: >
|
||||
Uma lista com 10 tópicos em bullet points das informações mais relevantes sobre {topic}
|
||||
agent: researcher
|
||||
guardrail: garanta que cada bullet point contenha no mínimo 100 palavras
|
||||
|
||||
reporting_task:
|
||||
description: >
|
||||
@@ -332,7 +332,9 @@ analysis_task = Task(
|
||||
|
||||
Guardrails (trilhas de proteção) de tarefas fornecem uma maneira de validar e transformar as saídas das tarefas antes que elas sejam passadas para a próxima tarefa. Esse recurso assegura a qualidade dos dados e oferece feedback aos agentes quando sua saída não atende a critérios específicos.
|
||||
|
||||
Guardrails são implementados como funções Python que contêm lógica de validação customizada, proporcionando controle total sobre o processo de validação e garantindo resultados confiáveis e determinísticos.
|
||||
**Guardrails podem ser definidos de duas maneiras:**
|
||||
1. **Guardrails baseados em função**: Funções Python que implementam lógica de validação customizada
|
||||
2. **Guardrails baseados em string**: Descrições em linguagem natural que são automaticamente convertidas em validação baseada em LLM
|
||||
|
||||
### Guardrails Baseados em Função
|
||||
|
||||
@@ -374,7 +376,82 @@ blog_task = Task(
|
||||
- Em caso de sucesso: retorna uma tupla `(True, resultado_validado)`
|
||||
- Em caso de falha: retorna uma tupla `(False, "mensagem de erro explicando a falha")`
|
||||
|
||||
### Guardrails Baseados em String
|
||||
|
||||
Guardrails baseados em string permitem que você descreva critérios de validação em linguagem natural. Quando você fornece uma string em vez de uma função, o CrewAI automaticamente a converte em um `LLMGuardrail` que usa um agente de IA para validar a saída da tarefa.
|
||||
|
||||
#### Usando Guardrails de String em Python
|
||||
|
||||
```python Code
|
||||
from crewai import Task
|
||||
|
||||
# Guardrail simples baseado em string
|
||||
blog_task = Task(
|
||||
description="Escreva um post de blog sobre IA",
|
||||
expected_output="Um post de blog com menos de 200 palavras",
|
||||
agent=blog_agent,
|
||||
guardrail="Garanta que o post do blog tenha menos de 200 palavras e inclua exemplos práticos"
|
||||
)
|
||||
|
||||
# Critérios de validação mais complexos
|
||||
research_task = Task(
|
||||
description="Pesquise tendências de IA para 2025",
|
||||
expected_output="Um relatório abrangente de pesquisa",
|
||||
agent=research_agent,
|
||||
guardrail="Garanta que cada descoberta inclua uma fonte confiável e seja respaldada por dados recentes de 2024-2025"
|
||||
)
|
||||
```
|
||||
|
||||
#### Usando Guardrails de String em YAML
|
||||
|
||||
```yaml
|
||||
research_task:
|
||||
description: Pesquise os últimos desenvolvimentos em IA
|
||||
expected_output: Uma lista de 10 bullet points sobre IA
|
||||
agent: researcher
|
||||
guardrail: garanta que cada bullet point contenha no mínimo 100 palavras
|
||||
|
||||
validation_task:
|
||||
description: Valide os achados da pesquisa
|
||||
expected_output: Um relatório de validação
|
||||
agent: validator
|
||||
guardrail: confirme que todas as fontes são de publicações respeitáveis e publicadas nos últimos 2 anos
|
||||
```
|
||||
|
||||
#### Como Funcionam os Guardrails de String
|
||||
|
||||
Quando você fornece um guardrail de string, o CrewAI automaticamente:
|
||||
1. Cria uma instância `LLMGuardrail` usando a string como critério de validação
|
||||
2. Usa o LLM do agente da tarefa para alimentar a validação
|
||||
3. Cria um agente temporário de validação que verifica a saída contra seus critérios
|
||||
4. Retorna feedback detalhado se a validação falhar
|
||||
|
||||
Esta abordagem é ideal quando você quer usar linguagem natural para descrever regras de validação sem escrever funções de validação customizadas.
|
||||
|
||||
### Classe LLMGuardrail
|
||||
|
||||
A classe `LLMGuardrail` é o mecanismo subjacente que alimenta os guardrails baseados em string. Você também pode usá-la diretamente para maior controle avançado:
|
||||
|
||||
```python Code
|
||||
from crewai import Task
|
||||
from crewai.tasks.llm_guardrail import LLMGuardrail
|
||||
from crewai.llm import LLM
|
||||
|
||||
# Crie um LLMGuardrail customizado com LLM específico
|
||||
custom_guardrail = LLMGuardrail(
|
||||
description="Garanta que a resposta contenha exatamente 5 bullet points com citações adequadas",
|
||||
llm=LLM(model="gpt-4o-mini")
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Pesquise medidas de segurança em IA",
|
||||
expected_output="Uma análise detalhada com bullet points",
|
||||
agent=research_agent,
|
||||
guardrail=custom_guardrail
|
||||
)
|
||||
```
|
||||
|
||||
**Nota**: Quando você usa um guardrail de string, o CrewAI automaticamente cria uma instância `LLMGuardrail` usando o LLM do agente da sua tarefa. Usar `LLMGuardrail` diretamente lhe dá mais controle sobre o processo de validação e seleção de LLM.
|
||||
|
||||
### Melhores Práticas de Tratamento de Erros
|
||||
|
||||
@@ -825,7 +902,26 @@ task = Task(
|
||||
)
|
||||
```
|
||||
|
||||
#### Use uma abordagem no-code para validação
|
||||
|
||||
```python Code
|
||||
from crewai import Task
|
||||
|
||||
task = Task(
|
||||
description="Gerar dados em JSON",
|
||||
expected_output="Objeto JSON válido",
|
||||
guardrail="Garanta que a resposta é um objeto JSON válido"
|
||||
)
|
||||
```
|
||||
|
||||
#### Usando YAML
|
||||
|
||||
```yaml
|
||||
research_task:
|
||||
...
|
||||
guardrail: garanta que cada bullet tenha no mínimo 100 palavras
|
||||
...
|
||||
```
|
||||
|
||||
```python Code
|
||||
@CrewBase
|
||||
@@ -941,87 +1037,21 @@ task = Task(
|
||||
|
||||
## Criando Diretórios ao Salvar Arquivos
|
||||
|
||||
O parâmetro `create_directory` controla se o CrewAI deve criar automaticamente diretórios ao salvar saídas de tarefas em arquivos. Este recurso é particularmente útil para organizar outputs e garantir que os caminhos de arquivos estejam estruturados corretamente, especialmente ao trabalhar com hierarquias de projetos complexas.
|
||||
|
||||
### Comportamento Padrão
|
||||
|
||||
Por padrão, `create_directory=True`, o que significa que o CrewAI criará automaticamente qualquer diretório ausente no caminho do arquivo de saída:
|
||||
Agora é possível especificar se uma tarefa deve criar diretórios ao salvar sua saída em arquivo. Isso é útil para organizar outputs e garantir que os caminhos estejam corretos.
|
||||
|
||||
```python Code
|
||||
# Comportamento padrão - diretórios são criados automaticamente
|
||||
report_task = Task(
|
||||
description='Gerar um relatório abrangente de análise de mercado',
|
||||
expected_output='Uma análise detalhada de mercado com gráficos e insights',
|
||||
agent=analyst_agent,
|
||||
output_file='reports/2025/market_analysis.md', # Cria 'reports/2025/' se não existir
|
||||
markdown=True
|
||||
# ...
|
||||
|
||||
save_output_task = Task(
|
||||
description='Salve o resumo das notícias de IA em um arquivo',
|
||||
expected_output='Arquivo salvo com sucesso',
|
||||
agent=research_agent,
|
||||
tools=[file_save_tool],
|
||||
output_file='outputs/ai_news_summary.txt',
|
||||
create_directory=True
|
||||
)
|
||||
```
|
||||
|
||||
### Desabilitando a Criação de Diretórios
|
||||
|
||||
Se você quiser evitar a criação automática de diretórios e garantir que o diretório já exista, defina `create_directory=False`:
|
||||
|
||||
```python Code
|
||||
# Modo estrito - o diretório já deve existir
|
||||
strict_output_task = Task(
|
||||
description='Salvar dados críticos que requerem infraestrutura existente',
|
||||
expected_output='Dados salvos em localização pré-configurada',
|
||||
agent=data_agent,
|
||||
output_file='secure/vault/critical_data.json',
|
||||
create_directory=False # Gerará RuntimeError se 'secure/vault/' não existir
|
||||
)
|
||||
```
|
||||
|
||||
### Configuração YAML
|
||||
|
||||
Você também pode configurar este comportamento em suas definições de tarefas YAML:
|
||||
|
||||
```yaml tasks.yaml
|
||||
analysis_task:
|
||||
description: >
|
||||
Gerar análise financeira trimestral
|
||||
expected_output: >
|
||||
Um relatório financeiro abrangente com insights trimestrais
|
||||
agent: financial_analyst
|
||||
output_file: reports/quarterly/q4_2024_analysis.pdf
|
||||
create_directory: true # Criar automaticamente o diretório 'reports/quarterly/'
|
||||
|
||||
audit_task:
|
||||
description: >
|
||||
Realizar auditoria de conformidade e salvar no diretório de auditoria existente
|
||||
expected_output: >
|
||||
Um relatório de auditoria de conformidade
|
||||
agent: auditor
|
||||
output_file: audit/compliance_report.md
|
||||
create_directory: false # O diretório já deve existir
|
||||
```
|
||||
|
||||
### Casos de Uso
|
||||
|
||||
**Criação Automática de Diretórios (`create_directory=True`):**
|
||||
- Ambientes de desenvolvimento e prototipagem
|
||||
- Geração dinâmica de relatórios com pastas baseadas em datas
|
||||
- Fluxos de trabalho automatizados onde a estrutura de diretórios pode variar
|
||||
- Aplicações multi-tenant com pastas específicas do usuário
|
||||
|
||||
**Gerenciamento Manual de Diretórios (`create_directory=False`):**
|
||||
- Ambientes de produção com controles rígidos do sistema de arquivos
|
||||
- Aplicações sensíveis à segurança onde diretórios devem ser pré-configurados
|
||||
- Sistemas com requisitos específicos de permissão
|
||||
- Ambientes de conformidade onde a criação de diretórios é auditada
|
||||
|
||||
### Tratamento de Erros
|
||||
|
||||
Quando `create_directory=False` e o diretório não existe, o CrewAI gerará um `RuntimeError`:
|
||||
|
||||
```python Code
|
||||
try:
|
||||
result = crew.kickoff()
|
||||
except RuntimeError as e:
|
||||
# Tratar erro de diretório ausente
|
||||
print(f"Falha na criação do diretório: {e}")
|
||||
# Criar diretório manualmente ou usar local alternativo
|
||||
#...
|
||||
```
|
||||
|
||||
Veja o vídeo abaixo para aprender como utilizar saídas estruturadas no CrewAI:
|
||||
|
||||
@@ -11,7 +11,7 @@ dependencies = [
|
||||
# Core Dependencies
|
||||
"pydantic>=2.4.2",
|
||||
"openai>=1.13.3",
|
||||
"litellm>=1.74.3",
|
||||
"litellm==1.72.6",
|
||||
"instructor>=1.3.3",
|
||||
# Text Processing
|
||||
"pdfplumber>=0.11.4",
|
||||
@@ -39,7 +39,6 @@ dependencies = [
|
||||
"tomli>=2.0.2",
|
||||
"blinker>=1.9.0",
|
||||
"json5>=0.10.0",
|
||||
"portalocker==2.7.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -48,7 +47,7 @@ Documentation = "https://docs.crewai.com"
|
||||
Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = ["crewai-tools~=0.55.0"]
|
||||
tools = ["crewai-tools~=0.51.0"]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
]
|
||||
|
||||
@@ -54,7 +54,7 @@ def _track_install_async():
|
||||
|
||||
_track_install_async()
|
||||
|
||||
__version__ = "0.148.0"
|
||||
__version__ = "0.141.0"
|
||||
__all__ = [
|
||||
"Agent",
|
||||
"Crew",
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any, Callable, Dict, List, Optional, Union
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.agents.agent_builder.base_agent_executor_mixin import CrewAgentExecutorMixin
|
||||
from crewai.agents.parser import (
|
||||
CrewAgentParser,
|
||||
AgentAction,
|
||||
AgentFinish,
|
||||
OutputParserException,
|
||||
@@ -95,6 +96,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
||||
else self.stop
|
||||
)
|
||||
)
|
||||
self._parser = CrewAgentParser(agent=self)
|
||||
|
||||
def invoke(self, inputs: Dict[str, str]) -> Dict[str, Any]:
|
||||
if "system" in self.prompt:
|
||||
@@ -120,8 +122,11 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
||||
raise
|
||||
except Exception as e:
|
||||
handle_unknown_error(self._printer, e)
|
||||
raise
|
||||
|
||||
if e.__class__.__module__.startswith("litellm"):
|
||||
# Do not retry on litellm errors
|
||||
raise e
|
||||
else:
|
||||
raise e
|
||||
|
||||
if self.ask_for_human_input:
|
||||
formatted_answer = self._handle_human_feedback(formatted_answer)
|
||||
@@ -140,6 +145,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
||||
while not isinstance(formatted_answer, AgentFinish):
|
||||
try:
|
||||
if has_reached_max_iterations(self.iterations, self.max_iter):
|
||||
self._parser.reached_max_iterations()
|
||||
formatted_answer = handle_max_iterations_exceeded(
|
||||
formatted_answer,
|
||||
printer=self._printer,
|
||||
@@ -147,6 +153,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
||||
messages=self.messages,
|
||||
llm=self.llm,
|
||||
callbacks=self.callbacks,
|
||||
parser=self._parser,
|
||||
)
|
||||
|
||||
enforce_rpm_limit(self.request_within_rpm_limit)
|
||||
@@ -158,7 +165,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
||||
printer=self._printer,
|
||||
from_task=self.task
|
||||
)
|
||||
formatted_answer = process_llm_response(answer, self.use_stop_words)
|
||||
formatted_answer = process_llm_response(answer, self.use_stop_words, self._parser)
|
||||
|
||||
if isinstance(formatted_answer, AgentAction):
|
||||
# Extract agent fingerprint if available
|
||||
|
||||
@@ -65,33 +65,26 @@ class CrewAgentParser:
|
||||
"""
|
||||
|
||||
_i18n: I18N = I18N()
|
||||
_max_iterations_reached: bool = False
|
||||
agent: Any = None
|
||||
|
||||
def __init__(self, agent: Optional[Any] = None):
|
||||
self.agent = agent
|
||||
|
||||
@staticmethod
|
||||
def parse_text(text: str) -> Union[AgentAction, AgentFinish]:
|
||||
"""
|
||||
Static method to parse text into an AgentAction or AgentFinish without needing to instantiate the class.
|
||||
|
||||
Args:
|
||||
text: The text to parse.
|
||||
|
||||
Returns:
|
||||
Either an AgentAction or AgentFinish based on the parsed content.
|
||||
"""
|
||||
parser = CrewAgentParser()
|
||||
return parser.parse(text)
|
||||
def reached_max_iterations(self) -> None:
|
||||
self._max_iterations_reached = True
|
||||
|
||||
def parse(self, text: str) -> Union[AgentAction, AgentFinish]:
|
||||
thought = self._extract_thought(text)
|
||||
includes_answer = FINAL_ANSWER_ACTION in text
|
||||
regex = (
|
||||
r"Action\s*\d*\s*:[\s]*(.*?)[\s]*Action\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"
|
||||
)
|
||||
action_match = re.search(regex, text, re.DOTALL)
|
||||
if includes_answer:
|
||||
action_match = self._find_last_action_input_pair(text)
|
||||
|
||||
# Prevent tool bypassing when both Action and Final Answer are present
|
||||
# If the model returns both, we PRIORITIZE the action to force tool execution
|
||||
if not self._max_iterations_reached and includes_answer and action_match:
|
||||
return self._create_agent_action(thought, action_match, text)
|
||||
|
||||
elif includes_answer:
|
||||
final_answer = text.split(FINAL_ANSWER_ACTION)[-1].strip()
|
||||
# Check whether the final answer ends with triple backticks.
|
||||
if final_answer.endswith("```"):
|
||||
@@ -103,15 +96,7 @@ class CrewAgentParser:
|
||||
return AgentFinish(thought, final_answer, text)
|
||||
|
||||
elif action_match:
|
||||
action = action_match.group(1)
|
||||
clean_action = self._clean_action(action)
|
||||
|
||||
action_input = action_match.group(2).strip()
|
||||
|
||||
tool_input = action_input.strip(" ").strip('"')
|
||||
safe_tool_input = self._safe_repair_json(tool_input)
|
||||
|
||||
return AgentAction(thought, clean_action, safe_tool_input, text)
|
||||
return self._create_agent_action(thought, action_match, text)
|
||||
|
||||
if not re.search(r"Action\s*\d*\s*:[\s]*(.*?)", text, re.DOTALL):
|
||||
raise OutputParserException(
|
||||
@@ -167,3 +152,69 @@ class CrewAgentParser:
|
||||
return tool_input
|
||||
|
||||
return str(result)
|
||||
|
||||
def _create_agent_action(self, thought: str, action_match: dict, text: str) -> AgentAction:
|
||||
cleaned_text = self._clean_agent_observations(text)
|
||||
action = action_match["action"]
|
||||
clean_action = self._clean_action(action)
|
||||
action_input = action_match["input"]
|
||||
|
||||
tool_input = action_input.strip(" ").strip('"')
|
||||
safe_tool_input = self._safe_repair_json(tool_input)
|
||||
|
||||
return AgentAction(thought, clean_action, safe_tool_input, cleaned_text)
|
||||
|
||||
def _find_last_action_input_pair(self, text: str) -> Optional[dict]:
|
||||
"""
|
||||
Finds the last complete Action / Action Input pair in the given text.
|
||||
Useful for handling multiple action/observation cycles.
|
||||
"""
|
||||
def _match_all_pairs(text: str) -> list[tuple[str, str]]:
|
||||
pattern = (
|
||||
r"Action\s*\d*\s*:\s*([^\n]+)" # Action content
|
||||
r"\s*[\n]+" # Optional whitespace/newline
|
||||
r"Action\s*\d*\s*Input\s*\d*\s*:\s*" # Action Input label
|
||||
r"([^\n]*(?:\n(?!Observation:|Thought:|Action\s*\d*\s*:|Final Answer:)[^\n]*)*)"
|
||||
)
|
||||
return re.findall(pattern, text, re.MULTILINE | re.DOTALL)
|
||||
|
||||
def _match_fallback_pair(text: str) -> Optional[dict]:
|
||||
fallback_pattern = (
|
||||
r"Action\s*\d*\s*:\s*(.*?)"
|
||||
r"\s*Action\s*\d*\s*Input\s*\d*\s*:\s*"
|
||||
r"(.*?)(?=\n(?:Observation:|Thought:|Action\s*\d*\s*:|Final Answer:)|$)"
|
||||
)
|
||||
match = re.search(fallback_pattern, text, re.DOTALL)
|
||||
if match:
|
||||
return {
|
||||
"action": match.group(1).strip(),
|
||||
"input": match.group(2).strip()
|
||||
}
|
||||
return None
|
||||
|
||||
matches = _match_all_pairs(text)
|
||||
if matches:
|
||||
last_action, last_input = matches[-1]
|
||||
return {
|
||||
"action": last_action.strip(),
|
||||
"input": last_input.strip()
|
||||
}
|
||||
|
||||
return _match_fallback_pair(text)
|
||||
|
||||
|
||||
def _clean_agent_observations(self, text: str) -> str:
|
||||
# Pattern: capture Action/Input lines, then Observation block until next Thought or end-of-string
|
||||
obs_pattern = re.compile(
|
||||
r'^(\s*Action:.*\n\s*Action Input:.*\n)' # group 1: Action + Action Input
|
||||
r'\s*Observation:.*?(?=(?:\n\s*Thought:|\Z))', # non-greedy until Thought: or end-of-string
|
||||
flags=re.DOTALL | re.MULTILINE
|
||||
)
|
||||
|
||||
if obs_pattern.search(text):
|
||||
text = obs_pattern.sub(r'\1', text)
|
||||
# Remove Final Answer and everything following if present
|
||||
text = re.sub(r'\n\s*Final\s+Answer:.*', '', text, flags=re.DOTALL | re.MULTILINE)
|
||||
# Normalize blank lines
|
||||
text = re.sub(r'\n\s*\n\s*\n+', '\n\n', text).strip()
|
||||
return text
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.148.0,<1.0.0"
|
||||
"crewai[tools]>=0.141.0,<1.0.0"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.148.0,<1.0.0",
|
||||
"crewai[tools]>=0.141.0,<1.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.148.0"
|
||||
"crewai[tools]>=0.141.0"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -161,7 +161,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
)
|
||||
user_memory: Optional[InstanceOf[UserMemory]] = Field(
|
||||
default=None,
|
||||
description="DEPRECATED: Will be removed in version 0.156.0 or on 2025-08-04, whichever comes first. Use external_memory instead.",
|
||||
description="An instance of the UserMemory to be used by the Crew to store/fetch memories of a specific user.",
|
||||
)
|
||||
external_memory: Optional[InstanceOf[ExternalMemory]] = Field(
|
||||
default=None,
|
||||
@@ -327,7 +327,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
self._short_term_memory = self.short_term_memory
|
||||
self._entity_memory = self.entity_memory
|
||||
|
||||
# UserMemory will be removed in version 0.156.0 or on 2025-08-04, whichever comes first
|
||||
# UserMemory is gonna to be deprecated in the future, but we have to initialize a default value for now
|
||||
self._user_memory = None
|
||||
|
||||
if self.memory:
|
||||
@@ -1255,7 +1255,6 @@ class Crew(FlowTrackable, BaseModel):
|
||||
if self.external_memory:
|
||||
copied_data["external_memory"] = self.external_memory.model_copy(deep=True)
|
||||
if self.user_memory:
|
||||
# DEPRECATED: UserMemory will be removed in version 0.156.0 or on 2025-08-04
|
||||
copied_data["user_memory"] = self.user_memory.model_copy(deep=True)
|
||||
|
||||
copied_data.pop("agents", None)
|
||||
|
||||
@@ -18,7 +18,6 @@ from crewai.utilities.chromadb import sanitize_collection_name
|
||||
from crewai.utilities.constants import KNOWLEDGE_DIRECTORY
|
||||
from crewai.utilities.logger import Logger
|
||||
from crewai.utilities.paths import db_storage_path
|
||||
from crewai.utilities.chromadb import create_persistent_client
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
@@ -85,11 +84,14 @@ class KnowledgeStorage(BaseKnowledgeStorage):
|
||||
raise Exception("Collection not initialized")
|
||||
|
||||
def initialize_knowledge_storage(self):
|
||||
self.app = create_persistent_client(
|
||||
path=os.path.join(db_storage_path(), "knowledge"),
|
||||
base_path = os.path.join(db_storage_path(), "knowledge")
|
||||
chroma_client = chromadb.PersistentClient(
|
||||
path=base_path,
|
||||
settings=Settings(allow_reset=True),
|
||||
)
|
||||
|
||||
self.app = chroma_client
|
||||
|
||||
try:
|
||||
collection_name = (
|
||||
f"knowledge_{self.collection_name}"
|
||||
@@ -109,8 +111,9 @@ class KnowledgeStorage(BaseKnowledgeStorage):
|
||||
def reset(self):
|
||||
base_path = os.path.join(db_storage_path(), KNOWLEDGE_DIRECTORY)
|
||||
if not self.app:
|
||||
self.app = create_persistent_client(
|
||||
path=base_path, settings=Settings(allow_reset=True)
|
||||
self.app = chromadb.PersistentClient(
|
||||
path=base_path,
|
||||
settings=Settings(allow_reset=True),
|
||||
)
|
||||
|
||||
self.app.reset()
|
||||
|
||||
@@ -35,6 +35,7 @@ from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess
|
||||
from crewai.agents.cache import CacheHandler
|
||||
from crewai.agents.parser import (
|
||||
CrewAgentParser,
|
||||
AgentAction,
|
||||
AgentFinish,
|
||||
OutputParserException,
|
||||
@@ -204,6 +205,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
_printer: Printer = PrivateAttr(default_factory=Printer)
|
||||
_guardrail: Optional[Callable] = PrivateAttr(default=None)
|
||||
_guardrail_retry_count: int = PrivateAttr(default=0)
|
||||
_parser: CrewAgentParser = PrivateAttr(default_factory=CrewAgentParser)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def setup_llm(self):
|
||||
@@ -239,6 +241,13 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def setup_parser(self):
|
||||
"""Set up the parser after initialization."""
|
||||
self._parser = CrewAgentParser(agent=self.original_agent)
|
||||
|
||||
return self
|
||||
|
||||
@field_validator("guardrail", mode="before")
|
||||
@classmethod
|
||||
def validate_guardrail_function(
|
||||
@@ -511,6 +520,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
messages=self._messages,
|
||||
llm=cast(LLM, self.llm),
|
||||
callbacks=self._callbacks,
|
||||
parser=self._parser,
|
||||
)
|
||||
|
||||
enforce_rpm_limit(self.request_within_rpm_limit)
|
||||
@@ -553,7 +563,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
)
|
||||
raise e
|
||||
|
||||
formatted_answer = process_llm_response(answer, self.use_stop_words)
|
||||
formatted_answer = process_llm_response(answer, self.use_stop_words, self._parser)
|
||||
|
||||
if isinstance(formatted_answer, AgentAction):
|
||||
try:
|
||||
@@ -622,4 +632,4 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
|
||||
def _append_message(self, text: str, role: str = "assistant") -> None:
|
||||
"""Append a message to the message list with the given role."""
|
||||
self._messages.append(format_message_for_llm(text, role=role))
|
||||
self._messages.append(format_message_for_llm(text, role=role))
|
||||
|
||||
@@ -59,7 +59,6 @@ from crewai.utilities.exceptions.context_window_exceeding_exception import (
|
||||
|
||||
load_dotenv()
|
||||
|
||||
litellm.suppress_debug_info = True
|
||||
|
||||
class FilteredStream(io.TextIOBase):
|
||||
_lock = None
|
||||
@@ -77,7 +76,9 @@ class FilteredStream(io.TextIOBase):
|
||||
|
||||
# Skip common noisy LiteLLM banners and any other lines that contain "litellm"
|
||||
if (
|
||||
"litellm.info:" in lower_s
|
||||
"give feedback / get help" in lower_s
|
||||
or "litellm.info:" in lower_s
|
||||
or "litellm" in lower_s
|
||||
or "Consider using a smaller input or implementing a text splitting strategy" in lower_s
|
||||
):
|
||||
return 0
|
||||
@@ -759,7 +760,7 @@ class LLM(BaseLLM):
|
||||
available_functions: Optional[Dict[str, Any]] = None,
|
||||
from_task: Optional[Any] = None,
|
||||
from_agent: Optional[Any] = None,
|
||||
) -> str | Any:
|
||||
) -> str:
|
||||
"""Handle a non-streaming response from the LLM.
|
||||
|
||||
Args:
|
||||
@@ -783,11 +784,13 @@ class LLM(BaseLLM):
|
||||
# Convert litellm's context window error to our own exception type
|
||||
# for consistent handling in the rest of the codebase
|
||||
raise LLMContextLengthExceededException(str(e))
|
||||
|
||||
# --- 2) Extract response message and content
|
||||
response_message = cast(Choices, cast(ModelResponse, response).choices)[
|
||||
0
|
||||
].message
|
||||
text_response = response_message.content or ""
|
||||
|
||||
# --- 3) Handle callbacks with usage info
|
||||
if callbacks and len(callbacks) > 0:
|
||||
for callback in callbacks:
|
||||
@@ -800,22 +803,21 @@ class LLM(BaseLLM):
|
||||
start_time=0,
|
||||
end_time=0,
|
||||
)
|
||||
|
||||
# --- 4) Check for tool calls
|
||||
tool_calls = getattr(response_message, "tool_calls", [])
|
||||
|
||||
# --- 5) If no tool calls or no available functions, return the text response directly as long as there is a text response
|
||||
if (not tool_calls or not available_functions) and text_response:
|
||||
# --- 5) If no tool calls or no available functions, return the text response directly
|
||||
if not tool_calls or not available_functions:
|
||||
self._handle_emit_call_events(response=text_response, call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, messages=params["messages"])
|
||||
return text_response
|
||||
# --- 6) If there is no text response, no available functions, but there are tool calls, return the tool calls
|
||||
elif tool_calls and not available_functions and not text_response:
|
||||
return tool_calls
|
||||
|
||||
# --- 7) Handle tool calls if present
|
||||
# --- 6) Handle tool calls if present
|
||||
tool_result = self._handle_tool_call(tool_calls, available_functions)
|
||||
if tool_result is not None:
|
||||
return tool_result
|
||||
# --- 8) If tool call handling didn't return a result, emit completion event and return text response
|
||||
|
||||
# --- 7) If tool call handling didn't return a result, emit completion event and return text response
|
||||
self._handle_emit_call_events(response=text_response, call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, messages=params["messages"])
|
||||
return text_response
|
||||
|
||||
@@ -950,18 +952,22 @@ class LLM(BaseLLM):
|
||||
# --- 3) Convert string messages to proper format if needed
|
||||
if isinstance(messages, str):
|
||||
messages = [{"role": "user", "content": messages}]
|
||||
|
||||
# --- 4) Handle O1 model special case (system messages not supported)
|
||||
if "o1" in self.model.lower():
|
||||
for message in messages:
|
||||
if message.get("role") == "system":
|
||||
message["role"] = "assistant"
|
||||
|
||||
# --- 5) Set up callbacks if provided
|
||||
with suppress_warnings():
|
||||
if callbacks and len(callbacks) > 0:
|
||||
self.set_callbacks(callbacks)
|
||||
|
||||
try:
|
||||
# --- 6) Prepare parameters for the completion call
|
||||
params = self._prepare_completion_params(messages, tools)
|
||||
|
||||
# --- 7) Make the completion call and handle response
|
||||
if self.stream:
|
||||
return self._handle_streaming_response(
|
||||
@@ -978,32 +984,12 @@ class LLM(BaseLLM):
|
||||
# whether to summarize the content or abort based on the respect_context_window flag
|
||||
raise
|
||||
except Exception as e:
|
||||
unsupported_stop = "Unsupported parameter" in str(e) and "'stop'" in str(e)
|
||||
|
||||
if unsupported_stop:
|
||||
if "additional_drop_params" in self.additional_params and isinstance(self.additional_params["additional_drop_params"], list):
|
||||
self.additional_params["additional_drop_params"].append("stop")
|
||||
else:
|
||||
self.additional_params = {"additional_drop_params": ["stop"]}
|
||||
|
||||
logging.info(
|
||||
"Retrying LLM call without the unsupported 'stop'"
|
||||
)
|
||||
|
||||
return self.call(
|
||||
messages,
|
||||
tools=tools,
|
||||
callbacks=callbacks,
|
||||
available_functions=available_functions,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
)
|
||||
|
||||
assert hasattr(crewai_event_bus, "emit")
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=LLMCallFailedEvent(error=str(e), from_task=from_task, from_agent=from_agent),
|
||||
)
|
||||
logging.error(f"LiteLLM call failed: {str(e)}")
|
||||
raise
|
||||
|
||||
def _handle_emit_call_events(self, response: Any, call_type: LLMCallType, from_task: Optional[Any] = None, from_agent: Optional[Any] = None, messages: str | list[dict[str, Any]] | None = None):
|
||||
@@ -1072,15 +1058,6 @@ class LLM(BaseLLM):
|
||||
messages.append({"role": "user", "content": "Please continue."})
|
||||
return messages
|
||||
|
||||
# TODO: Remove this code after merging PR https://github.com/BerriAI/litellm/pull/10917
|
||||
# Ollama doesn't supports last message to be 'assistant'
|
||||
if "ollama" in self.model.lower() and messages and messages[-1]["role"] == "assistant":
|
||||
messages = messages.copy()
|
||||
messages.append(
|
||||
{"role": "user", "content": ""}
|
||||
)
|
||||
return messages
|
||||
|
||||
# Handle Anthropic models
|
||||
if not self.is_anthropic:
|
||||
return messages
|
||||
|
||||
@@ -108,7 +108,6 @@ class ContextualMemory:
|
||||
|
||||
def _fetch_user_context(self, query: str) -> str:
|
||||
"""
|
||||
DEPRECATED: Will be removed in version 0.156.0 or on 2025-08-04, whichever comes first.
|
||||
Fetches and formats relevant user information from User Memory.
|
||||
Args:
|
||||
query (str): The search query to find relevant user memories.
|
||||
|
||||
@@ -64,7 +64,6 @@ class Mem0Storage(Storage):
|
||||
def save(self, value: Any, metadata: Dict[str, Any]) -> None:
|
||||
user_id = self._get_user_id()
|
||||
agent_name = self._get_agent_name()
|
||||
assistant_message = [{"role" : "assistant","content" : value}]
|
||||
params = None
|
||||
if self.memory_type == "short_term":
|
||||
params = {
|
||||
@@ -94,8 +93,7 @@ class Mem0Storage(Storage):
|
||||
if params:
|
||||
if isinstance(self.memory, MemoryClient):
|
||||
params["output_format"] = "v1.1"
|
||||
|
||||
self.memory.add(assistant_message, **params)
|
||||
self.memory.add(value, **params)
|
||||
|
||||
def search(
|
||||
self,
|
||||
|
||||
@@ -4,12 +4,12 @@ import logging
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from chromadb.api import ClientAPI
|
||||
|
||||
from crewai.memory.storage.base_rag_storage import BaseRAGStorage
|
||||
from crewai.utilities import EmbeddingConfigurator
|
||||
from crewai.utilities.chromadb import create_persistent_client
|
||||
from crewai.utilities.constants import MAX_FILE_NAME_LENGTH
|
||||
from crewai.utilities.paths import db_storage_path
|
||||
|
||||
@@ -60,15 +60,17 @@ class RAGStorage(BaseRAGStorage):
|
||||
self.embedder_config = configurator.configure_embedder(self.embedder_config)
|
||||
|
||||
def _initialize_app(self):
|
||||
import chromadb
|
||||
from chromadb.config import Settings
|
||||
|
||||
self._set_embedder_config()
|
||||
|
||||
self.app = create_persistent_client(
|
||||
chroma_client = chromadb.PersistentClient(
|
||||
path=self.path if self.path else self.storage_file_name,
|
||||
settings=Settings(allow_reset=self.allow_reset),
|
||||
)
|
||||
|
||||
self.app = chroma_client
|
||||
|
||||
self.collection = self.app.get_or_create_collection(
|
||||
name=self.type, embedding_function=self.embedder_config
|
||||
)
|
||||
|
||||
@@ -14,8 +14,7 @@ class UserMemory(Memory):
|
||||
|
||||
def __init__(self, crew=None):
|
||||
warnings.warn(
|
||||
"UserMemory is deprecated and will be removed in version 0.156.0 "
|
||||
"or on 2025-08-04, whichever comes first. "
|
||||
"UserMemory is deprecated and will be removed in a future version. "
|
||||
"Please use ExternalMemory instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import warnings
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class UserMemoryItem:
|
||||
def __init__(self, data: Any, user: str, metadata: Optional[Dict[str, Any]] = None):
|
||||
warnings.warn(
|
||||
"UserMemoryItem is deprecated and will be removed in version 0.156.0 "
|
||||
"or on 2025-08-04, whichever comes first. "
|
||||
"Please use ExternalMemory instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.data = data
|
||||
self.user = user
|
||||
self.metadata = metadata if metadata is not None else {}
|
||||
|
||||
@@ -71,6 +71,7 @@ def handle_max_iterations_exceeded(
|
||||
messages: List[Dict[str, str]],
|
||||
llm: Union[LLM, BaseLLM],
|
||||
callbacks: List[Any],
|
||||
parser: CrewAgentParser
|
||||
) -> Union[AgentAction, AgentFinish]:
|
||||
"""
|
||||
Handles the case when the maximum number of iterations is exceeded.
|
||||
@@ -109,7 +110,7 @@ def handle_max_iterations_exceeded(
|
||||
)
|
||||
raise ValueError("Invalid response from LLM call - None or empty.")
|
||||
|
||||
formatted_answer = format_answer(answer)
|
||||
formatted_answer = format_answer(parser, answer)
|
||||
# Return the formatted answer, regardless of its type
|
||||
return formatted_answer
|
||||
|
||||
@@ -119,10 +120,10 @@ def format_message_for_llm(prompt: str, role: str = "user") -> Dict[str, str]:
|
||||
return {"role": role, "content": prompt}
|
||||
|
||||
|
||||
def format_answer(answer: str) -> Union[AgentAction, AgentFinish]:
|
||||
def format_answer(parser: CrewAgentParser, answer: str) -> Union[AgentAction, AgentFinish]:
|
||||
"""Format a response from the LLM into an AgentAction or AgentFinish."""
|
||||
try:
|
||||
return CrewAgentParser.parse_text(answer)
|
||||
return parser.parse(answer)
|
||||
except Exception:
|
||||
# If parsing fails, return a default AgentFinish
|
||||
return AgentFinish(
|
||||
@@ -157,6 +158,10 @@ def get_llm_response(
|
||||
from_agent=from_agent,
|
||||
)
|
||||
except Exception as e:
|
||||
printer.print(
|
||||
content=f"Error during LLM call: {e}",
|
||||
color="red",
|
||||
)
|
||||
raise e
|
||||
if not answer:
|
||||
printer.print(
|
||||
@@ -169,18 +174,18 @@ def get_llm_response(
|
||||
|
||||
|
||||
def process_llm_response(
|
||||
answer: str, use_stop_words: bool
|
||||
answer: str, use_stop_words: bool, parser: CrewAgentParser
|
||||
) -> Union[AgentAction, AgentFinish]:
|
||||
"""Process the LLM response and format it into an AgentAction or AgentFinish."""
|
||||
if not use_stop_words:
|
||||
try:
|
||||
# Preliminary parsing to check for errors.
|
||||
format_answer(answer)
|
||||
format_answer(parser, answer)
|
||||
except OutputParserException as e:
|
||||
if FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE in e.error:
|
||||
answer = answer.split("Observation:")[0].strip()
|
||||
|
||||
return format_answer(answer)
|
||||
return format_answer(parser, answer)
|
||||
|
||||
|
||||
def handle_agent_action_core(
|
||||
@@ -228,17 +233,12 @@ def handle_unknown_error(printer: Any, exception: Exception) -> None:
|
||||
printer: Printer instance for output
|
||||
exception: The exception that occurred
|
||||
"""
|
||||
error_message = str(exception)
|
||||
|
||||
if "litellm" in error_message:
|
||||
return
|
||||
|
||||
printer.print(
|
||||
content="An unknown error occurred. Please check the details below.",
|
||||
color="red",
|
||||
)
|
||||
printer.print(
|
||||
content=f"Error details: {error_message}",
|
||||
content=f"Error details: {exception}",
|
||||
color="red",
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import re
|
||||
import portalocker
|
||||
from chromadb import PersistentClient
|
||||
from hashlib import md5
|
||||
from typing import Optional
|
||||
|
||||
|
||||
MIN_COLLECTION_LENGTH = 3
|
||||
MAX_COLLECTION_LENGTH = 63
|
||||
DEFAULT_COLLECTION = "default_collection"
|
||||
@@ -64,16 +60,3 @@ def sanitize_collection_name(name: Optional[str], max_collection_length: int = M
|
||||
sanitized = sanitized[:-1] + "z"
|
||||
|
||||
return sanitized
|
||||
|
||||
|
||||
def create_persistent_client(path: str, **kwargs):
|
||||
"""
|
||||
Creates a persistent client for ChromaDB with a lock file to prevent
|
||||
concurrent creations. Works for both multi-threads and multi-processes
|
||||
environments.
|
||||
"""
|
||||
lockfile = f"chromadb-{md5(path.encode(), usedforsecurity=False).hexdigest()}.lock"
|
||||
with portalocker.Lock(lockfile):
|
||||
client = PersistentClient(path=path, **kwargs)
|
||||
|
||||
return client
|
||||
|
||||
@@ -2010,6 +2010,7 @@ def test_crew_agent_executor_litellm_auth_error():
|
||||
from litellm.exceptions import AuthenticationError
|
||||
|
||||
from crewai.agents.tools_handler import ToolsHandler
|
||||
from crewai.utilities import Printer
|
||||
|
||||
# Create an agent and executor
|
||||
agent = Agent(
|
||||
@@ -2042,6 +2043,7 @@ def test_crew_agent_executor_litellm_auth_error():
|
||||
# Mock the LLM call to raise AuthenticationError
|
||||
with (
|
||||
patch.object(LLM, "call") as mock_llm_call,
|
||||
patch.object(Printer, "print") as mock_printer,
|
||||
pytest.raises(AuthenticationError) as exc_info,
|
||||
):
|
||||
mock_llm_call.side_effect = AuthenticationError(
|
||||
@@ -2055,6 +2057,13 @@ def test_crew_agent_executor_litellm_auth_error():
|
||||
}
|
||||
)
|
||||
|
||||
# Verify error handling messages
|
||||
error_message = f"Error during LLM call: {str(mock_llm_call.side_effect)}"
|
||||
mock_printer.assert_any_call(
|
||||
content=error_message,
|
||||
color="red",
|
||||
)
|
||||
|
||||
# Verify the call was only made once (no retries)
|
||||
mock_llm_call.assert_called_once()
|
||||
|
||||
|
||||
@@ -371,3 +371,151 @@ class MockAgent:
|
||||
|
||||
|
||||
# TODO: ADD TEST TO MAKE SURE ** REMOVAL DOESN'T MESS UP ANYTHING
|
||||
|
||||
def test_ensure_agent_action_is_selected_when_model_hallucinates_observation_and_final_answer(parser):
|
||||
text = """Let's continue our effort to gather comprehensive, well-rounded information about AI in healthcare in 2023 to compile a detailed research report effectively.
|
||||
|
||||
Action: Web Search
|
||||
Action Input: {"search_query": "innovations in AI for healthcare 2023 latest updates and challenges"}
|
||||
|
||||
Observation: The search is yielding repeated and abundant information on the fragmented, redundant regulatory frameworks, clinical validation importance, and varied insights about AI’s ongoing integration challenges in healthcare. To ensure a rich mix of insights, let's compile, structure, and organize these insights into a coherent report.
|
||||
|
||||
Content Synthesis:
|
||||
- **Innovations and Trends**:
|
||||
- AI is significantly contributing to personalized medicine, enabling more accurate patient diagnosis and treatment plans.
|
||||
- Deep learning models, especially in image and pattern recognition, are revolutionizing radiology and pathology.
|
||||
- AI's role in drug discovery is speeding up research and reducing costs and time for new drugs entering the market.
|
||||
- AI-driven wearable devices are proving crucial for patient monitoring, predicting potential health issues, and facilitating proactive care.
|
||||
|
||||
Thought: I now have ample information to construct a research report detailing innovations, challenges, and opportunities of AI in healthcare in 2023.
|
||||
|
||||
Final Answer: The finalized detailed research report on AI in Healthcare, 2023:
|
||||
|
||||
Title: Current Innovations, Challenges, and Potential of AI in Healthcare - 2023 Overview
|
||||
|
||||
Introduction:
|
||||
The integration of Artificial Intelligence (AI) in healthcare is heralding a new era of modern medicine. In 2023, substantial technological advancements have brought about transformative changes in healthcare delivery. This report explores the latest AI innovations, identifies prevalent challenges, and discusses the potential opportunities in healthcare.
|
||||
|
||||
Potential and Opportunities:
|
||||
AI's potential in healthcare is vast, presenting numerous opportunities:
|
||||
- Cost Reduction: AI has the capacity to streamline operations, cutting costs significantly.
|
||||
- Preventive Healthcare: Utilizing predictive analytics allows for early intervention and prevention, alleviating pressure on emergency and critical care resources.
|
||||
- Enhanced Surgeries: Robotic surgeries guided by AI improve surgical outcomes and patient recovery times.
|
||||
- Improved Patient Experience: AI-driven solutions personalize patient interaction, improving engagement and healthcare experiences.
|
||||
|
||||
Conclusion:
|
||||
AI continues to reshape the healthcare landscape in 2023. Facing challenges head-on with robust solutions will unlock unparalleled benefits, positioning AI as a cornerstone for future medical and healthcare advancements. With ongoing improvements in regulations, data quality, and validation processes, the full potential of AI in healthcare stands to be realized.
|
||||
"""
|
||||
result = parser.parse(text)
|
||||
expected_text = """Let's continue our effort to gather comprehensive, well-rounded information about AI in healthcare in 2023 to compile a detailed research report effectively.
|
||||
|
||||
Action: Web Search
|
||||
Action Input: {"search_query": "innovations in AI for healthcare 2023 latest updates and challenges"}
|
||||
|
||||
Thought: I now have ample information to construct a research report detailing innovations, challenges, and opportunities of AI in healthcare in 2023.
|
||||
"""
|
||||
assert isinstance(result, AgentAction)
|
||||
assert result.text.strip() == expected_text.strip()
|
||||
|
||||
def test_ensure_agent_action_is_selected_when_model_hallucinates_observation_field(parser):
|
||||
text = """Let's continue our effort to gather comprehensive, well-rounded information about AI in healthcare in 2023 to compile a detailed research report effectively.
|
||||
|
||||
Action: Web Search
|
||||
Action Input: {"search_query": "innovations in AI for healthcare 2023 latest updates and challenges"}
|
||||
|
||||
Observation: The search is yielding repeated and abundant information on the fragmented, redundant regulatory frameworks, clinical validation importance, and varied insights about AI’s ongoing integration challenges in healthcare. To ensure a rich mix of insights, let's compile, structure, and organize these insights into a coherent report.
|
||||
|
||||
Content Synthesis:
|
||||
- **Innovations and Trends**:
|
||||
- AI is significantly contributing to personalized medicine, enabling more accurate patient diagnosis and treatment plans.
|
||||
- Deep learning models, especially in image and pattern recognition, are revolutionizing radiology and pathology.
|
||||
|
||||
Final Answer: The finalized detailed research report on AI in Healthcare, 2023:
|
||||
|
||||
Title: Current Innovations, Challenges, and Potential of AI in Healthcare - 2023 Overview
|
||||
|
||||
Introduction:
|
||||
The integration of Artificial Intelligence (AI) in healthcare is heralding a new era of modern medicine. In 2023, substantial technological advancements have brought about transformative changes in healthcare delivery. This report explores the latest AI innovations, identifies prevalent challenges, and discusses the potential opportunities in healthcare.
|
||||
|
||||
Innovations and Trends:
|
||||
AI technologies are becoming deeply embedded in various aspects of healthcare operations. Key advancements include:
|
||||
- Personalized Medicine: AI's analytical capabilities produce precise diagnostic outcomes and tailored treatment plans, fostering personalized medicine.
|
||||
- Radiology and Pathology: AI, particularly through advanced deep learning models, is improving imaging accuracy, thereby transforming radiological and pathological analyses.
|
||||
"""
|
||||
result = parser.parse(text)
|
||||
expected_text = """Let's continue our effort to gather comprehensive, well-rounded information about AI in healthcare in 2023 to compile a detailed research report effectively.
|
||||
|
||||
Action: Web Search
|
||||
Action Input: {"search_query": "innovations in AI for healthcare 2023 latest updates and challenges"}
|
||||
"""
|
||||
assert isinstance(result, AgentAction)
|
||||
assert result.text.strip() == expected_text.strip()
|
||||
|
||||
|
||||
def test_ensure_agent_finish_is_selected_when_no_action_was_provided(parser):
|
||||
text = """
|
||||
```
|
||||
Thought: The repeated results indicate that there may be a technical issue retrieving new information. I will summarize the available knowledge to complete the task.
|
||||
Final Answer:
|
||||
Research Report on AI in Healthcare (2023)
|
||||
|
||||
1. Introduction:
|
||||
AI technologies have become increasingly important in healthcare for their potential to transform patient care, diagnostics, and operational efficiencies. As we progress through 2023, significant advancements are noted alongside various challenges that need addressing.
|
||||
|
||||
2. Developments in AI Technologies:
|
||||
Recent years have seen AI significantly impact medical imaging, precision medicine, drug discovery, and robotic surgery. AI algorithms, such as neural networks and machine learning models, provide breakthroughs in analyzing large datasets to identify disease patterns, optimize treatment plans, and predict outcomes. In 2023, AI continues to be integrated within electronic health records, telemedicine platforms, and virtual health assistants, expanding its access and utility.
|
||||
|
||||
3. Challenges:
|
||||
- **Data Quality and Availability:** AI models require accurate, comprehensive data. However, healthcare data often remains fragmented and inconsistent, limiting AI's efficacy. High-quality data collection and management are crucial.
|
||||
- **Regulatory Frameworks:** Establishing clear regulations is imperative to ensure AI is used safely in clinical environments. Policymakers need to develop standards for AI research, implementation, and continuous monitoring.
|
||||
- **Clinical Validation:** Before deploying AI models in healthcare applications, they must undergo rigorous clinical validation to confirm their safety and effectiveness.
|
||||
- **Privacy and Consent:** Patient data privacy concerns persist. AI systems need robust mechanisms for data protection and maintaining patient consent when using personal health information.
|
||||
|
||||
4. Future Potentials:
|
||||
AI holds the potential to democratize access to healthcare services by making diagnostic tools more accessible and improving personalized treatment plans. Future research and investments are expected to focus on enhancing AI models to process and generate insights from electronic health records, predict patient admissions, and improve monitoring systems in real time.
|
||||
|
||||
5. Conclusion:
|
||||
In 2023, AI in healthcare continues to grow, supported by technological advancements and increased investment, despite ongoing challenges. Addressing these issues could allow AI to revolutionize healthcare, improving patient outcomes, and streamlining the efficiency of healthcare systems worldwide.
|
||||
```
|
||||
"""
|
||||
result = parser.parse(text)
|
||||
|
||||
assert isinstance(result, AgentFinish)
|
||||
assert result.text.strip() == text.strip()
|
||||
|
||||
def test_ensure_max_iteration_reached_and_agent_hallucinates_observation_and_final_answer(parser):
|
||||
text = """Let's continue our effort to gather comprehensive, well-rounded information about AI in healthcare in 2023 to compile a detailed research report effectively.
|
||||
|
||||
Action: Web Search
|
||||
Action Input: {"search_query": "innovations in AI for healthcare 2023 latest updates and challenges"}
|
||||
|
||||
Observation: The search is yielding repeated and abundant information on the fragmented, redundant regulatory frameworks, clinical validation importance, and varied insights about AI’s ongoing integration challenges in healthcare. To ensure a rich mix of insights, let's compile, structure, and organize these insights into a coherent report.
|
||||
|
||||
Thought: I now have ample information to construct a research report detailing innovations, challenges, and opportunities of AI in healthcare in 2023.
|
||||
|
||||
Final Answer: The finalized detailed research report on AI in Healthcare, 2023:
|
||||
|
||||
Title: Current Innovations, Challenges, and Potential of AI in Healthcare - 2023 Overview
|
||||
|
||||
Introduction:
|
||||
The integration of Artificial Intelligence (AI) in healthcare is heralding a new era of modern medicine. In 2023, substantial technological advancements have brought about transformative changes in healthcare delivery. This report explores the latest AI innovations, identifies prevalent challenges, and discusses the potential opportunities in healthcare.
|
||||
|
||||
Conclusion:
|
||||
AI continues to reshape the healthcare landscape in 2023. Facing challenges head-on with robust solutions will unlock unparalleled benefits, positioning AI as a cornerstone for future medical and healthcare advancements. With ongoing improvements in regulations, data quality, and validation processes, the full potential of AI in healthcare stands to be realized.
|
||||
"""
|
||||
|
||||
parser.reached_max_iterations()
|
||||
result = parser.parse(text)
|
||||
expected_text = """
|
||||
The finalized detailed research report on AI in Healthcare, 2023:
|
||||
|
||||
Title: Current Innovations, Challenges, and Potential of AI in Healthcare - 2023 Overview
|
||||
|
||||
Introduction:
|
||||
The integration of Artificial Intelligence (AI) in healthcare is heralding a new era of modern medicine. In 2023, substantial technological advancements have brought about transformative changes in healthcare delivery. This report explores the latest AI innovations, identifies prevalent challenges, and discusses the potential opportunities in healthcare.
|
||||
|
||||
Conclusion:
|
||||
AI continues to reshape the healthcare landscape in 2023. Facing challenges head-on with robust solutions will unlock unparalleled benefits, positioning AI as a cornerstone for future medical and healthcare advancements. With ongoing improvements in regulations, data quality, and validation processes, the full potential of AI in healthcare stands to be realized.
|
||||
"""
|
||||
assert isinstance(result, AgentFinish)
|
||||
assert result.output.strip() == expected_text.strip()
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages": [{"role": "user", "content": "What is the capital of France?"}],
|
||||
"model": "o1-mini", "stop": ["stop"]}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate, zstd
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '115'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.75.0
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.75.0
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
x-stainless-read-timeout:
|
||||
- '600.0'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.11.12
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"error\": {\n \"message\": \"Unsupported parameter: 'stop'
|
||||
is not supported with this model.\",\n \"type\": \"invalid_request_error\",\n
|
||||
\ \"param\": \"stop\",\n \"code\": \"unsupported_parameter\"\n }\n}"
|
||||
headers:
|
||||
CF-RAY:
|
||||
- 961215744c94cb45-GIG
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '196'
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Fri, 18 Jul 2025 12:46:46 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- __cf_bm=KwJ1K47OHX4n2TZN8bMW37yKzKyK__S4HbTiCfyWjXM-1752842806-1.0.1.1-lweHFR7Kv2v7hT5I6xxYVz_7Ruu6aBdEgpJrSWrMxi_ficAeWC0oDeQ.0w2Lr1WRejIjqqcwSgdl6RixF2qEkjJZfS0pz_Vjjqexe44ayp4;
|
||||
path=/; expires=Fri, 18-Jul-25 13:16:46 GMT; domain=.api.openai.com; HttpOnly;
|
||||
Secure; SameSite=None
|
||||
- _cfuvid=zv09c6bwcgNsYU80ah3wXzqeaIKyt_h61EAh_XRA87I-1752842806652-0.0.1.1-604800000;
|
||||
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '20'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
strict-transport-security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
x-envoy-upstream-service-time:
|
||||
- '32'
|
||||
x-ratelimit-limit-requests:
|
||||
- '30000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '29999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '149999990'
|
||||
x-ratelimit-reset-requests:
|
||||
- 2ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_7be4715c3ee32aa406eacb68c7cc966e
|
||||
status:
|
||||
code: 400
|
||||
message: Bad Request
|
||||
- request:
|
||||
body: '{"messages": [{"role": "user", "content": "What is the capital of France?"}],
|
||||
"model": "o1-mini"}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate, zstd
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '97'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- __cf_bm=KwJ1K47OHX4n2TZN8bMW37yKzKyK__S4HbTiCfyWjXM-1752842806-1.0.1.1-lweHFR7Kv2v7hT5I6xxYVz_7Ruu6aBdEgpJrSWrMxi_ficAeWC0oDeQ.0w2Lr1WRejIjqqcwSgdl6RixF2qEkjJZfS0pz_Vjjqexe44ayp4;
|
||||
_cfuvid=zv09c6bwcgNsYU80ah3wXzqeaIKyt_h61EAh_XRA87I-1752842806652-0.0.1.1-604800000
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.75.0
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.75.0
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
x-stainless-read-timeout:
|
||||
- '600.0'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.11.12
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAA3RSwU7jMBC95ytGPlYNakJhQ2/sgSsg7QUhFA32pJni2JHtwFao/76yC3XQwsWH
|
||||
efOe35uZ9wJAsBIbELLHIIdRl78nGvaqOt/dPDxf71/fdg/9bXO3e5ETXt+LZWTY5x3J8Mk6k3YY
|
||||
NQW25ghLRxgoqla/LupmXTeXqwQMVpGONFuVAxsu61W9LldXZVV/MHvLkrzYwGMBAPCe3ujRKPor
|
||||
NpB0UmUg73FLYnNqAhDO6lgR6D37gCaIZQalNYFMsv2nJ5A4ckANtoMbh0YSsIfF4g4d+8XibM50
|
||||
1E0eo3MzaT0D0BgbMCZPnp8+kMPJZceGfd86Qm9N/NkHO4qEHgqAp5R6+hJEjM4OY2iDfaEkW62P
|
||||
ciLPOYPNJxhsQJ3rV83yG7VWUUDWfjY1IVH2pDIzjxgnxXYGFLNs/5v5TvuYm802q1yuf9TPgJQ0
|
||||
BlLt6Eix/Jo4tzmKZ/hT22nIybHw5F5ZUhuYXFyEog4nfTwQ4fc+0NB2bLbkRsfpSuKui0PxDwAA
|
||||
//8DAN7IUy8kAwAA
|
||||
headers:
|
||||
CF-RAY:
|
||||
- 961216c3f9837e07-GRU
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Fri, 18 Jul 2025 12:47:41 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '1027'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
strict-transport-security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
x-envoy-upstream-service-time:
|
||||
- '1029'
|
||||
x-ratelimit-limit-requests:
|
||||
- '30000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '29999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '149999990'
|
||||
x-ratelimit-reset-requests:
|
||||
- 2ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_19a0763b09f0410b9d09598078a04cd6
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -1,206 +0,0 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages": [{"role": "user", "content": "What is the capital of France?"}],
|
||||
"model": "o1-mini", "stop": ["stop"]}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate, zstd
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '115'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- __cf_bm=KwJ1K47OHX4n2TZN8bMW37yKzKyK__S4HbTiCfyWjXM-1752842806-1.0.1.1-lweHFR7Kv2v7hT5I6xxYVz_7Ruu6aBdEgpJrSWrMxi_ficAeWC0oDeQ.0w2Lr1WRejIjqqcwSgdl6RixF2qEkjJZfS0pz_Vjjqexe44ayp4;
|
||||
_cfuvid=zv09c6bwcgNsYU80ah3wXzqeaIKyt_h61EAh_XRA87I-1752842806652-0.0.1.1-604800000
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.75.0
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.75.0
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
x-stainless-read-timeout:
|
||||
- '600.0'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.11.12
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"error\": {\n \"message\": \"Unsupported parameter: 'stop'
|
||||
is not supported with this model.\",\n \"type\": \"invalid_request_error\",\n
|
||||
\ \"param\": \"stop\",\n \"code\": \"unsupported_parameter\"\n }\n}"
|
||||
headers:
|
||||
CF-RAY:
|
||||
- 961220323a627e05-GRU
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '196'
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Fri, 18 Jul 2025 12:54:06 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '9'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
strict-transport-security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
x-envoy-upstream-service-time:
|
||||
- '11'
|
||||
x-ratelimit-limit-requests:
|
||||
- '30000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '29999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '149999990'
|
||||
x-ratelimit-reset-requests:
|
||||
- 2ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_e8d7880c5977029062d8487d215e5282
|
||||
status:
|
||||
code: 400
|
||||
message: Bad Request
|
||||
- request:
|
||||
body: '{"messages": [{"role": "user", "content": "What is the capital of France?"}],
|
||||
"model": "o1-mini"}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate, zstd
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '97'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- __cf_bm=KwJ1K47OHX4n2TZN8bMW37yKzKyK__S4HbTiCfyWjXM-1752842806-1.0.1.1-lweHFR7Kv2v7hT5I6xxYVz_7Ruu6aBdEgpJrSWrMxi_ficAeWC0oDeQ.0w2Lr1WRejIjqqcwSgdl6RixF2qEkjJZfS0pz_Vjjqexe44ayp4;
|
||||
_cfuvid=zv09c6bwcgNsYU80ah3wXzqeaIKyt_h61EAh_XRA87I-1752842806652-0.0.1.1-604800000
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.75.0
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.75.0
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
x-stainless-read-timeout:
|
||||
- '600.0'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.11.12
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAA3SSQW/bMAyF7/4Vgo5BXCSeV6c5bkAPPTVbMaAYCoOT6JitLAkSPbQo8t8HKWns
|
||||
Yu1FB3181HsUXwshJGm5FVL1wGrwpvw2In/fXY3Pcd/sftzf9ENvnurm569dc9/IZVK4P4+o+E11
|
||||
odzgDTI5e8QqIDCmruvma7Wpv1T1ZQaD02iSzK3LgSyV1aqqy9VVua5Oyt6Rwii34nchhBCv+Uwe
|
||||
rcZnuRWr5dvNgDHCHuX2XCSEDM6kGwkxUmSwLJcTVM4y2mz7rkehwBODEa4T1wGsQkFRLBa3ECgu
|
||||
FhdzZcBujJCc29GYGQBrHUNKnj0/nMjh7LIjS7FvA0J0Nr0c2XmZ6aEQ4iGnHt8FkT64wXPL7glz
|
||||
23V9bCenOc/h5kTZMZgZuKyWH/RrNTKQibO5SQWqRz1JpyHDqMnNQDFL97+dj3ofk5Pdz5xVm08f
|
||||
mIBS6Bl16wNqUu9DT2UB0yZ+Vnaec7YsI4a/pLBlwpD+QmMHoznuiIwvkXFoO7J7DD5QXpT03cWh
|
||||
+AcAAP//AwAo/zsSJwMAAA==
|
||||
headers:
|
||||
CF-RAY:
|
||||
- 961220338bd47e05-GRU
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Fri, 18 Jul 2025 12:54:08 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '1280'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
strict-transport-security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
x-envoy-upstream-service-time:
|
||||
- '1286'
|
||||
x-ratelimit-limit-requests:
|
||||
- '30000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '29999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '149999990'
|
||||
x-ratelimit-reset-requests:
|
||||
- 2ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_b7390d46fa4e14380d42162cb22045df
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
import os
|
||||
from time import sleep
|
||||
from unittest.mock import MagicMock, patch
|
||||
@@ -665,49 +664,3 @@ def test_handle_streaming_tool_calls_no_tools(mock_emit):
|
||||
expected_completed_llm_call=1,
|
||||
expected_final_chunk_result=response,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
def test_llm_call_when_stop_is_unsupported(caplog):
|
||||
llm = LLM(model="o1-mini", stop=["stop"])
|
||||
with caplog.at_level(logging.INFO):
|
||||
result = llm.call("What is the capital of France?")
|
||||
assert "Retrying LLM call without the unsupported 'stop'" in caplog.text
|
||||
assert isinstance(result, str)
|
||||
assert "Paris" in result
|
||||
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
def test_llm_call_when_stop_is_unsupported_when_additional_drop_params_is_provided(caplog):
|
||||
llm = LLM(model="o1-mini", stop=["stop"], additional_drop_params=["another_param"])
|
||||
with caplog.at_level(logging.INFO):
|
||||
result = llm.call("What is the capital of France?")
|
||||
assert "Retrying LLM call without the unsupported 'stop'" in caplog.text
|
||||
assert isinstance(result, str)
|
||||
assert "Paris" in result
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ollama_llm():
|
||||
return LLM(model="ollama/llama3.2:3b")
|
||||
|
||||
def test_ollama_appends_dummy_user_message_when_last_is_assistant(ollama_llm):
|
||||
original_messages = [
|
||||
{"role": "user", "content": "Hi there"},
|
||||
{"role": "assistant", "content": "Hello!"},
|
||||
]
|
||||
|
||||
formatted = ollama_llm._format_messages_for_provider(original_messages)
|
||||
|
||||
assert len(formatted) == len(original_messages) + 1
|
||||
assert formatted[-1]["role"] == "user"
|
||||
assert formatted[-1]["content"] == ""
|
||||
|
||||
|
||||
def test_ollama_does_not_modify_when_last_is_user(ollama_llm):
|
||||
original_messages = [
|
||||
{"role": "user", "content": "Tell me a joke."},
|
||||
]
|
||||
|
||||
formatted = ollama_llm._format_messages_for_provider(original_messages)
|
||||
|
||||
assert formatted == original_messages
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from mem0.client.main import MemoryClient
|
||||
from mem0.memory.main import Memory
|
||||
|
||||
from crewai.agent import Agent
|
||||
from crewai.crew import Crew
|
||||
from crewai.memory.storage.mem0_storage import Mem0Storage
|
||||
from crewai.task import Task
|
||||
|
||||
|
||||
# Define the class (if not already defined)
|
||||
@@ -168,7 +172,7 @@ def test_save_method_with_memory_oss(mem0_storage_with_mocked_config):
|
||||
mem0_storage.save(test_value, test_metadata)
|
||||
|
||||
mem0_storage.memory.add.assert_called_once_with(
|
||||
[{'role': 'assistant' , 'content': test_value}],
|
||||
test_value,
|
||||
agent_id="Test_Agent",
|
||||
infer=False,
|
||||
metadata={"type": "short_term", "key": "value"},
|
||||
@@ -187,7 +191,7 @@ def test_save_method_with_memory_client(mem0_storage_with_memory_client_using_co
|
||||
mem0_storage.save(test_value, test_metadata)
|
||||
|
||||
mem0_storage.memory.add.assert_called_once_with(
|
||||
[{'role': 'assistant' , 'content': test_value}],
|
||||
test_value,
|
||||
agent_id="Test_Agent",
|
||||
infer=False,
|
||||
metadata={"type": "short_term", "key": "value"},
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import pytest
|
||||
import importlib.metadata
|
||||
from packaging import version
|
||||
|
||||
from crewai.llm import LLM
|
||||
from crewai.agent import Agent
|
||||
from crewai.task import Task
|
||||
from crewai.crew import Crew
|
||||
|
||||
|
||||
def test_litellm_minimum_version_constraint():
|
||||
"""Test that litellm meets the minimum version requirement."""
|
||||
try:
|
||||
litellm_version = importlib.metadata.version("litellm")
|
||||
minimum_version = "1.74.3"
|
||||
|
||||
assert version.parse(litellm_version) >= version.parse(minimum_version), (
|
||||
f"litellm version {litellm_version} is below minimum required version {minimum_version}"
|
||||
)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
pytest.fail("litellm package is not installed")
|
||||
|
||||
|
||||
def test_llm_creation_with_relaxed_litellm_constraint():
|
||||
"""Test that LLM can be created successfully with the relaxed litellm constraint."""
|
||||
llm = LLM(model="gpt-4o-mini")
|
||||
assert llm is not None
|
||||
assert llm.model == "gpt-4o-mini"
|
||||
|
||||
|
||||
def test_basic_llm_functionality_with_relaxed_constraint():
|
||||
"""Test that basic LLM functionality works with the relaxed litellm constraint."""
|
||||
llm = LLM(model="gpt-4o-mini", temperature=0.7, max_tokens=100)
|
||||
|
||||
assert llm.model == "gpt-4o-mini"
|
||||
assert llm.temperature == 0.7
|
||||
assert llm.max_tokens == 100
|
||||
|
||||
|
||||
def test_agent_creation_with_relaxed_litellm_constraint():
|
||||
"""Test that Agent can be created with LLM using relaxed litellm constraint."""
|
||||
llm = LLM(model="gpt-4o-mini")
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
llm=llm
|
||||
)
|
||||
|
||||
assert agent is not None
|
||||
assert agent.llm == llm
|
||||
assert agent.role == "Test Agent"
|
||||
|
||||
|
||||
def test_crew_functionality_with_relaxed_litellm_constraint():
|
||||
"""Test that Crew functionality works with the relaxed litellm constraint."""
|
||||
llm = LLM(model="gpt-4o-mini")
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
llm=llm
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Test task description",
|
||||
expected_output="Test output",
|
||||
agent=agent
|
||||
)
|
||||
|
||||
crew = Crew(
|
||||
agents=[agent],
|
||||
tasks=[task]
|
||||
)
|
||||
|
||||
assert crew is not None
|
||||
assert len(crew.agents) == 1
|
||||
assert len(crew.tasks) == 1
|
||||
assert crew.agents[0] == agent
|
||||
assert crew.tasks[0] == task
|
||||
|
||||
|
||||
def test_litellm_import_functionality():
|
||||
"""Test that litellm can be imported and basic functionality works."""
|
||||
import litellm
|
||||
from litellm.exceptions import ContextWindowExceededError, AuthenticationError
|
||||
|
||||
assert hasattr(litellm, 'completion')
|
||||
assert ContextWindowExceededError is not None
|
||||
assert AuthenticationError is not None
|
||||
|
||||
|
||||
def test_llm_supports_function_calling():
|
||||
"""Test that LLM function calling support detection works with relaxed constraint."""
|
||||
llm = LLM(model="gpt-4o-mini")
|
||||
|
||||
supports_functions = llm.supports_function_calling()
|
||||
assert isinstance(supports_functions, bool)
|
||||
|
||||
|
||||
def test_llm_context_window_size():
|
||||
"""Test that LLM context window size detection works with relaxed constraint."""
|
||||
llm = LLM(model="gpt-4o-mini")
|
||||
|
||||
context_window = llm.get_context_window_size()
|
||||
assert isinstance(context_window, int)
|
||||
assert context_window > 0
|
||||
|
||||
|
||||
def test_llm_anthropic_model_detection():
|
||||
"""Test that Anthropic model detection works with relaxed constraint."""
|
||||
anthropic_llm = LLM(model="anthropic/claude-3-sonnet")
|
||||
openai_llm = LLM(model="gpt-4o-mini")
|
||||
|
||||
assert anthropic_llm._is_anthropic_model() is True
|
||||
assert openai_llm._is_anthropic_model() is False
|
||||
@@ -1,27 +1,16 @@
|
||||
import multiprocessing
|
||||
import tempfile
|
||||
import unittest
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from chromadb.config import Settings
|
||||
from unittest.mock import patch, MagicMock
|
||||
import pytest
|
||||
|
||||
from crewai.utilities.chromadb import (
|
||||
MAX_COLLECTION_LENGTH,
|
||||
MIN_COLLECTION_LENGTH,
|
||||
is_ipv4_pattern,
|
||||
sanitize_collection_name,
|
||||
create_persistent_client,
|
||||
)
|
||||
|
||||
|
||||
def persistent_client_worker(path, queue):
|
||||
try:
|
||||
create_persistent_client(path=path)
|
||||
queue.put(None)
|
||||
except Exception as e:
|
||||
queue.put(e)
|
||||
|
||||
|
||||
class TestChromadbUtils(unittest.TestCase):
|
||||
def test_sanitize_collection_name_long_name(self):
|
||||
"""Test sanitizing a very long collection name."""
|
||||
@@ -90,34 +79,3 @@ class TestChromadbUtils(unittest.TestCase):
|
||||
self.assertLessEqual(len(sanitized), MAX_COLLECTION_LENGTH)
|
||||
self.assertTrue(sanitized[0].isalnum())
|
||||
self.assertTrue(sanitized[-1].isalnum())
|
||||
|
||||
def test_create_persistent_client_passes_args(self):
|
||||
with patch(
|
||||
"crewai.utilities.chromadb.PersistentClient"
|
||||
) as mock_persistent_client, tempfile.TemporaryDirectory() as tmpdir:
|
||||
mock_instance = MagicMock()
|
||||
mock_persistent_client.return_value = mock_instance
|
||||
|
||||
settings = Settings(allow_reset=True)
|
||||
client = create_persistent_client(path=tmpdir, settings=settings)
|
||||
|
||||
mock_persistent_client.assert_called_once_with(
|
||||
path=tmpdir, settings=settings
|
||||
)
|
||||
self.assertIs(client, mock_instance)
|
||||
|
||||
def test_create_persistent_client_process_safe(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
queue = multiprocessing.Queue()
|
||||
processes = [
|
||||
multiprocessing.Process(
|
||||
target=persistent_client_worker, args=(tmpdir, queue)
|
||||
)
|
||||
for _ in range(5)
|
||||
]
|
||||
|
||||
[p.start() for p in processes]
|
||||
[p.join() for p in processes]
|
||||
|
||||
errors = [queue.get(timeout=5) for _ in processes]
|
||||
self.assertTrue(all(err is None for err in errors))
|
||||
|
||||
Reference in New Issue
Block a user