File size: 14,094 Bytes
89b6a21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127bfda
 
 
89b6a21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127bfda
 
 
 
 
89b6a21
 
 
 
 
 
 
 
127bfda
 
 
89b6a21
 
 
 
 
 
 
 
 
 
127bfda
 
 
89b6a21
127bfda
 
 
 
 
 
89b6a21
127bfda
 
 
89b6a21
 
 
127bfda
 
 
89b6a21
 
 
 
 
127bfda
 
 
 
 
89b6a21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127bfda
 
 
89b6a21
 
 
 
 
 
 
127bfda
 
 
 
89b6a21
 
 
 
 
127bfda
 
 
89b6a21
 
 
 
 
127bfda
 
 
 
 
 
 
89b6a21
127bfda
 
 
 
 
89b6a21
 
 
127bfda
 
 
 
 
 
 
89b6a21
127bfda
 
 
 
 
 
89b6a21
 
 
 
 
 
 
 
127bfda
 
 
89b6a21
 
 
127bfda
 
 
 
 
 
89b6a21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127bfda
 
 
 
 
 
 
 
89b6a21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127bfda
 
 
89b6a21
 
 
 
 
 
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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
import re

from bot.byte_semantics_base import (
    is_current_events_prompt,
    is_follow_up_prompt,
    is_high_risk_current_events_prompt,
    is_serious_technical_prompt,
)
from bot.byte_semantics_constants import (
    ANALYTICAL_STYLE_TERMS,
    BRIEF_STYLE_TERMS,
    EXISTENCE_QUESTION_TERMS,
    GENERIC_RESPONSE_HINT_TERMS,
    OVERCONFIDENT_HINT_TERMS,
    PLAYFUL_STYLE_TERMS,
    QUALITY_SAFE_FALLBACK,
    QUALITY_STOPWORDS,
    QUESTION_OPENING_TERMS,
    RECOMMENDATION_HINT_TERMS,
    REWRITE_REASON_GUIDANCE,
)
from bot.byte_semantics_current_events import (
    build_server_time_anchor_instruction,
    build_verifiable_prompt,
    has_current_events_confidence_label,
    has_current_events_source_anchor,
    has_current_events_temporal_anchor,
    has_current_events_uncertainty,
    is_canonical_high_risk_fallback,
)


def build_research_priority_instruction(prompt: str) -> str:
    if not is_serious_technical_prompt(prompt):
        return ""
    return (
        "Instrucoes de pesquisa aprofundada: priorize evidencia recente (2025-2026) e fontes confiaveis "
        "(universidades, orgaos de saude, journals). "
        "Quando existir contexto em alta relacionado ao tema, incorpore esse contexto explicitamente. "
        "Se houver contribuicao relevante do Brasil no tema, mencione de forma objetiva."
    )


def build_anti_generic_contract_instruction(prompt: str) -> str:
    clean_prompt = (prompt or "").strip()
    if not clean_prompt:
        return ""
    return (
        "Contrato anti-generico: responda o caso especifico da pergunta, sem texto enciclopedico amplo. "
        "Nao use frases vagas como 'depende', 'em geral' ou 'cada caso' sem antes entregar resposta objetiva."
    )


def build_direct_answer_instruction(prompt: str) -> str:
    normalized = " ".join((prompt or "").lower().split()).strip(" ?!.,:")
    if not normalized:
        return ""
    is_question = "?" in (prompt or "")
    starts_like_question = normalized.startswith(QUESTION_OPENING_TERMS)
    if not is_question and not starts_like_question:
        return ""

    instructions = [
        "Formato de resposta obrigatorio: responda a pergunta principal na primeira linha, sem introducao."
    ]
    if normalized.startswith(EXISTENCE_QUESTION_TERMS):
        instructions.append("Se for pergunta de existencia, comece com 'Sim,' ou 'Nao,'.")
    if any(term in normalized for term in RECOMMENDATION_HINT_TERMS):
        instructions.append("Se houver opcoes concretas, cite 1 a 3 nomes diretamente relevantes.")
    instructions.append("Evite texto generico e nao desvie do foco da pergunta.")
    return " ".join(instructions)


def build_adaptive_ai_instruction(prompt: str) -> str:
    lowered = (prompt or "").lower()
    if any(term in lowered for term in BRIEF_STYLE_TERMS):
        return "Estilo de resposta: ultra objetivo, no maximo 3 linhas."
    if any(term in lowered for term in ANALYTICAL_STYLE_TERMS):
        return "Estilo de resposta: analise pratica em ate 4 linhas, com 2 ou 3 pontos claros."
    if any(term in lowered for term in PLAYFUL_STYLE_TERMS):
        return "Estilo de resposta: humor leve e inteligente, sem perder precisao factual."
    return ""


def _quality_tokenize(text: str) -> list[str]:
    tokens = re.findall(r"[a-z0-9]{3,}", (text or "").lower())
    normalized: list[str] = []
    for token in tokens:
        if len(token) > 4 and token.endswith("s"):
            token = token[:-1]
        normalized.append(token)
    return normalized


def _extract_focus_terms(prompt: str, max_terms: int = 10) -> list[str]:
    terms: list[str] = []
    for token in _quality_tokenize(prompt):
        if token in QUALITY_STOPWORDS:
            continue
        if token.isdigit() and len(token) < 4:
            continue
        if token in terms:
            continue
        terms.append(token)
        if len(terms) >= max_terms:
            break
    return terms


def _count_focus_overlap(focus_terms: list[str], answer_tokens: set[str]) -> int:
    if not focus_terms or not answer_tokens:
        return 0
    overlap = 0
    for focus_term in focus_terms:
        if focus_term in answer_tokens:
            overlap += 1
            continue
        focus_prefix = focus_term[:5]
        if len(focus_prefix) < 4:
            continue
        if any(
            token.startswith(focus_prefix) or focus_term.startswith(token[:5])
            for token in answer_tokens
            if len(token) >= 4
        ):
            overlap += 1
    return overlap


def build_quality_prompt_script(prompt: str, server_time_instruction: str | None = None) -> str:
    clean_prompt = (prompt or "").strip()
    is_current = is_current_events_prompt(clean_prompt)
    is_high_risk_current = is_high_risk_current_events_prompt(clean_prompt)
    active_server_time_instruction = (
        server_time_instruction or build_server_time_anchor_instruction()
    )
    lines = [
        "Script de qualidade obrigatorio:\n"
        "1) Responda a pergunta principal na primeira linha, sem introducao.\n"
        "2) Inclua 1 ou 2 detalhes concretos ligados ao pedido (nome, data, local, obra ou numero).\n"
        "3) Maximize densidade informacional: cada linha precisa trazer dado novo.\n"
        "4) Corte frases vagas/repetitivas que nao agregam informacao.\n"
        "5) Se houver baixa confianca, admita incerteza de forma explicita e peca 1 fonte."
    ]
    if is_current:
        lines.append(f"6) {active_server_time_instruction}")
        lines.append(
            "7) Para tema atual, separe explicitamente o que esta confirmado agora e o que ainda e rumor."
        )
        if is_high_risk_current:
            lines.append(
                "8) Para noticia/anuncio atual, inclua as linhas finais: 'Confianca: alta|media|baixa' e 'Fonte: ...'."
            )
            lines.append(
                f"9) Se faltar confirmacao robusta, use exatamente: '{QUALITY_SAFE_FALLBACK}'"
            )
        else:
            lines.append(
                f"8) Se faltar confirmacao robusta, use exatamente: '{QUALITY_SAFE_FALLBACK}'"
            )
    else:
        lines.append("6) Mantenha foco total no objeto perguntado.")
        lines.append("7) Evite qualquer afirmacao absoluta sem base verificavel.")
    lines.append(
        "10) Nao termine com pergunta aberta, exceto se faltar dado essencial para responder."
    )
    return "\n".join(lines)


def is_low_quality_answer(prompt: str, answer: str) -> tuple[bool, str]:
    clean_prompt = " ".join((prompt or "").split()).strip()
    answer_lines = [
        line.strip()
        for line in (answer or "").replace("\r\n", "\n").replace("\r", "\n").split("\n")
        if line.strip()
    ]
    if not answer_lines:
        return True, "resposta_vazia"
    clean_answer = "\n".join(answer_lines)
    compact_answer = " ".join(clean_answer.split()).strip()
    if not compact_answer:
        return True, "resposta_vazia"

    normalized_prompt = " ".join(clean_prompt.lower().split()).strip(" ?!.,:")
    lowered_answer = clean_answer.lower()
    lowered_compact_answer = compact_answer.lower()
    generic_hits = sum(1 for term in GENERIC_RESPONSE_HINT_TERMS if term in lowered_answer)
    overconfident_hits = sum(1 for term in OVERCONFIDENT_HINT_TERMS if term in lowered_answer)
    has_uncertainty = has_current_events_uncertainty(clean_answer)
    has_temporal_anchor = has_current_events_temporal_anchor(clean_answer)
    has_source_anchor = has_current_events_source_anchor(clean_answer)
    has_confidence_label = has_current_events_confidence_label(clean_answer)

    focus_terms = _extract_focus_terms(clean_prompt)
    answer_tokens = set(_quality_tokenize(compact_answer))
    overlap_count = _count_focus_overlap(focus_terms, answer_tokens)
    first_line = answer_lines[0].strip().lower()

    starts_too_generic = lowered_answer.startswith(
        ("depende", "em geral", "de forma geral", "geralmente", "na maioria dos casos")
    )
    prompt_is_complex = len(clean_prompt) >= 40
    answer_is_short = len(clean_answer) < 70
    follow_up_prompt = is_follow_up_prompt(clean_prompt)
    current_events_prompt = is_current_events_prompt(clean_prompt)
    current_events_high_risk = is_high_risk_current_events_prompt(clean_prompt)
    existence_question = normalized_prompt.startswith(EXISTENCE_QUESTION_TERMS)

    if (
        "conexao com o modelo instavel" in lowered_answer
        or "conexão com o modelo instável" in lowered_answer
    ):
        return True, "modelo_indisponivel"
    if generic_hits >= 2 and overlap_count <= 1:
        return True, "resposta_generica"
    if starts_too_generic and overlap_count <= 1:
        return True, "abertura_generica"
    if existence_question and not first_line.startswith(
        ("sim", "nao", "não", "existe", "tem", "ha ", "há ")
    ):
        return True, "resposta_existencia_sem_posicao"
    if prompt_is_complex and answer_is_short and overlap_count <= 1:
        return True, "resposta_curta_sem_substancia"
    if len(focus_terms) >= 3 and overlap_count == 0 and (generic_hits > 0 or answer_is_short):
        return True, "off_topic"
    if (
        current_events_prompt
        and not follow_up_prompt
        and len(clean_prompt) >= 18
        and not has_temporal_anchor
        and not has_uncertainty
    ):
        return True, "tema_atual_sem_ancora_temporal"
    if (
        current_events_high_risk
        and has_uncertainty
        and not is_canonical_high_risk_fallback(clean_answer)
    ):
        return True, "incerteza_fora_fallback_canonico"
    if current_events_high_risk and not follow_up_prompt and not has_confidence_label:
        return True, "tema_atual_sem_confianca_explicita"
    if (
        current_events_high_risk
        and not follow_up_prompt
        and not has_uncertainty
        and not has_source_anchor
        and len(clean_answer) >= 120
    ):
        return True, "tema_atual_sem_base_verificavel"
    if (
        current_events_prompt
        and has_uncertainty
        and "link/fonte" not in lowered_answer
        and "fonte" not in lowered_answer
    ):
        return True, "incerteza_sem_pedido_de_fonte"
    if lowered_compact_answer.endswith("?") and not has_uncertainty and not follow_up_prompt:
        return True, "termina_com_pergunta_aberta"
    if overconfident_hits > 0 and not has_temporal_anchor and not has_uncertainty:
        return True, "confianca_absoluta_sem_base"
    return False, ""


def build_quality_rewrite_prompt(
    prompt: str, draft_answer: str, reason: str, server_time_instruction: str | None = None
) -> str:
    clean_prompt = (prompt or "").strip()
    clean_draft = (draft_answer or "").strip()
    safe_reason = (reason or "qualidade_insuficiente").strip()
    active_server_time_instruction = (
        server_time_instruction or build_server_time_anchor_instruction()
    )
    reason_fix = REWRITE_REASON_GUIDANCE.get(
        safe_reason, "Ajuste a resposta para ficar objetiva, verificavel e aderente a pergunta."
    )
    return (
        f"Pergunta original: {clean_prompt}\n"
        f"Rascunho anterior reprovado ({safe_reason}): {clean_draft}\n"
        "Reescreva para chat Twitch com as regras:\n"
        f"- {active_server_time_instruction}\n"
        f"- Correcao alvo: {reason_fix}\n"
        "- Primeira linha responde direto.\n"
        "- No maximo 4 linhas.\n"
        "- Alta densidade: cada linha deve adicionar informacao nova.\n"
        "- Inclua 1 ou 2 fatos concretos e relevantes.\n"
        "- Para tema atual, diferencie confirmado agora vs rumor.\n"
        "- Nao termine com pergunta aberta.\n"
        "- Nao use frase vaga/generica.\n"
        "- Se nao der para confirmar, use exatamente: "
        f"'{QUALITY_SAFE_FALLBACK}'"
    )


def build_llm_enhanced_prompt(prompt: str, server_time_instruction: str | None = None) -> str:
    clean_prompt = (prompt or "").strip()
    if not clean_prompt:
        return ""

    serious_mode = is_serious_technical_prompt(clean_prompt)
    active_server_time_instruction = (
        server_time_instruction or build_server_time_anchor_instruction()
    )
    enriched_prompt = build_verifiable_prompt(
        clean_prompt,
        concise_mode=not serious_mode,
        server_time_instruction=active_server_time_instruction,
    )
    extra_instructions: list[str] = []
    if active_server_time_instruction not in enriched_prompt:
        extra_instructions.append(active_server_time_instruction)

    anti_generic_instruction = build_anti_generic_contract_instruction(clean_prompt)
    if anti_generic_instruction:
        extra_instructions.append(anti_generic_instruction)
    direct_answer_instruction = build_direct_answer_instruction(clean_prompt)
    if direct_answer_instruction:
        extra_instructions.append(direct_answer_instruction)

    if is_follow_up_prompt(clean_prompt):
        extra_instructions.append(
            "Instrucoes de continuidade: trate esta pergunta como follow-up e resolva referencias vagas "
            "(ex.: ele, ela, isso) usando o historico recente do chat."
        )

    research_instruction = build_research_priority_instruction(clean_prompt)
    if research_instruction:
        extra_instructions.append(research_instruction)

    if serious_mode:
        extra_instructions.append(
            "Para tema tecnico serio e relevante, responda em uma unica mensagem. "
            "Contrato obrigatorio: no maximo 4 linhas, alta densidade, sem separador de multiparte."
        )

    if not is_current_events_prompt(clean_prompt):
        adaptive_instruction = build_adaptive_ai_instruction(clean_prompt)
        if adaptive_instruction:
            extra_instructions.append(adaptive_instruction)

    quality_script = build_quality_prompt_script(
        clean_prompt, server_time_instruction=active_server_time_instruction
    )
    if quality_script:
        extra_instructions.append(quality_script)

    if not extra_instructions:
        return enriched_prompt
    return f"{enriched_prompt}\n" + "\n".join(extra_instructions)