Merge branch 'feature/moshui20260409-guideline-assistant-message-error' into staging

This commit is contained in:
朱潮 2026-04-09 15:05:23 +08:00
commit feb8557c23
3 changed files with 147 additions and 9 deletions

View File

@ -6,7 +6,7 @@ from utils.fastapi_utils import (extract_block_from_system_prompt, format_messag
from langchain.chat_models import BaseChatModel
from langgraph.runtime import Runtime
from langchain_core.messages import SystemMessage
from langchain_core.messages import SystemMessage, HumanMessage
from typing import Any, Callable
from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.outputs import LLMResult
@ -124,10 +124,11 @@ Action: Provide concise, friendly, and personified natural responses.
response.additional_kwargs["message_tag"] = "THINK"
response.content = f"<think>{response.content}</think>"
# 将响应添加到原始消息列表
state['messages'] = state['messages'] + [response]
# 将响应添加到原始消息列表,并追加 HumanMessage 确保消息以 user 结尾
# 某些模型不支持 assistant message prefill要求最后一条消息必须是 user
state['messages'] = state['messages'] + [response, HumanMessage(content=self._get_follow_up_prompt())]
return state
async def abefore_agent(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
if not self.guidelines:
return None
@ -148,10 +149,23 @@ Action: Provide concise, friendly, and personified natural responses.
response.additional_kwargs["message_tag"] = "THINK"
response.content = f"<think>{response.content}</think>"
# 将响应添加到原始消息列表
state['messages'] = state['messages'] + [response]
# 将响应添加到原始消息列表,并追加 HumanMessage 确保消息以 user 结尾
# 某些模型不支持 assistant message prefill要求最后一条消息必须是 user
state['messages'] = state['messages'] + [response, HumanMessage(content=self._get_follow_up_prompt())]
return state
def _get_follow_up_prompt(self) -> str:
"""根据语言返回引导主 agent 回复的提示"""
prompts = {
"ja": "以上の分析に基づいて、ユーザーに返信してください。",
"jp": "以上の分析に基づいて、ユーザーに返信してください。",
"zh": "请根据以上分析,回复用户。",
"zh-TW": "請根據以上分析,回覆用戶。",
"ko": "위 분석을 바탕으로 사용자에게 답변해 주세요.",
"en": "Based on the above analysis, please respond to the user.",
}
return prompts.get(self.language, prompts["en"])
def wrap_model_call(
self,
request: ModelRequest,

View File

@ -148,6 +148,21 @@ Output: {{"facts" : []}}
Input: DR1の照明状態を教えて
Output: {{"facts" : []}}
Input: 私は林檎好きです
Output: {{"facts" : ["林檎が好き"]}}
Input: コーヒー飲みたい、毎朝
Output: {{"facts" : ["毎朝コーヒーを飲みたい"]}}
Input: 昨日映画見た、すごくよかった
Output: {{"facts" : ["昨日映画を見た", "映画がすごくよかった"]}}
Input: 我喜欢吃苹果
Output: {{"facts" : ["喜欢吃苹果"]}}
Input: 나는 사과를 좋아해
Output: {{"facts" : ["사과를 좋아함"]}}
Return the facts and preferences in a json format as shown above.
Remember the following:
@ -159,12 +174,15 @@ Remember the following:
- If you do not find anything relevant in the below conversation, you can return an empty list corresponding to the "facts" key.
- Create the facts based on the user and assistant messages only. Do not pick anything from the system messages.
- Make sure to return the response in the format mentioned in the examples. The response should be in json with a key as "facts" and corresponding value will be a list of strings.
- **CRITICAL - Do NOT memorize actions or operations**: Do not extract facts about queries the user asked you to perform, devices the user asked you to operate, or any one-time transient actions. Only memorize information ABOUT the user (preferences, relationships, personal details, plans), not actions the user asked the assistant to DO. Ask yourself: "Is this a fact about WHO the user IS, or what the user asked me to DO?" Only remember the former.
- **CRITICAL for Semantic Completeness**:
- Each extracted fact MUST preserve the complete semantic meaning. Never truncate or drop key parts of the meaning.
- For colloquial or grammatically informal expressions (common in spoken Japanese, Chinese, Korean, etc.), understand the full intended meaning and record it in a clear, semantically complete form.
- In Japanese, spoken language often omits particles (e.g., が, を, に). When extracting facts, include the necessary particles to make the meaning unambiguous. For example: "私は林檎好きです" should be understood as "林檎が好き" (likes apples), not literally "私は林檎好き".
- When the user expresses a preference or opinion in casual speech, record the core preference/opinion clearly. Remove the subject pronoun (私は/I) since facts are about the user by default, but keep all other semantic components intact.
- **CRITICAL for People/Relationship Tracking**:
- Write people-related facts in plain, natural language. Do NOT use structured formats like "Contact:", "referred as", or "DEFAULT when user says".
- Good examples: "Michael Johnson is a colleague, also called Mike", "田中さんは友達", "滨田太郎は「滨田」とも呼ばれている"

View File

@ -1,13 +1,13 @@
"""
Memory 管理 API 路由
提供记忆查看和删除功能
提供记忆查看添加和删除功能
"""
import logging
from typing import Optional, List, Dict, Any
from typing import Literal, Optional, List, Dict, Any
from fastapi import APIRouter, HTTPException, Header, Query
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from pydantic import BaseModel, Field
logger = logging.getLogger('app')
@ -33,6 +33,26 @@ class DeleteAllResponse(BaseModel):
deleted_count: int
class ConversationMessage(BaseModel):
"""对话消息"""
role: Literal["user", "assistant"]
content: str = Field(..., min_length=1)
class AddMemoryRequest(BaseModel):
"""添加记忆的请求体"""
bot_id: str = Field(..., min_length=1)
user_id: str = Field(..., min_length=1)
messages: List[ConversationMessage] = Field(..., max_length=200)
class AddMemoryResponse(BaseModel):
"""添加记忆的响应"""
success: bool
pairs_processed: int
pairs_failed: int = 0
async def get_user_identifier_from_request(
authorization: Optional[str],
user_id: Optional[str] = None
@ -63,6 +83,92 @@ async def get_user_identifier_from_request(
)
@router.post("/memory", response_model=AddMemoryResponse)
async def add_memory_from_conversation(data: AddMemoryRequest):
"""
从对话消息中提取并保存记忆
将用户和助手的对话配对通过 Mem0 提取关键事实并存储
用于 realtime 语音对话等不经过 Agent 中间件的场景
此端点供内部服务调用 felo-mygpt不暴露给外部用户
"""
try:
from agent.mem0_manager import get_mem0_manager
from utils.settings import MEM0_ENABLED
if not MEM0_ENABLED:
raise HTTPException(
status_code=503,
detail="Memory feature is not enabled"
)
if not data.messages:
return AddMemoryResponse(success=True, pairs_processed=0)
manager = get_mem0_manager()
# 将消息配对为 user-assistant 对,然后调用 add_memory
pairs_processed = 0
pairs_failed = 0
i = 0
while i < len(data.messages):
msg = data.messages[i]
if msg.role == 'user':
# 收集连续的 user 消息
user_contents = [msg.content]
j = i + 1
while j < len(data.messages) and data.messages[j].role == 'user':
user_contents.append(data.messages[j].content)
j += 1
user_text = '\n'.join(user_contents)
# 检查是否有对应的 assistant 回复
assistant_text = ""
if j < len(data.messages) and data.messages[j].role == 'assistant':
assistant_text = data.messages[j].content or ""
j += 1
if user_text and assistant_text:
conversation_text = f"User: {user_text}\nAssistant: {assistant_text}"
try:
await manager.add_memory(
text=conversation_text,
user_id=data.user_id,
agent_id=data.bot_id,
metadata={"type": "realtime_conversation"},
)
pairs_processed += 1
except Exception as pair_error:
pairs_failed += 1
logger.error(
f"Failed to add memory for pair: {pair_error}"
)
i = j
else:
i += 1
logger.info(
f"Added {pairs_processed} memory pairs (failed={pairs_failed}) "
f"for user={data.user_id}, bot={data.bot_id}"
)
return AddMemoryResponse(
success=pairs_failed == 0,
pairs_processed=pairs_processed,
pairs_failed=pairs_failed,
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to add memory from conversation: {e}")
raise HTTPException(
status_code=500,
detail="Failed to add memory from conversation"
)
@router.get("/memory", response_model=MemoryListResponse)
async def get_memories(
bot_id: str = Query(..., description="Bot ID (对应 agent_id)"),