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.
Files changed (157) hide show
  1. icelake-0.1.0/.github/workflows/publish.yml +43 -0
  2. icelake-0.1.0/.gitignore +22 -0
  3. icelake-0.1.0/.python-version +1 -0
  4. icelake-0.1.0/AGENTS.md +76 -0
  5. icelake-0.1.0/LICENSE +21 -0
  6. icelake-0.1.0/PKG-INFO +513 -0
  7. icelake-0.1.0/README.md +480 -0
  8. icelake-0.1.0/docs/API.md +599 -0
  9. icelake-0.1.0/docs/PLAN.md +740 -0
  10. icelake-0.1.0/docs/RESEARCH.md +554 -0
  11. icelake-0.1.0/docs/ROADMAP.md +246 -0
  12. icelake-0.1.0/docs/USAGE.md +410 -0
  13. icelake-0.1.0/evals/golden/adversarial_injection.yaml +8 -0
  14. icelake-0.1.0/evals/golden/ambiguous_identity.yaml +14 -0
  15. icelake-0.1.0/evals/golden/bare_link_skip.yaml +7 -0
  16. icelake-0.1.0/evals/golden/contradiction_supersede.yaml +14 -0
  17. icelake-0.1.0/evals/golden/cross_user_isolation.yaml +21 -0
  18. icelake-0.1.0/evals/golden/durable_preference.yaml +14 -0
  19. icelake-0.1.0/evals/golden/mention_extraction.yaml +18 -0
  20. icelake-0.1.0/evals/golden/reinforce_not_duplicate.yaml +15 -0
  21. icelake-0.1.0/evals/golden/server_culture.yaml +14 -0
  22. icelake-0.1.0/evals/golden/snowflake_poisoning.yaml +7 -0
  23. icelake-0.1.0/evals/golden/third_party_attribution.yaml +18 -0
  24. icelake-0.1.0/evals/golden/transient_state_skip.yaml +9 -0
  25. icelake-0.1.0/evals/golden/write_create_biographical.yaml +14 -0
  26. icelake-0.1.0/evals/golden/write_skip_noise.yaml +9 -0
  27. icelake-0.1.0/evals/golden/write_skip_questions.yaml +9 -0
  28. icelake-0.1.0/evals/golden_runner.py +173 -0
  29. icelake-0.1.0/examples/bench_models.py +176 -0
  30. icelake-0.1.0/examples/e2e_simulation.py +1410 -0
  31. icelake-0.1.0/examples/omni_style_bot.py +380 -0
  32. icelake-0.1.0/examples/ping_reply_bot.py +275 -0
  33. icelake-0.1.0/examples/relationship_queries.py +266 -0
  34. icelake-0.1.0/pyproject.toml +88 -0
  35. icelake-0.1.0/src/icelake/__init__.py +172 -0
  36. icelake-0.1.0/src/icelake/_json.py +125 -0
  37. icelake-0.1.0/src/icelake/adapters/__init__.py +1 -0
  38. icelake-0.1.0/src/icelake/adapters/embedders/__init__.py +126 -0
  39. icelake-0.1.0/src/icelake/adapters/embedders/cached.py +74 -0
  40. icelake-0.1.0/src/icelake/adapters/embedders/local.py +51 -0
  41. icelake-0.1.0/src/icelake/adapters/in_memory/__init__.py +10 -0
  42. icelake-0.1.0/src/icelake/adapters/in_memory/queue.py +245 -0
  43. icelake-0.1.0/src/icelake/adapters/in_memory/store.py +819 -0
  44. icelake-0.1.0/src/icelake/adapters/in_memory/vectors.py +58 -0
  45. icelake-0.1.0/src/icelake/adapters/llm_cache.py +51 -0
  46. icelake-0.1.0/src/icelake/adapters/llm_openai_compat.py +235 -0
  47. icelake-0.1.0/src/icelake/adapters/llm_openrouter.py +41 -0
  48. icelake-0.1.0/src/icelake/adapters/meter.py +163 -0
  49. icelake-0.1.0/src/icelake/adapters/mongo/__init__.py +1108 -0
  50. icelake-0.1.0/src/icelake/adapters/mongo/mapping.py +269 -0
  51. icelake-0.1.0/src/icelake/adapters/mongo/queue.py +300 -0
  52. icelake-0.1.0/src/icelake/adapters/mongo/vectors.py +102 -0
  53. icelake-0.1.0/src/icelake/adapters/sqlite/connection.py +311 -0
  54. icelake-0.1.0/src/icelake/adapters/sqlite/llm_cache.py +40 -0
  55. icelake-0.1.0/src/icelake/adapters/sqlite/queue.py +315 -0
  56. icelake-0.1.0/src/icelake/adapters/sqlite/store.py +68 -0
  57. icelake-0.1.0/src/icelake/adapters/sqlite/store_facts.py +827 -0
  58. icelake-0.1.0/src/icelake/adapters/sqlite/store_graph.py +465 -0
  59. icelake-0.1.0/src/icelake/adapters/sqlite/vectors.py +121 -0
  60. icelake-0.1.0/src/icelake/api/classify.py +85 -0
  61. icelake-0.1.0/src/icelake/api/client.py +809 -0
  62. icelake-0.1.0/src/icelake/api/events.py +77 -0
  63. icelake-0.1.0/src/icelake/api/facts_api.py +350 -0
  64. icelake-0.1.0/src/icelake/api/groups.py +274 -0
  65. icelake-0.1.0/src/icelake/config.py +321 -0
  66. icelake-0.1.0/src/icelake/consolidation/service.py +162 -0
  67. icelake-0.1.0/src/icelake/errors.py +72 -0
  68. icelake-0.1.0/src/icelake/graph/__init__.py +5 -0
  69. icelake-0.1.0/src/icelake/graph/relations.py +88 -0
  70. icelake-0.1.0/src/icelake/graph/traversal.py +102 -0
  71. icelake-0.1.0/src/icelake/graph/writes.py +213 -0
  72. icelake-0.1.0/src/icelake/identity/__init__.py +6 -0
  73. icelake-0.1.0/src/icelake/identity/aliases.py +162 -0
  74. icelake-0.1.0/src/icelake/identity/guards.py +63 -0
  75. icelake-0.1.0/src/icelake/identity/resolver.py +96 -0
  76. icelake-0.1.0/src/icelake/ids.py +27 -0
  77. icelake-0.1.0/src/icelake/ingest/__init__.py +5 -0
  78. icelake-0.1.0/src/icelake/ingest/context_builder.py +60 -0
  79. icelake-0.1.0/src/icelake/ingest/executor.py +292 -0
  80. icelake-0.1.0/src/icelake/ingest/extraction.py +149 -0
  81. icelake-0.1.0/src/icelake/ingest/gates.py +186 -0
  82. icelake-0.1.0/src/icelake/ingest/pipeline.py +684 -0
  83. icelake-0.1.0/src/icelake/ingest/reconcile.py +269 -0
  84. icelake-0.1.0/src/icelake/ingest/roster.py +98 -0
  85. icelake-0.1.0/src/icelake/integrations/__init__.py +5 -0
  86. icelake-0.1.0/src/icelake/integrations/discord_py.py +166 -0
  87. icelake-0.1.0/src/icelake/lifecycle/__init__.py +13 -0
  88. icelake-0.1.0/src/icelake/lifecycle/maintenance.py +92 -0
  89. icelake-0.1.0/src/icelake/lifecycle/prune.py +50 -0
  90. icelake-0.1.0/src/icelake/lifecycle/strength.py +54 -0
  91. icelake-0.1.0/src/icelake/lifecycle/tiers.py +86 -0
  92. icelake-0.1.0/src/icelake/models/__init__.py +117 -0
  93. icelake-0.1.0/src/icelake/models/admin.py +96 -0
  94. icelake-0.1.0/src/icelake/models/common.py +46 -0
  95. icelake-0.1.0/src/icelake/models/events.py +137 -0
  96. icelake-0.1.0/src/icelake/models/facts.py +166 -0
  97. icelake-0.1.0/src/icelake/models/graph.py +111 -0
  98. icelake-0.1.0/src/icelake/models/identity.py +75 -0
  99. icelake-0.1.0/src/icelake/models/operations.py +119 -0
  100. icelake-0.1.0/src/icelake/models/retrieval.py +175 -0
  101. icelake-0.1.0/src/icelake/ports/__init__.py +39 -0
  102. icelake-0.1.0/src/icelake/ports/clock.py +51 -0
  103. icelake-0.1.0/src/icelake/ports/llm.py +103 -0
  104. icelake-0.1.0/src/icelake/ports/queue.py +133 -0
  105. icelake-0.1.0/src/icelake/ports/store.py +351 -0
  106. icelake-0.1.0/src/icelake/ports/vectors.py +64 -0
  107. icelake-0.1.0/src/icelake/prompts/__init__.py +1 -0
  108. icelake-0.1.0/src/icelake/prompts/extraction.py +105 -0
  109. icelake-0.1.0/src/icelake/py.typed +0 -0
  110. icelake-0.1.0/src/icelake/retrieval/__init__.py +5 -0
  111. icelake-0.1.0/src/icelake/retrieval/channels.py +220 -0
  112. icelake-0.1.0/src/icelake/retrieval/injection.py +171 -0
  113. icelake-0.1.0/src/icelake/retrieval/service.py +388 -0
  114. icelake-0.1.0/src/icelake/scoring/__init__.py +5 -0
  115. icelake-0.1.0/src/icelake/scoring/fusion.py +105 -0
  116. icelake-0.1.0/src/icelake/structured.py +75 -0
  117. icelake-0.1.0/tests/__init__.py +0 -0
  118. icelake-0.1.0/tests/conftest.py +160 -0
  119. icelake-0.1.0/tests/integration/__init__.py +0 -0
  120. icelake-0.1.0/tests/integration/test_adversarial.py +350 -0
  121. icelake-0.1.0/tests/integration/test_agentic_e2e.py +464 -0
  122. icelake-0.1.0/tests/integration/test_api_groups.py +139 -0
  123. icelake-0.1.0/tests/integration/test_coverage_backends.py +308 -0
  124. icelake-0.1.0/tests/integration/test_decay_loop.py +109 -0
  125. icelake-0.1.0/tests/integration/test_discord_integration.py +152 -0
  126. icelake-0.1.0/tests/integration/test_examples.py +189 -0
  127. icelake-0.1.0/tests/integration/test_golden_evals.py +18 -0
  128. icelake-0.1.0/tests/integration/test_graph_recall.py +124 -0
  129. icelake-0.1.0/tests/integration/test_hardening_round.py +614 -0
  130. icelake-0.1.0/tests/integration/test_import_export.py +57 -0
  131. icelake-0.1.0/tests/integration/test_mongo_live.py +214 -0
  132. icelake-0.1.0/tests/integration/test_mongo_vectors.py +74 -0
  133. icelake-0.1.0/tests/integration/test_phase0.py +262 -0
  134. icelake-0.1.0/tests/integration/test_pipeline_e2e.py +802 -0
  135. icelake-0.1.0/tests/integration/test_queue_conformance.py +126 -0
  136. icelake-0.1.0/tests/integration/test_recall_prompt.py +272 -0
  137. icelake-0.1.0/tests/integration/test_review_round.py +426 -0
  138. icelake-0.1.0/tests/integration/test_store_conformance.py +435 -0
  139. icelake-0.1.0/tests/unit/__init__.py +0 -0
  140. icelake-0.1.0/tests/unit/test_adapters.py +456 -0
  141. icelake-0.1.0/tests/unit/test_adapters_edge.py +226 -0
  142. icelake-0.1.0/tests/unit/test_config.py +177 -0
  143. icelake-0.1.0/tests/unit/test_coverage_completion.py +407 -0
  144. icelake-0.1.0/tests/unit/test_coverage_final.py +435 -0
  145. icelake-0.1.0/tests/unit/test_executor.py +282 -0
  146. icelake-0.1.0/tests/unit/test_extraction.py +294 -0
  147. icelake-0.1.0/tests/unit/test_gates.py +113 -0
  148. icelake-0.1.0/tests/unit/test_identity.py +129 -0
  149. icelake-0.1.0/tests/unit/test_injection_roster.py +225 -0
  150. icelake-0.1.0/tests/unit/test_lifecycle.py +211 -0
  151. icelake-0.1.0/tests/unit/test_models.py +141 -0
  152. icelake-0.1.0/tests/unit/test_mongo_adapter.py +176 -0
  153. icelake-0.1.0/tests/unit/test_reconcile.py +253 -0
  154. icelake-0.1.0/tests/unit/test_scoring_graph.py +189 -0
  155. icelake-0.1.0/tests/unit/test_structured.py +56 -0
  156. icelake-0.1.0/tests/unit/test_structured_outputs.py +182 -0
  157. 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
@@ -0,0 +1,22 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ *.db
13
+ *.db-shm
14
+ *.db-wal
15
+
16
+ .coverage
17
+
18
+ .bench/
19
+
20
+ .env
21
+
22
+ bench_runs/
@@ -0,0 +1 @@
1
+ 3.14
@@ -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