icelake 0.1.0__py3-none-any.whl

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 (86) hide show
  1. icelake/__init__.py +172 -0
  2. icelake/_json.py +125 -0
  3. icelake/adapters/__init__.py +1 -0
  4. icelake/adapters/embedders/__init__.py +126 -0
  5. icelake/adapters/embedders/cached.py +74 -0
  6. icelake/adapters/embedders/local.py +51 -0
  7. icelake/adapters/in_memory/__init__.py +10 -0
  8. icelake/adapters/in_memory/queue.py +245 -0
  9. icelake/adapters/in_memory/store.py +819 -0
  10. icelake/adapters/in_memory/vectors.py +58 -0
  11. icelake/adapters/llm_cache.py +51 -0
  12. icelake/adapters/llm_openai_compat.py +235 -0
  13. icelake/adapters/llm_openrouter.py +41 -0
  14. icelake/adapters/meter.py +163 -0
  15. icelake/adapters/mongo/__init__.py +1108 -0
  16. icelake/adapters/mongo/mapping.py +269 -0
  17. icelake/adapters/mongo/queue.py +300 -0
  18. icelake/adapters/mongo/vectors.py +102 -0
  19. icelake/adapters/sqlite/connection.py +311 -0
  20. icelake/adapters/sqlite/llm_cache.py +40 -0
  21. icelake/adapters/sqlite/queue.py +315 -0
  22. icelake/adapters/sqlite/store.py +68 -0
  23. icelake/adapters/sqlite/store_facts.py +827 -0
  24. icelake/adapters/sqlite/store_graph.py +465 -0
  25. icelake/adapters/sqlite/vectors.py +121 -0
  26. icelake/api/classify.py +85 -0
  27. icelake/api/client.py +809 -0
  28. icelake/api/events.py +77 -0
  29. icelake/api/facts_api.py +350 -0
  30. icelake/api/groups.py +274 -0
  31. icelake/config.py +321 -0
  32. icelake/consolidation/service.py +162 -0
  33. icelake/errors.py +72 -0
  34. icelake/graph/__init__.py +5 -0
  35. icelake/graph/relations.py +88 -0
  36. icelake/graph/traversal.py +102 -0
  37. icelake/graph/writes.py +213 -0
  38. icelake/identity/__init__.py +6 -0
  39. icelake/identity/aliases.py +162 -0
  40. icelake/identity/guards.py +63 -0
  41. icelake/identity/resolver.py +96 -0
  42. icelake/ids.py +27 -0
  43. icelake/ingest/__init__.py +5 -0
  44. icelake/ingest/context_builder.py +60 -0
  45. icelake/ingest/executor.py +292 -0
  46. icelake/ingest/extraction.py +149 -0
  47. icelake/ingest/gates.py +186 -0
  48. icelake/ingest/pipeline.py +684 -0
  49. icelake/ingest/reconcile.py +269 -0
  50. icelake/ingest/roster.py +98 -0
  51. icelake/integrations/__init__.py +5 -0
  52. icelake/integrations/discord_py.py +166 -0
  53. icelake/lifecycle/__init__.py +13 -0
  54. icelake/lifecycle/maintenance.py +92 -0
  55. icelake/lifecycle/prune.py +50 -0
  56. icelake/lifecycle/strength.py +54 -0
  57. icelake/lifecycle/tiers.py +86 -0
  58. icelake/models/__init__.py +117 -0
  59. icelake/models/admin.py +96 -0
  60. icelake/models/common.py +46 -0
  61. icelake/models/events.py +137 -0
  62. icelake/models/facts.py +166 -0
  63. icelake/models/graph.py +111 -0
  64. icelake/models/identity.py +75 -0
  65. icelake/models/operations.py +119 -0
  66. icelake/models/retrieval.py +175 -0
  67. icelake/ports/__init__.py +39 -0
  68. icelake/ports/clock.py +51 -0
  69. icelake/ports/llm.py +103 -0
  70. icelake/ports/queue.py +133 -0
  71. icelake/ports/store.py +351 -0
  72. icelake/ports/vectors.py +64 -0
  73. icelake/prompts/__init__.py +1 -0
  74. icelake/prompts/extraction.py +105 -0
  75. icelake/py.typed +0 -0
  76. icelake/retrieval/__init__.py +5 -0
  77. icelake/retrieval/channels.py +220 -0
  78. icelake/retrieval/injection.py +171 -0
  79. icelake/retrieval/service.py +388 -0
  80. icelake/scoring/__init__.py +5 -0
  81. icelake/scoring/fusion.py +105 -0
  82. icelake/structured.py +75 -0
  83. icelake-0.1.0.dist-info/METADATA +513 -0
  84. icelake-0.1.0.dist-info/RECORD +86 -0
  85. icelake-0.1.0.dist-info/WHEEL +4 -0
  86. icelake-0.1.0.dist-info/licenses/LICENSE +21 -0
icelake/__init__.py ADDED
@@ -0,0 +1,172 @@
1
+ """icelake: accurate, scalable agentic memory for Discord bots.
2
+
3
+ Quickstart::
4
+
5
+ from icelake import DiscordMemory, MemoryConfig, MessageEvent
6
+
7
+ memory = DiscordMemory(MemoryConfig(
8
+ storage="sqlite:///memory.db",
9
+ llm="openai://$KEY@openrouter.ai/api/v1?model=google/gemini-2.5-flash",
10
+ ))
11
+ async with memory:
12
+ await memory.observe(event)
13
+ ctx = await memory.prompt_context(guild_id=g, asker_id=u, text=msg)
14
+ system_prompt += ctx.injection_block
15
+
16
+ See docs/API.md for the complete consumer contract and docs/PLAN.md for design.
17
+ """
18
+
19
+ from icelake.api.classify import CommandAction, UserMemoryCommand
20
+ from icelake.api.client import DiscordMemory
21
+ from icelake.config import (
22
+ BatchingConfig,
23
+ BudgetsConfig,
24
+ EmbeddingsConfig,
25
+ ExtractionConfig,
26
+ LifecycleConfig,
27
+ LlmConfig,
28
+ MemoryConfig,
29
+ MeterConfig,
30
+ ObserveConfig,
31
+ PrivacyConfig,
32
+ RetrievalConfig,
33
+ StorageConfig,
34
+ WorkersConfig,
35
+ )
36
+ from icelake.errors import (
37
+ BudgetExceededError,
38
+ ConfigError,
39
+ DiscordMemoryError,
40
+ FactNotFoundError,
41
+ IdentityAmbiguousError,
42
+ LlmCapabilityError,
43
+ SchemaValidationError,
44
+ StorageUnavailableError,
45
+ StructuredOutputError,
46
+ SubjectNotAllowedError,
47
+ WorkerNotRunningError,
48
+ )
49
+ from icelake.models import (
50
+ CHANNELS_ALL,
51
+ CHANNELS_DEFAULT,
52
+ CHANNELS_DISCOVERY,
53
+ Attribution,
54
+ AttributionType,
55
+ ChannelName,
56
+ Citation,
57
+ EdgeKind,
58
+ EntityRecord,
59
+ FactCategory,
60
+ FactHistoryEntry,
61
+ FactRecord,
62
+ GuildStats,
63
+ HealthReport,
64
+ IgnoreReason,
65
+ MemoryExport,
66
+ MemoryTier,
67
+ MessageEvent,
68
+ NeighborInfo,
69
+ NodeType,
70
+ ObserveReceipt,
71
+ ObserveStatus,
72
+ Polarity,
73
+ PromptContext,
74
+ PurgeReport,
75
+ RecallQuery,
76
+ RecallResult,
77
+ RecallWarning,
78
+ RejectReason,
79
+ RelationEdge,
80
+ Resolution,
81
+ ResolvedCandidate,
82
+ Scope,
83
+ ScoreComponents,
84
+ ScoredFact,
85
+ SourceRef,
86
+ StanceSummary,
87
+ channels,
88
+ )
89
+ from icelake.models.events import (
90
+ BatchCompleted,
91
+ BudgetWarning,
92
+ ComponentDegraded,
93
+ ExtractionFailed,
94
+ FactCommitted,
95
+ FactSupersededEvent,
96
+ )
97
+
98
+ __version__ = "0.1.0"
99
+
100
+ __all__ = [
101
+ "CHANNELS_ALL",
102
+ "CHANNELS_DEFAULT",
103
+ "CHANNELS_DISCOVERY",
104
+ "Attribution",
105
+ "AttributionType",
106
+ "BatchCompleted",
107
+ "BatchingConfig",
108
+ "BudgetExceededError",
109
+ "BudgetWarning",
110
+ "BudgetsConfig",
111
+ "ChannelName",
112
+ "Citation",
113
+ "CommandAction",
114
+ "ComponentDegraded",
115
+ "ConfigError",
116
+ "DiscordMemory",
117
+ "DiscordMemoryError",
118
+ "EdgeKind",
119
+ "EmbeddingsConfig",
120
+ "EntityRecord",
121
+ "ExtractionConfig",
122
+ "ExtractionFailed",
123
+ "FactCategory",
124
+ "FactCommitted",
125
+ "FactHistoryEntry",
126
+ "FactNotFoundError",
127
+ "FactRecord",
128
+ "FactSupersededEvent",
129
+ "GuildStats",
130
+ "HealthReport",
131
+ "IdentityAmbiguousError",
132
+ "IgnoreReason",
133
+ "LifecycleConfig",
134
+ "LlmCapabilityError",
135
+ "LlmConfig",
136
+ "MemoryConfig",
137
+ "MemoryExport",
138
+ "MemoryTier",
139
+ "MessageEvent",
140
+ "MeterConfig",
141
+ "NeighborInfo",
142
+ "NodeType",
143
+ "ObserveConfig",
144
+ "ObserveReceipt",
145
+ "ObserveStatus",
146
+ "Polarity",
147
+ "PrivacyConfig",
148
+ "PromptContext",
149
+ "PurgeReport",
150
+ "RecallQuery",
151
+ "RecallResult",
152
+ "RecallWarning",
153
+ "RejectReason",
154
+ "RelationEdge",
155
+ "Resolution",
156
+ "ResolvedCandidate",
157
+ "RetrievalConfig",
158
+ "SchemaValidationError",
159
+ "Scope",
160
+ "ScoreComponents",
161
+ "ScoredFact",
162
+ "SourceRef",
163
+ "StanceSummary",
164
+ "StorageConfig",
165
+ "StorageUnavailableError",
166
+ "StructuredOutputError",
167
+ "SubjectNotAllowedError",
168
+ "UserMemoryCommand",
169
+ "WorkerNotRunningError",
170
+ "WorkersConfig",
171
+ "channels",
172
+ ]
icelake/_json.py ADDED
@@ -0,0 +1,125 @@
1
+ """LLM JSON extraction with staged repair (ported from bot utils/llm_json.py).
2
+
3
+ Ladder: fence strip -> balanced-object scan (string/escape-aware) -> truncation
4
+ repair (closes open strings + bracket stack) -> candidate slicing. A truncated
5
+ extraction response should lose nothing recoverable.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Final
12
+
13
+ _FENCES: Final = (("```json", "```"), ("```JSON", "```"), ("```", "```"))
14
+
15
+
16
+ def _strip_fence(text: str) -> str:
17
+ stripped = text.strip()
18
+ for opening, closing in _FENCES:
19
+ if stripped.startswith(opening):
20
+ inner = stripped[len(opening) :]
21
+ end = inner.rfind(closing)
22
+ return (inner[:end] if end != -1 else inner).strip()
23
+ return stripped
24
+
25
+
26
+ def _scan_balanced(text: str, start: int) -> str | None:
27
+ """Extract one balanced JSON object starting at ``text[start] == '{'``."""
28
+ depth = 0
29
+ in_string = False
30
+ escaped = False
31
+ for index in range(start, len(text)):
32
+ char = text[index]
33
+ if in_string:
34
+ if escaped:
35
+ escaped = False
36
+ elif char == "\\":
37
+ escaped = True
38
+ elif char == '"':
39
+ in_string = False
40
+ continue
41
+ if char == '"':
42
+ in_string = True
43
+ elif char == "{":
44
+ depth += 1
45
+ elif char == "}":
46
+ depth -= 1
47
+ if depth == 0:
48
+ return text[start : index + 1]
49
+ return None
50
+
51
+
52
+ def _close_open_constructs(candidate: str) -> str:
53
+ """Repair truncated JSON: close an open string literal, then brackets."""
54
+ in_string = False
55
+ escaped = False
56
+ stack: list[str] = []
57
+ for char in candidate:
58
+ if in_string:
59
+ if escaped:
60
+ escaped = False
61
+ elif char == "\\":
62
+ escaped = True
63
+ elif char == '"':
64
+ in_string = False
65
+ continue
66
+ if char == '"':
67
+ in_string = True
68
+ elif char == "{":
69
+ stack.append("}")
70
+ elif char == "[":
71
+ stack.append("]")
72
+ elif char in {"}", "]"} and stack and stack[-1] == char:
73
+ stack.pop()
74
+ repaired = candidate
75
+ if escaped:
76
+ repaired += '"'
77
+ if in_string:
78
+ repaired += '"'
79
+ while stack:
80
+ repaired += stack.pop()
81
+ return repaired
82
+
83
+
84
+ def parse_json_object(text: str) -> dict[str, object]:
85
+ """Parse the first JSON object in ``text`` with staged repair.
86
+
87
+ Raises ``ValueError`` only when no object can be recovered at all.
88
+ """
89
+ cleaned = _strip_fence(text)
90
+
91
+ direct_start = cleaned.find("{")
92
+ direct_end = cleaned.rfind("}")
93
+ if direct_start != -1 and direct_end > direct_start:
94
+ try:
95
+ parsed = json.loads(cleaned[direct_start : direct_end + 1])
96
+ if isinstance(parsed, dict):
97
+ return parsed
98
+ except json.JSONDecodeError:
99
+ pass
100
+
101
+ scanner_start = max(direct_start, 0)
102
+ while scanner_start != -1:
103
+ balanced = _scan_balanced(cleaned, scanner_start)
104
+ if balanced is not None:
105
+ try:
106
+ parsed = json.loads(balanced)
107
+ if isinstance(parsed, dict):
108
+ return parsed
109
+ except json.JSONDecodeError:
110
+ pass
111
+ scanner_start = cleaned.find("{", scanner_start + 1)
112
+
113
+ if direct_start != -1:
114
+ truncated = _close_open_constructs(cleaned[direct_start:])
115
+ try:
116
+ parsed = json.loads(truncated)
117
+ if isinstance(parsed, dict):
118
+ return parsed
119
+ except json.JSONDecodeError:
120
+ pass
121
+
122
+ raise ValueError("no recoverable JSON object in response")
123
+
124
+
125
+ __all__ = ["parse_json_object"]
@@ -0,0 +1 @@
1
+ """Adapter packages. Only adapters import vendor SDKs (import-linter enforced)."""
@@ -0,0 +1,126 @@
1
+ """Embedding adapters: deterministic hashing (default) and OpenAI-compatible API.
2
+
3
+ The hashing embedder is the zero-dependency default: signed feature-hashing over word
4
+ and char n-grams, L2-normalized. Deterministic, fast, free — good lexical-ish
5
+ semantics for small deployments and perfectly reproducible in tests. Swap to a real
6
+ model via config without touching any other code.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import math
13
+ from collections.abc import Sequence
14
+ from typing import TYPE_CHECKING
15
+
16
+ import httpx
17
+
18
+ from icelake.config import EmbeddingsConfig, EmbeddingsProvider
19
+ from icelake.errors import ConfigError
20
+
21
+ if TYPE_CHECKING:
22
+ from icelake.ports.llm import Embedder
23
+
24
+
25
+ class HashingEmbedder:
26
+ """Signed feature-hashing embedder; stable across processes."""
27
+
28
+ def __init__(self, dimensions: int = 256) -> None:
29
+ if dimensions < 32:
30
+ raise ConfigError("hashing embedder needs at least 32 dimensions")
31
+ self._dimensions = dimensions
32
+
33
+ @property
34
+ def dimensions(self) -> int:
35
+ return self._dimensions
36
+
37
+ async def embed(self, texts: Sequence[str]) -> tuple[tuple[float, ...], ...]:
38
+ return tuple(self._embed_one(text) for text in texts)
39
+
40
+ def _embed_one(self, text: str) -> tuple[float, ...]:
41
+ vector = [0.0] * self._dimensions
42
+ lowered = " ".join(text.lower().split())
43
+ tokens = lowered.split()
44
+ for token in tokens:
45
+ self._add_feature(vector, token, 1.0)
46
+ for i in range(len(tokens) - 1):
47
+ bigram = f"{tokens[i]}_{tokens[i + 1]}"
48
+ self._add_feature(vector, bigram, 0.7)
49
+ for i in range(max(0, len(lowered) - 3)):
50
+ self._add_feature(vector, "c:" + lowered[i : i + 4], 0.25)
51
+ norm = math.sqrt(sum(v * v for v in vector))
52
+ if norm == 0.0:
53
+ return tuple(vector)
54
+ return tuple(round(v / norm, 8) for v in vector)
55
+
56
+ def _add_feature(self, vector: list[float], feature: str, weight: float) -> None:
57
+ digest = hashlib.md5(feature.encode("utf-8")).digest()
58
+ index = int.from_bytes(digest[:4], "little") % self._dimensions
59
+ sign = 1.0 if digest[4] % 2 == 0 else -1.0
60
+ vector[index] += sign * weight
61
+
62
+
63
+ class OpenAICompatEmbedder:
64
+ """OpenAI ``/embeddings``-compatible adapter (OpenRouter/OpenAI/vLLM/Ollama)."""
65
+
66
+ def __init__(self, config: EmbeddingsConfig) -> None:
67
+ if not config.base_url or not config.model:
68
+ raise ConfigError("openai embeddings require base_url and model")
69
+ self._config = config
70
+ self._client = httpx.AsyncClient(timeout=30.0)
71
+
72
+ @property
73
+ def dimensions(self) -> int:
74
+ return self._config.dimensions
75
+
76
+ async def embed(self, texts: Sequence[str]) -> tuple[tuple[float, ...], ...]:
77
+ out: list[tuple[float, ...]] = []
78
+ batch_size = 64
79
+ for start in range(0, len(texts), batch_size):
80
+ chunk = texts[start : start + batch_size]
81
+ response = await self._client.post(
82
+ f"{self._config.base_url}/embeddings",
83
+ headers=self._headers(),
84
+ json={"model": self._config.model, "input": list(chunk)},
85
+ )
86
+ response.raise_for_status()
87
+ payload = response.json()
88
+ data = sorted(payload["data"], key=lambda item: item["index"])
89
+ out.extend(tuple(float(x) for x in item["embedding"]) for item in data)
90
+ return tuple(out)
91
+
92
+ def _headers(self) -> dict[str, str]:
93
+ headers = {"Content-Type": "application/json"}
94
+ if self._config.api_key:
95
+ headers["Authorization"] = f"Bearer {self._config.api_key}"
96
+ return headers
97
+
98
+
99
+ def build_embedder(config: EmbeddingsConfig) -> Embedder:
100
+ """Factory honoring the configured provider with a graceful local fallback."""
101
+ inner = _build_inner(config)
102
+ if config.cache_enabled:
103
+ from icelake.adapters.embedders.cached import CachedEmbedder
104
+
105
+ return CachedEmbedder(inner, max_entries=config.cache_max_entries)
106
+ return inner
107
+
108
+
109
+ def _build_inner(config: EmbeddingsConfig) -> Embedder:
110
+ if config.provider is EmbeddingsProvider.HASHING:
111
+ return HashingEmbedder(config.dimensions)
112
+ if config.provider is EmbeddingsProvider.OPENAI:
113
+ return OpenAICompatEmbedder(config)
114
+ import importlib.util
115
+
116
+ if importlib.util.find_spec("sentence_transformers") is None:
117
+ raise ConfigError(
118
+ "local embeddings require the 'local-embeddings' extra "
119
+ "(pip install icelake[local-embeddings])",
120
+ )
121
+ from icelake.adapters.embedders.local import LocalEmbedder
122
+
123
+ return LocalEmbedder(config)
124
+
125
+
126
+ __all__ = ["HashingEmbedder", "OpenAICompatEmbedder", "build_embedder"]
@@ -0,0 +1,74 @@
1
+ """Embedding cache: content-hash LRU over any inner Embedder.
2
+
3
+ Cuts recall-path latency and API cost when the same or similar texts are
4
+ embedded repeatedly (query-embedding reuse, reconcile collision checks,
5
+ consolidation sanity checks). Thread-safe; bounded by ``max_entries``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import threading
12
+ from collections import OrderedDict
13
+ from collections.abc import Sequence
14
+
15
+ from icelake.ports.llm import Embedder
16
+
17
+
18
+ class CachedEmbedder:
19
+ """LRU-cached wrapper implementing the Embedder protocol."""
20
+
21
+ def __init__(self, inner: Embedder, *, max_entries: int = 50_000) -> None:
22
+ self._inner = inner
23
+ self._max_entries = max_entries
24
+ self._cache: OrderedDict[str, tuple[float, ...]] = OrderedDict()
25
+ self._lock = threading.Lock()
26
+ self.hits = 0
27
+ self.misses = 0
28
+
29
+ @property
30
+ def dimensions(self) -> int:
31
+ return self._inner.dimensions
32
+
33
+ @property
34
+ def hit_rate(self) -> float:
35
+ total = self.hits + self.misses
36
+ return self.hits / total if total else 0.0
37
+
38
+ async def embed(self, texts: Sequence[str]) -> tuple[tuple[float, ...], ...]:
39
+ keys = [self._key(text) for text in texts]
40
+ with self._lock:
41
+ cached: dict[int, tuple[float, ...]] = {}
42
+ missing_indices: list[int] = []
43
+ for index, key in enumerate(keys):
44
+ if key in self._cache:
45
+ cached[index] = self._cache[key]
46
+ self.hits += 1
47
+ else:
48
+ missing_indices.append(index)
49
+ self.misses += 1
50
+ # LRU touch for hits
51
+ for key in (keys[i] for i in cached):
52
+ self._cache.move_to_end(key)
53
+
54
+ if missing_indices:
55
+ uncached_texts = [texts[i] for i in missing_indices]
56
+ new_vectors = await self._inner.embed(uncached_texts)
57
+ for local_idx, vector in zip(missing_indices, new_vectors, strict=True):
58
+ cached[local_idx] = vector
59
+ with self._lock:
60
+ self._cache[keys[local_idx]] = vector
61
+ self._evict_if_needed()
62
+
63
+ return tuple(cached[i] for i in range(len(texts)))
64
+
65
+ def _evict_if_needed(self) -> None:
66
+ while len(self._cache) > self._max_entries:
67
+ self._cache.popitem(last=False)
68
+
69
+ @staticmethod
70
+ def _key(text: str) -> str:
71
+ return hashlib.sha256(text.encode()).hexdigest()
72
+
73
+
74
+ __all__ = ["CachedEmbedder"]
@@ -0,0 +1,51 @@
1
+ """Optional sentence-transformers embedder (extra: ``local-embeddings``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Sequence
7
+ from typing import TYPE_CHECKING, Any, Protocol
8
+
9
+ from icelake.config import EmbeddingsConfig
10
+
11
+ if TYPE_CHECKING:
12
+ pass
13
+
14
+
15
+ class _Model(Protocol):
16
+ def encode(self, sentences: list[str], show_progress_bar: bool) -> list[list[float]]: ...
17
+
18
+
19
+ class LocalEmbedder:
20
+ """Runs a local sentence-transformers model off-loop in a single-thread executor."""
21
+
22
+ def __init__(self, config: EmbeddingsConfig) -> None:
23
+ self._config = config
24
+ self._model: _Model | None = None
25
+
26
+ @property
27
+ def dimensions(self) -> int:
28
+ return self._config.dimensions
29
+
30
+ def _load(self) -> _Model:
31
+ if self._model is None:
32
+ from sentence_transformers import SentenceTransformer
33
+
34
+ name = (self._config.model or "").removeprefix("sentence-transformers/")
35
+ model: Any = SentenceTransformer(name)
36
+ self._model = model
37
+ return self._model
38
+
39
+ async def embed(self, texts: Sequence[str]) -> tuple[tuple[float, ...], ...]:
40
+ if not texts:
41
+ return ()
42
+ model = await asyncio.to_thread(self._load)
43
+ vectors = await asyncio.to_thread(
44
+ model.encode,
45
+ list(texts),
46
+ False,
47
+ )
48
+ return tuple(tuple(float(x) for x in vector) for vector in vectors)
49
+
50
+
51
+ __all__ = ["LocalEmbedder"]
@@ -0,0 +1,10 @@
1
+ """In-memory adapter package exports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from icelake.adapters.in_memory.queue import InMemoryIngestQueue
6
+ from icelake.adapters.in_memory.store import InMemoryStore
7
+ from icelake.adapters.in_memory.vectors import InMemoryVectorIndex
8
+ from icelake.ports.vectors import cosine
9
+
10
+ __all__ = ["InMemoryIngestQueue", "InMemoryStore", "InMemoryVectorIndex", "cosine"]