noesium 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- noesium/core/__init__.py +4 -0
- noesium/core/agent/__init__.py +14 -0
- noesium/core/agent/base.py +227 -0
- noesium/core/consts.py +6 -0
- noesium/core/goalith/conflict/conflict.py +104 -0
- noesium/core/goalith/conflict/detector.py +53 -0
- noesium/core/goalith/decomposer/__init__.py +6 -0
- noesium/core/goalith/decomposer/base.py +46 -0
- noesium/core/goalith/decomposer/callable_decomposer.py +65 -0
- noesium/core/goalith/decomposer/llm_decomposer.py +326 -0
- noesium/core/goalith/decomposer/prompts.py +140 -0
- noesium/core/goalith/decomposer/simple_decomposer.py +61 -0
- noesium/core/goalith/errors.py +22 -0
- noesium/core/goalith/goalgraph/graph.py +526 -0
- noesium/core/goalith/goalgraph/node.py +179 -0
- noesium/core/goalith/replanner/base.py +31 -0
- noesium/core/goalith/replanner/replanner.py +36 -0
- noesium/core/goalith/service.py +26 -0
- noesium/core/llm/__init__.py +154 -0
- noesium/core/llm/base.py +152 -0
- noesium/core/llm/litellm.py +528 -0
- noesium/core/llm/llamacpp.py +487 -0
- noesium/core/llm/message.py +184 -0
- noesium/core/llm/ollama.py +459 -0
- noesium/core/llm/openai.py +520 -0
- noesium/core/llm/openrouter.py +89 -0
- noesium/core/llm/prompt.py +551 -0
- noesium/core/memory/__init__.py +11 -0
- noesium/core/memory/base.py +464 -0
- noesium/core/memory/memu/__init__.py +24 -0
- noesium/core/memory/memu/config/__init__.py +26 -0
- noesium/core/memory/memu/config/activity/config.py +46 -0
- noesium/core/memory/memu/config/event/config.py +46 -0
- noesium/core/memory/memu/config/markdown_config.py +241 -0
- noesium/core/memory/memu/config/profile/config.py +48 -0
- noesium/core/memory/memu/llm_adapter.py +129 -0
- noesium/core/memory/memu/memory/__init__.py +31 -0
- noesium/core/memory/memu/memory/actions/__init__.py +40 -0
- noesium/core/memory/memu/memory/actions/add_activity_memory.py +299 -0
- noesium/core/memory/memu/memory/actions/base_action.py +342 -0
- noesium/core/memory/memu/memory/actions/cluster_memories.py +262 -0
- noesium/core/memory/memu/memory/actions/generate_suggestions.py +198 -0
- noesium/core/memory/memu/memory/actions/get_available_categories.py +66 -0
- noesium/core/memory/memu/memory/actions/link_related_memories.py +515 -0
- noesium/core/memory/memu/memory/actions/run_theory_of_mind.py +254 -0
- noesium/core/memory/memu/memory/actions/update_memory_with_suggestions.py +514 -0
- noesium/core/memory/memu/memory/embeddings.py +130 -0
- noesium/core/memory/memu/memory/file_manager.py +306 -0
- noesium/core/memory/memu/memory/memory_agent.py +578 -0
- noesium/core/memory/memu/memory/recall_agent.py +376 -0
- noesium/core/memory/memu/memory_store.py +628 -0
- noesium/core/memory/models.py +149 -0
- noesium/core/msgbus/__init__.py +12 -0
- noesium/core/msgbus/base.py +395 -0
- noesium/core/orchestrix/__init__.py +0 -0
- noesium/core/py.typed +0 -0
- noesium/core/routing/__init__.py +20 -0
- noesium/core/routing/base.py +66 -0
- noesium/core/routing/router.py +241 -0
- noesium/core/routing/strategies/__init__.py +9 -0
- noesium/core/routing/strategies/dynamic_complexity.py +361 -0
- noesium/core/routing/strategies/self_assessment.py +147 -0
- noesium/core/routing/types.py +38 -0
- noesium/core/toolify/__init__.py +39 -0
- noesium/core/toolify/base.py +360 -0
- noesium/core/toolify/config.py +138 -0
- noesium/core/toolify/mcp_integration.py +275 -0
- noesium/core/toolify/registry.py +214 -0
- noesium/core/toolify/toolkits/__init__.py +1 -0
- noesium/core/tracing/__init__.py +37 -0
- noesium/core/tracing/langgraph_hooks.py +308 -0
- noesium/core/tracing/opik_tracing.py +144 -0
- noesium/core/tracing/token_tracker.py +166 -0
- noesium/core/utils/__init__.py +10 -0
- noesium/core/utils/logging.py +172 -0
- noesium/core/utils/statistics.py +12 -0
- noesium/core/utils/typing.py +17 -0
- noesium/core/vector_store/__init__.py +79 -0
- noesium/core/vector_store/base.py +94 -0
- noesium/core/vector_store/pgvector.py +304 -0
- noesium/core/vector_store/weaviate.py +383 -0
- noesium-0.1.0.dist-info/METADATA +525 -0
- noesium-0.1.0.dist-info/RECORD +86 -0
- noesium-0.1.0.dist-info/WHEEL +5 -0
- noesium-0.1.0.dist-info/licenses/LICENSE +21 -0
- noesium-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Link Related Memories Action
|
|
3
|
+
|
|
4
|
+
Automatically finds and links related memories using embedding search.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import math
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from .base_action import BaseAction
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LinkRelatedMemoriesAction(BaseAction):
|
|
18
|
+
"""
|
|
19
|
+
Action to find and link related memories using embedding search
|
|
20
|
+
|
|
21
|
+
This action takes a memory item and finds the most related existing memories,
|
|
22
|
+
then creates links between them that can be written into documentation.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, memory_core):
|
|
26
|
+
super().__init__(memory_core)
|
|
27
|
+
self.description = "Find and link related memories using embedding search"
|
|
28
|
+
self.filter_with_llm = False
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def action_name(self) -> str:
|
|
32
|
+
"""Return the name of this action"""
|
|
33
|
+
return "link_related_memories"
|
|
34
|
+
|
|
35
|
+
def get_schema(self) -> Dict[str, Any]:
|
|
36
|
+
"""Get OpenAI function schema for linking related memories"""
|
|
37
|
+
return {
|
|
38
|
+
"name": "link_related_memories",
|
|
39
|
+
"description": "Find related memories using embedding search and create links between them",
|
|
40
|
+
"parameters": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"properties": {
|
|
43
|
+
"character_name": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Name of the character",
|
|
46
|
+
},
|
|
47
|
+
"memory_id": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"description": "ID of the memory item to find related memories for (optional if link_all_items is true)",
|
|
50
|
+
},
|
|
51
|
+
"category": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "Category containing the target memory item",
|
|
54
|
+
},
|
|
55
|
+
"top_k": {
|
|
56
|
+
"type": "integer",
|
|
57
|
+
"description": "Number of top related memories to find",
|
|
58
|
+
"default": 5,
|
|
59
|
+
},
|
|
60
|
+
"min_similarity": {
|
|
61
|
+
"type": "number",
|
|
62
|
+
"description": "Minimum similarity threshold (0.0-1.0)",
|
|
63
|
+
"default": 0.3,
|
|
64
|
+
},
|
|
65
|
+
"search_categories": {
|
|
66
|
+
"type": "array",
|
|
67
|
+
"items": {"type": "string"},
|
|
68
|
+
"description": "Categories to search in (default: all available categories)",
|
|
69
|
+
},
|
|
70
|
+
"link_all_items": {
|
|
71
|
+
"type": "boolean",
|
|
72
|
+
"description": "Whether to link all memory items in the category (if true, memory_id can be omitted)",
|
|
73
|
+
"default": False,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
"required": ["character_name", "category"],
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def execute(
|
|
81
|
+
self,
|
|
82
|
+
character_name: str,
|
|
83
|
+
category: str,
|
|
84
|
+
memory_id: Optional[str] = None,
|
|
85
|
+
top_k: int = 5,
|
|
86
|
+
min_similarity: float = 0.3,
|
|
87
|
+
search_categories: Optional[List[str]] = None,
|
|
88
|
+
link_all_items: bool = False,
|
|
89
|
+
write_to_memory: bool = True,
|
|
90
|
+
) -> Dict[str, Any]:
|
|
91
|
+
"""
|
|
92
|
+
Execute link related memories operation
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
character_name: Name of the character
|
|
96
|
+
memory_id: ID of the memory item to find related memories for
|
|
97
|
+
category: Category containing the target memory item
|
|
98
|
+
top_k: Number of top related memories to find
|
|
99
|
+
min_similarity: Minimum similarity threshold
|
|
100
|
+
search_categories: Categories to search in
|
|
101
|
+
link_format: Format for generating links
|
|
102
|
+
write_to_memory: Whether to append links to original memory
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Dict containing related memories and generated links
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
# Validate inputs
|
|
109
|
+
if not self.embeddings_enabled:
|
|
110
|
+
return self._add_metadata(
|
|
111
|
+
{
|
|
112
|
+
"success": False,
|
|
113
|
+
"error": "Embeddings are not enabled. Cannot perform similarity search.",
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if category not in self.basic_memory_types:
|
|
118
|
+
return self._add_metadata(
|
|
119
|
+
{
|
|
120
|
+
"success": False,
|
|
121
|
+
"error": f"Skipping category '{category}' not in available categories. Available: {list(self.basic_memory_types.keys())}",
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# If link_all_items is True, process all memory items in the category
|
|
126
|
+
if link_all_items:
|
|
127
|
+
return self._link_all_items_in_category(
|
|
128
|
+
character_name,
|
|
129
|
+
category,
|
|
130
|
+
top_k,
|
|
131
|
+
min_similarity,
|
|
132
|
+
search_categories,
|
|
133
|
+
write_to_memory,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Otherwise, process single memory item
|
|
137
|
+
if not memory_id:
|
|
138
|
+
return self._add_metadata(
|
|
139
|
+
{
|
|
140
|
+
"success": False,
|
|
141
|
+
"error": "memory_id is required when link_all_items is False",
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Find the target memory item
|
|
146
|
+
target_memory = self._find_memory_item(character_name, category, memory_id)
|
|
147
|
+
if not target_memory:
|
|
148
|
+
return self._add_metadata(
|
|
149
|
+
{
|
|
150
|
+
"success": False,
|
|
151
|
+
"error": f"Memory ID '{memory_id}' not found in {category} for {character_name}",
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Generate embedding for target content
|
|
156
|
+
target_embedding = self.embedding_client.embed(target_memory["content"])
|
|
157
|
+
|
|
158
|
+
# Determine search categories - search in ALL categories by default
|
|
159
|
+
if search_categories is None:
|
|
160
|
+
search_categories = list(self.basic_memory_types.keys())
|
|
161
|
+
|
|
162
|
+
# Find related memories
|
|
163
|
+
related_memories = self._find_related_memories(
|
|
164
|
+
character_name,
|
|
165
|
+
target_embedding,
|
|
166
|
+
target_memory["content"],
|
|
167
|
+
search_categories,
|
|
168
|
+
top_k,
|
|
169
|
+
min_similarity,
|
|
170
|
+
memory_id,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Get memory IDs for links
|
|
174
|
+
link_ids = [memory["memory_id"] for memory in related_memories]
|
|
175
|
+
|
|
176
|
+
# Optionally write links to memory
|
|
177
|
+
if write_to_memory and link_ids:
|
|
178
|
+
self._append_links_to_memory(character_name, category, memory_id, link_ids)
|
|
179
|
+
|
|
180
|
+
return self._add_metadata(
|
|
181
|
+
{
|
|
182
|
+
"success": True,
|
|
183
|
+
"character_name": character_name,
|
|
184
|
+
"linked_memory_ids": link_ids,
|
|
185
|
+
"total_related": len(related_memories),
|
|
186
|
+
"message": f"Found {len(related_memories)} related memories for {memory_id}",
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
return self._handle_error(e)
|
|
192
|
+
|
|
193
|
+
def _find_memory_item(self, character_name: str, category: str, memory_id: str) -> Optional[Dict[str, Any]]:
|
|
194
|
+
"""Find a specific memory item by ID"""
|
|
195
|
+
try:
|
|
196
|
+
content = self._read_memory_content(character_name, category)
|
|
197
|
+
if not content:
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
memory_items = self._parse_memory_items(content)
|
|
201
|
+
for item in memory_items:
|
|
202
|
+
if item["memory_id"] == memory_id:
|
|
203
|
+
return item
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.error(f"Error finding memory item {memory_id}: {e}")
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
def _find_related_memories(
|
|
211
|
+
self,
|
|
212
|
+
character_name: str,
|
|
213
|
+
target_embedding: List[float],
|
|
214
|
+
target_content: str,
|
|
215
|
+
search_categories: List[str],
|
|
216
|
+
top_k: int,
|
|
217
|
+
min_similarity: float,
|
|
218
|
+
exclude_memory_id: str,
|
|
219
|
+
) -> List[Dict[str, Any]]:
|
|
220
|
+
"""Find related memories using embedding similarity across all categories"""
|
|
221
|
+
all_candidates = []
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
# Get character embeddings directory from storage manager
|
|
225
|
+
char_embeddings_dir = self.storage_manager.get_char_embeddings_dir()
|
|
226
|
+
if not char_embeddings_dir.exists():
|
|
227
|
+
return []
|
|
228
|
+
|
|
229
|
+
# Collect ALL embeddings from all categories first
|
|
230
|
+
for category in search_categories:
|
|
231
|
+
embeddings_file = char_embeddings_dir / f"{category}_embeddings.json"
|
|
232
|
+
|
|
233
|
+
if embeddings_file.exists():
|
|
234
|
+
try:
|
|
235
|
+
with open(embeddings_file, "r", encoding="utf-8") as f:
|
|
236
|
+
embeddings_data = json.load(f)
|
|
237
|
+
|
|
238
|
+
for emb_data in embeddings_data.get("embeddings", []):
|
|
239
|
+
# Skip the target memory itself
|
|
240
|
+
if emb_data.get("memory_id") == exclude_memory_id:
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
similarity = self._cosine_similarity(target_embedding, emb_data["embedding"])
|
|
244
|
+
|
|
245
|
+
# Add ALL candidates regardless of similarity threshold initially
|
|
246
|
+
all_candidates.append(
|
|
247
|
+
{
|
|
248
|
+
"memory_id": emb_data.get("memory_id", ""),
|
|
249
|
+
"content": emb_data["text"],
|
|
250
|
+
"full_line": emb_data.get("full_line", emb_data["text"]),
|
|
251
|
+
"category": category,
|
|
252
|
+
"similarity": similarity,
|
|
253
|
+
"item_id": emb_data.get("item_id", ""),
|
|
254
|
+
"line_number": emb_data.get("line_number", 0),
|
|
255
|
+
}
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
except Exception as e:
|
|
259
|
+
logger.warning(f"Failed to load embeddings for {category}: {repr(e)}")
|
|
260
|
+
|
|
261
|
+
# Sort ALL candidates by similarity globally
|
|
262
|
+
all_candidates.sort(key=lambda x: x["similarity"], reverse=True)
|
|
263
|
+
|
|
264
|
+
# Apply similarity threshold and take top K candidates for LLM filtering
|
|
265
|
+
filtered_results = [
|
|
266
|
+
candidate
|
|
267
|
+
for candidate in all_candidates[: top_k * 2] # Take more candidates first
|
|
268
|
+
if candidate["similarity"] >= min_similarity
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
# Use LLM to filter for truly relevant memories
|
|
272
|
+
if filtered_results:
|
|
273
|
+
if self.filter_with_llm:
|
|
274
|
+
relevant_memories = self._filter_relevant_memories_with_llm(
|
|
275
|
+
character_name, filtered_results, target_content, top_k
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
relevant_memories = sorted(filtered_results, key=lambda x: x["similarity"], reverse=True)[:top_k]
|
|
279
|
+
return relevant_memories
|
|
280
|
+
else:
|
|
281
|
+
return []
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.error(f"Error finding related memories: {e}")
|
|
285
|
+
return []
|
|
286
|
+
|
|
287
|
+
def _cosine_similarity(self, vec1: List[float], vec2: List[float]) -> float:
|
|
288
|
+
"""Calculate cosine similarity between two vectors"""
|
|
289
|
+
try:
|
|
290
|
+
if len(vec1) != len(vec2):
|
|
291
|
+
return 0.0
|
|
292
|
+
|
|
293
|
+
dot_product = sum(a * b for a, b in zip(vec1, vec2))
|
|
294
|
+
magnitude1 = math.sqrt(sum(a * a for a in vec1))
|
|
295
|
+
magnitude2 = math.sqrt(sum(a * a for a in vec2))
|
|
296
|
+
|
|
297
|
+
if magnitude1 == 0 or magnitude2 == 0:
|
|
298
|
+
return 0.0
|
|
299
|
+
|
|
300
|
+
return dot_product / (magnitude1 * magnitude2)
|
|
301
|
+
|
|
302
|
+
except Exception:
|
|
303
|
+
return 0.0
|
|
304
|
+
|
|
305
|
+
def _append_links_to_memory(
|
|
306
|
+
self, character_name: str, category: str, memory_id: str, related_memories: List[str]
|
|
307
|
+
) -> Optional[str]:
|
|
308
|
+
"""Append links to the original memory content"""
|
|
309
|
+
try:
|
|
310
|
+
# Read current content
|
|
311
|
+
content = self._read_memory_content(character_name, category)
|
|
312
|
+
if not content:
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
memory_items = self._parse_memory_items(content)
|
|
316
|
+
updated_lines = []
|
|
317
|
+
|
|
318
|
+
for item in memory_items:
|
|
319
|
+
if item["memory_id"] == memory_id:
|
|
320
|
+
updated_line = f"[{item['memory_id']}][mentioned at {item['mentioned_at']}] {item['content']} [{','.join(related_memories)}]"
|
|
321
|
+
updated_lines.append(updated_line)
|
|
322
|
+
else:
|
|
323
|
+
updated_lines.append(item["full_line"])
|
|
324
|
+
|
|
325
|
+
# Save updated content
|
|
326
|
+
updated_content = "\n".join(updated_lines)
|
|
327
|
+
success = self._save_memory_content(character_name, category, updated_content)
|
|
328
|
+
|
|
329
|
+
if success:
|
|
330
|
+
return updated_content
|
|
331
|
+
else:
|
|
332
|
+
logger.error("Failed to save updated memory content")
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
except Exception as e:
|
|
336
|
+
logger.error(f"Error appending links to memory: {e}")
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
def _link_all_items_in_category(
|
|
340
|
+
self,
|
|
341
|
+
character_name: str,
|
|
342
|
+
category: str,
|
|
343
|
+
top_k: int,
|
|
344
|
+
min_similarity: float,
|
|
345
|
+
search_categories: Optional[List[str]],
|
|
346
|
+
write_to_memory: bool,
|
|
347
|
+
) -> Dict[str, Any]:
|
|
348
|
+
"""Link all memory items in a category to related memories"""
|
|
349
|
+
try:
|
|
350
|
+
# Get all memory items in the category
|
|
351
|
+
content = self._read_memory_content(character_name, category)
|
|
352
|
+
if not content:
|
|
353
|
+
return self._add_metadata(
|
|
354
|
+
{
|
|
355
|
+
"success": False,
|
|
356
|
+
"error": f"No content found in {category} for {character_name}",
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
memory_items = self._parse_memory_items(content)
|
|
361
|
+
if not memory_items:
|
|
362
|
+
return self._add_metadata({"success": False, "error": f"No memory items found in {category}"})
|
|
363
|
+
|
|
364
|
+
# Determine search categories - search in ALL categories by default
|
|
365
|
+
if search_categories is None:
|
|
366
|
+
search_categories = list(self.basic_memory_types.keys())
|
|
367
|
+
|
|
368
|
+
total_linked = 0
|
|
369
|
+
|
|
370
|
+
# Process each memory item
|
|
371
|
+
for item in memory_items:
|
|
372
|
+
memory_id = item["memory_id"]
|
|
373
|
+
|
|
374
|
+
# Generate embedding for this item's content
|
|
375
|
+
target_embedding = self.embedding_client.embed(item["content"])
|
|
376
|
+
|
|
377
|
+
# Find related memories
|
|
378
|
+
related_memories = self._find_related_memories(
|
|
379
|
+
character_name,
|
|
380
|
+
target_embedding,
|
|
381
|
+
item["content"],
|
|
382
|
+
search_categories,
|
|
383
|
+
top_k,
|
|
384
|
+
min_similarity,
|
|
385
|
+
memory_id,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Get memory IDs for links
|
|
389
|
+
link_ids = [memory["memory_id"] for memory in related_memories]
|
|
390
|
+
|
|
391
|
+
# Optionally write links to memory
|
|
392
|
+
if write_to_memory and link_ids:
|
|
393
|
+
self._append_links_to_memory(character_name, category, memory_id, link_ids)
|
|
394
|
+
total_linked += 1
|
|
395
|
+
|
|
396
|
+
return self._add_metadata(
|
|
397
|
+
{
|
|
398
|
+
"success": True,
|
|
399
|
+
"character_name": character_name,
|
|
400
|
+
"category": category,
|
|
401
|
+
"total_items_processed": len(memory_items),
|
|
402
|
+
"total_items_linked": total_linked,
|
|
403
|
+
"message": f"Linked {total_linked} out of {len(memory_items)} memory items in {category}",
|
|
404
|
+
}
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
except Exception as e:
|
|
408
|
+
return self._handle_error(e)
|
|
409
|
+
|
|
410
|
+
def _filter_relevant_memories_with_llm(
|
|
411
|
+
self,
|
|
412
|
+
character_name: str,
|
|
413
|
+
candidate_memories: List[Dict[str, Any]],
|
|
414
|
+
target_content: str,
|
|
415
|
+
max_links: int,
|
|
416
|
+
) -> List[Dict[str, Any]]:
|
|
417
|
+
"""
|
|
418
|
+
Use LLM to filter candidate memories and keep only truly relevant ones
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
character_name: Name of the character
|
|
422
|
+
candidate_memories: List of candidate memories from embedding search
|
|
423
|
+
target_content: The target memory content to compare against
|
|
424
|
+
max_links: Maximum number of links to return
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
List of filtered relevant memories
|
|
428
|
+
"""
|
|
429
|
+
try:
|
|
430
|
+
if not candidate_memories:
|
|
431
|
+
return []
|
|
432
|
+
|
|
433
|
+
# Prepare candidate memories for LLM evaluation
|
|
434
|
+
candidates_text = ""
|
|
435
|
+
for i, memory in enumerate(candidate_memories, 1):
|
|
436
|
+
candidates_text += f"{i}. [ID: {memory['memory_id']}] [{memory['category']}] {memory['content']} (similarity: {memory['similarity']:.3f})\n"
|
|
437
|
+
|
|
438
|
+
# Create LLM prompt for relevance filtering
|
|
439
|
+
relevance_prompt = f"""You are evaluating whether candidate memories are truly related to a target memory for {character_name}.
|
|
440
|
+
|
|
441
|
+
TARGET MEMORY:
|
|
442
|
+
{target_content}
|
|
443
|
+
|
|
444
|
+
CANDIDATE MEMORIES:
|
|
445
|
+
{candidates_text}
|
|
446
|
+
|
|
447
|
+
**TASK**: Determine which candidate memories are genuinely related to the target memory.
|
|
448
|
+
|
|
449
|
+
**CRITERIA FOR RELEVANCE**:
|
|
450
|
+
- Memories should share meaningful connections (people, places, events, topics, themes)
|
|
451
|
+
- Avoid superficial similarities (just sharing common words like "the", "and", "is")
|
|
452
|
+
- Consider contextual relationships (cause-effect, temporal sequences, thematic connections)
|
|
453
|
+
- Focus on memories that would provide useful context or background for understanding the target memory
|
|
454
|
+
|
|
455
|
+
**EVALUATION GUIDELINES**:
|
|
456
|
+
- ✅ RELEVANT: Memories about the same people, events, locations, or directly related topics
|
|
457
|
+
- ✅ RELEVANT: Memories that provide context, background, or related information
|
|
458
|
+
- ❌ NOT RELEVANT: Memories that only share common words but different contexts
|
|
459
|
+
- ❌ NOT RELEVANT: Memories about completely different topics/people/events
|
|
460
|
+
|
|
461
|
+
**OUTPUT FORMAT**:
|
|
462
|
+
Return ONLY the numbers (1, 2, 3, etc.) of the truly relevant memories, separated by commas. If no memories are relevant, return "NONE".
|
|
463
|
+
|
|
464
|
+
Examples:
|
|
465
|
+
- If memories 1, 3, and 5 are relevant: "1, 3, 5"
|
|
466
|
+
- If no memories are relevant: "NONE"
|
|
467
|
+
- If only memory 2 is relevant: "2"
|
|
468
|
+
|
|
469
|
+
RELEVANT MEMORY NUMBERS:"""
|
|
470
|
+
|
|
471
|
+
# Call LLM to evaluate relevance
|
|
472
|
+
llm_response = self.llm_client.simple_chat(relevance_prompt)
|
|
473
|
+
|
|
474
|
+
# Parse LLM response
|
|
475
|
+
relevant_indices = self._parse_relevance_response(llm_response.strip())
|
|
476
|
+
|
|
477
|
+
# Filter memories based on LLM evaluation
|
|
478
|
+
relevant_memories = []
|
|
479
|
+
for idx in relevant_indices:
|
|
480
|
+
if 1 <= idx <= len(candidate_memories):
|
|
481
|
+
relevant_memories.append(candidate_memories[idx - 1]) # Convert to 0-based index
|
|
482
|
+
|
|
483
|
+
# Limit to max_links
|
|
484
|
+
return relevant_memories[:max_links]
|
|
485
|
+
|
|
486
|
+
except Exception as e:
|
|
487
|
+
logger.error(f"Error filtering memories with LLM: {e}")
|
|
488
|
+
# Fallback to original top candidates if LLM filtering fails
|
|
489
|
+
return candidate_memories[:max_links]
|
|
490
|
+
|
|
491
|
+
def _parse_relevance_response(self, response: str) -> List[int]:
|
|
492
|
+
"""
|
|
493
|
+
Parse LLM response to extract relevant memory indices
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
response: LLM response containing memory numbers
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
List of memory indices (1-based)
|
|
500
|
+
"""
|
|
501
|
+
import re
|
|
502
|
+
|
|
503
|
+
try:
|
|
504
|
+
response = response.strip().upper()
|
|
505
|
+
|
|
506
|
+
if response == "NONE" or not response:
|
|
507
|
+
return []
|
|
508
|
+
|
|
509
|
+
# Extract numbers from response
|
|
510
|
+
numbers = re.findall(r"\b(\d+)\b", response)
|
|
511
|
+
return [int(num) for num in numbers if num.isdigit()]
|
|
512
|
+
|
|
513
|
+
except Exception as e:
|
|
514
|
+
logger.warning(f"Failed to parse relevance response '{response}': {repr(e)}")
|
|
515
|
+
return []
|