reme-ai 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.
- reme_ai/__init__.py +6 -0
- reme_ai/app.py +17 -0
- reme_ai/config/__init__.py +0 -0
- reme_ai/config/config_parser.py +6 -0
- reme_ai/constants/__init__.py +7 -0
- reme_ai/constants/common_constants.py +48 -0
- reme_ai/constants/language_constants.py +215 -0
- reme_ai/enumeration/__init__.py +0 -0
- reme_ai/enumeration/language_constants.py +215 -0
- reme_ai/react/__init__.py +1 -0
- reme_ai/react/simple_react_op.py +21 -0
- reme_ai/retrieve/__init__.py +2 -0
- reme_ai/retrieve/personal/__init__.py +17 -0
- reme_ai/retrieve/personal/extract_time_op.py +97 -0
- reme_ai/retrieve/personal/fuse_rerank_op.py +180 -0
- reme_ai/retrieve/personal/print_memory_op.py +131 -0
- reme_ai/retrieve/personal/read_message_op.py +52 -0
- reme_ai/retrieve/personal/retrieve_memory_op.py +13 -0
- reme_ai/retrieve/personal/semantic_rank_op.py +170 -0
- reme_ai/retrieve/personal/set_query_op.py +37 -0
- reme_ai/retrieve/task/__init__.py +4 -0
- reme_ai/retrieve/task/build_query_op.py +38 -0
- reme_ai/retrieve/task/merge_memory_op.py +27 -0
- reme_ai/retrieve/task/rerank_memory_op.py +149 -0
- reme_ai/retrieve/task/rewrite_memory_op.py +149 -0
- reme_ai/schema/__init__.py +1 -0
- reme_ai/schema/memory.py +144 -0
- reme_ai/summary/__init__.py +2 -0
- reme_ai/summary/personal/__init__.py +8 -0
- reme_ai/summary/personal/contra_repeat_op.py +143 -0
- reme_ai/summary/personal/get_observation_op.py +147 -0
- reme_ai/summary/personal/get_observation_with_time_op.py +165 -0
- reme_ai/summary/personal/get_reflection_subject_op.py +179 -0
- reme_ai/summary/personal/info_filter_op.py +177 -0
- reme_ai/summary/personal/load_today_memory_op.py +117 -0
- reme_ai/summary/personal/long_contra_repeat_op.py +210 -0
- reme_ai/summary/personal/update_insight_op.py +244 -0
- reme_ai/summary/task/__init__.py +10 -0
- reme_ai/summary/task/comparative_extraction_op.py +233 -0
- reme_ai/summary/task/failure_extraction_op.py +73 -0
- reme_ai/summary/task/memory_deduplication_op.py +163 -0
- reme_ai/summary/task/memory_validation_op.py +108 -0
- reme_ai/summary/task/pdf_preprocess_op_wrapper.py +50 -0
- reme_ai/summary/task/simple_comparative_summary_op.py +71 -0
- reme_ai/summary/task/simple_summary_op.py +67 -0
- reme_ai/summary/task/success_extraction_op.py +73 -0
- reme_ai/summary/task/trajectory_preprocess_op.py +76 -0
- reme_ai/summary/task/trajectory_segmentation_op.py +118 -0
- reme_ai/utils/__init__.py +0 -0
- reme_ai/utils/datetime_handler.py +345 -0
- reme_ai/utils/miner_u_pdf_processor.py +726 -0
- reme_ai/utils/op_utils.py +115 -0
- reme_ai/vector_store/__init__.py +6 -0
- reme_ai/vector_store/delete_memory_op.py +25 -0
- reme_ai/vector_store/recall_vector_store_op.py +36 -0
- reme_ai/vector_store/update_memory_freq_op.py +33 -0
- reme_ai/vector_store/update_memory_utility_op.py +32 -0
- reme_ai/vector_store/update_vector_store_op.py +32 -0
- reme_ai/vector_store/vector_store_action_op.py +55 -0
- reme_ai-0.1.0.dist-info/METADATA +218 -0
- reme_ai-0.1.0.dist-info/RECORD +65 -0
- reme_ai-0.1.0.dist-info/WHEEL +5 -0
- reme_ai-0.1.0.dist-info/entry_points.txt +2 -0
- reme_ai-0.1.0.dist-info/licenses/LICENSE +201 -0
- reme_ai-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,180 @@
|
|
1
|
+
from typing import Dict, List
|
2
|
+
|
3
|
+
from flowllm import C, BaseLLMOp
|
4
|
+
from loguru import logger
|
5
|
+
|
6
|
+
from reme_ai.constants.common_constants import EXTRACT_TIME_DICT
|
7
|
+
from reme_ai.schema.memory import BaseMemory
|
8
|
+
|
9
|
+
|
10
|
+
@C.register_op()
|
11
|
+
class FuseRerankOp(BaseLLMOp):
|
12
|
+
"""
|
13
|
+
Reranks the memory nodes by scores, types, and temporal relevance. Formats the top-K reranked nodes to print.
|
14
|
+
"""
|
15
|
+
file_path: str = __file__
|
16
|
+
|
17
|
+
@staticmethod
|
18
|
+
def match_memory_time(extract_time_dict: Dict[str, str], memory: BaseMemory):
|
19
|
+
"""
|
20
|
+
Determines whether the memory is relevant based on time matching.
|
21
|
+
"""
|
22
|
+
if extract_time_dict:
|
23
|
+
match_event_flag = True
|
24
|
+
for k, v in extract_time_dict.items():
|
25
|
+
event_value = memory.metadata.get(f"event_{k}", "")
|
26
|
+
if event_value in ["-1", v]:
|
27
|
+
continue
|
28
|
+
else:
|
29
|
+
match_event_flag = False
|
30
|
+
break
|
31
|
+
|
32
|
+
match_msg_flag = True
|
33
|
+
for k, v in extract_time_dict.items():
|
34
|
+
msg_value = memory.metadata.get(f"msg_{k}", "")
|
35
|
+
if msg_value == v:
|
36
|
+
continue
|
37
|
+
else:
|
38
|
+
match_msg_flag = False
|
39
|
+
break
|
40
|
+
else:
|
41
|
+
match_event_flag = False
|
42
|
+
match_msg_flag = False
|
43
|
+
|
44
|
+
memory.metadata["match_event_flag"] = str(int(match_event_flag))
|
45
|
+
memory.metadata["match_msg_flag"] = str(int(match_msg_flag))
|
46
|
+
return match_event_flag, match_msg_flag
|
47
|
+
|
48
|
+
def execute(self):
|
49
|
+
"""
|
50
|
+
Executes the reranking process on memories considering their scores, types, and temporal relevance.
|
51
|
+
|
52
|
+
This method performs the following steps:
|
53
|
+
1. Retrieves extraction time data and a list of memories from the context.
|
54
|
+
2. Reranks memories based on a combination of their original score, type,
|
55
|
+
and temporal alignment with extracted events/messages.
|
56
|
+
3. Selects the top-K reranked memories according to the predefined threshold.
|
57
|
+
4. Formats the final list of memories for output.
|
58
|
+
5. Sets both response.answer and response.metadata["memory_list"]
|
59
|
+
"""
|
60
|
+
# Get operation parameters
|
61
|
+
fuse_score_threshold = self.op_params.get("fuse_score_threshold", 0.1)
|
62
|
+
fuse_ratio_dict = self.op_params.get("fuse_ratio_dict", {
|
63
|
+
"conversation": 0.5,
|
64
|
+
"observation": 1,
|
65
|
+
"obs_customized": 1.2,
|
66
|
+
"insight": 2.0
|
67
|
+
})
|
68
|
+
fuse_time_ratio = self.op_params.get("fuse_time_ratio", 2.0)
|
69
|
+
output_memory_max_count = self.op_params.get("output_memory_max_count", 5)
|
70
|
+
|
71
|
+
# Parse input parameters from the context
|
72
|
+
extract_time_dict: Dict[str, str] = self.context.get(EXTRACT_TIME_DICT, {})
|
73
|
+
memory_list: List[BaseMemory] = self.context.response.metadata.get("memory_list", [])
|
74
|
+
|
75
|
+
# Check if memories are available; warn and return if not
|
76
|
+
if not memory_list:
|
77
|
+
logger.warning("No memories available for fuse reranking")
|
78
|
+
self.context.response.answer = ""
|
79
|
+
self.context.response.metadata["memory_list"] = []
|
80
|
+
return
|
81
|
+
|
82
|
+
logger.info(f"Fuse reranking {len(memory_list)} memories with time dict: {bool(extract_time_dict)}")
|
83
|
+
|
84
|
+
# Perform reranking based on score, type, and time relevance
|
85
|
+
reranked_memories = self._apply_fuse_reranking(
|
86
|
+
memory_list, extract_time_dict, fuse_score_threshold,
|
87
|
+
fuse_ratio_dict, fuse_time_ratio
|
88
|
+
)
|
89
|
+
|
90
|
+
# Sort and select top-k memories
|
91
|
+
reranked_memories = sorted(reranked_memories,
|
92
|
+
key=lambda x: x.score or 0.0,
|
93
|
+
reverse=True)[:output_memory_max_count]
|
94
|
+
|
95
|
+
logger.info(f"Final reranked memories: {len(reranked_memories)}")
|
96
|
+
|
97
|
+
# Format memories for output
|
98
|
+
formatted_memories = self._format_memories_for_output(reranked_memories)
|
99
|
+
|
100
|
+
# Store results in context - both answer and metadata as required
|
101
|
+
self.context.response.metadata["memory_list"] = reranked_memories
|
102
|
+
self.context.response.answer = "\n".join(formatted_memories)
|
103
|
+
|
104
|
+
def _apply_fuse_reranking(self,
|
105
|
+
memory_list: List[BaseMemory],
|
106
|
+
extract_time_dict: Dict[str, str],
|
107
|
+
fuse_score_threshold: float,
|
108
|
+
fuse_ratio_dict: Dict[str, float],
|
109
|
+
fuse_time_ratio: float) -> List[BaseMemory]:
|
110
|
+
"""Apply fuse reranking logic to memories"""
|
111
|
+
reranked_memories = []
|
112
|
+
|
113
|
+
for memory in memory_list:
|
114
|
+
# Skip memories below the fuse score threshold
|
115
|
+
memory_score = memory.score or 0.0
|
116
|
+
if memory_score < fuse_score_threshold:
|
117
|
+
continue
|
118
|
+
|
119
|
+
# Calculate type-based adjustment factor
|
120
|
+
memory_type = memory.metadata.get("memory_type", "default")
|
121
|
+
if memory_type not in fuse_ratio_dict:
|
122
|
+
logger.debug(f"Memory type '{memory_type}' not in fuse_ratio_dict, using default 0.1")
|
123
|
+
type_ratio: float = fuse_ratio_dict.get(memory_type, 0.1)
|
124
|
+
|
125
|
+
# Determine time relevance adjustment factor
|
126
|
+
match_event_flag, match_msg_flag = self.match_memory_time(extract_time_dict, memory)
|
127
|
+
time_ratio: float = fuse_time_ratio if match_event_flag or match_msg_flag else 1.0
|
128
|
+
|
129
|
+
# Apply reranking score adjustments
|
130
|
+
original_score = memory_score
|
131
|
+
memory.score = memory_score * type_ratio * time_ratio
|
132
|
+
|
133
|
+
logger.debug(f"Memory reranked: {original_score:.3f} -> {memory.score:.3f} "
|
134
|
+
f"(type={type_ratio}, time={time_ratio})")
|
135
|
+
|
136
|
+
reranked_memories.append(memory)
|
137
|
+
|
138
|
+
return reranked_memories
|
139
|
+
|
140
|
+
def _format_memories_for_output(self, memories: List[BaseMemory]) -> List[str]:
|
141
|
+
"""Format memories for final output"""
|
142
|
+
formatted_memories = []
|
143
|
+
|
144
|
+
for memory in memories:
|
145
|
+
# Log reranking details
|
146
|
+
logger.info(f"Final memory: Score={memory.score:.3f}, "
|
147
|
+
f"Event={memory.metadata.get('match_event_flag', '0')}, "
|
148
|
+
f"Msg={memory.metadata.get('match_msg_flag', '0')}, "
|
149
|
+
f"Content={memory.content[:50]}...")
|
150
|
+
|
151
|
+
# Format memory with timestamp if available
|
152
|
+
formatted_content = self._format_memory_with_timestamp(memory, self.language)
|
153
|
+
formatted_memories.append(formatted_content)
|
154
|
+
|
155
|
+
return formatted_memories
|
156
|
+
|
157
|
+
@staticmethod
|
158
|
+
def _format_memory_with_timestamp(memory, language: str = "en") -> str:
|
159
|
+
"""
|
160
|
+
Format memory content with timestamp if available.
|
161
|
+
|
162
|
+
Args:
|
163
|
+
memory: Memory object
|
164
|
+
language: Language for formatting
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
Formatted memory content string
|
168
|
+
"""
|
169
|
+
try:
|
170
|
+
if hasattr(memory, 'timestamp') and memory.timestamp:
|
171
|
+
from reme_ai.utils.datetime_handler import DatetimeHandler
|
172
|
+
dt_handler = DatetimeHandler(memory.timestamp)
|
173
|
+
datetime_str = dt_handler.datetime_format("%Y-%m-%d %H:%M:%S")
|
174
|
+
weekday = dt_handler.get_dt_info_dict(language)["weekday"]
|
175
|
+
return f"[{datetime_str} {weekday}] {memory.content}"
|
176
|
+
else:
|
177
|
+
return memory.content
|
178
|
+
except Exception as e:
|
179
|
+
logger.warning(f"Failed to format memory with timestamp: {e}")
|
180
|
+
return memory.content
|
@@ -0,0 +1,131 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from flowllm import C, BaseOp
|
4
|
+
from loguru import logger
|
5
|
+
|
6
|
+
from reme_ai.schema.memory import BaseMemory
|
7
|
+
|
8
|
+
|
9
|
+
@C.register_op()
|
10
|
+
class PrintMemoryOp(BaseOp):
|
11
|
+
"""
|
12
|
+
Formats the memories to print.
|
13
|
+
"""
|
14
|
+
file_path: str = __file__
|
15
|
+
|
16
|
+
def execute(self):
|
17
|
+
"""
|
18
|
+
Executes the primary function, it involves:
|
19
|
+
1. Fetches the memories.
|
20
|
+
2. Formats them for printing.
|
21
|
+
3. Set the formatted string back into the context
|
22
|
+
"""
|
23
|
+
# Get memory list from context
|
24
|
+
memory_list: List[BaseMemory] = self.context.response.metadata.get("memory_list", [])
|
25
|
+
|
26
|
+
if not memory_list:
|
27
|
+
logger.info("No memories to print")
|
28
|
+
self.context.response.answer = "No memories found."
|
29
|
+
return
|
30
|
+
|
31
|
+
logger.info(f"Formatting {len(memory_list)} memories for printing")
|
32
|
+
|
33
|
+
# Format memories for printing
|
34
|
+
formatted_memories = self._format_memories_for_print(memory_list)
|
35
|
+
|
36
|
+
# Store result in context
|
37
|
+
self.context.response.answer = formatted_memories
|
38
|
+
logger.info(f"Formatted memories: {formatted_memories}")
|
39
|
+
|
40
|
+
@staticmethod
|
41
|
+
def _format_memories_for_print(memories: List[BaseMemory]) -> str:
|
42
|
+
"""Format memories for printing"""
|
43
|
+
if not memories:
|
44
|
+
return "No memories available."
|
45
|
+
|
46
|
+
formatted_memories = []
|
47
|
+
|
48
|
+
for i, memory in enumerate(memories, 1):
|
49
|
+
memory_text = f"Memory {i}:\n"
|
50
|
+
memory_text += f" When to use: {memory.when_to_use}\n"
|
51
|
+
memory_text += f" Content: {memory.content}\n"
|
52
|
+
|
53
|
+
# Add additional metadata if available
|
54
|
+
if hasattr(memory, 'metadata') and memory.metadata:
|
55
|
+
metadata_items = []
|
56
|
+
for key, value in memory.metadata.items():
|
57
|
+
if key not in ['when_to_use', 'content']:
|
58
|
+
metadata_items.append(f"{key}: {value}")
|
59
|
+
if metadata_items:
|
60
|
+
memory_text += f" Metadata: {', '.join(metadata_items)}\n"
|
61
|
+
|
62
|
+
formatted_memories.append(memory_text)
|
63
|
+
|
64
|
+
return "\n".join(formatted_memories)
|
65
|
+
|
66
|
+
@staticmethod
|
67
|
+
def format_memories_for_output(memories: List) -> str:
|
68
|
+
"""
|
69
|
+
Format memory list for output string.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
memories: List of memory objects
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
Formatted string
|
76
|
+
"""
|
77
|
+
if not memories:
|
78
|
+
return ""
|
79
|
+
|
80
|
+
formatted_parts = []
|
81
|
+
for i, memory in enumerate(memories, 1):
|
82
|
+
when_to_use = getattr(memory, 'when_to_use', '') or memory.get('when_to_use', '')
|
83
|
+
content = getattr(memory, 'content', '') or memory.get('content', '')
|
84
|
+
|
85
|
+
part = f"Memory {i}:\n"
|
86
|
+
if when_to_use:
|
87
|
+
part += f"When to use: {when_to_use}\n"
|
88
|
+
if content:
|
89
|
+
part += f"Content: {content}\n"
|
90
|
+
|
91
|
+
formatted_parts.append(part)
|
92
|
+
|
93
|
+
return "\n".join(formatted_parts)
|
94
|
+
|
95
|
+
@staticmethod
|
96
|
+
def format_memories_for_simple_output(memories: List) -> str:
|
97
|
+
"""
|
98
|
+
Format memory list for simple flow output.
|
99
|
+
|
100
|
+
Args:
|
101
|
+
memories: List of memory objects
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
Formatted string suitable for response.answer
|
105
|
+
"""
|
106
|
+
if not memories:
|
107
|
+
return "No relevant memories found."
|
108
|
+
|
109
|
+
content_parts = ["Previous Memory"]
|
110
|
+
|
111
|
+
for memory in memories:
|
112
|
+
# Safely get field values
|
113
|
+
when_to_use = getattr(memory, 'when_to_use', '') or memory.get('when_to_use', '')
|
114
|
+
content = getattr(memory, 'content', '') or memory.get('content', '')
|
115
|
+
|
116
|
+
# Skip memories with empty content
|
117
|
+
if not content:
|
118
|
+
continue
|
119
|
+
|
120
|
+
# Format individual memory
|
121
|
+
memory_text = f"- when_to_use: {when_to_use}\n content: {content}"
|
122
|
+
content_parts.append(memory_text)
|
123
|
+
|
124
|
+
# If no valid memories, return empty message
|
125
|
+
if len(content_parts) == 1: # Only title
|
126
|
+
return "No relevant memories with valid content found."
|
127
|
+
|
128
|
+
content_parts.append("\nPlease consider the helpful parts from these in answering the question, "
|
129
|
+
"to make the response more comprehensive and substantial.")
|
130
|
+
|
131
|
+
return "\n".join(content_parts)
|
@@ -0,0 +1,52 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from flowllm import C, BaseOp
|
4
|
+
from flowllm.schema.message import Message
|
5
|
+
from loguru import logger
|
6
|
+
|
7
|
+
|
8
|
+
@C.register_op()
|
9
|
+
class ReadMessageOp(BaseOp):
|
10
|
+
"""
|
11
|
+
Fetches unmemorized chat messages.
|
12
|
+
"""
|
13
|
+
file_path: str = __file__
|
14
|
+
|
15
|
+
def execute(self):
|
16
|
+
"""
|
17
|
+
Executes the primary function to fetch unmemorized chat messages.
|
18
|
+
"""
|
19
|
+
# Get chat messages from context
|
20
|
+
chat_messages = self.context.chat_messages
|
21
|
+
target_name = self.context.target_name
|
22
|
+
contextual_msg_max_count = self.op_params.get('contextual_msg_max_count', 10)
|
23
|
+
|
24
|
+
chat_messages_not_memorized: List[List[Message]] = []
|
25
|
+
for messages in chat_messages:
|
26
|
+
if not messages:
|
27
|
+
continue
|
28
|
+
|
29
|
+
if hasattr(messages[0], 'memorized') and messages[0].memorized:
|
30
|
+
continue
|
31
|
+
|
32
|
+
contain_flag = False
|
33
|
+
|
34
|
+
for msg in messages:
|
35
|
+
if hasattr(msg, 'role_name') and msg.role_name == target_name:
|
36
|
+
contain_flag = True
|
37
|
+
break
|
38
|
+
|
39
|
+
if contain_flag:
|
40
|
+
chat_messages_not_memorized.append(messages)
|
41
|
+
|
42
|
+
chat_message_scatter = []
|
43
|
+
for messages in chat_messages_not_memorized[-contextual_msg_max_count:]:
|
44
|
+
chat_message_scatter.extend(messages)
|
45
|
+
|
46
|
+
# Sort by time_created if available
|
47
|
+
if chat_message_scatter and hasattr(chat_message_scatter[0], 'time_created'):
|
48
|
+
chat_message_scatter.sort(key=lambda _: _.time_created)
|
49
|
+
|
50
|
+
# Store result in context
|
51
|
+
self.context.chat_messages = chat_message_scatter
|
52
|
+
logger.info(f"Retrieved {len(chat_message_scatter)} unmemorized chat messages")
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from flowllm import C
|
2
|
+
|
3
|
+
from reme_ai.vector_store import RecallVectorStoreOp
|
4
|
+
|
5
|
+
|
6
|
+
@C.register_op()
|
7
|
+
class RetrieveMemoryOp(RecallVectorStoreOp):
|
8
|
+
"""
|
9
|
+
Retrieves memories based on specified criteria such as status, type, and timestamp.
|
10
|
+
Processes these memories concurrently, sorts them by similarity, and logs the activity,
|
11
|
+
facilitating efficient memory retrieval operations within a given scope.
|
12
|
+
"""
|
13
|
+
file_path: str = __file__
|
@@ -0,0 +1,170 @@
|
|
1
|
+
import json
|
2
|
+
import re
|
3
|
+
from typing import List
|
4
|
+
|
5
|
+
from flowllm import C, BaseLLMOp
|
6
|
+
from loguru import logger
|
7
|
+
|
8
|
+
from reme_ai.schema import Message, Role
|
9
|
+
from reme_ai.schema.memory import BaseMemory
|
10
|
+
|
11
|
+
|
12
|
+
@C.register_op()
|
13
|
+
class SemanticRankOp(BaseLLMOp):
|
14
|
+
"""
|
15
|
+
The SemanticRankOp class processes queries by retrieving memory nodes,
|
16
|
+
removing duplicates, ranking them based on semantic relevance using a model,
|
17
|
+
assigning scores, sorting the nodes, and storing the ranked nodes back,
|
18
|
+
while logging relevant information.
|
19
|
+
"""
|
20
|
+
file_path: str = __file__
|
21
|
+
|
22
|
+
def execute(self):
|
23
|
+
"""
|
24
|
+
Executes the primary workflow of the SemanticRankOp which includes:
|
25
|
+
- Retrieves query and memory list from context.
|
26
|
+
- Removes duplicate memories.
|
27
|
+
- Ranks memories semantically using LLM.
|
28
|
+
- Assigns scores to memories.
|
29
|
+
- Sorts memories by score.
|
30
|
+
- Saves the ranked memories back to context.
|
31
|
+
|
32
|
+
If no memories are retrieved or if the ranking fails,
|
33
|
+
appropriate warnings are logged.
|
34
|
+
"""
|
35
|
+
# Get memory list from context - previous op guarantees this exists
|
36
|
+
memory_list: List[BaseMemory] = self.context.response.metadata["memory_list"]
|
37
|
+
query: str = self.context.query
|
38
|
+
|
39
|
+
# Get parameters from op_params
|
40
|
+
enable_ranker: bool = self.op_params.get("enable_ranker", True)
|
41
|
+
output_memory_max_count: int = self.op_params.get("output_memory_max_count", 10)
|
42
|
+
|
43
|
+
if not memory_list:
|
44
|
+
logger.warning("Memory list is empty!")
|
45
|
+
return
|
46
|
+
|
47
|
+
logger.info(f"Semantic ranking {len(memory_list)} memories for query: {query[:100]}...")
|
48
|
+
|
49
|
+
if not enable_ranker or len(memory_list) <= output_memory_max_count:
|
50
|
+
# Use original scores if ranker is disabled or memory count is small
|
51
|
+
logger.info("Skipping semantic ranking - using original scores")
|
52
|
+
else:
|
53
|
+
# Remove duplicates based on content
|
54
|
+
memory_dict = {memory.content.strip(): memory for memory in memory_list if memory.content.strip()}
|
55
|
+
memory_list = list(memory_dict.values())
|
56
|
+
logger.info(f"After deduplication: {len(memory_list)} memories")
|
57
|
+
|
58
|
+
# Perform semantic ranking using LLM
|
59
|
+
ranked_memories = self._semantic_rank_memories(query, memory_list)
|
60
|
+
if ranked_memories:
|
61
|
+
memory_list = ranked_memories
|
62
|
+
|
63
|
+
# Sort by score
|
64
|
+
memory_list = sorted(memory_list, key=lambda m: m.score, reverse=True)
|
65
|
+
|
66
|
+
# Log top ranked memories
|
67
|
+
logger.info(f"Semantic ranking completed for query: {query[:50]}...")
|
68
|
+
for i, memory in enumerate(memory_list[:5]): # Log top 5
|
69
|
+
logger.info(f"Top {i + 1}: Score={memory.score:.3f}, Content={memory.content[:80]}...")
|
70
|
+
|
71
|
+
# Save ranked memories back to context
|
72
|
+
self.context.response.metadata["memory_list"] = memory_list
|
73
|
+
|
74
|
+
def _semantic_rank_memories(self, query: str, memories: List[BaseMemory]) -> List[BaseMemory]:
|
75
|
+
"""
|
76
|
+
Use LLM to semantically rank memories based on relevance to the query
|
77
|
+
"""
|
78
|
+
if not memories:
|
79
|
+
return memories
|
80
|
+
|
81
|
+
# Format memories for ranking
|
82
|
+
formatted_memories = SemanticRankOp.format_memories_for_llm_ranking(memories)
|
83
|
+
|
84
|
+
# Create prompt for semantic ranking
|
85
|
+
prompt = f"""Given the query: "{query}"
|
86
|
+
|
87
|
+
Please rank the following memories by their semantic relevance to the query.
|
88
|
+
Rate each memory on a scale of 0.0 to 1.0 where 1.0 is most relevant.
|
89
|
+
|
90
|
+
Memories:
|
91
|
+
{formatted_memories}
|
92
|
+
|
93
|
+
Please respond in JSON format:
|
94
|
+
{{"rankings": [{{"index": 0, "score": 0.8}}, {{"index": 1, "score": 0.6}}, ...]}}"""
|
95
|
+
|
96
|
+
response = self.llm.chat([Message(role=Role.USER, content=prompt)])
|
97
|
+
|
98
|
+
if not response or not response.content:
|
99
|
+
logger.warning("LLM ranking failed, using original order")
|
100
|
+
return memories
|
101
|
+
|
102
|
+
# Parse and apply ranking results
|
103
|
+
rankings = SemanticRankOp.parse_llm_ranking_response(response.content)
|
104
|
+
|
105
|
+
if rankings:
|
106
|
+
applied_count = SemanticRankOp.apply_semantic_scores_to_memories(memories, rankings)
|
107
|
+
logger.info(f"Successfully applied semantic rankings to {applied_count} memories")
|
108
|
+
else:
|
109
|
+
logger.warning("Failed to parse ranking results")
|
110
|
+
|
111
|
+
return memories
|
112
|
+
|
113
|
+
@staticmethod
|
114
|
+
def parse_llm_ranking_response(response: str) -> List[dict]:
|
115
|
+
"""Parse LLM ranking response to extract rankings."""
|
116
|
+
try:
|
117
|
+
# Try to extract JSON blocks
|
118
|
+
json_pattern = r'```json\s*([\s\S]*?)\s*```'
|
119
|
+
json_blocks = re.findall(json_pattern, response)
|
120
|
+
|
121
|
+
if json_blocks:
|
122
|
+
parsed = json.loads(json_blocks[0])
|
123
|
+
if isinstance(parsed, dict) and "rankings" in parsed:
|
124
|
+
return parsed["rankings"]
|
125
|
+
|
126
|
+
# Fallback: try to parse the entire response as JSON
|
127
|
+
parsed = json.loads(response)
|
128
|
+
if isinstance(parsed, dict) and "rankings" in parsed:
|
129
|
+
return parsed["rankings"]
|
130
|
+
|
131
|
+
except json.JSONDecodeError:
|
132
|
+
logger.warning("Failed to parse ranking response as JSON")
|
133
|
+
|
134
|
+
return []
|
135
|
+
|
136
|
+
@staticmethod
|
137
|
+
def apply_semantic_scores_to_memories(memories: List, rankings: List[dict]) -> int:
|
138
|
+
"""Apply semantic ranking scores to memory objects."""
|
139
|
+
applied_count = 0
|
140
|
+
|
141
|
+
for ranking in rankings:
|
142
|
+
idx = ranking.get("index", -1)
|
143
|
+
score = ranking.get("score", 0.0)
|
144
|
+
|
145
|
+
if 0 <= idx < len(memories):
|
146
|
+
# Set score on memory object
|
147
|
+
if hasattr(memories[idx], 'score'):
|
148
|
+
memories[idx].score = score
|
149
|
+
applied_count += 1
|
150
|
+
else:
|
151
|
+
# Add score as metadata if score attribute doesn't exist
|
152
|
+
if not hasattr(memories[idx], 'metadata'):
|
153
|
+
memories[idx].metadata = {}
|
154
|
+
memories[idx].metadata['semantic_score'] = score
|
155
|
+
applied_count += 1
|
156
|
+
|
157
|
+
return applied_count
|
158
|
+
|
159
|
+
@staticmethod
|
160
|
+
def format_memories_for_llm_ranking(memories: List) -> str:
|
161
|
+
"""Format memories for LLM ranking input."""
|
162
|
+
formatted_memories = []
|
163
|
+
|
164
|
+
for i, memory in enumerate(memories):
|
165
|
+
memory_text = f"Memory {i}:\n"
|
166
|
+
memory_text += f"When to use: {memory.when_to_use}\n"
|
167
|
+
memory_text += f"Content: {memory.content}\n"
|
168
|
+
formatted_memories.append(memory_text)
|
169
|
+
|
170
|
+
return "\n---\n".join(formatted_memories)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import datetime
|
2
|
+
from typing import Tuple
|
3
|
+
|
4
|
+
from flowllm import C, BaseOp
|
5
|
+
from loguru import logger
|
6
|
+
|
7
|
+
from reme_ai.constants.common_constants import QUERY_WITH_TS
|
8
|
+
|
9
|
+
|
10
|
+
@C.register_op()
|
11
|
+
class SetQueryOp(BaseOp):
|
12
|
+
"""
|
13
|
+
The `SetQueryOp` class is responsible for setting a query and its associated timestamp
|
14
|
+
into the context, utilizing either provided parameters or details from the context.
|
15
|
+
"""
|
16
|
+
|
17
|
+
def execute(self):
|
18
|
+
"""
|
19
|
+
Executes the operation's primary function, which involves determining the query and its
|
20
|
+
timestamp, then storing these values within the context.
|
21
|
+
|
22
|
+
Input requirement: self.context.query must exist (flow input requirement)
|
23
|
+
"""
|
24
|
+
# Flow guarantees query exists - use it directly
|
25
|
+
query: str = self.context.query
|
26
|
+
timestamp: int = int(datetime.datetime.now().timestamp())
|
27
|
+
|
28
|
+
# Set timestamp if provided in op_params
|
29
|
+
_timestamp = self.op_params.get("timestamp")
|
30
|
+
if _timestamp and isinstance(_timestamp, int):
|
31
|
+
timestamp = _timestamp
|
32
|
+
|
33
|
+
# Store the query and its timestamp in the context
|
34
|
+
query_with_ts: Tuple[str, int] = (query, timestamp)
|
35
|
+
self.context[QUERY_WITH_TS] = query_with_ts
|
36
|
+
|
37
|
+
logger.info(f"Set query with timestamp: query='{query}', timestamp={timestamp}")
|
@@ -0,0 +1,38 @@
|
|
1
|
+
from flowllm import C, BaseLLMOp
|
2
|
+
from flowllm.utils.llm_utils import merge_messages_content
|
3
|
+
from loguru import logger
|
4
|
+
|
5
|
+
from reme_ai.schema import Message, Role
|
6
|
+
|
7
|
+
|
8
|
+
@C.register_op()
|
9
|
+
class BuildQueryOp(BaseLLMOp):
|
10
|
+
file_path: str = __file__
|
11
|
+
|
12
|
+
def execute(self):
|
13
|
+
if "query" in self.context:
|
14
|
+
query = self.context.query
|
15
|
+
|
16
|
+
elif "messages" in self.context:
|
17
|
+
if self.op_params.get("enable_llm_build", True):
|
18
|
+
execution_process = merge_messages_content(self.context.messages)
|
19
|
+
prompt = self.prompt_format(prompt_name="query_build", execution_process=execution_process)
|
20
|
+
message = self.llm.chat(messages=[Message(role=Role.USER, content=prompt)])
|
21
|
+
query = message.content
|
22
|
+
|
23
|
+
else:
|
24
|
+
context_parts = []
|
25
|
+
message_summaries = []
|
26
|
+
for message in self.context.messages[-3:]: # Last 3 messages
|
27
|
+
content = message.content[:200] + "..." if len(message.content) > 200 else message.content
|
28
|
+
message_summaries.append(f"- {message.role.value}: {content}")
|
29
|
+
if message_summaries:
|
30
|
+
context_parts.append("Recent messages:\n" + "\n".join(message_summaries))
|
31
|
+
|
32
|
+
query = "\n\n".join(context_parts)
|
33
|
+
|
34
|
+
else:
|
35
|
+
raise RuntimeError("query or messages is required!")
|
36
|
+
|
37
|
+
logger.info(f"build.query={query}")
|
38
|
+
self.context.query = query
|
@@ -0,0 +1,27 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from flowllm import C, BaseOp
|
4
|
+
from loguru import logger
|
5
|
+
|
6
|
+
from reme_ai.schema.memory import BaseMemory
|
7
|
+
|
8
|
+
|
9
|
+
@C.register_op()
|
10
|
+
class MergeMemoryOp(BaseOp):
|
11
|
+
|
12
|
+
def execute(self):
|
13
|
+
memory_list: List[BaseMemory] = self.context.response.metadata["memory_list"]
|
14
|
+
|
15
|
+
if not memory_list:
|
16
|
+
return
|
17
|
+
|
18
|
+
content_collector = ["Previous Memory"]
|
19
|
+
for memory in memory_list:
|
20
|
+
if not memory.content:
|
21
|
+
continue
|
22
|
+
|
23
|
+
content_collector.append(f"- {memory.when_to_use} {memory.content}\n")
|
24
|
+
content_collector.append("Please consider the helpful parts from these in answering the question, "
|
25
|
+
"to make the response more comprehensive and substantial.")
|
26
|
+
self.context.response.answer = "\n".join(content_collector)
|
27
|
+
logger.info(f"response.answer={self.context.response.answer}")
|