mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-07 18:19:00 +00:00
Compare commits
6 Commits
devin/1763
...
devin/1763
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ca58e2b8f | ||
|
|
9da1f0c0aa | ||
|
|
a559cedbd1 | ||
|
|
9b7d475750 | ||
|
|
bcc3e358cb | ||
|
|
d160f0874a |
@@ -90,6 +90,9 @@ from crewai_tools.tools.json_search_tool.json_search_tool import JSONSearchTool
|
||||
from crewai_tools.tools.linkup.linkup_search_tool import LinkupSearchTool
|
||||
from crewai_tools.tools.llamaindex_tool.llamaindex_tool import LlamaIndexTool
|
||||
from crewai_tools.tools.mdx_search_tool.mdx_search_tool import MDXSearchTool
|
||||
from crewai_tools.tools.merge_agent_handler_tool.merge_agent_handler_tool import (
|
||||
MergeAgentHandlerTool,
|
||||
)
|
||||
from crewai_tools.tools.mongodb_vector_search_tool.vector_search import (
|
||||
MongoDBVectorSearchConfig,
|
||||
MongoDBVectorSearchTool,
|
||||
@@ -235,6 +238,7 @@ __all__ = [
|
||||
"LlamaIndexTool",
|
||||
"MCPServerAdapter",
|
||||
"MDXSearchTool",
|
||||
"MergeAgentHandlerTool",
|
||||
"MongoDBVectorSearchConfig",
|
||||
"MongoDBVectorSearchTool",
|
||||
"MultiOnTool",
|
||||
|
||||
@@ -79,6 +79,9 @@ from crewai_tools.tools.json_search_tool.json_search_tool import JSONSearchTool
|
||||
from crewai_tools.tools.linkup.linkup_search_tool import LinkupSearchTool
|
||||
from crewai_tools.tools.llamaindex_tool.llamaindex_tool import LlamaIndexTool
|
||||
from crewai_tools.tools.mdx_search_tool.mdx_search_tool import MDXSearchTool
|
||||
from crewai_tools.tools.merge_agent_handler_tool.merge_agent_handler_tool import (
|
||||
MergeAgentHandlerTool,
|
||||
)
|
||||
from crewai_tools.tools.mongodb_vector_search_tool import (
|
||||
MongoDBToolSchema,
|
||||
MongoDBVectorSearchConfig,
|
||||
@@ -218,6 +221,7 @@ __all__ = [
|
||||
"LinkupSearchTool",
|
||||
"LlamaIndexTool",
|
||||
"MDXSearchTool",
|
||||
"MergeAgentHandlerTool",
|
||||
"MongoDBToolSchema",
|
||||
"MongoDBVectorSearchConfig",
|
||||
"MongoDBVectorSearchTool",
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
# MergeAgentHandlerTool Documentation
|
||||
|
||||
## Description
|
||||
|
||||
This tool is a wrapper around the Merge Agent Handler platform and gives your agent access to third-party tools and integrations via the Model Context Protocol (MCP). Merge Agent Handler securely manages authentication, permissions, and monitoring of all tool interactions across platforms like Linear, Jira, Slack, GitHub, and many more.
|
||||
|
||||
## Installation
|
||||
|
||||
### Step 1: Set up a virtual environment (recommended)
|
||||
|
||||
It's recommended to use a virtual environment to avoid conflicts with other packages:
|
||||
|
||||
```shell
|
||||
# Create a virtual environment
|
||||
python3 -m venv venv
|
||||
|
||||
# Activate the virtual environment
|
||||
# On macOS/Linux:
|
||||
source venv/bin/activate
|
||||
|
||||
# On Windows:
|
||||
# venv\Scripts\activate
|
||||
```
|
||||
|
||||
### Step 2: Install CrewAI Tools
|
||||
|
||||
To incorporate this tool into your project, install CrewAI with tools support:
|
||||
|
||||
```shell
|
||||
pip install 'crewai[tools]'
|
||||
```
|
||||
|
||||
### Step 3: Set up your Agent Handler credentials
|
||||
|
||||
You'll need to set up your Agent Handler API key. You can get your API key from the [Agent Handler dashboard](https://ah.merge.dev).
|
||||
|
||||
```shell
|
||||
# Set the API key in your current terminal session
|
||||
export AGENT_HANDLER_API_KEY='your-api-key-here'
|
||||
|
||||
# Or add it to your shell profile for persistence (e.g., ~/.bashrc, ~/.zshrc)
|
||||
echo "export AGENT_HANDLER_API_KEY='your-api-key-here'" >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
**Alternative: Use a `.env` file**
|
||||
|
||||
You can also use a `.env` file in your project directory:
|
||||
|
||||
```shell
|
||||
# Create a .env file
|
||||
echo "AGENT_HANDLER_API_KEY=your-api-key-here" > .env
|
||||
|
||||
# Load it in your Python script
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
```
|
||||
|
||||
**Note**: Make sure to add `.env` to your `.gitignore` to avoid committing secrets!
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using this tool, you need to:
|
||||
|
||||
1. **Create a Tool Pack** in Agent Handler with the connectors and tools you want to use
|
||||
2. **Register a User** who will be executing the tools
|
||||
3. **Authenticate connectors** for the registered user (using Agent Handler Link)
|
||||
|
||||
You can do this via the [Agent Handler dashboard](https://ah.merge.dev) or the [Agent Handler API](https://docs.ah.merge.dev).
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Example 1: Using a specific tool
|
||||
|
||||
The following example demonstrates how to initialize a specific tool and use it with a CrewAI agent:
|
||||
|
||||
```python
|
||||
from crewai_tools import MergeAgentHandlerTool
|
||||
from crewai import Agent, Task
|
||||
|
||||
# Initialize a specific tool
|
||||
create_issue_tool = MergeAgentHandlerTool.from_tool_name(
|
||||
tool_name="linear__create_issue",
|
||||
tool_pack_id="134e0111-0f67-44f6-98f0-597000290bb3",
|
||||
registered_user_id="91b2b905-e866-40c8-8be2-efe53827a0aa"
|
||||
)
|
||||
|
||||
# Define agent with the tool
|
||||
project_manager = Agent(
|
||||
role="Project Manager",
|
||||
goal="Create and manage project tasks efficiently",
|
||||
backstory=(
|
||||
"You are an experienced project manager who tracks tasks "
|
||||
"and issues across various project management tools."
|
||||
),
|
||||
verbose=True,
|
||||
tools=[create_issue_tool],
|
||||
)
|
||||
|
||||
# Execute task
|
||||
task = Task(
|
||||
description="Create a new issue in Linear titled 'Implement user authentication' with high priority",
|
||||
agent=project_manager,
|
||||
expected_output="Confirmation that the issue was created with its ID",
|
||||
)
|
||||
|
||||
task.execute()
|
||||
```
|
||||
|
||||
### Example 2: Loading all tools from a Tool Pack
|
||||
|
||||
You can load all tools from a Tool Pack at once:
|
||||
|
||||
```python
|
||||
from crewai_tools import MergeAgentHandlerTool
|
||||
from crewai import Agent, Task
|
||||
|
||||
# Load all tools from a Tool Pack
|
||||
tools = MergeAgentHandlerTool.from_tool_pack(
|
||||
tool_pack_id="134e0111-0f67-44f6-98f0-597000290bb3",
|
||||
registered_user_id="91b2b905-e866-40c8-8be2-efe53827a0aa"
|
||||
)
|
||||
|
||||
# Define agent with all tools
|
||||
support_agent = Agent(
|
||||
role="Support Engineer",
|
||||
goal="Handle customer support requests across multiple platforms",
|
||||
backstory=(
|
||||
"You are a skilled support engineer who can access customer "
|
||||
"data and create tickets across various support tools."
|
||||
),
|
||||
verbose=True,
|
||||
tools=tools,
|
||||
)
|
||||
```
|
||||
|
||||
### Example 3: Loading specific tools from a Tool Pack
|
||||
|
||||
You can also load only specific tools from a Tool Pack:
|
||||
|
||||
```python
|
||||
from crewai_tools import MergeAgentHandlerTool
|
||||
|
||||
# Load only specific tools
|
||||
tools = MergeAgentHandlerTool.from_tool_pack(
|
||||
tool_pack_id="134e0111-0f67-44f6-98f0-597000290bb3",
|
||||
registered_user_id="91b2b905-e866-40c8-8be2-efe53827a0aa",
|
||||
tool_names=["linear__create_issue", "linear__get_issues", "slack__send_message"]
|
||||
)
|
||||
```
|
||||
|
||||
### Example 4: Using with local/staging environment
|
||||
|
||||
For development, you can point to a different Agent Handler environment:
|
||||
|
||||
```python
|
||||
from crewai_tools import MergeAgentHandlerTool
|
||||
|
||||
# Use with local or staging environment
|
||||
tool = MergeAgentHandlerTool.from_tool_name(
|
||||
tool_name="linear__create_issue",
|
||||
tool_pack_id="your-tool-pack-id",
|
||||
registered_user_id="your-user-id",
|
||||
base_url="http://localhost:8000" # or your staging URL
|
||||
)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Class Methods
|
||||
|
||||
#### `from_tool_name()`
|
||||
|
||||
Create a single tool instance for a specific tool.
|
||||
|
||||
**Parameters:**
|
||||
- `tool_name` (str): Name of the tool (e.g., "linear__create_issue")
|
||||
- `tool_pack_id` (str): UUID of the Tool Pack
|
||||
- `registered_user_id` (str): UUID or origin_id of the registered user
|
||||
- `base_url` (str, optional): Base URL for Agent Handler API (defaults to "https://api.ah.merge.dev")
|
||||
|
||||
**Returns:** `MergeAgentHandlerTool` instance
|
||||
|
||||
#### `from_tool_pack()`
|
||||
|
||||
Create multiple tool instances from a Tool Pack.
|
||||
|
||||
**Parameters:**
|
||||
- `tool_pack_id` (str): UUID of the Tool Pack
|
||||
- `registered_user_id` (str): UUID or origin_id of the registered user
|
||||
- `tool_names` (List[str], optional): List of specific tool names to load. If None, loads all tools.
|
||||
- `base_url` (str, optional): Base URL for Agent Handler API (defaults to "https://api.ah.merge.dev")
|
||||
|
||||
**Returns:** `List[MergeAgentHandlerTool]` instances
|
||||
|
||||
## Available Connectors
|
||||
|
||||
Merge Agent Handler supports 100+ integrations including:
|
||||
|
||||
**Project Management:** Linear, Jira, Asana, Monday, ClickUp, Height, Shortcut
|
||||
|
||||
**Communication:** Slack, Microsoft Teams, Discord
|
||||
|
||||
**CRM:** Salesforce, HubSpot, Pipedrive
|
||||
|
||||
**Development:** GitHub, GitLab, Bitbucket
|
||||
|
||||
**Documentation:** Notion, Confluence, Google Docs
|
||||
|
||||
**And many more...**
|
||||
|
||||
For a complete list of available connectors and tools, visit the [Agent Handler documentation](https://docs.ah.merge.dev).
|
||||
|
||||
## Authentication
|
||||
|
||||
Agent Handler handles all authentication for you. Users authenticate to third-party services via Agent Handler Link, and the platform securely manages tokens and credentials. Your agents can then execute tools without worrying about authentication details.
|
||||
|
||||
## Security
|
||||
|
||||
All tool executions are:
|
||||
- **Logged and monitored** for audit trails
|
||||
- **Scanned for PII** to prevent sensitive data leaks
|
||||
- **Rate limited** based on your plan
|
||||
- **Permission-controlled** at the user and organization level
|
||||
|
||||
## Support
|
||||
|
||||
For questions or issues:
|
||||
- 📚 [Documentation](https://docs.ah.merge.dev)
|
||||
- 💬 [Discord Community](https://merge.dev/discord)
|
||||
- 📧 [Support Email](mailto:support@merge.dev)
|
||||
@@ -0,0 +1,8 @@
|
||||
"""Merge Agent Handler tool for CrewAI."""
|
||||
|
||||
from crewai_tools.tools.merge_agent_handler_tool.merge_agent_handler_tool import (
|
||||
MergeAgentHandlerTool,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["MergeAgentHandlerTool"]
|
||||
@@ -0,0 +1,362 @@
|
||||
"""Merge Agent Handler tools wrapper for CrewAI."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from crewai.tools import BaseTool, EnvVar
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
import requests
|
||||
import typing_extensions as te
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MergeAgentHandlerToolError(Exception):
|
||||
"""Base exception for Merge Agent Handler tool errors."""
|
||||
|
||||
|
||||
|
||||
class MergeAgentHandlerTool(BaseTool):
|
||||
"""
|
||||
Wrapper for Merge Agent Handler tools.
|
||||
|
||||
This tool allows CrewAI agents to execute tools from Merge Agent Handler,
|
||||
which provides secure access to third-party integrations via the Model Context Protocol (MCP).
|
||||
|
||||
Agent Handler manages authentication, permissions, and monitoring of all tool interactions.
|
||||
"""
|
||||
|
||||
tool_pack_id: str = Field(
|
||||
..., description="UUID of the Agent Handler Tool Pack to use"
|
||||
)
|
||||
registered_user_id: str = Field(
|
||||
..., description="UUID or origin_id of the registered user"
|
||||
)
|
||||
tool_name: str = Field(..., description="Name of the specific tool to execute")
|
||||
base_url: str = Field(
|
||||
default="https://ah-api.merge.dev",
|
||||
description="Base URL for Agent Handler API",
|
||||
)
|
||||
session_id: str | None = Field(
|
||||
default=None, description="MCP session ID (generated if not provided)"
|
||||
)
|
||||
env_vars: list[EnvVar] = Field(
|
||||
default_factory=lambda: [
|
||||
EnvVar(
|
||||
name="AGENT_HANDLER_API_KEY",
|
||||
description="Production API key for Agent Handler services",
|
||||
required=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
"""Initialize session ID if not provided."""
|
||||
super().model_post_init(__context)
|
||||
if self.session_id is None:
|
||||
self.session_id = str(uuid4())
|
||||
|
||||
def _get_api_key(self) -> str:
|
||||
"""Get the API key from environment variables."""
|
||||
import os
|
||||
|
||||
api_key = os.environ.get("AGENT_HANDLER_API_KEY")
|
||||
if not api_key:
|
||||
raise MergeAgentHandlerToolError(
|
||||
"AGENT_HANDLER_API_KEY environment variable is required. "
|
||||
"Set it with: export AGENT_HANDLER_API_KEY='your-key-here'"
|
||||
)
|
||||
return api_key
|
||||
|
||||
def _make_mcp_request(
|
||||
self, method: str, params: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Make a JSON-RPC 2.0 MCP request to Agent Handler."""
|
||||
url = f"{self.base_url}/api/v1/tool-packs/{self.tool_pack_id}/registered-users/{self.registered_user_id}/mcp"
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self._get_api_key()}",
|
||||
"Mcp-Session-Id": self.session_id or str(uuid4()),
|
||||
}
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"id": str(uuid4()),
|
||||
}
|
||||
|
||||
if params:
|
||||
payload["params"] = params
|
||||
|
||||
# Log the full payload for debugging
|
||||
logger.debug(f"MCP Request to {url}: {json.dumps(payload, indent=2)}")
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=payload, headers=headers, timeout=60)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
# Handle JSON-RPC error responses
|
||||
if "error" in result:
|
||||
error_msg = result["error"].get("message", "Unknown error")
|
||||
error_code = result["error"].get("code", -1)
|
||||
logger.error(
|
||||
f"Agent Handler API error (code {error_code}): {error_msg}"
|
||||
)
|
||||
raise MergeAgentHandlerToolError(f"API Error: {error_msg}")
|
||||
|
||||
return result
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Failed to call Agent Handler API: {e!s}")
|
||||
raise MergeAgentHandlerToolError(
|
||||
f"Failed to communicate with Agent Handler API: {e!s}"
|
||||
) from e
|
||||
|
||||
def _run(self, **kwargs: Any) -> Any:
|
||||
"""Execute the Agent Handler tool with the given arguments."""
|
||||
try:
|
||||
# Log what we're about to send
|
||||
logger.info(f"Executing {self.tool_name} with arguments: {kwargs}")
|
||||
|
||||
# Make the tool call via MCP
|
||||
result = self._make_mcp_request(
|
||||
method="tools/call",
|
||||
params={"name": self.tool_name, "arguments": kwargs},
|
||||
)
|
||||
|
||||
# Extract the actual result from the MCP response
|
||||
if "result" in result and "content" in result["result"]:
|
||||
content = result["result"]["content"]
|
||||
if content and len(content) > 0:
|
||||
# Parse the text content (it's JSON-encoded)
|
||||
text_content = content[0].get("text", "")
|
||||
try:
|
||||
return json.loads(text_content)
|
||||
except json.JSONDecodeError:
|
||||
return text_content
|
||||
|
||||
return result
|
||||
|
||||
except MergeAgentHandlerToolError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error executing tool {self.tool_name}: {e!s}")
|
||||
raise MergeAgentHandlerToolError(f"Tool execution failed: {e!s}") from e
|
||||
|
||||
@classmethod
|
||||
def from_tool_name(
|
||||
cls,
|
||||
tool_name: str,
|
||||
tool_pack_id: str,
|
||||
registered_user_id: str,
|
||||
base_url: str = "https://ah-api.merge.dev",
|
||||
**kwargs: Any,
|
||||
) -> te.Self:
|
||||
"""
|
||||
Create a MergeAgentHandlerTool from a tool name.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool (e.g., "linear__create_issue")
|
||||
tool_pack_id: UUID of the Tool Pack
|
||||
registered_user_id: UUID of the registered user
|
||||
base_url: Base URL for Agent Handler API (defaults to production)
|
||||
**kwargs: Additional arguments to pass to the tool
|
||||
|
||||
Returns:
|
||||
MergeAgentHandlerTool instance ready to use
|
||||
|
||||
Example:
|
||||
>>> tool = MergeAgentHandlerTool.from_tool_name(
|
||||
... tool_name="linear__create_issue",
|
||||
... tool_pack_id="134e0111-0f67-44f6-98f0-597000290bb3",
|
||||
... registered_user_id="91b2b905-e866-40c8-8be2-efe53827a0aa"
|
||||
... )
|
||||
"""
|
||||
# Create an empty args schema model (proper BaseModel subclass)
|
||||
empty_args_schema = create_model(f"{tool_name.replace('__', '_').title()}Args")
|
||||
|
||||
# Initialize session and get tool schema
|
||||
instance = cls(
|
||||
name=tool_name,
|
||||
description=f"Execute {tool_name} via Agent Handler",
|
||||
tool_pack_id=tool_pack_id,
|
||||
registered_user_id=registered_user_id,
|
||||
tool_name=tool_name,
|
||||
base_url=base_url,
|
||||
args_schema=empty_args_schema, # Empty schema that properly inherits from BaseModel
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Try to fetch the actual tool schema from Agent Handler
|
||||
try:
|
||||
result = instance._make_mcp_request(method="tools/list")
|
||||
if "result" in result and "tools" in result["result"]:
|
||||
tools = result["result"]["tools"]
|
||||
tool_schema = next(
|
||||
(t for t in tools if t.get("name") == tool_name), None
|
||||
)
|
||||
|
||||
if tool_schema:
|
||||
instance.description = tool_schema.get(
|
||||
"description", instance.description
|
||||
)
|
||||
|
||||
# Convert parameters schema to Pydantic model
|
||||
if "parameters" in tool_schema:
|
||||
try:
|
||||
params = tool_schema["parameters"]
|
||||
if params.get("type") == "object" and "properties" in params:
|
||||
# Build field definitions for Pydantic
|
||||
fields = {}
|
||||
properties = params["properties"]
|
||||
required = params.get("required", [])
|
||||
|
||||
for field_name, field_schema in properties.items():
|
||||
field_type = Any # Default type
|
||||
field_default = ... # Required by default
|
||||
|
||||
# Map JSON schema types to Python types
|
||||
json_type = field_schema.get("type", "string")
|
||||
if json_type == "string":
|
||||
field_type = str
|
||||
elif json_type == "integer":
|
||||
field_type = int
|
||||
elif json_type == "number":
|
||||
field_type = float
|
||||
elif json_type == "boolean":
|
||||
field_type = bool
|
||||
elif json_type == "array":
|
||||
field_type = list[Any]
|
||||
elif json_type == "object":
|
||||
field_type = dict[str, Any]
|
||||
|
||||
# Make field optional if not required
|
||||
if field_name not in required:
|
||||
field_type = field_type | None
|
||||
field_default = None
|
||||
|
||||
field_description = field_schema.get("description")
|
||||
if field_description:
|
||||
fields[field_name] = (
|
||||
field_type,
|
||||
Field(
|
||||
default=field_default,
|
||||
description=field_description,
|
||||
),
|
||||
)
|
||||
else:
|
||||
fields[field_name] = (field_type, field_default)
|
||||
|
||||
# Create the Pydantic model
|
||||
if fields:
|
||||
args_schema = create_model(
|
||||
f"{tool_name.replace('__', '_').title()}Args",
|
||||
**fields,
|
||||
)
|
||||
instance.args_schema = args_schema
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to create args schema for {tool_name}: {e!s}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to fetch tool schema for {tool_name}, using defaults: {e!s}"
|
||||
)
|
||||
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def from_tool_pack(
|
||||
cls,
|
||||
tool_pack_id: str,
|
||||
registered_user_id: str,
|
||||
tool_names: list[str] | None = None,
|
||||
base_url: str = "https://ah-api.merge.dev",
|
||||
**kwargs: Any,
|
||||
) -> list[te.Self]:
|
||||
"""
|
||||
Create multiple MergeAgentHandlerTool instances from a Tool Pack.
|
||||
|
||||
Args:
|
||||
tool_pack_id: UUID of the Tool Pack
|
||||
registered_user_id: UUID or origin_id of the registered user
|
||||
tool_names: Optional list of specific tool names to load. If None, loads all tools.
|
||||
base_url: Base URL for Agent Handler API (defaults to production)
|
||||
**kwargs: Additional arguments to pass to each tool
|
||||
|
||||
Returns:
|
||||
List of MergeAgentHandlerTool instances
|
||||
|
||||
Example:
|
||||
>>> tools = MergeAgentHandlerTool.from_tool_pack(
|
||||
... tool_pack_id="134e0111-0f67-44f6-98f0-597000290bb3",
|
||||
... registered_user_id="91b2b905-e866-40c8-8be2-efe53827a0aa",
|
||||
... tool_names=["linear__create_issue", "linear__get_issues"]
|
||||
... )
|
||||
"""
|
||||
# Create a temporary instance to fetch the tool list
|
||||
temp_instance = cls(
|
||||
name="temp",
|
||||
description="temp",
|
||||
tool_pack_id=tool_pack_id,
|
||||
registered_user_id=registered_user_id,
|
||||
tool_name="temp",
|
||||
base_url=base_url,
|
||||
args_schema=BaseModel,
|
||||
)
|
||||
|
||||
try:
|
||||
# Fetch available tools
|
||||
result = temp_instance._make_mcp_request(method="tools/list")
|
||||
|
||||
if "result" not in result or "tools" not in result["result"]:
|
||||
raise MergeAgentHandlerToolError(
|
||||
"Failed to fetch tools from Agent Handler Tool Pack"
|
||||
)
|
||||
|
||||
available_tools = result["result"]["tools"]
|
||||
|
||||
# Filter tools if specific names were requested
|
||||
if tool_names:
|
||||
available_tools = [
|
||||
t for t in available_tools if t.get("name") in tool_names
|
||||
]
|
||||
|
||||
# Check if all requested tools were found
|
||||
found_names = {t.get("name") for t in available_tools}
|
||||
missing_names = set(tool_names) - found_names
|
||||
if missing_names:
|
||||
logger.warning(
|
||||
f"The following tools were not found in the Tool Pack: {missing_names}"
|
||||
)
|
||||
|
||||
# Create tool instances
|
||||
tools = []
|
||||
for tool_schema in available_tools:
|
||||
tool_name = tool_schema.get("name")
|
||||
if not tool_name:
|
||||
continue
|
||||
|
||||
tool = cls.from_tool_name(
|
||||
tool_name=tool_name,
|
||||
tool_pack_id=tool_pack_id,
|
||||
registered_user_id=registered_user_id,
|
||||
base_url=base_url,
|
||||
**kwargs,
|
||||
)
|
||||
tools.append(tool)
|
||||
|
||||
return tools
|
||||
|
||||
except MergeAgentHandlerToolError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create tools from Tool Pack: {e!s}")
|
||||
raise MergeAgentHandlerToolError(f"Failed to load Tool Pack: {e!s}") from e
|
||||
490
lib/crewai-tools/tests/tools/merge_agent_handler_tool_test.py
Normal file
490
lib/crewai-tools/tests/tools/merge_agent_handler_tool_test.py
Normal file
@@ -0,0 +1,490 @@
|
||||
"""Tests for MergeAgentHandlerTool."""
|
||||
|
||||
import os
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai_tools import MergeAgentHandlerTool
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_agent_handler_api_key():
|
||||
"""Mock the Agent Handler API key environment variable."""
|
||||
with patch.dict(os.environ, {"AGENT_HANDLER_API_KEY": "test_key"}):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_tool_pack_response():
|
||||
"""Mock response for tools/list MCP request."""
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "test-id",
|
||||
"result": {
|
||||
"tools": [
|
||||
{
|
||||
"name": "linear__create_issue",
|
||||
"description": "Creates a new issue in Linear",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "The issue title",
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "The issue description",
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"description": "Priority level (1-4)",
|
||||
},
|
||||
},
|
||||
"required": ["title"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "linear__get_issues",
|
||||
"description": "Get issues from Linear",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"type": "object",
|
||||
"description": "Filter criteria",
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_tool_execute_response():
|
||||
"""Mock response for tools/call MCP request."""
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "test-id",
|
||||
"result": {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": '{"success": true, "id": "ISS-123", "title": "Test Issue"}',
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_tool_initialization():
|
||||
"""Test basic tool initialization."""
|
||||
tool = MergeAgentHandlerTool(
|
||||
name="test_tool",
|
||||
description="Test tool",
|
||||
tool_pack_id="test-pack-id",
|
||||
registered_user_id="test-user-id",
|
||||
tool_name="linear__create_issue",
|
||||
)
|
||||
|
||||
assert tool.name == "test_tool"
|
||||
assert "Test tool" in tool.description # Description gets formatted by BaseTool
|
||||
assert tool.tool_pack_id == "test-pack-id"
|
||||
assert tool.registered_user_id == "test-user-id"
|
||||
assert tool.tool_name == "linear__create_issue"
|
||||
assert tool.base_url == "https://ah-api.merge.dev"
|
||||
assert tool.session_id is not None
|
||||
|
||||
|
||||
def test_tool_initialization_with_custom_base_url():
|
||||
"""Test tool initialization with custom base URL."""
|
||||
tool = MergeAgentHandlerTool(
|
||||
name="test_tool",
|
||||
description="Test tool",
|
||||
tool_pack_id="test-pack-id",
|
||||
registered_user_id="test-user-id",
|
||||
tool_name="linear__create_issue",
|
||||
base_url="http://localhost:8000",
|
||||
)
|
||||
|
||||
assert tool.base_url == "http://localhost:8000"
|
||||
|
||||
|
||||
def test_missing_api_key():
|
||||
"""Test that missing API key raises appropriate error."""
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
tool = MergeAgentHandlerTool(
|
||||
name="test_tool",
|
||||
description="Test tool",
|
||||
tool_pack_id="test-pack-id",
|
||||
registered_user_id="test-user-id",
|
||||
tool_name="linear__create_issue",
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
tool._get_api_key()
|
||||
|
||||
assert "AGENT_HANDLER_API_KEY" in str(exc_info.value)
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_mcp_request_success(mock_post, mock_tool_pack_response):
|
||||
"""Test successful MCP request."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = mock_tool_pack_response
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool = MergeAgentHandlerTool(
|
||||
name="test_tool",
|
||||
description="Test tool",
|
||||
tool_pack_id="test-pack-id",
|
||||
registered_user_id="test-user-id",
|
||||
tool_name="linear__create_issue",
|
||||
)
|
||||
|
||||
result = tool._make_mcp_request(method="tools/list")
|
||||
|
||||
assert "result" in result
|
||||
assert "tools" in result["result"]
|
||||
assert len(result["result"]["tools"]) == 2
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_mcp_request_error(mock_post):
|
||||
"""Test MCP request with error response."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "test-id",
|
||||
"error": {"code": -32601, "message": "Method not found"},
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool = MergeAgentHandlerTool(
|
||||
name="test_tool",
|
||||
description="Test tool",
|
||||
tool_pack_id="test-pack-id",
|
||||
registered_user_id="test-user-id",
|
||||
tool_name="linear__create_issue",
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
tool._make_mcp_request(method="invalid/method")
|
||||
|
||||
assert "Method not found" in str(exc_info.value)
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_mcp_request_http_error(mock_post):
|
||||
"""Test MCP request with HTTP error."""
|
||||
mock_post.side_effect = Exception("Connection error")
|
||||
|
||||
tool = MergeAgentHandlerTool(
|
||||
name="test_tool",
|
||||
description="Test tool",
|
||||
tool_pack_id="test-pack-id",
|
||||
registered_user_id="test-user-id",
|
||||
tool_name="linear__create_issue",
|
||||
)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
tool._make_mcp_request(method="tools/list")
|
||||
|
||||
assert "Connection error" in str(exc_info.value)
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_tool_execution(mock_post, mock_tool_execute_response):
|
||||
"""Test tool execution via _run method."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = mock_tool_execute_response
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool = MergeAgentHandlerTool(
|
||||
name="test_tool",
|
||||
description="Test tool",
|
||||
tool_pack_id="test-pack-id",
|
||||
registered_user_id="test-user-id",
|
||||
tool_name="linear__create_issue",
|
||||
)
|
||||
|
||||
result = tool._run(title="Test Issue", description="Test description")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["id"] == "ISS-123"
|
||||
assert result["title"] == "Test Issue"
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_from_tool_name(mock_post, mock_tool_pack_response):
|
||||
"""Test creating tool from tool name."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = mock_tool_pack_response
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool = MergeAgentHandlerTool.from_tool_name(
|
||||
tool_name="linear__create_issue",
|
||||
tool_pack_id="test-pack-id",
|
||||
registered_user_id="test-user-id",
|
||||
)
|
||||
|
||||
assert tool.name == "linear__create_issue"
|
||||
assert tool.description == "Creates a new issue in Linear"
|
||||
assert tool.tool_name == "linear__create_issue"
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_from_tool_name_with_custom_base_url(mock_post, mock_tool_pack_response):
|
||||
"""Test creating tool from tool name with custom base URL."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = mock_tool_pack_response
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool = MergeAgentHandlerTool.from_tool_name(
|
||||
tool_name="linear__create_issue",
|
||||
tool_pack_id="test-pack-id",
|
||||
registered_user_id="test-user-id",
|
||||
base_url="http://localhost:8000",
|
||||
)
|
||||
|
||||
assert tool.base_url == "http://localhost:8000"
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_from_tool_pack_all_tools(mock_post, mock_tool_pack_response):
|
||||
"""Test creating all tools from a Tool Pack."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = mock_tool_pack_response
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tools = MergeAgentHandlerTool.from_tool_pack(
|
||||
tool_pack_id="test-pack-id",
|
||||
registered_user_id="test-user-id",
|
||||
)
|
||||
|
||||
assert len(tools) == 2
|
||||
assert tools[0].name == "linear__create_issue"
|
||||
assert tools[1].name == "linear__get_issues"
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_from_tool_pack_specific_tools(mock_post, mock_tool_pack_response):
|
||||
"""Test creating specific tools from a Tool Pack."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = mock_tool_pack_response
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tools = MergeAgentHandlerTool.from_tool_pack(
|
||||
tool_pack_id="test-pack-id",
|
||||
registered_user_id="test-user-id",
|
||||
tool_names=["linear__create_issue"],
|
||||
)
|
||||
|
||||
assert len(tools) == 1
|
||||
assert tools[0].name == "linear__create_issue"
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_from_tool_pack_with_custom_base_url(mock_post, mock_tool_pack_response):
|
||||
"""Test creating tools from Tool Pack with custom base URL."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = mock_tool_pack_response
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tools = MergeAgentHandlerTool.from_tool_pack(
|
||||
tool_pack_id="test-pack-id",
|
||||
registered_user_id="test-user-id",
|
||||
base_url="http://localhost:8000",
|
||||
)
|
||||
|
||||
assert len(tools) == 2
|
||||
assert all(tool.base_url == "http://localhost:8000" for tool in tools)
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_tool_execution_with_text_response(mock_post):
|
||||
"""Test tool execution with plain text response."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "test-id",
|
||||
"result": {"content": [{"type": "text", "text": "Plain text result"}]},
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool = MergeAgentHandlerTool(
|
||||
name="test_tool",
|
||||
description="Test tool",
|
||||
tool_pack_id="test-pack-id",
|
||||
registered_user_id="test-user-id",
|
||||
tool_name="linear__create_issue",
|
||||
)
|
||||
|
||||
result = tool._run(title="Test")
|
||||
|
||||
assert result == "Plain text result"
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_mcp_request_builds_correct_url(mock_post, mock_tool_pack_response):
|
||||
"""Test that MCP request builds correct URL."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = mock_tool_pack_response
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool = MergeAgentHandlerTool(
|
||||
name="test_tool",
|
||||
description="Test tool",
|
||||
tool_pack_id="test-pack-123",
|
||||
registered_user_id="user-456",
|
||||
tool_name="linear__create_issue",
|
||||
base_url="https://ah-api.merge.dev",
|
||||
)
|
||||
|
||||
tool._make_mcp_request(method="tools/list")
|
||||
|
||||
expected_url = (
|
||||
"https://ah-api.merge.dev/api/v1/tool-packs/"
|
||||
"test-pack-123/registered-users/user-456/mcp"
|
||||
)
|
||||
mock_post.assert_called_once()
|
||||
assert mock_post.call_args[0][0] == expected_url
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_mcp_request_includes_correct_headers(mock_post, mock_tool_pack_response):
|
||||
"""Test that MCP request includes correct headers."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = mock_tool_pack_response
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool = MergeAgentHandlerTool(
|
||||
name="test_tool",
|
||||
description="Test tool",
|
||||
tool_pack_id="test-pack-id",
|
||||
registered_user_id="test-user-id",
|
||||
tool_name="linear__create_issue",
|
||||
)
|
||||
|
||||
tool._make_mcp_request(method="tools/list")
|
||||
|
||||
mock_post.assert_called_once()
|
||||
headers = mock_post.call_args.kwargs["headers"]
|
||||
assert headers["Content-Type"] == "application/json"
|
||||
assert headers["Authorization"] == "Bearer test_key"
|
||||
assert "Mcp-Session-Id" in headers
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_tool_parameters_are_passed_in_request(mock_post):
|
||||
"""Test that tool parameters are correctly included in the MCP request."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "test-id",
|
||||
"result": {"content": [{"type": "text", "text": '{"success": true}'}]},
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
tool = MergeAgentHandlerTool(
|
||||
name="test_tool",
|
||||
description="Test tool",
|
||||
tool_pack_id="test-pack-id",
|
||||
registered_user_id="test-user-id",
|
||||
tool_name="linear__update_issue",
|
||||
)
|
||||
|
||||
# Execute tool with specific parameters
|
||||
tool._run(id="issue-123", title="New Title", priority=1)
|
||||
|
||||
# Verify the request was made
|
||||
mock_post.assert_called_once()
|
||||
|
||||
# Get the JSON payload that was sent
|
||||
payload = mock_post.call_args.kwargs["json"]
|
||||
|
||||
# Verify MCP structure
|
||||
assert payload["jsonrpc"] == "2.0"
|
||||
assert payload["method"] == "tools/call"
|
||||
assert "id" in payload
|
||||
|
||||
# Verify parameters are in the request
|
||||
assert "params" in payload
|
||||
assert payload["params"]["name"] == "linear__update_issue"
|
||||
assert "arguments" in payload["params"]
|
||||
|
||||
# Verify the actual arguments were passed
|
||||
arguments = payload["params"]["arguments"]
|
||||
assert arguments["id"] == "issue-123"
|
||||
assert arguments["title"] == "New Title"
|
||||
assert arguments["priority"] == 1
|
||||
|
||||
|
||||
@patch("requests.post")
|
||||
def test_tool_run_method_passes_parameters(mock_post, mock_tool_pack_response):
|
||||
"""Test that parameters are passed when using the .run() method (how CrewAI calls it)."""
|
||||
# Mock the tools/list response
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
|
||||
# First call: tools/list
|
||||
# Second call: tools/call
|
||||
mock_response.json.side_effect = [
|
||||
mock_tool_pack_response, # tools/list response
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "test-id",
|
||||
"result": {"content": [{"type": "text", "text": '{"success": true, "id": "issue-123"}'}]},
|
||||
}, # tools/call response
|
||||
]
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
# Create tool using from_tool_name (which fetches schema)
|
||||
tool = MergeAgentHandlerTool.from_tool_name(
|
||||
tool_name="linear__create_issue",
|
||||
tool_pack_id="test-pack-id",
|
||||
registered_user_id="test-user-id",
|
||||
)
|
||||
|
||||
# Call using .run() method (this is how CrewAI invokes tools)
|
||||
result = tool.run(title="Test Issue", description="Test description", priority=2)
|
||||
|
||||
# Verify two calls were made: tools/list and tools/call
|
||||
assert mock_post.call_count == 2
|
||||
|
||||
# Get the second call (tools/call)
|
||||
second_call = mock_post.call_args_list[1]
|
||||
payload = second_call.kwargs["json"]
|
||||
|
||||
# Verify it's a tools/call request
|
||||
assert payload["method"] == "tools/call"
|
||||
assert payload["params"]["name"] == "linear__create_issue"
|
||||
|
||||
# Verify parameters were passed
|
||||
arguments = payload["params"]["arguments"]
|
||||
assert arguments["title"] == "Test Issue"
|
||||
assert arguments["description"] == "Test description"
|
||||
assert arguments["priority"] == 2
|
||||
|
||||
# Verify result was returned
|
||||
assert result["success"] is True
|
||||
assert result["id"] == "issue-123"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -101,24 +101,25 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class EventListener(BaseEventListener):
|
||||
_instance = None
|
||||
_instance: EventListener | None = None
|
||||
_initialized: bool = False
|
||||
_telemetry: Telemetry = PrivateAttr(default_factory=lambda: Telemetry())
|
||||
logger = Logger(verbose=True, default_color=EMITTER_COLOR)
|
||||
logger: Logger = Logger(verbose=True, default_color=EMITTER_COLOR)
|
||||
execution_spans: dict[Task, Any] = Field(default_factory=dict)
|
||||
next_chunk = 0
|
||||
text_stream = StringIO()
|
||||
knowledge_retrieval_in_progress = False
|
||||
knowledge_query_in_progress = False
|
||||
next_chunk: int = 0
|
||||
text_stream: StringIO = StringIO()
|
||||
knowledge_retrieval_in_progress: bool = False
|
||||
knowledge_query_in_progress: bool = False
|
||||
method_branches: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
def __new__(cls):
|
||||
def __new__(cls) -> EventListener:
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance._initialized = False
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if not hasattr(self, "_initialized") or not self._initialized:
|
||||
def __init__(self) -> None:
|
||||
if not self._initialized:
|
||||
super().__init__()
|
||||
self._telemetry = Telemetry()
|
||||
self._telemetry.set_tracer()
|
||||
@@ -136,14 +137,14 @@ class EventListener(BaseEventListener):
|
||||
|
||||
def setup_listeners(self, crewai_event_bus: CrewAIEventsBus) -> None:
|
||||
@crewai_event_bus.on(CrewKickoffStartedEvent)
|
||||
def on_crew_started(source, event: CrewKickoffStartedEvent) -> None:
|
||||
def on_crew_started(source: Any, event: CrewKickoffStartedEvent) -> None:
|
||||
with self._crew_tree_lock:
|
||||
self.formatter.create_crew_tree(event.crew_name or "Crew", source.id)
|
||||
self._telemetry.crew_execution_span(source, event.inputs)
|
||||
self._crew_tree_lock.notify_all()
|
||||
|
||||
@crewai_event_bus.on(CrewKickoffCompletedEvent)
|
||||
def on_crew_completed(source, event: CrewKickoffCompletedEvent) -> None:
|
||||
def on_crew_completed(source: Any, event: CrewKickoffCompletedEvent) -> None:
|
||||
# Handle telemetry
|
||||
final_string_output = event.output.raw
|
||||
self._telemetry.end_crew(source, final_string_output)
|
||||
@@ -157,7 +158,7 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(CrewKickoffFailedEvent)
|
||||
def on_crew_failed(source, event: CrewKickoffFailedEvent) -> None:
|
||||
def on_crew_failed(source: Any, event: CrewKickoffFailedEvent) -> None:
|
||||
self.formatter.update_crew_tree(
|
||||
self.formatter.current_crew_tree,
|
||||
event.crew_name or "Crew",
|
||||
@@ -166,23 +167,23 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(CrewTrainStartedEvent)
|
||||
def on_crew_train_started(source, event: CrewTrainStartedEvent) -> None:
|
||||
def on_crew_train_started(_: Any, event: CrewTrainStartedEvent) -> None:
|
||||
self.formatter.handle_crew_train_started(
|
||||
event.crew_name or "Crew", str(event.timestamp)
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(CrewTrainCompletedEvent)
|
||||
def on_crew_train_completed(source, event: CrewTrainCompletedEvent) -> None:
|
||||
def on_crew_train_completed(_: Any, event: CrewTrainCompletedEvent) -> None:
|
||||
self.formatter.handle_crew_train_completed(
|
||||
event.crew_name or "Crew", str(event.timestamp)
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(CrewTrainFailedEvent)
|
||||
def on_crew_train_failed(source, event: CrewTrainFailedEvent) -> None:
|
||||
def on_crew_train_failed(_: Any, event: CrewTrainFailedEvent) -> None:
|
||||
self.formatter.handle_crew_train_failed(event.crew_name or "Crew")
|
||||
|
||||
@crewai_event_bus.on(CrewTestResultEvent)
|
||||
def on_crew_test_result(source, event: CrewTestResultEvent) -> None:
|
||||
def on_crew_test_result(source: Any, event: CrewTestResultEvent) -> None:
|
||||
self._telemetry.individual_test_result_span(
|
||||
source.crew,
|
||||
event.quality,
|
||||
@@ -193,7 +194,7 @@ class EventListener(BaseEventListener):
|
||||
# ----------- TASK EVENTS -----------
|
||||
|
||||
@crewai_event_bus.on(TaskStartedEvent)
|
||||
def on_task_started(source, event: TaskStartedEvent) -> None:
|
||||
def on_task_started(source: Any, event: TaskStartedEvent) -> None:
|
||||
span = self._telemetry.task_started(crew=source.agent.crew, task=source)
|
||||
self.execution_spans[source] = span
|
||||
|
||||
@@ -211,7 +212,7 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(TaskCompletedEvent)
|
||||
def on_task_completed(source, event: TaskCompletedEvent):
|
||||
def on_task_completed(source: Any, event: TaskCompletedEvent) -> None:
|
||||
# Handle telemetry
|
||||
span = self.execution_spans.get(source)
|
||||
if span:
|
||||
@@ -229,7 +230,7 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(TaskFailedEvent)
|
||||
def on_task_failed(source, event: TaskFailedEvent):
|
||||
def on_task_failed(source: Any, event: TaskFailedEvent) -> None:
|
||||
span = self.execution_spans.get(source)
|
||||
if span:
|
||||
if source.agent and source.agent.crew:
|
||||
@@ -249,7 +250,9 @@ class EventListener(BaseEventListener):
|
||||
# ----------- AGENT EVENTS -----------
|
||||
|
||||
@crewai_event_bus.on(AgentExecutionStartedEvent)
|
||||
def on_agent_execution_started(source, event: AgentExecutionStartedEvent):
|
||||
def on_agent_execution_started(
|
||||
_: Any, event: AgentExecutionStartedEvent
|
||||
) -> None:
|
||||
self.formatter.create_agent_branch(
|
||||
self.formatter.current_task_branch,
|
||||
event.agent.role,
|
||||
@@ -257,7 +260,9 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(AgentExecutionCompletedEvent)
|
||||
def on_agent_execution_completed(source, event: AgentExecutionCompletedEvent):
|
||||
def on_agent_execution_completed(
|
||||
_: Any, event: AgentExecutionCompletedEvent
|
||||
) -> None:
|
||||
self.formatter.update_agent_status(
|
||||
self.formatter.current_agent_branch,
|
||||
event.agent.role,
|
||||
@@ -268,8 +273,8 @@ class EventListener(BaseEventListener):
|
||||
|
||||
@crewai_event_bus.on(LiteAgentExecutionStartedEvent)
|
||||
def on_lite_agent_execution_started(
|
||||
source, event: LiteAgentExecutionStartedEvent
|
||||
):
|
||||
_: Any, event: LiteAgentExecutionStartedEvent
|
||||
) -> None:
|
||||
"""Handle LiteAgent execution started event."""
|
||||
self.formatter.handle_lite_agent_execution(
|
||||
event.agent_info["role"], status="started", **event.agent_info
|
||||
@@ -277,15 +282,17 @@ class EventListener(BaseEventListener):
|
||||
|
||||
@crewai_event_bus.on(LiteAgentExecutionCompletedEvent)
|
||||
def on_lite_agent_execution_completed(
|
||||
source, event: LiteAgentExecutionCompletedEvent
|
||||
):
|
||||
_: Any, event: LiteAgentExecutionCompletedEvent
|
||||
) -> None:
|
||||
"""Handle LiteAgent execution completed event."""
|
||||
self.formatter.handle_lite_agent_execution(
|
||||
event.agent_info["role"], status="completed", **event.agent_info
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(LiteAgentExecutionErrorEvent)
|
||||
def on_lite_agent_execution_error(source, event: LiteAgentExecutionErrorEvent):
|
||||
def on_lite_agent_execution_error(
|
||||
_: Any, event: LiteAgentExecutionErrorEvent
|
||||
) -> None:
|
||||
"""Handle LiteAgent execution error event."""
|
||||
self.formatter.handle_lite_agent_execution(
|
||||
event.agent_info["role"],
|
||||
@@ -297,26 +304,28 @@ class EventListener(BaseEventListener):
|
||||
# ----------- FLOW EVENTS -----------
|
||||
|
||||
@crewai_event_bus.on(FlowCreatedEvent)
|
||||
def on_flow_created(source, event: FlowCreatedEvent):
|
||||
def on_flow_created(_: Any, event: FlowCreatedEvent) -> None:
|
||||
self._telemetry.flow_creation_span(event.flow_name)
|
||||
tree = self.formatter.create_flow_tree(event.flow_name, str(source.flow_id))
|
||||
self.formatter.current_flow_tree = tree
|
||||
|
||||
@crewai_event_bus.on(FlowStartedEvent)
|
||||
def on_flow_started(source, event: FlowStartedEvent):
|
||||
def on_flow_started(source: Any, event: FlowStartedEvent) -> None:
|
||||
self._telemetry.flow_execution_span(
|
||||
event.flow_name, list(source._methods.keys())
|
||||
)
|
||||
tree = self.formatter.create_flow_tree(event.flow_name, str(source.flow_id))
|
||||
self.formatter.current_flow_tree = tree
|
||||
self.formatter.start_flow(event.flow_name, str(source.flow_id))
|
||||
|
||||
@crewai_event_bus.on(FlowFinishedEvent)
|
||||
def on_flow_finished(source, event: FlowFinishedEvent):
|
||||
def on_flow_finished(source: Any, event: FlowFinishedEvent) -> None:
|
||||
self.formatter.update_flow_status(
|
||||
self.formatter.current_flow_tree, event.flow_name, source.flow_id
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(MethodExecutionStartedEvent)
|
||||
def on_method_execution_started(source, event: MethodExecutionStartedEvent):
|
||||
def on_method_execution_started(
|
||||
_: Any, event: MethodExecutionStartedEvent
|
||||
) -> None:
|
||||
method_branch = self.method_branches.get(event.method_name)
|
||||
updated_branch = self.formatter.update_method_status(
|
||||
method_branch,
|
||||
@@ -327,7 +336,9 @@ class EventListener(BaseEventListener):
|
||||
self.method_branches[event.method_name] = updated_branch
|
||||
|
||||
@crewai_event_bus.on(MethodExecutionFinishedEvent)
|
||||
def on_method_execution_finished(source, event: MethodExecutionFinishedEvent):
|
||||
def on_method_execution_finished(
|
||||
_: Any, event: MethodExecutionFinishedEvent
|
||||
) -> None:
|
||||
method_branch = self.method_branches.get(event.method_name)
|
||||
updated_branch = self.formatter.update_method_status(
|
||||
method_branch,
|
||||
@@ -338,7 +349,9 @@ class EventListener(BaseEventListener):
|
||||
self.method_branches[event.method_name] = updated_branch
|
||||
|
||||
@crewai_event_bus.on(MethodExecutionFailedEvent)
|
||||
def on_method_execution_failed(source, event: MethodExecutionFailedEvent):
|
||||
def on_method_execution_failed(
|
||||
_: Any, event: MethodExecutionFailedEvent
|
||||
) -> None:
|
||||
method_branch = self.method_branches.get(event.method_name)
|
||||
updated_branch = self.formatter.update_method_status(
|
||||
method_branch,
|
||||
@@ -351,7 +364,7 @@ class EventListener(BaseEventListener):
|
||||
# ----------- TOOL USAGE EVENTS -----------
|
||||
|
||||
@crewai_event_bus.on(ToolUsageStartedEvent)
|
||||
def on_tool_usage_started(source, event: ToolUsageStartedEvent):
|
||||
def on_tool_usage_started(source: Any, event: ToolUsageStartedEvent) -> None:
|
||||
if isinstance(source, LLM):
|
||||
self.formatter.handle_llm_tool_usage_started(
|
||||
event.tool_name,
|
||||
@@ -365,7 +378,7 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(ToolUsageFinishedEvent)
|
||||
def on_tool_usage_finished(source, event: ToolUsageFinishedEvent):
|
||||
def on_tool_usage_finished(source: Any, event: ToolUsageFinishedEvent) -> None:
|
||||
if isinstance(source, LLM):
|
||||
self.formatter.handle_llm_tool_usage_finished(
|
||||
event.tool_name,
|
||||
@@ -378,7 +391,7 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(ToolUsageErrorEvent)
|
||||
def on_tool_usage_error(source, event: ToolUsageErrorEvent):
|
||||
def on_tool_usage_error(source: Any, event: ToolUsageErrorEvent) -> None:
|
||||
if isinstance(source, LLM):
|
||||
self.formatter.handle_llm_tool_usage_error(
|
||||
event.tool_name,
|
||||
@@ -395,7 +408,7 @@ class EventListener(BaseEventListener):
|
||||
# ----------- LLM EVENTS -----------
|
||||
|
||||
@crewai_event_bus.on(LLMCallStartedEvent)
|
||||
def on_llm_call_started(source, event: LLMCallStartedEvent):
|
||||
def on_llm_call_started(_: Any, event: LLMCallStartedEvent) -> None:
|
||||
# Capture the returned tool branch and update the current_tool_branch reference
|
||||
thinking_branch = self.formatter.handle_llm_call_started(
|
||||
self.formatter.current_agent_branch,
|
||||
@@ -406,7 +419,7 @@ class EventListener(BaseEventListener):
|
||||
self.formatter.current_tool_branch = thinking_branch
|
||||
|
||||
@crewai_event_bus.on(LLMCallCompletedEvent)
|
||||
def on_llm_call_completed(source, event: LLMCallCompletedEvent):
|
||||
def on_llm_call_completed(_: Any, event: LLMCallCompletedEvent) -> None:
|
||||
self.formatter.handle_llm_call_completed(
|
||||
self.formatter.current_tool_branch,
|
||||
self.formatter.current_agent_branch,
|
||||
@@ -414,7 +427,7 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(LLMCallFailedEvent)
|
||||
def on_llm_call_failed(source, event: LLMCallFailedEvent):
|
||||
def on_llm_call_failed(_: Any, event: LLMCallFailedEvent) -> None:
|
||||
self.formatter.handle_llm_call_failed(
|
||||
self.formatter.current_tool_branch,
|
||||
event.error,
|
||||
@@ -422,7 +435,7 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(LLMStreamChunkEvent)
|
||||
def on_llm_stream_chunk(source, event: LLMStreamChunkEvent):
|
||||
def on_llm_stream_chunk(_: Any, event: LLMStreamChunkEvent) -> None:
|
||||
self.text_stream.write(event.chunk)
|
||||
self.text_stream.seek(self.next_chunk)
|
||||
self.text_stream.read()
|
||||
@@ -431,7 +444,7 @@ class EventListener(BaseEventListener):
|
||||
# ----------- LLM GUARDRAIL EVENTS -----------
|
||||
|
||||
@crewai_event_bus.on(LLMGuardrailStartedEvent)
|
||||
def on_llm_guardrail_started(source, event: LLMGuardrailStartedEvent):
|
||||
def on_llm_guardrail_started(_: Any, event: LLMGuardrailStartedEvent) -> None:
|
||||
guardrail_str = str(event.guardrail)
|
||||
guardrail_name = (
|
||||
guardrail_str[:50] + "..." if len(guardrail_str) > 50 else guardrail_str
|
||||
@@ -440,13 +453,15 @@ class EventListener(BaseEventListener):
|
||||
self.formatter.handle_guardrail_started(guardrail_name, event.retry_count)
|
||||
|
||||
@crewai_event_bus.on(LLMGuardrailCompletedEvent)
|
||||
def on_llm_guardrail_completed(source, event: LLMGuardrailCompletedEvent):
|
||||
def on_llm_guardrail_completed(
|
||||
_: Any, event: LLMGuardrailCompletedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_guardrail_completed(
|
||||
event.success, event.error, event.retry_count
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(CrewTestStartedEvent)
|
||||
def on_crew_test_started(source, event: CrewTestStartedEvent):
|
||||
def on_crew_test_started(source: Any, event: CrewTestStartedEvent) -> None:
|
||||
cloned_crew = source.copy()
|
||||
self._telemetry.test_execution_span(
|
||||
cloned_crew,
|
||||
@@ -460,20 +475,20 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(CrewTestCompletedEvent)
|
||||
def on_crew_test_completed(source, event: CrewTestCompletedEvent):
|
||||
def on_crew_test_completed(_: Any, event: CrewTestCompletedEvent) -> None:
|
||||
self.formatter.handle_crew_test_completed(
|
||||
self.formatter.current_flow_tree,
|
||||
event.crew_name or "Crew",
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(CrewTestFailedEvent)
|
||||
def on_crew_test_failed(source, event: CrewTestFailedEvent):
|
||||
def on_crew_test_failed(_: Any, event: CrewTestFailedEvent) -> None:
|
||||
self.formatter.handle_crew_test_failed(event.crew_name or "Crew")
|
||||
|
||||
@crewai_event_bus.on(KnowledgeRetrievalStartedEvent)
|
||||
def on_knowledge_retrieval_started(
|
||||
source, event: KnowledgeRetrievalStartedEvent
|
||||
):
|
||||
_: Any, event: KnowledgeRetrievalStartedEvent
|
||||
) -> None:
|
||||
if self.knowledge_retrieval_in_progress:
|
||||
return
|
||||
|
||||
@@ -486,8 +501,8 @@ class EventListener(BaseEventListener):
|
||||
|
||||
@crewai_event_bus.on(KnowledgeRetrievalCompletedEvent)
|
||||
def on_knowledge_retrieval_completed(
|
||||
source, event: KnowledgeRetrievalCompletedEvent
|
||||
):
|
||||
_: Any, event: KnowledgeRetrievalCompletedEvent
|
||||
) -> None:
|
||||
if not self.knowledge_retrieval_in_progress:
|
||||
return
|
||||
|
||||
@@ -499,11 +514,13 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(KnowledgeQueryStartedEvent)
|
||||
def on_knowledge_query_started(source, event: KnowledgeQueryStartedEvent):
|
||||
def on_knowledge_query_started(
|
||||
_: Any, event: KnowledgeQueryStartedEvent
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@crewai_event_bus.on(KnowledgeQueryFailedEvent)
|
||||
def on_knowledge_query_failed(source, event: KnowledgeQueryFailedEvent):
|
||||
def on_knowledge_query_failed(_: Any, event: KnowledgeQueryFailedEvent) -> None:
|
||||
self.formatter.handle_knowledge_query_failed(
|
||||
self.formatter.current_agent_branch,
|
||||
event.error,
|
||||
@@ -511,13 +528,15 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(KnowledgeQueryCompletedEvent)
|
||||
def on_knowledge_query_completed(source, event: KnowledgeQueryCompletedEvent):
|
||||
def on_knowledge_query_completed(
|
||||
_: Any, event: KnowledgeQueryCompletedEvent
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@crewai_event_bus.on(KnowledgeSearchQueryFailedEvent)
|
||||
def on_knowledge_search_query_failed(
|
||||
source, event: KnowledgeSearchQueryFailedEvent
|
||||
):
|
||||
_: Any, event: KnowledgeSearchQueryFailedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_knowledge_search_query_failed(
|
||||
self.formatter.current_agent_branch,
|
||||
event.error,
|
||||
@@ -527,7 +546,9 @@ class EventListener(BaseEventListener):
|
||||
# ----------- REASONING EVENTS -----------
|
||||
|
||||
@crewai_event_bus.on(AgentReasoningStartedEvent)
|
||||
def on_agent_reasoning_started(source, event: AgentReasoningStartedEvent):
|
||||
def on_agent_reasoning_started(
|
||||
_: Any, event: AgentReasoningStartedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_reasoning_started(
|
||||
self.formatter.current_agent_branch,
|
||||
event.attempt,
|
||||
@@ -535,7 +556,9 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(AgentReasoningCompletedEvent)
|
||||
def on_agent_reasoning_completed(source, event: AgentReasoningCompletedEvent):
|
||||
def on_agent_reasoning_completed(
|
||||
_: Any, event: AgentReasoningCompletedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_reasoning_completed(
|
||||
event.plan,
|
||||
event.ready,
|
||||
@@ -543,7 +566,7 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(AgentReasoningFailedEvent)
|
||||
def on_agent_reasoning_failed(source, event: AgentReasoningFailedEvent):
|
||||
def on_agent_reasoning_failed(_: Any, event: AgentReasoningFailedEvent) -> None:
|
||||
self.formatter.handle_reasoning_failed(
|
||||
event.error,
|
||||
self.formatter.current_crew_tree,
|
||||
@@ -552,7 +575,7 @@ class EventListener(BaseEventListener):
|
||||
# ----------- AGENT LOGGING EVENTS -----------
|
||||
|
||||
@crewai_event_bus.on(AgentLogsStartedEvent)
|
||||
def on_agent_logs_started(source, event: AgentLogsStartedEvent):
|
||||
def on_agent_logs_started(_: Any, event: AgentLogsStartedEvent) -> None:
|
||||
self.formatter.handle_agent_logs_started(
|
||||
event.agent_role,
|
||||
event.task_description,
|
||||
@@ -560,7 +583,7 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(AgentLogsExecutionEvent)
|
||||
def on_agent_logs_execution(source, event: AgentLogsExecutionEvent):
|
||||
def on_agent_logs_execution(_: Any, event: AgentLogsExecutionEvent) -> None:
|
||||
self.formatter.handle_agent_logs_execution(
|
||||
event.agent_role,
|
||||
event.formatted_answer,
|
||||
@@ -568,7 +591,7 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(A2ADelegationStartedEvent)
|
||||
def on_a2a_delegation_started(source, event: A2ADelegationStartedEvent):
|
||||
def on_a2a_delegation_started(_: Any, event: A2ADelegationStartedEvent) -> None:
|
||||
self.formatter.handle_a2a_delegation_started(
|
||||
event.endpoint,
|
||||
event.task_description,
|
||||
@@ -578,7 +601,9 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(A2ADelegationCompletedEvent)
|
||||
def on_a2a_delegation_completed(source, event: A2ADelegationCompletedEvent):
|
||||
def on_a2a_delegation_completed(
|
||||
_: Any, event: A2ADelegationCompletedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_a2a_delegation_completed(
|
||||
event.status,
|
||||
event.result,
|
||||
@@ -587,7 +612,9 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(A2AConversationStartedEvent)
|
||||
def on_a2a_conversation_started(source, event: A2AConversationStartedEvent):
|
||||
def on_a2a_conversation_started(
|
||||
_: Any, event: A2AConversationStartedEvent
|
||||
) -> None:
|
||||
# Store A2A agent name for display in conversation tree
|
||||
if event.a2a_agent_name:
|
||||
self.formatter._current_a2a_agent_name = event.a2a_agent_name
|
||||
@@ -598,7 +625,7 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(A2AMessageSentEvent)
|
||||
def on_a2a_message_sent(source, event: A2AMessageSentEvent):
|
||||
def on_a2a_message_sent(_: Any, event: A2AMessageSentEvent) -> None:
|
||||
self.formatter.handle_a2a_message_sent(
|
||||
event.message,
|
||||
event.turn_number,
|
||||
@@ -606,7 +633,7 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(A2AResponseReceivedEvent)
|
||||
def on_a2a_response_received(source, event: A2AResponseReceivedEvent):
|
||||
def on_a2a_response_received(_: Any, event: A2AResponseReceivedEvent) -> None:
|
||||
self.formatter.handle_a2a_response_received(
|
||||
event.response,
|
||||
event.turn_number,
|
||||
@@ -615,7 +642,9 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(A2AConversationCompletedEvent)
|
||||
def on_a2a_conversation_completed(source, event: A2AConversationCompletedEvent):
|
||||
def on_a2a_conversation_completed(
|
||||
_: Any, event: A2AConversationCompletedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_a2a_conversation_completed(
|
||||
event.status,
|
||||
event.final_result,
|
||||
@@ -626,7 +655,7 @@ class EventListener(BaseEventListener):
|
||||
# ----------- MCP EVENTS -----------
|
||||
|
||||
@crewai_event_bus.on(MCPConnectionStartedEvent)
|
||||
def on_mcp_connection_started(source, event: MCPConnectionStartedEvent):
|
||||
def on_mcp_connection_started(_: Any, event: MCPConnectionStartedEvent) -> None:
|
||||
self.formatter.handle_mcp_connection_started(
|
||||
event.server_name,
|
||||
event.server_url,
|
||||
@@ -636,7 +665,9 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(MCPConnectionCompletedEvent)
|
||||
def on_mcp_connection_completed(source, event: MCPConnectionCompletedEvent):
|
||||
def on_mcp_connection_completed(
|
||||
_: Any, event: MCPConnectionCompletedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_mcp_connection_completed(
|
||||
event.server_name,
|
||||
event.server_url,
|
||||
@@ -646,7 +677,7 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(MCPConnectionFailedEvent)
|
||||
def on_mcp_connection_failed(source, event: MCPConnectionFailedEvent):
|
||||
def on_mcp_connection_failed(_: Any, event: MCPConnectionFailedEvent) -> None:
|
||||
self.formatter.handle_mcp_connection_failed(
|
||||
event.server_name,
|
||||
event.server_url,
|
||||
@@ -656,7 +687,9 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(MCPToolExecutionStartedEvent)
|
||||
def on_mcp_tool_execution_started(source, event: MCPToolExecutionStartedEvent):
|
||||
def on_mcp_tool_execution_started(
|
||||
_: Any, event: MCPToolExecutionStartedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_mcp_tool_execution_started(
|
||||
event.server_name,
|
||||
event.tool_name,
|
||||
@@ -665,8 +698,8 @@ class EventListener(BaseEventListener):
|
||||
|
||||
@crewai_event_bus.on(MCPToolExecutionCompletedEvent)
|
||||
def on_mcp_tool_execution_completed(
|
||||
source, event: MCPToolExecutionCompletedEvent
|
||||
):
|
||||
_: Any, event: MCPToolExecutionCompletedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_mcp_tool_execution_completed(
|
||||
event.server_name,
|
||||
event.tool_name,
|
||||
@@ -676,7 +709,9 @@ class EventListener(BaseEventListener):
|
||||
)
|
||||
|
||||
@crewai_event_bus.on(MCPToolExecutionFailedEvent)
|
||||
def on_mcp_tool_execution_failed(source, event: MCPToolExecutionFailedEvent):
|
||||
def on_mcp_tool_execution_failed(
|
||||
_: Any, event: MCPToolExecutionFailedEvent
|
||||
) -> None:
|
||||
self.formatter.handle_mcp_tool_execution_failed(
|
||||
event.server_name,
|
||||
event.tool_name,
|
||||
|
||||
@@ -50,6 +50,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
top_p: float | None = None,
|
||||
stop_sequences: list[str] | None = None,
|
||||
stream: bool = False,
|
||||
thinking: dict[str, Any] | None = None,
|
||||
client_params: dict[str, Any] | None = None,
|
||||
interceptor: BaseInterceptor[httpx.Request, httpx.Response] | None = None,
|
||||
**kwargs: Any,
|
||||
@@ -67,6 +68,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
top_p: Nucleus sampling parameter
|
||||
stop_sequences: Stop sequences (Anthropic uses stop_sequences, not stop)
|
||||
stream: Enable streaming responses
|
||||
thinking: Extended thinking configuration (e.g., {'type': 'enabled', 'budget_tokens': 5000})
|
||||
client_params: Additional parameters for the Anthropic client
|
||||
interceptor: HTTP interceptor for modifying requests/responses at transport level.
|
||||
**kwargs: Additional parameters
|
||||
@@ -89,6 +91,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
self.top_p = top_p
|
||||
self.stream = stream
|
||||
self.stop_sequences = stop_sequences or []
|
||||
self.thinking = thinking
|
||||
|
||||
# Model-specific settings
|
||||
self.is_claude_3 = "claude-3" in model.lower()
|
||||
@@ -248,6 +251,10 @@ class AnthropicCompletion(BaseLLM):
|
||||
if self.stop_sequences:
|
||||
params["stop_sequences"] = self.stop_sequences
|
||||
|
||||
# Add extended thinking configuration if set
|
||||
if self.thinking is not None:
|
||||
params["thinking"] = self.thinking
|
||||
|
||||
# Handle tools for Claude 3+
|
||||
if tools and self.supports_tools:
|
||||
params["tools"] = self._convert_tools_for_interference(tools)
|
||||
|
||||
@@ -307,27 +307,22 @@ def test_cache_hitting():
|
||||
event_handled = True
|
||||
condition.notify()
|
||||
|
||||
with (
|
||||
patch.object(CacheHandler, "read") as read,
|
||||
):
|
||||
read.return_value = "0"
|
||||
task = Task(
|
||||
description="What is 2 times 6? Ignore correctness and just return the result of the multiplication tool, you must use the tool.",
|
||||
agent=agent,
|
||||
expected_output="The number that is the result of the multiplication tool.",
|
||||
)
|
||||
output = agent.execute_task(task)
|
||||
assert output == "0"
|
||||
read.assert_called_with(
|
||||
tool="multiplier", input='{"first_number": 2, "second_number": 6}'
|
||||
)
|
||||
with condition:
|
||||
if not event_handled:
|
||||
condition.wait(timeout=5)
|
||||
assert event_handled, "Timeout waiting for tool usage event"
|
||||
assert len(received_events) == 1
|
||||
assert isinstance(received_events[0], ToolUsageFinishedEvent)
|
||||
assert received_events[0].from_cache
|
||||
task = Task(
|
||||
description="What is 2 times 6? Return only the result of the multiplication.",
|
||||
agent=agent,
|
||||
expected_output="The result of the multiplication.",
|
||||
)
|
||||
output = agent.execute_task(task)
|
||||
assert output == "12"
|
||||
|
||||
with condition:
|
||||
if not event_handled:
|
||||
condition.wait(timeout=5)
|
||||
assert event_handled, "Timeout waiting for tool usage event"
|
||||
assert len(received_events) == 1
|
||||
assert isinstance(received_events[0], ToolUsageFinishedEvent)
|
||||
assert received_events[0].from_cache
|
||||
assert received_events[0].output == "12"
|
||||
|
||||
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages": [{"role": "system", "content": "You are test role. test backstory\nYour
|
||||
body: '{"messages":[{"role":"system","content":"You are test role. test backstory\nYour
|
||||
personal goal is: test goal\nYou ONLY have access to the following tools, and
|
||||
should NEVER make up tools that are not listed here:\n\nTool Name: dummy_tool\nTool
|
||||
Arguments: {''query'': {''description'': None, ''type'': ''str''}}\nTool Description:
|
||||
Useful for when you need to get a dummy result for a query.\n\nUse the following
|
||||
format:\n\nThought: you should always think about what to do\nAction: the action
|
||||
to take, only one name of [dummy_tool], just the name, exactly as it''s written.\nAction
|
||||
Input: the input to the action, just a simple python dictionary, enclosed in
|
||||
curly braces, using \" to wrap keys and values.\nObservation: the result of
|
||||
the action\n\nOnce all necessary information is gathered:\n\nThought: I now
|
||||
know the final answer\nFinal Answer: the final answer to the original input
|
||||
question"}, {"role": "user", "content": "\nCurrent Task: Use the dummy tool
|
||||
to get a result for ''test query''\n\nThis is the expect criteria for your final
|
||||
answer: The result from the dummy tool\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-3.5-turbo", "stop": ["\nObservation:"],
|
||||
"stream": false}'
|
||||
Useful for when you need to get a dummy result for a query.\n\nIMPORTANT: Use
|
||||
the following format in your response:\n\n```\nThought: you should always think
|
||||
about what to do\nAction: the action to take, only one name of [dummy_tool],
|
||||
just the name, exactly as it''s written.\nAction Input: the input to the action,
|
||||
just a simple JSON object, enclosed in curly braces, using \" to wrap keys and
|
||||
values.\nObservation: the result of the action\n```\n\nOnce all necessary information
|
||||
is gathered, return the following format:\n\n```\nThought: I now know the final
|
||||
answer\nFinal Answer: the final answer to the original input question\n```"},{"role":"user","content":"\nCurrent
|
||||
Task: Use the dummy tool to get a result for ''test query''\n\nThis is the expected
|
||||
criteria for your final answer: The result from the dummy tool\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-3.5-turbo"}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
@@ -26,13 +25,13 @@ interactions:
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '1363'
|
||||
- '1381'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.52.1
|
||||
- OpenAI/Python 1.109.1
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
@@ -42,35 +41,33 @@ interactions:
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.52.1
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
- 1.109.1
|
||||
x-stainless-read-timeout:
|
||||
- '600'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.12.7
|
||||
- 3.12.10
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
content: "{\n \"id\": \"chatcmpl-AmjTkjHtNtJfKGo6wS35grXEzfoqv\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1736177928,\n \"model\": \"gpt-3.5-turbo-0125\",\n
|
||||
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
||||
\"assistant\",\n \"content\": \"I should use the dummy tool to get a
|
||||
result for the 'test query'.\\n\\nAction: dummy_tool\\nAction Input: {\\\"query\\\":
|
||||
\\\"test query\\\"}\",\n \"refusal\": null\n },\n \"logprobs\":
|
||||
null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
|
||||
271,\n \"completion_tokens\": 31,\n \"total_tokens\": 302,\n \"prompt_tokens_details\":
|
||||
{\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
|
||||
{\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
|
||||
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"system_fingerprint\":
|
||||
null\n}\n"
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAA4xTwW4TMRC95ytGvvSSVGlDWthbqYSIECAQSFRstXK8s7tuvR5jj5uGKv+O7CTd
|
||||
FAriYtnz5j2/8YwfRgBC16IAoTrJqndmctl8ff3tJsxWd29vLu/7d1eXnz4vfq7cVft+1ohxYtDy
|
||||
BhXvWceKemeQNdktrDxKxqR6cn72YjqdzU/mGeipRpNorePJ7Hg+4eiXNJmenM53zI60wiAK+D4C
|
||||
AHjIa/Joa7wXBUzH+0iPIcgWRfGYBCA8mRQRMgQdWFoW4wFUZBlttr2A0FE0NcSAwB1CHft+XTGR
|
||||
ASZokUGCxxANQ0M+pxwxBoYfEf366Li0FyoVXBww9zFYWBe5gIdS5OxS5H2NQXntUkaKfCCLYygF
|
||||
rx2mcykC+1JsNqX9uAzo7+RW/8veHWR3nQzgkaO3WIPcIf92WtovHcW24wIWYGkFt2lJiY220oC0
|
||||
YYW+tG/y6SKftvfudT31wytlH4fv6rGJQaa+2mjMASCtJc5l5I5e75DNYw8Ntc7TMvxGFY22OnSV
|
||||
RxnIpn4FJicyuhkBXOdZiU/aL5yn3nHFdIv5utOXr7Z6YhjPAT2f7UAmlmaIz85Ox8/oVTWy1CYc
|
||||
TJtQUnVYD9RhNGWsNR0Ao4Oq/3TznPa2cm3b/5EfAKXQMdaV81hr9bTiIc1j+r1/S3t85WxYpEnU
|
||||
CivW6FMnamxkNNt/JcI6MPZVo22L3nmdP1fq5Ggz+gUAAP//AwDDsh2ZWwQAAA==
|
||||
headers:
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-RAY:
|
||||
- 8fdccc13af387bb2-ATL
|
||||
- 9a3a73adce2d43c2-EWR
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
@@ -78,15 +75,17 @@ interactions:
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Mon, 06 Jan 2025 15:38:48 GMT
|
||||
- Mon, 24 Nov 2025 16:58:36 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- __cf_bm=PdbRW9vzO7559czIqn0xmXQjbN8_vV_J7k1DlkB4d_Y-1736177928-1.0.1.1-7yNcyljwqHI.TVflr9ZnkS705G.K5hgPbHpxRzcO3ZMFi5lHCBPs_KB5pFE043wYzPmDIHpn6fu6jIY9mlNoLQ;
|
||||
path=/; expires=Mon, 06-Jan-25 16:08:48 GMT; domain=.api.openai.com; HttpOnly;
|
||||
- __cf_bm=Xa8khOM9zEqqwwmzvZrdS.nMU9nW06e0gk4Xg8ga5BI-1764003516-1.0.1.1-mR_vAWrgEyaykpsxgHq76VhaNTOdAWeNJweR1bmH1wVJgzoE0fuSPEKZMJy9Uon.1KBTV3yJVxLvQ4PjPLuE30IUdwY9Lrfbz.Rhb6UVbwY;
|
||||
path=/; expires=Mon, 24-Nov-25 17:28:36 GMT; domain=.api.openai.com; HttpOnly;
|
||||
Secure; SameSite=None
|
||||
- _cfuvid=lOOz0FbrrPaRb4IFEeHNcj7QghHzxI1tTV2N0jD9icA-1736177928767-0.0.1.1-604800000;
|
||||
- _cfuvid=GP8hWglm1PiEe8AjYsdeCiIUtkA7483Hr9Ws4AZWe5U-1764003516772-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:
|
||||
@@ -95,14 +94,20 @@ interactions:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
- REDACTED
|
||||
openai-processing-ms:
|
||||
- '444'
|
||||
- '1413'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
strict-transport-security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
x-envoy-upstream-service-time:
|
||||
- '1606'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- '10000'
|
||||
x-ratelimit-limit-tokens:
|
||||
@@ -110,36 +115,52 @@ interactions:
|
||||
x-ratelimit-remaining-requests:
|
||||
- '9999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '49999686'
|
||||
- '49999684'
|
||||
x-ratelimit-reset-requests:
|
||||
- 6ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_5b3e93f5d4e6ab8feef83dc26b6eb623
|
||||
http_version: HTTP/1.1
|
||||
status_code: 200
|
||||
- req_REDACTED
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"messages": [{"role": "system", "content": "You are test role. test backstory\nYour
|
||||
body: '{"messages":[{"role":"system","content":"You are test role. test backstory\nYour
|
||||
personal goal is: test goal\nYou ONLY have access to the following tools, and
|
||||
should NEVER make up tools that are not listed here:\n\nTool Name: dummy_tool\nTool
|
||||
Arguments: {''query'': {''description'': None, ''type'': ''str''}}\nTool Description:
|
||||
Useful for when you need to get a dummy result for a query.\n\nUse the following
|
||||
format:\n\nThought: you should always think about what to do\nAction: the action
|
||||
to take, only one name of [dummy_tool], just the name, exactly as it''s written.\nAction
|
||||
Input: the input to the action, just a simple python dictionary, enclosed in
|
||||
curly braces, using \" to wrap keys and values.\nObservation: the result of
|
||||
the action\n\nOnce all necessary information is gathered:\n\nThought: I now
|
||||
know the final answer\nFinal Answer: the final answer to the original input
|
||||
question"}, {"role": "user", "content": "\nCurrent Task: Use the dummy tool
|
||||
to get a result for ''test query''\n\nThis is the expect criteria for your final
|
||||
answer: The result from the dummy tool\nyou MUST return the actual complete
|
||||
content as the final answer, not a summary.\n\nBegin! This is VERY important
|
||||
to you, use the tools available and give your best Final Answer, your job depends
|
||||
on it!\n\nThought:"}, {"role": "assistant", "content": "I should use the dummy
|
||||
tool to get a result for the ''test query''.\n\nAction: dummy_tool\nAction Input:
|
||||
{\"query\": \"test query\"}\nObservation: Dummy result for: test query"}], "model":
|
||||
"gpt-3.5-turbo", "stop": ["\nObservation:"], "stream": false}'
|
||||
Useful for when you need to get a dummy result for a query.\n\nIMPORTANT: Use
|
||||
the following format in your response:\n\n```\nThought: you should always think
|
||||
about what to do\nAction: the action to take, only one name of [dummy_tool],
|
||||
just the name, exactly as it''s written.\nAction Input: the input to the action,
|
||||
just a simple JSON object, enclosed in curly braces, using \" to wrap keys and
|
||||
values.\nObservation: the result of the action\n```\n\nOnce all necessary information
|
||||
is gathered, return the following format:\n\n```\nThought: I now know the final
|
||||
answer\nFinal Answer: the final answer to the original input question\n```"},{"role":"user","content":"\nCurrent
|
||||
Task: Use the dummy tool to get a result for ''test query''\n\nThis is the expected
|
||||
criteria for your final answer: The result from the dummy tool\nyou MUST return
|
||||
the actual complete content as the final answer, not a summary.\n\nBegin! This
|
||||
is VERY important to you, use the tools available and give your best Final Answer,
|
||||
your job depends on it!\n\nThought:"},{"role":"assistant","content":"I should
|
||||
use the dummy_tool to get a result for the ''test query''.\nAction: dummy_tool\nAction
|
||||
Input: {\"query\": {\"description\": None, \"type\": \"str\"}}\nObservation:
|
||||
\nI encountered an error while trying to use the tool. This was the error: Arguments
|
||||
validation failed: 1 validation error for Dummy_Tool\nquery\n Input should
|
||||
be a valid string [type=string_type, input_value={''description'': ''None'',
|
||||
''type'': ''str''}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/string_type.\n
|
||||
Tool dummy_tool accepts these inputs: Tool Name: dummy_tool\nTool Arguments:
|
||||
{''query'': {''description'': None, ''type'': ''str''}}\nTool Description: Useful
|
||||
for when you need to get a dummy result for a query..\nMoving on then. I MUST
|
||||
either use a tool (use one at time) OR give my best final answer not both at
|
||||
the same time. When responding, I must use the following format:\n\n```\nThought:
|
||||
you should always think about what to do\nAction: the action to take, should
|
||||
be one of [dummy_tool]\nAction Input: the input to the action, dictionary enclosed
|
||||
in curly braces\nObservation: the result of the action\n```\nThis Thought/Action/Action
|
||||
Input/Result can repeat N times. Once I know the final answer, I must return
|
||||
the following format:\n\n```\nThought: I now can give a great answer\nFinal
|
||||
Answer: Your final answer must be the great and the most complete as possible,
|
||||
it must be outcome described\n\n```"}],"model":"gpt-3.5-turbo"}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
@@ -148,16 +169,16 @@ interactions:
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '1574'
|
||||
- '2841'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- __cf_bm=PdbRW9vzO7559czIqn0xmXQjbN8_vV_J7k1DlkB4d_Y-1736177928-1.0.1.1-7yNcyljwqHI.TVflr9ZnkS705G.K5hgPbHpxRzcO3ZMFi5lHCBPs_KB5pFE043wYzPmDIHpn6fu6jIY9mlNoLQ;
|
||||
_cfuvid=lOOz0FbrrPaRb4IFEeHNcj7QghHzxI1tTV2N0jD9icA-1736177928767-0.0.1.1-604800000
|
||||
- __cf_bm=Xa8khOM9zEqqwwmzvZrdS.nMU9nW06e0gk4Xg8ga5BI-1764003516-1.0.1.1-mR_vAWrgEyaykpsxgHq76VhaNTOdAWeNJweR1bmH1wVJgzoE0fuSPEKZMJy9Uon.1KBTV3yJVxLvQ4PjPLuE30IUdwY9Lrfbz.Rhb6UVbwY;
|
||||
_cfuvid=GP8hWglm1PiEe8AjYsdeCiIUtkA7483Hr9Ws4AZWe5U-1764003516772-0.0.1.1-604800000
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.52.1
|
||||
- OpenAI/Python 1.109.1
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
@@ -167,34 +188,34 @@ interactions:
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.52.1
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
- 1.109.1
|
||||
x-stainless-read-timeout:
|
||||
- '600'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.12.7
|
||||
- 3.12.10
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
content: "{\n \"id\": \"chatcmpl-AmjTkjtDnt98YQ3k4y71C523EQM9p\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1736177928,\n \"model\": \"gpt-3.5-turbo-0125\",\n
|
||||
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
||||
\"assistant\",\n \"content\": \"Final Answer: Dummy result for: test
|
||||
query\",\n \"refusal\": null\n },\n \"logprobs\": null,\n \"finish_reason\":
|
||||
\"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 315,\n \"completion_tokens\":
|
||||
9,\n \"total_tokens\": 324,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
|
||||
0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n
|
||||
\ \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
|
||||
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"system_fingerprint\":
|
||||
null\n}\n"
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//pFPbahsxEH33Vwx6yYtt7LhO0n1LWgomlFKaFko3LLJ2dletdrSRRklN
|
||||
8L8HyZdd9wKFvgikM2cuOmeeRwBClyIDoRrJqu3M5E31+UaeL+ct335c3Ty8/frFLW5vF6G9dNfv
|
||||
xTgy7Po7Kj6wpsq2nUHWlnawcigZY9b55cWr2WyxnF8loLUlmkirO54spssJB7e2k9n8fLlnNlYr
|
||||
9CKDbyMAgOd0xh6pxJ8ig9n48NKi97JGkR2DAISzJr4I6b32LInFuAeVJUZKbd81NtQNZ7CCJ20M
|
||||
KOscKgZuEDR1gaGyrpUMkkpgt4HgNdUJLkPbbgq21oCspaZpTtcqzp4NoMMbrGKyDJ5z8RDQbXKR
|
||||
QS4YPcP+vs3pw9qje5S7HDndNQgOfTAMlbNtXxRSUe0z+BSUQu+rYMwG7JqlJixB7sMOZOsS96wv
|
||||
dzbNKRY4Dk/2CZQkqPUjgoQ6CgeS/BO6nN5pkgau0+0/ag4lcFgFL6MFKBgzACSR5fQFSfz7PbI9
|
||||
ym1s3Tm79r9QRaVJ+6ZwKL2lKK1n24mEbkcA98lW4cQponO27bhg+wNTuYvzva1E7+Qevbzag2xZ
|
||||
mgHr9QE4yVeUyFIbPzCmUFI1WPbU3sUylNoOgNFg6t+7+VPu3eSa6n9J3wNKYcdYFp3DUqvTifsw
|
||||
h3HR/xZ2/OXUsIgu1goL1uiiEiVWMpjdCgq/8YxtUWmq0XVOpz2MSo62oxcAAAD//wMA+UmELoYE
|
||||
AAA=
|
||||
headers:
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-RAY:
|
||||
- 8fdccc171b647bb2-ATL
|
||||
- 9a3a73bbf9d943c2-EWR
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
@@ -202,9 +223,11 @@ interactions:
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Mon, 06 Jan 2025 15:38:49 GMT
|
||||
- Mon, 24 Nov 2025 16:58:39 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
@@ -213,14 +236,20 @@ interactions:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
- REDACTED
|
||||
openai-processing-ms:
|
||||
- '249'
|
||||
- '1513'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
strict-transport-security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
x-envoy-upstream-service-time:
|
||||
- '1753'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- '10000'
|
||||
x-ratelimit-limit-tokens:
|
||||
@@ -228,103 +257,156 @@ interactions:
|
||||
x-ratelimit-remaining-requests:
|
||||
- '9999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '49999643'
|
||||
- '49999334'
|
||||
x-ratelimit-reset-requests:
|
||||
- 6ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_cdc7b25a3877bb9a7cb7c6d2645ff447
|
||||
http_version: HTTP/1.1
|
||||
status_code: 200
|
||||
- req_REDACTED
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"trace_id": "1581aff1-2567-43f4-a1f2-a2816533eb7d", "execution_type":
|
||||
"crew", "user_identifier": null, "execution_context": {"crew_fingerprint": null,
|
||||
"crew_name": "Unknown Crew", "flow_name": null, "crewai_version": "0.201.1",
|
||||
"privacy_level": "standard"}, "execution_metadata": {"expected_duration_estimate":
|
||||
300, "agent_count": 0, "task_count": 0, "flow_method_count": 0, "execution_started_at":
|
||||
"2025-10-08T18:11:28.008595+00:00"}}'
|
||||
body: '{"messages":[{"role":"system","content":"You are test role. test backstory\nYour
|
||||
personal goal is: test goal\nYou ONLY have access to the following tools, and
|
||||
should NEVER make up tools that are not listed here:\n\nTool Name: dummy_tool\nTool
|
||||
Arguments: {''query'': {''description'': None, ''type'': ''str''}}\nTool Description:
|
||||
Useful for when you need to get a dummy result for a query.\n\nIMPORTANT: Use
|
||||
the following format in your response:\n\n```\nThought: you should always think
|
||||
about what to do\nAction: the action to take, only one name of [dummy_tool],
|
||||
just the name, exactly as it''s written.\nAction Input: the input to the action,
|
||||
just a simple JSON object, enclosed in curly braces, using \" to wrap keys and
|
||||
values.\nObservation: the result of the action\n```\n\nOnce all necessary information
|
||||
is gathered, return the following format:\n\n```\nThought: I now know the final
|
||||
answer\nFinal Answer: the final answer to the original input question\n```"},{"role":"user","content":"\nCurrent
|
||||
Task: Use the dummy tool to get a result for ''test query''\n\nThis is the expected
|
||||
criteria for your final answer: The result from the dummy tool\nyou MUST return
|
||||
the actual complete content as the final answer, not a summary.\n\nBegin! This
|
||||
is VERY important to you, use the tools available and give your best Final Answer,
|
||||
your job depends on it!\n\nThought:"},{"role":"assistant","content":"I should
|
||||
use the dummy_tool to get a result for the ''test query''.\nAction: dummy_tool\nAction
|
||||
Input: {\"query\": {\"description\": None, \"type\": \"str\"}}\nObservation:
|
||||
\nI encountered an error while trying to use the tool. This was the error: Arguments
|
||||
validation failed: 1 validation error for Dummy_Tool\nquery\n Input should
|
||||
be a valid string [type=string_type, input_value={''description'': ''None'',
|
||||
''type'': ''str''}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/string_type.\n
|
||||
Tool dummy_tool accepts these inputs: Tool Name: dummy_tool\nTool Arguments:
|
||||
{''query'': {''description'': None, ''type'': ''str''}}\nTool Description: Useful
|
||||
for when you need to get a dummy result for a query..\nMoving on then. I MUST
|
||||
either use a tool (use one at time) OR give my best final answer not both at
|
||||
the same time. When responding, I must use the following format:\n\n```\nThought:
|
||||
you should always think about what to do\nAction: the action to take, should
|
||||
be one of [dummy_tool]\nAction Input: the input to the action, dictionary enclosed
|
||||
in curly braces\nObservation: the result of the action\n```\nThis Thought/Action/Action
|
||||
Input/Result can repeat N times. Once I know the final answer, I must return
|
||||
the following format:\n\n```\nThought: I now can give a great answer\nFinal
|
||||
Answer: Your final answer must be the great and the most complete as possible,
|
||||
it must be outcome described\n\n```"},{"role":"assistant","content":"Thought:
|
||||
I will correct the input format and try using the dummy_tool again.\nAction:
|
||||
dummy_tool\nAction Input: {\"query\": \"test query\"}\nObservation: Dummy result
|
||||
for: test query"}],"model":"gpt-3.5-turbo"}'
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate, zstd
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '436'
|
||||
Content-Type:
|
||||
accept:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- CrewAI-CLI/0.201.1
|
||||
X-Crewai-Organization-Id:
|
||||
- d3a3d10c-35db-423f-a7a4-c026030ba64d
|
||||
X-Crewai-Version:
|
||||
- 0.201.1
|
||||
accept-encoding:
|
||||
- gzip, deflate
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '3057'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- __cf_bm=Xa8khOM9zEqqwwmzvZrdS.nMU9nW06e0gk4Xg8ga5BI-1764003516-1.0.1.1-mR_vAWrgEyaykpsxgHq76VhaNTOdAWeNJweR1bmH1wVJgzoE0fuSPEKZMJy9Uon.1KBTV3yJVxLvQ4PjPLuE30IUdwY9Lrfbz.Rhb6UVbwY;
|
||||
_cfuvid=GP8hWglm1PiEe8AjYsdeCiIUtkA7483Hr9Ws4AZWe5U-1764003516772-0.0.1.1-604800000
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.109.1
|
||||
x-stainless-arch:
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.109.1
|
||||
x-stainless-read-timeout:
|
||||
- '600'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.12.10
|
||||
method: POST
|
||||
uri: http://localhost:3000/crewai_plus/api/v1/tracing/batches
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: '{"id":"30844ebe-8ac6-4f67-939a-7a072d792654","trace_id":"1581aff1-2567-43f4-a1f2-a2816533eb7d","execution_type":"crew","crew_name":"Unknown
|
||||
Crew","flow_name":null,"status":"running","duration_ms":null,"crewai_version":"0.201.1","privacy_level":"standard","total_events":0,"execution_context":{"crew_fingerprint":null,"crew_name":"Unknown
|
||||
Crew","flow_name":null,"crewai_version":"0.201.1","privacy_level":"standard"},"created_at":"2025-10-08T18:11:28.353Z","updated_at":"2025-10-08T18:11:28.353Z"}'
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFLBbhMxEL3vV4x8TqqkTULZWwFFAq4gpEK18npnd028HmOPW6Iq/47s
|
||||
pNktFKkXS/abN37vzTwWAEI3ogSheslqcGb+vv36rt7e0uqzbna0ut18uv8mtxSDrddKzBKD6p+o
|
||||
+Il1oWhwBlmTPcLKo2RMXZdvNqvF4mq9fJuBgRo0idY5nl9drOccfU3zxfJyfWL2pBUGUcL3AgDg
|
||||
MZ9Jo23wtyhhMXt6GTAE2aEoz0UAwpNJL0KGoANLy2I2gooso82yv/QUu55L+AiWHmCXDu4RWm2l
|
||||
AWnDA/ofdptvN/lWwoc4DHvwGKJhaMmXwBgYfkX0++k3HtsYZLJpozETQFpLLFNM2eDdCTmcLRnq
|
||||
nKc6/EUVrbY69JVHGcgm+YHJiYweCoC7HF18loZwngbHFdMO83ebzerYT4zTGtHl9QlkYmkmrOvL
|
||||
2Qv9qgZZahMm4QslVY/NSB0nJWOjaQIUE9f/qnmp99G5tt1r2o+AUugYm8p5bLR67ngs85iW+X9l
|
||||
55SzYBHQ32uFFWv0aRINtjKa45qJsA+MQ9Vq26F3XuddS5MsDsUfAAAA//8DANWDXp9qAwAA
|
||||
headers:
|
||||
Content-Length:
|
||||
- '496'
|
||||
cache-control:
|
||||
- no-store
|
||||
content-security-policy:
|
||||
- 'default-src ''self'' *.crewai.com crewai.com; script-src ''self'' ''unsafe-inline''
|
||||
*.crewai.com crewai.com https://cdn.jsdelivr.net/npm/apexcharts https://www.gstatic.com
|
||||
https://run.pstmn.io https://apis.google.com https://apis.google.com/js/api.js
|
||||
https://accounts.google.com https://accounts.google.com/gsi/client https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css.map
|
||||
https://*.google.com https://docs.google.com https://slides.google.com https://js.hs-scripts.com
|
||||
https://js.sentry-cdn.com https://browser.sentry-cdn.com https://www.googletagmanager.com
|
||||
https://js-na1.hs-scripts.com https://share.descript.com/; style-src ''self''
|
||||
''unsafe-inline'' *.crewai.com crewai.com https://cdn.jsdelivr.net/npm/apexcharts;
|
||||
img-src ''self'' data: *.crewai.com crewai.com https://zeus.tools.crewai.com
|
||||
https://dashboard.tools.crewai.com https://cdn.jsdelivr.net; font-src ''self''
|
||||
data: *.crewai.com crewai.com; connect-src ''self'' *.crewai.com crewai.com
|
||||
https://zeus.tools.crewai.com https://connect.useparagon.com/ https://zeus.useparagon.com/*
|
||||
https://*.useparagon.com/* https://run.pstmn.io https://connect.tools.crewai.com/
|
||||
https://*.sentry.io https://www.google-analytics.com ws://localhost:3036 wss://localhost:3036;
|
||||
frame-src ''self'' *.crewai.com crewai.com https://connect.useparagon.com/
|
||||
https://zeus.tools.crewai.com https://zeus.useparagon.com/* https://connect.tools.crewai.com/
|
||||
https://docs.google.com https://drive.google.com https://slides.google.com
|
||||
https://accounts.google.com https://*.google.com https://www.youtube.com https://share.descript.com'
|
||||
content-type:
|
||||
- application/json; charset=utf-8
|
||||
etag:
|
||||
- W/"a548892c6a8a52833595a42b35b10009"
|
||||
expires:
|
||||
- '0'
|
||||
permissions-policy:
|
||||
- camera=(), microphone=(self), geolocation=()
|
||||
pragma:
|
||||
- no-cache
|
||||
referrer-policy:
|
||||
- strict-origin-when-cross-origin
|
||||
server-timing:
|
||||
- cache_read.active_support;dur=0.05, cache_fetch_hit.active_support;dur=0.00,
|
||||
cache_read_multi.active_support;dur=0.12, start_processing.action_controller;dur=0.00,
|
||||
sql.active_record;dur=30.46, instantiation.active_record;dur=0.38, feature_operation.flipper;dur=0.03,
|
||||
start_transaction.active_record;dur=0.01, transaction.active_record;dur=16.78,
|
||||
process_action.action_controller;dur=309.67
|
||||
vary:
|
||||
- Accept
|
||||
x-content-type-options:
|
||||
CF-RAY:
|
||||
- 9a3a73cd4ff343c2-EWR
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Mon, 24 Nov 2025 16:58:40 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
x-frame-options:
|
||||
- SAMEORIGIN
|
||||
x-permitted-cross-domain-policies:
|
||||
- none
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- REDACTED
|
||||
openai-processing-ms:
|
||||
- '401'
|
||||
openai-project:
|
||||
- proj_xitITlrFeen7zjNSzML82h9x
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '421'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- '10000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '50000000'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '9999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '49999290'
|
||||
x-ratelimit-reset-requests:
|
||||
- 6ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- 7ec132be-e871-4b0a-93f7-81f8d7c0ccae
|
||||
x-runtime:
|
||||
- '0.358533'
|
||||
x-xss-protection:
|
||||
- 1; mode=block
|
||||
- req_REDACTED
|
||||
status:
|
||||
code: 201
|
||||
message: Created
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ load_result = load_dotenv(override=True)
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_test_environment():
|
||||
"""Set up test environment with a temporary directory for SQLite storage."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir:
|
||||
# Create the directory with proper permissions
|
||||
storage_dir = Path(temp_dir) / "crewai_test_storage"
|
||||
storage_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -128,8 +128,6 @@ class TestAgentEvaluator:
|
||||
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
def test_eval_specific_agents_from_crew(self, mock_crew):
|
||||
from crewai.events.types.task_events import TaskCompletedEvent
|
||||
|
||||
agent = Agent(
|
||||
role="Test Agent Eval",
|
||||
goal="Complete test tasks successfully",
|
||||
@@ -144,9 +142,8 @@ class TestAgentEvaluator:
|
||||
mock_crew.tasks.append(task)
|
||||
|
||||
events = {}
|
||||
started_event = threading.Event()
|
||||
completed_event = threading.Event()
|
||||
task_completed_event = threading.Event()
|
||||
results_condition = threading.Condition()
|
||||
completed_event_received = False
|
||||
|
||||
agent_evaluator = AgentEvaluator(
|
||||
agents=[agent], evaluators=[GoalAlignmentEvaluator()]
|
||||
@@ -156,31 +153,26 @@ class TestAgentEvaluator:
|
||||
async def capture_started(source, event):
|
||||
if event.agent_id == str(agent.id):
|
||||
events["started"] = event
|
||||
started_event.set()
|
||||
|
||||
@crewai_event_bus.on(AgentEvaluationCompletedEvent)
|
||||
async def capture_completed(source, event):
|
||||
nonlocal completed_event_received
|
||||
if event.agent_id == str(agent.id):
|
||||
events["completed"] = event
|
||||
completed_event.set()
|
||||
with results_condition:
|
||||
completed_event_received = True
|
||||
results_condition.notify()
|
||||
|
||||
@crewai_event_bus.on(AgentEvaluationFailedEvent)
|
||||
def capture_failed(source, event):
|
||||
events["failed"] = event
|
||||
|
||||
@crewai_event_bus.on(TaskCompletedEvent)
|
||||
async def on_task_completed(source, event):
|
||||
# TaskCompletedEvent fires AFTER evaluation results are stored
|
||||
if event.task and event.task.id == task.id:
|
||||
task_completed_event.set()
|
||||
|
||||
mock_crew.kickoff()
|
||||
|
||||
assert started_event.wait(timeout=5), "Timeout waiting for started event"
|
||||
assert completed_event.wait(timeout=5), "Timeout waiting for completed event"
|
||||
assert task_completed_event.wait(timeout=5), (
|
||||
"Timeout waiting for task completion"
|
||||
)
|
||||
with results_condition:
|
||||
assert results_condition.wait_for(
|
||||
lambda: completed_event_received, timeout=5
|
||||
), "Timeout waiting for evaluation completed event"
|
||||
|
||||
assert events.keys() == {"started", "completed"}
|
||||
assert events["started"].agent_id == str(agent.id)
|
||||
|
||||
@@ -698,3 +698,253 @@ def test_anthropic_stop_sequences_sent_to_api():
|
||||
assert result is not None
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
def test_anthropic_extended_thinking_parameter():
|
||||
"""
|
||||
Test that AnthropicCompletion accepts and stores the thinking parameter
|
||||
"""
|
||||
thinking_config = {"type": "enabled", "budget_tokens": 5000}
|
||||
|
||||
llm = LLM(
|
||||
model="anthropic/claude-3-5-sonnet-20241022",
|
||||
thinking=thinking_config
|
||||
)
|
||||
|
||||
from crewai.llms.providers.anthropic.completion import AnthropicCompletion
|
||||
assert isinstance(llm, AnthropicCompletion)
|
||||
assert llm.thinking == thinking_config
|
||||
|
||||
|
||||
def test_anthropic_extended_thinking_added_to_api_call():
|
||||
"""
|
||||
Test that the thinking parameter is added to the API call parameters
|
||||
"""
|
||||
from crewai.llms.providers.anthropic.completion import AnthropicCompletion
|
||||
|
||||
thinking_config = {"type": "enabled", "budget_tokens": 5000}
|
||||
completion = AnthropicCompletion(
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
thinking=thinking_config
|
||||
)
|
||||
|
||||
# Test _prepare_completion_params includes thinking
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
params = completion._prepare_completion_params(messages)
|
||||
|
||||
assert "thinking" in params
|
||||
assert params["thinking"] == thinking_config
|
||||
|
||||
|
||||
def test_anthropic_extended_thinking_not_added_when_none():
|
||||
"""
|
||||
Test that the thinking parameter is not added to API call when None
|
||||
"""
|
||||
from crewai.llms.providers.anthropic.completion import AnthropicCompletion
|
||||
|
||||
completion = AnthropicCompletion(model="claude-3-5-sonnet-20241022")
|
||||
|
||||
# Test _prepare_completion_params does not include thinking when None
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
params = completion._prepare_completion_params(messages)
|
||||
|
||||
assert "thinking" not in params
|
||||
|
||||
|
||||
def test_anthropic_extended_thinking_with_tool_use_preserves_thinking_blocks():
|
||||
"""
|
||||
Test that thinking blocks are preserved in tool use conversation flow
|
||||
"""
|
||||
from unittest.mock import Mock, patch
|
||||
from crewai.llms.providers.anthropic.completion import AnthropicCompletion
|
||||
from anthropic.types.tool_use_block import ToolUseBlock
|
||||
from anthropic.types import ThinkingBlock
|
||||
|
||||
thinking_config = {"type": "enabled", "budget_tokens": 5000}
|
||||
completion = AnthropicCompletion(
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
thinking=thinking_config
|
||||
)
|
||||
|
||||
# Mock tool function
|
||||
def mock_calculator(operation: str, a: int, b: int) -> str:
|
||||
if operation == "add":
|
||||
return str(a + b)
|
||||
return "0"
|
||||
|
||||
available_functions = {"calculator": mock_calculator}
|
||||
|
||||
# Mock the Anthropic client responses
|
||||
with patch.object(completion.client.messages, 'create') as mock_create:
|
||||
# Mock thinking block
|
||||
mock_thinking_block = Mock(spec=ThinkingBlock)
|
||||
mock_thinking_block.type = "thinking"
|
||||
mock_thinking_block.thinking = "I need to use the calculator tool to add 5 and 3"
|
||||
|
||||
# Mock tool use block
|
||||
mock_tool_use = Mock(spec=ToolUseBlock)
|
||||
mock_tool_use.id = "tool_456"
|
||||
mock_tool_use.name = "calculator"
|
||||
mock_tool_use.input = {"operation": "add", "a": 5, "b": 3}
|
||||
mock_tool_use.type = "tool_use"
|
||||
|
||||
# Mock initial response with thinking block + tool use
|
||||
mock_initial_response = Mock()
|
||||
mock_initial_response.content = [mock_thinking_block, mock_tool_use]
|
||||
mock_initial_response.usage = Mock()
|
||||
mock_initial_response.usage.input_tokens = 100
|
||||
mock_initial_response.usage.output_tokens = 50
|
||||
|
||||
# Mock final response after tool result
|
||||
mock_text_block = Mock()
|
||||
mock_text_block.configure_mock(text="The sum of 5 and 3 is 8.")
|
||||
|
||||
mock_final_response = Mock()
|
||||
mock_final_response.content = [mock_text_block]
|
||||
mock_final_response.usage = Mock()
|
||||
mock_final_response.usage.input_tokens = 150
|
||||
mock_final_response.usage.output_tokens = 75
|
||||
|
||||
# Configure mock to return different responses on successive calls
|
||||
mock_create.side_effect = [mock_initial_response, mock_final_response]
|
||||
|
||||
# Test the call
|
||||
messages = [{"role": "user", "content": "What is 5 + 3?"}]
|
||||
result = completion.call(
|
||||
messages=messages,
|
||||
available_functions=available_functions
|
||||
)
|
||||
|
||||
# Verify the result contains the final response
|
||||
assert "sum of 5 and 3 is 8" in result
|
||||
|
||||
# Verify that two API calls were made (initial + follow-up)
|
||||
assert mock_create.call_count == 2
|
||||
|
||||
# Verify the first call includes thinking parameter
|
||||
first_call_kwargs = mock_create.call_args_list[0][1]
|
||||
assert "thinking" in first_call_kwargs
|
||||
assert first_call_kwargs["thinking"] == thinking_config
|
||||
|
||||
# Verify the second call includes thinking blocks in assistant message
|
||||
second_call_kwargs = mock_create.call_args_list[1][1]
|
||||
messages_in_second_call = second_call_kwargs["messages"]
|
||||
|
||||
# Should have original user message + assistant (with thinking + tool_use) + user tool result
|
||||
assert len(messages_in_second_call) == 3
|
||||
assert messages_in_second_call[0]["role"] == "user"
|
||||
assert messages_in_second_call[1]["role"] == "assistant"
|
||||
assert messages_in_second_call[2]["role"] == "user"
|
||||
|
||||
# Verify assistant message content includes both thinking and tool_use blocks
|
||||
assistant_content = messages_in_second_call[1]["content"]
|
||||
assert len(assistant_content) == 2
|
||||
assert assistant_content[0] == mock_thinking_block # Thinking block preserved
|
||||
assert assistant_content[1] == mock_tool_use # Tool use block preserved
|
||||
|
||||
# Verify tool result format
|
||||
tool_result = messages_in_second_call[2]["content"][0]
|
||||
assert tool_result["type"] == "tool_result"
|
||||
assert tool_result["tool_use_id"] == "tool_456"
|
||||
assert "8" in tool_result["content"]
|
||||
|
||||
# Verify the second call also includes thinking parameter
|
||||
assert "thinking" in second_call_kwargs
|
||||
assert second_call_kwargs["thinking"] == thinking_config
|
||||
|
||||
|
||||
def test_anthropic_extended_thinking_with_multiple_tool_calls():
|
||||
"""
|
||||
Test that thinking blocks are preserved across multiple tool calls
|
||||
"""
|
||||
from unittest.mock import Mock, patch
|
||||
from crewai.llms.providers.anthropic.completion import AnthropicCompletion
|
||||
from anthropic.types.tool_use_block import ToolUseBlock
|
||||
from anthropic.types import ThinkingBlock
|
||||
|
||||
thinking_config = {"type": "enabled", "budget_tokens": 10000}
|
||||
completion = AnthropicCompletion(
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
thinking=thinking_config
|
||||
)
|
||||
|
||||
# Mock tool functions
|
||||
def mock_search(query: str) -> str:
|
||||
return f"Search results for: {query}"
|
||||
|
||||
def mock_summarize(text: str) -> str:
|
||||
return f"Summary: {text[:20]}..."
|
||||
|
||||
available_functions = {
|
||||
"search": mock_search,
|
||||
"summarize": mock_summarize
|
||||
}
|
||||
|
||||
# Mock the Anthropic client responses
|
||||
with patch.object(completion.client.messages, 'create') as mock_create:
|
||||
# Mock thinking block
|
||||
mock_thinking_block = Mock(spec=ThinkingBlock)
|
||||
mock_thinking_block.type = "thinking"
|
||||
mock_thinking_block.thinking = "I should search first, then summarize"
|
||||
|
||||
# Mock first tool use
|
||||
mock_tool_use_1 = Mock(spec=ToolUseBlock)
|
||||
mock_tool_use_1.id = "tool_1"
|
||||
mock_tool_use_1.name = "search"
|
||||
mock_tool_use_1.input = {"query": "AI agents"}
|
||||
mock_tool_use_1.type = "tool_use"
|
||||
|
||||
# Mock second tool use
|
||||
mock_tool_use_2 = Mock(spec=ToolUseBlock)
|
||||
mock_tool_use_2.id = "tool_2"
|
||||
mock_tool_use_2.name = "summarize"
|
||||
mock_tool_use_2.input = {"text": "Search results for: AI agents"}
|
||||
mock_tool_use_2.type = "tool_use"
|
||||
|
||||
# Mock initial response with thinking + multiple tool uses
|
||||
mock_initial_response = Mock()
|
||||
mock_initial_response.content = [mock_thinking_block, mock_tool_use_1, mock_tool_use_2]
|
||||
mock_initial_response.usage = Mock()
|
||||
mock_initial_response.usage.input_tokens = 100
|
||||
mock_initial_response.usage.output_tokens = 50
|
||||
|
||||
# Mock final response
|
||||
mock_text_block = Mock()
|
||||
mock_text_block.configure_mock(text="Here's the summary of AI agents research.")
|
||||
|
||||
mock_final_response = Mock()
|
||||
mock_final_response.content = [mock_text_block]
|
||||
mock_final_response.usage = Mock()
|
||||
mock_final_response.usage.input_tokens = 200
|
||||
mock_final_response.usage.output_tokens = 100
|
||||
|
||||
mock_create.side_effect = [mock_initial_response, mock_final_response]
|
||||
|
||||
# Test the call
|
||||
messages = [{"role": "user", "content": "Research AI agents and summarize"}]
|
||||
result = completion.call(
|
||||
messages=messages,
|
||||
available_functions=available_functions
|
||||
)
|
||||
|
||||
# Verify result
|
||||
assert "summary of AI agents" in result
|
||||
|
||||
# Verify two API calls
|
||||
assert mock_create.call_count == 2
|
||||
|
||||
# Verify the second call preserves thinking block and all tool uses
|
||||
second_call_kwargs = mock_create.call_args_list[1][1]
|
||||
messages_in_second_call = second_call_kwargs["messages"]
|
||||
|
||||
assistant_content = messages_in_second_call[1]["content"]
|
||||
assert len(assistant_content) == 3 # thinking + 2 tool uses
|
||||
assert assistant_content[0] == mock_thinking_block
|
||||
assert assistant_content[1] == mock_tool_use_1
|
||||
assert assistant_content[2] == mock_tool_use_2
|
||||
|
||||
# Verify tool results
|
||||
tool_results = messages_in_second_call[2]["content"]
|
||||
assert len(tool_results) == 2
|
||||
assert tool_results[0]["tool_use_id"] == "tool_1"
|
||||
assert tool_results[1]["tool_use_id"] == "tool_2"
|
||||
|
||||
@@ -647,6 +647,7 @@ def test_handle_streaming_tool_calls_no_tools(mock_emit):
|
||||
|
||||
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
@pytest.mark.skip(reason="Highly flaky on ci")
|
||||
def test_llm_call_when_stop_is_unsupported(caplog):
|
||||
llm = LLM(model="o1-mini", stop=["stop"], is_litellm=True)
|
||||
with caplog.at_level(logging.INFO):
|
||||
@@ -657,6 +658,7 @@ def test_llm_call_when_stop_is_unsupported(caplog):
|
||||
|
||||
|
||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||
@pytest.mark.skip(reason="Highly flaky on ci")
|
||||
def test_llm_call_when_stop_is_unsupported_when_additional_drop_params_is_provided(
|
||||
caplog,
|
||||
):
|
||||
@@ -664,7 +666,6 @@ def test_llm_call_when_stop_is_unsupported_when_additional_drop_params_is_provid
|
||||
model="o1-mini",
|
||||
stop=["stop"],
|
||||
additional_drop_params=["another_param"],
|
||||
is_litellm=True,
|
||||
)
|
||||
with caplog.at_level(logging.INFO):
|
||||
result = llm.call("What is the capital of France?")
|
||||
|
||||
@@ -273,12 +273,15 @@ def another_simple_tool():
|
||||
|
||||
|
||||
def test_internal_crew_with_mcp():
|
||||
from crewai_tools import MCPServerAdapter
|
||||
from crewai_tools.adapters.mcp_adapter import ToolCollection
|
||||
from crewai_tools.adapters.tool_collection import ToolCollection
|
||||
|
||||
mock = Mock(spec=MCPServerAdapter)
|
||||
mock.tools = ToolCollection([simple_tool, another_simple_tool])
|
||||
with patch("crewai_tools.MCPServerAdapter", return_value=mock) as adapter_mock:
|
||||
mock_adapter = Mock()
|
||||
mock_adapter.tools = ToolCollection([simple_tool, another_simple_tool])
|
||||
|
||||
with (
|
||||
patch("crewai_tools.MCPServerAdapter", return_value=mock_adapter) as adapter_mock,
|
||||
patch("crewai.llm.LLM.__new__", return_value=Mock()),
|
||||
):
|
||||
crew = InternalCrewWithMCP()
|
||||
assert crew.reporting_analyst().tools == [simple_tool, another_simple_tool]
|
||||
assert crew.researcher().tools == [simple_tool]
|
||||
|
||||
Reference in New Issue
Block a user