엔진 게이트웨이 계약 고정
This commit is contained in:
parent
ebef20560e
commit
f0771db919
8 changed files with 1123 additions and 72 deletions
|
|
@ -31,6 +31,14 @@ from ..engine_client import (
|
|||
GenerateResponse,
|
||||
StreamRequest,
|
||||
)
|
||||
from ..contracts.engine_gateway import (
|
||||
ENGINE_GATEWAY_SSE_DONE,
|
||||
ENGINE_GATEWAY_SSE_ERROR,
|
||||
EngineGatewaySseDecodeError,
|
||||
StreamDoneEvent,
|
||||
StreamErrorEvent,
|
||||
StreamTokenEvent,
|
||||
)
|
||||
from . import guardrail, persona, state_machine
|
||||
from .persona import PersonaCard, PersonaStateContext
|
||||
from .state_machine import SessionState, Stage
|
||||
|
|
@ -345,31 +353,23 @@ async def run_turn_stream(
|
|||
return
|
||||
|
||||
try:
|
||||
current_event = "message"
|
||||
started = time.perf_counter()
|
||||
async for raw in engine.stream(req):
|
||||
# engine_client.stream 은 게이트웨이 SSE 의 *원시 라인*을 그대로 yield 한다.
|
||||
# 게이트웨이 프레이밍: "event: token|done|error" + "data: {...}".
|
||||
line = raw.strip()
|
||||
if line.startswith("event:"):
|
||||
current_event = line[len("event:"):].strip() or "message"
|
||||
continue
|
||||
if not line.startswith("data:"):
|
||||
continue
|
||||
|
||||
payload = _extract_sse_payload(line)
|
||||
if current_event == "error":
|
||||
detail = _payload_detail(payload, "engine stream error")
|
||||
async for packet in engine.stream_packets(req):
|
||||
if packet.event == ENGINE_GATEWAY_SSE_ERROR:
|
||||
payload = packet.payload
|
||||
detail = payload.detail if isinstance(payload, StreamErrorEvent) else "engine stream error"
|
||||
yield StreamEvent("error", {"detail": detail})
|
||||
return
|
||||
if current_event == "done":
|
||||
if isinstance(payload, dict):
|
||||
stream_meta = payload
|
||||
if packet.event == ENGINE_GATEWAY_SSE_DONE:
|
||||
payload = packet.payload
|
||||
if isinstance(payload, StreamDoneEvent):
|
||||
stream_meta = payload.model_dump()
|
||||
break
|
||||
|
||||
text_piece = _payload_text(payload)
|
||||
if text_piece is None:
|
||||
payload = packet.payload
|
||||
if not isinstance(payload, StreamTokenEvent):
|
||||
continue
|
||||
text_piece = payload.text
|
||||
accumulated += text_piece
|
||||
|
||||
# 출력 가드레일(누적 스캔) — 수단정보 발견 시 차단·재생성 신호
|
||||
|
|
@ -411,6 +411,8 @@ async def run_turn_stream(
|
|||
"cost_usd": _safe_float(stream_meta.get("cost_usd")),
|
||||
},
|
||||
)
|
||||
except EngineGatewaySseDecodeError as e:
|
||||
yield StreamEvent("error", {"detail": str(e)})
|
||||
except EngineError as e:
|
||||
yield StreamEvent("error", {"detail": str(e)})
|
||||
|
||||
|
|
@ -427,34 +429,6 @@ async def _record_llm_audit(
|
|||
return
|
||||
|
||||
|
||||
def _extract_sse_payload(raw_line: str) -> Any:
|
||||
"""게이트웨이 SSE data 라인의 JSON payload를 추출.
|
||||
|
||||
token은 {"text": "..."}이고, done/error도 JSON 객체다. 구형/테스트 fixture가
|
||||
plain text data를 보내면 문자열 그대로 반환한다.
|
||||
"""
|
||||
import json as _json
|
||||
|
||||
line = raw_line.strip()
|
||||
if not line.startswith("data:"):
|
||||
return None
|
||||
payload = line[len("data:"):].strip()
|
||||
if not payload or payload == "[DONE]":
|
||||
return None
|
||||
try:
|
||||
return _json.loads(payload)
|
||||
except _json.JSONDecodeError:
|
||||
return payload
|
||||
|
||||
|
||||
def _payload_text(payload: Any) -> Optional[str]:
|
||||
if isinstance(payload, dict) and "text" in payload:
|
||||
return str(payload["text"])
|
||||
if isinstance(payload, str):
|
||||
return payload
|
||||
return None
|
||||
|
||||
|
||||
def _optional_str(value: Any) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
|
|
@ -462,14 +436,6 @@ def _optional_str(value: Any) -> Optional[str]:
|
|||
return text or None
|
||||
|
||||
|
||||
def _payload_detail(payload: Any, fallback: str) -> str:
|
||||
if isinstance(payload, dict) and payload.get("detail"):
|
||||
return str(payload["detail"])
|
||||
if isinstance(payload, str) and payload:
|
||||
return payload
|
||||
return fallback
|
||||
|
||||
|
||||
def _safe_int(value: Any) -> int:
|
||||
try:
|
||||
return int(value or 0)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue