Skip to Content
音声ターンテイキング

ターン制AIディベート

以下のコードスニペットは、Mastraを使ってターン制のマルチエージェント会話システムを実装する方法を示しています。この例では、2人のAIエージェント(楽観主義者と懐疑主義者)がユーザーが提供したトピックについてディベートを行い、お互いの意見に順番に応答します。

音声機能を持つエージェントの作成

まず、異なる個性と音声機能を持つ2つのエージェントを作成します。

src/mastra/agents/index.ts
import { openai } from "@ai-sdk/openai"; import { Agent } from "@mastra/core/agent"; import { OpenAIVoice } from "@mastra/voice-openai"; export const optimistAgent = new Agent({ name: "Optimist", instructions: "You are an optimistic debater who sees the positive side of every topic. Keep your responses concise and engaging, about 2-3 sentences.", model: openai("gpt-4o"), voice: new OpenAIVoice({ speaker: "alloy", }), }); export const skepticAgent = new Agent({ name: "Skeptic", instructions: "You are a RUDE skeptical debater who questions assumptions and points out potential issues. Keep your responses concise and engaging, about 2-3 sentences.", model: openai("gpt-4o"), voice: new OpenAIVoice({ speaker: "echo", }), });

Mastra へのエージェントの登録

次に、両方のエージェントをあなたの Mastra インスタンスに登録します。

src/mastra/index.ts
import { createLogger } from "@mastra/core/logger"; import { Mastra } from "@mastra/core/mastra"; import { optimistAgent, skepticAgent } from "./agents"; export const mastra = new Mastra({ agents: { optimistAgent, skepticAgent, }, logger: createLogger({ name: "Mastra", level: "info", }), });

ディベートにおけるターンテイキングの管理

この例では、エージェント同士のターンテイキングの流れを管理し、各エージェントが前のエージェントの発言に応答することを確実にする方法を示します。

src/debate/turn-taking.ts
import { mastra } from "../../mastra"; import { playAudio, Recorder } from "@mastra/node-audio"; import * as p from "@clack/prompts"; // Helper function to format text with line wrapping function formatText(text: string, maxWidth: number): string { const words = text.split(" "); let result = ""; let currentLine = ""; for (const word of words) { if (currentLine.length + word.length + 1 <= maxWidth) { currentLine += (currentLine ? " " : "") + word; } else { result += (result ? "\n" : "") + currentLine; currentLine = word; } } if (currentLine) { result += (result ? "\n" : "") + currentLine; } return result; } // Initialize audio recorder const recorder = new Recorder({ outputPath: "./debate.mp3", }); // Process one turn of the conversation async function processTurn( agentName: "optimistAgent" | "skepticAgent", otherAgentName: string, topic: string, previousResponse: string = "", ) { const agent = mastra.getAgent(agentName); const spinner = p.spinner(); spinner.start(`${agent.name} is thinking...`); let prompt; if (!previousResponse) { // First turn prompt = `Discuss this topic: ${topic}. Introduce your perspective on it.`; } else { // Responding to the other agent prompt = `The topic is: ${topic}. ${otherAgentName} just said: "${previousResponse}". Respond to their points.`; } // Generate text response const { text } = await agent.generate(prompt, { temperature: 0.9, }); spinner.message(`${agent.name} is speaking...`); // Convert to speech and play const audioStream = await agent.voice.speak(text, { speed: 1.2, responseFormat: "wav", // Optional: specify a response format }); if (audioStream) { audioStream.on("data", (chunk) => { recorder.write(chunk); }); } spinner.stop(`${agent.name} said:`); // Format the text to wrap at 80 characters for better display const formattedText = formatText(text, 80); p.note(formattedText, agent.name); if (audioStream) { const speaker = playAudio(audioStream); await new Promise<void>((resolve) => { speaker.once("close", () => { resolve(); }); }); } return text; } // Main function to run the debate export async function runDebate(topic: string, turns: number = 3) { recorder.start(); p.intro("AI Debate - Two Agents Discussing a Topic"); p.log.info(`Starting a debate on: ${topic}`); p.log.info( `The debate will continue for ${turns} turns each. Press Ctrl+C to exit at any time.`, ); let optimistResponse = ""; let skepticResponse = ""; const responses = []; for (let turn = 1; turn <= turns; turn++) { p.log.step(`Turn ${turn}`); // Optimist's turn optimistResponse = await processTurn( "optimistAgent", "Skeptic", topic, skepticResponse, ); responses.push({ agent: "Optimist", text: optimistResponse, }); // Skeptic's turn skepticResponse = await processTurn( "skepticAgent", "Optimist", topic, optimistResponse, ); responses.push({ agent: "Skeptic", text: skepticResponse, }); } recorder.end(); p.outro("Debate concluded! The full audio has been saved to debate.mp3"); return responses; }

コマンドラインからディベートを実行する

コマンドラインからディベートを実行するためのシンプルなスクリプトはこちらです:

src/index.ts
import { runDebate } from "./debate/turn-taking"; import * as p from "@clack/prompts"; async function main() { // Get the topic from the user const topic = await p.text({ message: "Enter a topic for the agents to discuss:", placeholder: "Climate change", validate(value) { if (!value) return "Please enter a topic"; return; }, }); // Exit if cancelled if (p.isCancel(topic)) { p.cancel("Operation cancelled."); process.exit(0); } // Get the number of turns const turnsInput = await p.text({ message: "How many turns should each agent have?", placeholder: "3", initialValue: "3", validate(value) { const num = parseInt(value); if (isNaN(num) || num < 1) return "Please enter a positive number"; return; }, }); // Exit if cancelled if (p.isCancel(turnsInput)) { p.cancel("Operation cancelled."); process.exit(0); } const turns = parseInt(turnsInput as string); // Run the debate await runDebate(topic as string, turns); } main().catch((error) => { p.log.error("An error occurred:"); console.error(error); process.exit(1); });

ディベート用のウェブインターフェースを作成する

ウェブアプリケーションの場合、ユーザーがディベートを開始し、エージェントの応答を聞くことができるシンプルなNext.jsコンポーネントを作成できます。

app/components/DebateInterface.tsx
"use client"; import { useState, useRef } from "react"; import { MastraClient } from "@mastra/client-js"; const mastraClient = new MastraClient({ baseUrl: process.env.NEXT_PUBLIC_MASTRA_URL || "http://localhost:4111", }); export default function DebateInterface() { const [topic, setTopic] = useState(""); const [turns, setTurns] = useState(3); const [isDebating, setIsDebating] = useState(false); const [responses, setResponses] = useState<any[]>([]); const [isPlaying, setIsPlaying] = useState(false); const audioRef = useRef<HTMLAudioElement>(null); // Function to start the debate const startDebate = async () => { if (!topic) return; setIsDebating(true); setResponses([]); try { const optimist = mastraClient.getAgent("optimistAgent"); const skeptic = mastraClient.getAgent("skepticAgent"); const newResponses = []; let optimistResponse = ""; let skepticResponse = ""; for (let turn = 1; turn <= turns; turn++) { // Optimist's turn let prompt; if (turn === 1) { prompt = `Discuss this topic: ${topic}. Introduce your perspective on it.`; } else { prompt = `The topic is: ${topic}. Skeptic just said: "${skepticResponse}". Respond to their points.`; } const optimistResult = await optimist.generate({ messages: [{ role: "user", content: prompt }], }); optimistResponse = optimistResult.text; newResponses.push({ agent: "Optimist", text: optimistResponse, }); // Update UI after each response setResponses([...newResponses]); // Skeptic's turn prompt = `The topic is: ${topic}. Optimist just said: "${optimistResponse}". Respond to their points.`; const skepticResult = await skeptic.generate({ messages: [{ role: "user", content: prompt }], }); skepticResponse = skepticResult.text; newResponses.push({ agent: "Skeptic", text: skepticResponse, }); // Update UI after each response setResponses([...newResponses]); } } catch (error) { console.error("Error starting debate:", error); } finally { setIsDebating(false); } }; // Function to play audio for a specific response const playAudio = async (text: string, agent: string) => { if (isPlaying) return; try { setIsPlaying(true); const agentClient = mastraClient.getAgent( agent === "Optimist" ? "optimistAgent" : "skepticAgent", ); const audioResponse = await agentClient.voice.speak(text); if (!audioResponse.body) { throw new Error("No audio stream received"); } // Convert stream to blob const reader = audioResponse.body.getReader(); const chunks = []; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); } const blob = new Blob(chunks, { type: "audio/mpeg" }); const url = URL.createObjectURL(blob); if (audioRef.current) { audioRef.current.src = url; audioRef.current.onended = () => { setIsPlaying(false); URL.revokeObjectURL(url); }; audioRef.current.play(); } } catch (error) { console.error("Error playing audio:", error); setIsPlaying(false); } }; return ( <div className="max-w-4xl mx-auto p-4"> <h1 className="text-2xl font-bold mb-4">ターン制AIディベート</h1> <div className="mb-6"> <label className="block mb-2">ディベートのトピック:</label> <input type="text" value={topic} onChange={(e) => setTopic(e.target.value)} className="w-full p-2 border rounded" placeholder="例:気候変動、AI倫理、宇宙探査" /> </div> <div className="mb-6"> <label className="block mb-2">各エージェントのターン数:</label> <input type="number" value={turns} onChange={(e) => setTurns(parseInt(e.target.value))} min={1} max={10} className="w-full p-2 border rounded" /> </div> <button onClick={startDebate} disabled={isDebating || !topic} className="px-4 py-2 bg-blue-500 text-white rounded disabled:bg-gray-300" > {isDebating ? "ディベート進行中..." : "ディベート開始"} </button> <audio ref={audioRef} className="hidden" /> {responses.length > 0 && ( <div className="mt-8"> <h2 className="text-xl font-semibold mb-4">ディベート記録</h2> <div className="space-y-4"> {responses.map((response, index) => ( <div key={index} className={`p-4 rounded ${ response.agent === "Optimist" ? "bg-blue-100" : "bg-gray-100" }`} > <div className="flex justify-between items-center"> <div className="font-bold">{response.agent}</div> <button onClick={() => playAudio(response.text, response.agent)} disabled={isPlaying} className="text-sm px-2 py-1 bg-blue-500 text-white rounded disabled:bg-gray-300" > {isPlaying ? "再生中..." : "再生"} </button> </div> <p className="mt-2">{response.text}</p> </div> ))} </div> </div> )} </div> ); }

この例では、Mastra を使用してターン制のマルチエージェント会話システムを作成する方法を示します。エージェントたちはユーザーが選んだトピックについてディベートを行い、それぞれが前のエージェントの発言に応答します。また、システムは各エージェントの応答を音声に変換し、没入感のあるディベート体験を提供します。

AI Debate with Turn Taking の完全な実装は、私たちの GitHub リポジトリでご覧いただけます。






GitHubで例を見る