mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-26 11:28:12 +00:00
Compare commits
5 Commits
main
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
584baaef55 | ||
|
|
8b2e7e6004 | ||
|
|
1700a38fd2 | ||
|
|
3349cfcb92 | ||
|
|
4b08a8308d |
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 "/"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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/
|
||||
)
|
||||
|
||||
@@ -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():
|
||||
"""
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
|
||||
@@ -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],
|
||||
*,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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],
|
||||
)
|
||||
|
||||
|
||||
@@ -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"})()
|
||||
|
||||
@@ -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
12
uv.lock
generated
@@ -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]]
|
||||
|
||||
Reference in New Issue
Block a user