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
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,5 @@
1
+ # EchoMind platform adapters
2
+ from .hermes_provider import EchomindMemoryProvider
3
+ from .http_api import app
4
+
5
+ __all__ = ["EchomindMemoryProvider", "app"]
@@ -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)