mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-04-24 03:42:38 +00:00
Compare commits
21 Commits
bugfix/fix
...
bugfix/upd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
940fb30e0e | ||
|
|
dea6ed7ef0 | ||
|
|
d3a0dad323 | ||
|
|
67bf4aea56 | ||
|
|
8c76bad50f | ||
|
|
e27a15023c | ||
|
|
a836f466f4 | ||
|
|
67f0de1f90 | ||
|
|
c642ebf97e | ||
|
|
a21e310d78 | ||
|
|
aba68da542 | ||
|
|
e254f11933 | ||
|
|
ab2274caf0 | ||
|
|
3e4f112f39 | ||
|
|
cc018bf128 | ||
|
|
46d3e4d4d9 | ||
|
|
627bb3f5f6 | ||
|
|
4a44245de9 | ||
|
|
30d027158a | ||
|
|
3fecde49b6 | ||
|
|
cc129a0bce |
@@ -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:
|
To use the CrewAI CLI, make sure you have CrewAI installed:
|
||||||
|
|
||||||
```shell
|
```shell Terminal
|
||||||
pip install crewai
|
pip install crewai
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ pip install crewai
|
|||||||
|
|
||||||
The basic structure of a CrewAI CLI command is:
|
The basic structure of a CrewAI CLI command is:
|
||||||
|
|
||||||
```shell
|
```shell Terminal
|
||||||
crewai [COMMAND] [OPTIONS] [ARGUMENTS]
|
crewai [COMMAND] [OPTIONS] [ARGUMENTS]
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ crewai [COMMAND] [OPTIONS] [ARGUMENTS]
|
|||||||
|
|
||||||
Create a new crew or flow.
|
Create a new crew or flow.
|
||||||
|
|
||||||
```shell
|
```shell Terminal
|
||||||
crewai create [OPTIONS] TYPE NAME
|
crewai create [OPTIONS] TYPE NAME
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ crewai create [OPTIONS] TYPE NAME
|
|||||||
- `NAME`: Name of the crew or flow
|
- `NAME`: Name of the crew or flow
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```shell
|
```shell Terminal
|
||||||
crewai create crew my_new_crew
|
crewai create crew my_new_crew
|
||||||
crewai create flow my_new_flow
|
crewai create flow my_new_flow
|
||||||
```
|
```
|
||||||
@@ -47,14 +47,14 @@ crewai create flow my_new_flow
|
|||||||
|
|
||||||
Show the installed version of CrewAI.
|
Show the installed version of CrewAI.
|
||||||
|
|
||||||
```shell
|
```shell Terminal
|
||||||
crewai version [OPTIONS]
|
crewai version [OPTIONS]
|
||||||
```
|
```
|
||||||
|
|
||||||
- `--tools`: (Optional) Show the installed version of CrewAI tools
|
- `--tools`: (Optional) Show the installed version of CrewAI tools
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```shell
|
```shell Terminal
|
||||||
crewai version
|
crewai version
|
||||||
crewai version --tools
|
crewai version --tools
|
||||||
```
|
```
|
||||||
@@ -63,7 +63,7 @@ crewai version --tools
|
|||||||
|
|
||||||
Train the crew for a specified number of iterations.
|
Train the crew for a specified number of iterations.
|
||||||
|
|
||||||
```shell
|
```shell Terminal
|
||||||
crewai train [OPTIONS]
|
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")
|
- `-f, --filename TEXT`: Path to a custom file for training (default: "trained_agents_data.pkl")
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```shell
|
```shell Terminal
|
||||||
crewai train -n 10 -f my_training_data.pkl
|
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.
|
Replay the crew execution from a specific task.
|
||||||
|
|
||||||
```shell
|
```shell Terminal
|
||||||
crewai replay [OPTIONS]
|
crewai replay [OPTIONS]
|
||||||
```
|
```
|
||||||
|
|
||||||
- `-t, --task_id TEXT`: Replay the crew from this task ID, including all subsequent tasks
|
- `-t, --task_id TEXT`: Replay the crew from this task ID, including all subsequent tasks
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```shell
|
```shell Terminal
|
||||||
crewai replay -t task_123456
|
crewai replay -t task_123456
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ crewai replay -t task_123456
|
|||||||
|
|
||||||
Retrieve your latest crew.kickoff() task outputs.
|
Retrieve your latest crew.kickoff() task outputs.
|
||||||
|
|
||||||
```shell
|
```shell Terminal
|
||||||
crewai log-tasks-outputs
|
crewai log-tasks-outputs
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ crewai log-tasks-outputs
|
|||||||
|
|
||||||
Reset the crew memories (long, short, entity, latest_crew_kickoff_outputs).
|
Reset the crew memories (long, short, entity, latest_crew_kickoff_outputs).
|
||||||
|
|
||||||
```shell
|
```shell Terminal
|
||||||
crewai reset-memories [OPTIONS]
|
crewai reset-memories [OPTIONS]
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ crewai reset-memories [OPTIONS]
|
|||||||
- `-a, --all`: Reset ALL memories
|
- `-a, --all`: Reset ALL memories
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```shell
|
```shell Terminal
|
||||||
crewai reset-memories --long --short
|
crewai reset-memories --long --short
|
||||||
crewai reset-memories --all
|
crewai reset-memories --all
|
||||||
```
|
```
|
||||||
@@ -122,7 +122,7 @@ crewai reset-memories --all
|
|||||||
|
|
||||||
Test the crew and evaluate the results.
|
Test the crew and evaluate the results.
|
||||||
|
|
||||||
```shell
|
```shell Terminal
|
||||||
crewai test [OPTIONS]
|
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")
|
- `-m, --model TEXT`: LLM Model to run the tests on the Crew (default: "gpt-4o-mini")
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```shell
|
```shell Terminal
|
||||||
crewai test -n 5 -m gpt-3.5-turbo
|
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.
|
Run the crew.
|
||||||
|
|
||||||
```shell
|
```shell Terminal
|
||||||
crewai run
|
crewai run
|
||||||
```
|
```
|
||||||
<Note>
|
<Note>
|
||||||
@@ -147,7 +147,36 @@ Some commands may require additional configuration or setup within your project
|
|||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
|
|
||||||
### 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
|
||||||
|
```
|
||||||
|
<Note>
|
||||||
|
Ensure you execute these commands from your CrewAI project's root directory.
|
||||||
|
</Note>
|
||||||
|
<Note>
|
||||||
|
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
|
||||||
|
)
|
||||||
|
```
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
|
||||||
|
### 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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
|
## Flow Control
|
||||||
|
|
||||||
### Conditional Logic: `or`
|
### Conditional Logic: `or`
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ The `embedder` parameter supports various embedding model providers that include
|
|||||||
- `ollama`: Local embeddings with Ollama
|
- `ollama`: Local embeddings with Ollama
|
||||||
- `vertexai`: Google Cloud VertexAI embeddings
|
- `vertexai`: Google Cloud VertexAI embeddings
|
||||||
- `cohere`: Cohere's embedding models
|
- `cohere`: Cohere's embedding models
|
||||||
|
- `voyageai`: VoyageAI's embedding models
|
||||||
- `bedrock`: AWS Bedrock embeddings
|
- `bedrock`: AWS Bedrock embeddings
|
||||||
- `huggingface`: Hugging Face models
|
- `huggingface`: Hugging Face models
|
||||||
- `watson`: IBM Watson embeddings
|
- `watson`: IBM Watson embeddings
|
||||||
|
|||||||
@@ -243,6 +243,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/amazon.titan-text-express-v1
|
||||||
# llm: bedrock/meta.llama2-70b-chat-v1
|
# llm: bedrock/meta.llama2-70b-chat-v1
|
||||||
|
|
||||||
|
# Amazon SageMaker Models - Enterprise-grade
|
||||||
|
# llm: sagemaker/<my-endpoint>
|
||||||
|
|
||||||
# Mistral Models - Open source alternative
|
# Mistral Models - Open source alternative
|
||||||
# llm: mistral/mistral-large-latest
|
# llm: mistral/mistral-large-latest
|
||||||
# llm: mistral/mistral-medium-latest
|
# llm: mistral/mistral-medium-latest
|
||||||
@@ -506,6 +509,21 @@ Learn how to get the most out of your LLM configuration:
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="Amazon SageMaker">
|
||||||
|
```python Code
|
||||||
|
AWS_ACCESS_KEY_ID=<your-access-key>
|
||||||
|
AWS_SECRET_ACCESS_KEY=<your-secret-key>
|
||||||
|
AWS_DEFAULT_REGION=<your-region>
|
||||||
|
```
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
```python Code
|
||||||
|
llm = LLM(
|
||||||
|
model="sagemaker/<my-endpoint>"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="Mistral">
|
<Accordion title="Mistral">
|
||||||
```python Code
|
```python Code
|
||||||
|
|||||||
@@ -293,6 +293,26 @@ my_crew = Crew(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
### Using VoyageAI embeddings
|
||||||
|
|
||||||
|
```python Code
|
||||||
|
from crewai import Crew, Agent, Task, Process
|
||||||
|
|
||||||
|
my_crew = Crew(
|
||||||
|
agents=[...],
|
||||||
|
tasks=[...],
|
||||||
|
process=Process.sequential,
|
||||||
|
memory=True,
|
||||||
|
verbose=True,
|
||||||
|
embedder={
|
||||||
|
"provider": "voyageai",
|
||||||
|
"config": {
|
||||||
|
"api_key": "YOUR_API_KEY",
|
||||||
|
"model_name": "<model_name>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
### Using HuggingFace embeddings
|
### Using HuggingFace embeddings
|
||||||
|
|
||||||
```python Code
|
```python Code
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ LiteLLM supports a wide range of providers, including but not limited to:
|
|||||||
- Azure OpenAI
|
- Azure OpenAI
|
||||||
- AWS (Bedrock, SageMaker)
|
- AWS (Bedrock, SageMaker)
|
||||||
- Cohere
|
- Cohere
|
||||||
|
- VoyageAI
|
||||||
- Hugging Face
|
- Hugging Face
|
||||||
- Ollama
|
- Ollama
|
||||||
- Mistral AI
|
- Mistral AI
|
||||||
|
|||||||
@@ -15,10 +15,48 @@ icon: wrench
|
|||||||
If you need to update Python, visit [python.org/downloads](https://python.org/downloads)
|
If you need to update Python, visit [python.org/downloads](https://python.org/downloads)
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create a Virtual Environment">
|
||||||
|
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
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Activate the Virtual Environment">
|
||||||
|
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
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
# Installing CrewAI
|
# Installing CrewAI
|
||||||
|
|
||||||
CrewAI is a flexible and powerful AI framework that enables you to create and manage AI agents, tools, and tasks efficiently.
|
Now let's get you set up! 🚀
|
||||||
Let's get you set up! 🚀
|
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Install CrewAI">
|
<Step title="Install CrewAI">
|
||||||
@@ -72,9 +110,9 @@ Let's get you set up! 🚀
|
|||||||
|
|
||||||
# Creating a New Project
|
# Creating a New Project
|
||||||
|
|
||||||
<Info>
|
<Tip>
|
||||||
We recommend using the YAML Template scaffolding for a structured approach to defining agents and tasks.
|
We recommend using the YAML Template scaffolding for a structured approach to defining agents and tasks.
|
||||||
</Info>
|
</Tip>
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Generate Project Structure">
|
<Step title="Generate Project Structure">
|
||||||
@@ -104,7 +142,18 @@ Let's get you set up! 🚀
|
|||||||
└── tasks.yaml
|
└── tasks.yaml
|
||||||
```
|
```
|
||||||
</Frame>
|
</Frame>
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Install Additional Tools">
|
||||||
|
You can install additional tools using UV:
|
||||||
|
```shell Terminal
|
||||||
|
uv add <tool-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
UV is our preferred package manager as it's significantly faster than pip and provides better dependency resolution.
|
||||||
|
</Tip>
|
||||||
|
</Step>
|
||||||
|
|
||||||
<Step title="Customize Your Project">
|
<Step title="Customize Your Project">
|
||||||
Your project will contain these essential files:
|
Your project will contain these essential files:
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ email_summarizer:
|
|||||||
Summarize emails into a concise and clear summary
|
Summarize emails into a concise and clear summary
|
||||||
backstory: >
|
backstory: >
|
||||||
You will create a 5 bullet point summary of the report
|
You will create a 5 bullet point summary of the report
|
||||||
llm: mixtal_llm
|
llm: openai/gpt-4o
|
||||||
```
|
```
|
||||||
|
|
||||||
<Tip>
|
<Tip>
|
||||||
|
|||||||
@@ -1,78 +1,118 @@
|
|||||||
---
|
---
|
||||||
title: Composio Tool
|
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
|
icon: gear-code
|
||||||
---
|
---
|
||||||
|
|
||||||
# `ComposioTool`
|
# `ComposioToolSet`
|
||||||
|
|
||||||
## Description
|
## 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
|
## 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
|
```shell
|
||||||
pip install composio-core
|
pip install composio-crewai
|
||||||
pip install 'crewai[tools]'
|
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
|
## Example
|
||||||
|
|
||||||
The following example demonstrates how to initialize the tool and execute a github action:
|
The following example demonstrates how to initialize the tool and execute a github action:
|
||||||
|
|
||||||
1. Initialize Composio tools
|
1. Initialize Composio toolset
|
||||||
|
|
||||||
```python Code
|
```python Code
|
||||||
from composio import App
|
from composio_crewai import ComposioToolSet, App, Action
|
||||||
from crewai_tools import ComposioTool
|
from crewai import Agent, Task, Crew
|
||||||
from crewai import Agent, Task
|
|
||||||
|
|
||||||
|
toolset = ComposioToolSet()
|
||||||
tools = [ComposioTool.from_action(action=Action.GITHUB_ACTIVITY_STAR_REPO_FOR_AUTHENTICATED_USER)]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
||||||
|
<CodeGroup>
|
||||||
|
```shell CLI
|
||||||
|
composio add github
|
||||||
|
```
|
||||||
```python Code
|
```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}")
|
||||||
```
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
or use `use_case` to search relevant actions
|
3. Get Tools
|
||||||
|
|
||||||
|
- Retrieving all the tools from an app (not recommended for production):
|
||||||
```python Code
|
```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)
|
||||||
|
```
|
||||||
|
<Tip>Set `advanced` to True to get actions for complex use cases</Tip>
|
||||||
|
|
||||||
|
- Using specific tools:
|
||||||
|
|
||||||
|
In this demo, we will use the `GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER` action from the GitHub app.
|
||||||
|
```python Code
|
||||||
|
tools = toolset.get_tools(
|
||||||
|
actions=[Action.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
Learn more about filtering actions [here](https://docs.composio.dev/patterns/tools/use-tools/use-specific-actions)
|
||||||
|
|
||||||
|
4. Define agent
|
||||||
|
|
||||||
```python Code
|
```python Code
|
||||||
crewai_agent = Agent(
|
crewai_agent = Agent(
|
||||||
role="Github Agent",
|
role="GitHub Agent",
|
||||||
goal="You take action on Github using Github APIs",
|
goal="You take action on GitHub using GitHub APIs",
|
||||||
backstory=(
|
backstory="You are AI agent that is responsible for taking actions on GitHub on behalf of users using GitHub APIs",
|
||||||
"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"
|
|
||||||
),
|
|
||||||
verbose=True,
|
verbose=True,
|
||||||
tools=tools,
|
tools=tools,
|
||||||
|
llm= # pass an llm
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Execute task
|
5. Execute task
|
||||||
|
|
||||||
```python Code
|
```python Code
|
||||||
task = Task(
|
task = Task(
|
||||||
description="Star a repo ComposioHQ/composio on GitHub",
|
description="Star a repo composiohq/composio on GitHub",
|
||||||
agent=crewai_agent,
|
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)
|
* More detailed list of tools can be found [here](https://app.composio.dev)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "crewai"
|
name = "crewai"
|
||||||
version = "0.95.0"
|
version = "0.98.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."
|
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"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
@@ -13,25 +13,20 @@ dependencies = [
|
|||||||
"openai>=1.13.3",
|
"openai>=1.13.3",
|
||||||
"litellm==1.57.4",
|
"litellm==1.57.4",
|
||||||
"instructor>=1.3.3",
|
"instructor>=1.3.3",
|
||||||
|
|
||||||
# Text Processing
|
# Text Processing
|
||||||
"pdfplumber>=0.11.4",
|
"pdfplumber>=0.11.4",
|
||||||
"regex>=2024.9.11",
|
"regex>=2024.9.11",
|
||||||
|
|
||||||
# Telemetry and Monitoring
|
# Telemetry and Monitoring
|
||||||
"opentelemetry-api>=1.22.0",
|
"opentelemetry-api>=1.22.0",
|
||||||
"opentelemetry-sdk>=1.22.0",
|
"opentelemetry-sdk>=1.22.0",
|
||||||
"opentelemetry-exporter-otlp-proto-http>=1.22.0",
|
"opentelemetry-exporter-otlp-proto-http>=1.22.0",
|
||||||
|
|
||||||
# Data Handling
|
# Data Handling
|
||||||
"chromadb>=0.5.23",
|
"chromadb>=0.5.23",
|
||||||
"openpyxl>=3.1.5",
|
"openpyxl>=3.1.5",
|
||||||
"pyvis>=0.3.2",
|
"pyvis>=0.3.2",
|
||||||
|
|
||||||
# Authentication and Security
|
# Authentication and Security
|
||||||
"auth0-python>=4.7.1",
|
"auth0-python>=4.7.1",
|
||||||
"python-dotenv>=1.0.0",
|
"python-dotenv>=1.0.0",
|
||||||
|
|
||||||
# Configuration and Utils
|
# Configuration and Utils
|
||||||
"click>=8.1.7",
|
"click>=8.1.7",
|
||||||
"appdirs>=1.4.4",
|
"appdirs>=1.4.4",
|
||||||
@@ -40,7 +35,8 @@ dependencies = [
|
|||||||
"uv>=0.4.25",
|
"uv>=0.4.25",
|
||||||
"tomli-w>=1.1.0",
|
"tomli-w>=1.1.0",
|
||||||
"tomli>=2.0.2",
|
"tomli>=2.0.2",
|
||||||
"blinker>=1.9.0"
|
"blinker>=1.9.0",
|
||||||
|
"json5>=0.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@@ -49,7 +45,7 @@ Documentation = "https://docs.crewai.com"
|
|||||||
Repository = "https://github.com/crewAIInc/crewAI"
|
Repository = "https://github.com/crewAIInc/crewAI"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
tools = ["crewai-tools>=0.25.5"]
|
tools = ["crewai-tools>=0.32.1"]
|
||||||
embeddings = [
|
embeddings = [
|
||||||
"tiktoken~=0.7.0"
|
"tiktoken~=0.7.0"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ warnings.filterwarnings(
|
|||||||
category=UserWarning,
|
category=UserWarning,
|
||||||
module="pydantic.main",
|
module="pydantic.main",
|
||||||
)
|
)
|
||||||
__version__ = "0.95.0"
|
__version__ = "0.98.0"
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Agent",
|
"Agent",
|
||||||
"Crew",
|
"Crew",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Any, Dict, List, Literal, Optional, Union
|
from typing import Any, Dict, List, Literal, Optional, Union
|
||||||
@@ -8,7 +7,6 @@ from pydantic import Field, InstanceOf, PrivateAttr, model_validator
|
|||||||
from crewai.agents import CacheHandler
|
from crewai.agents import CacheHandler
|
||||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||||
from crewai.agents.crew_agent_executor import CrewAgentExecutor
|
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.knowledge import Knowledge
|
||||||
from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
|
from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
|
||||||
from crewai.knowledge.utils.knowledge_utils import extract_knowledge_context
|
from crewai.knowledge.utils.knowledge_utils import extract_knowledge_context
|
||||||
@@ -261,6 +259,9 @@ class Agent(BaseAgent):
|
|||||||
}
|
}
|
||||||
)["output"]
|
)["output"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if e.__class__.__module__.startswith("litellm"):
|
||||||
|
# Do not retry on litellm errors
|
||||||
|
raise e
|
||||||
self._times_executed += 1
|
self._times_executed += 1
|
||||||
if self._times_executed > self.max_retry_limit:
|
if self._times_executed > self.max_retry_limit:
|
||||||
raise e
|
raise e
|
||||||
|
|||||||
@@ -2,26 +2,26 @@ from crewai.types.usage_metrics import UsageMetrics
|
|||||||
|
|
||||||
|
|
||||||
class TokenProcess:
|
class TokenProcess:
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.total_tokens: int = 0
|
self.total_tokens: int = 0
|
||||||
self.prompt_tokens: int = 0
|
self.prompt_tokens: int = 0
|
||||||
self.cached_prompt_tokens: int = 0
|
self.cached_prompt_tokens: int = 0
|
||||||
self.completion_tokens: int = 0
|
self.completion_tokens: int = 0
|
||||||
self.successful_requests: int = 0
|
self.successful_requests: int = 0
|
||||||
|
|
||||||
def sum_prompt_tokens(self, tokens: int):
|
def sum_prompt_tokens(self, tokens: int) -> None:
|
||||||
self.prompt_tokens = self.prompt_tokens + tokens
|
self.prompt_tokens += tokens
|
||||||
self.total_tokens = self.total_tokens + tokens
|
self.total_tokens += tokens
|
||||||
|
|
||||||
def sum_completion_tokens(self, tokens: int):
|
def sum_completion_tokens(self, tokens: int) -> None:
|
||||||
self.completion_tokens = self.completion_tokens + tokens
|
self.completion_tokens += tokens
|
||||||
self.total_tokens = self.total_tokens + tokens
|
self.total_tokens += tokens
|
||||||
|
|
||||||
def sum_cached_prompt_tokens(self, tokens: int):
|
def sum_cached_prompt_tokens(self, tokens: int) -> None:
|
||||||
self.cached_prompt_tokens = self.cached_prompt_tokens + tokens
|
self.cached_prompt_tokens += tokens
|
||||||
|
|
||||||
def sum_successful_requests(self, requests: int):
|
def sum_successful_requests(self, requests: int) -> None:
|
||||||
self.successful_requests = self.successful_requests + requests
|
self.successful_requests += requests
|
||||||
|
|
||||||
def get_summary(self) -> UsageMetrics:
|
def get_summary(self) -> UsageMetrics:
|
||||||
return UsageMetrics(
|
return UsageMetrics(
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from crewai.agents.parser import (
|
|||||||
OutputParserException,
|
OutputParserException,
|
||||||
)
|
)
|
||||||
from crewai.agents.tools_handler import ToolsHandler
|
from crewai.agents.tools_handler import ToolsHandler
|
||||||
|
from crewai.llm import LLM
|
||||||
from crewai.tools.base_tool import BaseTool
|
from crewai.tools.base_tool import BaseTool
|
||||||
from crewai.tools.tool_usage import ToolUsage, ToolUsageErrorException
|
from crewai.tools.tool_usage import ToolUsage, ToolUsageErrorException
|
||||||
from crewai.utilities import I18N, Printer
|
from crewai.utilities import I18N, Printer
|
||||||
@@ -54,7 +55,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
|||||||
callbacks: List[Any] = [],
|
callbacks: List[Any] = [],
|
||||||
):
|
):
|
||||||
self._i18n: I18N = I18N()
|
self._i18n: I18N = I18N()
|
||||||
self.llm = llm
|
self.llm: LLM = llm
|
||||||
self.task = task
|
self.task = task
|
||||||
self.agent = agent
|
self.agent = agent
|
||||||
self.crew = crew
|
self.crew = crew
|
||||||
@@ -80,10 +81,8 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
|||||||
self.tool_name_to_tool_map: Dict[str, BaseTool] = {
|
self.tool_name_to_tool_map: Dict[str, BaseTool] = {
|
||||||
tool.name: tool for tool in self.tools
|
tool.name: tool for tool in self.tools
|
||||||
}
|
}
|
||||||
if self.llm.stop:
|
self.stop = stop_words
|
||||||
self.llm.stop = list(set(self.llm.stop + self.stop))
|
self.llm.stop = list(set(self.llm.stop + self.stop))
|
||||||
else:
|
|
||||||
self.llm.stop = self.stop
|
|
||||||
|
|
||||||
def invoke(self, inputs: Dict[str, str]) -> Dict[str, Any]:
|
def invoke(self, inputs: Dict[str, str]) -> Dict[str, Any]:
|
||||||
if "system" in self.prompt:
|
if "system" in self.prompt:
|
||||||
@@ -98,7 +97,16 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
|||||||
self._show_start_logs()
|
self._show_start_logs()
|
||||||
|
|
||||||
self.ask_for_human_input = bool(inputs.get("ask_for_human_input", False))
|
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 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:
|
if self.ask_for_human_input:
|
||||||
formatted_answer = self._handle_human_feedback(formatted_answer)
|
formatted_answer = self._handle_human_feedback(formatted_answer)
|
||||||
@@ -124,7 +132,6 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
|||||||
self._enforce_rpm_limit()
|
self._enforce_rpm_limit()
|
||||||
|
|
||||||
answer = self._get_llm_response()
|
answer = self._get_llm_response()
|
||||||
|
|
||||||
formatted_answer = self._process_llm_response(answer)
|
formatted_answer = self._process_llm_response(answer)
|
||||||
|
|
||||||
if isinstance(formatted_answer, AgentAction):
|
if isinstance(formatted_answer, AgentAction):
|
||||||
@@ -142,13 +149,32 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
|||||||
formatted_answer = self._handle_output_parser_exception(e)
|
formatted_answer = self._handle_output_parser_exception(e)
|
||||||
|
|
||||||
except Exception as 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):
|
if self._is_context_length_exceeded(e):
|
||||||
self._handle_context_length()
|
self._handle_context_length()
|
||||||
continue
|
continue
|
||||||
|
else:
|
||||||
|
self._handle_unknown_error(e)
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
self.iterations += 1
|
||||||
|
|
||||||
self._show_logs(formatted_answer)
|
self._show_logs(formatted_answer)
|
||||||
return 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:
|
def _has_reached_max_iterations(self) -> bool:
|
||||||
"""Check if the maximum number of iterations has been reached."""
|
"""Check if the maximum number of iterations has been reached."""
|
||||||
return self.iterations >= self.max_iter
|
return self.iterations >= self.max_iter
|
||||||
@@ -160,10 +186,17 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
|||||||
|
|
||||||
def _get_llm_response(self) -> str:
|
def _get_llm_response(self) -> str:
|
||||||
"""Call the LLM and return the response, handling any invalid responses."""
|
"""Call the LLM and return the response, handling any invalid responses."""
|
||||||
answer = self.llm.call(
|
try:
|
||||||
self.messages,
|
answer = self.llm.call(
|
||||||
callbacks=self.callbacks,
|
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:
|
if not answer:
|
||||||
self._printer.print(
|
self._printer.print(
|
||||||
@@ -184,7 +217,6 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
|
|||||||
if FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE in e.error:
|
if FINAL_ANSWER_AND_PARSABLE_ACTION_ERROR_MESSAGE in e.error:
|
||||||
answer = answer.split("Observation:")[0].strip()
|
answer = answer.split("Observation:")[0].strip()
|
||||||
|
|
||||||
self.iterations += 1
|
|
||||||
return self._format_answer(answer)
|
return self._format_answer(answer)
|
||||||
|
|
||||||
def _handle_agent_action(
|
def _handle_agent_action(
|
||||||
|
|||||||
@@ -350,7 +350,10 @@ def chat():
|
|||||||
Start a conversation with the Crew, collecting user-supplied inputs,
|
Start a conversation with the Crew, collecting user-supplied inputs,
|
||||||
and using the Chat LLM to generate responses.
|
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()
|
run_chat()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,52 @@
|
|||||||
import json
|
import json
|
||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Set, Tuple
|
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import tomli
|
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.crew import Crew
|
||||||
from crewai.llm import LLM
|
from crewai.llm import LLM
|
||||||
from crewai.types.crew_chat import ChatInputField, ChatInputs
|
from crewai.types.crew_chat import ChatInputField, ChatInputs
|
||||||
from crewai.utilities.llm_utils import create_llm
|
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():
|
def run_chat():
|
||||||
"""
|
"""
|
||||||
@@ -19,20 +54,47 @@ def run_chat():
|
|||||||
Incorporates crew_name, crew_description, and input fields to build a tool schema.
|
Incorporates crew_name, crew_description, and input fields to build a tool schema.
|
||||||
Exits if crew_name or crew_description are missing.
|
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()
|
crew, crew_name = load_crew_and_name()
|
||||||
chat_llm = initialize_chat_llm(crew)
|
chat_llm = initialize_chat_llm(crew)
|
||||||
if not chat_llm:
|
if not chat_llm:
|
||||||
return
|
return
|
||||||
|
|
||||||
crew_chat_inputs = generate_crew_chat_inputs(crew, crew_name, chat_llm)
|
# Indicate that the crew is being analyzed
|
||||||
crew_tool_schema = generate_crew_tool_schema(crew_chat_inputs)
|
click.secho(
|
||||||
system_message = build_system_message(crew_chat_inputs)
|
"\nAnalyzing crew and required inputs - this may take 3 to 30 seconds "
|
||||||
|
"depending on the complexity of your crew.",
|
||||||
# Call the LLM to generate the introductory message
|
fg="white",
|
||||||
introductory_message = chat_llm.call(
|
|
||||||
messages=[{"role": "system", "content": system_message}]
|
|
||||||
)
|
)
|
||||||
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 = [
|
messages = [
|
||||||
{"role": "system", "content": system_message},
|
{"role": "system", "content": system_message},
|
||||||
@@ -43,15 +105,17 @@ def run_chat():
|
|||||||
crew_chat_inputs.crew_name: create_tool_function(crew, messages),
|
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)
|
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]:
|
def initialize_chat_llm(crew: Crew) -> Optional[LLM]:
|
||||||
"""Initializes the chat LLM and handles exceptions."""
|
"""Initializes the chat LLM and handles exceptions."""
|
||||||
try:
|
try:
|
||||||
@@ -85,7 +149,7 @@ def build_system_message(crew_chat_inputs: ChatInputs) -> str:
|
|||||||
"Please keep your responses concise and friendly. "
|
"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. "
|
"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. "
|
"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?' "
|
"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.'"
|
"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}"
|
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
|
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):
|
def chat_loop(chat_llm, messages, crew_tool_schema, available_functions):
|
||||||
"""Main chat loop for interacting with the user."""
|
"""Main chat loop for interacting with the user."""
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
user_input = click.prompt("You", type=str)
|
# Flush any pending input before accepting new input
|
||||||
if user_input.strip().lower() in ["exit", "quit"]:
|
flush_input()
|
||||||
click.echo("Exiting chat. Goodbye!")
|
|
||||||
break
|
|
||||||
|
|
||||||
messages.append({"role": "user", "content": user_input})
|
user_input = get_user_input()
|
||||||
final_response = chat_llm.call(
|
handle_user_input(
|
||||||
messages=messages,
|
user_input, chat_llm, messages, crew_tool_schema, available_functions
|
||||||
tools=[crew_tool_schema],
|
|
||||||
available_functions=available_functions,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
messages.append({"role": "assistant", "content": final_response})
|
|
||||||
click.secho(f"\nAssistant: {final_response}\n", fg="green")
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
click.echo("\nExiting chat. Goodbye!")
|
click.echo("\nExiting chat. Goodbye!")
|
||||||
break
|
break
|
||||||
@@ -129,6 +201,55 @@ def chat_loop(chat_llm, messages, crew_tool_schema, available_functions):
|
|||||||
break
|
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:
|
def generate_crew_tool_schema(crew_inputs: ChatInputs) -> dict:
|
||||||
"""
|
"""
|
||||||
Dynamically build a Littellm 'function' schema for the given crew.
|
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
|
# Replace placeholders with input names
|
||||||
task_description = placeholder_pattern.sub(
|
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(
|
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"Task Description: {task_description}")
|
||||||
context_texts.append(f"Expected Output: {expected_output}")
|
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
|
or f"{{{input_name}}}" in agent.backstory
|
||||||
):
|
):
|
||||||
# Replace placeholders with input names
|
# Replace placeholders with input names
|
||||||
agent_role = placeholder_pattern.sub(lambda m: m.group(1), agent.role)
|
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)
|
agent_goal = placeholder_pattern.sub(lambda m: m.group(1), agent.goal or "")
|
||||||
agent_backstory = placeholder_pattern.sub(
|
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 Role: {agent_role}")
|
||||||
context_texts.append(f"Agent Goal: {agent_goal}")
|
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:
|
for task in crew.tasks:
|
||||||
# Replace placeholders with input names
|
# Replace placeholders with input names
|
||||||
task_description = placeholder_pattern.sub(
|
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(
|
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"Task Description: {task_description}")
|
||||||
context_texts.append(f"Expected Output: {expected_output}")
|
context_texts.append(f"Expected Output: {expected_output}")
|
||||||
for agent in crew.agents:
|
for agent in crew.agents:
|
||||||
# Replace placeholders with input names
|
# Replace placeholders with input names
|
||||||
agent_role = placeholder_pattern.sub(lambda m: m.group(1), agent.role)
|
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)
|
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)
|
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 Role: {agent_role}")
|
||||||
context_texts.append(f"Agent Goal: {agent_goal}")
|
context_texts.append(f"Agent Goal: {agent_goal}")
|
||||||
context_texts.append(f"Agent Backstory: {agent_backstory}")
|
context_texts.append(f"Agent Backstory: {agent_backstory}")
|
||||||
|
|||||||
1
src/crewai/cli/templates/crew/.gitignore
vendored
1
src/crewai/cli/templates/crew/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
.env
|
.env
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
.DS_Store
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
|||||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crewai[tools]>=0.95.0,<1.0.0"
|
"crewai[tools]>=0.98.0,<1.0.0"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
1
src/crewai/cli/templates/flow/.gitignore
vendored
1
src/crewai/cli/templates/flow/.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
.env
|
.env
|
||||||
__pycache__/
|
__pycache__/
|
||||||
lib/
|
lib/
|
||||||
|
.DS_Store
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from random import randint
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
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
|
from {{folder_name}}.crews.poem_crew.poem_crew import PoemCrew
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
|||||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crewai[tools]>=0.95.0,<1.0.0",
|
"crewai[tools]>=0.98.0,<1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crewai[tools]>=0.95.0"
|
"crewai[tools]>=0.98.0"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.crewai]
|
[tool.crewai]
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ from crewai.tasks.task_output import TaskOutput
|
|||||||
from crewai.telemetry import Telemetry
|
from crewai.telemetry import Telemetry
|
||||||
from crewai.tools.agent_tools.agent_tools import AgentTools
|
from crewai.tools.agent_tools.agent_tools import AgentTools
|
||||||
from crewai.tools.base_tool import Tool
|
from crewai.tools.base_tool import Tool
|
||||||
from crewai.types.crew_chat import ChatInputs
|
|
||||||
from crewai.types.usage_metrics import UsageMetrics
|
from crewai.types.usage_metrics import UsageMetrics
|
||||||
from crewai.utilities import I18N, FileHandler, Logger, RPMController
|
from crewai.utilities import I18N, FileHandler, Logger, RPMController
|
||||||
from crewai.utilities.constants import TRAINING_DATA_FILE
|
from crewai.utilities.constants import TRAINING_DATA_FILE
|
||||||
@@ -84,6 +83,7 @@ class Crew(BaseModel):
|
|||||||
step_callback: Callback to be executed after each step for every agents execution.
|
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.
|
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.
|
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
|
__hash__ = object.__hash__ # type: ignore
|
||||||
|
|||||||
@@ -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"]
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
import uuid
|
import logging
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
@@ -13,7 +13,6 @@ from typing import (
|
|||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
overload,
|
|
||||||
)
|
)
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
@@ -27,54 +26,68 @@ from crewai.flow.flow_events import (
|
|||||||
MethodExecutionStartedEvent,
|
MethodExecutionStartedEvent,
|
||||||
)
|
)
|
||||||
from crewai.flow.flow_visualizer import plot_flow
|
from crewai.flow.flow_visualizer import plot_flow
|
||||||
from crewai.flow.persistence import FlowPersistence
|
|
||||||
from crewai.flow.persistence.base import FlowPersistence
|
from crewai.flow.persistence.base import FlowPersistence
|
||||||
from crewai.flow.utils import get_possible_return_constants
|
from crewai.flow.utils import get_possible_return_constants
|
||||||
from crewai.telemetry import Telemetry
|
from crewai.telemetry import Telemetry
|
||||||
|
from crewai.utilities.printer import Printer
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class FlowState(BaseModel):
|
class FlowState(BaseModel):
|
||||||
"""Base model for all flow states, ensuring each state has a unique ID."""
|
"""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
|
# Type variables with explicit bounds
|
||||||
T = TypeVar("T", bound=Union[Dict[str, Any], BaseModel]) # Generic flow state type parameter
|
T = TypeVar(
|
||||||
StateT = TypeVar("StateT", bound=Union[Dict[str, Any], BaseModel]) # State validation type parameter
|
"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:
|
def ensure_state_type(state: Any, expected_type: Type[StateT]) -> StateT:
|
||||||
"""Ensure state matches expected type with proper validation.
|
"""Ensure state matches expected type with proper validation.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
state: State instance to validate
|
state: State instance to validate
|
||||||
expected_type: Expected type for the state
|
expected_type: Expected type for the state
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Validated state instance
|
Validated state instance
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
TypeError: If state doesn't match expected type
|
TypeError: If state doesn't match expected type
|
||||||
ValueError: If state validation fails
|
ValueError: If state validation fails
|
||||||
"""
|
"""
|
||||||
"""Ensure state matches expected type with proper validation.
|
"""Ensure state matches expected type with proper validation.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
state: State instance to validate
|
state: State instance to validate
|
||||||
expected_type: Expected type for the state
|
expected_type: Expected type for the state
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Validated state instance
|
Validated state instance
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
TypeError: If state doesn't match expected type
|
TypeError: If state doesn't match expected type
|
||||||
ValueError: If state validation fails
|
ValueError: If state validation fails
|
||||||
"""
|
"""
|
||||||
if expected_type == dict:
|
if expected_type is dict:
|
||||||
if not isinstance(state, dict):
|
if not isinstance(state, dict):
|
||||||
raise TypeError(f"Expected dict, got {type(state).__name__}")
|
raise TypeError(f"Expected dict, got {type(state).__name__}")
|
||||||
return cast(StateT, state)
|
return cast(StateT, state)
|
||||||
if isinstance(expected_type, type) and issubclass(expected_type, BaseModel):
|
if isinstance(expected_type, type) and issubclass(expected_type, BaseModel):
|
||||||
if not isinstance(state, expected_type):
|
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)
|
return cast(StateT, state)
|
||||||
raise TypeError(f"Invalid expected_type: {expected_type}")
|
raise TypeError(f"Invalid expected_type: {expected_type}")
|
||||||
|
|
||||||
@@ -120,6 +133,7 @@ def start(condition: Optional[Union[str, dict, Callable]] = None) -> Callable:
|
|||||||
>>> def complex_start(self):
|
>>> def complex_start(self):
|
||||||
... pass
|
... pass
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
func.__is_start_method__ = True
|
func.__is_start_method__ = True
|
||||||
if condition is not None:
|
if condition is not None:
|
||||||
@@ -144,6 +158,7 @@ def start(condition: Optional[Union[str, dict, Callable]] = None) -> Callable:
|
|||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def listen(condition: Union[str, dict, Callable]) -> Callable:
|
def listen(condition: Union[str, dict, Callable]) -> Callable:
|
||||||
"""
|
"""
|
||||||
Creates a listener that executes when specified conditions are met.
|
Creates a listener that executes when specified conditions are met.
|
||||||
@@ -180,6 +195,7 @@ def listen(condition: Union[str, dict, Callable]) -> Callable:
|
|||||||
>>> def handle_completion(self):
|
>>> def handle_completion(self):
|
||||||
... pass
|
... pass
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
if isinstance(condition, str):
|
if isinstance(condition, str):
|
||||||
func.__trigger_methods__ = [condition]
|
func.__trigger_methods__ = [condition]
|
||||||
@@ -244,6 +260,7 @@ def router(condition: Union[str, dict, Callable]) -> Callable:
|
|||||||
... return CONTINUE
|
... return CONTINUE
|
||||||
... return STOP
|
... return STOP
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
func.__is_router__ = True
|
func.__is_router__ = True
|
||||||
if isinstance(condition, str):
|
if isinstance(condition, str):
|
||||||
@@ -267,6 +284,7 @@ def router(condition: Union[str, dict, Callable]) -> Callable:
|
|||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def or_(*conditions: Union[str, dict, Callable]) -> dict:
|
def or_(*conditions: Union[str, dict, Callable]) -> dict:
|
||||||
"""
|
"""
|
||||||
Combines multiple conditions with OR logic for flow control.
|
Combines multiple conditions with OR logic for flow control.
|
||||||
@@ -370,22 +388,27 @@ class FlowMeta(type):
|
|||||||
|
|
||||||
for attr_name, attr_value in dct.items():
|
for attr_name, attr_value in dct.items():
|
||||||
# Check for any flow-related attributes
|
# Check for any flow-related attributes
|
||||||
if (hasattr(attr_value, "__is_flow_method__") or
|
if (
|
||||||
hasattr(attr_value, "__is_start_method__") or
|
hasattr(attr_value, "__is_flow_method__")
|
||||||
hasattr(attr_value, "__trigger_methods__") or
|
or hasattr(attr_value, "__is_start_method__")
|
||||||
hasattr(attr_value, "__is_router__")):
|
or hasattr(attr_value, "__trigger_methods__")
|
||||||
|
or hasattr(attr_value, "__is_router__")
|
||||||
|
):
|
||||||
|
|
||||||
# Register start methods
|
# Register start methods
|
||||||
if hasattr(attr_value, "__is_start_method__"):
|
if hasattr(attr_value, "__is_start_method__"):
|
||||||
start_methods.append(attr_name)
|
start_methods.append(attr_name)
|
||||||
|
|
||||||
# Register listeners and routers
|
# Register listeners and routers
|
||||||
if hasattr(attr_value, "__trigger_methods__"):
|
if hasattr(attr_value, "__trigger_methods__"):
|
||||||
methods = attr_value.__trigger_methods__
|
methods = attr_value.__trigger_methods__
|
||||||
condition_type = getattr(attr_value, "__condition_type__", "OR")
|
condition_type = getattr(attr_value, "__condition_type__", "OR")
|
||||||
listeners[attr_name] = (condition_type, methods)
|
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)
|
routers.add(attr_name)
|
||||||
possible_returns = get_possible_return_constants(attr_value)
|
possible_returns = get_possible_return_constants(attr_value)
|
||||||
if possible_returns:
|
if possible_returns:
|
||||||
@@ -401,9 +424,11 @@ class FlowMeta(type):
|
|||||||
|
|
||||||
class Flow(Generic[T], metaclass=FlowMeta):
|
class Flow(Generic[T], metaclass=FlowMeta):
|
||||||
"""Base class for all flows.
|
"""Base class for all flows.
|
||||||
|
|
||||||
Type parameter T must be either Dict[str, Any] or a subclass of BaseModel."""
|
Type parameter T must be either Dict[str, Any] or a subclass of BaseModel."""
|
||||||
|
|
||||||
_telemetry = Telemetry()
|
_telemetry = Telemetry()
|
||||||
|
_printer = Printer()
|
||||||
|
|
||||||
_start_methods: List[str] = []
|
_start_methods: List[str] = []
|
||||||
_listeners: Dict[str, tuple[str, List[str]]] = {}
|
_listeners: Dict[str, tuple[str, List[str]]] = {}
|
||||||
@@ -422,14 +447,12 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
persistence: Optional[FlowPersistence] = None,
|
persistence: Optional[FlowPersistence] = None,
|
||||||
restore_uuid: Optional[str] = None,
|
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a new Flow instance.
|
"""Initialize a new Flow instance.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
persistence: Optional persistence backend for storing flow states
|
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
|
**kwargs: Additional state values to initialize or override
|
||||||
"""
|
"""
|
||||||
# Initialize basic instance attributes
|
# Initialize basic instance attributes
|
||||||
@@ -438,54 +461,13 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
|||||||
self._pending_and_listeners: Dict[str, Set[str]] = {}
|
self._pending_and_listeners: Dict[str, Set[str]] = {}
|
||||||
self._method_outputs: List[Any] = [] # List to store all method outputs
|
self._method_outputs: List[Any] = [] # List to store all method outputs
|
||||||
self._persistence: Optional[FlowPersistence] = persistence
|
self._persistence: Optional[FlowPersistence] = persistence
|
||||||
|
|
||||||
# Validate state model before initialization
|
# Initialize state with initial values
|
||||||
if isinstance(self.initial_state, type):
|
self._state = self._create_initial_state()
|
||||||
if issubclass(self.initial_state, BaseModel) and not issubclass(self.initial_state, FlowState):
|
|
||||||
# Check if model has id field
|
# Apply any additional kwargs
|
||||||
model_fields = getattr(self.initial_state, "model_fields", None)
|
if kwargs:
|
||||||
if not model_fields or "id" not in model_fields:
|
self._initialize_state(kwargs)
|
||||||
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)
|
|
||||||
|
|
||||||
self._telemetry.flow_creation_span(self.__class__.__name__)
|
self._telemetry.flow_creation_span(self.__class__.__name__)
|
||||||
|
|
||||||
@@ -494,23 +476,23 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
|||||||
if not method_name.startswith("_"):
|
if not method_name.startswith("_"):
|
||||||
method = getattr(self, method_name)
|
method = getattr(self, method_name)
|
||||||
# Check for any flow-related attributes
|
# Check for any flow-related attributes
|
||||||
if (hasattr(method, "__is_flow_method__") or
|
if (
|
||||||
hasattr(method, "__is_start_method__") or
|
hasattr(method, "__is_flow_method__")
|
||||||
hasattr(method, "__trigger_methods__") or
|
or hasattr(method, "__is_start_method__")
|
||||||
hasattr(method, "__is_router__")):
|
or hasattr(method, "__trigger_methods__")
|
||||||
|
or hasattr(method, "__is_router__")
|
||||||
|
):
|
||||||
# Ensure method is bound to this instance
|
# Ensure method is bound to this instance
|
||||||
if not hasattr(method, "__self__"):
|
if not hasattr(method, "__self__"):
|
||||||
method = method.__get__(self, self.__class__)
|
method = method.__get__(self, self.__class__)
|
||||||
self._methods[method_name] = method
|
self._methods[method_name] = method
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _create_initial_state(self) -> T:
|
def _create_initial_state(self) -> T:
|
||||||
"""Create and initialize flow state with UUID and default values.
|
"""Create and initialize flow state with UUID and default values.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
New state instance with UUID and default values initialized
|
New state instance with UUID and default values initialized
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If structured state model lacks 'id' field
|
ValueError: If structured state model lacks 'id' field
|
||||||
TypeError: If state is neither BaseModel nor dictionary
|
TypeError: If state is neither BaseModel nor dictionary
|
||||||
@@ -522,24 +504,25 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
|||||||
if issubclass(state_type, FlowState):
|
if issubclass(state_type, FlowState):
|
||||||
# Create instance without id, then set it
|
# Create instance without id, then set it
|
||||||
instance = state_type()
|
instance = state_type()
|
||||||
if not hasattr(instance, 'id'):
|
if not hasattr(instance, "id"):
|
||||||
setattr(instance, 'id', str(uuid4()))
|
setattr(instance, "id", str(uuid4()))
|
||||||
return cast(T, instance)
|
return cast(T, instance)
|
||||||
elif issubclass(state_type, BaseModel):
|
elif issubclass(state_type, BaseModel):
|
||||||
# Create a new type that includes the ID field
|
# Create a new type that includes the ID field
|
||||||
class StateWithId(state_type, FlowState): # type: ignore
|
class StateWithId(state_type, FlowState): # type: ignore
|
||||||
pass
|
pass
|
||||||
|
|
||||||
instance = StateWithId()
|
instance = StateWithId()
|
||||||
if not hasattr(instance, 'id'):
|
if not hasattr(instance, "id"):
|
||||||
setattr(instance, 'id', str(uuid4()))
|
setattr(instance, "id", str(uuid4()))
|
||||||
return cast(T, instance)
|
return cast(T, instance)
|
||||||
elif state_type == dict:
|
elif state_type is dict:
|
||||||
return cast(T, {"id": str(uuid4())}) # Minimal dict state
|
return cast(T, {"id": str(uuid4())})
|
||||||
|
|
||||||
# Handle case where no initial state is provided
|
# Handle case where no initial state is provided
|
||||||
if self.initial_state is None:
|
if self.initial_state is None:
|
||||||
return cast(T, {"id": str(uuid4())})
|
return cast(T, {"id": str(uuid4())})
|
||||||
|
|
||||||
# Handle case where initial_state is a type (class)
|
# Handle case where initial_state is a type (class)
|
||||||
if isinstance(self.initial_state, type):
|
if isinstance(self.initial_state, type):
|
||||||
if issubclass(self.initial_state, FlowState):
|
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:
|
if not model_fields or "id" not in model_fields:
|
||||||
raise ValueError("Flow state model must have an 'id' field")
|
raise ValueError("Flow state model must have an 'id' field")
|
||||||
return cast(T, self.initial_state()) # Uses model defaults
|
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())})
|
return cast(T, {"id": str(uuid4())})
|
||||||
|
|
||||||
# Handle dictionary instance case
|
# Handle dictionary instance case
|
||||||
if isinstance(self.initial_state, dict):
|
if isinstance(self.initial_state, dict):
|
||||||
new_state = dict(self.initial_state) # Copy to avoid mutations
|
new_state = dict(self.initial_state) # Copy to avoid mutations
|
||||||
if "id" not in new_state:
|
if "id" not in new_state:
|
||||||
new_state["id"] = str(uuid4())
|
new_state["id"] = str(uuid4())
|
||||||
return cast(T, new_state)
|
return cast(T, new_state)
|
||||||
|
|
||||||
# Handle BaseModel instance case
|
# Handle BaseModel instance case
|
||||||
if isinstance(self.initial_state, BaseModel):
|
if isinstance(self.initial_state, BaseModel):
|
||||||
model = cast(BaseModel, self.initial_state)
|
model = cast(BaseModel, self.initial_state)
|
||||||
if not hasattr(model, "id"):
|
if not hasattr(model, "id"):
|
||||||
raise ValueError("Flow state model must have an 'id' field")
|
raise ValueError("Flow state model must have an 'id' field")
|
||||||
|
|
||||||
# Create new instance with same values to avoid mutations
|
# Create new instance with same values to avoid mutations
|
||||||
if hasattr(model, "model_dump"):
|
if hasattr(model, "model_dump"):
|
||||||
# Pydantic v2
|
# Pydantic v2
|
||||||
@@ -576,60 +559,12 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
|||||||
else:
|
else:
|
||||||
# Fallback for other BaseModel implementations
|
# Fallback for other BaseModel implementations
|
||||||
state_dict = {
|
state_dict = {
|
||||||
k: v for k, v in model.__dict__.items()
|
k: v for k, v in model.__dict__.items() if not k.startswith("_")
|
||||||
if not k.startswith("_")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create new instance of the same class
|
# Create new instance of the same class
|
||||||
model_class = type(model)
|
model_class = type(model)
|
||||||
return cast(T, model_class(**state_dict))
|
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(
|
raise TypeError(
|
||||||
f"Initial state must be dict or BaseModel, got {type(self.initial_state)}"
|
f"Initial state must be dict or BaseModel, got {type(self.initial_state)}"
|
||||||
)
|
)
|
||||||
@@ -643,12 +578,45 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
|||||||
"""Returns the list of all outputs from executed methods."""
|
"""Returns the list of all outputs from executed methods."""
|
||||||
return self._method_outputs
|
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:
|
def _initialize_state(self, inputs: Dict[str, Any]) -> None:
|
||||||
"""Initialize or update flow state with new inputs.
|
"""Initialize or update flow state with new inputs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
inputs: Dictionary of state values to set/update
|
inputs: Dictionary of state values to set/update
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If validation fails for structured state
|
ValueError: If validation fails for structured state
|
||||||
TypeError: If state is neither BaseModel nor dictionary
|
TypeError: If state is neither BaseModel nor dictionary
|
||||||
@@ -675,13 +643,12 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
|||||||
current_state = model.dict()
|
current_state = model.dict()
|
||||||
else:
|
else:
|
||||||
current_state = {
|
current_state = {
|
||||||
k: v for k, v in model.__dict__.items()
|
k: v for k, v in model.__dict__.items() if not k.startswith("_")
|
||||||
if not k.startswith("_")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create new state with preserved fields and updates
|
# Create new state with preserved fields and updates
|
||||||
new_state = {**current_state, **inputs}
|
new_state = {**current_state, **inputs}
|
||||||
|
|
||||||
# Create new instance with merged state
|
# Create new instance with merged state
|
||||||
model_class = type(model)
|
model_class = type(model)
|
||||||
if hasattr(model_class, "model_validate"):
|
if hasattr(model_class, "model_validate"):
|
||||||
@@ -697,13 +664,13 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
|||||||
raise ValueError(f"Invalid inputs for structured state: {e}") from e
|
raise ValueError(f"Invalid inputs for structured state: {e}") from e
|
||||||
else:
|
else:
|
||||||
raise TypeError("State must be a BaseModel instance or a dictionary.")
|
raise TypeError("State must be a BaseModel instance or a dictionary.")
|
||||||
|
|
||||||
def _restore_state(self, stored_state: Dict[str, Any]) -> None:
|
def _restore_state(self, stored_state: Dict[str, Any]) -> None:
|
||||||
"""Restore flow state from persistence.
|
"""Restore flow state from persistence.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
stored_state: Previously stored state to restore
|
stored_state: Previously stored state to restore
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If validation fails for structured state
|
ValueError: If validation fails for structured state
|
||||||
TypeError: If state is neither BaseModel nor dictionary
|
TypeError: If state is neither BaseModel nor dictionary
|
||||||
@@ -712,7 +679,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
|||||||
stored_id = stored_state.get("id")
|
stored_id = stored_state.get("id")
|
||||||
if not stored_id:
|
if not stored_id:
|
||||||
raise ValueError("Stored state must have an 'id' field")
|
raise ValueError("Stored state must have an 'id' field")
|
||||||
|
|
||||||
if isinstance(self._state, dict):
|
if isinstance(self._state, dict):
|
||||||
# For dict states, update all fields from stored state
|
# For dict states, update all fields from stored state
|
||||||
self._state.clear()
|
self._state.clear()
|
||||||
@@ -730,11 +697,39 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
|||||||
# Fallback for other BaseModel implementations
|
# Fallback for other BaseModel implementations
|
||||||
self._state = cast(T, type(model)(**stored_state))
|
self._state = cast(T, type(model)(**stored_state))
|
||||||
else:
|
else:
|
||||||
raise TypeError(
|
raise TypeError(f"State must be dict or BaseModel, got {type(self._state)}")
|
||||||
f"State must be dict or BaseModel, got {type(self._state)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def kickoff(self, inputs: Optional[Dict[str, Any]] = None) -> Any:
|
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_emitter.send(
|
||||||
self,
|
self,
|
||||||
event=FlowStartedEvent(
|
event=FlowStartedEvent(
|
||||||
@@ -742,9 +737,11 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
|||||||
flow_name=self.__class__.__name__,
|
flow_name=self.__class__.__name__,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
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)
|
self._initialize_state(inputs)
|
||||||
|
|
||||||
return asyncio.run(self.kickoff_async())
|
return asyncio.run(self.kickoff_async())
|
||||||
|
|
||||||
async def kickoff_async(self, inputs: Optional[Dict[str, Any]] = None) -> Any:
|
async def kickoff_async(self, inputs: Optional[Dict[str, Any]] = None) -> Any:
|
||||||
@@ -987,6 +984,30 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
|||||||
|
|
||||||
traceback.print_exc()
|
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:
|
def plot(self, filename: str = "crewai_flow") -> None:
|
||||||
self._telemetry.flow_plotting_span(
|
self._telemetry.flow_plotting_span(
|
||||||
self.__class__.__name__, list(self._methods.keys())
|
self.__class__.__name__, list(self._methods.keys())
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ Example:
|
|||||||
```python
|
```python
|
||||||
from crewai.flow.flow import Flow, start
|
from crewai.flow.flow import Flow, start
|
||||||
from crewai.flow.persistence import persist, SQLiteFlowPersistence
|
from crewai.flow.persistence import persist, SQLiteFlowPersistence
|
||||||
|
|
||||||
class MyFlow(Flow):
|
class MyFlow(Flow):
|
||||||
@start()
|
@start()
|
||||||
@persist(SQLiteFlowPersistence())
|
@persist(SQLiteFlowPersistence())
|
||||||
def sync_method(self):
|
def sync_method(self):
|
||||||
# Synchronous method implementation
|
# Synchronous method implementation
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@start()
|
@start()
|
||||||
@persist(SQLiteFlowPersistence())
|
@persist(SQLiteFlowPersistence())
|
||||||
async def async_method(self):
|
async def async_method(self):
|
||||||
@@ -23,48 +23,117 @@ Example:
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
|
||||||
import logging
|
import logging
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
Dict,
|
|
||||||
Optional,
|
Optional,
|
||||||
Type,
|
Type,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
get_type_hints,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from crewai.flow.persistence.base import FlowPersistence
|
from crewai.flow.persistence.base import FlowPersistence
|
||||||
from crewai.flow.persistence.sqlite import SQLiteFlowPersistence
|
from crewai.flow.persistence.sqlite import SQLiteFlowPersistence
|
||||||
|
from crewai.utilities.printer import Printer
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
T = TypeVar("T")
|
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):
|
def persist(persistence: Optional[FlowPersistence] = None):
|
||||||
"""Decorator to persist flow state.
|
"""Decorator to persist flow state.
|
||||||
|
|
||||||
This decorator can be applied at either the class level or method level.
|
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
|
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
|
states. When applied at the method level, it persists only that method's
|
||||||
state.
|
state.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
persistence: Optional FlowPersistence implementation to use.
|
persistence: Optional FlowPersistence implementation to use.
|
||||||
If not provided, uses SQLiteFlowPersistence.
|
If not provided, uses SQLiteFlowPersistence.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A decorator that can be applied to either a class or method
|
A decorator that can be applied to either a class or method
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If the flow state doesn't have an 'id' field
|
ValueError: If the flow state doesn't have an 'id' field
|
||||||
RuntimeError: If state persistence fails
|
RuntimeError: If state persistence fails
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
@persist # Class-level persistence with default SQLite
|
@persist # Class-level persistence with default SQLite
|
||||||
class MyFlow(Flow[MyState]):
|
class MyFlow(Flow[MyState]):
|
||||||
@@ -72,81 +141,85 @@ def persist(persistence: Optional[FlowPersistence] = None):
|
|||||||
def begin(self):
|
def begin(self):
|
||||||
pass
|
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]]:
|
def decorator(target: Union[Type, Callable[..., T]]) -> Union[Type, Callable[..., T]]:
|
||||||
"""Decorator that handles both class and method decoration."""
|
"""Decorator that handles both class and method decoration."""
|
||||||
actual_persistence = persistence or SQLiteFlowPersistence()
|
actual_persistence = persistence or SQLiteFlowPersistence()
|
||||||
|
|
||||||
if isinstance(target, type):
|
if isinstance(target, type):
|
||||||
# Class decoration
|
# 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():
|
for name, method in target.__dict__.items():
|
||||||
if callable(method) and hasattr(method, "__is_flow_method__"):
|
if callable(method) and (
|
||||||
# Wrap each flow method with persistence
|
hasattr(method, "__is_start_method__") or
|
||||||
if asyncio.iscoroutinefunction(method):
|
hasattr(method, "__trigger_methods__") or
|
||||||
@functools.wraps(method)
|
hasattr(method, "__condition_type__") or
|
||||||
async def class_async_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
hasattr(method, "__is_flow_method__") or
|
||||||
method_coro = method(self, *args, **kwargs)
|
hasattr(method, "__is_router__")
|
||||||
if asyncio.iscoroutine(method_coro):
|
):
|
||||||
result = await method_coro
|
original_methods[name] = method
|
||||||
else:
|
|
||||||
result = method_coro
|
# Create wrapped versions of the methods that include persistence
|
||||||
_persist_state(self, method.__name__, actual_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
|
return result
|
||||||
class_methods[name] = class_async_wrapper
|
return method_wrapper
|
||||||
else:
|
|
||||||
@functools.wraps(method)
|
wrapped = create_async_wrapper(name, method)
|
||||||
def class_sync_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
||||||
result = method(self, *args, **kwargs)
|
# Preserve all original decorators and attributes
|
||||||
_persist_state(self, method.__name__, actual_persistence)
|
|
||||||
return result
|
|
||||||
class_methods[name] = class_sync_wrapper
|
|
||||||
|
|
||||||
# Preserve flow-specific attributes
|
|
||||||
for attr in ["__is_start_method__", "__trigger_methods__", "__condition_type__", "__is_router__"]:
|
for attr in ["__is_start_method__", "__trigger_methods__", "__condition_type__", "__is_router__"]:
|
||||||
if hasattr(method, attr):
|
if hasattr(method, attr):
|
||||||
setattr(class_methods[name], attr, getattr(method, attr))
|
setattr(wrapped, attr, getattr(method, attr))
|
||||||
setattr(class_methods[name], "__is_flow_method__", True)
|
setattr(wrapped, "__is_flow_method__", True)
|
||||||
|
|
||||||
# Update class with wrapped methods
|
# Update the class with the wrapped method
|
||||||
for name, method in class_methods.items():
|
setattr(target, name, wrapped)
|
||||||
setattr(target, name, method)
|
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
|
return target
|
||||||
else:
|
else:
|
||||||
# Method decoration
|
# Method decoration
|
||||||
method = target
|
method = target
|
||||||
setattr(method, "__is_flow_method__", True)
|
setattr(method, "__is_flow_method__", True)
|
||||||
|
|
||||||
if asyncio.iscoroutinefunction(method):
|
if asyncio.iscoroutinefunction(method):
|
||||||
@functools.wraps(method)
|
@functools.wraps(method)
|
||||||
async def method_async_wrapper(flow_instance: Any, *args: Any, **kwargs: Any) -> T:
|
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
|
result = await method_coro
|
||||||
else:
|
else:
|
||||||
result = method_coro
|
result = method_coro
|
||||||
_persist_state(flow_instance, method.__name__, actual_persistence)
|
PersistenceDecorator.persist_state(flow_instance, method.__name__, actual_persistence)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
for attr in ["__is_start_method__", "__trigger_methods__", "__condition_type__", "__is_router__"]:
|
for attr in ["__is_start_method__", "__trigger_methods__", "__condition_type__", "__is_router__"]:
|
||||||
if hasattr(method, attr):
|
if hasattr(method, attr):
|
||||||
setattr(method_async_wrapper, attr, getattr(method, attr))
|
setattr(method_async_wrapper, attr, getattr(method, attr))
|
||||||
@@ -166,12 +240,13 @@ def persist(persistence: Optional[FlowPersistence] = None):
|
|||||||
@functools.wraps(method)
|
@functools.wraps(method)
|
||||||
def method_sync_wrapper(flow_instance: Any, *args: Any, **kwargs: Any) -> T:
|
def method_sync_wrapper(flow_instance: Any, *args: Any, **kwargs: Any) -> T:
|
||||||
result = method(flow_instance, *args, **kwargs)
|
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
|
return result
|
||||||
|
|
||||||
for attr in ["__is_start_method__", "__trigger_methods__", "__condition_type__", "__is_router__"]:
|
for attr in ["__is_start_method__", "__trigger_methods__", "__condition_type__", "__is_router__"]:
|
||||||
if hasattr(method, attr):
|
if hasattr(method, attr):
|
||||||
setattr(method_sync_wrapper, attr, getattr(method, attr))
|
setattr(method_sync_wrapper, attr, getattr(method, attr))
|
||||||
setattr(method_sync_wrapper, "__is_flow_method__", True)
|
setattr(method_sync_wrapper, "__is_flow_method__", True)
|
||||||
return cast(Callable[..., T], method_sync_wrapper)
|
return cast(Callable[..., T], method_sync_wrapper)
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ SQLite-based implementation of flow state persistence.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import tempfile
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional, Union
|
from typing import Any, Dict, Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -16,34 +15,34 @@ from crewai.flow.persistence.base import FlowPersistence
|
|||||||
|
|
||||||
class SQLiteFlowPersistence(FlowPersistence):
|
class SQLiteFlowPersistence(FlowPersistence):
|
||||||
"""SQLite-based implementation of flow state persistence.
|
"""SQLite-based implementation of flow state persistence.
|
||||||
|
|
||||||
This class provides a simple, file-based persistence implementation using SQLite.
|
This class provides a simple, file-based persistence implementation using SQLite.
|
||||||
It's suitable for development and testing, or for production use cases with
|
It's suitable for development and testing, or for production use cases with
|
||||||
moderate performance requirements.
|
moderate performance requirements.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
db_path: str # Type annotation for instance variable
|
db_path: str # Type annotation for instance variable
|
||||||
|
|
||||||
def __init__(self, db_path: Optional[str] = None):
|
def __init__(self, db_path: Optional[str] = None):
|
||||||
"""Initialize SQLite persistence.
|
"""Initialize SQLite persistence.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db_path: Path to the SQLite database file. If not provided, uses
|
db_path: Path to the SQLite database file. If not provided, uses
|
||||||
db_storage_path() from utilities.paths.
|
db_storage_path() from utilities.paths.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If db_path is invalid
|
ValueError: If db_path is invalid
|
||||||
"""
|
"""
|
||||||
from crewai.utilities.paths import db_storage_path
|
from crewai.utilities.paths import db_storage_path
|
||||||
# Get path from argument or default location
|
# 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:
|
if not path:
|
||||||
raise ValueError("Database path must be provided")
|
raise ValueError("Database path must be provided")
|
||||||
|
|
||||||
self.db_path = path # Now mypy knows this is str
|
self.db_path = path # Now mypy knows this is str
|
||||||
self.init_db()
|
self.init_db()
|
||||||
|
|
||||||
def init_db(self) -> None:
|
def init_db(self) -> None:
|
||||||
"""Create the necessary tables if they don't exist."""
|
"""Create the necessary tables if they don't exist."""
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
@@ -58,10 +57,10 @@ class SQLiteFlowPersistence(FlowPersistence):
|
|||||||
""")
|
""")
|
||||||
# Add index for faster UUID lookups
|
# Add index for faster UUID lookups
|
||||||
conn.execute("""
|
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)
|
ON flow_states(flow_uuid)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
def save_state(
|
def save_state(
|
||||||
self,
|
self,
|
||||||
flow_uuid: str,
|
flow_uuid: str,
|
||||||
@@ -69,7 +68,7 @@ class SQLiteFlowPersistence(FlowPersistence):
|
|||||||
state_data: Union[Dict[str, Any], BaseModel],
|
state_data: Union[Dict[str, Any], BaseModel],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Save the current flow state to SQLite.
|
"""Save the current flow state to SQLite.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
flow_uuid: Unique identifier for the flow instance
|
flow_uuid: Unique identifier for the flow instance
|
||||||
method_name: Name of the method that just completed
|
method_name: Name of the method that just completed
|
||||||
@@ -84,7 +83,7 @@ class SQLiteFlowPersistence(FlowPersistence):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"state_data must be either a Pydantic BaseModel or dict, got {type(state_data)}"
|
f"state_data must be either a Pydantic BaseModel or dict, got {type(state_data)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
INSERT INTO flow_states (
|
INSERT INTO flow_states (
|
||||||
@@ -99,13 +98,13 @@ class SQLiteFlowPersistence(FlowPersistence):
|
|||||||
datetime.utcnow().isoformat(),
|
datetime.utcnow().isoformat(),
|
||||||
json.dumps(state_dict),
|
json.dumps(state_dict),
|
||||||
))
|
))
|
||||||
|
|
||||||
def load_state(self, flow_uuid: str) -> Optional[Dict[str, Any]]:
|
def load_state(self, flow_uuid: str) -> Optional[Dict[str, Any]]:
|
||||||
"""Load the most recent state for a given flow UUID.
|
"""Load the most recent state for a given flow UUID.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
flow_uuid: Unique identifier for the flow instance
|
flow_uuid: Unique identifier for the flow instance
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The most recent state as a dictionary, or None if no state exists
|
The most recent state as a dictionary, or None if no state exists
|
||||||
"""
|
"""
|
||||||
@@ -118,7 +117,7 @@ class SQLiteFlowPersistence(FlowPersistence):
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
""", (flow_uuid,))
|
""", (flow_uuid,))
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
|
|
||||||
if row:
|
if row:
|
||||||
return json.loads(row[0])
|
return json.loads(row[0])
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -142,7 +142,6 @@ class LLM:
|
|||||||
self.temperature = temperature
|
self.temperature = temperature
|
||||||
self.top_p = top_p
|
self.top_p = top_p
|
||||||
self.n = n
|
self.n = n
|
||||||
self.stop = stop
|
|
||||||
self.max_completion_tokens = max_completion_tokens
|
self.max_completion_tokens = max_completion_tokens
|
||||||
self.max_tokens = max_tokens
|
self.max_tokens = max_tokens
|
||||||
self.presence_penalty = presence_penalty
|
self.presence_penalty = presence_penalty
|
||||||
@@ -160,37 +159,63 @@ class LLM:
|
|||||||
|
|
||||||
litellm.drop_params = True
|
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_callbacks(callbacks)
|
||||||
self.set_env_callbacks()
|
self.set_env_callbacks()
|
||||||
|
|
||||||
def call(
|
def call(
|
||||||
self,
|
self,
|
||||||
messages: List[Dict[str, str]],
|
messages: Union[str, List[Dict[str, str]]],
|
||||||
tools: Optional[List[dict]] = None,
|
tools: Optional[List[dict]] = None,
|
||||||
callbacks: Optional[List[Any]] = None,
|
callbacks: Optional[List[Any]] = None,
|
||||||
available_functions: Optional[Dict[str, Any]] = None,
|
available_functions: Optional[Dict[str, Any]] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
High-level call method that:
|
High-level llm call method that:
|
||||||
1) Calls litellm.completion
|
1) Accepts either a string or a list of messages
|
||||||
2) Checks for function/tool calls
|
2) Converts string input to the required message format
|
||||||
3) If a tool call is found:
|
3) Calls litellm.completion
|
||||||
a) executes the function
|
4) Handles function/tool calls if any
|
||||||
b) returns the result
|
5) Returns the final text response or tool result
|
||||||
4) If no tool call, returns the text response
|
|
||||||
|
|
||||||
:param messages: The conversation messages
|
Parameters:
|
||||||
:param tools: Optional list of function schemas for function calling
|
- messages (Union[str, List[Dict[str, str]]]): The input messages for the LLM.
|
||||||
:param callbacks: Optional list of callbacks
|
- If a string is provided, it will be converted into a message list with a single entry.
|
||||||
:param available_functions: A dictionary mapping function_name -> actual Python function
|
- If a list of dictionaries is provided, each dictionary should have 'role' and 'content' keys.
|
||||||
:return: Final text response from the LLM or the tool result
|
- tools (Optional[List[dict]]): A list of tool schemas for function calling.
|
||||||
|
- callbacks (Optional[List[Any]]): A list of callback functions to be executed.
|
||||||
|
- available_functions (Optional[Dict[str, Any]]): A dictionary mapping function names to actual Python functions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- str: The final text response from the LLM or the result of a tool function call.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
---------
|
||||||
|
# Example 1: Using a string input
|
||||||
|
response = llm.call("Return the name of a random city in the world.")
|
||||||
|
print(response)
|
||||||
|
|
||||||
|
# Example 2: Using a list of messages
|
||||||
|
messages = [{"role": "user", "content": "What is the capital of France?"}]
|
||||||
|
response = llm.call(messages)
|
||||||
|
print(response)
|
||||||
"""
|
"""
|
||||||
|
if isinstance(messages, str):
|
||||||
|
messages = [{"role": "user", "content": messages}]
|
||||||
|
|
||||||
with suppress_warnings():
|
with suppress_warnings():
|
||||||
if callbacks and len(callbacks) > 0:
|
if callbacks and len(callbacks) > 0:
|
||||||
self.set_callbacks(callbacks)
|
self.set_callbacks(callbacks)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# --- 1) Make the completion call
|
# --- 1) Prepare the parameters for the completion call
|
||||||
params = {
|
params = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
@@ -211,19 +236,21 @@ class LLM:
|
|||||||
"api_version": self.api_version,
|
"api_version": self.api_version,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
"stream": False,
|
"stream": False,
|
||||||
"tools": tools, # pass the tool schema
|
"tools": tools,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Remove None values from params
|
||||||
params = {k: v for k, v in params.items() if v is not None}
|
params = {k: v for k, v in params.items() if v is not None}
|
||||||
|
|
||||||
|
# --- 2) Make the completion call
|
||||||
response = litellm.completion(**params)
|
response = litellm.completion(**params)
|
||||||
response_message = cast(Choices, cast(ModelResponse, response).choices)[
|
response_message = cast(Choices, cast(ModelResponse, response).choices)[
|
||||||
0
|
0
|
||||||
].message
|
].message
|
||||||
text_response = response_message.content or ""
|
text_response = response_message.content or ""
|
||||||
tool_calls = getattr(response_message, "tool_calls", [])
|
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:
|
if callbacks and len(callbacks) > 0:
|
||||||
for callback in callbacks:
|
for callback in callbacks:
|
||||||
if hasattr(callback, "log_success_event"):
|
if hasattr(callback, "log_success_event"):
|
||||||
@@ -236,11 +263,11 @@ class LLM:
|
|||||||
end_time=0,
|
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:
|
if not tool_calls or not available_functions:
|
||||||
return text_response
|
return text_response
|
||||||
|
|
||||||
# --- 3) Handle the tool call
|
# --- 5) Handle the tool call
|
||||||
tool_call = tool_calls[0]
|
tool_call = tool_calls[0]
|
||||||
function_name = tool_call.function.name
|
function_name = tool_call.function.name
|
||||||
|
|
||||||
@@ -255,7 +282,6 @@ class LLM:
|
|||||||
try:
|
try:
|
||||||
# Call the actual tool function
|
# Call the actual tool function
|
||||||
result = fn(**function_args)
|
result = fn(**function_args)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class KickoffTaskOutputsSQLiteStorage:
|
|||||||
) -> None:
|
) -> None:
|
||||||
if db_path is None:
|
if db_path is None:
|
||||||
# Get the parent directory of the default db path and create our db file there
|
# 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.db_path = db_path
|
||||||
self._printer: Printer = Printer()
|
self._printer: Printer = Printer()
|
||||||
self._initialize_db()
|
self._initialize_db()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class LTMSQLiteStorage:
|
|||||||
) -> None:
|
) -> None:
|
||||||
if db_path is None:
|
if db_path is None:
|
||||||
# Get the parent directory of the default db path and create our db file there
|
# 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.db_path = db_path
|
||||||
self._printer: Printer = Printer()
|
self._printer: Printer = Printer()
|
||||||
# Ensure parent directory exists
|
# Ensure parent directory exists
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import ast
|
import ast
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import re
|
|
||||||
import time
|
import time
|
||||||
from difflib import SequenceMatcher
|
from difflib import SequenceMatcher
|
||||||
|
from json import JSONDecodeError
|
||||||
from textwrap import dedent
|
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
|
from json_repair import repair_json
|
||||||
|
|
||||||
import crewai.utilities.events as events
|
import crewai.utilities.events as events
|
||||||
@@ -407,28 +408,55 @@ class ToolUsage:
|
|||||||
)
|
)
|
||||||
return self._tool_calling(tool_string)
|
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:
|
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)
|
arguments = json.loads(tool_input)
|
||||||
except json.JSONDecodeError:
|
if isinstance(arguments, dict):
|
||||||
# Attempt to repair JSON string
|
return arguments
|
||||||
repaired_input = repair_json(tool_input)
|
except (JSONDecodeError, TypeError):
|
||||||
try:
|
pass # Continue to the next parsing attempt
|
||||||
arguments = json.loads(repaired_input)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
raise Exception(f"Invalid tool input JSON: {e}")
|
|
||||||
|
|
||||||
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:
|
def on_tool_error(self, tool: Any, tool_calling: ToolCalling, e: Exception) -> None:
|
||||||
event_data = self._prepare_event_data(tool, tool_calling)
|
event_data = self._prepare_event_data(tool, tool_calling)
|
||||||
|
|||||||
@@ -241,9 +241,13 @@ def generate_model_description(model: Type[BaseModel]) -> str:
|
|||||||
origin = get_origin(field_type)
|
origin = get_origin(field_type)
|
||||||
args = get_args(field_type)
|
args = get_args(field_type)
|
||||||
|
|
||||||
if origin is Union and type(None) in args:
|
if origin is Union or (origin is None and len(args) > 0):
|
||||||
|
# Handle both Union and the new '|' syntax
|
||||||
non_none_args = [arg for arg in args if arg is not type(None)]
|
non_none_args = [arg for arg in args if arg is not type(None)]
|
||||||
return f"Optional[{describe_field(non_none_args[0])}]"
|
if len(non_none_args) == 1:
|
||||||
|
return f"Optional[{describe_field(non_none_args[0])}]"
|
||||||
|
else:
|
||||||
|
return f"Optional[Union[{', '.join(describe_field(arg) for arg in non_none_args)}]]"
|
||||||
elif origin is list:
|
elif origin is list:
|
||||||
return f"List[{describe_field(args[0])}]"
|
return f"List[{describe_field(args[0])}]"
|
||||||
elif origin is dict:
|
elif origin is dict:
|
||||||
@@ -252,8 +256,10 @@ def generate_model_description(model: Type[BaseModel]) -> str:
|
|||||||
return f"Dict[{key_type}, {value_type}]"
|
return f"Dict[{key_type}, {value_type}]"
|
||||||
elif isinstance(field_type, type) and issubclass(field_type, BaseModel):
|
elif isinstance(field_type, type) and issubclass(field_type, BaseModel):
|
||||||
return generate_model_description(field_type)
|
return generate_model_description(field_type)
|
||||||
else:
|
elif hasattr(field_type, "__name__"):
|
||||||
return field_type.__name__
|
return field_type.__name__
|
||||||
|
else:
|
||||||
|
return str(field_type)
|
||||||
|
|
||||||
fields = model.__annotations__
|
fields = model.__annotations__
|
||||||
field_descriptions = [
|
field_descriptions = [
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class EmbeddingConfigurator:
|
|||||||
"vertexai": self._configure_vertexai,
|
"vertexai": self._configure_vertexai,
|
||||||
"google": self._configure_google,
|
"google": self._configure_google,
|
||||||
"cohere": self._configure_cohere,
|
"cohere": self._configure_cohere,
|
||||||
|
"voyageai": self._configure_voyageai,
|
||||||
"bedrock": self._configure_bedrock,
|
"bedrock": self._configure_bedrock,
|
||||||
"huggingface": self._configure_huggingface,
|
"huggingface": self._configure_huggingface,
|
||||||
"watson": self._configure_watson,
|
"watson": self._configure_watson,
|
||||||
@@ -124,6 +125,17 @@ class EmbeddingConfigurator:
|
|||||||
api_key=config.get("api_key"),
|
api_key=config.get("api_key"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _configure_voyageai(config, model_name):
|
||||||
|
from chromadb.utils.embedding_functions.voyageai_embedding_function import (
|
||||||
|
VoyageAIEmbeddingFunction,
|
||||||
|
)
|
||||||
|
|
||||||
|
return VoyageAIEmbeddingFunction(
|
||||||
|
model_name=model_name,
|
||||||
|
api_key=config.get("api_key"),
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _configure_bedrock(config, model_name):
|
def _configure_bedrock(config, model_name):
|
||||||
from chromadb.utils.embedding_functions.amazon_bedrock_embedding_function import (
|
from chromadb.utils.embedding_functions.amazon_bedrock_embedding_function import (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import appdirs
|
|||||||
|
|
||||||
def db_storage_path() -> str:
|
def db_storage_path() -> str:
|
||||||
"""Returns the path for SQLite database storage.
|
"""Returns the path for SQLite database storage.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Full path to the SQLite database file
|
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 = Path(appdirs.user_data_dir(app_name, app_author))
|
||||||
data_dir.mkdir(parents=True, exist_ok=True)
|
data_dir.mkdir(parents=True, exist_ok=True)
|
||||||
return str(data_dir / "crewai_flows.db")
|
return str(data_dir)
|
||||||
|
|
||||||
|
|
||||||
def get_project_directory_name():
|
def get_project_directory_name():
|
||||||
@@ -28,4 +28,4 @@ def get_project_directory_name():
|
|||||||
else:
|
else:
|
||||||
cwd = Path.cwd()
|
cwd = Path.cwd()
|
||||||
project_directory_name = cwd.name
|
project_directory_name = cwd.name
|
||||||
return project_directory_name
|
return project_directory_name
|
||||||
@@ -21,6 +21,16 @@ class Printer:
|
|||||||
self._print_yellow(content)
|
self._print_yellow(content)
|
||||||
elif color == "bold_yellow":
|
elif color == "bold_yellow":
|
||||||
self._print_bold_yellow(content)
|
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:
|
else:
|
||||||
print(content)
|
print(content)
|
||||||
|
|
||||||
@@ -44,3 +54,18 @@ class Printer:
|
|||||||
|
|
||||||
def _print_bold_yellow(self, content):
|
def _print_bold_yellow(self, content):
|
||||||
print("\033[1m\033[93m {}\033[00m".format(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))
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from crewai.tools import tool
|
|||||||
from crewai.tools.tool_calling import InstructorToolCalling
|
from crewai.tools.tool_calling import InstructorToolCalling
|
||||||
from crewai.tools.tool_usage import ToolUsage
|
from crewai.tools.tool_usage import ToolUsage
|
||||||
from crewai.tools.tool_usage_events import ToolUsageFinished
|
from crewai.tools.tool_usage_events import ToolUsageFinished
|
||||||
from crewai.utilities import RPMController
|
from crewai.utilities import Printer, RPMController
|
||||||
from crewai.utilities.events import Emitter
|
from crewai.utilities.events import Emitter
|
||||||
|
|
||||||
|
|
||||||
@@ -114,35 +114,6 @@ def test_custom_llm_temperature_preservation():
|
|||||||
assert agent.llm.temperature == 0.7
|
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"])
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
def test_agent_execution():
|
def test_agent_execution():
|
||||||
agent = Agent(
|
agent = Agent(
|
||||||
@@ -1629,3 +1600,142 @@ def test_agent_with_knowledge_sources():
|
|||||||
|
|
||||||
# Assert that the agent provides the correct information
|
# Assert that the agent provides the correct information
|
||||||
assert "red" in result.raw.lower()
|
assert "red" in result.raw.lower()
|
||||||
|
|
||||||
|
|
||||||
|
@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()
|
||||||
|
|||||||
@@ -2,21 +2,21 @@ interactions:
|
|||||||
- request:
|
- request:
|
||||||
body: '{"messages": [{"role": "system", "content": "You are test role. test backstory\nYour
|
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
|
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:
|
should NEVER make up tools that are not listed here:\n\nTool Name: get_final_answer\nTool
|
||||||
Any, **kwargs: Any) -> Any\nTool Description: get_final_answer() - Get the final
|
Arguments: {}\nTool Description: Get the final answer but don''t give it yet,
|
||||||
answer but don''t give it yet, just re-use this tool non-stop. \nTool
|
just re-use this\n tool non-stop.\n\nIMPORTANT: Use the following format
|
||||||
Arguments: {}\n\nUse the following format:\n\nThought: you should always think
|
in your response:\n\n```\nThought: you should always think about what to do\nAction:
|
||||||
about what to do\nAction: the action to take, only one name of [get_final_answer],
|
the action to take, only one name of [get_final_answer], just the name, exactly
|
||||||
just the name, exactly as it''s written.\nAction Input: the input to the action,
|
as it''s written.\nAction Input: the input to the action, just a simple JSON
|
||||||
just a simple python dictionary, enclosed in curly braces, using \" to wrap
|
object, enclosed in curly braces, using \" to wrap keys and values.\nObservation:
|
||||||
keys and values.\nObservation: the result of the action\n\nOnce all necessary
|
the result of the action\n```\n\nOnce all necessary information is gathered,
|
||||||
information is gathered:\n\nThought: I now know the final answer\nFinal Answer:
|
return the following format:\n\n```\nThought: I now know the final answer\nFinal
|
||||||
the final answer to the original input question\n"}, {"role": "user", "content":
|
Answer: the final answer to the original input question\n```"}, {"role": "user",
|
||||||
"\nCurrent Task: Use the get_final_answer tool.\n\nThis is the expect criteria
|
"content": "\nCurrent Task: Use the get_final_answer tool.\n\nThis is the expect
|
||||||
for your final answer: The final answer\nyou MUST return the actual complete
|
criteria for your final answer: The final answer\nyou MUST return the actual
|
||||||
content as the final answer, not a summary.\n\nBegin! This is VERY important
|
complete content as the final answer, not a summary.\n\nBegin! This is VERY
|
||||||
to you, use the tools available and give your best Final Answer, your job depends
|
important to you, use the tools available and give your best Final Answer, your
|
||||||
on it!\n\nThought:"}], "model": "gpt-4o"}'
|
job depends on it!\n\nThought:"}], "model": "gpt-4o", "stop": ["\nObservation:"]}'
|
||||||
headers:
|
headers:
|
||||||
accept:
|
accept:
|
||||||
- application/json
|
- application/json
|
||||||
@@ -25,16 +25,13 @@ interactions:
|
|||||||
connection:
|
connection:
|
||||||
- keep-alive
|
- keep-alive
|
||||||
content-length:
|
content-length:
|
||||||
- '1325'
|
- '1367'
|
||||||
content-type:
|
content-type:
|
||||||
- application/json
|
- application/json
|
||||||
cookie:
|
|
||||||
- _cfuvid=ePJSDFdHag2D8lj21_ijAMWjoA6xfnPNxN4uekvC728-1727226247743-0.0.1.1-604800000;
|
|
||||||
__cf_bm=3giyBOIM0GNudFELtsBWYXwLrpLBTNLsh81wfXgu2tg-1727226247-1.0.1.1-ugUDz0c5EhmfVpyGtcdedlIWeDGuy2q0tXQTKVpv83HZhvxgBcS7SBL1wS4rapPM38yhfEcfwA79ARt3HQEzKA
|
|
||||||
host:
|
host:
|
||||||
- api.openai.com
|
- api.openai.com
|
||||||
user-agent:
|
user-agent:
|
||||||
- OpenAI/Python 1.47.0
|
- OpenAI/Python 1.59.6
|
||||||
x-stainless-arch:
|
x-stainless-arch:
|
||||||
- arm64
|
- arm64
|
||||||
x-stainless-async:
|
x-stainless-async:
|
||||||
@@ -44,30 +41,35 @@ interactions:
|
|||||||
x-stainless-os:
|
x-stainless-os:
|
||||||
- MacOS
|
- MacOS
|
||||||
x-stainless-package-version:
|
x-stainless-package-version:
|
||||||
- 1.47.0
|
- 1.59.6
|
||||||
x-stainless-raw-response:
|
x-stainless-raw-response:
|
||||||
- 'true'
|
- 'true'
|
||||||
|
x-stainless-retry-count:
|
||||||
|
- '0'
|
||||||
x-stainless-runtime:
|
x-stainless-runtime:
|
||||||
- CPython
|
- CPython
|
||||||
x-stainless-runtime-version:
|
x-stainless-runtime-version:
|
||||||
- 3.11.7
|
- 3.12.7
|
||||||
method: POST
|
method: POST
|
||||||
uri: https://api.openai.com/v1/chat/completions
|
uri: https://api.openai.com/v1/chat/completions
|
||||||
response:
|
response:
|
||||||
content: "{\n \"id\": \"chatcmpl-ABAtOWmVjvzQ9X58tKAUcOF4gmXwx\",\n \"object\":
|
content: "{\n \"id\": \"chatcmpl-AsXdf4OZKCZSigmN4k0gyh67NciqP\",\n \"object\":
|
||||||
\"chat.completion\",\n \"created\": 1727226842,\n \"model\": \"gpt-4o-2024-05-13\",\n
|
\"chat.completion\",\n \"created\": 1737562383,\n \"model\": \"gpt-4o-2024-08-06\",\n
|
||||||
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
||||||
\"assistant\",\n \"content\": \"Thought: I need to use the get_final_answer
|
\"assistant\",\n \"content\": \"```\\nThought: I have to use the available
|
||||||
tool to determine the final answer.\\nAction: get_final_answer\\nAction Input:
|
tool to get the final answer. Let's proceed with executing it.\\nAction: get_final_answer\\nAction
|
||||||
{}\",\n \"refusal\": null\n },\n \"logprobs\": null,\n \"finish_reason\":
|
Input: {}\",\n \"refusal\": null\n },\n \"logprobs\": null,\n
|
||||||
\"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 274,\n \"completion_tokens\":
|
\ \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
|
||||||
27,\n \"total_tokens\": 301,\n \"completion_tokens_details\": {\n \"reasoning_tokens\":
|
274,\n \"completion_tokens\": 33,\n \"total_tokens\": 307,\n \"prompt_tokens_details\":
|
||||||
0\n }\n },\n \"system_fingerprint\": \"fp_e375328146\"\n}\n"
|
{\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:
|
headers:
|
||||||
CF-Cache-Status:
|
CF-Cache-Status:
|
||||||
- DYNAMIC
|
- DYNAMIC
|
||||||
CF-RAY:
|
CF-RAY:
|
||||||
- 8c8727b3492f31e6-MIA
|
- 9060d43e3be1d690-IAD
|
||||||
Connection:
|
Connection:
|
||||||
- keep-alive
|
- keep-alive
|
||||||
Content-Encoding:
|
Content-Encoding:
|
||||||
@@ -75,19 +77,27 @@ interactions:
|
|||||||
Content-Type:
|
Content-Type:
|
||||||
- application/json
|
- application/json
|
||||||
Date:
|
Date:
|
||||||
- Wed, 25 Sep 2024 01:14:03 GMT
|
- Wed, 22 Jan 2025 16:13:03 GMT
|
||||||
Server:
|
Server:
|
||||||
- cloudflare
|
- 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:
|
Transfer-Encoding:
|
||||||
- chunked
|
- chunked
|
||||||
X-Content-Type-Options:
|
X-Content-Type-Options:
|
||||||
- nosniff
|
- nosniff
|
||||||
access-control-expose-headers:
|
access-control-expose-headers:
|
||||||
- X-Request-ID
|
- X-Request-ID
|
||||||
|
alt-svc:
|
||||||
|
- h3=":443"; ma=86400
|
||||||
openai-organization:
|
openai-organization:
|
||||||
- crewai-iuxna1
|
- crewai-iuxna1
|
||||||
openai-processing-ms:
|
openai-processing-ms:
|
||||||
- '348'
|
- '791'
|
||||||
openai-version:
|
openai-version:
|
||||||
- '2020-10-01'
|
- '2020-10-01'
|
||||||
strict-transport-security:
|
strict-transport-security:
|
||||||
@@ -99,45 +109,59 @@ interactions:
|
|||||||
x-ratelimit-remaining-requests:
|
x-ratelimit-remaining-requests:
|
||||||
- '9999'
|
- '9999'
|
||||||
x-ratelimit-remaining-tokens:
|
x-ratelimit-remaining-tokens:
|
||||||
- '29999682'
|
- '29999680'
|
||||||
x-ratelimit-reset-requests:
|
x-ratelimit-reset-requests:
|
||||||
- 6ms
|
- 6ms
|
||||||
x-ratelimit-reset-tokens:
|
x-ratelimit-reset-tokens:
|
||||||
- 0s
|
- 0s
|
||||||
x-request-id:
|
x-request-id:
|
||||||
- req_be929caac49706f487950548bdcdd46e
|
- req_eeed99acafd3aeb1e3d4a6c8063192b0
|
||||||
http_version: HTTP/1.1
|
http_version: HTTP/1.1
|
||||||
status_code: 200
|
status_code: 200
|
||||||
- request:
|
- request:
|
||||||
body: '{"messages": [{"role": "system", "content": "You are test role. test backstory\nYour
|
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
|
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:
|
should NEVER make up tools that are not listed here:\n\nTool Name: get_final_answer\nTool
|
||||||
Any, **kwargs: Any) -> Any\nTool Description: get_final_answer() - Get the final
|
Arguments: {}\nTool Description: Get the final answer but don''t give it yet,
|
||||||
answer but don''t give it yet, just re-use this tool non-stop. \nTool
|
just re-use this\n tool non-stop.\n\nIMPORTANT: Use the following format
|
||||||
Arguments: {}\n\nUse the following format:\n\nThought: you should always think
|
in your response:\n\n```\nThought: you should always think about what to do\nAction:
|
||||||
about what to do\nAction: the action to take, only one name of [get_final_answer],
|
the action to take, only one name of [get_final_answer], just the name, exactly
|
||||||
just the name, exactly as it''s written.\nAction Input: the input to the action,
|
as it''s written.\nAction Input: the input to the action, just a simple JSON
|
||||||
just a simple python dictionary, enclosed in curly braces, using \" to wrap
|
object, enclosed in curly braces, using \" to wrap keys and values.\nObservation:
|
||||||
keys and values.\nObservation: the result of the action\n\nOnce all necessary
|
the result of the action\n```\n\nOnce all necessary information is gathered,
|
||||||
information is gathered:\n\nThought: I now know the final answer\nFinal Answer:
|
return the following format:\n\n```\nThought: I now know the final answer\nFinal
|
||||||
the final answer to the original input question\n"}, {"role": "user", "content":
|
Answer: the final answer to the original input question\n```"}, {"role": "user",
|
||||||
"\nCurrent Task: Use the get_final_answer tool.\n\nThis is the expect criteria
|
"content": "\nCurrent Task: Use the get_final_answer tool.\n\nThis is the expect
|
||||||
for your final answer: The final answer\nyou MUST return the actual complete
|
criteria for your final answer: The final answer\nyou MUST return the actual
|
||||||
content as the final answer, not a summary.\n\nBegin! This is VERY important
|
complete content as the final answer, not a summary.\n\nBegin! This is VERY
|
||||||
to you, use the tools available and give your best Final Answer, your job depends
|
important to you, use the tools available and give your best Final Answer, your
|
||||||
on it!\n\nThought:"}, {"role": "user", "content": "Thought: I need to use the
|
job depends on it!\n\nThought:"}, {"role": "assistant", "content": "```\nThought:
|
||||||
get_final_answer tool to determine the final answer.\nAction: get_final_answer\nAction
|
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
|
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
|
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
|
not both at the same time. When responding, I must use the following format:\n\n```\nThought:
|
||||||
always think about what to do\nAction: the action to take, should be one of
|
you should always think about what to do\nAction: the action to take, should
|
||||||
[get_final_answer]\nAction Input: the input to the action, dictionary enclosed
|
be one of [get_final_answer]\nAction Input: the input to the action, dictionary
|
||||||
in curly braces\nObservation: the result of the action\n... (this Thought/Action/Action
|
enclosed in curly braces\nObservation: the result of the action\n```\nThis Thought/Action/Action
|
||||||
Input/Result can repeat N times)\nThought: I now can give a great answer\nFinal
|
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,
|
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
|
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:
|
headers:
|
||||||
accept:
|
accept:
|
||||||
- application/json
|
- application/json
|
||||||
@@ -146,16 +170,16 @@ interactions:
|
|||||||
connection:
|
connection:
|
||||||
- keep-alive
|
- keep-alive
|
||||||
content-length:
|
content-length:
|
||||||
- '2320'
|
- '3445'
|
||||||
content-type:
|
content-type:
|
||||||
- application/json
|
- application/json
|
||||||
cookie:
|
cookie:
|
||||||
- _cfuvid=ePJSDFdHag2D8lj21_ijAMWjoA6xfnPNxN4uekvC728-1727226247743-0.0.1.1-604800000;
|
- __cf_bm=_Jcp7wnO_mXdvOnborCN6j8HwJxJXbszedJC1l7pFUg-1737562383-1.0.1.1-pDSLXlg.nKjG4wsT7mTJPjUvOX1UJITiS4MqKp6yfMWwRSJINsW1qC48SAcjBjakx2H5I1ESVk9JtUpUFDtf4g;
|
||||||
__cf_bm=3giyBOIM0GNudFELtsBWYXwLrpLBTNLsh81wfXgu2tg-1727226247-1.0.1.1-ugUDz0c5EhmfVpyGtcdedlIWeDGuy2q0tXQTKVpv83HZhvxgBcS7SBL1wS4rapPM38yhfEcfwA79ARt3HQEzKA
|
_cfuvid=x3SYvzL2nq_PTBGtE8R9cl5CkeaaDzZFQIrYfo91S2s-1737562383916-0.0.1.1-604800000
|
||||||
host:
|
host:
|
||||||
- api.openai.com
|
- api.openai.com
|
||||||
user-agent:
|
user-agent:
|
||||||
- OpenAI/Python 1.47.0
|
- OpenAI/Python 1.59.6
|
||||||
x-stainless-arch:
|
x-stainless-arch:
|
||||||
- arm64
|
- arm64
|
||||||
x-stainless-async:
|
x-stainless-async:
|
||||||
@@ -165,29 +189,36 @@ interactions:
|
|||||||
x-stainless-os:
|
x-stainless-os:
|
||||||
- MacOS
|
- MacOS
|
||||||
x-stainless-package-version:
|
x-stainless-package-version:
|
||||||
- 1.47.0
|
- 1.59.6
|
||||||
x-stainless-raw-response:
|
x-stainless-raw-response:
|
||||||
- 'true'
|
- 'true'
|
||||||
|
x-stainless-retry-count:
|
||||||
|
- '0'
|
||||||
x-stainless-runtime:
|
x-stainless-runtime:
|
||||||
- CPython
|
- CPython
|
||||||
x-stainless-runtime-version:
|
x-stainless-runtime-version:
|
||||||
- 3.11.7
|
- 3.12.7
|
||||||
method: POST
|
method: POST
|
||||||
uri: https://api.openai.com/v1/chat/completions
|
uri: https://api.openai.com/v1/chat/completions
|
||||||
response:
|
response:
|
||||||
content: "{\n \"id\": \"chatcmpl-ABAtPaaeRfdNsZ3k06CfAmrEW8IJu\",\n \"object\":
|
content: "{\n \"id\": \"chatcmpl-AsXdg9UrLvAiqWP979E6DszLsQ84k\",\n \"object\":
|
||||||
\"chat.completion\",\n \"created\": 1727226843,\n \"model\": \"gpt-4o-2024-05-13\",\n
|
\"chat.completion\",\n \"created\": 1737562384,\n \"model\": \"gpt-4o-2024-08-06\",\n
|
||||||
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
||||||
\"assistant\",\n \"content\": \"Final Answer: The final answer\",\n \"refusal\":
|
\"assistant\",\n \"content\": \"```\\nThought: I now know the final answer\\nFinal
|
||||||
null\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n
|
Answer: The final answer must be the great and the most complete as possible,
|
||||||
\ }\n ],\n \"usage\": {\n \"prompt_tokens\": 483,\n \"completion_tokens\":
|
it must be outcome described.\\n```\",\n \"refusal\": null\n },\n
|
||||||
6,\n \"total_tokens\": 489,\n \"completion_tokens_details\": {\n \"reasoning_tokens\":
|
\ \"logprobs\": null,\n \"finish_reason\": \"stop\"\n }\n ],\n
|
||||||
0\n }\n },\n \"system_fingerprint\": \"fp_e375328146\"\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:
|
headers:
|
||||||
CF-Cache-Status:
|
CF-Cache-Status:
|
||||||
- DYNAMIC
|
- DYNAMIC
|
||||||
CF-RAY:
|
CF-RAY:
|
||||||
- 8c8727b9da1f31e6-MIA
|
- 9060d4441edad690-IAD
|
||||||
Connection:
|
Connection:
|
||||||
- keep-alive
|
- keep-alive
|
||||||
Content-Encoding:
|
Content-Encoding:
|
||||||
@@ -195,7 +226,7 @@ interactions:
|
|||||||
Content-Type:
|
Content-Type:
|
||||||
- application/json
|
- application/json
|
||||||
Date:
|
Date:
|
||||||
- Wed, 25 Sep 2024 01:14:03 GMT
|
- Wed, 22 Jan 2025 16:13:05 GMT
|
||||||
Server:
|
Server:
|
||||||
- cloudflare
|
- cloudflare
|
||||||
Transfer-Encoding:
|
Transfer-Encoding:
|
||||||
@@ -209,7 +240,7 @@ interactions:
|
|||||||
openai-organization:
|
openai-organization:
|
||||||
- crewai-iuxna1
|
- crewai-iuxna1
|
||||||
openai-processing-ms:
|
openai-processing-ms:
|
||||||
- '188'
|
- '928'
|
||||||
openai-version:
|
openai-version:
|
||||||
- '2020-10-01'
|
- '2020-10-01'
|
||||||
strict-transport-security:
|
strict-transport-security:
|
||||||
@@ -221,13 +252,13 @@ interactions:
|
|||||||
x-ratelimit-remaining-requests:
|
x-ratelimit-remaining-requests:
|
||||||
- '9999'
|
- '9999'
|
||||||
x-ratelimit-remaining-tokens:
|
x-ratelimit-remaining-tokens:
|
||||||
- '29999445'
|
- '29999187'
|
||||||
x-ratelimit-reset-requests:
|
x-ratelimit-reset-requests:
|
||||||
- 6ms
|
- 6ms
|
||||||
x-ratelimit-reset-tokens:
|
x-ratelimit-reset-tokens:
|
||||||
- 1ms
|
- 1ms
|
||||||
x-request-id:
|
x-request-id:
|
||||||
- req_d8e32538689fe064627468bad802d9a8
|
- req_61fc7506e6db326ec572224aec81ef23
|
||||||
http_version: HTTP/1.1
|
http_version: HTTP/1.1
|
||||||
status_code: 200
|
status_code: 200
|
||||||
version: 1
|
version: 1
|
||||||
|
|||||||
@@ -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
|
|
||||||
102
tests/cassettes/test_llm_call_with_message_list.yaml
Normal file
102
tests/cassettes/test_llm_call_with_message_list.yaml
Normal file
@@ -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
|
||||||
108
tests/cassettes/test_llm_call_with_string_input.yaml
Normal file
108
tests/cassettes/test_llm_call_with_string_input.yaml
Normal file
@@ -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
|
||||||
@@ -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
|
||||||
111
tests/cassettes/test_llm_call_with_tool_and_message_list.yaml
Normal file
111
tests/cassettes/test_llm_call_with_tool_and_message_list.yaml
Normal file
@@ -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
|
||||||
107
tests/cassettes/test_llm_call_with_tool_and_string_input.yaml
Normal file
107
tests/cassettes/test_llm_call_with_tool_and_string_input.yaml
Normal file
@@ -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
|
||||||
@@ -3480,10 +3480,12 @@ def test_crew_guardrail_feedback_in_context():
|
|||||||
|
|
||||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
def test_before_kickoff_callback():
|
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
|
@CrewBase
|
||||||
class TestCrewClass:
|
class TestCrewClass:
|
||||||
|
from crewai.project import crew
|
||||||
|
|
||||||
agents_config = None
|
agents_config = None
|
||||||
tasks_config = None
|
tasks_config = None
|
||||||
|
|
||||||
@@ -3510,7 +3512,7 @@ def test_before_kickoff_callback():
|
|||||||
task = Task(
|
task = Task(
|
||||||
description="Test task description",
|
description="Test task description",
|
||||||
expected_output="Test expected output",
|
expected_output="Test expected output",
|
||||||
agent=self.my_agent(), # Use the agent instance
|
agent=self.my_agent(),
|
||||||
)
|
)
|
||||||
return task
|
return task
|
||||||
|
|
||||||
@@ -3520,28 +3522,30 @@ def test_before_kickoff_callback():
|
|||||||
|
|
||||||
test_crew_instance = TestCrewClass()
|
test_crew_instance = TestCrewClass()
|
||||||
|
|
||||||
crew = test_crew_instance.crew()
|
test_crew = test_crew_instance.crew()
|
||||||
|
|
||||||
# Verify that the before_kickoff_callbacks are set
|
# 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
|
# Prepare inputs
|
||||||
inputs = {"initial": True}
|
inputs = {"initial": True}
|
||||||
|
|
||||||
# Call kickoff
|
# Call kickoff
|
||||||
crew.kickoff(inputs=inputs)
|
test_crew.kickoff(inputs=inputs)
|
||||||
|
|
||||||
# Check that the before_kickoff function was called and modified inputs
|
# Check that the before_kickoff function was called and modified inputs
|
||||||
assert test_crew_instance.inputs_modified
|
assert test_crew_instance.inputs_modified
|
||||||
assert inputs.get("modified") == True
|
assert inputs.get("modified")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
def test_before_kickoff_without_inputs():
|
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
|
@CrewBase
|
||||||
class TestCrewClass:
|
class TestCrewClass:
|
||||||
|
from crewai.project import crew
|
||||||
|
|
||||||
agents_config = None
|
agents_config = None
|
||||||
tasks_config = None
|
tasks_config = None
|
||||||
|
|
||||||
@@ -3579,12 +3583,12 @@ def test_before_kickoff_without_inputs():
|
|||||||
# Instantiate the class
|
# Instantiate the class
|
||||||
test_crew_instance = TestCrewClass()
|
test_crew_instance = TestCrewClass()
|
||||||
# Build the crew
|
# Build the crew
|
||||||
crew = test_crew_instance.crew()
|
test_crew = test_crew_instance.crew()
|
||||||
# Verify that the before_kickoff_callback is registered
|
# 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
|
# Call kickoff without passing inputs
|
||||||
output = crew.kickoff()
|
test_crew.kickoff()
|
||||||
|
|
||||||
# Check that the before_kickoff function was called
|
# Check that the before_kickoff function was called
|
||||||
assert test_crew_instance.inputs_modified
|
assert test_crew_instance.inputs_modified
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import pytest
|
|||||||
|
|
||||||
from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess
|
from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess
|
||||||
from crewai.llm import LLM
|
from crewai.llm import LLM
|
||||||
|
from crewai.tools import tool
|
||||||
from crewai.utilities.token_counter_callback import TokenCalcHandler
|
from crewai.utilities.token_counter_callback import TokenCalcHandler
|
||||||
|
|
||||||
|
|
||||||
@@ -37,3 +38,119 @@ def test_llm_callback_replacement():
|
|||||||
assert usage_metrics_1.successful_requests == 1
|
assert usage_metrics_1.successful_requests == 1
|
||||||
assert usage_metrics_2.successful_requests == 1
|
assert usage_metrics_2.successful_requests == 1
|
||||||
assert usage_metrics_1 == calc_handler_1.token_cost_process.get_summary()
|
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
|
||||||
|
|||||||
112
tests/test_flow_default_override.py
Normal file
112
tests/test_flow_default_override.py
Normal file
@@ -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"
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
"""Test flow state persistence functionality."""
|
"""Test flow state persistence functionality."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from typing import Dict, Optional
|
from typing import Dict
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from crewai.flow.flow import Flow, FlowState, start
|
from crewai.flow.flow import Flow, FlowState, listen, start
|
||||||
from crewai.flow.persistence import FlowPersistence, persist
|
from crewai.flow.persistence import persist
|
||||||
from crewai.flow.persistence.sqlite import SQLiteFlowPersistence
|
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."""
|
"""Test that @persist decorator saves state in SQLite."""
|
||||||
db_path = os.path.join(tmp_path, "test_flows.db")
|
db_path = os.path.join(tmp_path, "test_flows.db")
|
||||||
persistence = SQLiteFlowPersistence(db_path)
|
persistence = SQLiteFlowPersistence(db_path)
|
||||||
|
|
||||||
class TestFlow(Flow[Dict[str, str]]):
|
class TestFlow(Flow[Dict[str, str]]):
|
||||||
initial_state = dict() # Use dict instance as initial state
|
initial_state = dict() # Use dict instance as initial state
|
||||||
|
|
||||||
@start()
|
@start()
|
||||||
@persist(persistence)
|
@persist(persistence)
|
||||||
def init_step(self):
|
def init_step(self):
|
||||||
self.state["message"] = "Hello, World!"
|
self.state["message"] = "Hello, World!"
|
||||||
self.state["id"] = "test-uuid" # Ensure we have an ID for persistence
|
self.state["id"] = "test-uuid" # Ensure we have an ID for persistence
|
||||||
|
|
||||||
# Run flow and verify state is saved
|
# Run flow and verify state is saved
|
||||||
flow = TestFlow(persistence=persistence)
|
flow = TestFlow(persistence=persistence)
|
||||||
flow.kickoff()
|
flow.kickoff()
|
||||||
|
|
||||||
# Load state from DB and verify
|
# Load state from DB and verify
|
||||||
saved_state = persistence.load_state(flow.state["id"])
|
saved_state = persistence.load_state(flow.state["id"])
|
||||||
assert saved_state is not None
|
assert saved_state is not None
|
||||||
@@ -45,20 +45,20 @@ def test_structured_state_persistence(tmp_path):
|
|||||||
"""Test persistence with Pydantic model state."""
|
"""Test persistence with Pydantic model state."""
|
||||||
db_path = os.path.join(tmp_path, "test_flows.db")
|
db_path = os.path.join(tmp_path, "test_flows.db")
|
||||||
persistence = SQLiteFlowPersistence(db_path)
|
persistence = SQLiteFlowPersistence(db_path)
|
||||||
|
|
||||||
class StructuredFlow(Flow[TestState]):
|
class StructuredFlow(Flow[TestState]):
|
||||||
initial_state = TestState
|
initial_state = TestState
|
||||||
|
|
||||||
@start()
|
@start()
|
||||||
@persist(persistence)
|
@persist(persistence)
|
||||||
def count_up(self):
|
def count_up(self):
|
||||||
self.state.counter += 1
|
self.state.counter += 1
|
||||||
self.state.message = f"Count is {self.state.counter}"
|
self.state.message = f"Count is {self.state.counter}"
|
||||||
|
|
||||||
# Run flow and verify state changes are saved
|
# Run flow and verify state changes are saved
|
||||||
flow = StructuredFlow(persistence=persistence)
|
flow = StructuredFlow(persistence=persistence)
|
||||||
flow.kickoff()
|
flow.kickoff()
|
||||||
|
|
||||||
# Load and verify state
|
# Load and verify state
|
||||||
saved_state = persistence.load_state(flow.state.id)
|
saved_state = persistence.load_state(flow.state.id)
|
||||||
assert saved_state is not None
|
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."""
|
"""Test restoring flow state from persistence with various restoration methods."""
|
||||||
db_path = os.path.join(tmp_path, "test_flows.db")
|
db_path = os.path.join(tmp_path, "test_flows.db")
|
||||||
persistence = SQLiteFlowPersistence(db_path)
|
persistence = SQLiteFlowPersistence(db_path)
|
||||||
|
|
||||||
# First flow execution to create initial state
|
# First flow execution to create initial state
|
||||||
class RestorableFlow(Flow[TestState]):
|
class RestorableFlow(Flow[TestState]):
|
||||||
initial_state = TestState
|
|
||||||
|
|
||||||
@start()
|
@start()
|
||||||
@persist(persistence)
|
@persist(persistence)
|
||||||
def set_message(self):
|
def set_message(self):
|
||||||
self.state.message = "Original message"
|
if self.state.message == "":
|
||||||
self.state.counter = 42
|
self.state.message = "Original message"
|
||||||
|
if self.state.counter == 0:
|
||||||
|
self.state.counter = 42
|
||||||
|
|
||||||
# Create and persist initial state
|
# Create and persist initial state
|
||||||
flow1 = RestorableFlow(persistence=persistence)
|
flow1 = RestorableFlow(persistence=persistence)
|
||||||
flow1.kickoff()
|
flow1.kickoff()
|
||||||
original_uuid = flow1.state.id
|
original_uuid = flow1.state.id
|
||||||
|
|
||||||
# Test case 1: Restore using restore_uuid with field override
|
# Test case 1: Restore using restore_uuid with field override
|
||||||
flow2 = RestorableFlow(
|
flow2 = RestorableFlow(persistence=persistence)
|
||||||
persistence=persistence,
|
flow2.kickoff(inputs={
|
||||||
restore_uuid=original_uuid,
|
"id": original_uuid,
|
||||||
counter=43, # Override counter
|
"counter": 43
|
||||||
)
|
})
|
||||||
|
|
||||||
# Verify state restoration and selective field override
|
# Verify state restoration and selective field override
|
||||||
assert flow2.state.id == original_uuid
|
assert flow2.state.id == original_uuid
|
||||||
assert flow2.state.message == "Original message" # Preserved
|
assert flow2.state.message == "Original message" # Preserved
|
||||||
assert flow2.state.counter == 43 # Overridden
|
assert flow2.state.counter == 43 # Overridden
|
||||||
|
|
||||||
# Test case 2: Restore using kwargs['id']
|
# Test case 2: Restore using kwargs['id']
|
||||||
flow3 = RestorableFlow(
|
flow3 = RestorableFlow(persistence=persistence)
|
||||||
persistence=persistence,
|
flow3.kickoff(inputs={
|
||||||
id=original_uuid,
|
"id": original_uuid,
|
||||||
message="Updated message", # Override message
|
"message": "Updated message"
|
||||||
)
|
})
|
||||||
|
|
||||||
# Verify state restoration and selective field override
|
# Verify state restoration and selective field override
|
||||||
assert flow3.state.id == original_uuid
|
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
|
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):
|
def test_multiple_method_persistence(tmp_path):
|
||||||
"""Test state persistence across multiple method executions."""
|
"""Test state persistence across multiple method executions."""
|
||||||
db_path = os.path.join(tmp_path, "test_flows.db")
|
db_path = os.path.join(tmp_path, "test_flows.db")
|
||||||
persistence = SQLiteFlowPersistence(db_path)
|
persistence = SQLiteFlowPersistence(db_path)
|
||||||
|
|
||||||
class MultiStepFlow(Flow[TestState]):
|
class MultiStepFlow(Flow[TestState]):
|
||||||
initial_state = TestState
|
|
||||||
|
|
||||||
@start()
|
@start()
|
||||||
@persist(persistence)
|
@persist(persistence)
|
||||||
def step_1(self):
|
def step_1(self):
|
||||||
self.state.counter = 1
|
if self.state.counter == 1:
|
||||||
self.state.message = "Step 1"
|
self.state.counter = 99999
|
||||||
|
self.state.message = "Step 99999"
|
||||||
@start()
|
else:
|
||||||
|
self.state.counter = 1
|
||||||
|
self.state.message = "Step 1"
|
||||||
|
|
||||||
|
@listen(step_1)
|
||||||
@persist(persistence)
|
@persist(persistence)
|
||||||
def step_2(self):
|
def step_2(self):
|
||||||
self.state.counter = 2
|
if self.state.counter == 1:
|
||||||
self.state.message = "Step 2"
|
self.state.counter = 2
|
||||||
|
self.state.message = "Step 2"
|
||||||
|
|
||||||
flow = MultiStepFlow(persistence=persistence)
|
flow = MultiStepFlow(persistence=persistence)
|
||||||
flow.kickoff()
|
flow.kickoff()
|
||||||
|
|
||||||
|
flow2 = MultiStepFlow(persistence=persistence)
|
||||||
|
flow2.kickoff(inputs={"id": flow.state.id})
|
||||||
|
|
||||||
# Load final state
|
# Load final state
|
||||||
final_state = persistence.load_state(flow.state.id)
|
final_state = flow2.state
|
||||||
assert final_state is not None
|
assert final_state is not None
|
||||||
assert final_state["counter"] == 2
|
assert final_state.counter == 2
|
||||||
assert final_state["message"] == "Step 2"
|
assert final_state.message == "Step 2"
|
||||||
|
|
||||||
|
class NoPersistenceMultiStepFlow(Flow[TestState]):
|
||||||
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
|
|
||||||
|
|
||||||
@start()
|
@start()
|
||||||
@persist(persistence)
|
@persist(persistence)
|
||||||
def will_fail(self):
|
def step_1(self):
|
||||||
self.state.value = "test"
|
if self.state.counter == 1:
|
||||||
|
self.state.counter = 99999
|
||||||
with pytest.raises(ValueError) as exc_info:
|
self.state.message = "Step 99999"
|
||||||
flow = InvalidFlow(persistence=persistence)
|
else:
|
||||||
|
self.state.counter = 1
|
||||||
assert "must have an 'id' field" in str(exc_info.value)
|
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"
|
||||||
|
|||||||
@@ -231,3 +231,255 @@ def test_validate_tool_input_with_special_characters():
|
|||||||
|
|
||||||
arguments = tool_usage._validate_tool_input(tool_input)
|
arguments = tool_usage._validate_tool_input(tool_input)
|
||||||
assert arguments == expected_arguments
|
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
|
||||||
|
|||||||
@@ -588,3 +588,12 @@ def test_converter_with_function_calling():
|
|||||||
assert output.name == "Eve"
|
assert output.name == "Eve"
|
||||||
assert output.age == 35
|
assert output.age == 35
|
||||||
instructor.to_pydantic.assert_called_once()
|
instructor.to_pydantic.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_model_description_union_field():
|
||||||
|
class UnionModel(BaseModel):
|
||||||
|
field: int | str | None
|
||||||
|
|
||||||
|
description = generate_model_description(UnionModel)
|
||||||
|
expected_description = '{\n "field": int | str | None\n}'
|
||||||
|
assert description == expected_description
|
||||||
|
|||||||
211
uv.lock
generated
211
uv.lock
generated
@@ -1,7 +1,6 @@
|
|||||||
version = 1
|
version = 1
|
||||||
requires-python = ">=3.10, <3.13"
|
requires-python = ">=3.10, <3.13"
|
||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
|
|
||||||
"python_full_version < '3.11' and platform_system == 'Darwin' and sys_platform == 'darwin'",
|
"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 == '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')",
|
"(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 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 == '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 == '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]]
|
[[package]]
|
||||||
@@ -199,6 +198,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 },
|
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asn1crypto"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "asttokens"
|
name = "asttokens"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
@@ -220,6 +228,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721 },
|
{ url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atpublic"
|
||||||
|
version = "5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5d/18/b1d247792440378abeeb0853f9daa2a127284b68776af6815990be7fcdb0/atpublic-5.0.tar.gz", hash = "sha256:d5cb6cbabf00ec1d34e282e8ce7cbc9b74ba4cb732e766c24e2d78d1ad7f723f", size = 14646 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/03/2cb0e5326e19b7d877bc9c3a7ef436a30a06835b638580d1f5e21a0409ed/atpublic-5.0-py3-none-any.whl", hash = "sha256:b651dcd886666b1042d1e38158a22a4f2c267748f4e97fde94bc492a4a28a3f3", size = 5207 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
version = "24.2.0"
|
version = "24.2.0"
|
||||||
@@ -346,7 +363,7 @@ name = "build"
|
|||||||
version = "1.2.2.post1"
|
version = "1.2.2.post1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
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 = "importlib-metadata", marker = "python_full_version < '3.10.2'" },
|
||||||
{ name = "packaging" },
|
{ name = "packaging" },
|
||||||
{ name = "pyproject-hooks" },
|
{ name = "pyproject-hooks" },
|
||||||
@@ -581,7 +598,7 @@ name = "click"
|
|||||||
version = "8.1.7"
|
version = "8.1.7"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
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/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -632,7 +649,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crewai"
|
name = "crewai"
|
||||||
version = "0.95.0"
|
version = "0.98.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "appdirs" },
|
{ name = "appdirs" },
|
||||||
@@ -642,6 +659,7 @@ dependencies = [
|
|||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "instructor" },
|
{ name = "instructor" },
|
||||||
{ name = "json-repair" },
|
{ name = "json-repair" },
|
||||||
|
{ name = "json5" },
|
||||||
{ name = "jsonref" },
|
{ name = "jsonref" },
|
||||||
{ name = "litellm" },
|
{ name = "litellm" },
|
||||||
{ name = "openai" },
|
{ name = "openai" },
|
||||||
@@ -715,11 +733,12 @@ requires-dist = [
|
|||||||
{ name = "blinker", specifier = ">=1.9.0" },
|
{ name = "blinker", specifier = ">=1.9.0" },
|
||||||
{ name = "chromadb", specifier = ">=0.5.23" },
|
{ name = "chromadb", specifier = ">=0.5.23" },
|
||||||
{ name = "click", specifier = ">=8.1.7" },
|
{ name = "click", specifier = ">=8.1.7" },
|
||||||
{ name = "crewai-tools", marker = "extra == 'tools'", specifier = ">=0.25.5" },
|
{ name = "crewai-tools", marker = "extra == 'tools'", specifier = ">=0.32.1" },
|
||||||
{ name = "docling", marker = "extra == 'docling'", specifier = ">=2.12.0" },
|
{ name = "docling", marker = "extra == 'docling'", specifier = ">=2.12.0" },
|
||||||
{ name = "fastembed", marker = "extra == 'fastembed'", specifier = ">=0.4.1" },
|
{ name = "fastembed", marker = "extra == 'fastembed'", specifier = ">=0.4.1" },
|
||||||
{ name = "instructor", specifier = ">=1.3.3" },
|
{ name = "instructor", specifier = ">=1.3.3" },
|
||||||
{ name = "json-repair", specifier = ">=0.25.2" },
|
{ name = "json-repair", specifier = ">=0.25.2" },
|
||||||
|
{ name = "json5", specifier = ">=0.10.0" },
|
||||||
{ name = "jsonref", specifier = ">=1.1.0" },
|
{ name = "jsonref", specifier = ">=1.1.0" },
|
||||||
{ name = "litellm", specifier = "==1.57.4" },
|
{ name = "litellm", specifier = "==1.57.4" },
|
||||||
{ name = "mem0ai", marker = "extra == 'mem0'", specifier = ">=0.1.29" },
|
{ name = "mem0ai", marker = "extra == 'mem0'", specifier = ">=0.1.29" },
|
||||||
@@ -763,7 +782,7 @@ dev = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crewai-tools"
|
name = "crewai-tools"
|
||||||
version = "0.25.6"
|
version = "0.32.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "beautifulsoup4" },
|
{ name = "beautifulsoup4" },
|
||||||
@@ -775,20 +794,21 @@ dependencies = [
|
|||||||
{ name = "lancedb" },
|
{ name = "lancedb" },
|
||||||
{ name = "linkup-sdk" },
|
{ name = "linkup-sdk" },
|
||||||
{ name = "openai" },
|
{ name = "openai" },
|
||||||
|
{ name = "patronus" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pyright" },
|
{ name = "pyright" },
|
||||||
{ name = "pytest" },
|
|
||||||
{ name = "pytube" },
|
{ name = "pytube" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "scrapegraph-py" },
|
{ name = "scrapegraph-py" },
|
||||||
{ name = "selenium" },
|
{ name = "selenium" },
|
||||||
{ name = "serpapi" },
|
{ name = "serpapi" },
|
||||||
|
{ name = "snowflake" },
|
||||||
{ name = "spider-client" },
|
{ name = "spider-client" },
|
||||||
{ name = "weaviate-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/e9/e7/fb07f0089028f7c9003770641d21f5844d4fa22bf5cc4c4b3676bfa0e1fe/crewai_tools-0.32.1.tar.gz", hash = "sha256:41acea9243b17a463f355d48dfe7d73bd59738c8862a8da780eae008e0136414", size = 887378 }
|
||||||
wheels = [
|
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/36/f0/8f98f1a2b90b9b989bd01cf48b5e3bb2d842be2062bfd3177a77561e7b61/crewai_tools-0.32.1-py3-none-any.whl", hash = "sha256:6cb436dc66e19e35285a4fce501158a13bce99b244370574f568ec33c5513351", size = 537264 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2059,6 +2079,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 },
|
{ 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]]
|
[[package]]
|
||||||
name = "jsonlines"
|
name = "jsonlines"
|
||||||
version = "3.1.0"
|
version = "3.1.0"
|
||||||
@@ -2588,7 +2617,7 @@ version = "1.6.1"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
{ name = "colorama", marker = "platform_system == 'Windows'" },
|
||||||
{ name = "ghp-import" },
|
{ name = "ghp-import" },
|
||||||
{ name = "jinja2" },
|
{ name = "jinja2" },
|
||||||
{ name = "markdown" },
|
{ name = "markdown" },
|
||||||
@@ -2769,7 +2798,7 @@ version = "2.10.2"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pygments" },
|
{ name = "pygments" },
|
||||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
{ name = "pywin32", marker = "platform_system == 'Windows'" },
|
||||||
{ name = "tqdm" },
|
{ name = "tqdm" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/93/80ac75c20ce54c785648b4ed363c88f148bf22637e10c9863db4fbe73e74/mpire-2.10.2.tar.gz", hash = "sha256:f66a321e93fadff34585a4bfa05e95bd946cf714b442f51c529038eb45773d97", size = 271270 }
|
sdist = { url = "https://files.pythonhosted.org/packages/3a/93/80ac75c20ce54c785648b4ed363c88f148bf22637e10c9863db4fbe73e74/mpire-2.10.2.tar.gz", hash = "sha256:f66a321e93fadff34585a4bfa05e95bd946cf714b442f51c529038eb45773d97", size = 271270 }
|
||||||
@@ -3016,7 +3045,7 @@ name = "nvidia-cudnn-cu12"
|
|||||||
version = "9.1.0.70"
|
version = "9.1.0.70"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
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 = [
|
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 },
|
{ 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 +3074,7 @@ source = { registry = "https://pypi.org/simple" }
|
|||||||
dependencies = [
|
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')" },
|
||||||
{ 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-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 = [
|
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 },
|
{ 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 +3085,7 @@ name = "nvidia-cusparse-cu12"
|
|||||||
version = "12.1.0.106"
|
version = "12.1.0.106"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
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 = [
|
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 },
|
{ 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 },
|
||||||
@@ -3496,6 +3525,24 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
|
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "patronus"
|
||||||
|
version = "0.0.17"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "httpx" },
|
||||||
|
{ name = "pandas" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "pydantic-settings" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "tqdm" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c5/a0/d5218ff6f2eab18c5a90266d21cdac673c85070e82e3f8aba538b3200f54/patronus-0.0.17.tar.gz", hash = "sha256:7298f770d4f6774b955806fb319c2c872fda3551bd7fa63d975bbeedc14b28de", size = 27377 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/9e/717c4508d675549ff081a7fecf25af7d70f9d7ad87ea0d4825e02de3b801/patronus-0.0.17-py3-none-any.whl", hash = "sha256:1f322eeee838974515fdb7cbf8530ad25c6c59686abbcb28c1fdbf23d34eb10d", size = 31516 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pdfminer-six"
|
name = "pdfminer-six"
|
||||||
version = "20231228"
|
version = "20231228"
|
||||||
@@ -3606,7 +3653,7 @@ name = "portalocker"
|
|||||||
version = "2.10.1"
|
version = "2.10.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
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 }
|
sdist = { url = "https://files.pythonhosted.org/packages/ed/d3/c6c64067759e87af98cc668c1cc75171347d0f1577fab7ca3749134e3cd4/portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f", size = 40891 }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -4056,6 +4103,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c2/35/c0edf199257ef0a7d407d29cd51c4e70d1dad4370a5f44deb65a7a5475e2/pymdown_extensions-10.11.2-py3-none-any.whl", hash = "sha256:41cdde0a77290e480cf53892f5c5e50921a7ee3e5cd60ba91bf19837b33badcf", size = 259044 },
|
{ url = "https://files.pythonhosted.org/packages/c2/35/c0edf199257ef0a7d407d29cd51c4e70d1dad4370a5f44deb65a7a5475e2/pymdown_extensions-10.11.2-py3-none-any.whl", hash = "sha256:41cdde0a77290e480cf53892f5c5e50921a7ee3e5cd60ba91bf19837b33badcf", size = 259044 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyopenssl"
|
||||||
|
version = "24.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cryptography" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c1/d4/1067b82c4fc674d6f6e9e8d26b3dff978da46d351ca3bac171544693e085/pyopenssl-24.3.0.tar.gz", hash = "sha256:49f7a019577d834746bc55c5fce6ecbcec0f2b4ec5ce1cf43a9a173b8138bb36", size = 178944 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/22/40f9162e943f86f0fc927ebc648078be87def360d9d8db346619fb97df2b/pyOpenSSL-24.3.0-py3-none-any.whl", hash = "sha256:e474f5a473cd7f92221cc04976e48f4d11502804657a08a989fb3be5514c904a", size = 56111 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pypdf"
|
name = "pypdf"
|
||||||
version = "5.0.1"
|
version = "5.0.1"
|
||||||
@@ -4924,6 +4983,87 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "snowflake"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "snowflake-core" },
|
||||||
|
{ name = "snowflake-legacy" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/80/d1/830929fb7b54586f4ee601f409e80343e16f32b9b579246cd6fa9984bcff/snowflake-1.0.2.tar.gz", hash = "sha256:4009e59af24e444de4a9e9d340fff0979cca8a02a4feee4665da97eb9c76d958", size = 6033 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/25/4cbba4da3f9b333d132680a66221d1a101309cce330fa8be38b674ceafd0/snowflake-1.0.2-py3-none-any.whl", hash = "sha256:6bb0fc70aa10234769202861ccb4b091f5e9fb1bbc61a1e708db93baa3f221f4", size = 5623 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "snowflake-connector-python"
|
||||||
|
version = "3.12.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "asn1crypto" },
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "cffi" },
|
||||||
|
{ name = "charset-normalizer" },
|
||||||
|
{ name = "cryptography" },
|
||||||
|
{ name = "filelock" },
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
{ name = "pyjwt" },
|
||||||
|
{ name = "pyopenssl" },
|
||||||
|
{ name = "pytz" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "sortedcontainers" },
|
||||||
|
{ name = "tomlkit" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6b/de/f43d9c827ccc1974696ffd3c0495e2d4e98b0414b2353b7de932621f23dd/snowflake_connector_python-3.12.4.tar.gz", hash = "sha256:289e0691dfbf8ec8b7a8f58bcbb95a819890fe5e5b278fdbfc885059a63a946f", size = 743445 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/6c/edc8909e424654a7a3c18cbf804d8a35c17a65a2131f866a87ed8e762bd0/snowflake_connector_python-3.12.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f141c159e3244bd660279f87f32e39351b2845fcb75f8138f31d2219f983b05", size = 958038 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/a3/34c5082dfb9b555c914f4233224b8bc1f2c4d5668bc71bb587680b8dcd73/snowflake_connector_python-3.12.4-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:091458ba777c24adff659c5c28f0f5bb0bcca8a9b6ecc5641ae25b7c20a8f43d", size = 970665 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/87/9eceaaba58b2ec4f9094fc3a04d953bbabbfdcc05a6b14ef12610c1039f9/snowflake_connector_python-3.12.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23049d341da681ec7131cead71cdf7b1761ae5bcc08bcbdb931dcef6c25e8a5f", size = 2496731 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/0a/e35e9e0a142f3779007b0246166a245305858b198ed0dd3a41a3d2405512/snowflake_connector_python-3.12.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc88a09d77a8ce7e445094b2409b606ddb208b5fc9f7c7a379d0255a8d566e9d", size = 2520041 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/77/9a238c153600adff8fbd1136d9f4be1e42cb827cbe1865924bfe84653e85/snowflake_connector_python-3.12.4-cp310-cp310-win_amd64.whl", hash = "sha256:3c33fbba036805c1767ea48eb40ffc3fb79d61f2a4bb4e77b571ea6f6a998be8", size = 918272 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/95/e8aac28d6913e4b59f96e6d361f31b9576b5f0abe4d2c4f7decf9f075932/snowflake_connector_python-3.12.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ec5cfaa1526084cf4d0e7849d5ace601245cb4ad9675ab3cd7d799b3abea481", size = 958125 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/b6/a847a94e03bdf39010048feacd57f250a91a655eed333d7d32b165f65201/snowflake_connector_python-3.12.4-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:ff225824b3a0fa5e822442de72172f97028f04ae183877f1305d538d8d6c5d11", size = 970770 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/91/f97812ae9946944bcd9bfe1965af1cb9b1844919da879d90b90dfd3e5086/snowflake_connector_python-3.12.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9beced2789dc75e8f1e749aa637e7ec9b03302b4ed4b793ae0f1ff32823370e", size = 2519875 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/52/500d72079bfb322ebdf3892180ecf3dc73c117b3a966ee8d4bb1378882b2/snowflake_connector_python-3.12.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ea47450a04ff713f3adf28053e34103bd990291e62daee9721c76597af4b2b5", size = 2542320 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/92/74ead6bee8dd29fe372002ce59477221e04b9da96ad7aafe584afce02937/snowflake_connector_python-3.12.4-cp311-cp311-win_amd64.whl", hash = "sha256:748f9125854dca07ea471bb2bb3c5bb932a53f9b8a77ba348b50b738c77203ce", size = 918363 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/a3/1cbe0b52b810f069bdc96c372b2d91ac51aeac32986c2832aa3fe0b0b0e5/snowflake_connector_python-3.12.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bcd0371b20d199f15e6a3c0b489bf18e27f2a88c84cf3194b2569ca039fa7d1", size = 957561 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/05/8a5e16bd908a89f36d59686d356890c4bd6a976a487f86274181010f4b49/snowflake_connector_python-3.12.4-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:7900d82a450b206fa2ed6c42cd65d9b3b9fd4547eca1696937175fac2a03ba37", size = 969045 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/1b/8f5ab15d224d7bf76533c55cfd8ce73b185ce94d84241f0e900739ce3f37/snowflake_connector_python-3.12.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:300f0562aeea55e40ee03b45205dbef7b78f5ba2f1787a278c7b807e7d8db22c", size = 2533969 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/d9/2e2fd72e0251691b5c54a219256c455141a2d3c104e411b82de598c62553/snowflake_connector_python-3.12.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6762a00948f003be55d7dc5de9de690315d01951a94371ec3db069d9303daba", size = 2558052 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/cb/e0ab230ad5adc9932e595bdbec693b2499d446666daf6cb9cae306a41dd2/snowflake_connector_python-3.12.4-cp312-cp312-win_amd64.whl", hash = "sha256:83ca896790a7463b6c8cd42e1a29b8ea197cc920839ae6ee96a467475eab4ec2", size = 916627 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "snowflake-core"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "atpublic" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "snowflake-connector-python" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1d/cf/6f91e5b2daaf3df9ae666a65f5ba3938f11a40784e4ada5218ecf154b29a/snowflake_core-1.0.2.tar.gz", hash = "sha256:8bf267ff1efcd17f157432c6e24f6d2eb6c2aeed66f43ab34b215aa76d8edf02", size = 1092618 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/3c/ec228b7325b32781081c72254dd0ef793943e853d82616e862e231909c6c/snowflake_core-1.0.2-py3-none-any.whl", hash = "sha256:55c37cf526a0d78dd3359ad96b9ecd7130bbbbc2f5a2fec77bb3da0dac2dc688", size = 1555690 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "snowflake-legacy"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/41/a6211bd2109913eee1506d37865ab13cf9a8cc2faa41833da3d1ffec654b/snowflake_legacy-1.0.0.tar.gz", hash = "sha256:2044661c79ba01841ab279c5e74b994532244c9d103224eba16eb159c8ed6033", size = 4043 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/8c/64f9b5ee0c3f376a733584c480b31addbf2baff7bb41f655e5e3f3719d3b/snowflake_legacy-1.0.0-py3-none-any.whl", hash = "sha256:25f9678f180d7d5f5b60d17f8112f0ee8a7a77b82c67fd599ed6e27bd502be5a", size = 3059 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sortedcontainers"
|
name = "sortedcontainers"
|
||||||
version = "2.4.0"
|
version = "2.4.0"
|
||||||
@@ -5185,6 +5325,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c4/ac/ce90573ba446a9bbe65838ded066a805234d159b4446ae9f8ec5bbd36cbd/tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7", size = 6440 },
|
{ url = "https://files.pythonhosted.org/packages/c4/ac/ce90573ba446a9bbe65838ded066a805234d159b4446ae9f8ec5bbd36cbd/tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7", size = 6440 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomlkit"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "torch"
|
name = "torch"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
@@ -5194,19 +5343,19 @@ dependencies = [
|
|||||||
{ name = "fsspec" },
|
{ name = "fsspec" },
|
||||||
{ name = "jinja2" },
|
{ name = "jinja2" },
|
||||||
{ name = "networkx" },
|
{ name = "networkx" },
|
||||||
{ name = "nvidia-cublas-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 sys_platform == '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 sys_platform == '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 sys_platform == '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 sys_platform == 'linux'" },
|
{ name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||||
{ name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
{ name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||||
{ name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
{ name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||||
{ name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
{ name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||||
{ name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
{ name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||||
{ name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
{ name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||||
{ name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
{ name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" },
|
||||||
{ name = "sympy" },
|
{ 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" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -5253,7 +5402,7 @@ name = "tqdm"
|
|||||||
version = "4.66.5"
|
version = "4.66.5"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
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 }
|
sdist = { url = "https://files.pythonhosted.org/packages/58/83/6ba9844a41128c62e810fddddd72473201f3eacde02046066142a2d96cc5/tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad", size = 169504 }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -5296,7 +5445,7 @@ version = "0.27.0"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "attrs" },
|
{ 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 = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" },
|
||||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||||
{ name = "idna" },
|
{ name = "idna" },
|
||||||
{ name = "outcome" },
|
{ name = "outcome" },
|
||||||
@@ -5327,7 +5476,7 @@ name = "triton"
|
|||||||
version = "3.0.0"
|
version = "3.0.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
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 = [
|
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 },
|
{ 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 },
|
||||||
|
|||||||
Reference in New Issue
Block a user