File size: 5,832 Bytes
644a6a3 3e3d78c ccb56e6 644a6a3 149f646 644a6a3 149f646 644a6a3 149f646 644a6a3 | 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 | /**
* SSE client for the GeoForce FastAPI backend.
*
* Uses fetch + ReadableStream to consume Server-Sent Events from POST /query.
* (Native EventSource only supports GET, so we parse SSE manually.)
*/
export type Scenario = {
id: string;
question: string;
scenario: Record<string, unknown>;
[k: string]: unknown;
};
export type AgentEvent =
| { type: "text"; text: string }
| { type: "tool"; name: string; input: Record<string, unknown> }
| { type: "result"; final_text: string; stop_reason: string | null }
| { type: "error"; message: string };
const API_BASE = import.meta.env.VITE_API_BASE ?? "/api";
export type FieldResult = {
grid: { nx: number; ny: number; dx: number; dy: number };
temperature: number[][];
pressure: number[][];
t_min: number;
t_max: number;
p_min_MPa: number;
p_max_MPa: number;
elapsed_seconds: number;
};
export type PredictResponse = {
engine: "both" | "solver" | "surrogate";
solver?: FieldResult;
surrogate?: FieldResult;
};
export async function predictFields(
scenario_id: string,
engine: "both" | "solver" | "surrogate" = "both",
): Promise<PredictResponse> {
const res = await fetch(`${API_BASE}/predict`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ scenario_id, engine }),
});
if (!res.ok) throw new Error(`predict: ${res.status}`);
return res.json();
}
/** Run /predict with an inline scenario dict (what the agent passes to its
* predict_solver / predict_surrogate tools). Returns fields for the dashboard
* canvas; runs locally on the backend, no extra API tokens consumed. */
export async function predictFieldsInline(
scenario: Record<string, unknown>,
engine: "solver" | "surrogate",
): Promise<PredictResponse> {
const res = await fetch(`${API_BASE}/predict`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ scenario, engine }),
});
if (!res.ok) throw new Error(`predict: ${res.status}`);
return res.json();
}
export async function fetchScenarios(): Promise<Scenario[]> {
const res = await fetch(`${API_BASE}/scenarios`);
if (!res.ok) throw new Error(`scenarios: ${res.status}`);
const data = await res.json();
return (data?.scenarios ?? []) as Scenario[];
}
export async function checkHealth(): Promise<boolean> {
try {
const res = await fetch(`${API_BASE}/health`);
return res.ok;
} catch {
return false;
}
}
/**
* POST /query and stream SSE events (single-shot, no session).
* Calls onEvent for each parsed event; resolves when the stream ends.
*/
export async function streamQuery(
body: { query: string; scenario_id?: string },
onEvent: (e: AgentEvent) => void,
signal?: AbortSignal,
): Promise<void> {
await _streamSSE(`${API_BASE}/query`, body, onEvent, signal);
}
/** Open a multi-turn session. The returned id is used by streamSessionQuery. */
export async function createSession(): Promise<string> {
const res = await fetch(`${API_BASE}/sessions`, { method: "POST" });
if (!res.ok) throw new Error(`sessions: ${res.status}`);
const data = (await res.json()) as { session_id: string };
return data.session_id;
}
export async function deleteSession(sessionId: string): Promise<void> {
try {
await fetch(`${API_BASE}/sessions/${sessionId}`, { method: "DELETE" });
} catch {
/* best-effort */
}
}
/** Stream one turn against an existing session — the model sees prior turns. */
export async function streamSessionQuery(
sessionId: string,
body: { query: string; scenario_id?: string },
onEvent: (e: AgentEvent) => void,
signal?: AbortSignal,
): Promise<void> {
await _streamSSE(
`${API_BASE}/sessions/${sessionId}/query`,
body,
onEvent,
signal,
);
}
async function _streamSSE(
url: string,
body: Record<string, unknown>,
onEvent: (e: AgentEvent) => void,
signal?: AbortSignal,
): Promise<void> {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
body: JSON.stringify(body),
signal,
});
if (!res.ok || !res.body) throw new Error(`sse: ${res.status}`);
const reader = res.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const chunks = buffer.split(/\r?\n\r?\n/);
buffer = chunks.pop() ?? "";
for (const chunk of chunks) {
if (!chunk.trim()) continue;
const parsed = parseSSEChunk(chunk);
if (parsed) onEvent(parsed);
}
}
}
function parseSSEChunk(chunk: string): AgentEvent | null {
let eventName = "message";
const dataLines: string[] = [];
for (const line of chunk.split(/\r?\n/)) {
if (line.startsWith("event:")) {
eventName = line.slice(6).trim();
} else if (line.startsWith("data:")) {
dataLines.push(line.slice(5).trim());
}
}
if (dataLines.length === 0) return null;
const dataStr = dataLines.join("\n");
let payload: Record<string, unknown> = {};
try {
payload = JSON.parse(dataStr);
} catch {
return null;
}
switch (eventName) {
case "text":
return { type: "text", text: String(payload.text ?? "") };
case "tool":
return {
type: "tool",
name: String(payload.name ?? "unknown"),
input: (payload.input as Record<string, unknown>) ?? {},
};
case "result":
return {
type: "result",
final_text: String(payload.final_text ?? ""),
stop_reason: (payload.stop_reason as string | null) ?? null,
};
case "error":
return { type: "error", message: String(payload.message ?? "unknown error") };
default:
return null;
}
}
|