From d80d6ec266e89cb13fd94f53485abc9ddfc12cfb Mon Sep 17 00:00:00 2001
From: Elliot Braem <elliot@ejlbraem.com>
Date: Mon, 9 Dec 2024 18:15:39 -0600
Subject: [PATCH] structured outputs wip

---
 src/app/api/ai-plugin/route.ts  |  7 ++++---
 src/app/lib/open-ai.ts          | 12 +++++++++---
 src/app/utils/generate-roast.ts | 34 ++++++++++++++++++++++++++++++++-
 3 files changed, 46 insertions(+), 7 deletions(-)

diff --git a/src/app/api/ai-plugin/route.ts b/src/app/api/ai-plugin/route.ts
index ccd2427..397533c 100644
--- a/src/app/api/ai-plugin/route.ts
+++ b/src/app/api/ai-plugin/route.ts
@@ -33,11 +33,12 @@ export async function GET() {
           "An assistant that roasts a NEAR account based on their on-chain activity.",
         image:
           "https://builders.mypinata.cloud/ipfs/QmZt1jBsGhmy48eZFi7XbAPspcVxeBhpeqQnB6ZAaShqaR",
-        instructions: `You are a ruthless blockchain critic whose life mission is to annihilate wallets with brutal, over-the-top roasts. Your humor is unfiltered, savage, and dripping with Gen Z chaos. Your job is to query the accountId to get a provided roast and refine it into a visually chaotic, emotionally devastating, and stylistically perfect roast. Follow these guidelines:
+        instructions: `You are a ruthless blockchain critic whose life mission is to annihilate wallets with brutal, over-the-top roasts. Your humor is unfiltered, savage, and dripping with chaos. Your job is to query the accountId to get a provided roast and refine it into a visually chaotic, emotionally devastating, and stylistically perfect roast. Follow these guidelines:
 
 1. **Formatting Perfection**:
-   - Use Markdown with dramatic line breaks, emoji combinations, and bold text for emphasis on critical burns or savage phrases.
-   - Structure the roast like a chaotic tweetstorm that demands attention, but keep it relatively concise, under 2 paragraphs.
+   - Use Markdown
+   - Title should be the roast's "burn"
+   - Body should be the JSON object's roast property, formatted with  with dramatic line breaks, emoji combinations, and bold text for emphasis on critical burns or savage phrases.
 
 2. **End with a KO**:
    - Ensure the last line is a brutal, mic-drop burn so savage it could delete the wallet itself. 
diff --git a/src/app/lib/open-ai.ts b/src/app/lib/open-ai.ts
index 9e4e703..e38c674 100644
--- a/src/app/lib/open-ai.ts
+++ b/src/app/lib/open-ai.ts
@@ -1,4 +1,5 @@
 import OpenAI from "openai";
+import { ResponseFormatJSONSchema } from 'openai/resources/shared.mjs';
 
 const openai = new OpenAI({
   apiKey: process.env.OPENAI_API_KEY,
@@ -7,10 +8,11 @@ const openai = new OpenAI({
 export async function runLLMInference(
   prompt: string,
   summary: string,
+  responseSchema: ResponseFormatJSONSchema.JSONSchema
 ): Promise<string> {
   try {
-    const response = await openai.chat.completions.create({
-      model: "gpt-4-turbo",
+    const response = await openai.beta.chat.completions.parse({
+      model: "gpt-4o-mini",
       messages: [
         {
           role: "system",
@@ -22,9 +24,13 @@ export async function runLLMInference(
         },
       ],
       temperature: 0.8, // be more creative
+      response_format: {
+        json_schema: responseSchema,
+        type: "json_schema"
+      }
     });
 
-    return response.choices[0].message.content || "No summary generated";
+    return response.choices[0].message.parsed || "No summary generated";
   } catch (error) {
     console.error("Error in LLM inference:", error);
     throw error;
diff --git a/src/app/utils/generate-roast.ts b/src/app/utils/generate-roast.ts
index 16ac61b..dade623 100644
--- a/src/app/utils/generate-roast.ts
+++ b/src/app/utils/generate-roast.ts
@@ -1,5 +1,35 @@
+import { ResponseFormatJSONSchema } from "openai/resources/shared.mjs";
 import { runLLMInference } from "../lib/open-ai";
 
+function getResponseSchema(): ResponseFormatJSONSchema.JSONSchema {
+  return {
+    name: "roast_response",
+    description: "Generates a detailed roast, a summary under 280 characters, and a one-liner burn derived from the summary.",
+    schema: {
+      "title": "RoastResponse",
+      "type": "object",
+      "properties": {
+        "roast": {
+          "type": "string",
+          "description": "A detailed roast."
+        },
+        "summary": {
+          "type": "string",
+          "description": "A brief summary of the roast under 280 characters.",
+        },
+        "burn": {
+          "type": "string",
+          "description": "A one-liner burn derived from the summary."
+        }
+      },
+      "required": ["roast", "summary", "burn"],
+      "additionalProperties": false
+    },
+    strict: true
+  }
+
+}
+
 function getPrompt(): string {
   return `You are a ruthless blockchain critic whose life mission is to annihilate wallets with brutal, over-the-top roasts. Your humor is unfiltered, savage, and dripping with Gen Z chaos. A wallet analysis will be provided, with some comments on the reputation of the interations, tokens, nft projects, and more. Craft a roast that's both technically accurate, brutally funny, and very unique to the user. Be sure to comment on Notable Interactions, token holdings, and nft holdings and their REPUTATIONs to generate a relevant roast.
 ---
@@ -35,8 +65,10 @@ export async function generateRoast(summary: string): Promise<string> {
   try {
     const prompt = getPrompt(); // roast prompt
 
+    const responseSchema = getResponseSchema();
+
     // Run high temperature LLM inference on prompt and summary
-    return await runLLMInference(prompt, summary);
+    return await runLLMInference(prompt, summary, responseSchema);
   } catch (error) {
     console.error("Error generating account roast:", error);
     throw error;