mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-06 07:29:24 +00:00
fix: address CI failures — ruff, mypy, mock OpenAI tests, JSONC support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,14 +10,13 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
from rich.markup import escape as _rich_escape
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from rich.markup import escape as _rich_escape
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Horizontal, Vertical, VerticalScroll
|
||||
@@ -33,10 +32,11 @@ from textual.widgets import (
|
||||
RadioButton,
|
||||
RadioSet,
|
||||
Static,
|
||||
TabbedContent,
|
||||
TabPane,
|
||||
TabbedContent,
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
from textual.suggester import Suggester
|
||||
|
||||
@@ -122,7 +122,6 @@ def _history_dir() -> Path:
|
||||
|
||||
class ChatBubble(Static):
|
||||
"""A styled chat message bubble."""
|
||||
pass
|
||||
|
||||
|
||||
_STATE_ICONS = {
|
||||
@@ -150,7 +149,9 @@ class ThinkingIndicator(Static):
|
||||
self._prev_output: int = 0
|
||||
self._step_start: float = time.monotonic()
|
||||
|
||||
def update_status(self, state: str, detail: str | None, input_tokens: int, output_tokens: int) -> None:
|
||||
def update_status(
|
||||
self, state: str, detail: str | None, input_tokens: int, output_tokens: int
|
||||
) -> None:
|
||||
label = detail or state or "working…"
|
||||
# Mark the previous step as done (skip the initial placeholder,
|
||||
# but keep its creation timestamp so the first real step inherits it)
|
||||
@@ -200,7 +201,9 @@ class ThinkingIndicator(Static):
|
||||
lines: list[str] = []
|
||||
for step in self._steps:
|
||||
lines.append(step)
|
||||
current = f"[{_CORAL}]{ch}[/] [{_DIM}]{self._agent_name}[/] {self._current_status}"
|
||||
current = (
|
||||
f"[{_CORAL}]{ch}[/] [{_DIM}]{self._agent_name}[/] {self._current_status}"
|
||||
)
|
||||
if self._tokens:
|
||||
current += f" {self._tokens}"
|
||||
lines.append(current)
|
||||
@@ -255,7 +258,9 @@ class CreateRoomScreen(ModalScreen[dict[str, Any] | None]):
|
||||
yield Checkbox(name, value=True, id=f"cb-{name}")
|
||||
yield Label("Engagement")
|
||||
with RadioSet(id="engagement-radio"):
|
||||
yield RadioButton("Organic — agents auto-respond", value=True, id="radio-organic")
|
||||
yield RadioButton(
|
||||
"Organic — agents auto-respond", value=True, id="radio-organic"
|
||||
)
|
||||
yield RadioButton("Tagged — @mention required", id="radio-tagged")
|
||||
with Horizontal():
|
||||
yield Button("Create", variant="primary", id="btn-create-room")
|
||||
@@ -272,7 +277,8 @@ class CreateRoomScreen(ModalScreen[dict[str, Any] | None]):
|
||||
name_input.focus()
|
||||
return
|
||||
agents = [
|
||||
n for n in self._agent_names
|
||||
n
|
||||
for n in self._agent_names
|
||||
if self.query_one(f"#cb-{n}", Checkbox).value
|
||||
]
|
||||
radio = self.query_one("#engagement-radio", RadioSet)
|
||||
@@ -510,8 +516,13 @@ class AgentTUI(App[None]):
|
||||
yield Static("AGENTS", classes="sidebar-label")
|
||||
yield OptionList(id="agent-list")
|
||||
with TabPane("Memory", id="tab-memory"):
|
||||
yield Static("Click below to open the memory browser.", id="memory-scope-label")
|
||||
yield Button("Open Memory Browser", id="btn-memory", variant="default")
|
||||
yield Static(
|
||||
"Click below to open the memory browser.",
|
||||
id="memory-scope-label",
|
||||
)
|
||||
yield Button(
|
||||
"Open Memory Browser", id="btn-memory", variant="default"
|
||||
)
|
||||
with Horizontal(id="sidebar-actions"):
|
||||
yield Button("Provenance", id="btn-provenance", variant="default")
|
||||
with Vertical(id="chat-area"):
|
||||
@@ -571,6 +582,7 @@ class AgentTUI(App[None]):
|
||||
self._status_listener = None
|
||||
try:
|
||||
from crewai.events.event_bus import CrewAIEventsBus
|
||||
|
||||
bus = CrewAIEventsBus()
|
||||
except Exception:
|
||||
bus = None
|
||||
@@ -581,9 +593,7 @@ class AgentTUI(App[None]):
|
||||
|
||||
@bus.on(NewAgentStatusUpdateEvent)
|
||||
def _on_status_update(source: Any, event: Any) -> None:
|
||||
self.call_from_thread(
|
||||
self._handle_status_update, source, event
|
||||
)
|
||||
self.call_from_thread(self._handle_status_update, source, event)
|
||||
|
||||
self._status_listener = _on_status_update
|
||||
except Exception:
|
||||
@@ -626,6 +636,7 @@ class AgentTUI(App[None]):
|
||||
|
||||
try:
|
||||
from crewai.new_agent.scheduler import TaskScheduler
|
||||
|
||||
self._scheduler = TaskScheduler()
|
||||
self._scheduler.set_callback(self._on_scheduled_task_due)
|
||||
self._scheduler.start()
|
||||
@@ -645,7 +656,9 @@ class AgentTUI(App[None]):
|
||||
if self._is_room(self._current_room):
|
||||
engagement = self._room_engagement(self._current_room)
|
||||
if engagement == "organic":
|
||||
chat_input.placeholder = "Type a message — agents will respond automatically"
|
||||
chat_input.placeholder = (
|
||||
"Type a message — agents will respond automatically"
|
||||
)
|
||||
else:
|
||||
chat_input.placeholder = "Use @agent_name to direct your message"
|
||||
else:
|
||||
@@ -738,7 +751,8 @@ class AgentTUI(App[None]):
|
||||
if not targets and self._is_room(self._current_room):
|
||||
room_agent_names = self._room_agents(self._current_room)
|
||||
room_agent_defs = [
|
||||
d for d in self._agent_defs
|
||||
d
|
||||
for d in self._agent_defs
|
||||
if d.get("name", d.get("role", "unnamed")) in room_agent_names
|
||||
]
|
||||
engagement = self._room_engagement(self._current_room)
|
||||
@@ -758,9 +772,7 @@ class AgentTUI(App[None]):
|
||||
and scored[1][1] >= top_score * self._RELEVANCE_TIE_THRESHOLD
|
||||
):
|
||||
best.append(scored[1][0])
|
||||
targets = [
|
||||
d.get("name", d.get("role", "unnamed")) for d in best
|
||||
]
|
||||
targets = [d.get("name", d.get("role", "unnamed")) for d in best]
|
||||
else:
|
||||
targets = [self._last_active_agent or room_agent_names[0]]
|
||||
elif len(room_agent_names) == 1:
|
||||
@@ -768,8 +780,7 @@ class AgentTUI(App[None]):
|
||||
else:
|
||||
first = room_agent_names[0] if room_agent_names else "agent"
|
||||
self._mount_sys(
|
||||
"Tip: use @agent_name to direct your message, "
|
||||
f"e.g. @{first}"
|
||||
f"Tip: use @agent_name to direct your message, e.g. @{first}"
|
||||
)
|
||||
return
|
||||
|
||||
@@ -782,13 +793,9 @@ class AgentTUI(App[None]):
|
||||
scroll.mount(thinking)
|
||||
if near_bottom:
|
||||
scroll.scroll_end(animate=False)
|
||||
asyncio.ensure_future(
|
||||
self._process(targets[0], clean_text, thinking, room)
|
||||
)
|
||||
asyncio.ensure_future(self._process(targets[0], clean_text, thinking, room))
|
||||
else:
|
||||
asyncio.ensure_future(
|
||||
self._process_multi(targets, clean_text, room)
|
||||
)
|
||||
asyncio.ensure_future(self._process_multi(targets, clean_text, room))
|
||||
|
||||
# ── Organic mode relevance check (GAP-28) ──
|
||||
|
||||
@@ -839,9 +846,7 @@ class AgentTUI(App[None]):
|
||||
if not isinstance(names, list):
|
||||
return None
|
||||
|
||||
name_to_def = {
|
||||
d.get("name", d.get("role", "unnamed")): d for d in agents
|
||||
}
|
||||
name_to_def = {d.get("name", d.get("role", "unnamed")): d for d in agents}
|
||||
scored: list[tuple[dict[str, Any], int]] = []
|
||||
for rank, name in enumerate(names):
|
||||
if name in name_to_def:
|
||||
@@ -869,11 +874,52 @@ class AgentTUI(App[None]):
|
||||
return stems
|
||||
|
||||
_STOP_WORDS: set[str] = {
|
||||
"the", "a", "an", "is", "to", "and", "or", "of", "in", "it", "on",
|
||||
"for", "i", "my", "me", "can", "you", "do", "what", "how", "please",
|
||||
"help", "this", "that", "with", "are", "be", "was", "were", "has",
|
||||
"have", "had", "will", "would", "could", "should", "about", "just",
|
||||
"not", "but", "if", "they", "them", "their", "there", "here",
|
||||
"the",
|
||||
"a",
|
||||
"an",
|
||||
"is",
|
||||
"to",
|
||||
"and",
|
||||
"or",
|
||||
"of",
|
||||
"in",
|
||||
"it",
|
||||
"on",
|
||||
"for",
|
||||
"i",
|
||||
"my",
|
||||
"me",
|
||||
"can",
|
||||
"you",
|
||||
"do",
|
||||
"what",
|
||||
"how",
|
||||
"please",
|
||||
"help",
|
||||
"this",
|
||||
"that",
|
||||
"with",
|
||||
"are",
|
||||
"be",
|
||||
"was",
|
||||
"were",
|
||||
"has",
|
||||
"have",
|
||||
"had",
|
||||
"will",
|
||||
"would",
|
||||
"could",
|
||||
"should",
|
||||
"about",
|
||||
"just",
|
||||
"not",
|
||||
"but",
|
||||
"if",
|
||||
"they",
|
||||
"them",
|
||||
"their",
|
||||
"there",
|
||||
"here",
|
||||
}
|
||||
|
||||
_RELEVANCE_TIE_THRESHOLD: float = 0.8
|
||||
@@ -892,11 +938,13 @@ class AgentTUI(App[None]):
|
||||
|
||||
scored: list[tuple[dict[str, Any], int]] = []
|
||||
for agent in agents:
|
||||
agent_text = " ".join([
|
||||
agent.get("role", ""),
|
||||
agent.get("goal", ""),
|
||||
agent.get("backstory", ""),
|
||||
]).lower()
|
||||
agent_text = " ".join(
|
||||
[
|
||||
agent.get("role", ""),
|
||||
agent.get("goal", ""),
|
||||
agent.get("backstory", ""),
|
||||
]
|
||||
).lower()
|
||||
agent_words = set(agent_text.split()) - self._STOP_WORDS
|
||||
agent_stems = self._stem_words(agent_words)
|
||||
|
||||
@@ -967,6 +1015,7 @@ class AgentTUI(App[None]):
|
||||
"""Show or cancel scheduled tasks."""
|
||||
try:
|
||||
from crewai.new_agent.scheduler import TaskScheduler
|
||||
|
||||
scheduler = TaskScheduler()
|
||||
except Exception:
|
||||
self._mount_sys("Scheduler not available.")
|
||||
@@ -983,14 +1032,19 @@ class AgentTUI(App[None]):
|
||||
show_all = len(parts) > 1 and parts[1] == "all"
|
||||
tasks = scheduler.list_tasks(include_done=show_all)
|
||||
if not tasks:
|
||||
self._mount_sys("No scheduled tasks." if not show_all else "No tasks found.")
|
||||
self._mount_sys(
|
||||
"No scheduled tasks." if not show_all else "No tasks found."
|
||||
)
|
||||
return
|
||||
|
||||
lines: list[str] = [f"[bold]Scheduled Tasks[/] ({len(tasks)})"]
|
||||
for t in tasks:
|
||||
status_icon = {
|
||||
"pending": "◻", "running": "▶", "completed": "✓",
|
||||
"failed": "✗", "cancelled": "—",
|
||||
"pending": "◻",
|
||||
"running": "▶",
|
||||
"completed": "✓",
|
||||
"failed": "✗",
|
||||
"cancelled": "—",
|
||||
}.get(t.status, "?")
|
||||
agent = t.agent_name or "unknown"
|
||||
due = t.next_run_at[:16].replace("T", " ") if t.next_run_at else "—"
|
||||
@@ -1189,27 +1243,31 @@ class AgentTUI(App[None]):
|
||||
|
||||
async def _call_agent(target: str) -> tuple[str, Any, Exception | None]:
|
||||
try:
|
||||
agent = await asyncio.to_thread(
|
||||
self._get_or_create_agent, target
|
||||
)
|
||||
agent = await asyncio.to_thread(self._get_or_create_agent, target)
|
||||
if agent is None:
|
||||
error_detail = getattr(self, "_last_agent_error", "")
|
||||
detail = f": {error_detail}" if error_detail else ""
|
||||
return target, None, ValueError(f"Could not load '{target}'{detail}")
|
||||
return (
|
||||
target,
|
||||
None,
|
||||
ValueError(f"Could not load '{target}'{detail}"),
|
||||
)
|
||||
msg = room_context if room_context else text
|
||||
resp = await asyncio.to_thread(agent.message, msg)
|
||||
return target, resp, None
|
||||
except Exception as exc:
|
||||
return target, None, exc
|
||||
|
||||
results = await asyncio.gather(
|
||||
*[_call_agent(t) for t in targets]
|
||||
)
|
||||
results = await asyncio.gather(*[_call_agent(t) for t in targets])
|
||||
|
||||
for target, response, error in results:
|
||||
await self._safe_remove(indicators.get(target)) # type: ignore[arg-type]
|
||||
if error or response is None:
|
||||
msg = f"Error from {target}: {error}" if error else f"Could not load agent '{target}'."
|
||||
msg = (
|
||||
f"Error from {target}: {error}"
|
||||
if error
|
||||
else f"Could not load agent '{target}'."
|
||||
)
|
||||
self._append_msg(room, "system", msg)
|
||||
if self._current_room == room:
|
||||
self._mount_sys(msg)
|
||||
@@ -1242,9 +1300,7 @@ class AgentTUI(App[None]):
|
||||
history = self._chat_histories.get(room, [])
|
||||
# Only include user and agent messages (skip system)
|
||||
relevant = [
|
||||
(sender, content)
|
||||
for sender, content, _ in history
|
||||
if sender != "system"
|
||||
(sender, content) for sender, content, _ in history if sender != "system"
|
||||
]
|
||||
if not relevant:
|
||||
return ""
|
||||
@@ -1304,7 +1360,9 @@ class AgentTUI(App[None]):
|
||||
stream_start = time.monotonic()
|
||||
stream_chars = 0
|
||||
|
||||
def _stream_markup(text: str, final: bool = False, metadata: str = "") -> str:
|
||||
def _stream_markup(
|
||||
text: str, final: bool = False, metadata: str = ""
|
||||
) -> str:
|
||||
rendered = _safe_render(text)
|
||||
mk = f"[bold {_CORAL}]{target}[/]\n{rendered}"
|
||||
if final:
|
||||
@@ -1341,7 +1399,9 @@ class AgentTUI(App[None]):
|
||||
response = getattr(agent, "last_stream_result", None)
|
||||
meta_parts: list[str] = []
|
||||
if response:
|
||||
if getattr(response, "input_tokens", 0) or getattr(response, "output_tokens", 0):
|
||||
if getattr(response, "input_tokens", 0) or getattr(
|
||||
response, "output_tokens", 0
|
||||
):
|
||||
meta_parts.append(
|
||||
f"↑ {response.input_tokens or 0:,} "
|
||||
f"↓ {response.output_tokens or 0:,} tokens"
|
||||
@@ -1351,7 +1411,9 @@ class AgentTUI(App[None]):
|
||||
metadata = " · ".join(meta_parts)
|
||||
|
||||
if bubble is not None:
|
||||
bubble.update(_stream_markup(accumulated, final=True, metadata=metadata))
|
||||
bubble.update(
|
||||
_stream_markup(accumulated, final=True, metadata=metadata)
|
||||
)
|
||||
|
||||
content = accumulated or (response.content if response else "")
|
||||
self._append_msg(room, target, content, metadata)
|
||||
@@ -1379,11 +1441,7 @@ class AgentTUI(App[None]):
|
||||
return self._agent_instances[name]
|
||||
|
||||
defn = next(
|
||||
(
|
||||
d
|
||||
for d in self._agent_defs
|
||||
if d.get("name", d.get("role", "")) == name
|
||||
),
|
||||
(d for d in self._agent_defs if d.get("name", d.get("role", "")) == name),
|
||||
None,
|
||||
)
|
||||
if defn is None:
|
||||
@@ -1409,9 +1467,7 @@ class AgentTUI(App[None]):
|
||||
return True
|
||||
return scroll.scroll_y >= scroll.max_scroll_y - 80
|
||||
|
||||
def _mount_bubble(
|
||||
self, sender: str, content: str, metadata: str = ""
|
||||
) -> None:
|
||||
def _mount_bubble(self, sender: str, content: str, metadata: str = "") -> None:
|
||||
scroll = self.query_one("#chat-scroll", VerticalScroll)
|
||||
near_bottom = self._is_near_bottom(scroll)
|
||||
scroll.mount(self._make_bubble(sender, content, metadata))
|
||||
@@ -1421,9 +1477,7 @@ class AgentTUI(App[None]):
|
||||
def _mount_sys(self, text: str) -> None:
|
||||
self._mount_bubble("system", text)
|
||||
|
||||
def _make_bubble(
|
||||
self, sender: str, content: str, metadata: str = ""
|
||||
) -> ChatBubble:
|
||||
def _make_bubble(self, sender: str, content: str, metadata: str = "") -> ChatBubble:
|
||||
if sender == "You":
|
||||
markup = f"[bold #e8e8e8]You[/]\n{_safe_render(content)}"
|
||||
return ChatBubble(markup, classes="user-bubble")
|
||||
@@ -1484,9 +1538,7 @@ class AgentTUI(App[None]):
|
||||
for room, msgs in self._chat_histories.items():
|
||||
safe = room.replace("/", "_").replace("\\", "_")
|
||||
path = hdir / f"{safe}.json"
|
||||
data = [
|
||||
{"sender": s, "content": c, "metadata": m} for s, c, m in msgs
|
||||
]
|
||||
data = [{"sender": s, "content": c, "metadata": m} for s, c, m in msgs]
|
||||
try:
|
||||
path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
||||
except Exception:
|
||||
@@ -1504,8 +1556,7 @@ class AgentTUI(App[None]):
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
self._chat_histories[room] = [
|
||||
(d["sender"], d["content"], d.get("metadata", ""))
|
||||
for d in data
|
||||
(d["sender"], d["content"], d.get("metadata", "")) for d in data
|
||||
]
|
||||
except Exception:
|
||||
pass
|
||||
@@ -1515,9 +1566,14 @@ class AgentTUI(App[None]):
|
||||
def _launch_memory_browser(self) -> None:
|
||||
"""Suspend this TUI and launch the memory browser as a subprocess."""
|
||||
import subprocess
|
||||
|
||||
with self.suspend():
|
||||
subprocess.run(
|
||||
[sys.executable, "-c", "from crewai_cli.memory_tui import MemoryTUI; MemoryTUI().run()"],
|
||||
[
|
||||
sys.executable,
|
||||
"-c",
|
||||
"from crewai_cli.memory_tui import MemoryTUI; MemoryTUI().run()",
|
||||
],
|
||||
)
|
||||
|
||||
def _find_agent_with_pending_suggestion(self) -> str | None:
|
||||
@@ -1539,7 +1595,9 @@ class AgentTUI(App[None]):
|
||||
return self._current_room
|
||||
return None
|
||||
|
||||
def on_tabbed_content_tab_activated(self, event: TabbedContent.TabActivated) -> None:
|
||||
def on_tabbed_content_tab_activated(
|
||||
self, event: TabbedContent.TabActivated
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
# ── Sidebar buttons ──
|
||||
@@ -1563,6 +1621,7 @@ class AgentTUI(App[None]):
|
||||
|
||||
try:
|
||||
from crewai.new_agent.cli_provider import _get_storage
|
||||
|
||||
entries = _get_storage(agent_name).load_provenance()
|
||||
except Exception:
|
||||
entries = []
|
||||
@@ -1571,7 +1630,9 @@ class AgentTUI(App[None]):
|
||||
self._mount_sys(f"No provenance data for {agent_name}.")
|
||||
return
|
||||
|
||||
lines = [f"[bold {_CORAL}]Provenance — {agent_name}[/] ({len(entries)} entries)\n"]
|
||||
lines = [
|
||||
f"[bold {_CORAL}]Provenance — {agent_name}[/] ({len(entries)} entries)\n"
|
||||
]
|
||||
for i, entry in enumerate(entries[-10:], 1):
|
||||
action = getattr(entry, "action", "?")
|
||||
reasoning = getattr(entry, "reasoning", "") or ""
|
||||
@@ -1629,7 +1690,9 @@ class AgentTUI(App[None]):
|
||||
self._render_chat()
|
||||
self._mount_sys(f"Room '{display}' created with {n_agents} agent(s).")
|
||||
|
||||
def _save_room_to_config(self, name: str, agents: list[str], engagement: str) -> None:
|
||||
def _save_room_to_config(
|
||||
self, name: str, agents: list[str], engagement: str
|
||||
) -> None:
|
||||
try:
|
||||
if self._config_path.exists():
|
||||
raw = self._config_path.read_text(encoding="utf-8")
|
||||
@@ -1656,6 +1719,7 @@ class AgentTUI(App[None]):
|
||||
pass
|
||||
try:
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
|
||||
crewai_event_bus.shutdown(wait=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -90,9 +90,7 @@ def load_benchmark_cases(path: str | Path) -> LoadedCases:
|
||||
if threshold is not None:
|
||||
threshold = float(threshold)
|
||||
if "cases" not in data:
|
||||
raise ValueError(
|
||||
"Object-format benchmark file must have a 'cases' array"
|
||||
)
|
||||
raise ValueError("Object-format benchmark file must have a 'cases' array")
|
||||
data = data["cases"]
|
||||
|
||||
if not isinstance(data, list):
|
||||
@@ -103,16 +101,19 @@ def load_benchmark_cases(path: str | Path) -> LoadedCases:
|
||||
if not isinstance(item, dict):
|
||||
raise ValueError(f"Benchmark case at index {i} must be a JSON object")
|
||||
if "input" not in item:
|
||||
raise ValueError(f"Benchmark case at index {i} missing required 'input' field")
|
||||
raise ValueError(
|
||||
f"Benchmark case at index {i} missing required 'input' field"
|
||||
)
|
||||
cases.append(BenchmarkCase(**item))
|
||||
|
||||
return LoadedCases(cases, threshold)
|
||||
|
||||
|
||||
def _strip_jsonc_comments(text: str) -> str:
|
||||
"""Strip // and /* */ comments from JSONC text."""
|
||||
"""Strip // and /* */ comments and trailing commas from JSONC text."""
|
||||
result = re.sub(r"(?<!:)//.*?$", "", text, flags=re.MULTILINE)
|
||||
result = re.sub(r"/\*.*?\*/", "", result, flags=re.DOTALL)
|
||||
result = re.sub(r",\s*([}\]])", r"\1", result)
|
||||
return result
|
||||
|
||||
|
||||
@@ -172,12 +173,14 @@ async def _judge_with_llm(
|
||||
def _parse_definition(source: Any) -> dict[str, Any]:
|
||||
"""Parse an agent definition — delegates to crewai's parser."""
|
||||
from crewai.new_agent.definition_parser import parse_agent_definition
|
||||
|
||||
return parse_agent_definition(source)
|
||||
|
||||
|
||||
def _load_agent(source: Any, agents_dir: Path | None = None) -> Any:
|
||||
"""Load a NewAgent from a definition — delegates to crewai's loader."""
|
||||
from crewai.new_agent.definition_parser import load_agent_from_definition
|
||||
|
||||
return load_agent_from_definition(source, agents_dir=agents_dir)
|
||||
|
||||
|
||||
@@ -202,7 +205,15 @@ async def _run_model_benchmark(
|
||||
|
||||
async def _run_case(i: int, case: BenchmarkCase) -> BenchmarkResult:
|
||||
async with sem:
|
||||
emit({"type": "case_start", "model": model, "case_index": i, "total_cases": total, "input": case.input})
|
||||
emit(
|
||||
{
|
||||
"type": "case_start",
|
||||
"model": model,
|
||||
"case_index": i,
|
||||
"total_cases": total,
|
||||
"input": case.input,
|
||||
}
|
||||
)
|
||||
|
||||
bench_defn = dict(defn)
|
||||
bench_defn["settings"] = dict(defn.get("settings", {}))
|
||||
@@ -216,10 +227,26 @@ async def _run_model_benchmark(
|
||||
try:
|
||||
agent = _load_agent(bench_defn, agents_dir=agents_dir)
|
||||
except Exception as e:
|
||||
emit({"type": "case_done", "model": model, "case_index": i, "total_cases": total, "passed": False, "score": 0.0, "time_ms": 0, "error": str(e)})
|
||||
emit(
|
||||
{
|
||||
"type": "case_done",
|
||||
"model": model,
|
||||
"case_index": i,
|
||||
"total_cases": total,
|
||||
"passed": False,
|
||||
"score": 0.0,
|
||||
"time_ms": 0,
|
||||
"error": str(e),
|
||||
}
|
||||
)
|
||||
return BenchmarkResult(
|
||||
case_index=i, input=case.input, expected=case.expected,
|
||||
actual=f"[Agent creation error: {e}]", model=model, passed=False, score=0.0,
|
||||
case_index=i,
|
||||
input=case.input,
|
||||
expected=case.expected,
|
||||
actual=f"[Agent creation error: {e}]",
|
||||
model=model,
|
||||
passed=False,
|
||||
score=0.0,
|
||||
)
|
||||
|
||||
start_ms = _current_time_ms()
|
||||
@@ -235,18 +262,50 @@ async def _run_model_benchmark(
|
||||
cost = response.cost
|
||||
except asyncio.TimeoutError:
|
||||
elapsed_ms = _current_time_ms() - start_ms
|
||||
emit({"type": "case_done", "model": model, "case_index": i, "total_cases": total, "passed": False, "score": 0.0, "time_ms": elapsed_ms, "error": "timeout"})
|
||||
emit(
|
||||
{
|
||||
"type": "case_done",
|
||||
"model": model,
|
||||
"case_index": i,
|
||||
"total_cases": total,
|
||||
"passed": False,
|
||||
"score": 0.0,
|
||||
"time_ms": elapsed_ms,
|
||||
"error": "timeout",
|
||||
}
|
||||
)
|
||||
return BenchmarkResult(
|
||||
case_index=i, input=case.input, expected=case.expected,
|
||||
actual=f"[Timeout after {_CASE_TIMEOUT_SECONDS}s]", model=model, passed=False, score=0.0,
|
||||
case_index=i,
|
||||
input=case.input,
|
||||
expected=case.expected,
|
||||
actual=f"[Timeout after {_CASE_TIMEOUT_SECONDS}s]",
|
||||
model=model,
|
||||
passed=False,
|
||||
score=0.0,
|
||||
response_time_ms=elapsed_ms,
|
||||
)
|
||||
except Exception as e:
|
||||
elapsed_ms = _current_time_ms() - start_ms
|
||||
emit({"type": "case_done", "model": model, "case_index": i, "total_cases": total, "passed": False, "score": 0.0, "time_ms": elapsed_ms, "error": str(e)})
|
||||
emit(
|
||||
{
|
||||
"type": "case_done",
|
||||
"model": model,
|
||||
"case_index": i,
|
||||
"total_cases": total,
|
||||
"passed": False,
|
||||
"score": 0.0,
|
||||
"time_ms": elapsed_ms,
|
||||
"error": str(e),
|
||||
}
|
||||
)
|
||||
return BenchmarkResult(
|
||||
case_index=i, input=case.input, expected=case.expected,
|
||||
actual=f"[Error: {e}]", model=model, passed=False, score=0.0,
|
||||
case_index=i,
|
||||
input=case.input,
|
||||
expected=case.expected,
|
||||
actual=f"[Error: {e}]",
|
||||
model=model,
|
||||
passed=False,
|
||||
score=0.0,
|
||||
response_time_ms=elapsed_ms,
|
||||
)
|
||||
|
||||
@@ -254,7 +313,14 @@ async def _run_model_benchmark(
|
||||
if case.expected is not None:
|
||||
passed, score = _check_expected(case.expected, actual)
|
||||
if case.criteria is not None:
|
||||
emit({"type": "judging", "model": model, "case_index": i, "total_cases": total})
|
||||
emit(
|
||||
{
|
||||
"type": "judging",
|
||||
"model": model,
|
||||
"case_index": i,
|
||||
"total_cases": total,
|
||||
}
|
||||
)
|
||||
try:
|
||||
criteria_passed, criteria_score = await asyncio.wait_for(
|
||||
_judge_with_llm(case.criteria, case.input, actual, judge_model),
|
||||
@@ -268,29 +334,60 @@ async def _run_model_benchmark(
|
||||
else:
|
||||
passed, score = criteria_passed, criteria_score
|
||||
|
||||
emit({"type": "case_done", "model": model, "case_index": i, "total_cases": total, "passed": passed, "score": score, "time_ms": elapsed_ms})
|
||||
emit(
|
||||
{
|
||||
"type": "case_done",
|
||||
"model": model,
|
||||
"case_index": i,
|
||||
"total_cases": total,
|
||||
"passed": passed,
|
||||
"score": score,
|
||||
"time_ms": elapsed_ms,
|
||||
}
|
||||
)
|
||||
return BenchmarkResult(
|
||||
case_index=i, input=case.input, expected=case.expected, actual=actual,
|
||||
model=model, passed=passed, score=score,
|
||||
input_tokens=input_tokens, output_tokens=output_tokens,
|
||||
response_time_ms=elapsed_ms, cost=cost,
|
||||
case_index=i,
|
||||
input=case.input,
|
||||
expected=case.expected,
|
||||
actual=actual,
|
||||
model=model,
|
||||
passed=passed,
|
||||
score=score,
|
||||
input_tokens=input_tokens,
|
||||
output_tokens=output_tokens,
|
||||
response_time_ms=elapsed_ms,
|
||||
cost=cost,
|
||||
)
|
||||
|
||||
model_results = await asyncio.gather(*[_run_case(i, case) for i, case in enumerate(cases)])
|
||||
model_results = await asyncio.gather(
|
||||
*[_run_case(i, case) for i, case in enumerate(cases)]
|
||||
)
|
||||
|
||||
total_passed = sum(1 for r in model_results if r.passed)
|
||||
avg_score = sum(r.score for r in model_results) / len(model_results) if model_results else 0.0
|
||||
total_time = max(r.response_time_ms for r in model_results) / 1000 if model_results else 0.0
|
||||
avg_score = (
|
||||
sum(r.score for r in model_results) / len(model_results)
|
||||
if model_results
|
||||
else 0.0
|
||||
)
|
||||
total_time = (
|
||||
max(r.response_time_ms for r in model_results) / 1000 if model_results else 0.0
|
||||
)
|
||||
total_in = sum(r.input_tokens for r in model_results)
|
||||
total_out = sum(r.output_tokens for r in model_results)
|
||||
total_cost = sum(r.cost for r in model_results if r.cost is not None)
|
||||
emit({
|
||||
"type": "model_done", "model": model,
|
||||
"passed": total_passed, "total": len(model_results),
|
||||
"avg_score": avg_score, "total_time": total_time,
|
||||
"input_tokens": total_in, "output_tokens": total_out,
|
||||
"total_cost": total_cost if total_cost > 0 else None,
|
||||
})
|
||||
emit(
|
||||
{
|
||||
"type": "model_done",
|
||||
"model": model,
|
||||
"passed": total_passed,
|
||||
"total": len(model_results),
|
||||
"avg_score": avg_score,
|
||||
"total_time": total_time,
|
||||
"input_tokens": total_in,
|
||||
"output_tokens": total_out,
|
||||
"total_cost": total_cost if total_cost > 0 else None,
|
||||
}
|
||||
)
|
||||
|
||||
return model_results
|
||||
|
||||
@@ -332,7 +429,15 @@ async def run_benchmark(
|
||||
on_progress(event)
|
||||
|
||||
tasks = [
|
||||
_run_model_benchmark(defn, model, cases, judge_model, _emit, agents_dir=agents_dir, verbose=verbose)
|
||||
_run_model_benchmark(
|
||||
defn,
|
||||
model,
|
||||
cases,
|
||||
judge_model,
|
||||
_emit,
|
||||
agents_dir=agents_dir,
|
||||
verbose=verbose,
|
||||
)
|
||||
for model in models
|
||||
]
|
||||
all_results = await asyncio.gather(*tasks)
|
||||
@@ -345,11 +450,13 @@ class SuppressBenchmarkOutput:
|
||||
|
||||
def __enter__(self):
|
||||
import logging
|
||||
|
||||
self._saved_formatter = None
|
||||
try:
|
||||
from crewai.events.listeners.tracing.trace_listener import (
|
||||
TraceCollectionListener,
|
||||
)
|
||||
|
||||
listener = TraceCollectionListener._instance
|
||||
if listener:
|
||||
self._saved_formatter = listener.formatter
|
||||
@@ -357,7 +464,12 @@ class SuppressBenchmarkOutput:
|
||||
except Exception:
|
||||
pass
|
||||
self._loggers = []
|
||||
for name in (None, "crewai.new_agent.event_listener", "crewai.new_agent.executor", "crewai"):
|
||||
for name in (
|
||||
None,
|
||||
"crewai.new_agent.event_listener",
|
||||
"crewai.new_agent.executor",
|
||||
"crewai",
|
||||
):
|
||||
lg = logging.getLogger(name)
|
||||
self._loggers.append((lg, lg.level))
|
||||
lg.setLevel(logging.CRITICAL)
|
||||
@@ -371,6 +483,7 @@ class SuppressBenchmarkOutput:
|
||||
from crewai.events.listeners.tracing.trace_listener import (
|
||||
TraceCollectionListener,
|
||||
)
|
||||
|
||||
listener = TraceCollectionListener._instance
|
||||
if listener:
|
||||
listener.formatter = self._saved_formatter
|
||||
@@ -384,22 +497,26 @@ class VerboseBenchmarkOutput:
|
||||
def __enter__(self):
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.new_agent.events import (
|
||||
NewAgentLLMCallStartedEvent,
|
||||
NewAgentContextSummarizedEvent,
|
||||
NewAgentLLMCallCompletedEvent,
|
||||
NewAgentLLMCallFailedEvent,
|
||||
NewAgentToolUsageStartedEvent,
|
||||
NewAgentLLMCallStartedEvent,
|
||||
NewAgentStatusUpdateEvent,
|
||||
NewAgentToolUsageCompletedEvent,
|
||||
NewAgentToolUsageFailedEvent,
|
||||
NewAgentStatusUpdateEvent,
|
||||
NewAgentContextSummarizedEvent,
|
||||
NewAgentToolUsageStartedEvent,
|
||||
)
|
||||
|
||||
# Suppress Rich formatter panels — we print our own structured output
|
||||
self._saved_formatter = None
|
||||
try:
|
||||
from crewai.events.listeners.tracing.trace_listener import TraceCollectionListener
|
||||
from crewai.events.listeners.tracing.trace_listener import (
|
||||
TraceCollectionListener,
|
||||
)
|
||||
|
||||
listener = TraceCollectionListener._instance
|
||||
if listener:
|
||||
self._saved_formatter = listener.formatter
|
||||
@@ -409,7 +526,12 @@ class VerboseBenchmarkOutput:
|
||||
|
||||
# Quiet loggers to WARNING — keep warnings visible, suppress debug/info spam
|
||||
self._loggers = []
|
||||
for name in (None, "crewai.new_agent.event_listener", "crewai.new_agent.executor", "crewai"):
|
||||
for name in (
|
||||
None,
|
||||
"crewai.new_agent.event_listener",
|
||||
"crewai.new_agent.executor",
|
||||
"crewai",
|
||||
):
|
||||
lg = logging.getLogger(name)
|
||||
self._loggers.append((lg, lg.level))
|
||||
lg.setLevel(logging.WARNING)
|
||||
@@ -420,29 +542,39 @@ class VerboseBenchmarkOutput:
|
||||
fl = sys.stderr.flush
|
||||
|
||||
def _on_llm_start(_src, ev: NewAgentLLMCallStartedEvent):
|
||||
w(f"\033[36m[llm] calling {ev.model}…\033[0m\n"); fl()
|
||||
w(f"\033[36m[llm] calling {ev.model}…\033[0m\n")
|
||||
fl()
|
||||
|
||||
def _on_llm_done(_src, ev: NewAgentLLMCallCompletedEvent):
|
||||
w(f"\033[36m[llm] {ev.model} {ev.input_tokens}→{ev.output_tokens} tokens {ev.response_time_ms}ms\033[0m\n"); fl()
|
||||
w(
|
||||
f"\033[36m[llm] {ev.model} {ev.input_tokens}→{ev.output_tokens} tokens {ev.response_time_ms}ms\033[0m\n"
|
||||
)
|
||||
fl()
|
||||
|
||||
def _on_llm_fail(_src, ev: NewAgentLLMCallFailedEvent):
|
||||
w(f"\033[31m[llm] FAILED: {ev.error[:200]}\033[0m\n"); fl()
|
||||
w(f"\033[31m[llm] FAILED: {ev.error[:200]}\033[0m\n")
|
||||
fl()
|
||||
|
||||
def _on_tool_start(_src, ev: NewAgentToolUsageStartedEvent):
|
||||
w(f"\033[33m[tool] using {ev.tool_name}…\033[0m\n"); fl()
|
||||
w(f"\033[33m[tool] using {ev.tool_name}…\033[0m\n")
|
||||
fl()
|
||||
|
||||
def _on_tool_done(_src, ev: NewAgentToolUsageCompletedEvent):
|
||||
w(f"\033[33m[tool] {ev.tool_name} done\033[0m\n"); fl()
|
||||
w(f"\033[33m[tool] {ev.tool_name} done\033[0m\n")
|
||||
fl()
|
||||
|
||||
def _on_tool_fail(_src, ev: NewAgentToolUsageFailedEvent):
|
||||
w(f"\033[31m[tool] {ev.tool_name} FAILED: {ev.error[:200]}\033[0m\n"); fl()
|
||||
w(f"\033[31m[tool] {ev.tool_name} FAILED: {ev.error[:200]}\033[0m\n")
|
||||
fl()
|
||||
|
||||
def _on_status(_src, ev: NewAgentStatusUpdateEvent):
|
||||
if ev.detail:
|
||||
w(f"\033[2m[status] {ev.state}: {ev.detail}\033[0m\n"); fl()
|
||||
w(f"\033[2m[status] {ev.state}: {ev.detail}\033[0m\n")
|
||||
fl()
|
||||
|
||||
def _on_summarized(_src, ev: NewAgentContextSummarizedEvent):
|
||||
w(f"\033[35m[context] summarized — context was too large\033[0m\n"); fl()
|
||||
w("\033[35m[context] summarized — context was too large\033[0m\n")
|
||||
fl()
|
||||
|
||||
pairs = [
|
||||
(NewAgentLLMCallStartedEvent, _on_llm_start),
|
||||
@@ -469,7 +601,10 @@ class VerboseBenchmarkOutput:
|
||||
lg.setLevel(level)
|
||||
if self._saved_formatter is not None:
|
||||
try:
|
||||
from crewai.events.listeners.tracing.trace_listener import TraceCollectionListener
|
||||
from crewai.events.listeners.tracing.trace_listener import (
|
||||
TraceCollectionListener,
|
||||
)
|
||||
|
||||
listener = TraceCollectionListener._instance
|
||||
if listener:
|
||||
listener.formatter = self._saved_formatter
|
||||
@@ -490,6 +625,7 @@ class ArtifactsSandbox:
|
||||
|
||||
def __enter__(self):
|
||||
import os
|
||||
|
||||
self._base.mkdir(parents=True, exist_ok=True)
|
||||
gitignore = self._base / ".gitignore"
|
||||
if not gitignore.exists():
|
||||
@@ -500,6 +636,7 @@ class ArtifactsSandbox:
|
||||
|
||||
def __exit__(self, *exc):
|
||||
import os
|
||||
|
||||
if self._prev_cwd:
|
||||
os.chdir(self._prev_cwd)
|
||||
|
||||
@@ -554,9 +691,11 @@ def format_results_table(results: list[BenchmarkResult]) -> str:
|
||||
lines.append("-" * 80)
|
||||
n = len(results)
|
||||
avg_score = total_score / n if n > 0 else 0.0
|
||||
lines.append(f"Total: {total_passed}/{n} passed | Avg score: {avg_score:.2f} | "
|
||||
f"Tokens: {total_input_tokens}/{total_output_tokens} | "
|
||||
f"Total time: {total_time_ms}ms")
|
||||
lines.append(
|
||||
f"Total: {total_passed}/{n} passed | Avg score: {avg_score:.2f} | "
|
||||
f"Tokens: {total_input_tokens}/{total_output_tokens} | "
|
||||
f"Total time: {total_time_ms}ms"
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -623,6 +762,7 @@ def format_comparison_table(results_by_model: dict[str, list[BenchmarkResult]])
|
||||
# Rich-based terminal charts
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _score_color(score: float) -> str:
|
||||
if score >= 0.7:
|
||||
return "green"
|
||||
@@ -680,7 +820,7 @@ def print_results_chart(
|
||||
|
||||
rows: list[str] = []
|
||||
for r in results:
|
||||
inp = r.input[:input_w - 1] + "…" if len(r.input) >= input_w else r.input
|
||||
inp = r.input[: input_w - 1] + "…" if len(r.input) >= input_w else r.input
|
||||
inp_pad = inp + " " * max(0, input_w - len(inp))
|
||||
bar = _score_bar(r.score, bar_w)
|
||||
badge = "[green]PASS[/green]" if r.passed else "[red]FAIL[/red]"
|
||||
@@ -746,10 +886,16 @@ def print_comparison_chart(
|
||||
avg = sum(r.score for r in results) / n if n else 0.0
|
||||
total_time = max((r.response_time_ms for r in results), default=0) / 1000
|
||||
total_tokens = sum(r.input_tokens + r.output_tokens for r in results)
|
||||
models_data.append({
|
||||
"model": model, "passed": passed, "n": n,
|
||||
"avg": avg, "time": total_time, "tokens": total_tokens,
|
||||
})
|
||||
models_data.append(
|
||||
{
|
||||
"model": model,
|
||||
"passed": passed,
|
||||
"n": n,
|
||||
"avg": avg,
|
||||
"time": total_time,
|
||||
"tokens": total_tokens,
|
||||
}
|
||||
)
|
||||
max_time = max(max_time, total_time)
|
||||
max_tokens = max(max_tokens, total_tokens)
|
||||
|
||||
@@ -768,10 +914,18 @@ def print_comparison_chart(
|
||||
lines: list[str] = []
|
||||
for md in models_data:
|
||||
name_raw = md["model"]
|
||||
name = (name_raw[:max_name_len - 1] + "…" if len(name_raw) > max_name_len else name_raw).ljust(max_name_len)
|
||||
name = (
|
||||
name_raw[: max_name_len - 1] + "…"
|
||||
if len(name_raw) > max_name_len
|
||||
else name_raw
|
||||
).ljust(max_name_len)
|
||||
bar = _score_bar(md["avg"], bar_width)
|
||||
pass_color = _score_color(md["avg"])
|
||||
star = " [bold green]★[/bold green]" if best and md["model"] == best["model"] else ""
|
||||
star = (
|
||||
" [bold green]★[/bold green]"
|
||||
if best and md["model"] == best["model"]
|
||||
else ""
|
||||
)
|
||||
tokens_str = _fmt_tokens(md["tokens"])
|
||||
lines.append(
|
||||
f" {name} {bar} {md['avg']:.2f} "
|
||||
|
||||
@@ -45,7 +45,7 @@ def _get_cli_version() -> str:
|
||||
# Prefer crewai version if installed (keeps existing UX)
|
||||
try:
|
||||
return get_version("crewai")
|
||||
except Exception: # noqa: S110
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return get_version("crewai-cli")
|
||||
@@ -58,6 +58,7 @@ def _get_cli_version() -> str:
|
||||
def crewai() -> None:
|
||||
"""Top-level command group for crewai."""
|
||||
from pathlib import Path
|
||||
|
||||
env_path = Path.cwd() / ".env"
|
||||
if env_path.exists():
|
||||
try:
|
||||
@@ -130,7 +131,9 @@ def create(
|
||||
elif type == "agent":
|
||||
create_agent(name)
|
||||
else:
|
||||
click.secho("Error: Invalid type. Must be 'crew', 'flow', or 'agent'.", fg="red")
|
||||
click.secho(
|
||||
"Error: Invalid type. Must be 'crew', 'flow', or 'agent'.", fg="red"
|
||||
)
|
||||
|
||||
|
||||
@crewai.command()
|
||||
@@ -226,10 +229,15 @@ def _train_new_agents(agent_files: list, n_iterations: int) -> None:
|
||||
continue
|
||||
|
||||
click.echo()
|
||||
click.secho(f"Training {agent_name} ({len(cases)} cases, {n_iterations} iterations)", fg="cyan", bold=True)
|
||||
click.secho(
|
||||
f"Training {agent_name} ({len(cases)} cases, {n_iterations} iterations)",
|
||||
fg="cyan",
|
||||
bold=True,
|
||||
)
|
||||
|
||||
try:
|
||||
from crewai.new_agent.definition_parser import load_agent_from_definition
|
||||
|
||||
agent = load_agent_from_definition(str(agent_path))
|
||||
except Exception as e:
|
||||
click.secho(f" Error loading agent {agent_name}: {e}", fg="red")
|
||||
@@ -248,6 +256,7 @@ def _train_new_agents(agent_files: list, n_iterations: int) -> None:
|
||||
|
||||
try:
|
||||
import time as _time
|
||||
|
||||
_t0 = _time.monotonic()
|
||||
with _console.status("[cyan] Running…[/]", spinner="dots"):
|
||||
response = asyncio.run(agent.amessage(user_input))
|
||||
@@ -279,7 +288,9 @@ def _train_new_agents(agent_files: list, n_iterations: int) -> None:
|
||||
if agents_trained == 0:
|
||||
click.secho("No agents with matching benchmark cases found.", fg="yellow")
|
||||
else:
|
||||
click.secho(f"Training complete ({agents_trained} agent(s)).", fg="green", bold=True)
|
||||
click.secho(
|
||||
f"Training complete ({agents_trained} agent(s)).", fg="green", bold=True
|
||||
)
|
||||
|
||||
|
||||
@crewai.command()
|
||||
@@ -512,7 +523,8 @@ def memory(
|
||||
"Defaults to test.judge_model in config.json (openai/gpt-4o-mini if not set).",
|
||||
)
|
||||
@click.option(
|
||||
"-v", "--verbose",
|
||||
"-v",
|
||||
"--verbose",
|
||||
is_flag=True,
|
||||
help="Show agent execution details (tool calls, LLM responses, errors).",
|
||||
)
|
||||
@@ -534,13 +546,25 @@ def test(
|
||||
from crewai_cli.run_crew import _needs_uv_relaunch, _relaunch_via_uv
|
||||
|
||||
agents_dir = Path("agents")
|
||||
agent_files = sorted(agents_dir.glob("*.json")) + sorted(agents_dir.glob("*.jsonc")) if agents_dir.is_dir() else []
|
||||
agent_files = (
|
||||
sorted(agents_dir.glob("*.json")) + sorted(agents_dir.glob("*.jsonc"))
|
||||
if agents_dir.is_dir()
|
||||
else []
|
||||
)
|
||||
|
||||
if agent_files:
|
||||
effective_judge = judge_model or _read_config("test", "judge_model") or "openai/gpt-4o-mini"
|
||||
effective_judge = (
|
||||
judge_model or _read_config("test", "judge_model") or "openai/gpt-4o-mini"
|
||||
)
|
||||
|
||||
if _needs_uv_relaunch():
|
||||
uv_args = ["test", "-n", str(n_iterations), "--judge-model", effective_judge]
|
||||
uv_args = [
|
||||
"test",
|
||||
"-n",
|
||||
str(n_iterations),
|
||||
"--judge-model",
|
||||
effective_judge,
|
||||
]
|
||||
if threshold is not None:
|
||||
uv_args.extend(["--threshold", str(threshold)])
|
||||
if model:
|
||||
@@ -554,12 +578,25 @@ def test(
|
||||
config_threshold = _read_config("test", "threshold")
|
||||
if config_threshold is None:
|
||||
config_threshold = _read_config("test_threshold")
|
||||
effective_threshold = threshold if threshold is not None else (float(config_threshold) if config_threshold is not None else 0.7)
|
||||
effective_threshold = (
|
||||
threshold
|
||||
if threshold is not None
|
||||
else (float(config_threshold) if config_threshold is not None else 0.7)
|
||||
)
|
||||
|
||||
_test_new_agents(agent_files, n_iterations, model, effective_threshold, effective_judge, verbose=verbose)
|
||||
_test_new_agents(
|
||||
agent_files,
|
||||
n_iterations,
|
||||
model,
|
||||
effective_threshold,
|
||||
effective_judge,
|
||||
verbose=verbose,
|
||||
)
|
||||
else:
|
||||
crew_model = model or "gpt-4o-mini"
|
||||
click.echo(f"Testing the crew for {n_iterations} iterations with model {crew_model}")
|
||||
click.echo(
|
||||
f"Testing the crew for {n_iterations} iterations with model {crew_model}"
|
||||
)
|
||||
evaluate_crew(n_iterations, crew_model, trained_agents_file=trained_agents_file)
|
||||
|
||||
|
||||
@@ -577,6 +614,7 @@ def _read_config(*keys: str) -> Any:
|
||||
try:
|
||||
raw = config_path.read_text(encoding="utf-8")
|
||||
import re
|
||||
|
||||
clean = re.sub(r"(?<!:)//.*?$", "", raw, flags=re.MULTILINE)
|
||||
clean = re.sub(r"/\*.*?\*/", "", clean, flags=re.DOTALL)
|
||||
data = json.loads(clean)
|
||||
@@ -596,12 +634,14 @@ class _BenchmarkLiveProgress:
|
||||
|
||||
def __init__(self, console=None):
|
||||
from rich.console import Console
|
||||
|
||||
self._console = console or Console()
|
||||
self._state: dict[str, dict] = {}
|
||||
self._live = None
|
||||
|
||||
def start(self):
|
||||
from rich.live import Live
|
||||
|
||||
self._live = Live(
|
||||
self._render(),
|
||||
console=self._console,
|
||||
@@ -622,10 +662,15 @@ class _BenchmarkLiveProgress:
|
||||
|
||||
if t == "model_start":
|
||||
self._state[model] = {
|
||||
"done": 0, "total": event["total_cases"],
|
||||
"status": "starting", "passed": 0,
|
||||
"avg": 0.0, "time": 0.0,
|
||||
"in_tokens": 0, "out_tokens": 0, "cost": None,
|
||||
"done": 0,
|
||||
"total": event["total_cases"],
|
||||
"status": "starting",
|
||||
"passed": 0,
|
||||
"avg": 0.0,
|
||||
"time": 0.0,
|
||||
"in_tokens": 0,
|
||||
"out_tokens": 0,
|
||||
"cost": None,
|
||||
}
|
||||
elif t == "case_start":
|
||||
self._state[model]["status"] = "running"
|
||||
@@ -667,14 +712,14 @@ class _BenchmarkLiveProgress:
|
||||
n_cols = 7 if has_cost else 6
|
||||
|
||||
table = Table(box=box.SIMPLE, show_header=False, padding=(0, 1), expand=False)
|
||||
table.add_column("", width=1) # icon
|
||||
table.add_column("", no_wrap=True) # model
|
||||
table.add_column("", no_wrap=True, justify="right") # passed or bar
|
||||
table.add_column("", no_wrap=True, justify="right") # score
|
||||
table.add_column("", no_wrap=True, justify="right") # time
|
||||
table.add_column("", no_wrap=True, justify="right") # tokens
|
||||
table.add_column("", width=1) # icon
|
||||
table.add_column("", no_wrap=True) # model
|
||||
table.add_column("", no_wrap=True, justify="right") # passed or bar
|
||||
table.add_column("", no_wrap=True, justify="right") # score
|
||||
table.add_column("", no_wrap=True, justify="right") # time
|
||||
table.add_column("", no_wrap=True, justify="right") # tokens
|
||||
if has_cost:
|
||||
table.add_column("", no_wrap=True, justify="right") # cost
|
||||
table.add_column("", no_wrap=True, justify="right") # cost
|
||||
|
||||
for model, info in self._state.items():
|
||||
if info["status"] == "done":
|
||||
@@ -683,10 +728,15 @@ class _BenchmarkLiveProgress:
|
||||
cols = [
|
||||
icon,
|
||||
model,
|
||||
Text.from_markup(f"[{color}]{info['passed']}/{info['total']}[/{color}]"),
|
||||
Text.from_markup(
|
||||
f"[{color}]{info['passed']}/{info['total']}[/{color}]"
|
||||
),
|
||||
Text.from_markup(f"[{color}]{info['avg']:.2f}[/{color}]"),
|
||||
Text(f"{info['time']:.1f}s", style="dim"),
|
||||
Text(f"↑{_fmt_tokens(info['in_tokens'])} ↓{_fmt_tokens(info['out_tokens'])}", style="dim"),
|
||||
Text(
|
||||
f"↑{_fmt_tokens(info['in_tokens'])} ↓{_fmt_tokens(info['out_tokens'])}",
|
||||
style="dim",
|
||||
),
|
||||
]
|
||||
if has_cost:
|
||||
if info["cost"] is not None:
|
||||
@@ -749,12 +799,14 @@ def _test_new_agents(
|
||||
continue
|
||||
|
||||
file_threshold = loaded.threshold if loaded.threshold is not None else threshold
|
||||
jobs.append({
|
||||
"agent_name": agent_name,
|
||||
"agent_path": str(agent_path.resolve()),
|
||||
"cases": loaded.cases,
|
||||
"threshold": file_threshold,
|
||||
})
|
||||
jobs.append(
|
||||
{
|
||||
"agent_name": agent_name,
|
||||
"agent_path": str(agent_path.resolve()),
|
||||
"cases": loaded.cases,
|
||||
"threshold": file_threshold,
|
||||
}
|
||||
)
|
||||
|
||||
if not jobs:
|
||||
click.secho("No agents with matching benchmark cases found.", fg="yellow")
|
||||
@@ -771,6 +823,7 @@ def _test_new_agents(
|
||||
if "model" in prefixed:
|
||||
prefixed["model"] = f"{agent_name}/{prefixed['model']}"
|
||||
progress.on_progress(prefixed)
|
||||
|
||||
return _cb
|
||||
|
||||
async def _run_all():
|
||||
@@ -782,7 +835,9 @@ def _test_new_agents(
|
||||
cases=job["cases"],
|
||||
models=model_list,
|
||||
judge_model=judge_model,
|
||||
on_progress=None if verbose else _make_progress_cb(job["agent_name"]),
|
||||
on_progress=None
|
||||
if verbose
|
||||
else _make_progress_cb(job["agent_name"]),
|
||||
verbose=verbose,
|
||||
)
|
||||
)
|
||||
@@ -792,10 +847,15 @@ def _test_new_agents(
|
||||
click.echo()
|
||||
click.secho(
|
||||
f"Testing {len(jobs)} agent(s), {case_count} cases (threshold={threshold})",
|
||||
fg="cyan", bold=True,
|
||||
fg="cyan",
|
||||
bold=True,
|
||||
)
|
||||
|
||||
from crewai_cli.benchmark import ArtifactsSandbox, SuppressBenchmarkOutput, VerboseBenchmarkOutput
|
||||
from crewai_cli.benchmark import (
|
||||
ArtifactsSandbox,
|
||||
SuppressBenchmarkOutput,
|
||||
VerboseBenchmarkOutput,
|
||||
)
|
||||
|
||||
if not verbose:
|
||||
progress.start()
|
||||
@@ -816,7 +876,9 @@ def _test_new_agents(
|
||||
agents_tested = 0
|
||||
for job, result in zip(jobs, all_results):
|
||||
if isinstance(result, Exception):
|
||||
click.secho(f" Error running tests for {job['agent_name']}: {result}", fg="red")
|
||||
click.secho(
|
||||
f" Error running tests for {job['agent_name']}: {result}", fg="red"
|
||||
)
|
||||
all_passed = False
|
||||
continue
|
||||
|
||||
@@ -831,7 +893,9 @@ def _test_new_agents(
|
||||
)
|
||||
for r in failed:
|
||||
inp = r.input[:60] + ("…" if len(r.input) > 60 else "")
|
||||
_con.print(f" [red]#{r.case_index + 1}[/red] [dim]{inp}[/dim] [red]{r.score:.2f}[/red]")
|
||||
_con.print(
|
||||
f" [red]#{r.case_index + 1}[/red] [dim]{inp}[/dim] [red]{r.score:.2f}[/red]"
|
||||
)
|
||||
else:
|
||||
_con.print(
|
||||
f" [green bold]{job['agent_name']}: PASSED all {len(results)} cases >= {job['threshold']}[/green bold]"
|
||||
@@ -840,7 +904,9 @@ def _test_new_agents(
|
||||
click.secho("No agents completed successfully.", fg="yellow")
|
||||
raise SystemExit(1)
|
||||
if all_passed:
|
||||
click.secho(f"All tests passed ({agents_tested} agent(s)).", fg="green", bold=True)
|
||||
click.secho(
|
||||
f"All tests passed ({agents_tested} agent(s)).", fg="green", bold=True
|
||||
)
|
||||
else:
|
||||
click.secho("Some tests failed.", fg="red", bold=True)
|
||||
raise SystemExit(1)
|
||||
@@ -1149,7 +1215,10 @@ def agent_memory(name: str, search: str | None, clear: bool, limit_: int) -> Non
|
||||
|
||||
if clear:
|
||||
if click.confirm(f"Clear all memories for '{name}'?"):
|
||||
if hasattr(agent_instance, "_memory_instance") and agent_instance._memory_instance:
|
||||
if (
|
||||
hasattr(agent_instance, "_memory_instance")
|
||||
and agent_instance._memory_instance
|
||||
):
|
||||
try:
|
||||
agent_instance._memory_instance.reset()
|
||||
click.echo(f"Memories cleared for '{name}'.")
|
||||
@@ -1159,7 +1228,10 @@ def agent_memory(name: str, search: str | None, clear: bool, limit_: int) -> Non
|
||||
click.echo(f"No memory configured for '{name}'.")
|
||||
return
|
||||
|
||||
if not hasattr(agent_instance, "_memory_instance") or not agent_instance._memory_instance:
|
||||
if (
|
||||
not hasattr(agent_instance, "_memory_instance")
|
||||
or not agent_instance._memory_instance
|
||||
):
|
||||
click.echo(f"No memory configured for '{name}'.")
|
||||
return
|
||||
|
||||
@@ -1173,18 +1245,28 @@ def agent_memory(name: str, search: str | None, clear: bool, limit_: int) -> Non
|
||||
|
||||
try:
|
||||
if search:
|
||||
results = agent_instance._memory_instance.recall(search, limit=limit_, depth="shallow")
|
||||
results = agent_instance._memory_instance.recall(
|
||||
search, limit=limit_, depth="shallow"
|
||||
)
|
||||
else:
|
||||
results = agent_instance._memory_instance.list_records(limit=limit_)
|
||||
|
||||
if not results:
|
||||
msg = f"No memories matching '{search}'" if search else f"No memories stored for '{name}'."
|
||||
msg = (
|
||||
f"No memories matching '{search}'"
|
||||
if search
|
||||
else f"No memories stored for '{name}'."
|
||||
)
|
||||
click.echo(msg)
|
||||
return
|
||||
|
||||
if Console is not None:
|
||||
console = Console()
|
||||
title = f"Memories matching '{search}' — {name}" if search else f"Memories — {name}"
|
||||
title = (
|
||||
f"Memories matching '{search}' — {name}"
|
||||
if search
|
||||
else f"Memories — {name}"
|
||||
)
|
||||
table = Table(title=title, show_lines=True)
|
||||
table.add_column("#", style="dim", width=4)
|
||||
table.add_column("Content", min_width=40)
|
||||
@@ -1203,7 +1285,11 @@ def agent_memory(name: str, search: str | None, clear: bool, limit_: int) -> Non
|
||||
|
||||
console.print(table)
|
||||
else:
|
||||
heading = f"Memories matching '{search}':" if search else f"Recent memories for '{name}':"
|
||||
heading = (
|
||||
f"Memories matching '{search}':"
|
||||
if search
|
||||
else f"Recent memories for '{name}':"
|
||||
)
|
||||
click.echo(heading)
|
||||
for i, r in enumerate(results, 1):
|
||||
click.echo(f" {i}. {str(r)[:100]}")
|
||||
@@ -1583,7 +1669,8 @@ def checkpoint_prune(
|
||||
"Defaults to test.judge_model in config.json (openai/gpt-4o-mini if not set).",
|
||||
)
|
||||
@click.option(
|
||||
"-v", "--verbose",
|
||||
"-v",
|
||||
"--verbose",
|
||||
is_flag=True,
|
||||
help="Show agent execution details (tool calls, LLM responses, errors).",
|
||||
)
|
||||
@@ -1599,7 +1686,9 @@ def benchmark(
|
||||
|
||||
from crewai_cli.run_crew import _needs_uv_relaunch, _relaunch_via_uv
|
||||
|
||||
judge_model = judge_model or _read_config("test", "judge_model") or "openai/gpt-4o-mini"
|
||||
judge_model = (
|
||||
judge_model or _read_config("test", "judge_model") or "openai/gpt-4o-mini"
|
||||
)
|
||||
|
||||
if _needs_uv_relaunch():
|
||||
uv_args = ["benchmark", agent_path, cases_path, "--judge-model", judge_model]
|
||||
@@ -1620,6 +1709,7 @@ def benchmark(
|
||||
_con = _RichConsole()
|
||||
|
||||
from pathlib import Path as _P
|
||||
|
||||
agent_path = str(_P(agent_path).resolve())
|
||||
cases_path = str(_P(cases_path).resolve())
|
||||
|
||||
@@ -1638,7 +1728,11 @@ def benchmark(
|
||||
click.echo(f"Judge model: {judge_model}")
|
||||
click.echo()
|
||||
|
||||
from crewai_cli.benchmark import ArtifactsSandbox, SuppressBenchmarkOutput, VerboseBenchmarkOutput
|
||||
from crewai_cli.benchmark import (
|
||||
ArtifactsSandbox,
|
||||
SuppressBenchmarkOutput,
|
||||
VerboseBenchmarkOutput,
|
||||
)
|
||||
|
||||
progress = None if verbose else _BenchmarkLiveProgress(console=_con)
|
||||
if progress:
|
||||
|
||||
@@ -270,17 +270,23 @@ def _maybe_add_provider_extra(pyproject_path: Path, provider: str) -> None:
|
||||
try:
|
||||
content = pyproject_path.read_text(encoding="utf-8")
|
||||
missing = [
|
||||
e for e in all_extras
|
||||
if f"[{e}]" not in content and f",{e}]" not in content and f",{e}," not in content
|
||||
e
|
||||
for e in all_extras
|
||||
if f"[{e}]" not in content
|
||||
and f",{e}]" not in content
|
||||
and f",{e}," not in content
|
||||
]
|
||||
if not missing:
|
||||
return
|
||||
import re as _re
|
||||
|
||||
suffix = "," + ",".join(missing)
|
||||
|
||||
def _add_extras(m: _re.Match[str]) -> str:
|
||||
bracket: str = m.group(0)
|
||||
return bracket[:-1] + suffix + "]"
|
||||
updated = _re.sub(r'crewai\[[^\]]+\]', _add_extras, content, count=1)
|
||||
|
||||
updated = _re.sub(r"crewai\[[^\]]+\]", _add_extras, content, count=1)
|
||||
if updated != content:
|
||||
pyproject_path.write_text(updated, encoding="utf-8")
|
||||
except Exception:
|
||||
@@ -291,6 +297,7 @@ def _get_crewai_version() -> str:
|
||||
"""Get the installed crewai version for the dependency pin."""
|
||||
try:
|
||||
from crewai_cli.version import get_crewai_version
|
||||
|
||||
return get_crewai_version()
|
||||
except Exception:
|
||||
return "1.14.5"
|
||||
@@ -428,6 +435,7 @@ def _read_key() -> str:
|
||||
"""Read a single keypress. Returns 'up', 'down', 'enter', 'space', or the char."""
|
||||
if sys.platform == "win32":
|
||||
import msvcrt
|
||||
|
||||
ch = msvcrt.getwch()
|
||||
if ch in ("\x00", "\xe0"):
|
||||
ch2 = msvcrt.getwch()
|
||||
@@ -442,6 +450,7 @@ def _read_key() -> str:
|
||||
|
||||
import termios
|
||||
import tty
|
||||
|
||||
fd = sys.stdin.fileno()
|
||||
old = termios.tcgetattr(fd)
|
||||
try:
|
||||
@@ -478,7 +487,9 @@ def _draw_single(labels: list[str], cursor: int, *, clear: bool = False) -> None
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _draw_multi(labels: list[str], cursor: int, selected: set[int], *, clear: bool = False) -> None:
|
||||
def _draw_multi(
|
||||
labels: list[str], cursor: int, selected: set[int], *, clear: bool = False
|
||||
) -> None:
|
||||
"""Draw multi-select menu with checkboxes."""
|
||||
hint = f" {_DIM}↑↓ navigate, space toggle, enter confirm{_RESET}"
|
||||
total = len(labels) + 1 # +1 for hint line
|
||||
@@ -530,7 +541,9 @@ def create_agent(name: str | None = None) -> None:
|
||||
goal = click.prompt(" Goal (the agent's objective)", type=str)
|
||||
backstory = click.prompt(
|
||||
" Backstory (context that shapes personality, optional)",
|
||||
type=str, default="", show_default=False,
|
||||
type=str,
|
||||
default="",
|
||||
show_default=False,
|
||||
)
|
||||
|
||||
llm = _select_model()
|
||||
@@ -671,7 +684,9 @@ def _select_tools() -> list[str]:
|
||||
if has_custom:
|
||||
custom = click.prompt(
|
||||
" Custom tool class names (comma-separated)",
|
||||
type=str, default="", show_default=False,
|
||||
type=str,
|
||||
default="",
|
||||
show_default=False,
|
||||
)
|
||||
for name in custom.split(","):
|
||||
name = name.strip()
|
||||
@@ -717,7 +732,10 @@ def _select_tools_fallback(labels: list[str]) -> list[int]:
|
||||
click.echo()
|
||||
|
||||
raw = click.prompt(
|
||||
" Select tools (e.g. 1 3 5)", type=str, default="", show_default=False,
|
||||
" Select tools (e.g. 1 3 5)",
|
||||
type=str,
|
||||
default="",
|
||||
show_default=False,
|
||||
)
|
||||
if not raw.strip():
|
||||
return []
|
||||
@@ -762,7 +780,8 @@ def _setup_env(base: Path, llm_model: str) -> None:
|
||||
continue
|
||||
value = click.prompt(
|
||||
f" {details.get('prompt', f'Enter {key_name}')}",
|
||||
default="", show_default=False,
|
||||
default="",
|
||||
show_default=False,
|
||||
)
|
||||
if value.strip():
|
||||
env_vars[key_name] = value.strip()
|
||||
@@ -795,9 +814,9 @@ def _prompt_agent_name() -> str:
|
||||
|
||||
def _strip_comments(text: str) -> str:
|
||||
"""Strip // and /* */ comments from JSONC text, then fix trailing commas."""
|
||||
result = re.sub(r'(?<!:)//.*?$', '', text, flags=re.MULTILINE)
|
||||
result = re.sub(r'/\*.*?\*/', '', result, flags=re.DOTALL)
|
||||
result = re.sub(r',\s*([}\]])', r'\1', result)
|
||||
result = re.sub(r"(?<!:)//.*?$", "", text, flags=re.MULTILINE)
|
||||
result = re.sub(r"/\*.*?\*/", "", result, flags=re.DOTALL)
|
||||
result = re.sub(r",\s*([}\]])", r"\1", result)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from packaging import version
|
||||
from crewai_cli.utils import build_env_with_all_tool_credentials, read_toml
|
||||
from crewai_cli.version import get_crewai_version
|
||||
|
||||
|
||||
_UV_CONTEXT_VAR = "_CREWAI_UV"
|
||||
|
||||
|
||||
@@ -20,6 +21,7 @@ class CrewType(Enum):
|
||||
def _has_agents_dir() -> bool:
|
||||
"""Check if current directory has an agents/ directory with definitions."""
|
||||
from pathlib import Path
|
||||
|
||||
agents_dir = Path.cwd() / "agents"
|
||||
if not agents_dir.is_dir():
|
||||
return False
|
||||
@@ -32,6 +34,7 @@ def _needs_uv_relaunch() -> bool:
|
||||
if os.environ.get(_UV_CONTEXT_VAR):
|
||||
return False
|
||||
from pathlib import Path
|
||||
|
||||
pyproject = Path.cwd() / "pyproject.toml"
|
||||
if not pyproject.exists():
|
||||
return False
|
||||
@@ -79,6 +82,7 @@ def run_crew(trained_agents_file: str | None = None) -> None:
|
||||
_relaunch_via_uv(uv_args)
|
||||
click.echo("Launching agent TUI...")
|
||||
from crewai_cli.agent_tui import run_agent_tui
|
||||
|
||||
run_agent_tui()
|
||||
return
|
||||
|
||||
@@ -124,7 +128,7 @@ def execute_command(
|
||||
env[CREWAI_TRAINED_AGENTS_FILE_ENV] = trained_agents_file
|
||||
|
||||
try:
|
||||
subprocess.run(command, capture_output=False, text=True, check=True, env=env) # noqa: S603
|
||||
subprocess.run(command, capture_output=False, text=True, check=True, env=env)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
handle_error(e, crew_type)
|
||||
|
||||
Reference in New Issue
Block a user