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:
alex-clawd
2026-05-13 01:13:02 -07:00
parent 0ddedbc48a
commit 94b5e2ea7b
27 changed files with 2079 additions and 947 deletions

View File

@@ -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

View File

@@ -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} "

View File

@@ -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:

View File

@@ -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

View File

@@ -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)