symfonic.memory.janitor¶
janitor ¶
SemanticMerge -- deduplicates procedural memory nodes by semantic similarity.
Uses difflib.SequenceMatcher for label/content comparison so no additional dependencies are required. The default threshold of 0.65 captures cross-language similarities (e.g. "Ventas" vs "Sales") while still avoiding false positives on unrelated content.
SemanticMerge ¶
Deduplicates procedural nodes by semantic (textual) similarity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
threshold
|
float
|
Minimum similarity score (0.0–1.0) required for two nodes to be considered duplicates. Defaults to 0.75. |
0.65
|
Source code in src/symfonic/memory/janitor.py
deduplicate
async
¶
deduplicate(
scope: TenantScope,
graph_store: GraphMemoryStore,
new_node: MemoryNode,
) -> MemoryNode
Check for duplicates and merge when found; otherwise return new_node.
If one or more duplicate nodes exist this method:
1. Merges new_node into the first duplicate (sequential merge for
multiple duplicates would require an additional loop; a single merge
covers the most common case).
2. Persists the merged node back to the graph via update_node.
3. Returns the canonical (merged) node.
If no duplicates are found the caller should persist new_node as a fresh graph record.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
TenantScope
|
Tenant context for graph queries. |
required |
graph_store
|
GraphMemoryStore
|
Live graph store to query and update. |
required |
new_node
|
MemoryNode
|
Incoming node that may duplicate an existing record. |
required |
Returns:
| Type | Description |
|---|---|
MemoryNode
|
The canonical MemoryNode (either the merged existing node or the |
MemoryNode
|
unchanged new_node when no duplicate was found). |
Source code in src/symfonic/memory/janitor.py
deduplicate_all
async
¶
deduplicate_all(
scope: TenantScope, graph_store: GraphMemoryStore
) -> tuple[int, list[MemoryNode]]
Scan all procedural nodes for the tenant and merge duplicates.
This is the bulk operation exposed by the /procedures/deduplicate
endpoint. It iterates over all nodes and greedily merges pairs whose
similarity exceeds the threshold, then deletes the absorbed nodes.
Returns:
| Type | Description |
|---|---|
tuple[int, list[MemoryNode]]
|
A tuple of (merged_count, remaining_nodes). |
Source code in src/symfonic/memory/janitor.py
find_duplicates
async
¶
find_duplicates(
scope: TenantScope,
existing_nodes: list[MemoryNode],
new_node: MemoryNode,
) -> list[MemoryNode]
Return nodes from existing_nodes that overlap with new_node.
A node is considered a duplicate when the similarity between its
representative text and that of new_node meets or exceeds
self.threshold. Nodes whose representative text is shorter than
_MIN_TEXT_LENGTH characters are skipped to avoid false positives
caused by incidental substring overlap in short identifiers.
v7.5 authored-tier guard (Jarvio Ask 7 Option B):
* When new_node itself is authored, return [] -- the
caller's store_skill call must produce a fresh graph
row even if a near-identical auto-promoted entry already
exists. Domain plugins own the authored skill's
lifecycle.
* Authored entries in existing_nodes are skipped as merge
candidates so an incoming auto-promoted draft cannot
absorb the authored canonical (which would mutate the
hand-edited fields).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
TenantScope
|
Tenant context (used for logging only at this level). |
required |
existing_nodes
|
list[MemoryNode]
|
Candidate nodes already stored in the graph. |
required |
new_node
|
MemoryNode
|
The incoming node to compare against. |
required |
Returns:
| Type | Description |
|---|---|
list[MemoryNode]
|
Subset of existing_nodes whose similarity score >= threshold. |
Source code in src/symfonic/memory/janitor.py
merge
async
¶
Merge new into existing and return the canonical node.
Merge rules: - Label: Keep the longer label (assumed more specific / descriptive). - Steps: Union of both step lists, preserving order and removing exact duplicates. - Importance: Take the maximum of the two scores. - Active flag: True if either node was active. - Properties: Shallow-merge; existing values are overridden by new values, except for the fields governed by the rules above. - Identity: The existing node's ID is preserved so callers can update the graph record in-place rather than creating a new one.