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.
- pdm_memory/__init__.py +22 -0
- pdm_memory/auth/__init__.py +4 -0
- pdm_memory/auth/jwt_handler.py +119 -0
- pdm_memory/bench.py +5 -0
- pdm_memory/core/__init__.py +1 -0
- pdm_memory/core/math.py +329 -0
- pdm_memory/core/retrieval.py +362 -0
- pdm_memory/core/signature.py +257 -0
- pdm_memory/ingest/__init__.py +6 -0
- pdm_memory/ingest/auto_signature.py +202 -0
- pdm_memory/ingest/batch.py +164 -0
- pdm_memory/ingest/ingester.py +272 -0
- pdm_memory/integrations/__init__.py +21 -0
- pdm_memory/integrations/anthropic_adapter.py +179 -0
- pdm_memory/integrations/context_manager.py +134 -0
- pdm_memory/integrations/gemini_adapter.py +215 -0
- pdm_memory/integrations/groq_adapter.py +171 -0
- pdm_memory/integrations/ollama_adapter.py +184 -0
- pdm_memory/integrations/openai_adapter.py +191 -0
- pdm_memory/memory.py +552 -0
- pdm_memory/storage/__init__.py +5 -0
- pdm_memory/storage/base.py +123 -0
- pdm_memory/storage/cloud_driver.py +271 -0
- pdm_memory/storage/sqlite_driver.py +331 -0
- pdm_memory/sync.py +145 -0
- pdm_memory/tools/__init__.py +1 -0
- pdm_memory/tools/bench.py +294 -0
- pdm_memory/tools/cli.py +207 -0
- pdm_memory-0.1.2.dist-info/METADATA +485 -0
- pdm_memory-0.1.2.dist-info/RECORD +34 -0
- pdm_memory-0.1.2.dist-info/WHEEL +5 -0
- pdm_memory-0.1.2.dist-info/entry_points.txt +2 -0
- pdm_memory-0.1.2.dist-info/licenses/LICENSE +50 -0
- pdm_memory-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Anthropic Adapter — Task 4.1
|
|
3
|
+
|
|
4
|
+
Wraps an Anthropic client for automatic PDM memory injection and saving.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from pdm_memory.integrations import wrap_anthropic
|
|
8
|
+
from pdm_memory import Memory
|
|
9
|
+
|
|
10
|
+
mem = Memory(store="./my_app.db")
|
|
11
|
+
client = wrap_anthropic(api_key="sk-ant-...", memory=mem)
|
|
12
|
+
reply = client.chat("What units should I use?")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any, Optional
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PDMAnthropicClient:
|
|
24
|
+
"""
|
|
25
|
+
Memory-augmented Anthropic client.
|
|
26
|
+
|
|
27
|
+
All chat() calls automatically:
|
|
28
|
+
- Recall relevant memories before the API call.
|
|
29
|
+
- Inject them via the system prompt.
|
|
30
|
+
- Save both turns as new memories.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
anthropic_client: Any,
|
|
36
|
+
memory: Any,
|
|
37
|
+
model: str = "claude-3-haiku-20240307",
|
|
38
|
+
max_memory_tokens: int = 1500,
|
|
39
|
+
recall_k: int = 5,
|
|
40
|
+
auto_save: bool = True,
|
|
41
|
+
system_prompt: str = "You are a helpful AI assistant.",
|
|
42
|
+
) -> None:
|
|
43
|
+
self._client = anthropic_client
|
|
44
|
+
self._memory = memory
|
|
45
|
+
self._model = model
|
|
46
|
+
self._max_memory_tokens = max_memory_tokens
|
|
47
|
+
self._recall_k = recall_k
|
|
48
|
+
self._auto_save = auto_save
|
|
49
|
+
self._system_prompt = system_prompt
|
|
50
|
+
|
|
51
|
+
from pdm_memory.integrations.context_manager import ContextWindowManager
|
|
52
|
+
self._ctx_manager = ContextWindowManager(
|
|
53
|
+
max_tokens=max_memory_tokens,
|
|
54
|
+
model="gpt-4o-mini", # Use cl100k_base approximation for Anthropic
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def chat(
|
|
58
|
+
self,
|
|
59
|
+
message: str,
|
|
60
|
+
model: Optional[str] = None,
|
|
61
|
+
system_prompt: Optional[str] = None,
|
|
62
|
+
recall_k: Optional[int] = None,
|
|
63
|
+
save_reply: Optional[bool] = None,
|
|
64
|
+
max_tokens: int = 1024,
|
|
65
|
+
**anthropic_kwargs: Any,
|
|
66
|
+
) -> str:
|
|
67
|
+
"""
|
|
68
|
+
Send a message to Anthropic Claude with automatic PDM memory injection.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
message: The user's message.
|
|
72
|
+
model: Override the default model.
|
|
73
|
+
system_prompt: Override the base system prompt.
|
|
74
|
+
recall_k: Override the number of memories to recall.
|
|
75
|
+
save_reply: Override whether to save this exchange to memory.
|
|
76
|
+
max_tokens: Max tokens for Claude's response.
|
|
77
|
+
**anthropic_kwargs: Passed to anthropic.messages.create().
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The assistant's reply text.
|
|
81
|
+
"""
|
|
82
|
+
k = recall_k if recall_k is not None else self._recall_k
|
|
83
|
+
should_save = save_reply if save_reply is not None else self._auto_save
|
|
84
|
+
base_system = system_prompt or self._system_prompt
|
|
85
|
+
|
|
86
|
+
# Step 1: Recall relevant memories
|
|
87
|
+
hits = self._memory.recall(query=message, k=k)
|
|
88
|
+
trimmed = self._ctx_manager.fit(hits)
|
|
89
|
+
|
|
90
|
+
# Step 2: Build system message with memory block
|
|
91
|
+
memory_block = self._ctx_manager.format_for_prompt(trimmed)
|
|
92
|
+
full_system = base_system
|
|
93
|
+
if memory_block:
|
|
94
|
+
full_system = f"{base_system}\n\n{memory_block}"
|
|
95
|
+
|
|
96
|
+
# Step 3: Call Anthropic
|
|
97
|
+
response = self._client.messages.create(
|
|
98
|
+
model=model or self._model,
|
|
99
|
+
max_tokens=max_tokens,
|
|
100
|
+
system=full_system,
|
|
101
|
+
messages=[{"role": "user", "content": message}],
|
|
102
|
+
**anthropic_kwargs,
|
|
103
|
+
)
|
|
104
|
+
reply_text = response.content[0].text if response.content else ""
|
|
105
|
+
|
|
106
|
+
# Step 4: Save this turn to memory
|
|
107
|
+
if should_save:
|
|
108
|
+
self._save_turn(message, reply_text)
|
|
109
|
+
|
|
110
|
+
logger.debug(
|
|
111
|
+
"[PDM-Anthropic] chat() | recalled=%d injected=%d model=%s",
|
|
112
|
+
len(hits), len(trimmed), model or self._model,
|
|
113
|
+
)
|
|
114
|
+
return reply_text
|
|
115
|
+
|
|
116
|
+
def _save_turn(self, user_msg: str, assistant_reply: str) -> None:
|
|
117
|
+
try:
|
|
118
|
+
if user_msg.strip():
|
|
119
|
+
self._memory.save(
|
|
120
|
+
text=user_msg[:500],
|
|
121
|
+
source="anthropic_chat",
|
|
122
|
+
tags=["conversation", "user_input"],
|
|
123
|
+
p_magnitude=40.0,
|
|
124
|
+
)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.warning("[PDM-Anthropic] Failed to save user message: %s", e)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
if assistant_reply.strip():
|
|
130
|
+
self._memory.save(
|
|
131
|
+
text=assistant_reply[:500],
|
|
132
|
+
source="anthropic_chat",
|
|
133
|
+
tags=["conversation", "ai_reply"],
|
|
134
|
+
p_magnitude=35.0,
|
|
135
|
+
)
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.warning("[PDM-Anthropic] Failed to save assistant reply: %s", e)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def wrap_anthropic(
|
|
141
|
+
api_key: str,
|
|
142
|
+
memory: Any,
|
|
143
|
+
model: str = "claude-3-haiku-20240307",
|
|
144
|
+
max_memory_tokens: int = 1500,
|
|
145
|
+
recall_k: int = 5,
|
|
146
|
+
system_prompt: str = "You are a helpful AI assistant.",
|
|
147
|
+
**anthropic_init_kwargs: Any,
|
|
148
|
+
) -> PDMAnthropicClient:
|
|
149
|
+
"""
|
|
150
|
+
Create a memory-augmented Anthropic client.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
api_key: Your Anthropic API key.
|
|
154
|
+
memory: pdm_memory.Memory instance.
|
|
155
|
+
model: Default Claude model.
|
|
156
|
+
max_memory_tokens: Token budget for injected memories.
|
|
157
|
+
recall_k: Memories to recall per turn.
|
|
158
|
+
system_prompt: Base system prompt.
|
|
159
|
+
**anthropic_init_kwargs: Passed to anthropic.Anthropic().
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
PDMAnthropicClient wrapping an anthropic.Anthropic client.
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
import anthropic
|
|
166
|
+
except ImportError:
|
|
167
|
+
raise ImportError(
|
|
168
|
+
"anthropic package is required. Install it: pip install pdm-memory[anthropic]"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
client = anthropic.Anthropic(api_key=api_key, **anthropic_init_kwargs)
|
|
172
|
+
return PDMAnthropicClient(
|
|
173
|
+
anthropic_client=client,
|
|
174
|
+
memory=memory,
|
|
175
|
+
model=model,
|
|
176
|
+
max_memory_tokens=max_memory_tokens,
|
|
177
|
+
recall_k=recall_k,
|
|
178
|
+
system_prompt=system_prompt,
|
|
179
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context Window Manager — Task 4.2
|
|
3
|
+
|
|
4
|
+
Trims recalled memories to fit within a model's token budget.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
manager = ContextWindowManager(max_tokens=1500, model="gpt-4o")
|
|
8
|
+
trimmed_hits = manager.fit(hits)
|
|
9
|
+
system_block = manager.format_for_prompt(trimmed_hits)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from typing import List, Optional
|
|
16
|
+
|
|
17
|
+
from pdm_memory.core.signature import MemoryHit
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ContextWindowManager:
|
|
23
|
+
"""
|
|
24
|
+
Manages the token budget for PDM memories injected into LLM context.
|
|
25
|
+
|
|
26
|
+
Trimming strategy: drop memories with the lowest p_effective first,
|
|
27
|
+
until the remaining set fits within max_tokens.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
max_tokens: Maximum tokens the memory block may consume.
|
|
31
|
+
model: Model name (used to load the correct tokeniser).
|
|
32
|
+
chars_per_token: Approximation fallback if tiktoken is unavailable (default 4).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
max_tokens: int = 1500,
|
|
38
|
+
model: str = "gpt-4o-mini",
|
|
39
|
+
chars_per_token: float = 4.0,
|
|
40
|
+
) -> None:
|
|
41
|
+
self.max_tokens = max_tokens
|
|
42
|
+
self.model = model
|
|
43
|
+
self._chars_per_token = chars_per_token
|
|
44
|
+
self._encoder = self._load_encoder(model)
|
|
45
|
+
|
|
46
|
+
# ------------------------------------------------------------------
|
|
47
|
+
# Public API
|
|
48
|
+
# ------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def fit(self, hits: List[MemoryHit]) -> List[MemoryHit]:
|
|
51
|
+
"""
|
|
52
|
+
Return the highest-pressure subset of hits that fits within max_tokens.
|
|
53
|
+
|
|
54
|
+
Memories are sorted by p_effective descending. The lowest-p ones
|
|
55
|
+
are dropped first to respect the token budget.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
hits: List of MemoryHit objects from mem.recall().
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Trimmed list of MemoryHit, still sorted by p_effective desc.
|
|
62
|
+
"""
|
|
63
|
+
if not hits:
|
|
64
|
+
return hits
|
|
65
|
+
|
|
66
|
+
# Sort highest pressure first (most important memories stay)
|
|
67
|
+
sorted_hits = sorted(hits, key=lambda h: h.p_effective, reverse=True)
|
|
68
|
+
|
|
69
|
+
kept: List[MemoryHit] = []
|
|
70
|
+
used_tokens = 0
|
|
71
|
+
|
|
72
|
+
for hit in sorted_hits:
|
|
73
|
+
tokens = self.count_tokens(hit.text)
|
|
74
|
+
if used_tokens + tokens <= self.max_tokens:
|
|
75
|
+
kept.append(hit)
|
|
76
|
+
used_tokens += tokens
|
|
77
|
+
else:
|
|
78
|
+
logger.debug(
|
|
79
|
+
"[CTX] Trimmed memory %s (P=%.1f, tokens=%d): budget exhausted",
|
|
80
|
+
hit.id[:8], hit.p_effective, tokens,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if len(kept) < len(hits):
|
|
84
|
+
logger.info(
|
|
85
|
+
"[CTX] Token budget %d: kept %d/%d memories (used %d tokens)",
|
|
86
|
+
self.max_tokens, len(kept), len(hits), used_tokens,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return kept
|
|
90
|
+
|
|
91
|
+
def format_for_prompt(self, hits: List[MemoryHit]) -> str:
|
|
92
|
+
"""
|
|
93
|
+
Format a list of MemoryHit objects into a system prompt block.
|
|
94
|
+
|
|
95
|
+
Returns an empty string if no hits.
|
|
96
|
+
"""
|
|
97
|
+
if not hits:
|
|
98
|
+
return ""
|
|
99
|
+
|
|
100
|
+
lines = ["[MEMORY CONTEXT — Pressure-Driven Memory]"]
|
|
101
|
+
for i, hit in enumerate(hits, 1):
|
|
102
|
+
tags = ", ".join(hit.intent_tags) if hit.intent_tags else hit.domain
|
|
103
|
+
lines.append(
|
|
104
|
+
f"{i}. [{hit.drawer}] {hit.text} "
|
|
105
|
+
f"(relevance: {hit.p_effective:.0f}/100, tags: {tags})"
|
|
106
|
+
)
|
|
107
|
+
lines.append("[END MEMORY CONTEXT]")
|
|
108
|
+
return "\n".join(lines)
|
|
109
|
+
|
|
110
|
+
def count_tokens(self, text: str) -> int:
|
|
111
|
+
"""Estimate token count for a string."""
|
|
112
|
+
if self._encoder is not None:
|
|
113
|
+
try:
|
|
114
|
+
return len(self._encoder.encode(text))
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
# Fallback: character approximation
|
|
118
|
+
return max(1, int(len(text) / self._chars_per_token))
|
|
119
|
+
|
|
120
|
+
# ------------------------------------------------------------------
|
|
121
|
+
# Private
|
|
122
|
+
# ------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def _load_encoder(model: str) -> Optional[object]:
|
|
126
|
+
try:
|
|
127
|
+
import tiktoken
|
|
128
|
+
try:
|
|
129
|
+
return tiktoken.encoding_for_model(model)
|
|
130
|
+
except KeyError:
|
|
131
|
+
return tiktoken.get_encoding("cl100k_base")
|
|
132
|
+
except ImportError:
|
|
133
|
+
logger.debug("[CTX] tiktoken not installed; using char approximation")
|
|
134
|
+
return None
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gemini Adapter
|
|
3
|
+
|
|
4
|
+
Wraps Google Gemini client for automatic PDM memory injection and saving.
|
|
5
|
+
Supports both the new google-genai Client and legacy google-generativeai GenerativeModel.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from pdm_memory.integrations import wrap_gemini
|
|
9
|
+
from pdm_memory import Memory
|
|
10
|
+
from google import genai
|
|
11
|
+
|
|
12
|
+
mem = Memory(store="./my_app.db")
|
|
13
|
+
client = genai.Client(api_key="...")
|
|
14
|
+
wrapped_client = wrap_gemini(client, memory=mem)
|
|
15
|
+
reply = wrapped_client.chat("What are my preferences?")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from typing import Any, Optional
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PDMGeminiClient:
|
|
27
|
+
"""
|
|
28
|
+
Memory-augmented Gemini client.
|
|
29
|
+
|
|
30
|
+
All chat() calls automatically:
|
|
31
|
+
- Recall relevant memories before the API call.
|
|
32
|
+
- Inject them into the system instruction/prompt.
|
|
33
|
+
- Save both user message and AI reply as new memories.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
gemini_client: Any,
|
|
39
|
+
memory: Any, # pdm_memory.Memory instance
|
|
40
|
+
model: str = "gemini-2.5-flash",
|
|
41
|
+
max_memory_tokens: int = 1500,
|
|
42
|
+
recall_k: int = 5,
|
|
43
|
+
auto_save: bool = True,
|
|
44
|
+
system_prompt: str = "You are a helpful AI assistant.",
|
|
45
|
+
) -> None:
|
|
46
|
+
self._client = gemini_client
|
|
47
|
+
self._memory = memory
|
|
48
|
+
self._model = model
|
|
49
|
+
self._max_memory_tokens = max_memory_tokens
|
|
50
|
+
self._recall_k = recall_k
|
|
51
|
+
self._auto_save = auto_save
|
|
52
|
+
self._system_prompt = system_prompt
|
|
53
|
+
|
|
54
|
+
from pdm_memory.integrations.context_manager import ContextWindowManager
|
|
55
|
+
self._ctx_manager = ContextWindowManager(
|
|
56
|
+
max_tokens=max_memory_tokens,
|
|
57
|
+
model="gpt-4o-mini", # Use cl100k_base approximation
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def chat(
|
|
61
|
+
self,
|
|
62
|
+
message: str,
|
|
63
|
+
model: Optional[str] = None,
|
|
64
|
+
system_prompt: Optional[str] = None,
|
|
65
|
+
recall_k: Optional[int] = None,
|
|
66
|
+
save_reply: Optional[bool] = None,
|
|
67
|
+
**gemini_kwargs: Any,
|
|
68
|
+
) -> str:
|
|
69
|
+
"""
|
|
70
|
+
Send a message to Gemini with automatic PDM memory injection.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
message: The user's message.
|
|
74
|
+
model: Override the default model name.
|
|
75
|
+
system_prompt: Override the base system prompt.
|
|
76
|
+
recall_k: Override the number of memories to recall.
|
|
77
|
+
save_reply: Override whether to save this exchange to memory.
|
|
78
|
+
**gemini_kwargs: Passed through to generate_content API calls.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
The assistant's reply text.
|
|
82
|
+
"""
|
|
83
|
+
k = recall_k if recall_k is not None else self._recall_k
|
|
84
|
+
should_save = save_reply if save_reply is not None else self._auto_save
|
|
85
|
+
base_system = system_prompt or self._system_prompt
|
|
86
|
+
|
|
87
|
+
# Step 1: Recall relevant memories
|
|
88
|
+
hits = self._memory.recall(query=message, k=k)
|
|
89
|
+
trimmed = self._ctx_manager.fit(hits)
|
|
90
|
+
|
|
91
|
+
# Step 2: Build system message with memory block
|
|
92
|
+
memory_block = self._ctx_manager.format_for_prompt(trimmed)
|
|
93
|
+
full_system = base_system
|
|
94
|
+
if memory_block:
|
|
95
|
+
full_system = f"{base_system}\n\n{memory_block}"
|
|
96
|
+
|
|
97
|
+
model_name = model or self._model
|
|
98
|
+
|
|
99
|
+
# Step 3: Call Gemini API
|
|
100
|
+
if hasattr(self._client, "models") and hasattr(self._client.models, "generate_content"):
|
|
101
|
+
# New google-genai Client SDK
|
|
102
|
+
config = gemini_kwargs.pop("config", None)
|
|
103
|
+
if config is None:
|
|
104
|
+
try:
|
|
105
|
+
from google.genai import types
|
|
106
|
+
config = types.GenerateContentConfig(system_instruction=full_system)
|
|
107
|
+
except ImportError:
|
|
108
|
+
config = {"system_instruction": full_system}
|
|
109
|
+
else:
|
|
110
|
+
if hasattr(config, "system_instruction"):
|
|
111
|
+
config.system_instruction = full_system
|
|
112
|
+
elif isinstance(config, dict):
|
|
113
|
+
config["system_instruction"] = full_system
|
|
114
|
+
|
|
115
|
+
response = self._client.models.generate_content(
|
|
116
|
+
model=model_name,
|
|
117
|
+
contents=message,
|
|
118
|
+
config=config,
|
|
119
|
+
**gemini_kwargs,
|
|
120
|
+
)
|
|
121
|
+
reply_text = response.text or ""
|
|
122
|
+
elif hasattr(self._client, "generate_content"):
|
|
123
|
+
# Legacy google-generativeai SDK (GenerativeModel object)
|
|
124
|
+
# Recreate model with dynamic system_instruction or prepend to content
|
|
125
|
+
try:
|
|
126
|
+
import google.generativeai as genai
|
|
127
|
+
temp_model = genai.GenerativeModel(
|
|
128
|
+
model_name=getattr(self._client, "model_name", model_name),
|
|
129
|
+
system_instruction=full_system,
|
|
130
|
+
)
|
|
131
|
+
response = temp_model.generate_content(
|
|
132
|
+
contents=message,
|
|
133
|
+
**gemini_kwargs,
|
|
134
|
+
)
|
|
135
|
+
reply_text = response.text or ""
|
|
136
|
+
except ImportError:
|
|
137
|
+
# Fallback: prepend system prompt as context if package is not importable
|
|
138
|
+
# (unlikely if legacy client was passed)
|
|
139
|
+
contents_with_ctx = f"System Instruction:\n{full_system}\n\nUser Input:\n{message}"
|
|
140
|
+
response = self._client.generate_content(
|
|
141
|
+
contents=contents_with_ctx,
|
|
142
|
+
**gemini_kwargs,
|
|
143
|
+
)
|
|
144
|
+
reply_text = response.text or ""
|
|
145
|
+
else:
|
|
146
|
+
raise TypeError(
|
|
147
|
+
"Unsupported Gemini client type. Expected a Client from google-genai "
|
|
148
|
+
"or GenerativeModel from google-generativeai."
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Step 4: Save this turn to memory
|
|
152
|
+
if should_save:
|
|
153
|
+
self._save_turn(message, reply_text)
|
|
154
|
+
|
|
155
|
+
logger.debug(
|
|
156
|
+
"[PDM-Gemini] chat() | recalled=%d injected=%d model=%s",
|
|
157
|
+
len(hits), len(trimmed), model_name,
|
|
158
|
+
)
|
|
159
|
+
return reply_text
|
|
160
|
+
|
|
161
|
+
def _save_turn(self, user_msg: str, assistant_reply: str) -> None:
|
|
162
|
+
"""Save user message and assistant reply as memories."""
|
|
163
|
+
try:
|
|
164
|
+
if user_msg.strip():
|
|
165
|
+
self._memory.save(
|
|
166
|
+
text=user_msg[:500],
|
|
167
|
+
source="gemini_chat",
|
|
168
|
+
tags=["conversation", "user_input"],
|
|
169
|
+
p_magnitude=40.0,
|
|
170
|
+
)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.warning("[PDM-Gemini] Failed to save user message: %s", e)
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
if assistant_reply.strip():
|
|
176
|
+
self._memory.save(
|
|
177
|
+
text=assistant_reply[:500],
|
|
178
|
+
source="gemini_chat",
|
|
179
|
+
tags=["conversation", "ai_reply"],
|
|
180
|
+
p_magnitude=35.0,
|
|
181
|
+
)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.warning("[PDM-Gemini] Failed to save assistant reply: %s", e)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def wrap_gemini(
|
|
187
|
+
client: Any,
|
|
188
|
+
memory: Any,
|
|
189
|
+
model: str = "gemini-2.5-flash",
|
|
190
|
+
max_memory_tokens: int = 1500,
|
|
191
|
+
recall_k: int = 5,
|
|
192
|
+
system_prompt: str = "You are a helpful AI assistant.",
|
|
193
|
+
) -> PDMGeminiClient:
|
|
194
|
+
"""
|
|
195
|
+
Create a memory-augmented Gemini client.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
client: google-genai Client or google-generativeai GenerativeModel instance.
|
|
199
|
+
memory: pdm_memory.Memory instance.
|
|
200
|
+
model: Default model (default: gemini-2.5-flash).
|
|
201
|
+
max_memory_tokens: Token budget for injected memories.
|
|
202
|
+
recall_k: Number of memories to recall per turn.
|
|
203
|
+
system_prompt: Base system prompt.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
PDMGeminiClient wrapping the provided client.
|
|
207
|
+
"""
|
|
208
|
+
return PDMGeminiClient(
|
|
209
|
+
gemini_client=client,
|
|
210
|
+
memory=memory,
|
|
211
|
+
model=model,
|
|
212
|
+
max_memory_tokens=max_memory_tokens,
|
|
213
|
+
recall_k=recall_k,
|
|
214
|
+
system_prompt=system_prompt,
|
|
215
|
+
)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Groq Adapter
|
|
3
|
+
|
|
4
|
+
Wraps Groq client for automatic PDM memory injection and saving.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from pdm_memory.integrations import wrap_groq
|
|
8
|
+
from pdm_memory import Memory
|
|
9
|
+
from groq import Groq
|
|
10
|
+
|
|
11
|
+
mem = Memory(store="./my_app.db")
|
|
12
|
+
client = Groq(api_key="gsk_...")
|
|
13
|
+
wrapped_client = wrap_groq(client, memory=mem)
|
|
14
|
+
reply = wrapped_client.chat("What are my preferences?")
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Any, Optional
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PDMGroqClient:
|
|
26
|
+
"""
|
|
27
|
+
Memory-augmented Groq client.
|
|
28
|
+
|
|
29
|
+
All chat() calls automatically:
|
|
30
|
+
- Recall relevant memories before the API call.
|
|
31
|
+
- Inject them into the system prompt.
|
|
32
|
+
- Save both user message and AI reply as new memories.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
groq_client: Any,
|
|
38
|
+
memory: Any, # pdm_memory.Memory instance
|
|
39
|
+
model: str = "llama-3.1-70b-versatile",
|
|
40
|
+
max_memory_tokens: int = 1500,
|
|
41
|
+
recall_k: int = 5,
|
|
42
|
+
auto_save: bool = True,
|
|
43
|
+
system_prompt: str = "You are a helpful AI assistant.",
|
|
44
|
+
) -> None:
|
|
45
|
+
self._client = groq_client
|
|
46
|
+
self._memory = memory
|
|
47
|
+
self._model = model
|
|
48
|
+
self._max_memory_tokens = max_memory_tokens
|
|
49
|
+
self._recall_k = recall_k
|
|
50
|
+
self._auto_save = auto_save
|
|
51
|
+
self._system_prompt = system_prompt
|
|
52
|
+
|
|
53
|
+
from pdm_memory.integrations.context_manager import ContextWindowManager
|
|
54
|
+
self._ctx_manager = ContextWindowManager(
|
|
55
|
+
max_tokens=max_memory_tokens,
|
|
56
|
+
model="gpt-4o-mini", # Use cl100k_base approximation
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def chat(
|
|
60
|
+
self,
|
|
61
|
+
message: str,
|
|
62
|
+
model: Optional[str] = None,
|
|
63
|
+
system_prompt: Optional[str] = None,
|
|
64
|
+
recall_k: Optional[int] = None,
|
|
65
|
+
save_reply: Optional[bool] = None,
|
|
66
|
+
**groq_kwargs: Any,
|
|
67
|
+
) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Send a message to Groq with automatic PDM memory injection.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
message: The user's message.
|
|
73
|
+
model: Override the default model.
|
|
74
|
+
system_prompt: Override the base system prompt.
|
|
75
|
+
recall_k: Override the number of memories to recall.
|
|
76
|
+
save_reply: Override whether to save this exchange to memory.
|
|
77
|
+
**groq_kwargs: Passed through to groq.chat.completions.create().
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The assistant's reply text.
|
|
81
|
+
"""
|
|
82
|
+
k = recall_k if recall_k is not None else self._recall_k
|
|
83
|
+
should_save = save_reply if save_reply is not None else self._auto_save
|
|
84
|
+
base_system = system_prompt or self._system_prompt
|
|
85
|
+
|
|
86
|
+
# Step 1: Recall relevant memories
|
|
87
|
+
hits = self._memory.recall(query=message, k=k)
|
|
88
|
+
trimmed = self._ctx_manager.fit(hits)
|
|
89
|
+
|
|
90
|
+
# Step 2: Build system message with memory block
|
|
91
|
+
memory_block = self._ctx_manager.format_for_prompt(trimmed)
|
|
92
|
+
full_system = base_system
|
|
93
|
+
if memory_block:
|
|
94
|
+
full_system = f"{base_system}\n\n{memory_block}"
|
|
95
|
+
|
|
96
|
+
# Step 3: Call Groq
|
|
97
|
+
response = self._client.chat.completions.create(
|
|
98
|
+
model=model or self._model,
|
|
99
|
+
messages=[
|
|
100
|
+
{"role": "system", "content": full_system},
|
|
101
|
+
{"role": "user", "content": message},
|
|
102
|
+
],
|
|
103
|
+
**groq_kwargs,
|
|
104
|
+
)
|
|
105
|
+
reply_text = response.choices[0].message.content or ""
|
|
106
|
+
|
|
107
|
+
# Step 4: Save this turn to memory
|
|
108
|
+
if should_save:
|
|
109
|
+
self._save_turn(message, reply_text)
|
|
110
|
+
|
|
111
|
+
logger.debug(
|
|
112
|
+
"[PDM-Groq] chat() | recalled=%d injected=%d model=%s",
|
|
113
|
+
len(hits), len(trimmed), model or self._model,
|
|
114
|
+
)
|
|
115
|
+
return reply_text
|
|
116
|
+
|
|
117
|
+
def _save_turn(self, user_msg: str, assistant_reply: str) -> None:
|
|
118
|
+
"""Save user message and assistant reply as memories."""
|
|
119
|
+
try:
|
|
120
|
+
if user_msg.strip():
|
|
121
|
+
self._memory.save(
|
|
122
|
+
text=user_msg[:500],
|
|
123
|
+
source="groq_chat",
|
|
124
|
+
tags=["conversation", "user_input"],
|
|
125
|
+
p_magnitude=40.0,
|
|
126
|
+
)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.warning("[PDM-Groq] Failed to save user message: %s", e)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
if assistant_reply.strip():
|
|
132
|
+
self._memory.save(
|
|
133
|
+
text=assistant_reply[:500],
|
|
134
|
+
source="groq_chat",
|
|
135
|
+
tags=["conversation", "ai_reply"],
|
|
136
|
+
p_magnitude=35.0,
|
|
137
|
+
)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.warning("[PDM-Groq] Failed to save assistant reply: %s", e)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def wrap_groq(
|
|
143
|
+
client: Any,
|
|
144
|
+
memory: Any,
|
|
145
|
+
model: str = "llama-3.1-70b-versatile",
|
|
146
|
+
max_memory_tokens: int = 1500,
|
|
147
|
+
recall_k: int = 5,
|
|
148
|
+
system_prompt: str = "You are a helpful AI assistant.",
|
|
149
|
+
) -> PDMGroqClient:
|
|
150
|
+
"""
|
|
151
|
+
Create a memory-augmented Groq client.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
client: groq.Groq client instance.
|
|
155
|
+
memory: pdm_memory.Memory instance.
|
|
156
|
+
model: Default model (default: llama-3.1-70b-versatile).
|
|
157
|
+
max_memory_tokens: Token budget for injected memories.
|
|
158
|
+
recall_k: Number of memories to recall per turn.
|
|
159
|
+
system_prompt: Base system prompt.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
PDMGroqClient wrapping the provided groq client.
|
|
163
|
+
"""
|
|
164
|
+
return PDMGroqClient(
|
|
165
|
+
groq_client=client,
|
|
166
|
+
memory=memory,
|
|
167
|
+
model=model,
|
|
168
|
+
max_memory_tokens=max_memory_tokens,
|
|
169
|
+
recall_k=recall_k,
|
|
170
|
+
system_prompt=system_prompt,
|
|
171
|
+
)
|