pdm-memory 0.1.2__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,202 @@
1
+ """
2
+ Auto-Signature Generator — Task 3.2
3
+
4
+ Uses an LLM to auto-generate:
5
+ 1. compressed_fact — ≤500 char compressed memory from raw text
6
+ 2. intent_tags — exactly 3 mandatory tags (+ up to 2 optional)
7
+ 3. p_magnitude — suggested importance score (0–100)
8
+
9
+ Works with both OpenAI and Anthropic clients (duck-typed).
10
+
11
+ Usage:
12
+ from pdm_memory.ingest.auto_signature import AutoSignatureGenerator
13
+ gen = AutoSignatureGenerator(openai_client)
14
+ result = gen.generate("User said they prefer dark mode and smaller fonts.")
15
+ print(result.compressed_fact)
16
+ print(result.intent_tags)
17
+ print(result.p_magnitude)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import logging
24
+ from dataclasses import dataclass
25
+ from typing import Any, List, Optional
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ GENERATION_PROMPT = """You are a memory compression engine for an AI assistant.
31
+
32
+ Your job: compress the raw input into a PDM (Pressure-Driven Memory) signature.
33
+
34
+ Rules:
35
+ 1. compressed_fact: A concise factual statement (max 500 characters).
36
+ Strip conversational filler. Keep only the durable, important information.
37
+ 2. intent_tags: Exactly 3 to 5 lowercase tags (strings) that categorise this memory.
38
+ Tags must be specific and searchable (e.g. ["units", "formatting", "preferences"]).
39
+ The first 3 are mandatory.
40
+ 3. p_magnitude: An integer 0-100 representing importance.
41
+ - 80-100: Critical, user-defining preference or fact
42
+ - 60-79: Important, frequently relevant
43
+ - 40-59: Useful, occasionally relevant
44
+ - 20-39: Low signal, rarely matters
45
+ - 0-19: Near-trivial
46
+
47
+ Return ONLY valid JSON in this exact format (no markdown):
48
+ {
49
+ "compressed_fact": "...",
50
+ "intent_tags": ["tag1", "tag2", "tag3"],
51
+ "p_magnitude": 65
52
+ }
53
+
54
+ Raw input to compress:
55
+ """
56
+
57
+
58
+ @dataclass
59
+ class AutoSignatureResult:
60
+ compressed_fact: str
61
+ intent_tags: List[str]
62
+ p_magnitude: float
63
+ raw_response: str = ""
64
+
65
+
66
+ class AutoSignatureGenerator:
67
+ """
68
+ Generates PDM signatures from raw text using an LLM.
69
+
70
+ Args:
71
+ llm_client: An OpenAI or Anthropic client instance.
72
+ The generator will duck-type to detect which one.
73
+ model: Model name override (defaults per provider).
74
+ max_tokens: Max tokens for the LLM response.
75
+ """
76
+
77
+ OPENAI_DEFAULT_MODEL = "gpt-4o-mini"
78
+ ANTHROPIC_DEFAULT_MODEL = "claude-3-haiku-20240307"
79
+
80
+ def __init__(
81
+ self,
82
+ llm_client: Any,
83
+ model: Optional[str] = None,
84
+ max_tokens: int = 256,
85
+ ) -> None:
86
+ self._client = llm_client
87
+ self._model = model
88
+ self._max_tokens = max_tokens
89
+ self._provider = self._detect_provider()
90
+
91
+ def generate(self, raw_text: str) -> Optional[AutoSignatureResult]:
92
+ """
93
+ Generate a PDM signature for raw_text.
94
+
95
+ Args:
96
+ raw_text: The text to compress into a memory.
97
+
98
+ Returns:
99
+ AutoSignatureResult or None on failure.
100
+ """
101
+ prompt = GENERATION_PROMPT + raw_text.strip()[:2000] # Cap input length
102
+
103
+ try:
104
+ if self._provider == "openai":
105
+ return self._call_openai(prompt)
106
+ elif self._provider == "anthropic":
107
+ return self._call_anthropic(prompt)
108
+ else:
109
+ logger.warning("[PDM-AutoSig] Unknown LLM provider; trying OpenAI API shape")
110
+ return self._call_openai(prompt)
111
+ except Exception as e:
112
+ logger.warning("[PDM-AutoSig] Generation failed: %s", e)
113
+ return None
114
+
115
+ # ------------------------------------------------------------------
116
+ # Provider-specific calls
117
+ # ------------------------------------------------------------------
118
+
119
+ def _call_openai(self, prompt: str) -> Optional[AutoSignatureResult]:
120
+ model = self._model or self.OPENAI_DEFAULT_MODEL
121
+ resp = self._client.chat.completions.create(
122
+ model=model,
123
+ messages=[{"role": "user", "content": prompt}],
124
+ max_tokens=self._max_tokens,
125
+ temperature=0.2,
126
+ )
127
+ raw = resp.choices[0].message.content or ""
128
+ return self._parse_response(raw)
129
+
130
+ def _call_anthropic(self, prompt: str) -> Optional[AutoSignatureResult]:
131
+ model = self._model or self.ANTHROPIC_DEFAULT_MODEL
132
+ resp = self._client.messages.create(
133
+ model=model,
134
+ max_tokens=self._max_tokens,
135
+ messages=[{"role": "user", "content": prompt}],
136
+ )
137
+ raw = resp.content[0].text if resp.content else ""
138
+ return self._parse_response(raw)
139
+
140
+ # ------------------------------------------------------------------
141
+ # Parsing
142
+ # ------------------------------------------------------------------
143
+
144
+ def _parse_response(self, raw: str) -> Optional[AutoSignatureResult]:
145
+ raw = raw.strip()
146
+ # Strip markdown code fences if present
147
+ if raw.startswith("```"):
148
+ lines = raw.split("\n")
149
+ raw = "\n".join(lines[1:-1] if lines[-1].strip() == "```" else lines[1:])
150
+
151
+ try:
152
+ data = json.loads(raw)
153
+ except json.JSONDecodeError:
154
+ # Try to extract JSON from mixed content
155
+ import re
156
+ match = re.search(r"\{.*\}", raw, re.DOTALL)
157
+ if not match:
158
+ logger.warning("[PDM-AutoSig] Could not parse JSON from: %s", raw[:200])
159
+ return None
160
+ try:
161
+ data = json.loads(match.group(0))
162
+ except json.JSONDecodeError:
163
+ return None
164
+
165
+ fact = str(data.get("compressed_fact", "")).strip()[:500]
166
+ tags = data.get("intent_tags", [])
167
+ if isinstance(tags, list):
168
+ tags = [str(t).lower().strip() for t in tags if t][:5]
169
+ else:
170
+ tags = []
171
+
172
+ if len(tags) < 3:
173
+ logger.warning("[PDM-AutoSig] LLM returned fewer than 3 tags: %s", tags)
174
+
175
+ p_mag = float(data.get("p_magnitude", 50))
176
+ p_mag = max(0.0, min(100.0, p_mag))
177
+
178
+ if not fact:
179
+ return None
180
+
181
+ return AutoSignatureResult(
182
+ compressed_fact=fact,
183
+ intent_tags=tags,
184
+ p_magnitude=p_mag,
185
+ raw_response=raw,
186
+ )
187
+
188
+ def _detect_provider(self) -> str:
189
+ """Duck-type detection of OpenAI vs Anthropic client."""
190
+ client_type = type(self._client).__name__.lower()
191
+ module = getattr(type(self._client), "__module__", "") or ""
192
+
193
+ if "anthropic" in module or "anthropic" in client_type:
194
+ return "anthropic"
195
+ if "openai" in module or "openai" in client_type:
196
+ return "openai"
197
+ # Check for common attribute shapes
198
+ if hasattr(self._client, "messages") and hasattr(self._client.messages, "create"):
199
+ return "anthropic"
200
+ if hasattr(self._client, "chat") and hasattr(self._client.chat, "completions"):
201
+ return "openai"
202
+ return "openai" # default fallback
@@ -0,0 +1,164 @@
1
+ """
2
+ Batch Processing Handler — Task 3.3
3
+
4
+ Splits large data sources into chunks and processes them with rate limiting.
5
+ Prevents OOM and API overload when ingesting large datasets (10,000+ rows).
6
+
7
+ Usage:
8
+ processor = BatchProcessor(batch_size=50, delay_seconds=0.5)
9
+ counts = processor.process(data_source, ingester, on_progress=print_progress)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import time
16
+ from typing import Any, Callable, Dict, List, Optional
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class BatchProcessor:
22
+ """
23
+ Processes large data sources in batches.
24
+
25
+ Args:
26
+ batch_size: Number of records per batch (default 50).
27
+ delay_seconds: Pause between batches in seconds (default 0.2).
28
+ max_retries: Retries per batch on transient errors (default 3).
29
+ retry_delay: Wait before retry, doubles each attempt (default 1.0s).
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ batch_size: int = 50,
35
+ delay_seconds: float = 0.2,
36
+ max_retries: int = 3,
37
+ retry_delay: float = 1.0,
38
+ ) -> None:
39
+ self.batch_size = max(1, batch_size)
40
+ self.delay_seconds = max(0.0, delay_seconds)
41
+ self.max_retries = max(0, max_retries)
42
+ self.retry_delay = max(0.0, retry_delay)
43
+
44
+ def process(
45
+ self,
46
+ data_source: Any,
47
+ ingester: Any, # DataIngester instance
48
+ on_progress: Optional[Callable[[int, int], None]] = None,
49
+ ) -> Dict[str, int]:
50
+ """
51
+ Process a data source through the ingester in batches.
52
+
53
+ Args:
54
+ data_source: Any of:
55
+ - list[dict]
56
+ - str (CSV file path or raw CSV content)
57
+ - list[str] (plain text lines → auto-converted to dicts)
58
+ ingester: DataIngester instance.
59
+ on_progress: Optional callback(processed: int, total: int).
60
+
61
+ Returns:
62
+ Aggregated counts: {saved, skipped, errors}.
63
+ """
64
+ rows = self._normalise_source(data_source)
65
+ total = len(rows)
66
+ counts = {"saved": 0, "skipped": 0, "errors": 0}
67
+ processed = 0
68
+
69
+ logger.info("[PDM-Batch] Starting: %d records, batch_size=%d", total, self.batch_size)
70
+
71
+ for i in range(0, total, self.batch_size):
72
+ batch = rows[i : i + self.batch_size]
73
+ batch_counts = self._process_batch(batch, ingester)
74
+
75
+ counts["saved"] += batch_counts["saved"]
76
+ counts["skipped"] += batch_counts["skipped"]
77
+ counts["errors"] += batch_counts["errors"]
78
+ processed += len(batch)
79
+
80
+ if on_progress:
81
+ try:
82
+ on_progress(processed, total)
83
+ except Exception:
84
+ pass
85
+
86
+ logger.debug(
87
+ "[PDM-Batch] Batch %d/%d processed | saved=%d skipped=%d errors=%d",
88
+ processed, total,
89
+ batch_counts["saved"], batch_counts["skipped"], batch_counts["errors"],
90
+ )
91
+
92
+ # Rate-limit between batches (not after the last one)
93
+ if i + self.batch_size < total and self.delay_seconds > 0:
94
+ time.sleep(self.delay_seconds)
95
+
96
+ logger.info(
97
+ "[PDM-Batch] Done: saved=%d skipped=%d errors=%d",
98
+ counts["saved"], counts["skipped"], counts["errors"],
99
+ )
100
+ return counts
101
+
102
+ # ------------------------------------------------------------------
103
+ # Private
104
+ # ------------------------------------------------------------------
105
+
106
+ def _process_batch(
107
+ self,
108
+ batch: List[Dict[str, Any]],
109
+ ingester: Any,
110
+ ) -> Dict[str, int]:
111
+ """Process one batch with retry logic."""
112
+ last_error = None
113
+ delay = self.retry_delay
114
+
115
+ for attempt in range(self.max_retries + 1):
116
+ try:
117
+ return ingester.ingest_rows(batch)
118
+ except Exception as e:
119
+ last_error = e
120
+ if attempt < self.max_retries:
121
+ logger.warning(
122
+ "[PDM-Batch] Batch attempt %d failed (%s). Retrying in %.1fs…",
123
+ attempt + 1, e, delay,
124
+ )
125
+ time.sleep(delay)
126
+ delay *= 2 # Exponential backoff
127
+
128
+ logger.error("[PDM-Batch] Batch failed after %d attempts: %s", self.max_retries + 1, last_error)
129
+ return {"saved": 0, "skipped": 0, "errors": len(batch)}
130
+
131
+ @staticmethod
132
+ def _normalise_source(data_source: Any) -> List[Dict[str, Any]]:
133
+ """Convert any supported source type to list[dict]."""
134
+ if isinstance(data_source, list):
135
+ if not data_source:
136
+ return []
137
+ if isinstance(data_source[0], dict):
138
+ return data_source
139
+ # Assume list of strings
140
+ return [{"text": str(item)} for item in data_source]
141
+
142
+ if isinstance(data_source, str):
143
+ # Try as CSV file path or CSV content
144
+ import csv
145
+ from io import StringIO
146
+ try:
147
+ with open(data_source, newline="", encoding="utf-8") as f:
148
+ content = f.read()
149
+ except (FileNotFoundError, OSError):
150
+ content = data_source # Treat as raw CSV string
151
+ reader = csv.DictReader(StringIO(content))
152
+ return list(reader)
153
+
154
+ # Try iterator/generator
155
+ try:
156
+ rows = list(data_source)
157
+ if rows and isinstance(rows[0], dict):
158
+ return rows
159
+ return [{"text": str(r)} for r in rows]
160
+ except Exception:
161
+ raise ValueError(
162
+ f"Unsupported data_source type: {type(data_source)}. "
163
+ "Expected list[dict], list[str], or a CSV file path/string."
164
+ )
@@ -0,0 +1,272 @@
1
+ """
2
+ Data Ingester — Task 3.1
3
+
4
+ Convert any legacy data source (list[dict], CSV, raw messages) into
5
+ PDM SignatureRecords.
6
+
7
+ Usage (via Memory):
8
+ mem.ingest(
9
+ data_source=[
10
+ {"text": "User dislikes long responses", "importance": 70},
11
+ {"message": "Prefers Python over JS"},
12
+ ],
13
+ mapping={"text": "compressed_fact", "importance": "p_magnitude"},
14
+ )
15
+
16
+ Usage (standalone):
17
+ from pdm_memory.ingest.ingester import DataIngester
18
+ ingester = DataIngester(storage=driver, user="alice")
19
+ count = ingester.ingest_rows(rows)
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import csv
25
+ import logging
26
+ from io import StringIO
27
+ from typing import Any, Dict, List, Optional
28
+
29
+ from pdm_memory.core.math import calculate_effective_spike, infer_domain
30
+ from pdm_memory.core.signature import SignatureRecord
31
+ from pdm_memory.storage.base import BaseStorage
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ # Auto-detect common field name aliases
37
+ _FIELD_ALIASES: Dict[str, str] = {
38
+ # compressed_fact aliases
39
+ "text": "compressed_fact",
40
+ "content": "compressed_fact",
41
+ "message": "compressed_fact",
42
+ "body": "compressed_fact",
43
+ "fact": "compressed_fact",
44
+ "memory": "compressed_fact",
45
+ "summary": "compressed_fact",
46
+ # p_magnitude aliases
47
+ "importance": "p_magnitude",
48
+ "priority": "p_magnitude",
49
+ "pressure": "p_magnitude",
50
+ "score": "p_magnitude",
51
+ "weight": "p_magnitude",
52
+ # intent_tags aliases
53
+ "tags": "intent_tags",
54
+ "labels": "intent_tags",
55
+ "categories": "intent_tags",
56
+ "keywords": "intent_tags",
57
+ # drawer aliases
58
+ "category": "drawer_domain",
59
+ "drawer": "drawer_domain",
60
+ "topic": "drawer_domain",
61
+ "domain": "drawer_domain",
62
+ # source aliases
63
+ "origin": "source",
64
+ "channel": "source",
65
+ # regime aliases
66
+ "context": "question_regime",
67
+ "regime": "question_regime",
68
+ }
69
+
70
+
71
+ class DataIngester:
72
+ """
73
+ Converts structured rows into SignatureRecords and saves them.
74
+
75
+ Args:
76
+ storage: The active BaseStorage backend.
77
+ user: User identifier.
78
+ mapping: Optional explicit field mapping (source_key → pdm_field).
79
+ If None, auto-detect via _FIELD_ALIASES.
80
+ llm_client: Optional LLM client for auto-signature generation.
81
+ If provided, missing fields are filled via the LLM.
82
+ """
83
+
84
+ def __init__(
85
+ self,
86
+ storage: BaseStorage,
87
+ user: str = "default",
88
+ mapping: Optional[Dict[str, str]] = None,
89
+ llm_client: Optional[Any] = None,
90
+ ) -> None:
91
+ self._storage = storage
92
+ self._user = user
93
+ self._mapping = mapping
94
+ self._llm_client = llm_client
95
+
96
+ def ingest_rows(self, rows: List[Dict[str, Any]]) -> Dict[str, int]:
97
+ """
98
+ Ingest a list of dicts. Returns {'saved': N, 'skipped': N, 'errors': N}.
99
+ """
100
+ counts = {"saved": 0, "skipped": 0, "errors": 0}
101
+
102
+ for i, row in enumerate(rows):
103
+ try:
104
+ sig = self._row_to_signature(row)
105
+ if sig is None:
106
+ counts["skipped"] += 1
107
+ continue
108
+ self._storage.save(sig)
109
+ counts["saved"] += 1
110
+ except Exception as e:
111
+ logger.warning("[PDM-Ingest] Row %d failed: %s", i, e)
112
+ counts["errors"] += 1
113
+
114
+ return counts
115
+
116
+ def ingest_csv(self, csv_path_or_content: str) -> Dict[str, int]:
117
+ """
118
+ Ingest from a CSV file path or raw CSV string.
119
+ """
120
+ try:
121
+ with open(csv_path_or_content, newline="", encoding="utf-8") as f:
122
+ content = f.read()
123
+ except (FileNotFoundError, OSError):
124
+ content = csv_path_or_content # Treat as raw CSV string
125
+
126
+ reader = csv.DictReader(StringIO(content))
127
+ return self.ingest_rows(list(reader))
128
+
129
+ # ------------------------------------------------------------------
130
+ # Private
131
+ # ------------------------------------------------------------------
132
+
133
+ def _row_to_signature(self, row: Dict[str, Any]) -> Optional[SignatureRecord]:
134
+ """Map one row to a SignatureRecord using auto-detect or explicit mapping."""
135
+ # Build effective mapping
136
+ field_map = self._build_field_map(row)
137
+
138
+ # Extract compressed_fact (required)
139
+ text = field_map.get("compressed_fact", "")
140
+ if not text:
141
+ # Try LLM fallback
142
+ if self._llm_client:
143
+ raw_data = " | ".join(f"{k}: {v}" for k, v in row.items())
144
+ text = self._generate_fact_via_llm(raw_data)
145
+ if not text:
146
+ logger.debug("[PDM-Ingest] Skipping row with no text: %s", row)
147
+ return None
148
+
149
+ text = str(text).strip()[:500]
150
+
151
+ # Extract other fields with defaults
152
+ p_mag = _safe_float(field_map.get("p_magnitude"), default=50.0, min_v=0, max_v=100)
153
+ t_pers = _safe_float(field_map.get("t_persistence"), default=30.0, min_v=1, max_v=365)
154
+ phase = _safe_float(field_map.get("phase_privilege"), default=1.0)
155
+ tags = _parse_tags(field_map.get("intent_tags"))
156
+ drawer = str(field_map.get("drawer_domain") or "general").strip()
157
+ source = str(field_map.get("source") or "csv").strip()
158
+ regime = str(field_map.get("question_regime") or "neutral").strip()
159
+
160
+ # Auto-generate tags via LLM if none found
161
+ if not tags and self._llm_client:
162
+ tags = self._generate_tags_via_llm(text)
163
+
164
+ # Auto-generate tags (minimum 3 recommended)
165
+ if not tags:
166
+ tags = self._auto_tags_from_text(text)
167
+
168
+ domain = infer_domain(tags)
169
+ eff_spike = calculate_effective_spike(p_mag, t_pers, phase)
170
+
171
+ return SignatureRecord(
172
+ user=self._user,
173
+ compressed_fact=text,
174
+ source=source,
175
+ p_magnitude=p_mag,
176
+ t_persistence=t_pers,
177
+ phase_privilege=phase,
178
+ effective_spike=eff_spike,
179
+ intent_tags=tags,
180
+ question_regime=regime,
181
+ domain=domain,
182
+ drawer_domain=drawer,
183
+ )
184
+
185
+ def _build_field_map(self, row: Dict[str, Any]) -> Dict[str, Any]:
186
+ """Apply explicit or auto mapping to a row."""
187
+ result: Dict[str, Any] = {}
188
+
189
+ if self._mapping:
190
+ # Explicit mapping
191
+ for src_key, pdm_key in self._mapping.items():
192
+ if src_key in row:
193
+ result[pdm_key] = row[src_key]
194
+ # Also pass through keys that directly match PDM fields
195
+ for k, v in row.items():
196
+ if k not in result:
197
+ result[k] = v
198
+ else:
199
+ # Auto-detect
200
+ for k, v in row.items():
201
+ canonical = _FIELD_ALIASES.get(k.lower(), k.lower())
202
+ result[canonical] = v
203
+
204
+ return result
205
+
206
+ def _generate_fact_via_llm(self, raw: str) -> str:
207
+ """Ask the LLM to compress raw data into a ≤500 char fact."""
208
+ from pdm_memory.ingest.auto_signature import AutoSignatureGenerator
209
+ gen = AutoSignatureGenerator(self._llm_client)
210
+ result = gen.generate(raw)
211
+ return result.compressed_fact if result else ""
212
+
213
+ def _generate_tags_via_llm(self, text: str) -> List[str]:
214
+ """Ask the LLM to generate 3+ tags for a text."""
215
+ from pdm_memory.ingest.auto_signature import AutoSignatureGenerator
216
+ gen = AutoSignatureGenerator(self._llm_client)
217
+ result = gen.generate(text)
218
+ return result.intent_tags if result else []
219
+
220
+ @staticmethod
221
+ def _auto_tags_from_text(text: str) -> List[str]:
222
+ """Extract simple keyword tags from text without an LLM."""
223
+ import re
224
+ words = re.findall(r"\b[a-zA-Z]{4,}\b", text.lower())
225
+ stopwords = {
226
+ "this", "that", "with", "from", "have", "been", "will",
227
+ "which", "when", "they", "their", "there", "about",
228
+ }
229
+ seen = set()
230
+ tags = []
231
+ for w in words:
232
+ if w not in stopwords and w not in seen:
233
+ seen.add(w)
234
+ tags.append(w)
235
+ if len(tags) >= 5:
236
+ break
237
+ return tags[:5]
238
+
239
+
240
+ # ---------------------------------------------------------------------------
241
+ # Helpers
242
+ # ---------------------------------------------------------------------------
243
+
244
+
245
+ def _safe_float(val: Any, default: float = 0.0, min_v: float = None, max_v: float = None) -> float:
246
+ try:
247
+ v = float(val)
248
+ if min_v is not None:
249
+ v = max(min_v, v)
250
+ if max_v is not None:
251
+ v = min(max_v, v)
252
+ return v
253
+ except (TypeError, ValueError):
254
+ return default
255
+
256
+
257
+ def _parse_tags(val: Any) -> List[str]:
258
+ if val is None:
259
+ return []
260
+ if isinstance(val, list):
261
+ return [str(t).strip() for t in val if t]
262
+ if isinstance(val, str):
263
+ import json
264
+ try:
265
+ parsed = json.loads(val)
266
+ if isinstance(parsed, list):
267
+ return [str(t).strip() for t in parsed if t]
268
+ except (json.JSONDecodeError, ValueError):
269
+ pass
270
+ # Comma or space separated
271
+ return [t.strip() for t in val.replace(",", " ").split() if t.strip()]
272
+ return []
@@ -0,0 +1,21 @@
1
+ """pdm_memory.integrations package."""
2
+ from pdm_memory.integrations.openai_adapter import wrap_openai, PDMOpenAIClient
3
+ from pdm_memory.integrations.anthropic_adapter import wrap_anthropic, PDMAnthropicClient
4
+ from pdm_memory.integrations.context_manager import ContextWindowManager
5
+ from pdm_memory.integrations.gemini_adapter import wrap_gemini, PDMGeminiClient
6
+ from pdm_memory.integrations.ollama_adapter import wrap_ollama, PDMOllamaClient
7
+ from pdm_memory.integrations.groq_adapter import wrap_groq, PDMGroqClient
8
+
9
+ __all__ = [
10
+ "wrap_openai",
11
+ "wrap_anthropic",
12
+ "wrap_gemini",
13
+ "wrap_ollama",
14
+ "wrap_groq",
15
+ "PDMOpenAIClient",
16
+ "PDMAnthropicClient",
17
+ "PDMGeminiClient",
18
+ "PDMOllamaClient",
19
+ "PDMGroqClient",
20
+ "ContextWindowManager",
21
+ ]