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.
Files changed (123) hide show
  1. powermem/__init__.py +103 -0
  2. powermem/agent/__init__.py +35 -0
  3. powermem/agent/abstract/__init__.py +22 -0
  4. powermem/agent/abstract/collaboration.py +259 -0
  5. powermem/agent/abstract/context.py +187 -0
  6. powermem/agent/abstract/manager.py +232 -0
  7. powermem/agent/abstract/permission.py +217 -0
  8. powermem/agent/abstract/privacy.py +267 -0
  9. powermem/agent/abstract/scope.py +199 -0
  10. powermem/agent/agent.py +791 -0
  11. powermem/agent/components/__init__.py +18 -0
  12. powermem/agent/components/collaboration_coordinator.py +645 -0
  13. powermem/agent/components/permission_controller.py +586 -0
  14. powermem/agent/components/privacy_protector.py +767 -0
  15. powermem/agent/components/scope_controller.py +685 -0
  16. powermem/agent/factories/__init__.py +16 -0
  17. powermem/agent/factories/agent_factory.py +266 -0
  18. powermem/agent/factories/config_factory.py +308 -0
  19. powermem/agent/factories/memory_factory.py +229 -0
  20. powermem/agent/implementations/__init__.py +16 -0
  21. powermem/agent/implementations/hybrid.py +728 -0
  22. powermem/agent/implementations/multi_agent.py +1040 -0
  23. powermem/agent/implementations/multi_user.py +1020 -0
  24. powermem/agent/types.py +53 -0
  25. powermem/agent/wrappers/__init__.py +14 -0
  26. powermem/agent/wrappers/agent_memory_wrapper.py +427 -0
  27. powermem/agent/wrappers/compatibility_wrapper.py +520 -0
  28. powermem/config_loader.py +318 -0
  29. powermem/configs.py +249 -0
  30. powermem/core/__init__.py +19 -0
  31. powermem/core/async_memory.py +1493 -0
  32. powermem/core/audit.py +258 -0
  33. powermem/core/base.py +165 -0
  34. powermem/core/memory.py +1567 -0
  35. powermem/core/setup.py +162 -0
  36. powermem/core/telemetry.py +215 -0
  37. powermem/integrations/__init__.py +17 -0
  38. powermem/integrations/embeddings/__init__.py +13 -0
  39. powermem/integrations/embeddings/aws_bedrock.py +100 -0
  40. powermem/integrations/embeddings/azure_openai.py +55 -0
  41. powermem/integrations/embeddings/base.py +31 -0
  42. powermem/integrations/embeddings/config/base.py +132 -0
  43. powermem/integrations/embeddings/configs.py +31 -0
  44. powermem/integrations/embeddings/factory.py +48 -0
  45. powermem/integrations/embeddings/gemini.py +39 -0
  46. powermem/integrations/embeddings/huggingface.py +41 -0
  47. powermem/integrations/embeddings/langchain.py +35 -0
  48. powermem/integrations/embeddings/lmstudio.py +29 -0
  49. powermem/integrations/embeddings/mock.py +11 -0
  50. powermem/integrations/embeddings/ollama.py +53 -0
  51. powermem/integrations/embeddings/openai.py +49 -0
  52. powermem/integrations/embeddings/qwen.py +102 -0
  53. powermem/integrations/embeddings/together.py +31 -0
  54. powermem/integrations/embeddings/vertexai.py +54 -0
  55. powermem/integrations/llm/__init__.py +18 -0
  56. powermem/integrations/llm/anthropic.py +87 -0
  57. powermem/integrations/llm/base.py +132 -0
  58. powermem/integrations/llm/config/anthropic.py +56 -0
  59. powermem/integrations/llm/config/azure.py +56 -0
  60. powermem/integrations/llm/config/base.py +62 -0
  61. powermem/integrations/llm/config/deepseek.py +56 -0
  62. powermem/integrations/llm/config/ollama.py +56 -0
  63. powermem/integrations/llm/config/openai.py +79 -0
  64. powermem/integrations/llm/config/qwen.py +68 -0
  65. powermem/integrations/llm/config/qwen_asr.py +46 -0
  66. powermem/integrations/llm/config/vllm.py +56 -0
  67. powermem/integrations/llm/configs.py +26 -0
  68. powermem/integrations/llm/deepseek.py +106 -0
  69. powermem/integrations/llm/factory.py +118 -0
  70. powermem/integrations/llm/gemini.py +201 -0
  71. powermem/integrations/llm/langchain.py +65 -0
  72. powermem/integrations/llm/ollama.py +106 -0
  73. powermem/integrations/llm/openai.py +166 -0
  74. powermem/integrations/llm/openai_structured.py +80 -0
  75. powermem/integrations/llm/qwen.py +207 -0
  76. powermem/integrations/llm/qwen_asr.py +171 -0
  77. powermem/integrations/llm/vllm.py +106 -0
  78. powermem/integrations/rerank/__init__.py +20 -0
  79. powermem/integrations/rerank/base.py +43 -0
  80. powermem/integrations/rerank/config/__init__.py +7 -0
  81. powermem/integrations/rerank/config/base.py +27 -0
  82. powermem/integrations/rerank/configs.py +23 -0
  83. powermem/integrations/rerank/factory.py +68 -0
  84. powermem/integrations/rerank/qwen.py +159 -0
  85. powermem/intelligence/__init__.py +17 -0
  86. powermem/intelligence/ebbinghaus_algorithm.py +354 -0
  87. powermem/intelligence/importance_evaluator.py +361 -0
  88. powermem/intelligence/intelligent_memory_manager.py +284 -0
  89. powermem/intelligence/manager.py +148 -0
  90. powermem/intelligence/plugin.py +229 -0
  91. powermem/prompts/__init__.py +29 -0
  92. powermem/prompts/graph/graph_prompts.py +217 -0
  93. powermem/prompts/graph/graph_tools_prompts.py +469 -0
  94. powermem/prompts/importance_evaluation.py +246 -0
  95. powermem/prompts/intelligent_memory_prompts.py +163 -0
  96. powermem/prompts/templates.py +193 -0
  97. powermem/storage/__init__.py +14 -0
  98. powermem/storage/adapter.py +896 -0
  99. powermem/storage/base.py +109 -0
  100. powermem/storage/config/base.py +13 -0
  101. powermem/storage/config/oceanbase.py +58 -0
  102. powermem/storage/config/pgvector.py +52 -0
  103. powermem/storage/config/sqlite.py +27 -0
  104. powermem/storage/configs.py +159 -0
  105. powermem/storage/factory.py +59 -0
  106. powermem/storage/migration_manager.py +438 -0
  107. powermem/storage/oceanbase/__init__.py +8 -0
  108. powermem/storage/oceanbase/constants.py +162 -0
  109. powermem/storage/oceanbase/oceanbase.py +1384 -0
  110. powermem/storage/oceanbase/oceanbase_graph.py +1441 -0
  111. powermem/storage/pgvector/__init__.py +7 -0
  112. powermem/storage/pgvector/pgvector.py +420 -0
  113. powermem/storage/sqlite/__init__.py +0 -0
  114. powermem/storage/sqlite/sqlite.py +218 -0
  115. powermem/storage/sqlite/sqlite_vector_store.py +311 -0
  116. powermem/utils/__init__.py +35 -0
  117. powermem/utils/utils.py +605 -0
  118. powermem/version.py +23 -0
  119. powermem-0.1.0.dist-info/METADATA +187 -0
  120. powermem-0.1.0.dist-info/RECORD +123 -0
  121. powermem-0.1.0.dist-info/WHEEL +5 -0
  122. powermem-0.1.0.dist-info/licenses/LICENSE +206 -0
  123. powermem-0.1.0.dist-info/top_level.txt +1 -0
@@ -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