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,262 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
|
|
4
|
+
from .base_action import BaseAction
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ClusterMemoriesAction(BaseAction):
|
|
8
|
+
"""
|
|
9
|
+
Cluster memories into different categories.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def action_name(self) -> str:
|
|
14
|
+
return "cluster_memories"
|
|
15
|
+
|
|
16
|
+
def get_schema(self) -> Dict[str, Any]:
|
|
17
|
+
"""Return OpenAI-compatible function schema"""
|
|
18
|
+
return {
|
|
19
|
+
"name": self.action_name,
|
|
20
|
+
"description": "Cluster memories into different categories",
|
|
21
|
+
"parameters": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {
|
|
24
|
+
"character_name": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Name of the character",
|
|
27
|
+
},
|
|
28
|
+
"conversation_content": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "The full conversation content to provide context for clustering",
|
|
31
|
+
},
|
|
32
|
+
"new_memory_items": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"items": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {
|
|
37
|
+
"memory_id": {"type": "string"},
|
|
38
|
+
"content": {"type": "string"},
|
|
39
|
+
"mentioned_at": {"type": "string"},
|
|
40
|
+
},
|
|
41
|
+
"required": ["memory_id", "content", "mentioned_at"],
|
|
42
|
+
},
|
|
43
|
+
"description": "List of new memory items from the conversation",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
"required": ["character_name", "conversation_content", "new_memory_items"],
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
def execute(
|
|
51
|
+
self,
|
|
52
|
+
character_name: str,
|
|
53
|
+
conversation_content: str,
|
|
54
|
+
new_memory_items: List[Dict[str, str]],
|
|
55
|
+
new_theory_of_mind_items: List[Dict[str, str]] = [],
|
|
56
|
+
# available_categories: List[str]
|
|
57
|
+
session_date: str = None,
|
|
58
|
+
) -> Dict[str, Any]:
|
|
59
|
+
"""
|
|
60
|
+
Cluster memories into different categories
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
if session_date:
|
|
64
|
+
for item in new_memory_items:
|
|
65
|
+
if not item.get("mentioned_at", None):
|
|
66
|
+
item["mentioned_at"] = session_date
|
|
67
|
+
|
|
68
|
+
existing_clusters = self.memory_types["cluster"].keys()
|
|
69
|
+
# existing_clusters = [cluster.replace("_", " ") for cluster in existing_clusters]
|
|
70
|
+
|
|
71
|
+
updated_clusters = {}
|
|
72
|
+
if existing_clusters:
|
|
73
|
+
updated_clusters = self._merge_existing_clusters(
|
|
74
|
+
character_name,
|
|
75
|
+
conversation_content,
|
|
76
|
+
existing_clusters,
|
|
77
|
+
new_memory_items,
|
|
78
|
+
new_theory_of_mind_items,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
new_clusters = self._detect_new_clusters(
|
|
82
|
+
character_name,
|
|
83
|
+
conversation_content,
|
|
84
|
+
existing_clusters,
|
|
85
|
+
new_memory_items,
|
|
86
|
+
new_theory_of_mind_items,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return self._add_metadata(
|
|
90
|
+
{
|
|
91
|
+
"success": True,
|
|
92
|
+
"character_name": character_name,
|
|
93
|
+
"updated_clusters": sorted(updated_clusters.keys()),
|
|
94
|
+
"new_clusters": sorted(new_clusters.keys()),
|
|
95
|
+
"message": f"Analyzed {len(new_memory_items)} new memory items. Updated {len(updated_clusters)} existing clusters and detected {len(new_clusters)} new clusters",
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _format_memory_item(self, memory_item: Dict[str, str]) -> str:
|
|
100
|
+
"""Format memory items into a string"""
|
|
101
|
+
return f"[{memory_item['memory_id']}][mentioned at {memory_item['mentioned_at']}] {memory_item['content']} [{memory_item['memory_id']}]"
|
|
102
|
+
|
|
103
|
+
def _merge_existing_clusters(
|
|
104
|
+
self,
|
|
105
|
+
character_name: str,
|
|
106
|
+
conversation_content: str,
|
|
107
|
+
existing_clusters: List[str],
|
|
108
|
+
new_memory_items: List[Dict[str, str]],
|
|
109
|
+
new_theory_of_mind_items: List[Dict[str, str]],
|
|
110
|
+
count_threshold: int = 3,
|
|
111
|
+
) -> List[str]:
|
|
112
|
+
"""
|
|
113
|
+
Merge existing clusters with new memory items and theory of mind items
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
all_items = {item["memory_id"]: item for item in itertools.chain(new_memory_items, new_theory_of_mind_items)}
|
|
117
|
+
|
|
118
|
+
memory_items_text = "\n".join(
|
|
119
|
+
[f"Memory ID: {item['memory_id']}\nContent: {item['content']}" for item in all_items.values()]
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Create cluster list string outside f-string to avoid backslash issue
|
|
123
|
+
clusters_list = "\n".join(f"- {cluster}" for cluster in existing_clusters)
|
|
124
|
+
|
|
125
|
+
system_message = f"""You are an expert in analyzing and categorizing memories items.
|
|
126
|
+
|
|
127
|
+
You are given a list of existing clusters, a list of memory items, and the full conversation context that generated these memories.
|
|
128
|
+
Your task is to analyze if each of the memory items is related to any of the existing clusters.
|
|
129
|
+
|
|
130
|
+
**CONVERSATION CONTEXT:**
|
|
131
|
+
{conversation_content}
|
|
132
|
+
|
|
133
|
+
**EXISTING CLUSTERS:**
|
|
134
|
+
{clusters_list}
|
|
135
|
+
|
|
136
|
+
**MEMORY ITEMS:**
|
|
137
|
+
{memory_items_text}
|
|
138
|
+
|
|
139
|
+
**INSTRUCTIONS:**
|
|
140
|
+
1. Use the conversation context to better understand the background and relationships of the memory items.
|
|
141
|
+
2. It is possible that a memory item is related to multiple clusters.
|
|
142
|
+
Example: "We went to hiking in Blue Ridge Mountains this summer" is related to both "hiking" and "summer events" clusters, if both these two clusters are in the Existing Clusters.
|
|
143
|
+
3. If it possible that some memory items are not related to any of the existing clusters, you don't need to force them into any cluster.
|
|
144
|
+
4. DO NOT output memory items that are not related to any of the existing clusters.
|
|
145
|
+
5. Consider the conversation context when determining relationships - topics discussed together might belong to the same cluster.
|
|
146
|
+
|
|
147
|
+
**OUTPUT FORMAT:**
|
|
148
|
+
- [Memory ID]: [Cluster names that the memory item is related to, separated by comma]
|
|
149
|
+
- [Memory ID]: [Cluster names that the memory item is related to, separated by comma]
|
|
150
|
+
- ...
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
response = self.llm_client.simple_chat(system_message)
|
|
154
|
+
|
|
155
|
+
if not response.strip():
|
|
156
|
+
return self._add_metadata({"success": False, "error": "LLM returned empty response"})
|
|
157
|
+
|
|
158
|
+
updated_clusters = {}
|
|
159
|
+
|
|
160
|
+
for line in response.split("\n"):
|
|
161
|
+
if not line.startswith("- "):
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
memory_id, clusters = line[2:].split(": ", 1)
|
|
165
|
+
memory_id = memory_id.strip()
|
|
166
|
+
if memory_id not in all_items:
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
for cluster in clusters.split(","):
|
|
170
|
+
if cluster not in existing_clusters:
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
self.storage_manager.append_memory_file(cluster, self._format_memory_item(all_items[memory_id]))
|
|
174
|
+
|
|
175
|
+
if cluster not in updated_clusters:
|
|
176
|
+
updated_clusters[cluster] = []
|
|
177
|
+
updated_clusters[cluster].append(memory_id)
|
|
178
|
+
|
|
179
|
+
return updated_clusters
|
|
180
|
+
|
|
181
|
+
def _detect_new_clusters(
|
|
182
|
+
self,
|
|
183
|
+
character_name: str,
|
|
184
|
+
conversation_content: str,
|
|
185
|
+
existing_clusters: List[str],
|
|
186
|
+
new_memory_items: List[Dict[str, str]],
|
|
187
|
+
new_theory_of_mind_items: List[Dict[str, str]],
|
|
188
|
+
count_threshold: int = 3,
|
|
189
|
+
) -> List[str]:
|
|
190
|
+
"""
|
|
191
|
+
Detect new clusters from new memory items and theory of mind items
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
all_items = {item["memory_id"]: item for item in itertools.chain(new_memory_items, new_theory_of_mind_items)}
|
|
195
|
+
|
|
196
|
+
memory_items_text = "\n".join(
|
|
197
|
+
[f"Memory ID: {item['memory_id']}\nContent: {item['content']}" for item in all_items.values()]
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Create cluster list string outside f-string to avoid backslash issue
|
|
201
|
+
existing_clusters_list = "\n".join(f"- {cluster}" for cluster in existing_clusters)
|
|
202
|
+
|
|
203
|
+
system_message = f"""You are an expert in discovering some important or repeating events in one's memory records.
|
|
204
|
+
|
|
205
|
+
You are given a conversation context, a list of memory items extracted from this conversation, and existing clusters.
|
|
206
|
+
Your task is to discover NEW events/themes that are either:
|
|
207
|
+
- Important (e.g., marriage, job promotion, etc.), or
|
|
208
|
+
- Repeating, periodical, or routine (e.g., going to gym, attending specific events, etc.).
|
|
209
|
+
|
|
210
|
+
**CONVERSATION CONTEXT:**
|
|
211
|
+
{conversation_content}
|
|
212
|
+
|
|
213
|
+
**EXISTING CLUSTERS (DO NOT recreate these):**
|
|
214
|
+
{existing_clusters_list}
|
|
215
|
+
|
|
216
|
+
**MEMORY ITEMS:**
|
|
217
|
+
{memory_items_text}
|
|
218
|
+
|
|
219
|
+
**INSTRUCTIONS:**
|
|
220
|
+
1. Use the conversation context to better understand the significance and relationships of events.
|
|
221
|
+
2. Only create NEW clusters - do not recreate existing clusters listed above.
|
|
222
|
+
3. You should create a Event Name for each NEW event you discover.
|
|
223
|
+
4. The Event Name should be short and clear. A single word is the best (e.g., "marriage", "hiking"). Never let the name be longer than 3 words.
|
|
224
|
+
5. The Event Name should contains only alphabets or space. DO NOT use any other characters including hyphen, underscore, etc.
|
|
225
|
+
6. An event can be considered repeating, periodical, or routine, if they are mentioned at least twice in the memory items OR if the conversation context suggests it's a recurring theme.
|
|
226
|
+
7. If an event is considered important enough (e.g., proposal), you should record it no matter how many times it is mentioned.
|
|
227
|
+
8. For event content that are close (e.g., hiking and backpacking), you can merge them into a single event, and accumulate the count.
|
|
228
|
+
9. Consider the conversation flow - events discussed together might indicate related themes or patterns.
|
|
229
|
+
|
|
230
|
+
**OUTPUT FORMAT:**
|
|
231
|
+
- [Event Name]: [Memory ID of ALL memory items related to this event, separated by comma]
|
|
232
|
+
- [Event Name]: [Memory ID of ALL memory items related to this event, separated by comma]
|
|
233
|
+
- ...
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
response = self.llm_client.simple_chat(system_message)
|
|
237
|
+
|
|
238
|
+
if not response.strip():
|
|
239
|
+
return self._add_metadata({"success": False, "error": "LLM returned empty response"})
|
|
240
|
+
|
|
241
|
+
new_clusters = {}
|
|
242
|
+
|
|
243
|
+
for line in response.split("\n"):
|
|
244
|
+
if not line.startswith("- "):
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
cluster, memory_ids = line[2:].split(": ", 1)
|
|
248
|
+
cluster = cluster.strip().lower()
|
|
249
|
+
|
|
250
|
+
if cluster not in self.memory_types["cluster"]:
|
|
251
|
+
new_clusters[cluster] = []
|
|
252
|
+
self.storage_manager.create_cluster_category(cluster)
|
|
253
|
+
|
|
254
|
+
for memory_id in memory_ids.split(","):
|
|
255
|
+
memory_id = memory_id.strip()
|
|
256
|
+
if memory_id not in all_items:
|
|
257
|
+
continue
|
|
258
|
+
self.storage_manager.append_memory_file(cluster, self._format_memory_item(all_items[memory_id]))
|
|
259
|
+
|
|
260
|
+
new_clusters[cluster].append(memory_id)
|
|
261
|
+
|
|
262
|
+
return new_clusters
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate Memory Suggestions Action
|
|
3
|
+
|
|
4
|
+
Analyzes new memory items and suggests what should be added to different memory categories.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from .base_action import BaseAction
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GenerateMemorySuggestionsAction(BaseAction):
|
|
13
|
+
"""
|
|
14
|
+
Generate suggestions for what memory content should be added to different categories
|
|
15
|
+
based on new memory items from conversations.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def action_name(self) -> str:
|
|
20
|
+
return "generate_memory_suggestions"
|
|
21
|
+
|
|
22
|
+
def get_schema(self) -> Dict[str, Any]:
|
|
23
|
+
"""Return OpenAI-compatible function schema"""
|
|
24
|
+
return {
|
|
25
|
+
"name": self.action_name,
|
|
26
|
+
"description": "Analyze new memory items and generate suggestions for what should be added to different memory categories",
|
|
27
|
+
"parameters": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"character_name": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Name of the character",
|
|
33
|
+
},
|
|
34
|
+
"new_memory_items": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"memory_id": {"type": "string"},
|
|
40
|
+
"content": {"type": "string"},
|
|
41
|
+
"mentioned_at": {"type": "string"},
|
|
42
|
+
},
|
|
43
|
+
"required": ["memory_id", "content"],
|
|
44
|
+
},
|
|
45
|
+
"description": "List of new memory items from the conversation",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
"required": ["character_name", "new_memory_items"],
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
def execute(
|
|
53
|
+
self,
|
|
54
|
+
character_name: str,
|
|
55
|
+
new_memory_items: list[dict[str, str]],
|
|
56
|
+
available_categories: Optional[list[str]] = None,
|
|
57
|
+
) -> Dict[str, Any]:
|
|
58
|
+
"""
|
|
59
|
+
Generate memory suggestions for different categories
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
character_name: Name of the character
|
|
63
|
+
new_memory_items: List of new memory items with memory_id and content
|
|
64
|
+
available_categories: List of available memory categories
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Dict containing suggestions for each category
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
if not new_memory_items:
|
|
71
|
+
return self._add_metadata({"success": False, "error": "No memory items provided"})
|
|
72
|
+
|
|
73
|
+
if available_categories is None:
|
|
74
|
+
available_categories = self._get_available_categories(character_name)
|
|
75
|
+
if not available_categories:
|
|
76
|
+
return self._add_metadata({"success": False, "error": "No available categories found"})
|
|
77
|
+
|
|
78
|
+
# Convert memory items to text for analysis
|
|
79
|
+
memory_items_text = "\n".join(
|
|
80
|
+
[
|
|
81
|
+
# f"Memory ID: {item['memory_id']}\nContent: {item['content']}"
|
|
82
|
+
f"- {item['content']}"
|
|
83
|
+
for item in new_memory_items
|
|
84
|
+
]
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Create enhanced prompt for LLM to analyze and generate suggestions
|
|
88
|
+
suggestions_prompt = f"""You are an expert in analyzing the provided memory items for {character_name} and suggesting the memory items that should be added to each memory category.
|
|
89
|
+
|
|
90
|
+
New Memory Items:
|
|
91
|
+
{memory_items_text}
|
|
92
|
+
|
|
93
|
+
Available Categories: {', '.join(available_categories)}
|
|
94
|
+
|
|
95
|
+
**CRITICAL REQUIREMENT: Suggestions must be SELF-CONTAINED MEMORY ITEMS**
|
|
96
|
+
|
|
97
|
+
**SELF-CONTAINED MEMORY REQUIREMENTS:**
|
|
98
|
+
- EVERY activity item must be complete and standalone
|
|
99
|
+
- ALWAYS include the full subject (do not use "she/he/they/it")
|
|
100
|
+
- NEVER use pronouns that depend on context (no "she", "he", "they", "it")
|
|
101
|
+
- Include specific names, places, dates, and full context in each item
|
|
102
|
+
- Each activity should be understandable without reading other items
|
|
103
|
+
- Include all relevant details, emotions, and outcomes in the activity description
|
|
104
|
+
|
|
105
|
+
**CATEGORY-SPECIFIC REQUIREMENTS:**
|
|
106
|
+
|
|
107
|
+
For each category, analyze the new memory items and suggest what specific information should be extracted and added to that category:
|
|
108
|
+
|
|
109
|
+
- **activity**: Detailed description of the conversation, including the time, place, and people involved
|
|
110
|
+
- **profile**: ONLY basic personal information (age, location, occupation, education, family status, demographics) - EXCLUDE events, activities, things they did
|
|
111
|
+
- **event**: Specific events, dates, milestones, appointments, meetings, activities with time references
|
|
112
|
+
- **Other categories**: Relevant information for each specific category
|
|
113
|
+
|
|
114
|
+
**CRITICAL DISTINCTION - Profile vs Activity/Event:**
|
|
115
|
+
- Profile (GOOD): "Alice lives in San Francisco", "Alice is 28 years old", "Alice works at TechFlow Solutions"
|
|
116
|
+
- Profile (BAD): "Alice went hiking" (this is activity), "Alice attended workshop" (this is event)
|
|
117
|
+
- Activity/Event (GOOD): "Alice went hiking in Blue Ridge Mountains", "Alice attended photography workshop"
|
|
118
|
+
|
|
119
|
+
**SUGGESTION REQUIREMENTS:**
|
|
120
|
+
- Specify that memory items should include "{character_name}" as the subject
|
|
121
|
+
- Mention specific names, places, titles, and dates that should be included
|
|
122
|
+
- Ensure suggestions lead to complete, self-contained memory items
|
|
123
|
+
- Avoid suggesting content that would result in pronouns or incomplete sentences
|
|
124
|
+
- For profile: Focus ONLY on stable, factual, demographic information
|
|
125
|
+
- If one input memory item involves information belongs to multiple categories, you should reasonable seperete the information and provide suggestions to all involved categories
|
|
126
|
+
- **IMPORTANT** If the input memory item use modal adverbs (perhaps, probably, likely, etc.) to indicate an uncertain inference, keep the modal adverbs as-is in your suggestions
|
|
127
|
+
|
|
128
|
+
**OUTPUT INSTRUCTIONS:**
|
|
129
|
+
- **IMPORTANT** NEVER suggest categories that are not in the Available Categories
|
|
130
|
+
- Only output categories where there are suggestions for new memory items
|
|
131
|
+
|
|
132
|
+
**OUTPUT FORMAT:**
|
|
133
|
+
|
|
134
|
+
**Category: [category_name]**
|
|
135
|
+
- Suggestion: [What specific self-contained content should be added to this category, ensuring full subjects and complete context]
|
|
136
|
+
- Suggestion: [What specific self-contained content should be added to this category, ensuring full subjects and complete context]
|
|
137
|
+
|
|
138
|
+
**Category: [category_name]**
|
|
139
|
+
- Suggestion: [What specific self-contained content should be added to this category, ensuring full subjects and complete context]
|
|
140
|
+
|
|
141
|
+
... other categories ...
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
# Call LLM to generate suggestions
|
|
145
|
+
response = self.llm_client.simple_chat(suggestions_prompt)
|
|
146
|
+
|
|
147
|
+
if not response.strip():
|
|
148
|
+
return self._add_metadata({"success": False, "error": "LLM returned empty suggestions"})
|
|
149
|
+
|
|
150
|
+
# Parse text response
|
|
151
|
+
suggestions = self._parse_suggestions_from_text(response.strip(), available_categories, new_memory_items)
|
|
152
|
+
|
|
153
|
+
return self._add_metadata(
|
|
154
|
+
{
|
|
155
|
+
"success": True,
|
|
156
|
+
"character_name": character_name,
|
|
157
|
+
"suggestions": suggestions,
|
|
158
|
+
"categories_analyzed": available_categories,
|
|
159
|
+
"message": f"Generated self-contained suggestions for {len(suggestions)} categories based on {len(new_memory_items)} memory items",
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
return self._handle_error(e)
|
|
165
|
+
|
|
166
|
+
def _get_available_categories(self, character_name: str) -> list[str]:
|
|
167
|
+
"""Get available categories for a character"""
|
|
168
|
+
return [category for category in self.basic_memory_types.keys() if category != "activity"]
|
|
169
|
+
|
|
170
|
+
def _parse_suggestions_from_text(
|
|
171
|
+
self,
|
|
172
|
+
response_text: str,
|
|
173
|
+
available_categories: list[str],
|
|
174
|
+
new_memory_items: list[dict[str, str]],
|
|
175
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
176
|
+
"""Parse suggestions from text format response"""
|
|
177
|
+
suggestions = {}
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
lines = response_text.split("\n")
|
|
181
|
+
current_category = None
|
|
182
|
+
|
|
183
|
+
for line in lines:
|
|
184
|
+
line = line.strip()
|
|
185
|
+
|
|
186
|
+
if line.startswith("**Category:") and line.endswith("**"):
|
|
187
|
+
category_name = line.replace("**Category:", "").replace("**", "").strip()
|
|
188
|
+
if category_name in available_categories:
|
|
189
|
+
current_category = category_name
|
|
190
|
+
suggestions[current_category] = ""
|
|
191
|
+
elif current_category and line.startswith("- Suggestion:"):
|
|
192
|
+
suggestion_text = line.replace("- Suggestion:", "").strip()
|
|
193
|
+
suggestions[current_category] += f"{suggestion_text}\n"
|
|
194
|
+
|
|
195
|
+
suggestions = {k: v for k, v in suggestions.items() if v.strip()}
|
|
196
|
+
except Exception:
|
|
197
|
+
raise
|
|
198
|
+
return suggestions
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Get Available Categories Action
|
|
3
|
+
|
|
4
|
+
Gets all available memory categories and their descriptions, excluding activity category.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict
|
|
9
|
+
|
|
10
|
+
from .base_action import BaseAction
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GetAvailableCategoriesAction(BaseAction):
|
|
16
|
+
"""Action to get all available memory categories from config, excluding activity"""
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def action_name(self) -> str:
|
|
20
|
+
return "get_available_categories"
|
|
21
|
+
|
|
22
|
+
def get_schema(self) -> Dict[str, Any]:
|
|
23
|
+
"""Return OpenAI-compatible function schema"""
|
|
24
|
+
return {
|
|
25
|
+
"name": "get_available_categories",
|
|
26
|
+
"description": "Get all available memory categories and their descriptions (excluding activity category)",
|
|
27
|
+
"parameters": {"type": "object", "properties": {}, "required": []},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def execute(self) -> Dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Execute get available categories operation
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Dict containing category information (excluding activity category)
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
categories = {}
|
|
39
|
+
|
|
40
|
+
for category, filename in self.basic_memory_types.items():
|
|
41
|
+
# Skip activity category as it's handled separately by add_activity_memory
|
|
42
|
+
if category == "activity":
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
description = self.config_manager.get_file_description(category)
|
|
46
|
+
|
|
47
|
+
categories[category] = {
|
|
48
|
+
"filename": filename,
|
|
49
|
+
"description": description,
|
|
50
|
+
"config_source": self.config_manager.get_folder_path(category),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return self._add_metadata(
|
|
54
|
+
{
|
|
55
|
+
"success": True,
|
|
56
|
+
"categories": categories,
|
|
57
|
+
"total_categories": len(categories),
|
|
58
|
+
"processing_order": [cat for cat in self.processing_order if cat != "activity"],
|
|
59
|
+
"embeddings_enabled": self.embeddings_enabled,
|
|
60
|
+
"excluded_categories": ["activity"],
|
|
61
|
+
"message": f"Found {len(categories)} memory categories from config (excluding activity)",
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
return self._handle_error(e)
|