icelake 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- icelake-0.1.0/.github/workflows/publish.yml +43 -0
- icelake-0.1.0/.gitignore +22 -0
- icelake-0.1.0/.python-version +1 -0
- icelake-0.1.0/AGENTS.md +76 -0
- icelake-0.1.0/LICENSE +21 -0
- icelake-0.1.0/PKG-INFO +513 -0
- icelake-0.1.0/README.md +480 -0
- icelake-0.1.0/docs/API.md +599 -0
- icelake-0.1.0/docs/PLAN.md +740 -0
- icelake-0.1.0/docs/RESEARCH.md +554 -0
- icelake-0.1.0/docs/ROADMAP.md +246 -0
- icelake-0.1.0/docs/USAGE.md +410 -0
- icelake-0.1.0/evals/golden/adversarial_injection.yaml +8 -0
- icelake-0.1.0/evals/golden/ambiguous_identity.yaml +14 -0
- icelake-0.1.0/evals/golden/bare_link_skip.yaml +7 -0
- icelake-0.1.0/evals/golden/contradiction_supersede.yaml +14 -0
- icelake-0.1.0/evals/golden/cross_user_isolation.yaml +21 -0
- icelake-0.1.0/evals/golden/durable_preference.yaml +14 -0
- icelake-0.1.0/evals/golden/mention_extraction.yaml +18 -0
- icelake-0.1.0/evals/golden/reinforce_not_duplicate.yaml +15 -0
- icelake-0.1.0/evals/golden/server_culture.yaml +14 -0
- icelake-0.1.0/evals/golden/snowflake_poisoning.yaml +7 -0
- icelake-0.1.0/evals/golden/third_party_attribution.yaml +18 -0
- icelake-0.1.0/evals/golden/transient_state_skip.yaml +9 -0
- icelake-0.1.0/evals/golden/write_create_biographical.yaml +14 -0
- icelake-0.1.0/evals/golden/write_skip_noise.yaml +9 -0
- icelake-0.1.0/evals/golden/write_skip_questions.yaml +9 -0
- icelake-0.1.0/evals/golden_runner.py +173 -0
- icelake-0.1.0/examples/bench_models.py +176 -0
- icelake-0.1.0/examples/e2e_simulation.py +1410 -0
- icelake-0.1.0/examples/omni_style_bot.py +380 -0
- icelake-0.1.0/examples/ping_reply_bot.py +275 -0
- icelake-0.1.0/examples/relationship_queries.py +266 -0
- icelake-0.1.0/pyproject.toml +88 -0
- icelake-0.1.0/src/icelake/__init__.py +172 -0
- icelake-0.1.0/src/icelake/_json.py +125 -0
- icelake-0.1.0/src/icelake/adapters/__init__.py +1 -0
- icelake-0.1.0/src/icelake/adapters/embedders/__init__.py +126 -0
- icelake-0.1.0/src/icelake/adapters/embedders/cached.py +74 -0
- icelake-0.1.0/src/icelake/adapters/embedders/local.py +51 -0
- icelake-0.1.0/src/icelake/adapters/in_memory/__init__.py +10 -0
- icelake-0.1.0/src/icelake/adapters/in_memory/queue.py +245 -0
- icelake-0.1.0/src/icelake/adapters/in_memory/store.py +819 -0
- icelake-0.1.0/src/icelake/adapters/in_memory/vectors.py +58 -0
- icelake-0.1.0/src/icelake/adapters/llm_cache.py +51 -0
- icelake-0.1.0/src/icelake/adapters/llm_openai_compat.py +235 -0
- icelake-0.1.0/src/icelake/adapters/llm_openrouter.py +41 -0
- icelake-0.1.0/src/icelake/adapters/meter.py +163 -0
- icelake-0.1.0/src/icelake/adapters/mongo/__init__.py +1108 -0
- icelake-0.1.0/src/icelake/adapters/mongo/mapping.py +269 -0
- icelake-0.1.0/src/icelake/adapters/mongo/queue.py +300 -0
- icelake-0.1.0/src/icelake/adapters/mongo/vectors.py +102 -0
- icelake-0.1.0/src/icelake/adapters/sqlite/connection.py +311 -0
- icelake-0.1.0/src/icelake/adapters/sqlite/llm_cache.py +40 -0
- icelake-0.1.0/src/icelake/adapters/sqlite/queue.py +315 -0
- icelake-0.1.0/src/icelake/adapters/sqlite/store.py +68 -0
- icelake-0.1.0/src/icelake/adapters/sqlite/store_facts.py +827 -0
- icelake-0.1.0/src/icelake/adapters/sqlite/store_graph.py +465 -0
- icelake-0.1.0/src/icelake/adapters/sqlite/vectors.py +121 -0
- icelake-0.1.0/src/icelake/api/classify.py +85 -0
- icelake-0.1.0/src/icelake/api/client.py +809 -0
- icelake-0.1.0/src/icelake/api/events.py +77 -0
- icelake-0.1.0/src/icelake/api/facts_api.py +350 -0
- icelake-0.1.0/src/icelake/api/groups.py +274 -0
- icelake-0.1.0/src/icelake/config.py +321 -0
- icelake-0.1.0/src/icelake/consolidation/service.py +162 -0
- icelake-0.1.0/src/icelake/errors.py +72 -0
- icelake-0.1.0/src/icelake/graph/__init__.py +5 -0
- icelake-0.1.0/src/icelake/graph/relations.py +88 -0
- icelake-0.1.0/src/icelake/graph/traversal.py +102 -0
- icelake-0.1.0/src/icelake/graph/writes.py +213 -0
- icelake-0.1.0/src/icelake/identity/__init__.py +6 -0
- icelake-0.1.0/src/icelake/identity/aliases.py +162 -0
- icelake-0.1.0/src/icelake/identity/guards.py +63 -0
- icelake-0.1.0/src/icelake/identity/resolver.py +96 -0
- icelake-0.1.0/src/icelake/ids.py +27 -0
- icelake-0.1.0/src/icelake/ingest/__init__.py +5 -0
- icelake-0.1.0/src/icelake/ingest/context_builder.py +60 -0
- icelake-0.1.0/src/icelake/ingest/executor.py +292 -0
- icelake-0.1.0/src/icelake/ingest/extraction.py +149 -0
- icelake-0.1.0/src/icelake/ingest/gates.py +186 -0
- icelake-0.1.0/src/icelake/ingest/pipeline.py +684 -0
- icelake-0.1.0/src/icelake/ingest/reconcile.py +269 -0
- icelake-0.1.0/src/icelake/ingest/roster.py +98 -0
- icelake-0.1.0/src/icelake/integrations/__init__.py +5 -0
- icelake-0.1.0/src/icelake/integrations/discord_py.py +166 -0
- icelake-0.1.0/src/icelake/lifecycle/__init__.py +13 -0
- icelake-0.1.0/src/icelake/lifecycle/maintenance.py +92 -0
- icelake-0.1.0/src/icelake/lifecycle/prune.py +50 -0
- icelake-0.1.0/src/icelake/lifecycle/strength.py +54 -0
- icelake-0.1.0/src/icelake/lifecycle/tiers.py +86 -0
- icelake-0.1.0/src/icelake/models/__init__.py +117 -0
- icelake-0.1.0/src/icelake/models/admin.py +96 -0
- icelake-0.1.0/src/icelake/models/common.py +46 -0
- icelake-0.1.0/src/icelake/models/events.py +137 -0
- icelake-0.1.0/src/icelake/models/facts.py +166 -0
- icelake-0.1.0/src/icelake/models/graph.py +111 -0
- icelake-0.1.0/src/icelake/models/identity.py +75 -0
- icelake-0.1.0/src/icelake/models/operations.py +119 -0
- icelake-0.1.0/src/icelake/models/retrieval.py +175 -0
- icelake-0.1.0/src/icelake/ports/__init__.py +39 -0
- icelake-0.1.0/src/icelake/ports/clock.py +51 -0
- icelake-0.1.0/src/icelake/ports/llm.py +103 -0
- icelake-0.1.0/src/icelake/ports/queue.py +133 -0
- icelake-0.1.0/src/icelake/ports/store.py +351 -0
- icelake-0.1.0/src/icelake/ports/vectors.py +64 -0
- icelake-0.1.0/src/icelake/prompts/__init__.py +1 -0
- icelake-0.1.0/src/icelake/prompts/extraction.py +105 -0
- icelake-0.1.0/src/icelake/py.typed +0 -0
- icelake-0.1.0/src/icelake/retrieval/__init__.py +5 -0
- icelake-0.1.0/src/icelake/retrieval/channels.py +220 -0
- icelake-0.1.0/src/icelake/retrieval/injection.py +171 -0
- icelake-0.1.0/src/icelake/retrieval/service.py +388 -0
- icelake-0.1.0/src/icelake/scoring/__init__.py +5 -0
- icelake-0.1.0/src/icelake/scoring/fusion.py +105 -0
- icelake-0.1.0/src/icelake/structured.py +75 -0
- icelake-0.1.0/tests/__init__.py +0 -0
- icelake-0.1.0/tests/conftest.py +160 -0
- icelake-0.1.0/tests/integration/__init__.py +0 -0
- icelake-0.1.0/tests/integration/test_adversarial.py +350 -0
- icelake-0.1.0/tests/integration/test_agentic_e2e.py +464 -0
- icelake-0.1.0/tests/integration/test_api_groups.py +139 -0
- icelake-0.1.0/tests/integration/test_coverage_backends.py +308 -0
- icelake-0.1.0/tests/integration/test_decay_loop.py +109 -0
- icelake-0.1.0/tests/integration/test_discord_integration.py +152 -0
- icelake-0.1.0/tests/integration/test_examples.py +189 -0
- icelake-0.1.0/tests/integration/test_golden_evals.py +18 -0
- icelake-0.1.0/tests/integration/test_graph_recall.py +124 -0
- icelake-0.1.0/tests/integration/test_hardening_round.py +614 -0
- icelake-0.1.0/tests/integration/test_import_export.py +57 -0
- icelake-0.1.0/tests/integration/test_mongo_live.py +214 -0
- icelake-0.1.0/tests/integration/test_mongo_vectors.py +74 -0
- icelake-0.1.0/tests/integration/test_phase0.py +262 -0
- icelake-0.1.0/tests/integration/test_pipeline_e2e.py +802 -0
- icelake-0.1.0/tests/integration/test_queue_conformance.py +126 -0
- icelake-0.1.0/tests/integration/test_recall_prompt.py +272 -0
- icelake-0.1.0/tests/integration/test_review_round.py +426 -0
- icelake-0.1.0/tests/integration/test_store_conformance.py +435 -0
- icelake-0.1.0/tests/unit/__init__.py +0 -0
- icelake-0.1.0/tests/unit/test_adapters.py +456 -0
- icelake-0.1.0/tests/unit/test_adapters_edge.py +226 -0
- icelake-0.1.0/tests/unit/test_config.py +177 -0
- icelake-0.1.0/tests/unit/test_coverage_completion.py +407 -0
- icelake-0.1.0/tests/unit/test_coverage_final.py +435 -0
- icelake-0.1.0/tests/unit/test_executor.py +282 -0
- icelake-0.1.0/tests/unit/test_extraction.py +294 -0
- icelake-0.1.0/tests/unit/test_gates.py +113 -0
- icelake-0.1.0/tests/unit/test_identity.py +129 -0
- icelake-0.1.0/tests/unit/test_injection_roster.py +225 -0
- icelake-0.1.0/tests/unit/test_lifecycle.py +211 -0
- icelake-0.1.0/tests/unit/test_models.py +141 -0
- icelake-0.1.0/tests/unit/test_mongo_adapter.py +176 -0
- icelake-0.1.0/tests/unit/test_reconcile.py +253 -0
- icelake-0.1.0/tests/unit/test_scoring_graph.py +189 -0
- icelake-0.1.0/tests/unit/test_structured.py +56 -0
- icelake-0.1.0/tests/unit/test_structured_outputs.py +182 -0
- icelake-0.1.0/uv.lock +2307 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v[0-9]+.[0-9]+.[0-9]+"
|
|
7
|
+
- "v[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
|
|
8
|
+
- "v[0-9]+.[0-9]+.[0-9]+[ab][0-9]+"
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
- name: Test
|
|
21
|
+
run: uv sync --group dev && uv run pytest tests/ -q
|
|
22
|
+
- name: Build
|
|
23
|
+
run: uv build
|
|
24
|
+
- uses: actions/upload-artifact@v4
|
|
25
|
+
with:
|
|
26
|
+
name: dist
|
|
27
|
+
path: dist/
|
|
28
|
+
|
|
29
|
+
publish:
|
|
30
|
+
needs: build
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
environment:
|
|
33
|
+
name: pypi
|
|
34
|
+
url: https://pypi.org/p/icelake
|
|
35
|
+
permissions:
|
|
36
|
+
id-token: write
|
|
37
|
+
steps:
|
|
38
|
+
- uses: astral-sh/setup-uv@v6
|
|
39
|
+
- uses: actions/download-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
name: dist
|
|
42
|
+
path: dist/
|
|
43
|
+
- run: uv publish
|
icelake-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
icelake-0.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Core Principles (Ordered - Higher Wins on Conflict)
|
|
2
|
+
|
|
3
|
+
## Code is a liability, not an asset
|
|
4
|
+
|
|
5
|
+
- Before writing code, answer: (1) Does a dependency or existing module already do
|
|
6
|
+
this? (2) What breaks upstream? (3) What depends on this downstream?
|
|
7
|
+
- Equal designs -> fewer lines wins. No speculative abstractions, no "just in case"
|
|
8
|
+
helpers, no unused parameters.
|
|
9
|
+
- Deleting code is a feature. When a module loses its justification, say so.
|
|
10
|
+
- Never add code to appear thorough. Deliberate, minimal additions only.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## Adopt proven machinery; never rebuild it
|
|
14
|
+
|
|
15
|
+
- If a mature, maintained dependency solves the problem, use it — don't hand-roll
|
|
16
|
+
a weaker version. Check the project's architecture decision records / plan docs
|
|
17
|
+
for what has been adopted and what is explicitly out of scope.
|
|
18
|
+
- Conversely: if a dependency is deprecated or unmaintained, flag it rather than
|
|
19
|
+
building deeper on top of it.
|
|
20
|
+
|
|
21
|
+
## One concern per module
|
|
22
|
+
|
|
23
|
+
- Each module owns one concern and exposes it through a narrow interface.
|
|
24
|
+
Reaching into another module's internals is a violation — use its contract.
|
|
25
|
+
|
|
26
|
+
## Design for the swap you can't predict
|
|
27
|
+
|
|
28
|
+
- Every external dependency (database, LLM provider, library) is treated as
|
|
29
|
+
replaceable. The test: "if we swapped X tomorrow, how many files change?"
|
|
30
|
+
If the answer is more than the adapter plus config, the abstraction has leaked.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Code Quality Standards
|
|
34
|
+
|
|
35
|
+
## Baseline
|
|
36
|
+
|
|
37
|
+
- Full type annotations on all public interfaces; strict type-checker clean.
|
|
38
|
+
- Linter + formatter clean before merge; no suppressions without a comment
|
|
39
|
+
explaining why.
|
|
40
|
+
- All data crossing a boundary (API I/O, persistence, inter-process, LLM I/O)
|
|
41
|
+
is validated by a schema. No raw dicts at boundaries.
|
|
42
|
+
- Fixed sets of values are enums, never raw strings.
|
|
43
|
+
|
|
44
|
+
## Clarity
|
|
45
|
+
|
|
46
|
+
- Names state intent and units (`timeout_seconds`, not `timeout`).
|
|
47
|
+
- Public capability entry points carry user-facing docstrings — treat them as
|
|
48
|
+
product surface, not developer notes.
|
|
49
|
+
- Async on all I/O paths; never block the event loop.
|
|
50
|
+
|
|
51
|
+
## Size gates (need explicit justification to exceed)
|
|
52
|
+
|
|
53
|
+
- Function: ~40 lines. Module: ~300 lines. Justification means a comment or PR
|
|
54
|
+
note explaining why splitting would be worse.
|
|
55
|
+
|
|
56
|
+
## Testing
|
|
57
|
+
|
|
58
|
+
- Business logic must be testable without transport, network, or live services —
|
|
59
|
+
inject fakes at the seams. Core correctness logic (validation, verification,
|
|
60
|
+
state transitions) gets exhaustive tests.
|
|
61
|
+
- Test behavior at interfaces, not implementation details.
|
|
62
|
+
|
|
63
|
+
## Every change passes this checklist
|
|
64
|
+
|
|
65
|
+
1. Does something that exists already do this?
|
|
66
|
+
2. What breaks upstream? What depends on this downstream?
|
|
67
|
+
3. Can this be fewer lines?
|
|
68
|
+
4. Is this logic duplicated elsewhere (or does it belong in a shared layer)?
|
|
69
|
+
5. Are new boundaries schema-validated?
|
|
70
|
+
6. Are new costs (API calls, storage growth, latency) metered and bounded?
|
|
71
|
+
7. Does this work with N processes, or only one?
|
|
72
|
+
|
|
73
|
+
## Hygiene
|
|
74
|
+
|
|
75
|
+
- No commented-out code (git is the history). No TODOs without a linked issue.
|
|
76
|
+
- No dead config knobs, no unused exports.
|
icelake-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nolan Gregory
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
icelake-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: icelake
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Accurate, scalable, cost-effective agentic memory layer for Discord bots.
|
|
5
|
+
Project-URL: Homepage, https://github.com/nulzo/icelake
|
|
6
|
+
Project-URL: Documentation, https://github.com/nulzo/icelake/tree/main/docs
|
|
7
|
+
Project-URL: Source, https://github.com/nulzo/icelake
|
|
8
|
+
Project-URL: Issues, https://github.com/nulzo/icelake/issues
|
|
9
|
+
Author-email: Nolan Gregory <nolanpgregory@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,discord,llm,memory,rag
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Communications :: Chat
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: pydantic>=2.9
|
|
25
|
+
Provides-Extra: discord
|
|
26
|
+
Requires-Dist: discord-py>=2.4; extra == 'discord'
|
|
27
|
+
Provides-Extra: local-embeddings
|
|
28
|
+
Requires-Dist: sentence-transformers>=3.0; extra == 'local-embeddings'
|
|
29
|
+
Provides-Extra: mongo
|
|
30
|
+
Requires-Dist: dnspython>=2.6; extra == 'mongo'
|
|
31
|
+
Requires-Dist: pymongo>=4.10; extra == 'mongo'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# icelake
|
|
35
|
+
|
|
36
|
+
Accurate, scalable, cost-effective **agentic memory for Discord bots** — ChatGPT/Claude-style
|
|
37
|
+
memory of your users, hardened against cross-user attribution errors, working across every
|
|
38
|
+
member of a server.
|
|
39
|
+
|
|
40
|
+
> Priority order (non-negotiable): **Accuracy → Cost → Performance**.
|
|
41
|
+
|
|
42
|
+
- **Accurate**: facts attach to *hardened* Discord user IDs via a roster-token protocol that
|
|
43
|
+
structurally prevents LLM hallucinated attribution; third-party statements ("X called Y a
|
|
44
|
+
hacker") anchor on the person they're about, with the speaker kept as attribution.
|
|
45
|
+
- **Cost-effective**: batched extraction (~1 LLM call per ~10 messages), conditional
|
|
46
|
+
reconciliation (phase-2 fires only on collisions), zero-LLM retrieval, pluggable embeddings.
|
|
47
|
+
- **Scalable**: durable lease queue safe across processes, guild-partitioned storage,
|
|
48
|
+
hub-aware bounded graph traversal, per-guild budgets with graceful degradation.
|
|
49
|
+
- **Composable**: every external dependency sits behind a Protocol port — swap storage,
|
|
50
|
+
LLM provider, embedder, or clock with one constructor argument.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install icelake # core (SQLite backend, hashing embedder)
|
|
56
|
+
pip install "icelake[discord]" # + discord.py integration
|
|
57
|
+
pip install "icelake[mongo]" # + MongoDB backend (PyMongo Async)
|
|
58
|
+
pip install "icelake[local-embeddings]"# + sentence-transformers embeddings
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Requires Python ≥ 3.12.
|
|
62
|
+
|
|
63
|
+
## Quickstart
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import asyncio
|
|
67
|
+
from datetime import UTC, datetime
|
|
68
|
+
|
|
69
|
+
from icelake import DiscordMemory, MemoryConfig, MessageEvent
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def main() -> None:
|
|
73
|
+
memory = DiscordMemory(MemoryConfig(
|
|
74
|
+
storage="sqlite:///memory.db",
|
|
75
|
+
llm="openai://$OPENROUTER_API_KEY@openrouter.ai/api/v1"
|
|
76
|
+
"?model=google/gemini-3.7-flash",
|
|
77
|
+
))
|
|
78
|
+
async with memory:
|
|
79
|
+
# 1. Feed it messages (fire-and-forget; extraction happens in background)
|
|
80
|
+
receipt = await memory.observe(MessageEvent(
|
|
81
|
+
message_id="9001", guild_id="555", channel_id="777",
|
|
82
|
+
author_id="100000000000000001",
|
|
83
|
+
content="I've been learning Rust for about a year now!",
|
|
84
|
+
created_at=datetime.now(UTC),
|
|
85
|
+
author_display_name="alice",
|
|
86
|
+
))
|
|
87
|
+
|
|
88
|
+
# 2. Build prompt context for a reply: asker + mentioned users + server.
|
|
89
|
+
ctx = await memory.prompt_context(
|
|
90
|
+
guild_id="555",
|
|
91
|
+
asker_id="100000000000000001",
|
|
92
|
+
text="what am I learning these days?",
|
|
93
|
+
mentioned_ids=("200000000000000002",), # @mentions in this message
|
|
94
|
+
)
|
|
95
|
+
print(ctx.injection_block) # labeled, budgeted, cite-tagged block
|
|
96
|
+
|
|
97
|
+
# 3. After generation, resolve echoed [mem:N] tags into jump links.
|
|
98
|
+
reply = ctx.apply_citations("You're learning Rust [mem:1]!")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
asyncio.run(main())
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Runnable, complete examples live in [`examples/`](examples/):
|
|
105
|
+
|
|
106
|
+
| File | What it demonstrates |
|
|
107
|
+
|---|---|
|
|
108
|
+
| [`examples/omni_style_bot.py`](examples/omni_style_bot.py) | **Production-shaped bot**: passive learning, ping/reply turns, `/memory` slash group, OpenRouter chat + embeddings |
|
|
109
|
+
| [`examples/ping_reply_bot.py`](examples/ping_reply_bot.py) | Classic chat bot: observe every message, reply when pinged, citations, remember/forget, nickname tracking |
|
|
110
|
+
| [`examples/relationship_queries.py`](examples/relationship_queries.py) | Zero-LLM graph demo: 8 members, pair recall, stances, 2-hop neighbors. No Discord required |
|
|
111
|
+
| [`examples/e2e_simulation.py`](examples/e2e_simulation.py) | Public-API eval: 81 hard invariants + model expectations against a scripted guild |
|
|
112
|
+
| [`examples/bench_models.py`](examples/bench_models.py) | Parallel model matrix; writes JSON + Markdown reports |
|
|
113
|
+
|
|
114
|
+
### The ping-reply turn, step by step
|
|
115
|
+
|
|
116
|
+
When `@Bot what happened between alice and bob?` arrives:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
# 1. Resolve memories for EVERYONE in the conversation in one call:
|
|
120
|
+
ctx = await memory.prompt_context(
|
|
121
|
+
guild_id=guild_id,
|
|
122
|
+
asker_id=str(message.author.id), # who is talking -> their profile
|
|
123
|
+
text=question, # query + entity hints
|
|
124
|
+
mentioned_ids=("alice_id", "bob_id"), # referenced users' profiles
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# ctx.injection_block is labeled so facts never bleed across users:
|
|
128
|
+
#
|
|
129
|
+
# [MEMORY CONTEXT]
|
|
130
|
+
#
|
|
131
|
+
# WHAT I KNOW ABOUT THE CURRENT ASKER
|
|
132
|
+
# Facts about the asker ONLY:
|
|
133
|
+
# - [mem:1] alice mains support in every ranked game she plays
|
|
134
|
+
#
|
|
135
|
+
# REFERENCED USER: bob
|
|
136
|
+
# Facts about bob ONLY. Do NOT attribute these to the asker.
|
|
137
|
+
# - [mem:2] bob was called a hacker by alice during the ranked match
|
|
138
|
+
#
|
|
139
|
+
# SERVER COMMUNITY FACTS
|
|
140
|
+
# Community-wide traits:
|
|
141
|
+
# - [mem:3] the community bonds over late night gaming sessions
|
|
142
|
+
#
|
|
143
|
+
# When you use a fact above in your reply, echo its [mem:N] tag ...
|
|
144
|
+
|
|
145
|
+
# 2. Generate your LLM reply using system_prompt + ctx.injection_block.
|
|
146
|
+
|
|
147
|
+
# 3. Resolve echoed tags into jump links (deleted-message safe):
|
|
148
|
+
reply = ctx.apply_citations(reply_text)
|
|
149
|
+
# "... bob was called out [[mem:2]](https://discord.com/channels/...)"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Cross-user questions ("what does X think about Y")
|
|
153
|
+
|
|
154
|
+
Names resolve through the alias ladder (mention ID / username / display name /
|
|
155
|
+
saved real name); ambiguity never guesses:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
resolution = await memory.identity.resolve(guild_id, "klim") # or snowflake/@mention
|
|
159
|
+
if resolution.ambiguous:
|
|
160
|
+
... # ask which member; never guess
|
|
161
|
+
|
|
162
|
+
edges = await memory.graph.between(guild_id, x_id, y_id) # typed edges
|
|
163
|
+
stances = await memory.graph.entity_stances(guild_id, "movies") # opposing stances co-presented
|
|
164
|
+
neighbors = await memory.graph.neighbors(guild_id, x_id, depth=2) # hop discovery w/ paths
|
|
165
|
+
similar = await memory.graph.similar_users(guild_id, x_id) # Jaccard over traits
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Chat-native commands (ChatGPT style)
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
command = await memory.classify_command("hey bot remember that I hate pineapple")
|
|
172
|
+
# UserMemoryCommand(action="remember", target_text="that I hate pineapple", confidence=0.9)
|
|
173
|
+
if command.action == "remember":
|
|
174
|
+
await memory.facts.remember(
|
|
175
|
+
guild_id=guild_id, subject_id=user_id,
|
|
176
|
+
text=command.target_text, actor_id=user_id,
|
|
177
|
+
)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Manual facts accept graph participation too:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from icelake.models.operations import ProposedRelation
|
|
184
|
+
|
|
185
|
+
await memory.facts.remember(
|
|
186
|
+
guild_id=guild_id, subject_id=bob_id,
|
|
187
|
+
text="carol called bob a sore loser during game night",
|
|
188
|
+
actor_id=carol_id, speaker_id=carol_id, # third-party attribution
|
|
189
|
+
relations=(ProposedRelation(
|
|
190
|
+
verb="called_out", from_token=carol_id, to_token=bob_id),),
|
|
191
|
+
)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Governance every production bot should wire
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
@bot.command()
|
|
198
|
+
async def forgetme(ctx: commands.Context):
|
|
199
|
+
await memory.admin.purge_user(str(ctx.guild.id), str(ctx.author.id),
|
|
200
|
+
dry_run=False)
|
|
201
|
+
await ctx.reply("All memories about you have been purged.", mention_author=False)
|
|
202
|
+
|
|
203
|
+
# opt-out is enforced instantly across observe AND recall:
|
|
204
|
+
await memory.admin.set_opt_out(guild_id, user_id, True)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Full usage documentation: [`docs/USAGE.md`](docs/USAGE.md) · Complete API contract:
|
|
208
|
+
[`docs/API.md`](docs/API.md) · Design: [`docs/PLAN.md`](docs/PLAN.md) · What ships next:
|
|
209
|
+
[`docs/ROADMAP.md`](docs/ROADMAP.md).
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Full bot example (omni-style)
|
|
214
|
+
|
|
215
|
+
For a production-shaped deployment — passive learning for everyone, replies only when
|
|
216
|
+
addressed, requester-first multi-person context, `/memory` slash group, governance — see
|
|
217
|
+
[`examples/omni_style_bot.py`](examples/omni_style_bot.py). It mirrors the architecture of
|
|
218
|
+
a memory-native production bot and wires **OpenRouter** for both chat (`google/gemini-3.7-flash`)
|
|
219
|
+
and embeddings (`openai/text-embedding-3-small`) so reconcile collisions and recall work on
|
|
220
|
+
paraphrases, not just exact text matches.
|
|
221
|
+
|
|
222
|
+
- **Composition root**: `build_memory()` is the single place config → adapters → client
|
|
223
|
+
get wired; everything else receives `memory`.
|
|
224
|
+
- **Learn from everyone, answer the addressed**: `on_message` observes every message
|
|
225
|
+
(bots included — they're registered as never-a-subject), then answers only when pinged
|
|
226
|
+
**or replied to**.
|
|
227
|
+
- **Requester-first turn context** (capped at 4 subjects): asker + @mentions + reply-target,
|
|
228
|
+
resolved in one `prompt_context` call with per-person labeled sections.
|
|
229
|
+
- **Coreference lines**: members known by several names get an explicit
|
|
230
|
+
*"these names all refer to ONE person"* line so the model never splits them.
|
|
231
|
+
- **`/memory` group**: `show` (profile w/ aliases), `related` (typed edges),
|
|
232
|
+
`shared` (common entities), `edit` (teach a fact), `alias` (teach a nickname).
|
|
233
|
+
- **Governance built in**: `/forgetme`, `/optout`, daily guild budgets, health.
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
class OmniStyleBot(commands.Bot):
|
|
237
|
+
def __init__(self, memory: DiscordMemory) -> None:
|
|
238
|
+
...
|
|
239
|
+
self.memory = memory
|
|
240
|
+
|
|
241
|
+
@commands.Cog.listener()
|
|
242
|
+
async def on_message(self, message: discord.Message) -> None:
|
|
243
|
+
if message.guild is None:
|
|
244
|
+
return
|
|
245
|
+
await self.memory.observe(to_event(message)) # learn; never blocks
|
|
246
|
+
if message.author.bot:
|
|
247
|
+
return
|
|
248
|
+
if await self._is_addressed(message): # ping or reply-to-us
|
|
249
|
+
await self._handle_turn(message)
|
|
250
|
+
|
|
251
|
+
async def _handle_turn(self, message: discord.Message) -> None:
|
|
252
|
+
question = strip_bot_mention(message.content, self.user.id).strip()
|
|
253
|
+
subjects = await self._collect_subjects(message) # mentions + reply target
|
|
254
|
+
ctx = await self.memory.prompt_context(
|
|
255
|
+
guild_id=guild_id,
|
|
256
|
+
asker_id=str(message.author.id),
|
|
257
|
+
text=question,
|
|
258
|
+
mentioned_ids=tuple(subjects),
|
|
259
|
+
token_budget_tokens=800,
|
|
260
|
+
)
|
|
261
|
+
system_prompt = PERSONA + "\n\n" + ctx.injection_block
|
|
262
|
+
reply = await generate(system_prompt, history, question)
|
|
263
|
+
await message.reply(ctx.apply_citations(reply)[:1900], mention_author=False)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The injection block a turn produces looks like:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
[MEMORY CONTEXT]
|
|
270
|
+
|
|
271
|
+
REFERENCED USER: bob
|
|
272
|
+
Coreference: these names all refer to ONE person: bob, bobert, bobby.
|
|
273
|
+
Facts about bob ONLY. Do NOT attribute these to the asker.
|
|
274
|
+
- [mem:1] bob was called a hacker by alice during the ranked match
|
|
275
|
+
|
|
276
|
+
SERVER COMMUNITY FACTS
|
|
277
|
+
Community-wide traits:
|
|
278
|
+
- [mem:2] the community bonds over late night ranked gaming sessions
|
|
279
|
+
|
|
280
|
+
When you use a fact above in your reply, echo its [mem:N] tag so the user
|
|
281
|
+
can see the source. Do not invent tags for facts that were not listed.
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## How it works
|
|
287
|
+
|
|
288
|
+
```mermaid
|
|
289
|
+
flowchart TB
|
|
290
|
+
observe["observe(event)"] --> queue["Pending message queue"]
|
|
291
|
+
queue --> worker["Lease worker<br/>one claim per guild + author"]
|
|
292
|
+
worker --> noise{"Noise gate"}
|
|
293
|
+
noise -->|chatter| skip["Ack - no LLM"]
|
|
294
|
+
noise -->|worth extracting| roster["Mint roster tokens<br/>p0, p1, server"]
|
|
295
|
+
roster --> extract["LLM extraction"]
|
|
296
|
+
extract --> schema{"Valid JSON schema?"}
|
|
297
|
+
schema -->|no after one repair| dead["Dead-letter the batch"]
|
|
298
|
+
schema -->|yes| gates["Quality gates"]
|
|
299
|
+
gates --> hit{"Near-duplicate collision?"}
|
|
300
|
+
hit -->|no| add["ADD fact"]
|
|
301
|
+
hit -->|yes| recon["Reconcile LLM"]
|
|
302
|
+
recon --> add
|
|
303
|
+
recon --> history["SUPERSEDE or INVALIDATE<br/>history kept"]
|
|
304
|
+
add --> store["Fact store<br/>bitemporal, one subject anchor"]
|
|
305
|
+
add --> vectors["Vector index"]
|
|
306
|
+
add --> kg["Knowledge graph<br/>incidence + typed edges"]
|
|
307
|
+
add --> digest["Profile digest<br/>every N new facts"]
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`observe` is fire-and-forget: persist + enqueue, then return. Workers claim with keyed
|
|
311
|
+
leases so multiple processes cannot double-extract the same author. Invalid extraction
|
|
312
|
+
JSON is repaired once, then dead-lettered rather than silently acked empty.
|
|
313
|
+
|
|
314
|
+
### Accuracy model
|
|
315
|
+
|
|
316
|
+
- **Roster-token protocol** — identity fields (`subject_token`, `speaker_token`, relation
|
|
317
|
+
endpoints) may only use tokens we minted for this batch (`p0`, `p1`, `server`). The
|
|
318
|
+
model never sees Discord snowflakes; unknown tokens are dropped. Stored *prose* uses
|
|
319
|
+
display names (with a detokenize pass if the model leaks `p0` into `text`). Attribution
|
|
320
|
+
is the snowflake on `subject_id`, not the name in the sentence — renames add aliases;
|
|
321
|
+
they do not move rows.
|
|
322
|
+
- **Anchoring invariant** — every fact has exactly one owner (a user or the guild).
|
|
323
|
+
Links between people/entities are additive.
|
|
324
|
+
- **Truth maintenance** — contradictions *invalidate* (bitemporal `valid_until`) or
|
|
325
|
+
*supersede* (refinement chain); nothing auto-deletes. Citations and related-user
|
|
326
|
+
links survive supersede. Full audit history per fact.
|
|
327
|
+
- **Quality gates** — refusals, LLM meta-talk, raw quotes (≥0.88 similarity), questions,
|
|
328
|
+
snowflakes in text, ephemeral media shares, and low-confidence claims are rejected by
|
|
329
|
+
pure, unit-tested gates. Registered bots are never subjects and are stripped from
|
|
330
|
+
mention links.
|
|
331
|
+
- **Identity ladder** — usernames, display names, and saved real names resolve through
|
|
332
|
+
a source-ranked alias index. Ambiguity never guesses.
|
|
333
|
+
- **Profile digests** — a paragraph summary regenerates after
|
|
334
|
+
`extraction.auto_consolidate_after_adds` new facts (default 5; not on a timer),
|
|
335
|
+
stamped with the library clock.
|
|
336
|
+
|
|
337
|
+
### Query shapes (all zero-LLM by default)
|
|
338
|
+
|
|
339
|
+
| Shape | Example | API |
|
|
340
|
+
|---|---|---|
|
|
341
|
+
| Profile | "what do you know about X" | `recall(subject_ids=(x,))` |
|
|
342
|
+
| Cross-linked | "did X call Y a hacker?" | facts touching both via link intersect |
|
|
343
|
+
| Relationship | "what does X think about Y" | `graph.between(x, y)` |
|
|
344
|
+
| Entity stance | "who likes movies?" | `graph.entity_stances("movies")` |
|
|
345
|
+
| Hop discovery | "shared connections of X" | `graph.neighbors(x, depth=2)` |
|
|
346
|
+
|
|
347
|
+
## The consumer surface
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
memory.observe(event) # → ObserveReceipt (never raises operational errors)
|
|
351
|
+
memory.observe_many(events) # bulk backfill
|
|
352
|
+
memory.flush(guild_id=...) # force-extract pending batches now
|
|
353
|
+
memory.register_bot_id(bot_user_id) # never a memory subject; stripped from mention links
|
|
354
|
+
|
|
355
|
+
memory.prompt_context(...) # → PromptContext (injection block + citations)
|
|
356
|
+
memory.recall(RecallQuery(...)) # explicit query model
|
|
357
|
+
|
|
358
|
+
memory.facts.remember/update/forget/reinforce/history/list_for_subject/search
|
|
359
|
+
memory.identity.resolve/register_alias/handle_member_rename/aliases_of
|
|
360
|
+
memory.graph.between/entity_stances/neighbors/relations_of/similar_users
|
|
361
|
+
memory.admin.set_opt_out/purge_user/export_guild/get_opt_out
|
|
362
|
+
memory.ops.run_pending/retry_dead_letters/meter_snapshot/health
|
|
363
|
+
memory.events.subscribe(BatchCompleted, handler) # typed hook events
|
|
364
|
+
memory.classify_command(text) # "remember that…" / "forget…" intent detection
|
|
365
|
+
memory.regenerate_summaries(guild_id) # force profile digests (else every N new facts)
|
|
366
|
+
memory.stats(guild_id) # GuildStats snapshot
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
Full contract with signatures and semantics: [`docs/API.md`](docs/API.md).
|
|
371
|
+
Design: [`docs/PLAN.md`](docs/PLAN.md) · Roadmap: [`docs/ROADMAP.md`](docs/ROADMAP.md).
|
|
372
|
+
|
|
373
|
+
## discord.py integration
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
# pip install icelake[discord]
|
|
377
|
+
from discord.ext import commands
|
|
378
|
+
from icelake import MemoryConfig
|
|
379
|
+
from icelake.integrations import setup_discord_memory
|
|
380
|
+
|
|
381
|
+
config = MemoryConfig(
|
|
382
|
+
storage="sqlite:///bot-memory.db",
|
|
383
|
+
llm="openai://$KEY@openrouter.ai/api/v1?model=google/gemini-3.7-flash",
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
class MyBot(commands.Bot):
|
|
387
|
+
async def setup_hook(self) -> None:
|
|
388
|
+
memory, helpers = await setup_discord_memory(self, config)
|
|
389
|
+
self.memory = memory
|
|
390
|
+
# helpers.me / helpers.remember / helpers.forget_me — bind to your own slash commands
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
The integration wires `on_message → observe`, `on_member_update → alias refresh`, and
|
|
394
|
+
`on_ready → start` + `register_bot_id`. The returned `MemoryCog` is a helper with
|
|
395
|
+
`me` / `remember` / `forget_me` methods you bind to slash commands in your bot — it is
|
|
396
|
+
not a `commands.Cog` subclass. For a full `/memory` slash group, see
|
|
397
|
+
[`examples/omni_style_bot.py`](examples/omni_style_bot.py).
|
|
398
|
+
|
|
399
|
+
## Configuration
|
|
400
|
+
|
|
401
|
+
Providers are URL strings; nested typed configs also accepted:
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
MemoryConfig(
|
|
405
|
+
storage="sqlite:///memory.db", # or mongodb://… ([mongo] extra)
|
|
406
|
+
llm="openai://$KEY@openrouter.ai/api/v1?model=…", # OpenAI-compatible endpoints
|
|
407
|
+
embeddings="hashing", # free default (see below)
|
|
408
|
+
# embeddings="local", # sentence-transformers extra
|
|
409
|
+
# embeddings="openai://$KEY@openrouter.ai/api/v1?model=openai/text-embedding-3-small",
|
|
410
|
+
# embeddings="openai://$KEY@api.openai.com/v1?model=text-embedding-3-small",
|
|
411
|
+
batching={"batch_size_messages": 10, "max_age_seconds": 300},
|
|
412
|
+
extraction={"auto_consolidate_after_adds": 5}, # 0 disables profile digests
|
|
413
|
+
budgets={"guild_daily_prompt_tokens": 200_000}, # graceful degradation ladder
|
|
414
|
+
privacy={"store_raw_messages": True},
|
|
415
|
+
workers={"enabled": True, "count": 2},
|
|
416
|
+
)
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
LLM URL query knobs that matter in production: `model`, `temperature=none` (omit sampling
|
|
420
|
+
on reasoning endpoints), `reasoning=low`, `max_tokens`, `max_tokens_key=max_completion_tokens`
|
|
421
|
+
(Azure), `structured_outputs=json_object` if the endpoint cannot enforce `json_schema`.
|
|
422
|
+
Capability mismatches raise `LlmCapabilityError` instead of silently degrading.
|
|
423
|
+
|
|
424
|
+
`postgresql://` is recognized and rejected with a clear error — there is no Postgres adapter
|
|
425
|
+
yet (and no `[postgres]` extra).
|
|
426
|
+
|
|
427
|
+
Unknown keys raise immediately — typo protection by construction.
|
|
428
|
+
|
|
429
|
+
### Embeddings (`embeddings=`)
|
|
430
|
+
|
|
431
|
+
| Provider | Spec | Best for |
|
|
432
|
+
|---|---|---|
|
|
433
|
+
| **Hashing** (default) | `"hashing"` | Tests, zero-dependency demos, deterministic CI |
|
|
434
|
+
| **Hosted** | `"openai://$KEY@openrouter.ai/api/v1?model=openai/text-embedding-3-small"` | Production bots already on OpenRouter/OpenAI |
|
|
435
|
+
| **Local** | `"local"` | Air-gapped or no embedding API cost (`pip install icelake[local-embeddings]`) |
|
|
436
|
+
|
|
437
|
+
Embeddings power **semantic recall**, **reconcile collision detection** (paraphrase → reinforce
|
|
438
|
+
instead of duplicate ADD), and consolidation sanity checks. Cosine similarity is compared against
|
|
439
|
+
`extraction.reconcile_collision_threshold` (default `0.85`).
|
|
440
|
+
|
|
441
|
+
#### Hashing embedder limitations
|
|
442
|
+
|
|
443
|
+
The default hashing embedder is a signed feature-hash over word/char n-grams — **not** a neural
|
|
444
|
+
model. It is fast, free, and reproducible, but:
|
|
445
|
+
|
|
446
|
+
- **Paraphrases do not cluster.** "Loves coding in Go" and "Enjoys programming in Go" often score
|
|
447
|
+
below the reconcile threshold, so the pipeline treats them as unrelated facts and you can end up
|
|
448
|
+
with many near-duplicate memories per user.
|
|
449
|
+
- **Recall is lexical-ish.** Vector search channels rank by token overlap more than meaning;
|
|
450
|
+
semantic recall quality is noticeably weaker than with a real embedding model.
|
|
451
|
+
- **Reinforcement depends on collisions.** Ingest reinforce/update/noop only triggers semantic
|
|
452
|
+
collision when cosine similarity clears the threshold; hashing misses most real-world re-statements.
|
|
453
|
+
|
|
454
|
+
Use **hosted** (OpenRouter/OpenAI) or **local** embeddings for any deployment where users repeat
|
|
455
|
+
the same preference in different words — including the omni-style example, which sets
|
|
456
|
+
`embeddings=EMBEDDINGS_URL` accordingly. Switching embedders invalidates existing vectors; re-embed
|
|
457
|
+
or start fresh on dev databases when changing provider.
|
|
458
|
+
|
|
459
|
+
## Deployment topologies
|
|
460
|
+
|
|
461
|
+
| Topology | Config |
|
|
462
|
+
|---|---|
|
|
463
|
+
| Single process (small bots) | defaults — workers run as background tasks |
|
|
464
|
+
| Split bot + worker | bot: `workers={"enabled": False}`; worker process: same storage, call `await memory.ops.run_pending()` in a loop |
|
|
465
|
+
| Cron-style | workers disabled; invoke `ops.run_pending` from your scheduler |
|
|
466
|
+
| Multi-process scale-out | any number of processes share one database — keyed leases make workers cooperative |
|
|
467
|
+
|
|
468
|
+
## Extending (ports)
|
|
469
|
+
|
|
470
|
+
Every dependency is a Protocol you can replace at construction:
|
|
471
|
+
|
|
472
|
+
```python
|
|
473
|
+
memory = DiscordMemory(
|
|
474
|
+
config,
|
|
475
|
+
store=MyPostgresStore(), # implements MemoryStore (+ optional .queue/.vectors)
|
|
476
|
+
llm=MyLLM(), # implements ChatLLM (OpenAI-compatible shape)
|
|
477
|
+
embedder=MyEmbedder(), # implements Embedder
|
|
478
|
+
clock=FakeClock(...), # deterministic time in tests
|
|
479
|
+
)
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
New backends must pass the executable conformance suite
|
|
483
|
+
(`tests/integration/test_store_conformance.py`) — the port contract is literally a test.
|
|
484
|
+
|
|
485
|
+
## Development
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
uv sync --group dev
|
|
489
|
+
uv run pytest tests/ -q --cov=icelake # ≥90% coverage enforced
|
|
490
|
+
uv run ruff check src tests && uv run ruff format --check src tests
|
|
491
|
+
uv run mypy # strict mode
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
## Status & limitations (v0.1)
|
|
495
|
+
|
|
496
|
+
- Storage backends shipped: **SQLite** (default), **MongoDB** (`[mongo]` extra), and in-memory
|
|
497
|
+
(tests). A Postgres/pgvector adapter is planned — `postgresql://` fails loudly today.
|
|
498
|
+
- Default **hashing** embeddings are not suitable for production dedup/recall — see
|
|
499
|
+
[Embeddings](#embeddings-embeddings) above.
|
|
500
|
+
- Invalid extraction JSON is repaired once, then **dead-lettered** (not silently stored as
|
|
501
|
+
empty). Re-drive with `ops.retry_dead_letters`.
|
|
502
|
+
- Caps and TTL prune weakest-first (manual/CORE last). Budgets meter per-process; cross-process
|
|
503
|
+
budget accounting needs store-backed counters.
|
|
504
|
+
- Server-scope ("community") batches read the recent-message window; watermarking across
|
|
505
|
+
restarts is best-effort.
|
|
506
|
+
- `similar_users` uses capped Jaccard over entity adjacency (no Louvain/PPR by design).
|
|
507
|
+
|
|
508
|
+
See [`docs/ROADMAP.md`](docs/ROADMAP.md) for sequenced next work and
|
|
509
|
+
[`docs/PLAN.md`](docs/PLAN.md) for design rationale.
|
|
510
|
+
|
|
511
|
+
## License
|
|
512
|
+
|
|
513
|
+
MIT
|