File size: 2,585 Bytes
cdc87cb
 
 
d1ac4a8
cdc87cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4129a71
358bb8e
cdc87cb
 
d1ac4a8
cdc87cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""validate_chunks — pure-Python relevance scoring (no extra LLM call)."""

import re
from typing import Set, List, Tuple

from src.graph.config import CONFIDENCE_THRESHOLD, MAX_RETRIEVAL_ATTEMPTS
from src.graph.utils import traced

_STOPWORDS = {
    "a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
    "and", "or", "but", "if", "then", "of", "in", "on", "at", "to", "for",
    "with", "by", "from", "as", "what", "who", "whom", "whose", "which",
    "where", "when", "why", "how", "do", "does", "did", "can", "could",
    "should", "would", "may", "might", "i", "you", "we", "they", "it",
    "this", "that", "these", "those", "there", "here", "about",
}

_TOKEN_RE = re.compile(r"[a-zA-Z]{2,}")


def _tokens(text: str) -> Set[str]:
    return {t for token in _TOKEN_RE.findall(text or "") if (t := token.lower()) not in _STOPWORDS}


def _score(query: str, chunks: List[dict]) -> Tuple[float, dict]:
    if not chunks:
        return 0.0, {"reason": "no_chunks"}

    qtoks = _tokens(query)
    if not qtoks:
        return 0.5, {"reason": "no_query_tokens"}  # don't punish trivial queries

    joined = " ".join(c.get("content", "") for c in chunks)
    ctoks = _tokens(joined)

    overlap = len(qtoks & ctoks) / max(1, len(qtoks))   # 0..1

    avg_len = sum(len(c.get("content", "")) for c in chunks) / len(chunks)
    length_factor = min(1.0, avg_len / 400.0)            # 400 chars ≈ healthy chunk

    confidence = round(0.6 * overlap + 0.4 * length_factor, 3)
    return confidence, {
        "overlap": round(overlap, 3),
        "avg_chunk_len": round(avg_len, 1),
        "length_factor": round(length_factor, 3),
    }


@traced("validator")
def validate_chunks(state: dict) -> dict:
    query = state.get("query", "")
    chunks = state.get("chunks", [])
    attempt = state.get("retrieval_attempt", 0)

    confidence, breakdown = _score(query, chunks)
    forced = attempt >= MAX_RETRIEVAL_ATTEMPTS
    is_relevant = confidence >= CONFIDENCE_THRESHOLD or forced

    if forced and confidence < CONFIDENCE_THRESHOLD:
        reason = f"forced after {attempt} attempts (conf={confidence})"
    else:
        reason = f"score={confidence} (threshold={CONFIDENCE_THRESHOLD})"

    return {
        "confidence": confidence,
        "is_relevant": is_relevant,
        "validator_reason": reason,
        "_summary": f"conf={confidence} relevant={is_relevant}",
        "_payload": {
            "confidence": confidence,
            "is_relevant": is_relevant,
            "reason": reason,
            "breakdown": breakdown,
        },
    }