Add version files and update imports for trip model; enhance error handling
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import { fail, handleOptions, json } from "../_shared/http.ts";
|
||||
import { requireUser } from "../_shared/supabase.ts";
|
||||
|
||||
const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY");
|
||||
const OPENAI_MODEL = Deno.env.get("OPENAI_MODEL") ?? "gpt-5.2";
|
||||
|
||||
type RequestBody = {
|
||||
channel_id?: string;
|
||||
stop_names?: string[];
|
||||
};
|
||||
|
||||
type OpenAiAlias = {
|
||||
original?: string;
|
||||
estimated?: string;
|
||||
};
|
||||
|
||||
const prompt = `You are interpreting abbreviated station names from a rail replacement service display.
|
||||
|
||||
Each entry follows this structure:
|
||||
|
||||
* A 4-letter station code (derived from the real station name, often by removing vowels or compressing syllables)
|
||||
* A 2-letter stop code (ignore this; it is ambiguous and not needed)
|
||||
* An optional "T" indicating terminus (ignore for naming purposes)
|
||||
|
||||
Your task is to infer the full station names from the 4-letter codes.
|
||||
|
||||
Guidelines:
|
||||
|
||||
* Treat the 4-letter code as a compressed version of a real station name (e.g. consonant-heavy, missing vowels, or merged syllables)
|
||||
* Use pattern recognition rather than strict decoding
|
||||
* Prefer real-world plausibility over perfect letter matching
|
||||
* Assume all stations are on the same rail corridor or geographically connected route
|
||||
* Use the sequence of stops to inform your guesses (adjacent stations should make sense geographically)
|
||||
* If a code is slightly irregular, prioritise what fits the route best over what matches the letters exactly
|
||||
|
||||
Output:
|
||||
Return JSON with shape {"aliases":[{"original":"<code>","estimated":"<name>"}]}.
|
||||
Return only valid JSON with no markdown or explanation.`;
|
||||
|
||||
function extractCode(rawStopName: string): string {
|
||||
const firstToken = rawStopName.trim().split(/\s+/)[0] ?? "";
|
||||
const normalized = firstToken.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
||||
return normalized.slice(0, 8);
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const preflight = handleOptions(req);
|
||||
if (preflight) return preflight;
|
||||
|
||||
if (req.method !== "POST") return fail("Method not allowed", 405);
|
||||
if (!OPENAI_API_KEY) return fail("OPENAI_API_KEY is not configured", 500);
|
||||
|
||||
const { client, user, error: userError } = await requireUser(req);
|
||||
if (!user) return fail(userError ?? "Unauthorized", 401);
|
||||
|
||||
let body: RequestBody;
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return fail("Invalid JSON body");
|
||||
}
|
||||
|
||||
const channelId = (body.channel_id ?? "").trim();
|
||||
const stopNames = Array.isArray(body.stop_names) ? body.stop_names : [];
|
||||
if (!channelId) return fail("channel_id is required");
|
||||
if (stopNames.length == 0) return json({ aliases: [] });
|
||||
if (stopNames.length > 1000) return fail("stop_names is too large");
|
||||
|
||||
const { data: channel, error: channelError } = await client
|
||||
.from("channels")
|
||||
.select("id, type")
|
||||
.eq("id", channelId)
|
||||
.eq("type", "operations")
|
||||
.maybeSingle();
|
||||
if (channelError) return fail(channelError.message, 400);
|
||||
if (!channel) return fail("forbidden", 403);
|
||||
|
||||
const rawByCode = new Map<string, Set<string>>();
|
||||
for (const stopName of stopNames) {
|
||||
const raw = `${stopName ?? ""}`.trim();
|
||||
if (!raw) continue;
|
||||
const code = extractCode(raw);
|
||||
if (!code) continue;
|
||||
const existing = rawByCode.get(code) ?? new Set<string>();
|
||||
existing.add(raw);
|
||||
rawByCode.set(code, existing);
|
||||
}
|
||||
|
||||
const codes = [...rawByCode.keys()];
|
||||
if (codes.length == 0) return json({ aliases: [] });
|
||||
|
||||
const openAiResponse = await fetch("https://api.openai.com/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${OPENAI_API_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: OPENAI_MODEL,
|
||||
temperature: 0,
|
||||
response_format: { type: "json_object" },
|
||||
messages: [
|
||||
{ role: "system", content: prompt },
|
||||
{
|
||||
role: "user",
|
||||
content: JSON.stringify({
|
||||
channel_id: channelId,
|
||||
codes,
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!openAiResponse.ok) {
|
||||
const details = await openAiResponse.text();
|
||||
return fail(`OpenAI request failed: ${details}`, 502);
|
||||
}
|
||||
|
||||
const completion = await openAiResponse.json();
|
||||
const content = completion?.choices?.[0]?.message?.content;
|
||||
if (typeof content !== "string" || content.trim().length == 0) {
|
||||
return fail("OpenAI returned no content", 502);
|
||||
}
|
||||
|
||||
let parsed: { aliases?: OpenAiAlias[] };
|
||||
try {
|
||||
parsed = JSON.parse(content);
|
||||
} catch {
|
||||
return fail("OpenAI returned invalid JSON", 502);
|
||||
}
|
||||
|
||||
const codeToEstimated = new Map<string, string>();
|
||||
for (const row of parsed.aliases ?? []) {
|
||||
const original = `${row.original ?? ""}`.trim().toUpperCase();
|
||||
const estimated = `${row.estimated ?? ""}`.trim();
|
||||
if (!original || !estimated) continue;
|
||||
codeToEstimated.set(original, estimated);
|
||||
}
|
||||
|
||||
const aliases = [];
|
||||
for (const [code, rawStops] of rawByCode.entries()) {
|
||||
const estimated = codeToEstimated.get(code);
|
||||
if (!estimated) continue;
|
||||
for (const raw of rawStops) {
|
||||
aliases.push({
|
||||
raw_stop_name: raw,
|
||||
alias_stop_name: estimated,
|
||||
source: "ai",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return json({ aliases });
|
||||
});
|
||||
Reference in New Issue
Block a user