mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-07-29 20:34:59 +08:00
feat(agent): implement streaming responses for online Agent and enhance tool execution feedback
This commit is contained in:
+116
-31
@@ -63,6 +63,7 @@ type ResponseApiPayload = {
|
||||
code?: number;
|
||||
msg?: string;
|
||||
};
|
||||
type ResponseStreamState = { buffer: string; text: string; payload?: ResponseApiPayload; error?: string };
|
||||
|
||||
type ImageApiResponse = {
|
||||
data?: Array<Record<string, unknown>>;
|
||||
@@ -263,6 +264,108 @@ function parseToolResponse(payload: ResponseApiPayload): ToolResponseResult {
|
||||
return { content, toolCalls };
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function responseErrorMessage(value: unknown) {
|
||||
if (!isRecord(value)) return "";
|
||||
const error = isRecord(value.error) ? value.error : undefined;
|
||||
const response = isRecord(value.response) ? value.response : undefined;
|
||||
const responseError = response && isRecord(response.error) ? response.error : undefined;
|
||||
return stringValue(value.msg) || stringValue(error?.message) || stringValue(responseError?.message);
|
||||
}
|
||||
|
||||
function stringValue(value: unknown) {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function validateResponsePayload(payload: ResponseApiPayload) {
|
||||
if (typeof payload.code === "number" && payload.code !== 0) throw new Error(payload.msg || "请求失败");
|
||||
if (payload.error?.message) throw new Error(payload.error.message);
|
||||
}
|
||||
|
||||
async function readFetchError(response: Response, fallback: string) {
|
||||
const text = await response.text();
|
||||
if (!text) return readStatusError(response.status, fallback);
|
||||
try {
|
||||
return responseErrorMessage(JSON.parse(text)) || readStatusError(response.status, fallback);
|
||||
} catch {
|
||||
return text.slice(0, 300) || readStatusError(response.status, fallback);
|
||||
}
|
||||
}
|
||||
|
||||
function consumeResponseStreamBlock(block: string, state: ResponseStreamState, onDelta?: (text: string) => void) {
|
||||
const data = block
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.startsWith("data:"))
|
||||
.map((line) => line.slice(5).replace(/^ /, ""))
|
||||
.join("\n")
|
||||
.trim();
|
||||
if (!data || data === "[DONE]") return;
|
||||
const event = JSON.parse(data) as Record<string, unknown>;
|
||||
const type = stringValue(event.type);
|
||||
const errorMessage = responseErrorMessage(event);
|
||||
if (errorMessage) state.error = errorMessage;
|
||||
if (type === "response.output_text.delta" && typeof event.delta === "string") {
|
||||
state.text += event.delta;
|
||||
onDelta?.(state.text);
|
||||
}
|
||||
if (type === "response.output_text.done" && !state.text && typeof event.text === "string") {
|
||||
state.text = event.text;
|
||||
onDelta?.(state.text);
|
||||
}
|
||||
if (type === "response.completed" && isRecord(event.response)) {
|
||||
state.payload = event.response as ResponseApiPayload;
|
||||
} else if (Array.isArray(event.output)) {
|
||||
state.payload = event as ResponseApiPayload;
|
||||
}
|
||||
}
|
||||
|
||||
function consumeResponseStreamText(state: ResponseStreamState, text: string, onDelta?: (text: string) => void, flush = false) {
|
||||
state.buffer += text;
|
||||
for (;;) {
|
||||
const match = state.buffer.match(/\r?\n\r?\n/);
|
||||
if (!match) break;
|
||||
consumeResponseStreamBlock(state.buffer.slice(0, match.index), state, onDelta);
|
||||
state.buffer = state.buffer.slice(match.index + match[0].length);
|
||||
}
|
||||
if (flush && state.buffer.trim()) {
|
||||
consumeResponseStreamBlock(state.buffer, state, onDelta);
|
||||
state.buffer = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function requestStreamingResponse(config: AiConfig, body: Record<string, unknown>, onDelta?: (text: string) => void): Promise<ToolResponseResult> {
|
||||
const response = await fetch(aiApiUrl(config, "/responses"), {
|
||||
method: "POST",
|
||||
headers: { ...aiHeaders(config, "application/json"), Accept: "text/event-stream" },
|
||||
body: JSON.stringify({ ...body, stream: true }),
|
||||
});
|
||||
if (!response.ok) throw new Error(await readFetchError(response, "请求失败"));
|
||||
if (!response.body) {
|
||||
const payload = (await response.json()) as ResponseApiPayload;
|
||||
validateResponsePayload(payload);
|
||||
return parseToolResponse(payload);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
const state: ResponseStreamState = { buffer: "", text: "" };
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
consumeResponseStreamText(state, decoder.decode(value, { stream: true }), onDelta);
|
||||
if (state.error) throw new Error(state.error);
|
||||
}
|
||||
consumeResponseStreamText(state, decoder.decode(), onDelta, true);
|
||||
if (state.error) throw new Error(state.error);
|
||||
if (!state.payload) return { content: state.text, toolCalls: [] };
|
||||
validateResponsePayload(state.payload);
|
||||
const result = parseToolResponse(state.payload);
|
||||
return { ...result, content: state.text || result.content };
|
||||
}
|
||||
|
||||
export async function requestGeneration(config: AiConfig, prompt: string) {
|
||||
const requestConfig = resolveModelRequestConfig(config, config.model || config.imageModel);
|
||||
const n = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1)));
|
||||
@@ -325,45 +428,27 @@ export async function requestEdit(config: AiConfig, prompt: string, references:
|
||||
export async function requestImageQuestion(config: AiConfig, messages: AiTextMessage[], onDelta: (text: string) => void) {
|
||||
const requestConfig = resolveModelRequestConfig(config, config.model || config.textModel);
|
||||
try {
|
||||
const response = await axios.post<ResponseApiPayload>(
|
||||
aiApiUrl(requestConfig, "/responses"),
|
||||
{
|
||||
model: requestConfig.model,
|
||||
input: toResponseInput(withSystemMessage(requestConfig, messages)),
|
||||
},
|
||||
{
|
||||
headers: aiHeaders(requestConfig, "application/json"),
|
||||
},
|
||||
);
|
||||
if (typeof response.data.code === "number" && response.data.code !== 0) throw new Error(response.data.msg || "请求失败");
|
||||
if (response.data.error?.message) throw new Error(response.data.error.message);
|
||||
const answer = parseToolResponse(response.data).content || "没有返回内容";
|
||||
onDelta(answer);
|
||||
const answer = (await requestStreamingResponse(requestConfig, {
|
||||
model: requestConfig.model,
|
||||
input: toResponseInput(withSystemMessage(requestConfig, messages)),
|
||||
}, onDelta)).content || "没有返回内容";
|
||||
if (answer === "没有返回内容") onDelta(answer);
|
||||
return answer;
|
||||
} catch (error) {
|
||||
throw new Error(readAxiosError(error, "请求失败"));
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestToolResponse(config: AiConfig, messages: ResponseInputMessage[], tools: ResponseFunctionTool[], toolChoice: ToolChoice = "auto"): Promise<ToolResponseResult> {
|
||||
export async function requestToolResponse(config: AiConfig, messages: ResponseInputMessage[], tools: ResponseFunctionTool[], toolChoice: ToolChoice = "auto", onDelta?: (text: string) => void): Promise<ToolResponseResult> {
|
||||
const requestConfig = resolveModelRequestConfig(config, config.model || config.textModel);
|
||||
try {
|
||||
const response = await axios.post<ResponseApiPayload>(
|
||||
aiApiUrl(requestConfig, "/responses"),
|
||||
{
|
||||
model: requestConfig.model,
|
||||
input: toResponseInput(withSystemMessage(requestConfig, messages)),
|
||||
tools: tools.map(toResponseTool),
|
||||
tool_choice: toolChoice,
|
||||
parallel_tool_calls: false,
|
||||
},
|
||||
{
|
||||
headers: aiHeaders(requestConfig, "application/json"),
|
||||
},
|
||||
);
|
||||
if (typeof response.data.code === "number" && response.data.code !== 0) throw new Error(response.data.msg || "请求失败");
|
||||
if (response.data.error?.message) throw new Error(response.data.error.message);
|
||||
return parseToolResponse(response.data);
|
||||
return await requestStreamingResponse(requestConfig, {
|
||||
model: requestConfig.model,
|
||||
input: toResponseInput(withSystemMessage(requestConfig, messages)),
|
||||
tools: tools.map(toResponseTool),
|
||||
tool_choice: toolChoice,
|
||||
parallel_tool_calls: false,
|
||||
}, onDelta);
|
||||
} catch (error) {
|
||||
throw new Error(readAxiosError(error, "请求失败"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user