All files / web/src/app/api/realtime/euclid/think-hard route.ts

0% Statements 0/138
0% Branches 0/1
0% Functions 0/1
0% Lines 0/138

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139                                                                                                                                                                                                                                                                                     
/**
 * API route for the "think hard" tool — GPT-5.2 Responses API with vision.
 *
 * POST /api/realtime/euclid/think-hard
 * Body: { question, effort, screenshot, proofState, propositionId, currentStep }
 * Returns: { answer: string }
 *
 * Uses the OpenAI Responses API with configurable reasoning effort.
 */

import { NextResponse } from 'next/server'
import { withAuth } from '@/lib/auth/withAuth'
import { recordOpenAiResponsesUsage } from '@/lib/ai-usage/helpers'
import { AiFeature } from '@/lib/ai-usage/features'
import { PROP_REGISTRY } from '@/components/toys/euclid/propositions/registry'
import {
  PROPOSITION_SUMMARIES,
  buildReferenceContext,
} from '@/components/toys/euclid/agent/euclidReferenceContext'

const VALID_EFFORTS = ['low', 'medium', 'high', 'xhigh'] as const
type Effort = (typeof VALID_EFFORTS)[number]

// Map our effort levels to the Responses API reasoning.effort values
const EFFORT_MAP: Record<Effort, string> = {
  low: 'low',
  medium: 'medium',
  high: 'high',
  xhigh: 'high', // 'xhigh' maps to 'high' — the Responses API doesn't support 'xhigh'
}

export const POST = withAuth(async (request, { userId }) => {
  try {
    const body = await request.json()
    const { question, effort, screenshot, proofState, propositionId, currentStep } = body

    if (!question || typeof question !== 'string') {
      return NextResponse.json({ error: 'question is required' }, { status: 400 })
    }

    const effortLevel = VALID_EFFORTS.includes(effort) ? (effort as Effort) : 'medium'

    const apiKey = process.env.LLM_OPENAI_API_KEY || process.env.OPENAI_API_KEY
    if (!apiKey) {
      return NextResponse.json({ error: 'OpenAI API key not configured' }, { status: 503 })
    }

    // Build context for the reasoning model
    const propId = typeof propositionId === 'number' ? propositionId : 1
    const prop = PROP_REGISTRY[propId]
    const propSummary = PROPOSITION_SUMMARIES[propId]
    const referenceContext = buildReferenceContext(propId)

    const systemText = `You are a geometry reasoning engine analyzing Euclid's Elements Book I.

=== CURRENT PROPOSITION ===
Proposition I.${propId}: "${propSummary?.statement ?? prop?.title ?? 'Unknown'}" (${propSummary?.type ?? 'Unknown'})
Current step: ${typeof currentStep === 'number' ? currentStep + 1 : 'unknown'}

=== PROOF STATE ===
${typeof proofState === 'string' ? proofState : 'Not available'}

=== REFERENCE MATERIAL ===
${referenceContext}

=== TASK ===
Answer the following geometric question. Be rigorous and cite specific postulates, definitions, common notions, and previously proven propositions by name. If the question involves the visual construction, analyze the screenshot carefully.

Keep your answer concise but thorough — it will be spoken aloud by an AI character playing Euclid.`

    // Build input content parts
    const contentParts: Array<Record<string, unknown>> = [
      { type: 'input_text', text: `${systemText}\n\nQuestion: ${question}` },
    ]

    // Add screenshot if available
    if (screenshot && typeof screenshot === 'string') {
      const base64 = screenshot.includes(',') ? screenshot.split(',')[1] : screenshot
      contentParts.push({
        type: 'input_image',
        image_url: `data:image/png;base64,${base64}`,
      })
    }

    // Call the Responses API
    const response = await fetch('https://api.openai.com/v1/responses', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: 'gpt-5.2',
        input: [
          {
            role: 'user',
            content: contentParts,
          },
        ],
        reasoning: {
          effort: EFFORT_MAP[effortLevel],
        },
      }),
    })

    if (!response.ok) {
      const errText = await response.text()
      console.error('[think-hard] API error:', response.status, errText)
      return NextResponse.json(
        { error: 'Could not work through the proof right now.' },
        { status: 502 }
      )
    }

    const data = await response.json()
    recordOpenAiResponsesUsage(data, { userId, feature: AiFeature.EUCLID_THINK_HARD })

    // Extract text from response output
    let answer = 'I could not find the answer in my writings.'
    if (data.output && Array.isArray(data.output)) {
      for (const item of data.output) {
        if (item.type === 'message' && Array.isArray(item.content)) {
          for (const part of item.content) {
            if (part.type === 'output_text' && part.text) {
              answer = part.text
              break
            }
          }
        }
      }
    }

    return NextResponse.json({ answer })
  } catch (error) {
    console.error('[think-hard] Error:', error)
    return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
  }
})