umairali64488 commited on
Commit
ef2ff97
·
verified ·
1 Parent(s): 1932e6e

Update app/llm_chain.py

Browse files
Files changed (1) hide show
  1. app/llm_chain.py +56 -78
app/llm_chain.py CHANGED
@@ -3,17 +3,17 @@ llm_chain.py — LangChain LCEL pipeline for multi-LLM code debugging.
3
 
4
  Flow:
5
  1. Build one LCEL chain per panel model (PANEL_PROMPT | llm | StrOutputParser)
6
- 2. Run all 3 chains concurrently with asyncio.gather (ainvoke)
7
- 3. Feed all results into the judge chain (JUDGE_PROMPT | llm | StrOutputParser)
8
- 4. Parse judge output into {reasoning, final_answer}
9
- 5. Return structured dict consumed by the FastAPI endpoint
10
  """
11
  import asyncio
 
12
  import time
13
  from typing import Any
14
 
15
  from langchain_core.output_parsers import StrOutputParser
16
- from langchain_core.runnables import RunnablePassthrough
17
 
18
  from app.config import settings
19
  from app.llm_factory import make_panel_llm, make_judge_llm
@@ -22,22 +22,30 @@ from app.prompts import PANEL_PROMPT, JUDGE_PROMPT
22
 
23
  # ── Helpers ───────────────────────────────────────────────────────────────────
24
 
25
- def _split_judge_output(raw: str) -> tuple[str, str]:
26
  """
27
- Split judge output on the markdown headers we asked for.
28
- Returns (reasoning, final_answer).
29
  """
30
- reasoning = ""
31
- final_answer = raw.strip()
32
 
33
- if "## ✅ Final Answer" in raw:
34
- parts = raw.split("## ✅ Final Answer", 1)
35
- final_answer = parts[1].strip()
36
- reasoning = parts[0].replace("## 🔍 Reasoning", "").strip()
37
- elif "## 🔍 Reasoning" in raw:
38
- reasoning = raw.split("## 🔍 Reasoning", 1)[1].strip()
 
 
 
 
39
 
40
- return reasoning, final_answer
 
 
 
 
41
 
42
 
43
  def _error_panel(model: str, label: str, error: str, latency_ms: float) -> dict:
@@ -54,25 +62,20 @@ def _error_panel(model: str, label: str, error: str, latency_ms: float) -> dict:
54
 
55
  def build_panel_chain(model: str, temperature: float):
56
  """
57
- LCEL chain for a single panel model:
58
- PANEL_PROMPT | ChatOpenAI | StrOutputParser
59
  Input: {"question": str}
60
- Output: str (model's raw markdown answer)
61
  """
62
  llm = make_panel_llm(model, temperature=temperature)
63
  return PANEL_PROMPT | llm | StrOutputParser()
64
 
65
 
66
  async def _run_panel(model: str, label: str, question: str, temperature: float) -> dict:
67
- """
68
- Invoke one panel chain, return structured result dict.
69
- Errors are caught so one failing model doesn't abort everything.
70
- """
71
  chain = build_panel_chain(model, temperature)
72
  start = time.perf_counter()
73
  try:
74
- response = await chain.ainvoke({"question": question})
75
- latency = round((time.perf_counter() - start) * 1000, 1)
76
  return {
77
  "model": model,
78
  "label": label,
@@ -89,35 +92,25 @@ async def _run_panel(model: str, label: str, question: str, temperature: float)
89
 
90
  def build_judge_chain():
91
  """
92
- LCEL chain for the Qwen3 judge:
93
- JUDGE_PROMPT | ChatOpenAI | StrOutputParser
94
- Input: {"question", "label_1", "response_1", "label_2", "response_2",
95
- "label_3", "response_3"}
96
- Output: str (full judge markdown text)
97
  """
98
  llm = make_judge_llm()
99
  return JUDGE_PROMPT | llm | StrOutputParser()
100
 
101
 
102
  async def _run_judge(question: str, panel_results: list[dict]) -> dict:
103
- """
104
- Build judge inputs from panel results, invoke the judge chain,
105
- and parse the structured output.
106
- """
107
  chain = build_judge_chain()
108
  start = time.perf_counter()
109
 
110
- # Pad missing responses so the prompt always has 3 slots
111
- padded = []
112
- for r in panel_results:
113
- padded.append(r)
114
- while len(padded) < 3:
115
  padded.append({"label": "N/A", "response": "[No response — model failed]"})
116
 
117
  def _content(r: dict) -> str:
118
- if r.get("response"):
119
- return r["response"]
120
- return f"[Error: {r.get('error', 'unknown')}]"
121
 
122
  judge_input = {
123
  "question": question,
@@ -125,55 +118,43 @@ async def _run_judge(question: str, panel_results: list[dict]) -> dict:
125
  "response_1": _content(padded[0]),
126
  "label_2": padded[1]["label"],
127
  "response_2": _content(padded[1]),
128
- "label_3": padded[2]["label"],
129
- "response_3": _content(padded[2]),
130
  }
131
 
132
  try:
133
  raw = await chain.ainvoke(judge_input)
134
  latency = round((time.perf_counter() - start) * 1000, 1)
135
- reasoning, final_answer = _split_judge_output(raw)
136
  return {
137
- "model": settings.JUDGE_MODEL,
138
- "label": settings.JUDGE_LABEL,
139
- "reasoning": reasoning,
140
- "final_answer": final_answer,
141
- "full_response": raw.strip(),
142
- "latency_ms": latency,
143
- "error": None,
144
  }
145
  except Exception as exc:
146
  latency = round((time.perf_counter() - start) * 1000, 1)
147
  return {
148
- "model": settings.JUDGE_MODEL,
149
- "label": settings.JUDGE_LABEL,
150
- "reasoning": "",
151
- "final_answer": None,
152
- "full_response": None,
153
- "latency_ms": latency,
154
- "error": str(exc),
155
  }
156
 
157
 
158
- # ── Main entry point ──────────────────────────────────────────────────────────
159
 
160
  async def debug_query(question: str, temperature: float = 0.3) -> dict[str, Any]:
161
  """
162
- Full multi-LLM pipeline:
163
- Phase 1 — query 3 panel models concurrently (asyncio.gather)
164
- Phase 2 — send all results to the Qwen3 judge
165
-
166
- Returns:
167
- {
168
- "question": str,
169
- "panel": list[dict], # one dict per model
170
- "judge": dict, # reasoning + final_answer
171
- "total_ms": float,
172
- }
173
  """
174
  total_start = time.perf_counter()
175
 
176
- # ── Phase 1: parallel panel calls ────────────────────────────────────────
177
  panel_tasks = [
178
  _run_panel(
179
  model = model,
@@ -185,14 +166,11 @@ async def debug_query(question: str, temperature: float = 0.3) -> dict[str, Any]
185
  ]
186
  panel_results: list[dict] = list(await asyncio.gather(*panel_tasks))
187
 
188
- # ── Phase 2: judge ────────────────────────────────────────────────────────
189
  judge_result = await _run_judge(question, panel_results)
190
 
191
- total_ms = round((time.perf_counter() - total_start) * 1000, 1)
192
-
193
  return {
194
  "question": question,
195
  "panel": panel_results,
196
  "judge": judge_result,
197
- "total_ms": total_ms,
198
- }
 
3
 
4
  Flow:
5
  1. Build one LCEL chain per panel model (PANEL_PROMPT | llm | StrOutputParser)
6
+ 2. Run both chains concurrently (asyncio.gather + ainvoke)
7
+ 3. Feed results into judge chain (JUDGE_PROMPT | llm | StrOutputParser)
8
+ 4. Parse judge output into {error_analysis, corrected_code}
9
+ 5. Return structured dict consumed by FastAPI
10
  """
11
  import asyncio
12
+ import re
13
  import time
14
  from typing import Any
15
 
16
  from langchain_core.output_parsers import StrOutputParser
 
17
 
18
  from app.config import settings
19
  from app.llm_factory import make_panel_llm, make_judge_llm
 
22
 
23
  # ── Helpers ───────────────────────────────────────────────────────────────────
24
 
25
+ def _parse_judge_output(raw: str) -> tuple[str, str]:
26
  """
27
+ Parse judge response into (error_analysis, corrected_code).
28
+ Splits on the exact section headers defined in JUDGE_PROMPT.
29
  """
30
+ error_analysis = ""
31
+ corrected_code = ""
32
 
33
+ if "## ✅ Corrected Code" in raw:
34
+ parts = raw.split("## ✅ Corrected Code", 1)
35
+ corrected_code = parts[1].strip()
36
+ if "## 🐛 Error Analysis" in parts[0]:
37
+ error_analysis = parts[0].split("## 🐛 Error Analysis", 1)[1].strip()
38
+ else:
39
+ error_analysis = parts[0].strip()
40
+ elif "## 🐛 Error Analysis" in raw:
41
+ error_analysis = raw.split("## 🐛 Error Analysis", 1)[1].strip()
42
+ corrected_code = raw # fallback: treat whole thing as corrected code
43
 
44
+ # Strip leading/trailing whitespace from each
45
+ error_analysis = error_analysis.strip()
46
+ corrected_code = corrected_code.strip()
47
+
48
+ return error_analysis, corrected_code
49
 
50
 
51
  def _error_panel(model: str, label: str, error: str, latency_ms: float) -> dict:
 
62
 
63
  def build_panel_chain(model: str, temperature: float):
64
  """
65
+ LCEL chain: PANEL_PROMPT | ChatOpenAI | StrOutputParser
 
66
  Input: {"question": str}
67
+ Output: str
68
  """
69
  llm = make_panel_llm(model, temperature=temperature)
70
  return PANEL_PROMPT | llm | StrOutputParser()
71
 
72
 
73
  async def _run_panel(model: str, label: str, question: str, temperature: float) -> dict:
 
 
 
 
74
  chain = build_panel_chain(model, temperature)
75
  start = time.perf_counter()
76
  try:
77
+ response = await chain.ainvoke({"question": question})
78
+ latency = round((time.perf_counter() - start) * 1000, 1)
79
  return {
80
  "model": model,
81
  "label": label,
 
92
 
93
  def build_judge_chain():
94
  """
95
+ LCEL chain: JUDGE_PROMPT | ChatOpenAI | StrOutputParser
96
+ Input: {"question", "label_1", "response_1", "label_2", "response_2"}
97
+ Output: str
 
 
98
  """
99
  llm = make_judge_llm()
100
  return JUDGE_PROMPT | llm | StrOutputParser()
101
 
102
 
103
  async def _run_judge(question: str, panel_results: list[dict]) -> dict:
 
 
 
 
104
  chain = build_judge_chain()
105
  start = time.perf_counter()
106
 
107
+ # Always provide exactly 2 slots
108
+ padded = list(panel_results)
109
+ while len(padded) < 2:
 
 
110
  padded.append({"label": "N/A", "response": "[No response — model failed]"})
111
 
112
  def _content(r: dict) -> str:
113
+ return r["response"] if r.get("response") else f"[Error: {r.get('error', 'unknown')}]"
 
 
114
 
115
  judge_input = {
116
  "question": question,
 
118
  "response_1": _content(padded[0]),
119
  "label_2": padded[1]["label"],
120
  "response_2": _content(padded[1]),
 
 
121
  }
122
 
123
  try:
124
  raw = await chain.ainvoke(judge_input)
125
  latency = round((time.perf_counter() - start) * 1000, 1)
126
+ error_analysis, corrected_code = _parse_judge_output(raw)
127
  return {
128
+ "model": settings.JUDGE_MODEL,
129
+ "label": settings.JUDGE_LABEL,
130
+ "error_analysis": error_analysis,
131
+ "corrected_code": corrected_code,
132
+ "full_response": raw.strip(),
133
+ "latency_ms": latency,
134
+ "error": None,
135
  }
136
  except Exception as exc:
137
  latency = round((time.perf_counter() - start) * 1000, 1)
138
  return {
139
+ "model": settings.JUDGE_MODEL,
140
+ "label": settings.JUDGE_LABEL,
141
+ "error_analysis": None,
142
+ "corrected_code": None,
143
+ "full_response": None,
144
+ "latency_ms": latency,
145
+ "error": str(exc),
146
  }
147
 
148
 
149
+ # ── Main entry ────────────────────────────────────────────────────────────────
150
 
151
  async def debug_query(question: str, temperature: float = 0.3) -> dict[str, Any]:
152
  """
153
+ Phase 1 — 2 panel models in parallel (asyncio.gather)
154
+ Phase 2 — StepFun Flash judge → {error_analysis, corrected_code}
 
 
 
 
 
 
 
 
 
155
  """
156
  total_start = time.perf_counter()
157
 
 
158
  panel_tasks = [
159
  _run_panel(
160
  model = model,
 
166
  ]
167
  panel_results: list[dict] = list(await asyncio.gather(*panel_tasks))
168
 
 
169
  judge_result = await _run_judge(question, panel_results)
170
 
 
 
171
  return {
172
  "question": question,
173
  "panel": panel_results,
174
  "judge": judge_result,
175
+ "total_ms": round((time.perf_counter() - total_start) * 1000, 1),
176
+ }