feat: 增加对话总结部分

This commit is contained in:
ViperEkura 2026-03-25 19:19:03 +08:00
parent 75eaef0514
commit ca779ca227
3 changed files with 62 additions and 30 deletions

View File

@ -18,13 +18,14 @@ from backend.services.glm_client import GLMClient
class ChatService: class ChatService:
"""Chat completion service with tool support""" """Chat completion service with tool support"""
MAX_ITERATIONS = 5 MAX_ITERATIONS = 5
def __init__(self, glm_client: GLMClient): def __init__(self, glm_client: GLMClient):
self.glm_client = glm_client self.glm_client = glm_client
self.executor = ToolExecutor(registry=registry) self.executor = ToolExecutor(registry=registry)
def sync_response(self, conv: Conversation, tools_enabled: bool = True): def sync_response(self, conv: Conversation, tools_enabled: bool = True):
"""Sync response with tool call support""" """Sync response with tool call support"""
tools = registry.list_all() if tools_enabled else None tools = registry.list_all() if tools_enabled else None
@ -59,7 +60,7 @@ class ChatService:
usage = result.get("usage", {}) usage = result.get("usage", {})
prompt_tokens = usage.get("prompt_tokens", 0) prompt_tokens = usage.get("prompt_tokens", 0)
completion_tokens = usage.get("completion_tokens", 0) completion_tokens = usage.get("completion_tokens", 0)
# Create message # Create message
msg = Message( msg = Message(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
@ -70,14 +71,27 @@ class ChatService:
thinking_content=message.get("reasoning_content", ""), thinking_content=message.get("reasoning_content", ""),
) )
db.session.add(msg) db.session.add(msg)
# Create tool call records # Create tool call records
self._save_tool_calls(msg.id, all_tool_calls, all_tool_results) self._save_tool_calls(msg.id, all_tool_calls, all_tool_results)
db.session.commit() db.session.commit()
user = get_or_create_default_user() user = get_or_create_default_user()
record_token_usage(user.id, conv.model, prompt_tokens, completion_tokens) record_token_usage(user.id, conv.model, prompt_tokens, completion_tokens)
# Set title if needed (first message)
suggested_title = None
if not conv.title or conv.title == "新对话":
user_msg = Message.query.filter_by(
conversation_id=conv.id, role="user"
).order_by(Message.created_at.asc()).first()
if user_msg and user_msg.content:
suggested_title = user_msg.content.strip()[:30]
if not suggested_title:
suggested_title = "新对话"
conv.title = suggested_title
db.session.commit()
return ok({ return ok({
"message": self._message_to_dict(msg), "message": self._message_to_dict(msg),
"usage": { "usage": {
@ -85,6 +99,7 @@ class ChatService:
"completion_tokens": completion_tokens, "completion_tokens": completion_tokens,
"total_tokens": usage.get("total_tokens", 0) "total_tokens": usage.get("total_tokens", 0)
}, },
"suggested_title": suggested_title,
}) })
# Process tool calls # Process tool calls
@ -236,7 +251,8 @@ class ChatService:
if full_thinking: if full_thinking:
yield f"event: process_step\ndata: {json.dumps({'index': step_index, 'type': 'thinking', 'content': full_thinking}, ensure_ascii=False)}\n\n" yield f"event: process_step\ndata: {json.dumps({'index': step_index, 'type': 'thinking', 'content': full_thinking}, ensure_ascii=False)}\n\n"
step_index += 1 step_index += 1
suggested_title = None
with app.app_context(): with app.app_context():
msg = Message( msg = Message(
id=msg_id, id=msg_id,
@ -247,15 +263,34 @@ class ChatService:
thinking_content=full_thinking, thinking_content=full_thinking,
) )
db.session.add(msg) db.session.add(msg)
# Create tool call records # Create tool call records
self._save_tool_calls(msg_id, all_tool_calls, all_tool_results) self._save_tool_calls(msg_id, all_tool_calls, all_tool_results)
db.session.commit() db.session.commit()
user = get_or_create_default_user() user = get_or_create_default_user()
record_token_usage(user.id, conv_model, prompt_tokens, token_count) record_token_usage(user.id, conv_model, prompt_tokens, token_count)
yield f"event: done\ndata: {json.dumps({'message_id': msg_id, 'token_count': token_count})}\n\n" # Check if we need to set title (first message in conversation)
conv = db.session.get(Conversation, conv_id)
if conv and (not conv.title or conv.title == "新对话"):
# Get user message content
user_msg = Message.query.filter_by(
conversation_id=conv_id, role="user"
).order_by(Message.created_at.asc()).first()
if user_msg and user_msg.content:
# Use first 30 chars of user message as title
suggested_title = user_msg.content.strip()[:30]
if not suggested_title:
suggested_title = "新对话"
# Refresh conv to avoid stale state
db.session.refresh(conv)
conv.title = suggested_title
db.session.commit()
else:
suggested_title = None
yield f"event: done\ndata: {json.dumps({'message_id': msg_id, 'token_count': token_count, 'suggested_title': suggested_title}, ensure_ascii=False)}\n\n"
return return
yield f"event: error\ndata: {json.dumps({'content': 'exceeded maximum tool call iterations'}, ensure_ascii=False)}\n\n" yield f"event: error\ndata: {json.dumps({'content': 'exceeded maximum tool call iterations'}, ensure_ascii=False)}\n\n"

View File

@ -298,30 +298,37 @@ async function sendMessage(content) {
streamThinking.value = '' streamThinking.value = ''
streamToolCalls.value = [] streamToolCalls.value = []
streamProcessSteps.value = [] streamProcessSteps.value = []
// Update conversation in list (move to top) // Update conversation in list (move to top)
const idx = conversations.value.findIndex(c => c.id === convId) const idx = conversations.value.findIndex(c => c.id === convId)
if (idx > 0) { if (idx > 0) {
const [conv] = conversations.value.splice(idx, 1) const [conv] = conversations.value.splice(idx, 1)
conv.message_count = (conv.message_count || 0) + 2 conv.message_count = (conv.message_count || 0) + 2
if (data.suggested_title) {
conv.title = data.suggested_title
}
conversations.value.unshift(conv) conversations.value.unshift(conv)
} else if (idx === 0) { } else if (idx === 0) {
conversations.value[0].message_count = (conversations.value[0].message_count || 0) + 2 conversations.value[0].message_count = (conversations.value[0].message_count || 0) + 2
} if (data.suggested_title) {
// Auto title: use first message if title is empty conversations.value[0].title = data.suggested_title
if (conversations.value[0] && !conversations.value[0].title) { }
try {
await conversationApi.update(convId, { title: content.slice(0, 30) })
conversations.value[0].title = content.slice(0, 30)
} catch (_) {}
} }
} else { } else {
// //
try { try {
const res = await messageApi.list(convId, null, 50) const res = await messageApi.list(convId, null, 50)
// //
const idx = conversations.value.findIndex(c => c.id === convId) const idx = conversations.value.findIndex(c => c.id === convId)
if (idx >= 0) { if (idx >= 0) {
conversations.value[idx].message_count = res.data.items.length conversations.value[idx].message_count = res.data.items.length
//
if (res.data.items.length > 0) {
const convRes = await conversationApi.get(convId)
if (convRes.data.title) {
conversations.value[idx].title = convRes.data.title
}
}
} }
} catch (_) {} } catch (_) {}
} }

View File

@ -297,7 +297,6 @@ watch(() => props.streaming, (streaming) => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
animation: spin 2s linear infinite;
} }
.streaming-text { .streaming-text {
@ -450,15 +449,6 @@ watch(() => props.streaming, (streaming) => {
background: var(--bg-hover); background: var(--bg-hover);
} }
.process-item.loading.tool_call .process-icon {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.process-content { .process-content {
padding: 12px; padding: 12px;
background: var(--bg-primary); background: var(--bg-primary);