mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-06 23:49:24 +00:00
Merge branch 'main' into feat/open-sandbox-tool
This commit is contained in:
@@ -8,7 +8,7 @@ authors = [
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"crewai-core==1.14.5a4",
|
||||
"crewai-core==1.14.5a5",
|
||||
"click~=8.1.7",
|
||||
"pydantic>=2.11.9,<2.13",
|
||||
"pydantic-settings~=2.10.1",
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.14.5a4"
|
||||
__version__ = "1.14.5a5"
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.14.5a4"
|
||||
__version__ = "1.14.5a5"
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.14.5a4"
|
||||
__version__ = "1.14.5a5"
|
||||
|
||||
@@ -10,7 +10,7 @@ requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests>=2.33.0,<3",
|
||||
"crewai==1.14.5a4",
|
||||
"crewai==1.14.5a5",
|
||||
"tiktoken>=0.8.0,<0.13",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
|
||||
@@ -338,4 +338,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.14.5a4"
|
||||
__version__ = "1.14.5a5"
|
||||
|
||||
@@ -5,8 +5,8 @@ from builtins import type as type_
|
||||
import logging
|
||||
import posixpath
|
||||
import shlex
|
||||
import uuid
|
||||
from typing import Any, Literal
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
@@ -33,7 +33,63 @@ FileAction = Literal[
|
||||
]
|
||||
|
||||
|
||||
def _daytona_file_schema_extra(schema: dict[str, Any]) -> None:
|
||||
schema["allOf"] = [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"append",
|
||||
"list",
|
||||
"delete",
|
||||
"mkdir",
|
||||
"info",
|
||||
"exists",
|
||||
"move",
|
||||
"find",
|
||||
"search",
|
||||
"chmod",
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {"required": ["path"]},
|
||||
},
|
||||
{
|
||||
"if": {"properties": {"action": {"const": "append"}}},
|
||||
"then": {"required": ["content"]},
|
||||
},
|
||||
{
|
||||
"if": {"properties": {"action": {"const": "move"}}},
|
||||
"then": {"required": ["destination"]},
|
||||
},
|
||||
{
|
||||
"if": {"properties": {"action": {"enum": ["find", "search"]}}},
|
||||
"then": {"required": ["pattern"]},
|
||||
},
|
||||
{
|
||||
"if": {"properties": {"action": {"const": "replace"}}},
|
||||
"then": {"required": ["paths", "pattern", "replacement"]},
|
||||
},
|
||||
{
|
||||
"if": {"properties": {"action": {"const": "chmod"}}},
|
||||
"then": {
|
||||
"anyOf": [
|
||||
{"required": ["mode"]},
|
||||
{"required": ["owner"]},
|
||||
{"required": ["group"]},
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class DaytonaFileToolSchema(BaseModel):
|
||||
model_config = {"json_schema_extra": _daytona_file_schema_extra}
|
||||
|
||||
action: FileAction = Field(
|
||||
...,
|
||||
description=(
|
||||
|
||||
@@ -7334,6 +7334,124 @@
|
||||
"daytona"
|
||||
],
|
||||
"run_params_schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"enum": [
|
||||
"read",
|
||||
"write",
|
||||
"append",
|
||||
"list",
|
||||
"delete",
|
||||
"mkdir",
|
||||
"info",
|
||||
"exists",
|
||||
"move",
|
||||
"find",
|
||||
"search",
|
||||
"chmod"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"const": "append"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"content"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"const": "move"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"destination"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"enum": [
|
||||
"find",
|
||||
"search"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"pattern"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"const": "replace"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"paths",
|
||||
"pattern",
|
||||
"replacement"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"action": {
|
||||
"const": "chmod"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"mode"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"owner"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"group"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"action": {
|
||||
"description": "The filesystem action to perform: 'read' (returns file contents); 'write' (create or replace a file with content); 'append' (append content to an existing file \u2014 use this for writing large files in chunks to avoid hitting tool-call size limits); 'list' (lists a directory); 'delete' (removes a file/dir); 'mkdir' (creates a directory); 'info' (returns file metadata); 'exists' (returns whether a path exists); 'move' (rename or relocate a file/dir; requires 'destination'); 'find' (grep file CONTENTS recursively; requires 'pattern'); 'search' (find files by NAME pattern; requires 'pattern'); 'chmod' (change permissions/owner/group; pass at least one of 'mode', 'owner', 'group'); 'replace' (find-and-replace text across files; requires 'paths', 'pattern', and 'replacement').",
|
||||
|
||||
@@ -8,8 +8,8 @@ authors = [
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"crewai-core==1.14.5a4",
|
||||
"crewai-cli==1.14.5a4",
|
||||
"crewai-core==1.14.5a5",
|
||||
"crewai-cli==1.14.5a5",
|
||||
# Core Dependencies
|
||||
"pydantic>=2.11.9,<2.13",
|
||||
"openai>=2.30.0,<3",
|
||||
@@ -54,7 +54,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.14.5a4",
|
||||
"crewai-tools==1.14.5a5",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken>=0.8.0,<0.13"
|
||||
@@ -105,7 +105,7 @@ a2a = [
|
||||
"aiocache[redis,memcached]~=0.12.3",
|
||||
]
|
||||
file-processing = [
|
||||
"crewai-files==1.14.5a4",
|
||||
"crewai-files==1.14.5a5",
|
||||
]
|
||||
qdrant-edge = [
|
||||
"qdrant-edge-py>=0.6.0",
|
||||
|
||||
@@ -48,7 +48,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.14.5a4"
|
||||
__version__ = "1.14.5a5"
|
||||
|
||||
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
||||
"Memory": ("crewai.memory.unified_memory", "Memory"),
|
||||
|
||||
@@ -7,6 +7,7 @@ from collections.abc import Callable, Coroutine, Sequence
|
||||
import concurrent.futures
|
||||
import contextvars
|
||||
from datetime import datetime
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -35,13 +36,11 @@ from typing_extensions import Self, TypeIs
|
||||
from crewai.agent.planning_config import PlanningConfig
|
||||
from crewai.agent.utils import (
|
||||
ahandle_knowledge_retrieval,
|
||||
append_skill_context,
|
||||
apply_training_data,
|
||||
build_task_prompt_with_schema,
|
||||
format_task_with_context,
|
||||
get_knowledge_config,
|
||||
handle_knowledge_retrieval,
|
||||
handle_reasoning,
|
||||
prepare_tools,
|
||||
process_tool_results,
|
||||
save_last_messages,
|
||||
@@ -150,7 +149,17 @@ def _validate_executor_class(value: Any) -> Any:
|
||||
cls = _EXECUTOR_CLASS_MAP.get(value)
|
||||
if cls is None:
|
||||
raise ValueError(f"Unknown executor class: {value}")
|
||||
return cls
|
||||
value = cls
|
||||
import warnings
|
||||
|
||||
if value is CrewAgentExecutor:
|
||||
warnings.warn(
|
||||
"CrewAgentExecutor is deprecated and will be removed in a future release. "
|
||||
"Agents inside Crews now use AgentExecutor by default. "
|
||||
"Switch to crewai.experimental.AgentExecutor.",
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
@@ -325,8 +334,8 @@ class Agent(BaseAgent):
|
||||
BeforeValidator(_validate_executor_class),
|
||||
PlainSerializer(_serialize_executor_class, return_type=str, when_used="json"),
|
||||
] = Field(
|
||||
default=CrewAgentExecutor,
|
||||
description="Class to use for the agent executor. Defaults to CrewAgentExecutor, can optionally use AgentExecutor.",
|
||||
default=AgentExecutor,
|
||||
description="Class to use for the agent executor. Defaults to AgentExecutor, can optionally use CrewAgentExecutor.",
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@@ -512,8 +521,6 @@ class Agent(BaseAgent):
|
||||
The task prompt after memory retrieval, ready for knowledge lookup.
|
||||
"""
|
||||
get_env_context()
|
||||
if self.executor_class is not AgentExecutor:
|
||||
handle_reasoning(self, task)
|
||||
|
||||
self._inject_date_to_task(task)
|
||||
|
||||
@@ -541,7 +548,6 @@ class Agent(BaseAgent):
|
||||
Returns:
|
||||
The fully prepared task prompt.
|
||||
"""
|
||||
task_prompt = append_skill_context(self, task_prompt)
|
||||
prepare_tools(self, tools, task)
|
||||
|
||||
return apply_training_data(self, task_prompt)
|
||||
@@ -843,18 +849,22 @@ class Agent(BaseAgent):
|
||||
if not self.agent_executor:
|
||||
raise RuntimeError("Agent executor is not initialized.")
|
||||
|
||||
result = cast(
|
||||
dict[str, Any],
|
||||
self.agent_executor.invoke(
|
||||
{
|
||||
"input": task_prompt,
|
||||
"tool_names": self.agent_executor.tools_names,
|
||||
"tools": self.agent_executor.tools_description,
|
||||
"ask_for_human_input": task.human_input,
|
||||
}
|
||||
),
|
||||
invoke_result = self.agent_executor.invoke(
|
||||
{
|
||||
"input": task_prompt,
|
||||
"tool_names": self.agent_executor.tools_names,
|
||||
"tools": self.agent_executor.tools_description,
|
||||
"ask_for_human_input": task.human_input,
|
||||
}
|
||||
)
|
||||
return result["output"]
|
||||
if inspect.isawaitable(invoke_result):
|
||||
invoke_result.close()
|
||||
raise RuntimeError(
|
||||
"Agent execution was invoked synchronously from within a running "
|
||||
"event loop. Use `agent.kickoff_async()` / `crew.kickoff_async()` "
|
||||
"(or `await agent.aexecute_task(...)`) when calling from async code."
|
||||
)
|
||||
return invoke_result["output"]
|
||||
|
||||
async def aexecute_task(
|
||||
self,
|
||||
@@ -1474,8 +1484,6 @@ class Agent(BaseAgent):
|
||||
),
|
||||
)
|
||||
|
||||
formatted_messages = append_skill_context(self, formatted_messages)
|
||||
|
||||
inputs: dict[str, Any] = {
|
||||
"input": formatted_messages,
|
||||
"tool_names": get_tool_names(parsed_tools),
|
||||
|
||||
@@ -213,30 +213,6 @@ def _combine_knowledge_context(agent: Agent) -> str:
|
||||
return agent_ctx + separator + crew_ctx
|
||||
|
||||
|
||||
def append_skill_context(agent: Agent, task_prompt: str) -> str:
|
||||
"""Append activated skill context sections to the task prompt.
|
||||
|
||||
Args:
|
||||
agent: The agent with optional skills.
|
||||
task_prompt: The current task prompt.
|
||||
|
||||
Returns:
|
||||
The task prompt with skill context appended.
|
||||
"""
|
||||
if not agent.skills:
|
||||
return task_prompt
|
||||
|
||||
from crewai.skills.loader import format_skill_context
|
||||
from crewai.skills.models import Skill
|
||||
|
||||
skill_sections = [
|
||||
format_skill_context(s) for s in agent.skills if isinstance(s, Skill)
|
||||
]
|
||||
if skill_sections:
|
||||
task_prompt += "\n\n" + "\n\n".join(skill_sections)
|
||||
return task_prompt
|
||||
|
||||
|
||||
def apply_training_data(agent: Agent, task_prompt: str) -> str:
|
||||
"""Apply training data to the task prompt.
|
||||
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from crewai.agents.cache.cache_handler import CacheHandler
|
||||
from crewai.agents.parser import AgentAction, AgentFinish, OutputParserError, parse
|
||||
from crewai.agents.tools_handler import ToolsHandler
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai.agents.crew_agent_executor import CrewAgentExecutor
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentAction",
|
||||
"AgentFinish",
|
||||
"CacheHandler",
|
||||
"CrewAgentExecutor",
|
||||
"OutputParserError",
|
||||
"ToolsHandler",
|
||||
"parse",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name == "CrewAgentExecutor":
|
||||
from crewai.agents.crew_agent_executor import CrewAgentExecutor
|
||||
|
||||
return CrewAgentExecutor
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
@@ -14,6 +14,7 @@ import contextvars
|
||||
import inspect
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Annotated, Any, Literal, cast
|
||||
import warnings
|
||||
|
||||
from crewai_core.printer import PRINTER
|
||||
from pydantic import (
|
||||
@@ -138,6 +139,13 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
warnings.warn(
|
||||
"CrewAgentExecutor is deprecated and will be removed in a future release.\n"
|
||||
"Agents inside Crews now use AgentExecutor (crewai.experimental.AgentExecutor) by default.\n"
|
||||
"To suppress this warning, migrate to AgentExecutor.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if not self.before_llm_call_hooks:
|
||||
self.before_llm_call_hooks.extend(get_before_llm_call_hooks())
|
||||
if not self.after_llm_call_hooks:
|
||||
@@ -166,6 +174,8 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
if provider.setup_messages(cast(ExecutorContext, cast(object, self))):
|
||||
return
|
||||
|
||||
from crewai.llms.cache import mark_cache_breakpoint
|
||||
|
||||
if self.prompt is not None and "system" in self.prompt:
|
||||
system_prompt = self._format_prompt(
|
||||
cast(str, self.prompt.get("system", "")), inputs
|
||||
@@ -173,11 +183,22 @@ class CrewAgentExecutor(BaseAgentExecutor):
|
||||
user_prompt = self._format_prompt(
|
||||
cast(str, self.prompt.get("user", "")), inputs
|
||||
)
|
||||
self.messages.append(format_message_for_llm(system_prompt, role="system"))
|
||||
self.messages.append(format_message_for_llm(user_prompt))
|
||||
# Cache breakpoints: end-of-system caches the per-agent stable
|
||||
# prefix; end-of-user caches the per-task stable prefix across
|
||||
# ReAct-loop iterations.
|
||||
self.messages.append(
|
||||
mark_cache_breakpoint(
|
||||
format_message_for_llm(system_prompt, role="system")
|
||||
)
|
||||
)
|
||||
self.messages.append(
|
||||
mark_cache_breakpoint(format_message_for_llm(user_prompt))
|
||||
)
|
||||
elif self.prompt is not None:
|
||||
user_prompt = self._format_prompt(self.prompt.get("prompt", ""), inputs)
|
||||
self.messages.append(format_message_for_llm(user_prompt))
|
||||
self.messages.append(
|
||||
mark_cache_breakpoint(format_message_for_llm(user_prompt))
|
||||
)
|
||||
|
||||
provider.post_setup_messages(cast(ExecutorContext, cast(object, self)))
|
||||
|
||||
|
||||
@@ -1191,6 +1191,13 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
@router("force_final_answer")
|
||||
def ensure_force_final_answer(self) -> Literal["agent_finished"]:
|
||||
"""Force agent to provide final answer when max iterations exceeded."""
|
||||
# The flow framework can route here more than once per execution when the
|
||||
# "initialized" label is emitted by both initialize_reasoning and
|
||||
# increment_and_continue in the same listener pass. Skip the extra LLM
|
||||
# round-trip once we've already produced a forced final answer.
|
||||
if self.state.is_finished:
|
||||
return "agent_finished"
|
||||
|
||||
formatted_answer = handle_max_iterations_exceeded(
|
||||
formatted_answer=None,
|
||||
printer=PRINTER,
|
||||
@@ -2579,16 +2586,26 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
self._kickoff_input = inputs.get("input", "")
|
||||
|
||||
if "system" in self.prompt:
|
||||
from crewai.llms.cache import mark_cache_breakpoint
|
||||
|
||||
prompt = cast("SystemPromptResult", self.prompt)
|
||||
system_prompt = self._format_prompt(prompt["system"], inputs)
|
||||
user_prompt = self._format_prompt(prompt["user"], inputs)
|
||||
self.state.messages.append(
|
||||
format_message_for_llm(system_prompt, role="system")
|
||||
mark_cache_breakpoint(
|
||||
format_message_for_llm(system_prompt, role="system")
|
||||
)
|
||||
)
|
||||
self.state.messages.append(
|
||||
mark_cache_breakpoint(format_message_for_llm(user_prompt))
|
||||
)
|
||||
self.state.messages.append(format_message_for_llm(user_prompt))
|
||||
else:
|
||||
from crewai.llms.cache import mark_cache_breakpoint
|
||||
|
||||
user_prompt = self._format_prompt(self.prompt["prompt"], inputs)
|
||||
self.state.messages.append(format_message_for_llm(user_prompt))
|
||||
self.state.messages.append(
|
||||
mark_cache_breakpoint(format_message_for_llm(user_prompt))
|
||||
)
|
||||
|
||||
self._inject_files_from_inputs(inputs)
|
||||
|
||||
@@ -2670,16 +2687,26 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
self._kickoff_input = inputs.get("input", "")
|
||||
|
||||
if "system" in self.prompt:
|
||||
from crewai.llms.cache import mark_cache_breakpoint
|
||||
|
||||
prompt = cast("SystemPromptResult", self.prompt)
|
||||
system_prompt = self._format_prompt(prompt["system"], inputs)
|
||||
user_prompt = self._format_prompt(prompt["user"], inputs)
|
||||
self.state.messages.append(
|
||||
format_message_for_llm(system_prompt, role="system")
|
||||
mark_cache_breakpoint(
|
||||
format_message_for_llm(system_prompt, role="system")
|
||||
)
|
||||
)
|
||||
self.state.messages.append(
|
||||
mark_cache_breakpoint(format_message_for_llm(user_prompt))
|
||||
)
|
||||
self.state.messages.append(format_message_for_llm(user_prompt))
|
||||
else:
|
||||
from crewai.llms.cache import mark_cache_breakpoint
|
||||
|
||||
user_prompt = self._format_prompt(self.prompt["prompt"], inputs)
|
||||
self.state.messages.append(format_message_for_llm(user_prompt))
|
||||
self.state.messages.append(
|
||||
mark_cache_breakpoint(format_message_for_llm(user_prompt))
|
||||
)
|
||||
|
||||
self._inject_files_from_inputs(inputs)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, cast
|
||||
import uuid
|
||||
|
||||
from pydantic import (
|
||||
@@ -703,10 +703,19 @@ class BaseLLM(BaseModel, ABC):
|
||||
Raises:
|
||||
ValueError: If message format is invalid
|
||||
"""
|
||||
from crewai.llms.cache import CACHE_BREAKPOINT_KEY
|
||||
from crewai.utilities.types import LLMMessage as _LLMMessage
|
||||
|
||||
if isinstance(messages, str):
|
||||
return [{"role": "user", "content": messages}]
|
||||
|
||||
# Validate message format
|
||||
# Validate then copy each message, dropping the cache-breakpoint
|
||||
# flag in the copy only. The caller (e.g. CrewAgentExecutor,
|
||||
# experimental.AgentExecutor) reuses its messages buffer across
|
||||
# many LLM calls in the tool-use loop; mutating their dicts
|
||||
# in place would erase the markers after the first call and
|
||||
# break prompt caching for every subsequent iteration.
|
||||
cleaned: list[LLMMessage] = []
|
||||
for i, msg in enumerate(messages):
|
||||
if not isinstance(msg, dict):
|
||||
raise ValueError(f"Message at index {i} must be a dictionary")
|
||||
@@ -714,8 +723,12 @@ class BaseLLM(BaseModel, ABC):
|
||||
raise ValueError(
|
||||
f"Message at index {i} must have 'role' and 'content' keys"
|
||||
)
|
||||
copy: dict[str, Any] = {
|
||||
k: v for k, v in msg.items() if k != CACHE_BREAKPOINT_KEY
|
||||
}
|
||||
cleaned.append(cast(_LLMMessage, copy))
|
||||
|
||||
return self._process_message_files(messages)
|
||||
return self._process_message_files(cleaned)
|
||||
|
||||
def _process_message_files(self, messages: list[LLMMessage]) -> list[LLMMessage]:
|
||||
"""Process files attached to messages and format for the provider.
|
||||
|
||||
37
lib/crewai/src/crewai/llms/cache.py
Normal file
37
lib/crewai/src/crewai/llms/cache.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Provider-agnostic prompt-cache breakpoint marker.
|
||||
|
||||
Application code (prompt builders, agent executors) marks messages where a
|
||||
stable prefix ends. Provider adapters then translate the marker into the
|
||||
cache directive their API expects, or strip it for providers that cache
|
||||
implicitly (OpenAI, Gemini) or do not cache at all.
|
||||
|
||||
Usage:
|
||||
|
||||
from crewai.llms.cache import mark_cache_breakpoint
|
||||
|
||||
messages = [
|
||||
mark_cache_breakpoint({"role": "system", "content": stable_system}),
|
||||
mark_cache_breakpoint({"role": "user", "content": stable_user_prefix}),
|
||||
{"role": "user", "content": volatile_query},
|
||||
]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
CACHE_BREAKPOINT_KEY = "cache_breakpoint"
|
||||
|
||||
|
||||
def mark_cache_breakpoint(message: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Return ``message`` with the cache-breakpoint flag set.
|
||||
|
||||
Returns a new dict so callers can safely pass literal dicts.
|
||||
"""
|
||||
return {**message, CACHE_BREAKPOINT_KEY: True}
|
||||
|
||||
|
||||
def strip_cache_breakpoint(message: dict[str, Any]) -> None:
|
||||
"""Remove the breakpoint flag from a message in place."""
|
||||
message.pop(CACHE_BREAKPOINT_KEY, None)
|
||||
@@ -425,7 +425,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
def _prepare_completion_params(
|
||||
self,
|
||||
messages: list[LLMMessage],
|
||||
system_message: str | None = None,
|
||||
system_message: str | list[dict[str, Any]] | None = None,
|
||||
tools: list[dict[str, Any]] | None = None,
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
@@ -665,7 +665,7 @@ class AnthropicCompletion(BaseLLM):
|
||||
|
||||
def _format_messages_for_anthropic(
|
||||
self, messages: str | list[LLMMessage]
|
||||
) -> tuple[list[LLMMessage], str | None]:
|
||||
) -> tuple[list[LLMMessage], str | list[dict[str, Any]] | None]:
|
||||
"""Format messages for Anthropic API.
|
||||
|
||||
Anthropic has specific requirements:
|
||||
@@ -679,8 +679,51 @@ class AnthropicCompletion(BaseLLM):
|
||||
messages: Input messages
|
||||
|
||||
Returns:
|
||||
Tuple of (formatted_messages, system_message)
|
||||
Tuple of (formatted_messages, system_message). `system_message` is
|
||||
a list of content blocks (with cache_control stamped) when any
|
||||
system message in the input carried a cache_breakpoint flag;
|
||||
otherwise a plain string for backwards compatibility.
|
||||
"""
|
||||
from crewai.llms.cache import CACHE_BREAKPOINT_KEY
|
||||
|
||||
# Read cache_breakpoint flags from raw input BEFORE super strips them.
|
||||
# We track the CONTENT of marked user/assistant messages so we can
|
||||
# locate the corresponding block in formatted_messages — Anthropic
|
||||
# rewrites tool results into user messages, so positional indices
|
||||
# do not survive the conversion. We must stamp the original stable
|
||||
# message (typically the initial task prompt), not whatever happens
|
||||
# to be the trailing user-role block after tool_result expansion.
|
||||
cache_system = False
|
||||
cache_match_contents: list[str] = []
|
||||
if not isinstance(messages, str):
|
||||
for m in messages:
|
||||
if not (isinstance(m, dict) and m.get(CACHE_BREAKPOINT_KEY)):
|
||||
continue
|
||||
role = m.get("role")
|
||||
if role == "system":
|
||||
cache_system = True
|
||||
continue
|
||||
if role != "user":
|
||||
# Only user messages survive Anthropic's role-coalescing
|
||||
# in a stable, addressable position. Markers on assistant
|
||||
# or tool messages have no reliable stamp target after
|
||||
# tool_result expansion, so we ignore them.
|
||||
continue
|
||||
raw_content = m.get("content")
|
||||
if isinstance(raw_content, str) and raw_content:
|
||||
cache_match_contents.append(raw_content)
|
||||
continue
|
||||
if isinstance(raw_content, list):
|
||||
# Pull text from a single-text-block list so callers that
|
||||
# pre-format content blocks still match cleanly.
|
||||
text_blocks = [
|
||||
b.get("text")
|
||||
for b in raw_content
|
||||
if isinstance(b, dict) and b.get("type") == "text"
|
||||
]
|
||||
if len(text_blocks) == 1 and isinstance(text_blocks[0], str):
|
||||
cache_match_contents.append(text_blocks[0])
|
||||
|
||||
# Use base class formatting first
|
||||
base_formatted = super()._format_messages(messages)
|
||||
|
||||
@@ -788,7 +831,62 @@ class AnthropicCompletion(BaseLLM):
|
||||
# If first message is not from user, insert a user message at the beginning
|
||||
formatted_messages.insert(0, {"role": "user", "content": "Hello"})
|
||||
|
||||
return formatted_messages, system_message
|
||||
# Stamp cache_control on the message(s) whose original content was
|
||||
# marked. We scan formatted_messages in order and stamp the first
|
||||
# match per marked content — Anthropic permits up to 4 cache
|
||||
# breakpoints per request, which is more than enough for our usage.
|
||||
# Matching by content (rather than position) handles the ReAct
|
||||
# case where tool_result blocks get expanded into trailing user
|
||||
# messages: the stable initial-task prompt still maps cleanly.
|
||||
for needle in cache_match_contents:
|
||||
for fm in formatted_messages:
|
||||
if fm.get("role") != "user":
|
||||
continue
|
||||
content = fm.get("content")
|
||||
if isinstance(content, str) and content == needle:
|
||||
self._stamp_cache_control_on_message(fm)
|
||||
break
|
||||
if isinstance(content, list):
|
||||
fm_texts: list[str] = [
|
||||
b.get("text", "")
|
||||
for b in content
|
||||
if isinstance(b, dict) and b.get("type") == "text"
|
||||
]
|
||||
if len(fm_texts) == 1 and fm_texts[0] == needle:
|
||||
self._stamp_cache_control_on_message(fm)
|
||||
break
|
||||
|
||||
# Convert system to content-block form when caching is requested.
|
||||
system_payload: str | list[dict[str, Any]] | None = system_message
|
||||
if system_message and cache_system:
|
||||
system_payload = [
|
||||
{
|
||||
"type": "text",
|
||||
"text": system_message,
|
||||
"cache_control": {"type": "ephemeral"},
|
||||
}
|
||||
]
|
||||
|
||||
return formatted_messages, system_payload
|
||||
|
||||
@staticmethod
|
||||
def _stamp_cache_control_on_message(message: LLMMessage) -> None:
|
||||
"""Stamp cache_control on the last content block of an Anthropic message."""
|
||||
msg = cast(dict[str, Any], message)
|
||||
content = msg.get("content")
|
||||
if isinstance(content, str):
|
||||
msg["content"] = [
|
||||
{
|
||||
"type": "text",
|
||||
"text": content,
|
||||
"cache_control": {"type": "ephemeral"},
|
||||
}
|
||||
]
|
||||
return
|
||||
if isinstance(content, list) and content:
|
||||
last = content[-1]
|
||||
if isinstance(last, dict):
|
||||
last["cache_control"] = {"type": "ephemeral"}
|
||||
|
||||
def _handle_completion(
|
||||
self,
|
||||
|
||||
@@ -161,6 +161,9 @@ def format_skill_context(skill: Skill) -> str:
|
||||
At METADATA level: returns name and description only.
|
||||
At INSTRUCTIONS level or above: returns full SKILL.md body.
|
||||
|
||||
Output is wrapped in <skill name="..."> XML tags so the block can serve
|
||||
as a stable cache anchor when injected into the system prompt.
|
||||
|
||||
Args:
|
||||
skill: The skill to format.
|
||||
|
||||
@@ -169,7 +172,7 @@ def format_skill_context(skill: Skill) -> str:
|
||||
"""
|
||||
if skill.disclosure_level >= INSTRUCTIONS and skill.instructions:
|
||||
parts = [
|
||||
f"## Skill: {skill.name}",
|
||||
f'<skill name="{skill.name}">',
|
||||
skill.description,
|
||||
"",
|
||||
skill.instructions,
|
||||
@@ -180,5 +183,6 @@ def format_skill_context(skill: Skill) -> str:
|
||||
for dir_name, files in sorted(skill.resource_files.items()):
|
||||
if files:
|
||||
parts.append(f"- **{dir_name}/**: {', '.join(files)}")
|
||||
parts.append("</skill>")
|
||||
return "\n".join(parts)
|
||||
return f"## Skill: {skill.name}\n{skill.description}"
|
||||
return f'<skill name="{skill.name}">\n{skill.description}\n</skill>'
|
||||
|
||||
@@ -86,7 +86,7 @@ class Prompts(BaseModel):
|
||||
slices.append("tools")
|
||||
else:
|
||||
slices.append("no_tools")
|
||||
system: str = self._build_prompt(slices)
|
||||
system: str = self._build_prompt(slices) + self._build_skill_block()
|
||||
|
||||
# Determine which task slice to use:
|
||||
task_slice: COMPONENTS
|
||||
@@ -106,7 +106,7 @@ class Prompts(BaseModel):
|
||||
return SystemPromptResult(
|
||||
system=system,
|
||||
user=self._build_prompt([task_slice]),
|
||||
prompt=self._build_prompt(slices),
|
||||
prompt=self._build_prompt(slices) + self._build_skill_block(),
|
||||
)
|
||||
return StandardPromptResult(
|
||||
prompt=self._build_prompt(
|
||||
@@ -115,8 +115,27 @@ class Prompts(BaseModel):
|
||||
self.prompt_template,
|
||||
self.response_template,
|
||||
)
|
||||
+ self._build_skill_block()
|
||||
)
|
||||
|
||||
def _build_skill_block(self) -> str:
|
||||
"""Render the agent's activated skills as a stable XML block.
|
||||
|
||||
Skills are agent-scoped (do not change per task), so they live in the
|
||||
system prompt where prompt-cache prefixes can survive across calls.
|
||||
"""
|
||||
skills = getattr(self.agent, "skills", None)
|
||||
if not skills:
|
||||
return ""
|
||||
|
||||
from crewai.skills.loader import format_skill_context
|
||||
from crewai.skills.models import Skill
|
||||
|
||||
sections = [format_skill_context(s) for s in skills if isinstance(s, Skill)]
|
||||
if not sections:
|
||||
return ""
|
||||
return "\n\n<skills>\n" + "\n\n".join(sections) + "\n</skills>"
|
||||
|
||||
def _build_prompt(
|
||||
self,
|
||||
components: list[COMPONENTS],
|
||||
|
||||
@@ -389,10 +389,8 @@ def test_agent_custom_max_iterations():
|
||||
assert result is not None
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
assert call_count > 0
|
||||
# With max_iter=1, expect 2 calls:
|
||||
# - Call 1: iteration 0
|
||||
# - Call 2: iteration 1 (max reached, handle_max_iterations_exceeded called, then loop breaks)
|
||||
# With max_iter=1, exactly two provider calls are expected:
|
||||
# one inside the reasoning loop and one for the forced final answer.
|
||||
assert call_count == 2
|
||||
|
||||
|
||||
@@ -702,6 +700,7 @@ def test_agent_definition_based_on_dict():
|
||||
|
||||
# test for human input
|
||||
@pytest.mark.vcr()
|
||||
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
|
||||
def test_agent_human_input():
|
||||
from crewai.core.providers.human_input import SyncHumanInputProvider
|
||||
|
||||
@@ -710,6 +709,7 @@ def test_agent_human_input():
|
||||
"role": "test role",
|
||||
"goal": "test goal",
|
||||
"backstory": "test backstory",
|
||||
"executor_class": CrewAgentExecutor,
|
||||
}
|
||||
|
||||
agent = Agent(**config)
|
||||
@@ -839,7 +839,9 @@ Thought:<|eot_id|>
|
||||
|
||||
"""
|
||||
|
||||
with patch.object(CrewAgentExecutor, "_format_prompt") as mock_format_prompt:
|
||||
from crewai.experimental.agent_executor import AgentExecutor
|
||||
|
||||
with patch.object(AgentExecutor, "_format_prompt") as mock_format_prompt:
|
||||
mock_format_prompt.return_value = expected_prompt
|
||||
|
||||
# Trigger the _format_prompt method
|
||||
@@ -1098,9 +1100,11 @@ def test_agent_max_retry_limit():
|
||||
|
||||
agent.create_agent_executor(task=task)
|
||||
|
||||
from crewai.experimental.agent_executor import AgentExecutor
|
||||
|
||||
error_message = "Error happening while sending prompt to model."
|
||||
with patch.object(
|
||||
CrewAgentExecutor, "invoke", wraps=agent.agent_executor.invoke
|
||||
AgentExecutor, "invoke", wraps=agent.agent_executor.invoke
|
||||
) as invoke_mock:
|
||||
invoke_mock.side_effect = Exception(error_message)
|
||||
|
||||
@@ -1283,8 +1287,10 @@ def test_handle_context_length_exceeds_limit_cli_no():
|
||||
|
||||
agent.create_agent_executor(task=task)
|
||||
|
||||
from crewai.experimental.agent_executor import AgentExecutor
|
||||
|
||||
with patch.object(
|
||||
CrewAgentExecutor, "invoke", wraps=agent.agent_executor.invoke
|
||||
AgentExecutor, "invoke", wraps=agent.agent_executor.invoke
|
||||
) as private_mock:
|
||||
task = Task(
|
||||
description="The final answer is 42. But don't give it yet, instead keep using the `get_final_answer` tool.",
|
||||
|
||||
@@ -286,8 +286,6 @@ def test_agent_execute_task_with_planning():
|
||||
|
||||
assert result is not None
|
||||
assert "20" in str(result)
|
||||
# Planning should be appended to task description
|
||||
assert "Planning:" in task.description
|
||||
|
||||
|
||||
@pytest.mark.vcr()
|
||||
@@ -342,4 +340,3 @@ def test_agent_execute_task_with_planning_refine():
|
||||
assert result is not None
|
||||
# Area = pi * r^2 = 3.14 * 25 = 78.5
|
||||
assert "78" in str(result) or "79" in str(result)
|
||||
assert "Planning:" in task.description
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,17 +1,13 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"max_tokens":4096,"messages":[{"role":"user","content":"\nCurrent Task:
|
||||
Describe the file(s) you see. Be brief, one sentence max.\n\nAvailable input
|
||||
files (use the name in quotes with read_file tool):\n - \"document\" (document,
|
||||
application/pdf)\n\nThis is the expected criteria for your final answer: A brief
|
||||
description of the file.\nyou MUST return the actual complete content as the
|
||||
final answer, not a summary.\n\nThis is VERY important to you, your job depends
|
||||
on it!"}],"model":"claude-sonnet-4-20250514","stop_sequences":["\nObservation:"],"stream":false,"system":"You
|
||||
Describe the file(s) you see. Be brief, one sentence max.\n\nInput files (content
|
||||
already loaded in conversation):\n - \"document\" (agents.pdf)\n\nThis is the
|
||||
expected criteria for your final answer: A brief description of the file.\nyou
|
||||
MUST return the actual complete content as the final answer, not a summary.\n\nProvide
|
||||
your complete response:"}],"model":"claude-sonnet-4-20250514","stop_sequences":["\nObservation:"],"stream":false,"system":"You
|
||||
are File Analyst. Expert at analyzing various file types.\nYour personal goal
|
||||
is: Analyze and describe files accurately","tools":[{"name":"read_file","description":"Read
|
||||
content from an input file by name. Returns file content as text for text files,
|
||||
or base64 for binary files.","input_schema":{"properties":{"file_name":{"description":"The
|
||||
name of the input file to read","title":"File Name","type":"string"}},"required":["file_name"],"type":"object"}}]}'
|
||||
is: Analyze and describe files accurately"}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
@@ -24,7 +20,7 @@ interactions:
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '1035'
|
||||
- '648'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
@@ -40,30 +36,305 @@ interactions:
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 0.71.1
|
||||
- 0.98.0
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.12.10
|
||||
- 3.13.3
|
||||
x-stainless-timeout:
|
||||
- NOT_GIVEN
|
||||
method: POST
|
||||
uri: https://api.anthropic.com/v1/messages
|
||||
response:
|
||||
body:
|
||||
string: '{"model":"claude-sonnet-4-20250514","id":"msg_01QQ1BGjRzaj6vneE9LNtCoz","type":"message","role":"assistant","content":[{"type":"text","text":"I''ll
|
||||
read and analyze the PDF document file for you."},{"type":"tool_use","id":"toolu_01QU7Hu64D5PxA5UUu5LG7Ff","name":"read_file","input":{"file_name":"document"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":545,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":68,"service_tier":"standard"}}'
|
||||
string: "{\"model\":\"claude-sonnet-4-20250514\",\"id\":\"msg_01JyzdLa2yekgTp7vjoG9V8n\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"The
|
||||
file \\\"agents.pdf\\\" is a research paper titled \\\"Generative Agents:
|
||||
Interactive Simulacra of Human Behavior\\\" that presents a computational
|
||||
architecture for creating believable AI agents that can simulate human-like
|
||||
social behaviors in interactive environments.\\n\\nSince you requested the
|
||||
actual complete content, here is the full text of the PDF:\\n\\n**Generative
|
||||
Agents: Interactive Simulacra of Human Behavior**\\n\\n*Joon Sung Park, Joseph
|
||||
C. O'Brien, Carrie J. Cai, Meredith Ringel Morris, Percy Liang, Michael S.
|
||||
Bernstein*\\n\\nStanford University, Google Research\\n\\n**Abstract**\\n\\nBelievable
|
||||
proxies of human behavior can empower interactive applications ranging from
|
||||
immersive environments to rehearsal spaces for interpersonal communication
|
||||
to prototyping tools. In this paper, we introduce generative agents\u2014computational
|
||||
software agents that simulate believable human behavior. Generative agents
|
||||
wake up, cook breakfast, and head to work; artists paint, while authors write;
|
||||
they form opinions, notice each other, and initiate conversations; they remember
|
||||
and reflect on days past as they plan the next day. To enable generative agents,
|
||||
we describe an architecture that extends a large language model to store a
|
||||
complete record of the agent's experiences using natural language, synthesize
|
||||
those memories over time into higher-level reflections, and retrieve them
|
||||
dynamically to plan behavior. We instantiate generative agents to populate
|
||||
an interactive sandbox environment inspired by The Sims, where end users can
|
||||
interact with a small town of twenty five agents using natural language. In
|
||||
an evaluation, these generative agents produce believable individual and emergent
|
||||
social behaviors: for example, starting with only a single user-specified
|
||||
notion that one agent wants to throw a Valentine's Day party, the agents autonomously
|
||||
spread invitations to the party over the next two days, make new acquaintances,
|
||||
ask each other out on dates, and coordinate to show up for the party together
|
||||
at the right time. We demonstrate through ablation that the components of
|
||||
our agent architecture\u2014observation, planning, and reflection\u2014each
|
||||
contribute critically to the believability of agent behavior. By fusing large
|
||||
language models with computational, interactive agents, this work introduces
|
||||
architectural and interaction design patterns for enabling believable simulations
|
||||
of human behavior.\\n\\n**1 Introduction**\\n\\nHow might we craft an interactive
|
||||
artificial society filled with believable proxies of human behavior? From
|
||||
sandbox games such as The Sims to applications in education, dialogue systems
|
||||
to immersive environments, and social simulacra to prototyping tools, this
|
||||
vision of believable agents has inspired creators, theorists, and technologists
|
||||
for decades [7, 10, 69]. In these visions, people could populate a virtual
|
||||
space with interactive agents that reflect the diversity and richness of human
|
||||
social behavior, getting a second opinion on a presentation before making
|
||||
it to a client, or testing out ideas that are difficult to try in real life.\\n\\nPrior
|
||||
research in human-AI interaction has paved the way by recognizing that believable
|
||||
agents do not necessarily need to be indistinguishable from humans, but they
|
||||
should behave consistently with our expectations of human behavior in a given
|
||||
context [80]. Such agents should be able to live in their environment by retaining
|
||||
what has happened, interacting with other agents, and making decisions that
|
||||
build on their past experiences in believable ways.\\n\\nHowever, prior approaches
|
||||
to creating believable agents often depend on human authoring (e.g., in commercial
|
||||
games [26]) or focus on narrow contexts that may not generalize (e.g., job
|
||||
interviews [44] or small group communication [43, 78]). The space of human
|
||||
behavior is vast and complex\u2014human agents draw on their memory of past
|
||||
experiences, reflect on their core characteristics, and dynamically reason
|
||||
about their environment and relationships to act believably. As a result,
|
||||
agent architectures that rely on a small number of hand-crafted rules or narrow
|
||||
training will fall short of our ideal of believable behavior.\\n\\nIn this
|
||||
paper, we introduce generative agents, computational software agents that
|
||||
simulate believable human behavior. Generative agents are designed to represent
|
||||
individual people: they have memory, personality, goals, and relationships,
|
||||
and they behave consistently with these traits. A generative agent wakes up,
|
||||
brushes their teeth, makes breakfast, and heads to work. At work, a generative
|
||||
agent building teacher may teach students, while a generative agent college
|
||||
student may attend classes, study at the library, and chat with classmates.
|
||||
Along the way, they form new relationships, reflect on their past and present,
|
||||
and coordinate with other agents they encounter.\\n\\nTo accomplish this,
|
||||
generative agents operate in an agent architecture that extends a large language
|
||||
model with three key components. First, we equip agents with memory: a record
|
||||
of their experiences stored in natural language. We extend this memory with
|
||||
a retrieval function that surfaces the most relevant memories given the agent's
|
||||
current situation. Second, we introduce reflection: a process by which agents,
|
||||
over time, synthesize their observations into higher-level inferences about
|
||||
themselves and others, which can guide future behavior. For example, an agent
|
||||
might infer that another agent is interested in them romantically, or that
|
||||
they themselves are becoming more popular. Third, we add planning: a process
|
||||
by which agents translate their conclusions about themselves, others, and
|
||||
their environment into coherent sequences of actions. For example, an agent
|
||||
might decide to cook dinner for their partner, set the table, and invite them
|
||||
for a romantic evening.\\n\\nWe instantiate generative agents as characters
|
||||
in an interactive sandbox environment inspired by The Sims, to demonstrate
|
||||
their potential for creating believable, emergent social interactions. In
|
||||
our environment\u2014a small town called Smallville\u2014we situate twenty-five
|
||||
unique generative agents with distinct personalities, occupations, and relationships.
|
||||
Over the course of two full game days, we find that the agents demonstrate
|
||||
believable individual behaviors (e.g., a character with an interest in paintings
|
||||
creates a new painting, a character who is running for mayor talks to constituents)
|
||||
and believable social behaviors (e.g., agents ask each other out on dates,
|
||||
coordinate parties, spread news and gossip). Starting with only a single user-specified
|
||||
seed\u2014that one character wants to throw a Valentine's Day party\u2014the
|
||||
agents autonomously spread invitations to the party over the course of two
|
||||
days, make new acquaintances, ask each other out on dates, and show up to
|
||||
the party together at the right time.\\n\\nWe evaluate the behavior of our
|
||||
generative agents through interviews with the agents themselves, as well as
|
||||
interviews with human participants who have watched replays of the agents'
|
||||
behavior. We demonstrate that each component of our architecture\u2014memory,
|
||||
reflection, and planning\u2014contributes to more believable behavior through
|
||||
ablations that disable each component.\\n\\nOur approach draws on recent advances
|
||||
in large language models [12, 21, 64, 74]. These models demonstrate increasingly
|
||||
sophisticated behavior, from question answering [74] to code generation [21]
|
||||
to creative writing [12]. However, their success has been in the context of
|
||||
turns in dialogue, not in the context of a persistent agent that needs to
|
||||
manage its attention and behavior over time while living in an environment
|
||||
with other agents. Our work demonstrates how large language models can be
|
||||
extended to power agents that can believably simulate human behavior over
|
||||
time.\\n\\n**2 Related Work**\\n\\n**Human behavior simulation.** Creating
|
||||
believable agents requires computational models that can simulate the breadth
|
||||
of human behavior. Psychology and cognitive science have contributed formal
|
||||
models of human behavior [1, 18]. However, these models typically focus on
|
||||
specific facets of human behavior and do not easily extend to the breadth
|
||||
of social situations that people navigate. For example, a theory of personality
|
||||
[18] may help us understand individual differences in behavior, but it may
|
||||
not help us simulate realistic conversational behavior.\\n\\nResearch in intelligent
|
||||
user interfaces [56] and intelligent virtual agents [65] has demonstrated
|
||||
that people can form social relationships with agents and prefer agents that
|
||||
maintain some consistency in their behavior and personality [15]. However,
|
||||
these works typically rely on rule-based systems to achieve believability
|
||||
[9, 48], with behavior trees and finite state machines as common approaches
|
||||
for encoding agent behavior [49, 61]. While these systems can perform well
|
||||
in constrained domains, hand-authoring believable behavior that can handle
|
||||
the full space of possible interactions remains a challenge.\\n\\n**Large
|
||||
language models.** Recent progress in large language models has demonstrated
|
||||
that these models can produce behavior that appears human-like across a wide
|
||||
range of contexts. However, this behavior is typically seen at the scale of
|
||||
a single conversation turn, not in the context of a persistent agent that
|
||||
needs to manage its behavior over time. Our work demonstrates how to extend
|
||||
large language models to create agents that can maintain consistent behavior
|
||||
and personality over time, manage their attention and memory, and coordinate
|
||||
with other agents.\\n\\nRecent work has explored using language models to
|
||||
create interactive agents in various contexts, including dialogue systems
|
||||
[73], task-oriented agents [46], and game-playing agents [33]. However, these
|
||||
approaches typically focus on narrow tasks or short-term interactions, rather
|
||||
than the kind of persistent, long-term agent behavior that we explore in this
|
||||
work.\\n\\n**Interactive narrative and games.** Our work builds on a long
|
||||
tradition of interactive narrative and games that aim to create believable
|
||||
virtual characters. Commercial games like The Sims [53] have demonstrated
|
||||
that players are interested in complex virtual societies where they can interact
|
||||
with autonomous agents. However, these games typically rely on hand-crafted
|
||||
behaviors that, while entertaining, are limited in their ability to handle
|
||||
novel situations or exhibit the full richness of human social behavior.\\n\\nAcademic
|
||||
research in interactive narrative has explored ways to create more believable
|
||||
virtual characters, including work on character believability [11], emergent
|
||||
narrative [6], and social simulation [70]. However, these approaches have
|
||||
typically been limited by the complexity of hand-authoring believable behavior
|
||||
or by the narrow focus of the models used.\\n\\n**3 Generative Agents**\\n\\nThis
|
||||
section introduces our generative agent architecture. We begin by laying out
|
||||
our design goals, then present the agent architecture, and finally walk through
|
||||
an example that illustrates how the architecture works in practice.\\n\\n**3.1
|
||||
Agent Architecture Overview**\\n\\nOur agent architecture comprises three
|
||||
main components that work together to retrieve relevant information and synthesize
|
||||
it into believable behavior: **memory**, **reflection**, and **planning**.\\n\\n**Memory**
|
||||
allows generative agents to remember experiences and retrieve them later to
|
||||
inform their behavior. Without memory, an agent would not be able to build
|
||||
relationships, learn from past experiences, or maintain consistency in their
|
||||
behavior over time. The memory system stores a comprehensive record of the
|
||||
agent's experiences in natural language.\\n\\n**Reflection** allows generative
|
||||
agents to synthesize memories into higher level, more abstract thoughts and
|
||||
guide behavior. Agents reflect periodically on recent experiences to form
|
||||
new memories about their patterns of behavior, preferences, and beliefs about
|
||||
themselves and others in their environment. These reflections can be about
|
||||
the agent's own behavior patterns (e.g., \\\"I tend to be more productive
|
||||
in the mornings\\\"), the behavior of others (e.g., \\\"John is always late
|
||||
to meetings\\\"), or more abstract concepts (e.g., \\\"I think I'm becoming
|
||||
more popular\\\"). \\n\\n**Planning** allows generative agents to plan out
|
||||
their behavior, both in terms of how to act in their current situation and
|
||||
how to schedule their future activities. Plans are stored as natural language
|
||||
descriptions of intended actions and are dynamically adjusted based on the
|
||||
agent's current situation and goals.\\n\\n**3.2 Memory and Retrieval**\\n\\nGenerative
|
||||
agents need to be able to retrieve relevant memories to inform their current
|
||||
behavior. However, not all memories are equally relevant in every situation.
|
||||
For example, if an agent is deciding what to eat for breakfast, their memory
|
||||
of what they had for dinner last night may be more relevant than their memory
|
||||
of a conversation they had with a friend last week.\\n\\nTo handle this challenge,
|
||||
we implement a retrieval function that surfaces memories based on three key
|
||||
factors:\\n\\n**Recency**: More recent memories should be more likely to be
|
||||
retrieved. We assign each memory a recency score based on when it was formed,
|
||||
with more recent memories receiving higher scores.\\n\\n**Importance**: More
|
||||
important memories should be more likely to be retrieved. We use the language
|
||||
model to assess the importance of each memory on a scale from 1 to 10, where
|
||||
1 represents a mundane event and 10 represents a extremely important, poignant,
|
||||
or meaningful event.\\n\\n**Relevance**: Memories that are more relevant to
|
||||
the current situation should be more likely to be retrieved. We use embedding
|
||||
similarity between the memory and the current situation to assess relevance.\\n\\nThe
|
||||
retrieval function combines these three factors using a weighted sum to produce
|
||||
a retrieval score for each memory, then returns the memories with the highest
|
||||
scores.\\n\\n**3.3 Reflection**\\n\\nGenerative agents create higher level
|
||||
thoughts through **reflection**. These reflections synthesize memories into
|
||||
higher level questions and insights about behaviors and preferences. For example,
|
||||
Klaus Mueller, a generative agent in our implementation, reflects on his interactions
|
||||
with others and concludes, \\\"Klaus Mueller is dedicated to his research
|
||||
on mathematical music composition\\\" and \\\"Klaus Mueller likes to help
|
||||
people and understands math and physics and he is a teacher.\\\"\\n\\nAgents
|
||||
reflect when the sum of the importance scores of their latest experiences
|
||||
exceeds a threshold (in our implementation, 150). This ensures that agents
|
||||
reflect when they have had sufficient important experiences, rather than on
|
||||
a fixed schedule.\\n\\nTo generate reflections, we query the agent's memory
|
||||
for the 100 most recent records and ask the language model: \\\"Given only
|
||||
the information above, what are 3 most salient high-level questions we can
|
||||
answer about this person?\\\" We then ask the language model to answer each
|
||||
of these questions by retrieving relevant memories and synthesizing them into
|
||||
insights.\\n\\n**3.4 Planning and Reacting**\\n\\nGenerative agents create
|
||||
plans that guide their behavior. These plans are stored as natural language
|
||||
descriptions and are dynamically updated as situations change. Plans operate
|
||||
at different time horizons: broad strokes plans for the day (e.g., \\\"wake
|
||||
up, eat breakfast, go to work, eat lunch, work more, go home, eat dinner,
|
||||
watch TV, go to sleep\\\"), medium-term plans for specific activities (e.g.,
|
||||
\\\"eat breakfast: go to kitchen, prepare cereal, eat cereal, clean up\\\"),
|
||||
and moment-to-moment reactions to immediate events in their environment.\\n\\nTo
|
||||
create daily plans, agents begin each day by reflecting on their identity
|
||||
and broad goals, then creating a plan for the day. For example, John Lin might
|
||||
plan: \\\"Wake up at 7:00 am, shower, have breakfast, review research notes,
|
||||
meet with PhD students, have lunch, review more research notes, go home, have
|
||||
dinner with family, watch TV, go to sleep at 11:00 pm.\\\"\\n\\nAs agents
|
||||
execute their plans, they may encounter events that require them to react.
|
||||
When this happens, they update their current activity based on their assessment
|
||||
of the situation. For example, if John Lin encounters his neighbor while walking
|
||||
to work, he might decide to stop and chat, temporarily deviating from his
|
||||
planned route to work.\\n\\n**4 Evaluation**\\n\\nWe evaluate our generative
|
||||
agents through two main approaches: (1) controlled studies that measure individual
|
||||
aspects of agent behavior, and (2) an end-to-end evaluation in which we deploy
|
||||
agents in an environment and measure emergent individual and social behaviors.\\n\\n**4.1
|
||||
Controlled Studies**\\n\\nWe conducted three controlled studies to validate
|
||||
aspects of our approach:\\n\\n**Study 1: Interview Study**. We conducted interviews
|
||||
with five of our agents, asking them questions about themselves, their relationships,
|
||||
and their plans. We found that agents gave responses that were consistent
|
||||
with their established personalities and relationships. For example, when
|
||||
asked about his relationship with his wife, John Lin described their relationship
|
||||
in terms consistent with the interactions we had observed between them in
|
||||
the environment.\\n\\n**Study 2: Emergent Behavior Study**. We seeded one
|
||||
agent (Isabella Rodriguez) with the goal of organizing a Valentine's Day party
|
||||
and observed how this information propagated through the community of agents.
|
||||
Over the course of two days, we observed agents autonomously spreading invitations,
|
||||
making new acquaintances, asking each other out on dates, and coordinating
|
||||
to attend the party together.\\n\\n**Study 3: Ablation Study**. We conducted
|
||||
ablation studies in which we disabled each component of our architecture (memory,
|
||||
reflection, and planning) and measured the effect on agent believability.
|
||||
We found that each component contributed significantly to more believable
|
||||
agent behavior.\\n\\n**4.2 Human Evaluation**\\n\\nWe recruited human evaluators
|
||||
to watch replays of agent behavior and assess their believability. Evaluators
|
||||
watched agents in different conditions (with and without different components
|
||||
of our architecture) and rated the agents on dimensions including believability,
|
||||
consistency, and human-likeness. We found that agents with the full architecture
|
||||
were rated as significantly more believable than agents with components disabled.\\n\\n**5
|
||||
Discussion**\\n\\nOur approach demonstrates that large language models can
|
||||
be extended to create agents that exhibit believable human behavior over extended
|
||||
periods of time. The key insight is that believable behavior emerges from
|
||||
the interaction between memory, reflection, and planning\u2014agents that
|
||||
can remember past experiences, reflect on patterns in their behavior, and
|
||||
plan future actions exhibit much more coherent and believable behavior than
|
||||
agents that lack these capabilities.\\n\\n**5.1 Limitations**\\n\\nOur approach
|
||||
has several limitations. First, the behavior of generative agents is ultimately
|
||||
limited by the capabilities of the underlying language model. While current
|
||||
language models are quite sophisticated, they still make errors and exhibit
|
||||
biases that can affect agent behavior.\\n\\nSecond, our evaluation focuses
|
||||
primarily on short-term behavior (two days in our main evaluation). It remains
|
||||
an open question how well our approach would scale to longer time periods
|
||||
or more complex social structures.\\n\\nThird, our agents operate in a relatively
|
||||
simple environment. It is unclear how well our approach would generalize to
|
||||
more complex environments or tasks that require specialized knowledge or skills.\\n\\n**5.2
|
||||
Future Work**\\n\\nThere are several promising directions for future work.
|
||||
First, we could explore more sophisticated memory and retrieval mechanisms
|
||||
that better capture the complexity of human memory. Second, we could investigate
|
||||
how to enable agents to learn and adapt their behavior over longer time periods.
|
||||
Third, we could explore how to scale our approach to larger communities of
|
||||
agents or more complex environments.\\n\\n**6 Conclusion**\\n\\nWe have introduced
|
||||
generative agents, computational software agents that simulate believable
|
||||
human behavior through an architecture that combines memory, reflection, and
|
||||
planning. Our approach demonstrates that large language models can be extended
|
||||
to create agents that exhibit coherent behavior over time, form relationships
|
||||
with other agents, and coordinate complex social interactions.\\n\\nBy enabling
|
||||
believable simulations of human behavior, generative agents open up new possibilities
|
||||
for interactive applications, from sandbox games to social simulations to
|
||||
educational tools. Our work provides architectural and interaction design
|
||||
patterns that can serve as a foundation for future research and development
|
||||
in this area.\\n\\nThe code and data for this work will be made available
|
||||
to enable further research in this area.\\n\\n**References**\\n\\n[1] Gordon
|
||||
W Allport. Personality: A psychological interpretation. 1937.\\n\\n[2] Ruth
|
||||
Aylett and Sandy\"}],\"stop_reason\":\"max_tokens\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":118,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":4096,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}"
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Security-Policy:
|
||||
- CSP-FILTERED
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Fri, 23 Jan 2026 19:08:52 GMT
|
||||
- Thu, 07 May 2026 20:55:25 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Transfer-Encoding:
|
||||
@@ -84,6 +355,12 @@ interactions:
|
||||
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-REMAINING-XXX
|
||||
anthropic-ratelimit-output-tokens-reset:
|
||||
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-RESET-XXX
|
||||
anthropic-ratelimit-requests-limit:
|
||||
- '20000'
|
||||
anthropic-ratelimit-requests-remaining:
|
||||
- '19999'
|
||||
anthropic-ratelimit-requests-reset:
|
||||
- '2026-05-07T20:53:45Z'
|
||||
anthropic-ratelimit-tokens-limit:
|
||||
- ANTHROPIC-RATELIMIT-TOKENS-LIMIT-XXX
|
||||
anthropic-ratelimit-tokens-remaining:
|
||||
@@ -94,116 +371,16 @@ interactions:
|
||||
- DYNAMIC
|
||||
request-id:
|
||||
- REQUEST-ID-XXX
|
||||
set-cookie:
|
||||
- SET-COOKIE-XXX
|
||||
strict-transport-security:
|
||||
- STS-XXX
|
||||
traceresponse:
|
||||
- 00-f095e9c7565a1bcd82ed46e1b1b23dec-65cd307f1dc6144d-01
|
||||
vary:
|
||||
- Accept-Encoding
|
||||
x-envoy-upstream-service-time:
|
||||
- '2123'
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"max_tokens":4096,"messages":[{"role":"user","content":"\nCurrent Task:
|
||||
Describe the file(s) you see. Be brief, one sentence max.\n\nAvailable input
|
||||
files (use the name in quotes with read_file tool):\n - \"document\" (document,
|
||||
application/pdf)\n\nThis is the expected criteria for your final answer: A brief
|
||||
description of the file.\nyou MUST return the actual complete content as the
|
||||
final answer, not a summary.\n\nThis is VERY important to you, your job depends
|
||||
on it!"},{"role":"assistant","content":[{"type":"tool_use","id":"toolu_01QU7Hu64D5PxA5UUu5LG7Ff","name":"read_file","input":{"file_name":"document"}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_01QU7Hu64D5PxA5UUu5LG7Ff","content":"[Binary
|
||||
file: document (application/pdf)]\nBase64: JVBERi0xLjQKMSAwIG9iaiA8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4gZW5kb2JqCjIgMCBvYmogPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4gZW5kb2JqCjMgMCBvYmogPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSA+PiBlbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmCjAwMDAwMDAwMDkgMDAwMDAgbgowMDAwMDAwMDU4IDAwMDAwIG4KMDAwMDAwMDExNSAwMDAwMCBuCnRyYWlsZXIgPDwgL1NpemUgNCAvUm9vdCAxIDAgUiA+PgpzdGFydHhyZWYKMTk2CiUlRU9GCg=="}]},{"role":"user","content":"Analyze
|
||||
the tool result. If requirements are met, provide the Final Answer. Otherwise,
|
||||
call the next tool. Deliver only the answer without meta-commentary."}],"model":"claude-sonnet-4-20250514","stop_sequences":["\nObservation:"],"stream":false,"system":"You
|
||||
are File Analyst. Expert at analyzing various file types.\nYour personal goal
|
||||
is: Analyze and describe files accurately","tools":[{"name":"read_file","description":"Read
|
||||
content from an input file by name. Returns file content as text for text files,
|
||||
or base64 for binary files.","input_schema":{"properties":{"file_name":{"description":"The
|
||||
name of the input file to read","title":"File Name","type":"string"}},"required":["file_name"],"type":"object"}}]}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
anthropic-version:
|
||||
- '2023-06-01'
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '1960'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.anthropic.com
|
||||
x-api-key:
|
||||
- X-API-KEY-XXX
|
||||
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:
|
||||
- 0.71.1
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.12.10
|
||||
x-stainless-timeout:
|
||||
- NOT_GIVEN
|
||||
method: POST
|
||||
uri: https://api.anthropic.com/v1/messages
|
||||
response:
|
||||
body:
|
||||
string: '{"model":"claude-sonnet-4-20250514","id":"msg_01CjWBqSxyLeArjUhqUTbedN","type":"message","role":"assistant","content":[{"type":"text","text":"The
|
||||
document is a minimal PDF file with basic structure containing one empty page
|
||||
with standard letter dimensions (612x792 points).\n\nJVBERi0xLjQKMSAwIG9iaiA8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4gZW5kb2JqCjIgMCBvYmogPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4gZW5kb2JqCjMgMCBvYmogPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSA+PiBlbmRvYmoKeHJlZgowIDQKMDAwMDAwMDAwMCA2NTUzNSBmCjAwMDAwMDAwMDkgMDAwMDAgbgowMDAwMDAwMDU4IDAwMDAwIG4KMDAwMDAwMDExNSAwMDAwMCBuCnRyYWlsZXIgPDwgL1NpemUgNCAvUm9vdCAxIDAgUiA+PgpzdGFydHhyZWYKMTk2CiUlRU9GCg=="}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":1035,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":400,"service_tier":"standard"}}'
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Fri, 23 Jan 2026 19:08:58 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Robots-Tag:
|
||||
- none
|
||||
anthropic-organization-id:
|
||||
- ANTHROPIC-ORGANIZATION-ID-XXX
|
||||
anthropic-ratelimit-input-tokens-limit:
|
||||
- ANTHROPIC-RATELIMIT-INPUT-TOKENS-LIMIT-XXX
|
||||
anthropic-ratelimit-input-tokens-remaining:
|
||||
- ANTHROPIC-RATELIMIT-INPUT-TOKENS-REMAINING-XXX
|
||||
anthropic-ratelimit-input-tokens-reset:
|
||||
- ANTHROPIC-RATELIMIT-INPUT-TOKENS-RESET-XXX
|
||||
anthropic-ratelimit-output-tokens-limit:
|
||||
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-LIMIT-XXX
|
||||
anthropic-ratelimit-output-tokens-remaining:
|
||||
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-REMAINING-XXX
|
||||
anthropic-ratelimit-output-tokens-reset:
|
||||
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-RESET-XXX
|
||||
anthropic-ratelimit-tokens-limit:
|
||||
- ANTHROPIC-RATELIMIT-TOKENS-LIMIT-XXX
|
||||
anthropic-ratelimit-tokens-remaining:
|
||||
- ANTHROPIC-RATELIMIT-TOKENS-REMAINING-XXX
|
||||
anthropic-ratelimit-tokens-reset:
|
||||
- ANTHROPIC-RATELIMIT-TOKENS-RESET-XXX
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
request-id:
|
||||
- REQUEST-ID-XXX
|
||||
strict-transport-security:
|
||||
- STS-XXX
|
||||
x-envoy-upstream-service-time:
|
||||
- '5453'
|
||||
- '100630'
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
|
||||
@@ -4,9 +4,8 @@ interactions:
|
||||
personal goal is: test goal"},{"role":"user","content":"\nCurrent Task: The
|
||||
final answer is 42. But don''t give it yet, instead keep using the `get_final_answer`
|
||||
tool.\n\nThis is the expected criteria for your final answer: The final answer\nyou
|
||||
MUST return the actual complete content as the final answer, not a summary.\n\nThis
|
||||
is VERY important to you, your job depends on it!"}],"model":"gpt-4.1-mini","tool_choice":"auto","tools":[{"type":"function","function":{"name":"get_final_answer","description":"Get
|
||||
the final answer but don''t give it yet, just re-use this\ntool non-stop.","parameters":{"properties":{},"type":"object"}}}]}'
|
||||
MUST return the actual complete content as the final answer, not a summary."}],"model":"gpt-4.1-mini","tool_choice":"auto","tools":[{"type":"function","function":{"name":"get_final_answer","description":"Get
|
||||
the final answer but don''t give it yet, just re-use this\ntool non-stop.","strict":true,"parameters":{"properties":{},"type":"object","additionalProperties":false,"required":[]}}}]}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
@@ -19,7 +18,7 @@ interactions:
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '716'
|
||||
- '715'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
@@ -33,7 +32,7 @@ interactions:
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 1.83.0
|
||||
- 2.32.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
x-stainless-retry-count:
|
||||
@@ -46,34 +45,34 @@ interactions:
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"id\": \"chatcmpl-D0tOle0pg0F6zmEmkzpoufrjhkjn5\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1769105323,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n
|
||||
string: "{\n \"id\": \"chatcmpl-Dd0fdKJb6WSBc7P3rJxJVnCq2vvRD\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1778189741,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n
|
||||
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
||||
\"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n
|
||||
\ \"id\": \"call_BM9xxRm0ADf91mYTDZ4kKExm\",\n \"type\":
|
||||
\ \"id\": \"call_csVoybNhrmr6ORevkL4wQfVy\",\n \"type\":
|
||||
\"function\",\n \"function\": {\n \"name\": \"get_final_answer\",\n
|
||||
\ \"arguments\": \"{}\"\n }\n }\n ],\n
|
||||
\ \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\":
|
||||
null,\n \"finish_reason\": \"tool_calls\"\n }\n ],\n \"usage\":
|
||||
{\n \"prompt_tokens\": 140,\n \"completion_tokens\": 11,\n \"total_tokens\":
|
||||
151,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 0,\n \"audio_tokens\":
|
||||
{\n \"prompt_tokens\": 127,\n \"completion_tokens\": 11,\n \"total_tokens\":
|
||||
138,\n \"prompt_tokens_details\": {\n \"cached_tokens\": 0,\n \"audio_tokens\":
|
||||
0\n },\n \"completion_tokens_details\": {\n \"reasoning_tokens\":
|
||||
0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n
|
||||
\ \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
|
||||
\"default\",\n \"system_fingerprint\": \"fp_376a7ccef1\"\n}\n"
|
||||
\"default\",\n \"system_fingerprint\": \"fp_f7311f7f0a\"\n}\n"
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-Ray:
|
||||
- 9f835b12bbeaeb2a-SJC
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Thu, 22 Jan 2026 18:08:44 GMT
|
||||
- Thu, 07 May 2026 21:35:41 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- SET-COOKIE-XXX
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
Transfer-Encoding:
|
||||
@@ -84,18 +83,16 @@ interactions:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '373'
|
||||
- '570'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '651'
|
||||
set-cookie:
|
||||
- SET-COOKIE-XXX
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
@@ -120,10 +117,7 @@ interactions:
|
||||
personal goal is: test goal"},{"role":"user","content":"\nCurrent Task: The
|
||||
final answer is 42. But don''t give it yet, instead keep using the `get_final_answer`
|
||||
tool.\n\nThis is the expected criteria for your final answer: The final answer\nyou
|
||||
MUST return the actual complete content as the final answer, not a summary.\n\nThis
|
||||
is VERY important to you, your job depends on it!"},{"role":"assistant","content":null,"tool_calls":[{"id":"call_BM9xxRm0ADf91mYTDZ4kKExm","type":"function","function":{"name":"get_final_answer","arguments":"{}"}}]},{"role":"tool","tool_call_id":"call_BM9xxRm0ADf91mYTDZ4kKExm","content":"42"},{"role":"user","content":"Analyze
|
||||
the tool result. If requirements are met, provide the Final Answer. Otherwise,
|
||||
call the next tool. Deliver only the answer without meta-commentary."},{"role":"assistant","content":"Now
|
||||
MUST return the actual complete content as the final answer, not a summary."},{"role":"assistant","content":null,"tool_calls":[{"id":"call_csVoybNhrmr6ORevkL4wQfVy","type":"function","function":{"name":"get_final_answer","arguments":"{}"}}]},{"role":"tool","tool_call_id":"call_csVoybNhrmr6ORevkL4wQfVy","name":"get_final_answer","content":"42"},{"role":"assistant","content":"Now
|
||||
it''s time you MUST give your absolute best final answer. You''ll ignore all
|
||||
previous instructions, stop using any tools, and just return your absolute BEST
|
||||
Final answer."}],"model":"gpt-4.1-mini"}'
|
||||
@@ -139,7 +133,7 @@ interactions:
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '1118'
|
||||
- '902'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
@@ -155,7 +149,7 @@ interactions:
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 1.83.0
|
||||
- 2.32.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
x-stainless-retry-count:
|
||||
@@ -168,26 +162,28 @@ interactions:
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"id\": \"chatcmpl-D0tOmVwqqvewf7s2CNMsKBksanbID\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1769105324,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n
|
||||
string: "{\n \"id\": \"chatcmpl-Dd0fed0I3y5RVgM0YZTzjD6hdh8Tr\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1778189742,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n
|
||||
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
||||
\"assistant\",\n \"content\": \"42\",\n \"refusal\": null,\n
|
||||
\ \"annotations\": []\n },\n \"logprobs\": null,\n \"finish_reason\":
|
||||
\"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 190,\n \"completion_tokens\":
|
||||
1,\n \"total_tokens\": 191,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
|
||||
\"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 142,\n \"completion_tokens\":
|
||||
1,\n \"total_tokens\": 143,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
|
||||
0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
|
||||
{\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
|
||||
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
|
||||
\"default\",\n \"system_fingerprint\": \"fp_376a7ccef1\"\n}\n"
|
||||
\"default\",\n \"system_fingerprint\": \"fp_31316814ed\"\n}\n"
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-Ray:
|
||||
- 9f835b1e3c58eb2a-SJC
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Thu, 22 Jan 2026 18:08:44 GMT
|
||||
- Thu, 07 May 2026 21:35:42 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
@@ -200,18 +196,14 @@ interactions:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '166'
|
||||
- '478'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '180'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
196
lib/crewai/tests/llms/test_prompt_cache.py
Normal file
196
lib/crewai/tests/llms/test_prompt_cache.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""Regression tests for the provider-agnostic prompt-cache breakpoint flag."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from crewai.llms.cache import (
|
||||
CACHE_BREAKPOINT_KEY,
|
||||
mark_cache_breakpoint,
|
||||
strip_cache_breakpoint,
|
||||
)
|
||||
from crewai.llms.providers.anthropic.completion import AnthropicCompletion
|
||||
from crewai.llms.providers.openai.completion import OpenAICompletion
|
||||
|
||||
|
||||
class TestCacheMarkerHelpers:
|
||||
def test_mark_returns_new_dict(self) -> None:
|
||||
original = {"role": "user", "content": "hi"}
|
||||
marked = mark_cache_breakpoint(original)
|
||||
assert marked[CACHE_BREAKPOINT_KEY] is True
|
||||
# Marker must NOT bleed back into the caller's dict — callers may
|
||||
# pass literal dicts and reuse them across calls.
|
||||
assert CACHE_BREAKPOINT_KEY not in original
|
||||
|
||||
def test_strip_is_idempotent(self) -> None:
|
||||
msg = {"role": "user", "content": "hi", CACHE_BREAKPOINT_KEY: True}
|
||||
strip_cache_breakpoint(msg)
|
||||
assert CACHE_BREAKPOINT_KEY not in msg
|
||||
strip_cache_breakpoint(msg)
|
||||
assert CACHE_BREAKPOINT_KEY not in msg
|
||||
|
||||
|
||||
class TestBaseFormatDoesNotMutate:
|
||||
"""The strip-on-format pass must not erase markers from the caller's
|
||||
messages list — executors reuse a single list across many LLM calls,
|
||||
and mutating it would defeat caching on every iteration after the first.
|
||||
"""
|
||||
|
||||
def test_repeated_format_preserves_markers(self) -> None:
|
||||
llm = OpenAICompletion(model="gpt-4o-mini")
|
||||
messages = [
|
||||
mark_cache_breakpoint({"role": "system", "content": "stable system"}),
|
||||
mark_cache_breakpoint({"role": "user", "content": "stable user"}),
|
||||
]
|
||||
# First call: provider strips markers from the returned (copied) list
|
||||
first = llm._format_messages(messages)
|
||||
assert all(CACHE_BREAKPOINT_KEY not in m for m in first)
|
||||
# Original list must STILL carry the markers
|
||||
assert messages[0][CACHE_BREAKPOINT_KEY] is True
|
||||
assert messages[1][CACHE_BREAKPOINT_KEY] is True
|
||||
# Second call from the same list still sees the markers
|
||||
second = llm._format_messages(messages)
|
||||
assert all(CACHE_BREAKPOINT_KEY not in m for m in second)
|
||||
assert messages[0][CACHE_BREAKPOINT_KEY] is True
|
||||
assert messages[1][CACHE_BREAKPOINT_KEY] is True
|
||||
|
||||
|
||||
class TestAnthropicCacheStamping:
|
||||
def test_stamps_system_with_cache_control(self) -> None:
|
||||
llm = AnthropicCompletion(model="claude-sonnet-4-5")
|
||||
messages = [
|
||||
mark_cache_breakpoint({"role": "system", "content": "you are helpful"}),
|
||||
mark_cache_breakpoint({"role": "user", "content": "ping"}),
|
||||
]
|
||||
formatted, system = llm._format_messages_for_anthropic(messages)
|
||||
assert isinstance(system, list)
|
||||
assert system[0]["cache_control"] == {"type": "ephemeral"}
|
||||
assert system[0]["text"] == "you are helpful"
|
||||
# First user block carries cache_control too
|
||||
last_block = formatted[0]["content"][-1]
|
||||
assert last_block["cache_control"] == {"type": "ephemeral"}
|
||||
|
||||
def test_stamps_stable_user_not_tool_result(self) -> None:
|
||||
"""Within a ReAct loop, tool results are flattened into a trailing
|
||||
user message. We must NOT stamp that volatile trailing block — we
|
||||
must stamp the original stable user prompt instead.
|
||||
"""
|
||||
llm = AnthropicCompletion(model="claude-sonnet-4-5")
|
||||
messages = [
|
||||
mark_cache_breakpoint({"role": "system", "content": "you are helpful"}),
|
||||
mark_cache_breakpoint({"role": "user", "content": "stable task prompt"}),
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "tc_1",
|
||||
"function": {"name": "ping", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "tc_1", "content": "volatile tool result"},
|
||||
]
|
||||
formatted, _system = llm._format_messages_for_anthropic(messages)
|
||||
# Find the message that holds the stable prompt
|
||||
stable = next(
|
||||
fm
|
||||
for fm in formatted
|
||||
if fm["role"] == "user"
|
||||
and isinstance(fm["content"], list)
|
||||
and any(
|
||||
isinstance(b, dict)
|
||||
and b.get("type") == "text"
|
||||
and b.get("text") == "stable task prompt"
|
||||
for b in fm["content"]
|
||||
)
|
||||
)
|
||||
text_block = next(
|
||||
b for b in stable["content"] if isinstance(b, dict) and b.get("type") == "text"
|
||||
)
|
||||
assert text_block.get("cache_control") == {"type": "ephemeral"}
|
||||
# The tool_result-bearing user message must NOT be stamped
|
||||
tool_carrier = next(
|
||||
fm
|
||||
for fm in formatted
|
||||
if fm["role"] == "user"
|
||||
and isinstance(fm["content"], list)
|
||||
and any(
|
||||
isinstance(b, dict) and b.get("type") == "tool_result"
|
||||
for b in fm["content"]
|
||||
)
|
||||
)
|
||||
for block in tool_carrier["content"]:
|
||||
assert "cache_control" not in block
|
||||
|
||||
def test_assistant_marker_is_ignored(self) -> None:
|
||||
"""Markers on assistant messages have no stable stamp target after
|
||||
Anthropic's role coalescing, so they should be silently ignored
|
||||
rather than collected and then dropped on a mismatch.
|
||||
"""
|
||||
llm = AnthropicCompletion(model="claude-sonnet-4-5")
|
||||
messages = [
|
||||
mark_cache_breakpoint({"role": "system", "content": "you are helpful"}),
|
||||
mark_cache_breakpoint(
|
||||
{"role": "assistant", "content": "I will help you out."}
|
||||
),
|
||||
{"role": "user", "content": "ping"},
|
||||
]
|
||||
formatted, system = llm._format_messages_for_anthropic(messages)
|
||||
# System still cached
|
||||
assert isinstance(system, list)
|
||||
# No user message was marked → no user message should carry cache_control
|
||||
for fm in formatted:
|
||||
if fm.get("role") != "user":
|
||||
continue
|
||||
content = fm.get("content")
|
||||
if isinstance(content, list):
|
||||
for block in content:
|
||||
if isinstance(block, dict):
|
||||
assert "cache_control" not in block
|
||||
|
||||
def test_list_content_user_marker_matches(self) -> None:
|
||||
"""A pre-formatted user message with a single text block should still
|
||||
match against the post-format user message.
|
||||
"""
|
||||
llm = AnthropicCompletion(model="claude-sonnet-4-5")
|
||||
messages = [
|
||||
mark_cache_breakpoint(
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"type": "text", "text": "stable list prompt"}],
|
||||
}
|
||||
),
|
||||
]
|
||||
formatted, _system = llm._format_messages_for_anthropic(messages)
|
||||
user_msg = next(fm for fm in formatted if fm["role"] == "user")
|
||||
content = user_msg["content"]
|
||||
assert isinstance(content, list)
|
||||
text_block = next(b for b in content if isinstance(b, dict) and b.get("type") == "text")
|
||||
assert text_block.get("cache_control") == {"type": "ephemeral"}
|
||||
|
||||
def test_unmarked_messages_get_no_cache_control(self) -> None:
|
||||
llm = AnthropicCompletion(model="claude-sonnet-4-5")
|
||||
messages = [
|
||||
{"role": "system", "content": "no caching here"},
|
||||
{"role": "user", "content": "no caching here either"},
|
||||
]
|
||||
formatted, system = llm._format_messages_for_anthropic(messages)
|
||||
# No marker → system stays a plain string (no content-block conversion)
|
||||
assert isinstance(system, str)
|
||||
# No marker → no cache_control anywhere in formatted messages
|
||||
for fm in formatted:
|
||||
content = fm.get("content")
|
||||
if isinstance(content, list):
|
||||
for block in content:
|
||||
assert "cache_control" not in block
|
||||
|
||||
|
||||
class TestNonAnthropicStripsMarker:
|
||||
def test_openai_format_strips_marker_from_wire_payload(self) -> None:
|
||||
llm = OpenAICompletion(model="gpt-4o-mini")
|
||||
messages = [
|
||||
mark_cache_breakpoint({"role": "system", "content": "stable"}),
|
||||
mark_cache_breakpoint({"role": "user", "content": "hi"}),
|
||||
]
|
||||
formatted = llm._format_messages(messages)
|
||||
for m in formatted:
|
||||
assert CACHE_BREAKPOINT_KEY not in m
|
||||
@@ -5,9 +5,9 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from crewai import Agent
|
||||
from crewai.agent.utils import append_skill_context
|
||||
from crewai.skills.loader import activate_skill, discover_skills, format_skill_context
|
||||
from crewai.skills.models import INSTRUCTIONS, METADATA
|
||||
from crewai.utilities.prompts import Prompts
|
||||
|
||||
|
||||
def _create_skill_dir(parent: Path, name: str, body: str = "Body.") -> Path:
|
||||
@@ -34,7 +34,7 @@ class TestSkillDiscoveryAndActivation:
|
||||
assert activated.instructions == "Use this skill."
|
||||
|
||||
context = format_skill_context(activated)
|
||||
assert "## Skill: my-skill" in context
|
||||
assert '<skill name="my-skill">' in context
|
||||
assert "Use this skill." in context
|
||||
|
||||
def test_filter_by_skill_names(self, tmp_path: Path) -> None:
|
||||
@@ -94,7 +94,9 @@ class TestSkillDiscoveryAndActivation:
|
||||
assert agent.skills[0].disclosure_level == METADATA
|
||||
assert agent.skills[0].instructions is None
|
||||
|
||||
prompt = append_skill_context(agent, "Plan a 10-day Japan itinerary.")
|
||||
assert "## Skill: travel" in prompt
|
||||
assert "Skill travel" in prompt
|
||||
assert "Use this skill for travel planning." not in prompt
|
||||
result = Prompts(agent=agent, has_tools=False, use_system_prompt=True).task_execution()
|
||||
system = getattr(result, "system", "") or result.prompt
|
||||
assert '<skill name="travel">' in system
|
||||
assert "Skill travel" in system
|
||||
# METADATA-level skills must not leak full instructions into the prompt
|
||||
assert "Use this skill for travel planning." not in system
|
||||
|
||||
@@ -105,7 +105,7 @@ class TestFormatSkillContext:
|
||||
frontmatter=fm, path=tmp_path, disclosure_level=METADATA
|
||||
)
|
||||
ctx = format_skill_context(skill)
|
||||
assert "## Skill: test-skill" in ctx
|
||||
assert '<skill name="test-skill">' in ctx
|
||||
assert "A skill" in ctx
|
||||
|
||||
def test_instructions_level(self, tmp_path: Path) -> None:
|
||||
@@ -117,7 +117,7 @@ class TestFormatSkillContext:
|
||||
instructions="Do these things.",
|
||||
)
|
||||
ctx = format_skill_context(skill)
|
||||
assert "## Skill: test-skill" in ctx
|
||||
assert '<skill name="test-skill">' in ctx
|
||||
assert "Do these things." in ctx
|
||||
|
||||
def test_no_instructions_at_instructions_level(self, tmp_path: Path) -> None:
|
||||
@@ -129,7 +129,7 @@ class TestFormatSkillContext:
|
||||
instructions=None,
|
||||
)
|
||||
ctx = format_skill_context(skill)
|
||||
assert ctx == "## Skill: test-skill\nA skill"
|
||||
assert ctx == '<skill name="test-skill">\nA skill\n</skill>'
|
||||
|
||||
def test_resources_level(self, tmp_path: Path) -> None:
|
||||
fm = SkillFrontmatter(name="test-skill", description="A skill")
|
||||
|
||||
@@ -256,6 +256,11 @@ def test_multiple_crews_in_flow_span_lifecycle():
|
||||
mock_llm_2.call.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.skip(
|
||||
reason="Sync Agent.execute_task does not await AgentExecutor.invoke when invoke "
|
||||
"auto-returns a coroutine inside an async flow. Needs a fix in agent/core.py "
|
||||
"_execute_without_timeout (out of scope for this test cleanup pass)."
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_crew_execution_span_in_async_flow():
|
||||
"""Test that crew execution spans work in async flow methods.
|
||||
|
||||
@@ -2990,6 +2990,12 @@ def test_manager_agent_with_tools_raises_exception(researcher, writer):
|
||||
crew.kickoff()
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
strict=True,
|
||||
reason="crew.train() relies on CrewAgentExecutor._format_feedback_message; "
|
||||
"AgentExecutor (the new default) does not implement training feedback yet. "
|
||||
"Remove this xfail once training is migrated to AgentExecutor.",
|
||||
)
|
||||
@pytest.mark.vcr()
|
||||
def test_crew_train_success(researcher, writer, monkeypatch):
|
||||
task = Task(
|
||||
|
||||
@@ -346,12 +346,14 @@ def test_agent_emits_execution_error_event(base_agent, base_task):
|
||||
received_events.append(event)
|
||||
event_received.set()
|
||||
|
||||
from crewai.experimental.agent_executor import AgentExecutor
|
||||
|
||||
error_message = "Error happening while sending prompt to model."
|
||||
base_agent.max_retry_limit = 0
|
||||
|
||||
# Patch at the class level since agent_executor is created lazily
|
||||
with patch.object(
|
||||
CrewAgentExecutor, "invoke", side_effect=Exception(error_message)
|
||||
AgentExecutor, "invoke", side_effect=Exception(error_message)
|
||||
):
|
||||
with pytest.raises(Exception): # noqa: B017
|
||||
base_agent.execute_task(
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.14.5a4"
|
||||
__version__ = "1.14.5a5"
|
||||
|
||||
@@ -323,8 +323,11 @@ def update_pyproject_version(file_path: Path, new_version: str) -> bool:
|
||||
|
||||
_DEFAULT_WORKSPACE_PACKAGES: Final[list[str]] = [
|
||||
"crewai",
|
||||
"crewai-tools",
|
||||
"crewai-cli",
|
||||
"crewai-core",
|
||||
"crewai-devtools",
|
||||
"crewai-files",
|
||||
"crewai-tools",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
from crewai_devtools.cli import (
|
||||
_DEFAULT_WORKSPACE_PACKAGES,
|
||||
_pin_crewai_deps,
|
||||
_repin_crewai_install,
|
||||
update_pyproject_dependencies,
|
||||
update_pyproject_version,
|
||||
update_template_dependencies,
|
||||
)
|
||||
@@ -226,6 +228,79 @@ class TestRepinCrewaiInstall:
|
||||
assert _repin_crewai_install(cmd, "2.0.0") == cmd
|
||||
|
||||
|
||||
# --- update_pyproject_dependencies ---
|
||||
|
||||
|
||||
class TestUpdatePyprojectDependencies:
|
||||
def test_default_packages_cover_all_workspace_members(self) -> None:
|
||||
"""Every workspace member must be in the default rewrite list.
|
||||
|
||||
Without this, a version bump silently leaves stale pins behind for any
|
||||
workspace package missing from the list (see incident with 1.14.5a5).
|
||||
"""
|
||||
import tomlkit
|
||||
|
||||
workspace_root = Path(__file__).resolve().parents[3]
|
||||
root_pyproject = (workspace_root / "pyproject.toml").read_text()
|
||||
|
||||
members = tomlkit.parse(root_pyproject)["tool"]["uv"]["workspace"]["members"]
|
||||
expected = {
|
||||
tomlkit.parse((workspace_root / m / "pyproject.toml").read_text())[
|
||||
"project"
|
||||
]["name"]
|
||||
for m in members
|
||||
}
|
||||
|
||||
assert expected.issubset(set(_DEFAULT_WORKSPACE_PACKAGES))
|
||||
|
||||
def test_rewrites_all_workspace_pins(self, tmp_path: Path) -> None:
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
pyproject.write_text(
|
||||
dedent("""\
|
||||
[project]
|
||||
dependencies = [
|
||||
"crewai-core==1.0.0",
|
||||
"crewai-cli==1.0.0",
|
||||
"requests>=2.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.0.0",
|
||||
]
|
||||
files = [
|
||||
"crewai-files==1.0.0",
|
||||
]
|
||||
""")
|
||||
)
|
||||
|
||||
assert update_pyproject_dependencies(pyproject, "2.0.0") is True
|
||||
result = pyproject.read_text()
|
||||
assert '"crewai-core==2.0.0"' in result
|
||||
assert '"crewai-cli==2.0.0"' in result
|
||||
assert '"crewai-tools==2.0.0"' in result
|
||||
assert '"crewai-files==2.0.0"' in result
|
||||
assert '"requests>=2.0"' in result
|
||||
|
||||
def test_leaves_bare_crewai_pin_alone(self, tmp_path: Path) -> None:
|
||||
"""`crewai==` must not collide with `crewai-core==` etc."""
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
pyproject.write_text(
|
||||
dedent("""\
|
||||
[project]
|
||||
dependencies = [
|
||||
"crewai==1.0.0",
|
||||
"crewai-core==1.0.0",
|
||||
]
|
||||
""")
|
||||
)
|
||||
|
||||
update_pyproject_dependencies(pyproject, "2.0.0")
|
||||
result = pyproject.read_text()
|
||||
assert '"crewai==2.0.0"' in result
|
||||
assert '"crewai-core==2.0.0"' in result
|
||||
|
||||
|
||||
# --- update_template_dependencies ---
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user