File size: 3,705 Bytes
8db761b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
981881b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8db761b
 
 
 
 
 
 
 
 
 
 
 
 
 
981881b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from __future__ import annotations

from dataclasses import dataclass, field

from ..ingest.embedder import Embedder
from .citations import Citation, extract_citations, render_sources
from .llm import LLMClient
from .loader import RetrievalIndex
from .prompt import build_context, detect_language, system_prompt
from .retrieve import retrieve


@dataclass
class AnswerResult:
    answer: str
    sources: list[Citation]
    sources_md: str
    used_ids: list[str] = field(default_factory=list)


THREAD_RULE_EN = ("This is a continuing conversation. Cite ONLY [S#] ids that appear in the "
                  "CURRENT sources below; ids mentioned in earlier answers are not valid.")
THREAD_RULE_IT = ("Questa è una conversazione in corso. Cita SOLO gli id [S#] presenti nelle "
                  "fonti ATTUALI qui sotto; gli id citati nelle risposte precedenti non sono validi.")

_MAX_TURNS = 3        # turns of context shown to the model
_CLIP_CHARS = 600     # per-answer clip so old answers can't crowd out sources


def _conversation_block(prior: list[dict]) -> str:
    lines = []
    for t in prior[-_MAX_TURNS:]:
        a = str(t.get("a", ""))
        if len(a) > _CLIP_CHARS:
            a = a[:_CLIP_CHARS] + " …"
        lines.append(f"USER: {t.get('q', '')}\nASSISTANT: {a}")
    return "CONVERSATION SO FAR:\n" + "\n\n".join(lines)


def generate_from(query: str, scored, llm: LLMClient, token_budget: int = 4000) -> AnswerResult:
    """Build the grounded prompt from already-retrieved chunks, generate, attach citations."""
    context, id_map = build_context(scored, token_budget)
    if not id_map:
        return AnswerResult("I couldn't find anything about that in the materials.", [], "")
    text = llm.complete(system_prompt(detect_language(query)), f"SOURCES:\n{context}\n\nQUESTION: {query}")
    cites = extract_citations(text, id_map)
    return AnswerResult(answer=text, sources=cites, sources_md=render_sources(cites),
                        used_ids=[c.sid for c in cites])


def answer(query: str, index: RetrievalIndex, embedder: Embedder, llm: LLMClient,
           k: int = 6, token_budget: int = 4000, reranker=None) -> AnswerResult:
    return generate_from(query, retrieve(query, index, embedder, k=k, reranker=reranker), llm, token_budget)


def generate_in_thread(query: str, prior: list[dict], scored, llm: LLMClient,
                       token_budget: int = 4000) -> AnswerResult:
    """generate_from for an ongoing thread: same grounding, plus a clipped transcript of the
    last turns so follow-ups can resolve references; citations come only from current sources."""
    context, id_map = build_context(scored, token_budget)
    if not id_map:
        return AnswerResult("I couldn't find anything about that in the materials.", [], "")
    lang = detect_language(query)
    system = system_prompt(lang) + "\n" + (THREAD_RULE_IT if lang == "it" else THREAD_RULE_EN)
    convo = _conversation_block(prior) + "\n\n" if prior else ""
    text = llm.complete(system, f"{convo}SOURCES:\n{context}\n\nQUESTION: {query}")
    cites = extract_citations(text, id_map)
    return AnswerResult(answer=text, sources=cites, sources_md=render_sources(cites),
                        used_ids=[c.sid for c in cites])


def answer_in_thread(query: str, prior: list[dict], index: RetrievalIndex, embedder: Embedder,
                     llm: LLMClient, k: int = 6, token_budget: int = 4000, reranker=None) -> AnswerResult:
    """answer() with conversation context: retrieval still runs on the current query alone."""
    return generate_in_thread(query, prior, retrieve(query, index, embedder, k=k, reranker=reranker),
                              llm, token_budget)