symfonic.memory.embeddings.auto_embed¶
auto_embed ¶
Auto-embed helper for graph-backed memory layers.
v7.3 Roadmap Item 13.1 (PR-C). When a caller writes a node without
pre-computing an embedding, the graph-backed layers (semantic /
procedural / prospective) defer to :func:maybe_embed which:
- Caller-provided embeddings always win -- if
node.embeddingis already set the node is returned unchanged (no provider call, no cache touch). - Per-kind text selection -- the text fed to
EmbeddingProvider.embedis picked by :func:resolve_embedding_text(see its docstring for the rule). - Shared cache -- the orchestrator hands an :class:
EmbeddingCacheto every layer; identical text from different layers reuses one embedding instead of payingembed()twice. - Failure mode (load-bearing) -- if
provider.embedraises, the exception is caught, a single WARNING is logged ("embedding_provider failed: %s") and the original node is returned withembedding=None. The write proceeds. This is the risk-register §12.8 default: a flaky embedder must NEVER block a memory write.
v7.3 ships per-call sync embeds; batched embeds across an add_node
fan-out are deferred to v7.4 (see TODO at the call sites in the layer
wrappers).
DEFAULT_CACHE_CAP
module-attribute
¶
Maximum entries retained by the shared :class:EmbeddingCache.
Sized for ~12-15 active tenants emitting ~70 unique entity surfaces each
per consolidation cycle. Past the cap the oldest entry is evicted
(OrderedDict.popitem(last=False)). The cache is in-memory only --
process restart drops it.
EmbeddingCache ¶
sha256(text) -> embedding LRU cache shared across layers.
Backed by :class:collections.OrderedDict so eviction is O(1) and
deterministic. The cache is intentionally simple: no TTL, no
persistence, no thread-locking (the symfonic memory pipeline is
single-event-loop per process; concurrent writes from the same loop
serialise on the await boundary inside maybe_embed).
Source code in src/symfonic/memory/embeddings/auto_embed.py
get ¶
Return the cached embedding for text or None.
Touching an entry marks it as most-recently-used (moves to end).
Source code in src/symfonic/memory/embeddings/auto_embed.py
key_for
staticmethod
¶
set ¶
Store embedding for text. Evicts oldest when over cap.
Source code in src/symfonic/memory/embeddings/auto_embed.py
maybe_embed
async
¶
maybe_embed(
node: MemoryNode,
provider: EmbeddingProvider | None,
cache: EmbeddingCache | None = None,
) -> MemoryNode
Auto-embed node unless the caller already populated node.embedding.
This is the single chokepoint every graph-backed layer's
add_node / update_node calls through. The contract is:
- Caller-provided
node.embeddingis preserved verbatim -- the provider is NOT invoked. provider is None(no provider configured) returns the node unchanged. This preserves byte-identical pre-Item-13.1 behaviour so existing deployments are unaffected.- The text to embed is picked by :func:
resolve_embedding_text(entity nodes prefer label, everyone else prefers content). - Empty text short-circuits -- nothing to embed.
- Cache hits skip the provider call entirely. Cache misses populate the cache on success.
- Provider exceptions are caught, logged at WARNING, and the node is
returned with
embedding=None. This is the load-bearing failure mode (risk-register §12.8 option b) -- a flaky embedder must not block a write.
The returned node is the same instance when no auto-embed happened
(preserves Pydantic mutation expectations) and a model_copy with
embedding set otherwise.
Source code in src/symfonic/memory/embeddings/auto_embed.py
resolve_embedding_text ¶
Return the text to feed into EmbeddingProvider.embed for node.
Per-kind rule (v7.3 Item 13.1):
- Entity nodes -- when
node.labelstarts with"Entity:"ornode.properties['label_prefix']equals"Entity", embednode.label(the canonical surface form). This is whatRetrievalScorershould fuzzy-match across runs so spelling drift collapses to one retrieval hit. - Default -- embed
node.properties['content']when that key is non-empty; otherwise fall back tonode.label.MemoryNodehas no top-levelcontentfield -- the convention across all five layers isproperties['content']carries the rich text.
Returns the empty string when neither label nor content is usable (caller is expected to skip the embed call in that case).