From ee374d01de77a1ed3766d546d4218ce739d90e4a Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Wed, 25 Feb 2026 12:13:00 -0500 Subject: [PATCH 1/4] chore: add versioning logic for devtools --- .github/workflows/publish.yml | 5 - .../workflows/trigger-deployment-tests.yml | 18 - lib/devtools/src/crewai_devtools/cli.py | 342 +++++++++++++++++- lib/devtools/src/crewai_devtools/prompts.py | 15 + 4 files changed, 348 insertions(+), 32 deletions(-) delete mode 100644 .github/workflows/trigger-deployment-tests.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 04284c7fe..1b3000647 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,8 +1,6 @@ name: Publish to PyPI on: - repository_dispatch: - types: [deployment-tests-passed] workflow_dispatch: inputs: release_tag: @@ -20,11 +18,8 @@ jobs: - name: Determine release tag id: release run: | - # Priority: workflow_dispatch input > repository_dispatch payload > default branch if [ -n "${{ inputs.release_tag }}" ]; then echo "tag=${{ inputs.release_tag }}" >> $GITHUB_OUTPUT - elif [ -n "${{ github.event.client_payload.release_tag }}" ]; then - echo "tag=${{ github.event.client_payload.release_tag }}" >> $GITHUB_OUTPUT else echo "tag=" >> $GITHUB_OUTPUT fi diff --git a/.github/workflows/trigger-deployment-tests.yml b/.github/workflows/trigger-deployment-tests.yml deleted file mode 100644 index eaad490a5..000000000 --- a/.github/workflows/trigger-deployment-tests.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Trigger Deployment Tests - -on: - release: - types: [published] - -jobs: - trigger: - name: Trigger deployment tests - runs-on: ubuntu-latest - steps: - - name: Trigger deployment tests - uses: peter-evans/repository-dispatch@v3 - with: - token: ${{ secrets.CREWAI_DEPLOYMENTS_PAT }} - repository: ${{ secrets.CREWAI_DEPLOYMENTS_REPOSITORY }} - event-type: crewai-release - client-payload: '{"release_tag": "${{ github.event.release.tag_name }}", "release_name": "${{ github.event.release.name }}"}' diff --git a/lib/devtools/src/crewai_devtools/cli.py b/lib/devtools/src/crewai_devtools/cli.py index abe3709a7..e3ad1c7a6 100644 --- a/lib/devtools/src/crewai_devtools/cli.py +++ b/lib/devtools/src/crewai_devtools/cli.py @@ -14,7 +14,7 @@ from rich.markdown import Markdown from rich.panel import Panel from rich.prompt import Confirm -from crewai_devtools.prompts import RELEASE_NOTES_PROMPT +from crewai_devtools.prompts import RELEASE_NOTES_PROMPT, TRANSLATE_RELEASE_NOTES_PROMPT load_dotenv() @@ -191,6 +191,248 @@ def update_pyproject_dependencies(file_path: Path, new_version: str) -> bool: return False +def add_docs_version(docs_json_path: Path, version: str) -> bool: + """Add a new version to the Mintlify docs.json versioning config. + + Copies the current default version's tabs into a new version entry, + sets the new version as default, and marks the previous default as + non-default. Operates on all languages. + + Args: + docs_json_path: Path to docs/docs.json. + version: Version string (e.g., "1.10.0"). + + Returns: + True if docs.json was updated, False otherwise. + """ + import json + + if not docs_json_path.exists(): + return False + + data = json.loads(docs_json_path.read_text()) + version_label = f"v{version}" + updated = False + + for lang in data.get("navigation", {}).get("languages", []): + versions = lang.get("versions", []) + if not versions: + continue + + # Skip if this version already exists for this language + if any(v.get("version") == version_label for v in versions): + continue + + # Find the current default and copy its tabs + default_version = next( + (v for v in versions if v.get("default")), + versions[0], + ) + + new_version = { + "version": version_label, + "default": True, + "tabs": default_version.get("tabs", []), + } + + # Remove default flag from old default + default_version.pop("default", None) + + # Insert new version at the beginning + versions.insert(0, new_version) + updated = True + + if not updated: + return False + + docs_json_path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n") + return True + + +_PT_BR_MONTHS = { + 1: "jan", + 2: "fev", + 3: "mar", + 4: "abr", + 5: "mai", + 6: "jun", + 7: "jul", + 8: "ago", + 9: "set", + 10: "out", + 11: "nov", + 12: "dez", +} + +_CHANGELOG_LOCALES: dict[str, dict[str, str]] = { + "en": { + "link_text": "View release on GitHub", + "language_name": "English", + }, + "pt-BR": { + "link_text": "Ver release no GitHub", + "language_name": "Brazilian Portuguese", + }, + "ko": { + "link_text": "GitHub 릴리스 보기", + "language_name": "Korean", + }, +} + + +def translate_release_notes( + release_notes: str, + lang: str, + client: OpenAI, +) -> str: + """Translate release notes into the target language using OpenAI. + + Args: + release_notes: English release notes markdown. + lang: Language code (e.g., "pt-BR", "ko"). + client: OpenAI client instance. + + Returns: + Translated release notes, or original on failure. + """ + locale_cfg = _CHANGELOG_LOCALES.get(lang) + if not locale_cfg: + return release_notes + + language_name = locale_cfg["language_name"] + prompt = TRANSLATE_RELEASE_NOTES_PROMPT.substitute( + language=language_name, + release_notes=release_notes, + ) + + try: + response = client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + { + "role": "system", + "content": f"You are a professional translator. Translate technical documentation into {language_name}.", + }, + {"role": "user", "content": prompt}, + ], + temperature=0.3, + ) + return response.choices[0].message.content or release_notes + except Exception as e: + console.print( + f"[yellow]Warning:[/yellow] Could not translate to {language_name}: {e}" + ) + return release_notes + + +def _format_changelog_date(lang: str) -> str: + """Format today's date for a changelog entry in the given language.""" + from datetime import datetime + + now = datetime.now() + if lang == "ko": + return f"{now.year}년 {now.month}월 {now.day}일" + if lang == "pt-BR": + return f"{now.day:02d} {_PT_BR_MONTHS[now.month]} {now.year}" + return now.strftime("%b %d, %Y") + + +def update_changelog( + changelog_path: Path, + version: str, + release_notes: str, + lang: str = "en", +) -> bool: + """Prepend a new release entry to a docs changelog file. + + Args: + changelog_path: Path to the changelog.mdx file. + version: Version string (e.g., "1.9.3"). + release_notes: Markdown release notes content. + lang: Language code for localized date/link text. + + Returns: + True if changelog was updated, False otherwise. + """ + if not changelog_path.exists(): + return False + + locale_cfg = _CHANGELOG_LOCALES.get(lang, _CHANGELOG_LOCALES["en"]) + date_label = _format_changelog_date(lang) + link_text = locale_cfg["link_text"] + + # Indent each non-empty line with 2 spaces to match block format + indented_lines = [] + for line in release_notes.splitlines(): + if line.strip(): + indented_lines.append(f" {line}") + else: + indented_lines.append("") + indented_notes = "\n".join(indented_lines) + + entry = ( + f'\n' + f" ## v{version}\n" + f"\n" + f" [{link_text}]" + f"(https://github.com/crewAIInc/crewAI/releases/tag/{version})\n" + f"\n" + f"{indented_notes}\n" + f"\n" + f"" + ) + + content = changelog_path.read_text() + + # Insert after the frontmatter closing --- + parts = content.split("---", 2) + if len(parts) >= 3: + new_content = ( + parts[0] + + "---" + + parts[1] + + "---\n" + + entry + + "\n\n" + + parts[2].lstrip("\n") + ) + else: + new_content = entry + "\n\n" + content + + changelog_path.write_text(new_content) + return True + + +def update_template_dependencies(templates_dir: Path, new_version: str) -> list[Path]: + """Update crewai dependency versions in CLI template pyproject.toml files. + + Handles both pinned (==) and minimum (>=) version specifiers, + as well as extras like [tools]. + + Args: + templates_dir: Path to the CLI templates directory. + new_version: New version string. + + Returns: + List of paths that were updated. + """ + import re + + updated = [] + for pyproject in templates_dir.rglob("pyproject.toml"): + content = pyproject.read_text() + new_content = re.sub( + r'"crewai(\[tools\])?(==|>=)[^"]*"', + lambda m: f'"crewai{(m.group(1) or "")!s}=={new_version}"', + content, + ) + if new_content != content: + pyproject.write_text(new_content) + updated.append(pyproject) + + return updated + + def find_version_files(base_path: Path) -> list[Path]: """Find all __init__.py files that contain __version__. @@ -394,6 +636,22 @@ def bump(version: str, dry_run: bool, no_push: bool, no_commit: bool) -> None: "[yellow]Warning:[/yellow] No __version__ attributes found to update" ) + # Update CLI template pyproject.toml files + templates_dir = lib_dir / "crewai" / "src" / "crewai" / "cli" / "templates" + if templates_dir.exists(): + if dry_run: + for tpl in templates_dir.rglob("pyproject.toml"): + console.print( + f"[dim][DRY RUN][/dim] Would update template: {tpl.relative_to(cwd)}" + ) + else: + tpl_updated = update_template_dependencies(templates_dir, version) + for tpl in tpl_updated: + console.print( + f"[green]✓[/green] Updated template: {tpl.relative_to(cwd)}" + ) + updated_files.append(tpl) + if not dry_run: console.print("\nSyncing workspace...") run_command(["uv", "sync"]) @@ -575,9 +833,9 @@ def tag(dry_run: bool, no_edit: bool) -> None: github_contributors = get_github_contributors(commit_range) - if commits.strip(): - client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) + openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) + if commits.strip(): contributors_section = "" if github_contributors: contributors_section = f"\n\n## Contributors\n\n{', '.join([f'@{u}' for u in github_contributors])}" @@ -588,7 +846,7 @@ def tag(dry_run: bool, no_edit: bool) -> None: contributors_section=contributors_section, ) - response = client.chat.completions.create( + response = openai_client.chat.completions.create( model="gpt-4o-mini", messages=[ { @@ -643,6 +901,77 @@ def tag(dry_run: bool, no_edit: bool) -> None: "\n[green]✓[/green] Using generated release notes without editing" ) + is_prerelease = any( + indicator in version.lower() + for indicator in ["a", "b", "rc", "alpha", "beta", "dev"] + ) + + # Update docs: changelogs + version switcher + docs_json_path = cwd / "docs" / "docs.json" + changelog_langs = ["en", "pt-BR", "ko"] + if not dry_run: + docs_files_staged = [] + + for lang in changelog_langs: + cl_path = cwd / "docs" / lang / "changelog.mdx" + if lang == "en": + notes_for_lang = release_notes + else: + console.print(f"[dim]Translating release notes to {lang}...[/dim]") + notes_for_lang = translate_release_notes( + release_notes, lang, openai_client + ) + if update_changelog(cl_path, version, notes_for_lang, lang=lang): + console.print( + f"[green]✓[/green] Updated {cl_path.relative_to(cwd)}" + ) + docs_files_staged.append(str(cl_path)) + else: + console.print( + f"[yellow]Warning:[/yellow] Changelog not found at {cl_path.relative_to(cwd)}" + ) + + if not is_prerelease: + if add_docs_version(docs_json_path, version): + console.print( + f"[green]✓[/green] Added v{version} to docs version switcher" + ) + docs_files_staged.append(str(docs_json_path)) + else: + console.print( + f"[yellow]Warning:[/yellow] docs.json not found at {docs_json_path.relative_to(cwd)}" + ) + + if docs_files_staged: + for f in docs_files_staged: + run_command(["git", "add", f]) + run_command( + [ + "git", + "commit", + "-m", + f"docs: update changelog and version for v{version}", + ] + ) + console.print("[green]✓[/green] Committed docs updates") + run_command(["git", "push"]) + console.print("[green]✓[/green] Pushed docs updates") + else: + for lang in changelog_langs: + cl_path = cwd / "docs" / lang / "changelog.mdx" + translated = " (translated)" if lang != "en" else "" + console.print( + f"[dim][DRY RUN][/dim] Would update {cl_path.relative_to(cwd)}{translated}" + ) + if not is_prerelease: + console.print( + f"[dim][DRY RUN][/dim] Would add v{version} to docs version switcher" + ) + else: + console.print( + "[dim][DRY RUN][/dim] Skipping docs version (pre-release)" + ) + if not dry_run: with console.status(f"[cyan]Creating tag {tag_name}..."): try: @@ -660,11 +989,6 @@ def tag(dry_run: bool, no_edit: bool) -> None: sys.exit(1) console.print(f"[green]✓[/green] Pushed tag {tag_name}") - is_prerelease = any( - indicator in version.lower() - for indicator in ["a", "b", "rc", "alpha", "beta", "dev"] - ) - with console.status("[cyan]Creating GitHub Release..."): try: gh_cmd = [ diff --git a/lib/devtools/src/crewai_devtools/prompts.py b/lib/devtools/src/crewai_devtools/prompts.py index 1e96f03f4..6272972af 100644 --- a/lib/devtools/src/crewai_devtools/prompts.py +++ b/lib/devtools/src/crewai_devtools/prompts.py @@ -43,3 +43,18 @@ Instructions: Keep it professional and clear.""" ) + + +TRANSLATE_RELEASE_NOTES_PROMPT = Template( + """Translate the following release notes into $language. + +$release_notes + +Instructions: +- Translate all section headers and descriptions naturally +- Keep markdown formatting (##, ###, -, etc.) exactly as-is +- Keep all proper nouns, code identifiers, class names, and technical terms unchanged + (e.g. "CrewAI", "LiteAgent", "ChromaDB", "MCP", "@username") +- Keep the ## Contributors section and GitHub usernames unchanged +- Do not add or remove any content, only translate""" +) From 8102d0a6cadea9aef2e484cb9315674fe9695cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moura?= Date: Wed, 25 Feb 2026 10:13:31 -0800 Subject: [PATCH 2/4] feat: enhance JSON argument parsing and validation in CrewAgentExecutor and BaseTool * feat: enhance JSON argument parsing and validation in CrewAgentExecutor and BaseTool - Added error handling for malformed JSON tool arguments in CrewAgentExecutor, providing descriptive error messages. - Implemented schema validation for tool arguments in BaseTool, ensuring that invalid arguments raise appropriate exceptions. - Introduced tests to verify correct behavior for both valid and invalid JSON inputs, enhancing robustness of tool execution. * refactor: improve argument validation in BaseTool - Introduced a new private method to handle argument validation for tools, enhancing code clarity and reusability. - Updated the method to utilize the new validation method, ensuring consistent error handling for invalid arguments. - Enhanced exception handling to specifically catch , providing clearer error messages for tool argument validation failures. * feat: introduce parse_tool_call_args for improved argument parsing - Added a new utility function, parse_tool_call_args, to handle parsing of tool call arguments from JSON strings or dictionaries, enhancing error handling for malformed JSON inputs. - Updated CrewAgentExecutor and AgentExecutor to utilize the new parsing function, streamlining argument validation and improving clarity in error reporting. - Introduced unit tests for parse_tool_call_args to ensure robust functionality and correct handling of various input scenarios. * feat: add keyword argument validation in BaseTool and Tool classes - Introduced a new method `_validate_kwargs` in BaseTool to validate keyword arguments against the defined schema, ensuring proper argument handling. - Updated the `run` and `arun` methods in both BaseTool and Tool classes to utilize the new validation method, improving error handling and robustness. - Added comprehensive tests for asynchronous execution in `TestBaseToolArunValidation` to verify correct behavior for valid and invalid keyword arguments. * Potential fix for pull request finding 'Syntax error' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --------- Co-authored-by: lorenzejay Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com> Co-authored-by: Greyson LaLonde Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../src/crewai/agents/crew_agent_executor.py | 11 +- .../src/crewai/experimental/agent_executor.py | 11 +- lib/crewai/src/crewai/tools/base_tool.py | 30 ++- .../src/crewai/utilities/agent_utils.py | 30 +++ .../tests/agents/test_native_tool_calling.py | 149 ++++++++++++- lib/crewai/tests/tools/test_base_tool.py | 203 ++++++++++++++++++ .../tests/utilities/test_agent_utils.py | 54 +++++ 7 files changed, 472 insertions(+), 16 deletions(-) diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py index df51807f7..56abaae02 100644 --- a/lib/crewai/src/crewai/agents/crew_agent_executor.py +++ b/lib/crewai/src/crewai/agents/crew_agent_executor.py @@ -50,6 +50,7 @@ from crewai.utilities.agent_utils import ( handle_unknown_error, has_reached_max_iterations, is_context_length_exceeded, + parse_tool_call_args, process_llm_response, track_delegation_if_needed, ) @@ -894,13 +895,9 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): ToolUsageStartedEvent, ) - if isinstance(func_args, str): - try: - args_dict = json.loads(func_args) - except json.JSONDecodeError: - args_dict = {} - else: - args_dict = func_args + args_dict, parse_error = parse_tool_call_args(func_args, func_name, call_id, original_tool) + if parse_error is not None: + return parse_error if original_tool is None: for tool in self.original_tools or []: diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py index 56c4da030..e568dc0d4 100644 --- a/lib/crewai/src/crewai/experimental/agent_executor.py +++ b/lib/crewai/src/crewai/experimental/agent_executor.py @@ -66,6 +66,7 @@ from crewai.utilities.agent_utils import ( has_reached_max_iterations, is_context_length_exceeded, is_inside_event_loop, + parse_tool_call_args, process_llm_response, track_delegation_if_needed, ) @@ -848,13 +849,9 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin): call_id, func_name, func_args = info # Parse arguments - if isinstance(func_args, str): - try: - args_dict = json.loads(func_args) - except json.JSONDecodeError: - args_dict = {} - else: - args_dict = func_args + args_dict, parse_error = parse_tool_call_args(func_args, func_name, call_id) + if parse_error is not None: + return parse_error # Get agent_key for event tracking agent_key = getattr(self.agent, "key", "unknown") if self.agent else "unknown" diff --git a/lib/crewai/src/crewai/tools/base_tool.py b/lib/crewai/src/crewai/tools/base_tool.py index 8a10cdfa3..88c0826a9 100644 --- a/lib/crewai/src/crewai/tools/base_tool.py +++ b/lib/crewai/src/crewai/tools/base_tool.py @@ -18,6 +18,7 @@ from pydantic import ( BaseModel as PydanticBaseModel, ConfigDict, Field, + ValidationError, create_model, field_validator, ) @@ -150,14 +151,37 @@ class BaseTool(BaseModel, ABC): super().model_post_init(__context) + def _validate_kwargs(self, kwargs: dict[str, Any]) -> dict[str, Any]: + """Validate keyword arguments against args_schema if present. + + Args: + kwargs: The keyword arguments to validate. + + Returns: + Validated (and possibly coerced) keyword arguments. + + Raises: + ValueError: If validation against args_schema fails. + """ + if kwargs and self.args_schema is not None and self.args_schema.model_fields: + try: + validated = self.args_schema.model_validate(kwargs) + return validated.model_dump() + except Exception as e: + raise ValueError( + f"Tool '{self.name}' arguments validation failed: {e}" + ) from e + return kwargs + def run( self, *args: Any, **kwargs: Any, ) -> Any: + kwargs = self._validate_kwargs(kwargs) + result = self._run(*args, **kwargs) - # If _run is async, we safely run it if asyncio.iscoroutine(result): result = asyncio.run(result) @@ -179,6 +203,7 @@ class BaseTool(BaseModel, ABC): Returns: The result of the tool execution. """ + kwargs = self._validate_kwargs(kwargs) result = await self._arun(*args, **kwargs) self.current_usage_count += 1 return result @@ -331,6 +356,8 @@ class Tool(BaseTool, Generic[P, R]): Returns: The result of the tool execution. """ + kwargs = self._validate_kwargs(kwargs) + result = self.func(*args, **kwargs) if asyncio.iscoroutine(result): @@ -361,6 +388,7 @@ class Tool(BaseTool, Generic[P, R]): Returns: The result of the tool execution. """ + kwargs = self._validate_kwargs(kwargs) result = await self._arun(*args, **kwargs) self.current_usage_count += 1 return result diff --git a/lib/crewai/src/crewai/utilities/agent_utils.py b/lib/crewai/src/crewai/utilities/agent_utils.py index 80c80dbb6..7cad2ad67 100644 --- a/lib/crewai/src/crewai/utilities/agent_utils.py +++ b/lib/crewai/src/crewai/utilities/agent_utils.py @@ -1146,6 +1146,36 @@ def extract_tool_call_info( return None +def parse_tool_call_args( + func_args: dict[str, Any] | str, + func_name: str, + call_id: str, + original_tool: Any = None, +) -> tuple[dict[str, Any], None] | tuple[None, dict[str, Any]]: + """Parse tool call arguments from a JSON string or dict. + + Returns: + ``(args_dict, None)`` on success, or ``(None, error_result)`` on + JSON parse failure where ``error_result`` is a ready-to-return dict + with the same shape as ``_execute_single_native_tool_call`` return values. + """ + if isinstance(func_args, str): + try: + return json.loads(func_args), None + except json.JSONDecodeError as e: + return None, { + "call_id": call_id, + "func_name": func_name, + "result": ( + f"Error: Failed to parse tool arguments as JSON: {e}. " + f"Please provide valid JSON arguments for the '{func_name}' tool." + ), + "from_cache": False, + "original_tool": original_tool, + } + return func_args, None + + def _setup_before_llm_call_hooks( executor_context: CrewAgentExecutor | AgentExecutor | LiteAgent | None, printer: Printer, diff --git a/lib/crewai/tests/agents/test_native_tool_calling.py b/lib/crewai/tests/agents/test_native_tool_calling.py index 26b0a8e4a..558c34bb1 100644 --- a/lib/crewai/tests/agents/test_native_tool_calling.py +++ b/lib/crewai/tests/agents/test_native_tool_calling.py @@ -11,7 +11,7 @@ import os import threading import time from collections import Counter -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest from pydantic import BaseModel, Field @@ -1129,3 +1129,150 @@ class TestMaxUsageCountWithNativeToolCalling: # Verify the requested calls occurred while keeping usage bounded. assert tool.current_usage_count >= 2 assert tool.current_usage_count <= tool.max_usage_count + + +# ============================================================================= +# JSON Parse Error Handling Tests +# ============================================================================= + + +class TestNativeToolCallingJsonParseError: + """Tests that malformed JSON tool arguments produce clear errors + instead of silently dropping all arguments.""" + + def _make_executor(self, tools: list[BaseTool]) -> "CrewAgentExecutor": + """Create a minimal CrewAgentExecutor with mocked dependencies.""" + from crewai.agents.crew_agent_executor import CrewAgentExecutor + from crewai.tools.base_tool import to_langchain + + structured_tools = to_langchain(tools) + mock_agent = Mock() + mock_agent.key = "test_agent" + mock_agent.role = "tester" + mock_agent.verbose = False + mock_agent.fingerprint = None + mock_agent.tools_results = [] + + mock_task = Mock() + mock_task.name = "test" + mock_task.description = "test" + mock_task.id = "test-id" + + executor = object.__new__(CrewAgentExecutor) + executor.agent = mock_agent + executor.task = mock_task + executor.crew = Mock() + executor.tools = structured_tools + executor.original_tools = tools + executor.tools_handler = None + executor._printer = Mock() + executor.messages = [] + + return executor + + def test_malformed_json_returns_parse_error(self) -> None: + """Malformed JSON args must return a descriptive error, not silently become {}.""" + + class CodeTool(BaseTool): + name: str = "execute_code" + description: str = "Run code" + + def _run(self, code: str) -> str: + return f"ran: {code}" + + tool = CodeTool() + executor = self._make_executor([tool]) + + from crewai.utilities.agent_utils import convert_tools_to_openai_schema + _, available_functions = convert_tools_to_openai_schema([tool]) + + malformed_json = '{"code": "print("hello")"}' + + result = executor._execute_single_native_tool_call( + call_id="call_123", + func_name="execute_code", + func_args=malformed_json, + available_functions=available_functions, + ) + + assert "Failed to parse tool arguments as JSON" in result["result"] + assert tool.current_usage_count == 0 + + def test_valid_json_still_executes_normally(self) -> None: + """Valid JSON args should execute the tool as before.""" + + class CodeTool(BaseTool): + name: str = "execute_code" + description: str = "Run code" + + def _run(self, code: str) -> str: + return f"ran: {code}" + + tool = CodeTool() + executor = self._make_executor([tool]) + + from crewai.utilities.agent_utils import convert_tools_to_openai_schema + _, available_functions = convert_tools_to_openai_schema([tool]) + + valid_json = '{"code": "print(1)"}' + + result = executor._execute_single_native_tool_call( + call_id="call_456", + func_name="execute_code", + func_args=valid_json, + available_functions=available_functions, + ) + + assert result["result"] == "ran: print(1)" + + def test_dict_args_bypass_json_parsing(self) -> None: + """When func_args is already a dict, no JSON parsing occurs.""" + + class CodeTool(BaseTool): + name: str = "execute_code" + description: str = "Run code" + + def _run(self, code: str) -> str: + return f"ran: {code}" + + tool = CodeTool() + executor = self._make_executor([tool]) + + from crewai.utilities.agent_utils import convert_tools_to_openai_schema + _, available_functions = convert_tools_to_openai_schema([tool]) + + result = executor._execute_single_native_tool_call( + call_id="call_789", + func_name="execute_code", + func_args={"code": "x = 42"}, + available_functions=available_functions, + ) + + assert result["result"] == "ran: x = 42" + + def test_schema_validation_catches_missing_args_on_native_path(self) -> None: + """The native function calling path should now enforce args_schema, + catching missing required fields before _run is called.""" + + class StrictTool(BaseTool): + name: str = "strict_tool" + description: str = "A tool with required args" + + def _run(self, code: str, language: str) -> str: + return f"{language}: {code}" + + tool = StrictTool() + executor = self._make_executor([tool]) + + from crewai.utilities.agent_utils import convert_tools_to_openai_schema + _, available_functions = convert_tools_to_openai_schema([tool]) + + result = executor._execute_single_native_tool_call( + call_id="call_schema", + func_name="strict_tool", + func_args={"code": "print(1)"}, + available_functions=available_functions, + ) + + assert "Error" in result["result"] + assert "validation failed" in result["result"].lower() or "missing" in result["result"].lower() diff --git a/lib/crewai/tests/tools/test_base_tool.py b/lib/crewai/tests/tools/test_base_tool.py index 4a6850ce1..a9d3a2b6d 100644 --- a/lib/crewai/tests/tools/test_base_tool.py +++ b/lib/crewai/tests/tools/test_base_tool.py @@ -3,6 +3,8 @@ from typing import Callable from unittest.mock import patch import pytest +from pydantic import BaseModel, Field + from crewai.agent import Agent from crewai.crew import Crew from crewai.task import Task @@ -230,3 +232,204 @@ def test_max_usage_count_is_respected(): crew.kickoff() assert tool.max_usage_count == 5 assert tool.current_usage_count == 5 + + +# ============================================================================= +# Schema Validation in run() Tests +# ============================================================================= + + +class CodeExecutorInput(BaseModel): + code: str = Field(description="The code to execute") + language: str = Field(default="python", description="Programming language") + + +class CodeExecutorTool(BaseTool): + name: str = "code_executor" + description: str = "Execute code snippets" + args_schema: type[BaseModel] = CodeExecutorInput + + def _run(self, code: str, language: str = "python") -> str: + return f"Executed {language}: {code}" + + +class TestBaseToolRunValidation: + """Tests for args_schema validation in BaseTool.run().""" + + def test_run_with_valid_kwargs_passes_validation(self) -> None: + """Valid keyword arguments should pass schema validation and execute.""" + t = CodeExecutorTool() + result = t.run(code="print('hello')") + assert result == "Executed python: print('hello')" + + def test_run_with_all_kwargs_passes_validation(self) -> None: + """All keyword arguments including optional ones should pass.""" + t = CodeExecutorTool() + result = t.run(code="console.log('hi')", language="javascript") + assert result == "Executed javascript: console.log('hi')" + + def test_run_with_missing_required_kwarg_raises(self) -> None: + """Missing required kwargs should raise ValueError from schema validation.""" + t = CodeExecutorTool() + with pytest.raises(ValueError, match="validation failed"): + t.run(language="python") + + def test_run_with_wrong_field_name_raises(self) -> None: + """Kwargs not matching any schema field should trigger validation error + for missing required fields.""" + t = CodeExecutorTool() + with pytest.raises(ValueError, match="validation failed"): + t.run(wrong_arg="value") + + def test_run_with_positional_args_skips_validation(self) -> None: + """Positional-arg calls should bypass schema validation (backwards compat).""" + class SimpleTool(BaseTool): + name: str = "simple" + description: str = "A simple tool" + + def _run(self, question: str) -> str: + return question + + t = SimpleTool() + result = t.run("What is life?") + assert result == "What is life?" + + def test_run_strips_extra_kwargs_from_llm(self) -> None: + """Extra kwargs not in the schema should be silently stripped, + preventing unexpected-keyword crashes in _run.""" + t = CodeExecutorTool() + result = t.run(code="1+1", extra_hallucinated_field="junk") + assert result == "Executed python: 1+1" + + def test_run_increments_usage_after_validation(self) -> None: + """Usage count should still increment after validated execution.""" + t = CodeExecutorTool() + assert t.current_usage_count == 0 + t.run(code="x = 1") + assert t.current_usage_count == 1 + + def test_run_does_not_increment_usage_on_validation_error(self) -> None: + """Usage count should NOT increment when validation fails.""" + t = CodeExecutorTool() + assert t.current_usage_count == 0 + with pytest.raises(ValueError): + t.run(wrong="bad") + assert t.current_usage_count == 0 + + +class TestToolDecoratorRunValidation: + """Tests for args_schema validation in Tool.run() (decorator-based tools).""" + + def test_decorator_tool_run_validates_kwargs(self) -> None: + """Decorator-created tools should also validate kwargs against schema.""" + @tool("execute_code") + def execute_code(code: str, language: str = "python") -> str: + """Execute a code snippet.""" + return f"Executed {language}: {code}" + + result = execute_code.run(code="x = 1") + assert result == "Executed python: x = 1" + + def test_decorator_tool_run_rejects_missing_required(self) -> None: + """Decorator tools should reject missing required args via validation.""" + @tool("execute_code") + def execute_code(code: str) -> str: + """Execute a code snippet.""" + return f"Executed: {code}" + + with pytest.raises(ValueError, match="validation failed"): + execute_code.run(wrong_arg="value") + + def test_decorator_tool_positional_args_still_work(self) -> None: + """Positional args to decorator tools should bypass validation.""" + @tool("greet") + def greet(name: str) -> str: + """Greet someone.""" + return f"Hello, {name}!" + + result = greet.run("World") + assert result == "Hello, World!" + + +# ============================================================================= +# Async arun() Schema Validation Tests +# ============================================================================= + + +class AsyncCodeExecutorTool(BaseTool): + name: str = "async_code_executor" + description: str = "Execute code snippets asynchronously" + args_schema: type[BaseModel] = CodeExecutorInput + + async def _arun(self, code: str, language: str = "python") -> str: + return f"Async executed {language}: {code}" + + def _run(self, code: str, language: str = "python") -> str: + return f"Executed {language}: {code}" + + +class TestBaseToolArunValidation: + """Tests for args_schema validation in BaseTool.arun().""" + + @pytest.mark.asyncio + async def test_arun_with_valid_kwargs_passes_validation(self) -> None: + """Valid keyword arguments should pass schema validation in arun.""" + t = AsyncCodeExecutorTool() + result = await t.arun(code="print('hello')") + assert result == "Async executed python: print('hello')" + + @pytest.mark.asyncio + async def test_arun_with_missing_required_kwarg_raises(self) -> None: + """Missing required kwargs should raise ValueError in arun.""" + t = AsyncCodeExecutorTool() + with pytest.raises(ValueError, match="validation failed"): + await t.arun(language="python") + + @pytest.mark.asyncio + async def test_arun_with_wrong_field_name_raises(self) -> None: + """Kwargs not matching schema fields should trigger validation error in arun.""" + t = AsyncCodeExecutorTool() + with pytest.raises(ValueError, match="validation failed"): + await t.arun(wrong_arg="value") + + @pytest.mark.asyncio + async def test_arun_strips_extra_kwargs(self) -> None: + """Extra kwargs not in the schema should be stripped in arun.""" + t = AsyncCodeExecutorTool() + result = await t.arun(code="1+1", extra_field="junk") + assert result == "Async executed python: 1+1" + + @pytest.mark.asyncio + async def test_arun_does_not_increment_usage_on_validation_error(self) -> None: + """Usage count should NOT increment when arun validation fails.""" + t = AsyncCodeExecutorTool() + assert t.current_usage_count == 0 + with pytest.raises(ValueError): + await t.arun(wrong="bad") + assert t.current_usage_count == 0 + + +class TestToolDecoratorArunValidation: + """Tests for args_schema validation in Tool.arun() (decorator-based async tools).""" + + @pytest.mark.asyncio + async def test_async_decorator_tool_arun_validates_kwargs(self) -> None: + """Async decorator tools should validate kwargs in arun.""" + @tool("async_execute") + async def async_execute(code: str, language: str = "python") -> str: + """Execute code asynchronously.""" + return f"Async {language}: {code}" + + result = await async_execute.arun(code="x = 1") + assert result == "Async python: x = 1" + + @pytest.mark.asyncio + async def test_async_decorator_tool_arun_rejects_missing_required(self) -> None: + """Async decorator tools should reject missing required args in arun.""" + @tool("async_execute") + async def async_execute(code: str) -> str: + """Execute code asynchronously.""" + return f"Async: {code}" + + with pytest.raises(ValueError, match="validation failed"): + await async_execute.arun(wrong_arg="value") diff --git a/lib/crewai/tests/utilities/test_agent_utils.py b/lib/crewai/tests/utilities/test_agent_utils.py index 31d7b9705..8e3093219 100644 --- a/lib/crewai/tests/utilities/test_agent_utils.py +++ b/lib/crewai/tests/utilities/test_agent_utils.py @@ -17,6 +17,7 @@ from crewai.utilities.agent_utils import ( _format_messages_for_summary, _split_messages_into_chunks, convert_tools_to_openai_schema, + parse_tool_call_args, summarize_messages, ) @@ -922,3 +923,56 @@ class TestParallelSummarizationVCR: assert summary_msg["role"] == "user" assert "files" in summary_msg assert "report.pdf" in summary_msg["files"] + + +class TestParseToolCallArgs: + """Unit tests for parse_tool_call_args.""" + + def test_valid_json_string_returns_dict(self) -> None: + args_dict, error = parse_tool_call_args('{"code": "print(1)"}', "run_code", "call_1") + assert error is None + assert args_dict == {"code": "print(1)"} + + def test_malformed_json_returns_error_dict(self) -> None: + args_dict, error = parse_tool_call_args('{"code": "print("hi")"}', "run_code", "call_1") + assert args_dict is None + assert error is not None + assert error["call_id"] == "call_1" + assert error["func_name"] == "run_code" + assert error["from_cache"] is False + assert "Failed to parse tool arguments as JSON" in error["result"] + assert "run_code" in error["result"] + + def test_malformed_json_preserves_original_tool(self) -> None: + mock_tool = object() + _, error = parse_tool_call_args("{bad}", "my_tool", "call_2", original_tool=mock_tool) + assert error is not None + assert error["original_tool"] is mock_tool + + def test_malformed_json_original_tool_defaults_to_none(self) -> None: + _, error = parse_tool_call_args("{bad}", "my_tool", "call_3") + assert error is not None + assert error["original_tool"] is None + + def test_dict_input_returned_directly(self) -> None: + func_args = {"code": "x = 42"} + args_dict, error = parse_tool_call_args(func_args, "run_code", "call_4") + assert error is None + assert args_dict == {"code": "x = 42"} + + def test_empty_dict_input_returned_directly(self) -> None: + args_dict, error = parse_tool_call_args({}, "run_code", "call_5") + assert error is None + assert args_dict == {} + + def test_valid_json_with_nested_values(self) -> None: + args_dict, error = parse_tool_call_args( + '{"query": "hello", "options": {"limit": 10}}', "search", "call_6" + ) + assert error is None + assert args_dict == {"query": "hello", "options": {"limit": 10}} + + def test_error_result_has_correct_keys(self) -> None: + _, error = parse_tool_call_args("{bad json}", "tool", "call_7") + assert error is not None + assert set(error.keys()) == {"call_id", "func_name", "result", "from_cache", "original_tool"} From 02d911494f5fd9b4c7e2691a88fdd0dc123a8a36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:04:07 -0600 Subject: [PATCH 3/4] chore(deps): bump cryptography (#4506) Bumps the security-updates group with 1 update in the / directory: [cryptography](https://github.com/pyca/cryptography). Updates `cryptography` from 46.0.4 to 46.0.5 - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/46.0.4...46.0.5) --- updated-dependencies: - dependency-name: cryptography dependency-version: 46.0.5 dependency-type: indirect dependency-group: security-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 72 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/uv.lock b/uv.lock index f49801c32..82f29a0be 100644 --- a/uv.lock +++ b/uv.lock @@ -1471,48 +1471,48 @@ provides-extras = ["apify", "beautifulsoup4", "bedrock", "browserbase", "composi [[package]] name = "cryptography" -version = "46.0.4" +version = "46.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" }, - { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" }, - { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" }, - { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" }, - { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" }, - { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" }, - { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" }, - { url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" }, - { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" }, - { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" }, - { url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" }, - { url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" }, - { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" }, - { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" }, - { url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" }, - { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" }, - { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" }, - { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" }, - { url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" }, - { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" }, - { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" }, - { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" }, - { url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" }, - { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" }, - { url = "https://files.pythonhosted.org/packages/59/e0/f9c6c53e1f2a1c2507f00f2faba00f01d2f334b35b0fbfe5286715da2184/cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", size = 3476316, upload-time = "2026-01-28T00:24:24.144Z" }, - { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" }, - { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" }, - { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" }, - { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" }, - { url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" }, + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, ] [[package]] From 017189db7829d93963842d35dd55a7b6858fc1de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:37:21 -0600 Subject: [PATCH 4/4] chore(deps): bump nltk in the security-updates group across 1 directory (#4598) Bumps the security-updates group with 1 update in the / directory: [nltk](https://github.com/nltk/nltk). Updates `nltk` from 3.9.2 to 3.9.3 - [Changelog](https://github.com/nltk/nltk/blob/develop/ChangeLog) - [Commits](https://github.com/nltk/nltk/compare/3.9.2...3.9.3) --- updated-dependencies: - dependency-name: nltk dependency-version: 3.9.3 dependency-type: indirect dependency-group: security-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 82f29a0be..ea4af006f 100644 --- a/uv.lock +++ b/uv.lock @@ -4195,7 +4195,7 @@ wheels = [ [[package]] name = "nltk" -version = "3.9.2" +version = "3.9.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -4203,9 +4203,9 @@ dependencies = [ { name = "regex" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/76/3a5e4312c19a028770f86fd7c058cf9f4ec4321c6cf7526bab998a5b683c/nltk-3.9.2.tar.gz", hash = "sha256:0f409e9b069ca4177c1903c3e843eef90c7e92992fa4931ae607da6de49e1419", size = 2887629, upload-time = "2025-10-01T07:19:23.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/8f/915e1c12df07c70ed779d18ab83d065718a926e70d3ea33eb0cd66ffb7c0/nltk-3.9.3.tar.gz", hash = "sha256:cb5945d6424a98d694c2b9a0264519fab4363711065a46aa0ae7a2195b92e71f", size = 2923673, upload-time = "2026-02-24T12:05:53.833Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/90/81ac364ef94209c100e12579629dc92bf7a709a84af32f8c551b02c07e94/nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a", size = 1513404, upload-time = "2025-10-01T07:19:21.648Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7e/9af5a710a1236e4772de8dfcc6af942a561327bb9f42b5b4a24d0cf100fd/nltk-3.9.3-py3-none-any.whl", hash = "sha256:60b3db6e9995b3dd976b1f0fa7dec22069b2677e759c28eb69b62ddd44870522", size = 1525385, upload-time = "2026-02-24T12:05:46.54Z" }, ] [[package]]