Compare commits

..

21 Commits

Author SHA1 Message Date
Brandon Hancock
940fb30e0e quick fix for mike 2025-01-27 17:37:42 -05:00
Brandon Hancock (bhancock_ai)
dea6ed7ef0 fix issue pointed out by mike (#1986)
* fix issue pointed out by mike

* clean up

* Drop logger

* drop unused imports
2025-01-27 17:35:17 -05:00
Brandon Hancock (bhancock_ai)
d3a0dad323 Bugfix/litellm plus generic exceptions (#1965)
* wip

* More clean up

* Fix error

* clean up test

* Improve chat calling messages

* crewai chat improvements

* working but need to clean up

* Clean up chat
2025-01-27 13:41:46 -08:00
devin-ai-integration[bot]
67bf4aea56 Add version check to crew_chat.py (#1966)
* Add version check to crew_chat.py with min version 0.98.0

Co-Authored-By: brandon@crewai.com <brandon@crewai.com>

* Fix import sorting in crew_chat.py

Co-Authored-By: brandon@crewai.com <brandon@crewai.com>

* Fix import sorting in crew_chat.py (attempt 3)

Co-Authored-By: brandon@crewai.com <brandon@crewai.com>

* Update error message, add version check helper, fix import sorting

Co-Authored-By: brandon@crewai.com <brandon@crewai.com>

* Fix import sorting with Ruff auto-fix

Co-Authored-By: brandon@crewai.com <brandon@crewai.com>

* Remove poetry check and import comment headers in crew_chat.py

Co-Authored-By: brandon@crewai.com <brandon@crewai.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: brandon@crewai.com <brandon@crewai.com>
2025-01-24 17:04:41 -05:00
Brandon Hancock (bhancock_ai)
8c76bad50f Fix litellm issues to be more broad (#1960)
* Fix litellm issues to be more broad

* Fix tests
2025-01-23 23:32:10 -05:00
Bobby Lindsey
e27a15023c Add SageMaker as a LLM provider (#1947)
* Add SageMaker as a LLM provider

* Removed unnecessary constants; updated docs to align with bootstrap naming convention

---------

Co-authored-by: Brandon Hancock (bhancock_ai) <109994880+bhancockio@users.noreply.github.com>
2025-01-22 14:55:24 -05:00
Brandon Hancock (bhancock_ai)
a836f466f4 Updated calls and added tests to verify (#1953)
* Updated calls and added tests to verify

* Drop unused import
2025-01-22 14:36:15 -05:00
Brandon Hancock (bhancock_ai)
67f0de1f90 Bugfix/kickoff hangs when llm call fails (#1943)
* Wip to address https://github.com/crewAIInc/crewAI/issues/1934

* implement proper try / except

* clean up PR

* add tests

* Fix tests and code that was broken

* mnore clean up

* Fixing tests

* fix stop type errors]

* more fixes
2025-01-22 14:24:00 -05:00
Tony Kipkemboi
c642ebf97e docs: improve formatting and clarity in CLI and Composio Tool docs (#1946)
* docs: improve formatting and clarity in CLI and Composio Tool docs

- Add Terminal label to shell code blocks in CLI docs
- Update Composio Tool title and fix tip formatting

* docs: improve installation guide with virtual environment details

- Update Python version requirements and commands
- Add detailed virtual environment setup instructions
- Clarify project-specific environment activation steps
- Streamline additional tools installation with UV

* docs: simplify installation guide

- Remove redundant virtual environment instructions
- Simplify project creation steps
- Update UV package manager description
2025-01-22 10:30:16 -05:00
Brandon Hancock (bhancock_ai)
a21e310d78 add docs for crewai chat (#1936)
* add docs for crewai chat

* add version number
2025-01-21 11:10:25 -05:00
Abhishek Patil
aba68da542 feat: add Composio docs (#1904)
* feat: update Composio tool docs

* Update composiotool.mdx

* fix: minor changes

---------

Co-authored-by: Brandon Hancock (bhancock_ai) <109994880+bhancockio@users.noreply.github.com>
2025-01-21 11:03:37 -05:00
Sanjeed
e254f11933 Fix wrong llm value in example (#1929)
Original example had `mixtal-llm` which would result in an error.
Replaced with gpt-4o according to https://docs.crewai.com/concepts/llms
2025-01-21 02:55:27 -03:00
João Moura
ab2274caf0 Stateful flows (#1931)
* fix: ensure persisted state overrides class defaults

- Remove early return in Flow.__init__ to allow proper state initialization
- Add test_flow_default_override.py to verify state override behavior
- Fix issue where default values weren't being overridden by persisted state

Fixes the issue where persisted state values weren't properly overriding
class defaults when restarting a flow with a previously saved state ID.

Co-Authored-By: Joe Moura <joao@crewai.com>

* test: improve state restoration verification with has_set_count flag

Co-Authored-By: Joe Moura <joao@crewai.com>

* test: add has_set_count field to PoemState

Co-Authored-By: Joe Moura <joao@crewai.com>

* refactoring test

* fix: ensure persisted state overrides class defaults

- Remove early return in Flow.__init__ to allow proper state initialization
- Add test_flow_default_override.py to verify state override behavior
- Fix issue where default values weren't being overridden by persisted state

Fixes the issue where persisted state values weren't properly overriding
class defaults when restarting a flow with a previously saved state ID.

Co-Authored-By: Joe Moura <joao@crewai.com>

* test: improve state restoration verification with has_set_count flag

Co-Authored-By: Joe Moura <joao@crewai.com>

* test: add has_set_count field to PoemState

Co-Authored-By: Joe Moura <joao@crewai.com>

* refactoring test

* Fixing flow state

* fixing peristed stateful flows

* linter

* type fix

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Joe Moura <joao@crewai.com>
2025-01-20 13:30:09 -03:00
devin-ai-integration[bot]
3e4f112f39 feat: add colored logging for flow operations (#1923)
* feat: add colored logging for flow operations

- Add flow_id property for easy ID access
- Add yellow colored logging for flow start
- Add bold_yellow colored logging for state operations
- Implement consistent logging across flow lifecycle

Co-Authored-By: Joe Moura <joao@crewai.com>

* fix: sort imports to fix lint error

Co-Authored-By: Joe Moura <joao@crewai.com>

* feat: improve flow logging and error handling

- Add centralized logging method for flow events
- Add robust error handling in persistence decorator
- Add consistent log messages and levels
- Add color-coded error messages

Co-Authored-By: Joe Moura <joao@crewai.com>

* fix: sort imports and improve error handling

Co-Authored-By: Joe Moura <joao@crewai.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Joe Moura <joao@crewai.com>
Co-authored-by: João Moura <joaomdmoura@gmail.com>
2025-01-19 05:50:30 -03:00
João Moura
cc018bf128 updating tools version 2025-01-19 00:36:19 -08:00
devin-ai-integration[bot]
46d3e4d4d9 docs: add flow persistence section (#1922)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Joe Moura <joao@crewai.com>
2025-01-19 04:34:58 -03:00
Brandon Hancock (bhancock_ai)
627bb3f5f6 Brandon/new release cleanup (#1918)
* WIP

* fixes to match enterprise changes
2025-01-18 15:46:41 -03:00
João Moura
4a44245de9 preparing new verison 2025-01-18 10:18:56 -08:00
Brandon Hancock (bhancock_ai)
30d027158a Fix union issue that Daniel was running into (#1910) 2025-01-16 15:54:16 -05:00
fzowl
3fecde49b6 feature: Introducing VoyageAI (#1871)
* Introducing VoyageAI's embedding models

* Adding back the whitespaces

* Adding the whitespaces back
2025-01-16 13:49:46 -05:00
Brandon Hancock (bhancock_ai)
cc129a0bce Fix docling issues (#1909)
* Fix docling issues

* update docs
2025-01-16 12:47:59 -05:00
50 changed files with 2566 additions and 798 deletions

View File

@@ -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.

View File

@@ -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`

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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>

View File

@@ -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)

View File

@@ -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"
] ]

View File

@@ -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",

View File

@@ -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

View File

@@ -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(

View File

@@ -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(

View File

@@ -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()

View File

@@ -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}")

View File

@@ -1,2 +1,3 @@
.env .env
__pycache__/ __pycache__/
.DS_Store

View File

@@ -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]

View File

@@ -1,3 +1,4 @@
.env .env
__pycache__/ __pycache__/
lib/ lib/
.DS_Store

View File

@@ -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

View File

@@ -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]

View File

@@ -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]

View File

@@ -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

View File

@@ -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"]

View File

@@ -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())

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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 = [

View File

@@ -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 (

View File

@@ -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

View File

@@ -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))

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View 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

View 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

View File

@@ -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

View 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

View 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

View File

@@ -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

View File

@@ -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

View 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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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
View File

@@ -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 },