1347 lines
58 KiB
Python
1347 lines
58 KiB
Python
import sys
|
|
import os
|
|
|
|
# 修复Conda环境中文路径导致的Tcl/Tk初始化问题
|
|
if sys.platform == 'win32':
|
|
# 直接设置TCL/TK路径,避免中文路径编码问题
|
|
env_prefix = os.path.join(os.path.dirname(sys.executable), '..', '..', 'Library', 'lib')
|
|
env_prefix = os.path.normpath(env_prefix)
|
|
tcl_dir = os.path.join(env_prefix, 'tcl8.6')
|
|
tk_dir = os.path.join(env_prefix, 'tk8.6')
|
|
if os.path.isdir(tcl_dir):
|
|
os.environ['TCL_LIBRARY'] = tcl_dir
|
|
if os.path.isdir(tk_dir):
|
|
os.environ['TK_LIBRARY'] = tk_dir
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, filedialog
|
|
from datetime import datetime, timedelta
|
|
import requests
|
|
import json
|
|
import os
|
|
from openpyxl import Workbook
|
|
import anthropic
|
|
from difflib import SequenceMatcher
|
|
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
|
import threading
|
|
|
|
class UdeskLogViewer:
|
|
def __init__(self, root):
|
|
self.root = root
|
|
|
|
self.config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.json')
|
|
self.load_config()
|
|
|
|
win_cfg = self.config['window']
|
|
self.root.title("智能客服节点日志查看器")
|
|
self.root.geometry(f"{win_cfg['width']}x{win_cfg['height']}")
|
|
self.root.minsize(win_cfg['min_width'], win_cfg['min_height'])
|
|
|
|
self.cookies = self.config.get('cookies', {})
|
|
if not self.cookies:
|
|
self.cookies = {
|
|
'ut_user_id': 'null',
|
|
'ut_global_id': '%22ca426419-74d7-4dc8-bc5c-c866b096b297%22',
|
|
'_gcl_au': '1.1.505382516.1778740165',
|
|
'_ga': 'GA1.1.968517445.1778741596',
|
|
'sensorsdata2015jssdkcross': '%7B%22distinct_id%22%3A%2219e2542d1f1efd-0b7accb742cea4-4c657b58-2073600-19e2542d1f21d61%22%2C%22%24device_id%22%3A%2219e2542d1f1efd-0b7accb742cea4-4c657b58-2073600-19e2542d1f21d61%22%2C%22props%22%3A%7B%7D%7D',
|
|
'Qs_lvt_102458': '1778741596%2C1779092047',
|
|
'Hm_lvt_85cdbdd6ba7f014cd503e9f1cd5e5ba0': '1778741596,1779092047',
|
|
'Qs_pv_102458': '4477521906714103000%2C2213764348932670000%2C1655296003018746400%2C2955048170989231600%2C930806901093578800',
|
|
'_ga_WPQK651LHJ': 'GS2.1.s1779095976$o3$g0$t1779095976$j60$l0$h0',
|
|
}
|
|
token = self.config.get('api_token', '')
|
|
self.headers = {
|
|
'Accept': '*/*',
|
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
|
|
'Connection': 'keep-alive',
|
|
'Referer': f'https://agent.udesk.cn/app/{self.config["app_id"]}/logs',
|
|
'Sec-Fetch-Dest': 'empty',
|
|
'Sec-Fetch-Mode': 'cors',
|
|
'Sec-Fetch-Site': 'same-origin',
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0',
|
|
'authorization': f'Bearer {token}' if token else '',
|
|
'content-type': 'application/json',
|
|
'sec-ch-ua': '"Chromium";v="148", "Microsoft Edge";v="148", "Not/A)Brand";v="99"',
|
|
'sec-ch-ua-mobile': '?0',
|
|
'sec-ch-ua-platform': '"Windows"',
|
|
}
|
|
|
|
self.app_id = self.config['app_id']
|
|
|
|
self.records = []
|
|
self.logs = []
|
|
self.audit_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'audit_cache.json')
|
|
|
|
self.review_results = {}
|
|
self.test_cases = {}
|
|
self.init_llm_client()
|
|
|
|
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
|
self.create_widgets()
|
|
|
|
def load_config(self):
|
|
self.default_config = {
|
|
'app_id': 'a46947ea-c1bf-4884-b6ba-9d7be32e18c4',
|
|
'api_token': '',
|
|
'cookies': {},
|
|
'key_node_titles': ['大模型', '单次反思', '大模型二次生成'],
|
|
'window': {
|
|
'width': 1400,
|
|
'height': 800,
|
|
'min_width': 1200,
|
|
'min_height': 600
|
|
},
|
|
'default_date_range': {'days_before': 1},
|
|
'columns': {
|
|
'index': 50,
|
|
'chat_id': 120,
|
|
'title': 100,
|
|
'time': 120,
|
|
'query': 150,
|
|
'node_output': 150,
|
|
'node_audit': 100,
|
|
'answer': 150
|
|
},
|
|
'excel_export': {'default_dir': 'desktop'}
|
|
}
|
|
|
|
if os.path.exists(self.config_file):
|
|
try:
|
|
with open(self.config_file, 'r', encoding='utf-8') as f:
|
|
config = json.load(f)
|
|
self.config = {**self.default_config, **config}
|
|
except Exception:
|
|
self.config = self.default_config.copy()
|
|
else:
|
|
self.config = self.default_config.copy()
|
|
|
|
self.KEY_NODE_TITLES = self.config['key_node_titles']
|
|
|
|
def save_config(self):
|
|
try:
|
|
with open(self.config_file, 'w', encoding='utf-8') as f:
|
|
json.dump(self.config, f, ensure_ascii=False, indent=2)
|
|
except Exception as e:
|
|
messagebox.showerror("错误", f"保存配置失败: {str(e)}")
|
|
|
|
def show_config_dialog(self):
|
|
dialog = tk.Toplevel(self.root)
|
|
dialog.title("系统配置")
|
|
dialog.geometry("600x500")
|
|
dialog.resizable(True, True)
|
|
|
|
notebook = ttk.Notebook(dialog)
|
|
notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
|
|
basic_frame = ttk.Frame(notebook, padding=10)
|
|
notebook.add(basic_frame, text="基础配置")
|
|
|
|
node_frame = ttk.Frame(notebook, padding=10)
|
|
notebook.add(node_frame, text="关键节点")
|
|
|
|
self.config_entries = {}
|
|
|
|
row = 0
|
|
ttk.Label(basic_frame, text="Curl命令(粘贴后点击解析):").grid(row=row, column=0, sticky="w", pady=5, columnspan=2)
|
|
row += 1
|
|
curl_text = tk.Text(basic_frame, width=80, height=8)
|
|
curl_text.grid(row=row, column=0, sticky="w", pady=5, columnspan=2)
|
|
self.config_entries['curl'] = curl_text
|
|
row += 1
|
|
|
|
def parse_curl_cmd():
|
|
curl_cmd = curl_text.get("1.0", tk.END)
|
|
if curl_cmd.strip():
|
|
parsed = self.parse_curl(curl_cmd)
|
|
if parsed['app_id']:
|
|
self.config_entries['app_id'].delete(0, tk.END)
|
|
self.config_entries['app_id'].insert(0, parsed['app_id'])
|
|
self.config['app_id'] = parsed['app_id']
|
|
if parsed['token']:
|
|
self.config_entries['api_token'].delete(0, tk.END)
|
|
self.config_entries['api_token'].insert(0, parsed['token'])
|
|
self.config['api_token'] = parsed['token']
|
|
self.headers['authorization'] = f'Bearer {parsed["token"]}'
|
|
if parsed['cookies']:
|
|
self.config['cookies'] = parsed['cookies']
|
|
self.cookies = parsed['cookies']
|
|
messagebox.showinfo("成功", "Curl解析完成!配置已立即生效,可直接获取数据。")
|
|
|
|
ttk.Button(basic_frame, text="解析Curl", command=parse_curl_cmd).grid(row=row, column=0, sticky="w", pady=5)
|
|
row += 1
|
|
|
|
ttk.Label(basic_frame, text="App ID:").grid(row=row, column=0, sticky="w", pady=5)
|
|
app_id_entry = ttk.Entry(basic_frame, width=50)
|
|
app_id_entry.insert(0, self.config['app_id'])
|
|
app_id_entry.grid(row=row, column=1, sticky="w", pady=5)
|
|
self.config_entries['app_id'] = app_id_entry
|
|
row += 1
|
|
|
|
ttk.Label(basic_frame, text="API Token:").grid(row=row, column=0, sticky="w", pady=5)
|
|
token_entry = ttk.Entry(basic_frame, width=80)
|
|
token_entry.insert(0, self.config.get('api_token', ''))
|
|
token_entry.grid(row=row, column=1, sticky="w", pady=5)
|
|
self.config_entries['api_token'] = token_entry
|
|
row += 1
|
|
|
|
ttk.Label(basic_frame, text="窗口宽度:").grid(row=row, column=0, sticky="w", pady=5)
|
|
win_width_entry = ttk.Entry(basic_frame, width=10)
|
|
win_width_entry.insert(0, str(self.config['window']['width']))
|
|
win_width_entry.grid(row=row, column=1, sticky="w", pady=5)
|
|
self.config_entries['window_width'] = win_width_entry
|
|
row += 1
|
|
|
|
ttk.Label(basic_frame, text="窗口高度:").grid(row=row, column=0, sticky="w", pady=5)
|
|
win_height_entry = ttk.Entry(basic_frame, width=10)
|
|
win_height_entry.insert(0, str(self.config['window']['height']))
|
|
win_height_entry.grid(row=row, column=1, sticky="w", pady=5)
|
|
self.config_entries['window_height'] = win_height_entry
|
|
row += 1
|
|
|
|
ttk.Label(basic_frame, text="默认日期范围(天):").grid(row=row, column=0, sticky="w", pady=5)
|
|
days_entry = ttk.Entry(basic_frame, width=10)
|
|
days_entry.insert(0, str(self.config['default_date_range']['days_before']))
|
|
days_entry.grid(row=row, column=1, sticky="w", pady=5)
|
|
self.config_entries['days_before'] = days_entry
|
|
row += 1
|
|
|
|
ttk.Label(basic_frame, text="序号列宽度:").grid(row=row, column=0, sticky="w", pady=5)
|
|
idx_entry = ttk.Entry(basic_frame, width=10)
|
|
idx_entry.insert(0, str(self.config['columns']['index']))
|
|
idx_entry.grid(row=row, column=1, sticky="w", pady=5)
|
|
self.config_entries['col_index'] = idx_entry
|
|
row += 1
|
|
|
|
ttk.Label(basic_frame, text="节点输出列宽度:").grid(row=row, column=0, sticky="w", pady=5)
|
|
output_entry = ttk.Entry(basic_frame, width=10)
|
|
output_entry.insert(0, str(self.config['columns']['node_output']))
|
|
output_entry.grid(row=row, column=1, sticky="w", pady=5)
|
|
self.config_entries['col_output'] = output_entry
|
|
row += 1
|
|
|
|
ttk.Label(basic_frame, text="审核列宽度:").grid(row=row, column=0, sticky="w", pady=5)
|
|
audit_entry = ttk.Entry(basic_frame, width=10)
|
|
audit_entry.insert(0, str(self.config['columns']['node_audit']))
|
|
audit_entry.grid(row=row, column=1, sticky="w", pady=5)
|
|
self.config_entries['col_audit'] = audit_entry
|
|
|
|
ttk.Label(node_frame, text="关键节点列表(每行一个节点名称):").pack(anchor="w", pady=5)
|
|
self.node_text = tk.Text(node_frame, height=10, width=60)
|
|
self.node_text.pack(fill=tk.BOTH, expand=True, pady=5)
|
|
for title in self.KEY_NODE_TITLES:
|
|
self.node_text.insert(tk.END, title + "\n")
|
|
|
|
def apply_config():
|
|
try:
|
|
self.config['app_id'] = self.config_entries['app_id'].get().strip()
|
|
self.config['api_token'] = self.config_entries['api_token'].get().strip()
|
|
if not self.config.get('cookies'):
|
|
self.config['cookies'] = {}
|
|
self.config['window']['width'] = int(self.config_entries['window_width'].get())
|
|
self.config['window']['height'] = int(self.config_entries['window_height'].get())
|
|
self.config['default_date_range']['days_before'] = int(self.config_entries['days_before'].get())
|
|
self.config['columns']['index'] = int(self.config_entries['col_index'].get())
|
|
self.config['columns']['node_output'] = int(self.config_entries['col_output'].get())
|
|
self.config['columns']['node_audit'] = int(self.config_entries['col_audit'].get())
|
|
|
|
node_text_content = self.node_text.get(1.0, tk.END).strip()
|
|
if node_text_content:
|
|
self.config['key_node_titles'] = [line.strip() for line in node_text_content.split('\n') if line.strip()]
|
|
|
|
self.save_config()
|
|
messagebox.showinfo("成功", "配置已保存!部分配置需要重启程序才能生效。")
|
|
dialog.destroy()
|
|
except ValueError as e:
|
|
messagebox.showerror("错误", f"配置值格式错误: {str(e)}")
|
|
except Exception as e:
|
|
messagebox.showerror("错误", f"保存配置失败: {str(e)}")
|
|
|
|
def reset_config():
|
|
self.config = self.default_config.copy()
|
|
self.save_config()
|
|
messagebox.showinfo("成功", "已恢复默认配置,需要重启程序生效。")
|
|
dialog.destroy()
|
|
|
|
button_frame = ttk.Frame(dialog)
|
|
button_frame.pack(fill=tk.X, padx=10, pady=10)
|
|
|
|
ttk.Button(button_frame, text="应用", command=apply_config).pack(side=tk.RIGHT, padx=5)
|
|
ttk.Button(button_frame, text="重置默认", command=reset_config).pack(side=tk.RIGHT, padx=5)
|
|
ttk.Button(button_frame, text="关闭", command=dialog.destroy).pack(side=tk.RIGHT, padx=5)
|
|
|
|
dialog.transient(self.root)
|
|
dialog.grab_set()
|
|
self.root.wait_window(dialog)
|
|
|
|
def parse_curl(self, curl_cmd):
|
|
result = {
|
|
'url': '',
|
|
'headers': {},
|
|
'cookies': {},
|
|
'app_id': '',
|
|
'token': ''
|
|
}
|
|
|
|
import re
|
|
|
|
lines = curl_cmd.strip().split('\n')
|
|
for line in lines:
|
|
line = line.strip()
|
|
|
|
if line.startswith("curl '"):
|
|
url = line[6:-1].strip()
|
|
result['url'] = url
|
|
if 'apps/' in url:
|
|
parts = url.split('apps/')
|
|
if len(parts) > 1:
|
|
app_id_part = parts[1].split('/')[0]
|
|
result['app_id'] = app_id_part
|
|
|
|
elif line.startswith("-H '") and "authorization" in line.lower():
|
|
auth_match = re.search(r"authorization:\s*Bearer\s+([^\s'\"]+)", line, re.IGNORECASE)
|
|
if auth_match:
|
|
result['token'] = auth_match.group(1)
|
|
|
|
elif line.startswith("-H '") and "referer" in line.lower():
|
|
referer_match = re.search(r"referer:\s*['\"]([^'\"]+)['\"]", line, re.IGNORECASE)
|
|
if referer_match:
|
|
referer_value = referer_match.group(1)
|
|
if 'apps/' in referer_value:
|
|
parts = referer_value.split('apps/')
|
|
if len(parts) > 1:
|
|
app_id_part = parts[1].split('/')[0]
|
|
if app_id_part:
|
|
result['app_id'] = app_id_part
|
|
|
|
elif line.startswith("-b '"):
|
|
cookies_str = line[4:-1]
|
|
cookie_pairs = cookies_str.split('; ')
|
|
for pair in cookie_pairs:
|
|
if '=' in pair:
|
|
key, value = pair.split('=', 1)
|
|
result['cookies'][key.strip()] = value.strip()
|
|
|
|
return result
|
|
|
|
def init_llm_client(self):
|
|
api_key = "sk-cp-ayedGY_WYs9N0n2hYlAhbYYAYodr7ym7a1y8DgdyCcgx439ONVJzIgZmaR7JmB5bh4iA5ZiLlFy6dOLpHSLtmG8G5WH4EKLDLZXM9gbwAupxZUuqIAUnUEk"
|
|
try:
|
|
self.llm_client = anthropic.Anthropic(
|
|
api_key=api_key,
|
|
base_url="https://api.minimaxi.com/anthropic",
|
|
timeout=60.0
|
|
)
|
|
self.llm_available = True
|
|
except Exception:
|
|
self.llm_available = False
|
|
|
|
def load_test_cases(self, excel_path):
|
|
try:
|
|
import pandas as pd
|
|
df = pd.read_excel(excel_path)
|
|
self.test_cases = {}
|
|
for _, row in df.iterrows():
|
|
question = str(row['提问问题']).strip()
|
|
answer = str(row['答案']).strip()
|
|
if question:
|
|
self.test_cases[question] = answer
|
|
return True
|
|
except Exception as e:
|
|
messagebox.showerror("错误", f"加载测试用例失败: {str(e)}")
|
|
return False
|
|
|
|
def match_question(self, user_query, threshold=0.7):
|
|
best_match = None
|
|
best_score = 0
|
|
for std_question in self.test_cases.keys():
|
|
score = SequenceMatcher(None, user_query, std_question).ratio()
|
|
if score > best_score and score >= threshold:
|
|
best_score = score
|
|
best_match = std_question
|
|
return best_match, best_score
|
|
|
|
def evaluate_consistency(self, generated_answer, standard_answer):
|
|
system_prompt = """
|
|
你是一个专业的答案一致性评审助手。请按照以下标准评判生成答案与标准答案的一致性:
|
|
|
|
一致性评分标准:
|
|
- 10分:完全一致,内容、逻辑、步骤完全相同
|
|
- 8-9分:基本一致,核心内容相同,表述略有差异
|
|
- 6-7分:部分一致,核心思路相同,但有遗漏或错误步骤
|
|
- 4-5分:不太一致,只有部分内容相关
|
|
- 0-3分:不一致,内容无关或错误
|
|
|
|
请输出JSON格式,包含:
|
|
- score: 0-10的整数分数
|
|
- confidence: 0-100的整数置信度
|
|
- reason: 简短的评审理由(不超过100字)
|
|
"""
|
|
|
|
user_prompt = f"""
|
|
【生成答案】
|
|
{generated_answer}
|
|
|
|
【标准答案】
|
|
{standard_answer}
|
|
|
|
请根据上述标准进行评审,输出JSON格式结果。
|
|
"""
|
|
|
|
try:
|
|
message = self.llm_client.messages.create(
|
|
model="MiniMax-M2.7",
|
|
max_tokens=500,
|
|
system=system_prompt,
|
|
messages=[
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": user_prompt}
|
|
]
|
|
}
|
|
]
|
|
)
|
|
|
|
response_text = ""
|
|
for block in message.content:
|
|
if block.type == "text":
|
|
response_text += block.text
|
|
|
|
response_text = response_text.strip()
|
|
if response_text.startswith("```json"):
|
|
response_text = response_text[7:]
|
|
if response_text.endswith("```"):
|
|
response_text = response_text[:-3]
|
|
response_text = response_text.strip()
|
|
|
|
result = json.loads(response_text)
|
|
return result
|
|
except json.JSONDecodeError:
|
|
return {"score": 0, "confidence": 50, "reason": f"解析失败"}
|
|
except Exception as e:
|
|
return {"score": 0, "confidence": 0, "reason": f"API调用失败: {str(e)[:30]}"}
|
|
|
|
def show_review_dialog(self):
|
|
if not self.llm_available:
|
|
messagebox.showerror("错误", "大模型服务不可用")
|
|
return
|
|
|
|
dialog = tk.Toplevel(self.root)
|
|
dialog.title("大模型评测")
|
|
dialog.geometry("500x400")
|
|
|
|
ttk.Label(dialog, text="选择测试用例文件:").pack(pady=5)
|
|
excel_path = tk.StringVar()
|
|
ttk.Entry(dialog, textvariable=excel_path, width=50).pack(pady=5)
|
|
ttk.Button(dialog, text="浏览", command=lambda: excel_path.set(filedialog.askopenfilename(filetypes=[("Excel文件", "*.xlsx")]))).pack(pady=5)
|
|
|
|
ttk.Label(dialog, text="选择要评测的节点(可多选):").pack(pady=5)
|
|
|
|
selected_nodes = []
|
|
all_nodes = set()
|
|
for rec in self.records:
|
|
all_nodes.update(rec.get('key_outputs', {}).keys())
|
|
all_nodes.add('最终回答')
|
|
|
|
node_vars = {}
|
|
node_frame = ttk.Frame(dialog)
|
|
node_frame.pack(pady=5, fill=tk.X, padx=10)
|
|
|
|
for node in sorted(all_nodes):
|
|
var = tk.BooleanVar()
|
|
node_vars[node] = var
|
|
ttk.Checkbutton(node_frame, text=node, variable=var).pack(anchor='w')
|
|
|
|
def start_review():
|
|
if not excel_path.get():
|
|
messagebox.showwarning("提示", "请选择测试用例文件")
|
|
return
|
|
|
|
selected = [node for node, var in node_vars.items() if var.get()]
|
|
if not selected:
|
|
messagebox.showwarning("提示", "请至少选择一个节点")
|
|
return
|
|
|
|
if not self.load_test_cases(excel_path.get()):
|
|
return
|
|
|
|
dialog.destroy()
|
|
self.run_review(selected)
|
|
|
|
ttk.Button(dialog, text="开始评测", command=start_review).pack(pady=10)
|
|
ttk.Button(dialog, text="取消", command=dialog.destroy).pack(pady=5)
|
|
|
|
dialog.transient(self.root)
|
|
dialog.grab_set()
|
|
self.root.wait_window(dialog)
|
|
|
|
def run_review(self, selected_nodes):
|
|
self.update_status("正在进行大模型评测...")
|
|
self.review_results = {}
|
|
|
|
def review_thread():
|
|
total = len(self.records)
|
|
processed = 0
|
|
|
|
for idx, rec in enumerate(self.records):
|
|
user_query = rec.get('user_query', '').strip()
|
|
matched_question, match_score = self.match_question(user_query)
|
|
|
|
if matched_question and match_score >= 0.7:
|
|
standard_answer = self.test_cases[matched_question]
|
|
key = f"{rec['chat_log_id']}_{rec['wf_run_id']}"
|
|
self.review_results[key] = {
|
|
'matched_question': matched_question,
|
|
'match_score': match_score,
|
|
'standard_answer': standard_answer,
|
|
'node_results': {}
|
|
}
|
|
|
|
for node_name in selected_nodes:
|
|
if node_name == '最终回答':
|
|
generated_answer = rec.get('final_answer', '')
|
|
else:
|
|
generated_answer = rec.get('key_outputs', {}).get(node_name, '')
|
|
|
|
if generated_answer.strip():
|
|
result = self.evaluate_consistency(generated_answer, standard_answer)
|
|
self.review_results[key]['node_results'][node_name] = result
|
|
|
|
processed += 1
|
|
self.update_status(f"评测进度: {processed}/{total}")
|
|
|
|
self.update_status("大模型评测完成!")
|
|
messagebox.showinfo("成功", "大模型评测完成!结果将在导出Excel时包含")
|
|
|
|
thread = threading.Thread(target=review_thread)
|
|
thread.start()
|
|
|
|
def create_widgets(self):
|
|
top_frame = ttk.Frame(self.root, padding="10")
|
|
top_frame.pack(fill=tk.X, expand=False)
|
|
|
|
last_date_range = self.config.get('last_date_range', {})
|
|
start_date_val = last_date_range.get('start_date', '')
|
|
end_date_val = last_date_range.get('end_date', '')
|
|
|
|
if not start_date_val or not end_date_val:
|
|
days_before = self.config['default_date_range'].get('days_before', 1)
|
|
start_date_val = (datetime.now() - timedelta(days=days_before)).strftime("%Y-%m-%d")
|
|
end_date_val = datetime.now().strftime("%Y-%m-%d")
|
|
|
|
ttk.Label(top_frame, text="开始时间:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
|
self.start_date = ttk.Entry(top_frame, width=12)
|
|
self.start_date.insert(0, start_date_val)
|
|
self.start_date.grid(row=0, column=1, padx=5, pady=5)
|
|
|
|
ttk.Label(top_frame, text="结束时间:").grid(row=0, column=2, padx=5, pady=5, sticky="w")
|
|
self.end_date = ttk.Entry(top_frame, width=12)
|
|
self.end_date.insert(0, end_date_val)
|
|
self.end_date.grid(row=0, column=3, padx=5, pady=5)
|
|
|
|
self.fetch_btn = ttk.Button(top_frame, text="获取数据", command=self.start_fetch)
|
|
self.fetch_btn.grid(row=0, column=4, padx=5, pady=5)
|
|
|
|
self.export_btn = ttk.Button(top_frame, text="导出Excel", command=self.export_excel, state=tk.DISABLED)
|
|
self.export_btn.grid(row=0, column=5, padx=5, pady=5)
|
|
|
|
self.review_btn = ttk.Button(top_frame, text="大模型评测", command=self.show_review_dialog, state=tk.DISABLED)
|
|
self.review_btn.grid(row=0, column=6, padx=5, pady=5)
|
|
|
|
self.config_btn = ttk.Button(top_frame, text="配置", command=self.show_config_dialog)
|
|
self.config_btn.grid(row=0, column=7, padx=5, pady=5)
|
|
|
|
self.status_label = ttk.Label(top_frame, text="状态: 就绪")
|
|
self.status_label.grid(row=1, column=0, columnspan=6, padx=5, pady=5, sticky="w")
|
|
|
|
self.notebook = ttk.Notebook(self.root)
|
|
self.notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
|
|
self.audit_frame = ttk.Frame(self.notebook)
|
|
self.notebook.add(self.audit_frame, text="关键节点审核")
|
|
|
|
self.log_frame = ttk.Frame(self.notebook)
|
|
self.notebook.add(self.log_frame, text="全流程日志")
|
|
|
|
self.create_audit_tree()
|
|
self.create_log_tree()
|
|
|
|
def create_audit_tree(self):
|
|
self.audit_columns = ["index", "chat_id", "title", "time", "query"]
|
|
|
|
for title in self.KEY_NODE_TITLES:
|
|
self.audit_columns.append(f"node_{title}")
|
|
self.audit_columns.append(f"audit_{title}")
|
|
|
|
self.audit_columns.append("answer")
|
|
|
|
self.audit_tree = ttk.Treeview(self.audit_frame, columns=self.audit_columns, show="headings")
|
|
|
|
self.audit_tree.heading("index", text="序号")
|
|
self.audit_tree.heading("chat_id", text="对话ID")
|
|
self.audit_tree.heading("title", text="会话标题")
|
|
self.audit_tree.heading("time", text="时间")
|
|
self.audit_tree.heading("query", text="用户问题")
|
|
|
|
for title in self.KEY_NODE_TITLES:
|
|
self.audit_tree.heading(f"node_{title}", text=title)
|
|
self.audit_tree.heading(f"audit_{title}", text=f"{title}审核")
|
|
|
|
self.audit_tree.heading("answer", text="最终回答")
|
|
|
|
col_cfg = self.config['columns']
|
|
self.audit_tree.column("index", width=col_cfg['index'], anchor="center")
|
|
self.audit_tree.column("chat_id", width=col_cfg['chat_id'])
|
|
self.audit_tree.column("title", width=col_cfg['title'])
|
|
self.audit_tree.column("time", width=col_cfg['time'])
|
|
self.audit_tree.column("query", width=col_cfg['query'])
|
|
|
|
for title in self.KEY_NODE_TITLES:
|
|
self.audit_tree.column(f"node_{title}", width=col_cfg['node_output'])
|
|
self.audit_tree.column(f"audit_{title}", width=col_cfg['node_audit'], anchor="center")
|
|
|
|
self.audit_tree.column("answer", width=col_cfg['answer'])
|
|
|
|
self.audit_tree.bind("<ButtonRelease-1>", self.on_audit_select)
|
|
self.audit_tree.bind("<Double-1>", self.on_double_click_audit)
|
|
|
|
scrollbar = ttk.Scrollbar(self.audit_frame, orient="vertical", command=self.audit_tree.yview)
|
|
self.audit_tree.configure(yscrollcommand=scrollbar.set)
|
|
|
|
scrollbar_x = ttk.Scrollbar(self.audit_frame, orient="horizontal", command=self.audit_tree.xview)
|
|
self.audit_tree.configure(xscrollcommand=scrollbar_x.set)
|
|
|
|
scrollbar.pack(side="right", fill="y")
|
|
scrollbar_x.pack(side="bottom", fill="x")
|
|
self.audit_tree.pack(fill=tk.BOTH, expand=True)
|
|
|
|
self.detail_frame = ttk.Frame(self.audit_frame)
|
|
self.detail_frame.pack(fill=tk.X, expand=False, pady=5)
|
|
|
|
self.detail_text = tk.Text(self.detail_frame, height=8, wrap=tk.WORD)
|
|
self.detail_text.pack(fill=tk.BOTH, expand=True)
|
|
self.detail_text.insert(tk.END, "点击表格中的行查看详情...\n双击审核列可填写审核结果(可选:正确/错误/需改进)")
|
|
self.detail_text.config(state=tk.DISABLED)
|
|
|
|
def create_log_tree(self):
|
|
columns = ("index", "chat_id", "wf_id", "node_idx", "node_type", "node_title", "status", "start_time", "end_time")
|
|
self.log_tree = ttk.Treeview(self.log_frame, columns=columns, show="headings")
|
|
|
|
self.log_tree.heading("index", text="序号")
|
|
self.log_tree.heading("chat_id", text="对话ID")
|
|
self.log_tree.heading("wf_id", text="工作流ID")
|
|
self.log_tree.heading("node_idx", text="节点序号")
|
|
self.log_tree.heading("node_type", text="节点类型")
|
|
self.log_tree.heading("node_title", text="节点标题")
|
|
self.log_tree.heading("status", text="状态")
|
|
self.log_tree.heading("start_time", text="开始时间")
|
|
self.log_tree.heading("end_time", text="结束时间")
|
|
|
|
self.log_tree.column("index", width=50, anchor="center")
|
|
self.log_tree.column("chat_id", width=120)
|
|
self.log_tree.column("wf_id", width=120)
|
|
self.log_tree.column("node_idx", width=60, anchor="center")
|
|
self.log_tree.column("node_type", width=100)
|
|
self.log_tree.column("node_title", width=120)
|
|
self.log_tree.column("status", width=80, anchor="center")
|
|
self.log_tree.column("start_time", width=130)
|
|
self.log_tree.column("end_time", width=130)
|
|
|
|
self.log_tree.bind("<ButtonRelease-1>", self.on_log_select)
|
|
|
|
scrollbar = ttk.Scrollbar(self.log_frame, orient="vertical", command=self.log_tree.yview)
|
|
self.log_tree.configure(yscrollcommand=scrollbar.set)
|
|
|
|
scrollbar_x = ttk.Scrollbar(self.log_frame, orient="horizontal", command=self.log_tree.xview)
|
|
self.log_tree.configure(xscrollcommand=scrollbar_x.set)
|
|
|
|
scrollbar.pack(side="right", fill="y")
|
|
scrollbar_x.pack(side="bottom", fill="x")
|
|
self.log_tree.pack(fill=tk.BOTH, expand=True)
|
|
|
|
self.log_detail_frame = ttk.Frame(self.log_frame)
|
|
self.log_detail_frame.pack(fill=tk.X, expand=False, pady=5)
|
|
|
|
self.log_input_text = tk.Text(self.log_detail_frame, height=6, wrap=tk.WORD)
|
|
self.log_input_text.pack(fill=tk.BOTH, expand=True, side=tk.LEFT, padx=5)
|
|
self.log_input_text.insert(tk.END, "输入参数...")
|
|
self.log_input_text.config(state=tk.DISABLED)
|
|
|
|
self.log_output_text = tk.Text(self.log_detail_frame, height=6, wrap=tk.WORD)
|
|
self.log_output_text.pack(fill=tk.BOTH, expand=True, side=tk.RIGHT, padx=5)
|
|
self.log_output_text.insert(tk.END, "输出结果...")
|
|
self.log_output_text.config(state=tk.DISABLED)
|
|
|
|
def on_closing(self):
|
|
self.save_audit_cache()
|
|
self.root.destroy()
|
|
|
|
def save_audit_cache(self):
|
|
try:
|
|
cache = {}
|
|
for rec in self.records:
|
|
key = f"{rec['chat_log_id']}_{rec['wf_run_id']}"
|
|
audit_data = {}
|
|
for field_key, audit_val in rec['key_audit_fields'].items():
|
|
if audit_val:
|
|
audit_data[field_key] = audit_val
|
|
if audit_data:
|
|
cache[key] = audit_data
|
|
existing = {}
|
|
if os.path.exists(self.audit_file):
|
|
with open(self.audit_file, 'r', encoding='utf-8') as f:
|
|
existing = json.load(f)
|
|
existing.update(cache)
|
|
with open(self.audit_file, 'w', encoding='utf-8') as f:
|
|
json.dump(existing, f, ensure_ascii=False, indent=2)
|
|
except Exception:
|
|
pass
|
|
|
|
def load_audit_cache(self):
|
|
if not os.path.exists(self.audit_file):
|
|
return
|
|
try:
|
|
with open(self.audit_file, 'r', encoding='utf-8') as f:
|
|
cache = json.load(f)
|
|
for rec in self.records:
|
|
key = f"{rec['chat_log_id']}_{rec['wf_run_id']}"
|
|
if key in cache:
|
|
for field_key, audit_val in cache[key].items():
|
|
if field_key in rec['key_audit_fields']:
|
|
rec['key_audit_fields'][field_key] = audit_val
|
|
except Exception:
|
|
pass
|
|
|
|
def update_status(self, text):
|
|
self.status_label.config(text=f"状态: {text}")
|
|
self.root.update_idletasks()
|
|
|
|
def fetch_nodes(self, wf_run_id):
|
|
if not wf_run_id:
|
|
return []
|
|
try:
|
|
response = requests.get(
|
|
f'https://agent.udesk.cn/api/backend/new/apps/{self.app_id}/wfRuns/{wf_run_id}/nodeExecutions',
|
|
cookies=self.cookies,
|
|
headers=self.headers,
|
|
)
|
|
data = response.json()
|
|
return data.get('data', {}).get('list', [])
|
|
except Exception as e:
|
|
return []
|
|
|
|
def fetch_all_messages(self, chat_log_id):
|
|
all_messages = []
|
|
fid = None
|
|
page_size = 10
|
|
|
|
while True:
|
|
try:
|
|
c_params = {'id': chat_log_id, 'page_size': str(page_size)}
|
|
if fid:
|
|
c_params['fid'] = fid
|
|
|
|
c_resp = requests.get(
|
|
f'https://agent.udesk.cn/api/backend/apps/{self.app_id}/chatMessage',
|
|
params=c_params,
|
|
cookies=self.cookies,
|
|
headers=self.headers,
|
|
timeout=30
|
|
)
|
|
|
|
data = c_resp.json()
|
|
messages = data.get('data', {}).get('list', [])
|
|
|
|
if not messages:
|
|
break
|
|
|
|
all_messages.extend(messages)
|
|
|
|
if len(messages) < page_size:
|
|
break
|
|
|
|
fid = messages[0].get('id')
|
|
if not fid:
|
|
break
|
|
|
|
except Exception as e:
|
|
break
|
|
|
|
return all_messages
|
|
|
|
def start_fetch(self):
|
|
if self.fetch_btn["state"] == tk.DISABLED:
|
|
return
|
|
|
|
start_date = self.start_date.get()
|
|
end_date = self.end_date.get()
|
|
|
|
try:
|
|
datetime.strptime(start_date, "%Y-%m-%d")
|
|
datetime.strptime(end_date, "%Y-%m-%d")
|
|
except ValueError:
|
|
messagebox.showerror("错误", "日期格式不正确,请使用 YYYY-MM-DD 格式")
|
|
return
|
|
|
|
self.config['last_date_range'] = {
|
|
'start_date': start_date,
|
|
'end_date': end_date
|
|
}
|
|
self.save_config()
|
|
|
|
self.fetch_btn.config(state=tk.DISABLED)
|
|
self.export_btn.config(state=tk.DISABLED)
|
|
self.update_status("正在获取数据...")
|
|
|
|
thread = threading.Thread(target=self.fetch_data, args=(start_date, end_date))
|
|
thread.start()
|
|
|
|
def fetch_data(self, start_date, end_date):
|
|
try:
|
|
self.records = []
|
|
self.logs = []
|
|
|
|
params = {
|
|
'page_number': '1',
|
|
'page_size': '10',
|
|
'start_time': f'{start_date} 00:00',
|
|
'end_time': f'{end_date} 23:59',
|
|
'order_by': '-created_at',
|
|
'status': 'all',
|
|
}
|
|
|
|
self.update_status("正在连接服务器...")
|
|
self.update_status(f"App ID: {self.app_id}")
|
|
self.update_status(f"Token存在: {'是' if self.headers.get('authorization', '').startswith('Bearer ') else '否'}")
|
|
self.update_status(f"Cookies数量: {len(self.cookies)}")
|
|
|
|
response = requests.get(
|
|
f'https://agent.udesk.cn/api/backend/apps/{self.app_id}/log/chat/pages',
|
|
params=params,
|
|
cookies=self.cookies,
|
|
headers=self.headers,
|
|
timeout=30
|
|
)
|
|
|
|
self.update_status(f"响应状态码: {response.status_code}")
|
|
if response.status_code != 200:
|
|
try:
|
|
error_data = response.json()
|
|
error_msg = error_data.get('msg', error_data.get('message', '未知错误'))
|
|
except:
|
|
error_msg = response.text[:200]
|
|
self.update_status(f"请求失败: {response.status_code} - {error_msg}")
|
|
raise Exception(f"HTTP错误: {response.status_code} - {error_msg}")
|
|
|
|
data = response.json()
|
|
total = data.get('data', {}).get('total', 0)
|
|
total_page = max(total // 10 + (1 if total % 10 else 0), 1)
|
|
|
|
self.update_status(f"共 {total} 条对话,{total_page} 页")
|
|
|
|
chart_list = []
|
|
for page in range(1, total_page + 1):
|
|
params['page_number'] = str(page)
|
|
response = requests.get(
|
|
f'https://agent.udesk.cn/api/backend/apps/{self.app_id}/log/chat/pages',
|
|
params=params,
|
|
cookies=self.cookies,
|
|
headers=self.headers,
|
|
)
|
|
page_data = response.json().get('data', {}).get('list', [])
|
|
chart_list.extend(page_data)
|
|
self.update_status(f"获取第 {page}/{total_page} 页...")
|
|
|
|
log_idx = 0
|
|
for idx, chart in enumerate(chart_list):
|
|
chat_log_id = chart.get('chat_log_id', '')
|
|
chat_title = chart.get('title') or chart.get('chat_log_name') or ''
|
|
created_time = str(chart.get('created_time', ''))
|
|
|
|
messages = self.fetch_all_messages(chat_log_id)
|
|
|
|
for msg in messages:
|
|
wf_run_id = msg.get('workflow_run_id', '')
|
|
if not wf_run_id:
|
|
continue
|
|
nodes = self.fetch_nodes(wf_run_id)
|
|
|
|
user_query = ''
|
|
for n in nodes:
|
|
if n.get('node_type') == 'start' and n.get('outputs', {}).get('sys.query'):
|
|
user_query = n['outputs']['sys.query']
|
|
break
|
|
|
|
key_outputs = {}
|
|
key_audit_fields = {}
|
|
final_answer = ''
|
|
title_counter = {}
|
|
|
|
for n in nodes:
|
|
title = n.get('title', '')
|
|
node_output = (n.get('outputs') or {}).get('text', '')
|
|
|
|
if title in self.KEY_NODE_TITLES:
|
|
if title in title_counter:
|
|
title_counter[title] += 1
|
|
unique_title = f"{title}-{title_counter[title]}"
|
|
else:
|
|
title_counter[title] = 1
|
|
unique_title = title
|
|
|
|
key_outputs[unique_title] = node_output
|
|
key_audit_fields[unique_title] = ''
|
|
|
|
if n.get('node_type') == 'answer':
|
|
final_answer = (n.get('outputs') or {}).get('answer', '')
|
|
|
|
self.records.append({
|
|
'chat_log_id': chat_log_id,
|
|
'chat_title': chat_title,
|
|
'created_time': created_time,
|
|
'user_query': user_query,
|
|
'wf_run_id': wf_run_id,
|
|
'nodes': nodes,
|
|
'key_outputs': key_outputs,
|
|
'key_audit_fields': key_audit_fields,
|
|
'final_answer': final_answer,
|
|
})
|
|
|
|
for node in nodes:
|
|
log_idx += 1
|
|
created_at = node.get('created_at') or node.get('started_at', 0)
|
|
finished_at = node.get('finished_at', 0)
|
|
self.logs.append({
|
|
'idx': log_idx,
|
|
'chat_log_id': chat_log_id,
|
|
'user_query': user_query,
|
|
'wf_run_id': wf_run_id,
|
|
'node_index': node.get('index', ''),
|
|
'node_type': node.get('node_type', ''),
|
|
'node_title': node.get('title', ''),
|
|
'status': node.get('status', ''),
|
|
'start_time': self.ts_to_datetime(created_at),
|
|
'end_time': self.ts_to_datetime(finished_at),
|
|
'inputs': node.get('inputs'),
|
|
'outputs': node.get('outputs'),
|
|
'error': node.get('error', ''),
|
|
})
|
|
|
|
self.update_status(f"处理第 {idx+1}/{len(chart_list)} 条对话...")
|
|
|
|
self.load_audit_cache()
|
|
self.root.after(0, self.update_trees)
|
|
self.update_status(f"完成!共 {len(self.records)} 条审核记录,{len(self.logs)} 条日志")
|
|
self.export_btn.config(state=tk.NORMAL)
|
|
self.review_btn.config(state=tk.NORMAL)
|
|
|
|
except Exception as e:
|
|
self.update_status(f"获取失败: {str(e)}")
|
|
messagebox.showerror("错误", f"获取数据失败: {str(e)}")
|
|
finally:
|
|
self.fetch_btn.config(state=tk.NORMAL)
|
|
|
|
def ts_to_datetime(self, ts):
|
|
try:
|
|
return datetime.fromtimestamp(int(ts)).strftime('%Y-%m-%d %H:%M:%S')
|
|
except Exception:
|
|
return ''
|
|
|
|
def update_trees(self):
|
|
for item in self.audit_tree.get_children():
|
|
self.audit_tree.delete(item)
|
|
|
|
for idx, rec in enumerate(self.records):
|
|
values = [
|
|
idx+1,
|
|
rec['chat_log_id'],
|
|
rec['chat_title'],
|
|
rec['created_time'],
|
|
self.truncate_text(rec['user_query'], 30),
|
|
]
|
|
|
|
for title in self.KEY_NODE_TITLES:
|
|
output_text = ''
|
|
audit_text = ''
|
|
|
|
if title in rec['key_outputs']:
|
|
output_text = self.truncate_text(rec['key_outputs'].get(title, ''), 30)
|
|
audit_text = rec['key_audit_fields'].get(title, '')
|
|
elif f"{title}-1" in rec['key_outputs']:
|
|
output_text = self.truncate_text(rec['key_outputs'].get(f"{title}-1", ''), 30)
|
|
audit_text = rec['key_audit_fields'].get(f"{title}-1", '')
|
|
|
|
values.append(output_text)
|
|
values.append(audit_text)
|
|
|
|
values.append(self.truncate_text(rec['final_answer'], 30))
|
|
self.audit_tree.insert("", "end", values=values)
|
|
|
|
for item in self.log_tree.get_children():
|
|
self.log_tree.delete(item)
|
|
|
|
for log in self.logs:
|
|
tag = 'failed' if log['status'] == 'failed' else ''
|
|
self.log_tree.insert("", "end", values=(
|
|
log['idx'],
|
|
log['chat_log_id'],
|
|
log['wf_run_id'],
|
|
log['node_index'],
|
|
log['node_type'],
|
|
log['node_title'],
|
|
log['status'],
|
|
log['start_time'],
|
|
log['end_time'],
|
|
), tags=(tag,))
|
|
|
|
self.log_tree.tag_configure('failed', foreground='red')
|
|
|
|
def truncate_text(self, text, max_len):
|
|
if text and len(text) > max_len:
|
|
return text[:max_len] + "..."
|
|
return text
|
|
|
|
def on_audit_select(self, event):
|
|
item = self.audit_tree.selection()
|
|
if not item:
|
|
return
|
|
|
|
idx = int(self.audit_tree.item(item[0])['values'][0]) - 1
|
|
if idx < 0 or idx >= len(self.records):
|
|
return
|
|
|
|
rec = self.records[idx]
|
|
detail = f"""对话ID: {rec['chat_log_id']}
|
|
会话标题: {rec['chat_title']}
|
|
时间: {rec['created_time']}
|
|
|
|
【用户问题】
|
|
{rec['user_query']}
|
|
|
|
"""
|
|
|
|
for title in sorted(rec['key_outputs'].keys()):
|
|
detail += f"【{title}】\n{rec['key_outputs'].get(title, '')}\n\n"
|
|
|
|
detail += f"【最终回答】\n{rec['final_answer']}\n\n【审核状态】\n"
|
|
|
|
for title in sorted(rec['key_audit_fields'].keys()):
|
|
audit_val = rec['key_audit_fields'].get(title, '')
|
|
detail += f"{title}: {audit_val if audit_val else '未审核'}\n"
|
|
|
|
self.detail_text.config(state=tk.NORMAL)
|
|
self.detail_text.delete(1.0, tk.END)
|
|
self.detail_text.insert(tk.END, detail)
|
|
self.detail_text.config(state=tk.DISABLED)
|
|
|
|
def on_double_click_audit(self, event):
|
|
item = self.audit_tree.selection()
|
|
if not item:
|
|
return
|
|
|
|
idx = int(self.audit_tree.item(item[0])['values'][0]) - 1
|
|
if idx < 0 or idx >= len(self.records):
|
|
return
|
|
|
|
col_num = int(self.audit_tree.identify_column(event.x).replace('#', ''))
|
|
|
|
for i, title in enumerate(self.KEY_NODE_TITLES):
|
|
audit_col_idx = 6 + i * 2
|
|
if col_num == audit_col_idx:
|
|
for key in sorted(self.records[idx]['key_audit_fields'].keys()):
|
|
if key.startswith(title):
|
|
self.show_audit_dialog(idx, key, f"{key}审核")
|
|
return
|
|
|
|
def show_audit_dialog(self, idx, field_key, title):
|
|
dialog = tk.Toplevel(self.root)
|
|
dialog.title(title)
|
|
dialog.geometry("400x250")
|
|
|
|
current_value = self.records[idx]['key_audit_fields'].get(field_key, '')
|
|
|
|
ttk.Label(dialog, text=f"请选择{title}结果:", padding=10).pack()
|
|
|
|
var = tk.StringVar(value=current_value)
|
|
|
|
options = ["正确", "错误", "需改进", ""]
|
|
for opt in options:
|
|
ttk.Radiobutton(dialog, text=opt, variable=var, value=opt).pack(anchor='w', padx=20, pady=2)
|
|
|
|
ttk.Label(dialog, text="备注(可选):", padding=10).pack()
|
|
note_entry = ttk.Entry(dialog, width=40)
|
|
note_entry.pack(padx=10)
|
|
|
|
def save():
|
|
self.records[idx]['key_audit_fields'][field_key] = var.get()
|
|
self.update_trees()
|
|
self.save_audit_cache()
|
|
dialog.destroy()
|
|
self.update_status(f"已保存:{title} = {var.get()}")
|
|
|
|
ttk.Button(dialog, text="保存", command=save).pack(pady=10)
|
|
ttk.Button(dialog, text="取消", command=dialog.destroy).pack(pady=5)
|
|
|
|
dialog.transient(self.root)
|
|
dialog.grab_set()
|
|
self.root.wait_window(dialog)
|
|
|
|
def on_log_select(self, event):
|
|
item = self.log_tree.selection()
|
|
if not item:
|
|
return
|
|
|
|
idx = int(self.log_tree.item(item[0])['values'][0]) - 1
|
|
if idx < 0 or idx >= len(self.logs):
|
|
return
|
|
|
|
log = self.logs[idx]
|
|
|
|
self.log_input_text.config(state=tk.NORMAL)
|
|
self.log_input_text.delete(1.0, tk.END)
|
|
self.log_input_text.insert(tk.END, json.dumps(log['inputs'], ensure_ascii=False, indent=2))
|
|
self.log_input_text.config(state=tk.DISABLED)
|
|
|
|
self.log_output_text.config(state=tk.NORMAL)
|
|
self.log_output_text.delete(1.0, tk.END)
|
|
self.log_output_text.insert(tk.END, json.dumps(log['outputs'], ensure_ascii=False, indent=2))
|
|
self.log_output_text.config(state=tk.DISABLED)
|
|
|
|
def export_excel(self):
|
|
if not self.records:
|
|
messagebox.showwarning("提示", "没有数据可导出")
|
|
return
|
|
|
|
try:
|
|
desktop = os.path.expanduser("~\\Desktop")
|
|
default_dir = desktop if os.path.exists(desktop) else os.path.expanduser("~")
|
|
except:
|
|
default_dir = os.path.expanduser("~")
|
|
|
|
file_path = filedialog.asksaveasfilename(
|
|
defaultextension=".xlsx",
|
|
filetypes=[("Excel文件", "*.xlsx")],
|
|
initialdir=default_dir,
|
|
initialfile=f"智能客服节点审核_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
|
)
|
|
|
|
if not file_path:
|
|
return
|
|
|
|
try:
|
|
self.update_status("正在生成Excel...")
|
|
|
|
wb = Workbook()
|
|
|
|
ws1 = wb.active
|
|
ws1.title = '关键节点审核'
|
|
|
|
header_font = Font(name='微软雅黑', bold=True, size=11, color='FFFFFF')
|
|
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
|
|
key_fill = PatternFill(start_color='FFF2CC', end_color='FFF2CC', fill_type='solid')
|
|
data_font = Font(name='微软雅黑', size=10)
|
|
thin_border = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin'))
|
|
wrap_alignment = Alignment(horizontal='left', vertical='top', wrap_text=True)
|
|
center_alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
|
|
|
|
sheet1_headers = ['序号', '对话ID', '会话标题', '对话时间', '用户问题']
|
|
for title in self.KEY_NODE_TITLES:
|
|
sheet1_headers.append(f'{title}_输出')
|
|
sheet1_headers.append(f'{title}审核')
|
|
sheet1_headers.extend(['最终回答', '备注'])
|
|
|
|
key_cols = []
|
|
for col_idx, header in enumerate(sheet1_headers, 1):
|
|
cell = ws1.cell(row=1, column=col_idx, value=header)
|
|
cell.font = header_font
|
|
cell.fill = header_fill
|
|
cell.alignment = center_alignment
|
|
cell.border = thin_border
|
|
if col_idx >= 6:
|
|
key_cols.append(col_idx)
|
|
|
|
for row_idx, rec in enumerate(self.records):
|
|
r = row_idx + 2
|
|
values = [
|
|
row_idx + 1,
|
|
rec['chat_log_id'],
|
|
rec['chat_title'],
|
|
rec['created_time'],
|
|
rec['user_query'],
|
|
]
|
|
|
|
for title in self.KEY_NODE_TITLES:
|
|
output_text = ''
|
|
audit_text = ''
|
|
if title in rec['key_outputs']:
|
|
output_text = rec['key_outputs'].get(title, '')
|
|
audit_text = rec['key_audit_fields'].get(title, '')
|
|
elif f"{title}-1" in rec['key_outputs']:
|
|
output_text = rec['key_outputs'].get(f"{title}-1", '')
|
|
audit_text = rec['key_audit_fields'].get(f"{title}-1", '')
|
|
values.append(output_text)
|
|
values.append(audit_text)
|
|
|
|
values.extend([rec['final_answer'], ''])
|
|
|
|
for col_idx, val in enumerate(values, 1):
|
|
cell = ws1.cell(row=r, column=col_idx, value=val)
|
|
cell.font = data_font
|
|
cell.alignment = wrap_alignment
|
|
cell.border = thin_border
|
|
if col_idx in key_cols:
|
|
cell.fill = key_fill
|
|
|
|
ws1.column_dimensions['A'].width = 6
|
|
ws1.column_dimensions['B'].width = 38
|
|
ws1.column_dimensions['C'].width = 20
|
|
ws1.column_dimensions['D'].width = 20
|
|
ws1.column_dimensions['E'].width = 40
|
|
|
|
col_letter = 'F'
|
|
for _ in self.KEY_NODE_TITLES:
|
|
ws1.column_dimensions[col_letter].width = 50
|
|
col_letter = chr(ord(col_letter) + 1)
|
|
ws1.column_dimensions[col_letter].width = 12
|
|
col_letter = chr(ord(col_letter) + 1)
|
|
|
|
ws1.column_dimensions[col_letter].width = 50
|
|
col_letter = chr(ord(col_letter) + 1)
|
|
ws1.column_dimensions[col_letter].width = 20
|
|
|
|
ws1.freeze_panes = 'E2'
|
|
ws1.auto_filter.ref = f'A1:{col_letter}{len(self.records) + 1}'
|
|
|
|
ws2 = wb.create_sheet('全流程日志明细')
|
|
|
|
sheet2_headers = ['序号', '对话ID', '用户问题', '工作流运行ID', '节点序号', '节点类型', '节点标题', '节点状态', '执行开始时间', '执行结束时间', '节点输入(JSON)', '节点输出(JSON)', '错误信息']
|
|
for col_idx, header in enumerate(sheet2_headers, 1):
|
|
cell = ws2.cell(row=1, column=col_idx, value=header)
|
|
cell.font = header_font
|
|
cell.fill = PatternFill(start_color='548235', end_color='548235', fill_type='solid')
|
|
cell.alignment = center_alignment
|
|
cell.border = thin_border
|
|
|
|
for row_idx, log in enumerate(self.logs):
|
|
r = row_idx + 2
|
|
values = [
|
|
log['idx'],
|
|
log['chat_log_id'],
|
|
log['user_query'],
|
|
log['wf_run_id'],
|
|
log['node_index'],
|
|
log['node_type'],
|
|
log['node_title'],
|
|
log['status'],
|
|
log['start_time'],
|
|
log['end_time'],
|
|
json.dumps(log['inputs'], ensure_ascii=False),
|
|
json.dumps(log['outputs'], ensure_ascii=False),
|
|
log['error'],
|
|
]
|
|
for col_idx, val in enumerate(values, 1):
|
|
cell = ws2.cell(row=r, column=col_idx, value=val)
|
|
cell.font = data_font
|
|
cell.alignment = wrap_alignment
|
|
cell.border = thin_border
|
|
if log['status'] == 'failed':
|
|
cell.fill = PatternFill(start_color='FFC7CE', end_color='FFC7CE', fill_type='solid')
|
|
|
|
ws2.column_dimensions['A'].width = 6
|
|
ws2.column_dimensions['B'].width = 38
|
|
ws2.column_dimensions['C'].width = 40
|
|
ws2.column_dimensions['D'].width = 38
|
|
ws2.column_dimensions['E'].width = 8
|
|
ws2.column_dimensions['F'].width = 22
|
|
ws2.column_dimensions['G'].width = 20
|
|
ws2.column_dimensions['H'].width = 12
|
|
ws2.column_dimensions['I'].width = 20
|
|
ws2.column_dimensions['J'].width = 20
|
|
ws2.column_dimensions['K'].width = 60
|
|
ws2.column_dimensions['L'].width = 60
|
|
ws2.column_dimensions['M'].width = 30
|
|
ws2.freeze_panes = 'F2'
|
|
ws2.auto_filter.ref = f'A1:M{len(self.logs) + 1}'
|
|
|
|
if self.review_results:
|
|
ws3 = wb.create_sheet('大模型评测结果')
|
|
|
|
sheet3_headers = ['序号', '对话ID', '用户问题', '匹配问题', '匹配度', '标准回答']
|
|
reviewed_nodes = set()
|
|
for key, result in self.review_results.items():
|
|
reviewed_nodes.update(result.get('node_results', {}).keys())
|
|
|
|
for node in sorted(reviewed_nodes):
|
|
sheet3_headers.append(f'{node}_一致性评分')
|
|
sheet3_headers.append(f'{node}_置信度(%)')
|
|
sheet3_headers.append(f'{node}_评审理由')
|
|
|
|
for col_idx, header in enumerate(sheet3_headers, 1):
|
|
cell = ws3.cell(row=1, column=col_idx, value=header)
|
|
cell.font = header_font
|
|
cell.fill = PatternFill(start_color='7030A0', end_color='7030A0', fill_type='solid')
|
|
cell.alignment = center_alignment
|
|
cell.border = thin_border
|
|
|
|
row_idx = 0
|
|
for rec in self.records:
|
|
key = f"{rec['chat_log_id']}_{rec['wf_run_id']}"
|
|
if key in self.review_results:
|
|
row_idx += 1
|
|
r = row_idx + 2
|
|
result = self.review_results[key]
|
|
values = [
|
|
row_idx,
|
|
rec['chat_log_id'],
|
|
rec['user_query'],
|
|
result.get('matched_question', ''),
|
|
f"{result.get('match_score', 0):.2f}",
|
|
result.get('standard_answer', '')[:200],
|
|
]
|
|
|
|
for node in sorted(reviewed_nodes):
|
|
node_result = result.get('node_results', {}).get(node, {})
|
|
values.append(node_result.get('score', ''))
|
|
values.append(node_result.get('confidence', ''))
|
|
values.append(node_result.get('reason', '')[:100])
|
|
|
|
for col_idx, val in enumerate(values, 1):
|
|
cell = ws3.cell(row=r, column=col_idx, value=val)
|
|
cell.font = data_font
|
|
cell.alignment = wrap_alignment
|
|
cell.border = thin_border
|
|
|
|
ws3.column_dimensions['A'].width = 6
|
|
ws3.column_dimensions['B'].width = 38
|
|
ws3.column_dimensions['C'].width = 40
|
|
ws3.column_dimensions['D'].width = 40
|
|
ws3.column_dimensions['E'].width = 12
|
|
ws3.column_dimensions['F'].width = 50
|
|
|
|
col_letter = 'G'
|
|
for _ in reviewed_nodes:
|
|
ws3.column_dimensions[col_letter].width = 15
|
|
col_letter = chr(ord(col_letter) + 1)
|
|
ws3.column_dimensions[col_letter].width = 12
|
|
col_letter = chr(ord(col_letter) + 1)
|
|
ws3.column_dimensions[col_letter].width = 40
|
|
col_letter = chr(ord(col_letter) + 1)
|
|
|
|
ws3.freeze_panes = 'F2'
|
|
|
|
wb.save(file_path)
|
|
self.update_status("Excel导出完成")
|
|
messagebox.showinfo("成功", f"Excel已导出到:\n{file_path}")
|
|
except PermissionError:
|
|
messagebox.showerror("错误", f"无法保存到该位置!请检查权限或选择其他路径。")
|
|
self.update_status("保存失败:权限不足")
|
|
except Exception as e:
|
|
messagebox.showerror("错误", f"导出Excel时发生错误:{str(e)}")
|
|
self.update_status("保存失败")
|
|
|
|
if __name__ == "__main__":
|
|
root = tk.Tk()
|
|
app = UdeskLogViewer(root)
|
|
root.mainloop()
|