diff --git a/backend/apps/chat/api/chat.py b/backend/apps/chat/api/chat.py index 7fe8b5d6..8d0c532d 100644 --- a/backend/apps/chat/api/chat.py +++ b/backend/apps/chat/api/chat.py @@ -98,6 +98,13 @@ def inner(): return await asyncio.to_thread(inner) +@router.get("/record/{chat_record_id}/usage", summary=f"{PLACEHOLDER_PREFIX}get_record_usage") +async def chat_record_usage(session: SessionDep, current_user: CurrentUser, chat_record_id: int): + def inner(): + return get_chat_log_history(session, chat_record_id, current_user, True) + + return await asyncio.to_thread(inner) + """ @router.post("/rename", response_model=str, summary=f"{PLACEHOLDER_PREFIX}rename_chat") @system_log(LogConfig( diff --git a/backend/apps/chat/curd/chat.py b/backend/apps/chat/curd/chat.py index dc5baaa0..7d9a5939 100644 --- a/backend/apps/chat/curd/chat.py +++ b/backend/apps/chat/curd/chat.py @@ -514,7 +514,7 @@ def format_record(record: ChatRecordResult): return _dict -def get_chat_log_history(session: SessionDep, chat_record_id: int, current_user: CurrentUser) -> ChatLogHistory: +def get_chat_log_history(session: SessionDep, chat_record_id: int, current_user: CurrentUser, without_steps: bool = False) -> ChatLogHistory: """ 获取ChatRecord的详细历史记录 @@ -522,6 +522,7 @@ def get_chat_log_history(session: SessionDep, chat_record_id: int, current_user: session: 数据库会话 chat_record_id: ChatRecord的ID current_user: 当前用户 + without_steps Returns: ChatLogHistory: 包含历史步骤和时间信息的对象 @@ -536,7 +537,8 @@ def get_chat_log_history(session: SessionDep, chat_record_id: int, current_user: # 2. 查询与该ChatRecord相关的所有ChatLog记录 chat_logs = session.query(ChatLog).filter( - ChatLog.pid == chat_record_id + ChatLog.pid == chat_record_id, + ChatLog.operate != OperationEnum.GENERATE_RECOMMENDED_QUESTIONS ).order_by(ChatLog.start_time).all() # 3. 计算总的时间和token信息 @@ -544,15 +546,6 @@ def get_chat_log_history(session: SessionDep, chat_record_id: int, current_user: steps = [] for log in chat_logs: - # 计算单条记录的耗时 - duration = None - if log.start_time and log.finish_time: - try: - time_diff = log.finish_time - log.start_time - duration = time_diff.total_seconds() - except Exception: - duration = None - # 计算单条记录的token消耗 log_tokens = 0 if log.token_usage is not None: @@ -567,16 +560,46 @@ def get_chat_log_history(session: SessionDep, chat_record_id: int, current_user: # 累加到总token消耗 total_tokens += log_tokens - # 创建ChatLogHistoryItem - history_item = ChatLogHistoryItem( - start_time=log.start_time, - finish_time=log.finish_time, - duration=duration, - total_tokens=log_tokens, - operate=log.operate, - local_operation=log.local_operation - ) - steps.append(history_item) + if not without_steps: + # 计算单条记录的耗时 + duration = None + if log.start_time and log.finish_time: + try: + time_diff = log.finish_time - log.start_time + duration = round(time_diff.total_seconds(), 2) + except Exception: + duration = None + + # 获取操作类型的枚举名称 + operate_name = None + if log.operate: + # 如果是OperationEnum枚举实例 + if isinstance(log.operate, OperationEnum): + operate_name = log.operate.name + # 如果是字符串,尝试从枚举值获取名称 + elif isinstance(log.operate, str): + try: + # 通过枚举值找到对应的枚举实例 + for enum_item in OperationEnum: + if enum_item.value == log.operate: + operate_name = enum_item.name + break + except Exception: + operate_name = log.operate + else: + operate_name = str(log.operate) + + # 创建ChatLogHistoryItem + history_item = ChatLogHistoryItem( + start_time=log.start_time, + finish_time=log.finish_time, + duration=duration, + total_tokens=log_tokens, + operate=operate_name, + local_operation=log.local_operation + ) + + steps.append(history_item) # 4. 计算总耗时(使用ChatRecord的时间) total_duration = None diff --git a/backend/apps/chat/models/chat_model.py b/backend/apps/chat/models/chat_model.py index 011218d5..defed2e3 100644 --- a/backend/apps/chat/models/chat_model.py +++ b/backend/apps/chat/models/chat_model.py @@ -193,7 +193,7 @@ class ChatLogHistoryItem(BaseModel): finish_time: Optional[datetime] = None duration: Optional[float] = None # 耗时字段(单位:秒) total_tokens: Optional[int] = None # token总消耗 - operate: Optional[OperationEnum] = None + operate: Optional[str] = None local_operation: Optional[bool] = False class ChatLogHistory(BaseModel): diff --git a/backend/apps/swagger/locales/en.json b/backend/apps/swagger/locales/en.json index 837e5c84..3f16d5ea 100644 --- a/backend/apps/swagger/locales/en.json +++ b/backend/apps/swagger/locales/en.json @@ -142,6 +142,7 @@ "get_chart_data": "Get Chart Data", "get_chart_predict_data": "Get Chart Prediction Data", "get_record_log": "Get Chart Record Log", + "get_record_usage": "Get Chart Record Token Usage & Duration", "rename_chat": "Rename Chat", "delete_chat": "Delete Chat", "start_chat": "Create Chat", diff --git a/backend/apps/swagger/locales/zh.json b/backend/apps/swagger/locales/zh.json index 59e3d139..d03dd2d9 100644 --- a/backend/apps/swagger/locales/zh.json +++ b/backend/apps/swagger/locales/zh.json @@ -142,6 +142,7 @@ "get_chart_data": "获取图表数据", "get_chart_predict_data": "获取图表预测数据", "get_record_log": "获取对话日志", + "get_record_usage": "获取对话Token使用量及耗时", "rename_chat": "重命名对话", "delete_chat": "删除对话", "start_chat": "创建对话", diff --git a/frontend/src/api/chat.ts b/frontend/src/api/chat.ts index beea7937..2b1d85f5 100644 --- a/frontend/src/api/chat.ts +++ b/frontend/src/api/chat.ts @@ -1,5 +1,8 @@ import { request } from '@/utils/request' import { getDate } from '@/utils/utils.ts' +import { i18n } from '@/i18n' + +const { t } = i18n.global export const questionApi = { pager: (pageNumber: number, pageSize: number) => @@ -320,7 +323,7 @@ export class ChatLogHistoryItem { this.finish_time = getDate(finish_time) this.duration = duration this.total_tokens = total_tokens - this.operate = operate + this.operate = t('chat.log.' + operate) this.local_operation = !!local_operation } } @@ -441,6 +444,9 @@ export const chatApi = { get_chart_log_history: (record_id?: number): Promise => { return request.get(`/chat/record/${record_id}/log`) }, + get_chart_usage: (record_id?: number): Promise => { + return request.get(`/chat/record/${record_id}/usage`) + }, startChat: (data: any): Promise => { return request.post('/chat/start', data) }, diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 5a2d060c..9b395f21 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -709,7 +709,22 @@ "exec-sql-err": "Execute SQL failed", "no_data": "No Data", "loading_data": "Loading ...", - "show_error_detail": "Show error info" + "show_error_detail": "Show error info", + "log": { + "GENERATE_SQL": "Generate SQL", + "GENERATE_CHART": "Generate Chart Structure", + "ANALYSIS": "Analyze", + "PREDICT_DATA": "Predict", + "GENERATE_SQL_WITH_PERMISSIONS": "Apply Row Permissions to SQL", + "CHOOSE_DATASOURCE": "Match Datasource", + "GENERATE_DYNAMIC_SQL": "Generate Dynamic SQL", + "CHOOSE_TABLE": "Match Data Table (Schema)", + "FILTER_TERMS": "Match Terms", + "FILTER_SQL_EXAMPLE": "Match SQL Examples", + "FILTER_CUSTOM_PROMPT": "Match Custom Prompts", + "EXECUTE_SQL": "Execute SQL", + "GENERATE_PICTURE": "Generate Picture" + } }, "about": { "title": "About", @@ -874,4 +889,4 @@ "to_doc": "View API", "trigger_limit": "Supports up to {0} API Keys" } -} \ No newline at end of file +} diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index a80cb53f..115d0ff5 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -709,7 +709,22 @@ "exec-sql-err": "SQL 실행 실패", "no_data": "데이터가 없습니다", "loading_data": "로딩 중 ...", - "show_error_detail": "구체적인 정보 보기" + "show_error_detail": "구체적인 정보 보기", + "log": { + "GENERATE_SQL": "SQL 생성", + "GENERATE_CHART": "차트 구조 생성", + "ANALYSIS": "분석", + "PREDICT_DATA": "예측", + "GENERATE_SQL_WITH_PERMISSIONS": "SQL에 행 권한 적용", + "CHOOSE_DATASOURCE": "데이터 소스 매칭", + "GENERATE_DYNAMIC_SQL": "동적 SQL 생성", + "CHOOSE_TABLE": "데이터 테이블 매칭 (스키마)", + "FILTER_TERMS": "용어 매칭", + "FILTER_SQL_EXAMPLE": "SQL 예시 매칭", + "FILTER_CUSTOM_PROMPT": "사용자 정의 프롬프트 매칭", + "EXECUTE_SQL": "SQL 실행", + "GENERATE_PICTURE": "이미지 생성" + } }, "about": { "title": "정보", @@ -874,4 +889,4 @@ "to_doc": "API 보기", "trigger_limit": "최대 {0}개의 API 키 생성 지원" } -} \ No newline at end of file +} diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index be93cd3e..7917701e 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -709,7 +709,22 @@ "exec-sql-err": "执行SQL失败", "no_data": "暂无数据", "loading_data": "加载中...", - "show_error_detail": "查看具体信息" + "show_error_detail": "查看具体信息", + "log": { + "GENERATE_SQL": "生成 SQL", + "GENERATE_CHART": "生成图表结构", + "ANALYSIS": "分析", + "PREDICT_DATA": "预测", + "GENERATE_SQL_WITH_PERMISSIONS": "拼接 SQL 行权限", + "CHOOSE_DATASOURCE": "匹配数据源", + "GENERATE_DYNAMIC_SQL": "生成动态 SQL", + "CHOOSE_TABLE": "匹配数据表 (Schema)", + "FILTER_TERMS": "匹配术语", + "FILTER_SQL_EXAMPLE": "匹配 SQL 示例", + "FILTER_CUSTOM_PROMPT": "匹配自定义提示词", + "EXECUTE_SQL": "执行 SQL", + "GENERATE_PICTURE": "生成图片" + } }, "about": { "title": "关于", @@ -874,4 +889,4 @@ "to_doc": "查看 API", "trigger_limit": "最多支持创建 {0} 个 API Key" } -} \ No newline at end of file +} diff --git a/frontend/src/views/chat/answer/AnalysisAnswer.vue b/frontend/src/views/chat/answer/AnalysisAnswer.vue index 8e3f495d..0b2dd3ca 100644 --- a/frontend/src/views/chat/answer/AnalysisAnswer.vue +++ b/frontend/src/views/chat/answer/AnalysisAnswer.vue @@ -158,7 +158,7 @@ const sendMessage = async () => { break case 'error': currentRecord.error = data.content - emits('error') + emits('error', currentRecord.id) break case 'analysis-result': analysis_answer += data.content diff --git a/frontend/src/views/chat/answer/ChartAnswer.vue b/frontend/src/views/chat/answer/ChartAnswer.vue index 68c74059..5bec0b63 100644 --- a/frontend/src/views/chat/answer/ChartAnswer.vue +++ b/frontend/src/views/chat/answer/ChartAnswer.vue @@ -185,7 +185,7 @@ const sendMessage = async () => { break case 'error': currentRecord.error = data.content - emits('error') + emits('error', currentRecord.id) break case 'sql-result': sql_answer += data.reasoning_content diff --git a/frontend/src/views/chat/answer/PredictAnswer.vue b/frontend/src/views/chat/answer/PredictAnswer.vue index 60c5aed2..89e7e232 100644 --- a/frontend/src/views/chat/answer/PredictAnswer.vue +++ b/frontend/src/views/chat/answer/PredictAnswer.vue @@ -162,7 +162,7 @@ const sendMessage = async () => { break case 'error': currentRecord.error = data.content - emits('error') + emits('error', currentRecord.id) break case 'predict-result': predict_answer += data.reasoning_content @@ -171,7 +171,7 @@ const sendMessage = async () => { _currentChat.value.records[index.value].predict_content = predict_content break case 'predict-failed': - emits('error') + emits('error', currentRecord.id) break case 'predict-success': //currentChat.value.records[_index].predict_data = data.content diff --git a/frontend/src/views/chat/index.vue b/frontend/src/views/chat/index.vue index 598f145b..47584c30 100644 --- a/frontend/src/views/chat/index.vue +++ b/frontend/src/views/chat/index.vue @@ -774,6 +774,7 @@ async function onChartAnswerFinish(id: number) { getRecommendQuestionsLoading.value = true loading.value = false isTyping.value = false + getRecordUsage(id) getRecommendQuestions(id) } @@ -781,10 +782,10 @@ const loadingOver = () => { getRecommendQuestionsLoading.value = false } -function onChartAnswerError() { +function onChartAnswerError(id: number) { loading.value = false isTyping.value = false - console.debug('onChartAnswerError') + getRecordUsage(id) } function onChatStop() { @@ -864,12 +865,13 @@ const analysisAnswerRef = ref() async function onAnalysisAnswerFinish(id: number) { loading.value = false isTyping.value = false - console.debug(id) + getRecordUsage(id) //await getRecommendQuestions(id) } -function onAnalysisAnswerError() { +function onAnalysisAnswerError(id: number) { loading.value = false isTyping.value = false + getRecordUsage(id) } function askAgain(message: ChatMessage) { @@ -931,17 +933,34 @@ async function clickAnalysis(id?: number) { return } +function getRecordUsage(recordId: any) { + chatApi.get_chart_usage(recordId).then((res) => { + const logHistory = chatApi.toChatLogHistory(res) + if (logHistory) { + currentChat.value.records.forEach((record) => { + if (record.id === recordId) { + record.duration = logHistory.duration + record.finish_time = logHistory.finish_time + record.total_tokens = logHistory.total_tokens + } + }) + } + }) +} + const predictAnswerRef = ref() async function onPredictAnswerFinish(id: number) { loading.value = false isTyping.value = false - console.debug('onPredictAnswerFinish: ', id) + // console.debug('onPredictAnswerFinish: ', id) + getRecordUsage(id) //await getRecommendQuestions(id) } -function onPredictAnswerError() { +function onPredictAnswerError(id: number) { loading.value = false isTyping.value = false + getRecordUsage(id) } async function clickPredict(id?: number) {