diff --git a/README.md b/README.md index 9f2b26d..d64b9a2 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,14 @@ yarn gen:keys > See all the available [examples](/examples/). +### Integrations + +Examples integrating XMTP with external libraries from the ecosystem + +- [grok](/integrations/grok/): Integrate XMTP to the Grok API + +> See all the available [examples](/integrations/). + ## Web inbox Interact with the XMTP network using [xmtp.chat](https://xmtp.chat), the official web inbox for developers. diff --git a/examples/gpt/index.ts b/examples/gpt/index.ts index c02fc3f..44c400e 100644 --- a/examples/gpt/index.ts +++ b/examples/gpt/index.ts @@ -47,7 +47,7 @@ async function main() { await client.conversations.sync(); console.log( - `Agent initialized on ${client.accountAddress}\nSend a message on http://xmtp.chat/dm/${client.accountAddress}`, + `Agent initialized on ${client.accountAddress}\nSend a message on http://xmtp.chat/dm/${client.accountAddress}?env=${env}`, ); console.log("Waiting for messages..."); diff --git a/integrations/grok/.env.example b/integrations/grok/.env.example new file mode 100644 index 0000000..9eeda7f --- /dev/null +++ b/integrations/grok/.env.example @@ -0,0 +1,5 @@ +WALLET_KEY= # the private key of the wallet +ENCRYPTION_KEY= # a second random 32 bytes encryption key for local db encryption + +# grok api key +GROK_API_KEY= # the API key for the Grok API diff --git a/integrations/grok/README.md b/integrations/grok/README.md new file mode 100644 index 0000000..87c8797 --- /dev/null +++ b/integrations/grok/README.md @@ -0,0 +1,158 @@ +# Grok agent example + +This example uses the [Grok](https://x.ai/api) API for responses and [XMTP](https://xmtp.org) for secure messaging. You can test your agent on [xmtp.chat](https://xmtp.chat) or any other XMTP-compatible client. + +## Environment variables + +Add the following keys to a `.env` file: + +```bash +WALLET_KEY= # the private key of the wallet +ENCRYPTION_KEY= # a second random 32 bytes encryption key for local db encryption +GROK_API_KEY= # the API key for the Grok API +``` + +You can generate random keys with the following command: + +```bash +yarn gen:keys +``` + +> [!WARNING] +> Running the `gen:keys` script will overwrite the existing `.env` file. + +## Usage + +```tsx +import { Client, type XmtpEnv } from "@xmtp/node-sdk"; +import { createSigner, getEncryptionKeyFromHex } from "@/helpers"; + +/* Get the wallet key associated to the public key of + * the agent and the encryption key for the local db + * that stores your agent's messages */ +const { WALLET_KEY, ENCRYPTION_KEY, GROK_API_KEY } = process.env; + +/* Check if the environment variables are set */ +if (!WALLET_KEY) { + throw new Error("WALLET_KEY must be set"); +} + +/* Check if the encryption key is set */ +if (!ENCRYPTION_KEY) { + throw new Error("ENCRYPTION_KEY must be set"); +} + +/* Check if the Grok API key is set */ +if (!GROK_API_KEY) { + throw new Error("GROK_API_KEY must be set"); +} + +/* Create the signer using viem and parse the encryption key for the local db */ +const signer = createSigner(WALLET_KEY); +const encryptionKey = getEncryptionKeyFromHex(ENCRYPTION_KEY); + +/* Set the environment to dev or production */ +const env: XmtpEnv = "dev"; + +/** + * Main function to run the agent + */ +async function main() { + console.log(`Creating client on the '${env}' network...`); + /* Initialize the xmtp client */ + const client = await Client.create(signer, encryptionKey, { + env, + }); + + console.log("Syncing conversations..."); + /* Sync the conversations from the network to update the local db */ + await client.conversations.sync(); + + console.log( + `Agent initialized on ${client.accountAddress}\nSend a message on http://xmtp.chat/dm/${client.accountAddress}?env=${env}`, + ); + console.log("Waiting for messages..."); + /* Stream all messages from the network */ + const stream = client.conversations.streamAllMessages(); + + for await (const message of await stream) { + /* Ignore messages from the same agent or non-text messages */ + if ( + message?.senderInboxId.toLowerCase() === client.inboxId.toLowerCase() || + message?.contentType?.typeId !== "text" + ) { + continue; + } + + console.log( + `Received message: ${message.content as string} by ${message.senderInboxId}`, + ); + + /* Get the conversation from the local db */ + const conversation = client.conversations.getConversationById( + message.conversationId, + ); + + /* If the conversation is not found, skip the message */ + if (!conversation) { + console.log("Unable to find conversation, skipping"); + continue; + } + + try { + /* Get the AI response from Grok */ + const response = await fetch("https://api.x.ai/v1/chat/completions", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${GROK_API_KEY}`, // Use the same API key variable + }, + body: JSON.stringify({ + messages: [ + { role: "system", content: "You are a test assistant." }, + { role: "user", content: message.content as string }, + ], + model: "grok-2-latest", + stream: false, + temperature: 0, + }), + }).then( + (res) => + res.json() as Promise<{ + choices: { message: { content: string } }[]; + }>, + ); + const aiResponse = response.choices[0]?.message?.content || ""; + console.log(`Sending AI response: ${aiResponse}`); + /* Send the AI response to the conversation */ + await conversation.send(aiResponse); + } catch (error) { + console.error("Error getting AI response:", error); + await conversation.send( + "Sorry, I encountered an error processing your message.", + ); + } + + console.log("Waiting for messages..."); + } +} + +main().catch(console.error); +``` + +## Run the agent + +```bash +# git clone repo +git clone https://github.com/ephemeraHQ/xmtp-agent-examples.git +# go to the folder +cd xmtp-agent-examples +cd integrations +cd grok +# install packages +yarn +# generate random keys (optional) +yarn gen:keys +# run the example +yarn dev +``` diff --git a/integrations/grok/index.ts b/integrations/grok/index.ts new file mode 100644 index 0000000..cbac903 --- /dev/null +++ b/integrations/grok/index.ts @@ -0,0 +1,114 @@ +import { Client, type XmtpEnv } from "@xmtp/node-sdk"; +import { createSigner, getEncryptionKeyFromHex } from "@/helpers"; + +/* Get the wallet key associated to the public key of + * the agent and the encryption key for the local db + * that stores your agent's messages */ +const { WALLET_KEY, ENCRYPTION_KEY, GROK_API_KEY } = process.env; + +/* Check if the environment variables are set */ +if (!WALLET_KEY) { + throw new Error("WALLET_KEY must be set"); +} + +/* Check if the encryption key is set */ +if (!ENCRYPTION_KEY) { + throw new Error("ENCRYPTION_KEY must be set"); +} + +/* Check if the Grok API key is set */ +if (!GROK_API_KEY) { + throw new Error("GROK_API_KEY must be set"); +} + +/* Create the signer using viem and parse the encryption key for the local db */ +const signer = createSigner(WALLET_KEY); +const encryptionKey = getEncryptionKeyFromHex(ENCRYPTION_KEY); + +/* Set the environment to dev or production */ +const env: XmtpEnv = "dev"; + +/** + * Main function to run the agent + */ +async function main() { + console.log(`Creating client on the '${env}' network...`); + /* Initialize the xmtp client */ + const client = await Client.create(signer, encryptionKey, { + env, + }); + + console.log("Syncing conversations..."); + /* Sync the conversations from the network to update the local db */ + await client.conversations.sync(); + + console.log( + `Agent initialized on ${client.accountAddress}\nSend a message on http://xmtp.chat/dm/${client.accountAddress}?env=${env}`, + ); + console.log("Waiting for messages..."); + /* Stream all messages from the network */ + const stream = client.conversations.streamAllMessages(); + + for await (const message of await stream) { + /* Ignore messages from the same agent or non-text messages */ + if ( + message?.senderInboxId.toLowerCase() === client.inboxId.toLowerCase() || + message?.contentType?.typeId !== "text" + ) { + continue; + } + + console.log( + `Received message: ${message.content as string} by ${message.senderInboxId}`, + ); + + /* Get the conversation from the local db */ + const conversation = client.conversations.getConversationById( + message.conversationId, + ); + + /* If the conversation is not found, skip the message */ + if (!conversation) { + console.log("Unable to find conversation, skipping"); + continue; + } + + try { + /* Get the AI response from Grok */ + const response = await fetch("https://api.x.ai/v1/chat/completions", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${GROK_API_KEY}`, // Use the same API key variable + }, + body: JSON.stringify({ + messages: [ + { role: "system", content: "You are a test assistant." }, + { role: "user", content: message.content as string }, + ], + model: "grok-2-latest", + stream: false, + temperature: 0, + }), + }).then( + (res) => + res.json() as Promise<{ + choices: { message: { content: string } }[]; + }>, + ); + const aiResponse = response.choices[0]?.message?.content || ""; + console.log(`Sending AI response: ${aiResponse}`); + /* Send the AI response to the conversation */ + await conversation.send(aiResponse); + } catch (error) { + console.error("Error getting AI response:", error); + await conversation.send( + "Sorry, I encountered an error processing your message.", + ); + } + + console.log("Waiting for messages..."); + } +} + +main().catch(console.error); diff --git a/integrations/grok/package.json b/integrations/grok/package.json new file mode 100644 index 0000000..f1de7e0 --- /dev/null +++ b/integrations/grok/package.json @@ -0,0 +1,20 @@ +{ + "name": "@integrations/gpt", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx --env-file=.env index.ts", + "gen:keys": "tsx ../../scripts/generateKeys.ts" + }, + "dependencies": { + "@xmtp/node-sdk": "0.0.42", + "tsx": "^4.19.2", + "uint8arrays": "^5.1.0", + "viem": "^2.22.17" + }, + "packageManager": "yarn@4.6.0", + "engines": { + "node": ">=20" + } +} diff --git a/package.json b/package.json index 2ee4d6d..c963392 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,10 @@ "version": "0.0.0", "private": true, "type": "module", + "workspaces": [ + "examples/*", + "integrations/*" + ], "scripts": { "clean": "rimraf node_modules && yarn clean:dbs", "clean:dbs": "rimraf *.db3* ||:", @@ -16,8 +20,7 @@ "typecheck": "tsc" }, "dependencies": { - "@xmtp/content-type-text": "^2.0.0", - "@xmtp/node-sdk": "0.0.40", + "@xmtp/node-sdk": "0.0.42", "alchemy-sdk": "^3.0.0", "openai": "latest", "tsx": "^4.19.2", diff --git a/scripts/generateKeys.ts b/scripts/generateKeys.ts index 6396c02..d49280a 100644 --- a/scripts/generateKeys.ts +++ b/scripts/generateKeys.ts @@ -1,23 +1,20 @@ import { writeFile } from "node:fs/promises"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; +import { join } from "node:path"; import { generatePrivateKey } from "viem/accounts"; import { generateEncryptionKeyHex } from "@/helpers"; -const __dirname = dirname(fileURLToPath(import.meta.url)); - console.log("Generating keys..."); const walletKey = generatePrivateKey(); const encryptionKeyHex = generateEncryptionKeyHex(); -const envFilePath = join(__dirname, "../.env"); +const filePath = join(process.cwd(), ".env"); await writeFile( - envFilePath, + filePath, `WALLET_KEY=${walletKey} ENCRYPTION_KEY=${encryptionKeyHex} `, ); -console.log(`Keys written to ${envFilePath}`); +console.log(`Keys written to ${filePath}`); diff --git a/yarn.lock b/yarn.lock index f1a0992..6f159de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -808,6 +808,17 @@ __metadata: languageName: node linkType: hard +"@integrations/gpt@workspace:integrations/grok": + version: 0.0.0-use.local + resolution: "@integrations/gpt@workspace:integrations/grok" + dependencies: + "@xmtp/node-sdk": "npm:0.0.42" + tsx: "npm:^4.19.2" + uint8arrays: "npm:^5.1.0" + viem: "npm:^2.22.17" + languageName: unknown + linkType: soft + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -1263,23 +1274,23 @@ __metadata: languageName: node linkType: hard -"@xmtp/node-bindings@npm:^0.0.34": - version: 0.0.34 - resolution: "@xmtp/node-bindings@npm:0.0.34" - checksum: 10/4f5314d40dc1f07e303e52a4be1d4517f1199089b4ac1677fabb11f64a6b4f57a989b25849b31036d6d228e9de494a8326d2b53386067146ee47e28691bba999 +"@xmtp/node-bindings@npm:^0.0.37": + version: 0.0.37 + resolution: "@xmtp/node-bindings@npm:0.0.37" + checksum: 10/06d12ee27f306a2ad767b75eedf3ceb0457cab3f574a545f44e487cbb39d07a3866c81277954a4937dc1f048a75c10b34b5ffa5de4fc43eaf9b1310a1c4dce8a languageName: node linkType: hard -"@xmtp/node-sdk@npm:0.0.40": - version: 0.0.40 - resolution: "@xmtp/node-sdk@npm:0.0.40" +"@xmtp/node-sdk@npm:0.0.42": + version: 0.0.42 + resolution: "@xmtp/node-sdk@npm:0.0.42" dependencies: "@xmtp/content-type-group-updated": "npm:^2.0.0" "@xmtp/content-type-primitives": "npm:^2.0.0" "@xmtp/content-type-text": "npm:^2.0.0" - "@xmtp/node-bindings": "npm:^0.0.34" + "@xmtp/node-bindings": "npm:^0.0.37" "@xmtp/proto": "npm:^3.72.3" - checksum: 10/eeed6072513dbf1c5ed7f0cfb9d5876c086d648b3fe93d757f0fefa87d03312a8aa5b3f3256b27339b37bbddf75c604eade1480a643909e04dc3865903915b2f + checksum: 10/6163ede6d9cd7bc36b3a237d9a63a8be0041a11a1eefc6f56fefbf7cb8b8cc02c817c4696996a29a4f296d7545fd3172d8ebf8f85e1aeb23dcff917719dfcd10 languageName: node linkType: hard @@ -3820,8 +3831,7 @@ __metadata: "@ianvs/prettier-plugin-sort-imports": "npm:^4.4.1" "@types/eslint__js": "npm:^8.42.3" "@types/node": "npm:^22.13.0" - "@xmtp/content-type-text": "npm:^2.0.0" - "@xmtp/node-sdk": "npm:0.0.40" + "@xmtp/node-sdk": "npm:0.0.42" alchemy-sdk: "npm:^3.0.0" eslint: "npm:^9.19.0" eslint-config-prettier: "npm:^10.0.1"