chore: add ContextVar hooks and small refactors

Introduce ContextVar-backed hooks and small API/behavior changes to improve extensibility and testability.

Changes include:
- agents: mark configure_structured_output as abstract and change its parameter to task to reflect use of task metadata.
- tracing: convert _first_time_trace_hook to a ContextVar and call .get() to safely retrieve the hook.
- console formatter: add _disable_version_check ContextVar and skip version checks when set (avoids noisy checks in certain contexts).
- flow: use current_triggering_event_id variable when scheduling listener tasks to keep naming consistent.
- hallucination guardrail: make context optional, add _validate_output_hook to allow custom validation hooks, update examples and return contract to allow hooks to override behavior.
- agent utilities: add _create_plus_client_hook for injecting a Plus client (used in tests/alternate flows), ensure structured tools have current_usage_count initialized and propagate to original tool, and fall back to creating PlusAPI client when no hook is provided.
This commit is contained in:
Greyson LaLonde
2026-02-04 20:19:58 -05:00
parent cea22e1ce7
commit 138eff6184
6 changed files with 54 additions and 23 deletions

View File

@@ -37,9 +37,10 @@ class BaseAgentAdapter(BaseAgent, ABC):
tools: Optional list of BaseTool instances to be configured
"""
def configure_structured_output(self, structured_output: Any) -> None:
@abstractmethod
def configure_structured_output(self, task: Any) -> None:
"""Configure the structured output for the specific agent implementation.
Args:
structured_output: The structured output to be configured
task: The task object containing output format specifications.
"""

View File

@@ -27,7 +27,9 @@ logger = logging.getLogger(__name__)
_tracing_enabled: ContextVar[bool | None] = ContextVar("_tracing_enabled", default=None)
_first_time_trace_hook: Callable[[], bool] | None = None
_first_time_trace_hook: ContextVar[Callable[[], bool] | None] = ContextVar(
"_first_time_trace_hook", default=None
)
def should_enable_tracing(*, override: bool | None = None) -> bool:
@@ -413,8 +415,9 @@ def should_auto_collect_first_time_traces() -> bool:
Returns:
True if first-time user AND telemetry not disabled AND tracing not explicitly enabled, False otherwise.
"""
if _first_time_trace_hook is not None:
return _first_time_trace_hook()
hook = _first_time_trace_hook.get()
if hook is not None:
return hook()
if _is_test_environment():
return False

View File

@@ -1,3 +1,4 @@
from contextvars import ContextVar
import os
import threading
from typing import Any, ClassVar, cast
@@ -10,6 +11,11 @@ from rich.text import Text
from crewai.cli.version import is_newer_version_available
_disable_version_check: ContextVar[bool] = ContextVar(
"_disable_version_check", default=False
)
class ConsoleFormatter:
tool_usage_counts: ClassVar[dict[str, int]] = {}
@@ -46,6 +52,9 @@ class ConsoleFormatter:
if not self.verbose:
return
if _disable_version_check.get():
return
if os.getenv("CI", "").lower() in ("true", "1"):
return

View File

@@ -2074,12 +2074,14 @@ class Flow(Generic[T], metaclass=FlowMeta):
racing_members,
other_listeners,
listener_result,
triggering_event_id,
current_triggering_event_id,
)
else:
tasks = [
self._execute_single_listener(
listener_name, listener_result, triggering_event_id
listener_name,
listener_result,
current_triggering_event_id,
)
for listener_name in listeners_triggered
]

View File

@@ -6,6 +6,7 @@ Classes:
HallucinationGuardrail: Placeholder guardrail that validates task outputs.
"""
from collections.abc import Callable
from typing import Any
from crewai.llm import LLM
@@ -13,32 +14,36 @@ from crewai.tasks.task_output import TaskOutput
from crewai.utilities.logger import Logger
_validate_output_hook: Callable[..., tuple[bool, Any]] | None = None
class HallucinationGuardrail:
"""Placeholder for the HallucinationGuardrail feature.
Attributes:
context: The reference context that outputs would be checked against.
context: Optional reference context that outputs would be checked against.
llm: The language model that would be used for evaluation.
threshold: Optional minimum faithfulness score that would be required to pass.
tool_response: Optional tool response information that would be used in evaluation.
Examples:
>>> # Basic usage with default verdict logic
>>> # Basic usage without context (uses task expected_output as context)
>>> guardrail = HallucinationGuardrail(llm=agent.llm)
>>> # With context for reference
>>> guardrail = HallucinationGuardrail(
... context="AI helps with various tasks including analysis and generation.",
... llm=agent.llm,
... context="AI helps with various tasks including analysis and generation.",
... )
>>> # With custom threshold for stricter validation
>>> strict_guardrail = HallucinationGuardrail(
... context="Quantum computing uses qubits in superposition.",
... llm=agent.llm,
... threshold=8.0, # Would require score >= 8 to pass in enterprise version
... threshold=8.0, # Require score >= 8 to pass
... )
>>> # With tool response for additional context
>>> guardrail_with_tools = HallucinationGuardrail(
... context="The current weather data",
... llm=agent.llm,
... tool_response="Weather API returned: Temperature 22°C, Humidity 65%",
... )
@@ -46,16 +51,17 @@ class HallucinationGuardrail:
def __init__(
self,
context: str,
llm: LLM,
context: str | None = None,
threshold: float | None = None,
tool_response: str = "",
):
"""Initialize the HallucinationGuardrail placeholder.
Args:
context: The reference context that outputs would be checked against.
llm: The language model that would be used for evaluation.
context: Optional reference context that outputs would be checked against.
If not provided, the task's expected_output will be used as context.
threshold: Optional minimum faithfulness score that would be required to pass.
tool_response: Optional tool response information that would be used in evaluation.
"""
@@ -78,16 +84,17 @@ class HallucinationGuardrail:
def __call__(self, task_output: TaskOutput) -> tuple[bool, Any]:
"""Validate a task output against hallucination criteria.
In the open source, this method always returns that the output is valid.
Args:
task_output: The output to be validated.
Returns:
A tuple containing:
- True
- The raw task output
- True if validation passed, False otherwise
- The raw task output if valid, or error feedback if invalid
"""
if callable(_validate_output_hook):
return _validate_output_hook(self, task_output)
self._logger.log(
"warning",
"Premium hallucination detection skipped (use for free at https://app.crewai.com)\n",

View File

@@ -42,6 +42,8 @@ if TYPE_CHECKING:
from crewai.llm import LLM
from crewai.task import Task
_create_plus_client_hook: Callable[[], Any] | None = None
class SummaryContent(TypedDict):
"""Structure for summary content entries.
@@ -91,7 +93,11 @@ def parse_tools(tools: list[BaseTool]) -> list[CrewStructuredTool]:
for tool in tools:
if isinstance(tool, CrewAITool):
tools_list.append(tool.to_structured_tool())
structured_tool = tool.to_structured_tool()
structured_tool.current_usage_count = 0
if structured_tool._original_tool:
structured_tool._original_tool.current_usage_count = 0
tools_list.append(structured_tool)
else:
raise ValueError("Tool is not a CrewStructuredTool or BaseTool")
@@ -818,10 +824,13 @@ def load_agent_from_repository(from_repository: str) -> dict[str, Any]:
if from_repository:
import importlib
from crewai.cli.authentication.token import get_auth_token
from crewai.cli.plus_api import PlusAPI
if callable(_create_plus_client_hook):
client = _create_plus_client_hook()
else:
from crewai.cli.authentication.token import get_auth_token
from crewai.cli.plus_api import PlusAPI
client = PlusAPI(api_key=get_auth_token())
client = PlusAPI(api_key=get_auth_token())
_print_current_organization()
response = client.get_agent(from_repository)
if response.status_code == 404: