feat: auto-generate chat titles with personalization toggle
This commit is contained in:
parent
7b735e252f
commit
5bdbfa138e
@ -34,6 +34,7 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = {
|
||||
"thinking_interval": None,
|
||||
"disabled_tool_categories": [],
|
||||
"default_run_mode": None,
|
||||
"auto_generate_title": True,
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
@ -108,6 +109,7 @@ def sanitize_personalization_payload(
|
||||
return _sanitize_short_field(base.get(key))
|
||||
|
||||
base["enabled"] = bool(data.get("enabled", base["enabled"]))
|
||||
base["auto_generate_title"] = bool(data.get("auto_generate_title", base["auto_generate_title"]))
|
||||
base["self_identify"] = _resolve_short_field("self_identify")
|
||||
base["user_name"] = _resolve_short_field("user_name")
|
||||
base["profession"] = _resolve_short_field("profession")
|
||||
|
||||
@ -207,6 +207,20 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="behavior-field">
|
||||
<div class="behavior-field-header">
|
||||
<span class="field-title">自动生成对话标题</span>
|
||||
<p class="field-desc">默认开启;关闭后标题将沿用首条消息。</p>
|
||||
</div>
|
||||
<label class="toggle-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="form.auto_generate_title"
|
||||
@change="personalization.updateField({ key: 'auto_generate_title', value: $event.target.checked })"
|
||||
/>
|
||||
<span>使用快速模型为新对话生成含 emoji 的简短标题</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="behavior-field">
|
||||
<div class="behavior-field-header">
|
||||
<span class="field-title">思考频率</span>
|
||||
|
||||
@ -4,6 +4,7 @@ type RunMode = 'fast' | 'thinking' | 'deep';
|
||||
|
||||
interface PersonalForm {
|
||||
enabled: boolean;
|
||||
auto_generate_title: boolean;
|
||||
self_identify: string;
|
||||
user_name: string;
|
||||
profession: string;
|
||||
@ -51,6 +52,7 @@ const EXPERIMENT_STORAGE_KEY = 'agents_personalization_experiments';
|
||||
|
||||
const defaultForm = (): PersonalForm => ({
|
||||
enabled: false,
|
||||
auto_generate_title: true,
|
||||
self_identify: '',
|
||||
user_name: '',
|
||||
profession: '',
|
||||
@ -169,6 +171,7 @@ export const usePersonalizationStore = defineStore('personalization', {
|
||||
applyPersonalizationData(data: any) {
|
||||
this.form = {
|
||||
enabled: !!data.enabled,
|
||||
auto_generate_title: data.auto_generate_title !== false,
|
||||
self_identify: data.self_identify || '',
|
||||
user_name: data.user_name || '',
|
||||
profession: data.profession || '',
|
||||
@ -282,7 +285,7 @@ export const usePersonalizationStore = defineStore('personalization', {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
updateField(payload: { key: keyof PersonalForm; value: string }) {
|
||||
updateField(payload: { key: keyof PersonalForm; value: any }) {
|
||||
if (!payload || !payload.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -323,6 +323,25 @@ class ConversationManager:
|
||||
print(f"📝 创建新对话: {conversation_id} - {conversation_data['title']}")
|
||||
|
||||
return conversation_id
|
||||
|
||||
def update_conversation_title(self, conversation_id: str, title: str) -> bool:
|
||||
"""更新对话标题并刷新索引。"""
|
||||
if not conversation_id or not title:
|
||||
return False
|
||||
try:
|
||||
data = self.load_conversation(conversation_id)
|
||||
if not data:
|
||||
return False
|
||||
data["title"] = title
|
||||
data["updated_at"] = datetime.now().isoformat()
|
||||
self._save_conversation_file(conversation_id, data)
|
||||
self._update_index(conversation_id, data)
|
||||
if self.current_conversation_id == conversation_id:
|
||||
self.current_conversation_title = title
|
||||
return True
|
||||
except Exception as exc:
|
||||
print(f"⚠️ 更新对话标题失败 {conversation_id}: {exc}")
|
||||
return False
|
||||
|
||||
def _save_conversation_file(self, conversation_id: str, data: Dict):
|
||||
"""保存对话文件"""
|
||||
|
||||
@ -100,6 +100,7 @@ from modules.user_container_manager import UserContainerManager
|
||||
from modules.usage_tracker import UsageTracker, QUOTA_DEFAULTS
|
||||
from utils.tool_result_formatter import format_tool_result_for_context
|
||||
from utils.conversation_manager import ConversationManager
|
||||
from utils.api_client import DeepSeekClient
|
||||
|
||||
app = Flask(__name__, static_folder='static')
|
||||
app.config['MAX_CONTENT_LENGTH'] = MAX_UPLOAD_SIZE
|
||||
@ -180,6 +181,18 @@ LAST_ACTIVE_FILE = Path(LOGS_DIR).expanduser().resolve() / "last_active.json"
|
||||
_last_active_lock = threading.Lock()
|
||||
_last_active_cache: Dict[str, float] = {}
|
||||
_idle_reaper_started = False
|
||||
TITLE_PROMPT = """### 任务:
|
||||
生成一个简洁的、3-5个词的标题,并包含一个emoji,用于总结聊天内容。
|
||||
|
||||
### 指导原则:
|
||||
- 标题应清晰代表主要话题
|
||||
- 使用与话题相关且单个的emoji
|
||||
- 保持简洁(3-5个词)
|
||||
- 清晰度优先于创意
|
||||
- 使用用户输入的语言回复
|
||||
|
||||
### 输出格式:
|
||||
仅输出标题本身,不要附加解释。"""
|
||||
|
||||
|
||||
def sanitize_filename_preserve_unicode(filename: str) -> str:
|
||||
@ -284,6 +297,64 @@ def start_background_jobs():
|
||||
_load_last_active_cache()
|
||||
socketio.start_background_task(idle_reaper_loop)
|
||||
|
||||
|
||||
async def _generate_title_async(user_message: str) -> Optional[str]:
|
||||
"""使用快速模型生成对话标题。"""
|
||||
if not user_message:
|
||||
return None
|
||||
client = DeepSeekClient(thinking_mode=False, web_mode=True)
|
||||
messages = [
|
||||
{"role": "system", "content": TITLE_PROMPT},
|
||||
{"role": "user", "content": user_message}
|
||||
]
|
||||
try:
|
||||
async for resp in client.chat(messages, tools=[], stream=False):
|
||||
try:
|
||||
content = resp.get("choices", [{}])[0].get("message", {}).get("content")
|
||||
if content:
|
||||
return " ".join(str(content).strip().split())
|
||||
except Exception:
|
||||
continue
|
||||
except Exception as exc:
|
||||
debug_log(f"[TitleGen] 生成标题异常: {exc}")
|
||||
return None
|
||||
|
||||
|
||||
def generate_conversation_title_background(web_terminal: WebTerminal, conversation_id: str, user_message: str, username: str):
|
||||
"""在后台生成对话标题并更新索引、推送给前端。"""
|
||||
if not conversation_id or not user_message:
|
||||
return
|
||||
|
||||
async def _runner():
|
||||
title = await _generate_title_async(user_message)
|
||||
if not title:
|
||||
return
|
||||
# 限长,避免标题过长
|
||||
safe_title = title[:80]
|
||||
ok = False
|
||||
try:
|
||||
ok = web_terminal.context_manager.conversation_manager.update_conversation_title(conversation_id, safe_title)
|
||||
except Exception as exc:
|
||||
debug_log(f"[TitleGen] 保存标题失败: {exc}")
|
||||
if not ok:
|
||||
return
|
||||
try:
|
||||
socketio.emit('conversation_changed', {
|
||||
'conversation_id': conversation_id,
|
||||
'title': safe_title
|
||||
}, room=f"user_{username}")
|
||||
socketio.emit('conversation_list_update', {
|
||||
'action': 'updated',
|
||||
'conversation_id': conversation_id
|
||||
}, room=f"user_{username}")
|
||||
except Exception as exc:
|
||||
debug_log(f"[TitleGen] 推送标题更新失败: {exc}")
|
||||
|
||||
try:
|
||||
asyncio.run(_runner())
|
||||
except Exception as exc:
|
||||
debug_log(f"[TitleGen] 任务执行失败: {exc}")
|
||||
|
||||
def cache_monitor_snapshot(execution_id: Optional[str], stage: str, snapshot: Optional[Dict[str, Any]]):
|
||||
"""缓存工具执行前/后的文件快照。"""
|
||||
if not execution_id or not snapshot or not snapshot.get('content'):
|
||||
@ -2300,7 +2371,7 @@ def handle_message(data):
|
||||
send_to_client(event_type, data)
|
||||
|
||||
# 传递客户端ID
|
||||
socketio.start_background_task(process_message_task, terminal, message, send_with_activity, client_sid)
|
||||
socketio.start_background_task(process_message_task, terminal, message, send_with_activity, client_sid, workspace, username)
|
||||
|
||||
|
||||
@socketio.on('client_chunk_log')
|
||||
@ -2783,14 +2854,14 @@ def get_current_conversation(terminal: WebTerminal, workspace: UserWorkspace, us
|
||||
"error": str(e)
|
||||
}), 500
|
||||
|
||||
def process_message_task(terminal: WebTerminal, message: str, sender, client_sid):
|
||||
def process_message_task(terminal: WebTerminal, message: str, sender, client_sid, workspace: UserWorkspace, username: str):
|
||||
"""在后台处理消息任务"""
|
||||
try:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# 创建可取消的任务
|
||||
task = loop.create_task(handle_task_with_sender(terminal, message, sender, client_sid))
|
||||
task = loop.create_task(handle_task_with_sender(terminal, workspace, message, sender, client_sid, username))
|
||||
|
||||
entry = stop_flags.get(client_sid)
|
||||
if not isinstance(entry, dict):
|
||||
@ -2865,7 +2936,7 @@ def detect_malformed_tool_call(text):
|
||||
|
||||
return False
|
||||
|
||||
async def handle_task_with_sender(terminal: WebTerminal, message, sender, client_sid):
|
||||
async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspace, message, sender, client_sid, username: str):
|
||||
"""处理任务并发送消息 - 集成token统计版本"""
|
||||
web_terminal = terminal
|
||||
conversation_id = getattr(web_terminal.context_manager, "current_conversation_id", None)
|
||||
@ -2879,8 +2950,26 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
|
||||
state["suppress_next"] = False
|
||||
|
||||
# 添加到对话历史
|
||||
history_len_before = len(getattr(web_terminal.context_manager, "conversation_history", []) or [])
|
||||
is_first_user_message = history_len_before == 0
|
||||
web_terminal.context_manager.add_conversation("user", message)
|
||||
|
||||
if is_first_user_message and getattr(web_terminal, "context_manager", None):
|
||||
try:
|
||||
personal_config = load_personalization_config(workspace.data_dir)
|
||||
except Exception:
|
||||
personal_config = {}
|
||||
auto_title_enabled = personal_config.get("auto_generate_title", True)
|
||||
if auto_title_enabled:
|
||||
conv_id = getattr(web_terminal.context_manager, "current_conversation_id", None)
|
||||
socketio.start_background_task(
|
||||
generate_conversation_title_background,
|
||||
web_terminal,
|
||||
conv_id,
|
||||
message,
|
||||
username
|
||||
)
|
||||
|
||||
# === 移除:不在这里计算输入token,改为在每次API调用前计算 ===
|
||||
|
||||
# 构建上下文和消息(用于API调用)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user