mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-02 12:48:30 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c162411b7 | ||
|
|
8f4a6cc61c | ||
|
|
7dc86dc79a | ||
|
|
7ce20cfcc6 | ||
|
|
1d9523c98f | ||
|
|
9f1d7d1aa9 | ||
|
|
79b375f6fa | ||
|
|
75752479c2 | ||
|
|
477bc1f09e | ||
|
|
66567bdc2f | ||
|
|
0b31bbe957 | ||
|
|
246cf588cd | ||
|
|
88ed91561f | ||
|
|
9a347ad458 | ||
|
|
34c3075fdb | ||
|
|
498e8dc6e8 | ||
|
|
cb522cf500 | ||
|
|
017acc74f5 | ||
|
|
fab86d197a | ||
|
|
864e9bfb76 | ||
|
|
d3b45d197c | ||
|
|
579153b070 |
@@ -218,6 +218,7 @@
|
||||
"en/observability/overview",
|
||||
"en/observability/agentops",
|
||||
"en/observability/arize-phoenix",
|
||||
"en/observability/langdb",
|
||||
"en/observability/langfuse",
|
||||
"en/observability/langtrace",
|
||||
"en/observability/maxim",
|
||||
@@ -555,6 +556,7 @@
|
||||
"pt-BR/observability/overview",
|
||||
"pt-BR/observability/agentops",
|
||||
"pt-BR/observability/arize-phoenix",
|
||||
"pt-BR/observability/langdb",
|
||||
"pt-BR/observability/langfuse",
|
||||
"pt-BR/observability/langtrace",
|
||||
"pt-BR/observability/maxim",
|
||||
|
||||
@@ -88,7 +88,7 @@ crewai replay [OPTIONS]
|
||||
- `-t, --task_id TEXT`: Replay the crew from this task ID, including all subsequent tasks
|
||||
|
||||
Example:
|
||||
```shell Terminal
|
||||
```shell Terminal
|
||||
crewai replay -t task_123456
|
||||
```
|
||||
|
||||
@@ -134,7 +134,7 @@ crewai test [OPTIONS]
|
||||
- `-m, --model TEXT`: LLM Model to run the tests on the Crew (default: "gpt-4o-mini")
|
||||
|
||||
Example:
|
||||
```shell Terminal
|
||||
```shell Terminal
|
||||
crewai test -n 5 -m gpt-3.5-turbo
|
||||
```
|
||||
|
||||
@@ -151,7 +151,7 @@ Starting from version 0.103.0, the `crewai run` command can be used to run both
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
Make sure to run these commands from the directory where your CrewAI project is set up.
|
||||
Make sure to run these commands from the directory where your CrewAI project is set up.
|
||||
Some commands may require additional configuration or setup within your project structure.
|
||||
</Note>
|
||||
|
||||
@@ -235,7 +235,7 @@ You must be authenticated to CrewAI Enterprise to use these organization managem
|
||||
- **Deploy the Crew**: Once you are authenticated, you can deploy your crew or flow to CrewAI Enterprise.
|
||||
```shell Terminal
|
||||
crewai deploy push
|
||||
```
|
||||
```
|
||||
- Initiates the deployment process on the CrewAI Enterprise platform.
|
||||
- Upon successful initiation, it will output the Deployment created successfully! message along with the Deployment Name and a unique Deployment ID (UUID).
|
||||
|
||||
@@ -309,3 +309,82 @@ When you select a provider, the CLI will prompt you to enter the Key name and th
|
||||
See the following link for each provider's key name:
|
||||
|
||||
* [LiteLLM Providers](https://docs.litellm.ai/docs/providers)
|
||||
|
||||
### 12. Configuration Management
|
||||
|
||||
Manage CLI configuration settings for CrewAI.
|
||||
|
||||
```shell Terminal
|
||||
crewai config [COMMAND] [OPTIONS]
|
||||
```
|
||||
|
||||
#### Commands:
|
||||
|
||||
- `list`: Display all CLI configuration parameters
|
||||
```shell Terminal
|
||||
crewai config list
|
||||
```
|
||||
|
||||
- `set`: Set a CLI configuration parameter
|
||||
```shell Terminal
|
||||
crewai config set <key> <value>
|
||||
```
|
||||
|
||||
- `reset`: Reset all CLI configuration parameters to default values
|
||||
```shell Terminal
|
||||
crewai config reset
|
||||
```
|
||||
|
||||
#### Available Configuration Parameters
|
||||
|
||||
- `enterprise_base_url`: Base URL of the CrewAI Enterprise instance
|
||||
- `oauth2_provider`: OAuth2 provider used for authentication (e.g., workos, okta, auth0)
|
||||
- `oauth2_audience`: OAuth2 audience value, typically used to identify the target API or resource
|
||||
- `oauth2_client_id`: OAuth2 client ID issued by the provider, used during authentication requests
|
||||
- `oauth2_domain`: OAuth2 provider's domain (e.g., your-org.auth0.com) used for issuing tokens
|
||||
|
||||
#### Examples
|
||||
|
||||
Display current configuration:
|
||||
```shell Terminal
|
||||
crewai config list
|
||||
```
|
||||
|
||||
Example output:
|
||||
```
|
||||
CrewAI CLI Configuration
|
||||
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Setting ┃ Value ┃ Description ┃
|
||||
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ enterprise_base_url│ https://app.crewai.com │ Base URL of the CrewAI Enterprise instance │
|
||||
│ org_name │ Not set │ Name of the currently active organization │
|
||||
│ org_uuid │ Not set │ UUID of the currently active organization │
|
||||
│ oauth2_provider │ workos │ OAuth2 provider used for authentication (e.g., workos, okta, auth0). │
|
||||
│ oauth2_audience │ client_01YYY │ OAuth2 audience value, typically used to identify the target API or resource. │
|
||||
│ oauth2_client_id │ client_01XXX │ OAuth2 client ID issued by the provider, used during authentication requests. │
|
||||
│ oauth2_domain │ login.crewai.com │ OAuth2 provider's domain (e.g., your-org.auth0.com) used for issuing tokens. │
|
||||
```
|
||||
|
||||
Set the enterprise base URL:
|
||||
```shell Terminal
|
||||
crewai config set enterprise_base_url https://my-enterprise.crewai.com
|
||||
```
|
||||
|
||||
Set OAuth2 provider:
|
||||
```shell Terminal
|
||||
crewai config set oauth2_provider auth0
|
||||
```
|
||||
|
||||
Set OAuth2 domain:
|
||||
```shell Terminal
|
||||
crewai config set oauth2_domain my-company.auth0.com
|
||||
```
|
||||
|
||||
Reset all configuration to defaults:
|
||||
```shell Terminal
|
||||
crewai config reset
|
||||
```
|
||||
|
||||
<Note>
|
||||
Configuration settings are stored in `~/.config/crewai/settings.json`. Some settings like organization name and UUID are read-only and managed through authentication and organization commands. Tool repository related settings are hidden and cannot be set directly by users.
|
||||
</Note>
|
||||
|
||||
@@ -20,8 +20,7 @@ A crew in crewAI represents a collaborative group of agents working together to
|
||||
| **Function Calling LLM** _(optional)_ | `function_calling_llm` | If passed, the crew will use this LLM to do function calling for tools for all agents in the crew. Each agent can have its own LLM, which overrides the crew's LLM for function calling. |
|
||||
| **Config** _(optional)_ | `config` | Optional configuration settings for the crew, in `Json` or `Dict[str, Any]` format. |
|
||||
| **Max RPM** _(optional)_ | `max_rpm` | Maximum requests per minute the crew adheres to during execution. Defaults to `None`. |
|
||||
| **Memory** _(optional)_ | `memory` | Utilized for storing execution memories (short-term, long-term, entity memory). |
|
||||
| **Memory Config** _(optional)_ | `memory_config` | Configuration for the memory provider to be used by the crew. |
|
||||
| **Memory** _(optional)_ | `memory` | Utilized for storing execution memories (short-term, long-term, entity memory). | |
|
||||
| **Cache** _(optional)_ | `cache` | Specifies whether to use a cache for storing the results of tools' execution. Defaults to `True`. |
|
||||
| **Embedder** _(optional)_ | `embedder` | Configuration for the embedder to be used by the crew. Mostly used by memory for now. Default is `{"provider": "openai"}`. |
|
||||
| **Step Callback** _(optional)_ | `step_callback` | A function that is called after each step of every agent. This can be used to log the agent's actions or to perform other operations; it won't override the agent-specific `step_callback`. |
|
||||
|
||||
@@ -270,7 +270,7 @@ In this section, you'll find detailed examples that help you select, configure,
|
||||
from crewai import LLM
|
||||
|
||||
llm = LLM(
|
||||
model="gemini/gemini-1.5-pro-latest",
|
||||
model="gemini-1.5-pro-latest", # or vertex_ai/gemini-1.5-pro-latest
|
||||
temperature=0.7,
|
||||
vertex_credentials=vertex_credentials_json
|
||||
)
|
||||
|
||||
@@ -9,8 +9,7 @@ icon: database
|
||||
The CrewAI framework provides a sophisticated memory system designed to significantly enhance AI agent capabilities. CrewAI offers **three distinct memory approaches** that serve different use cases:
|
||||
|
||||
1. **Basic Memory System** - Built-in short-term, long-term, and entity memory
|
||||
2. **User Memory** - User-specific memory with Mem0 integration (legacy approach)
|
||||
3. **External Memory** - Standalone external memory providers (new approach)
|
||||
2. **External Memory** - Standalone external memory providers
|
||||
|
||||
## Memory System Components
|
||||
|
||||
@@ -19,7 +18,7 @@ The CrewAI framework provides a sophisticated memory system designed to signific
|
||||
| **Short-Term Memory**| Temporarily stores recent interactions and outcomes using `RAG`, enabling agents to recall and utilize information relevant to their current context during the current executions.|
|
||||
| **Long-Term Memory** | Preserves valuable insights and learnings from past executions, allowing agents to build and refine their knowledge over time. |
|
||||
| **Entity Memory** | Captures and organizes information about entities (people, places, concepts) encountered during tasks, facilitating deeper understanding and relationship mapping. Uses `RAG` for storing entity information. |
|
||||
| **Contextual Memory**| Maintains the context of interactions by combining `ShortTermMemory`, `LongTermMemory`, and `EntityMemory`, aiding in the coherence and relevance of agent responses over a sequence of tasks or a conversation. |
|
||||
| **Contextual Memory**| Maintains the context of interactions by combining `ShortTermMemory`, `LongTermMemory`, `ExternalMemory` and `EntityMemory`, aiding in the coherence and relevance of agent responses over a sequence of tasks or a conversation. |
|
||||
|
||||
## 1. Basic Memory System (Recommended)
|
||||
|
||||
@@ -202,7 +201,7 @@ crew = Crew(
|
||||
tasks=[task],
|
||||
memory=True,
|
||||
embedder={
|
||||
"provider": "anthropic", # Match your LLM provider
|
||||
"provider": "anthropic", # Match your LLM provider
|
||||
"config": {
|
||||
"api_key": "your-anthropic-key",
|
||||
"model": "text-embedding-3-small"
|
||||
@@ -623,7 +622,7 @@ for provider in providers_to_test:
|
||||
**Model not found errors:**
|
||||
```python
|
||||
# Verify model availability
|
||||
from crewai.utilities.embedding_configurator import EmbeddingConfigurator
|
||||
from crewai.rag.embeddings.configurator import EmbeddingConfigurator
|
||||
|
||||
configurator = EmbeddingConfigurator()
|
||||
try:
|
||||
@@ -684,81 +683,18 @@ print(f"OpenAI: {openai_time:.2f}s")
|
||||
print(f"Ollama: {ollama_time:.2f}s")
|
||||
```
|
||||
|
||||
## 2. User Memory with Mem0 (Legacy)
|
||||
## 2. External Memory
|
||||
External Memory provides a standalone memory system that operates independently from the crew's built-in memory. This is ideal for specialized memory providers or cross-application memory sharing.
|
||||
|
||||
<Warning>
|
||||
**Legacy Approach**: While fully functional, this approach is considered legacy. For new projects requiring user-specific memory, consider using External Memory instead.
|
||||
</Warning>
|
||||
|
||||
User Memory integrates with [Mem0](https://mem0.ai/) to provide user-specific memory that persists across sessions and integrates with the crew's contextual memory system.
|
||||
|
||||
### Prerequisites
|
||||
```bash
|
||||
pip install mem0ai
|
||||
```
|
||||
|
||||
### Mem0 Cloud Configuration
|
||||
### Basic External Memory with Mem0
|
||||
```python
|
||||
import os
|
||||
from crewai import Crew, Process
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.memory.external.external_memory import ExternalMemory
|
||||
|
||||
# Set your Mem0 API key
|
||||
os.environ["MEM0_API_KEY"] = "m0-your-api-key"
|
||||
|
||||
crew = Crew(
|
||||
agents=[...],
|
||||
tasks=[...],
|
||||
memory=True, # Required for contextual memory integration
|
||||
memory_config={
|
||||
"provider": "mem0",
|
||||
"config": {"user_id": "john"},
|
||||
"user_memory": {} # DEPRECATED: Will be removed in version 0.156.0 or on 2025-08-04, use external_memory instead
|
||||
},
|
||||
process=Process.sequential,
|
||||
verbose=True
|
||||
)
|
||||
```
|
||||
|
||||
### Advanced Mem0 Configuration
|
||||
When using Mem0 Client, you can customize the memory configuration further, by using parameters like 'includes', 'excludes', 'custom_categories' and 'run_id' (this is only for short-term memory).
|
||||
You can find more details in the [Mem0 documentation](https://docs.mem0.ai/).
|
||||
```python
|
||||
|
||||
new_categories = [
|
||||
{"lifestyle_management_concerns": "Tracks daily routines, habits, hobbies and interests including cooking, time management and work-life balance"},
|
||||
{"seeking_structure": "Documents goals around creating routines, schedules, and organized systems in various life areas"},
|
||||
{"personal_information": "Basic information about the user including name, preferences, and personality traits"}
|
||||
]
|
||||
|
||||
crew = Crew(
|
||||
agents=[...],
|
||||
tasks=[...],
|
||||
memory=True,
|
||||
memory_config={
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"user_id": "john",
|
||||
"org_id": "my_org_id", # Optional
|
||||
"project_id": "my_project_id", # Optional
|
||||
"api_key": "custom-api-key" # Optional - overrides env var
|
||||
"run_id": "my_run_id", # Optional - for short-term memory
|
||||
"includes": "include1", # Optional
|
||||
"excludes": "exclude1", # Optional
|
||||
"infer": True
|
||||
"custom_categories": new_categories # Optional - custom categories for user memory
|
||||
},
|
||||
"user_memory": {}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Local Mem0 Configuration
|
||||
```python
|
||||
crew = Crew(
|
||||
agents=[...],
|
||||
tasks=[...],
|
||||
memory=True,
|
||||
memory_config={
|
||||
# Create external memory instance with local Mem0 Configuration
|
||||
external_memory = ExternalMemory(
|
||||
embedder_config={
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"user_id": "john",
|
||||
@@ -776,37 +712,59 @@ crew = Crew(
|
||||
"config": {"api_key": "your-api-key", "model": "text-embedding-3-small"}
|
||||
}
|
||||
},
|
||||
"infer": True
|
||||
"infer": True # Optional defaults to True
|
||||
},
|
||||
"user_memory": {}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## 3. External Memory (New Approach)
|
||||
|
||||
External Memory provides a standalone memory system that operates independently from the crew's built-in memory. This is ideal for specialized memory providers or cross-application memory sharing.
|
||||
|
||||
### Basic External Memory with Mem0
|
||||
```python
|
||||
import os
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.memory.external.external_memory import ExternalMemory
|
||||
|
||||
os.environ["MEM0_API_KEY"] = "your-api-key"
|
||||
|
||||
# Create external memory instance
|
||||
external_memory = ExternalMemory(
|
||||
embedder_config={
|
||||
"provider": "mem0",
|
||||
"config": {"user_id": "U-123"}
|
||||
}
|
||||
)
|
||||
|
||||
crew = Crew(
|
||||
agents=[...],
|
||||
tasks=[...],
|
||||
external_memory=external_memory, # Separate from basic memory
|
||||
external_memory=external_memory, # Separate from basic memory
|
||||
process=Process.sequential,
|
||||
verbose=True
|
||||
)
|
||||
```
|
||||
|
||||
### Advanced External Memory with Mem0 Client
|
||||
When using Mem0 Client, you can customize the memory configuration further, by using parameters like 'includes', 'excludes', 'custom_categories', 'infer' and 'run_id' (this is only for short-term memory).
|
||||
You can find more details in the [Mem0 documentation](https://docs.mem0.ai/).
|
||||
|
||||
```python
|
||||
import os
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.memory.external.external_memory import ExternalMemory
|
||||
|
||||
new_categories = [
|
||||
{"lifestyle_management_concerns": "Tracks daily routines, habits, hobbies and interests including cooking, time management and work-life balance"},
|
||||
{"seeking_structure": "Documents goals around creating routines, schedules, and organized systems in various life areas"},
|
||||
{"personal_information": "Basic information about the user including name, preferences, and personality traits"}
|
||||
]
|
||||
|
||||
os.environ["MEM0_API_KEY"] = "your-api-key"
|
||||
|
||||
# Create external memory instance with Mem0 Client
|
||||
external_memory = ExternalMemory(
|
||||
embedder_config={
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"user_id": "john",
|
||||
"org_id": "my_org_id", # Optional
|
||||
"project_id": "my_project_id", # Optional
|
||||
"api_key": "custom-api-key" # Optional - overrides env var
|
||||
"run_id": "my_run_id", # Optional - for short-term memory
|
||||
"includes": "include1", # Optional
|
||||
"excludes": "exclude1", # Optional
|
||||
"infer": True # Optional defaults to True
|
||||
"custom_categories": new_categories # Optional - custom categories for user memory
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
crew = Crew(
|
||||
agents=[...],
|
||||
tasks=[...],
|
||||
external_memory=external_memory, # Separate from basic memory
|
||||
process=Process.sequential,
|
||||
verbose=True
|
||||
)
|
||||
@@ -845,17 +803,18 @@ crew = Crew(
|
||||
)
|
||||
```
|
||||
|
||||
## Memory System Comparison
|
||||
## 🧠 Memory System Comparison
|
||||
|
||||
| **Category** | **Feature** | **Basic Memory** | **External Memory** |
|
||||
|---------------------|------------------------|-----------------------------|------------------------------|
|
||||
| **Ease of Use** | Setup Complexity | Simple | Moderate |
|
||||
| | Integration | Built-in (contextual) | Standalone |
|
||||
| **Persistence** | Storage | Local files | Custom / Mem0 |
|
||||
| | Cross-session Support | ✅ | ✅ |
|
||||
| **Personalization** | User-specific Memory | ❌ | ✅ |
|
||||
| | Custom Providers | Limited | Any provider |
|
||||
| **Use Case Fit** | Recommended For | Most general use cases | Specialized / custom needs |
|
||||
|
||||
| Feature | Basic Memory | User Memory (Legacy) | External Memory |
|
||||
|---------|-------------|---------------------|----------------|
|
||||
| **Setup Complexity** | Simple | Medium | Medium |
|
||||
| **Integration** | Built-in contextual | Contextual + User-specific | Standalone |
|
||||
| **Storage** | Local files | Mem0 Cloud/Local | Custom/Mem0 |
|
||||
| **Cross-session** | ✅ | ✅ | ✅ |
|
||||
| **User-specific** | ❌ | ✅ | ✅ |
|
||||
| **Custom providers** | Limited | Mem0 only | Any provider |
|
||||
| **Recommended for** | Most use cases | Legacy projects | Specialized needs |
|
||||
|
||||
## Supported Embedding Providers
|
||||
|
||||
|
||||
@@ -44,6 +44,19 @@ The `MCPServerAdapter` class from `crewai-tools` is the primary way to connect t
|
||||
|
||||
Using a Python context manager (`with` statement) is the **recommended approach** for `MCPServerAdapter`. It automatically handles starting and stopping the connection to the MCP server.
|
||||
|
||||
## Connection Configuration
|
||||
|
||||
The `MCPServerAdapter` supports several configuration options to customize the connection behavior:
|
||||
|
||||
- **`connect_timeout`** (optional): Maximum time in seconds to wait for establishing a connection to the MCP server. Defaults to 30 seconds if not specified. This is particularly useful for remote servers that may have variable response times.
|
||||
|
||||
```python
|
||||
# Example with custom connection timeout
|
||||
with MCPServerAdapter(server_params, connect_timeout=60) as tools:
|
||||
# Connection will timeout after 60 seconds if not established
|
||||
pass
|
||||
```
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
from crewai_tools import MCPServerAdapter
|
||||
@@ -70,7 +83,7 @@ server_params = {
|
||||
}
|
||||
|
||||
# Example usage (uncomment and adapt once server_params is set):
|
||||
with MCPServerAdapter(server_params) as mcp_tools:
|
||||
with MCPServerAdapter(server_params, connect_timeout=60) as mcp_tools:
|
||||
print(f"Available tools: {[tool.name for tool in mcp_tools]}")
|
||||
|
||||
my_agent = Agent(
|
||||
@@ -95,7 +108,7 @@ There are two ways to filter tools:
|
||||
### Accessing a specific tool using dictionary-style indexing.
|
||||
|
||||
```python
|
||||
with MCPServerAdapter(server_params) as mcp_tools:
|
||||
with MCPServerAdapter(server_params, connect_timeout=60) as mcp_tools:
|
||||
print(f"Available tools: {[tool.name for tool in mcp_tools]}")
|
||||
|
||||
my_agent = Agent(
|
||||
@@ -112,7 +125,7 @@ with MCPServerAdapter(server_params) as mcp_tools:
|
||||
### Pass a list of tool names to the `MCPServerAdapter` constructor.
|
||||
|
||||
```python
|
||||
with MCPServerAdapter(server_params, "tool_name") as mcp_tools:
|
||||
with MCPServerAdapter(server_params, "tool_name", connect_timeout=60) as mcp_tools:
|
||||
print(f"Available tools: {[tool.name for tool in mcp_tools]}")
|
||||
|
||||
my_agent = Agent(
|
||||
|
||||
286
docs/en/observability/langdb.mdx
Normal file
286
docs/en/observability/langdb.mdx
Normal file
@@ -0,0 +1,286 @@
|
||||
---
|
||||
title: LangDB Integration
|
||||
description: Govern, secure, and optimize your CrewAI workflows with LangDB AI Gateway—access 350+ models, automatic routing, cost optimization, and full observability.
|
||||
icon: database
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
[LangDB AI Gateway](https://langdb.ai) provides OpenAI-compatible APIs to connect with multiple Large Language Models and serves as an observability platform that makes it effortless to trace CrewAI workflows end-to-end while providing access to 350+ language models. With a single `init()` call, all agent interactions, task executions, and LLM calls are captured, providing comprehensive observability and production-ready AI infrastructure for your applications.
|
||||
|
||||
<Frame caption="LangDB CrewAI Trace Example">
|
||||
<img src="/images/langdb-1.png" alt="LangDB CrewAI trace example" />
|
||||
</Frame>
|
||||
|
||||
**Checkout:** [View the live trace example](https://app.langdb.ai/sharing/threads/3becbfed-a1be-ae84-ea3c-4942867a3e22)
|
||||
|
||||
## Features
|
||||
|
||||
### AI Gateway Capabilities
|
||||
- **Access to 350+ LLMs**: Connect to all major language models through a single integration
|
||||
- **Virtual Models**: Create custom model configurations with specific parameters and routing rules
|
||||
- **Virtual MCP**: Enable compatibility and integration with MCP (Model Context Protocol) systems for enhanced agent communication
|
||||
- **Guardrails**: Implement safety measures and compliance controls for agent behavior
|
||||
|
||||
### Observability & Tracing
|
||||
- **Automatic Tracing**: Single `init()` call captures all CrewAI interactions
|
||||
- **End-to-End Visibility**: Monitor agent workflows from start to finish
|
||||
- **Tool Usage Tracking**: Track which tools agents use and their outcomes
|
||||
- **Model Call Monitoring**: Detailed insights into LLM interactions
|
||||
- **Performance Analytics**: Monitor latency, token usage, and costs
|
||||
- **Debugging Support**: Step-through execution for troubleshooting
|
||||
- **Real-time Monitoring**: Live traces and metrics dashboard
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
<Steps>
|
||||
<Step title="Install LangDB">
|
||||
Install the LangDB client with CrewAI feature flag:
|
||||
```bash
|
||||
pip install 'pylangdb[crewai]'
|
||||
```
|
||||
</Step>
|
||||
<Step title="Set Environment Variables">
|
||||
Configure your LangDB credentials:
|
||||
```bash
|
||||
export LANGDB_API_KEY="<your_langdb_api_key>"
|
||||
export LANGDB_PROJECT_ID="<your_langdb_project_id>"
|
||||
export LANGDB_API_BASE_URL='https://api.us-east-1.langdb.ai'
|
||||
```
|
||||
</Step>
|
||||
<Step title="Initialize Tracing">
|
||||
Import and initialize LangDB before configuring your CrewAI code:
|
||||
```python
|
||||
from pylangdb.crewai import init
|
||||
# Initialize LangDB
|
||||
init()
|
||||
```
|
||||
</Step>
|
||||
<Step title="Configure CrewAI with LangDB">
|
||||
Set up your LLM with LangDB headers:
|
||||
```python
|
||||
from crewai import Agent, Task, Crew, LLM
|
||||
import os
|
||||
|
||||
# Configure LLM with LangDB headers
|
||||
llm = LLM(
|
||||
model="openai/gpt-4o", # Replace with the model you want to use
|
||||
api_key=os.getenv("LANGDB_API_KEY"),
|
||||
base_url=os.getenv("LANGDB_API_BASE_URL"),
|
||||
extra_headers={"x-project-id": os.getenv("LANGDB_PROJECT_ID")}
|
||||
)
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Quick Start Example
|
||||
|
||||
Here's a simple example to get you started with LangDB and CrewAI:
|
||||
|
||||
```python
|
||||
import os
|
||||
from pylangdb.crewai import init
|
||||
from crewai import Agent, Task, Crew, LLM
|
||||
|
||||
# Initialize LangDB before any CrewAI imports
|
||||
init()
|
||||
|
||||
def create_llm(model):
|
||||
return LLM(
|
||||
model=model,
|
||||
api_key=os.environ.get("LANGDB_API_KEY"),
|
||||
base_url=os.environ.get("LANGDB_API_BASE_URL"),
|
||||
extra_headers={"x-project-id": os.environ.get("LANGDB_PROJECT_ID")}
|
||||
)
|
||||
|
||||
# Define your agent
|
||||
researcher = Agent(
|
||||
role="Research Specialist",
|
||||
goal="Research topics thoroughly",
|
||||
backstory="Expert researcher with skills in finding information",
|
||||
llm=create_llm("openai/gpt-4o"), # Replace with the model you want to use
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Create a task
|
||||
task = Task(
|
||||
description="Research the given topic and provide a comprehensive summary",
|
||||
agent=researcher,
|
||||
expected_output="Detailed research summary with key findings"
|
||||
)
|
||||
|
||||
# Create and run the crew
|
||||
crew = Crew(agents=[researcher], tasks=[task])
|
||||
result = crew.kickoff()
|
||||
print(result)
|
||||
```
|
||||
|
||||
## Complete Example: Research and Planning Agent
|
||||
|
||||
This comprehensive example demonstrates a multi-agent workflow with research and planning capabilities.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
pip install crewai 'pylangdb[crewai]' crewai_tools setuptools python-dotenv
|
||||
```
|
||||
|
||||
### Environment Setup
|
||||
|
||||
```bash
|
||||
# LangDB credentials
|
||||
export LANGDB_API_KEY="<your_langdb_api_key>"
|
||||
export LANGDB_PROJECT_ID="<your_langdb_project_id>"
|
||||
export LANGDB_API_BASE_URL='https://api.us-east-1.langdb.ai'
|
||||
|
||||
# Additional API keys (optional)
|
||||
export SERPER_API_KEY="<your_serper_api_key>" # For web search capabilities
|
||||
```
|
||||
|
||||
### Complete Implementation
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pylangdb.crewai import init
|
||||
init() # Initialize LangDB before any CrewAI imports
|
||||
from dotenv import load_dotenv
|
||||
from crewai import Agent, Task, Crew, Process, LLM
|
||||
from crewai_tools import SerperDevTool
|
||||
|
||||
load_dotenv()
|
||||
|
||||
def create_llm(model):
|
||||
return LLM(
|
||||
model=model,
|
||||
api_key=os.environ.get("LANGDB_API_KEY"),
|
||||
base_url=os.environ.get("LANGDB_API_BASE_URL"),
|
||||
extra_headers={"x-project-id": os.environ.get("LANGDB_PROJECT_ID")}
|
||||
)
|
||||
|
||||
class ResearchPlanningCrew:
|
||||
def researcher(self) -> Agent:
|
||||
return Agent(
|
||||
role="Research Specialist",
|
||||
goal="Research topics thoroughly and compile comprehensive information",
|
||||
backstory="Expert researcher with skills in finding and analyzing information from various sources",
|
||||
tools=[SerperDevTool()],
|
||||
llm=create_llm("openai/gpt-4o"),
|
||||
verbose=True
|
||||
)
|
||||
|
||||
def planner(self) -> Agent:
|
||||
return Agent(
|
||||
role="Strategic Planner",
|
||||
goal="Create actionable plans based on research findings",
|
||||
backstory="Strategic planner who breaks down complex challenges into executable plans",
|
||||
reasoning=True,
|
||||
max_reasoning_attempts=3,
|
||||
llm=create_llm("openai/anthropic/claude-3.7-sonnet"),
|
||||
verbose=True
|
||||
)
|
||||
|
||||
def research_task(self) -> Task:
|
||||
return Task(
|
||||
description="Research the topic thoroughly and compile comprehensive information",
|
||||
agent=self.researcher(),
|
||||
expected_output="Comprehensive research report with key findings and insights"
|
||||
)
|
||||
|
||||
def planning_task(self) -> Task:
|
||||
return Task(
|
||||
description="Create a strategic plan based on the research findings",
|
||||
agent=self.planner(),
|
||||
expected_output="Strategic execution plan with phases, goals, and actionable steps",
|
||||
context=[self.research_task()]
|
||||
)
|
||||
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=[self.researcher(), self.planner()],
|
||||
tasks=[self.research_task(), self.planning_task()],
|
||||
verbose=True,
|
||||
process=Process.sequential
|
||||
)
|
||||
|
||||
def main():
|
||||
topic = sys.argv[1] if len(sys.argv) > 1 else "Artificial Intelligence in Healthcare"
|
||||
|
||||
crew_instance = ResearchPlanningCrew()
|
||||
|
||||
# Update task descriptions with the specific topic
|
||||
crew_instance.research_task().description = f"Research {topic} thoroughly and compile comprehensive information"
|
||||
crew_instance.planning_task().description = f"Create a strategic plan for {topic} based on the research findings"
|
||||
|
||||
result = crew_instance.crew().kickoff()
|
||||
print(result)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
### Running the Example
|
||||
|
||||
```bash
|
||||
python main.py "Sustainable Energy Solutions"
|
||||
```
|
||||
|
||||
## Viewing Traces in LangDB
|
||||
|
||||
After running your CrewAI application, you can view detailed traces in the LangDB dashboard:
|
||||
|
||||
<Frame caption="LangDB Trace Dashboard">
|
||||
<img src="/images/langdb-2.png" alt="LangDB trace dashboard showing CrewAI workflow" />
|
||||
</Frame>
|
||||
|
||||
### What You'll See
|
||||
|
||||
- **Agent Interactions**: Complete flow of agent conversations and task handoffs
|
||||
- **Tool Usage**: Which tools were called, their inputs, and outputs
|
||||
- **Model Calls**: Detailed LLM interactions with prompts image.pngand responses
|
||||
- **Performance Metrics**: Latency, token usage, and cost tracking
|
||||
- **Execution Timeline**: Step-by-step view of the entire workflow
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
- **No traces appearing**: Ensure `init()` is called before any CrewAI imports
|
||||
- **Authentication errors**: Verify your LangDB API key and project ID
|
||||
|
||||
|
||||
## Resources
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="LangDB Documentation" icon="book" href="https://docs.langdb.ai">
|
||||
Official LangDB documentation and guides
|
||||
</Card>
|
||||
<Card title="LangDB Guides" icon="graduation-cap" href="https://docs.langdb.ai/guides">
|
||||
Step-by-step tutorials for building AI agents
|
||||
</Card>
|
||||
<Card title="GitHub Examples" icon="github" href="https://github.com/langdb/langdb-samples/tree/main/examples/crewai" >
|
||||
Complete CrewAI integration examples
|
||||
</Card>
|
||||
<Card title="LangDB Dashboard" icon="chart-line" href="https://app.langdb.ai">
|
||||
Access your traces and analytics
|
||||
</Card>
|
||||
<Card title="Model Catalog" icon="list" href="https://app.langdb.ai/models">
|
||||
Browse 350+ available language models
|
||||
</Card>
|
||||
<Card title="Enterprise Features" icon="building" href="https://docs.langdb.ai/enterprise">
|
||||
Self-hosted options and enterprise capabilities
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Next Steps
|
||||
|
||||
This guide covered the basics of integrating LangDB AI Gateway with CrewAI. To further enhance your AI workflows, explore:
|
||||
|
||||
- **Virtual Models**: Create custom model configurations with routing strategies
|
||||
- **Guardrails & Safety**: Implement content filtering and compliance controls
|
||||
- **Production Deployment**: Configure fallbacks, retries, and load balancing
|
||||
|
||||
For more advanced features and use cases, visit the [LangDB Documentation](https://docs.langdb.ai) or explore the [Model Catalog](https://app.langdb.ai/models) to discover all available models.
|
||||
@@ -25,6 +25,10 @@ Observability is crucial for understanding how your CrewAI agents perform, ident
|
||||
Session replays, metrics, and monitoring for agent development and production.
|
||||
</Card>
|
||||
|
||||
<Card title="LangDB" icon="database" href="/en/observability/langdb">
|
||||
End-to-end tracing for CrewAI workflows with automatic agent interaction capture.
|
||||
</Card>
|
||||
|
||||
<Card title="OpenLIT" icon="magnifying-glass-chart" href="/en/observability/openlit">
|
||||
OpenTelemetry-native monitoring with cost tracking and performance analytics.
|
||||
</Card>
|
||||
|
||||
BIN
docs/images/langdb-1.png
Normal file
BIN
docs/images/langdb-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
BIN
docs/images/langdb-2.png
Normal file
BIN
docs/images/langdb-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
@@ -84,8 +84,8 @@ filename = "seu_modelo.pkl"
|
||||
|
||||
try:
|
||||
SuaCrew().crew().train(
|
||||
n_iterations=n_iterations,
|
||||
inputs=inputs,
|
||||
n_iterations=n_iterations,
|
||||
inputs=inputs,
|
||||
filename=filename
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -103,7 +103,7 @@ crewai replay [OPTIONS]
|
||||
- `-t, --task_id TEXT`: Reexecuta o crew a partir deste task ID, incluindo todas as tarefas subsequentes
|
||||
|
||||
Exemplo:
|
||||
```shell Terminal
|
||||
```shell Terminal
|
||||
crewai replay -t task_123456
|
||||
```
|
||||
|
||||
@@ -149,7 +149,7 @@ crewai test [OPTIONS]
|
||||
- `-m, --model TEXT`: Modelo LLM para executar os testes no Crew (padrão: "gpt-4o-mini")
|
||||
|
||||
Exemplo:
|
||||
```shell Terminal
|
||||
```shell Terminal
|
||||
crewai test -n 5 -m gpt-3.5-turbo
|
||||
```
|
||||
|
||||
@@ -203,10 +203,7 @@ def crew(self) -> Crew:
|
||||
Implemente o crew ou flow no [CrewAI Enterprise](https://app.crewai.com).
|
||||
|
||||
- **Autenticação**: Você precisa estar autenticado para implementar no CrewAI Enterprise.
|
||||
```shell Terminal
|
||||
crewai signup
|
||||
```
|
||||
Caso já tenha uma conta, você pode fazer login com:
|
||||
Você pode fazer login ou criar uma conta com:
|
||||
```shell Terminal
|
||||
crewai login
|
||||
```
|
||||
@@ -253,7 +250,7 @@ Você deve estar autenticado no CrewAI Enterprise para usar estes comandos de ge
|
||||
- **Implantar o Crew**: Depois de autenticado, você pode implantar seu crew ou flow no CrewAI Enterprise.
|
||||
```shell Terminal
|
||||
crewai deploy push
|
||||
```
|
||||
```
|
||||
- Inicia o processo de deployment na plataforma CrewAI Enterprise.
|
||||
- Após a iniciação bem-sucedida, será exibida a mensagem Deployment created successfully! juntamente com o Nome do Deployment e um Deployment ID (UUID) único.
|
||||
|
||||
@@ -326,4 +323,83 @@ Ao escolher um provedor, o CLI solicitará que você informe o nome da chave e a
|
||||
|
||||
Veja o seguinte link para o nome de chave de cada provedor:
|
||||
|
||||
* [LiteLLM Providers](https://docs.litellm.ai/docs/providers)
|
||||
* [LiteLLM Providers](https://docs.litellm.ai/docs/providers)
|
||||
|
||||
### 12. Gerenciamento de Configuração
|
||||
|
||||
Gerencie as configurações do CLI para CrewAI.
|
||||
|
||||
```shell Terminal
|
||||
crewai config [COMANDO] [OPÇÕES]
|
||||
```
|
||||
|
||||
#### Comandos:
|
||||
|
||||
- `list`: Exibir todos os parâmetros de configuração do CLI
|
||||
```shell Terminal
|
||||
crewai config list
|
||||
```
|
||||
|
||||
- `set`: Definir um parâmetro de configuração do CLI
|
||||
```shell Terminal
|
||||
crewai config set <chave> <valor>
|
||||
```
|
||||
|
||||
- `reset`: Redefinir todos os parâmetros de configuração do CLI para valores padrão
|
||||
```shell Terminal
|
||||
crewai config reset
|
||||
```
|
||||
|
||||
#### Parâmetros de Configuração Disponíveis
|
||||
|
||||
- `enterprise_base_url`: URL base da instância CrewAI Enterprise
|
||||
- `oauth2_provider`: Provedor OAuth2 usado para autenticação (ex: workos, okta, auth0)
|
||||
- `oauth2_audience`: Valor de audiência OAuth2, tipicamente usado para identificar a API ou recurso de destino
|
||||
- `oauth2_client_id`: ID do cliente OAuth2 emitido pelo provedor, usado durante solicitações de autenticação
|
||||
- `oauth2_domain`: Domínio do provedor OAuth2 (ex: sua-org.auth0.com) usado para emissão de tokens
|
||||
|
||||
#### Exemplos
|
||||
|
||||
Exibir configuração atual:
|
||||
```shell Terminal
|
||||
crewai config list
|
||||
```
|
||||
|
||||
Exemplo de saída:
|
||||
```
|
||||
CrewAI CLI Configuration
|
||||
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Setting ┃ Value ┃ Description ┃
|
||||
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ enterprise_base_url│ https://app.crewai.com │ Base URL of the CrewAI Enterprise instance │
|
||||
│ org_name │ Not set │ Name of the currently active organization │
|
||||
│ org_uuid │ Not set │ UUID of the currently active organization │
|
||||
│ oauth2_provider │ workos │ OAuth2 provider used for authentication (e.g., workos, okta, auth0). │
|
||||
│ oauth2_audience │ client_01YYY │ OAuth2 audience value, typically used to identify the target API or resource. │
|
||||
│ oauth2_client_id │ client_01XXX │ OAuth2 client ID issued by the provider, used during authentication requests. │
|
||||
│ oauth2_domain │ login.crewai.com │ OAuth2 provider's domain (e.g., your-org.auth0.com) used for issuing tokens. │
|
||||
```
|
||||
|
||||
Definir a URL base do enterprise:
|
||||
```shell Terminal
|
||||
crewai config set enterprise_base_url https://minha-empresa.crewai.com
|
||||
```
|
||||
|
||||
Definir provedor OAuth2:
|
||||
```shell Terminal
|
||||
crewai config set oauth2_provider auth0
|
||||
```
|
||||
|
||||
Definir domínio OAuth2:
|
||||
```shell Terminal
|
||||
crewai config set oauth2_domain minha-empresa.auth0.com
|
||||
```
|
||||
|
||||
Redefinir todas as configurações para padrões:
|
||||
```shell Terminal
|
||||
crewai config reset
|
||||
```
|
||||
|
||||
<Note>
|
||||
As configurações são armazenadas em `~/.config/crewai/settings.json`. Algumas configurações como nome da organização e UUID são somente leitura e gerenciadas através de comandos de autenticação e organização. Configurações relacionadas ao repositório de ferramentas são ocultas e não podem ser definidas diretamente pelo usuário.
|
||||
</Note>
|
||||
|
||||
@@ -20,8 +20,7 @@ Uma crew no crewAI representa um grupo colaborativo de agentes trabalhando em co
|
||||
| **Function Calling LLM** _(opcional)_ | `function_calling_llm` | Se definido, a crew utilizará este LLM para invocar funções das ferramentas para todos os agentes da crew. Cada agente pode ter seu próprio LLM, que substitui o LLM da crew para chamadas de função. |
|
||||
| **Config** _(opcional)_ | `config` | Configurações opcionais para a crew, no formato `Json` ou `Dict[str, Any]`. |
|
||||
| **Max RPM** _(opcional)_ | `max_rpm` | Número máximo de requisições por minuto que a crew respeita durante a execução. O padrão é `None`. |
|
||||
| **Memory** _(opcional)_ | `memory` | Utilizada para armazenar memórias de execução (curto prazo, longo prazo, memória de entidade). |
|
||||
| **Memory Config** _(opcional)_ | `memory_config` | Configuração para o provedor de memória a ser utilizada pela crew. |
|
||||
| **Memory** _(opcional)_ | `memory` | Utilizada para armazenar memórias de execução (curto prazo, longo prazo, memória de entidade). | |
|
||||
| **Cache** _(opcional)_ | `cache` | Especifica se deve usar cache para armazenar os resultados da execução de ferramentas. O padrão é `True`. |
|
||||
| **Embedder** _(opcional)_ | `embedder` | Configuração do embedder a ser utilizado pela crew. Atualmente mais usado por memory. O padrão é `{"provider": "openai"}`. |
|
||||
| **Step Callback** _(opcional)_ | `step_callback` | Uma função chamada após cada etapa de cada agente. Pode ser usada para registrar as ações do agente ou executar outras operações; não sobrescreve o `step_callback` específico do agente. |
|
||||
|
||||
@@ -268,7 +268,7 @@ Nesta seção, você encontrará exemplos detalhados que ajudam a selecionar, co
|
||||
from crewai import LLM
|
||||
|
||||
llm = LLM(
|
||||
model="gemini/gemini-1.5-pro-latest",
|
||||
model="gemini-1.5-pro-latest", # or vertex_ai/gemini-1.5-pro-latest
|
||||
temperature=0.7,
|
||||
vertex_credentials=vertex_credentials_json
|
||||
)
|
||||
|
||||
@@ -9,8 +9,7 @@ icon: database
|
||||
O framework CrewAI oferece um sistema de memória sofisticado projetado para aprimorar significativamente as capacidades dos agentes de IA. O CrewAI disponibiliza **três abordagens distintas de memória** que atendem a diferentes casos de uso:
|
||||
|
||||
1. **Sistema Básico de Memória** - Memória de curto prazo, longo prazo e de entidades integradas
|
||||
2. **Memória de Usuário** - Memória específica do usuário com integração ao Mem0 (abordagem legada)
|
||||
3. **Memória Externa** - Provedores de memória externos autônomos (nova abordagem)
|
||||
2. **Memória Externa** - Provedores de memória externos autônomos
|
||||
|
||||
## Componentes do Sistema de Memória
|
||||
|
||||
@@ -19,7 +18,7 @@ O framework CrewAI oferece um sistema de memória sofisticado projetado para apr
|
||||
| **Memória de Curto Prazo** | Armazena temporariamente interações e resultados recentes usando `RAG`, permitindo que os agentes recordem e utilizem informações relevantes ao contexto atual durante as execuções. |
|
||||
| **Memória de Longo Prazo** | Preserva informações valiosas e aprendizados de execuções passadas, permitindo que os agentes construam e refinem seu conhecimento ao longo do tempo. |
|
||||
| **Memória de Entidades** | Captura e organiza informações sobre entidades (pessoas, lugares, conceitos) encontradas durante tarefas, facilitando um entendimento mais profundo e o mapeamento de relacionamentos. Utiliza `RAG` para armazenar informações de entidades. |
|
||||
| **Memória Contextual** | Mantém o contexto das interações combinando `ShortTermMemory`, `LongTermMemory` e `EntityMemory`, auxiliando na coerência e relevância das respostas dos agentes ao longo de uma sequência de tarefas ou conversas. |
|
||||
| **Memória Contextual** | Mantém o contexto das interações combinando `ShortTermMemory`, `LongTermMemory` , `ExternalMemory` e `EntityMemory`, auxiliando na coerência e relevância das respostas dos agentes ao longo de uma sequência de tarefas ou conversas. |
|
||||
|
||||
## 1. Sistema Básico de Memória (Recomendado)
|
||||
|
||||
@@ -623,7 +622,7 @@ for provider in providers_to_test:
|
||||
**Erros de modelo não encontrado:**
|
||||
```python
|
||||
# Verifique disponibilidade do modelo
|
||||
from crewai.utilities.embedding_configurator import EmbeddingConfigurator
|
||||
from crewai.rag.embeddings.configurator import EmbeddingConfigurator
|
||||
|
||||
configurator = EmbeddingConfigurator()
|
||||
try:
|
||||
@@ -684,67 +683,19 @@ print(f"OpenAI: {openai_time:.2f}s")
|
||||
print(f"Ollama: {ollama_time:.2f}s")
|
||||
```
|
||||
|
||||
## 2. Memória de Usuário com Mem0 (Legado)
|
||||
## 2. Memória Externa
|
||||
|
||||
<Warning>
|
||||
**Abordagem Legada**: Embora totalmente funcional, esta abordagem é considerada legada. Para novos projetos que exijam memória específica do usuário, considere usar Memória Externa.
|
||||
</Warning>
|
||||
A Memória Externa fornece um sistema de memória autônomo que opera independentemente da memória interna da crew. Isso é ideal para provedores de memória especializados ou compartilhamento de memória entre aplicações.
|
||||
|
||||
A Memória de Usuário se integra com o [Mem0](https://mem0.ai/) para fornecer memória específica do usuário que persiste entre sessões e se integra ao sistema de memória contextual da crew.
|
||||
|
||||
### Pré-requisitos
|
||||
```bash
|
||||
pip install mem0ai
|
||||
```
|
||||
|
||||
### Configuração Mem0 na Nuvem
|
||||
### Memória Externa Básica com Mem0
|
||||
```python
|
||||
import os
|
||||
from crewai import Crew, Process
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.memory.external.external_memory import ExternalMemory
|
||||
|
||||
# Defina sua chave de API do Mem0
|
||||
os.environ["MEM0_API_KEY"] = "m0-your-api-key"
|
||||
|
||||
crew = Crew(
|
||||
agents=[...],
|
||||
tasks=[...],
|
||||
memory=True, # Necessário para integração com a memória contextual
|
||||
memory_config={
|
||||
"provider": "mem0",
|
||||
"config": {"user_id": "john"},
|
||||
"user_memory": {} # Obrigatório - inicializa a memória de usuário
|
||||
},
|
||||
process=Process.sequential,
|
||||
verbose=True
|
||||
)
|
||||
```
|
||||
|
||||
### Configuração Avançada Mem0
|
||||
```python
|
||||
crew = Crew(
|
||||
agents=[...],
|
||||
tasks=[...],
|
||||
memory=True,
|
||||
memory_config={
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"user_id": "john",
|
||||
"org_id": "my_org_id", # Opcional
|
||||
"project_id": "my_project_id", # Opcional
|
||||
"api_key": "custom-api-key" # Opcional - sobrescreve variável de ambiente
|
||||
},
|
||||
"user_memory": {}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Configuração Mem0 Local
|
||||
```python
|
||||
crew = Crew(
|
||||
agents=[...],
|
||||
tasks=[...],
|
||||
memory=True,
|
||||
memory_config={
|
||||
# Create external memory instance with local Mem0 Configuration
|
||||
external_memory = ExternalMemory(
|
||||
embedder_config={
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"user_id": "john",
|
||||
@@ -761,37 +712,60 @@ crew = Crew(
|
||||
"provider": "openai",
|
||||
"config": {"api_key": "your-api-key", "model": "text-embedding-3-small"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"infer": True # Optional defaults to True
|
||||
},
|
||||
"user_memory": {}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## 3. Memória Externa (Nova Abordagem)
|
||||
|
||||
A Memória Externa fornece um sistema de memória autônomo que opera independentemente da memória interna da crew. Isso é ideal para provedores de memória especializados ou compartilhamento de memória entre aplicações.
|
||||
|
||||
### Memória Externa Básica com Mem0
|
||||
```python
|
||||
import os
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.memory.external.external_memory import ExternalMemory
|
||||
|
||||
os.environ["MEM0_API_KEY"] = "your-api-key"
|
||||
|
||||
# Criar instância de memória externa
|
||||
external_memory = ExternalMemory(
|
||||
embedder_config={
|
||||
"provider": "mem0",
|
||||
"config": {"user_id": "U-123"}
|
||||
}
|
||||
)
|
||||
|
||||
crew = Crew(
|
||||
agents=[...],
|
||||
tasks=[...],
|
||||
external_memory=external_memory, # Independente da memória básica
|
||||
external_memory=external_memory, # Separate from basic memory
|
||||
process=Process.sequential,
|
||||
verbose=True
|
||||
)
|
||||
```
|
||||
|
||||
### Memória Externa Avançada com o Cliente Mem0
|
||||
Ao usar o Cliente Mem0, você pode personalizar ainda mais a configuração de memória usando parâmetros como "includes", "excludes", "custom_categories", "infer" e "run_id" (apenas para memória de curto prazo).
|
||||
Você pode encontrar mais detalhes na [documentação do Mem0](https://docs.mem0.ai/).
|
||||
|
||||
```python
|
||||
import os
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.memory.external.external_memory import ExternalMemory
|
||||
|
||||
new_categories = [
|
||||
{"lifestyle_management_concerns": "Tracks daily routines, habits, hobbies and interests including cooking, time management and work-life balance"},
|
||||
{"seeking_structure": "Documents goals around creating routines, schedules, and organized systems in various life areas"},
|
||||
{"personal_information": "Basic information about the user including name, preferences, and personality traits"}
|
||||
]
|
||||
|
||||
os.environ["MEM0_API_KEY"] = "your-api-key"
|
||||
|
||||
# Create external memory instance with Mem0 Client
|
||||
external_memory = ExternalMemory(
|
||||
embedder_config={
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"user_id": "john",
|
||||
"org_id": "my_org_id", # Optional
|
||||
"project_id": "my_project_id", # Optional
|
||||
"api_key": "custom-api-key" # Optional - overrides env var
|
||||
"run_id": "my_run_id", # Optional - for short-term memory
|
||||
"includes": "include1", # Optional
|
||||
"excludes": "exclude1", # Optional
|
||||
"infer": True # Optional defaults to True
|
||||
"custom_categories": new_categories # Optional - custom categories for user memory
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
crew = Crew(
|
||||
agents=[...],
|
||||
tasks=[...],
|
||||
external_memory=external_memory, # Separate from basic memory
|
||||
process=Process.sequential,
|
||||
verbose=True
|
||||
)
|
||||
@@ -830,17 +804,18 @@ crew = Crew(
|
||||
)
|
||||
```
|
||||
|
||||
## Comparação dos Sistemas de Memória
|
||||
## 🧠 Comparação dos Sistemas de Memória
|
||||
|
||||
| **Categoria** | **Recurso** | **Memória Básica** | **Memória Externa** |
|
||||
|------------------------|-------------------------------|-------------------------------|----------------------------------|
|
||||
| **Facilidade de Uso** | Complexidade de Setup | Simples | Média |
|
||||
| | Integração | Contextual integrada | Autônoma |
|
||||
| **Persistência** | Armazenamento | Arquivos locais | Customizada / Mem0 |
|
||||
| | Multi-sessão | ✅ | ✅ |
|
||||
| **Personalização** | Especificidade do Usuário | ❌ | ✅ |
|
||||
| | Provedores Customizados | Limitado | Qualquer provedor |
|
||||
| **Aplicação Recomendada** | Recomendado para | Maioria dos casos | Necessidades especializadas |
|
||||
|
||||
| Recurso | Memória Básica | Memória de Usuário (Legado) | Memória Externa |
|
||||
|---------|---------------|-----------------------------|----------------|
|
||||
| **Complexidade de Setup** | Simples | Média | Média |
|
||||
| **Integração** | Contextual integrada | Contextual + específica do usuário | Autônoma |
|
||||
| **Armazenamento** | Arquivos locais | Mem0 Cloud/Local | Customizada/Mem0 |
|
||||
| **Multi-sessão** | ✅ | ✅ | ✅ |
|
||||
| **Especificidade do Usuário** | ❌ | ✅ | ✅ |
|
||||
| **Provedores Customizados** | Limitado | Apenas Mem0 | Qualquer provedor |
|
||||
| **Recomendado para** | Maioria dos casos | Projetos legados | Necessidades especializadas |
|
||||
|
||||
## Provedores de Embedding Suportados
|
||||
|
||||
@@ -989,4 +964,4 @@ crew = Crew(
|
||||
## Conclusão
|
||||
|
||||
Integrar o sistema de memória do CrewAI em seus projetos é simples. Ao aproveitar os componentes e configurações oferecidos,
|
||||
você rapidamente capacita seus agentes a lembrar, raciocinar e aprender com suas interações, desbloqueando novos níveis de inteligência e capacidade.
|
||||
você rapidamente capacita seus agentes a lembrar, raciocinar e aprender com suas interações, desbloqueando novos níveis de inteligência e capacidade.
|
||||
|
||||
@@ -44,6 +44,19 @@ A classe `MCPServerAdapter` da `crewai-tools` é a principal forma de conectar-s
|
||||
|
||||
O uso de um gerenciador de contexto Python (`with`) é a **abordagem recomendada** para o `MCPServerAdapter`. Ele lida automaticamente com a abertura e o fechamento da conexão com o servidor MCP.
|
||||
|
||||
## Configuração de Conexão
|
||||
|
||||
O `MCPServerAdapter` suporta várias opções de configuração para personalizar o comportamento da conexão:
|
||||
|
||||
- **`connect_timeout`** (opcional): Tempo máximo em segundos para aguardar o estabelecimento de uma conexão com o servidor MCP. O padrão é 30 segundos se não especificado. Isso é particularmente útil para servidores remotos que podem ter tempos de resposta variáveis.
|
||||
|
||||
```python
|
||||
# Exemplo com timeout personalizado para conexão
|
||||
with MCPServerAdapter(server_params, connect_timeout=60) as tools:
|
||||
# A conexão terá timeout após 60 segundos se não estabelecida
|
||||
pass
|
||||
```
|
||||
|
||||
```python
|
||||
from crewai import Agent
|
||||
from crewai_tools import MCPServerAdapter
|
||||
@@ -70,7 +83,7 @@ server_params = {
|
||||
}
|
||||
|
||||
# Exemplo de uso (descomente e adapte após definir server_params):
|
||||
with MCPServerAdapter(server_params) as mcp_tools:
|
||||
with MCPServerAdapter(server_params, connect_timeout=60) as mcp_tools:
|
||||
print(f"Available tools: {[tool.name for tool in mcp_tools]}")
|
||||
|
||||
meu_agente = Agent(
|
||||
@@ -88,7 +101,7 @@ Este padrão geral mostra como integrar ferramentas. Para exemplos específicos
|
||||
## Filtrando Ferramentas
|
||||
|
||||
```python
|
||||
with MCPServerAdapter(server_params) as mcp_tools:
|
||||
with MCPServerAdapter(server_params, connect_timeout=60) as mcp_tools:
|
||||
print(f"Available tools: {[tool.name for tool in mcp_tools]}")
|
||||
|
||||
meu_agente = Agent(
|
||||
|
||||
286
docs/pt-BR/observability/langdb.mdx
Normal file
286
docs/pt-BR/observability/langdb.mdx
Normal file
@@ -0,0 +1,286 @@
|
||||
---
|
||||
title: Integração LangDB
|
||||
description: Governe, proteja e otimize seus fluxos de trabalho CrewAI com LangDB AI Gateway—acesse mais de 350 modelos, roteamento automático, otimização de custos e observabilidade completa.
|
||||
icon: database
|
||||
---
|
||||
|
||||
# Introdução
|
||||
|
||||
[LangDB AI Gateway](https://langdb.ai) fornece APIs compatíveis com OpenAI para conectar com múltiplos Modelos de Linguagem Grandes e serve como uma plataforma de observabilidade que torna effortless rastrear fluxos de trabalho CrewAI de ponta a ponta, proporcionando acesso a mais de 350 modelos de linguagem. Com uma única chamada `init()`, todas as interações de agentes, execuções de tarefas e chamadas LLM são capturadas, fornecendo observabilidade abrangente e infraestrutura de IA pronta para produção para suas aplicações.
|
||||
|
||||
<Frame caption="Exemplo de Rastreamento CrewAI LangDB">
|
||||
<img src="/images/langdb-1.png" alt="Exemplo de rastreamento CrewAI LangDB" />
|
||||
</Frame>
|
||||
|
||||
**Confira:** [Ver o exemplo de trace ao vivo](https://app.langdb.ai/sharing/threads/3becbfed-a1be-ae84-ea3c-4942867a3e22)
|
||||
|
||||
## Recursos
|
||||
|
||||
### Capacidades do AI Gateway
|
||||
- **Acesso a mais de 350 LLMs**: Conecte-se a todos os principais modelos de linguagem através de uma única integração
|
||||
- **Modelos Virtuais**: Crie configurações de modelo personalizadas com parâmetros específicos e regras de roteamento
|
||||
- **MCP Virtual**: Habilite compatibilidade e integração com sistemas MCP (Model Context Protocol) para comunicação aprimorada de agentes
|
||||
- **Guardrails**: Implemente medidas de segurança e controles de conformidade para comportamento de agentes
|
||||
|
||||
### Observabilidade e Rastreamento
|
||||
- **Rastreamento Automático**: Uma única chamada `init()` captura todas as interações CrewAI
|
||||
- **Visibilidade Ponta a Ponta**: Monitore fluxos de trabalho de agentes do início ao fim
|
||||
- **Rastreamento de Uso de Ferramentas**: Rastreie quais ferramentas os agentes usam e seus resultados
|
||||
- **Monitoramento de Chamadas de Modelo**: Insights detalhados sobre interações LLM
|
||||
- **Análise de Performance**: Monitore latência, uso de tokens e custos
|
||||
- **Suporte a Depuração**: Execução passo a passo para solução de problemas
|
||||
- **Monitoramento em Tempo Real**: Dashboard de traces e métricas ao vivo
|
||||
|
||||
## Instruções de Configuração
|
||||
|
||||
<Steps>
|
||||
<Step title="Instalar LangDB">
|
||||
Instale o cliente LangDB com flag de recurso CrewAI:
|
||||
```bash
|
||||
pip install 'pylangdb[crewai]'
|
||||
```
|
||||
</Step>
|
||||
<Step title="Definir Variáveis de Ambiente">
|
||||
Configure suas credenciais LangDB:
|
||||
```bash
|
||||
export LANGDB_API_KEY="<sua_chave_api_langdb>"
|
||||
export LANGDB_PROJECT_ID="<seu_id_projeto_langdb>"
|
||||
export LANGDB_API_BASE_URL='https://api.us-east-1.langdb.ai'
|
||||
```
|
||||
</Step>
|
||||
<Step title="Inicializar Rastreamento">
|
||||
Importe e inicialize LangDB antes de configurar seu código CrewAI:
|
||||
```python
|
||||
from pylangdb.crewai import init
|
||||
# Inicializar LangDB
|
||||
init()
|
||||
```
|
||||
</Step>
|
||||
<Step title="Configurar CrewAI com LangDB">
|
||||
Configure seu LLM com cabeçalhos LangDB:
|
||||
```python
|
||||
from crewai import Agent, Task, Crew, LLM
|
||||
import os
|
||||
|
||||
# Configurar LLM com cabeçalhos LangDB
|
||||
llm = LLM(
|
||||
model="openai/gpt-4o", # Substitua pelo modelo que você quer usar
|
||||
api_key=os.getenv("LANGDB_API_KEY"),
|
||||
base_url=os.getenv("LANGDB_API_BASE_URL"),
|
||||
extra_headers={"x-project-id": os.getenv("LANGDB_PROJECT_ID")}
|
||||
)
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Exemplo de Início Rápido
|
||||
|
||||
Aqui está um exemplo simples para começar com LangDB e CrewAI:
|
||||
|
||||
```python
|
||||
import os
|
||||
from pylangdb.crewai import init
|
||||
from crewai import Agent, Task, Crew, LLM
|
||||
|
||||
# Inicializar LangDB antes de qualquer importação CrewAI
|
||||
init()
|
||||
|
||||
def create_llm(model):
|
||||
return LLM(
|
||||
model=model,
|
||||
api_key=os.environ.get("LANGDB_API_KEY"),
|
||||
base_url=os.environ.get("LANGDB_API_BASE_URL"),
|
||||
extra_headers={"x-project-id": os.environ.get("LANGDB_PROJECT_ID")}
|
||||
)
|
||||
|
||||
# Defina seu agente
|
||||
researcher = Agent(
|
||||
role="Especialista em Pesquisa",
|
||||
goal="Pesquisar tópicos minuciosamente",
|
||||
backstory="Pesquisador especialista com habilidades em encontrar informações",
|
||||
llm=create_llm("openai/gpt-4o"), # Substitua pelo modelo que você quer usar
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# Criar uma tarefa
|
||||
task = Task(
|
||||
description="Pesquise o tópico dado e forneça um resumo abrangente",
|
||||
agent=researcher,
|
||||
expected_output="Resumo de pesquisa detalhado com principais descobertas"
|
||||
)
|
||||
|
||||
# Criar e executar a equipe
|
||||
crew = Crew(agents=[researcher], tasks=[task])
|
||||
result = crew.kickoff()
|
||||
print(result)
|
||||
```
|
||||
|
||||
## Exemplo Completo: Agente de Pesquisa e Planejamento
|
||||
|
||||
Este exemplo abrangente demonstra um fluxo de trabalho multi-agente com capacidades de pesquisa e planejamento.
|
||||
|
||||
### Pré-requisitos
|
||||
|
||||
```bash
|
||||
pip install crewai 'pylangdb[crewai]' crewai_tools setuptools python-dotenv
|
||||
```
|
||||
|
||||
### Configuração do Ambiente
|
||||
|
||||
```bash
|
||||
# Credenciais LangDB
|
||||
export LANGDB_API_KEY="<sua_chave_api_langdb>"
|
||||
export LANGDB_PROJECT_ID="<seu_id_projeto_langdb>"
|
||||
export LANGDB_API_BASE_URL='https://api.us-east-1.langdb.ai'
|
||||
|
||||
# Chaves API adicionais (opcional)
|
||||
export SERPER_API_KEY="<sua_chave_api_serper>" # Para capacidades de busca na web
|
||||
```
|
||||
|
||||
### Implementação Completa
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pylangdb.crewai import init
|
||||
init() # Inicializar LangDB antes de qualquer importação CrewAI
|
||||
from dotenv import load_dotenv
|
||||
from crewai import Agent, Task, Crew, Process, LLM
|
||||
from crewai_tools import SerperDevTool
|
||||
|
||||
load_dotenv()
|
||||
|
||||
def create_llm(model):
|
||||
return LLM(
|
||||
model=model,
|
||||
api_key=os.environ.get("LANGDB_API_KEY"),
|
||||
base_url=os.environ.get("LANGDB_API_BASE_URL"),
|
||||
extra_headers={"x-project-id": os.environ.get("LANGDB_PROJECT_ID")}
|
||||
)
|
||||
|
||||
class ResearchPlanningCrew:
|
||||
def researcher(self) -> Agent:
|
||||
return Agent(
|
||||
role="Especialista em Pesquisa",
|
||||
goal="Pesquisar tópicos minuciosamente e compilar informações abrangentes",
|
||||
backstory="Pesquisador especialista com habilidades em encontrar e analisar informações de várias fontes",
|
||||
tools=[SerperDevTool()],
|
||||
llm=create_llm("openai/gpt-4o"),
|
||||
verbose=True
|
||||
)
|
||||
|
||||
def planner(self) -> Agent:
|
||||
return Agent(
|
||||
role="Planejador Estratégico",
|
||||
goal="Criar planos acionáveis baseados em descobertas de pesquisa",
|
||||
backstory="Planejador estratégico que divide desafios complexos em planos executáveis",
|
||||
reasoning=True,
|
||||
max_reasoning_attempts=3,
|
||||
llm=create_llm("openai/anthropic/claude-3.7-sonnet"),
|
||||
verbose=True
|
||||
)
|
||||
|
||||
def research_task(self) -> Task:
|
||||
return Task(
|
||||
description="Pesquise o tópico minuciosamente e compile informações abrangentes",
|
||||
agent=self.researcher(),
|
||||
expected_output="Relatório de pesquisa abrangente com principais descobertas e insights"
|
||||
)
|
||||
|
||||
def planning_task(self) -> Task:
|
||||
return Task(
|
||||
description="Crie um plano estratégico baseado nas descobertas da pesquisa",
|
||||
agent=self.planner(),
|
||||
expected_output="Plano de execução estratégica com fases, objetivos e etapas acionáveis",
|
||||
context=[self.research_task()]
|
||||
)
|
||||
|
||||
def crew(self) -> Crew:
|
||||
return Crew(
|
||||
agents=[self.researcher(), self.planner()],
|
||||
tasks=[self.research_task(), self.planning_task()],
|
||||
verbose=True,
|
||||
process=Process.sequential
|
||||
)
|
||||
|
||||
def main():
|
||||
topic = sys.argv[1] if len(sys.argv) > 1 else "Inteligência Artificial na Saúde"
|
||||
|
||||
crew_instance = ResearchPlanningCrew()
|
||||
|
||||
# Atualizar descrições de tarefas com o tópico específico
|
||||
crew_instance.research_task().description = f"Pesquise {topic} minuciosamente e compile informações abrangentes"
|
||||
crew_instance.planning_task().description = f"Crie um plano estratégico para {topic} baseado nas descobertas da pesquisa"
|
||||
|
||||
result = crew_instance.crew().kickoff()
|
||||
print(result)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
### Executando o Exemplo
|
||||
|
||||
```bash
|
||||
python main.py "Soluções de Energia Sustentável"
|
||||
```
|
||||
|
||||
## Visualizando Traces no LangDB
|
||||
|
||||
Após executar sua aplicação CrewAI, você pode visualizar traces detalhados no dashboard LangDB:
|
||||
|
||||
<Frame caption="Dashboard de Trace LangDB">
|
||||
<img src="/images/langdb-2.png" alt="Dashboard de trace LangDB mostrando fluxo de trabalho CrewAI" />
|
||||
</Frame>
|
||||
|
||||
### O Que Você Verá
|
||||
|
||||
- **Interações de Agentes**: Fluxo completo de conversas de agentes e transferências de tarefas
|
||||
- **Uso de Ferramentas**: Quais ferramentas foram chamadas, suas entradas e saídas
|
||||
- **Chamadas de Modelo**: Interações LLM detalhadas com prompts e respostas
|
||||
- **Métricas de Performance**: Rastreamento de latência, uso de tokens e custos
|
||||
- **Linha do Tempo de Execução**: Visualização passo a passo de todo o fluxo de trabalho
|
||||
|
||||
|
||||
## Solução de Problemas
|
||||
|
||||
### Problemas Comuns
|
||||
|
||||
- **Nenhum trace aparecendo**: Certifique-se de que `init()` seja chamado antes de qualquer importação CrewAI
|
||||
- **Erros de autenticação**: Verifique sua chave API LangDB e ID do projeto
|
||||
|
||||
|
||||
## Recursos
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Documentação LangDB" icon="book" href="https://docs.langdb.ai">
|
||||
Documentação oficial e guias LangDB
|
||||
</Card>
|
||||
<Card title="Guias LangDB" icon="graduation-cap" href="https://docs.langdb.ai/guides">
|
||||
Tutoriais passo a passo para construir agentes de IA
|
||||
</Card>
|
||||
<Card title="Exemplos GitHub" icon="github" href="https://github.com/langdb/langdb-samples/tree/main/examples/crewai" >
|
||||
Exemplos completos de integração CrewAI
|
||||
</Card>
|
||||
<Card title="Dashboard LangDB" icon="chart-line" href="https://app.langdb.ai">
|
||||
Acesse seus traces e análises
|
||||
</Card>
|
||||
<Card title="Catálogo de Modelos" icon="list" href="https://app.langdb.ai/models">
|
||||
Navegue por mais de 350 modelos de linguagem disponíveis
|
||||
</Card>
|
||||
<Card title="Recursos Enterprise" icon="building" href="https://docs.langdb.ai/enterprise">
|
||||
Opções auto-hospedadas e capacidades empresariais
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Próximos Passos
|
||||
|
||||
Este guia cobriu o básico da integração do LangDB AI Gateway com CrewAI. Para aprimorar ainda mais seus fluxos de trabalho de IA, explore:
|
||||
|
||||
- **Modelos Virtuais**: Crie configurações de modelo personalizadas com estratégias de roteamento
|
||||
- **Guardrails e Segurança**: Implemente filtragem de conteúdo e controles de conformidade
|
||||
- **Implantação em Produção**: Configure fallbacks, tentativas e balanceamento de carga
|
||||
|
||||
Para recursos mais avançados e casos de uso, visite a [Documentação LangDB](https://docs.langdb.ai) ou explore o [Catálogo de Modelos](https://app.langdb.ai/models) para descobrir todos os modelos disponíveis.
|
||||
@@ -25,6 +25,10 @@ A observabilidade é fundamental para entender como seus agentes CrewAI estão d
|
||||
Replays de sessões, métricas e monitoramento para desenvolvimento e produção de agentes.
|
||||
</Card>
|
||||
|
||||
<Card title="LangDB" icon="database" href="/pt-BR/observability/langdb">
|
||||
Rastreamento ponta a ponta para fluxos de trabalho CrewAI com captura automática de interações de agentes.
|
||||
</Card>
|
||||
|
||||
<Card title="OpenLIT" icon="magnifying-glass-chart" href="/pt-BR/observability/openlit">
|
||||
Monitoramento nativo OpenTelemetry com rastreamento de custos e análises de desempenho.
|
||||
</Card>
|
||||
|
||||
@@ -11,7 +11,7 @@ dependencies = [
|
||||
# Core Dependencies
|
||||
"pydantic>=2.4.2",
|
||||
"openai>=1.13.3",
|
||||
"litellm==1.74.3",
|
||||
"litellm==1.74.9",
|
||||
"instructor>=1.3.3",
|
||||
# Text Processing
|
||||
"pdfplumber>=0.11.4",
|
||||
@@ -48,7 +48,7 @@ Documentation = "https://docs.crewai.com"
|
||||
Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = ["crewai-tools~=0.58.0"]
|
||||
tools = ["crewai-tools~=0.60.0"]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
]
|
||||
|
||||
@@ -54,7 +54,7 @@ def _track_install_async():
|
||||
|
||||
_track_install_async()
|
||||
|
||||
__version__ = "0.150.0"
|
||||
__version__ = "0.157.0"
|
||||
__all__ = [
|
||||
"Agent",
|
||||
"Crew",
|
||||
|
||||
@@ -222,11 +222,9 @@ class Agent(BaseAgent):
|
||||
|
||||
memory_attributes = [
|
||||
"memory",
|
||||
"memory_config",
|
||||
"_short_term_memory",
|
||||
"_long_term_memory",
|
||||
"_entity_memory",
|
||||
"_user_memory",
|
||||
"_external_memory",
|
||||
]
|
||||
|
||||
@@ -316,11 +314,9 @@ class Agent(BaseAgent):
|
||||
|
||||
start_time = time.time()
|
||||
contextual_memory = ContextualMemory(
|
||||
self.crew.memory_config,
|
||||
self.crew._short_term_memory,
|
||||
self.crew._long_term_memory,
|
||||
self.crew._entity_memory,
|
||||
self.crew._user_memory,
|
||||
self.crew._external_memory,
|
||||
)
|
||||
memory = contextual_memory.build_context_for_task(task, context)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
ALGORITHMS = ["RS256"]
|
||||
|
||||
#TODO: The AUTH0 constants should be removed after WorkOS migration is completed
|
||||
AUTH0_DOMAIN = "crewai.us.auth0.com"
|
||||
AUTH0_CLIENT_ID = "DEVC5Fw6NlRoSzmDCcOhVq85EfLBjKa8"
|
||||
AUTH0_AUDIENCE = "https://crewai.us.auth0.com/api/v2/"
|
||||
|
||||
WORKOS_DOMAIN = "login.crewai.com"
|
||||
WORKOS_CLI_CONNECT_APP_ID = "client_01JYT06R59SP0NXYGD994NFXXX"
|
||||
WORKOS_ENVIRONMENT_ID = "client_01JNJQWBJ4SPFN3SWJM5T7BDG8"
|
||||
|
||||
@@ -1,76 +1,92 @@
|
||||
import time
|
||||
import webbrowser
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import requests
|
||||
from rich.console import Console
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .constants import (
|
||||
AUTH0_AUDIENCE,
|
||||
AUTH0_CLIENT_ID,
|
||||
AUTH0_DOMAIN,
|
||||
WORKOS_DOMAIN,
|
||||
WORKOS_CLI_CONNECT_APP_ID,
|
||||
WORKOS_ENVIRONMENT_ID,
|
||||
)
|
||||
|
||||
from .utils import TokenManager, validate_jwt_token
|
||||
from urllib.parse import quote
|
||||
from crewai.cli.plus_api import PlusAPI
|
||||
from crewai.cli.config import Settings
|
||||
from crewai.cli.authentication.constants import (
|
||||
AUTH0_AUDIENCE,
|
||||
AUTH0_CLIENT_ID,
|
||||
AUTH0_DOMAIN,
|
||||
)
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
class Oauth2Settings(BaseModel):
|
||||
provider: str = Field(description="OAuth2 provider used for authentication (e.g., workos, okta, auth0).")
|
||||
client_id: str = Field(description="OAuth2 client ID issued by the provider, used during authentication requests.")
|
||||
domain: str = Field(description="OAuth2 provider's domain (e.g., your-org.auth0.com) used for issuing tokens.")
|
||||
audience: Optional[str] = Field(description="OAuth2 audience value, typically used to identify the target API or resource.", default=None)
|
||||
|
||||
@classmethod
|
||||
def from_settings(cls):
|
||||
settings = Settings()
|
||||
|
||||
return cls(
|
||||
provider=settings.oauth2_provider,
|
||||
domain=settings.oauth2_domain,
|
||||
client_id=settings.oauth2_client_id,
|
||||
audience=settings.oauth2_audience,
|
||||
)
|
||||
|
||||
|
||||
class ProviderFactory:
|
||||
@classmethod
|
||||
def from_settings(cls, settings: Optional[Oauth2Settings] = None):
|
||||
settings = settings or Oauth2Settings.from_settings()
|
||||
|
||||
import importlib
|
||||
module = importlib.import_module(f"crewai.cli.authentication.providers.{settings.provider.lower()}")
|
||||
provider = getattr(module, f"{settings.provider.capitalize()}Provider")
|
||||
|
||||
return provider(settings)
|
||||
|
||||
class AuthenticationCommand:
|
||||
AUTH0_DEVICE_CODE_URL = f"https://{AUTH0_DOMAIN}/oauth/device/code"
|
||||
AUTH0_TOKEN_URL = f"https://{AUTH0_DOMAIN}/oauth/token"
|
||||
|
||||
WORKOS_DEVICE_CODE_URL = f"https://{WORKOS_DOMAIN}/oauth2/device_authorization"
|
||||
WORKOS_TOKEN_URL = f"https://{WORKOS_DOMAIN}/oauth2/token"
|
||||
|
||||
def __init__(self):
|
||||
self.token_manager = TokenManager()
|
||||
# TODO: WORKOS - This variable is temporary until migration to WorkOS is complete.
|
||||
self.user_provider = "workos"
|
||||
self.oauth2_provider = ProviderFactory.from_settings()
|
||||
|
||||
def login(self) -> None:
|
||||
"""Sign up to CrewAI+"""
|
||||
|
||||
device_code_url = self.WORKOS_DEVICE_CODE_URL
|
||||
token_url = self.WORKOS_TOKEN_URL
|
||||
client_id = WORKOS_CLI_CONNECT_APP_ID
|
||||
audience = None
|
||||
|
||||
console.print("Signing in to CrewAI Enterprise...\n", style="bold blue")
|
||||
|
||||
# TODO: WORKOS - Next line and conditional are temporary until migration to WorkOS is complete.
|
||||
user_provider = self._determine_user_provider()
|
||||
if user_provider == "auth0":
|
||||
device_code_url = self.AUTH0_DEVICE_CODE_URL
|
||||
token_url = self.AUTH0_TOKEN_URL
|
||||
client_id = AUTH0_CLIENT_ID
|
||||
audience = AUTH0_AUDIENCE
|
||||
self.user_provider = "auth0"
|
||||
settings = Oauth2Settings(
|
||||
provider="auth0",
|
||||
client_id=AUTH0_CLIENT_ID,
|
||||
domain=AUTH0_DOMAIN,
|
||||
audience=AUTH0_AUDIENCE
|
||||
)
|
||||
self.oauth2_provider = ProviderFactory.from_settings(settings)
|
||||
# End of temporary code.
|
||||
|
||||
device_code_data = self._get_device_code(client_id, device_code_url, audience)
|
||||
device_code_data = self._get_device_code()
|
||||
self._display_auth_instructions(device_code_data)
|
||||
|
||||
return self._poll_for_token(device_code_data, client_id, token_url)
|
||||
return self._poll_for_token(device_code_data)
|
||||
|
||||
def _get_device_code(
|
||||
self, client_id: str, device_code_url: str, audience: str | None = None
|
||||
self
|
||||
) -> Dict[str, Any]:
|
||||
"""Get the device code to authenticate the user."""
|
||||
|
||||
device_code_payload = {
|
||||
"client_id": client_id,
|
||||
"client_id": self.oauth2_provider.get_client_id(),
|
||||
"scope": "openid",
|
||||
"audience": audience,
|
||||
"audience": self.oauth2_provider.get_audience(),
|
||||
}
|
||||
response = requests.post(
|
||||
url=device_code_url, data=device_code_payload, timeout=20
|
||||
url=self.oauth2_provider.get_authorize_url(), data=device_code_payload, timeout=20
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
@@ -82,21 +98,21 @@ class AuthenticationCommand:
|
||||
webbrowser.open(device_code_data["verification_uri_complete"])
|
||||
|
||||
def _poll_for_token(
|
||||
self, device_code_data: Dict[str, Any], client_id: str, token_poll_url: str
|
||||
self, device_code_data: Dict[str, Any]
|
||||
) -> None:
|
||||
"""Polls the server for the token until it is received, or max attempts are reached."""
|
||||
|
||||
token_payload = {
|
||||
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
||||
"device_code": device_code_data["device_code"],
|
||||
"client_id": client_id,
|
||||
"client_id": self.oauth2_provider.get_client_id(),
|
||||
}
|
||||
|
||||
console.print("\nWaiting for authentication... ", style="bold blue", end="")
|
||||
|
||||
attempts = 0
|
||||
while True and attempts < 10:
|
||||
response = requests.post(token_poll_url, data=token_payload, timeout=30)
|
||||
response = requests.post(self.oauth2_provider.get_token_url(), data=token_payload, timeout=30)
|
||||
token_data = response.json()
|
||||
|
||||
if response.status_code == 200:
|
||||
@@ -128,19 +144,14 @@ class AuthenticationCommand:
|
||||
"""Validates the JWT token and saves the token to the token manager."""
|
||||
|
||||
jwt_token = token_data["access_token"]
|
||||
issuer = self.oauth2_provider.get_issuer()
|
||||
jwt_token_data = {
|
||||
"jwt_token": jwt_token,
|
||||
"jwks_url": f"https://{WORKOS_DOMAIN}/oauth2/jwks",
|
||||
"issuer": f"https://{WORKOS_DOMAIN}",
|
||||
"audience": WORKOS_ENVIRONMENT_ID,
|
||||
"jwks_url": self.oauth2_provider.get_jwks_url(),
|
||||
"issuer": issuer,
|
||||
"audience": self.oauth2_provider.get_audience(),
|
||||
}
|
||||
|
||||
# TODO: WORKOS - The following conditional is temporary until migration to WorkOS is complete.
|
||||
if self.user_provider == "auth0":
|
||||
jwt_token_data["jwks_url"] = f"https://{AUTH0_DOMAIN}/.well-known/jwks.json"
|
||||
jwt_token_data["issuer"] = f"https://{AUTH0_DOMAIN}/"
|
||||
jwt_token_data["audience"] = AUTH0_AUDIENCE
|
||||
|
||||
decoded_token = validate_jwt_token(**jwt_token_data)
|
||||
|
||||
expires_at = decoded_token.get("exp", 0)
|
||||
|
||||
26
src/crewai/cli/authentication/providers/auth0.py
Normal file
26
src/crewai/cli/authentication/providers/auth0.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from crewai.cli.authentication.providers.base_provider import BaseProvider
|
||||
|
||||
class Auth0Provider(BaseProvider):
|
||||
def get_authorize_url(self) -> str:
|
||||
return f"https://{self._get_domain()}/oauth/device/code"
|
||||
|
||||
def get_token_url(self) -> str:
|
||||
return f"https://{self._get_domain()}/oauth/token"
|
||||
|
||||
def get_jwks_url(self) -> str:
|
||||
return f"https://{self._get_domain()}/.well-known/jwks.json"
|
||||
|
||||
def get_issuer(self) -> str:
|
||||
return f"https://{self._get_domain()}/"
|
||||
|
||||
def get_audience(self) -> str:
|
||||
assert self.settings.audience is not None, "Audience is required"
|
||||
return self.settings.audience
|
||||
|
||||
def get_client_id(self) -> str:
|
||||
assert self.settings.client_id is not None, "Client ID is required"
|
||||
return self.settings.client_id
|
||||
|
||||
def _get_domain(self) -> str:
|
||||
assert self.settings.domain is not None, "Domain is required"
|
||||
return self.settings.domain
|
||||
30
src/crewai/cli/authentication/providers/base_provider.py
Normal file
30
src/crewai/cli/authentication/providers/base_provider.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from crewai.cli.authentication.main import Oauth2Settings
|
||||
|
||||
class BaseProvider(ABC):
|
||||
def __init__(self, settings: Oauth2Settings):
|
||||
self.settings = settings
|
||||
|
||||
@abstractmethod
|
||||
def get_authorize_url(self) -> str:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_token_url(self) -> str:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_jwks_url(self) -> str:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_issuer(self) -> str:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_audience(self) -> str:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_client_id(self) -> str:
|
||||
...
|
||||
22
src/crewai/cli/authentication/providers/okta.py
Normal file
22
src/crewai/cli/authentication/providers/okta.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from crewai.cli.authentication.providers.base_provider import BaseProvider
|
||||
|
||||
class OktaProvider(BaseProvider):
|
||||
def get_authorize_url(self) -> str:
|
||||
return f"https://{self.settings.domain}/oauth2/default/v1/device/authorize"
|
||||
|
||||
def get_token_url(self) -> str:
|
||||
return f"https://{self.settings.domain}/oauth2/default/v1/token"
|
||||
|
||||
def get_jwks_url(self) -> str:
|
||||
return f"https://{self.settings.domain}/oauth2/default/v1/keys"
|
||||
|
||||
def get_issuer(self) -> str:
|
||||
return f"https://{self.settings.domain}/oauth2/default"
|
||||
|
||||
def get_audience(self) -> str:
|
||||
assert self.settings.audience is not None
|
||||
return self.settings.audience
|
||||
|
||||
def get_client_id(self) -> str:
|
||||
assert self.settings.client_id is not None
|
||||
return self.settings.client_id
|
||||
25
src/crewai/cli/authentication/providers/workos.py
Normal file
25
src/crewai/cli/authentication/providers/workos.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from crewai.cli.authentication.providers.base_provider import BaseProvider
|
||||
|
||||
class WorkosProvider(BaseProvider):
|
||||
def get_authorize_url(self) -> str:
|
||||
return f"https://{self._get_domain()}/oauth2/device_authorization"
|
||||
|
||||
def get_token_url(self) -> str:
|
||||
return f"https://{self._get_domain()}/oauth2/token"
|
||||
|
||||
def get_jwks_url(self) -> str:
|
||||
return f"https://{self._get_domain()}/oauth2/jwks"
|
||||
|
||||
def get_issuer(self) -> str:
|
||||
return f"https://{self._get_domain()}"
|
||||
|
||||
def get_audience(self) -> str:
|
||||
return self.settings.audience or ""
|
||||
|
||||
def get_client_id(self) -> str:
|
||||
assert self.settings.client_id is not None, "Client ID is required"
|
||||
return self.settings.client_id
|
||||
|
||||
def _get_domain(self) -> str:
|
||||
assert self.settings.domain is not None, "Domain is required"
|
||||
return self.settings.domain
|
||||
@@ -3,6 +3,7 @@ from typing import Optional
|
||||
|
||||
import click
|
||||
from crewai.cli.config import Settings
|
||||
from crewai.cli.settings.main import SettingsCommand
|
||||
from crewai.cli.add_crew_to_flow import add_crew_to_flow
|
||||
from crewai.cli.create_crew import create_crew
|
||||
from crewai.cli.create_flow import create_flow
|
||||
@@ -227,7 +228,7 @@ def update():
|
||||
@crewai.command()
|
||||
def login():
|
||||
"""Sign Up/Login to CrewAI Enterprise."""
|
||||
Settings().clear()
|
||||
Settings().clear_user_settings()
|
||||
AuthenticationCommand().login()
|
||||
|
||||
|
||||
@@ -369,8 +370,8 @@ def org():
|
||||
pass
|
||||
|
||||
|
||||
@org.command()
|
||||
def list():
|
||||
@org.command("list")
|
||||
def org_list():
|
||||
"""List available organizations."""
|
||||
org_command = OrganizationCommand()
|
||||
org_command.list()
|
||||
@@ -391,5 +392,34 @@ def current():
|
||||
org_command.current()
|
||||
|
||||
|
||||
@crewai.group()
|
||||
def config():
|
||||
"""CLI Configuration commands."""
|
||||
pass
|
||||
|
||||
|
||||
@config.command("list")
|
||||
def config_list():
|
||||
"""List all CLI configuration parameters."""
|
||||
config_command = SettingsCommand()
|
||||
config_command.list()
|
||||
|
||||
|
||||
@config.command("set")
|
||||
@click.argument("key")
|
||||
@click.argument("value")
|
||||
def config_set(key: str, value: str):
|
||||
"""Set a CLI configuration parameter."""
|
||||
config_command = SettingsCommand()
|
||||
config_command.set(key, value)
|
||||
|
||||
|
||||
@config.command("reset")
|
||||
def config_reset():
|
||||
"""Reset all CLI configuration parameters to default values."""
|
||||
config_command = SettingsCommand()
|
||||
config_command.reset_all_settings()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
crewai()
|
||||
|
||||
@@ -26,7 +26,7 @@ class PlusAPIMixin:
|
||||
"Please sign up/login to CrewAI+ before using the CLI.",
|
||||
style="bold red",
|
||||
)
|
||||
console.print("Run 'crewai signup' to sign up/login.", style="bold green")
|
||||
console.print("Run 'crewai login' to sign up/login.", style="bold green")
|
||||
raise SystemExit
|
||||
|
||||
def _validate_response(self, response: requests.Response) -> None:
|
||||
|
||||
@@ -4,10 +4,60 @@ from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from crewai.cli.constants import (
|
||||
DEFAULT_CREWAI_ENTERPRISE_URL,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_PROVIDER,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_CLIENT_ID,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN,
|
||||
)
|
||||
|
||||
DEFAULT_CONFIG_PATH = Path.home() / ".config" / "crewai" / "settings.json"
|
||||
|
||||
# Settings that are related to the user's account
|
||||
USER_SETTINGS_KEYS = [
|
||||
"tool_repository_username",
|
||||
"tool_repository_password",
|
||||
"org_name",
|
||||
"org_uuid",
|
||||
]
|
||||
|
||||
# Settings that are related to the CLI
|
||||
CLI_SETTINGS_KEYS = [
|
||||
"enterprise_base_url",
|
||||
"oauth2_provider",
|
||||
"oauth2_audience",
|
||||
"oauth2_client_id",
|
||||
"oauth2_domain",
|
||||
]
|
||||
|
||||
# Default values for CLI settings
|
||||
DEFAULT_CLI_SETTINGS = {
|
||||
"enterprise_base_url": DEFAULT_CREWAI_ENTERPRISE_URL,
|
||||
"oauth2_provider": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_PROVIDER,
|
||||
"oauth2_audience": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE,
|
||||
"oauth2_client_id": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_CLIENT_ID,
|
||||
"oauth2_domain": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN,
|
||||
}
|
||||
|
||||
# Readonly settings - cannot be set by the user
|
||||
READONLY_SETTINGS_KEYS = [
|
||||
"org_name",
|
||||
"org_uuid",
|
||||
]
|
||||
|
||||
# Hidden settings - not displayed by the 'list' command and cannot be set by the user
|
||||
HIDDEN_SETTINGS_KEYS = [
|
||||
"config_path",
|
||||
"tool_repository_username",
|
||||
"tool_repository_password",
|
||||
]
|
||||
|
||||
class Settings(BaseModel):
|
||||
enterprise_base_url: Optional[str] = Field(
|
||||
default=DEFAULT_CLI_SETTINGS["enterprise_base_url"],
|
||||
description="Base URL of the CrewAI Enterprise instance",
|
||||
)
|
||||
tool_repository_username: Optional[str] = Field(
|
||||
None, description="Username for interacting with the Tool Repository"
|
||||
)
|
||||
@@ -20,7 +70,27 @@ class Settings(BaseModel):
|
||||
org_uuid: Optional[str] = Field(
|
||||
None, description="UUID of the currently active organization"
|
||||
)
|
||||
config_path: Path = Field(default=DEFAULT_CONFIG_PATH, exclude=True)
|
||||
config_path: Path = Field(default=DEFAULT_CONFIG_PATH, frozen=True, exclude=True)
|
||||
|
||||
oauth2_provider: str = Field(
|
||||
description="OAuth2 provider used for authentication (e.g., workos, okta, auth0).",
|
||||
default=DEFAULT_CLI_SETTINGS["oauth2_provider"]
|
||||
)
|
||||
|
||||
oauth2_audience: Optional[str] = Field(
|
||||
description="OAuth2 audience value, typically used to identify the target API or resource.",
|
||||
default=DEFAULT_CLI_SETTINGS["oauth2_audience"]
|
||||
)
|
||||
|
||||
oauth2_client_id: str = Field(
|
||||
default=DEFAULT_CLI_SETTINGS["oauth2_client_id"],
|
||||
description="OAuth2 client ID issued by the provider, used during authentication requests.",
|
||||
)
|
||||
|
||||
oauth2_domain: str = Field(
|
||||
description="OAuth2 provider's domain (e.g., your-org.auth0.com) used for issuing tokens.",
|
||||
default=DEFAULT_CLI_SETTINGS["oauth2_domain"]
|
||||
)
|
||||
|
||||
def __init__(self, config_path: Path = DEFAULT_CONFIG_PATH, **data):
|
||||
"""Load Settings from config path"""
|
||||
@@ -37,9 +107,16 @@ class Settings(BaseModel):
|
||||
merged_data = {**file_data, **data}
|
||||
super().__init__(config_path=config_path, **merged_data)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear all settings"""
|
||||
self.config_path.unlink(missing_ok=True)
|
||||
def clear_user_settings(self) -> None:
|
||||
"""Clear all user settings"""
|
||||
self._reset_user_settings()
|
||||
self.dump()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset all settings to default values"""
|
||||
self._reset_user_settings()
|
||||
self._reset_cli_settings()
|
||||
self.dump()
|
||||
|
||||
def dump(self) -> None:
|
||||
"""Save current settings to settings.json"""
|
||||
@@ -52,3 +129,13 @@ class Settings(BaseModel):
|
||||
updated_data = {**existing_data, **self.model_dump(exclude_unset=True)}
|
||||
with self.config_path.open("w") as f:
|
||||
json.dump(updated_data, f, indent=4)
|
||||
|
||||
def _reset_user_settings(self) -> None:
|
||||
"""Reset all user settings to default values"""
|
||||
for key in USER_SETTINGS_KEYS:
|
||||
setattr(self, key, None)
|
||||
|
||||
def _reset_cli_settings(self) -> None:
|
||||
"""Reset all CLI settings to default values"""
|
||||
for key in CLI_SETTINGS_KEYS:
|
||||
setattr(self, key, DEFAULT_CLI_SETTINGS.get(key))
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
DEFAULT_CREWAI_ENTERPRISE_URL = "https://app.crewai.com"
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_PROVIDER = "workos"
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE = "client_01JNJQWBJ4SPFN3SWJM5T7BDG8"
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_CLIENT_ID = "client_01JYT06R59SP0NXYGD994NFXXX"
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN = "login.crewai.com"
|
||||
|
||||
ENV_VARS = {
|
||||
"openai": [
|
||||
{
|
||||
@@ -320,5 +326,4 @@ DEFAULT_LLM_MODEL = "gpt-4o-mini"
|
||||
|
||||
JSON_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
|
||||
|
||||
|
||||
LITELLM_PARAMS = ["api_key", "api_base", "api_version"]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from os import getenv
|
||||
from typing import List, Optional
|
||||
from urllib.parse import urljoin
|
||||
|
||||
@@ -6,6 +5,7 @@ import requests
|
||||
|
||||
from crewai.cli.config import Settings
|
||||
from crewai.cli.version import get_crewai_version
|
||||
from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
|
||||
|
||||
class PlusAPI:
|
||||
@@ -17,6 +17,7 @@ class PlusAPI:
|
||||
ORGANIZATIONS_RESOURCE = "/crewai_plus/api/v1/me/organizations"
|
||||
CREWS_RESOURCE = "/crewai_plus/api/v1/crews"
|
||||
AGENTS_RESOURCE = "/crewai_plus/api/v1/agents"
|
||||
TRACING_RESOURCE = "/crewai_plus/api/v1/tracing"
|
||||
|
||||
def __init__(self, api_key: str) -> None:
|
||||
self.api_key = api_key
|
||||
@@ -29,7 +30,10 @@ class PlusAPI:
|
||||
settings = Settings()
|
||||
if settings.org_uuid:
|
||||
self.headers["X-Crewai-Organization-Id"] = settings.org_uuid
|
||||
self.base_url = getenv("CREWAI_BASE_URL", "https://app.crewai.com")
|
||||
|
||||
self.base_url = (
|
||||
str(settings.enterprise_base_url) or DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
)
|
||||
|
||||
def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
|
||||
url = urljoin(self.base_url, endpoint)
|
||||
@@ -108,7 +112,28 @@ class PlusAPI:
|
||||
|
||||
def create_crew(self, payload) -> requests.Response:
|
||||
return self._make_request("POST", self.CREWS_RESOURCE, json=payload)
|
||||
|
||||
|
||||
def get_organizations(self) -> requests.Response:
|
||||
return self._make_request("GET", self.ORGANIZATIONS_RESOURCE)
|
||||
|
||||
|
||||
def send_trace_batch(self, payload) -> requests.Response:
|
||||
return self._make_request("POST", self.TRACING_RESOURCE, json=payload)
|
||||
|
||||
def initialize_trace_batch(self, payload) -> requests.Response:
|
||||
return self._make_request(
|
||||
"POST", f"{self.TRACING_RESOURCE}/batches", json=payload
|
||||
)
|
||||
|
||||
def send_trace_events(self, trace_batch_id: str, payload) -> requests.Response:
|
||||
return self._make_request(
|
||||
"POST",
|
||||
f"{self.TRACING_RESOURCE}/batches/{trace_batch_id}/events",
|
||||
json=payload,
|
||||
)
|
||||
|
||||
def finalize_trace_batch(self, trace_batch_id: str, payload) -> requests.Response:
|
||||
return self._make_request(
|
||||
"PATCH",
|
||||
f"{self.TRACING_RESOURCE}/batches/{trace_batch_id}/finalize",
|
||||
json=payload,
|
||||
)
|
||||
|
||||
67
src/crewai/cli/settings/main.py
Normal file
67
src/crewai/cli/settings/main.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from crewai.cli.command import BaseCommand
|
||||
from crewai.cli.config import Settings, READONLY_SETTINGS_KEYS, HIDDEN_SETTINGS_KEYS
|
||||
from typing import Any
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
class SettingsCommand(BaseCommand):
|
||||
"""A class to handle CLI configuration commands."""
|
||||
|
||||
def __init__(self, settings_kwargs: dict[str, Any] = {}):
|
||||
super().__init__()
|
||||
self.settings = Settings(**settings_kwargs)
|
||||
|
||||
def list(self) -> None:
|
||||
"""List all CLI configuration parameters."""
|
||||
table = Table(title="CrewAI CLI Configuration")
|
||||
table.add_column("Setting", style="cyan", no_wrap=True)
|
||||
table.add_column("Value", style="green")
|
||||
table.add_column("Description", style="yellow")
|
||||
|
||||
# Add all settings to the table
|
||||
for field_name, field_info in Settings.model_fields.items():
|
||||
if field_name in HIDDEN_SETTINGS_KEYS:
|
||||
# Do not display hidden settings
|
||||
continue
|
||||
|
||||
current_value = getattr(self.settings, field_name)
|
||||
description = field_info.description or "No description available"
|
||||
display_value = (
|
||||
str(current_value) if current_value is not None else "Not set"
|
||||
)
|
||||
|
||||
table.add_row(field_name, display_value, description)
|
||||
|
||||
console.print(table)
|
||||
|
||||
def set(self, key: str, value: str) -> None:
|
||||
"""Set a CLI configuration parameter."""
|
||||
|
||||
readonly_settings = READONLY_SETTINGS_KEYS + HIDDEN_SETTINGS_KEYS
|
||||
|
||||
if not hasattr(self.settings, key) or key in readonly_settings:
|
||||
console.print(
|
||||
f"Error: Unknown or readonly configuration key '{key}'",
|
||||
style="bold red",
|
||||
)
|
||||
console.print("Available keys:", style="yellow")
|
||||
for field_name in Settings.model_fields.keys():
|
||||
if field_name not in readonly_settings:
|
||||
console.print(f" - {field_name}", style="yellow")
|
||||
raise SystemExit(1)
|
||||
|
||||
setattr(self.settings, key, value)
|
||||
self.settings.dump()
|
||||
|
||||
console.print(f"Successfully set '{key}' to '{value}'", style="bold green")
|
||||
|
||||
def reset_all_settings(self) -> None:
|
||||
"""Reset all CLI configuration parameters to default values."""
|
||||
self.settings.reset()
|
||||
console.print(
|
||||
"Successfully reset all configuration parameters to default values. It is recommended to run [bold yellow]'crewai login'[/bold yellow] to re-authenticate.",
|
||||
style="bold green",
|
||||
)
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.150.0,<1.0.0"
|
||||
"crewai[tools]>=0.157.0,<1.0.0"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.150.0,<1.0.0",
|
||||
"crewai[tools]>=0.157.0,<1.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]>=0.150.0"
|
||||
"crewai[tools]>=0.157.0"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
@@ -47,7 +48,6 @@ from crewai.memory.entity.entity_memory import EntityMemory
|
||||
from crewai.memory.external.external_memory import ExternalMemory
|
||||
from crewai.memory.long_term.long_term_memory import LongTermMemory
|
||||
from crewai.memory.short_term.short_term_memory import ShortTermMemory
|
||||
from crewai.memory.user.user_memory import UserMemory
|
||||
from crewai.process import Process
|
||||
from crewai.security import Fingerprint, SecurityConfig
|
||||
from crewai.task import Task
|
||||
@@ -73,6 +73,11 @@ from crewai.utilities.events.crew_events import (
|
||||
)
|
||||
from crewai.utilities.events.crewai_event_bus import crewai_event_bus
|
||||
from crewai.utilities.events.event_listener import EventListener
|
||||
from crewai.utilities.events.listeners.tracing.trace_listener import (
|
||||
TraceCollectionListener,
|
||||
)
|
||||
|
||||
|
||||
from crewai.utilities.formatter import (
|
||||
aggregate_raw_outputs_from_task_outputs,
|
||||
aggregate_raw_outputs_from_tasks,
|
||||
@@ -95,7 +100,6 @@ class Crew(FlowTrackable, BaseModel):
|
||||
manager_llm: The language model that will run manager agent.
|
||||
manager_agent: Custom agent that will be used as manager.
|
||||
memory: Whether the crew should use memory to store memories of it's execution.
|
||||
memory_config: Configuration for the memory to be used for the crew.
|
||||
cache: Whether the crew should use a cache to store the results of the tools execution.
|
||||
function_calling_llm: The language model that will run the tool calling for all the agents.
|
||||
process: The process flow that the crew will follow (e.g., sequential, hierarchical).
|
||||
@@ -121,7 +125,6 @@ class Crew(FlowTrackable, BaseModel):
|
||||
_short_term_memory: Optional[InstanceOf[ShortTermMemory]] = PrivateAttr()
|
||||
_long_term_memory: Optional[InstanceOf[LongTermMemory]] = PrivateAttr()
|
||||
_entity_memory: Optional[InstanceOf[EntityMemory]] = PrivateAttr()
|
||||
_user_memory: Optional[InstanceOf[UserMemory]] = PrivateAttr()
|
||||
_external_memory: Optional[InstanceOf[ExternalMemory]] = PrivateAttr()
|
||||
_train: Optional[bool] = PrivateAttr(default=False)
|
||||
_train_iteration: Optional[int] = PrivateAttr()
|
||||
@@ -133,7 +136,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
default_factory=TaskOutputStorageHandler
|
||||
)
|
||||
|
||||
name: Optional[str] = Field(default=None)
|
||||
name: Optional[str] = Field(default="crew")
|
||||
cache: bool = Field(default=True)
|
||||
tasks: List[Task] = Field(default_factory=list)
|
||||
agents: List[BaseAgent] = Field(default_factory=list)
|
||||
@@ -143,10 +146,6 @@ class Crew(FlowTrackable, BaseModel):
|
||||
default=False,
|
||||
description="Whether the crew should use memory to store memories of it's execution",
|
||||
)
|
||||
memory_config: Optional[Dict[str, Any]] = Field(
|
||||
default=None,
|
||||
description="Configuration for the memory to be used for the crew.",
|
||||
)
|
||||
short_term_memory: Optional[InstanceOf[ShortTermMemory]] = Field(
|
||||
default=None,
|
||||
description="An Instance of the ShortTermMemory to be used by the Crew",
|
||||
@@ -159,10 +158,6 @@ class Crew(FlowTrackable, BaseModel):
|
||||
default=None,
|
||||
description="An Instance of the EntityMemory to be used by the Crew",
|
||||
)
|
||||
user_memory: Optional[InstanceOf[UserMemory]] = Field(
|
||||
default=None,
|
||||
description="DEPRECATED: Will be removed in version 0.156.0 or on 2025-08-04, whichever comes first. Use external_memory instead.",
|
||||
)
|
||||
external_memory: Optional[InstanceOf[ExternalMemory]] = Field(
|
||||
default=None,
|
||||
description="An Instance of the ExternalMemory to be used by the Crew",
|
||||
@@ -249,6 +244,10 @@ class Crew(FlowTrackable, BaseModel):
|
||||
default_factory=SecurityConfig,
|
||||
description="Security configuration for the crew, including fingerprinting.",
|
||||
)
|
||||
token_usage: Optional[UsageMetrics] = Field(
|
||||
default=None,
|
||||
description="Metrics for the LLM usage during all tasks execution.",
|
||||
)
|
||||
|
||||
@field_validator("id", mode="before")
|
||||
@classmethod
|
||||
@@ -280,6 +279,9 @@ class Crew(FlowTrackable, BaseModel):
|
||||
|
||||
self._cache_handler = CacheHandler()
|
||||
event_listener = EventListener()
|
||||
if os.getenv("CREWAI_TRACING_ENABLED", "false").lower() == "true":
|
||||
trace_listener = TraceCollectionListener()
|
||||
trace_listener.setup_listeners(crewai_event_bus)
|
||||
event_listener.verbose = self.verbose
|
||||
event_listener.formatter.verbose = self.verbose
|
||||
self._logger = Logger(verbose=self.verbose)
|
||||
@@ -291,20 +293,6 @@ class Crew(FlowTrackable, BaseModel):
|
||||
|
||||
return self
|
||||
|
||||
def _initialize_user_memory(self):
|
||||
if (
|
||||
self.memory_config
|
||||
and "user_memory" in self.memory_config
|
||||
and self.memory_config.get("provider") == "mem0"
|
||||
): # Check for user_memory in config
|
||||
user_memory_config = self.memory_config["user_memory"]
|
||||
if isinstance(
|
||||
user_memory_config, dict
|
||||
): # Check if it's a configuration dict
|
||||
self._user_memory = UserMemory(crew=self)
|
||||
else:
|
||||
raise TypeError("user_memory must be a configuration dictionary")
|
||||
|
||||
def _initialize_default_memories(self):
|
||||
self._long_term_memory = self._long_term_memory or LongTermMemory()
|
||||
self._short_term_memory = self._short_term_memory or ShortTermMemory(
|
||||
@@ -327,12 +315,8 @@ class Crew(FlowTrackable, BaseModel):
|
||||
self._short_term_memory = self.short_term_memory
|
||||
self._entity_memory = self.entity_memory
|
||||
|
||||
# UserMemory will be removed in version 0.156.0 or on 2025-08-04, whichever comes first
|
||||
self._user_memory = None
|
||||
|
||||
if self.memory:
|
||||
self._initialize_default_memories()
|
||||
self._initialize_user_memory()
|
||||
|
||||
return self
|
||||
|
||||
@@ -575,7 +559,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
CrewTrainStartedEvent(
|
||||
crew_name=self.name or "crew",
|
||||
crew_name=self.name,
|
||||
n_iterations=n_iterations,
|
||||
filename=filename,
|
||||
inputs=inputs,
|
||||
@@ -602,7 +586,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
CrewTrainCompletedEvent(
|
||||
crew_name=self.name or "crew",
|
||||
crew_name=self.name,
|
||||
n_iterations=n_iterations,
|
||||
filename=filename,
|
||||
),
|
||||
@@ -610,7 +594,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
except Exception as e:
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
CrewTrainFailedEvent(error=str(e), crew_name=self.name or "crew"),
|
||||
CrewTrainFailedEvent(error=str(e), crew_name=self.name),
|
||||
)
|
||||
self._logger.log("error", f"Training failed: {e}", color="red")
|
||||
CrewTrainingHandler(TRAINING_DATA_FILE).clear()
|
||||
@@ -634,7 +618,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
CrewKickoffStartedEvent(crew_name=self.name or "crew", inputs=inputs),
|
||||
CrewKickoffStartedEvent(crew_name=self.name, inputs=inputs),
|
||||
)
|
||||
|
||||
# Starts the crew to work on its assigned tasks.
|
||||
@@ -683,7 +667,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
except Exception as e:
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
CrewKickoffFailedEvent(error=str(e), crew_name=self.name or "crew"),
|
||||
CrewKickoffFailedEvent(error=str(e), crew_name=self.name),
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
@@ -1073,11 +1057,13 @@ class Crew(FlowTrackable, BaseModel):
|
||||
|
||||
final_string_output = final_task_output.raw
|
||||
self._finish_execution(final_string_output)
|
||||
token_usage = self.calculate_usage_metrics()
|
||||
self.token_usage = self.calculate_usage_metrics()
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
CrewKickoffCompletedEvent(
|
||||
crew_name=self.name or "crew", output=final_task_output
|
||||
crew_name=self.name,
|
||||
output=final_task_output,
|
||||
total_tokens=self.token_usage.total_tokens,
|
||||
),
|
||||
)
|
||||
return CrewOutput(
|
||||
@@ -1085,7 +1071,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
pydantic=final_task_output.pydantic,
|
||||
json_dict=final_task_output.json_dict,
|
||||
tasks_output=task_outputs,
|
||||
token_usage=token_usage,
|
||||
token_usage=self.token_usage,
|
||||
)
|
||||
|
||||
def _process_async_tasks(
|
||||
@@ -1254,9 +1240,6 @@ class Crew(FlowTrackable, BaseModel):
|
||||
copied_data["entity_memory"] = self.entity_memory.model_copy(deep=True)
|
||||
if self.external_memory:
|
||||
copied_data["external_memory"] = self.external_memory.model_copy(deep=True)
|
||||
if self.user_memory:
|
||||
# DEPRECATED: UserMemory will be removed in version 0.156.0 or on 2025-08-04
|
||||
copied_data["user_memory"] = self.user_memory.model_copy(deep=True)
|
||||
|
||||
copied_data.pop("agents", None)
|
||||
copied_data.pop("tasks", None)
|
||||
@@ -1325,7 +1308,7 @@ class Crew(FlowTrackable, BaseModel):
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
CrewTestStartedEvent(
|
||||
crew_name=self.name or "crew",
|
||||
crew_name=self.name,
|
||||
n_iterations=n_iterations,
|
||||
eval_llm=llm_instance,
|
||||
inputs=inputs,
|
||||
@@ -1344,13 +1327,13 @@ class Crew(FlowTrackable, BaseModel):
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
CrewTestCompletedEvent(
|
||||
crew_name=self.name or "crew",
|
||||
crew_name=self.name,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
CrewTestFailedEvent(error=str(e), crew_name=self.name or "crew"),
|
||||
CrewTestFailedEvent(error=str(e), crew_name=self.name),
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import copy
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
@@ -32,6 +33,9 @@ from crewai.utilities.events.flow_events import (
|
||||
MethodExecutionFinishedEvent,
|
||||
MethodExecutionStartedEvent,
|
||||
)
|
||||
from crewai.utilities.events.listeners.tracing.trace_listener import (
|
||||
TraceCollectionListener,
|
||||
)
|
||||
from crewai.utilities.printer import Printer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -436,6 +440,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
_routers: Set[str] = set()
|
||||
_router_paths: Dict[str, List[str]] = {}
|
||||
initial_state: Union[Type[T], T, None] = None
|
||||
name: Optional[str] = None
|
||||
|
||||
def __class_getitem__(cls: Type["Flow"], item: Type[T]) -> Type["Flow"]:
|
||||
class _FlowGeneric(cls): # type: ignore
|
||||
@@ -464,7 +469,9 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
|
||||
# Initialize state with initial values
|
||||
self._state = self._create_initial_state()
|
||||
|
||||
if os.getenv("CREWAI_TRACING_ENABLED", "false").lower() == "true":
|
||||
trace_listener = TraceCollectionListener()
|
||||
trace_listener.setup_listeners(crewai_event_bus)
|
||||
# Apply any additional kwargs
|
||||
if kwargs:
|
||||
self._initialize_state(kwargs)
|
||||
@@ -473,7 +480,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
self,
|
||||
FlowCreatedEvent(
|
||||
type="flow_created",
|
||||
flow_name=self.__class__.__name__,
|
||||
flow_name=self.name or self.__class__.__name__,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -769,7 +776,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
self,
|
||||
FlowStartedEvent(
|
||||
type="flow_started",
|
||||
flow_name=self.__class__.__name__,
|
||||
flow_name=self.name or self.__class__.__name__,
|
||||
inputs=inputs,
|
||||
),
|
||||
)
|
||||
@@ -792,7 +799,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
self,
|
||||
FlowFinishedEvent(
|
||||
type="flow_finished",
|
||||
flow_name=self.__class__.__name__,
|
||||
flow_name=self.name or self.__class__.__name__,
|
||||
result=final_output,
|
||||
),
|
||||
)
|
||||
@@ -834,7 +841,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
MethodExecutionStartedEvent(
|
||||
type="method_execution_started",
|
||||
method_name=method_name,
|
||||
flow_name=self.__class__.__name__,
|
||||
flow_name=self.name or self.__class__.__name__,
|
||||
params=dumped_params,
|
||||
state=self._copy_state(),
|
||||
),
|
||||
@@ -856,7 +863,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
MethodExecutionFinishedEvent(
|
||||
type="method_execution_finished",
|
||||
method_name=method_name,
|
||||
flow_name=self.__class__.__name__,
|
||||
flow_name=self.name or self.__class__.__name__,
|
||||
state=self._copy_state(),
|
||||
result=result,
|
||||
),
|
||||
@@ -869,7 +876,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
MethodExecutionFailedEvent(
|
||||
type="method_execution_failed",
|
||||
method_name=method_name,
|
||||
flow_name=self.__class__.__name__,
|
||||
flow_name=self.name or self.__class__.__name__,
|
||||
error=e,
|
||||
),
|
||||
)
|
||||
@@ -1076,7 +1083,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
self,
|
||||
FlowPlotEvent(
|
||||
type="flow_plot",
|
||||
flow_name=self.__class__.__name__,
|
||||
flow_name=self.name or self.__class__.__name__,
|
||||
),
|
||||
)
|
||||
plot_flow(self, filename)
|
||||
|
||||
@@ -81,7 +81,7 @@ class SQLiteFlowPersistence(FlowPersistence):
|
||||
"""
|
||||
# Convert state_data to dict, handling both Pydantic and dict cases
|
||||
if isinstance(state_data, BaseModel):
|
||||
state_dict = dict(state_data) # Use dict() for better type compatibility
|
||||
state_dict = state_data.model_dump()
|
||||
elif isinstance(state_data, dict):
|
||||
state_dict = state_data
|
||||
else:
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
class BaseEmbedder(ABC):
|
||||
"""
|
||||
Abstract base class for text embedding models
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def embed_chunks(self, chunks: List[str]) -> np.ndarray:
|
||||
"""
|
||||
Generate embeddings for a list of text chunks
|
||||
|
||||
Args:
|
||||
chunks: List of text chunks to embed
|
||||
|
||||
Returns:
|
||||
Array of embeddings
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def embed_texts(self, texts: List[str]) -> np.ndarray:
|
||||
"""
|
||||
Generate embeddings for a list of texts
|
||||
|
||||
Args:
|
||||
texts: List of texts to embed
|
||||
|
||||
Returns:
|
||||
Array of embeddings
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def embed_text(self, text: str) -> np.ndarray:
|
||||
"""
|
||||
Generate embedding for a single text
|
||||
|
||||
Args:
|
||||
text: Text to embed
|
||||
|
||||
Returns:
|
||||
Embedding array
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def dimension(self) -> int:
|
||||
"""Get the dimension of the embeddings"""
|
||||
pass
|
||||
@@ -13,7 +13,7 @@ from chromadb.api.types import OneOrMany
|
||||
from chromadb.config import Settings
|
||||
|
||||
from crewai.knowledge.storage.base_knowledge_storage import BaseKnowledgeStorage
|
||||
from crewai.utilities import EmbeddingConfigurator
|
||||
from crewai.rag.embeddings.configurator import EmbeddingConfigurator
|
||||
from crewai.utilities.chromadb import sanitize_collection_name
|
||||
from crewai.utilities.constants import KNOWLEDGE_DIRECTORY
|
||||
from crewai.utilities.logger import Logger
|
||||
|
||||
@@ -28,7 +28,7 @@ from pydantic import (
|
||||
InstanceOf,
|
||||
PrivateAttr,
|
||||
model_validator,
|
||||
field_validator
|
||||
field_validator,
|
||||
)
|
||||
|
||||
from crewai.agents.agent_builder.base_agent import BaseAgent
|
||||
@@ -147,7 +147,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
default=15, description="Maximum number of iterations for tool usage"
|
||||
)
|
||||
max_execution_time: Optional[int] = Field(
|
||||
default=None, description="Maximum execution time in seconds"
|
||||
default=None, description=". Maximum execution time in seconds"
|
||||
)
|
||||
respect_context_window: bool = Field(
|
||||
default=True,
|
||||
@@ -210,7 +210,9 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
"""Set up the LLM and other components after initialization."""
|
||||
self.llm = create_llm(self.llm)
|
||||
if not isinstance(self.llm, BaseLLM):
|
||||
raise ValueError(f"Expected LLM instance of type BaseLLM, got {type(self.llm).__name__}")
|
||||
raise ValueError(
|
||||
f"Expected LLM instance of type BaseLLM, got {type(self.llm).__name__}"
|
||||
)
|
||||
|
||||
# Initialize callbacks
|
||||
token_callback = TokenCalcHandler(token_cost_process=self._token_process)
|
||||
@@ -233,7 +235,9 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
from crewai.tasks.llm_guardrail import LLMGuardrail
|
||||
|
||||
if not isinstance(self.llm, BaseLLM):
|
||||
raise TypeError(f"Guardrail requires LLM instance of type BaseLLM, got {type(self.llm).__name__}")
|
||||
raise TypeError(
|
||||
f"Guardrail requires LLM instance of type BaseLLM, got {type(self.llm).__name__}"
|
||||
)
|
||||
|
||||
self._guardrail = LLMGuardrail(description=self.guardrail, llm=self.llm)
|
||||
|
||||
@@ -515,7 +519,8 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
|
||||
enforce_rpm_limit(self.request_within_rpm_limit)
|
||||
|
||||
# Emit LLM call started event
|
||||
llm = cast(LLM, self.llm)
|
||||
model = llm.model if hasattr(llm, "model") else "unknown"
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=LLMCallStartedEvent(
|
||||
@@ -523,6 +528,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
tools=None,
|
||||
callbacks=self._callbacks,
|
||||
from_agent=self,
|
||||
model=model,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -543,6 +549,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
response=answer,
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
from_agent=self,
|
||||
model=model,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -622,4 +629,4 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
|
||||
def _append_message(self, text: str, role: str = "assistant") -> None:
|
||||
"""Append a message to the message list with the given role."""
|
||||
self._messages.append(format_message_for_llm(text, role=role))
|
||||
self._messages.append(format_message_for_llm(text, role=role))
|
||||
|
||||
@@ -61,6 +61,7 @@ load_dotenv()
|
||||
|
||||
litellm.suppress_debug_info = True
|
||||
|
||||
|
||||
class FilteredStream(io.TextIOBase):
|
||||
_lock = None
|
||||
|
||||
@@ -78,7 +79,8 @@ class FilteredStream(io.TextIOBase):
|
||||
# Skip common noisy LiteLLM banners and any other lines that contain "litellm"
|
||||
if (
|
||||
"litellm.info:" in lower_s
|
||||
or "Consider using a smaller input or implementing a text splitting strategy" in lower_s
|
||||
or "Consider using a smaller input or implementing a text splitting strategy"
|
||||
in lower_s
|
||||
):
|
||||
return 0
|
||||
|
||||
@@ -286,6 +288,8 @@ class AccumulatedToolArgs(BaseModel):
|
||||
|
||||
|
||||
class LLM(BaseLLM):
|
||||
completion_cost: Optional[float] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str,
|
||||
@@ -532,7 +536,11 @@ class LLM(BaseLLM):
|
||||
assert hasattr(crewai_event_bus, "emit")
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=LLMStreamChunkEvent(chunk=chunk_content, from_task=from_task, from_agent=from_agent),
|
||||
event=LLMStreamChunkEvent(
|
||||
chunk=chunk_content,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
),
|
||||
)
|
||||
# --- 4) Fallback to non-streaming if no content received
|
||||
if not full_response.strip() and chunk_count == 0:
|
||||
@@ -545,7 +553,11 @@ class LLM(BaseLLM):
|
||||
"stream_options", None
|
||||
) # Remove stream_options for non-streaming call
|
||||
return self._handle_non_streaming_response(
|
||||
non_streaming_params, callbacks, available_functions, from_task, from_agent
|
||||
non_streaming_params,
|
||||
callbacks,
|
||||
available_functions,
|
||||
from_task,
|
||||
from_agent,
|
||||
)
|
||||
|
||||
# --- 5) Handle empty response with chunks
|
||||
@@ -630,7 +642,13 @@ class LLM(BaseLLM):
|
||||
# Log token usage if available in streaming mode
|
||||
self._handle_streaming_callbacks(callbacks, usage_info, last_chunk)
|
||||
# Emit completion event and return response
|
||||
self._handle_emit_call_events(response=full_response, call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, messages=params["messages"])
|
||||
self._handle_emit_call_events(
|
||||
response=full_response,
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
)
|
||||
return full_response
|
||||
|
||||
# --- 9) Handle tool calls if present
|
||||
@@ -642,7 +660,13 @@ class LLM(BaseLLM):
|
||||
self._handle_streaming_callbacks(callbacks, usage_info, last_chunk)
|
||||
|
||||
# --- 11) Emit completion event and return response
|
||||
self._handle_emit_call_events(response=full_response, call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, messages=params["messages"])
|
||||
self._handle_emit_call_events(
|
||||
response=full_response,
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
)
|
||||
return full_response
|
||||
|
||||
except ContextWindowExceededError as e:
|
||||
@@ -654,14 +678,22 @@ class LLM(BaseLLM):
|
||||
logging.error(f"Error in streaming response: {str(e)}")
|
||||
if full_response.strip():
|
||||
logging.warning(f"Returning partial response despite error: {str(e)}")
|
||||
self._handle_emit_call_events(response=full_response, call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, messages=params["messages"])
|
||||
self._handle_emit_call_events(
|
||||
response=full_response,
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
)
|
||||
return full_response
|
||||
|
||||
# Emit failed event and re-raise the exception
|
||||
assert hasattr(crewai_event_bus, "emit")
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=LLMCallFailedEvent(error=str(e), from_task=from_task, from_agent=from_agent),
|
||||
event=LLMCallFailedEvent(
|
||||
error=str(e), from_task=from_task, from_agent=from_agent
|
||||
),
|
||||
)
|
||||
raise Exception(f"Failed to get streaming response: {str(e)}")
|
||||
|
||||
@@ -779,6 +811,7 @@ class LLM(BaseLLM):
|
||||
# across the codebase. This allows CrewAgentExecutor to handle context
|
||||
# length issues appropriately.
|
||||
response = litellm.completion(**params)
|
||||
|
||||
except ContextWindowExceededError as e:
|
||||
# Convert litellm's context window error to our own exception type
|
||||
# for consistent handling in the rest of the codebase
|
||||
@@ -805,7 +838,13 @@ class LLM(BaseLLM):
|
||||
|
||||
# --- 5) If no tool calls or no available functions, return the text response directly as long as there is a text response
|
||||
if (not tool_calls or not available_functions) and text_response:
|
||||
self._handle_emit_call_events(response=text_response, call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, messages=params["messages"])
|
||||
self._handle_emit_call_events(
|
||||
response=text_response,
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
)
|
||||
return text_response
|
||||
# --- 6) If there is no text response, no available functions, but there are tool calls, return the tool calls
|
||||
elif tool_calls and not available_functions and not text_response:
|
||||
@@ -816,7 +855,13 @@ class LLM(BaseLLM):
|
||||
if tool_result is not None:
|
||||
return tool_result
|
||||
# --- 8) If tool call handling didn't return a result, emit completion event and return text response
|
||||
self._handle_emit_call_events(response=text_response, call_type=LLMCallType.LLM_CALL, from_task=from_task, from_agent=from_agent, messages=params["messages"])
|
||||
self._handle_emit_call_events(
|
||||
response=text_response,
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
)
|
||||
return text_response
|
||||
|
||||
def _handle_tool_call(
|
||||
@@ -873,7 +918,9 @@ class LLM(BaseLLM):
|
||||
)
|
||||
|
||||
# --- 3.3) Emit success event
|
||||
self._handle_emit_call_events(response=result, call_type=LLMCallType.TOOL_CALL)
|
||||
self._handle_emit_call_events(
|
||||
response=result, call_type=LLMCallType.TOOL_CALL
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
# --- 3.4) Handle execution errors
|
||||
@@ -891,7 +938,7 @@ class LLM(BaseLLM):
|
||||
event=ToolUsageErrorEvent(
|
||||
tool_name=function_name,
|
||||
tool_args=function_args,
|
||||
error=f"Tool execution error: {str(e)}"
|
||||
error=f"Tool execution error: {str(e)}",
|
||||
),
|
||||
)
|
||||
return None
|
||||
@@ -941,6 +988,7 @@ class LLM(BaseLLM):
|
||||
available_functions=available_functions,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
model=self.model,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -978,17 +1026,22 @@ class LLM(BaseLLM):
|
||||
# whether to summarize the content or abort based on the respect_context_window flag
|
||||
raise
|
||||
except Exception as e:
|
||||
unsupported_stop = "Unsupported parameter" in str(e) and "'stop'" in str(e)
|
||||
unsupported_stop = "Unsupported parameter" in str(
|
||||
e
|
||||
) and "'stop'" in str(e)
|
||||
|
||||
if unsupported_stop:
|
||||
if "additional_drop_params" in self.additional_params and isinstance(self.additional_params["additional_drop_params"], list):
|
||||
if (
|
||||
"additional_drop_params" in self.additional_params
|
||||
and isinstance(
|
||||
self.additional_params["additional_drop_params"], list
|
||||
)
|
||||
):
|
||||
self.additional_params["additional_drop_params"].append("stop")
|
||||
else:
|
||||
self.additional_params = {"additional_drop_params": ["stop"]}
|
||||
|
||||
logging.info(
|
||||
"Retrying LLM call without the unsupported 'stop'"
|
||||
)
|
||||
logging.info("Retrying LLM call without the unsupported 'stop'")
|
||||
|
||||
return self.call(
|
||||
messages,
|
||||
@@ -1002,11 +1055,20 @@ class LLM(BaseLLM):
|
||||
assert hasattr(crewai_event_bus, "emit")
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=LLMCallFailedEvent(error=str(e), from_task=from_task, from_agent=from_agent),
|
||||
event=LLMCallFailedEvent(
|
||||
error=str(e), from_task=from_task, from_agent=from_agent
|
||||
),
|
||||
)
|
||||
raise
|
||||
|
||||
def _handle_emit_call_events(self, response: Any, call_type: LLMCallType, from_task: Optional[Any] = None, from_agent: Optional[Any] = None, messages: str | list[dict[str, Any]] | None = None):
|
||||
def _handle_emit_call_events(
|
||||
self,
|
||||
response: Any,
|
||||
call_type: LLMCallType,
|
||||
from_task: Optional[Any] = None,
|
||||
from_agent: Optional[Any] = None,
|
||||
messages: str | list[dict[str, Any]] | None = None,
|
||||
):
|
||||
"""Handle the events for the LLM call.
|
||||
|
||||
Args:
|
||||
@@ -1019,7 +1081,14 @@ class LLM(BaseLLM):
|
||||
assert hasattr(crewai_event_bus, "emit")
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=LLMCallCompletedEvent(messages=messages, response=response, call_type=call_type, from_task=from_task, from_agent=from_agent),
|
||||
event=LLMCallCompletedEvent(
|
||||
messages=messages,
|
||||
response=response,
|
||||
call_type=call_type,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
model=self.model,
|
||||
),
|
||||
)
|
||||
|
||||
def _format_messages_for_provider(
|
||||
@@ -1074,11 +1143,13 @@ class LLM(BaseLLM):
|
||||
|
||||
# TODO: Remove this code after merging PR https://github.com/BerriAI/litellm/pull/10917
|
||||
# Ollama doesn't supports last message to be 'assistant'
|
||||
if "ollama" in self.model.lower() and messages and messages[-1]["role"] == "assistant":
|
||||
if (
|
||||
"ollama" in self.model.lower()
|
||||
and messages
|
||||
and messages[-1]["role"] == "assistant"
|
||||
):
|
||||
messages = messages.copy()
|
||||
messages.append(
|
||||
{"role": "user", "content": ""}
|
||||
)
|
||||
messages.append({"role": "user", "content": ""})
|
||||
return messages
|
||||
|
||||
# Handle Anthropic models
|
||||
@@ -1100,7 +1171,7 @@ class LLM(BaseLLM):
|
||||
- If there is no '/', defaults to "openai".
|
||||
"""
|
||||
if "/" in self.model:
|
||||
return self.model.split("/")[0]
|
||||
return self.model.partition("/")[0]
|
||||
return None
|
||||
|
||||
def _validate_call_params(self) -> None:
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
from .entity.entity_memory import EntityMemory
|
||||
from .long_term.long_term_memory import LongTermMemory
|
||||
from .short_term.short_term_memory import ShortTermMemory
|
||||
from .user.user_memory import UserMemory
|
||||
from .external.external_memory import ExternalMemory
|
||||
|
||||
__all__ = [
|
||||
"UserMemory",
|
||||
"EntityMemory",
|
||||
"LongTermMemory",
|
||||
"ShortTermMemory",
|
||||
|
||||
@@ -1,32 +1,24 @@
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Optional
|
||||
|
||||
from crewai.memory import (
|
||||
EntityMemory,
|
||||
ExternalMemory,
|
||||
LongTermMemory,
|
||||
ShortTermMemory,
|
||||
UserMemory,
|
||||
)
|
||||
|
||||
|
||||
class ContextualMemory:
|
||||
def __init__(
|
||||
self,
|
||||
memory_config: Optional[Dict[str, Any]],
|
||||
stm: ShortTermMemory,
|
||||
ltm: LongTermMemory,
|
||||
em: EntityMemory,
|
||||
um: UserMemory,
|
||||
exm: ExternalMemory,
|
||||
):
|
||||
if memory_config is not None:
|
||||
self.memory_provider = memory_config.get("provider")
|
||||
else:
|
||||
self.memory_provider = None
|
||||
self.stm = stm
|
||||
self.ltm = ltm
|
||||
self.em = em
|
||||
self.um = um
|
||||
self.exm = exm
|
||||
|
||||
def build_context_for_task(self, task, context) -> str:
|
||||
@@ -44,8 +36,6 @@ class ContextualMemory:
|
||||
context.append(self._fetch_stm_context(query))
|
||||
context.append(self._fetch_entity_context(query))
|
||||
context.append(self._fetch_external_context(query))
|
||||
if self.memory_provider == "mem0":
|
||||
context.append(self._fetch_user_context(query))
|
||||
return "\n".join(filter(None, context))
|
||||
|
||||
def _fetch_stm_context(self, query) -> str:
|
||||
@@ -60,7 +50,7 @@ class ContextualMemory:
|
||||
stm_results = self.stm.search(query)
|
||||
formatted_results = "\n".join(
|
||||
[
|
||||
f"- {result['memory'] if self.memory_provider == 'mem0' else result['context']}"
|
||||
f"- {result['context']}"
|
||||
for result in stm_results
|
||||
]
|
||||
)
|
||||
@@ -100,34 +90,12 @@ class ContextualMemory:
|
||||
em_results = self.em.search(query)
|
||||
formatted_results = "\n".join(
|
||||
[
|
||||
f"- {result['memory'] if self.memory_provider == 'mem0' else result['context']}"
|
||||
f"- {result['context']}"
|
||||
for result in em_results
|
||||
] # type: ignore # Invalid index type "str" for "str"; expected type "SupportsIndex | slice"
|
||||
)
|
||||
return f"Entities:\n{formatted_results}" if em_results else ""
|
||||
|
||||
def _fetch_user_context(self, query: str) -> str:
|
||||
"""
|
||||
DEPRECATED: Will be removed in version 0.156.0 or on 2025-08-04, whichever comes first.
|
||||
Fetches and formats relevant user information from User Memory.
|
||||
Args:
|
||||
query (str): The search query to find relevant user memories.
|
||||
Returns:
|
||||
str: Formatted user memories as bullet points, or an empty string if none found.
|
||||
"""
|
||||
|
||||
if self.um is None:
|
||||
return ""
|
||||
|
||||
user_memories = self.um.search(query)
|
||||
if not user_memories:
|
||||
return ""
|
||||
|
||||
formatted_memories = "\n".join(
|
||||
f"- {result['memory']}" for result in user_memories
|
||||
)
|
||||
return f"User memories/preferences:\n{formatted_memories}"
|
||||
|
||||
def _fetch_external_context(self, query: str) -> str:
|
||||
"""
|
||||
Fetches and formats relevant information from External Memory.
|
||||
@@ -145,6 +113,6 @@ class ContextualMemory:
|
||||
return ""
|
||||
|
||||
formatted_memories = "\n".join(
|
||||
f"- {result['memory']}" for result in external_memories
|
||||
f"- {result['context']}" for result in external_memories
|
||||
)
|
||||
return f"External memories:\n{formatted_memories}"
|
||||
|
||||
@@ -27,11 +27,7 @@ class EntityMemory(Memory):
|
||||
_memory_provider: Optional[str] = PrivateAttr()
|
||||
|
||||
def __init__(self, crew=None, embedder_config=None, storage=None, path=None):
|
||||
if crew and hasattr(crew, "memory_config") and crew.memory_config is not None:
|
||||
memory_provider = crew.memory_config.get("provider")
|
||||
else:
|
||||
memory_provider = None
|
||||
|
||||
memory_provider = embedder_config.get("provider") if embedder_config else None
|
||||
if memory_provider == "mem0":
|
||||
try:
|
||||
from crewai.memory.storage.mem0_storage import Mem0Storage
|
||||
@@ -39,7 +35,8 @@ class EntityMemory(Memory):
|
||||
raise ImportError(
|
||||
"Mem0 is not installed. Please install it with `pip install mem0ai`."
|
||||
)
|
||||
storage = Mem0Storage(type="entities", crew=crew)
|
||||
config = embedder_config.get("config")
|
||||
storage = Mem0Storage(type="short_term", crew=crew, config=config)
|
||||
else:
|
||||
storage = (
|
||||
storage
|
||||
|
||||
@@ -29,11 +29,7 @@ class ShortTermMemory(Memory):
|
||||
_memory_provider: Optional[str] = PrivateAttr()
|
||||
|
||||
def __init__(self, crew=None, embedder_config=None, storage=None, path=None):
|
||||
if crew and hasattr(crew, "memory_config") and crew.memory_config is not None:
|
||||
memory_provider = crew.memory_config.get("provider")
|
||||
else:
|
||||
memory_provider = None
|
||||
|
||||
memory_provider = embedder_config.get("provider") if embedder_config else None
|
||||
if memory_provider == "mem0":
|
||||
try:
|
||||
from crewai.memory.storage.mem0_storage import Mem0Storage
|
||||
@@ -41,7 +37,8 @@ class ShortTermMemory(Memory):
|
||||
raise ImportError(
|
||||
"Mem0 is not installed. Please install it with `pip install mem0ai`."
|
||||
)
|
||||
storage = Mem0Storage(type="short_term", crew=crew)
|
||||
config = embedder_config.get("config")
|
||||
storage = Mem0Storage(type="short_term", crew=crew, config=config)
|
||||
else:
|
||||
storage = (
|
||||
storage
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import os
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from collections import defaultdict
|
||||
from mem0 import Memory, MemoryClient
|
||||
from crewai.utilities.chromadb import sanitize_collection_name
|
||||
|
||||
from crewai.memory.storage.interface import Storage
|
||||
|
||||
@@ -18,32 +19,24 @@ class Mem0Storage(Storage):
|
||||
self._validate_type(type)
|
||||
self.memory_type = type
|
||||
self.crew = crew
|
||||
self.config = config or {}
|
||||
|
||||
# TODO: Memory config will be removed in the future the config will be passed as a parameter
|
||||
self.config = config or getattr(crew, "memory_config", {}).get("config", {}) or {}
|
||||
|
||||
self._validate_user_id()
|
||||
self._extract_config_values()
|
||||
self._initialize_memory()
|
||||
|
||||
def _validate_type(self, type):
|
||||
supported_types = {"user", "short_term", "long_term", "entities", "external"}
|
||||
supported_types = {"short_term", "long_term", "entities", "external"}
|
||||
if type not in supported_types:
|
||||
raise ValueError(
|
||||
f"Invalid type '{type}' for Mem0Storage. Must be one of: {', '.join(supported_types)}"
|
||||
)
|
||||
|
||||
def _validate_user_id(self):
|
||||
if self.memory_type == "user" and not self.config.get("user_id", ""):
|
||||
raise ValueError("User ID is required for user memory type")
|
||||
|
||||
def _extract_config_values(self):
|
||||
cfg = self.config
|
||||
self.mem0_run_id = cfg.get("run_id")
|
||||
self.includes = cfg.get("includes")
|
||||
self.excludes = cfg.get("excludes")
|
||||
self.custom_categories = cfg.get("custom_categories")
|
||||
self.infer = cfg.get("infer", False)
|
||||
self.mem0_run_id = self.config.get("run_id")
|
||||
self.includes = self.config.get("includes")
|
||||
self.excludes = self.config.get("excludes")
|
||||
self.custom_categories = self.config.get("custom_categories")
|
||||
self.infer = self.config.get("infer", True)
|
||||
|
||||
def _initialize_memory(self):
|
||||
api_key = self.config.get("api_key") or os.getenv("MEM0_API_KEY")
|
||||
@@ -70,26 +63,32 @@ class Mem0Storage(Storage):
|
||||
"""
|
||||
Returns:
|
||||
dict: A filter dictionary containing AND conditions for querying data.
|
||||
- Includes user_id if memory_type is 'external'.
|
||||
- Includes user_id and agent_id if both are present.
|
||||
- Includes user_id if only user_id is present.
|
||||
- Includes agent_id if only agent_id is present.
|
||||
- Includes run_id if memory_type is 'short_term' and mem0_run_id is present.
|
||||
"""
|
||||
filter = {
|
||||
"AND": []
|
||||
}
|
||||
filter = defaultdict(list)
|
||||
|
||||
# Add user_id condition if the memory type is external
|
||||
if self.memory_type == "external":
|
||||
filter["AND"].append({"user_id": self.config.get("user_id", "")})
|
||||
|
||||
# Add run_id condition if the memory type is short_term and a run ID is set
|
||||
if self.memory_type == "short_term" and self.mem0_run_id:
|
||||
filter["AND"].append({"run_id": self.mem0_run_id})
|
||||
else:
|
||||
user_id = self.config.get("user_id", "")
|
||||
agent_id = self.config.get("agent_id", "")
|
||||
|
||||
if user_id and agent_id:
|
||||
filter["OR"].append({"user_id": user_id})
|
||||
filter["OR"].append({"agent_id": agent_id})
|
||||
elif user_id:
|
||||
filter["AND"].append({"user_id": user_id})
|
||||
elif agent_id:
|
||||
filter["AND"].append({"agent_id": agent_id})
|
||||
|
||||
return filter
|
||||
|
||||
def save(self, value: Any, metadata: Dict[str, Any]) -> None:
|
||||
user_id = self.config.get("user_id", "")
|
||||
assistant_message = [{"role" : "assistant","content" : value}]
|
||||
assistant_message = [{"role" : "assistant","content" : value}]
|
||||
|
||||
base_metadata = {
|
||||
"short_term": "short_term",
|
||||
@@ -104,31 +103,32 @@ class Mem0Storage(Storage):
|
||||
"infer": self.infer
|
||||
}
|
||||
|
||||
if self.memory_type == "external":
|
||||
# MemoryClient-specific overrides
|
||||
if isinstance(self.memory, MemoryClient):
|
||||
params["includes"] = self.includes
|
||||
params["excludes"] = self.excludes
|
||||
params["output_format"] = "v1.1"
|
||||
params["version"] = "v2"
|
||||
|
||||
if self.memory_type == "short_term" and self.mem0_run_id:
|
||||
params["run_id"] = self.mem0_run_id
|
||||
|
||||
if user_id:
|
||||
params["user_id"] = user_id
|
||||
|
||||
|
||||
if params:
|
||||
# MemoryClient-specific overrides
|
||||
if isinstance(self.memory, MemoryClient):
|
||||
params["includes"] = self.includes
|
||||
params["excludes"] = self.excludes
|
||||
params["output_format"] = "v1.1"
|
||||
params["version"]="v2"
|
||||
if agent_id := self.config.get("agent_id", self._get_agent_name()):
|
||||
params["agent_id"] = agent_id
|
||||
|
||||
if self.memory_type == "short_term":
|
||||
params["run_id"] = self.mem0_run_id
|
||||
|
||||
self.memory.add(assistant_message, **params)
|
||||
self.memory.add(assistant_message, **params)
|
||||
|
||||
def search(self,query: str,limit: int = 3,score_threshold: float = 0.35) -> List[Any]:
|
||||
params = {
|
||||
"query": query,
|
||||
"limit": limit,
|
||||
"query": query,
|
||||
"limit": limit,
|
||||
"version": "v2",
|
||||
"output_format": "v1.1"
|
||||
}
|
||||
|
||||
|
||||
if user_id := self.config.get("user_id", ""):
|
||||
params["user_id"] = user_id
|
||||
|
||||
@@ -138,7 +138,7 @@ class Mem0Storage(Storage):
|
||||
"entities": {"type": "entity"},
|
||||
"external": {"type": "external"},
|
||||
}
|
||||
|
||||
|
||||
if self.memory_type in memory_type_map:
|
||||
params["metadata"] = memory_type_map[self.memory_type]
|
||||
if self.memory_type == "short_term":
|
||||
@@ -151,11 +151,33 @@ class Mem0Storage(Storage):
|
||||
params['threshold'] = score_threshold
|
||||
|
||||
if isinstance(self.memory, Memory):
|
||||
del params["metadata"], params["version"], params["run_id"], params['output_format']
|
||||
del params["metadata"], params["version"], params['output_format']
|
||||
if params.get("run_id"):
|
||||
del params["run_id"]
|
||||
|
||||
results = self.memory.search(**params)
|
||||
|
||||
# This makes it compatible for Contextual Memory to retrieve
|
||||
for result in results["results"]:
|
||||
result["context"] = result["memory"]
|
||||
|
||||
return [r for r in results["results"]]
|
||||
|
||||
|
||||
def reset(self):
|
||||
if self.memory:
|
||||
self.memory.reset()
|
||||
|
||||
def _sanitize_role(self, role: str) -> str:
|
||||
"""
|
||||
Sanitizes agent roles to ensure valid directory names.
|
||||
"""
|
||||
return role.replace("\n", "").replace(" ", "_").replace("/", "_")
|
||||
|
||||
def _get_agent_name(self) -> str:
|
||||
if not self.crew:
|
||||
return ""
|
||||
|
||||
agents = self.crew.agents
|
||||
agents = [self._sanitize_role(agent.role) for agent in agents]
|
||||
agents = "_".join(agents)
|
||||
return sanitize_collection_name(name=agents, max_collection_length=MAX_AGENT_ID_LENGTH_MEM0)
|
||||
|
||||
@@ -7,8 +7,8 @@ import uuid
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from chromadb.api import ClientAPI
|
||||
from crewai.memory.storage.base_rag_storage import BaseRAGStorage
|
||||
from crewai.utilities import EmbeddingConfigurator
|
||||
from crewai.rag.storage.base_rag_storage import BaseRAGStorage
|
||||
from crewai.rag.embeddings.configurator import EmbeddingConfigurator
|
||||
from crewai.utilities.chromadb import create_persistent_client
|
||||
from crewai.utilities.constants import MAX_FILE_NAME_LENGTH
|
||||
from crewai.utilities.paths import db_storage_path
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import warnings
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from crewai.memory.memory import Memory
|
||||
|
||||
|
||||
class UserMemory(Memory):
|
||||
"""
|
||||
UserMemory class for handling user memory storage and retrieval.
|
||||
Inherits from the Memory class and utilizes an instance of a class that
|
||||
adheres to the Storage for data storage, specifically working with
|
||||
MemoryItem instances.
|
||||
"""
|
||||
|
||||
def __init__(self, crew=None):
|
||||
warnings.warn(
|
||||
"UserMemory is deprecated and will be removed in version 0.156.0 "
|
||||
"or on 2025-08-04, whichever comes first. "
|
||||
"Please use ExternalMemory instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
try:
|
||||
from crewai.memory.storage.mem0_storage import Mem0Storage
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Mem0 is not installed. Please install it with `pip install mem0ai`."
|
||||
)
|
||||
storage = Mem0Storage(type="user", crew=crew)
|
||||
super().__init__(storage)
|
||||
|
||||
def save(
|
||||
self,
|
||||
value,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
agent: Optional[str] = None,
|
||||
) -> None:
|
||||
# TODO: Change this function since we want to take care of the case where we save memories for the usr
|
||||
data = f"Remember the details about the user: {value}"
|
||||
super().save(data, metadata)
|
||||
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
limit: int = 3,
|
||||
score_threshold: float = 0.35,
|
||||
):
|
||||
results = self.storage.search(
|
||||
query=query,
|
||||
limit=limit,
|
||||
score_threshold=score_threshold,
|
||||
)
|
||||
return results
|
||||
|
||||
def reset(self) -> None:
|
||||
try:
|
||||
self.storage.reset()
|
||||
except Exception as e:
|
||||
raise Exception(f"An error occurred while resetting the user memory: {e}")
|
||||
@@ -1,16 +0,0 @@
|
||||
import warnings
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class UserMemoryItem:
|
||||
def __init__(self, data: Any, user: str, metadata: Optional[Dict[str, Any]] = None):
|
||||
warnings.warn(
|
||||
"UserMemoryItem is deprecated and will be removed in version 0.156.0 "
|
||||
"or on 2025-08-04, whichever comes first. "
|
||||
"Please use ExternalMemory instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.data = data
|
||||
self.user = user
|
||||
self.metadata = metadata if metadata is not None else {}
|
||||
1
src/crewai/rag/__init__.py
Normal file
1
src/crewai/rag/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""RAG (Retrieval-Augmented Generation) infrastructure for CrewAI."""
|
||||
1
src/crewai/rag/embeddings/__init__.py
Normal file
1
src/crewai/rag/embeddings/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Embedding components for RAG infrastructure."""
|
||||
@@ -38,7 +38,14 @@ class EmbeddingConfigurator:
|
||||
f"Unsupported embedding provider: {provider}, supported providers: {list(self.embedding_functions.keys())}"
|
||||
)
|
||||
|
||||
embedding_function = self.embedding_functions[provider]
|
||||
try:
|
||||
embedding_function = self.embedding_functions[provider]
|
||||
except ImportError as e:
|
||||
missing_package = str(e).split()[-1]
|
||||
raise ImportError(
|
||||
f"{missing_package} is not installed. Please install it with: pip install {missing_package}"
|
||||
)
|
||||
|
||||
return (
|
||||
embedding_function(config)
|
||||
if provider == "custom"
|
||||
1
src/crewai/rag/storage/__init__.py
Normal file
1
src/crewai/rag/storage/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Storage components for RAG infrastructure."""
|
||||
@@ -10,7 +10,6 @@ from .rpm_controller import RPMController
|
||||
from .exceptions.context_window_exceeding_exception import (
|
||||
LLMContextLengthExceededException,
|
||||
)
|
||||
from .embedding_configurator import EmbeddingConfigurator
|
||||
|
||||
__all__ = [
|
||||
"Converter",
|
||||
@@ -24,5 +23,4 @@ __all__ = [
|
||||
"RPMController",
|
||||
"YamlParser",
|
||||
"LLMContextLengthExceededException",
|
||||
"EmbeddingConfigurator",
|
||||
]
|
||||
|
||||
@@ -400,7 +400,7 @@ def show_agent_logs(
|
||||
if not verbose:
|
||||
return
|
||||
|
||||
agent_role = agent_role.split("\n")[0]
|
||||
agent_role = agent_role.partition("\n")[0]
|
||||
|
||||
if formatted_answer is None:
|
||||
# Start logs
|
||||
|
||||
@@ -16,3 +16,4 @@ class _NotSpecified:
|
||||
# Unlike `None`, which might be a valid value from the user, `NOT_SPECIFIED` allows
|
||||
# us to distinguish between "not passed at all" and "explicitly passed None" or "[]".
|
||||
NOT_SPECIFIED = _NotSpecified()
|
||||
CREWAI_BASE_URL = "https://app.crewai.com/"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from crewai.utilities.serialization import to_serializable
|
||||
@@ -9,7 +8,7 @@ from crewai.utilities.serialization import to_serializable
|
||||
class BaseEvent(BaseModel):
|
||||
"""Base class for all events"""
|
||||
|
||||
timestamp: datetime = Field(default_factory=datetime.now)
|
||||
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
type: str
|
||||
source_fingerprint: Optional[str] = None # UUID string of the source entity
|
||||
source_type: Optional[str] = None # "agent", "task", "crew", "memory", "entity_memory", "short_term_memory", "long_term_memory", "external_memory"
|
||||
|
||||
@@ -47,6 +47,7 @@ class CrewKickoffCompletedEvent(CrewBaseEvent):
|
||||
|
||||
output: Any
|
||||
type: str = "crew_kickoff_completed"
|
||||
total_tokens: int = 0
|
||||
|
||||
|
||||
class CrewKickoffFailedEvent(CrewBaseEvent):
|
||||
|
||||
33
src/crewai/utilities/events/listeners/tracing/interfaces.py
Normal file
33
src/crewai/utilities/events/listeners/tracing/interfaces.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from crewai.cli.plus_api import PlusAPI
|
||||
from crewai.cli.authentication.token import get_auth_token
|
||||
from pydantic import BaseModel
|
||||
from .trace_batch_manager import TraceBatch
|
||||
from logging import getLogger
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class TraceSender(BaseModel):
|
||||
"""Trace sender for sending trace batches to the backend"""
|
||||
|
||||
def send_batch(self, batch: TraceBatch) -> bool:
|
||||
"""Print trace batch to console"""
|
||||
try:
|
||||
payload = batch.to_dict()
|
||||
|
||||
def datetime_handler(obj):
|
||||
if isinstance(obj, datetime):
|
||||
return obj.isoformat()
|
||||
|
||||
serialized_payload = json.loads(
|
||||
json.dumps(payload, default=datetime_handler)
|
||||
)
|
||||
|
||||
PlusAPI(api_key=get_auth_token()).send_trace_batch(serialized_payload)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending trace batch: {e}")
|
||||
return False
|
||||
@@ -0,0 +1,252 @@
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Any, Optional
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from crewai.utilities.constants import CREWAI_BASE_URL
|
||||
from crewai.cli.authentication.token import get_auth_token
|
||||
|
||||
from crewai.cli.version import get_crewai_version
|
||||
from crewai.cli.plus_api import PlusAPI
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
from crewai.utilities.events.listeners.tracing.types import TraceEvent
|
||||
from logging import getLogger
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TraceBatch:
|
||||
"""Batch of events to send to backend"""
|
||||
|
||||
version: str = field(default_factory=get_crewai_version)
|
||||
batch_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
user_context: Dict[str, str] = field(default_factory=dict)
|
||||
execution_metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
events: List[TraceEvent] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"version": self.version,
|
||||
"batch_id": self.batch_id,
|
||||
"user_context": self.user_context,
|
||||
"execution_metadata": self.execution_metadata,
|
||||
"events": [event.to_dict() for event in self.events],
|
||||
}
|
||||
|
||||
|
||||
class TraceBatchManager:
|
||||
"""Single responsibility: Manage batches and event buffering"""
|
||||
|
||||
def __init__(self):
|
||||
self.plus_api = PlusAPI(api_key=get_auth_token())
|
||||
self.trace_batch_id: Optional[str] = None # Backend ID
|
||||
self.current_batch: Optional[TraceBatch] = None
|
||||
self.event_buffer: List[TraceEvent] = []
|
||||
self.execution_start_times: Dict[str, datetime] = {}
|
||||
|
||||
def initialize_batch(
|
||||
self, user_context: Dict[str, str], execution_metadata: Dict[str, Any]
|
||||
) -> TraceBatch:
|
||||
"""Initialize a new trace batch"""
|
||||
self.current_batch = TraceBatch(
|
||||
user_context=user_context, execution_metadata=execution_metadata
|
||||
)
|
||||
self.event_buffer.clear()
|
||||
|
||||
self.record_start_time("execution")
|
||||
|
||||
self._initialize_backend_batch(user_context, execution_metadata)
|
||||
|
||||
return self.current_batch
|
||||
|
||||
def _initialize_backend_batch(
|
||||
self, user_context: Dict[str, str], execution_metadata: Dict[str, Any]
|
||||
):
|
||||
"""Send batch initialization to backend"""
|
||||
if not self.plus_api or not self.current_batch:
|
||||
return
|
||||
|
||||
try:
|
||||
payload = {
|
||||
"trace_id": self.current_batch.batch_id,
|
||||
"execution_type": execution_metadata.get("execution_type", "crew"),
|
||||
"execution_context": {
|
||||
"crew_fingerprint": execution_metadata.get("crew_fingerprint"),
|
||||
"crew_name": execution_metadata.get("crew_name", "Unknown Crew"),
|
||||
"flow_name": execution_metadata.get("flow_name", "Unknown Flow"),
|
||||
"crewai_version": self.current_batch.version,
|
||||
"privacy_level": user_context.get("privacy_level", "standard"),
|
||||
},
|
||||
"execution_metadata": {
|
||||
"expected_duration_estimate": execution_metadata.get(
|
||||
"expected_duration_estimate", 300
|
||||
),
|
||||
"agent_count": execution_metadata.get("agent_count", 0),
|
||||
"task_count": execution_metadata.get("task_count", 0),
|
||||
"flow_method_count": execution_metadata.get("flow_method_count", 0),
|
||||
"execution_started_at": datetime.now(timezone.utc).isoformat(),
|
||||
},
|
||||
}
|
||||
|
||||
response = self.plus_api.initialize_trace_batch(payload)
|
||||
|
||||
if response.status_code == 201 or response.status_code == 200:
|
||||
response_data = response.json()
|
||||
self.trace_batch_id = response_data["trace_id"]
|
||||
console = Console()
|
||||
panel = Panel(
|
||||
f"✅ Trace batch initialized with session ID: {self.trace_batch_id}",
|
||||
title="Trace Batch Initialization",
|
||||
border_style="green",
|
||||
)
|
||||
console.print(panel)
|
||||
else:
|
||||
logger.error(
|
||||
f"❌ Failed to initialize trace batch: {response.status_code} - {response.text}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error initializing trace batch: {str(e)}")
|
||||
|
||||
def add_event(self, trace_event: TraceEvent):
|
||||
"""Add event to buffer"""
|
||||
self.event_buffer.append(trace_event)
|
||||
|
||||
def _send_events_to_backend(self):
|
||||
"""Send buffered events to backend"""
|
||||
if not self.plus_api or not self.trace_batch_id or not self.event_buffer:
|
||||
return
|
||||
|
||||
try:
|
||||
payload = {
|
||||
"events": [event.to_dict() for event in self.event_buffer],
|
||||
"batch_metadata": {
|
||||
"events_count": len(self.event_buffer),
|
||||
"batch_sequence": 1,
|
||||
"is_final_batch": False,
|
||||
},
|
||||
}
|
||||
|
||||
if not self.trace_batch_id:
|
||||
raise Exception("❌ Trace batch ID not found")
|
||||
|
||||
response = self.plus_api.send_trace_events(self.trace_batch_id, payload)
|
||||
|
||||
if response.status_code == 200 or response.status_code == 201:
|
||||
self.event_buffer.clear()
|
||||
else:
|
||||
logger.error(
|
||||
f"❌ Failed to send events: {response.status_code} - {response.text}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error sending events to backend: {str(e)}")
|
||||
|
||||
def finalize_batch(self) -> Optional[TraceBatch]:
|
||||
"""Finalize batch and return it for sending"""
|
||||
if not self.current_batch:
|
||||
return None
|
||||
|
||||
if self.event_buffer:
|
||||
self._send_events_to_backend()
|
||||
|
||||
self._finalize_backend_batch()
|
||||
|
||||
self.current_batch.events = self.event_buffer.copy()
|
||||
|
||||
finalized_batch = self.current_batch
|
||||
|
||||
self.current_batch = None
|
||||
self.event_buffer.clear()
|
||||
self.trace_batch_id = None
|
||||
|
||||
self._cleanup_batch_data()
|
||||
|
||||
return finalized_batch
|
||||
|
||||
def _finalize_backend_batch(self):
|
||||
"""Send batch finalization to backend"""
|
||||
if not self.plus_api or not self.trace_batch_id:
|
||||
return
|
||||
|
||||
try:
|
||||
total_events = len(self.current_batch.events) if self.current_batch else 0
|
||||
|
||||
payload = {
|
||||
"status": "completed",
|
||||
"duration_ms": self.calculate_duration("execution"),
|
||||
"final_event_count": total_events,
|
||||
}
|
||||
|
||||
response = self.plus_api.finalize_trace_batch(self.trace_batch_id, payload)
|
||||
|
||||
if response.status_code == 200:
|
||||
console = Console()
|
||||
panel = Panel(
|
||||
f"✅ Trace batch finalized with session ID: {self.trace_batch_id}. View here: {CREWAI_BASE_URL}/crewai_plus/trace_batches/{self.trace_batch_id}",
|
||||
title="Trace Batch Finalization",
|
||||
border_style="green",
|
||||
)
|
||||
console.print(panel)
|
||||
|
||||
else:
|
||||
logger.error(
|
||||
f"❌ Failed to finalize trace batch: {response.status_code} - {response.text}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error finalizing trace batch: {str(e)}")
|
||||
# TODO: send error to app
|
||||
|
||||
def _cleanup_batch_data(self):
|
||||
"""Clean up batch data after successful finalization to free memory"""
|
||||
try:
|
||||
if hasattr(self, "event_buffer") and self.event_buffer:
|
||||
self.event_buffer.clear()
|
||||
|
||||
if hasattr(self, "current_batch") and self.current_batch:
|
||||
if hasattr(self.current_batch, "events") and self.current_batch.events:
|
||||
self.current_batch.events.clear()
|
||||
self.current_batch = None
|
||||
|
||||
if hasattr(self, "batch_sequence"):
|
||||
self.batch_sequence = 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Warning: Error during cleanup: {str(e)}")
|
||||
|
||||
def has_events(self) -> bool:
|
||||
"""Check if there are events in the buffer"""
|
||||
return len(self.event_buffer) > 0
|
||||
|
||||
def get_event_count(self) -> int:
|
||||
"""Get number of events in buffer"""
|
||||
return len(self.event_buffer)
|
||||
|
||||
def is_batch_initialized(self) -> bool:
|
||||
"""Check if batch is initialized"""
|
||||
return self.current_batch is not None
|
||||
|
||||
def record_start_time(self, key: str):
|
||||
"""Record start time for duration calculation"""
|
||||
self.execution_start_times[key] = datetime.now(timezone.utc)
|
||||
|
||||
def calculate_duration(self, key: str) -> int:
|
||||
"""Calculate duration in milliseconds from recorded start time"""
|
||||
start_time = self.execution_start_times.get(key)
|
||||
if start_time:
|
||||
duration_ms = int(
|
||||
(datetime.now(timezone.utc) - start_time).total_seconds() * 1000
|
||||
)
|
||||
del self.execution_start_times[key]
|
||||
return duration_ms
|
||||
return 0
|
||||
|
||||
def get_trace_id(self) -> Optional[str]:
|
||||
"""Get current trace ID"""
|
||||
if self.current_batch:
|
||||
return self.current_batch.user_context.get("trace_id")
|
||||
return None
|
||||
414
src/crewai/utilities/events/listeners/tracing/trace_listener.py
Normal file
414
src/crewai/utilities/events/listeners/tracing/trace_listener.py
Normal file
@@ -0,0 +1,414 @@
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from crewai.utilities.events.base_event_listener import BaseEventListener
|
||||
from crewai.utilities.events.agent_events import (
|
||||
AgentExecutionCompletedEvent,
|
||||
AgentExecutionStartedEvent,
|
||||
LiteAgentExecutionStartedEvent,
|
||||
LiteAgentExecutionCompletedEvent,
|
||||
LiteAgentExecutionErrorEvent,
|
||||
AgentExecutionErrorEvent,
|
||||
)
|
||||
from crewai.utilities.events.listeners.tracing.types import TraceEvent
|
||||
from crewai.utilities.events.reasoning_events import (
|
||||
AgentReasoningStartedEvent,
|
||||
AgentReasoningCompletedEvent,
|
||||
AgentReasoningFailedEvent,
|
||||
)
|
||||
from crewai.utilities.events.crew_events import (
|
||||
CrewKickoffCompletedEvent,
|
||||
CrewKickoffFailedEvent,
|
||||
CrewKickoffStartedEvent,
|
||||
)
|
||||
from crewai.utilities.events.task_events import (
|
||||
TaskCompletedEvent,
|
||||
TaskFailedEvent,
|
||||
TaskStartedEvent,
|
||||
)
|
||||
from crewai.utilities.events.tool_usage_events import (
|
||||
ToolUsageErrorEvent,
|
||||
ToolUsageFinishedEvent,
|
||||
ToolUsageStartedEvent,
|
||||
)
|
||||
from crewai.utilities.events.llm_events import (
|
||||
LLMCallCompletedEvent,
|
||||
LLMCallFailedEvent,
|
||||
LLMCallStartedEvent,
|
||||
)
|
||||
|
||||
from crewai.utilities.events.flow_events import (
|
||||
FlowCreatedEvent,
|
||||
FlowStartedEvent,
|
||||
FlowFinishedEvent,
|
||||
MethodExecutionStartedEvent,
|
||||
MethodExecutionFinishedEvent,
|
||||
MethodExecutionFailedEvent,
|
||||
FlowPlotEvent,
|
||||
)
|
||||
from crewai.utilities.events.llm_guardrail_events import (
|
||||
LLMGuardrailStartedEvent,
|
||||
LLMGuardrailCompletedEvent,
|
||||
)
|
||||
from crewai.utilities.serialization import to_serializable
|
||||
|
||||
|
||||
from .trace_batch_manager import TraceBatchManager
|
||||
|
||||
from crewai.utilities.events.memory_events import (
|
||||
MemoryQueryStartedEvent,
|
||||
MemoryQueryCompletedEvent,
|
||||
MemoryQueryFailedEvent,
|
||||
MemorySaveStartedEvent,
|
||||
MemorySaveCompletedEvent,
|
||||
MemorySaveFailedEvent,
|
||||
)
|
||||
from .interfaces import TraceSender
|
||||
from crewai.cli.authentication.token import get_auth_token
|
||||
from crewai.cli.version import get_crewai_version
|
||||
|
||||
|
||||
class TraceCollectionListener(BaseEventListener):
|
||||
"""
|
||||
Trace collection listener that orchestrates trace collection
|
||||
"""
|
||||
|
||||
trace_enabled: bool = False
|
||||
complex_events = ["task_started", "llm_call_started", "llm_call_completed"]
|
||||
|
||||
_instance = None
|
||||
_initialized = False
|
||||
|
||||
def __new__(cls, batch_manager=None, trace_sender=None):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
batch_manager: Optional[TraceBatchManager] = None,
|
||||
trace_sender: Optional[TraceSender] = None,
|
||||
):
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
super().__init__()
|
||||
self.batch_manager = batch_manager or TraceBatchManager()
|
||||
self.trace_sender = trace_sender or TraceSender()
|
||||
self.trace_enabled = self._check_trace_enabled()
|
||||
self._initialized = True
|
||||
|
||||
def _check_trace_enabled(self) -> bool:
|
||||
"""Check if tracing should be enabled"""
|
||||
auth_token = get_auth_token()
|
||||
if not auth_token:
|
||||
return False
|
||||
|
||||
return os.getenv("CREWAI_TRACING_ENABLED", "false").lower() == "true" or bool(
|
||||
os.getenv("CREWAI_USER_TOKEN")
|
||||
)
|
||||
|
||||
def _get_user_context(self) -> Dict[str, str]:
|
||||
"""Extract user context for tracing"""
|
||||
return {
|
||||
"user_id": os.getenv("CREWAI_USER_ID", "anonymous"),
|
||||
"organization_id": os.getenv("CREWAI_ORG_ID", ""),
|
||||
"session_id": str(uuid.uuid4()),
|
||||
"trace_id": str(uuid.uuid4()),
|
||||
}
|
||||
|
||||
def setup_listeners(self, crewai_event_bus):
|
||||
"""Setup event listeners - delegates to specific handlers"""
|
||||
if not self.trace_enabled:
|
||||
return
|
||||
|
||||
self._register_flow_event_handlers(crewai_event_bus)
|
||||
self._register_context_event_handlers(crewai_event_bus)
|
||||
self._register_action_event_handlers(crewai_event_bus)
|
||||
|
||||
def _register_flow_event_handlers(self, event_bus):
|
||||
"""Register handlers for flow events"""
|
||||
|
||||
@event_bus.on(FlowCreatedEvent)
|
||||
def on_flow_created(source, event):
|
||||
pass
|
||||
|
||||
@event_bus.on(FlowStartedEvent)
|
||||
def on_flow_started(source, event):
|
||||
if not self.batch_manager.is_batch_initialized():
|
||||
self._initialize_flow_batch(source, event)
|
||||
self._handle_trace_event("flow_started", source, event)
|
||||
|
||||
@event_bus.on(MethodExecutionStartedEvent)
|
||||
def on_method_started(source, event):
|
||||
self._handle_trace_event("method_execution_started", source, event)
|
||||
|
||||
@event_bus.on(MethodExecutionFinishedEvent)
|
||||
def on_method_finished(source, event):
|
||||
self._handle_trace_event("method_execution_finished", source, event)
|
||||
|
||||
@event_bus.on(MethodExecutionFailedEvent)
|
||||
def on_method_failed(source, event):
|
||||
self._handle_trace_event("method_execution_failed", source, event)
|
||||
|
||||
@event_bus.on(FlowFinishedEvent)
|
||||
def on_flow_finished(source, event):
|
||||
self._handle_trace_event("flow_finished", source, event)
|
||||
self._send_batch()
|
||||
|
||||
@event_bus.on(FlowPlotEvent)
|
||||
def on_flow_plot(source, event):
|
||||
self._handle_action_event("flow_plot", source, event)
|
||||
|
||||
def _register_context_event_handlers(self, event_bus):
|
||||
"""Register handlers for context events (start/end)"""
|
||||
|
||||
@event_bus.on(CrewKickoffStartedEvent)
|
||||
def on_crew_started(source, event):
|
||||
if not self.batch_manager.is_batch_initialized():
|
||||
self._initialize_batch(source, event)
|
||||
self._handle_trace_event("crew_kickoff_started", source, event)
|
||||
|
||||
@event_bus.on(CrewKickoffCompletedEvent)
|
||||
def on_crew_completed(source, event):
|
||||
self._handle_trace_event("crew_kickoff_completed", source, event)
|
||||
self._send_batch()
|
||||
|
||||
@event_bus.on(CrewKickoffFailedEvent)
|
||||
def on_crew_failed(source, event):
|
||||
self._handle_trace_event("crew_kickoff_failed", source, event)
|
||||
self._send_batch()
|
||||
|
||||
@event_bus.on(TaskStartedEvent)
|
||||
def on_task_started(source, event):
|
||||
self._handle_trace_event("task_started", source, event)
|
||||
|
||||
@event_bus.on(TaskCompletedEvent)
|
||||
def on_task_completed(source, event):
|
||||
self._handle_trace_event("task_completed", source, event)
|
||||
|
||||
@event_bus.on(TaskFailedEvent)
|
||||
def on_task_failed(source, event):
|
||||
self._handle_trace_event("task_failed", source, event)
|
||||
|
||||
@event_bus.on(AgentExecutionStartedEvent)
|
||||
def on_agent_started(source, event):
|
||||
self._handle_trace_event("agent_execution_started", source, event)
|
||||
|
||||
@event_bus.on(AgentExecutionCompletedEvent)
|
||||
def on_agent_completed(source, event):
|
||||
self._handle_trace_event("agent_execution_completed", source, event)
|
||||
|
||||
@event_bus.on(LiteAgentExecutionStartedEvent)
|
||||
def on_lite_agent_started(source, event):
|
||||
self._handle_trace_event("lite_agent_execution_started", source, event)
|
||||
|
||||
@event_bus.on(LiteAgentExecutionCompletedEvent)
|
||||
def on_lite_agent_completed(source, event):
|
||||
self._handle_trace_event("lite_agent_execution_completed", source, event)
|
||||
|
||||
@event_bus.on(LiteAgentExecutionErrorEvent)
|
||||
def on_lite_agent_error(source, event):
|
||||
self._handle_trace_event("lite_agent_execution_error", source, event)
|
||||
|
||||
@event_bus.on(AgentExecutionErrorEvent)
|
||||
def on_agent_error(source, event):
|
||||
self._handle_trace_event("agent_execution_error", source, event)
|
||||
|
||||
@event_bus.on(LLMGuardrailStartedEvent)
|
||||
def on_guardrail_started(source, event):
|
||||
self._handle_trace_event("llm_guardrail_started", source, event)
|
||||
|
||||
@event_bus.on(LLMGuardrailCompletedEvent)
|
||||
def on_guardrail_completed(source, event):
|
||||
self._handle_trace_event("llm_guardrail_completed", source, event)
|
||||
|
||||
def _register_action_event_handlers(self, event_bus):
|
||||
"""Register handlers for action events (LLM calls, tool usage, memory)"""
|
||||
|
||||
@event_bus.on(LLMCallStartedEvent)
|
||||
def on_llm_call_started(source, event):
|
||||
self._handle_action_event("llm_call_started", source, event)
|
||||
|
||||
@event_bus.on(LLMCallCompletedEvent)
|
||||
def on_llm_call_completed(source, event):
|
||||
self._handle_action_event("llm_call_completed", source, event)
|
||||
|
||||
@event_bus.on(LLMCallFailedEvent)
|
||||
def on_llm_call_failed(source, event):
|
||||
self._handle_action_event("llm_call_failed", source, event)
|
||||
|
||||
@event_bus.on(ToolUsageStartedEvent)
|
||||
def on_tool_started(source, event):
|
||||
self._handle_action_event("tool_usage_started", source, event)
|
||||
|
||||
@event_bus.on(ToolUsageFinishedEvent)
|
||||
def on_tool_finished(source, event):
|
||||
self._handle_action_event("tool_usage_finished", source, event)
|
||||
|
||||
@event_bus.on(ToolUsageErrorEvent)
|
||||
def on_tool_error(source, event):
|
||||
self._handle_action_event("tool_usage_error", source, event)
|
||||
|
||||
@event_bus.on(MemoryQueryStartedEvent)
|
||||
def on_memory_query_started(source, event):
|
||||
self._handle_action_event("memory_query_started", source, event)
|
||||
|
||||
@event_bus.on(MemoryQueryCompletedEvent)
|
||||
def on_memory_query_completed(source, event):
|
||||
self._handle_action_event("memory_query_completed", source, event)
|
||||
|
||||
@event_bus.on(MemoryQueryFailedEvent)
|
||||
def on_memory_query_failed(source, event):
|
||||
self._handle_action_event("memory_query_failed", source, event)
|
||||
|
||||
@event_bus.on(MemorySaveStartedEvent)
|
||||
def on_memory_save_started(source, event):
|
||||
self._handle_action_event("memory_save_started", source, event)
|
||||
|
||||
@event_bus.on(MemorySaveCompletedEvent)
|
||||
def on_memory_save_completed(source, event):
|
||||
self._handle_action_event("memory_save_completed", source, event)
|
||||
|
||||
@event_bus.on(MemorySaveFailedEvent)
|
||||
def on_memory_save_failed(source, event):
|
||||
self._handle_action_event("memory_save_failed", source, event)
|
||||
|
||||
@event_bus.on(AgentReasoningStartedEvent)
|
||||
def on_agent_reasoning_started(source, event):
|
||||
self._handle_action_event("agent_reasoning_started", source, event)
|
||||
|
||||
@event_bus.on(AgentReasoningCompletedEvent)
|
||||
def on_agent_reasoning_completed(source, event):
|
||||
self._handle_action_event("agent_reasoning_completed", source, event)
|
||||
|
||||
@event_bus.on(AgentReasoningFailedEvent)
|
||||
def on_agent_reasoning_failed(source, event):
|
||||
self._handle_action_event("agent_reasoning_failed", source, event)
|
||||
|
||||
def _initialize_batch(self, source: Any, event: Any):
|
||||
"""Initialize trace batch"""
|
||||
user_context = self._get_user_context()
|
||||
execution_metadata = {
|
||||
"crew_name": getattr(event, "crew_name", "Unknown Crew"),
|
||||
"execution_start": event.timestamp if hasattr(event, "timestamp") else None,
|
||||
"crewai_version": get_crewai_version(),
|
||||
}
|
||||
|
||||
self.batch_manager.initialize_batch(user_context, execution_metadata)
|
||||
|
||||
def _initialize_flow_batch(self, source: Any, event: Any):
|
||||
"""Initialize trace batch for Flow execution"""
|
||||
user_context = self._get_user_context()
|
||||
execution_metadata = {
|
||||
"flow_name": getattr(source, "__class__.__name__", "Unknown Flow"),
|
||||
"execution_start": event.timestamp if hasattr(event, "timestamp") else None,
|
||||
"crewai_version": get_crewai_version(),
|
||||
"execution_type": "flow",
|
||||
}
|
||||
|
||||
self.batch_manager.initialize_batch(user_context, execution_metadata)
|
||||
|
||||
def _handle_trace_event(self, event_type: str, source: Any, event: Any):
|
||||
"""Generic handler for context end events"""
|
||||
|
||||
trace_event = self._create_trace_event(event_type, source, event)
|
||||
|
||||
self.batch_manager.add_event(trace_event)
|
||||
|
||||
def _handle_action_event(self, event_type: str, source: Any, event: Any):
|
||||
"""Generic handler for action events (LLM calls, tool usage)"""
|
||||
|
||||
if not self.batch_manager.is_batch_initialized():
|
||||
user_context = self._get_user_context()
|
||||
execution_metadata = {
|
||||
"crew_name": getattr(source, "name", "Unknown Crew"),
|
||||
"crewai_version": get_crewai_version(),
|
||||
}
|
||||
self.batch_manager.initialize_batch(user_context, execution_metadata)
|
||||
|
||||
trace_event = self._create_trace_event(event_type, source, event)
|
||||
self.batch_manager.add_event(trace_event)
|
||||
|
||||
def _send_batch(self):
|
||||
"""Send finalized batch using the configured sender"""
|
||||
batch = self.batch_manager.finalize_batch()
|
||||
if batch:
|
||||
success = self.trace_sender.send_batch(batch)
|
||||
if not success:
|
||||
print("⚠️ Failed to send trace batch")
|
||||
|
||||
def _create_trace_event(
|
||||
self, event_type: str, source: Any, event: Any
|
||||
) -> TraceEvent:
|
||||
"""Create a trace event"""
|
||||
trace_event = TraceEvent(
|
||||
type=event_type,
|
||||
)
|
||||
|
||||
trace_event.event_data = self._build_event_data(event_type, event, source)
|
||||
return trace_event
|
||||
|
||||
def _build_event_data(
|
||||
self, event_type: str, event: Any, source: Any
|
||||
) -> Dict[str, Any]:
|
||||
"""Build event data"""
|
||||
if event_type not in self.complex_events:
|
||||
return self._safe_serialize_to_dict(event)
|
||||
elif event_type == "task_started":
|
||||
return {
|
||||
"task_description": event.task.description,
|
||||
"task_name": event.task.name,
|
||||
"context": event.context,
|
||||
"agent": source.agent.role,
|
||||
}
|
||||
elif event_type == "llm_call_started":
|
||||
return {
|
||||
**self._safe_serialize_to_dict(event),
|
||||
"messages": self._truncate_messages(event.messages),
|
||||
}
|
||||
elif event_type == "llm_call_completed":
|
||||
return {
|
||||
**self._safe_serialize_to_dict(event),
|
||||
"messages": self._truncate_messages(event.messages),
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"event_type": event_type,
|
||||
"event": self._safe_serialize_to_dict(event),
|
||||
"source": source,
|
||||
}
|
||||
|
||||
# TODO: move to utils
|
||||
def _safe_serialize_to_dict(
|
||||
self, obj, exclude: set[str] | None = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Safely serialize an object to a dictionary for event data."""
|
||||
try:
|
||||
serialized = to_serializable(obj, exclude)
|
||||
if isinstance(serialized, dict):
|
||||
return serialized
|
||||
else:
|
||||
return {"serialized_data": serialized}
|
||||
except Exception as e:
|
||||
return {"serialization_error": str(e), "object_type": type(obj).__name__}
|
||||
|
||||
# TODO: move to utils
|
||||
def _truncate_messages(self, messages, max_content_length=200, max_messages=5):
|
||||
"""Truncate message content and limit number of messages"""
|
||||
if not messages or not isinstance(messages, list):
|
||||
return messages
|
||||
|
||||
# Limit number of messages
|
||||
limited_messages = messages[:max_messages]
|
||||
|
||||
# Truncate each message content
|
||||
for msg in limited_messages:
|
||||
if isinstance(msg, dict) and "content" in msg:
|
||||
content = msg["content"]
|
||||
if len(content) > max_content_length:
|
||||
msg["content"] = content[:max_content_length] + "..."
|
||||
|
||||
return limited_messages
|
||||
19
src/crewai/utilities/events/listeners/tracing/types.py
Normal file
19
src/crewai/utilities/events/listeners/tracing/types.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Any
|
||||
import uuid
|
||||
|
||||
|
||||
@dataclass
|
||||
class TraceEvent:
|
||||
"""Individual trace event payload"""
|
||||
|
||||
event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
timestamp: str = field(
|
||||
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
||||
)
|
||||
type: str = ""
|
||||
event_data: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return asdict(self)
|
||||
@@ -5,6 +5,7 @@ from pydantic import BaseModel
|
||||
|
||||
from crewai.utilities.events.base_events import BaseEvent
|
||||
|
||||
|
||||
class LLMEventBase(BaseEvent):
|
||||
task_name: Optional[str] = None
|
||||
task_id: Optional[str] = None
|
||||
@@ -32,6 +33,7 @@ class LLMEventBase(BaseEvent):
|
||||
self.task_id = task.id
|
||||
self.task_name = task.name
|
||||
|
||||
|
||||
class LLMCallType(Enum):
|
||||
"""Type of LLM call being made"""
|
||||
|
||||
@@ -48,6 +50,7 @@ class LLMCallStartedEvent(LLMEventBase):
|
||||
"""
|
||||
|
||||
type: str = "llm_call_started"
|
||||
model: Optional[str] = None
|
||||
messages: Optional[Union[str, List[Dict[str, Any]]]] = None
|
||||
tools: Optional[List[dict[str, Any]]] = None
|
||||
callbacks: Optional[List[Any]] = None
|
||||
@@ -61,6 +64,8 @@ class LLMCallCompletedEvent(LLMEventBase):
|
||||
messages: str | list[dict[str, Any]] | None = None
|
||||
response: Any
|
||||
call_type: LLMCallType
|
||||
model: Optional[str] = None
|
||||
|
||||
|
||||
class LLMCallFailedEvent(LLMEventBase):
|
||||
"""Event emitted when a LLM call fails"""
|
||||
|
||||
@@ -1321,7 +1321,7 @@ class ConsoleFormatter:
|
||||
if not verbose:
|
||||
return
|
||||
|
||||
agent_role = agent_role.split("\n")[0]
|
||||
agent_role = agent_role.partition("\n")[0]
|
||||
|
||||
# Create panel content
|
||||
content = Text()
|
||||
@@ -1356,7 +1356,7 @@ class ConsoleFormatter:
|
||||
import json
|
||||
import re
|
||||
|
||||
agent_role = agent_role.split("\n")[0]
|
||||
agent_role = agent_role.partition("\n")[0]
|
||||
|
||||
if isinstance(formatted_answer, AgentAction):
|
||||
thought = re.sub(r"\n+", "\n", formatted_answer.thought)
|
||||
@@ -1387,6 +1387,7 @@ class ConsoleFormatter:
|
||||
theme="monokai",
|
||||
line_numbers=False,
|
||||
background_color="default",
|
||||
word_wrap=True,
|
||||
)
|
||||
|
||||
content.append("\n")
|
||||
|
||||
@@ -148,7 +148,7 @@ def _llm_via_environment_or_fallback() -> Optional[LLM]:
|
||||
"AWS_SECRET_ACCESS_KEY",
|
||||
"AWS_REGION_NAME",
|
||||
]
|
||||
set_provider = model_name.split("/")[0] if "/" in model_name else "openai"
|
||||
set_provider = model_name.partition("/")[0] if "/" in model_name else "openai"
|
||||
|
||||
if set_provider in ENV_VARS:
|
||||
env_vars_for_provider = ENV_VARS[set_provider]
|
||||
|
||||
@@ -0,0 +1,470 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages": [{"role": "system", "content": "You are Test Agent. Test backstory\nYour
|
||||
personal goal is: Test goal\nTo give my best complete final answer to the task
|
||||
respond using the exact following format:\n\nThought: I now can give a great
|
||||
answer\nFinal Answer: Your final answer must be the great and the most complete
|
||||
as possible, it must be outcome described.\n\nI MUST use these formats, my job
|
||||
depends on it!"}, {"role": "user", "content": "\nCurrent Task: Say hello to
|
||||
the world\n\nThis is the expected criteria for your final answer: hello world\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", "stop":
|
||||
["\nObservation:"]}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate, zstd
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '825'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- __cf_bm=ePO5hy0kEoADCuKcboFy1iS1qckCE5KCpifQaXnlomM-1754508545-1.0.1.1-ieWfjcdIxQIXGfaMizvmgTvZPRFehqDXliegaOT7EO.kt7KSSFGmNDcC35_D9hOhE.fJ5K302uX0snQF3nLaapds2dqgGbNcsyFPOKNvAdI;
|
||||
_cfuvid=NaXWifUGChHp6Ap1mvfMrNzmO4HdzddrqXkSR9T.hYo-1754508545647-0.0.1.1-604800000
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.93.0
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.93.0
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
x-stainless-read-timeout:
|
||||
- '600.0'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.12.9
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFLBbpwwEL3zFSOflwqykE25RZWiRuq9hzZCEzOAE+NxbbPbJtp/rwyb
|
||||
hU1bqRck5s17fm9mXhMAoRpRgZA9BjlYnX7K6dq/jNkXaV9uyx/9vfx617Ed9mP+uRObyODHJ5Lh
|
||||
jfVB8mA1BcVmhqUjDBRV811ZlNlNWVxPwMAN6UjrbEgLTgdlVHqVXRVptkvzmxO7ZyXJiwq+JQAA
|
||||
r9M3+jQN/RQVZJu3ykDeY0eiOjcBCMc6VgR6r3xAE8RmASWbQGayfg+GDyDRQKf2BAhdtA1o/IEc
|
||||
wHdzpwxquJ3+K+hJa4YDO92sBR21o8cYyoxarwA0hgPGoUxRHk7I8Wxec2cdP/p3VNEqo3xfO0LP
|
||||
Jhr1ga2Y0GMC8DANabzILazjwYY68DNNz+XlbtYTy25W6PYEBg6oV/XdabSXenVDAZX2qzELibKn
|
||||
ZqEuO8GxUbwCklXqP938TXtOrkz3P/ILICXZQE1tHTVKXiZe2hzF0/1X23nKk2Hhye2VpDoocnET
|
||||
DbU46vmghP/lAw11q0xHzjo1X1Vr622BZYH0cStFckx+AwAA//8DAMHQtj5jAwAA
|
||||
headers:
|
||||
CF-RAY:
|
||||
- 96b0f0f0ac9e7ad9-SJC
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 06 Aug 2025 19:29:07 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '653'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '667'
|
||||
x-ratelimit-limit-project-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-limit-requests:
|
||||
- '30000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-remaining-project-tokens:
|
||||
- '149999830'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '29999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '149999827'
|
||||
x-ratelimit-reset-project-tokens:
|
||||
- 0s
|
||||
x-ratelimit-reset-requests:
|
||||
- 2ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_3f500b79ab1a400ea9e26d0f12e890bb
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"messages": [{"role": "system", "content": "You are Test Agent. Test backstory\nYour
|
||||
personal goal is: Test goal\nTo give my best complete final answer to the task
|
||||
respond using the exact following format:\n\nThought: I now can give a great
|
||||
answer\nFinal Answer: Your final answer must be the great and the most complete
|
||||
as possible, it must be outcome described.\n\nI MUST use these formats, my job
|
||||
depends on it!"}, {"role": "user", "content": "\nCurrent Task: Say hello to
|
||||
the world\n\nThis is the expected criteria for your final answer: hello world\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"}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate, zstd
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '797'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- __cf_bm=f59gEPi_nA3TTxtjbKaSQpvkTwezaAqOvqfxiGzRnVQ-1754508546-1.0.1.1-JrSaytxVIQSVE00I.vyGj7d4HJbbMV6R9fWPJbkDKu0Y8ueMRzTwTUnfz0YzP5nsZX5oxoE6WlmFxOuz0rRuq9YhZZsO_TbaFBOFk1jGK9U;
|
||||
_cfuvid=3D66v3.J_RcVoYy9dlF.jHwq1zTIm842xynZxzSy1Wc-1754508546352-0.0.1.1-604800000
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.93.0
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.93.0
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
x-stainless-read-timeout:
|
||||
- '200.0'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.12.9
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFLBatwwEL37K6Y622V3Y8eJb6FQ2kIPoS0EmmAm8tirRNYISc52Cfvv
|
||||
RfZm7bQp9GLwvHlP783McwIgVCMqEHKLQfZWZx/WdB42Nw9Xtux3dPMNr89/fPm+f7zct9dfRRoZ
|
||||
fP9AMryw3kvuraag2EywdISBouq6LPJidVHk5Qj03JCOtM6GLOesV0Zlm9Umz1Zltr44sresJHlR
|
||||
wc8EAOB5/EafpqFfooJV+lLpyXvsSFSnJgDhWMeKQO+VD2iCSGdQsglkRuufwfAOJBro1BMBQhdt
|
||||
Axq/Iwdwaz4qgxquxv8KPpHWnMKOnW7eLSUdtYPHGMsMWi8ANIYDxrGMYe6OyOFkX3NnHd/7P6ii
|
||||
VUb5be0IPZto1Qe2YkQPCcDdOKbhVXJhHfc21IEfaXxuXZSTnpi3s0SPYOCAelEvN+kbenVDAZX2
|
||||
i0ELiXJLzUydt4JDo3gBJIvUf7t5S3tKrkz3P/IzICXZQE1tHTVKvk48tzmKx/uvttOUR8PCk3tS
|
||||
kuqgyMVNNNTioKeTEn7vA/V1q0xHzjo13VVr67Mcixzp8kyK5JD8BgAA//8DAB06pnJlAwAA
|
||||
headers:
|
||||
CF-RAY:
|
||||
- 96b0f0f54d6aeb2c-SJC
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 06 Aug 2025 19:29:08 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '809'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '823'
|
||||
x-ratelimit-limit-project-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-limit-requests:
|
||||
- '30000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-remaining-project-tokens:
|
||||
- '149999827'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '29999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '149999827'
|
||||
x-ratelimit-reset-project-tokens:
|
||||
- 0s
|
||||
x-ratelimit-reset-requests:
|
||||
- 2ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_806f7071fb664da48953f5b216b56d9a
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"trace_id": "eb9e0ee1-15ed-4044-b84b-f17e493a1e28", "execution_type":
|
||||
"crew", "execution_context": {"crew_fingerprint": null, "crew_name": "crew",
|
||||
"flow_name": "Unknown Flow", "crewai_version": "0.152.0", "privacy_level": "standard"},
|
||||
"execution_metadata": {"expected_duration_estimate": 300, "agent_count": 0,
|
||||
"task_count": 0, "flow_method_count": 0, "execution_started_at": "2025-08-06T19:30:52.210701+00:00"}}'
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate, zstd
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '413'
|
||||
Content-Type:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- CrewAI-CLI/0.152.0
|
||||
X-Crewai-Organization-Id:
|
||||
- d3a3d10c-35db-423f-a7a4-c026030ba64d
|
||||
X-Crewai-Version:
|
||||
- 0.152.0
|
||||
method: POST
|
||||
uri: https://app.crewai.com/crewai_plus/api/v1/tracing/batches
|
||||
response:
|
||||
body:
|
||||
string: "<!DOCTYPE html>\n<html>\n<head>\n <title>The page you were looking
|
||||
for doesn't exist (404)</title>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n
|
||||
\ <style>\n .rails-default-error-page {\n background-color: #EFEFEF;\n
|
||||
\ color: #2E2F30;\n text-align: center;\n font-family: arial, sans-serif;\n
|
||||
\ margin: 0;\n }\n\n .rails-default-error-page div.dialog {\n width:
|
||||
95%;\n max-width: 33em;\n margin: 4em auto 0;\n }\n\n .rails-default-error-page
|
||||
div.dialog > div {\n border: 1px solid #CCC;\n border-right-color: #999;\n
|
||||
\ border-left-color: #999;\n border-bottom-color: #BBB;\n border-top:
|
||||
#B00100 solid 4px;\n border-top-left-radius: 9px;\n border-top-right-radius:
|
||||
9px;\n background-color: white;\n padding: 7px 12% 0;\n box-shadow:
|
||||
0 3px 8px rgba(50, 50, 50, 0.17);\n }\n\n .rails-default-error-page h1 {\n
|
||||
\ font-size: 100%;\n color: #730E15;\n line-height: 1.5em;\n }\n\n
|
||||
\ .rails-default-error-page div.dialog > p {\n margin: 0 0 1em;\n padding:
|
||||
1em;\n background-color: #F7F7F7;\n border: 1px solid #CCC;\n border-right-color:
|
||||
#999;\n border-left-color: #999;\n border-bottom-color: #999;\n border-bottom-left-radius:
|
||||
4px;\n border-bottom-right-radius: 4px;\n border-top-color: #DADADA;\n
|
||||
\ color: #666;\n box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);\n }\n
|
||||
\ </style>\n</head>\n\n<body class=\"rails-default-error-page\">\n <!-- This
|
||||
file lives in public/404.html -->\n <div class=\"dialog\">\n <div>\n <h1>The
|
||||
page you were looking for doesn't exist.</h1>\n <p>You may have mistyped
|
||||
the address or the page may have moved.</p>\n </div>\n <p>If you are
|
||||
the application owner check the logs for more information.</p>\n </div>\n</body>\n</html>\n"
|
||||
headers:
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '1722'
|
||||
Content-Type:
|
||||
- text/html; charset=UTF-8
|
||||
Date:
|
||||
- Wed, 06 Aug 2025 19:30:52 GMT
|
||||
strict-transport-security:
|
||||
- max-age=63072000; includeSubDomains
|
||||
x-request-id:
|
||||
- bec0cf39-af9c-4955-b600-607187a7b10b
|
||||
x-runtime:
|
||||
- '0.005352'
|
||||
status:
|
||||
code: 404
|
||||
message: Not Found
|
||||
- request:
|
||||
body: '{"version": "0.152.0", "batch_id": "eb9e0ee1-15ed-4044-b84b-f17e493a1e28",
|
||||
"user_context": {"user_id": "anonymous", "organization_id": "", "session_id":
|
||||
"e7e7a716-e64b-490b-96db-5c5367042114", "trace_id": "54e95e1f-cd41-4ece-9e5e-21984d635e6a"},
|
||||
"execution_metadata": {"crew_name": "crew", "execution_start": "2025-08-06T19:30:52.209750+00:00",
|
||||
"crewai_version": "0.152.0"}, "events": [{"event_id": "98b2a833-63fc-457c-a2e0-6ce228a8214c",
|
||||
"timestamp": "2025-08-06T19:30:52.328066+00:00", "type": "crew_kickoff_started",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.209750+00:00", "type": "crew_kickoff_started",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"crew_name": "crew", "crew": null, "inputs": null}}, {"event_id": "4abf563c-d35f-4a09-867d-75c1c54b3fed",
|
||||
"timestamp": "2025-08-06T19:30:52.328113+00:00", "type": "crew_kickoff_started",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.209750+00:00", "type": "crew_kickoff_started",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"crew_name": "crew", "crew": null, "inputs": null}}, {"event_id": "60bdc932-6b56-4f1d-bcc2-5b3b57c8dc94",
|
||||
"timestamp": "2025-08-06T19:30:52.330079+00:00", "type": "task_started", "event_data":
|
||||
{"task_description": "Say hello to the world", "task_name": null, "context":
|
||||
"", "agent": "Test Agent"}}, {"event_id": "97761b9f-d132-47e7-8857-5fdda8c80b65",
|
||||
"timestamp": "2025-08-06T19:30:52.330089+00:00", "type": "task_started", "event_data":
|
||||
{"task_description": "Say hello to the world", "task_name": null, "context":
|
||||
"", "agent": "Test Agent"}}, {"event_id": "cdaa47c1-448f-476e-9761-14a25f26c481",
|
||||
"timestamp": "2025-08-06T19:30:52.330477+00:00", "type": "agent_execution_started",
|
||||
"event_data": {"serialization_error": "Circular reference detected (id repeated)",
|
||||
"object_type": "AgentExecutionStartedEvent"}}, {"event_id": "7aa43738-3903-44cf-8416-d47542469537",
|
||||
"timestamp": "2025-08-06T19:30:52.330612+00:00", "type": "agent_execution_started",
|
||||
"event_data": {"serialization_error": "Circular reference detected (id repeated)",
|
||||
"object_type": "AgentExecutionStartedEvent"}}, {"event_id": "6eb42795-be95-4f1c-b70f-385c59483e43",
|
||||
"timestamp": "2025-08-06T19:30:52.330751+00:00", "type": "llm_call_started",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.330725+00:00", "type": "llm_call_started",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"task_name": null, "task_id": "1bfe4b49-ba6a-464d-9b6a-ca2eb8e965d8", "agent_id":
|
||||
"5d6dbe70-71fc-42e2-ba0d-61b460542dad", "agent_role": "Test Agent", "model":
|
||||
"gpt-4o-mini", "messages": [{"role": "system", "content": "You are Test Agent.
|
||||
Test backstory\nYour personal goal is: Test goal\nTo give my best complete final
|
||||
answer to the task respond using the exact following format:\n\nThought: I now
|
||||
can give a great answer\n..."}, {"role": "user", "content": "\nCurrent Task:
|
||||
Say hello to the world\n\nThis is the expected criteria for your final answer:
|
||||
hello world\nyou MUST return the actual complete content as the final answer,
|
||||
not a summary.\n\nBegin! This is ..."}], "tools": null, "callbacks": ["<crewai.utilities.token_counter_callback.TokenCalcHandler
|
||||
object at 0x11bd10a10>"], "available_functions": null}}, {"event_id": "679e3211-ef91-45c0-9d4a-e5118e653dbd",
|
||||
"timestamp": "2025-08-06T19:30:52.330798+00:00", "type": "llm_call_started",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.330725+00:00", "type": "llm_call_started",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"task_name": null, "task_id": "1bfe4b49-ba6a-464d-9b6a-ca2eb8e965d8", "agent_id":
|
||||
"5d6dbe70-71fc-42e2-ba0d-61b460542dad", "agent_role": "Test Agent", "model":
|
||||
"gpt-4o-mini", "messages": [{"role": "system", "content": "You are Test Agent.
|
||||
Test backstory\nYour personal goal is: Test goal\nTo give my best complete final
|
||||
answer to the task respond using the exact following format:\n\nThought: I now
|
||||
can give a great answer\n..."}, {"role": "user", "content": "\nCurrent Task:
|
||||
Say hello to the world\n\nThis is the expected criteria for your final answer:
|
||||
hello world\nyou MUST return the actual complete content as the final answer,
|
||||
not a summary.\n\nBegin! This is ..."}], "tools": null, "callbacks": ["<crewai.utilities.token_counter_callback.TokenCalcHandler
|
||||
object at 0x11bd10a10>"], "available_functions": null}}, {"event_id": "911c67ea-125b-4adf-87a5-4a9265575f93",
|
||||
"timestamp": "2025-08-06T19:30:52.335757+00:00", "type": "llm_call_completed",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.335728+00:00", "type": "llm_call_completed",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"task_name": null, "task_id": "1bfe4b49-ba6a-464d-9b6a-ca2eb8e965d8", "agent_id":
|
||||
"5d6dbe70-71fc-42e2-ba0d-61b460542dad", "agent_role": "Test Agent", "messages":
|
||||
[{"role": "system", "content": "You are Test Agent. Test backstory\nYour personal
|
||||
goal is: Test goal\nTo give my best complete final answer to the task respond
|
||||
using the exact following format:\n\nThought: I now can give a great answer\n..."},
|
||||
{"role": "user", "content": "\nCurrent Task: Say hello to the world\n\nThis
|
||||
is the expected criteria for your final answer: hello world\nyou MUST return
|
||||
the actual complete content as the final answer, not a summary.\n\nBegin! This
|
||||
is ..."}], "response": "I now can give a great answer \nFinal Answer: hello
|
||||
world", "call_type": "<LLMCallType.LLM_CALL: ''llm_call''>", "response_cost":
|
||||
3.255e-05, "model": "gpt-4o-mini"}}, {"event_id": "1c93586f-82b9-4999-adda-78c8010b59f6",
|
||||
"timestamp": "2025-08-06T19:30:52.335800+00:00", "type": "llm_call_completed",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.335728+00:00", "type": "llm_call_completed",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"task_name": null, "task_id": "1bfe4b49-ba6a-464d-9b6a-ca2eb8e965d8", "agent_id":
|
||||
"5d6dbe70-71fc-42e2-ba0d-61b460542dad", "agent_role": "Test Agent", "messages":
|
||||
[{"role": "system", "content": "You are Test Agent. Test backstory\nYour personal
|
||||
goal is: Test goal\nTo give my best complete final answer to the task respond
|
||||
using the exact following format:\n\nThought: I now can give a great answer\n..."},
|
||||
{"role": "user", "content": "\nCurrent Task: Say hello to the world\n\nThis
|
||||
is the expected criteria for your final answer: hello world\nyou MUST return
|
||||
the actual complete content as the final answer, not a summary.\n\nBegin! This
|
||||
is ..."}], "response": "I now can give a great answer \nFinal Answer: hello
|
||||
world", "call_type": "<LLMCallType.LLM_CALL: ''llm_call''>", "response_cost":
|
||||
3.255e-05, "model": "gpt-4o-mini"}}, {"event_id": "eb9d53af-ce39-4241-ab93-e40545a1ee78",
|
||||
"timestamp": "2025-08-06T19:30:52.335904+00:00", "type": "agent_execution_completed",
|
||||
"event_data": {"serialization_error": "Circular reference detected (id repeated)",
|
||||
"object_type": "AgentExecutionCompletedEvent"}}, {"event_id": "dc71f5f8-5762-4e44-ac60-aa20b033c9f9",
|
||||
"timestamp": "2025-08-06T19:30:52.335989+00:00", "type": "agent_execution_completed",
|
||||
"event_data": {"serialization_error": "Circular reference detected (id repeated)",
|
||||
"object_type": "AgentExecutionCompletedEvent"}}, {"event_id": "84da8fb8-9247-4718-bc85-a69033c9261f",
|
||||
"timestamp": "2025-08-06T19:30:52.336082+00:00", "type": "task_completed", "event_data":
|
||||
{"serialization_error": "Circular reference detected (id repeated)", "object_type":
|
||||
"TaskCompletedEvent"}}, {"event_id": "c1a23877-2b87-40be-98a1-a3b2630c8657",
|
||||
"timestamp": "2025-08-06T19:30:52.336107+00:00", "type": "task_completed", "event_data":
|
||||
{"serialization_error": "Circular reference detected (id repeated)", "object_type":
|
||||
"TaskCompletedEvent"}}, {"event_id": "c77587d7-68d6-4600-b98a-74fe58af41fc",
|
||||
"timestamp": "2025-08-06T19:30:52.337164+00:00", "type": "crew_kickoff_completed",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.337145+00:00", "type": "crew_kickoff_completed",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"crew_name": "crew", "crew": null, "output": {"description": "Say hello to the
|
||||
world", "name": null, "expected_output": "hello world", "summary": "Say hello
|
||||
to the world...", "raw": "hello world", "pydantic": null, "json_dict": null,
|
||||
"agent": "Test Agent", "output_format": "raw"}, "total_tokens": 170}}]}'
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate, zstd
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '8300'
|
||||
Content-Type:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- CrewAI-CLI/0.152.0
|
||||
X-Crewai-Organization-Id:
|
||||
- d3a3d10c-35db-423f-a7a4-c026030ba64d
|
||||
X-Crewai-Version:
|
||||
- 0.152.0
|
||||
method: POST
|
||||
uri: https://app.crewai.com/crewai_plus/api/v1/tracing
|
||||
response:
|
||||
body:
|
||||
string: "<!DOCTYPE html>\n<html>\n<head>\n <title>The page you were looking
|
||||
for doesn't exist (404)</title>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n
|
||||
\ <style>\n .rails-default-error-page {\n background-color: #EFEFEF;\n
|
||||
\ color: #2E2F30;\n text-align: center;\n font-family: arial, sans-serif;\n
|
||||
\ margin: 0;\n }\n\n .rails-default-error-page div.dialog {\n width:
|
||||
95%;\n max-width: 33em;\n margin: 4em auto 0;\n }\n\n .rails-default-error-page
|
||||
div.dialog > div {\n border: 1px solid #CCC;\n border-right-color: #999;\n
|
||||
\ border-left-color: #999;\n border-bottom-color: #BBB;\n border-top:
|
||||
#B00100 solid 4px;\n border-top-left-radius: 9px;\n border-top-right-radius:
|
||||
9px;\n background-color: white;\n padding: 7px 12% 0;\n box-shadow:
|
||||
0 3px 8px rgba(50, 50, 50, 0.17);\n }\n\n .rails-default-error-page h1 {\n
|
||||
\ font-size: 100%;\n color: #730E15;\n line-height: 1.5em;\n }\n\n
|
||||
\ .rails-default-error-page div.dialog > p {\n margin: 0 0 1em;\n padding:
|
||||
1em;\n background-color: #F7F7F7;\n border: 1px solid #CCC;\n border-right-color:
|
||||
#999;\n border-left-color: #999;\n border-bottom-color: #999;\n border-bottom-left-radius:
|
||||
4px;\n border-bottom-right-radius: 4px;\n border-top-color: #DADADA;\n
|
||||
\ color: #666;\n box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);\n }\n
|
||||
\ </style>\n</head>\n\n<body class=\"rails-default-error-page\">\n <!-- This
|
||||
file lives in public/404.html -->\n <div class=\"dialog\">\n <div>\n <h1>The
|
||||
page you were looking for doesn't exist.</h1>\n <p>You may have mistyped
|
||||
the address or the page may have moved.</p>\n </div>\n <p>If you are
|
||||
the application owner check the logs for more information.</p>\n </div>\n</body>\n</html>\n"
|
||||
headers:
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '1722'
|
||||
Content-Type:
|
||||
- text/html; charset=UTF-8
|
||||
Date:
|
||||
- Wed, 06 Aug 2025 19:30:52 GMT
|
||||
strict-transport-security:
|
||||
- max-age=63072000; includeSubDomains
|
||||
x-request-id:
|
||||
- 78674bcb-6c8a-4eaf-8577-5cb27cac4089
|
||||
x-runtime:
|
||||
- '0.006009'
|
||||
status:
|
||||
code: 404
|
||||
message: Not Found
|
||||
version: 1
|
||||
@@ -0,0 +1,470 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages": [{"role": "system", "content": "You are Test Agent. Test backstory\nYour
|
||||
personal goal is: Test goal\nTo give my best complete final answer to the task
|
||||
respond using the exact following format:\n\nThought: I now can give a great
|
||||
answer\nFinal Answer: Your final answer must be the great and the most complete
|
||||
as possible, it must be outcome described.\n\nI MUST use these formats, my job
|
||||
depends on it!"}, {"role": "user", "content": "\nCurrent Task: Say hello to
|
||||
the world\n\nThis is the expected criteria for your final answer: hello world\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", "stop":
|
||||
["\nObservation:"]}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate, zstd
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '825'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- __cf_bm=ePO5hy0kEoADCuKcboFy1iS1qckCE5KCpifQaXnlomM-1754508545-1.0.1.1-ieWfjcdIxQIXGfaMizvmgTvZPRFehqDXliegaOT7EO.kt7KSSFGmNDcC35_D9hOhE.fJ5K302uX0snQF3nLaapds2dqgGbNcsyFPOKNvAdI;
|
||||
_cfuvid=NaXWifUGChHp6Ap1mvfMrNzmO4HdzddrqXkSR9T.hYo-1754508545647-0.0.1.1-604800000
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.93.0
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.93.0
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
x-stainless-read-timeout:
|
||||
- '600.0'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.12.9
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFJNa9wwEL37Vww6x8Wbtbu7voXQQC+lh0Jb2mAm0thWI0tCkuOWsP+9
|
||||
SN6svf2AXAyeN+/pvZl5zgCYFKwGxnsMfLAqv93Q22mP+O7z9OlwUIW4LT7S+LX68nj3YWJXkWEe
|
||||
fhAPL6w33AxWUZBGzzB3hIGi6mZXlVWxr6oiAYMRpCKtsyEvTT5ILfPr4rrMi12+2Z/YvZGcPKvh
|
||||
WwYA8Jy+0acW9JPVkLRSZSDvsSNWn5sAmDMqVhh6L31AHdjVAnKjA+lk/T1oMwFHDZ18IkDoom1A
|
||||
7SdyAN/1ndSo4Cb919CTUgYm45RYCzpqR48xlB6VWgGotQkYh5Ki3J+Q49m8Mp115sH/QWWt1NL3
|
||||
jSP0RkejPhjLEnrMAO7TkMaL3Mw6M9jQBPNI6blNtZv12LKbFbo9gcEEVKv67jTaS71GUECp/GrM
|
||||
jCPvSSzUZSc4CmlWQLZK/bebf2nPyaXuXiO/AJyTDSQa60hIfpl4aXMUT/d/becpJ8PMk3uSnJog
|
||||
ycVNCGpxVPNBMf/LBxqaVuqOnHVyvqrWNtsSqxLpsOUsO2a/AQAA//8DAD59q5pjAwAA
|
||||
headers:
|
||||
CF-RAY:
|
||||
- 96b0f1059ae17ad9-SJC
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 06 Aug 2025 19:29:10 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '521'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '537'
|
||||
x-ratelimit-limit-project-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-limit-requests:
|
||||
- '30000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-remaining-project-tokens:
|
||||
- '149999827'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '29999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '149999827'
|
||||
x-ratelimit-reset-project-tokens:
|
||||
- 0s
|
||||
x-ratelimit-reset-requests:
|
||||
- 2ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_c94c2a416aee4c93bae1f801c8ae3e72
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"messages": [{"role": "system", "content": "You are Test Agent. Test backstory\nYour
|
||||
personal goal is: Test goal\nTo give my best complete final answer to the task
|
||||
respond using the exact following format:\n\nThought: I now can give a great
|
||||
answer\nFinal Answer: Your final answer must be the great and the most complete
|
||||
as possible, it must be outcome described.\n\nI MUST use these formats, my job
|
||||
depends on it!"}, {"role": "user", "content": "\nCurrent Task: Say hello to
|
||||
the world\n\nThis is the expected criteria for your final answer: hello world\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"}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate, zstd
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '797'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- __cf_bm=f59gEPi_nA3TTxtjbKaSQpvkTwezaAqOvqfxiGzRnVQ-1754508546-1.0.1.1-JrSaytxVIQSVE00I.vyGj7d4HJbbMV6R9fWPJbkDKu0Y8ueMRzTwTUnfz0YzP5nsZX5oxoE6WlmFxOuz0rRuq9YhZZsO_TbaFBOFk1jGK9U;
|
||||
_cfuvid=3D66v3.J_RcVoYy9dlF.jHwq1zTIm842xynZxzSy1Wc-1754508546352-0.0.1.1-604800000
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.93.0
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.93.0
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
x-stainless-read-timeout:
|
||||
- '200.0'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.12.9
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFJNa9wwEL37Vww6x2U/7GzqW2j6BYVSaE5tMBN5bKuVNYok7zaE/e9F
|
||||
2u3a2ybQi8Hz5j29NzNPGYBQjahAyB6DHKzO3yzpcnf74f3nh6+rx/bT9saa2+u343D55eGmFBeR
|
||||
wfc/SIY/rFeSB6spKDYHWDrCQFF1uSmLcnFVlosEDNyQjrTOhrzgfFBG5avFqsgXm3x5dWT3rCR5
|
||||
UcG3DADgKX2jT9PQL1FB0kqVgbzHjkR1agIQjnWsCPRe+YAmiIsJlGwCmWT9IxjegUQDndoSIHTR
|
||||
NqDxO3IA3807ZVDDdfqvoCetGXbsdDMXdNSOHmMoM2o9A9AYDhiHkqLcHZH9ybzmzjq+939RRauM
|
||||
8n3tCD2baNQHtiKh+wzgLg1pPMstrOPBhjrwT0rPLcvNQU9Mu5mh6yMYOKCe1TfH0Z7r1Q0FVNrP
|
||||
xiwkyp6aiTrtBMdG8QzIZqn/dfOc9iG5Mt3/yE+AlGQDNbV11Ch5nnhqcxRP96W205STYeHJbZWk
|
||||
OihycRMNtTjqw0EJ/+gDDXWrTEfOOnW4qtbW6wLLAun1Wopsn/0GAAD//wMASJr3q2MDAAA=
|
||||
headers:
|
||||
CF-RAY:
|
||||
- 96b0f109ae7aeb2c-SJC
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 06 Aug 2025 19:29:11 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '499'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '511'
|
||||
x-ratelimit-limit-project-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-limit-requests:
|
||||
- '30000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-remaining-project-tokens:
|
||||
- '149999830'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '29999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '149999827'
|
||||
x-ratelimit-reset-project-tokens:
|
||||
- 0s
|
||||
x-ratelimit-reset-requests:
|
||||
- 2ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_dece4be9f37c4d64b324ab36d1ed9cf4
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"trace_id": "ff5ac8a9-dec2-4b73-8928-3dd06d12051f", "execution_type":
|
||||
"crew", "execution_context": {"crew_fingerprint": null, "crew_name": "crew",
|
||||
"flow_name": "Unknown Flow", "crewai_version": "0.152.0", "privacy_level": "standard"},
|
||||
"execution_metadata": {"expected_duration_estimate": 300, "agent_count": 0,
|
||||
"task_count": 0, "flow_method_count": 0, "execution_started_at": "2025-08-06T19:30:51.727534+00:00"}}'
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate, zstd
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '413'
|
||||
Content-Type:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- CrewAI-CLI/0.152.0
|
||||
X-Crewai-Organization-Id:
|
||||
- d3a3d10c-35db-423f-a7a4-c026030ba64d
|
||||
X-Crewai-Version:
|
||||
- 0.152.0
|
||||
method: POST
|
||||
uri: https://app.crewai.com/crewai_plus/api/v1/tracing/batches
|
||||
response:
|
||||
body:
|
||||
string: "<!DOCTYPE html>\n<html>\n<head>\n <title>The page you were looking
|
||||
for doesn't exist (404)</title>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n
|
||||
\ <style>\n .rails-default-error-page {\n background-color: #EFEFEF;\n
|
||||
\ color: #2E2F30;\n text-align: center;\n font-family: arial, sans-serif;\n
|
||||
\ margin: 0;\n }\n\n .rails-default-error-page div.dialog {\n width:
|
||||
95%;\n max-width: 33em;\n margin: 4em auto 0;\n }\n\n .rails-default-error-page
|
||||
div.dialog > div {\n border: 1px solid #CCC;\n border-right-color: #999;\n
|
||||
\ border-left-color: #999;\n border-bottom-color: #BBB;\n border-top:
|
||||
#B00100 solid 4px;\n border-top-left-radius: 9px;\n border-top-right-radius:
|
||||
9px;\n background-color: white;\n padding: 7px 12% 0;\n box-shadow:
|
||||
0 3px 8px rgba(50, 50, 50, 0.17);\n }\n\n .rails-default-error-page h1 {\n
|
||||
\ font-size: 100%;\n color: #730E15;\n line-height: 1.5em;\n }\n\n
|
||||
\ .rails-default-error-page div.dialog > p {\n margin: 0 0 1em;\n padding:
|
||||
1em;\n background-color: #F7F7F7;\n border: 1px solid #CCC;\n border-right-color:
|
||||
#999;\n border-left-color: #999;\n border-bottom-color: #999;\n border-bottom-left-radius:
|
||||
4px;\n border-bottom-right-radius: 4px;\n border-top-color: #DADADA;\n
|
||||
\ color: #666;\n box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);\n }\n
|
||||
\ </style>\n</head>\n\n<body class=\"rails-default-error-page\">\n <!-- This
|
||||
file lives in public/404.html -->\n <div class=\"dialog\">\n <div>\n <h1>The
|
||||
page you were looking for doesn't exist.</h1>\n <p>You may have mistyped
|
||||
the address or the page may have moved.</p>\n </div>\n <p>If you are
|
||||
the application owner check the logs for more information.</p>\n </div>\n</body>\n</html>\n"
|
||||
headers:
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '1722'
|
||||
Content-Type:
|
||||
- text/html; charset=UTF-8
|
||||
Date:
|
||||
- Wed, 06 Aug 2025 19:30:51 GMT
|
||||
strict-transport-security:
|
||||
- max-age=63072000; includeSubDomains
|
||||
x-request-id:
|
||||
- 0b6a5ff5-789e-4c0d-a10b-316fecc0e905
|
||||
x-runtime:
|
||||
- '0.005528'
|
||||
status:
|
||||
code: 404
|
||||
message: Not Found
|
||||
- request:
|
||||
body: '{"version": "0.152.0", "batch_id": "ff5ac8a9-dec2-4b73-8928-3dd06d12051f",
|
||||
"user_context": {"user_id": "anonymous", "organization_id": "", "session_id":
|
||||
"aabc00e7-d423-4385-8b83-0468c03ae47b", "trace_id": "0a0586da-135c-4080-a352-dbe47bb2ac86"},
|
||||
"execution_metadata": {"crew_name": "crew", "execution_start": "2025-08-06T19:30:51.726805+00:00",
|
||||
"crewai_version": "0.152.0"}, "events": [{"event_id": "211eb90d-fb76-4ee5-bee7-62cc2f1d9aa8",
|
||||
"timestamp": "2025-08-06T19:30:51.842887+00:00", "type": "crew_kickoff_started",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:51.726805+00:00", "type": "crew_kickoff_started",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"crew_name": "crew", "crew": null, "inputs": null}}, {"event_id": "713e4dbd-887f-4481-a6c8-554b637848e2",
|
||||
"timestamp": "2025-08-06T19:30:51.842982+00:00", "type": "crew_kickoff_started",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:51.726805+00:00", "type": "crew_kickoff_started",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"crew_name": "crew", "crew": null, "inputs": null}}, {"event_id": "b920108c-c6fe-40d7-baa3-29c23d76a8e1",
|
||||
"timestamp": "2025-08-06T19:30:51.844489+00:00", "type": "task_started", "event_data":
|
||||
{"task_description": "Say hello to the world", "task_name": null, "context":
|
||||
"", "agent": "Test Agent"}}, {"event_id": "96180117-d060-49ab-8327-712f230653f2",
|
||||
"timestamp": "2025-08-06T19:30:51.844512+00:00", "type": "task_started", "event_data":
|
||||
{"task_description": "Say hello to the world", "task_name": null, "context":
|
||||
"", "agent": "Test Agent"}}, {"event_id": "82baa39d-d1ae-44f8-8f35-40646fdec793",
|
||||
"timestamp": "2025-08-06T19:30:51.845195+00:00", "type": "agent_execution_started",
|
||||
"event_data": {"serialization_error": "Circular reference detected (id repeated)",
|
||||
"object_type": "AgentExecutionStartedEvent"}}, {"event_id": "c34d2e12-6671-4593-a45d-8742704f6ace",
|
||||
"timestamp": "2025-08-06T19:30:51.845868+00:00", "type": "agent_execution_started",
|
||||
"event_data": {"serialization_error": "Circular reference detected (id repeated)",
|
||||
"object_type": "AgentExecutionStartedEvent"}}, {"event_id": "87d12818-f0b4-46d0-8ecc-e46afaf8eddb",
|
||||
"timestamp": "2025-08-06T19:30:51.846100+00:00", "type": "llm_call_started",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:51.846006+00:00", "type": "llm_call_started",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"task_name": null, "task_id": "7f026c34-8c77-4710-8ecb-9d4c830b9eb4", "agent_id":
|
||||
"bd02dc4e-982e-481c-9358-2b4a7ac73831", "agent_role": "Test Agent", "model":
|
||||
"gpt-4o-mini", "messages": [{"role": "system", "content": "You are Test Agent.
|
||||
Test backstory\nYour personal goal is: Test goal\nTo give my best complete final
|
||||
answer to the task respond using the exact following format:\n\nThought: I now
|
||||
can give a great answer\n..."}, {"role": "user", "content": "\nCurrent Task:
|
||||
Say hello to the world\n\nThis is the expected criteria for your final answer:
|
||||
hello world\nyou MUST return the actual complete content as the final answer,
|
||||
not a summary.\n\nBegin! This is ..."}], "tools": null, "callbacks": ["<crewai.utilities.token_counter_callback.TokenCalcHandler
|
||||
object at 0x11bd13470>"], "available_functions": null}}, {"event_id": "bbfd4480-87aa-4a56-b988-2dcc9e142c20",
|
||||
"timestamp": "2025-08-06T19:30:51.846155+00:00", "type": "llm_call_started",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:51.846006+00:00", "type": "llm_call_started",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"task_name": null, "task_id": "7f026c34-8c77-4710-8ecb-9d4c830b9eb4", "agent_id":
|
||||
"bd02dc4e-982e-481c-9358-2b4a7ac73831", "agent_role": "Test Agent", "model":
|
||||
"gpt-4o-mini", "messages": [{"role": "system", "content": "You are Test Agent.
|
||||
Test backstory\nYour personal goal is: Test goal\nTo give my best complete final
|
||||
answer to the task respond using the exact following format:\n\nThought: I now
|
||||
can give a great answer\n..."}, {"role": "user", "content": "\nCurrent Task:
|
||||
Say hello to the world\n\nThis is the expected criteria for your final answer:
|
||||
hello world\nyou MUST return the actual complete content as the final answer,
|
||||
not a summary.\n\nBegin! This is ..."}], "tools": null, "callbacks": ["<crewai.utilities.token_counter_callback.TokenCalcHandler
|
||||
object at 0x11bd13470>"], "available_functions": null}}, {"event_id": "25a17ec7-b2ee-4eeb-bdf5-27efffed961c",
|
||||
"timestamp": "2025-08-06T19:30:52.018207+00:00", "type": "llm_call_completed",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.017914+00:00", "type": "llm_call_completed",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"task_name": null, "task_id": "7f026c34-8c77-4710-8ecb-9d4c830b9eb4", "agent_id":
|
||||
"bd02dc4e-982e-481c-9358-2b4a7ac73831", "agent_role": "Test Agent", "messages":
|
||||
[{"role": "system", "content": "You are Test Agent. Test backstory\nYour personal
|
||||
goal is: Test goal\nTo give my best complete final answer to the task respond
|
||||
using the exact following format:\n\nThought: I now can give a great answer\n..."},
|
||||
{"role": "user", "content": "\nCurrent Task: Say hello to the world\n\nThis
|
||||
is the expected criteria for your final answer: hello world\nyou MUST return
|
||||
the actual complete content as the final answer, not a summary.\n\nBegin! This
|
||||
is ..."}], "response": "I now can give a great answer \nFinal Answer: hello
|
||||
world", "call_type": "<LLMCallType.LLM_CALL: ''llm_call''>", "response_cost":
|
||||
3.135e-05, "model": "gpt-4o-mini"}}, {"event_id": "0ccb9b70-c5ad-4f7f-b3ee-ecfd62c2d7cc",
|
||||
"timestamp": "2025-08-06T19:30:52.018273+00:00", "type": "llm_call_completed",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.017914+00:00", "type": "llm_call_completed",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"task_name": null, "task_id": "7f026c34-8c77-4710-8ecb-9d4c830b9eb4", "agent_id":
|
||||
"bd02dc4e-982e-481c-9358-2b4a7ac73831", "agent_role": "Test Agent", "messages":
|
||||
[{"role": "system", "content": "You are Test Agent. Test backstory\nYour personal
|
||||
goal is: Test goal\nTo give my best complete final answer to the task respond
|
||||
using the exact following format:\n\nThought: I now can give a great answer\n..."},
|
||||
{"role": "user", "content": "\nCurrent Task: Say hello to the world\n\nThis
|
||||
is the expected criteria for your final answer: hello world\nyou MUST return
|
||||
the actual complete content as the final answer, not a summary.\n\nBegin! This
|
||||
is ..."}], "response": "I now can give a great answer \nFinal Answer: hello
|
||||
world", "call_type": "<LLMCallType.LLM_CALL: ''llm_call''>", "response_cost":
|
||||
3.135e-05, "model": "gpt-4o-mini"}}, {"event_id": "d7f4440b-8f9f-4e29-a946-6d10f4bdfc3c",
|
||||
"timestamp": "2025-08-06T19:30:52.018559+00:00", "type": "agent_execution_completed",
|
||||
"event_data": {"serialization_error": "Circular reference detected (id repeated)",
|
||||
"object_type": "AgentExecutionCompletedEvent"}}, {"event_id": "072195c3-54df-4cba-9068-b9a25bbb8d7c",
|
||||
"timestamp": "2025-08-06T19:30:52.018669+00:00", "type": "agent_execution_completed",
|
||||
"event_data": {"serialization_error": "Circular reference detected (id repeated)",
|
||||
"object_type": "AgentExecutionCompletedEvent"}}, {"event_id": "0b6f9e85-32c9-4c62-9049-6890953e2143",
|
||||
"timestamp": "2025-08-06T19:30:52.018838+00:00", "type": "task_completed", "event_data":
|
||||
{"serialization_error": "Circular reference detected (id repeated)", "object_type":
|
||||
"TaskCompletedEvent"}}, {"event_id": "5ff20fcb-ec10-40ac-bb90-9568aa4eb1de",
|
||||
"timestamp": "2025-08-06T19:30:52.018867+00:00", "type": "task_completed", "event_data":
|
||||
{"serialization_error": "Circular reference detected (id repeated)", "object_type":
|
||||
"TaskCompletedEvent"}}, {"event_id": "c5a36300-3911-4d75-a660-d133a7a4be94",
|
||||
"timestamp": "2025-08-06T19:30:52.020135+00:00", "type": "crew_kickoff_completed",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.020115+00:00", "type": "crew_kickoff_completed",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"crew_name": "crew", "crew": null, "output": {"description": "Say hello to the
|
||||
world", "name": null, "expected_output": "hello world", "summary": "Say hello
|
||||
to the world...", "raw": "hello world", "pydantic": null, "json_dict": null,
|
||||
"agent": "Test Agent", "output_format": "raw"}, "total_tokens": 170}}]}'
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate, zstd
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '8300'
|
||||
Content-Type:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- CrewAI-CLI/0.152.0
|
||||
X-Crewai-Organization-Id:
|
||||
- d3a3d10c-35db-423f-a7a4-c026030ba64d
|
||||
X-Crewai-Version:
|
||||
- 0.152.0
|
||||
method: POST
|
||||
uri: https://app.crewai.com/crewai_plus/api/v1/tracing
|
||||
response:
|
||||
body:
|
||||
string: "<!DOCTYPE html>\n<html>\n<head>\n <title>The page you were looking
|
||||
for doesn't exist (404)</title>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n
|
||||
\ <style>\n .rails-default-error-page {\n background-color: #EFEFEF;\n
|
||||
\ color: #2E2F30;\n text-align: center;\n font-family: arial, sans-serif;\n
|
||||
\ margin: 0;\n }\n\n .rails-default-error-page div.dialog {\n width:
|
||||
95%;\n max-width: 33em;\n margin: 4em auto 0;\n }\n\n .rails-default-error-page
|
||||
div.dialog > div {\n border: 1px solid #CCC;\n border-right-color: #999;\n
|
||||
\ border-left-color: #999;\n border-bottom-color: #BBB;\n border-top:
|
||||
#B00100 solid 4px;\n border-top-left-radius: 9px;\n border-top-right-radius:
|
||||
9px;\n background-color: white;\n padding: 7px 12% 0;\n box-shadow:
|
||||
0 3px 8px rgba(50, 50, 50, 0.17);\n }\n\n .rails-default-error-page h1 {\n
|
||||
\ font-size: 100%;\n color: #730E15;\n line-height: 1.5em;\n }\n\n
|
||||
\ .rails-default-error-page div.dialog > p {\n margin: 0 0 1em;\n padding:
|
||||
1em;\n background-color: #F7F7F7;\n border: 1px solid #CCC;\n border-right-color:
|
||||
#999;\n border-left-color: #999;\n border-bottom-color: #999;\n border-bottom-left-radius:
|
||||
4px;\n border-bottom-right-radius: 4px;\n border-top-color: #DADADA;\n
|
||||
\ color: #666;\n box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);\n }\n
|
||||
\ </style>\n</head>\n\n<body class=\"rails-default-error-page\">\n <!-- This
|
||||
file lives in public/404.html -->\n <div class=\"dialog\">\n <div>\n <h1>The
|
||||
page you were looking for doesn't exist.</h1>\n <p>You may have mistyped
|
||||
the address or the page may have moved.</p>\n </div>\n <p>If you are
|
||||
the application owner check the logs for more information.</p>\n </div>\n</body>\n</html>\n"
|
||||
headers:
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '1722'
|
||||
Content-Type:
|
||||
- text/html; charset=UTF-8
|
||||
Date:
|
||||
- Wed, 06 Aug 2025 19:30:52 GMT
|
||||
strict-transport-security:
|
||||
- max-age=63072000; includeSubDomains
|
||||
x-request-id:
|
||||
- 9edcdee4-f720-431e-9d6d-2dbc1a7bb8fe
|
||||
x-runtime:
|
||||
- '0.005504'
|
||||
status:
|
||||
code: 404
|
||||
message: Not Found
|
||||
version: 1
|
||||
@@ -0,0 +1,450 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages": [{"role": "system", "content": "You are Test Agent. Test backstory\nYour
|
||||
personal goal is: Test goal\nTo give my best complete final answer to the task
|
||||
respond using the exact following format:\n\nThought: I now can give a great
|
||||
answer\nFinal Answer: Your final answer must be the great and the most complete
|
||||
as possible, it must be outcome described.\n\nI MUST use these formats, my job
|
||||
depends on it!"}, {"role": "user", "content": "\nCurrent Task: Say hello to
|
||||
the world\n\nThis is the expected criteria for your final answer: hello world\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", "stop":
|
||||
["\nObservation:"]}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate, zstd
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '825'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- __cf_bm=ePO5hy0kEoADCuKcboFy1iS1qckCE5KCpifQaXnlomM-1754508545-1.0.1.1-ieWfjcdIxQIXGfaMizvmgTvZPRFehqDXliegaOT7EO.kt7KSSFGmNDcC35_D9hOhE.fJ5K302uX0snQF3nLaapds2dqgGbNcsyFPOKNvAdI;
|
||||
_cfuvid=NaXWifUGChHp6Ap1mvfMrNzmO4HdzddrqXkSR9T.hYo-1754508545647-0.0.1.1-604800000
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.93.0
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.93.0
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
x-stainless-read-timeout:
|
||||
- '600.0'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.12.9
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFLBbpwwEL3zFSOfl2oJ0N1ySypV6aG3ntpGaGIGcGI8lm2yjaL998qw
|
||||
Wdi2kXJBYt685/dm5iUBEKoRFQjZY5CD1ennjD6ON/5Hrr4dysxu8/72++NNd/twXT4PYhMZfP9A
|
||||
MryyPkgerKag2MywdISBomq2K4tyuy+L/QQM3JCOtM6GtOB0UEalV9urIt3u0mx/YvesJHlRwc8E
|
||||
AOBl+kafpqHfooLt5rUykPfYkajOTQDCsY4Vgd4rH9AEsVlAySaQmax/BcMHkGigU08ECF20DWj8
|
||||
gRzAL/NFGdRwPf1X0JPWDAd2ulkLOmpHjzGUGbVeAWgMB4xDmaLcnZDj2bzmzjq+939RRauM8n3t
|
||||
CD2baNQHtmJCjwnA3TSk8SK3sI4HG+rAjzQ9l5W7WU8su1mh+QkMHFCv6rvTaC/16oYCKu1XYxYS
|
||||
ZU/NQl12gmOjeAUkq9T/uvmf9pxcme498gsgJdlATW0dNUpeJl7aHMXTfavtPOXJsPDknpSkOihy
|
||||
cRMNtTjq+aCEf/aBhrpVpiNnnZqvqrV1XmBZIH3KpUiOyR8AAAD//wMAErrW9WMDAAA=
|
||||
headers:
|
||||
CF-RAY:
|
||||
- 96b0f0fb5c067ad9-SJC
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 06 Aug 2025 19:29:09 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '628'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '657'
|
||||
x-ratelimit-limit-project-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-limit-requests:
|
||||
- '30000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-remaining-project-tokens:
|
||||
- '149999827'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '29999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '149999827'
|
||||
x-ratelimit-reset-project-tokens:
|
||||
- 0s
|
||||
x-ratelimit-reset-requests:
|
||||
- 2ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_a0daca00035c423daf0e9df208720180
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"messages": [{"role": "system", "content": "You are Test Agent. Test backstory\nYour
|
||||
personal goal is: Test goal\nTo give my best complete final answer to the task
|
||||
respond using the exact following format:\n\nThought: I now can give a great
|
||||
answer\nFinal Answer: Your final answer must be the great and the most complete
|
||||
as possible, it must be outcome described.\n\nI MUST use these formats, my job
|
||||
depends on it!"}, {"role": "user", "content": "\nCurrent Task: Say hello to
|
||||
the world\n\nThis is the expected criteria for your final answer: hello world\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"}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate, zstd
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '797'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- __cf_bm=f59gEPi_nA3TTxtjbKaSQpvkTwezaAqOvqfxiGzRnVQ-1754508546-1.0.1.1-JrSaytxVIQSVE00I.vyGj7d4HJbbMV6R9fWPJbkDKu0Y8ueMRzTwTUnfz0YzP5nsZX5oxoE6WlmFxOuz0rRuq9YhZZsO_TbaFBOFk1jGK9U;
|
||||
_cfuvid=3D66v3.J_RcVoYy9dlF.jHwq1zTIm842xynZxzSy1Wc-1754508546352-0.0.1.1-604800000
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.93.0
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.93.0
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
x-stainless-read-timeout:
|
||||
- '200.0'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.12.9
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFLLjtswDLz7Kwid4yLZ2HXiW7FA0R567KXtwmAk2tZWlgSJTlos8u+F
|
||||
nGzs9AH0YsAczmiG5EsGILQSNQjZI8vBm/xxQ2+Pnz89n8Lj7gvvP3BArNb94cClqsQqMdzhmSS/
|
||||
st5IN3hDrJ29wDIQMiXVTVUW5XpXFvsJGJwik2id57xw+aCtzh/WD0W+rvLN7srunZYURQ1fMwCA
|
||||
l+mbfFpFP0QN69VrZaAYsSNR35oARHAmVQTGqCOjZbGaQeksk52sfwTrTiDRQqePBAhdsg1o44kC
|
||||
wDf7Xls08G76r6EnYxycXDBqKRioHSOmUHY0ZgGgtY4xDWWK8nRFzjfzxnU+uEP8jSpabXXsm0AY
|
||||
nU1GIzsvJvScATxNQxrvcgsf3OC5Yfedpuc2ZXXRE/NuFuj2CrJjNIt6dR3tvV6jiFGbuBizkCh7
|
||||
UjN13gmOSrsFkC1S/+nmb9qX5Np2/yM/A1KSZ1KND6S0vE88twVKp/uvttuUJ8MiUjhqSQ1rCmkT
|
||||
iloczeWgRPwZmYam1baj4IO+XFXrm22BZYG030qRnbNfAAAA//8DAOX6h6tjAwAA
|
||||
headers:
|
||||
CF-RAY:
|
||||
- 96b0f101793aeb2c-SJC
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 06 Aug 2025 19:29:09 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '541'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '557'
|
||||
x-ratelimit-limit-project-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-limit-requests:
|
||||
- '30000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-remaining-project-tokens:
|
||||
- '149999827'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '29999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '149999827'
|
||||
x-ratelimit-reset-project-tokens:
|
||||
- 0s
|
||||
x-ratelimit-reset-requests:
|
||||
- 2ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_df70f95325b14817a692f23cf9cca880
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"trace_id": "2487456d-e03a-4eae-92a1-e9779e8f06a1", "execution_type":
|
||||
"crew", "execution_context": {"crew_fingerprint": null, "crew_name": "Unknown
|
||||
Crew", "flow_name": "Unknown Flow", "crewai_version": "0.152.0", "privacy_level":
|
||||
"standard"}, "execution_metadata": {"expected_duration_estimate": 300, "agent_count":
|
||||
0, "task_count": 0, "flow_method_count": 0, "execution_started_at": "2025-08-06T19:30:52.475039+00:00"}}'
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate, zstd
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '421'
|
||||
Content-Type:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- CrewAI-CLI/0.152.0
|
||||
X-Crewai-Organization-Id:
|
||||
- d3a3d10c-35db-423f-a7a4-c026030ba64d
|
||||
X-Crewai-Version:
|
||||
- 0.152.0
|
||||
method: POST
|
||||
uri: https://app.crewai.com/crewai_plus/api/v1/tracing/batches
|
||||
response:
|
||||
body:
|
||||
string: "<!DOCTYPE html>\n<html>\n<head>\n <title>The page you were looking
|
||||
for doesn't exist (404)</title>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n
|
||||
\ <style>\n .rails-default-error-page {\n background-color: #EFEFEF;\n
|
||||
\ color: #2E2F30;\n text-align: center;\n font-family: arial, sans-serif;\n
|
||||
\ margin: 0;\n }\n\n .rails-default-error-page div.dialog {\n width:
|
||||
95%;\n max-width: 33em;\n margin: 4em auto 0;\n }\n\n .rails-default-error-page
|
||||
div.dialog > div {\n border: 1px solid #CCC;\n border-right-color: #999;\n
|
||||
\ border-left-color: #999;\n border-bottom-color: #BBB;\n border-top:
|
||||
#B00100 solid 4px;\n border-top-left-radius: 9px;\n border-top-right-radius:
|
||||
9px;\n background-color: white;\n padding: 7px 12% 0;\n box-shadow:
|
||||
0 3px 8px rgba(50, 50, 50, 0.17);\n }\n\n .rails-default-error-page h1 {\n
|
||||
\ font-size: 100%;\n color: #730E15;\n line-height: 1.5em;\n }\n\n
|
||||
\ .rails-default-error-page div.dialog > p {\n margin: 0 0 1em;\n padding:
|
||||
1em;\n background-color: #F7F7F7;\n border: 1px solid #CCC;\n border-right-color:
|
||||
#999;\n border-left-color: #999;\n border-bottom-color: #999;\n border-bottom-left-radius:
|
||||
4px;\n border-bottom-right-radius: 4px;\n border-top-color: #DADADA;\n
|
||||
\ color: #666;\n box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);\n }\n
|
||||
\ </style>\n</head>\n\n<body class=\"rails-default-error-page\">\n <!-- This
|
||||
file lives in public/404.html -->\n <div class=\"dialog\">\n <div>\n <h1>The
|
||||
page you were looking for doesn't exist.</h1>\n <p>You may have mistyped
|
||||
the address or the page may have moved.</p>\n </div>\n <p>If you are
|
||||
the application owner check the logs for more information.</p>\n </div>\n</body>\n</html>\n"
|
||||
headers:
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '1722'
|
||||
Content-Type:
|
||||
- text/html; charset=UTF-8
|
||||
Date:
|
||||
- Wed, 06 Aug 2025 19:30:52 GMT
|
||||
strict-transport-security:
|
||||
- max-age=63072000; includeSubDomains
|
||||
x-request-id:
|
||||
- 3640ddcd-56a3-48cc-9a5a-110ebe34ac80
|
||||
x-runtime:
|
||||
- '0.007004'
|
||||
status:
|
||||
code: 404
|
||||
message: Not Found
|
||||
- request:
|
||||
body: '{"version": "0.152.0", "batch_id": "2487456d-e03a-4eae-92a1-e9779e8f06a1",
|
||||
"user_context": {"user_id": "anonymous", "organization_id": "", "session_id":
|
||||
"57ab4cf7-915a-4d4e-b01d-3791f418dd39", "trace_id": "c780f111-40df-4c4b-ae88-b09c0f7e3276"},
|
||||
"execution_metadata": {"crew_name": "Unknown Crew", "crewai_version": "0.152.0"},
|
||||
"events": [{"event_id": "55b93497-f7f7-4c2e-baf9-eeec358cf90f", "timestamp":
|
||||
"2025-08-06T19:30:52.588780+00:00", "type": "llm_call_started", "event_data":
|
||||
{"timestamp": "2025-08-06T19:30:52.473897+00:00", "type": "llm_call_started",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"task_name": null, "task_id": "020f12f8-4bcf-4e49-9bf9-d8fee70eaf6a", "agent_id":
|
||||
"126a2971-a630-405c-a0d9-72d2c46e8074", "agent_role": "Test Agent", "model":
|
||||
"gpt-4o-mini", "messages": [{"role": "system", "content": "You are Test Agent.
|
||||
Test backstory\nYour personal goal is: Test goal\nTo give my best complete final
|
||||
answer to the task respond using the exact following format:\n\nThought: I now
|
||||
can give a great answer\n..."}, {"role": "user", "content": "\nCurrent Task:
|
||||
Say hello to the world\n\nThis is the expected criteria for your final answer:
|
||||
hello world\nyou MUST return the actual complete content as the final answer,
|
||||
not a summary.\n\nBegin! This is ..."}], "tools": null, "callbacks": ["<crewai.utilities.token_counter_callback.TokenCalcHandler
|
||||
object at 0x11c5860f0>"], "available_functions": null}}, {"event_id": "967e03f5-fc65-477f-a1ba-4614acbd0527",
|
||||
"timestamp": "2025-08-06T19:30:52.588932+00:00", "type": "llm_call_started",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.473897+00:00", "type": "llm_call_started",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"task_name": null, "task_id": "020f12f8-4bcf-4e49-9bf9-d8fee70eaf6a", "agent_id":
|
||||
"126a2971-a630-405c-a0d9-72d2c46e8074", "agent_role": "Test Agent", "model":
|
||||
"gpt-4o-mini", "messages": [{"role": "system", "content": "You are Test Agent.
|
||||
Test backstory\nYour personal goal is: Test goal\nTo give my best complete final
|
||||
answer to the task respond using the exact following format:\n\nThought: I now
|
||||
can give a great answer\n..."}, {"role": "user", "content": "\nCurrent Task:
|
||||
Say hello to the world\n\nThis is the expected criteria for your final answer:
|
||||
hello world\nyou MUST return the actual complete content as the final answer,
|
||||
not a summary.\n\nBegin! This is ..."}], "tools": null, "callbacks": ["<crewai.utilities.token_counter_callback.TokenCalcHandler
|
||||
object at 0x11c5860f0>"], "available_functions": null}}, {"event_id": "f67f33c0-f9ac-450e-987e-bb48880b9b69",
|
||||
"timestamp": "2025-08-06T19:30:52.597813+00:00", "type": "llm_call_completed",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.597748+00:00", "type": "llm_call_completed",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"task_name": null, "task_id": "020f12f8-4bcf-4e49-9bf9-d8fee70eaf6a", "agent_id":
|
||||
"126a2971-a630-405c-a0d9-72d2c46e8074", "agent_role": "Test Agent", "messages":
|
||||
[{"role": "system", "content": "You are Test Agent. Test backstory\nYour personal
|
||||
goal is: Test goal\nTo give my best complete final answer to the task respond
|
||||
using the exact following format:\n\nThought: I now can give a great answer\n..."},
|
||||
{"role": "user", "content": "\nCurrent Task: Say hello to the world\n\nThis
|
||||
is the expected criteria for your final answer: hello world\nyou MUST return
|
||||
the actual complete content as the final answer, not a summary.\n\nBegin! This
|
||||
is ..."}], "response": "I now can give a great answer \nFinal Answer: hello
|
||||
world", "call_type": "<LLMCallType.LLM_CALL: ''llm_call''>", "response_cost":
|
||||
3.135e-05, "model": "gpt-4o-mini"}}, {"event_id": "0868106f-9491-4214-9674-4f9c5621875b",
|
||||
"timestamp": "2025-08-06T19:30:52.597885+00:00", "type": "llm_call_completed",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.597748+00:00", "type": "llm_call_completed",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"task_name": null, "task_id": "020f12f8-4bcf-4e49-9bf9-d8fee70eaf6a", "agent_id":
|
||||
"126a2971-a630-405c-a0d9-72d2c46e8074", "agent_role": "Test Agent", "messages":
|
||||
[{"role": "system", "content": "You are Test Agent. Test backstory\nYour personal
|
||||
goal is: Test goal\nTo give my best complete final answer to the task respond
|
||||
using the exact following format:\n\nThought: I now can give a great answer\n..."},
|
||||
{"role": "user", "content": "\nCurrent Task: Say hello to the world\n\nThis
|
||||
is the expected criteria for your final answer: hello world\nyou MUST return
|
||||
the actual complete content as the final answer, not a summary.\n\nBegin! This
|
||||
is ..."}], "response": "I now can give a great answer \nFinal Answer: hello
|
||||
world", "call_type": "<LLMCallType.LLM_CALL: ''llm_call''>", "response_cost":
|
||||
3.135e-05, "model": "gpt-4o-mini"}}, {"event_id": "4b68fa53-1829-4201-bfa2-9364dbd8fc51",
|
||||
"timestamp": "2025-08-06T19:30:52.598054+00:00", "type": "agent_execution_completed",
|
||||
"event_data": {"serialization_error": "Circular reference detected (id repeated)",
|
||||
"object_type": "AgentExecutionCompletedEvent"}}, {"event_id": "264a2a9a-c234-45f2-8021-5e4b1b7c4c98",
|
||||
"timestamp": "2025-08-06T19:30:52.598224+00:00", "type": "agent_execution_completed",
|
||||
"event_data": {"serialization_error": "Circular reference detected (id repeated)",
|
||||
"object_type": "AgentExecutionCompletedEvent"}}, {"event_id": "b8b7f244-e2c0-4cac-9411-0efab7b9936a",
|
||||
"timestamp": "2025-08-06T19:30:52.598372+00:00", "type": "task_completed", "event_data":
|
||||
{"serialization_error": "Circular reference detected (id repeated)", "object_type":
|
||||
"TaskCompletedEvent"}}, {"event_id": "c15489a3-31af-48bb-8f32-cb3c3f46f7b9",
|
||||
"timestamp": "2025-08-06T19:30:52.598416+00:00", "type": "task_completed", "event_data":
|
||||
{"serialization_error": "Circular reference detected (id repeated)", "object_type":
|
||||
"TaskCompletedEvent"}}, {"event_id": "6806c6bd-1399-4261-aa23-5b6166c2ab31",
|
||||
"timestamp": "2025-08-06T19:30:52.601018+00:00", "type": "crew_kickoff_completed",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.600994+00:00", "type": "crew_kickoff_completed",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"crew_name": "crew", "crew": null, "output": {"description": "Say hello to the
|
||||
world", "name": null, "expected_output": "hello world", "summary": "Say hello
|
||||
to the world...", "raw": "hello world", "pydantic": null, "json_dict": null,
|
||||
"agent": "Test Agent", "output_format": "raw"}, "total_tokens": 170}}]}'
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate, zstd
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '6503'
|
||||
Content-Type:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- CrewAI-CLI/0.152.0
|
||||
X-Crewai-Organization-Id:
|
||||
- d3a3d10c-35db-423f-a7a4-c026030ba64d
|
||||
X-Crewai-Version:
|
||||
- 0.152.0
|
||||
method: POST
|
||||
uri: https://app.crewai.com/crewai_plus/api/v1/tracing
|
||||
response:
|
||||
body:
|
||||
string: "<!DOCTYPE html>\n<html>\n<head>\n <title>The page you were looking
|
||||
for doesn't exist (404)</title>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n
|
||||
\ <style>\n .rails-default-error-page {\n background-color: #EFEFEF;\n
|
||||
\ color: #2E2F30;\n text-align: center;\n font-family: arial, sans-serif;\n
|
||||
\ margin: 0;\n }\n\n .rails-default-error-page div.dialog {\n width:
|
||||
95%;\n max-width: 33em;\n margin: 4em auto 0;\n }\n\n .rails-default-error-page
|
||||
div.dialog > div {\n border: 1px solid #CCC;\n border-right-color: #999;\n
|
||||
\ border-left-color: #999;\n border-bottom-color: #BBB;\n border-top:
|
||||
#B00100 solid 4px;\n border-top-left-radius: 9px;\n border-top-right-radius:
|
||||
9px;\n background-color: white;\n padding: 7px 12% 0;\n box-shadow:
|
||||
0 3px 8px rgba(50, 50, 50, 0.17);\n }\n\n .rails-default-error-page h1 {\n
|
||||
\ font-size: 100%;\n color: #730E15;\n line-height: 1.5em;\n }\n\n
|
||||
\ .rails-default-error-page div.dialog > p {\n margin: 0 0 1em;\n padding:
|
||||
1em;\n background-color: #F7F7F7;\n border: 1px solid #CCC;\n border-right-color:
|
||||
#999;\n border-left-color: #999;\n border-bottom-color: #999;\n border-bottom-left-radius:
|
||||
4px;\n border-bottom-right-radius: 4px;\n border-top-color: #DADADA;\n
|
||||
\ color: #666;\n box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);\n }\n
|
||||
\ </style>\n</head>\n\n<body class=\"rails-default-error-page\">\n <!-- This
|
||||
file lives in public/404.html -->\n <div class=\"dialog\">\n <div>\n <h1>The
|
||||
page you were looking for doesn't exist.</h1>\n <p>You may have mistyped
|
||||
the address or the page may have moved.</p>\n </div>\n <p>If you are
|
||||
the application owner check the logs for more information.</p>\n </div>\n</body>\n</html>\n"
|
||||
headers:
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '1722'
|
||||
Content-Type:
|
||||
- text/html; charset=UTF-8
|
||||
Date:
|
||||
- Wed, 06 Aug 2025 19:30:52 GMT
|
||||
strict-transport-security:
|
||||
- max-age=63072000; includeSubDomains
|
||||
x-request-id:
|
||||
- 50885aa8-a8e2-4a5a-87f7-6d0e2f2f80f9
|
||||
x-runtime:
|
||||
- '0.006464'
|
||||
status:
|
||||
code: 404
|
||||
message: Not Found
|
||||
version: 1
|
||||
@@ -0,0 +1,433 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages": [{"role": "system", "content": "You are Test Agent. Test backstory\nYour
|
||||
personal goal is: Test goal\nTo give my best complete final answer to the task
|
||||
respond using the exact following format:\n\nThought: I now can give a great
|
||||
answer\nFinal Answer: Your final answer must be the great and the most complete
|
||||
as possible, it must be outcome described.\n\nI MUST use these formats, my job
|
||||
depends on it!"}, {"role": "user", "content": "\nCurrent Task: Say hello to
|
||||
the world\n\nThis is the expected criteria for your final answer: hello world\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", "stop":
|
||||
["\nObservation:"]}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate, zstd
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '825'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.93.0
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.93.0
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
x-stainless-read-timeout:
|
||||
- '600.0'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.12.9
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFJdb9QwEHzPr1j8nKDkmvTavBUQnw8goQoJqKKts8kZHK9lOy1Q3X9H
|
||||
Tq6XFIrES6Ts7IxndvcuARCqFTUIucMgB6uz5wWduvz8g3nx/tnny3d4MRabX6flW9O9qj6KNDL4
|
||||
+hvJcM96KnmwmoJiM8PSEQaKqsW2Kqv8rCqrCRi4JR1pvQ1ZydmgjMo2+abM8m1WnB3YO1aSvKjh
|
||||
SwIAcDd9o0/T0g9RQ57eVwbyHnsS9bEJQDjWsSLQe+UDmiDSBZRsApnJ+hswfAsSDfTqhgChj7YB
|
||||
jb8lB/DVvFQGNVxM/zW8Jq05hU/sdPtkLemoGz3GWGbUegWgMRwwjmUKc3VA9kf7mnvr+Nr/QRWd
|
||||
MsrvGkfo2USrPrAVE7pPAK6mMY0PkgvreLChCfydpueKajvriWU7a/QABg6oV/XtJn1Er2kpoNJ+
|
||||
NWghUe6oXajLVnBsFa+AZJX6bzePac/Jlen/R34BpCQbqG2so1bJh4mXNkfxeP/VdpzyZFh4cjdK
|
||||
UhMUubiJljoc9XxSwv/0gYamU6YnZ52a76qzzUmJVYl0fiJFsk9+AwAA//8DABLzfQllAwAA
|
||||
headers:
|
||||
CF-RAY:
|
||||
- 96b0f0e62d177ad9-SJC
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 06 Aug 2025 19:29:05 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- __cf_bm=ePO5hy0kEoADCuKcboFy1iS1qckCE5KCpifQaXnlomM-1754508545-1.0.1.1-ieWfjcdIxQIXGfaMizvmgTvZPRFehqDXliegaOT7EO.kt7KSSFGmNDcC35_D9hOhE.fJ5K302uX0snQF3nLaapds2dqgGbNcsyFPOKNvAdI;
|
||||
path=/; expires=Wed, 06-Aug-25 19:59:05 GMT; domain=.api.openai.com; HttpOnly;
|
||||
Secure; SameSite=None
|
||||
- _cfuvid=NaXWifUGChHp6Ap1mvfMrNzmO4HdzddrqXkSR9T.hYo-1754508545647-0.0.1.1-604800000;
|
||||
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '526'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '568'
|
||||
x-ratelimit-limit-project-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-limit-requests:
|
||||
- '30000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-remaining-project-tokens:
|
||||
- '149999827'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '29999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '149999827'
|
||||
x-ratelimit-reset-project-tokens:
|
||||
- 0s
|
||||
x-ratelimit-reset-requests:
|
||||
- 2ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_0e70b38c85e144d289fbdf89082cf16e
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"messages": [{"role": "system", "content": "You are Test Agent. Test backstory\nYour
|
||||
personal goal is: Test goal\nTo give my best complete final answer to the task
|
||||
respond using the exact following format:\n\nThought: I now can give a great
|
||||
answer\nFinal Answer: Your final answer must be the great and the most complete
|
||||
as possible, it must be outcome described.\n\nI MUST use these formats, my job
|
||||
depends on it!"}, {"role": "user", "content": "\nCurrent Task: Say hello to
|
||||
the world\n\nThis is the expected criteria for your final answer: hello world\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"}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate, zstd
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '797'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.93.0
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.93.0
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
x-stainless-read-timeout:
|
||||
- '200.0'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.12.9
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFJdj9MwEHzPr1j8nKC0TejRN4TE3YmTQIAEEpyiPWeTujheYztX0Kn/
|
||||
HTnpNbkPJF4iZWdnPLO7dwmAULXYgJBbDLKzOnu7oFdu//nLr8v8Ij//1jdXq93Xjx8+7a7OzXuR
|
||||
Rgbf7EiGe9ZLyZ3VFBSbEZaOMFBUXazLoszPyqIcgI5r0pHW2pAVnHXKqGyZL4ssX2eLsyN7y0qS
|
||||
Fxv4ngAA3A3f6NPU9FtsIE/vKx15jy2JzakJQDjWsSLQe+UDmiDSCZRsApnB+iUY3oNEA626JUBo
|
||||
o21A4/fkAH6Yd8qghjfD/wYuSGtOYc9O1y/mko6a3mOMZXqtZwAawwHjWIYw10fkcLKvubWOb/wj
|
||||
qmiUUX5bOULPJlr1ga0Y0EMCcD2MqX+QXFjHnQ1V4J80PLco16OemLYzR49g4IB6Vl8v02f0qpoC
|
||||
Ku1ngxYS5ZbqiTptBfta8QxIZqmfunlOe0yuTPs/8hMgJdlAdWUd1Uo+TDy1OYrH+6+205QHw8KT
|
||||
u1WSqqDIxU3U1GCvx5MS/o8P1FWNMi0569R4V42tVgWWBdLrlRTJIfkLAAD//wMAE4F9LmUDAAA=
|
||||
headers:
|
||||
CF-RAY:
|
||||
- 96b0f0eadf69eb2c-SJC
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 06 Aug 2025 19:29:06 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- __cf_bm=f59gEPi_nA3TTxtjbKaSQpvkTwezaAqOvqfxiGzRnVQ-1754508546-1.0.1.1-JrSaytxVIQSVE00I.vyGj7d4HJbbMV6R9fWPJbkDKu0Y8ueMRzTwTUnfz0YzP5nsZX5oxoE6WlmFxOuz0rRuq9YhZZsO_TbaFBOFk1jGK9U;
|
||||
path=/; expires=Wed, 06-Aug-25 19:59:06 GMT; domain=.api.openai.com; HttpOnly;
|
||||
Secure; SameSite=None
|
||||
- _cfuvid=3D66v3.J_RcVoYy9dlF.jHwq1zTIm842xynZxzSy1Wc-1754508546352-0.0.1.1-604800000;
|
||||
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '504'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '527'
|
||||
x-ratelimit-limit-project-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-limit-requests:
|
||||
- '30000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-remaining-project-tokens:
|
||||
- '149999827'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '29999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '149999830'
|
||||
x-ratelimit-reset-project-tokens:
|
||||
- 0s
|
||||
x-ratelimit-reset-requests:
|
||||
- 2ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_32abf5c6f27e42579bc84b0bfcc9c4b4
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"trace_id": "e3677f76-4763-4f55-94b2-f38707f353c3", "execution_type":
|
||||
"crew", "execution_context": {"crew_fingerprint": null, "crew_name": "crew",
|
||||
"flow_name": "Unknown Flow", "crewai_version": "0.152.0", "privacy_level": "standard"},
|
||||
"execution_metadata": {"expected_duration_estimate": 300, "agent_count": 0,
|
||||
"task_count": 0, "flow_method_count": 0, "execution_started_at": "2025-08-06T19:30:52.778875+00:00"}}'
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate, zstd
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '413'
|
||||
Content-Type:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- CrewAI-CLI/0.152.0
|
||||
X-Crewai-Organization-Id:
|
||||
- d3a3d10c-35db-423f-a7a4-c026030ba64d
|
||||
X-Crewai-Version:
|
||||
- 0.152.0
|
||||
method: POST
|
||||
uri: https://app.crewai.com/crewai_plus/api/v1/tracing/batches
|
||||
response:
|
||||
body:
|
||||
string: "<!DOCTYPE html>\n<html>\n<head>\n <title>The page you were looking
|
||||
for doesn't exist (404)</title>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n
|
||||
\ <style>\n .rails-default-error-page {\n background-color: #EFEFEF;\n
|
||||
\ color: #2E2F30;\n text-align: center;\n font-family: arial, sans-serif;\n
|
||||
\ margin: 0;\n }\n\n .rails-default-error-page div.dialog {\n width:
|
||||
95%;\n max-width: 33em;\n margin: 4em auto 0;\n }\n\n .rails-default-error-page
|
||||
div.dialog > div {\n border: 1px solid #CCC;\n border-right-color: #999;\n
|
||||
\ border-left-color: #999;\n border-bottom-color: #BBB;\n border-top:
|
||||
#B00100 solid 4px;\n border-top-left-radius: 9px;\n border-top-right-radius:
|
||||
9px;\n background-color: white;\n padding: 7px 12% 0;\n box-shadow:
|
||||
0 3px 8px rgba(50, 50, 50, 0.17);\n }\n\n .rails-default-error-page h1 {\n
|
||||
\ font-size: 100%;\n color: #730E15;\n line-height: 1.5em;\n }\n\n
|
||||
\ .rails-default-error-page div.dialog > p {\n margin: 0 0 1em;\n padding:
|
||||
1em;\n background-color: #F7F7F7;\n border: 1px solid #CCC;\n border-right-color:
|
||||
#999;\n border-left-color: #999;\n border-bottom-color: #999;\n border-bottom-left-radius:
|
||||
4px;\n border-bottom-right-radius: 4px;\n border-top-color: #DADADA;\n
|
||||
\ color: #666;\n box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);\n }\n
|
||||
\ </style>\n</head>\n\n<body class=\"rails-default-error-page\">\n <!-- This
|
||||
file lives in public/404.html -->\n <div class=\"dialog\">\n <div>\n <h1>The
|
||||
page you were looking for doesn't exist.</h1>\n <p>You may have mistyped
|
||||
the address or the page may have moved.</p>\n </div>\n <p>If you are
|
||||
the application owner check the logs for more information.</p>\n </div>\n</body>\n</html>\n"
|
||||
headers:
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '1722'
|
||||
Content-Type:
|
||||
- text/html; charset=UTF-8
|
||||
Date:
|
||||
- Wed, 06 Aug 2025 19:30:52 GMT
|
||||
strict-transport-security:
|
||||
- max-age=63072000; includeSubDomains
|
||||
x-request-id:
|
||||
- 8c9c1556-d30f-4736-b62c-5a41150e859f
|
||||
x-runtime:
|
||||
- '0.005329'
|
||||
status:
|
||||
code: 404
|
||||
message: Not Found
|
||||
- request:
|
||||
body: '{"version": "0.152.0", "batch_id": "e3677f76-4763-4f55-94b2-f38707f353c3",
|
||||
"user_context": {"user_id": "anonymous", "organization_id": "", "session_id":
|
||||
"eb96086e-c3b3-4757-a118-328be61c9aad", "trace_id": "90245ff6-bd46-4e0e-83da-b12edd241b0e"},
|
||||
"execution_metadata": {"crew_name": "crew", "execution_start": "2025-08-06T19:30:52.777333+00:00",
|
||||
"crewai_version": "0.152.0"}, "events": [{"event_id": "d5c81b9a-b8a9-4638-ab50-aa91792b95c8",
|
||||
"timestamp": "2025-08-06T19:30:52.909777+00:00", "type": "crew_kickoff_started",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.777333+00:00", "type": "crew_kickoff_started",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"crew_name": "crew", "crew": null, "inputs": null}}, {"event_id": "a5bd314d-f9eb-471f-b3c4-e176e6ec62f9",
|
||||
"timestamp": "2025-08-06T19:30:52.911914+00:00", "type": "task_started", "event_data":
|
||||
{"task_description": "Say hello to the world", "task_name": null, "context":
|
||||
"", "agent": "Test Agent"}}, {"event_id": "ce0e41d9-90a9-4585-8dc3-c04ee02232bc",
|
||||
"timestamp": "2025-08-06T19:30:52.912403+00:00", "type": "agent_execution_started",
|
||||
"event_data": {"serialization_error": "Circular reference detected (id repeated)",
|
||||
"object_type": "AgentExecutionStartedEvent"}}, {"event_id": "d7c3546e-fe60-4c8a-9e4a-a510fa631a8b",
|
||||
"timestamp": "2025-08-06T19:30:52.912693+00:00", "type": "llm_call_started",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.912657+00:00", "type": "llm_call_started",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"task_name": null, "task_id": "e4abe414-b25d-44ea-8a0d-4998d7e55ed3", "agent_id":
|
||||
"91a39492-d0c8-4994-b8b4-acdd256a2e96", "agent_role": "Test Agent", "model":
|
||||
"gpt-4o-mini", "messages": [{"role": "system", "content": "You are Test Agent.
|
||||
Test backstory\nYour personal goal is: Test goal\nTo give my best complete final
|
||||
answer to the task respond using the exact following format:\n\nThought: I now
|
||||
can give a great answer\n..."}, {"role": "user", "content": "\nCurrent Task:
|
||||
Say hello to the world\n\nThis is the expected criteria for your final answer:
|
||||
hello world\nyou MUST return the actual complete content as the final answer,
|
||||
not a summary.\n\nBegin! This is ..."}], "tools": null, "callbacks": ["<crewai.utilities.token_counter_callback.TokenCalcHandler
|
||||
object at 0x11c5b5e50>"], "available_functions": null}}, {"event_id": "6ed9e994-3e66-4653-97e0-a9e8e8c1d978",
|
||||
"timestamp": "2025-08-06T19:30:52.919664+00:00", "type": "llm_call_completed",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.919623+00:00", "type": "llm_call_completed",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"task_name": null, "task_id": "e4abe414-b25d-44ea-8a0d-4998d7e55ed3", "agent_id":
|
||||
"91a39492-d0c8-4994-b8b4-acdd256a2e96", "agent_role": "Test Agent", "messages":
|
||||
[{"role": "system", "content": "You are Test Agent. Test backstory\nYour personal
|
||||
goal is: Test goal\nTo give my best complete final answer to the task respond
|
||||
using the exact following format:\n\nThought: I now can give a great answer\n..."},
|
||||
{"role": "user", "content": "\nCurrent Task: Say hello to the world\n\nThis
|
||||
is the expected criteria for your final answer: hello world\nyou MUST return
|
||||
the actual complete content as the final answer, not a summary.\n\nBegin! This
|
||||
is ..."}], "response": "I now can give a great answer \nFinal Answer: Hello,
|
||||
World!", "call_type": "<LLMCallType.LLM_CALL: ''llm_call''>", "response_cost":
|
||||
3.255e-05, "model": "gpt-4o-mini"}}, {"event_id": "2f03d7fe-2faf-4d6b-a9e1-d3cb9e87ef10",
|
||||
"timestamp": "2025-08-06T19:30:52.919798+00:00", "type": "agent_execution_completed",
|
||||
"event_data": {"serialization_error": "Circular reference detected (id repeated)",
|
||||
"object_type": "AgentExecutionCompletedEvent"}}, {"event_id": "c2ed7fa8-0361-406f-8a0f-4cf0f580dbee",
|
||||
"timestamp": "2025-08-06T19:30:52.919953+00:00", "type": "task_completed", "event_data":
|
||||
{"serialization_error": "Circular reference detected (id repeated)", "object_type":
|
||||
"TaskCompletedEvent"}}, {"event_id": "727d1ea2-4f7b-4d12-b491-42a27f3c3123",
|
||||
"timestamp": "2025-08-06T19:30:52.921547+00:00", "type": "crew_kickoff_completed",
|
||||
"event_data": {"timestamp": "2025-08-06T19:30:52.921522+00:00", "type": "crew_kickoff_completed",
|
||||
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
|
||||
"crew_name": "crew", "crew": null, "output": {"description": "Say hello to the
|
||||
world", "name": null, "expected_output": "hello world", "summary": "Say hello
|
||||
to the world...", "raw": "Hello, World!", "pydantic": null, "json_dict": null,
|
||||
"agent": "Test Agent", "output_format": "raw"}, "total_tokens": 172}}]}'
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate, zstd
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '4656'
|
||||
Content-Type:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- CrewAI-CLI/0.152.0
|
||||
X-Crewai-Organization-Id:
|
||||
- d3a3d10c-35db-423f-a7a4-c026030ba64d
|
||||
X-Crewai-Version:
|
||||
- 0.152.0
|
||||
method: POST
|
||||
uri: https://app.crewai.com/crewai_plus/api/v1/tracing
|
||||
response:
|
||||
body:
|
||||
string: "<!DOCTYPE html>\n<html>\n<head>\n <title>The page you were looking
|
||||
for doesn't exist (404)</title>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n
|
||||
\ <style>\n .rails-default-error-page {\n background-color: #EFEFEF;\n
|
||||
\ color: #2E2F30;\n text-align: center;\n font-family: arial, sans-serif;\n
|
||||
\ margin: 0;\n }\n\n .rails-default-error-page div.dialog {\n width:
|
||||
95%;\n max-width: 33em;\n margin: 4em auto 0;\n }\n\n .rails-default-error-page
|
||||
div.dialog > div {\n border: 1px solid #CCC;\n border-right-color: #999;\n
|
||||
\ border-left-color: #999;\n border-bottom-color: #BBB;\n border-top:
|
||||
#B00100 solid 4px;\n border-top-left-radius: 9px;\n border-top-right-radius:
|
||||
9px;\n background-color: white;\n padding: 7px 12% 0;\n box-shadow:
|
||||
0 3px 8px rgba(50, 50, 50, 0.17);\n }\n\n .rails-default-error-page h1 {\n
|
||||
\ font-size: 100%;\n color: #730E15;\n line-height: 1.5em;\n }\n\n
|
||||
\ .rails-default-error-page div.dialog > p {\n margin: 0 0 1em;\n padding:
|
||||
1em;\n background-color: #F7F7F7;\n border: 1px solid #CCC;\n border-right-color:
|
||||
#999;\n border-left-color: #999;\n border-bottom-color: #999;\n border-bottom-left-radius:
|
||||
4px;\n border-bottom-right-radius: 4px;\n border-top-color: #DADADA;\n
|
||||
\ color: #666;\n box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);\n }\n
|
||||
\ </style>\n</head>\n\n<body class=\"rails-default-error-page\">\n <!-- This
|
||||
file lives in public/404.html -->\n <div class=\"dialog\">\n <div>\n <h1>The
|
||||
page you were looking for doesn't exist.</h1>\n <p>You may have mistyped
|
||||
the address or the page may have moved.</p>\n </div>\n <p>If you are
|
||||
the application owner check the logs for more information.</p>\n </div>\n</body>\n</html>\n"
|
||||
headers:
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '1722'
|
||||
Content-Type:
|
||||
- text/html; charset=UTF-8
|
||||
Date:
|
||||
- Wed, 06 Aug 2025 19:30:53 GMT
|
||||
strict-transport-security:
|
||||
- max-age=63072000; includeSubDomains
|
||||
x-request-id:
|
||||
- 3b16d4bb-ba79-4a32-a776-26bbdf8d0a68
|
||||
x-runtime:
|
||||
- '0.005566'
|
||||
status:
|
||||
code: 404
|
||||
message: Not Found
|
||||
version: 1
|
||||
@@ -1,59 +0,0 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"contents": [{"role": "user", "parts": [{"text": "What is the capital
|
||||
of France?"}]}], "generationConfig": {"stop_sequences": []}}'
|
||||
headers:
|
||||
accept:
|
||||
- '*/*'
|
||||
accept-encoding:
|
||||
- gzip, deflate
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '131'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- generativelanguage.googleapis.com
|
||||
user-agent:
|
||||
- litellm/1.60.2
|
||||
method: POST
|
||||
uri: https://generativelanguage.googleapis.com/v1beta/models/gemma-3-12b-it:generateContent
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAC/2WRTWvDMAyG7/kVwpdBSEvX7jB23QfsMFa2MAZbD2qipGaOFWwFWkr/+5ykaVPq
|
||||
gGP0SvLrR/sIQGVoc52jkFcP8BMiAPtubzW2QlaCMIRCsEYn59x+7UfnkCK0bYtUuiHIsNaCBriA
|
||||
F4c2I9Ae4niJTvs4nv7a/nuVGw9oPIOEIoOuJC+QadmBtkNlsAoIpeF1aJgFZ+SgYAfBUQIF+o1m
|
||||
m0CJXhxbrnZJV5E1RhpHUzUyeTidV8n5aY4Ntb4rzskM6YchQRXaar/5IPRs27TP9H2pTqq2OW1D
|
||||
eBYNF3StVeOxpDcSDJDxhFLVjqtaUv4j+8hNB/m+7zUayYW8mB914QD0QrqbJVdd/VO4U5vxqEZT
|
||||
DE9EE+h2Y3r+TtUIg1yYGjB0/1V0BNIz+iLndQ+jpKrCyWJyO19PtKjoEP0DlZtdIF8CAAA=
|
||||
headers:
|
||||
Alt-Svc:
|
||||
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json; charset=UTF-8
|
||||
Date:
|
||||
- Tue, 22 Apr 2025 14:25:39 GMT
|
||||
Server:
|
||||
- scaffolding on HTTPServer2
|
||||
Server-Timing:
|
||||
- gfet4t7; dur=3835
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Vary:
|
||||
- Origin
|
||||
- X-Origin
|
||||
- Referer
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-XSS-Protection:
|
||||
- '0'
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -1,60 +0,0 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"contents": [{"role": "user", "parts": [{"text": "What is the capital
|
||||
of France?"}]}], "generationConfig": {"stop_sequences": []}}'
|
||||
headers:
|
||||
accept:
|
||||
- '*/*'
|
||||
accept-encoding:
|
||||
- gzip, deflate
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '131'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- generativelanguage.googleapis.com
|
||||
user-agent:
|
||||
- litellm/1.60.2
|
||||
method: POST
|
||||
uri: https://generativelanguage.googleapis.com/v1beta/models/gemma-3-1b-it:generateContent
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAC/2VRy07DQAy85yusPUZtBSoIxJWHxAFRQYSQKAc3cVqL7DrKuqKlqsRv8Ht8CZuk
|
||||
aVOxh314xuP1eBMBmBRdxhkqeXMFbyECsGn2GhOn5DQAXSgES6z0wG3XpncPFKVVnWSSBUGKJSsW
|
||||
IDncVehSAvYQxxOs2MfxCKZu6u719/vHA0Iq1ooDyz6UTqlUDi9doELDr1M1aMbiinXcSQ9gtlTg
|
||||
VqOGJc855VAztAZWvMInZ1SsoaJU5o6/KANxNDK9X2/39/fBoddKCqobsRLyO/q2I5icHfvFE6EX
|
||||
V9Oek8eJ2aPsMlqF8EnUFWikzdLjnB5IMbiOe29NWYktNZEPcteybFy/bLV6MzqCxxc7XCXYcASd
|
||||
nQ/+qfqbUJOL/ux6Yw0tYsG6buZ2+5qYng169KnOhuZ8j3aGtB69UOW5NWNO1uJwPDydDVlNtI3+
|
||||
AD6XWQdvAgAA
|
||||
headers:
|
||||
Alt-Svc:
|
||||
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json; charset=UTF-8
|
||||
Date:
|
||||
- Tue, 22 Apr 2025 14:25:32 GMT
|
||||
Server:
|
||||
- scaffolding on HTTPServer2
|
||||
Server-Timing:
|
||||
- gfet4t7; dur=1535
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Vary:
|
||||
- Origin
|
||||
- X-Origin
|
||||
- Referer
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-XSS-Protection:
|
||||
- '0'
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -6,7 +6,7 @@ interactions:
|
||||
accept:
|
||||
- '*/*'
|
||||
accept-encoding:
|
||||
- gzip, deflate
|
||||
- gzip, deflate, zstd
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
@@ -16,19 +16,19 @@ interactions:
|
||||
host:
|
||||
- generativelanguage.googleapis.com
|
||||
user-agent:
|
||||
- litellm/1.60.2
|
||||
- litellm/1.74.9
|
||||
method: POST
|
||||
uri: https://generativelanguage.googleapis.com/v1beta/models/gemma-3-27b-it:generateContent
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAC/2VRXUvDMBR976+45EUo3RDnUHwTnSA4HFpEcHuI7e16aZqU5NZNxv67abtuHTbQ
|
||||
hHPu5zm7AEAkUqeUSkYn7uDLIwC79t9wRjNq9kQPebCSlk+x3bcbvH0I47ZJEnGOkMiKWCowGTxZ
|
||||
qRMEchCGC2nJheEYlnqpn/nCQaHNRkNmLJDvSwkoP1kpbeFAUYHAvtiMsgwVxGaDNmqRF1P/WIR5
|
||||
7bAuI/ApLXxvE0gRYkumrHL0hIMNKtXcxA4y6XIyOoKkJkcau8ykVlxbHDczNUcM1tof36voJIY1
|
||||
CptNS5Oi6sP3fYDISJPL31A6o5uw9/h1IY4s6RS3Hr4M+gZtaVE7ucY5svS2yKP4orJ+F45NgfrB
|
||||
1K0tt12tgYln9PX0wLPxFpxR00n0r6p79D1JDc0d+O5XlIr4tzV29hmLgQx8NlQvQ3uvgoMgnUYf
|
||||
aB11YqyxLOVoMrq6+R4Ri2Af/AEDrXcbkQIAAA==
|
||||
H4sIAAAAAAAC/21RwWrbQBC96yuGvRSMFYp7aOktxAnY1MQ0ahtIfJhII3vwalfZGdUpxv/elRQ5
|
||||
CkQL2uXNm7f75h0TAJOjK7hAJTHf4SEiAMfu39a8U3IaCwMUwRqDvnH77zg6R4rSS9tksh1BjjUr
|
||||
WvAl3AR0OQELTCZrDCyTyQU8uke30E8Ce+cPDkofgOO9nIONL6sw7AUs7wk0il1zWZKFzB8oTDvk
|
||||
h2/+BoJVI9RUU4gtHXwZcigIssC+qncUCwIHsrbdWQVKlB17N4W8YWFHfWfeWG0CXbRvapcZ2Tqd
|
||||
z5vp2zCCt9Q6rXxBdqCfBoIp2bHsfhKKdy3tLrtdm3OVXUEvEf6cDBd00qYR3NKKFGMseB6+qUP0
|
||||
opnfk7vyTRfLt17LqI8j/rAyapJ5lGQ7zm4Ua3SAlvVfl9v1fWZGLvWd8uCy2zfJq99+BL8pCPde
|
||||
t1RVmH5JZ1+fUtZOzgSS2juhRdEylvgnw+VTMU/T5bPKmos7nV3+Mskp+Q+x/LCbmwIAAA==
|
||||
headers:
|
||||
Alt-Svc:
|
||||
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
|
||||
@@ -37,11 +37,11 @@ interactions:
|
||||
Content-Type:
|
||||
- application/json; charset=UTF-8
|
||||
Date:
|
||||
- Tue, 22 Apr 2025 14:25:41 GMT
|
||||
- Wed, 06 Aug 2025 18:55:33 GMT
|
||||
Server:
|
||||
- scaffolding on HTTPServer2
|
||||
Server-Timing:
|
||||
- gfet4t7; dur=2447
|
||||
- gfet4t7; dur=1529
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Vary:
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"contents": [{"role": "user", "parts": [{"text": "What is the capital
|
||||
of France?"}]}], "generationConfig": {"stop_sequences": []}}'
|
||||
headers:
|
||||
accept:
|
||||
- '*/*'
|
||||
accept-encoding:
|
||||
- gzip, deflate
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '131'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- generativelanguage.googleapis.com
|
||||
user-agent:
|
||||
- litellm/1.60.2
|
||||
method: POST
|
||||
uri: https://generativelanguage.googleapis.com/v1beta/models/gemma-3-4b-it:generateContent
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAC/2WRzUrDQBDH73mKYY+lLUKLiBcPfoAHsWhQwXqYJtNk6WYn7E6woRQ8+wR68t18
|
||||
Ah/BbWraFPeQXeb/z3z8ZhUBqARtqlMU8uoUnkMEYNV8NxpbIStBaEMhWKKTvXd7Vp13sAgtNz+p
|
||||
OCdIsNSCBngOVw5tQqA99HoTdNr3ekOY2qm9lu+3Tw8ImeFZ8CahKDmYs4NQrA9z9Llm24cMvTi2
|
||||
XNQQ2oakMlI5GsLP18d7k+mRK5NCzRUYvSAQhoXl12CuJdc2g4IdAc64Emg6OFOdzte790t/P69j
|
||||
Q5thCk7JtPZ1a1BzbbXP7wg9243tPr6dqJ2qbUrLED6K2gJNalV5zOiGBAN53PFVpeOilJgXZM+5
|
||||
asifbHN19nQgj1pdOFA+kMbH/X9Z/UWoqU13f53VhhHRaKmb3V0+xaqDQQ6aajE090v0B2TL6IGc
|
||||
11sYGRUFDkaD8WygRUXr6BcxmBLccwIAAA==
|
||||
headers:
|
||||
Alt-Svc:
|
||||
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json; charset=UTF-8
|
||||
Date:
|
||||
- Tue, 22 Apr 2025 14:25:35 GMT
|
||||
Server:
|
||||
- scaffolding on HTTPServer2
|
||||
Server-Timing:
|
||||
- gfet4t7; dur=2349
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Vary:
|
||||
- Origin
|
||||
- X-Origin
|
||||
- Referer
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
X-Frame-Options:
|
||||
- SAMEORIGIN
|
||||
X-XSS-Protection:
|
||||
- '0'
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
91
tests/cli/authentication/providers/test_auth0.py
Normal file
91
tests/cli/authentication/providers/test_auth0.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import pytest
|
||||
from crewai.cli.authentication.main import Oauth2Settings
|
||||
from crewai.cli.authentication.providers.auth0 import Auth0Provider
|
||||
|
||||
|
||||
|
||||
class TestAuth0Provider:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_method(self):
|
||||
self.valid_settings = Oauth2Settings(
|
||||
provider="auth0",
|
||||
domain="test-domain.auth0.com",
|
||||
client_id="test-client-id",
|
||||
audience="test-audience"
|
||||
)
|
||||
self.provider = Auth0Provider(self.valid_settings)
|
||||
|
||||
def test_initialization_with_valid_settings(self):
|
||||
provider = Auth0Provider(self.valid_settings)
|
||||
assert provider.settings == self.valid_settings
|
||||
assert provider.settings.provider == "auth0"
|
||||
assert provider.settings.domain == "test-domain.auth0.com"
|
||||
assert provider.settings.client_id == "test-client-id"
|
||||
assert provider.settings.audience == "test-audience"
|
||||
|
||||
def test_get_authorize_url(self):
|
||||
expected_url = "https://test-domain.auth0.com/oauth/device/code"
|
||||
assert self.provider.get_authorize_url() == expected_url
|
||||
|
||||
def test_get_authorize_url_with_different_domain(self):
|
||||
settings = Oauth2Settings(
|
||||
provider="auth0",
|
||||
domain="my-company.auth0.com",
|
||||
client_id="test-client",
|
||||
audience="test-audience"
|
||||
)
|
||||
provider = Auth0Provider(settings)
|
||||
expected_url = "https://my-company.auth0.com/oauth/device/code"
|
||||
assert provider.get_authorize_url() == expected_url
|
||||
|
||||
def test_get_token_url(self):
|
||||
expected_url = "https://test-domain.auth0.com/oauth/token"
|
||||
assert self.provider.get_token_url() == expected_url
|
||||
|
||||
def test_get_token_url_with_different_domain(self):
|
||||
settings = Oauth2Settings(
|
||||
provider="auth0",
|
||||
domain="another-domain.auth0.com",
|
||||
client_id="test-client",
|
||||
audience="test-audience"
|
||||
)
|
||||
provider = Auth0Provider(settings)
|
||||
expected_url = "https://another-domain.auth0.com/oauth/token"
|
||||
assert provider.get_token_url() == expected_url
|
||||
|
||||
def test_get_jwks_url(self):
|
||||
expected_url = "https://test-domain.auth0.com/.well-known/jwks.json"
|
||||
assert self.provider.get_jwks_url() == expected_url
|
||||
|
||||
def test_get_jwks_url_with_different_domain(self):
|
||||
settings = Oauth2Settings(
|
||||
provider="auth0",
|
||||
domain="dev.auth0.com",
|
||||
client_id="test-client",
|
||||
audience="test-audience"
|
||||
)
|
||||
provider = Auth0Provider(settings)
|
||||
expected_url = "https://dev.auth0.com/.well-known/jwks.json"
|
||||
assert provider.get_jwks_url() == expected_url
|
||||
|
||||
def test_get_issuer(self):
|
||||
expected_issuer = "https://test-domain.auth0.com/"
|
||||
assert self.provider.get_issuer() == expected_issuer
|
||||
|
||||
def test_get_issuer_with_different_domain(self):
|
||||
settings = Oauth2Settings(
|
||||
provider="auth0",
|
||||
domain="prod.auth0.com",
|
||||
client_id="test-client",
|
||||
audience="test-audience"
|
||||
)
|
||||
provider = Auth0Provider(settings)
|
||||
expected_issuer = "https://prod.auth0.com/"
|
||||
assert provider.get_issuer() == expected_issuer
|
||||
|
||||
def test_get_audience(self):
|
||||
assert self.provider.get_audience() == "test-audience"
|
||||
|
||||
def test_get_client_id(self):
|
||||
assert self.provider.get_client_id() == "test-client-id"
|
||||
102
tests/cli/authentication/providers/test_okta.py
Normal file
102
tests/cli/authentication/providers/test_okta.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import pytest
|
||||
from crewai.cli.authentication.main import Oauth2Settings
|
||||
from crewai.cli.authentication.providers.okta import OktaProvider
|
||||
|
||||
|
||||
class TestOktaProvider:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_method(self):
|
||||
self.valid_settings = Oauth2Settings(
|
||||
provider="okta",
|
||||
domain="test-domain.okta.com",
|
||||
client_id="test-client-id",
|
||||
audience="test-audience"
|
||||
)
|
||||
self.provider = OktaProvider(self.valid_settings)
|
||||
|
||||
def test_initialization_with_valid_settings(self):
|
||||
provider = OktaProvider(self.valid_settings)
|
||||
assert provider.settings == self.valid_settings
|
||||
assert provider.settings.provider == "okta"
|
||||
assert provider.settings.domain == "test-domain.okta.com"
|
||||
assert provider.settings.client_id == "test-client-id"
|
||||
assert provider.settings.audience == "test-audience"
|
||||
|
||||
def test_get_authorize_url(self):
|
||||
expected_url = "https://test-domain.okta.com/oauth2/default/v1/device/authorize"
|
||||
assert self.provider.get_authorize_url() == expected_url
|
||||
|
||||
def test_get_authorize_url_with_different_domain(self):
|
||||
settings = Oauth2Settings(
|
||||
provider="okta",
|
||||
domain="my-company.okta.com",
|
||||
client_id="test-client",
|
||||
audience="test-audience"
|
||||
)
|
||||
provider = OktaProvider(settings)
|
||||
expected_url = "https://my-company.okta.com/oauth2/default/v1/device/authorize"
|
||||
assert provider.get_authorize_url() == expected_url
|
||||
|
||||
def test_get_token_url(self):
|
||||
expected_url = "https://test-domain.okta.com/oauth2/default/v1/token"
|
||||
assert self.provider.get_token_url() == expected_url
|
||||
|
||||
def test_get_token_url_with_different_domain(self):
|
||||
settings = Oauth2Settings(
|
||||
provider="okta",
|
||||
domain="another-domain.okta.com",
|
||||
client_id="test-client",
|
||||
audience="test-audience"
|
||||
)
|
||||
provider = OktaProvider(settings)
|
||||
expected_url = "https://another-domain.okta.com/oauth2/default/v1/token"
|
||||
assert provider.get_token_url() == expected_url
|
||||
|
||||
def test_get_jwks_url(self):
|
||||
expected_url = "https://test-domain.okta.com/oauth2/default/v1/keys"
|
||||
assert self.provider.get_jwks_url() == expected_url
|
||||
|
||||
def test_get_jwks_url_with_different_domain(self):
|
||||
settings = Oauth2Settings(
|
||||
provider="okta",
|
||||
domain="dev.okta.com",
|
||||
client_id="test-client",
|
||||
audience="test-audience"
|
||||
)
|
||||
provider = OktaProvider(settings)
|
||||
expected_url = "https://dev.okta.com/oauth2/default/v1/keys"
|
||||
assert provider.get_jwks_url() == expected_url
|
||||
|
||||
def test_get_issuer(self):
|
||||
expected_issuer = "https://test-domain.okta.com/oauth2/default"
|
||||
assert self.provider.get_issuer() == expected_issuer
|
||||
|
||||
def test_get_issuer_with_different_domain(self):
|
||||
settings = Oauth2Settings(
|
||||
provider="okta",
|
||||
domain="prod.okta.com",
|
||||
client_id="test-client",
|
||||
audience="test-audience"
|
||||
)
|
||||
provider = OktaProvider(settings)
|
||||
expected_issuer = "https://prod.okta.com/oauth2/default"
|
||||
assert provider.get_issuer() == expected_issuer
|
||||
|
||||
def test_get_audience(self):
|
||||
assert self.provider.get_audience() == "test-audience"
|
||||
|
||||
def test_get_audience_assertion_error_when_none(self):
|
||||
settings = Oauth2Settings(
|
||||
provider="okta",
|
||||
domain="test-domain.okta.com",
|
||||
client_id="test-client-id",
|
||||
audience=None
|
||||
)
|
||||
provider = OktaProvider(settings)
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
provider.get_audience()
|
||||
|
||||
def test_get_client_id(self):
|
||||
assert self.provider.get_client_id() == "test-client-id"
|
||||
100
tests/cli/authentication/providers/test_workos.py
Normal file
100
tests/cli/authentication/providers/test_workos.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import pytest
|
||||
from crewai.cli.authentication.main import Oauth2Settings
|
||||
from crewai.cli.authentication.providers.workos import WorkosProvider
|
||||
|
||||
|
||||
class TestWorkosProvider:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_method(self):
|
||||
self.valid_settings = Oauth2Settings(
|
||||
provider="workos",
|
||||
domain="login.company.com",
|
||||
client_id="test-client-id",
|
||||
audience="test-audience"
|
||||
)
|
||||
self.provider = WorkosProvider(self.valid_settings)
|
||||
|
||||
def test_initialization_with_valid_settings(self):
|
||||
provider = WorkosProvider(self.valid_settings)
|
||||
assert provider.settings == self.valid_settings
|
||||
assert provider.settings.provider == "workos"
|
||||
assert provider.settings.domain == "login.company.com"
|
||||
assert provider.settings.client_id == "test-client-id"
|
||||
assert provider.settings.audience == "test-audience"
|
||||
|
||||
def test_get_authorize_url(self):
|
||||
expected_url = "https://login.company.com/oauth2/device_authorization"
|
||||
assert self.provider.get_authorize_url() == expected_url
|
||||
|
||||
def test_get_authorize_url_with_different_domain(self):
|
||||
settings = Oauth2Settings(
|
||||
provider="workos",
|
||||
domain="login.example.com",
|
||||
client_id="test-client",
|
||||
audience="test-audience"
|
||||
)
|
||||
provider = WorkosProvider(settings)
|
||||
expected_url = "https://login.example.com/oauth2/device_authorization"
|
||||
assert provider.get_authorize_url() == expected_url
|
||||
|
||||
def test_get_token_url(self):
|
||||
expected_url = "https://login.company.com/oauth2/token"
|
||||
assert self.provider.get_token_url() == expected_url
|
||||
|
||||
def test_get_token_url_with_different_domain(self):
|
||||
settings = Oauth2Settings(
|
||||
provider="workos",
|
||||
domain="api.workos.com",
|
||||
client_id="test-client",
|
||||
audience="test-audience"
|
||||
)
|
||||
provider = WorkosProvider(settings)
|
||||
expected_url = "https://api.workos.com/oauth2/token"
|
||||
assert provider.get_token_url() == expected_url
|
||||
|
||||
def test_get_jwks_url(self):
|
||||
expected_url = "https://login.company.com/oauth2/jwks"
|
||||
assert self.provider.get_jwks_url() == expected_url
|
||||
|
||||
def test_get_jwks_url_with_different_domain(self):
|
||||
settings = Oauth2Settings(
|
||||
provider="workos",
|
||||
domain="auth.enterprise.com",
|
||||
client_id="test-client",
|
||||
audience="test-audience"
|
||||
)
|
||||
provider = WorkosProvider(settings)
|
||||
expected_url = "https://auth.enterprise.com/oauth2/jwks"
|
||||
assert provider.get_jwks_url() == expected_url
|
||||
|
||||
def test_get_issuer(self):
|
||||
expected_issuer = "https://login.company.com"
|
||||
assert self.provider.get_issuer() == expected_issuer
|
||||
|
||||
def test_get_issuer_with_different_domain(self):
|
||||
settings = Oauth2Settings(
|
||||
provider="workos",
|
||||
domain="sso.company.com",
|
||||
client_id="test-client",
|
||||
audience="test-audience"
|
||||
)
|
||||
provider = WorkosProvider(settings)
|
||||
expected_issuer = "https://sso.company.com"
|
||||
assert provider.get_issuer() == expected_issuer
|
||||
|
||||
def test_get_audience(self):
|
||||
assert self.provider.get_audience() == "test-audience"
|
||||
|
||||
def test_get_audience_fallback_to_default(self):
|
||||
settings = Oauth2Settings(
|
||||
provider="workos",
|
||||
domain="login.company.com",
|
||||
client_id="test-client-id",
|
||||
audience=None
|
||||
)
|
||||
provider = WorkosProvider(settings)
|
||||
assert provider.get_audience() == ""
|
||||
|
||||
def test_get_client_id(self):
|
||||
assert self.provider.get_client_id() == "test-client-id"
|
||||
@@ -6,10 +6,12 @@ from crewai.cli.authentication.main import AuthenticationCommand
|
||||
from crewai.cli.authentication.constants import (
|
||||
AUTH0_AUDIENCE,
|
||||
AUTH0_CLIENT_ID,
|
||||
AUTH0_DOMAIN,
|
||||
WORKOS_DOMAIN,
|
||||
WORKOS_CLI_CONNECT_APP_ID,
|
||||
WORKOS_ENVIRONMENT_ID,
|
||||
AUTH0_DOMAIN
|
||||
)
|
||||
from crewai.cli.constants import (
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_CLIENT_ID,
|
||||
CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE,
|
||||
)
|
||||
|
||||
|
||||
@@ -27,14 +29,17 @@ class TestAuthenticationCommand:
|
||||
"token_url": f"https://{AUTH0_DOMAIN}/oauth/token",
|
||||
"client_id": AUTH0_CLIENT_ID,
|
||||
"audience": AUTH0_AUDIENCE,
|
||||
"domain": AUTH0_DOMAIN,
|
||||
},
|
||||
),
|
||||
(
|
||||
"workos",
|
||||
{
|
||||
"device_code_url": f"https://{WORKOS_DOMAIN}/oauth2/device_authorization",
|
||||
"token_url": f"https://{WORKOS_DOMAIN}/oauth2/token",
|
||||
"client_id": WORKOS_CLI_CONNECT_APP_ID,
|
||||
"device_code_url": f"https://{CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN}/oauth2/device_authorization",
|
||||
"token_url": f"https://{CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN}/oauth2/token",
|
||||
"client_id": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_CLIENT_ID,
|
||||
"audience": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE,
|
||||
"domain": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN,
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -70,19 +75,16 @@ class TestAuthenticationCommand:
|
||||
"Signing in to CrewAI Enterprise...\n", style="bold blue"
|
||||
)
|
||||
mock_determine_provider.assert_called_once()
|
||||
mock_get_device.assert_called_once_with(
|
||||
expected_urls["client_id"],
|
||||
expected_urls["device_code_url"],
|
||||
expected_urls.get("audience", None),
|
||||
)
|
||||
mock_get_device.assert_called_once()
|
||||
mock_display.assert_called_once_with(
|
||||
{"device_code": "test_code", "user_code": "123456"}
|
||||
)
|
||||
mock_poll.assert_called_once_with(
|
||||
{"device_code": "test_code", "user_code": "123456"},
|
||||
expected_urls["client_id"],
|
||||
expected_urls["token_url"],
|
||||
)
|
||||
assert self.auth_command.oauth2_provider.get_client_id() == expected_urls["client_id"]
|
||||
assert self.auth_command.oauth2_provider.get_audience() == expected_urls["audience"]
|
||||
assert self.auth_command.oauth2_provider._get_domain() == expected_urls["domain"]
|
||||
|
||||
@patch("crewai.cli.authentication.main.webbrowser")
|
||||
@patch("crewai.cli.authentication.main.console.print")
|
||||
@@ -115,9 +117,9 @@ class TestAuthenticationCommand:
|
||||
(
|
||||
"workos",
|
||||
{
|
||||
"jwks_url": f"https://{WORKOS_DOMAIN}/oauth2/jwks",
|
||||
"issuer": f"https://{WORKOS_DOMAIN}",
|
||||
"audience": WORKOS_ENVIRONMENT_ID,
|
||||
"jwks_url": f"https://{CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN}/oauth2/jwks",
|
||||
"issuer": f"https://{CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN}",
|
||||
"audience": CREWAI_ENTERPRISE_DEFAULT_OAUTH2_AUDIENCE,
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -133,7 +135,15 @@ class TestAuthenticationCommand:
|
||||
jwt_config,
|
||||
has_expiration,
|
||||
):
|
||||
self.auth_command.user_provider = user_provider
|
||||
from crewai.cli.authentication.providers.auth0 import Auth0Provider
|
||||
from crewai.cli.authentication.providers.workos import WorkosProvider
|
||||
from crewai.cli.authentication.main import Oauth2Settings
|
||||
|
||||
if user_provider == "auth0":
|
||||
self.auth_command.oauth2_provider = Auth0Provider(settings=Oauth2Settings(provider=user_provider, client_id="test-client-id", domain=AUTH0_DOMAIN, audience=jwt_config["audience"]))
|
||||
elif user_provider == "workos":
|
||||
self.auth_command.oauth2_provider = WorkosProvider(settings=Oauth2Settings(provider=user_provider, client_id="test-client-id", domain=CREWAI_ENTERPRISE_DEFAULT_OAUTH2_DOMAIN, audience=jwt_config["audience"]))
|
||||
|
||||
token_data = {"access_token": "test_access_token", "id_token": "test_id_token"}
|
||||
|
||||
if has_expiration:
|
||||
@@ -311,11 +321,12 @@ class TestAuthenticationCommand:
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
result = self.auth_command._get_device_code(
|
||||
client_id="test_client",
|
||||
device_code_url="https://example.com/device",
|
||||
audience="test_audience",
|
||||
)
|
||||
self.auth_command.oauth2_provider = MagicMock()
|
||||
self.auth_command.oauth2_provider.get_client_id.return_value = "test_client"
|
||||
self.auth_command.oauth2_provider.get_authorize_url.return_value = "https://example.com/device"
|
||||
self.auth_command.oauth2_provider.get_audience.return_value = "test_audience"
|
||||
|
||||
result = self.auth_command._get_device_code()
|
||||
|
||||
mock_post.assert_called_once_with(
|
||||
url="https://example.com/device",
|
||||
@@ -354,8 +365,12 @@ class TestAuthenticationCommand:
|
||||
self.auth_command, "_login_to_tool_repository"
|
||||
) as mock_tool_login,
|
||||
):
|
||||
self.auth_command.oauth2_provider = MagicMock()
|
||||
self.auth_command.oauth2_provider.get_token_url.return_value = "https://example.com/token"
|
||||
self.auth_command.oauth2_provider.get_client_id.return_value = "test_client"
|
||||
|
||||
self.auth_command._poll_for_token(
|
||||
device_code_data, "test_client", "https://example.com/token"
|
||||
device_code_data
|
||||
)
|
||||
|
||||
mock_post.assert_called_once_with(
|
||||
@@ -392,7 +407,7 @@ class TestAuthenticationCommand:
|
||||
}
|
||||
|
||||
self.auth_command._poll_for_token(
|
||||
device_code_data, "test_client", "https://example.com/token"
|
||||
device_code_data
|
||||
)
|
||||
|
||||
mock_console_print.assert_any_call(
|
||||
@@ -415,5 +430,14 @@ class TestAuthenticationCommand:
|
||||
|
||||
with pytest.raises(requests.HTTPError):
|
||||
self.auth_command._poll_for_token(
|
||||
device_code_data, "test_client", "https://example.com/token"
|
||||
device_code_data
|
||||
)
|
||||
# @patch(
|
||||
# "crewai.cli.authentication.main.AuthenticationCommand._determine_user_provider"
|
||||
# )
|
||||
# def test_login_with_auth0(self, mock_determine_provider):
|
||||
# from crewai.cli.authentication.providers.auth0 import Auth0Provider
|
||||
# from crewai.cli.authentication.main import Oauth2Settings
|
||||
|
||||
# self.auth_command.oauth2_provider = Auth0Provider(settings=Oauth2Settings(provider="auth0", client_id=AUTH0_CLIENT_ID, domain=AUTH0_DOMAIN, audience=AUTH0_AUDIENCE))
|
||||
# self.auth_command.login()
|
||||
|
||||
@@ -4,7 +4,12 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from crewai.cli.config import Settings
|
||||
from crewai.cli.config import (
|
||||
Settings,
|
||||
USER_SETTINGS_KEYS,
|
||||
CLI_SETTINGS_KEYS,
|
||||
DEFAULT_CLI_SETTINGS,
|
||||
)
|
||||
|
||||
|
||||
class TestSettings(unittest.TestCase):
|
||||
@@ -52,6 +57,30 @@ class TestSettings(unittest.TestCase):
|
||||
self.assertEqual(settings.tool_repository_username, "new_user")
|
||||
self.assertEqual(settings.tool_repository_password, "file_pass")
|
||||
|
||||
def test_clear_user_settings(self):
|
||||
user_settings = {key: f"value_for_{key}" for key in USER_SETTINGS_KEYS}
|
||||
|
||||
settings = Settings(config_path=self.config_path, **user_settings)
|
||||
settings.clear_user_settings()
|
||||
|
||||
for key in user_settings.keys():
|
||||
self.assertEqual(getattr(settings, key), None)
|
||||
|
||||
def test_reset_settings(self):
|
||||
user_settings = {key: f"value_for_{key}" for key in USER_SETTINGS_KEYS}
|
||||
cli_settings = {key: f"value_for_{key}" for key in CLI_SETTINGS_KEYS}
|
||||
|
||||
settings = Settings(
|
||||
config_path=self.config_path, **user_settings, **cli_settings
|
||||
)
|
||||
|
||||
settings.reset()
|
||||
|
||||
for key in user_settings.keys():
|
||||
self.assertEqual(getattr(settings, key), None)
|
||||
for key in cli_settings.keys():
|
||||
self.assertEqual(getattr(settings, key), DEFAULT_CLI_SETTINGS.get(key))
|
||||
|
||||
def test_dump_new_settings(self):
|
||||
settings = Settings(
|
||||
config_path=self.config_path, tool_repository_username="user1"
|
||||
|
||||
@@ -6,7 +6,7 @@ from click.testing import CliRunner
|
||||
import requests
|
||||
|
||||
from crewai.cli.organization.main import OrganizationCommand
|
||||
from crewai.cli.cli import list, switch, current
|
||||
from crewai.cli.cli import org_list, switch, current
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -16,44 +16,44 @@ def runner():
|
||||
|
||||
@pytest.fixture
|
||||
def org_command():
|
||||
with patch.object(OrganizationCommand, '__init__', return_value=None):
|
||||
with patch.object(OrganizationCommand, "__init__", return_value=None):
|
||||
command = OrganizationCommand()
|
||||
yield command
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_settings():
|
||||
with patch('crewai.cli.organization.main.Settings') as mock_settings_class:
|
||||
with patch("crewai.cli.organization.main.Settings") as mock_settings_class:
|
||||
mock_settings_instance = MagicMock()
|
||||
mock_settings_class.return_value = mock_settings_instance
|
||||
yield mock_settings_instance
|
||||
|
||||
|
||||
@patch('crewai.cli.cli.OrganizationCommand')
|
||||
@patch("crewai.cli.cli.OrganizationCommand")
|
||||
def test_org_list_command(mock_org_command_class, runner):
|
||||
mock_org_instance = MagicMock()
|
||||
mock_org_command_class.return_value = mock_org_instance
|
||||
|
||||
result = runner.invoke(list)
|
||||
result = runner.invoke(org_list)
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_org_command_class.assert_called_once()
|
||||
mock_org_instance.list.assert_called_once()
|
||||
|
||||
|
||||
@patch('crewai.cli.cli.OrganizationCommand')
|
||||
@patch("crewai.cli.cli.OrganizationCommand")
|
||||
def test_org_switch_command(mock_org_command_class, runner):
|
||||
mock_org_instance = MagicMock()
|
||||
mock_org_command_class.return_value = mock_org_instance
|
||||
|
||||
result = runner.invoke(switch, ['test-id'])
|
||||
result = runner.invoke(switch, ["test-id"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_org_command_class.assert_called_once()
|
||||
mock_org_instance.switch.assert_called_once_with('test-id')
|
||||
mock_org_instance.switch.assert_called_once_with("test-id")
|
||||
|
||||
|
||||
@patch('crewai.cli.cli.OrganizationCommand')
|
||||
@patch("crewai.cli.cli.OrganizationCommand")
|
||||
def test_org_current_command(mock_org_command_class, runner):
|
||||
mock_org_instance = MagicMock()
|
||||
mock_org_command_class.return_value = mock_org_instance
|
||||
@@ -67,18 +67,18 @@ def test_org_current_command(mock_org_command_class, runner):
|
||||
|
||||
class TestOrganizationCommand(unittest.TestCase):
|
||||
def setUp(self):
|
||||
with patch.object(OrganizationCommand, '__init__', return_value=None):
|
||||
with patch.object(OrganizationCommand, "__init__", return_value=None):
|
||||
self.org_command = OrganizationCommand()
|
||||
self.org_command.plus_api_client = MagicMock()
|
||||
|
||||
@patch('crewai.cli.organization.main.console')
|
||||
@patch('crewai.cli.organization.main.Table')
|
||||
@patch("crewai.cli.organization.main.console")
|
||||
@patch("crewai.cli.organization.main.Table")
|
||||
def test_list_organizations_success(self, mock_table, mock_console):
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_response.json.return_value = [
|
||||
{"name": "Org 1", "uuid": "org-123"},
|
||||
{"name": "Org 2", "uuid": "org-456"}
|
||||
{"name": "Org 2", "uuid": "org-456"},
|
||||
]
|
||||
self.org_command.plus_api_client = MagicMock()
|
||||
self.org_command.plus_api_client.get_organizations.return_value = mock_response
|
||||
@@ -89,16 +89,14 @@ class TestOrganizationCommand(unittest.TestCase):
|
||||
|
||||
self.org_command.plus_api_client.get_organizations.assert_called_once()
|
||||
mock_table.assert_called_once_with(title="Your Organizations")
|
||||
mock_table.return_value.add_column.assert_has_calls([
|
||||
call("Name", style="cyan"),
|
||||
call("ID", style="green")
|
||||
])
|
||||
mock_table.return_value.add_row.assert_has_calls([
|
||||
call("Org 1", "org-123"),
|
||||
call("Org 2", "org-456")
|
||||
])
|
||||
mock_table.return_value.add_column.assert_has_calls(
|
||||
[call("Name", style="cyan"), call("ID", style="green")]
|
||||
)
|
||||
mock_table.return_value.add_row.assert_has_calls(
|
||||
[call("Org 1", "org-123"), call("Org 2", "org-456")]
|
||||
)
|
||||
|
||||
@patch('crewai.cli.organization.main.console')
|
||||
@patch("crewai.cli.organization.main.console")
|
||||
def test_list_organizations_empty(self, mock_console):
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
@@ -110,33 +108,32 @@ class TestOrganizationCommand(unittest.TestCase):
|
||||
|
||||
self.org_command.plus_api_client.get_organizations.assert_called_once()
|
||||
mock_console.print.assert_called_once_with(
|
||||
"You don't belong to any organizations yet.",
|
||||
style="yellow"
|
||||
"You don't belong to any organizations yet.", style="yellow"
|
||||
)
|
||||
|
||||
@patch('crewai.cli.organization.main.console')
|
||||
@patch("crewai.cli.organization.main.console")
|
||||
def test_list_organizations_api_error(self, mock_console):
|
||||
self.org_command.plus_api_client = MagicMock()
|
||||
self.org_command.plus_api_client.get_organizations.side_effect = requests.exceptions.RequestException("API Error")
|
||||
self.org_command.plus_api_client.get_organizations.side_effect = (
|
||||
requests.exceptions.RequestException("API Error")
|
||||
)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
self.org_command.list()
|
||||
|
||||
|
||||
self.org_command.plus_api_client.get_organizations.assert_called_once()
|
||||
mock_console.print.assert_called_once_with(
|
||||
"Failed to retrieve organization list: API Error",
|
||||
style="bold red"
|
||||
"Failed to retrieve organization list: API Error", style="bold red"
|
||||
)
|
||||
|
||||
@patch('crewai.cli.organization.main.console')
|
||||
@patch('crewai.cli.organization.main.Settings')
|
||||
@patch("crewai.cli.organization.main.console")
|
||||
@patch("crewai.cli.organization.main.Settings")
|
||||
def test_switch_organization_success(self, mock_settings_class, mock_console):
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_response.json.return_value = [
|
||||
{"name": "Org 1", "uuid": "org-123"},
|
||||
{"name": "Test Org", "uuid": "test-id"}
|
||||
{"name": "Test Org", "uuid": "test-id"},
|
||||
]
|
||||
self.org_command.plus_api_client = MagicMock()
|
||||
self.org_command.plus_api_client.get_organizations.return_value = mock_response
|
||||
@@ -151,17 +148,16 @@ class TestOrganizationCommand(unittest.TestCase):
|
||||
assert mock_settings_instance.org_name == "Test Org"
|
||||
assert mock_settings_instance.org_uuid == "test-id"
|
||||
mock_console.print.assert_called_once_with(
|
||||
"Successfully switched to Test Org (test-id)",
|
||||
style="bold green"
|
||||
"Successfully switched to Test Org (test-id)", style="bold green"
|
||||
)
|
||||
|
||||
@patch('crewai.cli.organization.main.console')
|
||||
@patch("crewai.cli.organization.main.console")
|
||||
def test_switch_organization_not_found(self, mock_console):
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_response.json.return_value = [
|
||||
{"name": "Org 1", "uuid": "org-123"},
|
||||
{"name": "Org 2", "uuid": "org-456"}
|
||||
{"name": "Org 2", "uuid": "org-456"},
|
||||
]
|
||||
self.org_command.plus_api_client = MagicMock()
|
||||
self.org_command.plus_api_client.get_organizations.return_value = mock_response
|
||||
@@ -170,12 +166,11 @@ class TestOrganizationCommand(unittest.TestCase):
|
||||
|
||||
self.org_command.plus_api_client.get_organizations.assert_called_once()
|
||||
mock_console.print.assert_called_once_with(
|
||||
"Organization with id 'non-existent-id' not found.",
|
||||
style="bold red"
|
||||
"Organization with id 'non-existent-id' not found.", style="bold red"
|
||||
)
|
||||
|
||||
@patch('crewai.cli.organization.main.console')
|
||||
@patch('crewai.cli.organization.main.Settings')
|
||||
@patch("crewai.cli.organization.main.console")
|
||||
@patch("crewai.cli.organization.main.Settings")
|
||||
def test_current_organization_with_org(self, mock_settings_class, mock_console):
|
||||
mock_settings_instance = MagicMock()
|
||||
mock_settings_instance.org_name = "Test Org"
|
||||
@@ -186,12 +181,11 @@ class TestOrganizationCommand(unittest.TestCase):
|
||||
|
||||
self.org_command.plus_api_client.get_organizations.assert_not_called()
|
||||
mock_console.print.assert_called_once_with(
|
||||
"Currently logged in to organization Test Org (test-id)",
|
||||
style="bold green"
|
||||
"Currently logged in to organization Test Org (test-id)", style="bold green"
|
||||
)
|
||||
|
||||
@patch('crewai.cli.organization.main.console')
|
||||
@patch('crewai.cli.organization.main.Settings')
|
||||
@patch("crewai.cli.organization.main.console")
|
||||
@patch("crewai.cli.organization.main.Settings")
|
||||
def test_current_organization_without_org(self, mock_settings_class, mock_console):
|
||||
mock_settings_instance = MagicMock()
|
||||
mock_settings_instance.org_uuid = None
|
||||
@@ -201,16 +195,14 @@ class TestOrganizationCommand(unittest.TestCase):
|
||||
|
||||
assert mock_console.print.call_count == 3
|
||||
mock_console.print.assert_any_call(
|
||||
"You're not currently logged in to any organization.",
|
||||
style="yellow"
|
||||
"You're not currently logged in to any organization.", style="yellow"
|
||||
)
|
||||
|
||||
@patch('crewai.cli.organization.main.console')
|
||||
@patch("crewai.cli.organization.main.console")
|
||||
def test_list_organizations_unauthorized(self, mock_console):
|
||||
mock_response = MagicMock()
|
||||
mock_http_error = requests.exceptions.HTTPError(
|
||||
"401 Client Error: Unauthorized",
|
||||
response=MagicMock(status_code=401)
|
||||
"401 Client Error: Unauthorized", response=MagicMock(status_code=401)
|
||||
)
|
||||
|
||||
mock_response.raise_for_status.side_effect = mock_http_error
|
||||
@@ -221,15 +213,14 @@ class TestOrganizationCommand(unittest.TestCase):
|
||||
self.org_command.plus_api_client.get_organizations.assert_called_once()
|
||||
mock_console.print.assert_called_once_with(
|
||||
"You are not logged in to any organization. Use 'crewai login' to login.",
|
||||
style="bold red"
|
||||
style="bold red",
|
||||
)
|
||||
|
||||
@patch('crewai.cli.organization.main.console')
|
||||
@patch("crewai.cli.organization.main.console")
|
||||
def test_switch_organization_unauthorized(self, mock_console):
|
||||
mock_response = MagicMock()
|
||||
mock_http_error = requests.exceptions.HTTPError(
|
||||
"401 Client Error: Unauthorized",
|
||||
response=MagicMock(status_code=401)
|
||||
"401 Client Error: Unauthorized", response=MagicMock(status_code=401)
|
||||
)
|
||||
|
||||
mock_response.raise_for_status.side_effect = mock_http_error
|
||||
@@ -240,5 +231,5 @@ class TestOrganizationCommand(unittest.TestCase):
|
||||
self.org_command.plus_api_client.get_organizations.assert_called_once()
|
||||
mock_console.print.assert_called_once_with(
|
||||
"You are not logged in to any organization. Use 'crewai login' to login.",
|
||||
style="bold red"
|
||||
style="bold red",
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch, ANY
|
||||
|
||||
from crewai.cli.plus_api import PlusAPI
|
||||
from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
|
||||
|
||||
class TestPlusAPI(unittest.TestCase):
|
||||
@@ -30,29 +30,41 @@ class TestPlusAPI(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(response, mock_response)
|
||||
|
||||
def assert_request_with_org_id(self, mock_make_request, method: str, endpoint: str, **kwargs):
|
||||
def assert_request_with_org_id(
|
||||
self, mock_make_request, method: str, endpoint: str, **kwargs
|
||||
):
|
||||
mock_make_request.assert_called_once_with(
|
||||
method, f"https://app.crewai.com{endpoint}", headers={'Authorization': ANY, 'Content-Type': ANY, 'User-Agent': ANY, 'X-Crewai-Version': ANY, 'X-Crewai-Organization-Id': self.org_uuid}, **kwargs
|
||||
method,
|
||||
f"{DEFAULT_CREWAI_ENTERPRISE_URL}{endpoint}",
|
||||
headers={
|
||||
"Authorization": ANY,
|
||||
"Content-Type": ANY,
|
||||
"User-Agent": ANY,
|
||||
"X-Crewai-Version": ANY,
|
||||
"X-Crewai-Organization-Id": self.org_uuid,
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@patch("crewai.cli.plus_api.Settings")
|
||||
@patch("requests.Session.request")
|
||||
def test_login_to_tool_repository_with_org_uuid(self, mock_make_request, mock_settings_class):
|
||||
def test_login_to_tool_repository_with_org_uuid(
|
||||
self, mock_make_request, mock_settings_class
|
||||
):
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.org_uuid = self.org_uuid
|
||||
mock_settings.enterprise_base_url = DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
mock_settings_class.return_value = mock_settings
|
||||
# re-initialize Client
|
||||
self.api = PlusAPI(self.api_key)
|
||||
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_make_request.return_value = mock_response
|
||||
|
||||
response = self.api.login_to_tool_repository()
|
||||
|
||||
self.assert_request_with_org_id(
|
||||
mock_make_request,
|
||||
'POST',
|
||||
'/crewai_plus/api/v1/tools/login'
|
||||
mock_make_request, "POST", "/crewai_plus/api/v1/tools/login"
|
||||
)
|
||||
self.assertEqual(response, mock_response)
|
||||
|
||||
@@ -66,28 +78,27 @@ class TestPlusAPI(unittest.TestCase):
|
||||
"GET", "/crewai_plus/api/v1/agents/test_agent_handle"
|
||||
)
|
||||
self.assertEqual(response, mock_response)
|
||||
|
||||
|
||||
@patch("crewai.cli.plus_api.Settings")
|
||||
@patch("requests.Session.request")
|
||||
def test_get_agent_with_org_uuid(self, mock_make_request, mock_settings_class):
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.org_uuid = self.org_uuid
|
||||
mock_settings.enterprise_base_url = DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
mock_settings_class.return_value = mock_settings
|
||||
# re-initialize Client
|
||||
self.api = PlusAPI(self.api_key)
|
||||
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_make_request.return_value = mock_response
|
||||
|
||||
response = self.api.get_agent("test_agent_handle")
|
||||
|
||||
self.assert_request_with_org_id(
|
||||
mock_make_request,
|
||||
"GET",
|
||||
"/crewai_plus/api/v1/agents/test_agent_handle"
|
||||
mock_make_request, "GET", "/crewai_plus/api/v1/agents/test_agent_handle"
|
||||
)
|
||||
self.assertEqual(response, mock_response)
|
||||
|
||||
|
||||
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
||||
def test_get_tool(self, mock_make_request):
|
||||
mock_response = MagicMock()
|
||||
@@ -98,12 +109,13 @@ class TestPlusAPI(unittest.TestCase):
|
||||
"GET", "/crewai_plus/api/v1/tools/test_tool_handle"
|
||||
)
|
||||
self.assertEqual(response, mock_response)
|
||||
|
||||
|
||||
@patch("crewai.cli.plus_api.Settings")
|
||||
@patch("requests.Session.request")
|
||||
def test_get_tool_with_org_uuid(self, mock_make_request, mock_settings_class):
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.org_uuid = self.org_uuid
|
||||
mock_settings.enterprise_base_url = DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
mock_settings_class.return_value = mock_settings
|
||||
# re-initialize Client
|
||||
self.api = PlusAPI(self.api_key)
|
||||
@@ -115,9 +127,7 @@ class TestPlusAPI(unittest.TestCase):
|
||||
response = self.api.get_tool("test_tool_handle")
|
||||
|
||||
self.assert_request_with_org_id(
|
||||
mock_make_request,
|
||||
"GET",
|
||||
"/crewai_plus/api/v1/tools/test_tool_handle"
|
||||
mock_make_request, "GET", "/crewai_plus/api/v1/tools/test_tool_handle"
|
||||
)
|
||||
self.assertEqual(response, mock_response)
|
||||
|
||||
@@ -147,12 +157,13 @@ class TestPlusAPI(unittest.TestCase):
|
||||
"POST", "/crewai_plus/api/v1/tools", json=params
|
||||
)
|
||||
self.assertEqual(response, mock_response)
|
||||
|
||||
|
||||
@patch("crewai.cli.plus_api.Settings")
|
||||
@patch("requests.Session.request")
|
||||
def test_publish_tool_with_org_uuid(self, mock_make_request, mock_settings_class):
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.org_uuid = self.org_uuid
|
||||
mock_settings.enterprise_base_url = DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
mock_settings_class.return_value = mock_settings
|
||||
# re-initialize Client
|
||||
self.api = PlusAPI(self.api_key)
|
||||
@@ -160,7 +171,7 @@ class TestPlusAPI(unittest.TestCase):
|
||||
# Set up mock response
|
||||
mock_response = MagicMock()
|
||||
mock_make_request.return_value = mock_response
|
||||
|
||||
|
||||
handle = "test_tool_handle"
|
||||
public = True
|
||||
version = "1.0.0"
|
||||
@@ -180,12 +191,9 @@ class TestPlusAPI(unittest.TestCase):
|
||||
"description": description,
|
||||
"available_exports": None,
|
||||
}
|
||||
|
||||
|
||||
self.assert_request_with_org_id(
|
||||
mock_make_request,
|
||||
"POST",
|
||||
"/crewai_plus/api/v1/tools",
|
||||
json=expected_params
|
||||
mock_make_request, "POST", "/crewai_plus/api/v1/tools", json=expected_params
|
||||
)
|
||||
self.assertEqual(response, mock_response)
|
||||
|
||||
@@ -311,8 +319,11 @@ class TestPlusAPI(unittest.TestCase):
|
||||
"POST", "/crewai_plus/api/v1/crews", json=payload
|
||||
)
|
||||
|
||||
@patch.dict(os.environ, {"CREWAI_BASE_URL": "https://custom-url.com/api"})
|
||||
def test_custom_base_url(self):
|
||||
@patch("crewai.cli.plus_api.Settings")
|
||||
def test_custom_base_url(self, mock_settings_class):
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.enterprise_base_url = "https://custom-url.com/api"
|
||||
mock_settings_class.return_value = mock_settings
|
||||
custom_api = PlusAPI("test_key")
|
||||
self.assertEqual(
|
||||
custom_api.base_url,
|
||||
|
||||
90
tests/cli/test_settings_command.py
Normal file
90
tests/cli/test_settings_command.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock, call
|
||||
|
||||
from crewai.cli.settings.main import SettingsCommand
|
||||
from crewai.cli.config import (
|
||||
Settings,
|
||||
USER_SETTINGS_KEYS,
|
||||
CLI_SETTINGS_KEYS,
|
||||
DEFAULT_CLI_SETTINGS,
|
||||
HIDDEN_SETTINGS_KEYS,
|
||||
READONLY_SETTINGS_KEYS,
|
||||
)
|
||||
import shutil
|
||||
|
||||
|
||||
class TestSettingsCommand(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.test_dir = Path(tempfile.mkdtemp())
|
||||
self.config_path = self.test_dir / "settings.json"
|
||||
self.settings = Settings(config_path=self.config_path)
|
||||
self.settings_command = SettingsCommand(
|
||||
settings_kwargs={"config_path": self.config_path}
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.test_dir)
|
||||
|
||||
@patch("crewai.cli.settings.main.console")
|
||||
@patch("crewai.cli.settings.main.Table")
|
||||
def test_list_settings(self, mock_table_class, mock_console):
|
||||
mock_table_instance = MagicMock()
|
||||
mock_table_class.return_value = mock_table_instance
|
||||
|
||||
self.settings_command.list()
|
||||
|
||||
# Tests that the table is created skipping hidden settings
|
||||
mock_table_instance.add_row.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
field_name,
|
||||
getattr(self.settings, field_name) or "Not set",
|
||||
field_info.description,
|
||||
)
|
||||
for field_name, field_info in Settings.model_fields.items()
|
||||
if field_name not in HIDDEN_SETTINGS_KEYS
|
||||
]
|
||||
)
|
||||
|
||||
# Tests that the table is printed
|
||||
mock_console.print.assert_called_once_with(mock_table_instance)
|
||||
|
||||
def test_set_valid_keys(self):
|
||||
valid_keys = Settings.model_fields.keys() - (
|
||||
READONLY_SETTINGS_KEYS + HIDDEN_SETTINGS_KEYS
|
||||
)
|
||||
for key in valid_keys:
|
||||
test_value = f"some_value_for_{key}"
|
||||
self.settings_command.set(key, test_value)
|
||||
self.assertEqual(getattr(self.settings_command.settings, key), test_value)
|
||||
|
||||
def test_set_invalid_key(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
self.settings_command.set("invalid_key", "value")
|
||||
|
||||
def test_set_readonly_keys(self):
|
||||
for key in READONLY_SETTINGS_KEYS:
|
||||
with self.assertRaises(SystemExit):
|
||||
self.settings_command.set(key, "some_readonly_key_value")
|
||||
|
||||
def test_set_hidden_keys(self):
|
||||
for key in HIDDEN_SETTINGS_KEYS:
|
||||
with self.assertRaises(SystemExit):
|
||||
self.settings_command.set(key, "some_hidden_key_value")
|
||||
|
||||
def test_reset_all_settings(self):
|
||||
for key in USER_SETTINGS_KEYS + CLI_SETTINGS_KEYS:
|
||||
setattr(self.settings_command.settings, key, f"custom_value_for_{key}")
|
||||
self.settings_command.settings.dump()
|
||||
|
||||
self.settings_command.reset_all_settings()
|
||||
|
||||
for key in USER_SETTINGS_KEYS:
|
||||
self.assertEqual(getattr(self.settings_command.settings, key), None)
|
||||
|
||||
for key in CLI_SETTINGS_KEYS:
|
||||
self.assertEqual(
|
||||
getattr(self.settings_command.settings, key), DEFAULT_CLI_SETTINGS.get(key)
|
||||
)
|
||||
@@ -4475,7 +4475,7 @@ def test_crew_copy_with_memory():
|
||||
)
|
||||
original_entity_id = id(crew._entity_memory) if crew._entity_memory else None
|
||||
original_external_id = id(crew._external_memory) if crew._external_memory else None
|
||||
original_user_id = id(crew._user_memory) if crew._user_memory else None
|
||||
|
||||
|
||||
try:
|
||||
crew_copy = crew.copy()
|
||||
@@ -4526,20 +4526,6 @@ def test_crew_copy_with_memory():
|
||||
or crew_copy._external_memory is None
|
||||
), "Copied _external_memory should be None if not originally present"
|
||||
|
||||
if original_user_id:
|
||||
assert hasattr(
|
||||
crew_copy, "_user_memory"
|
||||
), "Copied crew should have _user_memory"
|
||||
assert (
|
||||
crew_copy._user_memory is not None
|
||||
), "Copied _user_memory should not be None"
|
||||
assert (
|
||||
id(crew_copy._user_memory) != original_user_id
|
||||
), "Copied _user_memory should be a new object"
|
||||
else:
|
||||
assert (
|
||||
not hasattr(crew_copy, "_user_memory") or crew_copy._user_memory is None
|
||||
), "Copied _user_memory should be None if not originally present"
|
||||
|
||||
except pydantic_core.ValidationError as e:
|
||||
if "Input should be an instance of" in str(e) and ("Memory" in str(e)):
|
||||
@@ -4756,3 +4742,13 @@ def test_reset_agent_knowledge_with_only_agent_knowledge(researcher, writer):
|
||||
mock_reset_agent_knowledge.assert_called_once_with(
|
||||
[mock_ks_research, mock_ks_writer]
|
||||
)
|
||||
|
||||
def test_default_crew_name(researcher, writer):
|
||||
crew = Crew(
|
||||
agents=[researcher, writer],
|
||||
tasks=[
|
||||
Task(description="Task 1", expected_output="output", agent=researcher),
|
||||
Task(description="Task 2", expected_output="output", agent=writer),
|
||||
],
|
||||
)
|
||||
assert crew.name == "crew"
|
||||
|
||||
@@ -755,3 +755,15 @@ def test_multiple_routers_from_same_trigger():
|
||||
assert execution_order.index("anemia_analysis") > execution_order.index(
|
||||
"anemia_router"
|
||||
)
|
||||
|
||||
|
||||
def test_flow_name():
|
||||
class MyFlow(Flow):
|
||||
name = "MyFlow"
|
||||
|
||||
@start()
|
||||
def start(self):
|
||||
return "Hello, world!"
|
||||
|
||||
flow = MyFlow()
|
||||
assert flow.name == "MyFlow"
|
||||
|
||||
@@ -282,9 +282,6 @@ def test_gemini_models(model):
|
||||
@pytest.mark.parametrize(
|
||||
"model",
|
||||
[
|
||||
"gemini/gemma-3-1b-it",
|
||||
"gemini/gemma-3-4b-it",
|
||||
"gemini/gemma-3-12b-it",
|
||||
"gemini/gemma-3-27b-it",
|
||||
],
|
||||
)
|
||||
@@ -377,6 +374,7 @@ def get_weather_tool_schema():
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_context_window_exceeded_error_handling():
|
||||
"""Test that litellm.ContextWindowExceededError is converted to LLMContextLengthExceededException."""
|
||||
from litellm.exceptions import ContextWindowExceededError
|
||||
@@ -392,7 +390,7 @@ def test_context_window_exceeded_error_handling():
|
||||
mock_completion.side_effect = ContextWindowExceededError(
|
||||
"This model's maximum context length is 8192 tokens. However, your messages resulted in 10000 tokens.",
|
||||
model="gpt-4",
|
||||
llm_provider="openai"
|
||||
llm_provider="openai",
|
||||
)
|
||||
|
||||
with pytest.raises(LLMContextLengthExceededException) as excinfo:
|
||||
@@ -407,7 +405,7 @@ def test_context_window_exceeded_error_handling():
|
||||
mock_completion.side_effect = ContextWindowExceededError(
|
||||
"This model's maximum context length is 8192 tokens. However, your messages resulted in 10000 tokens.",
|
||||
model="gpt-4",
|
||||
llm_provider="openai"
|
||||
llm_provider="openai",
|
||||
)
|
||||
|
||||
with pytest.raises(LLMContextLengthExceededException) as excinfo:
|
||||
@@ -598,6 +596,7 @@ def test_handle_streaming_tool_calls(get_weather_tool_schema, mock_emit):
|
||||
expected_final_chunk_result=expected_final_chunk_result,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
def test_handle_streaming_tool_calls_with_error(get_weather_tool_schema, mock_emit):
|
||||
def get_weather_error(location):
|
||||
@@ -609,9 +608,7 @@ def test_handle_streaming_tool_calls_with_error(get_weather_tool_schema, mock_em
|
||||
{"role": "user", "content": "What is the weather in New York?"},
|
||||
],
|
||||
tools=[get_weather_tool_schema],
|
||||
available_functions={
|
||||
"get_weather": get_weather_error
|
||||
},
|
||||
available_functions={"get_weather": get_weather_error},
|
||||
)
|
||||
assert response == ""
|
||||
expected_final_chunk_result = '{"location":"New York, NY"}'
|
||||
@@ -676,8 +673,11 @@ def test_llm_call_when_stop_is_unsupported(caplog):
|
||||
assert isinstance(result, str)
|
||||
assert "Paris" in result
|
||||
|
||||
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
def test_llm_call_when_stop_is_unsupported_when_additional_drop_params_is_provided(caplog):
|
||||
def test_llm_call_when_stop_is_unsupported_when_additional_drop_params_is_provided(
|
||||
caplog,
|
||||
):
|
||||
llm = LLM(model="o1-mini", stop=["stop"], additional_drop_params=["another_param"])
|
||||
with caplog.at_level(logging.INFO):
|
||||
result = llm.call("What is the capital of France?")
|
||||
@@ -690,6 +690,7 @@ def test_llm_call_when_stop_is_unsupported_when_additional_drop_params_is_provid
|
||||
def ollama_llm():
|
||||
return LLM(model="ollama/llama3.2:3b")
|
||||
|
||||
|
||||
def test_ollama_appends_dummy_user_message_when_last_is_assistant(ollama_llm):
|
||||
original_messages = [
|
||||
{"role": "user", "content": "Hi there"},
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from mem0.memory.main import Memory
|
||||
|
||||
from crewai.memory.user.user_memory import UserMemory
|
||||
from crewai.memory.user.user_memory_item import UserMemoryItem
|
||||
|
||||
|
||||
class MockCrew:
|
||||
def __init__(self, memory_config):
|
||||
self.memory_config = memory_config
|
||||
|
||||
@pytest.fixture
|
||||
def user_memory():
|
||||
"""Fixture to create a UserMemory instance"""
|
||||
crew = MockCrew(
|
||||
memory_config={
|
||||
"provider": "mem0",
|
||||
"config": {"user_id": "john"},
|
||||
"user_memory" : {}
|
||||
}
|
||||
)
|
||||
|
||||
user_memory = MagicMock(spec=UserMemory)
|
||||
|
||||
with patch.object(Memory,'__new__',return_value=user_memory):
|
||||
user_memory_instance = UserMemory(crew=crew)
|
||||
|
||||
return user_memory_instance
|
||||
|
||||
def test_save_and_search(user_memory):
|
||||
memory = UserMemoryItem(
|
||||
data="""test value test value test value test value test value test value
|
||||
test value test value test value test value test value test value
|
||||
test value test value test value test value test value test value""",
|
||||
user="test_user",
|
||||
metadata={"task": "test_task"},
|
||||
)
|
||||
|
||||
with patch.object(UserMemory, "save") as mock_save:
|
||||
user_memory.save(
|
||||
value=memory.data,
|
||||
metadata=memory.metadata,
|
||||
user=memory.user
|
||||
)
|
||||
|
||||
mock_save.assert_called_once_with(
|
||||
value=memory.data,
|
||||
metadata=memory.metadata,
|
||||
user=memory.user
|
||||
)
|
||||
|
||||
expected_result = [
|
||||
{
|
||||
"context": memory.data,
|
||||
"metadata": {"agent": "test_agent"},
|
||||
"score": 0.95,
|
||||
}
|
||||
]
|
||||
expected_result = ["mocked_result"]
|
||||
|
||||
# Use patch.object to mock UserMemory's search method
|
||||
with patch.object(UserMemory, 'search', return_value=expected_result) as mock_search:
|
||||
find = UserMemory.search("test value", score_threshold=0.01)[0]
|
||||
mock_search.assert_called_once_with("test value", score_threshold=0.01)
|
||||
assert find == expected_result[0]
|
||||
@@ -9,8 +9,7 @@ from crewai.memory.storage.mem0_storage import Mem0Storage
|
||||
|
||||
# Define the class (if not already defined)
|
||||
class MockCrew:
|
||||
def __init__(self, memory_config):
|
||||
self.memory_config = memory_config
|
||||
def __init__(self):
|
||||
self.agents = [MagicMock(role="Test Agent")]
|
||||
|
||||
|
||||
@@ -54,16 +53,12 @@ def mem0_storage_with_mocked_config(mock_mem0_memory):
|
||||
"custom_update_memory_prompt": "mock prompt 2",
|
||||
}
|
||||
|
||||
# Instantiate the class with memory_config
|
||||
# Parameters like run_id, includes, and excludes doesn't matter in Memory OSS
|
||||
crew = MockCrew(
|
||||
memory_config={
|
||||
"provider": "mem0",
|
||||
"config": {"user_id": "test_user", "local_mem0_config": config, "run_id": "my_run_id", "includes": "include1","excludes": "exclude1", "infer" : True},
|
||||
}
|
||||
)
|
||||
crew = MockCrew()
|
||||
|
||||
mem0_storage = Mem0Storage(type="short_term", crew=crew)
|
||||
embedder_config={"user_id": "test_user", "local_mem0_config": config, "run_id": "my_run_id", "includes": "include1","excludes": "exclude1", "infer" : True}
|
||||
|
||||
mem0_storage = Mem0Storage(type="short_term", crew=crew, config=embedder_config)
|
||||
return mem0_storage, mock_from_config, config
|
||||
|
||||
|
||||
@@ -88,10 +83,9 @@ def mem0_storage_with_memory_client_using_config_from_crew(mock_mem0_memory_clie
|
||||
|
||||
# We need to patch the MemoryClient before it's instantiated
|
||||
with patch.object(MemoryClient, "__new__", return_value=mock_mem0_memory_client):
|
||||
crew = MockCrew(
|
||||
memory_config={
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
crew = MockCrew()
|
||||
|
||||
embedder_config={
|
||||
"user_id": "test_user",
|
||||
"api_key": "ABCDEFGH",
|
||||
"org_id": "my_org_id",
|
||||
@@ -100,11 +94,9 @@ def mem0_storage_with_memory_client_using_config_from_crew(mock_mem0_memory_clie
|
||||
"includes": "include1",
|
||||
"excludes": "exclude1",
|
||||
"infer": True
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
mem0_storage = Mem0Storage(type="short_term", crew=crew)
|
||||
mem0_storage = Mem0Storage(type="short_term", crew=crew, config=embedder_config)
|
||||
return mem0_storage
|
||||
|
||||
|
||||
@@ -116,18 +108,7 @@ def mem0_storage_with_memory_client_using_explictly_config(mock_mem0_memory_clie
|
||||
with patch.object(MemoryClient, "__new__", return_value=mock_mem0_memory_client), \
|
||||
patch.object(Memory, "__new__", return_value=mock_mem0_memory):
|
||||
|
||||
crew = MockCrew(
|
||||
memory_config={
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"user_id": "test_user",
|
||||
"api_key": "ABCDEFGH",
|
||||
"org_id": "my_org_id",
|
||||
"project_id": "my_project_id",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
crew = MockCrew()
|
||||
new_config = {"provider": "mem0", "config": {"api_key": "new-api-key"}}
|
||||
|
||||
mem0_storage = Mem0Storage(type="short_term", crew=crew, config=new_config)
|
||||
@@ -164,21 +145,18 @@ def test_mem0_storage_updates_project_with_custom_categories(mock_mem0_memory_cl
|
||||
{"lifestyle_management_concerns": "Tracks daily routines, habits, hobbies and interests including cooking, time management and work-life balance"},
|
||||
]
|
||||
|
||||
crew = MockCrew(
|
||||
memory_config={
|
||||
"provider": "mem0",
|
||||
"config": {
|
||||
"user_id": "test_user",
|
||||
"api_key": "ABCDEFGH",
|
||||
"org_id": "my_org_id",
|
||||
"project_id": "my_project_id",
|
||||
"custom_categories": new_categories,
|
||||
},
|
||||
crew = MockCrew()
|
||||
|
||||
config={
|
||||
"user_id": "test_user",
|
||||
"api_key": "ABCDEFGH",
|
||||
"org_id": "my_org_id",
|
||||
"project_id": "my_project_id",
|
||||
"custom_categories": new_categories
|
||||
}
|
||||
)
|
||||
|
||||
with patch.object(MemoryClient, "__new__", return_value=mock_mem0_memory_client):
|
||||
_ = Mem0Storage(type="short_term", crew=crew)
|
||||
_ = Mem0Storage(type="short_term", crew=crew, config=config)
|
||||
|
||||
mock_mem0_memory_client.update_project.assert_called_once_with(
|
||||
custom_categories=new_categories
|
||||
@@ -191,17 +169,39 @@ def test_save_method_with_memory_oss(mem0_storage_with_mocked_config):
|
||||
"""Test save method for different memory types"""
|
||||
mem0_storage, _, _ = mem0_storage_with_mocked_config
|
||||
mem0_storage.memory.add = MagicMock()
|
||||
|
||||
|
||||
# Test short_term memory type (already set in fixture)
|
||||
test_value = "This is a test memory"
|
||||
test_metadata = {"key": "value"}
|
||||
|
||||
|
||||
mem0_storage.save(test_value, test_metadata)
|
||||
|
||||
|
||||
mem0_storage.memory.add.assert_called_once_with(
|
||||
[{'role': 'assistant' , 'content': test_value}],
|
||||
[{"role": "assistant" , "content": test_value}],
|
||||
infer=True,
|
||||
metadata={"type": "short_term", "key": "value"},
|
||||
run_id="my_run_id",
|
||||
user_id="test_user",
|
||||
agent_id='Test_Agent'
|
||||
)
|
||||
|
||||
def test_save_method_with_multiple_agents(mem0_storage_with_mocked_config):
|
||||
mem0_storage, _, _ = mem0_storage_with_mocked_config
|
||||
mem0_storage.crew.agents = [MagicMock(role="Test Agent"), MagicMock(role="Test Agent 2"), MagicMock(role="Test Agent 3")]
|
||||
mem0_storage.memory.add = MagicMock()
|
||||
|
||||
test_value = "This is a test memory"
|
||||
test_metadata = {"key": "value"}
|
||||
|
||||
mem0_storage.save(test_value, test_metadata)
|
||||
|
||||
mem0_storage.memory.add.assert_called_once_with(
|
||||
[{"role": "assistant" , "content": test_value}],
|
||||
infer=True,
|
||||
metadata={"type": "short_term", "key": "value"},
|
||||
run_id="my_run_id",
|
||||
user_id="test_user",
|
||||
agent_id='Test_Agent_Test_Agent_2_Test_Agent_3'
|
||||
)
|
||||
|
||||
|
||||
@@ -209,13 +209,13 @@ def test_save_method_with_memory_client(mem0_storage_with_memory_client_using_co
|
||||
"""Test save method for different memory types"""
|
||||
mem0_storage = mem0_storage_with_memory_client_using_config_from_crew
|
||||
mem0_storage.memory.add = MagicMock()
|
||||
|
||||
|
||||
# Test short_term memory type (already set in fixture)
|
||||
test_value = "This is a test memory"
|
||||
test_metadata = {"key": "value"}
|
||||
|
||||
|
||||
mem0_storage.save(test_value, test_metadata)
|
||||
|
||||
|
||||
mem0_storage.memory.add.assert_called_once_with(
|
||||
[{'role': 'assistant' , 'content': test_value}],
|
||||
infer=True,
|
||||
@@ -224,41 +224,43 @@ def test_save_method_with_memory_client(mem0_storage_with_memory_client_using_co
|
||||
run_id="my_run_id",
|
||||
includes="include1",
|
||||
excludes="exclude1",
|
||||
output_format='v1.1'
|
||||
output_format='v1.1',
|
||||
user_id='test_user',
|
||||
agent_id='Test_Agent'
|
||||
)
|
||||
|
||||
|
||||
def test_search_method_with_memory_oss(mem0_storage_with_mocked_config):
|
||||
"""Test search method for different memory types"""
|
||||
mem0_storage, _, _ = mem0_storage_with_mocked_config
|
||||
mock_results = {"results": [{"score": 0.9, "content": "Result 1"}, {"score": 0.4, "content": "Result 2"}]}
|
||||
mock_results = {"results": [{"score": 0.9, "memory": "Result 1"}, {"score": 0.4, "memory": "Result 2"}]}
|
||||
mem0_storage.memory.search = MagicMock(return_value=mock_results)
|
||||
|
||||
results = mem0_storage.search("test query", limit=5, score_threshold=0.5)
|
||||
|
||||
mem0_storage.memory.search.assert_called_once_with(
|
||||
query="test query",
|
||||
limit=5,
|
||||
query="test query",
|
||||
limit=5,
|
||||
user_id="test_user",
|
||||
filters={'AND': [{'run_id': 'my_run_id'}]},
|
||||
filters={'AND': [{'run_id': 'my_run_id'}]},
|
||||
threshold=0.5
|
||||
)
|
||||
|
||||
assert len(results) == 2
|
||||
assert results[0]["content"] == "Result 1"
|
||||
assert results[0]["context"] == "Result 1"
|
||||
|
||||
|
||||
def test_search_method_with_memory_client(mem0_storage_with_memory_client_using_config_from_crew):
|
||||
"""Test search method for different memory types"""
|
||||
mem0_storage = mem0_storage_with_memory_client_using_config_from_crew
|
||||
mock_results = {"results": [{"score": 0.9, "content": "Result 1"}, {"score": 0.4, "content": "Result 2"}]}
|
||||
mock_results = {"results": [{"score": 0.9, "memory": "Result 1"}, {"score": 0.4, "memory": "Result 2"}]}
|
||||
mem0_storage.memory.search = MagicMock(return_value=mock_results)
|
||||
|
||||
results = mem0_storage.search("test query", limit=5, score_threshold=0.5)
|
||||
|
||||
mem0_storage.memory.search.assert_called_once_with(
|
||||
query="test query",
|
||||
limit=5,
|
||||
query="test query",
|
||||
limit=5,
|
||||
metadata={"type": "short_term"},
|
||||
user_id="test_user",
|
||||
version='v2',
|
||||
@@ -269,4 +271,80 @@ def test_search_method_with_memory_client(mem0_storage_with_memory_client_using_
|
||||
)
|
||||
|
||||
assert len(results) == 2
|
||||
assert results[0]["content"] == "Result 1"
|
||||
assert results[0]["context"] == "Result 1"
|
||||
|
||||
|
||||
def test_mem0_storage_default_infer_value(mock_mem0_memory_client):
|
||||
"""Test that Mem0Storage sets infer=True by default for short_term memory."""
|
||||
with patch.object(MemoryClient, "__new__", return_value=mock_mem0_memory_client):
|
||||
crew = MockCrew()
|
||||
|
||||
config={
|
||||
"user_id": "test_user",
|
||||
"api_key": "ABCDEFGH"
|
||||
}
|
||||
|
||||
mem0_storage = Mem0Storage(type="short_term", crew=crew, config=config)
|
||||
assert mem0_storage.infer is True
|
||||
|
||||
def test_save_memory_using_agent_entity(mock_mem0_memory_client):
|
||||
config = {
|
||||
"agent_id": "agent-123",
|
||||
}
|
||||
|
||||
mock_memory = MagicMock(spec=Memory)
|
||||
with patch.object(Memory, "__new__", return_value=mock_memory):
|
||||
mem0_storage = Mem0Storage(type="external", config=config)
|
||||
mem0_storage.save("test memory", {"key": "value"})
|
||||
mem0_storage.memory.add.assert_called_once_with(
|
||||
[{'role': 'assistant' , 'content': 'test memory'}],
|
||||
infer=True,
|
||||
metadata={"type": "external", "key": "value"},
|
||||
agent_id="agent-123",
|
||||
)
|
||||
|
||||
def test_search_method_with_agent_entity():
|
||||
config = {
|
||||
"agent_id": "agent-123",
|
||||
}
|
||||
|
||||
mock_memory = MagicMock(spec=Memory)
|
||||
mock_results = {"results": [{"score": 0.9, "memory": "Result 1"}, {"score": 0.4, "memory": "Result 2"}]}
|
||||
|
||||
with patch.object(Memory, "__new__", return_value=mock_memory):
|
||||
mem0_storage = Mem0Storage(type="external", config=config)
|
||||
|
||||
mem0_storage.memory.search = MagicMock(return_value=mock_results)
|
||||
results = mem0_storage.search("test query", limit=5, score_threshold=0.5)
|
||||
|
||||
mem0_storage.memory.search.assert_called_once_with(
|
||||
query="test query",
|
||||
limit=5,
|
||||
filters={"AND": [{"agent_id": "agent-123"}]},
|
||||
threshold=0.5,
|
||||
)
|
||||
|
||||
assert len(results) == 2
|
||||
assert results[0]["context"] == "Result 1"
|
||||
|
||||
|
||||
def test_search_method_with_agent_id_and_user_id():
|
||||
mock_memory = MagicMock(spec=Memory)
|
||||
mock_results = {"results": [{"score": 0.9, "memory": "Result 1"}, {"score": 0.4, "memory": "Result 2"}]}
|
||||
|
||||
with patch.object(Memory, "__new__", return_value=mock_memory):
|
||||
mem0_storage = Mem0Storage(type="external", config={"agent_id": "agent-123", "user_id": "user-123"})
|
||||
|
||||
mem0_storage.memory.search = MagicMock(return_value=mock_results)
|
||||
results = mem0_storage.search("test query", limit=5, score_threshold=0.5)
|
||||
|
||||
mem0_storage.memory.search.assert_called_once_with(
|
||||
query="test query",
|
||||
limit=5,
|
||||
user_id='user-123',
|
||||
filters={"OR": [{"user_id": "user-123"}, {"agent_id": "agent-123"}]},
|
||||
threshold=0.5,
|
||||
)
|
||||
|
||||
assert len(results) == 2
|
||||
assert results[0]["context"] == "Result 1"
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""Test flow state persistence functionality."""
|
||||
|
||||
import os
|
||||
from typing import Dict
|
||||
from typing import Dict, List
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai.flow.flow import Flow, FlowState, listen, start
|
||||
@@ -208,3 +207,44 @@ def test_persist_decorator_verbose_logging(tmp_path, caplog):
|
||||
flow = VerboseFlow(persistence=persistence)
|
||||
flow.kickoff()
|
||||
assert "Saving flow state" in caplog.text
|
||||
|
||||
|
||||
|
||||
def test_persistence_with_base_model(tmp_path):
|
||||
db_path = os.path.join(tmp_path, "test_flows.db")
|
||||
persistence = SQLiteFlowPersistence(db_path)
|
||||
|
||||
class Message(BaseModel):
|
||||
role: str
|
||||
type: str
|
||||
content: str
|
||||
|
||||
class State(FlowState):
|
||||
latest_message: Message | None = None
|
||||
history: List[Message] = []
|
||||
|
||||
@persist(persistence)
|
||||
class BaseModelFlow(Flow[State]):
|
||||
initial_state = State(latest_message=None, history=[])
|
||||
|
||||
@start()
|
||||
def init_step(self):
|
||||
self.state.latest_message = Message(role="user", type="text", content="Hello, World!")
|
||||
self.state.history.append(self.state.latest_message)
|
||||
|
||||
flow = BaseModelFlow(persistence=persistence)
|
||||
flow.kickoff()
|
||||
|
||||
latest_message = flow.state.latest_message
|
||||
message, = flow.state.history
|
||||
|
||||
assert latest_message is not None
|
||||
assert latest_message.role == "user"
|
||||
assert latest_message.type == "text"
|
||||
assert latest_message.content == "Hello, World!"
|
||||
|
||||
assert len(flow.state.history) == 1
|
||||
assert message.role == "user"
|
||||
assert message.type == "text"
|
||||
assert message.content == "Hello, World!"
|
||||
assert isinstance(flow.state, State)
|
||||
|
||||
313
tests/tracing/test_tracing.py
Normal file
313
tests/tracing/test_tracing.py
Normal file
@@ -0,0 +1,313 @@
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# Remove the module-level patch
|
||||
from crewai import Agent, Task, Crew
|
||||
from crewai.utilities.events.listeners.tracing.trace_listener import (
|
||||
TraceCollectionListener,
|
||||
)
|
||||
from crewai.utilities.events.listeners.tracing.trace_batch_manager import (
|
||||
TraceBatchManager,
|
||||
)
|
||||
from crewai.utilities.events.listeners.tracing.types import TraceEvent
|
||||
|
||||
|
||||
class TestTraceListenerSetup:
|
||||
"""Test TraceListener is properly setup and collecting events"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_auth_token(self):
|
||||
"""Mock authentication token for all tests in this class"""
|
||||
# Need to patch all the places where get_auth_token is imported/used
|
||||
with (
|
||||
patch(
|
||||
"crewai.cli.authentication.token.get_auth_token",
|
||||
return_value="mock_token_12345",
|
||||
),
|
||||
patch(
|
||||
"crewai.utilities.events.listeners.tracing.trace_listener.get_auth_token",
|
||||
return_value="mock_token_12345",
|
||||
),
|
||||
patch(
|
||||
"crewai.utilities.events.listeners.tracing.trace_batch_manager.get_auth_token",
|
||||
return_value="mock_token_12345",
|
||||
),
|
||||
patch(
|
||||
"crewai.utilities.events.listeners.tracing.interfaces.get_auth_token",
|
||||
return_value="mock_token_12345",
|
||||
),
|
||||
):
|
||||
yield
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_event_bus(self):
|
||||
"""Clear event bus listeners before and after each test"""
|
||||
from crewai.utilities.events import crewai_event_bus
|
||||
|
||||
# Store original handlers
|
||||
original_handlers = crewai_event_bus._handlers.copy()
|
||||
|
||||
# Clear for test
|
||||
crewai_event_bus._handlers.clear()
|
||||
|
||||
yield
|
||||
|
||||
# Restore original state
|
||||
crewai_event_bus._handlers.clear()
|
||||
crewai_event_bus._handlers.update(original_handlers)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_tracing_singletons(self):
|
||||
"""Reset tracing singleton instances between tests"""
|
||||
# Reset TraceCollectionListener singleton
|
||||
if hasattr(TraceCollectionListener, "_instance"):
|
||||
TraceCollectionListener._instance = None
|
||||
TraceCollectionListener._initialized = False
|
||||
|
||||
yield
|
||||
|
||||
# Clean up after test
|
||||
if hasattr(TraceCollectionListener, "_instance"):
|
||||
TraceCollectionListener._instance = None
|
||||
TraceCollectionListener._initialized = False
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_plus_api_calls(self):
|
||||
"""Mock all PlusAPI HTTP calls to avoid network requests"""
|
||||
with (
|
||||
patch("requests.post") as mock_post,
|
||||
patch("requests.get") as mock_get,
|
||||
patch("requests.put") as mock_put,
|
||||
patch("requests.delete") as mock_delete,
|
||||
patch.object(TraceBatchManager, "initialize_batch", return_value=None),
|
||||
patch.object(
|
||||
TraceBatchManager, "_finalize_backend_batch", return_value=True
|
||||
),
|
||||
patch.object(TraceBatchManager, "_cleanup_batch_data", return_value=True),
|
||||
):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"id": "mock_trace_batch_id",
|
||||
"status": "success",
|
||||
"message": "Batch created successfully",
|
||||
}
|
||||
mock_response.raise_for_status.return_value = None
|
||||
|
||||
mock_post.return_value = mock_response
|
||||
mock_get.return_value = mock_response
|
||||
mock_put.return_value = mock_response
|
||||
mock_delete.return_value = mock_response
|
||||
|
||||
yield {
|
||||
"post": mock_post,
|
||||
"get": mock_get,
|
||||
"put": mock_put,
|
||||
"delete": mock_delete,
|
||||
}
|
||||
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
def test_trace_listener_collects_crew_events(self):
|
||||
"""Test that trace listener properly collects events from crew execution"""
|
||||
|
||||
with patch.dict(os.environ, {"CREWAI_TRACING_ENABLED": "true"}):
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
task = Task(
|
||||
description="Say hello to the world",
|
||||
expected_output="hello world",
|
||||
agent=agent,
|
||||
)
|
||||
crew = Crew(agents=[agent], tasks=[task], verbose=True)
|
||||
|
||||
trace_listener = TraceCollectionListener()
|
||||
from crewai.utilities.events import crewai_event_bus
|
||||
|
||||
trace_listener.setup_listeners(crewai_event_bus)
|
||||
|
||||
with patch.object(
|
||||
trace_listener.batch_manager,
|
||||
"initialize_batch",
|
||||
return_value=None,
|
||||
) as initialize_mock:
|
||||
crew.kickoff()
|
||||
|
||||
assert initialize_mock.call_count >= 1
|
||||
|
||||
call_args = initialize_mock.call_args_list[0]
|
||||
assert len(call_args[0]) == 2 # user_context, execution_metadata
|
||||
_, execution_metadata = call_args[0]
|
||||
assert isinstance(execution_metadata, dict)
|
||||
assert "crew_name" in execution_metadata
|
||||
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
def test_batch_manager_finalizes_batch_clears_buffer(self):
|
||||
"""Test that batch manager properly finalizes batch and clears buffer"""
|
||||
|
||||
with patch.dict(os.environ, {"CREWAI_TRACING_ENABLED": "true"}):
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
|
||||
task = Task(
|
||||
description="Say hello to the world",
|
||||
expected_output="hello world",
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
crew = Crew(agents=[agent], tasks=[task], verbose=True)
|
||||
|
||||
from crewai.utilities.events import crewai_event_bus
|
||||
|
||||
trace_listener = None
|
||||
for handler_list in crewai_event_bus._handlers.values():
|
||||
for handler in handler_list:
|
||||
if hasattr(handler, "__self__") and isinstance(
|
||||
handler.__self__, TraceCollectionListener
|
||||
):
|
||||
trace_listener = handler.__self__
|
||||
break
|
||||
if trace_listener:
|
||||
break
|
||||
|
||||
if not trace_listener:
|
||||
pytest.skip(
|
||||
"No trace listener found - tracing may not be properly enabled"
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
trace_listener.batch_manager,
|
||||
"finalize_batch",
|
||||
wraps=trace_listener.batch_manager.finalize_batch,
|
||||
) as finalize_mock:
|
||||
crew.kickoff()
|
||||
|
||||
assert finalize_mock.call_count >= 1
|
||||
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
def test_events_collection_batch_manager(self, mock_plus_api_calls):
|
||||
"""Test that trace listener properly collects events from crew execution"""
|
||||
|
||||
with patch.dict(os.environ, {"CREWAI_TRACING_ENABLED": "true"}):
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
task = Task(
|
||||
description="Say hello to the world",
|
||||
expected_output="hello world",
|
||||
agent=agent,
|
||||
)
|
||||
crew = Crew(agents=[agent], tasks=[task], verbose=True)
|
||||
|
||||
from crewai.utilities.events import crewai_event_bus
|
||||
|
||||
# Create and setup trace listener explicitly
|
||||
trace_listener = TraceCollectionListener()
|
||||
trace_listener.setup_listeners(crewai_event_bus)
|
||||
|
||||
with patch.object(
|
||||
trace_listener.batch_manager,
|
||||
"add_event",
|
||||
wraps=trace_listener.batch_manager.add_event,
|
||||
) as add_event_mock:
|
||||
crew.kickoff()
|
||||
|
||||
assert add_event_mock.call_count >= 2
|
||||
|
||||
completion_events = [
|
||||
call.args[0]
|
||||
for call in add_event_mock.call_args_list
|
||||
if call.args[0].type == "crew_kickoff_completed"
|
||||
]
|
||||
assert len(completion_events) >= 1
|
||||
|
||||
# Verify the first completion event has proper structure
|
||||
completion_event = completion_events[0]
|
||||
assert "crew_name" in completion_event.event_data
|
||||
assert completion_event.event_data["crew_name"] == "crew"
|
||||
|
||||
# Verify all events have proper structure
|
||||
for call in add_event_mock.call_args_list:
|
||||
event = call.args[0]
|
||||
assert isinstance(event, TraceEvent)
|
||||
assert hasattr(event, "event_data")
|
||||
assert hasattr(event, "type")
|
||||
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
def test_trace_listener_disabled_when_env_false(self):
|
||||
"""Test that trace listener doesn't make HTTP calls when tracing is disabled"""
|
||||
|
||||
with patch.dict(os.environ, {"CREWAI_TRACING_ENABLED": "false"}):
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
task = Task(
|
||||
description="Say hello to the world",
|
||||
expected_output="hello world",
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
crew = Crew(agents=[agent], tasks=[task], verbose=True)
|
||||
result = crew.kickoff()
|
||||
assert result is not None
|
||||
|
||||
from crewai.utilities.events import crewai_event_bus
|
||||
|
||||
trace_handlers = []
|
||||
for handlers in crewai_event_bus._handlers.values():
|
||||
for handler in handlers:
|
||||
if hasattr(handler, "__self__") and isinstance(
|
||||
handler.__self__, TraceCollectionListener
|
||||
):
|
||||
trace_handlers.append(handler)
|
||||
elif hasattr(handler, "__name__") and any(
|
||||
trace_name in handler.__name__
|
||||
for trace_name in [
|
||||
"on_crew_started",
|
||||
"on_crew_completed",
|
||||
"on_flow_started",
|
||||
]
|
||||
):
|
||||
trace_handlers.append(handler)
|
||||
|
||||
assert len(trace_handlers) == 0, (
|
||||
f"Found {len(trace_handlers)} trace handlers when tracing should be disabled"
|
||||
)
|
||||
|
||||
def test_trace_listener_setup_correctly(self):
|
||||
"""Test that trace listener is set up correctly when enabled"""
|
||||
|
||||
with patch.dict(os.environ, {"CREWAI_TRACING_ENABLED": "true"}):
|
||||
trace_listener = TraceCollectionListener()
|
||||
|
||||
assert trace_listener.trace_enabled is True
|
||||
assert trace_listener.batch_manager is not None
|
||||
assert trace_listener.trace_sender is not None
|
||||
|
||||
# Helper method to ensure cleanup
|
||||
def teardown_method(self):
|
||||
"""Cleanup after each test method"""
|
||||
from crewai.utilities.events import crewai_event_bus
|
||||
|
||||
crewai_event_bus._handlers.clear()
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
"""Final cleanup after all tests in this class"""
|
||||
from crewai.utilities.events import crewai_event_bus
|
||||
|
||||
crewai_event_bus._handlers.clear()
|
||||
25
tests/utilities/test_embedding_configuration.py
Normal file
25
tests/utilities/test_embedding_configuration.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.rag.embeddings.configurator import EmbeddingConfigurator
|
||||
|
||||
|
||||
def test_configure_embedder_importerror():
|
||||
configurator = EmbeddingConfigurator()
|
||||
|
||||
embedder_config = {
|
||||
'provider': 'openai',
|
||||
'config': {
|
||||
'model': 'text-embedding-ada-002',
|
||||
}
|
||||
}
|
||||
|
||||
with patch('chromadb.utils.embedding_functions.openai_embedding_function.OpenAIEmbeddingFunction') as mock_openai:
|
||||
mock_openai.side_effect = ImportError("Module not found.")
|
||||
|
||||
with pytest.raises(ImportError) as exc_info:
|
||||
configurator.configure_embedder(embedder_config)
|
||||
|
||||
assert str(exc_info.value) == "Module not found."
|
||||
mock_openai.assert_called_once()
|
||||
@@ -64,7 +64,8 @@ def base_agent():
|
||||
llm="gpt-4o-mini",
|
||||
goal="Just say hi",
|
||||
backstory="You are a helpful assistant that just says hi",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def base_task(base_agent):
|
||||
@@ -74,6 +75,7 @@ def base_task(base_agent):
|
||||
agent=base_agent,
|
||||
)
|
||||
|
||||
|
||||
event_listener = EventListener()
|
||||
|
||||
|
||||
@@ -448,6 +450,27 @@ def test_flow_emits_start_event():
|
||||
assert received_events[0].type == "flow_started"
|
||||
|
||||
|
||||
def test_flow_name_emitted_to_event_bus():
|
||||
received_events = []
|
||||
|
||||
class MyFlowClass(Flow):
|
||||
name = "PRODUCTION_FLOW"
|
||||
|
||||
@start()
|
||||
def start(self):
|
||||
return "Hello, world!"
|
||||
|
||||
@crewai_event_bus.on(FlowStartedEvent)
|
||||
def handle_flow_start(source, event):
|
||||
received_events.append(event)
|
||||
|
||||
flow = MyFlowClass()
|
||||
flow.kickoff()
|
||||
|
||||
assert len(received_events) == 1
|
||||
assert received_events[0].flow_name == "PRODUCTION_FLOW"
|
||||
|
||||
|
||||
def test_flow_emits_finish_event():
|
||||
received_events = []
|
||||
|
||||
@@ -756,6 +779,7 @@ def test_streaming_empty_response_handling():
|
||||
received_chunks = []
|
||||
|
||||
with crewai_event_bus.scoped_handlers():
|
||||
|
||||
@crewai_event_bus.on(LLMStreamChunkEvent)
|
||||
def handle_stream_chunk(source, event):
|
||||
received_chunks.append(event.chunk)
|
||||
@@ -793,6 +817,7 @@ def test_streaming_empty_response_handling():
|
||||
# Restore the original method
|
||||
llm.call = original_call
|
||||
|
||||
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
def test_stream_llm_emits_event_with_task_and_agent_info():
|
||||
completed_event = []
|
||||
@@ -801,6 +826,7 @@ def test_stream_llm_emits_event_with_task_and_agent_info():
|
||||
stream_event = []
|
||||
|
||||
with crewai_event_bus.scoped_handlers():
|
||||
|
||||
@crewai_event_bus.on(LLMCallFailedEvent)
|
||||
def handle_llm_failed(source, event):
|
||||
failed_event.append(event)
|
||||
@@ -827,7 +853,7 @@ def test_stream_llm_emits_event_with_task_and_agent_info():
|
||||
description="Just say hi",
|
||||
expected_output="hi",
|
||||
llm=LLM(model="gpt-4o-mini", stream=True),
|
||||
agent=agent
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
crew = Crew(agents=[agent], tasks=[task])
|
||||
@@ -855,6 +881,7 @@ def test_stream_llm_emits_event_with_task_and_agent_info():
|
||||
assert set(all_task_id) == {task.id}
|
||||
assert set(all_task_name) == {task.name}
|
||||
|
||||
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
def test_llm_emits_event_with_task_and_agent_info(base_agent, base_task):
|
||||
completed_event = []
|
||||
@@ -863,6 +890,7 @@ def test_llm_emits_event_with_task_and_agent_info(base_agent, base_task):
|
||||
stream_event = []
|
||||
|
||||
with crewai_event_bus.scoped_handlers():
|
||||
|
||||
@crewai_event_bus.on(LLMCallFailedEvent)
|
||||
def handle_llm_failed(source, event):
|
||||
failed_event.append(event)
|
||||
@@ -904,6 +932,7 @@ def test_llm_emits_event_with_task_and_agent_info(base_agent, base_task):
|
||||
assert set(all_task_id) == {base_task.id}
|
||||
assert set(all_task_name) == {base_task.name}
|
||||
|
||||
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
def test_llm_emits_event_with_lite_agent():
|
||||
completed_event = []
|
||||
@@ -912,6 +941,7 @@ def test_llm_emits_event_with_lite_agent():
|
||||
stream_event = []
|
||||
|
||||
with crewai_event_bus.scoped_handlers():
|
||||
|
||||
@crewai_event_bus.on(LLMCallFailedEvent)
|
||||
def handle_llm_failed(source, event):
|
||||
failed_event.append(event)
|
||||
@@ -936,7 +966,6 @@ def test_llm_emits_event_with_lite_agent():
|
||||
)
|
||||
agent.kickoff(messages=[{"role": "user", "content": "say hi!"}])
|
||||
|
||||
|
||||
assert len(completed_event) == 2
|
||||
assert len(failed_event) == 0
|
||||
assert len(started_event) == 2
|
||||
|
||||
17
uv.lock
generated
17
uv.lock
generated
@@ -798,13 +798,13 @@ requires-dist = [
|
||||
{ name = "blinker", specifier = ">=1.9.0" },
|
||||
{ name = "chromadb", specifier = ">=0.5.23" },
|
||||
{ name = "click", specifier = ">=8.1.7" },
|
||||
{ name = "crewai-tools", marker = "extra == 'tools'", specifier = "~=0.58.0" },
|
||||
{ name = "crewai-tools", marker = "extra == 'tools'", specifier = "~=0.60.0" },
|
||||
{ name = "docling", marker = "extra == 'docling'", specifier = ">=2.12.0" },
|
||||
{ name = "instructor", specifier = ">=1.3.3" },
|
||||
{ name = "json-repair", specifier = "==0.25.2" },
|
||||
{ name = "json5", specifier = ">=0.10.0" },
|
||||
{ name = "jsonref", specifier = ">=1.1.0" },
|
||||
{ name = "litellm", specifier = "==1.74.3" },
|
||||
{ name = "litellm", specifier = "==1.74.9" },
|
||||
{ name = "mem0ai", marker = "extra == 'mem0'", specifier = ">=0.1.94" },
|
||||
{ name = "onnxruntime", specifier = "==1.22.0" },
|
||||
{ name = "openai", specifier = ">=1.13.3" },
|
||||
@@ -850,7 +850,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "crewai-tools"
|
||||
version = "0.58.0"
|
||||
version = "0.60.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "chromadb" },
|
||||
@@ -860,6 +860,7 @@ dependencies = [
|
||||
{ name = "embedchain" },
|
||||
{ name = "lancedb" },
|
||||
{ name = "openai" },
|
||||
{ name = "portalocker" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pyright" },
|
||||
{ name = "pytube" },
|
||||
@@ -867,9 +868,9 @@ dependencies = [
|
||||
{ name = "stagehand" },
|
||||
{ name = "tiktoken" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/bf/72c3a0cb5a8be1f635a4e3b07ee2ad81a6d427e63b7748c2727a33ade0d4/crewai_tools-0.58.0.tar.gz", hash = "sha256:ea82d5df8611ae22a8291934c4cd0b7ed5b77eca475f81014f018b7eca4d3350", size = 1026853, upload-time = "2025-07-23T17:45:53.228Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/60/04fd70a8a15eaf4147ff648ada44f1d4afd453a528cf8facd618ef32e576/crewai_tools-0.60.0.tar.gz", hash = "sha256:9234f6912b65495afe5e1bfa330abca09a40725d47fe2c71a22387bf6eeb8e72", size = 1032373, upload-time = "2025-08-06T20:27:16.003Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/34/bd/1de36fbf8fb717817d3bf72a94da38e27af6cc5b888d7c1203a3f0b0cc2f/crewai_tools-0.58.0-py3-none-any.whl", hash = "sha256:151688bf0fa8c90e27dcdbaa8619f3dee2a14e97f1b420a38187b12d88305175", size = 650113, upload-time = "2025-07-23T17:45:51.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/55/984f3d2d5afbcfa87c380c7c17b728804e80617b768b3748f25220b2b32c/crewai_tools-0.60.0-py3-none-any.whl", hash = "sha256:a54277c973753de4a3269da17e5a7e4995d4c70fc331eb2872189b5f92cfdaaf", size = 657128, upload-time = "2025-08-06T20:27:14.295Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2304,7 +2305,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "litellm"
|
||||
version = "1.74.3"
|
||||
version = "1.74.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -2319,9 +2320,9 @@ dependencies = [
|
||||
{ name = "tiktoken" },
|
||||
{ name = "tokenizers" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/e3/3091066f6682016840e9a36111560656b609b95de04b2ec7b19ad2580eaa/litellm-1.74.3.tar.gz", hash = "sha256:a9e87ebe78947ceec67e75f830f1c956cc653b84563574241acea9c84e7e3ca1", size = 9256457, upload-time = "2025-07-12T20:06:06.128Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/5d/646bebdb4769d77e6a018b9152c9ccf17afe15d0f88974f338d3f2ee7c15/litellm-1.74.9.tar.gz", hash = "sha256:4a32eff70342e1aee4d1cbf2de2a6ed64a7c39d86345c58d4401036af018b7de", size = 9660510, upload-time = "2025-07-28T16:42:39.297Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/6f/07735b5178f32e28daf8a30ed6ad3e2c8c06ac374dc06aecde007110470f/litellm-1.74.3-py3-none-any.whl", hash = "sha256:638ec73633c6f2cf78a7343723d8f3bc13c192558fcbaa29f3ba6bc7802e8663", size = 8618899, upload-time = "2025-07-12T20:06:03.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e4/f1546746049c99c6b8b247e2f34485b9eae36faa9322b84e2a17262e6712/litellm-1.74.9-py3-none-any.whl", hash = "sha256:ab8f8a6e4d8689d3c7c4f9c3bbc7e46212cc3ebc74ddd0f3c0c921bb459c9874", size = 8740449, upload-time = "2025-07-28T16:42:36.8Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user