GitHub Actions commited on
Commit
f6b36ec
·
1 Parent(s): 5af4b9d

Deploy 33c771f

Browse files
app/api/chat.py CHANGED
@@ -246,6 +246,7 @@ async def chat_endpoint(
246
  "interaction_id": None,
247
  "retrieval_attempts": 0,
248
  "rewritten_query": None,
 
249
  "follow_ups": [],
250
  "path": None,
251
  "query_topic": None,
 
246
  "interaction_id": None,
247
  "retrieval_attempts": 0,
248
  "rewritten_query": None,
249
+ "rewrite_changed": False,
250
  "follow_ups": [],
251
  "path": None,
252
  "query_topic": None,
app/models/pipeline.py CHANGED
@@ -90,6 +90,9 @@ class PipelineState(TypedDict):
90
  # CRAG: counts retrieve node invocations; 2 = one retry was attempted.
91
  retrieval_attempts: int
92
  rewritten_query: Optional[str]
 
 
 
93
  # Top cross-encoder score from the last retrieve call. Used by CRAG routing.
94
  top_rerank_score: Optional[float]
95
  follow_ups: list[str]
 
90
  # CRAG: counts retrieve node invocations; 2 = one retry was attempted.
91
  retrieval_attempts: int
92
  rewritten_query: Optional[str]
93
+ # Whether the last CRAG rewrite produced a different query; a retry with the same one
94
+ # can only find the same chunks.
95
+ rewrite_changed: bool
96
  # Top cross-encoder score from the last retrieve call. Used by CRAG routing.
97
  top_rerank_score: Optional[float]
98
  follow_ups: list[str]
app/pipeline/graph.py CHANGED
@@ -116,6 +116,18 @@ def route_retrieve_result(state: PipelineState) -> str:
116
  return "generate"
117
 
118
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  def build_pipeline(services: dict) -> CompiledStateGraph:
120
  graph = StateGraph(PipelineState)
121
 
@@ -155,7 +167,8 @@ def build_pipeline(services: dict) -> CompiledStateGraph:
155
 
156
  # After rewrite: go straight back to retrieve for the next attempt.
157
  # The cycle terminates because route_retrieve_result checks retrieval_attempts.
158
- graph.add_edge("rewrite_query", "retrieve")
 
159
 
160
  graph.add_edge("generate", "log_eval")
161
  graph.add_edge("log_eval", END)
 
116
  return "generate"
117
 
118
 
119
+ def route_rewrite(state: PipelineState) -> str:
120
+ """
121
+ Retry retrieval with a different query, or when nothing usable was found (a retry
122
+ then widens to the broader fallback). Otherwise, when the rewrite could not change the
123
+ query (Gemini not configured, failed, or returned it as it was), a second retrieve finds
124
+ the same chunks and only adds its latency, so the chunks already found are answered from.
125
+ """
126
+ if state.get("rewrite_changed", False) or not state.get("reranked_chunks"):
127
+ return "retry"
128
+ return "generate"
129
+
130
+
131
  def build_pipeline(services: dict) -> CompiledStateGraph:
132
  graph = StateGraph(PipelineState)
133
 
 
167
 
168
  # After rewrite: go straight back to retrieve for the next attempt.
169
  # The cycle terminates because route_retrieve_result checks retrieval_attempts.
170
+ graph.add_conditional_edges("rewrite_query", route_rewrite,
171
+ {"retry": "retrieve", "generate": "generate"})
172
 
173
  graph.add_edge("generate", "log_eval")
174
  graph.add_edge("log_eval", END)
app/pipeline/nodes/rewrite_query.py CHANGED
@@ -67,6 +67,7 @@ def make_rewrite_query_node(gemini_client: GeminiClient) -> Any:
67
  logger.debug("Gemini not configured; skipping query rewrite.")
68
  return {
69
  "rewritten_query": query,
 
70
  "retrieval_attempts": state.get("retrieval_attempts", 1) + 1,
71
  "query_embedding": None, # Force re-embed so retrieve doesn't use stale embedding
72
  }
@@ -94,6 +95,7 @@ def make_rewrite_query_node(gemini_client: GeminiClient) -> Any:
94
  return {
95
  "query": rewritten,
96
  "rewritten_query": rewritten,
 
97
  "retrieval_attempts": state.get("retrieval_attempts", 1) + 1,
98
  "query_embedding": None,
99
  }
 
67
  logger.debug("Gemini not configured; skipping query rewrite.")
68
  return {
69
  "rewritten_query": query,
70
+ "rewrite_changed": False,
71
  "retrieval_attempts": state.get("retrieval_attempts", 1) + 1,
72
  "query_embedding": None, # Force re-embed so retrieve doesn't use stale embedding
73
  }
 
95
  return {
96
  "query": rewritten,
97
  "rewritten_query": rewritten,
98
+ "rewrite_changed": rewritten != query,
99
  "retrieval_attempts": state.get("retrieval_attempts", 1) + 1,
100
  "query_embedding": None,
101
  }
tests/test_graph_routing.py CHANGED
@@ -28,3 +28,13 @@ def test_attempt_three_unrelated_low_confidence_generates() -> None:
28
  "query": "what is the weather in london",
29
  }
30
  assert route_retrieve_result(state) == "generate"
 
 
 
 
 
 
 
 
 
 
 
28
  "query": "what is the weather in london",
29
  }
30
  assert route_retrieve_result(state) == "generate"
31
+
32
+
33
+ def test_an_unchanged_rewrite_answers_from_the_chunks_already_found() -> None:
34
+ from app.pipeline.graph import route_rewrite
35
+
36
+ found = [{"text": "x", "metadata": {}}]
37
+ assert route_rewrite({"rewrite_changed": False, "reranked_chunks": found}) == "generate"
38
+ assert route_rewrite({"rewrite_changed": True, "reranked_chunks": found}) == "retry"
39
+ # Nothing usable yet: the retry is what widens to the broader fallback.
40
+ assert route_rewrite({"rewrite_changed": False, "reranked_chunks": []}) == "retry"
tests/test_site_profile.py CHANGED
@@ -5,7 +5,7 @@ ROOT = Path(__file__).resolve().parents[2]
5
  if str(ROOT) not in sys.path:
6
  sys.path.insert(0, str(ROOT))
7
 
8
- from ingestion.site_profile import profile_documents
9
 
10
  LLMS_TXT = """# Darshan Chheda
11
 
@@ -25,14 +25,23 @@ Open to software engineering roles in the UK
25
  """
26
 
27
 
28
- def test_the_page_is_one_profile_and_each_off_site_profile_its_own_document() -> None:
29
- documents = profile_documents(LLMS_TXT)
30
 
31
- assert [(d["title"], d["url"], d["slug"], d["source_type"]) for d in documents] == [
32
- ("darshanchheda.com", "https://www.darshanchheda.com/", "site-profile", "profile"),
33
- ("GitHub", "https://github.com/1337XCode", "profile-github", "contact"),
34
- ("LinkedIn", "https://linkedin.com/in/darshchheda", "profile-linkedin", "contact"),
 
 
 
 
 
 
 
 
 
35
  ]
36
- assert "Open to software engineering roles in the UK" in documents[0]["clean_content"]
37
- assert documents[2]["clean_content"].endswith("https://linkedin.com/in/darshchheda")
38
- assert len(documents[2]["clean_content"].split()) >= 8
 
5
  if str(ROOT) not in sys.path:
6
  sys.path.insert(0, str(ROOT))
7
 
8
+ from ingestion.site_profile import profile_chunks
9
 
10
  LLMS_TXT = """# Darshan Chheda
11
 
 
25
  """
26
 
27
 
28
+ def test_each_section_is_a_chunk_that_says_whose_section_it_is() -> None:
29
+ [(page, label), *_] = profile_chunks(LLMS_TXT)
30
 
31
+ assert label == "site-profile"
32
+ assert [chunk["metadata"]["section"] for chunk in page] == ["Profile", "Availability", "Links", "Pages"]
33
+ assert page[0]["text"] == "Darshan Chheda — Profile:\n> London-based software engineer."
34
+ assert page[1]["text"] == "Darshan Chheda — Availability:\nOpen to software engineering roles in the UK"
35
+ assert {chunk["metadata"]["source_type"] for chunk in page} == {"profile"}
36
+
37
+
38
+ def test_each_off_site_profile_is_a_contact_chunk_of_its_own() -> None:
39
+ [_, *profiles] = profile_chunks(LLMS_TXT)
40
+
41
+ assert [(chunks[0]["metadata"]["source_title"], chunks[0]["metadata"]["source_url"], chunks[0]["metadata"]["source_type"]) for chunks, _ in profiles] == [
42
+ ("GitHub", "https://github.com/1337XCode", "contact"),
43
+ ("LinkedIn", "https://linkedin.com/in/darshchheda", "contact"),
44
  ]
45
+ linkedin = profiles[1][0][0]["text"]
46
+ assert linkedin.endswith("https://linkedin.com/in/darshchheda")
47
+ assert len(linkedin.split()) >= 8