Compare commits

..

6 Commits

Author SHA1 Message Date
Devin AI
29be99a74e Fix format_answer() to re-raise OutputParserError for retry logic
This commit fixes issue #3771 where format_answer() was catching all
exceptions including OutputParserError and converting them to AgentFinish,
which prevented the retry logic in _invoke_loop() from working correctly.

Changes:
- Modified format_answer() in agent_utils.py to specifically catch and
  re-raise OutputParserError, allowing the retry logic to handle malformed
  LLM outputs properly
- Added comprehensive tests in test_agent_utils.py to verify the fix and
  prevent regressions

The fix ensures that when an LLM returns malformed output (e.g., missing
colons in the Action/Action Input format), the agent will retry with an
error message instead of immediately returning an AgentFinish with the
malformed text.

Co-Authored-By: João <joao@crewai.com>
2025-10-22 02:08:31 +00:00
Lorenze Jay
d28daa26cd feat: bump versions to 1.1.0 (#3770)
Some checks failed
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Notify Downstream / notify-downstream (push) Has been cancelled
* feat: bump versions to 1.1.0

* chore: bump template versions

---------

Co-authored-by: Greyson Lalonde <greyson.r.lalonde@gmail.com>
2025-10-21 15:52:44 -07:00
Lorenze Jay
a850813f2b feat: enhance InternalInstructor to support multiple LLM providers (#3767)
* feat: enhance InternalInstructor to support multiple LLM providers

- Updated InternalInstructor to conditionally create an instructor client based on the LLM provider.
- Introduced a new method _create_instructor_client to handle client creation using the modern from_provider pattern.
- Added functionality to extract the provider from the LLM model name.
- Implemented tests for InternalInstructor with various LLM providers including OpenAI, Anthropic, Gemini, and Azure, ensuring robust integration and error handling.

This update improves flexibility and extensibility for different LLM integrations.

* fix test
2025-10-21 15:24:59 -07:00
Cameron Warren
5944a39629 fix: correct broken integration documentation links
Fix navigation paths for two integration tool cards that were redirecting to the
introduction page instead of their intended documentation pages.

Fixes #3516

Co-authored-by: Cwarre33 <cwarre33@charlotte.edu>
Co-authored-by: Greyson LaLonde <greyson.r.lalonde@gmail.com>
2025-10-21 18:12:08 -04:00
Greyson LaLonde
c594859ed0 feat: mypy plugin base
* feat: base mypy plugin with CrewBase

* fix: add crew method to protocol
2025-10-21 17:36:08 -04:00
Daniel Barreto
2ee27efca7 feat: improvements on QdrantVectorSearchTool
* Implement improvements on QdrantVectorSearchTool

- Allow search filters to be set at the constructor level
- Fix issue that prevented multiple records from being returned

* Implement improvements on QdrantVectorSearchTool

- Allow search filters to be set at the constructor level
- Fix issue that prevented multiple records from being returned

---------

Co-authored-by: Greyson LaLonde <greyson.r.lalonde@gmail.com>
2025-10-21 16:50:08 -04:00
21 changed files with 689 additions and 355 deletions

View File

@@ -11,7 +11,7 @@ mode: "wide"
<Card
title="Bedrock Invoke Agent Tool"
icon="cloud"
href="/en/tools/tool-integrations/bedrockinvokeagenttool"
href="/en/tools/integration/bedrockinvokeagenttool"
color="#0891B2"
>
Invoke Amazon Bedrock Agents from CrewAI to orchestrate actions across AWS services.
@@ -20,7 +20,7 @@ mode: "wide"
<Card
title="CrewAI Automation Tool"
icon="bolt"
href="/en/tools/tool-integrations/crewaiautomationtool"
href="/en/tools/integration/crewaiautomationtool"
color="#7C3AED"
>
Automate deployment and operations by integrating CrewAI with external platforms and workflows.

View File

@@ -12,7 +12,7 @@ dependencies = [
"pytube>=15.0.0",
"requests>=2.32.5",
"docker>=7.1.0",
"crewai==1.0.0",
"crewai==1.1.0",
"lancedb>=0.5.4",
"tiktoken>=0.8.0",
"beautifulsoup4>=4.13.4",

View File

@@ -287,4 +287,4 @@ __all__ = [
"ZapierActionTools",
]
__version__ = "1.0.0"
__version__ = "1.1.0"

View File

@@ -1,80 +1,42 @@
from collections.abc import Callable
from __future__ import annotations
import importlib
import json
import os
from collections.abc import Callable
from typing import Any
try:
from qdrant_client import QdrantClient
from qdrant_client.http.models import FieldCondition, Filter, MatchValue
QDRANT_AVAILABLE = True
except ImportError:
QDRANT_AVAILABLE = False
QdrantClient = Any # type: ignore[assignment,misc] # type placeholder
Filter = Any # type: ignore[assignment,misc]
FieldCondition = Any # type: ignore[assignment,misc]
MatchValue = Any # type: ignore[assignment,misc]
from crewai.tools import BaseTool, EnvVar
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, model_validator
from pydantic.types import ImportString
class QdrantToolSchema(BaseModel):
"""Input for QdrantTool."""
query: str = Field(..., description="Query to search in Qdrant DB.")
filter_by: str | None = None
filter_value: str | None = None
query: str = Field(
...,
description="The query to search retrieve relevant information from the Qdrant database. Pass only the query, not the question.",
)
filter_by: str | None = Field(
default=None,
description="Filter by properties. Pass only the properties, not the question.",
)
filter_value: str | None = Field(
default=None,
description="Filter by value. Pass only the value, not the question.",
)
class QdrantConfig(BaseModel):
"""All Qdrant connection and search settings."""
qdrant_url: str
qdrant_api_key: str | None = None
collection_name: str
limit: int = 3
score_threshold: float = 0.35
filter_conditions: list[tuple[str, Any]] = Field(default_factory=list)
class QdrantVectorSearchTool(BaseTool):
"""Tool to query and filter results from a Qdrant database.
This tool enables vector similarity search on internal documents stored in Qdrant,
with optional filtering capabilities.
Attributes:
client: Configured QdrantClient instance
collection_name: Name of the Qdrant collection to search
limit: Maximum number of results to return
score_threshold: Minimum similarity score threshold
qdrant_url: Qdrant server URL
qdrant_api_key: Authentication key for Qdrant
"""
"""Vector search tool for Qdrant."""
model_config = ConfigDict(arbitrary_types_allowed=True)
client: QdrantClient = None # type: ignore[assignment]
# --- Metadata ---
name: str = "QdrantVectorSearchTool"
description: str = "A tool to search the Qdrant database for relevant information on internal documents."
description: str = "Search Qdrant vector DB for relevant documents."
args_schema: type[BaseModel] = QdrantToolSchema
query: str | None = None
filter_by: str | None = None
filter_value: str | None = None
collection_name: str | None = None
limit: int | None = Field(default=3)
score_threshold: float = Field(default=0.35)
qdrant_url: str = Field(
...,
description="The URL of the Qdrant server",
)
qdrant_api_key: str | None = Field(
default=None,
description="The API key for the Qdrant server",
)
custom_embedding_fn: Callable | None = Field(
default=None,
description="A custom embedding function to use for vectorization. If not provided, the default model will be used.",
)
package_dependencies: list[str] = Field(default_factory=lambda: ["qdrant-client"])
env_vars: list[EnvVar] = Field(
default_factory=lambda: [
@@ -83,107 +45,81 @@ class QdrantVectorSearchTool(BaseTool):
)
]
)
qdrant_config: QdrantConfig
qdrant_package: ImportString[Any] = Field(
default="qdrant_client",
description="Base package path for Qdrant. Will dynamically import client and models.",
)
custom_embedding_fn: ImportString[Callable[[str], list[float]]] | None = Field(
default=None,
description="Optional embedding function or import path.",
)
client: Any | None = None
def __init__(self, **kwargs):
super().__init__(**kwargs)
if QDRANT_AVAILABLE:
self.client = QdrantClient(
url=self.qdrant_url,
api_key=self.qdrant_api_key if self.qdrant_api_key else None,
@model_validator(mode="after")
def _setup_qdrant(self) -> QdrantVectorSearchTool:
# Import the qdrant_package if it's a string
if isinstance(self.qdrant_package, str):
self.qdrant_package = importlib.import_module(self.qdrant_package)
if not self.client:
self.client = self.qdrant_package.QdrantClient(
url=self.qdrant_config.qdrant_url,
api_key=self.qdrant_config.qdrant_api_key or None,
)
else:
import click
if click.confirm(
"The 'qdrant-client' package is required to use the QdrantVectorSearchTool. "
"Would you like to install it?"
):
import subprocess
subprocess.run(["uv", "add", "qdrant-client"], check=True) # noqa: S607
else:
raise ImportError(
"The 'qdrant-client' package is required to use the QdrantVectorSearchTool. "
"Please install it with: uv add qdrant-client"
)
return self
def _run(
self,
query: str,
filter_by: str | None = None,
filter_value: str | None = None,
filter_value: Any | None = None,
) -> str:
"""Execute vector similarity search on Qdrant.
"""Perform vector similarity search."""
filter_ = self.qdrant_package.http.models.Filter
field_condition = self.qdrant_package.http.models.FieldCondition
match_value = self.qdrant_package.http.models.MatchValue
conditions = self.qdrant_config.filter_conditions.copy()
if filter_by and filter_value is not None:
conditions.append((filter_by, filter_value))
Args:
query: Search query to vectorize and match
filter_by: Optional metadata field to filter on
filter_value: Optional value to filter by
Returns:
JSON string containing search results with metadata and scores
Raises:
ImportError: If qdrant-client is not installed
ValueError: If Qdrant credentials are missing
"""
if not self.qdrant_url:
raise ValueError("QDRANT_URL is not set")
# Create filter if filter parameters are provided
search_filter = None
if filter_by and filter_value:
search_filter = Filter(
search_filter = (
filter_(
must=[
FieldCondition(key=filter_by, match=MatchValue(value=filter_value))
field_condition(key=k, match=match_value(value=v))
for k, v in conditions
]
)
# Search in Qdrant using the built-in query method
query_vector = (
self._vectorize_query(query, embedding_model="text-embedding-3-large")
if not self.custom_embedding_fn
else self.custom_embedding_fn(query)
if conditions
else None
)
search_results = self.client.query_points(
collection_name=self.collection_name, # type: ignore[arg-type]
query_vector = (
self.custom_embedding_fn(query)
if self.custom_embedding_fn
else (
lambda: __import__("openai")
.Client(api_key=os.getenv("OPENAI_API_KEY"))
.embeddings.create(input=[query], model="text-embedding-3-large")
.data[0]
.embedding
)()
)
results = self.client.query_points(
collection_name=self.qdrant_config.collection_name,
query=query_vector,
query_filter=search_filter,
limit=self.limit, # type: ignore[arg-type]
score_threshold=self.score_threshold,
limit=self.qdrant_config.limit,
score_threshold=self.qdrant_config.score_threshold,
)
# Format results similar to storage implementation
results = []
# Extract the list of ScoredPoint objects from the tuple
for point in search_results:
result = {
"metadata": point[1][0].payload.get("metadata", {}),
"context": point[1][0].payload.get("text", ""),
"distance": point[1][0].score,
}
results.append(result)
return json.dumps(results, indent=2)
def _vectorize_query(self, query: str, embedding_model: str) -> list[float]:
"""Default vectorization function with openai.
Args:
query (str): The query to vectorize
embedding_model (str): The embedding model to use
Returns:
list[float]: The vectorized query
"""
import openai
client = openai.Client(api_key=os.getenv("OPENAI_API_KEY"))
return (
client.embeddings.create(
input=[query],
model=embedding_model,
)
.data[0]
.embedding
return json.dumps(
[
{
"distance": p.score,
"metadata": p.payload.get("metadata", {}) if p.payload else {},
"context": p.payload.get("text", "") if p.payload else {},
}
for p in results.points
],
indent=2,
)

View File

@@ -49,7 +49,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
tools = [
"crewai-tools==1.0.0",
"crewai-tools==1.1.0",
]
embeddings = [
"tiktoken~=0.8.0"

View File

@@ -40,7 +40,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
_suppress_pydantic_deprecation_warnings()
__version__ = "1.0.0"
__version__ = "1.1.0"
_telemetry_submitted = False

View File

@@ -239,6 +239,7 @@ class Agent(BaseAgent):
embedder=self.embedder,
collection_name=self.role,
)
self.knowledge.add_sources()
except (TypeError, ValueError) as e:
raise ValueError(f"Invalid Knowledge Configuration: {e!s}") from e

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.0.0"
"crewai[tools]==1.1.0"
]
[project.scripts]

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.0.0"
"crewai[tools]==1.1.0"
]
[project.scripts]

View File

@@ -371,6 +371,7 @@ class Crew(FlowTrackable, BaseModel):
embedder=self.embedder,
collection_name="crew",
)
self.knowledge.add_sources()
except Exception as e:
self._logger.log(

View File

@@ -1,6 +1,6 @@
import os
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
from pydantic import BaseModel, ConfigDict, Field
from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
from crewai.knowledge.storage.knowledge_storage import KnowledgeStorage
@@ -25,7 +25,6 @@ class Knowledge(BaseModel):
storage: KnowledgeStorage | None = Field(default=None)
embedder: EmbedderConfig | None = None
collection_name: str | None = None
_sources_loaded: bool = PrivateAttr(default=False)
def __init__(
self,
@@ -57,10 +56,6 @@ class Knowledge(BaseModel):
if self.storage is None:
raise ValueError("Storage is not initialized.")
if not self._sources_loaded:
self.add_sources()
self._sources_loaded = True
return self.storage.search(
query,
limit=results_limit,
@@ -72,7 +67,6 @@ class Knowledge(BaseModel):
for source in self.sources:
source.storage = self.storage
source.add()
self._sources_loaded = True
except Exception as e:
raise e

View File

@@ -0,0 +1,60 @@
"""Mypy plugin for CrewAI decorator type checking.
This plugin informs mypy about attributes injected by the @CrewBase decorator.
"""
from collections.abc import Callable
from mypy.nodes import MDEF, SymbolTableNode, Var
from mypy.plugin import ClassDefContext, Plugin
from mypy.types import AnyType, TypeOfAny
class CrewAIPlugin(Plugin):
"""Mypy plugin that handles @CrewBase decorator attribute injection."""
def get_class_decorator_hook(
self, fullname: str
) -> Callable[[ClassDefContext], None] | None:
"""Return hook for class decorators.
Args:
fullname: Fully qualified name of the decorator.
Returns:
Hook function if this is a CrewBase decorator, None otherwise.
"""
if fullname in ("crewai.project.CrewBase", "crewai.project.crew_base.CrewBase"):
return self._crew_base_hook
return None
@staticmethod
def _crew_base_hook(ctx: ClassDefContext) -> None:
"""Add injected attributes to @CrewBase decorated classes.
Args:
ctx: Context for the class being decorated.
"""
any_type = AnyType(TypeOfAny.explicit)
str_type = ctx.api.named_type("builtins.str")
dict_type = ctx.api.named_type("builtins.dict", [str_type, any_type])
agents_config_var = Var("agents_config", dict_type)
agents_config_var.info = ctx.cls.info
agents_config_var._fullname = f"{ctx.cls.info.fullname}.agents_config"
ctx.cls.info.names["agents_config"] = SymbolTableNode(MDEF, agents_config_var)
tasks_config_var = Var("tasks_config", dict_type)
tasks_config_var.info = ctx.cls.info
tasks_config_var._fullname = f"{ctx.cls.info.fullname}.tasks_config"
ctx.cls.info.names["tasks_config"] = SymbolTableNode(MDEF, tasks_config_var)
def plugin(_: str) -> type[Plugin]:
"""Entry point for mypy plugin.
Args:
_: Mypy version string.
Returns:
Plugin class.
"""
return CrewAIPlugin

View File

@@ -20,7 +20,7 @@ from typing_extensions import Self
if TYPE_CHECKING:
from crewai import Agent, Task
from crewai import Agent, Crew, Task
from crewai.crews.crew_output import CrewOutput
from crewai.tools import BaseTool
@@ -129,6 +129,7 @@ class CrewClass(Protocol):
_map_agent_variables: Callable[..., None]
map_all_task_variables: Callable[..., None]
_map_task_variables: Callable[..., None]
crew: Callable[..., Crew]
class DecoratedMethod(Generic[P, R]):

View File

@@ -196,10 +196,17 @@ def format_answer(answer: str) -> AgentAction | AgentFinish:
Returns:
Either an AgentAction or AgentFinish
Raises:
OutputParserError: If the LLM response format is invalid, allowing
the retry logic in _invoke_loop() to handle it.
"""
try:
return parse(answer)
except OutputParserError:
raise
except Exception:
# For unexpected errors, return a default AgentFinish
return AgentFinish(
thought="Failed to parse LLM response",
output=answer,

View File

@@ -4,6 +4,8 @@ from typing import TYPE_CHECKING, Any, Generic, TypeGuard, TypeVar
from pydantic import BaseModel
from crewai.utilities.logger_utils import suppress_warnings
if TYPE_CHECKING:
from crewai.agent import Agent
@@ -11,9 +13,6 @@ if TYPE_CHECKING:
from crewai.llms.base_llm import BaseLLM
from crewai.utilities.types import LLMMessage
from crewai.utilities.logger_utils import suppress_warnings
T = TypeVar("T", bound=BaseModel)
@@ -62,9 +61,59 @@ class InternalInstructor(Generic[T]):
with suppress_warnings():
import instructor # type: ignore[import-untyped]
from litellm import completion
self._client = instructor.from_litellm(completion)
if (
self.llm is not None
and hasattr(self.llm, "is_litellm")
and self.llm.is_litellm
):
from litellm import completion
self._client = instructor.from_litellm(completion)
else:
self._client = self._create_instructor_client()
def _create_instructor_client(self) -> Any:
"""Create instructor client using the modern from_provider pattern.
Returns:
Instructor client configured for the LLM provider
Raises:
ValueError: If the provider is not supported
"""
import instructor
if isinstance(self.llm, str):
model_string = self.llm
elif self.llm is not None and hasattr(self.llm, "model"):
model_string = self.llm.model
else:
raise ValueError("LLM must be a string or have a model attribute")
if isinstance(self.llm, str):
provider = self._extract_provider()
elif self.llm is not None and hasattr(self.llm, "provider"):
provider = self.llm.provider
else:
provider = "openai" # Default fallback
return instructor.from_provider(f"{provider}/{model_string}")
def _extract_provider(self) -> str:
"""Extract provider from LLM model name.
Returns:
Provider name (e.g., 'openai', 'anthropic', etc.)
"""
if self.llm is not None and hasattr(self.llm, "provider") and self.llm.provider:
return self.llm.provider
if isinstance(self.llm, str):
return self.llm.partition("/")[0] or "openai"
if self.llm is not None and hasattr(self.llm, "model"):
return self.llm.model.partition("/")[0] or "openai"
return "openai"
def to_json(self) -> str:
"""Convert the structured output to JSON format.
@@ -96,6 +145,6 @@ class InternalInstructor(Generic[T]):
else:
model_name = self.llm.model
return self._client.chat.completions.create(
return self._client.chat.completions.create( # type: ignore[no-any-return]
model=model_name, response_model=self.model, messages=messages
)

View File

@@ -902,7 +902,8 @@ def test_agent_step_callback():
@pytest.mark.vcr(filter_headers=["authorization"])
def test_agent_function_calling_llm():
llm = "gpt-4o"
from crewai.llm import LLM
llm = LLM(model="gpt-4o", is_litellm=True)
@tool
def learn_about_ai() -> str:

View File

@@ -1,137 +0,0 @@
"""Test lazy loading of knowledge sources to prevent premature authentication errors."""
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from crewai import Agent, Crew, Task
from crewai.knowledge.knowledge import Knowledge
from crewai.knowledge.source.text_file_knowledge_source import TextFileKnowledgeSource
def test_knowledge_sources_not_loaded_during_initialization(tmpdir):
"""Test that knowledge sources are not loaded during agent/crew initialization."""
# Create a test file
test_file = Path(tmpdir) / "test.txt"
test_file.write_text("Test content")
# Create knowledge source
knowledge_source = TextFileKnowledgeSource(file_paths=[test_file])
# Mock the storage to avoid actual database operations
with patch('crewai.knowledge.knowledge.KnowledgeStorage'):
# Create Knowledge object
knowledge = Knowledge(
collection_name="test",
sources=[knowledge_source],
embedder=None
)
# Verify that sources are not loaded yet
assert knowledge._sources_loaded is False
def test_knowledge_sources_loaded_on_first_query(tmpdir):
"""Test that knowledge sources are loaded only when first queried."""
# Create a test file
test_file = Path(tmpdir) / "test.txt"
test_file.write_text("Test content")
# Create knowledge source
knowledge_source = TextFileKnowledgeSource(file_paths=[test_file])
# Mock the storage to avoid actual database operations
with patch('crewai.knowledge.knowledge.KnowledgeStorage') as MockStorage:
mock_storage = MagicMock()
mock_storage.search.return_value = []
MockStorage.return_value = mock_storage
# Create Knowledge object
knowledge = Knowledge(
collection_name="test",
sources=[knowledge_source],
embedder=None
)
# Verify sources not loaded yet
assert knowledge._sources_loaded is False
with patch.object(Knowledge, 'add_sources', wraps=knowledge.add_sources) as mock_add_sources:
# Query should trigger loading
knowledge.query(["test query"])
# Verify add_sources was called
mock_add_sources.assert_called_once()
# Verify sources are now marked as loaded
assert knowledge._sources_loaded is True
# Query again - add_sources should not be called again
with patch.object(Knowledge, 'add_sources', wraps=knowledge.add_sources) as mock_add_sources:
knowledge.query(["another query"])
mock_add_sources.assert_not_called()
def test_agent_with_knowledge_sources_no_immediate_loading(tmpdir):
"""Test that creating an agent with knowledge sources doesn't immediately load them."""
# Create a test file
test_file = Path(tmpdir) / "test.txt"
test_file.write_text("Test content")
# Create knowledge source
knowledge_source = TextFileKnowledgeSource(file_paths=[test_file])
# Mock the storage to avoid authentication errors
with patch('crewai.knowledge.knowledge.KnowledgeStorage'):
# Create agent with knowledge source
agent = Agent(
role="Test Agent",
goal="Test goal",
backstory="Test backstory",
knowledge_sources=[knowledge_source],
)
# Create task and crew
task = Task(
description="Test task",
expected_output="Test output",
agent=agent
)
crew = Crew(
agents=[agent],
tasks=[task],
)
# but sources should not be loaded yet
if agent.knowledge is not None:
assert agent.knowledge._sources_loaded is False
def test_knowledge_add_sources_can_still_be_called_explicitly():
"""Test that add_sources can still be called explicitly if needed."""
# Create a mock knowledge source
mock_source = MagicMock()
mock_source.add = MagicMock()
# Mock the storage
with patch('crewai.knowledge.knowledge.KnowledgeStorage') as MockStorage:
mock_storage = MagicMock()
MockStorage.return_value = mock_storage
# Create Knowledge object
knowledge = Knowledge(
collection_name="test",
sources=[mock_source],
embedder=None
)
# Explicitly call add_sources
knowledge.add_sources()
# Verify add was called
mock_source.add.assert_called_once()
# Verify sources are marked as loaded
assert knowledge._sources_loaded is True

View File

@@ -0,0 +1,82 @@
"""Tests for agent_utils module, specifically format_answer function."""
import pytest
from crewai.agents.parser import AgentAction, AgentFinish, OutputParserError
from crewai.utilities.agent_utils import format_answer
def test_format_answer_with_valid_action():
"""Test that format_answer correctly parses valid action format."""
text = "Thought: Let's search\nAction: search\nAction Input: what is the weather?"
result = format_answer(text)
assert isinstance(result, AgentAction)
assert result.tool == "search"
assert result.tool_input == "what is the weather?"
def test_format_answer_with_valid_final_answer():
"""Test that format_answer correctly parses valid final answer format."""
text = "Thought: I have the answer\nFinal Answer: The weather is sunny"
result = format_answer(text)
assert isinstance(result, AgentFinish)
assert result.output == "The weather is sunny"
def test_format_answer_with_malformed_output_missing_colons():
"""Test that format_answer re-raises OutputParserError for malformed output.
This is the core issue from bug #3771. When the LLM returns malformed output
(e.g., missing colons after "Thought", "Action", "Action Input"), the
format_answer function should re-raise OutputParserError so the retry logic
in _invoke_loop() can handle it properly.
"""
malformed_text = """Thought
The user wants to verify something.
Action
Video Analysis Tool
Action Input:
{"query": "Is there something?"}"""
with pytest.raises(OutputParserError) as exc_info:
format_answer(malformed_text)
assert "Invalid Format" in str(exc_info.value) or "missed" in str(exc_info.value)
def test_format_answer_with_missing_action():
"""Test that format_answer re-raises OutputParserError when Action is missing."""
text = "Thought: Let's search\nAction Input: what is the weather?"
with pytest.raises(OutputParserError) as exc_info:
format_answer(text)
assert "Invalid Format: I missed the 'Action:' after 'Thought:'." in str(
exc_info.value
)
def test_format_answer_with_missing_action_input():
"""Test that format_answer re-raises OutputParserError when Action Input is missing."""
text = "Thought: Let's search\nAction: search"
with pytest.raises(OutputParserError) as exc_info:
format_answer(text)
assert "I missed the 'Action Input:' after 'Action:'." in str(exc_info.value)
def test_format_answer_with_unexpected_exception():
"""Test that format_answer returns AgentFinish for truly unexpected errors.
This tests that non-OutputParserError exceptions are still caught and
converted to AgentFinish as a fallback behavior.
"""
pass
def test_format_answer_preserves_original_text():
"""Test that format_answer preserves the original text in the result."""
text = "Thought: Let's search\nAction: search\nAction Input: weather"
result = format_answer(text)
assert result.text == text

View File

@@ -22,7 +22,7 @@ import pytest
@pytest.fixture(scope="module")
def vcr_config(request) -> dict:
def vcr_config(request: pytest.FixtureRequest) -> dict[str, str]:
return {
"cassette_library_dir": os.path.join(os.path.dirname(__file__), "cassettes"),
}
@@ -65,7 +65,7 @@ class CustomConverter(Converter):
# Fixtures
@pytest.fixture
def mock_agent():
def mock_agent() -> Mock:
agent = Mock()
agent.function_calling_llm = None
agent.llm = Mock()
@@ -73,7 +73,7 @@ def mock_agent():
# Tests for convert_to_model
def test_convert_to_model_with_valid_json():
def test_convert_to_model_with_valid_json() -> None:
result = '{"name": "John", "age": 30}'
output = convert_to_model(result, SimpleModel, None, None)
assert isinstance(output, SimpleModel)
@@ -81,7 +81,7 @@ def test_convert_to_model_with_valid_json():
assert output.age == 30
def test_convert_to_model_with_invalid_json():
def test_convert_to_model_with_invalid_json() -> None:
result = '{"name": "John", "age": "thirty"}'
with patch("crewai.utilities.converter.handle_partial_json") as mock_handle:
mock_handle.return_value = "Fallback result"
@@ -89,13 +89,13 @@ def test_convert_to_model_with_invalid_json():
assert output == "Fallback result"
def test_convert_to_model_with_no_model():
def test_convert_to_model_with_no_model() -> None:
result = "Plain text"
output = convert_to_model(result, None, None, None)
assert output == "Plain text"
def test_convert_to_model_with_special_characters():
def test_convert_to_model_with_special_characters() -> None:
json_string_test = """
{
"responses": [
@@ -114,7 +114,7 @@ def test_convert_to_model_with_special_characters():
)
def test_convert_to_model_with_escaped_special_characters():
def test_convert_to_model_with_escaped_special_characters() -> None:
json_string_test = json.dumps(
{
"responses": [
@@ -133,7 +133,7 @@ def test_convert_to_model_with_escaped_special_characters():
)
def test_convert_to_model_with_multiple_special_characters():
def test_convert_to_model_with_multiple_special_characters() -> None:
json_string_test = """
{
"responses": [
@@ -153,7 +153,7 @@ def test_convert_to_model_with_multiple_special_characters():
# Tests for validate_model
def test_validate_model_pydantic_output():
def test_validate_model_pydantic_output() -> None:
result = '{"name": "Alice", "age": 25}'
output = validate_model(result, SimpleModel, False)
assert isinstance(output, SimpleModel)
@@ -161,7 +161,7 @@ def test_validate_model_pydantic_output():
assert output.age == 25
def test_validate_model_json_output():
def test_validate_model_json_output() -> None:
result = '{"name": "Bob", "age": 40}'
output = validate_model(result, SimpleModel, True)
assert isinstance(output, dict)
@@ -169,7 +169,7 @@ def test_validate_model_json_output():
# Tests for handle_partial_json
def test_handle_partial_json_with_valid_partial():
def test_handle_partial_json_with_valid_partial() -> None:
result = 'Some text {"name": "Charlie", "age": 35} more text'
output = handle_partial_json(result, SimpleModel, False, None)
assert isinstance(output, SimpleModel)
@@ -177,7 +177,7 @@ def test_handle_partial_json_with_valid_partial():
assert output.age == 35
def test_handle_partial_json_with_invalid_partial(mock_agent):
def test_handle_partial_json_with_invalid_partial(mock_agent: Mock) -> None:
result = "No valid JSON here"
with patch("crewai.utilities.converter.convert_with_instructions") as mock_convert:
mock_convert.return_value = "Converted result"
@@ -189,8 +189,8 @@ def test_handle_partial_json_with_invalid_partial(mock_agent):
@patch("crewai.utilities.converter.create_converter")
@patch("crewai.utilities.converter.get_conversion_instructions")
def test_convert_with_instructions_success(
mock_get_instructions, mock_create_converter, mock_agent
):
mock_get_instructions: Mock, mock_create_converter: Mock, mock_agent: Mock
) -> None:
mock_get_instructions.return_value = "Instructions"
mock_converter = Mock()
mock_converter.to_pydantic.return_value = SimpleModel(name="David", age=50)
@@ -207,8 +207,8 @@ def test_convert_with_instructions_success(
@patch("crewai.utilities.converter.create_converter")
@patch("crewai.utilities.converter.get_conversion_instructions")
def test_convert_with_instructions_failure(
mock_get_instructions, mock_create_converter, mock_agent
):
mock_get_instructions: Mock, mock_create_converter: Mock, mock_agent: Mock
) -> None:
mock_get_instructions.return_value = "Instructions"
mock_converter = Mock()
mock_converter.to_pydantic.return_value = ConverterError("Conversion failed")
@@ -222,7 +222,7 @@ def test_convert_with_instructions_failure(
# Tests for get_conversion_instructions
def test_get_conversion_instructions_gpt():
def test_get_conversion_instructions_gpt() -> None:
llm = LLM(model="gpt-4o-mini")
with patch.object(LLM, "supports_function_calling") as supports_function_calling:
supports_function_calling.return_value = True
@@ -237,7 +237,7 @@ def test_get_conversion_instructions_gpt():
assert instructions == expected_instructions
def test_get_conversion_instructions_non_gpt():
def test_get_conversion_instructions_non_gpt() -> None:
llm = LLM(model="ollama/llama3.1", base_url="http://localhost:11434")
with patch.object(LLM, "supports_function_calling", return_value=False):
instructions = get_conversion_instructions(SimpleModel, llm)
@@ -246,17 +246,17 @@ def test_get_conversion_instructions_non_gpt():
# Tests for is_gpt
def test_supports_function_calling_true():
def test_supports_function_calling_true() -> None:
llm = LLM(model="gpt-4o")
assert llm.supports_function_calling() is True
def test_supports_function_calling_false():
def test_supports_function_calling_false() -> None:
llm = LLM(model="non-existent-model", is_litellm=True)
assert llm.supports_function_calling() is False
def test_create_converter_with_mock_agent():
def test_create_converter_with_mock_agent() -> None:
mock_agent = MagicMock()
mock_agent.get_output_converter.return_value = MagicMock(spec=Converter)
@@ -272,7 +272,7 @@ def test_create_converter_with_mock_agent():
mock_agent.get_output_converter.assert_called_once()
def test_create_converter_with_custom_converter():
def test_create_converter_with_custom_converter() -> None:
converter = create_converter(
converter_cls=CustomConverter,
llm=LLM(model="gpt-4o-mini"),
@@ -284,7 +284,7 @@ def test_create_converter_with_custom_converter():
assert isinstance(converter, CustomConverter)
def test_create_converter_fails_without_agent_or_converter_cls():
def test_create_converter_fails_without_agent_or_converter_cls() -> None:
with pytest.raises(
ValueError, match="Either agent or converter_cls must be provided"
):
@@ -293,13 +293,13 @@ def test_create_converter_fails_without_agent_or_converter_cls():
)
def test_generate_model_description_simple_model():
def test_generate_model_description_simple_model() -> None:
description = generate_model_description(SimpleModel)
expected_description = '{\n "name": str,\n "age": int\n}'
assert description == expected_description
def test_generate_model_description_nested_model():
def test_generate_model_description_nested_model() -> None:
description = generate_model_description(NestedModel)
expected_description = (
'{\n "id": int,\n "data": {\n "name": str,\n "age": int\n}\n}'
@@ -307,7 +307,7 @@ def test_generate_model_description_nested_model():
assert description == expected_description
def test_generate_model_description_optional_field():
def test_generate_model_description_optional_field() -> None:
class ModelWithOptionalField(BaseModel):
name: str
age: int | None
@@ -317,7 +317,7 @@ def test_generate_model_description_optional_field():
assert description == expected_description
def test_generate_model_description_list_field():
def test_generate_model_description_list_field() -> None:
class ModelWithListField(BaseModel):
items: list[int]
@@ -326,7 +326,7 @@ def test_generate_model_description_list_field():
assert description == expected_description
def test_generate_model_description_dict_field():
def test_generate_model_description_dict_field() -> None:
class ModelWithDictField(BaseModel):
attributes: dict[str, int]
@@ -336,7 +336,7 @@ def test_generate_model_description_dict_field():
@pytest.mark.vcr(filter_headers=["authorization"])
def test_convert_with_instructions():
def test_convert_with_instructions() -> None:
llm = LLM(model="gpt-4o-mini")
sample_text = "Name: Alice, Age: 30"
@@ -358,7 +358,7 @@ def test_convert_with_instructions():
@pytest.mark.vcr(filter_headers=["authorization"])
def test_converter_with_llama3_2_model():
def test_converter_with_llama3_2_model() -> None:
llm = LLM(model="openrouter/meta-llama/llama-3.2-3b-instruct")
sample_text = "Name: Alice Llama, Age: 30"
instructions = get_conversion_instructions(SimpleModel, llm)
@@ -375,7 +375,7 @@ def test_converter_with_llama3_2_model():
@pytest.mark.vcr(filter_headers=["authorization"])
def test_converter_with_llama3_1_model():
def test_converter_with_llama3_1_model() -> None:
llm = LLM(model="ollama/llama3.1", base_url="http://localhost:11434")
sample_text = "Name: Alice Llama, Age: 30"
instructions = get_conversion_instructions(SimpleModel, llm)
@@ -392,7 +392,7 @@ def test_converter_with_llama3_1_model():
@pytest.mark.vcr(filter_headers=["authorization"])
def test_converter_with_nested_model():
def test_converter_with_nested_model() -> None:
llm = LLM(model="gpt-4o-mini")
sample_text = "Name: John Doe\nAge: 30\nAddress: 123 Main St, Anytown, 12345"
@@ -416,7 +416,7 @@ def test_converter_with_nested_model():
# Tests for error handling
def test_converter_error_handling():
def test_converter_error_handling() -> None:
llm = Mock(spec=LLM)
llm.supports_function_calling.return_value = False
llm.call.return_value = "Invalid JSON"
@@ -437,7 +437,7 @@ def test_converter_error_handling():
# Tests for retry logic
def test_converter_retry_logic():
def test_converter_retry_logic() -> None:
llm = Mock(spec=LLM)
llm.supports_function_calling.return_value = False
llm.call.side_effect = [
@@ -465,7 +465,7 @@ def test_converter_retry_logic():
# Tests for optional fields
def test_converter_with_optional_fields():
def test_converter_with_optional_fields() -> None:
class OptionalModel(BaseModel):
name: str
age: int | None
@@ -492,7 +492,7 @@ def test_converter_with_optional_fields():
# Tests for list fields
def test_converter_with_list_field():
def test_converter_with_list_field() -> None:
class ListModel(BaseModel):
items: list[int]
@@ -515,7 +515,7 @@ def test_converter_with_list_field():
assert output.items == [1, 2, 3]
def test_converter_with_enum():
def test_converter_with_enum() -> None:
class Color(Enum):
RED = "red"
GREEN = "green"
@@ -546,7 +546,7 @@ def test_converter_with_enum():
# Tests for ambiguous input
def test_converter_with_ambiguous_input():
def test_converter_with_ambiguous_input() -> None:
llm = Mock(spec=LLM)
llm.supports_function_calling.return_value = False
llm.call.return_value = '{"name": "Charlie", "age": "Not an age"}'
@@ -567,7 +567,7 @@ def test_converter_with_ambiguous_input():
# Tests for function calling support
def test_converter_with_function_calling():
def test_converter_with_function_calling() -> None:
llm = Mock(spec=LLM)
llm.supports_function_calling.return_value = True
@@ -580,20 +580,359 @@ def test_converter_with_function_calling():
model=SimpleModel,
instructions="Convert this text.",
)
converter._create_instructor = Mock(return_value=instructor)
with patch.object(converter, '_create_instructor', return_value=instructor):
output = converter.to_pydantic()
output = converter.to_pydantic()
assert isinstance(output, SimpleModel)
assert output.name == "Eve"
assert output.age == 35
assert isinstance(output, SimpleModel)
assert output.name == "Eve"
assert output.age == 35
instructor.to_pydantic.assert_called_once()
def test_generate_model_description_union_field():
def test_generate_model_description_union_field() -> None:
class UnionModel(BaseModel):
field: int | str | None
description = generate_model_description(UnionModel)
expected_description = '{\n "field": int | str | None\n}'
assert description == expected_description
def test_internal_instructor_with_openai_provider() -> None:
"""Test InternalInstructor with OpenAI provider using registry pattern."""
from crewai.utilities.internal_instructor import InternalInstructor
# Mock LLM with OpenAI provider
mock_llm = Mock()
mock_llm.is_litellm = False
mock_llm.model = "gpt-4o"
mock_llm.provider = "openai"
# Mock instructor client
mock_client = Mock()
mock_client.chat.completions.create.return_value = SimpleModel(name="Test", age=25)
# Patch the instructor import at the method level
with patch.object(InternalInstructor, '_create_instructor_client') as mock_create_client:
mock_create_client.return_value = mock_client
instructor = InternalInstructor(
content="Test content",
model=SimpleModel,
llm=mock_llm
)
result = instructor.to_pydantic()
assert isinstance(result, SimpleModel)
assert result.name == "Test"
assert result.age == 25
# Verify the method was called with the correct LLM
mock_create_client.assert_called_once()
def test_internal_instructor_with_anthropic_provider() -> None:
"""Test InternalInstructor with Anthropic provider using registry pattern."""
from crewai.utilities.internal_instructor import InternalInstructor
# Mock LLM with Anthropic provider
mock_llm = Mock()
mock_llm.is_litellm = False
mock_llm.model = "claude-3-5-sonnet-20241022"
mock_llm.provider = "anthropic"
# Mock instructor client
mock_client = Mock()
mock_client.chat.completions.create.return_value = SimpleModel(name="Bob", age=25)
# Patch the instructor import at the method level
with patch.object(InternalInstructor, '_create_instructor_client') as mock_create_client:
mock_create_client.return_value = mock_client
instructor = InternalInstructor(
content="Name: Bob, Age: 25",
model=SimpleModel,
llm=mock_llm
)
result = instructor.to_pydantic()
assert isinstance(result, SimpleModel)
assert result.name == "Bob"
assert result.age == 25
# Verify the method was called with the correct LLM
mock_create_client.assert_called_once()
def test_factory_pattern_registry_extensibility() -> None:
"""Test that the factory pattern registry works with different providers."""
from crewai.utilities.internal_instructor import InternalInstructor
# Test with OpenAI provider
mock_llm_openai = Mock()
mock_llm_openai.is_litellm = False
mock_llm_openai.model = "gpt-4o-mini"
mock_llm_openai.provider = "openai"
mock_client_openai = Mock()
mock_client_openai.chat.completions.create.return_value = SimpleModel(name="Alice", age=30)
with patch.object(InternalInstructor, '_create_instructor_client') as mock_create_client:
mock_create_client.return_value = mock_client_openai
instructor_openai = InternalInstructor(
content="Name: Alice, Age: 30",
model=SimpleModel,
llm=mock_llm_openai
)
result_openai = instructor_openai.to_pydantic()
assert isinstance(result_openai, SimpleModel)
assert result_openai.name == "Alice"
assert result_openai.age == 30
# Test with Anthropic provider
mock_llm_anthropic = Mock()
mock_llm_anthropic.is_litellm = False
mock_llm_anthropic.model = "claude-3-5-sonnet-20241022"
mock_llm_anthropic.provider = "anthropic"
mock_client_anthropic = Mock()
mock_client_anthropic.chat.completions.create.return_value = SimpleModel(name="Bob", age=25)
with patch.object(InternalInstructor, '_create_instructor_client') as mock_create_client:
mock_create_client.return_value = mock_client_anthropic
instructor_anthropic = InternalInstructor(
content="Name: Bob, Age: 25",
model=SimpleModel,
llm=mock_llm_anthropic
)
result_anthropic = instructor_anthropic.to_pydantic()
assert isinstance(result_anthropic, SimpleModel)
assert result_anthropic.name == "Bob"
assert result_anthropic.age == 25
# Test with Bedrock provider
mock_llm_bedrock = Mock()
mock_llm_bedrock.is_litellm = False
mock_llm_bedrock.model = "claude-3-5-sonnet-20241022"
mock_llm_bedrock.provider = "bedrock"
mock_client_bedrock = Mock()
mock_client_bedrock.chat.completions.create.return_value = SimpleModel(name="Charlie", age=35)
with patch.object(InternalInstructor, '_create_instructor_client') as mock_create_client:
mock_create_client.return_value = mock_client_bedrock
instructor_bedrock = InternalInstructor(
content="Name: Charlie, Age: 35",
model=SimpleModel,
llm=mock_llm_bedrock
)
result_bedrock = instructor_bedrock.to_pydantic()
assert isinstance(result_bedrock, SimpleModel)
assert result_bedrock.name == "Charlie"
assert result_bedrock.age == 35
# Test with Google provider
mock_llm_google = Mock()
mock_llm_google.is_litellm = False
mock_llm_google.model = "gemini-1.5-flash"
mock_llm_google.provider = "google"
mock_client_google = Mock()
mock_client_google.chat.completions.create.return_value = SimpleModel(name="Diana", age=28)
with patch.object(InternalInstructor, '_create_instructor_client') as mock_create_client:
mock_create_client.return_value = mock_client_google
instructor_google = InternalInstructor(
content="Name: Diana, Age: 28",
model=SimpleModel,
llm=mock_llm_google
)
result_google = instructor_google.to_pydantic()
assert isinstance(result_google, SimpleModel)
assert result_google.name == "Diana"
assert result_google.age == 28
# Test with Azure provider
mock_llm_azure = Mock()
mock_llm_azure.is_litellm = False
mock_llm_azure.model = "gpt-4o"
mock_llm_azure.provider = "azure"
mock_client_azure = Mock()
mock_client_azure.chat.completions.create.return_value = SimpleModel(name="Eve", age=32)
with patch.object(InternalInstructor, '_create_instructor_client') as mock_create_client:
mock_create_client.return_value = mock_client_azure
instructor_azure = InternalInstructor(
content="Name: Eve, Age: 32",
model=SimpleModel,
llm=mock_llm_azure
)
result_azure = instructor_azure.to_pydantic()
assert isinstance(result_azure, SimpleModel)
assert result_azure.name == "Eve"
assert result_azure.age == 32
def test_internal_instructor_with_bedrock_provider() -> None:
"""Test InternalInstructor with AWS Bedrock provider using registry pattern."""
from crewai.utilities.internal_instructor import InternalInstructor
# Mock LLM with Bedrock provider
mock_llm = Mock()
mock_llm.is_litellm = False
mock_llm.model = "claude-3-5-sonnet-20241022"
mock_llm.provider = "bedrock"
# Mock instructor client
mock_client = Mock()
mock_client.chat.completions.create.return_value = SimpleModel(name="Charlie", age=35)
# Patch the instructor import at the method level
with patch.object(InternalInstructor, '_create_instructor_client') as mock_create_client:
mock_create_client.return_value = mock_client
instructor = InternalInstructor(
content="Name: Charlie, Age: 35",
model=SimpleModel,
llm=mock_llm
)
result = instructor.to_pydantic()
assert isinstance(result, SimpleModel)
assert result.name == "Charlie"
assert result.age == 35
# Verify the method was called with the correct LLM
mock_create_client.assert_called_once()
def test_internal_instructor_with_gemini_provider() -> None:
"""Test InternalInstructor with Google Gemini provider using registry pattern."""
from crewai.utilities.internal_instructor import InternalInstructor
# Mock LLM with Gemini provider
mock_llm = Mock()
mock_llm.is_litellm = False
mock_llm.model = "gemini-1.5-flash"
mock_llm.provider = "google"
# Mock instructor client
mock_client = Mock()
mock_client.chat.completions.create.return_value = SimpleModel(name="Diana", age=28)
# Patch the instructor import at the method level
with patch.object(InternalInstructor, '_create_instructor_client') as mock_create_client:
mock_create_client.return_value = mock_client
instructor = InternalInstructor(
content="Name: Diana, Age: 28",
model=SimpleModel,
llm=mock_llm
)
result = instructor.to_pydantic()
assert isinstance(result, SimpleModel)
assert result.name == "Diana"
assert result.age == 28
# Verify the method was called with the correct LLM
mock_create_client.assert_called_once()
def test_internal_instructor_with_azure_provider() -> None:
"""Test InternalInstructor with Azure OpenAI provider using registry pattern."""
from crewai.utilities.internal_instructor import InternalInstructor
# Mock LLM with Azure provider
mock_llm = Mock()
mock_llm.is_litellm = False
mock_llm.model = "gpt-4o"
mock_llm.provider = "azure"
# Mock instructor client
mock_client = Mock()
mock_client.chat.completions.create.return_value = SimpleModel(name="Eve", age=32)
# Patch the instructor import at the method level
with patch.object(InternalInstructor, '_create_instructor_client') as mock_create_client:
mock_create_client.return_value = mock_client
instructor = InternalInstructor(
content="Name: Eve, Age: 32",
model=SimpleModel,
llm=mock_llm
)
result = instructor.to_pydantic()
assert isinstance(result, SimpleModel)
assert result.name == "Eve"
assert result.age == 32
# Verify the method was called with the correct LLM
mock_create_client.assert_called_once()
def test_internal_instructor_unsupported_provider() -> None:
"""Test InternalInstructor with unsupported provider raises appropriate error."""
from crewai.utilities.internal_instructor import InternalInstructor
# Mock LLM with unsupported provider
mock_llm = Mock()
mock_llm.is_litellm = False
mock_llm.model = "unsupported-model"
mock_llm.provider = "unsupported"
# Mock the _create_instructor_client method to raise an error for unsupported providers
with patch.object(InternalInstructor, '_create_instructor_client') as mock_create_client:
mock_create_client.side_effect = Exception("Unsupported provider: unsupported")
# This should raise an error when trying to create the instructor client
with pytest.raises(Exception) as exc_info:
instructor = InternalInstructor(
content="Test content",
model=SimpleModel,
llm=mock_llm
)
instructor.to_pydantic()
# Verify it's the expected error
assert "Unsupported provider" in str(exc_info.value)
def test_internal_instructor_real_unsupported_provider() -> None:
"""Test InternalInstructor with real unsupported provider using actual instructor library."""
from crewai.utilities.internal_instructor import InternalInstructor
# Mock LLM with unsupported provider that would actually fail with instructor
mock_llm = Mock()
mock_llm.is_litellm = False
mock_llm.model = "unsupported-model"
mock_llm.provider = "unsupported"
# This should raise a ConfigurationError from the real instructor library
with pytest.raises(Exception) as exc_info:
instructor = InternalInstructor(
content="Test content",
model=SimpleModel,
llm=mock_llm
)
instructor.to_pydantic()
# Verify it's a configuration error about unsupported provider
assert "Unsupported provider" in str(exc_info.value) or "unsupported" in str(exc_info.value).lower()

View File

@@ -1,3 +1,3 @@
"""CrewAI development tools."""
__version__ = "1.0.0"
__version__ = "1.1.0"

View File

@@ -124,7 +124,7 @@ exclude = [
"lib/crewai-tools/tests/",
"lib/crewai/src/crewai/experimental/a2a"
]
plugins = ["pydantic.mypy"]
plugins = ["pydantic.mypy", "crewai.mypy"]
[tool.bandit]