File size: 7,392 Bytes
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
import os
import time
import threading
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
from prompts import COUNSELOR_SYSTEM_PROMPT, STRATEGIC_GUIDANCE_TEMPLATE
from evaluator import DisclosureEvaluator
from strategic_advisor import StrategicAdvisor
from session_logger import SessionLogger
from strategy_visualizer import StrategyVisualizer


class PsychodynamicCounselor:

    def __init__(self):
        self.llm = ChatOpenAI(
            model="qwen-plus",
            base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
            api_key=os.getenv("DASHSCOPE_API_KEY"),
            temperature=0.7,
        )
        self.evaluator = DisclosureEvaluator()
        self.advisor = StrategicAdvisor()
        self.logger = SessionLogger()
        self.visualizer = StrategyVisualizer()
        self.history = [SystemMessage(content=COUNSELOR_SYSTEM_PROMPT)]
        self.turn_number = 0
        self.current_guidance = None
        self._last_disclosure_score = 1  # 跟踪当前揭露水平,供相对评分用
        self._last_dimensions = {}  # 最近一次揭露维度 A-E
        self._last_reasoning = ""  # 最近一次评估理由
        self._disclosure_history = []  # 揭露分数历史轨迹
        self._last_trace_stats = None  # 最近一次推理统计
        self._pending_trace = None  # 后台完成的战略推理结果
        self._bg_thread = None
        self._lock = threading.Lock()

    def _inject_guidance(self):
        """后台线程调用:current_guidance 已更新,无需修改 history。
        督导指令会在 respond() 调用模型时动态插入。"""
        pass

    def _run_strategic_reasoning(self, history_snapshot, current_disclosure):
        """后台线程:执行4层战略推理,完成后更新指导。"""
        print(f"[战略推理] 第{self.turn_number}轮触发,当前揭露={current_disclosure},开始后台推理...")
        try:
            best, guidance, strategic_trace = self.advisor.run(history_snapshot, current_disclosure)
            with self._lock:
                if best and best.get("score", 0) > 0:
                    self.current_guidance = {
                        "direction": guidance.get("direction", best["seed"]),
                        "principles": guidance.get("principles", []),
                        "evidence": guidance.get("evidence", ""),
                    }
                    print(f"[战略推理] 完成! 选中: {best['id']}.{best['branch']} score={best['score']} delta={best['delta']}")
                    print(f"  方向: {guidance.get('direction', '?')}")
                    for p in guidance.get("principles", []):
                        print(f"  原则: {p}")
                else:
                    print("[战略推理] 完成,但未产生有效方向建议")
                self._pending_trace = strategic_trace
                # 生成可视化报告
                self.visualizer.render(strategic_trace, self.turn_number)
        except Exception as e:
            print(f"[战略推理] 后台推理失败,跳过本轮: {e}")

    def respond(self, user_message):
        self.turn_number += 1
        self.history.append(HumanMessage(content=user_message))

        # 检查是否有后台完成的战略推理结果需要记录
        logged_trace = None
        with self._lock:
            if self._pending_trace is not None:
                logged_trace = self._pending_trace
                self._pending_trace = None

        # 评估来访者当前发言的揭露深度
        disclosure_result = self.evaluator.evaluate_disclosure(user_message)
        self._last_disclosure_score = disclosure_result["score"]
        self._last_dimensions = disclosure_result.get("dimensions", {})
        self._last_reasoning = disclosure_result.get("reasoning", "")
        self._disclosure_history.append(disclosure_result["score"])

        # 同步战略推理:先推理,再用督导结果回复
        print(f"[战略推理] 第{self.turn_number}轮,当前揭露={self._last_disclosure_score},同步推理中...")
        try:
            best, guidance, strategic_trace = self.advisor.run(
                list(self.history), self._last_disclosure_score
            )
            if best and best.get("score", 0) > 0:
                self.current_guidance = {
                    "direction": guidance.get("direction", best["seed"]),
                    "principles": guidance.get("principles", []),
                    "evidence": guidance.get("evidence", ""),
                }
                print(f"[战略推理] 完成! {best['id']}.{best['branch']} score={best['score']} delta={best['delta']}")
            logged_trace = strategic_trace
            self._last_trace_stats = {
                "total_paths": len(strategic_trace.get("candidates", [])),
                "deep_paths": len(strategic_trace.get("deep_paths", [])),
                "seeds": list(strategic_trace.get("seeds", {}).keys()),
                "selected": strategic_trace.get("selected", ""),
                "timing": strategic_trace.get("timing", {}),
                "best_score": best.get("score", 0) if best else 0,
                "best_delta": best.get("delta", 0) if best else 0,
                "predicted_disclosure": guidance.get("disclosure_level", best.get("score", "?")) if best else "?",
            }
            self.visualizer.render(strategic_trace, self.turn_number)
        except Exception as e:
            print(f"[战略推理] 推理失败,跳过: {e}")
            logged_trace = None

        # 前台模型生成回复:动态构建消息列表,督导指令插在最新用户消息之前
        if self.current_guidance:
            principles_text = "\n".join(f"- {p}" for p in self.current_guidance.get("principles", []))
            guidance_text = STRATEGIC_GUIDANCE_TEMPLATE.replace(
                "{direction}", self.current_guidance["direction"]
            ).replace("{principles}", principles_text
            ).replace("{evidence}", self.current_guidance.get("evidence", ""))
            messages_to_send = self.history[:-1] + [SystemMessage(content=guidance_text)] + [self.history[-1]]
            print(f"[前台] 第{self.turn_number}轮 | 督导方向已注入: {self.current_guidance['direction'][:40]}")
        else:
            messages_to_send = self.history
            print(f"[前台] 第{self.turn_number}轮 | 无督导指令")
        # 带重试的前台调用(防止并发限流 403)
        for _retry in range(3):
            try:
                response = self.llm.invoke(messages_to_send)
                break
            except Exception as e:
                if _retry < 2:
                    print(f"[前台] 调用失败({e}), 重试中...")
                    time.sleep(1)
                else:
                    raise
        self.history.append(AIMessage(content=response.content))

        self.logger.log_turn(
            self.turn_number,
            user_message,
            response.content,
            disclosure_result["score"],
            disclosure_result["dimensions"],
            disclosure_result["reasoning"],
            mcts_trace=logged_trace,
        )

        return response.content

    def get_session_filepath(self):
        return self.logger.get_filepath()