Spaces:
Sleeping
Sleeping
File size: 8,105 Bytes
5a45328 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | """Document optimization & evaluation — SSE streaming API."""
import json
import sys
import time
from pathlib import Path
from fastapi import APIRouter, HTTPException, Query, Request
from fastapi.responses import StreamingResponse
# Ensure app dir and root are on path
APP_DIR = Path(__file__).resolve().parent.parent
ROOT_DIR = APP_DIR.parent
for p in (str(APP_DIR), str(ROOT_DIR)):
if p not in sys.path:
sys.path.insert(0, p)
from db import get_distractor_set, save_record
from config_ui import get_missing_key_message
router = APIRouter(prefix="/api/optimize", tags=["optimize"])
def _pct(before: float, after: float) -> str:
if before == 0:
return "N/A"
change = (after - before) / before * 100
return f"{change:+.1f}%"
def _optimize_stream(
query: str,
document: str,
set_id: int,
engine_llm: str,
max_rounds: int,
):
"""Synchronous generator yielding SSE events for the full optimization flow."""
from engine import run_evaluate, run_rewrite_iterative
# Validate key
msg = get_missing_key_message(engine_llm)
if msg:
yield f"data: {json.dumps({'type': 'error', 'message': msg})}\n\n"
return
# Load distractor set
ds = get_distractor_set(set_id)
if not ds:
yield f"data: {json.dumps({'type': 'error', 'message': '干扰文档集合不存在'})}\n\n"
return
distractors = json.loads(ds["distractors"])
text_list = [document.strip()] + [d for d in distractors if d.strip()]
target_id = 0
# ---- Step 1: Evaluate original (baseline) ----
yield f"data: {json.dumps({'type': 'progress', 'stage': 'baseline', 'text': '评估原始文档...'})}\n\n"
try:
vanilla_scores = run_evaluate(
query=query.strip(),
text_list=text_list,
target_id=target_id,
engine_llm=engine_llm,
)
yield f"data: {json.dumps({'type': 'baseline_complete', 'scores': vanilla_scores})}\n\n"
except ValueError as e:
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
return
except Exception as e:
yield f"data: {json.dumps({'type': 'error', 'message': f'原始文档评估失败: {e}'})}\n\n"
return
# ---- Step 2: Iterative rewrite + evaluate ----
rounds_data = []
def progress_callback(round_num: int, status: str):
"""Called by run_rewrite_iterative for each stage."""
stage_text = "改写中" if status == "rewriting" else ("优化中" if status == "refining" else "评估中")
# This callback is synchronous and called from inside the engine.
# We can't yield from here directly in a generator pattern,
# so we store the state and yield from the wrapper.
progress_callback.last = {"round": round_num, "status": status, "text": stage_text}
progress_callback.last = None
try:
# We need to run the iterative process and capture results.
# The progress_callback can't yield SSE events directly,
# so we wrap each round manually to emit SSE.
from engine import run_rewrite, refine_document
total_rounds = max_rounds
rewritten = None
# Round 1: Standard rewrite
yield f"data: {json.dumps({'type': 'progress', 'round': 1, 'total_rounds': total_rounds, 'stage': 'rewriting', 'text': f'第 1/{total_rounds} 轮:改写中...'})}\n\n"
rewritten = run_rewrite(document=document, engine_llm=engine_llm)
yield f"data: {json.dumps({'type': 'progress', 'round': 1, 'total_rounds': total_rounds, 'stage': 'evaluating', 'text': f'第 1/{total_rounds} 轮:评估中...'})}\n\n"
text_list_eval = [rewritten] + text_list[1:]
scores = run_evaluate(
query=query.strip(),
text_list=text_list_eval,
target_id=target_id,
engine_llm=engine_llm,
)
rounds_data.append({"round": 1, "text": rewritten, "scores": scores})
yield f"data: {json.dumps({'type': 'round_complete', 'round': 1, 'scores': scores, 'text': rewritten})}\n\n"
# Rounds 2+: Refine with feedback
for rnd in range(2, total_rounds + 1):
prev = rounds_data[-1]
# Check if already excellent
if all(v >= 0.9 for v in prev["scores"].values()):
break
yield f"data: {json.dumps({'type': 'progress', 'round': rnd, 'total_rounds': total_rounds, 'stage': 'refining', 'text': f'第 {rnd}/{total_rounds} 轮:优化中...'})}\n\n"
refined = refine_document(
original_doc=document,
previous_rewrite=prev["text"],
scores=prev["scores"],
query=query,
engine_llm=engine_llm,
)
yield f"data: {json.dumps({'type': 'progress', 'round': rnd, 'total_rounds': total_rounds, 'stage': 'evaluating', 'text': f'第 {rnd}/{total_rounds} 轮:评估中...'})}\n\n"
text_list_eval = [refined] + text_list[1:]
scores = run_evaluate(
query=query.strip(),
text_list=text_list_eval,
target_id=target_id,
engine_llm=engine_llm,
)
rounds_data.append({"round": rnd, "text": refined, "scores": scores})
yield f"data: {json.dumps({'type': 'round_complete', 'round': rnd, 'scores': scores, 'text': refined})}\n\n"
# Early stop
if rnd >= 3:
prev_prev = rounds_data[-2]
prev_cur = rounds_data[-1]
if prev_cur["scores"]["wordpos"] <= prev_prev["scores"]["wordpos"] * 1.02:
break
# ---- Step 3: Best result ----
best = max(rounds_data, key=lambda x: x["scores"]["wordpos"])
rewritten_text = best["text"]
optimized_scores = best["scores"]
# Save to history
save_record(
model=engine_llm,
query=query.strip(),
original_doc=document.strip(),
rewritten_doc=rewritten_text,
vanilla_scores=vanilla_scores,
optimized_scores=optimized_scores,
distractors=distractors,
)
yield f"data: {json.dumps({
'type': 'complete',
'vanilla_scores': vanilla_scores,
'optimized_scores': optimized_scores,
'rewritten_text': rewritten_text,
'rounds': rounds_data,
'improvements': {
'pos': _pct(vanilla_scores['pos'], optimized_scores['pos']),
'word': _pct(vanilla_scores['word'], optimized_scores['word']),
'wordpos': _pct(vanilla_scores['wordpos'], optimized_scores['wordpos']),
},
})}\n\n"
except ValueError as e:
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
except Exception as e:
yield f"data: {json.dumps({'type': 'error', 'message': f'优化失败: {e}'})}\n\n"
@router.get("/stream")
async def optimize_stream(
query: str = Query(..., description="用户查询"),
document: str = Query(..., description="原始文档"),
set_id: int = Query(..., description="干扰文档集合 ID"),
engine_llm: str = Query("doubao", description="引擎"),
max_rounds: int = Query(2, ge=1, le=5, description="最大迭代轮数"),
):
"""SSE endpoint — stream optimization + evaluation progress."""
# Validate
if not query.strip():
raise HTTPException(status_code=400, detail="查询不能为空")
if not document.strip():
raise HTTPException(status_code=400, detail="文档不能为空")
if max_rounds < 1 or max_rounds > 5:
raise HTTPException(status_code=400, detail="迭代轮数需要 1-5")
return StreamingResponse(
_optimize_stream(query.strip(), document.strip(), set_id, engine_llm, max_rounds),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
|