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.
Files changed (86) hide show
  1. noesium/core/__init__.py +4 -0
  2. noesium/core/agent/__init__.py +14 -0
  3. noesium/core/agent/base.py +227 -0
  4. noesium/core/consts.py +6 -0
  5. noesium/core/goalith/conflict/conflict.py +104 -0
  6. noesium/core/goalith/conflict/detector.py +53 -0
  7. noesium/core/goalith/decomposer/__init__.py +6 -0
  8. noesium/core/goalith/decomposer/base.py +46 -0
  9. noesium/core/goalith/decomposer/callable_decomposer.py +65 -0
  10. noesium/core/goalith/decomposer/llm_decomposer.py +326 -0
  11. noesium/core/goalith/decomposer/prompts.py +140 -0
  12. noesium/core/goalith/decomposer/simple_decomposer.py +61 -0
  13. noesium/core/goalith/errors.py +22 -0
  14. noesium/core/goalith/goalgraph/graph.py +526 -0
  15. noesium/core/goalith/goalgraph/node.py +179 -0
  16. noesium/core/goalith/replanner/base.py +31 -0
  17. noesium/core/goalith/replanner/replanner.py +36 -0
  18. noesium/core/goalith/service.py +26 -0
  19. noesium/core/llm/__init__.py +154 -0
  20. noesium/core/llm/base.py +152 -0
  21. noesium/core/llm/litellm.py +528 -0
  22. noesium/core/llm/llamacpp.py +487 -0
  23. noesium/core/llm/message.py +184 -0
  24. noesium/core/llm/ollama.py +459 -0
  25. noesium/core/llm/openai.py +520 -0
  26. noesium/core/llm/openrouter.py +89 -0
  27. noesium/core/llm/prompt.py +551 -0
  28. noesium/core/memory/__init__.py +11 -0
  29. noesium/core/memory/base.py +464 -0
  30. noesium/core/memory/memu/__init__.py +24 -0
  31. noesium/core/memory/memu/config/__init__.py +26 -0
  32. noesium/core/memory/memu/config/activity/config.py +46 -0
  33. noesium/core/memory/memu/config/event/config.py +46 -0
  34. noesium/core/memory/memu/config/markdown_config.py +241 -0
  35. noesium/core/memory/memu/config/profile/config.py +48 -0
  36. noesium/core/memory/memu/llm_adapter.py +129 -0
  37. noesium/core/memory/memu/memory/__init__.py +31 -0
  38. noesium/core/memory/memu/memory/actions/__init__.py +40 -0
  39. noesium/core/memory/memu/memory/actions/add_activity_memory.py +299 -0
  40. noesium/core/memory/memu/memory/actions/base_action.py +342 -0
  41. noesium/core/memory/memu/memory/actions/cluster_memories.py +262 -0
  42. noesium/core/memory/memu/memory/actions/generate_suggestions.py +198 -0
  43. noesium/core/memory/memu/memory/actions/get_available_categories.py +66 -0
  44. noesium/core/memory/memu/memory/actions/link_related_memories.py +515 -0
  45. noesium/core/memory/memu/memory/actions/run_theory_of_mind.py +254 -0
  46. noesium/core/memory/memu/memory/actions/update_memory_with_suggestions.py +514 -0
  47. noesium/core/memory/memu/memory/embeddings.py +130 -0
  48. noesium/core/memory/memu/memory/file_manager.py +306 -0
  49. noesium/core/memory/memu/memory/memory_agent.py +578 -0
  50. noesium/core/memory/memu/memory/recall_agent.py +376 -0
  51. noesium/core/memory/memu/memory_store.py +628 -0
  52. noesium/core/memory/models.py +149 -0
  53. noesium/core/msgbus/__init__.py +12 -0
  54. noesium/core/msgbus/base.py +395 -0
  55. noesium/core/orchestrix/__init__.py +0 -0
  56. noesium/core/py.typed +0 -0
  57. noesium/core/routing/__init__.py +20 -0
  58. noesium/core/routing/base.py +66 -0
  59. noesium/core/routing/router.py +241 -0
  60. noesium/core/routing/strategies/__init__.py +9 -0
  61. noesium/core/routing/strategies/dynamic_complexity.py +361 -0
  62. noesium/core/routing/strategies/self_assessment.py +147 -0
  63. noesium/core/routing/types.py +38 -0
  64. noesium/core/toolify/__init__.py +39 -0
  65. noesium/core/toolify/base.py +360 -0
  66. noesium/core/toolify/config.py +138 -0
  67. noesium/core/toolify/mcp_integration.py +275 -0
  68. noesium/core/toolify/registry.py +214 -0
  69. noesium/core/toolify/toolkits/__init__.py +1 -0
  70. noesium/core/tracing/__init__.py +37 -0
  71. noesium/core/tracing/langgraph_hooks.py +308 -0
  72. noesium/core/tracing/opik_tracing.py +144 -0
  73. noesium/core/tracing/token_tracker.py +166 -0
  74. noesium/core/utils/__init__.py +10 -0
  75. noesium/core/utils/logging.py +172 -0
  76. noesium/core/utils/statistics.py +12 -0
  77. noesium/core/utils/typing.py +17 -0
  78. noesium/core/vector_store/__init__.py +79 -0
  79. noesium/core/vector_store/base.py +94 -0
  80. noesium/core/vector_store/pgvector.py +304 -0
  81. noesium/core/vector_store/weaviate.py +383 -0
  82. noesium-0.1.0.dist-info/METADATA +525 -0
  83. noesium-0.1.0.dist-info/RECORD +86 -0
  84. noesium-0.1.0.dist-info/WHEEL +5 -0
  85. noesium-0.1.0.dist-info/licenses/LICENSE +21 -0
  86. 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 []