Spaces:
Sleeping
Sleeping
| """Natural-language explanation of the recommendations (OpenAI). | |
| This is the NLP block consuming the ML output: the prompt is grounded ("light RAG") in the | |
| actual retrieved books, including the model's predicted rating, so the LLM explains *why* | |
| the numeric/semantic pipeline chose them. Two prompt variants are provided as the required | |
| NLP comparison. If no API key is set, a template fallback keeps the app functional. | |
| """ | |
| import os | |
| import pandas as pd | |
| from src import config as cfg | |
| PROMPT_VARIANTS = { | |
| "concise": ( | |
| "You are a knowledgeable librarian. In 3-4 sentences, explain to the reader why the " | |
| "books below fit their request. Be specific and honest; say so if a pick is only a " | |
| "loose match. Do not invent facts beyond the data given.\n\n" | |
| "Reader request: {query}\n\nBooks:\n{books}" | |
| ), | |
| "structured": ( | |
| "You are a knowledgeable librarian. For the reader request and the candidate books below, " | |
| "write a short note with three parts:\n" | |
| "1) Why these fit (reference concrete themes/genres).\n" | |
| "2) Possible downsides or caveats (length, pacing, or a lower predicted rating).\n" | |
| "3) One 'if you like these, also try' suggestion.\n" | |
| "Be honest and specific. Do not invent facts beyond the data given.\n\n" | |
| "Reader request: {query}\n\nBooks:\n{books}" | |
| ), | |
| } | |
| def _books_block(results: pd.DataFrame) -> str: | |
| lines = [] | |
| for _, b in results.iterrows(): | |
| pred = b.get("predicted_rating") | |
| pred_s = f"{pred:.2f}" if pred == pred else "n/a" # NaN check | |
| desc = (b.get("description") or "")[:300] | |
| lines.append( | |
| f"- {b['title']} by {b.get('authors', '?')} " | |
| f"| genres: {b.get('genre_str') or 'n/a'} " | |
| f"| actual rating: {b.get('average_rating', float('nan')):.2f} " | |
| f"| predicted: {pred_s} | match: {b.get('semantic', 0):.2f}\n" | |
| f" description: {desc}" | |
| ) | |
| return "\n".join(lines) | |
| def _fallback(query: str, results: pd.DataFrame) -> str: | |
| top = results.iloc[0] | |
| return ( | |
| f"Closest match for '{query}': **{top['title']}** by {top.get('authors', '?')} " | |
| f"(match {top.get('semantic', 0):.2f}). " | |
| "Set OPENAI_API_KEY to enable the full natural-language explanation." | |
| ) | |
| def explain_recommendation(query, results, variant="concise", model=None) -> str: | |
| api_key = os.environ.get("OPENAI_API_KEY") | |
| if not api_key: | |
| return _fallback(query, results) | |
| try: | |
| from openai import OpenAI | |
| client = OpenAI(api_key=api_key) | |
| prompt = PROMPT_VARIANTS[variant].format(query=query, books=_books_block(results)) | |
| resp = client.chat.completions.create( | |
| model=model or cfg.OPENAI_MODEL, | |
| messages=[{"role": "user", "content": prompt}], | |
| temperature=0.5, | |
| max_tokens=400, | |
| ) | |
| return resp.choices[0].message.content.strip() | |
| except Exception as e: | |
| return _fallback(query, results) + f"\n\n_(LLM error: {e})_" | |