Skip to content

Commit e8e5974

Browse files
CaralHsiyuan.wangWang-Daoji
authored
feat: adjust skill prompt and fix some bugs (#988)
* feat: skill with history (#986) * feat: skill with history * feat: pass chat-history into skill * feat: modify chat-history passing in skills * feat: modify code * fix: we don't need to pass history in part B * fix: process skill memory * feat: we do not return None with few history now * feat: update skill * feat: modify _split_task_chunk_by_llm * feat: filter by embedding * feat: modify skill --------- Co-authored-by: yuan.wang <yuan.wang@yuanwangdebijibendiannao.local> Co-authored-by: Wang Daoji <75928131+Wang-Daoji@users.noreply.github.com> * feat: skill with history (#987) * feat: skill with history * feat: pass chat-history into skill * feat: modify chat-history passing in skills * feat: modify code * fix: we don't need to pass history in part B * fix: process skill memory * feat: we do not return None with few history now * feat: update skill * feat: modify _split_task_chunk_by_llm * feat: filter by embedding * feat: modify skill * feat: reinforce update rule --------- Co-authored-by: yuan.wang <yuan.wang@yuanwangdebijibendiannao.local> Co-authored-by: Wang Daoji <75928131+Wang-Daoji@users.noreply.github.com> --------- Co-authored-by: yuan.wang <yuan.wang@yuanwangdebijibendiannao.local> Co-authored-by: Wang Daoji <75928131+Wang-Daoji@users.noreply.github.com>
1 parent fd25c3d commit e8e5974

9 files changed

Lines changed: 117 additions & 28 deletions

File tree

src/memos/api/handlers/search_handler.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -377,16 +377,13 @@ def _extract_embeddings(memories: list[dict[str, Any]]) -> list[list[float]] | N
377377

378378
@staticmethod
379379
def _strip_embeddings(results: dict[str, Any]) -> None:
380-
for bucket in results.get("text_mem", []):
381-
for mem in bucket.get("memories", []):
382-
metadata = mem.get("metadata", {})
383-
if "embedding" in metadata:
384-
metadata["embedding"] = []
385-
for bucket in results.get("tool_mem", []):
386-
for mem in bucket.get("memories", []):
387-
metadata = mem.get("metadata", {})
388-
if "embedding" in metadata:
389-
metadata["embedding"] = []
380+
for _mem_type, mem_results in results.items():
381+
if isinstance(mem_results, list):
382+
for bucket in mem_results:
383+
for mem in bucket.get("memories", []):
384+
metadata = mem.get("metadata", {})
385+
if "embedding" in metadata:
386+
metadata["embedding"] = []
390387

391388
@staticmethod
392389
def _dice_similarity(text1: str, text2: str) -> float:

src/memos/mem_reader/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def set_searcher(self, searcher: "Searcher | None") -> None:
4242

4343
@abstractmethod
4444
def get_memory(
45-
self, scene_data: list, type: str, info: dict[str, Any], mode: str = "fast"
45+
self, scene_data: list, type: str, info: dict[str, Any], mode: str = "fast", **kwargs
4646
) -> list[list[TextualMemoryItem]]:
4747
"""Various types of memories extracted from scene_data"""
4848

src/memos/mem_reader/multi_modal_struct.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,7 @@ def _process_multi_modal_data(
862862
# Part A: call llm in parallel using thread pool
863863
fine_memory_items = []
864864

865-
with ContextThreadPoolExecutor(max_workers=2) as executor:
865+
with ContextThreadPoolExecutor(max_workers=3) as executor:
866866
future_string = executor.submit(
867867
self._process_string_fine, fast_memory_items, info, custom_tags, **kwargs
868868
)

src/memos/mem_reader/read_skill_memory/process_skill_memory.py

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,17 @@ def _reconstruct_messages_from_memory_items(memory_items: list[TextualMemoryItem
105105
return reconstructed_messages
106106

107107

108+
def _preprocess_extract_messages(
109+
history: MessageList, messages: MessageList
110+
) -> (MessageList, MessageList):
111+
"""Process data and check whether to extract skill memory"""
112+
history = history[-20:]
113+
if (len(history) + len(messages)) < 10:
114+
# TODO: maybe directly return []
115+
logger.warning("[PROCESS_SKILLS] Not enough messages to extract skill memory")
116+
return history, messages
117+
118+
108119
def _add_index_to_message(messages: MessageList) -> MessageList:
109120
for i, message in enumerate(messages):
110121
message["idx"] = i
@@ -144,18 +155,27 @@ def _split_task_chunk_by_llm(llm: BaseLLM, messages: MessageList) -> dict[str, M
144155
message_indices = item["message_indices"]
145156
for indices in message_indices:
146157
# Validate that indices is a list/tuple with exactly 2 elements
147-
if not isinstance(indices, list | tuple) or len(indices) != 2:
158+
if isinstance(indices, list) and len(indices) == 1:
159+
start, end = indices[0], indices[0] + 1
160+
elif isinstance(indices, int):
161+
start, end = indices, indices + 1
162+
elif isinstance(indices, list) and len(indices) == 2:
163+
start, end = indices[0], indices[1] + 1
164+
else:
148165
logger.warning(
149166
f"[PROCESS_SKILLS] Invalid message indices format for task '{task_name}': {indices}, skipping"
150167
)
151168
continue
152-
start, end = indices
153-
task_chunks.setdefault(task_name, []).extend(messages[start : end + 1])
169+
task_chunks.setdefault(task_name, []).extend(messages[start:end])
154170
return task_chunks
155171

156172

157173
def _extract_skill_memory_by_llm(
158-
messages: MessageList, old_memories: list[TextualMemoryItem], llm: BaseLLM
174+
messages: MessageList,
175+
old_memories: list[TextualMemoryItem],
176+
llm: BaseLLM,
177+
chat_history: MessageList,
178+
chat_history_max_length: int = 5000,
159179
) -> dict[str, Any]:
160180
old_memories_dict = [skill_memory.model_dump() for skill_memory in old_memories]
161181
old_mem_references = [
@@ -169,7 +189,7 @@ def _extract_skill_memory_by_llm(
169189
"examples": mem["metadata"]["examples"],
170190
"tags": mem["metadata"]["tags"],
171191
"scripts": mem["metadata"].get("scripts"),
172-
"others": mem["metadata"]["others"],
192+
"others": mem["metadata"].get("others"),
173193
}
174194
for mem in old_memories_dict
175195
]
@@ -179,17 +199,26 @@ def _extract_skill_memory_by_llm(
179199
[f"{message['role']}: {message['content']}" for message in messages]
180200
)
181201

202+
# Prepare history context
203+
chat_history_context = "\n".join(
204+
[f"{history['role']}: {history['content']}" for history in chat_history]
205+
)
206+
chat_history_context = chat_history_context[-chat_history_max_length:]
207+
182208
# Prepare old memories context
183209
old_memories_context = json.dumps(old_mem_references, ensure_ascii=False, indent=2)
184210

185211
# Prepare prompt
186212
lang = detect_lang(messages_context)
187213
template = SKILL_MEMORY_EXTRACTION_PROMPT_ZH if lang == "zh" else SKILL_MEMORY_EXTRACTION_PROMPT
188-
prompt_content = template.replace("{old_memories}", old_memories_context).replace(
189-
"{messages}", messages_context
214+
prompt_content = (
215+
template.replace("{old_memories}", old_memories_context)
216+
.replace("{messages}", messages_context)
217+
.replace("{chat_history}", chat_history_context)
190218
)
191219

192220
prompt = [{"role": "user", "content": prompt_content}]
221+
logger.info(f"[Skill Memory]: Prompt {prompt_content}")
193222

194223
# Call LLM to extract skill memory with retry logic
195224
for attempt in range(3):
@@ -198,7 +227,8 @@ def _extract_skill_memory_by_llm(
198227
skills_llm = os.getenv("SKILLS_LLM", None)
199228
llm_kwargs = {"model_name_or_path": skills_llm} if skills_llm else {}
200229
response_text = llm.generate(prompt, **llm_kwargs)
201-
# Clean up response (remove markdown code blocks if present)
230+
# Clean up response (remove Markdown code blocks if present)
231+
logger.info(f"[Skill Memory]: response_text {response_text}")
202232
response_text = response_text.strip()
203233
response_text = response_text.replace("```json", "").replace("```", "").strip()
204234

@@ -537,6 +567,11 @@ def process_skill_memory_fine(
537567
)
538568
return []
539569

570+
chat_history = kwargs.get("chat_history")
571+
if not chat_history or not isinstance(chat_history, list):
572+
chat_history = []
573+
logger.warning("[PROCESS_SKILLS] History is None in Skills")
574+
540575
# Validate skills_dir has required keys
541576
required_keys = ["skills_local_dir", "skills_oss_dir"]
542577
missing_keys = [key for key in required_keys if key not in skills_dir_config]
@@ -552,7 +587,13 @@ def process_skill_memory_fine(
552587
return []
553588

554589
messages = _reconstruct_messages_from_memory_items(fast_memory_items)
590+
591+
chat_history, messages = _preprocess_extract_messages(chat_history, messages)
592+
if not messages:
593+
return []
594+
555595
messages = _add_index_to_message(messages)
596+
chat_history = _add_index_to_message(chat_history)
556597

557598
task_chunks = _split_task_chunk_by_llm(llm, messages)
558599
if not task_chunks:
@@ -594,6 +635,7 @@ def process_skill_memory_fine(
594635
messages,
595636
related_skill_memories_by_task.get(task_type, []),
596637
llm,
638+
chat_history,
597639
): task_type
598640
for task_type, messages in task_chunks.items()
599641
}
@@ -608,7 +650,7 @@ def process_skill_memory_fine(
608650

609651
# write skills to file and get zip paths
610652
skill_memory_with_paths = []
611-
with ContextThreadPoolExecutor(max_workers=min(len(skill_memories), 5)) as executor:
653+
with ContextThreadPoolExecutor(max_workers=5) as executor:
612654
futures = {
613655
executor.submit(
614656
_write_skills_to_file, skill_memory, info, skills_dir_config
@@ -713,9 +755,11 @@ def process_skill_memory_fine(
713755
continue
714756

715757
# TODO: deprecate this funtion and call
716-
for skill_memory in skill_memory_items:
758+
for skill_memory, skill_memory_item in zip(skill_memories, skill_memory_items, strict=False):
759+
if skill_memory.get("update", False) and skill_memory.get("old_memory_id", ""):
760+
continue
717761
add_id_to_mysql(
718-
memory_id=skill_memory.id, mem_cube_id=kwargs.get("user_name", info.get("user_id", ""))
762+
memory_id=skill_memory_item.id,
763+
mem_cube_id=kwargs.get("user_name", info.get("user_id", "")),
719764
)
720-
721765
return skill_memory_items

src/memos/mem_reader/simple_struct.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,7 @@ def get_memory(
428428
info: dict[str, Any],
429429
mode: str = "fine",
430430
user_name: str | None = None,
431+
**kwargs,
431432
) -> list[list[TextualMemoryItem]]:
432433
"""
433434
Extract and classify memory content from scene_data.
@@ -471,7 +472,9 @@ def get_memory(
471472
# Backward compatibility, after coercing scene_data, we only tackle
472473
# with standard scene_data type: MessagesType
473474
standard_scene_data = coerce_scene_data(scene_data, type)
474-
return self._read_memory(standard_scene_data, type, info, mode, user_name=user_name)
475+
return self._read_memory(
476+
standard_scene_data, type, info, mode, user_name=user_name, **kwargs
477+
)
475478

476479
def rewrite_memories(
477480
self, messages: list[dict], memory_list: list[TextualMemoryItem], user_only: bool = True

src/memos/mem_scheduler/general_scheduler.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,7 @@ def process_message(message: ScheduleMessageItem):
764764
content = message.content
765765
user_name = message.user_name
766766
info = message.info or {}
767+
chat_history = message.chat_history
767768

768769
# Parse the memory IDs from content
769770
mem_ids = json.loads(content) if isinstance(content, str) else content
@@ -790,6 +791,7 @@ def process_message(message: ScheduleMessageItem):
790791
custom_tags=info.get("custom_tags", None),
791792
task_id=message.task_id,
792793
info=info,
794+
chat_history=chat_history,
793795
)
794796

795797
logger.info(
@@ -817,6 +819,7 @@ def _process_memories_with_reader(
817819
custom_tags: list[str] | None = None,
818820
task_id: str | None = None,
819821
info: dict | None = None,
822+
chat_history: list | None = None,
820823
) -> None:
821824
logger.info(
822825
f"[DIAGNOSTIC] general_scheduler._process_memories_with_reader called. mem_ids: {mem_ids}, user_id: {user_id}, mem_cube_id: {mem_cube_id}, task_id: {task_id}"
@@ -878,6 +881,7 @@ def _process_memories_with_reader(
878881
type="chat",
879882
custom_tags=custom_tags,
880883
user_name=user_name,
884+
chat_history=chat_history,
881885
)
882886
except Exception as e:
883887
logger.warning(f"{e}: Fail to transfer mem: {memory_items}")

src/memos/mem_scheduler/schemas/message_schemas.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class ScheduleMessageItem(BaseModel, DictConversionMixin):
5454
default=None,
5555
description="Optional business-level task ID. Multiple items can share the same task_id.",
5656
)
57+
chat_history: list | None = Field(default=None, description="user chat history")
5758

5859
# Pydantic V2 model configuration
5960
model_config = ConfigDict(
@@ -89,6 +90,7 @@ def to_dict(self) -> dict:
8990
"timestamp": self.timestamp.isoformat(),
9091
"user_name": self.user_name,
9192
"task_id": self.task_id if self.task_id is not None else "",
93+
"chat_history": self.chat_history if self.chat_history is not None else [],
9294
}
9395

9496
@classmethod
@@ -104,6 +106,7 @@ def from_dict(cls, data: dict) -> "ScheduleMessageItem":
104106
timestamp=datetime.fromisoformat(data["timestamp"]),
105107
user_name=data.get("user_name"),
106108
task_id=data.get("task_id"),
109+
chat_history=data.get("chat_history"),
107110
)
108111

109112

@@ -158,6 +161,7 @@ class ScheduleLogForWebItem(BaseModel, DictConversionMixin):
158161
default=None, description="Completion status of the task (e.g., 'completed', 'failed')"
159162
)
160163
source_doc_id: str | None = Field(default=None, description="Source document ID")
164+
chat_history: list | None = Field(default=None, description="user chat history")
161165

162166
def debug_info(self) -> dict[str, Any]:
163167
"""Return structured debug information for logging purposes."""

src/memos/multi_mem_cube/single_cube.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,7 @@ def _schedule_memory_tasks(
553553
timestamp=datetime.utcnow(),
554554
user_name=self.cube_id,
555555
info=add_req.info,
556+
chat_history=add_req.chat_history,
556557
)
557558
self.mem_scheduler.submit_messages(messages=[message_item_read])
558559
self.logger.info(
@@ -807,6 +808,7 @@ def _process_text_mem(
807808
},
808809
mode=extract_mode,
809810
user_name=user_context.mem_cube_id,
811+
chat_history=add_req.chat_history,
810812
)
811813
self.logger.info(
812814
f"Time for get_memory in extract mode {extract_mode}: {time.time() - init_time}"

src/memos/templates/skill_mem_prompt.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@
7878
# Existing Skill Memories
7979
{old_memories}
8080
81+
# Chat_history
82+
{chat_history}
83+
8184
# Conversation Messages
8285
{messages}
8386
@@ -86,6 +89,11 @@
8689
2. **Universality**: All fields except "example" must remain general and scenario-independent.
8790
3. **Similarity Check**: If similar skill exists, set "update": true with "old_memory_id". Otherwise, set "update": false and leave "old_memory_id" empty.
8891
4. **Language Consistency**: Match the conversation language.
92+
5. **History Usage Constraints**:
93+
- `chat_history` serves only as auxiliary context to supplement stable preferences or methodologies that are not explicitly stated in `messages` but may affect skill abstraction.
94+
- `chat_history` may be considered only when it provides information **missing from `messages`** and **relevant to the current task’s goals, execution approach, or constraints**.
95+
- `chat_history` must not be the primary source of a skill, and may only be used to enrich auxiliary fields such as `preference` or `experience`.
96+
- If `chat_history` does not provide any valid information beyond what already exists in `messages`, or contains only greetings or background content, it must be completely ignored.
8997
9098
# Output Format
9199
```json
@@ -100,7 +108,9 @@
100108
"scripts": {"script_name.py": "# Python code here\nprint('Hello')", "another_script.py": "# More code\nimport os"},
101109
"others": {"Section Title": "Content here", "reference.md": "# Reference content for this skill"},
102110
"update": false,
103-
"old_memory_id": ""
111+
"old_memory_id": "",
112+
"whether_use_chat_history": false,
113+
"content_of_related_chat_history": ""
104114
}
105115
```
106116
@@ -119,6 +129,10 @@
119129
- **examples**: Complete output templates showing the final deliverable format and structure. Should demonstrate how the task result looks when this skill is applied, including format, sections, and content organization. Content can be abbreviated but must show the complete structure. Use markdown format for better readability
120130
- **update**: true if updating existing skill, false if new
121131
- **old_memory_id**: ID of skill being updated, or empty string if new
132+
- **whether_use_chat_history**: Indicates whether information from chat_history that does not appear in messages was incorporated into the skill
133+
- **content_of_related_chat_history**:
134+
If whether_use_chat_history is true, provide a high-level summary of the type of historical information used (e.g., “long-term preference: prioritizes cultural attractions”); do not quote the original dialogue verbatim
135+
If not used, leave this field as an empty string
122136
123137
# Critical Guidelines
124138
- Keep all fields general except "examples"
@@ -141,14 +155,23 @@
141155
# 现有技能记忆
142156
{old_memories}
143157
144-
# 对话消息
158+
# 对话消息的上下文chat_history
159+
{chat_history}
160+
161+
# 当前对话消息
145162
{messages}
146163
147164
# 核心原则
148165
1. **通用化**:提取可跨场景应用的抽象方法论。避免具体细节(如"旅行规划"而非"北京旅行规划")。
149166
2. **普适性**:除"examples"外,所有字段必须保持通用,与具体场景无关。
150167
3. **相似性检查**:如存在相似技能,设置"update": true 及"old_memory_id"。否则设置"update": false 并将"old_memory_id"留空。
151168
4. **语言一致性**:与对话语言保持一致。
169+
5. **历史使用约束**:
170+
- chat_history 仅作为辅助上下文,用于补充 messages 中未明确出现的、但会影响技能抽象的稳定偏好或方法论。
171+
- 当 chat_history 能提供 messages 中缺失、且与当前任务目标、执行方式或约束相关的信息增量时,可以纳入考虑。
172+
- chat_history 不得作为技能的主要来源,仅可用于完善 preference、experience 等辅助字段。
173+
- 若 chat_history 未提供任何 messages 中不存在的有效信息,或仅包含寒暄、背景性内容,应完全忽略。
174+
6. 如果你提取的抽象方法论和已有的技能记忆描述的是同一个主题(比如同一个生活场景),请务必使用更新操作,不要新建一个方法论,注意合理的追加到已有的技能记忆上,保证通顺且不丢失已有方法论的信息。
152175
153176
# 输出格式
154177
```json
@@ -163,7 +186,10 @@
163186
"scripts": {"script_name.py": "# Python 代码\nprint('Hello')", "another_script.py": "# 更多代码\nimport os"},
164187
"others": {"章节标题": "这里的内容", "reference.md": "# 此技能的参考内容"},
165188
"update": false,
166-
"old_memory_id": ""
189+
"old_memory_id": "",
190+
"content_of_current_message": "",
191+
"whether_use_chat_history": false,
192+
"content_of_related_chat_history": "",
167193
}
168194
```
169195
@@ -182,12 +208,21 @@
182208
- **examples**:展示最终任务成果的输出模板,包括格式、章节和内容组织结构。应展示应用此技能后任务结果的样子,包含所有必要的部分。内容可以省略但必须展示完整结构。使用 markdown 格式以提高可读性
183209
- **update**:更新现有技能为true,新建为false
184210
- **old_memory_id**:被更新技能的ID,新建则为空字符串
211+
- **content_of_current_message**: 从当前对话消息中提取的核心内容(简写但必填),
212+
- **whether_use_chat_history**:是否从 chat_history 中引用了 messages 中没有的内容并提取到skill中
213+
- **content_of_related_chat_history**:若 whether_use_chat_history 为 true,
214+
仅需概括性说明所使用的历史信息类型(如“长期偏好:文化类景点优先”),
215+
不要求逐字引用原始对话内容;
216+
若未使用,则置为空字符串。
185217
186218
# 关键指导
187219
- 除"examples"外保持所有字段通用
188220
- "examples"应展示完整的最终输出格式和结构,包含所有必要章节
189221
- "others"包含补充说明或扩展信息
190222
- 无法提取技能时返回null
223+
- 注意区分chat_history与当前对话消息,如果能提出skill,必须有一部分来自于当前对话消息
224+
- 一定仅在必要时才新建方法论,同样的场景尽量合并("update": true),
225+
如饮食规划合并为一条,不要已有“饮食规划”的情况下,再新增一个“生酮饮食规划”。
191226
192227
# 输出格式
193228
仅输出JSON对象。

0 commit comments

Comments
 (0)