feat(dashboard): migrate from groq to gemini
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 1m12s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 1m12s
use gpt-oss-120b on groq instead of gemini because of more generous rate limits
This commit is contained in:
2
apps/backend/src/env.d.ts
vendored
2
apps/backend/src/env.d.ts
vendored
@@ -4,7 +4,7 @@ declare namespace NodeJS {
|
||||
ADP_SERVICE_ID: string
|
||||
ADP_KEY_ID: string
|
||||
ADP_KEY_PATH: string
|
||||
GEMINI_API_KEY: string
|
||||
GROQ_API_KEY: string
|
||||
BESZEL_HOST?: string
|
||||
BESZEL_EMAIL?: string
|
||||
BESZEL_PASSWORD?: string
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* Gemini AI integration for shortening TfL disruption descriptions
|
||||
* Groq AI integration for shortening TfL disruption descriptions
|
||||
* Uses Groq's OpenAI-compatible API with GPT OSS 120B model
|
||||
*/
|
||||
|
||||
import { getCachedShortened, setCachedShortened } from "./cache"
|
||||
@@ -38,22 +39,24 @@ function stripLineName(text: string, lineName: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorten multiple disruption reasons in a single Gemini API call
|
||||
* Shorten multiple disruption reasons in a single Groq API call
|
||||
*/
|
||||
export async function shortenMultipleDisruptions(
|
||||
disruptions: DisruptionToShorten[]
|
||||
): Promise<Map<string, string>> {
|
||||
const apiKey = process.env.GEMINI_API_KEY
|
||||
const apiKey = process.env.GROQ_API_KEY
|
||||
const results = new Map<string, string>()
|
||||
|
||||
if (!apiKey) {
|
||||
console.warn("GEMINI_API_KEY not set, returning stripped versions")
|
||||
if (!apiKey || apiKey.trim() === "") {
|
||||
console.warn("GROQ_API_KEY not set or empty, returning stripped versions")
|
||||
for (const disruption of disruptions) {
|
||||
results.set(disruption.lineName, stripLineName(disruption.reason, disruption.lineName))
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
console.log(`[TFL Groq] Processing ${disruptions.length} disruptions with API key (length: ${apiKey.length})`)
|
||||
|
||||
// Filter disruptions that need shortening
|
||||
const toShorten: DisruptionToShorten[] = []
|
||||
|
||||
@@ -80,41 +83,58 @@ export async function shortenMultipleDisruptions(
|
||||
|
||||
// If nothing needs shortening, return early
|
||||
if (toShorten.length === 0) {
|
||||
console.log(`[TFL Groq] All ${disruptions.length} disruptions were cached or already short`)
|
||||
return results
|
||||
}
|
||||
|
||||
console.log(`[TFL Groq] Shortening ${toShorten.length} disruptions via API`)
|
||||
|
||||
// Build batch prompt
|
||||
const prompt = buildBatchShorteningPrompt(toShorten)
|
||||
|
||||
try {
|
||||
// Groq uses OpenAI-compatible API format
|
||||
const response = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent?key=${apiKey}`,
|
||||
"https://api.groq.com/openai/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
contents: [
|
||||
model: "openai/gpt-oss-120b",
|
||||
messages: [
|
||||
{
|
||||
parts: [
|
||||
{
|
||||
text: prompt,
|
||||
},
|
||||
],
|
||||
role: "user",
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
generationConfig: {
|
||||
temperature: 0.3,
|
||||
maxOutputTokens: 2000, // Higher limit to account for thinking tokens in Gemini 2.5 Flash
|
||||
topP: 0.9,
|
||||
},
|
||||
temperature: 0.3,
|
||||
max_tokens: 2000,
|
||||
top_p: 0.9,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Gemini API error: ${response.status}`)
|
||||
const errorText = await response.text().catch(() => "Unable to read error response")
|
||||
console.error(`Groq API error: ${response.status} ${response.statusText}`)
|
||||
|
||||
// Check for quota/rate limit errors
|
||||
if (response.status === 429) {
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
const message = errorJson.error?.message || errorText
|
||||
console.error("[TFL Groq] QUOTA EXCEEDED - Rate limit hit!")
|
||||
console.error("[TFL Groq] Error details:", message)
|
||||
} catch {
|
||||
console.error("Error response:", errorText)
|
||||
}
|
||||
} else {
|
||||
console.error("Error response:", errorText)
|
||||
}
|
||||
|
||||
// Fallback to stripped versions
|
||||
for (const disruption of toShorten) {
|
||||
results.set(disruption.lineName, disruption.reason)
|
||||
@@ -123,7 +143,18 @@ export async function shortenMultipleDisruptions(
|
||||
}
|
||||
|
||||
const data = (await response.json()) as any
|
||||
const responseText = data.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || ""
|
||||
|
||||
// Groq uses OpenAI-compatible response format
|
||||
const responseText = data.choices?.[0]?.message?.content?.trim() || ""
|
||||
|
||||
if (!responseText) {
|
||||
console.error("Groq API returned empty response text. Full response:", JSON.stringify(data, null, 2))
|
||||
// Fallback to stripped versions
|
||||
for (const disruption of toShorten) {
|
||||
results.set(disruption.lineName, disruption.reason)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Parse JSON response
|
||||
try {
|
||||
@@ -136,8 +167,21 @@ export async function shortenMultipleDisruptions(
|
||||
|
||||
const shortened = JSON.parse(jsonText) as ShortenedResult[]
|
||||
|
||||
if (!Array.isArray(shortened)) {
|
||||
console.error("Gemini API response is not an array:", shortened)
|
||||
// Fallback to stripped versions
|
||||
for (const disruption of toShorten) {
|
||||
results.set(disruption.lineName, disruption.reason)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Map results
|
||||
for (const item of shortened) {
|
||||
if (!item.lineName || !item.shortened) {
|
||||
console.warn("Invalid shortened result item:", item)
|
||||
continue
|
||||
}
|
||||
results.set(item.lineName, item.shortened)
|
||||
// Cache the result
|
||||
const original = toShorten.find(d => d.lineName === item.lineName)
|
||||
@@ -145,9 +189,12 @@ export async function shortenMultipleDisruptions(
|
||||
setCachedShortened(original.reason, item.shortened)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[TFL Groq] Successfully shortened ${results.size} disruptions`)
|
||||
} catch (parseError) {
|
||||
console.error("Failed to parse Gemini JSON response:", parseError)
|
||||
console.error("Response was:", responseText)
|
||||
console.error("Failed to parse Groq JSON response:", parseError)
|
||||
console.error("Response text was:", responseText)
|
||||
console.error("Full API response:", JSON.stringify(data, null, 2))
|
||||
// Fallback to stripped versions
|
||||
for (const disruption of toShorten) {
|
||||
results.set(disruption.lineName, disruption.reason)
|
||||
@@ -165,7 +212,7 @@ export async function shortenMultipleDisruptions(
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a batch prompt for Gemini to shorten multiple disruptions at once
|
||||
* Builds a batch prompt for Groq to shorten multiple disruptions at once
|
||||
*/
|
||||
function buildBatchShorteningPrompt(disruptions: DisruptionToShorten[]): string {
|
||||
const disruptionsList = disruptions.map((d, i) =>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* Gemini AI integration for generating weather descriptions
|
||||
* Groq AI integration for generating weather descriptions
|
||||
* Uses Groq's OpenAI-compatible API with GPT OSS 120B model
|
||||
*/
|
||||
|
||||
interface WeatherData {
|
||||
@@ -20,53 +21,60 @@ interface WeatherData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a concise weather description using Gemini 2.5 Flash
|
||||
* Generates a concise weather description using Groq's GPT OSS 120B
|
||||
*/
|
||||
export async function generateWeatherDescription(
|
||||
weatherData: WeatherData,
|
||||
): Promise<string> {
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
const apiKey = process.env.GROQ_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error("GEMINI_API_KEY environment variable is not set");
|
||||
throw new Error("GROQ_API_KEY environment variable is not set");
|
||||
}
|
||||
|
||||
const prompt = buildWeatherPrompt(weatherData);
|
||||
|
||||
try {
|
||||
// Groq uses OpenAI-compatible API format
|
||||
const response = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent?key=${apiKey}`,
|
||||
"https://api.groq.com/openai/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
contents: [
|
||||
model: "openai/gpt-oss-120b",
|
||||
messages: [
|
||||
{
|
||||
parts: [
|
||||
{
|
||||
text: prompt,
|
||||
},
|
||||
],
|
||||
role: "user",
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
generationConfig: {
|
||||
temperature: 0.7,
|
||||
maxOutputTokens: 1000,
|
||||
topP: 0.95,
|
||||
},
|
||||
temperature: 0.7,
|
||||
max_tokens: 1000,
|
||||
top_p: 0.95,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Gemini API error: ${response.status}`);
|
||||
const errorText = await response.text().catch(() => "Unable to read error response");
|
||||
console.error(`Groq API error: ${response.status} ${response.statusText}`);
|
||||
console.error("Error response:", errorText);
|
||||
throw new Error(`Groq API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as any;
|
||||
const description =
|
||||
data.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || "";
|
||||
// Groq uses OpenAI-compatible response format
|
||||
const description = data.choices?.[0]?.message?.content?.trim() || "";
|
||||
|
||||
if (!description) {
|
||||
console.error("Groq API returned empty response. Full response:", JSON.stringify(data, null, 2));
|
||||
// Fallback to basic description
|
||||
return `${weatherData.condition}, ${Math.round(weatherData.temperature)}°C`;
|
||||
}
|
||||
|
||||
return description;
|
||||
} catch (error) {
|
||||
@@ -77,7 +85,7 @@ export async function generateWeatherDescription(
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an optimized prompt for Gemini to generate weather descriptions
|
||||
* Builds an optimized prompt for Groq to generate weather descriptions
|
||||
*/
|
||||
function buildWeatherPrompt(weatherData: WeatherData): string {
|
||||
let laterConditions = "";
|
||||
|
||||
@@ -170,7 +170,7 @@ weather.get("/description/:lat/:lon", async (c) => {
|
||||
// Get tomorrow's forecast if it's nighttime
|
||||
const tomorrow = isNighttime ? data.forecastDaily?.days?.[1] : null
|
||||
|
||||
// Generate description using Gemini
|
||||
// Generate description using Groq
|
||||
const description = await generateWeatherDescription({
|
||||
condition: current.conditionCode,
|
||||
temperature: current.temperature,
|
||||
|
||||
184
apps/backend/test-groq-weather.ts
Normal file
184
apps/backend/test-groq-weather.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Test script to verify Groq API integration for weather descriptions
|
||||
*/
|
||||
|
||||
const apiKey = process.env.GROQ_API_KEY
|
||||
|
||||
if (!apiKey || apiKey.trim() === "") {
|
||||
console.error("ERROR: GROQ_API_KEY is not set or empty")
|
||||
console.error("Please check your .env file")
|
||||
console.error("Add: GROQ_API_KEY=your_api_key_here")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`API Key found (length: ${apiKey.length}, starts with: ${apiKey.substring(0, 10)}...)`)
|
||||
|
||||
// Sample weather data
|
||||
const sampleWeatherData = {
|
||||
condition: "PartlyCloudy",
|
||||
temperature: 18,
|
||||
feelsLike: 16,
|
||||
humidity: 0.65,
|
||||
windSpeed: 15,
|
||||
precipitationChance: 0.3,
|
||||
uvIndex: 6,
|
||||
daytimeCondition: "MostlyCloudy",
|
||||
isNighttime: false,
|
||||
}
|
||||
|
||||
// Build the same prompt that the real code uses
|
||||
function buildWeatherPrompt(weatherData: typeof sampleWeatherData): string {
|
||||
let laterConditions = ""
|
||||
|
||||
// If it's nighttime, mention tomorrow's weather
|
||||
if (weatherData.isNighttime && (weatherData as any).tomorrowCondition) {
|
||||
laterConditions = `\n\nTomorrow's forecast:
|
||||
- Condition: ${(weatherData as any).tomorrowCondition}
|
||||
- High: ${(weatherData as any).tomorrowHighTemp ? Math.round((weatherData as any).tomorrowHighTemp) : "N/A"}°C
|
||||
- Low: ${(weatherData as any).tomorrowLowTemp ? Math.round((weatherData as any).tomorrowLowTemp) : "N/A"}°C
|
||||
${(weatherData as any).tomorrowPrecipitationChance ? `- Precipitation chance: ${Math.round((weatherData as any).tomorrowPrecipitationChance * 100)}%` : ""}`
|
||||
}
|
||||
// Otherwise, mention changes later today
|
||||
else if (weatherData.daytimeCondition || (weatherData as any).overnightCondition) {
|
||||
laterConditions = `\n- Later today: ${weatherData.daytimeCondition || (weatherData as any).overnightCondition}`
|
||||
}
|
||||
|
||||
return `Generate a concise, natural weather description for a dashboard. Keep it under 25 words.
|
||||
|
||||
Current conditions:
|
||||
- Condition: ${weatherData.condition}
|
||||
- Feels like: ${Math.round(weatherData.feelsLike)}°C
|
||||
- Humidity: ${Math.round(weatherData.humidity * 100)}%
|
||||
- Wind speed: ${Math.round(weatherData.windSpeed)} km/h
|
||||
${weatherData.precipitationChance ? `- Precipitation chance: ${Math.round(weatherData.precipitationChance * 100)}%` : ""}
|
||||
- UV index: ${weatherData.uvIndex}${laterConditions}
|
||||
|
||||
Requirements:
|
||||
- Be conversational and friendly
|
||||
- Focus on what matters most (condition, any warnings)
|
||||
- DO NOT mention the current temperature - it will be displayed separately
|
||||
- CRITICAL: If it's nighttime and tomorrow's forecast is provided, PRIORITIZE tomorrow's weather (e.g., "Cool night. Tomorrow will be partly cloudy with a high of 10°C.")
|
||||
- If it's daytime and conditions change later, mention it (e.g., "turning cloudy later", "clearing up tonight")
|
||||
- Tomorrow's temperature is OK to mention
|
||||
- Mention feels-like only if significantly different (>3°C) and explain WHY (e.g., "due to wind", "due to humidity")
|
||||
- Include precipitation chance if >30%
|
||||
- For wind: Use descriptive terms (calm, light, moderate, strong, extreme) - NEVER use specific km/h numbers
|
||||
- For UV: Use descriptive terms (low, moderate, high, very high, extreme) - NEVER use specific numbers
|
||||
- Warn about extreme conditions (very hot/cold, high UV, strong winds)
|
||||
- Use natural language, not technical jargon
|
||||
- NO emojis
|
||||
- One or two short sentences maximum
|
||||
|
||||
Example good outputs (DAYTIME):
|
||||
- "Partly cloudy and pleasant. Light winds make it comfortable."
|
||||
- "Clear skies, but feels hotter. High UV - wear sunscreen."
|
||||
- "Mostly sunny, turning cloudy later. Comfortable conditions."
|
||||
- "Rainy with 70% chance of more rain. Bring an umbrella."
|
||||
- "Feels much colder due to strong winds. Bundle up."
|
||||
- "Cloudy and mild, clearing up tonight."
|
||||
- "Feels warmer due to humidity. Stay hydrated."
|
||||
|
||||
Example good outputs (NIGHTTIME - focus on tomorrow):
|
||||
- "Cool night. Tomorrow will be sunny and warm with a high of 24°C."
|
||||
- "Clear skies. Expect partly cloudy skies tomorrow, high of 10°C."
|
||||
- "Chilly night. Tomorrow brings rain with a high of 15°C."
|
||||
- "Mild evening. Tomorrow will be hot and sunny, reaching 32°C."
|
||||
|
||||
Example BAD outputs (avoid these):
|
||||
- "Mostly clear at 7°C, feels like 0°C due to the 21 km/h wind." ❌ (don't mention current temp, don't use specific wind speed)
|
||||
- "Sunny at 28°C with UV index of 9." ❌ (don't mention current temp, don't use specific UV number)
|
||||
- "Temperature is 22°C with 65% humidity." ❌ (don't mention current temp, too technical)
|
||||
|
||||
Generate description:`
|
||||
}
|
||||
|
||||
const prompt = buildWeatherPrompt(sampleWeatherData)
|
||||
|
||||
console.log("\n=== Sending request to Groq API ===\n")
|
||||
console.log("URL:", "https://api.groq.com/openai/v1/chat/completions")
|
||||
console.log("Model:", "openai/gpt-oss-120b")
|
||||
console.log("\nPrompt length:", prompt.length, "characters")
|
||||
console.log("\nFirst 500 chars of prompt:")
|
||||
console.log(prompt.substring(0, 500))
|
||||
console.log("...\n")
|
||||
|
||||
const requestBody = {
|
||||
model: "openai/gpt-oss-120b",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
temperature: 0.7,
|
||||
max_tokens: 1000,
|
||||
top_p: 0.95,
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("Making API request...\n")
|
||||
const response = await fetch(
|
||||
"https://api.groq.com/openai/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
}
|
||||
)
|
||||
|
||||
console.log("=== Response Status ===")
|
||||
console.log(`Status: ${response.status} ${response.statusText}`)
|
||||
console.log(`OK: ${response.ok}`)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.log("\n=== Error Response Body ===")
|
||||
console.log(errorText)
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
console.log("\n=== Parsed Error JSON ===")
|
||||
console.log(JSON.stringify(errorJson, null, 2))
|
||||
} catch {
|
||||
// Not JSON, that's fine
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
console.log("\n=== Full API Response ===")
|
||||
console.log(JSON.stringify(data, null, 2))
|
||||
|
||||
console.log("\n=== Extracting Response Text ===")
|
||||
const description = data.choices?.[0]?.message?.content?.trim() || ""
|
||||
|
||||
if (!description) {
|
||||
console.error("ERROR: Response text is empty!")
|
||||
console.log("Response structure:")
|
||||
console.log("- choices exists:", !!data.choices)
|
||||
console.log("- choices[0] exists:", !!data.choices?.[0])
|
||||
console.log("- message exists:", !!data.choices?.[0]?.message)
|
||||
console.log("- content exists:", !!data.choices?.[0]?.message?.content)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log("\n=== Weather Description ===")
|
||||
console.log(description)
|
||||
console.log("\n=== Description Length ===")
|
||||
console.log(description.length, "characters")
|
||||
console.log(description.split(" ").length, "words")
|
||||
|
||||
console.log("\n✅ Test completed successfully!")
|
||||
} catch (error) {
|
||||
console.error("\n=== Request Failed ===")
|
||||
console.error(error)
|
||||
if (error instanceof Error) {
|
||||
console.error("Error message:", error.message)
|
||||
console.error("Error stack:", error.stack)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
190
apps/backend/test-groq.ts
Normal file
190
apps/backend/test-groq.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* Test script to diagnose Groq API issues with TFL status summarization
|
||||
*/
|
||||
|
||||
const apiKey = process.env.GROQ_API_KEY
|
||||
|
||||
if (!apiKey || apiKey.trim() === "") {
|
||||
console.error("ERROR: GROQ_API_KEY is not set or empty")
|
||||
console.error("Please check your .env file")
|
||||
console.error("Add: GROQ_API_KEY=your_api_key_here")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`API Key found (length: ${apiKey.length}, starts with: ${apiKey.substring(0, 10)}...)`)
|
||||
|
||||
// Sample TFL disruption data (similar to what the real code sends)
|
||||
const sampleDisruptions = [
|
||||
{
|
||||
lineName: "Piccadilly",
|
||||
status: "Part Suspended",
|
||||
reason: "Piccadilly Line: No service between Rayners Lane and Uxbridge due to Storm Benjamin. Use Metropolitan line services between Rayners Lane and Uxbridge. Good service on the rest of the line."
|
||||
},
|
||||
{
|
||||
lineName: "Central",
|
||||
status: "Minor Delays",
|
||||
reason: "Central Line: Minor delays due to train cancellations."
|
||||
}
|
||||
]
|
||||
|
||||
// Build the same prompt that the real code uses
|
||||
function buildBatchShorteningPrompt(disruptions: typeof sampleDisruptions): string {
|
||||
const disruptionsList = disruptions.map((d, i) =>
|
||||
`${i + 1}. Line: ${d.lineName}\n Status: ${d.status}\n Message: "${d.reason}"`
|
||||
).join('\n\n')
|
||||
|
||||
return `Shorten these London transport disruption messages for a dashboard display. Return your response as a JSON array.
|
||||
|
||||
Disruptions to shorten:
|
||||
${disruptionsList}
|
||||
|
||||
Requirements:
|
||||
- Keep each shortened message under 80 characters
|
||||
- Be concise but keep essential information (reason, locations, alternatives, time info)
|
||||
- DO NOT include line names in the shortened text (they're displayed separately)
|
||||
- Use natural, clear language
|
||||
- NO emojis
|
||||
|
||||
Return ONLY a JSON array in this exact format:
|
||||
[
|
||||
{"lineName": "Piccadilly", "shortened": "Suspended Rayners Lane-Uxbridge until Fri due to Storm Benjamin. Use Metropolitan line."},
|
||||
{"lineName": "Central", "shortened": "Minor delays due to train cancellations"},
|
||||
...
|
||||
]
|
||||
|
||||
Good examples of shortened messages:
|
||||
- "Suspended Rayners Lane-Uxbridge until Fri due to Storm Benjamin. Use Metropolitan line."
|
||||
- "Minor delays due to train cancellations"
|
||||
- "Minor delays due to earlier incidents at Gospel Oak & Highbury"
|
||||
- "Severe delays - signal failure at King's Cross. Use buses/Elizabeth line."
|
||||
- "No service Earls Court-Wimbledon until Sun 27 Oct (engineering)"
|
||||
|
||||
Generate JSON array:`
|
||||
}
|
||||
|
||||
const prompt = buildBatchShorteningPrompt(sampleDisruptions)
|
||||
|
||||
console.log("\n=== Sending request to Groq API ===\n")
|
||||
console.log("URL:", "https://api.groq.com/openai/v1/chat/completions")
|
||||
console.log("Model:", "openai/gpt-oss-120b")
|
||||
console.log("\nPrompt length:", prompt.length, "characters")
|
||||
console.log("\nFirst 500 chars of prompt:")
|
||||
console.log(prompt.substring(0, 500))
|
||||
console.log("...\n")
|
||||
|
||||
const requestBody = {
|
||||
model: "openai/gpt-oss-120b",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
temperature: 0.3,
|
||||
max_tokens: 2000,
|
||||
top_p: 0.9,
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("Making API request...\n")
|
||||
const response = await fetch(
|
||||
"https://api.groq.com/openai/v1/chat/completions",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
}
|
||||
)
|
||||
|
||||
console.log("=== Response Status ===")
|
||||
console.log(`Status: ${response.status} ${response.statusText}`)
|
||||
console.log(`OK: ${response.ok}`)
|
||||
console.log("\n=== Response Headers ===")
|
||||
for (const [key, value] of response.headers.entries()) {
|
||||
console.log(`${key}: ${value}`)
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.log("\n=== Error Response Body ===")
|
||||
console.log(errorText)
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText)
|
||||
console.log("\n=== Parsed Error JSON ===")
|
||||
console.log(JSON.stringify(errorJson, null, 2))
|
||||
} catch {
|
||||
// Not JSON, that's fine
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
console.log("\n=== Full API Response ===")
|
||||
console.log(JSON.stringify(data, null, 2))
|
||||
|
||||
console.log("\n=== Extracting Response Text ===")
|
||||
const responseText = data.choices?.[0]?.message?.content?.trim() || ""
|
||||
|
||||
if (!responseText) {
|
||||
console.error("ERROR: Response text is empty!")
|
||||
console.log("Response structure:")
|
||||
console.log("- choices exists:", !!data.choices)
|
||||
console.log("- choices[0] exists:", !!data.choices?.[0])
|
||||
console.log("- message exists:", !!data.choices?.[0]?.message)
|
||||
console.log("- content exists:", !!data.choices?.[0]?.message?.content)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log("\n=== Response Text ===")
|
||||
console.log(responseText)
|
||||
console.log("\n=== Response Text Length ===")
|
||||
console.log(responseText.length, "characters")
|
||||
|
||||
// Try to parse JSON
|
||||
console.log("\n=== Attempting to Parse JSON ===")
|
||||
let jsonText = responseText
|
||||
const jsonMatch = responseText.match(/```json\s*([\s\S]*?)\s*```/)
|
||||
if (jsonMatch) {
|
||||
console.log("Found JSON in markdown code block")
|
||||
jsonText = jsonMatch[1]
|
||||
} else {
|
||||
console.log("No markdown code block found, using response text directly")
|
||||
}
|
||||
|
||||
try {
|
||||
const shortened = JSON.parse(jsonText)
|
||||
console.log("\n=== Successfully Parsed JSON ===")
|
||||
console.log(JSON.stringify(shortened, null, 2))
|
||||
|
||||
if (Array.isArray(shortened)) {
|
||||
console.log("\n=== Summary ===")
|
||||
console.log(`Parsed ${shortened.length} items:`)
|
||||
for (const item of shortened) {
|
||||
console.log(` - ${item.lineName}: "${item.shortened}"`)
|
||||
}
|
||||
} else {
|
||||
console.log("WARNING: Response is not an array!")
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error("\n=== JSON Parse Error ===")
|
||||
console.error(parseError)
|
||||
console.log("\n=== Text that failed to parse ===")
|
||||
console.log(jsonText)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log("\n✅ Test completed successfully!")
|
||||
} catch (error) {
|
||||
console.error("\n=== Request Failed ===")
|
||||
console.error(error)
|
||||
if (error instanceof Error) {
|
||||
console.error("Error message:", error.message)
|
||||
console.error("Error stack:", error.stack)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user