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
echomind/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""EchoMind Memory Skill — Hermes Plugin
|
|
2
|
+
AI Persistent memory system: 6 memory types + RL self-optimization + Self-Reflective Agent
|
|
3
|
+
|
|
4
|
+
as MemoryProvider: set memory.provider: echomind in config.yaml
|
|
5
|
+
as Plugin: run hermes plugins enable echomind
|
|
6
|
+
"""
|
|
7
|
+
import os, sys
|
|
8
|
+
# Hermes plugin loader creates this module as _hermes_user_memory.echomind
|
|
9
|
+
# with spec_from_file_location — Python has no package context, so relative
|
|
10
|
+
# imports fail. Add plugin dir to sys.path so absolute imports work.
|
|
11
|
+
_plugin_dir = os.path.dirname(os.path.abspath(__file__))
|
|
12
|
+
_pkg_root = os.path.dirname(_plugin_dir) # 父目录:让 `import echomind` 可解析
|
|
13
|
+
for _p in (_pkg_root, _plugin_dir):
|
|
14
|
+
if _p not in sys.path:
|
|
15
|
+
sys.path.insert(0, _p)
|
|
16
|
+
|
|
17
|
+
from echomind.core._reflective_version import get_echomind_version
|
|
18
|
+
|
|
19
|
+
__version__ = get_echomind_version()
|
|
20
|
+
|
|
21
|
+
from echomind.adapters.hermes_provider import EchomindMemoryProvider
|
|
22
|
+
|
|
23
|
+
__all__ = ["EchomindMemoryProvider", "__version__"]
|
|
24
|
+
|
|
25
|
+
# ── Startup confirmation (user-visible, for Hermes plugin load) ──
|
|
26
|
+
try:
|
|
27
|
+
from echomind.core.storage.sqlite_store import SqliteStore
|
|
28
|
+
db = SqliteStore()
|
|
29
|
+
db.connect()
|
|
30
|
+
db.ensure_tables()
|
|
31
|
+
row_count = db._conn.execute("SELECT count(*) FROM user_memory").fetchone()[0]
|
|
32
|
+
print(f"🧠 EchoMind v{__version__} — 记忆存储正常 ({row_count} 条用户记忆)")
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"⚠️ EchoMind v{__version__} — 记忆存储异常: {e}")
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
# EchoMind Memory — Hermes MemoryProvider Adapter
|
|
2
|
+
# implement Hermes v0.13.0+ MemoryProvider interface, providing 100% automated memory access
|
|
3
|
+
#
|
|
4
|
+
# Install: copy to ~/.hermes/plugins/echomind-memory/
|
|
5
|
+
# Activate: hermes config set memory.provider echomind
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import sys
|
|
10
|
+
import os
|
|
11
|
+
import random
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
import requests # Hermes bundled, zero new dependencies
|
|
15
|
+
|
|
16
|
+
# Safe import MemoryProvider ABC (supports packaged + source Hermes)
|
|
17
|
+
try:
|
|
18
|
+
from agent.memory_provider import MemoryProvider
|
|
19
|
+
except ImportError:
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
|
|
22
|
+
class MemoryProvider(ABC):
|
|
23
|
+
"""Minimal MemoryProvider fallback for when Hermes agent module is unavailable."""
|
|
24
|
+
name: str = ""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def is_available(self) -> bool: ...
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def initialize(self, session_id: str, **kwargs): ...
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def get_tool_schemas(self): ...
|
|
32
|
+
|
|
33
|
+
# Ensure core module is importable
|
|
34
|
+
_skill_dir = os.path.dirname(os.path.abspath(__file__)) # .../echomind/adapters
|
|
35
|
+
_pkg_dir = os.path.dirname(_skill_dir) # .../echomind
|
|
36
|
+
_root = os.path.dirname(_pkg_dir) # src/ —— 让 `import echomind` 可解析
|
|
37
|
+
for _p in (_root, _pkg_dir):
|
|
38
|
+
if _p not in sys.path:
|
|
39
|
+
sys.path.insert(0, _p)
|
|
40
|
+
|
|
41
|
+
from echomind.core.memory_agent import MainMemoryAgent
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger("EchomindProvider")
|
|
44
|
+
|
|
45
|
+
PLATFORM = "hermes"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class EchomindMemoryProvider(MemoryProvider):
|
|
49
|
+
"""EchoMind Memory Provider — Hermes Agent loop deep integration
|
|
50
|
+
|
|
51
|
+
Automatic invocation sequence (managed by Hermes run_agent.py):
|
|
52
|
+
- before each turn: prefetch(query, session_id) → retrieve memory, inject system prompt
|
|
53
|
+
- after each turn: sync_turn(user, assistant, session_id) → store current-turn conversation
|
|
54
|
+
- after each turn: queue_prefetch(query, session_id) → queue for next-turn retrieval
|
|
55
|
+
- session switch: on_session_switch(new_sid, ...) → Update session state
|
|
56
|
+
- before compression: on_pre_compress(messages) → extract memories about to be compressed
|
|
57
|
+
- session end: shutdown() → Persist + cleanup
|
|
58
|
+
|
|
59
|
+
also exposes memory_search/memory_retrieve tools, LLM can call explicitly.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
name = "echomind"
|
|
63
|
+
|
|
64
|
+
def __init__(self):
|
|
65
|
+
self._agent: Optional[MainMemoryAgent] = None
|
|
66
|
+
self._session_id: str = ""
|
|
67
|
+
self._user_id: str = ""
|
|
68
|
+
self._turn_count: int = 0
|
|
69
|
+
self._context_buffer: List[Dict] = []
|
|
70
|
+
self._skip_writes: bool = False # non-primary context Skip write
|
|
71
|
+
self._project_id: str = "default"
|
|
72
|
+
self._session_title: str = ""
|
|
73
|
+
|
|
74
|
+
# ═══════════════════════════════════════════════════
|
|
75
|
+
# Lifecycle
|
|
76
|
+
# ═══════════════════════════════════════════════════
|
|
77
|
+
|
|
78
|
+
def is_available(self) -> bool:
|
|
79
|
+
"""Check if available(no network required, local SQLite)"""
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
def initialize(self, session_id: str, **kwargs):
|
|
83
|
+
"""Hermes called on startup, connects SQLite + Load history on startup
|
|
84
|
+
|
|
85
|
+
kwargs may contain: hermes_home, platform, agent_context, user_id etc.
|
|
86
|
+
agent_context used to distinguish primary/subagent/cron/flush scenarios.
|
|
87
|
+
"""
|
|
88
|
+
# Non-primary context (subagent/cron/flush) — do not write to user memory
|
|
89
|
+
agent_ctx = kwargs.get("agent_context", "primary")
|
|
90
|
+
if agent_ctx != "primary":
|
|
91
|
+
logger.info(
|
|
92
|
+
"EchoMind: skipping initialize for non-primary context=%s session=%s",
|
|
93
|
+
agent_ctx, session_id,
|
|
94
|
+
)
|
|
95
|
+
self._skip_writes = True
|
|
96
|
+
self._session_id = session_id
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
self._skip_writes = False
|
|
100
|
+
self._session_id = session_id
|
|
101
|
+
self._user_id = kwargs.get("user_id", session_id)
|
|
102
|
+
self._project_id = kwargs.get("project", "default")
|
|
103
|
+
self._turn_count = 0
|
|
104
|
+
self._context_buffer = []
|
|
105
|
+
|
|
106
|
+
self._agent = MainMemoryAgent()
|
|
107
|
+
self._agent.enable_persistence()
|
|
108
|
+
|
|
109
|
+
logger.info(
|
|
110
|
+
f"EchoMind Memory initialized: session={session_id}, "
|
|
111
|
+
f"user={self._user_id}, platform={PLATFORM}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def shutdown(self):
|
|
115
|
+
"""Hermes called on exit"""
|
|
116
|
+
if self._agent:
|
|
117
|
+
self._agent.disable_persistence()
|
|
118
|
+
logger.info("EchoMind Memory shutdown")
|
|
119
|
+
|
|
120
|
+
def get_config_schema(self):
|
|
121
|
+
"""Hermes v0.13.0+ config wizard schema"""
|
|
122
|
+
return []
|
|
123
|
+
|
|
124
|
+
def save_config(self, values, hermes_home: str) -> None:
|
|
125
|
+
"""Hermes v0.13.0+ save config from setup wizard"""
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
# ═══════════════════════════════════════════════════
|
|
129
|
+
# Core methods called automatically (agent_loop driven, 100% reliable)
|
|
130
|
+
# ═══════════════════════════════════════════════════
|
|
131
|
+
|
|
132
|
+
def system_prompt_block(self) -> str:
|
|
133
|
+
"""injected into system prompt static text of"""
|
|
134
|
+
return (
|
|
135
|
+
"[EchoMind Memory]\n"
|
|
136
|
+
"Long-term memory system activated. Your conversations will be automatically recorded and retrieved.\n"
|
|
137
|
+
"For explicit search: use memory_search(query) to find relevant memories.\n"
|
|
138
|
+
"To view user profile: use memory_retrieve() to get preferences and history.\n"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def prefetch(self, query: str, session_id: str) -> str:
|
|
142
|
+
"""Automatically called before each conversation turn — Retrieve relevant memories and inject into context
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Formatted memory text, injected into the current conversation's messages
|
|
146
|
+
"""
|
|
147
|
+
if self._skip_writes or not self._agent:
|
|
148
|
+
return ""
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
result = self._agent.retrieve_for_task(
|
|
152
|
+
task_context=query,
|
|
153
|
+
user_id=self._user_id,
|
|
154
|
+
task_id=session_id,
|
|
155
|
+
platform=PLATFORM,
|
|
156
|
+
project=self._project_id,
|
|
157
|
+
session_id=self._session_id,
|
|
158
|
+
)
|
|
159
|
+
return self._format_prefetch_context(result)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.error(f"prefetch error: {e}")
|
|
162
|
+
return ""
|
|
163
|
+
|
|
164
|
+
def queue_prefetch(self, query: str, session_id: str = "") -> None:
|
|
165
|
+
"""Queue background prefetch -- Hermes v0.13.0+ new interface.
|
|
166
|
+
|
|
167
|
+
echomind prefetch is synchronous (local SQLite), no queue needed,
|
|
168
|
+
but must exist for compatibility with MemoryProvider ABC.
|
|
169
|
+
"""
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
def sync_turn(self, user_content: str, assistant_content: str, session_id: str):
|
|
173
|
+
"""Automatically called after each conversation turn — store current turn to SQLite"""
|
|
174
|
+
if self._skip_writes or not self._agent:
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
self._turn_count += 1
|
|
178
|
+
self._session_id = session_id
|
|
179
|
+
|
|
180
|
+
# Build context message list
|
|
181
|
+
messages = []
|
|
182
|
+
if self._context_buffer:
|
|
183
|
+
messages.extend(self._context_buffer)
|
|
184
|
+
self._context_buffer = []
|
|
185
|
+
messages.extend([
|
|
186
|
+
{"role": "user", "content": user_content},
|
|
187
|
+
{"role": "assistant", "content": assistant_content},
|
|
188
|
+
])
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
self._agent.store(
|
|
192
|
+
user_id=self._user_id,
|
|
193
|
+
task_id=f"{session_id}:turn{self._turn_count}",
|
|
194
|
+
context=messages,
|
|
195
|
+
task_status="completed",
|
|
196
|
+
success=True,
|
|
197
|
+
platform=PLATFORM,
|
|
198
|
+
project=self._project_id,
|
|
199
|
+
session_id=session_id,
|
|
200
|
+
)
|
|
201
|
+
logger.debug(f"sync_turn: turn {self._turn_count} stored")
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.error(f"sync_turn error: {e}")
|
|
204
|
+
# cache to on failure buffer retry on next
|
|
205
|
+
self._context_buffer = messages[-6:] # keep recent 6 messages
|
|
206
|
+
|
|
207
|
+
# ═══════════════════════════════════════════════════
|
|
208
|
+
# LLM Tool definitions(Explicit invocation, supplementing auto-access)
|
|
209
|
+
# ═══════════════════════════════════════════════════
|
|
210
|
+
|
|
211
|
+
def get_tool_schemas(self) -> List[Dict]:
|
|
212
|
+
"""exposed to LLM tool definitions for"""
|
|
213
|
+
return [
|
|
214
|
+
{
|
|
215
|
+
"name": "memory_search",
|
|
216
|
+
"description": "Search long-term memory for relevant content. Used to find previously discussed topics, decisions, or preferences.",
|
|
217
|
+
"parameters": {
|
|
218
|
+
"type": "object",
|
|
219
|
+
"properties": {
|
|
220
|
+
"query": {
|
|
221
|
+
"type": "string",
|
|
222
|
+
"description": "Search keywords or questions, e.g. 'Python web crawler'、'last week's architecture discussion'",
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
"required": ["query"],
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"name": "memory_retrieve",
|
|
230
|
+
"description": "Get the current user's complete memory profile, including preferences, habits, and recent activity.",
|
|
231
|
+
"parameters": {
|
|
232
|
+
"type": "object",
|
|
233
|
+
"properties": {
|
|
234
|
+
"detail_level": {
|
|
235
|
+
"type": "string",
|
|
236
|
+
"enum": ["brief", "full"],
|
|
237
|
+
"default": "brief",
|
|
238
|
+
"description": "Detail level: brief=summary full=full memory",
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs) -> str:
|
|
246
|
+
"""Handle LLM Tools called explicitly.
|
|
247
|
+
|
|
248
|
+
Hermes ABC Requires returning JSON string.
|
|
249
|
+
"""
|
|
250
|
+
if not self._agent:
|
|
251
|
+
return json.dumps({"error": "EchoMind Not initialized"}, ensure_ascii=False)
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
if tool_name == "memory_search":
|
|
255
|
+
result = self._search(args.get("query", ""))
|
|
256
|
+
return json.dumps({"result": result}, ensure_ascii=False)
|
|
257
|
+
elif tool_name == "memory_retrieve":
|
|
258
|
+
result = self._retrieve(args.get("detail_level", "brief"))
|
|
259
|
+
return json.dumps({"result": result}, ensure_ascii=False)
|
|
260
|
+
else:
|
|
261
|
+
return json.dumps({"error": f"Unknown tool: {tool_name}"}, ensure_ascii=False)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
logger.error(f"handle_tool_call error: {e}")
|
|
264
|
+
return json.dumps({"error": f"Memory operation failed: {str(e)}"}, ensure_ascii=False)
|
|
265
|
+
|
|
266
|
+
# ═══════════════════════════════════════════════════
|
|
267
|
+
# Optional hooks(Hermes v0.13.0+ standard)
|
|
268
|
+
# ═══════════════════════════════════════════════════
|
|
269
|
+
|
|
270
|
+
def on_turn_start(self, turn_number: int, message: str, **kwargs):
|
|
271
|
+
"""Hook at the start of each turn(Can be used for preloading)"""
|
|
272
|
+
self._turn_count = turn_number
|
|
273
|
+
|
|
274
|
+
def on_session_end(self, messages: List[Dict]):
|
|
275
|
+
"""Hook at the end of a session.
|
|
276
|
+
|
|
277
|
+
Store session summary to long-term memory.
|
|
278
|
+
"""
|
|
279
|
+
if self._skip_writes or not self._agent or not messages:
|
|
280
|
+
return
|
|
281
|
+
try:
|
|
282
|
+
# Save full transcript for session search
|
|
283
|
+
if self._agent and getattr(self._agent, '_persistence_enabled', False):
|
|
284
|
+
try:
|
|
285
|
+
self._agent.db.save_transcript(
|
|
286
|
+
self._session_id, self._user_id,
|
|
287
|
+
messages, project=self._project_id)
|
|
288
|
+
except Exception as ex:
|
|
289
|
+
logger.error(f"transcript save error: {ex}")
|
|
290
|
+
# Filter to standard role/content messages only
|
|
291
|
+
clean_messages = [
|
|
292
|
+
{"role": m["role"], "content": m["content"]}
|
|
293
|
+
for m in messages[-10:]
|
|
294
|
+
if isinstance(m, dict) and "role" in m and "content" in m
|
|
295
|
+
]
|
|
296
|
+
if not clean_messages:
|
|
297
|
+
return
|
|
298
|
+
self._agent.store(
|
|
299
|
+
user_id=self._user_id,
|
|
300
|
+
task_id=f"{self._session_id}:summary",
|
|
301
|
+
context=clean_messages,
|
|
302
|
+
task_status="session_end",
|
|
303
|
+
success=True,
|
|
304
|
+
platform=PLATFORM,
|
|
305
|
+
project=self._project_id,
|
|
306
|
+
session_id=self._session_id,
|
|
307
|
+
)
|
|
308
|
+
logger.info("on_session_end: session summary stored")
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.error(f"on_session_end error: {e}")
|
|
311
|
+
|
|
312
|
+
# v1.1.0: Check if reflection should be triggered
|
|
313
|
+
self._trigger_reflection_if_needed()
|
|
314
|
+
|
|
315
|
+
def on_session_switch(
|
|
316
|
+
self,
|
|
317
|
+
new_session_id: str,
|
|
318
|
+
parent_session_id: str = "",
|
|
319
|
+
reset: bool = False,
|
|
320
|
+
**kwargs,
|
|
321
|
+
):
|
|
322
|
+
"""Called on session switch(Hermes v0.13.0+ New)。
|
|
323
|
+
|
|
324
|
+
/resume, /branch, /reset, context Triggered by operations like compression.
|
|
325
|
+
Update internal session_id state, ensuring subsequent writes use the correct session。
|
|
326
|
+
"""
|
|
327
|
+
if reset:
|
|
328
|
+
# /new, /reset: Clear buffer
|
|
329
|
+
self._context_buffer = []
|
|
330
|
+
self._turn_count = 0
|
|
331
|
+
self._session_id = new_session_id
|
|
332
|
+
logger.info(
|
|
333
|
+
"on_session_switch: new=%s parent=%s reset=%s",
|
|
334
|
+
new_session_id, parent_session_id, reset,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def on_pre_compress(self, messages: List[Dict[str, Any]]) -> str:
|
|
338
|
+
"""Called before context compression(Hermes v0.13.0+ New)。
|
|
339
|
+
|
|
340
|
+
Extract retainable memories from messages before they are discarded.
|
|
341
|
+
Return empty string to indicate no additional compression hints(Memory auto-stored)。
|
|
342
|
+
"""
|
|
343
|
+
if self._skip_writes or not self._agent or not messages:
|
|
344
|
+
return ""
|
|
345
|
+
try:
|
|
346
|
+
# Store messages about to be compressed into long-term memory
|
|
347
|
+
self._agent.store(
|
|
348
|
+
user_id=self._user_id,
|
|
349
|
+
task_id=f"{self._session_id}:compress",
|
|
350
|
+
context=messages[-20:],
|
|
351
|
+
task_status="compressed",
|
|
352
|
+
project=self._project_id,
|
|
353
|
+
session_id=self._session_id,
|
|
354
|
+
success=True,
|
|
355
|
+
platform=PLATFORM,
|
|
356
|
+
)
|
|
357
|
+
logger.debug("on_pre_compress: %d messages preserved", min(20, len(messages)))
|
|
358
|
+
except Exception as e:
|
|
359
|
+
logger.error(f"on_pre_compress error: {e}")
|
|
360
|
+
return ""
|
|
361
|
+
|
|
362
|
+
def on_delegation(self, task: str, result: str, child_session_id: str = "", **kwargs):
|
|
363
|
+
"""Sub-agent completed callback (Hermes v0.13.0+).
|
|
364
|
+
|
|
365
|
+
Store child agent task and result as experience.
|
|
366
|
+
"""
|
|
367
|
+
if self._skip_writes or not self._agent:
|
|
368
|
+
return
|
|
369
|
+
try:
|
|
370
|
+
self._agent.store(
|
|
371
|
+
user_id=self._user_id,
|
|
372
|
+
task_id=f"{self._session_id}:delegation:{child_session_id}",
|
|
373
|
+
context=[
|
|
374
|
+
{"role": "user", "content": f"[Delegation Task]\n{task}"},
|
|
375
|
+
{"role": "assistant", "content": f"[Delegation Result]\n{result[:2000]}"},
|
|
376
|
+
],
|
|
377
|
+
task_status="delegated",
|
|
378
|
+
success=True,
|
|
379
|
+
platform=PLATFORM,
|
|
380
|
+
)
|
|
381
|
+
logger.debug("on_delegation: child=%s stored", child_session_id)
|
|
382
|
+
except Exception as e:
|
|
383
|
+
logger.error(f"on_delegation error: {e}")
|
|
384
|
+
|
|
385
|
+
def _trigger_reflection_if_needed(self):
|
|
386
|
+
"""If store Count reaches threshold, auto-invoke LLM perform reflection"""
|
|
387
|
+
if not self._agent:
|
|
388
|
+
return
|
|
389
|
+
from echomind.core.config_manager import get_config_manager
|
|
390
|
+
cfg = get_config_manager().get_section("reflection")
|
|
391
|
+
batch_size = cfg.get("batch_size", 8)
|
|
392
|
+
if isinstance(batch_size, (list, tuple)):
|
|
393
|
+
batch_size = int(random.uniform(batch_size[0], batch_size[1]))
|
|
394
|
+
min_records = cfg.get("min_records", 6)
|
|
395
|
+
pending = getattr(self._agent, '_pending_reflection', False)
|
|
396
|
+
if not pending:
|
|
397
|
+
return
|
|
398
|
+
records = self._agent.get_recent_episodic(self._user_id, count=batch_size)
|
|
399
|
+
if len(records) < min_records:
|
|
400
|
+
return
|
|
401
|
+
logger.info(f"Reflection triggered: {len(records)} records")
|
|
402
|
+
try:
|
|
403
|
+
result = self._agent.reflective.reflect_with_llm(
|
|
404
|
+
records, self._user_id, PLATFORM, _hermes_llm_fn,
|
|
405
|
+
)
|
|
406
|
+
if result:
|
|
407
|
+
logger.info(
|
|
408
|
+
f"Reflection done: {len(result.key_insights)} insights "
|
|
409
|
+
f"(confidence={result.confidence:.2f})"
|
|
410
|
+
)
|
|
411
|
+
except Exception as e:
|
|
412
|
+
logger.warning(f"Reflection failed: {e}")
|
|
413
|
+
finally:
|
|
414
|
+
self._agent.clear_pending_reflection()
|
|
415
|
+
|
|
416
|
+
def on_memory_write(self, action: str, target: str, content: str, metadata: Optional[Dict] = None):
|
|
417
|
+
"""Mirror Hermes built-in memory write operations"""
|
|
418
|
+
if self._skip_writes or not self._agent:
|
|
419
|
+
return
|
|
420
|
+
mirror = [
|
|
421
|
+
{"role": "system", "content": f"[Memory {action}] target={target}"},
|
|
422
|
+
{"role": "assistant", "content": content},
|
|
423
|
+
]
|
|
424
|
+
try:
|
|
425
|
+
self._agent.store(
|
|
426
|
+
user_id=self._user_id,
|
|
427
|
+
task_id=f"{self._session_id}:memory:{action}",
|
|
428
|
+
context=mirror,
|
|
429
|
+
task_status="memory_mirror",
|
|
430
|
+
success=True,
|
|
431
|
+
platform=PLATFORM,
|
|
432
|
+
)
|
|
433
|
+
logger.info(
|
|
434
|
+
"EchoMind: mirrored memory %s target=%s content_len=%d",
|
|
435
|
+
action, target, len(content),
|
|
436
|
+
)
|
|
437
|
+
except Exception as e:
|
|
438
|
+
logger.error(
|
|
439
|
+
"EchoMind: on_memory_write failed action=%s target=%s: %s",
|
|
440
|
+
action, target, e,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# ═══════════════════════════════════════════════════
|
|
444
|
+
# Internal method
|
|
445
|
+
# ═══════════════════════════════════════════════════
|
|
446
|
+
|
|
447
|
+
def _search(self, query: str) -> str:
|
|
448
|
+
"""Execute memory search"""
|
|
449
|
+
if not query:
|
|
450
|
+
return "Please provide search keywords"
|
|
451
|
+
result = self._agent.retrieve_for_task(
|
|
452
|
+
task_context=query,
|
|
453
|
+
user_id=self._user_id,
|
|
454
|
+
platform=PLATFORM,
|
|
455
|
+
)
|
|
456
|
+
return self._format_search_result(result)
|
|
457
|
+
|
|
458
|
+
def _retrieve(self, detail: str = "brief") -> str:
|
|
459
|
+
"""Get user memory profile"""
|
|
460
|
+
user_data = self._agent.user_agent.get(self._user_id, platform=PLATFORM)
|
|
461
|
+
if detail == "brief":
|
|
462
|
+
prefs = user_data.get("preferences", {})
|
|
463
|
+
if not prefs:
|
|
464
|
+
return "No user preference records yet"
|
|
465
|
+
lines = [f"- {k}: {v}" for k, v in list(prefs.items())[:8]]
|
|
466
|
+
return "User preferences:\n" + "\n".join(lines) if lines else "No user preference records yet"
|
|
467
|
+
else:
|
|
468
|
+
return json.dumps(user_data, indent=2, ensure_ascii=False, default=str)
|
|
469
|
+
|
|
470
|
+
def _format_prefetch_context(self, result: Dict) -> str:
|
|
471
|
+
"""Format retrieval results as context injection"""
|
|
472
|
+
memories = result.get("retrieved_memories", [])
|
|
473
|
+
if not memories:
|
|
474
|
+
return ""
|
|
475
|
+
|
|
476
|
+
lines = ["[EchoMind — Relevant memories]"]
|
|
477
|
+
for mem in memories[:5]:
|
|
478
|
+
source_label = mem.source.replace("_", " ").title()
|
|
479
|
+
lines.append(f"[{source_label}] {mem.content[:200]}")
|
|
480
|
+
|
|
481
|
+
# Append user preferences
|
|
482
|
+
user_data = result.get("user", {})
|
|
483
|
+
prefs = user_data.get("preferences", {})
|
|
484
|
+
if prefs:
|
|
485
|
+
pref_items = ", ".join(f"{k}={v}" for k, v in list(prefs.items())[:5])
|
|
486
|
+
lines.append(f"[User Prefs] {pref_items}")
|
|
487
|
+
|
|
488
|
+
return "\n".join(lines)
|
|
489
|
+
|
|
490
|
+
def _format_search_result(self, result: Dict) -> str:
|
|
491
|
+
"""Format retrieval results as LLM friendly text"""
|
|
492
|
+
memories = result.get("retrieved_memories", [])
|
|
493
|
+
if not memories:
|
|
494
|
+
return "No relevant memories found"
|
|
495
|
+
|
|
496
|
+
lines = ["Found the following relevant memories:"]
|
|
497
|
+
for i, mem in enumerate(memories[:8], 1):
|
|
498
|
+
lines.append(f"{i}. [{mem.source}] {mem.content[:150]}")
|
|
499
|
+
return "\n".join(lines)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# Hermes plugin Registration entry point
|
|
503
|
+
# Hermes will through importlib load this module and call register()
|
|
504
|
+
def register(ctx, **kwargs):
|
|
505
|
+
provider = EchomindMemoryProvider()
|
|
506
|
+
ctx.register_memory_provider(provider)
|
|
507
|
+
logger.info("EchoMind Memory Provider registered for Hermes")
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# v1.1.0: LLM function injection(passed UnifiedLLMClient Unified invocation, supports external API)
|
|
511
|
+
_llm_client = None
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _get_llm_client():
|
|
515
|
+
global _llm_client
|
|
516
|
+
if _llm_client is None:
|
|
517
|
+
from echomind.core.llm_client import get_llm_client
|
|
518
|
+
_llm_client = get_llm_client()
|
|
519
|
+
return _llm_client
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _hermes_llm_fn(prompt: str) -> str:
|
|
523
|
+
"""passed UnifiedLLMClient calls LLM for reflection.
|
|
524
|
+
|
|
525
|
+
Supports external API endpoints (OpenAI / vLLM / Ollama, etc.).
|
|
526
|
+
"""
|
|
527
|
+
client = _get_llm_client()
|
|
528
|
+
if client is None:
|
|
529
|
+
return ""
|
|
530
|
+
return client.chat(prompt)
|