File size: 5,640 Bytes
b7f63db
 
 
 
 
 
 
 
 
ef7c5ab
b7f63db
 
 
 
 
 
 
 
 
b1e8a83
b7f63db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23d234b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b7f63db
 
 
b1e8a83
 
b7f63db
 
 
ef7c5ab
 
 
 
 
 
 
 
b1e8a83
 
 
 
 
 
ef7c5ab
b7f63db
 
 
 
 
ef7c5ab
 
 
 
 
b1e8a83
 
 
 
 
 
 
 
 
 
 
b7f63db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23d234b
 
 
 
 
 
 
 
 
b7f63db
 
 
 
ef7c5ab
 
 
 
 
b1e8a83
 
 
 
 
 
 
 
 
 
 
ef7c5ab
b7f63db
 
 
 
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
"""
Formatter node — converts agent responses to clean markdown.

Smart passthrough: skips the LLM call if the response is already
well-formatted or very short.
"""

from __future__ import annotations

import hashlib
import logging
import re

from langchain_core.messages import HumanMessage, SystemMessage

from src.agents.config import Config
from src.agents.formatter.prompt import FORMATTER_SYSTEM_PROMPT
from src.graphs.state import AgentState
from src.graphs.utils import sanitize_handoff_phrases
from src.diagnostics.response_boundary import update_trace

logger = logging.getLogger(__name__)

# Maximum length for "short" responses that skip formatting
_SHORT_RESPONSE_THRESHOLD = 120

# Structural quality: headers/lists MUST be at the start of a line
_STRUCTURAL_MARKERS = re.compile(
    r"(^#{1,3}\s"       # headers at line start
    r"|^\s*[-*]\s"      # bullets at line start
    r"|^\d+\.\s"        # numbered list at line start
    r"|^\|.+\|$"        # table rows
    r")",
    re.MULTILINE,
)


def _already_formatted(text: str) -> bool:
    """Return True if text is properly structured markdown.

    Having **bold** alone is not enough — we need structural elements
    (headers, lists, tables) on their own lines to consider the text
    well-formatted.
    """
    if len(text) <= _SHORT_RESPONSE_THRESHOLD:
        return True
    structural_matches = _STRUCTURAL_MARKERS.findall(text)
    # Need at least 3 structural elements on proper lines
    return len(structural_matches) >= 3


def _is_safe_formatted_output(original: str, formatted: str) -> bool:
    """Return True when formatter output safely preserves substantive content.

    Formatting is presentation-only. A formatter result must never replace a
    meaningful response with empty, malformed, or catastrophically truncated
    output.
    """
    original_clean = original.strip()
    formatted_clean = formatted.strip()

    if not formatted_clean:
        return False

    # A markdown marker without header text is never a valid final response.
    if formatted_clean in {"#", "##", "###"}:
        return False

    # The formatter is instructed to preserve all information. Large content
    # loss therefore indicates a failed or truncated formatter completion.
    if len(original_clean) > _SHORT_RESPONSE_THRESHOLD:
        minimum_safe_length = max(
            _SHORT_RESPONSE_THRESHOLD,
            len(original_clean) // 2,
        )
        if len(formatted_clean) < minimum_safe_length:
            return False

    return True


def formatter_node(state: AgentState) -> dict:
    """Format the agent response as clean markdown."""
    response_text = state.get("final_response", "")
    user_id = state.get("user_id")
    conversation_id = state.get("conversation_id")
    nodes = list(state.get("nodes_executed", []))
    nodes.append("formatter_node")

    def _digest(value: str) -> str:
        return hashlib.sha256(value.encode("utf-8")).hexdigest()[:16]

    logger.info(
        "response.boundary.formatter_input length=%d sha256_16=%s",
        len(response_text),
        _digest(response_text),
    )
    formatter_trace = {
        "input": {
            "length": len(response_text),
            "sha256_16": _digest(response_text),
        },
    }

    # Always sanitize handoff phrases
    response_text = sanitize_handoff_phrases(response_text)

    # Smart passthrough — skip LLM if already formatted or very short
    if not response_text or _already_formatted(response_text):
        logger.info(
            "response.boundary.formatter_output passthrough=true length=%d sha256_16=%s",
            len(response_text),
            _digest(response_text),
        )
        formatter_trace["output"] = {
            "passthrough": True,
            "length": len(response_text),
            "sha256_16": _digest(response_text),
        }
        update_trace(
            user_id,
            conversation_id,
            section="formatter",
            value=formatter_trace,
        )
        return {
            "final_response": response_text,
            "nodes_executed": nodes,
        }

    # Use LLM to format
    try:
        from src.llm.tiers import ModelTier
        llm = Config.get_llm(model=ModelTier.FORMATTER, with_cost_tracking=True)
        result = llm.invoke([
            SystemMessage(content=FORMATTER_SYSTEM_PROMPT),
            HumanMessage(content=response_text),
        ])
        formatted = result.content if isinstance(result.content, str) else response_text
        # Final sanitization
        formatted = sanitize_handoff_phrases(formatted)

        if not _is_safe_formatted_output(response_text, formatted):
            logger.warning(
                "Formatter output failed integrity validation; using original response. "
                "original_length=%d formatted_length=%d",
                len(response_text),
                len(formatted),
            )
            formatted = response_text
    except Exception:
        logger.exception("Formatter LLM call failed; using original response.")
        formatted = response_text

    logger.info(
        "response.boundary.formatter_output passthrough=false length=%d sha256_16=%s",
        len(formatted),
        _digest(formatted),
    )
    formatter_trace["output"] = {
        "passthrough": False,
        "length": len(formatted),
        "sha256_16": _digest(formatted),
    }
    update_trace(
        user_id,
        conversation_id,
        section="formatter",
        value=formatter_trace,
    )

    return {
        "final_response": formatted,
        "nodes_executed": nodes,
    }