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.
- memini_ai/__init__.py +3 -0
- memini_ai/config.py +394 -0
- memini_ai/decay.py +811 -0
- memini_ai/dialectic.py +1103 -0
- memini_ai/entity_extractor.py +439 -0
- memini_ai/extractor.py +281 -0
- memini_ai/graph.py +323 -0
- memini_ai/indexer/__init__.py +47 -0
- memini_ai/indexer/chunker.py +460 -0
- memini_ai/indexer/constants.py +186 -0
- memini_ai/indexer/file_tracker.py +211 -0
- memini_ai/indexer/indexer.py +402 -0
- memini_ai/indexer/pause_controller.py +89 -0
- memini_ai/indexer/snapshot.py +192 -0
- memini_ai/indexer/watcher.py +217 -0
- memini_ai/knowledge_graph.py +1355 -0
- memini_ai/main.py +52 -0
- memini_ai/memory/__init__.py +32 -0
- memini_ai/memory/database.py +1095 -0
- memini_ai/memory/schema.py +305 -0
- memini_ai/memory/search.py +486 -0
- memini_ai/memory/system.py +530 -0
- memini_ai/model/__init__.py +15 -0
- memini_ai/model/embeddings.py +106 -0
- memini_ai/model/manager.py +199 -0
- memini_ai/multi_peer.py +861 -0
- memini_ai/postgres/__init__.py +5 -0
- memini_ai/postgres/database.py +593 -0
- memini_ai/postgres/queries.py +256 -0
- memini_ai/postgres/schema.py +288 -0
- memini_ai/precompress.py +135 -0
- memini_ai/server.py +2383 -0
- memini_ai/tiered_loader.py +557 -0
- memini_ai/trust_engine.py +299 -0
- memini_ai/user_model.py +543 -0
- memini_ai/utils/__init__.py +6 -0
- memini_ai/utils/hash.py +43 -0
- memini_ai/utils/logger.py +50 -0
- memini_ai_dev-0.2.0.dist-info/METADATA +370 -0
- memini_ai_dev-0.2.0.dist-info/RECORD +42 -0
- memini_ai_dev-0.2.0.dist-info/WHEEL +4 -0
- memini_ai_dev-0.2.0.dist-info/entry_points.txt +2 -0
memini_ai/__init__.py
ADDED
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
|