mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(prompter): align frontend API client with the backend session contract
The Prompter frontend and backend were built against mismatched contracts:
- createSession POSTed no body (the endpoint requires one) and read a
`session_id` field the response doesn't have (it is `id`), so every session
create returned 422.
- sendMessage hit `/sessions/{id}/chat` with `{message}` and expected a
`{reply, draft}` object, but the session endpoint is `/sessions/{id}/messages`
with `{content}` and returns the full message list.
- API errors rendered as "[object Object]" because getErrorMessage String()'d a
FastAPI validation array.
Point createSession/sendMessage at the real endpoints, adapt the message-list
response (latest assistant message is the reply; fetch the structured draft when
the reply signals readiness), and make getErrorMessage format string /
validation-array / structured-object details into a readable line.
This commit is contained in:
@@ -75,12 +75,43 @@ api.interceptors.response.use(
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Turn a FastAPI `detail` payload — a string, a validation array
|
||||
* ([{loc, msg, ...}]), or a structured `{error, message}` object — into one
|
||||
* readable line. Returns "" when nothing useful can be extracted.
|
||||
*/
|
||||
function formatErrorDetail(detail: unknown): string {
|
||||
if (typeof detail === "string") return detail;
|
||||
if (Array.isArray(detail)) {
|
||||
return detail
|
||||
.map((item) => {
|
||||
if (item && typeof item === "object" && "msg" in item) {
|
||||
const e = item as { loc?: unknown[]; msg?: unknown };
|
||||
const loc = Array.isArray(e.loc)
|
||||
? e.loc.filter((p) => p !== "body").join(".")
|
||||
: "";
|
||||
const msg = String(e.msg ?? "");
|
||||
return loc ? `${loc}: ${msg}` : msg;
|
||||
}
|
||||
return typeof item === "string" ? item : "";
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("; ");
|
||||
}
|
||||
if (detail && typeof detail === "object") {
|
||||
const obj = detail as Record<string, unknown>;
|
||||
if (typeof obj.message === "string") return obj.message;
|
||||
if (typeof obj.error === "string") return obj.error;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to extract user-friendly error message from API error
|
||||
*/
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const axiosError = error as AxiosError<{ detail?: string }>;
|
||||
const axiosError = error as AxiosError<{ detail?: unknown }>;
|
||||
|
||||
// Check for specific error codes
|
||||
if (error.code === "ECONNABORTED") {
|
||||
@@ -90,9 +121,13 @@ export function getErrorMessage(error: unknown): string {
|
||||
return "Cannot connect to server. Check if the backend is running.";
|
||||
}
|
||||
|
||||
// Check for API error response
|
||||
if (axiosError.response?.data?.detail) {
|
||||
return String(axiosError.response.data.detail);
|
||||
// Check for API error response. `detail` may be a plain string, a FastAPI
|
||||
// validation array ([{loc, msg, ...}]), or a structured envelope object —
|
||||
// never blindly String() it (that yields "[object Object]").
|
||||
const detail = axiosError.response?.data?.detail;
|
||||
if (detail) {
|
||||
const formatted = formatErrorDetail(detail);
|
||||
if (formatted) return formatted;
|
||||
}
|
||||
|
||||
// Check for HTTP status
|
||||
|
||||
@@ -25,6 +25,32 @@ export interface CreateSessionResponse {
|
||||
session_id: string;
|
||||
}
|
||||
|
||||
// A message record as returned by the backend (PrompterMessageResponse).
|
||||
interface BackendMessage {
|
||||
id: string;
|
||||
session_id: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Mirrors the backend's draft-ready signal phrases (services/prompter.py).
|
||||
// If the backend list ever drifts, the draft simply doesn't auto-surface (the
|
||||
// user can keep chatting) — it never breaks the conversation flow.
|
||||
const DRAFT_READY_SIGNALS = [
|
||||
"i have enough information",
|
||||
"ready to generate a draft",
|
||||
"ready to draft",
|
||||
"i can now draft",
|
||||
"draft_ready=true",
|
||||
"draft ready",
|
||||
];
|
||||
|
||||
function replyLooksDraftReady(reply: string): boolean {
|
||||
const lower = reply.toLowerCase();
|
||||
return DRAFT_READY_SIGNALS.some((s) => lower.includes(s));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// API functions
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -32,29 +58,47 @@ export interface CreateSessionResponse {
|
||||
export const prompterApi = {
|
||||
/**
|
||||
* Create a new prompter session, returning a session ID.
|
||||
* The endpoint requires a JSON body (optional `context`), so send `{}`, and
|
||||
* map the backend's `id` field onto our `session_id`.
|
||||
*/
|
||||
createSession: async (): Promise<CreateSessionResponse> => {
|
||||
const { data } = await api.post<CreateSessionResponse>("/prompter/sessions");
|
||||
return data;
|
||||
const { data } = await api.post<{ id: string }>("/prompter/sessions", {});
|
||||
return { session_id: data.id };
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a chat message in an existing session.
|
||||
* Returns the assistant reply and, if ready, a draft task proposal.
|
||||
* Send a chat message in an existing session. The backend appends the user
|
||||
* message, replies, and returns the full message list. We surface the latest
|
||||
* assistant message as the reply and, when it signals readiness, fetch the
|
||||
* structured draft.
|
||||
*/
|
||||
sendMessage: async (
|
||||
sessionId: string,
|
||||
message: string
|
||||
): Promise<ChatResponse> => {
|
||||
const { data } = await api.post<ChatResponse>(
|
||||
`/prompter/sessions/${sessionId}/chat`,
|
||||
{ message }
|
||||
const { data: messages } = await api.post<BackendMessage[]>(
|
||||
`/prompter/sessions/${sessionId}/messages`,
|
||||
{ content: message }
|
||||
);
|
||||
return data;
|
||||
const lastAssistant = [...messages]
|
||||
.reverse()
|
||||
.find((m) => m.role === "assistant");
|
||||
const reply = lastAssistant?.content ?? "";
|
||||
|
||||
let draft: DraftProposal | null = null;
|
||||
if (replyLooksDraftReady(reply)) {
|
||||
try {
|
||||
draft = await prompterApi.getDraft(sessionId);
|
||||
} catch {
|
||||
draft = null;
|
||||
}
|
||||
}
|
||||
return { reply, draft, session_id: sessionId };
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch the current draft for a session (if the LLM has produced one).
|
||||
* The backend returns a TaskDraftResponse whose `draft` field holds the task.
|
||||
*/
|
||||
getDraft: async (sessionId: string): Promise<DraftProposal | null> => {
|
||||
const { data } = await api.get<{ draft: DraftProposal | null }>(
|
||||
|
||||
Reference in New Issue
Block a user