studyhub-app / studyhub /core /answer.py
parhamkhoshsolat's picture
Study features + Academy theme + threads + invite links + auto-ingest
981881b verified
Raw
History Blame Contribute Delete
3.71 kB
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)