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",
        },
    )