Files
wehub-resource-sync c889a57b6b
Test Suites / Build CI Environment (push) Has been cancelled
Test Suites / Basic Tests (push) Has been cancelled
Test Suites / End-to-End Tests (push) Has been cancelled
Test Suites / CLI Tests (push) Has been cancelled
Test Suites / Slow End-to-End Tests (push) Has been cancelled
Test Suites / Graph Database Tests (push) Has been cancelled
Test Suites / Vector DB Tests (push) Has been cancelled
Test Suites / Temporal Graph Test (push) Has been cancelled
Test Suites / Search Test on Different DBs (push) Has been cancelled
Test Suites / Example Tests (push) Has been cancelled
Test Suites / Notebook Tests (push) Has been cancelled
Test Suites / OS and Python Tests Ubuntu (push) Has been cancelled
Test Suites / OS and Python Tests Extended (push) Has been cancelled
Test Suites / LLM Test Suite (push) Has been cancelled
Test Suites / S3 File Storage Test (push) Has been cancelled
Test Suites / Run Integration Tests (push) Has been cancelled
Test Suites / MCP Tests (push) Has been cancelled
Test Suites / Docker Compose Test (push) Has been cancelled
Test Suites / Docker CI test (push) Has been cancelled
Test Suites / Relational DB Migration Tests (push) Has been cancelled
Test Suites / Distributed Cognee Test (push) Has been cancelled
Test Suites / DB Examples Tests (push) Has been cancelled
Test Suites / Test Completion Status (push) Has been cancelled
Test Suites / Claude Code Review (push) Has been cancelled
Test Suites / basic checks (push) Has been cancelled
build | Build and Push Cognee MCP Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
build | Build and Push Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.11) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.12) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (kuzu, kuzu) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (neo4j, neo4j) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Examples (push) Has been cancelled
Weighted Edges Tests / Code Quality for Weighted Edges (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:02:24 +08:00

1147 lines
42 KiB
Python

import asyncio
import time
from uuid import UUID
from typing import Union, BinaryIO, List, Optional, Any, Literal
try:
from typing import Unpack
except ImportError:
from typing_extensions import Unpack
from typing_extensions import TypedDict
from cognee.shared.logging_utils import get_logger
from cognee.tasks.ingestion.data_item import DataItem
from cognee.memory import (
MemoryEntry,
QAEntry,
TraceEntry,
FeedbackEntry,
SkillRunEntry,
)
from cognee.memory.entries import MEMORY_ENTRY_TYPES
from cognee.modules.migration.sources.base import MemorySource
from cognee.modules.pipelines.layers.resolve_authorized_user_datasets import (
resolve_authorized_user_datasets,
)
from cognee.modules.observability import (
new_span,
COGNEE_DATASET_NAME,
COGNEE_SESSION_ID,
COGNEE_DATA_SIZE_BYTES,
COGNEE_OPERATION_MODE,
COGNEE_DATA_ITEM_COUNT,
OtelStatusCode,
)
logger = get_logger("remember")
class RememberKwargs(TypedDict, total=False):
"""Power-user overrides for remember(). Most users never need these."""
graph_model: Any
node_set: List[str]
dataset_id: UUID
preferred_loaders: list
incremental_loading: bool
data_cache: bool
data_per_batch: int
chunks_per_batch: int
user: object
vector_db_config: dict
graph_db_config: dict
content_type: Literal["skills"]
skill_improvement: dict[str, Any]
skills_text: str
skill_name: str
primary_key: str
write_disposition: str
query: str
max_rows_per_table: int
llm_config: Any
embedding_config: Any
# Kwarg routing: which RememberKwargs go to add(), cognify(), or both.
# Kept in sync with RememberKwargs above and the add()/cognify() signatures.
_ADD_ONLY = frozenset(
{
"dataset_id",
"node_set",
"preferred_loaders",
"importance_weight",
"primary_key",
"write_disposition",
"query",
"max_rows_per_table",
}
)
_COGNIFY_ONLY = frozenset({"graph_model", "chunks_per_batch", "config", "temporal_cognify"})
_SHARED = frozenset(
{
"user",
"vector_db_config",
"graph_db_config",
"incremental_loading",
"data_cache",
"data_per_batch",
"run_in_background",
"llm_config",
"embedding_config",
}
)
def _estimate_data_size(data) -> int:
"""Estimate the byte size of input data."""
if isinstance(data, str):
return len(data.encode("utf-8", errors="replace"))
if isinstance(data, bytes):
return len(data)
if isinstance(data, list):
return sum(_estimate_data_size(item) for item in data)
if hasattr(data, "seek") and hasattr(data, "tell"):
pos = data.tell()
data.seek(0, 2)
size = data.tell()
data.seek(pos)
return size
return 0
def _data_to_text(data) -> str:
"""Convert ingested data to its full text representation."""
if isinstance(data, str):
return data
if isinstance(data, list):
parts = []
for item in data:
if isinstance(item, str):
parts.append(item)
elif hasattr(item, "name"):
parts.append(f"[file: {item.name}]")
else:
parts.append(f"[{type(item).__name__}]")
return "\n\n".join(parts)
if hasattr(data, "name"):
return f"[file: {data.name}]"
return f"[{type(data).__name__}]"
_SESSION_PLACEHOLDER_PREFIXES = ("[UploadFile]", "[file:", "[BinaryIO", "[SpooledTemporaryFile")
async def _add_to_session(session_id: str, data, user):
"""Add a Q&A entry to the session cache.
Sessions store chat-shaped content (prompts, assistant answers,
Q&A turns). File-upload data coerces to placeholder strings like
``[UploadFile]`` / ``[file: name]`` — those are useless in the
session cache and pollute recall results, so they're skipped.
"""
from cognee.infrastructure.session.get_session_manager import get_session_manager
sm = get_session_manager()
if not sm.is_available:
logger.warning("remember: session cache not available (enable CACHING=true)")
return
user_id = str(user.id) if user and hasattr(user, "id") else None
if not user_id:
return
text = _data_to_text(data)
stripped = text.strip()
if not stripped:
return
if any(stripped.startswith(prefix) for prefix in _SESSION_PLACEHOLDER_PREFIXES):
logger.debug(
"remember: skipping session write for placeholder-only payload (%.40s…)",
stripped,
)
return
await sm.add_qa(
user_id=user_id,
session_id=session_id,
question="",
context="",
answer=text,
)
logger.info("remember: added entry to session '%s'", session_id)
async def _remember_entry(
entry,
*,
dataset_name: str,
session_id: Optional[str],
user,
skill_improvement: Optional[dict[str, Any]] = None,
) -> "RememberResult":
"""Top-level dispatcher for typed MemoryEntry payloads.
Routes to the remote HTTP client when ``cognee.serve(url=...)`` is
active, otherwise runs the SessionManager call in-process.
"""
from cognee.api.v1.serve.state import get_remote_client
client = get_remote_client()
if client is not None:
payload = await client.remember_entry(
entry,
dataset_name=dataset_name,
session_id=session_id,
skill_improvement=skill_improvement,
)
# Reconstruct a RememberResult from the server's response
result = RememberResult(
status=payload.get("status", "session_stored"),
dataset_name=payload.get("dataset_name", dataset_name),
dataset_id=payload.get("dataset_id"),
session_ids=payload.get("session_ids"),
pipeline_run_id=payload.get("pipeline_run_id"),
)
result.entry_type = payload.get("entry_type")
result.entry_id = payload.get("entry_id")
result.elapsed_seconds = payload.get("elapsed_seconds")
result.items_processed = payload.get("items_processed", 0)
result.items = payload.get("items", []) or []
result.content_hash = payload.get("content_hash")
result.raw_result = payload
if payload.get("error"):
result.error = payload["error"]
return result
return await _dispatch_session_entry(
entry,
dataset_name=dataset_name,
session_id=session_id,
user=user,
skill_improvement=skill_improvement,
)
async def _dispatch_session_entry(
entry: "MemoryEntry",
*,
dataset_name: str,
session_id: Optional[str],
user,
skill_improvement: Optional[dict[str, Any]] = None,
) -> "RememberResult":
"""Route a typed memory entry to the right SessionManager method.
Session-backed entries require a session_id. SkillRunEntry is
graph-backed and may be recorded without a session_id. Returns a
RememberResult with entry_id/entry_type fields populated so callers
can chain writes.
"""
if isinstance(entry, SkillRunEntry):
from cognee.modules.tools.skill_runs import remember_skill_run_entry
run, dataset = await remember_skill_run_entry(
entry,
dataset_name=dataset_name,
session_id=session_id,
user=user,
)
result = RememberResult(
status="completed",
dataset_name=dataset.name,
dataset_id=str(dataset.id),
session_ids=[session_id] if session_id else None,
)
result.elapsed_seconds = time.monotonic() - result._started_at
result.entry_type = entry.type
result.entry_id = run.run_id
result.items_processed = 1
result.items = [
{
"kind": "skill_run",
"run_id": run.run_id,
"selected_skill_id": run.selected_skill_id,
"selected_skill_name": run.selected_skill_name,
"success_score": run.success_score,
}
]
if skill_improvement is not None:
from cognee.modules.memify.skill_improvement import improve_skill_from_config
config = dict(skill_improvement)
config.setdefault("skill_name", run.selected_skill_name or entry.selected_skill_id)
proposal = await improve_skill_from_config(config, dataset=dataset, user=user)
if proposal is not None:
result.items.append(
{
"kind": "skill_improvement_proposal",
"proposal_id": proposal.proposal_id,
"skill_name": proposal.skill_name,
"status": proposal.status,
}
)
return result
from cognee.infrastructure.session.get_session_manager import get_session_manager
from cognee.modules.engine.operations.setup import setup
if not session_id:
raise ValueError(
f"session_id is required for typed memory entries (got {type(entry).__name__})"
)
await setup()
if user is None:
from cognee.modules.users.methods import get_default_user
user = await get_default_user()
user_id = str(user.id) if user and hasattr(user, "id") else None
if not user_id:
raise ValueError("Could not resolve user for session entry")
sm = get_session_manager()
if not sm.is_available:
raise RuntimeError("Session cache unavailable — set CACHING=true to enable session memory")
# Resolve the dataset UUID for this session so the session_records
# row carries dataset_id — otherwise downstream permission filtering
# (e.g. the dashboard listing) can't match the session via granted
# dataset reads. We backfill via ensure_and_touch_session, which
# upserts the row and only sets dataset_id when currently null.
try:
from uuid import UUID as _UUID
from cognee.modules.data.methods.get_authorized_dataset import get_authorized_dataset
from cognee.modules.session_lifecycle.metrics import ensure_and_touch_session
resolved_dataset = None
try:
ds = await get_authorized_dataset(user, dataset_name, "write")
if ds is not None:
resolved_dataset = ds.id
except Exception:
# Fall through with None — we still create the session row.
resolved_dataset = None
await ensure_and_touch_session(
session_id=session_id,
user_id=_UUID(user_id),
dataset_id=resolved_dataset,
)
except Exception as exc:
logger.debug("remember: pre-upsert session_record failed (%s)", exc)
result = RememberResult(
status="session_stored",
dataset_name=dataset_name,
session_ids=[session_id],
)
result.elapsed_seconds = time.monotonic() - result._started_at
result.entry_type = entry.type
if isinstance(entry, QAEntry):
qa_id = await sm.add_qa(
user_id=user_id,
session_id=session_id,
question=entry.question,
context=entry.context,
answer=entry.answer,
feedback_text=entry.feedback_text,
feedback_score=entry.feedback_score,
used_graph_element_ids=entry.used_graph_element_ids,
)
result.entry_id = qa_id
if qa_id is None:
result.status = "errored"
result.error = "SessionManager.add_qa returned None"
return result
if isinstance(entry, TraceEntry):
trace_id = await sm.add_agent_trace_step(
user_id=user_id,
session_id=session_id,
origin_function=entry.origin_function,
status=entry.status,
generate_feedback_with_llm=entry.generate_feedback_with_llm,
memory_query=entry.memory_query,
memory_context=entry.memory_context,
method_params=entry.method_params,
method_return_value=entry.method_return_value,
error_message=entry.error_message,
)
result.entry_id = trace_id
if trace_id is None:
result.status = "errored"
result.error = "SessionManager.add_agent_trace_step returned None"
return result
if isinstance(entry, FeedbackEntry):
ok = await sm.add_feedback(
user_id=user_id,
session_id=session_id,
qa_id=entry.qa_id,
feedback_text=entry.feedback_text,
feedback_score=entry.feedback_score,
)
result.entry_id = entry.qa_id
if not ok:
result.status = "errored"
result.error = f"add_feedback: QA {entry.qa_id} not found in session {session_id}"
return result
raise TypeError(f"Unsupported memory entry type: {type(entry).__name__}")
class RememberResult:
"""Promise-like result from ``remember()``.
Can be printed for a quick summary, awaited to block until the
pipeline finishes (background mode), or inspected via attributes.
Attributes:
status: ``"running"``, ``"completed"``, ``"errored"``,
or ``"session_stored"``.
dataset_name: Target dataset.
dataset_id: Dataset UUID (str) when available.
session_id: Session ID (session-only mode).
pipeline_run_id: Pipeline run UUID (str) when available.
error: Error message if the pipeline failed.
elapsed_seconds: Wall-clock time from start to completion.
content_hash: Content hash of the processed data (first item).
items_processed: Number of data items processed.
items: List of dicts with per-item info (name, content_hash,
token_count) for each data item in the pipeline run.
raw_result: The original cognify() return value (dict of
dataset_id -> PipelineRunInfo) for advanced inspection.
Example::
result = await cognee.remember("Einstein was born in Ulm.")
print(result)
# RememberResult(status='completed', dataset='main_dataset',
# items=1, elapsed=4.2s)
result.content_hash # 'a1b2c3...'
result.items # [{'name': '...', 'content_hash': '...', ...}]
# Background mode:
result = await cognee.remember("data", run_in_background=True)
print(result.done) # False
await result # blocks until done
print(result) # status='completed'
"""
def __init__(
self,
*,
status: str,
dataset_name: str,
dataset_id: Optional[str] = None,
session_ids: Optional[List[str]] = None,
pipeline_run_id: Optional[str] = None,
):
self.status = status
self.dataset_name = dataset_name
self.dataset_id = dataset_id
self.session_ids: Optional[List[str]] = session_ids
self.pipeline_run_id = pipeline_run_id
self.error: Optional[str] = None
self.raw_result: Optional[dict] = None
self.elapsed_seconds: Optional[float] = None
self.content_hash: Optional[str] = None
self.items_processed: int = 0
self.items: List[dict] = []
# Populated when the call dispatched a typed MemoryEntry.
# entry_type is one of "qa", "trace", "feedback", or
# "skill_run"; entry_id is the qa_id / trace_id / run_id
# returned by the storage backend.
self.entry_type: Optional[str] = None
self.entry_id: Optional[str] = None
self._task: Optional[asyncio.Task] = None
self._started_at: float = time.monotonic()
@property
def session_id(self) -> Optional[str]:
"""The session ID when exactly one session is involved, else None."""
if self.session_ids and len(self.session_ids) == 1:
return self.session_ids[0]
return None
def __repr__(self):
parts = [f"status={self.status!r}", f"dataset={self.dataset_name!r}"]
if self.session_ids:
if len(self.session_ids) == 1:
parts.append(f"session_id={self.session_ids[0]!r}")
else:
parts.append(f"session_ids={self.session_ids!r}")
if self.dataset_id:
parts.append(f"dataset_id={self.dataset_id!r}")
if self.pipeline_run_id:
parts.append(f"pipeline_run_id={self.pipeline_run_id!r}")
if self.items_processed:
parts.append(f"items={self.items_processed}")
if self.content_hash:
parts.append(f"content_hash={self.content_hash!r}")
if self.elapsed_seconds is not None:
parts.append(f"elapsed={self.elapsed_seconds:.1f}s")
if self.error:
parts.append(f"error={self.error!r}")
return f"RememberResult({', '.join(parts)})"
def __str__(self):
return repr(self)
def to_dict(self) -> dict:
"""Serialize to a plain dict suitable for JSON API responses."""
d = {
"status": self.status,
"dataset_name": self.dataset_name,
"dataset_id": self.dataset_id,
"pipeline_run_id": self.pipeline_run_id,
"items_processed": self.items_processed,
"elapsed_seconds": self.elapsed_seconds,
}
if self.session_ids:
d["session_ids"] = self.session_ids
if self.content_hash:
d["content_hash"] = self.content_hash
if self.items:
d["items"] = self.items
if self.entry_type:
d["entry_type"] = self.entry_type
if self.entry_id:
d["entry_id"] = self.entry_id
if self.error:
d["error"] = self.error
return d
def __bool__(self):
"""True if status is completed or session_stored."""
return self.status in ("completed", "session_stored")
def __await__(self):
return self._await_impl().__await__()
async def _await_impl(self):
"""Await the background task if running, then return self.
The background coroutine handles its own exceptions and writes
them to ``self.error`` / ``self.status``, so this method never
re-raises — it just waits for the task to finish.
"""
if self._task is not None and not self._task.done():
await asyncio.shield(self._task)
return self
@property
def done(self) -> bool:
"""True if the pipeline has finished (success or failure).
For session-stored results (no pipeline runs), always True.
For blocking results (no background task), reflects status.
For background results, delegates to the asyncio.Task.
"""
if self._task is not None:
return self._task.done()
return self.status != "running"
def _resolve(self, cognify_result):
"""Extract fields from the cognify() return value.
Called once when the pipeline finishes. Sets status, dataset_id,
pipeline_run_id, elapsed_seconds, item info, and stores the raw
result.
"""
self.raw_result = cognify_result
self.elapsed_seconds = time.monotonic() - self._started_at
if not cognify_result or not isinstance(cognify_result, dict):
self.status = "completed"
return
# cognify returns {dataset_id: PipelineRunInfo}
# remember() always processes a single dataset, so take the first.
ds_id, run_info = next(iter(cognify_result.items()))
self.dataset_id = str(ds_id)
if hasattr(run_info, "status"):
self.status = "errored" if "Errored" in run_info.status else "completed"
if hasattr(run_info, "pipeline_run_id"):
self.pipeline_run_id = str(run_info.pipeline_run_id)
else:
self.status = "completed"
# Extract per-item details from the payload (list of Data objects)
payload = getattr(run_info, "payload", None)
if payload and isinstance(payload, list):
for data_item in payload:
item_info = {}
if hasattr(data_item, "id"):
item_info["id"] = str(data_item.id)
if hasattr(data_item, "name"):
item_info["name"] = data_item.name
if hasattr(data_item, "content_hash"):
item_info["content_hash"] = data_item.content_hash
if hasattr(data_item, "token_count"):
item_info["token_count"] = data_item.token_count
if hasattr(data_item, "mime_type"):
item_info["mime_type"] = data_item.mime_type
if hasattr(data_item, "data_size"):
item_info["data_size"] = data_item.data_size
if item_info:
self.items.append(item_info)
self.items_processed = len(self.items)
if self.items and self.items[0].get("content_hash"):
self.content_hash = self.items[0]["content_hash"]
else:
# PipelineRunCompleted carries no `payload` — per-item results live
# in data_ingestion_info as {"run_info": ..., "data_id": ...} dicts.
ingestion_info = getattr(run_info, "data_ingestion_info", None)
if ingestion_info and isinstance(ingestion_info, list):
processed = 0
for entry in ingestion_info:
if not isinstance(entry, dict):
continue
status = getattr(entry.get("run_info"), "status", "")
if "Errored" in status:
continue
processed += 1
if entry.get("data_id") is not None:
self.items.append({"id": str(entry["data_id"])})
self.items_processed = processed
def _fail(self, exc: BaseException):
"""Mark the result as failed with an error message and elapsed time."""
self.status = "errored"
self.error = str(exc)
self.elapsed_seconds = time.monotonic() - self._started_at
async def remember(
data: Union[
BinaryIO,
list[BinaryIO],
str,
list[str],
DataItem,
list[DataItem],
"MemoryEntry",
MemorySource,
],
dataset_name: str = "main_dataset",
*,
session_id: Optional[str] = None,
chunk_size: Optional[int] = None,
chunker: Optional[Any] = None,
custom_prompt: Optional[str] = None,
run_in_background: bool = False,
self_improvement: bool = True,
session_ids: Optional[List[str]] = None,
**kwargs: Unpack[RememberKwargs],
) -> "RememberResult":
"""Store data in memory.
Two modes depending on whether ``session_id`` is provided:
**Without session_id (permanent memory):** Runs ``add()`` +
``cognify()`` to ingest data and build the knowledge graph.
**With session_id (session memory):** Stores the data in the
session cache for fast retrieval. When ``self_improvement`` is
True (default), also bridges the session data into the permanent
graph in the background via ``improve()``. The call returns
immediately — await the result to wait for the background sync.
Args:
data: The data to store (text, file paths, binary streams, etc.).
dataset_name: Target dataset. Defaults to ``"main_dataset"``.
session_id: Optional session ID. When set, stores data in the
session cache instead of the permanent graph.
chunk_size: Max tokens per chunk. Auto-calculated when *None*.
chunker: Text chunking strategy. Defaults to *TextChunker*.
custom_prompt: Custom prompt for entity extraction.
run_in_background: If *True*, run as a background task.
self_improvement: If *True* (default), automatically runs
``improve()`` after cognify to enrich the graph with
triplet embeddings and indexing.
session_ids: Session IDs to sync graph knowledge back to.
Only used when ``self_improvement=True``. When provided,
``improve()`` will also copy recent graph relationships
into these sessions for fast retrieval.
content_type: Set to ``"skills"`` to explicitly ingest SKILL.md
files as dataset-scoped Skill nodes. ``remember()`` does not
auto-detect skill paths.
skill_improvement: Internal skill-improvement control dict used with
``SkillRunEntry`` or ``content_type="skills"``. ``apply=True``
requires an existing ``proposal_id``.
**kwargs: Additional options -- see ``RememberKwargs``.
Returns:
RememberResult: A promise-like object. Print it for a summary,
await it to block until background processing finishes, or
inspect ``.status``, ``.dataset_name``, ``.elapsed_seconds``, etc.
Example::
result = await cognee.remember("Einstein was born in Ulm.")
print(result)
# RememberResult(status='completed', dataset='main_dataset', elapsed=4.2s)
# Background mode:
result = await cognee.remember("data", run_in_background=True)
print(result) # status='running'
await result # blocks until done
print(result) # status='completed'
# Access raw pipeline result:
result.raw_result # {dataset_id: PipelineRunInfo}
"""
from cognee.shared.utils import send_telemetry
from cognee import __version__ as cognee_version
# Migration dispatch: a MemorySource streams COGX records from an external
# memory system (Mem0, Zep/Graphiti, Letta, a COGX archive, ...). The
# migration loader routes them through add/cognify or direct graph storage
# depending on the source's fidelity mode.
if isinstance(data, MemorySource):
from cognee.api.v1.serve.state import get_remote_client
from cognee.modules.migration.import_source import import_memory_source
if get_remote_client() is not None:
raise ValueError(
"remember() cannot import a MemorySource while connected to a remote "
"Cognee instance — the import would write to the local graph, not the "
"remote one. Call cognee.disconnect() first to import locally, or use "
"cognee.push() to upload the data to the remote instance."
)
if session_id is not None:
raise ValueError(
"session_id is not applicable to MemorySource imports; imported "
"records are stored in the permanent graph, not a session cache."
)
with new_span("cognee.api.remember.import") as span:
span.set_attribute(COGNEE_DATASET_NAME, dataset_name)
span.set_attribute(COGNEE_OPERATION_MODE, data.mode)
span.set_attribute("cognee.source_system", data.source_system)
send_telemetry(
"cognee.remember.import",
kwargs.get("user", "sdk"),
additional_properties={
"source_system": data.source_system,
"mode": data.mode,
"dataset_name": dataset_name,
"run_in_background": run_in_background,
"cognee_version": cognee_version,
},
)
return await import_memory_source(
data,
dataset_name=dataset_name,
run_in_background=run_in_background,
chunk_size=chunk_size,
chunker=chunker,
custom_prompt=custom_prompt,
self_improvement=self_improvement,
**kwargs,
)
# Typed MemoryEntry dispatch: trace steps, rich QA, feedback, and
# explicit skill-run scores. These short-circuit the add+cognify path.
if isinstance(data, MEMORY_ENTRY_TYPES):
return await _remember_entry(
data,
dataset_name=dataset_name,
session_id=session_id,
user=kwargs.get("user"),
skill_improvement=kwargs.get("skill_improvement"),
)
data_size = _estimate_data_size(data)
item_count = len(data) if isinstance(data, list) else 1
mode = "session" if session_id else "permanent"
with new_span("cognee.api.remember") as span:
span.set_attribute(COGNEE_DATASET_NAME, dataset_name)
span.set_attribute(COGNEE_OPERATION_MODE, mode)
span.set_attribute(COGNEE_DATA_SIZE_BYTES, data_size)
span.set_attribute(COGNEE_DATA_ITEM_COUNT, item_count)
if session_id:
span.set_attribute(COGNEE_SESSION_ID, session_id)
send_telemetry(
"cognee.remember",
kwargs.get("user", "sdk"),
additional_properties={
"mode": mode,
"dataset_name": dataset_name,
"data_size_bytes": data_size,
"item_count": item_count,
"session_id": session_id or "",
"self_improvement": self_improvement,
"run_in_background": run_in_background,
"cognee_version": cognee_version,
},
)
return await _remember_inner(
data,
dataset_name,
session_id=session_id,
chunk_size=chunk_size,
chunker=chunker,
custom_prompt=custom_prompt,
run_in_background=run_in_background,
self_improvement=self_improvement,
session_ids=session_ids,
span=span,
**kwargs,
)
def _materialize_inline_skill(skills_text, skill_name):
"""Write inline SKILL.md markdown into a temporary ``<slug>/SKILL.md`` folder.
The no-code companion to the file-upload skills path: callers can pass the
SKILL.md body as a string (e.g. from an n8n field) instead of uploading a
file. The parser derives the skill name from the parent directory, so the
file is nested under ``<slug>/``. Returns ``(cleanup_handle, source_root)``.
"""
import tempfile
from pathlib import Path as _Path
slug = _Path((skill_name or "skill").strip() or "skill").name
tmp = tempfile.TemporaryDirectory(prefix="cognee-skills-")
skill_dir = _Path(tmp.name) / slug
skill_dir.mkdir(parents=True, exist_ok=True)
(skill_dir / "SKILL.md").write_text(
skills_text if isinstance(skills_text, str) else str(skills_text),
encoding="utf-8",
)
return tmp, _Path(tmp.name)
async def _remember_inner(
data,
dataset_name,
*,
session_id,
chunk_size,
chunker,
custom_prompt,
run_in_background,
self_improvement,
session_ids,
span,
**kwargs,
) -> "RememberResult":
from cognee.api.v1.serve.state import get_remote_client
client = get_remote_client()
if client is not None:
span.set_attribute(COGNEE_OPERATION_MODE, "cloud")
return await client.remember(
data,
dataset_name,
session_id=session_id,
chunk_size=chunk_size,
custom_prompt=custom_prompt,
run_in_background=run_in_background,
**kwargs,
)
# Run vector migrations lazily on the first local SDK call.
# This ensures stale LanceDB schemas are migrated before any
# writes, even when the API server was never started. Scoped to the
# dataset this call targets (dataset_id override, else dataset_name).
from cognee.modules.migrations.startup import run_migrations_and_block
await run_migrations_and_block(kwargs.get("dataset_id") or dataset_name, kwargs.get("user"))
# Normalize "" to None — HTML forms and Swagger UI submit untouched
# optional fields as empty strings.
content_type = kwargs.pop("content_type", None) or None
skill_improvement = kwargs.pop("skill_improvement", None)
# Pop skills-only kwargs so they don't reach the add()/cognify() kwarg router,
# which rejects unknown keys. The router always forwards these (as None for a
# normal remember), so they must be consumed here regardless of content_type.
skills_text = kwargs.pop("skills_text", None)
skill_name = kwargs.pop("skill_name", None)
def _requested_node_set(default: str) -> str:
requested_node_set = kwargs.get("node_set") or [default]
if isinstance(requested_node_set, str):
return requested_node_set
if requested_node_set:
return requested_node_set[0]
return default
if content_type not in (None, "skills"):
raise ValueError("Unsupported remember content_type. Supported values: 'skills'.")
if skill_improvement is not None and content_type != "skills":
raise ValueError(
"skill_improvement is supported only for SkillRunEntry or content_type='skills'."
)
if content_type == "skills":
import shutil
import tempfile
from pathlib import Path as _Path
from cognee.context_global_variables import set_database_global_context_variables
from cognee.modules.engine.operations.setup import setup
from cognee.modules.tools import add_skills
await setup()
user = kwargs.get("user")
dataset_id = kwargs.get("dataset_id")
dataset_ref = dataset_id or dataset_name
user, authorized_datasets = await resolve_authorized_user_datasets(dataset_ref, user)
dataset = authorized_datasets[0]
skills_node_set = _requested_node_set("skills")
owner_id = getattr(dataset, "owner_id", None) or getattr(user, "id", None)
if owner_id is None:
raise ValueError("Skill ingestion requires a dataset owner or user.")
# HTTP callers (CloudClient + Swagger) deliver SKILL.md content as
# UploadFile/file-like objects, not paths. add_skills reads paths from
# the local filesystem, so materialize the uploads into a tempdir
# under cwd (which is always allowed by _configured_skill_source_roots)
# before handing off. Local SDK callers continue to pass a path.
skill_source: Any = data
tmp_dir: Optional[tempfile.TemporaryDirectory] = None
normalized_uploads: list = []
if isinstance(data, list):
for item in data:
if not isinstance(item, (str, _Path)) and hasattr(item, "read"):
normalized_uploads.append(item)
elif data is not None and not isinstance(data, (str, _Path)) and hasattr(data, "read"):
normalized_uploads.append(data)
if normalized_uploads:
tmp_dir = tempfile.TemporaryDirectory(prefix="cognee-skills-", dir=_Path.cwd())
tmp_root = _Path(tmp_dir.name)
for upload in normalized_uploads:
rel_name = (
getattr(upload, "filename", None) or getattr(upload, "name", None) or "SKILL.md"
)
# Defensive: reject absolute paths / traversal in client-sent names.
safe_rel = _Path(rel_name).as_posix().lstrip("/")
if ".." in _Path(safe_rel).parts:
raise ValueError(f"Invalid skill filename: {rel_name}")
dest = tmp_root / safe_rel
dest.parent.mkdir(parents=True, exist_ok=True)
# UploadFile.read() is async; plain file-like .read() is sync.
read_result = upload.read()
payload = await read_result if hasattr(read_result, "__await__") else read_result
if isinstance(payload, str):
payload = payload.encode("utf-8")
dest.write_bytes(payload or b"")
skill_files = list(tmp_root.rglob("SKILL.md"))
if skill_files:
skill_source = tmp_root
else:
raw_files = [path for path in tmp_root.rglob("*") if path.is_file()]
if raw_files:
canonical_root = tmp_root / "__canonical_skills__"
for index, raw_file in enumerate(raw_files):
target_dir = canonical_root / f"skill-{index:04d}"
target_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(raw_file, target_dir / "SKILL.md")
skill_source = canonical_root
else:
skill_source = tmp_root
# No-code path: inline SKILL.md markdown supplied as a string instead of
# an uploaded file. Reuses the same add_skills pipeline as the upload path.
if not normalized_uploads and skills_text:
tmp_dir, skill_source = _materialize_inline_skill(skills_text, skill_name)
try:
async with set_database_global_context_variables(dataset.id, owner_id):
skills = await add_skills(
skill_source,
node_set=skills_node_set,
user=user,
dataset=dataset,
)
finally:
if tmp_dir is not None:
tmp_dir.cleanup()
result = RememberResult(
status="completed",
dataset_name=dataset.name,
dataset_id=str(dataset.id),
session_ids=None,
)
result.elapsed_seconds = time.monotonic() - result._started_at
result.items_processed = len(skills)
result.items = [
{"name": s.name, "kind": "skill", "declared_tools": s.declared_tools} for s in skills
]
if skill_improvement is not None:
from cognee.modules.memify.skill_improvement import improve_skill_from_config
proposal = await improve_skill_from_config(
skill_improvement, dataset=dataset, user=user
)
if proposal is not None:
result.items.append(
{
"kind": "skill_improvement_proposal",
"proposal_id": proposal.proposal_id,
"skill_name": proposal.skill_name,
"status": proposal.status,
}
)
return result
from cognee.api.v1.add import add
from cognee.api.v1.cognify import cognify
if chunker is None:
from cognee.modules.chunking.TextChunker import TextChunker
chunker = TextChunker
# Route kwargs to add(), cognify(), or both
remaining = dict(kwargs)
add_kwargs = {}
cognify_kwargs = {}
shared_kwargs = {}
for key in list(remaining):
if key in _SHARED:
shared_kwargs[key] = remaining.pop(key)
elif key in _ADD_ONLY:
add_kwargs[key] = remaining.pop(key)
elif key in _COGNIFY_ONLY:
cognify_kwargs[key] = remaining.pop(key)
if remaining:
raise TypeError(f"Unexpected keyword arguments: {', '.join(remaining)}")
dataset_id = add_kwargs.pop("dataset_id", None) or shared_kwargs.get("dataset_id")
# Ensure database is initialized (same as add() does internally).
# Must run before get_default_user() which queries the DB.
from cognee.modules.engine.operations.setup import setup
await setup()
# Resolve user early so we can use it for session init
user = shared_kwargs.get("user")
if user is None:
from cognee.modules.users.methods import get_default_user
user = await get_default_user()
shared_kwargs["user"] = user
# Session memory: store in session cache, then optionally bridge to graph
if session_id:
await _add_to_session(session_id, data, user)
result = RememberResult(
status="session_stored",
dataset_name=dataset_name,
session_ids=[session_id],
)
result.elapsed_seconds = time.monotonic() - result._started_at
# Bridge session data to permanent graph in the background
if self_improvement:
from cognee.api.v1.improve import improve
async def _session_improve():
try:
await improve(
dataset=dataset_name,
session_ids=[session_id],
user=user,
)
logger.info("remember: session '%s' bridged to permanent graph", session_id)
except Exception as exc:
logger.warning("remember: session improve failed (non-fatal): %s", exc)
result._task = asyncio.create_task(_session_improve())
return result
# Build the result object — starts as "running"
if not dataset_id and dataset_name:
# Create dataset if it doesn't exist
user, dataset_id = await resolve_authorized_user_datasets(dataset_name, user)
dataset_id = dataset_id[0].id if dataset_id else None
result = RememberResult(
status="running",
dataset_name=dataset_name,
dataset_id=str(dataset_id) if dataset_id else None,
session_ids=session_ids,
)
# Permanent memory: add + cognify (+ optional improve)
async def _run():
await add(
data=data,
dataset_name=dataset_name,
dataset_id=dataset_id,
**shared_kwargs,
**add_kwargs,
)
datasets_arg = [dataset_name] if dataset_id is None else [dataset_id]
cognify_result = await cognify(
datasets=datasets_arg,
chunker=chunker,
chunk_size=chunk_size,
custom_prompt=custom_prompt,
run_in_background=False,
**shared_kwargs,
**cognify_kwargs,
)
result._resolve(cognify_result)
if self_improvement:
from cognee.api.v1.improve import improve
logger.info("remember: running self-improvement on dataset '%s'", dataset_name)
improve_kwargs = {"dataset": dataset_name, "user": user}
if session_ids:
improve_kwargs["session_ids"] = session_ids
await improve(**improve_kwargs)
if run_in_background:
# Background runs must not depend on caller/request-scoped stream lifetimes.
# Materialize stream-like inputs into owned in-memory buffers up front.
from cognee.tasks.ingestion.utils import materialize_stream_for_background
data = await materialize_stream_for_background(data)
async def _remember_background():
try:
await _run()
except Exception as exc:
result._fail(exc)
logger.exception("Background remember failed")
result._task = asyncio.create_task(_remember_background())
return result
# Blocking mode
try:
await _run()
except Exception as exc:
result._fail(exc)
raise
return result