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

343 lines
12 KiB
Python

"""COGX — the Cognee eXchange format for portable memory.
COGX is the hub format for memory migration: importers translate external
providers (Mem0, Zep/Graphiti, Letta, ...) into COGX records, a single loader
ingests COGX into Cognee, and a single dumper exports Cognee datasets into a
COGX archive that emitters can translate onward into other formats.
An archive is a directory containing ``manifest.json`` plus one JSONL file per
record kind. Records are Pydantic models discriminated by their ``kind`` field.
"""
import json
import math
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Iterator, List, Literal, Optional, Union
from pydantic import BaseModel, Field, TypeAdapter
COGX_VERSION = "0.1"
def parse_timestamp(value: Any) -> Optional[datetime]:
"""Parse a timestamp from ISO strings or epoch seconds/milli/micro/nanoseconds."""
if value is None or value == "":
return None
if isinstance(value, datetime):
return value
if isinstance(value, (int, float)):
# Heuristic: values past the year ~2603 in seconds are a finer epoch
# unit (milli/micro/nanoseconds); scale down until plausible.
seconds = float(value)
if not math.isfinite(seconds):
return None
while abs(seconds) > 2e10:
seconds /= 1000
try:
return datetime.fromtimestamp(seconds, tz=timezone.utc)
except (ValueError, OverflowError, OSError):
return None
if isinstance(value, str):
try:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
return None
return None
class COGXScope(BaseModel):
"""Ownership scope of a memory record in the source system."""
user_id: Optional[str] = None
agent_id: Optional[str] = None
session_id: Optional[str] = None
run_id: Optional[str] = None
class COGXRecordBase(BaseModel):
external_system: str = "unknown"
external_id: str
scope: COGXScope = Field(default_factory=COGXScope)
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
metadata: Dict[str, Any] = Field(default_factory=dict)
class COGXDocument(COGXRecordBase):
"""Raw source content: a file, archival passage, or standalone text."""
kind: Literal["document"] = "document"
content: str
title: Optional[str] = None
mime_type: Optional[str] = None
class COGXTurn(BaseModel):
role: str
content: str
occurred_at: Optional[datetime] = None
class COGXEpisode(COGXRecordBase):
"""A conversation episode: ordered turns with roles and timestamps."""
kind: Literal["episode"] = "episode"
turns: List[COGXTurn] = Field(default_factory=list)
title: Optional[str] = None
class COGXEntity(COGXRecordBase):
"""An extracted entity with optional type, aliases, and description."""
kind: Literal["entity"] = "entity"
name: str
entity_type: Optional[str] = None
description: Optional[str] = None
aliases: List[str] = Field(default_factory=list)
attributes: Dict[str, Any] = Field(default_factory=dict)
class COGXFact(COGXRecordBase):
"""A triplet fact. Subject/object refer to entity external_ids or plain names.
Temporal validity (``valid_at``/``invalid_at``) is always carried, even
where the loader cannot yet query it natively — it is stored as edge
properties so the data is in place when bi-temporal search lands.
"""
kind: Literal["fact"] = "fact"
subject_ref: str
predicate: str
object_ref: str
fact_text: Optional[str] = None
valid_at: Optional[datetime] = None
invalid_at: Optional[datetime] = None
confidence: Optional[float] = None
provenance: List[str] = Field(default_factory=list)
class COGXMemory(COGXRecordBase):
"""An atomic derived memory (Mem0-style short fact text)."""
kind: Literal["memory"] = "memory"
content: str
categories: List[str] = Field(default_factory=list)
class COGXMemoryBlock(COGXRecordBase):
"""A named, bounded core-memory block (Letta-style)."""
kind: Literal["memory_block"] = "memory_block"
label: str
value: str
limit: Optional[int] = None
class COGXRawNode(BaseModel):
"""A graph node persisted verbatim (full fidelity) when no typed mapping exists.
``properties`` is the raw node property dict, including its ``id`` and
``type``. On import these are rehydrated back into DataPoint instances so
facts referencing them stay resolvable.
"""
kind: Literal["raw_node"] = "raw_node"
properties: Dict[str, Any] = Field(default_factory=dict)
COGXRecord = Union[
COGXDocument,
COGXEpisode,
COGXEntity,
COGXFact,
COGXMemory,
COGXMemoryBlock,
COGXRawNode,
]
_record_adapter: TypeAdapter = TypeAdapter(COGXRecord)
RECORD_FILES: Dict[str, str] = {
"document": "documents.jsonl",
"episode": "episodes.jsonl",
"entity": "entities.jsonl",
"fact": "facts.jsonl",
"memory": "memories.jsonl",
"memory_block": "memory_blocks.jsonl",
}
MANIFEST_FILE = "manifest.json"
RAW_NODES_FILE = "nodes.jsonl"
PERMISSIONS_FILE = "permissions.json"
class COGXManifest(BaseModel):
cogx_version: str = COGX_VERSION
source_system: str = "unknown"
exported_at: Optional[datetime] = None
counts: Dict[str, int] = Field(default_factory=dict)
embedding_model: Optional[str] = None
# Cognee-origin archives only: the source store's stamped data-migration
# revision at export time. Raw nodes keep their source-store ids, so the
# importing store must not claim a newer revision than the exported data
# actually has (None = unknown; the import keeps its own stamp).
migration_revision: Optional[str] = None
notes: List[str] = Field(default_factory=list)
def parse_record(data: Dict[str, Any]) -> COGXRecord:
return _record_adapter.validate_python(data)
def validate_cogx_version(version: str) -> None:
"""Reject archives written by a newer major COGX version than this reader."""
try:
archive_major = int(str(version).split(".")[0])
current_major = int(COGX_VERSION.split(".")[0])
except (ValueError, IndexError):
raise ValueError(f"Unrecognized COGX version {version!r} (reader supports {COGX_VERSION}).")
if archive_major > current_major:
raise ValueError(
f"COGX archive version {version} is newer than this reader "
f"supports ({COGX_VERSION}). Upgrade cognee to import it."
)
class COGXArchiveWriter:
"""Writes COGX records into an archive directory, one JSONL file per kind.
The destination is treated as owned by the writer: any record files or
manifest left by a previous export session are removed at init so a
re-export never appends duplicates next to a stale manifest.
"""
def __init__(self, directory: Union[str, Path], source_system: str = "cognee"):
self.directory = Path(directory)
self.directory.mkdir(parents=True, exist_ok=True)
for file_name in (*RECORD_FILES.values(), RAW_NODES_FILE, MANIFEST_FILE, PERMISSIONS_FILE):
(self.directory / file_name).unlink(missing_ok=True)
self.source_system = source_system
self.counts: Dict[str, int] = {}
self.notes: List[str] = []
self.embedding_model: Optional[str] = None
self.migration_revision: Optional[str] = None
self._handles: Dict[str, Any] = {}
def __enter__(self) -> "COGXArchiveWriter":
return self
def __exit__(self, exc_type, exc_value, traceback) -> None:
self.close(write_manifest=exc_type is None)
def write(self, record: COGXRecord) -> None:
file_name = RECORD_FILES[record.kind]
handle = self._handles.get(file_name)
if handle is None:
handle = open(self.directory / file_name, "a", encoding="utf-8")
self._handles[file_name] = handle
handle.write(record.model_dump_json(exclude_none=True) + "\n")
self.counts[record.kind] = self.counts.get(record.kind, 0) + 1
def write_raw_node(self, node: Dict[str, Any]) -> None:
"""Persist a graph node that has no typed COGX mapping (full fidelity)."""
handle = self._handles.get(RAW_NODES_FILE)
if handle is None:
handle = open(self.directory / RAW_NODES_FILE, "a", encoding="utf-8")
self._handles[RAW_NODES_FILE] = handle
handle.write(json.dumps(node, default=str) + "\n")
self.counts["raw_node"] = self.counts.get("raw_node", 0) + 1
def add_note(self, note: str) -> None:
self.notes.append(note)
def close(self, write_manifest: bool = True) -> None:
for handle in self._handles.values():
handle.close()
self._handles.clear()
if write_manifest:
manifest = COGXManifest(
source_system=self.source_system,
exported_at=datetime.now(timezone.utc),
counts=self.counts,
embedding_model=self.embedding_model,
migration_revision=self.migration_revision,
notes=self.notes,
)
manifest_path = self.directory / MANIFEST_FILE
manifest_path.write_text(manifest.model_dump_json(indent=2), encoding="utf-8")
def read_manifest(directory: Union[str, Path]) -> Optional[COGXManifest]:
manifest_path = Path(directory) / MANIFEST_FILE
if not manifest_path.exists():
return None
manifest = COGXManifest.model_validate_json(manifest_path.read_text(encoding="utf-8"))
validate_cogx_version(manifest.cogx_version)
return manifest
def write_social_layer(directory: Union[str, Path], payload: Dict[str, Any]) -> None:
"""Persist the dataset's social layer (owner + grants, WITH credentials).
Written only on explicit request (``export(include_permissions=True)``):
the payload carries user emails, password HASHES, and account flags, so an
archive containing this file must be handled as a secret, not just data.
"""
(Path(directory) / PERMISSIONS_FILE).write_text(
json.dumps(payload, indent=2, default=str), encoding="utf-8"
)
def read_social_layer(directory: Union[str, Path]) -> Optional[Dict[str, Any]]:
permissions_path = Path(directory) / PERMISSIONS_FILE
if not permissions_path.exists():
return None
return json.loads(permissions_path.read_text(encoding="utf-8"))
def _archive_file(base: Path, file_name: str) -> Optional[Path]:
"""Resolve ``base / file_name`` and confirm it stays inside ``base``.
Archive record file names are fixed constants, but the archive directory
itself is caller-supplied; resolving and containment-checking the joined
path keeps a crafted base from steering reads outside its own tree
(CodeQL py/path-injection). Returns ``None`` when the file is absent or
would resolve outside ``base``.
"""
resolved_base = base.resolve()
candidate = (resolved_base / file_name).resolve()
if not candidate.is_relative_to(resolved_base):
return None
if not candidate.is_file():
return None
return candidate
def read_archive(directory: Union[str, Path]) -> Iterator[COGXRecord]:
"""Stream typed records from a COGX archive directory.
Raw graph nodes (``nodes.jsonl``) are yielded as :class:`COGXRawNode`
records after the typed record files, so importers can restore them with
full fidelity.
"""
directory = Path(directory)
if not directory.is_dir():
raise FileNotFoundError(f"COGX archive directory not found: {directory}")
for file_name in RECORD_FILES.values():
file_path = _archive_file(directory, file_name)
if file_path is None:
continue
with open(file_path, "r", encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if line:
yield parse_record(json.loads(line))
raw_nodes_path = _archive_file(directory, RAW_NODES_FILE)
if raw_nodes_path is not None:
with open(raw_nodes_path, "r", encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if line:
yield COGXRawNode(properties=json.loads(line))