File size: 5,970 Bytes
fb1e53a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from __future__ import annotations

from dataclasses import dataclass
from typing import Any


MAX_HISTORY_MESSAGES = 6
VALID_CONTEXT_DECISIONS = {"standalone_new_topic", "follow_up", "ambiguous"}
VALID_CONFIDENCE_LEVELS = {"high", "medium", "low"}


@dataclass(frozen=True)
class ContextResolution:
    """Kết quả quyết định có nên dùng lịch sử chat cho câu hỏi hiện tại."""

    history_used: bool
    relevant_history: list[dict[str, str]]
    reason: str
    decision: str = "standalone_new_topic"
    confidence: str = "none"
    standalone_query: str | None = None
    needs_clarification: bool = False
    clarification_question: str | None = None
    referenced_turns: list[int] | None = None
    llm_called: bool = False
    error_type: str | None = None
    error_message: str | None = None

    def to_dict(self) -> dict[str, object]:
        return {
            "history_used": self.history_used,
            "history_message_count": len(self.relevant_history),
            "reason": self.reason,
            "decision": self.decision,
            "confidence": self.confidence,
            "standalone_query": self.standalone_query,
            "needs_clarification": self.needs_clarification,
            "clarification_question": self.clarification_question,
            "referenced_turns": self.referenced_turns or [],
            "llm_called": self.llm_called,
            "error_type": self.error_type,
            "error_message": self.error_message,
        }


def resolve_query_context(
    query: str,
    chat_history: list[dict[str, str]] | None,
    llm_payload: dict[str, Any] | None = None,
) -> ContextResolution:
    """Đọc quyết định context từ LLM thay vì tự đoán bằng danh sách từ khóa.

    Hàm này không còn hardcode cụm follow-up như "còn", "vậy", "thì sao".
    Nếu có history, LLM Context Resolver phải trả JSON để phân loại câu hiện tại.
    Code chỉ giữ vai trò kiểm tra confidence và chặn các quyết định không chắc.
    """
    cleaned_history = clean_history(chat_history)
    if not cleaned_history:
        return ContextResolution(False, [], "no_history")

    if llm_payload is None:
        return ContextResolution(
            history_used=False,
            relevant_history=[],
            reason="awaiting_llm_context_resolution",
            decision="ambiguous",
            confidence="none",
            needs_clarification=True,
            clarification_question=(
                "Bạn muốn hỏi tiếp nội dung trước đó hay đang chuyển sang một chủ đề mới?"
            ),
        )

    decision = _clean_decision(llm_payload.get("decision"))
    confidence = _clean_confidence(llm_payload.get("confidence"))
    standalone_query = _clean_optional_string(llm_payload.get("standalone_query"))
    clarification_question = _clean_optional_string(
        llm_payload.get("clarification_question")
    )
    referenced_turns = _clean_referenced_turns(llm_payload.get("referenced_turns"))
    reason = (
        _clean_optional_string(llm_payload.get("reason")) or "llm_context_resolution"
    )

    if decision == "follow_up" and confidence == "high" and standalone_query:
        return ContextResolution(
            history_used=True,
            relevant_history=cleaned_history[-MAX_HISTORY_MESSAGES:],
            reason=reason,
            decision=decision,
            confidence=confidence,
            standalone_query=standalone_query,
            referenced_turns=referenced_turns,
            llm_called=True,
        )

    if decision == "standalone_new_topic" and confidence in {"high", "medium"}:
        return ContextResolution(
            history_used=False,
            relevant_history=[],
            reason=reason,
            decision=decision,
            confidence=confidence,
            standalone_query=standalone_query,
            referenced_turns=referenced_turns,
            llm_called=True,
        )

    return ContextResolution(
        history_used=False,
        relevant_history=[],
        reason=reason,
        decision=decision,
        confidence=confidence,
        standalone_query=standalone_query,
        needs_clarification=True,
        clarification_question=clarification_question
        or "Bạn muốn hỏi tiếp nội dung trước đó hay đang chuyển sang một chủ đề mới?",
        referenced_turns=referenced_turns,
        llm_called=True,
    )


def clean_history(chat_history: list[dict[str, str]] | None) -> list[dict[str, str]]:
    if not chat_history:
        return []

    cleaned = []
    for message in chat_history:
        # Chỉ giữ role/content để prompt context không bị nhiễm metadata lạ từ frontend.
        role = str(message.get("role", "user")).strip().lower()
        content = str(message.get("content", "")).strip()
        if not content:
            continue
        if role not in {"user", "assistant"}:
            role = "user"
        cleaned.append({"role": role, "content": content})
    return cleaned


def _clean_decision(value: Any) -> str:
    decision = str(value or "").strip().lower()
    if decision not in VALID_CONTEXT_DECISIONS:
        return "ambiguous"
    return decision


def _clean_confidence(value: Any) -> str:
    confidence = str(value or "").strip().lower()
    if confidence not in VALID_CONFIDENCE_LEVELS:
        return "low"
    return confidence


def _clean_optional_string(value: Any) -> str | None:
    if value is None:
        return None
    cleaned = str(value).strip()
    return cleaned or None


def _clean_referenced_turns(value: Any) -> list[int]:
    if not isinstance(value, list):
        return []

    turns: list[int] = []
    for item in value:
        try:
            turn = int(item)
        except (TypeError, ValueError):
            continue
        if turn >= 0:
            turns.append(turn)
    return turns