diff --git a/integrations/gaia/.env.example b/integrations/gaia/.env.example new file mode 100644 index 0000000..58ab873 --- /dev/null +++ b/integrations/gaia/.env.example @@ -0,0 +1,4 @@ +#GAIA Integration +GAIA_NODE_URL= # the URL of the Gaia node (Ex: https://llama8b.gaia.domains/v1) +GAIA_API_KEY= # the API key for the Gaia node (Ex: gaia-xxxxxxxxxx) +GAIA_MODEL_NAME= # the name of model running on your Gaia node (Ex: llama) \ No newline at end of file diff --git a/integrations/gaia/README.md b/integrations/gaia/README.md new file mode 100644 index 0000000..b3bdbca --- /dev/null +++ b/integrations/gaia/README.md @@ -0,0 +1,148 @@ +# Gaia agent example + +This example uses a [Gaia](https://docs.gaianet.ai) API for AI based 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. + +Using Gaia, you can also run your own [node](https://docs.gaianet.ai/getting-started/quick-start) and use the OpenAI compatible API in this library. + +## 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 +GAIA_API_KEY= # Your API key from https://gaianet.ai +GAIA_NODE_URL= # Your custom Gaia node URL or a public node, ex: https://llama8b.gaia.domains/v1 +GAIA_MODEL_NAME= # Model name running in your Gaia node or a public node, ex: llama +``` + +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 OpenAI from "openai"; +import { createSigner, getEncryptionKeyFromHex } from "@/helpers"; + +const { WALLET_KEY, ENCRYPTION_KEY, GAIA_NODE_URL, GAIA_API_KEY, GAIA_MODEL_NAME } = process.env; + +if (!WALLET_KEY) { + throw new Error("WALLET_KEY must be set"); +} + +if (!ENCRYPTION_KEY) { + throw new Error("ENCRYPTION_KEY must be set"); +} + +/* Check if the Gaia API key is set */ +if (!GAIA_API_KEY) { + throw new Error("GAIA_API_KEY must be set"); +} + +/* Check if the Gaia node's base URL is set */ +if (!GAIA_NODE_URL) { + throw new Error("GAIA_NODE_URL must be set"); +} + +/* Check if the the model name for the Gaia node is set */ +if (!GAIA_MODEL_NAME) { + throw new Error("GAIA_MODEL_NAME must be set"); +} + +const signer = createSigner(WALLET_KEY); +const encryptionKey = getEncryptionKeyFromHex(ENCRYPTION_KEY); +const openai = new OpenAI({ + baseURL: GAIA_NODE_URL, + apiKey: GAIA_API_KEY +}); + +/* Set the environment to dev or production */ +const env: XmtpEnv = "dev"; + +async function main() { + console.log(`Creating client on the '${env}' network...`); + 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}`, + ); + + console.log("Waiting for messages..."); + 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}`, + ); + + const conversation = client.conversations.getConversationById( + message.conversationId, + ); + + if (!conversation) { + console.log("Unable to find conversation, skipping"); + continue; + } + + try { + const completion = await openai.chat.completions.create({ + messages: [{ role: "user", content: message.content as string }], + model: GAIA_MODEL_NAME, + }); + + /* Get the AI response */ + const response = + completion.choices[0]?.message?.content || + "I'm not sure how to respond to that."; + + console.log(`Sending AI response: ${response}`); + await conversation.send(response); + } 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 +# install packages +yarn + +cd integrations/gaia +yarn dev +``` diff --git a/integrations/gaia/index.ts b/integrations/gaia/index.ts new file mode 100644 index 0000000..d062eaa --- /dev/null +++ b/integrations/gaia/index.ts @@ -0,0 +1,126 @@ +import { Client, type XmtpEnv } from "@xmtp/node-sdk"; +import OpenAI from "openai"; +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, + GAIA_NODE_URL, + GAIA_API_KEY, + GAIA_MODEL_NAME, +} = 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 OpenAI API key is set */ +if (!GAIA_API_KEY) { + throw new Error("GAIA_API_KEY must be set"); +} + +/* Check if the Gaia node's base URL is set */ +if (!GAIA_NODE_URL) { + throw new Error("GAIA_NODE_URL must be set"); +} + +/* Check if the the model name for the Gaia node is set */ +if (!GAIA_MODEL_NAME) { + throw new Error("GAIA_MODEL_NAME 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); + +/* Initialize the OpenAI client */ +const openai = new OpenAI({ + baseURL: GAIA_NODE_URL, + apiKey: GAIA_API_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}`, + ); + + 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 */ + const completion = await openai.chat.completions.create({ + messages: [{ role: "user", content: message.content as string }], + model: GAIA_MODEL_NAME as string, + }); + + /* Get the AI response */ + const response = + completion.choices[0]?.message?.content || + "I'm not sure how to respond to that."; + + console.log(`Sending AI response: ${response}`); + /* Send the AI response to the conversation */ + await conversation.send(response); + } 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/gaia/package.json b/integrations/gaia/package.json new file mode 100644 index 0000000..9fe6a66 --- /dev/null +++ b/integrations/gaia/package.json @@ -0,0 +1,23 @@ +{ + "name": "@integrations/gaia", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "build": "cd ../.. && yarn tsc", + "dev": "tsx --env-file=.env index.ts", + "gen:keys": "tsx ../../scripts/generateKeys.ts", + "lint": "cd ../.. && yarn eslint integrations/gaia", + "typecheck": "cd ../.. && yarn typecheck" + }, + "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/integrations/gaia/tsconfig.json b/integrations/gaia/tsconfig.json new file mode 100644 index 0000000..a13356c --- /dev/null +++ b/integrations/gaia/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "paths": { + "@/helpers": ["./helpers/index.ts"] + } + }, + "include": ["**/*.ts"] +} diff --git a/yarn.lock b/yarn.lock index c1e12f0..81598df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -808,6 +808,17 @@ __metadata: languageName: node linkType: hard +"@integrations/gaia@workspace:integrations/gaia": + version: 0.0.0-use.local + resolution: "@integrations/gaia@workspace:integrations/gaia" + 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 + "@integrations/gpt@workspace:integrations/grok": version: 0.0.0-use.local resolution: "@integrations/gpt@workspace:integrations/grok"