diff --git a/deep-sea-stories/packages/backend/src/agent/elevenlabs/api.ts b/deep-sea-stories/packages/backend/src/agent/elevenlabs/api.ts deleted file mode 100644 index f27a1ba..0000000 --- a/deep-sea-stories/packages/backend/src/agent/elevenlabs/api.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { ElevenLabsClient } from '@elevenlabs/elevenlabs-js'; -import { - ClientTools, - Conversation, -} from '@elevenlabs/elevenlabs-js/api/resources/conversationalAi/conversation/index.js'; -import type { Story } from '../../types.js'; -import { - getFirstMessageForStory, - getInstructionsForStory, - getToolDescriptionForStory, -} from '../../utils.js'; -import type { AgentConfig, VoiceAgentApi } from '../api.js'; -import type { VoiceAgentSession } from '../session.js'; -import { ForwardingAudioInterface } from './audioInterface.js'; -import { ElevenLabsSession } from './session.js'; - -export class ElevenLabsApi implements VoiceAgentApi { - private elevenLabs: ElevenLabsClient; - - constructor(apiKey: string) { - this.elevenLabs = new ElevenLabsClient({ apiKey }); - } - - async createAgentSession(config: AgentConfig): Promise { - const story = config.story; - const instructions = getInstructionsForStory(story); - const firstMessage = getFirstMessageForStory(story); - const endGameToolId = await this.ensureGameEndingTool(story); - - console.log( - `Creating ElevenLabs agent for story "${story.title}" (ID: ${story.id})`, - ); - - const prompt = { prompt: instructions, toolIds: [endGameToolId] }; - - const params = { - conversationConfig: { - conversation: { - maxDurationSeconds: config.gameTimeLimitSeconds, - }, - agent: { - firstMessage, - language: 'en', - prompt, - }, - }, - }; - - const { agentId } = - await this.elevenLabs.conversationalAi.agents.create(params); - - return await this.createElevenLabsSession(config, agentId); - } - - private async createElevenLabsSession(config: AgentConfig, agentId: string) { - const clientTools = new ClientTools(); - clientTools.register('endGame', (_) => config.onEndGame()); - - const audioInterface = new ForwardingAudioInterface(); - - const conversation = new Conversation({ - agentId, - requiresAuth: false, - audioInterface, - clientTools, - callbackAgentResponse: (response) => config.onTranscription(response), - }); - - return new ElevenLabsSession(audioInterface, conversation); - } - - private async ensureGameEndingTool(story: Story): Promise { - const toolName = 'endGame'; - - const toolDescription = getToolDescriptionForStory(story); - - const { tools: allTools } = - await this.elevenLabs.conversationalAi.tools.list(); - const existingTool = allTools.find( - ({ toolConfig }) => - toolConfig.type === 'client' && - toolConfig.name === toolName && - toolConfig.description === toolDescription, - ); - - if (existingTool) return existingTool.id; - - const createdTool = await this.elevenLabs.conversationalAi.tools.create({ - toolConfig: { - type: 'client', - name: toolName, - description: toolDescription, - }, - }); - - return createdTool.id; - } -} diff --git a/deep-sea-stories/packages/backend/src/agent/elevenlabs/audioInterface.ts b/deep-sea-stories/packages/backend/src/agent/elevenlabs/audioInterface.ts deleted file mode 100644 index 004b0f3..0000000 --- a/deep-sea-stories/packages/backend/src/agent/elevenlabs/audioInterface.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { AudioInterface } from '@elevenlabs/elevenlabs-js/api/resources/conversationalAi/conversation/index.js'; - -export class ForwardingAudioInterface extends AudioInterface { - private inputCallback: ((audio: Buffer) => void) | null = null; - private onAgentAudio: ((audio: Buffer) => void) | null = null; - private onInterrupt: (() => void) | null = null; - - setInterruptCallback(onInterrupt: () => void) { - this.onInterrupt = onInterrupt; - } - - setAgentAudioCallback(onAgentAudio: (audio: Buffer) => void) { - this.onAgentAudio = onAgentAudio; - } - - sendAudio(audio: Buffer): void { - if (!this.inputCallback) return; - - this.inputCallback(audio); - } - - start(inputCallback: (audio: Buffer) => void): void { - this.inputCallback = inputCallback; - } - - stop(): void { - this.inputCallback = null; - } - - output(buffer: Buffer): void { - if (buffer.length <= 0) { - console.warn('[Audio Interface] Received empty audio buffer from agent'); - return; - } - - if (!this.onAgentAudio) console.error('Agent callback missing!'); - - this.onAgentAudio?.(buffer); - } - - interrupt(): void { - console.warn('Agent interrupted!'); - this.onInterrupt?.(); - } -} diff --git a/deep-sea-stories/packages/backend/src/agent/elevenlabs/session.ts b/deep-sea-stories/packages/backend/src/agent/elevenlabs/session.ts deleted file mode 100644 index 7483acd..0000000 --- a/deep-sea-stories/packages/backend/src/agent/elevenlabs/session.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { Conversation } from '@elevenlabs/elevenlabs-js/api/resources/conversationalAi/conversation/index.js'; -import type { VoiceAgentSession } from '../session.js'; -import type { ForwardingAudioInterface } from './audioInterface.js'; - -export class ElevenLabsSession implements VoiceAgentSession { - private session: Conversation; - private audioInterface: ForwardingAudioInterface; - - constructor(audioInterface: ForwardingAudioInterface, session: Conversation) { - this.audioInterface = audioInterface; - this.session = session; - } - - sendAudio(audio: Buffer) { - this.audioInterface.sendAudio(this.boostAudioVolume(audio, 7.0)); - } - - registerInterruptionCallback(onInterrupt: () => void) { - this.audioInterface.setInterruptCallback(onInterrupt); - } - - registerAgentAudioCallback(onAgentAudio: (audio: Buffer) => void) { - this.audioInterface.setAgentAudioCallback(onAgentAudio); - } - - async announceTimeExpired() { - console.log('ElevenLabs session time expired (handled by platform)'); - } - - async open() { - await this.session.startSession(); - } - - async close(_wait: boolean) { - this.session.endSession(); - } - - private boostAudioVolume(audioBuffer: Buffer, gain = 2.0): Buffer { - for (let offset = 0; offset < audioBuffer.length - 1; offset += 2) { - const sample = audioBuffer.readInt16LE(offset); - const amplified = Math.round(sample * gain); - const clamped = Math.max(-32768, Math.min(32767, amplified)); - audioBuffer.writeInt16LE(clamped, offset); - } - - return audioBuffer; - } -} diff --git a/deep-sea-stories/packages/backend/src/prompts/first-message-template.md b/deep-sea-stories/packages/backend/src/prompts/first-message-template.md deleted file mode 100644 index 4f533bb..0000000 --- a/deep-sea-stories/packages/backend/src/prompts/first-message-template.md +++ /dev/null @@ -1,9 +0,0 @@ -Welcome to Deep Sea Stories! I'm your riddle master for today. - -Here's the scenario: {{ FRONT }} - -Your mission is to uncover the full story behind this intriguing situation. You can ask me yes or no questions to piece together what really happened. - -When you think you've solved the mystery, simply say "I'm guessing now..." followed by your solution. - -You have {{ TIME_LIMIT }} minutes to solve the riddle, good luck! diff --git a/deep-sea-stories/packages/backend/src/prompts/instructions-template.md b/deep-sea-stories/packages/backend/src/prompts/instructions-template.md index c9c2a11..9a6ab44 100644 --- a/deep-sea-stories/packages/backend/src/prompts/instructions-template.md +++ b/deep-sea-stories/packages/backend/src/prompts/instructions-template.md @@ -1,21 +1,40 @@ -You are a riddle master, tasked with playing Deep Sea Stories. +You are a riddle master playing Deep Sea Stories. -## Voice & Output Format -- **Crucial:** You must speak in a fluid, continuous manner. -- **Do not** pause or wait for user confirmation in the middle of a response. -- Ensure all your responses are at most a **single continuous paragraph**. +## Voice & Pacing (CRITICAL) + +- **Pacing:** Speak at a STANDARD, STEADY conversational rate. Do not drag your words. Do not drawl. +- **Tone:** Cold, detached, and clinical. +- **Vibe:** You are not a ghost; you are the ocean itself—indifferent and unyielding. +- **Energy:** Calm but ALERT. +- Your horror comes from how _smoothly_ and _casually_ you describe scary stories, not from sounding tired. + +## Rules + +- Do not be helpful; be efficient and uncaring. +- Do not pause or wait for user confirmation mid-response. +- Keep responses to a single continuous paragraph. +- Answer "Yes", "No", or "Irrelevant" strictly. + +## Atmosphere + +- You are the Abyss. Unsettling, deep, and emotionless. ## Gameplay + Deep Sea Stories is a storytelling and guessing game with a "partial story" and "full story": ### partial story + {{ FRONT }} ### full story + {{ BACK }} ## Critical Tool Usage + **You have access to a tool named `endGame`.** + - This tool is the **only way** to mark the game as won. - Merely saying "You won" is NOT enough. - If the user guesses correctly, you **MUST** call this tool immediately after your closing speech. @@ -32,6 +51,7 @@ When the user wants to guess the story, they will start by saying something like **Criteria for a Correct Guess:** Their guess is correct if: + 1. The guess is consistent with the full story. 2. The guess identifies the core cause of the event. @@ -48,6 +68,7 @@ If their guess is correct, execute following sequence STRICTLY in this order: **Procedure for an Incorrect Guess:** If their guess is not correct, then **DO NOT TELL THEM THE FULL STORY**, instead: + - If their guess is inconsistent with the full story: Tell the user that their guess is wrong and the part of the guess that is wrong. - If their guess is consistent with the full story, but does not identify the core cause: Tell the user they're on the right track and give the user an example part from the **partial story** they have not explained. @@ -69,7 +90,7 @@ To EVERY question about the full story, respond with EXACTLY ONE FULL sentence. When asked to "introduce yourself" by the user, you MUST respond with these exact words: Welcome to Deep Sea Stories! I'm your riddle master for today. -Here's the scenario: {{ FRONT }} -Your mission is to uncover the full story behind this intriguing situation. You can ask me yes or no questions to piece together what really happened. +Your mission is to uncover the full story behind an intriguing situation. You can ask me yes or no questions to piece together what really happened. When you think you've solved the mystery, simply say "I'm guessing now..." followed by your solution. +Here's the scenario: {{ FRONT }} You have {{ TIME_LIMIT }} minutes to solve the riddle, good luck!