From 504c5c9b0453583eaf89b4e22b7738c12099b86b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moura?= Date: Thu, 18 Jun 2026 14:14:54 -0300 Subject: [PATCH] JSON crew fixes (#6217) * feat: update pyproject.toml to specify wheel targets Added a new section to the pyproject.toml file to include only specific files in the wheel build, enhancing the packaging process. Updated tests to verify the inclusion of these targets. * feat: add memory save event handling to activity log Implemented event handlers for MemorySaveStartedEvent, MemorySaveCompletedEvent, and MemorySaveFailedEvent in the crew_run_tui module. This allows the application to log memory save operations, capturing their status and details in the activity log. Added corresponding tests to verify the correct logging behavior for successful and failed memory saves. * feat: enhance memory save event handling in activity log Added functionality to suppress nested memory save events and updated the handling of MemorySaveStartedEvent, MemorySaveCompletedEvent, and MemorySaveFailedEvent to improve logging accuracy. Introduced new tests to verify the correct behavior of memory save events, including scenarios for nested events and completion updates for timed-out entries. * Fix memory save activity log handling * Normalize alpha package versions * Update scaffolded crew dependency * feat: add button to copy setup instructions for CrewAI coding agents Introduced a button in the documentation that allows users to easily copy setup instructions for CrewAI coding agents. The instructions include installation steps, environment setup, and best practices for using the CrewAI CLI. This enhancement aims to streamline the onboarding process for new users. * Improve missing CrewAI install guidance * fix: address pr review feedback * fix: avoid mismatched memory save rows * fix: wait for queued memory save events * fix: avoid matching memory saves on missing ids * chore: normalize prerelease version to 1.14.8a1 --- docs/edge/en/index.mdx | 54 ++ docs/edge/en/installation.mdx | 55 +- lib/cli/pyproject.toml | 2 +- lib/cli/src/crewai_cli/__init__.py | 2 +- lib/cli/src/crewai_cli/create_json_crew.py | 5 +- lib/cli/src/crewai_cli/crew_run_tui.py | 250 +++++++- lib/cli/src/crewai_cli/run_crew.py | 46 +- .../crewai_cli/templates/crew/pyproject.toml | 2 +- .../crewai_cli/templates/flow/pyproject.toml | 2 +- .../crewai_cli/templates/tool/pyproject.toml | 2 +- lib/cli/tests/test_create_crew.py | 11 + lib/cli/tests/test_crew_run_tui.py | 583 +++++++++++++++++- lib/cli/tests/test_run_crew.py | 21 + lib/crewai-core/src/crewai_core/__init__.py | 2 +- lib/crewai-files/pyproject.toml | 4 +- 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 | 6 +- lib/crewai/src/crewai/__init__.py | 2 +- .../src/crewai/memory/unified_memory.py | 24 +- .../tests/memory/test_unified_memory.py | 33 + lib/devtools/src/crewai_devtools/__init__.py | 2 +- pyproject.toml | 7 +- uv.lock | 13 +- 25 files changed, 1101 insertions(+), 33 deletions(-) diff --git a/docs/edge/en/index.mdx b/docs/edge/en/index.mdx index ff6466f27..d091196e5 100644 --- a/docs/edge/en/index.mdx +++ b/docs/edge/en/index.mdx @@ -28,6 +28,60 @@ mode: "wide"
Get started + View changelog API Reference
diff --git a/docs/edge/en/installation.mdx b/docs/edge/en/installation.mdx index bd277790a..66f80d248 100644 --- a/docs/edge/en/installation.mdx +++ b/docs/edge/en/installation.mdx @@ -9,7 +9,60 @@ mode: "wide" Install our coding agent skills (Claude Code, Codex, ...) to quickly get your coding agents up and running with CrewAI. -You can install it with `npx skills add crewaiinc/skills` + diff --git a/lib/cli/pyproject.toml b/lib/cli/pyproject.toml index e2e50af45..d49abe56a 100644 --- a/lib/cli/pyproject.toml +++ b/lib/cli/pyproject.toml @@ -8,7 +8,7 @@ authors = [ ] requires-python = ">=3.10, <3.14" dependencies = [ - "crewai-core==1.14.8a", + "crewai-core==1.14.8a1", "click>=8.1.7,<9", "pydantic>=2.11.9,<2.13", "pydantic-settings~=2.10.1", diff --git a/lib/cli/src/crewai_cli/__init__.py b/lib/cli/src/crewai_cli/__init__.py index 4e3686826..cb6f48866 100644 --- a/lib/cli/src/crewai_cli/__init__.py +++ b/lib/cli/src/crewai_cli/__init__.py @@ -1 +1 @@ -__version__ = "1.14.8a" +__version__ = "1.14.8a1" diff --git a/lib/cli/src/crewai_cli/create_json_crew.py b/lib/cli/src/crewai_cli/create_json_crew.py index cf84a7f1b..1df9f0310 100644 --- a/lib/cli/src/crewai_cli/create_json_crew.py +++ b/lib/cli/src/crewai_cli/create_json_crew.py @@ -89,13 +89,16 @@ description = "{name} using crewAI" authors = [{{ name = "Your Name", email = "you@example.com" }}] requires-python = ">=3.10,<3.14" dependencies = [ - "crewai[tools]>=1.14.7" + "crewai[tools]==1.14.8a1" ] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" +[tool.hatch.build.targets.wheel] +only-include = ["agents", "crew.jsonc", "tools", "knowledge", "skills"] + [tool.crewai] type = "crew" """ diff --git a/lib/cli/src/crewai_cli/crew_run_tui.py b/lib/cli/src/crewai_cli/crew_run_tui.py index ce60c2e93..9b3930350 100644 --- a/lib/cli/src/crewai_cli/crew_run_tui.py +++ b/lib/cli/src/crewai_cli/crew_run_tui.py @@ -34,6 +34,25 @@ _C_MUTED = "#666666" # dimmer than _C_DIM for past timeline _STEP_NUMBER_RE = re.compile(r"\bstep\s+(\d+)\b", re.IGNORECASE) _REFINEMENT_RE = re.compile(r"^\s*step\s+(\d+)\s*:\s*(.+)\s*$", re.IGNORECASE) _INTERNAL_TOOL_NAMES = {"create_reasoning_plan"} +_LOG_ARGS_TEXT_LIMIT = 3_000 +_LOG_RESULT_TEXT_LIMIT = 5_000 +_LOG_TRUNCATION_SUFFIX = "... [truncated]" +# Background memory saves can emit their start event just after kickoff returns. +_MEMORY_SAVE_DRAIN_GRACE_SECONDS = 2.0 + + +def _is_save_to_memory_tool(tool_name: str | None) -> bool: + return (tool_name or "").replace(" ", "_").lower() == "save_to_memory" + + +def _truncate_log_text(value: Any, limit: int) -> str | None: + if value is None: + return None + text = str(value) + if len(text) <= limit: + return text + suffix = _LOG_TRUNCATION_SUFFIX + return f"{text[: max(0, limit - len(suffix))]}{suffix}" def _enable_tracing_in_dotenv() -> None: @@ -519,6 +538,8 @@ FooterKey .footer-key--key { self._log_expanded: set[int] = set() self._log_scroll_needed: bool = False self._log_line_map: list[tuple[int, int, int]] = [] + self._suppressed_memory_save_event_ids: set[str] = set() + self._memory_save_drain_timer: Any = None self._event_handlers: list[tuple[type, Any]] = [] @@ -633,7 +654,6 @@ FooterKey .footer-key--key { self.call_from_thread(self._on_crew_failed, str(e)) def _on_crew_done(self, output: str | None) -> None: - self._unsubscribe() with self._lock: self._status = "completed" self._final_output = output @@ -649,6 +669,8 @@ FooterKey .footer-key--key { now = time.time() for entry in self._log_entries: if entry["status"] == "running": + if entry["tool_name"] == "memory_save": + continue entry["status"] = "timeout" entry["error"] = "No result received before crew completed" entry["duration"] = now - entry["start_time"] @@ -680,9 +702,9 @@ FooterKey .footer-key--key { self.call_later(self._focus_activity_log) self._tick_timer.stop() self._tick_timer = self.set_interval(1 / 2, self._tick) + self._unsubscribe_if_no_running_memory_save(wait_for_queued=True) def _on_crew_failed(self, error: str) -> None: - self._unsubscribe() with self._lock: self._status = "failed" self._error = error @@ -692,12 +714,16 @@ FooterKey .footer-key--key { now = time.time() for entry in self._log_entries: if entry["status"] == "running": + if entry["tool_name"] == "memory_save": + continue entry["status"] = "error" + entry["error"] = "No result received before crew failed" entry["duration"] = now - entry["start_time"] self._tick() self.call_later(self._focus_activity_log) self._tick_timer.stop() self._tick_timer = self.set_interval(1 / 2, self._tick) + self._unsubscribe_if_no_running_memory_save(wait_for_queued=True) # ── Actions ───────────────────────────────────────────── @@ -1514,6 +1540,53 @@ FooterKey .footer-key--key { pass self._event_handlers.clear() + def _has_running_memory_save_locked(self) -> bool: + return any( + entry["tool_name"] == "memory_save" and entry["status"] == "running" + for entry in self._log_entries + ) + + def _on_memory_save_drain_elapsed(self) -> None: + self._memory_save_drain_timer = None + self._unsubscribe_if_no_running_memory_save() + + def _schedule_memory_save_drain_unsubscribe(self) -> bool: + loop = getattr(self, "_loop", None) + if loop is None: + return False + if getattr(self, "_thread_id", None) != threading.get_ident(): + try: + loop.call_soon_threadsafe(self._schedule_memory_save_drain_unsubscribe) + except RuntimeError: + return False + return True + if self._memory_save_drain_timer is not None: + self._memory_save_drain_timer.stop() + self._memory_save_drain_timer = self.set_timer( + _MEMORY_SAVE_DRAIN_GRACE_SECONDS, + self._on_memory_save_drain_elapsed, + name="memory-save-drain", + ) + return True + + def _unsubscribe_if_no_running_memory_save( + self, *, wait_for_queued: bool = False + ) -> None: + with self._lock: + should_unsubscribe = ( + self._status + in { + "completed", + "failed", + } + and not self._has_running_memory_save_locked() + ) + + if should_unsubscribe: + if wait_for_queued and self._schedule_memory_save_drain_unsubscribe(): + return + self._unsubscribe() + def _subscribe(self) -> None: from crewai.events.event_bus import crewai_event_bus from crewai.events.types.crew_events import CrewKickoffStartedEvent @@ -1802,6 +1875,8 @@ FooterKey .footer-key--key { entry["status"] == "running" and entry["tool_name"] != event.tool_name ): + if entry["tool_name"] == "memory_save": + continue entry["status"] = "timeout" entry["error"] = ( "No result received before the next tool started" @@ -1830,6 +1905,7 @@ FooterKey .footer-key--key { "duration": None, "task_idx": self._current_task_idx, "plan_step_number": plan_step_number, + "event_id": event.event_id, } ) self._complete_step("teal", f"⚡ {event.tool_name}…") @@ -1923,8 +1999,178 @@ FooterKey .footer-key--key { MemoryRetrievalCompletedEvent, MemoryRetrievalFailedEvent, MemoryRetrievalStartedEvent, + MemorySaveCompletedEvent, + MemorySaveFailedEvent, + MemorySaveStartedEvent, ) + def is_nested_save_to_memory_event(event: Any) -> bool: + if event.parent_event_id is None: + return False + state = crewai_event_bus.runtime_state + if state is None: + return False + parent_node = state.event_record.nodes.get(event.parent_event_id) + parent_event = getattr(parent_node, "event", None) + return getattr( + parent_event, "type", None + ) == "tool_usage_started" and _is_save_to_memory_tool( + getattr(parent_event, "tool_name", None) + ) + + @crewai_event_bus.on(MemorySaveStartedEvent) + def on_memory_save_started(source: Any, event: MemorySaveStartedEvent) -> None: + with self._lock: + if is_nested_save_to_memory_event(event): + self._suppressed_memory_save_event_ids.add(event.event_id) + return + for entry in reversed(self._log_entries): + if ( + _is_save_to_memory_tool(entry["tool_name"]) + and entry.get("event_id") == event.parent_event_id + ): + self._suppressed_memory_save_event_ids.add(event.event_id) + return + for entry in reversed(self._log_entries): + if ( + entry["tool_name"] == "memory_save" + and entry.get("started_event_id") == event.event_id + ): + entry["args"] = _truncate_log_text( + event.value, _LOG_ARGS_TEXT_LIMIT + ) + return + self._log_entries.append( + { + "tool_name": "memory_save", + "status": "running", + "args": _truncate_log_text(event.value, _LOG_ARGS_TEXT_LIMIT), + "result": None, + "error": None, + "start_time": time.time(), + "duration": None, + "task_idx": self._current_task_idx, + "event_id": event.event_id, + } + ) + + self._register_handler(MemorySaveStartedEvent, on_memory_save_started) + + @crewai_event_bus.on(MemorySaveCompletedEvent) + def on_memory_save_completed( + source: Any, event: MemorySaveCompletedEvent + ) -> None: + with self._lock: + if ( + event.started_event_id in self._suppressed_memory_save_event_ids + or is_nested_save_to_memory_event(event) + ): + if event.started_event_id is not None: + self._suppressed_memory_save_event_ids.discard( + event.started_event_id + ) + else: + for entry in reversed(self._log_entries): + has_started_event_match = ( + event.started_event_id is not None + and ( + entry.get("event_id") == event.started_event_id + or entry.get("started_event_id") + == event.started_event_id + ) + ) + has_running_event_without_id = ( + event.started_event_id is None + and entry["status"] == "running" + ) + if entry["tool_name"] == "memory_save" and ( + has_running_event_without_id or has_started_event_match + ): + entry["status"] = "success" + entry["duration"] = event.save_time_ms / 1000 + entry["result"] = _truncate_log_text( + event.value, _LOG_RESULT_TEXT_LIMIT + ) + entry["error"] = None + entry["started_event_id"] = event.started_event_id + break + else: + self._log_entries.append( + { + "tool_name": "memory_save", + "status": "success", + "args": None, + "result": _truncate_log_text( + event.value, _LOG_RESULT_TEXT_LIMIT + ), + "error": None, + "start_time": time.time(), + "duration": event.save_time_ms / 1000, + "task_idx": self._current_task_idx, + "started_event_id": event.started_event_id, + } + ) + + self._unsubscribe_if_no_running_memory_save(wait_for_queued=True) + + self._register_handler(MemorySaveCompletedEvent, on_memory_save_completed) + + @crewai_event_bus.on(MemorySaveFailedEvent) + def on_memory_save_failed(source: Any, event: MemorySaveFailedEvent) -> None: + with self._lock: + if ( + event.started_event_id in self._suppressed_memory_save_event_ids + or is_nested_save_to_memory_event(event) + ): + if event.started_event_id is not None: + self._suppressed_memory_save_event_ids.discard( + event.started_event_id + ) + else: + for idx, entry in reversed(list(enumerate(self._log_entries))): + has_started_event_match = ( + event.started_event_id is not None + and ( + entry.get("event_id") == event.started_event_id + or entry.get("started_event_id") + == event.started_event_id + ) + ) + has_running_event_without_id = ( + event.started_event_id is None + and entry["status"] == "running" + ) + if entry["tool_name"] == "memory_save" and ( + has_running_event_without_id or has_started_event_match + ): + entry["status"] = "error" + entry["error"] = event.error + entry["duration"] = time.time() - entry["start_time"] + entry["started_event_id"] = event.started_event_id + self._log_expanded.add(idx) + break + else: + self._log_entries.append( + { + "tool_name": "memory_save", + "status": "error", + "args": _truncate_log_text( + event.value, _LOG_ARGS_TEXT_LIMIT + ), + "result": None, + "error": event.error, + "start_time": time.time(), + "duration": 0, + "task_idx": self._current_task_idx, + "started_event_id": event.started_event_id, + } + ) + self._log_expanded.add(len(self._log_entries) - 1) + + self._unsubscribe_if_no_running_memory_save(wait_for_queued=True) + + self._register_handler(MemorySaveFailedEvent, on_memory_save_failed) + @crewai_event_bus.on(MemoryRetrievalStartedEvent) def on_memory_retrieval_started( source: Any, event: MemoryRetrievalStartedEvent diff --git a/lib/cli/src/crewai_cli/run_crew.py b/lib/cli/src/crewai_cli/run_crew.py index cbc14ff86..cbf2445d1 100644 --- a/lib/cli/src/crewai_cli/run_crew.py +++ b/lib/cli/src/crewai_cli/run_crew.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Callable from contextlib import AbstractContextManager, nullcontext from enum import Enum import os @@ -7,10 +8,9 @@ from pathlib import Path import re import subprocess import sys -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast import click -from crewai.project.json_loader import find_crew_json_file from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV from packaging import version @@ -38,6 +38,15 @@ class CrewType(Enum): _INPUT_PLACEHOLDER_RE = re.compile(r"(? Callable[[], Path | None]: + from crewai.project.json_loader import find_crew_json_file as _find_crew_json_file + + return cast("Callable[[], Path | None]", _find_crew_json_file) + + +def _is_missing_crewai_package(exc: ModuleNotFoundError) -> bool: + return bool(exc.name and exc.name.startswith("crewai")) + + +def _full_crewai_install_error() -> click.ClickException: + return click.ClickException(_FULL_CREWAI_INSTALL_MESSAGE) + + +def find_crew_json_file() -> Path | None: + try: + return _import_find_crew_json_file()() + except ModuleNotFoundError as exc: + if _is_missing_crewai_package(exc): + raise _full_crewai_install_error() from exc + raise + + def _has_json_crew() -> bool: """Check if this is a JSON-defined crew project. diff --git a/lib/cli/src/crewai_cli/templates/crew/pyproject.toml b/lib/cli/src/crewai_cli/templates/crew/pyproject.toml index d947fa4b2..222ad7141 100644 --- a/lib/cli/src/crewai_cli/templates/crew/pyproject.toml +++ b/lib/cli/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.8a" + "crewai[tools]==1.14.8a1" ] [project.scripts] diff --git a/lib/cli/src/crewai_cli/templates/flow/pyproject.toml b/lib/cli/src/crewai_cli/templates/flow/pyproject.toml index e63ee69ea..9767c17d9 100644 --- a/lib/cli/src/crewai_cli/templates/flow/pyproject.toml +++ b/lib/cli/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.8a" + "crewai[tools]==1.14.8a1" ] [project.scripts] diff --git a/lib/cli/src/crewai_cli/templates/tool/pyproject.toml b/lib/cli/src/crewai_cli/templates/tool/pyproject.toml index 95d8762c3..c25698a91 100644 --- a/lib/cli/src/crewai_cli/templates/tool/pyproject.toml +++ b/lib/cli/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.8a" + "crewai[tools]==1.14.8a1" ] [tool.crewai] diff --git a/lib/cli/tests/test_create_crew.py b/lib/cli/tests/test_create_crew.py index bb4bc68be..adfe1757f 100644 --- a/lib/cli/tests/test_create_crew.py +++ b/lib/cli/tests/test_create_crew.py @@ -5,7 +5,10 @@ from pathlib import Path from unittest import mock import pytest +import tomli from click.testing import CliRunner +from packaging.requirements import Requirement +from packaging.version import Version import crewai_cli.create_json_crew as json_crew import crewai_cli.tui_picker as tui_picker from crewai_cli.create_crew import create_crew, create_folder_structure @@ -712,6 +715,14 @@ def test_json_create_provider_preselects_default_model(tmp_path, monkeypatch): assert not (tmp_path / "json_crew" / "tests").exists() assert not (tmp_path / "json_crew" / "config.jsonc").exists() + pyproject = tomli.loads((tmp_path / "json_crew" / "pyproject.toml").read_text()) + dependency = pyproject["project"]["dependencies"][0] + assert dependency == "crewai[tools]==1.14.8a1" + assert Version("1.14.8a1") in Requirement(dependency).specifier + assert pyproject["tool"]["hatch"]["build"]["targets"]["wheel"][ + "only-include" + ] == ["agents", "crew.jsonc", "tools", "knowledge", "skills"] + crew_template = (tmp_path / "json_crew" / "crew.jsonc").read_text() assert ( '"guardrail": "Every factual claim needs context support."' diff --git a/lib/cli/tests/test_crew_run_tui.py b/lib/cli/tests/test_crew_run_tui.py index 7b018107a..969bc5ae2 100644 --- a/lib/cli/tests/test_crew_run_tui.py +++ b/lib/cli/tests/test_crew_run_tui.py @@ -4,6 +4,11 @@ import time import pytest from crewai.events.event_bus import crewai_event_bus +from crewai.events.types.memory_events import ( + MemorySaveCompletedEvent, + MemorySaveFailedEvent, + MemorySaveStartedEvent, +) from crewai.events.types.observation_events import ( GoalAchievedEarlyEvent, PlanRefinementEvent, @@ -21,7 +26,12 @@ from crewai.events.types.tool_usage_events import ( ) from crewai_cli.command import AuthenticationRequiredError from crewai_cli import run_crew -from crewai_cli.crew_run_tui import CrewRunApp +from crewai_cli.crew_run_tui import ( + CrewRunApp, + _LOG_ARGS_TEXT_LIMIT, + _LOG_RESULT_TEXT_LIMIT, + _LOG_TRUNCATION_SUFFIX, +) def _app_with_plan() -> CrewRunApp: @@ -335,6 +345,396 @@ def test_internal_reasoning_function_call_is_hidden_from_activity_log() -> None: assert app._current_task_steps == [] +def test_memory_save_events_are_shown_in_activity_log() -> None: + app = _app_with_plan() + app._current_task_idx = 1 + app._subscribe() + try: + _emit_event( + MemorySaveStartedEvent( + value="2 memories (background)", + metadata={}, + source_type="unified_memory", + ) + ) + _emit_event( + MemorySaveCompletedEvent( + value="2 memories saved", + metadata={}, + save_time_ms=123, + source_type="unified_memory", + ) + ) + finally: + app._unsubscribe() + + assert len(app._log_entries) == 1 + assert app._log_entries[0]["tool_name"] == "memory_save" + assert app._log_entries[0]["status"] == "success" + assert app._log_entries[0]["args"] == "2 memories (background)" + assert app._log_entries[0]["result"] == "2 memories saved" + assert app._log_entries[0]["error"] is None + assert app._log_entries[0]["duration"] == 0.123 + assert app._log_entries[0]["task_idx"] == 1 + + +def test_nested_memory_save_event_is_hidden_for_save_to_memory_tool() -> None: + app = _app_with_plan() + app._subscribe() + try: + tool_args = {"contents": ["Fact to remember."]} + _emit_event( + ToolUsageStartedEvent( + tool_name="save_to_memory", + tool_args=tool_args, + ) + ) + _emit_event( + MemorySaveStartedEvent( + value="Fact to remember.", + metadata={}, + source_type="unified_memory", + ) + ) + _emit_event( + MemorySaveCompletedEvent( + value="Fact to remember.", + metadata={}, + save_time_ms=123, + source_type="unified_memory", + ) + ) + now = datetime.now() + _emit_event( + ToolUsageFinishedEvent( + tool_name="save_to_memory", + tool_args=tool_args, + started_at=now, + finished_at=now, + output="Saved to memory.", + ) + ) + finally: + app._unsubscribe() + + assert len(app._log_entries) == 1 + assert app._log_entries[0]["tool_name"] == "save_to_memory" + assert app._log_entries[0]["status"] == "success" + assert app._log_entries[0]["result"] == "Saved to memory." + + +def test_memory_save_failure_is_shown_in_activity_log() -> None: + app = _app_with_plan() + app._subscribe() + try: + _emit_event( + MemorySaveStartedEvent( + value="background save", + metadata={}, + source_type="unified_memory", + ) + ) + _emit_event( + MemorySaveFailedEvent( + value="background save", + metadata={}, + error="embedding connection failed", + source_type="unified_memory", + ) + ) + finally: + app._unsubscribe() + + assert app._log_entries[0]["tool_name"] == "memory_save" + assert app._log_entries[0]["status"] == "error" + assert app._log_entries[0]["error"] == "embedding connection failed" + assert app._log_expanded == {0} + + +def test_memory_save_completion_updates_timed_out_row() -> None: + app = _app_with_plan() + app._subscribe() + try: + _emit_event( + MemorySaveStartedEvent( + value="9 memories (background)", + metadata={}, + source_type="unified_memory", + ) + ) + + app._log_entries[0]["status"] = "timeout" + app._log_entries[0]["error"] = "No result received before crew completed" + app._log_entries[0]["duration"] = 8.3 + + _emit_event( + MemorySaveCompletedEvent( + value="9 memories saved", + metadata={}, + save_time_ms=8300, + source_type="unified_memory", + ) + ) + finally: + app._unsubscribe() + + assert len(app._log_entries) == 1 + assert app._log_entries[0]["tool_name"] == "memory_save" + assert app._log_entries[0]["status"] == "success" + assert app._log_entries[0]["result"] == "9 memories saved" + assert app._log_entries[0]["error"] is None + assert app._log_entries[0]["duration"] == 8.3 + + +def test_memory_save_completion_with_unmatched_id_does_not_update_running_row() -> None: + app = _app_with_plan() + app._subscribe() + try: + _emit_event( + MemorySaveStartedEvent( + value="first background save", + metadata={}, + source_type="unified_memory", + parent_event_id="manual-parent", + ) + ) + _emit_event( + MemorySaveStartedEvent( + value="second background save", + metadata={}, + source_type="unified_memory", + parent_event_id="manual-parent", + ) + ) + + _emit_event( + MemorySaveCompletedEvent( + value="orphan save completed", + metadata={}, + save_time_ms=2800, + source_type="unified_memory", + parent_event_id="manual-parent", + started_event_id="missing-memory-save-start", + ) + ) + finally: + app._unsubscribe() + + assert [entry["status"] for entry in app._log_entries] == [ + "running", + "running", + "success", + ] + assert app._log_entries[0]["args"] == "first background save" + assert app._log_entries[1]["args"] == "second background save" + assert app._log_entries[2]["result"] == "orphan save completed" + assert app._log_entries[2]["started_event_id"] == "missing-memory-save-start" + + +def test_memory_save_failure_with_unmatched_id_does_not_update_running_row() -> None: + app = _app_with_plan() + app._subscribe() + try: + _emit_event( + MemorySaveStartedEvent( + value="first background save", + metadata={}, + source_type="unified_memory", + parent_event_id="manual-parent", + ) + ) + _emit_event( + MemorySaveStartedEvent( + value="second background save", + metadata={}, + source_type="unified_memory", + parent_event_id="manual-parent", + ) + ) + + _emit_event( + MemorySaveFailedEvent( + value="orphan save failed", + metadata={}, + error="embedding connection failed", + source_type="unified_memory", + parent_event_id="manual-parent", + started_event_id="missing-memory-save-start", + ) + ) + finally: + app._unsubscribe() + + assert [entry["status"] for entry in app._log_entries] == [ + "running", + "running", + "error", + ] + assert app._log_entries[0]["args"] == "first background save" + assert app._log_entries[1]["args"] == "second background save" + assert app._log_entries[2]["args"] == "orphan save failed" + assert app._log_entries[2]["error"] == "embedding connection failed" + assert app._log_entries[2]["started_event_id"] == "missing-memory-save-start" + assert app._log_expanded == {2} + + +def test_memory_save_completion_without_id_does_not_update_stale_row() -> None: + app = _app_with_plan() + now = time.time() + app._log_entries = [ + { + "tool_name": "memory_save", + "status": "running", + "args": "current background save", + "result": None, + "error": None, + "start_time": now, + "duration": None, + "task_idx": 1, + }, + { + "tool_name": "memory_save", + "status": "success", + "args": "stale background save", + "result": "stale save completed", + "error": None, + "start_time": now - 10, + "duration": 1.0, + "task_idx": 1, + }, + ] + + app._subscribe() + try: + _emit_event( + MemorySaveCompletedEvent( + value="current save completed", + metadata={}, + save_time_ms=2800, + source_type="unified_memory", + parent_event_id="manual-parent", + ) + ) + finally: + app._unsubscribe() + + assert [entry["status"] for entry in app._log_entries] == [ + "success", + "success", + ] + assert app._log_entries[0]["args"] == "current background save" + assert app._log_entries[0]["result"] == "current save completed" + assert app._log_entries[1]["args"] == "stale background save" + assert app._log_entries[1]["result"] == "stale save completed" + + +def test_memory_save_failure_without_id_does_not_update_stale_row() -> None: + app = _app_with_plan() + now = time.time() + app._log_entries = [ + { + "tool_name": "memory_save", + "status": "running", + "args": "current background save", + "result": None, + "error": None, + "start_time": now, + "duration": None, + "task_idx": 1, + }, + { + "tool_name": "memory_save", + "status": "success", + "args": "stale background save", + "result": "stale save completed", + "error": None, + "start_time": now - 10, + "duration": 1.0, + "task_idx": 1, + }, + ] + + app._subscribe() + try: + _emit_event( + MemorySaveFailedEvent( + value="current save failed", + metadata={}, + error="embedding connection failed", + source_type="unified_memory", + parent_event_id="manual-parent", + ) + ) + finally: + app._unsubscribe() + + assert [entry["status"] for entry in app._log_entries] == ["error", "success"] + assert app._log_entries[0]["args"] == "current background save" + assert app._log_entries[0]["error"] == "embedding connection failed" + assert app._log_entries[1]["args"] == "stale background save" + assert app._log_entries[1]["result"] == "stale save completed" + assert app._log_entries[1]["error"] is None + assert app._log_expanded == {0} + + +def test_memory_save_payloads_are_truncated_in_activity_log() -> None: + app = _app_with_plan() + long_args = "a" * (_LOG_ARGS_TEXT_LIMIT + 10) + long_result = "r" * (_LOG_RESULT_TEXT_LIMIT + 10) + + app._subscribe() + try: + _emit_event( + MemorySaveStartedEvent( + value=long_args, + metadata={}, + source_type="unified_memory", + ) + ) + _emit_event( + MemorySaveCompletedEvent( + value=long_result, + metadata={}, + save_time_ms=8300, + source_type="unified_memory", + ) + ) + finally: + app._unsubscribe() + + assert len(app._log_entries[0]["args"]) == _LOG_ARGS_TEXT_LIMIT + assert app._log_entries[0]["args"].endswith(_LOG_TRUNCATION_SUFFIX) + assert len(app._log_entries[0]["result"]) == _LOG_RESULT_TEXT_LIMIT + assert app._log_entries[0]["result"].endswith(_LOG_TRUNCATION_SUFFIX) + + +def test_starting_next_tool_does_not_timeout_memory_save() -> None: + app = _app_with_plan() + app._subscribe() + try: + _emit_event( + MemorySaveStartedEvent( + value="9 memories (background)", + metadata={}, + source_type="unified_memory", + ) + ) + _emit_event( + ToolUsageStartedEvent( + tool_name="read_website_content", + tool_args={"url": "https://example.com"}, + ) + ) + finally: + app._unsubscribe() + + assert app._log_entries[0]["tool_name"] == "memory_save" + assert app._log_entries[0]["status"] == "running" + assert app._log_entries[0]["error"] is None + assert app._log_entries[1]["tool_name"] == "read_website_content" + assert app._log_entries[1]["status"] == "running" + + def test_tool_failure_does_not_override_successful_plan_step_completion() -> None: app = _app_with_plan() app._subscribe() @@ -480,6 +880,187 @@ async def test_crew_done_does_not_mark_unfinished_tool_successful() -> None: assert app._plan_step_status == {1: "failed", 2: "done", 3: "done"} +@pytest.mark.asyncio +async def test_crew_done_does_not_timeout_memory_save() -> None: + app = _app_with_plan() + + async with app.run_test(size=(100, 40)) as pilot: + app._log_entries = [ + { + "tool_name": "memory_save", + "status": "running", + "args": "9 memories (background)", + "result": None, + "error": None, + "start_time": time.time() - 8, + "duration": None, + "task_idx": 1, + }, + { + "tool_name": "search", + "status": "running", + "args": '{"query": "CrewAI"}', + "result": None, + "error": None, + "start_time": time.time() - 2, + "duration": None, + "task_idx": 1, + }, + ] + + app._on_crew_done("final output") + await pilot.pause() + + assert app._log_entries[0]["status"] == "running" + assert app._log_entries[0]["error"] is None + assert app._log_entries[1]["status"] == "timeout" + assert app._log_entries[1]["error"] == "No result received before crew completed" + + +@pytest.mark.asyncio +async def test_crew_done_keeps_memory_save_subscription_until_completion( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr( + "crewai_cli.crew_run_tui._MEMORY_SAVE_DRAIN_GRACE_SECONDS", 0.05 + ) + app = _app_with_plan() + auto_unsubscribed = False + + async with app.run_test(size=(100, 40)) as pilot: + try: + assert app._event_handlers + started_event = MemorySaveStartedEvent( + value="9 memories (background)", + metadata={}, + source_type="unified_memory", + ) + _emit_event(started_event) + + app._on_crew_done("final output") + await pilot.pause() + + assert app._log_entries[0]["status"] == "running" + assert app._event_handlers + + _emit_event( + MemorySaveCompletedEvent( + value="9 memories saved", + metadata={}, + save_time_ms=8300, + source_type="unified_memory", + started_event_id=started_event.event_id, + ) + ) + await pilot.pause() + + assert app._event_handlers + await pilot.pause(0.08) + auto_unsubscribed = not app._event_handlers + finally: + app._unsubscribe() + + assert app._log_entries[0]["tool_name"] == "memory_save" + assert app._log_entries[0]["status"] == "success" + assert app._log_entries[0]["result"] == "9 memories saved" + assert app._log_entries[0]["error"] is None + assert app._log_entries[0]["duration"] == 8.3 + assert auto_unsubscribed is True + + +@pytest.mark.asyncio +async def test_crew_done_waits_for_queued_memory_save_events( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr( + "crewai_cli.crew_run_tui._MEMORY_SAVE_DRAIN_GRACE_SECONDS", 0.05 + ) + app = _app_with_plan() + auto_unsubscribed = False + + async with app.run_test(size=(100, 40)) as pilot: + try: + assert app._event_handlers + + app._on_crew_done("final output") + + assert app._event_handlers + started_event = MemorySaveStartedEvent( + value="9 memories (background)", + metadata={}, + source_type="unified_memory", + parent_event_id="manual-parent", + ) + _emit_event(started_event) + await pilot.pause() + + assert app._log_entries[0]["tool_name"] == "memory_save" + assert app._log_entries[0]["status"] == "running" + + _emit_event( + MemorySaveCompletedEvent( + value="9 memories saved", + metadata={}, + save_time_ms=8300, + source_type="unified_memory", + parent_event_id="manual-parent", + started_event_id=started_event.event_id, + ) + ) + await pilot.pause() + + assert app._event_handlers + await pilot.pause(0.08) + auto_unsubscribed = not app._event_handlers + finally: + app._unsubscribe() + + assert app._log_entries[0]["tool_name"] == "memory_save" + assert app._log_entries[0]["status"] == "success" + assert app._log_entries[0]["args"] == "9 memories (background)" + assert app._log_entries[0]["result"] == "9 memories saved" + assert app._log_entries[0]["error"] is None + assert app._log_entries[0]["duration"] == 8.3 + assert auto_unsubscribed is True + + +@pytest.mark.asyncio +async def test_crew_failed_does_not_timeout_memory_save() -> None: + app = _app_with_plan() + + async with app.run_test(size=(100, 40)) as pilot: + app._log_entries = [ + { + "tool_name": "memory_save", + "status": "running", + "args": "9 memories (background)", + "result": None, + "error": None, + "start_time": time.time() - 8, + "duration": None, + "task_idx": 1, + }, + { + "tool_name": "search", + "status": "running", + "args": '{"query": "CrewAI"}', + "result": None, + "error": None, + "start_time": time.time() - 2, + "duration": None, + "task_idx": 1, + }, + ] + + app._on_crew_failed("boom") + await pilot.pause() + + assert app._log_entries[0]["status"] == "running" + assert app._log_entries[0]["error"] is None + assert app._log_entries[1]["status"] == "error" + assert app._log_entries[1]["error"] == "No result received before crew failed" + + def test_streamed_step_observation_updates_named_step_only() -> None: app = _app_with_plan() diff --git a/lib/cli/tests/test_run_crew.py b/lib/cli/tests/test_run_crew.py index 88d31fa1e..ebb48d72b 100644 --- a/lib/cli/tests/test_run_crew.py +++ b/lib/cli/tests/test_run_crew.py @@ -5,12 +5,33 @@ from pathlib import Path import subprocess import sys +import click import pytest from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV import crewai_cli.run_crew as run_crew_module +def test_missing_crewai_package_shows_full_install_hint(monkeypatch): + def missing_crewai_package(): + raise ModuleNotFoundError("No module named 'crewai'", name="crewai") + + monkeypatch.setattr( + run_crew_module, "_import_find_crew_json_file", missing_crewai_package + ) + + with pytest.raises(click.ClickException) as exc_info: + run_crew_module.find_crew_json_file() + + message = exc_info.value.message + assert "CrewAI CLI is installed without the `crewai` package" in message + assert ( + "uv tool install --force --prerelease=allow 'crewai[tools]==1.14.8a1'" + in message + ) + assert "quotes are required in zsh" in message + + def test_run_crew_forwards_trained_agents_file_to_json_crews(monkeypatch): """crewai run -f must reach JSON crews, not only classic subprocess crews.""" monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: True) diff --git a/lib/crewai-core/src/crewai_core/__init__.py b/lib/crewai-core/src/crewai_core/__init__.py index 4e3686826..cb6f48866 100644 --- a/lib/crewai-core/src/crewai_core/__init__.py +++ b/lib/crewai-core/src/crewai_core/__init__.py @@ -1 +1 @@ -__version__ = "1.14.8a" +__version__ = "1.14.8a1" diff --git a/lib/crewai-files/pyproject.toml b/lib/crewai-files/pyproject.toml index 0302b5900..e4d6cd29e 100644 --- a/lib/crewai-files/pyproject.toml +++ b/lib/crewai-files/pyproject.toml @@ -9,7 +9,7 @@ authors = [ requires-python = ">=3.10, <3.14" dependencies = [ "Pillow~=12.1.1", - "pypdf~=6.10.0", + "pypdf~=6.13.3", "python-magic>=0.4.27", "aiocache~=0.12.3", "aiofiles~=24.1.0", @@ -19,6 +19,8 @@ dependencies = [ [tool.uv] exclude-newer = "3 days" +# pypdf 6.13.3 is a security fix newer than the global supply-chain cutoff. +exclude-newer-package = { pypdf = "2026-06-18T00:00:00Z" } [build-system] requires = ["hatchling"] diff --git a/lib/crewai-files/src/crewai_files/__init__.py b/lib/crewai-files/src/crewai_files/__init__.py index fbb2e6c8b..fdc70ce7d 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.8a" +__version__ = "1.14.8a1" diff --git a/lib/crewai-tools/pyproject.toml b/lib/crewai-tools/pyproject.toml index 50e80a7a9..111f0a2ec 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.33.0,<3", - "crewai==1.14.8a", + "crewai==1.14.8a1", "tiktoken>=0.8.0,<0.13", "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 a6ba3c303..89761b074 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -330,4 +330,4 @@ __all__ = [ "ZapierActionTools", ] -__version__ = "1.14.8a" +__version__ = "1.14.8a1" diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index 1d8608fe4..44fa7104f 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -8,8 +8,8 @@ authors = [ ] requires-python = ">=3.10, <3.14" dependencies = [ - "crewai-core==1.14.8a", - "crewai-cli==1.14.8a", + "crewai-core==1.14.8a1", + "crewai-cli==1.14.8a1", # Core Dependencies "pydantic>=2.11.9,<2.13", "openai>=2.30.0,<3", @@ -55,7 +55,7 @@ Repository = "https://github.com/crewAIInc/crewAI" [project.optional-dependencies] tools = [ - "crewai-tools==1.14.8a", + "crewai-tools==1.14.8a1", ] embeddings = [ "tiktoken>=0.8.0,<0.13" diff --git a/lib/crewai/src/crewai/__init__.py b/lib/crewai/src/crewai/__init__.py index 4a9f40a58..e8e8f4faa 100644 --- a/lib/crewai/src/crewai/__init__.py +++ b/lib/crewai/src/crewai/__init__.py @@ -48,7 +48,7 @@ def _suppress_pydantic_deprecation_warnings() -> None: _suppress_pydantic_deprecation_warnings() -__version__ = "1.14.8a" +__version__ = "1.14.8a1" _LAZY_IMPORTS: dict[str, tuple[str, str]] = { "Memory": ("crewai.memory.unified_memory", "Memory"), diff --git a/lib/crewai/src/crewai/memory/unified_memory.py b/lib/crewai/src/crewai/memory/unified_memory.py index 5a8718eda..dcd5383ce 100644 --- a/lib/crewai/src/crewai/memory/unified_memory.py +++ b/lib/crewai/src/crewai/memory/unified_memory.py @@ -3,7 +3,9 @@ from __future__ import annotations from concurrent.futures import Future, ThreadPoolExecutor +from contextlib import suppress import contextvars +import copy from datetime import datetime import threading import time @@ -53,6 +55,24 @@ def _default_embedder() -> OpenAIEmbeddingFunction: return build_embedder(spec) +def _non_streaming_analysis_llm(llm: Any) -> Any: + """Return an isolated non-streaming LLM for internal memory analysis.""" + if not isinstance(llm, BaseLLM): + return llm + + try: + analysis_llm = copy.copy(llm) + except Exception: + try: + analysis_llm = llm.model_copy(deep=False) + except Exception: + return llm + + with suppress(Exception): + analysis_llm.stream = False + return analysis_llm + + class Memory(BaseModel): """Unified memory: standalone, LLM-analyzed, with intelligent recall flow. @@ -200,7 +220,9 @@ class Memory(BaseModel): query_analysis_threshold=self.query_analysis_threshold, ) - self._llm_instance = None if isinstance(self.llm, str) else self.llm + self._llm_instance = ( + None if isinstance(self.llm, str) else _non_streaming_analysis_llm(self.llm) + ) self._embedder_instance = ( self.embedder if (self.embedder is not None and not isinstance(self.embedder, dict)) diff --git a/lib/crewai/tests/memory/test_unified_memory.py b/lib/crewai/tests/memory/test_unified_memory.py index cbd9cc3ee..8a3d52e7a 100644 --- a/lib/crewai/tests/memory/test_unified_memory.py +++ b/lib/crewai/tests/memory/test_unified_memory.py @@ -19,6 +19,39 @@ from crewai.memory.types import ( ) +def test_memory_analysis_llm_is_isolated_from_streaming_agent_llm( + tmp_path: Path, +) -> None: + """Memory analysis should not share a mutable streaming LLM with the agent UI.""" + from crewai.llms.base_llm import BaseLLM + from crewai.memory.unified_memory import Memory + from crewai.utilities.types import LLMMessage + + class FakeStreamingLLM(BaseLLM): + def call( + self, + messages: str | list[LLMMessage], + tools: list[dict] | None = None, + callbacks: list | None = None, + available_functions: dict | None = None, + from_task: object | None = None, + from_agent: object | None = None, + response_model: type | None = None, + ) -> str: + return "" + + agent_llm = FakeStreamingLLM(model="fake-model", stream=True) + mem = Memory( + storage=str(tmp_path / "db"), + llm=agent_llm, + embedder=lambda texts: [[0.1] for _ in texts], + ) + + assert mem._llm is not agent_llm + assert mem._llm.stream is False + + agent_llm.stream = True + assert mem._llm.stream is False def test_memory_record_defaults() -> None: diff --git a/lib/devtools/src/crewai_devtools/__init__.py b/lib/devtools/src/crewai_devtools/__init__.py index f676ae0b8..5d5c35002 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.8a" +__version__ = "1.14.8a1" diff --git a/pyproject.toml b/pyproject.toml index 018988af4..d36586f4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -171,6 +171,8 @@ info = "Commits must follow Conventional Commits 1.0.0." [tool.uv] exclude-newer = "3 days" +# pypdf 6.13.3 is a security fix newer than the global supply-chain cutoff. +exclude-newer-package = { pypdf = "2026-06-18T00:00:00Z" } # composio-core pins rich<14 but textual requires rich>=14. # onnxruntime 1.24+ dropped Python 3.10 wheels; cap it so qdrant[fastembed] resolves on 3.10. @@ -180,7 +182,8 @@ exclude-newer = "3 days" # langchain-text-splitters <1.1.2 has GHSA-fv5p-p927-qmxr (SSRF bypass in split_text_from_url). # transformers 4.57.6 has CVE-2026-1839; force 5.4+ (docling 2.84 allows huggingface-hub>=1). # cryptography 46.0.6 has CVE-2026-39892; force 46.0.7+. -# pypdf <6.10.2 has GHSA-4pxv-j86v-mhcw, GHSA-7gw9-cf7v-778f, GHSA-x284-j5p8-9c5p; force 6.10.2+. +# pypdf <6.10.2 has GHSA-4pxv-j86v-mhcw, GHSA-7gw9-cf7v-778f, GHSA-x284-j5p8-9c5p. +# pypdf <6.13.3 has GHSA-jm82-fx9c-mx94; force 6.13.3+. # uv <0.11.15 has GHSA-4gg8-gxpx-9rph (and earlier GHSA-pjjw-68hj-v9mw); force 0.11.15+. # python-multipart <0.0.27 has GHSA-pp6c-gr5w-3c5g (DoS via unbounded multipart headers). # gitpython <3.1.50 has GHSA-mv93-w799-cj2w (config_writer newline injection bypassing the 3.1.49 patch -> RCE via core.hooksPath). @@ -205,7 +208,7 @@ override-dependencies = [ "urllib3>=2.7.0", "transformers>=5.4.0; python_version >= '3.10'", "cryptography>=46.0.7", - "pypdf>=6.10.2,<7", + "pypdf>=6.13.3,<7", "uv>=0.11.15,<1", "python-multipart>=0.0.27,<1", "gitpython>=3.1.50,<4", diff --git a/uv.lock b/uv.lock index 1ebe9c0b5..d2a5c37fc 100644 --- a/uv.lock +++ b/uv.lock @@ -16,6 +16,9 @@ resolution-markers = [ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P3D" +[options.exclude-newer-package] +pypdf = "2026-06-18T00:00:00Z" + [manifest] members = [ "crewai", @@ -40,7 +43,7 @@ overrides = [ { name = "pillow", specifier = ">=12.1.1" }, { name = "pip", specifier = ">=26.1.2" }, { name = "pydantic-settings", specifier = ">=2.14.0" }, - { name = "pypdf", specifier = ">=6.10.2,<7" }, + { name = "pypdf", specifier = ">=6.13.3,<7" }, { name = "python-multipart", specifier = ">=0.0.27,<1" }, { name = "rich", specifier = ">=13.7.1" }, { name = "starlette", specifier = ">=1.3.1" }, @@ -1584,7 +1587,7 @@ requires-dist = [ { name = "aiofiles", specifier = "~=24.1.0" }, { name = "av", specifier = "~=13.0.0" }, { name = "pillow", specifier = "~=12.1.1" }, - { name = "pypdf", specifier = "~=6.10.0" }, + { name = "pypdf", specifier = "~=6.13.3" }, { name = "python-magic", specifier = ">=0.4.27" }, { name = "tinytag", specifier = "~=2.2.1" }, ] @@ -7188,14 +7191,14 @@ wheels = [ [[package]] name = "pypdf" -version = "6.13.1" +version = "6.13.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/15/d9/9d12fa0d9660d03320725ff686c961b645a4218940a82296e1272d9e1ff0/pypdf-6.13.1.tar.gz", hash = "sha256:4841d8a4c1589e5833915dc0c7ddfacff80a2e0bcbeb5d1e681fecaa1674b03a", size = 6477811, upload-time = "2026-06-08T11:01:49.344Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/18/9947cc201af9ccf76720fd3347bf4f70eb882ce3fcf4cb05f7443e4cf871/pypdf-6.13.3.tar.gz", hash = "sha256:f3cb822769725f1bac658c406cfc9460399043f3750c2d3e4650e0a85eacabd7", size = 6484063, upload-time = "2026-06-17T15:22:00.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/dd/8f03e0a5788a5d1feb4550617c3e6db5e9099eaee248a3e482ddaeacbbb0/pypdf-6.13.1-py3-none-any.whl", hash = "sha256:e555e4ce3f561ef069307622f1374136ba964ca6ca24f24158701decaf83ed9b", size = 346259, upload-time = "2026-06-08T11:01:47.741Z" }, + { url = "https://files.pythonhosted.org/packages/94/56/2967e621598987905fb8cdfadd8f8de6b5c68c9351f0523c4df8409f28f1/pypdf-6.13.3-py3-none-any.whl", hash = "sha256:c6e3f86afb625791510b02ad5480e94b63970bb957df75d44657c282ecc52224", size = 347288, upload-time = "2026-06-17T15:21:59.512Z" }, ] [[package]]