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,
|
"thinking_interval": None,
|
||||||
"disabled_tool_categories": [],
|
"disabled_tool_categories": [],
|
||||||
"default_run_mode": None,
|
"default_run_mode": None,
|
||||||
|
"auto_generate_title": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -108,6 +109,7 @@ def sanitize_personalization_payload(
|
|||||||
return _sanitize_short_field(base.get(key))
|
return _sanitize_short_field(base.get(key))
|
||||||
|
|
||||||
base["enabled"] = bool(data.get("enabled", base["enabled"]))
|
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["self_identify"] = _resolve_short_field("self_identify")
|
||||||
base["user_name"] = _resolve_short_field("user_name")
|
base["user_name"] = _resolve_short_field("user_name")
|
||||||
base["profession"] = _resolve_short_field("profession")
|
base["profession"] = _resolve_short_field("profession")
|
||||||
|
|||||||
@ -207,6 +207,20 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
||||||
<div class="behavior-field-header">
|
<div class="behavior-field-header">
|
||||||
<span class="field-title">思考频率</span>
|
<span class="field-title">思考频率</span>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ type RunMode = 'fast' | 'thinking' | 'deep';
|
|||||||
|
|
||||||
interface PersonalForm {
|
interface PersonalForm {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
auto_generate_title: boolean;
|
||||||
self_identify: string;
|
self_identify: string;
|
||||||
user_name: string;
|
user_name: string;
|
||||||
profession: string;
|
profession: string;
|
||||||
@ -51,6 +52,7 @@ const EXPERIMENT_STORAGE_KEY = 'agents_personalization_experiments';
|
|||||||
|
|
||||||
const defaultForm = (): PersonalForm => ({
|
const defaultForm = (): PersonalForm => ({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
auto_generate_title: true,
|
||||||
self_identify: '',
|
self_identify: '',
|
||||||
user_name: '',
|
user_name: '',
|
||||||
profession: '',
|
profession: '',
|
||||||
@ -169,6 +171,7 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
applyPersonalizationData(data: any) {
|
applyPersonalizationData(data: any) {
|
||||||
this.form = {
|
this.form = {
|
||||||
enabled: !!data.enabled,
|
enabled: !!data.enabled,
|
||||||
|
auto_generate_title: data.auto_generate_title !== false,
|
||||||
self_identify: data.self_identify || '',
|
self_identify: data.self_identify || '',
|
||||||
user_name: data.user_name || '',
|
user_name: data.user_name || '',
|
||||||
profession: data.profession || '',
|
profession: data.profession || '',
|
||||||
@ -282,7 +285,7 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
this.saving = false;
|
this.saving = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateField(payload: { key: keyof PersonalForm; value: string }) {
|
updateField(payload: { key: keyof PersonalForm; value: any }) {
|
||||||
if (!payload || !payload.key) {
|
if (!payload || !payload.key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -323,6 +323,25 @@ class ConversationManager:
|
|||||||
print(f"📝 创建新对话: {conversation_id} - {conversation_data['title']}")
|
print(f"📝 创建新对话: {conversation_id} - {conversation_data['title']}")
|
||||||
|
|
||||||
return conversation_id
|
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):
|
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 modules.usage_tracker import UsageTracker, QUOTA_DEFAULTS
|
||||||
from utils.tool_result_formatter import format_tool_result_for_context
|
from utils.tool_result_formatter import format_tool_result_for_context
|
||||||
from utils.conversation_manager import ConversationManager
|
from utils.conversation_manager import ConversationManager
|
||||||
|
from utils.api_client import DeepSeekClient
|
||||||
|
|
||||||
app = Flask(__name__, static_folder='static')
|
app = Flask(__name__, static_folder='static')
|
||||||
app.config['MAX_CONTENT_LENGTH'] = MAX_UPLOAD_SIZE
|
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_lock = threading.Lock()
|
||||||
_last_active_cache: Dict[str, float] = {}
|
_last_active_cache: Dict[str, float] = {}
|
||||||
_idle_reaper_started = False
|
_idle_reaper_started = False
|
||||||
|
TITLE_PROMPT = """### 任务:
|
||||||
|
生成一个简洁的、3-5个词的标题,并包含一个emoji,用于总结聊天内容。
|
||||||
|
|
||||||
|
### 指导原则:
|
||||||
|
- 标题应清晰代表主要话题
|
||||||
|
- 使用与话题相关且单个的emoji
|
||||||
|
- 保持简洁(3-5个词)
|
||||||
|
- 清晰度优先于创意
|
||||||
|
- 使用用户输入的语言回复
|
||||||
|
|
||||||
|
### 输出格式:
|
||||||
|
仅输出标题本身,不要附加解释。"""
|
||||||
|
|
||||||
|
|
||||||
def sanitize_filename_preserve_unicode(filename: str) -> str:
|
def sanitize_filename_preserve_unicode(filename: str) -> str:
|
||||||
@ -284,6 +297,64 @@ def start_background_jobs():
|
|||||||
_load_last_active_cache()
|
_load_last_active_cache()
|
||||||
socketio.start_background_task(idle_reaper_loop)
|
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]]):
|
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'):
|
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)
|
send_to_client(event_type, data)
|
||||||
|
|
||||||
# 传递客户端ID
|
# 传递客户端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')
|
@socketio.on('client_chunk_log')
|
||||||
@ -2783,14 +2854,14 @@ def get_current_conversation(terminal: WebTerminal, workspace: UserWorkspace, us
|
|||||||
"error": str(e)
|
"error": str(e)
|
||||||
}), 500
|
}), 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:
|
try:
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(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)
|
entry = stop_flags.get(client_sid)
|
||||||
if not isinstance(entry, dict):
|
if not isinstance(entry, dict):
|
||||||
@ -2865,7 +2936,7 @@ def detect_malformed_tool_call(text):
|
|||||||
|
|
||||||
return False
|
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统计版本"""
|
"""处理任务并发送消息 - 集成token统计版本"""
|
||||||
web_terminal = terminal
|
web_terminal = terminal
|
||||||
conversation_id = getattr(web_terminal.context_manager, "current_conversation_id", None)
|
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
|
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)
|
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调用前计算 ===
|
# === 移除:不在这里计算输入token,改为在每次API调用前计算 ===
|
||||||
|
|
||||||
# 构建上下文和消息(用于API调用)
|
# 构建上下文和消息(用于API调用)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user