From f4c0667d34c8562bb11b5e460b7d9ce894247aa3 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Wed, 8 Apr 2026 18:59:51 +0800 Subject: [PATCH 01/70] fix: bump transformers to 5.5.0 to resolve CVE-2026-1839 Bumps docling pin from ~=2.75.0 to ~=2.84.0 (allows huggingface-hub>=1) and adds a transformers>=5.4.0 override to force resolution past 4.57.6. --- lib/crewai/pyproject.toml | 2 +- pyproject.toml | 2 ++ uv.lock | 33 ++++++++++++++++++--------------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index e883035c1..99749cc67 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -68,7 +68,7 @@ openpyxl = [ ] mem0 = ["mem0ai~=0.1.94"] docling = [ - "docling~=2.75.0", + "docling~=2.84.0", ] qdrant = [ "qdrant-client[fastembed]~=1.14.3", diff --git a/pyproject.toml b/pyproject.toml index 44b966533..6f6404677 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -166,12 +166,14 @@ exclude-newer = "3 days" # onnxruntime 1.24+ dropped Python 3.10 wheels; cap it so qdrant[fastembed] resolves on 3.10. # fastembed 0.7.x and docling 2.63 cap pillow<12; the removed APIs don't affect them. # langchain-core <1.2.11 has SSRF via image_url token counting (CVE-2026-26013). +# transformers 4.57.6 has CVE-2026-1839; force 5.4+ (docling 2.84 allows huggingface-hub>=1). override-dependencies = [ "rich>=13.7.1", "onnxruntime<1.24; python_version < '3.11'", "pillow>=12.1.1", "langchain-core>=1.2.11,<2", "urllib3>=2.6.3", + "transformers>=5.4.0; python_version >= '3.10'", ] [tool.uv.workspace] diff --git a/uv.lock b/uv.lock index 2f0922173..d52f6621f 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-04-04T15:11:41.651093Z" +exclude-newer = "2026-04-05T10:53:01.907268Z" exclude-newer-span = "P3D" [manifest] @@ -28,6 +28,7 @@ overrides = [ { name = "onnxruntime", marker = "python_full_version < '3.11'", specifier = "<1.24" }, { name = "pillow", specifier = ">=12.1.1" }, { name = "rich", specifier = ">=13.7.1" }, + { name = "transformers", marker = "python_full_version >= '3.10'", specifier = ">=5.4.0" }, { name = "urllib3", specifier = ">=2.6.3" }, ] @@ -1307,7 +1308,7 @@ requires-dist = [ { name = "click", specifier = "~=8.1.7" }, { name = "crewai-files", marker = "extra == 'file-processing'", editable = "lib/crewai-files" }, { name = "crewai-tools", marker = "extra == 'tools'", editable = "lib/crewai-tools" }, - { name = "docling", marker = "extra == 'docling'", specifier = "~=2.75.0" }, + { name = "docling", marker = "extra == 'docling'", specifier = "~=2.84.0" }, { name = "google-genai", marker = "extra == 'google-genai'", specifier = "~=1.65.0" }, { name = "httpx", specifier = "~=0.28.1" }, { name = "httpx-auth", marker = "extra == 'a2a'", specifier = "~=0.23.1" }, @@ -1820,7 +1821,7 @@ wheels = [ [[package]] name = "docling" -version = "2.75.0" +version = "2.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate" }, @@ -1851,12 +1852,14 @@ dependencies = [ { name = "rtree" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "torch" }, + { name = "torchvision" }, { name = "tqdm" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/0b/8ea363fd3c8bb4facb8d3c37aebfe7ad5265fecc1c6bd40f979d1f6179ba/docling-2.75.0.tar.gz", hash = "sha256:1b0a77766e201e5e2d118e236c006f3814afcea2e13726fb3c7389d666a56622", size = 364929, upload-time = "2026-02-24T20:18:04.896Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/1f/85560d7ba90a20f46c65396b45990fad34b7c95da23ca6e547456631d0e6/docling-2.84.0.tar.gz", hash = "sha256:007b0bad3c0ec45dc91af6083cbe1f0a93ddef1686304f466e8a168a1fb1dccb", size = 425470, upload-time = "2026-04-01T18:36:31.377Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/85/5c6885547ce5cde33af43201e3b2b04cf2360e6854abc07485f54b8d265d/docling-2.75.0-py3-none-any.whl", hash = "sha256:6e156f0326edb6471fc076e978ac64f902f54aac0da13cf89df456013e377bcc", size = 396243, upload-time = "2026-02-24T20:18:03.57Z" }, + { url = "https://files.pythonhosted.org/packages/22/e1/054e6ddf45e5760d51053b93b1a4f8be1568882b50c5ceeb88e6adaa6918/docling-2.84.0-py3-none-any.whl", hash = "sha256:ee431e5bb20cbebdd957f6173918f133d769340462814f3479df3446743d240e", size = 451391, upload-time = "2026-04-01T18:36:29.379Z" }, ] [[package]] @@ -2735,21 +2738,22 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "0.36.2" +version = "1.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "fsspec" }, - { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, { name = "packaging" }, { name = "pyyaml" }, - { name = "requests" }, { name = "tqdm" }, + { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/bb/62c7aa86f63a05e2f9b96642fdef9b94526a23979820b09f5455deff4983/huggingface_hub-1.9.0.tar.gz", hash = "sha256:0ea5be7a56135c91797cae6ad726e38eaeb6eb4b77cefff5c9d38ba0ecf874f7", size = 750326, upload-time = "2026-04-03T08:35:55.888Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" }, + { url = "https://files.pythonhosted.org/packages/73/37/0d15d16150e1829f3e90962c99f28257f6de9e526a680b4c6f5acdb54fd2/huggingface_hub-1.9.0-py3-none-any.whl", hash = "sha256:2999328c058d39fd19ab748dd09bd4da2fbaa4f4c1ddea823eab103051e14a1f", size = 637355, upload-time = "2026-04-03T08:35:53.897Z" }, ] [[package]] @@ -8172,24 +8176,23 @@ wheels = [ [[package]] name = "transformers" -version = "4.57.6" +version = "5.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock" }, { name = "huggingface-hub" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, { name = "pyyaml" }, { name = "regex" }, - { name = "requests" }, { name = "safetensors" }, { name = "tokenizers" }, { name = "tqdm" }, + { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/9d/fb46e729b461985f41a5740167688b924a4019141e5c164bea77548d3d9e/transformers-5.5.0.tar.gz", hash = "sha256:c8db656cf51c600cd8c75f06b20ef85c72e8b8ff9abc880c5d3e8bc70e0ddcbd", size = 8237745, upload-time = "2026-04-02T16:13:08.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, + { url = "https://files.pythonhosted.org/packages/e7/28/35f7411ff80a3640c1f4fc907dcbb6a65061ebb82f66950e38bfc9f7f740/transformers-5.5.0-py3-none-any.whl", hash = "sha256:821a9ff0961abbb29eb1eb686d78df1c85929fdf213a3fe49dc6bd94f9efa944", size = 10245591, upload-time = "2026-04-02T16:13:03.462Z" }, ] [[package]] From fc9280ccf63871a85854bde526a39383bbe2ac98 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Wed, 8 Apr 2026 19:52:51 +0800 Subject: [PATCH 02/70] refactor: replace regex with tomlkit in devtools CLI --- lib/devtools/pyproject.toml | 6 +- lib/devtools/src/crewai_devtools/cli.py | 151 +++++++++++++--- lib/devtools/tests/__init__.py | 0 lib/devtools/tests/test_toml_updates.py | 225 ++++++++++++++++++++++++ pyproject.toml | 1 + uv.lock | 21 +-- 6 files changed, 365 insertions(+), 39 deletions(-) create mode 100644 lib/devtools/tests/__init__.py create mode 100644 lib/devtools/tests/test_toml_updates.py diff --git a/lib/devtools/pyproject.toml b/lib/devtools/pyproject.toml index 815c8392f..7eebc9ea4 100644 --- a/lib/devtools/pyproject.toml +++ b/lib/devtools/pyproject.toml @@ -11,7 +11,7 @@ classifiers = ["Private :: Do Not Upload"] private = true dependencies = [ "click~=8.1.7", - "toml~=0.10.2", + "tomlkit~=0.13.2", "openai>=1.83.0,<3", "python-dotenv~=1.1.1", "pygithub~=1.59.1", @@ -25,6 +25,10 @@ release = "crewai_devtools.cli:release" docs-check = "crewai_devtools.docs_check:docs_check" devtools = "crewai_devtools.cli:main" +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "--noconftest" + [tool.uv] exclude-newer = "3 days" diff --git a/lib/devtools/src/crewai_devtools/cli.py b/lib/devtools/src/crewai_devtools/cli.py index 9f7b469be..785f943c7 100644 --- a/lib/devtools/src/crewai_devtools/cli.py +++ b/lib/devtools/src/crewai_devtools/cli.py @@ -1,8 +1,8 @@ """Development tools for version bumping and git automation.""" +from collections.abc import Mapping import os from pathlib import Path -import re import subprocess import sys import tempfile @@ -18,6 +18,7 @@ from rich.console import Console from rich.markdown import Markdown from rich.panel import Panel from rich.prompt import Confirm +import tomlkit from crewai_devtools.docs_check import docs_check from crewai_devtools.prompts import RELEASE_NOTES_PROMPT, TRANSLATE_RELEASE_NOTES_PROMPT @@ -169,18 +170,17 @@ def update_pyproject_version(file_path: Path, new_version: str) -> bool: if not file_path.exists(): return False - content = file_path.read_text() - new_content = re.sub( - r'^(version\s*=\s*")[^"]+(")', - rf"\g<1>{new_version}\2", - content, - count=1, - flags=re.MULTILINE, - ) - if new_content != content: - file_path.write_text(new_content) - return True - return False + doc = tomlkit.parse(file_path.read_text()) + project = doc.get("project") + if project is None: + return False + old_version = project.get("version") + if old_version is None or old_version == new_version: + return False + + project["version"] = new_version + file_path.write_text(tomlkit.dumps(doc)) + return True _DEFAULT_WORKSPACE_PACKAGES: Final[list[str]] = [ @@ -473,6 +473,14 @@ def update_changelog( return True +def _is_crewai_dep(spec: str) -> bool: + """Return True if *spec* is a ``crewai`` or ``crewai[...]`` dependency.""" + if not spec.startswith("crewai"): + return False + rest = spec[6:] # after "crewai" + return len(rest) > 0 and rest[0] in ("[", "=", ">", "<", "~", "!") + + def _pin_crewai_deps(content: str, version: str) -> str: """Replace crewai dependency version pins in a pyproject.toml string. @@ -486,11 +494,21 @@ def _pin_crewai_deps(content: str, version: str) -> str: Returns: Transformed content. """ - return re.sub( - r'"crewai(\[tools\])?(==|>=)[^"]*"', - lambda m: f'"crewai{(m.group(1) or "")!s}=={version}"', - content, - ) + doc = tomlkit.parse(content) + for key in ("dependencies", "optional-dependencies"): + deps = doc.get("project", {}).get(key) + if deps is None: + continue + # optional-dependencies is a table of lists; dependencies is a list + dep_lists = deps.values() if isinstance(deps, Mapping) else [deps] + for dep_list in dep_lists: + for i, dep in enumerate(dep_list): + s = str(dep) + if not _is_crewai_dep(s) or ("==" not in s and ">=" not in s): + continue + extras = s[6 : s.index("]") + 1] if "[" in s[6:7] else "" + dep_list[i] = f"crewai{extras}=={version}" + return tomlkit.dumps(doc) def update_template_dependencies(templates_dir: Path, new_version: str) -> list[Path]: @@ -1049,6 +1067,11 @@ _ENTERPRISE_EXTRA_PACKAGES: Final[tuple[str, ...]] = tuple( for p in os.getenv("ENTERPRISE_EXTRA_PACKAGES", "").split(",") if p.strip() ) +_ENTERPRISE_WORKFLOW_PATHS: Final[tuple[str, ...]] = tuple( + p.strip() + for p in os.getenv("ENTERPRISE_WORKFLOW_PATHS", "").split(",") + if p.strip() +) def _update_enterprise_crewai_dep(pyproject_path: Path, version: str) -> bool: @@ -1072,6 +1095,86 @@ def _update_enterprise_crewai_dep(pyproject_path: Path, version: str) -> bool: return False +def _update_enterprise_workflows(repo_dir: Path, version: str) -> list[Path]: + """Update crewai version pins in enterprise CI workflow files. + + Applies ``_repin_crewai_install`` line-by-line on the raw file so + only version numbers change and all formatting is preserved. + + Args: + repo_dir: Root of the cloned enterprise repo. + version: New crewai version string. + + Returns: + List of workflow paths that were modified. + """ + updated: list[Path] = [] + for rel_path in _ENTERPRISE_WORKFLOW_PATHS: + workflow = repo_dir / rel_path + if not workflow.exists(): + continue + + raw = workflow.read_text() + lines = raw.splitlines(keepends=True) + changed = False + for i, line in enumerate(lines): + if "crewai[" not in line: + continue + new_line = _repin_crewai_install(line, version) + if new_line != line: + lines[i] = new_line + changed = True + + if changed: + new_raw = "".join(lines) + else: + new_raw = raw + + if new_raw != raw: + workflow.write_text(new_raw) + updated.append(workflow) + + return updated + + +def _repin_crewai_install(run_value: str, version: str) -> str: + """Rewrite ``crewai[extras]==old`` pins in a shell command string. + + Splits on the known ``crewai[`` prefix and reconstructs the pin + with the new version, avoiding regex. + + Args: + run_value: The ``run:`` string from a workflow step. + version: New version to pin to. + + Returns: + The updated string. + """ + result: list[str] = [] + remainder = run_value + marker = "crewai[" + while marker in remainder: + before, _, after = remainder.partition(marker) + result.append(before) + # after looks like: a2a]==1.14.0" ... + bracket_end = after.index("]") + extras = after[:bracket_end] + rest = after[bracket_end + 1 :] + if rest.startswith("=="): + # Find end of version — next quote or whitespace + ver_start = 2 # len("==") + ver_end = ver_start + while ver_end < len(rest) and rest[ver_end] not in ('"', "'", " ", "\n"): + ver_end += 1 + result.append(f"crewai[{extras}]=={version}") + remainder = rest[ver_end:] + else: + result.append(f"crewai[{extras}]") + remainder = rest + result.append(remainder) + return "".join(result) + + _DEPLOYMENT_TEST_REPO: Final[str] = "crewAIInc/crew_deployment_test" _PYPI_POLL_INTERVAL: Final[int] = 15 @@ -1099,11 +1202,7 @@ def _update_deployment_test_repo(version: str, is_prerelease: bool) -> None: pyproject = repo_dir / "pyproject.toml" content = pyproject.read_text() - new_content = re.sub( - r'"crewai\[tools\]==[^"]+"', - f'"crewai[tools]=={version}"', - content, - ) + new_content = _pin_crewai_deps(content, version) if new_content == content: console.print( "[yellow]Warning:[/yellow] No crewai[tools] pin found to update" @@ -1262,6 +1361,12 @@ def _release_enterprise(version: str, is_prerelease: bool, dry_run: bool) -> Non f"[green]✓[/green] Updated crewai[tools] dep in {enterprise_dep_path}" ) + # --- update crewai pins in CI workflows --- + for wf in _update_enterprise_workflows(repo_dir, version): + console.print( + f"[green]✓[/green] Updated crewai pin in {wf.relative_to(repo_dir)}" + ) + _wait_for_pypi("crewai", version) console.print("\nSyncing workspace...") diff --git a/lib/devtools/tests/__init__.py b/lib/devtools/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lib/devtools/tests/test_toml_updates.py b/lib/devtools/tests/test_toml_updates.py new file mode 100644 index 000000000..eb93dd235 --- /dev/null +++ b/lib/devtools/tests/test_toml_updates.py @@ -0,0 +1,225 @@ +"""Tests for TOML-based version and dependency update functions.""" + +from pathlib import Path +from textwrap import dedent + +from crewai_devtools.cli import ( + _pin_crewai_deps, + _repin_crewai_install, + update_pyproject_version, +) + + +# --- update_pyproject_version --- + + +class TestUpdatePyprojectVersion: + def test_updates_version(self, tmp_path: Path) -> None: + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text( + dedent("""\ + [project] + name = "my-pkg" + version = "1.0.0" + """) + ) + + assert update_pyproject_version(pyproject, "2.0.0") is True + assert 'version = "2.0.0"' in pyproject.read_text() + + def test_returns_false_when_already_current(self, tmp_path: Path) -> None: + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text( + dedent("""\ + [project] + name = "my-pkg" + version = "1.0.0" + """) + ) + + assert update_pyproject_version(pyproject, "1.0.0") is False + + def test_returns_false_when_no_project_section(self, tmp_path: Path) -> None: + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text("[tool.ruff]\nline-length = 88\n") + + assert update_pyproject_version(pyproject, "1.0.0") is False + + def test_returns_false_when_version_is_dynamic(self, tmp_path: Path) -> None: + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text( + dedent("""\ + [project] + name = "my-pkg" + dynamic = ["version"] + """) + ) + + assert update_pyproject_version(pyproject, "1.0.0") is False + assert 'version = "1.0.0"' not in pyproject.read_text() + + def test_returns_false_for_missing_file(self, tmp_path: Path) -> None: + assert update_pyproject_version(tmp_path / "nope.toml", "1.0.0") is False + + def test_preserves_comments_and_formatting(self, tmp_path: Path) -> None: + content = dedent("""\ + # This is important + [project] + name = "my-pkg" + version = "1.0.0" # current version + description = "A package" + """) + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text(content) + + update_pyproject_version(pyproject, "2.0.0") + result = pyproject.read_text() + + assert "# This is important" in result + assert 'description = "A package"' in result + + +# --- _pin_crewai_deps --- + + +class TestPinCrewaiDeps: + def test_pins_exact_version(self) -> None: + content = dedent("""\ + [project] + dependencies = [ + "crewai==1.0.0", + ] + """) + result = _pin_crewai_deps(content, "2.0.0") + assert '"crewai==2.0.0"' in result + + def test_pins_minimum_version(self) -> None: + content = dedent("""\ + [project] + dependencies = [ + "crewai>=1.0.0", + ] + """) + result = _pin_crewai_deps(content, "2.0.0") + assert '"crewai==2.0.0"' in result + assert ">=" not in result + + def test_pins_with_tools_extra(self) -> None: + content = dedent("""\ + [project] + dependencies = [ + "crewai[tools]==1.0.0", + ] + """) + result = _pin_crewai_deps(content, "2.0.0") + assert '"crewai[tools]==2.0.0"' in result + + def test_leaves_unrelated_deps_alone(self) -> None: + content = dedent("""\ + [project] + dependencies = [ + "requests>=2.0", + "crewai==1.0.0", + "click~=8.1", + ] + """) + result = _pin_crewai_deps(content, "2.0.0") + assert '"requests>=2.0"' in result + assert '"click~=8.1"' in result + + def test_handles_optional_dependencies(self) -> None: + content = dedent("""\ + [project] + dependencies = [] + + [project.optional-dependencies] + tools = [ + "crewai[tools]>=1.0.0", + ] + """) + result = _pin_crewai_deps(content, "3.0.0") + assert '"crewai[tools]==3.0.0"' in result + + def test_handles_multiple_crewai_entries(self) -> None: + content = dedent("""\ + [project] + dependencies = [ + "crewai==1.0.0", + "crewai[tools]==1.0.0", + ] + """) + result = _pin_crewai_deps(content, "2.0.0") + assert '"crewai==2.0.0"' in result + assert '"crewai[tools]==2.0.0"' in result + + def test_preserves_arbitrary_extras(self) -> None: + content = dedent("""\ + [project] + dependencies = [ + "crewai[a2a]==1.0.0", + ] + """) + result = _pin_crewai_deps(content, "2.0.0") + assert '"crewai[a2a]==2.0.0"' in result + + def test_no_deps_returns_unchanged(self) -> None: + content = dedent("""\ + [project] + name = "empty" + """) + result = _pin_crewai_deps(content, "2.0.0") + assert "empty" in result + + def test_skips_crewai_without_version_specifier(self) -> None: + content = dedent("""\ + [project] + dependencies = [ + "crewai-tools~=1.0", + ] + """) + result = _pin_crewai_deps(content, "2.0.0") + assert '"crewai-tools~=1.0"' in result + + def test_skips_crewai_extras_without_pin(self) -> None: + content = dedent("""\ + [project] + dependencies = [ + "crewai[tools]", + ] + """) + result = _pin_crewai_deps(content, "2.0.0") + assert '"crewai[tools]"' in result + assert "==" not in result + + +# --- _repin_crewai_install --- + + +class TestRepinCrewaiInstall: + def test_repins_a2a_extra(self) -> None: + result = _repin_crewai_install('uv pip install "crewai[a2a]==1.14.0"', "2.0.0") + assert result == 'uv pip install "crewai[a2a]==2.0.0"' + + def test_repins_tools_extra(self) -> None: + result = _repin_crewai_install('uv pip install "crewai[tools]==1.0.0"', "3.0.0") + assert result == 'uv pip install "crewai[tools]==3.0.0"' + + def test_leaves_unrelated_commands_alone(self) -> None: + cmd = "uv pip install requests" + assert _repin_crewai_install(cmd, "2.0.0") == cmd + + def test_handles_multiple_pins(self) -> None: + cmd = 'pip install "crewai[a2a]==1.0.0" "crewai[tools]==1.0.0"' + result = _repin_crewai_install(cmd, "2.0.0") + assert result == 'pip install "crewai[a2a]==2.0.0" "crewai[tools]==2.0.0"' + + def test_preserves_surrounding_text(self) -> None: + cmd = 'echo hello && uv pip install "crewai[a2a]==1.14.0" && echo done' + result = _repin_crewai_install(cmd, "2.0.0") + assert ( + result == 'echo hello && uv pip install "crewai[a2a]==2.0.0" && echo done' + ) + + def test_no_version_specifier_unchanged(self) -> None: + cmd = 'pip install "crewai[tools]>=1.0"' + assert _repin_crewai_install(cmd, "2.0.0") == cmd diff --git a/pyproject.toml b/pyproject.toml index 6f6404677..5894a9b2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,6 +107,7 @@ ignore-decorators = ["typing.overload"] "lib/crewai/tests/**/*.py" = ["S101", "RET504", "S105", "S106"] # Allow assert statements, unnecessary assignments, and hardcoded passwords in tests "lib/crewai-tools/tests/**/*.py" = ["S101", "RET504", "S105", "S106", "RUF012", "N818", "E402", "RUF043", "S110", "B017"] # Allow various test-specific patterns "lib/crewai-files/tests/**/*.py" = ["S101", "RET504", "S105", "S106", "B017", "F841"] # Allow assert statements and blind exception assertions in tests +"lib/devtools/tests/**/*.py" = ["S101"] [tool.mypy] diff --git a/uv.lock b/uv.lock index d52f6621f..5f637e156 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-04-05T10:53:01.907268Z" +exclude-newer = "2026-04-05T11:09:48.9111Z" exclude-newer-span = "P3D" [manifest] @@ -1358,7 +1358,7 @@ dependencies = [ { name = "pygithub" }, { name = "python-dotenv" }, { name = "rich" }, - { name = "toml" }, + { name = "tomlkit" }, ] [package.metadata] @@ -1368,7 +1368,7 @@ requires-dist = [ { name = "pygithub", specifier = "~=1.59.1" }, { name = "python-dotenv", specifier = "~=1.1.1" }, { name = "rich", specifier = ">=13.9.4" }, - { name = "toml", specifier = "~=0.10.2" }, + { name = "tomlkit", specifier = "~=0.13.2" }, ] [[package]] @@ -8037,15 +8037,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, ] -[[package]] -name = "toml" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, -] - [[package]] name = "tomli" version = "2.0.2" @@ -8066,11 +8057,11 @@ wheels = [ [[package]] name = "tomlkit" -version = "0.14.0" +version = "0.13.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, ] [[package]] From 98e0d1054fd4a80561013a8816bb798963eb3618 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Wed, 8 Apr 2026 21:02:25 +0800 Subject: [PATCH 03/70] fix: sanitize tool names in hook decorator filters --- lib/crewai/src/crewai/hooks/decorators.py | 5 +++ lib/crewai/src/crewai/utilities/tool_utils.py | 8 ++--- lib/crewai/tests/hooks/test_decorators.py | 32 +++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/lib/crewai/src/crewai/hooks/decorators.py b/lib/crewai/src/crewai/hooks/decorators.py index 6007f19bb..4f1da08f5 100644 --- a/lib/crewai/src/crewai/hooks/decorators.py +++ b/lib/crewai/src/crewai/hooks/decorators.py @@ -5,6 +5,8 @@ from functools import wraps import inspect from typing import TYPE_CHECKING, Any, TypeVar, overload +from crewai.utilities.string_utils import sanitize_tool_name + if TYPE_CHECKING: from crewai.hooks.llm_hooks import LLMCallHookContext @@ -37,6 +39,9 @@ def _create_hook_decorator( tools: list[str] | None = None, agents: list[str] | None = None, ) -> Callable[..., Any]: + if tools: + tools = [sanitize_tool_name(t) for t in tools] + def decorator(f: Callable[..., Any]) -> Callable[..., Any]: setattr(f, marker_attribute, True) diff --git a/lib/crewai/src/crewai/utilities/tool_utils.py b/lib/crewai/src/crewai/utilities/tool_utils.py index 027f136ed..b77eb9192 100644 --- a/lib/crewai/src/crewai/utilities/tool_utils.py +++ b/lib/crewai/src/crewai/utilities/tool_utils.py @@ -96,7 +96,7 @@ async def aexecute_tool_and_check_finality( if tool: tool_input = tool_calling.arguments if tool_calling.arguments else {} hook_context = ToolCallHookContext( - tool_name=tool_calling.tool_name, + tool_name=sanitized_tool_name, tool_input=tool_input, tool=tool, agent=agent, @@ -120,7 +120,7 @@ async def aexecute_tool_and_check_finality( tool_result = await tool_usage.ause(tool_calling, agent_action.text) after_hook_context = ToolCallHookContext( - tool_name=tool_calling.tool_name, + tool_name=sanitized_tool_name, tool_input=tool_input, tool=tool, agent=agent, @@ -216,7 +216,7 @@ def execute_tool_and_check_finality( if tool: tool_input = tool_calling.arguments if tool_calling.arguments else {} hook_context = ToolCallHookContext( - tool_name=tool_calling.tool_name, + tool_name=sanitized_tool_name, tool_input=tool_input, tool=tool, agent=agent, @@ -240,7 +240,7 @@ def execute_tool_and_check_finality( tool_result = tool_usage.use(tool_calling, agent_action.text) after_hook_context = ToolCallHookContext( - tool_name=tool_calling.tool_name, + tool_name=sanitized_tool_name, tool_input=tool_input, tool=tool, agent=agent, diff --git a/lib/crewai/tests/hooks/test_decorators.py b/lib/crewai/tests/hooks/test_decorators.py index ec147068d..a19a0f740 100644 --- a/lib/crewai/tests/hooks/test_decorators.py +++ b/lib/crewai/tests/hooks/test_decorators.py @@ -192,6 +192,38 @@ class TestToolHookDecorators: # Should still be 1 (hook didn't execute for read_file) assert len(execution_log) == 1 + def test_before_tool_call_tool_filter_sanitizes_names(self): + """Tool filter should auto-sanitize names so users can pass BaseTool.name directly.""" + execution_log = [] + + # User passes the human-readable tool name (e.g. BaseTool.name) + @before_tool_call(tools=["Delete File", "Execute Code"]) + def filtered_hook(context): + execution_log.append(context.tool_name) + return None + + hooks = get_before_tool_call_hooks() + assert len(hooks) == 1 + + mock_tool = Mock() + # Context uses the sanitized name (as set by the executor) + context = ToolCallHookContext( + tool_name="delete_file", + tool_input={}, + tool=mock_tool, + ) + hooks[0](context) + assert execution_log == ["delete_file"] + + # Non-matching tool still filtered out + context2 = ToolCallHookContext( + tool_name="read_file", + tool_input={}, + tool=mock_tool, + ) + hooks[0](context2) + assert execution_log == ["delete_file"] + def test_before_tool_call_with_combined_filters(self): """Test that combined tool and agent filters work.""" execution_log = [] From 0e8ed759475e152d1c6af81468e52b38476b5f68 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Wed, 8 Apr 2026 23:32:37 +0800 Subject: [PATCH 04/70] feat: add aclose()/close() and async context manager to streaming outputs --- docs/ar/learn/streaming-crew-execution.mdx | 28 +++ docs/ar/learn/streaming-flow-execution.mdx | 28 +++ docs/en/learn/streaming-crew-execution.mdx | 28 +++ docs/en/learn/streaming-flow-execution.mdx | 28 +++ docs/ko/learn/streaming-crew-execution.mdx | 28 +++ docs/pt-BR/learn/streaming-crew-execution.mdx | 28 +++ lib/crewai/src/crewai/crew.py | 4 + lib/crewai/src/crewai/crews/utils.py | 2 + lib/crewai/src/crewai/flow/flow.py | 3 + lib/crewai/src/crewai/types/streaming.py | 230 +++++++++--------- lib/crewai/src/crewai/utilities/streaming.py | 26 +- lib/crewai/tests/test_streaming.py | 152 ++++++++++++ 12 files changed, 464 insertions(+), 121 deletions(-) diff --git a/docs/ar/learn/streaming-crew-execution.mdx b/docs/ar/learn/streaming-crew-execution.mdx index 930ef389f..4dfe1859f 100644 --- a/docs/ar/learn/streaming-crew-execution.mdx +++ b/docs/ar/learn/streaming-crew-execution.mdx @@ -325,6 +325,34 @@ asyncio.run(interactive_research()) - **تجربة المستخدم**: تقليل زمن الاستجابة المتصور بعرض نتائج تدريجية - **لوحات المعلومات الحية**: بناء واجهات مراقبة تعرض حالة تنفيذ الطاقم +## الإلغاء وتنظيف الموارد + +يدعم `CrewStreamingOutput` الإلغاء السلس بحيث يتوقف العمل الجاري فوراً عند انقطاع اتصال المستهلك. + +### مدير السياق غير المتزامن + +```python Code +streaming = await crew.akickoff(inputs={"topic": "AI"}) + +async with streaming: + async for chunk in streaming: + print(chunk.content, end="", flush=True) +``` + +### الإلغاء الصريح + +```python Code +streaming = await crew.akickoff(inputs={"topic": "AI"}) +try: + async for chunk in streaming: + print(chunk.content, end="", flush=True) +finally: + await streaming.aclose() # غير متزامن + # streaming.close() # المكافئ المتزامن +``` + +بعد الإلغاء، يكون كل من `streaming.is_cancelled` و `streaming.is_completed` بقيمة `True`. كل من `aclose()` و `close()` متساويان القوة. + ## ملاحظات مهمة - يفعّل البث تلقائياً بث LLM لجميع الوكلاء في الطاقم diff --git a/docs/ar/learn/streaming-flow-execution.mdx b/docs/ar/learn/streaming-flow-execution.mdx index 53663c111..de4575b1c 100644 --- a/docs/ar/learn/streaming-flow-execution.mdx +++ b/docs/ar/learn/streaming-flow-execution.mdx @@ -420,6 +420,34 @@ except Exception as e: print("Streaming completed but flow encountered an error") ``` +## الإلغاء وتنظيف الموارد + +يدعم `FlowStreamingOutput` الإلغاء السلس بحيث يتوقف العمل الجاري فوراً عند انقطاع اتصال المستهلك. + +### مدير السياق غير المتزامن + +```python Code +streaming = await flow.kickoff_async() + +async with streaming: + async for chunk in streaming: + print(chunk.content, end="", flush=True) +``` + +### الإلغاء الصريح + +```python Code +streaming = await flow.kickoff_async() +try: + async for chunk in streaming: + print(chunk.content, end="", flush=True) +finally: + await streaming.aclose() # غير متزامن + # streaming.close() # المكافئ المتزامن +``` + +بعد الإلغاء، يكون كل من `streaming.is_cancelled` و `streaming.is_completed` بقيمة `True`. كل من `aclose()` و `close()` متساويان القوة. + ## ملاحظات مهمة - يفعّل البث تلقائياً بث LLM لأي أطقم مستخدمة داخل التدفق diff --git a/docs/en/learn/streaming-crew-execution.mdx b/docs/en/learn/streaming-crew-execution.mdx index bfcd0850d..ff0a3cd7f 100644 --- a/docs/en/learn/streaming-crew-execution.mdx +++ b/docs/en/learn/streaming-crew-execution.mdx @@ -325,6 +325,34 @@ Streaming is particularly valuable for: - **User Experience**: Reduce perceived latency by showing incremental results - **Live Dashboards**: Build monitoring interfaces that display crew execution status +## Cancellation and Resource Cleanup + +`CrewStreamingOutput` supports graceful cancellation so that in-flight work stops promptly when the consumer disconnects. + +### Async Context Manager + +```python Code +streaming = await crew.akickoff(inputs={"topic": "AI"}) + +async with streaming: + async for chunk in streaming: + print(chunk.content, end="", flush=True) +``` + +### Explicit Cancellation + +```python Code +streaming = await crew.akickoff(inputs={"topic": "AI"}) +try: + async for chunk in streaming: + print(chunk.content, end="", flush=True) +finally: + await streaming.aclose() # async + # streaming.close() # sync equivalent +``` + +After cancellation, `streaming.is_cancelled` and `streaming.is_completed` are both `True`. Both `aclose()` and `close()` are idempotent. + ## Important Notes - Streaming automatically enables LLM streaming for all agents in the crew diff --git a/docs/en/learn/streaming-flow-execution.mdx b/docs/en/learn/streaming-flow-execution.mdx index df0fec91d..31ca0f376 100644 --- a/docs/en/learn/streaming-flow-execution.mdx +++ b/docs/en/learn/streaming-flow-execution.mdx @@ -420,6 +420,34 @@ except Exception as e: print("Streaming completed but flow encountered an error") ``` +## Cancellation and Resource Cleanup + +`FlowStreamingOutput` supports graceful cancellation so that in-flight work stops promptly when the consumer disconnects. + +### Async Context Manager + +```python Code +streaming = await flow.kickoff_async() + +async with streaming: + async for chunk in streaming: + print(chunk.content, end="", flush=True) +``` + +### Explicit Cancellation + +```python Code +streaming = await flow.kickoff_async() +try: + async for chunk in streaming: + print(chunk.content, end="", flush=True) +finally: + await streaming.aclose() # async + # streaming.close() # sync equivalent +``` + +After cancellation, `streaming.is_cancelled` and `streaming.is_completed` are both `True`. Both `aclose()` and `close()` are idempotent. + ## Important Notes - Streaming automatically enables LLM streaming for any crews used within the flow diff --git a/docs/ko/learn/streaming-crew-execution.mdx b/docs/ko/learn/streaming-crew-execution.mdx index aec56caed..db2ce1c0c 100644 --- a/docs/ko/learn/streaming-crew-execution.mdx +++ b/docs/ko/learn/streaming-crew-execution.mdx @@ -325,6 +325,34 @@ asyncio.run(interactive_research()) - **사용자 경험**: 점진적인 결과를 표시하여 체감 지연 시간 감소 - **라이브 대시보드**: crew 실행 상태를 표시하는 모니터링 인터페이스 구축 +## 취소 및 리소스 정리 + +`CrewStreamingOutput`은 소비자가 연결을 끊을 때 진행 중인 작업을 즉시 중단하는 정상적인 취소를 지원합니다. + +### 비동기 컨텍스트 매니저 + +```python Code +streaming = await crew.akickoff(inputs={"topic": "AI"}) + +async with streaming: + async for chunk in streaming: + print(chunk.content, end="", flush=True) +``` + +### 명시적 취소 + +```python Code +streaming = await crew.akickoff(inputs={"topic": "AI"}) +try: + async for chunk in streaming: + print(chunk.content, end="", flush=True) +finally: + await streaming.aclose() # 비동기 + # streaming.close() # 동기 버전 +``` + +취소 후 `streaming.is_cancelled`와 `streaming.is_completed`는 모두 `True`입니다. `aclose()`와 `close()` 모두 멱등성을 가집니다. + ## 중요 사항 - 스트리밍은 crew의 모든 에이전트에 대해 자동으로 LLM 스트리밍을 활성화합니다 diff --git a/docs/pt-BR/learn/streaming-crew-execution.mdx b/docs/pt-BR/learn/streaming-crew-execution.mdx index 85a26e370..4a3df07ef 100644 --- a/docs/pt-BR/learn/streaming-crew-execution.mdx +++ b/docs/pt-BR/learn/streaming-crew-execution.mdx @@ -325,6 +325,34 @@ O streaming é particularmente valioso para: - **Experiência do Usuário**: Reduzir latência percebida mostrando resultados incrementais - **Dashboards ao Vivo**: Construir interfaces de monitoramento que exibem status de execução da crew +## Cancelamento e Limpeza de Recursos + +`CrewStreamingOutput` suporta cancelamento gracioso para que o trabalho em andamento pare imediatamente quando o consumidor desconecta. + +### Gerenciador de Contexto Assíncrono + +```python Code +streaming = await crew.akickoff(inputs={"topic": "AI"}) + +async with streaming: + async for chunk in streaming: + print(chunk.content, end="", flush=True) +``` + +### Cancelamento Explícito + +```python Code +streaming = await crew.akickoff(inputs={"topic": "AI"}) +try: + async for chunk in streaming: + print(chunk.content, end="", flush=True) +finally: + await streaming.aclose() # assíncrono + # streaming.close() # equivalente síncrono +``` + +Após o cancelamento, `streaming.is_cancelled` e `streaming.is_completed` são ambos `True`. Tanto `aclose()` quanto `close()` são idempotentes. + ## Notas Importantes - O streaming ativa automaticamente o streaming do LLM para todos os agentes na crew diff --git a/lib/crewai/src/crewai/crew.py b/lib/crewai/src/crewai/crew.py index e630ec5b0..1c671467e 100644 --- a/lib/crewai/src/crewai/crew.py +++ b/lib/crewai/src/crewai/crew.py @@ -134,6 +134,7 @@ from crewai.utilities.rpm_controller import RPMController from crewai.utilities.streaming import ( create_async_chunk_generator, create_chunk_generator, + register_cleanup, signal_end, signal_error, ) @@ -882,6 +883,7 @@ class Crew(FlowTrackable, BaseModel): ctx.state, run_crew, ctx.output_holder ) ) + register_cleanup(streaming_output, ctx.state) ctx.output_holder.append(streaming_output) return streaming_output @@ -1007,6 +1009,7 @@ class Crew(FlowTrackable, BaseModel): ctx.state, run_crew, ctx.output_holder ) ) + register_cleanup(streaming_output, ctx.state) ctx.output_holder.append(streaming_output) return streaming_output @@ -1078,6 +1081,7 @@ class Crew(FlowTrackable, BaseModel): ctx.state, run_crew, ctx.output_holder ) ) + register_cleanup(streaming_output, ctx.state) ctx.output_holder.append(streaming_output) return streaming_output diff --git a/lib/crewai/src/crewai/crews/utils.py b/lib/crewai/src/crewai/crews/utils.py index 4077a9a19..e85a48b05 100644 --- a/lib/crewai/src/crewai/crews/utils.py +++ b/lib/crewai/src/crewai/crews/utils.py @@ -431,6 +431,7 @@ async def run_for_each_async( from crewai.types.usage_metrics import UsageMetrics from crewai.utilities.streaming import ( create_async_chunk_generator, + register_cleanup, signal_end, signal_error, ) @@ -480,6 +481,7 @@ async def run_for_each_async( streaming_output._set_results(result) streaming_output._set_result = set_results_wrapper # type: ignore[method-assign] + register_cleanup(streaming_output, ctx.state) ctx.output_holder.append(streaming_output) return streaming_output diff --git a/lib/crewai/src/crewai/flow/flow.py b/lib/crewai/src/crewai/flow/flow.py index 60d03b069..97e6bdf20 100644 --- a/lib/crewai/src/crewai/flow/flow.py +++ b/lib/crewai/src/crewai/flow/flow.py @@ -132,6 +132,7 @@ from crewai.utilities.streaming import ( create_async_chunk_generator, create_chunk_generator, create_streaming_state, + register_cleanup, signal_end, signal_error, ) @@ -1962,6 +1963,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta): streaming_output = FlowStreamingOutput( sync_iterator=create_chunk_generator(state, run_flow, output_holder) ) + register_cleanup(streaming_output, state) output_holder.append(streaming_output) return streaming_output @@ -2035,6 +2037,7 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta): state, run_flow, output_holder ) ) + register_cleanup(streaming_output, state) output_holder.append(streaming_output) return streaming_output diff --git a/lib/crewai/src/crewai/types/streaming.py b/lib/crewai/src/crewai/types/streaming.py index a1f6e4ef7..eb3ddbde1 100644 --- a/lib/crewai/src/crewai/types/streaming.py +++ b/lib/crewai/src/crewai/types/streaming.py @@ -2,11 +2,12 @@ from __future__ import annotations -from collections.abc import AsyncIterator, Iterator +from collections.abc import AsyncIterator, Callable, Iterator from enum import Enum from typing import TYPE_CHECKING, Any, Generic, TypeVar from pydantic import BaseModel, Field +from typing_extensions import Self if TYPE_CHECKING: @@ -78,12 +79,21 @@ class StreamingOutputBase(Generic[T]): via the .result property after streaming completes. """ - def __init__(self) -> None: + def __init__( + self, + sync_iterator: Iterator[StreamChunk] | None = None, + async_iterator: AsyncIterator[StreamChunk] | None = None, + ) -> None: """Initialize streaming output base.""" self._result: T | None = None self._completed: bool = False self._chunks: list[StreamChunk] = [] self._error: Exception | None = None + self._cancelled: bool = False + self._exhausted: bool = False + self._on_cleanup: Callable[[], None] | None = None + self._sync_iterator = sync_iterator + self._async_iterator = async_iterator @property def result(self) -> T: @@ -112,6 +122,11 @@ class StreamingOutputBase(Generic[T]): """Check if streaming has completed.""" return self._completed + @property + def is_cancelled(self) -> bool: + """Check if streaming was cancelled.""" + return self._cancelled + @property def chunks(self) -> list[StreamChunk]: """Get all collected chunks so far.""" @@ -129,6 +144,98 @@ class StreamingOutputBase(Generic[T]): if chunk.chunk_type == StreamChunkType.TEXT ) + async def __aenter__(self) -> Self: + """Enter async context manager.""" + return self + + async def __aexit__(self, *exc_info: Any) -> None: + """Exit async context manager, cancelling if still running.""" + await self.aclose() + + async def aclose(self) -> None: + """Cancel streaming and clean up resources. + + Cancels any in-flight tasks and closes the underlying async iterator. + Safe to call multiple times. No-op if already cancelled or fully consumed. + """ + if self._cancelled or self._exhausted or self._error is not None: + return + self._cancelled = True + self._completed = True + if self._async_iterator is not None and hasattr(self._async_iterator, "aclose"): + await self._async_iterator.aclose() + if self._on_cleanup is not None: + self._on_cleanup() + self._on_cleanup = None + + def close(self) -> None: + """Cancel streaming and clean up resources (sync). + + Closes the underlying sync iterator. Safe to call multiple times. + No-op if already cancelled, fully consumed, or errored. + """ + if self._cancelled or self._exhausted or self._error is not None: + return + self._cancelled = True + self._completed = True + if self._sync_iterator is not None and hasattr(self._sync_iterator, "close"): + self._sync_iterator.close() + if self._on_cleanup is not None: + self._on_cleanup() + self._on_cleanup = None + + def __iter__(self) -> Iterator[StreamChunk]: + """Iterate over stream chunks synchronously. + + Yields: + StreamChunk objects as they arrive. + + Raises: + RuntimeError: If sync iterator not available. + """ + if self._sync_iterator is None: + raise RuntimeError("Sync iterator not available") + try: + for chunk in self._sync_iterator: + self._chunks.append(chunk) + yield chunk + self._exhausted = True + except Exception as e: + self._error = e + raise + finally: + self._completed = True + + def __aiter__(self) -> AsyncIterator[StreamChunk]: + """Return async iterator for stream chunks. + + Returns: + Async iterator for StreamChunk objects. + """ + return self._async_iterate() + + async def _async_iterate(self) -> AsyncIterator[StreamChunk]: + """Iterate over stream chunks asynchronously. + + Yields: + StreamChunk objects as they arrive. + + Raises: + RuntimeError: If async iterator not available. + """ + if self._async_iterator is None: + raise RuntimeError("Async iterator not available") + try: + async for chunk in self._async_iterator: + self._chunks.append(chunk) + yield chunk + self._exhausted = True + except Exception as e: + self._error = e + raise + finally: + self._completed = True + class CrewStreamingOutput(StreamingOutputBase["CrewOutput"]): """Streaming output wrapper for crew execution. @@ -167,9 +274,7 @@ class CrewStreamingOutput(StreamingOutputBase["CrewOutput"]): sync_iterator: Synchronous iterator for chunks. async_iterator: Asynchronous iterator for chunks. """ - super().__init__() - self._sync_iterator = sync_iterator - self._async_iterator = async_iterator + super().__init__(sync_iterator=sync_iterator, async_iterator=async_iterator) self._results: list[CrewOutput] | None = None @property @@ -204,56 +309,6 @@ class CrewStreamingOutput(StreamingOutputBase["CrewOutput"]): self._results = results self._completed = True - def __iter__(self) -> Iterator[StreamChunk]: - """Iterate over stream chunks synchronously. - - Yields: - StreamChunk objects as they arrive. - - Raises: - RuntimeError: If sync iterator not available. - """ - if self._sync_iterator is None: - raise RuntimeError("Sync iterator not available") - try: - for chunk in self._sync_iterator: - self._chunks.append(chunk) - yield chunk - except Exception as e: - self._error = e - raise - finally: - self._completed = True - - def __aiter__(self) -> AsyncIterator[StreamChunk]: - """Return async iterator for stream chunks. - - Returns: - Async iterator for StreamChunk objects. - """ - return self._async_iterate() - - async def _async_iterate(self) -> AsyncIterator[StreamChunk]: - """Iterate over stream chunks asynchronously. - - Yields: - StreamChunk objects as they arrive. - - Raises: - RuntimeError: If async iterator not available. - """ - if self._async_iterator is None: - raise RuntimeError("Async iterator not available") - try: - async for chunk in self._async_iterator: - self._chunks.append(chunk) - yield chunk - except Exception as e: - self._error = e - raise - finally: - self._completed = True - def _set_result(self, result: CrewOutput) -> None: """Set the final result after streaming completes. @@ -286,71 +341,6 @@ class FlowStreamingOutput(StreamingOutputBase[Any]): ``` """ - def __init__( - self, - sync_iterator: Iterator[StreamChunk] | None = None, - async_iterator: AsyncIterator[StreamChunk] | None = None, - ) -> None: - """Initialize flow streaming output. - - Args: - sync_iterator: Synchronous iterator for chunks. - async_iterator: Asynchronous iterator for chunks. - """ - super().__init__() - self._sync_iterator = sync_iterator - self._async_iterator = async_iterator - - def __iter__(self) -> Iterator[StreamChunk]: - """Iterate over stream chunks synchronously. - - Yields: - StreamChunk objects as they arrive. - - Raises: - RuntimeError: If sync iterator not available. - """ - if self._sync_iterator is None: - raise RuntimeError("Sync iterator not available") - try: - for chunk in self._sync_iterator: - self._chunks.append(chunk) - yield chunk - except Exception as e: - self._error = e - raise - finally: - self._completed = True - - def __aiter__(self) -> AsyncIterator[StreamChunk]: - """Return async iterator for stream chunks. - - Returns: - Async iterator for StreamChunk objects. - """ - return self._async_iterate() - - async def _async_iterate(self) -> AsyncIterator[StreamChunk]: - """Iterate over stream chunks asynchronously. - - Yields: - StreamChunk objects as they arrive. - - Raises: - RuntimeError: If async iterator not available. - """ - if self._async_iterator is None: - raise RuntimeError("Async iterator not available") - try: - async for chunk in self._async_iterator: - self._chunks.append(chunk) - yield chunk - except Exception as e: - self._error = e - raise - finally: - self._completed = True - def _set_result(self, result: Any) -> None: """Set the final result after streaming completes. diff --git a/lib/crewai/src/crewai/utilities/streaming.py b/lib/crewai/src/crewai/utilities/streaming.py index dd0992684..008144bff 100644 --- a/lib/crewai/src/crewai/utilities/streaming.py +++ b/lib/crewai/src/crewai/utilities/streaming.py @@ -3,6 +3,7 @@ import asyncio from collections.abc import AsyncIterator, Callable, Iterator import contextvars +import logging import queue import threading from typing import Any, NamedTuple @@ -22,6 +23,9 @@ from crewai.types.streaming import ( from crewai.utilities.string_utils import sanitize_tool_name +logger = logging.getLogger(__name__) + + class TaskInfo(TypedDict): """Task context information for streaming.""" @@ -159,10 +163,23 @@ def _finalize_streaming( streaming_output: The streaming output to set the result on. """ _unregister_handler(state.handler) + streaming_output._on_cleanup = None if state.result_holder: streaming_output._set_result(state.result_holder[0]) +def register_cleanup( + streaming_output: CrewStreamingOutput | FlowStreamingOutput, + state: StreamingState, +) -> None: + """Register a cleanup callback on the streaming output. + + Ensures the event handler is unregistered even if aclose()/close() + is called before iteration starts. + """ + streaming_output._on_cleanup = lambda: _unregister_handler(state.handler) + + def create_streaming_state( current_task_info: TaskInfo, result_holder: list[Any], @@ -294,7 +311,14 @@ async def create_async_chunk_generator( raise item yield item finally: - await task + if not task.done(): + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + except Exception: + logger.debug("Background streaming task failed", exc_info=True) if output_holder: _finalize_streaming(state, output_holder[0]) else: diff --git a/lib/crewai/tests/test_streaming.py b/lib/crewai/tests/test_streaming.py index 8eb63694e..7b1c8e1ba 100644 --- a/lib/crewai/tests/test_streaming.py +++ b/lib/crewai/tests/test_streaming.py @@ -709,6 +709,158 @@ class TestStreamingEdgeCases: assert streaming.is_completed +class TestStreamingCancellation: + """Tests for streaming cancellation and resource cleanup.""" + + @pytest.mark.asyncio + async def test_aclose_cancels_async_streaming(self) -> None: + """Test that aclose() stops iteration and marks as cancelled.""" + chunks_yielded: list[str] = [] + + async def slow_gen() -> AsyncIterator[StreamChunk]: + for i in range(100): + await asyncio.sleep(0.01) + chunks_yielded.append(f"chunk-{i}") + yield StreamChunk(content=f"chunk-{i}") + + streaming = CrewStreamingOutput(async_iterator=slow_gen()) + collected: list[StreamChunk] = [] + + async for chunk in streaming: + collected.append(chunk) + if len(collected) >= 3: + break + + await streaming.aclose() + + assert streaming.is_cancelled + assert streaming.is_completed + assert len(collected) == 3 + + @pytest.mark.asyncio + async def test_aclose_idempotent(self) -> None: + """Test that calling aclose() multiple times is safe.""" + async def gen() -> AsyncIterator[StreamChunk]: + yield StreamChunk(content="test") + + streaming = CrewStreamingOutput(async_iterator=gen()) + async for _ in streaming: + pass + + await streaming.aclose() + await streaming.aclose() + assert not streaming.is_cancelled + assert streaming.is_completed + + @pytest.mark.asyncio + async def test_async_context_manager(self) -> None: + """Test using streaming output as async context manager.""" + async def gen() -> AsyncIterator[StreamChunk]: + yield StreamChunk(content="hello") + yield StreamChunk(content="world") + + streaming = CrewStreamingOutput(async_iterator=gen()) + collected: list[StreamChunk] = [] + + async with streaming: + async for chunk in streaming: + collected.append(chunk) + + assert not streaming.is_cancelled + assert streaming.is_completed + assert len(collected) == 2 + + @pytest.mark.asyncio + async def test_async_context_manager_early_exit(self) -> None: + """Test context manager cleans up on early exit.""" + async def gen() -> AsyncIterator[StreamChunk]: + for i in range(100): + await asyncio.sleep(0.01) + yield StreamChunk(content=f"chunk-{i}") + + streaming = CrewStreamingOutput(async_iterator=gen()) + + async with streaming: + async for chunk in streaming: + if chunk.content == "chunk-2": + break + + assert streaming.is_cancelled + assert streaming.is_completed + + def test_close_cancels_sync_streaming(self) -> None: + """Test that close() stops sync streaming and marks as cancelled.""" + def gen() -> Generator[StreamChunk, None, None]: + for i in range(100): + yield StreamChunk(content=f"chunk-{i}") + + streaming = CrewStreamingOutput(sync_iterator=gen()) + collected: list[StreamChunk] = [] + + for chunk in streaming: + collected.append(chunk) + if len(collected) >= 3: + break + + streaming.close() + + assert streaming.is_cancelled + assert streaming.is_completed + + def test_close_idempotent(self) -> None: + """Test that calling close() multiple times is safe.""" + def gen() -> Generator[StreamChunk, None, None]: + yield StreamChunk(content="test") + + streaming = CrewStreamingOutput(sync_iterator=gen()) + list(streaming) + + streaming.close() + streaming.close() + assert not streaming.is_cancelled + assert streaming.is_completed + + @pytest.mark.asyncio + async def test_flow_aclose(self) -> None: + """Test that FlowStreamingOutput aclose() is no-op after normal completion.""" + async def gen() -> AsyncIterator[StreamChunk]: + yield StreamChunk(content="flow-chunk") + + streaming = FlowStreamingOutput(async_iterator=gen()) + async for _ in streaming: + pass + + await streaming.aclose() + assert not streaming.is_cancelled + assert streaming.is_completed + + @pytest.mark.asyncio + async def test_flow_async_context_manager(self) -> None: + """Test FlowStreamingOutput as async context manager with full consumption.""" + async def gen() -> AsyncIterator[StreamChunk]: + yield StreamChunk(content="flow-chunk") + + streaming = FlowStreamingOutput(async_iterator=gen()) + + async with streaming: + async for _ in streaming: + pass + + assert not streaming.is_cancelled + assert streaming.is_completed + + def test_flow_close(self) -> None: + """Test that FlowStreamingOutput close() is no-op after normal completion.""" + def gen() -> Generator[StreamChunk, None, None]: + yield StreamChunk(content="flow-chunk") + + streaming = FlowStreamingOutput(sync_iterator=gen()) + list(streaming) + + streaming.close() + assert not streaming.is_cancelled + + class TestStreamingImports: """Tests for correct imports of streaming types.""" From 1ae237a287304cdc72605db56d0a24fdc5e8c110 Mon Sep 17 00:00:00 2001 From: iris-clawd Date: Wed, 8 Apr 2026 08:49:16 -0700 Subject: [PATCH 05/70] refactor: replace hardcoded denylist with dynamic BaseTool field exclusion in spec gen (#5347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec generator previously used a hardcoded list of field names to exclude from init_params_schema. Any new field or computed_field added to BaseTool (like tool_type from 86ce54f) would silently leak into tool.specs.json unless someone remembered to update that list. Now _extract_init_params() dynamically computes BaseTool's fields at import time via model_fields + model_computed_fields, so any future additions to BaseTool are automatically excluded. Fields from intermediate base classes (RagTool, BraveSearchToolBase, SerpApiBaseTool) are correctly preserved since they're not on BaseTool. TDD: - RED: 3 new tests confirming BaseTool field leak, intermediate base preservation, and future-proofing — all failed before the fix - GREEN: Dynamic allowlist applied — all 10 tests pass - Regenerated tool.specs.json (tool_type removed from all tools) --- .../src/crewai_tools/generate_tool_specs.py | 32 +- .../tests/test_generate_tool_specs.py | 101 +++ lib/crewai-tools/tool.specs.json | 787 +++--------------- 3 files changed, 212 insertions(+), 708 deletions(-) diff --git a/lib/crewai-tools/src/crewai_tools/generate_tool_specs.py b/lib/crewai-tools/src/crewai_tools/generate_tool_specs.py index 34d78e074..579adaa30 100644 --- a/lib/crewai-tools/src/crewai_tools/generate_tool_specs.py +++ b/lib/crewai-tools/src/crewai_tools/generate_tool_specs.py @@ -154,21 +154,19 @@ class ToolSpecExtractor: return default_value + # Dynamically computed from BaseTool so that any future fields or + # computed_fields added to BaseTool are automatically excluded from + # the generated spec — no hardcoded denylist to maintain. + # ``package_dependencies`` is not a BaseTool field but is extracted + # into its own top-level key, so it's also excluded from init_params. + _BASE_TOOL_FIELDS: set[str] = ( + set(BaseTool.model_fields) + | set(BaseTool.model_computed_fields) + | {"package_dependencies"} + ) + @staticmethod def _extract_init_params(tool_class: type[BaseTool]) -> dict[str, Any]: - ignored_init_params = [ - "name", - "description", - "env_vars", - "args_schema", - "description_updated", - "cache_function", - "result_as_answer", - "max_usage_count", - "current_usage_count", - "package_dependencies", - ] - json_schema = tool_class.model_json_schema( schema_generator=SchemaGenerator, mode="serialization" ) @@ -176,8 +174,14 @@ class ToolSpecExtractor: json_schema["properties"] = { key: value for key, value in json_schema["properties"].items() - if key not in ignored_init_params + if key not in ToolSpecExtractor._BASE_TOOL_FIELDS } + if "required" in json_schema: + json_schema["required"] = [ + key + for key in json_schema["required"] + if key not in ToolSpecExtractor._BASE_TOOL_FIELDS + ] return json_schema def save_to_json(self, output_path: str) -> None: diff --git a/lib/crewai-tools/tests/test_generate_tool_specs.py b/lib/crewai-tools/tests/test_generate_tool_specs.py index 7506c4ee4..0841eeda6 100644 --- a/lib/crewai-tools/tests/test_generate_tool_specs.py +++ b/lib/crewai-tools/tests/test_generate_tool_specs.py @@ -45,6 +45,26 @@ class MockTool(BaseTool): ) +# --- Intermediate base class (like RagTool, BraveSearchToolBase) --- +class MockIntermediateBase(BaseTool): + """Simulates an intermediate tool base class (e.g. RagTool, BraveSearchToolBase).""" + + name: str = "Intermediate Base" + description: str = "An intermediate tool base" + shared_config: str = Field("default_config", description="Config from intermediate base") + + def _run(self, query: str) -> str: + return query + + +class MockDerivedTool(MockIntermediateBase): + """A tool inheriting from an intermediate base, like CodeDocsSearchTool(RagTool).""" + + name: str = "Derived Tool" + description: str = "A tool that inherits from intermediate base" + derived_param: str = Field("derived_default", description="Param specific to derived tool") + + @pytest.fixture def extractor(): ext = ToolSpecExtractor() @@ -169,6 +189,87 @@ def test_extract_package_dependencies(mock_tool_extractor): ] +def test_base_tool_fields_excluded_from_init_params(mock_tool_extractor): + """BaseTool internal fields (including computed_field like tool_type) must + never appear in init_params_schema. Studio reads this schema to render + the tool config UI — internal fields confuse users.""" + init_schema = mock_tool_extractor["init_params_schema"] + props = set(init_schema.get("properties", {}).keys()) + required = set(init_schema.get("required", [])) + + # These are all BaseTool's own fields — none should leak + base_fields = {"name", "description", "env_vars", "args_schema", + "description_updated", "cache_function", "result_as_answer", + "max_usage_count", "current_usage_count", "tool_type", + "package_dependencies"} + + leaked_props = base_fields & props + assert not leaked_props, ( + f"BaseTool fields leaked into init_params_schema properties: {leaked_props}" + ) + leaked_required = base_fields & required + assert not leaked_required, ( + f"BaseTool fields leaked into init_params_schema required: {leaked_required}" + ) + + +def test_intermediate_base_fields_preserved_for_derived_tool(extractor): + """When a tool inherits from an intermediate base (e.g. RagTool), + the intermediate's fields should be included — only BaseTool's own + fields are excluded.""" + with ( + mock.patch( + "crewai_tools.generate_tool_specs.dir", + return_value=["MockDerivedTool"], + ), + mock.patch( + "crewai_tools.generate_tool_specs.getattr", + return_value=MockDerivedTool, + ), + ): + extractor.extract_all_tools() + assert len(extractor.tools_spec) == 1 + tool_info = extractor.tools_spec[0] + + props = set(tool_info["init_params_schema"].get("properties", {}).keys()) + + # Intermediate base's field should be preserved + assert "shared_config" in props, ( + "Intermediate base class fields should be preserved in init_params_schema" + ) + # Derived tool's own field should be preserved + assert "derived_param" in props, ( + "Derived tool's own fields should be preserved in init_params_schema" + ) + # BaseTool internals should still be excluded + assert "tool_type" not in props + assert "cache_function" not in props + assert "result_as_answer" not in props + + +def test_future_base_tool_field_auto_excluded(extractor): + """If a new field is added to BaseTool in the future, it should be + automatically excluded from spec generation without needing to update + the ignored list. This test verifies the allowlist approach works + by checking that ONLY non-BaseTool fields appear.""" + with ( + mock.patch("crewai_tools.generate_tool_specs.dir", return_value=["MockTool"]), + mock.patch("crewai_tools.generate_tool_specs.getattr", return_value=MockTool), + ): + extractor.extract_all_tools() + tool_info = extractor.tools_spec[0] + + props = set(tool_info["init_params_schema"].get("properties", {}).keys()) + base_all = set(BaseTool.model_fields) | set(BaseTool.model_computed_fields) + + leaked = base_all & props + assert not leaked, ( + f"BaseTool fields should be auto-excluded but found: {leaked}. " + "The spec generator should dynamically compute BaseTool's fields " + "instead of using a hardcoded denylist." + ) + + def test_save_to_json(extractor, tmp_path): extractor.tools_spec = [ { diff --git a/lib/crewai-tools/tool.specs.json b/lib/crewai-tools/tool.specs.json index adc392bab..76ff76a4b 100644 --- a/lib/crewai-tools/tool.specs.json +++ b/lib/crewai-tools/tool.specs.json @@ -81,16 +81,9 @@ ], "default": null, "title": "Mind Name" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "AIMindTool", "type": "object" }, @@ -168,20 +161,13 @@ "title": "Save Dir", "type": "string" }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - }, "use_title_as_filename": { "default": false, "title": "Use Title As Filename", "type": "boolean" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "ArxivPaperTool", "type": "object" }, @@ -297,16 +283,9 @@ "default": "https://api.search.brave.com/res/v1/images/search", "title": "Search Url", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "BraveImageSearchTool", "type": "object" }, @@ -488,16 +467,9 @@ "default": "https://api.search.brave.com/res/v1/llm/context", "title": "Search Url", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "BraveLLMContextTool", "type": "object" }, @@ -775,16 +747,9 @@ "default": "https://api.search.brave.com/res/v1/local/descriptions", "title": "Search Url", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "BraveLocalPOIsDescriptionTool", "type": "object" }, @@ -896,16 +861,9 @@ "default": "https://api.search.brave.com/res/v1/local/pois", "title": "Search Url", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "BraveLocalPOIsTool", "type": "object" }, @@ -1062,16 +1020,9 @@ "default": "https://api.search.brave.com/res/v1/news/search", "title": "Search Url", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "BraveNewsSearchTool", "type": "object" }, @@ -1344,16 +1295,9 @@ "default": "https://api.search.brave.com/res/v1/web/search", "title": "Search Url", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "BraveSearchTool", "type": "object" }, @@ -1729,16 +1673,9 @@ "default": "https://api.search.brave.com/res/v1/videos/search", "title": "Search Url", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "BraveVideoSearchTool", "type": "object" }, @@ -1999,16 +1936,9 @@ "default": "https://api.search.brave.com/res/v1/web/search", "title": "Search Url", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "BraveWebSearchTool", "type": "object" }, @@ -2380,11 +2310,6 @@ "title": "Format", "type": "string" }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - }, "url": { "anyOf": [ { @@ -2410,9 +2335,7 @@ "title": "Zipcode" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "BrightDataDatasetTool", "type": "object" }, @@ -2590,20 +2513,13 @@ "default": null, "title": "Search Type" }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - }, "zone": { "default": "", "title": "Zone", "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "BrightDataSearchTool", "type": "object" }, @@ -2774,11 +2690,6 @@ "title": "Format", "type": "string" }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - }, "url": { "anyOf": [ { @@ -2797,9 +2708,7 @@ "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "BrightDataWebUnlockerTool", "type": "object" }, @@ -2972,16 +2881,9 @@ ], "default": false, "title": "Text Content" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "BrowserbaseLoadTool", "type": "object" }, @@ -4026,16 +3928,9 @@ "default": false, "title": "Summarize", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "CSVSearchTool", "type": "object" }, @@ -5085,16 +4980,9 @@ "default": false, "title": "Summarize", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "CodeDocsSearchTool", "type": "object" }, @@ -5172,18 +5060,8 @@ } }, "description": "Wrapper for composio tools.", - "properties": { - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - } - }, - "required": [ - "name", - "description", - "tool_type" - ], + "properties": {}, + "required": [], "title": "ComposioTool", "type": "object" }, @@ -5246,16 +5124,10 @@ "contextual_client": { "default": null, "title": "Contextual Client" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ - "api_key", - "tool_type" + "api_key" ], "title": "ContextualAICreateAgentTool", "type": "object" @@ -5348,16 +5220,10 @@ "api_key": { "title": "Api Key", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ - "api_key", - "tool_type" + "api_key" ], "title": "ContextualAIParseTool", "type": "object" @@ -5475,16 +5341,10 @@ "contextual_client": { "default": null, "title": "Contextual Client" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ - "api_key", - "tool_type" + "api_key" ], "title": "ContextualAIQueryTool", "type": "object" @@ -5575,16 +5435,10 @@ "api_key": { "title": "Api Key", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ - "api_key", - "tool_type" + "api_key" ], "title": "ContextualAIRerankTool", "type": "object" @@ -5751,11 +5605,6 @@ "description": "Specify whether the index is scoped. Is True by default.", "title": "Scoped Index", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ @@ -5763,8 +5612,7 @@ "collection_name", "scope_name", "bucket_name", - "index_name", - "tool_type" + "index_name" ], "title": "CouchbaseFTSVectorSearchTool", "type": "object" @@ -6809,16 +6657,9 @@ "default": false, "title": "Summarize", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "DOCXSearchTool", "type": "object" }, @@ -6954,16 +6795,9 @@ ], "default": "1024x1024", "title": "Size" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "DallETool", "type": "object" }, @@ -7064,16 +6898,9 @@ ], "default": null, "title": "Default Warehouse Id" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "DatabricksQueryTool", "type": "object" }, @@ -7203,16 +7030,9 @@ ], "default": null, "title": "Directory" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "DirectoryReadTool", "type": "object" }, @@ -8256,16 +8076,9 @@ "default": false, "title": "Summarize", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "DirectorySearchTool", "type": "object" }, @@ -8409,11 +8222,6 @@ "default": false, "title": "Summary" }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - }, "type": { "anyOf": [ { @@ -8427,9 +8235,7 @@ "title": "Type" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "EXASearchTool", "type": "object" }, @@ -8536,16 +8342,8 @@ "type": "object" } }, - "properties": { - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - } - }, - "required": [ - "tool_type" - ], + "properties": {}, + "required": [], "title": "FileCompressorTool", "type": "object" }, @@ -8647,16 +8445,9 @@ ], "default": null, "title": "File Path" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "FileReadTool", "type": "object" }, @@ -8746,16 +8537,8 @@ "type": "object" } }, - "properties": { - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - } - }, - "required": [ - "tool_type" - ], + "properties": {}, + "required": [], "title": "FileWriterTool", "type": "object" }, @@ -8878,16 +8661,9 @@ } ], "title": "Config" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "FirecrawlCrawlWebsiteTool", "type": "object" }, @@ -8977,16 +8753,9 @@ "additionalProperties": true, "title": "Config", "type": "object" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "FirecrawlScrapeWebsiteTool", "type": "object" }, @@ -9083,16 +8852,9 @@ } ], "title": "Config" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "FirecrawlSearchTool", "type": "object" }, @@ -9187,16 +8949,9 @@ ], "description": "The user's Personal Access Token to access CrewAI AMP API. If not provided, it will be loaded from the environment variable CREWAI_PERSONAL_ACCESS_TOKEN.", "title": "Personal Access Token" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "GenerateCrewaiAutomationTool", "type": "object" }, @@ -10264,16 +10019,10 @@ "default": false, "title": "Summarize", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ - "gh_token", - "tool_type" + "gh_token" ], "title": "GithubSearchTool", "type": "object" @@ -10383,16 +10132,9 @@ ], "default": null, "title": "Hyperbrowser" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "HyperbrowserLoadTool", "type": "object" }, @@ -10495,17 +10237,11 @@ "default": 600, "title": "Max Polling Time", "type": "integer" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ "crew_api_url", - "crew_bearer_token", - "tool_type" + "crew_bearer_token" ], "title": "InvokeCrewAIAutomationTool", "type": "object" @@ -11550,16 +11286,9 @@ "default": false, "title": "Summarize", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "JSONSearchTool", "type": "object" }, @@ -11649,11 +11378,6 @@ "title": "Headers", "type": "object" }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - }, "website_url": { "anyOf": [ { @@ -11667,9 +11391,7 @@ "title": "Website Url" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "JinaScrapeWebsiteTool", "type": "object" }, @@ -11740,16 +11462,8 @@ "type": "object" } }, - "properties": { - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - } - }, - "required": [ - "tool_type" - ], + "properties": {}, + "required": [], "title": "LinkupSearchTool", "type": "object" }, @@ -11809,18 +11523,10 @@ "properties": { "llama_index_tool": { "title": "Llama Index Tool" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ - "name", - "description", - "llama_index_tool", - "tool_type" + "llama_index_tool" ], "title": "LlamaIndexTool", "type": "object" @@ -12855,16 +12561,9 @@ "default": false, "title": "Summarize", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "MDXSearchTool", "type": "object" }, @@ -12976,20 +12675,12 @@ "description": "UUID of the Agent Handler Tool Pack to use", "title": "Tool Pack Id", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ - "name", - "description", "tool_pack_id", "registered_user_id", - "tool_name", - "tool_type" + "tool_name" ], "title": "MergeAgentHandlerTool", "type": "object" @@ -13173,11 +12864,6 @@ "title": "Text Key", "type": "string" }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - }, "vector_index_name": { "default": "vector_index", "description": "Name of the Atlas Search vector index", @@ -13188,8 +12874,7 @@ "required": [ "database_name", "collection_name", - "connection_string", - "tool_type" + "connection_string" ], "title": "MongoDBVectorSearchTool", "type": "object" @@ -13296,16 +12981,9 @@ ], "default": null, "title": "Session Id" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "MultiOnTool", "type": "object" }, @@ -14346,16 +14024,10 @@ "default": false, "title": "Summarize", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ - "db_uri", - "tool_type" + "db_uri" ], "title": "MySQLSearchTool", "type": "object" @@ -14451,16 +14123,10 @@ }, "title": "Tables", "type": "array" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ - "db_uri", - "tool_type" + "db_uri" ], "title": "NL2SQLTool", "type": "object" @@ -14869,16 +14535,9 @@ "properties": { "llm": { "$ref": "#/$defs/LLM" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "OCRTool", "type": "object" }, @@ -15075,17 +14734,11 @@ }, "oxylabs_api": { "title": "Oxylabs Api" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ "oxylabs_api", - "config", - "tool_type" + "config" ], "title": "OxylabsAmazonProductScraperTool", "type": "object" @@ -15310,17 +14963,11 @@ }, "oxylabs_api": { "title": "Oxylabs Api" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ "oxylabs_api", - "config", - "tool_type" + "config" ], "title": "OxylabsAmazonSearchScraperTool", "type": "object" @@ -15558,17 +15205,11 @@ }, "oxylabs_api": { "title": "Oxylabs Api" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ "oxylabs_api", - "config", - "tool_type" + "config" ], "title": "OxylabsGoogleSearchScraperTool", "type": "object" @@ -15754,17 +15395,11 @@ }, "oxylabs_api": { "title": "Oxylabs Api" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ "oxylabs_api", - "config", - "tool_type" + "config" ], "title": "OxylabsUniversalScraperTool", "type": "object" @@ -16822,16 +16457,9 @@ "default": false, "title": "Summarize", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "PDFSearchTool", "type": "object" }, @@ -16913,16 +16541,9 @@ "default": "https://api.parallel.ai/v1beta/search", "title": "Search Url", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "ParallelSearchTool", "type": "object" }, @@ -17081,16 +16702,9 @@ }, "title": "Evaluators", "type": "array" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "PatronusEvalTool", "type": "object" }, @@ -17156,17 +16770,11 @@ "evaluator": { "title": "Evaluator", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ "evaluator", - "evaluated_model_gold_answer", - "tool_type" + "evaluated_model_gold_answer" ], "title": "PatronusLocalEvaluatorTool", "type": "object" @@ -17272,16 +16880,9 @@ }, "title": "Evaluators", "type": "array" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "PatronusPredefinedCriteriaEvalTool", "type": "object" }, @@ -17470,16 +17071,10 @@ "description": "Base package path for Qdrant. Will dynamically import client and models.", "title": "Qdrant Package", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ - "qdrant_config", - "tool_type" + "qdrant_config" ], "title": "QdrantVectorSearchTool", "type": "object" @@ -18549,16 +18144,9 @@ "default": false, "title": "Summarize", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "RagTool", "type": "object" }, @@ -18654,11 +18242,6 @@ ], "title": "Headers" }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - }, "website_url": { "anyOf": [ { @@ -18672,9 +18255,7 @@ "title": "Website Url" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "ScrapeElementFromWebsiteTool", "type": "object" }, @@ -18774,11 +18355,6 @@ ], "title": "Headers" }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - }, "website_url": { "anyOf": [ { @@ -18792,9 +18368,7 @@ "title": "Website Url" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "ScrapeWebsiteTool", "type": "object" }, @@ -18884,11 +18458,6 @@ "title": "Enable Logging", "type": "boolean" }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - }, "user_prompt": { "anyOf": [ { @@ -18914,9 +18483,7 @@ "title": "Website Url" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "ScrapegraphScrapeTool", "type": "object" }, @@ -19017,16 +18584,9 @@ ], "default": null, "title": "Scrapfly" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "ScrapflyScrapeWebsiteTool", "type": "object" }, @@ -19184,11 +18744,6 @@ "default": false, "title": "Return Html" }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - }, "wait_time": { "anyOf": [ { @@ -19214,9 +18769,7 @@ "title": "Website Url" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "SeleniumScrapingTool", "type": "object" }, @@ -19306,16 +18859,9 @@ ], "default": null, "title": "Client" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "SerpApiGoogleSearchTool", "type": "object" }, @@ -19411,16 +18957,9 @@ ], "default": null, "title": "Client" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "SerpApiGoogleShoppingTool", "type": "object" }, @@ -19562,16 +19101,9 @@ "default": "search", "title": "Search Type", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "SerperDevTool", "type": "object" }, @@ -19642,16 +19174,8 @@ "type": "object" } }, - "properties": { - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - } - }, - "required": [ - "tool_type" - ], + "properties": {}, + "required": [], "title": "SerperScrapeWebsiteTool", "type": "object" }, @@ -20739,16 +20263,9 @@ "default": false, "title": "Summarize", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "SerplyJobSearchTool", "type": "object" }, @@ -20862,16 +20379,9 @@ "default": "https://api.serply.io/v1/news/", "title": "Search Url", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "SerplyNewsSearchTool", "type": "object" }, @@ -20985,16 +20495,9 @@ "default": "https://api.serply.io/v1/scholar/", "title": "Search Url", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "SerplyScholarSearchTool", "type": "object" }, @@ -21144,16 +20647,9 @@ "default": "https://api.serply.io/v1/search/", "title": "Search Url", "type": "string" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "SerplyWebSearchTool", "type": "object" }, @@ -22234,16 +21730,9 @@ "default": false, "title": "Summarize", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "SerplyWebpageToMarkdownTool", "type": "object" }, @@ -22384,16 +21873,9 @@ ], "default": null, "title": "Connection Pool" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "SingleStoreSearchTool", "type": "object" }, @@ -22604,16 +22086,10 @@ "description": "Delay between retries in seconds", "title": "Retry Delay", "type": "number" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, "required": [ - "config", - "tool_type" + "config" ], "title": "SnowflakeSearchTool", "type": "object" @@ -22800,11 +22276,6 @@ "default": null, "title": "Spider" }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - }, "website_url": { "anyOf": [ { @@ -22818,9 +22289,7 @@ "title": "Website Url" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "SpiderTool", "type": "object" }, @@ -22983,11 +22452,6 @@ "default": "https://api.stagehand.browserbase.com/v1", "title": "Server Url" }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - }, "use_simplified_dom": { "default": true, "title": "Use Simplified Dom", @@ -23004,9 +22468,7 @@ "type": "boolean" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "StagehandTool", "type": "object" }, @@ -24084,11 +23546,6 @@ "title": "Summarize", "type": "boolean" }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - }, "txt": { "anyOf": [ { @@ -24102,9 +23559,7 @@ "title": "Txt" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "TXTSearchTool", "type": "object" }, @@ -24251,16 +23706,9 @@ "description": "The timeout for the extraction request in seconds.", "title": "Timeout", "type": "integer" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "TavilyExtractorTool", "type": "object" }, @@ -24507,11 +23955,6 @@ "title": "Timeout", "type": "integer" }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - }, "topic": { "default": "general", "description": "The topic to focus the search on.", @@ -24524,9 +23967,7 @@ "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "TavilySearchTool", "type": "object" }, @@ -24600,16 +24041,8 @@ } }, "description": "Tool for analyzing images using vision models.\n\nArgs:\n llm: Optional LLM instance to use\n model: Model identifier to use if no LLM is provided", - "properties": { - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - } - }, - "required": [ - "tool_type" - ], + "properties": {}, + "required": [], "title": "VisionTool", "type": "object" }, @@ -24731,11 +24164,6 @@ "default": null, "title": "Query" }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" - }, "vectorizer": { "title": "Vectorizer" }, @@ -24753,8 +24181,7 @@ "required": [ "collection_name", "weaviate_cluster_url", - "weaviate_api_key", - "tool_type" + "weaviate_api_key" ], "title": "WeaviateVectorSearchTool", "type": "object" @@ -25801,16 +25228,9 @@ "default": false, "title": "Summarize", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "WebsiteSearchTool", "type": "object" }, @@ -26860,16 +26280,9 @@ "default": false, "title": "Summarize", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "XMLSearchTool", "type": "object" }, @@ -27919,16 +27332,9 @@ "default": false, "title": "Summarize", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "YoutubeChannelSearchTool", "type": "object" }, @@ -28978,16 +28384,9 @@ "default": false, "title": "Summarize", "type": "boolean" - }, - "tool_type": { - "readOnly": true, - "title": "Tool Type", - "type": "string" } }, - "required": [ - "tool_type" - ], + "required": [], "title": "YoutubeVideoSearchTool", "type": "object" }, From 1c784695c1732dd03fedca21dbd2eb3d15dae8c9 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Wed, 8 Apr 2026 23:59:09 +0800 Subject: [PATCH 06/70] feat: add async checkpoint TUI browser Launch a Textual TUI via `crewai checkpoint` to browse and resume from checkpoints. Uses run_async/akickoff for fully async execution. Adds provider auto-detection from file magic bytes. --- lib/crewai/src/crewai/cli/checkpoint_tui.py | 366 ++++++++++++++++++ lib/crewai/src/crewai/cli/cli.py | 16 +- lib/crewai/src/crewai/crew.py | 4 + lib/crewai/src/crewai/state/provider/utils.py | 34 ++ 4 files changed, 417 insertions(+), 3 deletions(-) create mode 100644 lib/crewai/src/crewai/cli/checkpoint_tui.py create mode 100644 lib/crewai/src/crewai/state/provider/utils.py diff --git a/lib/crewai/src/crewai/cli/checkpoint_tui.py b/lib/crewai/src/crewai/cli/checkpoint_tui.py new file mode 100644 index 000000000..5f81d5fad --- /dev/null +++ b/lib/crewai/src/crewai/cli/checkpoint_tui.py @@ -0,0 +1,366 @@ +"""Textual TUI for browsing checkpoint files.""" + +from __future__ import annotations + +from typing import Any, ClassVar + +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.containers import Horizontal, Vertical +from textual.screen import ModalScreen +from textual.widgets import Button, Footer, Header, OptionList, Static +from textual.widgets.option_list import Option + +from crewai.cli.checkpoint_cli import ( + _entity_summary, + _format_size, + _is_sqlite, + _list_json, + _list_sqlite, +) + + +_PRIMARY = "#eb6658" +_SECONDARY = "#1F7982" +_TERTIARY = "#ffffff" +_DIM = "#888888" +_BG_DARK = "#0d1117" +_BG_PANEL = "#161b22" + + +def _load_entries(location: str) -> list[dict[str, Any]]: + if _is_sqlite(location): + return _list_sqlite(location) + return _list_json(location) + + +def _format_list_label(entry: dict[str, Any]) -> str: + """Format a checkpoint entry for the list panel.""" + name = entry.get("name", "") + ts = entry.get("ts") or "" + trigger = entry.get("trigger") or "" + summary = _entity_summary(entry.get("entities", [])) + + line1 = f"[bold]{name}[/]" + parts = [] + if ts: + parts.append(f"[dim]{ts}[/]") + if "size" in entry: + parts.append(f"[dim]{_format_size(entry['size'])}[/]") + if trigger: + parts.append(f"[{_PRIMARY}]{trigger}[/]") + line2 = " ".join(parts) + line3 = f" [{_DIM}]{summary}[/]" + + return f"{line1}\n{line2}\n{line3}" + + +def _format_detail(entry: dict[str, Any]) -> str: + """Format checkpoint details for the right panel.""" + lines: list[str] = [] + + # Header + name = entry.get("name", "") + lines.append(f"[bold {_PRIMARY}]{name}[/]") + lines.append(f"[{_DIM}]{'─' * 50}[/]") + lines.append("") + + # Metadata table + ts = entry.get("ts") or "unknown" + trigger = entry.get("trigger") or "" + lines.append(f" [bold]Time[/] {ts}") + if "size" in entry: + lines.append(f" [bold]Size[/] {_format_size(entry['size'])}") + lines.append(f" [bold]Events[/] {entry.get('event_count', 0)}") + if trigger: + lines.append(f" [bold]Trigger[/] [{_PRIMARY}]{trigger}[/]") + if "path" in entry: + lines.append(f" [bold]Path[/] [{_DIM}]{entry['path']}[/]") + if "db" in entry: + lines.append(f" [bold]Database[/] [{_DIM}]{entry['db']}[/]") + + # Entities + for ent in entry.get("entities", []): + eid = str(ent.get("id", ""))[:8] + etype = ent.get("type", "unknown") + ename = ent.get("name", "unnamed") + + lines.append("") + lines.append(f" [{_DIM}]{'─' * 50}[/]") + lines.append(f" [bold {_SECONDARY}]{etype}[/]: {ename} [{_DIM}]{eid}[/]") + + tasks = ent.get("tasks") + if isinstance(tasks, list): + completed = ent.get("tasks_completed", 0) + total = ent.get("tasks_total", 0) + pct = int(completed / total * 100) if total else 0 + bar_len = 20 + filled = int(bar_len * completed / total) if total else 0 + bar = f"[{_PRIMARY}]{'█' * filled}[/][{_DIM}]{'░' * (bar_len - filled)}[/]" + + lines.append(f" {bar} {completed}/{total} tasks ({pct}%)") + lines.append("") + + for i, task in enumerate(tasks): + if task.get("completed"): + icon = "[green]✓[/]" + else: + icon = "[yellow]○[/]" + desc = str(task.get("description", "")) + if len(desc) > 55: + desc = desc[:52] + "..." + lines.append(f" {icon} {i + 1}. {desc}") + + return "\n".join(lines) + + +class ConfirmResumeScreen(ModalScreen[bool]): + """Modal confirmation before resuming from a checkpoint.""" + + CSS = f""" + ConfirmResumeScreen {{ + align: center middle; + }} + #confirm-dialog {{ + width: 60; + height: auto; + padding: 1 2; + background: {_BG_PANEL}; + border: round {_PRIMARY}; + }} + #confirm-label {{ + width: 100%; + content-align: center middle; + margin-bottom: 1; + }} + #confirm-name {{ + width: 100%; + content-align: center middle; + color: {_PRIMARY}; + text-style: bold; + margin-bottom: 1; + }} + #confirm-buttons {{ + width: 100%; + height: 3; + layout: horizontal; + align: center middle; + }} + Button {{ + margin: 0 2; + min-width: 12; + }} + """ + + def __init__(self, checkpoint_name: str) -> None: + super().__init__() + self._checkpoint_name = checkpoint_name + + def compose(self) -> ComposeResult: + with Vertical(id="confirm-dialog"): + yield Static("Resume from this checkpoint?", id="confirm-label") + yield Static(self._checkpoint_name, id="confirm-name") + with Horizontal(id="confirm-buttons"): + yield Button("Resume", variant="success", id="btn-yes") + yield Button("Cancel", variant="default", id="btn-no") + + def on_button_pressed(self, event: Button.Pressed) -> None: + self.dismiss(event.button.id == "btn-yes") + + def on_key(self, event: Any) -> None: + if event.key == "y": + self.dismiss(True) + elif event.key in ("n", "escape"): + self.dismiss(False) + + +class CheckpointTUI(App[str | None]): + """TUI to browse and inspect checkpoints. + + Returns the checkpoint location string to resume from, or None if + the user quit without selecting. + """ + + TITLE = "CrewAI Checkpoints" + + CSS = f""" + Screen {{ + background: {_BG_DARK}; + }} + Header {{ + background: {_PRIMARY}; + color: {_TERTIARY}; + }} + Footer {{ + background: {_SECONDARY}; + color: {_TERTIARY}; + }} + Footer > .footer-key--key {{ + background: {_PRIMARY}; + color: {_TERTIARY}; + }} + Horizontal {{ + height: 1fr; + }} + #cp-list {{ + width: 38%; + background: {_BG_PANEL}; + border: round {_SECONDARY}; + padding: 0 1; + scrollbar-color: {_PRIMARY}; + }} + #cp-list:focus {{ + border: round {_PRIMARY}; + }} + #cp-list > .option-list--option-highlighted {{ + background: {_SECONDARY}; + color: {_TERTIARY}; + text-style: none; + }} + #cp-list > .option-list--option-highlighted * {{ + color: {_TERTIARY}; + }} + #detail-container {{ + width: 62%; + padding: 0 1; + }} + #detail {{ + height: 1fr; + background: {_BG_PANEL}; + border: round {_SECONDARY}; + padding: 1 2; + overflow-y: auto; + scrollbar-color: {_PRIMARY}; + }} + #detail:focus {{ + border: round {_PRIMARY}; + }} + #status {{ + height: 1; + padding: 0 2; + color: {_DIM}; + }} + """ + + BINDINGS: ClassVar[list[Binding | tuple[str, str] | tuple[str, str, str]]] = [ + ("q", "quit", "Quit"), + ("r", "refresh", "Refresh"), + ("j", "cursor_down", "Down"), + ("k", "cursor_up", "Up"), + ] + + def __init__(self, location: str = "./.checkpoints") -> None: + super().__init__() + self._location = location + self._entries: list[dict[str, Any]] = [] + self._selected_idx: int = 0 + self._pending_location: str = "" + + def compose(self) -> ComposeResult: + yield Header(show_clock=False) + with Horizontal(): + yield OptionList(id="cp-list") + with Vertical(id="detail-container"): + yield Static("", id="status") + yield Static( + f"\n [{_DIM}]Select a checkpoint from the list[/]", # noqa: S608 + id="detail", + ) + yield Footer() + + async def on_mount(self) -> None: + self.query_one("#cp-list", OptionList).border_title = "Checkpoints" + self.query_one("#detail", Static).border_title = "Detail" + self._refresh_list() + + def _refresh_list(self) -> None: + self._entries = _load_entries(self._location) + option_list = self.query_one("#cp-list", OptionList) + option_list.clear_options() + + if not self._entries: + self.query_one("#detail", Static).update( + f"\n [{_DIM}]No checkpoints in {self._location}[/]" + ) + self.query_one("#status", Static).update("") + self.sub_title = self._location + return + + for entry in self._entries: + option_list.add_option(Option(_format_list_label(entry))) + + count = len(self._entries) + storage = "SQLite" if _is_sqlite(self._location) else "JSON" + self.sub_title = f"{self._location}" + self.query_one("#status", Static).update(f" {count} checkpoint(s) | {storage}") + + async def on_option_list_option_highlighted( + self, + event: OptionList.OptionHighlighted, + ) -> None: + idx = event.option_index + if idx is None: + return + if idx < len(self._entries): + self._selected_idx = idx + entry = self._entries[idx] + self.query_one("#detail", Static).update(_format_detail(entry)) + + def action_cursor_down(self) -> None: + self.query_one("#cp-list", OptionList).action_cursor_down() + + def action_cursor_up(self) -> None: + self.query_one("#cp-list", OptionList).action_cursor_up() + + async def on_option_list_option_selected( + self, + event: OptionList.OptionSelected, + ) -> None: + idx = event.option_index + if idx is None or idx >= len(self._entries): + return + entry = self._entries[idx] + if "path" in entry: + loc = entry["path"] + elif _is_sqlite(self._location): + loc = f"{self._location}#{entry['name']}" + else: + loc = entry.get("name", "") + self._pending_location = loc + name = entry.get("name", loc) + self.push_screen(ConfirmResumeScreen(name), self._on_confirm) + + def _on_confirm(self, confirmed: bool | None) -> None: + if confirmed: + self.exit(self._pending_location) + else: + self._pending_location = "" + + def action_refresh(self) -> None: + self._refresh_list() + + +async def _run_checkpoint_tui_async(location: str) -> None: + """Async implementation of the checkpoint TUI flow.""" + import click + + app = CheckpointTUI(location=location) + selected = await app.run_async() + + if selected is None: + return + + click.echo(f"\nResuming from: {selected}\n") + + from crewai.crew import Crew + + crew = Crew.from_checkpoint(selected) + result = await crew.akickoff() + click.echo(f"\nResult: {getattr(result, 'raw', result)}") + + +def run_checkpoint_tui(location: str = "./.checkpoints") -> None: + """Launch the checkpoint browser TUI.""" + import asyncio + + asyncio.run(_run_checkpoint_tui_async(location)) diff --git a/lib/crewai/src/crewai/cli/cli.py b/lib/crewai/src/crewai/cli/cli.py index 57ff4551a..20a65dbe1 100644 --- a/lib/crewai/src/crewai/cli/cli.py +++ b/lib/crewai/src/crewai/cli/cli.py @@ -786,9 +786,19 @@ def traces_status() -> None: console.print(panel) -@crewai.group() -def checkpoint() -> None: - """Inspect checkpoint files.""" +@crewai.group(invoke_without_command=True) +@click.option( + "--location", default="./.checkpoints", help="Checkpoint directory or SQLite file." +) +@click.pass_context +def checkpoint(ctx: click.Context, location: str) -> None: + """Browse and inspect checkpoints. Launches a TUI when called without a subcommand.""" + ctx.ensure_object(dict) + ctx.obj["location"] = location + if ctx.invoked_subcommand is None: + from crewai.cli.checkpoint_tui import run_checkpoint_tui + + run_checkpoint_tui(location) @checkpoint.command("list") diff --git a/lib/crewai/src/crewai/crew.py b/lib/crewai/src/crewai/crew.py index 1c671467e..4090e706b 100644 --- a/lib/crewai/src/crewai/crew.py +++ b/lib/crewai/src/crewai/crew.py @@ -380,8 +380,12 @@ class Crew(FlowTrackable, BaseModel): from crewai.context import apply_execution_context from crewai.events.event_bus import crewai_event_bus from crewai.state.provider.json_provider import JsonProvider + from crewai.state.provider.utils import detect_provider from crewai.state.runtime import RuntimeState + if provider is None: + provider = detect_provider(path) + state = RuntimeState.from_checkpoint( path, provider=provider or JsonProvider(), diff --git a/lib/crewai/src/crewai/state/provider/utils.py b/lib/crewai/src/crewai/state/provider/utils.py new file mode 100644 index 000000000..f4854cbe5 --- /dev/null +++ b/lib/crewai/src/crewai/state/provider/utils.py @@ -0,0 +1,34 @@ +"""Provider detection utilities.""" + +from __future__ import annotations + +from crewai.state.provider.core import BaseProvider + + +_SQLITE_MAGIC = b"SQLite format 3\x00" + + +def detect_provider(path: str) -> BaseProvider: + """Detect the storage provider from a checkpoint path. + + Reads the file's magic bytes to determine if it's a SQLite database. + For paths containing ``#``, checks the portion before the ``#``. + Falls back to JsonProvider. + + Args: + path: A checkpoint file path, directory, or ``db_path#checkpoint_id``. + + Returns: + The appropriate provider instance. + """ + from crewai.state.provider.json_provider import JsonProvider + from crewai.state.provider.sqlite_provider import SqliteProvider + + file_path = path.split("#")[0] if "#" in path else path + try: + with open(file_path, "rb") as f: + if f.read(16) == _SQLITE_MAGIC: + return SqliteProvider() + except OSError: + pass + return JsonProvider() From 8bae7408993e81a5b98cdd7a0df6293d937b6b5c Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Thu, 9 Apr 2026 00:13:07 +0800 Subject: [PATCH 07/70] fix: use regex for template pyproject.toml version bumps tomlkit.parse() fails on Jinja placeholders like {{folder_name}} in CLI template files. Switch to regex replacement for templates. --- lib/devtools/src/crewai_devtools/cli.py | 9 ++++- lib/devtools/tests/test_toml_updates.py | 49 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/lib/devtools/src/crewai_devtools/cli.py b/lib/devtools/src/crewai_devtools/cli.py index 785f943c7..c155026d9 100644 --- a/lib/devtools/src/crewai_devtools/cli.py +++ b/lib/devtools/src/crewai_devtools/cli.py @@ -514,6 +514,10 @@ def _pin_crewai_deps(content: str, version: str) -> str: def update_template_dependencies(templates_dir: Path, new_version: str) -> list[Path]: """Update crewai dependency versions in CLI template pyproject.toml files. + Uses simple string replacement instead of TOML parsing because + template files contain Jinja placeholders (``{{folder_name}}``) + that are not valid TOML. + Args: templates_dir: Path to the CLI templates directory. new_version: New version string. @@ -521,10 +525,13 @@ def update_template_dependencies(templates_dir: Path, new_version: str) -> list[ Returns: List of paths that were updated. """ + import re + + pattern = re.compile(r"(crewai(?:\[[\w,]+\])?)(?:==|>=)[^\s\"']+") updated = [] for pyproject in templates_dir.rglob("pyproject.toml"): content = pyproject.read_text() - new_content = _pin_crewai_deps(content, new_version) + new_content = pattern.sub(rf"\1=={new_version}", content) if new_content != content: pyproject.write_text(new_content) updated.append(pyproject) diff --git a/lib/devtools/tests/test_toml_updates.py b/lib/devtools/tests/test_toml_updates.py index eb93dd235..0a47283a9 100644 --- a/lib/devtools/tests/test_toml_updates.py +++ b/lib/devtools/tests/test_toml_updates.py @@ -7,6 +7,7 @@ from crewai_devtools.cli import ( _pin_crewai_deps, _repin_crewai_install, update_pyproject_version, + update_template_dependencies, ) @@ -223,3 +224,51 @@ class TestRepinCrewaiInstall: def test_no_version_specifier_unchanged(self) -> None: cmd = 'pip install "crewai[tools]>=1.0"' assert _repin_crewai_install(cmd, "2.0.0") == cmd + + +# --- update_template_dependencies --- + + +class TestUpdateTemplateDependencies: + def test_updates_jinja_template(self, tmp_path: Path) -> None: + """Template pyproject.toml files with Jinja placeholders should not break.""" + tpl = tmp_path / "crew" / "pyproject.toml" + tpl.parent.mkdir() + tpl.write_text( + dedent("""\ + [project] + name = "{{folder_name}}" + version = "0.1.0" + dependencies = [ + "crewai[tools]==1.14.0" + ] + + [project.scripts] + {{folder_name}} = "{{folder_name}}.main:run" + """) + ) + + updated = update_template_dependencies(tmp_path, "2.0.0") + + assert len(updated) == 1 + content = tpl.read_text() + assert '"crewai[tools]==2.0.0"' in content + assert "{{folder_name}}" in content + + def test_updates_bare_crewai(self, tmp_path: Path) -> None: + tpl = tmp_path / "pyproject.toml" + tpl.write_text('dependencies = [\n "crewai==1.0.0"\n]\n') + + updated = update_template_dependencies(tmp_path, "3.0.0") + + assert len(updated) == 1 + assert '"crewai==3.0.0"' in tpl.read_text() + + def test_skips_unrelated_deps(self, tmp_path: Path) -> None: + tpl = tmp_path / "pyproject.toml" + tpl.write_text('dependencies = [\n "requests>=2.0"\n]\n') + + updated = update_template_dependencies(tmp_path, "2.0.0") + + assert len(updated) == 0 + assert '"requests>=2.0"' in tpl.read_text() From 52c227ab17eb8ad2892c2cd0bbf8cebdd046f8fb Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Thu, 9 Apr 2026 00:22:24 +0800 Subject: [PATCH 08/70] feat: bump versions to 1.14.1rc1 --- lib/crewai-files/src/crewai_files/__init__.py | 2 +- lib/crewai-tools/pyproject.toml | 2 +- lib/crewai-tools/src/crewai_tools/__init__.py | 2 +- lib/crewai/pyproject.toml | 2 +- lib/crewai/src/crewai/__init__.py | 2 +- lib/crewai/src/crewai/cli/templates/crew/pyproject.toml | 2 +- lib/crewai/src/crewai/cli/templates/flow/pyproject.toml | 2 +- lib/crewai/src/crewai/cli/templates/tool/pyproject.toml | 2 +- lib/devtools/src/crewai_devtools/__init__.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/crewai-files/src/crewai_files/__init__.py b/lib/crewai-files/src/crewai_files/__init__.py index 7430288b5..0ca503d5a 100644 --- a/lib/crewai-files/src/crewai_files/__init__.py +++ b/lib/crewai-files/src/crewai_files/__init__.py @@ -152,4 +152,4 @@ __all__ = [ "wrap_file_source", ] -__version__ = "1.14.0" +__version__ = "1.14.1rc1" diff --git a/lib/crewai-tools/pyproject.toml b/lib/crewai-tools/pyproject.toml index 7653f9851..f91954070 100644 --- a/lib/crewai-tools/pyproject.toml +++ b/lib/crewai-tools/pyproject.toml @@ -10,7 +10,7 @@ requires-python = ">=3.10, <3.14" dependencies = [ "pytube~=15.0.0", "requests~=2.32.5", - "crewai==1.14.0", + "crewai==1.14.1rc1", "tiktoken~=0.8.0", "beautifulsoup4~=4.13.4", "python-docx~=1.2.0", diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index 2230e9afc..24fa5671f 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -305,4 +305,4 @@ __all__ = [ "ZapierActionTools", ] -__version__ = "1.14.0" +__version__ = "1.14.1rc1" diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index 99749cc67..816dc132e 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -55,7 +55,7 @@ Repository = "https://github.com/crewAIInc/crewAI" [project.optional-dependencies] tools = [ - "crewai-tools==1.14.0", + "crewai-tools==1.14.1rc1", ] embeddings = [ "tiktoken~=0.8.0" diff --git a/lib/crewai/src/crewai/__init__.py b/lib/crewai/src/crewai/__init__.py index 1fdf84e70..98e5d5d11 100644 --- a/lib/crewai/src/crewai/__init__.py +++ b/lib/crewai/src/crewai/__init__.py @@ -46,7 +46,7 @@ def _suppress_pydantic_deprecation_warnings() -> None: _suppress_pydantic_deprecation_warnings() -__version__ = "1.14.0" +__version__ = "1.14.1rc1" _telemetry_submitted = False diff --git a/lib/crewai/src/crewai/cli/templates/crew/pyproject.toml b/lib/crewai/src/crewai/cli/templates/crew/pyproject.toml index 0fabbb1b3..1651fa1e3 100644 --- a/lib/crewai/src/crewai/cli/templates/crew/pyproject.toml +++ b/lib/crewai/src/crewai/cli/templates/crew/pyproject.toml @@ -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.0" + "crewai[tools]==1.14.1rc1" ] [project.scripts] diff --git a/lib/crewai/src/crewai/cli/templates/flow/pyproject.toml b/lib/crewai/src/crewai/cli/templates/flow/pyproject.toml index e2f3e567e..7cd694ddf 100644 --- a/lib/crewai/src/crewai/cli/templates/flow/pyproject.toml +++ b/lib/crewai/src/crewai/cli/templates/flow/pyproject.toml @@ -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.0" + "crewai[tools]==1.14.1rc1" ] [project.scripts] diff --git a/lib/crewai/src/crewai/cli/templates/tool/pyproject.toml b/lib/crewai/src/crewai/cli/templates/tool/pyproject.toml index 7f65a59a0..d88425e96 100644 --- a/lib/crewai/src/crewai/cli/templates/tool/pyproject.toml +++ b/lib/crewai/src/crewai/cli/templates/tool/pyproject.toml @@ -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.0" + "crewai[tools]==1.14.1rc1" ] [tool.crewai] diff --git a/lib/devtools/src/crewai_devtools/__init__.py b/lib/devtools/src/crewai_devtools/__init__.py index 54244d24f..9821c3cc3 100644 --- a/lib/devtools/src/crewai_devtools/__init__.py +++ b/lib/devtools/src/crewai_devtools/__init__.py @@ -1,3 +1,3 @@ """CrewAI development tools.""" -__version__ = "1.14.0" +__version__ = "1.14.1rc1" From fe028ef4006013a40d0672fa822fee24d5c47c54 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Thu, 9 Apr 2026 00:29:04 +0800 Subject: [PATCH 09/70] docs: update changelog and version for v1.14.1rc1 --- docs/ar/changelog.mdx | 34 ++++++++++++++++++++++++++++++++++ docs/en/changelog.mdx | 34 ++++++++++++++++++++++++++++++++++ docs/ko/changelog.mdx | 34 ++++++++++++++++++++++++++++++++++ docs/pt-BR/changelog.mdx | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+) diff --git a/docs/ar/changelog.mdx b/docs/ar/changelog.mdx index b2f335d6c..8b6739241 100644 --- a/docs/ar/changelog.mdx +++ b/docs/ar/changelog.mdx @@ -4,6 +4,40 @@ description: "تحديثات المنتج والتحسينات وإصلاحات icon: "clock" mode: "wide" --- + + ## v1.14.1rc1 + + [عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.1rc1) + + ## ما الذي تغير + + ### الميزات + - إضافة متصفح TUI لنقطة التحقق غير المتزامنة + - إضافة aclose()/close() ومدير سياق غير متزامن لمخرجات البث + + ### إصلاحات الأخطاء + - إصلاح زيادة إصدارات pyproject.toml باستخدام التعبيرات العادية + - تنظيف أسماء الأدوات في مرشحات ديكور المكونات + - زيادة إصدار transformers إلى 5.5.0 لحل CVE-2026-1839 + - تسجيل معالجات نقطة التحقق عند إنشاء CheckpointConfig + + ### إعادة الهيكلة + - استبدال القائمة المحظورة الثابتة باستبعاد حقل BaseTool الديناميكي في توليد المواصفات + - استبدال التعبيرات العادية بـ tomlkit في واجهة سطر الأوامر devtools + - استخدام كائن PRINTER المشترك + - جعل BaseProvider نموذجًا أساسيًا مع مميز نوع المزود + - إزالة غلاف stdout/stderr لـ FilteredStream + - إزالة flow/config.py غير المستخدمة + + ### الوثائق + - تحديث سجل التغييرات والإصدار لـ v1.14.0 + + ## المساهمون + + @greysonlalonde, @iris-clawd, @joaomdmoura + + + ## v1.14.0 diff --git a/docs/en/changelog.mdx b/docs/en/changelog.mdx index 891d9fc8b..62945af05 100644 --- a/docs/en/changelog.mdx +++ b/docs/en/changelog.mdx @@ -4,6 +4,40 @@ description: "Product updates, improvements, and bug fixes for CrewAI" icon: "clock" mode: "wide" --- + + ## v1.14.1rc1 + + [View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.1rc1) + + ## What's Changed + + ### Features + - Add async checkpoint TUI browser + - Add aclose()/close() and async context manager to streaming outputs + + ### Bug Fixes + - Fix template pyproject.toml version bumps using regex + - Sanitize tool names in hook decorator filters + - Bump transformers to 5.5.0 to resolve CVE-2026-1839 + - Register checkpoint handlers when CheckpointConfig is created + + ### Refactoring + - Replace hardcoded denylist with dynamic BaseTool field exclusion in spec gen + - Replace regex with tomlkit in devtools CLI + - Use shared PRINTER singleton + - Make BaseProvider a BaseModel with provider_type discriminator + - Remove FilteredStream stdout/stderr wrapper + - Remove unused flow/config.py + + ### Documentation + - Update changelog and version for v1.14.0 + + ## Contributors + + @greysonlalonde, @iris-clawd, @joaomdmoura + + + ## v1.14.0 diff --git a/docs/ko/changelog.mdx b/docs/ko/changelog.mdx index ad4a3db79..5524cd317 100644 --- a/docs/ko/changelog.mdx +++ b/docs/ko/changelog.mdx @@ -4,6 +4,40 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정" icon: "clock" mode: "wide" --- + + ## v1.14.1rc1 + + [GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.1rc1) + + ## 변경 사항 + + ### 기능 + - 비동기 체크포인트 TUI 브라우저 추가 + - 스트리밍 출력에 aclose()/close() 및 비동기 컨텍스트 관리자 추가 + + ### 버그 수정 + - 정규 표현식을 사용하여 템플릿 pyproject.toml 버전 증가 수정 + - 후크 데코레이터 필터에서 도구 이름 정리 + - CVE-2026-1839 해결을 위해 transformers를 5.5.0으로 업데이트 + - CheckpointConfig가 생성될 때 체크포인트 핸들러 등록 + + ### 리팩토링 + - 하드코딩된 거부 목록을 동적 BaseTool 필드 제외로 교체 + - devtools CLI에서 정규 표현식을 tomlkit으로 교체 + - 공유 PRINTER 싱글톤 사용 + - BaseProvider를 provider_type 구분자가 있는 BaseModel로 변경 + - FilteredStream stdout/stderr 래퍼 제거 + - 사용되지 않는 flow/config.py 제거 + + ### 문서 + - v1.14.0에 대한 변경 로그 및 버전 업데이트 + + ## 기여자 + + @greysonlalonde, @iris-clawd, @joaomdmoura + + + ## v1.14.0 diff --git a/docs/pt-BR/changelog.mdx b/docs/pt-BR/changelog.mdx index febf0d886..fd7e7c7cd 100644 --- a/docs/pt-BR/changelog.mdx +++ b/docs/pt-BR/changelog.mdx @@ -4,6 +4,40 @@ description: "Atualizações de produto, melhorias e correções do CrewAI" icon: "clock" mode: "wide" --- + + ## v1.14.1rc1 + + [Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.1rc1) + + ## O que Mudou + + ### Recursos + - Adicionar navegador TUI de ponto de verificação assíncrono + - Adicionar aclose()/close() e gerenciador de contexto assíncrono para saídas de streaming + + ### Correções de Bugs + - Corrigir aumentos de versão do template pyproject.toml usando regex + - Sanitizar nomes de ferramentas nos filtros do decorador de hook + - Atualizar transformers para 5.5.0 para resolver CVE-2026-1839 + - Registrar manipuladores de ponto de verificação quando CheckpointConfig é criado + + ### Refatoração + - Substituir lista de negação codificada por exclusão dinâmica de campo BaseTool na geração de especificações + - Substituir regex por tomlkit na CLI do devtools + - Usar singleton PRINTER compartilhado + - Tornar BaseProvider um BaseModel com discriminador de tipo de provedor + - Remover wrapper stdout/stderr de FilteredStream + - Remover flow/config.py não utilizado + + ### Documentação + - Atualizar changelog e versão para v1.14.0 + + ## Contribuidores + + @greysonlalonde, @iris-clawd, @joaomdmoura + + + ## v1.14.0 From 5c08e566b5b5f51a59f63910b6223d345322fe79 Mon Sep 17 00:00:00 2001 From: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:10:18 -0700 Subject: [PATCH 10/70] dedicate skills page (#5331) --- docs/ar/skills.mdx | 50 +++++++++++++++++++++++++++++++++++++++++++ docs/docs.json | 40 ++++++++++++++++++++++++++++++++++ docs/en/skills.mdx | 50 +++++++++++++++++++++++++++++++++++++++++++ docs/ko/skills.mdx | 50 +++++++++++++++++++++++++++++++++++++++++++ docs/pt-BR/skills.mdx | 50 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 240 insertions(+) create mode 100644 docs/ar/skills.mdx create mode 100644 docs/en/skills.mdx create mode 100644 docs/ko/skills.mdx create mode 100644 docs/pt-BR/skills.mdx diff --git a/docs/ar/skills.mdx b/docs/ar/skills.mdx new file mode 100644 index 000000000..4e0bf6e22 --- /dev/null +++ b/docs/ar/skills.mdx @@ -0,0 +1,50 @@ +--- +title: Skills +description: ثبّت crewaiinc/skills من السجل الرسمي على skills.sh—Flows وCrews ووكلاء مرتبطون بالوثائق لـ Claude Code وCursor وCodex وغيرها. +icon: wand-magic-sparkles +mode: "wide" +--- + +# Skills + +**امنح وكيل البرمجة سياق CrewAI في أمر واحد.** + +تُنشر **Skills** الخاصة بـ CrewAI على **[skills.sh/crewaiinc/skills](https://skills.sh/crewaiinc/skills)**—السجل الرسمي لـ `crewaiinc/skills`، بما في ذلك كل مهارة (مثل **design-agent** و**getting-started** و**design-task** و**ask-docs**) وإحصاءات التثبيت والتدقيقات. تعلّم وكلاء البرمجة—مثل Claude Code وCursor وCodex—هيكلة Flows وضبط Crews واستخدام الأدوات واتباع أنماط CrewAI. نفّذ الأمر أدناه (أو الصقه في الوكيل). + +```shell Terminal +npx skills add crewaiinc/skills +``` + +يضيف ذلك حزمة المهارات إلى سير عمل الوكيل لتطبيق اتفاقيات CrewAI دون إعادة شرح الإطار في كل جلسة. المصدر والقضايا على [GitHub](https://github.com/crewAIInc/skills). + +## ما يحصل عليه الوكيل + +- **Flows** — تطبيقات ذات حالة وخطوات وkickoffs للـ crew على نمط CrewAI +- **Crews والوكلاء** — أنماط YAML أولاً، أدوار، مهام، وتفويض +- **الأدوات والتكاملات** — ربط الوكلاء بالبحث وواجهات API وأدوات CrewAI الشائعة +- **هيكل المشروع** — مواءمة مع قوالب CLI واتفاقيات المستودع +- **أنماط محدثة** — تتبع المهارات وثائق CrewAI والممارسات الموصى بها + +## تعرّف أكثر على هذا الموقع + + + + استخدام `AGENTS.md` وسير عمل وكلاء البرمجة مع CrewAI. + + + ابنِ أول Flow وcrew من البداية للنهاية. + + + ثبّت CrewAI CLI وحزمة Python. + + + القائمة الرسمية لـ `crewaiinc/skills`—المهارات والتثبيتات والتدقيقات. + + + مصدر الحزمة والتحديثات والقضايا. + + + +### فيديو: CrewAI مع مهارات وكلاء البرمجة + +