mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-04-15 15:32:40 +00:00
Compare commits
5 Commits
worktree-l
...
1.14.2a5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c90d574ab | ||
|
|
3a7c550512 | ||
|
|
5b6f89fe64 | ||
|
|
ad5e66d1d0 | ||
|
|
94e7d86df1 |
@@ -4,6 +4,46 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="15 أبريل 2026">
|
||||
## v1.14.2a5
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a5)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.14.2a4
|
||||
|
||||
## المساهمون
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="15 أبريل 2026">
|
||||
## v1.14.2a4
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### الميزات
|
||||
- إضافة تلميحات استئناف إلى إصدار أدوات المطورين عند الفشل
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- إصلاح توجيه وضع الصرامة إلى واجهة برمجة تطبيقات Bedrock Converse
|
||||
- إصلاح إصدار pytest إلى 9.0.3 لثغرة الأمان GHSA-6w46-j5rx-g56g
|
||||
- رفع الحد الأدنى لـ OpenAI إلى >=2.0.0
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار لـ v1.14.2a3
|
||||
|
||||
## المساهمون
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="13 أبريل 2026">
|
||||
## v1.14.2a3
|
||||
|
||||
|
||||
@@ -4,6 +4,46 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Apr 15, 2026">
|
||||
## v1.14.2a5
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a5)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.14.2a4
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Apr 15, 2026">
|
||||
## v1.14.2a4
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add resume hints to devtools release on failure
|
||||
|
||||
### Bug Fixes
|
||||
- Fix strict mode forwarding to Bedrock Converse API
|
||||
- Fix pytest version to 9.0.3 for security vulnerability GHSA-6w46-j5rx-g56g
|
||||
- Bump OpenAI lower bound to >=2.0.0
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.14.2a3
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Apr 13, 2026">
|
||||
## v1.14.2a3
|
||||
|
||||
|
||||
@@ -4,6 +4,46 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 4월 15일">
|
||||
## v1.14.2a5
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a5)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 문서
|
||||
- v1.14.2a4의 변경 로그 및 버전 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 4월 15일">
|
||||
## v1.14.2a4
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- 실패 시 devtools 릴리스에 이력서 힌트 추가
|
||||
|
||||
### 버그 수정
|
||||
- Bedrock Converse API로의 엄격 모드 포워딩 수정
|
||||
- 보안 취약점 GHSA-6w46-j5rx-g56g에 대해 pytest 버전을 9.0.3으로 수정
|
||||
- OpenAI 하한을 >=2.0.0으로 상향 조정
|
||||
|
||||
### 문서
|
||||
- v1.14.2a3에 대한 변경 로그 및 버전 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 4월 13일">
|
||||
## v1.14.2a3
|
||||
|
||||
|
||||
@@ -4,6 +4,46 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="15 abr 2026">
|
||||
## v1.14.2a5
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a5)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Documentação
|
||||
- Atualizar changelog e versão para v1.14.2a4
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="15 abr 2026">
|
||||
## v1.14.2a4
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Recursos
|
||||
- Adicionar dicas de retomar ao release do devtools em caso de falha
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir o encaminhamento do modo estrito para a API Bedrock Converse
|
||||
- Corrigir a versão do pytest para 9.0.3 devido à vulnerabilidade de segurança GHSA-6w46-j5rx-g56g
|
||||
- Aumentar o limite inferior do OpenAI para >=2.0.0
|
||||
|
||||
### Documentação
|
||||
- Atualizar o changelog e a versão para v1.14.2a3
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@greysonlalonde
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="13 abr 2026">
|
||||
## v1.14.2a3
|
||||
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.14.2a3"
|
||||
__version__ = "1.14.2a5"
|
||||
|
||||
@@ -10,7 +10,7 @@ requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests>=2.33.0,<3",
|
||||
"crewai==1.14.2a3",
|
||||
"crewai==1.14.2a5",
|
||||
"tiktoken~=0.8.0",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
|
||||
@@ -305,4 +305,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.14.2a3"
|
||||
__version__ = "1.14.2a5"
|
||||
|
||||
@@ -55,7 +55,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.14.2a3",
|
||||
"crewai-tools==1.14.2a5",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
|
||||
@@ -46,7 +46,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.14.2a3"
|
||||
__version__ = "1.14.2a5"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ from crewai.cli.install_crew import install_crew
|
||||
from crewai.cli.kickoff_flow import kickoff_flow
|
||||
from crewai.cli.organization.main import OrganizationCommand
|
||||
from crewai.cli.plot_flow import plot_flow
|
||||
from crewai.cli.remote_template.main import TemplateCommand
|
||||
from crewai.cli.replay_from_task import replay_task_command
|
||||
from crewai.cli.reset_memories_command import reset_memories_command
|
||||
from crewai.cli.run_crew import run_crew
|
||||
@@ -497,33 +496,6 @@ def tool_publish(is_public: bool, force: bool) -> None:
|
||||
tool_cmd.publish(is_public, force)
|
||||
|
||||
|
||||
@crewai.group()
|
||||
def template() -> None:
|
||||
"""Browse and install project templates."""
|
||||
|
||||
|
||||
@template.command(name="list")
|
||||
def template_list() -> None:
|
||||
"""List available templates and select one to install."""
|
||||
template_cmd = TemplateCommand()
|
||||
template_cmd.list_templates()
|
||||
|
||||
|
||||
@template.command(name="add")
|
||||
@click.argument("name")
|
||||
@click.option(
|
||||
"-o",
|
||||
"--output-dir",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Directory name for the template (defaults to template name)",
|
||||
)
|
||||
def template_add(name: str, output_dir: str | None) -> None:
|
||||
"""Add a template to the current directory."""
|
||||
template_cmd = TemplateCommand()
|
||||
template_cmd.add_template(name, output_dir)
|
||||
|
||||
|
||||
@crewai.group()
|
||||
def flow() -> None:
|
||||
"""Flow related commands."""
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from typing import Any
|
||||
import zipfile
|
||||
|
||||
import click
|
||||
import httpx
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from crewai.cli.command import BaseCommand
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
console = Console()
|
||||
|
||||
GITHUB_ORG = "crewAIInc"
|
||||
TEMPLATE_PREFIX = "template_"
|
||||
GITHUB_API_BASE = "https://api.github.com"
|
||||
|
||||
BANNER = """\
|
||||
[bold white] ██████╗██████╗ ███████╗██╗ ██╗[/bold white] [bold red] █████╗ ██╗[/bold red]
|
||||
[bold white]██╔════╝██╔══██╗██╔════╝██║ ██║[/bold white] [bold red]██╔══██╗██║[/bold red]
|
||||
[bold white]██║ ██████╔╝█████╗ ██║ █╗ ██║[/bold white] [bold red]███████║██║[/bold red]
|
||||
[bold white]██║ ██╔══██╗██╔══╝ ██║███╗██║[/bold white] [bold red]██╔══██║██║[/bold red]
|
||||
[bold white]╚██████╗██║ ██║███████╗╚███╔███╔╝[/bold white] [bold red]██║ ██║██║[/bold red]
|
||||
[bold white] ╚═════╝╚═╝ ╚═╝╚══════╝ ╚══╝╚══╝[/bold white] [bold red]╚═╝ ╚═╝╚═╝[/bold red]
|
||||
[dim white]████████╗███████╗███╗ ███╗██████╗ ██╗ █████╗ ████████╗███████╗███████╗[/dim white]
|
||||
[dim white]╚══██╔══╝██╔════╝████╗ ████║██╔══██╗██║ ██╔══██╗╚══██╔══╝██╔════╝██╔════╝[/dim white]
|
||||
[dim white] ██║ █████╗ ██╔████╔██║██████╔╝██║ ███████║ ██║ █████╗ ███████╗[/dim white]
|
||||
[dim white] ██║ ██╔══╝ ██║╚██╔╝██║██╔═══╝ ██║ ██╔══██║ ██║ ██╔══╝ ╚════██║[/dim white]
|
||||
[dim white] ██║ ███████╗██║ ╚═╝ ██║██║ ███████╗██║ ██║ ██║ ███████╗███████║[/dim white]
|
||||
[dim white] ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝[/dim white]"""
|
||||
|
||||
|
||||
class TemplateCommand(BaseCommand):
|
||||
"""Handle template-related operations for CrewAI projects."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
def list_templates(self) -> None:
|
||||
"""List available templates with an interactive selector to install."""
|
||||
templates = self._fetch_templates()
|
||||
if not templates:
|
||||
click.echo("No templates found.")
|
||||
return
|
||||
|
||||
console.print(f"\n{BANNER}\n")
|
||||
console.print(" [on cyan] templates [/on cyan]\n")
|
||||
console.print(f" [green]o[/green] Source: https://github.com/{GITHUB_ORG}")
|
||||
console.print(
|
||||
f" [green]o[/green] Found [bold]{len(templates)}[/bold] templates\n"
|
||||
)
|
||||
console.print(" [green]o[/green] Select a template to install")
|
||||
|
||||
for idx, repo in enumerate(templates, start=1):
|
||||
name = repo["name"].removeprefix(TEMPLATE_PREFIX)
|
||||
description = repo.get("description") or ""
|
||||
if description:
|
||||
console.print(
|
||||
f" [bold cyan]{idx}.[/bold cyan] [bold white]{name}[/bold white] [dim]({description})[/dim]"
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
f" [bold cyan]{idx}.[/bold cyan] [bold white]{name}[/bold white]"
|
||||
)
|
||||
|
||||
console.print(" [bold cyan]q.[/bold cyan] [dim]Quit[/dim]\n")
|
||||
|
||||
while True:
|
||||
choice = click.prompt("Enter your choice", type=str)
|
||||
|
||||
if choice.lower() == "q":
|
||||
return
|
||||
|
||||
try:
|
||||
selected_index = int(choice) - 1
|
||||
if 0 <= selected_index < len(templates):
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
click.secho(
|
||||
f"Please enter a number between 1 and {len(templates)}, or 'q' to quit.",
|
||||
fg="yellow",
|
||||
)
|
||||
|
||||
selected = templates[selected_index]
|
||||
repo_name = selected["name"]
|
||||
template_name = repo_name.removeprefix(TEMPLATE_PREFIX)
|
||||
self.add_template(template_name)
|
||||
|
||||
def add_template(self, name: str, output_dir: str | None = None) -> None:
|
||||
"""Download a template and copy it into the current working directory.
|
||||
|
||||
Args:
|
||||
name: Template name (with or without the template_ prefix).
|
||||
output_dir: Optional directory name. Defaults to the template name.
|
||||
"""
|
||||
repo_name = self._resolve_repo_name(name)
|
||||
if repo_name is None:
|
||||
click.secho(f"Template '{name}' not found.", fg="red")
|
||||
click.echo("Run 'crewai template list' to see available templates.")
|
||||
raise SystemExit(1)
|
||||
|
||||
folder_name = output_dir or repo_name.removeprefix(TEMPLATE_PREFIX)
|
||||
dest = os.path.join(os.getcwd(), folder_name)
|
||||
|
||||
while os.path.exists(dest):
|
||||
click.secho(f"Directory '{folder_name}' already exists.", fg="yellow")
|
||||
folder_name = click.prompt(
|
||||
"Enter a different directory name (or 'q' to quit)", type=str
|
||||
)
|
||||
if folder_name.lower() == "q":
|
||||
return
|
||||
dest = os.path.join(os.getcwd(), folder_name)
|
||||
|
||||
click.echo(
|
||||
f"Downloading template '{repo_name.removeprefix(TEMPLATE_PREFIX)}'..."
|
||||
)
|
||||
|
||||
zip_bytes = self._download_zip(repo_name)
|
||||
self._extract_zip(zip_bytes, dest)
|
||||
|
||||
try:
|
||||
from crewai.telemetry import Telemetry
|
||||
|
||||
telemetry = Telemetry()
|
||||
telemetry.set_tracer()
|
||||
telemetry.template_installed_span(repo_name.removeprefix(TEMPLATE_PREFIX))
|
||||
except Exception:
|
||||
logger.debug("Failed to record template install telemetry")
|
||||
|
||||
console.print(
|
||||
f"\n [green]\u2713[/green] Installed template [bold white]{folder_name}[/bold white]"
|
||||
f" [dim](source: github.com/{GITHUB_ORG}/{repo_name})[/dim]\n"
|
||||
)
|
||||
|
||||
next_steps = Text()
|
||||
next_steps.append(f" cd {folder_name}\n", style="bold white")
|
||||
next_steps.append(" crewai install", style="bold white")
|
||||
|
||||
panel = Panel(
|
||||
next_steps,
|
||||
title="[green]\u25c7 Next steps[/green]",
|
||||
title_align="left",
|
||||
border_style="dim",
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print(panel)
|
||||
|
||||
def _fetch_templates(self) -> list[dict[str, Any]]:
|
||||
"""Fetch all template repos from the GitHub org."""
|
||||
templates: list[dict[str, Any]] = []
|
||||
page = 1
|
||||
while True:
|
||||
url = f"{GITHUB_API_BASE}/orgs/{GITHUB_ORG}/repos"
|
||||
params: dict[str, str | int] = {
|
||||
"per_page": 100,
|
||||
"page": page,
|
||||
"type": "public",
|
||||
}
|
||||
try:
|
||||
response = httpx.get(url, params=params, timeout=15)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPError as e:
|
||||
click.secho(f"Failed to fetch templates from GitHub: {e}", fg="red")
|
||||
raise SystemExit(1) from e
|
||||
|
||||
repos = response.json()
|
||||
if not repos:
|
||||
break
|
||||
|
||||
templates.extend(
|
||||
repo
|
||||
for repo in repos
|
||||
if repo["name"].startswith(TEMPLATE_PREFIX) and not repo.get("private")
|
||||
)
|
||||
|
||||
page += 1
|
||||
|
||||
templates.sort(key=lambda r: r["name"])
|
||||
return templates
|
||||
|
||||
def _resolve_repo_name(self, name: str) -> str | None:
|
||||
"""Resolve user input to a full repo name, or None if not found."""
|
||||
# Accept both 'deep_research' and 'template_deep_research'
|
||||
candidates = [
|
||||
f"{TEMPLATE_PREFIX}{name}"
|
||||
if not name.startswith(TEMPLATE_PREFIX)
|
||||
else name,
|
||||
name,
|
||||
]
|
||||
|
||||
templates = self._fetch_templates()
|
||||
template_names = {t["name"] for t in templates}
|
||||
|
||||
for candidate in candidates:
|
||||
if candidate in template_names:
|
||||
return candidate
|
||||
|
||||
return None
|
||||
|
||||
def _download_zip(self, repo_name: str) -> bytes:
|
||||
"""Download the default branch zipball for a repo."""
|
||||
url = f"{GITHUB_API_BASE}/repos/{GITHUB_ORG}/{repo_name}/zipball"
|
||||
try:
|
||||
response = httpx.get(url, follow_redirects=True, timeout=60)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPError as e:
|
||||
click.secho(f"Failed to download template: {e}", fg="red")
|
||||
raise SystemExit(1) from e
|
||||
|
||||
return response.content
|
||||
|
||||
def _extract_zip(self, zip_bytes: bytes, dest: str) -> None:
|
||||
"""Extract a GitHub zipball into dest, stripping the top-level directory."""
|
||||
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
|
||||
# GitHub zipballs have a single top-level dir like 'crewAIInc-template_xxx-<sha>/'
|
||||
members = zf.namelist()
|
||||
if not members:
|
||||
click.secho("Downloaded archive is empty.", fg="red")
|
||||
raise SystemExit(1)
|
||||
|
||||
top_dir = members[0].split("/")[0] + "/"
|
||||
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
|
||||
for member in members:
|
||||
if member == top_dir or not member.startswith(top_dir):
|
||||
continue
|
||||
|
||||
relative_path = member[len(top_dir) :]
|
||||
if not relative_path:
|
||||
continue
|
||||
|
||||
target = os.path.join(dest, relative_path)
|
||||
|
||||
if member.endswith("/"):
|
||||
os.makedirs(target, exist_ok=True)
|
||||
else:
|
||||
os.makedirs(os.path.dirname(target), exist_ok=True)
|
||||
with zf.open(member) as src, open(target, "wb") as dst:
|
||||
shutil.copyfileobj(src, dst)
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.14.2a3"
|
||||
"crewai[tools]==1.14.2a5"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.14.2a3"
|
||||
"crewai[tools]==1.14.2a5"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.14.2a3"
|
||||
"crewai[tools]==1.14.2a5"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -17,10 +17,7 @@ from crewai.utilities.agent_utils import is_context_length_exceeded
|
||||
from crewai.utilities.exceptions.context_window_exceeding_exception import (
|
||||
LLMContextLengthExceededError,
|
||||
)
|
||||
from crewai.utilities.pydantic_schema_utils import (
|
||||
generate_model_description,
|
||||
sanitize_tool_params_for_bedrock_strict,
|
||||
)
|
||||
from crewai.utilities.pydantic_schema_utils import generate_model_description
|
||||
from crewai.utilities.types import LLMMessage
|
||||
|
||||
|
||||
@@ -173,7 +170,6 @@ class ToolSpec(TypedDict, total=False):
|
||||
name: Required[str]
|
||||
description: Required[str]
|
||||
inputSchema: ToolInputSchema
|
||||
strict: bool
|
||||
|
||||
|
||||
class ConverseToolTypeDef(TypedDict):
|
||||
@@ -1988,21 +1984,10 @@ class BedrockCompletion(BaseLLM):
|
||||
"description": description,
|
||||
}
|
||||
|
||||
func_info = tool.get("function", {})
|
||||
strict_enabled = bool(func_info.get("strict"))
|
||||
|
||||
if parameters and isinstance(parameters, dict):
|
||||
schema_params = (
|
||||
sanitize_tool_params_for_bedrock_strict(parameters)
|
||||
if strict_enabled
|
||||
else parameters
|
||||
)
|
||||
input_schema: ToolInputSchema = {"json": schema_params}
|
||||
input_schema: ToolInputSchema = {"json": parameters}
|
||||
tool_spec["inputSchema"] = input_schema
|
||||
|
||||
if strict_enabled:
|
||||
tool_spec["strict"] = True
|
||||
|
||||
converse_tool: ConverseToolTypeDef = {"toolSpec": tool_spec}
|
||||
|
||||
converse_tools.append(converse_tool)
|
||||
|
||||
@@ -1058,20 +1058,3 @@ class Telemetry:
|
||||
close_span(span)
|
||||
|
||||
self._safe_telemetry_operation(_operation)
|
||||
|
||||
def template_installed_span(self, template_name: str) -> None:
|
||||
"""Records when a template is downloaded and installed.
|
||||
|
||||
Args:
|
||||
template_name: Name of the template that was installed
|
||||
(without the template_ prefix).
|
||||
"""
|
||||
|
||||
def _operation() -> None:
|
||||
tracer = trace.get_tracer("crewai.telemetry")
|
||||
span = tracer.start_span("Template Installed")
|
||||
self._add_attribute(span, "crewai_version", version("crewai"))
|
||||
self._add_attribute(span, "template_name", template_name)
|
||||
close_span(span)
|
||||
|
||||
self._safe_telemetry_operation(_operation)
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
import io
|
||||
import os
|
||||
import zipfile
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from crewai.cli.cli import template_add, template_list
|
||||
from crewai.cli.remote_template.main import TemplateCommand
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner():
|
||||
return CliRunner()
|
||||
|
||||
|
||||
SAMPLE_REPOS = [
|
||||
{"name": "template_deep_research", "description": "Deep research template", "private": False},
|
||||
{"name": "template_pull_request_review", "description": "PR review template", "private": False},
|
||||
{"name": "template_conversational_example", "description": "Conversational demo", "private": False},
|
||||
{"name": "crewai", "description": "Main repo", "private": False},
|
||||
{"name": "marketplace-crew-template", "description": "Marketplace", "private": False},
|
||||
]
|
||||
|
||||
|
||||
def _make_zipball(files: dict[str, str], top_dir: str = "crewAIInc-template_test-abc123") -> bytes:
|
||||
"""Create an in-memory zipball mimicking GitHub's format."""
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w") as zf:
|
||||
zf.writestr(f"{top_dir}/", "")
|
||||
for path, content in files.items():
|
||||
zf.writestr(f"{top_dir}/{path}", content)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
# --- CLI command tests ---
|
||||
|
||||
|
||||
@patch("crewai.cli.cli.TemplateCommand")
|
||||
def test_template_list_command(mock_cls, runner):
|
||||
mock_instance = MagicMock()
|
||||
mock_cls.return_value = mock_instance
|
||||
|
||||
result = runner.invoke(template_list)
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_cls.assert_called_once()
|
||||
mock_instance.list_templates.assert_called_once()
|
||||
|
||||
|
||||
@patch("crewai.cli.cli.TemplateCommand")
|
||||
def test_template_add_command(mock_cls, runner):
|
||||
mock_instance = MagicMock()
|
||||
mock_cls.return_value = mock_instance
|
||||
|
||||
result = runner.invoke(template_add, ["deep_research"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_cls.assert_called_once()
|
||||
mock_instance.add_template.assert_called_once_with("deep_research", None)
|
||||
|
||||
|
||||
@patch("crewai.cli.cli.TemplateCommand")
|
||||
def test_template_add_with_output_dir(mock_cls, runner):
|
||||
mock_instance = MagicMock()
|
||||
mock_cls.return_value = mock_instance
|
||||
|
||||
result = runner.invoke(template_add, ["deep_research", "-o", "my_project"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_instance.add_template.assert_called_once_with("deep_research", "my_project")
|
||||
|
||||
|
||||
# --- TemplateCommand unit tests ---
|
||||
|
||||
|
||||
class TestTemplateCommand:
|
||||
@pytest.fixture
|
||||
def cmd(self):
|
||||
with patch.object(TemplateCommand, "__init__", return_value=None):
|
||||
return TemplateCommand()
|
||||
|
||||
@patch("crewai.cli.remote_template.main.httpx.get")
|
||||
def test_fetch_templates_filters_by_prefix(self, mock_get, cmd):
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = SAMPLE_REPOS
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
# Return empty on page 2 to stop pagination
|
||||
mock_empty = MagicMock()
|
||||
mock_empty.json.return_value = []
|
||||
mock_empty.raise_for_status = MagicMock()
|
||||
mock_get.side_effect = [mock_response, mock_empty]
|
||||
|
||||
templates = cmd._fetch_templates()
|
||||
|
||||
assert len(templates) == 3
|
||||
assert all(t["name"].startswith("template_") for t in templates)
|
||||
|
||||
@patch("crewai.cli.remote_template.main.httpx.get")
|
||||
def test_fetch_templates_excludes_private(self, mock_get, cmd):
|
||||
repos = [
|
||||
{"name": "template_private_one", "description": "", "private": True},
|
||||
{"name": "template_public_one", "description": "", "private": False},
|
||||
]
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = repos
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_empty = MagicMock()
|
||||
mock_empty.json.return_value = []
|
||||
mock_empty.raise_for_status = MagicMock()
|
||||
mock_get.side_effect = [mock_response, mock_empty]
|
||||
|
||||
templates = cmd._fetch_templates()
|
||||
|
||||
assert len(templates) == 1
|
||||
assert templates[0]["name"] == "template_public_one"
|
||||
|
||||
@patch("crewai.cli.remote_template.main.httpx.get")
|
||||
def test_fetch_templates_api_error(self, mock_get, cmd):
|
||||
mock_get.side_effect = httpx.HTTPError("connection error")
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
cmd._fetch_templates()
|
||||
|
||||
@patch("crewai.cli.remote_template.main.click.prompt", return_value="q")
|
||||
@patch("crewai.cli.remote_template.main.httpx.get")
|
||||
def test_list_templates_prints_output(self, mock_get, mock_prompt, cmd):
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = SAMPLE_REPOS
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_empty = MagicMock()
|
||||
mock_empty.json.return_value = []
|
||||
mock_empty.raise_for_status = MagicMock()
|
||||
mock_get.side_effect = [mock_response, mock_empty]
|
||||
|
||||
with patch("crewai.cli.remote_template.main.console") as mock_console:
|
||||
cmd.list_templates()
|
||||
assert mock_console.print.call_count > 0
|
||||
|
||||
@patch("crewai.cli.remote_template.main.httpx.get")
|
||||
def test_resolve_repo_name_with_prefix(self, mock_get, cmd):
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = SAMPLE_REPOS
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_empty = MagicMock()
|
||||
mock_empty.json.return_value = []
|
||||
mock_empty.raise_for_status = MagicMock()
|
||||
mock_get.side_effect = [mock_response, mock_empty]
|
||||
|
||||
result = cmd._resolve_repo_name("template_deep_research")
|
||||
assert result == "template_deep_research"
|
||||
|
||||
@patch("crewai.cli.remote_template.main.httpx.get")
|
||||
def test_resolve_repo_name_without_prefix(self, mock_get, cmd):
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = SAMPLE_REPOS
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_empty = MagicMock()
|
||||
mock_empty.json.return_value = []
|
||||
mock_empty.raise_for_status = MagicMock()
|
||||
mock_get.side_effect = [mock_response, mock_empty]
|
||||
|
||||
result = cmd._resolve_repo_name("deep_research")
|
||||
assert result == "template_deep_research"
|
||||
|
||||
@patch("crewai.cli.remote_template.main.httpx.get")
|
||||
def test_resolve_repo_name_not_found(self, mock_get, cmd):
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = SAMPLE_REPOS
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_empty = MagicMock()
|
||||
mock_empty.json.return_value = []
|
||||
mock_empty.raise_for_status = MagicMock()
|
||||
mock_get.side_effect = [mock_response, mock_empty]
|
||||
|
||||
result = cmd._resolve_repo_name("nonexistent")
|
||||
assert result is None
|
||||
|
||||
def test_extract_zip(self, cmd, tmp_path):
|
||||
files = {
|
||||
"README.md": "# Test Template",
|
||||
"src/main.py": "print('hello')",
|
||||
"config/settings.yaml": "key: value",
|
||||
}
|
||||
zip_bytes = _make_zipball(files)
|
||||
dest = str(tmp_path / "output")
|
||||
|
||||
cmd._extract_zip(zip_bytes, dest)
|
||||
|
||||
assert os.path.isfile(os.path.join(dest, "README.md"))
|
||||
assert os.path.isfile(os.path.join(dest, "src", "main.py"))
|
||||
assert os.path.isfile(os.path.join(dest, "config", "settings.yaml"))
|
||||
|
||||
with open(os.path.join(dest, "src", "main.py")) as f:
|
||||
assert f.read() == "print('hello')"
|
||||
|
||||
@patch.object(TemplateCommand, "_extract_zip")
|
||||
@patch.object(TemplateCommand, "_download_zip")
|
||||
@patch.object(TemplateCommand, "_resolve_repo_name")
|
||||
def test_add_template_success(self, mock_resolve, mock_download, mock_extract, cmd, tmp_path):
|
||||
mock_resolve.return_value = "template_deep_research"
|
||||
mock_download.return_value = b"fake-zip-bytes"
|
||||
|
||||
os.chdir(tmp_path)
|
||||
cmd.add_template("deep_research")
|
||||
|
||||
mock_resolve.assert_called_once_with("deep_research")
|
||||
mock_download.assert_called_once_with("template_deep_research")
|
||||
expected_dest = os.path.join(str(tmp_path), "deep_research")
|
||||
mock_extract.assert_called_once_with(b"fake-zip-bytes", expected_dest)
|
||||
|
||||
@patch.object(TemplateCommand, "_resolve_repo_name")
|
||||
def test_add_template_not_found(self, mock_resolve, cmd):
|
||||
mock_resolve.return_value = None
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
cmd.add_template("nonexistent")
|
||||
|
||||
@patch.object(TemplateCommand, "_extract_zip")
|
||||
@patch.object(TemplateCommand, "_download_zip")
|
||||
@patch("crewai.cli.remote_template.main.click.prompt", return_value="my_project")
|
||||
@patch.object(TemplateCommand, "_resolve_repo_name")
|
||||
def test_add_template_dir_exists_prompts_rename(self, mock_resolve, mock_prompt, mock_download, mock_extract, cmd, tmp_path):
|
||||
mock_resolve.return_value = "template_deep_research"
|
||||
mock_download.return_value = b"fake-zip-bytes"
|
||||
existing = tmp_path / "deep_research"
|
||||
existing.mkdir()
|
||||
|
||||
os.chdir(tmp_path)
|
||||
cmd.add_template("deep_research")
|
||||
|
||||
expected_dest = os.path.join(str(tmp_path), "my_project")
|
||||
mock_extract.assert_called_once_with(b"fake-zip-bytes", expected_dest)
|
||||
|
||||
@patch.object(TemplateCommand, "_resolve_repo_name")
|
||||
@patch("crewai.cli.remote_template.main.click.prompt", return_value="q")
|
||||
def test_add_template_dir_exists_quit(self, mock_prompt, mock_resolve, cmd, tmp_path):
|
||||
mock_resolve.return_value = "template_deep_research"
|
||||
existing = tmp_path / "deep_research"
|
||||
existing.mkdir()
|
||||
|
||||
os.chdir(tmp_path)
|
||||
cmd.add_template("deep_research")
|
||||
# Should return without downloading
|
||||
|
||||
@patch.object(TemplateCommand, "add_template")
|
||||
@patch("crewai.cli.remote_template.main.click.prompt", return_value="2")
|
||||
@patch("crewai.cli.remote_template.main.httpx.get")
|
||||
def test_list_templates_selects_and_installs(self, mock_get, mock_prompt, mock_add, cmd):
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = SAMPLE_REPOS
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_empty = MagicMock()
|
||||
mock_empty.json.return_value = []
|
||||
mock_empty.raise_for_status = MagicMock()
|
||||
mock_get.side_effect = [mock_response, mock_empty]
|
||||
|
||||
with patch("crewai.cli.remote_template.main.console"):
|
||||
cmd.list_templates()
|
||||
|
||||
# Templates are sorted by name; index 1 (choice "2") = template_deep_research
|
||||
mock_add.assert_called_once_with("deep_research")
|
||||
|
||||
@patch.object(TemplateCommand, "add_template")
|
||||
@patch("crewai.cli.remote_template.main.click.prompt", return_value="q")
|
||||
@patch("crewai.cli.remote_template.main.httpx.get")
|
||||
def test_list_templates_quit(self, mock_get, mock_prompt, mock_add, cmd):
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = SAMPLE_REPOS
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_empty = MagicMock()
|
||||
mock_empty.json.return_value = []
|
||||
mock_empty.raise_for_status = MagicMock()
|
||||
mock_get.side_effect = [mock_response, mock_empty]
|
||||
|
||||
with patch("crewai.cli.remote_template.main.console"):
|
||||
cmd.list_templates()
|
||||
|
||||
mock_add.assert_not_called()
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.14.2a3"
|
||||
__version__ = "1.14.2a5"
|
||||
|
||||
Reference in New Issue
Block a user