symfonic.memory.embeddings¶
embeddings ¶
Embedding helpers shared by the graph-backed memory layers.
v7.3 Roadmap Item 13.1 surface. Provides:
- :class:
EmbeddingCache-- a shared sha256-keyed LRU mapping (cap 1024) that the orchestrator hands to every graph-backed layer so identical text from different layers reuses a singleembed()call. - :func:
maybe_embed-- the auto-embed wrapper invoked by :class:~symfonic.memory.layers.semantic.SemanticLayer, :class:~symfonic.memory.layers.procedural.layer.ProceduralLayer, and :class:~symfonic.memory.layers.prospective.ProspectiveLayerbefore delegating toGraphMemoryStore.add_node/update_node. - :func:
make_embedding_provider-- the string-identifier factory consumed by the scaffold'sEMBEDDING_PROVIDERenv-var path so callers can opt into a provider without importing the symfonic-core internals directly.
The protocol itself (EmbeddingProvider) lives in
symfonic.memory.protocols and is re-exported here for convenience.
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.
SUPPORTED_PROVIDER_IDENTIFIERS
module-attribute
¶
String identifiers accepted by :func:make_embedding_provider.
The literal "none" and the empty string are also accepted and map
to None (no auto-embed; caller still populates manually).
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
EmbeddingProvider ¶
make_embedding_provider ¶
Build an :class:EmbeddingProvider from a string identifier.
Returns None for the default-off path (identifier in
{None, "", "none"}). Raises ValueError on an unknown string so
a typo in .env does not silently degrade to keyword-only
retrieval.
The actual provider instance is constructed lazily -- importing this
module does NOT pull in sentence_transformers or the OpenAI SDK.
Each branch handles its own ImportError and re-raises as
ValueError with the required pip-install hint.
Source code in src/symfonic/memory/embeddings/factory.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).