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.
Files changed (42) hide show
  1. echomind/__init__.py +34 -0
  2. echomind/adapters/__init__.py +5 -0
  3. echomind/adapters/hermes_provider.py +530 -0
  4. echomind/adapters/http_api.py +323 -0
  5. echomind/code_format/__init__.py +0 -0
  6. echomind/code_format/cli.py +74 -0
  7. echomind/code_format/memory.schema.json +33 -0
  8. echomind/core/__init__.py +25 -0
  9. echomind/core/_reflective_fallback.py +200 -0
  10. echomind/core/_reflective_version.py +19 -0
  11. echomind/core/agents/__init__.py +17 -0
  12. echomind/core/agents/context_agent.py +25 -0
  13. echomind/core/agents/experience_agent.py +76 -0
  14. echomind/core/agents/knowledge_agent.py +100 -0
  15. echomind/core/agents/research_agent.py +100 -0
  16. echomind/core/agents/task_agent.py +63 -0
  17. echomind/core/agents/user_agent.py +60 -0
  18. echomind/core/config_manager.py +218 -0
  19. echomind/core/learning/__init__.py +3 -0
  20. echomind/core/learning/rl_weight_optimizer.py +170 -0
  21. echomind/core/llm_client.py +210 -0
  22. echomind/core/memory_agent.py +807 -0
  23. echomind/core/models/__init__.py +13 -0
  24. echomind/core/models/context.py +15 -0
  25. echomind/core/models/experience.py +19 -0
  26. echomind/core/models/knowledge.py +11 -0
  27. echomind/core/models/reflection.py +45 -0
  28. echomind/core/models/research.py +34 -0
  29. echomind/core/models/task.py +20 -0
  30. echomind/core/models/user.py +12 -0
  31. echomind/core/reflective_agent.py +128 -0
  32. echomind/core/storage/__init__.py +3 -0
  33. echomind/core/storage/sqlite_store.py +648 -0
  34. echomind/domain_keywords.yaml +159 -0
  35. echomind/echomind_config.yaml +113 -0
  36. echomind/main.py +200 -0
  37. echomind_memory-1.1.5.dist-info/METADATA +118 -0
  38. echomind_memory-1.1.5.dist-info/RECORD +42 -0
  39. echomind_memory-1.1.5.dist-info/WHEEL +5 -0
  40. echomind_memory-1.1.5.dist-info/entry_points.txt +2 -0
  41. echomind_memory-1.1.5.dist-info/licenses/LICENSE +21 -0
  42. 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