freud-zero-mvp / main.py
Feng Chike
Revert "redesign: 深色古朴风 UI,左右分栏,serif 字体,精神动力学气质"
dcda85a
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"<details><summary>📋 评估理由</summary>\n\n{c._last_reasoning}\n\n</details>")
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)