├── pnpm-workspace.yaml ├── apps ├── web │ ├── .dockerignore │ ├── .prettierrc │ ├── postcss.config.mjs │ ├── src │ │ ├── app │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── page.tsx │ │ │ ├── layout.tsx │ │ │ └── globals.css │ │ ├── lib │ │ │ ├── utils.ts │ │ │ ├── api-key.tsx │ │ │ ├── agent-inbox-interrupt.ts │ │ │ └── ensure-tool-responses.ts │ │ ├── providers │ │ │ ├── client.ts │ │ │ └── Thread.tsx │ │ ├── components │ │ │ ├── ui │ │ │ │ ├── skeleton.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── textarea.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── password-input.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── tooltip.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── sheet.tsx │ │ │ │ └── audio-player.tsx │ │ │ ├── thread │ │ │ │ ├── utils.ts │ │ │ │ ├── markdown-styles.css │ │ │ │ ├── tooltip-icon-button.tsx │ │ │ │ ├── syntax-highlighter.tsx │ │ │ │ ├── agent-inbox │ │ │ │ │ ├── components │ │ │ │ │ │ ├── tool-call-table.tsx │ │ │ │ │ │ ├── thread-id.tsx │ │ │ │ │ │ ├── thread-actions-view.tsx │ │ │ │ │ │ └── state-view.tsx │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── hooks │ │ │ │ │ │ └── use-interrupted-actions.tsx │ │ │ │ ├── messages │ │ │ │ │ ├── human.tsx │ │ │ │ │ ├── generic-interrupt.tsx │ │ │ │ │ ├── shared.tsx │ │ │ │ │ ├── tool-calls.tsx │ │ │ │ │ └── ai.tsx │ │ │ │ ├── history │ │ │ │ │ └── index.tsx │ │ │ │ └── markdown-text.tsx │ │ │ ├── icons │ │ │ │ ├── github.tsx │ │ │ │ └── langgraph.tsx │ │ │ ├── debug-stream │ │ │ │ └── index.tsx │ │ │ ├── audio-visualizer-fallback.tsx │ │ │ └── audio-visualizer-with-fallback.tsx │ │ └── hooks │ │ │ ├── useMediaQuery.tsx │ │ │ ├── useTTSPlayer.tsx │ │ │ └── useSpeechRecording.tsx │ ├── next.config.mjs │ ├── turbo.json │ ├── .gitignore │ ├── components.json │ ├── tsconfig.json │ ├── eslint.config.js │ ├── README.md │ ├── tailwind.config.js │ └── package.json └── agents │ ├── src │ └── react-agent │ │ ├── tests │ │ ├── unit │ │ │ └── graph.test.ts │ │ └── integration │ │ │ └── graph.int.test.ts │ │ ├── static │ │ └── studio_ui.png │ │ ├── prompts.ts │ │ ├── configuration.ts │ │ ├── utils.ts │ │ ├── tools.ts │ │ ├── graph.ts │ │ ├── speech-to-text.ts │ │ └── text-to-speech.ts │ ├── turbo.json │ ├── .prettierrc │ ├── tsconfig.json │ ├── package.json │ ├── test-tts.js │ └── eslint.config.js ├── langgraph.json ├── .env.sample ├── turbo.json ├── tsconfig.json ├── .gitignore ├── package.json ├── LICENSE ├── README.md └── TTS_SETUP.md /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'apps/*' 3 | -------------------------------------------------------------------------------- /apps/web/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .git 4 | .env -------------------------------------------------------------------------------- /apps/web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /apps/web/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /apps/web/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjichat/voice_agent_base/HEAD/apps/web/src/app/favicon.ico -------------------------------------------------------------------------------- /apps/agents/src/react-agent/tests/unit/graph.test.ts: -------------------------------------------------------------------------------- 1 | import { it } from "@jest/globals"; 2 | 3 | it("Test", async () => {}); 4 | -------------------------------------------------------------------------------- /apps/web/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /apps/web/src/app/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjichat/voice_agent_base/HEAD/apps/web/src/app/favicon-16x16.png -------------------------------------------------------------------------------- /apps/web/src/app/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjichat/voice_agent_base/HEAD/apps/web/src/app/favicon-32x32.png -------------------------------------------------------------------------------- /apps/web/src/app/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjichat/voice_agent_base/HEAD/apps/web/src/app/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/web/src/app/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjichat/voice_agent_base/HEAD/apps/web/src/app/android-chrome-192x192.png -------------------------------------------------------------------------------- /apps/web/src/app/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjichat/voice_agent_base/HEAD/apps/web/src/app/android-chrome-512x512.png -------------------------------------------------------------------------------- /apps/agents/src/react-agent/static/studio_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjichat/voice_agent_base/HEAD/apps/agents/src/react-agent/static/studio_ui.png -------------------------------------------------------------------------------- /apps/web/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /langgraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "node_version": "20", 3 | "dependencies": [ 4 | "." 5 | ], 6 | "graphs": { 7 | "agent": "./apps/agents/src/react-agent/graph.ts:graph" 8 | }, 9 | "env": ".env" 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/src/providers/client.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "@langchain/langgraph-sdk"; 2 | 3 | export function createClient(apiUrl: string, apiKey: string | undefined) { 4 | return new Client({ 5 | apiKey, 6 | apiUrl, 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /apps/web/src/lib/api-key.tsx: -------------------------------------------------------------------------------- 1 | export function getApiKey(): string | null { 2 | try { 3 | if (typeof window === "undefined") return null; 4 | return window.localStorage.getItem("lg:chat:apiKey") ?? null; 5 | } catch { 6 | // no-op 7 | } 8 | 9 | return null; 10 | } 11 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | LANGSMITH_API_KEY=blabla232323 2 | LANGSMITH_TRACING_V2="true" 3 | LANGSMITH_PROJECT="your_app" 4 | OPENAI_API_KEY="sk-proj-hackme23232323232dfdegerngfhbg45" 5 | ELEVENLABS_API_KEY="sk_544b148b2this4707wont2f3work391c09b3fd55f92" 6 | TAVILY_API_KEY="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" -------------------------------------------------------------------------------- /apps/agents/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["//"], 3 | "tasks": { 4 | "build": { 5 | "outputs": ["**/dist/**"] 6 | }, 7 | "build:internal": { 8 | "dependsOn": ["^build:internal"] 9 | }, 10 | "dev": { 11 | "dependsOn": ["^dev"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["//"], 3 | "tasks": { 4 | "build": { 5 | "outputs": [".next/**", "!.next/cache/**"] 6 | }, 7 | "build:internal": { 8 | "dependsOn": ["^build:internal"] 9 | }, 10 | "dev": { 11 | "dependsOn": ["^dev"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ); 11 | } 12 | 13 | export { Skeleton }; 14 | -------------------------------------------------------------------------------- /apps/web/src/components/thread/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Message } from "@langchain/langgraph-sdk"; 2 | 3 | export function getContentString(content: Message["content"]): string { 4 | if (typeof content === "string") return content; 5 | const texts = content 6 | .filter((c): c is { type: "text"; text: string } => c.type === "text") 7 | .map((c) => c.text); 8 | return texts.join(" "); 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # LangGraph API 27 | .langgraph_api 28 | .env 29 | .next/ 30 | next-env.d.ts -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env"], 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**", ".next/**", "!.next/cache/**"] 8 | }, 9 | "lint": { 10 | "dependsOn": ["^lint"] 11 | }, 12 | "lint:fix": { 13 | "dependsOn": ["^lint:fix"] 14 | }, 15 | "format": { 16 | "dependsOn": ["^format"] 17 | }, 18 | "dev": { 19 | "dependsOn": ["^dev"] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/src/hooks/useMediaQuery.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export function useMediaQuery(query: string) { 4 | const [matches, setMatches] = useState(false); 5 | 6 | useEffect(() => { 7 | const media = window.matchMedia(query); 8 | setMatches(media.matches); 9 | 10 | const listener = (e: MediaQueryListEvent) => setMatches(e.matches); 11 | media.addEventListener("change", listener); 12 | return () => media.removeEventListener("change", listener); 13 | }, [query]); 14 | 15 | return matches; 16 | } 17 | -------------------------------------------------------------------------------- /apps/agents/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "printWidth": 80, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "semi": true, 7 | "singleQuote": false, 8 | "quoteProps": "as-needed", 9 | "jsxSingleQuote": false, 10 | "trailingComma": "es5", 11 | "bracketSpacing": true, 12 | "arrowParens": "always", 13 | "requirePragma": false, 14 | "insertPragma": false, 15 | "proseWrap": "preserve", 16 | "htmlWhitespaceSensitivity": "css", 17 | "vueIndentScriptAndStyle": false, 18 | "endOfLine": "lf" 19 | } 20 | -------------------------------------------------------------------------------- /apps/agents/src/react-agent/tests/integration/graph.int.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect } from "@jest/globals"; 2 | import { BaseMessage } from "@langchain/core/messages"; 3 | 4 | import { graph } from "../../graph.js"; 5 | 6 | it("Simple runthrough", async () => { 7 | const res = await graph.invoke({ 8 | messages: [ 9 | { 10 | role: "user", 11 | content: "What is the current weather in SF?", 12 | }, 13 | ], 14 | }); 15 | expect( 16 | res.messages.find((message: BaseMessage) => message._getType() === "tool"), 17 | ).toBeDefined(); 18 | }); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ] 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { VoiceOnlyInterface } from "@/components/voice-only-interface"; 4 | import { StreamProvider } from "@/providers/Stream"; 5 | import { ThreadProvider } from "@/providers/Thread"; 6 | import { Toaster } from "@/components/ui/sonner"; 7 | import React from "react"; 8 | 9 | export default function DemoPage(): React.ReactNode { 10 | return ( 11 | Loading...
}> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | function Label({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ); 20 | } 21 | 22 | export { Label }; 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | **/node_modules 6 | /.pnp 7 | .pnp.js 8 | .yarn/install-state.gz 9 | .yarn/cache 10 | 11 | # testing 12 | /coverage 13 | 14 | # next.js 15 | /.next/ 16 | /out/ 17 | 18 | # production 19 | /build 20 | /dist 21 | **/dist 22 | .turbo/ 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | 33 | # local env files 34 | .env*.local 35 | .env 36 | 37 | # vercel 38 | .vercel 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | next-env.d.ts 43 | 44 | credentials.json 45 | 46 | # LangGraph API 47 | .langgraph_api 48 | 49 | apps/agents/tts-outputs* 50 | -------------------------------------------------------------------------------- /apps/agents/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src", 6 | "baseUrl": ".", 7 | "target": "ES2021", 8 | "lib": ["ES2021", "ES2022.Object", "DOM", "es2023"], 9 | "module": "NodeNext", 10 | "moduleResolution": "NodeNext", 11 | "esModuleInterop": true, 12 | "declaration": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "useDefineForClassFields": true, 18 | "strictPropertyInitialization": false, 19 | "allowJs": true, 20 | "strict": true 21 | }, 22 | "include": ["src/"], 23 | "exclude": ["node_modules/", "dist"] 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import "./globals.css"; 3 | import { Inter } from "next/font/google"; 4 | import React from "react"; 5 | import { NuqsAdapter } from "nuqs/adapters/next/app"; 6 | 7 | const inter = Inter({ 8 | subsets: ["latin"], 9 | preload: true, 10 | display: "swap", 11 | }); 12 | 13 | export const metadata: Metadata = { 14 | title: "Pod", 15 | description: "Pod is a voice-only interface for AI agents.", 16 | }; 17 | 18 | export default function RootLayout({ 19 | children, 20 | }: Readonly<{ 21 | children: React.ReactNode; 22 | }>) { 23 | return ( 24 | 25 | 26 | {children} 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /apps/web/src/lib/agent-inbox-interrupt.ts: -------------------------------------------------------------------------------- 1 | import { HumanInterrupt } from "@langchain/langgraph/prebuilt"; 2 | 3 | export function isAgentInboxInterruptSchema( 4 | value: unknown, 5 | ): value is HumanInterrupt | HumanInterrupt[] { 6 | const valueAsObject = Array.isArray(value) ? value[0] : value; 7 | return ( 8 | valueAsObject && 9 | typeof valueAsObject === "object" && 10 | "action_request" in valueAsObject && 11 | typeof valueAsObject.action_request === "object" && 12 | "config" in valueAsObject && 13 | typeof valueAsObject.config === "object" && 14 | "allow_respond" in valueAsObject.config && 15 | "allow_accept" in valueAsObject.config && 16 | "allow_edit" in valueAsObject.config && 17 | "allow_ignore" in valueAsObject.config 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /apps/agents/src/react-agent/prompts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Default prompts used by the agent. 3 | */ 4 | 5 | export const SYSTEM_PROMPT_TEMPLATE = `You are a helpful AI assistant with access to real-time web search capabilities. 6 | 7 | You can search the web for current information when needed to answer questions accurately. Use the search tool when: 8 | - The user asks about current events, news, or recent information 9 | - You need up-to-date facts, statistics, or data 10 | - The question requires information beyond your training data 11 | 12 | Please keep your responses concise and informative. When using search results, cite the information appropriately. 13 | 14 | Your output will be converted to audio, so keep your responses short and concise in a conversational tone. 15 | 16 | System time: {system_time}`; 17 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | function Separator({ 7 | className, 8 | orientation = "horizontal", 9 | decorative = true, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 23 | ); 24 | } 25 | 26 | export { Separator }; 27 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |