BPL-RAG-Fall-2025 / src /RAG /response.py
Nathan Chang
bringingn src folder back
411d917
#!/usr/bin/env python3
"""
Response generation module for RAG system.
Handles LLM-based catalog summary generation and JSON parsing.
"""
import re
import json
import time
import logging
from typing import Any, List
from pydantic import ValidationError
from langchain_core.documents import Document
from .models import CatalogResponse
def generate_catalog_summary(
llm: Any,
query: str,
context: str
) -> str:
"""
Generate a catalog-style summary using LLM.
Args:
llm: Language model instance
query: Original (or expanded) user query
context: Context string from reranked documents
Returns:
Summary string describing available catalog materials
"""
logging.info("πŸ—’οΈ Generating final LLM summary...")
start = time.time()
# Catalog-focused prompt
prompt = f"""You are a professional librarian at the Boston Public Library helping a patron find relevant materials.
IMPORTANT: You only have access to CATALOG METADATA (titles, dates, locations, subjects, collections) - NOT the actual content of documents.
Your task: Based on the catalog entries below, tell the patron which items, collections, or materials might be relevant to their query.
Guidelines:
- List the most relevant items found (titles, dates, collections)
- Mention key time periods, locations, or subjects that appear
- If results include newspapers, mention specific editions and dates
- If results include images/photographs, describe what collections they're from
- Be helpful even if results aren't perfect - describe what WAS found
- If very few relevant items, suggest broader search terms
- DO NOT make up information not in the catalog entries
- DO NOT try to answer factual questions - only describe available materials
Respond ONLY in valid JSON format:
{{
"summary": "Your response describing what catalog items are available"
}}
Example response format:
"I found several relevant items in our collection: The Dorchester Beacon newspaper has multiple editions from 1919 that likely covered major Boston events, including editions from January 11, 1919 (around the time of the molasses disaster) and several from September-November 1919 (Boston Police Strike period). These are available in the Boston Public Library Newspapers collection. I also found references to North End historical materials from this era."
Catalog Entries:
{context}
Patron's Query: {query}
"""
logging.info("πŸ“ LLM prompt for summary:\n%s", prompt[:1500])
response = llm.invoke(prompt)
logging.info("πŸ“© LLM raw response (summary): %s", response.content[:2000])
parsed = parse_json_response(response.content)
logging.info(f"βœ… Summary generated in {time.time() - start:.2f}s.")
return parsed
def parse_json_response(output: str) -> str:
"""
Parse JSON response safely and return the summary.
Automatically strips markdown code fences if present.
Args:
output: Raw LLM output string
Returns:
Parsed summary string, or raw output if parsing fails
"""
try:
# Clean and normalize the model output
output = output.strip()
if output.startswith("```"):
output = re.sub(r"^```[a-zA-Z0-9]*\n?", "", output)
output = re.sub(r"```$", "", output)
output = output.strip()
# If still not valid JSON, attempt to extract the first JSON object
if not output.startswith("{"):
match = re.search(r"\{[\s\S]*\}", output)
if match:
output = match.group(0).strip()
# Try to parse the cleaned JSON
data = json.loads(output)
parsed = CatalogResponse(**data)
return parsed.summary.strip()
except (json.JSONDecodeError, ValidationError) as e:
logging.error(f"❌ JSON parsing/validation failed: {e}")
# Fallback: return the raw output if JSON parsing fails
return output if output else "Unable to generate response."