fix(gameplay): start campaigns in playable state
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -1,17 +1,9 @@
|
||||
import { z } from 'zod'
|
||||
import { buildJsonCompletion, parseJsonCompletion } from '~/server/utils/ai-provider'
|
||||
import { CoauthorQuestionSchema, StoredCoauthorQuestionSchema } from '~/server/utils/coauthor-question'
|
||||
import { requireStageTwoSafeText, requireStageTwoUser, stageTwoApiError, stageTwoDatabase, stageTwoRpc, stageTwoUuid } from '~/server/utils/stage-two-supabase'
|
||||
|
||||
const MessageSchema = z.object({ role: z.enum(['user', 'assistant']), content: z.string().min(1).max(5000) })
|
||||
const QuestionSchema = z.object({
|
||||
question: z.string().trim().min(5).max(300),
|
||||
options: z.array(z.string().trim().min(1).max(100)).length(3),
|
||||
})
|
||||
const StoredQuestionSchema = z.object({
|
||||
id: z.string().trim().min(1).max(80),
|
||||
label: z.string().trim().min(5).max(300),
|
||||
options: z.array(z.string().trim().min(1).max(100)).length(3),
|
||||
})
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
@@ -27,29 +19,42 @@ export default defineEventHandler(async (event) => {
|
||||
const answerCount = Math.max(0, messages.filter(message => message.role === 'user').length - 1)
|
||||
if (answerCount >= 4) return { readyToGenerate: true }
|
||||
if (session.current_question) {
|
||||
return { readyToGenerate: false, question: StoredQuestionSchema.parse(session.current_question) }
|
||||
const storedQuestion = StoredCoauthorQuestionSchema.safeParse(session.current_question)
|
||||
if (storedQuestion.success) return { readyToGenerate: false, question: storedQuestion.data }
|
||||
}
|
||||
|
||||
const completion = buildJsonCompletion(useRuntimeConfig(), {
|
||||
system: `You are the Dungeons & Ground coauthor. Based only on this conversation, ask one focused question that makes the original TTRPG world more playable. This is clarification ${answerCount + 1} of 4. Do not repeat a topic already answered. Adapt to the requested genre. Use the language of the first user message for the question and every answer option unless the user explicitly requests another language. Provide exactly three concise, mutually distinct answer options. Keep everything suitable for ages 13+.`,
|
||||
messages,
|
||||
schemaName: 'coauthor_question',
|
||||
jsonSchema: QuestionSchema.toJSONSchema(),
|
||||
})
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 30_000)
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(completion.endpoint, {
|
||||
method: 'POST', headers: completion.headers, body: JSON.stringify(completion.body), signal: controller.signal,
|
||||
let question: z.infer<typeof CoauthorQuestionSchema> | undefined
|
||||
for (let attempt = 0; attempt < 2 && !question; attempt += 1) {
|
||||
const correction = attempt
|
||||
? ' The previous response was malformed. Ensure every option is a natural-language answer, not punctuation, a JSON key, or a copy of the question.'
|
||||
: ''
|
||||
const completion = buildJsonCompletion(useRuntimeConfig(), {
|
||||
system: `You are the Dungeons & Ground coauthor. Based only on this conversation, ask one focused question that makes the original TTRPG world more playable. This is clarification ${answerCount + 1} of 4. Do not repeat a topic already answered. Adapt to the requested genre. Use the language of the first user message for the question and every answer option unless the user explicitly requests another language. Provide exactly three concise, mutually distinct natural-language answers to the question. Never use punctuation or JSON field names as an option, and never repeat the question as an option. Keep everything suitable for ages 13+.${correction}`,
|
||||
messages,
|
||||
schemaName: 'coauthor_question',
|
||||
jsonSchema: CoauthorQuestionSchema.toJSONSchema(),
|
||||
})
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 30_000)
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(completion.endpoint, {
|
||||
method: 'POST', headers: completion.headers, body: JSON.stringify(completion.body), signal: controller.signal,
|
||||
})
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
if (!response.ok) throw createError({ statusCode: 502, statusMessage: 'The coauthor is temporarily unavailable.' })
|
||||
try {
|
||||
const parsed = CoauthorQuestionSchema.safeParse(parseJsonCompletion(await response.json()))
|
||||
if (parsed.success) question = parsed.data
|
||||
} catch {
|
||||
// Retry once when the provider ignores structured-output requirements.
|
||||
}
|
||||
}
|
||||
if (!response.ok) throw createError({ statusCode: 502, statusMessage: 'The coauthor is temporarily unavailable.' })
|
||||
const question = QuestionSchema.parse(parseJsonCompletion(await response.json()))
|
||||
if (!question) throw createError({ statusCode: 502, statusMessage: 'The coauthor returned invalid answer options. Try the question again.' })
|
||||
requireStageTwoSafeText([question.question, ...question.options].join('\n'))
|
||||
const storedQuestion = StoredQuestionSchema.parse({
|
||||
const storedQuestion = StoredCoauthorQuestionSchema.parse({
|
||||
id: `question-${answerCount + 1}`,
|
||||
label: question.question,
|
||||
options: question.options,
|
||||
|
||||
Reference in New Issue
Block a user