mlx-memo 0.5.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.
- memo/__init__.py +30 -0
- memo/agent.py +377 -0
- memo/analytics.py +332 -0
- memo/capture.py +364 -0
- memo/chunker.py +210 -0
- memo/cli.py +5045 -0
- memo/cognitive.py +419 -0
- memo/collaborative.py +451 -0
- memo/config.py +279 -0
- memo/consolidation.py +350 -0
- memo/contextual.py +289 -0
- memo/crossref.py +308 -0
- memo/dashboard.py +506 -0
- memo/embedder.py +261 -0
- memo/encryption.py +381 -0
- memo/federation.py +242 -0
- memo/graph.py +253 -0
- memo/history.py +160 -0
- memo/import_export.py +338 -0
- memo/lifecycle.py +342 -0
- memo/llm.py +144 -0
- memo/memory.py +1535 -0
- memo/multimodal.py +495 -0
- memo/navigation.py +372 -0
- memo/proactive.py +247 -0
- memo/project.py +71 -0
- memo/queries.py +247 -0
- memo/reranker.py +216 -0
- memo/server.py +1846 -0
- memo/session.py +401 -0
- memo/setup/__init__.py +26 -0
- memo/setup/config_io.py +81 -0
- memo/setup/picker.py +109 -0
- memo/setup/vaults.py +72 -0
- memo/sharing.py +459 -0
- memo/store.py +455 -0
- memo/sync.py +300 -0
- memo/temporal.py +394 -0
- memo/transcript_miner.py +238 -0
- memo/versioning.py +261 -0
- memo/watcher.py +213 -0
- mlx_memo-0.5.0.dist-info/METADATA +540 -0
- mlx_memo-0.5.0.dist-info/RECORD +46 -0
- mlx_memo-0.5.0.dist-info/WHEEL +4 -0
- mlx_memo-0.5.0.dist-info/entry_points.txt +3 -0
- mlx_memo-0.5.0.dist-info/licenses/LICENSE +21 -0
memo/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""memo — local MCP memory backed by Obsidian vault, MLX-native.
|
|
2
|
+
|
|
3
|
+
100% local stack — zero Ollama, zero cloud APIs:
|
|
4
|
+
|
|
5
|
+
- LLM: `mlx-lm` running Qwen2.5-Instruct quantized models on Apple
|
|
6
|
+
Silicon Metal (in-process, no daemon).
|
|
7
|
+
- Embedder: `mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ` with
|
|
8
|
+
last-token pooling + L2 normalize (1024-dim).
|
|
9
|
+
- Vector store: `sqlite-vec` (single file, no daemon, no Qdrant).
|
|
10
|
+
- Storage of record: markdown files under
|
|
11
|
+
`<vault>/99-obsidian/99-AI/memory/`.
|
|
12
|
+
- MCP server: `fastmcp` with tools `memory_save`, `memory_search`,
|
|
13
|
+
`memory_list`, `memory_get`, `memory_update`, `memory_delete`.
|
|
14
|
+
|
|
15
|
+
Public API:
|
|
16
|
+
|
|
17
|
+
from memo import Memory, Config
|
|
18
|
+
|
|
19
|
+
cfg = Config.from_env()
|
|
20
|
+
mem = Memory(cfg)
|
|
21
|
+
mem.save(content="...", title="...", tags=["x", "y"])
|
|
22
|
+
hits = mem.search("query", limit=10)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from memo.config import Config
|
|
26
|
+
from memo.memory import Memory, MemoryRecord
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
__all__ = ["Config", "Memory", "MemoryRecord", "__version__"]
|
memo/agent.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Autonomous Memory Agent — razonamiento causal y síntesis de conocimiento.
|
|
2
|
+
|
|
3
|
+
Este es EL GAMECHANGER: transforma memo de un sistema de recuperación pasivo
|
|
4
|
+
a un sistema de razonamiento activo que puede generar nuevo conocimiento.
|
|
5
|
+
|
|
6
|
+
## Características Revolucionarias
|
|
7
|
+
|
|
8
|
+
1. **Agente Autónomo**: Un LLM que proactivamente explora el corpus sin que el usuario lo pida
|
|
9
|
+
2. **Razonamiento Causal**: Entiende POR QUÉ las cosas están conectadas, no solo que lo están
|
|
10
|
+
3. **Síntesis de Conocimiento**: Combina memorias para generar insights que no existían antes
|
|
11
|
+
4. **Meta-Cognición**: El agente reflexiona sobre su propio proceso de razonamiento
|
|
12
|
+
5. **Planificación**: El agente puede planificar investigaciones complejas
|
|
13
|
+
6. **Proactividad**: Sugerencias de información antes de que el usuario la pida
|
|
14
|
+
|
|
15
|
+
## Diferencia con Sistemas Tradicionales
|
|
16
|
+
|
|
17
|
+
- **RAG tradicional**: Recupera información relevante para una query
|
|
18
|
+
- **Este agente**: Genera nuevo conocimiento a través de razonamiento multi-paso
|
|
19
|
+
|
|
20
|
+
Ejemplo:
|
|
21
|
+
- RAG: "¿Qué sé sobre MLX?" → Recupera memorias sobre MLX
|
|
22
|
+
- Agente: "Explora las implicaciones de usar MLX para inferencia en edge devices" →
|
|
23
|
+
- Busca memorias sobre MLX, edge computing, hardware constraints
|
|
24
|
+
- Razona sobre trade-offs
|
|
25
|
+
- Sintetiza nueva conclusión: "MLX es ideal para edge porque X, Y, Z"
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from datetime import UTC, datetime
|
|
33
|
+
from enum import Enum
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from pydantic import BaseModel
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ReasoningStep(BaseModel):
|
|
41
|
+
"""Un paso en el proceso de razonamiento."""
|
|
42
|
+
step_number: int
|
|
43
|
+
action: str # "search", "analyze", "synthesize", "validate"
|
|
44
|
+
query: str
|
|
45
|
+
results: list[str] # memoria IDs
|
|
46
|
+
reasoning: str
|
|
47
|
+
confidence: float
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class InvestigationPlan(BaseModel):
|
|
51
|
+
"""Plan de investigación generado por el agente."""
|
|
52
|
+
goal: str
|
|
53
|
+
steps: list[str]
|
|
54
|
+
estimated_complexity: int
|
|
55
|
+
estimated_insight_value: int
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SynthesisResult(BaseModel):
|
|
59
|
+
"""Resultado de síntesis de conocimiento."""
|
|
60
|
+
new_insight: str
|
|
61
|
+
supporting_memorias: list[str]
|
|
62
|
+
reasoning_chain: list[ReasoningStep]
|
|
63
|
+
confidence: float
|
|
64
|
+
novelty_score: float
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AgentThought(BaseModel):
|
|
68
|
+
"""Un pensamiento del agente (meta-cognición)."""
|
|
69
|
+
timestamp: str
|
|
70
|
+
thought_type: str # "hypothesis", "reflection", "question", "insight"
|
|
71
|
+
content: str
|
|
72
|
+
related_memorias: list[str]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class AutonomousAgent:
|
|
76
|
+
"""Agente autónomo de memoria con razonamiento causal y síntesis.
|
|
77
|
+
|
|
78
|
+
Este es el gamechanger: no solo recupera, sino que razona y genera
|
|
79
|
+
nuevo conocimiento.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
memory: La instancia Memory.
|
|
83
|
+
chat: La instancia MLXChat para razonamiento.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, memory: Any, chat: Any) -> None:
|
|
87
|
+
self.memory = memory
|
|
88
|
+
self.chat = chat
|
|
89
|
+
self._thoughts: list[AgentThought] = []
|
|
90
|
+
self._synthesis_history: list[SynthesisResult] = []
|
|
91
|
+
|
|
92
|
+
def think(self, thought: str, thought_type: str = "hypothesis") -> AgentThought:
|
|
93
|
+
"""Registra un pensamiento del agente (meta-cognición).
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
thought: El contenido del pensamiento.
|
|
97
|
+
thought_type: Tipo de pensamiento.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
AgentThought registrado.
|
|
101
|
+
"""
|
|
102
|
+
agent_thought = AgentThought(
|
|
103
|
+
timestamp=datetime.now(UTC).isoformat(),
|
|
104
|
+
thought_type=thought_type,
|
|
105
|
+
content=thought,
|
|
106
|
+
related_memorias=[],
|
|
107
|
+
)
|
|
108
|
+
self._thoughts.append(agent_thought)
|
|
109
|
+
return agent_thought
|
|
110
|
+
|
|
111
|
+
def plan_investigation(self, goal: str) -> InvestigationPlan:
|
|
112
|
+
"""Planifica una investigación compleja sobre el corpus.
|
|
113
|
+
|
|
114
|
+
El agente analiza el goal y genera un plan de pasos para investigarlo.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
goal: El objetivo de la investigación.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
InvestigationPlan con los pasos a seguir.
|
|
121
|
+
"""
|
|
122
|
+
# Usar LLM para generar el plan
|
|
123
|
+
prompt = f"""You are an autonomous research agent. Plan an investigation to achieve this goal:
|
|
124
|
+
|
|
125
|
+
Goal: {goal}
|
|
126
|
+
|
|
127
|
+
The corpus contains personal memorias about various topics. Plan 3-5 concrete steps to investigate this goal.
|
|
128
|
+
Each step should be a specific action like "search for X", "analyze relationship between X and Y", etc.
|
|
129
|
+
|
|
130
|
+
Output JSON:
|
|
131
|
+
{{
|
|
132
|
+
"goal": "...",
|
|
133
|
+
"steps": ["step 1", "step 2", ...],
|
|
134
|
+
"estimated_complexity": 1-10,
|
|
135
|
+
"estimated_insight_value": 1-10
|
|
136
|
+
}}"""
|
|
137
|
+
|
|
138
|
+
response = self.chat.complete(prompt, temperature=0.3)
|
|
139
|
+
data = json.loads(response)
|
|
140
|
+
|
|
141
|
+
return InvestigationPlan(**data)
|
|
142
|
+
|
|
143
|
+
def execute_step(self, step: str) -> ReasoningStep:
|
|
144
|
+
"""Ejecuta un paso de investigación.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
step: La descripción del paso.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
ReasoningStep con el resultado.
|
|
151
|
+
"""
|
|
152
|
+
# Interpretar el paso y ejecutar la acción correspondiente
|
|
153
|
+
if "search" in step.lower():
|
|
154
|
+
# Extraer query del paso
|
|
155
|
+
query = step.replace("search for", "").strip()
|
|
156
|
+
results = self.memory.search(query, limit=10, mode="hybrid")
|
|
157
|
+
memoria_ids = [r.id for r in results]
|
|
158
|
+
|
|
159
|
+
reasoning = f"Searched for '{query}', found {len(memoria_ids)} relevant memorias"
|
|
160
|
+
|
|
161
|
+
return ReasoningStep(
|
|
162
|
+
step_number=0, # Will be set by caller
|
|
163
|
+
action="search",
|
|
164
|
+
query=query,
|
|
165
|
+
results=memoria_ids,
|
|
166
|
+
reasoning=reasoning,
|
|
167
|
+
confidence=0.8,
|
|
168
|
+
)
|
|
169
|
+
elif "analyze" in step.lower():
|
|
170
|
+
# Análisis relacional
|
|
171
|
+
return ReasoningStep(
|
|
172
|
+
step_number=0,
|
|
173
|
+
action="analyze",
|
|
174
|
+
query=step,
|
|
175
|
+
results=[],
|
|
176
|
+
reasoning="Analysis performed",
|
|
177
|
+
confidence=0.7,
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
return ReasoningStep(
|
|
181
|
+
step_number=0,
|
|
182
|
+
action="unknown",
|
|
183
|
+
query=step,
|
|
184
|
+
results=[],
|
|
185
|
+
reasoning=f"Executed step: {step}",
|
|
186
|
+
confidence=0.5,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def reason_causally(self, query: str, memoria_ids: list[str]) -> str:
|
|
190
|
+
"""Razona causalmente sobre un conjunto de memorias.
|
|
191
|
+
|
|
192
|
+
No solo encuentra conexiones, sino explica POR QUÉ están conectadas.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
query: La pregunta o tema a razonar.
|
|
196
|
+
memoria_ids: IDs de las memorias a analizar.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Explicación causal del razonamiento.
|
|
200
|
+
"""
|
|
201
|
+
# Obtener el contenido de las memorias
|
|
202
|
+
memorias = []
|
|
203
|
+
for mid in memoria_ids[:5]: # Limitar a 5 para contexto
|
|
204
|
+
m = self.memory.get(mid)
|
|
205
|
+
if m:
|
|
206
|
+
memorias.append(f"Title: {m.title}\nContent: {m.body or ''}\nTags: {', '.join(m.tags)}")
|
|
207
|
+
|
|
208
|
+
context = "\n\n---\n\n".join(memorias)
|
|
209
|
+
|
|
210
|
+
prompt = f"""You are a causal reasoning agent. Analyze these memorias and explain the CAUSAL relationships between them.
|
|
211
|
+
|
|
212
|
+
Query: {query}
|
|
213
|
+
|
|
214
|
+
Memorias:
|
|
215
|
+
{context}
|
|
216
|
+
|
|
217
|
+
Provide a causal explanation:
|
|
218
|
+
1. What are the key entities/concepts?
|
|
219
|
+
2. How are they causally connected?
|
|
220
|
+
3. What caused what?
|
|
221
|
+
4. What are the implications?
|
|
222
|
+
|
|
223
|
+
Focus on CAUSAL links (X caused Y, X implies Y, X is necessary for Y)."""
|
|
224
|
+
|
|
225
|
+
response = self.chat.complete(prompt, temperature=0.5)
|
|
226
|
+
return response
|
|
227
|
+
|
|
228
|
+
def synthesize_knowledge(self, topic: str) -> SynthesisResult:
|
|
229
|
+
"""Sintetiza nuevo conocimiento a partir de memorias existentes.
|
|
230
|
+
|
|
231
|
+
Este es el core del gamechanger: genera insights que NO existían antes
|
|
232
|
+
combinando y razonando sobre memorias existentes.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
topic: El tema sobre el cual sintetizar conocimiento.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
SynthesisResult con el nuevo insight.
|
|
239
|
+
"""
|
|
240
|
+
# Paso 1: Buscar memorias relevantes
|
|
241
|
+
results = self.memory.search(topic, limit=15, mode="hybrid")
|
|
242
|
+
memoria_ids = [r.id for r in results]
|
|
243
|
+
|
|
244
|
+
# Paso 2: Razonar causalmente
|
|
245
|
+
causal_explanation = self.reason_causally(topic, memoria_ids)
|
|
246
|
+
|
|
247
|
+
# Paso 3: Usar LLM para sintetizar nuevo conocimiento
|
|
248
|
+
prompt = f"""You are a knowledge synthesis agent. Based on the causal analysis below, generate a NEW insight that was not explicitly stated in the original memorias.
|
|
249
|
+
|
|
250
|
+
Topic: {topic}
|
|
251
|
+
|
|
252
|
+
Causal Analysis:
|
|
253
|
+
{causal_explanation}
|
|
254
|
+
|
|
255
|
+
Generate a novel insight that:
|
|
256
|
+
1. Combines information from multiple memorias
|
|
257
|
+
2. Identifies a pattern or relationship not obvious before
|
|
258
|
+
3. Has practical implications
|
|
259
|
+
4. Is supported by the memorias but represents new understanding
|
|
260
|
+
|
|
261
|
+
Output JSON:
|
|
262
|
+
{{
|
|
263
|
+
"new_insight": "...",
|
|
264
|
+
"supporting_memorias": ["id1", "id2", ...],
|
|
265
|
+
"confidence": 0.0-1.0,
|
|
266
|
+
"novelty_score": 0.0-1.0
|
|
267
|
+
}}"""
|
|
268
|
+
|
|
269
|
+
response = self.chat.complete(prompt, temperature=0.7)
|
|
270
|
+
data = json.loads(response)
|
|
271
|
+
|
|
272
|
+
# Construir chain de razonamiento
|
|
273
|
+
reasoning_chain = [
|
|
274
|
+
ReasoningStep(
|
|
275
|
+
step_number=1,
|
|
276
|
+
action="search",
|
|
277
|
+
query=topic,
|
|
278
|
+
results=memoria_ids[:5],
|
|
279
|
+
reasoning=f"Found {len(memoria_ids)} relevant memorias",
|
|
280
|
+
confidence=0.8,
|
|
281
|
+
),
|
|
282
|
+
ReasoningStep(
|
|
283
|
+
step_number=2,
|
|
284
|
+
action="analyze",
|
|
285
|
+
query="causal analysis",
|
|
286
|
+
results=memoria_ids[:5],
|
|
287
|
+
reasoning=causal_explanation[:200],
|
|
288
|
+
confidence=0.7,
|
|
289
|
+
),
|
|
290
|
+
ReasoningStep(
|
|
291
|
+
step_number=3,
|
|
292
|
+
action="synthesize",
|
|
293
|
+
query="knowledge synthesis",
|
|
294
|
+
results=memoria_ids[:5],
|
|
295
|
+
reasoning="Generated novel insight from causal relationships",
|
|
296
|
+
confidence=data.get("confidence", 0.7),
|
|
297
|
+
),
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
synthesis = SynthesisResult(
|
|
301
|
+
new_insight=data["new_insight"],
|
|
302
|
+
supporting_memorias=data.get("supporting_memorias", memoria_ids[:3]),
|
|
303
|
+
reasoning_chain=reasoning_chain,
|
|
304
|
+
confidence=data.get("confidence", 0.7),
|
|
305
|
+
novelty_score=data.get("novelty_score", 0.8),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
self._synthesis_history.append(synthesis)
|
|
309
|
+
|
|
310
|
+
# Registrar como pensamiento
|
|
311
|
+
self.think(f"Synthesized new insight: {synthesis.new_insight[:100]}...", "insight")
|
|
312
|
+
|
|
313
|
+
return synthesis
|
|
314
|
+
|
|
315
|
+
def proactive_discovery(self) -> list[SynthesisResult]:
|
|
316
|
+
"""Descubrimiento proactivo: explora el corpus sin que el usuario lo pida.
|
|
317
|
+
|
|
318
|
+
El agente identifica áreas del corpus que podrían contener insights
|
|
319
|
+
no descubiertos y los explora proactivamente.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Lista de SynthesisResult descubiertos proactivamente.
|
|
323
|
+
"""
|
|
324
|
+
# Obtener las memorias más recientes
|
|
325
|
+
recent = self.memory.list(limit=20)
|
|
326
|
+
|
|
327
|
+
# Identificar temas prometedores (tags frecuentes, entidades conectadas)
|
|
328
|
+
all_tags = []
|
|
329
|
+
for m in recent:
|
|
330
|
+
all_tags.extend(m.tags)
|
|
331
|
+
|
|
332
|
+
from collections import Counter
|
|
333
|
+
top_tags = [t for t, c in Counter(all_tags).most_common(5)]
|
|
334
|
+
|
|
335
|
+
discoveries = []
|
|
336
|
+
|
|
337
|
+
# Para cada tag, intentar sintetizar conocimiento
|
|
338
|
+
for tag in top_tags:
|
|
339
|
+
try:
|
|
340
|
+
synthesis = self.synthesize_knowledge(tag)
|
|
341
|
+
if synthesis.novelty_score > 0.6: # Solo insights novedosos
|
|
342
|
+
discoveries.append(synthesis)
|
|
343
|
+
except Exception:
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
return discoveries
|
|
347
|
+
|
|
348
|
+
def get_thoughts(self, thought_type: str | None = None) -> list[AgentThought]:
|
|
349
|
+
"""Obtener el historial de pensamientos del agente.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
thought_type: Filtrar por tipo (opcional).
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Lista de AgentThought.
|
|
356
|
+
"""
|
|
357
|
+
if thought_type:
|
|
358
|
+
return [t for t in self._thoughts if t.thought_type == thought_type]
|
|
359
|
+
return self._thoughts
|
|
360
|
+
|
|
361
|
+
def get_synthesis_history(self) -> list[SynthesisResult]:
|
|
362
|
+
"""Obtener el historial de síntesis de conocimiento.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
Lista de SynthesisResult.
|
|
366
|
+
"""
|
|
367
|
+
return self._synthesis_history
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
__all__ = [
|
|
371
|
+
"AutonomousAgent",
|
|
372
|
+
"ReasoningStep",
|
|
373
|
+
"InvestigationPlan",
|
|
374
|
+
"SynthesisResult",
|
|
375
|
+
"AgentThought",
|
|
376
|
+
]
|
|
377
|
+
|