# Introduction (/smartmemory) SmartMemory gives AI agents persistent, long-term memory — one API over a graph and vector store, with a pipeline that extracts, links, and recalls what matters. ## Start here [#start-here] # Assertion Challenging (/smartmemory/concepts/assertion-challenging) > Code: `smartmemory/reasoning/challenger.py` · Class: `AssertionChallenger` SmartMemory detects contradictions between new facts and existing knowledge using a multi-method detection cascade and an auto-resolution layer. Use it to flag — or correct — factually inconsistent input as it arrives. ## Detection cascade [#detection-cascade] ``` LLM → Graph → Embedding → Heuristic ``` Each method runs only if the previous one did not produce a high-confidence verdict. | Method | Reliability | Speed | What it catches | | ------------- | ----------- | ------ | --------------------------------------------------- | | **LLM** | ⭐⭐⭐⭐⭐ | Slow | Nuanced contradictions, context-dependent conflicts | | **Graph** | ⭐⭐⭐⭐ | Fast | Functional properties (capital, CEO, born in) | | **Embedding** | ⭐⭐⭐ | Medium | High similarity + opposite polarity | | **Heuristic** | ⭐⭐ | Fast | Obvious negations ("is" vs "is not") | ## Pipeline [#pipeline] ## Usage [#usage] ```python # Challenge a new assertion explicitly result = memory.challenge("Paris is the capital of Germany") if result.has_conflicts: for conflict in result.conflicts: print(f"Contradicts: {conflict.existing_fact}") print(f"Method: {conflict.explanation}") # [LLM], [Graph], [Embedding], etc. print(f"Confidence: {conflict.confidence}") # Auto-challenge during ingestion (only fires for factual-looking content) memory.ingest("The speed of light is 300,000 km/s") # Force or skip explicitly memory.ingest(content, auto_challenge=True) # always challenge memory.ingest(content, auto_challenge=False) # never challenge ``` ## Smart triggering [#smart-triggering] Auto-challenge only runs for content that looks like a factual claim. Conversational, opinion, and ephemeral content is skipped to avoid false positives and unnecessary LLM cost. | Triggered ✅ | Skipped ❌ | | ----------------------------------- | ---------------------------------------- | | "X is the Y" patterns | Opinions ("I think...") | | Numeric claims | Questions | | "capital of", "CEO of" | Greetings | | Absolute claims ("always", "never") | Temporal/personal ("today", "currently") | ## Conflict types [#conflict-types] | Type | Description | Example | | ---------------------- | ----------------------------- | ------------------------------------ | | `DIRECT_CONTRADICTION` | Opposite claims | "X is Y" vs "X is not Y" | | `TEMPORAL_CONFLICT` | Time-based conflict | "Was X" vs "Now Y" | | `NUMERIC_MISMATCH` | Different values | "Population is 14M" vs "37M" | | `ENTITY_CONFUSION` | Same name, different entities | "Apple (fruit)" vs "Apple (company)" | ## Auto-resolution [#auto-resolution] Before deferring to human review, SmartMemory attempts to resolve the conflict from authoritative sources: ``` Wikipedia Lookup → LLM Reasoning → Grounding Check → Recency Heuristic ``` | Method | What it does | Confidence | | ----------------- | -------------------------------------------- | ---------- | | **Wikipedia** | Looks up entities, checks which fact aligns | 0.85 | | **LLM Reasoning** | Asks the model to fact-check with reasoning | 0.7+ | | **Grounding** | Checks existing provenance / trusted sources | 0.75 | | **Recency** | For temporal conflicts, prefers recent info | 0.65 | ```python result = challenger.resolve_conflict(conflict, auto_resolve=True) if result["auto_resolved"]: print(f"Resolved via {result['method']}: {result['evidence']}") else: print("Could not auto-resolve, needs human review") ``` ## Manual resolution strategies [#manual-resolution-strategies] When auto-resolution defers, the caller picks one of: * **`KEEP_EXISTING`** — trust the existing fact * **`ACCEPT_NEW`** — replace with the new fact (decays old confidence) * **`KEEP_BOTH`** — store both with a conflict marker * **`DEFER`** — flag for human review ## Behaviour notes [#behaviour-notes] * **Semantic, not regex.** Detection uses LLM reasoning over retrieved context, not pattern matching, so it tolerates paraphrase and surface variation. * **Gentle by default.** When a conflict surfaces during conversational ingest, the system suggests a gentle challenge rather than rejecting the input — appropriate for assistant-style products. * **Cost-aware.** The cascade short-circuits as soon as a high-confidence verdict is reached; embedding and heuristic checks are cheap, the LLM only runs when needed. ## Related [#related] * [Decision Memory](/smartmemory/concepts/decision-memory/) — first-class decisions with their own contradict/supersede lifecycle * [Opinions & Observations](/smartmemory/concepts/opinions-observations/) — synthesis from supporting evidence (the inverse of contradiction) * [Inference Engine](/smartmemory/concepts/inference-engine/) — broader reasoning surface that the challenger sits inside # Bi-Temporal Versioning (/smartmemory/concepts/bi-temporal-versioning) # Bi-Temporal Versioning & Time-Travel Queries [#bi-temporal-versioning--time-travel-queries] SmartMemory tracks two independent time dimensions for memory items, so you can answer both *"what did we believe was true on date X?"* and *"what did the system have stored on date X?"*. ## The two times [#the-two-times] | Dimension | Semantics | | -------------------- | ---------------------------------------------- | | **Valid time** | When the fact is or was true in the real world | | **Transaction time** | When the system recorded the version | Each version is represented by the real `TemporalVersion` dataclass: ```python from dataclasses import fields from smartmemory.temporal import TemporalVersion print([field.name for field in fields(TemporalVersion)]) ``` Source: `smart-memory-core/smartmemory/temporal/queries.py`. A version's transaction-time interval `[transaction_time_start, transaction_time_end)` is closed on the left and exclusive on the right. The current version has `transaction_time_end = None`. The same shape applies to valid time: an open `valid_time_end` means *"still believed to be true."* ## How versions are created [#how-versions-are-created] Manual supersession records that one memory replaces another. The default search path returns the current value only. Passing `include_superseded=True` returns the old and new values. `as_of_date` depends on a preserved creation time for the old value. In lite mode, put that time in `metadata={"reference_time": }` before superseding. Without `reference_time`, the old value may be anchored at supersession time, so a query for an earlier moment returns the new value. ## Querying current state and history [#querying-current-state-and-history] ```python from smartmemory.models.memory_item import MemoryItem from smartmemory.pipeline.config import PipelineConfig from smartmemory.tools.factory import lite_context lite = dict(pipeline_profile=PipelineConfig.lite(llm_enabled=False)) with lite_context(**lite) as memory: old = memory.add( MemoryItem( content="The incident channel is #ops-old", memory_type="semantic", ) ) new = memory.add( MemoryItem( content="The incident channel is #ops-war-room", memory_type="semantic", ) ) memory.supersede(old, new, reason="Team renamed the channel") print([r.content for r in memory.search("incident channel")]) print([ r.content for r in memory.search("incident channel", include_superseded=True) ]) ``` ## Querying what was believed before a change [#querying-what-was-believed-before-a-change] ```python from datetime import datetime, timezone from smartmemory.models.memory_item import MemoryItem from smartmemory.pipeline.config import PipelineConfig from smartmemory.tools.factory import lite_context lite = dict(pipeline_profile=PipelineConfig.lite(llm_enabled=False)) with lite_context(**lite) as memory: old = memory.add( MemoryItem( content="The billing provider is Stripe", memory_type="semantic", metadata={ "reference_time": datetime.now(timezone.utc).isoformat(), }, ) ) t_before_change = datetime.now(timezone.utc) new = memory.add( MemoryItem( content="The billing provider is Paddle", memory_type="semantic", metadata={ "reference_time": datetime.now(timezone.utc).isoformat(), }, ) ) memory.supersede(old, new, reason="Pricing model changed") historical = memory.search( "billing provider", as_of_date=t_before_change, include_superseded=True, ) current = memory.search("billing provider") print([r.content for r in historical]) print([r.content for r in current]) ``` ## Service query API [#service-query-api] Service routes live under `/memory/temporal/*`. The JS SDK mirrors them on `client.memories.*`: | Endpoint | SDK method | Returns | | ---------------------------------------------------------- | -------------------------- | ----------------------------------------- | | `GET /memory/temporal/{id}/history` | `getHistory(id, opts)` | All versions of `id` | | `GET /memory/temporal/at/{timestamp}` | `timeTravel(ts)` | The state of every item at `ts` | | `GET /memory/temporal/{id}/at/{timestamp}` | `getItemAtTime(id, ts)` | The version of `id` valid at `ts` | | `GET /memory/temporal/{id}/changes` | `getChanges(id, opts)` | Change events between `since` and `until` | | `POST /memory/temporal/{id}/compare?v1=&v2=` | `compareVersions(id, ...)` | Field-level diff between two versions | | `POST /memory/temporal/{id}/rollback` | `rollback(id, opts)` | Replay a past version as the new HEAD | | `GET /memory/temporal/{id}/audit` | *(REST only)* | Full audit trail with provenance | | `GET /memory/temporal/search/during` | *(REST only)* | All items valid during a window | | `GET /memory/temporal/compliance/report` | *(REST only)* | Aggregate retention and change report | | `GET /memory/temporal/relationships/{rel_id}/history` etc. | *(REST only)* | Bi-temporal queries on edges | ## JS SDK examples [#js-sdk-examples] ```js const past = await client.memories.timeTravel('2024-09-01T00:00:00Z', { limit: 100, }); ``` ```js const diff = await client.memories.compareVersions('item123', 3, 5); ``` ```js await client.memories.rollback('item123', { toVersion: 3 }); await client.memories.rollback('item123', { toTime: '2024-09-15T12:00:00Z' }); ``` A rollback writes a new version with the chosen prior content. It does not delete the intervening versions, so the audit trail stays intact. ## Use cases [#use-cases] * **Audit and compliance**: answer "what did this user see on date X" exactly. * **Backdated facts**: set `valid_time_start` in the past when ingesting historical records. * **Belief revision**: when a fact changes, the old version stays queryable. * **Diff and blame**: `compareVersions` shows which field changed between any two versions. ## Caveats [#caveats] * Time-travel queries iterate version chains in FalkorDB. For items with hundreds of versions, prefer `getChanges()` with a tight `since/until` window over `getHistory()` with no bound. * `as_of_date` over supersession needs the old record's creation time. In lite examples, preserve it with `metadata["reference_time"]`. * Rollback is best-effort for relations. Edges follow the same bi-temporal model, but rollback only restores node properties. ## Key code surfaces [#key-code-surfaces] * `smartmemory/temporal/queries.py`: `TemporalQueries` * `smartmemory/temporal/version_tracker.py`: version writes * `smartmemory/temporal/relationships.py`: bi-temporal edges # Decision Memory (/smartmemory/concepts/decision-memory) > **Part of the expertise layer.** > [Read the overview →](/smartmemory/concepts/expertise-vs-knowledge/) SmartMemory tracks **decisions** as first-class memory objects with confidence scoring, provenance chains, and lifecycle management. Decisions capture not just *what* was decided, but *why* and *how confident* the system is. ## Why Decision Memory Matters [#why-decision-memory-matters] Agents and users make decisions constantly. Without tracking them: * The same decision gets re-evaluated repeatedly * Contradictory decisions go unnoticed * There's no audit trail for "why did we choose X?" Decision Memory solves this by storing decisions with full provenance, confidence scoring, and conflict detection. ## Decision Types [#decision-types] | Type | Description | Example | | ---------------- | ---------------------------------- | ------------------------------------------------ | | `inference` | Conclusion drawn from evidence | "The performance issue is caused by N+1 queries" | | `preference` | Stated or detected preference | "User prefers dark mode for coding" | | `classification` | Categorization of something | "This issue is a security vulnerability" | | `choice` | Explicit selection between options | "We chose PostgreSQL over MySQL" | | `belief` | Held belief or conviction | "Microservices are overused for small teams" | | `policy` | Rule or guideline to follow | "Always write tests before implementation" | ## Lifecycle [#lifecycle] Decisions follow a lifecycle: * **Active**: Current, valid decision * **Superseded**: Replaced by a newer decision (linked via `SUPERSEDES` edge) * **Retracted**: Withdrawn, no longer valid ## Confidence Scoring [#confidence-scoring] Each decision has a confidence score (0.0–1.0) that evolves over time: ```python from smartmemory.models.decision import Decision decision = Decision( content="PostgreSQL is better for our use case", decision_type="choice", confidence=0.8 ) # Supporting evidence increases confidence (diminishing returns) decision.reinforce("benchmark_result_id") # confidence: 0.8 → 0.82 # Contradicting evidence decreases confidence (15% penalty) decision.contradict("mysql_benchmark_id") # confidence: 0.82 → 0.697 ``` ### Automatic Decay [#automatic-decay] The `DecisionConfidenceEvolver` applies time-based decay: * Decisions inactive for 30+ days start losing confidence (5% per cycle) * Decisions below 0.1 confidence are automatically retracted * Recently reinforced decisions are protected from decay ### Belief & Contest (Dempster-Shafer) [#belief--contest-dempster-shafer] The single `confidence` scalar can't distinguish a **contested** decision (strong support *and* strong contradiction) from an **unevidenced** one — both look like "medium confidence". SmartMemory adds Dempster-Shafer evidence belief over the frame `{hold, retract}` to separate them: | Field | Meaning | | ------------------- | --------------------------------------------------------------------------------------------- | | `belief_hold` | committed lower bound that the decision should stand | | `plausibility_hold` | upper bound (adds the ignorance band) | | `ignorance` | how much is genuinely unknown (no evidence either way) | | `contest` | **how disputed** the decision is — high only when support *and* contradiction are both strong | All four ship in decision API responses, so SDK, MCP and UI consumers get them without recomputing the kernel. A decision with 3 supporting and 3 contradicting pieces of evidence reports `contest ≈ 0.77` while an untouched decision reports `contest = 0.0`, even though both show the same `stability`. The belief reads are evidence-only and carry no prior. An untouched decision reads `belief_hold = 0.0` and `ignorance = 1.0` whatever its `confidence`, because `confidence` carries the prior and the belief state carries the evidence. That is how a caller says "the system does not know yet" rather than asserting a number it has nothing to back. The values are recomputed from the evidence counts on every read, so a stale persisted copy can never win. ## Creating Decisions [#creating-decisions] ### Programmatic [#programmatic] ```python from smartmemory.decisions.manager import DecisionManager manager = DecisionManager(smart_memory) decision = manager.create( content="We should use Redis for caching", decision_type="choice", confidence=0.85, domain="infrastructure", tags=["caching", "architecture"], evidence_ids=["benchmark_123", "analysis_456"], ) ``` ### Via Extraction [#via-extraction] The `DecisionExtractor` detects decisions in natural text: ```python from smartmemory.plugins.extractors.decision import DecisionExtractor extractor = DecisionExtractor() result = extractor.extract(""" I decided to use PostgreSQL for the new project. I believe microservices are overkill for our team size. We should always write integration tests for API endpoints. """) # result['decisions'] contains 3 detected decisions: # - choice: "use PostgreSQL for the new project" # - belief: "microservices are overkill for our team size" # - policy: "always write integration tests for API endpoints" ``` It also extracts from reasoning traces: ```python from smartmemory.models.reasoning import ReasoningTrace trace = ReasoningTrace(steps=[...]) decisions = extractor.extract_from_trace(trace) # Decisions linked to the trace via PRODUCED edge ``` ## Querying Decisions [#querying-decisions] ### List Active Decisions [#list-active-decisions] ```python from smartmemory.decisions.queries import DecisionQueries queries = DecisionQueries(smart_memory) # All active decisions decisions = queries.get_active_decisions() # Filtered by domain and type decisions = queries.get_active_decisions( domain="infrastructure", decision_type="choice", min_confidence=0.7, ) ``` ### Search by Topic [#search-by-topic] ```python decisions = queries.get_decisions_about(topic="database") ``` ## Provenance Chains [#provenance-chains] Every decision tracks its origin: ```python provenance = queries.get_decision_provenance(decision_id) # Returns: # { # "decision": Decision(...), # "reasoning_trace": ReasoningTrace(...) or None, # "evidence": [MemoryItem(...), ...], # "superseded": [Decision(...), ...], # } ``` ## Causal Chains [#causal-chains] Traverse cause-and-effect relationships: ```python chain = queries.get_causal_chain( decision_id, direction="both", # "causes", "effects", or "both" max_depth=3, ) # Returns: # { # "decision": Decision(...), # "causes": [...], # What led to this decision # "effects": [...], # What this decision caused # } ``` ## Conflict Detection [#conflict-detection] Find decisions that may contradict each other: ```python conflicts = manager.find_conflicts(decision) # Candidate gate: semantic search + content-overlap heuristic. # Results are ranked by pairwise contest severity (how confidently both decisions # assert their contradictory claims), most-contested first. # Drop weakly-held conflicts: conflicts = manager.find_conflicts(decision, min_contest=0.3) ``` The REST route `POST /memory/decisions/{id}/conflicts` accepts the same `min_contest` query parameter. ## Superseding Decisions [#superseding-decisions] When a decision is replaced: ```python new_decision = Decision( content="Actually, MongoDB fits better for our document-heavy workload", decision_type="choice", confidence=0.9, ) result = manager.supersede( old_decision_id, new_decision, reason="New benchmarks show MongoDB handles our access patterns better", ) # Old decision marked as "superseded" # SUPERSEDES edge created between new → old # Reason and timestamp recorded on the old decision's context_snapshot ``` The reason travels on the superseded decision itself, in `context_snapshot["superseded_reason"]` alongside `context_snapshot["superseded_at"]`. That matters because `status` and `superseded_by` are machine pointers: a reader that renders only the decision text would otherwise show replaced content as if it were still in force. Reading the old decision now tells you it was replaced and why, with no extra lookup. ```python old = manager.get_decision(old_decision_id) old.status # "superseded" old.superseded_by # the replacement's id old.context_snapshot["superseded_reason"] # why it was replaced ``` ## Retracting Decisions [#retracting-decisions] When a decision is no longer valid: ```python manager.retract(decision_id, reason="Requirements changed — JWT no longer needed") # Decision status set to "retracted" # Retracted decisions are excluded from active queries ``` ## Graduation from Plans [#graduation-from-plans] When a [Plan](/smartmemory/concepts/plan-memory/) completes, it can optionally **graduate** to a decision record — preserving the task decomposition as a permanent "we decided to do X" record: ```python from smartmemory.plans import PlanManager plan_manager = PlanManager(memory) result = plan_manager.create("Auth redesign", tasks=[...]) # ... tasks completed ... # Graduate creates a decision with DERIVED_FROM edge back to plan decision_id = plan_manager.complete(result["plan_id"], graduate=True) ``` The graduated decision has `decision_type="choice"`, `source_type="imported"`, and an evidence link to the original plan. ## Graph Edges [#graph-edges] Decision Memory introduces these edge types: | Edge | From → To | Meaning | | -------------- | ------------------------- | ------------------------------- | | `PRODUCED` | ReasoningTrace → Decision | Trace generated this decision | | `DERIVED_FROM` | Decision → Memory | Decision based on this evidence | | `SUPERSEDES` | Decision → Decision | New decision replaces old | | `CONTRADICTS` | Decision → Decision | Decisions conflict | | `INFLUENCES` | Decision → Memory | Decision affects other memories | ## Configuration [#configuration] ### DecisionExtractor [#decisionextractor] ```python from smartmemory.plugins.extractors.decision import DecisionExtractorConfig config = DecisionExtractorConfig( min_confidence=0.7, # Minimum confidence for extracted decisions min_content_length=20, # Minimum content length ) ``` ### DecisionConfidenceEvolver [#decisionconfidenceevolver] ```python from smartmemory.plugins.evolvers.decision_confidence import DecisionConfidenceConfig config = DecisionConfidenceConfig( min_confidence_threshold=0.1, # Auto-retract below this decay_after_days=30, # Start decay after inactivity decay_rate=0.05, # 5% confidence loss per cycle enable_decay=True, ) ``` ## REST API [#rest-api] Decision Memory is exposed via 12 endpoints at `/memory/decisions/`: | Method | Path | Description | | ------ | -------------------- | ----------------------- | | POST | `/create` | Create a decision | | GET | `/` | List active decisions | | GET | `/search` | Search by topic | | GET | `/pending` | List pending decisions | | GET | `/{id}` | Get a decision | | POST | `/{id}/supersede` | Replace a decision | | POST | `/{id}/retract` | Retract a decision | | POST | `/{id}/reinforce` | Add supporting evidence | | POST | `/{id}/contradict` | Add counter-evidence | | GET | `/{id}/provenance` | Full provenance chain | | GET | `/{id}/causal-chain` | Causal traversal | | POST | `/{id}/conflicts` | Find conflicts | # Evolution Algorithms (/smartmemory/concepts/evolution-algorithms) # Evolution Algorithms [#evolution-algorithms] SmartMemory's evolution system continuously adjusts memory retention, promotes important memories, and strengthens relationships between frequently co-activated memories. Evolution plugins run as the final stage (`evolve`) of the ingestion pipeline, and on-demand via the Evolution API. ## Overview [#overview] Evolution operates at two levels: 1. **Node-level evolution** — individual memories are promoted, decayed, or had their `retention_score` adjusted based on age, retrieval frequency, and inter-memory interference. 2. **Edge-level evolution** — relationships between memories grow stronger when those memories are consistently retrieved together (Hebbian reinforcement). All evolvers are implemented as `EvolverPlugin` subclasses in `smartmemory/plugins/evolvers/`. The enhanced evolvers (CORE-EVO-ENH series) operate on the workspace graph and write results back via `SmartMemory.update_properties()`. *** ## Core Evolvers [#core-evolvers] These evolvers handle memory lifecycle transitions — promotion between types, synthesis, and reinforcement of opinion/decision memories. | Evolver | What It Does | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **EpisodicToSemanticEvolver** | Promotes stable, high-confidence episodic events to long-term semantic memory. Requires configurable confidence threshold (default: 0.9) and minimum age (default: 3 days). | | **EpisodicToZettelEvolver** | Rolls up episodic events into zettelkasten notes on a periodic basis. | | **OpinionSynthesisEvolver** | Detects recurring patterns in episodic memories and forms opinion-type memories with confidence scores. Uses LLM for pattern detection. | | **ObservationSynthesisEvolver** | Creates entity summaries by synthesizing scattered facts across memories (e.g., combines "Alice works at Google" + "Alice started in 2020" into a coherent observation). | | **OpinionReinforcementEvolver** | Updates opinion confidence based on new evidence: supporting evidence boosts (+0.1), contradicting penalizes (−0.15). Applies time-based decay; archives below minimum confidence (0.2). | | **DecisionConfidenceEvolver** | Same pattern as opinion reinforcement but for decision-type memories. | *** ## Enhanced Evolvers (CORE-EVO-ENH Series) [#enhanced-evolvers-core-evo-enh-series] The enhanced evolver suite applies cognitive-science-inspired algorithms to improve retention quality over time. These evolvers run in sequence as part of `run_enhanced_evolution_cycle()`. ### ExponentialDecayEvolver (CORE-EVO-ENH-1) [#exponentialdecayevolver-core-evo-enh-1] Implements the Ebbinghaus forgetting curve for episodic memories: ``` retention = exp(−elapsed_days / stability) ``` `stability` is a per-memory value stored in `metadata["stability"]` (default: 30 days, reflecting how well-consolidated a memory is). Items that fall below `archive_threshold` (default: 0.1) are soft-archived automatically. **Config:** `default_stability=30.0`, `archive_threshold=0.1`, `max_memories=500` ### InterferenceBasedConsolidationEvolver (CORE-EVO-ENH-1) [#interferencebasedconsolidationevolver-core-evo-enh-1] Models retroactive and proactive interference: when two memories are highly similar, they compete for consolidation. For each embedded memory, searches up to `top_k_neighbors` similar items and applies a cosine-similarity-based retention penalty to pairs above `similarity_threshold`: ``` new_score = current_score × (1 − similarity × interference_weight) ``` Pair deduplication ensures each `(A, B)` pair is penalized exactly once. Items without embeddings are skipped. **Config:** `similarity_threshold=0.85`, `interference_weight=0.05`, `top_k_neighbors=5`, `max_memories=200` ### RetrievalBasedStrengtheningEvolver (CORE-EVO-ENH-2) [#retrievalbasedstrengtheningevolver-core-evo-enh-2] Implements the testing effect: memories that are frequently retrieved retain better; unretrieved memories decay faster. Reads `metadata["retrieval__profile"]` (written by `RetrievalFlushConsumer` every 5 minutes) and applies: ``` rank_weight = 1 / (1 + avg_search_rank) recency_weight = velocity_7d / (velocity_30d + ε) boost = retrieval_weight × log(1 + total_count) × rank_weight × recency_weight retention = min(1.0, base_retention + boost) ``` For memories with zero retrievals in the lookback window, a gentle unretrieved decay is applied instead: `retention *= (1 − rate × elapsed_days)`. **Config:** `retrieval_weight=0.1`, `unretrieved_decay_rate=0.005`, `lookback_days=30`, `max_memories=500` **Retrieval profile** (written to `metadata["retrieval__profile"]` by the worker): ```json { "total_count": 47, "search_count": 44, "avg_search_rank": 2.3, "velocity_7d": 3.1, "velocity_30d": 0.8, "top1_count": 12, "sources": {"api.POST /memory/search": 28} } ``` ### HebbianCoRetrievalEvolver (CORE-EVO-ENH-3) [#hebbiancoretrievalevolver-core-evo-enh-3] Implements Hebbian learning at the graph edge level: memories that consistently appear together in the same search result set accumulate stronger `CO_RETRIEVED` edges in FalkorDB over time. The evolver then boosts `retention_score` on nodes connected by the strongest edges. **"Neurons that fire together, wire together"** — but applied to memory retrieval rather than neurons. **How it works:** 1. `RetrievalFlushConsumer` extracts 2-combinations of `item_id`s from every search result batch. Each co-occurring pair gets its `CO_RETRIEVED` edge incremented: `co_retrieval_count += batch_count`. 2. `HebbianCoRetrievalEvolver` queries edges with `co_retrieval_count >= weight_threshold`, deduplicates by `item_id`, and applies: ``` boost = min(max_boost, (co_retrieval_count − weight_threshold) × retention_boost_per_unit) new_retention = min(1.0, existing_retention + boost) ``` **Config:** `weight_threshold=3.0`, `max_edges_per_cycle=500`, `retention_boost_per_unit=0.02`, `max_boost=0.3` **Example:** A pair co-retrieved 10 times with defaults gets `boost = min(0.3, (10−3) × 0.02) = 0.14`. **CO\_RETRIEVED edge properties:** | Property | Type | Description | | ----------------------- | --------- | ------------------------------------------------------------------ | | `co_retrieval_count` | int | Total times this pair has been co-retrieved | | `weight` | float | Hebbian weight (= co\_retrieval\_count, reserved for future decay) | | `first_co_retrieved_at` | str (ISO) | Timestamp of first co-retrieval | | `last_co_retrieved_at` | str (ISO) | Timestamp of most recent co-retrieval | *** ## Metadata Conventions [#metadata-conventions] Enhanced evolvers read and write standardized `metadata` fields on `MemoryItem`: | Field | Type | Written By | Read By | | -------------------- | ------------------------------ | ------------------------------------- | ------------------------------------ | | `retention_score` | float \[0, 1] | All enhanced evolvers | ExponentialDecay, Retrieval, Hebbian | | `stability` | float (days) | Set at ingest or by user | ExponentialDecayEvolver | | `archived` | bool | ExponentialDecayEvolver | Archive system | | `archive_reason` | str | ExponentialDecayEvolver | Archive system | | `interference_count` | int | InterferenceBasedConsolidationEvolver | Observability | | `retrieval__profile` | dict (JSON string in FalkorDB) | RetrievalFlushConsumer | RetrievalBasedStrengtheningEvolver | *** ## Evolution Cycle [#evolution-cycle] To run all enhanced evolvers for a workspace: ```python from smartmemory.plugins.evolvers.enhanced import run_enhanced_evolution_cycle # memory is a workspace-scoped SmartMemory instance run_enhanced_evolution_cycle(memory) ``` The cycle runs evolvers in registration order: 1. `EnhancedWorkingToEpisodicEvolver` 2. `ExponentialDecayEvolver` 3. `InterferenceBasedConsolidationEvolver` 4. `RetrievalBasedStrengtheningEvolver` 5. `HebbianCoRetrievalEvolver` Each evolver catches its own exceptions — a single evolver failure does not abort the cycle. The Evolution API also exposes manual triggers: | Method | Path | Description | | ------ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `POST` | `/memory/evolution/trigger` | Run full evolution cycle for the authenticated workspace | | `POST` | `/memory/evolution/dream` | **Deprecated** — routing now happens at-ingest (CORE-MEMORY-DYNAMICS-1 M1b); retained for compatibility | | `GET` | `/memory/evolution/status` | Get evolution readiness status | | `POST` | `/memory/evolution/synthesize/opinions` | Run opinion synthesis | | `POST` | `/memory/evolution/synthesize/observations` | Run observation synthesis | | `POST` | `/memory/evolution/reinforce/opinions` | Run opinion reinforcement | *** ## Memory Lifecycle [#memory-lifecycle] ``` Pending Memory │ ├──> Episodic Memory (WorkingToEpisodic: buffer full) │ │ │ ├──> Semantic Memory (EpisodicToSemantic: high confidence + age) │ │ │ ├──> Zettel Notes (EpisodicToZettel: periodic rollup) │ │ │ ├──> Opinions (OpinionSynthesis: pattern detection) │ │ │ └──> Observations (ObservationSynthesis: entity summaries) │ └──> Procedural Memory (WorkingToProcedural: repeated patterns) Decay: ExponentialDecay · InterferenceBased → retention_score ↓ Boost: RetrievalStrengthening · HebbianCoRetrieval → retention_score ↑ Edges: CO_RETRIEVED (HebbianCoRetrieval) → co_retrieval_count ↑ ``` # Expertise vs Knowledge (/smartmemory/concepts/expertise-vs-knowledge) # Expertise vs Knowledge [#expertise-vs-knowledge] Most "memory for AI" today stores **knowledge**, facts derivable from documents, chat history, or source code. That's table stakes, and SmartMemory does it well. The harder, scarcer thing is **expertise**: the choices that were made, the alternatives that were rejected, the constraints that were discovered the hard way, the opinions that hardened over time, the reasoning that justified a contested call. None of that is recoverable from a corpus. It only exists *because someone decided, tried, failed, learned*. SmartMemory treats expertise as a first-class layer with its own memory types, its own capture API, and its own recall channel. ## The distinction in one sentence [#the-distinction-in-one-sentence] > Knowledge is what's *true*. Expertise is what to *do*, and what *not* to do. A retrieval system that only returns knowledge tells the agent *what is the case*. An expertise layer also tells it *what was rejected, what was constrained, what bit you last time, what you came to believe*. ## The mapping [#the-mapping] Every memory type SmartMemory supports falls cleanly on one side of the line: | Side | Memory type | What it captures | Capture API | | ------------- | ------------- | ------------------------------------------------------------------ | -------------------------------- | | **Knowledge** | `semantic` | Durable facts | `add()` | | **Knowledge** | `episodic` | What happened, when | `add()` (episodes) | | **Knowledge** | `procedural` | How to do something | `add_plan()` / procedural ingest | | **Knowledge** | `zettel` | Linked notes, ideas-in-progress | `add()` (zettel) | | **Expertise** | `decision` | A choice made *with rejected alternatives, rationale, constraints* | `add_decision()` | | **Expertise** | `constraint` | A hard rule discovered (or imposed) | `add_constraint()` | | **Expertise** | `learned` | A lesson the hard way | `add_learning()` | | **Expertise** | `opinion` | A held belief, with confidence | `add_opinion()` | | **Expertise** | `reasoning` | A reasoning trace: premises → conclusion | `add_reasoning()` | | **Expertise** | `observation` | An attested fact about the world or self | `add_observation()` | Each expertise type has its own Manager + Queries + lifecycle (supersession, reinforcement, contradiction handling). They aren't just metadata tags on a generic memory item. ## Recall: the expertise channel [#recall-the-expertise-channel] Default `search()` returns a flat list ranked by hybrid retrieval, knowledge and expertise mixed together. When the agent is reasoning about a *decision* like "what's our position on X", "have we tried Y before", or "what's the constraint here", flat search isn't the right shape. Expertise needs to come back *partitioned* by type. ```python from smartmemory.pipeline.config import PipelineConfig from smartmemory.tools.factory import lite_context lite = dict(pipeline_profile=PipelineConfig.lite(llm_enabled=False)) with lite_context(**lite) as mem: decision = mem.add_decision( "Adopt FalkorDB as the graph substrate", rejected_alternatives=["Neo4j (license)", "Memgraph (ops weight)"], rationale="Smallest ops surface, vector-native, permissive license.", constraints=["Must support Cypher subset"], ) constraint = mem.add_constraint( "Never store PII in graph properties, always reference IDs." ) learned = mem.add_learning( "Mocking the database in integration tests masks migration bugs." ) # Capture wrappers return typed objects, not string IDs. print(decision.decision_id, constraint.constraint_id, learned.learned_id) # Recall a typed dict, bucketed by expertise type. results = mem.search("graph database choice", expertise=True) print([item.content for item in results["decision"]]) print([item.content for item in results["constraint"]]) print([item.content for item in results["learned"]]) ``` The recall channel ranks each bucket independently with a recency and confidence boost over the base hybrid score, then returns up to `top_k` items per bucket. The shape is stable: all six keys are always present, even when empty. ## What this is not [#what-this-is-not] * **Not a reasoning system.** SmartMemory stores reasoning traces. It does not *generate* new ones. That's the agent's job. * **Not a contradiction detector.** Contradictions across decisions and opinions are surfaced via the supersession lifecycle, not derived automatically. * **Not a replacement for semantic memory.** Knowledge and expertise are complementary. Most agents need both. * **Not free.** Expertise has to be *captured*. The wrappers exist for exactly this reason. An agent that only ever calls `add()` will accumulate knowledge but never expertise. ## Where to next [#where-to-next] * [Decision Memory](/smartmemory/concepts/decision-memory/), the original expertise type, now with structured `rejected_alternatives` / `rationale` / `constraints` fields. * [Opinions & Observations](/smartmemory/concepts/opinions-observations/), held beliefs, attested facts, the reinforcement lifecycle. * [Reasoning Traces](/smartmemory/concepts/reasoning-traces/), premises to conclusion, validation, supersession. * [Memory Types](/smartmemory/concepts/memory-types/), the full type catalogue. # Graph Health (/smartmemory/concepts/graph-health) SmartMemory measures the structural health of your knowledge graph through metrics like orphan ratio, provenance coverage, and type distribution. ## Health Metrics [#health-metrics] ### GraphHealthChecker [#graphhealthchecker] Collects health metrics via Cypher queries: | Metric | Description | Healthy Threshold | | --------------------- | ------------------------------------------- | ----------------- | | `total_nodes` | Number of nodes in the graph | — | | `total_edges` | Number of edges in the graph | — | | `orphan_count` | Nodes with zero edges | — | | `orphan_ratio` | orphan\_count / total\_nodes | \< 0.2 (20%) | | `provenance_coverage` | Fraction of decisions with provenance edges | > 0.5 (50%) | | `type_distribution` | Count of nodes by memory type | — | | `edge_distribution` | Count of edges by edge type | — | ### HealthReport [#healthreport] The `HealthReport` dataclass provides an `is_healthy` property that checks: * Orphan ratio is below 20% * Provenance coverage is above 50% ## Usage [#usage] ```python from smartmemory.metrics import GraphHealthChecker checker = GraphHealthChecker(smart_memory) report = checker.collect_health() print(f"Healthy: {report.is_healthy}") print(f"Orphan ratio: {report.orphan_ratio:.1%}") print(f"Provenance coverage: {report.provenance_coverage:.1%}") ``` ## REST API [#rest-api] | Method | Endpoint | Description | | ------ | ---------------------- | ---------------------- | | GET | `/memory/graph/health` | Collect health metrics | # Graph Validation (/smartmemory/concepts/graph-validation) SmartMemory validates memory items and edges at runtime against schema constraints, catching data quality issues before they propagate through the knowledge graph. ## Why Validation Matters [#why-validation-matters] Without validation: * Malformed memories enter the graph silently * Invalid edges connect unrelated node types * Orphaned nodes accumulate over time * Provenance gaps make reasoning unreliable ## Components [#components] ### MemoryValidator [#memoryvalidator] Validates individual memory items against schema rules: | Check | Severity | Description | | ------------------- | -------- | ---------------------------------------- | | Empty content | Error | Items must have non-empty content | | Unknown memory type | Warning | Type not in registered MEMORY\_TYPES set | | Confidence range | Error | Must be between 0.0 and 1.0 | | Decision fields | Warning | Decision items should have decision\_id | | Reasoning fields | Warning | Reasoning items should have trace\_id | ### EdgeValidator [#edgevalidator] Validates edges against registered `EdgeSchema` definitions: | Check | Severity | Description | | ------------------- | -------- | -------------------------------------- | | Unknown edge type | Warning | Edge type not in schema registry | | Invalid source type | Error | Source node type not allowed by schema | | Invalid target type | Error | Target node type not allowed by schema | Also provides: * **Orphan detection**: Find nodes with zero edges * **Provenance gap detection**: Find nodes missing DERIVED\_FROM, CAUSED\_BY, or PRODUCED edges ## Usage [#usage] ```python from smartmemory.validation import MemoryValidator, EdgeValidator validator = MemoryValidator(smart_memory) result = validator.validate_item(item) if not result.is_valid: for error in result.errors: print(f"ERROR: {error.field} - {error.message}") edge_validator = EdgeValidator(smart_memory) orphans = edge_validator.find_orphan_nodes() ``` ## REST API [#rest-api] | Method | Endpoint | Description | | ------ | ---------------------- | ------------------------ | | POST | `/memory/validate` | Validate a specific item | | GET | `/memory/graph/health` | Get graph health metrics | # Hybrid Retrieval (/smartmemory/concepts/hybrid-retrieval) Server hybrid retrieval combines enabled channels through weighted Reciprocal Rank Fusion with k=60. The native lexical channel contributes once with default weight 0.8. Other channels can contribute semantic or graph matches, so the final answer is not restricted to lexical AND matches. ```python results = memory.search("project bedrock", channel_weights={"lexical": 0.8}) # Explicit zero disables the lexical contribution. results = memory.search("project bedrock", channel_weights={"lexical": 0}) ``` Accepted channels are `entity-graph`, `ssg-traversal`, `semantic`, `regex-text`, `lexical`, `spreading-activation`, `facts` and `structural-semantic`. The removed `contains` and `keyword-bm25` names raise a validation error naming lexical. Replace owned defaults with one lexical weight of 0.8. Custom weights need an explicit choice. Lexical terms combine with AND. Quoted phrases preserve word order, repeated tokens and stopwords. Punctuation separates words. Unmatched quotes fail validation. Empty, nonalphanumeric and wholly-stopword input contributes no lexical matches. Public syntax does not expose OR, prefix, wildcard, field-selector or fuzzy operators. | Backend | Lexical index | Native ranking | | --------------- | ------------------------------------------------------------------ | ------------------------ | | SQLite lite | Contentful FTS5, unicode61 remove\_diacritics 0 | bm25 ascending | | Pinned FalkorDB | Item owner\_key/content full-text, no stemming, retained stopwords | Default score descending | Equal scores break by content then item ID. Cross-engine score or tokenizer identity is not promised. Scoped ownership is resolved before native LIMIT. SQLite lite is single-tenant, and its scoped SQL harness does not establish hosted isolation. Lite runs graph-first search with conditional FTS5 lexical fill, then vector fill. A full graph result skips lexical fill. `enable_hybrid` does not select lite dispatch. On the server, disabling hybrid keeps sequential first-nonempty retrieval and does not imply vector-only search. Bounded refill and hydration postfilters may underfill top\_k. Repeated-token phrases receive bounded candidate verification. The separate `regex-text` lane still scans. CORE-FACT-INDEX-1 owns the deferred fact ranking and path expansion work. Required lexical unavailability fails the entire search with LexicalIndexUnavailableError, surfaced by the service as HTTP 503. There is no successful partial 200 or scan fallback. Query validation returns HTTP 400. Neo4j lexical search is unsupported. First-open indexing is O(N). Quiesce old writers, verify the immutable capability pin, and keep maintenance exclusive until readiness checks complete. Use `sm rebuild --lexical` for the default SQLite target or explicitly select `--backend falkordb` with the intended graph configuration. See [migration and recovery](https://github.com/smartmemory/smart-memory-docs/blob/main/docs/features/CORE-LEXICAL-INDEX-1/migration.md) and the [versioned lexical contract](https://github.com/smartmemory/smart-memory-docs/blob/main/docs/features/CORE-LEXICAL-INDEX-1/lexical-contract.json) for supported semantics, pinning, stored-profile migration and measured limitations. # Hybrid Triple Storage Architecture (/smartmemory/concepts/hybrid-storage) # Hybrid Triple Storage Architecture [#hybrid-triple-storage-architecture] SmartMemory implements a **hybrid triple storage architecture** that combines three complementary storage systems to provide optimal performance, flexibility, and intelligence. This architecture enables sophisticated memory operations while maintaining high performance and scalability. ## Architecture Overview [#architecture-overview] ## Storage Components [#storage-components] ### 🌐 Graph Database Layer [#-graph-database-layer] **Primary Functions:** * Entity Storage: People, places, concepts, and objects * Relationship Mapping: Semantic, temporal, and causal relationships * Knowledge Graph: Structured representation of domain knowledge * Path Analysis: Finding connections between distant entities **Shipped Backends:** * **`FalkorDBBackend`** (default): Redis-based graph database — also stores vectors (see below) * **`SQLiteBackend`**: File-based graph backend for local/lite development ### 🎯 Vector Store Layer [#-vector-store-layer] **Primary Functions:** * Semantic Embeddings: Dense vector representations * Similarity Search: Finding semantically related memories * Clustering: Grouping similar memories automatically * Dimensionality Operations: Efficient storage and retrieval **Shipped Backends:** * **`falkordb`** (default, `FalkorVectorBackend`): vectors are stored **inside FalkorDB** using its native HNSW vector index — the same database that holds the knowledge graph. Embeddings are 768-dim with cosine similarity by default. * **`usearch`** (`UsearchVectorBackend`, optional): lightweight local vector index, used when the usearch dependency is installed. Because the graph and the vectors live in the **same FalkorDB instance**, "hybrid" does not require a second standalone vector service in the default deployment. ### 📋 Metadata Store Layer [#-metadata-store-layer] **Primary Functions:** * Temporal Data: Creation time, modification history * User Context: User-specific information, permissions * Classification: Memory types, quality scores * System Metadata: Indexing information, statistics Metadata is carried on each memory item and persisted alongside the graph. For file-based persistence, the shipped **`JSONFileBackend`** stores items and their metadata as JSON. ## Query Processing [#query-processing] ## Benefits of Hybrid Architecture [#benefits-of-hybrid-architecture] | Aspect | Benefit | Implementation | | ---------------- | ------------------------- | ---------------------------------------------------- | | **Performance** | Optimal query performance | Each storage type optimized for specific operations | | **Scalability** | Independent scaling | Each layer can scale based on workload | | **Flexibility** | Multiple query patterns | Graph traversal, semantic search, metadata filtering | | **Reliability** | Fault tolerance | Redundancy across storage types | | **Intelligence** | Rich data relationships | Combines structural and semantic understanding | ## Use Cases [#use-cases] ### 🔍 **Semantic Search** [#-semantic-search] ```python # search() fuses the vector and graph channels automatically results = memory.search("machine learning algorithms", top_k=5) ``` ### 🌐 **Relationship Discovery** [#-relationship-discovery] ```python # Uses the graph database to find the shortest path between two items path = memory.find_shortest_path(start_id="john", end_id="techcorp", max_hops=3) # Or expand a single item's immediate neighborhood neighbors = memory.get_neighbors("john", direction="both") ``` ### ⏰ **Temporal Queries** [#-temporal-queries] ```python # Bi-temporal "transaction-time travel": see memory as it existed on a date results = memory.search("Python programming", as_of_date="2024-01-31") ``` ### 🎯 **Multi-Hop Retrieval** [#-multi-hop-retrieval] ```python # Chain retrieval across hops; results from one hop seed the next results = memory.search( "Python programming", multi_hop=True, max_hops=3, budget_ms=1500, ) ``` ## Configuration [#configuration] ```json { "graph_db": { "backend_class": "FalkorDBBackend", "host": "localhost", "port": 9010, "graph_name": "smartmemory" }, "vector": { "backend": "falkordb", "dimension": 768, "metric": "cosine", "host": "localhost", "port": 9010 } } ``` The graph backend is selected by the `graph_db.backend_class` key (default `FalkorDBBackend`; `SQLiteBackend` is the lite alternative). The vector backend is selected by the `vector.backend` key (default `falkordb`, which co-locates the vector index with the graph in the same FalkorDB instance). This hybrid architecture enables SmartMemory to provide both the structural intelligence of graph databases and the semantic understanding of vector stores, while maintaining efficient metadata operations for optimal performance. # Inference Engine (/smartmemory/concepts/inference-engine) SmartMemory automatically enriches the knowledge graph by applying inference rules that create new edges from existing patterns — similar to OWL-RL deductive closure. ## Built-in Rules [#built-in-rules] | Rule | Pattern | Created Edge | Confidence | | ---------------------- | ------------------------------- | ---------------------------- | --------------------- | | Causal Transitivity | A CAUSES B, B CAUSES C | A CAUSES C | 0.7 x (conf1 x conf2) | | Contradiction Symmetry | A CONTRADICTS B | B CONTRADICTS A | 1.0 | | Topic Inheritance | Decision DERIVED\_FROM Semantic | Semantic INFLUENCES Decision | 0.6 | ## How It Works [#how-it-works] 1. Each rule has a **pattern Cypher** query that finds matching node pairs 2. For each match, the engine creates a new edge with provenance metadata 3. Metadata includes: `rule_name`, `confidence`, `inferred_at`, `inferred=true` 4. Confidence is computed per-rule (e.g., transitive chains multiply confidence) ## Usage [#usage] ```python from smartmemory.inference import InferenceEngine engine = InferenceEngine(smart_memory) # Preview what would be created result = engine.run(dry_run=True) print(f"Would create {result.edges_created} edges") # Apply for real result = engine.run(dry_run=False) print(f"Created {result.edges_created} inferred edges") ``` ## Custom Rules [#custom-rules] ```python from smartmemory.inference import InferenceRule custom_rule = InferenceRule( name="topic_similarity", description="Link items sharing the same topic", pattern_cypher="MATCH (a)-[:HAS_TOPIC]->(t)<-[:HAS_TOPIC]-(b) WHERE a <> b RETURN a, b", edge_type="SIMILAR_TOPIC", confidence=0.5, ) engine = InferenceEngine(smart_memory, rules=[custom_rule]) ``` ## REST API [#rest-api] | Method | Endpoint | Description | | ------ | ------------------------- | -------------------- | | POST | `/memory/inference` | Run inference engine | | GET | `/memory/inference/rules` | List available rules | # Ingestion Flow (/smartmemory/concepts/ingestion-flow) SmartMemory's ingestion pipeline transforms raw input into enriched, interconnected memories through a sophisticated multi-stage process. ## Overview [#overview] The ingestion flow consists of **11 stages** that process memories from initial input to final storage: 1. **classify** - Determine memory type (semantic, episodic, procedural, pending, zettel) 2. **coreference** - Resolve pronouns and entity mentions 3. **simplify** - Optional sentence simplification for cleaner extraction 4. **entity\_ruler** - Pattern-based entity tagging (public knowledge patterns + workspace patterns) 5. **llm\_extract** - LLM entity & relation extraction (SpacyExtractor as deterministic fallback) 6. **ontology\_constrain** - Constrain extracted types against the active ontology 7. **store** - Persist memory node + entity nodes (and embeddings) in FalkorDB 8. **link** - Connect the new item to related existing memories 9. **enrich** - Optional enrichers (basic, sentiment, topic, temporal, link expansion, skills/tools) 10. **ground** - Link entities to Wikidata/Wikipedia via `GROUNDED_IN` edges 11. **evolve** - Run evolvers (episodic→semantic, episodic→zettel, decay, opinion/observation synthesis, …) Entity clustering and bi-temporal version records run as separate post-pipeline operations rather than as in-pipeline stages — see [Hybrid Storage](/smartmemory/concepts/hybrid-storage/) and `memory.run_clustering()`. ## Stage Details [#stage-details] Raw input can be a plain string or a dict with content, metadata, and an explicit type: ```python memory.ingest("I learned Python programming in 2020") memory.ingest({ "content": "Meeting notes from today", "metadata": {"participants": ["Alice", "Bob"]}, "memory_type": "episodic" }) ``` **Note:** Use `ingest()` for full pipeline processing. Use `add()` for simple storage without extraction/linking/evolution. Use `ingest_structured()` for schema-mapped data that should bypass the NLP pipeline entirely, see [Structured Ingestion](#structured-ingestion) below. ### 1. classify [#1-classify] Decides which memory type the item becomes. **Automatic classification:** * Temporal markers → Episodic * Factual statements → Semantic * Process descriptions → Procedural * Atomic, cross-linked notes → Zettel * Everything else stays → Pending, the staging type **Manual override:** ```python memory.add(content, memory_type="semantic", force=True) ``` ### 2. coreference [#2-coreference] Resolves pronouns and repeated entity mentions to a single referent, so "she" and "Dr. Chen" collapse into one entity downstream instead of two. ### 3. simplify [#3-simplify] Optional sentence simplification. Long, clause-heavy sentences are broken into simpler ones so the extractor sees cleaner subject-verb-object structure. Disabled in profiles that skip LLM work. ### 4. entity\_ruler [#4-entity_ruler] Deterministic, pattern-based entity tagging that runs before any model is called. It applies the shipped public-knowledge patterns plus any workspace-specific patterns you have seeded, which makes well-known names and product terms stable across ingests and cheaper than an LLM pass. ### 5. llm\_extract [#5-llm_extract] Entity and relation extraction. This is where the bulk of the knowledge graph comes from. **Entity extraction:** * People, places, organizations * Dates, times, durations * Technical concepts and terms * Actions and events **Triple / relationship extraction:** * Subject-predicate-object triples * Temporal relationships (BEFORE, AFTER, DURING) * Causal connections (CAUSES, RESULTS\_IN) * Hierarchical structures (PART\_OF, CONTAINS) * Conversational flow (RESPONDS\_TO, FOLLOWS) #### Available Extractors [#available-extractors] The shipped plugins live in `smartmemory/plugins/extractors/`. The `llm_extract` stage routes through whichever extractor is configured, and `SpacyExtractor` is used as a deterministic fallback when LLM extraction is unavailable. **1. `LLMExtractor` (`llm`)** — default LLM-based extractor with structured triple output (entities + relationships). **2. `LLMSingleExtractor` (`llm_single`)** — single-pass LLM variant, lower latency, used by the lite/CC profile and as a CC default. `GroqExtractor` is a Groq-API subclass selected automatically when `GROQ_API_KEY` is set. **3. `ConversationAwareLLMExtractor`** — `LLMExtractor` subclass that adds short-term conversation context for chat-style ingestion. **4. `ReasoningExtractor`** — extracts chain-of-thought / reasoning traces from text containing explicit reasoning (used for the `reasoning` extended memory type). **5. `DecisionExtractor`** — extracts structured decision claims (used for the `decision` extended memory type). **6. `SpacyExtractor` (`spacy`)** — local spaCy-based NER fallback for entity extraction without LLM access. #### Provider routing [#provider-routing] `PipelineModelRouter` maps stages (e.g. `llm_extract`) to model identifiers and providers. Presets include `cost_optimized()`, `quality_optimized()`, and `balanced()`. Precedence: explicit `model=` > router > config > `get_default_model()`. When no LLM is available, `PipelineConfig.lite()` disables LLM extraction and the pipeline falls back to `SpacyExtractor` (DEGRADE-1d auto-detects this from `OPENAI_API_KEY` / `GROQ_API_KEY` env vars). #### Large text handling [#large-text-handling] Texts over 8000 characters are automatically chunked: * Split by sentence boundaries * Processed in parallel (ThreadPoolExecutor) * Results aggregated with entity deduplication ### 6. ontology\_constrain [#6-ontology_constrain] Constrains the extracted entity and relation types against the active ontology, so extraction cannot invent types the workspace has not declared. * Entity type hierarchies * Relationship type definitions * Knowledge graph schema enforcement The stage is omitted entirely when SmartMemory is built with `enable_ontology=False`. Triple extraction and ontology management are separate concerns, so you can extract relationships without running full ontology governance. See [Ontology Management](/smartmemory/guides/ontology-management/). ### 7. store [#7-store] Persists the memory node, the extracted entity nodes, and the embeddings. **FalkorDB HNSW index:** * Native vector indexing with the `vecf32` type * Configurable HNSW parameters (M, efConstruction, efRuntime) * Cosine similarity search * Automatic tenant isolation via `ScopeProvider` ### 8. link [#8-link] Connects the new item to related existing memories. **Similarity-based linking:** * Semantic similarity using embeddings * Temporal proximity for episodic memories * Conceptual overlap detection * Entity co-occurrence analysis **Explicit relationship creation:** * Causal relationships * Part-whole relationships * Temporal sequences * Conceptual hierarchies ### 9. enrich [#9-enrich] Runs the selected enrichers over the stored item. Choose them per call with `enricher_names=[...]`. **Semantic enhancement:** * Concept expansion and synonyms * Related topic identification * Contextual information addition **Temporal processing:** * Time normalization * Event sequencing * Duration calculation * Temporal relationship mapping Shipped enrichers include basic, sentiment, topic, temporal, link expansion, and skills/tools. ### 10. ground [#10-ground] Links extracted entities to public knowledge for provenance. * Look entities up in Wikidata/Wikipedia * Create Wikipedia nodes, which are shared globally rather than per workspace * Create `GROUNDED_IN` edges from entities to those nodes * Track source attribution Set `SMARTMEMORY_GROUNDING_OFFLINE=true` to skip this stage in air-gapped environments. ### 11. evolve [#11-evolve] Runs the evolvers, which move memories between types and adjust confidence over time. **Memory promotion:** * Pending → Episodic (threshold: 3+ items) * Pending → Procedural (threshold: 5+ items) * Episodic → Semantic (stable facts) * Episodic → Zettel (significant memories become atomic notes) * Episodic and semantic decay, plus opinion and observation synthesis See [Evolution Algorithms](/smartmemory/concepts/evolution-algorithms/) for the full evolver catalogue. ## Configuration [#configuration] The pipeline is controlled at two levels: a **profile set at construction time** that applies to every ingest, and **per-call selectors** that override the pipeline for a single `ingest()`. Construction-time profile and stage toggles: ```python from smartmemory import SmartMemory from smartmemory.pipeline.config import PipelineConfig memory = SmartMemory( pipeline_profile=PipelineConfig.lite(), # preset: disable network/LLM-bound stages compaction="standard", # strip extraction intermediates after store enable_ontology=False, # omit the ontology_constrain stage entirely observability=False, # disable metrics/event emission ) ``` Per-call component selection — swap or scope individual stages for one ingest: ```python item_id = memory.ingest( "Quarterly planning notes...", extractor_name="spacy", # choose the extraction stage enricher_names=["sentiment", "topic"], # run exactly these enrichers extract_decisions=True, # toggle decision extraction sync=True, # run inline vs. queue for background ) ``` For the full `ingest()` signature and every selector, see the [SmartMemory API reference](/smartmemory/reference/smart-memory/). ## Performance Characteristics [#performance-characteristics] * **Fast Ingestion**: Immediate storage with background enrichment * **Scalable Processing**: Parallel pipeline stages * **Fault Tolerance**: Graceful degradation and retry mechanisms * **Memory Efficiency**: Streaming processing for large inputs ## Monitoring and Debugging [#monitoring-and-debugging] ```python # Enable detailed logging memory.set_log_level("DEBUG") # Access ingestion metrics stats = memory.get_ingestion_stats() print(f"Processed: {stats.total_memories}") print(f"Average processing time: {stats.avg_processing_time}ms") ``` The ingestion flow is designed to balance speed, accuracy, and resource efficiency while providing rich, interconnected memories for intelligent retrieval and reasoning. ## Structured Ingestion [#structured-ingestion] Not all data needs the full 11-stage pipeline. Structured data — decisions, tool calls, plan tasks, code entities — already has its schema defined. Running it through LLM extraction is wasteful and lossy. `ingest_structured()` bypasses the NLP pipeline entirely. A registered handler validates the data, converts it to a `MemoryItem`, declares edges and indexes, and controls whether an embedding is generated. ### Three Strategies [#three-strategies] | Strategy | Embedding | Entity Extraction | Best For | | ----------- | ----------------- | ----------------- | ------------------------------------------------ | | **FULL** | Immediate | Yes | User-facing knowledge that needs semantic search | | **INDEXED** | No (can override) | No | System structures queried by typed fields | | **APPEND** | Never | Never | High-volume telemetry with recency-only access | ### Usage [#usage] ```python # Structured decision (INDEXED — queried by status, domain) memory.ingest_structured( {"content": "Use JWT for auth", "domain": "security", "status": "active"}, schema="decision" ) # Tool call telemetry (APPEND — recency + tool name filter) memory.ingest_structured( {"tool_name": "grep", "result": "found 3 matches", "success": True}, schema="tool_call" ) # Seed pack item (FULL — reference knowledge with embedding) memory.ingest_structured( {"content": "Python is a programming language", "memory_type": "semantic"}, schema="seed_item" ) ``` ### Available Schemas [#available-schemas] | Schema | Strategy | Handler | Description | | ------------------- | ---------------- | ----------------------- | ------------------------------------------ | | `decision` | INDEXED | DecisionHandler | Structured decisions with provenance edges | | `plan` | INDEXED | PlanHandler | Plan containers with task tracking | | `plan_task` | INDEXED | PlanTaskHandler | Individual task DAG nodes | | `code_entity` | INDEXED (+embed) | CodeEntityHandler | Code entities from repo indexing | | `seed_item` | FULL | SeedItemHandler | Reference knowledge from seed packs | | `tool_call` | APPEND | ToolCallHandler | Tool invocation telemetry | | `conversation_turn` | APPEND | ConversationTurnHandler | Conversation replay | | `hook_capture` | APPEND | HookCaptureHandler | Hook event telemetry | ### Custom Handlers [#custom-handlers] Create a handler by implementing the `StructuredHandler` protocol — no base class required: ```python class MyHandler: strategy = IngestionStrategy.INDEXED schema_name = "my_type" embed = None # None = defer to strategy default def validate(self, data: dict) -> bool: ... def to_memory_item(self, data: dict) -> MemoryItem: ... def get_edges(self, data: dict, item_id: str) -> list[tuple]: ... def get_indexes(self) -> list[str]: ... ``` Register it via `get_structured_registry().register("my_type", MyHandler())`. # Memory Dynamics (/smartmemory/concepts/memory-dynamics) SmartMemory doesn't treat every stored item as equally important forever. Memories gain and lose prominence over time based on how you use them, and the retrieval layer uses those signals to surface *what's actually relevant* to a query — not just what lexically matches. This page explains the memory-dynamics model: how activation, decay, compression, spreading, and centrality combine to decide what comes back from a `get_working_context()` call. ## Why this matters [#why-this-matters] A naïve memory system treats retrieval as lexical search: you ask "why did we pick JWT?" and you get back items that contain the words "why," "pick," or "JWT." Everything you know that's *related* to JWT but doesn't contain the word — load-balancer constraints, session-cookie failure modes, the decision thread that referenced a specific RFC — stays invisible unless your query happens to match its text. SmartMemory's dynamics model closes that gap. Frequently-used items get promoted; unused items fade; analytical questions pull in graph neighbors even when they don't lexically match; items you explicitly pin for a session are guaranteed on top. The result is that `get_working_context()` returns *contextually relevant* items, not just *lexically matching* items. ## The four behaviors [#the-four-behaviors] Four things happen in the background and at retrieval time. ### 1. Memory you use stays warm [#1-memory-you-use-stays-warm] Every time you retrieve an item via `search()`, `get()`, or a spreading-activation pass, its **activation score** goes up. The boost is a small additive nudge (`0.05` by default for a direct retrieval, smaller for surfacing and co-retrieval). Over a session of active use, frequently-touched items accumulate significantly higher activation than passive ones. ```python # Behind the scenes: every retrieval queues a boost to Redis. # A background worker (ActivationFlushJob) drains the queue every 5s # and writes the updated score to the item's metadata. memory.search("authentication") # items in the results get a +0.05 boost memory.get("item-123") # item-123 gets a +0.05 boost ``` You don't call a boost API directly. The system just tracks which memories actively participate in your workflow. ### 2. Unused memory fades (and eventually compresses) [#2-unused-memory-fades-and-eventually-compresses] Activation has a **half-life** (default: 30 days). A memory you haven't touched in a month is at roughly half the activation of when you last used it. After \~90 days of no activity, its activation approaches zero. Fading is visible two ways: * **Ranking** — low-activation items rank lower in retrieval results (they have to be compensated for by strong lexical relevance or explicit pinning). * **Compression** — very cold items get progressively compressed through a five-tier ladder: ``` FULL → SUMMARY → SKELETON → TOMBSTONE → FORGOTTEN ``` At the SUMMARY tier, the item's content is replaced with an LLM-generated digest. At SKELETON, just a title. TOMBSTONE keeps only the id. FORGOTTEN is removable by workspace policy. User-authored content (origin tier 1 — anything added via `memory.add()`, CLI, or API) is **never** compressed past FULL regardless of activation. Compression is opt-in (`SmartMemory(tier_compaction=True)` + `SMARTMEMORY_COMPACTION_ENABLED=true` env var). Decay is always on. ### 3. Related memory surfaces on analytical questions [#3-related-memory-surfaces-on-analytical-questions] When you ask a "why" or "how" question, SmartMemory's **surfacing router** classifies it as `deep:multi-hop` and does more than lexical search: 1. Find the top lexical hits (the items that directly match your query's words). 2. **Spread** from those seeds along the graph — bounded BFS, default 2 hops deep, 20-neighbor fan-out. Items reachable through `RELATED`, `MENTIONS`, `ANSWERED_BY`, `PART_OF`, and similar edges contribute to the candidate set even if they don't lexically match. 3. Compute **Personalized PageRank** from the same seeds over the workspace's subgraph. Items with high graph centrality relative to the seeds get a ranking lift. 4. Combine lexical relevance, activation, recency, spreading score, and PPR centrality into a single composite score. The practical effect: ask *"Why did we pick JWT?"* and get back the JWT decision item, plus the load-balancer note, plus the session-cookie failure thread, plus the RFC reference — not because they all contain "JWT," but because they sit in the graph neighborhood of the items that do. Two other strategies the router can pick: * **`fast:recency`** — short conversational queries. Skips spreading and PPR entirely; ranks by recent activity and lexical match. Sub-50ms p50. * **`factual:point-query`** — short lookups like `"users"` or `"list of decisions"`. Graph-only, skips vector semantic search (which would drown a single-entity query in noise). Three more strategies (`procedural:lookup`, `temporal:windowed`) are reserved in the enum for a future release and currently soft-degrade to `fast:recency`. ### 4. You can pin items for a session [#4-you-can-pin-items-for-a-session] For conversational workflows, you often want to say "keep this on top for the rest of our chat." SmartMemory supports session-scoped pins: ```python memory.pin_item(session_id="my-session", item_id="project-requirements-doc") # Now every get_working_context(session_id="my-session", ...) call # force-includes project-requirements-doc in the results. memory.unpin_item(session_id="my-session", item_id="project-requirements-doc") ``` Pins are Redis-backed, TTL'd at 24 hours, and **workspace-bound** — the first pin binds the session to one workspace, and subsequent cross-workspace pins raise `SessionPinWorkspaceConflict`. Reads of another workspace's pin set via a reused session id return empty. This prevents a poisoned session from bridging memory across tenants. ## The composite score [#the-composite-score] Every item in a `get_working_context()` response comes with a **score breakdown** showing exactly what contributed to its rank: ```python resp = memory.get_working_context(session_id="s1", query="Why did we pick JWT?") resp["items"][0]["score_breakdown"] # { # "activation": 0.73, # how warm the item is # "relevance": 0.92, # lexical match to query # "recency": 0.88, # exponential decay on last modified # "centrality": 0.65, # normalized PPR score over candidates # "anchor_forced": false, # true = force-included anchor # "session_pin_boost": 0.0, # pin_weight × 0.3 for pinned items # "freshness_boost": 0.15, # linear decay over 24h for new items # } ``` The composite formula (`surfacing_mode="router"`): ``` base = max(activation, 0.05) × relevance × recency centrality_lift = 1 + 0.5 × centrality_norm # 1.0 .. 1.5 additives = anchor_forced + session_pin_boost + freshness_boost score = base × centrality_lift + additives ``` Additives like pins and anchors don't *multiply* the score — they add a fixed lift and force the item into the returned set regardless of its base rank. A freshly-pinned item with zero activation still surfaces. ## Opt-in and defaults [#opt-in-and-defaults] All of this is **off by default**. Every existing caller of `get_working_context()` gets the M1a legacy behavior (lexical × recency × activation-with-floor) until they explicitly opt in: ```python memory = SmartMemory(surfacing_mode="router") # turns on router + composite + pins ``` With `surfacing_mode="router"` off: * The router is bypassed; all queries use `fast:recency`. * `session_pin_boost`, `freshness_boost`, and `centrality` stay zero in the response. * Spreading activation doesn't run. * PPR doesn't run; no cache, no hook. Legacy-mode callers see zero behavioral change. Opt-in is per-`SmartMemory` instance; a service can run both modes side-by-side during a migration. ## The four milestone arc [#the-four-milestone-arc] This feature shipped across four milestones under the `CORE-MEMORY-DYNAMICS-1` initiative: | Milestone | What it added | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | M1a | `get_working_context()` API + contract, observational ConsolidationRouter, MCP/SDK surface | | M1b | `working` → `pending` memory type rename (breaking change, contained) | | M2a | Activation primitives — boost, decay, retrieval-boost hook, ranking cutover, five activation-driven evolvers | | M2b | Flow expansion — `ResolutionChainEvolver` for Q→A edges, 5-tier progressive compression, workspace capacity cap with competitive inhibition | | M3 | Surfacing router, session pins, spreading activation, Personalized PageRank, graph write-hook invalidation | The system is feature-complete as of M3. Additional refinements (LLM-based query classifier, async-refresh fallback, `procedural:lookup` / `temporal:windowed` strategy implementations, anchor drift warnings in-band) remain on the roadmap but don't change the overall shape. ## Related documentation [#related-documentation] * [`api/surfacing.md`](/smartmemory/reference/surfacing/) — full API reference for `get_working_context`, pins, and surfacing mode * [`guides/tuning-surfacing.md`](/smartmemory/guides/tuning-surfacing/) — task-oriented guide for configuring the system * [`concepts/evolution-algorithms/retrieval-strengthening.md`](/smartmemory/concepts/evolution-algorithms/) — deeper dive on the retrieval-boost mechanism * [`concepts/evolution-algorithms/hebbian-co-retrieval.md`](/smartmemory/concepts/evolution-algorithms/) — how co-retrieved items get paired * [`concepts/evolution-algorithms/exponential-decay.md`](/smartmemory/concepts/evolution-algorithms/) — the decay math # Memory Snapshots (/smartmemory/concepts/memory-snapshots) > Feature: CORE-SUMMARY-1 · Code: `smartmemory/summary/` A memory snapshot is a first-class rollup of a workspace at a point in time: how many entities and relations exist, which domains the workspace clusters into, the top mentioned entities, what changed since the previous snapshot, and the share of content across origin tiers. Snapshots are stored as memory items themselves (`memory_type="snapshot"`), so they accumulate naturally in the graph and can be diffed, deleted, and surfaced through the same retrieval surface as any other content. This page covers the snapshot data model, the three triggers that produce snapshots, and the tiered retention policy. The HTTP surface is documented at [`/memory/summary/*`](/smartmemory/api/). ## Why snapshots [#why-snapshots] A live SmartMemory workspace grows continuously. There's no "version" of a workspace, no tag, no commit — only the current graph. That makes it hard to answer questions like: * *What did the agent learn this week?* * *How did the entity mix shift after we ingested the new corpus?* * *Which domains are growing fastest?* * *Are we accumulating tier-3 enrichments faster than tier-1 user content?* Snapshots compute these rollups on a cadence (or on demand), persist them, and expose deltas between adjacent or arbitrary pairs. They're the audit trail and the analytics substrate, in one shape. ## The snapshot model [#the-snapshot-model] A `MemorySnapshot` (contract: `docs/features/CORE-SUMMARY-1/snapshot-contract.json`) carries: | Field | Meaning | | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `snapshot_id` | Stable `snap_` ID — pre-generated by the composer so graph + Mongo write the same key. | | `workspace_id` | Tenant boundary; reads scope by both `_id` and `workspace_id`. | | `created_at`, `window_start` | Snapshot covers `[window_start, created_at]`. Time ordering uses `created_at`, not the ID. | | `trigger` | `"scheduled"` \| `"manual"` \| `"threshold"` | | `schedule` | Cron expression if scheduled, else `null` | | `entity_count`, `relation_count` | Total at time of snapshot. | | `domain_clusters` | List of `{name, item_count, growth_pct, representative_entities}`. | | `top_entities` | Top by mention count, with `entity_id`, `name`, `mention_count`, `entity_type`. | | `origin_tier_mix` | Map `tier_1..tier_4 → count`. | | `entities_added`, `entities_retracted`, `relations_added` | Window deltas. Retractions come from CORE-SUPERSEDE-1 `SUPERSEDED_BY` edges. | | `procedures_confirmed` | Phase-1: always `[]`. Phase-2 will track CFS-2 confirmation counts. | | `conflicts_resolved`, `new_domains` | Window deltas. | | `is_heartbeat` | `True` when a scheduled snapshot fires on an unchanged workspace; the renderer short-circuits to a one-line "no changes" digest. | | `notable_items` | Up to 50 `MemoryItemRef`s; one `PART_OF` edge written per ref. | | `prior_snapshot_id` | Link to the previous snapshot in the same workspace. | | `markdown` | Pre-rendered digest (empty body for heartbeats). | | `structured` | Agent-friendly JSON copy of the rollup. | The contract tracks `@dataclass` (extending `MemoryBaseModel`), not Pydantic — that's deliberate; snapshots are first-class memory items and inherit the same shape as everything else stored in the graph. The heavy payload (`top_entities`, `domain_clusters`, `notable_items`) is persisted to MongoDB keyed by `snapshot_id`; the graph row stores the light scalars plus a `metadata.snapshot_ref_id` pointer. This split means the graph stays compact while the rollup payload stays queryable. ## How a snapshot is composed [#how-a-snapshot-is-composed] Composition is split between core and the service: * **Composer** (`smartmemory/summary/composer.py`): pure function that reads the workspace state and returns a `MemorySnapshot` dataclass. Lives in core so it's reusable from CLI, batch jobs, or tests with no service dependency. * **Persister** (in the service): writes the Mongo payload, the light graph row, and the `PART_OF` edges to notable items as a single atomic operation. R3-5 rollback handling unwinds partial writes. A Redis Lua-CAD lock (`smartmemory:snapshot:lock:`) ensures cross-replica idempotency: scheduled, threshold, and manual triggers all share the same lock key, so a manual snapshot fired during a scheduled sweep window doesn't double-write. ## The three triggers [#the-three-triggers] ### 1. Scheduled [#1-scheduled] When `SNAPSHOT_SWEEP_ENABLED=true`, a background thread wakes every `SNAPSHOT_SWEEP_INTERVAL_SECONDS` (default 300) and walks every workspace. For each workspace whose `SnapshotConfig.schedule` (or the global `SNAPSHOT_DEFAULT_SCHEDULE`, default `"daily"`) is due, it acquires the lock and composes a snapshot. If the workspace is unchanged since the last scheduled snapshot, the trigger short-circuits and writes a **heartbeat snapshot** with `is_heartbeat=True` and an empty `markdown` body. This preserves the cadence (so consumers can always find a "today's snapshot") without duplicating real rollup work. ### 2. Threshold [#2-threshold] When `SNAPSHOT_THRESHOLD_ENABLED=true`, a poller thread checks every `SNAPSHOT_THRESHOLD_INTERVAL_SECONDS` (default 60) for workspaces that have crossed a watermark since their last snapshot: | Trigger | Default | Env var | | ------------------ | -------------------------------------- | -------------------------------- | | Ingest count | 100 ingests | `SNAPSHOT_THRESHOLD_INGESTS` | | Tier-1 byte volume | 100 KB of tier-1 (user) origin content | `SNAPSHOT_THRESHOLD_TIER1_BYTES` | Crossing **either** threshold fires a snapshot with `trigger="threshold"`. This catches bursty workloads — a long batch ingest produces a snapshot at the boundary, even when the next scheduled run is hours away. ### 3. Manual [#3-manual] `POST /memory/summary/generate` composes a snapshot synchronously with `trigger="manual"`. The same lock applies, so a manual call during an in-flight scheduled sweep returns the in-flight result rather than racing. ## Tiered retention [#tiered-retention] Snapshots accumulate, and old ones lose value. The retention sweep (gated on `SNAPSHOT_RETENTION_ENABLED=true`, runs every `SNAPSHOT_RETENTION_INTERVAL_SECONDS`, default 86400) applies a three-tier policy: | Tier | Window | Cadence kept | | ------- | ---------------- | ------------------------------- | | Daily | 0–30 days | every snapshot | | Weekly | 30 days – 1 year | one per ISO week | | Monthly | 1 year+ | one per calendar month, forever | Deletion is two-phase: Mongo payload + graph row + edges drop together, with a Redis tombstone written first. If the Mongo delete succeeds and the graph delete fails, the next sweep replays from the tombstone and finishes the cleanup. Snapshots are deliberately excluded from CORE-SUPERSEDE-1 supersession (they're append-only by design) and from tier-compaction in `[Memory Dynamics](/smartmemory/concepts/memory-dynamics/)` — they should never be folded into other items. ## Snapshot deltas [#snapshot-deltas] Adjacent deltas are precomputed when the newer snapshot is written and cached on the snapshot record, so `GET /memory/summary/delta?from=N&to=N+1` is O(1). Non-adjacent deltas (e.g. "compare this week to two months ago") are computed on the fly from the two payloads. A `SnapshotDelta` carries `entities_added`, `entities_retracted`, `relations_added`, `new_domains`, `conflicts_resolved`, and `time_span_seconds` — enough to render a "since the last snapshot" diff in a UI without loading the full rollup. ## Configuration summary [#configuration-summary] ```bash # Scheduled sweep SNAPSHOT_SWEEP_ENABLED=true SNAPSHOT_SWEEP_INTERVAL_SECONDS=300 SNAPSHOT_DEFAULT_SCHEDULE=daily # cron expr or shorthand # Threshold trigger SNAPSHOT_THRESHOLD_ENABLED=true SNAPSHOT_THRESHOLD_INTERVAL_SECONDS=60 SNAPSHOT_THRESHOLD_INGESTS=100 SNAPSHOT_THRESHOLD_TIER1_BYTES=100000 # Retention SNAPSHOT_RETENTION_ENABLED=true SNAPSHOT_RETENTION_INTERVAL_SECONDS=86400 ``` All four sweeps default to **off** so dev/CI doesn't auto-spin daemons. Production deployments enable them in `.env`. ## Related [#related] * [Snapshot HTTP API](/smartmemory/api/) — the seven `/memory/summary/*` routes * [Memory Dynamics](/smartmemory/concepts/memory-dynamics/) — snapshots are excluded from compaction # Memory Types (/smartmemory/concepts/memory-types) SmartMemory's memory types fall into two cohorts: * **Knowledge Layer**: what's *true*. Four knowledge types (`semantic`, `episodic`, `procedural`, `zettel`) plus `pending`, the staging type every item starts in. Captured through `add()` / `ingest()` and the standard pipeline. * **Expertise Layer**: what to *do*, and what *not* to do: `decision`, `constraint`, `learned`, `opinion`, `reasoning`, `observation`. Each has its own Manager + Queries lifecycle (supersession, reinforcement, contradiction handling) and an ergonomic capture wrapper. See [Expertise vs Knowledge](/smartmemory/concepts/expertise-vs-knowledge/) for the full distinction and recall channel. All five **knowledge-layer** types are detailed below. The six **expertise-layer** types are catalogued at the end of this page and have their own per-type concept docs. > **Looking for the API?** The [Memory Type API Matrix](/smartmemory/reference/memory-types-api/) is a per-type cheatsheet of how to create, query, and use every memory type across the SDK, MCP tools, and REST routes. ## Semantic Memory [#semantic-memory] Stores facts, concepts, and general knowledge that is context-independent. **Characteristics:** * Timeless information * Factual knowledge * Concepts and definitions * General principles **Examples:** * "Python is a programming language" * "The capital of France is Paris" * "Machine learning requires training data" ## Episodic Memory [#episodic-memory] Stores personal experiences and events with temporal and contextual information. **Characteristics:** * Time-bound experiences * Personal context * Situational details * Autobiographical events **Examples:** * "I learned Python in 2020 during the pandemic" * "Yesterday's team meeting discussed the new API" * "The debugging session on Friday solved the memory leak" ## Procedural Memory [#procedural-memory] Stores skills, procedures, and how-to knowledge for performing tasks. **Characteristics:** * Step-by-step processes * Skills and techniques * Operational knowledge * Action sequences **Examples:** * "How to deploy a Docker container" * "Steps for debugging memory issues" * "Process for code review workflow" ## Pending Memory [#pending-memory] Temporary storage for information currently being processed or actively used. **Characteristics:** * Short-term storage * Active processing * Current context * Session-specific **Examples:** * Current conversation context * Active problem-solving steps * Immediate tasks and goals ## Zettelkasten Memory [#zettelkasten-memory] Atomic, interconnected knowledge management inspired by the traditional Zettelkasten method. Stores individual ideas as notes with automatic cross-linking and knowledge graph formation. **Characteristics:** * Atomic notes (one idea per note) * Automatic entity extraction * Bidirectional linking * Emergent knowledge structure * Visual graph navigation **Examples:** * "Attention mechanisms allow models to focus on relevant input parts" * "Neural networks learn through backpropagation" (auto-linked to optimization notes) * Research insights with cross-references to related concepts **Unique Features:** * Interactive graph visualization * Background enrichment and auto-linking * Integration with external knowledge bases * Support for `[[note-id]]` cross-reference syntax > **Learn More**: See [Zettelkasten Memory](/smartmemory/concepts/zettelkasten-memory/) for comprehensive documentation. ## Expertise Layer Types [#expertise-layer-types] > **The expertise layer.** [Read the overview →](/smartmemory/concepts/expertise-vs-knowledge/) Beyond the five knowledge-layer types, SmartMemory supports six **expertise** memory types that store what to *do*, and what *not* to do. Each has its own Manager + Queries lifecycle (supersession, reinforcement, contradiction handling) and an ergonomic capture wrapper: * **`decision`**: A choice made, with structured `rejected_alternatives`, `rationale`, `constraints`. Confidence tracking, provenance chains, supersession. Capture: `mem.add_decision(...)` returns a `Decision` object with `.decision_id`. See [Decision Memory](/smartmemory/concepts/decision-memory/). * **`constraint`**: A hard rule discovered (or imposed). Capture: `mem.add_constraint(...)` returns a `Constraint` object with `.constraint_id`. * **`learned`**: A lesson learned the hard way. Capture: `mem.add_learning(...)` returns a `Learned` object with `.learned_id`. * **`opinion`**: A held belief with confidence that strengthens or weakens via reinforcement. Capture: `mem.add_opinion(...)`. See [Opinions & Observations](/smartmemory/concepts/opinions-observations/). * **`reasoning`**: Premises → conclusion chain-of-thought trace, with validation and supersession. Capture: `mem.add_reasoning(...)`. See [Reasoning Traces](/smartmemory/concepts/reasoning-traces/). * **`observation`**: An attested fact about the world or self, synthesized from scattered evidence. Capture: `mem.add_observation(...)`. See [Opinions & Observations](/smartmemory/concepts/opinions-observations/). Recall partitioned by expertise type via `mem.search(query, expertise=True)` returns a typed dict keyed by `decision`, `constraint`, `learned`, `opinion`, `reasoning`, and `observation`. ### Other extended types [#other-extended-types] * **Plan**: Task decompositions as DAGs with progress tracking, dependency resolution, and completion graduation. See [Plan Memory](/smartmemory/concepts/plan-memory/). * **Code**: Source code entities (classes, functions, routes) from code indexing. SmartMemory automatically classifies memories based on content analysis, but you can also specify the type explicitly: ```python memory.add("Paris is the capital of France", memory_type="semantic") memory.add("I visited paris last summer", memory_type="episodic") memory.add("How to book a flight to paris", memory_type="procedural") memory.add("Attention mechanisms allow models to focus on relevant input parts", memory_type="zettel") memory.add("We chose PostgreSQL for the new project", memory_type="decision") ``` ## Structured Memory Types [#structured-memory-types] Structured types bypass the NLP pipeline entirely. They are ingested via `ingest_structured()` where the data shape IS the content. Each type has a dedicated handler that determines its ingestion strategy. | Type | Strategy | Use Case | | ------------------- | ---------------- | -------------------------------------------------------- | | `plan` | INDEXED | Plan containers with task count and completion tracking | | `plan_task` | INDEXED | Individual task DAG nodes with dependency edges | | `decision` | INDEXED | Structured decision import (complements Decision Memory) | | `code_entity` | INDEXED (+embed) | Code entities from repository indexing | | `seed_item` | FULL | Reference knowledge from seed packs | | `tool_call` | APPEND | Tool invocation telemetry | | `conversation_turn` | APPEND | Conversation replay data | | `hook_capture` | APPEND | Hook event telemetry | **Ingestion strategies:** * **FULL**: embedding + entity extraction (searchable via semantic search) * **INDEXED**: graph node + typed indexes, no embedding (queryable by typed fields) * **APPEND**: minimal graph node, recency-only (high-volume telemetry) ```python # Structured ingestion, schema determines handler and strategy memory.ingest_structured({"content": "Use JWT for auth", "domain": "security"}, schema="decision") memory.ingest_structured({"tool_name": "grep", "result": "3 matches"}, schema="tool_call") ``` ## Bitemporal Capabilities [#bitemporal-capabilities] SmartMemory implements full bitemporal support, tracking two distinct time dimensions for each memory item: ### Valid Time vs Transaction Time [#valid-time-vs-transaction-time] **Valid Time (`valid_time`)** * When the fact was true in the real world * Represents the actual time period of validity * Can be in the past, present, or future * Example: "John worked at Company X from 2020-2023" **Transaction Time (`transaction_time`)** * When the fact was recorded in the system * Always in the past (when we learned about it) * Immutable once recorded * Used for audit trails and data provenance ### Temporal Enrichment [#temporal-enrichment] SmartMemory automatically enriches memories with temporal metadata: ```python # Example temporal enrichment result temporal_data = { "entities": { "John Smith": { "valid_start": "2020-01-15T00:00:00Z", "valid_end": "2023-12-31T23:59:59Z", "transaction_time": "2024-01-15T10:30:00Z" } } } ``` ### Use Cases for Bitemporal Data [#use-cases-for-bitemporal-data] * **Historical Analysis**: Query what we knew at any point in time * **Fact Correction**: Update information without losing historical context * **Audit Trails**: Track when facts were recorded vs when they were valid * **Time-Travel Queries**: "What did we know about X on date Y?" * **Data Provenance**: Understand the evolution of knowledge over time ### Comparison with Competitors [#comparison-with-competitors] * **Mem0**: No bitemporal support (basic timestamps only) * **Zep**: Single temporal dimension (no valid/transaction time separation) * **SmartMemory**: Full bitemporal model with automated temporal enrichment ## Cross-Type Relationships [#cross-type-relationships] SmartMemory automatically discovers relationships between different memory types: * **Semantic ↔ Episodic**: Facts connected to personal experiences * **Procedural ↔ Episodic**: Skills learned through specific events * **Pending ↔ All Types**: Temporary connections to long-term memories This creates a rich, interconnected memory network that mirrors human cognitive patterns. ## Memory Evolution [#memory-evolution] Memory types can evolve and transform: * **Pending → Episodic**: Recent interactions become experiences * **Episodic → Semantic**: Repeated patterns become general knowledge * **Episodic → Zettelkasten**: Important events become structured notes * **Semantic ↔ Zettelkasten**: Facts and notes cross-reference each other # Opinions & Observations (/smartmemory/concepts/opinions-observations) > **Part of the expertise layer.** > [Read the overview →](/smartmemory/concepts/expertise-vs-knowledge/) SmartMemory synthesizes **opinions** and **observations** from raw memories, enabling agents to form beliefs and entity summaries that evolve over time. ## Overview [#overview] | Type | Description | Example | | --------------- | ---------------------------- | ----------------------------------------------------- | | **Opinion** | Belief with confidence score | "User prefers functional programming (85% confident)" | | **Observation** | Entity summary | "Alice: Works at Google since 2020, promoted in 2023" | ## Opinions [#opinions] Opinions are beliefs formed from patterns in episodic memories. They have confidence scores that can be reinforced or contradicted. ### Formation [#formation] The `OpinionSynthesisEvolver` detects patterns: ### Confidence Scoring [#confidence-scoring] ```python from smartmemory.models.opinion import OpinionMetadata meta = OpinionMetadata( confidence=0.75, subject="functional programming", subject_type="preference" ) # When supporting evidence arrives meta.reinforce("evidence_123") # confidence: 0.75 → 0.775 (diminishing returns) # When contradicting evidence arrives meta.contradict("evidence_456") # confidence: 0.775 → 0.659 (15% penalty) ``` ### Disposition [#disposition] Opinions are influenced by configurable disposition traits: ```python from smartmemory.models.opinion import Disposition disposition = Disposition( skepticism=0.7, # Requires more evidence before forming opinions literalism=0.5, # Balance literal vs inferred meaning empathy=0.8 # Weight emotional/preference signals highly ) ``` ### Reinforcement Evolver [#reinforcement-evolver] The `OpinionReinforcementEvolver` runs as a background job: 1. Scans new episodic memories for evidence 2. Matches against existing opinions 3. Reinforces or contradicts based on similarity 4. Decays stale opinions over time 5. Archives opinions below confidence threshold ## Observations [#observations] Observations are synthesized entity summaries built from scattered facts. ### Synthesis [#synthesis] The `ObservationSynthesisEvolver` combines facts: ### Aspect Coverage [#aspect-coverage] Observations track which aspects of an entity are covered: ```python from smartmemory.models.opinion import ObservationMetadata meta = ObservationMetadata( entity_id="alice_123", entity_name="Alice", entity_type="person", aspects_covered=["career", "location", "skills"], completeness=0.6 # 60% complete ) ``` Available aspects: * `career` - Work, job, company, role * `education` - School, degree, study * `location` - City, country, address * `preferences` - Likes, favorites * `relationships` - Family, colleagues * `skills` - Expertise, proficiency * `interests` - Hobbies, passions ## The Reflect API [#the-reflect-api] Trigger synthesis on-demand: ```python # Run synthesis evolvers and get results result = memory.reflect(top_k=5) ``` This: 1. Runs `OpinionSynthesisEvolver` 2. Runs `ObservationSynthesisEvolver` 3. Returns newly formed opinions/observations ## Evolution Configuration [#evolution-configuration] ### Opinion Synthesis [#opinion-synthesis] ```python from smartmemory.plugins.evolvers.opinion_synthesis import OpinionSynthesisConfig config = OpinionSynthesisConfig( min_pattern_occurrences=3, # Pattern must appear 3+ times min_confidence=0.5, # Minimum confidence to create lookback_days=30, # Analyze last 30 days skepticism=0.5 # Disposition skepticism ) ``` ### Opinion Reinforcement [#opinion-reinforcement] ```python from smartmemory.plugins.evolvers.opinion_reinforcement import OpinionReinforcementConfig config = OpinionReinforcementConfig( min_confidence_threshold=0.2, # Archive below this lookback_days=7, # Check last 7 days for evidence enable_decay=True, # Enable staleness decay decay_after_days=30, # Start decaying after 30 days decay_rate=0.05 # 5% decay per cycle ) ``` ### Observation Synthesis [#observation-synthesis] ```python from smartmemory.plugins.evolvers.observation_synthesis import ObservationSynthesisConfig config = ObservationSynthesisConfig( min_facts_per_entity=2, # Need 2+ facts to synthesize lookback_days=90, # Look back 90 days max_observations_per_run=20 # Limit per evolution cycle ) ``` ## Memory Types [#memory-types] ```python # Opinion and observation are extended *string* memory types — pass them as # strings (they are not members of the `MemoryType` enum). # Store opinion memory.add( content="User prefers functional programming", memory_type="opinion", metadata={ "confidence": 0.85, "subject": "functional programming", "subject_type": "preference" } ) # Store observation memory.add( content="Alice: Senior engineer at Google since 2020", memory_type="observation", metadata={ "entity_id": "alice_123", "entity_name": "Alice", "aspects_covered": ["career"] } ) ``` # Core Concepts (/smartmemory/concepts/overview) Understanding SmartMemory's core concepts is essential for building effective memory-enabled applications. This guide covers the fundamental principles and components that make SmartMemory powerful. ## Memory Architecture [#memory-architecture] SmartMemory implements a **unified memory architecture** that combines multiple memory types with intelligent processing capabilities. Unlike traditional storage systems, SmartMemory doesn't just store data—it understands, connects, and evolves your information. SmartMemory's memory types fall into two cohorts: a **knowledge layer** that stores what's *true*, made of four knowledge types (semantic, episodic, procedural, zettel) plus pending, the staging type every item starts in, and an **expertise layer** (decision, constraint, learned, opinion, reasoning, observation) that stores what to *do* — and what not to do. The expertise layer captures the choices an agent makes, the alternatives it rejects, the constraints it discovers, the lessons it learns the hard way. See [Expertise vs Knowledge](/smartmemory/concepts/expertise-vs-knowledge/) for the full distinction. ### Memory Types [#memory-types] SmartMemory's knowledge layer has four knowledge types (semantic, episodic, procedural, zettel) plus pending, the staging type every item starts in. On top of that sit extended types for specialized use cases (reasoning, opinion, observation, decision, plan, code), and structured types for schema-mapped data (tool\_call, plan\_task, conversation\_turn, hook\_capture). See [Memory Types](/smartmemory/concepts/memory-types/) for the full reference. Every memory item carries **provenance metadata** — `origin` tracks where it came from (e.g., `user:cli`, `evolver:opinion_synthesis`, `structured:decision`), `confidence` tracks certainty (0.0-1.0), and `derived_from` links to parent items for lineage tracking. ### Memory Dynamics [#memory-dynamics] Items aren't static. SmartMemory tracks an **activation score** per item that rises on retrieval, decays over time, and feeds a retrieval-layer composite that decides what surfaces on a query. Cold items compress progressively through a five-tier ladder; analytical questions pull in graph neighbors via bounded spreading activation and Personalized PageRank. Session pins let callers force-include specific items for a conversational turn. The full model — activation, decay, tier compression, surfacing router, spreading, PPR, pins — is covered in [Memory Dynamics](/smartmemory/concepts/memory-dynamics/). API reference in [`api/surfacing`](/smartmemory/reference/surfacing/). Task-oriented configuration in [`guides/tuning-surfacing`](/smartmemory/guides/tuning-surfacing/). ### The Core Memory Types [#the-core-memory-types] #### 1. Semantic Memory [#1-semantic-memory] **What it stores**: Facts, concepts, and general knowledge that are independent of personal experience. ```python # Examples of semantic memory memory.ingest("Python is a programming language") memory.ingest("The capital of France is Paris") memory.ingest("Machine learning is a subset of AI") ``` **Characteristics**: * Timeless factual information * Shared knowledge across users * Structured relationships between concepts * High connectivity with other facts #### 2. Episodic Memory [#2-episodic-memory] **What it stores**: Personal experiences and events with temporal and contextual information. ```python # Examples of episodic memory memory.ingest("I attended the AI conference in San Francisco last week") memory.ingest("Had lunch with Sarah at the Italian restaurant yesterday") memory.ingest("Completed my first Python project in 2020") ``` **Characteristics**: * Time-bound experiences * Personal and contextual * Rich in emotional and sensory details * Connected to specific locations and people #### 3. Procedural Memory [#3-procedural-memory] **What it stores**: Skills, procedures, and how-to knowledge. ```python # Examples of procedural memory memory.ingest("To deploy to AWS: 1) Build image 2) Push to ECR 3) Update ECS") memory.ingest("Git workflow: branch → commit → push → PR → merge") memory.ingest("Morning routine: exercise → shower → coffee → emails") ``` **Characteristics**: * Step-by-step instructions * Action sequences and workflows * Skill-based knowledge * Executable procedures #### 4. Pending Memory [#4-pending-memory] **What it stores**: Temporary information that's currently being processed or actively used. ```python # Examples of pending memory memory.ingest("Currently debugging the authentication issue") memory.ingest("Meeting with team at 3 PM today") memory.ingest("Need to review PR #123 before EOD") ``` **Characteristics**: * Temporary and context-dependent * High priority for immediate access * Frequently updated or replaced * Limited capacity by design #### 5. Zettelkasten Memory [#5-zettelkasten-memory] **What it stores**: Atomic, interconnected notes with automatic cross-linking and knowledge graph formation. ```python # Examples of zettelkasten memory memory.ingest("Attention mechanisms allow models to focus on relevant input parts") memory.ingest("Neural networks learn through backpropagation") ``` **Characteristics**: * One idea per note (atomic) * Automatic entity extraction and linking * Bidirectional references * Emergent knowledge structure #### Extended Memory Types [#extended-memory-types] Beyond the five core types, SmartMemory supports extended memory types: * **Reasoning**: Chain-of-thought traces capturing the *why* behind decisions. See [Reasoning Traces](/smartmemory/concepts/reasoning-traces/). * **Opinion**: Beliefs with confidence scores that strengthen or weaken over time. See [Opinions & Observations](/smartmemory/concepts/opinions-observations/). * **Observation**: Synthesized entity summaries from scattered facts. See [Opinions & Observations](/smartmemory/concepts/opinions-observations/). * **Decision**: First-class decisions with confidence tracking, provenance chains, and lifecycle management. See [Decision Memory](/smartmemory/concepts/decision-memory/). ### Memory Type Interactions [#memory-type-interactions] ### Intelligent Processing Pipeline [#intelligent-processing-pipeline] ### 1. Ingestion Flow (11 Stages) [#1-ingestion-flow-11-stages] When you call `memory.ingest()`, content flows through an **11-stage pipeline**: ``` Input → Classification → Extraction → Storage → Linking → Vector → Enrichment → Grounding → Evolution → Clustering → Versioning ``` | Stage | Description | | ----------------------- | ----------------------------------------------------------------------- | | **1. Input Adaptation** | Convert str/dict/MemoryItem to standard format | | **2. Classification** | Determine memory type (semantic, episodic, procedural, zettel, pending) | | **3. Extraction** | Extract entities & relations (LLM → SpaCy → GLiNER fallback) | | **4. Storage** | Create memory node + entity nodes in FalkorDB | | **5. Linking** | Connect to related existing memories | | **6. Vector Storage** | Generate embeddings, store in HNSW index | | **7. Enrichment** | Add Wikipedia summaries, categories, metadata | | **8. Grounding** | Create GROUNDED\_IN edges to Wikipedia nodes | | **9. Evolution** | Promote pending → episodic/procedural if thresholds met | | **10. Clustering** | SemHash + embedding deduplication of entities | | **11. Versioning** | Create bi-temporal version record | #### Extraction (Fallback Chain) [#extraction-fallback-chain] SmartMemory uses multiple extractors with automatic fallback: ``` LLM → GLiNER → SpaCy ``` * **LLM Extractor**: Highest accuracy; uses a configurable LLM resolved at runtime via the model router (`get_default_model()`) * **GLiNER Extractor**: Fast local CPU inference, privacy-preserving * **SpaCy Extractor**: NER + dependency parsing with regex fallback #### Grounding (Wikipedia) [#grounding-wikipedia] Establishes provenance by linking entities to Wikipedia: * Creates shared Wikipedia nodes (`is_global=True`) * Creates `GROUNDED_IN` edges from entities to Wikipedia * Enables source verification and fact checking #### Clustering [#clustering] Deduplicates entities using multi-level resolution: 1. **SemHash**: Fast deterministic dedup (0.95 threshold) 2. **KMeans**: Embedding clustering (\~128 items per cluster) 3. **LLM**: Semantic clustering (Joe ↔ Joseph, ML ↔ machine learning) 4. **Merge**: Graph node consolidation #### Versioning (Bi-Temporal) [#versioning-bi-temporal] Tracks all changes with two time dimensions: * **Valid time**: When the fact was true in the real world * **Transaction time**: When the fact was recorded in the system * Enables time-travel queries and version comparison ### 2. Entity and Relationship Extraction [#2-entity-and-relationship-extraction] SmartMemory automatically identifies and extracts structured information: ```python # Input memory.add("John Smith works at Google as a software engineer") # Extracted entities entities = [ {"name": "John Smith", "type": "PERSON"}, {"name": "Google", "type": "ORGANIZATION"}, {"name": "software engineer", "type": "JOB_TITLE"} ] # Extracted relationships relationships = [ {"source": "John Smith", "target": "Google", "type": "WORKS_AT"}, {"source": "John Smith", "target": "software engineer", "type": "HAS_ROLE"} ] ``` ### 3. Automatic Linking [#3-automatic-linking] SmartMemory creates intelligent connections between memories: * **Semantic similarity**: Memories with similar meaning * **Entity overlap**: Memories sharing common entities * **Temporal proximity**: Memories from similar time periods * **Contextual relevance**: Memories from similar contexts ## Evolution Algorithms [#evolution-algorithms] SmartMemory continuously improves itself through evolution algorithms: ### 1. Maximal Connectivity Evolver [#1-maximal-connectivity-evolver] Creates dense, high-quality connections between related memories. ```python # Before evolution Memory A: "Python programming" Memory B: "Data science projects" # No connection # After evolution Memory A ←→ Memory B (USED_FOR relationship) ``` ### 2. Rapid Enrichment Evolver [#2-rapid-enrichment-evolver] Immediately enhances memories with comprehensive context and metadata. ### 3. Strategic Pruning Evolver [#3-strategic-pruning-evolver] Removes redundant or low-value memories while preserving important information through consolidation. ### 4. Hierarchical Organization Evolver [#4-hierarchical-organization-evolver] Creates hierarchical structures for efficient navigation and retrieval. ## Similarity Framework [#similarity-framework] SmartMemory uses a multi-dimensional similarity framework to understand relationships: ### Similarity Metrics [#similarity-metrics] 1. **Semantic Similarity**: Meaning-based comparison using embeddings 2. **Content Similarity**: Text-based overlap and patterns 3. **Temporal Similarity**: Time-based relationships 4. **Metadata Similarity**: Structural and categorical similarities ### Weighted Scoring [#weighted-scoring] ```python total_similarity = ( semantic_similarity * 0.4 + content_similarity * 0.3 + temporal_similarity * 0.2 + metadata_similarity * 0.1 ) ``` ## Graph-Based Storage [#graph-based-storage] SmartMemory uses graph databases to model complex relationships: ### Node Types [#node-types] * **Memory Nodes**: Individual memories with content and metadata * **Entity Nodes**: Extracted entities (dual-node pattern for reuse) * **Wikipedia Nodes**: Global grounding nodes (shared across users) * **Version Nodes**: Bi-temporal version records ### Relationship Types [#relationship-types] * **CONTAINS**: Memory contains entity * **GROUNDED\_IN**: Entity grounded to Wikipedia * **HAS\_VERSION**: Memory has version history * **RELATED\_TO**: General semantic relationships * **PRECEDES/FOLLOWS**: Temporal sequences * **CAUSED\_BY/CAUSES**: Causal relationships * **PART\_OF/HAS\_PART**: Compositional relationships ### Graph Queries [#graph-queries] ```python # Find all memories related to "Python" related_memories = memory.get_neighbors("python_concept_id") # Get linked memories (filter the returned triples by edge type as needed) sequence = memory.get_links("event_id", memory_type="episodic") # Find linked memories of another type causes = memory.get_links("outcome_id", memory_type="semantic") ``` ## Background Processing [#background-processing] SmartMemory supports asynchronous processing for better performance: ### Fast Ingestion [#fast-ingestion] ```python # Store immediately, process in background memory.ingest("Important information that needs immediate storage") ``` ### Processing Queue [#processing-queue] * **Priority-based**: Important memories processed first * **Batch processing**: Efficient bulk operations * **Retry logic**: Handles temporary failures * **Health monitoring**: Tracks processing status ## Ontology Management [#ontology-management] SmartMemory supports structured knowledge organization: ### Ontology Components [#ontology-components] * **Entity Types**: Definitions of entity categories * **Relationship Types**: Allowed relationship patterns * **Validation Rules**: Constraints and consistency checks * **Inheritance Hierarchies**: Type hierarchies and specialization ### Hybrid Approach [#hybrid-approach] * **Freeform extraction**: Automatic, flexible entity discovery * **Ontology-guided**: Structured, validated knowledge organization * **Seamless migration**: Evolution from freeform to structured ## Search and Retrieval [#search-and-retrieval] SmartMemory provides multiple search strategies: ### Search Types [#search-types] 1. **Semantic Search**: Meaning-based retrieval using embeddings 2. **Graph Search**: Relationship-based traversal 3. **Hybrid Search**: Combined semantic and graph approaches 4. **Filtered Search**: Type-specific or metadata-filtered results ### Search Context [#search-context] ```python # Context-aware search results = memory.search( query="machine learning", memory_type="semantic", top_k=10 ) # Note: User/tenant filtering is handled automatically by ScopeProvider # in multi-tenant deployments. Time-range filtering can be done post-search. ``` ## Component Architecture [#component-architecture] SmartMemory follows a modular, component-based architecture: ### Core Components [#core-components] * **CRUD**: Create, Read, Update, Delete operations * **Search**: Multi-strategy search (vector + graph + text fallbacks) * **Linking**: Relationship discovery and management * **Enrichment**: Wikipedia integration and metadata enhancement * **Grounding**: Provenance linking to external knowledge * **Evolution**: Memory promotion (pending → episodic/procedural) * **Clustering**: Entity deduplication (SemHash + KMeans + LLM) * **Challenging**: Contradiction detection (LLM → Graph → Embedding → Heuristic cascade) * **Versioning**: Bi-temporal tracking with time-travel queries * **Monitoring**: Performance and health tracking ### Benefits [#benefits] * **Modularity**: Easy to extend and customize * **Testability**: Components can be tested in isolation * **Flexibility**: Mix and match components as needed * **Maintainability**: Clear separation of concerns ## Integration Patterns [#integration-patterns] ### Tool-Based Integration [#tool-based-integration] SmartMemory provides tools for agentic workflows: ```python # MCP tools for LLM agents tools = [ "memory_add", "memory_search", "memory_get", "memory_update", "memory_delete" ] ``` ### API Integration [#api-integration] RESTful APIs for web applications and services. ### Library Integration [#library-integration] Direct Python library usage for embedded applications. ## Performance Considerations [#performance-considerations] ### Scalability [#scalability] * **Horizontal scaling**: Multiple worker processes * **Vertical scaling**: Optimized algorithms and data structures * **Caching**: Intelligent caching strategies * **Indexing**: Efficient search indexes ### Optimization [#optimization] * **Lazy loading**: Load data only when needed * **Batch operations**: Process multiple items efficiently * **Connection pooling**: Reuse database connections * **Memory management**: Efficient memory usage patterns ## Best Practices [#best-practices] 1. **Choose appropriate memory types** for your data 2. **Enable background processing** for better performance 3. **Use structured data** when possible for better extraction 4. **Monitor evolution algorithms** to ensure quality 5. **Configure similarity weights** for your use case 6. **Implement proper error handling** for robustness 7. **Use caching** for frequently accessed data ## Next Steps [#next-steps] * [Memory Types](/smartmemory/concepts/memory-types/) - Detailed guide to each memory type * [Ingestion Flow](/smartmemory/concepts/ingestion-flow/) - Deep dive into the processing pipeline * [Evolution Algorithms](/smartmemory/concepts/evolution-algorithms/) - Understanding memory optimization * [Similarity Framework](/smartmemory/concepts/similarity-framework/) - Advanced similarity concepts # Plan Memory (/smartmemory/concepts/plan-memory) Plan Memory gives agents persistent task decompositions as DAGs — solving two critical failure modes: **plan amnesia** (re-deriving decompositions after every step) and **retry loops** (repeating the same failing approach indefinitely). ## Architecture [#architecture] Plans use a two-node architecture: * **Plan container** (`plan` type) — stores plan-level metadata: title, status, task count, completion percentage * **Plan task nodes** (`plan_task` type) — individual tasks forming a DAG via `DEPENDS_ON` edges ``` ┌──────────────────────────────┐ │ Plan Container │ │ plan_id: "plan_abc123" │ │ status: "in_progress" │ │ completed_tasks: 1/4 │ └──────────┬───────────────────┘ │ plan_id = "plan_abc123" ┌──────┼──────────┬──────────┐ ▼ ▼ ▼ ▼ [t1] [t2] [t3] [t4] done in_prog pending pending │ │ │ └──DEPENDS_ON──┘ │ └──DEPENDS_ON──┘ ``` ## Usage [#usage] ### Creating a Plan [#creating-a-plan] ```python from smartmemory.plans import PlanManager manager = PlanManager(memory) result = manager.create( "Implement auth system", tasks=[ {"content": "Design JWT schema"}, {"content": "Implement token service", "depends_on": []}, {"content": "Add refresh flow"}, {"content": "Write integration tests"}, ], context="User requested JWT-based authentication", ) plan_id = result["plan_id"] # "plan_abc123def4" task_ids = result["task_ids"] # [uuid1, uuid2, uuid3, uuid4] ``` ### Tracking Progress [#tracking-progress] ```python # Complete a task — automatically recomputes plan completion % manager.update_task(plan_id, task_ids[0], "complete") # What can be worked on next? (pending tasks with all deps complete) next_tasks = manager.get_next_tasks(plan_id) # What's blocked? (tasks with incomplete dependencies) blocked = manager.get_blocked_tasks(plan_id) ``` ### Plan Lifecycle [#plan-lifecycle] ```python # Get full plan state plan = manager.get(plan_id) # {"plan": {..., "completed_tasks": 2, "total_tasks": 4}, "tasks": [...]} # List active plans active = manager.get_active() # Complete — optionally graduate to a decision record decision_id = manager.complete(plan_id, graduate=True) # Or mark as failed manager.fail(plan_id, reason="Dependencies unavailable") ``` ### Graduation [#graduation] When a plan completes, it can optionally **graduate** to a Decision record — preserving the decomposition as a first-class decision with a `DERIVED_FROM` edge back to the plan. This creates a permanent record of "we decided to do X, and here's the plan that proved it works." ## Task Dependencies [#task-dependencies] Tasks form a DAG (directed acyclic graph) via `DEPENDS_ON` edges. The dependency resolution logic: * **Next tasks**: pending tasks where ALL dependencies are complete * **Blocked tasks**: non-complete tasks where ANY dependency is incomplete * Tasks with no dependencies are always eligible as next tasks ## Plan Status Values [#plan-status-values] | Status | Meaning | | ----------- | ---------------------------------------------- | | `active` | Plan is in progress | | `completed` | All work done (may have graduated to decision) | | `failed` | Plan abandoned with reason | ## Integration with Structured Ingestion [#integration-with-structured-ingestion] Plan containers and tasks are both stored via the structured ingestion system: ```python # These are equivalent — PlanManager calls them internally memory.ingest_structured({"content": "My plan", "plan_id": "plan_123"}, schema="plan") memory.ingest_structured({"content": "Task 1", "plan_id": "plan_123"}, schema="plan_task") ``` See [Ingestion Flow](/smartmemory/concepts/ingestion-flow/#structured-ingestion) for details on structured ingestion strategies. # Reasoning Traces (System 2 Memory) (/smartmemory/concepts/reasoning-traces) > **Part of the expertise layer.** > [Read the overview →](/smartmemory/concepts/expertise-vs-knowledge/) SmartMemory captures **reasoning traces** - the chain-of-thought process behind decisions, not just the final outcomes. This enables agents to learn from *how* problems were solved. ## Why Reasoning Traces Matter [#why-reasoning-traces-matter] Traditional memory systems store: * ✅ **What** was decided * ❌ **Why** it was decided With reasoning traces, SmartMemory stores both: ``` Input: "Fix the login bug" Reasoning: Thought → Action → Observation → Decision → Conclusion Output: "Added null check on line 42" ``` ## Step Types [#step-types] | Type | Description | Example | | ------------- | ------------------ | ---------------------------------------- | | `thought` | Internal reasoning | "I think the issue is in authentication" | | `action` | Action taken | "Let me search for the login function" | | `observation` | Result observed | "Found null pointer in token validation" | | `decision` | Decision point | "I will add a null check" | | `conclusion` | Final answer | "Fixed by adding validation" | | `reflection` | Meta-reasoning | "This approach worked because..." | ## Extraction Modes [#extraction-modes] ### 1. Explicit Markers [#1-explicit-markers] When text contains explicit markers, they are parsed directly: ```text Thought: I need to analyze this bug. Action: Searching for the error pattern. Observation: Found the issue in line 42. Conclusion: Added null check to fix. ``` ### 2. Implicit Detection [#2-implicit-detection] For natural language without markers, the LLM detects reasoning patterns: ```text Let me think about this... First, I should check the logs. The error shows a null pointer. This means we need validation. Therefore, I'll add a null check before the call. ``` ## Usage [#usage] ### Automatic Extraction [#automatic-extraction] Reasoning extraction runs during `ingest()` when you opt in with the `extract_reasoning` flag: ```python memory = SmartMemory() # Traces are automatically extracted during ingestion memory.ingest("Thought: Analyzing the bug...", extract_reasoning=True) ``` ### Manual Extraction [#manual-extraction] ```python from smartmemory.plugins.extractors.reasoning import ReasoningExtractor extractor = ReasoningExtractor() result = extractor.extract(""" Thought: The tests are failing on CI. Action: Checking the test logs. Observation: Missing environment variable. Conclusion: Added the variable to CI config. """) trace = result['reasoning_trace'] print(f"Steps: {len(trace.steps)}") print(f"Quality: {trace.evaluation.quality_score}") ``` ## Quality Evaluation [#quality-evaluation] Traces are evaluated before storage: | Metric | Description | Threshold | | ---------------- | --------------------- | -------------- | | `quality_score` | Overall quality (0-1) | ≥ 0.4 to store | | `has_loops` | Repeated reasoning | Penalized | | `has_redundancy` | Similar steps | Penalized | | `step_diversity` | Variety of step types | Bonus | ```python if trace.evaluation.should_store: # Quality passes threshold memory.add(trace) ``` ## Task Context [#task-context] Each trace captures context for filtered retrieval: ```python trace.task_context = TaskContext( goal="Fix authentication bug", task_type="debugging", # code_generation, analysis, problem_solving domain="python", # javascript, frontend, backend, database complexity="medium" # low, medium, high ) ``` ### Searching by Context [#searching-by-context] ```python # Find similar debugging experiences results = memory.search( "authentication error", memory_type="reasoning" ) ``` ## Linking to Artifacts [#linking-to-artifacts] Reasoning traces can be linked to resulting code/docs: ```python trace.artifact_ids = ["commit_abc123", "doc_readme_update"] ``` This creates a `CAUSES` relationship in the knowledge graph. ## Configuration [#configuration] ```python from smartmemory.plugins.extractors.reasoning import ReasoningExtractorConfig config = ReasoningExtractorConfig( min_quality_score=0.4, # Minimum quality to store min_steps=2, # Minimum steps for valid trace use_llm_detection=True, # Enable implicit detection model_name="gpt-4o-mini" # LLM for detection ) extractor = ReasoningExtractor(config=config) ``` # Similarity Framework (/smartmemory/concepts/similarity-framework) SmartMemory's similarity framework provides sophisticated methods for measuring relationships between memories, enabling intelligent retrieval, linking, and organization. ## Overview [#overview] The similarity framework combines multiple similarity metrics to create a comprehensive understanding of memory relationships, supporting both semantic and structural similarity analysis. ## Similarity Metrics [#similarity-metrics] ### Semantic Similarity [#semantic-similarity] **Vector-Based Similarity:** * **Cosine Similarity** - Angular distance between embeddings * **Euclidean Distance** - Geometric distance in vector space * **Dot Product** - Direct vector multiplication * **Manhattan Distance** - Sum of absolute differences **Implementation:** The similarity framework is `EnhancedSimilarityFramework` in `smartmemory.similarity.framework`. Instantiate it directly with an optional `SimilarityConfig`, then call `calculate_similarity` with two `MemoryItem` objects. Pass `metrics=["semantic"]` to score on the semantic metric alone: ```python from smartmemory.similarity.framework import EnhancedSimilarityFramework, SimilarityConfig framework = EnhancedSimilarityFramework( config=SimilarityConfig( embedding_model="sentence-transformers/all-MiniLM-L6-v2", similarity_threshold=0.7, ) ) # Semantic-only score between two MemoryItem objects similarity = framework.calculate_similarity(item1, item2, metrics=["semantic"]) ``` > `EnhancedSimilarityFramework` is not currently exposed as a method on the `SmartMemory` facade. Use the class directly. The available metric names are `content`, `semantic`, `temporal`, `graph`, `metadata`, and `agent_workflow`. ### Temporal Similarity [#temporal-similarity] **Time-Based Relationships:** * **Temporal Proximity** - Closeness in time * **Sequence Similarity** - Order of events * **Duration Overlap** - Time period intersection * **Frequency Patterns** - Recurring temporal patterns **Features:** * Adaptive time windows * Context-aware weighting * Seasonal pattern recognition * Event sequence analysis ### Structural Similarity [#structural-similarity] Structural similarity is implemented by the `graph` metric (`GraphSimilarityMetric`), weighted via `SimilarityConfig.graph_weight`. **Graph-Based Metrics:** * **Neighborhood Similarity** - Shared connections * **Path Similarity** - Common relationship paths * **Centrality Similarity** - Similar importance in network * **Clustering Coefficient** - Local connectivity patterns ### Content Similarity [#content-similarity] **Text-Based Analysis:** * **N-gram Overlap** - Shared word sequences * **TF-IDF Similarity** - Term frequency analysis * **Jaccard Similarity** - Set intersection over union * **Edit Distance** - Character-level differences ## Multi-Dimensional Similarity [#multi-dimensional-similarity] ### Weighted Combination [#weighted-combination] `calculate_similarity` always returns a weighted combination of every enabled metric. The weights live on `SimilarityConfig` (`content_weight`, `semantic_weight`, `temporal_weight`, `graph_weight`, `metadata_weight`, `agent_workflow_weight`) and are normalized to sum to 1.0. Configure them at construction time: ```python from smartmemory.similarity.framework import EnhancedSimilarityFramework, SimilarityConfig framework = EnhancedSimilarityFramework( config=SimilarityConfig( semantic_weight=0.4, temporal_weight=0.3, graph_weight=0.2, content_weight=0.1, metadata_weight=0.0, agent_workflow_weight=0.0, ) ) # Weighted overall score (float) combined_similarity = framework.calculate_similarity(item1, item2) # Pass return_detailed=True for a SimilarityResult with per-metric breakdown result = framework.calculate_similarity(item1, item2, return_detailed=True) result.overall_score # weighted score result.semantic_score # individual metric scores result.confidence # agreement-based confidence ``` ### Adaptive Weighting [#adaptive-weighting] **Context-Dependent Weights:** * **Query Type** - Adjust weights based on search intent * **Memory Type** - Different weights for different memory types * **User Preferences** - Learn from user interactions * **Domain Specific** - Optimize for content domain ### Dynamic Thresholds [#dynamic-thresholds] **Adaptive Thresholds:** * **Statistical Analysis** - Data-driven threshold selection * **Performance Optimization** - Optimize for retrieval quality * **User Feedback** - Adjust based on relevance ratings * **Context Sensitivity** - Different thresholds for different contexts ## Similarity Applications [#similarity-applications] ### Memory Linking [#memory-linking] **Automatic Relationship Discovery:** `find_similar_items` ranks a candidate list against a target item and returns `(MemoryItem, score)` tuples above the threshold, sorted by similarity: ```python from smartmemory.similarity.framework import EnhancedSimilarityFramework framework = EnhancedSimilarityFramework() # Find similar memories from a candidate set similar_memories = framework.find_similar_items( target_item=target_memory, candidate_items=candidates, threshold=0.8, max_results=10, ) for memory_item, score in similar_memories: print(memory_item.item_id, score) ``` > The framework computes and ranks similarity. It does not create graph edges. To materialize relationships, persist links through `SmartMemory`'s graph/linking surface using the `item_id` and `score` returned above. ### Clustering and Organization [#clustering-and-organization] `cluster_items` performs threshold-based agglomerative clustering over a list of items, returning a list of clusters (each a list of `MemoryItem`): ```python from smartmemory.similarity.framework import EnhancedSimilarityFramework framework = EnhancedSimilarityFramework() clusters = framework.cluster_items(items, similarity_threshold=0.8) ``` A module-level `cluster_similar_items(items, similarity_threshold=0.8, config=None, graph_store=None)` convenience function wraps the same call. **Conceptual clustering strategies:** * **Hierarchical Clustering** - Tree-like organization * **K-Means Clustering** - Fixed number of clusters * **DBSCAN** - Density-based clustering * **Community Detection** - Graph-based communities ### Search and Retrieval [#search-and-retrieval] `SmartMemory.search` runs the canonical multi-channel retrieval path. It accepts `top_k` and `channel_weights` (a per-channel weight dict). It does not take `search_type`/`similarity_threshold`/`weights` keyword arguments: ```python # Standard retrieval (top-k ranked results) results = memory.search("machine learning concepts", top_k=5) # Re-weight retrieval channels results = memory.search( "recent project meetings", top_k=5, channel_weights={"semantic": 0.7, "entity-graph": 0.3, "lexical": 0.8}, ) ``` > To score arbitrary item pairs by combined semantic + temporal + graph similarity, use `EnhancedSimilarityFramework.calculate_similarity` (shown above) rather than `search`. ## Advanced Features [#advanced-features] ### Similarity Learning [#similarity-learning] **Machine Learning Enhancement:** * **Metric Learning** - Learn optimal similarity functions * **Deep Similarity Networks** - Neural network-based similarity * **Transfer Learning** - Adapt pre-trained models * **Active Learning** - Improve with user feedback ### Cross-Modal Similarity [#cross-modal-similarity] **Multi-Modal Comparison:** * **Text-Image Similarity** - Compare textual and visual content * **Audio-Text Similarity** - Speech and text comparison * **Temporal-Spatial Similarity** - Time and location relationships * **Structured-Unstructured** - Database and text comparison ### Similarity Caching [#similarity-caching] **Performance Optimization:** The framework caches pairwise similarity results in-memory (FIFO eviction). Caching is controlled by `SimilarityConfig` and inspected/reset via `get_cache_stats()` and `clear_cache()`: ```python from smartmemory.similarity.framework import EnhancedSimilarityFramework, SimilarityConfig framework = EnhancedSimilarityFramework( config=SimilarityConfig(enable_caching=True, max_cache_size=1000) ) framework.get_cache_stats() # {'caching_enabled': True, 'cache_size': ..., ...} framework.clear_cache() ``` To warm the cache, call `calculate_similarity` (or `get_similarity_matrix(items)`, which computes and caches all pairwise scores). There is no separate `precompute`/TTL API. Eviction is size-bounded via `max_cache_size`. ## Configuration and Tuning [#configuration-and-tuning] ### Similarity Profiles [#similarity-profiles] Profiles are expressed as a `SimilarityConfig` dataclass: per-metric weights plus the `similarity_threshold` (minimum to count as similar) and `high_similarity_threshold` (used for clustering). Construct a framework with the config, or swap it on a live framework via `update_config` (which reinitializes metrics and clears the cache): ```python from smartmemory.similarity.framework import EnhancedSimilarityFramework, SimilarityConfig research_profile = SimilarityConfig( semantic_weight=0.5, temporal_weight=0.2, graph_weight=0.2, content_weight=0.1, metadata_weight=0.0, agent_workflow_weight=0.0, similarity_threshold=0.6, high_similarity_threshold=0.8, ) framework = EnhancedSimilarityFramework(config=research_profile) # Or apply a new profile to an existing framework framework.update_config(research_profile) ``` > Weights that do not sum to 1.0 are automatically normalized (with a warning) by `SimilarityConfig.__post_init__`. ### Performance Tuning [#performance-tuning] **Optimization Strategies:** * **Approximate Similarity** - Trade accuracy for speed * **Hierarchical Search** - Coarse-to-fine similarity * **Parallel Processing** - Multi-threaded computation * **GPU Acceleration** - Hardware-accelerated similarity ### Evaluation Metrics [#evaluation-metrics] **Similarity Quality Assessment:** * **Precision\@K** - Relevant results in top K * **Recall** - Coverage of relevant memories * **F1-Score** - Harmonic mean of precision and recall * **Mean Average Precision** - Average precision across queries The similarity framework provides the foundation for intelligent memory organization and retrieval, enabling SmartMemory to understand complex relationships and provide relevant, contextual responses to user queries. # Symbolic Reasoning (/smartmemory/concepts/symbolic-reasoning) SmartMemory's symbolic reasoning layer routes queries to the cheapest effective method, builds auditable proof trees, and scores confidence across multiple dimensions. ## Components [#components] ### Query Router [#query-router] Routes queries to the best retrieval method based on pattern analysis: | Query Type | Method | Cost | Example | | ------------ | ---------------------- | --------- | ----------------------------------------- | | **Symbolic** | Cypher graph traversal | Free | "Who created Python?" | | **Semantic** | Vector search | \~$0.0001 | "Find things related to machine learning" | | **Hybrid** | Both + merge | \~$0.0001 | "Why did we choose PostgreSQL?" | Classification priority: hybrid patterns > semantic patterns > symbolic patterns > default (semantic). ### Proof Trees [#proof-trees] Build auditable reasoning chains by traversing the graph from a decision back to its evidence: ``` Decision: "Use PostgreSQL" ├── DERIVED_FROM: "PostgreSQL supports JSONB" │ └── DERIVED_FROM: "Database comparison analysis" ├── DERIVED_FROM: "Team has PostgreSQL experience" └── CONTRADICTS: "MySQL is simpler to set up" ``` ### Fuzzy Confidence [#fuzzy-confidence] Score decisions across 4 dimensions: | Dimension | Weight | Description | | ---------- | ------ | --------------------------------------------- | | Evidence | 0.4 | How many supporting edges exist | | Recency | 0.2 | How recently the decision was made/reinforced | | Consensus | 0.2 | Reinforcements vs contradictions | | Directness | 0.2 | Depth of evidence chain (direct = 1.0) | ### Residuation [#residuation] Pause decisions when data is incomplete: 1. Create a **pending** decision with requirements 2. When new data arrives, check if requirements are satisfied 3. Auto-activate the decision when all requirements are resolved 4. Pending decisions have confidence 0.0 (won't appear in high-confidence queries) ## REST API [#rest-api] | Method | Endpoint | Description | | ------ | ------------------------------------ | -------------------------------- | | POST | `/memory/reasoning/query` | Route and execute a query | | POST | `/memory/reasoning/proof` | Build proof tree for a decision | | POST | `/memory/reasoning/fuzzy-confidence` | Get multi-dimensional confidence | # Zettelkasten Memory (/smartmemory/concepts/zettelkasten-memory) The SmartMemory system includes a powerful **Zettelkasten memory type** that implements atomic, interconnected knowledge management inspired by the traditional Zettelkasten method used by researchers and knowledge workers. ## What is Zettelkasten Memory? [#what-is-zettelkasten-memory] Zettelkasten (German for "slip box") is a method of knowledge management that stores atomic ideas as individual notes with unique identifiers and creates dense networks of cross-references between related concepts. **Key Characteristics:** * **Atomic Notes**: Each memory item contains a single, focused idea * **Unique Identifiers**: Every note has a permanent, linkable ID * **Bidirectional Linking**: Notes automatically link to related content * **Emergent Structure**: Knowledge organization emerges from connections * **Contextual Discovery**: Find information through associative pathways ## How It Works in SmartMemory [#how-it-works-in-smartmemory] ### Creating Notes [#creating-notes] Zettel notes are a first-class core memory type (`zettel`). The supported public way to create one is through the standard `SmartMemory` write API with `memory_type="zettel"`: ```python from smartmemory import SmartMemory, MemoryItem memory = SmartMemory() # Store a note explicitly typed as a zettel item_id = memory.add(MemoryItem( content="Machine learning models require large datasets for training effectiveness", memory_type="zettel", metadata={"title": "ML data requirements", "tags": ["#ml", "#data", "#training"]}, )) ``` Over the REST API, the dedicated `POST /memory/zettel/notes` endpoint creates a note directly without LLM re-classification: ```bash curl -X POST "$API_URL/memory/zettel/notes" \ -H "Authorization: Bearer $TOKEN" -H "X-Team-Id: $TEAM" \ -H "Content-Type: application/json" \ -d '{ "content": "Machine learning models require large datasets for training effectiveness", "title": "ML data requirements", "tags": ["#ml", "#data", "#training"] }' # → { "note_id": "...", "memory_type": "zettel", "title": "...", "tags": [...] } ``` When notes are created, the system: * Parses `[[wikilinks]]`, `#hashtags`, and concept references from the content * Resolves wikilink titles to existing notes and writes bidirectional `LINKS_TO` edges * Stores tags and title as queryable properties on the note * Builds an interconnected knowledge graph > Notes can also be derived automatically: the `EpisodicToZettelEvolver` > promotes qualifying episodic memories into zettel notes during evolution > (origin `evolver:episodic_to_zettel`). ### Bidirectional Auto-Linking [#bidirectional-auto-linking] When you add new notes, the system automatically: 1. **Wikilink Linking**: Parses `[[Target]]` references and creates bidirectional `LINKS_TO` edges to resolved notes 2. **Backlink Tracking**: Every link is queryable from both directions via `get_backlinks()` and `notes_linked_to()` 3. **Dynamic Relations**: Arbitrary typed relations (e.g. `INSPIRED_BY`, `CONTRADICTS`, `ELABORATES_ON`) can be added between notes 4. **Tag / Concept Linking**: Notes are discoverable by shared tags, concepts, and mentioned entities ### Example: Linking Notes with Wikilinks [#example-linking-notes-with-wikilinks] ```python from smartmemory import SmartMemory, MemoryItem memory = SmartMemory() # Add foundational concepts memory.add(MemoryItem(content="Neural networks learn through backpropagation", memory_type="zettel", metadata={"title": "backpropagation", "tags": ["#neural-networks"]})) memory.add(MemoryItem(content="Gradient descent optimizes model parameters", memory_type="zettel", metadata={"title": "gradient-descent", "tags": ["#optimization"]})) # Add a related concept that explicitly references the earlier notes. # The [[wikilinks]] resolve to existing notes and create bidirectional edges. memory.add(MemoryItem( content="Deep learning combines [[backpropagation]] and [[gradient-descent]] across many layers", memory_type="zettel", metadata={"title": "deep-learning"}, )) ``` ## Interactive Graph Visualization [#interactive-graph-visualization] The system includes a **React-based graph viewer** that lets you: * **Visualize connections** between all your notes * **Navigate associatively** by clicking through related concepts * **Discover knowledge gaps** by seeing sparse areas * **Track knowledge growth** over time The subgraph backing a viewer is served by `GET /memory/zettel/{note_id}/graph`, which returns the notes and edges around a starting note for rendering. ## Graph Maintenance and Analytics [#graph-maintenance-and-analytics] The Zettelkasten memory exposes maintenance and analysis utilities on the `ZettelMemory` class: ### Linking [#linking] * **Wikilink resolution**: `[[Target]]` references resolve to existing notes and write bidirectional `LINKS_TO` edges * **Auto-link toggle**: `enable_auto_linking()` / `disable_auto_linking()` control whether new notes are wikilink-parsed (`auto_link` flag) * **Dynamic relations**: `add_dynamic_relation()` adds arbitrary typed edges ### Pruning [#pruning] * **Low-quality / duplicate detection**: `get_low_quality_or_duplicates(...)` returns prune candidates (short content, few connections, high Jaccard similarity) * **Soft delete**: `prune_or_merge(item)` archives a note rather than hard-deleting it ### Analytics [#analytics] * **Knowledge clusters**: `detect_knowledge_clusters()` finds emergent groupings * **Bridges and emergence**: `find_knowledge_bridges()`, `detect_concept_emergence()` * **System overview**: `get_zettelkasten_overview()` reports total notes, total connections, connection density, cluster count, and health * **Knowledge density**: Connectivity and coverage analysis ## Integration with Other Memory Types [#integration-with-other-memory-types] Zettelkasten memory works seamlessly with other SmartMemory types: ### Evolution from Episodic Memory [#evolution-from-episodic-memory] ```python # Episodic memories can be promoted into Zettel notes by the # EpisodicToZettelEvolver during the pipeline's evolve stage. # These derived notes carry origin="evolver:episodic_to_zettel". memory.add(MemoryItem(content="Learning about transformers", memory_type="episodic")) # During evolve, qualifying episodic items are re-derived as zettel notes # and linked into the existing knowledge graph. ``` ### Retrieving and Reading Notes [#retrieving-and-reading-notes] ```python # Zettel notes are retrieved through the standard SmartMemory read API. item = memory.get(item_id) # fetch a single note by id results = memory.search("transformers") # full-text / vector search across memory ``` > Note: there is no public "promote zettel → semantic" method. Cross-type > transformation happens through the evolution pipeline, not a direct call. ## Best Practices [#best-practices] ### Atomic Note Principle [#atomic-note-principle] ✅ **Good**: "Attention mechanisms allow models to focus on relevant input parts" ❌ **Bad**: "Transformers use attention and are good for NLP and have encoder-decoder..." ### Effective Tagging [#effective-tagging] ```python # Tags are passed in the note's metadata and stored as queryable properties. memory.add(MemoryItem( content="Attention mechanisms allow models to focus on relevant input parts", memory_type="zettel", metadata={ "title": "attention-mechanism", # Use hierarchical tags spanning domain and granularity "tags": ["#ml/attention", "#architecture/transformer", "#concept"], }, )) ``` ### Cross-Reference Patterns [#cross-reference-patterns] ```markdown # In your note content This builds on [[gradient-descent.md]] and relates to [[neural-networks.md]]. See also: [[backpropagation.md]], [[optimization-theory.md]] ``` ## Advanced Features [#advanced-features] These methods live on the `ZettelMemory` class. Construct it directly when you need the richer graph-analysis surface beyond simple create/read: ```python from smartmemory.memory.types.zettel_memory import ZettelMemory zettel = ZettelMemory() ``` ### Backlinks and Connections [#backlinks-and-connections] ```python # All notes that link TO a given note backlinks = zettel.get_backlinks(note_id) # Complete bidirectional view (incoming + outgoing) of a note's connections connections = zettel.get_bidirectional_connections(note_id) # Explicit typed relations between notes zettel.add_dynamic_relation(source_id, target_id, "ELABORATES_ON") ``` ### Emergent Structure [#emergent-structure] ```python # Detect emergent knowledge clusters from connection patterns clusters = zettel.detect_knowledge_clusters(min_cluster_size=3) # Find notes that bridge different knowledge domains bridges = zettel.find_knowledge_bridges() # Detect concepts that are emerging from connection patterns emerging = zettel.detect_concept_emergence() ``` ### Serendipitous Discovery [#serendipitous-discovery] ```python # Suggest related notes for a starting note suggestions = zettel.suggest_related_notes(note_id, suggestion_count=5) # Find paths between two notes through the graph paths = zettel.find_knowledge_paths(start_note_id, end_note_id, max_depth=5) # Random walk for serendipitous exploration walk = zettel.random_walk_discovery(note_id, walk_length=5) ``` > **Note on imports/integration:** there is no built-in Obsidian importer or > graph-export method on `ZettelMemory`. External vault import is handled by the > separate vault-sync application surface, and graph data for visualization is > served by the REST `GET /memory/zettel/{note_id}/graph` endpoint rather than a > Python `export_graph` call. ## Use Cases [#use-cases] ### Research & Learning [#research--learning] * **Literature review**: Connect papers, concepts, and insights * **Course notes**: Build interconnected understanding across subjects * **Project documentation**: Link decisions, implementations, and learnings ### Software Development [#software-development] * **Architecture documentation**: Connect components, patterns, and decisions * **Code knowledge**: Link algorithms, implementations, and examples * **Bug tracking**: Connect issues, solutions, and root causes ### Creative Work [#creative-work] * **Idea development**: Connect inspirations, concepts, and iterations * **Project planning**: Link requirements, resources, and constraints * **Knowledge synthesis**: Combine insights from multiple domains ## API Reference [#api-reference] ### Creating and reading notes (`SmartMemory`) [#creating-and-reading-notes-smartmemory] ```python from smartmemory import SmartMemory, MemoryItem memory = SmartMemory() # Create a zettel note item_id = memory.add(MemoryItem(content, memory_type="zettel", metadata={"title": ..., "tags": [...]})) # Retrieve and search note = memory.get(item_id) results = memory.search(query, top_k=10) ``` ### Graph operations (`ZettelMemory`) [#graph-operations-zettelmemory] ```python from smartmemory.memory.types.zettel_memory import ZettelMemory zettel = ZettelMemory() # Tag / property / relation queries notes = zettel.find_notes_by_tag(tag_name) notes = zettel.find_notes_by_property(key, value) notes = zettel.notes_linked_to(note_id) notes = zettel.notes_mentioning(entity_id) notes = zettel.query_by_dynamic_relation(source_id, relation_type) # Links and backlinks backlinks = zettel.get_backlinks(note_id) connections = zettel.get_bidirectional_connections(note_id) zettel.create_bidirectional_link(source_id, target_id, link_type="LINKS_TO") zettel.add_dynamic_relation(source_id, target_id, relation_type) # Emergent structure clusters = zettel.detect_knowledge_clusters(min_cluster_size=3) bridges = zettel.find_knowledge_bridges() emerging = zettel.detect_concept_emergence() # Discovery suggestions = zettel.suggest_related_notes(note_id, suggestion_count=5) paths = zettel.find_knowledge_paths(start_note_id, end_note_id, max_depth=5) walk = zettel.random_walk_discovery(note_id, walk_length=5) # Wikilink parsing and system overview parsed = zettel.parse_wikilinks(content) # → {wikilinks, concepts, hashtags} overview = zettel.get_zettelkasten_overview() # totals, density, clusters, health ``` ### REST endpoints (`/memory/zettel/*`) [#rest-endpoints-memoryzettel] ```text POST /memory/zettel/notes Create a note GET /memory/zettel/by-tag/{tag} Notes by tag GET /memory/zettel/by-property Notes by property (key, value) GET /memory/zettel/mentioning/{entity_id} Notes mentioning an entity GET /memory/zettel/{note_id}/backlinks Incoming links GET /memory/zettel/{note_id}/forward-links Outgoing links GET /memory/zettel/{note_id}/connections All connections GET /memory/zettel/{note_id}/path/{target_id} Path between two notes GET /memory/zettel/{note_id}/suggestions Suggested related notes GET /memory/zettel/{note_id}/graph Subgraph for visualization GET /memory/zettel/clusters Knowledge clusters GET /memory/zettel/bridges Bridge notes GET /memory/zettel/concept-emergence Emerging concepts POST /memory/zettel/wikilink/parse Parse wikilinks from content ``` ## Configuration [#configuration] The main runtime knob is automatic wikilink linking, controlled by the `auto_link` constructor flag (default `True`) and the toggle methods: ```python from smartmemory.memory.types.zettel_memory import ZettelMemory # Disable automatic wikilink parsing at construction time zettel = ZettelMemory(auto_link=False) # Or toggle it at runtime zettel.enable_auto_linking() zettel.disable_auto_linking() ``` Broader memory configuration (storage backend, embeddings, scoping) is supplied through the standard `MemoryConfig` passed to `ZettelMemory(config=...)` / `SmartMemory`, the same as every other memory type. The Zettelkasten memory type transforms SmartMemory into a powerful knowledge management system that grows smarter and more connected over time, supporting both personal learning and collaborative knowledge building. # Bulk Ingestion (/smartmemory/examples/bulk-ingestion) This walkthrough shows how to load a large dataset into SmartMemory using `ingest_batch()` for arbitrary items and `ingest_conversation()` for chat-style data with sessions. Both APIs run the **full 11-stage pipeline** per item — they're not shortcuts. They just parallelize the work with `asyncio.TaskGroup` + `Semaphore` so you don't pay round-trip latency on every item. ## When to use what [#when-to-use-what] | API | Input shape | Concurrency cap | Container node | | -------------------------------------------------- | ---------------------------------------- | ------------------ | ---------------------------------------------------- | | `ingest_batch` / `ingest_batch_sync` | List of strings, dicts, or `MemoryItem`s | `max_concurrent=8` | None | | `ingest_conversation` / `ingest_conversation_sync` | List of `{speaker, content}` turns | `max_concurrent=4` | One INDEXED `conversation` node with `PART_OF` edges | ## Bulk-ingesting an arbitrary dataset [#bulk-ingesting-an-arbitrary-dataset] ```python """Bulk-ingest a 1,000-item dataset and verify with search.""" from smartmemory import SmartMemory memory = SmartMemory() # 1. Build the items list. Strings, dicts, or MemoryItems all work. def load_articles(path: str) -> list[str]: with open(path) as f: # one summary per line for the demo return [line.strip() for line in f if line.strip()] items = load_articles("./articles.txt") # ~1000 lines print(f"loaded {len(items)} items") # 2. Run the batch (sync wrapper — must not be inside an event loop) response = memory.ingest_batch_sync(items, max_concurrent=8) # 3. Inspect aggregate metrics m = response.metrics print( f"ingested {m.succeeded}/{m.total_items} " f"in {m.total_duration_ms/1000:.1f}s " f"({m.items_per_second:.1f} items/sec, " f"{m.rate_limit_hits} rate-limit retries)" ) # 4. Spot per-item failures failures = [r for r in response.results if not r.ok] for f in failures[:5]: print(f"FAIL: {f.error}") # 5. Search to verify it landed hits = memory.search("renewable energy", top_k=5) for h in hits: print(f" - {h.content[:80]}") ``` ### Watching progress [#watching-progress] `ingest_batch()` doesn't return progress events directly, but each ingest call inside the batch emits to the [Progress Event Bus](/smartmemory/reference/progress-events/) on Redis Streams. Subscribe to `GET /memory/progress/stream` while the batch runs to watch it live. For a quick stdout-style progress bar, drive the loop yourself instead of using `ingest_batch_sync`: ```python import asyncio from smartmemory.batch import ingest_batch async def run_with_progress(memory, items): response = await ingest_batch(memory, items, max_concurrent=8) return response async def main(): response = await run_with_progress(memory, items) for i, r in enumerate(response.results): status = "ok" if r.ok else f"err: {r.error}" print(f"[{i+1}/{len(items)}] {r.duration_ms:.0f}ms {status}") asyncio.run(main()) ``` ### Forwarding kwargs [#forwarding-kwargs] All `ingest()` kwargs are forwarded per item: ```python response = memory.ingest_batch_sync( items, max_concurrent=8, extract_decisions=True, # auto-extract decisions per item extract_reasoning=False, context={"source": "research-corpus-2024"}, ) ``` ## Conversation ingestion [#conversation-ingestion] For chat history, use `ingest_conversation()`. It groups turns into session chunks (each chunk runs the full pipeline once), creates a single `conversation` container node, and links every chunk back via `PART_OF`. ```python """Import an exported chat into SmartMemory as session-chunked memories.""" from smartmemory import SmartMemory memory = SmartMemory() turns = [ {"speaker": "user", "content": "I'm refactoring the billing service."}, {"speaker": "assistant", "content": "What's the current architecture?"}, {"speaker": "user", "content": "Single Postgres, monolith. We want to split."}, # ... 100s more ... ] # Optional — explicit session boundaries (turn indices where new sessions start) session_boundaries = [0, 30, 75] # 3 sessions response = memory.ingest_conversation_sync( turns, session_boundaries=session_boundaries, conversation_id="acme-billing-refactor", turns_per_chunk=15, # auto-chunk size when no boundaries given max_chunk_chars=12000, # safety cap per chunk max_concurrent=4, ) print(f"conversation node: {response.conversation_node_id}") print(f"chunks: {response.chunks_ingested} ok / {response.chunks_failed} failed") print(f"total turns: {response.total_turns}") print(f"elapsed: {response.total_duration_ms/1000:.1f}s") # Each chunk is a regular memory item — search across them as usual for hit in memory.search("billing service architecture", top_k=3): print(f" - {hit.content[:100]}") ``` ### Auto-chunking [#auto-chunking] Omit `session_boundaries` and `ConversationChunker` will auto-chunk: groups of `turns_per_chunk` turns, capped at `max_chunk_chars`. Each chunk is concatenated as `[Speaker]: content` lines and runs through `ingest()`. ### Session dates [#session-dates] If you have explicit timestamps per session, pass `session_dates=["2024-09-01", "2024-09-12", ...]` (one per boundary) so the temporal enricher can ground each chunk correctly. ## Tuning concurrency [#tuning-concurrency] * **`max_concurrent`** is a hard cap on simultaneous LLM calls. Default 8 for `ingest_batch`, 4 for `ingest_conversation`. * **Rate-limit retries** — the batch runner retries up to 3 times on 429 responses with exponential backoff; surfaced as `metrics.rate_limit_hits`. * **Memory shared across threads** — each item runs `ingest()` in `asyncio.to_thread()`. The shared SmartMemory instance is safe; cache fills and FalkorDB writes have known benign races (see `docs/features/RLM-1e/design.md`). ## Verifying ingestion [#verifying-ingestion] After a large import, sanity-check it: ```python # Total count via search (sample query) n_hits = len(memory.search("the", top_k=1000)) print(f"sampled {n_hits} items via broad search") # Last batch metrics persist on the memory instance print(f"last_trajectory: {memory.last_trajectory.profile_name if memory.last_trajectory else 'none'}") ``` For exact counts or graph stats, query the API directly (`GET /memory/stats`) or the graph backend. ## Related [#related] * [Progress Events](/smartmemory/reference/progress-events/) — live ingestion feed * [Lite Mode](/smartmemory/guides/lite-mode/) — useful for benchmarking bulk ingest locally # Conversational AI with SmartMemory (/smartmemory/examples/conversational-ai) This example demonstrates how to build a conversational AI system that remembers previous conversations and builds context over time using SmartMemory. ## Overview [#overview] A memory-enhanced conversational AI can: * Remember previous conversations across sessions * Build understanding of user preferences and context * Provide personalized responses based on history * Learn from interactions to improve over time ## Basic Implementation [#basic-implementation] ### Simple Memory-Enhanced Chatbot [#simple-memory-enhanced-chatbot] ```python from smartmemory import SmartMemory import openai from datetime import datetime, UTC from typing import List, Dict class MemoryEnhancedChatbot: def __init__(self, user_id: str, openai_api_key: str): self.user_id = user_id self.memory = SmartMemory() openai.api_key = openai_api_key def chat(self, user_message: str) -> str: """Process user message and return AI response with memory context""" # 1. Store user message self._store_user_message(user_message) # 2. Retrieve relevant context context = self._get_relevant_context(user_message) # 3. Generate response with context ai_response = self._generate_response(user_message, context) # 4. Store AI response self._store_ai_response(ai_response) return ai_response def _store_user_message(self, message: str): """Store user message in memory""" self.memory.ingest({ "content": f"User said: {message}", "memory_type": "episodic", "metadata": { "timestamp": datetime.now(UTC).isoformat(), "speaker": "user", "message_type": "input" } }) def _store_ai_response(self, response: str, sources: List[str] = None): """Store AI response in memory with optional source grounding""" memory_item_id = self.memory.ingest({ "content": f"I responded: {response}", "memory_type": "episodic", "metadata": { "timestamp": datetime.now(UTC).isoformat(), "speaker": "assistant", "message_type": "response" } }) # Ground response to sources if provided if sources: for source_url in sources: self.memory.ground( item_id=memory_item_id, source_url=source_url, validation={ "confidence": 0.8, "grounding_type": "ai_response_source", "grounded_at": datetime.now(UTC).isoformat() } ) def _get_relevant_context(self, message: str, max_context: int = 5) -> List[str]: """Retrieve relevant conversation context""" # Search for relevant memories relevant_memories = self.memory.search( query=message, memory_type="episodic", top_k=max_context ) # Extract context strings context = [] for memory in relevant_memories: context.append(memory.content) return context def _generate_response(self, message: str, context: List[str]) -> str: """Generate AI response using OpenAI with memory context""" # Build context string context_str = "\n".join(context) if context else "No previous context." # Create prompt with context prompt = f""" Previous conversation context: {context_str} Current user message: {message} Please respond as a helpful assistant, taking into account the previous context. Be conversational and reference previous topics when relevant. """ try: response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a helpful assistant with memory of previous conversations."}, {"role": "user", "content": prompt} ], max_tokens=500, temperature=0.7 ) return response.choices[0].message.content.strip() except Exception as e: return f"I'm sorry, I encountered an error: {str(e)}" # Usage example chatbot = MemoryEnhancedChatbot( user_id="user123", openai_api_key="your-openai-api-key" ) # Conversation print(chatbot.chat("Hi, I'm working on a Python project")) print(chatbot.chat("I'm having trouble with authentication")) print(chatbot.chat("What was I working on yesterday?")) # References previous context ``` ## Advanced Implementation [#advanced-implementation] ### Multi-User Conversational AI with Personality [#multi-user-conversational-ai-with-personality] ```python from smartmemory import SmartMemory from typing import Dict, List, Optional import json from datetime import datetime, timedelta, UTC class AdvancedConversationalAI: def __init__(self): self.memory = SmartMemory() self.user_profiles = {} def chat(self, user_id: str, message: str, session_id: Optional[str] = None) -> Dict: """Enhanced chat with user profiling and session management""" # Get or create user profile profile = self._get_user_profile(user_id) # Store message with rich metadata self._store_message(user_id, message, "user", session_id) # Get personalized context context = self._get_personalized_context(user_id, message, session_id) # Generate response response = self._generate_personalized_response( user_id, message, context, profile ) # Store response self._store_message(user_id, response, "assistant", session_id) # Update user profile self._update_user_profile(user_id, message, response) return { "response": response, "context_used": len(context), "user_profile": profile, "session_id": session_id } def _get_user_profile(self, user_id: str) -> Dict: """Get or create user personality profile""" if user_id not in self.user_profiles: # Search for existing profile in memory # Note: User filtering is handled by ScopeProvider in multi-tenant deployments profile_memories = self.memory.search( f"user_profile_{user_id}", memory_type="semantic" ) if profile_memories: # Load existing profile profile_data = json.loads(profile_memories[0].content) self.user_profiles[user_id] = profile_data else: # Create new profile self.user_profiles[user_id] = { "preferences": {}, "interests": [], "communication_style": "neutral", "expertise_areas": [], "conversation_count": 0, "first_interaction": datetime.now(UTC).isoformat(), "last_interaction": datetime.now(UTC).isoformat() } return self.user_profiles[user_id] def _store_message(self, user_id: str, message: str, speaker: str, session_id: Optional[str]): """Store message with rich metadata""" self.memory.ingest({ "content": f"{speaker}: {message}", "memory_type": "episodic", "metadata": { "session_id": session_id or "default", "speaker": speaker, "timestamp": datetime.now(UTC).isoformat(), "message_length": len(message), "contains_question": "?" in message } }) def _get_personalized_context(self, user_id: str, message: str, session_id: Optional[str]) -> List[Dict]: """Get context personalized for the user""" context = [] # Recent session context (high priority) if session_id: recent_session = self.memory.search( query=message, memory_type="episodic", top_k=3 ) context.extend([{ "content": mem.content, "relevance": "session", "timestamp": mem.metadata.get("timestamp") } for mem in recent_session]) # Relevant historical context historical = self.memory.search( query=message, memory_type="episodic", top_k=5 ) context.extend([{ "content": mem.content, "relevance": "historical", "timestamp": mem.metadata.get("timestamp") } for mem in historical]) # User preferences and interests profile = self.user_profiles.get(user_id, {}) for interest in profile.get("interests", []): if interest.lower() in message.lower(): context.append({ "content": f"User is interested in {interest}", "relevance": "preference", "timestamp": None }) return context[:8] # Limit context size def _generate_personalized_response(self, user_id: str, message: str, context: List[Dict], profile: Dict) -> str: """Generate response adapted to user's communication style""" # Build context string with relevance context_parts = [] for ctx in context: relevance = ctx["relevance"] content = ctx["content"] context_parts.append(f"[{relevance}] {content}") context_str = "\n".join(context_parts) # Adapt style based on user profile style_instructions = self._get_style_instructions(profile) prompt = f""" User Profile: - Communication style: {profile.get('communication_style', 'neutral')} - Interests: {', '.join(profile.get('interests', []))} - Expertise areas: {', '.join(profile.get('expertise_areas', []))} - Conversation count: {profile.get('conversation_count', 0)} Conversation Context: {context_str} Style Instructions: {style_instructions} Current message: {message} Respond appropriately considering the user's profile and conversation history. """ # Use your preferred LLM here return self._call_llm(prompt) def _get_style_instructions(self, profile: Dict) -> str: """Get communication style instructions based on user profile""" style = profile.get("communication_style", "neutral") style_map = { "formal": "Use formal language and professional tone", "casual": "Use casual, friendly language with contractions", "technical": "Use technical terminology and detailed explanations", "concise": "Keep responses brief and to the point", "enthusiastic": "Use energetic and positive language", "neutral": "Use balanced, helpful tone" } return style_map.get(style, style_map["neutral"]) def _update_user_profile(self, user_id: str, user_message: str, ai_response: str): """Update user profile based on interaction""" profile = self.user_profiles[user_id] # Update conversation count profile["conversation_count"] += 1 profile["last_interaction"] = datetime.now(UTC).isoformat() # Extract interests from message self._extract_interests(user_message, profile) # Detect communication style self._detect_communication_style(user_message, profile) # Store updated profile self.memory.ingest({ "content": json.dumps(profile), "memory_type": "semantic", "metadata": { "content_type": "user_profile", "updated_at": datetime.now(UTC).isoformat() } }) def _extract_interests(self, message: str, profile: Dict): """Extract and update user interests""" # Simple keyword-based interest detection interest_keywords = { "programming": ["python", "javascript", "coding", "development"], "ai": ["ai", "machine learning", "neural networks", "llm"], "sports": ["football", "basketball", "soccer", "tennis"], "music": ["music", "guitar", "piano", "singing"], "travel": ["travel", "vacation", "trip", "country"] } message_lower = message.lower() for interest, keywords in interest_keywords.items(): if any(keyword in message_lower for keyword in keywords): if interest not in profile["interests"]: profile["interests"].append(interest) def _detect_communication_style(self, message: str, profile: Dict): """Detect and update communication style""" # Simple style detection if len(message.split()) > 20: profile["communication_style"] = "detailed" elif "!" in message or message.isupper(): profile["communication_style"] = "enthusiastic" elif any(word in message.lower() for word in ["please", "thank you", "sir", "madam"]): profile["communication_style"] = "formal" else: profile["communication_style"] = "casual" def _call_llm(self, prompt: str) -> str: """Call your preferred LLM service""" # Implement your LLM call here # This is a placeholder return "This is a placeholder response. Implement your LLM integration here." def get_conversation_summary(self, user_id: str, days: int = 7) -> Dict: """Get conversation summary for a user""" # Get recent conversations # Note: User filtering handled by ScopeProvider in multi-tenant deployments recent_memories = self.memory.search( query="*", # Wildcard to get all memory_type="episodic", top_k=50 ) # Filter by date cutoff_date = datetime.now(UTC) - timedelta(days=days) recent_conversations = [] for memory in recent_memories: timestamp_str = memory.metadata.get("timestamp") if timestamp_str: timestamp = datetime.fromisoformat(timestamp_str) if timestamp > cutoff_date: recent_conversations.append(memory) # Analyze conversations total_messages = len(recent_conversations) user_messages = len([m for m in recent_conversations if "User said:" in m.content]) ai_messages = len([m for m in recent_conversations if "I responded:" in m.content]) return { "total_messages": total_messages, "user_messages": user_messages, "ai_messages": ai_messages, "days_analyzed": days, "average_daily_messages": total_messages / days if days > 0 else 0, "recent_topics": self._extract_recent_topics(recent_conversations) } def _extract_recent_topics(self, conversations: List) -> List[str]: """Extract main topics from recent conversations""" # Simple topic extraction - could be enhanced with NLP all_text = " ".join([conv.content for conv in conversations]) # Basic keyword frequency words = all_text.lower().split() word_freq = {} # Filter out common words stop_words = {"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "by", "said", "responded", "user", "i"} for word in words: if len(word) > 3 and word not in stop_words: word_freq[word] = word_freq.get(word, 0) + 1 # Return top topics sorted_topics = sorted(word_freq.items(), key=lambda x: x[1], reverse=True) return [topic[0] for topic in sorted_topics[:5]] # Usage example ai = AdvancedConversationalAI() # Multi-turn conversation user_id = "alice123" session_id = "session_001" response1 = ai.chat(user_id, "Hi! I'm learning Python programming", session_id) print(f"AI: {response1['response']}") response2 = ai.chat(user_id, "I'm having trouble with loops", session_id) print(f"AI: {response2['response']}") response3 = ai.chat(user_id, "What was I learning about earlier?", session_id) print(f"AI: {response3['response']}") # Get conversation summary summary = ai.get_conversation_summary(user_id, days=7) print(f"Conversation summary: {summary}") ``` ## Integration with Popular Frameworks [#integration-with-popular-frameworks] ### LangChain Integration [#langchain-integration] ```python from langchain.memory import ConversationBufferMemory from langchain.chains import ConversationChain from langchain.llms import OpenAI from smartmemory import SmartMemory class SmartMemoryLangChain: def __init__(self, user_id: str): self.user_id = user_id self.smart_memory = SmartMemory() self.conversation_memory = ConversationBufferMemory() self.llm = OpenAI(temperature=0.7) self.chain = ConversationChain( llm=self.llm, memory=self.conversation_memory ) def chat(self, message: str) -> str: # Get relevant long-term context # Note: User filtering handled by ScopeProvider in multi-tenant deployments context = self.smart_memory.search( query=message, top_k=3 ) # Add context to conversation if context: context_str = "\n".join([mem.content for mem in context]) enhanced_message = f"Context: {context_str}\n\nUser: {message}" else: enhanced_message = message # Get response from LangChain response = self.chain.predict(input=enhanced_message) # Store in SmartMemory for long-term retention self.smart_memory.ingest(f"User: {message}") self.smart_memory.ingest(f"Assistant: {response}") return response ``` ### Streamlit Chat Interface [#streamlit-chat-interface] ```python import streamlit as st from smartmemory import SmartMemory def create_chat_interface(): st.title("Memory-Enhanced Chatbot") # Initialize session state if "memory" not in st.session_state: st.session_state.memory = SmartMemory() if "messages" not in st.session_state: st.session_state.messages = [] # Display chat history for message in st.session_state.messages: with st.chat_message(message["role"]): st.markdown(message["content"]) # Chat input if prompt := st.chat_input("What would you like to talk about?"): # Add user message to chat history st.session_state.messages.append({"role": "user", "content": prompt}) with st.chat_message("user"): st.markdown(prompt) # Get relevant context context = st.session_state.memory.search(prompt, top_k=3) # Generate response (simplified) if context: response = f"Based on our previous conversations about {', '.join([c.content[:50] for c in context])}, I think..." else: response = "I don't have previous context about this topic, but I can help you with..." # Add assistant response to chat history st.session_state.messages.append({"role": "assistant", "content": response}) with st.chat_message("assistant"): st.markdown(response) # Store conversation in memory st.session_state.memory.add(f"User: {prompt}") st.session_state.memory.add(f"Assistant: {response}") if __name__ == "__main__": create_chat_interface() ``` ## Best Practices [#best-practices] ### Memory Management [#memory-management] 1. **User Isolation**: Always use user\_id to separate conversations 2. **Session Management**: Use session\_id for conversation context 3. **Memory Types**: Use episodic for conversations, semantic for facts 4. **Context Limits**: Limit context size to avoid overwhelming the LLM ### Performance Optimization [#performance-optimization] 1. **Background Processing**: Use `ingest()` for real-time conversations 2. **Caching**: Cache user profiles and frequent queries 3. **Batch Operations**: Process multiple messages together when possible ### Privacy and Security [#privacy-and-security] 1. **Data Encryption**: Encrypt sensitive conversation data 2. **User Consent**: Get explicit consent for memory storage 3. **Data Retention**: Implement retention policies 4. **Access Control**: Restrict access to user-specific memories ## Next Steps [#next-steps] * [Learning Assistant Example](/smartmemory/examples/learning-assistant/) - Educational AI with memory * [Knowledge Graph Example](/smartmemory/examples/knowledge-graph/) - Build knowledge networks * [Memory Operations](/smartmemory/guides/memory-operations/) - Explore the full operation surface * [MCP Integration](/smartmemory/guides/mcp-integration/) - Connect with LLM agents # Decisions Lifecycle (/smartmemory/examples/decisions-lifecycle) This walkthrough drives a Decision through its lifecycle with the local Python API: 1. **Create** a decision with evidence, rejected alternatives, constraints, and rationale 2. **Reinforce** it as new evidence arrives 3. **Contradict** it when counter-evidence appears 4. **Supersede** it when reality changes 5. **Search and retrieve** the current decision 6. **Inspect provenance and causal chains** 7. **Retract** it when it is no longer valid Decisions are first-class memory items in SmartMemory. See [`concepts/decision-memory`](/smartmemory/concepts/decision-memory/) for the model. This guide focuses on the runnable local API surface. ## End-to-end lifecycle [#end-to-end-lifecycle] ```python from smartmemory.models.decision import Decision from smartmemory.models.memory_item import MemoryItem from smartmemory.pipeline.config import PipelineConfig from smartmemory.tools.factory import lite_context lite = dict(pipeline_profile=PipelineConfig.lite(llm_enabled=False)) with lite_context(**lite) as memory: evidence_id = memory.add( MemoryItem( content="Latency p95 measured at 850ms after switching to pgbouncer.", memory_type="observation", ) ) decision = memory.add_decision( "Use pgbouncer transaction pooling for the billing service.", decision_type="choice", confidence=0.85, evidence_ids=[evidence_id], domain="billing", tags=["infra", "pooling"], rationale="Reduces connection churn and brings p95 under 200ms.", rejected_alternatives=["session pooling", "client-side pooling"], constraints=["max_client_conns <= 200"], ) support_id = memory.add( MemoryItem( content="After 2 weeks in prod: p95 is about 140ms.", memory_type="observation", ) ) reinforced = memory.reinforce_decision(decision.decision_id, support_id) contradiction_id = memory.add( MemoryItem( content="Spike day: transaction pooling broke prepared statements.", memory_type="observation", ) ) contradicted = memory.contradict_decision( decision.decision_id, contradiction_id, ) replacement = Decision( content="Use pgbouncer session pooling with a prepared-statement workaround.", decision_type="choice", confidence=0.8, ) replacement = memory.supersede_decision( decision.decision_id, replacement, reason="Transaction pooling broke ORM prepared statements.", ) hits = memory.search("billing pooling decision", expertise=True) retrieved = memory.get_decision(replacement.decision_id) provenance = memory.get_decision_provenance(replacement.decision_id) causal = memory.get_decision_causal_chain( replacement.decision_id, direction="both", max_depth=3, ) memory.retract_decision( replacement.decision_id, reason="Pooling no longer needed after load dropped.", ) retracted = memory.get_decision(replacement.decision_id) print(decision.decision_id) print(reinforced.reinforcement_count, contradicted.contradiction_count) print(memory.get_decision(decision.decision_id).status, retrieved.status) print([d.content for d in hits["decision"]]) print(sorted(provenance.keys()), sorted(causal.keys())) print(retracted.status) ``` `add_decision(...)` returns a `Decision` object with `.decision_id`, `.rejected_alternatives`, `.rationale`, `.constraints`, `.confidence`, and `.status`. `reinforce_decision(...)`, `contradict_decision(...)`, `supersede_decision(...)`, and `retract_decision(...)` are real lifecycle methods on `SmartMemory`. `search(expertise=True)` returns a dictionary keyed by expertise type, not a flat list. Read decisions from `hits["decision"]`. ## Service and client notes [#service-and-client-notes] The authenticated service exposes the same lifecycle under `/memory/decisions/*`, and `smartmemory-client` exposes helpers such as `create_decision(...)`, `reinforce_decision(...)`, `supersede_decision(...)`, `retract_decision(...)`, `get_decision(...)`, `list_decisions(...)`, `get_provenance_chain(...)`, and `get_causal_chain(...)`. For local examples and tests, prefer the core API above. It runs without a service, token, or workspace UUID. ## Related [#related] * [Decision Memory concepts](/smartmemory/concepts/decision-memory/) * [Reasoning Traces](/smartmemory/concepts/reasoning-traces/), pair with `source_trace_id` on decision creation # Knowledge Graph Example (/smartmemory/examples/knowledge-graph) This example demonstrates how to build a dynamic knowledge graph using SmartMemory that automatically discovers relationships between concepts and visualizes knowledge networks. ## Overview [#overview] The Knowledge Graph example showcases SmartMemory's ability to: * **Automatic Relationship Discovery**: Find connections between concepts * **Graph Visualization**: Display knowledge networks * **Semantic Clustering**: Group related concepts * **Interactive Exploration**: Navigate through knowledge relationships * **Zettelkasten Integration**: Build interconnected note networks with automatic linking ## Implementation [#implementation] ### Basic Knowledge Graph [#basic-knowledge-graph] ```python from smartmemory import SmartMemory import networkx as nx import matplotlib.pyplot as plt class KnowledgeGraph: def __init__(self): self.memory = SmartMemory( config={ "graph": {"backend": "FalkorDBBackend"}, "evolution": {"relationship_discovery": {"enabled": True}} } ) self.graph = nx.Graph() def add_knowledge(self, concept, description, related_concepts=None): """Add a concept to the knowledge graph""" # Store in semantic memory memory_id = self.memory.ingest({ "content": f"{concept}: {description}", "memory_type": "semantic", "metadata": { "concept": concept, "description": description, "related_concepts": related_concepts or [] } }) # Add to NetworkX graph self.graph.add_node(concept, description=description, memory_id=memory_id) # Add explicit relationships if related_concepts: for related in related_concepts: self.graph.add_edge(concept, related, relationship="related") return memory_id def discover_relationships(self, similarity_threshold=0.7): """Discover implicit relationships between concepts""" from smartmemory.similarity.framework import EnhancedSimilarityFramework similarity_framework = EnhancedSimilarityFramework() concepts = list(self.graph.nodes()) for i, concept1 in enumerate(concepts): for concept2 in concepts[i+1:]: # Get memories for both concepts results1 = self.memory.search(concept1, memory_type="semantic", top_k=1) results2 = self.memory.search(concept2, memory_type="semantic", top_k=1) if not results1 or not results2: continue memory1, memory2 = results1[0], results2[0] # Calculate similarity using the framework similarity = similarity_framework.calculate_similarity(memory1, memory2) if similarity > similarity_threshold: self.graph.add_edge( concept1, concept2, relationship="similar", similarity=similarity ) ``` ### Graph Visualization [#graph-visualization] ```python def visualize_graph(self, layout="spring", figsize=(12, 8)): """Visualize the knowledge graph""" plt.figure(figsize=figsize) # Choose layout if layout == "spring": pos = nx.spring_layout(self.graph, k=1, iterations=50) elif layout == "circular": pos = nx.circular_layout(self.graph) else: pos = nx.random_layout(self.graph) # Draw nodes nx.draw_networkx_nodes( self.graph, pos, node_color='lightblue', node_size=1000, alpha=0.7 ) # Draw edges with different colors for different relationships edge_colors = [] for u, v, data in self.graph.edges(data=True): if data.get('relationship') == 'similar': edge_colors.append('red') else: edge_colors.append('blue') nx.draw_networkx_edges( self.graph, pos, edge_color=edge_colors, alpha=0.5 ) # Draw labels nx.draw_networkx_labels(self.graph, pos, font_size=8) plt.title("Knowledge Graph") plt.axis('off') plt.tight_layout() plt.show() def get_concept_neighbors(self, concept, max_depth=2): """Get neighboring concepts up to a certain depth""" if concept not in self.graph: return [] neighbors = [] visited = set() queue = [(concept, 0)] while queue: current, depth = queue.pop(0) if current in visited or depth > max_depth: continue visited.add(current) if depth > 0: # Don't include the starting concept neighbors.append({ "concept": current, "depth": depth, "description": self.graph.nodes[current].get("description", "") }) # Add neighbors to queue for neighbor in self.graph.neighbors(current): if neighbor not in visited: queue.append((neighbor, depth + 1)) return neighbors ``` ### Usage Example [#usage-example] ```python # Initialize knowledge graph kg = KnowledgeGraph() # Add machine learning concepts kg.add_knowledge( "Neural Networks", "Computing systems inspired by biological neural networks", related_concepts=["Deep Learning", "Artificial Intelligence"] ) kg.add_knowledge( "Deep Learning", "Machine learning using neural networks with multiple layers", related_concepts=["Neural Networks", "Machine Learning"] ) kg.add_knowledge( "Machine Learning", "Algorithms that improve through experience", related_concepts=["Artificial Intelligence", "Data Science"] ) kg.add_knowledge( "Natural Language Processing", "AI field focused on interaction between computers and human language", related_concepts=["Machine Learning", "Linguistics"] ) # Discover additional relationships kg.discover_relationships(similarity_threshold=0.6) # Visualize the graph kg.visualize_graph() # Explore concept relationships neighbors = kg.get_concept_neighbors("Machine Learning", max_depth=2) for neighbor in neighbors: print(f"Depth {neighbor['depth']}: {neighbor['concept']}") ``` ### Advanced Features [#advanced-features] ```python def get_concept_clusters(self, algorithm="louvain"): """Identify clusters of related concepts""" if algorithm == "louvain": import community clusters = community.best_partition(self.graph) else: # Use simple connected stages clusters = {} for i, component in enumerate(nx.connected_components(self.graph)): for node in component: clusters[node] = i return clusters def find_knowledge_paths(self, start_concept, end_concept): """Find paths between two concepts""" try: paths = list(nx.all_simple_paths( self.graph, start_concept, end_concept, cutoff=4 )) return paths except nx.NetworkXNoPath: return [] def get_central_concepts(self, metric="betweenness"): """Find the most central/important concepts""" if metric == "betweenness": centrality = nx.betweenness_centrality(self.graph) elif metric == "degree": centrality = nx.degree_centrality(self.graph) else: centrality = nx.closeness_centrality(self.graph) return sorted(centrality.items(), key=lambda x: x[1], reverse=True) ``` ## Features [#features] * **Automatic Relationship Discovery**: Uses similarity metrics to find connections * **Multiple Visualization Layouts**: Spring, circular, and random layouts * **Concept Clustering**: Group related concepts automatically * **Path Finding**: Discover knowledge paths between concepts * **Centrality Analysis**: Identify key concepts in the network * **Interactive Exploration**: Navigate through concept relationships This example demonstrates how SmartMemory can automatically build and maintain knowledge graphs that evolve as new information is added, providing powerful tools for knowledge exploration and discovery. # Learning Assistant Example (/smartmemory/examples/learning-assistant) This example demonstrates how to build an intelligent learning assistant using SmartMemory that can track learning progress, provide personalized recommendations, and adapt to individual learning patterns. ## Overview [#overview] The Learning Assistant leverages SmartMemory's multiple memory types to create a comprehensive learning experience: * **Semantic Memory**: Store facts, concepts, and knowledge * **Episodic Memory**: Track learning sessions and experiences * **Procedural Memory**: Remember learning strategies and methods * **Pending Memory**: Maintain current learning context ## Implementation [#implementation] ### Basic Setup [#basic-setup] ```python from smartmemory import SmartMemory from datetime import datetime, timedelta, UTC class LearningAssistant: def __init__(self): self.memory = SmartMemory( config={ "evolution": { "consolidation": {"enabled": True}, "relationship_discovery": {"enabled": True} } } ) self.current_session = None def start_learning_session(self, topic, learning_goals=None): """Start a new learning session""" session_data = { "topic": topic, "start_time": datetime.now(UTC).isoformat(), "goals": learning_goals or [], "session_id": f"session_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}" } # Store session start in episodic memory self.memory.ingest({ "content": f"Started learning session on {topic}", "memory_type": "episodic", "metadata": session_data }) self.current_session = session_data return session_data["session_id"] ``` ### Knowledge Tracking [#knowledge-tracking] ```python import smartmemory.utils def learn_concept(self, concept, definition, examples=None): """Add a new concept to semantic memory""" concept_data = { "concept": concept, "definition": definition, "examples": examples or [], "learned_at": smartmemory.utils.now().isoformat(), "session_id": self.current_session["session_id"] if self.current_session else None } # Store in semantic memory memory_id = self.memory.ingest({ "content": f"{concept}: {definition}", "memory_type": "semantic", "metadata": concept_data }) return memory_id ``` ### Usage Example [#usage-example] ```python # Initialize the learning assistant assistant = LearningAssistant() # Start a learning session session_id = assistant.start_learning_session( topic="Machine Learning Fundamentals", learning_goals=["Understand supervised learning", "Learn about neural networks"] ) # Learn new concepts assistant.learn_concept( concept="Supervised Learning", definition="A type of machine learning where the algorithm learns from labeled training data", examples=["Classification", "Regression"] ) # Get learning progress progress = assistant.get_learning_progress(topic="Machine Learning", days=7) print(f"Concepts learned: {progress['concepts_learned']}") ``` ## Features [#features] * **Progress Tracking**: Monitor learning over time * **Knowledge Gaps**: Identify areas needing attention * **Personalized Recommendations**: Suggest optimal learning strategies * **Session Management**: Track individual learning sessions * **Concept Relationships**: Discover connections between topics This example shows how SmartMemory's multi-type memory system can create sophisticated learning applications that adapt to individual needs and learning patterns. # Quickstart for agents (/smartmemory/getting-started/agents) **Agent: do not work from a summary of this page.** Fetch tools that summarize (such as WebFetch in Claude Code) drop the exact commands, and setup then fails. If you can run shell commands, first download the full text with the command below, read all of it (do not cut it short with `head` or a byte limit), then follow that text from the top: ```bash curl -fsSL https://smartmemory.ai/claude.md ``` This page is written for an AI agent (Claude Code, Cursor, Codex, Claude Desktop). A person can start it with one line: ```text Set up SmartMemory for this project: read https://smartmemory.ai/claude and do what it says ``` ## If you are an agent, read this first [#if-you-are-an-agent-read-this-first] * This page is a set of steps for you to carry out now, not a document to summarize. Run the commands yourself, in order, until you reach the report at the end. Do not stop to describe the page or to ask the user which option they want. * Use the LOCAL branch. It is the default: free, no account, and memories stay on this machine. Use the HOSTED branch only when one of these is true: 1. the user asked for the hosted service, a SmartMemory account, or memory shared across machines or teammates 2. you cannot run shell commands (for example the Claude.ai chat or the Claude Desktop chat window) 3. Step 1 finds no Python 3.11, 3.12 or 3.13 on this machine * Do not switch to HOSTED because local setup downloads models or writes files outside the project. Both are expected and listed in Step 3. Mention them to the user in your progress notes and keep going, do not wait for an answer. * Two steps download a lot: installing the package (Step 2) and the first setup (Step 3). They take a few minutes on a fast connection and can take more than an hour on a slow one. Your tools may stop any command that runs longer than a few minutes, so this page starts both steps in the background with `nohup`, writes their output to a log file inside `.venv`, records the process number next to it, and gives you a short check command. Run the check again and again until it says the step is done. Run the start command and every check from the project folder, in the same order as on this page. A step that is still running is not a failure. Never kill it, and never start it a second time while it is still running. * The LOCAL branch has six steps: 1 check Python, 2 install, 3 setup, 4 register the MCP server, 5 add the memory instructions, 6 smoke test. You are done only when the search in Step 6 has returned the memory you added in Step 6. Never report a smoke test you did not run. * This page ends with the line `END OF SMARTMEMORY SETUP INSTRUCTIONS`. If the text you have does not reach that line, it was cut off: download it again and read the rest before you start. * When you finish, report back with the template in the last section. ## LOCAL branch [#local-branch] ### Step 1. Check Python [#step-1-check-python] SmartMemory needs Python 3.11, 3.12 or 3.13. Find one: ```bash python3.13 --version python3.12 --version python3.11 --version python3 --version ``` Use the first command that prints 3.11, 3.12 or 3.13, and write it where this page says `python3`. Prefer those three over a newer Python, because some dependencies publish ready-made wheels only for released versions they have tested. If none of them works, local setup is not possible on this machine yet: tell the user that it needs Python 3.12, and use the HOSTED branch unless they would rather install Python first. ### Step 2. Install the package (in the background) [#step-2-install-the-package-in-the-background] If the project already has a virtual environment (a `.venv` or `venv` folder with a `bin/python` inside) running Python 3.11 or newer, use it. If its folder is `venv`, write `venv` wherever this page says `.venv`. If the project has no virtual environment, create one in the project folder first: ```bash python3 -m venv .venv ``` If you created `.venv` and the project has a `.gitignore` that does not list it, add a `.venv/` line. Start the install in the background (both lines, together). Its output goes to `.venv/smartmemory-install.log`, and its last line will be `SMARTMEMORY-INSTALL-DONE` or `SMARTMEMORY-INSTALL-FAILED`: ```bash nohup sh -c '.venv/bin/python -m pip install --progress-bar off smartmemory && echo SMARTMEMORY-INSTALL-DONE || echo SMARTMEMORY-INSTALL-FAILED' > .venv/smartmemory-install.log 2>&1 & echo $! > .venv/smartmemory-install.pid ``` Then check on it. This command waits up to 90 seconds and prints the newest lines of the log: ```bash t=0 until grep -qE 'SMARTMEMORY-INSTALL-(DONE|FAILED)' .venv/smartmemory-install.log || [ "$t" -ge 90 ] do sleep 5 t=$((t + 5)) done tail -n 3 .venv/smartmemory-install.log kill -0 "$(cat .venv/smartmemory-install.pid)" 2>/dev/null && echo 'STILL INSTALLING: run this check again' ``` Read the output: * `SMARTMEMORY-INSTALL-DONE`: the install finished. Go on. * `STILL INSTALLING`: pip is still downloading (the log shows which package). Run the same check again, as many times as it takes. Do not start the install again. * `SMARTMEMORY-INSTALL-FAILED`: read the reason with `tail -n 30 .venv/smartmemory-install.log`. If it is a network error (a timeout, or a connection that was reset or refused), start the install again with the same `nohup` command, once. pip keeps the files it already downloaded. Any other error, or a second network failure, means stop and report FAILED at step 2 with those 30 lines. * Neither marker and no `STILL INSTALLING`: something outside pip stopped it. Start the install again with the same `nohup` command, then check again. From here on, `SM` means the `sm` program inside that environment, `.venv/bin/sm`. Check it: ```bash .venv/bin/sm --version ``` On Windows there is no `nohup`: run `.venv\Scripts\python.exe -m pip install smartmemory` directly with the longest timeout your tools allow, and use `.venv\Scripts\sm.exe` for `SM`. ### Step 3. Run setup in local mode (in the background) [#step-3-run-setup-in-local-mode-in-the-background] Setup needs no account and no API key. An LLM key (for example `GROQ_API_KEY` or `OPENAI_API_KEY`) is optional and only improves entity extraction. The first run downloads about 300 MB of models, so it also starts in the background (both lines, together) and logs to `.venv/smartmemory-setup.log`: ```bash nohup sh -c '.venv/bin/sm setup --mode local --yes && echo SMARTMEMORY-SETUP-DONE || echo SMARTMEMORY-SETUP-FAILED' > .venv/smartmemory-setup.log 2>&1 & echo $! > .venv/smartmemory-setup.pid ``` Check on it the same way: ```bash t=0 until grep -qE 'SMARTMEMORY-SETUP-(DONE|FAILED)' .venv/smartmemory-setup.log || [ "$t" -ge 90 ] do sleep 5 t=$((t + 5)) done tail -n 5 .venv/smartmemory-setup.log kill -0 "$(cat .venv/smartmemory-setup.pid)" 2>/dev/null && echo 'STILL SETTING UP: run this check again' ``` * `SMARTMEMORY-SETUP-DONE`: go on to Step 4. * `STILL SETTING UP`: it is downloading models or starting the background service. Run the same check again. Do not start setup again. * `SMARTMEMORY-SETUP-FAILED`: read `tail -n 30 .venv/smartmemory-setup.log`. If it says "SmartMemory daemon did not start or become ready", that a port "is used by another program", or that some checks failed, the background service is usually still loading its models. Do NOT run setup again and do NOT run `sm restart`, because both start the loading over. Instead run this check, up to 5 times, until it prints `SmartMemory daemon: ok`: ```bash t=0 until .venv/bin/sm status 2>&1 | grep -q 'SmartMemory daemon: ok' || [ "$t" -ge 90 ] do sleep 10 t=$((t + 10)) done .venv/bin/sm status ``` If it is still not ok after that, run `.venv/bin/sm doctor` and include its output in your report. Any other setup failure: stop and report FAILED at step 3 with the last 30 lines of the log. What setup changes outside the project, so you can tell the user: * memories are stored in `~/.smartmemory` and settings in `~/.config/smartmemory/config.toml` * it installs SmartMemory hooks and skills for Claude Code in `~/.claude` (automatic recall and capture in every project) * on macOS it adds a login item (launchd) that keeps a small local background service running * `sm uninstall` removes the hooks, skills, login item and settings ### Step 4. Register the MCP server for this project only [#step-4-register-the-mcp-server-for-this-project-only] Use PROJECT scope, so the server is recorded in this project and not in the user's global configuration. The command must be an absolute path. Claude Code (run from the project folder): ```bash claude mcp add --scope project smartmemory -e SMARTMEMORY_MCP_MODE=local -- "$PWD/.venv/bin/smartmemory-mcp" claude mcp get smartmemory ``` `claude mcp get` should show `Scope: Project config` and the path you gave. A status of "Pending approval" is expected: the user approves the server the next time Claude Code starts in this project, and the tools load in that new session, not in this one. If the `claude` command is not available, create `.mcp.json` in the project folder with this content, using the real absolute path: ```json { "mcpServers": { "smartmemory": { "type": "stdio", "command": "/absolute/path/to/project/.venv/bin/smartmemory-mcp", "args": [], "env": { "SMARTMEMORY_MCP_MODE": "local" } } } } ``` Cursor: create `.cursor/mcp.json` in the project folder with the same `mcpServers` block (Cursor reads project MCP servers from that file). Claude Desktop has no project scope. Its file is `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS and `%APPDATA%\Claude\claude_desktop_config.json` on Windows. Ask the user before you edit it, add the same `smartmemory` entry under `mcpServers`, and tell them to restart Claude Desktop. `.mcp.json` holds an absolute path that only works on this machine. Tell the user before they commit it. ### Step 5. Add the memory instructions to the project [#step-5-add-the-memory-instructions-to-the-project] Append this block to `CLAUDE.md` in the project root (Claude Code). For Cursor, Codex and other agents append it to `AGENTS.md`. Create the file if it does not exist. Do not add the block twice. ```markdown ## Memory (SmartMemory) This project has long-term memory through the `smartmemory` MCP server. - At the start of a task, and before answering a question about earlier decisions or conventions, call `memory_search` with a short plain-language query. - To read one result in full, call `memory_get` with its item id. - After a decision, a fix whose cause was not obvious, or a convention the user states, call `memory_ingest` with one or two plain sentences that say what was decided and why. Never store secrets, keys, passwords or personal data. ``` ### Step 6. Smoke test [#step-6-smoke-test] Add one true memory and recall it. Run the lines exactly as written, from the project folder: the shell fills in the folder name and today's date, so do not edit, shorten or retype them. ```bash .venv/bin/sm add "SmartMemory was set up for the $(basename "$PWD") project on $(date +%Y-%m-%d) by an AI agent following smartmemory.ai/claude" .venv/bin/sm search "when was SmartMemory set up for this project" ``` The search output must contain the sentence you added. If `sm add` reports a timeout, run the search before you add again, because the add usually completed. If the search does not return it, run `.venv/bin/sm status` and `.venv/bin/sm doctor` and include both outputs in your report. ## HOSTED branch [#hosted-branch] Use this branch only in the three cases listed at the top of this page: the user asked for the managed service (memory shared across machines and teammates), you cannot run shell commands, or this machine has no Python 3.11 to 3.13. It ends with ONE step only the user can do: approve the sign-in in a browser. Claude Code: ```bash claude mcp add --scope project --transport http smartmemory https://mcp.smartmemory.ai/mcp ``` Cursor: add this to `.cursor/mcp.json` in the project folder. ```json { "mcpServers": { "smartmemory": { "url": "https://mcp.smartmemory.ai/mcp" } } } ``` Claude.ai or Claude Desktop: tell the user to add a custom connector with the URL `https://mcp.smartmemory.ai/mcp` in the connector settings. Then tell the user: "Restart your agent in this project. The first SmartMemory tool call opens a browser tab. Sign in or create a free account there and approve the connection." That click is the only human step. Add the Step 5 memory instructions to the project as well. Facts for the user: * The Free plan has 10,000 memories, 200 queries a day, 1 indexed code repository and 1 seat. Current figures: [https://www.smartmemory.ai/pricing](https://www.smartmemory.ai/pricing) * The hosted MCP server exposes 25 tools and signs in with OAuth. Details: [https://docs.smartmemory.ai/smartmemory/guides/hosted-mcp/](https://docs.smartmemory.ai/smartmemory/guides/hosted-mcp/) * REST API base URL: `https://api.smartmemory.ai`. Authenticate with `X-API-Key: YOUR_API_KEY` (an API key from the SmartMemory web app) or `Authorization: Bearer YOUR_SESSION_TOKEN`. Most routes also need `X-Workspace-Id`. Header reference: [https://docs.smartmemory.ai/smartmemory/guides/authentication-and-tenancy/](https://docs.smartmemory.ai/smartmemory/guides/authentication-and-tenancy/) * OpenAPI schema: [https://api.smartmemory.ai/openapi.json](https://api.smartmemory.ai/openapi.json) * When SmartMemory is the wrong tool: [https://docs.smartmemory.ai/smartmemory/getting-started/when-not-to-use/](https://docs.smartmemory.ai/smartmemory/getting-started/when-not-to-use/) ## Report back to the user [#report-back-to-the-user] End with a short report in exactly this shape. Copy the lines as plain text inside a code block, keep each label exactly as written (no bold, no bullets, no renamed labels), and fill in the parts in angle brackets. People and tools read these lines. You may add notes below the block. ```text SmartMemory setup: DONE (local) | DONE (hosted, waiting for your sign-in) | FAILED at step N Installed: smartmemory in MCP: registered for this project in . Restart your agent here and approve "smartmemory" to load the tools. Instructions: added to Smoke test: | Changed outside this project: Next: ``` END OF SMARTMEMORY SETUP INSTRUCTIONS # Configuration (/smartmemory/getting-started/configuration) SmartMemory consumer setup has two modes. Most users should choose one with `sm setup` (the `sm` CLI is the short form of the `smartmemory` command), not hand-wire a graph database. ## Lite or Service [#lite-or-service] Lite runs entirely on your machine and Service connects to the managed backend. For the full comparison and how to pick one, see [Lite or Service](/smartmemory/getting-started/installation/#lite-or-service) on the Installation page. ```bash # Local Lite mode sm setup # Managed Service mode sm setup --mode remote ``` For non-interactive Service setup, paste a key from the [SmartMemory dashboard](https://www.smartmemory.ai/dashboard) (Dashboard, then Settings, then API Keys): ```bash sm setup --mode remote --api-key sk_... ``` ## Consumer Environment Variables [#consumer-environment-variables] Service mode can also be configured with environment variables: ```bash export SMARTMEMORY_API_URL="https://api.smartmemory.ai" export SMARTMEMORY_API_KEY="sm_live_..." ``` Lite mode does not require any database, Docker service, or API key. ## LLM Configuration [#llm-configuration] ### OpenAI [#openai] ```json { "llm": { "provider": "openai", "model": "gpt-4", "api_key": "${OPENAI_API_KEY}", "temperature": 0.7, "max_tokens": 2000, "timeout": 30 } } ``` ### Azure OpenAI [#azure-openai] ```json { "llm": { "provider": "azure_openai", "api_key": "${AZURE_OPENAI_API_KEY}", "api_base": "${AZURE_OPENAI_ENDPOINT}", "api_version": "2023-05-15", "deployment_name": "gpt-4" } } ``` ### Anthropic Claude [#anthropic-claude] ```json { "llm": { "provider": "anthropic", "model": "claude-3-opus-20240229", "api_key": "${ANTHROPIC_API_KEY}", "max_tokens": 2000 } } ``` ## Extraction Configuration [#extraction-configuration] ### Entity and Relationship Extraction [#entity-and-relationship-extraction] ```json { "extraction": { "spacy_model": "en_core_web_sm", "enable_entity_extraction": true, "enable_relationship_extraction": true, "entity_types": [ "PERSON", "ORG", "GPE", "DATE", "TIME", "MONEY", "PRODUCT" ], "custom_patterns": [ { "label": "SKILL", "pattern": [{"LOWER": {"IN": ["python", "javascript", "sql"]}}] } ] } } ``` ### Advanced Extraction Settings [#advanced-extraction-settings] ```json { "extraction": { "use_llm_extraction": true, "llm_extraction_prompt": "Extract entities and relationships from: {text}", "confidence_threshold": 0.8, "max_entities_per_item": 20, "enable_coreference_resolution": true } } ``` ## Background Processing [#background-processing] ### Basic Configuration [#basic-configuration] ```json { "background": { "enabled": true, "max_workers": 3, "queue_size": 1000, "batch_size": 10, "processing_interval": 5.0 } } ``` ### Advanced Processing Options [#advanced-processing-options] ```json { "background": { "enabled": true, "max_workers": 5, "queue_size": 2000, "batch_size": 20, "processing_interval": 2.0, "retry_attempts": 3, "retry_delay": 1.0, "enable_priority_queue": true, "high_priority_types": ["episodic", "procedural"] } } ``` ## Similarity Metrics [#similarity-metrics] ### Weight Configuration [#weight-configuration] ```json { "similarity": { "semantic_weight": 0.4, "content_weight": 0.3, "temporal_weight": 0.2, "metadata_weight": 0.1, "enable_adaptive_weighting": true } } ``` ### Advanced Similarity Settings [#advanced-similarity-settings] ```json { "similarity": { "semantic_model": "all-MiniLM-L6-v2", "content_similarity_method": "jaccard", "temporal_decay_factor": 0.1, "metadata_fields": ["memory_type", "user_id", "tags"], "similarity_threshold": 0.3 } } ``` ## Evolution Algorithms [#evolution-algorithms] The optional keys `maximal_connectivity`, `rapid_enrichment`, `strategic_pruning`, and `hierarchical_organization` select the **experimental agent-optimized suite**. They map to the `MaximalConnectivityEvolver`, `RapidEnrichmentEvolver`, `StrategicPruningEvolver`, and `HierarchicalOrganizationEvolver` classes that live in `service_common/plugins/evolvers/optimized/` and are registered under those snake-case keys. They run aggressive connectivity, enrichment, pruning, and organization passes intended for agent workloads. The core shipped evolvers in `smartmemory/plugins/evolvers/`: | Evolver | Purpose | | -------------------------------- | ------------------------------------------------------------------ | | `EpisodicToSemanticEvolver` | Promote stable episodic facts to semantic | | `EpisodicToZettelEvolver` | Promote significant episodic memories to atomic Zettelkasten notes | | `EpisodicDecayEvolver` | Decay confidence on aging episodic items | | `SemanticDecayEvolver` | Decay confidence on aging semantic items | | `SemanticToProceduralEvolver` | Promote repeated semantic patterns to procedures | | `MemoryConsolidationEvolver` | Consolidate near-duplicate items | | `OpinionSynthesisEvolver` | Synthesize opinion items from supporting evidence | | `OpinionReinforcementEvolver` | Reinforce / weaken opinions on new evidence | | `ObservationSynthesisEvolver` | Build entity observations from scattered facts | | `DecisionConfidenceEvolver` | Update decision confidence from supporting/contradicting items | | `ProceduralReinforcementEvolver` | Reinforce procedures on successful re-use | | `ZettelPruneEvolver` | Prune low-value or orphaned Zettelkasten notes | | `StaleMemoryEvolver` | Mark items stale when valid\_end\_time has passed | | `AnchorReconciliationEvolver` | Reconcile session anchors with the live graph | | `ResolutionChainEvolver` | Track resolution chains for follow-up questions | | `QAHeuristicEvolver` | Heuristic Q\&A pair detection for episodic→semantic promotion | Configure via the `evolution` block in your config: ```json { "evolution": { "enabled": true, "interval_seconds": 3600 } } ``` Per-evolver tuning lives on the evolver's `Config` model (e.g. `EpisodicDecayConfig`, `OpinionSynthesisConfig`). See the source files under `smartmemory/plugins/evolvers/` for the authoritative parameter list. ## Ontology Configuration [#ontology-configuration] ### Basic Ontology Settings [#basic-ontology-settings] ```json { "ontology": { "enabled": true, "storage_backend": "FileSystemOntologyStorage", "storage_path": "./ontologies", "default_ontology": "general_knowledge" } } ``` ### Advanced Ontology Management [#advanced-ontology-management] ```json { "ontology": { "enabled": true, "auto_inference": true, "inference_threshold": 0.7, "enable_hitl_validation": true, "validation_rules": [ "entity_type_consistency", "relationship_constraints", "domain_validation" ] } } ``` ## Performance Tuning [#performance-tuning] ### High-Performance Configuration [#high-performance-configuration] ```json { "performance": { "enable_caching": true, "cache_size": 10000, "cache_ttl": 3600, "enable_batch_operations": true, "batch_size": 100, "connection_pool_size": 10, "query_timeout": 30 } } ``` ### Memory Optimization [#memory-optimization] ```json { "memory_optimization": { "enable_lazy_loading": true, "max_memory_usage_mb": 2048, "garbage_collection_interval": 300, "enable_compression": true, "compression_algorithm": "gzip" } } ``` ## Server Configuration [#server-configuration] Lite and managed Service users do not configure or run server storage backends. Server configuration is provided to Enterprise BYOC customers during onboarding. ## Configuration Loading [#configuration-loading] ### Programmatic Configuration [#programmatic-configuration] ```python from smartmemory import SmartMemory from smartmemory.configuration import MemoryConfig from smartmemory.utils import get_config # Load from file (recommended via environment variable SMARTMEMORY_CONFIG) cfg = MemoryConfig(config_path="config.json") cfg.validate() # SmartMemory reads configuration via the configuration subsystem memory = SmartMemory() # Access configuration at runtime vector_cfg = get_config('vector') print(vector_cfg.get('backend')) ``` ### Runtime Configuration Updates [#runtime-configuration-updates] ```python # Apply runtime config changes by editing the file, then either: from smartmemory.configuration import MemoryConfig cfg = MemoryConfig(config_path="config.json") cfg.reload_if_stale(force=True) # Or clear the cached config so subsequent get_config() calls reload from smartmemory.utils import get_config, clear_config_cache clear_config_cache() current_config = get_config() print(current_config.get('similarity')) ``` ## Environment Variables [#environment-variables] ### Required at Runtime [#required-at-runtime] ```bash # LLM API keys (required only for LLM-backed extraction) export OPENAI_API_KEY="your-openai-api-key" export ANTHROPIC_API_KEY="your-anthropic-api-key" # Service mode credentials export SMARTMEMORY_API_KEY="sm_live_..." export SMARTMEMORY_API_URL="https://api.smartmemory.ai" ``` ### `SMARTMEMORY_*`: Service mode clients [#smartmemory_-service-mode-clients] Read by the `smartmemory` package in Service mode. The Python client (`smartmemory-client`) and the MCP server also read `SMARTMEMORY_API_KEY`, `SMARTMEMORY_WORKSPACE_ID` and `SMARTMEMORY_TEAM_ID`. The Python client takes the API URL as its `base_url` argument. | Variable | Purpose | Example | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | | `SMARTMEMORY_API_URL` | Base URL of the SmartMemory hosted API. Overrides the `mode=remote` config default. | `https://api.smartmemory.ai` | | `SMARTMEMORY_API_KEY` | Bearer token for API auth. | `sm_live_...` | | `SMARTMEMORY_WORKSPACE_ID` | Workspace sent as the `X-Workspace-Id` header by the Python client and the MCP server. Wins over `SMARTMEMORY_TEAM_ID` when both are set. | UUID | | `SMARTMEMORY_TEAM_ID` | Older name for the workspace, still supported. The `smartmemory` CLI uses it for remote commands. | UUID | ### Debug / Logging [#debug--logging] ```bash # Enable verbose debug logging (read by smartmemory-core) export SMARTMEMORY_DEBUG="true" ``` ## Configuration Validation [#configuration-validation] SmartMemory automatically validates configuration on startup: ```python from smartmemory.configuration import MemoryConfig # Validate configuration file cfg = MemoryConfig(config_path="config.json") cfg.validate() ``` ## Best Practices [#best-practices] 1. **Use environment variables** for sensitive information like API keys 2. **Use Lite** when you want local, zero-infrastructure memory 3. **Use Service** when you want managed remote memory with nothing to operate 4. **Separate configurations** for different environments (dev, staging, prod) 5. **Enable background processing** in backend deployments for better performance 6. **Configure appropriate worker counts** based on your hardware 7. **Monitor resource usage** and adjust configuration accordingly ## Troubleshooting [#troubleshooting] ### Common Configuration Issues [#common-configuration-issues] 1. **Invalid JSON syntax** - Use a JSON validator to check your configuration 2. **Missing environment variables** - Ensure all required variables are set 3. **Service connection failures** - Verify `SMARTMEMORY_API_KEY` and `SMARTMEMORY_API_URL` 4. **Performance issues** - Adjust worker counts and batch sizes 5. **Memory usage** - Configure memory limits and garbage collection ### Configuration Debugging [#configuration-debugging] ```python # Enable debug logging import logging logging.basicConfig(level=logging.DEBUG) # Validate and inspect configuration from smartmemory.configuration import MemoryConfig cfg = MemoryConfig(config_path="config.json") cfg.validate() print(cfg.graph_db) ``` # Installation (/smartmemory/getting-started/installation) This guide helps you install SmartMemory as a package and choose where memory runs. ## Lite or Service [#lite-or-service] * **Lite** (the default): storage and search run on your machine with a SQLite graph and local usearch vectors. No Docker, database service or account is needed for local use. Optional LLM extraction through a hosted provider such as Groq calls that provider's service. * **Service**: connect to the managed SmartMemory backend instead of running storage locally. Run `smartmemory setup --mode remote` and paste an API key from the [SmartMemory dashboard](https://www.smartmemory.ai/dashboard) (Dashboard, then Settings, then API Keys). See [pricing](https://www.smartmemory.ai/pricing) for current plans and access options. You choose Lite or Service at `smartmemory setup` time, not at install time, and you can switch later by re-running setup. If client data must stay inside your network, read [Regulated and on-prem teams](/smartmemory/architecture/system-overview/#regulated-and-on-prem-teams) first. ## Requirements [#requirements] * Python 3.11 or higher * `pip` or a dedicated conda environment * No Docker or database service for normal Lite or Service use ## Install the Package [#install-the-package] Always install SmartMemory into a fresh, isolated environment, a virtualenv or a dedicated conda env, never into a shared or system environment. ```bash # venv (recommended) python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -U smartmemory ``` ```bash # or a dedicated conda env conda create -n smartmemory python=3.11 conda activate smartmemory pip install -U smartmemory ``` > **Check your active conda environment.** Run `conda info --envs`. The `*` marks the active environment. If `base` is active, create and activate a dedicated environment using the commands above. If your dedicated environment is already active, continue with setup. > **One store per environment.** Every environment uses the same local store, `~/.smartmemory`, and the same daemon port, 9014, unless you point it elsewhere. If more than one environment has SmartMemory installed (for example two conda envs on different versions), give each its own store and daemon port so two versions never write to one store. In conda, set them on the environment and reactivate it: > > ```bash > conda env config vars set SMARTMEMORY_DATA_DIR="$HOME/.smartmemory-myenv" SMARTMEMORY_DAEMON_PORT=9020 > conda activate myenv > ``` > > In a venv, export the same two variables in its `activate` script. The first `smartmemory` command in a project may ask whether to use these inherited settings: choose use. When `smartmemory` opens a store that a different version used last, it prints a warning that names both versions. ## Choose Lite [#choose-lite] Lite is the default local mode. It uses local SQLite storage and embedded vectors with zero infrastructure. ```bash smartmemory setup ``` Choose local storage. Groq is the recommended hosted LLM provider and the default at the LLM provider prompt. For local use without an LLM key, choose `none` at that prompt and keep the `local` embedding provider. ## Choose Service [#choose-service] Service mode connects the SDK and tools to the managed SmartMemory backend. ```bash smartmemory setup --mode remote ``` Paste an API key from the [SmartMemory dashboard](https://www.smartmemory.ai/dashboard) when prompted. Keys live under Dashboard, then Settings, then API Keys. See [pricing](https://www.smartmemory.ai/pricing) for current plans and API access options. There is no database or server to run locally. For non-interactive setup: ```bash smartmemory setup --mode remote --api-key sk_... ``` ## Verify Installation [#verify-installation] After local setup has downloaded the spaCy and embedding models, verify Lite storage and search with this no-key Python example. It reads setup's configured data directory and spaCy model, while keeping embeddings local and LLM extraction disabled. The Lite context selects SQLite and closes it when the example finishes. ```python from smartmemory.pipeline.config import PipelineConfig from smartmemory.tools.factory import lite_context from smartmemory_app.config import load_config config = load_config() pipeline = PipelineConfig.lite(llm_enabled=False) pipeline.extraction.entity_ruler.spacy_model = config.spacy_model with lite_context(data_dir=config.data_dir, pipeline_profile=pipeline) as memory: item_id = memory.ingest("Hello, SmartMemory!") results = memory.search("Hello") print(f"Stored {item_id}") print(f"Found {len(results)} memories") ``` On a fresh local store, this prints the stored memory ID and `Found 1 memories`. ## Optional LLM Keys [#optional-llm-keys] SmartMemory can store and search explicit memories without an LLM key. Choose a provider when you want LLM-backed extraction. **Groq is the recommended hosted LLM provider.** Re-run `sm setup`, choose local storage, and select `groq`. Setup prompts for `GROQ_API_KEY` and lets you skip the key. The local setup menu offers these LLM providers: | Setup choice | Authentication or runtime | | -------------- | ------------------------------------------------------- | | `groq` | Groq API key (`GROQ_API_KEY`), recommended | | `claude-agent` | Claude Agent SDK with OAuth, no API key | | `anthropic` | Anthropic API key (`ANTHROPIC_API_KEY`) | | `openai` | OpenAI API key (`OPENAI_API_KEY`) | | `gemini` | Google Gemini API key (`GEMINI_API_KEY`) | | `ollama` | Local Ollama runtime | | `lmstudio` | Local LM Studio endpoint compatible with the OpenAI API | | `none` | No LLM, local ruler extraction only | Embedding selection is separate. Keep `local` for embeddings without a provider key. Setup also offers `openai` and `ollama` embeddings. ## Install from Source [#install-from-source] Use a source install only when contributing to SmartMemory itself: ```bash git clone https://git.smartmemory.ai/smartmemory/smart-memory.git cd smart-memory python -m venv .venv source .venv/bin/activate pip install -e . ``` ## Troubleshooting [#troubleshooting] ### Command Not Found [#command-not-found] If `smartmemory` is not available after install, confirm that your virtual environment is active: ```bash which python which smartmemory python -m pip show smartmemory ``` ### Service Authentication Error [#service-authentication-error] For Service mode, confirm that setup stored the right API key or export it explicitly: ```bash export SMARTMEMORY_API_KEY="sm_live_..." export SMARTMEMORY_API_URL="https://api.smartmemory.ai" ``` ### Lite Storage Path Error [#lite-storage-path-error] Lite writes local data under the configured data directory. If startup fails, check that the directory exists and is writable. ### LLM Provider Key Error [#llm-provider-key-error] Re-run `sm setup` and check the selected provider and its key prompt. For the recommended Groq provider, use `GROQ_API_KEY`. To continue without an LLM key, choose `none` and keep local embeddings. Explicit memory storage and search work without LLM extraction. ## Next Steps [#next-steps] * [Quick Start Guide](/smartmemory/getting-started/quickstart/) - Build your first application * [Configuration Guide](/smartmemory/getting-started/configuration/) - Detailed configuration options * [Basic Usage](/smartmemory/guides/basic-usage/) - Learn the core APIs # Local mode (/smartmemory/getting-started/local-mode) Local mode keeps your memories on your own computer. There is no account, no server to run and no Docker. Search works offline once setup has downloaded its models. This page covers how to turn it on, what stays on the machine, and what changes if you later move to an account. If you want the same memory shared across several agents and machines, use a [SmartMemory account](/smartmemory/getting-started/installation/#choose-service) instead. ## Set it up [#set-it-up] Install the package into a fresh environment (see [Installation](/smartmemory/getting-started/installation/)), then run setup in local mode: ```bash sm setup --mode local ``` `sm` and `smartmemory` are the same command. Setup asks a few questions and downloads the language and embedding models it needs. To accept the default answer to every question, add `--yes`: ```bash sm setup --mode local --yes ``` If a run is interrupted, `sm setup --restart` starts over instead of resuming. To check the result at any time, run `sm doctor`. For the LLM provider and embedding choices that setup offers, see [Installation](/smartmemory/getting-started/installation/#choose-lite). Choose `none` at the LLM provider prompt and keep the `local` embedding provider to run with no API key at all. ## What stays on the machine [#what-stays-on-the-machine] Everything SmartMemory stores in local mode lives in one data directory, `~/.smartmemory` by default. You can pick another path during setup. | Stays on your machine | Notes | | ------------------------------------------- | ------------------------------------------------------------ | | Your memories and the graph built from them | A SQLite database in the data directory | | The search index | Local vector files in the data directory | | Your configuration | No account or API key is created or needed | | Local usage records | Written to a file in the data directory, never sent anywhere | Search and storage do not need the network. Three things can still reach the internet, and each one is optional or switchable: * **An LLM provider, if you choose one.** If you pick a hosted provider such as Groq for extraction, the text being extracted is sent to that provider. Choose `none` to avoid this. * **The first-run model download.** Setup fetches the language and embedding models once. After that they are cached locally. * **Update checks and crash reports.** The CLI checks PyPI for a newer version at most once a day. Set `SMARTMEMORY_NO_UPDATE_CHECK=1` to turn that off. Anonymous crash and support reports can be turned off with `SMARTMEMORY_CRASH_REPORTS=0`. ## Limits of local mode [#limits-of-local-mode] Local mode is one person on one machine. Memories are not shared with other machines or agents. The extraction quality without an LLM is lower than with one, because only pattern-based and spaCy extraction run. The developer-facing details, including which pipeline stages change, are in [Lite Mode](/smartmemory/guides/lite-mode/). ## Move to an account later [#move-to-an-account-later] You can switch at any time by running setup in service mode: ```bash sm setup --mode remote ``` Paste an API key from the [SmartMemory dashboard](https://www.smartmemory.ai/dashboard) when prompted. See [Installation](/smartmemory/getting-started/installation/#choose-service) for the non-interactive form. What this does and does not do: * It points the CLI and your tools at the hosted service. New memories go to your account. * It does **not** upload the memories you already have. They stay in your local data directory, untouched. * While you are in service mode, the local-only commands `sm export` and `sm import` are unavailable. If you want a portable copy of your local memories, run `sm export ` before you switch. It writes an OKF bundle, a folder of Markdown pages you can keep. To go back, run `sm setup --mode local` again. ## Remove local data [#remove-local-data] `sm uninstall` removes the hooks, configuration and, by default, the data directory itself, which is the only copy of your local memories. To remove everything except your memories, run: ```bash sm uninstall --keep-data ``` Before an uninstall without `--keep-data`, make an `sm export` bundle or a copy of `~/.smartmemory`. # Quickstart (/smartmemory/getting-started/quickstart) SmartMemory gives your AI assistant a memory that survives across sessions. You capture the decisions, preferences, and hard-won lessons from your work, and the relevant parts come back automatically next time. It runs on your machine by default, with no Docker and no database to install. This guide takes you from install to a working memory in about two minutes, then shows you the recall patterns a flat notes file cannot do. SmartMemory is built for memory that changes, one item at a time. Loading a whole table or a static corpus is not what it is for, see [When not to use SmartMemory](/smartmemory/getting-started/when-not-to-use/). ## Install [#install] ```bash pip install smartmemory smartmemory setup ``` `setup` asks a couple of questions with sensible defaults and wires SmartMemory into Claude Code. Using Cursor instead? Run `smartmemory setup --for cursor`. This is **Lite mode**, the local default: a SQLite graph plus embedded vectors, no external services, no account. If you would rather point at the managed backend, run `smartmemory setup --mode remote` and paste an API key from the [SmartMemory dashboard](https://www.smartmemory.ai/dashboard) (Dashboard, then Settings, then API Keys). Either way you can switch later by re-running setup. See [Installation](/smartmemory/getting-started/installation/) for the full matrix and troubleshooting. ## Your first memories [#your-first-memories] Add a few things worth remembering, then ask for them back in plain language: ```bash sm add "We chose Postgres over MongoDB because of the reporting queries" sm add "Deploys go through GitHub Actions, never manual" sm search "which database did we pick" ``` `sm` is the short alias for `smartmemory`, and either name works. That last search returns the Postgres memory even though none of its words appear in the query. `sm search` matches by meaning, not keywords, so you can ask a question instead of guessing the exact phrase you saved. A note on quotes: your shell expands `$` inside double quotes, so `$48` silently becomes empty. Wrap text that contains a dollar sign in single quotes: ```bash sm add "The plan cost was $48 against $19" # stores: The plan cost was against sm add 'The plan cost was $48 against $19' # stores: The plan cost was $48 against $19 ``` New memories land as `episodic` (things that happened) unless you say otherwise. Reach for a type when it helps you filter later: ```bash sm add --type procedural "Before a release, run the full test suite and update the changelog" sm add --project atlas "Atlas v2 ships on the 15th" sm search --project atlas "when do we ship" ``` ## See it prove itself [#see-it-prove-itself] Run the guided tour: ```bash sm tour ``` The tour spins up an **isolated** local store (it never touches your real memories), opens the graph viewer in your browser, seeds a few project facts, searches them, and then shows the moment that matters: a real token receipt and cross-session recall, side by side. Here is the idea the receipt makes concrete. A memory that you paste into context in full keeps growing as your project does. SmartMemory instead recalls only the memories relevant to what you are doing right now, so the context it feeds your assistant stays small even as your stored memory grows into thousands of items. The tour puts both counts on screen with a real tokenizer so you can see the gap yourself rather than take it on faith. Add `--no-viewer` for headless runs, or `--keep` if you want to inspect the temporary store afterward. ## What you get from here [#what-you-get-from-here] * **Your assistant remembers across sessions.** Setup installs hooks that capture context while you work and recall the relevant parts when the next session starts, so you stop re-explaining your project every morning. See [MCP integration](/smartmemory/guides/mcp-integration/). * **A knowledge graph you can see.** Everything you add gets linked into a graph of the people, projects, and concepts inside your memories. Open it with `sm viewer`, or read [Graph viewer](/smartmemory/guides/graph-viewer/). * **The same memory in Python.** Everything the CLI does is available from code, which is where the deeper recall patterns below live. ## Use it from Python [#use-it-from-python] ```python from smartmemory.tools.factory import create_lite_memory, lite_context # Simple usage. Full LLM extraction runs if OPENAI_API_KEY is set. memory = create_lite_memory() item_id = memory.ingest("Alice leads Project Atlas") results = memory.search("who leads Atlas", top_k=5) # Preferred in scripts: cleans up globals and closes SQLite on exit. with lite_context() as memory: item_id = memory.ingest("Alice leads Project Atlas") results = memory.search("who leads Atlas") ``` SmartMemory does not just save text. It extracts the people, projects, and concepts inside each memory and links them into the graph, which is why search can answer questions and why the viewer has something to draw. See [Lite mode](/smartmemory/guides/lite-mode/) for the local Python surface in full. ## Advanced recall in practice [#advanced-recall-in-practice] The steps above are capture and simple search. These are the patterns a flat memory file cannot do. Every example runs in local Lite mode with no API key. Shared setup: ```python from smartmemory.pipeline.config import PipelineConfig from smartmemory.tools.factory import lite_context from smartmemory.models.memory_item import MemoryItem lite = dict(pipeline_profile=PipelineConfig.lite(llm_enabled=False)) ``` **Decisions, including what you decided *not* to do.** A decision is a first-class object that keeps the rejected alternatives and the reasoning attached, so you can recall the road not taken. ```python with lite_context(**lite) as m: d = m.add_decision( "Use SQLite for local lite storage", rejected_alternatives=["Postgres sidecar", "remote-only cloud sync"], rationale="Zero-infra installs must work offline", ) print(m.get_decision(d.decision_id).rejected_alternatives) # -> ['Postgres sidecar', 'remote-only cloud sync'] hits = m.search("what did we decide NOT to do for storage", expertise=True) print([i.content for i in hits["decision"]]) # -> ['Use SQLite for local lite storage'] ``` `search(expertise=True)` returns a dict keyed by expertise type (`decision`, `constraint`, `learned`, and so on), not a flat list. **Truth maintenance: supersede a fact, and only the current one comes back.** ```python with lite_context(**lite) as m: old = m.add(MemoryItem(content="The incident channel is #ops-old", memory_type="semantic")) new = m.add(MemoryItem(content="The incident channel is #ops-war-room", memory_type="semantic")) m.supersede(old, new, reason="Team renamed the channel") print([r.content for r in m.search("incident channel")]) # -> ['The incident channel is #ops-war-room'] (current only) print([r.content for r in m.search("incident channel", include_superseded=True)]) # -> ['The incident channel is #ops-old', 'The incident channel is #ops-war-room'] (full history) ``` **Bi-temporal recall: what did we believe *before* it changed.** Supersession keeps the full history, and SmartMemory also records *when* each fact was true, so you can ask a memory as of a past moment and get the belief you held then rather than the current one. A markdown file cannot answer this: once you edit the line, the old value is gone. The point-in-time query depends on the storage backend preserving each record's time, so see [Bi-temporal versioning](/smartmemory/concepts/bi-temporal-versioning/) for how `as_of_date` and `reference_time` work and which backends resolve them. **Constraints and lessons, recalled as expertise.** ```python with lite_context(**lite) as m: m.add_constraint("Production deploys must use GitHub Actions", domain="release") m.add_learning("SQLite WAL avoids writer stalls under concurrent tests") hits = m.search("What guidance do we have for production deploys and SQLite?", expertise=True) print([i.content for i in hits["constraint"]]) # -> ['Production deploys must use GitHub Actions'] print([i.content for i in hits["learned"]]) # -> ['SQLite WAL avoids writer stalls under concurrent tests'] ``` **Multi-hop retrieval** chains results so each hop informs the next query, reaching facts a single lookup would miss. It pays off once your graph is rich (`semantic_hops=True` adds LLM-planned hops): ```python with lite_context(**lite) as m: hits = m.search("Redis migration fallout", multi_hop=True, max_hops=3) ``` **Code intelligence: index a repo, then search it by meaning.** `sm code index` builds an AST and call graph into memory. Recall it with `search_code`, which is semantic, so it finds code that shares no keywords with your query (a plain `grep` would miss it): ```bash sm code index ./my-project --repo my-project ``` ```python with lite_context(**lite) as m: # "clean up vendor billing records" matches a function actually named # reconcile_widget_invoice (no shared keywords, found by meaning). for hit in m.search_code("clean up vendor billing records", repo="my-project"): print(hit["name"], hit["entity_type"], f'{hit["file_path"]}:{hit["line_number"]}') ``` ## Next steps [#next-steps] * [Basic usage](/smartmemory/guides/basic-usage/) - the core APIs in depth * [Configuration](/smartmemory/getting-started/configuration/) - customize SmartMemory behavior * [Expertise vs knowledge](/smartmemory/concepts/expertise-vs-knowledge/) - when to use decisions, constraints, and lessons * [Bi-temporal versioning](/smartmemory/concepts/bi-temporal-versioning/) - how supersession and as-of recall work * [Learning assistant](/smartmemory/examples/learning-assistant/) and [Conversational AI](/smartmemory/examples/conversational-ai/) - fuller worked examples # When not to use SmartMemory (/smartmemory/getting-started/when-not-to-use) SmartMemory is built for memory that changes: things an assistant or an agent learns one at a time, links to what it already knows, and corrects later. It is not a bulk loader. If your job looks like one of the cases below, use the vector store or database you already run. ## Bulk catalogs reloaded nightly [#bulk-catalogs-reloaded-nightly] Every `sm add` or `memory.ingest()` call runs the full pipeline for one item: classify, extract, enrich, link and store. In our own timing with no LLM provider configured, a single `sm add` took about 210 ms. At that rate 2 million rows take about 117 hours on one process. The Python `add()` call stores an item without extraction, linking or enrichment, but then you get little more than a vector store with a local embedding model, which is what you may already run. A catalog that is replaced in place every night has no changing beliefs to supersede and no decisions to record. Your database is the source of truth, so keep it there. ## Static corpora [#static-corpora] A fixed set of documents that you index once and query many times does not need a memory layer. Memory earns its place when facts arrive over time, contradict each other, and need to be traced back to where they came from. For a static corpus, a plain vector index is simpler and cheaper. ## A single reader [#a-single-reader] If one chatbot reads one table, there is nobody to share memory with and nothing to carry between sessions. SmartMemory pays off when several people, agents or sessions need the same memory, or when one assistant must remember across days. ## Distributed workers that must share one store [#distributed-workers-that-must-share-one-store] Lite runs on one machine and has no shared store for many workers. For a shared store, use the managed Service. The self-serve plans have memory limits: Free is 10,000 memories, Pro is 100,000, and Team is 100,000 per seat, pooled. See [pricing](https://www.smartmemory.ai/pricing) for the current figures. A workload far above those limits is a conversation about Enterprise terms, not a self-serve fit. ## Where it does fit [#where-it-does-fit] Use SmartMemory when an assistant should remember decisions and preferences across sessions, when agents share what they learn, or when you need to ask why something is believed. Start with the [Quickstart](/smartmemory/getting-started/quickstart/). # Auditable Memory (/smartmemory/guides/auditable-memory) # Auditable Memory [#auditable-memory] SmartMemory can answer the question every agent eventually gets asked: "how do you know that?" Three surfaces work together. As-of recall returns what the system believed at a past instant. Explain returns the full provenance of one memory in a single call. Verify proves the history has not been rewritten. ## As-of recall [#as-of-recall] Every search can travel transaction time. Pass `as_of_date` and results reflect the belief the system held at that instant, not the belief it holds now. Pass `include_superseded` to keep replaced items visible. ```python from smartmemory_client import SmartMemoryClient client = SmartMemoryClient(base_url="...", team_id="...", api_key="...") # What did we believe on July 1st? results = client.search( "database migration plan", as_of_date="2026-07-01T00:00:00+00:00", ) # Include items that have since been replaced results = client.search("auth approach", include_superseded=True) ``` The JavaScript SDK mirrors this with `asOfDate` and `includeSuperseded`. The MCP `memory_search` tool accepts the same parameters, so agents can time travel directly. An as-of query never widens the tenant boundary. Time travel changes when, never who. ### When a result cannot be resolved [#when-a-result-cannot-be-resolved] Sometimes a memory's version history cannot be resolved back to the date you asked about. The honest answer matters more here than a tidy one, because a result that quietly carries today's content into a historical answer is indistinguishable from a real one. So every result of an as-of search carries `as_of_resolution`. `resolved` means a version current at that date supplied the content. `no_chain` means the memory was never superseded and already existed then. `unresolved` means resolution failed and you are looking at present-day content, which you should not read as a past belief. The response also carries an `as_of_diagnostics` block counting the unresolved results and naming them. If you would rather have no answer than a partial one, pass `as_of_strict` and the request fails with 422 instead. That verdict is judged on the results actually returned, so a result that ranking dropped from your page does not fail the request. Two cases refuse to answer at all rather than label anything. A date that cannot be parsed returns 400, and an unavailable version resolver returns 503. Both previously returned an ordinary present-day search with nothing marked, which looked like a successful answer to a question you did not ask. Read `unresolved_count` for what it is. Zero means no returned result was labelled unresolved. It does not certify the whole query, since a result dropped because its historical content could not be reconstructed never reaches the page to be counted. ## Explain [#explain] `explain` composes the complete audit answer for one memory: ```python answer = client.explain("memory_id") ``` The response carries the item's identity and origin tier, every belief the system held for it over time (each with its transaction-time window, change reason, and evidence), supersession in both directions, the canonical memories it derives from, any decisions that cite it as evidence, and the result of live chain verification. REST surface: `GET /memory/{id}/explain`. MCP tool: `memory_explain`. Both are available on every plan. An agent should always be able to show its work. A `chain_verified` of `null` means there is nothing to verify yet, such as a memory that predates hash chaining or was never superseded. It is not a warning. A `false` comes with `chain_break_at` and a reason. ## Tamper-evident version chains [#tamper-evident-version-chains] Every superseded belief is written as a hash-chained version. Each version records a content hash over its exact stored bytes and a chain hash linking it to its predecessor. Rewriting, reordering, or deleting a covered version breaks the chain, and verification reports where and why. Chains are per-memory and always on. They work the same in lite mode on SQLite as they do on the full server stack. Verification recomputes everything from stored rows. It never trusts stored pointers. A chain that began after a legacy history verifies over its hashed suffix only and reports the count of uncovered legacy versions, so a positive result is always an honest one. ## Workspace verification (Enterprise) [#workspace-verification-enterprise] `GET /memory/audit/verify` checks a single item's chain, or the whole workspace against the latest checkpoint. A background sweep periodically snapshots every committed chain head into a Merkle tree and stores the root. Verification then answers with exactly one state: `verified`, `stale`, `mismatch`, `witness_invalid`, `manifest_unavailable`, or `no_digest` when no checkpoint exists yet. Responses carry a trust tier. `local_only` means the history is consistent with a checkpoint stored in the same database, which an operator could in principle rewrite wholesale. `witnessed` means the checkpoint root was anchored externally through OpenTimestamps and the proof was validated against a real Bitcoin block at the moment you asked. Only the witnessed tier supports the strong claim. Validation is deliberately strict. The proof has to commit to the exact checkpoint being verified, and that commitment covers the workspace the checkpoint belongs to, so a genuine proof cannot be swapped in from another document or lifted from another workspace. The Bitcoin block header behind the attestation has to hash to the block hash its source reported, carry valid proof of work, clear a difficulty floor, and be agreed on by two independent sources. Be precise about what those checks buy. They put the cost of a forged header at days of the entire Bitcoin network, and they stop a single source from voting twice. They do not prove the header belongs to the real Bitcoin chain, because an isolated header carries no evidence of its own height or ancestry. Point the verifier at your own node with `AUDIT_WITNESS_BITCOIN_RPC_URL` and that gap closes. Rely on the default public explorers and you are trusting them to report the canonical chain, with the requirement that two of them agree before an answer is accepted. Alongside the tier, responses carry a `witness_validation` block holding the verdict, a machine-readable reason, the block height, and the block time. Read the anchor time from there. The `anchored_at` value stored on the checkpoint is only the moment a background job noticed the confirmation, written by the same system that stores the history, so it is a convenience field rather than evidence. Treat the block time as approximate. A miner picks it and consensus constrains it only loosely, so it is a coarse bound meaning the root existed by around then. It never shows the root existed any earlier. When validation cannot run at all, because no header source is reachable, the answer stays `local_only` and says why. An outage is never reported as tampering. Operator switches (sweep and witness submission are off by default): ```bash AUDIT_DIGEST_ENABLED=true # checkpoint sweep AUDIT_WITNESS_ENABLED=true # external witness submission + confirmation # Optional. Verification falls back to public explorers when these are unset. AUDIT_WITNESS_BITCOIN_RPC_URL= # your own bitcoind, sufficient on its own AUDIT_WITNESS_HEADER_SOURCES= # comma separated explorer APIs, two must agree AUDIT_WITNESS_HEADER_TIMEOUT=5 # seconds per request ``` ## What this proves, and what it does not [#what-this-proves-and-what-it-does-not] The chain proves history integrity over its hashed coverage. Any rewrite of a covered version is detectable. It is not per-transaction author signing, it does not cover versions written before chaining began, and a private chain alone cannot defend against an operator who recomputes everything from scratch. That last case is what the external witness addresses: a recomputed history can no longer match a root that was anchored publicly before the rewrite. One hole the witness does not close is rollback. An operator can restore the whole database to an older checkpoint that was genuinely witnessed, delete every later checkpoint, and verification will answer `verified` and `witnessed`, because every artifact involved is authentic and merely old. Staleness does not catch it either, since the timestamp the staleness check reads is stored in the same database. Closing that requires remembering the newest root somewhere the operator does not control, which is why a client that pins the last root and height it saw is meaningfully stronger than one that does not. Memory stays fully editable. This is a store that is still allowed to learn. Beliefs update, supersede, and consolidate freely. The chain seals the logbook, not the mind. # Authentication & Multi-Tenancy (/smartmemory/guides/authentication-and-tenancy) SmartMemory's hosted API enforces tenant isolation at the security layer (the `SecureSmartMemory` wrapper), not in route handlers. Every request resolves to a `MemoryScopeProvider` that auto-injects scope metadata on writes and auto-filters on reads. ## Required headers [#required-headers] | Header | Required | Purpose | | ------------------------------------ | ------------------------------- | ------------------------------------------------ | | `Authorization` | All authenticated routes | `Bearer ` (Clerk-issued or refresh-rotated) | | `X-Workspace-Id` (alias `X-Team-Id`) | Most routes — workspace context | Selects the workspace data boundary | | `X-API-Key` | API-key mode only | Replaces `Authorization` for server-to-server | The active workspace is required by every route whose `ScopePolicy` is `team_required` (most write paths and ingestion). Routes with `ScopePolicy.user_only` work for any authenticated user without a workspace. ## Isolation levels [#isolation-levels] `service_common.security.scope_provider.IsolationLevel` defines three modes: | Level | Reads see | Writes are tagged with | | --------------------------------- | -------------------------------------------- | ---------------------------------------- | | `WORKSPACE` *(default)* | Workspace data + the caller's personal items | `workspace_id` + `tenant_id` + `user_id` | | `USER` | Only the caller's items within the workspace | `workspace_id` + `user_id` | | `TENANT` | Every workspace in the tenant | `tenant_id` | Source: `smart-memory-common/service_common/security/scope_provider.py`. The default is intentionally permissive — the whole point of a workspace is shared collaboration. Drop to `USER` for personal-data-only workloads, raise to `TENANT` for organisation-wide admin tooling. ## Auth flow — Clerk JWT [#auth-flow--clerk-jwt] The hosted control plane uses Clerk for identity. The web SDK exchanges a Clerk session for a SmartMemory access + refresh token pair: ``` Clerk login POST /auth/clerk/session │ (body: { clerk_token }) ▼ ──────────────────────► client (browser) ──────► SmartMemory API ▲ ◄────────────────────── │ sets sm_access_token / sm_refresh_token │ cookies (or returns JSON for SDK) ``` Tokens are short-lived. The JS SDK's `RefreshManager` rotates them silently via `POST /auth/refresh` before expiry; on cookie auth, the body is empty and the refresh token is read from `sm_refresh_token`. Manual flow: ```bash curl -X POST https://api.smartmemory.ai/auth/clerk/session \ -H 'content-type: application/json' \ -d '{"clerk_token": ""}' # → { "access_token": "...", "refresh_token": "...", "user": {...} } # Inspect the active session curl https://api.smartmemory.ai/auth/me \ -H 'authorization: Bearer ' # Refresh curl -X POST https://api.smartmemory.ai/auth/refresh \ -H 'content-type: application/json' \ -d '{"refresh_token": "..."}' # Logout (this device) curl -X POST https://api.smartmemory.ai/auth/logout \ -H 'authorization: Bearer ' # Logout (all devices) curl -X POST https://api.smartmemory.ai/auth/logout-all \ -H 'authorization: Bearer ' ``` `/auth/onboarding/{start,complete,retry}` drives the post-signup workspace provisioning sequence — usually called by the web UI after Clerk signup. ## Auth flow — API keys [#auth-flow--api-keys] For server-to-server callers, mint a key under the user's account: ```bash # Create curl -X POST https://api.smartmemory.ai/memory/api-keys \ -H 'authorization: Bearer ' \ -H 'content-type: application/json' \ -d '{ "name": "ingestion-bot", "scopes": ["read:memories", "write:memories"], "expires_in_days": 90 }' # → { "id": "...", "key_prefix": "...", "key": "sm_live_..." } # The full key is only shown once. Minted keys are prefixed `sm_live_` # (production) or `sm_test_` (test environment). # List curl https://api.smartmemory.ai/memory/api-keys -H 'authorization: Bearer ' # Revoke curl -X DELETE https://api.smartmemory.ai/memory/api-keys/ \ -H 'authorization: Bearer ' ``` Use the key as either `Authorization: Bearer ` or `X-API-Key: `. Keys with the `write:telemetry` scope must be bound to a workspace (`team_id` in the create body). Managing keys needs a signed-in user session (a JWT), not an API key. A request made with an API key to create, list or revoke keys, or to delete an agent (which revokes the agent's keys), returns `403`. In the SDKs this covers Python `create_api_key`, `list_api_keys`, `revoke_api_key` and `delete_agent`, and the JS `AuthAPI` api-keys calls, so run them from a client signed in as the user. A key bound to a workspace also cannot create teams, workspaces or agents. ## Common patterns [#common-patterns] ### Per-user memory inside a workspace [#per-user-memory-inside-a-workspace] Create the `MemoryScopeProvider` with `IsolationLevel.USER`. Reads and writes are scoped to `(workspace_id, user_id)`. Useful for personal scratchpads in a shared product. ```python from service_common.security.scope_provider import ( MemoryScopeProvider, IsolationLevel, ) scope = MemoryScopeProvider( user_id=user.id, tenant_id=user.tenant_id, workspace_id=team.id, isolation_level=IsolationLevel.USER, ) ``` ### Team-shared memory (default) [#team-shared-memory-default] `IsolationLevel.WORKSPACE` is the default. All users in the workspace see shared items, plus their own personal items. This is the right choice for 99% of multi-user products. ## Key API routes [#key-api-routes] * `/auth/*`: Clerk session exchange, token refresh, current user, and logout (`POST /auth/clerk/session`, `POST /auth/refresh`, `GET /auth/me`, `POST /auth/logout`) * `/memory/api-keys`: create, list, and delete API keys ## See also [#see-also] * [JavaScript SDK](/smartmemory/guides/javascript-sdk/) — `mode: 'apiKey' | 'clerk' | 'sso'` # Basic Usage (/smartmemory/guides/basic-usage) This guide covers the fundamental operations and patterns for using SmartMemory effectively in your applications. ## Getting Started [#getting-started] Run `smartmemory setup` once before using the SDK. Lite is the default local mode and uses SQLite with no Docker or external services. To use the managed Service backend instead, run `smartmemory setup --mode remote` and paste an API key from the [SmartMemory dashboard](https://www.smartmemory.ai/dashboard) (Dashboard, then Settings, then API Keys). ### Initialize SmartMemory [#initialize-smartmemory] ```python from smartmemory import SmartMemory # Basic initialization memory = SmartMemory() # With configuration file memory = SmartMemory(config_path="config.json") ``` ## Ingesting Memories [#ingesting-memories] ### Simple Text Ingestion [#simple-text-ingestion] The most basic way to add information to SmartMemory using the full pipeline: ```python # Ingest simple facts (full pipeline: extract → store → link → enrich → evolve) memory.ingest("Python is a programming language") memory.ingest("The capital of France is Paris") memory.ingest("Machine learning is a subset of AI") # Ingest personal experiences memory.ingest("I learned Python programming in 2020") memory.ingest("Had lunch with Sarah at the Italian restaurant yesterday") # Ingest procedures memory.ingest("To make coffee: heat water, add grounds, brew for 4 minutes") ``` ### Simple Storage (No Pipeline) [#simple-storage-no-pipeline] For internal operations or when pipeline is not needed, use `add()`: ```python from smartmemory import MemoryItem # Simple storage without extraction/linking/evolution item = MemoryItem( content="Attention mechanisms allow models to focus on relevant input parts", memory_type="zettel", metadata={"tags": ["#ml", "#attention", "#transformer"]} ) memory.add(item) ``` ### Structured Data Ingestion [#structured-data-ingestion] For more control over how memories are processed: ```python # Ingest with explicit memory type memory.ingest({ "content": "Meeting with John about project timeline", "memory_type": "episodic", "metadata": { "participants": ["John"], "topic": "timeline", "urgency": "high" } }) # Ingest with pre-extracted entities and relations memory.ingest({ "content": "John Smith works at Google", "entities": [ {"name": "John Smith", "type": "PERSON"}, {"name": "Google", "type": "ORGANIZATION"} ], "relations": [ {"source": "John Smith", "target": "Google", "type": "WORKS_AT"} ] }) ``` ### Batch Ingestion [#batch-ingestion] For ingesting multiple memories efficiently: ```python memories = [ "Python is used for web development", "Django is a Python web framework", "Flask is another Python web framework", "FastAPI is a modern Python web framework" ] # Ingest all memories (full pipeline) for memory_text in memories: memory.ingest(memory_text) # Or use async for background processing for memory_text in memories: memory.ingest(memory_text, sync=False) ``` ### Ingesting with Source Attribution (Grounding) [#ingesting-with-source-attribution-grounding] Grounding allows you to link memories to their sources for transparency and verification: ```python # Ingest memory with source information memory_id = memory.ingest("The Earth's circumference is approximately 40,075 km") # Ground the memory to its source memory.ground( item_id=memory_id, source_url="https://en.wikipedia.org/wiki/Earth", validation={"confidence": 0.95, "verified": True} ) # Or ingest with source information directly memory.ingest( "Python was created by Guido van Rossum", context={ "source_url": "https://python.org/about", "source_type": "official", "confidence": 0.98 } ) ``` **Why use grounding?** * **Transparency**: Track where information comes from * **Fact-checking**: Verify claims against authoritative sources * **Trust**: Build confidence in AI-generated responses * **Audit trails**: Maintain records for compliance ## Searching Memories [#searching-memories] ### Basic Search [#basic-search] ```python # Search for relevant memories results = memory.search("Python programming") # Display results for result in results: print(f"ID: {result.item_id}") print(f"Content: {result.content}") print(f"Type: {result.memory_type}") print("---") ``` ### Type-Specific Search [#type-specific-search] ```python # Search only semantic memories (facts) facts = memory.search("artificial intelligence", memory_type="semantic") # Search only episodic memories (experiences) experiences = memory.search("meeting", memory_type="episodic") # Search only procedural memories (how-to) procedures = memory.search("deploy", memory_type="procedural") ``` ### Advanced Search Options [#advanced-search-options] ```python # Limit number of results top_results = memory.search("machine learning", top_k=3) # Search with memory type filter results = memory.search( query="programming", memory_type="semantic", top_k=5 ) ``` **Note:** User/tenant filtering is handled automatically by `ScopeProvider` in multi-tenant deployments. ## Retrieving Specific Memories [#retrieving-specific-memories] ### Get by ID [#get-by-id] ```python # Get a specific memory memory_item = memory.get("memory_id_123") if memory_item: print(f"Content: {memory_item.content}") print(f"Created: {memory_item.created_at}") print(f"Type: {memory_item.memory_type}") else: print("Memory not found") ``` ### Get Related Memories [#get-related-memories] ```python # Find memories related to a specific memory if memory_item: related = memory.get_neighbors(memory_item.item_id) print(f"Found {len(related)} related memories") for related_memory in related: print(f"- {related_memory.content}") ``` ## Working with Relationships [#working-with-relationships] ### Automatic Relationships [#automatic-relationships] SmartMemory automatically creates relationships based on: * Semantic similarity * Shared entities * Temporal proximity * Content overlap ```python # Add related memories - SmartMemory will link them automatically memory.add("Python is a programming language") memory.add("I use Python for data analysis") memory.add("Python has excellent machine learning libraries") # Search for Python-related memories python_memories = memory.search("Python") for mem in python_memories: # Get automatically discovered relationships related = memory.get_neighbors(mem.item_id) print(f"'{mem.content}' has {len(related)} related memories") # Create explicit relationships between memories # Add memories first (add() returns item_id string) python_memory_id = memory.add("Python programming language") project_memory_id = memory.add("Data science project") # Create explicit relationship memory.link( source_id=python_memory_id, target_id=project_memory_id, link_type="USED_FOR" ) # Get all links for a memory links = memory.get_links(python_memory_id) for link in links: print(link) # Prints link string representation ``` ### Semantic Memory (Facts & Knowledge) [#semantic-memory-facts--knowledge] Best for storing factual, timeless information: ```python # Scientific facts memory.add("Water boils at 100°C at sea level") memory.add("The speed of light is 299,792,458 m/s") # Technical knowledge memory.add("REST APIs use HTTP methods like GET, POST, PUT, DELETE") memory.add("Git is a distributed version control system") # Search semantic knowledge facts = memory.search("HTTP", memory_type="semantic") ``` ### Episodic Memory (Experiences) [#episodic-memory-experiences] Best for personal experiences and events: ```python # Personal events memory.add("Attended the Python conference in San Francisco last month") memory.add("Had a productive meeting with the development team yesterday") # Learning experiences memory.add("Completed the machine learning course on Coursera in March") memory.add("Fixed the authentication bug after 3 hours of debugging") # Search experiences experiences = memory.search("conference", memory_type="episodic") ``` ### Procedural Memory (How-to) [#procedural-memory-how-to] Best for step-by-step procedures and skills: ```python # Technical procedures memory.add("To deploy to AWS: 1) Build Docker image 2) Push to ECR 3) Update ECS service") memory.add("Git workflow: create branch → make changes → commit → push → create PR") # Daily procedures memory.add("Morning routine: exercise → shower → coffee → check emails") # Search procedures procedures = memory.search("deploy", memory_type="procedural") ``` ### Pending Memory (Temporary) [#pending-memory-temporary] Best for current context and temporary information: ```python # Current tasks memory.add("Currently debugging the authentication module") memory.add("Working on user registration feature") # Temporary variables memory.add("Session token: abc123, expires in 1 hour") # Search current context current_work = memory.search("debugging", memory_type="pending") ``` ## Updating and Deleting Memories [#updating-and-deleting-memories] ### Update Existing Memories [#update-existing-memories] ```python # Get existing memory memory_item = memory.get("memory_id_123") if memory_item: # Update content memory_item.content = "Updated content with new information" # Update metadata memory_item.metadata["status"] = "completed" # Save changes memory.update(memory_item) print(f"Updated item id: {memory_item.item_id}") ### Delete Memories ``` ```python # Delete specific memory success = memory.delete("memory_id_123") if success: print("Memory deleted successfully") else: print("Failed to delete memory") # Clear all memories (use with caution!) memory.clear() ``` ## Background Processing [#background-processing] ### Background Processing [#background-processing-1] ```python # Synchronous (process now via full pipeline) memory.ingest("Process immediately", sync=True) # Asynchronous (quick persist + enqueue for background workers) result = memory.ingest("Process in background", sync=False) print(result) # {"item_id": "...", "queued": True} # Note: a separate worker service must consume the background queue. ``` ### Monitor Background Processing [#monitor-background-processing] Background worker orchestration and monitoring are external to the core library. Use your queue system (e.g., Redis Streams) to implement metrics as needed. ## Error Handling [#error-handling] ### Basic Error Handling [#basic-error-handling] ```python # SmartMemory does not export a public exception hierarchy. Operations raise # standard Python exceptions (and the underlying backend's errors), catch # `Exception` and inspect the message/type as needed. try: # Add memory result = memory.add("Some content") print(f"Added: {result.item_id}") except Exception as e: print(f"Memory operation failed: {e}") ``` ### Search Error Handling [#search-error-handling] ```python try: results = memory.search("query") if not results: print("No memories found") else: print(f"Found {len(results)} memories") except Exception as e: print(f"Search failed: {e}") # Fallback to cached results or alternative search ``` ## Performance Best Practices [#performance-best-practices] ### Efficient Memory Addition [#efficient-memory-addition] ```python # Use background processing for non-critical additions memory.ingest("Non-critical information") # Use regular add() for critical information that needs immediate processing critical_memory = memory.add("Critical information") # Batch related additions related_memories = [ "Python programming", "Python web frameworks", "Python data science" ] for mem in related_memories: memory.ingest(mem) # Processed together in background ``` ### Efficient Searching [#efficient-searching] ```python # Use specific memory types for better performance semantic_results = memory.search("AI", memory_type="semantic") # Limit results for faster response quick_results = memory.search("programming", top_k=3) # Cache frequently accessed memories frequently_used = ["important_id_1", "important_id_2"] cached_memories = {id: memory.get(id) for id in frequently_used} ``` ### Memory Management [#memory-management] ```python # Get memory summary stats = memory.summary() print(f"Total memories: {stats.get('total_count', 0)}") # Periodic cleanup (if needed) # memory.clear() # Use with extreme caution ``` ## Integration Patterns [#integration-patterns] ### With Web Applications [#with-web-applications] ```python from flask import Flask, request, jsonify app = Flask(__name__) memory = SmartMemory() @app.route('/add_memory', methods=['POST']) def add_memory(): content = request.json.get('content') item_id = memory.add(content) return jsonify({'id': item_id, 'status': 'added'}) @app.route('/search', methods=['GET']) def search_memories(): query = request.args.get('q') results = memory.search(query, top_k=10) return jsonify([{ 'id': r.item_id, 'content': r.content, 'type': r.memory_type } for r in results]) ``` ### With Data Processing Pipelines [#with-data-processing-pipelines] ```python import smartmemory.utils def process_documents(documents): """Process a batch of documents into memory""" for doc in documents: # Extract key information summary = extract_summary(doc) entities = extract_entities(doc) # Add to memory with structure memory.add({ "content": summary, "memory_type": "semantic", "metadata": { "source": doc.source, "entities": entities, "processed_at": smartmemory.utils.now() } }) ``` ## Common Patterns [#common-patterns] ### Question-Answering System [#question-answering-system] ```python def answer_question(question): # Search for relevant memories relevant_memories = memory.search(question, top_k=5) if not relevant_memories: return "I don't have information about that." # Combine relevant information context = "\n".join([mem.content for mem in relevant_memories]) # Generate answer (using LLM or rule-based approach) answer = generate_answer(question, context) # Store the Q&A as new memory memory.add(f"Question: {question}\nAnswer: {answer}") return answer ``` ### Learning System [#learning-system] ```python class LearningSystem: def __init__(self): self.memory = SmartMemory() def learn_fact(self, fact): """Learn a new fact""" return self.memory.add(fact, memory_type="semantic") def record_experience(self, experience): """Record a personal experience""" return self.memory.add(experience, memory_type="episodic") def learn_procedure(self, procedure): """Learn a new procedure""" return self.memory.add(procedure, memory_type="procedural") def recall(self, query): """Recall relevant information""" return self.memory.search(query, top_k=5) ``` ## Next Steps [#next-steps] * [Memory Operations](/smartmemory/guides/memory-operations/) - Explore the full operation surface * [Ontology Management](/smartmemory/guides/ontology-management/) - Structure your knowledge * [Performance Tuning](/smartmemory/guides/performance-tuning/) - Optimize for your use case * [MCP Integration](/smartmemory/guides/mcp-integration/) - Connect with LLM agents # Code Indexing (/smartmemory/guides/code-indexing) SmartMemory can parse and index code repositories, turning source code into searchable knowledge graph nodes. Classes, functions, routes, tests, and their relationships become first-class entities that you can query alongside other memories. ## How it works [#how-it-works] `ingest_code()` performs a multi-pass analysis of your repository: 1. **File collection**: walks the directory tree, collecting files by language. 2. **AST parsing**: extracts code entities and relationships from each file. 3. **Cross-file resolution**: builds a symbol table to resolve import bindings across modules. 4. **Graph writing**: creates nodes and edges in the knowledge graph. 5. **Embedding generation**: generates vector embeddings for each entity. 6. **Pattern seeding**: promotes class and function names into the EntityRuler pattern set. ## Basic usage [#basic-usage] Index a repository with the CLI: ```bash sm code index . --repo my-project ``` Then recall indexed code from Python with `search_code()`: ```python from smartmemory.pipeline.config import PipelineConfig from smartmemory.tools.factory import lite_context lite = dict(pipeline_profile=PipelineConfig.lite(llm_enabled=False)) with lite_context(**lite) as memory: hits = memory.search_code( "clean up vendor billing records", repo="my-project", ) for hit in hits: print( hit["name"], hit["entity_type"], f'{hit["file_path"]}:{hit["line_number"]}', hit.get("docstring", ""), hit["score"], ) ``` `search_code()` returns a list of dictionaries with `name`, `entity_type`, `file_path`, `line_number`, `docstring`, and `score`. There is no `sm code search` command. Use the Python API for recall after indexing. ## CLI parameters [#cli-parameters] | Parameter | Description | | --------------- | ------------------------------------------------------------------------------------- | | `PATH` | Directory to index. Use `.` for the current repo. | | `--repo` | Short repository identifier such as `"my-service"`. | | `--language` | Repeatable language selector. Supports `python` and `typescript`. Defaults to Python. | | `--exclude` | Repeatable directory name to skip. | | `--commit-hash` | Override the Git commit SHA. Pass an empty string to suppress auto-detection. | ## Entity types [#entity-types] The parser extracts five entity types from source code: | Entity type | Description | Example | | ----------- | -------------------------------- | ------------------------------ | | `module` | A source file | `smartmemory/code/parser.py` | | `class` | Class definition | `class CodeParser` | | `function` | Function or method definition | `def parse_file(...)` | | `route` | FastAPI or Express route handler | `@router.get("/health")` | | `test` | Test function | `def test_index_python_repo()` | Each entity gets a deterministic ID in the format `code::{repo}::{file_path}::{name}`, making re-indexing idempotent. ## Node properties [#node-properties] Every code entity node carries these properties: | Property | Type | Always present | Description | | ------------- | ----- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `item_id` | `str` | yes | Deterministic node ID | | `name` | `str` | yes | Entity name | | `entity_type` | `str` | yes | One of `module`, `class`, `function`, `route`, `test` | | `file_path` | `str` | yes | Relative path from the repository root | | `line_number` | `int` | yes | Line number where the entity is defined | | `repo` | `str` | yes | Repository identifier | | `memory_type` | `str` | yes | Always `"code"` | | `docstring` | `str` | if present | First 500 characters of the entity's docstring | | `decorators` | `str` | if present | Comma-separated decorator names | | `bases` | `str` | if present | Comma-separated base class names | | `http_method` | `str` | routes only | HTTP method such as `GET` or `POST` | | `http_path` | `str` | routes only | Route path such as `/health` | | `commit_hash` | `str` | if provided | Git commit SHA at index time. A checkout with uncommitted or untracked changes is stamped `-dirty-`, so it never passes for the clean commit. | | `indexed_at` | `str` | yes | ISO timestamp of when the entity was indexed | ## Relationship edges [#relationship-edges] The indexer creates five types of edges between code entity nodes: | Edge type | Meaning | Example | | ---------- | ------------------------------------------- | ---------------------------------------- | | `DEFINES` | A module defines a class or function | `parser.py` defines `CodeParser` | | `IMPORTS` | A file imports a symbol from another module | `indexer.py` imports `CodeEntity` | | `CALLS` | A function calls another function | `index()` calls `parse_file()` | | `INHERITS` | A class extends another class | `SqliteStore` inherits `BaseStore` | | `TESTS` | A test function tests a specific entity | `test_parse_file()` tests `parse_file()` | Cross-file `CALLS` and `IMPORTS` edges are resolved through a two-pass symbol table that maps local import names to their target entity IDs. ## Repo names and checkout identity [#repo-names-and-checkout-identity] Each index is stored under a repo name. A name belongs to one checkout. The checkout is identified by its normalized Git remote URL, or by a hash of its absolute path when it has no remote. Two clones of the same remote count as the same repo. If you index a different checkout under a name that already belongs to another checkout in the same workspace, the index is refused with an error that names both. The existing index is not changed. Pick a different repo name for the second checkout. The MCP `code_index` tool derives the name from the Git remote when you do not pass `repo_name`, for example `github.com/acme/api`. A package inside a monorepo gets its path appended, as in `github.com/acme/mono#packages/web`. ## Looking up one entity [#looking-up-one-entity] `GET /memory/code/context` and `GET /memory/code/dependencies` match `entity_name` against the stored name or qualified name, either exactly or as the last dotted part. So `run` finds the method `Svc.run`. When more than one entity matches, the API returns HTTP 409 with every candidate (item ID, qualified name, file and line). Repeat the call with `file_path` or `item_id` from a candidate to choose one. ## Snapshot file for local tools [#snapshot-file-for-local-tools] `smartmemory code bundle` parses a checkout and writes one JSON file. It does not open a memory store, does not need an account or network, and does not create embeddings. Use it when a local tool wants the code graph without running the service. ```bash smartmemory code bundle . --repo myapp --out myapp.snapshot.json ``` | Option | Meaning | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--repo` | Repo name stamped on the file. Required. | | `--out` | File to write. Required. | | `--language` | Narrow to `python` or `typescript`. Repeatable. Default is every supported language present. | | `--exclude` | Extra directory names to skip. Repeatable. | | `--allow-partial` | Write the file even when some files fail to parse. | | `--fields` | `full` (default) or `minimal`. Minimal drops `framework_evidence` and the per-entity parse diagnostic when it is clean. The forge/stratum code-graph consumer (STRAT-CODEGRAPH-1) reads `minimal` by default, and full output is opt-in for it. | The file is written atomically, so a failed run never leaves a half-written file and never replaces an existing one. If you write the file inside the checkout and it is not git-ignored, the command prints a warning, because the next snapshot would see it as an uncommitted change. There is no option to override the commit hash. ### What the file holds [#what-the-file-holds] * `commit_hash` and `source`: the revision of the checkout. A dirty tree gets `-dirty-`. `source.repo_identity` is the same checkout identity the hosted index uses. * `entities`: each has an `item_id`, and every relation endpoint is the `item_id` of an entity in the same file. * `relations`: `DEFINES`, `IMPORTS`, `CALLS`, `INHERITS`, `TESTS` and `REFERENCES`. A call site with several candidates produces one relation per candidate. * `edge_state` on every call, test and reference relation: `resolved` (one candidate) or `ambiguous` (several). A site with no candidate, whether `unresolved` or `unsupported`, is not written as a relation. It stays in `call_evidence` or `test_evidence` with its state. A resolved edge is still a name or binding match, not a proof. Read it from the file and do not recompute it. * `call_evidence` and `test_evidence` on an entity hold the call and test sites that are not already a relation, including every unresolved or unsupported site, so none are lost. * `skipped_paths`: files left out on purpose, each with a reason. TypeScript and JavaScript files are skipped with `grammar_unavailable` when the optional grammars are missing. Install them with `pip install "smartmemory-core[typescript]"`. * `complete`: true when no file failed to parse. `failed_paths` lists the failures, and `budget_exhausted` says whether the run stopped at its entity limit. * `source.resolution_dependencies`: the files the resolvers read, such as `tsconfig.json` files in an `extends` chain and the nearest `package.json` of each JavaScript file. A change to one of them can change edges even when `commit_hash` does not change, so re-check them to decide whether a snapshot is stale. Content entries carry a `sha256`. Directory listing and glob entries carry `members_sha256`, a digest of the member files that does not depend on where the checkout lives. Adding or removing a member changes it. If a listing, glob member or subtree cannot be read, or a member resolves outside the checkout, `members_sha256` is null and `members_error` says why. Core also logs a warning. The list covers every file the run parsed, including files that failed, were skipped by policy or were dropped by a budget. By default the command refuses to write when any file fails to parse, and it prints the failing path. With `--allow-partial` it writes the files that did parse, sets `complete` to false, exits 0 and prints a warning that names the failed paths. Nothing from a failed file enters the entities, relations or evidence, and files that parsed with a partial result still contribute. If every file failed, the command refuses. It still refuses when the path is not a directory, when no entities were found, or when an entity path escapes the checkout. The snapshot is not a request body for the hosted index endpoint. Hosted indexing keeps its own upload path and its 64 MiB request limit. The snapshot has no size limit. ## Incremental updates [#incremental-updates] For adding or updating individual code entities without re-indexing an entire repository, use `ingest_structured()` with the `code_entity` schema: ```python from smartmemory.pipeline.config import PipelineConfig from smartmemory.tools.factory import lite_context lite = dict(pipeline_profile=PipelineConfig.lite(llm_enabled=False)) with lite_context(**lite) as memory: item_id = memory.ingest_structured( { "name": "AuthMiddleware", "entity_type": "class", "file_path": "middleware/auth.py", "line_number": 42, "repo": "my-service", "docstring": "JWT authentication middleware for FastAPI.", "bases": ["BaseHTTPMiddleware"], }, schema="code_entity", ) print(item_id) ``` The `code_entity` schema requires `name`, `entity_type`, `file_path`, `line_number`, and `repo`. The `entity_type` must be one of `module`, `class`, `function`, `route`, or `test`. ## Languages supported [#languages-supported] | Language | Status | File extensions | Notes | | ---------- | ------------ | ---------------------------- | -------------------------------------------------------------------------------------------------------------- | | Python | Full support | `.py` | AST-based parsing. Extracts classes, functions, FastAPI routes, pytest tests, imports, calls, and inheritance. | | TypeScript | Experimental | `.ts`, `.tsx`, `.js`, `.jsx` | Basic entity extraction. Pass `--language typescript` to the CLI. | ## IndexResult [#indexresult] The `ingest_code()` method returns the real `IndexResult` dataclass from `smartmemory.code.models`: ```python from dataclasses import fields from smartmemory.code.models import IndexResult print([field.name for field in fields(IndexResult)]) ``` Check `result.errors` after indexing to see any files that failed to parse. Failed files are skipped without aborting the run. ## Pattern seeding [#pattern-seeding] After indexing, `seed_patterns_from_code()` runs automatically. It extracts `{name: entity_type}` pairs from the indexed entities and merges them into the workspace's EntityRuler pattern set. This means that after you index a codebase, future `ingest()` calls over natural-language text can recognize references to your code symbols. For example, if you index a project containing `class SmartGraph`, then ingesting the text `"The SmartGraph module handles all graph queries"` can identify `SmartGraph` as a named entity of type `class`. # Connect any AI client (/smartmemory/guides/connect-mcp) Connect SmartMemory's hosted MCP server with this endpoint: ``` https://mcp.smartmemory.ai/mcp ``` ## Claude.ai and Claude Desktop [#claudeai-and-claude-desktop] Open **Settings → Connectors → Add custom connector**, then paste: ``` https://mcp.smartmemory.ai/mcp ``` ## Claude Code [#claude-code] ```bash claude mcp add --transport http smartmemory https://mcp.smartmemory.ai/mcp ``` Run `/mcp` to sign in. ## ChatGPT desktop app [#chatgpt-desktop-app] Open **Plugins → MCPs → Add custom MCP**. Select **Streamable HTTP**, paste the URL below, leave the bearer token empty, and select **Save**. Approve the two consent screens on first use. ``` https://mcp.smartmemory.ai/mcp ``` ## ChatGPT web [#chatgpt-web] Open **Settings → Apps & Connectors → Developer mode → Create**. Select OAuth, paste the URL below, and leave the client ID empty. ``` https://mcp.smartmemory.ai/mcp ``` ## Cursor [#cursor] Open **Settings → Tools & MCP → New MCP Server**, then add this to `mcp.json`: ```json { "mcpServers": { "smartmemory": { "url": "https://mcp.smartmemory.ai/mcp" } } } ``` As a shortcut, this link adds the same server in one step: [Install SmartMemory in Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=smartmemory\&config=eyJ1cmwiOiJodHRwczovL21jcC5zbWFydG1lbW9yeS5haS9tY3AifQ==). If the link does not open Cursor, use the Settings route. ## VS Code and Copilot [#vs-code-and-copilot] Open the Command Palette and run **MCP: Add Server**, or add this to `.vscode/mcp.json`: ```json { "servers": { "smartmemory": { "type": "http", "url": "https://mcp.smartmemory.ai/mcp" } } } ``` As a shortcut, this link adds the same server in one step: [Install SmartMemory in VS Code](vscode:mcp/install?%7B%22name%22%3A%22smartmemory%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.smartmemory.ai%2Fmcp%22%7D). If the link does not open VS Code, use the Command Palette route. ## Codex CLI and stdio-only clients [#codex-cli-and-stdio-only-clients] ```bash npx -y mcp-remote https://mcp.smartmemory.ai/mcp ``` ## Grok and the xAI API [#grok-and-the-xai-api] Use a SmartMemory API key as a static bearer token. See [Connect from Grok](/smartmemory/guides/hosted-mcp/#connect-from-grok) for the required configuration. ## Verify [#verify] Ask your assistant to run `whoami`. Expected fields include your user identity, active workspace or team, and authentication details. For OAuth, API-key authentication, and the hosted tool list, see the [Hosted MCP Server guide](/smartmemory/guides/hosted-mcp/). Connecting a client is subject to the SmartMemory [Terms of Service](https://www.smartmemory.ai/terms) and [Privacy Policy](https://www.smartmemory.ai/privacy). The privacy policy describes what a connected AI client can access on your behalf and how to revoke access. # Declaring Memory Types (/smartmemory/guides/declaring-memory-types) SmartMemory ships with built-in memory types (`semantic`, `episodic`, `procedural`, `zettel`, and the extended and structured types). When your application has its own record shape, such as a support ticket or a lab result, you can declare it as a memory type of your own and SmartMemory will validate every write against the schema you gave it. A declared type is an ontology class with `kind="record"`. The ontology is the schema: there is no second registry and no separate type system to learn. ## The core idea [#the-core-idea] An item's `memory_type` **is** the class name. When you declare a class called `SupportTicket`, you write items with `memory_type: "SupportTicket"` and SmartMemory resolves the class, checks the item against its schema, and stores it. Two consequences are worth knowing up front: * **Built-in types cannot be shadowed.** Writes resolve against the process registry first, so declaring a class named `semantic` will not override the built-in behaviour. * **Declarations are per workspace.** A class declared in one workspace is invisible to another, which is what makes declared types safe in a multi-tenant deployment. ## Declaring a type [#declaring-a-type] The declaration surface is `POST /memory/ontology/types`, with both SDKs wrapping it. ```bash curl -X POST "$SMARTMEMORY_API_URL/memory/ontology/types" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Workspace-Id: $WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "name": "SupportTicket", "kind": "record", "properties_schema": { "subject": {"type": "string", "indexed": true}, "severity": {"type": "integer"}, "opened_at":{"type": "datetime"}, "tags": {"type": "list", "of": "string"}, "resolved": {"type": "boolean", "default": false} }, "required_properties": ["subject", "severity"], "storage_strategy": "full", "storage_searchable": true }' ``` The same call from the Python SDK: ```python from smartmemory_client import SmartMemoryClient client = SmartMemoryClient(api_key=..., workspace_id=...) client.declare_type( "SupportTicket", kind="record", properties_schema={ "subject": {"type": "string", "indexed": True}, "severity": {"type": "integer"}, "opened_at": {"type": "datetime"}, "tags": {"type": "list", "of": "string"}, "resolved": {"type": "boolean", "default": False}, }, required_properties=["subject", "severity"], storage_strategy="full", storage_searchable=True, ) ``` And from the JavaScript SDK: ```javascript await client.ontology.declareType({ name: 'SupportTicket', kind: 'record', propertiesSchema: { subject: { type: 'string', indexed: true }, severity: { type: 'integer' }, opened_at: { type: 'datetime' }, tags: { type: 'list', of: 'string' }, resolved: { type: 'boolean', default: false }, }, requiredProperties: ['subject', 'severity'], storageStrategy: 'full', storageSearchable: true, }); ``` ### Declaration fields [#declaration-fields] | Field | Default | Purpose | | --------------------- | ----------- | ----------------------------------------------------------------------------------------------------- | | `name` | required | Class name. For `kind="record"` this is the value you write as `memory_type` | | `kind` | `entity` | `entity` for extraction vocabulary, `record` for a concrete record class | | `properties_schema` | none | Map of field name to field spec (see below) | | `required_properties` | none | Field names that must be present on every item | | `storage_strategy` | none | `full`, `indexed`, or `append`. Record classes only | | `storage_searchable` | none | Whether items are embedded for vector search by default. Record classes only | | `tier` | `confirmed` | Governance tier. Use `working` to route the class through review instead of taking effect immediately | `display_name`, `definition`, `description`, `aliases`, `examples`, `parent_types`, `iri`, and `wikidata_qid` are also accepted and are descriptive only. ### Field specs [#field-specs] Each entry in `properties_schema` maps a field name to a spec: | Key | Values | Meaning | | ------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------- | | `type` | `string`, `integer`, `number`, `boolean`, `datetime`, `object`, `list` | Required. The field's type | | `of` | `string`, `integer`, `number`, `boolean`, `datetime`, `object` | Element type, for `list` fields only | | `default` | any JSON value | Applied when the field is absent. Must be compatible with `type` | | `indexed` | `true` or `false` | Creates a database index on the field | | `append_only` | `true` or `false` | The field may be set once and never changed | Whether a field is required is expressed in `required_properties`, not in the field spec. Lists of scalars are stored as native array values. Lists of objects are stored JSON serialised, and per-element schemas are not validated. ### Storage strategy [#storage-strategy] `storage_strategy` controls how items of the class are persisted: * `full` stores the item and, when `storage_searchable` is true, embeds it for vector search. * `indexed` stores the item with indexed fields for structured lookup. * `append` is an append-only log shape. **Items written with `append` are never embedded**, regardless of `storage_searchable`, so do not expect them in vector search results. ## Writing items [#writing-items] Once the class exists, write items exactly as you would for a built-in type, using the class name as `memory_type`: ```python client.add( content="Checkout fails on the payment step for EU customers.", memory_type="SupportTicket", metadata={ "subject": "Checkout failure", "severity": 2, "opened_at": "2026-08-07T09:00:00Z", "tags": ["billing", "eu"], }, ) ``` ### Validation is strict by default [#validation-is-strict-by-default] Writes are checked against the declared schema. A missing required field, a wrong type, or a value that violates `append_only` is rejected with a `400`, and nothing is stored. This is deliberate: a silently dropped field is far more expensive to discover later than a rejected write. If you need the transitional behaviour instead, setting `SMARTMEMORY_STRICT_SCHEMA_VALIDATION=false` downgrades violations to warnings. Treat this as a migration aid rather than a steady state, because it lets malformed records into the store. Updates are revalidated on the same rules, so a field cannot be corrupted after the fact. ## Declaring relations [#declaring-relations] Relations between classes are declared with `POST /memory/ontology/relations` (`declare_relation` in Python, `declareRelation` in JavaScript). A declared relation carries a domain, a range, and a cardinality, and links are validated against them when they are created. As with schema validation, relation validation is strict by default and `SMARTMEMORY_STRICT_RELATION_VALIDATION=false` downgrades it to warnings. ## Lifecycle [#lifecycle] Declared classes are governed rather than simply deleted, so that history stays auditable. **Counting.** `GET /memory/ontology/types/{type_id}` returns the class, including an `instance_count` of the items currently held by it. The count resolves the effective class, so after a merge, asking about a merged-away name answers for the surviving class. To list only your record classes, pass the `kind` filter: `GET /memory/ontology/types?kind=record`. **Migrating.** `POST /memory/ontology/types/{from_id}/migrate-to/{to_id}` moves instances from one class to another: ```python client.migrate_ontology_type_instances( "SupportTicket", "Ticket", reason="Consolidating on the shorter class name", on_violation="refuse", ) ``` `on_violation` controls what happens when a source item does not satisfy the destination schema: * `refuse` (the default) validates every source item **before** any rewrite and refuses the whole migration if any item fails. There is no partial state. * `skip` migrates the conforming items and returns the ids it skipped. Migrations between classes of different kinds are refused. Flipping a declared class to a built-in type, or the reverse, is also refused. Use a migration instead. **Merging.** Merging two record classes refuses if the source class still holds items, so a merge can never strand data. **Retiring.** `POST /memory/ontology/types/{type_id}/retire` retires a class. The route accepts private record classes only, so a shared or built-in class cannot be retired out from under other callers. Entity classes are retired through the ontology curation queue instead. ```python client.retire_ontology_type("SupportTicket", reason="Superseded by Ticket") ``` A `reason` is required on both migration and retirement, and is recorded as audit evidence. The REST surface also accepts `superseded_by` on retirement to record an explicit redirect to a surviving class. ## Portability [#portability] Exported OKF bundles carry your class declarations, not only your items. A bundle exported from one workspace therefore imports cleanly into a fresh workspace, because the declarations arrive with the data and the destination can validate what it is being given. Imports are staged, and a conflict with an existing declaration refuses the whole import rather than merging silently. Older v0.1 bundles, which carry items only, still import. See [OKF Portability](/smartmemory/guides/okf-portability/) for the bundle format. ## API reference [#api-reference] | Route | Python SDK | JavaScript SDK | | ---------------------------------------------------- | --------------------------------- | ---------------------- | | `POST /memory/ontology/types` | `declare_type` | `declareType` | | `GET /memory/ontology/types` | `list_ontology_types` | `listTypes` | | `GET /memory/ontology/types/{id}` | `get_ontology_type` | `getType` | | `GET /memory/ontology/types/{id}/audit` | `get_ontology_type_audit` | `getTypeAudit` | | `POST /memory/ontology/types/{from}/migrate-to/{to}` | `migrate_ontology_type_instances` | `migrateTypeInstances` | | `POST /memory/ontology/types/{id}/retire` | `retire_ontology_type` | `retireType` | | `POST /memory/ontology/relations` | `declare_relation` | `declareRelation` | All routes require `Authorization: Bearer ` and `X-Workspace-Id`. ## Related [#related] * [Memory Types](/smartmemory/concepts/memory-types/) for the built-in types * [Ontology Management](/smartmemory/guides/ontology-management/) for entity vocabulary and extraction guidance * [OKF Portability](/smartmemory/guides/okf-portability/) for exporting and importing workspaces # Connect Google for your Workspace (admin steps) (/smartmemory/guides/google-workspace-oauth-client) SmartMemory connects to Google (Sheets, and other Google apps as they become available) through an OAuth client. By default every workspace uses SmartMemory's shared client. If your organization runs Google Workspace, an admin can create an OAuth client inside your own Google Cloud project and give it to SmartMemory. Your team's new Google connections then use your client. ## Why use your own client [#why-use-your-own-client] Google asks apps that read user data to go through its app verification. A client whose consent screen is set to **Internal** is only usable by accounts in your own Google Workspace organization, and Google does not require verification for it. Your users see your organization's app name on the consent screen and no unverified app warning. This applies only to users of your organization. People outside it cannot use an Internal client. Any SmartMemory workspace that has not saved its own client keeps using SmartMemory's shared client, so nothing changes for anyone else. ## What you need [#what-you-need] * A Google Workspace account with permission to create Google Cloud projects (or an existing project you manage). * Admin rights on the SmartMemory workspace you want to set up. ## Step 1: Create a Google Cloud project [#step-1-create-a-google-cloud-project] 1. Open the [Google Cloud console](https://console.cloud.google.com/) signed in with your Google Workspace account. 2. Create a new project for SmartMemory, or pick an existing one your organization owns. ## Step 2: Set up the OAuth consent screen as Internal [#step-2-set-up-the-oauth-consent-screen-as-internal] 1. In the console, open **APIs & Services**, then **OAuth consent screen**. Newer consoles call this area **Google Auth Platform**. 2. Set the user type (shown as **Audience** in newer consoles) to **Internal**. 3. Fill in the app name your users will see, a support email and a developer contact email, then save. If **Internal** is not offered, either the account you are using is not part of a Google Workspace organization, or the Google Cloud project is not inside your organization. Use an account from your organization, and create the project under your organization's resource in Google Cloud. ## Step 3: Create a Web application client [#step-3-create-a-web-application-client] 1. Open **Credentials** (or **Clients**), then choose **Create credentials** and **OAuth client ID**. 2. Choose **Web application** as the application type and give it a name, for example "SmartMemory". 3. Under **Authorized redirect URIs**, add exactly: ``` https://api.smartmemory.ai/memory/adapters/oauth/callback ``` 4. Create the client. Google shows a client ID and a client secret. Keep this window open, or download the details, for Step 5. The redirect URI must match exactly, including `https` and the path. SmartMemory always sends users back to this address, whichever client is used. ## Step 4: Enable the Google APIs [#step-4-enable-the-google-apis] In **APIs & Services**, open **Library** and enable the API for each Google app your team will connect: | Connection | API to enable | | --------------- | ------------------- | | Google Sheets | Google Sheets API | | Google Calendar | Google Calendar API | | Google Drive | Google Drive API | | Gmail | Gmail API | Enable only the ones you plan to use. A connection to a Google app whose API is not enabled fails when it first reads data. ## Step 5: Save the client in SmartMemory [#step-5-save-the-client-in-smartmemory] Save the client with the SmartMemory API, signed in as yourself (your normal sign-in token). Send `PUT /memory/adapters/oauth/clients/google` with the workspace header and a body of `{"client_id": "...", "client_secret": "..."}`. Only workspace admins can save or remove the client, and only when signed in as a person. An API key cannot, whatever its scopes. A person from another workspace, a non-admin and an API key all get the same refusal message, and it does not say which check failed. To remove the client, send `DELETE /memory/adapters/oauth/clients/google`, and `GET` on the same address shows which client is active. After saving, SmartMemory shows a short fingerprint of the client ID so you can confirm which client is active. The client secret is stored encrypted and is never shown again, in the app or through the API. The client ID is never shown again either, except that it appears in the Google sign-in address SmartMemory builds when someone connects, as Google requires. SmartMemory checks only that the values look like a Google web client. It does not send your secret to Google to test it, so the first real check happens when someone connects. If Google rejects the client at that point, the connection stops with the message "This connection needs an operator to act." and an admin needs to fix the client. ## What happens to existing connections [#what-happens-to-existing-connections] * **New connections** made after you save use your client. * **Connections made before** keep working with the client that created them. Ask their owners to reconnect if you want them on your client. * **Changing only the secret** (same client ID) keeps every existing connection working. * **Switching to a different client ID** moves new connections to the new client. Connections made with the old one keep working while it is still valid in Google. * **Removing your client** sends new connections back to SmartMemory's shared client. Connections made with your client stop refreshing and must be reconnected. SmartMemory tells you how many connections are affected when you remove it. ## Troubleshooting [#troubleshooting] * **"redirect\_uri\_mismatch" from Google:** the redirect URI in Step 3 does not match exactly. Copy it again from this page. * **"Access blocked" or an `org_internal` error for a user:** the user is signing in with an account outside your Google Workspace organization. Internal clients work only for your organization's accounts. * **A connection says "This connection needs an operator to act.":** one common cause is that Google rejected the client ID or secret. Check that the client still exists in Google Cloud and save it again with the API call in Step 5. # Graph Viewer (/smartmemory/guides/graph-viewer) SmartMemory stores your memories as a knowledge graph. This page shows how to look at that graph in each mode. | Mode | How to see the graph | | -------------------------- | ---------------------------------------------------------------- | | **Lite** (on your machine) | Built-in graph viewer, opened with `sm viewer` | | **Service** (managed API) | Fetch nodes and edges from the API with `GET /memory/graph/full` | {/* Screenshot placeholder removed in the Fumadocs migration; re-add a real asset under public/ when available. */} ## Lite: the built-in viewer [#lite-the-built-in-viewer] In Lite mode the local SmartMemory daemon serves a graph viewer. Open it with: ```bash sm viewer ``` The command starts the daemon if it isn't running and opens the viewer in your browser at `http://localhost:9014` (the default daemon port, set by `daemon_port` in your SmartMemory config). Use `sm viewer --port ` to open a different port. No account or API key is needed. ### What you see [#what-you-see] * **Nodes**: memory items (semantic, episodic, procedural, decision, opinion, observation, code, and more) and the entities extracted from them (concept, person, organization, location, technology, and more). Wikidata-grounded entities show as a separate `wikidata:Qxxx` node connected by a `GROUNDED_IN` edge. * **Edges**: relations such as `RELATED_TO`, `MENTIONS`, `GROUNDED_IN`, `PART_OF`, `SUPERSEDES`, and `DERIVED_FROM`, plus any custom relation labels promoted into the workspace ontology. * **Color coding**: memory types use warm colors (semantic, episodic, decision), and entity types use cool colors (concept, person, technology). * **Type filters**: color-coded checkboxes for memory types and entity types. * **Search**: search nodes by label (Cmd/Ctrl+K). * **Detail panel**: click a node to see its content, metadata, confidence, and timestamps. * **Layouts**: force-directed, hierarchical, circle, concentric, and grid. * **Export**: save the current view as PNG or SVG. * **Ask panel**: ask a question about your memories next to the graph. ### Live updates [#live-updates] The viewer subscribes to the daemon's progress stream at `GET /memory/progress/stream`, so new nodes and edges appear as memories are ingested. See [Progress Events](/smartmemory/reference/progress-events/) for the event contract. ## Service: graph data from the API [#service-graph-data-from-the-api] In Service mode your graph lives in the managed SmartMemory Service. Read it through the API at `https://api.smartmemory.ai` with your API key and active workspace: ```bash curl https://api.smartmemory.ai/memory/graph/full \ -H "Authorization: Bearer $SMARTMEMORY_API_KEY" \ -H "X-Workspace-Id: $WORKSPACE_ID" ``` The response holds the workspace's `nodes` and `edges`, plus `node_count` and `edge_count`. The Python client exposes the same call as `get_graph_full()`: ```python from smartmemory_client import SmartMemoryClient client = SmartMemoryClient( base_url="https://api.smartmemory.ai", api_key="sm_live_...", workspace_id="your-workspace-id", ) graph = client.get_graph_full(limit=500) ``` To watch ingestion live, subscribe to `GET /memory/progress/stream` on the same API. The [JavaScript SDK](/smartmemory/guides/javascript-sdk/#progress-streaming-sse) wraps it as `subscribeProgress`. ## Related [#related] * [Progress Events API](/smartmemory/reference/progress-events/): live-feed event contract * [Lite mode](/smartmemory/guides/lite-mode/) * [System Overview](/smartmemory/architecture/system-overview/) # Hosted MCP Server (/smartmemory/guides/hosted-mcp) SmartMemory runs a public MCP (Model Context Protocol) server at `https://mcp.smartmemory.ai/mcp`. It is the fastest way to give an AI client memory: one URL, no server to run yourself, no API key to manage by hand if your client supports OAuth. Every call is scoped to the signed-in user. The hosted server holds no shared credential and no local SmartMemory instance: each request carries its own identity, either an OAuth token issued through Clerk or a SmartMemory API key, and is executed against the managed SmartMemory API as that caller. It exposes 25 tools, a fixed allowlist rather than the full local tool catalogue (see [MCP Integration Guide](/smartmemory/guides/mcp-integration/) for the difference between hosted mode and running `smartmemory-mcp` yourself). Use of the hosted server is governed by the [Terms of Service](https://www.smartmemory.ai/terms) and [Privacy Policy](https://www.smartmemory.ai/privacy). Two ways to authenticate: * **OAuth 2.1**, via your Clerk-backed SmartMemory account. This is what Claude.ai, Claude Desktop, Claude Code, ChatGPT connectors, and Grok web connectors use. You sign in once in a browser and consent to the connection. * **A SmartMemory API key**, as a static bearer token. This is for clients that cannot open a browser to complete an OAuth flow, such as the xAI API and Grok Build. ## Connect from Claude.ai and Claude Desktop [#connect-from-claudeai-and-claude-desktop] In Claude.ai or Claude Desktop, add a custom connector with the URL: ``` https://mcp.smartmemory.ai/mcp ``` Claude will register itself automatically and open a browser tab to complete the OAuth consent. Approve the connection, and the 25 hosted tools become available in that conversation. ## Connect from Claude Code [#connect-from-claude-code] ```bash claude mcp add --transport http smartmemory https://mcp.smartmemory.ai/mcp ``` The first tool call opens a browser for consent. Claude Code registers itself through client id metadata, so nothing needs to be created on the SmartMemory side beforehand. ## Connect from ChatGPT [#connect-from-chatgpt] Add `https://mcp.smartmemory.ai/mcp` as a connector in ChatGPT's connector settings. ChatGPT completes the OAuth flow the same way as Claude: a browser consent step, then the tools are available in chat. ## Connect from Grok [#connect-from-grok] **Grok web connectors** do not register a client automatically. When Grok asks for a client id, paste the SmartMemory MCP OAuth application's client id (shown in the SmartMemory web app under connector setup). Grok then walks you through the same PKCE-protected OAuth consent as any other client. **Grok Build and the xAI API** take a static bearer token instead of an OAuth flow. Use a SmartMemory API key: ```json { "type": "mcp", "server_url": "https://mcp.smartmemory.ai/mcp", "authorization": "sm_live_your_key_here" } ``` An API key skips the OAuth flow entirely and acts as its owner, in that owner's default workspace. Legacy `sk_`-prefixed keys are also accepted. ## The 25 hosted tools [#the-25-hosted-tools] Everything not listed here is hidden. A tool that exists in the local `smartmemory-mcp` server does not appear hosted unless it is on this list, even if it is added to a module the hosted server otherwise uses. **Memory operations** `memory_ingest` `memory_search` `memory_recall` `read_around` `memory_get` `memory_explain` `memory_recall_pack` `memory_policy_bundle` `memory_add` `memory_update` `memory_delete` `memory_list` `memory_stats` `memory_distill` `memory_ingest_conversation` `memory_search_by_metadata` `memory_feedback` **Code intelligence** `code_search` `code_dead_code` `code_dependencies` **Agent recall profiles** `agent_set_recall_profile` `agent_get_recall_profile` **Reasoning** `reasoning_query_traces` **Session** `whoami` `switch_workspace` Deprecated: `switch_team`, an alias of `switch_workspace` that is removed in the next release. ## What is not available hosted, and why [#what-is-not-available-hosted-and-why] The hosted server is a single shared container talking to the managed SmartMemory API over the network. It has no local SmartMemory instance and no private disk per user, so three kinds of tool are left out on purpose rather than offered in a broken form: * **Anything that reads or writes a file path.** The container's disk would be shared by every tenant on it, so tools like exporting or importing to a path, indexing a local code directory, or reading a local transcript are not offered hosted. Run those locally with `smartmemory-mcp` instead. * **Anything that needs a local SmartMemory instance.** Some tools reach directly into the local graph or a local manager object rather than going through the network API. Those cannot work against a remote backend, so whole areas like anchors, plans, patterns, decisions, and zettelkasten stay local-only for now. * **Destructive bulk operations**, such as clearing all memories at once. Two tools have specific options that are refused rather than silently ignored: * `memory_search` with citations turned on (`cite=True`) needs a citation formatter that lives in the core SmartMemory package, which the hosted server does not ship. * `memory_recall` with a session id or with citations turned on needs a working-context builder that also lives in that same core package. Calling either with those options returns a clear tool error rather than a wrong or empty answer. ## Workspaces [#workspaces] Your initial workspace when you connect is: * The Clerk **organization** you consent for during sign-in, if that organization is also a SmartMemory tenant, or * Your **personal workspace**, otherwise. To work in a different workspace during a session, call `switch_workspace` with its id or name. It checks that you are actually a member of the workspace you are asking for before switching, and the switch only applies to your current session, never to anyone else's. There is no restart. If the switch is refused, the message names the workspace you are still using. Call `whoami` to see your user, tenant and workspace. ## Troubleshooting [#troubleshooting] **You get a 401 and are asked to re-authorize.** This is the normal OAuth challenge a client sees when it has no token yet, or when a token has expired. Complete the consent flow again in your client. **A tool call returns an error starting with `Unauthorized:`.** Your session's access token expired mid-conversation. This is different from the connection-level 401 above: the connection is still open, but the specific call needs a fresh token. Retry the call, your client should re-authorize automatically and the retry should succeed. **A tool call fails with `nda_required`.** Your account has not yet accepted the SmartMemory beta NDA. Sign in to the SmartMemory web app and accept it there, then retry. **You are being rate limited.** The hosted server limits registration, authorization, and token requests per client to keep the shared endpoint healthy for everyone. If you hit a limit, wait a minute and try again. A rate-limited response includes how long to wait. ## Related [#related] * [MCP Integration Guide](/smartmemory/guides/mcp-integration/): running `smartmemory-mcp` yourself, local and remote backends, the full local tool catalogue and tiers. * [Authentication and Tenancy](/smartmemory/guides/authentication-and-tenancy/): how SmartMemory workspaces, tenants, and API keys work. # JavaScript SDK (/smartmemory/guides/javascript-sdk) `@smartmemory/sdk-js` is the official JavaScript / TypeScript client for the SmartMemory REST API. It bundles auth (Clerk + API key + cookie SSO), 25 domain APIs, and a Server-Sent Events subscriber for the progress stream. ## Install [#install] ```bash npm install @smartmemory/sdk-js ``` The package exposes four entry points: | Import | Use it for | | ------------------------------ | ------------------------------------------------ | | `@smartmemory/sdk-js` | Top-level — `SmartMemoryClient`, all domain APIs | | `@smartmemory/sdk-js/core` | Framework-agnostic auth + APIs (no React) | | `@smartmemory/sdk-js/react` | React hooks + provider | | `@smartmemory/sdk-js/progress` | SSE subscriber for `/memory/progress/stream` | ## Quick start [#quick-start] ```js import { SmartMemoryClient } from '@smartmemory/sdk-js'; const client = new SmartMemoryClient({ mode: 'apiKey', apiBaseUrl: 'https://api.smartmemory.ai', apiKey: 'sm_live_...', }); // Add a memory const { item_id } = await client.memories.create({ content: 'Project Atlas kicks off Monday.', memoryType: 'semantic', }); // Search const { results } = await client.memories.search('atlas kickoff', { topK: 5 }); // Switch active workspace client.setTeamId('team_42'); ``` ## Auth modes [#auth-modes] `AuthCore` supports three modes, selected by the `mode` field: ```js // 1) API key — server-to-server, no token refresh new SmartMemoryClient({ mode: 'apiKey', apiBaseUrl: 'https://api.smartmemory.ai', apiKey: process.env.SMARTMEMORY_API_KEY, }); // 2) Clerk — exchanges Clerk JWT for SmartMemory access/refresh tokens new SmartMemoryClient({ mode: 'clerk', apiBaseUrl: 'https://api.smartmemory.ai', endpoints: { refresh: '/auth/refresh' }, }); // 3) SSO with cookie auth — for first-party frontends sharing a cookie domain new SmartMemoryClient({ mode: 'sso', apiBaseUrl: 'https://api.smartmemory.ai', webAppUrl: 'https://www.smartmemory.ai', useCookieAuth: true, }); ``` In `apiKey` mode the key is sent as `Authorization: Bearer `. In `clerk` / `sso` modes, `RefreshManager` automatically rotates the access token before expiry. The SDK also forwards the active workspace id as the `X-Workspace-Id` header — pulled from `tokenManager.getTeamId()`. ## Core memory operations [#core-memory-operations] `client.memories` (an instance of `MemoryAPI`) is the main surface for memory CRUD, search, and ingestion: ```js // Full pipeline ingestion (entity extraction, enrichment, grounding, evolution) await client.memories.ingest('Atlas ships in Q3, owned by Priya.', { profileName: null, extractorName: 'llm', }); // Simple add (no pipeline) — use for derived/internal items await client.memories.create({ content: '...', usePipeline: false }); // Search variants await client.memories.search('atlas', { topK: 5 }); await client.memories.searchAdvanced('atlas dependencies', { algorithm: 'query_traversal', maxResults: 15, useSSG: true, }); await client.memories.searchByMetadata('project', 'atlas'); // CRUD await client.memories.get(itemId); await client.memories.update(itemId, { content: 'new text', metadata: { tag: 'q3' } }); await client.memories.delete(itemId); await client.memories.list({ limit: 50, offset: 0, type: 'semantic' }); // Leave entity nodes out and match a word in the content. Both apply before paging, so total counts the filtered set. await client.memories.list({ excludeNodeCategory: ['entity'], q: 'robot' }); // Conversation / document ingestion await client.memories.ingestConversation(turns, { conversationId: 'abc' }); await client.memories.ingestDocument(text, { title: 'Atlas charter' }); // Working context — anchor-aware token-budgeted recall await client.memories.getWorkingContext(sessionId, 'why did we pick FalkorDB?', { k: 20, maxTokens: 4000, }); // Feedback (Hebbian co-retrieval) await client.memories.feedback([id1, id2, id3], 'helpful', 'atlas owner?'); ``` ## Domain APIs [#domain-apis] Every property on `SmartMemoryClient` is a thin wrapper over `BaseAPI`: | Property | Surface | | --------------------------------------------------------------- | ----------------------------------------------------- | | `memories` | Memory CRUD, search, ingestion, lineage, links | | `decisions` | Decisions lifecycle (create / supersede / contradict) | | `graph` | Graph reads + Cypher path / neighbour queries | | `temporal` | Time-travel queries, history, rollback | | `pipeline` | Pipeline run status, logs | | `archive` | Conversation archive store / retrieve | | `analytics` | Bias / topic analytics | | `governance` | Governance analyses + summaries | | `evolution` | Manual evolver triggers | | `reasoning` / `reasoningTraces` | Reasoning items + recorded traces | | `ontology` | Ontology read/write | | `validation` | Schema validation | | `procedureMatches` / `procedureCandidates` / `procedureDrift` | CFS-2/CFS-4 surfaces | | `summaries` | `/memory/summary/*` snapshots | | `zettelkasten` | Zettel links | | `agents` | Agentic memory profiles | | `teams` / `profiles` / `subscriptions` / `usage` / `tokenUsage` | Account surfaces | | `insights` | Insights dashboards | | `authAPI` | `/auth/*` endpoints (login, me, refresh) | ## Progress streaming (SSE) [#progress-streaming-sse] The progress event bus (`PLAT-PROGRESS-1`) is consumed via the dedicated `/progress` subpath. Native `EventSource` cannot attach custom headers, so the SDK uses `@microsoft/fetch-event-source` under the hood. ```js import { subscribeProgress } from '@smartmemory/sdk-js/progress'; const sub = subscribeProgress({ baseUrl: 'https://api.smartmemory.ai', token: accessToken, workspaceId: 'team_42', // Optional: replay a specific run from sequence 0 // runId: 'run_abc', // fromSeq: 0, onEvent: (e) => { console.log(`[${e.kind}] seq=${e.seq} status=${e.status}`, e.payload); }, onReconnect: () => console.warn('reconnecting…'), onError: (err) => console.error('SSE dead', err), }); // Tear down sub.close(); ``` Three modes are supported: * **Live** — no `runId` / `since` → drip feed of events for the active workspace. * **Run replay** — `runId` + `fromSeq=0` → the full event log for a single run. * **Scope resume** — `since=` → resume after a transient drop. The subscriber automatically retries up to 3 consecutive failures with linear-backoff (`RETRY_INTERVAL_MS * consecutiveFailures`) before calling `onError`. ## React integration [#react-integration] ```jsx import { SmartMemoryProvider, useSmartMemory, useAuth, AuthWrapper, } from '@smartmemory/sdk-js/react'; function App() { return ( ); } function Dashboard() { const client = useSmartMemory(); const { user, isAuthenticated } = useAuth(); // ... } ``` `AuthWrapper` handles bootstrap (`AuthCore.bootstrapSession()`) and renders the login flow when unauthenticated. `useAuth` exposes the auth state plus the familiar action helpers (`logout`, `refreshToken`, `setTeamId`). ## Error handling [#error-handling] All API methods reject with an `APIError` carrying `status` and `detail` fields: ```js import { APIError } from '@smartmemory/sdk-js'; try { await client.memories.get('does-not-exist'); } catch (e) { if (e instanceof APIError && e.status === 404) { // ... } throw e; } ``` ## See also [#see-also] * [Authentication & multi-tenancy](/smartmemory/guides/authentication-and-tenancy/) — auth orchestration and workspace scoping * [Progress Events](/smartmemory/reference/progress-events/) — the SSE wire format the SDK subscribes to * [Memory Operations](/smartmemory/guides/memory-operations/) — every memory method, end to end ## Creation-time windows [#creation-time-windows] Both `memory.search(query, { since, until })` and `memory.searchByMetadata(key, value, { since, until })` accept ISO strings or `Date` objects. Bounds filter item `created_at` in `[since, until)`, independently of `asOfDate`. The returned envelope preserves `coverage`, including an unknown `created_at_backfilled_boundary: null` for legacy creation-time uncertainty. ## Choosing a multi-hop planner [#choosing-a-multi-hop-planner] `hop_strategy` accepts `consensus`, `relevance`, or `semantic` on `POST /memory/search`, Python SDK `search()`, and MCP `memory_search`. JS SDK uses `memory.search(query, { multiHop: true, hopStrategy: 'relevance' })`. The Python equivalent is `client.search(query, multi_hop=True, hop_strategy="relevance")`. * `consensus` follows entities several top results agree on and may stop before a one-off bridge when distractors lower its frequency score. * `relevance` follows entities from the best results, including one-off bridges. * `semantic` asks an LLM to plan follow-up searches and incurs LLM calls. Omitted/null leaves core resolution unchanged: semantic when semantic hops/config is enabled, otherwise **consensus**. The measured fixture spike kept consensus: bridge F1 improved by 22.92 points with relevance, but median hops increased from 1 to 3, exceeding the pre-registered one-extra-hop bound. This is a fixture result, not a production or LoCoMo quality claim. Invalid strategies return HTTP 400 naming all three values. A supplied strategy with `multi_hop=false` is labelled in response `inert_parameters`; Python SDK retains this in `last_inert_parameters`, and MCP echoes it. CLI: `sm search "which team owns checkout?" --multi-hop --max-hops 3 --hop-strategy relevance`. `--max-hops` accepts 1–10; `--hop-strategy` without `--multi-hop` is rejected. Daemon, local fallback and remote wrapper paths carry the flags. # Lite Mode (/smartmemory/guides/lite-mode) Lite mode runs the full SmartMemory pipeline without Docker, FalkorDB, or Redis. It uses local SQLite for the graph, an embedded vector index, and an in-memory cache. The same 11-stage pipeline runs — only the storage providers and a few network-bound stages change. Use it for: * **Local development** without spinning up Docker * **CI / unit tests** that need a real backend but no infrastructure * **No-LLM environments** where neither `OPENAI_API_KEY` nor `GROQ_API_KEY` is set * **Notebook prototyping** and quickstarts ## What Lite Mode Changes [#what-lite-mode-changes] `PipelineConfig.lite()` (in `smartmemory/pipeline/config.py`) flips a small set of stage flags. Everything else — classification, simplification, EntityRuler NER, sentiment, temporal, topic enrichment, store, link, evolution — runs at full quality. | Stage | Default | Lite | | -------------------------------- | ------------------------- | --------------------------------------------------------------------------------- | | `coreference` (fastcoref) | enabled | **disabled** (avoids first-run model download) | | `llm_extract` | enabled | **auto-detected** from env (see below) | | `enrich.wikidata.sparql_enabled` | `True` (HTTP to Wikidata) | `False` (local SQLite alias lookup only) | | `enrich.enricher_names` | all enrichers | `["basic_enricher", "sentiment_enricher", "temporal_enricher", "topic_enricher"]` | | `evolve.run_clustering` | `True` | `False` (requires full graph scan) | | `evolve.run_evolution` | `True` | `True` (still runs — incremental via daemon thread) | Storage providers (set by `create_lite_memory()` in `smartmemory/tools/factory.py`): * Graph: `SQLiteBackend(db_path=/memory.db)` * Vector: `UsearchVectorBackend(persist_directory=)` * Cache: `NoOpCache` * Ontology: `SQLiteOntologyStore` on the same `memory.db` * Observability: disabled Default `data_dir` is `~/.smartmemory`. ## LLM Auto-Detection (DEGRADE-1d) [#llm-auto-detection-degrade-1d] `PipelineConfig.lite()` accepts `llm_enabled: bool | None`: ```python @classmethod def lite(cls, workspace_id=None, llm_enabled: Optional[bool] = None): if llm_enabled is None: llm_enabled = bool(os.getenv("OPENAI_API_KEY") or os.getenv("GROQ_API_KEY")) ... ``` * `None` (default) — auto-enables LLM extraction if either `OPENAI_API_KEY` or `GROQ_API_KEY` is set; otherwise disables it. * `True` — force on (will fail at extraction time without a key). * `False` — force off; rely on EntityRuler + spaCy NER only. When LLM extraction is off, the pipeline still extracts entities through EntityRuler (pattern-matched, \~4 ms) and spaCy NER, just without LLM-driven relation extraction. ## Three Usage Patterns [#three-usage-patterns] ### 1. `lite_context()` — recommended [#1-lite_context--recommended] Context manager that builds a SmartMemory and tears down deterministically. Closes the SQLite connection and stops the evolution worker on exit. ```python from smartmemory.tools.factory import lite_context with lite_context() as memory: item_id = memory.ingest("Paris is the capital of France.") results = memory.search("capital of France", top_k=3) for r in results: print(r.content) ``` ### 2. Constructor injection [#2-constructor-injection] For long-lived processes (servers, agents) where you manage the lifecycle directly: ```python from smartmemory.tools.factory import create_lite_memory from smartmemory.pipeline.config import PipelineConfig memory = create_lite_memory( data_dir="/var/lib/myapp/memory", pipeline_profile=PipelineConfig.lite(llm_enabled=False), # force no-LLM ) memory.ingest("Some content") # ... later ... memory.close() ``` You can also inject the lite profile into a regular `SmartMemory()` constructor if you want lite *stages* but server-mode storage: ```python from smartmemory import SmartMemory from smartmemory.pipeline.config import PipelineConfig memory = SmartMemory(pipeline_profile=PipelineConfig.lite()) ``` ### 3. Auto-fallback (zero config) [#3-auto-fallback-zero-config] If you never set `pipeline_profile`, default `SmartMemory()` will still degrade gracefully when LLM keys aren't present — the LLM extractor checks `GROQ_API_KEY` / `OPENAI_API_KEY` at call time and skips when neither is set. For predictable behaviour in CI, prefer pattern 1 or 2 with `llm_enabled=False`. ## Hermetic Variant [#hermetic-variant] `PipelineConfig.lite_hermetic()` is identical to `lite()` but **refuses to auto-download** the spaCy `en_core_web_sm` model. If the model isn't installed, it raises `RuntimeError` immediately instead of fetching it mid-test. Use this in CI parity tests where you need: * No network access during the test * No mid-test downloads that mask flaky network issues * Deterministic ordering between SQLite and FalkorDB branches of a parity matrix ```python from smartmemory.pipeline.config import PipelineConfig from smartmemory.tools.factory import lite_context # Install the model in your CI step BEFORE running tests: # python -m spacy download en_core_web_sm with lite_context(pipeline_profile=PipelineConfig.lite_hermetic()) as memory: memory.ingest("test content") ``` ## Limitations [#limitations] | Capability | Lite | Notes | | -------------------------- | ------------------- | -------------------------------------------- | | Entity extraction quality | Reduced when no LLM | EntityRuler + spaCy NER only | | Relation extraction | Off when no LLM | Requires LLM stage | | Coreference resolution | Off | `fastcoref` disabled to avoid model download | | Wikidata SPARQL grounding | Off | Local SQLite alias lookup only; no HTTP | | Wikipedia / link enrichers | Off | Network-dependent | | Graph clustering evolver | Off | Full graph scan, expensive | | Concurrent writes | Limited | SQLite is single-writer | | Multi-tenant scoping | Same as server | `ScopeProvider` works identically | ## End-to-End Example [#end-to-end-example] ```python """Quick lite-mode demo: ingest → search → cleanup.""" import os from smartmemory.tools.factory import lite_context from smartmemory.pipeline.config import PipelineConfig # Force no-LLM for a fully offline run profile = PipelineConfig.lite(llm_enabled=False) with lite_context(data_dir="./demo-memory", pipeline_profile=profile) as memory: # Ingest facts = [ "Ada Lovelace wrote the first algorithm intended for a machine.", "She collaborated with Charles Babbage on the Analytical Engine.", "The first algorithm computed Bernoulli numbers.", ] for f in facts: item_id = memory.ingest(f) print(f"stored {item_id}") # Search print("\nSearch: 'Ada Lovelace'") for hit in memory.search("Ada Lovelace", top_k=3): print(f" - {hit.content[:80]}") # Stats print(f"\nLast trajectory stages: " f"{len(memory.last_trajectory.stages) if memory.last_trajectory else 0}") ``` Output (on a machine without `OPENAI_API_KEY`): ``` stored 1a2b... stored 3c4d... stored 5e6f... Search: 'Ada Lovelace' - Ada Lovelace wrote the first algorithm intended for a machine. - She collaborated with Charles Babbage on the Analytical Engine. - The first algorithm computed Bernoulli numbers. Last trajectory stages: 11 ``` ## Related [#related] * [Basic Usage](/smartmemory/guides/basic-usage/) * [Performance Tuning](/smartmemory/guides/performance-tuning/) # Sync a local folder (/smartmemory/guides/local-folder-sync) `sm folder sync` reads a folder on your computer and adds its notes to your local SmartMemory. The reading happens on your machine. Nothing is fetched by our servers and no cloud account is involved. ## Before you start [#before-you-start] * SmartMemory running locally (`sm` with the local daemon). The command refuses to run against a remote workspace. * The folder extra: `pip install "smartmemory[folders]"`. It adds the folder reader and PDF text support. ## Run it [#run-it] ```bash sm folder sync ~/Notes --preset markdown sm folder sync ~/Notes --preset markdown --watch 300 # repeat every 300 seconds ``` Each run prints how many files were created, replaced, unchanged, archived and failed. It exits with an error if any file failed. ## Presets [#presets] | Preset | What it reads | | ------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `markdown` | Any folder: `.md`, `.markdown`, `.txt` and `.pdf` files | | `logseq` | A Logseq graph folder: the `pages/` and `journals/` folders only. The `logseq/` config folder and `bak/` backups are skipped | | `apple_notes` | A folder written by an Apple Notes exporter: Markdown, text and PDF files. `attachments` folders are skipped | The Logseq and Apple Notes folder layouts are **unverified** against the vendors' own documentation. The presets follow the layouts those tools are commonly seen to produce. If your export looks different, use the `markdown` preset. Apple Notes has no folder format of its own, so the folder always comes from a third-party exporter. ## What is skipped, always [#what-is-skipped-always] * Files larger than 5 MiB (the size cap). A skipped file is counted and logged by id, never by path. * Hidden files and folders, and common clutter such as `.git`, `node_modules` and temp files. * PDFs when the PDF reader is not installed. * Symbolic links that point outside the folder, and any path that resolves outside it. A hard link to a file elsewhere on disk cannot be detected and is read like any other file. ## Re-runs, edits and deletes [#re-runs-edits-and-deletes] * A file whose text has not changed is not written again, even if you touched it. * An edited file replaces its earlier memory. * A file that disappears is removed from memory only after it is missing in two complete scans in a row. A folder that cannot be read at all (an unmounted drive, a lost permission) never removes anything. * A file that fails is retried on the next run and marked failed for good after three attempts. Progress is saved in a small state file under your SmartMemory data folder, one per synced folder. Only one sync of a folder runs at a time. # Local git history as memory (/smartmemory/guides/local-git-history) The `local_git` connector reads the commit history of a repository on your computer and adds one memory per commit. The reading happens on your machine through the local runner. Nothing is fetched over the network and no account is needed. It covers history. The `git` connector covers the files in the tree, and the code indexer covers symbols. All three agree on the repository name and the commit id, so a commit memory points at the same commit the code index knows. ## Settings [#settings] | Setting | Meaning | | ---------------- | -------------------------------------------------------------------------------------- | | `repo` | The name used in memory ids. Required | | `path` | The checkout folder. Required | | `branch` | The branch to read, `main` by default | | `max_commits` | How many of the newest commits to keep, 5000 by default | | `max_diff_bytes` | Diff text kept per commit, 65536 by default. Zero keeps diffs out | | `blame` | Off by default. When on, records which authors own how many lines of the changed files | ## What a memory holds [#what-a-memory-holds] The commit message, the author and committer names with dates, the parent commits, the changed files with added and deleted line counts, and the diff up to the size limit. Email addresses are not stored. Blame stores line counts per author and never the text of a line. ## What it never does [#what-it-never-does] * It never runs anything the repository configures, such as hooks or filters. * It never accepts a branch or ref that could be read as a git option. File names that look like options are read as plain names, and a file name with a newline is listed but not blamed. * It never writes to the folder you pick, and it reads history only from that folder. At each sync it opens the folder once, refuses it if it is a link, and reads everything after that through the folder it opened. Swapping the folder for a link to another repository during a sync changes nothing it reads. The folders above the one you pick are followed as usual, so pick a path that nobody else can rearrange, and write it as a plain path with no `.` or `..` parts. At each sync it copies the repository's history into a private folder, checks every file as it copies, and runs git only on that copy. The folder must be the top level of an ordinary repository. It is refused when any part of the history it reads is a link or a hard-linked file, when `.git` is a file (linked worktrees and submodules), when it uses alternates of any kind, and when it is a bare repository, a folder inside another repository, uses the reftable format, or holds more than 2 GiB of history. A clone made with `git clone ` shares hard-linked files with its source, so clone with `--no-hardlinks` instead. Shallow clones do not work today. * It never prints a branch, path or message in an error. ## Secrets [#secrets] Keys and passwords that match well known shapes are replaced with a marker such as `[REDACTED:aws_access_key]`. Private key blocks are removed, and the diff of files that look secret (key files, `.env`, credential files) is left out. This is pattern matching and not a guarantee. If a secret has a shape we do not know, it can still reach memory. Set `max_diff_bytes` to zero to keep diffs out entirely. ## Rewritten history [#rewritten-history] Each run reads only the commits after the last one it indexed. If you rebase, amend or force-push so that the last indexed commit is no longer an ancestor, the connector reads the branch again from the top. Rewritten commits are new memories. The old ones are removed only after they are missing in two complete reads in a row, and so are commits that fall outside the `max_commits` window. # MCP Integration Guide (/smartmemory/guides/mcp-integration) The `smart-memory-mcp` package is an MCP server that exposes SmartMemory operations to MCP-compatible clients (Claude Desktop, Cursor, Maya, custom agents). It is a separate package from `smart-memory-core` and ships its own tool catalogue. For the full tool catalogue with signatures, see [`api/tools.md`](/smartmemory/reference/tools/). ## Install [#install] ```bash pip install smartmemory-mcp ``` The console script is `smartmemory-mcp` (registered in `pyproject.toml`): ```bash smartmemory-mcp # stdio transport (default) smartmemory-mcp --http # HTTP transport on 0.0.0.0:8011 smartmemory-mcp --http --port 9100 # custom HTTP port ``` You can also invoke the module directly: `python -m smartmemory_mcp`. ## Backends [#backends] The server can route every tool call to one of two backends; the choice is independent of the tier model. | Backend | When | How it's selected | | ---------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | **Local** | You have `smartmemory` installed locally. No FalkorDB or Redis needed. | Default. Uses the in-process Lite `SmartMemory` (SQLite graph, embedded vector index), see [Lite Mode](/smartmemory/guides/lite-mode/). | | **Remote** | You want to talk to the managed SmartMemory Service. | `smartmemory setup --mode remote` (from the desktop app), or set `SMARTMEMORY_API_KEY` and ensure `smartmemory_app` config has `mode=remote`. | `whoami()` reports the active backend. ## Tiers [#tiers] Tools are registered into three tiers at server startup. Restarting the server is required after any tier-changing event (login, env var change). | Tier | Tools (cumulative) | Unlock | | -------- | ------------------ | ----------------------------------------------- | | **FREE** | 26 | Default — no auth required | | **PRO** | 58 (+32) | `login(api_key)` (or `SMARTMEMORY_API_KEY` env) | | **PRO+** | 89 (+31) | PRO **and** `SMARTMEMORY_MCP_FULL_TOOLS=true` | ### FREE [#free] `login`, `whoami`, `switch_workspace`, `memory_ingest`, `memory_search`, `memory_recall` (deprecated), `get_working_context`, `memory_get`, `memory_export`, `memory_import`, `memory_migrate`, `memory_auto`, `memory_feedback`, and the full **decisions** suite (`decision_create`, `decision_get`, `decision_list`, `decision_search`, `decision_supersede`, `decision_retract`, `decision_reinforce`, `decision_contradict`, `decision_provenance`, `decision_find_conflicts`, `decision_create_pending`, `decision_resolve_requirement`, `decision_try_activate`). Decisions need no API key, the same as the Python SDK. ### PRO [#pro] PRO unlocks PRO-tier `memory_*` (CRUD: `memory_add`, `memory_update`, `memory_delete`, `memory_clear`, `memory_list`, `memory_stats`, `memory_distill`, `memory_ingest_conversation`, `memory_search_advanced`, `memory_search_by_metadata`) and the **code indexing**, **anchors**, **plans / failure recall**, **agent profiles**, and **structured ingestion** suites. ### PRO+ [#pro-1] PRO+ adds **reasoning**, **evolution**, **insights**, **dev workflow**, and **zettelkasten** suites. See [`api/tools.md`](/smartmemory/reference/tools/) for the complete per-tier list with signatures. ## Authentication [#authentication] Three sources are checked in order — the first non-empty value wins: 1. `SMARTMEMORY_API_KEY` environment variable. 2. The `smartmemory_app` desktop keyring (if installed). 3. The fallback file `~/.config/smartmemory/.api_key` (written by the `login` tool). Calling the `login` tool stores the key into the file fallback and validates it against the API. The server only re-reads credentials at startup, so: ```text restart the MCP server after login() ``` is mandatory. `switch_workspace(workspace)` takes a workspace id or name and switches the current session at once, with no restart. The next request uses the new workspace, and the service still checks that you are a member. A refused switch tells you which workspace is still in use. Pass `save_as_default=True` to also write it into the `smartmemory_app` config for new sessions. Without the desktop app installed, set `SMARTMEMORY_WORKSPACE_ID` instead. In local mode the only workspace is `default`. Listing your workspaces needs an API key with the `read:teams` scope. If your key lacks it, `switch_workspace` says so and leaves you in your current workspace. Create a key with Read teams enabled and use it instead. A new session starts in the first workspace that is set, in this order: 1. `SMARTMEMORY_WORKSPACE_ID` 2. `SMARTMEMORY_TEAM_ID` (the older name, still supported) 3. The workspace saved with `save_as_default=True` 4. Your account's default workspace The order is the same with or without the `smartmemory` package installed. If both variables are set to different workspaces, the server logs one warning that names both. An environment variable wins over a saved default, so `save_as_default=True` tells you when one is set. `whoami` shows your user, tenant and workspace. Your tenant comes from your credential and is never a tool argument. Deprecated: `switch_team(team_id)` still works for one more release as an alias of `switch_workspace`. It never saves a default. ## Client configuration [#client-configuration] ### Claude Desktop / Cursor [#claude-desktop--cursor] In the MCP client's config file (e.g. `claude_desktop_config.json`): ```json { "mcpServers": { "smartmemory": { "command": "smartmemory-mcp", "env": { "SMARTMEMORY_API_KEY": "sm_live_...", "SMARTMEMORY_MCP_FULL_TOOLS": "true" } } } } ``` ### HTTP transport [#http-transport] For multi-process deployments or browser clients, run in HTTP mode: ```bash smartmemory-mcp --http --port 8011 ``` Then point your MCP client at `http://localhost:8011`. ## Calling a tool [#calling-a-tool] Standard MCP `tools/call`: ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "memory_search", "arguments": { "query": "auth caching", "top_k": 5, "multi_hop": true } } } ``` All tools return strings, except `get_working_context` which returns a structured surfacing-response dict. ## Hosted MCP [#hosted-mcp] For the managed SmartMemory Service there is also a hosted MCP server at `https://mcp.smartmemory.ai/mcp`. You run nothing yourself: each call is authenticated with OAuth through your SmartMemory account or with a SmartMemory API key, and runs as that caller. It exposes a fixed allowlist of tools rather than the full local tool catalogue. See [Hosted MCP Server](/smartmemory/guides/hosted-mcp/) for setup. ## Troubleshooting [#troubleshooting] * **PRO tools missing after `login`** — restart the MCP server. Tier is resolved once at startup. * **`whoami` reports `Backend: remote (smartmemory package not installed)`** — install `smartmemory` for local backend, or this is expected if you intend remote-only. * **Tools time out**: with the local backend, the first call can wait on loading the local models. Run `sm warm` once to preload them. With the remote backend, confirm the API URL is reachable. * **Permission warning on `~/.config/smartmemory/.api_key`** — the loader logs a warning if the file is group/other-readable. `chmod 600` it. ## See also [#see-also] * [MCP Tools API](/smartmemory/reference/tools/) — every tool with its signature. * [SmartMemory API](/smartmemory/reference/smart-memory/) — Python API the tools wrap. # Memory Operations (/smartmemory/guides/memory-operations) # Memory Operations [#memory-operations] Auxiliary surfaces beyond `add` / `ingest` / `search`. Each is a real SmartMemory route family with its own SDK methods. ## Anchors — anti-drift requirements [#anchors--anti-drift-requirements] An *anchor* is a structured commitment (spec, scope, decision, constraint) that the agent should not violate. The drift checker compares recent outputs against active anchors and surfaces the ones that may have been broken. ```bash # REST POST /memory/anchors/create GET /memory/anchors/list?session_id=...&anchor_type=spec DELETE /memory/anchors/clear?session_id=... POST /memory/anchors/graduate POST /memory/anchors/check-drift ``` CLI mirror: ```bash smartmemory-core anchor "Retries must be idempotent" --type constraint --session sprint-42 smartmemory-core anchors --session sprint-42 smartmemory-core anchor-clear --session sprint-42 ``` The drift route accepts an array of recent output strings and returns each anchor with a `drift_score` and `drift_detected_at`. Use `graduate` to promote a satisfied anchor to long-term memory once the work is done. ## Feedback — explicit recall reinforcement [#feedback--explicit-recall-reinforcement] After surfacing recalled items, send back whether they were `helpful`, `misleading`, or `neutral`. The route bumps each item's `retention_score` and, on `helpful` outcomes with multiple items, strengthens `CO_RETRIEVED` edges between every pair (Hebbian co-retrieval). ```js await client.memories.feedback( ['item_123', 'item_456', 'item_789'], 'helpful', 'why did we pick FalkorDB?', ); // → { updated: 3, edges_strengthened: 3, outcome: 'helpful' } ``` | Outcome | `retention_score` delta | Co-retrieval edges | | ------------ | ----------------------- | ------------------ | | `helpful` | `+0.15` | Strengthened (+2) | | `misleading` | `-0.10` | Untouched | | `neutral` | `0` | Untouched | The boost (`+2`) intentionally outweighs implicit co-retrieval (which increments by 1) so explicit user signal wins. ## Failures journal — episodic memory of bugs [#failures-journal--episodic-memory-of-bugs] Log failures so the agent doesn't repeat them. Each failure becomes an episodic memory with `origin="failure:agent"`. ```bash POST /memory/failures/log { "error_type": "TimeoutError", "content": "Connection timed out after 30s", "context": "Pulling Docker image x/y:tag", "plan_id": "plan_abc", # optional "task_id": "task_42", # optional "attempted_fix": "Increased pull timeout to 60s", "resolution": "Switched mirror" } POST /memory/failures/check { "error_type": "TimeoutError", "context": "Pulling Docker image", "top_k": 3 } # → ranked prior failures with the same shape ``` ## Agents — first-class agent users [#agents--first-class-agent-users] Agents are users with API keys instead of passwords. Use them to give an autonomous worker its own memory partition and audit trail. ```bash POST /memory/agents # create GET /memory/agents # list GET /memory/agents/{id} DELETE /memory/agents/{id} GET /memory/agents/{id}/recall-profile PUT /memory/agents/{id}/recall-profile ``` `agent_config` is a free-form dict (model, capabilities, tool list). `recall-profile` controls which memory types and origins the agent sees on search. Agents can be assigned to teams via the standard team APIs. ## Links — explicit relations [#links--explicit-relations] Beyond the relations the pipeline extracts, you can create explicit links: ```bash POST /memory/link # link two memory items { "source_id": "...", "target_id": "...", "link_type": "RELATED" } POST /memory/edge # create a typed graph edge with properties GET /memory/{item_id}/links GET /memory/{item_id}/neighbors ``` `POST /memory/link` validates ownership of both endpoints — you can only link items inside your active workspace. `POST /memory/edge` is the lower-level form that lets you set arbitrary edge properties (used by Studio for manual graph curation). ## Archive — conversation artifacts [#archive--conversation-artifacts] Persist a JSON conversation payload outside the graph for compliance / replay use. Returns a content-hash-addressable URI. ```bash POST /memory/archive/store { "conversation_id": "conv_abc", "payload": { "turns": [...] }, "metadata": { "source": "maya" } } # → { archive_uri: "...", content_hash: "..." } GET /memory/archive/{archive_uri} ``` The archive lives in MongoDB; the graph row only carries the URI. ## Clustering — entity deduplication [#clustering--entity-deduplication] When ingestion produces duplicate entities (e.g. "John Smith" / "J. Smith"), run clustering to merge them by vector similarity: ```bash POST /memory/clustering/run?distance_threshold=0.1&dry_run=true GET /memory/clustering/stats ``` `dry_run=true` previews clusters without merging. Lower `distance_threshold` is stricter (fewer merges); 0.1 is the default. Always `dry_run` first on a new workspace to verify the merges look right. ## Analytics — bias, origins, drift [#analytics--bias-origins-drift] `/memory/analytics/*` exposes a few diagnostic views over the workspace: | Endpoint | Returns | | ------------------------------- | ------------------------------------------------------------- | | `GET /memory/analytics/status` | Whether analytics is enabled + which dependencies are present | | `GET /memory/analytics/origins` | Counts grouped by origin / origin tier | | `GET /memory/analytics/drift` | Drift indicators across recent ingests | | `POST /memory/analytics/bias` | Bias analysis (requires `scikit-learn`) | Bias analysis requires the optional `scikit-learn` dependency — if it's absent, the route 503s with a helpful message. ## See also [#see-also] * [JavaScript SDK](/smartmemory/guides/javascript-sdk/) — typed wrappers for these routes * [Decisions lifecycle walkthrough](/smartmemory/examples/decisions-lifecycle/) # Migration & Upgrade (/smartmemory/guides/migration-and-upgrade) Some SmartMemory releases rename or remove public APIs. Here are the renames and removals you'll encounter when upgrading from earlier versions, with the fix for each. Check the release history for [`smartmemory`](https://pypi.org/project/smartmemory/#history) and [`smartmemory-core`](https://pypi.org/project/smartmemory-core/#history) on PyPI before upgrading. ## CORE-MEMORY-DYNAMICS-1 M1b — `working` → `pending` (0.7.x) [#core-memory-dynamics-1-m1b--working--pending-07x] The vestigial `memory_type="working"` bucket was renamed across the codebase. Most callers don't notice — the `ConsolidationRouter` now handles routing at ingest time — but a few public surfaces moved. ### What changed [#what-changed] | Before | After | | -------------------------------------------------------- | ---------------------------------------------------------- | | `MemoryType.WORKING` | `MemoryType.PENDING` | | `memory_type="working"` | `memory_type="pending"` | | `WorkingMemory` class | `PendingMemory` class | | `MemoryFactory.create_working_memory(...)` | `MemoryFactory.create_pending_memory(...)` | | `get_working_memory()` | `get_pending_memory()` | | `SmartMemory.commit_working_to_episodic()` | **removed** (router handles it) | | `SmartMemory.commit_working_to_procedural()` | **removed** (router handles it) | | `WorkingToEpisodicEvolver`, `WorkingToProceduralEvolver` | **deleted** | | Config section `pipeline.working` | `pipeline.pending` (legacy key still read for one release) | ### How to migrate [#how-to-migrate] ```bash # Find references grep -rn 'memory_type="working"\|MemoryType\.WORKING\|create_working_memory' . # Rename sed -i '' 's/MemoryType\.WORKING/MemoryType.PENDING/g' your_code/*.py sed -i '' 's/memory_type="working"/memory_type="pending"/g' your_code/*.py ``` If you were calling `commit_working_to_*()`, drop those calls — the `ConsolidationRouter` (added in M1a) now performs at-ingest routing. Inspect `PipelineState.router_decision` if you need the routing reason. **Data migration:** none. The audit at rename time showed zero stored `memory_type="working"` rows in production. Hardcoded `"working"` queries return empty results until you rename them to `"pending"`. ## Package names: `smartmemory` and `smartmemory-core` (0.5.0) [#package-names-smartmemory-and-smartmemory-core-050] In 0.5.0 the library's PyPI distribution was renamed from `smartmemory` to `smartmemory-core`. The Python import name is unchanged: ```python from smartmemory import SmartMemory ``` Today the package you install is `smartmemory`. It provides the `smartmemory` and `sm` commands and Lite mode, and it pins the exact matching `smartmemory-core` version, so installing or upgrading `smartmemory` brings the right library with it: ```bash pip install -U smartmemory ``` ## `add()` / `ingest()` swap (0.4.x) [#add--ingest-swap-04x] Earlier versions overloaded `add()`. Method names now align with intent: | Method | Behaviour | | ------------------------------ | -------------------------------------------------------- | | `SmartMemory.ingest(content)` | Full pipeline (extract → store → link → enrich → evolve) | | `SmartMemory.add(memory_item)` | Simple storage (normalize → store → embed) | If you were calling `add()` for full ingestion, switch to `ingest()`. If you were calling `_add_basic()` (private) for plain writes, use the public `add()`. The legacy `ingest_old()` and async-queueing path was removed — asynchronous ingestion now goes through `ingest(sync=False)` / `ingest_batch()`. ## `SmartMemory.__init__` — `scope_provider` parameter (0.3.x) [#smartmemory__init__--scope_provider-parameter-03x] `SmartMemory()` no longer takes hardcoded scoping kwargs. Pass a `ScopeProvider` instance (or rely on the default for single-tenant use): ```diff - memory = SmartMemory(user_id="u", workspace_id="w") + memory = SmartMemory() # single-tenant + memory = SmartMemory(scope_provider=my_scope) # multi-tenant service ``` The service layer's `SecureSmartMemory` already does this for you — no change needed in route handlers. ## `PipelineState.simplified_sentences` (Pipeline v2) [#pipelinestatesimplified_sentences-pipeline-v2] `simplified_text: Optional[str]` was replaced with `simplified_sentences: List[str]` to support multi-sentence simplification. ```diff - text = state.simplified_text or item.content + text = " ".join(state.simplified_sentences) if state.simplified_sentences else item.content ``` ## Ontology label unification (ONTO-RECONCILE-1, ongoing) [#ontology-label-unification-onto-reconcile-1-ongoing] Legacy graph labels `:EntityType`, `:Concept`, `:RelationType` are being unified onto `:OntologyType` and `:OntologyRelation`. The migration is idempotent and runs in passes — it auto-pauses ingest while it's mid-flight. End user impact: * During the maintenance window, `ingest()` raises `IngestPaused`. Catch and retry with backoff. * After Task 11 ships, raw Cypher queries that match `:EntityType` / `:RelationType` will return zero rows; switch to `:OntologyType` / `:OntologyRelation`. If you have custom Cypher queries, audit them before pulling the next core release. ## `EventStream.read_all(limit)` removed (0.9.x) [#eventstreamread_alllimit-removed-09x] `read_all()` was deprecated in 0.9.x and has since been **removed** — the method no longer exists on `EventStream`. Use `read_recent(count)`, which pushes the bound to Redis instead of slicing in Python and prevents worker starvation on large streams. ```diff - events = stream.read_all(limit=100) + events = stream.read_recent(count=100) ``` ## Upgrade procedure [#upgrade-procedure] 1. Check the release history from your current version forward. 2. Upgrade the package. `sm update` checks PyPI, installs the latest `smartmemory`, and restarts the running daemon. `pip install -U smartmemory` also works. Either way the matching `smartmemory-core` comes with it. 3. Run your own test suite against the new version before rolling it out. 4. In Service mode, SmartMemory upgrades the managed API. You only upgrade the client packages you use, such as `smartmemory` or `@smartmemory/sdk-js`. ## See also [#see-also] * [Troubleshooting](/smartmemory/guides/troubleshooting/): for the legacy-string error mode # Multi-Hop Retrieval (/smartmemory/guides/multi-hop-retrieval) {/* Features: RLM-1c (heuristic), CORE-MULTIHOP-2 (semantic) · Code: `smartmemory/search/multi_hop.py` */} A single search pass answers questions where the query already names the answer. *"What did we decide about JWT?"* lexically matches the decision item. But many real questions require **chaining**: the answer to the first hop tells you what to ask next. *"Why did we change auth?"* needs to find what auth was changed to before it can find the rationale. Multi-hop recursive retrieval handles these cases. SmartMemory ships two heuristic strategies (`consensus` and `relevance`) and an LLM-driven `semantic` planner. Both are opt-in on the standard `search()` call. ## Enabling multi-hop [#enabling-multi-hop] ```python from smartmemory.models.memory_item import MemoryItem from smartmemory.pipeline.config import PipelineConfig from smartmemory.tools.factory import lite_context lite = dict(pipeline_profile=PipelineConfig.lite(llm_enabled=False)) with lite_context(**lite) as memory: memory.add( MemoryItem( content="The auth migration moved sessions to JWT tokens.", memory_type="semantic", metadata={ "entities": [ {"text": "auth migration", "label": "PROJECT"}, {"text": "JWT tokens", "label": "TECH"}, ], }, ) ) memory.add( MemoryItem( content="JWT tokens required rotating the cache invalidation key.", memory_type="semantic", metadata={ "entities": [ {"text": "JWT tokens", "label": "TECH"}, {"text": "cache invalidation", "label": "SYSTEM"}, ], }, ) ) results = memory.search( "How did the auth migration affect cache invalidation?", top_k=5, multi_hop=True, max_hops=2, budget_ms=1500, ) for result in results: hop = result.metadata.get("hop_n", 0) print(f"hop={hop} {result.content}") ``` The default planner is **consensus**. Pass `hop_strategy="relevance"` to follow one-off best-result bridges, or `hop_strategy="semantic"` for LLM planning. After each hop, it extracts the top-scoring entities from result metadata and uses the highest-novelty entity names to construct follow-up queries. Follow-ups are searched in parallel and merged with cross-hop weighted RRF. Later hops are weighted by the hop-decay factor, which defaults to `0.7` per hop. The chain stops when any of these hold: * `max_hops` is reached. * The remaining time budget drops below `MIN_HOP_BUDGET_MS` (100 ms in `smartmemory/search/multi_hop.py`). * The hop planner finds no novel entity above `MIN_ENTITY_SCORE` (0.3) to follow. `multi_hop=True` is incompatible with `memory_type="pending"` and raises `ValueError`. It is compatible with `decompose_query=True` and with `origin=` or `exclude_origins=` filters. ## Semantic hop planning [#semantic-hop-planning] The heuristic planner is fast but blind to user intent. The semantic planner asks an LLM to reason about which entity to follow given the original query and the current results. ```python from smartmemory.search.multi_hop import SemanticHopConfig config = SemanticHopConfig( enabled=True, budget_ms=500, max_output_tokens=200, ) print(config.enabled, config.budget_ms, config.max_output_tokens) ``` Use `semantic_hops=True` on an individual `search()` call when an LLM key is configured. If the LLM call times out or fails, SmartMemory falls back to the heuristic planner for that hop and logs a warning. ## Combining with query decomposition [#combining-with-query-decomposition] `multi_hop` and `decompose_query` solve different problems and compose: * **`decompose_query=True`** splits a multi-topic query into independent sub-queries, runs each, and RRF-merges the results. * **`multi_hop=True`** chains follow-up queries based on what each hop returns. Combine them when the query has multiple topics that each need follow-up: ```python from smartmemory.models.memory_item import MemoryItem from smartmemory.pipeline.config import PipelineConfig from smartmemory.tools.factory import lite_context lite = dict(pipeline_profile=PipelineConfig.lite(llm_enabled=False)) with lite_context(**lite) as memory: memory.add( MemoryItem( content="The auth migration moved sessions to JWT tokens.", memory_type="semantic", ) ) memory.add( MemoryItem( content="JWT tokens required rotating the cache invalidation key.", memory_type="semantic", ) ) results = memory.search( "How did the auth migration affect cache invalidation?", top_k=10, decompose_query=True, multi_hop=True, max_hops=2, budget_ms=3000, ) print([result.content for result in results]) ``` When both are enabled, decomposition runs first. Each decomposed sub-query then becomes the seed of its own multi-hop chain. Budget is shared across all chains. ## Live progress [#live-progress] Multi-hop search emits live progress events on the [progress event bus](/smartmemory/reference/progress-events/). Each hop fires a `search.hop` event with `payload.hop_n`, `payload.queries`, and the per-hop result count. The first event in a multi-hop run also emits a `pipeline.dag` event describing the planned chain. Subscribe from a UI: ```javascript import { subscribeProgress } from '@smartmemory/sdk-js/progress'; const sub = subscribeProgress({ runId, token, onEvent: (e) => { if (e.kind === 'search.hop') { console.log(`Hop ${e.payload.hop_n}:`, e.payload.queries); } }, }); ``` Or replay a completed run from the server side using `?run_id=&from_seq=0`. ## Tuning [#tuning] | Knob | Default | When to change | | --------------- | ------- | --------------------------------------------------------------------------------------------------- | | `max_hops` | 3 | Bump to 4 or 5 for "explain why" and forensic queries. Drop to 2 for latency-sensitive paths. | | `budget_ms` | 1500 | Bump to 3000+ when combining with `decompose_query`. Drop to 800 if you only hit hop 1 or 2. | | `semantic_hops` | `False` | Turn on for high-value chats where the heuristic picks the wrong entity. Adds about 500 ms per hop. | | `top_k` | 5 | Multi-hop returns the merged top-K. Bump for analytics, leave low for chat context. | The hop-decay value and minimum hop budget are constants in `smartmemory/search/multi_hop.py`. ## Related [#related] * [Hybrid Retrieval](/smartmemory/concepts/hybrid-retrieval/), single-hop retrieval mechanics * [Memory Dynamics](/smartmemory/concepts/memory-dynamics/), how activation interacts with multi-hop ranking * [Progress Events](/smartmemory/reference/progress-events/), `search.hop` event shape # Obsidian plugin (/smartmemory/guides/obsidian-plugin) The SmartMemory plugin runs inside Obsidian and sends your markdown notes to your SmartMemory workspace. You install it in Obsidian. There is no cloud storage link to set up. Once notes are in, you can search and recall them from Obsidian, see the entities SmartMemory found in a note, and browse your workspace graph in a pane. ## Before you start [#before-you-start] You need a SmartMemory account and an API key, unless you use Local mode (see below). Create the account at [www.smartmemory.ai](https://www.smartmemory.ai) and copy a key from the API Keys section of your settings. ## Install [#install] The plugin is not in Obsidian's Community plugin directory yet. Install it with one of these two routes. **With BRAT:** 1. Install the BRAT plugin from Obsidian's Community plugins. 2. Open the BRAT settings, choose **Add Beta Plugin**, paste `smartmemory/smart-memory-obsidian`, and select **Add**. 3. Enable **SmartMemory** under **Settings → Community plugins**. **Manually:** 1. Download `main.js`, `manifest.json`, and `styles.css` from the [latest release](https://github.com/smartmemory/smart-memory-obsidian/releases/latest). 2. Copy them into `/.obsidian/plugins/smartmemory/`. 3. Enable **SmartMemory** under **Settings → Community plugins**. The plugin needs Obsidian 1.5.0 or later. It is not limited to desktop. ## Connect it to your workspace [#connect-it-to-your-workspace] The first time the plugin loads, it opens a **Welcome to SmartMemory** window. Pick one of its options: **Open settings**, **Open signup**, **Connect to local daemon**, or **I'll set this up later**. Until you do, the plugin turns **Auto-ingest on save** and **Auto-ingest on create** back on each time it loads, even if you turned them off. Closing the window without a choice does not count, and the window opens again the next time Obsidian loads. 1. In Obsidian, open **Settings → SmartMemory**. 2. Paste your API key into **API key**. Text fields save a moment after you stop typing, or when you leave the settings tab. If Obsidian can see a `SMARTMEMORY_API_KEY` environment variable, it takes precedence over the field. 3. Select **Test** next to **Test connection**. The status bar dot is green when the plugin is connected, orange while it is syncing, and red when it is disconnected. Leave **Workspace ID override** (under **Advanced**) blank unless you belong to several workspaces and want a specific one. When it is blank and you use the hosted service, the plugin asks the API for your default workspace and saves it. The plugin stores your API key in plain text in its `data.json` file, inside `/.obsidian/plugins/smartmemory/`. If you sync your vault with git, add that file to `.gitignore`. ## Bring your notes in [#bring-your-notes-in] There are three ways to send notes. ### Ingest one note [#ingest-one-note] Open the note and run the command palette entry **SmartMemory: Ingest current note**. ### Ingest a folder [#ingest-a-folder] Run **SmartMemory: Ingest folder of current note**. It sends the markdown notes in the folder of the note you have open, and in its subfolders, apart from notes inside your **Exclude folders** list. Run it from a note at the top level of your vault to send the whole vault apart from those folders. When it finishes, it reports how many notes were ingested, how many failed, and how many were skipped because they are empty or in an excluded folder. ### Let auto-ingest do it [#let-auto-ingest-do-it] **Auto-ingest on save** and **Auto-ingest on create** are on by default. With **Auto-ingest on save** on, the plugin sends a markdown note after you change it. With **Auto-ingest on create** on, it sends a new markdown note after you create it. Each send waits for a pause, five seconds by default, so the plugin does not send on every keystroke. Changes made while a folder ingest is running are not sent automatically. Notes in excluded folders are never sent automatically. To stop automatic sends, turn both settings off after you have made a choice in the welcome window. ## What gets sent [#what-gets-sent] * **The note's text.** The plugin sends the text below the frontmatter block, the note's path in your vault, and an origin of `import:obsidian`. If a note has no text below its frontmatter, it is not sent. Automatic ingest skips it quietly and writes a console message that names the note, and **Ingest current note** shows a "note is empty" notice. * **An unclosed frontmatter block is sent as text.** If the block at the top of a note starts with `---` but has no closing `---` line, the plugin cannot separate it from the text, and sends the whole note, frontmatter included. * **Frontmatter context, each time the plugin can read it.** On each send, the plugin tries to read the frontmatter as OKF (see [OKF Portability](/smartmemory/guides/okf-portability/)). When that works, the plugin sends the frontmatter fields with the note as context, including fields SmartMemory does not use, such as a private field you added. Whether it works depends on what the frontmatter contains, and a field the plugin writes back does not guarantee it. Treat everything in the frontmatter of a note the plugin sends as sent. * **An unchanged note is not sent again.** If a note's text has not changed since its last send and the plugin still has its link to the earlier memory, it asks the API whether the earlier memory still exists, and sends the note again if that memory is gone. * **An edited note replaces its earlier memory.** When the text has changed, the plugin deletes the memory it made before and sends the new version. If the delete fails, the new version is still sent, so an older copy can remain. * **Deleting or renaming a note in Obsidian does not delete its memory.** The plugin updates or drops its own link to the note. ## What the plugin writes into your notes [#what-the-plugin-writes-into-your-notes] * **Keep notes OKF-conformant** is on by default. After a note is sent, the plugin writes a `resource` field that links the note to its SmartMemory item, and a `type` field. The `type` is the memory type SmartMemory reports, or your existing `type`, or `semantic` if there is none. This can replace a `type` you wrote yourself. * **Write SmartMemory OKF extension** is off by default. When it is on, the plugin adds the entities SmartMemory extracted and graph edges to a `smartmemory` block in the note. This happens after **Ingest current note**, after an automatic send, and after **SmartMemory: Enrich current note**. Folder ingest does not do it. This setting does not depend on **Keep notes OKF-conformant**. * **Fields from an earlier plugin version are rewritten whatever the settings say.** If a note has frontmatter fields from an earlier version of the plugin, such as `smartmemory_id` or `smartmemory_entities`, the plugin rewrites them into the current fields when Obsidian loads, when it first learns your workspace, and when it writes its fields to that note. Notes in excluded folders are left as they are. A `smartmemory_id` becomes a `resource` field once the plugin knows your workspace. Until then it stays as it is. * **Auto-link changes a note when you accept.** **SmartMemory: Auto-link entities in current note** shows a preview. The links you accept are written into the note and also recorded as edges in its `smartmemory` block, whichever way the extension setting is. No setting stops the plugin from editing your notes in every case. ## Exclude folders [#exclude-folders] **Update the plugin to version 0.2.18 or later** for the behavior below. In earlier versions, **Exclude folders** applied only to the folder command, and auto-ingest, **Ingest current note**, recall, inline suggestions and the startup rewrite of older plugin fields still reached excluded notes. **Exclude folders** lists folder path prefixes, and it starts as `templates/, .obsidian/`. Notes inside an excluded folder are left alone by the whole plugin. They are not sent by **Ingest current note**, by the folder command, or by auto-ingest. The plugin does not write its frontmatter fields into them, does not enrich them, and does not use their text for recall queries or inline suggestions. Their note panels show "This note is in an excluded folder" instead of results, and the plugin does not insert links to them or from them. Each skip is written to the developer console with the operation and the note's path, and the commands you run yourself show a notice that the note is in an excluded folder. How an entry matches: * Prefixes are matched against the note's path in your vault. Capital letters do not matter, and `/` and `\` count as the same separator. * An entry with a trailing `/` matches only that folder and its subfolders. An entry without one, such as `private`, also matches sibling names that start with it, such as `private-notes/`. Add the `/` to avoid that. * Entries are kept exactly as you type them. Entries such as `.`, `..` or `Private/..` do not exclude your whole vault. Excluding a folder does not delete anything. Memories the plugin already made from those notes stay in SmartMemory, and **Danger: purge all Obsidian-origin memories** still deletes them. Run that command only if you mean to. ## Local mode [#local-mode] The **Mode** setting can point the plugin at a SmartMemory daemon on your own machine instead of the hosted service. The default address is `http://127.0.0.1:9014`. In that mode the API key field is hidden and no key is needed. ## Graph pane [#graph-pane] **SmartMemory: Open knowledge graph pane** opens a pane where you browse your workspace graph. It shows your workspace, not the note you have open. Some actions in this pane change your SmartMemory data. The **Ground** button on an entity looks up that entity's name on Wikipedia. **Update the plugin to version 0.2.17 or later.** In earlier versions, pressing Delete or Backspace while nodes were selected in the graph deleted them from SmartMemory with no confirmation, even while you were typing in a note. Version 0.2.17 fixes this. Keys you press in a note or in any text field no longer reach the graph, and Delete and Backspace do not delete anything from this pane. ## Commands that delete SmartMemory data [#commands-that-delete-smartmemory-data] **SmartMemory: Danger: purge all Obsidian-origin memories from this workspace** deletes every memory whose origin starts with `import:obsidian`. It also removes the `resource` and `smartmemory` fields and the older `smartmemory_*` fields from every markdown note in your vault, except notes in excluded folders. The plugin forgets its links to those notes but does not edit them. **SmartMemory: Purge empty-content memories (legacy YAML-only ingests)** deletes every memory in your workspace that has no text, whatever app created it. Neither command deletes a note file. ## Privacy [#privacy] * The plugin's requests to SmartMemory go to the API URL in its settings, `https://api.smartmemory.ai` by default, or to the daemon address in Local mode. * The **Ground** button in the graph pane sends an entity's name to Wikipedia. Some buttons, such as upgrade and signup, open a page in your browser. * The plugin keeps a local `data.json` in its plugin folder. It holds your settings, a map between note paths and SmartMemory items, a content hash for each sent note, and a map from entity names to the notes for them. While Obsidian is open, the plugin talks to your workspace in the background, without a command from you. This includes: * connection and health checks when the plugin loads and when you change settings * a lookup of your default workspace when **Workspace ID override** is blank * checks on the note you are viewing when it is linked to a SmartMemory item: when Obsidian loads, when you switch to it, and on a timer, hourly by default. **Show supersession banner** turns these off. * automatic sends, as described above * reads after a single or automatic send, to pick up what SmartMemory found in the note * requests from panes you have open, for what they show Requests also happen when you use a feature. The search pane sends the text you type. **SmartMemory: Recall related memories for current note** sends your selected text, or the whole note file, frontmatter included, if nothing is selected. It does nothing for a note in an excluded folder. If you turn on **Enable inline suggestions**, which is off by default, the plugin sends the paragraph you are typing as a search query, unless the note is in an excluded folder. ## Troubleshooting [#troubleshooting] * **The status bar dot is not green.** Open **Settings → SmartMemory**, check the API key and API URL, and select **Test**. * **Ingest says it failed.** **Ingest current note** shows an "ingest failed" notice with the error text. Auto-ingest shows an "auto-ingest failed" notice, and folder ingest counts the note as failed. Fix the cause and run the command again. * **You see an upgrade message.** Your workspace has reached a limit. For the note limit, **Ingest current note** opens a window titled "Free tier note limit reached". A daily limit shows a "daily limit reached" notice. * **Auto-ingest turned itself back on.** Make a choice in the welcome window (see Connect it to your workspace), then turn it off again. * **Questions or bugs.** Open an issue at [github.com/smartmemory/smart-memory-obsidian/issues](https://github.com/smartmemory/smart-memory-obsidian/issues). # OKF Portability (/smartmemory/guides/okf-portability) # OKF Portability [#okf-portability] SmartMemory can export a workspace as an OKF bundle and import that bundle into another workspace. OKF is the Google Open Knowledge Format, a portable memory format built from Markdown files and YAML frontmatter. SmartMemory writes v0.2 bundles and still imports v0.1 bundles. Each exported bundle contains OKF pages for the workspace. The files are suitable for portable storage, transfer, and an Obsidian-native round trip. SmartMemory preserves its `smartmemory` extension data alongside the standard OKF fields so it can restore the behavior-bearing memory details that OKF alone cannot represent. ## Declared type portability [#declared-type-portability] A v0.2 bundle carries the workspace's declared class and relation declarations in a separate section, alongside the items. This matters when a workspace uses [declared memory types](/smartmemory/guides/declaring-memory-types/): without the declarations, items of a declared class would be refused on import into a workspace that has never heard of that class. Imports are staged. If a declaration in the bundle conflicts with one already present in the destination workspace, the whole import is refused rather than merged silently, so an import can never partially reshape an existing schema. Bundles at v0.1 carry items only and continue to import unchanged. ## Export a workspace [#export-a-workspace] Use the service endpoint to download a compressed bundle for the active workspace: ```text GET /memory/okf/export ``` The endpoint accepts optional `memory_type` and `limit` query parameters. Its response is a `tar.gz` download containing the OKF bundle. The CLI provides the same operation: ```bash sm export ``` ## Import a bundle [#import-a-bundle] Upload an OKF bundle to the destination workspace: ```text POST /memory/okf/import ``` The request uses a multipart `file` field. The response reports the number of pages imported, the number that failed, and the destination workspace ID. The CLI mirror is: ```bash sm import ``` Imports are copies into the destination workspace. Cross-workspace resources are rebound to the destination so a bundle does not retain another workspace's item identity. ## Obsidian-native round trip [#obsidian-native-round-trip] The SmartMemory Obsidian integration reads and writes the same OKF Markdown and YAML representation. You can export a workspace, keep or edit the pages in an Obsidian vault, and import the bundle again without converting it through a proprietary vault-only format. Unknown OKF fields are retained during a SmartMemory round trip when they are not promoted into SmartMemory fields. Formatting can change because the codec preserves document meaning rather than exact YAML layout. ## Decisions on import [#decisions-on-import] Decision pages import as fresh decisions in the destination workspace. Decision identity is item-addressed, so a cross-workspace import does not preserve supersession chains or evidence links. Treat an imported decision as a new destination-workspace decision, then recreate any required relationships there. # Ontology Management (/smartmemory/guides/ontology-management) SmartMemory includes an ontology management system that helps structure and organize knowledge for better extraction, validation, and retrieval. This guide covers how to define, manage, and leverage ontologies in your agentic memory system. ## Overview [#overview] An ontology in SmartMemory defines: * **Entity types**: The kinds of entities the system recognizes (e.g. `algorithm`, `person`, `organization`), including their properties and inheritance hierarchy * **Relationship types**: How entity types may relate to each other, with allowed source/target types * **Rules**: Validation, enrichment, and inference rules attached to the ontology * **Constraints**: Required properties and per-property constraints (type, cardinality, soft/hard enforcement) The runtime surface for building and managing ontologies is the `OntologyManager` class plus the `Ontology` model and its type-definition dataclasses. The correct imports are: ```python from smartmemory.ontology.manager import OntologyManager from smartmemory.ontology.models import ( Ontology, EntityTypeDefinition, RelationshipTypeDefinition, OntologyRule, PropertyConstraint, ) ``` ## OntologyManager API [#ontologymanager-api] `OntologyManager` is the entry point for ontology lifecycle operations. Its public methods are: | Method | Purpose | | ------------------------------------------------------------------------------- | -------------------------------------------------------- | | `create_ontology(name, domain="", description="")` | Create and persist a new empty `Ontology` | | `load_ontology(ontology_id)` | Load an ontology by ID (returns `Ontology` or `None`) | | `set_active_ontology(ontology)` | Set the ontology used for extraction guidance | | `get_active_ontology()` | Return the currently active ontology (or `None`) | | `infer_ontology_from_extractions(extraction_history, ontology_name="inferred")` | Build an ontology from historical extraction patterns | | `migrate_freeform_to_ontology(extraction_history, base_ontology=None)` | Migrate freeform extractions into an ontology-guided one | | `list_ontologies()` | List all stored ontologies with basic metadata | | `delete_ontology(ontology_id)` | Delete an ontology by ID | The manager is backed by pluggable storage (`OntologyStorage`). By default it uses `FileSystemOntologyStorage`, which persists each ontology as a JSON file: ```python from smartmemory.ontology.manager import OntologyManager from smartmemory.stores.ontology import FileSystemOntologyStorage # Defaults to FileSystemOntologyStorage("ontologies") manager = OntologyManager() # Or supply an explicit storage directory manager = OntologyManager(storage=FileSystemOntologyStorage("my_ontologies")) ``` ## Defining an Ontology [#defining-an-ontology] ### Create an ontology and add entity types [#create-an-ontology-and-add-entity-types] Entities are described with `EntityTypeDefinition`. Properties are a `name -> type` map, and `required_properties`/`parent_types`/`aliases` are sets. ```python from datetime import datetime, UTC from smartmemory.ontology.manager import OntologyManager from smartmemory.ontology.models import EntityTypeDefinition manager = OntologyManager() # Create a new ontology (persisted immediately) ontology = manager.create_ontology( "technology", domain="technology", description="Technology and ML concepts", ) # Define an entity type deep_learning = EntityTypeDefinition( name="deep_learning", description="Deep learning techniques", properties={"complexity": "number"}, # property_name -> property_type required_properties=set(), parent_types=set(), # inheritance hierarchy (type names) aliases=set(), examples=["ConvolutionalNeuralNetwork", "Transformer"], created_by="human", created_at=datetime.now(UTC), ) ontology.add_entity_type(deep_learning) # A child type that inherits from deep_learning cnn = EntityTypeDefinition( name="convolutional_neural_network", description="CNN architectures", properties={"complexity": "number"}, required_properties=set(), parent_types={"deep_learning"}, aliases={"cnn"}, examples=["ResNet", "VGG"], created_by="human", created_at=datetime.now(UTC), ) ontology.add_entity_type(cnn) ``` ### Define relationship types [#define-relationship-types] Relationships are described with `RelationshipTypeDefinition`. `source_types` and `target_types` constrain which entity types may participate. ```python from smartmemory.ontology.models import RelationshipTypeDefinition uses = RelationshipTypeDefinition( name="uses", description="One technique uses another", source_types={"deep_learning"}, target_types={"deep_learning"}, properties={}, ) ontology.add_relationship_type(uses) similar_to = RelationshipTypeDefinition( name="similar_to", description="Two techniques are similar", source_types={"deep_learning"}, target_types={"deep_learning"}, properties={}, bidirectional=True, ) ontology.add_relationship_type(similar_to) ``` ### Property constraints [#property-constraints] Per-property constraints are expressed with `PropertyConstraint` and attached to an entity type's `property_constraints` map. `kind` controls enforcement: `soft` warns, `hard` rejects. ```python from smartmemory.ontology.models import PropertyConstraint deep_learning.property_constraints = { "complexity": PropertyConstraint( required=False, type="number", cardinality="one", kind="soft", ), "application_domain": PropertyConstraint( required=False, type="enum", cardinality="many", kind="soft", enum_values=["nlp", "computer_vision", "robotics", "speech"], ), } ``` ### Add rules [#add-rules] Validation, enrichment, and inference rules use `OntologyRule`: ```python import uuid from datetime import datetime, UTC from smartmemory.ontology.models import OntologyRule rule = OntologyRule( id=str(uuid.uuid4()), name="require_complexity", description="Encourage a complexity score on deep_learning entities", rule_type="validation", conditions={"entity_type": "deep_learning"}, actions={"suggest_property": "complexity"}, ) ontology.add_rule(rule) ``` ### Persist changes [#persist-changes] `create_ontology()` saves on creation, but mutations to the in-memory `Ontology` object are not auto-persisted. Save through the manager's storage after editing: ```python manager.storage.save_ontology(ontology) ``` ## Validating Against an Ontology [#validating-against-an-ontology] The `Ontology` model can validate entities and relationships directly. Both methods return a list of error strings (empty when valid). ```python # Validate an entity instance against its type definition errors = ontology.validate_entity("deep_learning", {"complexity": 0.8}) # [] when valid; ["Unknown entity type: ..."] / missing-required-property messages otherwise # Validate a relationship's source/target types rel_errors = ontology.validate_relationship( "uses", source_type="deep_learning", target_type="deep_learning", ) ``` Lookups are case-insensitive: ```python entity_def = ontology.get_entity_type("Deep_Learning") rel_def = ontology.get_relationship_type("USES") ``` ## Inferring an Ontology From Extractions [#inferring-an-ontology-from-extractions] Rather than authoring an ontology by hand, you can infer one from prior extraction results. `infer_ontology_from_extractions` analyzes a list of extraction records (each with `entities` and `relations`) and builds entity/relationship type definitions for patterns seen at least twice. ```python extraction_history = [ { "entities": [ {"type": "algorithm", "name": "CNN", "properties": {"domain": "vision"}}, ], "relations": [ {"relation_type": "uses", "source": "CNN", "target": "convolution"}, ], }, # ... more extraction records ] inferred = manager.infer_ontology_from_extractions( extraction_history, ontology_name="inferred_tech", ) print(list(inferred.entity_types.keys())) print(list(inferred.relationship_types.keys())) ``` The inferred ontology is persisted automatically. Confidence scores scale with how often each type was observed, and required properties are derived from properties present in more than 80% of instances of a type. > **Note — OntoGPT engine:** When `ontology.inference.ontogpt.enabled` is set in config, inference is delegated to the `OntoGPTInferenceEngine`. If that path fails it logs a warning and falls back to the built-in frequency-based inference shown above. ## Migrating Freeform Extractions [#migrating-freeform-extractions] `migrate_freeform_to_ontology` turns a history of freeform (LLM-driven) extractions into an ontology-guided one. With no base ontology it infers a fresh one; with a base ontology it extends it and records a migration rule. ```python # Build a brand-new ontology from freeform extraction history migrated = manager.migrate_freeform_to_ontology(extraction_history) # Or extend an existing ontology migrated = manager.migrate_freeform_to_ontology( extraction_history, base_ontology=ontology, ) ``` ## Listing and Deleting Ontologies [#listing-and-deleting-ontologies] ```python # List stored ontologies (id, name, version, domain, entity_count, ...) for meta in manager.list_ontologies(): print(meta["id"], meta["name"], meta["entity_count"]) # Delete by ID manager.delete_ontology(ontology.id) ``` ## Ontology-Guided Ingestion [#ontology-guided-ingestion] SmartMemory's ingestion pipeline includes an `ontology_constrain` stage that validates and constrains extracted entities/relations against the active ontology. It is enabled by default and controlled by the `enable_ontology` constructor flag: ```python from smartmemory.smart_memory import SmartMemory # Ontology constraint stage on (default) memory = SmartMemory(enable_ontology=True) # Disable the stage entirely memory = SmartMemory(enable_ontology=False) item_id = memory.ingest( "Convolutional Neural Networks are a deep learning architecture " "used for computer vision tasks." ) ``` The `ontology_constrain` stage reads the unified `:OntologyType` / `:OntologyRelation` graph labels and routes all reads/writes through the internal `OntologyService`. This is the productized path that ontology definitions feed into — there is no separate `ontology_config=` constructor argument, and entity extraction is governed by the pipeline stage rather than by per-call `search()` flags. > **REST API:** The service exposes ontology management over HTTP under `/memory/ontology/*` — including `GET /memory/ontology/status`, `GET`/`POST /memory/ontology/registries`, registry snapshots/changelog/rollback, templates, patterns, import/export, and `POST /memory/ontology/inference/run`. See the service API reference for the full route list. ## Capabilities Not Currently Exposed [#capabilities-not-currently-exposed] Some ontology features that might be expected are **not** part of the current public API. They are noted here so you don't depend on methods that don't exist: * **Per-call ontology search expansion** — there is no `search(use_ontology=...)` / `expand_concepts` argument. Retrieval does not take ontology flags; ontology influence happens at ingestion time via the `ontology_constrain` stage. * **Concept-keyed memory listing / statistics** — methods like `get_memories_by_concept(...)` and `get_concept_statistics()` do not exist. Use standard `memory.search(...)` plus graph queries to retrieve entities and their relations. * **Per-memory extracted-concept readout** — there is no `get_extracted_concepts(memory_id)` accessor. Extracted entities are written to the knowledge graph during ingestion; query the graph to inspect them. These are intentional gaps in the current surface, not shipped methods — treat any code that calls them as incorrect. # Performance Tuning (/smartmemory/guides/performance-tuning) This guide covers optimization strategies and best practices for maximizing SmartMemory performance in production environments. ## Performance Overview [#performance-overview] SmartMemory performance depends on several key factors: * **Graph database backend** (FalkorDB, SQLite) * **Vector store configuration** (FalkorDB native HNSW, optional usearch) * **Background processing settings** * **Caching strategies** * **Query optimization** ## Backend Optimization [#backend-optimization] ### Graph Database Tuning [#graph-database-tuning] #### FalkorDB Configuration [#falkordb-configuration] ```python falkordb_config = { "graph_backend": { "type": "falkordb", "host": "localhost", "port": 6379, "connection_pool": { "max_connections": 20, "retry_on_timeout": True, "socket_keepalive": True, "socket_keepalive_options": { "TCP_KEEPIDLE": 1, "TCP_KEEPINTVL": 3, "TCP_KEEPCNT": 5 } }, "query_optimization": { "enable_query_cache": True, "cache_size": "256MB", "query_timeout": 30 } } } ``` #### SQLite Configuration (Lite Mode) [#sqlite-configuration-lite-mode] For zero-infrastructure local use, the SQLite backend stores the graph in a single file. It needs no connection pool or external service — point it at a file path: ```python sqlite_config = { "graph_db": { "backend_class": "SQLiteBackend", "db_path": "./data/memory.db" } } ``` SQLite is single-tenant and intended for SmartMemory Lite; for multi-tenant production workloads use FalkorDB. ### Vector Store Optimization [#vector-store-optimization] SmartMemory ships two vector backends: `falkordb` (the default — embeddings are stored natively inside FalkorDB via an HNSW index, so there is no separate vector service) and `usearch` (optional, embedded). The default is the recommended production choice. #### FalkorDB HNSW Tuning [#falkordb-hnsw-tuning] The HNSW index parameters are the primary recall/latency knobs. All of these are real config keys read by the FalkorDB vector backend: ```python falkordb_vector_config = { "vector": { "backend": "falkordb", "host": "localhost", "port": 9010, "dimension": 768, # derived from the embedding model; falls back to this "metric": "cosine", # 'cosine' | 'euclidean' "hnsw_m": 16, # graph connectivity — higher = better recall, more memory "hnsw_ef_construction": 200, # build-time candidate breadth — higher = better index, slower build "hnsw_ef_runtime": 64 # query-time candidate breadth — higher = better recall, slower queries } } ``` Tuning guidance: * **Recall too low** — raise `hnsw_ef_runtime` first (query-time only), then `hnsw_m` / `hnsw_ef_construction` (require a rebuild). * **Queries too slow** — lower `hnsw_ef_runtime`. * **Index build too slow / memory-heavy** — lower `hnsw_m` and `hnsw_ef_construction`. * `dimension` is auto-detected from the active embedding model; only set it explicitly if the probe cannot run. #### usearch (Optional) [#usearch-optional] The optional embedded backend honors the same `dimension` and `metric` keys: ```python usearch_vector_config = { "vector": { "backend": "usearch", "dimension": 768, "metric": "cosine" } } ``` ## Memory Configuration Optimization [#memory-configuration-optimization] ### Background Processing Tuning [#background-processing-tuning] ```python background_config = { "background_processing": { "enabled": True, "max_workers": 4, # Adjust based on CPU cores "queue_size": 10000, "batch_processing": { "enabled": True, "batch_size": 100, "batch_timeout": 5.0 }, "worker_config": { "enrichment_workers": 2, "evolution_workers": 1, "cleanup_workers": 1 } } } ``` ### Caching Configuration [#caching-configuration] ```python caching_config = { "caching": { "l1_cache": { "enabled": True, "max_size": 1000, "ttl": 300 # 5 minutes }, "l2_cache": { "enabled": True, "type": "redis", "host": "localhost", "port": 6379, "max_memory": "1GB", "ttl": 3600 # 1 hour }, "query_cache": { "enabled": True, "max_size": 500, "ttl": 600 # 10 minutes } } } ``` ## Query Optimization [#query-optimization] ### Efficient Search Patterns [#efficient-search-patterns] #### Optimized Vector Search [#optimized-vector-search] ```python # Use specific memory types to reduce search space results = memory.search( query="machine learning", memory_type="semantic", # Limit to specific type top_k=10, # Reasonable limit filters={ "user_id": "user123", # Pre-filter by user "date_range": { "start": "2024-01-01", "end": "2024-12-31" } } ) # Batch multiple searches queries = ["AI", "ML", "Deep Learning"] results = {q: memory.search(q, top_k=10, memory_type="semantic") for q in queries} ``` ```python # Get direct neighbors of a node related = memory.get_neighbors("memory_123") # Apply any filtering/weighting client-side as needed ``` ### Bulk Operations [#bulk-operations] #### Batch Ingestion [#batch-ingestion] ```python # Efficient batch processing items = [ {"content": f"Document {i}", "metadata": {"batch": "1"}} for i in range(1000) ] # Process in optimized batches (enqueue for background) def chunk(iterable, size): for i in range(0, len(iterable), size): yield iterable[i:i+size] for chunk_items in chunk(items, 100): for it in chunk_items: memory.ingest(it, sync=False) ``` #### Bulk Updates [#bulk-updates] ```python # Batch update operations updates = [ {"item_id": f"memory_{i}", "properties": {"importance": 0.8}} for i in range(100) ] for upd in updates: memory.update_properties(upd["item_id"], upd["properties"]) # merge semantics ``` ## Monitoring and Profiling [#monitoring-and-profiling] ### Performance Metrics Collection [#performance-metrics-collection] > **Illustrative only.** SmartMemory does not ship a `smartmemory.monitoring` > module or a `PerformanceMonitor` class. The pattern below sketches a > *user-authored* timing wrapper around the real `memory.search(...)` call — > adapt it to your own metrics backend. The decorator and stats dict are not > part of the public API. ```python import time from functools import wraps # Example: a thin timing wrapper you write yourself. def profile_operation(name): def decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): start = time.perf_counter() result = fn(*args, **kwargs) elapsed_ms = (time.perf_counter() - start) * 1000 print(f"{name}: {elapsed_ms:.2f}ms") # send to your metrics sink return result return wrapper return decorator @profile_operation("custom_search") def complex_search(query): return memory.search(query, top_k=50) ``` ### Resource Monitoring [#resource-monitoring] ```python import smartmemory.utils import psutil import asyncio class ResourceMonitor: def __init__(self, memory_instance): self.memory = memory_instance self.metrics = [] async def monitor_resources(self): while True: # System metrics cpu_percent = psutil.cpu_percent(interval=1) memory_info = psutil.virtual_memory() # Optional SmartMemory summary (counts by type) summary = self.memory.summary() metrics = { "timestamp": smartmemory.utils.now(), "cpu_percent": cpu_percent, "memory_percent": memory_info.percent, # add SmartMemory-derived metrics as needed from `summary` } self.metrics.append(metrics) # Alert on high resources usage if cpu_percent > 80 or memory_info.percent > 85: await self.send_alert(metrics) await asyncio.sleep(60) # Check every minute ``` ## Optimization Strategies [#optimization-strategies] ### Memory Lifecycle Optimization [#memory-lifecycle-optimization] > **Illustrative only.** There is no `memory.configure_lifecycle(...)` method on > `SmartMemory`. The config dict below is a *design sketch* of retention and > optimization knobs — it is not a callable API. Real lifecycle behaviour is > driven by the sleep-cycle daemon, snapshot retention sweeps, and tier > compaction (see the environment variables in the project README and the > Tuning & Surfacing guide), not by a single config call. ```python # Sketch of a retention/optimization policy (not a callable API). lifecycle_config = { "retention_policies": { "working_memory": { "max_items": 1000, "cleanup_interval": 300, # 5 minutes "cleanup_strategy": "lru" }, "episodic_memory": { "max_age_days": 90, "importance_threshold": 0.3, "cleanup_interval": 3600 # 1 hour } }, "optimization": { "enable_compression": True, "compression_threshold": 1000, # Compress after 1000 characters "enable_indexing": True, "index_rebuild_interval": 86400 # 24 hours } } ``` ### Query Pattern Optimization [#query-pattern-optimization] ```python class QueryOptimizer: def __init__(self, memory): self.memory = memory self.query_patterns = {} def optimize_query(self, query, user_context=None): # Analyze query pattern pattern = self.analyze_query_pattern(query) # Apply optimizations based on pattern if pattern == "semantic_search": return self.optimize_semantic_search(query) elif pattern == "temporal_search": return self.optimize_temporal_search(query, user_context) elif pattern == "relationship_search": return self.optimize_relationship_search(query) return query def optimize_semantic_search(self, query): # Use semantic-specific optimizations return { "query": query, "memory_type": "semantic", "use_vector_index": True, "embedding_cache": True } def optimize_temporal_search(self, query, user_context): # Add temporal constraints time_range = self.infer_time_range(query, user_context) return { "query": query, "memory_type": "episodic", "time_range": time_range, "use_temporal_index": True } ``` ## Scaling Strategies [#scaling-strategies] ### Horizontal Scaling [#horizontal-scaling] ```python # Distributed SmartMemory setup class DistributedSmartMemory: def __init__(self, shard_config): self.shards = {} self.load_balancer = LoadBalancer() for shard_id, config in shard_config.items(): self.shards[shard_id] = SmartMemory( config=config, shard_id=shard_id ) def route_query(self, query, user_id=None): # Route based on user_id or content hash if user_id: shard_id = self.hash_user_to_shard(user_id) else: shard_id = self.hash_content_to_shard(query) return self.shards[shard_id] async def distributed_search(self, query, user_id=None): if user_id: # Search specific user shard shard = self.route_query(query, user_id) return await shard.search_async(query) else: # Search all shards and merge results tasks = [ shard.search_async(query) for shard in self.shards.values() ] all_results = await asyncio.gather(*tasks) return self.merge_distributed_results(all_results) ``` ### Vertical Scaling [#vertical-scaling] ```python # High-performance single-instance configuration high_performance_config = { "graph_backend": { "type": "falkordb", "connection_pool_size": 50, "query_cache_size": "1GB", "memory_mapping": True }, "vector": { "backend": "falkordb", "metric": "cosine", "hnsw_m": 32, # higher connectivity for max recall "hnsw_ef_construction": 400, # broader build-time search "hnsw_ef_runtime": 128 # broader query-time search }, "background_processing": { "max_workers": 16, "queue_size": 50000, "batch_size": 500 }, "caching": { "memory_limit": "8GB", "enable_compression": True, "compression_algorithm": "lz4" } } ``` ## Performance Testing [#performance-testing] ### Benchmarking Framework [#benchmarking-framework] ```python import smartmemory.utils import time import statistics from concurrent.futures import ThreadPoolExecutor class PerformanceBenchmark: def __init__(self, memory): self.memory = memory self.results = {} def benchmark_search_performance(self, queries, iterations=100): """Benchmark search performance with multiple queries.""" times = [] for _ in range(iterations): start_time = time.time() for query in queries: results = self.memory.search(query, top_k=10) end_time = time.time() times.append(end_time - start_time) return { "avg_time": statistics.mean(times), "median_time": statistics.median(times), "min_time": min(times), "max_time": max(times), "std_dev": statistics.stdev(times) } def benchmark_concurrent_access(self, query, concurrent_users=10): """Benchmark concurrent access performance.""" def search_worker(): start_time = time.time() results = self.memory.search(query) end_time = time.time() return end_time - start_time with ThreadPoolExecutor(max_workers=concurrent_users) as executor: futures = [ executor.submit(search_worker) for _ in range(concurrent_users) ] times = [future.result() for future in futures] return { "concurrent_users": concurrent_users, "avg_response_time": statistics.mean(times), "max_response_time": max(times), "throughput": concurrent_users / max(times) } def run_full_benchmark(self): """Run comprehensive performance benchmark.""" test_queries = [ ``` # Shipping Lite inside an app (/smartmemory/guides/shipping-lite-in-an-app) SmartMemory Lite runs in your own process with a SQLite graph and local vectors, so it can ship inside a desktop app, a game or a local tool. This page collects the decisions in one place. Each section links to the page that holds the detail. ## 1. License [#1-license] A commercial product that includes the Lite runtime needs a Lite Redistribution Agreement. It is a one-page agreement, free during early access and below the revenue threshold it states. A free, non-commercial application does not need one. Read [Licensing](/smartmemory/reference/licensing/), complete the form there, and keep the PDF copy we email you. Your publisher or legal team will want to read it before you ship. The people who use your application never need a license from us. ## 2. Turn off the update check for your shipped build [#2-turn-off-the-update-check-for-your-shipped-build] On first use in a process, Lite asks the SmartMemory API once whether a newer version exists. The request carries a random install id, versions, platform and the request IP address, so a shipped app would send it from every user's machine. Decide whether you want that. To turn it off, set one of these in the environment of the process that runs SmartMemory: ```bash SMARTMEMORY_NO_UPDATE_CHECK=1 # or DO_NOT_TRACK=1 ``` After you sign the agreement, you can set `SMARTMEMORY_LICENSE_KEY` so installs are attributed to it. The key only attributes installs and unlocks nothing, and it is never read when the update check is off. See [Update check and install counts](/smartmemory/reference/licensing/#update-check-and-install-counts). ## 3. Build without an LLM key [#3-build-without-an-llm-key] You do not need a provider key to run Lite. Force the no-LLM profile so behavior does not depend on what is in the user's environment: ```python from smartmemory.tools.factory import create_lite_memory from smartmemory.pipeline.config import PipelineConfig memory = create_lite_memory( data_dir=save_slot_dir, pipeline_profile=PipelineConfig.lite(llm_enabled=False), ) ``` Entities still come from pattern matching and spaCy NER, without LLM relation extraction. See [Lite mode](/smartmemory/guides/lite-mode/#llm-auto-detection-degrade-1d). Do not ship a provider key inside your app. Also keep other providers' keys out of the process environment. `create_lite_memory` does not pin the embedding provider, so a provider key that appears after you ingested can switch the embedder and make semantic search return nothing. Set the `local` embedding provider in setup and keep it. ## 4. One data directory per store [#4-one-data-directory-per-store] `data_dir` holds the whole store. Use one directory per save slot, per user or per project, and delete the directory to delete that memory. The default is `~/.smartmemory`. To keep an application's memory out of that shared location, always pass `data_dir`. ## 5. Plan for the model download [#5-plan-for-the-model-download] Lite uses a spaCy model (`en_core_web_sm`) and a local embedding model. In our onboarding test the two came to roughly 115 MB (about 15 MB and about 100 MB) and were downloaded on first run. Decide whether your installer bundles them or your app downloads them on first launch. `PipelineConfig.lite_hermetic()` refuses to download the spaCy model and raises an error if it is missing, which is the right choice when the model must come from your bundle. See [Hermetic variant](/smartmemory/guides/lite-mode/#hermetic-variant). ## 6. Packaging as one executable [#6-packaging-as-one-executable] **Not tested.** We have not verified that a Python process using SmartMemory freezes into a single executable with PyInstaller or a similar tool. Lite loads compiled modules, spaCy models and a local embedding model, and each of those can need explicit handling in a freezer. If you need one executable, test a frozen build early and tell us what you find at [help@smartmemory.ai](mailto:help@smartmemory.ai). Until it is tested, the supported way to ship is a Python environment or a sidecar process that you install alongside your app. ## Search by metadata [#search-by-metadata] If you keep several characters, users or projects in one store, tag each memory with metadata and filter on it at search time, instead of running one store per tag. See [Filtering by metadata](/smartmemory/reference/smart-memory/#filtering-by-metadata). # Troubleshooting (/smartmemory/guides/troubleshooting) The most common problems you'll hit, with the fix. SmartMemory runs in one of two modes, chosen at `smartmemory setup` time (see [Installation](/smartmemory/getting-started/installation/)): * **Lite**: storage and search run on your machine with a local SQLite graph and local vectors. * **Service**: the SDK and tools connect to the managed SmartMemory API with an API key. Start with the checks that apply to both, then jump to your mode. ## First checks [#first-checks] ### `smartmemory: command not found` [#smartmemory-command-not-found] **Cause:** The virtual environment you installed into isn't active. **Fix:** Activate it and confirm the package is installed: ```bash which python which smartmemory python -m pip show smartmemory ``` ### Something is broken and you don't know where to start [#something-is-broken-and-you-dont-know-where-to-start] Run the built-in diagnostic: ```bash sm doctor ``` It checks your Python version (SmartMemory needs 3.11 or newer), checks that the installed `smartmemory-core` is recent enough for the `smartmemory` package, and flags a configured SOCKS proxy that is missing its transport library. Then check what mode you are in and whether the local daemon is healthy: ```bash sm status ``` ## Lite mode [#lite-mode] ### `SmartMemory daemon is not running.` [#smartmemory-daemon-is-not-running] **Cause:** The local daemon that serves the viewer and the local API isn't running. **Fix:** Start it, or restart it if `sm status` says it should be running but is not responding: ```bash smartmemory start sm restart ``` ### `sm status` reports `degraded` [#sm-status-reports-degraded] **Cause:** The daemon is up but could not open your saved memories. The status output prints the reason. **Fix:** Run `sm doctor` and follow its output. Also check that the configured data directory exists and is writable. ### The cloud dashboard shows 0 memories [#the-cloud-dashboard-shows-0-memories] **Cause:** This is expected in Lite mode. Lite memories are stored on your machine in a local SQLite graph. They are not in your cloud account, so the dashboard at smartmemory.ai does not see them. **Fix:** None needed. To use the managed service instead, re-run `smartmemory setup --mode remote` with an API key. ### The first add or search is slow [#the-first-add-or-search-is-slow] **Cause:** The first call loads the local embedding and reranker models (and downloads them the first time). **Fix:** Preload them once after install: ```bash sm warm ``` ### No entities extracted [#no-entities-extracted] **Cause:** No LLM provider is active, so ingestion falls back to local pattern-based extraction. `sm status` shows the selected LLM provider and flags a provider that is configured without an API key, in which case extraction is disabled. **Fix:** Re-run `sm setup`, choose local storage, and pick a provider. Groq is the recommended hosted provider and uses `GROQ_API_KEY`. Local runtimes such as Ollama and LM Studio need no key. Explicit memory storage and search work without an LLM. See [Lite mode](/smartmemory/guides/lite-mode/) for how the LLM is detected. ### Search results got worse after changing the embedding model [#search-results-got-worse-after-changing-the-embedding-model] **Cause:** Stored vectors were created with the previous embedding model. **Fix:** If the new model produces vectors of the same dimension as the old one, re-embed every memory with the current model. The daemon must be running: ```bash sm admin reindex ``` This re-embeds into the existing vector index. If the new model uses a different vector dimension, those memories are skipped and the index needs to be rebuilt at the new dimension. There is no CLI command for that rebuild, so contact SmartMemory support before switching to a model with a different dimension. ### `ValueError: 'working' is not a valid MemoryType` [#valueerror-working-is-not-a-valid-memorytype] **Cause:** Code or data still references the legacy `memory_type="working"`. **Fix:** Rename it to `pending`. See [Migration & upgrade](/smartmemory/guides/migration-and-upgrade/). ## Service mode [#service-mode] ### `No API key found` [#no-api-key-found] **Cause:** Service mode is configured but no API key was found in the environment or the OS keychain. On headless machines (Docker, SSH, CI) the keychain is often unavailable, so a key entered at setup is not saved. **Fix:** Create a key in the [SmartMemory dashboard](https://www.smartmemory.ai/dashboard) (Dashboard, then Settings, then API Keys) and either re-run setup or export it: ```bash smartmemory setup --mode remote --api-key sk_... # or export SMARTMEMORY_API_KEY="sm_live_..." ``` ### `SmartMemory API unreachable at ...` [#smartmemory-api-unreachable-at-] **Cause:** The client could not connect to the API URL. **Fix:** Check your network connection and that `SMARTMEMORY_API_URL`, if you set it, points at the managed API: ```bash export SMARTMEMORY_API_URL="https://api.smartmemory.ai" ``` ### `401 Unauthorized` [#401-unauthorized] **Cause:** The API key is missing, mistyped, or has been deleted. **Fix:** Confirm the key you are sending matches one listed in the dashboard. If in doubt, create a new key and update `SMARTMEMORY_API_KEY` or re-run setup. ### `400` with `Team context is required for this operation` [#400-with-team-context-is-required-for-this-operation] **Cause:** A workspace-scoped request was sent without an `X-Workspace-Id` header. **Fix:** Send the active workspace id in `X-Workspace-Id`. The JS SDK does this automatically once you call `client.setTeamId(...)`. ### `SSE connection failed: HTTP 400` on the progress stream [#sse-connection-failed-http-400-on-the-progress-stream] **Cause:** The progress stream request had no `X-Workspace-Id` header. **Fix:** Pass `workspaceId` to `subscribeProgress({...})`. See [JavaScript SDK](/smartmemory/guides/javascript-sdk/#progress-streaming-sse). ## See also [#see-also] * [Installation](/smartmemory/getting-started/installation/): Lite and Service setup * [Lite mode](/smartmemory/guides/lite-mode/): LLM detection and local behavior * [Authentication & multi-tenancy](/smartmemory/guides/authentication-and-tenancy/): workspace scoping, tokens, and isolation levels # Tuning Surfacing (/smartmemory/guides/tuning-surfacing) Task-oriented recipes for configuring SmartMemory's retrieval layer. Each recipe starts from a concrete goal ("I want X") and shows the minimum changes required. Conceptual background: [`concepts/memory-dynamics.md`](/smartmemory/concepts/memory-dynamics/) API reference: [`api/surfacing.md`](/smartmemory/reference/surfacing/) ## Recipes [#recipes] ### "I want to opt into the dynamics model" [#i-want-to-opt-into-the-dynamics-model] Default SmartMemory uses legacy (M1a) retrieval. Flip one constructor arg: ```python from smartmemory import SmartMemory memory = SmartMemory(surfacing_mode="router") ``` That's it. The router, pin API, spreading activation, PPR centrality, freshness boost, and session-pin read guards are all active. Legacy-mode callers using the same workspace are unaffected — opt-in is per-instance. ### "I want pinned items for my chat session" [#i-want-pinned-items-for-my-chat-session] Use `pin_item()` when the user (or your agent) decides an item is worth keeping top-of-mind for the current conversation. ```python memory = SmartMemory(surfacing_mode="router") # Your agent decides "always reference the project brief in this session." memory.pin_item("chat-2026-04-22-abc", "project-brief-item-id") # Every subsequent get_working_context for this session force-includes # the brief, regardless of what the user asks about. resp = memory.get_working_context("chat-2026-04-22-abc", "anything") assert any(i["item_id"] == "project-brief-item-id" for i in resp["items"]) # Clean up at session close. memory.unpin_item("chat-2026-04-22-abc", "project-brief-item-id") ``` Pins TTL at 24h so forgotten cleanups don't leak indefinitely. ### "I want recent memories to rank higher" [#i-want-recent-memories-to-rank-higher] The `freshness_boost` term adds a small additive lift for items under 24 hours old, decaying linearly to zero at the window edge. It's on by default in router mode with weight `0.2`. To make fresh items rank more aggressively, pass a higher weight via the `smartmemory.activation.constants` module *at import time* (no runtime config API yet): ```python # In your app's bootstrap, before constructing SmartMemory: import smartmemory.activation.constants as _consts _consts.FRESHNESS_BOOST_WEIGHT = 0.5 # up from 0.2 memory = SmartMemory(surfacing_mode="router") ``` To widen the window (e.g. boost anything from this week, not just today): ```python _consts.FRESHNESS_WINDOW_HOURS = 168 # 7 days, up from 24 ``` Both are global — one setting per process. ### "I want analytical queries to pull in graph neighbors" [#i-want-analytical-queries-to-pull-in-graph-neighbors] Already happens automatically when `surfacing_mode="router"` is on and the router classifies your query as `deep:multi-hop`. No config needed. Signals that trigger `deep:multi-hop`: * Query contains `why`, `how`, `what caused`, `explain` * Query has 2+ detected entities * Query is longer than 80 characters If you *know* the query is analytical and want to skip the classifier: ```python resp = memory.get_working_context( session_id="s1", query="explain this", strategy="deep:multi-hop", ) ``` ### "I want to make spreading activation more aggressive" [#i-want-to-make-spreading-activation-more-aggressive] The default `spread()` walks 2 hops deep with a 20-neighbor fan-out per hop and a `0.5` damping factor. To tune, call `spread()` directly (the wrapper that `get_working_context` uses internally): ```python from smartmemory.search.spreading_activation import spread # Wider net — more hops, less damping, larger frontier. scores = spread( memory, seed_item_ids=["item-a"], depth=3, # up from 2 — reaches further max_fan_out=40, # up from 20 — larger frontier per hop damping=0.7, # up from 0.5 — distant items contribute more budget_ms=300, # up from 150 — allow more Cypher time ) ``` Raising `depth` past 3 typically blows the budget on any non-trivial workspace. Prefer raising `max_fan_out` first. To override the edge-type allowlist (e.g. only spread along co-retrieval edges): ```python scores = spread( memory, seed_item_ids=["item-a"], edge_types=["CO_RETRIEVED", "ANSWERED_BY"], ) ``` ### "My workspace is huge — will PPR cost me?" [#my-workspace-is-huge--will-ppr-cost-me] Personalized PageRank materializes the workspace subgraph into memory for NetworkX. Workspaces with more than `10000` nodes are automatically skipped (PPR contributes nothing, `centrality` stays `0` in the composite, spreading and other signals still work). The cutoff is configurable at call time: ```python from smartmemory.search.ppr import personalized_pagerank scores = personalized_pagerank( memory, seed_item_ids=["item-a"], budget_ms=500, # up from 200 for larger subgraphs ) ``` To change the max-nodes threshold, pass it to the backend directly: ```python memory._graph.backend.materialize_workspace_subgraph(max_nodes=20000) # Caller uses whatever subgraph is returned. ``` PPR p99 latency on synthetic graphs (verified at release): | Nodes | Median | p99 | | ------ | ------- | ---------------------------- | | 1,000 | \~2 ms | \~90 ms (JIT warmup outlier) | | 5,000 | \~11 ms | \~12 ms | | 10,000 | \~23 ms | \~25 ms | The 10k cutoff is a safety margin, not a performance wall. ### "I want to disable caching for a benchmark" [#i-want-to-disable-caching-for-a-benchmark] By default the router caches PPR results per `(workspace, seed_hash)` with a 5-minute TTL and write-through invalidation on graph mutations. To skip the cache for a benchmark pass, call PPR directly without a cache argument: ```python scores = personalized_pagerank( memory, seed_item_ids=["item-a"], cache=None, # no cache read or write ) ``` To force-invalidate all PPR state for a workspace (useful in tests): ```python cache = memory._ppr_cache() if cache is not None: cache.invalidate_workspace(memory.scope_provider.workspace_id) ``` ### "I want to observe what the router decided" [#i-want-to-observe-what-the-router-decided] After each `get_working_context()` call: ```python resp = memory.get_working_context("s1", "Why did we pick JWT?") decision = memory.last_surfacing_decision print(decision.strategy.value) # "deep:multi-hop" print(decision.confidence) # 0.75 print(decision.reasons) # ["keyword:why"] print(decision.signals) # {"query_length": 27, "word_count": 6, "entity_count": 0} ``` Also surfaced in the response as `resp["strategy_used"]`. ### "I want to enable progressive compression of cold items" [#i-want-to-enable-progressive-compression-of-cold-items] Compression is a separate opt-in from surfacing — it runs in the background worker and compacts items whose activation has dropped below tier thresholds. ```python # Core-side: enable tier compaction on the SmartMemory instance. memory = SmartMemory( surfacing_mode="router", tier_compaction=True, # M2b Slice B ) ``` ```bash # Worker-side: gate job registration. SMARTMEMORY_COMPACTION_ENABLED=true ``` Without both flags the compaction sweep never registers and no items compress. User-authored content (origin tier 1) is always preserved at `FULL` regardless. To rehydrate a compacted item back to full fidelity: ```python item = memory.rehydrate_item("some-item-id", target_tier="FULL") # Content restored from the snapshot stored in metadata["snapshots"]. # needs_reembed=True is set so a future enrichment pass regenerates # the embedding. ``` *** ## Knobs reference [#knobs-reference] All activation/surfacing weights live in `smartmemory.activation.constants`: | Constant | Default | What it controls | | ------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `PPR_WEIGHT` | `0.5` | Centrality lift multiplier in the composite. `centrality_lift = 1 + PPR_WEIGHT × centrality_norm`. Max lift at `centrality_norm=1` is `1.5×`. | | `FRESHNESS_BOOST_WEIGHT` | `0.2` | Additive cap on the freshness boost for items \< 24h old. | | `FRESHNESS_WINDOW_HOURS` | `24` | Linear-decay window for the freshness boost. | | `SESSION_PIN_MULTIPLIER` | `0.3` | `session_pin_boost = pin_weight × SESSION_PIN_MULTIPLIER`. | | `ACTIVATION_FLOOR` | `0.05` | Minimum activation used in composite's `max(activation, floor)` so cold items aren't zero-multiplied out of the running when relevance is high. | | `BOOST_SIZE_RETRIEVE` | `0.05` | Per-retrieval boost added to an item's activation score. | | `BOOST_SIZE_SURFACE` | `0.02` | Per-surfacing boost (less than retrieval — surfacing is less "intentional"). | | `BOOST_SIZE_CORETRIEVE` | `0.01` | Boost applied to co-retrieved items by the Hebbian evolver. | | `DEFAULT_HALF_LIFE_HOURS` | `720` | 30-day half-life for activation decay. | | `DECAY_INTERVAL_SECONDS` | `3600` | How often the worker's decay sweep runs. | | `FLUSH_INTERVAL_SECONDS` | `5` | How often queued retrieval boosts flush from Redis into persisted metadata. | Redis keyspaces used (all on `ACTIVATION_REDIS_DB`, default DB 1): ``` sm:activation:pending:{workspace_id}:{item_id} — queued boosts sm:activation:decay-lock:{workspace_id} — decay sweep lock sm:activation:compaction-lock:{workspace_id} — compaction sweep lock sm:session:{session_id}:pins — pin hash sm:session:{session_id}:pinned_workspace — pin workspace binding sm:ppr:{workspace_id}:{seed_hash} — PPR result cache ``` *** ## Related documentation [#related-documentation] * [`concepts/memory-dynamics.md`](/smartmemory/concepts/memory-dynamics/) — the model explained * [`api/surfacing.md`](/smartmemory/reference/surfacing/) — exact signatures and response shapes * [`guides/performance-tuning.md`](/smartmemory/guides/performance-tuning/) — broader performance guidance * [`guides/memory-operations.md`](/smartmemory/guides/memory-operations/) — other advanced operations # Licensing (/smartmemory/reference/licensing) SmartMemory Lite is the local, zero-infrastructure runtime published on PyPI as `smartmemory` and `smartmemory-core`. It ships as a compiled runtime under the SmartMemory Lite Runtime License. The hosted service, the server components, and the full platform are licensed separately. ## What you can do without asking [#what-you-can-do-without-asking] The Lite Runtime License lets you install and use the runtime for: * personal use * internal tools and services inside your organization * evaluation, research, and education * building and testing applications * shipping a non-commercial application that is free of charge No source disclosure is ever required for your application. ## Shipping Lite inside a commercial product [#shipping-lite-inside-a-commercial-product] Distributing the runtime as part of a product that is sold, subscribed to, or otherwise commercial needs a Lite Redistribution Agreement. It is a one-page agreement, and it is free: * during early access, and * for organizations below the revenue threshold stated in the agreement. The threshold is your organization's total revenue in its most recently completed fiscal year. The people who use your application never need a license from us and never pay us anything. Above the threshold, commercial terms are agreed case by case with at least 90 days notice before any fee applies. Nothing is ever charged retroactively. Licensees above the threshold tell us within 30 days of their fiscal year end. You can complete one here. Read the agreement, fill in your organization and the applications that will ship SmartMemory Lite, and accept. We email you a PDF copy of the completed agreement straight away. If you would rather do this by email, write to [help@smartmemory.ai](mailto:help@smartmemory.ai) with your product name and a contact address. ## What the license does not allow [#what-the-license-does-not-allow] * modifying, decompiling, or reverse engineering the runtime * redistributing the runtime on its own, or as the core of a memory or retrieval product * using the runtime to build a service that competes with the SmartMemory hosted service * removing copyright or attribution notices ## Update check and install counts [#update-check-and-install-counts] On the first `SmartMemory()` construction in a process, the runtime asks our API once whether a newer `smartmemory-core` is available, and again at most once every 24 hours after that. This tells you when to upgrade, and it is also how we count installs. What is sent: * a random install id generated on first run and stored locally, not derived from your hardware or hostname * the runtime version and Python version * your platform (for example `linux`, `darwin`, `win32`) and CPU architecture (for example `arm64`) * the backend mode of the memory that started the check (`lite` or `server`) * your `SMARTMEMORY_LICENSE_KEY`, if you have set one What is never sent: memory content, query text, file paths, project names, your hostname, or your username. What our server keeps for each ping, in addition to the fields above: the time it arrived, the request IP address (used for rate limiting and coarse service health, never resolved to a person or an organization), and the `User-Agent` header, which is `smartmemory-lite/`. An IP address together with a persistent install id is personal data under the GDPR, which is why the opt-out below is a real switch and not a preference. Two environment variables turn the check off completely: * `SMARTMEMORY_NO_UPDATE_CHECK=1` * `DO_NOT_TRACK=1` The check also skips itself automatically in CI environments, runs on a background thread with a short timeout, and never blocks startup or raises an error. If you have a Lite Redistribution Agreement, set `SMARTMEMORY_LICENSE_KEY` to the key from your agreement in the application you ship, so its installs are attributed to your agreement rather than counted as unlicensed. ### What reads `SMARTMEMORY_LICENSE_KEY` [#what-reads-smartmemory_license_key] Exactly one thing: the update check described above, in `smartmemory/update_check.py`. It reads the variable and includes the value in the ping so the install is attributed to your agreement. Nothing else in the runtime reads it. It does not unlock features, it is not checked by the vector store, the graph store, the pipeline, or the CLI, and a missing, empty, or unknown key never changes behavior. If you disable the update check, the key is never read at all. A contract test in core pins this so it stays true across releases. ## Client libraries [#client-libraries] The Python client (`smartmemory-client`) and the JavaScript SDK (`@smartmemory/sdk-js`) are thin API clients and carry their own permissive licenses. ## Full text [#full-text] The complete license is included in every wheel as `LICENSE` and in the package metadata on PyPI. # Memory Type API Matrix (/smartmemory/reference/memory-types-api) A per-type cheatsheet: for every memory type, **where to read more**, how to **create** it, **query** it, and the **key fields** it carries across all three surfaces. Each type name links to its concept, guide, or example page. **Surfaces** (columns): * **SDK**: `SmartMemory` Python methods (see the [SmartMemory API](/smartmemory/reference/smart-memory/)). * **MCP**: agent tools (see [MCP Tools](/smartmemory/reference/tools/)). * **REST**: HTTP routes. **All REST paths below are under the `/memory` prefix** (e.g. `POST /decisions/create` → `POST /memory/decisions/create`). For the conceptual model of each type, see [Memory Types](/smartmemory/concepts/memory-types/). ## Universal operations (any type) [#universal-operations-any-type] These work on every memory type. The type is set by `memory_type` or auto-classified by `ingest()`. | Operation | SDK | MCP | REST | | ------------------ | -------------------------------- | --------------------------- | ------------------- | | Create (pipeline) | `ingest(text)` | `memory_ingest` | `POST /ingest` | | Create (direct) | `add(MemoryItem(memory_type=…))` | `memory_add` | `POST /add` | | Get by id | `get(id)` | `memory_get` | `GET /{item_id}` | | Update | `update(id, …)` | `memory_update` | `PATCH /{item_id}` | | Delete | `delete(id)` | `memory_delete` | `DELETE /{item_id}` | | Search | `search(q, memory_type=…)` | `memory_search` | `POST /search` | | Search by metadata | `search_by_metadata(k, v)` | `memory_search_by_metadata` | `GET /by-metadata` | | List | `list()` | `memory_list` | `GET /list` | ## Knowledge layer [#knowledge-layer] Captured through `ingest()` (auto-classified) or `add(memory_type=…)`. Queried by `search(memory_type=…)`. These types evolve into one another (see [Memory Dynamics](/smartmemory/concepts/memory-dynamics/)). | Type | Create (SDK · MCP · REST) | Query | Key fields / notes | | ------------------------------------------------------ | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | [`semantic`](/smartmemory/examples/knowledge-graph/) | `add("…", memory_type="semantic")` · `memory_add` · `POST /add` | `search(memory_type="semantic")` | Context-independent facts. Default classification. | | [`episodic`](/smartmemory/examples/conversational-ai/) | `add(memory_type="episodic")` · `memory_add` · `POST /add` | `search(memory_type="episodic")` | Time-bound events, carries `valid_time`. Evolves → `semantic` / `zettel`. | | [`procedural`](/smartmemory/guides/basic-usage/) | `add(memory_type="procedural")` · `dev_record_pattern` · `POST /add` | `search(memory_type="procedural")` | How-to / step sequences. | | [`pending`](/smartmemory/concepts/memory-dynamics/) | `add(memory_type="pending")` / `distill()` · `memory_distill` · `POST /add` | `search(memory_type="pending")` | Holding bucket, `ConsolidationRouter` routes at ingest. | | [`zettel`](/smartmemory/concepts/zettelkasten-memory/) | `add(memory_type="zettel")` · `memory_add` · `POST /zettel/notes` | `search(memory_type="zettel")` + `zettel_backlinks` · `GET /zettel/{note_id}/backlinks` | Atomic notes, `[[wikilink]]` cross-refs, `zettel_tags`. Rich graph API under `/zettel/*`. | ## Expertise layer [#expertise-layer] What to *do* (and not do). Each has a Manager + Queries lifecycle (supersession, reinforcement, contradiction). Recalled as a typed dict via `search(query, expertise=True)`. Overview: [Expertise vs Knowledge](/smartmemory/concepts/expertise-vs-knowledge/). The SDK capture wrappers return typed objects, not string IDs: `add_decision(...)` returns a `Decision`, `add_constraint(...)` returns a `Constraint`, and `add_learning(...)` returns a `Learned`. | Type | Create (SDK · MCP · REST) | Query | Lifecycle & key fields | | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`decision`](/smartmemory/concepts/decision-memory/) | `add_decision(content, …)` returns `Decision` · `decision_create` · `POST /decisions/create` | `search(expertise=True)["decision"]` · `decision_search` / `decision_list` / `decision_get` · `GET /decisions`, `/decisions/search`, `/decisions/{id}` | `supersede` / `reinforce` / `contradict` / `retract` / `provenance` / `find_conflicts`. Fields: `content`, `rejected_alternatives`, `rationale`, `decision_type`, `status`, `decision_id`, `confidence`, `constraints`, `domain`. Walkthrough: [decisions lifecycle](/smartmemory/examples/decisions-lifecycle/). | | [`opinion`](/smartmemory/concepts/opinions-observations/) | `add_opinion(content, subject)` · *(derived)* `evolution_synthesize_opinions` · `POST /opinions/create` | `search(expertise=True)["opinion"]` | Reinforce via `evolution_reinforce_opinions` · `POST /evolution/reinforce/opinions`. Fields: `subject`, `confidence`. | | [`observation`](/smartmemory/concepts/opinions-observations/) | `add_observation(entity_id, fact_id, aspect)` · *(derived)* `evolution_synthesize_observations` · `POST /evolution/synthesize/observations` | `search(expertise=True)["observation"]` | Synthesized from scattered evidence. Fields: `entity_id`, `fact_id`, `aspect`. | | [`reasoning`](/smartmemory/concepts/reasoning-traces/) | `add_reasoning(trace, source_item_id)` · `reasoning_extract_trace` · `POST /reasoning/traces/store` | `search(expertise=True)["reasoning"]` · `reasoning_query_traces` · `POST /reasoning/traces/query`, `GET /reasoning/traces/{trace_id}` | Premises → conclusion chains, validation, and supersession. Also `reasoning_challenge`, `reasoning_proof_tree`. | | [`constraint`](/smartmemory/concepts/expertise-vs-knowledge/) | `add_constraint(content)` returns `Constraint` / `ingest_structured(schema="constraint")` · `memory_ingest_structured` · `POST /ingest/structured` | `search(expertise=True)["constraint"]` | Hard rule discovered or imposed. Fields include `constraint_id`, `domain`, and `source`. | | [`learned`](/smartmemory/concepts/expertise-vs-knowledge/) | `add_learning(content)` returns `Learned` / `ingest_structured(schema="learned")` · `memory_ingest_structured` · `POST /ingest/structured` | `search(expertise=True)["learned"]` | A lesson learned the hard way. Fields include `learned_id`, `lesson`, and `context`. | ## Operational types [#operational-types] | Type | Create (SDK · MCP · REST) | Query / read | Lifecycle & key fields | | -------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | [`plan`](/smartmemory/concepts/plan-memory/) | *(PlanManager)* / `ingest_structured(schema="plan")` · `memory_plan_create` · `POST /plans/create` | `memory_plan_get` / `memory_plan_active` · `GET /plans/{id}`, `/plans/active/list` | Update task: `memory_plan_update_task` · `PATCH /plans/{id}/task`, `complete` / `fail`. Fields: `title`, `tasks[]`, `status`, `context`. | | [`plan_task`](/smartmemory/concepts/plan-memory/) | `ingest_structured(schema="plan_task")` · `memory_ingest_structured` · `POST /ingest/structured` | via parent plan (`memory_plan_get`) | DAG node, `task_id`, `status`, `DEPENDS_ON` edges. INDEXED. | | [`code`](/smartmemory/guides/code-indexing/) | `ingest_code(directory)` · `code_index` · `POST /code/index` | `code_search` / `code_dependencies` / `code_dead_code` · `GET /code/search`, `/code/dependencies`, `/code/dead-code` | Code entities (class/function/route). Fields: `entity_type`, `repo`. | | `evaluation` | *(derived from `EvaluationEvolver`)* | `get_evaluation(agent_id, dimension, domain)` / `list_evaluation_history()` · `agent_evaluation_get` · `GET /agents/{agent_id}/evaluation`, `/evaluation/history` | Per-`(agent, dimension, domain)` score, bi-temporal supersession. Not user-created. | | [`anchor`](/smartmemory/guides/memory-operations/) | `ingest_structured(schema="anchor")` · `memory_anchor_set` · `POST /anchors/create` | `memory_anchor_list` · `GET /anchors/list` | `graduate` → decision · `memory_anchor_graduate`, `check_drift` / `clear`. Fields: `anchor_type`, `session_id`. | ## Structured & telemetry types [#structured--telemetry-types] Schema-mapped data that bypasses the NLP pipeline via `ingest_structured(data, schema=…)` (MCP: `memory_ingest_structured(data, schema_name=…)`, REST: `POST /ingest/structured`). Background: the [Structured Memory Types](/smartmemory/concepts/memory-types/) section. The handler sets the **ingestion strategy**: * **FULL**: embedding + entity extraction (semantically searchable) * **INDEXED**: typed graph node + indexes, no embedding (queried by fields) * **APPEND**: minimal node, recency-only (high-volume telemetry) | Type | Strategy | Dedicated create surface (if any) | Notes / key fields | | ---------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `tool_call` | APPEND | `memory_ingest_structured(schema_name="tool_call")` | Tool telemetry. Fields: `tool_name`, `result`. Read via `memory_search_by_metadata`. | | `conversation_turn` | APPEND | `memory_distill` (turn-pair) / `schema="conversation_turn"` | Conversation replay rows. | | [`conversation`](/smartmemory/examples/conversational-ai/) | not applicable | `ingest_conversation(turns)` · `memory_ingest_conversation` · `POST /ingest/conversation` | Session-chunked container (INDEXED) with `PART_OF` edges. See also [bulk ingestion](/smartmemory/examples/bulk-ingestion/). | | `document` | not applicable | `ingest_document(source)` · `memory_ingest_document` · `POST /ingest/document` | URL/file ingest, chunked. | | `hook_capture` | APPEND | `schema="hook_capture"` | Hook event telemetry. | | `timeline_entry` | INDEXED | `schema="timeline_entry"` | Derived FS-canonical vault timeline index. | | `seed_item` | FULL | `schema="seed_item"` | Reference knowledge from seed packs (searchable). | ## Notes [#notes] * **Two recall channels.** Knowledge-layer types are filtered by `search(memory_type=…)`. Expertise-layer types come back together as a typed dict from `search(query, expertise=True)`. * **Origin tiers govern visibility.** Derived types (`opinion`, `observation`, and evolver output) carry tier-3 origins, searchable but not recalled by default. See the origin taxonomy in the [SmartMemory API](/smartmemory/reference/smart-memory/). * **Auto-classification.** `ingest()` picks the knowledge-layer type for you. Pass `memory_type=` to `add()` to force one. ## See also [#see-also] * [Memory Types](/smartmemory/concepts/memory-types/), the conceptual model behind each type * [SmartMemory API](/smartmemory/reference/smart-memory/), full SDK method signatures * [MCP Tools](/smartmemory/reference/tools/), full tool signatures and tiers # Progress Events (/smartmemory/reference/progress-events) *** ## title: Progress Events [#title-progress-events] SmartMemory publishes real-time progress for long-running operations (pipeline ingests, evolution runs, grounding passes, Studio-scheduled jobs) through a single, workspace-scoped event bus on Redis Streams. Consumers subscribe over Server-Sent Events at `/memory/progress/stream`. The contract is defined once at `docs/features/PLAT-PROGRESS-1/progress-event-contract.json` and used verbatim across producer, transport, and SDK layers. ## Event shape [#event-shape] ```json { "run_id": "uuid-of-parent-operation", "scope": "workspace:", "seq": 42, "ts": 1713096000.123, "kind": "pipeline.stage", "status": "progress", "stage": "extract", "payload": { "progress": 50, "message": "Extracting entities" } } ``` Fields per the v1.5.0 contract: * `run_id` — UUID of the parent operation; required for replay queries. * `scope` — `"workspace:"`. Derived server-side from the caller's JWT workspace; never accepted as a client-supplied override. * `seq` — monotonic per-run sequence. Clients order and dedupe on this. * `ts` — server-side epoch seconds at emission. * `kind` — dotted, producer-owned tag. Consumers filter on this. No transport-level registry. * `status` — one of `started | progress | ok | warn | error`. * `stage` — pipeline stage name when applicable. * `payload` — kind-specific object. Validation happens at producer and consumer; the transport is untyped. ### Common `kind` values [#common-kind-values] * `pipeline.stage` — core ingest pipeline per-stage progress * `evolver.result` — evolution stage outcomes (when `CORE-EVO-REPORT-1` lands) * `graph.node` / `graph.edge` — graph viewer drip feed * `sweep.iter` — parameter sweep iteration * `batch.item` — batch ingest per-item * `search.hop` — multi-hop retrieval trace * `studio.job` — Studio-scheduled stage jobs ## Producing events [#producing-events] Server-side Python producers use the three helpers in `smartmemory.progress`: ```python from smartmemory.progress import bind, unbind, bound_progress, emit # Bind run + scope once per operation (reads contextvars), emit freely. with bound_progress(run_id="run-42", scope="workspace:team-alpha"): emit(kind="pipeline.stage", status="progress", stage="extract", payload={"progress": 50, "message": "Extracting entities"}) # ...more emits inside the same run ``` The context-managed form is required inside any path that runs in a fresh context (FastAPI `BackgroundTasks`, `asyncio.create_task`, `threading.Thread`) — contextvars set by a calling request handler do **not** propagate into those. Environment: * `SMARTMEMORY_PROGRESS_MAXLEN` — approximate ring-buffer length per workspace stream (default `10000`). Bump to 50k-100k for batch-heavy deployments where a single run can emit tens of thousands of events before any client reconnects. ## Subscribing — SSE endpoint [#subscribing--sse-endpoint] `GET /memory/progress/stream` Three modes depending on query params: | Mode | Query | Behavior | | -------------- | -------------------------- | -------------------------------------------------------------------- | | `live` | *(none)* | `XREAD BLOCK` at the tail of the caller's workspace stream | | `run_replay` | `run_id=&from_seq=0` | `XRANGE` the full run, then transition to live tail | | `scope_resume` | `since=` | Resume from a specific stream ID (used by `Last-Event-ID` reconnect) | `from_seq` requires `run_id`. `from_seq` and `since` are mutually exclusive. Auth: `Authorization: Bearer ` or `X-API-Key: `. Workspace membership required. The scope is server-derived; no client query param can override it. Browser clients must use a fetch-based SSE transport (native `EventSource` cannot attach custom headers). Frame format: ``` id: 1713096000123-0 data: { "run_id": "...", "scope": "workspace:...", ... } : keepalive ``` Keepalive comments arrive every 15 seconds to prevent idle-connection timeouts. Errors: | Code | Meaning | | ---- | -------------------------------------------------------------------------------------- | | 400 | Invalid query combination (e.g., `from_seq` without `run_id`, or `from_seq` + `since`) | | 401 | Missing or invalid auth | | 403 | Authenticated user has no membership in any workspace | | 404 | `run_id` specified but no events for that run in the current stream window | ## JS SDK subscriber (`@internal`) [#js-sdk-subscriber-internal] The JS SDK provides a reconnect-aware subscriber at `@smartmemory/sdk-js/progress`. It is marked `@internal` — consumed by the first-party UIs (`smart-memory-graph`, `smart-memory-studio`, `smart-memory-insights`) and not listed in the public API index. ```javascript import { subscribeProgress } from '@smartmemory/sdk-js/progress'; const sub = subscribeProgress({ runId: 'run-42', fromSeq: 0, // replay from start of the run token: '', // or apiKey onEvent: (event) => { if (event.kind !== 'studio.job') return; console.log(event.payload.progress, event.payload.message); if (event.status === 'ok' || event.status === 'error') sub.close(); }, onError: (err) => console.error('SSE fatal', err), onReconnect: () => console.log('reconnecting…'), }); ``` The SDK uses `@microsoft/fetch-event-source` under the hood for header-based auth. Reconnect + `Last-Event-ID` resume is automatic. ## Related [#related] * [Graph Viewer](/smartmemory/guides/graph-viewer/) — consumes the live progress feed * [Bulk Ingestion](/smartmemory/examples/bulk-ingestion/) — the longest-running producer of these events # Basic initialization (/smartmemory/reference/smart-memory) The `SmartMemory` class is the main entry point for all memory operations. It provides a unified interface for storing, retrieving, and managing different types of memories with intelligent processing capabilities. ## Class: SmartMemory [#class-smartmemory] ```python from smartmemory import SmartMemory memory = SmartMemory( config_path: Optional[str] = None, config: Optional[Dict] = None, **kwargs ) ``` ### Constructor Parameters [#constructor-parameters] | Parameter | Type | Default | Description | | ------------- | ---------------- | ------- | ----------------------------------------- | | `config_path` | `Optional[str]` | `None` | Path to JSON configuration file | | `config` | `Optional[Dict]` | `None` | Configuration dictionary (overrides file) | | `**kwargs` | `Any` | - | Additional configuration options | ### Example [#example] ```python memory = SmartMemory() # With custom configuration memory = SmartMemory(config_path="config.json") # With inline configuration memory = SmartMemory(config={ "graph": {"backend": "FalkorDBBackend"} }) ``` ## Core Methods [#core-methods] ### ingest() [#ingest] Ingest content with full 11-stage intelligent processing pipeline: ``` classify → coreference → simplify → entity_ruler → llm_extract → ontology_constrain → store → link → enrich → ground → evolve ``` ```python def ingest( self, item: Union[str, Dict, MemoryItem], context: Optional[Dict] = None, adapter_name: Optional[str] = None, converter_name: Optional[str] = None, extractor_name: Optional[str] = None, enricher_names: Optional[List[str]] = None, conversation_context: Optional[Union[ConversationContext, Dict[str, Any]]] = None, pipeline_config: Optional[PipelineConfigBundle] = None, sync: Optional[bool] = None, auto_challenge: Optional[bool] = None, extract_decisions: Optional[bool] = None, extract_reasoning: bool = False, **kwargs ) -> Union[str, Dict[str, Any]] ``` #### Parameters [#parameters] | Parameter | Type | Description | | ---------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `item` | `Union[str, Dict, MemoryItem]` | Content to ingest | | `context` | `Optional[Dict]` | Additional context for processing | | `adapter_name` | `Optional[str]` | Specific adapter to use | | `converter_name` | `Optional[str]` | Specific converter to use | | `extractor_name` | `Optional[str]` | Specific extractor to use | | `enricher_names` | `Optional[List[str]]` | Specific enrichers to use | | `conversation_context` | `Optional[Union[ConversationContext, Dict]]` | Conversation context | | `pipeline_config` | `Optional[PipelineConfigBundle]` | Per-call pipeline configuration override | | `sync` | `Optional[bool]` | If `None` (default), resolves to `sync=True` in local mode. If `False`, queue for background processing | | `auto_challenge` | `Optional[bool]` | If True, always challenge. If False, never challenge. If None (default), use smart triggering | | `extract_decisions` | `Optional[bool]` | If `None` (default), decision extraction follows the default and pipeline profile, which is on. If True, force it on. If False, turn it off for this call (a warning is logged) | | `extract_reasoning` | `bool` | If True, extract reasoning traces during ingestion (default `False`) | | `**kwargs` | `Any` | Additional processing options (e.g. `user_id` is absorbed here, not a named parameter) | #### Returns [#returns] * `str` - The item\_id (when sync=True) * `Dict[str, Any]` - `{"item_id": str, "queued": bool}` (when sync=False) * `Dict[str, Any]` - A receipt with a `status` of `created`, `unchanged`, `replaced` or `held` (re-imports with a stable source, and low-confidence outcomes). Check `status` before using `item_id`. **Low-confidence extraction.** SmartMemory always keeps your content when extraction is uncertain: * If some extracted entities or relations score below the confidence threshold (0.5), only those are dropped. The memory and the rest of its extraction are stored, and the stored memory records what was dropped under `extraction_pruned`. * If every extracted signal scores below the threshold, the memory is stored without entities or relations. The receipt has `status: "created"` and an `extraction_held` object with the reason, the confidence and the threshold. * `held` (with `item_id: None`) means nothing was stored. It happens when no extraction signal was measured and you passed a `confidence` below the threshold. Operators can restore the stricter earlier behavior, where one low signal holds the whole item, with `SMARTMEMORY_LOW_CONFIDENCE_MODE=veto`. #### Examples [#examples] ```python # Simple ingestion (full pipeline) item_id = memory.ingest("I learned Python programming in 2020") # With user isolation item_id = memory.ingest( "Meeting with John about project timeline", user_id="alice" ) # Async ingestion (queue for background) result = memory.ingest("Large document...", sync=False) print(f"Queued: {result['item_id']}, queued: {result['queued']}") # With conversation context from smartmemory.conversation.context import ConversationContext conv_ctx = ConversationContext(conversation_id="conv_123") item_id = memory.ingest( "User prefers Python for data science", conversation_context=conv_ctx ) ``` *** ### add() [#add] Simple storage without the full ingestion pipeline. Use for internal operations, derived items, or when pipeline is not needed. ```python def add( self, item: Union[str, Dict, MemoryItem], **kwargs ) -> str # item_id ``` #### Parameters [#parameters-1] | Parameter | Type | Description | | ---------- | ------------------------------ | -------------------------- | | `item` | `Union[str, Dict, MemoryItem]` | Content to store | | `**kwargs` | `Any` | Additional storage options | #### Returns [#returns-1] `str` - The item\_id of the stored memory item #### Examples [#examples-1] ```python from smartmemory import MemoryItem # Simple storage (no extraction/linking/evolution) item = MemoryItem(content="Quick note", memory_type="semantic") item_id = memory.add(item) # With metadata item = MemoryItem( content="Derived fact", memory_type="semantic", metadata={"source": "enrichment"} ) item_id = memory.add(item) ``` **When to use which:** * `ingest()` - User input, external data, anything needing full processing * `add()` - Internal operations, derived items, evolution/enrichment output *** ### ingest\_structured() [#ingest_structured] Ingest structured data via a registered handler, bypassing the NLP pipeline. The handler determines the ingestion strategy (FULL/INDEXED/APPEND), validates input, and declares edges and indexes. ```python def ingest_structured( self, data: dict, schema: str, **kwargs ) -> str # item_id ``` #### Parameters [#parameters-2] | Parameter | Type | Description | | ---------- | ------ | ------------------------------------------- | | `data` | `dict` | Structured data matching the handler schema | | `schema` | `str` | Handler schema name | | `**kwargs` | `Any` | Forwarded to `add()` | #### Returns [#returns-2] `str` - The item\_id of the stored item #### Available Schemas [#available-schemas] | Schema | Strategy | Description | | ------------------- | ------------- | ---------------------------------------- | | `decision` | INDEXED | Decision records with status tracking | | `plan` | INDEXED | Plan documents with phase/task hierarchy | | `plan_task` | INDEXED | Individual plan tasks | | `code_entity` | INDEXED+embed | Code symbols with embeddings | | `seed_item` | FULL | Full pipeline seed items | | `tool_call` | APPEND | Tool invocation records | | `conversation_turn` | APPEND | Conversation history turns | | `hook_capture` | APPEND | Hook capture events | #### Examples [#examples-2] ```python # Store a decision item_id = memory.ingest_structured( data={ "title": "Use FalkorDB for graph storage", "status": "accepted", "rationale": "Native graph + vector support", "alternatives": ["Neo4j", "ArangoDB"] }, schema="decision" ) # Record a tool call item_id = memory.ingest_structured( data={ "tool": "memory_search", "input": {"query": "Python frameworks"}, "output": {"results": 5} }, schema="tool_call" ) # Store a plan via PlanManager item_id = memory.ingest_structured( data={ "title": "Migration Plan", "phases": [{"name": "Phase 1", "tasks": ["Setup", "Migrate"]}] }, schema="plan" ) ``` *** ### ingest\_code() [#ingest_code] Index a code repository, creating searchable graph nodes for classes, functions, routes, and tests. ```python def ingest_code( self, directory: str, repo: str, commit_hash: str | None = None, exclude_dirs: set[str] | None = None, languages: list[str] | None = None, ) -> IndexResult ``` #### Parameters [#parameters-3] | Parameter | Type | Default | Description | | -------------- | ------------------- | ------------ | ----------------------------------------------- | | `directory` | `str` | - | Path to the repository root | | `repo` | `str` | - | Repository identifier | | `commit_hash` | `str \| None` | `None` | Commit hash (auto-detected from git if omitted) | | `exclude_dirs` | `set[str] \| None` | `None` | Directories to skip during indexing | | `languages` | `list[str] \| None` | `["python"]` | Languages to index | #### Returns [#returns-3] `IndexResult` with the following fields: | Field | Type | Description | | ---------------------- | ------- | ------------------------------------- | | `entities_created` | `int` | Number of code entity nodes created | | `edges_created` | `int` | Number of relationship edges created | | `embeddings_generated` | `int` | Number of vector embeddings generated | | `elapsed_seconds` | `float` | Total indexing time | #### Example [#example-1] ```python # Index a Python repository result = memory.ingest_code( directory="/path/to/my-project", repo="my-project", exclude_dirs={"node_modules", ".venv", "__pycache__"} ) print(f"Indexed {result.entities_created} entities") print(f"Created {result.edges_created} edges") print(f"Generated {result.embeddings_generated} embeddings") print(f"Completed in {result.elapsed_seconds:.1f}s") ``` *** ### search() [#search] Search for memories using various strategies. ```python def search( self, query: str, top_k: Optional[int] = None, memory_type: str = None, conversation_context: Optional[Union[ConversationContext, Dict[str, Any]]] = None, decompose_query: bool = False, origin: Optional[str] = None, exclude_origins: Optional[list] = None, channel_weights: Optional[dict] = None, multi_hop: bool = False, max_hops: int = 3, budget_ms: int = 1500, semantic_hops: bool = False, hop_strategy: Optional[str] = None, cleanup: bool = False, attention_fusion=None, include_superseded: bool = False, include_retracted: bool = False, include_archived: bool = False, include_consolidated: bool = False, consolidation_first: bool = False, expertise: bool = False, as_of_date: Optional[Any] = None, as_of_strict: bool = False, since: Optional[datetime | str] = None, until: Optional[datetime | str] = None, sort_by: Optional[str] = None, reranker: Optional[str] = None, domain: Optional[str] = None, metadata_filter: Optional[dict] = None, use_ssg: Optional[bool] = None, enable_hybrid: bool = True, include_reference: bool = False, memory_types: Optional[list[str]] = None, limit: Optional[int] = None, ) -> SearchResults ``` This is the signature in `smartmemory/smart_memory.py` (`SmartMemory.search`) in smart-memory-core. `SearchResults` behaves like a list of `MemoryItem`. Run `help(memory.search)` to see the docstring for your installed version. #### Cross-encoder evidence on search results [#cross-encoder-evidence-on-search-results] `GET /memory/search` and `POST /memory/search` return these fields on each item, including expertise buckets and citation modes. Python SDK `MemoryItem` preserves them; the JavaScript SDK passes them through. They expose existing inference only and do not apply an admission threshold or change ranking. | Field | Meaning | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `rerank_score` | Finite prediction value, or `null` when unverified. A raw logit for the default Identity model; separate from calibrated `score`. | | `rerank_status` | `scored`, `unscored_tail`, `bypass_disabled`, `bypass_pending`, `bypass_memory_type`, `bypass_recency`, `bypass_empty_query`, `bypass_small_pool`, `model_unavailable`, `prediction_failed`, `invalid_score`, `post_rerank_insertion`, or `not_reranked`. | | `rerank_model` | Actual loaded model `{name, revision, activation}`, or `null` when no model was obtained. Unknown revision/activation is explicitly `null`. | | `rerank_pool_size` | Candidate pool entering reranking, before final truncation. | | `rerank_candidate_count` | Pairs submitted to prediction, including failed attempts; zero for bypass/cold. | | `rerank_scored_count` | Finite scores obtained before final truncation. | | `rerank_pool_capped` | Whether a warm reranker selected a smaller head and left an unscored tail. | | `rerank_max_doc_chars` | Effective prefix limit when prediction was attempted; otherwise `null`. | `null` never means zero or implicitly relevant. An unscored tail can move during later temporal/strength ordering, so check each returned item's status. Items inserted after reranking are also unverified for that query. Uninstrumented/legacy paths report `not_reranked` with null diagnostics. Missing fields from older servers also provide no evidence of relevance. The ordinary search call currently uses effective defaults of 25 candidates and 512 document characters; custom `RerankerConfig.max_candidates` and `max_doc_chars` remain unforwarded on this path because activating them would change behavior. Counts describe the original pool, not the returned page. Unknown model revision or activation is insufficient evidence for comparing thresholds across model deployments. No relevance gate or threshold is shipped by this exposure change. #### Parameters [#parameters-4] The parameters you will use most: | Parameter | Type | Default | Description | | ---------------------------------------- | ---------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------ | | `query` | `str` | - | Search query string | | `top_k` | `Optional[int]` | `None` (resolves to `5`) | Maximum number of results. `0` returns no results | | `limit` | `Optional[int]` | `None` | Alias for `top_k`. Passing both is allowed only when the values agree | | `memory_type` | `Optional[str]` | `None` | Filter by memory type | | `metadata_filter` | `Optional[dict]` | `None` | Keep only results whose stored fields match. See [Filtering by metadata](#filtering-by-metadata) | | `origin` / `exclude_origins` | `Optional[str]` / `Optional[list]` | `None` | Keep items whose origin starts with a prefix, or drop items with any of the listed prefixes | | `since` / `until` | `datetime` or ISO-8601 `str` | `None` | `created_at` window. `since` is inclusive and `until` is exclusive. A naive time means UTC | | `include_superseded` | `bool` | `False` | Include memories that a newer memory has replaced | | `include_retracted` / `include_archived` | `bool` | `False` | Include withdrawn or archived memories, which are hidden by default | | `as_of_date` | `datetime` or ISO-8601 `str` | `None` | Return the beliefs the system held at that moment | | `multi_hop` / `max_hops` / `budget_ms` | `bool` / `int` / `int` | `False` / `3` / `1500` | Follow entities across results, with a hop limit (1 to 10) and a time budget in milliseconds | | `reranker` | `Optional[str]` | `None` | A registered reranker name for this call. `"none"` keeps retrieval order | | `domain` | `Optional[str]` | `None` | Search one registered domain such as `"code"`. Plain retrieval only | | `expertise` | `bool` | `False` | Return expertise buckets instead of a flat result list | The remaining parameters in the signature (`conversation_context`, `decompose_query`, `channel_weights`, `semantic_hops`, `hop_strategy`, `cleanup`, `attention_fusion`, `include_consolidated`, `consolidation_first`, `as_of_strict`, `sort_by`, `use_ssg`, `enable_hybrid`, `include_reference`, `memory_types`) tune ranking and retrieval lanes. Their behavior is described in the method docstring. **Note:** User/tenant filtering is handled automatically by `ScopeProvider`. No manual `user_id` parameter needed. #### Returns [#returns-4] * `SearchResults` (a list of `MemoryItem`, plus diagnostics for the call) for standard search * `dict[str, list]` when `expertise=True`, keyed by `decision`, `constraint`, `learned`, `opinion`, `reasoning`, and `observation` #### Filtering by metadata [#filtering-by-metadata] `metadata_filter` takes a dict. A plain value means equality. A dict with one of `gte`, `gt`, `lte` or `lt` means a range. Keys name stored fields, either top-level fields such as `memory_type` and `origin` or your own metadata keys. ```python memory.search("the bridge", metadata_filter={"npc": "mira"}) memory.search("bridge", metadata_filter={"npc": "mira"}, include_superseded=True) memory.search("invoices", metadata_filter={"amount": {"gte": 1000}}) ``` Any other operator, such as `$in` or `contains`, raises `ValueError` rather than matching nothing. The filter runs on the results that retrieval already returned, not inside the query. Matches ranked below the `top_k` cutoff can be missing, and the result's `coverage.metadata_filter_incomplete` flag reports when that happened. To get every match, use `search_by_metadata(metadata_filters, top_k=5, since=None, until=None)`, which puts the same conditions into the graph query itself. `metadata_filter` cannot be combined with `expertise=True`. #### Examples [#examples-3] ```python # Basic search results = memory.search("Python programming") # Type-specific search semantic_results = memory.search("France", memory_type="semantic") episodic_results = memory.search("meeting", memory_type="episodic") # Limited results top_results = memory.search("AI", top_k=3) # Expertise search returns buckets by expertise type expertise_results = memory.search("storage decisions", expertise=True) for decision in expertise_results["decision"]: print(decision.decision_id) ``` ### Expertise Capture [#expertise-capture] The expertise capture methods return typed objects, not string IDs. ```python def add_decision(self, content: str, **kwargs) -> "Decision" def add_constraint(self, content: str, **kwargs) -> "Constraint" def add_learning(self, content: str, **kwargs) -> "Learned" ``` * `add_decision(...)` returns a `Decision` with `content`, `rejected_alternatives`, `rationale`, `decision_type`, `status`, `decision_id`, `confidence`, `constraints`, and `domain`. * `add_constraint(...)` returns a `Constraint` with `.constraint_id`. * `add_learning(...)` returns a `Learned` with `.learned_id`. ### get() [#get] Retrieve a specific memory item by ID. ```python def get(self, item_id: str, **kwargs) -> Optional[MemoryItem] ``` #### Parameters [#parameters-5] | Parameter | Type | Description | | --------- | ----- | -------------- | | `item_id` | `str` | Memory item ID | #### Returns [#returns-5] `Optional[MemoryItem]` - The memory item if found, None otherwise #### Example [#example-2] ```python # Get specific memory memory_item = memory.get("memory_id_123") if memory_item: print(f"Content: {memory_item.content}") ``` ### update() [#update] Update an existing memory item. ```python def update(self, item: Union[MemoryItem, Dict]) -> str # item_id ``` #### Parameters [#parameters-6] | Parameter | Type | Description | | --------- | ------------------------- | --------------------------- | | `item` | `Union[MemoryItem, Dict]` | Updated memory item or data | #### Returns [#returns-6] `str` - The item\_id of the updated item #### Example [#example-3] ```python # Update existing memory memory_item = memory.get("memory_id_123") memory_item.content = "Updated content" updated_item = memory.update(memory_item) ``` ### delete() [#delete] Delete a memory item. ```python def delete(self, item_id: str, **kwargs) -> bool ``` #### Parameters [#parameters-7] | Parameter | Type | Description | | --------- | ----- | ------------------------ | | `item_id` | `str` | Memory item ID to delete | #### Returns [#returns-7] `bool` - True if deleted successfully, False otherwise #### Example [#example-4] ```python # Delete memory success = memory.delete("memory_id_123") print(f"Deleted: {success}") ``` ## MemoryItem Fields [#memoryitem-fields] The `MemoryItem` class represents a single memory record. All fields are available on items returned by `get()`, `search()`, and other retrieval methods. ```python from smartmemory import MemoryItem ``` | Field | Type | Default | Description | | ------------------ | ----------------------- | ------------------- | --------------------------------------------------------------------- | | `content` | `str` | - | The memory content | | `memory_type` | `str` | `"semantic"` | Memory type (see [Memory Types](/smartmemory/concepts/memory-types/)) | | `item_id` | `str` | auto-generated UUID | Unique identifier | | `origin` | `str` | `"unknown"` | Provenance tracking (see origin taxonomy below) | | `confidence` | `float` | `1.0` | Confidence score (0.0-1.0), affected by reinforcement/contradiction | | `stale` | `bool` | `False` | Demotion flag, set when `valid_end_time < now` | | `access_count` | `int` | `0` | Retrieval counter, incremented on each access | | `last_accessed` | `Optional[datetime]` | `None` | Timestamp of last retrieval | | `derived_from` | `Optional[str]` | `None` | Parent item\_id for lineage (creates `DERIVED_FROM` edge) | | `reference` | `bool` | `False` | Reference data excluded from default search results | | `metadata` | `dict` | `{}` | Arbitrary metadata dictionary | | `embedding` | `Optional[List[float]]` | `None` | Vector embedding for similarity search | | `valid_start_time` | `Optional[datetime]` | `None` | Bitemporal valid time start | | `valid_end_time` | `Optional[datetime]` | `None` | Bitemporal valid time end | | `transaction_time` | `datetime` | auto-set | When the record was persisted | ### Origin Taxonomy [#origin-taxonomy] The `origin` field tracks how a memory was created. Prefixes are organized into 4 visibility tiers in `smartmemory/origin_policy.py`. Common patterns: | Pattern | Description | Example | | -------------- | --------------------------------- | -------------------------------------------------------- | | `user:*` | User-submitted content | `user:ingest`, `user:api` | | `cli:*` | Created from the CLI | `cli:add`, `cli:ingest` | | `api:*` | Created from the REST API | `api:ingest` | | `import:*` | Imported from an external source | `import:vault` | | `evolver:*` | Created by evolution plugins | `evolver:episodic_to_semantic`, `evolver:temporal_aging` | | `enricher:*` | Created by enrichment plugins | `enricher:wikipedia`, `enricher:related` | | `structured:*` | Created via `ingest_structured()` | `structured:decision`, `structured:plan` | | `hook:*` | Captured by lifecycle hooks | `hook:persist`, `hook:observe` | | `failure:*` | Captured from failure paths | `failure:extraction`, `failure:grounding` | ## Relationship Methods [#relationship-methods] ### link() [#link] Create explicit relationships between memories. ```python def link( self, source_id: str, target_id: str, link_type: Union[str, LinkType] = "RELATED" ) -> str # "Linked to as " ``` #### Parameters [#parameters-8] | Parameter | Type | Default | Description | | ----------- | ---------------------- | ----------- | -------------------- | | `source_id` | `str` | - | Source memory ID | | `target_id` | `str` | - | Target memory ID | | `link_type` | `Union[str, LinkType]` | `"RELATED"` | Type of relationship | #### Returns [#returns-8] `str` - Confirmation string #### Example [#example-5] ```python # Create relationship memory.link("python_memory", "project_memory", "USED_FOR") ``` ### get\_links() [#get_links] Get all links for a memory item. ```python def get_links( self, item_id: str, memory_type: str = "semantic" ) -> List[str] # List of link/triple strings ``` #### Parameters [#parameters-9] | Parameter | Type | Default | Description | | ------------- | ----- | ------------ | ------------------- | | `item_id` | `str` | - | Memory item ID | | `memory_type` | `str` | `"semantic"` | Memory type context | #### Returns [#returns-9] `List[str]` - List of link/triple strings for all edges involving item\_id #### Example [#example-6] ```python # Get all links links = memory.get_links("memory_id_123") for link in links: print(link) # Prints link string representation ``` ### get\_neighbors() [#get_neighbors] Get neighboring memories through relationships. ```python def get_neighbors(self, item_id: str, direction: str = "both") -> List[MemoryItem] ``` #### Parameters [#parameters-10] | Parameter | Type | Default | Description | | ----------- | ----- | -------- | -------------------------------------------- | | `item_id` | `str` | - | Memory item ID | | `direction` | `str` | `"both"` | Edge direction: `"in"`, `"out"`, or `"both"` | #### Returns [#returns-10] `List[MemoryItem]` - List of neighboring memory items #### Example [#example-7] ```python # Get related memories neighbors = memory.get_neighbors("memory_id_123") print(f"Found {len(neighbors)} related memories") ``` ## Background Processing [#background-processing] SmartMemory supports synchronous and asynchronous ingestion via the `ingest()` method. ```python # Synchronous (process now via full pipeline) memory.ingest("Process immediately", sync=True) # Asynchronous (quick persist + enqueue for background workers) result = memory.ingest("Process in background", sync=False) print(result) # {"item_id": "...", "queued": True} # Note: a separate worker service must consume the background queue. ``` ## Utility Methods [#utility-methods] ### ground() [#ground] Ground a memory item to an external source for provenance and validation. ```python def ground( self, item_id: str, source_url: str, validation: Optional[Dict] = None ) -> None ``` #### Parameters [#parameters-11] | Parameter | Type | Description | | ------------ | ---------------- | ----------------------------------------- | | `item_id` | `str` | ID of the memory item to ground | | `source_url` | `str` | URL of the external source for provenance | | `validation` | `Optional[Dict]` | Optional validation metadata | #### Description [#description] Grounding establishes provenance by linking a memory item to its external source. This is crucial for: * **Fact verification**: Link memories to authoritative sources * **Audit trails**: Track the origin of information * **Quality assurance**: Associate validation metadata * **Transparency**: Enable source traceability for AI decisions #### Example [#example-8] ```python # Ground a memory to its source memory.ground( item_id="fact_123", source_url="https://example.com/article", validation={"confidence": 0.95, "verified_at": "2024-01-01"} ) # Ground with minimal information memory.ground("memory_456", "https://source.com/data") ``` ### clear() [#clear] Clear all memory data. ```python def clear(self) -> None ``` #### Example [#example-9] ```python # Clear all memories (use with caution!) memory.clear() ``` ## Error Handling [#error-handling] SmartMemory does **not** expose a broad public exception hierarchy. Core operations (`add`, `ingest`, `search`) surface standard Python exceptions, backend/connection failures propagate as the underlying driver errors (e.g. `redis`/`falkordb` exceptions), and invalid arguments raise the usual `ValueError` / `TypeError`. Wrap calls in a generic handler: ```python try: result = memory.add("content") results = memory.search("query") except Exception as e: # Backend/connection failures and invalid input both surface here print(f"Memory operation failed: {e}") ``` ### Managed-type errors [#managed-type-errors] The only first-class exceptions SmartMemory raises are from the **managed-type** layer (schema-validated structured types). They live in `smartmemory.managed.exceptions`: ```python from smartmemory.managed.exceptions import ManagedTypeError, QualityGateRejected try: memory.ingest_structured(payload, schema="tool_call") except QualityGateRejected as e: # subclass of ManagedTypeError print(f"Rejected by quality gate: {e}") except ManagedTypeError as e: print(f"Managed-type validation failed: {e}") ``` ## Configuration Properties [#configuration-properties] ### Access Configuration [#access-configuration] ```python # Configuration is set during initialization memory = SmartMemory(config_path="config.json") # Or with inline configuration memory = SmartMemory(config={ "graph": {"backend": "FalkorDBBackend"}, "similarity": {"semantic_weight": 0.5} }) ``` **Note**: Configuration is set at initialization time. Runtime configuration updates are not supported. ## Performance Tips [#performance-tips] 1. **Batch operations** when possible: ```python items = ["item1", "item2", "item3"] for item in items: memory.ingest(item, sync=False) # Enqueue for background workers ``` 2. **Cache frequently accessed memories**: ```python # Store frequently used memory IDs important_memories = ["id1", "id2", "id3"] cached_memories = {id: memory.get(id) for id in important_memories} ``` 3. **Use specific memory types** for better search performance: ```python # More efficient than general search results = memory.search("query", memory_type="semantic") ``` ## Additional Methods [#additional-methods] ### Evolution and Memory Management [#evolution-and-memory-management] #### `run_evolution_cycle()` [#run_evolution_cycle] Trigger a memory evolution cycle for consolidation and optimization. ```python def run_evolution_cycle(self) -> None ``` **Example:** ```python # Trigger evolution cycle memory.run_evolution_cycle() ``` #### `challenge()` [#challenge] Challenge an assertion against existing knowledge to detect contradictions using a multi-method detection cascade. ```python def challenge( self, assertion: str, memory_type: str = "semantic", use_llm: bool = True ) -> ChallengeResult ``` **Parameters:** * `assertion`: The new fact/assertion to challenge * `memory_type`: Type of memory to search (default: "semantic") * `use_llm`: Whether to use LLM for deep contradiction analysis **Returns:** `ChallengeResult` with detected conflicts **Detection Cascade:** 1. **LLM** (if `use_llm=True`) - Most accurate, analyzes nuance and context 2. **Graph** - Detects functional property conflicts (capital, CEO, born in) 3. **Embedding** - High semantic similarity + opposite polarity 4. **Heuristic** - Pattern matching fallback **Example:** ```python result = memory.challenge("Paris is the capital of Germany") if result.has_conflicts: for conflict in result.conflicts: print(f"Contradicts: {conflict.existing_fact}") print(f"Method: {conflict.explanation}") # Shows [LLM], [Graph], etc. print(f"Confidence: {conflict.confidence}") # Fast challenge (skip LLM) result = memory.challenge("Some fact", use_llm=False) ``` **Using AssertionChallenger directly:** ```python from smartmemory.reasoning import AssertionChallenger, ResolutionStrategy challenger = AssertionChallenger( memory, use_llm=True, # LLM detection use_graph=True, # Graph structure analysis use_embedding=True, # Semantic + polarity use_heuristic=True # Pattern matching ) result = challenger.challenge("Paris is the capital of Germany") # Auto-resolve conflicts (tries Wikipedia, LLM, grounding, recency) for conflict in result.conflicts: resolution = challenger.resolve_conflict(conflict, auto_resolve=True) if resolution["auto_resolved"]: print(f"Auto-resolved via {resolution['method']}") print(f"Evidence: {resolution['evidence']}") else: # Fall back to manual strategy challenger.resolve_conflict(conflict, strategy=ResolutionStrategy.DEFER) ``` **Auto-Resolution Methods:** 1. **Wikipedia** - Looks up entities, checks which fact aligns (0.85 confidence) 2. **LLM Reasoning** - GPT fact-checks with reasoning (0.7+ confidence required) 3. **Grounding** - Checks existing provenance/trusted sources (0.75 confidence) 4. **Recency** - For temporal conflicts, prefers recent info (0.65 confidence) #### `run_clustering()` [#run_clustering] Run entity clustering to deduplicate entities across the graph. ```python def run_clustering(self) -> dict ``` **Returns:** Dictionary with clustering statistics **Example:** ```python # Run clustering (SemHash + embedding + LLM) stats = memory.run_clustering() print(f"Merged {stats.get('merged_count', 0)} duplicate entities") print(f"Found {stats.get('clusters_found', 0)} clusters") ``` **Clustering Pipeline:** 1. **SemHash pre-deduplication** - Fast deterministic dedup (0.95 threshold) 2. **KMeans embedding clustering** - Group similar entities (\~128 per cluster) 3. **LLM semantic clustering** - Find aliases (Joe ↔ Joseph, ML ↔ machine learning) 4. **Graph node merging** - Rewire edges, merge properties #### ~~`commit_working_to_episodic()`~~ / ~~`commit_working_to_procedural()`~~ (REMOVED) [#commit_working_to_episodic--commit_working_to_procedural-removed] **Removed in CORE-MEMORY-DYNAMICS-1 M1 (2026-04).** The retroactive-promotion pattern was replaced by the `ConsolidationRouter` pipeline stage, which routes items to episodic/procedural/semantic/drop at ingest time based on content signals. Items awaiting a routing decision live under `memory_type="pending"` (renamed from `"working"`) and the router's decision is observable on `PipelineState.router_decision`. Migration: instead of explicit `commit_working_to_episodic()`, store items with `memory_type="pending"` (or let the pipeline's auto-classification pick a type) and the router will handle consolidation. ### Graph Operations [#graph-operations] #### `add_edge()` [#add_edge] Add an edge between two memory nodes. ```python def add_edge( self, source_id: str, target_id: str, relation_type: str, properties: Optional[Dict] = None ) -> Any ``` #### `create_or_merge_node()` [#create_or_merge_node] Create or merge a node with properties. ```python def create_or_merge_node( self, item_id: str, properties: Dict, memory_type: Optional[str] = None ) -> str ``` ### Monitoring and Analytics [#monitoring-and-analytics] #### `summary()` [#summary] Get summary statistics about memory contents. ```python def summary(self) -> Dict ``` **Example:** ```python stats = memory.summary() print(f"Total items: {stats.get('total_count', 0)}") ``` #### `orphaned_notes()` [#orphaned_notes] Find orphaned memory items (no connections). ```python def orphaned_notes(self) -> List ``` #### `prune()` [#prune] Prune old or unused memories. ```python def prune(self, strategy: str = "old", days: int = 365, **kwargs) -> Any ``` ### Temporal Queries [#temporal-queries] #### `time_travel()` [#time_travel] Context manager for temporal queries at a specific point in time. ```python def time_travel(self, to: str) -> ContextManager ``` **Example:** ```python # Query memories as they existed on a specific date with memory.time_travel("2024-09-01"): results = memory.search("Python") # Results reflect state on Sept 1st, 2024 ``` ### Version Tracking [#version-tracking] SmartMemory provides bi-temporal versioning via `version_tracker`: #### `version_tracker.get_versions()` [#version_trackerget_versions] Get all versions of a memory item. ```python versions = memory.version_tracker.get_versions(item_id) for v in versions: print(f"Version {v.version_number}: {v.content[:50]}...") ``` #### `version_tracker.get_version_at_time()` [#version_trackerget_version_at_time] Get the version that was current at a specific time. ```python from datetime import datetime version = memory.version_tracker.get_version_at_time( item_id="mem_123", time=datetime(2024, 1, 15), time_type='transaction' # or 'valid' ) ``` #### `version_tracker.compare_versions()` [#version_trackercompare_versions] Compare two versions of an item. ```python diff = memory.version_tracker.compare_versions( item_id="mem_123", version1=1, version2=2 ) print(f"Content changed: {diff['content_changed']}") print(f"Metadata changes: {diff['metadata_changes']}") ``` ### Archive Operations [#archive-operations] #### `archive_put()` [#archive_put] Archive a conversation artifact durably. ```python def archive_put( self, conversation_id: str, payload: Union[bytes, Dict[str, Any]], metadata: Dict[str, Any] ) -> Dict[str, str] ``` **Returns:** Dictionary with `archive_uri` and `content_hash` #### `archive_get()` [#archive_get] Retrieve an archived artifact by URI. ```python def archive_get(self, archive_uri: str) -> Union[bytes, Dict[str, Any]] ``` ### Tags and Metadata [#tags-and-metadata] #### `add_tags()` [#add_tags] Add tags to a memory item. ```python def add_tags(self, item_id: str, tags: List[str]) -> bool ``` **Example:** ```python memory.add_tags("memory_123", ["important", "reference", "python"]) ``` #### `update_properties()` [#update_properties] Update memory node properties with merge/replace semantics. ```python def update_properties( self, item_id: str, properties: Dict, write_mode: Optional[str] = None ) -> Any ``` ## Thread Safety [#thread-safety] SmartMemory is thread-safe for read operations but requires coordination for write operations: ```python import threading # Safe for concurrent reads def read_worker(): results = memory.search("query") return results # Coordinate writes write_lock = threading.Lock() def write_worker(content): with write_lock: memory.add(content) ``` ## Graph Integrity Methods [#graph-integrity-methods] *Added in v0.3.8* ### delete\_run() [#delete_run] Delete all entities created by a specific pipeline run. ```python def delete_run(self, run_id: str) -> int ``` #### Parameters [#parameters-12] | Parameter | Type | Description | | --------- | ----- | ------------------------------------------------ | | `run_id` | `str` | Pipeline run identifier (returned by `ingest()`) | #### Returns [#returns-11] `int` - Number of entities deleted #### Example [#example-10] ```python # Ingest returns run_id for tracking result = memory.ingest("Some content about Python and AI") run_id = result.get("run_id") # Later, clean up all entities from that run deleted_count = memory.delete_run(run_id) print(f"Deleted {deleted_count} entities") ``` **Use cases:** * Clean up failed/partial pipeline runs * Re-run extraction on same content without duplicates * Undo ingestion for rollback scenarios ### rename\_entity\_type() [#rename_entity_type] Rename an entity type across all matching entities in the graph. ```python def rename_entity_type(self, old_type: str, new_type: str) -> int ``` #### Parameters [#parameters-13] | Parameter | Type | Description | | ---------- | ----- | ------------------------ | | `old_type` | `str` | Current entity type name | | `new_type` | `str` | New entity type name | #### Returns [#returns-12] `int` - Number of entities updated #### Example [#example-11] ```python # Rename "Framework" entities to "Library" updated = memory.rename_entity_type("Framework", "Library") print(f"Updated {updated} entities") ``` **Use cases:** * Ontology evolution without re-extraction * Fix inconsistent entity type names * Merge similar concepts under canonical names ### merge\_entity\_types() [#merge_entity_types] Merge multiple entity types into a single target type. ```python def merge_entity_types(self, source_types: List[str], target_type: str) -> int ``` #### Parameters [#parameters-14] | Parameter | Type | Description | | -------------- | ----------- | ------------------------------ | | `source_types` | `List[str]` | List of entity types to rename | | `target_type` | `str` | Target entity type name | #### Returns [#returns-13] `int` - Total number of entities updated #### Example [#example-12] ```python # Consolidate fragmented types updated = memory.merge_entity_types( ["JS_Framework", "Python_Framework", "Web_Framework"], "Library" ) print(f"Merged {updated} entities into 'Library' type") ``` **Use cases:** * Consolidate fragmented entity types from different extraction runs * Simplify ontology by collapsing related types * Prepare for ontology template migration *** ## Next Steps [#next-steps] * [Memory Types API](/smartmemory/reference/memory-types-api/) - Specific memory type interfaces * [Tools API](/smartmemory/reference/tools/) - MCP tools and integrations ## Creation-time search windows [#creation-time-search-windows] `SmartMemory.search(query, since=None, until=None)` and `SmartMemory.search_by_metadata(filters, top_k=5, since=None, until=None)` accept ISO-8601 strings or Python datetimes. They filter storage `created_at` using the half-open interval `[since, until)`. Naive inputs mean UTC; offsets are converted to UTC. An invalid timestamp or `since > until` raises `ValueError`. Metadata filters retain equality and also accept the closed operators `gte`, `gt`, `lte`, `lt`, for example `{"created_at": {"gte": "2026-09-01", "lt": "2026-09-03"}}`. Operands are bound query parameters; unknown operators raise. `POST /memory/search` accepts optional `since` and `until` strings. The existing `GET /memory/by-metadata` also accepts them. Invalid windows return HTTP 400 with the offending value. Missing bounds are omitted. Responses carry `coverage` when creation-time accuracy is uncertain, including an explicitly unknown `created_at_backfilled_boundary: null`. Legacy rows may carry their first-update time. Core list results expose this via `results.diagnostics.coverage`. The Python HTTP SDK accepts `since` and `until` on both `search()` and `search_by_metadata()`; `client.last_search_coverage` preserves the search envelope's caveat even for empty results. `as_of_date` selects the historical corpus; creation bounds filter within it. `GET /memory/temporal/search/during` answers a different question: matching **versions** by version timestamp. If version tracking is unavailable it logs a warning and marks current-item rows `range_applied: false`. ## Choosing a multi-hop planner [#choosing-a-multi-hop-planner] `hop_strategy` accepts `consensus`, `relevance`, or `semantic` on `POST /memory/search`, Python SDK `search()`, and MCP `memory_search`. JS SDK uses `memory.search(query, { multiHop: true, hopStrategy: 'relevance' })`. The Python equivalent is `client.search(query, multi_hop=True, hop_strategy="relevance")`. * `consensus` follows entities several top results agree on and may stop before a one-off bridge when distractors lower its frequency score. * `relevance` follows entities from the best results, including one-off bridges. * `semantic` asks an LLM to plan follow-up searches and incurs LLM calls. Omitted/null leaves core resolution unchanged: semantic when semantic hops/config is enabled, otherwise **consensus**. The measured fixture spike kept consensus: bridge F1 improved by 22.92 points with relevance, but median hops increased from 1 to 3, exceeding the pre-registered one-extra-hop bound. This is a fixture result, not a production or LoCoMo quality claim. Invalid strategies return HTTP 400 naming all three values. A supplied strategy with `multi_hop=false` is labelled in response `inert_parameters`; Python SDK retains this in `last_inert_parameters`, and MCP echoes it. CLI: `sm search "which team owns checkout?" --multi-hop --max-hops 3 --hop-strategy relevance`. `--max-hops` accepts 1–10; `--hop-strategy` without `--multi-hop` is rejected. Daemon, local fallback and remote wrapper paths carry the flags. # Surfacing API (/smartmemory/reference/surfacing) API reference for SmartMemory's retrieval-time surfacing layer: the opt-in router, cross-type context assembly, and session pins. Conceptual background lives in [`concepts/memory-dynamics.md`](/smartmemory/concepts/memory-dynamics/). This page is the developer reference. ## Constructor parameter [#constructor-parameter] ### `surfacing_mode` [#surfacing_mode] ```python SmartMemory(..., surfacing_mode: Literal["legacy", "router"] = "legacy") ``` Gates the entire M3 retrieval layer. `"legacy"` (default) preserves the M1a behavior — lexical × recency × activation-with-floor, no spreading, no PPR, `session_pin_boost`/`freshness_boost`/`centrality` stay zero in responses. `"router"` opts into the full dynamics model. ```python from smartmemory import SmartMemory # Default — unchanged from pre-M3 behavior. memory = SmartMemory() # Opt in to router-mode surfacing. memory = SmartMemory(surfacing_mode="router") ``` Invalid values raise `ValueError` at construction time. *** ## `get_working_context()` [#get_working_context] The primary retrieval API. Returns a contract-shaped dict with ranked items for the current turn. ### Signature [#signature] ```python def get_working_context( self, session_id: str, query: str, k: int = 20, max_tokens: Optional[int] = None, strategy: Optional[str] = None, ) -> Dict[str, Any] ``` ### Parameters [#parameters] | Parameter | Type | Default | Description | | ------------ | --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `session_id` | `str` | required | Session scope for anchors and pins. | | `query` | `str` | required | Natural-language query string. | | `k` | `int` | `20` | Max non-mandatory items returned (anchors + pins are additive). Must be `1..100`. | | `max_tokens` | `Optional[int]` | `None` | Soft token budget for returned content (≈ `len(content) // 4`). Mandatory items (anchors + pins) always included even when over budget. | | `strategy` | `Optional[str]` | `None` | Override the router's classification. Valid values in router mode: `"fast:recency"`, `"deep:multi-hop"`, `"factual:point-query"`. Legacy mode only accepts `None` or `"fast:recency"` (others raise). | ### Response shape [#response-shape] ```python { "decision_id": "uuid-hex", # unique per call "strategy_used": "deep:multi-hop", # resolved strategy "items": [ { "item_id": "abc...", "content": "...", "memory_type": "semantic", "metadata": {...}, "score_breakdown": { "activation": 0.73, "relevance": 0.92, "recency": 0.88, "centrality": 0.65, # 0 in legacy mode and Slice A "anchor_forced": False, "session_pin_boost": 0.0, "freshness_boost": 0.15, }, }, ... ], "drift_warnings": [], # reserved, empty in current release "tokens_used": 420, # estimated tokens of returned content "tokens_budget": null, # echo of max_tokens "deprecation": null, # populated via the memory_recall shim } ``` ### Strategy precedence [#strategy-precedence] 1. **Caller-explicit `strategy` wins.** In router mode, whatever the caller passes is honored (reserved strategies soft-degrade to `fast:recency` with a reason flag). 2. **Else, router classifies** from the query text. Heuristic rules: | Strategy | When it fires | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `deep:multi-hop` | Query contains `why`, `how`, `what caused`, or `explain`; OR 2+ detected entities; OR query > 80 chars. | | `factual:point-query` | Short (≤ 6 words), not a question, starts with `list`/`show`/`get`/`find`/`all` OR is a bare noun. Recency signals (`recent`, `latest`, `newest`, `today`, `yesterday`) hard-skip to `fast:recency`. | | `fast:recency` | Default fallback. | 3. **Legacy mode** — any non-`fast:recency` explicit strategy raises `ValueError`. ### Observability [#observability] After each call, `memory.last_surfacing_decision` holds the `SurfacingDecision` that drove the dispatch: ```python memory.get_working_context("s1", "Why did we pick JWT?") memory.last_surfacing_decision # SurfacingDecision( # strategy=SurfacingStrategy.DEEP_MULTI_HOP, # confidence=0.75, # reasons=["keyword:why"], # signals={"query_length": 27, "word_count": 6, "entity_count": 0}, # ) ``` ### Examples [#examples] ```python # Simple turn-level retrieval. resp = memory.get_working_context( session_id="chat-abc", query="What was our decision on JWT?", ) for item in resp["items"]: print(item["item_id"], item["score_breakdown"]["activation"]) # Token-budgeted for an LLM context window. resp = memory.get_working_context( session_id="chat-abc", query="walk me through the auth flow", max_tokens=4000, ) # Force a specific strategy (router mode). resp = memory.get_working_context( session_id="chat-abc", query="user list", strategy="factual:point-query", ) ``` *** ## Session pins [#session-pins] Pins let you force-include specific items in `get_working_context()` results for a session, regardless of lexical match. ### `pin_item()` [#pin_item] ```python def pin_item(self, session_id: str, item_id: str, weight: float = 1.0) -> None ``` Adds a pin for `item_id` in `session_id` with the given weight. The item is force-included in all subsequent `get_working_context(session_id=..., ...)` calls and receives an additive `session_pin_boost = weight × 0.3` in its score breakdown. **Workspace binding.** The first pin in a session binds the session to the current workspace. Subsequent pins that disagree raise `SessionPinWorkspaceConflict` (a `ValueError` subclass). ### `unpin_item()` [#unpin_item] ```python def unpin_item(self, session_id: str, item_id: str) -> None ``` Removes the pin. No-op if the pin doesn't exist. ### `get_pins()` [#get_pins] ```python def get_pins(self, session_id: str) -> Dict[str, float] ``` Returns the current pin set for `session_id` as `{item_id: weight}`. Returns `{}` for unknown sessions, expired sessions, or sessions bound to a different workspace than the current `SmartMemory` instance (cross-workspace leak guard). ### Example [#example] ```python from smartmemory import SmartMemory from smartmemory.session.pins import SessionPinWorkspaceConflict memory = SmartMemory(surfacing_mode="router") memory.pin_item("session-1", "project-requirements", weight=1.0) memory.pin_item("session-1", "style-guide", weight=0.5) memory.get_pins("session-1") # {"project-requirements": 1.0, "style-guide": 0.5} # Pinned items force-include regardless of query. resp = memory.get_working_context("session-1", "anything") # Both pinned items appear with session_pin_boost > 0. memory.unpin_item("session-1", "style-guide") # Cross-workspace pin raises. other_memory = SmartMemory(surfacing_mode="router", scope_provider=other_ws) try: other_memory.pin_item("session-1", "some-item", weight=1.0) except SessionPinWorkspaceConflict: # Session-1 is bound to the first workspace. pass ``` ### TTL and cleanup [#ttl-and-cleanup] Pins and the session's workspace binding both have a 24-hour Redis TTL. Callers should unpin explicitly at session close when possible; the TTL is a safety net for forgotten cleanups, not a primary expiry mechanism. *** ## Strategy enum [#strategy-enum] ```python from smartmemory.search.surfacing_router import SurfacingStrategy SurfacingStrategy.FAST_RECENCY # "fast:recency" — shipped SurfacingStrategy.DEEP_MULTI_HOP # "deep:multi-hop" — shipped SurfacingStrategy.FACTUAL_POINT # "factual:point-query" — shipped SurfacingStrategy.PROCEDURAL_LOOKUP # "procedural:lookup" — reserved, soft-degrades to fast:recency SurfacingStrategy.TEMPORAL_WINDOWED # "temporal:windowed" — reserved, soft-degrades to fast:recency ``` *** ## Spreading and PPR internals [#spreading-and-ppr-internals] These are called automatically by `get_working_context()` in router mode — most callers don't need them directly. Exposed for advanced use (custom retrieval pipelines, benchmarking). ### `spread()` [#spread] ```python from smartmemory.search.spreading_activation import spread result = spread( memory, seed_item_ids=["item-a", "item-b"], depth=2, max_fan_out=20, damping=0.5, edge_types=None, # None → default allowlist (see below) budget_ms=150, ) # {item_id: propagated_score, ...} ``` Default edge allowlist: `RELATED, RELATES_TO, CO_RETRIEVED, ANSWERED_BY, PART_OF, MENTIONS, MENTIONED_IN, CONTAINS_ENTITY, LINKS_TO, LINKED_TO, DERIVED_FROM, INFERRED_FROM, HAS_SKILL, HAS_CONTEXT, TAGGED_WITH, ALIAS_OF`. Excluded by default: `CONTRADICTS`, `SUPERSEDES`, `GROUNDED_IN` (design rationale: contradictions shouldn't propagate boosts; supersession is a replacement relation; grounding edges lead to global Wikidata entities the workspace filter would skip anyway). Returns `{}` on empty seeds, no graph hits, or backend timeout. Never raises for budget exhaustion. ### `personalized_pagerank()` [#personalized_pagerank] ```python from smartmemory.search.ppr import personalized_pagerank scores = personalized_pagerank( memory, seed_item_ids=["item-a"], alpha=0.85, top_k=100, budget_ms=200, cache=memory._ppr_cache(), # optional — enables the Redis cache workspace_id="ws-a", # required when cache is provided ) ``` Runs NetworkX's exact `pagerank` against the materialized workspace subgraph. Bounded-concurrency: at most 2 concurrent tasks per process (`threading.BoundedSemaphore(2)`); additional callers short-circuit to `{}` with a WARN log. Workspaces exceeding `max_nodes=10000` return `{}` (PPR skipped + INFO log); the returned cache is empty and `centrality` stays 0 in the composite. *** ## Related documentation [#related-documentation] * [`concepts/memory-dynamics.md`](/smartmemory/concepts/memory-dynamics/) — conceptual overview * [`guides/tuning-surfacing.md`](/smartmemory/guides/tuning-surfacing/) — task-oriented configuration guide * [`api/smart-memory.md`](/smartmemory/reference/smart-memory/) — the rest of the SmartMemory class # MCP Tools API Reference (/smartmemory/reference/tools) The `smart-memory-mcp` package exposes SmartMemory operations as Model Context Protocol tools. Tools are defined with FastMCP's `@mcp.tool()` decorator and registered into one of three tiers: * **FREE** — registered for every session (26 tools, including the decision tools). * **PRO** — unlocked after `login(api_key)` (58 tools). * **PRO+** — additionally requires `SMARTMEMORY_MCP_FULL_TOOLS=true` (89 tools). All tools return strings (or, for `get_working_context`, a structured dict). Parameter types below are taken directly from the function signatures in `smart-memory-mcp/smartmemory_mcp/tools/`. For the integration walkthrough, see [`guides/mcp-integration.md`](/smartmemory/guides/mcp-integration/). For the underlying Python API the tools wrap, see [`api/smart-memory.md`](/smartmemory/reference/smart-memory/). ## Auth & session (FREE) [#auth--session-free] | Tool | Signature | Description | | ------------------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `login` | `(api_key: str)` | Authenticate with a SmartMemory API key. Restart server to unlock PRO tools. | | `whoami` | `()` | Show current session: user, tenant, workspace, tier, backend mode. | | `switch_workspace` | `(workspace: str, save_as_default: bool = False)` | Switch the session to another of your workspaces by id or name. No restart. `save_as_default` is local server only. | | `switch_team` | `(team_id: str)` | Deprecated alias of `switch_workspace`, removed in the next release. | ```json { "name": "login", "arguments": { "api_key": "sm_live_..." } } ``` ## Memory CRUD (FREE + PRO) [#memory-crud-free--pro] ### FREE tier [#free-tier] | Tool | Signature | Description | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `memory_ingest` | `(content: str, memory_type: str = "semantic")` | Run content through the full 11-stage pipeline with extraction. | | `memory_search` | `(query: str, top_k: int = 5, memory_type: str = None, enable_hybrid: bool = True, catalog_mode: bool = True, decompose: bool = False, channel_weights: dict = None, multi_hop: bool = False, max_hops: int = 3, budget_ms: int = 1500)` | Semantic similarity search with optional hybrid / decompose / multi-hop. | | `memory_recall` | `(query: str, session_id: str = None, top_k: int = 5)` | **Deprecated** — use `get_working_context`. | | `get_working_context` | `(session_id: str, query: str, k: int = 20, max_tokens: int = None, strategy: str = None) -> dict` | Structured surfacing response (CORE-MEMORY-DYNAMICS-1 M1a). | | `memory_get` | `(item_id: str)` | Retrieve a memory item by ID with full content and metadata. | | `memory_feedback` | `(search_session_id: str, result_used: list[str])` | Report which results from a previous `memory_search` were used. Registered unconditionally (before the PRO gate). | ```json { "name": "memory_search", "arguments": { "query": "auth caching", "top_k": 5, "multi_hop": true } } ``` ### PRO tier [#pro-tier] | Tool | Signature | Description | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------- | | `memory_add` | `(content: str, memory_type: str = "semantic", metadata: dict = None, tags: list[str] = None)` | Add a memory item directly without the NLP pipeline. | | `memory_update` | `(item_id: str, content: str = None, metadata: dict = None, properties: dict = None, write_mode: str = None)` | Update a memory item's properties (CORE-CRUD-UPDATE-1). | | `memory_delete` | `(item_id: str)` | Delete a memory item by ID. | | `memory_clear` | `(confirm: bool = False)` | Clear all memories permanently — requires `confirm=True`. | | `memory_list` | `(limit: int = 20, offset: int = 0)` | List memories with pagination. | | `memory_stats` | `()` | Memory count statistics grouped by type. | | `memory_distill` | `(user_turn: str, assistant_turn: str, session_id: str = None)` | Ingest a conversation turn pair into pending memory. | | `memory_ingest_conversation` | `(turns: list, session_boundaries: list = None, conversation_id: str = None, session_dates: list = None, turns_per_chunk: int = 15, max_chunk_chars: int = 12000)` | Ingest a conversation as session chunks (RLM-1g). | | `memory_search_advanced` | `(query: str, algorithm: str = "query_traversal", max_results: int = 15)` | Similarity Graph Traversal (SSG) search. | | `memory_search_by_metadata` | `(metadata_key: str, metadata_value: str, top_k: int = 10)` | Exact metadata key/value match. | ## Portability (FREE) [#portability-free] | Tool | Signature | Description | | ---------------- | --------------- | -------------------------------------------------------------- | | `memory_export` | `(path: str)` | Export all memories to a JSONL file. | | `memory_import` | `(path: str)` | Import memories from a JSONL file, re-ingesting each. | | `memory_migrate` | `(target: str)` | Migrate all memories to a different backend (local or remote). | ## Lifecycle (FREE) [#lifecycle-free] | Tool | Signature | Description | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | `memory_auto` | `(session_id: str = "", enabled: bool = True, recall_strategy: str = "topic_change", orient_budget: int = 1500, recall_budget: int = 500, observe_tool_calls: bool = True, distill_turns: bool = True, learn_from_errors: bool = True)` | Enable or disable automatic memory lifecycle for a session. | ## Decisions (FREE) [#decisions-free] The decision tools need no API key. A keyless local server lists and runs all of them, the same as the Python SDK's `add_decision`. | Tool | Signature | Description | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `decision_create` | `(content: str, decision_type: str = "inference", confidence: float = 0.8, evidence_ids: list[str] = None, domain: str = None, tags: list[str] = None, source_trace_id: str = None, rejected_alternatives: list[str] = None, rationale: str = None, constraints: list[str] = None)` | Create a new decision with provenance tracking. | | `decision_get` | `(decision_id: str)` | Retrieve a decision by ID. | | `decision_list` | `(domain: str = None, decision_type: str = None, min_confidence: float = 0.0, limit: int = 50)` | List active decisions with optional filtering. | | `decision_search` | `(topic: str, limit: int = 20)` | Search active decisions related to a topic. | | `decision_supersede` | `(decision_id: str, new_content: str, reason: str, new_decision_type: str = "inference", new_confidence: float = 0.8)` | Replace a decision; mark old as superseded. | | `decision_retract` | `(decision_id: str, reason: str)` | Retract a decision as no longer valid. | | `decision_reinforce` | `(decision_id: str, evidence_id: str)` | Record supporting evidence. | | `decision_contradict` | `(decision_id: str, evidence_id: str)` | Record contradicting evidence. | | `decision_provenance` | `(decision_id: str)` | Full provenance chain. | | `decision_find_conflicts` | `(decision_id: str)` | Find decisions that may conflict. | | `decision_create_pending` | `(content: str, requirements: list[dict], domain: str = None, tags: list[str] = None)` | Create a pending decision with unresolved requirements. The server assigns each `requirement_id`. | | `decision_resolve_requirement` | `(decision_id: str, requirement_id: str, memory_id: str)` | Mark a requirement on a pending decision resolved by a memory item. | | `decision_try_activate` | `(decision_id: str)` | Activate a pending decision once its requirements are resolved. | ## Code indexing (PRO) [#code-indexing-pro] | Tool | Signature | Description | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `code_index` | `(directory: str, repo_name: str = None, exclude_dirs: str = None)` | Index a Python codebase into the knowledge graph. Without `repo_name`, the repo is named after its Git remote URL. | | `code_search` | `(query: str, entity_type: str = None, repo: str = None, limit: int = 20)` | Search indexed code entities. | | `code_dead_code` | `(repo: str, exclude_decorators: str = None, limit: int = 50, include_exported: bool = False, production_only: bool = False)` | Find potentially unused functions, components and hooks. A function that is called, tested or used by value (passed as a callback, stored, exported or used as a default) is not reported. Methods of a class that is itself used, exported or an entry point are not reported. A use that exists only in a TypeScript type annotation does not count. Uses from another workspace or repository never count. Candidates with unknown registration reachability are excluded. Two options are off by default. `include_exported=True` also reports exported functions that nothing calls (entry points, unknown reachability and members of a used class are still excluded). `production_only=True` counts only uses from non-test files, so a function used only by tests is reported. A test file is a file that defines at least one test. The result echoes both options under `options`. The two options are independent. `include_exported` reports exported functions with no counted caller, and test callers count unless `production_only` is on, so an exported function used only by tests is reported only when both options are on. | | `code_dependencies` | `(entity_name: str, direction: str = "both", repo: str = None)` | Trace what calls / imports / inherits what. A name that matches several entities returns the list of candidates. | Framework registration analysis distinguishes proven entry points, unknown reachability and unsupported registrations. Unknown candidates are excluded from dead-code results across local, remote and hosted tools. Their `framework_evidence` records "reachability unknown" as `reachability="unknown"`, with the reason in `basis`. Dependency results expose that evidence. A receiver or callback name duplicated or written anywhere in its file cannot establish registration provenance. Type-only imports (`import type`) never establish it either. If a file contains any registration that cannot be proven or ruled out, every function in that file, and every function those functions call, is treated as unknown rather than dead. ## Anchors — anti-drift (PRO) [#anchors--anti-drift-pro] | Tool | Signature | Description | | --------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------- | | `memory_anchor_set` | `(content: str, anchor_type: str = "spec", session_id: str = "")` | Pin a requirement or constraint for a session. | | `memory_anchor_list` | `(session_id: str = "", anchor_type: str = None)` | List active anchors for a session. | | `memory_anchor_clear` | `(session_id: str = "", anchor_type: str = None)` | Deactivate anchors for a session. | | `memory_anchor_graduate` | `(session_id: str = "")` | Promote active anchors to persistent decisions. | | `memory_anchor_check_drift` | `(session_id: str = "", recent_outputs: list[str] = None)` | Check if recent outputs drifted from anchored requirements. | ## Plans & failure recall (PRO) [#plans--failure-recall-pro] | Tool | Signature | Description | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | `memory_plan_create` | `(title: str, tasks: list[dict] = None, context: str = "", created_by: str = "")` | Create a plan with optional task nodes. | | `memory_plan_get` | `(plan_id: str)` | Get a plan with all its tasks. | | `memory_plan_active` | `()` | Get all active plans. | | `memory_plan_update_task` | `(plan_id: str, task_id: str, status: str)` | Update task status (`pending` / `in_progress` / `complete` / `blocked`). | | `memory_log_failure` | `(error_type: str, content: str, context: str, plan_id: str = None, task_id: str = None, attempted_fix: str = None, resolution: str = None)` | Log an agent failure for cross-session error recovery. | | `memory_check_failure` | `(error_type: str, context: str, top_k: int = 3)` | Check for matching past failures before retrying. | ## Agent profiles (PRO) [#agent-profiles-pro] | Tool | Signature | Description | | -------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | `agent_set_recall_profile` | `(agent_id: str, memory_type_weights: dict[str, float] = None)` | Set personality-aware re-ranking weights. | | `agent_get_recall_profile` | `(agent_id: str)` | Get an agent's recall profile. | | `agent_evaluation_get` | `(agent_id: str, dimension: str, domain: str)` | Get the current evaluation score for an agent on a `(dimension, domain)` slot (CORE-AGENT-2). | ## Structured ingestion (PRO) [#structured-ingestion-pro] | Tool | Signature | Description | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- | | `memory_ingest_structured` | `(data: dict, schema_name: str)` | Ingest structured data via a registered handler, bypassing NLP. | | `memory_ingest_document` | `(source: str, source_type: str = "auto", chunk_size: int = 2000, chunk_strategy: str = "paragraph", reference: bool = False)` | Ingest a document from a URL or file path. | ## Graph (PRO) [#graph-pro] | Tool | Signature | Description | | ------------------------ | ----------------------------------------------------- | ----------------------------------------------------------------- | | `memory_resolve_aliases` | `(dry_run: bool = False, disambiguate: bool = False)` | Merge alias entities into their canonical nodes across the graph. | ## Patterns (PRO) [#patterns-pro] | Tool | Signature | Description | | ------------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | | `pattern_query` | `(scope: str = None, severity: str = None, free_text: str = None, limit: int = 100)` | Query the graph-backed adherence pattern catalog (filters are conjunctive). | | `pattern_get` | `(pattern_id: str)` | Retrieve a single pattern by ID. | | `pattern_list` | `(limit: int = 200)` | List patterns in the catalog. | | `memory_get_violation_patterns` | `(rule_id: str = None, rule_type: str = "feedback", memory_dir: str = None)` | Return the `## Detection patterns` catalog from local rule files (distinct from the graph-backed catalog). | ## Reasoning (PRO+) [#reasoning-pro] | Tool | Signature | Description | | ---------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | | `reasoning_challenge` | `(assertion: str, memory_type: str = "semantic", use_llm: bool = True)` | Challenge an assertion against existing knowledge. | | `reasoning_resolve_conflict` | `(existing_item_id: str, new_fact: str, use_wikipedia: bool = True, use_llm: bool = True)` | Resolve conflict via Wikipedia, LLM, grounding, or recency. | | `reasoning_query` | `(query: str, top_k: int = 10)` | Route a query to the cheapest effective retrieval method. | | `reasoning_proof_tree` | `(decision_id: str, max_depth: int = 5)` | Build an auditable proof tree for a decision. | | `reasoning_fuzzy_confidence` | `(decision_id: str)` | Multi-dimensional confidence (evidence, recency, consensus, directness). | | `reasoning_extract_trace` | `(content: str, min_steps: int = 2, use_llm_detection: bool = True)` | Extract chain-of-thought reasoning traces from content. | | `reasoning_query_traces` | `(query: str, limit: int = 10)` | Query stored reasoning traces. | ## Evolution (PRO+) [#evolution-pro] | Tool | Signature | Description | | ----------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------- | | `evolution_trigger` | `()` | Run a full evolution cycle (clustering, dedup, consolidation). | | `evolution_dream` | `()` | **Deprecated** — routing now happens at-ingest (CORE-MEMORY-DYNAMICS-1 M1b). | | `evolution_status` | `()` | Status of memory evolution processes. | | `evolution_synthesize_opinions` | `()` | Detect patterns in episodic memories and form opinions. | | `evolution_synthesize_observations` | `()` | Build entity summaries from scattered facts. | | `evolution_reinforce_opinions` | `()` | Update confidence scores based on new evidence. | | `clustering_run` | `(distance_threshold: float = 0.1)` | Merge duplicate entities by vector similarity. | ## Insights (PRO+) [#insights-pro] | Tool | Signature | Description | | ---------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------- | | `insight_health` | `()` | Memory health summary — counts, score, detected issues. | | `insight_reflect` | `(top_k: int = 10)` | Identify key concepts, top entities, dominant topics. | | `insight_maintenance_status` | `(stale_threshold_days: int = 90, low_confidence_threshold: float = 0.5)` | Orphaned, stale, low-confidence counts. | | `insight_plugins` | `()` | List available extractors and enrichers. | | `insight_personalize` | `(traits: dict = None, preferences: dict = None)` | Apply personalization for retrieval. | | `insight_feedback` | `(feedback: dict = None, memory_type: str = "semantic")` | Update memory system based on user feedback. | | `insight_ground` | `(item_id: str, source_url: str)` | Ground a memory item to an external URL. | ## Dev workflow (PRO+) [#dev-workflow-pro] | Tool | Signature | Description | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | | `dev_record_decision` | `(title: str, context: str, decision: str, rationale: str, alternatives: str = None, feature_id: str = None, tags: list[str] = None)` | Record an architectural decision as a decision memory. | | `dev_query_decisions` | `(query: str, feature_id: str = None, since: str = None, limit: int = 5)` | Search past decisions by keyword, feature, or time range. | | `dev_save_session` | `(summary: str, key_findings: list[str] = None, files_modified: list[str] = None, next_steps: list[str] = None, tags: list[str] = None)` | Save a development session summary as episodic memory. | | `dev_load_context` | `(topic: str, sessions: int = 3, include_decisions: bool = True)` | Load recent session context and related decisions. | | `dev_record_pattern` | `(name: str, description: str, example: str = None, applies_to: str = None, tags: list[str] = None)` | Record a code pattern as procedural memory. | | `dev_log_friction` | `(description: str, category: str, severity: str = "medium", context: str = None)` | Log a friction event during development. | ## Zettelkasten (PRO+) [#zettelkasten-pro] | Tool | Signature | Description | | -------------------- | ------------------------------------------- | ---------------------------------------------- | | `zettel_backlinks` | `(note_id: str)` | Notes that link TO this note. | | `zettel_connections` | `(note_id: str)` | All connections (backlinks + forward links). | | `zettel_clusters` | `(min_size: int = 3)` | Detect knowledge clusters in the Zettelkasten. | | `zettel_discover` | `(note_id: str, min_surprise: float = 0.5)` | Find unexpected (serendipitous) connections. | ## Example MCP call [#example-mcp-call] All tools follow standard MCP `tools/call` framing: ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "memory_ingest", "arguments": { "content": "Alice met Bob at OpenAI in Paris.", "memory_type": "episodic" } } } ``` ## See also [#see-also] * [MCP Integration Guide](/smartmemory/guides/mcp-integration/) — installation, transports, client config, tier model. * [SmartMemory API](/smartmemory/reference/smart-memory/) — the underlying Python API the tools wrap. ## Search creation windows [#search-creation-windows] MCP `memory_search` and `memory_search_by_metadata` accept `since` and `until` as ISO-8601 strings or relative durations such as `7d`, `24h`, and `30m`. Both bounds resolve at one instant; all output modes echo the resolved absolute `window` and creation-time `coverage`, including empty and citation responses. Both local and remote backends forward the bounds. CLI: `sm search "atlas budget" --since 7d --until 24h`. The flags filter storage creation time with an inclusive lower and exclusive upper bound on both daemon and in-process paths. `sm search --help` describes the accepted forms. ## Choosing a multi-hop planner [#choosing-a-multi-hop-planner] `hop_strategy` accepts `consensus`, `relevance`, or `semantic` on `POST /memory/search`, Python SDK `search()`, and MCP `memory_search`. JS SDK uses `memory.search(query, { multiHop: true, hopStrategy: 'relevance' })`. The Python equivalent is `client.search(query, multi_hop=True, hop_strategy="relevance")`. * `consensus` follows entities several top results agree on and may stop before a one-off bridge when distractors lower its frequency score. * `relevance` follows entities from the best results, including one-off bridges. * `semantic` asks an LLM to plan follow-up searches and incurs LLM calls. Omitted/null leaves core resolution unchanged: semantic when semantic hops/config is enabled, otherwise **consensus**. The measured fixture spike kept consensus: bridge F1 improved by 22.92 points with relevance, but median hops increased from 1 to 3, exceeding the pre-registered one-extra-hop bound. This is a fixture result, not a production or LoCoMo quality claim. Invalid strategies return HTTP 400 naming all three values. A supplied strategy with `multi_hop=false` is labelled in response `inert_parameters`; Python SDK retains this in `last_inert_parameters`, and MCP echoes it. CLI: `sm search "which team owns checkout?" --multi-hop --max-hops 3 --hop-strategy relevance`. `--max-hops` accepts 1–10; `--hop-strategy` without `--multi-hop` is rejected. Daemon, local fallback and remote wrapper paths carry the flags. # Breaking changes in 2.0.0 (/smartmemory/upgrade-to-2-0/breaking-changes) {/* Source map. Decision ids (G-ids) are in smart-memory-docs/docs/features/FULL-ALIGN-1/design.md. Inventory: G94 (design.md:326) plus G129, G131, G140, G169 amendments (SM2-RELEASE-1/design.md, G94 row). Quick-return default: G10, G57, G90 (design.md:67, :317). Removed routes: G51 (design.md:158), G53 (:163), G56 (:173). Receipt: G52 (design.md:160), G128 (:414), G173 (:535). Field names: SM2-SAVE-1/save-receipt-contract.json (d40c58f3). MCP tools: G56 (design.md:173), G140 (:448). Mapping and signatures read from smart-memory-mcp origin/2.0 a9f2ea49f30c smartmemory_mcp/tools/save_tools.py:1-8, :208-300. Stage calls: G169 (design.md:525), G185 (:621), G186 (:623). Current method names read from smart-memory-client origin/2.0 18f4c63877ad smartmemory_client/client.py:3164-3210. Promotion option: G129 (design.md:418). Edit precondition: G131 (:424). Retry keys: G178 (:549). Items in G168 (design.md:523) are deliberately not listed in the notice. */} > **Not released yet.** SmartMemory 2.0.0 has not been released. This is the planned list. Check it against the release > notes when 2.0.0 ships. > **Planned for 2.1.** Four changes on this page are not in 2.0.0. They move to SmartMemory 2.1: the quick `202` save > return (#1), the removal of `/memory/ingest/full` and `/ingest/async/{run_id}` (part of #3), and the revision > check on content edits (#6). Each one is marked "planned for 2.1" below. None is dropped. ## The list [#the-list] | # | Change | Who is affected | | - | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | 1 | Planned for 2.1: saves return quickly with `202` for every caller | REST, SDK and MCP callers that assume a save is finished when the call returns | | 2 | The save receipt replaces the id string | Python and JS SDK callers | | 3 | Three routes are removed with no alias (public `/memory/ingest/structured` in 2.0.0, the other two planned for 2.1) | REST callers of `/memory/ingest/full`, `/ingest/async/{run_id}` and public `/memory/ingest/structured` | | 4 | Six MCP save tools are removed and replaced by three | Agents, hooks and prompts that name the old tools | | 5 | Public pipeline-stage calls return receipts | Python client, JS `PipelineAPI` and REST callers that chain stages | | 6 | Planned for 2.1: content edits need the revision you opened | Clients that edit item content | | 7 | The promotion "remove source items" option is gone | Callers that promote and delete the source | ## Saves return quickly [#saves-return-quickly] **Planned for 2.1.** This is not part of 2.0.0. Until 2.1, saves keep the answer they give today. Every caller gets the quick-return default at once. A save is accepted and the response comes back with a receipt. Extraction and linking finish in the background. Nothing about this is opt-in. What to do: if your code reads the extraction result straight after a save, ask for it with `wait`. `wait=stored` returns when the item is stored. `wait=extracted` returns when extraction has finished, with the extraction result attached to the receipt. ## The save receipt replaces the id string [#the-save-receipt-replaces-the-id-string] Every save-shaped call returns one versioned receipt instead of a bare id. The receipt carries the item id, the run id, the processing stage (`received`, `stored`, `extracted` or `failed`) and a status URL scoped to you. Both SDKs return a typed receipt object. The receipt exposes `.item_id`. Before: ```python item_id = client.add("Met Ada, she prefers async standups") memory = client.get(item_id) ``` After: ```python receipt = client.add("Met Ada, she prefers async standups") memory = client.get(receipt.item_id) ``` The string form of a receipt shows the id and the current status, for example `mem_123 (stored)`. It never implies the work is complete. A wait that runs out of time does not raise. It returns the still-pending receipt with `client_timed_out` set to true, so you keep the handle and can wait again. The receipt carries a `schema_version`. Readers accept any 1.x version and keep fields they do not know as opaque data, so a later 1.x server does not break your client. ## Lite: ingest results and re-imports [#lite-ingest-results-and-re-imports] In Lite, `ingest()` returns a receipt for a new save, and you read the id from `.item_id`. Importing the same `source_path` a second time is different. For now that call returns the 1.x dict instead, with an `item_id` and a `status` of `unchanged`, `replaced` or `repaired`. A later release will return a receipt here too. Until then, one line reads the id in both cases: ```python r = memory.ingest(text, context={"source_path": path}) item_id = r.item_id if hasattr(r, "item_id") else r["item_id"] ``` Known issue: re-importing an edited note replaces the stored text but keeps the old enrichment. Lite logs a warning that the stored item is left for the reextract command. Run `smartmemory-core reextract` to queue a refresh of the enrichment. ## Removed routes [#removed-routes] **Planned for 2.1:** the removal of `POST /memory/ingest/full` and `GET /ingest/async/{run_id}`. Both routes keep working in 2.0.0. Only the public `POST /memory/ingest/structured` route is removed at 2.0.0. Once removed, these routes have no alias. A caller of `/memory/ingest/full` will get a `404`. | Removed | Use instead | | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `POST /memory/ingest/full` | `POST /memory/ingest` with `wait=extracted`. The Python `ingest_full()` becomes `ingest(wait="extracted")`. | | `GET /ingest/async/{run_id}` | The scoped status route that the receipt's status URL points to. Both SDKs and MCP ship an await helper that polls it. | | `POST /memory/ingest/structured` (public route) | The single save request with `data` and a `memory_type`. | The live progress stream stays available for interfaces that want to show progress. Nothing needs it for correctness. Reading pipeline state is a different call from polling a save, and it does not change. ## MCP save tools [#mcp-save-tools] Six tools stop working on upgrade day. They have no aliases. Hooks, prompts and saved agent instructions that call them must change the same day. | Old tool | New tool | | ---------------------------- | -------------------------------------------------------------------------------- | | `memory_add` | `memory_save` (use `memory_type="pending"` to store the text as is) | | `memory_ingest` | `memory_save` with `content` | | `memory_ingest_structured` | `memory_save` with `data` and `memory_type` set to the registered schema name | | `memory_ingest_conversation` | `memory_save_conversation` with the transcript in `turns` | | `memory_distill` | `memory_save_conversation` with exactly one user turn and one assistant turn | | `memory_ingest_document` | `memory_save_document` with `source` set to a URL or, in local mode, a file path | You choose the tool by the input you hold. There is no extraction setting for agents to set. Free text through `memory_save` is extracted, except `memory_type="pending"`, which is stored as is. Each tool returns a one-line summary and structured content with `status` (`saved`, `held`, `queued` or `failed`), the saved id and the receipt. A `held` status means the memory was held for review and `reason` says why. The tools for decisions, plans, anchors and the failure log do not change. `memory_import` does not change. ## Pipeline stage calls return receipts [#pipeline-stage-calls-return-receipts] The public stage calls (`run_extraction_stage` and the other `run_*_stage` methods in the Python client, the matching `PipelineAPI` methods in the JS SDK, and the REST stage routes) used to return the stage result straight away. In 2.0.0 each call returns a job receipt. Before, each stage result fed the next: ```python extracted = client.run_extraction_stage(text) stored = client.run_storage_stage(extracted) ``` After, each stage returns a receipt. You receive it, wait for its terminal result within your scope, and only then pass that result to the next stage: 1. Call the extraction stage and keep the receipt it returns. 2. Await the receipt until it reaches a terminal state. The await helper polls the scoped status route. 3. Pass the completed result to the storage stage, and repeat for each later stage. Preview profiles are rejected on public saves and public stage calls, with an error that points to the Studio lab. A note that is held returns a receipt that says it is held, not stored, and when the hold expires. It never carries an id for something that was not stored. Studio's lab stepping stays synchronous and does not change. ## Content edits need the revision you opened [#content-edits-need-the-revision-you-opened] **Planned for 2.1.** This is not part of 2.0.0. Until 2.1, content edits do not need a revision. Every edit that changes content, including `properties.content`, must say which revision it started from. If the item has changed since, the edit is rejected with a conflict response scoped to you, and no run is created. Keep the draft, show the current text, and let the person decide whether to retry. Retrying an edit that was already accepted returns the original receipt. Existing items get a stable current revision at the cutover, so their first edit after the upgrade has a real revision to point at. ## Promotion no longer deletes sources [#promotion-no-longer-deletes-sources] The option to remove source items after promoting a procedure is removed. The evidence behind the promotion is kept. If you passed that option, drop it. ## Related [#related] * [Upgrade to SmartMemory 2.0](/smartmemory/upgrade-to-2-0/) # Upgrade to SmartMemory 2.0 (/smartmemory/upgrade-to-2-0) {/* Source map (rows are in smart-memory-ops/status/2.0-to-public.md, decisions G-ids are in smart-memory-docs/docs/features/FULL-ALIGN-1/design.md, owner map in SM2-RELEASE-1/design.md). Version 2.0.0 and "carries every breaking change": G57, G93, G94 (status row 2a, 5a). Docs and upgrade note must exist before release: status section 3 row "Docs". Legacy items and ownerless facts: G45, G136, G49. Retry keys: G178. Self-hosted MongoDB: G177, SM2-RELEASE-1/replset-runbook.md section 1. Upgrade day for agents and hooks: G140. */} > **Not released yet.** SmartMemory 2.0.0 has not been released. This page describes the planned upgrade so you can > prepare. Behavior is final only when 2.0.0 ships. SmartMemory 2.0.0 is a normal upgrade, not an opt-in preview. Every caller moves at the same time. It is a major version because some calls change shape and some are removed. Every breaking change is listed on one page: [Breaking changes in 2.0.0](/smartmemory/upgrade-to-2-0/breaking-changes/). Four changes are planned for 2.1 and are not in 2.0.0: the quick `202` save return, the removal of `/memory/ingest/full` and `/ingest/async/{run_id}`, and the revision check on content edits. Later 2.x releases add features without breaking callers. ## Who needs to do something [#who-needs-to-do-something] | You use | What you do | | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | The Python or JS SDK | Read the save receipt instead of an id string. See [the receipt change](/smartmemory/upgrade-to-2-0/breaking-changes/#the-save-receipt-replaces-the-id-string). | | The REST API directly | Move off the removed routes. In 2.0.0 that is the public `/memory/ingest/structured` route. The quick `202` save return and the removal of `/memory/ingest/full` and `/ingest/async/{run_id}` are planned for 2.1. See [the removed routes](/smartmemory/upgrade-to-2-0/breaking-changes/#removed-routes). | | MCP with hooks, prompts or saved agent instructions | Rename six save tools on upgrade day. See [the MCP save tools](/smartmemory/upgrade-to-2-0/breaking-changes/#mcp-save-tools). | | The public pipeline-stage calls | Await each stage receipt before the next stage. See [the stage chain](/smartmemory/upgrade-to-2-0/breaking-changes/#pipeline-stage-calls-return-receipts). | | A self-hosted install | Move MongoDB to a single-node replica set. See [self-hosted MongoDB](#self-hosted-mongodb). | | Only the hosted web app | Nothing in your code to change. First-party clients migrate with the release. | ## Before you upgrade [#before-you-upgrade] 1. Search your code, hooks and prompts for the items in the [breaking changes](/smartmemory/upgrade-to-2-0/breaking-changes/) table. 2. Search for the six old MCP save tool names (`memory_add`, `memory_ingest`, `memory_ingest_structured`, `memory_ingest_conversation`, `memory_ingest_document`, `memory_distill`). They stop working at 2.0.0 with no aliases. 3. If you call the public `/memory/ingest/structured`, plan the replacement now, because 2.0.0 removes it with no alias. If you call `/memory/ingest/full` or `/ingest/async/{run_id}`, they keep working in 2.0.0. Removing them is planned for 2.1, so you can plan their replacements then. 4. Self-hosted: back up MongoDB and plan a short window with writers stopped. ## What happens to your existing data [#what-happens-to-your-existing-data] * **Existing items stay.** At the cutover every existing item gets a stable current revision and a status you can read. Items with no recorded processing history show the label "legacy processing status unavailable" instead of a guess. * **Unfinished background work is drained first.** Old save admission is fenced, work already in flight is brought to a recorded outcome, and only then does the new path switch on. Saves keep working. Background processing pauses for a short, bounded time. * **Old facts with no stored owner stay hidden.** They are not guessed into an owner. Items with no recorded origin stay visible and are treated as legacy. * **No automatic re-upload.** Obsidian adopts an existing item by id without saving it again. Capture never uploads an unmatched clip on its own. ## Retry keys last 30 days [#retry-keys-last-30-days] A retry with the same idempotency key always resolves to the original request for 30 days after the server first accepted the key. After that the server rejects the retry with a typed "retry key expired, cannot match" error. It never silently creates a second save. If you see that error, show the unsent draft to the person and ask whether to save it as new. The first-party clients (Capture, Obsidian, the Python and JS SDKs, local and hosted MCP, Maya) keep the draft and ask. ## Self-hosted MongoDB [#self-hosted-mongodb] 2.0.0 needs MongoDB running as a single-node replica set so that a save can be admitted in one transaction. 2.0.0 detects a standalone MongoDB at startup and either refuses it or upgrades it. The change to your database container is small: * mongod starts with `--replSet rs0 --bind_ip_all --oplogSize 2048`. * One member, set name `rs0`, on the same data volume. Your data files do not move. * The health check becomes `db.hello().isWritablePrimary`. * An app connection string with no replica-set options keeps working from inside the container network. * From outside that network, connect with `directConnection=true`. Stop everything that writes to MongoDB, take a logical backup with `mongodump`, then restart with the new flags. A full step-by-step runbook with rollback accompanies the 2.0.0 install docs. ## Next [#next] * [Breaking changes in 2.0.0](/smartmemory/upgrade-to-2-0/breaking-changes/) * The existing [Migration and Upgrade](/smartmemory/guides/migration-and-upgrade/) page covers earlier renames. # Architecture Deep Dive (/smartmemory/architecture/deep-dive) A guided tour through SmartMemory's architecture: how content flows through the ingestion pipeline, how the five memory types relate, how the Zettelkasten layer organises atomic knowledge, how clustering deduplicates entities, how MCP integration works, and how it all sits on the storage substrate. Aimed at readers evaluating whether SmartMemory fits their architecture, and at integrators who want a complete mental model before reaching for the API reference. For the short version, see [Introduction](/smartmemory/). For task-oriented usage see the [Guides](/smartmemory/guides/basic-usage/). For the API surface see [`SmartMemory`](/smartmemory/reference/smart-memory/). 🧠 **SmartMemory** - Advanced Agentic Memory System for Intelligent Applications with Enterprise-Grade Multi-User Support *** ## 🚀 What is SmartMemory? [#-what-is-smartmemory] SmartMemory is a **next-generation memory system** that goes far beyond simple storage and retrieval. It's designed specifically for **AI agents and intelligent applications** that need sophisticated memory capabilities with human-like cognitive processing and **complete user isolation**. ### 🌟 Key Features [#-key-features] * **🧠 [Cognitive Memory Types](/smartmemory/concepts/memory-types/)**: four knowledge types (semantic, episodic, procedural, zettel) plus pending, the staging type every item starts in * **🔄 [Automatic Evolution](/smartmemory/concepts/evolution-algorithms/)**: Memories improve and consolidate over time * **🎯 [Entity Extraction](/smartmemory/concepts/ingestion-flow/)**: LLM, SpaCy, and GLiNER extractors with automatic fallback * **🔗 [Entity Clustering](/smartmemory/concepts/hybrid-storage/)**: deduplication (SemHash + KMeans + LLM) * **🌐 Knowledge Grounding**: Automatic Wikipedia linking for entity provenance * **🔍 [Assertion Challenging](/smartmemory/concepts/assertion-challenging/)**: Detect contradictions between new and existing facts * **🔍 [Semantic Search](/smartmemory/concepts/hybrid-retrieval/)**: Vector embeddings + graph traversal + text fallbacks * **⏱️ [Temporal Versioning](/smartmemory/concepts/bi-temporal-versioning/)**: Bi-temporal tracking with time-travel queries * **🌐 Framework Integration**: Works with LangChain, CrewAI, AutoGen, and more *** ## 🔄 **Ingestion Pipeline** [#-ingestion-pipeline] When you call `memory.ingest()`, content flows through an **11-stage pipeline**: | Stage | Description | | ----------------------- | ----------------------------------------------------------------------- | | **classify** | Determine memory type (semantic, episodic, procedural, pending, zettel) | | **coreference** | Resolve pronouns and entity mentions in the content | | **simplify** | Optional sentence simplification for cleaner extraction | | **entity\_ruler** | Pattern-based entity tagging from public knowledge + workspace patterns | | **llm\_extract** | LLM entity & relation extraction (with SpaCy fallback) | | **ontology\_constrain** | Constrain extracted types against the active ontology | | **store** | Persist memory node + entity nodes (and embeddings) in FalkorDB | | **link** | Connect the new item to related existing memories | | **enrich** | Optional enrichers (sentiment, topic, temporal, link expansion, etc.) | | **ground** | Link entities to Wikidata/Wikipedia via `GROUNDED_IN` edges | | **evolve** | Run evolvers (episodic→semantic, decay, opinion synthesis, …) | *** ## 🧠 **Memory Type Distribution** [#-memory-type-distribution] SmartMemory organizes information into five distinct cognitive memory types, each optimized for different kinds of knowledge and usage patterns, mimicking human cognitive architecture. ### 🧬 **Memory Evolution & Intelligence** [#-memory-evolution--intelligence] Memory improves itself in the background while you continue chatting. Memories consolidate, prune, and enhance relationships as well as create new memories to self-improve. The system also goes out to the web to find new information and ground existing memories in facts. ## 🗂️ **Zettelkasten Memory System** [#️-zettelkasten-memory-system] SmartMemory includes a sophisticated Zettelkasten system for atomic knowledge management, automatically creating interconnected knowledge networks that evolve and strengthen over time. ### Zettelkasten Creation [#zettelkasten-creation] ### Knowledge Graph Evolution [#knowledge-graph-evolution] ### Intelligent Retrieval [#intelligent-retrieval] ## 🧬 **Entity Clustering & Deduplication** [#-entity-clustering--deduplication] SmartMemory provides multi-level entity resolution: | Layer | Method | Purpose | | ----------- | ----------------------------------------------------- | -------------------------------------------------- | | **SemHash** | Semantic hashing + singularization | Fast deterministic dedup (0.95 threshold) | | **KMeans** | Embedding clustering | Group similar entities (\~128 per cluster) | | **LLM** | semantic analysis (default `llama-3.3-70b-versatile`) | Find aliases (Joe ↔ Joseph, ML ↔ machine learning) | | **Merge** | Graph node consolidation | Rewire edges, merge properties | **Runs automatically** after each `ingest()` call, or manually via `memory.run_clustering()`. *** ## 🔍 **Advanced Search & Retrieval** [#-advanced-search--retrieval] Multi-dimensional search capabilities that go beyond simple keyword matching, using semantic understanding, graph traversal, and temporal patterns to find the most relevant memories. ## 🔌 **MCP Server Integration** [#-mcp-server-integration] Seamless integration with AI agents through the Model Context Protocol, providing standardized tools and interfaces for memory operations within agentic workflows. ## 💾 **Storage Architecture** [#-storage-architecture] SmartMemory uses a **unified FalkorDB backend** for both graph and vector storage: **Key Features:** * **Dual-node pattern**: Memory nodes + separate entity nodes for reuse * **Global Wikipedia nodes**: Shared across users (`is_global=True`) * **HNSW vector index**: Native FalkorDB vector search with configurable M, efConstruction * **Multi-tenant isolation**: `ScopeProvider` filters all queries by tenant/user/workspace ## 🎯 **Agentic Framework Integration** [#-agentic-framework-integration] Seamless integration with major AI agent frameworks through specialized adapters, enabling SmartMemory to work natively within your existing agentic workflows and toolchains. *** ### 🎯 **Key Differentiators** [#-key-differentiators] | Feature | Traditional Databases | SmartMemory | | ------------------------- | ----------------------- | -------------------------------------------------------------------------------- | | **Storage Architecture** | Static records | Hybrid triple storage (Graph + Vector + Metadata) | | **Relationships** | Manual foreign keys | Automatic intelligent linking + evolution | | **Processing** | Query-time only | Continuous background enrichment + evolution | | **Intelligence** | None | Entity extraction, semantic analysis, ontology management | | **Memory Types** | Single table/collection | 5 cognitive memory types (Semantic, Episodic, Procedural, Pending, Zettelkasten) | | **Agent Integration** | Basic CRUD | MCP protocol, tool interfaces, agentic workflows | | **Evolution** | Static data | Memory consolidation, pruning, quality enhancement | | **Ontology Management** | None | Hybrid freeform + structured knowledge extraction | | **Human-in-the-Loop** | Manual queries only | HITL ontology editing, validation, and refinement | | **Similarity Framework** | Basic text matching | Multi-dimensional similarity (semantic, temporal, structural) | | **Background Processing** | None | Asynchronous enrichment, evolution, and optimization | | **Context Awareness** | None | Temporal patterns, user context, cross-session intelligence | ## 🌟 Key Features & Architecture Blocks [#-key-features--architecture-blocks] ### 🧠 **Cognitive Memory Types** [#-cognitive-memory-types] | Memory Type | Description | Key Features | | -------------------- | ---------------------------------------- | ---------------------------------------------------------------------- | | **🔍 Semantic** | Facts, concepts, and general knowledge | Automatic concept extraction and linking, ontology-guided organization | | **📚 Episodic** | Personal experiences and events | Rich temporal context, narrative understanding | | **⚙️ Procedural** | Skills, procedures, and how-to knowledge | Step-by-step process understanding, action sequences | | **💭 Pending** | Temporary, active information processing | Context-aware short-term storage, dynamic attention management | | **🗂️ Zettelkasten** | Atomic knowledge notes | Interconnected knowledge networks, automatic linking | **Core Processing Capabilities:** * **🤖 Automatic Entity Extraction**: Identify people, places, concepts, and relationships * **🕸️ Intelligent Linking**: Discover connections between memories automatically * **📈 Semantic Enrichment**: Enhance memories with contextual information * **🎯 Similarity Analysis**: Multi-dimensional similarity scoring and clustering * **🔄 Evolution Algorithms**: Continuous memory consolidation and optimization ### ⚡ **High-Performance Architecture** [#-high-performance-architecture] **🚀 Key Performance Benefits:** * **Asynchronous Processing**: Non-blocking memory enhancement * **Fast Ingestion**: Store immediately, process in background * **Multi-Backend Support**: Graph databases, vector stores, hybrid approaches * **Intelligent Caching**: Smart retrieval optimization * **Scalable Components**: Modular, extensible architecture ### 🤖 **Agent-First Design** [#-agent-first-design] * **🔌 MCP Integration**: Model Context Protocol support for seamless LLM integration * **🛠️ Tool Interface**: Ready-to-use tools for agentic workflows * **🧬 Evolution Algorithms**: Memory optimization designed specifically for AI agents * **🎯 Contextual Retrieval**: Intelligent memory access patterns ## Quick Start [#quick-start] ```python from smartmemory import SmartMemory # Initialize SmartMemory memory = SmartMemory() # Ingest memories (full 11-stage pipeline) # classify → coreference → simplify → entity_ruler → llm_extract → # ontology_constrain → store → link → enrich → ground → evolve memory.ingest("I learned Python programming in 2020") memory.ingest("Paris is the capital of France") memory.ingest("John works at Google as an engineer") # Search memories (vector + graph + text fallbacks) results = memory.search("programming languages") print(results) # Get related memories via graph traversal related = memory.get_neighbors(results[0].item_id) print(related) # Run clustering to deduplicate entities stats = memory.run_clustering() print(f"Merged {stats['merged_count']} duplicate entities") ``` | Feature | SmartMemory | Zep | Mem0 | | ----------------------------- | ----------------------------------------------------- | ------------- | ---------------- | | **Core Memory Types** | | | | | Semantic Memory | ✅ Advanced with entity extraction | ✅ Basic | ✅ Basic | | Episodic Memory | ✅ Temporal + contextual | ❌ Limited | ✅ Basic | | Procedural Memory | ✅ Skills + workflows | ❌ No | ❌ No | | Pending Memory | ✅ Adaptive capacity | ❌ No | ❌ No | | Zettelkasten Memory | ✅ Atomic knowledge notes | ❌ No | ❌ No | | **Intelligence & Processing** | | | | | Entity Extraction | ✅ LLM + SpaCy + GLiNER (fallback chain) | ❌ Basic | ✅ Basic | | Relationship Discovery | ✅ Automatic + intelligent | ❌ Manual | ✅ Limited | | Entity Clustering | ✅ SemHash + KMeans + LLM | ❌ No | ❌ No | | Grounding & Provenance | ✅ Wikipedia linking + GROUNDED\_IN edges | ❌ No | ❌ Limited | | Background Processing | ✅ Async + configurable | ❌ No | ✅ Basic | | Evolution Algorithms | ✅ 14+ sophisticated evolvers | ❌ No | ❌ Basic | | **Storage & Architecture** | | | | | Storage Architecture | ✅ Unified FalkorDB (Graph + Vector) | ❌ Vector only | ✅ Vector + basic | | Graph Database Support | ✅ FalkorDB (primary), SQLite (lite) | ❌ No | ❌ Limited | | Vector Database Support | ✅ FalkorDB HNSW (native, default), usearch (optional) | ✅ Multiple | ✅ Multiple | | Multi-Backend Support | ✅ Pluggable backends | ❌ Limited | ✅ Yes | | Hybrid Search | ✅ Semantic + Graph + Metadata | ❌ Vector only | ✅ Semantic | | **AI Agent Integration** | | | | | MCP Protocol | ✅ Full MCP tool suite | ❌ No | ❌ Limited | | LangChain Integration | ✅ Native support | ✅ Yes | ✅ Yes | | AutoGen Compatible | ✅ Multi-agent support | ❌ Limited | ✅ Basic | | Custom Tool Creation | ✅ Extensible toolbox | ❌ Limited | ✅ Basic | | Agent Memory Isolation | ✅ Multi-tenant + namespaces | ❌ Basic | ✅ User-based | | **Search & Retrieval** | | | | | Semantic Search | ✅ Advanced embeddings | ✅ Yes | ✅ Yes | | Graph Traversal | ✅ Relationship-based discovery | ❌ No | ❌ Limited | | Multi-Modal Queries | ✅ Content+Meta+Temporal | ❌ Basic | ✅ Content | | Context-Aware Search | ✅ User+Time+Type filtering | ❌ Limited | ✅ Basic | | Real-Time Performance | ✅ Sub-millisecond | ✅ Fast | ✅ Fast | | **Developer Experience** | | | | | API Completeness | ✅ Full CRUD + advanced ops | ✅ Basic CRUD | ✅ CRUD + search | | Configuration System | ✅ JSON/YAML + env vars | ❌ Basic | ✅ Config files | | Documentation Quality | ✅ Comprehensive + examples | ❌ Limited | ✅ Good | | Testing Framework | ✅ Full test suite + benchmarks | ❌ Basic | ✅ Tests | | Monitoring & Analytics | ✅ Metrics + health checks | ❌ No | ❌ Basic | | **Scalability & Performance** | | | | | Horizontal Scaling | ✅ Multi-worker + load balancing | ❌ Limited | ✅ Cloud-native | | Caching Strategy | ✅ Multi-level intelligent caching | ❌ Basic | ✅ Basic | | Batch Processing | ✅ Efficient bulk operations | ❌ No | ✅ Limited | | Background Evolution | ✅ Continuous optimization | ❌ No | ❌ No | | Resource Management | ✅ Connection pooling + optimization | ❌ Basic | ✅ Cloud-managed | | **Enterprise Features** | | | | | Multi-Tenancy | ✅ Secure namespace isolation | ❌ No | ✅ User isolation | | Audit Trails | ✅ Full provenance tracking | ❌ No | ❌ Limited | | Temporal Versioning | ✅ Bi-temporal with time-travel queries | ❌ No | ❌ No | | Security Model | ✅ Comprehensive access control | ❌ Basic | ✅ User-based | | Backup & Recovery | ✅ Point-in-time recovery | ❌ Manual | ✅ Cloud backup | | Compliance Support | ✅ GDPR + audit ready | ❌ No | ✅ Basic | ### **Key Differentiators** [#key-differentiators] 🧠 **SmartMemory**: Most comprehensive system with 5 memory types, advanced evolution algorithms, hybrid storage, and enterprise-grade features 🔧 **Zep**: Lightweight vector-based system focused on simplicity and basic retrieval ☁️ **Mem0**: Cloud-native solution with good LLM integration but limited memory sophistication ### **Best Use Cases** [#best-use-cases] * **SmartMemory**: Complex AI agents, enterprise applications, research systems, sophisticated conversational AI * **Zep**: Simple chatbots, basic RAG applications, proof-of-concepts * **Mem0**: Cloud-first applications, basic personalization, simple memory needs ## Use Cases [#use-cases] * **Conversational AI**: Persistent memory for chatbots and assistants * **Learning Systems**: Educational applications with knowledge tracking * **Knowledge Management**: Enterprise knowledge bases with intelligent organization * **Research Tools**: Academic and scientific knowledge organization * **Personal AI**: Individual memory systems for personal assistants ## Next Steps [#next-steps] 1. **[Installation](/smartmemory/getting-started/installation/)** - Set up SmartMemory in your environment 2. **[Quick Start](/smartmemory/getting-started/quickstart/)** - Build your first memory-enabled application 3. **[Core Concepts](/smartmemory/concepts/overview/)** - Understand the fundamental concepts 4. **[API Reference](/smartmemory/reference/smart-memory/)** - Detailed API documentation *** *SmartMemory is designed for developers building intelligent applications that need sophisticated memory capabilities. Whether you're creating conversational AI, learning systems, or knowledge management tools, SmartMemory provides the foundation for intelligent memory processing.* # System Architecture Overview (/smartmemory/architecture/system-overview) SmartMemory is built on a modular, component-based architecture that provides a unified interface for agentic memory operations while maintaining flexibility and extensibility. This document provides a comprehensive overview of the system architecture, design principles, and component interactions. ## Architecture Principles [#architecture-principles] ### Core Design Principles [#core-design-principles] 1. **Modularity**: Each component has a single responsibility and clear interfaces 2. **Extensibility**: New memory types, backends, and algorithms can be easily added 3. **Performance**: Optimized for both throughput and latency with background processing 4. **Reliability**: Robust error handling and graceful degradation 5. **Scalability**: Designed to handle large-scale memory operations 6. **Interoperability**: Standard interfaces for integration with external systems ### Architectural Patterns [#architectural-patterns] * **Component-Based Architecture**: Loosely coupled components with dependency injection * **Factory Pattern**: Consistent creation and initialization of memory components * **Strategy Pattern**: Pluggable algorithms for similarity, evolution, and extraction * **Observer Pattern**: Event-driven processing and monitoring * **Adapter Pattern**: Integration with various backends and external services ## High-Level Architecture [#high-level-architecture] ## Component Architecture [#component-architecture] ### SmartMemory Core Class [#smartmemory-core-class] The `SmartMemory` class serves as the unified entry point and orchestrator: ```python class SmartMemory(MemoryBase): """ Unified agentic memory store combining semantic, episodic, procedural, and pending memory. """ def __init__(self, **kwargs): # Core storage self._graph = SmartGraph() # Component delegation self._crud = CRUD(self._graph) self._linking = Linking(self._graph) self._enrichment = Enrichment(self._graph) self._grounding = Grounding(self._graph) self._personalization = Personalization(self._graph) self._search = Search(self._graph) self._monitoring = Monitoring(self._graph) self._evolution = EvolutionOrchestrator(self) self._clustering = GlobalClustering(self._graph) # Temporal versioning self.version_tracker = VersionTracker(self._graph) # Processing flow (constructed lazily on first ingest) self._ingestion_flow = MemoryIngestionFlow( self, linking=self._linking, enrichment=self._enrichment ) ``` **Key Responsibilities:** * Unified API for all memory operations * Component lifecycle management * Request routing and delegation * Background processing coordination * Configuration management ### Memory Types [#memory-types] #### Semantic Memory [#semantic-memory] * **Purpose**: Long-term factual knowledge and concepts * **Storage**: Graph nodes with rich semantic relationships * **Processing**: Entity extraction, concept linking, ontology integration * **Use Cases**: Knowledge bases, fact storage, concept hierarchies #### Episodic Memory [#episodic-memory] * **Purpose**: Temporal experiences and events * **Storage**: Time-ordered sequences with contextual metadata * **Processing**: Temporal clustering, event correlation, narrative construction * **Use Cases**: Conversation history, user interactions, experience tracking #### Procedural Memory [#procedural-memory] * **Purpose**: Skills, procedures, and how-to knowledge * **Storage**: Step-based workflows and process graphs * **Processing**: Skill extraction, procedure optimization, workflow analysis * **Use Cases**: Task automation, skill learning, process documentation #### Pending Memory [#pending-memory] * **Purpose**: Active, short-term contextual information * **Storage**: Temporary nodes with expiration policies * **Processing**: Context management, relevance scoring, automatic cleanup * **Use Cases**: Current conversation context, active task state ### Processing Pipeline [#processing-pipeline] #### Ingestion Flow (11 Stages) [#ingestion-flow-11-stages] ``` Input → Classification → Extraction → Storage → Linking → Vector → Enrichment → Grounding → Evolution → Clustering → Versioning ``` **Stages:** 1. **Input Adaptation**: Convert str/dict/MemoryItem to standard format 2. **Classification**: Determine memory type (semantic, episodic, procedural, zettel, pending) 3. **Extraction**: Extract entities `&` relations (LLM → SpaCy → GLiNER fallback chain) 4. **Storage**: Create memory node + entity nodes in FalkorDB 5. **Linking**: Connect to related existing memories 6. **Vector Storage**: Generate embeddings, store in HNSW index 7. **Enrichment**: Add Wikipedia summaries, categories, metadata 8. **Grounding**: Create GROUNDED\_IN edges to Wikipedia nodes 9. **Evolution**: Promote pending → episodic/procedural if thresholds met 10. **Clustering**: SemHash + embedding deduplication of entities 11. **Versioning**: Create bi-temporal version record #### Fast Ingestion Flow [#fast-ingestion-flow] ``` Input → Quick Storage → Background Processing Queue ``` **Benefits:** * Immediate storage for fast response times * Background enrichment for full processing * Scalable for high-throughput scenarios ### Graph Operations [#graph-operations] #### CRUD Component [#crud-component] * **Create**: Add new memory items with validation * **Read**: Retrieve items by ID, type, or criteria * **Update**: Modify existing items with change tracking * **Delete**: Remove items with cascade handling #### Linking Component [#linking-component] * **Automatic Linking**: AI-driven relationship discovery * **Explicit Linking**: User-defined relationships * **Link Types**: Semantic, temporal, causal, hierarchical * **Link Strength**: Weighted relationships with confidence scores #### Search Component [#search-component] * **Vector Search**: Semantic similarity using embeddings * **Graph Traversal**: Relationship-based discovery * **Hybrid Search**: Combined vector and graph approaches * **Faceted Search**: Multi-dimensional filtering ### External Integration [#external-integration] #### MCP Tools [#mcp-tools] * **Standardized Interface**: Model Context Protocol compliance * **Tool Discovery**: Auto-registration and service discovery * **Agent Integration**: LangChain, AutoGen, custom frameworks * **Operation Coverage**: Full CRUD and search capabilities #### LLM Providers [#llm-providers] * **OpenAI**: GPT models for extraction and enrichment * **Anthropic**: Claude for analysis and reasoning * **Azure OpenAI**: Enterprise-grade AI services * **Local Models**: Local or self-hosted LLM endpoints for extraction #### Vector Stores [#vector-stores] * **FalkorDB HNSW (default)**: Embeddings stored natively inside FalkorDB via an HNSW vector index — no separate vector service * **usearch (optional)**: Embedded, file-backed similarity search * **Extensible**: Backend registry for adding new vector providers ## Data Flow Architecture [#data-flow-architecture] ### Write Path [#write-path] ``` User Input → SmartMemory.add() → Ingestion Flow → Graph Storage + Vector Storage → Background Enrichment ``` ### Read Path [#read-path] ``` Query → SmartMemory.search() → Search Component → Vector Search + Graph Traversal → Result Ranking → Response ``` ### Background Processing [#background-processing] ``` Queued Items → Background Processor → Enrichment Pipeline → Evolution Algorithms → Updated Storage ``` ## Storage Architecture [#storage-architecture] ### Graph Database Layer (FalkorDB) [#graph-database-layer-falkordb] **Node Types:** * **Memory Nodes**: Core memory items with content and metadata * **Entity Nodes**: Extracted entities with properties (dual-node pattern) * **Wikipedia Nodes**: Global grounding nodes (shared across users) * **Version Nodes**: Bi-temporal version records **Relationship Types:** * **CONTAINS**: Memory contains entity * **GROUNDED\_IN**: Entity grounded to Wikipedia * **HAS\_VERSION**: Memory has version history * **RELATES\_TO**: Semantic relationships * **FOLLOWS**: Temporal sequences * **PART\_OF**: Hierarchical structures * **SIMILAR\_TO**: Similarity relationships * **PRODUCED**: ReasoningTrace generated a Decision * **DERIVED\_FROM**: Decision based on evidence * **SUPERSEDES**: New decision replaces old * **CONTRADICTS**: Decisions conflict * **INFLUENCES**: Decision affects other memories **Properties:** * **Timestamps**: Creation, modification, access times * **Scores**: Relevance, confidence, importance * **Metadata**: User context, source information * **Embeddings**: Vector representations for similarity ### Vector Storage Layer (FalkorDB HNSW) [#vector-storage-layer-falkordb-hnsw] **Unified Backend:** * FalkorDB provides both graph and vector storage * Native HNSW index with `vecf32` type * Configurable parameters: M, efConstruction, efRuntime * Cosine similarity search **Embedding Types:** * **Content Embeddings**: Full text semantic vectors (sentence-transformers) * **Entity Embeddings**: Specific entity representations **Indexing Strategy:** * **HNSW Index**: Hierarchical Navigable Small World graphs * **Tenant Isolation**: ScopeProvider filters all queries * **Incremental Updates**: Real-time index maintenance ### Reliability & Durability (FalkorDB) [#reliability--durability-falkordb] **Persistence:** Production deployments enable Redis Append-Only File (AOF) on FalkorDB for \~1-second worst-case data loss, alongside the default RDB snapshot mechanism for fast offline-copy backups. * **Configuration mechanism:** AOF is enabled via the `REDIS_ARGS` environment variable on the FalkorDB container (`REDIS_ARGS="--appendonly yes --appendfsync everysec"`). The FalkorDB image entrypoint constructs the `redis-server` invocation from this env var; compose `command:` flags are not honored. * **AOF layout:** Redis 7+ multi-part format under `/var/lib/falkordb/data/appendonlydir/` (manifest + base.rdb + incr.aof). Replays cleanly on container restart, including FalkorDB-module GRAPH commands. * **Volume mount:** `falkordb_data:/var/lib/falkordb/data` (env `FALKORDB_DATA_PATH`). The legacy `/data` mount used by older image versions persists nothing in current FalkorDB releases. **Crash recovery:** FalkorDB's Redis-derived signal handler can dump crash state and leave the container in a "Up but non-responsive" state where every healthcheck fails but no automatic restart fires. Production deployments add a `willfarrell/autoheal` sidecar that watches for the `autoheal: "true"` container label and force-restarts unhealthy containers via the Docker socket. Verified end-to-end recovery time: \~20 seconds from `docker pause` to fully healthy with all data preserved (vs \~64 minutes for an unattended hung-container incident pre-feature). **Healthcheck cadence:** `interval: 15s, timeout: 10s, retries: 3` on the FalkorDB container. The 10-second timeout absorbs entity-resolution write bursts and AOF-rewrite fork pressure without producing false unhealthy flips. For the full design and execution record, see `docs/features/PLAT-FALKOR-1/` in the smart-memory-docs repository. ## Performance Architecture [#performance-architecture] ### Optimization Strategies [#optimization-strategies] #### Caching Layer [#caching-layer] ```python # Multi-level caching strategy L1_CACHE = LRUCache(maxsize=1000) # Hot data L2_CACHE = RedisCache(ttl=3600) # Warm data L3_CACHE = DiskCache(size="1GB") # Cold data ``` #### Background Processing [#background-processing-1] ```python # Asynchronous processing pipeline class BackgroundProcessor: def __init__(self, max_workers=3): self.enrichment_queue = Queue() self.evolution_queue = Queue() self.cleanup_queue = Queue() ``` #### Connection Pooling [#connection-pooling] ```python # Database connection management class ConnectionPool: def __init__(self, backend_type, pool_size=10): self.pool = create_pool(backend_type, pool_size) self.health_checker = HealthChecker() ``` ### Scalability Patterns [#scalability-patterns] #### Horizontal Scaling [#horizontal-scaling] * **Sharded Storage**: Distribute data across multiple backends * **Load Balancing**: Route requests across multiple instances * **Microservices**: Decompose into specialized services #### Vertical Scaling [#vertical-scaling] * **Resource Optimization**: Memory and CPU tuning * **Batch Processing**: Efficient bulk operations * **Index Optimization**: Smart indexing strategies ## Security Architecture [#security-architecture] ### Access Control [#access-control] * **User Isolation**: Strict user data separation * **Permission Model**: Role-based access control * **API Security**: Authentication and authorization * **Data Encryption**: At-rest and in-transit protection ### Privacy Protection [#privacy-protection] * **Data Anonymization**: PII removal and masking * **Retention Policies**: Automatic data expiration * **Audit Logging**: Comprehensive access tracking * **Compliance**: GDPR, CCPA, and other regulations ## Monitoring and Observability [#monitoring-and-observability] ### Metrics Collection [#metrics-collection] ```python # Key performance indicators METRICS = { "throughput": "operations_per_second", "latency": "response_time_percentiles", "accuracy": "search_relevance_scores", "resource_usage": "memory_cpu_disk_utilization" } ``` ### Health Monitoring [#health-monitoring] * **Component Health**: Individual component status * **Dependency Health**: External service monitoring * **Performance Alerts**: Threshold-based notifications * **Capacity Planning**: Resource usage trends ### Debugging Support [#debugging-support] * **Distributed Tracing**: Request flow tracking * **Structured Logging**: Searchable log events * **Error Tracking**: Exception monitoring and alerting * **Performance Profiling**: Bottleneck identification ## Configuration Architecture [#configuration-architecture] ### Configuration Hierarchy [#configuration-hierarchy] ``` Environment Variables → Config Files → Runtime Parameters → Defaults ``` ### Configuration Validation [#configuration-validation] ```python # Schema-based validation CONFIG_SCHEMA = { "graph_db": {"backend_class": "FalkorDBBackend", "host": "localhost", ...} "vector": {"backend": "falkordb", "dimension": 768, "metric": "cosine", ...} "background": {"max_workers": 16, ...} "llm_provider": {"type": "string", "enum": ["openai", "anthropic", "azure"]}, "background_processing": {"type": "boolean", "default": True} } ``` ### Environment-Specific Configurations [#environment-specific-configurations] * **Development**: Local backends, debug logging * **Testing**: In-memory storage, mock services * **Staging**: Production-like setup with test data * **Production**: Optimized for performance and reliability ## Where it runs [#where-it-runs] SmartMemory runs in three ways: * **Lite (free, on your own machine):** install with `pip install smartmemory`. Storage and search run locally with a SQLite graph and local vectors. No Docker, database service, or account is needed. See [Installation](/smartmemory/getting-started/installation/). * **SmartMemory Service (managed):** connect the SDKs, CLI, and MCP tools to the managed API with an API key from the [SmartMemory dashboard](https://www.smartmemory.ai/dashboard). SmartMemory operates the graph, vector, and metadata stores for you. * **Enterprise BYOC:** the full service runs in the customer's own AWS account. This is available to Enterprise customers through sales and is currently at pilot stage. Running the full server stack in your own infrastructure is available only through Enterprise BYOC. ### Regulated and on-prem teams [#regulated-and-on-prem-teams] If client data cannot leave your network, this is what you can run today: * **Fully local Lite.** Choose the `ollama` LLM provider (or `none`) and `local` embeddings during `smartmemory setup`. Storage, search and embeddings stay on your machine and no API key is needed. See [Installation](/smartmemory/getting-started/installation/). * **Update check.** Lite pings the SmartMemory API once on first use with an install id, versions and platform, and never sends memory content or query text. Set `DO_NOT_TRACK=1` or `SMARTMEMORY_NO_UPDATE_CHECK=1` to turn it off. See [Licensing](/smartmemory/reference/licensing/). * **Enterprise BYOC.** The full server stack runs in your own AWS account. It is available through sales and is at pilot stage. It does not run outside AWS. * **Managed service.** These docs do not yet state a hosting region or a data residency option for it. Do not assume it fits data that must stay in a given country. Self-hosting the full stack on any cloud or on-prem, with Docker or Kubernetes, is on the roadmap but not built. We are looking for one design partner in a regulated firm to build it with. If that is you, write to [help@smartmemory.ai](mailto:help@smartmemory.ai) with your environment and requirements. ## Extension Points [#extension-points] ### Custom Components [#custom-components] ```python # Plugin architecture class CustomExtractor(BaseExtractor): def extract(self, content): # Custom extraction logic pass # Registration register_component("extractor", "custom", CustomExtractor) ``` ### Custom Algorithms [#custom-algorithms] ```python # Pluggable algorithms class CustomSimilarity(BaseSimilarity): def compute_similarity(self, item1, item2): # Custom similarity computation pass # Integration similarity_registry.register("custom", CustomSimilarity) ``` ### Custom Backends [#custom-backends] ```python # Backend abstraction class CustomBackend(BaseBackend): def connect(self): # Custom connection logic pass def query(self, cypher): # Custom query execution pass # Registration backend_registry.register("custom", CustomBackend) ``` ## Future Architecture Considerations [#future-architecture-considerations] ### Planned Enhancements [#planned-enhancements] * **Distributed Architecture**: Multi-node deployment * **Event Sourcing**: Complete audit trail * **CQRS Pattern**: Separate read/write models * **GraphQL API**: Flexible query interface ### Research Directions [#research-directions] * **Neuromorphic Computing**: Brain-inspired architectures * **Quantum Computing**: Quantum similarity algorithms * **Edge Computing**: Distributed memory networks * **Federated Learning**: Collaborative memory systems ## Best Practices [#best-practices] ### Development Guidelines [#development-guidelines] 1. **Component Isolation**: Minimize dependencies between components 2. **Interface Contracts**: Clear API definitions and contracts 3. **Error Handling**: Comprehensive error recovery strategies 4. **Testing Strategy**: Unit, integration, and performance tests 5. **Documentation**: Comprehensive API and architecture docs ### Operational Guidelines [#operational-guidelines] 1. **Monitoring**: Comprehensive observability setup 2. **Backup Strategy**: Regular data backup and recovery testing 3. **Capacity Planning**: Proactive resource management 4. **Security Updates**: Regular dependency and security updates 5. **Performance Tuning**: Continuous optimization based on metrics This architecture provides a solid foundation for building scalable, reliable, and extensible agentic memory systems while maintaining flexibility for future enhancements and integrations. # Ask an as-of question (/smartmemory/examples/clinical-trials/as-of-question) Find out which consent form version a site should have used on a given day. Set `SMARTMEMORY_API_KEY`, `SMARTMEMORY_WORKSPACE_ID` and optionally `SMARTMEMORY_BASE_URL` (default `https://api.smartmemory.ai`). See the [overview](/smartmemory/examples/clinical-trials/) for setup. ## What it uses [#what-it-uses] * `client.search` and `client.memories.search` take the question as written. A day named in the question narrows retrieval to records that were valid on that day. * `client.ask` and `client.memories.ask` answer from the retrieved records and return the evidence they used. They raise an error when the deployment's language model cannot answer, there is no fallback answer. * Both SDKs also take `as_of_date` (`asOfDate`) on search. That one is transaction time: it returns what the system believed on that date, which is the audit question. The day in the question text is valid time: it returns what was true on that date. ## Python [#python] ```python """Ask an as-of question: which ICF version should site 03 have used on 2026-03-03?""" import os from smartmemory_client import SmartMemoryClient from smartmemory_client.errors import SmartMemoryServerError client = SmartMemoryClient( base_url=os.environ.get("SMARTMEMORY_BASE_URL", "https://api.smartmemory.ai"), api_key=os.environ["SMARTMEMORY_API_KEY"], workspace_id=os.environ["SMARTMEMORY_WORKSPACE_ID"], ) QUESTION = "Which ICF version should site 03 have used on 2026-03-03?" # A day named in the question narrows retrieval to records that were valid on that day. print("Records valid on 2026-03-03:") for hit in client.search(QUESTION, top_k=3): print(" ", hit.metadata.get("record_id"), hit.valid_start_time[:10], "|", hit.content[:70]) # ask() answers from those records and returns the evidence it used. It raises when the # deployment's language model cannot answer, it never invents a fallback. try: result = client.ask(QUESTION, limit=5) except SmartMemoryServerError as error: print("ask() unavailable:", error.status_code) else: print("Answer:", result["answer"]) for item in result["evidence"]: print(" evidence:", item["item_id"]) ``` Output: ```text Records valid on 2026-03-03: CT-DEV-03 2026-03-03 | Protocol deviation DEV-03: consent on superseded ICF 2.0, participant ask() unavailable: 502 ``` ## JavaScript [#javascript] ```js // Ask an as-of question: which ICF version should site 03 have used on 2026-03-03? import { SmartMemoryClient } from "@smartmemory/sdk-js"; const client = new SmartMemoryClient({ apiBaseUrl: process.env.SMARTMEMORY_BASE_URL ?? "https://api.smartmemory.ai", apiKey: process.env.SMARTMEMORY_API_KEY, storage: "memory", // a script has no browser storage }); client.setWorkspaceId(process.env.SMARTMEMORY_WORKSPACE_ID); const QUESTION = "Which ICF version should site 03 have used on 2026-03-03?"; // A day named in the question narrows retrieval to records that were valid on that day. console.log("Records valid on 2026-03-03:"); const { results } = await client.memories.search(QUESTION, { topK: 3 }); for (const hit of results) { console.log(" ", hit.metadata?.record_id, hit.valid_start_time.slice(0, 10), "|", hit.content.slice(0, 70)); } // ask() answers from those records and returns the evidence it used. It rejects when the // deployment's language model cannot answer, it never invents a fallback. try { const result = await client.memories.ask(QUESTION, { limit: 5 }); console.log("Answer:", result.answer); for (const item of result.evidence) console.log(" evidence:", item.item_id); } catch (error) { console.log("ask() unavailable:", error.status); } ``` Output: ```text Records valid on 2026-03-03: CT-DEV-03 2026-03-03 | Protocol deviation DEV-03: consent on superseded ICF 2.0, participant [session] Service unavailable: HTTP 502 ask() unavailable: 502 ``` ## Notes [#notes] The search step ran locally and found the record that answers the question: the deviation record for a participant consented on 2026-03-03 on a superseded form. The `ask` step was not run locally: the local language model provider is unavailable, so the service answered 502 and the script printed that. Not run locally, expected output shape below. The field names come from the SDK's documented return value, the values depend on the deployment. ```json { "answer": "", "reasoning": "", "evidence": [{"item_id": "", "content": ""}], "relations": [{"source": "...", "type": "...", "target": "...", "source_id": "...", "target_id": "..."}] } ``` The output below is from a real run against a local test stack seeded with the demo dataset. It shows record ids and dates only. # Provenance of a decision (/smartmemory/examples/clinical-trials/decision-provenance) Read who recommended the dose level 2 decision, who decided, what was rejected, and which records it rests on. Set `SMARTMEMORY_API_KEY`, `SMARTMEMORY_WORKSPACE_ID` and optionally `SMARTMEMORY_BASE_URL` (default `https://api.smartmemory.ai`). See the [overview](/smartmemory/examples/clinical-trials/) for setup. ## What it uses [#what-it-uses] * `client.search_decisions` and `client.decisions.search` find decisions by topic. * `client.get_provenance_chain` and `client.decisions.getProvenance` return the decision, its rationale, its rejected alternatives and the evidence records behind it. * The recommender and the decider are fields on the decision record in the evidence list (`recommended_by`, `decided_by`). ## Python [#python] ```python """Provenance of the dose level 2 decision: who recommended, who decided, what was rejected, on what evidence.""" import os from smartmemory_client import SmartMemoryClient client = SmartMemoryClient( base_url=os.environ.get("SMARTMEMORY_BASE_URL", "https://api.smartmemory.ai"), api_key=os.environ["SMARTMEMORY_API_KEY"], workspace_id=os.environ["SMARTMEMORY_WORKSPACE_ID"], ) found = client.search_decisions("dose level 2")["decisions"] decision_id = found[0]["decision_id"] chain = client.get_provenance_chain(decision_id) decision = chain["decision"] print("Decision:", decision["content"][:96] + "...") print("Status:", decision["status"], "| domain:", decision["domain"]) print("Rationale:", decision["rationale"][:96] + "...") print("Rejected alternatives:") for option in decision["rejected_alternatives"]: print(" -", option.split(":")[0]) print("Evidence:") for row in chain["evidence"]: memory = row["memory"] meta = memory["metadata"] print(f" {meta['record_id']:<10} {meta['event_date']} {meta['title'][:60]}") if meta.get("record_type") == "decision": print(f" recommended by {meta['recommended_by']}, decided by {meta['decided_by']}") ``` Output: ```text Decision: Dose level 2 continues unchanged at 200 mg once daily with enhanced liver monitoring (weekly LFT... Status: active | domain: safety Rationale: The DMC ad hoc review of 2026-02-09 saw no imbalance that warranted dropping or changing a dose ... Rejected alternatives: - Stop randomization to dose level 2 - Reduce dose level 2 to 150 mg - Change nothing until the scheduled May DMC meeting Evidence: CT-SAE-01 2026-01-27 SAE-01: serious hepatic event, participant 02-003, site 02 ( CT-EP-02 2026-02-02 Medical monitor requests an ad hoc DMC review (2026-02-02) CT-EP-04 2026-02-09 DMC ad hoc meeting outcome (2026-02-09) CT-DEC-02 2026-02-10 Decision: dose level 2 continues at 200 mg with enhanced liv recommended by Helena Voss, decided by Daniel Okoro ``` ## JavaScript [#javascript] ```js // Provenance of the dose level 2 decision: who recommended, who decided, what was rejected, on what evidence. import { SmartMemoryClient } from "@smartmemory/sdk-js"; const client = new SmartMemoryClient({ apiBaseUrl: process.env.SMARTMEMORY_BASE_URL ?? "https://api.smartmemory.ai", apiKey: process.env.SMARTMEMORY_API_KEY, storage: "memory", // a script has no browser storage }); client.setWorkspaceId(process.env.SMARTMEMORY_WORKSPACE_ID); const found = await client.decisions.search("dose level 2"); const decisionId = found.decisions[0].decision_id; const chain = await client.decisions.getProvenance(decisionId); const decision = chain.decision; console.log("Decision:", decision.content.slice(0, 96) + "..."); console.log("Status:", decision.status, "| domain:", decision.domain); console.log("Rationale:", decision.rationale.slice(0, 96) + "..."); console.log("Rejected alternatives:"); for (const option of decision.rejected_alternatives) console.log(" -", option.split(":")[0]); console.log("Evidence:"); for (const { memory } of chain.evidence) { const meta = memory.metadata; console.log(` ${meta.record_id.padEnd(10)} ${meta.event_date} ${meta.title.slice(0, 60)}`); if (meta.record_type === "decision") { console.log(` recommended by ${meta.recommended_by}, decided by ${meta.decided_by}`); } } ``` Output: ```text Decision: Dose level 2 continues unchanged at 200 mg once daily with enhanced liver monitoring (weekly LFT... Status: active | domain: safety Rationale: The DMC ad hoc review of 2026-02-09 saw no imbalance that warranted dropping or changing a dose ... Rejected alternatives: - Stop randomization to dose level 2 - Reduce dose level 2 to 150 mg - Change nothing until the scheduled May DMC meeting Evidence: CT-SAE-01 2026-01-27 SAE-01: serious hepatic event, participant 02-003, site 02 ( CT-EP-02 2026-02-02 Medical monitor requests an ad hoc DMC review (2026-02-02) CT-EP-04 2026-02-09 DMC ad hoc meeting outcome (2026-02-09) CT-DEC-02 2026-02-10 Decision: dose level 2 continues at 200 mg with enhanced liv recommended by Helena Voss, decided by Daniel Okoro ``` The output below is from a real run against a local test stack seeded with the demo dataset. It shows record ids and dates only. # Export the audit trail (/smartmemory/examples/clinical-trials/export-audit-trail) Export the whole workspace as a portable bundle, and write one audit line per study record. Set `SMARTMEMORY_API_KEY`, `SMARTMEMORY_WORKSPACE_ID` and optionally `SMARTMEMORY_BASE_URL` (default `https://api.smartmemory.ai`). See the [overview](/smartmemory/examples/clinical-trials/) for setup. ## What it uses [#what-it-uses] * `client.export_okf` and `client.exportOkf` download the workspace as a gzipped OKF bundle. They need workspace admin. * `client.explain` and `client.memories.explain` return the audit answer for one memory: when it was recorded, when it was valid, its version history, and the decisions that cite it. * `chain_verified` is `null` when a record has no edits to verify. That is not a warning. ## Python [#python] ```python """Export the workspace: a portable OKF bundle, plus one audit line per memory.""" import io import json import os import tarfile from smartmemory_client import SmartMemoryClient client = SmartMemoryClient( base_url=os.environ.get("SMARTMEMORY_BASE_URL", "https://api.smartmemory.ai"), api_key=os.environ["SMARTMEMORY_API_KEY"], workspace_id=os.environ["SMARTMEMORY_WORKSPACE_ID"], ) # 1. The whole workspace as a gzipped bundle. Needs workspace admin. bundle = client.export_okf() with open("clinical-trial-demo.okf.tar.gz", "wb") as out: out.write(bundle) with tarfile.open(fileobj=io.BytesIO(bundle)) as archive: print("bundle:", len(bundle), "bytes,", len(archive.getnames()), "files") # 2. The audit answer for every study memory: when it was recorded, when it was valid, # its version history, and the decisions that cite it. lines = 0 offset = 0 with open("clinical-trial-demo.audit.jsonl", "w") as out: while True: page = client.list_memories( metadata_key="study", metadata_value="KVR-417-201", limit=50, offset=offset ) for item in page["items"]: audit = client.explain(item["item_id"]) row = { "record_id": item["metadata"]["record_id"], "recorded_at": audit["item"]["transaction_time"], "valid_from": audit["item"]["valid_start_time"], "versions": len(audit["versions"]), "chain_verified": audit["chain_verified"], "cited_by": [d["decision_id"] for d in audit["decision_provenance"]["citing_decisions"]], } out.write(json.dumps(row) + "\n") lines += 1 offset += len(page["items"]) if not page["items"] or offset >= page["total"]: break print("audit lines:", lines) ``` Output: ```text bundle: 37048 bytes, 75 files audit lines: 61 ``` ## JavaScript [#javascript] ```js // Export the workspace: a portable OKF bundle, plus one audit line per memory. import { writeFileSync, createWriteStream } from "node:fs"; import { gunzipSync } from "node:zlib"; import { SmartMemoryClient } from "@smartmemory/sdk-js"; const client = new SmartMemoryClient({ apiBaseUrl: process.env.SMARTMEMORY_BASE_URL ?? "https://api.smartmemory.ai", apiKey: process.env.SMARTMEMORY_API_KEY, storage: "memory", // a script has no browser storage }); client.setWorkspaceId(process.env.SMARTMEMORY_WORKSPACE_ID); // 1. The whole workspace as a gzipped bundle. Needs workspace admin. const bundle = Buffer.from(await client.exportOkf()); writeFileSync("clinical-trial-demo.okf.tar.gz", bundle); // A tar archive is 512-byte blocks: a header block, then the file body padded to a block. // Extended-attribute headers (type x) describe the next entry, they are not files. const tar = gunzipSync(bundle); let files = 0; for (let at = 0; at + 512 <= tar.length && tar[at] !== 0; ) { const size = parseInt(tar.toString("ascii", at + 124, at + 135).trim(), 8) || 0; if (tar[at + 156] !== 0x78) files += 1; at += 512 + Math.ceil(size / 512) * 512; } console.log("bundle:", bundle.length, "bytes,", files, "files"); // 2. The audit answer for every study memory: when it was recorded, when it was valid, // its version history, and the decisions that cite it. const out = createWriteStream("clinical-trial-demo.audit.jsonl"); let lines = 0; for (let offset = 0; ; ) { const page = await client.memories.list({ metadataKey: "study", metadataValue: "KVR-417-201", limit: 50, offset, }); for (const item of page.items) { const audit = await client.memories.explain(item.item_id); out.write(JSON.stringify({ record_id: item.metadata.record_id, recorded_at: audit.item.transaction_time, valid_from: audit.item.valid_start_time, versions: audit.versions.length, chain_verified: audit.chain_verified, cited_by: audit.decision_provenance.citing_decisions.map((d) => d.decision_id), }) + "\n"); lines += 1; } offset += page.items.length; if (!page.items.length || offset >= page.total) break; } out.end(); console.log("audit lines:", lines); ``` Output: ```text bundle: 40625 bytes, 75 files audit lines: 61 ``` ## Audit line [#audit-line] Both scripts write `clinical-trial-demo.okf.tar.gz` and `clinical-trial-demo.audit.jsonl` to the current directory. One audit line, from the local run: ```json { "record_id": "CT-DEC-02", "recorded_at": "2026-11-02T19:38:35.236241+00:00", "valid_from": "2026-02-10T00:00:00+00:00", "versions": 0, "chain_verified": null, "cited_by": [ "dec_4df2ad5d51e2" ] } ``` The local workspace had 61 study records, 18 of them cited by a decision. The output below is from a real run against a local test stack seeded with the demo dataset. It shows record ids and dates only. # Clinical trial operations (/smartmemory/examples/clinical-trials) Four runnable examples, each in Python and JavaScript, for a clinical trial operations team that wants a record of what the study decided, when, and on what evidence. They use a fictional Phase II study (KVR-417-201) with four sites, a CRO and a sponsor, all invented for the demo. | Example | What it does | | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | [Ingest a monitoring visit report](/smartmemory/examples/clinical-trials/ingest-visit-report/) | Add a new CRA visit report with the visit date as its valid time | | [Ask an as-of question](/smartmemory/examples/clinical-trials/as-of-question/) | Which consent form version should site 03 have used on 2026-03-03 | | [Provenance of a decision](/smartmemory/examples/clinical-trials/decision-provenance/) | Who recommended and who decided dose level 2, what was rejected, and the evidence | | [Export the audit trail](/smartmemory/examples/clinical-trials/export-audit-trail/) | Download the workspace and write one audit line per record | ## Before you run them [#before-you-run-them] You need the "Clinical trial demo" workspace, an API key for it, and the SmartMemory 2.0 SDKs (the ones whose `add` returns a save receipt). ```bash pip install smartmemory-client # Python, 2.0 line npm install @smartmemory/sdk-js # JavaScript, 2.0 line export SMARTMEMORY_API_KEY=... # your key export SMARTMEMORY_WORKSPACE_ID=... # the demo workspace id # optional, defaults to https://api.smartmemory.ai # export SMARTMEMORY_BASE_URL=... ``` The JavaScript files are ES modules. Run them with `node 01_ingest_visit_report.mjs` and the Python files with `python 01_ingest_visit_report.py`. Each example has its own page with the full code: [ingest a visit report](/smartmemory/examples/clinical-trials/ingest-visit-report/), [ask an as-of question](/smartmemory/examples/clinical-trials/as-of-question/), [provenance of a decision](/smartmemory/examples/clinical-trials/decision-provenance/), and [export the audit trail](/smartmemory/examples/clinical-trials/export-audit-trail/). ## Read more [#read-more] * The whitepaper: [Memory for clinical trial operations](https://docs.smartmemory.ai/whitepapers/smartmemory-clinical-trial-operations.pdf) * A live demo app on the same dataset: [trial.smartmemory.ai](https://trial.smartmemory.ai) * [Decisions lifecycle example](/smartmemory/examples/decisions-lifecycle/) ## What this is not [#what-this-is-not] SmartMemory keeps a traceable, exportable record next to your trial systems. It does not make clinical decisions and it does not replace your EDC or CTMS. # Ingest a monitoring visit report (/smartmemory/examples/clinical-trials/ingest-visit-report) Add a new CRA monitoring visit report (MVR) to the workspace, with the visit date as its valid time so that date-aware questions find it. Set `SMARTMEMORY_API_KEY`, `SMARTMEMORY_WORKSPACE_ID` and optionally `SMARTMEMORY_BASE_URL` (default `https://api.smartmemory.ai`). See the [overview](/smartmemory/examples/clinical-trials/) for setup. ## What it uses [#what-it-uses] * `client.add` (Python) and `client.memories.create` (JavaScript) store the report. Both return a save receipt, not a bare id. * `await_receipt` and `awaitReceipt` wait until the save has a final outcome. * `metadata.record_id` is how this dataset identifies a record, so the script looks it up first and a second run adds nothing. * `metadata.valid_start_time` is the day the visit happened. Without it the service picks a date it finds in the text, which can be off by a day. ## Python [#python] ```python """Ingest a new monitoring visit report (MVR) into the "Clinical trial demo" workspace.""" import os from smartmemory_client import SmartMemoryClient client = SmartMemoryClient( base_url=os.environ.get("SMARTMEMORY_BASE_URL", "https://api.smartmemory.ai"), api_key=os.environ["SMARTMEMORY_API_KEY"], workspace_id=os.environ["SMARTMEMORY_WORKSPACE_ID"], ) RECORD_ID = "EX-MVR-S01-20260714" REPORT = ( "Monitoring visit report: site 01, interim visit, 2026-07-14. " "CRA Nadia Kerr made an on-site interim monitoring visit to site 01 (Halvard) on 2026-07-14. " "Findings: (1) 100 percent source data verification of consent and eligibility for the 11 participants " "randomized since the last visit, no findings, (2) the week 6 liver test tracker added after DEV-05 is in use " "and every due test in the period was drawn inside its window, (3) the deviation log matches the visit " "findings under procedure v1.1, no new deviations, (4) enrolment under the raised cap of 40 is on track, " "(5) 7 open queries, none older than 8 business days. " "Action items: send the follow-up letter (Nadia Kerr, 2026-07-21), confirm the cap tracker with the " "sponsor (Daniel Okoro, 2026-07-24). Report finalized on 2026-07-21." ) # Re-running must not add a second copy: records are identified by metadata.record_id. existing = client.list_memories(metadata_key="record_id", metadata_value=RECORD_ID) if existing["total"]: print(f"{RECORD_ID} is already in the workspace, nothing to do") raise SystemExit(0) receipt = client.add( REPORT, memory_type="episodic", metadata={ "record_id": RECORD_ID, "record_type": "monitoring_report", "study": "KVR-417-201", "site": "01", "event_date": "2026-07-14", # Valid time: the day the visit happened. As-of questions filter on this field. "valid_start_time": "2026-07-14T00:00:00+00:00", }, # Leave extraction on against a real deployment. CT_EXAMPLES_PIPELINE=0 stores the text as written, # for a stack whose language model provider is not reachable. use_pipeline=os.environ.get("CT_EXAMPLES_PIPELINE", "1") != "0", ) receipt = client.await_receipt(receipt) print("item_id:", receipt.item_id) print("stage:", receipt.processing_stage, "| outcome:", receipt.outcome, "| terminal:", receipt.terminal) stored = client.get(receipt.item_id) print("record_id:", stored.metadata["record_id"]) print("valid_start_time:", stored.valid_start_time) ``` Output: ```text item_id: 1c943109-f2d0-427a-bd0e-b40030de14ae stage: stored | outcome: skipped | terminal: True record_id: EX-MVR-S01-20260714 valid_start_time: 2026-07-14T00:00:00+00:00 ``` ## JavaScript [#javascript] ```js // Ingest a new monitoring visit report (MVR) into the "Clinical trial demo" workspace. import { SmartMemoryClient } from "@smartmemory/sdk-js"; const client = new SmartMemoryClient({ apiBaseUrl: process.env.SMARTMEMORY_BASE_URL ?? "https://api.smartmemory.ai", apiKey: process.env.SMARTMEMORY_API_KEY, storage: "memory", // a script has no browser storage }); client.setWorkspaceId(process.env.SMARTMEMORY_WORKSPACE_ID); const RECORD_ID = "EX-MVR-S01-20260714"; const REPORT = "Monitoring visit report: site 01, interim visit, 2026-07-14. " + "CRA Nadia Kerr made an on-site interim monitoring visit to site 01 (Halvard) on 2026-07-14. " + "Findings: (1) 100 percent source data verification of consent and eligibility for the 11 participants " + "randomized since the last visit, no findings, (2) the week 6 liver test tracker added after DEV-05 is in use " + "and every due test in the period was drawn inside its window, (3) the deviation log matches the visit " + "findings under procedure v1.1, no new deviations, (4) enrolment under the raised cap of 40 is on track, " + "(5) 7 open queries, none older than 8 business days. " + "Action items: send the follow-up letter (Nadia Kerr, 2026-07-21), confirm the cap tracker with the " + "sponsor (Daniel Okoro, 2026-07-24). Report finalized on 2026-07-21."; // Re-running must not add a second copy: records are identified by metadata.record_id. const existing = await client.memories.list({ metadataKey: "record_id", metadataValue: RECORD_ID }); if (existing.total) { console.log(`${RECORD_ID} is already in the workspace, nothing to do`); process.exit(0); } let receipt = await client.memories.create({ content: REPORT, memoryType: "episodic", metadata: { record_id: RECORD_ID, record_type: "monitoring_report", study: "KVR-417-201", site: "01", event_date: "2026-07-14", // Valid time: the day the visit happened. As-of questions filter on this field. valid_start_time: "2026-07-14T00:00:00+00:00", }, // Leave extraction on against a real deployment. CT_EXAMPLES_PIPELINE=0 stores the text as written, // for a stack whose language model provider is not reachable. usePipeline: process.env.CT_EXAMPLES_PIPELINE !== "0", }); receipt = await client.memories.awaitReceipt(receipt); console.log("item_id:", receipt.item_id); console.log("stage:", receipt.processing_stage, "| outcome:", receipt.outcome, "| terminal:", receipt.terminal); const stored = await client.memories.get(receipt.item_id); console.log("record_id:", stored.metadata.record_id); console.log("valid_start_time:", stored.valid_start_time); ``` Output: ```text item_id: 303c4645-b4fb-4fb7-afd0-fbef797f6551 stage: stored | outcome: skipped | terminal: true record_id: EX-MVR-S01-20260714 valid_start_time: 2026-07-14T00:00:00+00:00 ``` ## Notes [#notes] The report is fictional and new, it does not copy any record in the dataset. Against a deployment with a working language model leave extraction on (the default). The local run set `CT_EXAMPLES_PIPELINE=0` because the local stack's language model provider is unavailable, so the receipt shows `outcome: skipped` for the extraction step. The text and metadata are stored as written either way. The output below is from a real run against a local test stack seeded with the demo dataset. It shows record ids and dates only.