cortexlayer 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.
@@ -0,0 +1,196 @@
1
+ """Pluggable LLM and embedder for fact memory.
2
+
3
+ Defaults mirror the Cortex server's Mem0 configuration (Ollama, ``think`` off,
4
+ temperature/top_p/num_predict as Mem0's Ollama client: 0.1 / 0.1 / 2000) so the
5
+ extraction call is the same call. Anything with the same method works, so tests
6
+ and other providers need no Ollama.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from typing import Any, Callable, List, Optional, Protocol, Sequence
13
+
14
+ import httpx
15
+
16
+ from ...errors import CortexConfigError, LLMError
17
+
18
+ DEFAULT_OLLAMA_HOST = "http://localhost:11434"
19
+ DEFAULT_LLM_MODEL = "qwen3.5:9b"
20
+ DEFAULT_OLLAMA_EMBED_MODEL = "qwen3-embedding:8b"
21
+
22
+
23
+ def ollama_host() -> str:
24
+ return os.environ.get("OLLAMA_HOST", DEFAULT_OLLAMA_HOST).rstrip("/")
25
+
26
+
27
+ class LLM(Protocol):
28
+ def generate(self, system: str, user: str) -> str:
29
+ """Return the model's raw reply (expected to be a JSON object string)."""
30
+ ...
31
+
32
+
33
+ class Embedder(Protocol):
34
+ name: str # recorded on the store: vectors from different embedders can't be mixed
35
+
36
+ def embed_batch(self, texts: Sequence[str], action: str = "add") -> List[List[float]]:
37
+ """One vector per text. ``action`` is ``add`` | ``search`` | ``update``."""
38
+ ...
39
+
40
+
41
+ class OllamaLLM:
42
+ """Chat model over Ollama's ``/api/chat`` with native JSON output."""
43
+
44
+ def __init__(
45
+ self,
46
+ model: str = DEFAULT_LLM_MODEL,
47
+ host: Optional[str] = None,
48
+ *,
49
+ temperature: float = 0.1,
50
+ top_p: float = 0.1,
51
+ max_tokens: int = 2000,
52
+ think: bool = False,
53
+ timeout: float = 300.0,
54
+ http_client: Optional[httpx.Client] = None,
55
+ ) -> None:
56
+ self.model = model
57
+ self.host = (host or ollama_host()).rstrip("/")
58
+ self._opts = {"temperature": temperature, "top_p": top_p, "num_predict": max_tokens}
59
+ self._think = think
60
+ self._timeout = timeout
61
+ self._http = http_client
62
+
63
+ def generate(self, system: str, user: str) -> str:
64
+ body = {
65
+ "model": self.model,
66
+ "messages": [
67
+ {"role": "system", "content": system},
68
+ # Same nudge Mem0's Ollama client appends for JSON mode.
69
+ {"role": "user", "content": user + "\n\nPlease respond with valid JSON only."},
70
+ ],
71
+ "format": "json",
72
+ "stream": False,
73
+ # Thinking models otherwise return a trace and JSON extraction
74
+ # silently yields nothing (verified with qwen3.5:9b).
75
+ "think": self._think,
76
+ "options": self._opts,
77
+ }
78
+ try:
79
+ client = self._http or httpx.Client(timeout=self._timeout)
80
+ try:
81
+ resp = client.post(f"{self.host}/api/chat", json=body)
82
+ finally:
83
+ if self._http is None:
84
+ client.close()
85
+ resp.raise_for_status()
86
+ return resp.json()["message"]["content"]
87
+ except (httpx.HTTPError, KeyError, ValueError) as e:
88
+ raise LLMError(f"LLM call to {self.host} ({self.model}) failed: {e}") from e
89
+
90
+
91
+ class OllamaEmbedder:
92
+ """Embeddings over Ollama's ``/api/embed``."""
93
+
94
+ def __init__(
95
+ self,
96
+ model: str = DEFAULT_OLLAMA_EMBED_MODEL,
97
+ host: Optional[str] = None,
98
+ *,
99
+ timeout: float = 120.0,
100
+ http_client: Optional[httpx.Client] = None,
101
+ ) -> None:
102
+ self.model = model
103
+ self.host = (host or ollama_host()).rstrip("/")
104
+ self.name = f"ollama:{model}"
105
+ self._timeout = timeout
106
+ self._http = http_client
107
+
108
+ def embed_batch(self, texts: Sequence[str], action: str = "add") -> List[List[float]]:
109
+ if not texts:
110
+ return []
111
+ try:
112
+ client = self._http or httpx.Client(timeout=self._timeout)
113
+ try:
114
+ resp = client.post(
115
+ f"{self.host}/api/embed", json={"model": self.model, "input": list(texts)}
116
+ )
117
+ finally:
118
+ if self._http is None:
119
+ client.close()
120
+ resp.raise_for_status()
121
+ vectors = resp.json().get("embeddings") or []
122
+ except (httpx.HTTPError, ValueError) as e:
123
+ raise LLMError(f"Embedding call to {self.host} ({self.model}) failed: {e}") from e
124
+ if len(vectors) != len(texts):
125
+ raise LLMError(
126
+ f"Ollama returned {len(vectors)} embeddings for {len(texts)} texts ({self.model})"
127
+ )
128
+ return vectors
129
+
130
+
131
+ class ChromaEmbedder:
132
+ """Chroma's built-in ONNX MiniLM-L6-v2 — no Ollama or network needed after
133
+ the one-time ~80 MB model download. The library's default embedder."""
134
+
135
+ name = "chroma:onnx-minilm-l6-v2"
136
+
137
+ def __init__(self) -> None:
138
+ self._fn: Any = None
139
+
140
+ def embed_batch(self, texts: Sequence[str], action: str = "add") -> List[List[float]]:
141
+ if not texts:
142
+ return []
143
+ if self._fn is None:
144
+ from chromadb.utils.embedding_functions import DefaultEmbeddingFunction
145
+
146
+ self._fn = DefaultEmbeddingFunction()
147
+ return [[float(x) for x in v] for v in self._fn(list(texts))]
148
+
149
+
150
+ class _CallableLLM:
151
+ def __init__(self, fn: Callable[[str, str], str]) -> None:
152
+ self._fn = fn
153
+
154
+ def generate(self, system: str, user: str) -> str:
155
+ return self._fn(system, user)
156
+
157
+
158
+ def resolve_llm(spec: Any) -> LLM:
159
+ """``None`` → default Ollama; dict → ``OllamaLLM(**dict)``; an object with
160
+ ``generate`` → itself; a callable ``fn(system, user) -> str`` → wrapped."""
161
+ if spec is None:
162
+ return OllamaLLM()
163
+ if isinstance(spec, dict):
164
+ cfg = dict(spec)
165
+ provider = cfg.pop("provider", "ollama")
166
+ if provider != "ollama":
167
+ raise CortexConfigError(f"unknown llm provider {provider!r} (only 'ollama' is built in)")
168
+ return OllamaLLM(**cfg)
169
+ if hasattr(spec, "generate"):
170
+ return spec
171
+ if callable(spec):
172
+ return _CallableLLM(spec)
173
+ raise CortexConfigError(f"llm must be None, a dict, a callable or an object with generate(); got {spec!r}")
174
+
175
+
176
+ def resolve_embedder(spec: Any) -> Embedder:
177
+ """``None``/``"chroma"`` → Chroma ONNX; ``"ollama"`` → Ollama default model;
178
+ dict → ``{"provider": "ollama"|"chroma", ...}``; an object with
179
+ ``embed_batch`` and ``name`` → itself."""
180
+ if spec is None or spec == "chroma":
181
+ return ChromaEmbedder()
182
+ if spec == "ollama":
183
+ return OllamaEmbedder()
184
+ if isinstance(spec, dict):
185
+ cfg = dict(spec)
186
+ provider = cfg.pop("provider", "ollama")
187
+ if provider == "ollama":
188
+ return OllamaEmbedder(**cfg)
189
+ if provider == "chroma":
190
+ return ChromaEmbedder()
191
+ raise CortexConfigError(f"unknown embedder provider {provider!r}")
192
+ if hasattr(spec, "embed_batch") and hasattr(spec, "name"):
193
+ return spec
194
+ raise CortexConfigError(
195
+ f"embedder must be None, 'chroma', 'ollama', a dict, or an object with embed_batch() and name; got {spec!r}"
196
+ )