echomind-memory 1.1.5__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.
- echomind/__init__.py +34 -0
- echomind/adapters/__init__.py +5 -0
- echomind/adapters/hermes_provider.py +530 -0
- echomind/adapters/http_api.py +323 -0
- echomind/code_format/__init__.py +0 -0
- echomind/code_format/cli.py +74 -0
- echomind/code_format/memory.schema.json +33 -0
- echomind/core/__init__.py +25 -0
- echomind/core/_reflective_fallback.py +200 -0
- echomind/core/_reflective_version.py +19 -0
- echomind/core/agents/__init__.py +17 -0
- echomind/core/agents/context_agent.py +25 -0
- echomind/core/agents/experience_agent.py +76 -0
- echomind/core/agents/knowledge_agent.py +100 -0
- echomind/core/agents/research_agent.py +100 -0
- echomind/core/agents/task_agent.py +63 -0
- echomind/core/agents/user_agent.py +60 -0
- echomind/core/config_manager.py +218 -0
- echomind/core/learning/__init__.py +3 -0
- echomind/core/learning/rl_weight_optimizer.py +170 -0
- echomind/core/llm_client.py +210 -0
- echomind/core/memory_agent.py +807 -0
- echomind/core/models/__init__.py +13 -0
- echomind/core/models/context.py +15 -0
- echomind/core/models/experience.py +19 -0
- echomind/core/models/knowledge.py +11 -0
- echomind/core/models/reflection.py +45 -0
- echomind/core/models/research.py +34 -0
- echomind/core/models/task.py +20 -0
- echomind/core/models/user.py +12 -0
- echomind/core/reflective_agent.py +128 -0
- echomind/core/storage/__init__.py +3 -0
- echomind/core/storage/sqlite_store.py +648 -0
- echomind/domain_keywords.yaml +159 -0
- echomind/echomind_config.yaml +113 -0
- echomind/main.py +200 -0
- echomind_memory-1.1.5.dist-info/METADATA +118 -0
- echomind_memory-1.1.5.dist-info/RECORD +42 -0
- echomind_memory-1.1.5.dist-info/WHEEL +5 -0
- echomind_memory-1.1.5.dist-info/entry_points.txt +2 -0
- echomind_memory-1.1.5.dist-info/licenses/LICENSE +21 -0
- echomind_memory-1.1.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# echomind_memory — FastAPI Service entry point (v2, SQLite)
|
|
2
|
+
# Based on upstream update: github.com/jasonatgit/echomind_memory.skill
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
|
|
10
|
+
from fastapi import FastAPI, HTTPException, Depends
|
|
11
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
12
|
+
from fastapi.security import APIKeyHeader
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
import logging
|
|
15
|
+
import uvicorn
|
|
16
|
+
|
|
17
|
+
from echomind.core.memory_agent import MainMemoryAgent
|
|
18
|
+
from echomind.core._reflective_version import get_echomind_version
|
|
19
|
+
from echomind.core.config_manager import get_config_manager
|
|
20
|
+
|
|
21
|
+
# ── Init ──
|
|
22
|
+
memory_agent = MainMemoryAgent()
|
|
23
|
+
|
|
24
|
+
cfg = get_config_manager().get_section("server")
|
|
25
|
+
|
|
26
|
+
# ── API Key Auth ──
|
|
27
|
+
API_KEY_NAME = "X-API-Key"
|
|
28
|
+
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def verify_api_key(api_key: str = Depends(api_key_header)):
|
|
32
|
+
expected = cfg.get("api_key", "")
|
|
33
|
+
if not expected:
|
|
34
|
+
return "" # no key configured — allow all (backward compat)
|
|
35
|
+
if api_key and api_key == expected:
|
|
36
|
+
return api_key
|
|
37
|
+
raise HTTPException(status_code=403, detail="Invalid or missing API key")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@asynccontextmanager
|
|
41
|
+
async def lifespan(app: FastAPI):
|
|
42
|
+
memory_agent.enable_persistence()
|
|
43
|
+
yield
|
|
44
|
+
memory_agent.disable_persistence()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
app = FastAPI(title="EchoMind Memory", version=get_echomind_version(), lifespan=lifespan)
|
|
48
|
+
cors_origins = cfg.get("cors_origins", ["http://localhost:8005"])
|
|
49
|
+
app.add_middleware(CORSMiddleware, allow_origins=cors_origins, allow_methods=["GET", "POST"], allow_headers=["Content-Type", "X-API-Key"])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ── Health ──
|
|
53
|
+
@app.get("/health")
|
|
54
|
+
async def health():
|
|
55
|
+
return {"status": "ok", "storage": "sqlite",
|
|
56
|
+
"db": os.path.basename(str(memory_agent.db.db_path)),
|
|
57
|
+
"version": get_echomind_version()}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ── Models ──
|
|
61
|
+
class RetrieveRequest(BaseModel):
|
|
62
|
+
user_id: str
|
|
63
|
+
platform: Optional[str] = None
|
|
64
|
+
query: str
|
|
65
|
+
task_id: Optional[str] = None
|
|
66
|
+
max_results: int = 5
|
|
67
|
+
|
|
68
|
+
class ContextMessage(BaseModel):
|
|
69
|
+
role: str
|
|
70
|
+
content: str
|
|
71
|
+
|
|
72
|
+
class StoreRequest(BaseModel):
|
|
73
|
+
user_id: str
|
|
74
|
+
platform: Optional[str] = None
|
|
75
|
+
task_id: str
|
|
76
|
+
title: Optional[str] = None
|
|
77
|
+
context: List[ContextMessage]
|
|
78
|
+
task_status: str
|
|
79
|
+
success: bool = False
|
|
80
|
+
experience_summary: Optional[str] = None
|
|
81
|
+
|
|
82
|
+
class FeedbackRequest(BaseModel):
|
|
83
|
+
user_id: str
|
|
84
|
+
task_id: str
|
|
85
|
+
feedback: str
|
|
86
|
+
retrieved_memories: List[Dict[str, Any]]
|
|
87
|
+
|
|
88
|
+
class SyncCodeRequest(BaseModel):
|
|
89
|
+
project_root: str
|
|
90
|
+
user_id: str
|
|
91
|
+
|
|
92
|
+
class ResearchPaperRequest(BaseModel):
|
|
93
|
+
title: str
|
|
94
|
+
authors: Optional[List[str]] = None
|
|
95
|
+
year: Optional[int] = None
|
|
96
|
+
journal: Optional[str] = None
|
|
97
|
+
abstract: str = ""
|
|
98
|
+
keywords: Optional[List[str]] = None
|
|
99
|
+
domain: str = "general"
|
|
100
|
+
paper_type: str = "theory"
|
|
101
|
+
key_points: Optional[List[str]] = None
|
|
102
|
+
importance_score: float = 0.5
|
|
103
|
+
|
|
104
|
+
class ResearchNoteRequest(BaseModel):
|
|
105
|
+
user_id: str
|
|
106
|
+
topic: str
|
|
107
|
+
content: str
|
|
108
|
+
linked_papers: List[str] = []
|
|
109
|
+
tags: List[str] = []
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ReflectRequest(BaseModel):
|
|
113
|
+
"""Reflection endpoint — build prompt or process result"""
|
|
114
|
+
user_id: str
|
|
115
|
+
platform: Optional[str] = "http"
|
|
116
|
+
count: int = 8
|
|
117
|
+
record_ids: Optional[List[str]] = None
|
|
118
|
+
llm_response: Optional[str] = None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ── Error handler (v1.2.0: avoid leaking internal details) ──
|
|
122
|
+
logger = logging.getLogger("EchoMind.API")
|
|
123
|
+
|
|
124
|
+
EXCEPTION_RESPONSE = {"status": "error", "detail": "Internal server error"}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def _safe_exception(e: Exception, context: str = "") -> dict:
|
|
128
|
+
logger.error(f"{context}: {e}", exc_info=True)
|
|
129
|
+
return EXCEPTION_RESPONSE
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ── Routes ──
|
|
133
|
+
@app.post("/api/memory/retrieve")
|
|
134
|
+
async def api_retrieve(req: RetrieveRequest, auth=Depends(verify_api_key)):
|
|
135
|
+
try:
|
|
136
|
+
result = memory_agent.retrieve_for_task(req.query, req.user_id, req.task_id, platform=req.platform)
|
|
137
|
+
working = [
|
|
138
|
+
{"source": m.source, "content": m.content,
|
|
139
|
+
"importance": m.importance, "metadata": m.metadata}
|
|
140
|
+
for m in result["working_memory"][:req.max_results]
|
|
141
|
+
]
|
|
142
|
+
confidence = (
|
|
143
|
+
sum(m.importance for m in result["working_memory"])
|
|
144
|
+
/ max(len(result["working_memory"]), 1)
|
|
145
|
+
)
|
|
146
|
+
return {
|
|
147
|
+
"working_memory": working,
|
|
148
|
+
"experience_memory": result.get("raw_memory_sources", {}).get("experience", []),
|
|
149
|
+
"knowledge_memory": result.get("raw_memory_sources", {}).get("knowledge", []),
|
|
150
|
+
"confidence_score": float(confidence),
|
|
151
|
+
"used_weights": memory_agent.rl_optimizer.get_current_weights(),
|
|
152
|
+
"feedback_requested": result.get("feedback_request", False),
|
|
153
|
+
"raw_memory_sources": result.get("raw_memory_sources", {}),
|
|
154
|
+
"task_features": result.get("task_features", {}),
|
|
155
|
+
}
|
|
156
|
+
except Exception as e:
|
|
157
|
+
return {
|
|
158
|
+
"working_memory": [], "raw_memory_sources": {},
|
|
159
|
+
"experience_memory": [], "knowledge_memory": [],
|
|
160
|
+
"confidence_score": 0.0,
|
|
161
|
+
"used_weights": {}, "feedback_requested": False, "error": str(e),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@app.post("/api/memory/store")
|
|
165
|
+
async def api_store(req: StoreRequest, auth=Depends(verify_api_key)):
|
|
166
|
+
try:
|
|
167
|
+
memory_agent.store(
|
|
168
|
+
req.user_id, req.task_id,
|
|
169
|
+
[{"role": m.role, "content": m.content} for m in req.context],
|
|
170
|
+
req.task_status, req.success, req.experience_summary,
|
|
171
|
+
platform=req.platform, title=req.title,
|
|
172
|
+
)
|
|
173
|
+
return {"status": "stored", "user_id": req.user_id, "task_id": req.task_id}
|
|
174
|
+
except Exception as e:
|
|
175
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
176
|
+
|
|
177
|
+
@app.get("/api/memory/search-sessions")
|
|
178
|
+
async def search_sessions(q: str = "", user_id: str = None, project: str = None, limit: int = 5):
|
|
179
|
+
if not memory_agent or not memory_agent.is_persistence_enabled():
|
|
180
|
+
return {"results": [], "message": "Persistence is not enabled"}
|
|
181
|
+
try:
|
|
182
|
+
results = memory_agent.db.search_transcripts(q, user_id, project, limit)
|
|
183
|
+
return {"results": results, "count": len(results)}
|
|
184
|
+
except Exception as e:
|
|
185
|
+
return {"results": [], "error": str(e)}
|
|
186
|
+
|
|
187
|
+
@app.post("/api/memory/feedback")
|
|
188
|
+
async def api_feedback(req: FeedbackRequest, auth=Depends(verify_api_key)):
|
|
189
|
+
try:
|
|
190
|
+
memory_agent.record_feedback(req.user_id, req.task_id, req.feedback, req.retrieved_memories)
|
|
191
|
+
return {"status": "feedback_received", "user_id": req.user_id}
|
|
192
|
+
except Exception as e:
|
|
193
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
194
|
+
|
|
195
|
+
@app.post("/api/memory/sync-code")
|
|
196
|
+
async def api_sync_code(req: SyncCodeRequest, auth=Depends(verify_api_key)):
|
|
197
|
+
try:
|
|
198
|
+
memory_agent.sync_to_code_project(req.project_root, req.user_id)
|
|
199
|
+
return {"status": "synced", "path": f"{req.project_root}/.echomind"}
|
|
200
|
+
except Exception as e:
|
|
201
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
202
|
+
|
|
203
|
+
@app.post("/api/research/paper")
|
|
204
|
+
async def api_add_paper(req: ResearchPaperRequest, auth=Depends(verify_api_key)):
|
|
205
|
+
try:
|
|
206
|
+
paper_id = memory_agent.add_research_paper(
|
|
207
|
+
title=req.title, authors=req.authors, year=req.year,
|
|
208
|
+
journal=req.journal, abstract=req.abstract, keywords=req.keywords,
|
|
209
|
+
domain=req.domain, paper_type=req.paper_type,
|
|
210
|
+
key_points=req.key_points, importance_score=req.importance_score)
|
|
211
|
+
return {"status": "stored", "paper_id": paper_id, "title": req.title}
|
|
212
|
+
except Exception as e:
|
|
213
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
214
|
+
|
|
215
|
+
@app.post("/api/research/note")
|
|
216
|
+
async def api_add_note(req: ResearchNoteRequest, auth=Depends(verify_api_key)):
|
|
217
|
+
try:
|
|
218
|
+
note_id = memory_agent.add_research_note(
|
|
219
|
+
user_id=req.user_id, topic=req.topic, content=req.content,
|
|
220
|
+
linked_papers=req.linked_papers, tags=req.tags)
|
|
221
|
+
return {"status": "stored", "note_id": note_id, "topic": req.topic}
|
|
222
|
+
except Exception as e:
|
|
223
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ── Reflection (v1.1.0) ──
|
|
227
|
+
@app.post("/api/reflect")
|
|
228
|
+
async def api_reflect(req: ReflectRequest, auth=Depends(verify_api_key)):
|
|
229
|
+
"""
|
|
230
|
+
Two-phase reflection endpoint:
|
|
231
|
+
- llm_response=None → returns prompt for caller to process (Path B)
|
|
232
|
+
- llm_response=... → parses and writes back (Path A / process)
|
|
233
|
+
"""
|
|
234
|
+
if req.llm_response is None:
|
|
235
|
+
records = memory_agent.get_recent_episodic(req.user_id, req.count)
|
|
236
|
+
prompt, record_ids = memory_agent.reflective.build_prompt(
|
|
237
|
+
records, req.user_id, req.platform or "http"
|
|
238
|
+
)
|
|
239
|
+
return {
|
|
240
|
+
"phase": "build",
|
|
241
|
+
"prompt": prompt,
|
|
242
|
+
"record_ids": record_ids,
|
|
243
|
+
"record_count": len(records),
|
|
244
|
+
}
|
|
245
|
+
else:
|
|
246
|
+
# Fetch full records (not just IDs) for process_result
|
|
247
|
+
full_records = memory_agent.get_recent_episodic(req.user_id, req.count)
|
|
248
|
+
# Filter to requested IDs (if specified)
|
|
249
|
+
if req.record_ids:
|
|
250
|
+
id_set = set(req.record_ids)
|
|
251
|
+
records = [r for r in full_records if r.get("id") in id_set]
|
|
252
|
+
if not records:
|
|
253
|
+
# Records may have been evicted; use placeholders
|
|
254
|
+
records = [{"id": rid, "content": "", "text": ""} for rid in id_set]
|
|
255
|
+
else:
|
|
256
|
+
records = full_records
|
|
257
|
+
output = memory_agent.reflective.process_result(
|
|
258
|
+
raw_response=req.llm_response,
|
|
259
|
+
records=records,
|
|
260
|
+
user_id=req.user_id,
|
|
261
|
+
platform=req.platform or "http",
|
|
262
|
+
)
|
|
263
|
+
if output is None:
|
|
264
|
+
raise HTTPException(
|
|
265
|
+
status_code=400,
|
|
266
|
+
detail="Reflection failed: parse error or confidence too low",
|
|
267
|
+
)
|
|
268
|
+
return {
|
|
269
|
+
"phase": "done",
|
|
270
|
+
"insights": len(output.key_insights),
|
|
271
|
+
"preferences": len(output.user_preferences),
|
|
272
|
+
"rules": len(output.procedural_rules),
|
|
273
|
+
"knowledge": len(output.new_knowledge),
|
|
274
|
+
"confidence": output.confidence,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# ── Config (v1.1.0) ──
|
|
279
|
+
class ConfigUpdateRequest(BaseModel):
|
|
280
|
+
section: str
|
|
281
|
+
key: str
|
|
282
|
+
value: Any
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@app.get("/api/config")
|
|
286
|
+
async def api_get_config(auth=Depends(verify_api_key)):
|
|
287
|
+
cfg = get_config_manager()
|
|
288
|
+
return {
|
|
289
|
+
"config_path": cfg.config_path,
|
|
290
|
+
"sections": {
|
|
291
|
+
"rl": cfg.get_section("rl"),
|
|
292
|
+
"reflection": cfg.get_section("reflection"),
|
|
293
|
+
"retrieval": cfg.get_section("retrieval"),
|
|
294
|
+
"inference": cfg.get_section("inference"),
|
|
295
|
+
"llm": {k: v for k, v in cfg.get_section("llm").items() if k != "api_key"},
|
|
296
|
+
"server": cfg.get_section("server"),
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@app.post("/api/config/parameter")
|
|
302
|
+
async def api_set_config_param(req: ConfigUpdateRequest, auth=Depends(verify_api_key)):
|
|
303
|
+
cfg = get_config_manager()
|
|
304
|
+
key_path = f"{req.section}.{req.key}"
|
|
305
|
+
cfg.set_runtime(key_path, req.value)
|
|
306
|
+
return {"status": "ok", "key": key_path, "value": req.value}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@app.post("/api/config/reload")
|
|
310
|
+
async def api_reload_config(auth=Depends(verify_api_key)):
|
|
311
|
+
cfg = get_config_manager()
|
|
312
|
+
cfg.reload()
|
|
313
|
+
return {"status": "reloaded", "config_path": cfg.config_path}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# ── Entry ──
|
|
317
|
+
if __name__ == "__main__":
|
|
318
|
+
port = int(sys.argv[1]) if len(sys.argv) > 1 else cfg.get("port", 8005)
|
|
319
|
+
host = cfg.get("host", "127.0.0.1")
|
|
320
|
+
print(f"EchoMind Memory v{get_echomind_version()} — HTTP API Mode")
|
|
321
|
+
print(f" Endpoint: http://{host}:{port}")
|
|
322
|
+
print(f" Docs: http://{host}:{port}/docs")
|
|
323
|
+
uvicorn.run(app, host=host, port=port, log_level="info")
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
# Support both direct run (`python cli.py`) and package import
|
|
7
|
+
_h = os.path.dirname(os.path.abspath(__file__)) # .../echomind/code_format
|
|
8
|
+
_h = os.path.dirname(_h) # .../echomind
|
|
9
|
+
_h = os.path.dirname(_h) # src/
|
|
10
|
+
if _h not in sys.path:
|
|
11
|
+
sys.path.insert(0, _h)
|
|
12
|
+
from echomind.core.memory_agent import MainMemoryAgent
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
if len(sys.argv) < 4:
|
|
17
|
+
print("Usage: echomind-cli [read|write] <user_id> <project_id> [file_path]")
|
|
18
|
+
sys.exit(1)
|
|
19
|
+
|
|
20
|
+
action = sys.argv[1]
|
|
21
|
+
user_id = sys.argv[2]
|
|
22
|
+
project_id = sys.argv[3]
|
|
23
|
+
agent = MainMemoryAgent()
|
|
24
|
+
agent.enable_persistence()
|
|
25
|
+
|
|
26
|
+
if action == "read":
|
|
27
|
+
user_mem = agent.user_agent.get(user_id)
|
|
28
|
+
exp_mem = agent.experience_agent.find_similar_tasks(
|
|
29
|
+
task_context=f"code style: {user_mem.get('preferences', {}).get('code_style', 'standard')}",
|
|
30
|
+
task_type="code_review",
|
|
31
|
+
min_success_rate=0.6,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
result = {
|
|
35
|
+
"user_id": user_id,
|
|
36
|
+
"project_id": project_id,
|
|
37
|
+
"preferences": user_mem.get("preferences", {}),
|
|
38
|
+
"experience": [
|
|
39
|
+
{
|
|
40
|
+
"action": "fix",
|
|
41
|
+
"location": "unknown",
|
|
42
|
+
"summary": m["summary"],
|
|
43
|
+
"success": m["success"],
|
|
44
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
45
|
+
}
|
|
46
|
+
for m in exp_mem
|
|
47
|
+
],
|
|
48
|
+
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
49
|
+
}
|
|
50
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
51
|
+
|
|
52
|
+
elif action == "write":
|
|
53
|
+
data = json.load(sys.stdin)
|
|
54
|
+
if "preferences" in data:
|
|
55
|
+
agent.user_agent.update(
|
|
56
|
+
user_id, "preferences", data["preferences"], source="code_cli"
|
|
57
|
+
)
|
|
58
|
+
if "experience" in data:
|
|
59
|
+
for exp in data["experience"]:
|
|
60
|
+
agent.experience_agent.store_experience(
|
|
61
|
+
user_id=user_id,
|
|
62
|
+
task_id=f"code_{exp['location']}",
|
|
63
|
+
task_type="code_review",
|
|
64
|
+
success=exp["success"],
|
|
65
|
+
steps=[exp["summary"]],
|
|
66
|
+
summary=exp["summary"],
|
|
67
|
+
)
|
|
68
|
+
print(json.dumps({"status": "written"}))
|
|
69
|
+
|
|
70
|
+
agent.disable_persistence()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "EchoMind Code Memory",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"user_id": { "type": "string" },
|
|
7
|
+
"project_id": { "type": "string" },
|
|
8
|
+
"preferences": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"properties": {
|
|
11
|
+
"code_style": { "enum": ["concise", "detailed", "standard"] },
|
|
12
|
+
"max_line_length": { "type": "integer", "minimum": 70, "maximum": 120 },
|
|
13
|
+
"comment_style": { "enum": ["docstring", "inline", "none"] }
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"experience": {
|
|
17
|
+
"type": "array",
|
|
18
|
+
"items": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {
|
|
21
|
+
"action": { "enum": ["fix", "refactor", "add", "remove"] },
|
|
22
|
+
"location": { "type": "string" },
|
|
23
|
+
"summary": { "type": "string" },
|
|
24
|
+
"success": { "type": "boolean" },
|
|
25
|
+
"created_at": { "type": "string", "format": "date-time" }
|
|
26
|
+
},
|
|
27
|
+
"required": ["action", "location", "summary", "success"]
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"updated_at": { "type": "string", "format": "date-time" }
|
|
31
|
+
},
|
|
32
|
+
"required": ["user_id", "project_id", "preferences", "experience", "updated_at"]
|
|
33
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# EchoMind Memory — Core module
|
|
2
|
+
# Platform-agnostic memory engine(6 Agent + RL + SQLite)
|
|
3
|
+
# Called by adapters, not dependent on any web framework
|
|
4
|
+
|
|
5
|
+
from .memory_agent import (
|
|
6
|
+
MainMemoryAgent,
|
|
7
|
+
ContextMemoryAgent,
|
|
8
|
+
TaskMemoryAgent,
|
|
9
|
+
UserMemoryAgent,
|
|
10
|
+
KnowledgeMemoryAgent,
|
|
11
|
+
ExperienceMemoryAgent,
|
|
12
|
+
ResearchMemoryAgent,
|
|
13
|
+
MemoryRecord,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"MainMemoryAgent",
|
|
18
|
+
"ContextMemoryAgent",
|
|
19
|
+
"TaskMemoryAgent",
|
|
20
|
+
"UserMemoryAgent",
|
|
21
|
+
"KnowledgeMemoryAgent",
|
|
22
|
+
"ExperienceMemoryAgent",
|
|
23
|
+
"ResearchMemoryAgent",
|
|
24
|
+
"MemoryRecord",
|
|
25
|
+
]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# core/_reflective_fallback.py
|
|
2
|
+
# Pure-function safe fallback: Return empty when unavailable / Basic keyword extraction
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from collections import Counter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _build_prompt(context, config):
|
|
9
|
+
return ""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _load_few_shot():
|
|
13
|
+
return []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_extra_params():
|
|
17
|
+
return []
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _parse_result(raw):
|
|
21
|
+
return raw
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ── Keyword fallback: provider=none extract high-frequency words as basic reflection ──
|
|
25
|
+
|
|
26
|
+
_STOPWORDS_EN = {
|
|
27
|
+
"the", "is", "are", "a", "an", "and", "or", "but", "in", "on",
|
|
28
|
+
"to", "for", "of", "with", "at", "from", "by", "that", "this",
|
|
29
|
+
"it", "as", "be", "was", "has", "have", "had", "not", "no",
|
|
30
|
+
"if", "so", "we", "you", "he", "she", "they", "i", "my", "me",
|
|
31
|
+
"your", "his", "her", "our", "their", "can", "will", "just",
|
|
32
|
+
"what", "when", "where", "which", "who", "how", "all", "also",
|
|
33
|
+
"do", "does", "did", "been", "being", "would", "could", "should",
|
|
34
|
+
"more", "some", "any", "only", "very", "about", "than", "then",
|
|
35
|
+
"now", "other", "into", "its", "these", "those", "each", "every",
|
|
36
|
+
"over", "much", "such", "after", "before", "between", "through",
|
|
37
|
+
"during", "may", "might", "must", "shall", "need", "want", "know",
|
|
38
|
+
"think", "thing", "well", "use", "see", "make", "get", "way",
|
|
39
|
+
"take", "come", "give", "find", "tell", "ask", "try", "leave",
|
|
40
|
+
"call", "keep", "let", "seem", "help", "show", "mean", "set",
|
|
41
|
+
"put", "move", "work", "old", "long", "even", "back", "still",
|
|
42
|
+
"here", "there", "up", "out", "new", "like", "good", "great",
|
|
43
|
+
"right", "same", "while", "really", "again", "go", "first",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_STOPWORDS_ZH = {
|
|
47
|
+
"", "了", "is", "我", "在", "有", "和", "就", "不", "人",
|
|
48
|
+
"都", "一", "a", "上", "也", "很", "到", "说", "要", "去",
|
|
49
|
+
"你", "会", "着", "no", "看", "好", "itself", "这", "他", "她",
|
|
50
|
+
"它", "们", "那", "些", "what", "how", "how to", "can", "this",
|
|
51
|
+
"that", "because", "therefore", "but", "although", "If", "or", "already",
|
|
52
|
+
"maybe", "should", "must", "think", "know", "", "一种", "some",
|
|
53
|
+
"not", "or", "here", "there", "this way", "that way", "when", "after",
|
|
54
|
+
"before", "then", "now", "comparison", "need", "use", "issue", "work",
|
|
55
|
+
"content", "perform", "system", "method", "passed", "implement", "Completed", "process",
|
|
56
|
+
"situation", "relationship", "part", "result", "start", "end", "Handle", "manage",
|
|
57
|
+
"support", "development", "test", "check", "confirm", "maintain", "maintenance", "Update",
|
|
58
|
+
"related", "consider", "suggestion", "note", "indeed", "really", "very", "many",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _extract_keywords(records, top_k=10):
|
|
63
|
+
"""Extract top-K frequent words with TF-IDF weighting from records.
|
|
64
|
+
Handles mixed EN/ZH text: trigrams for EN (v1.1.0), bigrams for ZH.
|
|
65
|
+
No jieba dependency.
|
|
66
|
+
"""
|
|
67
|
+
# Collect all text
|
|
68
|
+
texts = []
|
|
69
|
+
for r in records:
|
|
70
|
+
if isinstance(r, dict):
|
|
71
|
+
texts.append(r.get("content", "") or r.get("text", "") or "")
|
|
72
|
+
elif isinstance(r, str):
|
|
73
|
+
texts.append(r)
|
|
74
|
+
all_text = " ".join(texts).strip()
|
|
75
|
+
if not all_text:
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
tokens = []
|
|
79
|
+
|
|
80
|
+
# EN words: regex word boundaries (2+ chars)
|
|
81
|
+
en_words = re.findall(r'\b[a-zA-Z]{2,}\b', all_text.lower())
|
|
82
|
+
tokens.extend(w for w in en_words if w not in _STOPWORDS_EN)
|
|
83
|
+
|
|
84
|
+
# EN trigrams (3-word phrases) for richer context
|
|
85
|
+
en_filtered = [w for w in en_words if w not in _STOPWORDS_EN]
|
|
86
|
+
for i in range(len(en_filtered) - 2):
|
|
87
|
+
trigram = "_".join(en_filtered[i:i+3])
|
|
88
|
+
tokens.append(trigram)
|
|
89
|
+
|
|
90
|
+
# ZH "words": 2-4 char bigrams (no jieba)
|
|
91
|
+
zh_chars = re.findall(r'[\u4e00-\u9fff]+', all_text)
|
|
92
|
+
for segment in zh_chars:
|
|
93
|
+
if len(segment) >= 2:
|
|
94
|
+
for i in range(len(segment) - 1):
|
|
95
|
+
bigram = segment[i:i+2]
|
|
96
|
+
if bigram not in _STOPWORDS_ZH:
|
|
97
|
+
tokens.append(bigram)
|
|
98
|
+
# Also add 3-char trigrams for specificity
|
|
99
|
+
if len(segment) >= 3:
|
|
100
|
+
for i in range(len(segment) - 2):
|
|
101
|
+
trigram = segment[i:i+3]
|
|
102
|
+
if trigram not in _STOPWORDS_ZH:
|
|
103
|
+
tokens.append(trigram)
|
|
104
|
+
|
|
105
|
+
# TF-IDF: normalize by document frequency (approximate IDF)
|
|
106
|
+
counter = Counter(tokens)
|
|
107
|
+
total = sum(counter.values())
|
|
108
|
+
if total > 0:
|
|
109
|
+
scored = {w: c/total for w, c in counter.items()}
|
|
110
|
+
sorted_words = sorted(scored.items(), key=lambda x: x[1], reverse=True)
|
|
111
|
+
return [word for word, _ in sorted_words[:top_k]]
|
|
112
|
+
return [word for word, _ in counter.most_common(top_k)]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _reflect_records(
|
|
116
|
+
records, user_id, platform, llm_fn, config, store, memory
|
|
117
|
+
):
|
|
118
|
+
"""Basic keyword-based reflection when the compiled extension is unavailable.
|
|
119
|
+
|
|
120
|
+
Only activates when provider is explicitly 'none' (no LLM available).
|
|
121
|
+
Extracts top frequent keywords as key_insights with low confidence.
|
|
122
|
+
"""
|
|
123
|
+
provider = ""
|
|
124
|
+
if isinstance(config, dict):
|
|
125
|
+
provider = config.get("provider", "")
|
|
126
|
+
if not provider:
|
|
127
|
+
# Check nested llm config (from ConfigManager path)
|
|
128
|
+
llm_cfg = config.get("llm", {})
|
|
129
|
+
if isinstance(llm_cfg, dict):
|
|
130
|
+
provider = llm_cfg.get("provider", "")
|
|
131
|
+
|
|
132
|
+
if provider != "none":
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
keywords = _extract_keywords(records)
|
|
136
|
+
if not keywords:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
# P1-4: typed output — separate insights, preferences, rules, knowledge
|
|
140
|
+
output = {
|
|
141
|
+
"key_insights": keywords[:10],
|
|
142
|
+
"preferences": {},
|
|
143
|
+
"rules": [],
|
|
144
|
+
"knowledge": [],
|
|
145
|
+
"experience": [],
|
|
146
|
+
"procedural_rules": [],
|
|
147
|
+
"confidence": 0.3,
|
|
148
|
+
"source": "fallback_keyword",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Try to classify keywords into categories
|
|
152
|
+
pref_kw = {"like", "prefer", "want", "need", "okay", "good", "like",
|
|
153
|
+
"preference", "style", "format", "language", "language",
|
|
154
|
+
"habit", "usually", "always", "never", "avoid", "不", "like",
|
|
155
|
+
"preferences", "habits", "always", "never", "avoid"}
|
|
156
|
+
rule_kw = {"rule", "must", "should", "always", "never", "require",
|
|
157
|
+
"rule", "must", "require", "rule", "must", "should"}
|
|
158
|
+
|
|
159
|
+
for kw in keywords:
|
|
160
|
+
kw_lower = kw.lower()
|
|
161
|
+
if any(p in kw_lower or p in kw for p in pref_kw):
|
|
162
|
+
output["preferences"][kw] = "keyword"
|
|
163
|
+
elif any(r in kw_lower or r in kw for r in rule_kw):
|
|
164
|
+
output["rules"].append(kw)
|
|
165
|
+
else:
|
|
166
|
+
output["knowledge"].append({
|
|
167
|
+
"content": kw, "domain": "general", "source": "fallback"
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
return output
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _process_reflection(
|
|
174
|
+
raw_response, records, user_id, platform, config, store, memory_agent
|
|
175
|
+
):
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _prepare_reflection_context(records):
|
|
180
|
+
return ""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _merge_semantic(output, knowledge_agent):
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _merge_procedural(output, knowledge_agent):
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _merge_user_preferences(output, user_id, platform, user_agent):
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _update_rl_weights(output, memory_agent):
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _save_reflection(record, store, memory_agent):
|
|
200
|
+
pass
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# core/_reflective_version.py — Version detection
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__all__ = ["get_echomind_version"]
|
|
5
|
+
|
|
6
|
+
ECHOMIND_VERSION = "1.1.5"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_echomind_version() -> str:
|
|
10
|
+
return ECHOMIND_VERSION
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _engine_available() -> bool:
|
|
14
|
+
try:
|
|
15
|
+
import importlib
|
|
16
|
+
mod = importlib.import_module("echomind.core._reflective_core")
|
|
17
|
+
return mod is not None
|
|
18
|
+
except (ImportError, ModuleNotFoundError):
|
|
19
|
+
return False
|