Files

130 lines
5.4 KiB
Python

"""Pure, stateless Responses wire contract; never sends a request or loads credentials."""
import re
from app.schemas.rag import ContextRead, ParsedResponse, ResponsesOptions, ResponseUsage
from app.services.completions import (
INSTRUCTIONS,
INSUFFICIENT,
CompletionProtocolError,
canonical_json,
)
def request_body(query: str, evidence: list, options: ResponsesOptions):
body = {
"model": options.model,
"instructions": INSTRUCTIONS,
"input": [
{
"role": "user",
"content": [
{
"type": "input_text",
"text": canonical_json({"question": query, "evidence": evidence}),
}
],
}
],
"max_output_tokens": options.output_tokens,
"stream": False,
}
# DeepSeek is stateless and documents store as unsupported. Other profiles
# explicitly disable response storage; no previous_response_id is assumed.
if options.provider_profile != "deepseek":
body["store"] = False
if options.temperature is not None:
body["temperature"] = options.temperature
return body
def parse_response(payload: dict, context: ContextRead) -> ParsedResponse:
"""Parse final text output, never treating reasoning or partial text as an answer."""
try:
if (
not isinstance(context.completion, ResponsesOptions)
or context.status != "prepared"
or not context.evidence
or payload["object"] != "response"
):
raise ValueError("unexpected context or protocol")
response_id, model = payload["id"], payload["model"]
if not isinstance(response_id, str) or not response_id.strip() or len(response_id) > 256:
raise ValueError("invalid id")
if not isinstance(model, str) or not model.strip() or len(model) > 128:
raise ValueError("invalid model")
usage = (
ResponseUsage.model_validate(payload["usage"])
if payload.get("usage") is not None
else None
)
base = dict(response_id=response_id, model=model, usage=usage)
output = payload["output"]
if not isinstance(output, list):
raise ValueError("invalid output")
status = payload["status"]
if status == "failed":
error = payload["error"]
if not isinstance(error, dict) or not isinstance(error.get("code"), str):
raise ValueError("missing error")
return ParsedResponse(**base, status="failed", answer=None, citations=[])
if payload.get("error") is not None:
raise ValueError("conflicting error")
if status == "incomplete":
reason = payload["incomplete_details"]["reason"]
if reason not in {"max_output_tokens", "max_messages", "content_filter", "steered"}:
raise ValueError("unknown incomplete reason")
return ParsedResponse(
**base,
status="refused" if reason == "content_filter" else "incomplete",
answer=None,
citations=[],
)
if status != "completed" or payload.get("incomplete_details") is not None:
raise ValueError("not a completed response")
messages = []
for item in output:
if item["type"] == "reasoning":
continue
if item["type"] != "message":
raise ValueError("unsupported tool or output")
if item["role"] != "assistant" or item["status"] != "completed":
raise ValueError("incomplete assistant message")
messages.append(item)
if len(messages) != 1:
raise ValueError("expected one assistant message")
parts = messages[0]["content"]
if not isinstance(parts, list) or not parts:
raise ValueError("empty content")
texts, refused = [], False
for part in parts:
if part["type"] == "refusal":
if not isinstance(part["refusal"], str) or not part["refusal"].strip():
raise ValueError("invalid refusal")
refused = True
elif part["type"] == "output_text" and isinstance(part["text"], str):
texts.append(part["text"])
else:
raise ValueError("unsupported content")
if refused:
return ParsedResponse(**base, status="refused", answer=None, citations=[])
answer = "".join(texts).strip()
if not answer or len(answer) > 32000:
raise ValueError("invalid answer")
if answer == INSUFFICIENT:
return ParsedResponse(
**base, status="insufficient_evidence", answer=answer, citations=[]
)
markers = re.findall(r"\[[Ee][^\]\r\n]*\]", answer)
allowed = {f"[{item.citation_id}]" for item in context.evidence}
if not markers or any(marker not in allowed for marker in markers):
raise ValueError("missing or unknown citations")
return ParsedResponse(
**base,
status="answer_candidate",
answer=answer,
citations=list(dict.fromkeys(marker[1:-1] for marker in markers)),
)
except (KeyError, TypeError, ValueError, AttributeError) as exc:
raise CompletionProtocolError("模型响应格式或引用无效") from exc