Compare commits

..

5 Commits

Author SHA1 Message Date
lorenzejay
584baaef55 Update Gemini agent tests to include structured output with thoughts and bump model version to 2.5-flash 2026-05-21 17:56:09 -07:00
lorenzejay
8b2e7e6004 drop scripts 2026-05-21 16:34:43 -07:00
lorenzejay
1700a38fd2 Merge branch 'main' of github.com:crewAIInc/crewAI into codex/fix-oss-47-structured-output-tools 2026-05-21 16:11:36 -07:00
lorenzejay
3349cfcb92 addressing comments 2026-05-21 16:11:15 -07:00
lorenzejay
4b08a8308d Fix structured output leaks in tool-calling loops 2026-05-21 15:33:22 -07:00
53 changed files with 742 additions and 525 deletions

View File

@@ -8,6 +8,7 @@ from crewai_cli.utils import copy_template
def add_crew_to_flow(crew_name: str) -> None:
"""Add a new crew to the current flow."""
# Check if pyproject.toml exists in the current directory
if not Path("pyproject.toml").exists():
PRINTER.print(
"This command must be run from the root of a flow project.", color="red"
@@ -16,6 +17,7 @@ def add_crew_to_flow(crew_name: str) -> None:
"This command must be run from the root of a flow project."
)
# Determine the flow folder based on the current directory
flow_folder = Path.cwd()
crews_folder = flow_folder / "src" / flow_folder.name / "crews"
@@ -23,6 +25,7 @@ def add_crew_to_flow(crew_name: str) -> None:
PRINTER.print("Crews folder does not exist in the current flow.", color="red")
raise click.ClickException("Crews folder does not exist in the current flow.")
# Create the crew within the flow's crews directory
create_embedded_crew(crew_name, parent_folder=crews_folder)
click.echo(
@@ -48,12 +51,13 @@ def create_embedded_crew(crew_name: str, parent_folder: Path) -> None:
click.secho(f"Creating crew {folder_name}...", fg="green", bold=True)
crew_folder.mkdir(parents=True)
# Create config and crew.py files
config_folder = crew_folder / "config"
config_folder.mkdir(exist_ok=True)
templates_dir = Path(__file__).parent / "templates" / "crew"
config_template_files = ["agents.yaml", "tasks.yaml"]
crew_template_file = f"{folder_name}.py"
crew_template_file = f"{folder_name}.py" # Updated file name
for file_name in config_template_files:
src_file = templates_dir / "config" / file_name

View File

@@ -222,6 +222,9 @@ def _entity_summary(entities: list[dict[str, Any]]) -> str:
return ", ".join(parts) if parts else "empty"
# --- JSON directory ---
def _list_json(location: str) -> list[dict[str, Any]]:
pattern = os.path.join(location, "**", "*.json")
results = []
@@ -272,6 +275,9 @@ def _info_json_file(path: str) -> dict[str, Any]:
return meta
# --- SQLite ---
def _list_sqlite(db_path: str) -> list[dict[str, Any]]:
results = []
with sqlite3.connect(db_path) as conn:
@@ -321,6 +327,9 @@ def _info_sqlite_id(db_path: str, checkpoint_id: str) -> dict[str, Any] | None:
return meta
# --- Public API ---
def list_checkpoints(location: str) -> None:
"""List all checkpoints at a location."""
if _is_sqlite(location):
@@ -358,6 +367,7 @@ def info_checkpoint(path: str) -> None:
"""Show details of a single checkpoint."""
meta: dict[str, Any] | None = None
# db_path#checkpoint_id format
if "#" in path:
db_path, checkpoint_id = path.rsplit("#", 1)
if _is_sqlite(db_path):
@@ -366,6 +376,7 @@ def info_checkpoint(path: str) -> None:
click.echo(f"Checkpoint not found: {checkpoint_id}")
return
# SQLite file — show latest
if meta is None and _is_sqlite(path):
meta = _info_sqlite_latest(path)
if not meta:
@@ -373,6 +384,7 @@ def info_checkpoint(path: str) -> None:
return
click.echo(f"Latest checkpoint: {meta['name']}\n")
# Directory — show latest JSON
if meta is None and os.path.isdir(path):
meta = _info_json_latest(path)
if not meta:
@@ -380,6 +392,7 @@ def info_checkpoint(path: str) -> None:
return
click.echo(f"Latest checkpoint: {meta['name']}\n")
# Specific JSON file
if meta is None and os.path.isfile(path):
try:
meta = _info_json_file(path)

View File

@@ -320,6 +320,8 @@ class CheckpointTUI(App[_TuiResult]):
self._refresh_tree()
self.query_one("#tree-panel", Tree).root.expand()
# ── Tree building ──────────────────────────────────────────────
@staticmethod
def _top_level_entity(entry: dict[str, Any]) -> tuple[str, str]:
etype, ename = "unknown", ""
@@ -471,6 +473,8 @@ class CheckpointTUI(App[_TuiResult]):
self.sub_title = self._location
self.query_one("#status", Static).update(f" {count} checkpoint(s) | {storage}")
# ── Detail panel ───────────────────────────────────────────────
async def _clear_scroll(self, tab_id: str) -> VerticalScroll:
tab = self.query_one(f"#{tab_id}", TabPane)
scroll = tab.query_one(VerticalScroll)
@@ -657,6 +661,8 @@ class CheckpointTUI(App[_TuiResult]):
)
await scroll.mount(row)
# ── Data collection ────────────────────────────────────────────
def _collect_inputs(self) -> dict[str, Any] | None:
if not self._input_keys:
return None
@@ -693,6 +699,8 @@ class CheckpointTUI(App[_TuiResult]):
return f"{self._location}#{entry['name']}"
return str(entry.get("name", ""))
# ── Events ─────────────────────────────────────────────────────
async def on_tree_node_highlighted(
self, event: Tree.NodeHighlighted[dict[str, Any]]
) -> None:

View File

@@ -42,6 +42,7 @@ from crewai_cli.utils import build_env_with_all_tool_credentials, read_toml
def _get_cli_version() -> str:
"""Return the best available version string for the CLI."""
# Prefer crewai version if installed (keeps existing UX)
try:
return get_version("crewai")
except Exception: # noqa: S110
@@ -66,6 +67,7 @@ def crewai() -> None:
def uv(uv_args: tuple[str, ...]) -> None:
"""A wrapper around uv commands that adds custom tool authentication through env vars."""
try:
# Verify pyproject.toml exists first
read_toml()
except FileNotFoundError as e:
raise SystemExit(
@@ -319,6 +321,7 @@ def memory(
)
raise SystemExit(1) from exc
# Build embedder spec from CLI flags.
embedder_spec: dict[str, Any] | None = None
if embedder_config:
import json as _json
@@ -432,6 +435,7 @@ def logout(reset: bool) -> None:
click.echo("Successfully logged out from CrewAI AMP.")
# DEPLOY CREWAI+ COMMANDS
@crewai.group()
def deploy() -> None:
"""Deploy the Crew CLI group."""
@@ -762,14 +766,17 @@ def env_view() -> None:
console = Console()
# Check for .env file
env_file = Path(".env")
env_file_exists = env_file.exists()
# Create table for environment variables
table = Table(show_header=True, header_style="bold cyan", expand=True)
table.add_column("Environment Variable", style="cyan", width=30)
table.add_column("Value", style="white", width=20)
table.add_column("Source", style="yellow", width=20)
# Check CREWAI_TRACING_ENABLED
crewai_tracing = os.getenv("CREWAI_TRACING_ENABLED", "")
if crewai_tracing:
table.add_row(
@@ -784,6 +791,7 @@ def env_view() -> None:
"[dim]—[/dim]",
)
# Check other related env vars
crewai_testing = os.getenv("CREWAI_TESTING", "")
if crewai_testing:
table.add_row("CREWAI_TESTING", crewai_testing, "Environment/Shell")
@@ -796,6 +804,7 @@ def env_view() -> None:
if crewai_org_id:
table.add_row("CREWAI_ORG_ID", crewai_org_id, "Environment/Shell")
# Check if .env file exists
table.add_row(
".env file",
"✅ Found" if env_file_exists else "❌ Not found",
@@ -811,6 +820,7 @@ def env_view() -> None:
console.print("\n")
console.print(panel)
# Show helpful message
if env_file_exists:
console.print(
"\n[dim]💡 Tip: To enable tracing via .env, add: CREWAI_TRACING_ENABLED=true[/dim]"
@@ -886,9 +896,11 @@ def traces_status() -> None:
table.add_column("Setting", style="cyan")
table.add_column("Value", style="white")
# Check environment variable
env_enabled = os.getenv("CREWAI_TRACING_ENABLED", "false")
table.add_row("CREWAI_TRACING_ENABLED", env_enabled)
# Check user consent
trace_consent = user_data.get("trace_consent")
if trace_consent is True:
consent_status = "✅ Enabled (user consented)"
@@ -898,6 +910,7 @@ def traces_status() -> None:
consent_status = "⚪ Not set (first-time user)"
table.add_row("User Consent", consent_status)
# Check overall status
if is_tracing_enabled():
overall_status = "✅ ENABLED"
border_style = "green"

View File

@@ -50,6 +50,7 @@ def create_folder_structure(
folder_name = name.replace(" ", "_").replace("-", "_").lower()
folder_name = re.sub(r"[^a-zA-Z0-9_]", "", folder_name)
# Check if the name starts with invalid characters or is primarily invalid
if re.match(r"^[^a-zA-Z0-9_-]+", name):
raise ValueError(
f"Project name '{name}' contains no valid characters for a Python module name"
@@ -97,6 +98,7 @@ def create_folder_structure(
f"Project name '{name}' would generate class name '{class_name}' which cannot start with a digit"
)
# Check if the original name (before title casing) is a keyword
original_name_clean = re.sub(
r"[^a-zA-Z0-9_]", "", name.replace("_", "").replace("-", "").lower()
)
@@ -126,7 +128,7 @@ def create_folder_structure(
click.secho("Operation cancelled.", fg="yellow")
sys.exit(0)
click.secho(f"Overriding folder {folder_name}...", fg="green", bold=True)
shutil.rmtree(folder_path)
shutil.rmtree(folder_path) # Delete the existing folder and its contents
click.secho(
f"Creating {'crew' if parent_folder else 'folder'} {folder_name}...",
@@ -142,6 +144,7 @@ def create_folder_structure(
(folder_path / "src" / folder_name / "tools").mkdir(parents=True)
(folder_path / "src" / folder_name / "config").mkdir(parents=True)
# Copy AGENTS.md to project root (top-level projects only)
package_dir = Path(__file__).parent
agents_md_src = package_dir / "templates" / "AGENTS.md"
if agents_md_src.exists():
@@ -229,22 +232,25 @@ def create_crew(
while True:
selected_provider = select_provider(provider_models)
if selected_provider is None:
if selected_provider is None: # User typed 'q'
click.secho("Exiting...", fg="yellow")
sys.exit(0)
if selected_provider and isinstance(selected_provider, str):
if selected_provider and isinstance(
selected_provider, str
): # Valid selection
break
click.secho(
"No provider selected. Please try again or press 'q' to exit.", fg="red"
)
# Check if the selected provider has predefined models
if MODELS.get(selected_provider):
while True:
selected_model = select_model(selected_provider, provider_models)
if selected_model is None:
if selected_model is None: # User typed 'q'
click.secho("Exiting...", fg="yellow")
sys.exit(0)
if selected_model:
if selected_model: # Valid selection
break
click.secho(
"No model selected. Please try again or press 'q' to exit.",
@@ -252,14 +258,17 @@ def create_crew(
)
env_vars["MODEL"] = selected_model
# Check if the selected provider requires API keys
if selected_provider in ENV_VARS:
provider_env_vars = ENV_VARS[selected_provider]
for details in provider_env_vars:
if details.get("default", False):
# Automatically add default key-value pairs
for key, value in details.items():
if key not in ["prompt", "key_name", "default"]:
env_vars[key] = value
elif "key_name" in details:
# Prompt for non-default key-value pairs
prompt = details["prompt"]
key_name = details["key_name"]
api_key_value = click.prompt(prompt, default="", show_default=False)

View File

@@ -20,21 +20,25 @@ def create_flow(name: str) -> None:
telemetry = Telemetry()
telemetry.flow_creation_span(class_name)
# Create directory structure
(project_root / "src" / folder_name).mkdir(parents=True)
(project_root / "src" / folder_name / "crews").mkdir(parents=True)
(project_root / "src" / folder_name / "tools").mkdir(parents=True)
(project_root / "tests").mkdir(exist_ok=True)
# Create .env file
with open(project_root / ".env", "w") as file:
file.write("OPENAI_API_KEY=YOUR_API_KEY")
package_dir = Path(__file__).parent
templates_dir = package_dir / "templates" / "flow"
# Copy AGENTS.md to project root
agents_md_src = package_dir / "templates" / "AGENTS.md"
if agents_md_src.exists():
shutil.copy2(agents_md_src, project_root / "AGENTS.md")
# List of template files to copy
root_template_files = [".gitignore", "pyproject.toml", "README.md"]
src_template_files = ["__init__.py", "main.py"]
tools_template_files = ["tools/__init__.py", "tools/custom_tool.py"]
@@ -61,21 +65,25 @@ def create_flow(name: str) -> None:
with open(dst_file, "w") as file:
file.write(content)
# Copy and process root template files
for file_name in root_template_files:
src_file = templates_dir / file_name
dst_file = project_root / file_name
process_file(src_file, dst_file)
# Copy and process src template files
for file_name in src_template_files:
src_file = templates_dir / file_name
dst_file = project_root / "src" / folder_name / file_name
process_file(src_file, dst_file)
# Copy tools files
for file_name in tools_template_files:
src_file = templates_dir / file_name
dst_file = project_root / "src" / folder_name / file_name
process_file(src_file, dst_file)
# Copy crew folders
for crew_folder in crew_folders:
src_crew_folder = templates_dir / "crews" / crew_folder
dst_crew_folder = project_root / "src" / folder_name / "crews" / crew_folder

View File

@@ -74,6 +74,7 @@ class ValidationResult:
hint: str = ""
# Maps known provider env var names → label used in hint messages.
_KNOWN_API_KEY_HINTS: dict[str, str] = {
"OPENAI_API_KEY": "OpenAI",
"ANTHROPIC_API_KEY": "Anthropic",

View File

@@ -10,9 +10,10 @@ from textual.containers import Horizontal, Vertical
from textual.widgets import Footer, Header, Input, OptionList, Static, Tree
_PRIMARY = "#eb6658"
_SECONDARY = "#1F7982"
_TERTIARY = "#ffffff"
# -- CrewAI brand palette --
_PRIMARY = "#eb6658" # coral
_SECONDARY = "#1F7982" # teal
_TERTIARY = "#ffffff" # white
def _format_scope_info(info: Any) -> str:
@@ -192,6 +193,8 @@ class MemoryTUI(App[None]):
node = parent_node.add(label, data=child)
self._add_scope_children(node, child, depth + 1, max_depth)
# -- Populating the OptionList -------------------------------------------
def _populate_entry_list(self) -> None:
"""Clear the OptionList and fill it with the current scope's entries."""
option_list = self.query_one("#entry-list", OptionList)
@@ -223,6 +226,8 @@ class MemoryTUI(App[None]):
)
option_list.add_option(label)
# -- Detail rendering ----------------------------------------------------
def _format_record_detail(self, record: Any, context_line: str = "") -> str:
"""Format a full MemoryRecord as Rich markup for the detail view.
@@ -241,6 +246,7 @@ class MemoryTUI(App[None]):
lines.append(context_line)
lines.append("")
# -- Fields block --
lines.append(f"[dim]ID:[/] {record.id}")
lines.append(f"[dim]Scope:[/] [bold]{record.scope}[/]")
lines.append(f"[dim]Importance:[/] [bold]{record.importance:.2f}[/]")
@@ -258,10 +264,12 @@ class MemoryTUI(App[None]):
lines.append(f"[dim]Source:[/] {record.source or '-'}")
lines.append(f"[dim]Private:[/] {'Yes' if record.private else 'No'}")
# -- Content block --
lines.append(f"\n{sep}")
lines.append("[bold]Content[/]\n")
lines.append(record.content)
# -- Metadata block --
if record.metadata:
lines.append(f"\n{sep}")
lines.append("[bold]Metadata[/]\n")
@@ -270,6 +278,8 @@ class MemoryTUI(App[None]):
return "\n".join(lines)
# -- Event handlers ------------------------------------------------------
def on_tree_node_selected(self, event: Tree.NodeSelected[str]) -> None:
"""Load entries for the selected scope and populate the OptionList."""
path = event.node.data if event.node.data is not None else "/"

View File

@@ -68,12 +68,12 @@ def select_provider(provider_models: dict[str, list[str]]) -> str | None | bool:
provider = select_choice(
"Select a provider to set up:", [*predefined_providers, "other"]
)
if provider is None:
if provider is None: # User typed 'q'
return None
if provider == "other":
provider = select_choice("Select a provider from the full list:", all_providers)
if provider is None:
if provider is None: # User typed 'q'
return None
return provider.lower() if provider else False

View File

@@ -31,6 +31,7 @@ def run_crew(trained_agents_file: str | None = None) -> None:
min_required_version = "0.71.0"
pyproject_data = read_toml()
# Check for legacy poetry configuration
if pyproject_data.get("tool", {}).get("poetry") and (
version.parse(crewai_version) < version.parse(min_required_version)
):
@@ -40,11 +41,14 @@ def run_crew(trained_agents_file: str | None = None) -> None:
fg="red",
)
# Determine crew type
is_flow = pyproject_data.get("tool", {}).get("crewai", {}).get("type") == "flow"
crew_type = CrewType.FLOW if is_flow else CrewType.STANDARD
# Display appropriate message
click.echo(f"Running the {'Flow' if is_flow else 'Crew'}")
# Execute the appropriate command
execute_command(crew_type, trained_agents_file=trained_agents_file)

View File

@@ -28,8 +28,10 @@ class SettingsCommand(BaseCommand):
table.add_column("Value", style="green")
table.add_column("Description", style="yellow")
# Add all settings to the table
for field_name, field_info in Settings.model_fields.items():
if field_name in HIDDEN_SETTINGS_KEYS:
# Do not display hidden settings
continue
current_value = getattr(self.settings, field_name)
@@ -40,8 +42,10 @@ class SettingsCommand(BaseCommand):
table.add_row(field_name, display_value, description)
# Add trace-related settings from user data
user_data = _load_user_data()
# CREWAI_TRACING_ENABLED environment variable
env_tracing = os.getenv("CREWAI_TRACING_ENABLED", "")
env_tracing_display = env_tracing if env_tracing else "Not set"
table.add_row(
@@ -50,6 +54,7 @@ class SettingsCommand(BaseCommand):
"Environment variable to enable/disable tracing",
)
# Trace consent status
trace_consent = user_data.get("trace_consent")
if trace_consent is True:
consent_display = "✅ Enabled"
@@ -61,6 +66,7 @@ class SettingsCommand(BaseCommand):
"trace_consent", consent_display, "Whether trace collection is enabled"
)
# First execution timestamp
if user_data.get("first_execution_at"):
timestamp = datetime.fromtimestamp(user_data["first_execution_at"])
first_exec_display = timestamp.strftime("%Y-%m-%d %H:%M:%S")

View File

@@ -41,6 +41,10 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
BaseCommand.__init__(self)
PlusAPIMixin.__init__(self, telemetry=self._telemetry)
# ------------------------------------------------------------------
# create
# ------------------------------------------------------------------
def create(self, name: str, in_project: bool = True) -> None:
"""Scaffold a new skill directory.
@@ -69,6 +73,10 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
)
console.print(f"Edit [bold]{skill_md}[/bold] to define the skill instructions.")
# ------------------------------------------------------------------
# install
# ------------------------------------------------------------------
def install(self, ref: str) -> None:
"""Download and install a registry skill.
@@ -174,6 +182,10 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
f"[green]Installed [bold]{ref}[/bold]{' (' + version + ')' if version else ''} to global cache.[/green]"
)
# ------------------------------------------------------------------
# publish
# ------------------------------------------------------------------
def publish(self, is_public: bool, org: str | None, force: bool = False) -> None:
"""Publish the skill in the current directory to the registry."""
skill_md = Path("SKILL.md")
@@ -184,6 +196,7 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
)
raise SystemExit(1)
# Parse frontmatter to extract name + version
try:
frontmatter = self._parse_frontmatter(skill_md.read_text())
except ValueError as exc:
@@ -244,6 +257,10 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
f"Monitor status at: {base_url}/crewai_plus/skills/{effective_org}/{name}[/green]"
)
# ------------------------------------------------------------------
# list_cached
# ------------------------------------------------------------------
def list_cached(self) -> None:
"""Show locally installed skills."""
table = Table(title="Installed Skills", show_lines=True)
@@ -252,6 +269,7 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
table.add_column("Version")
table.add_column("Path")
# Project-local ./skills/
local_skills_dir = Path("skills")
if local_skills_dir.is_dir():
for skill_dir in sorted(local_skills_dir.iterdir()):
@@ -264,6 +282,7 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
str(skill_dir),
)
# Global cache
cache_root = Path.home() / ".crewai" / "skills"
if cache_root.exists():
for org_dir in sorted(cache_root.iterdir()):
@@ -287,6 +306,10 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
console.print(table)
# ------------------------------------------------------------------
# internal helpers
# ------------------------------------------------------------------
def _print_current_organization(self) -> None:
settings = Settings()
if settings.org_uuid:
@@ -303,6 +326,7 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
def _unpack_archive(self, archive_bytes: bytes, dest: Path) -> None:
"""Unpack a .tar.gz or .zip archive into dest."""
# Try tar first, then zip
try:
with tarfile.open(fileobj=io.BytesIO(archive_bytes), mode="r:gz") as tf:
try:
@@ -313,6 +337,7 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
except tarfile.TarError:
pass
# Fallback: zip
with zipfile.ZipFile(io.BytesIO(archive_bytes)) as zf:
_safe_extract_zip(zf, dest)

View File

@@ -1,7 +1,9 @@
from crewai import Agent, Crew, Process, Task
from crewai.project import CrewBase, agent, crew, task
from crewai.agents.agent_builder.base_agent import BaseAgent
# If you want to run a snippet of code before or after the crew starts,
# you can use the @before_kickoff and @after_kickoff decorators
# https://docs.crewai.com/concepts/crews#example-crew-class-with-decorators
@CrewBase
class {{crew_name}}():
@@ -10,6 +12,12 @@ class {{crew_name}}():
agents: list[BaseAgent]
tasks: list[Task]
# Learn more about YAML configuration files here:
# Agents: https://docs.crewai.com/concepts/agents#yaml-configuration-recommended
# Tasks: https://docs.crewai.com/concepts/tasks#yaml-configuration-recommended
# If you would like to add tools to your agents, you can learn more about it here:
# https://docs.crewai.com/concepts/agents#agent-tools
@agent
def researcher(self) -> Agent:
return Agent(
@@ -24,6 +32,9 @@ class {{crew_name}}():
verbose=True
)
# To learn more about structured task outputs,
# task dependencies, and task callbacks, check out the documentation:
# https://docs.crewai.com/concepts/tasks#overview-of-a-task
@task
def research_task(self) -> Task:
return Task(
@@ -40,9 +51,13 @@ class {{crew_name}}():
@crew
def crew(self) -> Crew:
"""Creates the {{crew_name}} crew"""
# To learn how to add knowledge sources to your crew, check out the documentation:
# https://docs.crewai.com/concepts/knowledge#what-is-knowledge
return Crew(
agents=self.agents,
tasks=self.tasks,
agents=self.agents, # Automatically created by the @agent decorator
tasks=self.tasks, # Automatically created by the @task decorator
process=Process.sequential,
verbose=True,
# process=Process.hierarchical, # In case you wanna use that instead https://docs.crewai.com/how-to/Hierarchical/
)

View File

@@ -8,6 +8,10 @@ from {{folder_name}}.crew import {{crew_name}}
warnings.filterwarnings("ignore", category=SyntaxWarning, module="pysbd")
# This main file is intended to be a way for you to run your
# crew locally, so refrain from adding unnecessary logic into this file.
# Replace with inputs you want to test with, it will automatically
# interpolate any tasks and agents information
def run():
"""

View File

@@ -15,4 +15,5 @@ class MyCustomTool(BaseTool):
args_schema: Type[BaseModel] = MyCustomToolInput
def _run(self, argument: str) -> str:
# Implementation goes here
return "this is an example of a tool output, ignore it and move along."

View File

@@ -2,6 +2,10 @@ from crewai import Agent, Crew, Process, Task
from crewai.agents.agent_builder.base_agent import BaseAgent
from crewai.project import CrewBase, agent, crew, task
# If you want to run a snippet of code before or after the crew starts,
# you can use the @before_kickoff and @after_kickoff decorators
# https://docs.crewai.com/concepts/crews#example-crew-class-with-decorators
@CrewBase
class ContentCrew:
@@ -10,9 +14,14 @@ class ContentCrew:
agents: list[BaseAgent]
tasks: list[Task]
# Learn more about YAML configuration files here:
# Agents: https://docs.crewai.com/concepts/agents#yaml-configuration-recommended
# Tasks: https://docs.crewai.com/concepts/tasks#yaml-configuration-recommended
agents_config = "config/agents.yaml"
tasks_config = "config/tasks.yaml"
# If you would like to add tools to your crew, you can learn more about it here:
# https://docs.crewai.com/concepts/agents#agent-tools
@agent
def planner(self) -> Agent:
return Agent(
@@ -31,6 +40,9 @@ class ContentCrew:
config=self.agents_config["editor"], # type: ignore[index]
)
# To learn more about structured task outputs,
# task dependencies, and task callbacks, check out the documentation:
# https://docs.crewai.com/concepts/tasks#overview-of-a-task
@task
def planning_task(self) -> Task:
return Task(
@@ -52,9 +64,12 @@ class ContentCrew:
@crew
def crew(self) -> Crew:
"""Creates the Content Crew"""
# To learn how to add knowledge sources to your crew, check out the documentation:
# https://docs.crewai.com/concepts/knowledge#what-is-knowledge
return Crew(
agents=self.agents,
tasks=self.tasks,
agents=self.agents, # Automatically created by the @agent decorator
tasks=self.tasks, # Automatically created by the @task decorator
process=Process.sequential,
verbose=True,
)

View File

@@ -68,6 +68,7 @@ def run_with_trigger():
import json
import sys
# Get trigger payload from command line argument
if len(sys.argv) < 2:
raise Exception("No trigger payload provided. Please provide JSON payload as argument.")
@@ -76,6 +77,8 @@ def run_with_trigger():
except json.JSONDecodeError:
raise Exception("Invalid JSON payload provided as argument")
# Create flow and kickoff with trigger payload
# The @start() methods will automatically receive crewai_trigger_payload parameter
content_flow = ContentFlow()
try:

View File

@@ -17,4 +17,5 @@ class MyCustomTool(BaseTool):
args_schema: Type[BaseModel] = MyCustomToolInput
def _run(self, argument: str) -> str:
# Implementation goes here
return "this is an example of a tool output, ignore it and move along."

View File

@@ -6,4 +6,5 @@ class {{class_name}}(BaseTool):
description: str = "What this tool does. It's vital for effective utilization."
def _run(self, argument: str) -> str:
# Your tool's logic here
return "Tool's result"

View File

@@ -82,6 +82,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
tree_find_and_replace(project_root, "{{folder_name}}", folder_name)
tree_find_and_replace(project_root, "{{class_name}}", class_name)
# Copy AGENTS.md to project root
agents_md_src = Path(__file__).parent.parent / "templates" / "AGENTS.md"
if agents_md_src.exists():
shutil.copy2(agents_md_src, project_root / "AGENTS.md")

View File

@@ -37,6 +37,7 @@ class TriggersCommand(BaseCommand, PlusAPIMixin):
def execute_with_trigger(self, trigger_path: str) -> None:
"""Execute crew with trigger payload."""
try:
# Parse app_slug/trigger_slug
if "/" not in trigger_path:
console.print(
"[bold red]Error: Trigger must be in format 'app_slug/trigger_slug'[/bold red]"
@@ -62,6 +63,7 @@ class TriggersCommand(BaseCommand, PlusAPIMixin):
trigger_data = response.json()
self._display_trigger_info(trigger_data)
# Run crew with trigger payload
self._run_crew_with_payload(trigger_data.get("sample_payload", {}))
except Exception as e:

View File

@@ -21,6 +21,7 @@ def migrate_pyproject(input_file: str, output_file: str) -> None:
When the time comes that uv supports the new format, this function will be deprecated.
"""
poetry_data = {}
# Read the input pyproject.toml
pyproject_data = read_toml()
new_pyproject: dict[str, Any] = {
@@ -28,6 +29,7 @@ def migrate_pyproject(input_file: str, output_file: str) -> None:
"build-system": {"requires": ["hatchling"], "build-backend": "hatchling.build"},
}
# Migrate project metadata
if "tool" in pyproject_data and "poetry" in pyproject_data["tool"]:
poetry_data = pyproject_data["tool"]["poetry"]
new_pyproject["project"]["name"] = poetry_data.get("name")
@@ -42,15 +44,18 @@ def migrate_pyproject(input_file: str, output_file: str) -> None:
]
new_pyproject["project"]["requires-python"] = poetry_data.get("python")
else:
# If it's already in the new format, just copy the project and tool sections
new_pyproject["project"] = pyproject_data.get("project", {})
new_pyproject["tool"] = pyproject_data.get("tool", {})
# Migrate or copy dependencies
if "dependencies" in new_pyproject["project"]:
# If dependencies are already in the new format, keep them as is
pass
elif poetry_data and "dependencies" in poetry_data:
new_pyproject["project"]["dependencies"] = []
for dep, version in poetry_data["dependencies"].items():
if isinstance(version, dict):
if isinstance(version, dict): # Handle extras
extras = ",".join(version.get("extras", []))
new_dep = f"{dep}[{extras}]"
if "version" in version:
@@ -62,6 +67,7 @@ def migrate_pyproject(input_file: str, output_file: str) -> None:
new_dep = f"{dep}{parse_version(version)}"
new_pyproject["project"]["dependencies"].append(new_dep)
# Migrate or copy scripts
if poetry_data and "scripts" in poetry_data:
new_pyproject["project"]["scripts"] = poetry_data["scripts"]
elif pyproject_data.get("project", {}) and "scripts" in pyproject_data["project"]:
@@ -73,6 +79,7 @@ def migrate_pyproject(input_file: str, output_file: str) -> None:
"run_crew" not in new_pyproject["project"]["scripts"]
and len(new_pyproject["project"]["scripts"]) > 0
):
# Extract the module name from any existing script
existing_scripts = new_pyproject["project"]["scripts"]
module_name = next(
(value.split(".")[0] for value in existing_scripts.values() if "." in value)
@@ -80,12 +87,15 @@ def migrate_pyproject(input_file: str, output_file: str) -> None:
new_pyproject["project"]["scripts"]["run_crew"] = f"{module_name}.main:run"
# Migrate optional dependencies
if poetry_data and "extras" in poetry_data:
new_pyproject["project"]["optional-dependencies"] = poetry_data["extras"]
# Backup the old pyproject.toml
backup_file = "pyproject-old.toml"
shutil.copy2(input_file, backup_file)
# Rename the poetry.lock file
lock_file = "poetry.lock"
lock_backup = "poetry-old.lock"
if os.path.exists(lock_file):
@@ -93,6 +103,7 @@ def migrate_pyproject(input_file: str, output_file: str) -> None:
else:
pass
# Write the new pyproject.toml
with open(output_file, "wb") as f:
tomli_w.dump(new_pyproject, f)

View File

@@ -333,6 +333,7 @@ class TestAuthenticationCommand:
@patch("crewai_core.auth.oauth2.httpx.post")
def test_poll_for_token_error(self, mock_post):
"""Test the method to poll for token (error path)."""
# Setup mock to return error
mock_response_error = MagicMock()
mock_response_error.status_code = 400
mock_response_error.json.return_value = {

View File

@@ -12,6 +12,7 @@ class TestUtils(unittest.TestCase):
def test_validate_jwt_token(self, mock_jwt, mock_pyjwkclient):
mock_jwt.decode.return_value = {"exp": 1719859200}
# Create signing key object mock with a .key attribute
mock_pyjwkclient.return_value.get_signing_key_from_jwt.return_value = MagicMock(
key="mock_signing_key"
)

View File

@@ -164,6 +164,7 @@ def test_poetry_lock_is_accepted(tmp_path: Path) -> None:
def test_stale_lockfile_warns(tmp_path: Path) -> None:
_scaffold_standard_crew(tmp_path)
# Make lockfile older than pyproject.
lock = tmp_path / "uv.lock"
pyproject = tmp_path / "pyproject.toml"
old_time = pyproject.stat().st_mtime - 60

View File

@@ -41,9 +41,14 @@ def skill_command():
yield cmd
# ---------------------------------------------------------------------------
# create
# ---------------------------------------------------------------------------
class TestSkillCreate:
def test_create_in_project(self, skill_command, tmp_path):
with in_temp_dir():
# Simulate being inside a project
Path("pyproject.toml").write_text("[tool.poetry]\nname = 'test'\n")
skill_command.create("my-skill")
assert Path("skills/my-skill/SKILL.md").exists()
@@ -70,6 +75,10 @@ class TestSkillCreate:
skill_command.create("existing-skill", in_project=False)
# ---------------------------------------------------------------------------
# install
# ---------------------------------------------------------------------------
class TestSkillInstall:
def _zip_skill(self, name: str) -> bytes:
buf = io.BytesIO()
@@ -109,6 +118,10 @@ class TestSkillInstall:
assert Path("skills/my-skill/SKILL.md").exists()
# ---------------------------------------------------------------------------
# publish
# ---------------------------------------------------------------------------
class TestSkillPublish:
def test_publish_no_skill_md(self, skill_command):
with in_temp_dir():
@@ -142,6 +155,7 @@ class TestSkillPublish:
mock_resp.status_code = 200
mock_resp.json.return_value = {}
mock_client.publish_skill.return_value = mock_resp
# No org set → should SystemExit (no org_name in settings)
with patch("crewai_cli.skills.main.Settings") as mock_settings_cls:
mock_settings_cls.return_value.org_name = None
mock_settings_cls.return_value.enterprise_base_url = None
@@ -170,10 +184,15 @@ class TestSkillPublish:
assert call_kwargs.kwargs["version"] == "1.0.0"
# ---------------------------------------------------------------------------
# list_cached
# ---------------------------------------------------------------------------
class TestSkillListCached:
def test_list_cached_empty(self, skill_command, capsys):
with in_temp_dir():
skill_command.list_cached()
# Should not raise
def test_list_cached_shows_project_skills(self, skill_command, capsys):
with in_temp_dir():
@@ -183,3 +202,4 @@ class TestSkillListCached:
"---\nname: my-skill\nversion: 0.5.0\ndescription: A skill.\n---\nBody."
)
skill_command.list_cached()
# Should complete without error

View File

@@ -83,6 +83,7 @@ def test_test_crew_called_process_error(mock_subprocess_run, click):
@mock.patch("crewai_cli.evaluate_crew.click")
@mock.patch("crewai_cli.evaluate_crew.subprocess.run")
def test_test_crew_unexpected_exception(mock_subprocess_run, click):
# Arrange
n_iterations = 5
mock_subprocess_run.side_effect = Exception("Unexpected error")
evaluate_crew.evaluate_crew(n_iterations, "gpt-4o")

View File

@@ -35,6 +35,7 @@ class TestSettingsCommand(unittest.TestCase):
self.settings_command.list()
# Tests that the table is created skipping hidden settings
mock_table_instance.add_row.assert_has_calls(
[
call(
@@ -47,6 +48,7 @@ class TestSettingsCommand(unittest.TestCase):
]
)
# Tests that the table is printed
mock_console.print.assert_called_once_with(mock_table_instance)
def test_set_valid_keys(self):

View File

@@ -146,6 +146,7 @@ class TestAtomicFileOperations(unittest.TestCase):
self.temp_dir = tempfile.mkdtemp()
self.original_get_path = TokenManager._get_secure_storage_path
# Patch to use temp directory
def mock_get_path() -> Path:
return Path(self.temp_dir)
@@ -181,6 +182,7 @@ class TestAtomicFileOperations(unittest.TestCase):
mock_get_key.return_value = Fernet.generate_key()
tm = TokenManager()
# Create file first
file_path = Path(self.temp_dir) / "test.txt"
file_path.write_bytes(b"original")
@@ -229,6 +231,7 @@ class TestAtomicFileOperations(unittest.TestCase):
tm._atomic_write_secure_file("test.txt", b"content")
# Check no temp files remain
temp_files = list(Path(self.temp_dir).glob(".test.txt.*"))
self.assertEqual(len(temp_files), 0)
@@ -282,6 +285,7 @@ class TestAtomicFileOperations(unittest.TestCase):
mock_get_key.return_value = Fernet.generate_key()
tm = TokenManager()
# Should not raise
tm._delete_secure_file("nonexistent.txt")

View File

@@ -27,7 +27,9 @@ def in_temp_dir():
@pytest.fixture
def tool_command():
# Create a temporary directory for each test to avoid token storage conflicts
with tempfile.TemporaryDirectory() as temp_dir:
# Mock the secure storage path to use the temp directory
with patch.object(
TokenManager, "_get_secure_storage_path", return_value=Path(temp_dir)
):
@@ -95,6 +97,7 @@ def test_install_success(
env=unittest.mock.ANY,
)
# Verify _print_current_organization was called
mock_print_org.assert_called_once()

View File

@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod
import os
from typing import Annotated, Any, Literal, cast
from typing import Any, Literal, cast
from crewai.rag.core.base_embeddings_callable import EmbeddingFunction
from crewai.rag.embeddings.factory import build_embedder
@@ -8,13 +8,10 @@ from crewai.rag.embeddings.types import ProviderSpec
from crewai.tools import BaseTool
from pydantic import (
BaseModel,
BeforeValidator,
ConfigDict,
Field,
PlainSerializer,
TypeAdapter,
ValidationError,
WithJsonSchema,
field_validator,
model_validator,
)
@@ -103,26 +100,6 @@ class Adapter(BaseModel, ABC):
"""Add content to the knowledge base."""
def _resolve_adapter(value: Any) -> Any:
"""Validate the ``adapter`` field, returning a placeholder for dict/None input.
Adapter state is not round-tripped; the ``_ensure_adapter`` post-validator
rebuilds a fresh adapter from the tool's ``config``.
"""
if isinstance(value, Adapter):
return value
if value is None or isinstance(value, dict):
return RagTool._AdapterPlaceholder()
return value
def _serialize_adapter(adapter: Any, info: Any) -> Any:
"""Serialize the ``adapter`` field, dropping runtime state from the payload."""
if not isinstance(adapter, Adapter):
return adapter
return None
class RagTool(BaseTool):
class _AdapterPlaceholder(Adapter):
def query(
@@ -146,12 +123,7 @@ class RagTool(BaseTool):
similarity_threshold: float = 0.6
limit: int = 5
collection_name: str = "rag_tool_collection"
adapter: Annotated[
Adapter,
BeforeValidator(_resolve_adapter),
PlainSerializer(_serialize_adapter, when_used="json"),
WithJsonSchema({"type": ["object", "null"]}),
] = Field(default_factory=_AdapterPlaceholder)
adapter: Adapter = Field(default_factory=_AdapterPlaceholder)
config: RagToolConfig = Field(
default_factory=RagToolConfig,
description="Configuration format accepted by RagTool.",

View File

@@ -2912,6 +2912,12 @@
"humanized_name": "Search a CSV's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -3897,10 +3903,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",
@@ -3961,6 +3964,12 @@
"humanized_name": "Search a Code Docs content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -4946,10 +4955,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",
@@ -5635,6 +5641,12 @@
"humanized_name": "Search a DOCX's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -6620,10 +6632,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",
@@ -7917,6 +7926,12 @@
"humanized_name": "Search a directory's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -8902,10 +8917,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",
@@ -10750,6 +10762,12 @@
"humanized_name": "Search a github repo's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -11735,10 +11753,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",
@@ -12026,6 +12041,12 @@
"humanized_name": "Search a JSON's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -13011,10 +13032,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",
@@ -13298,6 +13316,12 @@
"humanized_name": "Search a MDX's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -14283,10 +14307,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",
@@ -14753,6 +14774,12 @@
"humanized_name": "Search a database's table content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -15738,10 +15765,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",
@@ -15943,6 +15967,21 @@
"title": "EnvVar",
"type": "object"
},
"JsonResponseFormat": {
"description": "Response format requesting raw JSON output (e.g. ``{\"type\": \"json_object\"}``).",
"properties": {
"type": {
"const": "json_object",
"title": "Type",
"type": "string"
}
},
"required": [
"type"
],
"title": "JsonResponseFormat",
"type": "object"
},
"LLM": {
"properties": {
"additional_params": {
@@ -16171,6 +16210,16 @@
"title": "Reasoning Effort"
},
"response_format": {
"anyOf": [
{
"$ref": "#/$defs/JsonResponseFormat"
},
{},
{
"type": "null"
}
],
"default": null,
"title": "Response Format"
},
"seed": {
@@ -17158,6 +17207,12 @@
"humanized_name": "Search a PDF's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -18143,10 +18198,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",
@@ -18854,6 +18906,12 @@
"humanized_name": "Knowledge base",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -19839,10 +19897,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",
@@ -20939,6 +20994,12 @@
"humanized_name": "Job Search",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -21924,10 +21985,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",
@@ -22404,6 +22462,12 @@
"humanized_name": "Webpage to Markdown",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -23389,10 +23453,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",
@@ -24246,6 +24307,12 @@
"humanized_name": "Search a txt's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -25231,10 +25298,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",
@@ -26163,6 +26227,12 @@
"humanized_name": "Search in a specific website",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -27148,10 +27218,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",
@@ -27212,6 +27279,12 @@
"humanized_name": "Search a XML's content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -28197,10 +28270,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",
@@ -28261,6 +28331,12 @@
"humanized_name": "Search a Youtube Channels content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -29246,10 +29322,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",
@@ -29310,6 +29383,12 @@
"humanized_name": "Search a Youtube Video content",
"init_params_schema": {
"$defs": {
"Adapter": {
"description": "Abstract base class for RAG adapters.",
"properties": {},
"title": "Adapter",
"type": "object"
},
"AzureProviderConfig": {
"description": "Configuration for Azure provider.",
"properties": {
@@ -30295,10 +30374,7 @@
},
"properties": {
"adapter": {
"type": [
"object",
"null"
]
"$ref": "#/$defs/Adapter"
},
"collection_name": {
"default": "rag_tool_collection",

View File

@@ -16,7 +16,6 @@ from pydantic import (
FilePath,
PrivateAttr,
SecretStr,
field_serializer,
model_validator,
)
from typing_extensions import Self, deprecated
@@ -25,7 +24,6 @@ from crewai.a2a.auth.client_schemes import ClientAuthScheme
from crewai.a2a.auth.server_schemes import ServerAuthScheme
from crewai.a2a.extensions.base import ValidatedA2AExtension
from crewai.a2a.types import ProtocolVersion, TransportType, Url
from crewai.utilities.pydantic_schema_utils import serialize_model_class
try:
@@ -401,11 +399,6 @@ class A2AConfig(BaseModel):
default=None,
description="Optional Pydantic model for structured A2A agent responses",
)
@field_serializer("response_model", when_used="json")
def _serialize_response_model(self, value: Any) -> Any:
return serialize_model_class(value)
fail_fast: bool = Field(
default=True,
description="If True, raise error when agent unreachable; if False, skip",
@@ -495,11 +488,6 @@ class A2AClientConfig(BaseModel):
default=None,
description="Optional Pydantic model for structured A2A agent responses",
)
@field_serializer("response_model", when_used="json")
def _serialize_response_model(self, value: Any) -> Any:
return serialize_model_class(value)
fail_fast: bool = Field(
default=True,
description="If True, raise error when agent unreachable; if False, skip",

View File

@@ -28,6 +28,7 @@ from pydantic import (
ConfigDict,
Field,
PrivateAttr,
ValidationError,
model_validator,
)
from pydantic.functional_serializers import PlainSerializer
@@ -1109,14 +1110,9 @@ class Agent(BaseAgent):
"""
if self.agent_executor is None:
raise RuntimeError("Agent executor is not initialized.")
if not isinstance(self.llm, BaseLLM):
raise RuntimeError(
"LLM must be resolved before updating agent executor parameters."
)
if task is not None:
self.agent_executor.task = task
self.agent_executor.llm = self.llm
self.agent_executor.tools = tools
self.agent_executor.original_tools = raw_tools
self.agent_executor.prompt = prompt
@@ -1416,11 +1412,6 @@ class Agent(BaseAgent):
if _is_resuming_agent_executor(self.agent_executor):
executor = self.agent_executor
if not isinstance(self.llm, BaseLLM):
raise RuntimeError(
"LLM must be resolved before resuming agent executor."
)
executor.llm = self.llm
executor.tools = parsed_tools
executor.tools_names = get_tool_names(parsed_tools)
executor.tools_description = render_text_description_and_args(parsed_tools)
@@ -1701,24 +1692,32 @@ class Agent(BaseAgent):
elif response_format:
raw_output = str(output) if not isinstance(output, str) else output
try:
model_schema = generate_model_description(response_format)
schema = json.dumps(model_schema, indent=2)
instructions = I18N_DEFAULT.slice("formatted_task_instructions").format(
output_format=schema
)
formatted_result = response_format.model_validate_json(raw_output)
except ValidationError:
# Direct JSON validation failed; fall back to converter-based parsing below.
formatted_result = None
converter = Converter(
llm=cast(BaseLLM, self.llm),
text=raw_output,
model=response_format,
instructions=instructions,
)
if formatted_result is None:
try:
model_schema = generate_model_description(response_format)
schema = json.dumps(model_schema, indent=2)
instructions = I18N_DEFAULT.slice(
"formatted_task_instructions"
).format(output_format=schema)
conversion_result = converter.to_pydantic()
if isinstance(conversion_result, BaseModel):
formatted_result = conversion_result
except ConverterError:
pass
converter = Converter(
llm=cast(BaseLLM, self.llm),
text=raw_output,
model=response_format,
instructions=instructions,
)
conversion_result = converter.to_pydantic()
if isinstance(conversion_result, BaseModel):
formatted_result = conversion_result
except ConverterError:
# Conversion failure is non-fatal; raw output is preserved below.
pass
else:
raw_output = str(output) if not isinstance(output, str) else output

View File

@@ -350,6 +350,10 @@ class CrewAgentExecutor(BaseAgentExecutor):
enforce_rpm_limit(self.request_within_rpm_limit)
effective_response_model = (
None if self.original_tools else self.response_model
)
answer = get_llm_response(
llm=cast("BaseLLM", self.llm),
messages=self.messages,
@@ -357,11 +361,11 @@ class CrewAgentExecutor(BaseAgentExecutor):
printer=PRINTER,
from_task=self.task,
from_agent=self.agent,
response_model=self.response_model,
response_model=effective_response_model,
executor_context=self,
verbose=self.agent.verbose,
)
if self.response_model is not None:
if effective_response_model is not None:
try:
if isinstance(answer, BaseModel):
output_json = answer.model_dump_json()
@@ -502,7 +506,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
available_functions=None,
from_task=self.task,
from_agent=self.agent,
response_model=self.response_model,
response_model=None,
executor_context=self,
verbose=self.agent.verbose,
)
@@ -1161,6 +1165,10 @@ class CrewAgentExecutor(BaseAgentExecutor):
enforce_rpm_limit(self.request_within_rpm_limit)
effective_response_model = (
None if self.original_tools else self.response_model
)
answer = await aget_llm_response(
llm=cast("BaseLLM", self.llm),
messages=self.messages,
@@ -1168,12 +1176,12 @@ class CrewAgentExecutor(BaseAgentExecutor):
printer=PRINTER,
from_task=self.task,
from_agent=self.agent,
response_model=self.response_model,
response_model=effective_response_model,
executor_context=self,
verbose=self.agent.verbose,
)
if self.response_model is not None:
if effective_response_model is not None:
try:
if isinstance(answer, BaseModel):
output_json = answer.model_dump_json()
@@ -1314,7 +1322,7 @@ class CrewAgentExecutor(BaseAgentExecutor):
available_functions=None,
from_task=self.task,
from_agent=self.agent,
response_model=self.response_model,
response_model=None,
executor_context=self,
verbose=self.agent.verbose,
)

View File

@@ -382,15 +382,6 @@ class Crew(FlowTrackable, BaseModel):
checkpoint_train: bool | None = Field(default=None)
checkpoint_kickoff_event_id: str | None = Field(default=None)
@field_validator(
"before_kickoff_callbacks", "after_kickoff_callbacks", mode="before"
)
@classmethod
def _drop_unresolvable_callbacks(cls, value: Any) -> Any:
if isinstance(value, list):
return [v for v in value if v is not None]
return value
@classmethod
def from_checkpoint(cls, config: CheckpointConfig) -> Crew:
"""Restore a Crew from a checkpoint, ready to resume via kickoff().
@@ -452,20 +443,16 @@ class Crew(FlowTrackable, BaseModel):
if node.event.type == "task_started" and node.event.task_id:
started_task_ids.add(node.event.task_id)
is_hierarchical = self.process == Process.hierarchical
resuming_task_agent_roles: set[str] = set()
for task in self.tasks:
if task.output is not None or str(task.id) not in started_task_ids:
continue
executing_agent = self.manager_agent if is_hierarchical else task.agent
if executing_agent is not None:
resuming_task_agent_roles.add(executing_agent.role)
if (
task.output is None
and task.agent is not None
and str(task.id) in started_task_ids
):
resuming_task_agent_roles.add(task.agent.role)
candidate_agents: list[BaseAgent] = list(self.agents)
if self.manager_agent is not None:
candidate_agents.append(self.manager_agent)
for agent in candidate_agents:
for agent in self.agents:
agent.crew = self
executor = agent.agent_executor
if (
@@ -480,7 +467,7 @@ class Crew(FlowTrackable, BaseModel):
agent.agent_executor = None
for task in self.tasks:
if task.agent is not None:
for agent in candidate_agents:
for agent in self.agents:
if agent.role == task.agent.role:
task.agent = agent
if agent.agent_executor is not None and task.output is None:
@@ -549,9 +536,25 @@ class Crew(FlowTrackable, BaseModel):
if state is None:
return
# Restore crew scope and the in-progress task scope. Inner scopes
# (agent, llm, tool) are re-created by the executor on resume.
stack: list[tuple[str, str]] = []
if self._kickoff_event_id:
stack.append((self._kickoff_event_id, "crew_kickoff_started"))
# Find the task_started event for the in-progress task (skipped on resume)
for task in self.tasks:
if task.output is None:
task_id_str = str(task.id)
for node in state.event_record.nodes.values():
if (
node.event.type == "task_started"
and node.event.task_id == task_id_str
):
stack.append((node.event.event_id, "task_started"))
break
break
restore_event_scope(tuple(stack))
# Restore last_event_id and emission counter from the record

View File

@@ -138,36 +138,6 @@ def restore_event_scope(stack: tuple[tuple[str, str], ...]) -> None:
_event_id_stack.set(stack)
def resume_task_scope(task_id: str) -> bool:
"""Push the latest recorded ``task_started`` scope for a task.
Args:
task_id: The task identifier to look up in the active event record.
Returns:
``True`` if a prior scope was pushed; ``False`` otherwise.
"""
from crewai.events.event_bus import crewai_event_bus
state = crewai_event_bus._runtime_state
if state is None:
return False
latest_event_id: str | None = None
latest_seq = -1
for node in list(state.event_record.nodes.values()):
ev = node.event
if ev.type != "task_started" or ev.task_id != task_id:
continue
seq = ev.emission_sequence or 0
if seq > latest_seq:
latest_seq = seq
latest_event_id = ev.event_id
if latest_event_id is None:
return False
push_event_scope(latest_event_id, "task_started")
return True
def push_event_scope(event_id: str, event_type: str = "") -> None:
"""Push an event ID and type onto the scope stack."""
config = _event_context_config.get() or _default_config

View File

@@ -173,10 +173,8 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
executor_type: Literal["experimental"] = "experimental"
suppress_flow_events: bool = True # always suppress for executor
llm: BaseLLM | None = Field(default=None, exclude=True)
prompt: SystemPromptResult | StandardPromptResult | None = Field(
default=None, exclude=True
)
llm: BaseLLM = Field(exclude=True)
prompt: SystemPromptResult | StandardPromptResult = Field(exclude=True)
max_iter: int = Field(default=25, exclude=True)
tools: list[CrewStructuredTool] = Field(default_factory=list, exclude=True)
tools_names: str = Field(default="", exclude=True)
@@ -1226,6 +1224,10 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
try:
enforce_rpm_limit(self.request_within_rpm_limit)
effective_response_model = (
None if self.original_tools else self.response_model
)
answer = get_llm_response(
llm=self.llm,
messages=list(self.state.messages),
@@ -1233,7 +1235,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
printer=PRINTER,
from_task=self.task,
from_agent=self.agent,
response_model=self.response_model,
response_model=effective_response_model,
executor_context=self,
verbose=self.agent.verbose,
)
@@ -1321,7 +1323,7 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
available_functions=None,
from_task=self.task,
from_agent=self.agent,
response_model=self.response_model,
response_model=None,
executor_context=self,
verbose=self.agent.verbose,
)
@@ -2587,11 +2589,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
self._kickoff_input = inputs.get("input", "")
if self.llm is None or self.prompt is None:
raise RuntimeError(
"AgentExecutor.llm or .prompt is unset; the executor was "
"not fully restored or initialized before execution."
)
if "system" in self.prompt:
from crewai.llms.cache import mark_cache_breakpoint
@@ -2693,11 +2690,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
self._kickoff_input = inputs.get("input", "")
if self.llm is None or self.prompt is None:
raise RuntimeError(
"AgentExecutor.llm or .prompt is unset; the executor was "
"not fully restored or initialized before execution."
)
if "system" in self.prompt:
from crewai.llms.cache import mark_cache_breakpoint

View File

@@ -23,7 +23,7 @@ from pydantic import (
BaseModel,
Field,
PrivateAttr,
field_serializer,
ValidationError,
field_validator,
model_validator,
)
@@ -95,10 +95,7 @@ from crewai.utilities.guardrail import process_guardrail, serialize_guardrail_fo
from crewai.utilities.guardrail_types import GuardrailCallable, GuardrailType
from crewai.utilities.i18n import I18N_DEFAULT
from crewai.utilities.llm_utils import create_llm
from crewai.utilities.pydantic_schema_utils import (
generate_model_description,
serialize_model_class,
)
from crewai.utilities.pydantic_schema_utils import generate_model_description
from crewai.utilities.token_counter_callback import TokenCalcHandler
from crewai.utilities.tool_utils import execute_tool_and_check_finality
from crewai.utilities.types import LLMMessage
@@ -239,11 +236,6 @@ class LiteAgent(FlowTrackable, BaseModel):
response_format: type[BaseModel] | None = Field(
default=None, description="Pydantic model for structured output"
)
@field_serializer("response_format", when_used="json")
def _serialize_response_format(self, value: Any) -> Any:
return serialize_model_class(value)
verbose: bool = Field(
default=False, description="Whether to print execution details"
)
@@ -648,29 +640,38 @@ class LiteAgent(FlowTrackable, BaseModel):
formatted_result = agent_finish.output
elif active_response_format:
try:
model_schema = generate_model_description(active_response_format)
schema = json.dumps(model_schema, indent=2)
instructions = I18N_DEFAULT.slice("formatted_task_instructions").format(
output_format=schema
formatted_result = active_response_format.model_validate_json(
str(agent_finish.output)
)
except ValidationError:
# Direct JSON validation failed; fall back to converter-based parsing below.
formatted_result = None
converter = Converter(
llm=self.llm,
text=agent_finish.output,
model=active_response_format,
instructions=instructions,
)
if formatted_result is None:
try:
model_schema = generate_model_description(active_response_format)
schema = json.dumps(model_schema, indent=2)
instructions = I18N_DEFAULT.slice(
"formatted_task_instructions"
).format(output_format=schema)
result = converter.to_pydantic()
if isinstance(result, BaseModel):
formatted_result = result
except ConverterError as e:
if self.verbose:
PRINTER.print(
content=f"Failed to parse output into response format after retries: {e.message}",
color="yellow",
converter = Converter(
llm=self.llm,
text=agent_finish.output,
model=active_response_format,
instructions=instructions,
)
result = converter.to_pydantic()
if isinstance(result, BaseModel):
formatted_result = result
except ConverterError as e:
if self.verbose:
PRINTER.print(
content=f"Failed to parse output into response format after retries: {e.message}",
color="yellow",
)
# Calculate token usage metrics
if isinstance(self.llm, BaseLLM):
usage_metrics = self.llm.get_token_usage_summary()

View File

@@ -23,7 +23,6 @@ from pydantic import (
ConfigDict,
Field,
PrivateAttr,
field_serializer,
model_validator,
)
from typing_extensions import TypedDict
@@ -43,7 +42,6 @@ from crewai.events.types.tool_usage_events import (
ToolUsageStartedEvent,
)
from crewai.types.usage_metrics import UsageMetrics
from crewai.utilities.pydantic_schema_utils import serialize_model_class
try:
@@ -161,10 +159,6 @@ class BaseLLM(BaseModel, ABC):
)
additional_params: dict[str, Any] = Field(default_factory=dict)
@field_serializer("response_format", when_used="json", check_fields=False)
def _serialize_response_format(self, value: Any) -> Any:
return serialize_model_class(value)
def __setattr__(self, name: str, value: Any) -> None:
if name in ("stop", "stop_sequences"):
if value is None:

View File

@@ -67,11 +67,7 @@ class EventNode(BaseModel):
event: Annotated[
BaseEvent,
BeforeValidator(_resolve_event),
PlainSerializer(
lambda v, info: (
v.model_dump(mode="json") if info.mode == "json" else v.model_dump()
),
),
PlainSerializer(lambda v: v.model_dump()),
]
edges: dict[EdgeType, list[str]] = Field(default_factory=dict)

View File

@@ -40,7 +40,6 @@ from crewai.agents.agent_builder.base_agent import BaseAgent, _resolve_agent
from crewai.context import reset_current_task_id, set_current_task_id
from crewai.core.providers.content_processor import process_content
from crewai.events.event_bus import crewai_event_bus
from crewai.events.event_context import resume_task_scope
from crewai.events.types.task_events import (
TaskCompletedEvent,
TaskFailedEvent,
@@ -662,10 +661,7 @@ class Task(BaseModel):
tools = tools or self.tools or []
self.processed_by_agents.add(agent.role)
executor = agent.agent_executor
if not (
executor and executor._resuming and resume_task_scope(str(self.id))
):
if not (agent.agent_executor and agent.agent_executor._resuming):
crewai_event_bus.emit(
self, TaskStartedEvent(context=context, task=self)
)
@@ -787,10 +783,7 @@ class Task(BaseModel):
tools = tools or self.tools or []
self.processed_by_agents.add(agent.role)
executor = agent.agent_executor
if not (
executor and executor._resuming and resume_task_scope(str(self.id))
):
if not (agent.agent_executor and agent.agent_executor._resuming):
crewai_event_bus.emit(
self, TaskStartedEvent(context=context, task=self)
)

View File

@@ -8,6 +8,7 @@ from inspect import Parameter, signature
import json
import threading
from typing import (
Annotated,
Any,
Generic,
ParamSpec,
@@ -21,10 +22,10 @@ from pydantic import (
ConfigDict,
Field,
GetCoreSchemaHandler,
PlainSerializer,
PrivateAttr,
computed_field,
create_model,
field_serializer,
field_validator,
)
from pydantic_core import CoreSchema, core_schema
@@ -144,18 +145,15 @@ class BaseTool(BaseModel, ABC):
default_factory=list,
description="List of environment variables used by the tool.",
)
args_schema: type[PydanticBaseModel] = Field(
args_schema: Annotated[
type[PydanticBaseModel],
PlainSerializer(_serialize_schema, return_type=dict | None, when_used="json"),
] = Field(
default=_ArgsSchemaPlaceholder,
validate_default=True,
description="The schema for the arguments that the tool accepts.",
)
@field_serializer("args_schema", when_used="json")
def _serialize_args_schema(
self, schema: type[PydanticBaseModel] | None
) -> dict[str, Any] | None:
return _serialize_schema(schema)
description_updated: bool = Field(
default=False, description="Flag to check if the description has been updated."
)

View File

@@ -130,15 +130,18 @@ def _resolve_dotted_path(path: str) -> Callable[..., Any]:
raise ValueError(f"Cannot resolve callback {path!r}")
def callable_to_string(fn: Callable[..., Any]) -> str | None:
"""Serialize a module-level callable as a ``"module.qualname"`` string.
def callable_to_string(fn: Callable[..., Any]) -> str:
"""Serialize a callable to its dotted-path string representation.
Uses ``fn.__module__`` and ``fn.__qualname__`` to produce a string such
as ``"builtins.print"``. Lambdas and closures produce paths that contain
``<locals>`` and cannot be round-tripped via :func:`string_to_callable`.
Args:
fn: The callable to serialize.
Returns:
The dotted path, or ``None`` for lambdas and closures (not
resolvable by :func:`string_to_callable`).
A dotted string of the form ``"module.qualname"``.
"""
module = getattr(fn, "__module__", None)
qualname = getattr(fn, "__qualname__", None)
@@ -147,8 +150,6 @@ def callable_to_string(fn: Callable[..., Any]) -> str | None:
f"Cannot serialize {fn!r}: missing __module__ or __qualname__. "
"Use a module-level named function for checkpointable callbacks."
)
if "<locals>" in qualname or qualname == "<lambda>":
return None
return f"{module}.{qualname}"

View File

@@ -782,20 +782,6 @@ def _inline_top_level_ref(schema: dict[str, Any]) -> dict[str, Any]:
return schema
def serialize_model_class(value: Any) -> Any:
"""Serialize a ``type[BaseModel]`` field value as its JSON schema.
Args:
value: A ``type[BaseModel]`` subclass, ``None``, or another union member.
Returns:
``value.model_json_schema()`` for model classes, ``value`` otherwise.
"""
if isinstance(value, type) and issubclass(value, BaseModel):
return value.model_json_schema()
return value
def create_model_from_schema( # type: ignore[no-any-unimported]
json_schema: dict[str, Any],
*,

View File

@@ -12,6 +12,7 @@ from typing import Any
from unittest.mock import AsyncMock, Mock, patch
import pytest
from pydantic import BaseModel
from crewai.agents.tools_handler import ToolsHandler as _ToolsHandler
from crewai.agents.step_executor import StepExecutor
@@ -108,6 +109,9 @@ class TestAgentExecutorState:
class TestAgentExecutor:
"""Test AgentExecutor class."""
class StructuredResult(BaseModel):
value: str
@pytest.fixture
def mock_dependencies(self):
"""Create mock dependencies for executor."""
@@ -215,6 +219,49 @@ class TestAgentExecutor:
assert result == "check_iteration"
def test_call_llm_and_parse_does_not_pass_response_model_with_tools(
self, mock_dependencies
):
"""Structured output should not be requested during ReAct tool loops."""
executor = _build_executor(
**mock_dependencies,
original_tools=[Mock()],
response_model=self.StructuredResult,
callbacks=[],
)
executor.state.messages = [{"role": "user", "content": "Use a tool"}]
with patch(
"crewai.experimental.agent_executor.get_llm_response",
return_value="Thought: done\nFinal Answer: complete",
) as get_llm_response_mock:
result = executor.call_llm_and_parse()
assert result == "parsed"
assert get_llm_response_mock.call_args.kwargs["response_model"] is None
def test_call_llm_native_tools_does_not_pass_response_model_with_tools(
self, mock_dependencies
):
"""Structured output should not be requested during native tool calls."""
executor = _build_executor(
**mock_dependencies,
original_tools=[Mock()],
response_model=self.StructuredResult,
callbacks=[],
)
executor._openai_tools = [{"type": "function", "function": {"name": "lookup"}}]
executor.state.messages = [{"role": "user", "content": "Use a tool"}]
with patch(
"crewai.experimental.agent_executor.get_llm_response",
return_value="complete",
) as get_llm_response_mock:
result = executor.call_llm_native_tools()
assert result == "native_finished"
assert get_llm_response_mock.call_args.kwargs["response_model"] is None
def test_finalize_success(self, mock_dependencies):
"""Test finalize with valid AgentFinish."""
with patch.object(AgentExecutor, "_show_logs") as mock_show_logs:

View File

@@ -8,18 +8,8 @@ interactions:
[{"description": "Add two numbers together and return the sum.", "name": "add_numbers",
"parameters_json_schema": {"properties": {"a": {"title": "A", "type": "integer"},
"b": {"title": "B", "type": "integer"}}, "required": ["a", "b"], "type": "object",
"additionalProperties": false}}, {"description": "Use this tool to provide your
final structured response. Call this tool when you have gathered all necessary
information and are ready to provide the final answer in the required format.",
"name": "structured_output", "parameters_json_schema": {"description": "Structured
output for calculation results.", "properties": {"operation": {"description":
"The mathematical operation performed", "title": "Operation", "type": "string"},
"result": {"description": "The result of the calculation", "title": "Result",
"type": "integer"}, "explanation": {"description": "Brief explanation of the
calculation", "title": "Explanation", "type": "string"}}, "required": ["operation",
"result", "explanation"], "title": "CalculationResult", "type": "object", "additionalProperties":
false, "propertyOrdering": ["operation", "result", "explanation"]}}]}], "generationConfig":
{"stopSequences": ["\nObservation:"]}}'
"additionalProperties": false}}]}], "generationConfig": {"stopSequences": ["\nObservation:"],
"thinkingConfig": {"include_thoughts": true}}}'
headers:
User-Agent:
- X-USER-AGENT-XXX
@@ -30,13 +20,13 @@ interactions:
connection:
- keep-alive
content-length:
- '1592'
- '784'
content-type:
- application/json
host:
- generativelanguage.googleapis.com
x-goog-api-client:
- google-genai-sdk/1.49.0 gl-python/3.13.3
- google-genai-sdk/1.65.0 gl-python/3.13.3
x-goog-api-key:
- X-GOOG-API-KEY-XXX
method: POST
@@ -44,27 +34,36 @@ interactions:
response:
body:
string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\":
[\n {\n \"functionCall\": {\n \"name\": \"add_numbers\",\n
\ \"args\": {\n \"a\": 15,\n \"b\":
27\n }\n },\n \"thoughtSignature\": \"CqMFAb4+9vtoEAola3khZd5LD4cccGlQsdVI9cPJGQBURT0qF5Xqp8o1L7oGN4s5trQpk7NPhKe1CYDMXDJueC7zM/zGlcy2daSJAeuTd9pxAbtndEXCGjM/9Nt8QRpvaDV3Ff2bkKSn/JCOJdzsN5m6G5C6BMRGVt8bZyRHelwu7tjCNYiMEvFqVoQIWN6d+CWKkHnbSwOlSUTDXJEcWvUwP82Ou7s68l2k7XNbDWCY5Tt8LUdPgeqjfH15JoEgZUbPxbVKA0ykRln1svfpvQ4Vm3Hn7PL3voWZWGzP5uLnH6JF2M8H6TokSDYZETvlDo5bK1Cx9IzrdUgHkku6gNbct/e53CPEUgqSKbY1VhsLAXAHieT4PKqeMQ4B+7gyCLXHeL6TOGjqSVGBBOQLtF9yCbKbkXa5pPu3+DnPhoOeH7jEPb+bqIWv6rxERErbKhu0IlP+UNBRAAj+wXNDZxQvLnlrlXrLtWllO9wFshr1DzgDgNZSRsPQeVQq2L0bL+KRobCXAfjMpH/8bhxdTI3sgsCtU3+dKwV5Z8Fg6e5oRyBAss8AE2CmYtdnYpt+iss9IT8NlSpI2DcdmVErEFNsebVcSwnr+9YXoESh4O1i8er9lX59hKTBdYXdP2GJ63cq9cSOalzx/doKxA2FzP3QhdV+H11LiUQzsQCXHqv0D+D290z1QoPhpsHEd7b/1EoW7D/2rub4acV8tpUcG2oe/Mj1kzYQoiEwZkgM56JoUs++5+5tWBMW68e4y1AmkyhDTCDkiNIa4noE6AOdNsLjL/+EHvcNFRmayFXXiUShIcMT0WQ9xNriWQP/dbhd6F5K7BKSajdB1391OYeHVmSEzzXYxjnUWXd+jqORQcsiPNIVRQkZI7ZGl6+4exmZsfrKzbFy\"\n
[\n {\n \"text\": \"Okay, here's my thought process on
tackling this simple addition problem:\\n\\n**Calculating 15 + 27**\\n\\nRight,
so I need to calculate 15 plus 27. It's a straightforward arithmetic operation,
and I know I have a dedicated tool for that \u2013 the `add_numbers` function.
No need to reinvent the wheel, I should use the appropriate tool in my toolbox.\\n\\nTo
use the tool effectively, I need to provide the correct inputs. In this case,
I'll set `a` equal to 15, and `b` equal to 27. That should be a piece of cake
for the function. Now I just need to call it!\\n\",\n \"thought\":
true\n },\n {\n \"functionCall\": {\n \"name\":
\"add_numbers\",\n \"args\": {\n \"a\": 15,\n
\ \"b\": 27\n }\n },\n \"thoughtSignature\":
\"CqwBAQw51sfkbjIDS7wS47oc/Bej+AYsUu5Cs+BC2Ae5NMasRmWa/u5Ct426qkpkIpgzGSNjwwfitf1gK93Abse9EGj5m1swXmPU2XSkqhMYMEXZGH1mW2U2XH8zaXHRIAx2aI0O8VbJ3sL8h1lJgbVCkvLa2RyWwY6E8FRPhQOHtrOQEQtfAUtHdJz928j6UEgS818X/7hEwuWsQhIho0frtziX30UlI7yXOeBBWw==\"\n
\ }\n ],\n \"role\": \"model\"\n },\n \"finishReason\":
\"STOP\",\n \"index\": 0,\n \"finishMessage\": \"Model generated
function call(s).\"\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\":
202,\n \"candidatesTokenCount\": 22,\n \"totalTokenCount\": 403,\n \"promptTokensDetails\":
[\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 202\n
\ }\n ],\n \"thoughtsTokenCount\": 179\n },\n \"modelVersion\":
\"gemini-2.5-flash\",\n \"responseId\": \"AlCOadrrK7aVjMcPksrU-A0\"\n}\n"
104,\n \"candidatesTokenCount\": 22,\n \"totalTokenCount\": 173,\n \"promptTokensDetails\":
[\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 104\n
\ }\n ],\n \"thoughtsTokenCount\": 47,\n \"serviceTier\": \"standard\"\n
\ },\n \"modelVersion\": \"gemini-2.5-flash\",\n \"responseId\": \"MpgPapmwO_vN-sAPvrWJmA0\"\n}\n"
headers:
Alt-Svc:
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Type:
- application/json; charset=UTF-8
Date:
- Thu, 12 Feb 2026 22:11:14 GMT
- Thu, 21 May 2026 23:41:40 GMT
Server:
- scaffolding on HTTPServer2
Server-Timing:
- gfet4t7; dur=1417
- gfet4t7; dur=1920
Transfer-Encoding:
- chunked
Vary:
@@ -75,6 +74,8 @@ interactions:
- X-CONTENT-TYPE-XXX
X-Frame-Options:
- X-FRAME-OPTIONS-XXX
X-Gemini-Service-Tier:
- standard
X-XSS-Protection:
- '0'
status:
@@ -83,7 +84,7 @@ interactions:
- request:
body: '{"contents": [{"parts": [{"text": "\nCurrent Task: Calculate 15 + 27 using
your add_numbers tool. Report the result."}], "role": "user"}, {"parts": [{"functionCall":
{"args": {"a": 15, "b": 27}, "name": "add_numbers"}, "thoughtSignature": "CqMFAb4-9vtoEAola3khZd5LD4cccGlQsdVI9cPJGQBURT0qF5Xqp8o1L7oGN4s5trQpk7NPhKe1CYDMXDJueC7zM_zGlcy2daSJAeuTd9pxAbtndEXCGjM_9Nt8QRpvaDV3Ff2bkKSn_JCOJdzsN5m6G5C6BMRGVt8bZyRHelwu7tjCNYiMEvFqVoQIWN6d-CWKkHnbSwOlSUTDXJEcWvUwP82Ou7s68l2k7XNbDWCY5Tt8LUdPgeqjfH15JoEgZUbPxbVKA0ykRln1svfpvQ4Vm3Hn7PL3voWZWGzP5uLnH6JF2M8H6TokSDYZETvlDo5bK1Cx9IzrdUgHkku6gNbct_e53CPEUgqSKbY1VhsLAXAHieT4PKqeMQ4B-7gyCLXHeL6TOGjqSVGBBOQLtF9yCbKbkXa5pPu3-DnPhoOeH7jEPb-bqIWv6rxERErbKhu0IlP-UNBRAAj-wXNDZxQvLnlrlXrLtWllO9wFshr1DzgDgNZSRsPQeVQq2L0bL-KRobCXAfjMpH_8bhxdTI3sgsCtU3-dKwV5Z8Fg6e5oRyBAss8AE2CmYtdnYpt-iss9IT8NlSpI2DcdmVErEFNsebVcSwnr-9YXoESh4O1i8er9lX59hKTBdYXdP2GJ63cq9cSOalzx_doKxA2FzP3QhdV-H11LiUQzsQCXHqv0D-D290z1QoPhpsHEd7b_1EoW7D_2rub4acV8tpUcG2oe_Mj1kzYQoiEwZkgM56JoUs--5-5tWBMW68e4y1AmkyhDTCDkiNIa4noE6AOdNsLjL_-EHvcNFRmayFXXiUShIcMT0WQ9xNriWQP_dbhd6F5K7BKSajdB1391OYeHVmSEzzXYxjnUWXd-jqORQcsiPNIVRQkZI7ZGl6-4exmZsfrKzbFy"}],
{"args": {"a": 15, "b": 27}, "name": "add_numbers"}, "thoughtSignature": "CqwBAQw51sfkbjIDS7wS47oc_Bej-AYsUu5Cs-BC2Ae5NMasRmWa_u5Ct426qkpkIpgzGSNjwwfitf1gK93Abse9EGj5m1swXmPU2XSkqhMYMEXZGH1mW2U2XH8zaXHRIAx2aI0O8VbJ3sL8h1lJgbVCkvLa2RyWwY6E8FRPhQOHtrOQEQtfAUtHdJz928j6UEgS818X_7hEwuWsQhIho0frtziX30UlI7yXOeBBWw=="}],
"role": "model"}, {"parts": [{"functionResponse": {"name": "add_numbers", "response":
{"result": 42}}}], "role": "user"}], "systemInstruction": {"parts": [{"text":
"You are Calculator. You are a calculator assistant that uses tools to compute
@@ -92,18 +93,8 @@ interactions:
numbers together and return the sum.", "name": "add_numbers", "parameters_json_schema":
{"properties": {"a": {"title": "A", "type": "integer"}, "b": {"title": "B",
"type": "integer"}}, "required": ["a", "b"], "type": "object", "additionalProperties":
false}}, {"description": "Use this tool to provide your final structured response.
Call this tool when you have gathered all necessary information and are ready
to provide the final answer in the required format.", "name": "structured_output",
"parameters_json_schema": {"description": "Structured output for calculation
results.", "properties": {"operation": {"description": "The mathematical operation
performed", "title": "Operation", "type": "string"}, "result": {"description":
"The result of the calculation", "title": "Result", "type": "integer"}, "explanation":
{"description": "Brief explanation of the calculation", "title": "Explanation",
"type": "string"}}, "required": ["operation", "result", "explanation"], "title":
"CalculationResult", "type": "object", "additionalProperties": false, "propertyOrdering":
["operation", "result", "explanation"]}}]}], "generationConfig": {"stopSequences":
["\nObservation:"]}}'
false}}]}], "generationConfig": {"stopSequences": ["\nObservation:"], "thinkingConfig":
{"include_thoughts": true}}}'
headers:
User-Agent:
- X-USER-AGENT-XXX
@@ -114,13 +105,13 @@ interactions:
connection:
- keep-alive
content-length:
- '2725'
- '1249'
content-type:
- application/json
host:
- generativelanguage.googleapis.com
x-goog-api-client:
- google-genai-sdk/1.49.0 gl-python/3.13.3
- google-genai-sdk/1.65.0 gl-python/3.13.3
x-goog-api-key:
- X-GOOG-API-KEY-XXX
method: POST
@@ -128,28 +119,24 @@ interactions:
response:
body:
string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\":
[\n {\n \"functionCall\": {\n \"name\": \"structured_output\",\n
\ \"args\": {\n \"result\": 42,\n \"explanation\":
\"The sum of 15 and 27 is 42.\",\n \"operation\": \"Addition\"\n
\ }\n },\n \"thoughtSignature\": \"CtYCAb4+9vsKJoVFV1W8ORKk+Likt7GS9CuzuE53V9sbS2gFuiEjJ7ghBqWDG2UrgyRYFjPl6EalXUBnEbEq9rZNYGY27VpcweI1tv6p+477bgz1pmZnL0nfAcrp4nuphL+Ij0nXZQoo5cF4Gk29RQSNy49VRn3eP9eUW0hG7EpkPmfJiUSSDuaQENHN1UBBnFS9QUC+Fw+unnQ10B57fauyiXWNrBUkE2PYqgj5vELa5lVMtk5beh4ydWNnZ04t8gvQniCJ38EWWQr8VAXrSqE156oCBMwkFaFM7huPWHZk53n/HAG/VsQgPayf045STWKWjBzp6uTiwH9pYtoI1LBah3uxVbJRKOzH7HI4U0cHsffQqIIUn8cW4SP1UK/nvAivU1l0p6Bot8KIVJ5vqoF+o2oDmTuZv0HkDo5+UvXRqfsO5AylpUdM+JMGaXVAA7oZNqVPQybw\"\n
\ }\n ],\n \"role\": \"model\"\n },\n \"finishReason\":
\"STOP\",\n \"index\": 0,\n \"finishMessage\": \"Model generated
function call(s).\"\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\":
240,\n \"candidatesTokenCount\": 39,\n \"totalTokenCount\": 357,\n \"promptTokensDetails\":
[\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 240\n
\ }\n ],\n \"thoughtsTokenCount\": 78\n },\n \"modelVersion\":
\"gemini-2.5-flash\",\n \"responseId\": \"A1COaaWbKvKGjMcPsN-EkAs\"\n}\n"
[\n {\n \"text\": \"The sum of 15 and 27 is 42.\"\n }\n
\ ],\n \"role\": \"model\"\n },\n \"finishReason\":
\"STOP\",\n \"index\": 0\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\":
142,\n \"candidatesTokenCount\": 15,\n \"totalTokenCount\": 157,\n \"promptTokensDetails\":
[\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 142\n
\ }\n ],\n \"serviceTier\": \"standard\"\n },\n \"modelVersion\":
\"gemini-2.5-flash\",\n \"responseId\": \"NZgPasTKBf3F-sAP4Lu48Ak\"\n}\n"
headers:
Alt-Svc:
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Type:
- application/json; charset=UTF-8
Date:
- Thu, 12 Feb 2026 22:11:15 GMT
- Thu, 21 May 2026 23:41:41 GMT
Server:
- scaffolding on HTTPServer2
Server-Timing:
- gfet4t7; dur=906
- gfet4t7; dur=750
Transfer-Encoding:
- chunked
Vary:
@@ -160,6 +147,107 @@ interactions:
- X-CONTENT-TYPE-XXX
X-Frame-Options:
- X-FRAME-OPTIONS-XXX
X-Gemini-Service-Tier:
- standard
X-XSS-Protection:
- '0'
status:
code: 200
message: OK
- request:
body: '{"contents": [{"parts": [{"text": "The sum of 15 and 27 is 42."}], "role":
"user"}], "systemInstruction": {"parts": [{"text": "Format your final answer
according to the following OpenAPI schema: {\n \"type\": \"json_schema\",\n \"json_schema\":
{\n \"name\": \"CalculationResult\",\n \"strict\": true,\n \"schema\":
{\n \"description\": \"Structured output for calculation results.\",\n \"properties\":
{\n \"operation\": {\n \"description\": \"The mathematical operation
performed\",\n \"title\": \"Operation\",\n \"type\": \"string\"\n },\n \"result\":
{\n \"description\": \"The result of the calculation\",\n \"title\":
\"Result\",\n \"type\": \"integer\"\n },\n \"explanation\":
{\n \"description\": \"Brief explanation of the calculation\",\n \"title\":
\"Explanation\",\n \"type\": \"string\"\n }\n },\n \"required\":
[\n \"operation\",\n \"result\",\n \"explanation\"\n ],\n \"title\":
\"CalculationResult\",\n \"type\": \"object\",\n \"additionalProperties\":
false\n }\n }\n}\n\nIMPORTANT: Preserve the original content exactly as-is.
Do NOT rewrite, paraphrase, or modify the meaning of the content. Only structure
it to match the schema format.\n\nDo not include the OpenAPI schema in the final
output. Ensure the final output does not include any code block markers like
```json or ```python."}], "role": "user"}, "generationConfig": {"responseMimeType":
"application/json", "responseJsonSchema": {"description": "Structured output
for calculation results.", "properties": {"operation": {"description": "The
mathematical operation performed", "title": "Operation", "type": "string"},
"result": {"description": "The result of the calculation", "title": "Result",
"type": "integer"}, "explanation": {"description": "Brief explanation of the
calculation", "title": "Explanation", "type": "string"}}, "required": ["operation",
"result", "explanation"], "title": "CalculationResult", "type": "object", "additionalProperties":
false, "propertyOrdering": ["operation", "result", "explanation"]}, "thinkingConfig":
{"include_thoughts": true}}}'
headers:
User-Agent:
- X-USER-AGENT-XXX
accept:
- '*/*'
accept-encoding:
- ACCEPT-ENCODING-XXX
connection:
- keep-alive
content-length:
- '2247'
content-type:
- application/json
host:
- generativelanguage.googleapis.com
x-goog-api-client:
- google-genai-sdk/1.65.0 gl-python/3.13.3
x-goog-api-key:
- X-GOOG-API-KEY-XXX
method: POST
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
response:
body:
string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\":
[\n {\n \"text\": \"**Analyzing the User's Statement**\\n\\nOkay,
so the user has given me the statement: \\\"The sum of 15 and 27 is 42.\\\"
My task is to break this down into a structured JSON object according to the
`CalculationResult` schema I'm working with. This is straightforward; let's
extract the key pieces:\\n\\nFirst, the **operation**: The word \\\"sum\\\"
is a clear indicator that we're dealing with addition. No ambiguity there.\\n\\nNext,
the **result**: The statement explicitly tells us that the result \\\"is 42\\\".
That's a direct, easily extracted value.\\n\\nFinally, the **explanation**:
I can use the entire original statement, \\\"The sum of 15 and 27 is 42.\\\",
as the explanation. It's concise and perfectly encapsulates the context of
the calculation.\\n\",\n \"thought\": true\n },\n {\n
\ \"text\": \"{\\\"operation\\\":\\\"addition\\\",\\\"result\\\":42,\\\"explanation\\\":\\\"The
sum of 15 and 27 is 42.\\\"}\"\n }\n ],\n \"role\":
\"model\"\n },\n \"finishReason\": \"STOP\",\n \"index\": 0\n
\ }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\": 321,\n \"candidatesTokenCount\":
28,\n \"totalTokenCount\": 480,\n \"promptTokensDetails\": [\n {\n
\ \"modality\": \"TEXT\",\n \"tokenCount\": 321\n }\n ],\n
\ \"thoughtsTokenCount\": 131,\n \"serviceTier\": \"standard\"\n },\n
\ \"modelVersion\": \"gemini-2.5-flash\",\n \"responseId\": \"NZgPasDuKf2VjMcPwc6D4Ak\"\n}\n"
headers:
Alt-Svc:
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Type:
- application/json; charset=UTF-8
Date:
- Thu, 21 May 2026 23:41:44 GMT
Server:
- scaffolding on HTTPServer2
Server-Timing:
- gfet4t7; dur=2425
Transfer-Encoding:
- chunked
Vary:
- Origin
- X-Origin
- Referer
X-Content-Type-Options:
- X-CONTENT-TYPE-XXX
X-Frame-Options:
- X-FRAME-OPTIONS-XXX
X-Gemini-Service-Tier:
- standard
X-XSS-Protection:
- '0'
status:

View File

@@ -23,17 +23,8 @@ interactions:
numbers together and return the sum.", "name": "add_numbers", "parameters_json_schema":
{"properties": {"a": {"title": "A", "type": "integer"}, "b": {"title": "B",
"type": "integer"}}, "required": ["a", "b"], "type": "object", "additionalProperties":
false}}, {"description": "Use this tool to provide your final structured response.
Call this tool when you have gathered all necessary information and are ready
to provide the final answer in the required format.", "name": "structured_output",
"parameters_json_schema": {"properties": {"operation": {"description": "The
mathematical operation performed", "title": "Operation", "type": "string"},
"result": {"description": "The result of the calculation", "title": "Result",
"type": "integer"}, "explanation": {"description": "Brief explanation of the
calculation", "title": "Explanation", "type": "string"}}, "required": ["operation",
"result", "explanation"], "title": "CalculationResult", "type": "object", "additionalProperties":
false, "propertyOrdering": ["operation", "result", "explanation"]}}]}], "generationConfig":
{"stopSequences": ["\nObservation:"]}}'
false}}]}], "generationConfig": {"stopSequences": ["\nObservation:"], "thinkingConfig":
{"include_thoughts": true}}}'
headers:
User-Agent:
- X-USER-AGENT-XXX
@@ -44,41 +35,67 @@ interactions:
connection:
- keep-alive
content-length:
- '2763'
- '2016'
content-type:
- application/json
host:
- generativelanguage.googleapis.com
x-goog-api-client:
- google-genai-sdk/1.49.0 gl-python/3.13.12
- google-genai-sdk/1.65.0 gl-python/3.13.3
x-goog-api-key:
- X-GOOG-API-KEY-XXX
method: POST
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-001:generateContent
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
response:
body:
string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\":
[\n {\n \"functionCall\": {\n \"name\": \"add_numbers\",\n
[\n {\n \"text\": \"**My Calculation Process**\\n\\nAlright,
the user wants me to compute 15 + 27. Simple enough. I have a tool specifically
designed for this: `add_numbers(a: int, b: int)`. This is ideal; I'll utilize
that.\\n\\nSo, first step: I need to call the `add_numbers` tool. The inputs
are straightforward: `a` should be 15 and `b` should be 27. I'll execute that
call immediately.\\n\\nOnce I have the result \u2013 the sum \u2013 in hand,
I need to format the output. The user is specific about the structure; it
needs to be a JSON object conforming to that OpenAPI schema. Specifically,
the JSON should include: an \\\"operation\\\" field set to \\\"Addition\\\",
a \\\"result\\\" field populated with the calculated sum, and an \\\"explanation\\\"
field giving some context, which should be \\\"Added 15 and 27 together.\\\"\\n\\nTherefore,
my workflow is: call the tool with the provided numbers, get the sum, and
then construct the final JSON object in the required format.\\n\",\n \"thought\":
true\n },\n {\n \"text\": \"**My Reasoning for
Calculating 15 + 27**\\n\\nOkay, the user wants me to compute 15 plus 27.
No problem; that's straightforward. I've got a dedicated `add_numbers` tool
for this, which is exactly the right approach. Let's see...I need to call
that tool, providing `a=15` and `b=27` as input parameters. Once I receive
the sum, I can move on to formatting the result.\\n\\nNow, the important part:
the output needs to be a JSON object, specifically structured as dictated
by the OpenAPI schema. I need to make sure I include the \\\"operation\\\",
\\\"result\\\", and \\\"explanation\\\" fields. \\\"operation\\\" will be
\\\"Addition\\\", the \\\"result\\\" will naturally be the sum I get from
the tool, and the \\\"explanation\\\" will simply be \\\"Added 15 and 27 together.\\\"
That should cover everything, and the user should get a perfectly formatted
response.\\n\",\n \"thought\": true\n },\n {\n
\ \"functionCall\": {\n \"name\": \"add_numbers\",\n
\ \"args\": {\n \"a\": 15,\n \"b\":
27\n }\n }\n }\n ],\n \"role\":
\"model\"\n },\n \"finishReason\": \"STOP\",\n \"avgLogprobs\":
4.3579145442760951e-06\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\":
377,\n \"candidatesTokenCount\": 7,\n \"totalTokenCount\": 384,\n \"promptTokensDetails\":
[\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 377\n
\ }\n ],\n \"candidatesTokensDetails\": [\n {\n \"modality\":
\"TEXT\",\n \"tokenCount\": 7\n }\n ]\n },\n \"modelVersion\":
\"gemini-2.0-flash-001\",\n \"responseId\": \"vVefaYDSOouXjMcPicLCsQY\"\n}\n"
27\n }\n },\n \"thoughtSignature\": \"CtkGAQw51sf8m1RivBZ7Zp+ZkaqxDdZzSlzepmvlCKak9gl6edIuej/pHxR5dg3qZf89XmHvQ6HigZyzqHcYbSvcRHVGbpNkTr62FC0g10oK5ZEp/r1otLIcgXoVgyFGguJPe/NsfWSX3Uc7ZaYgV0Q2MHBsjUmEHicPH0Pj4Xmbe2I1pK/9DPrzSQqZW3duhLBlBIF9RwZUiltPH6mK+k71l8bN/ebsbbZM18FMXf0wg/7lf3OjvY2wdLDNUD/F2M7T8yfi7NelPWorjIuTGOVWlVRsdGW0QEzuVoyYY7OfbBJC+XsmTumYt+vqgIR3jcQZlA5/3yJdj3e/3mrNmzGt+8VvkjUnu3pz0IUkq2SoTG0+6Y/ajsUI0YA/BFiAXHjrRhH1Nx3ihGWT7E7VzpU/E1ZPFMJIOPLRSpRv8G6ITnjZGthozTZtKLgoHCF7kx4Ni8eVdOh2Us6kY7tYpVabM1dmw0gextEEt+fBMoI+qZGkXdL1YW6SEtQBHh3BGKX8khcrqNvqPZDFzSG0iieMJq7abbEYAIc8zRkeGlWEdX6ES6e+njnFN7JX0Nc32GzOjmpgx9gRhYe+wKonqBQ9RwLLNK+lFuflLTrU3D8jMiPCJyvoRsjdjEc+2JtHXo14ibOVXvZ6oYCHsTEB7f+90/qzcrITESyDBD/rmiT/SqgitBa44MZE9CZ9Ml+BW0xd9FfCy4oLy8w4vszVFDw9Eotr4pEzdCeDeWjMn35taJnWf6jUeF0z/0iyHjbi7XRubJXxI2YuKQ+HRCKX1RFaJWLhmxO4JNBDBqfYZqsO/FefqxjWi2pRzE8U/Upp8Tv/hy1FoN9Abs8W6lPoqgOyEiOcpVkM+u0CgUbf87I1X2EiPpuJF4D9dHlEJqumiPqIGazSLnrjW1qqbM5UpQQuPoTC7q+G092CEnNJBIwrufddZPDfD9rqINpmMa+7OswldKViVaCWR3VsgrSXJj7lVRntCyE2atWxTvtQVnR/JLDdyc98CAUChtAPnC4K/K3OVI4jffQQsHmfeOnTyg0n2VnZ6Yhgo0lMdE4IfMrNOWOuNvHodeHisD2yXjvTCgScO8B3s+EJTvenHMert3nRgjNRmFZ0cRNSjbTeG0UlB9s7Uy0uyrn5ODkKIgEMOdbH9yJU53jInG9boFeMXb1qif47Pc72taZkl6ZaMK4=\"\n
\ }\n ],\n \"role\": \"model\"\n },\n \"finishReason\":
\"STOP\",\n \"index\": 0,\n \"finishMessage\": \"Model generated
function call(s).\"\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\":
383,\n \"candidatesTokenCount\": 22,\n \"totalTokenCount\": 658,\n \"promptTokensDetails\":
[\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 383\n
\ }\n ],\n \"thoughtsTokenCount\": 253,\n \"serviceTier\": \"standard\"\n
\ },\n \"modelVersion\": \"gemini-2.5-flash\",\n \"responseId\": \"qagPas37Ba2R-8YPzYzI8AY\"\n}\n"
headers:
Alt-Svc:
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Type:
- application/json; charset=UTF-8
Date:
- Wed, 25 Feb 2026 20:12:46 GMT
- Fri, 22 May 2026 00:51:56 GMT
Server:
- scaffolding on HTTPServer2
Server-Timing:
- gfet4t7; dur=718
- gfet4t7; dur=3892
Transfer-Encoding:
- chunked
Vary:
@@ -89,6 +106,8 @@ interactions:
- X-CONTENT-TYPE-XXX
X-Frame-Options:
- X-FRAME-OPTIONS-XXX
X-Gemini-Service-Tier:
- standard
X-XSS-Protection:
- '0'
status:
@@ -112,27 +131,17 @@ interactions:
the schema format.\n\nDo not include the OpenAPI schema in the final output.
Ensure the final output does not include any code block markers like ```json
or ```python."}], "role": "user"}, {"parts": [{"functionCall": {"args": {"a":
15, "b": 27}, "name": "add_numbers"}}], "role": "model"}, {"parts": [{"functionResponse":
{"name": "add_numbers", "response": {"result": 42}}}], "role": "user"}, {"parts":
[{"text": "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": "user"}], "systemInstruction": {"parts": [{"text": "You are Calculator.
You are a calculator assistant that uses tools to compute results.\nYour personal
goal is: Perform calculations using available tools"}], "role": "user"}, "tools":
[{"functionDeclarations": [{"description": "Add two numbers together and return
the sum.", "name": "add_numbers", "parameters_json_schema": {"properties": {"a":
{"title": "A", "type": "integer"}, "b": {"title": "B", "type": "integer"}},
"required": ["a", "b"], "type": "object", "additionalProperties": false}}, {"description":
"Use this tool to provide your final structured response. Call this tool when
you have gathered all necessary information and are ready to provide the final
answer in the required format.", "name": "structured_output", "parameters_json_schema":
{"properties": {"operation": {"description": "The mathematical operation performed",
"title": "Operation", "type": "string"}, "result": {"description": "The result
of the calculation", "title": "Result", "type": "integer"}, "explanation": {"description":
"Brief explanation of the calculation", "title": "Explanation", "type": "string"}},
"required": ["operation", "result", "explanation"], "title": "CalculationResult",
"type": "object", "additionalProperties": false, "propertyOrdering": ["operation",
"result", "explanation"]}}]}], "generationConfig": {"stopSequences": ["\nObservation:"]}}'
15, "b": 27}, "name": "add_numbers"}, "thoughtSignature": "CtkGAQw51sf8m1RivBZ7Zp-ZkaqxDdZzSlzepmvlCKak9gl6edIuej_pHxR5dg3qZf89XmHvQ6HigZyzqHcYbSvcRHVGbpNkTr62FC0g10oK5ZEp_r1otLIcgXoVgyFGguJPe_NsfWSX3Uc7ZaYgV0Q2MHBsjUmEHicPH0Pj4Xmbe2I1pK_9DPrzSQqZW3duhLBlBIF9RwZUiltPH6mK-k71l8bN_ebsbbZM18FMXf0wg_7lf3OjvY2wdLDNUD_F2M7T8yfi7NelPWorjIuTGOVWlVRsdGW0QEzuVoyYY7OfbBJC-XsmTumYt-vqgIR3jcQZlA5_3yJdj3e_3mrNmzGt-8VvkjUnu3pz0IUkq2SoTG0-6Y_ajsUI0YA_BFiAXHjrRhH1Nx3ihGWT7E7VzpU_E1ZPFMJIOPLRSpRv8G6ITnjZGthozTZtKLgoHCF7kx4Ni8eVdOh2Us6kY7tYpVabM1dmw0gextEEt-fBMoI-qZGkXdL1YW6SEtQBHh3BGKX8khcrqNvqPZDFzSG0iieMJq7abbEYAIc8zRkeGlWEdX6ES6e-njnFN7JX0Nc32GzOjmpgx9gRhYe-wKonqBQ9RwLLNK-lFuflLTrU3D8jMiPCJyvoRsjdjEc-2JtHXo14ibOVXvZ6oYCHsTEB7f-90_qzcrITESyDBD_rmiT_SqgitBa44MZE9CZ9Ml-BW0xd9FfCy4oLy8w4vszVFDw9Eotr4pEzdCeDeWjMn35taJnWf6jUeF0z_0iyHjbi7XRubJXxI2YuKQ-HRCKX1RFaJWLhmxO4JNBDBqfYZqsO_FefqxjWi2pRzE8U_Upp8Tv_hy1FoN9Abs8W6lPoqgOyEiOcpVkM-u0CgUbf87I1X2EiPpuJF4D9dHlEJqumiPqIGazSLnrjW1qqbM5UpQQuPoTC7q-G092CEnNJBIwrufddZPDfD9rqINpmMa-7OswldKViVaCWR3VsgrSXJj7lVRntCyE2atWxTvtQVnR_JLDdyc98CAUChtAPnC4K_K3OVI4jffQQsHmfeOnTyg0n2VnZ6Yhgo0lMdE4IfMrNOWOuNvHodeHisD2yXjvTCgScO8B3s-EJTvenHMert3nRgjNRmFZ0cRNSjbTeG0UlB9s7Uy0uyrn5ODkKIgEMOdbH9yJU53jInG9boFeMXb1qif47Pc72taZkl6ZaMK4="}],
"role": "model"}, {"parts": [{"functionResponse": {"name": "add_numbers", "response":
{"result": 42}}}], "role": "user"}], "systemInstruction": {"parts": [{"text":
"You are Calculator. You are a calculator assistant that uses tools to compute
results.\nYour personal goal is: Perform calculations using available tools"}],
"role": "user"}, "tools": [{"functionDeclarations": [{"description": "Add two
numbers together and return the sum.", "name": "add_numbers", "parameters_json_schema":
{"properties": {"a": {"title": "A", "type": "integer"}, "b": {"title": "B",
"type": "integer"}}, "required": ["a", "b"], "type": "object", "additionalProperties":
false}}]}], "generationConfig": {"stopSequences": ["\nObservation:"], "thinkingConfig":
{"include_thoughts": true}}}'
headers:
User-Agent:
- X-USER-AGENT-XXX
@@ -143,42 +152,50 @@ interactions:
connection:
- keep-alive
content-length:
- '3166'
- '3441'
content-type:
- application/json
host:
- generativelanguage.googleapis.com
x-goog-api-client:
- google-genai-sdk/1.49.0 gl-python/3.13.12
- google-genai-sdk/1.65.0 gl-python/3.13.3
x-goog-api-key:
- X-GOOG-API-KEY-XXX
method: POST
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-001:generateContent
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
response:
body:
string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\":
[\n {\n \"functionCall\": {\n \"name\": \"structured_output\",\n
\ \"args\": {\n \"result\": 42,\n \"explanation\":
\"15 + 27 = 42\",\n \"operation\": \"addition\"\n }\n
\ }\n }\n ],\n \"role\": \"model\"\n },\n
\ \"finishReason\": \"STOP\",\n \"avgLogprobs\": -0.07498827245500353\n
\ }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\": 421,\n \"candidatesTokenCount\":
18,\n \"totalTokenCount\": 439,\n \"promptTokensDetails\": [\n {\n
\ \"modality\": \"TEXT\",\n \"tokenCount\": 421\n }\n ],\n
\ \"candidatesTokensDetails\": [\n {\n \"modality\": \"TEXT\",\n
\ \"tokenCount\": 18\n }\n ]\n },\n \"modelVersion\": \"gemini-2.0-flash-001\",\n
\ \"responseId\": \"vlefac7bJb6TjMcPzYWh0Ag\"\n}\n"
[\n {\n \"text\": \"**My Calculation and OpenAPI Formatting**\\n\\nOkay,
so I've just crunched the numbers, 15 plus 27, and the answer, as expected,
is 42. Now comes the formatting, the part I truly appreciate. It's time to
translate this into the precise structure that the OpenAPI schema dictates.\\n\\nLet's
see\u2026 I need to populate three key fields: `operation`, `result`, and
`explanation`. This is straightforward. For `operation`, I'll enter \\\"Addition,\\\"
because, well, that's what I did! For `result`, the answer I painstakingly
produced, 42. And for `explanation`, a concise note, \\\"Added 15 and 27 together.\\\"
Perfect. That should do the trick. Now I can move on to the next task.\\n\",\n
\ \"thought\": true\n },\n {\n \"text\":
\"{\\n \\\"operation\\\": \\\"Addition\\\",\\n \\\"result\\\": 42,\\n \\\"explanation\\\":
\\\"Added 15 and 27 together.\\\"\\n}\",\n \"thoughtSignature\":
\"CrcDAQw51sd+vh7cJvN7WGZKQIJGL6Xtr/+CNnL2r9YWGHv5EBrOm/Qdzr4eK0eyrJhuJGBZg5V5XakEuLMULUvth7stE+FTz16F5Vzx2yKMuM5Ictv/DlI7s4Z5WFaI5HJVJTojOhW9K/1TM8y48+eStJre8Qyz3tOvFg1DPEau6JCk+1uoRil2RoZHxFQpd0LaDhl4sKZkRr1e5c2gtfZiK3NHR3ipbRejIOGjDQah4+q4D6ASTiHZxHCBIjAP4HH8DF40oylF6mQR1WHWezKzvkZ0sz3ydPGExM5+lFOE8z1NUpqH2kJ3PfzdrbX2VccIT+5pWz9NsIW8N1JYbPvb7xKENh7mTI+tmCtuAiO9DTwWUf450OHNWdrHQPxn3MMT10+4RMzKBeDAjEFVZ6M68CyUDTJ0uDtqJZ6SXs3ZdmmLGQY1lAk29r0eklaMJLk226uMUKCXlHKwyDbJVH3+SwGkgU+Am81AiekV5ZWQjfe3gUN8ojOLSNvpAfaf/K8ZvR4uGCEMs2pJ2sqI6sCIE9w9+u2biTattqWtcO+wmPXZ/CwU/ASXHlBqt55cvB2XyFTXO7kQGw==\"\n
\ }\n ],\n \"role\": \"model\"\n },\n \"finishReason\":
\"STOP\",\n \"index\": 0\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\":
421,\n \"candidatesTokenCount\": 36,\n \"totalTokenCount\": 560,\n \"promptTokensDetails\":
[\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 421\n
\ }\n ],\n \"thoughtsTokenCount\": 103,\n \"serviceTier\": \"standard\"\n
\ },\n \"modelVersion\": \"gemini-2.5-flash\",\n \"responseId\": \"ragPaseeA6nT_uMP653cmAc\"\n}\n"
headers:
Alt-Svc:
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Content-Type:
- application/json; charset=UTF-8
Date:
- Wed, 25 Feb 2026 20:12:47 GMT
- Fri, 22 May 2026 00:51:59 GMT
Server:
- scaffolding on HTTPServer2
Server-Timing:
- gfet4t7; dur=774
- gfet4t7; dur=2377
Transfer-Encoding:
- chunked
Vary:
@@ -189,6 +206,8 @@ interactions:
- X-CONTENT-TYPE-XXX
X-Frame-Options:
- X-FRAME-OPTIONS-XXX
X-Gemini-Service-Tier:
- standard
X-XSS-Protection:
- '0'
status:

View File

@@ -11,7 +11,6 @@ from crewai.events.event_context import (
MismatchBehavior,
StackDepthExceededError,
_event_context_config,
_event_id_stack,
EventContextConfig,
get_current_parent_id,
get_enclosing_parent_id,
@@ -22,7 +21,6 @@ from crewai.events.event_context import (
pop_event_scope,
push_event_scope,
reset_last_event_id,
resume_task_scope,
set_last_event_id,
set_triggering_event_id,
triggered_by_scope,
@@ -182,91 +180,6 @@ class TestTriggeredByScope:
assert get_triggering_event_id() is None
class TestResumeTaskScope:
"""Tests for the checkpoint-resume scope helper."""
@pytest.fixture(autouse=True)
def _reset_stack(self) -> None:
_event_id_stack.set(())
def _bind_runtime_state(self, *event_dicts: dict[str, object]):
from crewai.events import crewai_event_bus
from crewai.events.types.task_events import TaskStartedEvent
from crewai.state.event_record import EventRecord
from crewai.state.runtime import RuntimeState
record = EventRecord()
for spec in event_dicts:
ev = TaskStartedEvent(context=None, task=None)
ev.task_id = spec["task_id"] # type: ignore[assignment]
ev.event_id = spec["event_id"] # type: ignore[assignment]
ev.emission_sequence = spec["emission_sequence"] # type: ignore[assignment]
record.add(ev)
state = RuntimeState(root=[])
state._event_record = record
previous = crewai_event_bus._runtime_state
crewai_event_bus._runtime_state = state
return crewai_event_bus, previous
def test_returns_false_when_no_runtime_state(self) -> None:
from crewai.events import crewai_event_bus
previous = crewai_event_bus._runtime_state
crewai_event_bus._runtime_state = None
try:
assert resume_task_scope("any-task") is False
assert _event_id_stack.get() == ()
finally:
crewai_event_bus._runtime_state = previous
def test_returns_false_when_no_matching_event(self) -> None:
bus, previous = self._bind_runtime_state(
{"task_id": "other", "event_id": "e1", "emission_sequence": 1},
)
try:
assert resume_task_scope("missing") is False
assert _event_id_stack.get() == ()
finally:
bus._runtime_state = previous
def test_pushes_latest_event_for_task(self) -> None:
bus, previous = self._bind_runtime_state(
{"task_id": "t1", "event_id": "e1", "emission_sequence": 1},
{"task_id": "t1", "event_id": "e2", "emission_sequence": 5},
{"task_id": "t1", "event_id": "e3", "emission_sequence": 3},
{"task_id": "t2", "event_id": "x1", "emission_sequence": 9},
)
try:
assert resume_task_scope("t1") is True
assert _event_id_stack.get() == (("e2", "task_started"),)
finally:
bus._runtime_state = previous
def test_pairs_cleanly_with_task_completed(self) -> None:
"""The pushed scope must be popped by a matching task_completed."""
from crewai.events import crewai_event_bus
from crewai.events.types.task_events import TaskCompletedEvent
from crewai.tasks.task_output import TaskOutput
push_event_scope("kickoff-1", "crew_kickoff_started")
bus, previous = self._bind_runtime_state(
{"task_id": "t1", "event_id": "started-1", "emission_sequence": 1},
)
try:
assert resume_task_scope("t1") is True
output = TaskOutput(description="d", raw="r", agent="a")
completed = TaskCompletedEvent(output=output, task=None)
completed.task_id = "t1"
crewai_event_bus.emit(None, completed)
crewai_event_bus.flush()
assert _event_id_stack.get() == (("kickoff-1", "crew_kickoff_started"),)
assert completed.started_event_id == "started-1"
finally:
bus._runtime_state = previous
_event_id_stack.set(())
def test_agent_scope_preserved_after_tool_error_event() -> None:
from crewai.events import crewai_event_bus
from crewai.events.types.tool_usage_events import (

View File

@@ -1025,7 +1025,7 @@ def test_gemini_crew_structured_output_with_tools():
role="Calculator",
goal="Perform calculations using available tools",
backstory="You are a calculator assistant that uses tools to compute results.",
llm=LLM(model="google/gemini-2.0-flash-001"),
llm=LLM(model="google/gemini-2.5-flash"),
tools=[add_numbers],
)

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import functools
import os
from collections.abc import Callable
from typing import Any
import pytest
from pydantic import BaseModel, ValidationError
@@ -94,18 +93,10 @@ class TestCallableToString:
result = callable_to_string(print)
assert result == "builtins.print"
def test_lambda_returns_none(self) -> None:
def test_lambda_produces_locals_path(self) -> None:
fn = lambda: None # noqa: E731
assert callable_to_string(fn) is None
def test_closure_returns_none(self) -> None:
def outer() -> Callable[[], None]:
def inner() -> None:
return None
return inner
assert callable_to_string(outer()) is None
result = callable_to_string(fn)
assert "<lambda>" in result
def test_missing_qualname_raises(self) -> None:
obj = type("NoQual", (), {"__module__": "test"})()

View File

@@ -189,7 +189,6 @@ exclude-newer = "3 days"
# authlib <1.6.11 has GHSA-jj8c-mmj3-mmgv (CSRF bypass in cache-based state storage).
# pip <26.1.1 has GHSA-58qw-9mgm-455v (archive handling); OSV considers 26.1.1 unaffected.
# paramiko <5.0.0 has GHSA-r374-rxx8-8654 (SHA-1 in rsakey.py); OSV considers 5.0.0 unaffected. Transitive via composio-core.
# starlette <1.0.1 has PYSEC-2026-161 (missing Host header validation poisons request.url.path, bypassing path-based auth). Transitive via fastapi.
# litellm 1.83.8+ hard-pins openai==2.24.0, missing openai.types.responses used by crewai;
# override to >=2.30.0 (the version litellm 1.83.7 used) until upstream relaxes the pin.
override-dependencies = [
@@ -210,7 +209,6 @@ override-dependencies = [
"authlib>=1.6.11",
"pip>=26.1.1",
"paramiko>=5.0.0",
"starlette>=1.0.1",
]
[tool.uv.workspace]

12
uv.lock generated
View File

@@ -13,12 +13,9 @@ resolution-markers = [
]
[options]
exclude-newer = "2026-05-19T15:27:50.647689Z"
exclude-newer = "2026-05-17T14:20:01.778505Z"
exclude-newer-span = "P3D"
[options.exclude-newer-package]
starlette = "2026-05-22T16:00:00Z"
[manifest]
members = [
"crewai",
@@ -43,7 +40,6 @@ overrides = [
{ name = "pypdf", specifier = ">=6.10.2,<7" },
{ name = "python-multipart", specifier = ">=0.0.27,<1" },
{ name = "rich", specifier = ">=13.7.1" },
{ name = "starlette", specifier = ">=1.0.1" },
{ name = "transformers", marker = "python_full_version >= '3.10'", specifier = ">=5.4.0" },
{ name = "urllib3", specifier = ">=2.7.0" },
{ name = "uv", specifier = ">=0.11.6,<1" },
@@ -8532,15 +8528,15 @@ wheels = [
[[package]]
name = "starlette"
version = "1.0.1"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/a3/84e821cc54b4ab50ae6dbc6ac3800a651b65ec35f045cc73785380654057/starlette-1.0.1.tar.gz", hash = "sha256:512399c5f1de7fac99c88572212ded9ddeddef2fb32afa82d724000e88b38f4f", size = 2659596, upload-time = "2026-05-21T21:58:58.433Z" }
sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/e1/b2df4bc09a1e51ff664c1e17018a4274b42e5e9352e4a478ea540512dc88/starlette-1.0.1-py3-none-any.whl", hash = "sha256:7c0e69b2ee1c848bd54669d908500117a3ee13de603a21427e5c6fc1adf98dcd", size = 72802, upload-time = "2026-05-21T21:58:56.551Z" },
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
]
[[package]]