Spaces:
Sleeping
Sleeping
GitHub Actions commited on
Commit ·
f6b36ec
1
Parent(s): 5af4b9d
Deploy 33c771f
Browse files- app/api/chat.py +1 -0
- app/models/pipeline.py +3 -0
- app/pipeline/graph.py +14 -1
- app/pipeline/nodes/rewrite_query.py +2 -0
- tests/test_graph_routing.py +10 -0
- tests/test_site_profile.py +19 -10
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.
|
|
|
|
| 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
|
| 9 |
|
| 10 |
LLMS_TXT = """# Darshan Chheda
|
| 11 |
|
|
@@ -25,14 +25,23 @@ Open to software engineering roles in the UK
|
|
| 25 |
"""
|
| 26 |
|
| 27 |
|
| 28 |
-
def
|
| 29 |
-
|
| 30 |
|
| 31 |
-
assert
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
]
|
| 36 |
-
|
| 37 |
-
assert
|
| 38 |
-
assert len(
|
|
|
|
| 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
|