# ------------------------------- # 4. Answerer # ------------------------------- from typing import List from pydantic import BaseModel from openai import OpenAI from modules.retriever import retrieve_videos # ------------------------------- # Structured Output Classes # ------------------------------- class VideoItem(BaseModel): video_id: str title: str channel: str description: str class LLMAnswer(BaseModel): answer_text: str top_videos: List[VideoItem] # ------------------------------- # Main Function # ------------------------------- def answer_query(query: str, collection, top_k: int = 5) -> LLMAnswer: """ Answer a user query using YouTube video metadata. Returns an LLMAnswer object with textual answer + list of videos. """ results = retrieve_videos(query, collection, top_k=top_k) if not results: return LLMAnswer(answer_text="No relevant videos found.", top_videos=[]) # Build context lines for the LLM context_lines = [] for r in results: if not isinstance(r, dict): continue vid_id = r.get("video_id", "") title = r.get("video_title") or r.get("title", "") channel = r.get("channel") or r.get("channel_title", "") description = r.get("description", "") context_lines.append( f"- {title} ({channel}) (https://youtube.com/watch?v={vid_id})\n description: {description}" ) context_text = "\n".join(context_lines) # Call LLM with structured output client = OpenAI() response = client.chat.completions.parse( model="gpt-4o-mini", messages=[ { "role": "system", "content": ( "You are a helpful assistant that answers questions using YouTube video metadata. " "Return your response strictly as the LLMAnswer class, including 'answer_text' and a list of **only the most relevant** 'top_videos'.\n" "- `answer_text` MUST be very short and concise in natural language (max 1–2 sentences).\n" "- Use `top_videos` to include only the 2–3 most relevant items from context.\n" "- Do not include all items unless all are clearly relevant.\n" ), }, { "role": "user", "content": f"Question: {query}\n\nCandidate videos:\n{context_text}\n\nPick only the relevant ones.", }, ], response_format=LLMAnswer, ) llm_answer = response.choices[0].message.parsed answer_text = llm_answer.answer_text video_html = build_video_html(llm_answer.top_videos) return answer_text, video_html def build_video_html(videos: list[VideoItem]) -> str: """Build a clean HTML table from top_videos.""" if not videos: return "
No relevant videos found.
" html = """Description | Watch |
---|---|
{v.description} | {embed_html} |