Metadata-Version: 2.4
Name: iglegais
Version: 0.9.0
Summary: memory that answers why, not just what. causal graph memory with an mcp server.
Project-URL: Repository, https://github.com/Cintu07/iglegais
Author: Pawan Kalyan
License: MIT License
        
        Copyright (c) 2026 Pawan Kalyan
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: ai,knowledge-graph,mcp,memory,vector-search
Requires-Python: >=3.10
Requires-Dist: mcp>=1.0
Requires-Dist: numpy>=1.24
Requires-Dist: openai>=1.50
Requires-Dist: sentence-transformers>=3.0
Provides-Extra: scale
Requires-Dist: sqlite-vec>=0.1.6; extra == 'scale'
Description-Content-Type: text/markdown

<p align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="assets/icon-dark.svg">
    <img src="assets/icon.svg" alt="iglegais" width="110">
  </picture>
</p>

# iglegais

memory that remembers why, not just what.

most memory tools keep your stuff as a flat pile of vectors. you ask something, they hand back the most similar chunk of text and call it a day. they cannot tell you why something happened, or how it changed over time.

iglegais stores each memory as a node with a vector *and* typed edges (`caused_by`, `contradicts`, `follows`). so recall does two things a flat store simply cannot:

1. vector search to find the memory you actually mean
2. walk the graph to hand you its cause, its contradictions, and how it evolved

it answers two questions similarity search cannot: **why** did something happen (walk the causal chain to the root), and **what is still true** (when a newer memory contradicts an older one, return the current belief, not the stale one).

### the diet coke problem

you say "i like diet coke" in january and "i don't like diet coke" in may. what
should a memory return today? similarity search is helpless here: to an
embedding model, `i like x` and `i don't like x` are almost identical (they
score ~0.87 similar), because embeddings are blind to negation. so it cannot
tell which one is current.

iglegais keys the memory to a **subject**. a later memory on the same subject
supersedes the earlier one, deterministically, no llm and no fuzzy matching:

```python
mg.add("I like diet coke", subject="diet coke")
mg.add("I don't like diet coke anymore", subject="diet coke")

mg.what_is_true(subject="diet coke")        # -> "I don't like diet coke anymore"
mg.what_is_true(subject="diet coke", as_of=jan_ts)  # -> "I like diet coke" (true back then)
mg.belief_history("diet coke")              # -> the full timeline, oldest to newest
```

it survives oscillation too (like -> dislike -> like again, latest wins) and
keeps every subject isolated. `belief_history` answers a question no flat memory
store can: how did my opinion on something change over time.

## the demo

```
Q: why did the pipeline go down?

closest memory:
   "ops got paged at 2am: the zorbex-9 pipeline went completely down."

caused by:
   "the zorbex-9 module leaked memory and crashed the data pipeline."

later fixed by:
   "patched the zorbex-9 memory leak and the pipeline recovered."
```

a flat store gives you the first line and shrugs. the graph is what finds the root cause.

## real world test

two totally separate incidents (an auth outage and a caching bug) jumbled into one graph. the hard part is telling them apart and blaming the right thing:

```
Q: why were users getting logged out?
   root cause:  migrated the auth service to a new jwt library     (correct)

Q: why was the dashboard showing old numbers?
   root cause:  enabled a new caching layer                        (correct)

both passed. it kept the incidents straight.
```

## runs on your machine, costs nothing

everything lives in a single local file. local embeddings, no server to run, no
cloud, no bill. your memories never leave the machine.

## use it

```bash
pip install iglegais
```

that's it. no database to install, nothing to start. add it to your assistant:

```bash
claude mcp add iglegais -- iglegais
```

your assistant now has four tools: `remember`, `recall`, `why`, `whats_true`.
it stores plain text, walks a chain of causes when you ask why something
happened, and tracks what is still true when a newer memory retracts an older
one.

```text
you: remember: the deploy failed because of a race in migrations
you: why did the deploy fail?

you: remember: the primary database is postgres
you: remember: we migrated the primary database to mysql
you: whats true about the primary database?   -> mysql (postgres is retracted)
```

memories are kept in `~/.iglegais/memory.db` (override with `IGLEGAIS_DB`).

### library use

```python
from iglegais import MemoryGraph

mg = MemoryGraph()
mg.setup()
mg.add("the deploy had a race condition")                 # or explicit edges
mg.recall("why did the service fail to boot?")            # cause + contradictions
mg.root_cause("why did the service fail to boot?")        # full chain to the root
```

### it builds the graph itself

with a free `CEREBRAS_API_KEY` set, `remember()` reads plain text and infers the
edges *and* the subject in one call, so nobody wires anything by hand:

```python
mg.remember("I really like diet coke")
mg.remember("actually I don't like diet coke anymore")    # inferred: same subject, contradicts
mg.what_is_true(query="do I like diet coke?")             # -> "...don't like diet coke anymore"
```

without a key it still stores everything; you just pass edges/subjects yourself.

### it consolidates itself (the "dream" pass)

a bigger pile of memories is not a better memory. `consolidate()` runs an offline
reconciliation pass, the way a brain reorganizes during sleep: it folds duplicate
memories together and reads clusters of related memories to write the higher-order
fact they imply, keeping every original as history with a link back to it.

```python
mg.add("ordered pad thai on friday")
mg.add("got thai takeout again this friday")
mg.add("friday dinner was pad thai from the usual spot")
mg.consolidate()
# synthesizes: "regularly orders pad thai on friday evenings, likely a favorite"
# with SYNTHESIZED_FROM edges back to the three sources. originals stay queryable.

mg.recall("what food does the user like?")   # now surfaces the pattern
```

nothing is ever destroyed: merges and syntheses are additive and keep provenance,
so you can always trace or undo. run it on idle, not the hot path.

it can also fire on a threshold instead of by hand, the way an lsm store compacts
once enough writes pile up:

```python
mg.consolidate_if_due(every=20)               # runs only once 20 new memories exist
LocalMemoryGraph(auto_consolidate_every=20)   # or let remember() trigger it
```

### it connects the same person, place, or thing

every memory is scanned for entities (a cheap local pass, no llm), so you can ask
for everything about something or find related memories:

```python
mg.add("Alice shipped the billing service in Berlin")
mg.find_entity("alice")     # every memory that mentions alice, newest first
mg.related(memory_id)       # other memories that share an entity with this one
```

### hybrid retrieval

`search_hybrid` fuses three signals with reciprocal rank fusion: vector meaning,
keyword overlap (exact terms like codes and ids that embeddings smear together),
and shared entities. it beats any single signal on its own.

```python
mg.search_hybrid("error code zx9q", k=5)   # the exact-code memory wins
```

### it says when it isn't sure

recall does not dress up a weak or tied match as fact. it abstains when nothing
is confidently relevant, and flags a near tie instead of guessing:

```python
mg.recall("something we never stored")     # confident: False, low confidence
mg.what_is_true(query="never stored")       # status: "unsure", answer: None
mg.recall("book a flight")                   # ambiguous: True if paris and berlin both match
```

every recall returns a `confidence` score and `confident` / `ambiguous` flags.

### scale: indexed search in the same file

for large memory sets, install the optional index and searches run in C inside
the same sqlite file (namespace is a partition key, so knn only scans one user):

```bash
pip install "iglegais[scale]"
```

search-only time, indexed vs the plain numpy scan, same correct result:

```
   corpus      numpy      sqlite-vec    speedup
    5,000      140 ms         18 ms       7.6x
  100,000     2448 ms        600 ms       4.1x
  300,000     7157 ms       1582 ms       4.5x
```

it is fully optional: with nothing extra installed, the numpy path is used and
everything works the same, just slower at very large sizes.

### many users, one instance

every call takes a `namespace` so one database serves many users or agents with
zero bleed between them:

```python
mg.add("my favorite language is python", subject="language", namespace="alice")
mg.add("my favorite language is rust",   subject="language", namespace="bob")
mg.what_is_true(subject="language", namespace="alice")    # -> python
mg.what_is_true(subject="language", namespace="bob")      # -> rust
```

### automatic edge inference (optional)

`remember(content)` can infer `caused_by` / `contradicts` / `follows` edges from
plain text using a hosted model. set a free `CEREBRAS_API_KEY` in the
environment or `~/.iglegais/.env` to turn it on. without a key, use `add()` with
explicit edges (shown above); everything else works the same.

### bigger datasets (optional)

for very large memory sets you can point iglegais at a graph+vector server
instead of the local file: set `IGLEGAIS_BACKEND=helix` (and `HELIX_URL` if not
`localhost:6969`). the local file is the default and is plenty for personal use.

### run the tests

```bash
python verify_local.py   # local backend, no server: asserts the root cause is found
python stress_test.py    # brutal suite: cycles, deep chains, discrimination under noise, scale
python temporal_test.py  # current-truth recall and point-in-time as-of queries
python belief_test.py    # the diet coke problem: preference reversal, oscillation, timelines
python namespace_test.py # multi-user isolation: no bleed between namespaces
python uncertainty_test.py # abstains on weak matches, flags near ties
python consolidate_test.py # the dream pass: merge duplicates, synthesize patterns
python entity_test.py    # entity linking: find everything about a person or thing
python hybrid_test.py    # rank fusion: exact-term queries beat pure vector search
python scheduled_test.py # threshold-triggered consolidation
python scale_bench.py    # indexed vs numpy search across corpus sizes
```

the stress suite tries to break the engine: causal loops and self loops (must
not hang), a 15 hop chain, six separate incidents jumbled with 120 noise
memories (must keep every root straight), persistence across reopen, unicode
and 20k char memories, and a needle in a 400 memory haystack.

### benchmark: root-cause retrieval

```bash
python benchmark.py
```

this measures the one thing this is built for: given a symptom question, return
the root cause, which sits several hops away and is worded nothing like the
symptom. same corpus, three systems:

```
corpus: 8 incidents, 224 total memories

  A. flat vector top-1   root-cause accuracy:  0%
  B. flat vector top-3   root-cause recall  :  0%
  C. iglegais root_cause accuracy           : 100%    (median ~15 ms/query)
```

it also measures temporal current-truth retrieval, where a fact is later
contradicted by an updated one:

```
  flat vector top-1  current-truth accuracy:   0%
  iglegais what_is_true accuracy           : 100%
```

similarity search lands on the symptom (or the stale fact) because it is worded
just like the query. walking the causal graph recovers the actual root, and the
contradiction edges plus timestamps recover the current truth. this is not a
general memory database benchmark, it is the two slices this is built for. run
it yourself.

## how it works

- `add(content, ...)` embeds the text and stores a memory node with optional
  `caused_by` / `contradicts` / `follows` edges to earlier memories.
- `recall(query)` vector searches to the closest memory, then walks the
  edges to give you the reasoning around it.
- `root_cause(query)` keeps walking `caused_by` hops until it hits the root.

a few hundred lines of python: memories are rows, edges are rows, vector search
is a dot product over normalized embeddings. small on purpose.

## where it goes next

- per user memory spaces
- dedup on ingest
- flag stale memories when a newer one contradicts them
- a visual graph of your memory

---

built by [@Cintu07](https://github.com/Cintu07).
