summarizer.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. """
  2. summarizer.py — the ONLY module that calls an LLM (Groq/Llama 4).
  3. Generates book-level and chapter-level summaries.
  4. Keeps prompts tight to minimise token spend.
  5. """
  6. from __future__ import annotations
  7. import logging
  8. from groq import Groq
  9. from .config import cfg
  10. log = logging.getLogger(__name__)
  11. _client: Groq | None = None
  12. def _get_client() -> Groq:
  13. global _client
  14. if _client is None:
  15. _client = Groq(api_key=cfg.groq_api_key)
  16. return _client
  17. def _call(prompt: str, max_tokens: int = 512) -> str:
  18. """Single Groq call. Returns text response."""
  19. response = _get_client().chat.completions.create(
  20. model=cfg.groq_model,
  21. messages=[{"role": "user", "content": prompt}],
  22. max_tokens=max_tokens,
  23. temperature=0.3, # low temp = factual, consistent summaries
  24. )
  25. return response.choices[0].message.content.strip()
  26. # ── Public API ─────────────────────────────────────────────────────────────────
  27. def summarize_book(title: str, chapter_summaries: list[str]) -> str:
  28. """
  29. Generate a high-level book summary from the chapter summaries.
  30. Input is cheap: we only send summaries, not raw text.
  31. """
  32. joined = "\n\n".join(
  33. f"[Section {i+1}]: {s}" for i, s in enumerate(chapter_summaries)
  34. )
  35. prompt = (
  36. f'You are summarizing the book "{title}".\n'
  37. f"Below are summaries of each chapter/section.\n"
  38. f"Write a concise overall summary (4-6 sentences) covering the main thesis, "
  39. f"key ideas, and conclusions. Be factual and dense — no filler.\n\n"
  40. f"{joined}"
  41. )
  42. log.info("Generating book summary for: %s", title)
  43. return _call(prompt, max_tokens=400)
  44. def summarize_chapter(
  45. book_title: str,
  46. chapter_title: str,
  47. chapter_text: str,
  48. max_input_chars: int = 6000,
  49. ) -> str:
  50. """
  51. Summarize a single chapter. Truncates input to keep token cost low.
  52. 6000 chars ≈ ~1500 tokens — well within Llama 4 context.
  53. """
  54. # Truncate raw text to control input tokens
  55. truncated = chapter_text[:max_input_chars]
  56. if len(chapter_text) > max_input_chars:
  57. truncated += "\n[... text truncated for summary ...]"
  58. prompt = (
  59. f'From the book "{book_title}", summarize the chapter "{chapter_title}".\n'
  60. f"Write 3-5 sentences covering the key points, arguments, and conclusions. "
  61. f"Be specific and factual.\n\n"
  62. f"{truncated}"
  63. )
  64. log.debug("Summarizing chapter: %s", chapter_title)
  65. return _call(prompt, max_tokens=300)
  66. def summarize_flat_document(title: str, full_text: str, max_input_chars: int = 8000) -> str:
  67. """
  68. Summarize a flat (unstructured) document.
  69. For long docs, summarizes the first portion — sufficient for most reference material.
  70. """
  71. truncated = full_text[:max_input_chars]
  72. if len(full_text) > max_input_chars:
  73. truncated += "\n[... text truncated for summary ...]"
  74. prompt = (
  75. f'Summarize the following document titled "{title}".\n'
  76. f"Write 4-6 sentences covering the main topic, key points, and conclusions. "
  77. f"Be specific and factual.\n\n"
  78. f"{truncated}"
  79. )
  80. log.info("Summarizing flat document: %s", title)
  81. return _call(prompt, max_tokens=400)