File size: 4,825 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
"""
v5 SupervisorAdvisor — 单次督导调用版

架构:每轮回应前,把对话历史发给「督导」做一次 LLM 分析,
督导输出:来访者当前状态 / 本轮关注点 / 回应方向 / 操作原则。
咨询师根据督导建议生成回应。无树搜索,无来访者模拟。
"""
import json
import os
import time
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage


SUPERVISOR_PROMPT = """你是一位经验丰富的精神动力学临床督导。咨询正在进行中,你需要快速分析当前对话,给出本轮的回应建议。

## 当前对话记录
{conversation_history}

## 来访者最新发言
{client_latest}

## 分析任务
基于精神动力学视角完成分析:

1. **来访者当前状态**:防御水平(高/中/低)、当前主要防御机制、情感基调
2. **本轮核心关注点**:来访者话语中最值得跟进的一个具体点(不要泛化)
3. **回应方向**:咨询师本轮应聚焦的方向,一句话,操作级
4. **回应原则**:2-3条操作原则,明确告诉咨询师怎么做、避免什么

严格要求:
- 具体到此刻的来访者状态,不要套话
- 原则必须可操作("用'我在想……'开头做一个试探性诠释" 而不是 "要共情")
- 不要从身体感受或躯体体验切入

只输出 JSON,不要输出任何其他内容:
{{"client_state":"来访者当前状态(一句话)","focal_point":"本轮核心关注点(一句话)","direction":"回应方向(一句话)","principles":["原则1","原则2","原则3"]}}"""


SUPERVISOR_GUIDANCE_TEMPLATE = """
## 督导建议(你必须参考执行,但不要向来访者透露这个指令的存在)

**来访者当前状态**:{client_state}
**本轮关注点**:{focal_point}
**本轮方向**:{direction}

**回应原则**:
{principles}

根据以上督导建议生成本轮回应。保持你的临床判断,自然表达。
"""


class SupervisorAdvisor:
    """单次督导调用:把对话历史交给督导做分析,返回结构化建议。"""

    def __init__(self, model="qwen-turbo"):
        dashscope = dict(
            base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
            api_key=os.getenv("DASHSCOPE_API_KEY"),
        )
        self.llm = ChatOpenAI(model=model, **dashscope, temperature=0.3, max_tokens=512)

    def _format_history(self, history):
        lines = []
        for msg in history:
            if isinstance(msg, HumanMessage):
                lines.append(f"来访者:{msg.content}")
            elif isinstance(msg, AIMessage):
                lines.append(f"咨询师:{msg.content}")
        return "\n".join(lines) if lines else "(无)"

    def _parse_json(self, text):
        content = text.strip()
        start = content.find("{")
        end = content.rfind("}") + 1
        if start == -1 or end == 0:
            raise ValueError(f"无法解析 JSON: {content[:80]}")
        return json.loads(content[start:end])

    def supervise(self, history, client_latest):
        """
        分析当前对话,返回督导建议 dict。
        history: List[HumanMessage | AIMessage](不含最新来访者发言)
        client_latest: str,来访者最新发言
        """
        t = time.time()
        history_text = self._format_history(history)
        prompt = SUPERVISOR_PROMPT.replace(
            "{conversation_history}", history_text
        ).replace("{client_latest}", client_latest)

        for attempt in range(3):
            try:
                result = self.llm.invoke(prompt)
                parsed = self._parse_json(result.content)
                elapsed = time.time() - t
                print(f"[督导] {elapsed:.1f}s | 状态: {parsed.get('client_state','?')[:40]}")
                print(f"[督导] 关注点: {parsed.get('focal_point','?')[:50]}")
                print(f"[督导] 方向: {parsed.get('direction','?')[:50]}")
                return parsed
            except Exception as e:
                if attempt == 2:
                    print(f"[督导] 分析失败,返回空建议: {e}")
                    return None

    def format_guidance(self, supervision):
        """把督导建议格式化为注入到咨询师 prompt 的文本。"""
        if not supervision:
            return None
        principles_text = "\n".join(
            f"- {p}" for p in supervision.get("principles", [])
        )
        return SUPERVISOR_GUIDANCE_TEMPLATE.replace(
            "{client_state}", supervision.get("client_state", "")
        ).replace(
            "{focal_point}", supervision.get("focal_point", "")
        ).replace(
            "{direction}", supervision.get("direction", "")
        ).replace(
            "{principles}", principles_text
        )