File size: 10,145 Bytes
408f650
 
 
 
 
 
 
 
ed14043
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408f650
 
 
 
 
ed14043
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408f650
 
ed14043
 
408f650
 
ed14043
 
 
 
 
 
 
 
 
 
 
 
 
 
408f650
 
 
 
 
 
 
ed14043
408f650
 
 
 
ed14043
 
408f650
ed14043
408f650
ed14043
408f650
ed14043
 
 
 
 
 
 
 
408f650
ed14043
 
 
 
 
408f650
 
ed14043
 
408f650
 
 
 
 
 
 
ed14043
 
408f650
 
ed14043
 
408f650
ed14043
 
 
408f650
ed14043
408f650
 
 
 
 
 
 
 
 
 
ed14043
408f650
ed14043
408f650
ed14043
 
 
 
 
 
 
 
 
 
408f650
ed14043
408f650
ed14043
408f650
ed14043
408f650
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dcda85a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408f650
dcda85a
 
 
 
408f650
dcda85a
408f650
 
dcda85a
 
408f650
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
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)