From b4b285764c83bc8aabae4ea1088d1a3c3b65e455 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Thu, 21 May 2026 14:53:40 +0800 Subject: [PATCH 1/4] fix: harden RuntimeState serialization across entity fields Adds missing serializers, discriminators, and exclude markers on entity fields that previously crashed model_dump_json or restored ambiguously: - Flow.persistence: add _serialize_persistence; drop | Any escape hatch - Flow.input_provider: SerializableInstance dotted-path round-trip - BaseAgent.agent_executor: add _serialize_executor_ref - BaseAgent.tools_handler / cache_handler: exclude=True - Memory / MemoryScope / MemorySlice: memory_kind Literal discriminator - Knowledge.storage / .embedder: exclude live client, serialize spec - BaseKnowledgeSource subclasses: source_type Literal + dict-resolver - BaseKnowledgeSource.storage / chunk_embeddings: exclude=True - input_provider: enforce InputProvider protocol via dedicated validator/serializer; reject non-class dotted paths in _dotted_path_to_instance - MemoryScope/MemorySlice: allow restore without live Memory; expose bind() to reattach the dependency post-restore - Knowledge.embedder: add BeforeValidator that resolves provider_class dotted paths back to a BaseEmbeddingsProvider subclass --- .../crewai/agents/agent_builder/base_agent.py | 53 +++++++-- lib/crewai/src/crewai/crew.py | 52 ++++++++- lib/crewai/src/crewai/flow/flow.py | 61 +++++++++- lib/crewai/src/crewai/knowledge/knowledge.py | 87 +++++++++++++- .../knowledge/source/base_knowledge_source.py | 4 +- .../knowledge/source/crew_docling_source.py | 3 +- .../knowledge/source/csv_knowledge_source.py | 3 + .../source/excel_knowledge_source.py | 3 +- .../knowledge/source/json_knowledge_source.py | 4 +- .../knowledge/source/pdf_knowledge_source.py | 3 + .../source/string_knowledge_source.py | 3 +- .../source/text_file_knowledge_source.py | 3 + lib/crewai/src/crewai/memory/memory_scope.py | 109 +++++++++++++----- .../src/crewai/memory/unified_memory.py | 2 + lib/crewai/src/crewai/state/runtime.py | 60 +++++++++- lib/crewai/src/crewai/types/callback.py | 86 +++++++++++++- .../src/crewai/utilities/reset_memories.py | 10 +- lib/crewai/tests/test_flow_ask.py | 76 +++++++++++- 18 files changed, 555 insertions(+), 67 deletions(-) diff --git a/lib/crewai/src/crewai/agents/agent_builder/base_agent.py b/lib/crewai/src/crewai/agents/agent_builder/base_agent.py index 9844bee03..8b5e36ff4 100644 --- a/lib/crewai/src/crewai/agents/agent_builder/base_agent.py +++ b/lib/crewai/src/crewai/agents/agent_builder/base_agent.py @@ -31,13 +31,13 @@ from crewai.agents.tools_handler import ToolsHandler from crewai.events.base_events import set_emission_counter from crewai.events.event_bus import crewai_event_bus from crewai.events.event_context import restore_event_scope, set_last_event_id -from crewai.knowledge.knowledge import Knowledge +from crewai.knowledge.knowledge import Knowledge, _resolve_knowledge_sources from crewai.knowledge.knowledge_config import KnowledgeConfig from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource from crewai.knowledge.storage.base_knowledge_storage import BaseKnowledgeStorage from crewai.llms.base_llm import BaseLLM from crewai.mcp.config import MCPServerConfig -from crewai.memory.memory_scope import MemoryScope, MemorySlice +from crewai.memory.memory_scope import MemoryScope, MemorySlice, _ensure_memory_kind from crewai.memory.unified_memory import Memory from crewai.rag.embeddings.types import EmbedderConfig from crewai.security.security_config import SecurityConfig @@ -127,6 +127,13 @@ def _validate_executor_ref(value: Any) -> Any: return value +def _serialize_executor_ref(value: Any) -> dict[str, Any] | None: + if value is None: + return None + result: dict[str, Any] = value.model_dump(mode="json") + return result + + def _serialize_llm_ref(value: Any) -> dict[str, Any] | None: if value is None: return None @@ -251,14 +258,13 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta): max_iter: int = Field( default=25, description="Maximum iterations for an agent to execute a task" ) - agent_executor: SerializeAsAny[BaseAgentExecutor] | None = Field( - default=None, description="An instance of the CrewAgentExecutor class." - ) - - @field_validator("agent_executor", mode="before") - @classmethod - def _validate_agent_executor(cls, v: Any) -> Any: - return _validate_executor_ref(v) + agent_executor: Annotated[ + SerializeAsAny[BaseAgentExecutor] | None, + BeforeValidator(_validate_executor_ref), + PlainSerializer( + _serialize_executor_ref, return_type=dict | None, when_used="json" + ), + ] = Field(default=None, description="An instance of the CrewAgentExecutor class.") llm: Annotated[ str | BaseLLM | None, @@ -288,7 +294,10 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta): knowledge: Knowledge | None = Field( default=None, description="Knowledge for the agent." ) - knowledge_sources: list[BaseKnowledgeSource] | None = Field( + knowledge_sources: Annotated[ + list[BaseKnowledgeSource] | None, + BeforeValidator(_resolve_knowledge_sources), + ] = Field( default=None, description="Knowledge sources for the agent.", ) @@ -326,7 +335,14 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta): default=None, description="List of MCP server references. Supports 'https://server.com/path' for external servers and bare slugs like 'notion' for connected MCP integrations. Use '#tool_name' suffix for specific tools.", ) - memory: bool | Memory | MemoryScope | MemorySlice | None = Field( + memory: Annotated[ + bool + | Annotated[ + Memory | MemoryScope | MemorySlice, Field(discriminator="memory_kind") + ] + | None, + BeforeValidator(_ensure_memory_kind), + ] = Field( default=None, description=( "Enable agent memory. Pass True for default Memory(), " @@ -397,8 +413,21 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta): self.agent_executor._resuming = True if self.checkpoint_kickoff_event_id is not None: self._kickoff_event_id = self.checkpoint_kickoff_event_id + self._rebind_memory_view() self._restore_event_scope(state) + def _rebind_memory_view(self) -> None: + """Reattach a fresh ``Memory`` to a restored ``MemoryScope``/``MemorySlice``. + + Checkpoint JSON omits the live ``Memory`` dependency, so scoped + memory views raise ``RuntimeError`` on first use after restore. + """ + if ( + isinstance(self.memory, MemoryScope | MemorySlice) + and self.memory._memory is None + ): + self.memory.bind(Memory()) + def _restore_event_scope(self, state: RuntimeState) -> None: """Rebuild the event scope stack from the checkpoint's event record. diff --git a/lib/crewai/src/crewai/crew.py b/lib/crewai/src/crewai/crew.py index acc90e965..9f69129f1 100644 --- a/lib/crewai/src/crewai/crew.py +++ b/lib/crewai/src/crewai/crew.py @@ -93,11 +93,11 @@ from crewai.events.types.crew_events import ( CrewTrainStartedEvent, ) from crewai.flow.flow_trackable import FlowTrackable -from crewai.knowledge.knowledge import Knowledge +from crewai.knowledge.knowledge import Knowledge, _resolve_knowledge_sources from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource from crewai.llm import LLM from crewai.llms.base_llm import BaseLLM -from crewai.memory.memory_scope import MemoryScope, MemorySlice +from crewai.memory.memory_scope import MemoryScope, MemorySlice, _ensure_memory_kind from crewai.memory.unified_memory import Memory from crewai.process import Process from crewai.rag.embeddings.types import EmbedderConfig @@ -223,7 +223,14 @@ class Crew(FlowTrackable, BaseModel): ] = Field(default_factory=list) process: Process = Field(default=Process.sequential) verbose: bool = Field(default=False) - memory: bool | Memory | MemoryScope | MemorySlice | None = Field( + memory: Annotated[ + bool + | Annotated[ + Memory | MemoryScope | MemorySlice, Field(discriminator="memory_kind") + ] + | None, + BeforeValidator(_ensure_memory_kind), + ] = Field( default=False, description=( "Enable crew memory. Pass True for default Memory(), " @@ -322,7 +329,10 @@ class Crew(FlowTrackable, BaseModel): default_factory=list, description="list of execution logs for tasks", ) - knowledge_sources: list[BaseKnowledgeSource] | None = Field( + knowledge_sources: Annotated[ + list[BaseKnowledgeSource] | None, + BeforeValidator(_resolve_knowledge_sources), + ] = Field( default=None, description=( "Knowledge sources for the crew. Add knowledge sources to the " @@ -477,8 +487,42 @@ class Crew(FlowTrackable, BaseModel): if self.checkpoint_train is not None: self._train = self.checkpoint_train + self._rebind_memory_views() self._restore_event_scope() + def _rebind_memory_views(self) -> None: + """Reattach a live ``Memory`` to restored ``MemoryScope``/``MemorySlice`` views. + + Checkpoint JSON omits the live ``Memory`` dependency on scope/slice + views, so after restore they raise ``RuntimeError`` on first use. + Prefer the crew's restored ``Memory`` (from ``create_crew_memory`` + or a ``Crew.memory=Memory(...)`` instance) so all views share one + backing store; fall back to a fresh ``Memory()`` only if nothing is + available. + """ + from crewai.memory.memory_scope import MemoryScope, MemorySlice + from crewai.memory.unified_memory import Memory + + backing: Memory | None = None + if isinstance(self._memory, Memory): + backing = self._memory + elif isinstance(self.memory, Memory): + backing = self.memory + + def _ensure(view: Any) -> None: + nonlocal backing + if not isinstance(view, MemoryScope | MemorySlice): + return + if view._memory is not None: + return + if backing is None: + backing = Memory() + view.bind(backing) + + _ensure(self.memory) + for agent in self.agents: + _ensure(agent.memory) + def _restore_event_scope(self) -> None: """Rebuild the event scope stack from the checkpoint's event record.""" from crewai.events.base_events import set_emission_counter diff --git a/lib/crewai/src/crewai/flow/flow.py b/lib/crewai/src/crewai/flow/flow.py index d22794873..ef9658128 100644 --- a/lib/crewai/src/crewai/flow/flow.py +++ b/lib/crewai/src/crewai/flow/flow.py @@ -113,7 +113,7 @@ from crewai.flow.utils import ( is_flow_method_name, is_simple_flow_condition, ) -from crewai.memory.memory_scope import MemoryScope, MemorySlice +from crewai.memory.memory_scope import MemoryScope, MemorySlice, _ensure_memory_kind from crewai.memory.unified_memory import Memory from crewai.state.checkpoint_config import ( CheckpointConfig, @@ -159,6 +159,39 @@ def _resolve_persistence(value: Any) -> Any: return value +def _serialize_persistence(value: Any) -> dict[str, Any] | None: + if value is None: + return None + if isinstance(value, FlowPersistence): + return value.model_dump(mode="json") + raise TypeError( + f"Cannot serialize Flow.persistence of type {type(value).__name__}: " + "expected FlowPersistence or None." + ) + + +def _validate_input_provider(value: Any) -> Any: + if value is None or isinstance(value, InputProvider): + return value + from crewai.types.callback import _dotted_path_to_instance + + resolved = _dotted_path_to_instance(value) + if resolved is None or isinstance(resolved, InputProvider): + return resolved + raise ValueError( + f"Resolved input_provider {resolved!r} does not implement the " + "InputProvider protocol (missing request_input)." + ) + + +def _serialize_input_provider(value: Any) -> str | None: + if value is None: + return None + from crewai.types.callback import _instance_to_dotted_path + + return _instance_to_dotted_path(value) + + _INITIAL_STATE_CLASS_MARKER = "__crewai_pydantic_class_schema__" @@ -949,15 +982,30 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta): name: str | None = Field(default=None) tracing: bool | None = Field(default=None) stream: bool = Field(default=False) - memory: Memory | MemoryScope | MemorySlice | None = Field(default=None) - input_provider: InputProvider | None = Field(default=None) + memory: Annotated[ + Annotated[ + Memory | MemoryScope | MemorySlice, Field(discriminator="memory_kind") + ] + | None, + BeforeValidator(_ensure_memory_kind), + ] = Field(default=None) + input_provider: Annotated[ + InputProvider | None, + BeforeValidator(_validate_input_provider), + PlainSerializer( + _serialize_input_provider, return_type=str | None, when_used="json" + ), + ] = Field(default=None) suppress_flow_events: bool = Field(default=False) human_feedback_history: list[HumanFeedbackResult] = Field(default_factory=list) last_human_feedback: HumanFeedbackResult | None = Field(default=None) persistence: Annotated[ - SerializeAsAny[FlowPersistence] | Any, + SerializeAsAny[FlowPersistence] | None, BeforeValidator(lambda v, _: _resolve_persistence(v)), + PlainSerializer( + _serialize_persistence, return_type=dict | None, when_used="json" + ), ] = Field(default=None) max_method_calls: int = Field(default=100) @@ -1050,6 +1098,11 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta): } if self.checkpoint_state is not None: self._restore_state(self.checkpoint_state) + if ( + isinstance(self.memory, MemoryScope | MemorySlice) + and self.memory._memory is None + ): + self.memory.bind(Memory()) restore_event_scope(()) reset_last_event_id() diff --git a/lib/crewai/src/crewai/knowledge/knowledge.py b/lib/crewai/src/crewai/knowledge/knowledge.py index eceef8b99..8dcf38f4e 100644 --- a/lib/crewai/src/crewai/knowledge/knowledge.py +++ b/lib/crewai/src/crewai/knowledge/knowledge.py @@ -1,16 +1,89 @@ import os +from typing import Annotated, Any -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, PlainSerializer from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource +from crewai.knowledge.source.crew_docling_source import CrewDoclingSource +from crewai.knowledge.source.csv_knowledge_source import CSVKnowledgeSource +from crewai.knowledge.source.excel_knowledge_source import ExcelKnowledgeSource +from crewai.knowledge.source.json_knowledge_source import JSONKnowledgeSource +from crewai.knowledge.source.pdf_knowledge_source import PDFKnowledgeSource +from crewai.knowledge.source.string_knowledge_source import StringKnowledgeSource +from crewai.knowledge.source.text_file_knowledge_source import ( + TextFileKnowledgeSource, +) from crewai.knowledge.storage.knowledge_storage import KnowledgeStorage +from crewai.rag.core.base_embeddings_provider import BaseEmbeddingsProvider from crewai.rag.embeddings.types import EmbedderConfig from crewai.rag.types import SearchResult +_KNOWN_SOURCES: dict[str, type[BaseKnowledgeSource]] = { + "string": StringKnowledgeSource, + "docling": CrewDoclingSource, + "csv": CSVKnowledgeSource, + "excel": ExcelKnowledgeSource, + "json": JSONKnowledgeSource, + "pdf": PDFKnowledgeSource, + "text_file": TextFileKnowledgeSource, +} + + +def _resolve_knowledge_sources(value: Any) -> Any: + """Coerce list of dicts into typed BaseKnowledgeSource subclasses via source_type. + + Pass-through for anything else (existing instances, mocks). + """ + if not isinstance(value, list): + return value + resolved: list[Any] = [] + for idx, item in enumerate(value): + if isinstance(item, dict): + tag = item.get("source_type") + if not isinstance(tag, str): + resolved.append(item) + continue + cls = _KNOWN_SOURCES.get(tag) + if cls is None: + raise ValueError( + f"Unknown source_type={tag!r} at index {idx}: " + f"expected one of {sorted(_KNOWN_SOURCES)}" + ) + try: + resolved.append(cls.model_validate(item)) + except Exception as exc: + raise ValueError( + f"Failed to validate knowledge source at index {idx} " + f"with source_type={tag!r}: {exc}" + ) from exc + else: + resolved.append(item) + return resolved + + os.environ["TOKENIZERS_PARALLELISM"] = "false" # removes logging from fastembed +def _serialize_embedder_spec(value: Any) -> dict[str, Any] | None: + if value is None: + return None + if isinstance(value, BaseEmbeddingsProvider): + return value.model_dump(mode="json") + if isinstance(value, dict): + return value + if isinstance(value, type) and issubclass(value, BaseEmbeddingsProvider): + raise TypeError( + f"Cannot checkpoint embedder class {value.__module__}.{value.__qualname__}: " + "build_embedder requires an instance or ProviderSpec dict, not a class. " + "Instantiate the provider before assigning it to Knowledge.embedder." + ) + raise TypeError( + f"Cannot serialize embedder of type {type(value).__name__}: " + "expected ProviderSpec dict or BaseEmbeddingsProvider instance." + ) + + class Knowledge(BaseModel): """ Knowledge is a collection of sources and setup for the vector store to save and query relevant context. @@ -20,10 +93,18 @@ class Knowledge(BaseModel): embedder: EmbedderConfig | None = None """ - sources: list[BaseKnowledgeSource] = Field(default_factory=list) + sources: Annotated[ + list[BaseKnowledgeSource], + BeforeValidator(_resolve_knowledge_sources), + ] = Field(default_factory=list) model_config = ConfigDict(arbitrary_types_allowed=True) storage: KnowledgeStorage | None = Field(default=None) - embedder: EmbedderConfig | None = None + embedder: Annotated[ + EmbedderConfig | None, + PlainSerializer( + _serialize_embedder_spec, return_type=dict | None, when_used="json" + ), + ] = None collection_name: str | None = None def __init__( diff --git a/lib/crewai/src/crewai/knowledge/source/base_knowledge_source.py b/lib/crewai/src/crewai/knowledge/source/base_knowledge_source.py index 4f4a53fb0..8c99b47b0 100644 --- a/lib/crewai/src/crewai/knowledge/source/base_knowledge_source.py +++ b/lib/crewai/src/crewai/knowledge/source/base_knowledge_source.py @@ -13,7 +13,9 @@ class BaseKnowledgeSource(BaseModel, ABC): chunk_size: int = 4000 chunk_overlap: int = 200 chunks: list[str] = Field(default_factory=list) - chunk_embeddings: list[np.ndarray[Any, np.dtype[Any]]] = Field(default_factory=list) + chunk_embeddings: list[np.ndarray[Any, np.dtype[Any]]] = Field( + default_factory=list, exclude=True + ) model_config = ConfigDict(arbitrary_types_allowed=True) storage: KnowledgeStorage | None = Field(default=None) diff --git a/lib/crewai/src/crewai/knowledge/source/crew_docling_source.py b/lib/crewai/src/crewai/knowledge/source/crew_docling_source.py index 3dddacfac..42d69049b 100644 --- a/lib/crewai/src/crewai/knowledge/source/crew_docling_source.py +++ b/lib/crewai/src/crewai/knowledge/source/crew_docling_source.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Iterator from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal from urllib.parse import urlparse @@ -45,6 +45,7 @@ class CrewDoclingSource(BaseKnowledgeSource): _logger: Logger = Logger(verbose=True) + source_type: Literal["docling"] = "docling" file_path: list[Path | str] | None = Field(default=None) file_paths: list[Path | str] = Field(default_factory=list) chunks: list[str] = Field(default_factory=list) diff --git a/lib/crewai/src/crewai/knowledge/source/csv_knowledge_source.py b/lib/crewai/src/crewai/knowledge/source/csv_knowledge_source.py index 7da82c3e3..8a87c6fb3 100644 --- a/lib/crewai/src/crewai/knowledge/source/csv_knowledge_source.py +++ b/lib/crewai/src/crewai/knowledge/source/csv_knowledge_source.py @@ -1,5 +1,6 @@ import csv from pathlib import Path +from typing import Literal from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledgeSource @@ -7,6 +8,8 @@ from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledge class CSVKnowledgeSource(BaseFileKnowledgeSource): """A knowledge source that stores and queries CSV file content using embeddings.""" + source_type: Literal["csv"] = "csv" + def load_content(self) -> dict[Path, str]: """Load and preprocess CSV file content.""" content_dict = {} diff --git a/lib/crewai/src/crewai/knowledge/source/excel_knowledge_source.py b/lib/crewai/src/crewai/knowledge/source/excel_knowledge_source.py index ece582053..2e492019f 100644 --- a/lib/crewai/src/crewai/knowledge/source/excel_knowledge_source.py +++ b/lib/crewai/src/crewai/knowledge/source/excel_knowledge_source.py @@ -1,6 +1,6 @@ from pathlib import Path from types import ModuleType -from typing import Any +from typing import Any, Literal from pydantic import Field, field_validator @@ -16,6 +16,7 @@ class ExcelKnowledgeSource(BaseKnowledgeSource): _logger: Logger = Logger(verbose=True) + source_type: Literal["excel"] = "excel" file_path: Path | list[Path] | str | list[str] | None = Field( default=None, description="[Deprecated] The path to the file. Use file_paths instead.", diff --git a/lib/crewai/src/crewai/knowledge/source/json_knowledge_source.py b/lib/crewai/src/crewai/knowledge/source/json_knowledge_source.py index ac527af2d..e547f318b 100644 --- a/lib/crewai/src/crewai/knowledge/source/json_knowledge_source.py +++ b/lib/crewai/src/crewai/knowledge/source/json_knowledge_source.py @@ -1,6 +1,6 @@ import json from pathlib import Path -from typing import Any +from typing import Any, Literal from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledgeSource @@ -8,6 +8,8 @@ from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledge class JSONKnowledgeSource(BaseFileKnowledgeSource): """A knowledge source that stores and queries JSON file content using embeddings.""" + source_type: Literal["json"] = "json" + def load_content(self) -> dict[Path, str]: """Load and preprocess JSON file content.""" content: dict[Path, str] = {} diff --git a/lib/crewai/src/crewai/knowledge/source/pdf_knowledge_source.py b/lib/crewai/src/crewai/knowledge/source/pdf_knowledge_source.py index 8af860875..733513aea 100644 --- a/lib/crewai/src/crewai/knowledge/source/pdf_knowledge_source.py +++ b/lib/crewai/src/crewai/knowledge/source/pdf_knowledge_source.py @@ -1,5 +1,6 @@ from pathlib import Path from types import ModuleType +from typing import Literal from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledgeSource @@ -7,6 +8,8 @@ from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledge class PDFKnowledgeSource(BaseFileKnowledgeSource): """A knowledge source that stores and queries PDF file content using embeddings.""" + source_type: Literal["pdf"] = "pdf" + def load_content(self) -> dict[Path, str]: """Load and preprocess PDF file content.""" pdfplumber = self._import_pdfplumber() diff --git a/lib/crewai/src/crewai/knowledge/source/string_knowledge_source.py b/lib/crewai/src/crewai/knowledge/source/string_knowledge_source.py index b1165c2d1..639ae98bd 100644 --- a/lib/crewai/src/crewai/knowledge/source/string_knowledge_source.py +++ b/lib/crewai/src/crewai/knowledge/source/string_knowledge_source.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Literal from pydantic import Field @@ -8,6 +8,7 @@ from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource class StringKnowledgeSource(BaseKnowledgeSource): """A knowledge source that stores and queries plain text content using embeddings.""" + source_type: Literal["string"] = "string" content: str = Field(...) collection_name: str | None = Field(default=None) diff --git a/lib/crewai/src/crewai/knowledge/source/text_file_knowledge_source.py b/lib/crewai/src/crewai/knowledge/source/text_file_knowledge_source.py index 00265743d..5e88da46f 100644 --- a/lib/crewai/src/crewai/knowledge/source/text_file_knowledge_source.py +++ b/lib/crewai/src/crewai/knowledge/source/text_file_knowledge_source.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import Literal from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledgeSource @@ -6,6 +7,8 @@ from crewai.knowledge.source.base_file_knowledge_source import BaseFileKnowledge class TextFileKnowledgeSource(BaseFileKnowledgeSource): """A knowledge source that stores and queries text file content using embeddings.""" + source_type: Literal["text_file"] = "text_file" + def load_content(self) -> dict[Path, str]: """Load and preprocess text file content.""" content = {} diff --git a/lib/crewai/src/crewai/memory/memory_scope.py b/lib/crewai/src/crewai/memory/memory_scope.py index b5418e03f..1cd09d476 100644 --- a/lib/crewai/src/crewai/memory/memory_scope.py +++ b/lib/crewai/src/crewai/memory/memory_scope.py @@ -6,6 +6,7 @@ from datetime import datetime from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator +from typing_extensions import Self from crewai.memory.types import ( _RECALL_OVERSAMPLE_FACTOR, @@ -16,15 +17,35 @@ from crewai.memory.types import ( from crewai.memory.unified_memory import Memory +def _ensure_memory_kind(value: Any) -> Any: + """Backfill ``memory_kind`` on legacy dicts that predate the discriminator. + + Lets pre-1.14.6 configs/checkpoints flow into the discriminated + ``Memory | MemoryScope | MemorySlice`` union without crashing. Inference: + ``scopes`` key → ``slice``; ``root_path`` → ``scope``; else ``memory``. + Pass-through for non-dict values (instances, ``bool``, ``None``). + """ + if isinstance(value, dict) and "memory_kind" not in value: + if "scopes" in value: + value["memory_kind"] = "slice" + elif "root_path" in value: + value["memory_kind"] = "scope" + else: + value["memory_kind"] = "memory" + return value + + class MemoryScope(BaseModel): """View of Memory restricted to a root path. All operations are scoped under that path.""" model_config = ConfigDict(arbitrary_types_allowed=True) + memory_kind: Literal["scope"] = "scope" + root_path: str = Field(default="/") - _memory: Memory = PrivateAttr() - _root: str = PrivateAttr() + _memory: Memory | None = PrivateAttr(default=None) + _root: str = PrivateAttr(default="") @model_validator(mode="wrap") @classmethod @@ -34,21 +55,38 @@ class MemoryScope(BaseModel): return data if not isinstance(data, dict): raise ValueError(f"Expected dict or MemoryScope, got {type(data).__name__}") - if "memory" not in data: - raise ValueError("MemoryScope requires a 'memory' key") - memory = data.pop("memory") + memory = data.pop("memory", None) instance: MemoryScope = handler(data) - instance._memory = memory + if memory is not None: + instance._memory = memory root = instance.root_path.rstrip("/") or "" if root and not root.startswith("/"): root = "/" + root instance._root = root return instance + def bind(self, memory: Memory) -> Self: + """Rebind the runtime ``Memory`` dependency after restore. + + Required after deserializing from a checkpoint, since the live + ``Memory`` cannot be serialized. + """ + self._memory = memory + return self + + def _require_memory(self) -> Memory: + """Return the bound ``Memory`` or raise a clear error if missing.""" + if self._memory is None: + raise RuntimeError( + "MemoryScope is not bound to a Memory; call .bind(memory) " + "after restore." + ) + return self._memory + @property def read_only(self) -> bool: """Whether the underlying memory is read-only.""" - return self._memory.read_only + return self._require_memory().read_only def _scope_path(self, scope: str | None) -> str: if not scope or scope == "/": @@ -73,7 +111,7 @@ class MemoryScope(BaseModel): ) -> MemoryRecord | None: """Remember content; scope is relative to this scope's root.""" path = self._scope_path(scope) - return self._memory.remember( + return self._require_memory().remember( content, scope=path, categories=categories, @@ -96,7 +134,7 @@ class MemoryScope(BaseModel): ) -> list[MemoryRecord]: """Remember multiple items; scope is relative to this scope's root.""" path = self._scope_path(scope) - return self._memory.remember_many( + return self._require_memory().remember_many( contents, scope=path, categories=categories, @@ -119,7 +157,7 @@ class MemoryScope(BaseModel): ) -> list[MemoryMatch]: """Recall within this scope (root path and below).""" search_scope = self._scope_path(scope) if scope else (self._root or "/") - return self._memory.recall( + return self._require_memory().recall( query, scope=search_scope, categories=categories, @@ -131,7 +169,7 @@ class MemoryScope(BaseModel): def extract_memories(self, content: str) -> list[str]: """Extract discrete memories from content; delegates to underlying Memory.""" - return self._memory.extract_memories(content) + return self._require_memory().extract_memories(content) def forget( self, @@ -143,7 +181,7 @@ class MemoryScope(BaseModel): ) -> int: """Forget within this scope.""" prefix = self._scope_path(scope) if scope else (self._root or "/") - return self._memory.forget( + return self._require_memory().forget( scope=prefix, categories=categories, older_than=older_than, @@ -154,27 +192,27 @@ class MemoryScope(BaseModel): def list_scopes(self, path: str = "/") -> list[str]: """List child scopes under path (relative to this scope's root).""" full = self._scope_path(path) - return self._memory.list_scopes(full) + return self._require_memory().list_scopes(full) def info(self, path: str = "/") -> ScopeInfo: """Info for path under this scope.""" full = self._scope_path(path) - return self._memory.info(full) + return self._require_memory().info(full) def tree(self, path: str = "/", max_depth: int = 3) -> str: """Tree under path within this scope.""" full = self._scope_path(path) - return self._memory.tree(full, max_depth=max_depth) + return self._require_memory().tree(full, max_depth=max_depth) def list_categories(self, path: str | None = None) -> dict[str, int]: """Categories in this scope; path None means this scope root.""" full = self._scope_path(path) if path else (self._root or "/") - return self._memory.list_categories(full) + return self._require_memory().list_categories(full) def reset(self, scope: str | None = None) -> None: """Reset within this scope.""" prefix = self._scope_path(scope) if scope else (self._root or "/") - self._memory.reset(scope=prefix) + self._require_memory().reset(scope=prefix) def subscope(self, path: str) -> MemoryScope: """Return a narrower scope under this scope.""" @@ -191,11 +229,13 @@ class MemorySlice(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) + memory_kind: Literal["slice"] = "slice" + scopes: list[str] = Field(default_factory=list) categories: list[str] | None = Field(default=None) read_only: bool = Field(default=True) - _memory: Memory = PrivateAttr() + _memory: Memory | None = PrivateAttr(default=None) @model_validator(mode="wrap") @classmethod @@ -205,14 +245,27 @@ class MemorySlice(BaseModel): return data if not isinstance(data, dict): raise ValueError(f"Expected dict or MemorySlice, got {type(data).__name__}") - if "memory" not in data: - raise ValueError("MemorySlice requires a 'memory' key") - memory = data.pop("memory") + memory = data.pop("memory", None) data["scopes"] = [s.rstrip("/") or "/" for s in data.get("scopes", [])] instance: MemorySlice = handler(data) - instance._memory = memory + if memory is not None: + instance._memory = memory return instance + def bind(self, memory: Memory) -> Self: + """Rebind the runtime ``Memory`` dependency after restore.""" + self._memory = memory + return self + + def _require_memory(self) -> Memory: + """Return the bound ``Memory`` or raise a clear error if missing.""" + if self._memory is None: + raise RuntimeError( + "MemorySlice is not bound to a Memory; call .bind(memory) " + "after restore." + ) + return self._memory + def remember( self, content: str, @@ -226,7 +279,7 @@ class MemorySlice(BaseModel): """Remember into an explicit scope. No-op when read_only=True.""" if self.read_only: return None - return self._memory.remember( + return self._require_memory().remember( content, scope=scope, categories=categories, @@ -250,7 +303,7 @@ class MemorySlice(BaseModel): cats = categories or self.categories all_matches: list[MemoryMatch] = [] for sc in self.scopes: - matches = self._memory.recall( + matches = self._require_memory().recall( query, scope=sc, categories=cats, @@ -272,14 +325,14 @@ class MemorySlice(BaseModel): def extract_memories(self, content: str) -> list[str]: """Extract discrete memories from content; delegates to underlying Memory.""" - return self._memory.extract_memories(content) + return self._require_memory().extract_memories(content) def list_scopes(self, path: str = "/") -> list[str]: """List scopes across all slice roots.""" out: list[str] = [] for sc in self.scopes: full = f"{sc.rstrip('/')}{path}" if sc != "/" else path - out.extend(self._memory.list_scopes(full)) + out.extend(self._require_memory().list_scopes(full)) return sorted(set(out)) def info(self, path: str = "/") -> ScopeInfo: @@ -291,7 +344,7 @@ class MemorySlice(BaseModel): children: list[str] = [] for sc in self.scopes: full = f"{sc.rstrip('/')}{path}" if sc != "/" else path - inf = self._memory.info(full) + inf = self._require_memory().info(full) total_records += inf.record_count all_categories.update(inf.categories) if inf.oldest_record: @@ -321,6 +374,6 @@ class MemorySlice(BaseModel): counts: dict[str, int] = {} for sc in self.scopes: full = (f"{sc.rstrip('/')}{path}" if sc != "/" else path) if path else sc - for k, v in self._memory.list_categories(full).items(): + for k, v in self._require_memory().list_categories(full).items(): counts[k] = counts.get(k, 0) + v return counts diff --git a/lib/crewai/src/crewai/memory/unified_memory.py b/lib/crewai/src/crewai/memory/unified_memory.py index d879bace0..27a2c109d 100644 --- a/lib/crewai/src/crewai/memory/unified_memory.py +++ b/lib/crewai/src/crewai/memory/unified_memory.py @@ -63,6 +63,8 @@ class Memory(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) + memory_kind: Literal["memory"] = "memory" + llm: Annotated[BaseLLM | str, PlainValidator(_passthrough)] = Field( default="gpt-4o-mini", description="LLM for analysis (model name or BaseLLM instance).", diff --git a/lib/crewai/src/crewai/state/runtime.py b/lib/crewai/src/crewai/state/runtime.py index 2662266d2..59c3171d9 100644 --- a/lib/crewai/src/crewai/state/runtime.py +++ b/lib/crewai/src/crewai/state/runtime.py @@ -113,12 +113,68 @@ def _migrate(data: dict[str, Any]) -> dict[str, Any]: ) # --- migrations in version order --- - # if stored < Version("X.Y.Z"): - # data.setdefault("some_field", "default") + if stored < Version("1.14.6"): + for entity in data.get("entities") or []: + _backfill_discriminators(entity) return data +def _backfill_memory_kind(value: Any) -> None: + """Infer ``memory_kind`` from structural fields on legacy memory dicts.""" + if not isinstance(value, dict) or "memory_kind" in value: + return + if "scopes" in value: + value["memory_kind"] = "slice" + elif "root_path" in value: + value["memory_kind"] = "scope" + else: + value["memory_kind"] = "memory" + + +def _backfill_source_type(source: Any) -> None: + """Infer ``source_type`` for legacy knowledge source dicts when possible. + + Only StringKnowledgeSource is reliably inferrable: it stores ``content`` + as a plain string. File-based sources (CSV/PDF/Excel/JSON/docling) also + have a ``content`` field but populate it with dicts/lists, so we leave + those untagged and let downstream validation surface a clear error. + """ + if not isinstance(source, dict) or "source_type" in source: + return + if isinstance(source.get("content"), str): + source["source_type"] = "string" + return + raise ValueError( + "Legacy knowledge source is missing 'source_type' and could not be " + "inferred during migration. Re-checkpoint after upgrading to 1.14.6+." + ) + + +def _backfill_sources_on(container: Any) -> None: + """Apply source_type backfill to ``sources`` and ``knowledge_sources`` lists.""" + if not isinstance(container, dict): + return + for key in ("sources", "knowledge_sources"): + for src in container.get(key) or []: + _backfill_source_type(src) + + +def _backfill_discriminators(entity: Any) -> None: + """Walk an entity dict and backfill discriminator fields added in 1.14.6.""" + if not isinstance(entity, dict): + return + _backfill_memory_kind(entity.get("memory")) + _backfill_sources_on(entity) + _backfill_sources_on(entity.get("knowledge")) + for agent in entity.get("agents") or []: + if not isinstance(agent, dict): + continue + _backfill_memory_kind(agent.get("memory")) + _backfill_sources_on(agent) + _backfill_sources_on(agent.get("knowledge")) + + class RuntimeState(RootModel): # type: ignore[type-arg] root: list[Entity] _provider: BaseProvider = PrivateAttr(default_factory=JsonProvider) diff --git a/lib/crewai/src/crewai/types/callback.py b/lib/crewai/src/crewai/types/callback.py index 2a8be235e..ea89effdb 100644 --- a/lib/crewai/src/crewai/types/callback.py +++ b/lib/crewai/src/crewai/types/callback.py @@ -19,6 +19,15 @@ from pydantic import BeforeValidator, WithJsonSchema from pydantic.functional_serializers import PlainSerializer +_TRUSTED_DESERIALIZE_VALUES = frozenset({"1", "true", "yes"}) + + +def _trusted_deserialize() -> bool: + """Return True only if ``CREWAI_DESERIALIZE_CALLBACKS`` is an explicit yes.""" + raw = os.environ.get("CREWAI_DESERIALIZE_CALLBACKS", "") + return raw.strip().lower() in _TRUSTED_DESERIALIZE_VALUES + + def _is_non_roundtrippable(fn: object) -> bool: """Return ``True`` if *fn* cannot survive a serialize/deserialize round-trip. @@ -76,7 +85,7 @@ def string_to_callable(value: Any) -> Callable[..., Any]: raise ValueError( f"Invalid callback path {value!r}: expected 'module.name' format" ) - if not os.environ.get("CREWAI_DESERIALIZE_CALLBACKS"): + if not _trusted_deserialize(): raise ValueError( f"Refusing to resolve callback path {value!r}: " "set CREWAI_DESERIALIZE_CALLBACKS=1 to allow. " @@ -150,3 +159,78 @@ SerializableCallable = Annotated[ PlainSerializer(callable_to_string, return_type=str, when_used="json"), WithJsonSchema({"type": "string"}), ] + + +def _instance_to_dotted_path(value: Any) -> str: + """Serialize an instance to a dotted path naming its class.""" + if inspect.isclass(value): + module = getattr(value, "__module__", "") + qualname = getattr( + value, "__qualname__", getattr(value, "__name__", str(type(value))) + ) + raise ValueError(f"Expected an instance, got class {module}.{qualname}.") + cls = type(value) + if cls.__module__ == "builtins": + raise ValueError( + f"Cannot serialize {value!r}: builtin values are not " + "checkpointable instances." + ) + module = getattr(cls, "__module__", None) + qualname = getattr(cls, "__qualname__", None) + if module is None or qualname is None: + raise ValueError( + f"Cannot serialize {value!r}: class missing __module__ or __qualname__. " + "Use a module-level class for checkpointable instances." + ) + if qualname.endswith("") or "" in qualname: + raise ValueError( + f"Cannot serialize {value!r}: class defined in . " + "Use a module-level class for checkpointable instances." + ) + return f"{module}.{qualname}" + + +def _dotted_path_to_instance(value: Any) -> Any: + """Resolve a dotted path to a class and instantiate it with no args. + + If *value* is already a non-string object it is returned as-is. + """ + if value is None: + return value + if not isinstance(value, str): + if inspect.isclass(value): + raise ValueError( + f"Expected an instance or dotted path string, got class " + f"{getattr(value, '__module__', '')}." + f"{getattr(value, '__qualname__', getattr(value, '__name__', ''))}." + ) + if type(value).__module__ == "builtins": + raise ValueError( + f"Expected an instance of a user-defined class or dotted " + f"path string, got builtin value {value!r}." + ) + return value + if "." not in value: + raise ValueError( + f"Invalid provider path {value!r}: expected 'module.name' format" + ) + if not _trusted_deserialize(): + raise ValueError( + f"Refusing to resolve provider path {value!r}: " + "set CREWAI_DESERIALIZE_CALLBACKS=1 to allow. " + "Only enable this for trusted checkpoint data." + ) + cls = _resolve_dotted_path(value) + if not inspect.isclass(cls): + raise ValueError( + f"Invalid provider path {value!r}: expected a class, got " + f"{type(cls).__name__}" + ) + try: + return cls() + except TypeError as exc: + raise ValueError( + f"Cannot reinstantiate {value!r} with no arguments: {exc}. " + "Only no-arg constructors are checkpointable; rebuild the " + "instance manually and assign it after restore." + ) from exc diff --git a/lib/crewai/src/crewai/utilities/reset_memories.py b/lib/crewai/src/crewai/utilities/reset_memories.py index 50d4a633e..e8239b83d 100644 --- a/lib/crewai/src/crewai/utilities/reset_memories.py +++ b/lib/crewai/src/crewai/utilities/reset_memories.py @@ -25,10 +25,16 @@ def _reset_flow_memory(flow: Flow[Any]) -> None: try: if hasattr(mem, "reset"): mem.reset() - elif hasattr(mem, "_memory") and hasattr(mem._memory, "reset"): + elif hasattr(mem, "_memory") and mem._memory is not None: mem._memory.reset() - except (FileNotFoundError, OSError): + except FileNotFoundError: + # Storage directory was never created — nothing to reset. pass + except OSError as exc: + click.echo(f"Memory reset skipped: storage I/O error ({exc}).", err=True) + except RuntimeError as exc: + # Restored MemoryScope/MemorySlice without a rebound Memory. + click.echo(f"Memory reset skipped: {exc}", err=True) def reset_memories_command( diff --git a/lib/crewai/tests/test_flow_ask.py b/lib/crewai/tests/test_flow_ask.py index d198e261c..5ba3729df 100644 --- a/lib/crewai/tests/test_flow_ask.py +++ b/lib/crewai/tests/test_flow_ask.py @@ -7,20 +7,87 @@ durability, input history tracking, and integration with flow machinery. from __future__ import annotations +import copy import time from datetime import datetime from typing import Any from unittest.mock import MagicMock, patch +from pydantic import BaseModel + from crewai.flow import Flow, flow_config, listen, start from crewai.flow.async_feedback.providers import ConsoleProvider from crewai.flow.flow import FlowState from crewai.flow.input_provider import InputProvider, InputResponse +from crewai.flow.persistence.base import FlowPersistence # ── Test helpers ───────────────────────────────────────────────── +class _SaveCall: + """Lightweight stand-in for ``MagicMock.call_args`` entries.""" + + __slots__ = ("args", "kwargs") + + def __init__(self, args: tuple[Any, ...], kwargs: dict[str, Any]) -> None: + self.args = args + self.kwargs = kwargs + + +class _SaveStateRecorder: + """Callable that records each ``save_state`` invocation.""" + + def __init__(self, owner: RecordingPersistence) -> None: + self._owner = owner + self.call_args_list: list[_SaveCall] = [] + + def __call__( + self, + flow_uuid: str, + method_name: str, + state_data: dict[str, Any] | BaseModel, + ) -> None: + snapshot: dict[str, Any] | BaseModel + if isinstance(state_data, BaseModel): + snapshot = state_data.model_copy(deep=True) + else: + snapshot = copy.deepcopy(state_data) + self.call_args_list.append( + _SaveCall((flow_uuid, method_name, snapshot), {}) + ) + self._owner._states[flow_uuid] = snapshot + + +class RecordingPersistence(FlowPersistence): + """In-memory FlowPersistence that records ``save_state`` invocations.""" + + persistence_type: str = "RecordingPersistence" + + def model_post_init(self, _: Any) -> None: + object.__setattr__(self, "_states", {}) + object.__setattr__(self, "save_state", _SaveStateRecorder(self)) + + def init_db(self) -> None: + return None + + def save_state( # type: ignore[no-redef] + self, + flow_uuid: str, + method_name: str, + state_data: dict[str, Any] | BaseModel, + ) -> None: + return None + + def load_state(self, flow_uuid: str) -> dict[str, Any] | None: + snapshot = self._states.get(flow_uuid) + if snapshot is None: + return None + if isinstance(snapshot, BaseModel): + return snapshot.model_copy(deep=True).model_dump() + return copy.deepcopy(snapshot) + + class MockInputProvider: """Mock input provider that returns pre-configured responses.""" @@ -436,8 +503,7 @@ class TestAskCheckpoint: def test_ask_checkpoints_state_before_waiting(self) -> None: """State is saved to persistence before waiting for input.""" - mock_persistence = MagicMock() - mock_persistence.load_state.return_value = None + mock_persistence = RecordingPersistence() class TestFlow(Flow): input_provider = MockInputProvider(["answer"]) @@ -480,8 +546,7 @@ class TestAskCheckpoint: server crashes while waiting for input, previously gathered data is safe. """ - mock_persistence = MagicMock() - mock_persistence.load_state.return_value = None + mock_persistence = RecordingPersistence() class GatherFlow(Flow): input_provider = MockInputProvider(["AI", "detailed"]) @@ -678,8 +743,7 @@ class TestAskIntegration: def test_ask_with_state_persistence_recovery(self) -> None: """Ask checkpoints state so previously gathered values survive.""" - mock_persistence = MagicMock() - mock_persistence.load_state.return_value = None + mock_persistence = RecordingPersistence() class RecoverableFlow(Flow): input_provider = MockInputProvider(["AI", "detailed"]) From 81c21e3166f375f4b8c26f24125ab4299c44e19d Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Thu, 21 May 2026 15:09:48 +0800 Subject: [PATCH 2/4] feat: bump versions to 1.14.6a1 --- lib/cli/pyproject.toml | 2 +- lib/cli/src/crewai_cli/__init__.py | 2 +- lib/crewai-core/src/crewai_core/__init__.py | 2 +- 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 +- lib/devtools/src/crewai_devtools/__init__.py | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/cli/pyproject.toml b/lib/cli/pyproject.toml index d73b02d1c..2a001eb6c 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.5", + "crewai-core==1.14.6a1", "click~=8.1.7", "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 dd8fc4166..376b6fff0 100644 --- a/lib/cli/src/crewai_cli/__init__.py +++ b/lib/cli/src/crewai_cli/__init__.py @@ -1 +1 @@ -__version__ = "1.14.5" +__version__ = "1.14.6a1" diff --git a/lib/crewai-core/src/crewai_core/__init__.py b/lib/crewai-core/src/crewai_core/__init__.py index dd8fc4166..376b6fff0 100644 --- a/lib/crewai-core/src/crewai_core/__init__.py +++ b/lib/crewai-core/src/crewai_core/__init__.py @@ -1 +1 @@ -__version__ = "1.14.5" +__version__ = "1.14.6a1" diff --git a/lib/crewai-files/src/crewai_files/__init__.py b/lib/crewai-files/src/crewai_files/__init__.py index e2dc8c161..78ec8f965 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.5" +__version__ = "1.14.6a1" diff --git a/lib/crewai-tools/pyproject.toml b/lib/crewai-tools/pyproject.toml index 7f8e760f2..bf76bd137 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.5", + "crewai==1.14.6a1", "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 ff76b9401..22f82c74d 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.5" +__version__ = "1.14.6a1" diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index 50f033dad..602cfadee 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.5", - "crewai-cli==1.14.5", + "crewai-core==1.14.6a1", + "crewai-cli==1.14.6a1", # Core Dependencies "pydantic>=2.11.9,<2.13", "openai>=2.30.0,<3", @@ -54,7 +54,7 @@ Repository = "https://github.com/crewAIInc/crewAI" [project.optional-dependencies] tools = [ - "crewai-tools==1.14.5", + "crewai-tools==1.14.6a1", ] embeddings = [ "tiktoken>=0.8.0,<0.13" diff --git a/lib/crewai/src/crewai/__init__.py b/lib/crewai/src/crewai/__init__.py index b6471293d..01ef5223a 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.5" +__version__ = "1.14.6a1" _LAZY_IMPORTS: dict[str, tuple[str, str]] = { "Memory": ("crewai.memory.unified_memory", "Memory"), diff --git a/lib/devtools/src/crewai_devtools/__init__.py b/lib/devtools/src/crewai_devtools/__init__.py index ad86cb649..2852aeaf5 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.5" +__version__ = "1.14.6a1" From d3e20900e8b3ffdd7d1ad8b713cd910be00f1170 Mon Sep 17 00:00:00 2001 From: Greyson LaLonde Date: Thu, 21 May 2026 21:27:13 +0800 Subject: [PATCH 3/4] docs: update changelog and version for v1.14.6a1 --- docs/ar/changelog.mdx | 25 +++++++++++++++++++++++++ docs/en/changelog.mdx | 25 +++++++++++++++++++++++++ docs/ko/changelog.mdx | 25 +++++++++++++++++++++++++ docs/pt-BR/changelog.mdx | 25 +++++++++++++++++++++++++ 4 files changed, 100 insertions(+) diff --git a/docs/ar/changelog.mdx b/docs/ar/changelog.mdx index 18395ef64..8cf648036 100644 --- a/docs/ar/changelog.mdx +++ b/docs/ar/changelog.mdx @@ -4,6 +4,31 @@ description: "تحديثات المنتج والتحسينات وإصلاحات icon: "clock" mode: "wide" --- + + ## v1.14.6a1 + + [عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.6a1) + + ## ما الذي تغير + + ### الميزات + - إضافة مستودع المهارات مع التسجيل، التخزين المؤقت، واجهة سطر الأوامر، وتكامل SDK + - توليد ملاحظات إصدار مصنفة للمؤسسات + + ### إصلاحات الأخطاء + - تعزيز تسلسل حالة وقت التشغيل عبر حقول الكيان + - تحديث idna إلى 3.15 لمعالجة مشكلة الأمان GHSA-65pc-fj4g-8rjx + - إزالة تعبيرات JSX `{" "}` التي تعطل عرض `` + + ### الوثائق + - تحديث سجل التغييرات والإصدار لـ v1.14.5 + + ## المساهمون + + @akaKuruma, @alex-clawd, @greysonlalonde + + + ## v1.14.5 diff --git a/docs/en/changelog.mdx b/docs/en/changelog.mdx index 4d02d6227..70b883b63 100644 --- a/docs/en/changelog.mdx +++ b/docs/en/changelog.mdx @@ -4,6 +4,31 @@ description: "Product updates, improvements, and bug fixes for CrewAI" icon: "clock" mode: "wide" --- + + ## v1.14.6a1 + + [View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.6a1) + + ## What's Changed + + ### Features + - Add Skills Repository with registry, cache, CLI, and SDK integration + - Generate categorized release notes for enterprise + + ### Bug Fixes + - Harden RuntimeState serialization across entity fields + - Bump idna to 3.15 to address security issue GHSA-65pc-fj4g-8rjx + - Remove `{" "}` JSX expressions breaking `` render + + ### Documentation + - Update changelog and version for v1.14.5 + + ## Contributors + + @akaKuruma, @alex-clawd, @greysonlalonde + + + ## v1.14.5 diff --git a/docs/ko/changelog.mdx b/docs/ko/changelog.mdx index 84bb37f1d..81936019e 100644 --- a/docs/ko/changelog.mdx +++ b/docs/ko/changelog.mdx @@ -4,6 +4,31 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정" icon: "clock" mode: "wide" --- + + ## v1.14.6a1 + + [GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.6a1) + + ## 변경 사항 + + ### 기능 + - 레지스트리, 캐시, CLI 및 SDK 통합이 포함된 기술 저장소 추가 + - 기업용으로 분류된 릴리스 노트 생성 + + ### 버그 수정 + - 엔티티 필드 전반에 걸쳐 RuntimeState 직렬화 강화 + - 보안 문제 GHSA-65pc-fj4g-8rjx를 해결하기 위해 idna를 3.15로 업데이트 + - `` 렌더링을 방해하는 `{" "}` JSX 표현식 제거 + + ### 문서 + - v1.14.5에 대한 변경 로그 및 버전 업데이트 + + ## 기여자 + + @akaKuruma, @alex-clawd, @greysonlalonde + + + ## v1.14.5 diff --git a/docs/pt-BR/changelog.mdx b/docs/pt-BR/changelog.mdx index a12044d3d..e05e6ccba 100644 --- a/docs/pt-BR/changelog.mdx +++ b/docs/pt-BR/changelog.mdx @@ -4,6 +4,31 @@ description: "Atualizações de produto, melhorias e correções do CrewAI" icon: "clock" mode: "wide" --- + + ## v1.14.6a1 + + [Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.6a1) + + ## O que Mudou + + ### Recursos + - Adicionar Repositório de Habilidades com registro, cache, CLI e integração SDK + - Gerar notas de versão categorizadas para empresas + + ### Correções de Bugs + - Fortalecer a serialização de RuntimeState entre os campos da entidade + - Atualizar idna para 3.15 para resolver problema de segurança GHSA-65pc-fj4g-8rjx + - Remover expressões JSX `{" "}` que quebram a renderização de `` + + ### Documentação + - Atualizar changelog e versão para v1.14.5 + + ## Contribuidores + + @akaKuruma, @alex-clawd, @greysonlalonde + + + ## v1.14.5 From 56b6594669f299e0132bde562f60ff78cb2d889d Mon Sep 17 00:00:00 2001 From: Thiago Moretto <168731+thiagomoretto@users.noreply.github.com> Date: Thu, 21 May 2026 11:57:17 -0300 Subject: [PATCH 4/4] fix(tools): correct mongdb typo to pymongo in package_dependencies (#5891) * fix(tools): correct mongdb typo to pymongo in package_dependencies The `package_dependencies` field in `MongoDBVectorSearchTool` referenced the non-existent package `mongdb` instead of the actual PyPI package `pymongo`, which is the driver imported and used throughout the file. * chore: update tool specifications --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../tools/mongodb_vector_search_tool/vector_search.py | 2 +- lib/crewai-tools/tool.specs.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/crewai-tools/src/crewai_tools/tools/mongodb_vector_search_tool/vector_search.py b/lib/crewai-tools/src/crewai_tools/tools/mongodb_vector_search_tool/vector_search.py index 8bcd99a4f..382576d55 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/mongodb_vector_search_tool/vector_search.py +++ b/lib/crewai-tools/src/crewai_tools/tools/mongodb_vector_search_tool/vector_search.py @@ -103,7 +103,7 @@ class MongoDBVectorSearchTool(BaseTool): ), ] ) - package_dependencies: list[str] = Field(default_factory=lambda: ["mongdb"]) + package_dependencies: list[str] = Field(default_factory=lambda: ["pymongo"]) def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) diff --git a/lib/crewai-tools/tool.specs.json b/lib/crewai-tools/tool.specs.json index c7144e2cc..6f759c5d8 100644 --- a/lib/crewai-tools/tool.specs.json +++ b/lib/crewai-tools/tool.specs.json @@ -14633,7 +14633,7 @@ }, "name": "MongoDBVectorSearchTool", "package_dependencies": [ - "mongdb" + "pymongo" ], "run_params_schema": { "description": "Input for MongoDBTool.",