Skip to content

Commit 180fd4b

Browse files
committed
feat: add chat log step display
#324 #742
1 parent bca72fc commit 180fd4b

File tree

8 files changed

+385
-8
lines changed

8 files changed

+385
-8
lines changed

backend/apps/chat/api/chat.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
list_chats, get_chat_with_records, create_chat, rename_chat, \
1515
delete_chat, get_chat_chart_data, get_chat_predict_data, get_chat_with_records_with_data, get_chat_record_by_id, \
1616
format_json_data, format_json_list_data, get_chart_config, list_recent_questions, get_chat as get_chat_exec, \
17-
rename_chat_with_user
17+
rename_chat_with_user, get_chat_log_history
1818
from apps.chat.models.chat_model import CreateChat, ChatRecord, RenameChat, ChatQuestion, AxisObj, QuickCommand, \
1919
ChatInfo, Chat, ChatFinishStep
2020
from apps.chat.task.llm import LLMService
@@ -91,6 +91,14 @@ def inner():
9191
return await asyncio.to_thread(inner)
9292

9393

94+
@router.get("/record/{chat_record_id}/log", summary=f"{PLACEHOLDER_PREFIX}get_record_log")
95+
async def chat_record_log(session: SessionDep, current_user: CurrentUser, chat_record_id: int):
96+
def inner():
97+
return get_chat_log_history(session, chat_record_id, current_user)
98+
99+
return await asyncio.to_thread(inner)
100+
101+
94102
""" @router.post("/rename", response_model=str, summary=f"{PLACEHOLDER_PREFIX}rename_chat")
95103
@system_log(LogConfig(
96104
operation_type=OperationType.UPDATE,

backend/apps/chat/curd/chat.py

Lines changed: 159 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from sqlalchemy.orm import aliased
99

1010
from apps.chat.models.chat_model import Chat, ChatRecord, CreateChat, ChatInfo, RenameChat, ChatQuestion, ChatLog, \
11-
TypeEnum, OperationEnum, ChatRecordResult
11+
TypeEnum, OperationEnum, ChatRecordResult, ChatLogHistory, ChatLogHistoryItem
1212
from apps.datasource.crud.recommended_problem import get_datasource_recommended_chart
1313
from apps.datasource.models.datasource import CoreDatasource
1414
from apps.db.constant import DB
@@ -345,11 +345,60 @@ def get_chat_with_records(session: SessionDep, chart_id: int, current_user: Curr
345345

346346
result = session.execute(stmt).all()
347347
record_list: list[ChatRecordResult] = []
348+
349+
# 批量获取所有ChatRecord的token消耗
350+
record_ids = [row.id for row in result]
351+
token_usage_map = {}
352+
353+
if record_ids:
354+
# 查询所有相关ChatLog的token_usage
355+
log_stmt = select(ChatLog.pid, ChatLog.token_usage).where(
356+
and_(
357+
ChatLog.pid.in_(record_ids),
358+
ChatLog.local_operation == False,
359+
ChatLog.token_usage.is_not(None) # 排除token_usage为空的记录
360+
)
361+
)
362+
log_results = session.execute(log_stmt).all()
363+
364+
# 按pid分组计算total_tokens总和
365+
for pid, token_usage in log_results:
366+
if pid and token_usage is not None:
367+
tokens_to_add = 0
368+
369+
if isinstance(token_usage, dict):
370+
# 处理字典类型: {"input_tokens": 961, "total_tokens": 1006, "output_tokens": 45}
371+
if token_usage: # 非空字典
372+
if "total_tokens" in token_usage:
373+
token_value = token_usage["total_tokens"]
374+
if isinstance(token_value, (int, float)):
375+
tokens_to_add = int(token_value)
376+
elif isinstance(token_usage, (int, float)):
377+
tokens_to_add = int(token_usage)
378+
if tokens_to_add > 0:
379+
if pid not in token_usage_map:
380+
token_usage_map[pid] = 0
381+
token_usage_map[pid] += tokens_to_add
382+
348383
for row in result:
384+
# 计算耗时
385+
duration = None
386+
if row.create_time and row.finish_time:
387+
try:
388+
time_diff = row.finish_time - row.create_time
389+
duration = time_diff.total_seconds() # 转换为秒
390+
except Exception:
391+
duration = None
392+
393+
# 获取token总消耗
394+
total_tokens = token_usage_map.get(row.id, 0)
395+
349396
if not with_data:
350397
record_list.append(
351398
ChatRecordResult(id=row.id, chat_id=row.chat_id, create_time=row.create_time,
352399
finish_time=row.finish_time,
400+
duration=duration,
401+
total_tokens=total_tokens,
353402
question=row.question, sql_answer=row.sql_answer, sql=row.sql,
354403
chart_answer=row.chart_answer, chart=row.chart,
355404
analysis=row.analysis, predict=row.predict,
@@ -367,6 +416,8 @@ def get_chat_with_records(session: SessionDep, chart_id: int, current_user: Curr
367416
record_list.append(
368417
ChatRecordResult(id=row.id, chat_id=row.chat_id, create_time=row.create_time,
369418
finish_time=row.finish_time,
419+
duration=duration,
420+
total_tokens=total_tokens,
370421
question=row.question, sql_answer=row.sql_answer, sql=row.sql,
371422
chart_answer=row.chart_answer, chart=row.chart,
372423
analysis=row.analysis, predict=row.predict,
@@ -437,6 +488,23 @@ def format_record(record: ChatRecordResult):
437488
_dict['sql'] = sqlparse.format(record.sql, reindent=True)
438489
except Exception:
439490
pass
491+
492+
# 格式化duration字段,保留2位小数
493+
if 'duration' in _dict and _dict['duration'] is not None:
494+
try:
495+
# 可以格式化为更易读的形式
496+
_dict['duration'] = round(_dict['duration'], 2) # 保留2位小数
497+
except Exception:
498+
pass
499+
500+
# 格式化total_tokens字段
501+
if 'total_tokens' in _dict and _dict['total_tokens'] is not None:
502+
try:
503+
# 确保是整数类型
504+
_dict['total_tokens'] = int(_dict['total_tokens']) if _dict['total_tokens'] else 0
505+
except Exception:
506+
_dict['total_tokens'] = 0
507+
440508
# 去除返回前端多余的字段
441509
_dict.pop('sql_reasoning_content', None)
442510
_dict.pop('chart_reasoning_content', None)
@@ -446,6 +514,90 @@ def format_record(record: ChatRecordResult):
446514
return _dict
447515

448516

517+
def get_chat_log_history(session: SessionDep, chat_record_id: int, current_user: CurrentUser) -> ChatLogHistory:
518+
"""
519+
获取ChatRecord的详细历史记录
520+
521+
Args:
522+
session: 数据库会话
523+
chat_record_id: ChatRecord的ID
524+
current_user: 当前用户
525+
526+
Returns:
527+
ChatLogHistory: 包含历史步骤和时间信息的对象
528+
"""
529+
# 1. 首先验证ChatRecord存在且属于当前用户
530+
chat_record = session.get(ChatRecord, chat_record_id)
531+
if not chat_record:
532+
raise Exception(f"ChatRecord with id {chat_record_id} not found")
533+
534+
if chat_record.create_by != current_user.id:
535+
raise Exception(f"ChatRecord with id {chat_record_id} not owned by the current user")
536+
537+
# 2. 查询与该ChatRecord相关的所有ChatLog记录
538+
chat_logs = session.query(ChatLog).filter(
539+
ChatLog.pid == chat_record_id
540+
).order_by(ChatLog.start_time).all()
541+
542+
# 3. 计算总的时间和token信息
543+
total_tokens = 0
544+
steps = []
545+
546+
for log in chat_logs:
547+
# 计算单条记录的耗时
548+
duration = None
549+
if log.start_time and log.finish_time:
550+
try:
551+
time_diff = log.finish_time - log.start_time
552+
duration = time_diff.total_seconds()
553+
except Exception:
554+
duration = None
555+
556+
# 计算单条记录的token消耗
557+
log_tokens = 0
558+
if log.token_usage is not None:
559+
if isinstance(log.token_usage, dict):
560+
if log.token_usage and "total_tokens" in log.token_usage:
561+
token_value = log.token_usage["total_tokens"]
562+
if isinstance(token_value, (int, float)):
563+
log_tokens = int(token_value)
564+
elif isinstance(log.token_usage, (int, float)):
565+
log_tokens = log.token_usage
566+
567+
# 累加到总token消耗
568+
total_tokens += log_tokens
569+
570+
# 创建ChatLogHistoryItem
571+
history_item = ChatLogHistoryItem(
572+
start_time=log.start_time,
573+
finish_time=log.finish_time,
574+
duration=duration,
575+
total_tokens=log_tokens,
576+
operate=log.operate,
577+
local_operation=log.local_operation
578+
)
579+
steps.append(history_item)
580+
581+
# 4. 计算总耗时(使用ChatRecord的时间)
582+
total_duration = None
583+
if chat_record.create_time and chat_record.finish_time:
584+
try:
585+
time_diff = chat_record.finish_time - chat_record.create_time
586+
total_duration = time_diff.total_seconds()
587+
except Exception:
588+
total_duration = None
589+
590+
# 5. 创建并返回ChatLogHistory对象
591+
chat_log_history = ChatLogHistory(
592+
start_time=chat_record.create_time, # 使用ChatRecord的create_time
593+
finish_time=chat_record.finish_time, # 使用ChatRecord的finish_time
594+
duration=total_duration,
595+
total_tokens=total_tokens,
596+
steps=steps
597+
)
598+
599+
return chat_log_history
600+
449601
def get_chat_brief_generate(session: SessionDep, chat_id: int):
450602
chat = get_chat(session=session, chat_id=chat_id)
451603
if chat is not None and chat.brief_generate is not None:
@@ -877,7 +1029,12 @@ def save_error_message(session: SessionDep, record_id: int, message: str) -> Cha
8771029

8781030
session.commit()
8791031

880-
# todo log error finish
1032+
# log error finish
1033+
stmt = update(ChatLog).where(and_(ChatLog.pid == record.id, ChatLog.finish_time.is_(None))).values(
1034+
finish_time=record.finish_time
1035+
)
1036+
session.execute(stmt)
1037+
session.commit()
8811038

8821039
return result
8831040

backend/apps/chat/models/chat_model.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ class ChatRecordResult(BaseModel):
155155
chart_reasoning_content: Optional[str] = None
156156
analysis_reasoning_content: Optional[str] = None
157157
predict_reasoning_content: Optional[str] = None
158+
duration: Optional[float] = None # 耗时字段(单位:秒)
159+
total_tokens: Optional[int] = None # token总消耗
158160

159161

160162
class CreateChat(BaseModel):
@@ -186,6 +188,22 @@ class ChatInfo(BaseModel):
186188
records: List[ChatRecord | dict] = []
187189

188190

191+
class ChatLogHistoryItem(BaseModel):
192+
start_time: Optional[datetime] = None
193+
finish_time: Optional[datetime] = None
194+
duration: Optional[float] = None # 耗时字段(单位:秒)
195+
total_tokens: Optional[int] = None # token总消耗
196+
operate: Optional[OperationEnum] = None
197+
local_operation: Optional[bool] = False
198+
199+
class ChatLogHistory(BaseModel):
200+
start_time: Optional[datetime] = None
201+
finish_time: Optional[datetime] = None
202+
duration: Optional[float] = None # 耗时字段(单位:秒)
203+
total_tokens: Optional[int] = None # token总消耗
204+
steps: List[ChatLogHistoryItem | dict] = []
205+
206+
189207
class AiModelQuestion(BaseModel):
190208
question: str = None
191209
ai_modal_id: int = None

backend/apps/swagger/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
"get_chat_with_data": "Get Chat Details (With Data)",
142142
"get_chart_data": "Get Chart Data",
143143
"get_chart_predict_data": "Get Chart Prediction Data",
144+
"get_record_log": "Get Chart Record Log",
144145
"rename_chat": "Rename Chat",
145146
"delete_chat": "Delete Chat",
146147
"start_chat": "Create Chat",

backend/apps/swagger/locales/zh.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
"get_chat_with_data": "获取对话详情(带数据)",
142142
"get_chart_data": "获取图表数据",
143143
"get_chart_predict_data": "获取图表预测数据",
144+
"get_record_log": "获取对话日志",
144145
"rename_chat": "重命名对话",
145146
"delete_chat": "删除对话",
146147
"start_chat": "创建对话",

0 commit comments

Comments
 (0)