File size: 5,880 Bytes
89b6a21
dbe6486
89b6a21
 
 
 
 
 
 
 
 
 
 
127bfda
89b6a21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dbe6486
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import re
import unicodedata

from bot.byte_semantics_constants import (
    BYTE_INTRO_TEMPLATES,
    BYTE_TRIGGER,
    BYTE_TRIGGER_PATTERN,
    COMPLEX_TECH_HINT_TERMS,
    CURRENT_EVENTS_HINT_TERMS,
    FOLLOW_UP_HINT_TERMS,
    HIGH_RISK_CURRENT_EVENTS_TERMS,
    MAX_CHAT_MESSAGE_LENGTH,
    MOVIE_FACT_SHEET_PATTERN,
    MULTIPART_SEPARATOR,
    RELEVANCE_HINT_TERMS,
    SERIOUS_TECH_TERMS,
)
from bot.logic_context import enforce_reply_limits

_intro_template_index = 0


def compact_message(text: str, max_len: int = 450) -> str:
    if len(text) <= max_len:
        return text
    head = text[: max_len - 3].rstrip()
    punctuation_positions = [head.rfind(symbol) for symbol in (".", "!", "?", ";", ":")]
    best_punctuation = max(punctuation_positions)
    if best_punctuation >= int(max_len * 0.5):
        return head[: best_punctuation + 1].strip()
    last_space = head.rfind(" ")
    if last_space >= int(max_len * 0.5):
        head = head[:last_space]
    return head.rstrip(" ,;:") + "..."


def normalize_text_for_scene(text: str, max_len: int = 120) -> str:
    compact = " ".join((text or "").split())
    if len(compact) <= max_len:
        return compact
    return compact[: max_len - 3].rstrip() + "..."


def format_chat_reply(text: str) -> str:
    normalized = str(text or "").replace(MULTIPART_SEPARATOR, "\n")
    return compact_message(
        enforce_reply_limits(normalized),
        max_len=MAX_CHAT_MESSAGE_LENGTH,
    )


def parse_byte_prompt(message_text: str) -> str | None:
    text = (message_text or "").strip()
    if not text:
        return None
    normalized_text = text.lower()
    if normalized_text in {BYTE_TRIGGER, f"!{BYTE_TRIGGER}", f"@{BYTE_TRIGGER}"}:
        return ""
    match = BYTE_TRIGGER_PATTERN.match(text)
    if not match:
        return None
    return match.group(1).strip()


def _normalize_ascii_prompt(prompt: str) -> str:
    lowered = (prompt or "").strip().lower()
    if not lowered:
        return ""
    folded = unicodedata.normalize("NFKD", lowered)
    without_accents = "".join(char for char in folded if not unicodedata.combining(char))
    return " ".join(without_accents.split())


def is_ascii_art_prompt(prompt: str) -> bool:
    normalized = _normalize_ascii_prompt(prompt)
    if not normalized:
        return False
    if "arte ascii" in normalized or "ascii art" in normalized:
        return True
    # Detecta "ascii" como palavra isolada (ex: "me faz uma ascii de ...")
    if " ascii " in normalized or normalized.startswith("ascii "):
        return True
    return False


def extract_ascii_subject(prompt: str) -> str:
    normalized = _normalize_ascii_prompt(prompt)
    if not normalized or not is_ascii_art_prompt(normalized):
        return ""

    # Remove prefixo "byte" se presente
    candidate = re.sub(r"^byte\s+", "", normalized, count=1)
    # Remove verbos de pedido
    candidate = re.sub(
        r"^(?:me\s+)?(?:faz(?:er)?|faca|gera(?:r)?|manda|quero)\s+(?:uma\s+)?",
        "",
        candidate,
        count=1,
    )
    # Remove marcadores de arte ASCII
    candidate = re.sub(r"^(?:arte\s+ascii|ascii\s+art|ascii)\b", "", candidate, count=1).strip()
    # Remove preposições
    candidate = re.sub(r"^(?:de|do|da|dos|das|sobre)\b", "", candidate, count=1).strip()
    candidate = candidate.strip(" ?!.,:;")
    if candidate in {"", "pfv", "por favor"}:
        return ""
    return candidate


def is_movie_fact_sheet_prompt(prompt: str) -> bool:
    lowered = (prompt or "").lower()
    return bool(MOVIE_FACT_SHEET_PATTERN.search(lowered))


def is_intro_prompt(prompt: str) -> bool:
    normalized = " ".join((prompt or "").lower().split()).strip(" ?!.,:")
    if not normalized:
        return False
    exact_triggers = {
        "se apresente",
        "apresente se",
        "apresente-se",
        "quem e voce",
        "quem e vc",
        "quem e o byte",
        "o que voce faz",
    }
    if normalized in exact_triggers:
        return True
    return normalized.startswith("se apresente")


def build_intro_reply() -> str:
    global _intro_template_index
    template = BYTE_INTRO_TEMPLATES[_intro_template_index % len(BYTE_INTRO_TEMPLATES)]
    _intro_template_index += 1
    return template


def is_current_events_prompt(prompt: str) -> bool:
    lowered = (prompt or "").lower()
    return any(hint in lowered for hint in CURRENT_EVENTS_HINT_TERMS)


def is_high_risk_current_events_prompt(prompt: str) -> bool:
    lowered = (prompt or "").lower()
    return any(hint in lowered for hint in HIGH_RISK_CURRENT_EVENTS_TERMS)


def is_serious_technical_prompt(prompt: str) -> bool:
    normalized = " ".join((prompt or "").lower().split()).strip(" ?!.,:")
    if len(normalized) < 24:
        return False
    has_technical_signal = any(term in normalized for term in SERIOUS_TECH_TERMS) or any(
        term in normalized for term in COMPLEX_TECH_HINT_TERMS
    )
    if not has_technical_signal:
        return False
    has_relevance_signal = any(
        term in normalized for term in RELEVANCE_HINT_TERMS
    ) or is_current_events_prompt(normalized)
    return has_relevance_signal


def is_follow_up_prompt(prompt: str) -> bool:
    normalized = " ".join((prompt or "").lower().split()).strip(" ?!.,:")
    if not normalized:
        return False
    if normalized in {"e agora", "e ai", "e aí", "e esse", "e essa", "e ele", "e ela"}:
        return True
    for term in FOLLOW_UP_HINT_TERMS:
        boundary_pattern = rf"(^|[^a-z0-9]){re.escape(term)}([^a-z0-9]|$)"
        if re.search(boundary_pattern, normalized):
            return True
    if normalized.startswith(("e ", "entao ", "então ")) and len(normalized.split()) <= 7:
        return True
    words = normalized.split()
    if len(words) <= 5 and any(word in {"isso", "ele", "ela", "esse", "essa"} for word in words):
        return True
    return False