import json import gradio as gr from pathlib import Path from counselor import PsychodynamicCounselor counselor = None def _depth_level(score): """将 1-10 分映射为探索层级""" if score <= 2: return "L1 · 表层", "社交性叙述,回避情感" elif score <= 4: return "L2 · 事件", "具体事件浮现,情绪初现" elif score <= 6: return "L3 · 情感", "情绪深化,开始反思模式" elif score <= 8: return "L4 · 核心", "触及核心冲突与早期经历" else: return "L5 · 突破", "深层揭露,防御松动" def _phase_label(turn): """会话阶段""" if turn <= 3: return "建立联盟" elif turn <= 8: return "探索展开" elif turn <= 15: return "深层工作" else: return "整合修通" def build_status_panel(c): """构建会话状态 Markdown 富文本面板""" score = c._last_disclosure_score turn = c.turn_number history = c._disclosure_history # ── 探索层级 ── level_name, level_desc = _depth_level(score) phase = _phase_label(turn) # 揭露深度进度条(渐变风格) bar_chars = "░▒▓█" filled_bar = "" for i in range(10): if i < score: ci = min(i // 3, 3) filled_bar += bar_chars[ci] else: filled_bar += "·" # 趋势计算 if len(history) >= 2: diff = history[-1] - history[-2] trend = "▲" if diff > 0 else ("▼" if diff < 0 else "━") trend_word = f"+{diff}" if diff > 0 else str(diff) if diff < 0 else "±0" else: trend = "·" trend_word = "—" # 均值 & 峰值 avg_score = sum(history) / len(history) if history else 0 peak_score = max(history) if history else 0 # 历史火花线 (sparkline) spark_chars = " ▁▂▃▄▅▆▇█" spark = "" for s in history[-20:]: idx = min(s, 9) spark += spark_chars[idx] if not spark: spark = "—" # 维度指示灯 dims = c._last_dimensions dim_labels = { "A": "具体事件", "B": "情绪表达", "C": "具体情绪", "D": "自我反思", "E": "回避触及", } dim_on = sum(1 for k in ["A", "B", "C", "D", "E"] if dims.get(k, False)) dim_parts = [] for k in ["A", "B", "C", "D", "E"]: on = dims.get(k, False) icon = "🟢" if on else "⚫" dim_parts.append(f"{icon} {dim_labels[k]}") dim_line = " · ".join(dim_parts) # ── 构建面板 ── lines = [] lines.append("### ◈ SESSION MONITOR") lines.append("") # 核心指标表 lines.append("| | |") lines.append("|:---|:---|") lines.append(f"| **轮次** | `T{turn}` · {phase} |") lines.append(f"| **探索层级** | **{level_name}** — {level_desc} |") lines.append(f"| **揭露深度** | `{filled_bar}` **{score}/10** {trend} ({trend_word}) |") lines.append(f"| **均值 / 峰值** | avg `{avg_score:.1f}` · peak `{peak_score}` |") lines.append(f"| **深度轨迹** | `{spark}` |") lines.append(f"| **维度命中** | **{dim_on}/5** |") lines.append("") # 维度详情 lines.append(f"> {dim_line}") lines.append("") # ── 督导模块 ── lines.append("---") if c.current_guidance: g = c.current_guidance direction = g.get("direction", "—") principles = g.get("principles", []) evidence = g.get("evidence", "") lines.append("#### ▸ 督导指令") lines.append("") lines.append(f"> 🎯 **{direction}**") if principles: lines.append(">") for i, p in enumerate(principles[:3], 1): lines.append(f"> {i}. {p}") if evidence: lines.append(">") lines.append(f"> 📌 {evidence[:100]}{'…' if len(evidence) > 100 else ''}") lines.append("") # ── PUCT 推理引擎 ── ts = c._last_trace_stats if ts: timing = ts.get("timing", {}) total_s = timing.get("total_seconds", 0) total_paths = ts.get("total_paths", 0) deep = ts.get("deep_paths", 0) seeds = ts.get("seeds", []) selected = ts.get("selected", "") best_score = ts.get("best_score", 0) best_delta = ts.get("best_delta", 0) predicted = ts.get("predicted_disclosure", "?") lines.append("---") lines.append("#### ▸ PUCT 推理引擎") lines.append("") lines.append("| | |") lines.append("|:---|:---|") lines.append(f"| **搜索架构** | `L1 → L2 → L3 → L4 → L5 → L6` (6层深度) |") lines.append(f"| **种子方向** | {' / '.join(seeds) if seeds else '—'} |") lines.append(f"| **路径探索** | {total_paths} 候选 → {deep} 深探 |") lines.append(f"| **最优路径** | `{selected}` |") lines.append(f"| **路径评分** | score=**{best_score}** · Δ=**{best_delta:+.1f}** |") lines.append(f"| **预测揭露** | **{predicted}**/10 |") lines.append(f"| **推理耗时** | `{total_s:.1f}s` |") else: lines.append("#### ▸ 推理引擎") lines.append("") lines.append("> ⏳ 同步推理中…") # ── 评估理由(折叠) ── if c._last_reasoning: lines.append("") lines.append(f"
📋 评估理由\n\n{c._last_reasoning}\n\n
") return "\n".join(lines) def start_session(): global counselor counselor = PsychodynamicCounselor() return [], "### 🧠 SESSION MONITOR\n\n> 新会话已开始,等待来访者发言…" def chat(user_message, chat_history): global counselor if counselor is None: counselor = PsychodynamicCounselor() if not user_message.strip(): return chat_history, "", "", "" response = counselor.respond(user_message) chat_history = chat_history or [] chat_history.append({"role": "user", "content": user_message}) chat_history.append({"role": "assistant", "content": response}) # 构建富文本状态面板 status = build_status_panel(counselor) return chat_history, "", status, "" def end_session(): global counselor if counselor: path = counselor.get_session_filepath() counselor = None return f"会话已结束。日志保存于:{path}" return "当前无活跃会话。" def view_sessions(): files = sorted(Path("sessions").glob("session_*.json")) if not files: return "暂无会话记录" output = "" for f in files: with open(f, encoding="utf-8") as fp: data = json.load(fp) total = data.get("total_turns", len(data["turns"])) output += f"\n{'='*50}\n" output += f"Session: {data['session_id']} | 轮次: {total}\n" output += f"{'='*50}\n" for t in data["turns"]: score = t.get("disclosure_score", "?") output += f"\n[轮次 {t['turn_number']}] 揭露评分: {score}/5\n" output += f"来访者: {t['user_message']}\n" output += f"咨询师: {t['counselor_message']}\n" dims = t.get("dimension_score", {}) reason = t.get("reason", "") if dims: output += f"维度: {dims}\n" if reason: output += f"理由: {reason}\n" # 战略推理记录(每5轮) trace = t.get("mcts_trace") if trace and "selected_direction" in trace: output += f"\n === 战略推理(第{t['turn_number']}轮触发) ===\n" output += f" 总结: {trace.get('summary', '')[:80]}...\n" output += f" 选中: {trace['selected']} → {trace['selected_direction'][:50]}\n" output += f" 预测揭露: {trace.get('selected_score', '?')}/10\n" for d in trace.get("directions", []): marker = " ★" if d["id"] == trace["selected"] else "" output += f" [{d['id']}]{marker} {d.get('direction', '')[:40]} → 揭露={d.get('disclosure_level', '?')}/10\n" output += f" ========================\n" return output def download_all_sessions(): files = sorted(Path("sessions").glob("session_*.json")) if not files: return None return [str(f) for f in files] with gr.Blocks(title="Freud-Zero MVP") as app: gr.Markdown("# Freud-Zero MVP") gr.Markdown("精神动力学取向回应性咨询师 · 自我揭露深度追踪") with gr.Row(): btn_start = gr.Button("开始新会话", variant="primary") btn_end = gr.Button("结束会话", variant="stop") chatbot = gr.Chatbot(label="对话", height=480, type="messages") with gr.Row(): user_input = gr.Textbox(placeholder="说你想说的……", show_label=False, scale=4) btn_send = gr.Button("发送", scale=1) status_output = gr.Markdown(value="### 🧠 SESSION MONITOR\n\n> 等待开始会话…") with gr.Accordion("研究者面板", open=False): with gr.Row(): btn_view = gr.Button("查看所有会话记录") btn_download = gr.Button("下载日志文件") log_display = gr.Textbox(label="会话日志", lines=20, interactive=False) file_output = gr.File(label="日志文件") # 绑定事件 btn_start.click(start_session, outputs=[chatbot, status_output]) btn_end.click(end_session, outputs=[status_output]) btn_send.click(chat, inputs=[user_input, chatbot], outputs=[chatbot, user_input, status_output, log_display]) user_input.submit(chat, inputs=[user_input, chatbot], outputs=[chatbot, user_input, status_output, log_display]) btn_view.click(view_sessions, outputs=[log_display]) btn_download.click(download_all_sessions, outputs=[file_output]) if __name__ == "__main__": app.launch(server_name="0.0.0.0", server_port=7860, share=True)