powermem 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.
- powermem/__init__.py +103 -0
- powermem/agent/__init__.py +35 -0
- powermem/agent/abstract/__init__.py +22 -0
- powermem/agent/abstract/collaboration.py +259 -0
- powermem/agent/abstract/context.py +187 -0
- powermem/agent/abstract/manager.py +232 -0
- powermem/agent/abstract/permission.py +217 -0
- powermem/agent/abstract/privacy.py +267 -0
- powermem/agent/abstract/scope.py +199 -0
- powermem/agent/agent.py +791 -0
- powermem/agent/components/__init__.py +18 -0
- powermem/agent/components/collaboration_coordinator.py +645 -0
- powermem/agent/components/permission_controller.py +586 -0
- powermem/agent/components/privacy_protector.py +767 -0
- powermem/agent/components/scope_controller.py +685 -0
- powermem/agent/factories/__init__.py +16 -0
- powermem/agent/factories/agent_factory.py +266 -0
- powermem/agent/factories/config_factory.py +308 -0
- powermem/agent/factories/memory_factory.py +229 -0
- powermem/agent/implementations/__init__.py +16 -0
- powermem/agent/implementations/hybrid.py +728 -0
- powermem/agent/implementations/multi_agent.py +1040 -0
- powermem/agent/implementations/multi_user.py +1020 -0
- powermem/agent/types.py +53 -0
- powermem/agent/wrappers/__init__.py +14 -0
- powermem/agent/wrappers/agent_memory_wrapper.py +427 -0
- powermem/agent/wrappers/compatibility_wrapper.py +520 -0
- powermem/config_loader.py +318 -0
- powermem/configs.py +249 -0
- powermem/core/__init__.py +19 -0
- powermem/core/async_memory.py +1493 -0
- powermem/core/audit.py +258 -0
- powermem/core/base.py +165 -0
- powermem/core/memory.py +1567 -0
- powermem/core/setup.py +162 -0
- powermem/core/telemetry.py +215 -0
- powermem/integrations/__init__.py +17 -0
- powermem/integrations/embeddings/__init__.py +13 -0
- powermem/integrations/embeddings/aws_bedrock.py +100 -0
- powermem/integrations/embeddings/azure_openai.py +55 -0
- powermem/integrations/embeddings/base.py +31 -0
- powermem/integrations/embeddings/config/base.py +132 -0
- powermem/integrations/embeddings/configs.py +31 -0
- powermem/integrations/embeddings/factory.py +48 -0
- powermem/integrations/embeddings/gemini.py +39 -0
- powermem/integrations/embeddings/huggingface.py +41 -0
- powermem/integrations/embeddings/langchain.py +35 -0
- powermem/integrations/embeddings/lmstudio.py +29 -0
- powermem/integrations/embeddings/mock.py +11 -0
- powermem/integrations/embeddings/ollama.py +53 -0
- powermem/integrations/embeddings/openai.py +49 -0
- powermem/integrations/embeddings/qwen.py +102 -0
- powermem/integrations/embeddings/together.py +31 -0
- powermem/integrations/embeddings/vertexai.py +54 -0
- powermem/integrations/llm/__init__.py +18 -0
- powermem/integrations/llm/anthropic.py +87 -0
- powermem/integrations/llm/base.py +132 -0
- powermem/integrations/llm/config/anthropic.py +56 -0
- powermem/integrations/llm/config/azure.py +56 -0
- powermem/integrations/llm/config/base.py +62 -0
- powermem/integrations/llm/config/deepseek.py +56 -0
- powermem/integrations/llm/config/ollama.py +56 -0
- powermem/integrations/llm/config/openai.py +79 -0
- powermem/integrations/llm/config/qwen.py +68 -0
- powermem/integrations/llm/config/qwen_asr.py +46 -0
- powermem/integrations/llm/config/vllm.py +56 -0
- powermem/integrations/llm/configs.py +26 -0
- powermem/integrations/llm/deepseek.py +106 -0
- powermem/integrations/llm/factory.py +118 -0
- powermem/integrations/llm/gemini.py +201 -0
- powermem/integrations/llm/langchain.py +65 -0
- powermem/integrations/llm/ollama.py +106 -0
- powermem/integrations/llm/openai.py +166 -0
- powermem/integrations/llm/openai_structured.py +80 -0
- powermem/integrations/llm/qwen.py +207 -0
- powermem/integrations/llm/qwen_asr.py +171 -0
- powermem/integrations/llm/vllm.py +106 -0
- powermem/integrations/rerank/__init__.py +20 -0
- powermem/integrations/rerank/base.py +43 -0
- powermem/integrations/rerank/config/__init__.py +7 -0
- powermem/integrations/rerank/config/base.py +27 -0
- powermem/integrations/rerank/configs.py +23 -0
- powermem/integrations/rerank/factory.py +68 -0
- powermem/integrations/rerank/qwen.py +159 -0
- powermem/intelligence/__init__.py +17 -0
- powermem/intelligence/ebbinghaus_algorithm.py +354 -0
- powermem/intelligence/importance_evaluator.py +361 -0
- powermem/intelligence/intelligent_memory_manager.py +284 -0
- powermem/intelligence/manager.py +148 -0
- powermem/intelligence/plugin.py +229 -0
- powermem/prompts/__init__.py +29 -0
- powermem/prompts/graph/graph_prompts.py +217 -0
- powermem/prompts/graph/graph_tools_prompts.py +469 -0
- powermem/prompts/importance_evaluation.py +246 -0
- powermem/prompts/intelligent_memory_prompts.py +163 -0
- powermem/prompts/templates.py +193 -0
- powermem/storage/__init__.py +14 -0
- powermem/storage/adapter.py +896 -0
- powermem/storage/base.py +109 -0
- powermem/storage/config/base.py +13 -0
- powermem/storage/config/oceanbase.py +58 -0
- powermem/storage/config/pgvector.py +52 -0
- powermem/storage/config/sqlite.py +27 -0
- powermem/storage/configs.py +159 -0
- powermem/storage/factory.py +59 -0
- powermem/storage/migration_manager.py +438 -0
- powermem/storage/oceanbase/__init__.py +8 -0
- powermem/storage/oceanbase/constants.py +162 -0
- powermem/storage/oceanbase/oceanbase.py +1384 -0
- powermem/storage/oceanbase/oceanbase_graph.py +1441 -0
- powermem/storage/pgvector/__init__.py +7 -0
- powermem/storage/pgvector/pgvector.py +420 -0
- powermem/storage/sqlite/__init__.py +0 -0
- powermem/storage/sqlite/sqlite.py +218 -0
- powermem/storage/sqlite/sqlite_vector_store.py +311 -0
- powermem/utils/__init__.py +35 -0
- powermem/utils/utils.py +605 -0
- powermem/version.py +23 -0
- powermem-0.1.0.dist-info/METADATA +187 -0
- powermem-0.1.0.dist-info/RECORD +123 -0
- powermem-0.1.0.dist-info/WHEEL +5 -0
- powermem-0.1.0.dist-info/licenses/LICENSE +206 -0
- powermem-0.1.0.dist-info/top_level.txt +1 -0
powermem/utils/utils.py
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions and classes
|
|
3
|
+
|
|
4
|
+
This module provides utility functions and helper classes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
import time
|
|
13
|
+
import threading
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def generate_memory_id(content: str, user_id: Optional[str] = None) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Generate a unique memory ID based on content and user.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
content: Memory content
|
|
26
|
+
user_id: User ID
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Unique memory ID
|
|
30
|
+
"""
|
|
31
|
+
data = f"{content}:{user_id}:{datetime.utcnow().isoformat()}"
|
|
32
|
+
return hashlib.md5(data.encode()).hexdigest()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def validate_memory_data(data: Dict[str, Any]) -> bool:
|
|
36
|
+
"""
|
|
37
|
+
Validate memory data structure.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
data: Memory data to validate
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
True if valid, False otherwise
|
|
44
|
+
"""
|
|
45
|
+
required_fields = ["content"]
|
|
46
|
+
|
|
47
|
+
for field in required_fields:
|
|
48
|
+
if field not in data:
|
|
49
|
+
logger.error(f"Missing required field: {field}")
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
if not isinstance(data["content"], str) or not data["content"].strip():
|
|
53
|
+
logger.error("Content must be a non-empty string")
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def sanitize_content(content: str) -> str:
|
|
60
|
+
"""
|
|
61
|
+
Sanitize memory content.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
content: Content to sanitize
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Sanitized content
|
|
68
|
+
"""
|
|
69
|
+
# Remove excessive whitespace
|
|
70
|
+
content = " ".join(content.split())
|
|
71
|
+
|
|
72
|
+
# Remove control characters
|
|
73
|
+
content = "".join(char for char in content if ord(char) >= 32 or char in "\n\t")
|
|
74
|
+
|
|
75
|
+
return content.strip()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def format_memory_for_display(memory: Dict[str, Any]) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Format memory for display.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
memory: Memory data
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Formatted memory string
|
|
87
|
+
"""
|
|
88
|
+
content = memory.get("content", "")
|
|
89
|
+
created_at = memory.get("created_at", "")
|
|
90
|
+
metadata = memory.get("metadata", {})
|
|
91
|
+
|
|
92
|
+
formatted = f"Content: {content}\n"
|
|
93
|
+
if created_at:
|
|
94
|
+
formatted += f"Created: {created_at}\n"
|
|
95
|
+
if metadata:
|
|
96
|
+
formatted += f"Metadata: {json.dumps(metadata, indent=2)}\n"
|
|
97
|
+
|
|
98
|
+
return formatted
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def merge_memories(memories: List[Dict[str, Any]]) -> str:
|
|
102
|
+
"""
|
|
103
|
+
Merge multiple memories into a single string.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
memories: List of memory data
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Merged memory content
|
|
110
|
+
"""
|
|
111
|
+
if not memories:
|
|
112
|
+
return ""
|
|
113
|
+
|
|
114
|
+
merged_content = []
|
|
115
|
+
for memory in memories:
|
|
116
|
+
content = memory.get("content", "")
|
|
117
|
+
if content:
|
|
118
|
+
merged_content.append(content)
|
|
119
|
+
|
|
120
|
+
return "\n\n".join(merged_content)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def calculate_similarity(text1: str, text2: str) -> float:
|
|
124
|
+
"""
|
|
125
|
+
Calculate similarity between two texts.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
text1: First text
|
|
129
|
+
text2: Second text
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Similarity score between 0 and 1
|
|
133
|
+
"""
|
|
134
|
+
# Simple word-based similarity
|
|
135
|
+
words1 = set(text1.lower().split())
|
|
136
|
+
words2 = set(text2.lower().split())
|
|
137
|
+
|
|
138
|
+
if not words1 and not words2:
|
|
139
|
+
return 1.0
|
|
140
|
+
|
|
141
|
+
if not words1 or not words2:
|
|
142
|
+
return 0.0
|
|
143
|
+
|
|
144
|
+
intersection = words1.intersection(words2)
|
|
145
|
+
union = words1.union(words2)
|
|
146
|
+
|
|
147
|
+
return len(intersection) / len(union)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def extract_keywords(text: str, max_keywords: int = 10) -> List[str]:
|
|
151
|
+
"""
|
|
152
|
+
Extract keywords from text.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
text: Text to extract keywords from
|
|
156
|
+
max_keywords: Maximum number of keywords
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
List of keywords
|
|
160
|
+
"""
|
|
161
|
+
# Simple keyword extraction
|
|
162
|
+
words = text.lower().split()
|
|
163
|
+
|
|
164
|
+
# Remove common stop words
|
|
165
|
+
stop_words = {
|
|
166
|
+
"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for",
|
|
167
|
+
"of", "with", "by", "is", "are", "was", "were", "be", "been", "have",
|
|
168
|
+
"has", "had", "do", "does", "did", "will", "would", "could", "should"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
keywords = [word for word in words if word not in stop_words and len(word) > 2]
|
|
172
|
+
|
|
173
|
+
# Count frequency
|
|
174
|
+
word_count = {}
|
|
175
|
+
for word in keywords:
|
|
176
|
+
word_count[word] = word_count.get(word, 0) + 1
|
|
177
|
+
|
|
178
|
+
# Sort by frequency
|
|
179
|
+
sorted_keywords = sorted(word_count.items(), key=lambda x: x[1], reverse=True)
|
|
180
|
+
|
|
181
|
+
return [word for word, count in sorted_keywords[:max_keywords]]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def format_timestamp(timestamp: datetime) -> str:
|
|
185
|
+
"""
|
|
186
|
+
Format timestamp for display.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
timestamp: Timestamp to format
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Formatted timestamp string
|
|
193
|
+
"""
|
|
194
|
+
return timestamp.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def parse_timestamp(timestamp_str: str) -> Optional[datetime]:
|
|
198
|
+
"""
|
|
199
|
+
Parse timestamp string.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
timestamp_str: Timestamp string to parse
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Parsed datetime object or None if invalid
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
return datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
|
|
209
|
+
except ValueError:
|
|
210
|
+
logger.error(f"Failed to parse timestamp: {timestamp_str}")
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def extract_json(text):
|
|
214
|
+
"""
|
|
215
|
+
Extracts JSON content from a string, removing enclosing triple backticks and optional 'json' tag if present.
|
|
216
|
+
If no code block is found, returns the text as-is.
|
|
217
|
+
"""
|
|
218
|
+
text = text.strip()
|
|
219
|
+
match = re.search(r"```(?:json)?\s*(.*?)\s*```", text, re.DOTALL)
|
|
220
|
+
if match:
|
|
221
|
+
json_str = match.group(1)
|
|
222
|
+
else:
|
|
223
|
+
json_str = text # assume it's raw JSON
|
|
224
|
+
return json_str
|
|
225
|
+
|
|
226
|
+
def format_entities(entities):
|
|
227
|
+
if not entities:
|
|
228
|
+
return ""
|
|
229
|
+
|
|
230
|
+
formatted_lines = []
|
|
231
|
+
for entity in entities:
|
|
232
|
+
simplified = f"{entity['source']} -- {entity['relationship']} -- {entity['destination']}"
|
|
233
|
+
formatted_lines.append(simplified)
|
|
234
|
+
|
|
235
|
+
return "\n".join(formatted_lines)
|
|
236
|
+
|
|
237
|
+
def remove_code_blocks(content: str) -> str:
|
|
238
|
+
"""
|
|
239
|
+
Removes enclosing code block markers ```[language] and ``` from a given string.
|
|
240
|
+
|
|
241
|
+
Remarks:
|
|
242
|
+
- The function uses a regex pattern to match code blocks that may start with ``` followed by an optional language tag (letters or numbers) and end with ```.
|
|
243
|
+
- If a code block is detected, it returns only the inner content, stripping out the markers.
|
|
244
|
+
- If no code block markers are found, the original content is returned as-is.
|
|
245
|
+
"""
|
|
246
|
+
pattern = r"^```[a-zA-Z0-9]*\n([\s\S]*?)\n```$"
|
|
247
|
+
match = re.match(pattern, content.strip())
|
|
248
|
+
return match.group(1).strip() if match else content.strip()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def get_image_description(image_obj: Any, llm: Any, vision_details: Any) -> str:
|
|
252
|
+
"""
|
|
253
|
+
- image_obj can be a URL string, or a prebuilt multimodal message (list/dict).
|
|
254
|
+
- vision_details can be "auto" or a dict; when dict we use detail = dict.get("detail", "auto").
|
|
255
|
+
"""
|
|
256
|
+
detail = vision_details
|
|
257
|
+
if isinstance(vision_details, dict):
|
|
258
|
+
detail = vision_details.get("detail", "auto")
|
|
259
|
+
if detail is None:
|
|
260
|
+
detail = "auto"
|
|
261
|
+
|
|
262
|
+
if isinstance(image_obj, str):
|
|
263
|
+
messages = [
|
|
264
|
+
{
|
|
265
|
+
"role": "user",
|
|
266
|
+
"content": [
|
|
267
|
+
{
|
|
268
|
+
"type": "text",
|
|
269
|
+
"text": "A user is providing an image. Provide a high level description of the image and do not include any additional text.",
|
|
270
|
+
},
|
|
271
|
+
{"type": "image_url", "image_url": {"url": image_obj, "detail": detail}},
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
]
|
|
275
|
+
else:
|
|
276
|
+
messages = [image_obj]
|
|
277
|
+
|
|
278
|
+
return llm.generate_response(messages=messages)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _process_content_item(item: Dict[str, Any], role: str, llm: Any, vision_details: Any, audio_llm: Any) -> Optional[str]:
|
|
282
|
+
"""
|
|
283
|
+
Process a single content item and return processed text content.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
item: Content item dict
|
|
287
|
+
role: Message role
|
|
288
|
+
llm: LLM instance for image description
|
|
289
|
+
vision_details: Vision details setting
|
|
290
|
+
audio_llm: Audio LLM instance for transcription
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Processed text content or None if item should be skipped
|
|
294
|
+
"""
|
|
295
|
+
if not isinstance(item, dict):
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
item_type = item.get("type")
|
|
299
|
+
|
|
300
|
+
if item_type == "text":
|
|
301
|
+
text_content = item.get("text", "")
|
|
302
|
+
return text_content if text_content else None
|
|
303
|
+
|
|
304
|
+
elif item_type == "image_url":
|
|
305
|
+
image_url = item.get("image_url", {}).get("url")
|
|
306
|
+
if image_url:
|
|
307
|
+
try:
|
|
308
|
+
description = get_image_description(image_url, llm, vision_details)
|
|
309
|
+
return description if description else None
|
|
310
|
+
except Exception as e:
|
|
311
|
+
raise Exception(f"Error while processing image {image_url}: {e}")
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
elif item_type == "audio":
|
|
315
|
+
if audio_llm is not None:
|
|
316
|
+
audio_content = item.get("content", {})
|
|
317
|
+
audio_url = audio_content.get("audio") if isinstance(audio_content, dict) else None
|
|
318
|
+
if audio_url:
|
|
319
|
+
try:
|
|
320
|
+
transcribed_text = audio_llm.transcribe(audio_url=audio_url)
|
|
321
|
+
return transcribed_text if transcribed_text else None
|
|
322
|
+
except Exception as e:
|
|
323
|
+
logger.error(f"Error while transcribing audio {audio_url}: {e}")
|
|
324
|
+
raise Exception(f"Error while transcribing audio {audio_url}: {e}")
|
|
325
|
+
else:
|
|
326
|
+
logger.warning(f"Audio item found but audio_llm is not configured: {item}")
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
else:
|
|
330
|
+
logger.warning(f"Unknown content type: {item_type}")
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def parse_vision_messages(messages: List[Dict[str, Any]], llm: Any = None, vision_details: Any = "auto", audio_llm: Any = None) -> List[Dict[str, Any]]:
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
Assumes input is already a list of message dicts with 'role' and 'content' fields.
|
|
338
|
+
- Keep system messages unchanged.
|
|
339
|
+
- If message.content is a list (multimodal blocks), call get_image_description and replace content with returned text.
|
|
340
|
+
- If message.content is a dict with type == "image_url", call get_image_description(url, ...) and replace content with returned text.
|
|
341
|
+
- If message.content contains type == "audio", use audio_llm to transcribe audio to text.
|
|
342
|
+
- Otherwise keep the original message (regular text).
|
|
343
|
+
- When llm is None, behave as pass-through for all messages.
|
|
344
|
+
"""
|
|
345
|
+
returned_messages: List[Dict[str, Any]] = []
|
|
346
|
+
for msg in messages:
|
|
347
|
+
if not isinstance(msg, dict) or "role" not in msg or "content" not in msg:
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
if msg["role"] == "system":
|
|
351
|
+
returned_messages.append(msg)
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
# If LLM not provided, passthrough without image description
|
|
355
|
+
if llm is None:
|
|
356
|
+
returned_messages.append(msg)
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
content = msg["content"]
|
|
360
|
+
role = msg["role"]
|
|
361
|
+
|
|
362
|
+
# Normalize content to list format for unified processing
|
|
363
|
+
items_to_process = []
|
|
364
|
+
if isinstance(content, list):
|
|
365
|
+
items_to_process = content
|
|
366
|
+
elif isinstance(content, dict):
|
|
367
|
+
# Handle single dict as image_url or audio
|
|
368
|
+
if content.get("type") in ("image_url", "audio"):
|
|
369
|
+
items_to_process = [content]
|
|
370
|
+
else:
|
|
371
|
+
# Unknown dict format, passthrough
|
|
372
|
+
returned_messages.append(msg)
|
|
373
|
+
continue
|
|
374
|
+
else:
|
|
375
|
+
# Regular text or other content, passthrough
|
|
376
|
+
returned_messages.append(msg)
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
# Process each item
|
|
380
|
+
for item in items_to_process:
|
|
381
|
+
processed_content = _process_content_item(item, role, llm, vision_details, audio_llm)
|
|
382
|
+
if processed_content:
|
|
383
|
+
returned_messages.append({"role": role, "content": processed_content})
|
|
384
|
+
|
|
385
|
+
return returned_messages
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def load_config_from_env() -> Dict[str, Any]:
|
|
389
|
+
"""
|
|
390
|
+
Load configuration from environment variables.
|
|
391
|
+
|
|
392
|
+
.. deprecated:: 0.1.0
|
|
393
|
+
This function is now in :mod:`mem.config_loader`.
|
|
394
|
+
Please use ``from powermem import load_config_from_env`` instead.
|
|
395
|
+
|
|
396
|
+
This is kept for backward compatibility.
|
|
397
|
+
For the actual implementation, see :mod:`mem.config_loader`.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Configuration dictionary built from environment variables
|
|
401
|
+
"""
|
|
402
|
+
# Import here to avoid circular import
|
|
403
|
+
from ..config_loader import load_config_from_env as _load_config_from_env
|
|
404
|
+
return _load_config_from_env()
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def serialize_datetime(value: Any) -> Any:
|
|
408
|
+
"""
|
|
409
|
+
Convert datetime objects to ISO format strings for JSON serialization.
|
|
410
|
+
Recursively handles dictionaries and lists.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
value: Value to serialize (can be datetime, dict, list, or primitive)
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
Serialized value with datetime objects converted to ISO format strings
|
|
417
|
+
"""
|
|
418
|
+
if isinstance(value, datetime):
|
|
419
|
+
return value.isoformat()
|
|
420
|
+
elif isinstance(value, dict):
|
|
421
|
+
return {k: serialize_datetime(v) for k, v in value.items()}
|
|
422
|
+
elif isinstance(value, list):
|
|
423
|
+
return [serialize_datetime(item) for item in value]
|
|
424
|
+
return value
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def convert_config_object_to_dict(obj: Any) -> Any:
|
|
428
|
+
"""
|
|
429
|
+
Recursively convert ConfigObject instances to dictionaries.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
obj: Object to convert (can be ConfigObject, dict, list, or primitive)
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Converted object with all ConfigObjects replaced by dicts
|
|
436
|
+
"""
|
|
437
|
+
if obj is None:
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
# Handle ConfigObject
|
|
441
|
+
if hasattr(obj, 'to_dict'):
|
|
442
|
+
obj = obj.to_dict()
|
|
443
|
+
|
|
444
|
+
# Handle dict
|
|
445
|
+
if isinstance(obj, dict):
|
|
446
|
+
return {key: convert_config_object_to_dict(value) for key, value in obj.items()}
|
|
447
|
+
|
|
448
|
+
# Handle list
|
|
449
|
+
if isinstance(obj, list):
|
|
450
|
+
return [convert_config_object_to_dict(item) for item in obj]
|
|
451
|
+
|
|
452
|
+
# Return primitive types as-is
|
|
453
|
+
return obj
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class SnowflakeIDGenerator:
|
|
457
|
+
"""
|
|
458
|
+
Snowflake ID generator for distributed systems.
|
|
459
|
+
|
|
460
|
+
Generates unique 64-bit IDs using the Snowflake algorithm:
|
|
461
|
+
- 41 bits for timestamp (milliseconds since epoch)
|
|
462
|
+
- 10 bits for machine ID (5 bits datacenter + 5 bits worker)
|
|
463
|
+
- 12 bits for sequence number
|
|
464
|
+
|
|
465
|
+
Thread-safe implementation.
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
# Snowflake parameters
|
|
469
|
+
EPOCH = 1609459200000 # 2021-01-01 00:00:00 UTC in milliseconds
|
|
470
|
+
TIMESTAMP_BITS = 41
|
|
471
|
+
DATACENTER_BITS = 5
|
|
472
|
+
WORKER_BITS = 5
|
|
473
|
+
SEQUENCE_BITS = 12
|
|
474
|
+
|
|
475
|
+
MAX_DATACENTER_ID = (1 << DATACENTER_BITS) - 1 # 31
|
|
476
|
+
MAX_WORKER_ID = (1 << WORKER_BITS) - 1 # 31
|
|
477
|
+
MAX_SEQUENCE = (1 << SEQUENCE_BITS) - 1 # 4095
|
|
478
|
+
|
|
479
|
+
# Bit shifts
|
|
480
|
+
TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_BITS + DATACENTER_BITS
|
|
481
|
+
DATACENTER_SHIFT = SEQUENCE_BITS + WORKER_BITS
|
|
482
|
+
WORKER_SHIFT = SEQUENCE_BITS
|
|
483
|
+
|
|
484
|
+
def __init__(self, datacenter_id: int = 0, worker_id: int = 0):
|
|
485
|
+
"""
|
|
486
|
+
Initialize Snowflake ID generator.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
datacenter_id: Datacenter ID (0-31)
|
|
490
|
+
worker_id: Worker ID (0-31)
|
|
491
|
+
|
|
492
|
+
Raises:
|
|
493
|
+
ValueError: If datacenter_id or worker_id is out of range
|
|
494
|
+
"""
|
|
495
|
+
if datacenter_id < 0 or datacenter_id > self.MAX_DATACENTER_ID:
|
|
496
|
+
raise ValueError(f"Datacenter ID must be between 0 and {self.MAX_DATACENTER_ID}")
|
|
497
|
+
if worker_id < 0 or worker_id > self.MAX_WORKER_ID:
|
|
498
|
+
raise ValueError(f"Worker ID must be between 0 and {self.MAX_WORKER_ID}")
|
|
499
|
+
|
|
500
|
+
self.datacenter_id = datacenter_id
|
|
501
|
+
self.worker_id = worker_id
|
|
502
|
+
self.sequence = 0
|
|
503
|
+
self.last_timestamp = -1
|
|
504
|
+
self._lock = threading.Lock()
|
|
505
|
+
|
|
506
|
+
def _current_timestamp(self) -> int:
|
|
507
|
+
"""Get current timestamp in milliseconds."""
|
|
508
|
+
return int(time.time() * 1000)
|
|
509
|
+
|
|
510
|
+
def _wait_next_millis(self, last_timestamp: int) -> int:
|
|
511
|
+
"""Wait until next millisecond."""
|
|
512
|
+
timestamp = self._current_timestamp()
|
|
513
|
+
while timestamp <= last_timestamp:
|
|
514
|
+
timestamp = self._current_timestamp()
|
|
515
|
+
return timestamp
|
|
516
|
+
|
|
517
|
+
def generate(self) -> int:
|
|
518
|
+
"""
|
|
519
|
+
Generate a new Snowflake ID.
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
64-bit integer ID
|
|
523
|
+
|
|
524
|
+
Raises:
|
|
525
|
+
RuntimeError: If clock moves backwards or sequence overflows
|
|
526
|
+
"""
|
|
527
|
+
with self._lock:
|
|
528
|
+
timestamp = self._current_timestamp()
|
|
529
|
+
|
|
530
|
+
# Handle clock backwards
|
|
531
|
+
if timestamp < self.last_timestamp:
|
|
532
|
+
raise RuntimeError(
|
|
533
|
+
f"Clock moved backwards. Refusing to generate ID for "
|
|
534
|
+
f"{self.last_timestamp - timestamp} milliseconds"
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
# Same millisecond, increment sequence
|
|
538
|
+
if timestamp == self.last_timestamp:
|
|
539
|
+
self.sequence = (self.sequence + 1) & self.MAX_SEQUENCE
|
|
540
|
+
# Sequence overflow, wait for next millisecond
|
|
541
|
+
if self.sequence == 0:
|
|
542
|
+
timestamp = self._wait_next_millis(self.last_timestamp)
|
|
543
|
+
else:
|
|
544
|
+
# New millisecond, reset sequence
|
|
545
|
+
self.sequence = 0
|
|
546
|
+
|
|
547
|
+
self.last_timestamp = timestamp
|
|
548
|
+
|
|
549
|
+
# Generate ID
|
|
550
|
+
return (
|
|
551
|
+
((timestamp - self.EPOCH) << self.TIMESTAMP_SHIFT) |
|
|
552
|
+
(self.datacenter_id << self.DATACENTER_SHIFT) |
|
|
553
|
+
(self.worker_id << self.WORKER_SHIFT) |
|
|
554
|
+
self.sequence
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
def generate_batch(self, count: int) -> List[int]:
|
|
558
|
+
"""
|
|
559
|
+
Generate a batch of Snowflake IDs.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
count: Number of IDs to generate
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
List of 64-bit integer IDs
|
|
566
|
+
"""
|
|
567
|
+
return [self.generate() for _ in range(count)]
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
# Global Snowflake ID generator instance
|
|
571
|
+
# Default to datacenter_id=0, worker_id=0
|
|
572
|
+
# Can be configured via environment variables if needed
|
|
573
|
+
_snowflake_generator: Optional[SnowflakeIDGenerator] = None
|
|
574
|
+
_snowflake_lock = threading.Lock()
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def get_snowflake_generator() -> SnowflakeIDGenerator:
|
|
578
|
+
"""
|
|
579
|
+
Get or create the global Snowflake ID generator instance.
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
Snowflake ID generator instance
|
|
583
|
+
"""
|
|
584
|
+
global _snowflake_generator
|
|
585
|
+
if _snowflake_generator is None:
|
|
586
|
+
with _snowflake_lock:
|
|
587
|
+
if _snowflake_generator is None:
|
|
588
|
+
# Try to get from environment variables
|
|
589
|
+
datacenter_id = int(os.getenv("SNOWFLAKE_DATACENTER_ID", "0"))
|
|
590
|
+
worker_id = int(os.getenv("SNOWFLAKE_WORKER_ID", "0"))
|
|
591
|
+
_snowflake_generator = SnowflakeIDGenerator(
|
|
592
|
+
datacenter_id=datacenter_id,
|
|
593
|
+
worker_id=worker_id
|
|
594
|
+
)
|
|
595
|
+
return _snowflake_generator
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def generate_snowflake_id() -> int:
|
|
599
|
+
"""
|
|
600
|
+
Generate a new Snowflake ID using the global generator.
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
64-bit integer ID
|
|
604
|
+
"""
|
|
605
|
+
return get_snowflake_generator().generate()
|
powermem/version.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Version information management
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
__version_info__ = tuple(map(int, __version__.split(".")))
|
|
7
|
+
|
|
8
|
+
# Version history
|
|
9
|
+
VERSION_HISTORY = {
|
|
10
|
+
"0.1.0": "2025-10-16 - Initial version release",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
def get_version() -> str:
|
|
14
|
+
"""Get current version number"""
|
|
15
|
+
return __version__
|
|
16
|
+
|
|
17
|
+
def get_version_info() -> tuple:
|
|
18
|
+
"""Get version info tuple"""
|
|
19
|
+
return __version_info__
|
|
20
|
+
|
|
21
|
+
def get_version_history() -> dict:
|
|
22
|
+
"""Get version history"""
|
|
23
|
+
return VERSION_HISTORY
|