memini-ai-dev 0.2.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 (42) hide show
  1. memini_ai/__init__.py +3 -0
  2. memini_ai/config.py +394 -0
  3. memini_ai/decay.py +811 -0
  4. memini_ai/dialectic.py +1103 -0
  5. memini_ai/entity_extractor.py +439 -0
  6. memini_ai/extractor.py +281 -0
  7. memini_ai/graph.py +323 -0
  8. memini_ai/indexer/__init__.py +47 -0
  9. memini_ai/indexer/chunker.py +460 -0
  10. memini_ai/indexer/constants.py +186 -0
  11. memini_ai/indexer/file_tracker.py +211 -0
  12. memini_ai/indexer/indexer.py +402 -0
  13. memini_ai/indexer/pause_controller.py +89 -0
  14. memini_ai/indexer/snapshot.py +192 -0
  15. memini_ai/indexer/watcher.py +217 -0
  16. memini_ai/knowledge_graph.py +1355 -0
  17. memini_ai/main.py +52 -0
  18. memini_ai/memory/__init__.py +32 -0
  19. memini_ai/memory/database.py +1095 -0
  20. memini_ai/memory/schema.py +305 -0
  21. memini_ai/memory/search.py +486 -0
  22. memini_ai/memory/system.py +530 -0
  23. memini_ai/model/__init__.py +15 -0
  24. memini_ai/model/embeddings.py +106 -0
  25. memini_ai/model/manager.py +199 -0
  26. memini_ai/multi_peer.py +861 -0
  27. memini_ai/postgres/__init__.py +5 -0
  28. memini_ai/postgres/database.py +593 -0
  29. memini_ai/postgres/queries.py +256 -0
  30. memini_ai/postgres/schema.py +288 -0
  31. memini_ai/precompress.py +135 -0
  32. memini_ai/server.py +2383 -0
  33. memini_ai/tiered_loader.py +557 -0
  34. memini_ai/trust_engine.py +299 -0
  35. memini_ai/user_model.py +543 -0
  36. memini_ai/utils/__init__.py +6 -0
  37. memini_ai/utils/hash.py +43 -0
  38. memini_ai/utils/logger.py +50 -0
  39. memini_ai_dev-0.2.0.dist-info/METADATA +370 -0
  40. memini_ai_dev-0.2.0.dist-info/RECORD +42 -0
  41. memini_ai_dev-0.2.0.dist-info/WHEEL +4 -0
  42. memini_ai_dev-0.2.0.dist-info/entry_points.txt +2 -0
memini_ai/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Memini-ai - Local-first semantic memory server."""
2
+
3
+ __version__ = "3.0.0"
memini_ai/config.py ADDED
@@ -0,0 +1,394 @@
1
+ """Configuration management using pydantic-settings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ from pathlib import Path
9
+
10
+ from pydantic import Field, field_validator
11
+ from pydantic_settings import BaseSettings, SettingsConfigDict
12
+
13
+
14
+ class MeminiConfig(BaseSettings):
15
+ """Main configuration class for memini-ai.
16
+
17
+ Configuration priority: env vars > JSON config file > defaults.
18
+ JSON config path: .opencode/memini-ai/config.json (auto-created if missing).
19
+ """
20
+
21
+ model_config = SettingsConfigDict(
22
+ env_prefix="MEMINI_",
23
+ env_file=".env",
24
+ env_file_encoding="utf-8",
25
+ extra="ignore",
26
+ )
27
+
28
+ # Model settings
29
+ precision: str = "fp16"
30
+ device: str = "auto"
31
+ use_gpu: bool = False
32
+ embedding_dim: int = 1024
33
+ batch_size: int = 32
34
+ eager_load: bool = False
35
+
36
+ # Database settings
37
+ qdrant_url: str = "http://localhost:6333"
38
+ table_name: str = "memories"
39
+ project_id: str | None = None
40
+ query_collections: list[str] | None = None
41
+
42
+ # PostgreSQL / pgvector settings
43
+ db_url: str = "" # e.g., "postgresql://postgres:password@localhost:5434/postgres"
44
+ db_pool_size: int = 10
45
+ db_min_size: int = 2
46
+ db_max_size: int = 20
47
+
48
+ # Indexer settings
49
+ chunk_size: int = 512
50
+ chunk_overlap: int = 50
51
+ max_file_size: int = Field(default_factory=lambda: 10 * 1024 * 1024)
52
+ exclude_patterns: list[str] = Field(
53
+ default_factory=lambda: ["node_modules", ".git", "dist"]
54
+ )
55
+
56
+ # Logging
57
+ log_level: str = "info"
58
+
59
+ # Performance
60
+ qdrant_max_retries: int = 3
61
+ qdrant_retry_delay_ms: int = 1000
62
+ workers: int = Field(default_factory=lambda: os.cpu_count() or 4)
63
+
64
+ # Trust Engine settings
65
+ trust_engine_enabled: bool = Field(default=False, alias="TRUST_ENGINE")
66
+ trust_threshold_archive: float = Field(default=0.2, alias="TRUST_THRESHOLD_ARCHIVE")
67
+ trust_threshold_promote: float = Field(default=0.8, alias="TRUST_THRESHOLD_PROMOTE")
68
+ trust_delta_use: float = Field(default=0.05, alias="TRUST_DELTA_USE")
69
+ trust_delta_ignore: float = Field(default=-0.02, alias="TRUST_DELTA_IGNORED")
70
+ trust_delta_correct: float = Field(default=-0.15, alias="TRUST_DELTA_CORRECT")
71
+ trust_delta_confirm: float = Field(default=0.10, alias="TRUST_DELTA_CONFIRM")
72
+
73
+ # Memory Graph settings
74
+ memory_graph_enabled: bool = Field(default=False, alias="MEMORY_GRAPH")
75
+ graph_entity_extraction: bool = Field(default=True, alias="GRAPH_ENTITY_EXTRACTION")
76
+ graph_relationship_suggestions: bool = Field(
77
+ default=True, alias="GRAPH_RELATIONSHIP_SUGGESTIONS"
78
+ )
79
+
80
+ # Auto-Extract settings
81
+ auto_extract_enabled: bool = Field(default=False, alias="AUTO_EXTRACT")
82
+ auto_extract_turns: int = Field(default=5, alias="AUTO_EXTRACT_TURNS")
83
+ llm_url: str = Field(default="http://localhost:11434/api/generate", alias="LLM_URL")
84
+ llm_model: str = Field(default="llama3.2", alias="LLM_MODEL")
85
+
86
+ # Pre-Compression Extraction settings
87
+ precompress_enabled: bool = Field(default=False, alias="PRECOMPRESS")
88
+ precompress_threshold: float = Field(default=0.8, alias="PRECOMPRESS_THRESHOLD")
89
+
90
+ # Tiered Loading settings
91
+ tiered_loading_enabled: bool = Field(default=False, alias="TIERED_LOADING")
92
+ tier0_max_tokens: int = Field(default=100, alias="TIER0_MAX_TOKENS")
93
+ tier1_max_tokens: int = Field(default=2000, alias="TIER1_MAX_TOKENS")
94
+ tier0_cache_ttl: int = Field(default=3600, alias="TIER0_CACHE_TTL") # seconds
95
+ tier1_cache_ttl: int = Field(default=7200, alias="TIER1_CACHE_TTL") # seconds
96
+
97
+ # User Modeling settings
98
+ user_modeling_enabled: bool = Field(default=False, alias="USER_MODELING")
99
+ user_model_min_sessions: int = Field(default=50, alias="USER_MODEL_MIN_SESSIONS")
100
+
101
+ # Phase 4A: Memory Decay settings
102
+ decay_enabled: bool = Field(default=False, alias="DECAY_ENABLED")
103
+ decay_half_life_days: int = Field(default=90, alias="DECAY_HALF_LIFE_DAYS")
104
+ consolidation_interval_hours: int = Field(default=168, alias="CONSOLIDATION_INTERVAL_HOURS")
105
+ consolidation_similarity_threshold: float = Field(default=0.92, alias="CONSOLIDATION_SIMILARITY_THRESHOLD")
106
+
107
+ # Phase 4B: Knowledge Graph settings
108
+ knowledge_graph_enabled: bool = Field(default=False, alias="KG_ENABLED")
109
+ kg_entity_extraction: bool = Field(default=True, alias="KG_ENTITY_EXTRACTION")
110
+ kg_inference_depth: int = Field(default=3, alias="KG_INFERENCE_DEPTH")
111
+ kg_max_results: int = Field(default=100, alias="KG_MAX_RESULTS")
112
+
113
+ # Phase 4C: Multi-Peer settings
114
+ multi_peer_enabled: bool = Field(default=False, alias="MULTI_PEER_ENABLED")
115
+ multi_peer_allow_guest_sharing: bool = Field(default=True, alias="MULTI_PEER_GUEST_SHARING")
116
+
117
+ # Phase 4D: Dialectic settings
118
+ dialectic_enabled: bool = Field(default=False, alias="DIALECTIC_ENABLED")
119
+ dialectic_llm_provider: str = Field(default="ollama", alias="DIALECTIC_LLM_PROVIDER")
120
+ dialectic_llm_model: str = Field(default="llama3", alias="DIALECTIC_LLM_MODEL")
121
+ dialectic_auto_threshold: float = Field(default=0.5, alias="DIALECTIC_AUTO_THRESHOLD")
122
+
123
+ _json_config_loaded: bool = False
124
+
125
+ @field_validator("workers", mode="before")
126
+ @classmethod
127
+ def _clamp_workers(cls, v: int | str) -> int:
128
+ """Clamp workers to reasonable range."""
129
+ val = int(v) if isinstance(v, str) else v
130
+ if val < 1:
131
+ return 1
132
+ if val > 64:
133
+ return 64
134
+ return val
135
+
136
+ @field_validator("qdrant_max_retries", mode="before")
137
+ @classmethod
138
+ def _clamp_retries(cls, v: int | str) -> int:
139
+ """Clamp retry count to safe range."""
140
+ val = int(v) if isinstance(v, str) else v
141
+ if val < 1:
142
+ return 1
143
+ if val > 10:
144
+ return 10
145
+ return val
146
+
147
+ @field_validator("qdrant_retry_delay_ms", mode="before")
148
+ @classmethod
149
+ def _clamp_retry_delay(cls, v: int | str) -> int:
150
+ """Clamp retry delay to safe range."""
151
+ val = int(v) if isinstance(v, str) else v
152
+ if val < 100:
153
+ return 100
154
+ if val > 30000:
155
+ return 30000
156
+ return val
157
+
158
+ @field_validator("chunk_size", mode="before")
159
+ @classmethod
160
+ def _clamp_chunk_size(cls, v: int | str) -> int:
161
+ """Clamp chunk size to valid range."""
162
+ val = int(v) if isinstance(v, str) else v
163
+ if val < 64:
164
+ return 64
165
+ if val > 8192:
166
+ return 8192
167
+ return val
168
+
169
+ @field_validator("chunk_overlap", mode="before")
170
+ @classmethod
171
+ def _clamp_chunk_overlap(cls, v: int | str) -> int:
172
+ """Clamp chunk overlap to valid range."""
173
+ val = int(v) if isinstance(v, str) else v
174
+ if val < 0:
175
+ return 0
176
+ return val
177
+
178
+ @field_validator("batch_size", mode="before")
179
+ @classmethod
180
+ def _clamp_batch_size(cls, v: int | str) -> int:
181
+ """Clamp batch size to valid range."""
182
+ val = int(v) if isinstance(v, str) else v
183
+ if val < 1:
184
+ return 1
185
+ if val > 256:
186
+ return 256
187
+ return val
188
+
189
+ @field_validator("max_file_size", mode="before")
190
+ @classmethod
191
+ def _clamp_max_file_size(cls, v: int | str) -> int:
192
+ """Clamp max file size to max 100MB."""
193
+ val = int(v) if isinstance(v, str) else v
194
+ max_allowed = 100 * 1024 * 1024
195
+ if val > max_allowed:
196
+ return max_allowed
197
+ return val
198
+
199
+ @field_validator(
200
+ "trust_threshold_archive", "trust_threshold_promote", mode="before"
201
+ )
202
+ @classmethod
203
+ def _clamp_trust_threshold(cls, v: float | str) -> float:
204
+ """Clamp trust threshold to valid range."""
205
+ val = float(v) if isinstance(v, str) else v
206
+ if val < 0.0:
207
+ return 0.0
208
+ if val > 1.0:
209
+ return 1.0
210
+ return val
211
+
212
+ @field_validator(
213
+ "trust_delta_use",
214
+ "trust_delta_ignore",
215
+ "trust_delta_correct",
216
+ "trust_delta_confirm",
217
+ mode="before",
218
+ )
219
+ @classmethod
220
+ def _clamp_trust_delta(cls, v: float | str) -> float:
221
+ """Clamp trust delta to valid range."""
222
+ val = float(v) if isinstance(v, str) else v
223
+ if val < -1.0:
224
+ return -1.0
225
+ if val > 1.0:
226
+ return 1.0
227
+ return val
228
+
229
+ @field_validator("precompress_threshold", mode="before")
230
+ @classmethod
231
+ def _clamp_precompress_threshold(cls, v: float | str) -> float:
232
+ """Clamp precompress threshold to valid range."""
233
+ val = float(v) if isinstance(v, str) else v
234
+ if val < 0.0:
235
+ return 0.0
236
+ if val > 1.0:
237
+ return 1.0
238
+ return val
239
+
240
+ @field_validator("user_model_min_sessions", mode="before")
241
+ @classmethod
242
+ def _clamp_user_model_min_sessions(cls, v: int | str) -> int:
243
+ """Clamp user model min sessions to valid range."""
244
+ val = int(v) if isinstance(v, str) else v
245
+ if val < 1:
246
+ return 1
247
+ if val > 500:
248
+ return 500
249
+ return val
250
+
251
+ @field_validator("decay_half_life_days", mode="before")
252
+ @classmethod
253
+ def _clamp_decay_half_life(cls, v: int | str) -> int:
254
+ """Clamp decay half-life to valid range."""
255
+ val = int(v) if isinstance(v, str) else v
256
+ if val < 1:
257
+ return 1
258
+ if val > 365:
259
+ return 365
260
+ return val
261
+
262
+ @field_validator("consolidation_interval_hours", mode="before")
263
+ @classmethod
264
+ def _clamp_consolidation_interval(cls, v: int | str) -> int:
265
+ """Clamp consolidation interval to valid range."""
266
+ val = int(v) if isinstance(v, str) else v
267
+ if val < 1:
268
+ return 1
269
+ if val > 8760: # Max 1 year
270
+ return 8760
271
+ return val
272
+
273
+ @field_validator("consolidation_similarity_threshold", mode="before")
274
+ @classmethod
275
+ def _clamp_consolidation_threshold(cls, v: float | str) -> float:
276
+ """Clamp consolidation similarity threshold to valid range."""
277
+ val = float(v) if isinstance(v, str) else v
278
+ if val < 0.0:
279
+ return 0.0
280
+ if val > 1.0:
281
+ return 1.0
282
+ return val
283
+
284
+ @field_validator("kg_inference_depth", mode="before")
285
+ @classmethod
286
+ def _clamp_kg_inference_depth(cls, v: int | str) -> int:
287
+ """Clamp KG inference depth to valid range."""
288
+ val = int(v) if isinstance(v, str) else v
289
+ if val < 1:
290
+ return 1
291
+ if val > 10:
292
+ return 10
293
+ return val
294
+
295
+ @field_validator("kg_max_results", mode="before")
296
+ @classmethod
297
+ def _clamp_kg_max_results(cls, v: int | str) -> int:
298
+ """Clamp KG max results to valid range."""
299
+ val = int(v) if isinstance(v, str) else v
300
+ if val < 1:
301
+ return 1
302
+ if val > 1000:
303
+ return 1000
304
+ return val
305
+
306
+ @field_validator("dialectic_auto_threshold", mode="before")
307
+ @classmethod
308
+ def _clamp_dialectic_auto_threshold(cls, v: float | str) -> float:
309
+ """Clamp dialectic auto threshold to valid range."""
310
+ val = float(v) if isinstance(v, str) else v
311
+ if val < 0.0:
312
+ return 0.0
313
+ if val > 1.0:
314
+ return 1.0
315
+ return val
316
+
317
+ def model_post_init(self, _context: object) -> None:
318
+ """Apply JSON config loading after initialization."""
319
+ # Only load JSON config once per instance
320
+ if not self._json_config_loaded:
321
+ self._json_config_loaded = True
322
+ self._load_json_config()
323
+ self._finalize_validation()
324
+
325
+ def _load_json_config(self) -> None:
326
+ """Load configuration from JSON file if it exists.
327
+
328
+ JSON config is at .opencode/memini-ai/config.json and is only loaded
329
+ if not already set via environment variables.
330
+ """
331
+ config_path = self._find_json_config_path()
332
+ if config_path is None or not config_path.exists():
333
+ return
334
+
335
+ try:
336
+ with open(config_path, encoding="utf-8") as f:
337
+ json_config = json.load(f)
338
+ # Apply JSON config values that aren't set by environment variables
339
+ for key, value in json_config.items():
340
+ if key not in self.model_fields_set and key in self.model_fields:
341
+ object.__setattr__(self, key, value)
342
+ except (json.JSONDecodeError, OSError):
343
+ # Silently skip invalid JSON config - defaults are sufficient
344
+ pass
345
+
346
+ def _find_json_config_path(self) -> Path | None:
347
+ """Find JSON config file path by traversing up from current directory."""
348
+ cwd = Path.cwd()
349
+ for parent in [cwd, *cwd.parents]:
350
+ config_path = parent / ".opencode" / "memini-ai" / "config.json"
351
+ if config_path.exists():
352
+ return config_path
353
+ return None
354
+
355
+ def _finalize_validation(self) -> None:
356
+ """Final validation and clamping that depends on multiple fields."""
357
+ # Clamp chunk_overlap based on chunk_size
358
+ if self.chunk_overlap > self.chunk_size:
359
+ object.__setattr__(self, "chunk_overlap", self.chunk_size // 2)
360
+
361
+ @property
362
+ def effective_project_id(self) -> str:
363
+ """Get effective project ID, generating from directory name if not set."""
364
+ if self.project_id:
365
+ return self.project_id
366
+ # Generate from directory name, sanitized
367
+ cwd = Path.cwd()
368
+ return _sanitize_project_id(cwd.name)
369
+
370
+
371
+ def _sanitize_project_id(name: str) -> str:
372
+ """Sanitize a directory name into a valid project ID."""
373
+ # Remove non-alphanumeric characters except hyphens/underscores
374
+ sanitized = re.sub(r"[^a-zA-Z0-9_-]", "-", name)
375
+ # Collapse multiple hyphens
376
+ sanitized = re.sub(r"-+", "-", sanitized)
377
+ # Remove leading/trailing hyphens
378
+ sanitized = sanitized.strip("-")
379
+ # Default if empty
380
+ if not sanitized:
381
+ return "default-project"
382
+ return sanitized
383
+
384
+
385
+ # Module-level singleton config instance
386
+ _config: MeminiConfig | None = None
387
+
388
+
389
+ def get_config() -> MeminiConfig:
390
+ """Get the global config instance, creating if necessary."""
391
+ global _config
392
+ if _config is None:
393
+ _config = MeminiConfig()
394
+ return _config