Skip to content

converters: reference converters emitting the MIF Container Profile (start: MemPalace) (#77) - #222

Open
perlowja wants to merge 5 commits into
modeled-information-format:mainfrom
perlowja:feat/reference-converters
Open

converters: reference converters emitting the MIF Container Profile (start: MemPalace) (#77)#222
perlowja wants to merge 5 commits into
modeled-information-format:mainfrom
perlowja:feat/reference-converters

Conversation

@perlowja

@perlowja perlowja commented Jul 8, 2026

Copy link
Copy Markdown

Starts the reference-converter suite the Container Profile section (#205) references (#77).

This PR: a MemPalace → MIF Container Profile converter (converters/mempalace/mempalace_to_mif.py) + a schema-validated example (examples/converters/mempalace.corpus.json) + converters/README.md documenting the suite.

  • Reads MemPalace's ChromaDB directly (only chromadb needed, no MemPalace runtime).
  • Emits the Container Profile envelope; each drawer → a kind: memory record whose payload is a valid MIF memory unit, typed conceptType: episodic per the ai-memory MemPalace mapping (docs(ai-memory): MemPalace → MIF Level-1 mapping + examples #206).
  • Source fields round-trip under extensions.mempalace; @ids are UUIDv5-stable (recovering an original UUID from YAML front matter when present).
  • Both emitted units validate against schema/mif.schema.json.

Follow-ups (same output-layer port, each its own PR + validated example): Mem0, Letta, Cognee, Graphiti — currently MPF-era adapters in the CHARON tooling, being re-targeted to emit this envelope. Pairs with #205 (Container Profile spec) and #206 (ai-memory mapping).

perlowja added 2 commits July 8, 2026 13:28
…odeled-information-format#77)

First of the reference converters the Container Profile section (modeled-information-format#205)
references. Reads a MemPalace palace (ChromaDB) directly and emits a MIF
Container Profile corpus: each drawer becomes a kind:memory record whose
payload is a schema-valid MIF memory unit, typed conceptType:episodic per
the ai-memory MemPalace mapping (modeled-information-format#206). Source-specific fields round-trip
under extensions.mempalace; ids are UUIDv5-stable.

Ships a schema-validated example (examples/converters/mempalace.corpus.json);
both emitted units validate against schema/mif.schema.json. converters/README
documents the suite; Mem0/Letta/Cognee/Graphiti follow the same output-layer
port, each as its own PR with a validated example.
…/Graphiti/Obsidian) (modeled-information-format#77)

Adds the remaining reference converters alongside MemPalace, all emitting the
MIF Container Profile natively (not the retired MPF envelope) via a shared
_mif_common helper:

  mem0     — Qdrant (+SQLite history, +Platform): fact/obs -> semantic, history -> episodic
  letta    — sqlite/server: core+archival -> semantic, messages -> episodic
  cognee   — graph+vector+catalog: entities/chunks -> semantic; source docs -> kind:document
  graphiti — Neo4j temporal KG: entity -> semantic, episode -> episodic, edges -> relationships[]
             (bitemporal validity kept in extensions, deferred to a Level-3 sidecar)
  obsidian — a vault of Markdown notes: note -> semantic, journal -> episodic,
             [[wikilinks]] -> relationships[] (converter only; MIF core stays
             vendor-neutral per ADR-017)

Each keeps its source-reading faithful to the CHARON adapter (stdlib + the one
source client; no MIF runtime) and re-targets only the output. Every converter
ships a schema-validated example under examples/converters/; all emitted memory
units validate against schema/mif.schema.json.
headers["X-Organization"] = self.org
req = urllib.request.Request(url, headers=headers, method="GET")
try:
with urllib.request.urlopen(req, timeout=60) as resp:
perlowja and others added 2 commits July 8, 2026 13:49
…llib)

The Letta server-mode GET builds its URL from the operator-supplied --base;
guard the scheme to http(s) so a misconfigured base cannot become a file://
arbitrary-file read, and annotate the (now-mitigated) urllib call for the
Semgrep dynamic-urllib audit rule.
@zircote
zircote marked this pull request as ready for review July 15, 2026 21:19
Copilot AI review requested due to automatic review settings July 15, 2026 21:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new converters/ suite of reference adapters that export various AI-memory backends into MIF’s Container Profile corpus format (*.corpus.json), alongside example corpora and a top-level converters README to document intended usage and mappings.

Changes:

  • Introduces multiple Python converters (MemPalace, Mem0, Letta, Cognee, Graphiti, Obsidian) plus a shared _mif_common.py helper for emitting corpus envelopes and MIF memory payloads.
  • Adds schema-validated example converter outputs under examples/converters/.
  • Documents the converter suite and its mapping/usage expectations in converters/README.md.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
converters/_mif_common.py Shared emission helpers for corpus envelope + memory payload shaping (currently not aligned with repo Container Profile schema).
converters/README.md Suite-level documentation and usage examples (includes a broken spec anchor link and an over-broad “all converters use _mif_common” claim).
converters/mempalace/mempalace_to_mif.py MemPalace (ChromaDB) reader and exporter (currently hand-builds a non-conformant envelope).
converters/mem0/mem0_to_mif.py Mem0 (Qdrant + optional SQLite history, or hosted API) exporter using _mif_common.
converters/letta/letta_to_mif.py Letta (SQLite or REST) exporter using _mif_common.
converters/cognee/cognee_to_mif.py Cognee exporter emitting both document and memory records using _mif_common.
converters/graphiti/graphiti_to_mif.py Graphiti (Neo4j) exporter with relationship mapping, using _mif_common.
converters/obsidian/obsidian_to_mif.py Obsidian vault exporter (currently derives wikilink targets inconsistently with note IDs).
examples/converters/mempalace.corpus.json MemPalace example corpus (top-level envelope fields don’t match repo Container Profile schema).
examples/converters/mem0.corpus.json Mem0 example corpus (top-level envelope fields don’t match repo Container Profile schema).
examples/converters/letta.corpus.json Letta example corpus (top-level envelope fields don’t match repo Container Profile schema).
examples/converters/cognee.corpus.json Cognee example corpus (top-level envelope fields don’t match repo Container Profile schema).
examples/converters/graphiti.corpus.json Graphiti example corpus (top-level envelope fields don’t match repo Container Profile schema).
examples/converters/obsidian.corpus.json Obsidian example corpus (top-level envelope fields don’t match repo Container Profile schema; wikilink targets don’t resolve within the corpus).

Comment thread converters/_mif_common.py
Comment on lines +91 to +113
def build_corpus(
records: List[Dict[str, Any]], *, source_system: str, source_instance: Optional[str] = None,
generated_at: Optional[str] = None,
) -> Dict[str, Any]:
"""Wrap `records` (each `{"kind": "memory"|"document", "payload": ...}`) in a
MIF Container Profile corpus."""
corpus: Dict[str, Any] = {
"@context": MIF_CONTEXT,
"@type": "MemoryCorpus",
"mif_version": MIF_VERSION,
"records": records,
"provenance": {
"@type": "prov:Entity",
"prov:wasGeneratedBy": {
"@type": "prov:Activity",
"prov:used": source_system,
"prov:generatedAtTime": generated_at or datetime.now(timezone.utc).isoformat(),
},
},
}
if source_instance:
corpus["provenance"]["prov:wasDerivedFrom"] = source_instance
return corpus
Comment thread converters/README.md
Comment on lines +5 to +8
Reference converters that read a source AI-memory system and emit a MIF
**[Container Profile](../SPECIFICATION.md#container-profile-corpus-envelope--proposed)**
corpus (`*.corpus.json`) — many MIF memory units (and, where applicable, their
source `document` records) as one transportable artifact.
Comment thread converters/README.md
Comment on lines +24 to +28
All MIF emission goes through the shared [`_mif_common.py`](_mif_common.py)
helper, so every converter produces consistent, schema-valid output. Each
converter ships a **schema-validated example** under
[`../examples/converters/`](../examples/converters/); every emitted memory unit
is validated against [`../schema/mif.schema.json`](../schema/mif.schema.json).
Comment on lines +1 to +5
{
"@context": "https://mif-spec.dev/schema/context.jsonld",
"@type": "MemoryCorpus",
"mif_version": "1.2.2",
"records": [
Comment on lines +22 to +31
"relationships": [
{
"type": "links-to",
"target": "urn:mif:d5845a39-f8d4-5d6d-a3d7-5f96889acd99",
"metadata": {
"obsidian": {
"wikilink": "Container Profile"
}
}
}
Comment on lines +1 to +5
{
"@context": "https://mif-spec.dev/schema/context.jsonld",
"@type": "MemoryCorpus",
"mif_version": "1.2.2",
"records": [
Comment on lines +1 to +5
{
"@context": "https://mif-spec.dev/schema/context.jsonld",
"@type": "MemoryCorpus",
"mif_version": "1.2.2",
"records": [
Comment on lines +1 to +5
{
"@context": "https://mif-spec.dev/schema/context.jsonld",
"@type": "MemoryCorpus",
"mif_version": "1.2.2",
"records": [
Comment on lines +202 to +224
def build_container(
palace_path: Path, *, source_instance: Optional[str] = None,
restore_original_ids: bool = True,
) -> Dict[str, Any]:
"""Assemble a MIF Container Profile corpus from a palace snapshot."""
records = list(iter_memory_records(palace_path, restore_original_ids=restore_original_ids))
corpus: Dict[str, Any] = {
"@context": MIF_CONTEXT,
"@type": "MemoryCorpus",
"mif_version": MIF_VERSION,
"records": records,
"provenance": {
"@type": "prov:Entity",
"prov:wasGeneratedBy": {
"@type": "prov:Activity",
"prov:used": SOURCE_SYSTEM,
"prov:generatedAtTime": datetime.now(timezone.utc).isoformat(),
},
},
}
if source_instance:
corpus["provenance"]["prov:wasDerivedFrom"] = source_instance
return corpus
Comment on lines +133 to +149
# [[wikilinks]] -> relationships (target = urn of the linked note)
rels: List[Dict[str, Any]] = []
seen = set()
for target_name in _WIKILINK_RE.findall(body):
tn = target_name.strip()
if not tn or tn in seen:
continue
seen.add(tn)
rels.append({
"type": "links-to",
"target": "urn:mif:" + str(uuid.uuid5(OBSIDIAN_NS, tn)),
"metadata": {"obsidian": {"wikilink": tn}},
})

mem = mif_memory(
mif_id=mif_uuid(OBSIDIAN_NS, rel.as_posix(), restore=fm.get("id") or fm.get("uid")),
concept_type=concept,

@zircote zircote left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same envelope issue applies to all six corpus files here (cognee, graphiti, letta, mem0, mempalace, obsidian): mif_version needs to become containerProfileVersion, and @context needs to be https://mif-spec.dev/schema/container-context.jsonld rather than the bare memory-unit context.

Two file-specific issues on top of that:

  • cognee.corpus.json records[0]: the document payload includes @id, created, extensions and namespace. None of those are allowed on DocumentReference. id (without the @) is already present separately, so @id can simply be removed.
  • obsidian.corpus.json records[0]: created: "2026-06-25" is not a valid date-time. It needs a full timestamp, for example 2026-06-25T00:00:00Z.

One unrelated note: issue #274 (a gap where memoryType was not accepted as a conceptType alternative, despite documentation stating it should be) has merged, but it does not affect this PR either way. None of these files hit that path; your memory-kind records already validate correctly as written.

…converters

Per @zircote's review:

- _mif_common.py build_corpus(): emit containerProfileVersion (not
  mif_version) and the container-context @context, per
  schema/container.schema.json. Fix provenance to use the plain keys
  the schema defines (wasGeneratedBy/used/generatedAtTime/
  wasDerivedFrom), not the prov:-prefixed variants.
- mempalace_to_mif.py: route build_container() through the shared
  _mif_common.build_corpus() instead of reimplementing it locally, so
  the README's "every converter" claim is actually true.
- All 6 checked-in examples/converters/*.corpus.json: same envelope +
  provenance-key fixes.
- cognee_to_mif.py + its example: DocumentReference is
  additionalProperties:false with no extensions mechanism, so @id,
  namespace, created, and extensions are all schema-violations there
  (confirmed empirically via scripts/validate_container.py, not just
  the @id the review named) -- dropped from the document payload.
- obsidian.corpus.json: fix the memory's created date to a valid
  date-time; fix a links-to relationship pointing at a non-existent
  record id, and drop a second relationship whose target record isn't
  present in this minimal example.
- converters/README.md: fix the Container Profile link, which pointed
  at a SPECIFICATION.md anchor that doesn't exist (the section lives
  at docs/CONTAINER-PROFILE.md).

Validated: python3 scripts/validate_container.py examples/ -> PASS (7 files).
@perlowja

Copy link
Copy Markdown
Author

Fixed in 48a18ad: mif_versioncontainerProfileVersion and the container-context @context (root-caused to converters/_mif_common.py's build_corpus(), shared by all 6 converters after routing mempalace through it too instead of its own local reimplementation), the cognee DocumentReference's disallowed @id/namespace/created/extensions (confirmed via scripts/validate_container.py, not just @id), the obsidian date format plus two broken wikilink relationships, provenance keys (wasGeneratedBy/wasDerivedFrom, not prov:-prefixed, per SPECIFICATION.md §12.3), and the README's broken SPECIFICATION.md anchor. All 6 examples/converters/*.corpus.json now validate PASS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants