Update app/llm_chain.py
Browse files- 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
|
| 7 |
-
3. Feed
|
| 8 |
-
4. Parse judge output into {
|
| 9 |
-
5. Return structured dict consumed by
|
| 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
|
| 26 |
"""
|
| 27 |
-
|
| 28 |
-
|
| 29 |
"""
|
| 30 |
-
|
| 31 |
-
|
| 32 |
|
| 33 |
-
if "## ✅
|
| 34 |
-
parts
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 58 |
-
PANEL_PROMPT | ChatOpenAI | StrOutputParser
|
| 59 |
Input: {"question": str}
|
| 60 |
-
Output: str
|
| 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
|
| 75 |
-
latency
|
| 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
|
| 93 |
-
|
| 94 |
-
|
| 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 |
-
#
|
| 111 |
-
padded =
|
| 112 |
-
|
| 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 |
-
|
| 136 |
return {
|
| 137 |
-
"model":
|
| 138 |
-
"label":
|
| 139 |
-
"
|
| 140 |
-
"
|
| 141 |
-
"full_response":
|
| 142 |
-
"latency_ms":
|
| 143 |
-
"error":
|
| 144 |
}
|
| 145 |
except Exception as exc:
|
| 146 |
latency = round((time.perf_counter() - start) * 1000, 1)
|
| 147 |
return {
|
| 148 |
-
"model":
|
| 149 |
-
"label":
|
| 150 |
-
"
|
| 151 |
-
"
|
| 152 |
-
"full_response":
|
| 153 |
-
"latency_ms":
|
| 154 |
-
"error":
|
| 155 |
}
|
| 156 |
|
| 157 |
|
| 158 |
-
# ── Main entry
|
| 159 |
|
| 160 |
async def debug_query(question: str, temperature: float = 0.3) -> dict[str, Any]:
|
| 161 |
"""
|
| 162 |
-
|
| 163 |
-
|
| 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":
|
| 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 |
+
}
|