causaliq-knowledge 0.2.0__py3-none-any.whl → 0.3.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,380 @@
1
+ """
2
+ LLM-specific cache encoder and data structures.
3
+
4
+ This module provides the LLMEntryEncoder for caching LLM requests and
5
+ responses with rich metadata for analysis.
6
+
7
+ Note: This module stays in causaliq-knowledge (LLM-specific).
8
+ The base cache infrastructure will migrate to causaliq-core.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import asdict, dataclass, field
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ from causaliq_knowledge.cache.encoders import JsonEncoder
19
+
20
+ if TYPE_CHECKING: # pragma: no cover
21
+ from causaliq_knowledge.cache.token_cache import TokenCache
22
+
23
+
24
+ @dataclass
25
+ class LLMTokenUsage:
26
+ """Token usage statistics for an LLM request.
27
+
28
+ Attributes:
29
+ input: Number of tokens in the prompt.
30
+ output: Number of tokens in the completion.
31
+ total: Total tokens (input + output).
32
+ """
33
+
34
+ input: int = 0
35
+ output: int = 0
36
+ total: int = 0
37
+
38
+
39
+ @dataclass
40
+ class LLMMetadata:
41
+ """Metadata for a cached LLM response.
42
+
43
+ Attributes:
44
+ provider: LLM provider name (openai, anthropic, etc.).
45
+ timestamp: When the original request was made (ISO format).
46
+ latency_ms: Response time in milliseconds.
47
+ tokens: Token usage statistics.
48
+ cost_usd: Estimated cost of the request in USD.
49
+ cache_hit: Whether this was served from cache.
50
+ """
51
+
52
+ provider: str = ""
53
+ timestamp: str = ""
54
+ latency_ms: int = 0
55
+ tokens: LLMTokenUsage = field(default_factory=LLMTokenUsage)
56
+ cost_usd: float = 0.0
57
+ cache_hit: bool = False
58
+
59
+ def to_dict(self) -> dict[str, Any]:
60
+ """Convert to dictionary for JSON serialisation."""
61
+ return {
62
+ "provider": self.provider,
63
+ "timestamp": self.timestamp,
64
+ "latency_ms": self.latency_ms,
65
+ "tokens": asdict(self.tokens),
66
+ "cost_usd": self.cost_usd,
67
+ "cache_hit": self.cache_hit,
68
+ }
69
+
70
+ @classmethod
71
+ def from_dict(cls, data: dict[str, Any]) -> LLMMetadata:
72
+ """Create from dictionary."""
73
+ tokens_data = data.get("tokens", {})
74
+ return cls(
75
+ provider=data.get("provider", ""),
76
+ timestamp=data.get("timestamp", ""),
77
+ latency_ms=data.get("latency_ms", 0),
78
+ tokens=LLMTokenUsage(
79
+ input=tokens_data.get("input", 0),
80
+ output=tokens_data.get("output", 0),
81
+ total=tokens_data.get("total", 0),
82
+ ),
83
+ cost_usd=data.get("cost_usd", 0.0),
84
+ cache_hit=data.get("cache_hit", False),
85
+ )
86
+
87
+
88
+ @dataclass
89
+ class LLMResponse:
90
+ """LLM response data for caching.
91
+
92
+ Attributes:
93
+ content: The full text response from the LLM.
94
+ finish_reason: Why generation stopped (stop, length, etc.).
95
+ model_version: Actual model version used.
96
+ """
97
+
98
+ content: str = ""
99
+ finish_reason: str = "stop"
100
+ model_version: str = ""
101
+
102
+ def to_dict(self) -> dict[str, Any]:
103
+ """Convert to dictionary for JSON serialisation."""
104
+ return {
105
+ "content": self.content,
106
+ "finish_reason": self.finish_reason,
107
+ "model_version": self.model_version,
108
+ }
109
+
110
+ @classmethod
111
+ def from_dict(cls, data: dict[str, Any]) -> LLMResponse:
112
+ """Create from dictionary."""
113
+ return cls(
114
+ content=data.get("content", ""),
115
+ finish_reason=data.get("finish_reason", "stop"),
116
+ model_version=data.get("model_version", ""),
117
+ )
118
+
119
+
120
+ @dataclass
121
+ class LLMCacheEntry:
122
+ """Complete LLM cache entry with request, response, and metadata.
123
+
124
+ Attributes:
125
+ model: The model name requested.
126
+ messages: The conversation messages.
127
+ temperature: Sampling temperature.
128
+ max_tokens: Maximum tokens in response.
129
+ response: The LLM response data.
130
+ metadata: Rich metadata for analysis.
131
+ """
132
+
133
+ model: str = ""
134
+ messages: list[dict[str, Any]] = field(default_factory=list)
135
+ temperature: float = 0.0
136
+ max_tokens: int | None = None
137
+ response: LLMResponse = field(default_factory=LLMResponse)
138
+ metadata: LLMMetadata = field(default_factory=LLMMetadata)
139
+
140
+ def to_dict(self) -> dict[str, Any]:
141
+ """Convert to dictionary for JSON serialisation."""
142
+ return {
143
+ "cache_key": {
144
+ "model": self.model,
145
+ "messages": self.messages,
146
+ "temperature": self.temperature,
147
+ "max_tokens": self.max_tokens,
148
+ },
149
+ "response": self.response.to_dict(),
150
+ "metadata": self.metadata.to_dict(),
151
+ }
152
+
153
+ @classmethod
154
+ def from_dict(cls, data: dict[str, Any]) -> LLMCacheEntry:
155
+ """Create from dictionary."""
156
+ cache_key = data.get("cache_key", {})
157
+ return cls(
158
+ model=cache_key.get("model", ""),
159
+ messages=cache_key.get("messages", []),
160
+ temperature=cache_key.get("temperature", 0.0),
161
+ max_tokens=cache_key.get("max_tokens"),
162
+ response=LLMResponse.from_dict(data.get("response", {})),
163
+ metadata=LLMMetadata.from_dict(data.get("metadata", {})),
164
+ )
165
+
166
+ @classmethod
167
+ def create(
168
+ cls,
169
+ model: str,
170
+ messages: list[dict[str, Any]],
171
+ content: str,
172
+ *,
173
+ temperature: float = 0.0,
174
+ max_tokens: int | None = None,
175
+ finish_reason: str = "stop",
176
+ model_version: str = "",
177
+ provider: str = "",
178
+ latency_ms: int = 0,
179
+ input_tokens: int = 0,
180
+ output_tokens: int = 0,
181
+ cost_usd: float = 0.0,
182
+ ) -> LLMCacheEntry:
183
+ """Create a cache entry with common parameters.
184
+
185
+ Args:
186
+ model: The model name requested.
187
+ messages: The conversation messages.
188
+ content: The response content.
189
+ temperature: Sampling temperature.
190
+ max_tokens: Maximum tokens in response.
191
+ finish_reason: Why generation stopped.
192
+ model_version: Actual model version.
193
+ provider: LLM provider name.
194
+ latency_ms: Response time in milliseconds.
195
+ input_tokens: Number of input tokens.
196
+ output_tokens: Number of output tokens.
197
+ cost_usd: Estimated cost in USD.
198
+
199
+ Returns:
200
+ Configured LLMCacheEntry.
201
+ """
202
+ return cls(
203
+ model=model,
204
+ messages=messages,
205
+ temperature=temperature,
206
+ max_tokens=max_tokens,
207
+ response=LLMResponse(
208
+ content=content,
209
+ finish_reason=finish_reason,
210
+ model_version=model_version or model,
211
+ ),
212
+ metadata=LLMMetadata(
213
+ provider=provider,
214
+ timestamp=datetime.now(timezone.utc).isoformat(),
215
+ latency_ms=latency_ms,
216
+ tokens=LLMTokenUsage(
217
+ input=input_tokens,
218
+ output=output_tokens,
219
+ total=input_tokens + output_tokens,
220
+ ),
221
+ cost_usd=cost_usd,
222
+ cache_hit=False,
223
+ ),
224
+ )
225
+
226
+
227
+ class LLMEntryEncoder(JsonEncoder):
228
+ """Encoder for LLM cache entries.
229
+
230
+ Extends JsonEncoder with LLM-specific convenience methods for
231
+ encoding/decoding LLMCacheEntry objects.
232
+
233
+ The encoder stores data in the standard JSON tokenised format,
234
+ achieving 50-70% compression through the shared token dictionary.
235
+
236
+ Example:
237
+ >>> from causaliq_knowledge.cache import TokenCache
238
+ >>> from causaliq_knowledge.llm.cache import (
239
+ ... LLMEntryEncoder, LLMCacheEntry,
240
+ ... )
241
+ >>> with TokenCache(":memory:") as cache:
242
+ ... encoder = LLMEntryEncoder()
243
+ ... entry = LLMCacheEntry.create(
244
+ ... model="gpt-4",
245
+ ... messages=[{"role": "user", "content": "Hello"}],
246
+ ... content="Hi there!",
247
+ ... provider="openai",
248
+ ... )
249
+ ... blob = encoder.encode(entry.to_dict(), cache)
250
+ ... data = encoder.decode(blob, cache)
251
+ ... restored = LLMCacheEntry.from_dict(data)
252
+ """
253
+
254
+ def encode_entry(self, entry: LLMCacheEntry, cache: TokenCache) -> bytes:
255
+ """Encode an LLMCacheEntry to bytes.
256
+
257
+ Convenience method that handles to_dict conversion.
258
+
259
+ Args:
260
+ entry: The cache entry to encode.
261
+ cache: TokenCache for token dictionary.
262
+
263
+ Returns:
264
+ Encoded bytes.
265
+ """
266
+ return self.encode(entry.to_dict(), cache)
267
+
268
+ def decode_entry(self, blob: bytes, cache: TokenCache) -> LLMCacheEntry:
269
+ """Decode bytes to an LLMCacheEntry.
270
+
271
+ Convenience method that handles from_dict conversion.
272
+
273
+ Args:
274
+ blob: Encoded bytes.
275
+ cache: TokenCache for token dictionary.
276
+
277
+ Returns:
278
+ Decoded LLMCacheEntry.
279
+ """
280
+ data = self.decode(blob, cache)
281
+ return LLMCacheEntry.from_dict(data)
282
+
283
+ def generate_export_filename(
284
+ self, entry: LLMCacheEntry, cache_key: str
285
+ ) -> str:
286
+ """Generate a human-readable filename for export.
287
+
288
+ Creates a filename from model name and query details, with a
289
+ short hash suffix for uniqueness.
290
+
291
+ For edge queries, extracts node names for format:
292
+ {model}_{node_a}_{node_b}_edge_{hash}.json
293
+
294
+ For other queries, uses prompt excerpt:
295
+ {model}_{prompt_excerpt}_{hash}.json
296
+
297
+ Args:
298
+ entry: The cache entry to generate filename for.
299
+ cache_key: The cache key (hash) for uniqueness suffix.
300
+
301
+ Returns:
302
+ Human-readable filename with .json extension.
303
+
304
+ Example:
305
+ >>> encoder = LLMEntryEncoder()
306
+ >>> entry = LLMCacheEntry.create(
307
+ ... model="gpt-4",
308
+ ... messages=[{"role": "user", "content": "smoking and lung"}],
309
+ ... content="Yes...",
310
+ ... )
311
+ >>> encoder.generate_export_filename(entry, "a1b2c3d4e5f6")
312
+ 'gpt4_smoking_lung_edge_a1b2.json'
313
+ """
314
+ import re
315
+
316
+ # Sanitize model name (alphanumeric only, lowercase)
317
+ model = re.sub(r"[^a-z0-9]", "", entry.model.lower())
318
+ if len(model) > 15:
319
+ model = model[:15]
320
+
321
+ # Extract user message content
322
+ prompt = ""
323
+ for msg in entry.messages:
324
+ if msg.get("role") == "user":
325
+ prompt = msg.get("content", "")
326
+ break
327
+
328
+ # Try to extract node names for edge queries
329
+ # Look for patterns like "X and Y", "X cause Y", "between X and Y"
330
+ prompt_lower = prompt.lower()
331
+ slug = ""
332
+
333
+ # Pattern: "between X and Y" or "X and Y"
334
+ match = re.search(r"(?:between\s+)?(\w+)\s+and\s+(\w+)", prompt_lower)
335
+ if match:
336
+ node_a = match.group(1)[:15]
337
+ node_b = match.group(2)[:15]
338
+ slug = f"{node_a}_{node_b}_edge"
339
+
340
+ # Fallback: extract first significant words from prompt
341
+ if not slug:
342
+ # Remove common words, keep alphanumeric
343
+ cleaned = re.sub(r"[^a-z0-9\s]", "", prompt_lower)
344
+ words = [
345
+ w
346
+ for w in cleaned.split()
347
+ if w
348
+ not in ("the", "a", "an", "is", "are", "does", "do", "can")
349
+ ]
350
+ slug = "_".join(words[:4])
351
+ if len(slug) > 30:
352
+ slug = slug[:30].rstrip("_")
353
+
354
+ # Short hash suffix for uniqueness (4 chars)
355
+ hash_suffix = cache_key[:4] if cache_key else "0000"
356
+
357
+ # Build filename
358
+ parts = [p for p in [model, slug, hash_suffix] if p]
359
+ return "_".join(parts) + ".json"
360
+
361
+ def export_entry(self, entry: LLMCacheEntry, path: Path) -> None:
362
+ """Export an LLMCacheEntry to a JSON file.
363
+
364
+ Args:
365
+ entry: The cache entry to export.
366
+ path: Destination file path.
367
+ """
368
+ self.export(entry.to_dict(), path)
369
+
370
+ def import_entry(self, path: Path) -> LLMCacheEntry:
371
+ """Import an LLMCacheEntry from a JSON file.
372
+
373
+ Args:
374
+ path: Source file path.
375
+
376
+ Returns:
377
+ Imported LLMCacheEntry.
378
+ """
379
+ data = self.import_(path)
380
+ return LLMCacheEntry.from_dict(data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: causaliq-knowledge
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Incorporating LLM and human knowledge into causal discovery
5
5
  Author-email: CausalIQ <info@causaliq.com>
6
6
  Maintainer-email: CausalIQ <info@causaliq.com>
@@ -89,10 +89,10 @@ Currently implemented releases:
89
89
 
90
90
  - **Release v0.1.0 - Foundation LLM**: Simple LLM queries to 1 or 2 LLMs about edge existence and orientation to support graph averaging
91
91
  - **Release v0.2.0 - Additional LLMs**: Support for 7 LLM providers (Groq, Gemini, OpenAI, Anthropic, DeepSeek, Mistral, Ollama)
92
+ - **Release v0.3.0 - LLM Caching** *(in development)*: SQLite-based response caching with CLI tools for cache management
92
93
 
93
94
  Planned:
94
95
 
95
- - **Release v0.3.0 - LLM Caching**: Caching of LLM queries and responses
96
96
  - **Release v0.4.0 - LLM Context**: Variable/role/literature etc context
97
97
  - **Release v0.5.0 - Algorithm integration**: Integration into structure learning algorithms
98
98
  - **Release v0.6.0 - Legacy Reference**: Support for legacy approaches of deriving knowledge from reference networks
@@ -1,10 +1,16 @@
1
- causaliq_knowledge/__init__.py,sha256=IcoxZ6fjiN6VrniikCUZhHkxf2D1eGixtLWNrvKevN0,851
1
+ causaliq_knowledge/__init__.py,sha256=3m-1i0_giGiTzvJj_8lDrMrvpDvnPD3IBOGlU3ZmxfM,843
2
2
  causaliq_knowledge/base.py,sha256=GBG-sftOKkmUoQzTpm6anDTjP-2nInRZN_36dxoYhvk,2917
3
- causaliq_knowledge/cli.py,sha256=2c8WYxF4T_-R8hDIo9JiZCx59fVbXHCCln66UGLqirs,13169
3
+ causaliq_knowledge/cli.py,sha256=FjdlpQ62Mm4SjWGLAaXnPdv8hYh73-IUweLQAhrBw9k,25010
4
4
  causaliq_knowledge/models.py,sha256=tWGf186ASwO8NHiN97pEOLuBJmJI6Q9jvpU0mYZNdS0,4058
5
+ causaliq_knowledge/cache/__init__.py,sha256=Av92YdCdVTRt9TmB2edRsIFDxq3f1Qi0daq0sFV1rp0,549
6
+ causaliq_knowledge/cache/token_cache.py,sha256=dURih1jr0csVBxU1pCtmcjV48GnQeCnVGi3j1E0KY7Q,21845
7
+ causaliq_knowledge/cache/encoders/__init__.py,sha256=gZ7gw96paFDbnJuc4v1aJsEJfVinI4zc03tXyFvfZxo,461
8
+ causaliq_knowledge/cache/encoders/base.py,sha256=jK7--Or3lVp1UkKghKYFo_gKJp0HsMxosL_8eYL7RQQ,2679
9
+ causaliq_knowledge/cache/encoders/json_encoder.py,sha256=44mcYpT6vJaJT9ZwtnWwdxCvTXIFyoeolqyiAXrgH1o,15110
5
10
  causaliq_knowledge/llm/__init__.py,sha256=30AL0h64zIkXoiqhMY7gjaf7mrtwtwMW38vzhns0My4,1663
6
11
  causaliq_knowledge/llm/anthropic_client.py,sha256=dPFHYGWL4xwQCtmQuGwGY4DBKSINOgOS-11ekznaiXo,8719
7
- causaliq_knowledge/llm/base_client.py,sha256=Dg5s9FqtTScliEK9MJ2_B0atTNwRRMNscv9gai6sEB4,7090
12
+ causaliq_knowledge/llm/base_client.py,sha256=o2qWu2_ttKMHT4isdkY4VUjma3B3jtdx1vhOLXVFLX4,12249
13
+ causaliq_knowledge/llm/cache.py,sha256=gBjZaYNJZ8HF54Hk25RWGVOvdBFwVPAv78_GYaanRTc,12723
8
14
  causaliq_knowledge/llm/deepseek_client.py,sha256=ZcOpgnYa66XHjiTaF5ekR_BtosRYvVmzlIafp_Gsx_A,3543
9
15
  causaliq_knowledge/llm/gemini_client.py,sha256=XJMq9sPo7zExrALSr2rIRHLheSPqKo8ENG0KtdJ1cjw,9924
10
16
  causaliq_knowledge/llm/groq_client.py,sha256=PnTXqtMF1Km9DY4HiCZXQ6LeOzdjZtQJaeuGe1GbeME,7531
@@ -14,9 +20,9 @@ causaliq_knowledge/llm/openai_client.py,sha256=MJmB6P32TZESMlXhn9d0-b3vFWXmf7ojH
14
20
  causaliq_knowledge/llm/openai_compat_client.py,sha256=L8ZW5csuhUePq4mt3EGOUqhR3tleFmM72UlhPBsgIMQ,9518
15
21
  causaliq_knowledge/llm/prompts.py,sha256=bJ9iVGKUfTfLi2eWh-FFM4cNzk5Ux4Z0x8R6Ia27Dbo,6598
16
22
  causaliq_knowledge/llm/provider.py,sha256=VDEv-1esT_EgJk_Gwlfl4423ojglOxzPCBCFbOFE4DQ,15184
17
- causaliq_knowledge-0.2.0.dist-info/licenses/LICENSE,sha256=vUFUzQnti-D-MLSi9NxFlsFYOKwU25sxxH7WgJOQFIs,1084
18
- causaliq_knowledge-0.2.0.dist-info/METADATA,sha256=NxnJJjL6hED91fu0DlclGSoeiji8litmEsy1sS_lt_0,8726
19
- causaliq_knowledge-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- causaliq_knowledge-0.2.0.dist-info/entry_points.txt,sha256=8iQjiMgFxZszRWwSTGHvoOBb_OBUkMmwvH3PzgsH-Cc,104
21
- causaliq_knowledge-0.2.0.dist-info/top_level.txt,sha256=GcxQf4BQAGa38i2-j8ylk2FmnBHtEZ9-8bSt-7Uka7k,19
22
- causaliq_knowledge-0.2.0.dist-info/RECORD,,
23
+ causaliq_knowledge-0.3.0.dist-info/licenses/LICENSE,sha256=vUFUzQnti-D-MLSi9NxFlsFYOKwU25sxxH7WgJOQFIs,1084
24
+ causaliq_knowledge-0.3.0.dist-info/METADATA,sha256=MIE-z6VqrnzuhHpU8j0DzxB48zwyDIobseO2SltVe-0,8774
25
+ causaliq_knowledge-0.3.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
26
+ causaliq_knowledge-0.3.0.dist-info/entry_points.txt,sha256=8iQjiMgFxZszRWwSTGHvoOBb_OBUkMmwvH3PzgsH-Cc,104
27
+ causaliq_knowledge-0.3.0.dist-info/top_level.txt,sha256=GcxQf4BQAGa38i2-j8ylk2FmnBHtEZ9-8bSt-7Uka7k,19
28
+ causaliq_knowledge-0.3.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5