diff --git a/components/CustomPersonalityInput.tsx b/components/CustomPersonalityInput.tsx index 9e744e4..5f044d6 100644 --- a/components/CustomPersonalityInput.tsx +++ b/components/CustomPersonalityInput.tsx @@ -1,5 +1,5 @@ -import { MAX_PERSONALITY_LENGTH } from "lib/debate/inputValidation.ts"; import { personalities } from "lib/debate/personalities.ts"; +import { MAX_PERSONALITY_LENGTH } from "lib/debate/schema.ts"; import { cleanContent, sanitizeInput } from "lib/utils.ts"; import { useEffect, useState } from "preact/hooks"; diff --git a/components/DebateFormInputs.tsx b/components/DebateFormInputs.tsx index 217e1b4..fce4124 100644 --- a/components/DebateFormInputs.tsx +++ b/components/DebateFormInputs.tsx @@ -1,4 +1,5 @@ import AgentSelector from "islands/AgentSelector.tsx"; +import type { Personality } from "lib/debate/personalities.ts"; import { MAX_AGENTS, MAX_DEBATE_CONTEXT_LENGTH, @@ -6,8 +7,7 @@ import { MAX_POSITION_LENGTH, MIN_AGENTS, MIN_DEBATE_ROUNDS, -} from "lib/debate/inputValidation.ts"; -import type { Personality } from "lib/debate/personalities.ts"; +} from "lib/debate/schema.ts"; import { cleanContent, sanitizeInput } from "lib/utils.ts"; import type { VoiceType } from "routes/api/voicesynth.tsx"; diff --git a/deno.json b/deno.json index 945e91e..bf4243a 100644 --- a/deno.json +++ b/deno.json @@ -38,7 +38,8 @@ "routes/": "./routes/", "tailwindcss": "npm:tailwindcss@3.4.10", "tailwindcss/": "npm:/tailwindcss@3.4.10/", - "tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js" + "tailwindcss/plugin": "npm:/tailwindcss@3.4.1/plugin.js", + "zod": "https://deno.land/x/zod@v3.23.8/mod.ts" }, "compilerOptions": { "jsx": "react-jsx", diff --git a/hooks/useDebateState.ts b/hooks/useDebateState.ts index e701472..bb81032 100644 --- a/hooks/useDebateState.ts +++ b/hooks/useDebateState.ts @@ -1,14 +1,13 @@ +import { validateDebateInput } from "lib/debate/inputValidation.ts"; +import { getRandomPersonalities, Personality } from "lib/debate/personalities.ts"; import { MAX_AGENTS, MAX_DEBATE_ROUNDS, MIN_AGENTS, MIN_DEBATE_ROUNDS, - validateDebateInput, -} from "lib/debate/inputValidation.ts"; -import { getRandomPersonalities, Personality } from "lib/debate/personalities.ts"; +} from "lib/debate/schema.ts"; import { clamp, sanitizeInput } from "lib/utils.ts"; import { useCallback, useEffect, useRef, useState } from "preact/hooks"; -import type { AgentDetails } from "routes/api/debate.tsx"; import { DEFAULT_VOICE, type VoiceType } from "routes/api/voicesynth.tsx"; type DebateMessage = { role: string; content: string }; @@ -73,15 +72,19 @@ export function useDebateState() { const sanitizedPosition = sanitizeInput(position); const sanitizedContext = sanitizeInput(context); - const { errors, valid } = validateDebateInput({ - agentDetails: agentDetails as AgentDetails[], - context: sanitizedContext, + const requestPayload = { position: sanitizedPosition, + context: sanitizedContext, numAgents, numDebateRounds, - uuid: userUUID, + agentDetails, moderatorVoice, - }); + uuid: userUUID, + }; + + console.log("Request payload:", requestPayload); + + const { errors, valid } = validateDebateInput(requestPayload); if (!valid) { setErrors(errors); diff --git a/islands/AgentSelector.tsx b/islands/AgentSelector.tsx index 9cf4dc9..ba13f07 100644 --- a/islands/AgentSelector.tsx +++ b/islands/AgentSelector.tsx @@ -1,6 +1,6 @@ import CustomPersonalityInput from "components/CustomPersonalityInput.tsx"; -import { MAX_NAME_LENGTH } from "lib/debate/inputValidation.ts"; import type { Personality } from "lib/debate/personalities.ts"; +import { MAX_NAME_LENGTH } from "lib/debate/schema.ts"; import { cleanContent, sanitizeInput } from "lib/utils.ts"; interface AgentSelectorProps { diff --git a/lib/debate/debate.ts b/lib/debate/debate.ts index 7f546b1..83f43d4 100644 --- a/lib/debate/debate.ts +++ b/lib/debate/debate.ts @@ -1,5 +1,5 @@ +import type { DebateRequest } from "lib/debate/schema.ts"; import type { OpenAI } from "openai"; -import type { DebateRequest } from "routes/api/debate.tsx"; import { Moderator } from "./moderator.ts"; import { createSystemMessage, makeAPIRequest } from "./submit.ts"; diff --git a/lib/debate/inputValidation.ts b/lib/debate/inputValidation.ts index 6e167f5..039135e 100644 --- a/lib/debate/inputValidation.ts +++ b/lib/debate/inputValidation.ts @@ -1,14 +1,6 @@ +import { MAX_AGENTS, MAX_DEBATE_CONTEXT_LENGTH, MAX_DEBATE_ROUNDS, MAX_NAME_LENGTH, MAX_PERSONALITY_LENGTH, MAX_POSITION_LENGTH, MIN_AGENTS, MIN_DEBATE_ROUNDS } from "lib/debate/schema.ts"; import type { DebateRequest } from "routes/api/debate.tsx"; -export const MIN_AGENTS = 2; -export const MAX_AGENTS = 4; -export const MAX_DEBATE_CONTEXT_LENGTH = 1028; -export const MIN_DEBATE_ROUNDS = 1; -export const MAX_DEBATE_ROUNDS = 3; -export const MAX_NAME_LENGTH = 32; -export const MAX_POSITION_LENGTH = 256; -export const MAX_PERSONALITY_LENGTH = 256; - export interface InputValidationResponse { valid: boolean; errors: string[]; diff --git a/lib/debate/schema.ts b/lib/debate/schema.ts new file mode 100644 index 0000000..d43c654 --- /dev/null +++ b/lib/debate/schema.ts @@ -0,0 +1,34 @@ +import { type VoiceType } from "routes/api/voicesynth.tsx"; +import { z } from "zod"; + +export const MIN_AGENTS = 2; +export const MAX_AGENTS = 4; +export const MAX_DEBATE_CONTEXT_LENGTH = 1028; +export const MIN_DEBATE_ROUNDS = 1; +export const MAX_DEBATE_ROUNDS = 3; +export const MAX_NAME_LENGTH = 32; +export const MAX_POSITION_LENGTH = 256; +export const MAX_PERSONALITY_LENGTH = 256; + +export const AgentDetailsSchema = z.object({ + name: z.string().min(1).max(MAX_NAME_LENGTH), + personality: z.string().min(1).max(MAX_PERSONALITY_LENGTH), + stance: z.enum(["for", "against", "undecided", "moderator"]), + voice: z.enum([z.custom()]), +}); + +export const DebateRequestSchema = z.object({ + position: z.string().min(1).max(MAX_POSITION_LENGTH), + context: z.string().max(MAX_DEBATE_CONTEXT_LENGTH).default(""), + numAgents: z.number().int().min(MIN_AGENTS).max(MAX_AGENTS), + agentDetails: z.array(AgentDetailsSchema), + uuid: z.string().uuid(), + numDebateRounds: z.number().int().min(MIN_DEBATE_ROUNDS).max(MAX_DEBATE_ROUNDS), + moderatorVoice: z.union([z.enum(["none"]), z.custom()]), +}).refine(data => data.agentDetails.length === data.numAgents, { + message: "Number of agent details must match numAgents", + path: ["agentDetails"], +}); + +export type AgentDetails = z.infer; +export type DebateRequest = z.infer; diff --git a/routes/api/debate.tsx b/routes/api/debate.tsx index eee5cf7..d96bc96 100644 --- a/routes/api/debate.tsx +++ b/routes/api/debate.tsx @@ -1,26 +1,9 @@ import { Handlers } from "$fresh/server.ts"; import { conductDebateStream } from "lib/debate/debate.ts"; -import { validateDebateInput } from "lib/debate/inputValidation.ts"; +import { DebateRequestSchema } from "lib/debate/schema.ts"; import { compressJson, kv } from "lib/kv.ts"; -import type { VoiceType } from "routes/api/voicesynth.tsx"; - -export interface AgentDetails { - name: string; - personality: string; - stance: "for" | "against" | "undecided" | "moderator"; - voice: string; -} - -export interface DebateRequest { - position: string; - context: string; - numAgents: number; - agentDetails: AgentDetails[]; - uuid: string; - numDebateRounds: number; - moderatorVoice: VoiceType | "none"; -} +export type { AgentDetails, DebateRequest } from "lib/debate/schema.ts"; export type DebateResponse = ReadableStream | { errors: string[] }; export const handler: Handlers = { @@ -34,34 +17,37 @@ export const handler: Handlers = { }); } - const input = await req.json() as DebateRequest; + const input = await req.json(); - const { errors, valid } = validateDebateInput(input); - if (!valid) { - return new Response(JSON.stringify({ errors }), { + const result = DebateRequestSchema.safeParse(input); + if (!result.success) { + console.error("Validation errors:", result.error.errors); + return new Response(JSON.stringify({ errors: result.error.errors.map(e => e.message) }), { status: 400, headers: { "Content-Type": "application/json" }, }); } + const validatedInput = result.data; + try { const timestamp = new Date().toISOString(); const data = JSON.stringify({ - position: input.position, - context: input.context, - numAgents: input.numAgents, - agentDetails: input.agentDetails, - numDebateRounds: input.numDebateRounds, - moderatorVoice: input.moderatorVoice, + position: validatedInput.position, + context: validatedInput.context, + numAgents: validatedInput.numAgents, + agentDetails: validatedInput.agentDetails, + numDebateRounds: validatedInput.numDebateRounds, + moderatorVoice: validatedInput.moderatorVoice, timestamp, }); - await kv.set(["debates", input.uuid, timestamp], compressJson(data)); + await kv.set(["debates", validatedInput.uuid, timestamp], compressJson(data)); } catch (error) { console.error("Error logging debate details:", error); } try { - const stream = await conductDebateStream(input); + const stream = await conductDebateStream(validatedInput); if (!(stream instanceof ReadableStream)) { throw new Error("Invalid stream returned from conductDebateStream"); }