File size: 2,836 Bytes
00e4869
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os
import logging

from sentence_transformers import SentenceTransformer, util

logger = logging.getLogger(__name__)

EMBED_MODEL = os.getenv("EMBED_MODEL", "sentence-transformers/all-MiniLM-L6-v2")

GENSHIN_ANCHORS = [
    "Genshin Impact character build",
    "elemental reaction damage",
    "Archon quest lore",
    "artifact set bonus",
    "team composition synergy",
    "vision and gnosis",
    "Mondstadt Liyue Inazuma Sumeru Fontaine Natlan",
    "polearm sword claymore bow catalyst",
    "Pyro Hydro Cryo Electro Anemo Geo Dendro",
    "Genshin resin domain spiral abyss",
    "Hu Tao Zhongli Venti Kazuha Raiden Shogun",
    "Neuvillette Furina Arlecchino Navia Wriothesley",
    "who is this Genshin character",
    "best build for Genshin character",
    "Genshin lore story explained",
    "where to find character in Genshin",
]

INJECTION_PATTERNS = [
    "ignore previous instructions",
    "ignore your instructions",
    "you are now",
    "pretend you are",
    "act as",
    "forget everything",
    "jailbreak",
    "dan mode",
    "do anything now",
]

_embedder = None
_anchor_embeddings = None


def _get_embedder():
    global _embedder, _anchor_embeddings
    if _embedder is None:
        _embedder = SentenceTransformer(EMBED_MODEL)
        _anchor_embeddings = _embedder.encode(
            GENSHIN_ANCHORS, convert_to_tensor=True
        )
    return _embedder, _anchor_embeddings


def is_in_domain(query: str, threshold: float = 0.15) -> bool:
    try:
        embedder, anchors = _get_embedder()
        query_vec = embedder.encode(query, convert_to_tensor=True)
        score = util.cos_sim(query_vec, anchors).max().item()
        logger.info(f"Domain score: {score:.3f}")
        return score >= threshold
    except Exception as e:
        logger.warning(f"Domain check failed ({e}) — passing query through")
        return True


def has_injection(query: str) -> bool:
    q = query.lower()
    return any(p in q for p in INJECTION_PATTERNS)


def validate_input(query: str) -> tuple[bool, str]:
    query = query.strip()

    if not query:
        return False, "Query cannot be empty."

    if len(query) > 500:
        return False, "Query is too long — please keep it under 500 characters."

    if has_injection(query):
        return False, "That type of query isn't something I can help with."

    if not is_in_domain(query):
        return (
            False,
            "I'm specialized in Genshin Impact — ask me about characters, "
            "builds, lore, or elemental mechanics!",
        )

    return True, ""


def validate_output(answer: str) -> tuple[bool, str]:
    answer = answer.strip().replace("</s>", "").strip()

    if len(answer) < 10:
        return False, "I couldn't find enough context to answer that — try rephrasing."

    return True, answer