Skip to content

Commit

Permalink
feat: dynamic example (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
matthew1809 authored Dec 10, 2024
1 parent bf161b7 commit c33f8f7
Show file tree
Hide file tree
Showing 23 changed files with 9,657 additions and 3,645 deletions.
4 changes: 4 additions & 0 deletions typescript/examples/dynamic/conversational-agent/.example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
NEXT_PUBLIC_ELEVEN_LABS_AGENT_ID=
NEXT_PUBLIC_SEPOLIA_RPC_URL=
NEXT_PUBLIC_COINGECKO_API_KEY=
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=
40 changes: 40 additions & 0 deletions typescript/examples/dynamic/conversational-agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
12 changes: 12 additions & 0 deletions typescript/examples/dynamic/conversational-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# conversational-agent

## 0.1.1

### Patch Changes

- Updated dependencies [f5ea302]
- Updated dependencies [e61b658]
- @goat-sdk/wallet-viem@0.1.4
- @goat-sdk/core@0.3.11
- @goat-sdk/adapter-eleven-labs@0.1.3
- @goat-sdk/plugin-coingecko@0.1.1
30 changes: 30 additions & 0 deletions typescript/examples/dynamic/conversational-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Dynamic + Eleven Labs Conversational Agent Example

This example uses the ElevenLabs base project described [here](https://elevenlabs.io/docs/conversational-ai/guides/conversational-ai-guide-nextjs) and follows the steps to add client tool calling described [here](https://elevenlabs.io/docs/conversational-ai/customization/client-tools) as well as the Dynamic SDK to connect to the wallet and handle network switching etc.

Video demo: https://www.loom.com/share/5a89a3fc47a54df18ab051901bf7cc9b

## Setup

1. Copy the `.example.env` and populate with your values.

```
cp .example.env .env
```

2. Create an ElevenLabs agent and get the agent ID. You can follow this guide: https://elevenlabs.io/docs/conversational-ai/docs/agent-setup

3. Set the `NEXT_PUBLIC_ELEVEN_LABS_AGENT_ID` environment variable with the agent ID.

4. ElevenLabs requires you to register each tool manually through the ElevenLabs dashboard. To make it easier, we've added a `logTools` option to the `getOnChainTools` function. This will log the tools with their respective descriptions and parameters to the console.

```typescript
const tools = await getOnChainTools({
wallet: viem(wallet),
options: {
logTools: true,
},
});
```

5. Run the app with `pnpm dev`, connect your wallet, and start the conversation!
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
};

export default nextConfig;
33 changes: 33 additions & 0 deletions typescript/examples/dynamic/conversational-agent/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "dynamic-conversational-agent",
"version": "0.1.1",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@11labs/react": "^0.0.4",
"@dynamic-labs/ethereum": "^3.0.0",
"@dynamic-labs/sdk-react-core": "^3.0.0",
"@dynamic-labs/wagmi-connector": "3.9.0",
"@goat-sdk/adapter-eleven-labs": "workspace:*",
"@goat-sdk/core": "workspace:*",
"@goat-sdk/plugin-coingecko": "workspace:*",
"@goat-sdk/wallet-viem": "workspace:*",
"@tanstack/react-query": "^5.62.2",
"next": "15.0.3",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"viem": "^2.21.49",
"wagmi": "^2.13.3"
},
"devDependencies": {
"@types/react": "^18",
"@types/react-dom": "^18",
"postcss": "^8",
"tailwindcss": "^3.4.1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};

export default config;
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"use client";

import { useConversation } from "@11labs/react";
import { getOnChainTools } from "@goat-sdk/adapter-eleven-labs";
import { useCallback, useEffect } from "react";

import { coingecko } from "@goat-sdk/plugin-coingecko";
import { viem } from "@goat-sdk/wallet-viem";
import { DynamicWidget, getNetwork, useSwitchNetwork, useDynamicContext } from "@dynamic-labs/sdk-react-core";
import { isEthereumWallet } from "@dynamic-labs/ethereum";
import { useAccount, useWalletClient } from "wagmi";
import { sendETH } from "../../../../../../packages/core/dist/plugins/send-eth";
import { sepolia } from "viem/chains";

export function Conversation() {
const { primaryWallet, sdkHasLoaded } = useDynamicContext();
const switchNetwork = useSwitchNetwork();

const checkAndSwitchNetwork = async () => {
if (!primaryWallet) {
throw new Error("Wallet not connected");
}

if (!isEthereumWallet(primaryWallet)) {
throw new Error("Wallet is not Ethereum");
}

const network = await getNetwork(primaryWallet?.connector);

if (network !== sepolia.id) {
await switchNetwork({ wallet: primaryWallet, network: sepolia.id });
}
};

useEffect(() => {
if (primaryWallet) {
checkAndSwitchNetwork();
}
}, [primaryWallet]);

const { isConnected } = useAccount();
const { data: wallet } = useWalletClient(); // Get the viem wallet client from Wagmi

console.log("wallet", wallet);

const conversation = useConversation({
onConnect: () => console.log("Connected"),
onDisconnect: () => console.log("Disconnected"),
onMessage: (message) => console.log("Message:", message),
onError: (error) => console.error("Error:", error),
});

const startConversation = useCallback(async () => {
try {
// Request microphone permission
await navigator.mediaDevices.getUserMedia({ audio: true });

if (!wallet) {
throw new Error("Wallet not connected");
}

// const wallet = viem Client
const tools = await getOnChainTools({
wallet: viem(wallet),
plugins: [
sendETH(),
coingecko({
apiKey: process.env.NEXT_PUBLIC_COINGECKO_API_KEY ?? "",
}),
],
options: {
logTools: true,
},
});

// Start the conversation with your agent
await conversation.startSession({
agentId: process.env.NEXT_PUBLIC_ELEVEN_LABS_AGENT_ID ?? "", // Replace with your agent ID
clientTools: tools,
});
} catch (error) {
console.error("Failed to start conversation:", error);
}
}, [conversation, wallet]);

const stopConversation = useCallback(async () => {
await conversation.endSession();
}, [conversation]);

if (!sdkHasLoaded) {
return <div>Loading...</div>;
}

return (
<div className="flex flex-col items-center gap-4">
<h1 className="text-2xl font-bold">{isConnected ? "1. You're Connected" : "1. Connect Wallet to start"}</h1>
<DynamicWidget />

<h1 className="text-2xl font-bold">2. Start Conversation with Agent</h1>
<div className="flex flex-col items-center gap-4">
<div className="flex gap-2">
<button
onClick={startConversation}
disabled={conversation.status === "connected" || !isConnected}
className="px-4 py-2 bg-blue-500 text-white rounded disabled:bg-gray-300"
type="button"
>
Start Conversation
</button>
<button
onClick={stopConversation}
disabled={conversation.status !== "connected"}
className="px-4 py-2 bg-red-500 text-white rounded disabled:bg-gray-300"
type="button"
>
Stop Conversation
</button>
</div>

<div className="flex flex-col items-center">
<p>Status: {conversation.status}</p>
{conversation.status === "connected" && (
<p>Agent is {conversation.isSpeaking ? "speaking" : "listening"}</p>
)}
</div>
</div>
</div>
);
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--background: #ffffff;
--foreground: #171717;
}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}

body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import { Web3Provider } from "./providers";

const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<Web3Provider>{children}</Web3Provider>
</body>
</html>
);
}
12 changes: 12 additions & 0 deletions typescript/examples/dynamic/conversational-agent/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Conversation } from "./components/conversation";

export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm">
<h1 className="text-4xl font-bold mb-8 text-center">GOAT 🐐 Conversational AI</h1>
<Conversation />
</div>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";

import { DynamicContextProvider } from "@dynamic-labs/sdk-react-core";
import { DynamicWagmiConnector } from "@dynamic-labs/wagmi-connector";
import { http, createConfig, WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { sepolia } from "viem/chains";
import { EthereumWalletConnectors } from "@dynamic-labs/ethereum";

const config = createConfig({
chains: [sepolia],
multiInjectedProviderDiscovery: false,
transports: {
[sepolia.id]: http(process.env.NEXT_PUBLIC_SEPOLIA_RPC_URL ?? ""),
},
});

const queryClient = new QueryClient();

export const Web3Provider = ({ children }: { children: React.ReactNode }) => {
return (
<DynamicContextProvider
settings={{
// Find your environment id at https://app.dynamic.xyz/dashboard/developer
environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID ?? "",
walletConnectors: [EthereumWalletConnectors],
}}
>
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<DynamicWagmiConnector>
{children}
</DynamicWagmiConnector>
</QueryClientProvider>
</WagmiProvider>
</DynamicContextProvider>
);
};
Loading

0 comments on commit c33f8f7

Please sign in to comment.