mirror of
https://github.com/crewAIInc/crewAI.git
synced 2025-12-15 11:58:31 +00:00
Lorenze/ensure hooks work with lite agents flows (#3981)
* liteagent support hooks * wip llm.call hooks work - needs tests for this * fix tests * fixed more * more tool hooks test cassettes
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from crewai.events.event_listener import event_listener
|
||||
from crewai.hooks.types import AfterLLMCallHookType, BeforeLLMCallHookType
|
||||
@@ -9,17 +9,22 @@ from crewai.utilities.printer import Printer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agents.crew_agent_executor import CrewAgentExecutor
|
||||
from crewai.lite_agent import LiteAgent
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
class LLMCallHookContext:
|
||||
"""Context object passed to LLM call hooks with full executor access.
|
||||
"""Context object passed to LLM call hooks.
|
||||
|
||||
Provides hooks with complete access to the executor state, allowing
|
||||
Provides hooks with complete access to the execution state, allowing
|
||||
modification of messages, responses, and executor attributes.
|
||||
|
||||
Supports both executor-based calls (agents in crews/flows) and direct LLM calls.
|
||||
|
||||
Attributes:
|
||||
executor: Full reference to the CrewAgentExecutor instance
|
||||
messages: Direct reference to executor.messages (mutable list).
|
||||
executor: Reference to the executor (CrewAgentExecutor/LiteAgent) or None for direct calls
|
||||
messages: Direct reference to messages (mutable list).
|
||||
Can be modified in both before_llm_call and after_llm_call hooks.
|
||||
Modifications in after_llm_call hooks persist to the next iteration,
|
||||
allowing hooks to modify conversation history for subsequent LLM calls.
|
||||
@@ -27,33 +32,75 @@ class LLMCallHookContext:
|
||||
Do NOT replace the list (e.g., context.messages = []), as this will break
|
||||
the executor. Use context.messages.append() or context.messages.extend()
|
||||
instead of assignment.
|
||||
agent: Reference to the agent executing the task
|
||||
task: Reference to the task being executed
|
||||
crew: Reference to the crew instance
|
||||
agent: Reference to the agent executing the task (None for direct LLM calls)
|
||||
task: Reference to the task being executed (None for direct LLM calls or LiteAgent)
|
||||
crew: Reference to the crew instance (None for direct LLM calls or LiteAgent)
|
||||
llm: Reference to the LLM instance
|
||||
iterations: Current iteration count
|
||||
iterations: Current iteration count (0 for direct LLM calls)
|
||||
response: LLM response string (only set for after_llm_call hooks).
|
||||
Can be modified by returning a new string from after_llm_call hook.
|
||||
"""
|
||||
|
||||
executor: CrewAgentExecutor | LiteAgent | None
|
||||
messages: list[LLMMessage]
|
||||
agent: Any
|
||||
task: Any
|
||||
crew: Any
|
||||
llm: BaseLLM | None | str | Any
|
||||
iterations: int
|
||||
response: str | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
executor: CrewAgentExecutor,
|
||||
executor: CrewAgentExecutor | LiteAgent | None = None,
|
||||
response: str | None = None,
|
||||
messages: list[LLMMessage] | None = None,
|
||||
llm: BaseLLM | str | Any | None = None, # TODO: look into
|
||||
agent: Any | None = None,
|
||||
task: Any | None = None,
|
||||
crew: Any | None = None,
|
||||
) -> None:
|
||||
"""Initialize hook context with executor reference.
|
||||
"""Initialize hook context with executor reference or direct parameters.
|
||||
|
||||
Args:
|
||||
executor: The CrewAgentExecutor instance
|
||||
executor: The CrewAgentExecutor or LiteAgent instance (None for direct LLM calls)
|
||||
response: Optional response string (for after_llm_call hooks)
|
||||
messages: Optional messages list (for direct LLM calls when executor is None)
|
||||
llm: Optional LLM instance (for direct LLM calls when executor is None)
|
||||
agent: Optional agent reference (for direct LLM calls when executor is None)
|
||||
task: Optional task reference (for direct LLM calls when executor is None)
|
||||
crew: Optional crew reference (for direct LLM calls when executor is None)
|
||||
"""
|
||||
self.executor = executor
|
||||
self.messages = executor.messages
|
||||
self.agent = executor.agent
|
||||
self.task = executor.task
|
||||
self.crew = executor.crew
|
||||
self.llm = executor.llm
|
||||
self.iterations = executor.iterations
|
||||
if executor is not None:
|
||||
# Existing path: extract from executor
|
||||
self.executor = executor
|
||||
self.messages = executor.messages
|
||||
self.llm = executor.llm
|
||||
self.iterations = executor.iterations
|
||||
# Handle CrewAgentExecutor vs LiteAgent differences
|
||||
if hasattr(executor, "agent"):
|
||||
self.agent = executor.agent
|
||||
self.task = cast("CrewAgentExecutor", executor).task
|
||||
self.crew = cast("CrewAgentExecutor", executor).crew
|
||||
else:
|
||||
# LiteAgent case - is the agent itself, doesn't have task/crew
|
||||
self.agent = (
|
||||
executor.original_agent
|
||||
if hasattr(executor, "original_agent")
|
||||
else executor
|
||||
)
|
||||
self.task = None
|
||||
self.crew = None
|
||||
else:
|
||||
# New path: direct LLM call with explicit parameters
|
||||
self.executor = None
|
||||
self.messages = messages or []
|
||||
self.llm = llm
|
||||
self.agent = agent
|
||||
self.task = task
|
||||
self.crew = crew
|
||||
self.iterations = 0
|
||||
|
||||
self.response = response
|
||||
|
||||
def request_human_input(
|
||||
|
||||
@@ -38,6 +38,8 @@ from crewai.events.types.agent_events import (
|
||||
)
|
||||
from crewai.events.types.logging_events import AgentLogsExecutionEvent
|
||||
from crewai.flow.flow_trackable import FlowTrackable
|
||||
from crewai.hooks.llm_hooks import get_after_llm_call_hooks, get_before_llm_call_hooks
|
||||
from crewai.hooks.types import AfterLLMCallHookType, BeforeLLMCallHookType
|
||||
from crewai.lite_agent_output import LiteAgentOutput
|
||||
from crewai.llm import LLM
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
@@ -155,6 +157,12 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
_guardrail: GuardrailCallable | None = PrivateAttr(default=None)
|
||||
_guardrail_retry_count: int = PrivateAttr(default=0)
|
||||
_callbacks: list[TokenCalcHandler] = PrivateAttr(default_factory=list)
|
||||
_before_llm_call_hooks: list[BeforeLLMCallHookType] = PrivateAttr(
|
||||
default_factory=get_before_llm_call_hooks
|
||||
)
|
||||
_after_llm_call_hooks: list[AfterLLMCallHookType] = PrivateAttr(
|
||||
default_factory=get_after_llm_call_hooks
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def setup_llm(self) -> Self:
|
||||
@@ -246,6 +254,26 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
"""Return the original role for compatibility with tool interfaces."""
|
||||
return self.role
|
||||
|
||||
@property
|
||||
def before_llm_call_hooks(self) -> list[BeforeLLMCallHookType]:
|
||||
"""Get the before_llm_call hooks for this agent."""
|
||||
return self._before_llm_call_hooks
|
||||
|
||||
@property
|
||||
def after_llm_call_hooks(self) -> list[AfterLLMCallHookType]:
|
||||
"""Get the after_llm_call hooks for this agent."""
|
||||
return self._after_llm_call_hooks
|
||||
|
||||
@property
|
||||
def messages(self) -> list[LLMMessage]:
|
||||
"""Get the messages list for hook context compatibility."""
|
||||
return self._messages
|
||||
|
||||
@property
|
||||
def iterations(self) -> int:
|
||||
"""Get the current iteration count for hook context compatibility."""
|
||||
return self._iterations
|
||||
|
||||
def kickoff(
|
||||
self,
|
||||
messages: str | list[LLMMessage],
|
||||
@@ -504,7 +532,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
AgentFinish: The final result of the agent execution.
|
||||
"""
|
||||
# Execute the agent loop
|
||||
formatted_answer = None
|
||||
formatted_answer: AgentAction | AgentFinish | None = None
|
||||
while not isinstance(formatted_answer, AgentFinish):
|
||||
try:
|
||||
if has_reached_max_iterations(self._iterations, self.max_iterations):
|
||||
@@ -526,6 +554,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
callbacks=self._callbacks,
|
||||
printer=self._printer,
|
||||
from_agent=self,
|
||||
executor_context=self,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -1642,6 +1642,10 @@ class LLM(BaseLLM):
|
||||
if message.get("role") == "system":
|
||||
msg_role: Literal["assistant"] = "assistant"
|
||||
message["role"] = msg_role
|
||||
|
||||
if not self._invoke_before_llm_call_hooks(messages, from_agent):
|
||||
raise ValueError("LLM call blocked by before_llm_call hook")
|
||||
|
||||
# --- 5) Set up callbacks if provided
|
||||
with suppress_warnings():
|
||||
if callbacks and len(callbacks) > 0:
|
||||
@@ -1651,7 +1655,16 @@ class LLM(BaseLLM):
|
||||
params = self._prepare_completion_params(messages, tools)
|
||||
# --- 7) Make the completion call and handle response
|
||||
if self.stream:
|
||||
return self._handle_streaming_response(
|
||||
result = self._handle_streaming_response(
|
||||
params=params,
|
||||
callbacks=callbacks,
|
||||
available_functions=available_functions,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
response_model=response_model,
|
||||
)
|
||||
else:
|
||||
result = self._handle_non_streaming_response(
|
||||
params=params,
|
||||
callbacks=callbacks,
|
||||
available_functions=available_functions,
|
||||
@@ -1660,14 +1673,12 @@ class LLM(BaseLLM):
|
||||
response_model=response_model,
|
||||
)
|
||||
|
||||
return self._handle_non_streaming_response(
|
||||
params=params,
|
||||
callbacks=callbacks,
|
||||
available_functions=available_functions,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
response_model=response_model,
|
||||
)
|
||||
if isinstance(result, str):
|
||||
result = self._invoke_after_llm_call_hooks(
|
||||
messages, result, from_agent
|
||||
)
|
||||
|
||||
return result
|
||||
except LLMContextLengthExceededError:
|
||||
# Re-raise LLMContextLengthExceededError as it should be handled
|
||||
# by the CrewAgentExecutor._invoke_loop method, which can then decide
|
||||
|
||||
@@ -314,7 +314,7 @@ class BaseLLM(ABC):
|
||||
call_type: LLMCallType,
|
||||
from_task: Task | None = None,
|
||||
from_agent: Agent | None = None,
|
||||
messages: str | list[dict[str, Any]] | None = None,
|
||||
messages: str | list[LLMMessage] | None = None,
|
||||
) -> None:
|
||||
"""Emit LLM call completed event."""
|
||||
crewai_event_bus.emit(
|
||||
@@ -586,3 +586,134 @@ class BaseLLM(ABC):
|
||||
Dictionary with token usage totals
|
||||
"""
|
||||
return UsageMetrics(**self._token_usage)
|
||||
|
||||
def _invoke_before_llm_call_hooks(
|
||||
self,
|
||||
messages: list[LLMMessage],
|
||||
from_agent: Agent | None = None,
|
||||
) -> bool:
|
||||
"""Invoke before_llm_call hooks for direct LLM calls (no agent context).
|
||||
|
||||
This method should be called by native provider implementations before
|
||||
making the actual LLM call when from_agent is None (direct calls).
|
||||
|
||||
Args:
|
||||
messages: The messages being sent to the LLM
|
||||
from_agent: The agent making the call (None for direct calls)
|
||||
|
||||
Returns:
|
||||
True if LLM call should proceed, False if blocked by hook
|
||||
|
||||
Example:
|
||||
>>> # In a native provider's call() method:
|
||||
>>> if from_agent is None and not self._invoke_before_llm_call_hooks(
|
||||
... messages, from_agent
|
||||
... ):
|
||||
... raise ValueError("LLM call blocked by hook")
|
||||
"""
|
||||
# Only invoke hooks for direct calls (no agent context)
|
||||
if from_agent is not None:
|
||||
return True
|
||||
|
||||
from crewai.hooks.llm_hooks import (
|
||||
LLMCallHookContext,
|
||||
get_before_llm_call_hooks,
|
||||
)
|
||||
from crewai.utilities.printer import Printer
|
||||
|
||||
before_hooks = get_before_llm_call_hooks()
|
||||
if not before_hooks:
|
||||
return True
|
||||
|
||||
hook_context = LLMCallHookContext(
|
||||
executor=None,
|
||||
messages=messages,
|
||||
llm=self,
|
||||
agent=None,
|
||||
task=None,
|
||||
crew=None,
|
||||
)
|
||||
printer = Printer()
|
||||
|
||||
try:
|
||||
for hook in before_hooks:
|
||||
result = hook(hook_context)
|
||||
if result is False:
|
||||
printer.print(
|
||||
content="LLM call blocked by before_llm_call hook",
|
||||
color="yellow",
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
printer.print(
|
||||
content=f"Error in before_llm_call hook: {e}",
|
||||
color="yellow",
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def _invoke_after_llm_call_hooks(
|
||||
self,
|
||||
messages: list[LLMMessage],
|
||||
response: str,
|
||||
from_agent: Agent | None = None,
|
||||
) -> str:
|
||||
"""Invoke after_llm_call hooks for direct LLM calls (no agent context).
|
||||
|
||||
This method should be called by native provider implementations after
|
||||
receiving the LLM response when from_agent is None (direct calls).
|
||||
|
||||
Args:
|
||||
messages: The messages that were sent to the LLM
|
||||
response: The response from the LLM
|
||||
from_agent: The agent that made the call (None for direct calls)
|
||||
|
||||
Returns:
|
||||
The potentially modified response string
|
||||
|
||||
Example:
|
||||
>>> # In a native provider's call() method:
|
||||
>>> if from_agent is None and isinstance(result, str):
|
||||
... result = self._invoke_after_llm_call_hooks(
|
||||
... messages, result, from_agent
|
||||
... )
|
||||
"""
|
||||
# Only invoke hooks for direct calls (no agent context)
|
||||
if from_agent is not None or not isinstance(response, str):
|
||||
return response
|
||||
|
||||
from crewai.hooks.llm_hooks import (
|
||||
LLMCallHookContext,
|
||||
get_after_llm_call_hooks,
|
||||
)
|
||||
from crewai.utilities.printer import Printer
|
||||
|
||||
after_hooks = get_after_llm_call_hooks()
|
||||
if not after_hooks:
|
||||
return response
|
||||
|
||||
hook_context = LLMCallHookContext(
|
||||
executor=None,
|
||||
messages=messages,
|
||||
llm=self,
|
||||
agent=None,
|
||||
task=None,
|
||||
crew=None,
|
||||
response=response,
|
||||
)
|
||||
printer = Printer()
|
||||
modified_response = response
|
||||
|
||||
try:
|
||||
for hook in after_hooks:
|
||||
result = hook(hook_context)
|
||||
if result is not None and isinstance(result, str):
|
||||
modified_response = result
|
||||
hook_context.response = modified_response
|
||||
except Exception as e:
|
||||
printer.print(
|
||||
content=f"Error in after_llm_call hook: {e}",
|
||||
color="yellow",
|
||||
)
|
||||
|
||||
return modified_response
|
||||
|
||||
@@ -187,6 +187,9 @@ class AnthropicCompletion(BaseLLM):
|
||||
messages
|
||||
)
|
||||
|
||||
if not self._invoke_before_llm_call_hooks(formatted_messages, from_agent):
|
||||
raise ValueError("LLM call blocked by before_llm_call hook")
|
||||
|
||||
# Prepare completion parameters
|
||||
completion_params = self._prepare_completion_params(
|
||||
formatted_messages, system_message, tools
|
||||
@@ -494,7 +497,9 @@ class AnthropicCompletion(BaseLLM):
|
||||
if usage.get("total_tokens", 0) > 0:
|
||||
logging.info(f"Anthropic API usage: {usage}")
|
||||
|
||||
return content
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
params["messages"], content, from_agent
|
||||
)
|
||||
|
||||
def _handle_streaming_completion(
|
||||
self,
|
||||
@@ -588,7 +593,9 @@ class AnthropicCompletion(BaseLLM):
|
||||
messages=params["messages"],
|
||||
)
|
||||
|
||||
return full_response
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
params["messages"], full_response, from_agent
|
||||
)
|
||||
|
||||
def _handle_tool_use_conversation(
|
||||
self,
|
||||
|
||||
@@ -216,6 +216,9 @@ class AzureCompletion(BaseLLM):
|
||||
# Format messages for Azure
|
||||
formatted_messages = self._format_messages_for_azure(messages)
|
||||
|
||||
if not self._invoke_before_llm_call_hooks(formatted_messages, from_agent):
|
||||
raise ValueError("LLM call blocked by before_llm_call hook")
|
||||
|
||||
# Prepare completion parameters
|
||||
completion_params = self._prepare_completion_params(
|
||||
formatted_messages, tools, response_model
|
||||
@@ -550,6 +553,10 @@ class AzureCompletion(BaseLLM):
|
||||
messages=params["messages"],
|
||||
)
|
||||
|
||||
content = self._invoke_after_llm_call_hooks(
|
||||
params["messages"], content, from_agent
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
if is_context_length_exceeded(e):
|
||||
logging.error(f"Context window exceeded: {e}")
|
||||
@@ -642,7 +649,9 @@ class AzureCompletion(BaseLLM):
|
||||
messages=params["messages"],
|
||||
)
|
||||
|
||||
return full_response
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
params["messages"], full_response, from_agent
|
||||
)
|
||||
|
||||
async def _ahandle_completion(
|
||||
self,
|
||||
|
||||
@@ -312,9 +312,14 @@ class BedrockCompletion(BaseLLM):
|
||||
|
||||
# Format messages for Converse API
|
||||
formatted_messages, system_message = self._format_messages_for_converse(
|
||||
messages # type: ignore[arg-type]
|
||||
messages
|
||||
)
|
||||
|
||||
if not self._invoke_before_llm_call_hooks(
|
||||
cast(list[LLMMessage], formatted_messages), from_agent
|
||||
):
|
||||
raise ValueError("LLM call blocked by before_llm_call hook")
|
||||
|
||||
# Prepare request body
|
||||
body: BedrockConverseRequestBody = {
|
||||
"inferenceConfig": self._get_inference_config(),
|
||||
@@ -356,11 +361,19 @@ class BedrockCompletion(BaseLLM):
|
||||
|
||||
if self.stream:
|
||||
return self._handle_streaming_converse(
|
||||
formatted_messages, body, available_functions, from_task, from_agent
|
||||
cast(list[LLMMessage], formatted_messages),
|
||||
body,
|
||||
available_functions,
|
||||
from_task,
|
||||
from_agent,
|
||||
)
|
||||
|
||||
return self._handle_converse(
|
||||
formatted_messages, body, available_functions, from_task, from_agent
|
||||
cast(list[LLMMessage], formatted_messages),
|
||||
body,
|
||||
available_functions,
|
||||
from_task,
|
||||
from_agent,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -481,7 +494,7 @@ class BedrockCompletion(BaseLLM):
|
||||
|
||||
def _handle_converse(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
messages: list[LLMMessage],
|
||||
body: BedrockConverseRequestBody,
|
||||
available_functions: Mapping[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
@@ -605,7 +618,11 @@ class BedrockCompletion(BaseLLM):
|
||||
messages=messages,
|
||||
)
|
||||
|
||||
return text_content
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
messages,
|
||||
text_content,
|
||||
from_agent,
|
||||
)
|
||||
|
||||
except ClientError as e:
|
||||
# Handle all AWS ClientError exceptions as per documentation
|
||||
@@ -662,7 +679,7 @@ class BedrockCompletion(BaseLLM):
|
||||
|
||||
def _handle_streaming_converse(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
messages: list[LLMMessage],
|
||||
body: BedrockConverseRequestBody,
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
@@ -1149,16 +1166,25 @@ class BedrockCompletion(BaseLLM):
|
||||
messages=messages,
|
||||
)
|
||||
|
||||
return full_response
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
messages,
|
||||
full_response,
|
||||
from_agent,
|
||||
)
|
||||
|
||||
def _format_messages_for_converse(
|
||||
self, messages: str | list[dict[str, str]]
|
||||
self, messages: str | list[LLMMessage]
|
||||
) -> tuple[list[dict[str, Any]], str | None]:
|
||||
"""Format messages for Converse API following AWS documentation."""
|
||||
# Use base class formatting first
|
||||
formatted_messages = self._format_messages(messages) # type: ignore[arg-type]
|
||||
"""Format messages for Converse API following AWS documentation.
|
||||
|
||||
converse_messages = []
|
||||
Note: Returns dict[str, Any] instead of LLMMessage because Bedrock uses
|
||||
a different content structure: {"role": str, "content": [{"text": str}]}
|
||||
rather than the standard {"role": str, "content": str}.
|
||||
"""
|
||||
# Use base class formatting first
|
||||
formatted_messages = self._format_messages(messages)
|
||||
|
||||
converse_messages: list[dict[str, Any]] = []
|
||||
system_message: str | None = None
|
||||
|
||||
for message in formatted_messages:
|
||||
|
||||
@@ -246,6 +246,11 @@ class GeminiCompletion(BaseLLM):
|
||||
messages
|
||||
)
|
||||
|
||||
messages_for_hooks = self._convert_contents_to_dict(formatted_content)
|
||||
|
||||
if not self._invoke_before_llm_call_hooks(messages_for_hooks, from_agent):
|
||||
raise ValueError("LLM call blocked by before_llm_call hook")
|
||||
|
||||
config = self._prepare_generation_config(
|
||||
system_instruction, tools, response_model
|
||||
)
|
||||
@@ -559,7 +564,9 @@ class GeminiCompletion(BaseLLM):
|
||||
messages=messages_for_event,
|
||||
)
|
||||
|
||||
return content
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
messages_for_event, content, from_agent
|
||||
)
|
||||
|
||||
def _handle_streaming_completion(
|
||||
self,
|
||||
@@ -639,7 +646,9 @@ class GeminiCompletion(BaseLLM):
|
||||
messages=messages_for_event,
|
||||
)
|
||||
|
||||
return full_response
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
messages_for_event, full_response, from_agent
|
||||
)
|
||||
|
||||
async def _ahandle_completion(
|
||||
self,
|
||||
@@ -787,7 +796,159 @@ class GeminiCompletion(BaseLLM):
|
||||
messages=messages_for_event,
|
||||
)
|
||||
|
||||
return full_response
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
messages_for_event, full_response, from_agent
|
||||
)
|
||||
|
||||
async def _ahandle_completion(
|
||||
self,
|
||||
contents: list[types.Content],
|
||||
system_instruction: str | None,
|
||||
config: types.GenerateContentConfig,
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
) -> str | Any:
|
||||
"""Handle async non-streaming content generation."""
|
||||
try:
|
||||
# The API accepts list[Content] but mypy is overly strict about variance
|
||||
contents_for_api: Any = contents
|
||||
response = await self.client.aio.models.generate_content(
|
||||
model=self.model,
|
||||
contents=contents_for_api,
|
||||
config=config,
|
||||
)
|
||||
|
||||
usage = self._extract_token_usage(response)
|
||||
except Exception as e:
|
||||
if is_context_length_exceeded(e):
|
||||
logging.error(f"Context window exceeded: {e}")
|
||||
raise LLMContextLengthExceededError(str(e)) from e
|
||||
raise e from e
|
||||
|
||||
self._track_token_usage_internal(usage)
|
||||
|
||||
if response.candidates and (self.tools or available_functions):
|
||||
candidate = response.candidates[0]
|
||||
if candidate.content and candidate.content.parts:
|
||||
for part in candidate.content.parts:
|
||||
if hasattr(part, "function_call") and part.function_call:
|
||||
function_name = part.function_call.name
|
||||
if function_name is None:
|
||||
continue
|
||||
function_args = (
|
||||
dict(part.function_call.args)
|
||||
if part.function_call.args
|
||||
else {}
|
||||
)
|
||||
|
||||
result = self._handle_tool_execution(
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
available_functions=available_functions or {},
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
content = response.text or ""
|
||||
content = self._apply_stop_words(content)
|
||||
|
||||
messages_for_event = self._convert_contents_to_dict(contents)
|
||||
|
||||
self._emit_call_completed_event(
|
||||
response=content,
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages_for_event,
|
||||
)
|
||||
|
||||
return content
|
||||
|
||||
async def _ahandle_streaming_completion(
|
||||
self,
|
||||
contents: list[types.Content],
|
||||
config: types.GenerateContentConfig,
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
) -> str:
|
||||
"""Handle async streaming content generation."""
|
||||
full_response = ""
|
||||
function_calls: dict[str, dict[str, Any]] = {}
|
||||
|
||||
# The API accepts list[Content] but mypy is overly strict about variance
|
||||
contents_for_api: Any = contents
|
||||
stream = await self.client.aio.models.generate_content_stream(
|
||||
model=self.model,
|
||||
contents=contents_for_api,
|
||||
config=config,
|
||||
)
|
||||
async for chunk in stream:
|
||||
if chunk.text:
|
||||
full_response += chunk.text
|
||||
self._emit_stream_chunk_event(
|
||||
chunk=chunk.text,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
)
|
||||
|
||||
if chunk.candidates:
|
||||
candidate = chunk.candidates[0]
|
||||
if candidate.content and candidate.content.parts:
|
||||
for part in candidate.content.parts:
|
||||
if hasattr(part, "function_call") and part.function_call:
|
||||
call_id = part.function_call.name or "default"
|
||||
if call_id not in function_calls:
|
||||
function_calls[call_id] = {
|
||||
"name": part.function_call.name,
|
||||
"args": dict(part.function_call.args)
|
||||
if part.function_call.args
|
||||
else {},
|
||||
}
|
||||
|
||||
if function_calls and available_functions:
|
||||
for call_data in function_calls.values():
|
||||
function_name = call_data["name"]
|
||||
function_args = call_data["args"]
|
||||
|
||||
# Skip if function_name is None
|
||||
if not isinstance(function_name, str):
|
||||
continue
|
||||
|
||||
# Ensure function_args is a dict
|
||||
if not isinstance(function_args, dict):
|
||||
function_args = {}
|
||||
|
||||
result = self._handle_tool_execution(
|
||||
function_name=function_name,
|
||||
function_args=function_args,
|
||||
available_functions=available_functions,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
messages_for_event = self._convert_contents_to_dict(contents)
|
||||
|
||||
self._emit_call_completed_event(
|
||||
response=full_response,
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=messages_for_event,
|
||||
)
|
||||
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
messages_for_event, full_response, from_agent
|
||||
)
|
||||
|
||||
def supports_function_calling(self) -> bool:
|
||||
"""Check if the model supports function calling."""
|
||||
@@ -851,7 +1012,7 @@ class GeminiCompletion(BaseLLM):
|
||||
def _convert_contents_to_dict(
|
||||
self,
|
||||
contents: list[types.Content],
|
||||
) -> list[dict[str, str]]:
|
||||
) -> list[LLMMessage]:
|
||||
"""Convert contents to dict format."""
|
||||
result: list[dict[str, str]] = []
|
||||
for content_obj in contents:
|
||||
|
||||
@@ -190,6 +190,9 @@ class OpenAICompletion(BaseLLM):
|
||||
|
||||
formatted_messages = self._format_messages(messages)
|
||||
|
||||
if not self._invoke_before_llm_call_hooks(formatted_messages, from_agent):
|
||||
raise ValueError("LLM call blocked by before_llm_call hook")
|
||||
|
||||
completion_params = self._prepare_completion_params(
|
||||
messages=formatted_messages, tools=tools
|
||||
)
|
||||
@@ -474,6 +477,10 @@ class OpenAICompletion(BaseLLM):
|
||||
|
||||
if usage.get("total_tokens", 0) > 0:
|
||||
logging.info(f"OpenAI API usage: {usage}")
|
||||
|
||||
content = self._invoke_after_llm_call_hooks(
|
||||
params["messages"], content, from_agent
|
||||
)
|
||||
except NotFoundError as e:
|
||||
error_msg = f"Model {self.model} not found: {e}"
|
||||
logging.error(error_msg)
|
||||
@@ -629,7 +636,9 @@ class OpenAICompletion(BaseLLM):
|
||||
messages=params["messages"],
|
||||
)
|
||||
|
||||
return full_response
|
||||
return self._invoke_after_llm_call_hooks(
|
||||
params["messages"], full_response, from_agent
|
||||
)
|
||||
|
||||
async def _ahandle_completion(
|
||||
self,
|
||||
|
||||
@@ -237,7 +237,7 @@ def get_llm_response(
|
||||
from_task: Task | None = None,
|
||||
from_agent: Agent | LiteAgent | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
executor_context: CrewAgentExecutor | None = None,
|
||||
executor_context: CrewAgentExecutor | LiteAgent | None = None,
|
||||
) -> str:
|
||||
"""Call the LLM and return the response, handling any invalid responses.
|
||||
|
||||
@@ -727,7 +727,7 @@ def load_agent_from_repository(from_repository: str) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _setup_before_llm_call_hooks(
|
||||
executor_context: CrewAgentExecutor | None, printer: Printer
|
||||
executor_context: CrewAgentExecutor | LiteAgent | None, printer: Printer
|
||||
) -> bool:
|
||||
"""Setup and invoke before_llm_call hooks for the executor context.
|
||||
|
||||
@@ -777,7 +777,7 @@ def _setup_before_llm_call_hooks(
|
||||
|
||||
|
||||
def _setup_after_llm_call_hooks(
|
||||
executor_context: CrewAgentExecutor | None,
|
||||
executor_context: CrewAgentExecutor | LiteAgent | None,
|
||||
answer: str,
|
||||
printer: Printer,
|
||||
) -> str:
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages":[{"role":"user","content":"Say hello"}],"model":"gpt-4o-mini"}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate, zstd
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '74'
|
||||
content-type:
|
||||
- application/json
|
||||
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.13.3
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFJNb9QwEL3nVww+b9Am7Ee7FyT2wCIQ0CJxqarItSdZg+Ox7AmwVPvf
|
||||
KyftJv1A4uLDvHnP783MbQYgjBYbEGovWbXe5tvm6/bv5ZeDu5AmlubTzr///G778fKi+O6/iVli
|
||||
0M0PVPzAeq2o9RbZkBtgFVAyJtVivVqUZbku3vRASxptojWe8wXlrXEmL+flIp+v8+Lsnr0nozCK
|
||||
DVxlAAC3/Zt8Oo1/xAbms4dKizHKBsXm1AQgAtlUETJGE1k6FrMRVOQYXW99h9bSK9jRb1DSwQcY
|
||||
CHCgDpi0PLydEgPWXZTJvOusnQDSOWKZwveWr++R48mkpcYHuolPqKI2zsR9FVBGcslQZPKiR48Z
|
||||
wHU/jO5RPuEDtZ4rpp/Yf3c+qIlxA88xJpZ2LBdnsxe0Ko0sjY2TUQol1R71yBznLjttaAJkk8TP
|
||||
vbykPaQ2rvkf+RFQCj2jrnxAbdTjvGNbwHSe/2o7Tbg3LCKGX0ZhxQZD2oLGWnZ2OBoRD5GxrWrj
|
||||
Ggw+mOFyal8tV3NZr3C5PBfZMbsDAAD//wMARXm1qUcDAAA=
|
||||
headers:
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Thu, 27 Nov 2025 05:51:54 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -0,0 +1,87 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages":[{"role":"system","content":"You are Test Assistant. You are
|
||||
a helpful test assistant\nYour personal goal is: Answer questions briefly\n\nTo
|
||||
give my best complete final answer to the task respond using the exact following
|
||||
format:\n\nThought: I now can give a great answer\nFinal Answer: Your final
|
||||
answer must be the great and the most complete as possible, it must be outcome
|
||||
described.\n\nI MUST use these formats, my job depends on it!"},{"role":"user","content":"Say
|
||||
''Hello World'' and nothing else"}],"model":"gpt-4.1-mini"}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate, zstd
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '540'
|
||||
content-type:
|
||||
- application/json
|
||||
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.13.3
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFLLbtswELzrKxY8W4HlSjasW5Gibfo6FU1fgUCTK4kuxSVIKm4a+N8L
|
||||
So6ltCnQiwDt7Axndvc+AWBKshKYaHkQndXpZXNVv3vz6cWv68/b/XtX0OHuw9v9l92r/uslZ4vI
|
||||
oN0eRXhgXQjqrMagyIywcMgDRtVss86z7aZYPxuAjiTqSGtsSPOLLO2UUelquSrSZZ5m+YnekhLo
|
||||
WQnfEgCA++EbjRqJP1kJy8VDpUPveYOsPDcBMEc6Vhj3XvnATWCLCRRkAprB+8eW+qYNJVyBoQMI
|
||||
bqBRtwgcmhgAuPEHdN/NS2W4hufDXwmvUWuCa3JaznUd1r3nMZzptZ4B3BgKPA5nSHRzQo7nDJoa
|
||||
62jn/6CyWhnl28oh92SiXx/IsgE9JgA3w6z6R/GZddTZUAX6gcNz2XI16rFpRzO0OIGBAtezerZZ
|
||||
PKFXSQxcaT+bNhNctCgn6rQa3ktFMyCZpf7bzVPaY3Jlmv+RnwAh0AaUlXUolXiceGpzGE/4X23n
|
||||
KQ+GmUd3qwRWQaGLm5BY816Pd8X8nQ/YVbUyDTrr1Hhcta22m/Uai3y7W7HkmPwGAAD//wMABY90
|
||||
7msDAAA=
|
||||
headers:
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 26 Nov 2025 22:52:43 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -0,0 +1,108 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages":[{"role":"user","content":"Say hello"}],"model":"gpt-4o-mini"}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '74'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.openai.com
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 1.83.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFKxbtswEN31FVfOViEbRix76RAUcIaiQzu1CASaPMlMSR5Bnhobgf+9
|
||||
oJRYSpMCXTjcu/f43t09FQDCaLEDoY6SlQu2vDXu+5f2HNeeTubuVDl3Wu6/Pn7ufuy/HcQiM+jw
|
||||
gIpfWB8VuWCRDfkRVhElY1Zdbm7Wm7rebG8GwJFGm2ld4HJNpTPelKtqtS6rTbmsn9lHMgqT2MHP
|
||||
AgDgaXizT6/xJHZQLV4qDlOSHYrdtQlARLK5ImRKJrH0LBYTqMgz+sH6Hq2lD7CnR1DSwx2MBDhT
|
||||
D0xanj/NiRHbPsls3vfWzgDpPbHM4QfL98/I5WrSUhciHdJfVNEab9KxiSgT+WwoMQUxoJcC4H4Y
|
||||
Rv8qnwiRXOCG6RcO321HNTFt4C3GxNJO5WW9eEer0cjS2DQbpVBSHVFPzGnusteGZkAxS/zWy3va
|
||||
Y2rju/+RnwClMDDqJkTURr3OO7VFzOf5r7brhAfDImH8bRQ2bDDmLWhsZW/HoxHpnBhd0xrfYQzR
|
||||
jJfThgaVrBTWq20tikvxBwAA//8DAFoGAGtHAwAA
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 03 Dec 2025 19:06:36 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- SET-COOKIE-XXX
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
access-control-expose-headers:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '371'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '387'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -0,0 +1,115 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages":[{"role":"system","content":"You are Test Assistant. You are
|
||||
a helpful test assistant\nYour personal goal is: Answer questions briefly\n\nTo
|
||||
give my best complete final answer to the task respond using the exact following
|
||||
format:\n\nThought: I now can give a great answer\nFinal Answer: Your final
|
||||
answer must be the great and the most complete as possible, it must be outcome
|
||||
described.\n\nI MUST use these formats, my job depends on it!"},{"role":"user","content":"Say
|
||||
''Hello World'' and nothing else"}],"model":"gpt-4.1-mini"}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '540'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.openai.com
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 1.83.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFLBbtQwEL3nK0Y+b6okZJuSW4VAFIq4rFQkqCLXmSQDjseynRZU7b8j
|
||||
Z7ebFIrEJVLmzXt+b2YeEwBBrahBqEEGNVqdvqFxd331hW+2hD5scyrd5asPxe7j9edPb8UmMvju
|
||||
O6rwxDpTPFqNgdgcYOVQBoyqeXVeVhcX1evtDIzcoo603oa0PMvTkQylRVZs06xM8/JIH5gUelHD
|
||||
1wQA4HH+RqOmxZ+ihmzzVBnRe9mjqE9NAMKxjhUhvScfpAlis4CKTUAze98NPPVDqOEKDD+AkgZ6
|
||||
ukeQ0McAII1/QPfNvCMjNVzOfzW8R60Zbtjpdq3rsJu8jOHMpPUKkMZwkHE4c6LbI7I/ZdDcW8d3
|
||||
/g+q6MiQHxqH0rOJfn1gK2Z0nwDczrOansUX1vFoQxP4B87P5Vlx0BPLjlbo9ggGDlKv6nm1eUGv
|
||||
aTFI0n41baGkGrBdqMtq5NQSr4BklfpvNy9pH5KT6f9HfgGUQhuwbazDltTzxEubw3jC/2o7TXk2
|
||||
LDy6e1LYBEIXN9FiJyd9uCvhf/mAY9OR6dFZR4fj6mxTlFWeqarLzkWyT34DAAD//wMAP95PRmsD
|
||||
AAA=
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 03 Dec 2025 19:06:36 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- SET-COOKIE-XXX
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
access-control-expose-headers:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '620'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '1891'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -0,0 +1,486 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages":[{"role":"system","content":"You are Calculator Assistant.
|
||||
You are a helpful calculator assistant\nYour personal goal is: Help with math
|
||||
calculations\n\nYou ONLY have access to the following tools, and should NEVER
|
||||
make up tools that are not listed here:\n\nTool Name: calculate_sum\nTool Arguments:
|
||||
{''a'': {''description'': None, ''type'': ''int''}, ''b'': {''description'':
|
||||
None, ''type'': ''int''}}\nTool Description: Add two numbers together.\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 [calculate_sum],
|
||||
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":"What
|
||||
is 5 + 3? Use the calculate_sum tool."}],"model":"gpt-4.1-mini"}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '1119'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.openai.com
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 1.83.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFPLbtswELzrKxY8W0Zsy3ajWxv0EbSXFm0CtAokmlxLTCiSIFdFWsP/
|
||||
XlB+SO4D6IUQdnZGs7PkLgFgSrIcmGg4idbp9EY9hbvZsrn7cP/l0/uv77JXzx9fv31UN2/u3U82
|
||||
iQy7eURBJ9ZU2NZpJGXNARYeOWFUna1X2frFfLVa90BrJepIqx2l2XSWtsqodH41X6ZXWTrLjvTG
|
||||
KoGB5fAtAQDY9Wc0aiQ+sxyuJqdKiyHwGll+bgJg3upYYTwEFYgbYpMBFNYQmt57VVWF+dzYrm4o
|
||||
h1sIje20hC4gUIMguBad5oRl6FogazWQBS4lLIEbCYtpYV6KOHV+2Xsqw61xHeWwKxgvWA7LCRRs
|
||||
E78W+8JUVTX25XHbBR7DMZ3WI4AbY4lHvT6RhyOyP2egbe283YTfqGyrjApN6ZEHa+K8gaxjPbpP
|
||||
AB76rLuL+JjztnVUkn3C/nfzRXbQY8OOBzQ7LoKRJa5HrPWJdaFXSiSudBhtiwkuGpQDdVgt76Sy
|
||||
IyAZTf2nm79pHyZXpv4f+QEQAh2hLJ1HqcTlxEObx/gE/tV2Trk3zAL670pgSQp93ITELe/04V6y
|
||||
8CMQtuVWmRq98+pwObeuvF6vVrjMrjdzluyTXwAAAP//AwAHWkpkqwMAAA==
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 03 Dec 2025 17:24:28 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- SET-COOKIE-XXX
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
access-control-expose-headers:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '681'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '871'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"messages":[{"role":"system","content":"You are Calculator Assistant.
|
||||
You are a helpful calculator assistant\nYour personal goal is: Help with math
|
||||
calculations\n\nYou ONLY have access to the following tools, and should NEVER
|
||||
make up tools that are not listed here:\n\nTool Name: calculate_sum\nTool Arguments:
|
||||
{''a'': {''description'': None, ''type'': ''int''}, ''b'': {''description'':
|
||||
None, ''type'': ''int''}}\nTool Description: Add two numbers together.\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 [calculate_sum],
|
||||
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":"What
|
||||
is 5 + 3? Use the calculate_sum tool."},{"role":"assistant","content":"```\nThought:
|
||||
I should use the calculate_sum tool to add 5 and 3.\nAction: calculate_sum\nAction
|
||||
Input: {\"a\": 5, \"b\": 3}\n```\nObservation: 8"}],"model":"gpt-4.1-mini"}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '1298'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- COOKIE-XXX
|
||||
host:
|
||||
- api.openai.com
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 1.83.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jFJNb5wwEL3zKyyfl2ghBAi3KtJKTS9VlX5I3QiMGcBZY7v20DSK9r9X
|
||||
hs1C2lTqxdLMm/c8b2aeA0KoaGhBKO8Z8sHI8EYc3Ff7pX663Y32w82Qffv46XNUR+Yu+7GjG8/Q
|
||||
9QNwfGFdcD0YCSi0mmFugSF41ShLkyyP0zSfgEE3ID2tMxgmF1E4CCXCeBtfhdskjJITvdeCg6MF
|
||||
+R4QQsjz9PpGVQO/aEG2m5fMAM6xDmhxLiKEWi19hjLnhEOmkG4WkGuFoKbeq6raq7tej12PBXlP
|
||||
lH4kB/9gD6QViknClHsEu1e7KXo3RQXJ96qqqrWqhXZ0zFtTo5QrgCmlkfnRTH7uT8jx7EDqzlhd
|
||||
uz+otBVKuL60wJxWvluH2tAJPQaE3E+TGl+Zp8bqwWCJ+gDTd3F+OevRZUMLGuUnEDUyueQvt9Hm
|
||||
Db2yAWRCutWsKWe8h2ahLothYyP0CghWrv/u5i3t2blQ3f/ILwDnYBCa0lhoBH/teCmz4A/4X2Xn
|
||||
KU8NUwf2p+BQogDrN9FAy0Y5XxV1Tw5hKFuhOrDGivm0WlNeZ2kKV8l1HdPgGPwGAAD//wMAJksH
|
||||
jGkDAAA=
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 03 Dec 2025 17:24:29 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
access-control-expose-headers:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '427'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '442'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"messages":[{"role":"system","content":"You are Calculator Assistant.
|
||||
You are a helpful calculator assistant\nYour personal goal is: Help with math
|
||||
calculations\n\nYou ONLY have access to the following tools, and should NEVER
|
||||
make up tools that are not listed here:\n\nTool Name: calculate_sum\nTool Arguments:
|
||||
{''a'': {''description'': None, ''type'': ''int''}, ''b'': {''description'':
|
||||
None, ''type'': ''int''}}\nTool Description: Add two numbers together.\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 [calculate_sum],
|
||||
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":"What
|
||||
is 5 + 3? Use the calculate_sum tool."}],"model":"gpt-4.1-mini"}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '1119'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.openai.com
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 1.83.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jJLLbtswEEX3+ooB15bhh2zH2iXtJovuYqAPBRJNjiU6FEmQo7ap4X8v
|
||||
JDmW3KZAN1rMmXs1c4enCIApyVJgouIkaqfjD6r+fFf9+KJ2u6/qeO92H1/mrw+ffj25h+OWTVqF
|
||||
3R9R0JtqKmztNJKypsfCIydsXeebdbK5286Wsw7UVqJuZaWjOJnO41oZFS9mi1U8S+J5cpFXVgkM
|
||||
LIVvEQDAqfu2gxqJP1kKnVlXqTEEXiJLr00AzFvdVhgPQQXihthkgMIaQtPNXhRFZp4q25QVpfAI
|
||||
BlECWeBSwgq4kbCEJihTAlUIgmvRaE6Yh6YGslZPM3Mv2q3TW/hWhkfjGkrhlDGesXQ1ydg+Y+ny
|
||||
nJmiKMZDeTw0gbfJmEbrEeDGWOKtWRfH84WcrwFoWzpv9+EPKTsoo0KVe+TBmnbZQNaxjp4jgOcu
|
||||
6OYmO+a8rR3lZF+w+91imfR+bDjwQJebCyRLXI9Um/nkHb9cInGlw+hUTHBRoRykw115I5UdgWi0
|
||||
9d/TvOfdb65M+T/2AxACHaHMnUepxO3GQ5vH9v3/q+2acjcwC+i/K4E5KfTtJSQeeKP7R8nCayCs
|
||||
84MyJXrnVf8yDy7fbtZrXCXb/YJF5+g3AAAA//8DANrSB6yoAwAA
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 03 Dec 2025 19:10:33 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- SET-COOKIE-XXX
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
access-control-expose-headers:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '2329'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '2349'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"messages":[{"role":"system","content":"You are Calculator Assistant.
|
||||
You are a helpful calculator assistant\nYour personal goal is: Help with math
|
||||
calculations\n\nYou ONLY have access to the following tools, and should NEVER
|
||||
make up tools that are not listed here:\n\nTool Name: calculate_sum\nTool Arguments:
|
||||
{''a'': {''description'': None, ''type'': ''int''}, ''b'': {''description'':
|
||||
None, ''type'': ''int''}}\nTool Description: Add two numbers together.\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 [calculate_sum],
|
||||
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":"What
|
||||
is 5 + 3? Use the calculate_sum tool."},{"role":"assistant","content":"```\nThought:
|
||||
I need to add 5 and 3 using the calculate_sum tool.\nAction: calculate_sum\nAction
|
||||
Input: {\"a\":5,\"b\":3}\n```\nObservation: 8"}],"model":"gpt-4.1-mini"}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '1295'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- COOKIE-XXX
|
||||
host:
|
||||
- api.openai.com
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 1.83.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
H4sIAAAAAAAAAwAAAP//jJJNb9QwEIbv+RWWz5tqN81+5QaVQHDi0AOIrRKvPUncOmNjTyhQ7X9H
|
||||
zn4kS0HiYsl+5h3POzMvCWNcK14wLltBsnMmvdPd57f+yzKovc/cR/z23ohfn+69yJX1fBYVdv8I
|
||||
ks6qG2k7Z4C0xSOWHgRBzLpYr/L1Zju/vR1AZxWYKGscpfnNIu006jSbZ8t0nqeL/CRvrZYQeMG+
|
||||
Jowx9jKcsVBU8IMXbD47v3QQgmiAF5cgxri3Jr5wEYIOJJD4bITSIgEOtVdVtcP71vZNSwX7wNA+
|
||||
s6d4UAus1igMExiewe/w3XB7M9wKttlhVVXTrB7qPohoDXtjJkAgWhKxNYOfhxM5XBwY2zhv9+EP
|
||||
Ka816tCWHkSwGKsNZB0f6CFh7GHoVH9lnjtvO0cl2ScYvss2p07xcUIjXWxOkCwJM1Ftz+AqX6mA
|
||||
hDZh0msuhWxBjdJxMKJX2k5AMnH9upq/5T4619j8T/oRSAmOQJXOg9Ly2vEY5iEu8L/CLl0eCuYB
|
||||
/HctoSQNPk5CQS16c9wqHn4Ggq6sNTbgndfH1apduV2vVrDMt/uMJ4fkNwAAAP//AwCRC7shaQMA
|
||||
AA==
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Encoding:
|
||||
- gzip
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 03 Dec 2025 19:10:35 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
access-control-expose-headers:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '1647'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '1694'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -309,3 +309,188 @@ class TestLLMHooksIntegration:
|
||||
clear_all_llm_call_hooks()
|
||||
hooks = get_before_llm_call_hooks()
|
||||
assert len(hooks) == 0
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_lite_agent_hooks_integration_with_real_llm(self):
|
||||
"""Test that LiteAgent executes before/after LLM call hooks and prints messages correctly."""
|
||||
import os
|
||||
from crewai.lite_agent import LiteAgent
|
||||
|
||||
# Skip if no API key available
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
pytest.skip("OPENAI_API_KEY not set - skipping real LLM test")
|
||||
|
||||
# Track hook invocations
|
||||
hook_calls = {"before": [], "after": []}
|
||||
|
||||
def before_llm_call_hook(context: LLMCallHookContext) -> bool:
|
||||
"""Log and verify before hook execution."""
|
||||
print(f"\n[BEFORE HOOK] Agent: {context.agent.role if context.agent else 'None'}")
|
||||
print(f"[BEFORE HOOK] Iterations: {context.iterations}")
|
||||
print(f"[BEFORE HOOK] Message count: {len(context.messages)}")
|
||||
print(f"[BEFORE HOOK] Messages: {context.messages}")
|
||||
|
||||
# Track the call
|
||||
hook_calls["before"].append({
|
||||
"iterations": context.iterations,
|
||||
"message_count": len(context.messages),
|
||||
"has_task": context.task is not None,
|
||||
"has_crew": context.crew is not None,
|
||||
})
|
||||
|
||||
return True # Allow execution
|
||||
|
||||
def after_llm_call_hook(context: LLMCallHookContext) -> str | None:
|
||||
"""Log and verify after hook execution."""
|
||||
print(f"\n[AFTER HOOK] Agent: {context.agent.role if context.agent else 'None'}")
|
||||
print(f"[AFTER HOOK] Iterations: {context.iterations}")
|
||||
print(f"[AFTER HOOK] Response: {context.response[:100] if context.response else 'None'}...")
|
||||
print(f"[AFTER HOOK] Final message count: {len(context.messages)}")
|
||||
|
||||
# Track the call
|
||||
hook_calls["after"].append({
|
||||
"iterations": context.iterations,
|
||||
"has_response": context.response is not None,
|
||||
"response_length": len(context.response) if context.response else 0,
|
||||
})
|
||||
|
||||
# Optionally modify response
|
||||
if context.response:
|
||||
return f"[HOOKED] {context.response}"
|
||||
return None
|
||||
|
||||
# Register hooks
|
||||
register_before_llm_call_hook(before_llm_call_hook)
|
||||
register_after_llm_call_hook(after_llm_call_hook)
|
||||
|
||||
try:
|
||||
# Create LiteAgent
|
||||
lite_agent = LiteAgent(
|
||||
role="Test Assistant",
|
||||
goal="Answer questions briefly",
|
||||
backstory="You are a helpful test assistant",
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Verify hooks are loaded
|
||||
assert len(lite_agent.before_llm_call_hooks) > 0, "Before hooks not loaded"
|
||||
assert len(lite_agent.after_llm_call_hooks) > 0, "After hooks not loaded"
|
||||
|
||||
# Execute with a simple prompt
|
||||
result = lite_agent.kickoff("Say 'Hello World' and nothing else")
|
||||
|
||||
|
||||
# Verify hooks were called
|
||||
assert len(hook_calls["before"]) > 0, "Before hook was never called"
|
||||
assert len(hook_calls["after"]) > 0, "After hook was never called"
|
||||
|
||||
# Verify context had correct attributes for LiteAgent (used in flows)
|
||||
# LiteAgent doesn't have task/crew context, unlike agents in CrewBase
|
||||
before_call = hook_calls["before"][0]
|
||||
assert before_call["has_task"] is False, "Task should be None for LiteAgent in flows"
|
||||
assert before_call["has_crew"] is False, "Crew should be None for LiteAgent in flows"
|
||||
assert before_call["message_count"] > 0, "Should have messages"
|
||||
|
||||
# Verify after hook received response
|
||||
after_call = hook_calls["after"][0]
|
||||
assert after_call["has_response"] is True, "After hook should have response"
|
||||
assert after_call["response_length"] > 0, "Response should not be empty"
|
||||
|
||||
# Verify response was modified by after hook
|
||||
# Note: The hook modifies the raw LLM response, but LiteAgent then parses it
|
||||
# to extract the "Final Answer" portion. We check the messages to see the modification.
|
||||
assert len(result.messages) > 2, "Should have assistant message in messages"
|
||||
last_message = result.messages[-1]
|
||||
assert last_message["role"] == "assistant", "Last message should be from assistant"
|
||||
assert "[HOOKED]" in last_message["content"], "Hook should have modified the assistant message"
|
||||
|
||||
|
||||
finally:
|
||||
# Clean up hooks
|
||||
unregister_before_llm_call_hook(before_llm_call_hook)
|
||||
unregister_after_llm_call_hook(after_llm_call_hook)
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_direct_llm_call_hooks_integration(self):
|
||||
"""Test that hooks work for direct llm.call() without agents."""
|
||||
import os
|
||||
from crewai.llm import LLM
|
||||
|
||||
# Skip if no API key available
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
pytest.skip("OPENAI_API_KEY not set - skipping real LLM test")
|
||||
|
||||
# Track hook invocations
|
||||
hook_calls = {"before": [], "after": []}
|
||||
|
||||
def before_hook(context: LLMCallHookContext) -> bool:
|
||||
"""Log and verify before hook execution."""
|
||||
print(f"\n[BEFORE HOOK] Agent: {context.agent}")
|
||||
print(f"[BEFORE HOOK] Task: {context.task}")
|
||||
print(f"[BEFORE HOOK] Crew: {context.crew}")
|
||||
print(f"[BEFORE HOOK] LLM: {context.llm}")
|
||||
print(f"[BEFORE HOOK] Iterations: {context.iterations}")
|
||||
print(f"[BEFORE HOOK] Message count: {len(context.messages)}")
|
||||
|
||||
# Track the call
|
||||
hook_calls["before"].append({
|
||||
"agent": context.agent,
|
||||
"task": context.task,
|
||||
"crew": context.crew,
|
||||
"llm": context.llm is not None,
|
||||
"message_count": len(context.messages),
|
||||
})
|
||||
|
||||
return True # Allow execution
|
||||
|
||||
def after_hook(context: LLMCallHookContext) -> str | None:
|
||||
"""Log and verify after hook execution."""
|
||||
print(f"\n[AFTER HOOK] Agent: {context.agent}")
|
||||
print(f"[AFTER HOOK] Response: {context.response[:100] if context.response else 'None'}...")
|
||||
|
||||
# Track the call
|
||||
hook_calls["after"].append({
|
||||
"has_response": context.response is not None,
|
||||
"response_length": len(context.response) if context.response else 0,
|
||||
})
|
||||
|
||||
# Modify response
|
||||
if context.response:
|
||||
return f"[HOOKED] {context.response}"
|
||||
return None
|
||||
|
||||
# Register hooks
|
||||
register_before_llm_call_hook(before_hook)
|
||||
register_after_llm_call_hook(after_hook)
|
||||
|
||||
try:
|
||||
# Create LLM and make direct call
|
||||
llm = LLM(model="gpt-4o-mini")
|
||||
result = llm.call([{"role": "user", "content": "Say hello"}])
|
||||
|
||||
print(f"\n[TEST] Final result: {result}")
|
||||
|
||||
# Verify hooks were called
|
||||
assert len(hook_calls["before"]) > 0, "Before hook was never called"
|
||||
assert len(hook_calls["after"]) > 0, "After hook was never called"
|
||||
|
||||
# Verify context had correct attributes for direct LLM calls
|
||||
before_call = hook_calls["before"][0]
|
||||
assert before_call["agent"] is None, "Agent should be None for direct LLM calls"
|
||||
assert before_call["task"] is None, "Task should be None for direct LLM calls"
|
||||
assert before_call["crew"] is None, "Crew should be None for direct LLM calls"
|
||||
assert before_call["llm"] is True, "LLM should be present"
|
||||
assert before_call["message_count"] > 0, "Should have messages"
|
||||
|
||||
# Verify after hook received response
|
||||
after_call = hook_calls["after"][0]
|
||||
assert after_call["has_response"] is True, "After hook should have response"
|
||||
assert after_call["response_length"] > 0, "Response should not be empty"
|
||||
|
||||
# Verify response was modified by after hook
|
||||
assert "[HOOKED]" in result, "Response should be modified by after hook"
|
||||
|
||||
finally:
|
||||
# Clean up hooks
|
||||
unregister_before_llm_call_hook(before_hook)
|
||||
unregister_after_llm_call_hook(after_hook)
|
||||
|
||||
@@ -496,3 +496,97 @@ class TestToolHooksIntegration:
|
||||
clear_all_tool_call_hooks()
|
||||
hooks = get_before_tool_call_hooks()
|
||||
assert len(hooks) == 0
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_lite_agent_hooks_integration_with_real_tool(self):
|
||||
"""Test that LiteAgent executes before/after tool call hooks with real tool calls."""
|
||||
import os
|
||||
from crewai.lite_agent import LiteAgent
|
||||
from crewai.tools import tool
|
||||
|
||||
# Skip if no API key available
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
pytest.skip("OPENAI_API_KEY not set - skipping real tool test")
|
||||
|
||||
# Track hook invocations
|
||||
hook_calls = {"before": [], "after": []}
|
||||
|
||||
# Create a simple test tool
|
||||
@tool("calculate_sum")
|
||||
def calculate_sum(a: int, b: int) -> int:
|
||||
"""Add two numbers together."""
|
||||
return a + b
|
||||
|
||||
def before_tool_call_hook(context: ToolCallHookContext) -> bool:
|
||||
"""Log and verify before hook execution."""
|
||||
print(f"\n[BEFORE HOOK] Tool: {context.tool_name}")
|
||||
print(f"[BEFORE HOOK] Tool input: {context.tool_input}")
|
||||
print(f"[BEFORE HOOK] Agent: {context.agent.role if context.agent else 'None'}")
|
||||
print(f"[BEFORE HOOK] Task: {context.task}")
|
||||
print(f"[BEFORE HOOK] Crew: {context.crew}")
|
||||
|
||||
# Track the call
|
||||
hook_calls["before"].append({
|
||||
"tool_name": context.tool_name,
|
||||
"tool_input": context.tool_input,
|
||||
"has_agent": context.agent is not None,
|
||||
"has_task": context.task is not None,
|
||||
"has_crew": context.crew is not None,
|
||||
})
|
||||
|
||||
return True # Allow execution
|
||||
|
||||
def after_tool_call_hook(context: ToolCallHookContext) -> str | None:
|
||||
"""Log and verify after hook execution."""
|
||||
print(f"\n[AFTER HOOK] Tool: {context.tool_name}")
|
||||
print(f"[AFTER HOOK] Tool result: {context.tool_result}")
|
||||
print(f"[AFTER HOOK] Agent: {context.agent.role if context.agent else 'None'}")
|
||||
|
||||
# Track the call
|
||||
hook_calls["after"].append({
|
||||
"tool_name": context.tool_name,
|
||||
"tool_result": context.tool_result,
|
||||
"has_result": context.tool_result is not None,
|
||||
})
|
||||
|
||||
return None # Don't modify result
|
||||
|
||||
# Register hooks
|
||||
register_before_tool_call_hook(before_tool_call_hook)
|
||||
register_after_tool_call_hook(after_tool_call_hook)
|
||||
|
||||
try:
|
||||
# Create LiteAgent with the tool
|
||||
lite_agent = LiteAgent(
|
||||
role="Calculator Assistant",
|
||||
goal="Help with math calculations",
|
||||
backstory="You are a helpful calculator assistant",
|
||||
tools=[calculate_sum],
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Execute with a prompt that should trigger tool usage
|
||||
result = lite_agent.kickoff("What is 5 + 3? Use the calculate_sum tool.")
|
||||
|
||||
# Verify hooks were called
|
||||
assert len(hook_calls["before"]) > 0, "Before hook was never called"
|
||||
assert len(hook_calls["after"]) > 0, "After hook was never called"
|
||||
|
||||
# Verify context had correct attributes for LiteAgent (used in flows)
|
||||
# LiteAgent doesn't have task/crew context, unlike agents in CrewBase
|
||||
before_call = hook_calls["before"][0]
|
||||
assert before_call["tool_name"] == "calculate_sum", "Tool name should be 'calculate_sum'"
|
||||
assert "a" in before_call["tool_input"], "Tool input should have 'a' parameter"
|
||||
assert "b" in before_call["tool_input"], "Tool input should have 'b' parameter"
|
||||
|
||||
# Verify after hook received result
|
||||
after_call = hook_calls["after"][0]
|
||||
assert after_call["has_result"] is True, "After hook should have tool result"
|
||||
assert after_call["tool_name"] == "calculate_sum", "Tool name should match"
|
||||
# The result should contain the sum (8)
|
||||
assert "8" in str(after_call["tool_result"]), "Tool result should contain the sum"
|
||||
|
||||
finally:
|
||||
# Clean up hooks
|
||||
unregister_before_tool_call_hook(before_tool_call_hook)
|
||||
unregister_after_tool_call_hook(after_tool_call_hook)
|
||||
|
||||
Reference in New Issue
Block a user