04-knowledge-retrieval-agent / src /answer_generation.py
FlyingNunchucks's picture
Gate answers on retrieval evidence strength
db71234
Raw History Blame Contribute Delete
4.29 kB
from typing import Dict, List
import re
from src.retrieval import assess_evidence
def clean_markdown(text: str) -> str:
text = text.replace("\n", " ")
text = re.sub(r"```.*?```", " ", text, flags=re.DOTALL)
text = re.sub(r"#+\s*", "", text)
text = re.sub(r"[-*+]\s+", "", text)
text = re.sub(r"\[(.*?)\]\(.*?\)", r"\1", text)
text = re.sub(r"\*\*(.*?)\*\*", r"\1", text)
text = re.sub(r"\*(.*?)\*", r"\1", text)
text = re.sub(r"\s{2,}", " ", text)
return text.strip()
def question_terms(question: str) -> List[str]:
cleaned = re.sub(r"[^a-zA-Z0-9 ]", " ", question.lower())
tokens = [token for token in cleaned.split() if len(token) > 2]
stop_words = {
"what", "when", "where", "which", "who", "why", "how",
"the", "and", "for", "that", "this", "from", "with", "about",
"can", "use", "uses", "using", "are", "is", "of",
}
return [token for token in tokens if token not in stop_words]
def extract_sentences(text: str) -> List[str]:
cleaned = clean_markdown(text)
parts = re.split(r"(?<=[.!?])\s+", cleaned)
sentences = [part.strip() for part in parts if len(part.strip()) > 20]
return sentences
def score_sentence(sentence: str, terms: List[str]) -> float:
lower = sentence.lower()
term_matches = sum(1 for term in terms if term in lower)
length_bonus = min(len(sentence.split()) / 25, 1)
return term_matches * 2 + length_bonus
def extract_relevant_sentences(text: str, question: str, max_sentences: int = 2) -> List[str]:
sentences = extract_sentences(text)
if not sentences:
return []
terms = question_terms(question)
scored = [(score_sentence(sentence, terms), sentence) for sentence in sentences]
scored.sort(key=lambda pair: (pair[0], len(pair[1].split())), reverse=True)
chosen = []
for _, sentence in scored:
if len(chosen) >= max_sentences:
break
if sentence not in chosen:
chosen.append(sentence)
return chosen or sentences[:max_sentences]
def estimate_confidence(results: List[Dict]) -> str:
evidence = assess_evidence(results)
if evidence["status"] == "sufficient":
return "High enough to answer from the approved corpus"
if evidence["status"] == "limited":
return "Limited — answer should be qualified"
return "Insufficient — abstain"
def build_evidence_answer(question: str, results: List[Dict]) -> str:
evidence = assess_evidence(results)
if evidence["status"] == "insufficient":
return (
"**Evidence status: Insufficient**\n\n"
"I could not find a strong enough match in the approved document collection to answer this question reliably. "
"I will not fill the gap from model memory or outside knowledge."
)
sorted_results = sorted(results, key=lambda item: item.get("hybrid_score", 0), reverse=True)
top_results = sorted_results[:3]
if evidence["status"] == "limited":
lead = (
"**Evidence status: Limited**\n\n"
"The corpus contains potentially relevant material, but the retrieval match is weak. Treat this as a qualified evidence extract rather than a confident answer."
)
else:
lead = "**Evidence status: Sufficient retrieval match**"
answer_lines = [
lead,
"",
"Based only on the retrieved evidence:",
"",
]
cited_sentences = 0
for item in top_results:
chunk_num = item.get("chunk_number", "?")
source = item.get("source", "unknown source")
sentences = extract_relevant_sentences(item.get("text", ""), question, max_sentences=1)
for sentence in sentences:
answer_lines.append(f"- {sentence} **[{source} · chunk {chunk_num}]**")
cited_sentences += 1
if cited_sentences == 0:
answer_lines.append("- No concise answer sentence could be extracted from the retrieved chunks.")
answer_lines.extend(["", "**Evidence used**", ""])
for item in top_results:
answer_lines.append(
f"- {item.get('source', 'unknown source')} · chunk {item.get('chunk_number', '?')} · hybrid score {item.get('hybrid_score', 0):.3f}"
)
return "\n".join(answer_lines)