diff --git a/README.md b/README.md
index edcbb6f51..06044f0c0 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,18 @@
-
+
# **CrewAI**
-π€ **CrewAI**: Production-grade framework for orchestrating sophisticated AI agent systems. From simple automations to complex real-world applications, CrewAI provides precise control and deep customization. By fostering collaborative intelligence through flexible, production-ready architecture, CrewAI empowers agents to work together seamlessly, tackling complex business challenges with predictable, consistent results.
+**CrewAI**: Production-grade framework for orchestrating sophisticated AI agent systems. From simple automations to complex real-world applications, CrewAI provides precise control and deep customization. By fostering collaborative intelligence through flexible, production-ready architecture, CrewAI empowers agents to work together seamlessly, tackling complex business challenges with predictable, consistent results.
+
+**CrewAI Enterprise**
+Want to plan, build (+ no code), deploy, monitor and interare your agents: [CrewAI Enterprise](https://www.crewai.com/enterprise). Designed for complex, real-world applications, our enterprise solution offers:
+
+- **Seamless Integrations**
+- **Scalable & Secure Deployment**
+- **Actionable Insights**
+- **24/7 Support**
@@ -190,7 +198,7 @@ research_task:
description: >
Conduct a thorough research about {topic}
Make sure you find any interesting and relevant information given
- the current year is 2024.
+ the current year is 2025.
expected_output: >
A list with 10 bullet points of the most relevant information about {topic}
agent: researcher
@@ -392,7 +400,7 @@ class AdvancedAnalysisFlow(Flow[MarketState]):
goal="Gather and validate supporting market data",
backstory="You excel at finding and correlating multiple data sources"
)
-
+
analysis_task = Task(
description="Analyze {sector} sector data for the past {timeframe}",
expected_output="Detailed market analysis with confidence score",
@@ -403,7 +411,7 @@ class AdvancedAnalysisFlow(Flow[MarketState]):
expected_output="Corroborating evidence and potential contradictions",
agent=researcher
)
-
+
# Demonstrate crew autonomy
analysis_crew = Crew(
agents=[analyst, researcher],
diff --git a/docs/concepts/agents.mdx b/docs/concepts/agents.mdx
index 2f81138a7..b81099386 100644
--- a/docs/concepts/agents.mdx
+++ b/docs/concepts/agents.mdx
@@ -43,7 +43,7 @@ Think of an agent as a specialized team member with specific skills, expertise,
| **Max Retry Limit** _(optional)_ | `max_retry_limit` | `int` | Maximum number of retries when an error occurs. Default is 2. |
| **Respect Context Window** _(optional)_ | `respect_context_window` | `bool` | Keep messages under context window size by summarizing. Default is True. |
| **Code Execution Mode** _(optional)_ | `code_execution_mode` | `Literal["safe", "unsafe"]` | Mode for code execution: 'safe' (using Docker) or 'unsafe' (direct). Default is 'safe'. |
-| **Embedder Config** _(optional)_ | `embedder_config` | `Optional[Dict[str, Any]]` | Configuration for the embedder used by the agent. |
+| **Embedder** _(optional)_ | `embedder` | `Optional[Dict[str, Any]]` | Configuration for the embedder used by the agent. |
| **Knowledge Sources** _(optional)_ | `knowledge_sources` | `Optional[List[BaseKnowledgeSource]]` | Knowledge sources available to the agent. |
| **Use System Prompt** _(optional)_ | `use_system_prompt` | `Optional[bool]` | Whether to use system prompt (for o1 model support). Default is True. |
@@ -152,7 +152,7 @@ agent = Agent(
use_system_prompt=True, # Default: True
tools=[SerperDevTool()], # Optional: List of tools
knowledge_sources=None, # Optional: List of knowledge sources
- embedder_config=None, # Optional: Custom embedder configuration
+ embedder=None, # Optional: Custom embedder configuration
system_template=None, # Optional: Custom system prompt template
prompt_template=None, # Optional: Custom prompt template
response_template=None, # Optional: Custom response template
diff --git a/docs/concepts/cli.mdx b/docs/concepts/cli.mdx
index ad64ee938..4c9f617ba 100644
--- a/docs/concepts/cli.mdx
+++ b/docs/concepts/cli.mdx
@@ -12,7 +12,7 @@ The CrewAI CLI provides a set of commands to interact with CrewAI, allowing you
To use the CrewAI CLI, make sure you have CrewAI installed:
-```shell
+```shell Terminal
pip install crewai
```
@@ -20,7 +20,7 @@ pip install crewai
The basic structure of a CrewAI CLI command is:
-```shell
+```shell Terminal
crewai [COMMAND] [OPTIONS] [ARGUMENTS]
```
@@ -30,7 +30,7 @@ crewai [COMMAND] [OPTIONS] [ARGUMENTS]
Create a new crew or flow.
-```shell
+```shell Terminal
crewai create [OPTIONS] TYPE NAME
```
@@ -38,7 +38,7 @@ crewai create [OPTIONS] TYPE NAME
- `NAME`: Name of the crew or flow
Example:
-```shell
+```shell Terminal
crewai create crew my_new_crew
crewai create flow my_new_flow
```
@@ -47,14 +47,14 @@ crewai create flow my_new_flow
Show the installed version of CrewAI.
-```shell
+```shell Terminal
crewai version [OPTIONS]
```
- `--tools`: (Optional) Show the installed version of CrewAI tools
Example:
-```shell
+```shell Terminal
crewai version
crewai version --tools
```
@@ -63,7 +63,7 @@ crewai version --tools
Train the crew for a specified number of iterations.
-```shell
+```shell Terminal
crewai train [OPTIONS]
```
@@ -71,7 +71,7 @@ crewai train [OPTIONS]
- `-f, --filename TEXT`: Path to a custom file for training (default: "trained_agents_data.pkl")
Example:
-```shell
+```shell Terminal
crewai train -n 10 -f my_training_data.pkl
```
@@ -79,14 +79,14 @@ crewai train -n 10 -f my_training_data.pkl
Replay the crew execution from a specific task.
-```shell
+```shell Terminal
crewai replay [OPTIONS]
```
- `-t, --task_id TEXT`: Replay the crew from this task ID, including all subsequent tasks
Example:
-```shell
+```shell Terminal
crewai replay -t task_123456
```
@@ -94,7 +94,7 @@ crewai replay -t task_123456
Retrieve your latest crew.kickoff() task outputs.
-```shell
+```shell Terminal
crewai log-tasks-outputs
```
@@ -102,7 +102,7 @@ crewai log-tasks-outputs
Reset the crew memories (long, short, entity, latest_crew_kickoff_outputs).
-```shell
+```shell Terminal
crewai reset-memories [OPTIONS]
```
@@ -113,7 +113,7 @@ crewai reset-memories [OPTIONS]
- `-a, --all`: Reset ALL memories
Example:
-```shell
+```shell Terminal
crewai reset-memories --long --short
crewai reset-memories --all
```
@@ -122,7 +122,7 @@ crewai reset-memories --all
Test the crew and evaluate the results.
-```shell
+```shell Terminal
crewai test [OPTIONS]
```
@@ -130,7 +130,7 @@ crewai test [OPTIONS]
- `-m, --model TEXT`: LLM Model to run the tests on the Crew (default: "gpt-4o-mini")
Example:
-```shell
+```shell Terminal
crewai test -n 5 -m gpt-3.5-turbo
```
@@ -138,7 +138,7 @@ crewai test -n 5 -m gpt-3.5-turbo
Run the crew.
-```shell
+```shell Terminal
crewai run
```
@@ -147,7 +147,36 @@ Some commands may require additional configuration or setup within your project
-### 9. API Keys
+### 9. Chat
+
+Starting in version `0.98.0`, when you run the `crewai chat` command, you start an interactive session with your crew. The AI assistant will guide you by asking for necessary inputs to execute the crew. Once all inputs are provided, the crew will execute its tasks.
+
+After receiving the results, you can continue interacting with the assistant for further instructions or questions.
+
+```shell Terminal
+crewai chat
+```
+
+Ensure you execute these commands from your CrewAI project's root directory.
+
+
+IMPORTANT: Set the `chat_llm` property in your `crew.py` file to enable this command.
+
+```python
+@crew
+def crew(self) -> Crew:
+ return Crew(
+ agents=self.agents,
+ tasks=self.tasks,
+ process=Process.sequential,
+ verbose=True,
+ chat_llm="gpt-4o", # LLM for chat orchestration
+ )
+```
+
+
+
+### 10. API Keys
When running ```crewai create crew``` command, the CLI will first show you the top 5 most common LLM providers and ask you to select one.
diff --git a/docs/concepts/crews.mdx b/docs/concepts/crews.mdx
index 58511b07c..3792e752d 100644
--- a/docs/concepts/crews.mdx
+++ b/docs/concepts/crews.mdx
@@ -23,14 +23,14 @@ A crew in crewAI represents a collaborative group of agents working together to
| **Language** _(optional)_ | `language` | Language used for the crew, defaults to English. |
| **Language File** _(optional)_ | `language_file` | Path to the language file to be used for the crew. |
| **Memory** _(optional)_ | `memory` | Utilized for storing execution memories (short-term, long-term, entity memory). |
-| **Memory Config** _(optional)_ | `memory_config` | Configuration for the memory provider to be used by the crew. |
-| **Cache** _(optional)_ | `cache` | Specifies whether to use a cache for storing the results of tools' execution. Defaults to `True`. |
-| **Embedder** _(optional)_ | `embedder` | Configuration for the embedder to be used by the crew. Mostly used by memory for now. Default is `{"provider": "openai"}`. |
-| **Full Output** _(optional)_ | `full_output` | Whether the crew should return the full output with all tasks outputs or just the final output. Defaults to `False`. |
+| **Memory Config** _(optional)_ | `memory_config` | Configuration for the memory provider to be used by the crew. |
+| **Cache** _(optional)_ | `cache` | Specifies whether to use a cache for storing the results of tools' execution. Defaults to `True`. |
+| **Embedder** _(optional)_ | `embedder` | Configuration for the embedder to be used by the crew. Mostly used by memory for now. Default is `{"provider": "openai"}`. |
+| **Full Output** _(optional)_ | `full_output` | Whether the crew should return the full output with all tasks outputs or just the final output. Defaults to `False`. |
| **Step Callback** _(optional)_ | `step_callback` | A function that is called after each step of every agent. This can be used to log the agent's actions or to perform other operations; it won't override the agent-specific `step_callback`. |
| **Task Callback** _(optional)_ | `task_callback` | A function that is called after the completion of each task. Useful for monitoring or additional operations post-task execution. |
| **Share Crew** _(optional)_ | `share_crew` | Whether you want to share the complete crew information and execution with the crewAI team to make the library better, and allow us to train models. |
-| **Output Log File** _(optional)_ | `output_log_file` | Whether you want to have a file with the complete crew output and execution. You can set it using True and it will default to the folder you are currently in and it will be called logs.txt or passing a string with the full path and name of the file. |
+| **Output Log File** _(optional)_ | `output_log_file` | Set to True to save logs as logs.txt in the current directory or provide a file path. Logs will be in JSON format if the filename ends in .json, otherwise .txt. Defautls to `None`. |
| **Manager Agent** _(optional)_ | `manager_agent` | `manager` sets a custom agent that will be used as a manager. |
| **Prompt File** _(optional)_ | `prompt_file` | Path to the prompt JSON file to be used for the crew. |
| **Planning** *(optional)* | `planning` | Adds planning ability to the Crew. When activated before each Crew iteration, all Crew data is sent to an AgentPlanner that will plan the tasks and this plan will be added to each task description. |
@@ -240,6 +240,23 @@ print(f"Tasks Output: {crew_output.tasks_output}")
print(f"Token Usage: {crew_output.token_usage}")
```
+## Accessing Crew Logs
+
+You can see real time log of the crew execution, by setting `output_log_file` as a `True(Boolean)` or a `file_name(str)`. Supports logging of events as both `file_name.txt` and `file_name.json`.
+In case of `True(Boolean)` will save as `logs.txt`.
+
+In case of `output_log_file` is set as `False(Booelan)` or `None`, the logs will not be populated.
+
+```python Code
+# Save crew logs
+crew = Crew(output_log_file = True) # Logs will be saved as logs.txt
+crew = Crew(output_log_file = file_name) # Logs will be saved as file_name.txt
+crew = Crew(output_log_file = file_name.txt) # Logs will be saved as file_name.txt
+crew = Crew(output_log_file = file_name.json) # Logs will be saved as file_name.json
+```
+
+
+
## Memory Utilization
Crews can utilize memory (short-term, long-term, and entity memory) to enhance their execution and learning over time. This feature allows crews to store and recall execution memories, aiding in decision-making and task execution strategies.
@@ -279,9 +296,9 @@ print(result)
Once your crew is assembled, initiate the workflow with the appropriate kickoff method. CrewAI provides several methods for better control over the kickoff process: `kickoff()`, `kickoff_for_each()`, `kickoff_async()`, and `kickoff_for_each_async()`.
- `kickoff()`: Starts the execution process according to the defined process flow.
-- `kickoff_for_each()`: Executes tasks for each agent individually.
+- `kickoff_for_each()`: Executes tasks sequentially for each provided input event or item in the collection.
- `kickoff_async()`: Initiates the workflow asynchronously.
-- `kickoff_for_each_async()`: Executes tasks for each agent individually in an asynchronous manner.
+- `kickoff_for_each_async()`: Executes tasks concurrently for each provided input event or item, leveraging asynchronous processing.
```python Code
# Start the crew's task execution
diff --git a/docs/concepts/flows.mdx b/docs/concepts/flows.mdx
index de01f4c1d..c22a619fe 100644
--- a/docs/concepts/flows.mdx
+++ b/docs/concepts/flows.mdx
@@ -232,18 +232,18 @@ class UnstructuredExampleFlow(Flow):
def first_method(self):
# The state automatically includes an 'id' field
print(f"State ID: {self.state['id']}")
- self.state.message = "Hello from structured flow"
- self.state.counter = 0
+ self.state['counter'] = 0
+ self.state['message'] = "Hello from structured flow"
@listen(first_method)
def second_method(self):
- self.state.counter += 1
- self.state.message += " - updated"
+ self.state['counter'] += 1
+ self.state['message'] += " - updated"
@listen(second_method)
def third_method(self):
- self.state.counter += 1
- self.state.message += " - updated again"
+ self.state['counter'] += 1
+ self.state['message'] += " - updated again"
print(f"State after third_method: {self.state}")
@@ -323,6 +323,91 @@ flow.kickoff()
By providing both unstructured and structured state management options, CrewAI Flows empowers developers to build AI workflows that are both flexible and robust, catering to a wide range of application requirements.
+## Flow Persistence
+
+The @persist decorator enables automatic state persistence in CrewAI Flows, allowing you to maintain flow state across restarts or different workflow executions. This decorator can be applied at either the class level or method level, providing flexibility in how you manage state persistence.
+
+### Class-Level Persistence
+
+When applied at the class level, the @persist decorator automatically persists all flow method states:
+
+```python
+@persist # Using SQLiteFlowPersistence by default
+class MyFlow(Flow[MyState]):
+ @start()
+ def initialize_flow(self):
+ # This method will automatically have its state persisted
+ self.state.counter = 1
+ print("Initialized flow. State ID:", self.state.id)
+
+ @listen(initialize_flow)
+ def next_step(self):
+ # The state (including self.state.id) is automatically reloaded
+ self.state.counter += 1
+ print("Flow state is persisted. Counter:", self.state.counter)
+```
+
+### Method-Level Persistence
+
+For more granular control, you can apply @persist to specific methods:
+
+```python
+class AnotherFlow(Flow[dict]):
+ @persist # Persists only this method's state
+ @start()
+ def begin(self):
+ if "runs" not in self.state:
+ self.state["runs"] = 0
+ self.state["runs"] += 1
+ print("Method-level persisted runs:", self.state["runs"])
+```
+
+### How It Works
+
+1. **Unique State Identification**
+ - Each flow state automatically receives a unique UUID
+ - The ID is preserved across state updates and method calls
+ - Supports both structured (Pydantic BaseModel) and unstructured (dictionary) states
+
+2. **Default SQLite Backend**
+ - SQLiteFlowPersistence is the default storage backend
+ - States are automatically saved to a local SQLite database
+ - Robust error handling ensures clear messages if database operations fail
+
+3. **Error Handling**
+ - Comprehensive error messages for database operations
+ - Automatic state validation during save and load
+ - Clear feedback when persistence operations encounter issues
+
+### Important Considerations
+
+- **State Types**: Both structured (Pydantic BaseModel) and unstructured (dictionary) states are supported
+- **Automatic ID**: The `id` field is automatically added if not present
+- **State Recovery**: Failed or restarted flows can automatically reload their previous state
+- **Custom Implementation**: You can provide your own FlowPersistence implementation for specialized storage needs
+
+### Technical Advantages
+
+1. **Precise Control Through Low-Level Access**
+ - Direct access to persistence operations for advanced use cases
+ - Fine-grained control via method-level persistence decorators
+ - Built-in state inspection and debugging capabilities
+ - Full visibility into state changes and persistence operations
+
+2. **Enhanced Reliability**
+ - Automatic state recovery after system failures or restarts
+ - Transaction-based state updates for data integrity
+ - Comprehensive error handling with clear error messages
+ - Robust validation during state save and load operations
+
+3. **Extensible Architecture**
+ - Customizable persistence backend through FlowPersistence interface
+ - Support for specialized storage solutions beyond SQLite
+ - Compatible with both structured (Pydantic) and unstructured (dict) states
+ - Seamless integration with existing CrewAI flow patterns
+
+The persistence system's architecture emphasizes technical precision and customization options, allowing developers to maintain full control over state management while benefiting from built-in reliability features.
+
## Flow Control
### Conditional Logic: `or`
diff --git a/docs/concepts/knowledge.mdx b/docs/concepts/knowledge.mdx
index e4e40ba3e..b5827551a 100644
--- a/docs/concepts/knowledge.mdx
+++ b/docs/concepts/knowledge.mdx
@@ -91,7 +91,7 @@ result = crew.kickoff(inputs={"question": "What city does John live in and how o
```
-Here's another example with the `CrewDoclingSource`. The CrewDoclingSource is actually quite versatile and can handle multiple file formats including TXT, PDF, DOCX, HTML, and more.
+Here's another example with the `CrewDoclingSource`. The CrewDoclingSource is actually quite versatile and can handle multiple file formats including MD, PDF, DOCX, HTML, and more.
You need to install `docling` for the following example to work: `uv add docling`
@@ -152,10 +152,10 @@ Here are examples of how to use different types of knowledge sources:
### Text File Knowledge Source
```python
-from crewai.knowledge.source.crew_docling_source import CrewDoclingSource
+from crewai.knowledge.source.text_file_knowledge_source import TextFileKnowledgeSource
# Create a text file knowledge source
-text_source = CrewDoclingSource(
+text_source = TextFileKnowledgeSource(
file_paths=["document.txt", "another.txt"]
)
@@ -324,6 +324,13 @@ agent = Agent(
verbose=True,
allow_delegation=False,
llm=gemini_llm,
+ embedder={
+ "provider": "google",
+ "config": {
+ "model": "models/text-embedding-004",
+ "api_key": GEMINI_API_KEY,
+ }
+ }
)
task = Task(
diff --git a/docs/concepts/llms.mdx b/docs/concepts/llms.mdx
index 851e93085..117face04 100644
--- a/docs/concepts/llms.mdx
+++ b/docs/concepts/llms.mdx
@@ -38,6 +38,7 @@ Here's a detailed breakdown of supported models and their capabilities, you can
| GPT-4 | 8,192 tokens | High-accuracy tasks, complex reasoning |
| GPT-4 Turbo | 128,000 tokens | Long-form content, document analysis |
| GPT-4o & GPT-4o-mini | 128,000 tokens | Cost-effective large context processing |
+ | o3-mini | 200,000 tokens | Fast reasoning, complex reasoning |
1 token β 4 characters in English. For example, 8,192 tokens β 32,768 characters or about 6,000 words.
@@ -162,7 +163,8 @@ Here's a detailed breakdown of supported models and their capabilities, you can
| Provider | Context Window | Key Features |
|----------|---------------|--------------|
- | Deepseek Chat | 128,000 tokens | Specialized in technical discussions |
+ | Deepseek Chat | 64,000 tokens | Specialized in technical discussions |
+ | Deepseek R1 | 64,000 tokens | Affordable reasoning model |
| Claude 3 | Up to 200K tokens | Strong reasoning, code understanding |
| Gemma Series | 8,192 tokens | Efficient, smaller-scale tasks |
@@ -243,6 +245,9 @@ There are three ways to configure LLMs in CrewAI. Choose the method that best fi
# llm: bedrock/amazon.titan-text-express-v1
# llm: bedrock/meta.llama2-70b-chat-v1
+ # Amazon SageMaker Models - Enterprise-grade
+ # llm: sagemaker/
+
# Mistral Models - Open source alternative
# llm: mistral/mistral-large-latest
# llm: mistral/mistral-medium-latest
@@ -293,6 +298,10 @@ There are three ways to configure LLMs in CrewAI. Choose the method that best fi
# llm: sambanova/Meta-Llama-3.1-8B-Instruct
# llm: sambanova/BioMistral-7B
# llm: sambanova/Falcon-180B
+
+ # Open Router Models - Affordable reasoning
+ # llm: openrouter/deepseek/deepseek-r1
+ # llm: openrouter/deepseek/deepseek-chat
```
@@ -454,19 +463,36 @@ Learn how to get the most out of your LLM configuration:
```python Code
- # Option 1. Gemini accessed with an API key.
+ # Option 1: Gemini accessed with an API key.
# https://ai.google.dev/gemini-api/docs/api-key
GEMINI_API_KEY=
- # Option 2. Vertex AI IAM credentials for Gemini, Anthropic, and anything in the Model Garden.
+ # Option 2: Vertex AI IAM credentials for Gemini, Anthropic, and Model Garden.
# https://cloud.google.com/vertex-ai/generative-ai/docs/overview
```
+ Get credentials:
+ ```python Code
+ import json
+
+ file_path = 'path/to/vertex_ai_service_account.json'
+
+ # Load the JSON file
+ with open(file_path, 'r') as file:
+ vertex_credentials = json.load(file)
+
+ # Convert the credentials to a JSON string
+ vertex_credentials_json = json.dumps(vertex_credentials)
+ ```
+
Example usage:
```python Code
+ from crewai import LLM
+
llm = LLM(
model="gemini/gemini-1.5-pro-latest",
- temperature=0.7
+ temperature=0.7,
+ vertex_credentials=vertex_credentials_json
)
```
@@ -506,6 +532,21 @@ Learn how to get the most out of your LLM configuration:
)
```
+
+
+ ```python Code
+ AWS_ACCESS_KEY_ID=
+ AWS_SECRET_ACCESS_KEY=
+ AWS_DEFAULT_REGION=
+ ```
+
+ Example usage:
+ ```python Code
+ llm = LLM(
+ model="sagemaker/"
+ )
+ ```
+
```python Code
@@ -662,8 +703,53 @@ Learn how to get the most out of your LLM configuration:
- Support for long context windows
+
+
+ ```python Code
+ OPENROUTER_API_KEY=
+ ```
+
+ Example usage:
+ ```python Code
+ llm = LLM(
+ model="openrouter/deepseek/deepseek-r1",
+ base_url="https://openrouter.ai/api/v1",
+ api_key=OPENROUTER_API_KEY
+ )
+ ```
+
+
+ Open Router models:
+ - openrouter/deepseek/deepseek-r1
+ - openrouter/deepseek/deepseek-chat
+
+
+## Structured LLM Calls
+
+CrewAI supports structured responses from LLM calls by allowing you to define a `response_format` using a Pydantic model. This enables the framework to automatically parse and validate the output, making it easier to integrate the response into your application without manual post-processing.
+
+For example, you can define a Pydantic model to represent the expected response structure and pass it as the `response_format` when instantiating the LLM. The model will then be used to convert the LLM output into a structured Python object.
+
+```python Code
+from crewai import LLM
+
+class Dog(BaseModel):
+ name: str
+ age: int
+ breed: str
+
+
+llm = LLM(model="gpt-4o", response_format=Dog)
+
+response = llm.call(
+ "Analyze the following messages and return the name, age, and breed. "
+ "Meet Kona! She is 3 years old and is a black german shepherd."
+)
+print(response)
+```
+
## Common Issues and Solutions
diff --git a/docs/concepts/memory.mdx b/docs/concepts/memory.mdx
index 751b6dd2e..ae65db290 100644
--- a/docs/concepts/memory.mdx
+++ b/docs/concepts/memory.mdx
@@ -58,41 +58,107 @@ my_crew = Crew(
### Example: Use Custom Memory Instances e.g FAISS as the VectorDB
```python Code
-from crewai import Crew, Agent, Task, Process
+from crewai import Crew, Process
+from crewai.memory import LongTermMemory, ShortTermMemory, EntityMemory
+from crewai.memory.storage import LTMSQLiteStorage, RAGStorage
+from typing import List, Optional
# Assemble your crew with memory capabilities
-my_crew = Crew(
- agents=[...],
- tasks=[...],
- process="Process.sequential",
- memory=True,
- long_term_memory=EnhanceLongTermMemory(
+my_crew: Crew = Crew(
+ agents = [...],
+ tasks = [...],
+ process = Process.sequential,
+ memory = True,
+ # Long-term memory for persistent storage across sessions
+ long_term_memory = LongTermMemory(
storage=LTMSQLiteStorage(
- db_path="/my_data_dir/my_crew1/long_term_memory_storage.db"
+ db_path="/my_crew1/long_term_memory_storage.db"
)
),
- short_term_memory=EnhanceShortTermMemory(
- storage=CustomRAGStorage(
- crew_name="my_crew",
- storage_type="short_term",
- data_dir="//my_data_dir",
- model=embedder["model"],
- dimension=embedder["dimension"],
+ # Short-term memory for current context using RAG
+ short_term_memory = ShortTermMemory(
+ storage = RAGStorage(
+ embedder_config={
+ "provider": "openai",
+ "config": {
+ "model": 'text-embedding-3-small'
+ }
+ },
+ type="short_term",
+ path="/my_crew1/"
+ )
),
),
- entity_memory=EnhanceEntityMemory(
- storage=CustomRAGStorage(
- crew_name="my_crew",
- storage_type="entities",
- data_dir="//my_data_dir",
- model=embedder["model"],
- dimension=embedder["dimension"],
- ),
+ # Entity memory for tracking key information about entities
+ entity_memory = EntityMemory(
+ storage=RAGStorage(
+ embedder_config={
+ "provider": "openai",
+ "config": {
+ "model": 'text-embedding-3-small'
+ }
+ },
+ type="short_term",
+ path="/my_crew1/"
+ )
),
verbose=True,
)
```
+## Security Considerations
+
+When configuring memory storage:
+- Use environment variables for storage paths (e.g., `CREWAI_STORAGE_DIR`)
+- Never hardcode sensitive information like database credentials
+- Consider access permissions for storage directories
+- Use relative paths when possible to maintain portability
+
+Example using environment variables:
+```python
+import os
+from crewai import Crew
+from crewai.memory import LongTermMemory
+from crewai.memory.storage import LTMSQLiteStorage
+
+# Configure storage path using environment variable
+storage_path = os.getenv("CREWAI_STORAGE_DIR", "./storage")
+crew = Crew(
+ memory=True,
+ long_term_memory=LongTermMemory(
+ storage=LTMSQLiteStorage(
+ db_path="{storage_path}/memory.db".format(storage_path=storage_path)
+ )
+ )
+)
+```
+
+## Configuration Examples
+
+### Basic Memory Configuration
+```python
+from crewai import Crew
+from crewai.memory import LongTermMemory
+
+# Simple memory configuration
+crew = Crew(memory=True) # Uses default storage locations
+```
+
+### Custom Storage Configuration
+```python
+from crewai import Crew
+from crewai.memory import LongTermMemory
+from crewai.memory.storage import LTMSQLiteStorage
+
+# Configure custom storage paths
+crew = Crew(
+ memory=True,
+ long_term_memory=LongTermMemory(
+ storage=LTMSQLiteStorage(db_path="./memory.db")
+ )
+)
+```
+
## Integrating Mem0 for Enhanced User Memory
[Mem0](https://mem0.ai/) is a self-improving memory layer for LLM applications, enabling personalized AI experiences.
@@ -185,7 +251,12 @@ my_crew = Crew(
process=Process.sequential,
memory=True,
verbose=True,
- embedder=OpenAIEmbeddingFunction(api_key=os.getenv("OPENAI_API_KEY"), model_name="text-embedding-3-small"),
+ embedder={
+ "provider": "openai",
+ "config": {
+ "model": 'text-embedding-3-small'
+ }
+ }
)
```
@@ -211,6 +282,19 @@ my_crew = Crew(
### Using Google AI embeddings
+#### Prerequisites
+Before using Google AI embeddings, ensure you have:
+- Access to the Gemini API
+- The necessary API keys and permissions
+
+You will need to update your *pyproject.toml* dependencies:
+```YAML
+dependencies = [
+ "google-generativeai>=0.8.4", #main version in January/2025 - crewai v.0.100.0 and crewai-tools 0.33.0
+ "crewai[tools]>=0.100.0,<1.0.0"
+]
+```
+
```python Code
from crewai import Crew, Agent, Task, Process
@@ -224,7 +308,7 @@ my_crew = Crew(
"provider": "google",
"config": {
"api_key": "",
- "model_name": ""
+ "model": ""
}
}
)
@@ -242,13 +326,15 @@ my_crew = Crew(
process=Process.sequential,
memory=True,
verbose=True,
- embedder=OpenAIEmbeddingFunction(
- api_key="YOUR_API_KEY",
- api_base="YOUR_API_BASE_PATH",
- api_type="azure",
- api_version="YOUR_API_VERSION",
- model_name="text-embedding-3-small"
- )
+ embedder={
+ "provider": "openai",
+ "config": {
+ "api_key": "YOUR_API_KEY",
+ "api_base": "YOUR_API_BASE_PATH",
+ "api_version": "YOUR_API_VERSION",
+ "model_name": 'text-embedding-3-small'
+ }
+ }
)
```
@@ -264,12 +350,15 @@ my_crew = Crew(
process=Process.sequential,
memory=True,
verbose=True,
- embedder=GoogleVertexEmbeddingFunction(
- project_id="YOUR_PROJECT_ID",
- region="YOUR_REGION",
- api_key="YOUR_API_KEY",
- model_name="textembedding-gecko"
- )
+ embedder={
+ "provider": "vertexai",
+ "config": {
+ "project_id"="YOUR_PROJECT_ID",
+ "region"="YOUR_REGION",
+ "api_key"="YOUR_API_KEY",
+ "model_name"="textembedding-gecko"
+ }
+ }
)
```
@@ -288,7 +377,7 @@ my_crew = Crew(
"provider": "cohere",
"config": {
"api_key": "YOUR_API_KEY",
- "model_name": ""
+ "model": ""
}
}
)
@@ -308,7 +397,7 @@ my_crew = Crew(
"provider": "voyageai",
"config": {
"api_key": "YOUR_API_KEY",
- "model_name": ""
+ "model": ""
}
}
)
@@ -358,6 +447,65 @@ my_crew = Crew(
)
```
+### Using Amazon Bedrock embeddings
+
+```python Code
+# Note: Ensure you have installed `boto3` for Bedrock embeddings to work.
+
+import os
+import boto3
+from crewai import Crew, Agent, Task, Process
+
+boto3_session = boto3.Session(
+ region_name=os.environ.get("AWS_REGION_NAME"),
+ aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
+ aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY")
+)
+
+my_crew = Crew(
+ agents=[...],
+ tasks=[...],
+ process=Process.sequential,
+ memory=True,
+ embedder={
+ "provider": "bedrock",
+ "config":{
+ "session": boto3_session,
+ "model": "amazon.titan-embed-text-v2:0",
+ "vector_dimension": 1024
+ }
+ }
+ verbose=True
+)
+```
+
+### Adding Custom Embedding Function
+
+```python Code
+from crewai import Crew, Agent, Task, Process
+from chromadb import Documents, EmbeddingFunction, Embeddings
+
+# Create a custom embedding function
+class CustomEmbedder(EmbeddingFunction):
+ def __call__(self, input: Documents) -> Embeddings:
+ # generate embeddings
+ return [1, 2, 3] # this is a dummy embedding
+
+my_crew = Crew(
+ agents=[...],
+ tasks=[...],
+ process=Process.sequential,
+ memory=True,
+ verbose=True,
+ embedder={
+ "provider": "custom",
+ "config": {
+ "embedder": CustomEmbedder()
+ }
+ }
+)
+```
+
### Resetting Memory
```shell
diff --git a/docs/concepts/planning.mdx b/docs/concepts/planning.mdx
index 79e747425..c92d64ebe 100644
--- a/docs/concepts/planning.mdx
+++ b/docs/concepts/planning.mdx
@@ -81,8 +81,8 @@ my_crew.kickoff()
3. **Collect Data:**
- - Search for the latest papers, articles, and reports published in 2023 and early 2024.
- - Use keywords like "Large Language Models 2024", "AI LLM advancements", "AI ethics 2024", etc.
+ - Search for the latest papers, articles, and reports published in 2024 and early 2025.
+ - Use keywords like "Large Language Models 2025", "AI LLM advancements", "AI ethics 2025", etc.
4. **Analyze Findings:**
diff --git a/docs/concepts/tasks.mdx b/docs/concepts/tasks.mdx
index 6ffd95e19..120f5d547 100644
--- a/docs/concepts/tasks.mdx
+++ b/docs/concepts/tasks.mdx
@@ -33,11 +33,12 @@ crew = Crew(
| :------------------------------- | :---------------- | :---------------------------- | :------------------------------------------------------------------------------------------------------------------- |
| **Description** | `description` | `str` | A clear, concise statement of what the task entails. |
| **Expected Output** | `expected_output` | `str` | A detailed description of what the task's completion looks like. |
-| **Name** _(optional)_ | `name` | `Optional[str]` | A name identifier for the task. |
-| **Agent** _(optional)_ | `agent` | `Optional[BaseAgent]` | The agent responsible for executing the task. |
-| **Tools** _(optional)_ | `tools` | `List[BaseTool]` | The tools/resources the agent is limited to use for this task. |
+| **Name** _(optional)_ | `name` | `Optional[str]` | A name identifier for the task. |
+| **Agent** _(optional)_ | `agent` | `Optional[BaseAgent]` | The agent responsible for executing the task. |
+| **Tools** _(optional)_ | `tools` | `List[BaseTool]` | The tools/resources the agent is limited to use for this task. |
| **Context** _(optional)_ | `context` | `Optional[List["Task"]]` | Other tasks whose outputs will be used as context for this task. |
| **Async Execution** _(optional)_ | `async_execution` | `Optional[bool]` | Whether the task should be executed asynchronously. Defaults to False. |
+| **Human Input** _(optional)_ | `human_input` | `Optional[bool]` | Whether the task should have a human review the final answer of the agent. 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. |
| **Output JSON** _(optional)_ | `output_json` | `Optional[Type[BaseModel]]` | A Pydantic model to structure the JSON output. |
@@ -68,7 +69,7 @@ research_task:
description: >
Conduct a thorough research about {topic}
Make sure you find any interesting and relevant information given
- the current year is 2024.
+ the current year is 2025.
expected_output: >
A list with 10 bullet points of the most relevant information about {topic}
agent: researcher
@@ -154,7 +155,7 @@ research_task = Task(
description="""
Conduct a thorough research about AI Agents.
Make sure you find any interesting and relevant information given
- the current year is 2024.
+ the current year is 2025.
""",
expected_output="""
A list with 10 bullet points of the most relevant information about AI Agents
@@ -267,7 +268,7 @@ analysis_task = Task(
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
-efeedback to agents when their output doesn't meet specific criteria.
+feedback to agents when their output doesn't meet specific criteria.
### Using Task Guardrails
diff --git a/docs/how-to/human-input-on-execution.mdx b/docs/how-to/human-input-on-execution.mdx
index de8c8a0a8..bf243981d 100644
--- a/docs/how-to/human-input-on-execution.mdx
+++ b/docs/how-to/human-input-on-execution.mdx
@@ -60,12 +60,12 @@ writer = Agent(
# Create tasks for your agents
task1 = Task(
description=(
- "Conduct a comprehensive analysis of the latest advancements in AI in 2024. "
+ "Conduct a comprehensive analysis of the latest advancements in AI in 2025. "
"Identify key trends, breakthrough technologies, and potential industry impacts. "
"Compile your findings in a detailed report. "
"Make sure to check with a human if the draft is good before finalizing your answer."
),
- expected_output='A comprehensive full report on the latest AI advancements in 2024, leave nothing out',
+ expected_output='A comprehensive full report on the latest AI advancements in 2025, leave nothing out',
agent=researcher,
human_input=True
)
@@ -76,7 +76,7 @@ task2 = Task(
"Your post should be informative yet accessible, catering to a tech-savvy audience. "
"Aim for a narrative that captures the essence of these breakthroughs and their implications for the future."
),
- expected_output='A compelling 3 paragraphs blog post formatted as markdown about the latest AI advancements in 2024',
+ expected_output='A compelling 3 paragraphs blog post formatted as markdown about the latest AI advancements in 2025',
agent=writer,
human_input=True
)
diff --git a/docs/how-to/langfuse-observability.mdx b/docs/how-to/langfuse-observability.mdx
new file mode 100644
index 000000000..c0feb6d87
--- /dev/null
+++ b/docs/how-to/langfuse-observability.mdx
@@ -0,0 +1,98 @@
+---
+title: Agent Monitoring with Langfuse
+description: Learn how to integrate Langfuse with CrewAI via OpenTelemetry using OpenLit
+icon: magnifying-glass-chart
+---
+
+# Integrate Langfuse with CrewAI
+
+This notebook demonstrates how to integrate **Langfuse** with **CrewAI** using OpenTelemetry via the **OpenLit** SDK. By the end of this notebook, you will be able to trace your CrewAI applications with Langfuse for improved observability and debugging.
+
+> **What is Langfuse?** [Langfuse](https://langfuse.com) is an open-source LLM engineering platform. It provides tracing and monitoring capabilities for LLM applications, helping developers debug, analyze, and optimize their AI systems. Langfuse integrates with various tools and frameworks via native integrations, OpenTelemetry, and APIs/SDKs.
+
+## Get Started
+
+We'll walk through a simple example of using CrewAI and integrating it with Langfuse via OpenTelemetry using OpenLit.
+
+### Step 1: Install Dependencies
+
+
+```python
+%pip install langfuse openlit crewai crewai_tools
+```
+
+### Step 2: Set Up Environment Variables
+
+Set your Langfuse API keys and configure OpenTelemetry export settings to send traces to Langfuse. Please refer to the [Langfuse OpenTelemetry Docs](https://langfuse.com/docs/opentelemetry/get-started) for more information on the Langfuse OpenTelemetry endpoint `/api/public/otel` and authentication.
+
+
+```python
+import os
+import base64
+
+LANGFUSE_PUBLIC_KEY="pk-lf-..."
+LANGFUSE_SECRET_KEY="sk-lf-..."
+LANGFUSE_AUTH=base64.b64encode(f"{LANGFUSE_PUBLIC_KEY}:{LANGFUSE_SECRET_KEY}".encode()).decode()
+
+os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = "https://cloud.langfuse.com/api/public/otel" # EU data region
+# os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = "https://us.cloud.langfuse.com/api/public/otel" # US data region
+os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"Authorization=Basic {LANGFUSE_AUTH}"
+
+# your openai key
+os.environ["OPENAI_API_KEY"] = "sk-..."
+```
+
+### Step 3: Initialize OpenLit
+
+Initialize the OpenLit OpenTelemetry instrumentation SDK to start capturing OpenTelemetry traces.
+
+
+```python
+import openlit
+
+openlit.init()
+```
+
+### Step 4: Create a Simple CrewAI Application
+
+We'll create a simple CrewAI application where multiple agents collaborate to answer a user's question.
+
+
+```python
+from crewai import Agent, Task, Crew
+
+from crewai_tools import (
+ WebsiteSearchTool
+)
+
+web_rag_tool = WebsiteSearchTool()
+
+writer = Agent(
+ role="Writer",
+ goal="You make math engaging and understandable for young children through poetry",
+ backstory="You're an expert in writing haikus but you know nothing of math.",
+ tools=[web_rag_tool],
+ )
+
+task = Task(description=("What is {multiplication}?"),
+ expected_output=("Compose a haiku that includes the answer."),
+ agent=writer)
+
+crew = Crew(
+ agents=[writer],
+ tasks=[task],
+ share_crew=False
+)
+```
+
+### Step 5: See Traces in Langfuse
+
+After running the agent, you can view the traces generated by your CrewAI application in [Langfuse](https://cloud.langfuse.com). You should see detailed steps of the LLM interactions, which can help you debug and optimize your AI agent.
+
+
+
+_[Public example trace in Langfuse](https://cloud.langfuse.com/project/cloramnkj0002jz088vzn1ja4/traces/e2cf380ffc8d47d28da98f136140642b?timestamp=2025-02-05T15%3A12%3A02.717Z&observation=3b32338ee6a5d9af)_
+
+## References
+
+- [Langfuse OpenTelemetry Docs](https://langfuse.com/docs/opentelemetry/get-started)
diff --git a/docs/how-to/mlflow-observability.mdx b/docs/how-to/mlflow-observability.mdx
new file mode 100644
index 000000000..f0c4a9ec4
--- /dev/null
+++ b/docs/how-to/mlflow-observability.mdx
@@ -0,0 +1,206 @@
+---
+title: Agent Monitoring with MLflow
+description: Quickly start monitoring your Agents with MLflow.
+icon: bars-staggered
+---
+
+# MLflow Overview
+
+[MLflow](https://mlflow.org/) is an open-source platform to assist machine learning practitioners and teams in handling the complexities of the machine learning process.
+
+It provides a tracing feature that enhances LLM observability in your Generative AI applications by capturing detailed information about the execution of your applicationβs services.
+Tracing provides a way to record the inputs, outputs, and metadata associated with each intermediate step of a request, enabling you to easily pinpoint the source of bugs and unexpected behaviors.
+
+
+
+### Features
+
+- **Tracing Dashboard**: Monitor activities of your crewAI agents with detailed dashboards that include inputs, outputs and metadata of spans.
+- **Automated Tracing**: A fully automated integration with crewAI, which can be enabled by running `mlflow.crewai.autolog()`.
+- **Manual Trace Instrumentation with minor efforts**: Customize trace instrumentation through MLflow's high-level fluent APIs such as decorators, function wrappers and context managers.
+- **OpenTelemetry Compatibility**: MLflow Tracing supports exporting traces to an OpenTelemetry Collector, which can then be used to export traces to various backends such as Jaeger, Zipkin, and AWS X-Ray.
+- **Package and Deploy Agents**: Package and deploy your crewAI agents to an inference server with a variety of deployment targets.
+- **Securely Host LLMs**: Host multiple LLM from various providers in one unified endpoint through MFflow gateway.
+- **Evaluation**: Evaluate your crewAI agents with a wide range of metrics using a convenient API `mlflow.evaluate()`.
+
+## Setup Instructions
+
+
+
+ ```shell
+ # The crewAI integration is available in mlflow>=2.19.0
+ pip install mlflow
+ ```
+
+
+ ```shell
+ # This process is optional, but it is recommended to use MLflow tracking server for better visualization and broader features.
+ mlflow server
+ ```
+
+
+ Add the following two lines to your application code:
+
+ ```python
+ import mlflow
+
+ mlflow.crewai.autolog()
+
+ # Optional: Set a tracking URI and an experiment name if you have a tracking server
+ mlflow.set_tracking_uri("http://localhost:5000")
+ mlflow.set_experiment("CrewAI")
+ ```
+
+ Example Usage for tracing CrewAI Agents:
+
+ ```python
+ from crewai import Agent, Crew, Task
+ from crewai.knowledge.source.string_knowledge_source import StringKnowledgeSource
+ from crewai_tools import SerperDevTool, WebsiteSearchTool
+
+ from textwrap import dedent
+
+ content = "Users name is John. He is 30 years old and lives in San Francisco."
+ string_source = StringKnowledgeSource(
+ content=content, metadata={"preference": "personal"}
+ )
+
+ search_tool = WebsiteSearchTool()
+
+
+ class TripAgents:
+ def city_selection_agent(self):
+ return Agent(
+ role="City Selection Expert",
+ goal="Select the best city based on weather, season, and prices",
+ backstory="An expert in analyzing travel data to pick ideal destinations",
+ tools=[
+ search_tool,
+ ],
+ verbose=True,
+ )
+
+ def local_expert(self):
+ return Agent(
+ role="Local Expert at this city",
+ goal="Provide the BEST insights about the selected city",
+ backstory="""A knowledgeable local guide with extensive information
+ about the city, it's attractions and customs""",
+ tools=[search_tool],
+ verbose=True,
+ )
+
+
+ class TripTasks:
+ def identify_task(self, agent, origin, cities, interests, range):
+ return Task(
+ description=dedent(
+ f"""
+ Analyze and select the best city for the trip based
+ on specific criteria such as weather patterns, seasonal
+ events, and travel costs. This task involves comparing
+ multiple cities, considering factors like current weather
+ conditions, upcoming cultural or seasonal events, and
+ overall travel expenses.
+ Your final answer must be a detailed
+ report on the chosen city, and everything you found out
+ about it, including the actual flight costs, weather
+ forecast and attractions.
+
+ Traveling from: {origin}
+ City Options: {cities}
+ Trip Date: {range}
+ Traveler Interests: {interests}
+ """
+ ),
+ agent=agent,
+ expected_output="Detailed report on the chosen city including flight costs, weather forecast, and attractions",
+ )
+
+ def gather_task(self, agent, origin, interests, range):
+ return Task(
+ description=dedent(
+ f"""
+ As a local expert on this city you must compile an
+ in-depth guide for someone traveling there and wanting
+ to have THE BEST trip ever!
+ Gather information about key attractions, local customs,
+ special events, and daily activity recommendations.
+ Find the best spots to go to, the kind of place only a
+ local would know.
+ This guide should provide a thorough overview of what
+ the city has to offer, including hidden gems, cultural
+ hotspots, must-visit landmarks, weather forecasts, and
+ high level costs.
+ The final answer must be a comprehensive city guide,
+ rich in cultural insights and practical tips,
+ tailored to enhance the travel experience.
+
+ Trip Date: {range}
+ Traveling from: {origin}
+ Traveler Interests: {interests}
+ """
+ ),
+ agent=agent,
+ expected_output="Comprehensive city guide including hidden gems, cultural hotspots, and practical travel tips",
+ )
+
+
+ class TripCrew:
+ def __init__(self, origin, cities, date_range, interests):
+ self.cities = cities
+ self.origin = origin
+ self.interests = interests
+ self.date_range = date_range
+
+ def run(self):
+ agents = TripAgents()
+ tasks = TripTasks()
+
+ city_selector_agent = agents.city_selection_agent()
+ local_expert_agent = agents.local_expert()
+
+ identify_task = tasks.identify_task(
+ city_selector_agent,
+ self.origin,
+ self.cities,
+ self.interests,
+ self.date_range,
+ )
+ gather_task = tasks.gather_task(
+ local_expert_agent, self.origin, self.interests, self.date_range
+ )
+
+ crew = Crew(
+ agents=[city_selector_agent, local_expert_agent],
+ tasks=[identify_task, gather_task],
+ verbose=True,
+ memory=True,
+ knowledge={
+ "sources": [string_source],
+ "metadata": {"preference": "personal"},
+ },
+ )
+
+ result = crew.kickoff()
+ return result
+
+
+ trip_crew = TripCrew("California", "Tokyo", "Dec 12 - Dec 20", "sports")
+ result = trip_crew.run()
+
+ print(result)
+ ```
+ Refer to [MLflow Tracing Documentation](https://mlflow.org/docs/latest/llms/tracing/index.html) for more configurations and use cases.
+
+
+ Now traces for your crewAI agents are captured by MLflow.
+ Let's visit MLflow tracking server to view the traces and get insights into your Agents.
+
+ Open `127.0.0.1:5000` on your browser to visit MLflow tracking server.
+
+
+
+
+
+
diff --git a/docs/how-to/multimodal-agents.mdx b/docs/how-to/multimodal-agents.mdx
index b3bebbc81..d59b6da5a 100644
--- a/docs/how-to/multimodal-agents.mdx
+++ b/docs/how-to/multimodal-agents.mdx
@@ -45,6 +45,7 @@ image_analyst = Agent(
# Create a task for image analysis
task = Task(
description="Analyze the product image at https://example.com/product.jpg and provide a detailed description",
+ expected_output="A detailed description of the product image",
agent=image_analyst
)
@@ -81,6 +82,7 @@ inspection_task = Task(
3. Compliance with standards
Provide a detailed report highlighting any issues found.
""",
+ expected_output="A detailed report highlighting any issues found",
agent=expert_analyst
)
diff --git a/docs/how-to/portkey-observability-and-guardrails.mdx b/docs/how-to/portkey-observability-and-guardrails.mdx
deleted file mode 100644
index f4f7a696e..000000000
--- a/docs/how-to/portkey-observability-and-guardrails.mdx
+++ /dev/null
@@ -1,211 +0,0 @@
-# Portkey Integration with CrewAI
-
-
-
-[Portkey](https://portkey.ai/?utm_source=crewai&utm_medium=crewai&utm_campaign=crewai) is a 2-line upgrade to make your CrewAI agents reliable, cost-efficient, and fast.
-
-Portkey adds 4 core production capabilities to any CrewAI agent:
-1. Routing to **200+ LLMs**
-2. Making each LLM call more robust
-3. Full-stack tracing & cost, performance analytics
-4. Real-time guardrails to enforce behavior
-
-
-
-
-
-## Getting Started
-
-1. **Install Required Packages:**
-
-```bash
-pip install -qU crewai portkey-ai
-```
-
-2. **Configure the LLM Client:**
-
-To build CrewAI Agents with Portkey, you'll need two keys:
-- **Portkey API Key**: Sign up on the [Portkey app](https://app.portkey.ai/?utm_source=crewai&utm_medium=crewai&utm_campaign=crewai) and copy your API key
-- **Virtual Key**: Virtual Keys securely manage your LLM API keys in one place. Store your LLM provider API keys securely in Portkey's vault
-
-```python
-from crewai import LLM
-from portkey_ai import createHeaders, PORTKEY_GATEWAY_URL
-
-gpt_llm = LLM(
- model="gpt-4",
- base_url=PORTKEY_GATEWAY_URL,
- api_key="dummy", # We are using Virtual key
- extra_headers=createHeaders(
- api_key="YOUR_PORTKEY_API_KEY",
- virtual_key="YOUR_VIRTUAL_KEY", # Enter your Virtual key from Portkey
- )
-)
-```
-
-3. **Create and Run Your First Agent:**
-
-```python
-from crewai import Agent, Task, Crew
-
-# Define your agents with roles and goals
-coder = Agent(
- role='Software developer',
- goal='Write clear, concise code on demand',
- backstory='An expert coder with a keen eye for software trends.',
- llm=gpt_llm
-)
-
-# Create tasks for your agents
-task1 = Task(
- description="Define the HTML for making a simple website with heading- Hello World! Portkey is working!",
- expected_output="A clear and concise HTML code",
- agent=coder
-)
-
-# Instantiate your crew
-crew = Crew(
- agents=[coder],
- tasks=[task1],
-)
-
-result = crew.kickoff()
-print(result)
-```
-
-
-## Key Features
-
-| Feature | Description |
-|---------|-------------|
-| π Multi-LLM Support | Access OpenAI, Anthropic, Gemini, Azure, and 250+ providers through a unified interface |
-| π‘οΈ Production Reliability | Implement retries, timeouts, load balancing, and fallbacks |
-| π Advanced Observability | Track 40+ metrics including costs, tokens, latency, and custom metadata |
-| π Comprehensive Logging | Debug with detailed execution traces and function call logs |
-| π§ Security Controls | Set budget limits and implement role-based access control |
-| π Performance Analytics | Capture and analyze feedback for continuous improvement |
-| πΎ Intelligent Caching | Reduce costs and latency with semantic or simple caching |
-
-
-## Production Features with Portkey Configs
-
-All features mentioned below are through Portkey's Config system. Portkey's Config system allows you to define routing strategies using simple JSON objects in your LLM API calls. You can create and manage Configs directly in your code or through the Portkey Dashboard. Each Config has a unique ID for easy reference.
-
-
-
-
-
-
-### 1. Use 250+ LLMs
-Access various LLMs like Anthropic, Gemini, Mistral, Azure OpenAI, and more with minimal code changes. Switch between providers or use them together seamlessly. [Learn more about Universal API](https://portkey.ai/docs/product/ai-gateway/universal-api)
-
-
-Easily switch between different LLM providers:
-
-```python
-# Anthropic Configuration
-anthropic_llm = LLM(
- model="claude-3-5-sonnet-latest",
- base_url=PORTKEY_GATEWAY_URL,
- api_key="dummy",
- extra_headers=createHeaders(
- api_key="YOUR_PORTKEY_API_KEY",
- virtual_key="YOUR_ANTHROPIC_VIRTUAL_KEY", #You don't need provider when using Virtual keys
- trace_id="anthropic_agent"
- )
-)
-
-# Azure OpenAI Configuration
-azure_llm = LLM(
- model="gpt-4",
- base_url=PORTKEY_GATEWAY_URL,
- api_key="dummy",
- extra_headers=createHeaders(
- api_key="YOUR_PORTKEY_API_KEY",
- virtual_key="YOUR_AZURE_VIRTUAL_KEY", #You don't need provider when using Virtual keys
- trace_id="azure_agent"
- )
-)
-```
-
-
-### 2. Caching
-Improve response times and reduce costs with two powerful caching modes:
-- **Simple Cache**: Perfect for exact matches
-- **Semantic Cache**: Matches responses for requests that are semantically similar
-[Learn more about Caching](https://portkey.ai/docs/product/ai-gateway/cache-simple-and-semantic)
-
-```py
-config = {
- "cache": {
- "mode": "semantic", # or "simple" for exact matching
- }
-}
-```
-
-### 3. Production Reliability
-Portkey provides comprehensive reliability features:
-- **Automatic Retries**: Handle temporary failures gracefully
-- **Request Timeouts**: Prevent hanging operations
-- **Conditional Routing**: Route requests based on specific conditions
-- **Fallbacks**: Set up automatic provider failovers
-- **Load Balancing**: Distribute requests efficiently
-
-[Learn more about Reliability Features](https://portkey.ai/docs/product/ai-gateway/)
-
-
-
-### 4. Metrics
-
-Agent runs are complex. Portkey automatically logs **40+ comprehensive metrics** for your AI agents, including cost, tokens used, latency, etc. Whether you need a broad overview or granular insights into your agent runs, Portkey's customizable filters provide the metrics you need.
-
-
-- Cost per agent interaction
-- Response times and latency
-- Token usage and efficiency
-- Success/failure rates
-- Cache hit rates
-
-
-
-### 5. Detailed Logging
-Logs are essential for understanding agent behavior, diagnosing issues, and improving performance. They provide a detailed record of agent activities and tool use, which is crucial for debugging and optimizing processes.
-
-
-Access a dedicated section to view records of agent executions, including parameters, outcomes, function calls, and errors. Filter logs based on multiple parameters such as trace ID, model, tokens used, and metadata.
-
-
- Traces
-
-
-
-
- Logs
-
-
-
-### 6. Enterprise Security Features
-- Set budget limit and rate limts per Virtual Key (disposable API keys)
-- Implement role-based access control
-- Track system changes with audit logs
-- Configure data retention policies
-
-
-
-For detailed information on creating and managing Configs, visit the [Portkey documentation](https://docs.portkey.ai/product/ai-gateway/configs).
-
-## Resources
-
-- [π Portkey Documentation](https://docs.portkey.ai)
-- [π Portkey Dashboard](https://app.portkey.ai/?utm_source=crewai&utm_medium=crewai&utm_campaign=crewai)
-- [π¦ Twitter](https://twitter.com/portkeyai)
-- [π¬ Discord Community](https://discord.gg/DD7vgKK299)
-
-
-
-
-
-
-
-
-
diff --git a/docs/how-to/portkey-observability.mdx b/docs/how-to/portkey-observability.mdx
index 4002323a5..d071b49fc 100644
--- a/docs/how-to/portkey-observability.mdx
+++ b/docs/how-to/portkey-observability.mdx
@@ -1,5 +1,5 @@
---
-title: Portkey Observability and Guardrails
+title: Agent Monitoring with Portkey
description: How to use Portkey with CrewAI
icon: key
---
diff --git a/docs/images/mlflow-tracing.gif b/docs/images/mlflow-tracing.gif
new file mode 100644
index 000000000..e968c0924
Binary files /dev/null and b/docs/images/mlflow-tracing.gif differ
diff --git a/docs/images/mlflow1.png b/docs/images/mlflow1.png
new file mode 100644
index 000000000..b2c6f3b80
Binary files /dev/null and b/docs/images/mlflow1.png differ
diff --git a/docs/installation.mdx b/docs/installation.mdx
index c98bce9ef..8abba152a 100644
--- a/docs/installation.mdx
+++ b/docs/installation.mdx
@@ -15,10 +15,48 @@ icon: wrench
If you need to update Python, visit [python.org/downloads](https://python.org/downloads)
+# Setting Up Your Environment
+
+Before installing CrewAI, it's recommended to set up a virtual environment. This helps isolate your project dependencies and avoid conflicts.
+
+
+
+ Choose your preferred method to create a virtual environment:
+
+ **Using venv (Python's built-in tool):**
+ ```shell Terminal
+ python3 -m venv .venv
+ ```
+
+ **Using conda:**
+ ```shell Terminal
+ conda create -n crewai-env python=3.12
+ ```
+
+
+
+ Activate your virtual environment based on your platform:
+
+ **On macOS/Linux (venv):**
+ ```shell Terminal
+ source .venv/bin/activate
+ ```
+
+ **On Windows (venv):**
+ ```shell Terminal
+ .venv\Scripts\activate
+ ```
+
+ **Using conda (all platforms):**
+ ```shell Terminal
+ conda activate crewai-env
+ ```
+
+
+
# Installing CrewAI
-CrewAI is a flexible and powerful AI framework that enables you to create and manage AI agents, tools, and tasks efficiently.
-Let's get you set up! π
+Now let's get you set up! π
@@ -72,9 +110,9 @@ Let's get you set up! π
# Creating a New Project
-
+
We recommend using the YAML Template scaffolding for a structured approach to defining agents and tasks.
-
+
@@ -104,7 +142,18 @@ Let's get you set up! π
βββ tasks.yaml
```
-
+
+
+
+ You can install additional tools using UV:
+ ```shell Terminal
+ uv add
+ ```
+
+
+ UV is our preferred package manager as it's significantly faster than pip and provides better dependency resolution.
+
+
Your project will contain these essential files:
diff --git a/docs/mint.json b/docs/mint.json
index 585fc0abd..fb0dcfdf5 100644
--- a/docs/mint.json
+++ b/docs/mint.json
@@ -101,8 +101,10 @@
"how-to/conditional-tasks",
"how-to/agentops-observability",
"how-to/langtrace-observability",
+ "how-to/mlflow-observability",
"how-to/openlit-observability",
- "how-to/portkey-observability"
+ "how-to/portkey-observability",
+ "how-to/langfuse-observability"
]
},
{
diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx
index 3062cdd0d..9fb8f783a 100644
--- a/docs/quickstart.mdx
+++ b/docs/quickstart.mdx
@@ -58,7 +58,7 @@ Follow the steps below to get crewing! π£ββοΈ
description: >
Conduct a thorough research about {topic}
Make sure you find any interesting and relevant information given
- the current year is 2024.
+ the current year is 2025.
expected_output: >
A list with 10 bullet points of the most relevant information about {topic}
agent: researcher
@@ -195,10 +195,10 @@ Follow the steps below to get crewing! π£ββοΈ
```markdown output/report.md
- # Comprehensive Report on the Rise and Impact of AI Agents in 2024
+ # Comprehensive Report on the Rise and Impact of AI Agents in 2025
## 1. Introduction to AI Agents
- In 2024, Artificial Intelligence (AI) agents are at the forefront of innovation across various industries. As intelligent systems that can perform tasks typically requiring human cognition, AI agents are paving the way for significant advancements in operational efficiency, decision-making, and overall productivity within sectors like Human Resources (HR) and Finance. This report aims to detail the rise of AI agents, their frameworks, applications, and potential implications on the workforce.
+ In 2025, Artificial Intelligence (AI) agents are at the forefront of innovation across various industries. As intelligent systems that can perform tasks typically requiring human cognition, AI agents are paving the way for significant advancements in operational efficiency, decision-making, and overall productivity within sectors like Human Resources (HR) and Finance. This report aims to detail the rise of AI agents, their frameworks, applications, and potential implications on the workforce.
## 2. Benefits of AI Agents
AI agents bring numerous advantages that are transforming traditional work environments. Key benefits include:
@@ -252,7 +252,7 @@ Follow the steps below to get crewing! π£ββοΈ
To stay competitive and harness the full potential of AI agents, organizations must remain vigilant about latest developments in AI technology and consider continuous learning and adaptation in their strategic planning.
## 8. Conclusion
- The emergence of AI agents is undeniably reshaping the workplace landscape in 2024. With their ability to automate tasks, enhance efficiency, and improve decision-making, AI agents are critical in driving operational success. Organizations must embrace and adapt to AI developments to thrive in an increasingly digital business environment.
+ The emergence of AI agents is undeniably reshaping the workplace landscape in 5. With their ability to automate tasks, enhance efficiency, and improve decision-making, AI agents are critical in driving operational success. Organizations must embrace and adapt to AI developments to thrive in an increasingly digital business environment.
```
@@ -278,7 +278,7 @@ email_summarizer:
Summarize emails into a concise and clear summary
backstory: >
You will create a 5 bullet point summary of the report
- llm: mixtal_llm
+ llm: openai/gpt-4o
```
diff --git a/docs/tools/composiotool.mdx b/docs/tools/composiotool.mdx
index 0ba1fcf60..b72e09dcd 100644
--- a/docs/tools/composiotool.mdx
+++ b/docs/tools/composiotool.mdx
@@ -1,78 +1,118 @@
---
title: Composio Tool
-description: The `ComposioTool` is a wrapper around the composio set of tools and gives your agent access to a wide variety of tools from the Composio SDK.
+description: Composio provides 250+ production-ready tools for AI agents with flexible authentication management.
icon: gear-code
---
-# `ComposioTool`
+# `ComposioToolSet`
## Description
+Composio is an integration platform that allows you to connect your AI agents to 250+ tools. Key features include:
-This tools is a wrapper around the composio set of tools and gives your agent access to a wide variety of tools from the Composio SDK.
+- **Enterprise-Grade Authentication**: Built-in support for OAuth, API Keys, JWT with automatic token refresh
+- **Full Observability**: Detailed tool usage logs, execution timestamps, and more
## Installation
-To incorporate this tool into your project, follow the installation instructions below:
+To incorporate Composio tools into your project, follow the instructions below:
```shell
-pip install composio-core
-pip install 'crewai[tools]'
+pip install composio-crewai
+pip install crewai
```
-after the installation is complete, either run `composio login` or export your composio API key as `COMPOSIO_API_KEY`.
+After the installation is complete, either run `composio login` or export your composio API key as `COMPOSIO_API_KEY`. Get your Composio API key from [here](https://app.composio.dev)
## Example
The following example demonstrates how to initialize the tool and execute a github action:
-1. Initialize Composio tools
+1. Initialize Composio toolset
```python Code
-from composio import App
-from crewai_tools import ComposioTool
-from crewai import Agent, Task
+from composio_crewai import ComposioToolSet, App, Action
+from crewai import Agent, Task, Crew
-
-tools = [ComposioTool.from_action(action=Action.GITHUB_ACTIVITY_STAR_REPO_FOR_AUTHENTICATED_USER)]
+toolset = ComposioToolSet()
```
-If you don't know what action you want to use, use `from_app` and `tags` filter to get relevant actions
-
+2. Connect your GitHub account
+
+```shell CLI
+composio add github
+```
```python Code
-tools = ComposioTool.from_app(App.GITHUB, tags=["important"])
+request = toolset.initiate_connection(app=App.GITHUB)
+print(f"Open this URL to authenticate: {request.redirectUrl}")
```
+
-or use `use_case` to search relevant actions
+3. Get Tools
+- Retrieving all the tools from an app (not recommended for production):
```python Code
-tools = ComposioTool.from_app(App.GITHUB, use_case="Star a github repository")
+tools = toolset.get_tools(apps=[App.GITHUB])
```
-2. Define agent
+- Filtering tools based on tags:
+```python Code
+tag = "users"
+
+filtered_action_enums = toolset.find_actions_by_tags(
+ App.GITHUB,
+ tags=[tag],
+)
+
+tools = toolset.get_tools(actions=filtered_action_enums)
+```
+
+- Filtering tools based on use case:
+```python Code
+use_case = "Star a repository on GitHub"
+
+filtered_action_enums = toolset.find_actions_by_use_case(
+ App.GITHUB, use_case=use_case, advanced=False
+)
+
+tools = toolset.get_tools(actions=filtered_action_enums)
+```
+Set `advanced` to True to get actions for complex use cases
+
+- Using specific tools:
+
+In this demo, we will use the `GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER` action from the GitHub app.
+```python Code
+tools = toolset.get_tools(
+ actions=[Action.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER]
+)
+```
+Learn more about filtering actions [here](https://docs.composio.dev/patterns/tools/use-tools/use-specific-actions)
+
+4. Define agent
```python Code
crewai_agent = Agent(
- role="Github Agent",
- goal="You take action on Github using Github APIs",
- backstory=(
- "You are AI agent that is responsible for taking actions on Github "
- "on users behalf. You need to take action on Github using Github APIs"
- ),
+ role="GitHub Agent",
+ goal="You take action on GitHub using GitHub APIs",
+ backstory="You are AI agent that is responsible for taking actions on GitHub on behalf of users using GitHub APIs",
verbose=True,
tools=tools,
+ llm= # pass an llm
)
```
-3. Execute task
+5. Execute task
```python Code
task = Task(
- description="Star a repo ComposioHQ/composio on GitHub",
+ description="Star a repo composiohq/composio on GitHub",
agent=crewai_agent,
- expected_output="if the star happened",
+ expected_output="Status of the operation",
)
-task.execute()
+crew = Crew(agents=[crewai_agent], tasks=[task])
+
+crew.kickoff()
```
-* More detailed list of tools can be found [here](https://app.composio.dev)
\ No newline at end of file
+* More detailed list of tools can be found [here](https://app.composio.dev)
diff --git a/docs/tools/filewritetool.mdx b/docs/tools/filewritetool.mdx
index f5dffb2ad..5e00801b7 100644
--- a/docs/tools/filewritetool.mdx
+++ b/docs/tools/filewritetool.mdx
@@ -8,9 +8,9 @@ icon: file-pen
## Description
-The `FileWriterTool` is a component of the crewai_tools package, designed to simplify the process of writing content to files.
+The `FileWriterTool` is a component of the crewai_tools package, designed to simplify the process of writing content to files with cross-platform compatibility (Windows, Linux, macOS).
It is particularly useful in scenarios such as generating reports, saving logs, creating configuration files, and more.
-This tool supports creating new directories if they don't exist, making it easier to organize your output.
+This tool handles path differences across operating systems, supports UTF-8 encoding, and automatically creates directories if they don't exist, making it easier to organize your output reliably across different platforms.
## Installation
@@ -43,6 +43,8 @@ print(result)
## Conclusion
-By integrating the `FileWriterTool` into your crews, the agents can execute the process of writing content to files and creating directories.
-This tool is essential for tasks that require saving output data, creating structured file systems, and more. By adhering to the setup and usage guidelines provided,
-incorporating this tool into projects is straightforward and efficient.
\ No newline at end of file
+By integrating the `FileWriterTool` into your crews, the agents can reliably write content to files across different operating systems.
+This tool is essential for tasks that require saving output data, creating structured file systems, and handling cross-platform file operations.
+It's particularly recommended for Windows users who may encounter file writing issues with standard Python file operations.
+
+By adhering to the setup and usage guidelines provided, incorporating this tool into projects is straightforward and ensures consistent file writing behavior across all platforms.
diff --git a/mkdocs.yml b/mkdocs.yml
index 948744de2..511078fff 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -152,6 +152,7 @@ nav:
- Agent Monitoring with AgentOps: 'how-to/AgentOps-Observability.md'
- Agent Monitoring with LangTrace: 'how-to/Langtrace-Observability.md'
- Agent Monitoring with OpenLIT: 'how-to/openlit-Observability.md'
+ - Agent Monitoring with MLflow: 'how-to/mlflow-Observability.md'
- Tools Docs:
- Browserbase Web Loader: 'tools/BrowserbaseLoadTool.md'
- Code Docs RAG Search: 'tools/CodeDocsSearchTool.md'
diff --git a/pyproject.toml b/pyproject.toml
index d032fb0c8..c17d5fedb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "crewai"
-version = "0.95.0"
+version = "0.102.0"
description = "Cutting-edge framework for orchestrating role-playing, autonomous AI agents. By fostering collaborative intelligence, CrewAI empowers agents to work together seamlessly, tackling complex tasks."
readme = "README.md"
requires-python = ">=3.10,<3.13"
@@ -11,27 +11,22 @@ dependencies = [
# Core Dependencies
"pydantic>=2.4.2",
"openai>=1.13.3",
- "litellm==1.57.4",
+ "litellm==1.60.2",
"instructor>=1.3.3",
-
# Text Processing
"pdfplumber>=0.11.4",
"regex>=2024.9.11",
-
# Telemetry and Monitoring
"opentelemetry-api>=1.22.0",
"opentelemetry-sdk>=1.22.0",
"opentelemetry-exporter-otlp-proto-http>=1.22.0",
-
# Data Handling
"chromadb>=0.5.23",
"openpyxl>=3.1.5",
"pyvis>=0.3.2",
-
# Authentication and Security
"auth0-python>=4.7.1",
"python-dotenv>=1.0.0",
-
# Configuration and Utils
"click>=8.1.7",
"appdirs>=1.4.4",
@@ -40,7 +35,8 @@ dependencies = [
"uv>=0.4.25",
"tomli-w>=1.1.0",
"tomli>=2.0.2",
- "blinker>=1.9.0"
+ "blinker>=1.9.0",
+ "json5>=0.10.0",
]
[project.urls]
@@ -49,7 +45,7 @@ Documentation = "https://docs.crewai.com"
Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
-tools = ["crewai-tools>=0.25.5"]
+tools = ["crewai-tools>=0.36.0"]
embeddings = [
"tiktoken~=0.7.0"
]
diff --git a/src/crewai/__init__.py b/src/crewai/__init__.py
index f0ba35a38..662af2563 100644
--- a/src/crewai/__init__.py
+++ b/src/crewai/__init__.py
@@ -14,7 +14,7 @@ warnings.filterwarnings(
category=UserWarning,
module="pydantic.main",
)
-__version__ = "0.95.0"
+__version__ = "0.102.0"
__all__ = [
"Agent",
"Crew",
diff --git a/src/crewai/agent.py b/src/crewai/agent.py
index b7f0b2896..4c1e3c393 100644
--- a/src/crewai/agent.py
+++ b/src/crewai/agent.py
@@ -1,14 +1,13 @@
-import os
+import re
import shutil
import subprocess
-from typing import Any, Dict, List, Literal, Optional, Union
+from typing import Any, Dict, List, Literal, Optional, Sequence, Union
from pydantic import Field, InstanceOf, PrivateAttr, model_validator
from crewai.agents import CacheHandler
from crewai.agents.agent_builder.base_agent import BaseAgent
from crewai.agents.crew_agent_executor import CrewAgentExecutor
-from crewai.cli.constants import ENV_VARS, LITELLM_PARAMS
from crewai.knowledge.knowledge import Knowledge
from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
from crewai.knowledge.utils.knowledge_utils import extract_knowledge_context
@@ -17,7 +16,6 @@ from crewai.memory.contextual.contextual_memory import ContextualMemory
from crewai.task import Task
from crewai.tools import BaseTool
from crewai.tools.agent_tools.agent_tools import AgentTools
-from crewai.tools.base_tool import Tool
from crewai.utilities import Converter, Prompts
from crewai.utilities.constants import TRAINED_AGENTS_DATA_FILE, TRAINING_DATA_FILE
from crewai.utilities.converter import generate_model_description
@@ -56,13 +54,13 @@ class Agent(BaseAgent):
llm: The language model that will run the agent.
function_calling_llm: The language model that will handle the tool calling for this agent, it overrides the crew function_calling_llm.
max_iter: Maximum number of iterations for an agent to execute a task.
- memory: Whether the agent should have memory or not.
max_rpm: Maximum number of requests per minute for the agent execution to be respected.
verbose: Whether the agent execution should be in verbose mode.
allow_delegation: Whether the agent is allowed to delegate tasks to other agents.
tools: Tools at agents disposal
step_callback: Callback to be executed after each step of the agent execution.
knowledge_sources: Knowledge sources for the agent.
+ embedder: Embedder configuration for the agent.
"""
_times_executed: int = PrivateAttr(default=0)
@@ -72,9 +70,6 @@ class Agent(BaseAgent):
)
agent_ops_agent_name: str = None # type: ignore # Incompatible types in assignment (expression has type "None", variable has type "str")
agent_ops_agent_id: str = None # type: ignore # Incompatible types in assignment (expression has type "None", variable has type "str")
- cache_handler: InstanceOf[CacheHandler] = Field(
- default=None, description="An instance of the CacheHandler class."
- )
step_callback: Optional[Any] = Field(
default=None,
description="Callback to be executed after each step of the agent execution.",
@@ -108,10 +103,6 @@ class Agent(BaseAgent):
default=True,
description="Keep messages under the context window size by summarizing content.",
)
- max_iter: int = Field(
- default=20,
- description="Maximum number of iterations for an agent to execute a task before giving it's best answer",
- )
max_retry_limit: int = Field(
default=2,
description="Maximum number of retries for an agent to execute a task when an error occurs.",
@@ -124,17 +115,10 @@ class Agent(BaseAgent):
default="safe",
description="Mode for code execution: 'safe' (using Docker) or 'unsafe' (direct execution).",
)
- embedder_config: Optional[Dict[str, Any]] = Field(
+ embedder: Optional[Dict[str, Any]] = Field(
default=None,
description="Embedder configuration for the agent.",
)
- knowledge_sources: Optional[List[BaseKnowledgeSource]] = Field(
- default=None,
- description="Knowledge sources for the agent.",
- )
- _knowledge: Optional[Knowledge] = PrivateAttr(
- default=None,
- )
@model_validator(mode="after")
def post_init_setup(self):
@@ -161,14 +145,16 @@ class Agent(BaseAgent):
def _set_knowledge(self):
try:
if self.knowledge_sources:
- knowledge_agent_name = f"{self.role.replace(' ', '_')}"
+ full_pattern = re.compile(r"[^a-zA-Z0-9\-_\r\n]|(\.\.)")
+ knowledge_agent_name = f"{re.sub(full_pattern, '_', self.role)}"
if isinstance(self.knowledge_sources, list) and all(
isinstance(k, BaseKnowledgeSource) for k in self.knowledge_sources
):
- self._knowledge = Knowledge(
+ self.knowledge = Knowledge(
sources=self.knowledge_sources,
- embedder_config=self.embedder_config,
+ embedder=self.embedder,
collection_name=knowledge_agent_name,
+ storage=self.knowledge_storage or None,
)
except (TypeError, ValueError) as e:
raise ValueError(f"Invalid Knowledge Configuration: {str(e)}")
@@ -202,13 +188,15 @@ class Agent(BaseAgent):
if task.output_json:
# schema = json.dumps(task.output_json, indent=2)
schema = generate_model_description(task.output_json)
+ task_prompt += "\n" + self.i18n.slice(
+ "formatted_task_instructions"
+ ).format(output_format=schema)
elif task.output_pydantic:
schema = generate_model_description(task.output_pydantic)
-
- task_prompt += "\n" + self.i18n.slice("formatted_task_instructions").format(
- output_format=schema
- )
+ task_prompt += "\n" + self.i18n.slice(
+ "formatted_task_instructions"
+ ).format(output_format=schema)
if context:
task_prompt = self.i18n.slice("task_with_context").format(
@@ -227,8 +215,8 @@ class Agent(BaseAgent):
if memory.strip() != "":
task_prompt += self.i18n.slice("memory").format(memory=memory)
- if self._knowledge:
- agent_knowledge_snippets = self._knowledge.query([task.prompt()])
+ if self.knowledge:
+ agent_knowledge_snippets = self.knowledge.query([task.prompt()])
if agent_knowledge_snippets:
agent_knowledge_context = extract_knowledge_context(
agent_knowledge_snippets
@@ -261,6 +249,9 @@ class Agent(BaseAgent):
}
)["output"]
except Exception as e:
+ if e.__class__.__module__.startswith("litellm"):
+ # Do not retry on litellm errors
+ raise e
self._times_executed += 1
if self._times_executed > self.max_retry_limit:
raise e
@@ -333,14 +324,14 @@ class Agent(BaseAgent):
tools = agent_tools.tools()
return tools
- def get_multimodal_tools(self) -> List[Tool]:
+ def get_multimodal_tools(self) -> Sequence[BaseTool]:
from crewai.tools.agent_tools.add_image_tool import AddImageTool
return [AddImageTool()]
def get_code_execution_tools(self):
try:
- from crewai_tools import CodeInterpreterTool
+ from crewai_tools import CodeInterpreterTool # type: ignore
# Set the unsafe_mode based on the code_execution_mode attribute
unsafe_mode = self.code_execution_mode == "unsafe"
diff --git a/src/crewai/agents/agent_builder/base_agent.py b/src/crewai/agents/agent_builder/base_agent.py
index 207a1769a..e602e42a9 100644
--- a/src/crewai/agents/agent_builder/base_agent.py
+++ b/src/crewai/agents/agent_builder/base_agent.py
@@ -18,10 +18,13 @@ from pydantic_core import PydanticCustomError
from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess
from crewai.agents.cache.cache_handler import CacheHandler
from crewai.agents.tools_handler import ToolsHandler
+from crewai.knowledge.knowledge import Knowledge
+from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
from crewai.tools import BaseTool
from crewai.tools.base_tool import Tool
from crewai.utilities import I18N, Logger, RPMController
from crewai.utilities.config import process_config
+from crewai.utilities.converter import Converter
T = TypeVar("T", bound="BaseAgent")
@@ -40,7 +43,7 @@ class BaseAgent(ABC, BaseModel):
max_rpm (Optional[int]): Maximum number of requests per minute for the agent execution.
allow_delegation (bool): Allow delegation of tasks to agents.
tools (Optional[List[Any]]): Tools at the agent's disposal.
- max_iter (Optional[int]): Maximum iterations for an agent to execute a task.
+ max_iter (int): Maximum iterations for an agent to execute a task.
agent_executor (InstanceOf): An instance of the CrewAgentExecutor class.
llm (Any): Language model that will run the agent.
crew (Any): Crew to which the agent belongs.
@@ -48,6 +51,8 @@ class BaseAgent(ABC, BaseModel):
cache_handler (InstanceOf[CacheHandler]): An instance of the CacheHandler class.
tools_handler (InstanceOf[ToolsHandler]): An instance of the ToolsHandler class.
max_tokens: Maximum number of tokens for the agent to generate in a response.
+ knowledge_sources: Knowledge sources for the agent.
+ knowledge_storage: Custom knowledge storage for the agent.
Methods:
@@ -110,7 +115,7 @@ class BaseAgent(ABC, BaseModel):
tools: Optional[List[Any]] = Field(
default_factory=list, description="Tools at agents' disposal"
)
- max_iter: Optional[int] = Field(
+ max_iter: int = Field(
default=25, description="Maximum iterations for an agent to execute a task"
)
agent_executor: InstanceOf = Field(
@@ -121,15 +126,27 @@ class BaseAgent(ABC, BaseModel):
)
crew: Any = Field(default=None, description="Crew to which the agent belongs.")
i18n: I18N = Field(default=I18N(), description="Internationalization settings.")
- cache_handler: InstanceOf[CacheHandler] = Field(
+ cache_handler: Optional[InstanceOf[CacheHandler]] = Field(
default=None, description="An instance of the CacheHandler class."
)
tools_handler: InstanceOf[ToolsHandler] = Field(
- default=None, description="An instance of the ToolsHandler class."
+ default_factory=ToolsHandler,
+ description="An instance of the ToolsHandler class.",
)
max_tokens: Optional[int] = Field(
default=None, description="Maximum number of tokens for the agent's execution."
)
+ knowledge: Optional[Knowledge] = Field(
+ default=None, description="Knowledge for the agent."
+ )
+ knowledge_sources: Optional[List[BaseKnowledgeSource]] = Field(
+ default=None,
+ description="Knowledge sources for the agent.",
+ )
+ knowledge_storage: Optional[Any] = Field(
+ default=None,
+ description="Custom knowledge storage for the agent.",
+ )
@model_validator(mode="before")
@classmethod
@@ -239,7 +256,7 @@ class BaseAgent(ABC, BaseModel):
@abstractmethod
def get_output_converter(
self, llm: Any, text: str, model: type[BaseModel] | None, instructions: str
- ):
+ ) -> Converter:
"""Get the converter class for the agent to create json/pydantic outputs."""
pass
@@ -256,13 +273,44 @@ class BaseAgent(ABC, BaseModel):
"tools_handler",
"cache_handler",
"llm",
+ "knowledge_sources",
+ "knowledge_storage",
+ "knowledge",
}
- # Copy llm and clear callbacks
+ # Copy llm
existing_llm = shallow_copy(self.llm)
+ copied_knowledge = shallow_copy(self.knowledge)
+ copied_knowledge_storage = shallow_copy(self.knowledge_storage)
+ # Properly copy knowledge sources if they exist
+ existing_knowledge_sources = None
+ if self.knowledge_sources:
+ # Create a shared storage instance for all knowledge sources
+ shared_storage = (
+ self.knowledge_sources[0].storage if self.knowledge_sources else None
+ )
+
+ existing_knowledge_sources = []
+ for source in self.knowledge_sources:
+ copied_source = (
+ source.model_copy()
+ if hasattr(source, "model_copy")
+ else shallow_copy(source)
+ )
+ # Ensure all copied sources use the same storage instance
+ copied_source.storage = shared_storage
+ existing_knowledge_sources.append(copied_source)
+
copied_data = self.model_dump(exclude=exclude)
copied_data = {k: v for k, v in copied_data.items() if v is not None}
- copied_agent = type(self)(**copied_data, llm=existing_llm, tools=self.tools)
+ copied_agent = type(self)(
+ **copied_data,
+ llm=existing_llm,
+ tools=self.tools,
+ knowledge_sources=existing_knowledge_sources,
+ knowledge=copied_knowledge,
+ knowledge_storage=copied_knowledge_storage,
+ )
return copied_agent
diff --git a/src/crewai/agents/agent_builder/base_agent_executor_mixin.py b/src/crewai/agents/agent_builder/base_agent_executor_mixin.py
index bcc585731..924cef71c 100644
--- a/src/crewai/agents/agent_builder/base_agent_executor_mixin.py
+++ b/src/crewai/agents/agent_builder/base_agent_executor_mixin.py
@@ -95,18 +95,29 @@ class CrewAgentExecutorMixin:
pass
def _ask_human_input(self, final_answer: str) -> str:
- """Prompt human input for final decision making."""
+ """Prompt human input with mode-appropriate messaging."""
self._printer.print(
content=f"\033[1m\033[95m ## Final Result:\033[00m \033[92m{final_answer}\033[00m"
)
- self._printer.print(
- content=(
+ # Training mode prompt (single iteration)
+ if self.crew and getattr(self.crew, "_train", False):
+ prompt = (
"\n\n=====\n"
- "## Please provide feedback on the Final Result and the Agent's actions. "
- "Respond with 'looks good' or a similar phrase when you're satisfied.\n"
+ "## TRAINING MODE: Provide feedback to improve the agent's performance.\n"
+ "This will be used to train better versions of the agent.\n"
+ "Please provide detailed feedback about the result quality and reasoning process.\n"
"=====\n"
- ),
- color="bold_yellow",
- )
+ )
+ # Regular human-in-the-loop prompt (multiple iterations)
+ else:
+ prompt = (
+ "\n\n=====\n"
+ "## HUMAN FEEDBACK: Provide feedback on the Final Result and Agent's actions.\n"
+ "Respond with 'looks good' to accept or provide specific improvement requests.\n"
+ "You can provide multiple rounds of feedback until satisfied.\n"
+ "=====\n"
+ )
+
+ self._printer.print(content=prompt, color="bold_yellow")
return input()
diff --git a/src/crewai/agents/agent_builder/utilities/base_token_process.py b/src/crewai/agents/agent_builder/utilities/base_token_process.py
index 322fade0e..3ce5cfb82 100644
--- a/src/crewai/agents/agent_builder/utilities/base_token_process.py
+++ b/src/crewai/agents/agent_builder/utilities/base_token_process.py
@@ -2,26 +2,26 @@ from crewai.types.usage_metrics import UsageMetrics
class TokenProcess:
- def __init__(self):
+ def __init__(self) -> None:
self.total_tokens: int = 0
self.prompt_tokens: int = 0
self.cached_prompt_tokens: int = 0
self.completion_tokens: int = 0
self.successful_requests: int = 0
- def sum_prompt_tokens(self, tokens: int):
- self.prompt_tokens = self.prompt_tokens + tokens
- self.total_tokens = self.total_tokens + tokens
+ def sum_prompt_tokens(self, tokens: int) -> None:
+ self.prompt_tokens += tokens
+ self.total_tokens += tokens
- def sum_completion_tokens(self, tokens: int):
- self.completion_tokens = self.completion_tokens + tokens
- self.total_tokens = self.total_tokens + tokens
+ def sum_completion_tokens(self, tokens: int) -> None:
+ self.completion_tokens += tokens
+ self.total_tokens += tokens
- def sum_cached_prompt_tokens(self, tokens: int):
- self.cached_prompt_tokens = self.cached_prompt_tokens + tokens
+ def sum_cached_prompt_tokens(self, tokens: int) -> None:
+ self.cached_prompt_tokens += tokens
- def sum_successful_requests(self, requests: int):
- self.successful_requests = self.successful_requests + requests
+ def sum_successful_requests(self, requests: int) -> None:
+ self.successful_requests += requests
def get_summary(self) -> UsageMetrics:
return UsageMetrics(
diff --git a/src/crewai/agents/crew_agent_executor.py b/src/crewai/agents/crew_agent_executor.py
index 286f92e67..ed89008fd 100644
--- a/src/crewai/agents/crew_agent_executor.py
+++ b/src/crewai/agents/crew_agent_executor.py
@@ -13,6 +13,7 @@ from crewai.agents.parser import (
OutputParserException,
)
from crewai.agents.tools_handler import ToolsHandler
+from crewai.llm import LLM
from crewai.tools.base_tool import BaseTool
from crewai.tools.tool_usage import ToolUsage, ToolUsageErrorException
from crewai.utilities import I18N, Printer
@@ -54,7 +55,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
callbacks: List[Any] = [],
):
self._i18n: I18N = I18N()
- self.llm = llm
+ self.llm: LLM = llm
self.task = task
self.agent = agent
self.crew = crew
@@ -80,10 +81,8 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
self.tool_name_to_tool_map: Dict[str, BaseTool] = {
tool.name: tool for tool in self.tools
}
- if self.llm.stop:
- self.llm.stop = list(set(self.llm.stop + self.stop))
- else:
- self.llm.stop = self.stop
+ self.stop = stop_words
+ self.llm.stop = list(set(self.llm.stop + self.stop))
def invoke(self, inputs: Dict[str, str]) -> Dict[str, Any]:
if "system" in self.prompt:
@@ -98,7 +97,22 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
self._show_start_logs()
self.ask_for_human_input = bool(inputs.get("ask_for_human_input", False))
- formatted_answer = self._invoke_loop()
+
+ try:
+ formatted_answer = self._invoke_loop()
+ except AssertionError:
+ self._printer.print(
+ content="Agent failed to reach a final answer. This is likely a bug - please report it.",
+ color="red",
+ )
+ raise
+ except Exception as e:
+ if e.__class__.__module__.startswith("litellm"):
+ # Do not retry on litellm errors
+ raise e
+ else:
+ self._handle_unknown_error(e)
+ raise e
if self.ask_for_human_input:
formatted_answer = self._handle_human_feedback(formatted_answer)
@@ -107,7 +121,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
self._create_long_term_memory(formatted_answer)
return {"output": formatted_answer.output}
- def _invoke_loop(self):
+ def _invoke_loop(self) -> AgentFinish:
"""
Main loop to invoke the agent's thought process until it reaches a conclusion
or the maximum number of iterations is reached.
@@ -124,7 +138,6 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
self._enforce_rpm_limit()
answer = self._get_llm_response()
-
formatted_answer = self._process_llm_response(answer)
if isinstance(formatted_answer, AgentAction):
@@ -142,13 +155,37 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
formatted_answer = self._handle_output_parser_exception(e)
except Exception as e:
+ if e.__class__.__module__.startswith("litellm"):
+ # Do not retry on litellm errors
+ raise e
if self._is_context_length_exceeded(e):
self._handle_context_length()
continue
+ else:
+ self._handle_unknown_error(e)
+ raise e
+ finally:
+ self.iterations += 1
+ # During the invoke loop, formatted_answer alternates between AgentAction
+ # (when the agent is using tools) and eventually becomes AgentFinish
+ # (when the agent reaches a final answer). This assertion confirms we've
+ # reached a final answer and helps type checking understand this transition.
+ assert isinstance(formatted_answer, AgentFinish)
self._show_logs(formatted_answer)
return formatted_answer
+ def _handle_unknown_error(self, exception: Exception) -> None:
+ """Handle unknown errors by informing the user."""
+ self._printer.print(
+ content="An unknown error occurred. Please check the details below.",
+ color="red",
+ )
+ self._printer.print(
+ content=f"Error details: {exception}",
+ color="red",
+ )
+
def _has_reached_max_iterations(self) -> bool:
"""Check if the maximum number of iterations has been reached."""
return self.iterations >= self.max_iter
@@ -160,10 +197,17 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
def _get_llm_response(self) -> str:
"""Call the LLM and return the response, handling any invalid responses."""
- answer = self.llm.call(
- self.messages,
- callbacks=self.callbacks,
- )
+ try:
+ answer = self.llm.call(
+ self.messages,
+ callbacks=self.callbacks,
+ )
+ except Exception as e:
+ self._printer.print(
+ content=f"Error during LLM call: {e}",
+ color="red",
+ )
+ raise e
if not answer:
self._printer.print(
@@ -184,7 +228,6 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
if FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE in e.error:
answer = answer.split("Observation:")[0].strip()
- self.iterations += 1
return self._format_answer(answer)
def _handle_agent_action(
@@ -260,8 +303,11 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
self._printer.print(
content=f"\033[1m\033[95m# Agent:\033[00m \033[1m\033[92m{agent_role}\033[00m"
)
+ description = (
+ getattr(self.task, "description") if self.task else "Not Found"
+ )
self._printer.print(
- content=f"\033[95m## Task:\033[00m \033[92m{self.task.description}\033[00m"
+ content=f"\033[95m## Task:\033[00m \033[92m{description}\033[00m"
)
def _show_logs(self, formatted_answer: Union[AgentAction, AgentFinish]):
@@ -386,58 +432,50 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
)
def _handle_crew_training_output(
- self, result: AgentFinish, human_feedback: str | None = None
+ self, result: AgentFinish, human_feedback: Optional[str] = None
) -> None:
- """Function to handle the process of the training data."""
+ """Handle the process of saving training data."""
agent_id = str(self.agent.id) # type: ignore
+ train_iteration = (
+ getattr(self.crew, "_train_iteration", None) if self.crew else None
+ )
+
+ if train_iteration is None or not isinstance(train_iteration, int):
+ self._printer.print(
+ content="Invalid or missing train iteration. Cannot save training data.",
+ color="red",
+ )
+ return
- # Load training data
training_handler = CrewTrainingHandler(TRAINING_DATA_FILE)
- training_data = training_handler.load()
+ training_data = training_handler.load() or {}
- # Check if training data exists, human input is not requested, and self.crew is valid
- if training_data and not self.ask_for_human_input:
- if self.crew is not None and hasattr(self.crew, "_train_iteration"):
- train_iteration = self.crew._train_iteration
- if agent_id in training_data and isinstance(train_iteration, int):
- training_data[agent_id][train_iteration][
- "improved_output"
- ] = result.output
- training_handler.save(training_data)
- else:
- self._printer.print(
- content="Invalid train iteration type or agent_id not in training data.",
- color="red",
- )
- else:
- self._printer.print(
- content="Crew is None or does not have _train_iteration attribute.",
- color="red",
- )
+ # Initialize or retrieve agent's training data
+ agent_training_data = training_data.get(agent_id, {})
- if self.ask_for_human_input and human_feedback is not None:
- training_data = {
+ if human_feedback is not None:
+ # Save initial output and human feedback
+ agent_training_data[train_iteration] = {
"initial_output": result.output,
"human_feedback": human_feedback,
- "agent": agent_id,
- "agent_role": self.agent.role, # type: ignore
}
- if self.crew is not None and hasattr(self.crew, "_train_iteration"):
- train_iteration = self.crew._train_iteration
- if isinstance(train_iteration, int):
- CrewTrainingHandler(TRAINING_DATA_FILE).append(
- train_iteration, agent_id, training_data
- )
- else:
- self._printer.print(
- content="Invalid train iteration type. Expected int.",
- color="red",
- )
+ else:
+ # Save improved output
+ if train_iteration in agent_training_data:
+ agent_training_data[train_iteration]["improved_output"] = result.output
else:
self._printer.print(
- content="Crew is None or does not have _train_iteration attribute.",
+ content=(
+ f"No existing training data for agent {agent_id} and iteration "
+ f"{train_iteration}. Cannot save improved output."
+ ),
color="red",
)
+ return
+
+ # Update the training data and save
+ training_data[agent_id] = agent_training_data
+ training_handler.save(training_data)
def _format_prompt(self, prompt: str, inputs: Dict[str, str]) -> str:
prompt = prompt.replace("{input}", inputs["input"])
@@ -453,82 +491,111 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
return {"role": role, "content": prompt}
def _handle_human_feedback(self, formatted_answer: AgentFinish) -> AgentFinish:
- """
- Handles the human feedback loop, allowing the user to provide feedback
- on the agent's output and determining if additional iterations are needed.
+ """Handle human feedback with different flows for training vs regular use.
- Parameters:
- formatted_answer (AgentFinish): The initial output from the agent.
+ Args:
+ formatted_answer: The initial AgentFinish result to get feedback on
Returns:
- AgentFinish: The final output after incorporating human feedback.
+ AgentFinish: The final answer after processing feedback
"""
+ human_feedback = self._ask_human_input(formatted_answer.output)
+
+ if self._is_training_mode():
+ return self._handle_training_feedback(formatted_answer, human_feedback)
+
+ return self._handle_regular_feedback(formatted_answer, human_feedback)
+
+ def _is_training_mode(self) -> bool:
+ """Check if crew is in training mode."""
+ return bool(self.crew and self.crew._train)
+
+ def _handle_training_feedback(
+ self, initial_answer: AgentFinish, feedback: str
+ ) -> AgentFinish:
+ """Process feedback for training scenarios with single iteration."""
+ self._printer.print(
+ content="\nProcessing training feedback.\n",
+ color="yellow",
+ )
+ self._handle_crew_training_output(initial_answer, feedback)
+ self.messages.append(
+ self._format_msg(
+ self._i18n.slice("feedback_instructions").format(feedback=feedback)
+ )
+ )
+ improved_answer = self._invoke_loop()
+ self._handle_crew_training_output(improved_answer)
+ self.ask_for_human_input = False
+ return improved_answer
+
+ def _handle_regular_feedback(
+ self, current_answer: AgentFinish, initial_feedback: str
+ ) -> AgentFinish:
+ """Process feedback for regular use with potential multiple iterations."""
+ feedback = initial_feedback
+ answer = current_answer
+
while self.ask_for_human_input:
- human_feedback = self._ask_human_input(formatted_answer.output)
+ response = self._get_llm_feedback_response(feedback)
- if self.crew and self.crew._train:
- self._handle_crew_training_output(formatted_answer, human_feedback)
-
- # Make an LLM call to verify if additional changes are requested based on human feedback
- additional_changes_prompt = self._i18n.slice(
- "human_feedback_classification"
- ).format(feedback=human_feedback)
-
- retry_count = 0
- llm_call_successful = False
- additional_changes_response = None
-
- while retry_count < MAX_LLM_RETRY and not llm_call_successful:
- try:
- additional_changes_response = (
- self.llm.call(
- [
- self._format_msg(
- additional_changes_prompt, role="system"
- )
- ],
- callbacks=self.callbacks,
- )
- .strip()
- .lower()
- )
- llm_call_successful = True
- except Exception as e:
- retry_count += 1
-
- self._printer.print(
- content=f"Error during LLM call to classify human feedback: {e}. Retrying... ({retry_count}/{MAX_LLM_RETRY})",
- color="red",
- )
-
- if not llm_call_successful:
- self._printer.print(
- content="Error processing feedback after multiple attempts.",
- color="red",
- )
+ if not self._feedback_requires_changes(response):
self.ask_for_human_input = False
- break
-
- if additional_changes_response == "false":
- self.ask_for_human_input = False
- elif additional_changes_response == "true":
- self.ask_for_human_input = True
- # Add human feedback to messages
- self.messages.append(self._format_msg(f"Feedback: {human_feedback}"))
- # Invoke the loop again with updated messages
- formatted_answer = self._invoke_loop()
-
- if self.crew and self.crew._train:
- self._handle_crew_training_output(formatted_answer)
else:
- # Unexpected response
- self._printer.print(
- content=f"Unexpected response from LLM: '{additional_changes_response}'. Assuming no additional changes requested.",
- color="red",
- )
- self.ask_for_human_input = False
+ answer = self._process_feedback_iteration(feedback)
+ feedback = self._ask_human_input(answer.output)
- return formatted_answer
+ return answer
+
+ def _get_llm_feedback_response(self, feedback: str) -> Optional[str]:
+ """Get LLM classification of whether feedback requires changes."""
+ prompt = self._i18n.slice("human_feedback_classification").format(
+ feedback=feedback
+ )
+ message = self._format_msg(prompt, role="system")
+
+ for retry in range(MAX_LLM_RETRY):
+ try:
+ response = self.llm.call([message], callbacks=self.callbacks)
+ return response.strip().lower() if response else None
+ except Exception as error:
+ self._log_feedback_error(retry, error)
+
+ self._log_max_retries_exceeded()
+ return None
+
+ def _feedback_requires_changes(self, response: Optional[str]) -> bool:
+ """Determine if feedback response indicates need for changes."""
+ return response == "true" if response else False
+
+ def _process_feedback_iteration(self, feedback: str) -> AgentFinish:
+ """Process a single feedback iteration."""
+ self.messages.append(
+ self._format_msg(
+ self._i18n.slice("feedback_instructions").format(feedback=feedback)
+ )
+ )
+ return self._invoke_loop()
+
+ def _log_feedback_error(self, retry_count: int, error: Exception) -> None:
+ """Log feedback processing errors."""
+ self._printer.print(
+ content=(
+ f"Error processing feedback: {error}. "
+ f"Retrying... ({retry_count + 1}/{MAX_LLM_RETRY})"
+ ),
+ color="red",
+ )
+
+ def _log_max_retries_exceeded(self) -> None:
+ """Log when max retries for feedback processing are exceeded."""
+ self._printer.print(
+ content=(
+ f"Failed to process feedback after {MAX_LLM_RETRY} attempts. "
+ "Ending feedback loop."
+ ),
+ color="red",
+ )
def _handle_max_iterations_exceeded(self, formatted_answer):
"""
diff --git a/src/crewai/cli/cli.py b/src/crewai/cli/cli.py
index 334759a6d..761cc52ad 100644
--- a/src/crewai/cli/cli.py
+++ b/src/crewai/cli/cli.py
@@ -350,7 +350,10 @@ def chat():
Start a conversation with the Crew, collecting user-supplied inputs,
and using the Chat LLM to generate responses.
"""
- click.echo("Starting a conversation with the Crew")
+ click.secho(
+ "\nStarting a conversation with the Crew\n" "Type 'exit' or Ctrl+C to quit.\n",
+ )
+
run_chat()
diff --git a/src/crewai/cli/crew_chat.py b/src/crewai/cli/crew_chat.py
index a38315620..cd0da2bb8 100644
--- a/src/crewai/cli/crew_chat.py
+++ b/src/crewai/cli/crew_chat.py
@@ -1,17 +1,52 @@
import json
+import platform
import re
import sys
+import threading
+import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple
import click
import tomli
+from packaging import version
+from crewai.cli.utils import read_toml
+from crewai.cli.version import get_crewai_version
from crewai.crew import Crew
from crewai.llm import LLM
from crewai.types.crew_chat import ChatInputField, ChatInputs
from crewai.utilities.llm_utils import create_llm
+MIN_REQUIRED_VERSION = "0.98.0"
+
+
+def check_conversational_crews_version(
+ crewai_version: str, pyproject_data: dict
+) -> bool:
+ """
+ Check if the installed crewAI version supports conversational crews.
+
+ Args:
+ crewai_version: The current version of crewAI.
+ pyproject_data: Dictionary containing pyproject.toml data.
+
+ Returns:
+ bool: True if version check passes, False otherwise.
+ """
+ try:
+ if version.parse(crewai_version) < version.parse(MIN_REQUIRED_VERSION):
+ click.secho(
+ "You are using an older version of crewAI that doesn't support conversational crews. "
+ "Run 'uv upgrade crewai' to get the latest version.",
+ fg="red",
+ )
+ return False
+ except version.InvalidVersion:
+ click.secho("Invalid crewAI version format detected.", fg="red")
+ return False
+ return True
+
def run_chat():
"""
@@ -19,20 +54,47 @@ def run_chat():
Incorporates crew_name, crew_description, and input fields to build a tool schema.
Exits if crew_name or crew_description are missing.
"""
+ crewai_version = get_crewai_version()
+ pyproject_data = read_toml()
+
+ if not check_conversational_crews_version(crewai_version, pyproject_data):
+ return
+
crew, crew_name = load_crew_and_name()
chat_llm = initialize_chat_llm(crew)
if not chat_llm:
return
- crew_chat_inputs = generate_crew_chat_inputs(crew, crew_name, chat_llm)
- crew_tool_schema = generate_crew_tool_schema(crew_chat_inputs)
- system_message = build_system_message(crew_chat_inputs)
-
- # Call the LLM to generate the introductory message
- introductory_message = chat_llm.call(
- messages=[{"role": "system", "content": system_message}]
+ # Indicate that the crew is being analyzed
+ click.secho(
+ "\nAnalyzing crew and required inputs - this may take 3 to 30 seconds "
+ "depending on the complexity of your crew.",
+ fg="white",
)
- click.secho(f"\nAssistant: {introductory_message}\n", fg="green")
+
+ # Start loading indicator
+ loading_complete = threading.Event()
+ loading_thread = threading.Thread(target=show_loading, args=(loading_complete,))
+ loading_thread.start()
+
+ try:
+ crew_chat_inputs = generate_crew_chat_inputs(crew, crew_name, chat_llm)
+ crew_tool_schema = generate_crew_tool_schema(crew_chat_inputs)
+ system_message = build_system_message(crew_chat_inputs)
+
+ # Call the LLM to generate the introductory message
+ introductory_message = chat_llm.call(
+ messages=[{"role": "system", "content": system_message}]
+ )
+ finally:
+ # Stop loading indicator
+ loading_complete.set()
+ loading_thread.join()
+
+ # Indicate that the analysis is complete
+ click.secho("\nFinished analyzing crew.\n", fg="white")
+
+ click.secho(f"Assistant: {introductory_message}\n", fg="green")
messages = [
{"role": "system", "content": system_message},
@@ -43,15 +105,17 @@ def run_chat():
crew_chat_inputs.crew_name: create_tool_function(crew, messages),
}
- click.secho(
- "\nEntering an interactive chat loop with function-calling.\n"
- "Type 'exit' or Ctrl+C to quit.\n",
- fg="cyan",
- )
-
chat_loop(chat_llm, messages, crew_tool_schema, available_functions)
+def show_loading(event: threading.Event):
+ """Display animated loading dots while processing."""
+ while not event.is_set():
+ print(".", end="", flush=True)
+ time.sleep(1)
+ print()
+
+
def initialize_chat_llm(crew: Crew) -> Optional[LLM]:
"""Initializes the chat LLM and handles exceptions."""
try:
@@ -85,7 +149,7 @@ def build_system_message(crew_chat_inputs: ChatInputs) -> str:
"Please keep your responses concise and friendly. "
"If a user asks a question outside the crew's scope, provide a brief answer and remind them of the crew's purpose. "
"After calling the tool, be prepared to take user feedback and make adjustments as needed. "
- "If you are ever unsure about a user's request or need clarification, ask the user for more information."
+ "If you are ever unsure about a user's request or need clarification, ask the user for more information. "
"Before doing anything else, introduce yourself with a friendly message like: 'Hey! I'm here to help you with [crew's purpose]. Could you please provide me with [inputs] so we can get started?' "
"For example: 'Hey! I'm here to help you with uncovering and reporting cutting-edge developments through thorough research and detailed analysis. Could you please provide me with a topic you're interested in? This will help us generate a comprehensive research report and detailed analysis.'"
f"\nCrew Name: {crew_chat_inputs.crew_name}"
@@ -102,25 +166,33 @@ def create_tool_function(crew: Crew, messages: List[Dict[str, str]]) -> Any:
return run_crew_tool_with_messages
+def flush_input():
+ """Flush any pending input from the user."""
+ if platform.system() == "Windows":
+ # Windows platform
+ import msvcrt
+
+ while msvcrt.kbhit():
+ msvcrt.getch()
+ else:
+ # Unix-like platforms (Linux, macOS)
+ import termios
+
+ termios.tcflush(sys.stdin, termios.TCIFLUSH)
+
+
def chat_loop(chat_llm, messages, crew_tool_schema, available_functions):
"""Main chat loop for interacting with the user."""
while True:
try:
- user_input = click.prompt("You", type=str)
- if user_input.strip().lower() in ["exit", "quit"]:
- click.echo("Exiting chat. Goodbye!")
- break
+ # Flush any pending input before accepting new input
+ flush_input()
- messages.append({"role": "user", "content": user_input})
- final_response = chat_llm.call(
- messages=messages,
- tools=[crew_tool_schema],
- available_functions=available_functions,
+ user_input = get_user_input()
+ handle_user_input(
+ user_input, chat_llm, messages, crew_tool_schema, available_functions
)
- messages.append({"role": "assistant", "content": final_response})
- click.secho(f"\nAssistant: {final_response}\n", fg="green")
-
except KeyboardInterrupt:
click.echo("\nExiting chat. Goodbye!")
break
@@ -129,6 +201,55 @@ def chat_loop(chat_llm, messages, crew_tool_schema, available_functions):
break
+def get_user_input() -> str:
+ """Collect multi-line user input with exit handling."""
+ click.secho(
+ "\nYou (type your message below. Press 'Enter' twice when you're done):",
+ fg="blue",
+ )
+ user_input_lines = []
+ while True:
+ line = input()
+ if line.strip().lower() == "exit":
+ return "exit"
+ if line == "":
+ break
+ user_input_lines.append(line)
+ return "\n".join(user_input_lines)
+
+
+def handle_user_input(
+ user_input: str,
+ chat_llm: LLM,
+ messages: List[Dict[str, str]],
+ crew_tool_schema: Dict[str, Any],
+ available_functions: Dict[str, Any],
+) -> None:
+ if user_input.strip().lower() == "exit":
+ click.echo("Exiting chat. Goodbye!")
+ return
+
+ if not user_input.strip():
+ click.echo("Empty message. Please provide input or type 'exit' to quit.")
+ return
+
+ messages.append({"role": "user", "content": user_input})
+
+ # Indicate that assistant is processing
+ click.echo()
+ click.secho("Assistant is processing your input. Please wait...", fg="green")
+
+ # Process assistant's response
+ final_response = chat_llm.call(
+ messages=messages,
+ tools=[crew_tool_schema],
+ available_functions=available_functions,
+ )
+
+ messages.append({"role": "assistant", "content": final_response})
+ click.secho(f"\nAssistant: {final_response}\n", fg="green")
+
+
def generate_crew_tool_schema(crew_inputs: ChatInputs) -> dict:
"""
Dynamically build a Littellm 'function' schema for the given crew.
@@ -323,10 +444,10 @@ def generate_input_description_with_ai(input_name: str, crew: Crew, chat_llm) ->
):
# Replace placeholders with input names
task_description = placeholder_pattern.sub(
- lambda m: m.group(1), task.description
+ lambda m: m.group(1), task.description or ""
)
expected_output = placeholder_pattern.sub(
- lambda m: m.group(1), task.expected_output
+ lambda m: m.group(1), task.expected_output or ""
)
context_texts.append(f"Task Description: {task_description}")
context_texts.append(f"Expected Output: {expected_output}")
@@ -337,10 +458,10 @@ def generate_input_description_with_ai(input_name: str, crew: Crew, chat_llm) ->
or f"{{{input_name}}}" in agent.backstory
):
# Replace placeholders with input names
- agent_role = placeholder_pattern.sub(lambda m: m.group(1), agent.role)
- agent_goal = placeholder_pattern.sub(lambda m: m.group(1), agent.goal)
+ agent_role = placeholder_pattern.sub(lambda m: m.group(1), agent.role or "")
+ agent_goal = placeholder_pattern.sub(lambda m: m.group(1), agent.goal or "")
agent_backstory = placeholder_pattern.sub(
- lambda m: m.group(1), agent.backstory
+ lambda m: m.group(1), agent.backstory or ""
)
context_texts.append(f"Agent Role: {agent_role}")
context_texts.append(f"Agent Goal: {agent_goal}")
@@ -381,18 +502,20 @@ def generate_crew_description_with_ai(crew: Crew, chat_llm) -> str:
for task in crew.tasks:
# Replace placeholders with input names
task_description = placeholder_pattern.sub(
- lambda m: m.group(1), task.description
+ lambda m: m.group(1), task.description or ""
)
expected_output = placeholder_pattern.sub(
- lambda m: m.group(1), task.expected_output
+ lambda m: m.group(1), task.expected_output or ""
)
context_texts.append(f"Task Description: {task_description}")
context_texts.append(f"Expected Output: {expected_output}")
for agent in crew.agents:
# Replace placeholders with input names
- agent_role = placeholder_pattern.sub(lambda m: m.group(1), agent.role)
- agent_goal = placeholder_pattern.sub(lambda m: m.group(1), agent.goal)
- agent_backstory = placeholder_pattern.sub(lambda m: m.group(1), agent.backstory)
+ agent_role = placeholder_pattern.sub(lambda m: m.group(1), agent.role or "")
+ agent_goal = placeholder_pattern.sub(lambda m: m.group(1), agent.goal or "")
+ agent_backstory = placeholder_pattern.sub(
+ lambda m: m.group(1), agent.backstory or ""
+ )
context_texts.append(f"Agent Role: {agent_role}")
context_texts.append(f"Agent Goal: {agent_goal}")
context_texts.append(f"Agent Backstory: {agent_backstory}")
diff --git a/src/crewai/cli/reset_memories_command.py b/src/crewai/cli/reset_memories_command.py
index 554232f52..4870d6424 100644
--- a/src/crewai/cli/reset_memories_command.py
+++ b/src/crewai/cli/reset_memories_command.py
@@ -2,11 +2,7 @@ import subprocess
import click
-from crewai.knowledge.storage.knowledge_storage import KnowledgeStorage
-from crewai.memory.entity.entity_memory import EntityMemory
-from crewai.memory.long_term.long_term_memory import LongTermMemory
-from crewai.memory.short_term.short_term_memory import ShortTermMemory
-from crewai.utilities.task_output_storage_handler import TaskOutputStorageHandler
+from crewai.cli.utils import get_crew
def reset_memories_command(
@@ -30,30 +26,35 @@ def reset_memories_command(
"""
try:
+ crew = get_crew()
+ if not crew:
+ raise ValueError("No crew found.")
if all:
- ShortTermMemory().reset()
- EntityMemory().reset()
- LongTermMemory().reset()
- TaskOutputStorageHandler().reset()
- KnowledgeStorage().reset()
+ crew.reset_memories(command_type="all")
click.echo("All memories have been reset.")
- else:
- if long:
- LongTermMemory().reset()
- click.echo("Long term memory has been reset.")
+ return
- if short:
- ShortTermMemory().reset()
- click.echo("Short term memory has been reset.")
- if entity:
- EntityMemory().reset()
- click.echo("Entity memory has been reset.")
- if kickoff_outputs:
- TaskOutputStorageHandler().reset()
- click.echo("Latest Kickoff outputs stored has been reset.")
- if knowledge:
- KnowledgeStorage().reset()
- click.echo("Knowledge has been reset.")
+ if not any([long, short, entity, kickoff_outputs, knowledge]):
+ click.echo(
+ "No memory type specified. Please specify at least one type to reset."
+ )
+ return
+
+ if long:
+ crew.reset_memories(command_type="long")
+ click.echo("Long term memory has been reset.")
+ if short:
+ crew.reset_memories(command_type="short")
+ click.echo("Short term memory has been reset.")
+ if entity:
+ crew.reset_memories(command_type="entity")
+ click.echo("Entity memory has been reset.")
+ if kickoff_outputs:
+ crew.reset_memories(command_type="kickoff_outputs")
+ click.echo("Latest Kickoff outputs stored has been reset.")
+ if knowledge:
+ crew.reset_memories(command_type="knowledge")
+ click.echo("Knowledge has been reset.")
except subprocess.CalledProcessError as e:
click.echo(f"An error occurred while resetting the memories: {e}", err=True)
diff --git a/src/crewai/cli/templates/crew/.gitignore b/src/crewai/cli/templates/crew/.gitignore
index d50a09fc9..7279347af 100644
--- a/src/crewai/cli/templates/crew/.gitignore
+++ b/src/crewai/cli/templates/crew/.gitignore
@@ -1,2 +1,3 @@
.env
__pycache__/
+.DS_Store
diff --git a/src/crewai/cli/templates/crew/main.py b/src/crewai/cli/templates/crew/main.py
index 104f146c0..d9fe85d42 100644
--- a/src/crewai/cli/templates/crew/main.py
+++ b/src/crewai/cli/templates/crew/main.py
@@ -56,7 +56,8 @@ def test():
Test the crew execution and returns the results.
"""
inputs = {
- "topic": "AI LLMs"
+ "topic": "AI LLMs",
+ "current_year": str(datetime.now().year)
}
try:
{{crew_name}}().crew().test(n_iterations=int(sys.argv[1]), openai_model_name=sys.argv[2], inputs=inputs)
diff --git a/src/crewai/cli/templates/crew/pyproject.toml b/src/crewai/cli/templates/crew/pyproject.toml
index bd2d871c5..a7d9c1425 100644
--- a/src/crewai/cli/templates/crew/pyproject.toml
+++ b/src/crewai/cli/templates/crew/pyproject.toml
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.13"
dependencies = [
- "crewai[tools]>=0.95.0,<1.0.0"
+ "crewai[tools]>=0.102.0,<1.0.0"
]
[project.scripts]
diff --git a/src/crewai/cli/templates/flow/.gitignore b/src/crewai/cli/templates/flow/.gitignore
index 02dc677b9..3b6f1bec0 100644
--- a/src/crewai/cli/templates/flow/.gitignore
+++ b/src/crewai/cli/templates/flow/.gitignore
@@ -1,3 +1,4 @@
.env
__pycache__/
lib/
+.DS_Store
diff --git a/src/crewai/cli/templates/flow/main.py b/src/crewai/cli/templates/flow/main.py
index 83f48ddd6..920b56c04 100644
--- a/src/crewai/cli/templates/flow/main.py
+++ b/src/crewai/cli/templates/flow/main.py
@@ -3,7 +3,7 @@ from random import randint
from pydantic import BaseModel
-from crewai.flow.flow import Flow, listen, start
+from crewai.flow import Flow, listen, start
from {{folder_name}}.crews.poem_crew.poem_crew import PoemCrew
diff --git a/src/crewai/cli/templates/flow/pyproject.toml b/src/crewai/cli/templates/flow/pyproject.toml
index 6ca589497..63635bb9a 100644
--- a/src/crewai/cli/templates/flow/pyproject.toml
+++ b/src/crewai/cli/templates/flow/pyproject.toml
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.13"
dependencies = [
- "crewai[tools]>=0.95.0,<1.0.0",
+ "crewai[tools]>=0.102.0,<1.0.0",
]
[project.scripts]
diff --git a/src/crewai/cli/templates/tool/pyproject.toml b/src/crewai/cli/templates/tool/pyproject.toml
index 0c4b88e9c..2606f6ddb 100644
--- a/src/crewai/cli/templates/tool/pyproject.toml
+++ b/src/crewai/cli/templates/tool/pyproject.toml
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
readme = "README.md"
requires-python = ">=3.10,<3.13"
dependencies = [
- "crewai[tools]>=0.95.0"
+ "crewai[tools]>=0.102.0"
]
[tool.crewai]
diff --git a/src/crewai/cli/utils.py b/src/crewai/cli/utils.py
index a385e1f37..60eb2488a 100644
--- a/src/crewai/cli/utils.py
+++ b/src/crewai/cli/utils.py
@@ -9,6 +9,7 @@ import tomli
from rich.console import Console
from crewai.cli.constants import ENV_VARS
+from crewai.crew import Crew
if sys.version_info >= (3, 11):
import tomllib
@@ -247,3 +248,64 @@ def write_env_file(folder_path, env_vars):
with open(env_file_path, "w") as file:
for key, value in env_vars.items():
file.write(f"{key}={value}\n")
+
+
+def get_crew(crew_path: str = "crew.py", require: bool = False) -> Crew | None:
+ """Get the crew instance from the crew.py file."""
+ try:
+ import importlib.util
+ import os
+
+ for root, _, files in os.walk("."):
+ if "crew.py" in files:
+ crew_path = os.path.join(root, "crew.py")
+ try:
+ spec = importlib.util.spec_from_file_location(
+ "crew_module", crew_path
+ )
+ if not spec or not spec.loader:
+ continue
+ module = importlib.util.module_from_spec(spec)
+ try:
+ sys.modules[spec.name] = module
+ spec.loader.exec_module(module)
+
+ for attr_name in dir(module):
+ attr = getattr(module, attr_name)
+ try:
+ if callable(attr) and hasattr(attr, "crew"):
+ crew_instance = attr().crew()
+ return crew_instance
+
+ except Exception as e:
+ print(f"Error processing attribute {attr_name}: {e}")
+ continue
+
+ except Exception as exec_error:
+ print(f"Error executing module: {exec_error}")
+ import traceback
+
+ print(f"Traceback: {traceback.format_exc()}")
+
+ except (ImportError, AttributeError) as e:
+ if require:
+ console.print(
+ f"Error importing crew from {crew_path}: {str(e)}",
+ style="bold red",
+ )
+ continue
+
+ break
+
+ if require:
+ console.print("No valid Crew instance found in crew.py", style="bold red")
+ raise SystemExit
+ return None
+
+ except Exception as e:
+ if require:
+ console.print(
+ f"Unexpected error while loading crew: {str(e)}", style="bold red"
+ )
+ raise SystemExit
+ return None
diff --git a/src/crewai/crew.py b/src/crewai/crew.py
index 38b96a0e0..9ae9ce2c0 100644
--- a/src/crewai/crew.py
+++ b/src/crewai/crew.py
@@ -4,6 +4,7 @@ import re
import uuid
import warnings
from concurrent.futures import Future
+from copy import copy as shallow_copy
from hashlib import md5
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
@@ -37,7 +38,6 @@ from crewai.tasks.task_output import TaskOutput
from crewai.telemetry import Telemetry
from crewai.tools.agent_tools.agent_tools import AgentTools
from crewai.tools.base_tool import Tool
-from crewai.types.crew_chat import ChatInputs
from crewai.types.usage_metrics import UsageMetrics
from crewai.utilities import I18N, FileHandler, Logger, RPMController
from crewai.utilities.constants import TRAINING_DATA_FILE
@@ -84,6 +84,7 @@ class Crew(BaseModel):
step_callback: Callback to be executed after each step for every agents execution.
share_crew: Whether you want to share the complete crew information and execution with crewAI to make the library better, and allow us to train models.
planning: Plan the crew execution and add the plan to the crew.
+ chat_llm: The language model used for orchestrating chat interactions with the crew.
"""
__hash__ = object.__hash__ # type: ignore
@@ -182,9 +183,9 @@ class Crew(BaseModel):
default=None,
description="Path to the prompt json file to be used for the crew.",
)
- output_log_file: Optional[str] = Field(
+ output_log_file: Optional[Union[bool, str]] = Field(
default=None,
- description="output_log_file",
+ description="Path to the log file to be saved",
)
planning: Optional[bool] = Field(
default=False,
@@ -210,8 +211,9 @@ class Crew(BaseModel):
default=None,
description="LLM used to handle chatting with the crew.",
)
- _knowledge: Optional[Knowledge] = PrivateAttr(
+ knowledge: Optional[Knowledge] = Field(
default=None,
+ description="Knowledge for the crew.",
)
@field_validator("id", mode="before")
@@ -289,9 +291,9 @@ class Crew(BaseModel):
if isinstance(self.knowledge_sources, list) and all(
isinstance(k, BaseKnowledgeSource) for k in self.knowledge_sources
):
- self._knowledge = Knowledge(
+ self.knowledge = Knowledge(
sources=self.knowledge_sources,
- embedder_config=self.embedder,
+ embedder=self.embedder,
collection_name="crew",
)
@@ -378,6 +380,22 @@ class Crew(BaseModel):
return self
+ @model_validator(mode="after")
+ def validate_must_have_non_conditional_task(self) -> "Crew":
+ """Ensure that a crew has at least one non-conditional task."""
+ if not self.tasks:
+ return self
+ non_conditional_count = sum(
+ 1 for task in self.tasks if not isinstance(task, ConditionalTask)
+ )
+ if non_conditional_count == 0:
+ raise PydanticCustomError(
+ "only_conditional_tasks",
+ "Crew must include at least one non-conditional task",
+ {},
+ )
+ return self
+
@model_validator(mode="after")
def validate_first_task(self) -> "Crew":
"""Ensure the first task is not a ConditionalTask."""
@@ -437,6 +455,8 @@ class Crew(BaseModel):
)
return self
+
+
@property
def key(self) -> str:
source = [agent.key for agent in self.agents] + [
@@ -492,21 +512,26 @@ class Crew(BaseModel):
train_crew = self.copy()
train_crew._setup_for_training(filename)
- for n_iteration in range(n_iterations):
- train_crew._train_iteration = n_iteration
- train_crew.kickoff(inputs=inputs)
+ try:
+ for n_iteration in range(n_iterations):
+ train_crew._train_iteration = n_iteration
+ train_crew.kickoff(inputs=inputs)
- training_data = CrewTrainingHandler(TRAINING_DATA_FILE).load()
+ training_data = CrewTrainingHandler(TRAINING_DATA_FILE).load()
- for agent in train_crew.agents:
- if training_data.get(str(agent.id)):
- result = TaskEvaluator(agent).evaluate_training_data(
- training_data=training_data, agent_id=str(agent.id)
- )
-
- CrewTrainingHandler(filename).save_trained_data(
- agent_id=str(agent.role), trained_data=result.model_dump()
- )
+ for agent in train_crew.agents:
+ if training_data.get(str(agent.id)):
+ result = TaskEvaluator(agent).evaluate_training_data(
+ training_data=training_data, agent_id=str(agent.id)
+ )
+ CrewTrainingHandler(filename).save_trained_data(
+ agent_id=str(agent.role), trained_data=result.model_dump()
+ )
+ except Exception as e:
+ self._logger.log("error", f"Training failed: {e}", color="red")
+ CrewTrainingHandler(TRAINING_DATA_FILE).clear()
+ CrewTrainingHandler(filename).clear()
+ raise
def kickoff(
self,
@@ -674,12 +699,7 @@ class Crew(BaseModel):
manager.tools = []
raise Exception("Manager agent should not have tools")
else:
- self.manager_llm = (
- getattr(self.manager_llm, "model_name", None)
- or getattr(self.manager_llm, "model", None)
- or getattr(self.manager_llm, "deployment_name", None)
- or self.manager_llm
- )
+ self.manager_llm = create_llm(self.manager_llm)
manager = Agent(
role=i18n.retrieve("hierarchical_manager_agent", "role"),
goal=i18n.retrieve("hierarchical_manager_agent", "goal"),
@@ -739,6 +759,7 @@ class Crew(BaseModel):
task, task_outputs, futures, task_index, was_replayed
)
if skipped_task_output:
+ task_outputs.append(skipped_task_output)
continue
if task.async_execution:
@@ -762,7 +783,7 @@ class Crew(BaseModel):
context=context,
tools=tools_for_task,
)
- task_outputs = [task_output]
+ task_outputs.append(task_output)
self._process_task_result(task, task_output)
self._store_execution_log(task, task_output, task_index, was_replayed)
@@ -783,7 +804,7 @@ class Crew(BaseModel):
task_outputs = self._process_async_tasks(futures, was_replayed)
futures.clear()
- previous_output = task_outputs[task_index - 1] if task_outputs else None
+ previous_output = task_outputs[-1] if task_outputs else None
if previous_output is not None and not task.should_execute(previous_output):
self._logger.log(
"debug",
@@ -905,11 +926,15 @@ class Crew(BaseModel):
)
def _create_crew_output(self, task_outputs: List[TaskOutput]) -> CrewOutput:
- if len(task_outputs) != 1:
- raise ValueError(
- "Something went wrong. Kickoff should return only one task output."
- )
- final_task_output = task_outputs[0]
+ if not task_outputs:
+ raise ValueError("No task outputs available to create crew output.")
+
+ # Filter out empty outputs and get the last valid one as the main output
+ valid_outputs = [t for t in task_outputs if t.raw]
+ if not valid_outputs:
+ raise ValueError("No valid task outputs available to create crew output.")
+ final_task_output = valid_outputs[-1]
+
final_string_output = final_task_output.raw
self._finish_execution(final_string_output)
token_usage = self.calculate_usage_metrics()
@@ -918,7 +943,7 @@ class Crew(BaseModel):
raw=final_task_output.raw,
pydantic=final_task_output.pydantic,
json_dict=final_task_output.json_dict,
- tasks_output=[task.output for task in self.tasks if task.output],
+ tasks_output=task_outputs,
token_usage=token_usage,
)
@@ -991,8 +1016,8 @@ class Crew(BaseModel):
return result
def query_knowledge(self, query: List[str]) -> Union[List[Dict[str, Any]], None]:
- if self._knowledge:
- return self._knowledge.query(query)
+ if self.knowledge:
+ return self.knowledge.query(query)
return None
def fetch_inputs(self) -> Set[str]:
@@ -1036,6 +1061,8 @@ class Crew(BaseModel):
"_telemetry",
"agents",
"tasks",
+ "knowledge_sources",
+ "knowledge",
}
cloned_agents = [agent.copy() for agent in self.agents]
@@ -1043,6 +1070,9 @@ class Crew(BaseModel):
task_mapping = {}
cloned_tasks = []
+ existing_knowledge_sources = shallow_copy(self.knowledge_sources)
+ existing_knowledge = shallow_copy(self.knowledge)
+
for task in self.tasks:
cloned_task = task.copy(cloned_agents, task_mapping)
cloned_tasks.append(cloned_task)
@@ -1062,7 +1092,13 @@ class Crew(BaseModel):
copied_data.pop("agents", None)
copied_data.pop("tasks", None)
- copied_crew = Crew(**copied_data, agents=cloned_agents, tasks=cloned_tasks)
+ copied_crew = Crew(
+ **copied_data,
+ agents=cloned_agents,
+ tasks=cloned_tasks,
+ knowledge_sources=existing_knowledge_sources,
+ knowledge=existing_knowledge,
+ )
return copied_crew
@@ -1134,3 +1170,80 @@ class Crew(BaseModel):
def __repr__(self):
return f"Crew(id={self.id}, process={self.process}, number_of_agents={len(self.agents)}, number_of_tasks={len(self.tasks)})"
+
+ def reset_memories(self, command_type: str) -> None:
+ """Reset specific or all memories for the crew.
+
+ Args:
+ command_type: Type of memory to reset.
+ Valid options: 'long', 'short', 'entity', 'knowledge',
+ 'kickoff_outputs', or 'all'
+
+ Raises:
+ ValueError: If an invalid command type is provided.
+ RuntimeError: If memory reset operation fails.
+ """
+ VALID_TYPES = frozenset(
+ ["long", "short", "entity", "knowledge", "kickoff_outputs", "all"]
+ )
+
+ if command_type not in VALID_TYPES:
+ raise ValueError(
+ f"Invalid command type. Must be one of: {', '.join(sorted(VALID_TYPES))}"
+ )
+
+ try:
+ if command_type == "all":
+ self._reset_all_memories()
+ else:
+ self._reset_specific_memory(command_type)
+
+ self._logger.log("info", f"{command_type} memory has been reset")
+
+ except Exception as e:
+ error_msg = f"Failed to reset {command_type} memory: {str(e)}"
+ self._logger.log("error", error_msg)
+ raise RuntimeError(error_msg) from e
+
+ def _reset_all_memories(self) -> None:
+ """Reset all available memory systems."""
+ memory_systems = [
+ ("short term", self._short_term_memory),
+ ("entity", self._entity_memory),
+ ("long term", self._long_term_memory),
+ ("task output", self._task_output_handler),
+ ("knowledge", self.knowledge),
+ ]
+
+ for name, system in memory_systems:
+ if system is not None:
+ try:
+ system.reset()
+ except Exception as e:
+ raise RuntimeError(f"Failed to reset {name} memory") from e
+
+ def _reset_specific_memory(self, memory_type: str) -> None:
+ """Reset a specific memory system.
+
+ Args:
+ memory_type: Type of memory to reset
+
+ Raises:
+ RuntimeError: If the specified memory system fails to reset
+ """
+ reset_functions = {
+ "long": (self._long_term_memory, "long term"),
+ "short": (self._short_term_memory, "short term"),
+ "entity": (self._entity_memory, "entity"),
+ "knowledge": (self.knowledge, "knowledge"),
+ "kickoff_outputs": (self._task_output_handler, "task output"),
+ }
+
+ memory_system, name = reset_functions[memory_type]
+ if memory_system is None:
+ raise RuntimeError(f"{name} memory system is not initialized")
+
+ try:
+ memory_system.reset()
+ except Exception as e:
+ raise RuntimeError(f"Failed to reset {name} memory") from e
diff --git a/src/crewai/flow/__init__.py b/src/crewai/flow/__init__.py
index b8c530bc3..48a49666d 100644
--- a/src/crewai/flow/__init__.py
+++ b/src/crewai/flow/__init__.py
@@ -1,3 +1,5 @@
-from crewai.flow.flow import Flow
+from crewai.flow.flow import Flow, start, listen, or_, and_, router
+from crewai.flow.persistence import persist
+
+__all__ = ["Flow", "start", "listen", "or_", "and_", "router", "persist"]
-__all__ = ["Flow"]
diff --git a/src/crewai/flow/flow.py b/src/crewai/flow/flow.py
index ef688b9c1..f1242a2bf 100644
--- a/src/crewai/flow/flow.py
+++ b/src/crewai/flow/flow.py
@@ -1,6 +1,7 @@
import asyncio
+import copy
import inspect
-import uuid
+import logging
from typing import (
Any,
Callable,
@@ -13,7 +14,6 @@ from typing import (
TypeVar,
Union,
cast,
- overload,
)
from uuid import uuid4
@@ -27,54 +27,68 @@ from crewai.flow.flow_events import (
MethodExecutionStartedEvent,
)
from crewai.flow.flow_visualizer import plot_flow
-from crewai.flow.persistence import FlowPersistence
from crewai.flow.persistence.base import FlowPersistence
from crewai.flow.utils import get_possible_return_constants
from crewai.telemetry import Telemetry
+from crewai.utilities.printer import Printer
+
+logger = logging.getLogger(__name__)
class FlowState(BaseModel):
"""Base model for all flow states, ensuring each state has a unique ID."""
- id: str = Field(default_factory=lambda: str(uuid4()), description="Unique identifier for the flow state")
+
+ id: str = Field(
+ default_factory=lambda: str(uuid4()),
+ description="Unique identifier for the flow state",
+ )
+
# Type variables with explicit bounds
-T = TypeVar("T", bound=Union[Dict[str, Any], BaseModel]) # Generic flow state type parameter
-StateT = TypeVar("StateT", bound=Union[Dict[str, Any], BaseModel]) # State validation type parameter
+T = TypeVar(
+ "T", bound=Union[Dict[str, Any], BaseModel]
+) # Generic flow state type parameter
+StateT = TypeVar(
+ "StateT", bound=Union[Dict[str, Any], BaseModel]
+) # State validation type parameter
+
def ensure_state_type(state: Any, expected_type: Type[StateT]) -> StateT:
"""Ensure state matches expected type with proper validation.
-
+
Args:
state: State instance to validate
expected_type: Expected type for the state
-
+
Returns:
Validated state instance
-
+
Raises:
TypeError: If state doesn't match expected type
ValueError: If state validation fails
"""
"""Ensure state matches expected type with proper validation.
-
+
Args:
state: State instance to validate
expected_type: Expected type for the state
-
+
Returns:
Validated state instance
-
+
Raises:
TypeError: If state doesn't match expected type
ValueError: If state validation fails
"""
- if expected_type == dict:
+ if expected_type is dict:
if not isinstance(state, dict):
raise TypeError(f"Expected dict, got {type(state).__name__}")
return cast(StateT, state)
if isinstance(expected_type, type) and issubclass(expected_type, BaseModel):
if not isinstance(state, expected_type):
- raise TypeError(f"Expected {expected_type.__name__}, got {type(state).__name__}")
+ raise TypeError(
+ f"Expected {expected_type.__name__}, got {type(state).__name__}"
+ )
return cast(StateT, state)
raise TypeError(f"Invalid expected_type: {expected_type}")
@@ -120,6 +134,7 @@ def start(condition: Optional[Union[str, dict, Callable]] = None) -> Callable:
>>> def complex_start(self):
... pass
"""
+
def decorator(func):
func.__is_start_method__ = True
if condition is not None:
@@ -144,6 +159,7 @@ def start(condition: Optional[Union[str, dict, Callable]] = None) -> Callable:
return decorator
+
def listen(condition: Union[str, dict, Callable]) -> Callable:
"""
Creates a listener that executes when specified conditions are met.
@@ -180,6 +196,7 @@ def listen(condition: Union[str, dict, Callable]) -> Callable:
>>> def handle_completion(self):
... pass
"""
+
def decorator(func):
if isinstance(condition, str):
func.__trigger_methods__ = [condition]
@@ -244,6 +261,7 @@ def router(condition: Union[str, dict, Callable]) -> Callable:
... return CONTINUE
... return STOP
"""
+
def decorator(func):
func.__is_router__ = True
if isinstance(condition, str):
@@ -267,6 +285,7 @@ def router(condition: Union[str, dict, Callable]) -> Callable:
return decorator
+
def or_(*conditions: Union[str, dict, Callable]) -> dict:
"""
Combines multiple conditions with OR logic for flow control.
@@ -370,22 +389,26 @@ class FlowMeta(type):
for attr_name, attr_value in dct.items():
# Check for any flow-related attributes
- if (hasattr(attr_value, "__is_flow_method__") or
- hasattr(attr_value, "__is_start_method__") or
- hasattr(attr_value, "__trigger_methods__") or
- hasattr(attr_value, "__is_router__")):
-
+ if (
+ hasattr(attr_value, "__is_flow_method__")
+ or hasattr(attr_value, "__is_start_method__")
+ or hasattr(attr_value, "__trigger_methods__")
+ or hasattr(attr_value, "__is_router__")
+ ):
# Register start methods
if hasattr(attr_value, "__is_start_method__"):
start_methods.append(attr_name)
-
+
# Register listeners and routers
if hasattr(attr_value, "__trigger_methods__"):
methods = attr_value.__trigger_methods__
condition_type = getattr(attr_value, "__condition_type__", "OR")
listeners[attr_name] = (condition_type, methods)
-
- if hasattr(attr_value, "__is_router__") and attr_value.__is_router__:
+
+ if (
+ hasattr(attr_value, "__is_router__")
+ and attr_value.__is_router__
+ ):
routers.add(attr_name)
possible_returns = get_possible_return_constants(attr_value)
if possible_returns:
@@ -401,9 +424,11 @@ class FlowMeta(type):
class Flow(Generic[T], metaclass=FlowMeta):
"""Base class for all flows.
-
+
Type parameter T must be either Dict[str, Any] or a subclass of BaseModel."""
+
_telemetry = Telemetry()
+ _printer = Printer()
_start_methods: List[str] = []
_listeners: Dict[str, tuple[str, List[str]]] = {}
@@ -422,14 +447,12 @@ class Flow(Generic[T], metaclass=FlowMeta):
def __init__(
self,
persistence: Optional[FlowPersistence] = None,
- restore_uuid: Optional[str] = None,
**kwargs: Any,
) -> None:
"""Initialize a new Flow instance.
-
+
Args:
persistence: Optional persistence backend for storing flow states
- restore_uuid: Optional UUID to restore state from persistence
**kwargs: Additional state values to initialize or override
"""
# Initialize basic instance attributes
@@ -438,54 +461,13 @@ class Flow(Generic[T], metaclass=FlowMeta):
self._pending_and_listeners: Dict[str, Set[str]] = {}
self._method_outputs: List[Any] = [] # List to store all method outputs
self._persistence: Optional[FlowPersistence] = persistence
-
- # Validate state model before initialization
- if isinstance(self.initial_state, type):
- if issubclass(self.initial_state, BaseModel) and not issubclass(self.initial_state, FlowState):
- # Check if model has id field
- model_fields = getattr(self.initial_state, "model_fields", None)
- if not model_fields or "id" not in model_fields:
- raise ValueError("Flow state model must have an 'id' field")
-
- # Handle persistence and potential ID conflicts
- stored_state = None
- if self._persistence is not None:
- if restore_uuid and kwargs and "id" in kwargs and restore_uuid != kwargs["id"]:
- raise ValueError(
- f"Conflicting IDs provided: restore_uuid='{restore_uuid}' "
- f"vs kwargs['id']='{kwargs['id']}'. Use only one ID for restoration."
- )
-
- # Attempt to load state, prioritizing restore_uuid
- if restore_uuid:
- stored_state = self._persistence.load_state(restore_uuid)
- if not stored_state:
- raise ValueError(f"No state found for restore_uuid='{restore_uuid}'")
- elif kwargs and "id" in kwargs:
- stored_state = self._persistence.load_state(kwargs["id"])
- if not stored_state:
- # For kwargs["id"], we allow creating new state if not found
- self._state = self._create_initial_state()
- if kwargs:
- self._initialize_state(kwargs)
- return
-
- # Initialize state based on persistence and kwargs
- if stored_state:
- # Create initial state and restore from persistence
- self._state = self._create_initial_state()
- self._restore_state(stored_state)
- # Apply any additional kwargs to override specific fields
- if kwargs:
- filtered_kwargs = {k: v for k, v in kwargs.items() if k != "id"}
- if filtered_kwargs:
- self._initialize_state(filtered_kwargs)
- else:
- # No stored state, create new state with initial values
- self._state = self._create_initial_state()
- # Apply any additional kwargs
- if kwargs:
- self._initialize_state(kwargs)
+
+ # Initialize state with initial values
+ self._state = self._create_initial_state()
+
+ # Apply any additional kwargs
+ if kwargs:
+ self._initialize_state(kwargs)
self._telemetry.flow_creation_span(self.__class__.__name__)
@@ -494,23 +476,23 @@ class Flow(Generic[T], metaclass=FlowMeta):
if not method_name.startswith("_"):
method = getattr(self, method_name)
# Check for any flow-related attributes
- if (hasattr(method, "__is_flow_method__") or
- hasattr(method, "__is_start_method__") or
- hasattr(method, "__trigger_methods__") or
- hasattr(method, "__is_router__")):
+ if (
+ hasattr(method, "__is_flow_method__")
+ or hasattr(method, "__is_start_method__")
+ or hasattr(method, "__trigger_methods__")
+ or hasattr(method, "__is_router__")
+ ):
# Ensure method is bound to this instance
if not hasattr(method, "__self__"):
method = method.__get__(self, self.__class__)
self._methods[method_name] = method
-
-
def _create_initial_state(self) -> T:
"""Create and initialize flow state with UUID and default values.
-
+
Returns:
New state instance with UUID and default values initialized
-
+
Raises:
ValueError: If structured state model lacks 'id' field
TypeError: If state is neither BaseModel nor dictionary
@@ -522,24 +504,25 @@ class Flow(Generic[T], metaclass=FlowMeta):
if issubclass(state_type, FlowState):
# Create instance without id, then set it
instance = state_type()
- if not hasattr(instance, 'id'):
- setattr(instance, 'id', str(uuid4()))
+ if not hasattr(instance, "id"):
+ setattr(instance, "id", str(uuid4()))
return cast(T, instance)
elif issubclass(state_type, BaseModel):
# Create a new type that includes the ID field
class StateWithId(state_type, FlowState): # type: ignore
pass
+
instance = StateWithId()
- if not hasattr(instance, 'id'):
- setattr(instance, 'id', str(uuid4()))
+ if not hasattr(instance, "id"):
+ setattr(instance, "id", str(uuid4()))
return cast(T, instance)
- elif state_type == dict:
- return cast(T, {"id": str(uuid4())}) # Minimal dict state
-
+ elif state_type is dict:
+ return cast(T, {"id": str(uuid4())})
+
# Handle case where no initial state is provided
if self.initial_state is None:
return cast(T, {"id": str(uuid4())})
-
+
# Handle case where initial_state is a type (class)
if isinstance(self.initial_state, type):
if issubclass(self.initial_state, FlowState):
@@ -550,22 +533,22 @@ class Flow(Generic[T], metaclass=FlowMeta):
if not model_fields or "id" not in model_fields:
raise ValueError("Flow state model must have an 'id' field")
return cast(T, self.initial_state()) # Uses model defaults
- elif self.initial_state == dict:
+ elif self.initial_state is dict:
return cast(T, {"id": str(uuid4())})
-
+
# Handle dictionary instance case
if isinstance(self.initial_state, dict):
new_state = dict(self.initial_state) # Copy to avoid mutations
if "id" not in new_state:
new_state["id"] = str(uuid4())
return cast(T, new_state)
-
+
# Handle BaseModel instance case
if isinstance(self.initial_state, BaseModel):
model = cast(BaseModel, self.initial_state)
if not hasattr(model, "id"):
raise ValueError("Flow state model must have an 'id' field")
-
+
# Create new instance with same values to avoid mutations
if hasattr(model, "model_dump"):
# Pydantic v2
@@ -576,63 +559,18 @@ class Flow(Generic[T], metaclass=FlowMeta):
else:
# Fallback for other BaseModel implementations
state_dict = {
- k: v for k, v in model.__dict__.items()
- if not k.startswith("_")
+ k: v for k, v in model.__dict__.items() if not k.startswith("_")
}
-
+
# Create new instance of the same class
model_class = type(model)
return cast(T, model_class(**state_dict))
-
raise TypeError(
f"Initial state must be dict or BaseModel, got {type(self.initial_state)}"
)
- # Handle case where initial_state is None but we have a type parameter
- if self.initial_state is None and hasattr(self, "_initial_state_T"):
- state_type = getattr(self, "_initial_state_T")
- if isinstance(state_type, type):
- if issubclass(state_type, FlowState):
- return cast(T, state_type())
- elif issubclass(state_type, BaseModel):
- # Create a new type that includes the ID field
- class StateWithId(state_type, FlowState): # type: ignore
- pass
- return cast(T, StateWithId())
- elif state_type == dict:
- return cast(T, {"id": str(uuid4())})
- # Handle case where no initial state is provided
- if self.initial_state is None:
- return cast(T, {"id": str(uuid4())})
-
- # Handle case where initial_state is a type (class)
- if isinstance(self.initial_state, type):
- if issubclass(self.initial_state, FlowState):
- return cast(T, self.initial_state())
- elif issubclass(self.initial_state, BaseModel):
- # Validate that the model has an id field
- model_fields = getattr(self.initial_state, "model_fields", None)
- if not model_fields or "id" not in model_fields:
- raise ValueError("Flow state model must have an 'id' field")
- return cast(T, self.initial_state())
- elif self.initial_state == dict:
- return cast(T, {"id": str(uuid4())})
-
- # Handle dictionary instance case
- if isinstance(self.initial_state, dict):
- if "id" not in self.initial_state:
- self.initial_state["id"] = str(uuid4())
- return cast(T, dict(self.initial_state)) # Create new dict to avoid mutations
-
- # Handle BaseModel instance case
- if isinstance(self.initial_state, BaseModel):
- if not hasattr(self.initial_state, "id"):
- raise ValueError("Flow state model must have an 'id' field")
- return cast(T, self.initial_state)
-
- raise TypeError(
- f"Initial state must be dict or BaseModel, got {type(self.initial_state)}"
- )
+ def _copy_state(self) -> T:
+ return copy.deepcopy(self._state)
@property
def state(self) -> T:
@@ -643,12 +581,45 @@ class Flow(Generic[T], metaclass=FlowMeta):
"""Returns the list of all outputs from executed methods."""
return self._method_outputs
+ @property
+ def flow_id(self) -> str:
+ """Returns the unique identifier of this flow instance.
+
+ This property provides a consistent way to access the flow's unique identifier
+ regardless of the underlying state implementation (dict or BaseModel).
+
+ Returns:
+ str: The flow's unique identifier, or an empty string if not found
+
+ Note:
+ This property safely handles both dictionary and BaseModel state types,
+ returning an empty string if the ID cannot be retrieved rather than raising
+ an exception.
+
+ Example:
+ ```python
+ flow = MyFlow()
+ print(f"Current flow ID: {flow.flow_id}") # Safely get flow ID
+ ```
+ """
+ try:
+ if not hasattr(self, "_state"):
+ return ""
+
+ if isinstance(self._state, dict):
+ return str(self._state.get("id", ""))
+ elif isinstance(self._state, BaseModel):
+ return str(getattr(self._state, "id", ""))
+ return ""
+ except (AttributeError, TypeError):
+ return "" # Safely handle any unexpected attribute access issues
+
def _initialize_state(self, inputs: Dict[str, Any]) -> None:
"""Initialize or update flow state with new inputs.
-
+
Args:
inputs: Dictionary of state values to set/update
-
+
Raises:
ValueError: If validation fails for structured state
TypeError: If state is neither BaseModel nor dictionary
@@ -675,13 +646,12 @@ class Flow(Generic[T], metaclass=FlowMeta):
current_state = model.dict()
else:
current_state = {
- k: v for k, v in model.__dict__.items()
- if not k.startswith("_")
+ k: v for k, v in model.__dict__.items() if not k.startswith("_")
}
-
+
# Create new state with preserved fields and updates
new_state = {**current_state, **inputs}
-
+
# Create new instance with merged state
model_class = type(model)
if hasattr(model_class, "model_validate"):
@@ -697,13 +667,13 @@ class Flow(Generic[T], metaclass=FlowMeta):
raise ValueError(f"Invalid inputs for structured state: {e}") from e
else:
raise TypeError("State must be a BaseModel instance or a dictionary.")
-
+
def _restore_state(self, stored_state: Dict[str, Any]) -> None:
"""Restore flow state from persistence.
-
+
Args:
stored_state: Previously stored state to restore
-
+
Raises:
ValueError: If validation fails for structured state
TypeError: If state is neither BaseModel nor dictionary
@@ -712,7 +682,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
stored_id = stored_state.get("id")
if not stored_id:
raise ValueError("Stored state must have an 'id' field")
-
+
if isinstance(self._state, dict):
# For dict states, update all fields from stored state
self._state.clear()
@@ -730,21 +700,59 @@ class Flow(Generic[T], metaclass=FlowMeta):
# Fallback for other BaseModel implementations
self._state = cast(T, type(model)(**stored_state))
else:
- raise TypeError(
- f"State must be dict or BaseModel, got {type(self._state)}"
- )
+ raise TypeError(f"State must be dict or BaseModel, got {type(self._state)}")
def kickoff(self, inputs: Optional[Dict[str, Any]] = None) -> Any:
+ """Start the flow execution.
+
+ Args:
+ inputs: Optional dictionary containing input values and potentially a state ID to restore
+ """
+ # Handle state restoration if ID is provided in inputs
+ if inputs and "id" in inputs and self._persistence is not None:
+ restore_uuid = inputs["id"]
+ stored_state = self._persistence.load_state(restore_uuid)
+
+ # Override the id in the state if it exists in inputs
+ if "id" in inputs:
+ if isinstance(self._state, dict):
+ self._state["id"] = inputs["id"]
+ elif isinstance(self._state, BaseModel):
+ setattr(self._state, "id", inputs["id"])
+
+ if stored_state:
+ self._log_flow_event(
+ f"Loading flow state from memory for UUID: {restore_uuid}",
+ color="yellow",
+ )
+ # Restore the state
+ self._restore_state(stored_state)
+ else:
+ self._log_flow_event(
+ f"No flow state found for UUID: {restore_uuid}", color="red"
+ )
+
+ # Apply any additional inputs after restoration
+ filtered_inputs = {k: v for k, v in inputs.items() if k != "id"}
+ if filtered_inputs:
+ self._initialize_state(filtered_inputs)
+
+ # Start flow execution
self.event_emitter.send(
self,
event=FlowStartedEvent(
type="flow_started",
flow_name=self.__class__.__name__,
+ inputs=inputs,
),
)
+ self._log_flow_event(
+ f"Flow started with ID: {self.flow_id}", color="bold_magenta"
+ )
- if inputs is not None:
+ if inputs is not None and "id" not in inputs:
self._initialize_state(inputs)
+
return asyncio.run(self.kickoff_async())
async def kickoff_async(self, inputs: Optional[Dict[str, Any]] = None) -> Any:
@@ -799,6 +807,18 @@ class Flow(Generic[T], metaclass=FlowMeta):
async def _execute_method(
self, method_name: str, method: Callable, *args: Any, **kwargs: Any
) -> Any:
+ dumped_params = {f"_{i}": arg for i, arg in enumerate(args)} | (kwargs or {})
+ self.event_emitter.send(
+ self,
+ event=MethodExecutionStartedEvent(
+ type="method_execution_started",
+ method_name=method_name,
+ flow_name=self.__class__.__name__,
+ params=dumped_params,
+ state=self._copy_state(),
+ ),
+ )
+
result = (
await method(*args, **kwargs)
if asyncio.iscoroutinefunction(method)
@@ -808,6 +828,18 @@ class Flow(Generic[T], metaclass=FlowMeta):
self._method_execution_counts[method_name] = (
self._method_execution_counts.get(method_name, 0) + 1
)
+
+ self.event_emitter.send(
+ self,
+ event=MethodExecutionFinishedEvent(
+ type="method_execution_finished",
+ method_name=method_name,
+ flow_name=self.__class__.__name__,
+ state=self._copy_state(),
+ result=result,
+ ),
+ )
+
return result
async def _execute_listeners(self, trigger_method: str, result: Any) -> None:
@@ -946,16 +978,6 @@ class Flow(Generic[T], metaclass=FlowMeta):
"""
try:
method = self._methods[listener_name]
-
- self.event_emitter.send(
- self,
- event=MethodExecutionStartedEvent(
- type="method_execution_started",
- method_name=listener_name,
- flow_name=self.__class__.__name__,
- ),
- )
-
sig = inspect.signature(method)
params = list(sig.parameters.values())
method_params = [p for p in params if p.name != "self"]
@@ -967,15 +989,6 @@ class Flow(Generic[T], metaclass=FlowMeta):
else:
listener_result = await self._execute_method(listener_name, method)
- self.event_emitter.send(
- self,
- event=MethodExecutionFinishedEvent(
- type="method_execution_finished",
- method_name=listener_name,
- flow_name=self.__class__.__name__,
- ),
- )
-
# Execute listeners (and possibly routers) of this listener
await self._execute_listeners(listener_name, listener_result)
@@ -987,6 +1000,32 @@ class Flow(Generic[T], metaclass=FlowMeta):
traceback.print_exc()
+ def _log_flow_event(
+ self, message: str, color: str = "yellow", level: str = "info"
+ ) -> None:
+ """Centralized logging method for flow events.
+
+ This method provides a consistent interface for logging flow-related events,
+ combining both console output with colors and proper logging levels.
+
+ Args:
+ message: The message to log
+ color: Color to use for console output (default: yellow)
+ Available colors: purple, red, bold_green, bold_purple,
+ bold_blue, yellow, yellow
+ level: Log level to use (default: info)
+ Supported levels: info, warning
+
+ Note:
+ This method uses the Printer utility for colored console output
+ and the standard logging module for log level support.
+ """
+ self._printer.print(message, color=color)
+ if level == "info":
+ logger.info(message)
+ elif level == "warning":
+ logger.warning(message)
+
def plot(self, filename: str = "crewai_flow") -> None:
self._telemetry.flow_plotting_span(
self.__class__.__name__, list(self._methods.keys())
diff --git a/src/crewai/flow/flow_events.py b/src/crewai/flow/flow_events.py
index 068005ebe..c8f9e9694 100644
--- a/src/crewai/flow/flow_events.py
+++ b/src/crewai/flow/flow_events.py
@@ -1,6 +1,8 @@
from dataclasses import dataclass, field
from datetime import datetime
-from typing import Any, Optional
+from typing import Any, Dict, Optional, Union
+
+from pydantic import BaseModel
@dataclass
@@ -15,17 +17,21 @@ class Event:
@dataclass
class FlowStartedEvent(Event):
- pass
+ inputs: Optional[Dict[str, Any]] = None
@dataclass
class MethodExecutionStartedEvent(Event):
method_name: str
+ state: Union[Dict[str, Any], BaseModel]
+ params: Optional[Dict[str, Any]] = None
@dataclass
class MethodExecutionFinishedEvent(Event):
method_name: str
+ state: Union[Dict[str, Any], BaseModel]
+ result: Any = None
@dataclass
diff --git a/src/crewai/flow/persistence/decorators.py b/src/crewai/flow/persistence/decorators.py
index 4906e95d5..ebf3778b7 100644
--- a/src/crewai/flow/persistence/decorators.py
+++ b/src/crewai/flow/persistence/decorators.py
@@ -5,14 +5,14 @@ Example:
```python
from crewai.flow.flow import Flow, start
from crewai.flow.persistence import persist, SQLiteFlowPersistence
-
+
class MyFlow(Flow):
@start()
@persist(SQLiteFlowPersistence())
def sync_method(self):
# Synchronous method implementation
pass
-
+
@start()
@persist(SQLiteFlowPersistence())
async def async_method(self):
@@ -23,48 +23,117 @@ Example:
import asyncio
import functools
-import inspect
import logging
from typing import (
Any,
Callable,
- Dict,
Optional,
Type,
TypeVar,
Union,
cast,
- get_type_hints,
)
from pydantic import BaseModel
from crewai.flow.persistence.base import FlowPersistence
from crewai.flow.persistence.sqlite import SQLiteFlowPersistence
+from crewai.utilities.printer import Printer
logger = logging.getLogger(__name__)
T = TypeVar("T")
+# Constants for log messages
+LOG_MESSAGES = {
+ "save_state": "Saving flow state to memory for ID: {}",
+ "save_error": "Failed to persist state for method {}: {}",
+ "state_missing": "Flow instance has no state",
+ "id_missing": "Flow state must have an 'id' field for persistence"
+}
+
+
+class PersistenceDecorator:
+ """Class to handle flow state persistence with consistent logging."""
+
+ _printer = Printer() # Class-level printer instance
+
+ @classmethod
+ def persist_state(cls, flow_instance: Any, method_name: str, persistence_instance: FlowPersistence) -> None:
+ """Persist flow state with proper error handling and logging.
+
+ This method handles the persistence of flow state data, including proper
+ error handling and colored console output for status updates.
+
+ Args:
+ flow_instance: The flow instance whose state to persist
+ method_name: Name of the method that triggered persistence
+ persistence_instance: The persistence backend to use
+
+ Raises:
+ ValueError: If flow has no state or state lacks an ID
+ RuntimeError: If state persistence fails
+ AttributeError: If flow instance lacks required state attributes
+ """
+ try:
+ state = getattr(flow_instance, 'state', None)
+ if state is None:
+ raise ValueError("Flow instance has no state")
+
+ flow_uuid: Optional[str] = None
+ if isinstance(state, dict):
+ flow_uuid = state.get('id')
+ elif isinstance(state, BaseModel):
+ flow_uuid = getattr(state, 'id', None)
+
+ if not flow_uuid:
+ raise ValueError("Flow state must have an 'id' field for persistence")
+
+ # Log state saving with consistent message
+ cls._printer.print(LOG_MESSAGES["save_state"].format(flow_uuid), color="cyan")
+ logger.info(LOG_MESSAGES["save_state"].format(flow_uuid))
+
+ try:
+ persistence_instance.save_state(
+ flow_uuid=flow_uuid,
+ method_name=method_name,
+ state_data=state,
+ )
+ except Exception as e:
+ error_msg = LOG_MESSAGES["save_error"].format(method_name, str(e))
+ cls._printer.print(error_msg, color="red")
+ logger.error(error_msg)
+ raise RuntimeError(f"State persistence failed: {str(e)}") from e
+ except AttributeError:
+ error_msg = LOG_MESSAGES["state_missing"]
+ cls._printer.print(error_msg, color="red")
+ logger.error(error_msg)
+ raise ValueError(error_msg)
+ except (TypeError, ValueError) as e:
+ error_msg = LOG_MESSAGES["id_missing"]
+ cls._printer.print(error_msg, color="red")
+ logger.error(error_msg)
+ raise ValueError(error_msg) from e
+
def persist(persistence: Optional[FlowPersistence] = None):
"""Decorator to persist flow state.
-
+
This decorator can be applied at either the class level or method level.
When applied at the class level, it automatically persists all flow method
states. When applied at the method level, it persists only that method's
state.
-
+
Args:
persistence: Optional FlowPersistence implementation to use.
If not provided, uses SQLiteFlowPersistence.
-
+
Returns:
A decorator that can be applied to either a class or method
-
+
Raises:
ValueError: If the flow state doesn't have an 'id' field
RuntimeError: If state persistence fails
-
+
Example:
@persist # Class-level persistence with default SQLite
class MyFlow(Flow[MyState]):
@@ -72,81 +141,85 @@ def persist(persistence: Optional[FlowPersistence] = None):
def begin(self):
pass
"""
- def _persist_state(flow_instance: Any, method_name: str, persistence_instance: FlowPersistence) -> None:
- """Helper to persist state with error handling."""
- try:
- # Get flow UUID from state
- state = getattr(flow_instance, 'state', None)
- if state is None:
- raise ValueError("Flow instance has no state")
-
- flow_uuid: Optional[str] = None
- if isinstance(state, dict):
- flow_uuid = state.get('id')
- elif isinstance(state, BaseModel):
- flow_uuid = getattr(state, 'id', None)
-
- if not flow_uuid:
- raise ValueError(
- "Flow state must have an 'id' field for persistence"
- )
-
- # Persist the state
- persistence_instance.save_state(
- flow_uuid=flow_uuid,
- method_name=method_name,
- state_data=state,
- )
- except Exception as e:
- logger.error(
- f"Failed to persist state for method {method_name}: {str(e)}"
- )
- raise RuntimeError(f"State persistence failed: {str(e)}") from e
-
+
def decorator(target: Union[Type, Callable[..., T]]) -> Union[Type, Callable[..., T]]:
"""Decorator that handles both class and method decoration."""
actual_persistence = persistence or SQLiteFlowPersistence()
-
+
if isinstance(target, type):
# Class decoration
- class_methods = {}
+ original_init = getattr(target, "__init__")
+
+ @functools.wraps(original_init)
+ def new_init(self: Any, *args: Any, **kwargs: Any) -> None:
+ if 'persistence' not in kwargs:
+ kwargs['persistence'] = actual_persistence
+ original_init(self, *args, **kwargs)
+
+ setattr(target, "__init__", new_init)
+
+ # Store original methods to preserve their decorators
+ original_methods = {}
+
for name, method in target.__dict__.items():
- if callable(method) and hasattr(method, "__is_flow_method__"):
- # Wrap each flow method with persistence
- if asyncio.iscoroutinefunction(method):
- @functools.wraps(method)
- async def class_async_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
- method_coro = method(self, *args, **kwargs)
- if asyncio.iscoroutine(method_coro):
- result = await method_coro
- else:
- result = method_coro
- _persist_state(self, method.__name__, actual_persistence)
+ if callable(method) and (
+ hasattr(method, "__is_start_method__") or
+ hasattr(method, "__trigger_methods__") or
+ hasattr(method, "__condition_type__") or
+ hasattr(method, "__is_flow_method__") or
+ hasattr(method, "__is_router__")
+ ):
+ original_methods[name] = method
+
+ # Create wrapped versions of the methods that include persistence
+ for name, method in original_methods.items():
+ if asyncio.iscoroutinefunction(method):
+ # Create a closure to capture the current name and method
+ def create_async_wrapper(method_name: str, original_method: Callable):
+ @functools.wraps(original_method)
+ async def method_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
+ result = await original_method(self, *args, **kwargs)
+ PersistenceDecorator.persist_state(self, method_name, actual_persistence)
return result
- class_methods[name] = class_async_wrapper
- else:
- @functools.wraps(method)
- def class_sync_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
- result = method(self, *args, **kwargs)
- _persist_state(self, method.__name__, actual_persistence)
- return result
- class_methods[name] = class_sync_wrapper
-
- # Preserve flow-specific attributes
+ return method_wrapper
+
+ wrapped = create_async_wrapper(name, method)
+
+ # Preserve all original decorators and attributes
for attr in ["__is_start_method__", "__trigger_methods__", "__condition_type__", "__is_router__"]:
if hasattr(method, attr):
- setattr(class_methods[name], attr, getattr(method, attr))
- setattr(class_methods[name], "__is_flow_method__", True)
-
- # Update class with wrapped methods
- for name, method in class_methods.items():
- setattr(target, name, method)
+ setattr(wrapped, attr, getattr(method, attr))
+ setattr(wrapped, "__is_flow_method__", True)
+
+ # Update the class with the wrapped method
+ setattr(target, name, wrapped)
+ else:
+ # Create a closure to capture the current name and method
+ def create_sync_wrapper(method_name: str, original_method: Callable):
+ @functools.wraps(original_method)
+ def method_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
+ result = original_method(self, *args, **kwargs)
+ PersistenceDecorator.persist_state(self, method_name, actual_persistence)
+ return result
+ return method_wrapper
+
+ wrapped = create_sync_wrapper(name, method)
+
+ # Preserve all original decorators and attributes
+ for attr in ["__is_start_method__", "__trigger_methods__", "__condition_type__", "__is_router__"]:
+ if hasattr(method, attr):
+ setattr(wrapped, attr, getattr(method, attr))
+ setattr(wrapped, "__is_flow_method__", True)
+
+ # Update the class with the wrapped method
+ setattr(target, name, wrapped)
+
return target
else:
# Method decoration
method = target
setattr(method, "__is_flow_method__", True)
-
+
if asyncio.iscoroutinefunction(method):
@functools.wraps(method)
async def method_async_wrapper(flow_instance: Any, *args: Any, **kwargs: Any) -> T:
@@ -155,8 +228,9 @@ def persist(persistence: Optional[FlowPersistence] = None):
result = await method_coro
else:
result = method_coro
- _persist_state(flow_instance, method.__name__, actual_persistence)
+ PersistenceDecorator.persist_state(flow_instance, method.__name__, actual_persistence)
return result
+
for attr in ["__is_start_method__", "__trigger_methods__", "__condition_type__", "__is_router__"]:
if hasattr(method, attr):
setattr(method_async_wrapper, attr, getattr(method, attr))
@@ -166,12 +240,13 @@ def persist(persistence: Optional[FlowPersistence] = None):
@functools.wraps(method)
def method_sync_wrapper(flow_instance: Any, *args: Any, **kwargs: Any) -> T:
result = method(flow_instance, *args, **kwargs)
- _persist_state(flow_instance, method.__name__, actual_persistence)
+ PersistenceDecorator.persist_state(flow_instance, method.__name__, actual_persistence)
return result
+
for attr in ["__is_start_method__", "__trigger_methods__", "__condition_type__", "__is_router__"]:
if hasattr(method, attr):
setattr(method_sync_wrapper, attr, getattr(method, attr))
setattr(method_sync_wrapper, "__is_flow_method__", True)
return cast(Callable[..., T], method_sync_wrapper)
-
+
return decorator
diff --git a/src/crewai/flow/persistence/sqlite.py b/src/crewai/flow/persistence/sqlite.py
index bdd091b2b..7a6f134fa 100644
--- a/src/crewai/flow/persistence/sqlite.py
+++ b/src/crewai/flow/persistence/sqlite.py
@@ -3,10 +3,9 @@ SQLite-based implementation of flow state persistence.
"""
import json
-import os
import sqlite3
-import tempfile
from datetime import datetime
+from pathlib import Path
from typing import Any, Dict, Optional, Union
from pydantic import BaseModel
@@ -16,34 +15,34 @@ from crewai.flow.persistence.base import FlowPersistence
class SQLiteFlowPersistence(FlowPersistence):
"""SQLite-based implementation of flow state persistence.
-
+
This class provides a simple, file-based persistence implementation using SQLite.
It's suitable for development and testing, or for production use cases with
moderate performance requirements.
"""
-
+
db_path: str # Type annotation for instance variable
-
+
def __init__(self, db_path: Optional[str] = None):
"""Initialize SQLite persistence.
-
+
Args:
db_path: Path to the SQLite database file. If not provided, uses
db_storage_path() from utilities.paths.
-
+
Raises:
ValueError: If db_path is invalid
"""
from crewai.utilities.paths import db_storage_path
# Get path from argument or default location
- path = db_path or db_storage_path()
-
+ path = db_path or str(Path(db_storage_path()) / "flow_states.db")
+
if not path:
raise ValueError("Database path must be provided")
-
+
self.db_path = path # Now mypy knows this is str
self.init_db()
-
+
def init_db(self) -> None:
"""Create the necessary tables if they don't exist."""
with sqlite3.connect(self.db_path) as conn:
@@ -58,10 +57,10 @@ class SQLiteFlowPersistence(FlowPersistence):
""")
# Add index for faster UUID lookups
conn.execute("""
- CREATE INDEX IF NOT EXISTS idx_flow_states_uuid
+ CREATE INDEX IF NOT EXISTS idx_flow_states_uuid
ON flow_states(flow_uuid)
""")
-
+
def save_state(
self,
flow_uuid: str,
@@ -69,7 +68,7 @@ class SQLiteFlowPersistence(FlowPersistence):
state_data: Union[Dict[str, Any], BaseModel],
) -> None:
"""Save the current flow state to SQLite.
-
+
Args:
flow_uuid: Unique identifier for the flow instance
method_name: Name of the method that just completed
@@ -84,7 +83,7 @@ class SQLiteFlowPersistence(FlowPersistence):
raise ValueError(
f"state_data must be either a Pydantic BaseModel or dict, got {type(state_data)}"
)
-
+
with sqlite3.connect(self.db_path) as conn:
conn.execute("""
INSERT INTO flow_states (
@@ -99,13 +98,13 @@ class SQLiteFlowPersistence(FlowPersistence):
datetime.utcnow().isoformat(),
json.dumps(state_dict),
))
-
+
def load_state(self, flow_uuid: str) -> Optional[Dict[str, Any]]:
"""Load the most recent state for a given flow UUID.
-
+
Args:
flow_uuid: Unique identifier for the flow instance
-
+
Returns:
The most recent state as a dictionary, or None if no state exists
"""
@@ -118,7 +117,7 @@ class SQLiteFlowPersistence(FlowPersistence):
LIMIT 1
""", (flow_uuid,))
row = cursor.fetchone()
-
+
if row:
return json.loads(row[0])
return None
diff --git a/src/crewai/knowledge/knowledge.py b/src/crewai/knowledge/knowledge.py
index c964333c8..da1db90a8 100644
--- a/src/crewai/knowledge/knowledge.py
+++ b/src/crewai/knowledge/knowledge.py
@@ -15,20 +15,20 @@ class Knowledge(BaseModel):
Args:
sources: List[BaseKnowledgeSource] = Field(default_factory=list)
storage: Optional[KnowledgeStorage] = Field(default=None)
- embedder_config: Optional[Dict[str, Any]] = None
+ embedder: Optional[Dict[str, Any]] = None
"""
sources: List[BaseKnowledgeSource] = Field(default_factory=list)
model_config = ConfigDict(arbitrary_types_allowed=True)
storage: Optional[KnowledgeStorage] = Field(default=None)
- embedder_config: Optional[Dict[str, Any]] = None
+ embedder: Optional[Dict[str, Any]] = None
collection_name: Optional[str] = None
def __init__(
self,
collection_name: str,
sources: List[BaseKnowledgeSource],
- embedder_config: Optional[Dict[str, Any]] = None,
+ embedder: Optional[Dict[str, Any]] = None,
storage: Optional[KnowledgeStorage] = None,
**data,
):
@@ -37,25 +37,23 @@ class Knowledge(BaseModel):
self.storage = storage
else:
self.storage = KnowledgeStorage(
- embedder_config=embedder_config, collection_name=collection_name
+ embedder=embedder, collection_name=collection_name
)
self.sources = sources
self.storage.initialize_knowledge_storage()
- for source in sources:
- source.storage = self.storage
- source.add()
+ self._add_sources()
def query(self, query: List[str], limit: int = 3) -> List[Dict[str, Any]]:
"""
Query across all knowledge sources to find the most relevant information.
Returns the top_k most relevant chunks.
-
+
Raises:
ValueError: If storage is not initialized.
"""
if self.storage is None:
raise ValueError("Storage is not initialized.")
-
+
results = self.storage.search(
query,
limit,
@@ -63,6 +61,15 @@ class Knowledge(BaseModel):
return results
def _add_sources(self):
- for source in self.sources:
- source.storage = self.storage
- source.add()
+ try:
+ for source in self.sources:
+ source.storage = self.storage
+ source.add()
+ except Exception as e:
+ raise e
+
+ def reset(self) -> None:
+ if self.storage:
+ self.storage.reset()
+ else:
+ raise ValueError("Storage is not initialized.")
diff --git a/src/crewai/knowledge/source/base_file_knowledge_source.py b/src/crewai/knowledge/source/base_file_knowledge_source.py
index ac345b6a6..4c4b9b337 100644
--- a/src/crewai/knowledge/source/base_file_knowledge_source.py
+++ b/src/crewai/knowledge/source/base_file_knowledge_source.py
@@ -29,7 +29,13 @@ class BaseFileKnowledgeSource(BaseKnowledgeSource, ABC):
def validate_file_path(cls, v, info):
"""Validate that at least one of file_path or file_paths is provided."""
# Single check if both are None, O(1) instead of nested conditions
- if v is None and info.data.get("file_path" if info.field_name == "file_paths" else "file_paths") is None:
+ if (
+ v is None
+ and info.data.get(
+ "file_path" if info.field_name == "file_paths" else "file_paths"
+ )
+ is None
+ ):
raise ValueError("Either file_path or file_paths must be provided")
return v
diff --git a/src/crewai/knowledge/source/excel_knowledge_source.py b/src/crewai/knowledge/source/excel_knowledge_source.py
index d379c2763..a73afb1df 100644
--- a/src/crewai/knowledge/source/excel_knowledge_source.py
+++ b/src/crewai/knowledge/source/excel_knowledge_source.py
@@ -1,28 +1,138 @@
from pathlib import Path
-from typing import Dict, List
+from typing import Dict, Iterator, List, Optional, Union
+from urllib.parse import urlparse
-from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledgeSource
+from pydantic import Field, field_validator
+
+from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
+from crewai.utilities.constants import KNOWLEDGE_DIRECTORY
+from crewai.utilities.logger import Logger
-class ExcelKnowledgeSource(BaseFileKnowledgeSource):
+class ExcelKnowledgeSource(BaseKnowledgeSource):
"""A knowledge source that stores and queries Excel file content using embeddings."""
- def load_content(self) -> Dict[Path, str]:
- """Load and preprocess Excel file content."""
- pd = self._import_dependencies()
+ # override content to be a dict of file paths to sheet names to csv content
+ _logger: Logger = Logger(verbose=True)
+
+ file_path: Optional[Union[Path, List[Path], str, List[str]]] = Field(
+ default=None,
+ description="[Deprecated] The path to the file. Use file_paths instead.",
+ )
+ file_paths: Optional[Union[Path, List[Path], str, List[str]]] = Field(
+ default_factory=list, description="The path to the file"
+ )
+ chunks: List[str] = Field(default_factory=list)
+ content: Dict[Path, Dict[str, str]] = Field(default_factory=dict)
+ safe_file_paths: List[Path] = Field(default_factory=list)
+
+ @field_validator("file_path", "file_paths", mode="before")
+ def validate_file_path(cls, v, info):
+ """Validate that at least one of file_path or file_paths is provided."""
+ # Single check if both are None, O(1) instead of nested conditions
+ if (
+ v is None
+ and info.data.get(
+ "file_path" if info.field_name == "file_paths" else "file_paths"
+ )
+ is None
+ ):
+ raise ValueError("Either file_path or file_paths must be provided")
+ return v
+
+ def _process_file_paths(self) -> List[Path]:
+ """Convert file_path to a list of Path objects."""
+
+ if hasattr(self, "file_path") and self.file_path is not None:
+ self._logger.log(
+ "warning",
+ "The 'file_path' attribute is deprecated and will be removed in a future version. Please use 'file_paths' instead.",
+ color="yellow",
+ )
+ self.file_paths = self.file_path
+
+ if self.file_paths is None:
+ raise ValueError("Your source must be provided with a file_paths: []")
+
+ # Convert single path to list
+ path_list: List[Union[Path, str]] = (
+ [self.file_paths]
+ if isinstance(self.file_paths, (str, Path))
+ else list(self.file_paths)
+ if isinstance(self.file_paths, list)
+ else []
+ )
+
+ if not path_list:
+ raise ValueError(
+ "file_path/file_paths must be a Path, str, or a list of these types"
+ )
+
+ return [self.convert_to_path(path) for path in path_list]
+
+ def validate_content(self):
+ """Validate the paths."""
+ for path in self.safe_file_paths:
+ if not path.exists():
+ self._logger.log(
+ "error",
+ f"File not found: {path}. Try adding sources to the knowledge directory. If it's inside the knowledge directory, use the relative path.",
+ color="red",
+ )
+ raise FileNotFoundError(f"File not found: {path}")
+ if not path.is_file():
+ self._logger.log(
+ "error",
+ f"Path is not a file: {path}",
+ color="red",
+ )
+
+ def model_post_init(self, _) -> None:
+ if self.file_path:
+ self._logger.log(
+ "warning",
+ "The 'file_path' attribute is deprecated and will be removed in a future version. Please use 'file_paths' instead.",
+ color="yellow",
+ )
+ self.file_paths = self.file_path
+ self.safe_file_paths = self._process_file_paths()
+ self.validate_content()
+ self.content = self._load_content()
+
+ def _load_content(self) -> Dict[Path, Dict[str, str]]:
+ """Load and preprocess Excel file content from multiple sheets.
+
+ Each sheet's content is converted to CSV format and stored.
+
+ Returns:
+ Dict[Path, Dict[str, str]]: A mapping of file paths to their respective sheet contents.
+
+ Raises:
+ ImportError: If required dependencies are missing.
+ FileNotFoundError: If the specified Excel file cannot be opened.
+ """
+ pd = self._import_dependencies()
content_dict = {}
for file_path in self.safe_file_paths:
file_path = self.convert_to_path(file_path)
- df = pd.read_excel(file_path)
- content = df.to_csv(index=False)
- content_dict[file_path] = content
+ with pd.ExcelFile(file_path) as xl:
+ sheet_dict = {
+ str(sheet_name): str(
+ pd.read_excel(xl, sheet_name).to_csv(index=False)
+ )
+ for sheet_name in xl.sheet_names
+ }
+ content_dict[file_path] = sheet_dict
return content_dict
+ def convert_to_path(self, path: Union[Path, str]) -> Path:
+ """Convert a path to a Path object."""
+ return Path(KNOWLEDGE_DIRECTORY + "/" + path) if isinstance(path, str) else path
+
def _import_dependencies(self):
"""Dynamically import dependencies."""
try:
- import openpyxl # noqa
import pandas as pd
return pd
@@ -38,10 +148,14 @@ class ExcelKnowledgeSource(BaseFileKnowledgeSource):
and save the embeddings.
"""
# Convert dictionary values to a single string if content is a dictionary
- if isinstance(self.content, dict):
- content_str = "\n".join(str(value) for value in self.content.values())
- else:
- content_str = str(self.content)
+ # Updated to account for .xlsx workbooks with multiple tabs/sheets
+ content_str = ""
+ for value in self.content.values():
+ if isinstance(value, dict):
+ for sheet_value in value.values():
+ content_str += str(sheet_value) + "\n"
+ else:
+ content_str += str(value) + "\n"
new_chunks = self._chunk_text(content_str)
self.chunks.extend(new_chunks)
diff --git a/src/crewai/knowledge/storage/knowledge_storage.py b/src/crewai/knowledge/storage/knowledge_storage.py
index 4a70c5997..9e6ab8041 100644
--- a/src/crewai/knowledge/storage/knowledge_storage.py
+++ b/src/crewai/knowledge/storage/knowledge_storage.py
@@ -48,11 +48,11 @@ class KnowledgeStorage(BaseKnowledgeStorage):
def __init__(
self,
- embedder_config: Optional[Dict[str, Any]] = None,
+ embedder: Optional[Dict[str, Any]] = None,
collection_name: Optional[str] = None,
):
self.collection_name = collection_name
- self._set_embedder_config(embedder_config)
+ self._set_embedder_config(embedder)
def search(
self,
@@ -99,7 +99,7 @@ class KnowledgeStorage(BaseKnowledgeStorage):
)
if self.app:
self.collection = self.app.get_or_create_collection(
- name=collection_name, embedding_function=self.embedder_config
+ name=collection_name, embedding_function=self.embedder
)
else:
raise Exception("Vector Database Client not initialized")
@@ -187,17 +187,15 @@ class KnowledgeStorage(BaseKnowledgeStorage):
api_key=os.getenv("OPENAI_API_KEY"), model_name="text-embedding-3-small"
)
- def _set_embedder_config(
- self, embedder_config: Optional[Dict[str, Any]] = None
- ) -> None:
+ def _set_embedder_config(self, embedder: Optional[Dict[str, Any]] = None) -> None:
"""Set the embedding configuration for the knowledge storage.
Args:
embedder_config (Optional[Dict[str, Any]]): Configuration dictionary for the embedder.
If None or empty, defaults to the default embedding function.
"""
- self.embedder_config = (
- EmbeddingConfigurator().configure_embedder(embedder_config)
- if embedder_config
+ self.embedder = (
+ EmbeddingConfigurator().configure_embedder(embedder)
+ if embedder
else self._create_default_embedding_function()
)
diff --git a/src/crewai/llm.py b/src/crewai/llm.py
index 790d13ead..ada5c9bf3 100644
--- a/src/crewai/llm.py
+++ b/src/crewai/llm.py
@@ -5,15 +5,17 @@ import sys
import threading
import warnings
from contextlib import contextmanager
-from typing import Any, Dict, List, Optional, Union, cast
+from typing import Any, Dict, List, Literal, Optional, Type, Union, cast
from dotenv import load_dotenv
+from pydantic import BaseModel
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
import litellm
from litellm import Choices, get_supported_openai_params
from litellm.types.utils import ModelResponse
+ from litellm.utils import supports_response_schema
from crewai.utilities.exceptions.context_window_exceeding_exception import (
@@ -128,21 +130,23 @@ class LLM:
presence_penalty: Optional[float] = None,
frequency_penalty: Optional[float] = None,
logit_bias: Optional[Dict[int, float]] = None,
- response_format: Optional[Dict[str, Any]] = None,
+ response_format: Optional[Type[BaseModel]] = None,
seed: Optional[int] = None,
logprobs: Optional[int] = None,
top_logprobs: Optional[int] = None,
base_url: Optional[str] = None,
+ api_base: Optional[str] = None,
api_version: Optional[str] = None,
api_key: Optional[str] = None,
callbacks: List[Any] = [],
+ reasoning_effort: Optional[Literal["none", "low", "medium", "high"]] = None,
+ **kwargs,
):
self.model = model
self.timeout = timeout
self.temperature = temperature
self.top_p = top_p
self.n = n
- self.stop = stop
self.max_completion_tokens = max_completion_tokens
self.max_tokens = max_tokens
self.presence_penalty = presence_penalty
@@ -153,47 +157,110 @@ class LLM:
self.logprobs = logprobs
self.top_logprobs = top_logprobs
self.base_url = base_url
+ self.api_base = api_base
self.api_version = api_version
self.api_key = api_key
self.callbacks = callbacks
self.context_window_size = 0
+ self.reasoning_effort = reasoning_effort
+ self.additional_params = kwargs
+ self.is_anthropic = self._is_anthropic_model(model)
litellm.drop_params = True
+ # Normalize self.stop to always be a List[str]
+ if stop is None:
+ self.stop: List[str] = []
+ elif isinstance(stop, str):
+ self.stop = [stop]
+ else:
+ self.stop = stop
+
self.set_callbacks(callbacks)
self.set_env_callbacks()
+ def _is_anthropic_model(self, model: str) -> bool:
+ """Determine if the model is from Anthropic provider.
+
+ Args:
+ model: The model identifier string.
+
+ Returns:
+ bool: True if the model is from Anthropic, False otherwise.
+ """
+ ANTHROPIC_PREFIXES = ('anthropic/', 'claude-', 'claude/')
+ return any(prefix in model.lower() for prefix in ANTHROPIC_PREFIXES)
+
def call(
self,
- messages: List[Dict[str, str]],
+ messages: Union[str, List[Dict[str, str]]],
tools: Optional[List[dict]] = None,
callbacks: Optional[List[Any]] = None,
available_functions: Optional[Dict[str, Any]] = None,
- ) -> str:
+ ) -> Union[str, Any]:
+ """High-level LLM call method.
+
+ Args:
+ messages: Input messages for the LLM.
+ Can be a string or list of message dictionaries.
+ If string, it will be converted to a single user message.
+ If list, each dict must have 'role' and 'content' keys.
+ tools: Optional list of tool schemas for function calling.
+ Each tool should define its name, description, and parameters.
+ callbacks: Optional list of callback functions to be executed
+ during and after the LLM call.
+ available_functions: Optional dict mapping function names to callables
+ that can be invoked by the LLM.
+
+ Returns:
+ Union[str, Any]: Either a text response from the LLM (str) or
+ the result of a tool function call (Any).
+
+ Raises:
+ TypeError: If messages format is invalid
+ ValueError: If response format is not supported
+ LLMContextLengthExceededException: If input exceeds model's context limit
+
+ Examples:
+ # Example 1: Simple string input
+ >>> response = llm.call("Return the name of a random city.")
+ >>> print(response)
+ "Paris"
+
+ # Example 2: Message list with system and user messages
+ >>> messages = [
+ ... {"role": "system", "content": "You are a geography expert"},
+ ... {"role": "user", "content": "What is France's capital?"}
+ ... ]
+ >>> response = llm.call(messages)
+ >>> print(response)
+ "The capital of France is Paris."
"""
- High-level call method that:
- 1) Calls litellm.completion
- 2) Checks for function/tool calls
- 3) If a tool call is found:
- a) executes the function
- b) returns the result
- 4) If no tool call, returns the text response
+ # Validate parameters before proceeding with the call.
+ self._validate_call_params()
+
+ if isinstance(messages, str):
+ messages = [{"role": "user", "content": messages}]
+
+ # For O1 models, system messages are not supported.
+ # Convert any system messages into assistant messages.
+ if "o1" in self.model.lower():
+ for message in messages:
+ if message.get("role") == "system":
+ message["role"] = "assistant"
- :param messages: The conversation messages
- :param tools: Optional list of function schemas for function calling
- :param callbacks: Optional list of callbacks
- :param available_functions: A dictionary mapping function_name -> actual Python function
- :return: Final text response from the LLM or the tool result
- """
with suppress_warnings():
if callbacks and len(callbacks) > 0:
self.set_callbacks(callbacks)
try:
- # --- 1) Make the completion call
+ # --- 1) Format messages according to provider requirements
+ formatted_messages = self._format_messages_for_provider(messages)
+
+ # --- 2) Prepare the parameters for the completion call
params = {
"model": self.model,
- "messages": messages,
+ "messages": formatted_messages,
"timeout": self.timeout,
"temperature": self.temperature,
"top_p": self.top_p,
@@ -207,23 +274,28 @@ class LLM:
"seed": self.seed,
"logprobs": self.logprobs,
"top_logprobs": self.top_logprobs,
- "api_base": self.base_url,
+ "api_base": self.api_base,
+ "base_url": self.base_url,
"api_version": self.api_version,
"api_key": self.api_key,
"stream": False,
- "tools": tools, # pass the tool schema
+ "tools": tools,
+ "reasoning_effort": self.reasoning_effort,
+ **self.additional_params,
}
+ # Remove None values from params
params = {k: v for k, v in params.items() if v is not None}
+ # --- 2) Make the completion call
response = litellm.completion(**params)
response_message = cast(Choices, cast(ModelResponse, response).choices)[
0
].message
text_response = response_message.content or ""
tool_calls = getattr(response_message, "tool_calls", [])
-
- # Ensure callbacks get the full response object with usage info
+
+ # --- 3) Handle callbacks with usage info
if callbacks and len(callbacks) > 0:
for callback in callbacks:
if hasattr(callback, "log_success_event"):
@@ -236,11 +308,11 @@ class LLM:
end_time=0,
)
- # --- 2) If no tool calls, return the text response
+ # --- 4) If no tool calls, return the text response
if not tool_calls or not available_functions:
return text_response
- # --- 3) Handle the tool call
+ # --- 5) Handle the tool call
tool_call = tool_calls[0]
function_name = tool_call.function.name
@@ -255,7 +327,6 @@ class LLM:
try:
# Call the actual tool function
result = fn(**function_args)
-
return result
except Exception as e:
@@ -277,6 +348,68 @@ class LLM:
logging.error(f"LiteLLM call failed: {str(e)}")
raise
+ def _format_messages_for_provider(self, messages: List[Dict[str, str]]) -> List[Dict[str, str]]:
+ """Format messages according to provider requirements.
+
+ Args:
+ messages: List of message dictionaries with 'role' and 'content' keys.
+ Can be empty or None.
+
+ Returns:
+ List of formatted messages according to provider requirements.
+ For Anthropic models, ensures first message has 'user' role.
+
+ Raises:
+ TypeError: If messages is None or contains invalid message format.
+ """
+ if messages is None:
+ raise TypeError("Messages cannot be None")
+
+ # Validate message format first
+ for msg in messages:
+ if not isinstance(msg, dict) or "role" not in msg or "content" not in msg:
+ raise TypeError("Invalid message format. Each message must be a dict with 'role' and 'content' keys")
+
+ if not self.is_anthropic:
+ return messages
+
+ # Anthropic requires messages to start with 'user' role
+ if not messages or messages[0]["role"] == "system":
+ # If first message is system or empty, add a placeholder user message
+ return [{"role": "user", "content": "."}, *messages]
+
+ return messages
+
+ def _get_custom_llm_provider(self) -> str:
+ """
+ Derives the custom_llm_provider from the model string.
+ - For example, if the model is "openrouter/deepseek/deepseek-chat", returns "openrouter".
+ - If the model is "gemini/gemini-1.5-pro", returns "gemini".
+ - If there is no '/', defaults to "openai".
+ """
+ if "/" in self.model:
+ return self.model.split("/")[0]
+ return "openai"
+
+ def _validate_call_params(self) -> None:
+ """
+ Validate parameters before making a call. Currently this only checks if
+ a response_format is provided and whether the model supports it.
+ The custom_llm_provider is dynamically determined from the model:
+ - E.g., "openrouter/deepseek/deepseek-chat" yields "openrouter"
+ - "gemini/gemini-1.5-pro" yields "gemini"
+ - If no slash is present, "openai" is assumed.
+ """
+ provider = self._get_custom_llm_provider()
+ if self.response_format is not None and not supports_response_schema(
+ model=self.model,
+ custom_llm_provider=provider,
+ ):
+ raise ValueError(
+ f"The model {self.model} does not support response_format for provider '{provider}'. "
+ "Please remove response_format or use a supported model."
+ )
+
def supports_function_calling(self) -> bool:
try:
params = get_supported_openai_params(model=self.model)
diff --git a/src/crewai/memory/entity/entity_memory.py b/src/crewai/memory/entity/entity_memory.py
index 67c72e927..264b64103 100644
--- a/src/crewai/memory/entity/entity_memory.py
+++ b/src/crewai/memory/entity/entity_memory.py
@@ -1,3 +1,7 @@
+from typing import Optional
+
+from pydantic import PrivateAttr
+
from crewai.memory.entity.entity_memory_item import EntityMemoryItem
from crewai.memory.memory import Memory
from crewai.memory.storage.rag_storage import RAGStorage
@@ -10,13 +14,15 @@ class EntityMemory(Memory):
Inherits from the Memory class.
"""
- def __init__(self, crew=None, embedder_config=None, storage=None, path=None):
- if hasattr(crew, "memory_config") and crew.memory_config is not None:
- self.memory_provider = crew.memory_config.get("provider")
- else:
- self.memory_provider = None
+ _memory_provider: Optional[str] = PrivateAttr()
- if self.memory_provider == "mem0":
+ def __init__(self, crew=None, embedder_config=None, storage=None, path=None):
+ if crew and hasattr(crew, "memory_config") and crew.memory_config is not None:
+ memory_provider = crew.memory_config.get("provider")
+ else:
+ memory_provider = None
+
+ if memory_provider == "mem0":
try:
from crewai.memory.storage.mem0_storage import Mem0Storage
except ImportError:
@@ -36,11 +42,13 @@ class EntityMemory(Memory):
path=path,
)
)
- super().__init__(storage)
+
+ super().__init__(storage=storage)
+ self._memory_provider = memory_provider
def save(self, item: EntityMemoryItem) -> None: # type: ignore # BUG?: Signature of "save" incompatible with supertype "Memory"
"""Saves an entity item into the SQLite storage."""
- if self.memory_provider == "mem0":
+ if self._memory_provider == "mem0":
data = f"""
Remember details about the following entity:
Name: {item.name}
diff --git a/src/crewai/memory/long_term/long_term_memory.py b/src/crewai/memory/long_term/long_term_memory.py
index 656709ac9..94aac3a97 100644
--- a/src/crewai/memory/long_term/long_term_memory.py
+++ b/src/crewai/memory/long_term/long_term_memory.py
@@ -17,7 +17,7 @@ class LongTermMemory(Memory):
def __init__(self, storage=None, path=None):
if not storage:
storage = LTMSQLiteStorage(db_path=path) if path else LTMSQLiteStorage()
- super().__init__(storage)
+ super().__init__(storage=storage)
def save(self, item: LongTermMemoryItem) -> None: # type: ignore # BUG?: Signature of "save" incompatible with supertype "Memory"
metadata = item.metadata
diff --git a/src/crewai/memory/memory.py b/src/crewai/memory/memory.py
index 46af2c04d..9a362a512 100644
--- a/src/crewai/memory/memory.py
+++ b/src/crewai/memory/memory.py
@@ -1,15 +1,19 @@
from typing import Any, Dict, List, Optional
-from crewai.memory.storage.rag_storage import RAGStorage
+from pydantic import BaseModel
-class Memory:
+class Memory(BaseModel):
"""
Base class for memory, now supporting agent tags and generic metadata.
"""
- def __init__(self, storage: RAGStorage):
- self.storage = storage
+ embedder_config: Optional[Dict[str, Any]] = None
+
+ storage: Any
+
+ def __init__(self, storage: Any, **data: Any):
+ super().__init__(storage=storage, **data)
def save(
self,
diff --git a/src/crewai/memory/short_term/short_term_memory.py b/src/crewai/memory/short_term/short_term_memory.py
index 4e5fbbb77..b7581f400 100644
--- a/src/crewai/memory/short_term/short_term_memory.py
+++ b/src/crewai/memory/short_term/short_term_memory.py
@@ -1,5 +1,7 @@
from typing import Any, Dict, Optional
+from pydantic import PrivateAttr
+
from crewai.memory.memory import Memory
from crewai.memory.short_term.short_term_memory_item import ShortTermMemoryItem
from crewai.memory.storage.rag_storage import RAGStorage
@@ -14,13 +16,15 @@ class ShortTermMemory(Memory):
MemoryItem instances.
"""
- def __init__(self, crew=None, embedder_config=None, storage=None, path=None):
- if hasattr(crew, "memory_config") and crew.memory_config is not None:
- self.memory_provider = crew.memory_config.get("provider")
- else:
- self.memory_provider = None
+ _memory_provider: Optional[str] = PrivateAttr()
- if self.memory_provider == "mem0":
+ def __init__(self, crew=None, embedder_config=None, storage=None, path=None):
+ if crew and hasattr(crew, "memory_config") and crew.memory_config is not None:
+ memory_provider = crew.memory_config.get("provider")
+ else:
+ memory_provider = None
+
+ if memory_provider == "mem0":
try:
from crewai.memory.storage.mem0_storage import Mem0Storage
except ImportError:
@@ -39,7 +43,8 @@ class ShortTermMemory(Memory):
path=path,
)
)
- super().__init__(storage)
+ super().__init__(storage=storage)
+ self._memory_provider = memory_provider
def save(
self,
@@ -48,7 +53,7 @@ class ShortTermMemory(Memory):
agent: Optional[str] = None,
) -> None:
item = ShortTermMemoryItem(data=value, metadata=metadata, agent=agent)
- if self.memory_provider == "mem0":
+ if self._memory_provider == "mem0":
item.data = f"Remember the following insights from Agent run: {item.data}"
super().save(value=item.data, metadata=item.metadata, agent=item.agent)
diff --git a/src/crewai/memory/storage/base_rag_storage.py b/src/crewai/memory/storage/base_rag_storage.py
index 10b82ebff..4ab9acb99 100644
--- a/src/crewai/memory/storage/base_rag_storage.py
+++ b/src/crewai/memory/storage/base_rag_storage.py
@@ -13,7 +13,7 @@ class BaseRAGStorage(ABC):
self,
type: str,
allow_reset: bool = True,
- embedder_config: Optional[Any] = None,
+ embedder_config: Optional[Dict[str, Any]] = None,
crew: Any = None,
):
self.type = type
diff --git a/src/crewai/memory/storage/kickoff_task_outputs_storage.py b/src/crewai/memory/storage/kickoff_task_outputs_storage.py
index ef99e7b86..2a035833d 100644
--- a/src/crewai/memory/storage/kickoff_task_outputs_storage.py
+++ b/src/crewai/memory/storage/kickoff_task_outputs_storage.py
@@ -23,7 +23,7 @@ class KickoffTaskOutputsSQLiteStorage:
) -> None:
if db_path is None:
# Get the parent directory of the default db path and create our db file there
- db_path = str(Path(db_storage_path()).parent / "latest_kickoff_task_outputs.db")
+ db_path = str(Path(db_storage_path()) / "latest_kickoff_task_outputs.db")
self.db_path = db_path
self._printer: Printer = Printer()
self._initialize_db()
diff --git a/src/crewai/memory/storage/ltm_sqlite_storage.py b/src/crewai/memory/storage/ltm_sqlite_storage.py
index 3d1208722..35f54e0e7 100644
--- a/src/crewai/memory/storage/ltm_sqlite_storage.py
+++ b/src/crewai/memory/storage/ltm_sqlite_storage.py
@@ -17,7 +17,7 @@ class LTMSQLiteStorage:
) -> None:
if db_path is None:
# Get the parent directory of the default db path and create our db file there
- db_path = str(Path(db_storage_path()).parent / "long_term_memory_storage.db")
+ db_path = str(Path(db_storage_path()) / "long_term_memory_storage.db")
self.db_path = db_path
self._printer: Printer = Printer()
# Ensure parent directory exists
diff --git a/src/crewai/task.py b/src/crewai/task.py
index 030bce779..4088c3fb0 100644
--- a/src/crewai/task.py
+++ b/src/crewai/task.py
@@ -423,6 +423,10 @@ class Task(BaseModel):
if self.callback:
self.callback(self.output)
+ crew = self.agent.crew # type: ignore[union-attr]
+ if crew and crew.task_callback and crew.task_callback != self.callback:
+ crew.task_callback(self.output)
+
if self._execution_span:
self._telemetry.task_ended(self._execution_span, self, agent.crew)
self._execution_span = None
@@ -431,7 +435,9 @@ class Task(BaseModel):
content = (
json_output
if json_output
- else pydantic_output.model_dump_json() if pydantic_output else result
+ else pydantic_output.model_dump_json()
+ if pydantic_output
+ else result
)
self._save_file(content)
@@ -452,7 +458,7 @@ class Task(BaseModel):
return "\n".join(tasks_slices)
def interpolate_inputs_and_add_conversation_history(
- self, inputs: Dict[str, Union[str, int, float]]
+ self, inputs: Dict[str, Union[str, int, float, Dict[str, Any], List[Any]]]
) -> None:
"""Interpolate inputs into the task description, expected output, and output file path.
Add conversation history if present.
@@ -524,7 +530,9 @@ class Task(BaseModel):
)
def interpolate_only(
- self, input_string: Optional[str], inputs: Dict[str, Union[str, int, float]]
+ self,
+ input_string: Optional[str],
+ inputs: Dict[str, Union[str, int, float, Dict[str, Any], List[Any]]],
) -> str:
"""Interpolate placeholders (e.g., {key}) in a string while leaving JSON untouched.
@@ -532,17 +540,39 @@ class Task(BaseModel):
input_string: The string containing template variables to interpolate.
Can be None or empty, in which case an empty string is returned.
inputs: Dictionary mapping template variables to their values.
- Supported value types are strings, integers, and floats.
- If input_string is empty or has no placeholders, inputs can be empty.
+ Supported value types are strings, integers, floats, and dicts/lists
+ containing only these types and other nested dicts/lists.
Returns:
The interpolated string with all template variables replaced with their values.
Empty string if input_string is None or empty.
Raises:
- ValueError: If a required template variable is missing from inputs.
- KeyError: If a template variable is not found in the inputs dictionary.
+ ValueError: If a value contains unsupported types
"""
+
+ # Validation function for recursive type checking
+ def validate_type(value: Any) -> None:
+ if value is None:
+ return
+ if isinstance(value, (str, int, float, bool)):
+ return
+ if isinstance(value, (dict, list)):
+ for item in value.values() if isinstance(value, dict) else value:
+ validate_type(item)
+ return
+ raise ValueError(
+ f"Unsupported type {type(value).__name__} in inputs. "
+ "Only str, int, float, bool, dict, and list are allowed."
+ )
+
+ # Validate all input values
+ for key, value in inputs.items():
+ try:
+ validate_type(value)
+ except ValueError as e:
+ raise ValueError(f"Invalid value for key '{key}': {str(e)}") from e
+
if input_string is None or not input_string:
return ""
if "{" not in input_string and "}" not in input_string:
@@ -551,15 +581,7 @@ class Task(BaseModel):
raise ValueError(
"Inputs dictionary cannot be empty when interpolating variables"
)
-
try:
- # Validate input types
- for key, value in inputs.items():
- if not isinstance(value, (str, int, float)):
- raise ValueError(
- f"Value for key '{key}' must be a string, integer, or float, got {type(value).__name__}"
- )
-
escaped_string = input_string.replace("{", "{{").replace("}", "}}")
for key in inputs.keys():
@@ -652,19 +674,32 @@ class Task(BaseModel):
return OutputFormat.PYDANTIC
return OutputFormat.RAW
- def _save_file(self, result: Any) -> None:
+ def _save_file(self, result: Union[Dict, str, Any]) -> None:
"""Save task output to a file.
+ Note:
+ For cross-platform file writing, especially on Windows, consider using FileWriterTool
+ from the crewai_tools package:
+ pip install 'crewai[tools]'
+ from crewai_tools import FileWriterTool
+
Args:
result: The result to save to the file. Can be a dict or any stringifiable object.
Raises:
ValueError: If output_file is not set
- RuntimeError: If there is an error writing to the file
+ RuntimeError: If there is an error writing to the file. For cross-platform
+ compatibility, especially on Windows, use FileWriterTool from crewai_tools
+ package.
"""
if self.output_file is None:
raise ValueError("output_file is not set.")
+ FILEWRITER_RECOMMENDATION = (
+ "For cross-platform file writing, especially on Windows, "
+ "use FileWriterTool from crewai_tools package."
+ )
+
try:
resolved_path = Path(self.output_file).expanduser().resolve()
directory = resolved_path.parent
@@ -680,7 +715,12 @@ class Task(BaseModel):
else:
file.write(str(result))
except (OSError, IOError) as e:
- raise RuntimeError(f"Failed to save output file: {e}")
+ raise RuntimeError(
+ "\n".join([
+ f"Failed to save output file: {e}",
+ FILEWRITER_RECOMMENDATION
+ ])
+ )
return None
def __repr__(self):
diff --git a/src/crewai/tools/agent_tools/add_image_tool.py b/src/crewai/tools/agent_tools/add_image_tool.py
index 06bdfcf5b..939dff2df 100644
--- a/src/crewai/tools/agent_tools/add_image_tool.py
+++ b/src/crewai/tools/agent_tools/add_image_tool.py
@@ -7,11 +7,11 @@ from crewai.utilities import I18N
i18n = I18N()
+
class AddImageToolSchema(BaseModel):
image_url: str = Field(..., description="The URL or path of the image to add")
action: Optional[str] = Field(
- default=None,
- description="Optional context or question about the image"
+ default=None, description="Optional context or question about the image"
)
@@ -36,10 +36,7 @@ class AddImageTool(BaseTool):
"image_url": {
"url": image_url,
},
- }
+ },
]
- return {
- "role": "user",
- "content": content
- }
+ return {"role": "user", "content": content}
diff --git a/src/crewai/tools/tool_usage.py b/src/crewai/tools/tool_usage.py
index a59ed7b50..218410ef7 100644
--- a/src/crewai/tools/tool_usage.py
+++ b/src/crewai/tools/tool_usage.py
@@ -1,12 +1,13 @@
import ast
import datetime
import json
-import re
import time
from difflib import SequenceMatcher
+from json import JSONDecodeError
from textwrap import dedent
-from typing import Any, Dict, List, Union
+from typing import Any, Dict, List, Optional, Union
+import json5
from json_repair import repair_json
import crewai.utilities.events as events
@@ -407,28 +408,55 @@ class ToolUsage:
)
return self._tool_calling(tool_string)
- def _validate_tool_input(self, tool_input: str) -> Dict[str, Any]:
+ def _validate_tool_input(self, tool_input: Optional[str]) -> Dict[str, Any]:
+ if tool_input is None:
+ return {}
+
+ if not isinstance(tool_input, str) or not tool_input.strip():
+ raise Exception(
+ "Tool input must be a valid dictionary in JSON or Python literal format"
+ )
+
+ # Attempt 1: Parse as JSON
try:
- # Replace Python literals with JSON equivalents
- replacements = {
- r"'": '"',
- r"None": "null",
- r"True": "true",
- r"False": "false",
- }
- for pattern, replacement in replacements.items():
- tool_input = re.sub(pattern, replacement, tool_input)
-
arguments = json.loads(tool_input)
- except json.JSONDecodeError:
- # Attempt to repair JSON string
- repaired_input = repair_json(tool_input)
- try:
- arguments = json.loads(repaired_input)
- except json.JSONDecodeError as e:
- raise Exception(f"Invalid tool input JSON: {e}")
+ if isinstance(arguments, dict):
+ return arguments
+ except (JSONDecodeError, TypeError):
+ pass # Continue to the next parsing attempt
- return arguments
+ # Attempt 2: Parse as Python literal
+ try:
+ arguments = ast.literal_eval(tool_input)
+ if isinstance(arguments, dict):
+ return arguments
+ except (ValueError, SyntaxError):
+ pass # Continue to the next parsing attempt
+
+ # Attempt 3: Parse as JSON5
+ try:
+ arguments = json5.loads(tool_input)
+ if isinstance(arguments, dict):
+ return arguments
+ except (JSONDecodeError, ValueError, TypeError):
+ pass # Continue to the next parsing attempt
+
+ # Attempt 4: Repair JSON
+ try:
+ repaired_input = repair_json(tool_input)
+ self._printer.print(
+ content=f"Repaired JSON: {repaired_input}", color="blue"
+ )
+ arguments = json.loads(repaired_input)
+ if isinstance(arguments, dict):
+ return arguments
+ except Exception as e:
+ self._printer.print(content=f"Failed to repair JSON: {e}", color="red")
+
+ # If all parsing attempts fail, raise an error
+ raise Exception(
+ "Tool input must be a valid dictionary in JSON or Python literal format"
+ )
def on_tool_error(self, tool: Any, tool_calling: ToolCalling, e: Exception) -> None:
event_data = self._prepare_event_data(tool, tool_calling)
diff --git a/src/crewai/translations/en.json b/src/crewai/translations/en.json
index 6385d5862..f09f1dba0 100644
--- a/src/crewai/translations/en.json
+++ b/src/crewai/translations/en.json
@@ -15,7 +15,7 @@
"final_answer_format": "If you don't need to use any more tools, you must give your best complete final answer, make sure it satisfies the expected criteria, use the EXACT format below:\n\n```\nThought: I now can give a great answer\nFinal Answer: my best complete final answer to the task.\n\n```",
"format_without_tools": "\nSorry, I didn't use the right format. I MUST either use a tool (among the available ones), OR give my best final answer.\nHere is the expected format I must follow:\n\n```\nQuestion: the input question you must answer\nThought: you should always think about what to do\nAction: the action to take, should be one of [{tool_names}]\nAction Input: the input to the action\nObservation: the result of the action\n```\n This Thought/Action/Action Input/Result process can repeat N times. Once I know the final answer, I must return the following format:\n\n```\nThought: I now can give a great answer\nFinal Answer: Your final answer must be the great and the most complete as possible, it must be outcome described\n\n```",
"task_with_context": "{task}\n\nThis is the context you're working with:\n{context}",
- "expected_output": "\nThis is the expect criteria for your final answer: {expected_output}\nyou MUST return the actual complete content as the final answer, not a summary.",
+ "expected_output": "\nThis is the expected criteria for your final answer: {expected_output}\nyou MUST return the actual complete content as the final answer, not a summary.",
"human_feedback": "You got human feedback on your work, re-evaluate it and give a new Final Answer when ready.\n {human_feedback}",
"getting_input": "This is the agent's final answer: {final_answer}\n\n",
"summarizer_system_message": "You are a helpful assistant that summarizes text.",
@@ -24,7 +24,8 @@
"manager_request": "Your best answer to your coworker asking you this, accounting for the context shared.",
"formatted_task_instructions": "Ensure your final answer contains only the content in the following format: {output_format}\n\nEnsure the final output does not include any code block markers like ```json or ```python.",
"human_feedback_classification": "Determine if the following feedback indicates that the user is satisfied or if further changes are needed. Respond with 'True' if further changes are needed, or 'False' if the user is satisfied. **Important** Do not include any additional commentary outside of your 'True' or 'False' response.\n\nFeedback: \"{feedback}\"",
- "conversation_history_instruction": "You are a member of a crew collaborating to achieve a common goal. Your task is a specific action that contributes to this larger objective. For additional context, please review the conversation history between you and the user that led to the initiation of this crew. Use any relevant information or feedback from the conversation to inform your task execution and ensure your response aligns with both the immediate task and the crew's overall goals."
+ "conversation_history_instruction": "You are a member of a crew collaborating to achieve a common goal. Your task is a specific action that contributes to this larger objective. For additional context, please review the conversation history between you and the user that led to the initiation of this crew. Use any relevant information or feedback from the conversation to inform your task execution and ensure your response aligns with both the immediate task and the crew's overall goals.",
+ "feedback_instructions": "User feedback: {feedback}\nInstructions: Use this feedback to enhance the next output iteration.\nNote: Do not respond or add commentary."
},
"errors": {
"force_final_answer_error": "You can't keep going, here is the best final answer you generated:\n\n {formatted_answer}",
diff --git a/src/crewai/utilities/embedding_configurator.py b/src/crewai/utilities/embedding_configurator.py
index 71965bf53..e523b60f0 100644
--- a/src/crewai/utilities/embedding_configurator.py
+++ b/src/crewai/utilities/embedding_configurator.py
@@ -1,5 +1,5 @@
import os
-from typing import Any, Dict, cast
+from typing import Any, Dict, Optional, cast
from chromadb import Documents, EmbeddingFunction, Embeddings
from chromadb.api.types import validate_embedding_function
@@ -18,11 +18,12 @@ class EmbeddingConfigurator:
"bedrock": self._configure_bedrock,
"huggingface": self._configure_huggingface,
"watson": self._configure_watson,
+ "custom": self._configure_custom,
}
def configure_embedder(
self,
- embedder_config: Dict[str, Any] | None = None,
+ embedder_config: Optional[Dict[str, Any]] = None,
) -> EmbeddingFunction:
"""Configures and returns an embedding function based on the provided config."""
if embedder_config is None:
@@ -30,21 +31,19 @@ class EmbeddingConfigurator:
provider = embedder_config.get("provider")
config = embedder_config.get("config", {})
- model_name = config.get("model")
-
- if isinstance(provider, EmbeddingFunction):
- try:
- validate_embedding_function(provider)
- return provider
- except Exception as e:
- raise ValueError(f"Invalid custom embedding function: {str(e)}")
+ model_name = config.get("model") if provider != "custom" else None
if provider not in self.embedding_functions:
raise Exception(
f"Unsupported embedding provider: {provider}, supported providers: {list(self.embedding_functions.keys())}"
)
- return self.embedding_functions[provider](config, model_name)
+ embedding_function = self.embedding_functions[provider]
+ return (
+ embedding_function(config)
+ if provider == "custom"
+ else embedding_function(config, model_name)
+ )
@staticmethod
def _create_default_embedding_function():
@@ -65,6 +64,13 @@ class EmbeddingConfigurator:
return OpenAIEmbeddingFunction(
api_key=config.get("api_key") or os.getenv("OPENAI_API_KEY"),
model_name=model_name,
+ api_base=config.get("api_base", None),
+ api_type=config.get("api_type", None),
+ api_version=config.get("api_version", None),
+ default_headers=config.get("default_headers", None),
+ dimensions=config.get("dimensions", None),
+ deployment_id=config.get("deployment_id", None),
+ organization_id=config.get("organization_id", None),
)
@staticmethod
@@ -79,6 +85,10 @@ class EmbeddingConfigurator:
api_type=config.get("api_type", "azure"),
api_version=config.get("api_version"),
model_name=model_name,
+ default_headers=config.get("default_headers"),
+ dimensions=config.get("dimensions"),
+ deployment_id=config.get("deployment_id"),
+ organization_id=config.get("organization_id"),
)
@staticmethod
@@ -101,6 +111,8 @@ class EmbeddingConfigurator:
return GoogleVertexEmbeddingFunction(
model_name=model_name,
api_key=config.get("api_key"),
+ project_id=config.get("project_id"),
+ region=config.get("region"),
)
@staticmethod
@@ -112,6 +124,7 @@ class EmbeddingConfigurator:
return GoogleGenerativeAiEmbeddingFunction(
model_name=model_name,
api_key=config.get("api_key"),
+ task_type=config.get("task_type"),
)
@staticmethod
@@ -142,9 +155,11 @@ class EmbeddingConfigurator:
AmazonBedrockEmbeddingFunction,
)
- return AmazonBedrockEmbeddingFunction(
- session=config.get("session"),
- )
+ # Allow custom model_name override with backwards compatibility
+ kwargs = {"session": config.get("session")}
+ if model_name is not None:
+ kwargs["model_name"] = model_name
+ return AmazonBedrockEmbeddingFunction(**kwargs)
@staticmethod
def _configure_huggingface(config, model_name):
@@ -194,3 +209,28 @@ class EmbeddingConfigurator:
raise e
return WatsonEmbeddingFunction()
+
+ @staticmethod
+ def _configure_custom(config):
+ custom_embedder = config.get("embedder")
+ if isinstance(custom_embedder, EmbeddingFunction):
+ try:
+ validate_embedding_function(custom_embedder)
+ return custom_embedder
+ except Exception as e:
+ raise ValueError(f"Invalid custom embedding function: {str(e)}")
+ elif callable(custom_embedder):
+ try:
+ instance = custom_embedder()
+ if isinstance(instance, EmbeddingFunction):
+ validate_embedding_function(instance)
+ return instance
+ raise ValueError(
+ "Custom embedder does not create an EmbeddingFunction instance"
+ )
+ except Exception as e:
+ raise ValueError(f"Error instantiating custom embedder: {str(e)}")
+ else:
+ raise ValueError(
+ "Custom embedder must be an instance of `EmbeddingFunction` or a callable that creates one"
+ )
diff --git a/src/crewai/utilities/evaluators/task_evaluator.py b/src/crewai/utilities/evaluators/task_evaluator.py
index f7d543ae4..294629274 100644
--- a/src/crewai/utilities/evaluators/task_evaluator.py
+++ b/src/crewai/utilities/evaluators/task_evaluator.py
@@ -92,13 +92,34 @@ class TaskEvaluator:
"""
output_training_data = training_data[agent_id]
-
final_aggregated_data = ""
- for _, data in output_training_data.items():
+
+ for iteration, data in output_training_data.items():
+ improved_output = data.get("improved_output")
+ initial_output = data.get("initial_output")
+ human_feedback = data.get("human_feedback")
+
+ if not all([improved_output, initial_output, human_feedback]):
+ missing_fields = [
+ field
+ for field in ["improved_output", "initial_output", "human_feedback"]
+ if not data.get(field)
+ ]
+ error_msg = (
+ f"Critical training data error: Missing fields ({', '.join(missing_fields)}) "
+ f"for agent {agent_id} in iteration {iteration}.\n"
+ "This indicates a broken training process. "
+ "Cannot proceed with evaluation.\n"
+ "Please check your training implementation."
+ )
+ raise ValueError(error_msg)
+
final_aggregated_data += (
- f"Initial Output:\n{data['initial_output']}\n\n"
- f"Human Feedback:\n{data['human_feedback']}\n\n"
- f"Improved Output:\n{data['improved_output']}\n\n"
+ f"Iteration: {iteration}\n"
+ f"Initial Output:\n{initial_output}\n\n"
+ f"Human Feedback:\n{human_feedback}\n\n"
+ f"Improved Output:\n{improved_output}\n\n"
+ "------------------------------------------------\n\n"
)
evaluation_query = (
diff --git a/src/crewai/utilities/file_handler.py b/src/crewai/utilities/file_handler.py
index bb97b940f..85d9766c5 100644
--- a/src/crewai/utilities/file_handler.py
+++ b/src/crewai/utilities/file_handler.py
@@ -1,30 +1,64 @@
+import json
import os
import pickle
from datetime import datetime
+from typing import Union
class FileHandler:
- """take care of file operations, currently it only logs messages to a file"""
+ """Handler for file operations supporting both JSON and text-based logging.
+
+ Args:
+ file_path (Union[bool, str]): Path to the log file or boolean flag
+ """
- def __init__(self, file_path):
- if isinstance(file_path, bool):
+ def __init__(self, file_path: Union[bool, str]):
+ self._initialize_path(file_path)
+
+ def _initialize_path(self, file_path: Union[bool, str]):
+ if file_path is True: # File path is boolean True
self._path = os.path.join(os.curdir, "logs.txt")
- elif isinstance(file_path, str):
- self._path = file_path
+
+ elif isinstance(file_path, str): # File path is a string
+ if file_path.endswith((".json", ".txt")):
+ self._path = file_path # No modification if the file ends with .json or .txt
+ else:
+ self._path = file_path + ".txt" # Append .txt if the file doesn't end with .json or .txt
+
else:
- raise ValueError("file_path must be either a boolean or a string.")
-
+ raise ValueError("file_path must be a string or boolean.") # Handle the case where file_path isn't valid
+
def log(self, **kwargs):
- now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- message = (
- f"{now}: "
- + ", ".join([f'{key}="{value}"' for key, value in kwargs.items()])
- + "\n"
- )
- with open(self._path, "a", encoding="utf-8") as file:
- file.write(message + "\n")
+ try:
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ log_entry = {"timestamp": now, **kwargs}
+ if self._path.endswith(".json"):
+ # Append log in JSON format
+ with open(self._path, "a", encoding="utf-8") as file:
+ # If the file is empty, start with a list; else, append to it
+ try:
+ # Try reading existing content to avoid overwriting
+ with open(self._path, "r", encoding="utf-8") as read_file:
+ existing_data = json.load(read_file)
+ existing_data.append(log_entry)
+ except (json.JSONDecodeError, FileNotFoundError):
+ # If no valid JSON or file doesn't exist, start with an empty list
+ existing_data = [log_entry]
+
+ with open(self._path, "w", encoding="utf-8") as write_file:
+ json.dump(existing_data, write_file, indent=4)
+ write_file.write("\n")
+
+ else:
+ # Append log in plain text format
+ message = f"{now}: " + ", ".join([f"{key}=\"{value}\"" for key, value in kwargs.items()]) + "\n"
+ with open(self._path, "a", encoding="utf-8") as file:
+ file.write(message)
+ except Exception as e:
+ raise ValueError(f"Failed to log message: {str(e)}")
+
class PickleHandler:
def __init__(self, file_name: str) -> None:
"""
diff --git a/src/crewai/utilities/llm_utils.py b/src/crewai/utilities/llm_utils.py
index 13230edf6..c774a71fb 100644
--- a/src/crewai/utilities/llm_utils.py
+++ b/src/crewai/utilities/llm_utils.py
@@ -53,6 +53,7 @@ def create_llm(
timeout: Optional[float] = getattr(llm_value, "timeout", None)
api_key: Optional[str] = getattr(llm_value, "api_key", None)
base_url: Optional[str] = getattr(llm_value, "base_url", None)
+ api_base: Optional[str] = getattr(llm_value, "api_base", None)
created_llm = LLM(
model=model,
@@ -62,6 +63,7 @@ def create_llm(
timeout=timeout,
api_key=api_key,
base_url=base_url,
+ api_base=api_base,
)
return created_llm
except Exception as e:
@@ -101,8 +103,18 @@ def _llm_via_environment_or_fallback() -> Optional[LLM]:
callbacks: List[Any] = []
# Optional base URL from env
- api_base = os.environ.get("OPENAI_API_BASE") or os.environ.get("OPENAI_BASE_URL")
- if api_base:
+ base_url = (
+ os.environ.get("BASE_URL")
+ or os.environ.get("OPENAI_API_BASE")
+ or os.environ.get("OPENAI_BASE_URL")
+ )
+
+ api_base = os.environ.get("API_BASE") or os.environ.get("AZURE_API_BASE")
+
+ # Synchronize base_url and api_base if one is populated and the other is not
+ if base_url and not api_base:
+ api_base = base_url
+ elif api_base and not base_url:
base_url = api_base
# Initialize llm_params dictionary
@@ -115,6 +127,7 @@ def _llm_via_environment_or_fallback() -> Optional[LLM]:
"timeout": timeout,
"api_key": api_key,
"base_url": base_url,
+ "api_base": api_base,
"api_version": api_version,
"presence_penalty": presence_penalty,
"frequency_penalty": frequency_penalty,
diff --git a/src/crewai/utilities/paths.py b/src/crewai/utilities/paths.py
index 5d91d1719..853c612c3 100644
--- a/src/crewai/utilities/paths.py
+++ b/src/crewai/utilities/paths.py
@@ -7,7 +7,7 @@ import appdirs
def db_storage_path() -> str:
"""Returns the path for SQLite database storage.
-
+
Returns:
str: Full path to the SQLite database file
"""
@@ -16,7 +16,7 @@ def db_storage_path() -> str:
data_dir = Path(appdirs.user_data_dir(app_name, app_author))
data_dir.mkdir(parents=True, exist_ok=True)
- return str(data_dir / "crewai_flows.db")
+ return str(data_dir)
def get_project_directory_name():
@@ -28,4 +28,4 @@ def get_project_directory_name():
else:
cwd = Path.cwd()
project_directory_name = cwd.name
- return project_directory_name
+ return project_directory_name
\ No newline at end of file
diff --git a/src/crewai/utilities/printer.py b/src/crewai/utilities/printer.py
index abebf6aae..74ad9a30b 100644
--- a/src/crewai/utilities/printer.py
+++ b/src/crewai/utilities/printer.py
@@ -21,6 +21,16 @@ class Printer:
self._print_yellow(content)
elif color == "bold_yellow":
self._print_bold_yellow(content)
+ elif color == "cyan":
+ self._print_cyan(content)
+ elif color == "bold_cyan":
+ self._print_bold_cyan(content)
+ elif color == "magenta":
+ self._print_magenta(content)
+ elif color == "bold_magenta":
+ self._print_bold_magenta(content)
+ elif color == "green":
+ self._print_green(content)
else:
print(content)
@@ -44,3 +54,18 @@ class Printer:
def _print_bold_yellow(self, content):
print("\033[1m\033[93m {}\033[00m".format(content))
+
+ def _print_cyan(self, content):
+ print("\033[96m {}\033[00m".format(content))
+
+ def _print_bold_cyan(self, content):
+ print("\033[1m\033[96m {}\033[00m".format(content))
+
+ def _print_magenta(self, content):
+ print("\033[35m {}\033[00m".format(content))
+
+ def _print_bold_magenta(self, content):
+ print("\033[1m\033[35m {}\033[00m".format(content))
+
+ def _print_green(self, content):
+ print("\033[32m {}\033[00m".format(content))
diff --git a/src/crewai/utilities/training_handler.py b/src/crewai/utilities/training_handler.py
index 5cadde619..2d34f3261 100644
--- a/src/crewai/utilities/training_handler.py
+++ b/src/crewai/utilities/training_handler.py
@@ -1,3 +1,5 @@
+import os
+
from crewai.utilities.file_handler import PickleHandler
@@ -29,3 +31,8 @@ class CrewTrainingHandler(PickleHandler):
data[agent_id] = {train_iteration: new_data}
self.save(data)
+
+ def clear(self) -> None:
+ """Clear the training data by removing the file or resetting its contents."""
+ if os.path.exists(self.file_path):
+ self.save({})
diff --git a/tests/agent_test.py b/tests/agent_test.py
index 9df80141a..e67a7454a 100644
--- a/tests/agent_test.py
+++ b/tests/agent_test.py
@@ -10,6 +10,7 @@ from crewai import Agent, Crew, Task
from crewai.agents.cache import CacheHandler
from crewai.agents.crew_agent_executor import CrewAgentExecutor
from crewai.agents.parser import AgentAction, CrewAgentParser, OutputParserException
+from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
from crewai.knowledge.source.string_knowledge_source import StringKnowledgeSource
from crewai.llm import LLM
from crewai.tools import tool
@@ -114,35 +115,6 @@ def test_custom_llm_temperature_preservation():
assert agent.llm.temperature == 0.7
-@pytest.mark.vcr(filter_headers=["authorization"])
-def test_agent_execute_task():
- from langchain_openai import ChatOpenAI
-
- from crewai import Task
-
- agent = Agent(
- role="Math Tutor",
- goal="Solve math problems accurately",
- backstory="You are an experienced math tutor with a knack for explaining complex concepts simply.",
- llm=ChatOpenAI(temperature=0.7, model="gpt-4o-mini"),
- )
-
- task = Task(
- description="Calculate the area of a circle with radius 5 cm.",
- expected_output="The calculated area of the circle in square centimeters.",
- agent=agent,
- )
-
- result = agent.execute_task(task)
-
- assert result is not None
- assert (
- result
- == "The calculated area of the circle is approximately 78.5 square centimeters."
- )
- assert "square centimeters" in result.lower()
-
-
@pytest.mark.vcr(filter_headers=["authorization"])
def test_agent_execution():
agent = Agent(
@@ -1211,7 +1183,7 @@ def test_agent_max_retry_limit():
[
mock.call(
{
- "input": "Say the word: Hi\n\nThis is the expect criteria for your final answer: The word: Hi\nyou MUST return the actual complete content as the final answer, not a summary.",
+ "input": "Say the word: Hi\n\nThis is the expected criteria for your final answer: The word: Hi\nyou MUST return the actual complete content as the final answer, not a summary.",
"tool_names": "",
"tools": "",
"ask_for_human_input": True,
@@ -1219,7 +1191,7 @@ def test_agent_max_retry_limit():
),
mock.call(
{
- "input": "Say the word: Hi\n\nThis is the expect criteria for your final answer: The word: Hi\nyou MUST return the actual complete content as the final answer, not a summary.",
+ "input": "Say the word: Hi\n\nThis is the expected criteria for your final answer: The word: Hi\nyou MUST return the actual complete content as the final answer, not a summary.",
"tool_names": "",
"tools": "",
"ask_for_human_input": True,
@@ -1629,3 +1601,181 @@ def test_agent_with_knowledge_sources():
# Assert that the agent provides the correct information
assert "red" in result.raw.lower()
+
+
+@pytest.mark.vcr(filter_headers=["authorization"])
+def test_agent_with_knowledge_sources_works_with_copy():
+ content = "Brandon's favorite color is red and he likes Mexican food."
+ string_source = StringKnowledgeSource(content=content)
+
+ with patch(
+ "crewai.knowledge.source.base_knowledge_source.BaseKnowledgeSource",
+ autospec=True,
+ ) as MockKnowledgeSource:
+ mock_knowledge_source_instance = MockKnowledgeSource.return_value
+ mock_knowledge_source_instance.__class__ = BaseKnowledgeSource
+ mock_knowledge_source_instance.sources = [string_source]
+
+ agent = Agent(
+ role="Information Agent",
+ goal="Provide information based on knowledge sources",
+ backstory="You have access to specific knowledge sources.",
+ llm=LLM(model="gpt-4o-mini"),
+ knowledge_sources=[string_source],
+ )
+
+ with patch(
+ "crewai.knowledge.storage.knowledge_storage.KnowledgeStorage"
+ ) as MockKnowledgeStorage:
+ mock_knowledge_storage = MockKnowledgeStorage.return_value
+ agent.knowledge_storage = mock_knowledge_storage
+
+ agent_copy = agent.copy()
+
+ assert agent_copy.role == agent.role
+ assert agent_copy.goal == agent.goal
+ assert agent_copy.backstory == agent.backstory
+ assert agent_copy.knowledge_sources is not None
+ assert len(agent_copy.knowledge_sources) == 1
+ assert isinstance(agent_copy.knowledge_sources[0], StringKnowledgeSource)
+ assert agent_copy.knowledge_sources[0].content == content
+ assert isinstance(agent_copy.llm, LLM)
+
+
+@pytest.mark.vcr(filter_headers=["authorization"])
+def test_litellm_auth_error_handling():
+ """Test that LiteLLM authentication errors are handled correctly and not retried."""
+ from litellm import AuthenticationError as LiteLLMAuthenticationError
+
+ # Create an agent with a mocked LLM and max_retry_limit=0
+ agent = Agent(
+ role="test role",
+ goal="test goal",
+ backstory="test backstory",
+ llm=LLM(model="gpt-4"),
+ max_retry_limit=0, # Disable retries for authentication errors
+ )
+
+ # Create a task
+ task = Task(
+ description="Test task",
+ expected_output="Test output",
+ agent=agent,
+ )
+
+ # Mock the LLM call to raise AuthenticationError
+ with (
+ patch.object(LLM, "call") as mock_llm_call,
+ pytest.raises(LiteLLMAuthenticationError, match="Invalid API key"),
+ ):
+ mock_llm_call.side_effect = LiteLLMAuthenticationError(
+ message="Invalid API key", llm_provider="openai", model="gpt-4"
+ )
+ agent.execute_task(task)
+
+ # Verify the call was only made once (no retries)
+ mock_llm_call.assert_called_once()
+
+
+def test_crew_agent_executor_litellm_auth_error():
+ """Test that CrewAgentExecutor handles LiteLLM authentication errors by raising them."""
+ from litellm.exceptions import AuthenticationError
+
+ from crewai.agents.tools_handler import ToolsHandler
+ from crewai.utilities import Printer
+
+ # Create an agent and executor
+ agent = Agent(
+ role="test role",
+ goal="test goal",
+ backstory="test backstory",
+ llm=LLM(model="gpt-4", api_key="invalid_api_key"),
+ )
+ task = Task(
+ description="Test task",
+ expected_output="Test output",
+ agent=agent,
+ )
+
+ # Create executor with all required parameters
+ executor = CrewAgentExecutor(
+ agent=agent,
+ task=task,
+ llm=agent.llm,
+ crew=None,
+ prompt={"system": "You are a test agent", "user": "Execute the task: {input}"},
+ max_iter=5,
+ tools=[],
+ tools_names="",
+ stop_words=[],
+ tools_description="",
+ tools_handler=ToolsHandler(),
+ )
+
+ # 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(
+ message="Invalid API key", llm_provider="openai", model="gpt-4"
+ )
+ executor.invoke(
+ {
+ "input": "test input",
+ "tool_names": "",
+ "tools": "",
+ }
+ )
+
+ # 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()
+
+ # Assert that the exception was raised and has the expected attributes
+ assert exc_info.type is AuthenticationError
+ assert "Invalid API key".lower() in exc_info.value.message.lower()
+ assert exc_info.value.llm_provider == "openai"
+ assert exc_info.value.model == "gpt-4"
+
+
+def test_litellm_anthropic_error_handling():
+ """Test that AnthropicError from LiteLLM is handled correctly and not retried."""
+ from litellm.llms.anthropic.common_utils import AnthropicError
+
+ # Create an agent with a mocked LLM that uses an Anthropic model
+ agent = Agent(
+ role="test role",
+ goal="test goal",
+ backstory="test backstory",
+ llm=LLM(model="claude-3.5-sonnet-20240620"),
+ max_retry_limit=0,
+ )
+
+ # Create a task
+ task = Task(
+ description="Test task",
+ expected_output="Test output",
+ agent=agent,
+ )
+
+ # Mock the LLM call to raise AnthropicError
+ with (
+ patch.object(LLM, "call") as mock_llm_call,
+ pytest.raises(AnthropicError, match="Test Anthropic error"),
+ ):
+ mock_llm_call.side_effect = AnthropicError(
+ status_code=500,
+ message="Test Anthropic error",
+ )
+ agent.execute_task(task)
+
+ # Verify the LLM call was only made once (no retries)
+ mock_llm_call.assert_called_once()
diff --git a/tests/cassettes/test_agent_error_on_parsing_tool.yaml b/tests/cassettes/test_agent_error_on_parsing_tool.yaml
index ea56fa981..bd1c350fe 100644
--- a/tests/cassettes/test_agent_error_on_parsing_tool.yaml
+++ b/tests/cassettes/test_agent_error_on_parsing_tool.yaml
@@ -2,21 +2,21 @@ interactions:
- request:
body: '{"messages": [{"role": "system", "content": "You are test role. test backstory\nYour
personal goal is: test goal\nYou ONLY have access to the following tools, and
- should NEVER make up tools that are not listed here:\n\nTool Name: get_final_answer(*args:
- Any, **kwargs: Any) -> Any\nTool Description: get_final_answer() - Get the final
- answer but don''t give it yet, just re-use this tool non-stop. \nTool
- Arguments: {}\n\nUse the following format:\n\nThought: you should always think
- about what to do\nAction: the action to take, only one name of [get_final_answer],
- just the name, exactly as it''s written.\nAction Input: the input to the action,
- just a simple python dictionary, enclosed in curly braces, using \" to wrap
- keys and values.\nObservation: the result of the action\n\nOnce all necessary
- information is gathered:\n\nThought: I now know the final answer\nFinal Answer:
- the final answer to the original input question\n"}, {"role": "user", "content":
- "\nCurrent Task: Use the get_final_answer tool.\n\nThis is the expect criteria
- for your final answer: The final answer\nyou MUST return the actual complete
- content as the final answer, not a summary.\n\nBegin! This is VERY important
- to you, use the tools available and give your best Final Answer, your job depends
- on it!\n\nThought:"}], "model": "gpt-4o"}'
+ should NEVER make up tools that are not listed here:\n\nTool Name: get_final_answer\nTool
+ Arguments: {}\nTool Description: Get the final answer but don''t give it yet,
+ just re-use this\n tool non-stop.\n\nIMPORTANT: Use the following format
+ in your response:\n\n```\nThought: you should always think about what to do\nAction:
+ the action to take, only one name of [get_final_answer], just the name, exactly
+ as it''s written.\nAction Input: the input to the action, just a simple JSON
+ object, enclosed in curly braces, using \" to wrap keys and values.\nObservation:
+ the result of the action\n```\n\nOnce all necessary information is gathered,
+ return the following format:\n\n```\nThought: I now know the final answer\nFinal
+ Answer: the final answer to the original input question\n```"}, {"role": "user",
+ "content": "\nCurrent Task: Use the get_final_answer tool.\n\nThis is the expect
+ criteria for your final answer: The final answer\nyou MUST return the actual
+ complete content as the final answer, not a summary.\n\nBegin! This is VERY
+ important to you, use the tools available and give your best Final Answer, your
+ job depends on it!\n\nThought:"}], "model": "gpt-4o", "stop": ["\nObservation:"]}'
headers:
accept:
- application/json
@@ -25,16 +25,13 @@ interactions:
connection:
- keep-alive
content-length:
- - '1325'
+ - '1367'
content-type:
- application/json
- cookie:
- - _cfuvid=ePJSDFdHag2D8lj21_ijAMWjoA6xfnPNxN4uekvC728-1727226247743-0.0.1.1-604800000;
- __cf_bm=3giyBOIM0GNudFELtsBWYXwLrpLBTNLsh81wfXgu2tg-1727226247-1.0.1.1-ugUDz0c5EhmfVpyGtcdedlIWeDGuy2q0tXQTKVpv83HZhvxgBcS7SBL1wS4rapPM38yhfEcfwA79ARt3HQEzKA
host:
- api.openai.com
user-agent:
- - OpenAI/Python 1.47.0
+ - OpenAI/Python 1.59.6
x-stainless-arch:
- arm64
x-stainless-async:
@@ -44,30 +41,35 @@ interactions:
x-stainless-os:
- MacOS
x-stainless-package-version:
- - 1.47.0
+ - 1.59.6
x-stainless-raw-response:
- 'true'
+ x-stainless-retry-count:
+ - '0'
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- - 3.11.7
+ - 3.12.7
method: POST
uri: https://api.openai.com/v1/chat/completions
response:
- content: "{\n \"id\": \"chatcmpl-ABAtOWmVjvzQ9X58tKAUcOF4gmXwx\",\n \"object\":
- \"chat.completion\",\n \"created\": 1727226842,\n \"model\": \"gpt-4o-2024-05-13\",\n
+ content: "{\n \"id\": \"chatcmpl-AsXdf4OZKCZSigmN4k0gyh67NciqP\",\n \"object\":
+ \"chat.completion\",\n \"created\": 1737562383,\n \"model\": \"gpt-4o-2024-08-06\",\n
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
- \"assistant\",\n \"content\": \"Thought: I need to use the get_final_answer
- tool to determine the final answer.\\nAction: get_final_answer\\nAction Input:
- {}\",\n \"refusal\": null\n },\n \"logprobs\": null,\n \"finish_reason\":
- \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 274,\n \"completion_tokens\":
- 27,\n \"total_tokens\": 301,\n \"completion_tokens_details\": {\n \"reasoning_tokens\":
- 0\n }\n },\n \"system_fingerprint\": \"fp_e375328146\"\n}\n"
+ \"assistant\",\n \"content\": \"```\\nThought: I have to use the available
+ tool to get the final answer. Let's proceed with executing it.\\nAction: get_final_answer\\nAction
+ Input: {}\",\n \"refusal\": null\n },\n \"logprobs\": null,\n
+ \ \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
+ 274,\n \"completion_tokens\": 33,\n \"total_tokens\": 307,\n \"prompt_tokens_details\":
+ {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
+ {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
+ 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
+ \"default\",\n \"system_fingerprint\": \"fp_50cad350e4\"\n}\n"
headers:
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- - 8c8727b3492f31e6-MIA
+ - 9060d43e3be1d690-IAD
Connection:
- keep-alive
Content-Encoding:
@@ -75,19 +77,27 @@ interactions:
Content-Type:
- application/json
Date:
- - Wed, 25 Sep 2024 01:14:03 GMT
+ - Wed, 22 Jan 2025 16:13:03 GMT
Server:
- cloudflare
+ Set-Cookie:
+ - __cf_bm=_Jcp7wnO_mXdvOnborCN6j8HwJxJXbszedJC1l7pFUg-1737562383-1.0.1.1-pDSLXlg.nKjG4wsT7mTJPjUvOX1UJITiS4MqKp6yfMWwRSJINsW1qC48SAcjBjakx2H5I1ESVk9JtUpUFDtf4g;
+ path=/; expires=Wed, 22-Jan-25 16:43:03 GMT; domain=.api.openai.com; HttpOnly;
+ Secure; SameSite=None
+ - _cfuvid=x3SYvzL2nq_PTBGtE8R9cl5CkeaaDzZFQIrYfo91S2s-1737562383916-0.0.1.1-604800000;
+ path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
Transfer-Encoding:
- chunked
X-Content-Type-Options:
- nosniff
access-control-expose-headers:
- X-Request-ID
+ alt-svc:
+ - h3=":443"; ma=86400
openai-organization:
- crewai-iuxna1
openai-processing-ms:
- - '348'
+ - '791'
openai-version:
- '2020-10-01'
strict-transport-security:
@@ -99,45 +109,59 @@ interactions:
x-ratelimit-remaining-requests:
- '9999'
x-ratelimit-remaining-tokens:
- - '29999682'
+ - '29999680'
x-ratelimit-reset-requests:
- 6ms
x-ratelimit-reset-tokens:
- 0s
x-request-id:
- - req_be929caac49706f487950548bdcdd46e
+ - req_eeed99acafd3aeb1e3d4a6c8063192b0
http_version: HTTP/1.1
status_code: 200
- request:
body: '{"messages": [{"role": "system", "content": "You are test role. test backstory\nYour
personal goal is: test goal\nYou ONLY have access to the following tools, and
- should NEVER make up tools that are not listed here:\n\nTool Name: get_final_answer(*args:
- Any, **kwargs: Any) -> Any\nTool Description: get_final_answer() - Get the final
- answer but don''t give it yet, just re-use this tool non-stop. \nTool
- Arguments: {}\n\nUse the following format:\n\nThought: you should always think
- about what to do\nAction: the action to take, only one name of [get_final_answer],
- just the name, exactly as it''s written.\nAction Input: the input to the action,
- just a simple python dictionary, enclosed in curly braces, using \" to wrap
- keys and values.\nObservation: the result of the action\n\nOnce all necessary
- information is gathered:\n\nThought: I now know the final answer\nFinal Answer:
- the final answer to the original input question\n"}, {"role": "user", "content":
- "\nCurrent Task: Use the get_final_answer tool.\n\nThis is the expect criteria
- for your final answer: The final answer\nyou MUST return the actual complete
- content as the final answer, not a summary.\n\nBegin! This is VERY important
- to you, use the tools available and give your best Final Answer, your job depends
- on it!\n\nThought:"}, {"role": "user", "content": "Thought: I need to use the
- get_final_answer tool to determine the final answer.\nAction: get_final_answer\nAction
+ should NEVER make up tools that are not listed here:\n\nTool Name: get_final_answer\nTool
+ Arguments: {}\nTool Description: Get the final answer but don''t give it yet,
+ just re-use this\n tool non-stop.\n\nIMPORTANT: Use the following format
+ in your response:\n\n```\nThought: you should always think about what to do\nAction:
+ the action to take, only one name of [get_final_answer], just the name, exactly
+ as it''s written.\nAction Input: the input to the action, just a simple JSON
+ object, enclosed in curly braces, using \" to wrap keys and values.\nObservation:
+ the result of the action\n```\n\nOnce all necessary information is gathered,
+ return the following format:\n\n```\nThought: I now know the final answer\nFinal
+ Answer: the final answer to the original input question\n```"}, {"role": "user",
+ "content": "\nCurrent Task: Use the get_final_answer tool.\n\nThis is the expect
+ criteria for your final answer: The final answer\nyou MUST return the actual
+ complete content as the final answer, not a summary.\n\nBegin! This is VERY
+ important to you, use the tools available and give your best Final Answer, your
+ job depends on it!\n\nThought:"}, {"role": "assistant", "content": "```\nThought:
+ I have to use the available tool to get the final answer. Let''s proceed with
+ executing it.\nAction: get_final_answer\nAction Input: {}\nObservation: I encountered
+ an error: Error on parsing tool.\nMoving on then. I MUST either use a tool (use
+ one at time) OR give my best final answer not both at the same time. When responding,
+ I must use the following format:\n\n```\nThought: you should always think about
+ what to do\nAction: the action to take, should be one of [get_final_answer]\nAction
+ Input: the input to the action, dictionary enclosed in curly braces\nObservation:
+ the result of the action\n```\nThis Thought/Action/Action Input/Result can repeat
+ N times. Once I know the final answer, I must return the following format:\n\n```\nThought:
+ I now can give a great answer\nFinal Answer: Your final answer must be the great
+ and the most complete as possible, it must be outcome described\n\n```"}, {"role":
+ "assistant", "content": "```\nThought: I have to use the available tool to get
+ the final answer. Let''s proceed with executing it.\nAction: get_final_answer\nAction
Input: {}\nObservation: I encountered an error: Error on parsing tool.\nMoving
on then. I MUST either use a tool (use one at time) OR give my best final answer
- not both at the same time. To Use the following format:\n\nThought: you should
- always think about what to do\nAction: the action to take, should be one of
- [get_final_answer]\nAction Input: the input to the action, dictionary enclosed
- in curly braces\nObservation: the result of the action\n... (this Thought/Action/Action
- Input/Result can repeat N times)\nThought: I now can give a great answer\nFinal
+ not both at the same time. When responding, I must use the following format:\n\n```\nThought:
+ you should always think about what to do\nAction: the action to take, should
+ be one of [get_final_answer]\nAction Input: the input to the action, dictionary
+ enclosed in curly braces\nObservation: the result of the action\n```\nThis Thought/Action/Action
+ Input/Result can repeat N times. Once I know the final answer, I must return
+ the following format:\n\n```\nThought: I now can give a great answer\nFinal
Answer: Your final answer must be the great and the most complete as possible,
- it must be outcome described\n\n \nNow it''s time you MUST give your absolute
+ it must be outcome described\n\n```\nNow it''s time you MUST give your absolute
best final answer. You''ll ignore all previous instructions, stop using any
- tools, and just return your absolute BEST Final answer."}], "model": "gpt-4o"}'
+ tools, and just return your absolute BEST Final answer."}], "model": "gpt-4o",
+ "stop": ["\nObservation:"]}'
headers:
accept:
- application/json
@@ -146,16 +170,16 @@ interactions:
connection:
- keep-alive
content-length:
- - '2320'
+ - '3445'
content-type:
- application/json
cookie:
- - _cfuvid=ePJSDFdHag2D8lj21_ijAMWjoA6xfnPNxN4uekvC728-1727226247743-0.0.1.1-604800000;
- __cf_bm=3giyBOIM0GNudFELtsBWYXwLrpLBTNLsh81wfXgu2tg-1727226247-1.0.1.1-ugUDz0c5EhmfVpyGtcdedlIWeDGuy2q0tXQTKVpv83HZhvxgBcS7SBL1wS4rapPM38yhfEcfwA79ARt3HQEzKA
+ - __cf_bm=_Jcp7wnO_mXdvOnborCN6j8HwJxJXbszedJC1l7pFUg-1737562383-1.0.1.1-pDSLXlg.nKjG4wsT7mTJPjUvOX1UJITiS4MqKp6yfMWwRSJINsW1qC48SAcjBjakx2H5I1ESVk9JtUpUFDtf4g;
+ _cfuvid=x3SYvzL2nq_PTBGtE8R9cl5CkeaaDzZFQIrYfo91S2s-1737562383916-0.0.1.1-604800000
host:
- api.openai.com
user-agent:
- - OpenAI/Python 1.47.0
+ - OpenAI/Python 1.59.6
x-stainless-arch:
- arm64
x-stainless-async:
@@ -165,29 +189,36 @@ interactions:
x-stainless-os:
- MacOS
x-stainless-package-version:
- - 1.47.0
+ - 1.59.6
x-stainless-raw-response:
- 'true'
+ x-stainless-retry-count:
+ - '0'
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- - 3.11.7
+ - 3.12.7
method: POST
uri: https://api.openai.com/v1/chat/completions
response:
- content: "{\n \"id\": \"chatcmpl-ABAtPaaeRfdNsZ3k06CfAmrEW8IJu\",\n \"object\":
- \"chat.completion\",\n \"created\": 1727226843,\n \"model\": \"gpt-4o-2024-05-13\",\n
+ content: "{\n \"id\": \"chatcmpl-AsXdg9UrLvAiqWP979E6DszLsQ84k\",\n \"object\":
+ \"chat.completion\",\n \"created\": 1737562384,\n \"model\": \"gpt-4o-2024-08-06\",\n
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
- \"assistant\",\n \"content\": \"Final Answer: The final answer\",\n \"refusal\":
- null\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n
- \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 483,\n \"completion_tokens\":
- 6,\n \"total_tokens\": 489,\n \"completion_tokens_details\": {\n \"reasoning_tokens\":
- 0\n }\n },\n \"system_fingerprint\": \"fp_e375328146\"\n}\n"
+ \"assistant\",\n \"content\": \"```\\nThought: I now know the final answer\\nFinal
+ Answer: The final answer must be the great and the most complete as possible,
+ it must be outcome described.\\n```\",\n \"refusal\": null\n },\n
+ \ \"logprobs\": null,\n \"finish_reason\": \"stop\"\n }\n ],\n
+ \ \"usage\": {\n \"prompt_tokens\": 719,\n \"completion_tokens\": 35,\n
+ \ \"total_tokens\": 754,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
+ 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n
+ \ \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
+ 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
+ \"default\",\n \"system_fingerprint\": \"fp_50cad350e4\"\n}\n"
headers:
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- - 8c8727b9da1f31e6-MIA
+ - 9060d4441edad690-IAD
Connection:
- keep-alive
Content-Encoding:
@@ -195,7 +226,7 @@ interactions:
Content-Type:
- application/json
Date:
- - Wed, 25 Sep 2024 01:14:03 GMT
+ - Wed, 22 Jan 2025 16:13:05 GMT
Server:
- cloudflare
Transfer-Encoding:
@@ -209,7 +240,7 @@ interactions:
openai-organization:
- crewai-iuxna1
openai-processing-ms:
- - '188'
+ - '928'
openai-version:
- '2020-10-01'
strict-transport-security:
@@ -221,13 +252,13 @@ interactions:
x-ratelimit-remaining-requests:
- '9999'
x-ratelimit-remaining-tokens:
- - '29999445'
+ - '29999187'
x-ratelimit-reset-requests:
- 6ms
x-ratelimit-reset-tokens:
- 1ms
x-request-id:
- - req_d8e32538689fe064627468bad802d9a8
+ - req_61fc7506e6db326ec572224aec81ef23
http_version: HTTP/1.1
status_code: 200
version: 1
diff --git a/tests/cassettes/test_agent_execute_task.yaml b/tests/cassettes/test_agent_execute_task.yaml
deleted file mode 100644
index d390b176d..000000000
--- a/tests/cassettes/test_agent_execute_task.yaml
+++ /dev/null
@@ -1,121 +0,0 @@
-interactions:
-- request:
- body: '{"messages": [{"role": "system", "content": "You are Math Tutor. You are
- an experienced math tutor with a knack for explaining complex concepts simply.\nYour
- personal goal is: Solve math problems accurately\nTo give my best complete final
- answer to the task use the exact following format:\n\nThought: I now can give
- a great answer\nFinal Answer: Your final answer must be the great and the most
- complete as possible, it must be outcome described.\n\nI MUST use these formats,
- my job depends on it!"}, {"role": "user", "content": "\nCurrent Task: Calculate
- the area of a circle with radius 5 cm.\n\nThis is the expect criteria for your
- final answer: The calculated area of the circle in square centimeters.\nyou
- MUST return the actual complete content as the final answer, not a summary.\n\nBegin!
- This is VERY important to you, use the tools available and give your best Final
- Answer, your job depends on it!\n\nThought:"}], "model": "gpt-4o-mini", "temperature":
- 0.7}'
- headers:
- accept:
- - application/json
- accept-encoding:
- - gzip, deflate
- connection:
- - keep-alive
- content-length:
- - '969'
- content-type:
- - application/json
- host:
- - api.openai.com
- user-agent:
- - OpenAI/Python 1.47.0
- x-stainless-arch:
- - arm64
- x-stainless-async:
- - 'false'
- x-stainless-lang:
- - python
- x-stainless-os:
- - MacOS
- x-stainless-package-version:
- - 1.47.0
- x-stainless-raw-response:
- - 'true'
- x-stainless-runtime:
- - CPython
- x-stainless-runtime-version:
- - 3.11.7
- method: POST
- uri: https://api.openai.com/v1/chat/completions
- response:
- content: "{\n \"id\": \"chatcmpl-AB7LEfa5gX4cncpI4avsK0CJG8pCb\",\n \"object\":
- \"chat.completion\",\n \"created\": 1727213192,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n
- \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
- \"assistant\",\n \"content\": \"I now can give a great answer\\n\\nTo
- calculate the area of a circle, we use the formula:\\n\\n\\\\[ A = \\\\pi r^2
- \\\\]\\n\\nwhere \\\\( A \\\\) is the area, \\\\( \\\\pi \\\\) (approximately
- 3.14), and \\\\( r \\\\) is the radius of the circle.\\n\\nGiven that the radius
- \\\\( r \\\\) is 5 cm, we can substitute this value into the formula:\\n\\n\\\\[
- A = \\\\pi (5 \\\\, \\\\text{cm})^2 \\\\]\\n\\nCalculating this step-by-step:\\n\\n1.
- First, square the radius:\\n \\\\[ (5 \\\\, \\\\text{cm})^2 = 25 \\\\, \\\\text{cm}^2
- \\\\]\\n\\n2. Then, multiply by \\\\( \\\\pi \\\\):\\n \\\\[ A = \\\\pi \\\\times
- 25 \\\\, \\\\text{cm}^2 \\\\]\\n\\nUsing the approximate value of \\\\( \\\\pi
- \\\\):\\n \\\\[ A \\\\approx 3.14 \\\\times 25 \\\\, \\\\text{cm}^2 \\\\]\\n
- \ \\\\[ A \\\\approx 78.5 \\\\, \\\\text{cm}^2 \\\\]\\n\\nThus, the area of
- the circle is approximately 78.5 square centimeters.\\n\\nFinal Answer: The
- calculated area of the circle is approximately 78.5 square centimeters.\",\n
- \ \"refusal\": null\n },\n \"logprobs\": null,\n \"finish_reason\":
- \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 182,\n \"completion_tokens\":
- 270,\n \"total_tokens\": 452,\n \"completion_tokens_details\": {\n \"reasoning_tokens\":
- 0\n }\n },\n \"system_fingerprint\": \"fp_1bb46167f9\"\n}\n"
- headers:
- CF-Cache-Status:
- - DYNAMIC
- CF-RAY:
- - 8c85da71fcac1cf3-GRU
- Connection:
- - keep-alive
- Content-Encoding:
- - gzip
- Content-Type:
- - application/json
- Date:
- - Tue, 24 Sep 2024 21:26:34 GMT
- Server:
- - cloudflare
- Set-Cookie:
- - __cf_bm=rb61BZH2ejzD5YPmLaEJqI7km71QqyNJGTVdNxBq6qk-1727213194-1.0.1.1-pJ49onmgX9IugEMuYQMralzD7oj_6W.CHbSu4Su1z3NyjTGYg.rhgJZWng8feFYah._oSnoYlkTjpK1Wd2C9FA;
- path=/; expires=Tue, 24-Sep-24 21:56:34 GMT; domain=.api.openai.com; HttpOnly;
- Secure; SameSite=None
- - _cfuvid=lbRdAddVWV6W3f5Dm9SaOPWDUOxqtZBSPr_fTW26nEA-1727213194587-0.0.1.1-604800000;
- path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
- Transfer-Encoding:
- - chunked
- X-Content-Type-Options:
- - nosniff
- access-control-expose-headers:
- - X-Request-ID
- openai-organization:
- - crewai-iuxna1
- openai-processing-ms:
- - '2244'
- openai-version:
- - '2020-10-01'
- strict-transport-security:
- - max-age=31536000; includeSubDomains; preload
- x-ratelimit-limit-requests:
- - '30000'
- x-ratelimit-limit-tokens:
- - '150000000'
- x-ratelimit-remaining-requests:
- - '29999'
- x-ratelimit-remaining-tokens:
- - '149999774'
- x-ratelimit-reset-requests:
- - 2ms
- x-ratelimit-reset-tokens:
- - 0s
- x-request-id:
- - req_2e565b5f24c38968e4e923a47ecc6233
- http_version: HTTP/1.1
- status_code: 200
-version: 1
diff --git "a/tests/cassettes/test_agent_tool_role_matching[ \"Futel Official Infopoint\" -True].yaml" "b/tests/cassettes/test_agent_tool_role_matching[ \"Futel Official Infopoint\" -True].yaml"
deleted file mode 100644
index 99c9d5ea2..000000000
--- "a/tests/cassettes/test_agent_tool_role_matching[ \"Futel Official Infopoint\" -True].yaml"
+++ /dev/null
@@ -1,117 +0,0 @@
-interactions:
-- request:
- body: '{"messages": [{"role": "system", "content": "You are Futel Official Infopoint.
- Futel Football Club info\nYour personal goal is: Answer questions about Futel\nTo
- give my best complete final answer to the task respond using the exact following
- format:\n\nThought: I now can give a great answer\nFinal Answer: Your final
- answer must be the great and the most complete as possible, it must be outcome
- described.\n\nI MUST use these formats, my job depends on it!"}, {"role": "user",
- "content": "\nCurrent Task: Test task\n\nThis is the expect criteria for your
- final answer: Your best answer to your coworker asking you this, accounting
- for the context shared.\nyou MUST return the actual complete content as the
- final answer, not a summary.\n\nBegin! This is VERY important to you, use the
- tools available and give your best Final Answer, your job depends on it!\n\nThought:"}],
- "model": "gpt-4o", "stop": ["\nObservation:"], "stream": false}'
- headers:
- accept:
- - application/json
- accept-encoding:
- - gzip, deflate
- connection:
- - keep-alive
- content-length:
- - '939'
- content-type:
- - application/json
- cookie:
- - __cf_bm=cwWdOaPJjFMNJaLtJfa8Kjqavswg5bzVRFzBX4gneGw-1736458417-1.0.1.1-bvf2HshgcMtgn7GdxqwySFDAIacGccDFfEXniBFTTDmbGMCiIIwf6t2DiwWnBldmUHixwc5kDO9gYs08g.feBA;
- _cfuvid=WMw7PSqkYqQOieguBRs0uNkwNU92A.ZKbgDbCAcV3EQ-1736458417825-0.0.1.1-604800000
- host:
- - api.openai.com
- user-agent:
- - OpenAI/Python 1.52.1
- x-stainless-arch:
- - arm64
- x-stainless-async:
- - 'false'
- x-stainless-lang:
- - python
- x-stainless-os:
- - MacOS
- x-stainless-package-version:
- - 1.52.1
- x-stainless-raw-response:
- - 'true'
- x-stainless-retry-count:
- - '0'
- x-stainless-runtime:
- - CPython
- x-stainless-runtime-version:
- - 3.12.7
- method: POST
- uri: https://api.openai.com/v1/chat/completions
- response:
- content: "{\n \"id\": \"chatcmpl-AnuRlxiTxduAVoXHHY58Fvfbll5IS\",\n \"object\":
- \"chat.completion\",\n \"created\": 1736458417,\n \"model\": \"gpt-4o-2024-08-06\",\n
- \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
- \"assistant\",\n \"content\": \"I now can give a great answer \\nFinal
- Answer: This is a test task, and the context or question from the coworker is
- not specified. Therefore, my best effort would be to affirm my readiness to
- answer accurately and in detail any question about Futel Football Club based
- on the context described. If provided with specific information or questions,
- I will ensure to respond comprehensively as required by my job directives.\",\n
- \ \"refusal\": null\n },\n \"logprobs\": null,\n \"finish_reason\":
- \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 177,\n \"completion_tokens\":
- 82,\n \"total_tokens\": 259,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
- 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n
- \ \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
- 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"system_fingerprint\":
- \"fp_703d4ff298\"\n}\n"
- headers:
- CF-Cache-Status:
- - DYNAMIC
- CF-RAY:
- - 8ff78bf7bd6cc002-ATL
- Connection:
- - keep-alive
- Content-Encoding:
- - gzip
- Content-Type:
- - application/json
- Date:
- - Thu, 09 Jan 2025 21:33:40 GMT
- Server:
- - cloudflare
- Transfer-Encoding:
- - chunked
- X-Content-Type-Options:
- - nosniff
- access-control-expose-headers:
- - X-Request-ID
- alt-svc:
- - h3=":443"; ma=86400
- openai-organization:
- - crewai-iuxna1
- openai-processing-ms:
- - '2263'
- openai-version:
- - '2020-10-01'
- strict-transport-security:
- - max-age=31536000; includeSubDomains; preload
- x-ratelimit-limit-requests:
- - '10000'
- x-ratelimit-limit-tokens:
- - '30000000'
- x-ratelimit-remaining-requests:
- - '9999'
- x-ratelimit-remaining-tokens:
- - '29999786'
- x-ratelimit-reset-requests:
- - 6ms
- x-ratelimit-reset-tokens:
- - 0s
- x-request-id:
- - req_7c1a31da73cd103e9f410f908e59187f
- http_version: HTTP/1.1
- status_code: 200
-version: 1
diff --git "a/tests/cassettes/test_agent_tool_role_matching[ FUTEL\\nOFFICIAL INFOPOINT -True].yaml" "b/tests/cassettes/test_agent_tool_role_matching[ FUTEL\\nOFFICIAL INFOPOINT -True].yaml"
deleted file mode 100644
index 25129c6c8..000000000
--- "a/tests/cassettes/test_agent_tool_role_matching[ FUTEL\\nOFFICIAL INFOPOINT -True].yaml"
+++ /dev/null
@@ -1,119 +0,0 @@
-interactions:
-- request:
- body: '{"messages": [{"role": "system", "content": "You are Futel Official Infopoint.
- Futel Football Club info\nYour personal goal is: Answer questions about Futel\nTo
- give my best complete final answer to the task respond using the exact following
- format:\n\nThought: I now can give a great answer\nFinal Answer: Your final
- answer must be the great and the most complete as possible, it must be outcome
- described.\n\nI MUST use these formats, my job depends on it!"}, {"role": "user",
- "content": "\nCurrent Task: Test task\n\nThis is the expect criteria for your
- final answer: Your best answer to your coworker asking you this, accounting
- for the context shared.\nyou MUST return the actual complete content as the
- final answer, not a summary.\n\nBegin! This is VERY important to you, use the
- tools available and give your best Final Answer, your job depends on it!\n\nThought:"}],
- "model": "gpt-4o", "stop": ["\nObservation:"], "stream": false}'
- headers:
- accept:
- - application/json
- accept-encoding:
- - gzip, deflate
- connection:
- - keep-alive
- content-length:
- - '939'
- content-type:
- - application/json
- cookie:
- - __cf_bm=cwWdOaPJjFMNJaLtJfa8Kjqavswg5bzVRFzBX4gneGw-1736458417-1.0.1.1-bvf2HshgcMtgn7GdxqwySFDAIacGccDFfEXniBFTTDmbGMCiIIwf6t2DiwWnBldmUHixwc5kDO9gYs08g.feBA;
- _cfuvid=WMw7PSqkYqQOieguBRs0uNkwNU92A.ZKbgDbCAcV3EQ-1736458417825-0.0.1.1-604800000
- host:
- - api.openai.com
- user-agent:
- - OpenAI/Python 1.52.1
- x-stainless-arch:
- - arm64
- x-stainless-async:
- - 'false'
- x-stainless-lang:
- - python
- x-stainless-os:
- - MacOS
- x-stainless-package-version:
- - 1.52.1
- x-stainless-raw-response:
- - 'true'
- x-stainless-retry-count:
- - '0'
- x-stainless-runtime:
- - CPython
- x-stainless-runtime-version:
- - 3.12.7
- method: POST
- uri: https://api.openai.com/v1/chat/completions
- response:
- content: "{\n \"id\": \"chatcmpl-AnuRrFJZGKw8cIEshvuW1PKwFZFKs\",\n \"object\":
- \"chat.completion\",\n \"created\": 1736458423,\n \"model\": \"gpt-4o-2024-08-06\",\n
- \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
- \"assistant\",\n \"content\": \"I now can give a great answer \\nFinal
- Answer: Although you mentioned this being a \\\"Test task\\\" and haven't provided
- a specific question regarding Futel Football Club, your request appears to involve
- ensuring accuracy and detail in responses. For a proper answer about Futel,
- I'd be ready to provide details about the club's history, management, players,
- match schedules, and recent performance statistics. Remember to ask specific
- questions to receive a targeted response. If this were a real context where
- information was shared, I would respond precisely to what's been asked regarding
- Futel Football Club.\",\n \"refusal\": null\n },\n \"logprobs\":
- null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
- 177,\n \"completion_tokens\": 113,\n \"total_tokens\": 290,\n \"prompt_tokens_details\":
- {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
- {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
- 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"system_fingerprint\":
- \"fp_703d4ff298\"\n}\n"
- headers:
- CF-Cache-Status:
- - DYNAMIC
- CF-RAY:
- - 8ff78c1d0ecdc002-ATL
- Connection:
- - keep-alive
- Content-Encoding:
- - gzip
- Content-Type:
- - application/json
- Date:
- - Thu, 09 Jan 2025 21:33:47 GMT
- Server:
- - cloudflare
- Transfer-Encoding:
- - chunked
- X-Content-Type-Options:
- - nosniff
- access-control-expose-headers:
- - X-Request-ID
- alt-svc:
- - h3=":443"; ma=86400
- openai-organization:
- - crewai-iuxna1
- openai-processing-ms:
- - '3097'
- openai-version:
- - '2020-10-01'
- strict-transport-security:
- - max-age=31536000; includeSubDomains; preload
- x-ratelimit-limit-requests:
- - '10000'
- x-ratelimit-limit-tokens:
- - '30000000'
- x-ratelimit-remaining-requests:
- - '9999'
- x-ratelimit-remaining-tokens:
- - '29999786'
- x-ratelimit-reset-requests:
- - 6ms
- x-ratelimit-reset-tokens:
- - 0s
- x-request-id:
- - req_179e1d56e2b17303e40480baffbc7b08
- http_version: HTTP/1.1
- status_code: 200
-version: 1
diff --git "a/tests/cassettes/test_agent_tool_role_matching[\"Futel Official Infopoint\"-True].yaml" "b/tests/cassettes/test_agent_tool_role_matching[\"Futel Official Infopoint\"-True].yaml"
deleted file mode 100644
index 57705d771..000000000
--- "a/tests/cassettes/test_agent_tool_role_matching[\"Futel Official Infopoint\"-True].yaml"
+++ /dev/null
@@ -1,114 +0,0 @@
-interactions:
-- request:
- body: '{"messages": [{"role": "system", "content": "You are Futel Official Infopoint.
- Futel Football Club info\nYour personal goal is: Answer questions about Futel\nTo
- give my best complete final answer to the task respond using the exact following
- format:\n\nThought: I now can give a great answer\nFinal Answer: Your final
- answer must be the great and the most complete as possible, it must be outcome
- described.\n\nI MUST use these formats, my job depends on it!"}, {"role": "user",
- "content": "\nCurrent Task: Test task\n\nThis is the expect criteria for your
- final answer: Your best answer to your coworker asking you this, accounting
- for the context shared.\nyou MUST return the actual complete content as the
- final answer, not a summary.\n\nBegin! This is VERY important to you, use the
- tools available and give your best Final Answer, your job depends on it!\n\nThought:"}],
- "model": "gpt-4o", "stop": ["\nObservation:"], "stream": false}'
- headers:
- accept:
- - application/json
- accept-encoding:
- - gzip, deflate
- connection:
- - keep-alive
- content-length:
- - '939'
- content-type:
- - application/json
- cookie:
- - __cf_bm=cwWdOaPJjFMNJaLtJfa8Kjqavswg5bzVRFzBX4gneGw-1736458417-1.0.1.1-bvf2HshgcMtgn7GdxqwySFDAIacGccDFfEXniBFTTDmbGMCiIIwf6t2DiwWnBldmUHixwc5kDO9gYs08g.feBA;
- _cfuvid=WMw7PSqkYqQOieguBRs0uNkwNU92A.ZKbgDbCAcV3EQ-1736458417825-0.0.1.1-604800000
- host:
- - api.openai.com
- user-agent:
- - OpenAI/Python 1.52.1
- x-stainless-arch:
- - arm64
- x-stainless-async:
- - 'false'
- x-stainless-lang:
- - python
- x-stainless-os:
- - MacOS
- x-stainless-package-version:
- - 1.52.1
- x-stainless-raw-response:
- - 'true'
- x-stainless-retry-count:
- - '0'
- x-stainless-runtime:
- - CPython
- x-stainless-runtime-version:
- - 3.12.7
- method: POST
- uri: https://api.openai.com/v1/chat/completions
- response:
- content: "{\n \"id\": \"chatcmpl-AnuRqgg7eiHnDi2DOqdk99fiqOboz\",\n \"object\":
- \"chat.completion\",\n \"created\": 1736458422,\n \"model\": \"gpt-4o-2024-08-06\",\n
- \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
- \"assistant\",\n \"content\": \"I now can give a great answer \\nFinal
- Answer: Your best answer to your coworker asking you this, accounting for the
- context shared. You MUST return the actual complete content as the final answer,
- not a summary.\",\n \"refusal\": null\n },\n \"logprobs\":
- null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
- 177,\n \"completion_tokens\": 44,\n \"total_tokens\": 221,\n \"prompt_tokens_details\":
- {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
- {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
- 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"system_fingerprint\":
- \"fp_703d4ff298\"\n}\n"
- headers:
- CF-Cache-Status:
- - DYNAMIC
- CF-RAY:
- - 8ff78c164ad2c002-ATL
- Connection:
- - keep-alive
- Content-Encoding:
- - gzip
- Content-Type:
- - application/json
- Date:
- - Thu, 09 Jan 2025 21:33:43 GMT
- Server:
- - cloudflare
- Transfer-Encoding:
- - chunked
- X-Content-Type-Options:
- - nosniff
- access-control-expose-headers:
- - X-Request-ID
- alt-svc:
- - h3=":443"; ma=86400
- openai-organization:
- - crewai-iuxna1
- openai-processing-ms:
- - '899'
- openai-version:
- - '2020-10-01'
- strict-transport-security:
- - max-age=31536000; includeSubDomains; preload
- x-ratelimit-limit-requests:
- - '10000'
- x-ratelimit-limit-tokens:
- - '30000000'
- x-ratelimit-remaining-requests:
- - '9999'
- x-ratelimit-remaining-tokens:
- - '29999786'
- x-ratelimit-reset-requests:
- - 6ms
- x-ratelimit-reset-tokens:
- - 0s
- x-request-id:
- - req_9f5226208edb90a27987aaf7e0ca03d3
- http_version: HTTP/1.1
- status_code: 200
-version: 1
diff --git a/tests/cassettes/test_agent_tool_role_matching[Futel Official Infopoint-True].yaml b/tests/cassettes/test_agent_tool_role_matching[Futel Official Infopoint-True].yaml
deleted file mode 100644
index f9163dd91..000000000
--- a/tests/cassettes/test_agent_tool_role_matching[Futel Official Infopoint-True].yaml
+++ /dev/null
@@ -1,119 +0,0 @@
-interactions:
-- request:
- body: '{"messages": [{"role": "system", "content": "You are Futel Official Infopoint.
- Futel Football Club info\nYour personal goal is: Answer questions about Futel\nTo
- give my best complete final answer to the task respond using the exact following
- format:\n\nThought: I now can give a great answer\nFinal Answer: Your final
- answer must be the great and the most complete as possible, it must be outcome
- described.\n\nI MUST use these formats, my job depends on it!"}, {"role": "user",
- "content": "\nCurrent Task: Test task\n\nThis is the expect criteria for your
- final answer: Your best answer to your coworker asking you this, accounting
- for the context shared.\nyou MUST return the actual complete content as the
- final answer, not a summary.\n\nBegin! This is VERY important to you, use the
- tools available and give your best Final Answer, your job depends on it!\n\nThought:"}],
- "model": "gpt-4o", "stop": ["\nObservation:"], "stream": false}'
- headers:
- accept:
- - application/json
- accept-encoding:
- - gzip, deflate
- connection:
- - keep-alive
- content-length:
- - '939'
- content-type:
- - application/json
- host:
- - api.openai.com
- user-agent:
- - OpenAI/Python 1.52.1
- x-stainless-arch:
- - arm64
- x-stainless-async:
- - 'false'
- x-stainless-lang:
- - python
- x-stainless-os:
- - MacOS
- x-stainless-package-version:
- - 1.52.1
- x-stainless-raw-response:
- - 'true'
- x-stainless-retry-count:
- - '0'
- x-stainless-runtime:
- - CPython
- x-stainless-runtime-version:
- - 3.12.7
- method: POST
- uri: https://api.openai.com/v1/chat/completions
- response:
- content: "{\n \"id\": \"chatcmpl-AnuRjmwH5mrykLxQhFwTqqTiDtuTf\",\n \"object\":
- \"chat.completion\",\n \"created\": 1736458415,\n \"model\": \"gpt-4o-2024-08-06\",\n
- \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
- \"assistant\",\n \"content\": \"I now can give a great answer \\nFinal
- Answer: As this is a test task, please note that Futel Football Club is fictional
- and any specific details about it would not be available. However, if you have
- specific questions or need information about a particular aspect of Futel or
- any general football club inquiry, feel free to ask, and I'll do my best to
- assist you with your query!\",\n \"refusal\": null\n },\n \"logprobs\":
- null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
- 177,\n \"completion_tokens\": 79,\n \"total_tokens\": 256,\n \"prompt_tokens_details\":
- {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
- {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
- 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"system_fingerprint\":
- \"fp_703d4ff298\"\n}\n"
- headers:
- CF-Cache-Status:
- - DYNAMIC
- CF-RAY:
- - 8ff78be5eebfc002-ATL
- Connection:
- - keep-alive
- Content-Encoding:
- - gzip
- Content-Type:
- - application/json
- Date:
- - Thu, 09 Jan 2025 21:33:37 GMT
- Server:
- - cloudflare
- Set-Cookie:
- - __cf_bm=cwWdOaPJjFMNJaLtJfa8Kjqavswg5bzVRFzBX4gneGw-1736458417-1.0.1.1-bvf2HshgcMtgn7GdxqwySFDAIacGccDFfEXniBFTTDmbGMCiIIwf6t2DiwWnBldmUHixwc5kDO9gYs08g.feBA;
- path=/; expires=Thu, 09-Jan-25 22:03:37 GMT; domain=.api.openai.com; HttpOnly;
- Secure; SameSite=None
- - _cfuvid=WMw7PSqkYqQOieguBRs0uNkwNU92A.ZKbgDbCAcV3EQ-1736458417825-0.0.1.1-604800000;
- path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
- Transfer-Encoding:
- - chunked
- X-Content-Type-Options:
- - nosniff
- access-control-expose-headers:
- - X-Request-ID
- alt-svc:
- - h3=":443"; ma=86400
- openai-organization:
- - crewai-iuxna1
- openai-processing-ms:
- - '2730'
- openai-version:
- - '2020-10-01'
- strict-transport-security:
- - max-age=31536000; includeSubDomains; preload
- x-ratelimit-limit-requests:
- - '10000'
- x-ratelimit-limit-tokens:
- - '30000000'
- x-ratelimit-remaining-requests:
- - '9999'
- x-ratelimit-remaining-tokens:
- - '29999786'
- x-ratelimit-reset-requests:
- - 6ms
- x-ratelimit-reset-tokens:
- - 0s
- x-request-id:
- - req_014478ba748f860d10ac250ca0ba824a
- http_version: HTTP/1.1
- status_code: 200
-version: 1
diff --git "a/tests/cassettes/test_agent_tool_role_matching[Futel Official Infopoint\\n-True].yaml" "b/tests/cassettes/test_agent_tool_role_matching[Futel Official Infopoint\\n-True].yaml"
deleted file mode 100644
index e093f57f8..000000000
--- "a/tests/cassettes/test_agent_tool_role_matching[Futel Official Infopoint\\n-True].yaml"
+++ /dev/null
@@ -1,119 +0,0 @@
-interactions:
-- request:
- body: '{"messages": [{"role": "system", "content": "You are Futel Official Infopoint.
- Futel Football Club info\nYour personal goal is: Answer questions about Futel\nTo
- give my best complete final answer to the task respond using the exact following
- format:\n\nThought: I now can give a great answer\nFinal Answer: Your final
- answer must be the great and the most complete as possible, it must be outcome
- described.\n\nI MUST use these formats, my job depends on it!"}, {"role": "user",
- "content": "\nCurrent Task: Test task\n\nThis is the expect criteria for your
- final answer: Your best answer to your coworker asking you this, accounting
- for the context shared.\nyou MUST return the actual complete content as the
- final answer, not a summary.\n\nBegin! This is VERY important to you, use the
- tools available and give your best Final Answer, your job depends on it!\n\nThought:"}],
- "model": "gpt-4o", "stop": ["\nObservation:"], "stream": false}'
- headers:
- accept:
- - application/json
- accept-encoding:
- - gzip, deflate
- connection:
- - keep-alive
- content-length:
- - '939'
- content-type:
- - application/json
- cookie:
- - __cf_bm=cwWdOaPJjFMNJaLtJfa8Kjqavswg5bzVRFzBX4gneGw-1736458417-1.0.1.1-bvf2HshgcMtgn7GdxqwySFDAIacGccDFfEXniBFTTDmbGMCiIIwf6t2DiwWnBldmUHixwc5kDO9gYs08g.feBA;
- _cfuvid=WMw7PSqkYqQOieguBRs0uNkwNU92A.ZKbgDbCAcV3EQ-1736458417825-0.0.1.1-604800000
- host:
- - api.openai.com
- user-agent:
- - OpenAI/Python 1.52.1
- x-stainless-arch:
- - arm64
- x-stainless-async:
- - 'false'
- x-stainless-lang:
- - python
- x-stainless-os:
- - MacOS
- x-stainless-package-version:
- - 1.52.1
- x-stainless-raw-response:
- - 'true'
- x-stainless-retry-count:
- - '0'
- x-stainless-runtime:
- - CPython
- x-stainless-runtime-version:
- - 3.12.7
- method: POST
- uri: https://api.openai.com/v1/chat/completions
- response:
- content: "{\n \"id\": \"chatcmpl-AnuRofLgmzWcDya5LILqYwIJYgFoq\",\n \"object\":
- \"chat.completion\",\n \"created\": 1736458420,\n \"model\": \"gpt-4o-2024-08-06\",\n
- \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
- \"assistant\",\n \"content\": \"I now can give a great answer \\nFinal
- Answer: As an official Futel Football Club infopoint, my responsibility is to
- provide detailed and accurate information about the club. This includes answering
- questions regarding team statistics, player performances, upcoming fixtures,
- ticketing and fan zone details, club history, and community initiatives. Our
- focus is to ensure that fans and stakeholders have access to the latest and
- most precise information about the club's on and off-pitch activities. If there's
- anything specific you need to know, just let me know, and I'll be more than
- happy to assist!\",\n \"refusal\": null\n },\n \"logprobs\":
- null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
- 177,\n \"completion_tokens\": 115,\n \"total_tokens\": 292,\n \"prompt_tokens_details\":
- {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
- {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
- 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"system_fingerprint\":
- \"fp_703d4ff298\"\n}\n"
- headers:
- CF-Cache-Status:
- - DYNAMIC
- CF-RAY:
- - 8ff78c066f37c002-ATL
- Connection:
- - keep-alive
- Content-Encoding:
- - gzip
- Content-Type:
- - application/json
- Date:
- - Thu, 09 Jan 2025 21:33:42 GMT
- Server:
- - cloudflare
- Transfer-Encoding:
- - chunked
- X-Content-Type-Options:
- - nosniff
- access-control-expose-headers:
- - X-Request-ID
- alt-svc:
- - h3=":443"; ma=86400
- openai-organization:
- - crewai-iuxna1
- openai-processing-ms:
- - '2459'
- openai-version:
- - '2020-10-01'
- strict-transport-security:
- - max-age=31536000; includeSubDomains; preload
- x-ratelimit-limit-requests:
- - '10000'
- x-ratelimit-limit-tokens:
- - '30000000'
- x-ratelimit-remaining-requests:
- - '9999'
- x-ratelimit-remaining-tokens:
- - '29999786'
- x-ratelimit-reset-requests:
- - 6ms
- x-ratelimit-reset-tokens:
- - 0s
- x-request-id:
- - req_a146dd27f040f39a576750970cca0f52
- http_version: HTTP/1.1
- status_code: 200
-version: 1
diff --git a/tests/cassettes/test_agent_with_knowledge_sources_works_with_copy.yaml b/tests/cassettes/test_agent_with_knowledge_sources_works_with_copy.yaml
new file mode 100644
index 000000000..176be39c8
--- /dev/null
+++ b/tests/cassettes/test_agent_with_knowledge_sources_works_with_copy.yaml
@@ -0,0 +1,206 @@
+interactions:
+- request:
+ body: '{"input": ["Brandon''s favorite color is red and he likes Mexican food."],
+ "model": "text-embedding-3-small", "encoding_format": "base64"}'
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '137'
+ content-type:
+ - application/json
+ host:
+ - api.openai.com
+ user-agent:
+ - OpenAI/Python 1.59.6
+ x-stainless-arch:
+ - arm64
+ x-stainless-async:
+ - 'false'
+ x-stainless-lang:
+ - python
+ x-stainless-os:
+ - MacOS
+ x-stainless-package-version:
+ - 1.59.6
+ x-stainless-retry-count:
+ - '0'
+ x-stainless-runtime:
+ - CPython
+ x-stainless-runtime-version:
+ - 3.12.8
+ method: POST
+ uri: https://api.openai.com/v1/embeddings
+ response:
+ content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\":
+ \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n
+ \ }\n ],\n \"model\": \"text-embedding-3-small\",\n \"usage\": {\n \"prompt_tokens\":
+ 12,\n \"total_tokens\": 12\n }\n}\n"
+ headers:
+ CF-RAY:
+ - 908b749c8cb41576-SJC
+ Connection:
+ - keep-alive
+ Content-Encoding:
+ - gzip
+ Content-Type:
+ - application/json
+ Date:
+ - Mon, 27 Jan 2025 20:22:34 GMT
+ Server:
+ - cloudflare
+ Set-Cookie:
+ - __cf_bm=NhRx2kcSiBEOhkZbWaKlY_pw46LGzb7BpUNF.ozrJrY-1738009354-1.0.1.1-naI_MYI5l4_BbeD3mwpu.Pi55FVDn3ImnfFjreNp0bbAvTuf8xOJY8HgxhE.W4XWbq247SbevyoE9aStMYq0ow;
+ path=/; expires=Mon, 27-Jan-25 20:52:34 GMT; domain=.api.openai.com; HttpOnly;
+ Secure; SameSite=None
+ - _cfuvid=xnfGIFZVE6LqgVkRMk6ORXsMurOmTu.z7TTz7afn810-1738009354083-0.0.1.1-604800000;
+ path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
+ Transfer-Encoding:
+ - chunked
+ X-Content-Type-Options:
+ - nosniff
+ access-control-allow-origin:
+ - '*'
+ access-control-expose-headers:
+ - X-Request-ID
+ alt-svc:
+ - h3=":443"; ma=86400
+ cf-cache-status:
+ - DYNAMIC
+ openai-model:
+ - text-embedding-3-small
+ openai-organization:
+ - crewai-iuxna1
+ openai-processing-ms:
+ - '75'
+ openai-version:
+ - '2020-10-01'
+ strict-transport-security:
+ - max-age=31536000; includeSubDomains; preload
+ via:
+ - envoy-router-75f99bb574-mb9tb
+ x-envoy-upstream-service-time:
+ - '29'
+ x-ratelimit-limit-requests:
+ - '10000'
+ x-ratelimit-limit-tokens:
+ - '10000000'
+ x-ratelimit-remaining-requests:
+ - '9999'
+ x-ratelimit-remaining-tokens:
+ - '9999986'
+ x-ratelimit-reset-requests:
+ - 6ms
+ x-ratelimit-reset-tokens:
+ - 0s
+ x-request-id:
+ - req_4e3d0c147826a183e2848ca1df2c9da9
+ http_version: HTTP/1.1
+ status_code: 200
+- request:
+ body: '{"input": ["Brandon''s favorite color is red and he likes Mexican food."],
+ "model": "text-embedding-3-small", "encoding_format": "base64"}'
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '137'
+ content-type:
+ - application/json
+ host:
+ - api.openai.com
+ user-agent:
+ - OpenAI/Python 1.59.6
+ x-stainless-arch:
+ - arm64
+ x-stainless-async:
+ - 'false'
+ x-stainless-lang:
+ - python
+ x-stainless-os:
+ - MacOS
+ x-stainless-package-version:
+ - 1.59.6
+ x-stainless-retry-count:
+ - '0'
+ x-stainless-runtime:
+ - CPython
+ x-stainless-runtime-version:
+ - 3.12.8
+ method: POST
+ uri: https://api.openai.com/v1/embeddings
+ response:
+ content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\":
+ \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n
+ \ }\n ],\n \"model\": \"text-embedding-3-small\",\n \"usage\": {\n \"prompt_tokens\":
+ 12,\n \"total_tokens\": 12\n }\n}\n"
+ headers:
+ CF-Cache-Status:
+ - DYNAMIC
+ CF-RAY:
+ - 908b749fcdbaed36-SJC
+ Connection:
+ - keep-alive
+ Content-Encoding:
+ - gzip
+ Content-Type:
+ - application/json
+ Date:
+ - Mon, 27 Jan 2025 20:22:34 GMT
+ Server:
+ - cloudflare
+ Set-Cookie:
+ - __cf_bm=hTW9TNu3pB35yAIOgg3sdy1hLtP_un1Js4.ZfsmNEXY-1738009354-1.0.1.1-pmAOhPxdO75O.Xt22Tnz_8pitmTMJY.vDeWPxXlJq3TTay0D.285FuCezcz8iy6gLi0hF9SRUc5f5xovdsaQOA;
+ path=/; expires=Mon, 27-Jan-25 20:52:34 GMT; domain=.api.openai.com; HttpOnly;
+ Secure; SameSite=None
+ - _cfuvid=KXf4AO65W0FpWKL_jL5Tw4Xdts32F1mkwYcniiqUZtU-1738009354603-0.0.1.1-604800000;
+ path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
+ Transfer-Encoding:
+ - chunked
+ X-Content-Type-Options:
+ - nosniff
+ access-control-allow-origin:
+ - '*'
+ access-control-expose-headers:
+ - X-Request-ID
+ alt-svc:
+ - h3=":443"; ma=86400
+ openai-model:
+ - text-embedding-3-small
+ openai-organization:
+ - crewai-iuxna1
+ openai-processing-ms:
+ - '113'
+ openai-version:
+ - '2020-10-01'
+ strict-transport-security:
+ - max-age=31536000; includeSubDomains; preload
+ via:
+ - envoy-router-5cc9fb545f-x4k6f
+ x-envoy-upstream-service-time:
+ - '74'
+ x-ratelimit-limit-requests:
+ - '10000'
+ x-ratelimit-limit-tokens:
+ - '10000000'
+ x-ratelimit-remaining-requests:
+ - '9999'
+ x-ratelimit-remaining-tokens:
+ - '9999986'
+ x-ratelimit-reset-requests:
+ - 6ms
+ x-ratelimit-reset-tokens:
+ - 0s
+ x-request-id:
+ - req_7b9c56b5c3be975b8ce088f3457a52f9
+ http_version: HTTP/1.1
+ status_code: 200
+version: 1
diff --git a/tests/cassettes/test_crew_with_knowledge_sources_works_with_copy.yaml b/tests/cassettes/test_crew_with_knowledge_sources_works_with_copy.yaml
new file mode 100644
index 000000000..c344ed7bd
--- /dev/null
+++ b/tests/cassettes/test_crew_with_knowledge_sources_works_with_copy.yaml
@@ -0,0 +1,206 @@
+interactions:
+- request:
+ body: '{"input": ["Brandon''s favorite color is red and he likes Mexican food."],
+ "model": "text-embedding-3-small", "encoding_format": "base64"}'
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '137'
+ content-type:
+ - application/json
+ host:
+ - api.openai.com
+ user-agent:
+ - OpenAI/Python 1.59.6
+ x-stainless-arch:
+ - arm64
+ x-stainless-async:
+ - 'false'
+ x-stainless-lang:
+ - python
+ x-stainless-os:
+ - MacOS
+ x-stainless-package-version:
+ - 1.59.6
+ x-stainless-retry-count:
+ - '0'
+ x-stainless-runtime:
+ - CPython
+ x-stainless-runtime-version:
+ - 3.12.8
+ method: POST
+ uri: https://api.openai.com/v1/embeddings
+ response:
+ content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\":
+ \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n
+ \ }\n ],\n \"model\": \"text-embedding-3-small\",\n \"usage\": {\n \"prompt_tokens\":
+ 12,\n \"total_tokens\": 12\n }\n}\n"
+ headers:
+ CF-Cache-Status:
+ - DYNAMIC
+ CF-RAY:
+ - 9072bf7c2a5a2368-SJC
+ Connection:
+ - keep-alive
+ Content-Encoding:
+ - gzip
+ Content-Type:
+ - application/json
+ Date:
+ - Fri, 24 Jan 2025 20:24:36 GMT
+ Server:
+ - cloudflare
+ Set-Cookie:
+ - __cf_bm=QK.uKShFHIukTlLKV8KaH39RKmf.DA9fbdIp_5JPWp0-1737750276-1.0.1.1-U7TK7a58ic2LWeNf6OwFzCGWgz2X06RW7R0O0mr8QRYXDoZzLKeG_c1nzqrtBldVwPNYiThnXzesVGG6xXiXSA;
+ path=/; expires=Fri, 24-Jan-25 20:54:36 GMT; domain=.api.openai.com; HttpOnly;
+ Secure; SameSite=None
+ - _cfuvid=uqbfBV84.dehSGRMLX4tXE1mi3miPlWZSPvMxI8q.9g-1737750276975-0.0.1.1-604800000;
+ path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
+ Transfer-Encoding:
+ - chunked
+ X-Content-Type-Options:
+ - nosniff
+ access-control-allow-origin:
+ - '*'
+ access-control-expose-headers:
+ - X-Request-ID
+ alt-svc:
+ - h3=":443"; ma=86400
+ openai-model:
+ - text-embedding-3-small
+ openai-organization:
+ - crewai-iuxna1
+ openai-processing-ms:
+ - '136'
+ openai-version:
+ - '2020-10-01'
+ strict-transport-security:
+ - max-age=31536000; includeSubDomains; preload
+ via:
+ - envoy-router-5985cc59bb-fvqpf
+ x-envoy-upstream-service-time:
+ - '46'
+ x-ratelimit-limit-requests:
+ - '10000'
+ x-ratelimit-limit-tokens:
+ - '10000000'
+ x-ratelimit-remaining-requests:
+ - '9999'
+ x-ratelimit-remaining-tokens:
+ - '9999986'
+ x-ratelimit-reset-requests:
+ - 6ms
+ x-ratelimit-reset-tokens:
+ - 0s
+ x-request-id:
+ - req_3983df28a40cce518f5a800922e01028
+ http_version: HTTP/1.1
+ status_code: 200
+- request:
+ body: '{"input": ["Brandon''s favorite color is red and he likes Mexican food."],
+ "model": "text-embedding-3-small", "encoding_format": "base64"}'
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '137'
+ content-type:
+ - application/json
+ host:
+ - api.openai.com
+ user-agent:
+ - OpenAI/Python 1.59.6
+ x-stainless-arch:
+ - arm64
+ x-stainless-async:
+ - 'false'
+ x-stainless-lang:
+ - python
+ x-stainless-os:
+ - MacOS
+ x-stainless-package-version:
+ - 1.59.6
+ x-stainless-retry-count:
+ - '0'
+ x-stainless-runtime:
+ - CPython
+ x-stainless-runtime-version:
+ - 3.12.8
+ method: POST
+ uri: https://api.openai.com/v1/embeddings
+ response:
+ content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\":
+ \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n
+ \ }\n ],\n \"model\": \"text-embedding-3-small\",\n \"usage\": {\n \"prompt_tokens\":
+ 12,\n \"total_tokens\": 12\n }\n}\n"
+ headers:
+ CF-Cache-Status:
+ - DYNAMIC
+ CF-RAY:
+ - 9072bf803bed7ae0-SJC
+ Connection:
+ - keep-alive
+ Content-Encoding:
+ - gzip
+ Content-Type:
+ - application/json
+ Date:
+ - Fri, 24 Jan 2025 20:24:38 GMT
+ Server:
+ - cloudflare
+ Set-Cookie:
+ - __cf_bm=jtzsU7lWc3d6B4KQxyjgxkttPzNPU8tMxA_s1vkjLI4-1737750278-1.0.1.1-GEGJzRKGIhPNMpEUz_Rh1dVq5Pl4.NRVTCurfAC_LMKDRZrKec4U8BF3B7egdjrrjsKssZ8eeHXAr1U7v6O9qQ;
+ path=/; expires=Fri, 24-Jan-25 20:54:38 GMT; domain=.api.openai.com; HttpOnly;
+ Secure; SameSite=None
+ - _cfuvid=iA.YuWEfBSZCELL7i1Nqta1cyNeMLTrl8AqxK0PB6XA-1737750278342-0.0.1.1-604800000;
+ path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
+ Transfer-Encoding:
+ - chunked
+ X-Content-Type-Options:
+ - nosniff
+ access-control-allow-origin:
+ - '*'
+ access-control-expose-headers:
+ - X-Request-ID
+ alt-svc:
+ - h3=":443"; ma=86400
+ openai-model:
+ - text-embedding-3-small
+ openai-organization:
+ - crewai-iuxna1
+ openai-processing-ms:
+ - '1090'
+ openai-version:
+ - '2020-10-01'
+ strict-transport-security:
+ - max-age=31536000; includeSubDomains; preload
+ via:
+ - envoy-router-7c649fddd4-v8m26
+ x-envoy-upstream-service-time:
+ - '1036'
+ x-ratelimit-limit-requests:
+ - '10000'
+ x-ratelimit-limit-tokens:
+ - '10000000'
+ x-ratelimit-remaining-requests:
+ - '9999'
+ x-ratelimit-remaining-tokens:
+ - '9999986'
+ x-ratelimit-reset-requests:
+ - 6ms
+ x-ratelimit-reset-tokens:
+ - 0s
+ x-request-id:
+ - req_77c48e76b18b892fec2de24815ac2b92
+ http_version: HTTP/1.1
+ status_code: 200
+version: 1
diff --git a/tests/cassettes/test_deepseek_r1_with_open_router.yaml b/tests/cassettes/test_deepseek_r1_with_open_router.yaml
new file mode 100644
index 000000000..a74c9283d
--- /dev/null
+++ b/tests/cassettes/test_deepseek_r1_with_open_router.yaml
@@ -0,0 +1,100 @@
+interactions:
+- request:
+ body: '{"model": "deepseek/deepseek-r1", "messages": [{"role": "user", "content":
+ "What is the capital of France?"}], "stop": [], "stream": false}'
+ headers:
+ accept:
+ - '*/*'
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '139'
+ host:
+ - openrouter.ai
+ http-referer:
+ - https://litellm.ai
+ user-agent:
+ - litellm/1.60.2
+ x-title:
+ - liteLLM
+ method: POST
+ uri: https://openrouter.ai/api/v1/chat/completions
+ response:
+ content: "\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n\n
+ \ \n\n \n\n \n\n \n\n \n\n \n{\"id\":\"gen-1738684300-YnD5WOSczQWsW0vQG78a\",\"provider\":\"Nebius\",\"model\":\"deepseek/deepseek-r1\",\"object\":\"chat.completion\",\"created\":1738684300,\"choices\":[{\"logprobs\":null,\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"The
+ capital of France is **Paris**. Known for its iconic landmarks such as the Eiffel
+ Tower, Notre-Dame Cathedral, and the Louvre Museum, Paris has served as the
+ political and cultural center of France for centuries. \U0001F1EB\U0001F1F7\",\"refusal\":null}}],\"usage\":{\"prompt_tokens\":10,\"completion_tokens\":261,\"total_tokens\":271}}"
+ headers:
+ Access-Control-Allow-Origin:
+ - '*'
+ CF-RAY:
+ - 90cbd2ceaf3ead5e-ATL
+ Connection:
+ - keep-alive
+ Content-Encoding:
+ - gzip
+ Content-Type:
+ - application/json
+ Date:
+ - Tue, 04 Feb 2025 15:51:40 GMT
+ Server:
+ - cloudflare
+ Transfer-Encoding:
+ - chunked
+ Vary:
+ - Accept-Encoding
+ x-clerk-auth-message:
+ - Invalid JWT form. A JWT consists of three parts separated by dots. (reason=token-invalid,
+ token-carrier=header)
+ x-clerk-auth-reason:
+ - token-invalid
+ x-clerk-auth-status:
+ - signed-out
+ http_version: HTTP/1.1
+ status_code: 200
+version: 1
diff --git a/tests/cassettes/test_llm_call_with_message_list.yaml b/tests/cassettes/test_llm_call_with_message_list.yaml
new file mode 100644
index 000000000..da204c647
--- /dev/null
+++ b/tests/cassettes/test_llm_call_with_message_list.yaml
@@ -0,0 +1,102 @@
+interactions:
+- request:
+ body: '{"messages": [{"role": "user", "content": "What is the capital of France?"}],
+ "model": "gpt-4o-mini"}'
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '101'
+ content-type:
+ - application/json
+ cookie:
+ - _cfuvid=8NrWEBP3dDmc8p2.csR.EdsSwS8zFvzWI1kPICaK_fM-1737568015338-0.0.1.1-604800000;
+ __cf_bm=pKr3NwXmTZN9rMSlKvEX40VPKbrxF93QwDNHunL2v8Y-1737568015-1.0.1.1-nR0EA7hYIwWpIBYUI53d9xQrUnl5iML6lgz4AGJW4ZGPBDxFma3PZ2cBhlr_hE7wKa5fV3r32eMu_rNWMXD.eA
+ host:
+ - api.openai.com
+ user-agent:
+ - OpenAI/Python 1.59.6
+ x-stainless-arch:
+ - arm64
+ x-stainless-async:
+ - 'false'
+ x-stainless-lang:
+ - python
+ x-stainless-os:
+ - MacOS
+ x-stainless-package-version:
+ - 1.59.6
+ x-stainless-raw-response:
+ - 'true'
+ x-stainless-retry-count:
+ - '0'
+ x-stainless-runtime:
+ - CPython
+ x-stainless-runtime-version:
+ - 3.12.7
+ method: POST
+ uri: https://api.openai.com/v1/chat/completions
+ response:
+ content: "{\n \"id\": \"chatcmpl-AsZ6WjNfEOrHwwEEdSZZCRBiTpBMS\",\n \"object\":
+ \"chat.completion\",\n \"created\": 1737568016,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n
+ \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
+ \"assistant\",\n \"content\": \"The capital of France is Paris.\",\n
+ \ \"refusal\": null\n },\n \"logprobs\": null,\n \"finish_reason\":
+ \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 14,\n \"completion_tokens\":
+ 8,\n \"total_tokens\": 22,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
+ 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n
+ \ \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
+ 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
+ \"default\",\n \"system_fingerprint\": \"fp_72ed7ab54c\"\n}\n"
+ headers:
+ CF-Cache-Status:
+ - DYNAMIC
+ CF-RAY:
+ - 90615dc63b805cb1-RDU
+ Connection:
+ - keep-alive
+ Content-Encoding:
+ - gzip
+ Content-Type:
+ - application/json
+ Date:
+ - Wed, 22 Jan 2025 17:46:56 GMT
+ Server:
+ - cloudflare
+ Transfer-Encoding:
+ - chunked
+ X-Content-Type-Options:
+ - nosniff
+ access-control-expose-headers:
+ - X-Request-ID
+ alt-svc:
+ - h3=":443"; ma=86400
+ openai-organization:
+ - crewai-iuxna1
+ openai-processing-ms:
+ - '355'
+ openai-version:
+ - '2020-10-01'
+ strict-transport-security:
+ - max-age=31536000; includeSubDomains; preload
+ x-ratelimit-limit-requests:
+ - '30000'
+ x-ratelimit-limit-tokens:
+ - '150000000'
+ x-ratelimit-remaining-requests:
+ - '29999'
+ x-ratelimit-remaining-tokens:
+ - '149999974'
+ x-ratelimit-reset-requests:
+ - 2ms
+ x-ratelimit-reset-tokens:
+ - 0s
+ x-request-id:
+ - req_cdbed69c9c63658eb552b07f1220df19
+ http_version: HTTP/1.1
+ status_code: 200
+version: 1
diff --git a/tests/cassettes/test_llm_call_with_string_input.yaml b/tests/cassettes/test_llm_call_with_string_input.yaml
new file mode 100644
index 000000000..f0c2a51e6
--- /dev/null
+++ b/tests/cassettes/test_llm_call_with_string_input.yaml
@@ -0,0 +1,108 @@
+interactions:
+- request:
+ body: '{"messages": [{"role": "user", "content": "Return the name of a random
+ city in the world."}], "model": "gpt-4o-mini"}'
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '117'
+ content-type:
+ - application/json
+ cookie:
+ - _cfuvid=3UeEmz_rnmsoZxrVUv32u35gJOi766GDWNe5_RTjiPk-1736537376739-0.0.1.1-604800000
+ host:
+ - api.openai.com
+ user-agent:
+ - OpenAI/Python 1.59.6
+ x-stainless-arch:
+ - arm64
+ x-stainless-async:
+ - 'false'
+ x-stainless-lang:
+ - python
+ x-stainless-os:
+ - MacOS
+ x-stainless-package-version:
+ - 1.59.6
+ x-stainless-raw-response:
+ - 'true'
+ x-stainless-retry-count:
+ - '0'
+ x-stainless-runtime:
+ - CPython
+ x-stainless-runtime-version:
+ - 3.12.7
+ method: POST
+ uri: https://api.openai.com/v1/chat/completions
+ response:
+ content: "{\n \"id\": \"chatcmpl-AsZ6UtbaNSMpNU9VJKxvn52t5eJTq\",\n \"object\":
+ \"chat.completion\",\n \"created\": 1737568014,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n
+ \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
+ \"assistant\",\n \"content\": \"How about \\\"Lisbon\\\"? It\u2019s the
+ capital city of Portugal, known for its rich history and vibrant culture.\",\n
+ \ \"refusal\": null\n },\n \"logprobs\": null,\n \"finish_reason\":
+ \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 18,\n \"completion_tokens\":
+ 24,\n \"total_tokens\": 42,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
+ 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n
+ \ \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
+ 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
+ \"default\",\n \"system_fingerprint\": \"fp_72ed7ab54c\"\n}\n"
+ headers:
+ CF-Cache-Status:
+ - DYNAMIC
+ CF-RAY:
+ - 90615dbcaefb5cb1-RDU
+ Connection:
+ - keep-alive
+ Content-Encoding:
+ - gzip
+ Content-Type:
+ - application/json
+ Date:
+ - Wed, 22 Jan 2025 17:46:55 GMT
+ Server:
+ - cloudflare
+ Set-Cookie:
+ - __cf_bm=pKr3NwXmTZN9rMSlKvEX40VPKbrxF93QwDNHunL2v8Y-1737568015-1.0.1.1-nR0EA7hYIwWpIBYUI53d9xQrUnl5iML6lgz4AGJW4ZGPBDxFma3PZ2cBhlr_hE7wKa5fV3r32eMu_rNWMXD.eA;
+ path=/; expires=Wed, 22-Jan-25 18:16:55 GMT; domain=.api.openai.com; HttpOnly;
+ Secure; SameSite=None
+ - _cfuvid=8NrWEBP3dDmc8p2.csR.EdsSwS8zFvzWI1kPICaK_fM-1737568015338-0.0.1.1-604800000;
+ path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
+ Transfer-Encoding:
+ - chunked
+ X-Content-Type-Options:
+ - nosniff
+ access-control-expose-headers:
+ - X-Request-ID
+ alt-svc:
+ - h3=":443"; ma=86400
+ openai-organization:
+ - crewai-iuxna1
+ openai-processing-ms:
+ - '449'
+ openai-version:
+ - '2020-10-01'
+ strict-transport-security:
+ - max-age=31536000; includeSubDomains; preload
+ x-ratelimit-limit-requests:
+ - '30000'
+ x-ratelimit-limit-tokens:
+ - '150000000'
+ x-ratelimit-remaining-requests:
+ - '29999'
+ x-ratelimit-remaining-tokens:
+ - '149999971'
+ x-ratelimit-reset-requests:
+ - 2ms
+ x-ratelimit-reset-tokens:
+ - 0s
+ x-request-id:
+ - req_898373758d2eae3cd84814050b2588e3
+ http_version: HTTP/1.1
+ status_code: 200
+version: 1
diff --git a/tests/cassettes/test_llm_call_with_string_input_and_callbacks.yaml b/tests/cassettes/test_llm_call_with_string_input_and_callbacks.yaml
new file mode 100644
index 000000000..a930a60a7
--- /dev/null
+++ b/tests/cassettes/test_llm_call_with_string_input_and_callbacks.yaml
@@ -0,0 +1,102 @@
+interactions:
+- request:
+ body: '{"messages": [{"role": "user", "content": "Tell me a joke."}], "model":
+ "gpt-4o-mini"}'
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '86'
+ content-type:
+ - application/json
+ cookie:
+ - _cfuvid=8NrWEBP3dDmc8p2.csR.EdsSwS8zFvzWI1kPICaK_fM-1737568015338-0.0.1.1-604800000;
+ __cf_bm=pKr3NwXmTZN9rMSlKvEX40VPKbrxF93QwDNHunL2v8Y-1737568015-1.0.1.1-nR0EA7hYIwWpIBYUI53d9xQrUnl5iML6lgz4AGJW4ZGPBDxFma3PZ2cBhlr_hE7wKa5fV3r32eMu_rNWMXD.eA
+ host:
+ - api.openai.com
+ user-agent:
+ - OpenAI/Python 1.59.6
+ x-stainless-arch:
+ - arm64
+ x-stainless-async:
+ - 'false'
+ x-stainless-lang:
+ - python
+ x-stainless-os:
+ - MacOS
+ x-stainless-package-version:
+ - 1.59.6
+ x-stainless-raw-response:
+ - 'true'
+ x-stainless-retry-count:
+ - '0'
+ x-stainless-runtime:
+ - CPython
+ x-stainless-runtime-version:
+ - 3.12.7
+ method: POST
+ uri: https://api.openai.com/v1/chat/completions
+ response:
+ content: "{\n \"id\": \"chatcmpl-AsZ6VyjuUcXYpChXmD8rUSy6nSGq8\",\n \"object\":
+ \"chat.completion\",\n \"created\": 1737568015,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n
+ \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
+ \"assistant\",\n \"content\": \"Why did the scarecrow win an award? \\n\\nBecause
+ he was outstanding in his field!\",\n \"refusal\": null\n },\n \"logprobs\":
+ null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
+ 12,\n \"completion_tokens\": 19,\n \"total_tokens\": 31,\n \"prompt_tokens_details\":
+ {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
+ {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
+ 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
+ \"default\",\n \"system_fingerprint\": \"fp_72ed7ab54c\"\n}\n"
+ headers:
+ CF-Cache-Status:
+ - DYNAMIC
+ CF-RAY:
+ - 90615dc03b6c5cb1-RDU
+ Connection:
+ - keep-alive
+ Content-Encoding:
+ - gzip
+ Content-Type:
+ - application/json
+ Date:
+ - Wed, 22 Jan 2025 17:46:56 GMT
+ Server:
+ - cloudflare
+ Transfer-Encoding:
+ - chunked
+ X-Content-Type-Options:
+ - nosniff
+ access-control-expose-headers:
+ - X-Request-ID
+ alt-svc:
+ - h3=":443"; ma=86400
+ openai-organization:
+ - crewai-iuxna1
+ openai-processing-ms:
+ - '825'
+ openai-version:
+ - '2020-10-01'
+ strict-transport-security:
+ - max-age=31536000; includeSubDomains; preload
+ x-ratelimit-limit-requests:
+ - '30000'
+ x-ratelimit-limit-tokens:
+ - '150000000'
+ x-ratelimit-remaining-requests:
+ - '29999'
+ x-ratelimit-remaining-tokens:
+ - '149999979'
+ x-ratelimit-reset-requests:
+ - 2ms
+ x-ratelimit-reset-tokens:
+ - 0s
+ x-request-id:
+ - req_4c1485d44e7461396d4a7316a63ff353
+ http_version: HTTP/1.1
+ status_code: 200
+version: 1
diff --git a/tests/cassettes/test_llm_call_with_tool_and_message_list.yaml b/tests/cassettes/test_llm_call_with_tool_and_message_list.yaml
new file mode 100644
index 000000000..6102d9ef1
--- /dev/null
+++ b/tests/cassettes/test_llm_call_with_tool_and_message_list.yaml
@@ -0,0 +1,111 @@
+interactions:
+- request:
+ body: '{"messages": [{"role": "user", "content": "What is the square of 5?"}],
+ "model": "gpt-4o-mini", "tools": [{"type": "function", "function": {"name":
+ "square_number", "description": "Returns the square of a number.", "parameters":
+ {"type": "object", "properties": {"number": {"type": "integer", "description":
+ "The number to square"}}, "required": ["number"]}}}]}'
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '361'
+ content-type:
+ - application/json
+ host:
+ - api.openai.com
+ user-agent:
+ - OpenAI/Python 1.59.6
+ x-stainless-arch:
+ - arm64
+ x-stainless-async:
+ - 'false'
+ x-stainless-lang:
+ - python
+ x-stainless-os:
+ - MacOS
+ x-stainless-package-version:
+ - 1.59.6
+ x-stainless-raw-response:
+ - 'true'
+ x-stainless-retry-count:
+ - '0'
+ x-stainless-runtime:
+ - CPython
+ x-stainless-runtime-version:
+ - 3.12.7
+ method: POST
+ uri: https://api.openai.com/v1/chat/completions
+ response:
+ content: "{\n \"id\": \"chatcmpl-AsZL5nGOaVpcGnDOesTxBZPHhMoaS\",\n \"object\":
+ \"chat.completion\",\n \"created\": 1737568919,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n
+ \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
+ \"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n
+ \ \"id\": \"call_i6JVJ1KxX79A4WzFri98E03U\",\n \"type\":
+ \"function\",\n \"function\": {\n \"name\": \"square_number\",\n
+ \ \"arguments\": \"{\\\"number\\\":5}\"\n }\n }\n
+ \ ],\n \"refusal\": null\n },\n \"logprobs\": null,\n
+ \ \"finish_reason\": \"tool_calls\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
+ 58,\n \"completion_tokens\": 15,\n \"total_tokens\": 73,\n \"prompt_tokens_details\":
+ {\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
+ {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
+ 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
+ \"default\",\n \"system_fingerprint\": \"fp_72ed7ab54c\"\n}\n"
+ headers:
+ CF-Cache-Status:
+ - DYNAMIC
+ CF-RAY:
+ - 906173d229b905f6-IAD
+ Connection:
+ - keep-alive
+ Content-Encoding:
+ - gzip
+ Content-Type:
+ - application/json
+ Date:
+ - Wed, 22 Jan 2025 18:02:00 GMT
+ Server:
+ - cloudflare
+ Set-Cookie:
+ - __cf_bm=BYDpIoqfPZyRxl9xcFxkt4IzTUGe8irWQlZ.aYLt8Xc-1737568920-1.0.1.1-Y_cVFN7TbguWRBorSKZynVY02QUtYbsbHuR2gR1wJ8LHuqOF4xIxtK5iHVCpWWgIyPDol9xOXiqUkU8xRV_vHA;
+ path=/; expires=Wed, 22-Jan-25 18:32:00 GMT; domain=.api.openai.com; HttpOnly;
+ Secure; SameSite=None
+ - _cfuvid=etTqqA9SBOnENmrFAUBIexdW0v2ZeO1x9_Ek_WChlfU-1737568920137-0.0.1.1-604800000;
+ path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
+ Transfer-Encoding:
+ - chunked
+ X-Content-Type-Options:
+ - nosniff
+ access-control-expose-headers:
+ - X-Request-ID
+ alt-svc:
+ - h3=":443"; ma=86400
+ openai-organization:
+ - crewai-iuxna1
+ openai-processing-ms:
+ - '642'
+ openai-version:
+ - '2020-10-01'
+ strict-transport-security:
+ - max-age=31536000; includeSubDomains; preload
+ x-ratelimit-limit-requests:
+ - '30000'
+ x-ratelimit-limit-tokens:
+ - '150000000'
+ x-ratelimit-remaining-requests:
+ - '29999'
+ x-ratelimit-remaining-tokens:
+ - '149999976'
+ x-ratelimit-reset-requests:
+ - 2ms
+ x-ratelimit-reset-tokens:
+ - 0s
+ x-request-id:
+ - req_388e63f9b8d4edc0dd153001f25388e5
+ http_version: HTTP/1.1
+ status_code: 200
+version: 1
diff --git a/tests/cassettes/test_llm_call_with_tool_and_string_input.yaml b/tests/cassettes/test_llm_call_with_tool_and_string_input.yaml
new file mode 100644
index 000000000..865d25826
--- /dev/null
+++ b/tests/cassettes/test_llm_call_with_tool_and_string_input.yaml
@@ -0,0 +1,107 @@
+interactions:
+- request:
+ body: '{"messages": [{"role": "user", "content": "What is the current year?"}],
+ "model": "gpt-4o-mini", "tools": [{"type": "function", "function": {"name":
+ "get_current_year", "description": "Returns the current year as a string.",
+ "parameters": {"type": "object", "properties": {}, "required": []}}}]}'
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '295'
+ content-type:
+ - application/json
+ cookie:
+ - _cfuvid=8NrWEBP3dDmc8p2.csR.EdsSwS8zFvzWI1kPICaK_fM-1737568015338-0.0.1.1-604800000;
+ __cf_bm=pKr3NwXmTZN9rMSlKvEX40VPKbrxF93QwDNHunL2v8Y-1737568015-1.0.1.1-nR0EA7hYIwWpIBYUI53d9xQrUnl5iML6lgz4AGJW4ZGPBDxFma3PZ2cBhlr_hE7wKa5fV3r32eMu_rNWMXD.eA
+ host:
+ - api.openai.com
+ user-agent:
+ - OpenAI/Python 1.59.6
+ x-stainless-arch:
+ - arm64
+ x-stainless-async:
+ - 'false'
+ x-stainless-lang:
+ - python
+ x-stainless-os:
+ - MacOS
+ x-stainless-package-version:
+ - 1.59.6
+ x-stainless-raw-response:
+ - 'true'
+ x-stainless-retry-count:
+ - '0'
+ x-stainless-runtime:
+ - CPython
+ x-stainless-runtime-version:
+ - 3.12.7
+ method: POST
+ uri: https://api.openai.com/v1/chat/completions
+ response:
+ content: "{\n \"id\": \"chatcmpl-AsZJ8HKXQU9nTB7xbGAkKxqrg9BZ2\",\n \"object\":
+ \"chat.completion\",\n \"created\": 1737568798,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n
+ \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
+ \"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n
+ \ \"id\": \"call_mfvEs2jngeFloVZpZOHZVaKY\",\n \"type\":
+ \"function\",\n \"function\": {\n \"name\": \"get_current_year\",\n
+ \ \"arguments\": \"{}\"\n }\n }\n ],\n
+ \ \"refusal\": null\n },\n \"logprobs\": null,\n \"finish_reason\":
+ \"tool_calls\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 46,\n \"completion_tokens\":
+ 12,\n \"total_tokens\": 58,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
+ 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n
+ \ \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
+ 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
+ \"default\",\n \"system_fingerprint\": \"fp_72ed7ab54c\"\n}\n"
+ headers:
+ CF-Cache-Status:
+ - DYNAMIC
+ CF-RAY:
+ - 906170e038281775-IAD
+ Connection:
+ - keep-alive
+ Content-Encoding:
+ - gzip
+ Content-Type:
+ - application/json
+ Date:
+ - Wed, 22 Jan 2025 17:59:59 GMT
+ Server:
+ - cloudflare
+ Transfer-Encoding:
+ - chunked
+ X-Content-Type-Options:
+ - nosniff
+ access-control-expose-headers:
+ - X-Request-ID
+ alt-svc:
+ - h3=":443"; ma=86400
+ openai-organization:
+ - crewai-iuxna1
+ openai-processing-ms:
+ - '416'
+ openai-version:
+ - '2020-10-01'
+ strict-transport-security:
+ - max-age=31536000; includeSubDomains; preload
+ x-ratelimit-limit-requests:
+ - '30000'
+ x-ratelimit-limit-tokens:
+ - '150000000'
+ x-ratelimit-remaining-requests:
+ - '29999'
+ x-ratelimit-remaining-tokens:
+ - '149999975'
+ x-ratelimit-reset-requests:
+ - 2ms
+ x-ratelimit-reset-tokens:
+ - 0s
+ x-request-id:
+ - req_4039a5e5772d1790a3131f0b1ea06139
+ http_version: HTTP/1.1
+ status_code: 200
+version: 1
diff --git a/tests/cassettes/test_o3_mini_reasoning_effort_high.yaml b/tests/cassettes/test_o3_mini_reasoning_effort_high.yaml
new file mode 100644
index 000000000..d21189e82
--- /dev/null
+++ b/tests/cassettes/test_o3_mini_reasoning_effort_high.yaml
@@ -0,0 +1,107 @@
+interactions:
+- request:
+ body: '{"messages": [{"role": "user", "content": "What is the capital of France?"}],
+ "model": "o3-mini", "reasoning_effort": "high", "stop": []}'
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '137'
+ content-type:
+ - application/json
+ cookie:
+ - _cfuvid=etTqqA9SBOnENmrFAUBIexdW0v2ZeO1x9_Ek_WChlfU-1737568920137-0.0.1.1-604800000
+ host:
+ - api.openai.com
+ user-agent:
+ - OpenAI/Python 1.61.0
+ x-stainless-arch:
+ - arm64
+ x-stainless-async:
+ - 'false'
+ x-stainless-lang:
+ - python
+ x-stainless-os:
+ - MacOS
+ x-stainless-package-version:
+ - 1.61.0
+ x-stainless-raw-response:
+ - 'true'
+ x-stainless-retry-count:
+ - '0'
+ x-stainless-runtime:
+ - CPython
+ x-stainless-runtime-version:
+ - 3.12.7
+ method: POST
+ uri: https://api.openai.com/v1/chat/completions
+ response:
+ content: "{\n \"id\": \"chatcmpl-AxFNUz7l4pwtY9xhFSPIGlwNfE4Sj\",\n \"object\":
+ \"chat.completion\",\n \"created\": 1738683828,\n \"model\": \"o3-mini-2025-01-31\",\n
+ \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
+ \"assistant\",\n \"content\": \"The capital of France is Paris.\",\n
+ \ \"refusal\": null\n },\n \"finish_reason\": \"stop\"\n }\n
+ \ ],\n \"usage\": {\n \"prompt_tokens\": 13,\n \"completion_tokens\":
+ 81,\n \"total_tokens\": 94,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
+ 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n
+ \ \"reasoning_tokens\": 64,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
+ 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
+ \"default\",\n \"system_fingerprint\": \"fp_8bcaa0ca21\"\n}\n"
+ headers:
+ CF-RAY:
+ - 90cbc745d91fb0ca-ATL
+ Connection:
+ - keep-alive
+ Content-Encoding:
+ - gzip
+ Content-Type:
+ - application/json
+ Date:
+ - Tue, 04 Feb 2025 15:43:50 GMT
+ Server:
+ - cloudflare
+ Set-Cookie:
+ - __cf_bm=.AP74BirsYr.lu61bSaimK2HRF6126qr5vCrr3HC6ak-1738683830-1.0.1.1-feh.bcMOv9wYnitoPpr.7UR7JrzCsbRLlzct09xCDm2SwmnRQQk5ZSSV41Ywer2S0rptbvufFwklV9wo9ATvWw;
+ path=/; expires=Tue, 04-Feb-25 16:13:50 GMT; domain=.api.openai.com; HttpOnly;
+ Secure; SameSite=None
+ - _cfuvid=JBfx8Sl7w82A0S_K1tQd5ZcwzWaZP5Gg5W1dqAdgwNU-1738683830528-0.0.1.1-604800000;
+ path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
+ 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:
+ - '2169'
+ openai-version:
+ - '2020-10-01'
+ strict-transport-security:
+ - max-age=31536000; includeSubDomains; preload
+ x-ratelimit-limit-requests:
+ - '30000'
+ x-ratelimit-limit-tokens:
+ - '150000000'
+ x-ratelimit-remaining-requests:
+ - '29999'
+ x-ratelimit-remaining-tokens:
+ - '149999974'
+ x-ratelimit-reset-requests:
+ - 2ms
+ x-ratelimit-reset-tokens:
+ - 0s
+ x-request-id:
+ - req_163e7bd79cb5a5e62d4688245b97d1d9
+ http_version: HTTP/1.1
+ status_code: 200
+version: 1
diff --git a/tests/cassettes/test_o3_mini_reasoning_effort_low.yaml b/tests/cassettes/test_o3_mini_reasoning_effort_low.yaml
new file mode 100644
index 000000000..624bf386d
--- /dev/null
+++ b/tests/cassettes/test_o3_mini_reasoning_effort_low.yaml
@@ -0,0 +1,102 @@
+interactions:
+- request:
+ body: '{"messages": [{"role": "user", "content": "What is the capital of France?"}],
+ "model": "o3-mini", "reasoning_effort": "low", "stop": []}'
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '136'
+ content-type:
+ - application/json
+ cookie:
+ - _cfuvid=JBfx8Sl7w82A0S_K1tQd5ZcwzWaZP5Gg5W1dqAdgwNU-1738683830528-0.0.1.1-604800000;
+ __cf_bm=.AP74BirsYr.lu61bSaimK2HRF6126qr5vCrr3HC6ak-1738683830-1.0.1.1-feh.bcMOv9wYnitoPpr.7UR7JrzCsbRLlzct09xCDm2SwmnRQQk5ZSSV41Ywer2S0rptbvufFwklV9wo9ATvWw
+ host:
+ - api.openai.com
+ user-agent:
+ - OpenAI/Python 1.61.0
+ x-stainless-arch:
+ - arm64
+ x-stainless-async:
+ - 'false'
+ x-stainless-lang:
+ - python
+ x-stainless-os:
+ - MacOS
+ x-stainless-package-version:
+ - 1.61.0
+ x-stainless-raw-response:
+ - 'true'
+ x-stainless-retry-count:
+ - '0'
+ x-stainless-runtime:
+ - CPython
+ x-stainless-runtime-version:
+ - 3.12.7
+ method: POST
+ uri: https://api.openai.com/v1/chat/completions
+ response:
+ content: "{\n \"id\": \"chatcmpl-AxFNWljEYFrf5qRwYj73OPQtAnPbF\",\n \"object\":
+ \"chat.completion\",\n \"created\": 1738683830,\n \"model\": \"o3-mini-2025-01-31\",\n
+ \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
+ \"assistant\",\n \"content\": \"The capital of France is Paris.\",\n
+ \ \"refusal\": null\n },\n \"finish_reason\": \"stop\"\n }\n
+ \ ],\n \"usage\": {\n \"prompt_tokens\": 13,\n \"completion_tokens\":
+ 17,\n \"total_tokens\": 30,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
+ 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n
+ \ \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
+ 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
+ \"default\",\n \"system_fingerprint\": \"fp_8bcaa0ca21\"\n}\n"
+ headers:
+ CF-RAY:
+ - 90cbc7551fe0b0ca-ATL
+ Connection:
+ - keep-alive
+ Content-Encoding:
+ - gzip
+ Content-Type:
+ - application/json
+ Date:
+ - Tue, 04 Feb 2025 15:43:51 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:
+ - '1103'
+ openai-version:
+ - '2020-10-01'
+ strict-transport-security:
+ - max-age=31536000; includeSubDomains; preload
+ x-ratelimit-limit-requests:
+ - '30000'
+ x-ratelimit-limit-tokens:
+ - '150000000'
+ x-ratelimit-remaining-requests:
+ - '29999'
+ x-ratelimit-remaining-tokens:
+ - '149999974'
+ x-ratelimit-reset-requests:
+ - 2ms
+ x-ratelimit-reset-tokens:
+ - 0s
+ x-request-id:
+ - req_fd7178a0e5060216d04f3bd023e8bca1
+ http_version: HTTP/1.1
+ status_code: 200
+version: 1
diff --git a/tests/cassettes/test_o3_mini_reasoning_effort_medium.yaml b/tests/cassettes/test_o3_mini_reasoning_effort_medium.yaml
new file mode 100644
index 000000000..d1e31eff5
--- /dev/null
+++ b/tests/cassettes/test_o3_mini_reasoning_effort_medium.yaml
@@ -0,0 +1,102 @@
+interactions:
+- request:
+ body: '{"messages": [{"role": "user", "content": "What is the capital of France?"}],
+ "model": "o3-mini", "reasoning_effort": "medium", "stop": []}'
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate
+ connection:
+ - keep-alive
+ content-length:
+ - '139'
+ content-type:
+ - application/json
+ cookie:
+ - _cfuvid=JBfx8Sl7w82A0S_K1tQd5ZcwzWaZP5Gg5W1dqAdgwNU-1738683830528-0.0.1.1-604800000;
+ __cf_bm=.AP74BirsYr.lu61bSaimK2HRF6126qr5vCrr3HC6ak-1738683830-1.0.1.1-feh.bcMOv9wYnitoPpr.7UR7JrzCsbRLlzct09xCDm2SwmnRQQk5ZSSV41Ywer2S0rptbvufFwklV9wo9ATvWw
+ host:
+ - api.openai.com
+ user-agent:
+ - OpenAI/Python 1.61.0
+ x-stainless-arch:
+ - arm64
+ x-stainless-async:
+ - 'false'
+ x-stainless-lang:
+ - python
+ x-stainless-os:
+ - MacOS
+ x-stainless-package-version:
+ - 1.61.0
+ x-stainless-raw-response:
+ - 'true'
+ x-stainless-retry-count:
+ - '0'
+ x-stainless-runtime:
+ - CPython
+ x-stainless-runtime-version:
+ - 3.12.7
+ method: POST
+ uri: https://api.openai.com/v1/chat/completions
+ response:
+ content: "{\n \"id\": \"chatcmpl-AxFS8IuMeYs6Rky2UbG8wH8P5PR4k\",\n \"object\":
+ \"chat.completion\",\n \"created\": 1738684116,\n \"model\": \"o3-mini-2025-01-31\",\n
+ \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
+ \"assistant\",\n \"content\": \"The capital of France is Paris.\",\n
+ \ \"refusal\": null\n },\n \"finish_reason\": \"stop\"\n }\n
+ \ ],\n \"usage\": {\n \"prompt_tokens\": 13,\n \"completion_tokens\":
+ 145,\n \"total_tokens\": 158,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
+ 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n
+ \ \"reasoning_tokens\": 128,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
+ 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
+ \"default\",\n \"system_fingerprint\": \"fp_8bcaa0ca21\"\n}\n"
+ headers:
+ CF-RAY:
+ - 90cbce51b946afb4-ATL
+ Connection:
+ - keep-alive
+ Content-Encoding:
+ - gzip
+ Content-Type:
+ - application/json
+ Date:
+ - Tue, 04 Feb 2025 15:48:39 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:
+ - '2365'
+ openai-version:
+ - '2020-10-01'
+ strict-transport-security:
+ - max-age=31536000; includeSubDomains; preload
+ x-ratelimit-limit-requests:
+ - '30000'
+ x-ratelimit-limit-tokens:
+ - '150000000'
+ x-ratelimit-remaining-requests:
+ - '29999'
+ x-ratelimit-remaining-tokens:
+ - '149999974'
+ x-ratelimit-reset-requests:
+ - 2ms
+ x-ratelimit-reset-tokens:
+ - 0s
+ x-request-id:
+ - req_bfd83679e674c3894991477f1fb043b2
+ http_version: HTTP/1.1
+ status_code: 200
+version: 1
diff --git a/tests/cli/cli_test.py b/tests/cli/cli_test.py
index 15ed81637..dc0c502b7 100644
--- a/tests/cli/cli_test.py
+++ b/tests/cli/cli_test.py
@@ -55,72 +55,83 @@ def test_train_invalid_string_iterations(train_crew, runner):
)
-@mock.patch("crewai.cli.reset_memories_command.ShortTermMemory")
-@mock.patch("crewai.cli.reset_memories_command.EntityMemory")
-@mock.patch("crewai.cli.reset_memories_command.LongTermMemory")
-@mock.patch("crewai.cli.reset_memories_command.TaskOutputStorageHandler")
-def test_reset_all_memories(
- MockTaskOutputStorageHandler,
- MockLongTermMemory,
- MockEntityMemory,
- MockShortTermMemory,
- runner,
-):
- result = runner.invoke(reset_memories, ["--all"])
- MockShortTermMemory().reset.assert_called_once()
- MockEntityMemory().reset.assert_called_once()
- MockLongTermMemory().reset.assert_called_once()
- MockTaskOutputStorageHandler().reset.assert_called_once()
+@mock.patch("crewai.cli.reset_memories_command.get_crew")
+def test_reset_all_memories(mock_get_crew, runner):
+ mock_crew = mock.Mock()
+ mock_get_crew.return_value = mock_crew
+ result = runner.invoke(reset_memories, ["-a"])
+ mock_crew.reset_memories.assert_called_once_with(command_type="all")
assert result.output == "All memories have been reset.\n"
-@mock.patch("crewai.cli.reset_memories_command.ShortTermMemory")
-def test_reset_short_term_memories(MockShortTermMemory, runner):
+@mock.patch("crewai.cli.reset_memories_command.get_crew")
+def test_reset_short_term_memories(mock_get_crew, runner):
+ mock_crew = mock.Mock()
+ mock_get_crew.return_value = mock_crew
result = runner.invoke(reset_memories, ["-s"])
- MockShortTermMemory().reset.assert_called_once()
+
+ mock_crew.reset_memories.assert_called_once_with(command_type="short")
assert result.output == "Short term memory has been reset.\n"
-@mock.patch("crewai.cli.reset_memories_command.EntityMemory")
-def test_reset_entity_memories(MockEntityMemory, runner):
+@mock.patch("crewai.cli.reset_memories_command.get_crew")
+def test_reset_entity_memories(mock_get_crew, runner):
+ mock_crew = mock.Mock()
+ mock_get_crew.return_value = mock_crew
result = runner.invoke(reset_memories, ["-e"])
- MockEntityMemory().reset.assert_called_once()
+
+ mock_crew.reset_memories.assert_called_once_with(command_type="entity")
assert result.output == "Entity memory has been reset.\n"
-@mock.patch("crewai.cli.reset_memories_command.LongTermMemory")
-def test_reset_long_term_memories(MockLongTermMemory, runner):
+@mock.patch("crewai.cli.reset_memories_command.get_crew")
+def test_reset_long_term_memories(mock_get_crew, runner):
+ mock_crew = mock.Mock()
+ mock_get_crew.return_value = mock_crew
result = runner.invoke(reset_memories, ["-l"])
- MockLongTermMemory().reset.assert_called_once()
+
+ mock_crew.reset_memories.assert_called_once_with(command_type="long")
assert result.output == "Long term memory has been reset.\n"
-@mock.patch("crewai.cli.reset_memories_command.TaskOutputStorageHandler")
-def test_reset_kickoff_outputs(MockTaskOutputStorageHandler, runner):
+@mock.patch("crewai.cli.reset_memories_command.get_crew")
+def test_reset_kickoff_outputs(mock_get_crew, runner):
+ mock_crew = mock.Mock()
+ mock_get_crew.return_value = mock_crew
result = runner.invoke(reset_memories, ["-k"])
- MockTaskOutputStorageHandler().reset.assert_called_once()
+
+ mock_crew.reset_memories.assert_called_once_with(command_type="kickoff_outputs")
assert result.output == "Latest Kickoff outputs stored has been reset.\n"
-@mock.patch("crewai.cli.reset_memories_command.ShortTermMemory")
-@mock.patch("crewai.cli.reset_memories_command.LongTermMemory")
-def test_reset_multiple_memory_flags(MockShortTermMemory, MockLongTermMemory, runner):
- result = runner.invoke(
- reset_memories,
- [
- "-s",
- "-l",
- ],
+@mock.patch("crewai.cli.reset_memories_command.get_crew")
+def test_reset_multiple_memory_flags(mock_get_crew, runner):
+ mock_crew = mock.Mock()
+ mock_get_crew.return_value = mock_crew
+ result = runner.invoke(reset_memories, ["-s", "-l"])
+
+ # Check that reset_memories was called twice with the correct arguments
+ assert mock_crew.reset_memories.call_count == 2
+ mock_crew.reset_memories.assert_has_calls(
+ [mock.call(command_type="long"), mock.call(command_type="short")]
)
- MockShortTermMemory().reset.assert_called_once()
- MockLongTermMemory().reset.assert_called_once()
assert (
result.output
== "Long term memory has been reset.\nShort term memory has been reset.\n"
)
+@mock.patch("crewai.cli.reset_memories_command.get_crew")
+def test_reset_knowledge(mock_get_crew, runner):
+ mock_crew = mock.Mock()
+ mock_get_crew.return_value = mock_crew
+ result = runner.invoke(reset_memories, ["--knowledge"])
+
+ mock_crew.reset_memories.assert_called_once_with(command_type="knowledge")
+ assert result.output == "Knowledge has been reset.\n"
+
+
def test_reset_no_memory_flags(runner):
result = runner.invoke(
reset_memories,
diff --git a/tests/config/tasks.yaml b/tests/config/tasks.yaml
index f30820855..88639debb 100644
--- a/tests/config/tasks.yaml
+++ b/tests/config/tasks.yaml
@@ -2,7 +2,7 @@ research_task:
description: >
Conduct a thorough research about {topic}
Make sure you find any interesting and relevant information given
- the current year is 2024.
+ the current year is 2025.
expected_output: >
A list with 10 bullet points of the most relevant information about {topic}
agent: researcher
diff --git a/tests/crew_test.py b/tests/crew_test.py
index 74a659738..0539ea347 100644
--- a/tests/crew_test.py
+++ b/tests/crew_test.py
@@ -14,6 +14,7 @@ from crewai.agent import Agent
from crewai.agents.cache import CacheHandler
from crewai.crew import Crew
from crewai.crews.crew_output import CrewOutput
+from crewai.knowledge.source.string_knowledge_source import StringKnowledgeSource
from crewai.memory.contextual.contextual_memory import ContextualMemory
from crewai.process import Process
from crewai.project import crew
@@ -48,6 +49,41 @@ writer = Agent(
)
+def test_crew_with_only_conditional_tasks_raises_error():
+ """Test that creating a crew with only conditional tasks raises an error."""
+
+ def condition_func(task_output: TaskOutput) -> bool:
+ return True
+
+ conditional1 = ConditionalTask(
+ description="Conditional task 1",
+ expected_output="Output 1",
+ agent=researcher,
+ condition=condition_func,
+ )
+ conditional2 = ConditionalTask(
+ description="Conditional task 2",
+ expected_output="Output 2",
+ agent=researcher,
+ condition=condition_func,
+ )
+ conditional3 = ConditionalTask(
+ description="Conditional task 3",
+ expected_output="Output 3",
+ agent=researcher,
+ condition=condition_func,
+ )
+
+ with pytest.raises(
+ pydantic_core._pydantic_core.ValidationError,
+ match="Crew must include at least one non-conditional task",
+ ):
+ Crew(
+ agents=[researcher],
+ tasks=[conditional1, conditional2, conditional3],
+ )
+
+
def test_crew_config_conditional_requirement():
with pytest.raises(ValueError):
Crew(process=Process.sequential)
@@ -1466,7 +1502,6 @@ def test_dont_set_agents_step_callback_if_already_set():
@pytest.mark.vcr(filter_headers=["authorization"])
def test_crew_function_calling_llm():
-
from crewai import LLM
from crewai.tools import tool
@@ -1917,6 +1952,78 @@ def test_task_callback_on_crew():
assert isinstance(args[0], TaskOutput)
+def test_task_callback_both_on_task_and_crew():
+ from unittest.mock import MagicMock, patch
+
+ mock_callback_on_task = MagicMock()
+ mock_callback_on_crew = MagicMock()
+
+ researcher_agent = Agent(
+ role="Researcher",
+ goal="Make the best research and analysis on content about AI and AI agents",
+ backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.",
+ allow_delegation=False,
+ )
+
+ list_ideas = Task(
+ description="Give me a list of 5 interesting ideas to explore for na article, what makes them unique and interesting.",
+ expected_output="Bullet point list of 5 important events.",
+ agent=researcher_agent,
+ async_execution=True,
+ callback=mock_callback_on_task,
+ )
+
+ crew = Crew(
+ agents=[researcher_agent],
+ process=Process.sequential,
+ tasks=[list_ideas],
+ task_callback=mock_callback_on_crew,
+ )
+
+ with patch.object(Agent, "execute_task") as execute:
+ execute.return_value = "ok"
+ crew.kickoff()
+
+ assert list_ideas.callback is not None
+ mock_callback_on_task.assert_called_once_with(list_ideas.output)
+ mock_callback_on_crew.assert_called_once_with(list_ideas.output)
+
+
+def test_task_same_callback_both_on_task_and_crew():
+ from unittest.mock import MagicMock, patch
+
+ mock_callback = MagicMock()
+
+ researcher_agent = Agent(
+ role="Researcher",
+ goal="Make the best research and analysis on content about AI and AI agents",
+ backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.",
+ allow_delegation=False,
+ )
+
+ list_ideas = Task(
+ description="Give me a list of 5 interesting ideas to explore for na article, what makes them unique and interesting.",
+ expected_output="Bullet point list of 5 important events.",
+ agent=researcher_agent,
+ async_execution=True,
+ callback=mock_callback,
+ )
+
+ crew = Crew(
+ agents=[researcher_agent],
+ process=Process.sequential,
+ tasks=[list_ideas],
+ task_callback=mock_callback,
+ )
+
+ with patch.object(Agent, "execute_task") as execute:
+ execute.return_value = "ok"
+ crew.kickoff()
+
+ assert list_ideas.callback is not None
+ mock_callback.assert_called_once_with(list_ideas.output)
+
+
@pytest.mark.vcr(filter_headers=["authorization"])
def test_tools_with_custom_caching():
from unittest.mock import patch
@@ -1989,6 +2096,210 @@ def test_tools_with_custom_caching():
assert result.raw == "3"
+@pytest.mark.vcr(filter_headers=["authorization"])
+def test_conditional_task_uses_last_output():
+ """Test that conditional tasks use the last task output for condition evaluation."""
+ task1 = Task(
+ description="First task",
+ expected_output="First output",
+ agent=researcher,
+ )
+
+ def condition_fails(task_output: TaskOutput) -> bool:
+ # This condition will never be met
+ return "never matches" in task_output.raw.lower()
+
+ def condition_succeeds(task_output: TaskOutput) -> bool:
+ # This condition will match first task's output
+ return "first success" in task_output.raw.lower()
+
+ conditional_task1 = ConditionalTask(
+ description="Second task - conditional that fails condition",
+ expected_output="Second output",
+ agent=researcher,
+ condition=condition_fails,
+ )
+
+ conditional_task2 = ConditionalTask(
+ description="Third task - conditional that succeeds using first task output",
+ expected_output="Third output",
+ agent=writer,
+ condition=condition_succeeds,
+ )
+
+ crew = Crew(
+ agents=[researcher, writer],
+ tasks=[task1, conditional_task1, conditional_task2],
+ )
+
+ # Mock outputs for tasks
+ mock_first = TaskOutput(
+ description="First task output",
+ raw="First success output", # Will be used by third task's condition
+ agent=researcher.role,
+ )
+ mock_third = TaskOutput(
+ description="Third task output",
+ raw="Third task executed", # Output when condition succeeds using first task output
+ agent=writer.role,
+ )
+
+ # Set up mocks for task execution and conditional logic
+ with patch.object(ConditionalTask, "should_execute") as mock_should_execute:
+ # First conditional fails, second succeeds
+ mock_should_execute.side_effect = [False, True]
+ with patch.object(Task, "execute_sync") as mock_execute:
+ mock_execute.side_effect = [mock_first, mock_third]
+ result = crew.kickoff()
+
+ # Verify execution behavior
+ assert mock_execute.call_count == 2 # Only first and third tasks execute
+ assert mock_should_execute.call_count == 2 # Both conditionals checked
+
+ # Verify outputs collection:
+ # First executed task output, followed by an automatically generated (skipped) output, then the conditional execution
+ assert len(result.tasks_output) == 3
+ assert (
+ result.tasks_output[0].raw == "First success output"
+ ) # First task succeeded
+ assert (
+ result.tasks_output[1].raw == ""
+ ) # Second task skipped (condition failed)
+ assert (
+ result.tasks_output[2].raw == "Third task executed"
+ ) # Third task used first task's output
+
+
+@pytest.mark.vcr(filter_headers=["authorization"])
+def test_conditional_tasks_result_collection():
+ """Test that task outputs are properly collected based on execution status."""
+ task1 = Task(
+ description="Normal task that always executes",
+ expected_output="First output",
+ agent=researcher,
+ )
+
+ def condition_never_met(task_output: TaskOutput) -> bool:
+ return "never matches" in task_output.raw.lower()
+
+ def condition_always_met(task_output: TaskOutput) -> bool:
+ return "success" in task_output.raw.lower()
+
+ task2 = ConditionalTask(
+ description="Conditional task that never executes",
+ expected_output="Second output",
+ agent=researcher,
+ condition=condition_never_met,
+ )
+
+ task3 = ConditionalTask(
+ description="Conditional task that always executes",
+ expected_output="Third output",
+ agent=writer,
+ condition=condition_always_met,
+ )
+
+ crew = Crew(
+ agents=[researcher, writer],
+ tasks=[task1, task2, task3],
+ )
+
+ # Mock outputs for different execution paths
+ mock_success = TaskOutput(
+ description="Success output",
+ raw="Success output", # Triggers third task's condition
+ agent=researcher.role,
+ )
+ mock_conditional = TaskOutput(
+ description="Conditional output",
+ raw="Conditional task executed",
+ agent=writer.role,
+ )
+
+ # Set up mocks for task execution and conditional logic
+ with patch.object(ConditionalTask, "should_execute") as mock_should_execute:
+ # First conditional fails, second succeeds
+ mock_should_execute.side_effect = [False, True]
+ with patch.object(Task, "execute_sync") as mock_execute:
+ mock_execute.side_effect = [mock_success, mock_conditional]
+ result = crew.kickoff()
+
+ # Verify execution behavior
+ assert mock_execute.call_count == 2 # Only first and third tasks execute
+ assert mock_should_execute.call_count == 2 # Both conditionals checked
+
+ # Verify task output collection:
+ # There should be three outputs: normal task, skipped conditional task (empty output),
+ # and the conditional task that executed.
+ assert len(result.tasks_output) == 3
+ assert (
+ result.tasks_output[0].raw == "Success output"
+ ) # Normal task executed
+ assert result.tasks_output[1].raw == "" # Second task skipped
+ assert (
+ result.tasks_output[2].raw == "Conditional task executed"
+ ) # Third task executed
+
+ # Verify task output collection
+ assert len(result.tasks_output) == 3
+ assert (
+ result.tasks_output[0].raw == "Success output"
+ ) # Normal task executed
+ assert result.tasks_output[1].raw == "" # Second task skipped
+ assert (
+ result.tasks_output[2].raw == "Conditional task executed"
+ ) # Third task executed
+
+
+@pytest.mark.vcr(filter_headers=["authorization"])
+def test_multiple_conditional_tasks():
+ """Test that having multiple conditional tasks in sequence works correctly."""
+ task1 = Task(
+ description="Initial research task",
+ expected_output="Research output",
+ agent=researcher,
+ )
+
+ def condition1(task_output: TaskOutput) -> bool:
+ return "success" in task_output.raw.lower()
+
+ def condition2(task_output: TaskOutput) -> bool:
+ return "proceed" in task_output.raw.lower()
+
+ task2 = ConditionalTask(
+ description="First conditional task",
+ expected_output="Conditional output 1",
+ agent=writer,
+ condition=condition1,
+ )
+
+ task3 = ConditionalTask(
+ description="Second conditional task",
+ expected_output="Conditional output 2",
+ agent=writer,
+ condition=condition2,
+ )
+
+ crew = Crew(
+ agents=[researcher, writer],
+ tasks=[task1, task2, task3],
+ )
+
+ # Mock different task outputs to test conditional logic
+ mock_success = TaskOutput(
+ description="Mock success",
+ raw="Success and proceed output",
+ agent=researcher.role,
+ )
+
+ # Set up mocks for task execution
+ with patch.object(Task, "execute_sync", return_value=mock_success) as mock_execute:
+ result = crew.kickoff()
+ # Verify all tasks were executed (no IndexError)
+ assert mock_execute.call_count == 3
+ assert len(result.tasks_output) == 3
+
+
@pytest.mark.vcr(filter_headers=["authorization"])
def test_using_contextual_memory():
from unittest.mock import patch
@@ -3480,10 +3791,12 @@ def test_crew_guardrail_feedback_in_context():
@pytest.mark.vcr(filter_headers=["authorization"])
def test_before_kickoff_callback():
- from crewai.project import CrewBase, agent, before_kickoff, crew, task
+ from crewai.project import CrewBase, agent, before_kickoff, task
@CrewBase
class TestCrewClass:
+ from crewai.project import crew
+
agents_config = None
tasks_config = None
@@ -3492,7 +3805,6 @@ def test_before_kickoff_callback():
@before_kickoff
def modify_inputs(self, inputs):
-
self.inputs_modified = True
inputs["modified"] = True
return inputs
@@ -3510,7 +3822,7 @@ def test_before_kickoff_callback():
task = Task(
description="Test task description",
expected_output="Test expected output",
- agent=self.my_agent(), # Use the agent instance
+ agent=self.my_agent(),
)
return task
@@ -3520,28 +3832,30 @@ def test_before_kickoff_callback():
test_crew_instance = TestCrewClass()
- crew = test_crew_instance.crew()
+ test_crew = test_crew_instance.crew()
# Verify that the before_kickoff_callbacks are set
- assert len(crew.before_kickoff_callbacks) == 1
+ assert len(test_crew.before_kickoff_callbacks) == 1
# Prepare inputs
inputs = {"initial": True}
# Call kickoff
- crew.kickoff(inputs=inputs)
+ test_crew.kickoff(inputs=inputs)
# Check that the before_kickoff function was called and modified inputs
assert test_crew_instance.inputs_modified
- assert inputs.get("modified") == True
+ assert inputs.get("modified")
@pytest.mark.vcr(filter_headers=["authorization"])
def test_before_kickoff_without_inputs():
- from crewai.project import CrewBase, agent, before_kickoff, crew, task
+ from crewai.project import CrewBase, agent, before_kickoff, task
@CrewBase
class TestCrewClass:
+ from crewai.project import crew
+
agents_config = None
tasks_config = None
@@ -3579,12 +3893,12 @@ def test_before_kickoff_without_inputs():
# Instantiate the class
test_crew_instance = TestCrewClass()
# Build the crew
- crew = test_crew_instance.crew()
+ test_crew = test_crew_instance.crew()
# Verify that the before_kickoff_callback is registered
- assert len(crew.before_kickoff_callbacks) == 1
+ assert len(test_crew.before_kickoff_callbacks) == 1
# Call kickoff without passing inputs
- output = crew.kickoff()
+ test_crew.kickoff()
# Check that the before_kickoff function was called
assert test_crew_instance.inputs_modified
@@ -3592,3 +3906,21 @@ def test_before_kickoff_without_inputs():
# Verify that the inputs were initialized and modified inside the before_kickoff method
assert test_crew_instance.received_inputs is not None
assert test_crew_instance.received_inputs.get("modified") is True
+
+
+@pytest.mark.vcr(filter_headers=["authorization"])
+def test_crew_with_knowledge_sources_works_with_copy():
+ content = "Brandon's favorite color is red and he likes Mexican food."
+ string_source = StringKnowledgeSource(content=content)
+
+ crew = Crew(
+ agents=[researcher, writer],
+ tasks=[Task(description="test", expected_output="test", agent=researcher)],
+ knowledge_sources=[string_source],
+ )
+
+ crew_copy = crew.copy()
+
+ assert crew_copy.knowledge_sources == crew.knowledge_sources
+ assert len(crew_copy.agents) == len(crew.agents)
+ assert len(crew_copy.tasks) == len(crew.tasks)
diff --git a/tests/flow_test.py b/tests/flow_test.py
index 44ea1d15d..d036f7987 100644
--- a/tests/flow_test.py
+++ b/tests/flow_test.py
@@ -1,11 +1,18 @@
"""Test Flow creation and execution basic functionality."""
import asyncio
+from datetime import datetime
import pytest
from pydantic import BaseModel
from crewai.flow.flow import Flow, and_, listen, or_, router, start
+from crewai.flow.flow_events import (
+ FlowFinishedEvent,
+ FlowStartedEvent,
+ MethodExecutionFinishedEvent,
+ MethodExecutionStartedEvent,
+)
def test_simple_sequential_flow():
@@ -398,3 +405,218 @@ def test_router_with_multiple_conditions():
# final_step should run after router_and
assert execution_order.index("log_final_step") > execution_order.index("router_and")
+
+
+def test_unstructured_flow_event_emission():
+ """Test that the correct events are emitted during unstructured flow
+ execution with all fields validated."""
+
+ class PoemFlow(Flow):
+ @start()
+ def prepare_flower(self):
+ self.state["flower"] = "roses"
+ return "foo"
+
+ @start()
+ def prepare_color(self):
+ self.state["color"] = "red"
+ return "bar"
+
+ @listen(prepare_color)
+ def write_first_sentence(self):
+ return f"{self.state['flower']} are {self.state['color']}"
+
+ @listen(write_first_sentence)
+ def finish_poem(self, first_sentence):
+ separator = self.state.get("separator", "\n")
+ return separator.join([first_sentence, "violets are blue"])
+
+ @listen(finish_poem)
+ def save_poem_to_database(self):
+ # A method without args/kwargs to ensure events are sent correctly
+ pass
+
+ event_log = []
+
+ def handle_event(_, event):
+ event_log.append(event)
+
+ flow = PoemFlow()
+ flow.event_emitter.connect(handle_event)
+ flow.kickoff(inputs={"separator": ", "})
+
+ assert isinstance(event_log[0], FlowStartedEvent)
+ assert event_log[0].flow_name == "PoemFlow"
+ assert event_log[0].inputs == {"separator": ", "}
+ assert isinstance(event_log[0].timestamp, datetime)
+
+ # Asserting for concurrent start method executions in a for loop as you
+ # can't guarantee ordering in asynchronous executions
+ for i in range(1, 5):
+ event = event_log[i]
+ assert isinstance(event.state, dict)
+ assert isinstance(event.state["id"], str)
+
+ if event.method_name == "prepare_flower":
+ if isinstance(event, MethodExecutionStartedEvent):
+ assert event.params == {}
+ assert event.state["separator"] == ", "
+ elif isinstance(event, MethodExecutionFinishedEvent):
+ assert event.result == "foo"
+ assert event.state["flower"] == "roses"
+ assert event.state["separator"] == ", "
+ else:
+ assert False, "Unexpected event type for prepare_flower"
+ elif event.method_name == "prepare_color":
+ if isinstance(event, MethodExecutionStartedEvent):
+ assert event.params == {}
+ assert event.state["separator"] == ", "
+ elif isinstance(event, MethodExecutionFinishedEvent):
+ assert event.result == "bar"
+ assert event.state["color"] == "red"
+ assert event.state["separator"] == ", "
+ else:
+ assert False, "Unexpected event type for prepare_color"
+ else:
+ assert False, f"Unexpected method {event.method_name} in prepare events"
+
+ assert isinstance(event_log[5], MethodExecutionStartedEvent)
+ assert event_log[5].method_name == "write_first_sentence"
+ assert event_log[5].params == {}
+ assert isinstance(event_log[5].state, dict)
+ assert event_log[5].state["flower"] == "roses"
+ assert event_log[5].state["color"] == "red"
+ assert event_log[5].state["separator"] == ", "
+
+ assert isinstance(event_log[6], MethodExecutionFinishedEvent)
+ assert event_log[6].method_name == "write_first_sentence"
+ assert event_log[6].result == "roses are red"
+
+ assert isinstance(event_log[7], MethodExecutionStartedEvent)
+ assert event_log[7].method_name == "finish_poem"
+ assert event_log[7].params == {"_0": "roses are red"}
+ assert isinstance(event_log[7].state, dict)
+ assert event_log[7].state["flower"] == "roses"
+ assert event_log[7].state["color"] == "red"
+
+ assert isinstance(event_log[8], MethodExecutionFinishedEvent)
+ assert event_log[8].method_name == "finish_poem"
+ assert event_log[8].result == "roses are red, violets are blue"
+
+ assert isinstance(event_log[9], MethodExecutionStartedEvent)
+ assert event_log[9].method_name == "save_poem_to_database"
+ assert event_log[9].params == {}
+ assert isinstance(event_log[9].state, dict)
+ assert event_log[9].state["flower"] == "roses"
+ assert event_log[9].state["color"] == "red"
+
+ assert isinstance(event_log[10], MethodExecutionFinishedEvent)
+ assert event_log[10].method_name == "save_poem_to_database"
+ assert event_log[10].result is None
+
+ assert isinstance(event_log[11], FlowFinishedEvent)
+ assert event_log[11].flow_name == "PoemFlow"
+ assert event_log[11].result is None
+ assert isinstance(event_log[11].timestamp, datetime)
+
+
+def test_structured_flow_event_emission():
+ """Test that the correct events are emitted during structured flow
+ execution with all fields validated."""
+
+ class OnboardingState(BaseModel):
+ name: str = ""
+ sent: bool = False
+
+ class OnboardingFlow(Flow[OnboardingState]):
+ @start()
+ def user_signs_up(self):
+ self.state.sent = False
+
+ @listen(user_signs_up)
+ def send_welcome_message(self):
+ self.state.sent = True
+ return f"Welcome, {self.state.name}!"
+
+ event_log = []
+
+ def handle_event(_, event):
+ event_log.append(event)
+
+ flow = OnboardingFlow()
+ flow.event_emitter.connect(handle_event)
+ flow.kickoff(inputs={"name": "Anakin"})
+
+ assert isinstance(event_log[0], FlowStartedEvent)
+ assert event_log[0].flow_name == "OnboardingFlow"
+ assert event_log[0].inputs == {"name": "Anakin"}
+ assert isinstance(event_log[0].timestamp, datetime)
+
+ assert isinstance(event_log[1], MethodExecutionStartedEvent)
+ assert event_log[1].method_name == "user_signs_up"
+
+ assert isinstance(event_log[2], MethodExecutionFinishedEvent)
+ assert event_log[2].method_name == "user_signs_up"
+
+ assert isinstance(event_log[3], MethodExecutionStartedEvent)
+ assert event_log[3].method_name == "send_welcome_message"
+ assert event_log[3].params == {}
+ assert getattr(event_log[3].state, "sent") is False
+
+ assert isinstance(event_log[4], MethodExecutionFinishedEvent)
+ assert event_log[4].method_name == "send_welcome_message"
+ assert getattr(event_log[4].state, "sent") is True
+ assert event_log[4].result == "Welcome, Anakin!"
+
+ assert isinstance(event_log[5], FlowFinishedEvent)
+ assert event_log[5].flow_name == "OnboardingFlow"
+ assert event_log[5].result == "Welcome, Anakin!"
+ assert isinstance(event_log[5].timestamp, datetime)
+
+
+def test_stateless_flow_event_emission():
+ """Test that the correct events are emitted stateless during flow execution
+ with all fields validated."""
+
+ class StatelessFlow(Flow):
+ @start()
+ def init(self):
+ pass
+
+ @listen(init)
+ def process(self):
+ return "Deeds will not be less valiant because they are unpraised."
+
+ event_log = []
+
+ def handle_event(_, event):
+ event_log.append(event)
+
+ flow = StatelessFlow()
+ flow.event_emitter.connect(handle_event)
+ flow.kickoff()
+
+ assert isinstance(event_log[0], FlowStartedEvent)
+ assert event_log[0].flow_name == "StatelessFlow"
+ assert event_log[0].inputs is None
+ assert isinstance(event_log[0].timestamp, datetime)
+
+ assert isinstance(event_log[1], MethodExecutionStartedEvent)
+ assert event_log[1].method_name == "init"
+
+ assert isinstance(event_log[2], MethodExecutionFinishedEvent)
+ assert event_log[2].method_name == "init"
+
+ assert isinstance(event_log[3], MethodExecutionStartedEvent)
+ assert event_log[3].method_name == "process"
+
+ assert isinstance(event_log[4], MethodExecutionFinishedEvent)
+ assert event_log[4].method_name == "process"
+
+ assert isinstance(event_log[5], FlowFinishedEvent)
+ assert event_log[5].flow_name == "StatelessFlow"
+ assert (
+ event_log[5].result
+ == "Deeds will not be less valiant because they are unpraised."
+ )
+ assert isinstance(event_log[5].timestamp, datetime)
diff --git a/tests/llm_test.py b/tests/llm_test.py
index 551309389..2e5faf774 100644
--- a/tests/llm_test.py
+++ b/tests/llm_test.py
@@ -1,9 +1,13 @@
+import os
from time import sleep
+from unittest.mock import MagicMock, patch
import pytest
+from pydantic import BaseModel
from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess
from crewai.llm import LLM
+from crewai.tools import tool
from crewai.utilities.token_counter_callback import TokenCalcHandler
@@ -37,3 +41,333 @@ def test_llm_callback_replacement():
assert usage_metrics_1.successful_requests == 1
assert usage_metrics_2.successful_requests == 1
assert usage_metrics_1 == calc_handler_1.token_cost_process.get_summary()
+
+
+@pytest.mark.vcr(filter_headers=["authorization"])
+def test_llm_call_with_string_input():
+ llm = LLM(model="gpt-4o-mini")
+
+ # Test the call method with a string input
+ result = llm.call("Return the name of a random city in the world.")
+ assert isinstance(result, str)
+ assert len(result.strip()) > 0 # Ensure the response is not empty
+
+
+@pytest.mark.vcr(filter_headers=["authorization"])
+def test_llm_call_with_string_input_and_callbacks():
+ llm = LLM(model="gpt-4o-mini")
+ calc_handler = TokenCalcHandler(token_cost_process=TokenProcess())
+
+ # Test the call method with a string input and callbacks
+ result = llm.call(
+ "Tell me a joke.",
+ callbacks=[calc_handler],
+ )
+ usage_metrics = calc_handler.token_cost_process.get_summary()
+
+ assert isinstance(result, str)
+ assert len(result.strip()) > 0
+ assert usage_metrics.successful_requests == 1
+
+
+@pytest.mark.vcr(filter_headers=["authorization"])
+def test_llm_call_with_message_list():
+ llm = LLM(model="gpt-4o-mini")
+ messages = [{"role": "user", "content": "What is the capital of France?"}]
+
+ # Test the call method with a list of messages
+ result = llm.call(messages)
+ assert isinstance(result, str)
+ assert "Paris" in result
+
+
+@pytest.mark.vcr(filter_headers=["authorization"])
+def test_llm_call_with_tool_and_string_input():
+ llm = LLM(model="gpt-4o-mini")
+
+ def get_current_year() -> str:
+ """Returns the current year as a string."""
+ from datetime import datetime
+
+ return str(datetime.now().year)
+
+ # Create tool schema
+ tool_schema = {
+ "type": "function",
+ "function": {
+ "name": "get_current_year",
+ "description": "Returns the current year as a string.",
+ "parameters": {
+ "type": "object",
+ "properties": {},
+ "required": [],
+ },
+ },
+ }
+
+ # Available functions mapping
+ available_functions = {"get_current_year": get_current_year}
+
+ # Test the call method with a string input and tool
+ result = llm.call(
+ "What is the current year?",
+ tools=[tool_schema],
+ available_functions=available_functions,
+ )
+
+ assert isinstance(result, str)
+ assert result == get_current_year()
+
+
+@pytest.mark.vcr(filter_headers=["authorization"])
+def test_llm_call_with_tool_and_message_list():
+ llm = LLM(model="gpt-4o-mini")
+
+ def square_number(number: int) -> int:
+ """Returns the square of a number."""
+ return number * number
+
+ # Create tool schema
+ tool_schema = {
+ "type": "function",
+ "function": {
+ "name": "square_number",
+ "description": "Returns the square of a number.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "number": {"type": "integer", "description": "The number to square"}
+ },
+ "required": ["number"],
+ },
+ },
+ }
+
+ # Available functions mapping
+ available_functions = {"square_number": square_number}
+
+ messages = [{"role": "user", "content": "What is the square of 5?"}]
+
+ # Test the call method with messages and tool
+ result = llm.call(
+ messages,
+ tools=[tool_schema],
+ available_functions=available_functions,
+ )
+
+ assert isinstance(result, int)
+ assert result == 25
+
+
+@pytest.mark.vcr(filter_headers=["authorization"])
+def test_llm_passes_additional_params():
+ llm = LLM(
+ model="gpt-4o-mini",
+ vertex_credentials="test_credentials",
+ vertex_project="test_project",
+ )
+
+ messages = [{"role": "user", "content": "Hello, world!"}]
+
+ with patch("litellm.completion") as mocked_completion:
+ # Create mocks for response structure
+ mock_message = MagicMock()
+ mock_message.content = "Test response"
+ mock_choice = MagicMock()
+ mock_choice.message = mock_message
+ mock_response = MagicMock()
+ mock_response.choices = [mock_choice]
+ mock_response.usage = {
+ "prompt_tokens": 5,
+ "completion_tokens": 5,
+ "total_tokens": 10,
+ }
+
+ # Set up the mocked completion to return the mock response
+ mocked_completion.return_value = mock_response
+
+ result = llm.call(messages)
+
+ # Assert that litellm.completion was called once
+ mocked_completion.assert_called_once()
+
+ # Retrieve the actual arguments with which litellm.completion was called
+ _, kwargs = mocked_completion.call_args
+
+ # Check that the additional_params were passed to litellm.completion
+ assert kwargs["vertex_credentials"] == "test_credentials"
+ assert kwargs["vertex_project"] == "test_project"
+
+ # Also verify that other expected parameters are present
+ assert kwargs["model"] == "gpt-4o-mini"
+ assert kwargs["messages"] == messages
+
+ # Check the result from llm.call
+ assert result == "Test response"
+
+
+def test_get_custom_llm_provider_openrouter():
+ llm = LLM(model="openrouter/deepseek/deepseek-chat")
+ assert llm._get_custom_llm_provider() == "openrouter"
+
+
+def test_get_custom_llm_provider_gemini():
+ llm = LLM(model="gemini/gemini-1.5-pro")
+ assert llm._get_custom_llm_provider() == "gemini"
+
+
+def test_get_custom_llm_provider_openai():
+ llm = LLM(model="gpt-4")
+ assert llm._get_custom_llm_provider() == "openai"
+
+
+def test_validate_call_params_supported():
+ class DummyResponse(BaseModel):
+ a: int
+
+ # Patch supports_response_schema to simulate a supported model.
+ with patch("crewai.llm.supports_response_schema", return_value=True):
+ llm = LLM(
+ model="openrouter/deepseek/deepseek-chat", response_format=DummyResponse
+ )
+ # Should not raise any error.
+ llm._validate_call_params()
+
+
+def test_validate_call_params_not_supported():
+ class DummyResponse(BaseModel):
+ a: int
+
+ # Patch supports_response_schema to simulate an unsupported model.
+ with patch("crewai.llm.supports_response_schema", return_value=False):
+ llm = LLM(model="gemini/gemini-1.5-pro", response_format=DummyResponse)
+ with pytest.raises(ValueError) as excinfo:
+ llm._validate_call_params()
+ assert "does not support response_format" in str(excinfo.value)
+
+
+def test_validate_call_params_no_response_format():
+ # When no response_format is provided, no validation error should occur.
+ llm = LLM(model="gemini/gemini-1.5-pro", response_format=None)
+ llm._validate_call_params()
+
+
+@pytest.mark.vcr(filter_headers=["authorization"])
+def test_o3_mini_reasoning_effort_high():
+ llm = LLM(
+ model="o3-mini",
+ reasoning_effort="high",
+ )
+ result = llm.call("What is the capital of France?")
+ assert isinstance(result, str)
+ assert "Paris" in result
+
+
+@pytest.mark.vcr(filter_headers=["authorization"])
+def test_o3_mini_reasoning_effort_low():
+ llm = LLM(
+ model="o3-mini",
+ reasoning_effort="low",
+ )
+ result = llm.call("What is the capital of France?")
+ assert isinstance(result, str)
+ assert "Paris" in result
+
+
+@pytest.mark.vcr(filter_headers=["authorization"])
+def test_o3_mini_reasoning_effort_medium():
+ llm = LLM(
+ model="o3-mini",
+ reasoning_effort="medium",
+ )
+ result = llm.call("What is the capital of France?")
+ assert isinstance(result, str)
+ assert "Paris" in result
+
+
+@pytest.mark.vcr(filter_headers=["authorization"])
+@pytest.fixture
+def anthropic_llm():
+ """Fixture providing an Anthropic LLM instance."""
+ return LLM(model="anthropic/claude-3-sonnet")
+
+@pytest.fixture
+def system_message():
+ """Fixture providing a system message."""
+ return {"role": "system", "content": "test"}
+
+@pytest.fixture
+def user_message():
+ """Fixture providing a user message."""
+ return {"role": "user", "content": "test"}
+
+def test_anthropic_message_formatting_edge_cases(anthropic_llm):
+ """Test edge cases for Anthropic message formatting."""
+ # Test None messages
+ with pytest.raises(TypeError, match="Messages cannot be None"):
+ anthropic_llm._format_messages_for_provider(None)
+
+ # Test empty message list
+ formatted = anthropic_llm._format_messages_for_provider([])
+ assert len(formatted) == 1
+ assert formatted[0]["role"] == "user"
+ assert formatted[0]["content"] == "."
+
+ # Test invalid message format
+ with pytest.raises(TypeError, match="Invalid message format"):
+ anthropic_llm._format_messages_for_provider([{"invalid": "message"}])
+
+def test_anthropic_model_detection():
+ """Test Anthropic model detection with various formats."""
+ models = [
+ ("anthropic/claude-3", True),
+ ("claude-instant", True),
+ ("claude/v1", True),
+ ("gpt-4", False),
+ ("", False),
+ ("anthropomorphic", False), # Should not match partial words
+ ]
+
+ for model, expected in models:
+ llm = LLM(model=model)
+ assert llm.is_anthropic == expected, f"Failed for model: {model}"
+
+def test_anthropic_message_formatting(anthropic_llm, system_message, user_message):
+ """Test Anthropic message formatting with fixtures."""
+ # Test when first message is system
+ formatted = anthropic_llm._format_messages_for_provider([system_message])
+ assert len(formatted) == 2
+ assert formatted[0]["role"] == "user"
+ assert formatted[0]["content"] == "."
+ assert formatted[1] == system_message
+
+ # Test when first message is already user
+ formatted = anthropic_llm._format_messages_for_provider([user_message])
+ assert len(formatted) == 1
+ assert formatted[0] == user_message
+
+ # Test with empty message list
+ formatted = anthropic_llm._format_messages_for_provider([])
+ assert len(formatted) == 1
+ assert formatted[0]["role"] == "user"
+ assert formatted[0]["content"] == "."
+
+ # Test with non-Anthropic model (should not modify messages)
+ non_anthropic_llm = LLM(model="gpt-4")
+ formatted = non_anthropic_llm._format_messages_for_provider([system_message])
+ assert len(formatted) == 1
+ assert formatted[0] == system_message
+
+
+def test_deepseek_r1_with_open_router():
+ if not os.getenv("OPEN_ROUTER_API_KEY"):
+ pytest.skip("OPEN_ROUTER_API_KEY not set; skipping test.")
+
+ llm = LLM(
+ model="openrouter/deepseek/deepseek-r1",
+ base_url="https://openrouter.ai/api/v1",
+ api_key=os.getenv("OPEN_ROUTER_API_KEY"),
+ )
+ result = llm.call("What is the capital of France?")
+ assert isinstance(result, str)
+ assert "Paris" in result
diff --git a/tests/task_test.py b/tests/task_test.py
index 59e58dcca..3cd11cfc7 100644
--- a/tests/task_test.py
+++ b/tests/task_test.py
@@ -723,14 +723,14 @@ def test_interpolate_inputs():
)
task.interpolate_inputs_and_add_conversation_history(
- inputs={"topic": "AI", "date": "2024"}
+ inputs={"topic": "AI", "date": "2025"}
)
assert (
task.description
== "Give me a list of 5 interesting ideas about AI to explore for an article, what makes them unique and interesting."
)
assert task.expected_output == "Bullet point list of 5 interesting ideas about AI."
- assert task.output_file == "/tmp/AI/output_2024.txt"
+ assert task.output_file == "/tmp/AI/output_2025.txt"
task.interpolate_inputs_and_add_conversation_history(
inputs={"topic": "ML", "date": "2025"}
@@ -779,6 +779,43 @@ def test_interpolate_only():
assert result == no_placeholders
+def test_interpolate_only_with_dict_inside_expected_output():
+ """Test the interpolate_only method for various scenarios including JSON structure preservation."""
+ task = Task(
+ description="Unused in this test",
+ expected_output="Unused in this test: {questions}",
+ )
+
+ json_string = '{"questions": {"main_question": "What is the user\'s name?", "secondary_question": "What is the user\'s age?"}}'
+ result = task.interpolate_only(
+ input_string=json_string,
+ inputs={
+ "questions": {
+ "main_question": "What is the user's name?",
+ "secondary_question": "What is the user's age?",
+ }
+ },
+ )
+ assert '"main_question": "What is the user\'s name?"' in result
+ assert '"secondary_question": "What is the user\'s age?"' in result
+ assert result == json_string
+
+ normal_string = "Hello {name}, welcome to {place}!"
+ result = task.interpolate_only(
+ input_string=normal_string, inputs={"name": "John", "place": "CrewAI"}
+ )
+ assert result == "Hello John, welcome to CrewAI!"
+
+ result = task.interpolate_only(input_string="", inputs={"unused": "value"})
+ assert result == ""
+
+ no_placeholders = "Hello, this is a test"
+ result = task.interpolate_only(
+ input_string=no_placeholders, inputs={"unused": "value"}
+ )
+ assert result == no_placeholders
+
+
def test_task_output_str_with_pydantic():
from crewai.tasks.output_format import OutputFormat
@@ -966,3 +1003,283 @@ def test_task_execution_times():
assert task.start_time is not None
assert task.end_time is not None
assert task.execution_duration == (task.end_time - task.start_time).total_seconds()
+
+
+def test_interpolate_with_list_of_strings():
+ task = Task(
+ description="Test list interpolation",
+ expected_output="List: {items}",
+ )
+
+ # Test simple list of strings
+ input_str = "Available items: {items}"
+ inputs = {"items": ["apple", "banana", "cherry"]}
+ result = task.interpolate_only(input_str, inputs)
+ assert result == f"Available items: {inputs['items']}"
+
+ # Test empty list
+ empty_list_input = {"items": []}
+ result = task.interpolate_only(input_str, empty_list_input)
+ assert result == "Available items: []"
+
+
+def test_interpolate_with_list_of_dicts():
+ task = Task(
+ description="Test list of dicts interpolation",
+ expected_output="People: {people}",
+ )
+
+ input_data = {
+ "people": [
+ {"name": "Alice", "age": 30, "skills": ["Python", "AI"]},
+ {"name": "Bob", "age": 25, "skills": ["Java", "Cloud"]},
+ ]
+ }
+ result = task.interpolate_only("{people}", input_data)
+
+ parsed_result = eval(result)
+ assert isinstance(parsed_result, list)
+ assert len(parsed_result) == 2
+ assert parsed_result[0]["name"] == "Alice"
+ assert parsed_result[0]["age"] == 30
+ assert parsed_result[0]["skills"] == ["Python", "AI"]
+ assert parsed_result[1]["name"] == "Bob"
+ assert parsed_result[1]["age"] == 25
+ assert parsed_result[1]["skills"] == ["Java", "Cloud"]
+
+
+def test_interpolate_with_nested_structures():
+ task = Task(
+ description="Test nested structures",
+ expected_output="Company: {company}",
+ )
+
+ input_data = {
+ "company": {
+ "name": "TechCorp",
+ "departments": [
+ {
+ "name": "Engineering",
+ "employees": 50,
+ "tools": ["Git", "Docker", "Kubernetes"],
+ },
+ {"name": "Sales", "employees": 20, "regions": {"north": 5, "south": 3}},
+ ],
+ }
+ }
+ result = task.interpolate_only("{company}", input_data)
+ parsed = eval(result)
+
+ assert parsed["name"] == "TechCorp"
+ assert len(parsed["departments"]) == 2
+ assert parsed["departments"][0]["tools"] == ["Git", "Docker", "Kubernetes"]
+ assert parsed["departments"][1]["regions"]["north"] == 5
+
+
+def test_interpolate_with_special_characters():
+ task = Task(
+ description="Test special characters in dicts",
+ expected_output="Data: {special_data}",
+ )
+
+ input_data = {
+ "special_data": {
+ "quotes": """This has "double" and 'single' quotes""",
+ "unicode": "ζεεγγγΉγ",
+ "symbols": "!@#$%^&*()",
+ "empty": "",
+ }
+ }
+ result = task.interpolate_only("{special_data}", input_data)
+ parsed = eval(result)
+
+ assert parsed["quotes"] == """This has "double" and 'single' quotes"""
+ assert parsed["unicode"] == "ζεεγγγΉγ"
+ assert parsed["symbols"] == "!@#$%^&*()"
+ assert parsed["empty"] == ""
+
+
+def test_interpolate_mixed_types():
+ task = Task(
+ description="Test mixed type interpolation",
+ expected_output="Mixed: {data}",
+ )
+
+ input_data = {
+ "data": {
+ "name": "Test Dataset",
+ "samples": 1000,
+ "features": ["age", "income", "location"],
+ "metadata": {
+ "source": "public",
+ "validated": True,
+ "tags": ["demo", "test", "temp"],
+ },
+ }
+ }
+ result = task.interpolate_only("{data}", input_data)
+ parsed = eval(result)
+
+ assert parsed["name"] == "Test Dataset"
+ assert parsed["samples"] == 1000
+ assert parsed["metadata"]["tags"] == ["demo", "test", "temp"]
+
+
+def test_interpolate_complex_combination():
+ task = Task(
+ description="Test complex combination",
+ expected_output="Report: {report}",
+ )
+
+ input_data = {
+ "report": [
+ {
+ "month": "January",
+ "metrics": {"sales": 15000, "expenses": 8000, "profit": 7000},
+ "top_products": ["Product A", "Product B"],
+ },
+ {
+ "month": "February",
+ "metrics": {"sales": 18000, "expenses": 8500, "profit": 9500},
+ "top_products": ["Product C", "Product D"],
+ },
+ ]
+ }
+ result = task.interpolate_only("{report}", input_data)
+ parsed = eval(result)
+
+ assert len(parsed) == 2
+ assert parsed[0]["month"] == "January"
+ assert parsed[1]["metrics"]["profit"] == 9500
+ assert "Product D" in parsed[1]["top_products"]
+
+
+def test_interpolate_invalid_type_validation():
+ task = Task(
+ description="Test invalid type validation",
+ expected_output="Should never reach here",
+ )
+
+ # Test with invalid top-level type
+ with pytest.raises(ValueError) as excinfo:
+ task.interpolate_only("{data}", {"data": set()}) # type: ignore we are purposely testing this failure
+
+ assert "Unsupported type set" in str(excinfo.value)
+
+ # Test with invalid nested type
+ invalid_nested = {
+ "profile": {
+ "name": "John",
+ "age": 30,
+ "tags": {"a", "b", "c"}, # Set is invalid
+ }
+ }
+ with pytest.raises(ValueError) as excinfo:
+ task.interpolate_only("{data}", {"data": invalid_nested})
+ assert "Unsupported type set" in str(excinfo.value)
+
+
+def test_interpolate_custom_object_validation():
+ task = Task(
+ description="Test custom object rejection",
+ expected_output="Should never reach here",
+ )
+
+ class CustomObject:
+ def __init__(self, value):
+ self.value = value
+
+ def __str__(self):
+ return str(self.value)
+
+ # Test with custom object at top level
+ with pytest.raises(ValueError) as excinfo:
+ task.interpolate_only("{obj}", {"obj": CustomObject(5)}) # type: ignore we are purposely testing this failure
+ assert "Unsupported type CustomObject" in str(excinfo.value)
+
+ # Test with nested custom object in dictionary
+ with pytest.raises(ValueError) as excinfo:
+ task.interpolate_only(
+ "{data}", {"data": {"valid": 1, "invalid": CustomObject(5)}}
+ )
+ assert "Unsupported type CustomObject" in str(excinfo.value)
+
+ # Test with nested custom object in list
+ with pytest.raises(ValueError) as excinfo:
+ task.interpolate_only("{data}", {"data": [1, "valid", CustomObject(5)]})
+ assert "Unsupported type CustomObject" in str(excinfo.value)
+
+ # Test with deeply nested custom object
+ with pytest.raises(ValueError) as excinfo:
+ task.interpolate_only(
+ "{data}", {"data": {"level1": {"level2": [{"level3": CustomObject(5)}]}}}
+ )
+ assert "Unsupported type CustomObject" in str(excinfo.value)
+
+
+def test_interpolate_valid_complex_types():
+ task = Task(
+ description="Test valid complex types",
+ expected_output="Validation should pass",
+ )
+
+ # Valid complex structure
+ valid_data = {
+ "name": "Valid Dataset",
+ "stats": {
+ "count": 1000,
+ "distribution": [0.2, 0.3, 0.5],
+ "features": ["age", "income"],
+ "nested": {"deep": [1, 2, 3], "deeper": {"a": 1, "b": 2.5}},
+ },
+ }
+
+ # Should not raise any errors
+ result = task.interpolate_only("{data}", {"data": valid_data})
+ parsed = eval(result)
+ assert parsed["name"] == "Valid Dataset"
+ assert parsed["stats"]["nested"]["deeper"]["b"] == 2.5
+
+
+def test_interpolate_edge_cases():
+ task = Task(
+ description="Test edge cases",
+ expected_output="Edge case handling",
+ )
+
+ # Test empty dict and list
+ assert task.interpolate_only("{}", {"data": {}}) == "{}"
+ assert task.interpolate_only("[]", {"data": []}) == "[]"
+
+ # Test numeric types
+ assert task.interpolate_only("{num}", {"num": 42}) == "42"
+ assert task.interpolate_only("{num}", {"num": 3.14}) == "3.14"
+
+ # Test boolean values (valid JSON types)
+ assert task.interpolate_only("{flag}", {"flag": True}) == "True"
+ assert task.interpolate_only("{flag}", {"flag": False}) == "False"
+
+
+def test_interpolate_valid_types():
+ task = Task(
+ description="Test valid types including null and boolean",
+ expected_output="Should pass validation",
+ )
+
+ # Test with boolean and null values (valid JSON types)
+ valid_data = {
+ "name": "Test",
+ "active": True,
+ "deleted": False,
+ "optional": None,
+ "nested": {"flag": True, "empty": None},
+ }
+
+ result = task.interpolate_only("{data}", {"data": valid_data})
+ parsed = eval(result)
+
+ assert parsed["active"] is True
+ assert parsed["deleted"] is False
+ assert parsed["optional"] is None
+ assert parsed["nested"]["flag"] is True
+ assert parsed["nested"]["empty"] is None
diff --git a/tests/test_flow_default_override.py b/tests/test_flow_default_override.py
new file mode 100644
index 000000000..f11b77982
--- /dev/null
+++ b/tests/test_flow_default_override.py
@@ -0,0 +1,112 @@
+"""Test that persisted state properly overrides default values."""
+
+from crewai.flow.flow import Flow, FlowState, listen, start
+from crewai.flow.persistence import persist
+
+
+class PoemState(FlowState):
+ """Test state model with default values that should be overridden."""
+ sentence_count: int = 1000 # Default that should be overridden
+ has_set_count: bool = False # Track whether we've set the count
+ poem_type: str = ""
+
+
+def test_default_value_override():
+ """Test that persisted state values override class defaults."""
+
+ @persist()
+ class PoemFlow(Flow[PoemState]):
+ initial_state = PoemState
+
+ @start()
+ def set_sentence_count(self):
+ if self.state.has_set_count and self.state.sentence_count == 2:
+ self.state.sentence_count = 3
+
+ elif self.state.has_set_count and self.state.sentence_count == 1000:
+ self.state.sentence_count = 1000
+
+ elif self.state.has_set_count and self.state.sentence_count == 5:
+ self.state.sentence_count = 5
+
+ else:
+ self.state.sentence_count = 2
+ self.state.has_set_count = True
+
+ # First run - should set sentence_count to 2
+ flow1 = PoemFlow()
+ flow1.kickoff()
+ original_uuid = flow1.state.id
+ assert flow1.state.sentence_count == 2
+
+ # Second run - should load sentence_count=2 instead of default 1000
+ flow2 = PoemFlow()
+ flow2.kickoff(inputs={"id": original_uuid})
+ assert flow2.state.sentence_count == 3 # Should load 2, not default 1000
+
+ # Fourth run - explicit override should work
+ flow3 = PoemFlow()
+ flow3.kickoff(inputs={
+ "id": original_uuid,
+ "has_set_count": True,
+ "sentence_count": 5, # Override persisted value
+ })
+ assert flow3.state.sentence_count == 5 # Should use override value
+
+ # Third run - should not load sentence_count=2 instead of default 1000
+ flow4 = PoemFlow()
+ flow4.kickoff(inputs={"has_set_count": True})
+ assert flow4.state.sentence_count == 1000 # Should load 1000, not 2
+
+
+def test_multi_step_default_override():
+ """Test default value override with multiple start methods."""
+
+ @persist()
+ class MultiStepPoemFlow(Flow[PoemState]):
+ initial_state = PoemState
+
+ @start()
+ def set_sentence_count(self):
+ print("Setting sentence count")
+ if not self.state.has_set_count:
+ self.state.sentence_count = 3
+ self.state.has_set_count = True
+
+ @listen(set_sentence_count)
+ def set_poem_type(self):
+ print("Setting poem type")
+ if self.state.sentence_count == 3:
+ self.state.poem_type = "haiku"
+ elif self.state.sentence_count == 5:
+ self.state.poem_type = "limerick"
+ else:
+ self.state.poem_type = "free_verse"
+
+ @listen(set_poem_type)
+ def finished(self):
+ print("finished")
+
+ # First run - should set both sentence count and poem type
+ flow1 = MultiStepPoemFlow()
+ flow1.kickoff()
+ original_uuid = flow1.state.id
+ assert flow1.state.sentence_count == 3
+ assert flow1.state.poem_type == "haiku"
+
+ # Second run - should load persisted state and update poem type
+ flow2 = MultiStepPoemFlow()
+ flow2.kickoff(inputs={
+ "id": original_uuid,
+ "sentence_count": 5
+ })
+ assert flow2.state.sentence_count == 5
+ assert flow2.state.poem_type == "limerick"
+
+ # Third run - new flow without persisted state should use defaults
+ flow3 = MultiStepPoemFlow()
+ flow3.kickoff(inputs={
+ "id": original_uuid
+ })
+ assert flow3.state.sentence_count == 5
+ assert flow3.state.poem_type == "limerick"
\ No newline at end of file
diff --git a/tests/test_flow_persistence.py b/tests/test_flow_persistence.py
index 74971f30d..e51806b05 100644
--- a/tests/test_flow_persistence.py
+++ b/tests/test_flow_persistence.py
@@ -1,13 +1,13 @@
"""Test flow state persistence functionality."""
import os
-from typing import Dict, Optional
+from typing import Dict
import pytest
from pydantic import BaseModel
-from crewai.flow.flow import Flow, FlowState, start
-from crewai.flow.persistence import FlowPersistence, persist
+from crewai.flow.flow import Flow, FlowState, listen, start
+from crewai.flow.persistence import persist
from crewai.flow.persistence.sqlite import SQLiteFlowPersistence
@@ -21,20 +21,20 @@ def test_persist_decorator_saves_state(tmp_path):
"""Test that @persist decorator saves state in SQLite."""
db_path = os.path.join(tmp_path, "test_flows.db")
persistence = SQLiteFlowPersistence(db_path)
-
+
class TestFlow(Flow[Dict[str, str]]):
initial_state = dict() # Use dict instance as initial state
-
+
@start()
@persist(persistence)
def init_step(self):
self.state["message"] = "Hello, World!"
self.state["id"] = "test-uuid" # Ensure we have an ID for persistence
-
+
# Run flow and verify state is saved
flow = TestFlow(persistence=persistence)
flow.kickoff()
-
+
# Load state from DB and verify
saved_state = persistence.load_state(flow.state["id"])
assert saved_state is not None
@@ -45,20 +45,20 @@ def test_structured_state_persistence(tmp_path):
"""Test persistence with Pydantic model state."""
db_path = os.path.join(tmp_path, "test_flows.db")
persistence = SQLiteFlowPersistence(db_path)
-
+
class StructuredFlow(Flow[TestState]):
initial_state = TestState
-
+
@start()
@persist(persistence)
def count_up(self):
self.state.counter += 1
self.state.message = f"Count is {self.state.counter}"
-
+
# Run flow and verify state changes are saved
flow = StructuredFlow(persistence=persistence)
flow.kickoff()
-
+
# Load and verify state
saved_state = persistence.load_state(flow.state.id)
assert saved_state is not None
@@ -70,126 +70,107 @@ def test_flow_state_restoration(tmp_path):
"""Test restoring flow state from persistence with various restoration methods."""
db_path = os.path.join(tmp_path, "test_flows.db")
persistence = SQLiteFlowPersistence(db_path)
-
+
# First flow execution to create initial state
class RestorableFlow(Flow[TestState]):
- initial_state = TestState
-
+
@start()
@persist(persistence)
def set_message(self):
- self.state.message = "Original message"
- self.state.counter = 42
-
+ if self.state.message == "":
+ self.state.message = "Original message"
+ if self.state.counter == 0:
+ self.state.counter = 42
+
# Create and persist initial state
flow1 = RestorableFlow(persistence=persistence)
flow1.kickoff()
original_uuid = flow1.state.id
-
+
# Test case 1: Restore using restore_uuid with field override
- flow2 = RestorableFlow(
- persistence=persistence,
- restore_uuid=original_uuid,
- counter=43, # Override counter
- )
-
+ flow2 = RestorableFlow(persistence=persistence)
+ flow2.kickoff(inputs={
+ "id": original_uuid,
+ "counter": 43
+ })
+
# Verify state restoration and selective field override
assert flow2.state.id == original_uuid
assert flow2.state.message == "Original message" # Preserved
assert flow2.state.counter == 43 # Overridden
-
+
# Test case 2: Restore using kwargs['id']
- flow3 = RestorableFlow(
- persistence=persistence,
- id=original_uuid,
- message="Updated message", # Override message
- )
-
+ flow3 = RestorableFlow(persistence=persistence)
+ flow3.kickoff(inputs={
+ "id": original_uuid,
+ "message": "Updated message"
+ })
+
# Verify state restoration and selective field override
assert flow3.state.id == original_uuid
- assert flow3.state.counter == 42 # Preserved
+ assert flow3.state.counter == 43 # Preserved
assert flow3.state.message == "Updated message" # Overridden
-
- # Test case 3: Verify error on conflicting IDs
- with pytest.raises(ValueError) as exc_info:
- RestorableFlow(
- persistence=persistence,
- restore_uuid=original_uuid,
- id="different-id", # Conflict with restore_uuid
- )
- assert "Conflicting IDs provided" in str(exc_info.value)
-
- # Test case 4: Verify error on non-existent restore_uuid
- with pytest.raises(ValueError) as exc_info:
- RestorableFlow(
- persistence=persistence,
- restore_uuid="non-existent-uuid",
- )
- assert "No state found" in str(exc_info.value)
-
- # Test case 5: Allow new state creation with kwargs['id']
- new_uuid = "new-flow-id"
- flow4 = RestorableFlow(
- persistence=persistence,
- id=new_uuid,
- message="New message",
- counter=100,
- )
-
- # Verify new state creation with provided ID
- assert flow4.state.id == new_uuid
- assert flow4.state.message == "New message"
- assert flow4.state.counter == 100
def test_multiple_method_persistence(tmp_path):
"""Test state persistence across multiple method executions."""
db_path = os.path.join(tmp_path, "test_flows.db")
persistence = SQLiteFlowPersistence(db_path)
-
+
class MultiStepFlow(Flow[TestState]):
- initial_state = TestState
-
@start()
@persist(persistence)
def step_1(self):
- self.state.counter = 1
- self.state.message = "Step 1"
-
- @start()
+ if self.state.counter == 1:
+ self.state.counter = 99999
+ self.state.message = "Step 99999"
+ else:
+ self.state.counter = 1
+ self.state.message = "Step 1"
+
+ @listen(step_1)
@persist(persistence)
def step_2(self):
- self.state.counter = 2
- self.state.message = "Step 2"
-
+ if self.state.counter == 1:
+ self.state.counter = 2
+ self.state.message = "Step 2"
+
flow = MultiStepFlow(persistence=persistence)
flow.kickoff()
-
+
+ flow2 = MultiStepFlow(persistence=persistence)
+ flow2.kickoff(inputs={"id": flow.state.id})
+
# Load final state
- final_state = persistence.load_state(flow.state.id)
+ final_state = flow2.state
assert final_state is not None
- assert final_state["counter"] == 2
- assert final_state["message"] == "Step 2"
+ assert final_state.counter == 2
+ assert final_state.message == "Step 2"
-
-def test_persistence_error_handling(tmp_path):
- """Test error handling in persistence operations."""
- db_path = os.path.join(tmp_path, "test_flows.db")
- persistence = SQLiteFlowPersistence(db_path)
-
- class InvalidFlow(Flow[TestState]):
- # Missing id field in initial state
- class InvalidState(BaseModel):
- value: str = ""
-
- initial_state = InvalidState
-
+ class NoPersistenceMultiStepFlow(Flow[TestState]):
@start()
@persist(persistence)
- def will_fail(self):
- self.state.value = "test"
-
- with pytest.raises(ValueError) as exc_info:
- flow = InvalidFlow(persistence=persistence)
-
- assert "must have an 'id' field" in str(exc_info.value)
+ def step_1(self):
+ if self.state.counter == 1:
+ self.state.counter = 99999
+ self.state.message = "Step 99999"
+ else:
+ self.state.counter = 1
+ self.state.message = "Step 1"
+
+ @listen(step_1)
+ def step_2(self):
+ if self.state.counter == 1:
+ self.state.counter = 2
+ self.state.message = "Step 2"
+
+ flow = NoPersistenceMultiStepFlow(persistence=persistence)
+ flow.kickoff()
+
+ flow2 = NoPersistenceMultiStepFlow(persistence=persistence)
+ flow2.kickoff(inputs={"id": flow.state.id})
+
+ # Load final state
+ final_state = flow2.state
+ assert final_state.counter == 99999
+ assert final_state.message == "Step 99999"
diff --git a/tests/test_manager_llm_delegation.py b/tests/test_manager_llm_delegation.py
deleted file mode 100644
index 0cab92d43..000000000
--- a/tests/test_manager_llm_delegation.py
+++ /dev/null
@@ -1,51 +0,0 @@
-import pytest
-
-from crewai import Agent
-from crewai.tools.agent_tools.base_agent_tools import BaseAgentTool
-
-
-class InternalAgentTool(BaseAgentTool):
- """Concrete implementation of BaseAgentTool for testing."""
-
- def _run(self, *args, **kwargs):
- """Implement required _run method."""
- return "Test response"
-
-
-@pytest.mark.parametrize(
- "role_name,should_match",
- [
- ("Futel Official Infopoint", True), # exact match
- (' "Futel Official Infopoint" ', True), # extra quotes and spaces
- ("Futel Official Infopoint\n", True), # trailing newline
- ('"Futel Official Infopoint"', True), # embedded quotes
- (" FUTEL\nOFFICIAL INFOPOINT ", True), # multiple whitespace and newline
- ],
-)
-@pytest.mark.vcr(filter_headers=["authorization"])
-def test_agent_tool_role_matching(role_name, should_match):
- """Test that agent tools can match roles regardless of case, whitespace, and special characters."""
- # Create test agent
- test_agent = Agent(
- role="Futel Official Infopoint",
- goal="Answer questions about Futel",
- backstory="Futel Football Club info",
- allow_delegation=False,
- )
-
- # Create test agent tool
- agent_tool = InternalAgentTool(
- name="test_tool", description="Test tool", agents=[test_agent]
- )
-
- # Test role matching
- result = agent_tool._execute(agent_name=role_name, task="Test task", context=None)
-
- if should_match:
- assert (
- "coworker mentioned not found" not in result.lower()
- ), f"Should find agent with role name: {role_name}"
- else:
- assert (
- "coworker mentioned not found" in result.lower()
- ), f"Should not find agent with role name: {role_name}"
diff --git a/tests/tools/test_tool_usage.py b/tests/tools/test_tool_usage.py
index 952011339..7b2ccd416 100644
--- a/tests/tools/test_tool_usage.py
+++ b/tests/tools/test_tool_usage.py
@@ -231,3 +231,255 @@ def test_validate_tool_input_with_special_characters():
arguments = tool_usage._validate_tool_input(tool_input)
assert arguments == expected_arguments
+
+
+def test_validate_tool_input_none_input():
+ tool_usage = ToolUsage(
+ tools_handler=MagicMock(),
+ tools=[],
+ original_tools=[],
+ tools_description="",
+ tools_names="",
+ task=MagicMock(),
+ function_calling_llm=None,
+ agent=MagicMock(),
+ action=MagicMock(),
+ )
+
+ arguments = tool_usage._validate_tool_input(None)
+ assert arguments == {}
+
+
+def test_validate_tool_input_valid_json():
+ tool_usage = ToolUsage(
+ tools_handler=MagicMock(),
+ tools=[],
+ original_tools=[],
+ tools_description="",
+ tools_names="",
+ task=MagicMock(),
+ function_calling_llm=None,
+ agent=MagicMock(),
+ action=MagicMock(),
+ )
+
+ tool_input = '{"key": "value", "number": 42, "flag": true}'
+ expected_arguments = {"key": "value", "number": 42, "flag": True}
+
+ arguments = tool_usage._validate_tool_input(tool_input)
+ assert arguments == expected_arguments
+
+
+def test_validate_tool_input_python_dict():
+ tool_usage = ToolUsage(
+ tools_handler=MagicMock(),
+ tools=[],
+ original_tools=[],
+ tools_description="",
+ tools_names="",
+ task=MagicMock(),
+ function_calling_llm=None,
+ agent=MagicMock(),
+ action=MagicMock(),
+ )
+
+ tool_input = "{'key': 'value', 'number': 42, 'flag': True}"
+ expected_arguments = {"key": "value", "number": 42, "flag": True}
+
+ arguments = tool_usage._validate_tool_input(tool_input)
+ assert arguments == expected_arguments
+
+
+def test_validate_tool_input_json5_unquoted_keys():
+ tool_usage = ToolUsage(
+ tools_handler=MagicMock(),
+ tools=[],
+ original_tools=[],
+ tools_description="",
+ tools_names="",
+ task=MagicMock(),
+ function_calling_llm=None,
+ agent=MagicMock(),
+ action=MagicMock(),
+ )
+
+ tool_input = "{key: 'value', number: 42, flag: true}"
+ expected_arguments = {"key": "value", "number": 42, "flag": True}
+
+ arguments = tool_usage._validate_tool_input(tool_input)
+ assert arguments == expected_arguments
+
+
+def test_validate_tool_input_with_trailing_commas():
+ tool_usage = ToolUsage(
+ tools_handler=MagicMock(),
+ tools=[],
+ original_tools=[],
+ tools_description="",
+ tools_names="",
+ task=MagicMock(),
+ function_calling_llm=None,
+ agent=MagicMock(),
+ action=MagicMock(),
+ )
+
+ tool_input = '{"key": "value", "number": 42, "flag": true,}'
+ expected_arguments = {"key": "value", "number": 42, "flag": True}
+
+ arguments = tool_usage._validate_tool_input(tool_input)
+ assert arguments == expected_arguments
+
+
+def test_validate_tool_input_invalid_input():
+ tool_usage = ToolUsage(
+ tools_handler=MagicMock(),
+ tools=[],
+ original_tools=[],
+ tools_description="",
+ tools_names="",
+ task=MagicMock(),
+ function_calling_llm=None,
+ agent=MagicMock(),
+ action=MagicMock(),
+ )
+
+ invalid_inputs = [
+ "Just a string",
+ "['list', 'of', 'values']",
+ "12345",
+ "",
+ ]
+
+ for invalid_input in invalid_inputs:
+ with pytest.raises(Exception) as e_info:
+ tool_usage._validate_tool_input(invalid_input)
+ assert (
+ "Tool input must be a valid dictionary in JSON or Python literal format"
+ in str(e_info.value)
+ )
+
+ # Test for None input separately
+ arguments = tool_usage._validate_tool_input(None)
+ assert arguments == {} # Expecting an empty dictionary
+
+
+def test_validate_tool_input_complex_structure():
+ tool_usage = ToolUsage(
+ tools_handler=MagicMock(),
+ tools=[],
+ original_tools=[],
+ tools_description="",
+ tools_names="",
+ task=MagicMock(),
+ function_calling_llm=None,
+ agent=MagicMock(),
+ action=MagicMock(),
+ )
+
+ tool_input = """
+ {
+ "user": {
+ "name": "Alice",
+ "age": 30
+ },
+ "items": [
+ {"id": 1, "value": "Item1"},
+ {"id": 2, "value": "Item2",}
+ ],
+ "active": true,
+ }
+ """
+ expected_arguments = {
+ "user": {"name": "Alice", "age": 30},
+ "items": [
+ {"id": 1, "value": "Item1"},
+ {"id": 2, "value": "Item2"},
+ ],
+ "active": True,
+ }
+
+ arguments = tool_usage._validate_tool_input(tool_input)
+ assert arguments == expected_arguments
+
+
+def test_validate_tool_input_code_content():
+ tool_usage = ToolUsage(
+ tools_handler=MagicMock(),
+ tools=[],
+ original_tools=[],
+ tools_description="",
+ tools_names="",
+ task=MagicMock(),
+ function_calling_llm=None,
+ agent=MagicMock(),
+ action=MagicMock(),
+ )
+
+ tool_input = '{"filename": "script.py", "content": "def hello():\\n print(\'Hello, world!\')"}'
+ expected_arguments = {
+ "filename": "script.py",
+ "content": "def hello():\n print('Hello, world!')",
+ }
+
+ arguments = tool_usage._validate_tool_input(tool_input)
+ assert arguments == expected_arguments
+
+
+def test_validate_tool_input_with_escaped_quotes():
+ tool_usage = ToolUsage(
+ tools_handler=MagicMock(),
+ tools=[],
+ original_tools=[],
+ tools_description="",
+ tools_names="",
+ task=MagicMock(),
+ function_calling_llm=None,
+ agent=MagicMock(),
+ action=MagicMock(),
+ )
+
+ tool_input = '{"text": "He said, \\"Hello, world!\\""}'
+ expected_arguments = {"text": 'He said, "Hello, world!"'}
+
+ arguments = tool_usage._validate_tool_input(tool_input)
+ assert arguments == expected_arguments
+
+
+def test_validate_tool_input_large_json_content():
+ tool_usage = ToolUsage(
+ tools_handler=MagicMock(),
+ tools=[],
+ original_tools=[],
+ tools_description="",
+ tools_names="",
+ task=MagicMock(),
+ function_calling_llm=None,
+ agent=MagicMock(),
+ action=MagicMock(),
+ )
+
+ # Simulate a large JSON content
+ tool_input = (
+ '{"data": ' + json.dumps([{"id": i, "value": i * 2} for i in range(1000)]) + "}"
+ )
+ expected_arguments = {"data": [{"id": i, "value": i * 2} for i in range(1000)]}
+
+ arguments = tool_usage._validate_tool_input(tool_input)
+ assert arguments == expected_arguments
+
+
+def test_validate_tool_input_none_input():
+ tool_usage = ToolUsage(
+ tools_handler=MagicMock(),
+ tools=[],
+ original_tools=[],
+ tools_description="",
+ tools_names="",
+ task=MagicMock(),
+ function_calling_llm=None,
+ agent=MagicMock(),
+ action=MagicMock(),
+ )
+
+ arguments = tool_usage._validate_tool_input(None)
+ assert arguments == {} # Expecting an empty dictionary
diff --git a/tests/utilities/evaluators/test_task_evaluator.py b/tests/utilities/evaluators/test_task_evaluator.py
index 8a0be027a..e4de1db62 100644
--- a/tests/utilities/evaluators/test_task_evaluator.py
+++ b/tests/utilities/evaluators/test_task_evaluator.py
@@ -48,9 +48,9 @@ def test_evaluate_training_data(converter_mock):
mock.call(
llm=original_agent.llm,
text="Assess the quality of the training data based on the llm output, human feedback , and llm "
- "output improved result.\n\nInitial Output:\nInitial output 1\n\nHuman Feedback:\nHuman feedback "
- "1\n\nImproved Output:\nImproved output 1\n\nInitial Output:\nInitial output 2\n\nHuman "
- "Feedback:\nHuman feedback 2\n\nImproved Output:\nImproved output 2\n\nPlease provide:\n- Provide "
+ "output improved result.\n\nIteration: data1\nInitial Output:\nInitial output 1\n\nHuman Feedback:\nHuman feedback "
+ "1\n\nImproved Output:\nImproved output 1\n\n------------------------------------------------\n\nIteration: data2\nInitial Output:\nInitial output 2\n\nHuman "
+ "Feedback:\nHuman feedback 2\n\nImproved Output:\nImproved output 2\n\n------------------------------------------------\n\nPlease provide:\n- Provide "
"a list of clear, actionable instructions derived from the Human Feedbacks to enhance the Agent's "
"performance. Analyze the differences between Initial Outputs and Improved Outputs to generate specific "
"action items for future tasks. Ensure all key and specificpoints from the human feedback are "
diff --git a/uv.lock b/uv.lock
index 11f2e6691..d02181f54 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,7 +1,6 @@
version = 1
requires-python = ">=3.10, <3.13"
resolution-markers = [
-
"python_full_version < '3.11' and platform_system == 'Darwin' and sys_platform == 'darwin'",
"python_full_version < '3.11' and platform_machine == 'aarch64' and platform_system == 'Linux' and sys_platform == 'darwin'",
"(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_system != 'Darwin' and sys_platform == 'darwin') or (python_full_version < '3.11' and platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'darwin')",
@@ -37,7 +36,7 @@ resolution-markers = [
"python_full_version >= '3.12.4' and platform_machine == 'aarch64' and platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'linux'",
"(python_full_version >= '3.12.4' and platform_machine != 'aarch64' and platform_system == 'Darwin' and sys_platform != 'darwin') or (python_full_version >= '3.12.4' and platform_system == 'Darwin' and sys_platform != 'darwin' and sys_platform != 'linux')",
"python_full_version >= '3.12.4' and platform_machine == 'aarch64' and platform_system == 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux'",
- "(python_full_version >= '3.12.4' and platform_machine != 'aarch64' and platform_system != 'Darwin' and sys_platform != 'darwin') or (python_full_version >= '3.12.4' and platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux')"
+ "(python_full_version >= '3.12.4' and platform_machine != 'aarch64' and platform_system != 'Darwin' and sys_platform != 'darwin') or (python_full_version >= '3.12.4' and platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux')",
]
[[package]]
@@ -245,18 +244,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/0e/38cb7b781371e79e9c697fb78f3ccd18fda8bd547d0a2e76e616561a3792/auth0_python-4.7.2-py3-none-any.whl", hash = "sha256:df2224f9b1e170b3aa12d8bc7ff02eadb7cc229307a09ec6b8a55fd1e0e05dc8", size = 131834 },
]
-[[package]]
-name = "authlib"
-version = "1.3.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cryptography" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/09/47/df70ecd34fbf86d69833fe4e25bb9ecbaab995c8e49df726dd416f6bb822/authlib-1.3.1.tar.gz", hash = "sha256:7ae843f03c06c5c0debd63c9db91f9fda64fa62a42a77419fa15fbb7e7a58917", size = 146074 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/87/1f/bc95e43ffb57c05b8efcc376dd55a0240bf58f47ddf5a0f92452b6457b75/Authlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:d35800b973099bbadc49b42b256ecb80041ad56b7fe1216a362c7943c088f377", size = 223827 },
-]
-
[[package]]
name = "autoflake"
version = "2.3.1"
@@ -346,7 +333,7 @@ name = "build"
version = "1.2.2.post1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "colorama", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" },
+ { name = "colorama", marker = "os_name == 'nt'" },
{ name = "importlib-metadata", marker = "python_full_version < '3.10.2'" },
{ name = "packaging" },
{ name = "pyproject-hooks" },
@@ -578,14 +565,14 @@ wheels = [
[[package]]
name = "click"
-version = "8.1.7"
+version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "colorama", marker = "platform_system == 'Windows'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
+sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 },
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
]
[[package]]
@@ -632,7 +619,7 @@ wheels = [
[[package]]
name = "crewai"
-version = "0.95.0"
+version = "0.102.0"
source = { editable = "." }
dependencies = [
{ name = "appdirs" },
@@ -642,6 +629,7 @@ dependencies = [
{ name = "click" },
{ name = "instructor" },
{ name = "json-repair" },
+ { name = "json5" },
{ name = "jsonref" },
{ name = "litellm" },
{ name = "openai" },
@@ -715,13 +703,14 @@ requires-dist = [
{ name = "blinker", specifier = ">=1.9.0" },
{ name = "chromadb", specifier = ">=0.5.23" },
{ name = "click", specifier = ">=8.1.7" },
- { name = "crewai-tools", marker = "extra == 'tools'", specifier = ">=0.25.5" },
+ { name = "crewai-tools", marker = "extra == 'tools'", specifier = ">=0.36.0" },
{ name = "docling", marker = "extra == 'docling'", specifier = ">=2.12.0" },
{ name = "fastembed", marker = "extra == 'fastembed'", specifier = ">=0.4.1" },
{ name = "instructor", specifier = ">=1.3.3" },
{ name = "json-repair", specifier = ">=0.25.2" },
+ { name = "json5", specifier = ">=0.10.0" },
{ name = "jsonref", specifier = ">=1.1.0" },
- { name = "litellm", specifier = "==1.57.4" },
+ { name = "litellm", specifier = "==1.60.2" },
{ name = "mem0ai", marker = "extra == 'mem0'", specifier = ">=0.1.29" },
{ name = "openai", specifier = ">=1.13.3" },
{ name = "openpyxl", specifier = ">=3.1.5" },
@@ -763,32 +752,24 @@ dev = [
[[package]]
name = "crewai-tools"
-version = "0.25.6"
+version = "0.36.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "beautifulsoup4" },
{ name = "chromadb" },
+ { name = "click" },
{ name = "crewai" },
{ name = "docker" },
- { name = "docx2txt" },
{ name = "embedchain" },
{ name = "lancedb" },
- { name = "linkup-sdk" },
{ name = "openai" },
{ name = "pydantic" },
{ name = "pyright" },
- { name = "pytest" },
{ name = "pytube" },
{ name = "requests" },
- { name = "scrapegraph-py" },
- { name = "selenium" },
- { name = "serpapi" },
- { name = "spider-client" },
- { name = "weaviate-client" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/23/2f/fbfd0dc8912d375a2d1272c503f79c83c25f3d2b4b72c230b0672278a1bd/crewai_tools-0.25.6.tar.gz", hash = "sha256:442a7e7e579cb3c671a53c5b7afce645cd31d2db913ecc6d1e22a4c5e1baa840", size = 883175 }
+sdist = { url = "https://files.pythonhosted.org/packages/4d/e1/d65778cf4aea106f3f60a4208521f04bc7f1d26be4e34eeb63cae6297d50/crewai_tools-0.36.0.tar.gz", hash = "sha256:761b396ee6a4019a988720dd6a14e1409f5de9d0cdc2a8662b487d87efb1a6bf", size = 900178 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ce/21/561a81b4f8cfcc2ac6a0c3db3ec86b70a7db6dabb0dd7d13c96be981b2fc/crewai_tools-0.25.6-py3-none-any.whl", hash = "sha256:463e0ee8d780ab7a801992e3960471fb8e64d038866429f70995ddd0a83e0679", size = 514758 },
+ { url = "https://files.pythonhosted.org/packages/bd/b6/533632a6c2a2e623fc4a1677458aff3539413a196fb220a7fece4ead3f71/crewai_tools-0.36.0-py3-none-any.whl", hash = "sha256:dbd0d95a080acfb281e105f4376e1e98576dae6d53d94f7b883c57af893668b3", size = 545937 },
]
[[package]]
@@ -1079,12 +1060,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533 },
]
-[[package]]
-name = "docx2txt"
-version = "0.8"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7d/7d/60ee3f2b16d9bfdfa72e8599470a2c1a5b759cb113c6fe1006be28359327/docx2txt-0.8.tar.gz", hash = "sha256:2c06d98d7cfe2d3947e5760a57d924e3ff07745b379c8737723922e7009236e5", size = 2814 }
-
[[package]]
name = "durationpy"
version = "0.9"
@@ -1626,19 +1601,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/1f/acf03ee901313446d52c3916d527d4981de9f6f3edc69267d05509dcfa7b/grpcio-1.67.0-cp312-cp312-win_amd64.whl", hash = "sha256:985b2686f786f3e20326c4367eebdaed3e7aa65848260ff0c6644f817042cb15", size = 4343545 },
]
-[[package]]
-name = "grpcio-health-checking"
-version = "1.62.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "grpcio" },
- { name = "protobuf" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/eb/9f/09df9b02fc8eafa3031d878c8a4674a0311293c8c6f1c942cdaeec204126/grpcio-health-checking-1.62.3.tar.gz", hash = "sha256:5074ba0ce8f0dcfe328408ec5c7551b2a835720ffd9b69dade7fa3e0dc1c7a93", size = 15640 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/40/4c/ee3173906196b741ac6ba55a9788ba9ebf2cd05f91715a49b6c3bfbb9d73/grpcio_health_checking-1.62.3-py3-none-any.whl", hash = "sha256:f29da7dd144d73b4465fe48f011a91453e9ff6c8af0d449254cf80021cab3e0d", size = 18547 },
-]
-
[[package]]
name = "grpcio-status"
version = "1.62.3"
@@ -1850,52 +1812,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
-[[package]]
-name = "ijson"
-version = "3.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6c/83/28e9e93a3a61913e334e3a2e78ea9924bb9f9b1ac45898977f9d9dd6133f/ijson-3.3.0.tar.gz", hash = "sha256:7f172e6ba1bee0d4c8f8ebd639577bfe429dee0f3f96775a067b8bae4492d8a0", size = 60079 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ad/89/96e3608499b4a500b9bc27aa8242704e675849dd65bdfa8682b00a92477e/ijson-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7f7a5250599c366369fbf3bc4e176f5daa28eb6bc7d6130d02462ed335361675", size = 85009 },
- { url = "https://files.pythonhosted.org/packages/e4/7e/1098503500f5316c5f7912a51c91aca5cbc609c09ce4ecd9c4809983c560/ijson-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f87a7e52f79059f9c58f6886c262061065eb6f7554a587be7ed3aa63e6b71b34", size = 57796 },
- { url = "https://files.pythonhosted.org/packages/78/f7/27b8c27a285628719ff55b68507581c86b551eb162ce810fe51e3e1a25f2/ijson-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b73b493af9e947caed75d329676b1b801d673b17481962823a3e55fe529c8b8b", size = 57218 },
- { url = "https://files.pythonhosted.org/packages/0c/c5/1698094cb6a336a223c30e1167cc1b15cdb4bfa75399c1a2eb82fa76cc3c/ijson-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5576415f3d76290b160aa093ff968f8bf6de7d681e16e463a0134106b506f49", size = 117153 },
- { url = "https://files.pythonhosted.org/packages/4b/21/c206dda0945bd832cc9b0894596b0efc2cb1819a0ac61d8be1429ac09494/ijson-3.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e9ffe358d5fdd6b878a8a364e96e15ca7ca57b92a48f588378cef315a8b019e", size = 110781 },
- { url = "https://files.pythonhosted.org/packages/f4/f5/2d733e64577109a9b255d14d031e44a801fa20df9ccc58b54a31e8ecf9e6/ijson-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8643c255a25824ddd0895c59f2319c019e13e949dc37162f876c41a283361527", size = 114527 },
- { url = "https://files.pythonhosted.org/packages/8d/a8/78bfee312aa23417b86189a65f30b0edbceaee96dc6a616cc15f611187d1/ijson-3.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df3ab5e078cab19f7eaeef1d5f063103e1ebf8c26d059767b26a6a0ad8b250a3", size = 116824 },
- { url = "https://files.pythonhosted.org/packages/5d/a4/aff410f7d6aa1a77ee2ab2d6a2d2758422726270cb149c908a9baf33cf58/ijson-3.3.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3dc1fb02c6ed0bae1b4bf96971258bf88aea72051b6e4cebae97cff7090c0607", size = 112647 },
- { url = "https://files.pythonhosted.org/packages/77/ee/2b5122dc4713f5a954267147da36e7156240ca21b04ed5295bc0cabf0fbe/ijson-3.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e9afd97339fc5a20f0542c971f90f3ca97e73d3050cdc488d540b63fae45329a", size = 114156 },
- { url = "https://files.pythonhosted.org/packages/b3/d7/ad3b266490b60c6939e8a07fd8e4b7e2002aea08eaa9572a016c3e3a9129/ijson-3.3.0-cp310-cp310-win32.whl", hash = "sha256:844c0d1c04c40fd1b60f148dc829d3f69b2de789d0ba239c35136efe9a386529", size = 48931 },
- { url = "https://files.pythonhosted.org/packages/0b/68/b9e1c743274c8a23dddb12d2ed13b5f021f6d21669d51ff7fa2e9e6c19df/ijson-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:d654d045adafdcc6c100e8e911508a2eedbd2a1b5f93f930ba13ea67d7704ee9", size = 50965 },
- { url = "https://files.pythonhosted.org/packages/fd/df/565ba72a6f4b2c833d051af8e2228cfa0b1fef17bb44995c00ad27470c52/ijson-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:501dce8eaa537e728aa35810656aa00460a2547dcb60937c8139f36ec344d7fc", size = 85041 },
- { url = "https://files.pythonhosted.org/packages/f0/42/1361eaa57ece921d0239881bae6a5e102333be5b6e0102a05ec3caadbd5a/ijson-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:658ba9cad0374d37b38c9893f4864f284cdcc7d32041f9808fba8c7bcaadf134", size = 57829 },
- { url = "https://files.pythonhosted.org/packages/f5/b0/143dbfe12e1d1303ea8d8cd6f40e95cea8f03bcad5b79708614a7856c22e/ijson-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2636cb8c0f1023ef16173f4b9a233bcdb1df11c400c603d5f299fac143ca8d70", size = 57217 },
- { url = "https://files.pythonhosted.org/packages/0d/80/b3b60c5e5be2839365b03b915718ca462c544fdc71e7a79b7262837995ef/ijson-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd174b90db68c3bcca273e9391934a25d76929d727dc75224bf244446b28b03b", size = 121878 },
- { url = "https://files.pythonhosted.org/packages/8d/eb/7560fafa4d40412efddf690cb65a9bf2d3429d6035e544103acbf5561dc4/ijson-3.3.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97a9aea46e2a8371c4cf5386d881de833ed782901ac9f67ebcb63bb3b7d115af", size = 115620 },
- { url = "https://files.pythonhosted.org/packages/51/2b/5a34c7841388dce161966e5286931518de832067cd83e6f003d93271e324/ijson-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c594c0abe69d9d6099f4ece17763d53072f65ba60b372d8ba6de8695ce6ee39e", size = 119200 },
- { url = "https://files.pythonhosted.org/packages/3e/b7/1d64fbec0d0a7b0c02e9ad988a89614532028ead8bb52a2456c92e6ee35a/ijson-3.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8e0ff16c224d9bfe4e9e6bd0395826096cda4a3ef51e6c301e1b61007ee2bd24", size = 121107 },
- { url = "https://files.pythonhosted.org/packages/d4/b9/01044f09850bc545ffc85b35aaec473d4f4ca2b6667299033d252c1b60dd/ijson-3.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0015354011303175eae7e2ef5136414e91de2298e5a2e9580ed100b728c07e51", size = 116658 },
- { url = "https://files.pythonhosted.org/packages/fb/0d/53856b61f3d952d299d1695c487e8e28058d01fa2adfba3d6d4b4660c242/ijson-3.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034642558afa57351a0ffe6de89e63907c4cf6849070cc10a3b2542dccda1afe", size = 118186 },
- { url = "https://files.pythonhosted.org/packages/95/2d/5bd86e2307dd594840ee51c4e32de953fee837f028acf0f6afb08914cd06/ijson-3.3.0-cp311-cp311-win32.whl", hash = "sha256:192e4b65495978b0bce0c78e859d14772e841724d3269fc1667dc6d2f53cc0ea", size = 48938 },
- { url = "https://files.pythonhosted.org/packages/55/e1/4ba2b65b87f67fb19d698984d92635e46d9ce9dd748ce7d009441a586710/ijson-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:72e3488453754bdb45c878e31ce557ea87e1eb0f8b4fc610373da35e8074ce42", size = 50972 },
- { url = "https://files.pythonhosted.org/packages/8a/4d/3992f7383e26a950e02dc704bc6c5786a080d5c25fe0fc5543ef477c1883/ijson-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:988e959f2f3d59ebd9c2962ae71b97c0df58323910d0b368cc190ad07429d1bb", size = 84550 },
- { url = "https://files.pythonhosted.org/packages/1b/cc/3d4372e0d0b02a821b982f1fdf10385512dae9b9443c1597719dd37769a9/ijson-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b2f73f0d0fce5300f23a1383d19b44d103bb113b57a69c36fd95b7c03099b181", size = 57572 },
- { url = "https://files.pythonhosted.org/packages/02/de/970d48b1ff9da5d9513c86fdd2acef5cb3415541c8069e0d92a151b84adb/ijson-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0ee57a28c6bf523d7cb0513096e4eb4dac16cd935695049de7608ec110c2b751", size = 56902 },
- { url = "https://files.pythonhosted.org/packages/5e/a0/4537722c8b3b05e82c23dfe09a3a64dd1e44a013a5ca58b1e77dfe48b2f1/ijson-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0155a8f079c688c2ccaea05de1ad69877995c547ba3d3612c1c336edc12a3a5", size = 127400 },
- { url = "https://files.pythonhosted.org/packages/b2/96/54956062a99cf49f7a7064b573dcd756da0563ce57910dc34e27a473d9b9/ijson-3.3.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ab00721304af1ae1afa4313ecfa1bf16b07f55ef91e4a5b93aeaa3e2bd7917c", size = 118786 },
- { url = "https://files.pythonhosted.org/packages/07/74/795319531c5b5504508f595e631d592957f24bed7ff51a15bc4c61e7b24c/ijson-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40ee3821ee90be0f0e95dcf9862d786a7439bd1113e370736bfdf197e9765bfb", size = 126288 },
- { url = "https://files.pythonhosted.org/packages/69/6a/e0cec06fbd98851d5d233b59058c1dc2ea767c9bb6feca41aa9164fff769/ijson-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3b6987a0bc3e6d0f721b42c7a0198ef897ae50579547b0345f7f02486898f5", size = 129569 },
- { url = "https://files.pythonhosted.org/packages/2a/4f/82c0d896d8dcb175f99ced7d87705057bcd13523998b48a629b90139a0dc/ijson-3.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:63afea5f2d50d931feb20dcc50954e23cef4127606cc0ecf7a27128ed9f9a9e6", size = 121508 },
- { url = "https://files.pythonhosted.org/packages/2b/b6/8973474eba4a917885e289d9e138267d3d1f052c2d93b8c968755661a42d/ijson-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5c3e285e0735fd8c5a26d177eca8b52512cdd8687ca86ec77a0c66e9c510182", size = 127896 },
- { url = "https://files.pythonhosted.org/packages/94/25/00e66af887adbbe70002e0479c3c2340bdfa17a168e25d4ab5a27b53582d/ijson-3.3.0-cp312-cp312-win32.whl", hash = "sha256:907f3a8674e489abdcb0206723e5560a5cb1fa42470dcc637942d7b10f28b695", size = 49272 },
- { url = "https://files.pythonhosted.org/packages/25/a2/e187beee237808b2c417109ae0f4f7ee7c81ecbe9706305d6ac2a509cc45/ijson-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f890d04ad33262d0c77ead53c85f13abfb82f2c8f078dfbf24b78f59534dfdd", size = 51272 },
- { url = "https://files.pythonhosted.org/packages/c3/28/2e1cf00abe5d97aef074e7835b86a94c9a06be4629a0e2c12600792b51ba/ijson-3.3.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2af323a8aec8a50fa9effa6d640691a30a9f8c4925bd5364a1ca97f1ac6b9b5c", size = 54308 },
- { url = "https://files.pythonhosted.org/packages/04/d2/8c541c28da4f931bac8177e251efe2b6902f7c486d2d4bdd669eed4ff5c0/ijson-3.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f64f01795119880023ba3ce43072283a393f0b90f52b66cc0ea1a89aa64a9ccb", size = 66010 },
- { url = "https://files.pythonhosted.org/packages/d0/02/8fec0b9037a368811dba7901035e8e0973ebda308f57f30c42101a16a5f7/ijson-3.3.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a716e05547a39b788deaf22725490855337fc36613288aa8ae1601dc8c525553", size = 66770 },
- { url = "https://files.pythonhosted.org/packages/47/23/90c61f978c83647112460047ea0137bde9c7fe26600ce255bb3e17ea7a21/ijson-3.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:473f5d921fadc135d1ad698e2697025045cd8ed7e5e842258295012d8a3bc702", size = 64159 },
- { url = "https://files.pythonhosted.org/packages/20/af/aab1a36072590af62d848f03981f1c587ca40a391fc61e418e388d8b0d46/ijson-3.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd26b396bc3a1e85f4acebeadbf627fa6117b97f4c10b177d5779577c6607744", size = 51095 },
-]
-
[[package]]
name = "imageio"
version = "2.36.1"
@@ -2059,6 +1975,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/23/38/34cb843cee4c5c27aa5c822e90e99bf96feb3dfa705713b5b6e601d17f5c/json_repair-0.30.0-py3-none-any.whl", hash = "sha256:bda4a5552dc12085c6363ff5acfcdb0c9cafc629989a2112081b7e205828228d", size = 17641 },
]
+[[package]]
+name = "json5"
+version = "0.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/85/3d/bbe62f3d0c05a689c711cff57b2e3ac3d3e526380adb7c781989f075115c/json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559", size = 48202 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/aa/42/797895b952b682c3dafe23b1834507ee7f02f4d6299b65aaa61425763278/json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa", size = 34049 },
+]
+
[[package]]
name = "jsonlines"
version = "3.1.0"
@@ -2330,22 +2255,9 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097 },
]
-[[package]]
-name = "linkup-sdk"
-version = "0.2.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "httpx" },
- { name = "pydantic" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/2e/ba/b06e8f2ca2f0ce255a40ee4505637536acfe83ec997cd8b61bd5cd031513/linkup_sdk-0.2.1.tar.gz", hash = "sha256:b00ba7cb0117358e975d50196501ac49b247509fd236121e40abe40e6a2a3e9a", size = 8918 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4f/90/2903b9e2eba501ceb6c6b4fc57bbeddde7e8964921a05d424f5a6125cbd0/linkup_sdk-0.2.1-py3-none-any.whl", hash = "sha256:bf50c88e659c6d9291cbd5e3e99b6a20a14c9b1eb2dc7acca763a3ae6f84b26e", size = 7961 },
-]
-
[[package]]
name = "litellm"
-version = "1.57.4"
+version = "1.60.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -2360,9 +2272,9 @@ dependencies = [
{ name = "tiktoken" },
{ name = "tokenizers" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/1a/9a/115bde058901b087e7fec1bed4be47baf8d5c78aff7dd2ffebcb922003ff/litellm-1.57.4.tar.gz", hash = "sha256:747a870ddee9c71f9560fc68ad02485bc1008fcad7d7a43e87867a59b8ed0669", size = 6304427 }
+sdist = { url = "https://files.pythonhosted.org/packages/94/8f/704cdb0fdbdd49dc5062a39ae5f1a8f308ae0ffd746df6e0137fc1776b8a/litellm-1.60.2.tar.gz", hash = "sha256:a8170584fcfd6f5175201d869e61ccd8a40ffe3264fc5e53c5b805ddf8a6e05a", size = 6447447 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9f/72/35c8509cb2a37343c213b794420405cbef2e1fdf8626ee981fcbba3d7c5c/litellm-1.57.4-py3-none-any.whl", hash = "sha256:afe48924d8a36db801018970a101622fce33d117fe9c54441c0095c491511abb", size = 6592126 },
+ { url = "https://files.pythonhosted.org/packages/8a/ba/0eaec9aee9f99fdf46ef1c0bddcfe7f5720b182f84f6ed27f13145d5ded2/litellm-1.60.2-py3-none-any.whl", hash = "sha256:1cb08cda04bf8c5ef3e690171a779979e4b16a5e3a24cd8dc1f198e7f198d5c4", size = 6746809 },
]
[[package]]
@@ -2588,7 +2500,7 @@ version = "1.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
- { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "colorama", marker = "platform_system == 'Windows'" },
{ name = "ghp-import" },
{ name = "jinja2" },
{ name = "markdown" },
@@ -2769,7 +2681,7 @@ version = "2.10.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pygments" },
- { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "pywin32", marker = "platform_system == 'Windows'" },
{ name = "tqdm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3a/93/80ac75c20ce54c785648b4ed363c88f148bf22637e10c9863db4fbe73e74/mpire-2.10.2.tar.gz", hash = "sha256:f66a321e93fadff34585a4bfa05e95bd946cf714b442f51c529038eb45773d97", size = 271270 }
@@ -3016,7 +2928,7 @@ name = "nvidia-cudnn-cu12"
version = "9.1.0.70"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'linux')" }
+ { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'linux')" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741 },
@@ -3045,7 +2957,7 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'linux')" },
{ name = "nvidia-cusparse-cu12", marker = "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'linux')" },
- { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'linux')" }
+ { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'linux')" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/1d/8de1e5c67099015c834315e333911273a8c6aaba78923dd1d1e25fc5f217/nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd", size = 124161928 },
@@ -3056,7 +2968,7 @@ name = "nvidia-cusparse-cu12"
version = "12.1.0.106"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'linux')" }
+ { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'linux')" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/65/5b/cfaeebf25cd9fdec14338ccb16f6b2c4c7fa9163aefcf057d86b9cc248bb/nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c", size = 195958278 },
@@ -3156,7 +3068,7 @@ wheels = [
[[package]]
name = "openai"
-version = "1.59.6"
+version = "1.61.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -3168,9 +3080,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/2e/7a/07fbe7bdabffd0a5be1bfe5903a02c4fff232e9acbae894014752a8e4def/openai-1.59.6.tar.gz", hash = "sha256:c7670727c2f1e4473f62fea6fa51475c8bc098c9ffb47bfb9eef5be23c747934", size = 344915 }
+sdist = { url = "https://files.pythonhosted.org/packages/32/2a/b3fa8790be17d632f59d4f50257b909a3f669036e5195c1ae55737274620/openai-1.61.0.tar.gz", hash = "sha256:216f325a24ed8578e929b0f1b3fb2052165f3b04b0461818adaa51aa29c71f8a", size = 350174 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/70/45/6de8e5fd670c804b29c777e4716f1916741c71604d5c7d952eee8432f7d3/openai-1.59.6-py3-none-any.whl", hash = "sha256:b28ed44eee3d5ebe1a3ea045ee1b4b50fea36ecd50741aaa5ce5a5559c900cb6", size = 454817 },
+ { url = "https://files.pythonhosted.org/packages/93/76/70c5ad6612b3e4c89fa520266bbf2430a89cae8bd87c1e2284698af5927e/openai-1.61.0-py3-none-any.whl", hash = "sha256:e8c512c0743accbdbe77f3429a1490d862f8352045de8dc81969301eb4a4f666", size = 460623 },
]
[[package]]
@@ -3395,18 +3307,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/8a/ce7c28e4ea337f6d95261345d7c61322f8561c52f57b263a3ad7025984f4/orjson-3.10.10-cp312-none-win_amd64.whl", hash = "sha256:384cd13579a1b4cd689d218e329f459eb9ddc504fa48c5a83ef4889db7fd7a4f", size = 139389 },
]
-[[package]]
-name = "outcome"
-version = "1.3.0.post0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "attrs" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692 },
-]
-
[[package]]
name = "overrides"
version = "7.7.0"
@@ -3606,7 +3506,7 @@ name = "portalocker"
version = "2.10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "pywin32", marker = "platform_system == 'Windows'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ed/d3/c6c64067759e87af98cc668c1cc75171347d0f1577fab7ca3749134e3cd4/portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f", size = 40891 }
wheels = [
@@ -4133,15 +4033,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/48/0a/c99fb7d7e176f8b176ef19704a32e6a9c6aafdf19ef75a187f701fc15801/pysbd-0.3.4-py3-none-any.whl", hash = "sha256:cd838939b7b0b185fcf86b0baf6636667dfb6e474743beeff878e9f42e022953", size = 71082 },
]
-[[package]]
-name = "pysocks"
-version = "1.7.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725 },
-]
-
[[package]]
name = "pytest"
version = "8.3.3"
@@ -4801,39 +4692,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/7d/43ab67228ef98c6b5dd42ab386eae2d7877036970a0d7e3dd3eb47a0d530/scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f", size = 44521212 },
]
-[[package]]
-name = "scrapegraph-py"
-version = "1.8.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "aiohttp" },
- { name = "beautifulsoup4" },
- { name = "pydantic" },
- { name = "python-dotenv" },
- { name = "requests" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/33/90/2388754061394a6c95fd5ad48cf4550208ce081c99cbc883672d52ccc360/scrapegraph_py-1.8.0.tar.gz", hash = "sha256:e075f6e6012a14a038537d0664609229069d9d2c2956bcbf9362f0c5c48de786", size = 108112 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f7/80/14aeb7ba092cfc6928844a6726855f0c33489107f344e71dd8071f6433ed/scrapegraph_py-1.8.0-py3-none-any.whl", hash = "sha256:279176c972a770bac37a284e0bc25e34793797f30ff24dfba8fbcbfda79c8c88", size = 14460 },
-]
-
-[[package]]
-name = "selenium"
-version = "4.25.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "trio" },
- { name = "trio-websocket" },
- { name = "typing-extensions" },
- { name = "urllib3", extra = ["socks"] },
- { name = "websocket-client" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/0e/5a/d3735b189b91715fd0f5a9b8d55e2605061309849470e96ab830f02cba40/selenium-4.25.0.tar.gz", hash = "sha256:95d08d3b82fb353f3c474895154516604c7f0e6a9a565ae6498ef36c9bac6921", size = 957765 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/aa/85/fa44f23dd5d5066a72f7c4304cce4b5ff9a6e7fd92431a48b2c63fbf63ec/selenium-4.25.0-py3-none-any.whl", hash = "sha256:3798d2d12b4a570bc5790163ba57fef10b2afee958bf1d80f2a3cf07c4141f33", size = 9693127 },
-]
-
[[package]]
name = "semchunk"
version = "2.2.0"
@@ -4847,18 +4705,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/85/3940bb4c586e10603d169d13ffccd59ed32fcb8d1b8104c3aef0e525b3b2/semchunk-2.2.0-py3-none-any.whl", hash = "sha256:7db19ca90ddb48f99265e789e07a7bb111ae25185f9cc3d44b94e1e61b9067fc", size = 10243 },
]
-[[package]]
-name = "serpapi"
-version = "0.1.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "requests" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/f0/fa/3fd8809287f3977a3e752bb88610e918d49cb1038b14f4bc51e13e594197/serpapi-0.1.5.tar.gz", hash = "sha256:b9707ed54750fdd2f62dc3a17c6a3fb7fa421dc37902fd65b2263c0ac765a1a5", size = 14191 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/df/6a/21deade04100d64844e494353a5d65e7971fbdfddf78eb1f248423593ad0/serpapi-0.1.5-py2.py3-none-any.whl", hash = "sha256:6467b6adec1231059f754ccaa952b229efeaa8b9cae6e71f879703ec9e5bb3d1", size = 10966 },
-]
-
[[package]]
name = "setuptools"
version = "75.2.0"
@@ -4924,15 +4770,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
-[[package]]
-name = "sortedcontainers"
-version = "2.4.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 },
-]
-
[[package]]
name = "soupsieve"
version = "2.6"
@@ -4942,18 +4779,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 },
]
-[[package]]
-name = "spider-client"
-version = "0.1.25"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "aiohttp" },
- { name = "ijson" },
- { name = "requests" },
- { name = "tenacity" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b8/f2/06d89322f0054ea72e8d5580199f580e29df23476cb3cfe83a70a2a58a1b/spider-client-0.1.25.tar.gz", hash = "sha256:92ca4ce1d9d715dd8db52684ea417653940d8f3bbc13383d78683bc4fbb899a2", size = 15412 }
-
[[package]]
name = "sqlalchemy"
version = "2.0.36"
@@ -5194,19 +5019,19 @@ dependencies = [
{ name = "fsspec" },
{ name = "jinja2" },
{ name = "networkx" },
- { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
- { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
- { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
- { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
- { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
- { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
- { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
- { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
- { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
- { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
- { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+ { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+ { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+ { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+ { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+ { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+ { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+ { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+ { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+ { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
+ { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
{ name = "sympy" },
- { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "triton", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
{ name = "typing-extensions" },
]
wheels = [
@@ -5253,7 +5078,7 @@ name = "tqdm"
version = "4.66.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "colorama", marker = "platform_system == 'Windows'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/58/83/6ba9844a41128c62e810fddddd72473201f3eacde02046066142a2d96cc5/tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad", size = 169504 }
wheels = [
@@ -5290,44 +5115,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/51/51/b87caa939fedf307496e4dbf412f4b909af3d9ca8b189fc3b65c1faa456f/transformers-4.46.3-py3-none-any.whl", hash = "sha256:a12ef6f52841fd190a3e5602145b542d03507222f2c64ebb7ee92e8788093aef", size = 10034536 },
]
-[[package]]
-name = "trio"
-version = "0.27.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "attrs" },
- { name = "cffi", marker = "(implementation_name != 'pypy' and os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (implementation_name != 'pypy' and os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" },
- { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
- { name = "idna" },
- { name = "outcome" },
- { name = "sniffio" },
- { name = "sortedcontainers" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/17/d1/a83dee5be404da7afe5a71783a33b8907bacb935a6dc8c69ab785e4a3eed/trio-0.27.0.tar.gz", hash = "sha256:1dcc95ab1726b2da054afea8fd761af74bad79bd52381b84eae408e983c76831", size = 568064 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3c/83/ec3196c360afffbc5b342ead48d1eb7393dd74fa70bca75d33905a86f211/trio-0.27.0-py3-none-any.whl", hash = "sha256:68eabbcf8f457d925df62da780eff15ff5dc68fd6b367e2dde59f7aaf2a0b884", size = 481734 },
-]
-
-[[package]]
-name = "trio-websocket"
-version = "0.11.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
- { name = "trio" },
- { name = "wsproto" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/dd/36/abad2385853077424a11b818d9fd8350d249d9e31d583cb9c11cd4c85eda/trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f", size = 26511 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/48/be/a9ae5f50cad5b6f85bd2574c2c923730098530096e170c1ce7452394d7aa/trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638", size = 17408 },
-]
-
[[package]]
name = "triton"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "filelock", marker = "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'linux')" }
+ { name = "filelock", marker = "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'linux')" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/45/27/14cc3101409b9b4b9241d2ba7deaa93535a217a211c86c4cc7151fb12181/triton-3.0.0-1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e1efef76935b2febc365bfadf74bcb65a6f959a9872e5bddf44cc9e0adce1e1a", size = 209376304 },
@@ -5402,11 +5195,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 },
]
-[package.optional-dependencies]
-socks = [
- { name = "pysocks" },
-]
-
[[package]]
name = "uv"
version = "0.4.26"
@@ -5483,15 +5271,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 },
]
-[[package]]
-name = "validators"
-version = "0.34.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/64/07/91582d69320f6f6daaf2d8072608a4ad8884683d4840e7e4f3a9dbdcc639/validators-0.34.0.tar.gz", hash = "sha256:647fe407b45af9a74d245b943b18e6a816acf4926974278f6dd617778e1e781f", size = 70955 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6e/78/36828a4d857b25896f9774c875714ba4e9b3bc8a92d2debe3f4df3a83d4f/validators-0.34.0-py3-none-any.whl", hash = "sha256:c804b476e3e6d3786fa07a30073a4ef694e617805eb1946ceee3fe5a9b8b1321", size = 43536 },
-]
-
[[package]]
name = "vcrpy"
version = "5.1.0"
@@ -5611,25 +5390,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
]
-[[package]]
-name = "weaviate-client"
-version = "4.9.6"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "authlib" },
- { name = "grpcio" },
- { name = "grpcio-health-checking" },
- { name = "grpcio-tools" },
- { name = "httpx" },
- { name = "pydantic" },
- { name = "requests" },
- { name = "validators" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5d/7d/3894d12065d006743271b0b6bcc3bf911910473e91179d5966966816d694/weaviate_client-4.9.6.tar.gz", hash = "sha256:56d67c40fc94b0d53e81e0aa4477baaebbf3646fbec26551df66e396a72adcb6", size = 696813 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2f/40/e3550e743b92ddd8dc69ebfd69cceb6de45b7d9a1cd439995454b499e9a3/weaviate_client-4.9.6-py3-none-any.whl", hash = "sha256:1d3b551939c0f7314f25e417cbcf4cf34e7adf942627993eef36ae6b4a044673", size = 386998 },
-]
-
[[package]]
name = "webencodings"
version = "0.5.1"
@@ -5744,18 +5504,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/21/abdedb4cdf6ff41ebf01a74087740a709e2edb146490e4d9beea054b0b7a/wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", size = 23362 },
]
-[[package]]
-name = "wsproto"
-version = "1.2.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "h11" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226 },
-]
-
[[package]]
name = "xlsxwriter"
version = "3.2.0"