pluto90 commited on
Commit
bb25312
Β·
verified Β·
1 Parent(s): 5ac77c8

Upload 7 files

Browse files
app/graph/nodes/evaluator.py CHANGED
@@ -1,109 +1,92 @@
1
- # # app/graph/nodes/evaluator.py
2
- # from app.core.llm_engine import llm
3
- # from app.core.prompts.evaluator_prompt import evaluator_prompt
4
- # from langchain_core.output_parsers import StrOutputParser
5
- # import json
6
-
7
- # chain = evaluator_prompt | llm | StrOutputParser()
8
-
9
-
10
- # def evaluator_node(state):
11
- # query = state.get("query")
12
- # answer = state.get("final_answer")
13
- # context = state.get("context", "")
14
-
15
- # try:
16
- # response = chain.invoke({
17
- # "query": query,
18
- # "answer": answer,
19
- # "context": context
20
- # })
21
-
22
- # # πŸ”₯ clean response (important)
23
- # response = response.strip()
24
-
25
- # # sometimes model adds ```json
26
- # if response.startswith("```"):
27
- # response = response.replace("```json", "").replace("```", "").strip()
28
-
29
- # evaluation = json.loads(response)
30
-
31
- # except Exception as e:
32
- # print("EVALUATOR ERROR β†’", e)
33
-
34
- # evaluation = {
35
- # "relevance_score": 0.5,
36
- # "context_usage": 0.5,
37
- # "hallucination": True
38
- # }
39
-
40
- # return {
41
- # **state,
42
- # "evaluation": evaluation
43
- # }
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
- # app/graph/nodes/evaluator.py
53
- from app.core.llm_engine import llm
54
- from app.core.prompts.evaluator_prompt import evaluator_prompt
55
- from langchain_core.output_parsers import StrOutputParser
56
- import json, re
57
-
58
- chain = evaluator_prompt | llm | StrOutputParser()
59
-
60
-
61
- def evaluator_node(state):
62
- query = state.get("query")
63
- answer = state.get("final_answer")
64
- context = state.get("context", "")
65
-
66
- try:
67
-
68
- response = chain.invoke({
69
- "query": query,
70
- "answer": answer,
71
- "context": context
72
- }).strip()
73
-
74
- # πŸ”₯ remove markdown/code blocks
75
- response = re.sub(r"```.*?```", "", response, flags=re.DOTALL).strip()
76
-
77
- # πŸ”₯ extract JSON only
78
- match = re.search(r"\{.*\}", response, re.DOTALL)
79
-
80
- if match:
81
- response = match.group(0)
82
-
83
- # πŸ”₯ validate JSON start
84
- if not response.startswith("{"):
85
- raise ValueError("Invalid JSON from LLM")
86
-
87
- evaluation = json.loads(response)
88
-
89
- # πŸ”₯ clamp values
90
- evaluation = {
91
- "relevance_score": min(max(evaluation.get("relevance_score", 0), 0), 1),
92
- "context_usage": min(max(evaluation.get("context_usage", 0), 0), 1),
93
- "hallucination": bool(evaluation.get("hallucination", True))
94
- }
95
-
96
- except Exception as e:
97
- print("EVALUATOR ERROR β†’", e)
98
-
99
- evaluation = {
100
- "relevance_score": 0.5,
101
- "context_usage": 0.5,
102
- "hallucination": True
103
- }
104
-
105
- return {
106
- **state,
107
- "evaluation": evaluation
108
- }
109
-
 
1
+ # app/graph/nodes/evaluator.py
2
+
3
+ from app.core.llm_engine import eval_llm
4
+ from app.core.prompts.evaluator_prompt import evaluator_prompt
5
+ from langchain_core.output_parsers import StrOutputParser
6
+ import json, re
7
+
8
+ chain = evaluator_prompt | eval_llm | StrOutputParser()
9
+
10
+
11
+ def _extract_json(text: str) -> dict:
12
+ """Robustly extract JSON from LLM response, handling thinking blocks."""
13
+
14
+ # βœ… Strip Gemini thinking/reasoning blocks
15
+ text = re.sub(r"<thinking>.*?</thinking>", "", text, flags=re.DOTALL)
16
+ text = re.sub(r"<thought>.*?</thought>", "", text, flags=re.DOTALL)
17
+
18
+ # βœ… Strip markdown code fences
19
+ text = re.sub(r"```(?:json)?", "", text)
20
+ text = text.strip()
21
+
22
+ # βœ… Greedy match β€” finds outermost { ... } correctly
23
+ # [^{}]* fails on any nested structure, use .* with DOTALL instead
24
+ match = re.search(r"\{.*\}", text, re.DOTALL)
25
+ if not match:
26
+ raise ValueError(f"No JSON found. Raw: {text[:300]}")
27
+
28
+ raw_json = match.group(0).strip()
29
+ return json.loads(raw_json)
30
+
31
+
32
+ def _fallback_evaluation():
33
+ """Explicit fallback β€” always returns a valid dict."""
34
+ return {
35
+ "relevance_score": 0.5,
36
+ "context_usage": 0.5,
37
+ "hallucination": True,
38
+ "route": "rag"
39
+ }
40
+
41
+
42
+
43
+ def evaluator_node(state):
44
+ query = state.get("query")
45
+ answer = state.get("final_answer")
46
+ context = state.get("context", "")
47
+ route = state.get("route", "general")
48
+
49
+ # βœ… Don't evaluate general answers against RAG context β€” they'll always score 0
50
+ if route == "general" or not context:
51
+ return {
52
+ **state,
53
+ "evaluation": {
54
+ "relevance_score": 1.0,
55
+ "context_usage": None, # N/A for general
56
+ "hallucination": False,
57
+ "route": "general"
58
+ }
59
+ }
60
+
61
+ try:
62
+ raw_response = chain.invoke({
63
+ "query": query,
64
+ "answer": answer,
65
+ "context": context[:600]
66
+ }).strip()
67
+
68
+ print(f"EVALUATOR RAW β†’ {raw_response[:300]}") # βœ… log first 200 chars to debug
69
+
70
+ parsed= _extract_json(raw_response)
71
+
72
+ evaluation = {
73
+ "relevance_score": round(min(max(float(parsed.get("relevance_score", 0)), 0), 1), 3),
74
+ "context_usage": round(min(max(float(parsed.get("context_usage", 0)), 0), 1), 3),
75
+ "hallucination": bool(parsed.get("hallucination", True)),
76
+ "route": "rag"
77
+ }
78
+
79
+ print(f"EVALUATOR SUCCESS β†’ {evaluation}")
80
+
81
+ # βœ… return is INSIDE try β€” only reached if no exception above
82
+ return {**state, "evaluation": evaluation}
83
+
84
+
85
+
86
+ except Exception as e:
87
+ print("EVALUATOR ERROR β†’", e)
88
+
89
+ # βœ… return is INSIDE except β€” evaluation variable always defined
90
+ return {**state, "evaluation": _fallback_evaluation()}
91
+
92
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/graph/nodes/hybrid_agent.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/graph/nodes/hybrid_agent.py
2
+ from app.core.llm_engine import llm
3
+ from langchain_core.prompts import PromptTemplate
4
+ from langchain_core.output_parsers import StrOutputParser
5
+
6
+ hybrid_prompt = PromptTemplate(
7
+ input_variables=["context", "query", "history"],
8
+ template=(
9
+ "You are a document-aware assistant.\n"
10
+ "The uploaded document has LIMITED information on this topic.\n\n"
11
+ "INSTRUCTIONS:\n"
12
+ "- Start your answer using what the document says (cite it briefly)\n"
13
+ "- Then expand with your general knowledge to give a complete answer\n"
14
+ "- Clearly separate what came from the document vs general knowledge\n"
15
+ "- Be concise and helpful\n\n"
16
+ "Conversation History:\n{history}\n\n"
17
+ "Document excerpt:\n{context}\n\n"
18
+ "Question:\n{query}\n\n"
19
+ "Answer:"
20
+ )
21
+ )
22
+
23
+ chain = hybrid_prompt | llm | StrOutputParser()
24
+
25
+ def hybrid_agent_node(state):
26
+ response = chain.invoke({
27
+ "context": state.get("context", ""),
28
+ "query": state.get("query", ""),
29
+ "history": state.get("history", "")
30
+ })
31
+ return {
32
+ **state,
33
+ "general_answer": response.strip() # synthesizer picks this up for hybrid route
34
+ }
app/graph/nodes/rag_agent.py CHANGED
@@ -23,19 +23,32 @@
23
 
24
 
25
  def rag_agent_node(state):
 
 
 
 
 
26
 
27
- print("DEBUG β†’ state received:", state)
28
 
29
  # βœ… context already comes from router now
30
- context = state.get("context")
31
- sources = state.get("sources")
 
32
 
33
- print("DEBUG β†’ context:", context[:200] if context else "EMPTY")
 
 
 
 
 
 
 
34
 
35
  return {
36
  **state,
37
- "context": context,
38
- "sources": sources
39
  }
40
 
41
 
 
23
 
24
 
25
  def rag_agent_node(state):
26
+ """
27
+ Context is already fetched by router_node.
28
+ This node exists to rerank or validate β€” keeps the graph extensible.
29
+ Right now it passes state through; add reranking here later.
30
+ """
31
 
32
+ # print("DEBUG β†’ state received:", state)
33
 
34
  # βœ… context already comes from router now
35
+ context = state.get("context", "")
36
+ sources = state.get("sources", [])
37
+ score= state.get("score", 0.0)
38
 
39
+ print(f"RAG AGENT β†’ context length: {len(context)} | score: {score:.3f}")
40
+
41
+ if not context:
42
+ # Fallback: if somehow context is empty, reroute to general
43
+ return {
44
+ **state,
45
+ "route": "general",
46
+ }
47
 
48
  return {
49
  **state,
50
+ # "context": context,
51
+ # "sources": sources
52
  }
53
 
54
 
app/graph/nodes/router.py CHANGED
@@ -1,52 +1,235 @@
1
  # app/graph/nodes/router.py
2
 
3
  from app.core.rag_service import get_rag_context
 
 
 
 
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  def router_node(state):
7
  query = state.get("query")
8
  doc_id = state.get("doc_id")
9
 
10
- # πŸ”₯ Step 1: Try retrieving context
11
- context, sources, scores = get_rag_context(query, doc_id)
12
- print("ROUTER DEBUG β†’ scores:", scores)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
- # print("ROUTER DEBUG β†’ context:", context[:100] if context else "EMPTY")
15
 
16
- # # πŸ”₯ Step 2: Decide route based on context presence
17
- # if context and len(context.strip()) > 50:
18
- # route = "rag"
19
- # else:
20
- # route = "general"
21
 
22
- # print("ROUTER DECISION β†’", route)
 
 
 
 
 
 
23
 
24
- # return {
25
- # **state,
26
- # "route": route,
27
- # "context": context, # βœ… pass forward (important)
28
- # "sources": sources
29
- # }
 
 
 
 
30
 
 
31
 
32
- # πŸ”₯ Step 1: get best score
33
- max_score = max(scores) if scores else 0
34
 
35
- # πŸ”₯ Step 2: threshold decision
36
- THRESHOLD = 0.75 # πŸ‘ˆ tune this
37
 
38
- if max_score >= THRESHOLD:
39
- route = "rag"
40
- else:
41
- route = "general"
42
- context = "" # ❗ important: clear bad context
43
 
44
- print("ROUTER DECISION β†’", route, "| score:", max_score)
45
 
46
- return {
47
- **state,
48
- "route": route,
49
- "context": context,
50
- "sources": sources,
51
- "score": max_score
52
- }
 
1
  # app/graph/nodes/router.py
2
 
3
  from app.core.rag_service import get_rag_context
4
+ from app.core.llm_engine import eval_llm # βœ… use eval_llm β€” faster, no thinking
5
+ from langchain_core.output_parsers import StrOutputParser
6
+ from langchain_core.prompts import PromptTemplate
7
+ import ast
8
 
9
+ # Query expansion prompt
10
+ expansion_prompt = PromptTemplate(
11
+ input_variables=["query"],
12
+ template=(
13
+ "Generate 3 short alternative phrasings of this question for document search.\n"
14
+ "Cover singular/plural, synonyms, and sub-concepts.\n"
15
+ "Return ONLY a Python list of strings, nothing else.\n"
16
+ "Example: ['What is an array?', 'array data structure', 'arrays in programming']\n\n"
17
+ "Question: {query}\n\n"
18
+ "List:"
19
+ )
20
+ )
21
+
22
+ # βœ… Sanity check β€” asks LLM if the retrieved context actually answers the query
23
+ relevance_check_prompt = PromptTemplate(
24
+ input_variables=["query", "context"],
25
+ template=(
26
+ "Does the following context contain enough information to answer the query?\n"
27
+ "Reply with ONLY one word: yes or no\n\n"
28
+ "Query: {query}\n\n"
29
+ "Context: {context}\n\n"
30
+ "Answer:"
31
+ )
32
+ )
33
+
34
+
35
+
36
+ expansion_chain = expansion_prompt | eval_llm | StrOutputParser()
37
+ relevance_chain = relevance_check_prompt | eval_llm | StrOutputParser()
38
+
39
+
40
+ def expand_query(query: str) -> list:
41
+ try:
42
+ raw = expansion_chain.invoke({"query": query}).strip()
43
+ expansions = ast.literal_eval(raw)
44
+ if isinstance(expansions, list):
45
+ return [query] + [q for q in expansions if q != query][:3]
46
+ except Exception as e:
47
+ print(f"QUERY EXPANSION FAILED β†’ {e}")
48
+ return [query]
49
+
50
+
51
+ def is_context_relevant(query: str, context: str) -> bool:
52
+ """LLM confirms whether retrieved context actually answers the query."""
53
+ try:
54
+ answer = relevance_chain.invoke({
55
+ "query": query,
56
+ "context": context[:800] # βœ… cap context sent to LLM β€” saves tokens
57
+ }).strip().lower()
58
+ print(f"RELEVANCE CHECK β†’ '{answer}'")
59
+ return answer.startswith("yes")
60
+ except Exception as e:
61
+ print(f"RELEVANCE CHECK FAILED β†’ {e}")
62
+ return False # βœ… fail safe: if check fails, go general
63
+
64
+
65
+ # def router_node(state):
66
+ # query = state.get("query")
67
+ # doc_id = state.get("doc_id")
68
+
69
+ # # ── Stage 1: score original query ──────────────────────────────────────
70
+ # original_context, original_sources, original_scores = get_rag_context(
71
+ # query, doc_id, top_k=3
72
+ # )
73
+ # original_max_score = max(original_scores) if original_scores else 0.0
74
+ # print(f"ORIGINAL QUERY SCORE β†’ {original_max_score:.3f}")
75
+
76
+ # HIGH_THRESHOLD = 0.70 # βœ… auto-RAG β€” very confident
77
+ # LOW_THRESHOLD = 0.50 # βœ… below this β†’ always general, no LLM check needed
78
+
79
+ # # ── Stage 2: definitive general (score too low) ─────────────────────────
80
+ # if original_max_score < LOW_THRESHOLD:
81
+ # print(f"ROUTER DECISION β†’ general | score too low: {original_max_score:.3f}")
82
+ # return {
83
+ # **state,
84
+ # "route": "general",
85
+ # "context": "",
86
+ # "sources": [],
87
+ # "score": original_max_score
88
+ # }
89
+
90
+ # # ── Stage 3: definitive RAG (score very high) ──────────────────────────
91
+ # if original_max_score >= HIGH_THRESHOLD:
92
+ # # Still expand to get more chunks, but don't need LLM sanity check
93
+ # expanded_queries = expand_query(query)
94
+ # print(f"EXPANDED QUERIES β†’ {expanded_queries}")
95
+ # all_contexts, all_scores, seen = _collect_chunks(
96
+ # expanded_queries, original_context, original_scores, doc_id
97
+ # )
98
+ # merged = "\n\n---\n\n".join(all_contexts)
99
+ # print(f"ROUTER DECISION β†’ rag (high confidence) | score: {original_max_score:.3f} | chunks: {len(all_contexts)}")
100
+ # return {
101
+ # **state,
102
+ # "route": "rag",
103
+ # "context": merged,
104
+ # "sources": all_contexts,
105
+ # "score": original_max_score
106
+ # }
107
+
108
+ # # ── Stage 4: ambiguous zone (0.50–0.70) β†’ LLM sanity check ────────────
109
+ # print(f"AMBIGUOUS SCORE β†’ {original_max_score:.3f} | running relevance check...")
110
+ # context_is_relevant = is_context_relevant(query, original_context)
111
+
112
+ # if not context_is_relevant:
113
+ # print(f"ROUTER DECISION β†’ general | LLM says context doesn't answer query")
114
+ # return {
115
+ # **state,
116
+ # "route": "general",
117
+ # "context": "",
118
+ # "sources": [],
119
+ # "score": original_max_score
120
+ # }
121
+
122
+ # # Context confirmed relevant β€” expand and collect chunks
123
+ # expanded_queries = expand_query(query)
124
+ # print(f"EXPANDED QUERIES β†’ {expanded_queries}")
125
+ # all_contexts, all_scores, seen = _collect_chunks(
126
+ # expanded_queries, original_context, original_scores, doc_id
127
+ # )
128
+ # merged = "\n\n---\n\n".join(all_contexts)
129
+ # print(f"ROUTER DECISION β†’ rag (llm confirmed) | score: {original_max_score:.3f} | chunks: {len(all_contexts)}")
130
+
131
+ # return {
132
+ # **state,
133
+ # "route": "rag",
134
+ # "context": merged,
135
+ # "sources": all_contexts,
136
+ # "score": original_max_score
137
+ # }
138
+
139
+
140
+
141
+
142
+
143
+ # app/graph/nodes/router.py
144
+ # Add a third threshold zone between general and ambiguous
145
 
146
  def router_node(state):
147
  query = state.get("query")
148
  doc_id = state.get("doc_id")
149
 
150
+ original_context, original_sources, original_scores = get_rag_context(
151
+ query, doc_id, top_k=3
152
+ )
153
+ original_max_score = max(original_scores) if original_scores else 0.0
154
+ print(f"ORIGINAL QUERY SCORE β†’ {original_max_score:.3f}")
155
+
156
+ HIGH_THRESHOLD = 0.70 # strong match β†’ RAG only
157
+ HYBRID_THRESHOLD = 0.40 # weak match β†’ hybrid (doc snippet + general knowledge)
158
+ LOW_THRESHOLD = 0.40 # below this β†’ pure general
159
+
160
+ # Pure general β€” no document relevance at all
161
+ if original_max_score < LOW_THRESHOLD:
162
+ print(f"ROUTER DECISION β†’ general | score: {original_max_score:.3f}")
163
+ return {**state, "route": "general", "context": "", "sources": [], "score": original_max_score}
164
+
165
+ # Strong match β€” full RAG
166
+ if original_max_score >= HIGH_THRESHOLD:
167
+ expanded_queries = expand_query(query)
168
+ print(f"EXPANDED QUERIES β†’ {expanded_queries}")
169
+ all_contexts, all_scores, _ = _collect_chunks(
170
+ expanded_queries, original_context, original_scores, doc_id
171
+ )
172
+ merged = "\n\n---\n\n".join(all_contexts)
173
+ print(f"ROUTER DECISION β†’ rag | score: {original_max_score:.3f} | chunks: {len(all_contexts)}")
174
+ return {**state, "route": "rag", "context": merged, "sources": all_contexts, "score": original_max_score}
175
+
176
+ # Ambiguous zone (0.40–0.70) β€” LLM sanity check first
177
+ print(f"AMBIGUOUS SCORE β†’ {original_max_score:.3f} | running relevance check...")
178
+ context_is_relevant = is_context_relevant(query, original_context)
179
+
180
+ if not context_is_relevant:
181
+ # Doc has weak overlap but context doesn't actually answer it β†’ hybrid
182
+ print(f"ROUTER DECISION β†’ hybrid | LLM says context partial")
183
+ return {
184
+ **state,
185
+ "route": "hybrid",
186
+ "context": original_context, # pass what we have β€” synthesizer will supplement
187
+ "sources": [original_context],
188
+ "score": original_max_score
189
+ }
190
+
191
+ # LLM confirmed context is relevant β€” full RAG with expansion
192
+ expanded_queries = expand_query(query)
193
+ print(f"EXPANDED QUERIES β†’ {expanded_queries}")
194
+ all_contexts, all_scores, _ = _collect_chunks(
195
+ expanded_queries, original_context, original_scores, doc_id
196
+ )
197
+ merged = "\n\n---\n\n".join(all_contexts)
198
+ print(f"ROUTER DECISION β†’ rag (confirmed) | score: {original_max_score:.3f} | chunks: {len(all_contexts)}")
199
+ return {**state, "route": "rag", "context": merged, "sources": all_contexts, "score": original_max_score}
200
+
201
+
202
+
203
 
 
204
 
205
+ def _collect_chunks(expanded_queries, original_context, original_scores, doc_id):
206
+ """Merge chunks from original + expanded queries, deduplicating by text."""
207
+ seen = set()
208
+ all_contexts = []
209
+ all_scores = []
210
 
211
+ # Seed with original results
212
+ for chunk, score in zip(original_context.split("\n\n---\n\n"), original_scores):
213
+ chunk = chunk.strip()
214
+ if chunk and chunk not in seen:
215
+ seen.add(chunk)
216
+ all_contexts.append(chunk)
217
+ all_scores.append(score)
218
 
219
+ # Add expanded query results
220
+ for q in expanded_queries[1:]:
221
+ ctx, _, scores = get_rag_context(q, doc_id, top_k=2)
222
+ if ctx:
223
+ for chunk, score in zip(ctx.split("\n\n---\n\n"), scores):
224
+ chunk = chunk.strip()
225
+ if chunk and chunk not in seen:
226
+ seen.add(chunk)
227
+ all_contexts.append(chunk)
228
+ all_scores.append(score)
229
 
230
+ return all_contexts, all_scores, seen
231
 
 
 
232
 
 
 
233
 
 
 
 
 
 
234
 
 
235
 
 
 
 
 
 
 
 
app/graph/nodes/synthesizer.py CHANGED
@@ -1,38 +1,36 @@
 
 
1
  from app.core.llm_engine import llm
2
  from app.core.prompts.rag_prompt import rag_prompt
3
  from langchain_core.output_parsers import StrOutputParser
4
 
5
- def synthesizer_node(state):
6
- query= state["query"]
7
- context= state.get("context", "")
8
- history= state.get("histroy", "")
9
 
 
 
10
  general_answer = state.get("general_answer")
11
 
12
- # If general route, skip RAG context
13
- if state.get("route") == "general":
14
  return {
15
  **state,
16
- "final_answer": general_answer or "No answer generated."
17
  }
18
 
19
- full_context= f"""
20
- Conversation History:
21
- {history}
22
 
23
- Retrieved Context:
24
- {context}
25
- """
26
-
27
-
28
 
29
  chain = rag_prompt | llm | StrOutputParser()
 
30
  answer = chain.invoke({
31
- "context": full_context,
32
- "query": query
 
33
  })
34
-
35
  return {
36
  **state,
37
  "final_answer": answer.strip()
38
- }
 
 
 
1
+ # app/graph/nodes/synthesizer.py
2
+
3
  from app.core.llm_engine import llm
4
  from app.core.prompts.rag_prompt import rag_prompt
5
  from langchain_core.output_parsers import StrOutputParser
6
 
 
 
 
 
7
 
8
+ def synthesizer_node(state):
9
+ route = state.get("route")
10
  general_answer = state.get("general_answer")
11
 
12
+ if route == ("general", "hybrid"):
 
13
  return {
14
  **state,
15
+ "final_answer": general_answer or "I couldn't find a relevant answer."
16
  }
17
 
18
+ query = state["query"]
19
+ context = state.get("context", "")
20
+ history = state.get("history", "")
21
 
 
 
 
 
 
22
 
23
  chain = rag_prompt | llm | StrOutputParser()
24
+
25
  answer = chain.invoke({
26
+ "context": context,
27
+ "query": query,
28
+ "history": history
29
  })
30
+
31
  return {
32
  **state,
33
  "final_answer": answer.strip()
34
+ }
35
+
36
+