import { OpenAI } from "openai"; // Configuration - use specified model with fallback to env vars export const MODEL = process.env.HF_MODEL || process.env.OPENAI_MODEL || "zai-org/GLM-4.7-Flash:novita"; const HF_TOKEN = process.env.HF_TOKEN; if (!HF_TOKEN) { throw new Error( "HF_TOKEN environment variable is required for Hugging Face Router authentication." ); } const SYS_PROMPT = ` # Welcome to the you own exploration game You are acting as an explorer in a 2D world game seen from a top-down perspective. Everything in this world, including objects and landmarks, is represented by emoji. The most important emoji is 🚶 (it's you). ## Important concepts ### Turn-based You can take one action per turn, then you'll get the result of your action before you can take another action. ### Goals Your primary goal is to collect as many diamonds (💎) as possible. A good strategy is to explore efficiently and avoid revisiting the same tiles while searching for diamonds. ## Available Actions You must respond your next action in JSON format with what action you want to take. Give no explanations just return the JSON. ### Move This action allows you to move one emoji away from your current location, choosing between the four available direct tiles around you (🚶): "up", "right", "down", or "left". You are only allowed to move on the empty ⬜ square, so always make sure that your target square is a ⬜. { "action": "move", "detail": move direction, can be "up", "right", "down", or "left" } ### Pick This action allows you to pick up an object from an adjacent tile (up, right, down, or left). The object will be added to your inventory and removed from the map. { "action": "pick", "detail": direction of the object to pick, can be "up", "right", "down", or "left" }`; // OpenAI client configured for HuggingFace router const client = new OpenAI({ baseURL: "https://router.huggingface.co/v1", apiKey: HF_TOKEN, }); export class AIService { /** * Get the next action from the AI agent * Returns a validated action object { action: "move"|"pick", detail: "up"|"down"|"left"|"right" } */ static async getNextAction( map: string, history: any[] ): Promise<{ action: string; detail: string }> { const messages: OpenAI.ChatCompletionMessageParam[] = [ { role: "system", content: SYS_PROMPT, }, ...history.slice(-15), { role: "user", content: `Please decide on what action to do next and provide the corresponding JSON response. Here is the current map: ${map} `, }, ]; // Log messages for debugging Bun.write("messages.json", JSON.stringify(messages, null, 2)); try { const chatCompletion = await client.chat.completions.create({ model: MODEL, messages, temperature: 0.7, }); let content = chatCompletion.choices[0].message.content?.trim() ?? ""; console.log("Raw AI response:", content); // Strip markdown code blocks if present if (content.startsWith("```")) { content = content.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, ""); } // Parse and validate the response const parsed = JSON.parse(content); if (this.isValidAction(parsed)) { return { action: parsed.action, detail: parsed.detail.toLowerCase(), }; } throw new Error("Invalid action format"); } catch (error) { console.error("Error getting AI action:", error); // Return a safe fallback action if anything fails return { action: "move", detail: "right" }; } } private static isValidAction(obj: any): boolean { if (!obj || typeof obj !== "object" || typeof obj.action !== "string") { return false; } if (obj.action === "move" || obj.action === "pick") { return ( typeof obj.detail === "string" && ["up", "down", "left", "right"].includes(obj.detail.toLowerCase()) ); } return false; } }