Update src/generation.py

#1
Files changed (1) hide show
  1. src/generation.py +58 -26
src/generation.py CHANGED
@@ -15,6 +15,7 @@ CONTRACT -- do not change:
15
  answer(question: str, chunks: list[Chunk], best_score: float) -> Answer
16
  =============================================================================
17
  """
 
18
 
19
  from config import FALLBACK_MESSAGE, GENERATION_MODEL, MAX_NEW_TOKENS, RELEVANCE_THRESHOLD
20
  from src.types import Answer, Chunk
@@ -31,6 +32,53 @@ Answer:"""
31
  # instruction-tuned but small, so phrasing matters a lot. Record what you
32
  # tried and what changed; prompt iteration is your results-paper material.
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  def answer(question: str, chunks: list[Chunk], best_score: float = 1.0) -> Answer:
36
  """Generate an answer grounded in the retrieved chunks.
@@ -50,29 +98,13 @@ def answer(question: str, chunks: list[Chunk], best_score: float = 1.0) -> Answe
50
  if not chunks or best_score < RELEVANCE_THRESHOLD:
51
  return Answer(text=FALLBACK_MESSAGE, chunks=[], in_scope=False)
52
 
53
- # =====================================================================
54
- # AKSHAY: YOUR CODE GOES HERE
55
- #
56
- # Suggested steps:
57
- # 1. Lazily load GENERATION_MODEL via transformers pipeline
58
- # ("text2text-generation" for flan-t5). Cache it at module level
59
- # -- reloading per question is unusably slow.
60
- # 2. Join the chunk texts into the {context} slot of PROMPT_TEMPLATE.
61
- # Mind the context window: flan-t5-base handles ~512 tokens, so
62
- # TOP_K chunks of CHUNK_SIZE may need truncating. Coordinate with
63
- # Suyash if you need him to return fewer or shorter chunks.
64
- # 3. Generate with MAX_NEW_TOKENS.
65
- # 4. Return Answer(text=..., chunks=chunks, in_scope=True). Always
66
- # pass the chunks back -- that is what makes citation possible.
67
- #
68
- # Watch for: the model answering from its own pretraining rather than
69
- # the context. Test with a deliberately wrong context and confirm it
70
- # follows the context, not its own memory.
71
- # =====================================================================
72
- _ = (GENERATION_MODEL, MAX_NEW_TOKENS, PROMPT_TEMPLATE) # remove when implemented
73
-
74
- return Answer(
75
- text=f"[STUB ANSWER] {chunks[0].text}",
76
- chunks=chunks,
77
- in_scope=True,
78
- )
 
15
  answer(question: str, chunks: list[Chunk], best_score: float) -> Answer
16
  =============================================================================
17
  """
18
+ from transformers import pipeline
19
 
20
  from config import FALLBACK_MESSAGE, GENERATION_MODEL, MAX_NEW_TOKENS, RELEVANCE_THRESHOLD
21
  from src.types import Answer, Chunk
 
32
  # instruction-tuned but small, so phrasing matters a lot. Record what you
33
  # tried and what changed; prompt iteration is your results-paper material.
34
 
35
+ # flan-t5-base's encoder handles ~512 tokens total. Leave headroom for the
36
+ # template text + question, so budget the context block conservatively.
37
+ # If this keeps truncating chunks in practice, that's a signal to ask
38
+ # Suyash for fewer/shorter chunks rather than raising this further.
39
+ MAX_CONTEXT_TOKENS = 400
40
+
41
+ _generator = None # module-level cache -- loaded once, reused across calls
42
+
43
+
44
+ def _get_generator():
45
+ """Lazily load and cache the generation pipeline.
46
+
47
+ Reloading the model per question is unusably slow (multi-second load
48
+ every call), so this is only ever done once per process.
49
+ """
50
+ global _generator
51
+ if _generator is None:
52
+ _generator = pipeline("text2text-generation", model=GENERATION_MODEL)
53
+ return _generator
54
+
55
+
56
+ def _build_context(chunks: list[Chunk], tokenizer) -> str:
57
+ """Join chunk texts into the context block, truncating to fit the
58
+ model's context window if necessary.
59
+
60
+ Chunks are added in the order given (i.e. Suyash's ranking) and we stop
61
+ adding once we'd exceed MAX_CONTEXT_TOKENS, rather than truncating mid
62
+ chunk -- a partial chunk is more likely to mislead the model than a
63
+ dropped low-ranked one.
64
+ """
65
+ parts: list[str] = []
66
+ used_tokens = 0
67
+
68
+ for chunk in chunks:
69
+ chunk_tokens = len(tokenizer.encode(chunk.text))
70
+ if used_tokens + chunk_tokens > MAX_CONTEXT_TOKENS:
71
+ if not parts:
72
+ # Even the single best chunk is too long -- truncate it
73
+ # directly rather than returning empty context.
74
+ truncated_ids = tokenizer.encode(chunk.text)[:MAX_CONTEXT_TOKENS]
75
+ parts.append(tokenizer.decode(truncated_ids, skip_special_tokens=True))
76
+ break
77
+ parts.append(chunk.text)
78
+ used_tokens += chunk_tokens
79
+
80
+ return "\n\n".join(parts)
81
+
82
 
83
  def answer(question: str, chunks: list[Chunk], best_score: float = 1.0) -> Answer:
84
  """Generate an answer grounded in the retrieved chunks.
 
98
  if not chunks or best_score < RELEVANCE_THRESHOLD:
99
  return Answer(text=FALLBACK_MESSAGE, chunks=[], in_scope=False)
100
 
101
+ generator = _get_generator()
102
+ tokenizer = generator.tokenizer
103
+
104
+ context = _build_context(chunks, tokenizer)
105
+ prompt = PROMPT_TEMPLATE.format(context=context, question=question)
106
+
107
+ result = generator(prompt, max_new_tokens=MAX_NEW_TOKENS, do_sample=False)
108
+ text = result[0]["generated_text"].strip()
109
+
110
+ return Answer(text=text, chunks=chunks, in_scope=True)