diff --git a/README.md b/README.md index 72f97c3..3c30afd 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ npx bgent ## Installation -Currently bgent is dependent on Supabase. You can install it with the following command: +Currently bgent is dependent on Supabase for local development. You can install it with the following command: ```bash npm install bgent @supabase/supabase-js @@ -115,17 +115,17 @@ npm run shell # start the shell in another terminal to talk to the default agent ## Usage ```typescript -import { BgentRuntime } from "bgent"; -import { createClient } from "@supabase/supabase-js"; -const supabase = new createClient( +import { BgentRuntime, SupabaseDatabaseAdapter } from "bgent"; + +const databaseAdapter = new SupabaseDatabaseAdapter( process.env.SUPABASE_URL, - process.env.SUPABASE_SERVICE_API_KEY, -); + process.env.SUPABASE_SERVICE_API_KEY) + ; const runtime = new BgentRuntime({ serverUrl: "https://api.openai.com/v1", token: process.env.OPENAI_API_KEY, // Can be an API key or JWT token for your AI services - supabase, + databaseAdapter, actions: [ /* your custom actions */ ], diff --git a/docs/docs/classes/BgentRuntime.md b/docs/docs/classes/BgentRuntime.md index 3d440f7..e830153 100644 --- a/docs/docs/classes/BgentRuntime.md +++ b/docs/docs/classes/BgentRuntime.md @@ -23,6 +23,7 @@ Creates an instance of BgentRuntime. | :------ | :------ | :------ | | `opts` | `Object` | The options for configuring the BgentRuntime. | | `opts.actions?` | [`Action`](../interfaces/Action.md)[] | Optional custom actions. | +| `opts.databaseAdapter` | `DatabaseAdapter` | The database adapter used for interacting with the database. | | `opts.debugMode?` | `boolean` | If true, debug messages will be logged. | | `opts.embeddingModel?` | `string` | The model to use for embedding. | | `opts.evaluators?` | [`Evaluator`](../interfaces/Evaluator.md)[] | Optional custom evaluators. | @@ -30,7 +31,6 @@ Creates an instance of BgentRuntime. | `opts.providers?` | [`Provider`](../interfaces/Provider.md)[] | Optional context providers. | | `opts.recentMessageCount?` | `number` | The number of messages to hold in the recent message cache. | | `opts.serverUrl?` | `string` | The URL of the worker. | -| `opts.supabase` | `default`\<`any`, ``"public"``, `any`\> | The Supabase client. | | `opts.token` | `string` | The JWT token, can be a JWT token if outside worker, or an OpenAI token if inside worker. | #### Returns @@ -47,6 +47,14 @@ Custom actions that the agent can perform. ___ +### databaseAdapter + +• **databaseAdapter**: `DatabaseAdapter` + +The database adapter used for interacting with the database. + +___ + ### debugMode • **debugMode**: `boolean` @@ -127,14 +135,6 @@ The base URL of the server where the agent's requests are processed. ___ -### supabase - -• **supabase**: `default`\<`any`, ``"public"``, `any`\> - -The Supabase client used for database interactions. - -___ - ### token • **token**: ``null`` \| `string` diff --git a/docs/docs/concepts.md b/docs/docs/concepts.md new file mode 100644 index 0000000..f8ba373 --- /dev/null +++ b/docs/docs/concepts.md @@ -0,0 +1,75 @@ +# Key Concepts in Bgent + +Bgent is a comprehensive and flexible framework for building intelligent agents. It provides a set of tools and abstractions that enable developers to create sophisticated agents tailored to their specific needs. The following concepts are the building blocks of bgent and form the foundation for understanding and working with the framework. + +## Actions + +Actions define the behaviors or responses an agent can perform in a given context. They contain the logic for handling specific user intents or situations and can be added or modified to extend the agent's capabilities. Actions are a fundamental building block of bgent's extensibility, allowing developers to customize their agents without modifying the underlying framework. + +When a user interacts with the agent, the appropriate action is triggered based on the context and the user's input. Actions can perform various tasks, such as generating responses, making API calls, updating the agent's state, or triggering other actions. Developers can create custom actions to define the specific behaviors and functionalities of their agents. + +## Evaluators + +Evaluators are similar to actions but are invoked after each interaction is stored. They assess the agent's state and provide insights or recommendations on how the agent should proceed. Evaluators analyze the context, user input, and other relevant factors to make decisions or generate additional information. + +Evaluators can be used for various purposes, such as sentiment analysis, entity recognition, topic classification, or generating personalized recommendations. By implementing custom evaluators, developers can add advanced reasoning capabilities to their agents and enable them to make informed decisions based on the interaction flow. + +## Providers + +Providers are components that add context to the agent's interactions by integrating external data sources or APIs. They allow agents to access and utilize relevant information during interactions, enhancing the agent's knowledge and capabilities. Providers can be used to retrieve data from databases, invoke external services, or fetch real-time information. + +For example, a weather provider can be implemented to provide the agent with current weather information based on the user's location. Similarly, a product catalog provider can be used to retrieve product details and recommendations based on the user's preferences. Providers enable agents to deliver more accurate and informative responses by incorporating external data sources. + +## State and Context + +Bgent emphasizes the importance of maintaining state and context to ensure coherent and contextually relevant interactions. The state represents a snapshot of the agent's current situation, capturing essential information such as user details, recent interactions, goals, and relevant facts. It provides a comprehensive view of the current context and helps the agent make informed decisions. + +The context is derived from the state and represents the information that is sent to the AI model for response generation. It includes relevant details from the state, such as the user's input, previous interactions, and any additional contextual information required by the AI model. The context is dynamically generated based on the current state and is used to guide the AI model in generating appropriate responses. + +## Memories + +Memories enable agents to store and retrieve interaction-related data. They are stored in the database as `Memory` objects and are managed by the `MemoryManager` class. Memories can be of different types, such as messages, facts, and lore, each serving a specific purpose. + +Messages represent the user inputs and agent responses that form the interaction history. They contain information such as the user ID, content, and associated action. Facts represent the knowledge or information derived from the interactions. They can be used to store key insights, important details, or conclusions drawn from the interactions. + +Lore is another type of memory that represents the contextual knowledge or information that the agent can access and retrieve during interactions. It can include a wide range of data, such as product information, FAQs, historical facts, domain-specific knowledge, or even creative content like stories or character backgrounds. Lore enables agents to provide informed and relevant responses by incorporating pre-existing knowledge. + +Developers can extend the memory system by adding custom memory types to suit their specific requirements. The `MemoryManager` class provides methods for storing, retrieving, and managing memories efficiently. + +## Messages + +Messages are the core unit of communication between users and agents in bgent. They are represented as objects that contain information such as the user ID, content, and associated action. Messages are exchanged between users and agents during interactions and form the basis for understanding and response generation. + +When a user sends a message, it is processed by the agent, triggering the appropriate actions and evaluators. The agent analyzes the message content, extracts relevant information, and generates a response based on the defined behaviors and rules. Messages can also be stored as memories to maintain a history of the interactions and enable the agent to refer back to previous exchanges. + +## Goals + +Goals represent high-level objectives or tasks that the agent aims to accomplish during its interactions. They provide a way to guide the agent's behavior towards specific outcomes and enable the agent to make decisions aligned with the desired results. Goals can be defined and tracked using the bgent framework. + +For example, in a task-oriented scenario, a goal could be to assist the user in completing a specific task or answering their question satisfactorily. The agent can break down the goal into smaller sub-goals or steps and work towards achieving them throughout the interaction. By defining and tracking goals, agents can adapt their behavior, ask relevant questions, and provide targeted responses to help users accomplish their objectives. + +## Relationships + +Relationships capture the connections and associations between entities in the agent's domain. They represent the social dynamics, roles, and interactions between users, objects, or other entities. Relationships enable agents to personalize interactions and maintain a coherent understanding of the domain. + +In bgent, relationships can be defined and managed using the `Relationship` object. It contains information such as the IDs of the related entities, the type of relationship (e.g., user-user, user-object), and any additional metadata relevant to the relationship. + +By understanding and utilizing relationships, agents can tailor their responses, provide personalized recommendations, and maintain a contextually appropriate interaction flow. Relationships can also be used to enforce access controls, permissions, and domain-specific constraints. + +## Stateful vs. Stateless Pattern + +Bgent supports both stateful and stateless patterns for managing the agent's state, providing flexibility to developers based on their specific requirements. The choice between stateful and stateless patterns depends on factors such as the nature of the agent's domain, the level of personalization required, and the scalability needs of the application. + +In the stateful pattern, the agent maintains a persistent state throughout its interactions. It retains the context and history across multiple exchanges, allowing for more complex and personalized interactions. The stateful pattern is particularly useful when dealing with multi-turn dialogues, user-specific preferences, or scenarios that require tracking long-term information. + +On the other hand, the stateless pattern treats each interaction as independent and self-contained. The agent does not maintain a persistent state across interactions, and each request is processed in isolation. Stateless agents rely solely on the information provided in the current input and external data sources to generate responses. This pattern offers simplicity and scalability, as it eliminates the need to manage and store interaction-specific data. + +Bgent provides the flexibility to adopt either approach or even combine them based on the specific requirements of the agent. Developers can choose the appropriate pattern based on their needs and design their agents accordingly. + +## Database Adapters + +Bgent includes database adapters to enable seamless integration with various storage systems. The default adapter supports Supabase, a cloud-based database service, allowing agents to persist and retrieve interaction-related data. However, the framework's modular design allows developers to create custom database adapters to integrate with other databases or storage solutions that best fit their needs. + +Database adapters abstract the underlying storage mechanisms, providing a consistent interface for querying and manipulating data related to the agent's interactions. They handle tasks such as storing and retrieving memories, managing goals and relationships, and persisting other relevant information. + +By using database adapters, developers can focus on building the agent's logic and capabilities without concerning themselves with the intricacies of data storage and retrieval. The adapters provide a layer of abstraction, allowing developers to switch between different storage solutions or migrate their data easily. \ No newline at end of file diff --git a/docs/docs/functions/cancelGoal.md b/docs/docs/functions/cancelGoal.md index 6a3b262..2564131 100644 --- a/docs/docs/functions/cancelGoal.md +++ b/docs/docs/functions/cancelGoal.md @@ -6,7 +6,7 @@ sidebar_position: 0 custom_edit_url: null --- -▸ **cancelGoal**(`«destructured»`): `Promise`\<`PostgrestSingleResponse`\<``null``\>\> +▸ **cancelGoal**(`«destructured»`): `Promise`\<`void`\> #### Parameters @@ -18,4 +18,4 @@ custom_edit_url: null #### Returns -`Promise`\<`PostgrestSingleResponse`\<``null``\>\> +`Promise`\<`void`\> diff --git a/docs/docs/functions/createGoal.md b/docs/docs/functions/createGoal.md index ef91ee1..2d275c5 100644 --- a/docs/docs/functions/createGoal.md +++ b/docs/docs/functions/createGoal.md @@ -6,7 +6,7 @@ sidebar_position: 0 custom_edit_url: null --- -▸ **createGoal**(`«destructured»`): `Promise`\<`PostgrestSingleResponse`\<``null``\>\> +▸ **createGoal**(`«destructured»`): `Promise`\<`void`\> #### Parameters @@ -18,4 +18,4 @@ custom_edit_url: null #### Returns -`Promise`\<`PostgrestSingleResponse`\<``null``\>\> +`Promise`\<`void`\> diff --git a/docs/docs/functions/finishGoal.md b/docs/docs/functions/finishGoal.md index 1c57d86..e2f369d 100644 --- a/docs/docs/functions/finishGoal.md +++ b/docs/docs/functions/finishGoal.md @@ -6,7 +6,7 @@ sidebar_position: 0 custom_edit_url: null --- -▸ **finishGoal**(`«destructured»`): `Promise`\<`PostgrestSingleResponse`\<``null``\>\> +▸ **finishGoal**(`«destructured»`): `Promise`\<`void`\> #### Parameters @@ -18,4 +18,4 @@ custom_edit_url: null #### Returns -`Promise`\<`PostgrestSingleResponse`\<``null``\>\> +`Promise`\<`void`\> diff --git a/docs/docs/functions/finishGoalObjective.md b/docs/docs/functions/finishGoalObjective.md index c3f0775..2b690de 100644 --- a/docs/docs/functions/finishGoalObjective.md +++ b/docs/docs/functions/finishGoalObjective.md @@ -6,7 +6,7 @@ sidebar_position: 0 custom_edit_url: null --- -▸ **finishGoalObjective**(`«destructured»`): `Promise`\<`PostgrestSingleResponse`\<``null``\>\> +▸ **finishGoalObjective**(`«destructured»`): `Promise`\<`void`\> #### Parameters @@ -19,4 +19,4 @@ custom_edit_url: null #### Returns -`Promise`\<`PostgrestSingleResponse`\<``null``\>\> +`Promise`\<`void`\> diff --git a/docs/docs/functions/formatRelationships.md b/docs/docs/functions/formatRelationships.md index f0b3609..c9a82a8 100644 --- a/docs/docs/functions/formatRelationships.md +++ b/docs/docs/functions/formatRelationships.md @@ -14,7 +14,7 @@ custom_edit_url: null | :------ | :------ | | `«destructured»` | `Object` | | › `runtime` | [`BgentRuntime`](../classes/BgentRuntime.md) | -| › `userId` | `string` | +| › `userId` | \`$\{string}-$\{string}-$\{string}-$\{string}-$\{string}\` | #### Returns diff --git a/docs/docs/functions/getGoals.md b/docs/docs/functions/getGoals.md index 6a9d6d4..deedad5 100644 --- a/docs/docs/functions/getGoals.md +++ b/docs/docs/functions/getGoals.md @@ -6,7 +6,7 @@ sidebar_position: 0 custom_edit_url: null --- -▸ **getGoals**(`«destructured»`): `Promise`\<`any`\> +▸ **getGoals**(`«destructured»`): `Promise`\<[`Goal`](../interfaces/Goal.md)[]\> #### Parameters @@ -16,9 +16,9 @@ custom_edit_url: null | › `count?` | `number` | `5` | | › `onlyInProgress?` | `boolean` | `true` | | › `runtime` | [`BgentRuntime`](../classes/BgentRuntime.md) | `undefined` | -| › `userId?` | ``null`` \| `string` | `null` | -| › `userIds` | `string`[] | `undefined` | +| › `userId?` | \`$\{string}-$\{string}-$\{string}-$\{string}-$\{string}\` | `undefined` | +| › `userIds` | \`$\{string}-$\{string}-$\{string}-$\{string}-$\{string}\`[] | `undefined` | #### Returns -`Promise`\<`any`\> +`Promise`\<[`Goal`](../interfaces/Goal.md)[]\> diff --git a/docs/docs/functions/getRelationship.md b/docs/docs/functions/getRelationship.md index fa8fe06..a46974f 100644 --- a/docs/docs/functions/getRelationship.md +++ b/docs/docs/functions/getRelationship.md @@ -6,7 +6,7 @@ sidebar_position: 0 custom_edit_url: null --- -▸ **getRelationship**(`«destructured»`): `Promise`\<`any`\> +▸ **getRelationship**(`«destructured»`): `Promise`\<``null`` \| [`Relationship`](../interfaces/Relationship.md)\> #### Parameters @@ -14,9 +14,9 @@ custom_edit_url: null | :------ | :------ | | `«destructured»` | `Object` | | › `runtime` | [`BgentRuntime`](../classes/BgentRuntime.md) | -| › `userA` | `string` | -| › `userB` | `string` | +| › `userA` | \`$\{string}-$\{string}-$\{string}-$\{string}-$\{string}\` | +| › `userB` | \`$\{string}-$\{string}-$\{string}-$\{string}-$\{string}\` | #### Returns -`Promise`\<`any`\> +`Promise`\<``null`` \| [`Relationship`](../interfaces/Relationship.md)\> diff --git a/docs/docs/functions/getRelationships.md b/docs/docs/functions/getRelationships.md index a353902..5cfc8f0 100644 --- a/docs/docs/functions/getRelationships.md +++ b/docs/docs/functions/getRelationships.md @@ -14,7 +14,7 @@ custom_edit_url: null | :------ | :------ | | `«destructured»` | `Object` | | › `runtime` | [`BgentRuntime`](../classes/BgentRuntime.md) | -| › `userId` | `string` | +| › `userId` | \`$\{string}-$\{string}-$\{string}-$\{string}-$\{string}\` | #### Returns diff --git a/docs/docs/functions/updateGoal.md b/docs/docs/functions/updateGoal.md index 2d92263..c3a481f 100644 --- a/docs/docs/functions/updateGoal.md +++ b/docs/docs/functions/updateGoal.md @@ -6,7 +6,7 @@ sidebar_position: 0 custom_edit_url: null --- -▸ **updateGoal**(`«destructured»`): `Promise`\<`PostgrestSingleResponse`\<``null``\>\> +▸ **updateGoal**(`«destructured»`): `Promise`\<`void`\> #### Parameters @@ -18,4 +18,4 @@ custom_edit_url: null #### Returns -`Promise`\<`PostgrestSingleResponse`\<``null``\>\> +`Promise`\<`void`\> diff --git a/docs/docs/index.md b/docs/docs/index.md index 97f218b..e6234ba 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -50,7 +50,7 @@ npx bgent ## Installation -Currently bgent is dependent on Supabase. You can install it with the following command: +Currently bgent is dependent on Supabase for local development. You can install it with the following command: ```bash npm install bgent @supabase/supabase-js @@ -123,17 +123,17 @@ npm run shell # start the shell in another terminal to talk to the default agent ## Usage ```typescript -import { BgentRuntime } from "bgent"; -import { createClient } from "@supabase/supabase-js"; -const supabase = new createClient( +import { BgentRuntime, SupabaseDatabaseAdapter } from "bgent"; + +const databaseAdapter = new SupabaseDatabaseAdapter( process.env.SUPABASE_URL, - process.env.SUPABASE_SERVICE_API_KEY, -); + process.env.SUPABASE_SERVICE_API_KEY) + ; const runtime = new BgentRuntime({ serverUrl: "https://api.openai.com/v1", token: process.env.OPENAI_API_KEY, // Can be an API key or JWT token for your AI services - supabase, + databaseAdapter, actions: [ /* your custom actions */ ], diff --git a/package.json b/package.json index 6f88fe1..e64bf76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bgent", - "version": "0.0.35", + "version": "0.0.36", "private": false, "description": "bgent. because agent was taken.", "type": "module", diff --git a/scripts/concat.mjs b/scripts/concat.mjs index 0476aae..6833c8e 100644 --- a/scripts/concat.mjs +++ b/scripts/concat.mjs @@ -5,7 +5,7 @@ import { fileURLToPath } from 'url' const instructions = 'The above code was taken from my codebase at https://github.com/jointhealliance/bgent.' // Patterns to ignore -const ignorePatterns = ['flavor', 'simple', 'cache', 'logger', 'index', 'data', 'templates'] +const ignorePatterns = ['tests', 'templates', 'test'] // __dirname is not defined in ES module scope, so we need to create it const __filename = fileURLToPath(import.meta.url) diff --git a/scripts/processDocs.mjs b/scripts/processDocs.mjs index 05ea7c6..8966e27 100644 --- a/scripts/processDocs.mjs +++ b/scripts/processDocs.mjs @@ -3,7 +3,7 @@ const { SupabaseClient } = s; import dotenv from 'dotenv'; import fs from 'fs/promises'; import path from 'path'; -import { BgentRuntime, addLore } from '../dist/index.esm.js'; +import { BgentRuntime, SupabaseDatabaseAdapter, addLore } from '../dist/index.esm.js'; dotenv.config({ path: '.dev.vars' }); const SUPABASE_URL = process.env.SUPABASE_URL ?? "https://rnxwpsbkzcugmqauwdax.supabase.co"; @@ -18,9 +18,14 @@ const supabase = new SupabaseClient(SUPABASE_URL, SUPABASE_SERVICE_API_KEY); // The first argument from the command line is the starting path const startingPath = process.argv[2]; +const databaseAdapter = new SupabaseDatabaseAdapter( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_API_KEY) + ; + const runtime = new BgentRuntime({ debugMode: process.env.NODE_ENV === "development", - supabase, + databaseAdapter, serverUrl: SERVER_URL, token: OPENAI_API_KEY, actions: [], diff --git a/src/agents/simple/index.ts b/src/agents/simple/index.ts index 67da847..7f49eed 100644 --- a/src/agents/simple/index.ts +++ b/src/agents/simple/index.ts @@ -1,9 +1,9 @@ -import { createClient, type SupabaseClient } from "@supabase/supabase-js"; import jwt from "@tsndr/cloudflare-worker-jwt"; import { type UUID } from "crypto"; import { composeContext } from "../../lib/context"; import { embeddingZeroVector } from "../../lib/memory"; +import { SupabaseDatabaseAdapter } from "../../lib/adapters/supabase"; import logger from "../../lib/logger"; import { BgentRuntime } from "../../lib/runtime"; import { messageHandlerTemplate } from "../../lib/templates"; @@ -65,21 +65,14 @@ async function handleMessage( stop: [], }); - runtime.supabase - .from("logs") - .insert({ - body: { message, context, response }, - user_id: senderId, - room_id, - user_ids: user_ids!, - agent_id: agentId!, - type: "main_completion", - }) - .then(({ error }) => { - if (error) { - console.error("error", error); - } - }); + runtime.databaseAdapter.log({ + body: { message, context, response }, + user_id: senderId, + room_id, + user_ids: user_ids!, + agent_id: agentId!, + type: "simple_agent_main_completion", + }); const parsedResponse = parseJSONObjectFromText( response, @@ -164,7 +157,6 @@ interface HandlerArgs { }; match?: RegExpMatchArray; userId: UUID; - supabase: SupabaseClient; } class Route { @@ -186,7 +178,7 @@ class Route { const routes: Route[] = [ { path: /^\/api\/agents\/message/, - async handler({ req, env, userId, supabase }: HandlerArgs) { + async handler({ req, env, userId }: HandlerArgs) { if (req.method === "OPTIONS") { return; } @@ -194,10 +186,15 @@ const routes: Route[] = [ // parse the body from the request const message = await req.json(); + const databaseAdapter = new SupabaseDatabaseAdapter( + env.SUPABASE_URL, + env.SUPABASE_SERVICE_API_KEY, + ); + const runtime = new BgentRuntime({ debugMode: env.NODE_ENV === "development", serverUrl: "https://api.openai.com/v1", - supabase, + databaseAdapter, token: env.OPENAI_API_KEY, }); @@ -258,16 +255,6 @@ async function handleRequest( if (matchUrl) { try { - const supabase = createClient( - env.SUPABASE_URL, - env.SUPABASE_SERVICE_API_KEY, - { - auth: { - persistSession: false, - }, - }, - ); - const token = req.headers.get("Authorization")?.replace("Bearer ", ""); const out = (token && jwt.decode(token)) as { @@ -292,7 +279,6 @@ async function handleRequest( env, match: matchUrl, userId: userId as UUID, - supabase, }); return response; diff --git a/src/lib/__tests__/actions.test.ts b/src/lib/__tests__/actions.test.ts index f88c02d..70b2b55 100644 --- a/src/lib/__tests__/actions.test.ts +++ b/src/lib/__tests__/actions.test.ts @@ -2,16 +2,16 @@ import { type User } from "@supabase/supabase-js"; import { type UUID } from "crypto"; import dotenv from "dotenv"; import { createRuntime } from "../../test/createRuntime"; +import { runAiTest } from "../../test/runAiTest"; import { TEST_ACTION, TEST_ACTION_FAIL } from "../../test/testAction"; import { zeroUuid } from "../constants"; -import { createRelationship, getRelationship } from "../relationships"; -import { type BgentRuntime } from "../runtime"; -import { Content, State, type Message } from "../types"; -import { runAiTest } from "../../test/runAiTest"; import { composeContext } from "../context"; import logger from "../logger"; import { embeddingZeroVector } from "../memory"; +import { createRelationship, getRelationship } from "../relationships"; +import { type BgentRuntime } from "../runtime"; import { messageHandlerTemplate } from "../templates"; +import { Content, State, type Message } from "../types"; import { parseJSONObjectFromText } from "../utils"; async function handleMessage( @@ -63,21 +63,14 @@ async function handleMessage( stop: [], }); - runtime.supabase - .from("logs") - .insert({ - body: { message, context, response }, - user_id: senderId, - room_id, - user_ids: user_ids!, - agent_id: agentId!, - type: "main_completion", - }) - .then(({ error }) => { - if (error) { - console.error("error", error); - } - }); + runtime.databaseAdapter.log({ + body: { message, context, response }, + user_id: senderId, + room_id, + user_ids: user_ids!, + agent_id: agentId!, + type: "actions_test_completion", + }); const parsedResponse = parseJSONObjectFromText( response, @@ -151,23 +144,17 @@ describe("Actions", () => { // check if the user id exists in the 'accounts' table // if it doesn't, create it with the name 'Test User' - const { data: accounts } = await runtime.supabase - .from("accounts") - .select("*") - .eq("id", user.id as UUID); - - if (accounts && accounts.length === 0) { - const { error } = await runtime.supabase.from("accounts").insert([ - { - id: user.id, + let account = await runtime.databaseAdapter.getAccountById(user.id as UUID); + + if (!account) { + account = await runtime.databaseAdapter.getAccountById(user.id as UUID); + if (!account) { + await runtime.databaseAdapter.createAccount({ + id: user.id as UUID, name: "Test User", email: user.email, avatar_url: "", - }, - ]); - - if (error) { - throw new Error(error.message); + }); } } diff --git a/src/lib/__tests__/evaluation.test.ts b/src/lib/__tests__/evaluation.test.ts index f26fefc..ff7e782 100644 --- a/src/lib/__tests__/evaluation.test.ts +++ b/src/lib/__tests__/evaluation.test.ts @@ -32,6 +32,11 @@ describe("Evaluation Process", () => { userA: user.id as UUID, userB: zeroUuid, }); + + if (!relationship) { + throw new Error("Relationship not found"); + } + room_id = relationship?.room_id; }); diff --git a/src/lib/__tests__/goals.test.ts b/src/lib/__tests__/goals.test.ts index 27195d3..27b8284 100644 --- a/src/lib/__tests__/goals.test.ts +++ b/src/lib/__tests__/goals.test.ts @@ -24,17 +24,24 @@ describe("Goals", () => { }); runtime = result.runtime; user = result.session.user; - await runtime.supabase.from("goals").delete().match({ user_id: user.id }); - - // delete all goals for the user + await runtime.databaseAdapter.removeAllMemoriesByUserIds( + [user.id as UUID], + "goals", + ); }); beforeEach(async () => { - await runtime.supabase.from("goals").delete().match({ user_id: user.id }); + await runtime.databaseAdapter.removeAllMemoriesByUserIds( + [user.id as UUID], + "goals", + ); }); afterAll(async () => { - await runtime.supabase.from("goals").delete().match({ user_id: user.id }); + await runtime.databaseAdapter.removeAllMemoriesByUserIds( + [user.id as UUID], + "goals", + ); }); // TODO: Write goal tests here @@ -96,8 +103,10 @@ describe("Goals", () => { userIds: [user?.id as UUID], onlyInProgress: false, }); - const existingGoal = goals.find((goal: Goal) => goal.name === newGoal.name); - const updatedGoal = { ...existingGoal, status: "COMPLETED" }; + const existingGoal = goals.find( + (goal: Goal) => goal.name === newGoal.name, + ) as Goal; + const updatedGoal = { ...existingGoal, status: GoalStatus.DONE }; await updateGoal({ runtime, goal: updatedGoal, @@ -114,7 +123,7 @@ describe("Goals", () => { (goal: Goal) => goal.id === existingGoal.id, ); - expect(updatedGoalInDb?.status).toEqual("COMPLETED"); + expect(updatedGoalInDb?.status).toEqual(GoalStatus.DONE); }); // Finishing a goal @@ -138,13 +147,15 @@ describe("Goals", () => { }); // Verify the goal is created in the database - let goals = await getGoals({ + let goals = (await getGoals({ runtime, userIds: [user?.id as UUID], onlyInProgress: false, - }); + })) as Goal[]; - const goalToFinish = goals.find((goal: Goal) => goal.name === newGoal.name); + const goalToFinish = goals.find( + (goal: Goal) => goal.name === newGoal.name, + ) as Goal; // now create the goal @@ -188,13 +199,15 @@ describe("Goals", () => { }); // Verify the goal is created in the database - let goals = await getGoals({ + let goals: Goal[] = await getGoals({ runtime, userIds: [user?.id as UUID], onlyInProgress: false, }); - const goalToFinish = goals.find((goal: Goal) => goal.name === newGoal.name); + const goalToFinish = goals.find( + (goal: Goal) => goal.name === newGoal.name, + ) as Goal; await cancelGoal({ runtime, @@ -236,13 +249,15 @@ describe("Goals", () => { }); // Verify the goal is created in the database - const goals = await getGoals({ + const goals: Goal[] = await getGoals({ runtime, userIds: [user?.id as UUID], onlyInProgress: false, }); - const goalToFinish = goals.find((goal: Goal) => goal.name === newGoal.name); + const goalToFinish = goals.find( + (goal: Goal) => goal.name === newGoal.name, + ) as Goal; const objectiveToFinish = goalToFinish.objectives[0]; diff --git a/src/lib/__tests__/memory.test.ts b/src/lib/__tests__/memory.test.ts index 3937c26..38e850e 100644 --- a/src/lib/__tests__/memory.test.ts +++ b/src/lib/__tests__/memory.test.ts @@ -28,6 +28,10 @@ describe("Memory", () => { userB: zeroUuid, }); + if (!data) { + throw new Error("Relationship not found"); + } + room_id = data?.room_id; memoryManager = new MemoryManager({ @@ -216,6 +220,10 @@ describe("Memory - Basic tests", () => { userB: zeroUuid, }); + if (!data) { + throw new Error("Relationship not found"); + } + room_id = data?.room_id; memoryManager = new MemoryManager({ @@ -307,6 +315,10 @@ describe("Memory - Extended Tests", () => { userB: zeroUuid, }); + if (!data) { + throw new Error("Relationship not found"); + } + room_id = data?.room_id; memoryManager = new MemoryManager({ diff --git a/src/lib/__tests__/relationships.test.ts b/src/lib/__tests__/relationships.test.ts index bca053a..c1b0e05 100644 --- a/src/lib/__tests__/relationships.test.ts +++ b/src/lib/__tests__/relationships.test.ts @@ -49,8 +49,8 @@ describe("Relationships Module", () => { userB, }); expect(relationship).toBeDefined(); - expect(relationship.user_a).toBe(userA); - expect(relationship.user_b).toBe(userB); + expect(relationship?.user_a).toBe(userA); + expect(relationship?.user_b).toBe(userB); }); test("getRelationships retrieves all relationships for a user", async () => { diff --git a/src/lib/__tests__/runtime.test.ts b/src/lib/__tests__/runtime.test.ts index 92f9238..425b8a7 100644 --- a/src/lib/__tests__/runtime.test.ts +++ b/src/lib/__tests__/runtime.test.ts @@ -64,6 +64,10 @@ describe("Agent Runtime", () => { userB: zeroUuid, }); + if (!data) { + throw new Error("Relationship not found"); + } + room_id = data?.room_id; await clearMemories(); // Clear memories before each test }); diff --git a/src/lib/actions/__tests__/elaborate.test.ts b/src/lib/actions/__tests__/elaborate.test.ts index 9d2ab88..302eee3 100644 --- a/src/lib/actions/__tests__/elaborate.test.ts +++ b/src/lib/actions/__tests__/elaborate.test.ts @@ -61,10 +61,14 @@ describe("User Profile", () => { const data = await getRelationship({ runtime, - userA: user.id, + userA: user.id as UUID, userB: zeroUuid, }); + if (!data) { + throw new Error("Relationship not found"); + } + room_id = data?.room_id; await cleanup(); diff --git a/src/lib/actions/__tests__/ignore.test.ts b/src/lib/actions/__tests__/ignore.test.ts index 4e5cd79..72995fb 100644 --- a/src/lib/actions/__tests__/ignore.test.ts +++ b/src/lib/actions/__tests__/ignore.test.ts @@ -69,21 +69,14 @@ async function handleMessage( stop: [], }); - runtime.supabase - .from("logs") - .insert({ - body: { message, context, response }, - user_id: senderId, - room_id, - user_ids: user_ids!, - agent_id: agentId!, - type: "main_completion", - }) - .then(({ error }) => { - if (error) { - console.error("error", error); - } - }); + await runtime.databaseAdapter.log({ + body: { message, context, response }, + user_id: senderId, + room_id, + user_ids: user_ids!, + agent_id: agentId!, + type: "ignore_test_completion", + }); const parsedResponse = parseJSONObjectFromText( response, @@ -164,6 +157,10 @@ describe("Ignore action tests", () => { userB: zeroUuid, }); + if (!data) { + throw new Error("Relationship not found"); + } + room_id = data?.room_id; await cleanup(); diff --git a/src/lib/actions/__tests__/wait.test.ts b/src/lib/actions/__tests__/wait.test.ts index 69f8756..3217bf8 100644 --- a/src/lib/actions/__tests__/wait.test.ts +++ b/src/lib/actions/__tests__/wait.test.ts @@ -40,6 +40,10 @@ describe("Wait Action Behavior", () => { userB: zeroUuid, }); + if (!data) { + throw new Error("Relationship not found"); + } + room_id = data?.room_id; await cleanup(); diff --git a/src/lib/actions/elaborate.ts b/src/lib/actions/elaborate.ts index f4128a0..e542b88 100644 --- a/src/lib/actions/elaborate.ts +++ b/src/lib/actions/elaborate.ts @@ -64,21 +64,14 @@ export default { stop: [], }); - runtime.supabase - .from("logs") - .insert({ - body: { message, context, response }, - user_id: senderId, - room_id, - user_ids: user_ids!, - agent_id: agentId!, - type: "main_completion", - }) - .then(({ error }) => { - if (error) { - console.error("error", error); - } - }); + runtime.databaseAdapter.log({ + body: { message, context, response }, + user_id: senderId, + room_id, + user_ids: user_ids!, + agent_id: agentId!, + type: "elaborate", + }); const parsedResponse = parseJSONObjectFromText( response, @@ -196,7 +189,10 @@ export default { }, { user: "{{user1}}", - content: { content: "Challenging, but rewarding.", action: "ELABORATE" }, + content: { + content: "Challenging, but rewarding.", + action: "ELABORATE", + }, }, { user: "{{user1}}", @@ -244,7 +240,10 @@ export default { }, { user: "{{user1}}", - content: { content: "Not sure lol, they are anon", action: "ELABORATE" }, + content: { + content: "Not sure lol, they are anon", + action: "ELABORATE", + }, }, { user: "{{user1}}", diff --git a/src/lib/adapters/supabase.ts b/src/lib/adapters/supabase.ts new file mode 100644 index 0000000..0b5da71 --- /dev/null +++ b/src/lib/adapters/supabase.ts @@ -0,0 +1,319 @@ +// File: /src/lib/database/SupabaseDatabaseAdapter.ts +import { createClient, type SupabaseClient } from "@supabase/supabase-js"; +import { type UUID } from "crypto"; +import { + type Memory, + type Goal, + type Relationship, + Actor, + GoalStatus, + Account, +} from "../types"; +import { DatabaseAdapter } from "../database"; + +export class SupabaseDatabaseAdapter extends DatabaseAdapter { + private supabase: SupabaseClient; + + constructor(supabaseUrl: string, supabaseKey: string) { + super(); + this.supabase = createClient(supabaseUrl, supabaseKey); + } + + async getAccountById(userId: UUID): Promise { + const { data, error } = await this.supabase + .from("accounts") + .select("*") + .eq("id", userId); + if (error) { + throw new Error(error.message); + } + return (data?.[0] as Account) || null; + } + + async createAccount(account: Account): Promise { + const { error } = await this.supabase.from("accounts").insert([account]); + if (error) { + throw new Error(error.message); + } + } + + async getActorDetails(params: { userIds: UUID[] }): Promise { + const response = await this.supabase + .from("accounts") + .select("*") + .in("id", params.userIds); + if (response.error) { + console.error(response.error); + return []; + } + const { data } = response; + return data.map((actor: Actor) => ({ + name: actor.name, + details: actor.details, + id: actor.id, + })); + } + + async searchMemories(params: { + tableName: string; + userIds: UUID[]; + embedding: number[]; + match_threshold: number; + match_count: number; + unique: boolean; + }): Promise { + const result = await this.supabase.rpc("search_memories", { + query_table_name: params.tableName, + query_user_ids: params.userIds, + query_embedding: params.embedding, + query_match_threshold: params.match_threshold, + query_match_count: params.match_count, + query_unique: params.unique, + }); + if (result.error) { + throw new Error(JSON.stringify(result.error)); + } + return result.data; + } + + async updateGoalStatus(params: { + goalId: UUID; + status: GoalStatus; + }): Promise { + await this.supabase + .from("goals") + .update({ status: params.status }) + .match({ id: params.goalId }); + } + + async log(params: { + body: { [key: string]: unknown }; + user_id: UUID; + room_id: UUID; + user_ids: UUID[]; + agent_id: UUID; + type: string; + }): Promise { + const { error } = await this.supabase.from("logs").insert({ + body: params.body, + user_id: params.user_id, + room_id: params.room_id, + user_ids: params.user_ids, + agent_id: params.agent_id, + type: params.type, + }); + + if (error) { + console.error("Error inserting log:", error); + throw new Error(error.message); + } + } + + async getMemoriesByIds(params: { + userIds: UUID[]; + count?: number; + unique?: boolean; + tableName: string; + }): Promise { + const result = await this.supabase.rpc("get_memories", { + query_table_name: params.tableName, + query_user_ids: params.userIds, + query_count: params.count, + query_unique: !!params.unique, + }); + if (result.error) { + throw new Error(JSON.stringify(result.error)); + } + if (!result.data) { + console.warn("data was null, no memories found for", { + userIds: params.userIds, + count: params.count, + }); + return []; + } + return result.data; + } + + async searchMemoriesByEmbedding( + embedding: number[], + params: { + match_threshold?: number; + count?: number; + userIds?: UUID[]; + unique?: boolean; + tableName: string; + }, + ): Promise { + const result = await this.supabase.rpc("search_memories", { + query_table_name: params.tableName, + query_user_ids: params.userIds, + query_embedding: embedding, + query_match_threshold: params.match_threshold, + query_match_count: params.count, + query_unique: !!params.unique, + }); + if (result.error) { + throw new Error(JSON.stringify(result.error)); + } + return result.data; + } + + async createMemory( + memory: Memory, + tableName: string, + unique = false, + ): Promise { + if (unique) { + const opts = { + query_table_name: tableName, + query_user_id: memory.user_id, + query_user_ids: memory.user_ids, + query_content: memory.content.content, + query_room_id: memory.room_id, + query_embedding: memory.embedding, + similarity_threshold: 0.95, + }; + + const result = await this.supabase.rpc( + "check_similarity_and_insert", + opts, + ); + + if (result.error) { + throw new Error(JSON.stringify(result.error)); + } + } else { + const result = await this.supabase.from(tableName).insert(memory); + const { error } = result; + if (error) { + throw new Error(JSON.stringify(error)); + } + } + } + + async removeMemory(memoryId: UUID, tableName: string): Promise { + const result = await this.supabase + .from(tableName) + .delete() + .eq("id", memoryId); + const { error } = result; + if (error) { + throw new Error(JSON.stringify(error)); + } + } + + async removeAllMemoriesByUserIds( + userIds: UUID[], + tableName: string, + ): Promise { + const result = await this.supabase.rpc("remove_memories", { + query_table_name: tableName, + query_user_ids: userIds, + }); + + if (result.error) { + throw new Error(JSON.stringify(result.error)); + } + } + + async countMemoriesByUserIds( + userIds: UUID[], + unique = true, + tableName: string, + ): Promise { + if (!tableName) { + throw new Error("tableName is required"); + } + const query = { + query_table_name: tableName, + query_user_ids: userIds, + query_unique: !!unique, + }; + const result = await this.supabase.rpc("count_memories", query); + + if (result.error) { + throw new Error(JSON.stringify(result.error)); + } + + return result.data; + } + + async getGoals(params: { + userIds: UUID[]; + userId?: UUID | null; + onlyInProgress?: boolean; + count?: number; + }): Promise { + const opts = { + query_user_ids: params.userIds, + query_user_id: params.userId, + only_in_progress: params.onlyInProgress, + row_count: params.count, + }; + const { data: goals, error } = await this.supabase.rpc( + "get_goals_by_user_ids", + opts, + ); + + if (error) { + throw new Error(error.message); + } + + return goals; + } + + async updateGoal(goal: Goal): Promise { + await this.supabase.from("goals").update(goal).match({ id: goal.id }); + } + + async createGoal(goal: Goal): Promise { + await this.supabase.from("goals").upsert(goal); + } + + async createRelationship(params: { + userA: UUID; + userB: UUID; + }): Promise { + const { error } = await this.supabase.from("relationships").upsert({ + user_a: params.userA, + user_b: params.userB, + user_id: params.userA, + }); + + if (error) { + throw new Error(error.message); + } + + return true; + } + + async getRelationship(params: { + userA: UUID; + userB: UUID; + }): Promise { + const { data, error } = await this.supabase.rpc("get_relationship", { + usera: params.userA, + userb: params.userB, + }); + + if (error) { + throw new Error(error.message); + } + + return data[0]; + } + + async getRelationships(params: { userId: UUID }): Promise { + const { data, error } = await this.supabase + .from("relationships") + .select("*") + .or(`user_a.eq.${params.userId},user_b.eq.${params.userId}`) + .eq("status", "FRIENDS"); + + if (error) { + throw new Error(error.message); + } + + return data as Relationship[]; + } +} diff --git a/src/lib/database.ts b/src/lib/database.ts new file mode 100644 index 0000000..b73b341 --- /dev/null +++ b/src/lib/database.ts @@ -0,0 +1,98 @@ +import { type UUID } from "crypto"; +import { + type Memory, + type Goal, + type Relationship, + Actor, + GoalStatus, + Account, +} from "./types"; + +export abstract class DatabaseAdapter { + abstract getAccountById(userId: UUID): Promise; + abstract createAccount(account: Account): Promise; + + abstract getMemoriesByIds(params: { + userIds: UUID[]; + count?: number; + unique?: boolean; + tableName: string; + }): Promise; + + abstract log(params: { + body: { [key: string]: unknown }; + user_id: UUID; + room_id: UUID; + user_ids: UUID[]; + agent_id: UUID; + type: string; + }): Promise; + + abstract getActorDetails(params: { userIds: UUID[] }): Promise; + + abstract searchMemories(params: { + tableName: string; + userIds: UUID[]; + embedding: number[]; + match_threshold: number; + match_count: number; + unique: boolean; + }): Promise; + abstract updateGoalStatus(params: { + goalId: UUID; + status: GoalStatus; + }): Promise; + + abstract searchMemoriesByEmbedding( + embedding: number[], + params: { + match_threshold?: number; + count?: number; + userIds?: UUID[]; + unique?: boolean; + tableName: string; + }, + ): Promise; + + abstract createMemory( + memory: Memory, + tableName: string, + unique?: boolean, + ): Promise; + + abstract removeMemory(memoryId: UUID, tableName: string): Promise; + + abstract removeAllMemoriesByUserIds( + userIds: UUID[], + tableName: string, + ): Promise; + + abstract countMemoriesByUserIds( + userIds: UUID[], + unique?: boolean, + tableName?: string, + ): Promise; + + abstract getGoals(params: { + userIds: UUID[]; + userId?: UUID | null; + onlyInProgress?: boolean; + count?: number; + }): Promise; + + abstract updateGoal(goal: Goal): Promise; + + abstract createGoal(goal: Goal): Promise; + + abstract createRelationship(params: { + userA: UUID; + userB: UUID; + }): Promise; + + abstract getRelationship(params: { + userA: UUID; + userB: UUID; + }): Promise; + + abstract getRelationships(params: { userId: UUID }): Promise; +} diff --git a/src/lib/evaluators/__tests__/fact.test.ts b/src/lib/evaluators/__tests__/fact.test.ts index 4ae9fcf..a9511d8 100644 --- a/src/lib/evaluators/__tests__/fact.test.ts +++ b/src/lib/evaluators/__tests__/fact.test.ts @@ -40,6 +40,10 @@ describe("Facts Evaluator", () => { userB: zeroUuid, }); + if (!data) { + throw new Error("Relationship not found"); + } + room_id = data.room_id; }); diff --git a/src/lib/evaluators/__tests__/goal.test.ts b/src/lib/evaluators/__tests__/goal.test.ts index d720a99..ea845a5 100644 --- a/src/lib/evaluators/__tests__/goal.test.ts +++ b/src/lib/evaluators/__tests__/goal.test.ts @@ -34,6 +34,10 @@ describe("Goals Evaluator", () => { userB: zeroUuid, }); + if (!data) { + throw new Error("Relationship not found"); + } + room_id = data.room_id; await cleanup(); @@ -45,11 +49,10 @@ describe("Goals Evaluator", () => { async function cleanup() { // delete all goals for the user - await runtime.supabase.from("goals").delete().match({ user_id: user.id }); - runtime.messageManager.removeAllMemoriesByUserIds([ - user.id as UUID, - zeroUuid, - ]); + await runtime.databaseAdapter.removeAllMemoriesByUserIds( + [user.id as UUID], + "goals", + ); } async function createTestGoal(name: string, objectives: Objective[]) { diff --git a/src/lib/evaluators/goal.ts b/src/lib/evaluators/goal.ts index b87952e..e1680e3 100644 --- a/src/lib/evaluators/goal.ts +++ b/src/lib/evaluators/goal.ts @@ -112,13 +112,7 @@ async function handler( const id = goal.id; // delete id from goal if (goal.id) delete goal.id; - const { error } = await runtime.supabase - .from("goals") - .update({ ...goal }) - .match({ id }); - if (error) { - console.log("ERROR: " + JSON.stringify(error)); - } + await runtime.databaseAdapter.updateGoal({ ...goal, id }); } return updatedGoals; // Return updated goals for further processing or logging diff --git a/src/lib/goals.ts b/src/lib/goals.ts index 12757b9..ed3374f 100644 --- a/src/lib/goals.ts +++ b/src/lib/goals.ts @@ -5,32 +5,22 @@ import { type Goal, GoalStatus, type Objective } from "./types"; export const getGoals = async ({ runtime, userIds, - userId = null, + userId, onlyInProgress = true, count = 5, }: { runtime: BgentRuntime; - userIds: string[]; - userId?: string | null; + userIds: UUID[]; + userId?: UUID; onlyInProgress?: boolean; count?: number; }) => { - const opts = { - query_user_ids: userIds, - query_user_id: userId, - only_in_progress: onlyInProgress, - row_count: count, - }; - const { data: goals, error } = await runtime.supabase.rpc( - "get_goals_by_user_ids", - opts, - ); - - if (error) { - throw new Error(error.message); - } - - return goals; + return runtime.databaseAdapter.getGoals({ + userIds, + userId, + onlyInProgress, + count, + }); }; export const formatGoalsAsString = async ({ goals }: { goals: Goal[] }) => { @@ -55,10 +45,7 @@ export const updateGoal = async ({ runtime: BgentRuntime; goal: Goal; }) => { - return await runtime.supabase - .from("goals") - .update(goal) - .match({ id: goal.id }); + return runtime.databaseAdapter.updateGoal(goal); }; export const createGoal = async ({ @@ -68,7 +55,7 @@ export const createGoal = async ({ runtime: BgentRuntime; goal: Goal; }) => { - return await runtime.supabase.from("goals").upsert(goal); + return runtime.databaseAdapter.createGoal(goal); }; export const cancelGoal = async ({ @@ -78,10 +65,10 @@ export const cancelGoal = async ({ runtime: BgentRuntime; goalId: UUID; }) => { - return await runtime.supabase - .from("goals") - .update({ status: GoalStatus.FAILED }) - .match({ id: goalId }); + return await runtime.databaseAdapter.updateGoalStatus({ + goalId, + status: GoalStatus.FAILED, + }); }; export const finishGoal = async ({ @@ -91,10 +78,16 @@ export const finishGoal = async ({ runtime: BgentRuntime; goalId: UUID; }) => { - return await runtime.supabase - .from("goals") - .update({ status: GoalStatus.DONE }) - .match({ id: goalId }); + const goal = await runtime.databaseAdapter.getGoals({ + userIds: [], + userId: null, + onlyInProgress: false, + count: 1, + }); + if (goal[0]?.id === goalId) { + goal[0].status = GoalStatus.DONE; + return runtime.databaseAdapter.updateGoal(goal[0]); + } }; export const finishGoalObjective = async ({ @@ -106,25 +99,18 @@ export const finishGoalObjective = async ({ goalId: UUID; objectiveId: string; }) => { - const { data: goal, error } = await runtime.supabase - .from("goals") - .select("*") - .match({ id: goalId }) - .single(); - - if (error) { - throw new Error(error.message); - } - - const updatedObjectives = goal.objectives.map((objective: Objective) => { - if (objective.id === objectiveId) { - return { ...objective, completed: true }; - } - return objective; + const goals = await runtime.databaseAdapter.getGoals({ + userIds: [], + userId: null, + onlyInProgress: false, + count: 1, }); - - return await runtime.supabase - .from("goals") - .update({ objectives: updatedObjectives }) - .match({ id: goalId }); + const goal = goals.find((g) => g.id === goalId); + if (goal) { + const objective = goal.objectives.find((o) => o.id === objectiveId); + if (objective) { + objective.completed = true; + return runtime.databaseAdapter.updateGoal(goal); + } + } }; diff --git a/src/lib/memory.ts b/src/lib/memory.ts index 7c4c3af..1e91a1c 100644 --- a/src/lib/memory.ts +++ b/src/lib/memory.ts @@ -74,23 +74,13 @@ export class MemoryManager { count?: number; unique?: boolean; }): Promise { - const result = await this.runtime.supabase.rpc("get_memories", { - query_table_name: this.tableName, - query_user_ids: userIds, - query_count: count, - query_unique: !!unique, + const result = await this.runtime.databaseAdapter.getMemoriesByIds({ + userIds, + count, + unique, + tableName: this.tableName, }); - if (result.error) { - throw new Error(JSON.stringify(result.error)); - } - if (!result.data) { - console.warn("data was null, no memories found for", { - userIds, - count, - }); - return []; - } - return result.data; + return result; } /** @@ -115,23 +105,20 @@ export class MemoryManager { const { match_threshold = defaultMatchThreshold, count = defaultMatchCount, - userIds = null, + userIds = [], unique, } = opts; - const result = await this.runtime.supabase.rpc("search_memories", { - query_table_name: this.tableName, - query_user_ids: userIds, - query_embedding: embedding, // Pass the embedding you want to compare - query_match_threshold: match_threshold, // Choose an appropriate threshold for your data - query_match_count: count, // Choose the number of matches - query_unique: !!unique, + const result = await this.runtime.databaseAdapter.searchMemories({ + tableName: this.tableName, + userIds: userIds, + embedding: embedding, + match_threshold: match_threshold, + match_count: count, + unique: !!unique, }); - if (result.error) { - throw new Error(JSON.stringify(result.error)); - } - return result.data; + return result; } /** @@ -141,34 +128,11 @@ export class MemoryManager { * @returns A Promise that resolves when the operation completes. */ async createMemory(memory: Memory, unique = false): Promise { - if (unique) { - const opts = { - query_table_name: this.tableName, - query_user_id: memory.user_id, - query_user_ids: memory.user_ids, - query_content: memory.content.content, - query_room_id: memory.room_id, - query_embedding: memory.embedding, - similarity_threshold: 0.95, - }; - - const result = await this.runtime.supabase.rpc( - "check_similarity_and_insert", - opts, - ); - - if (result.error) { - throw new Error(JSON.stringify(result.error)); - } - } else { - const result = await this.runtime.supabase - .from(this.tableName) - .insert(memory); - const { error } = result; - if (error) { - throw new Error(JSON.stringify(error)); - } - } + await this.runtime.databaseAdapter.createMemory( + memory, + this.tableName, + unique, + ); } /** @@ -177,14 +141,7 @@ export class MemoryManager { * @returns A Promise that resolves when the operation completes. */ async removeMemory(memoryId: UUID): Promise { - const result = await this.runtime.supabase - .from(this.tableName) - .delete() - .eq("id", memoryId); - const { error } = result; - if (error) { - throw new Error(JSON.stringify(error)); - } + await this.runtime.databaseAdapter.removeMemory(memoryId, this.tableName); } /** @@ -193,14 +150,10 @@ export class MemoryManager { * @returns A Promise that resolves when the operation completes. */ async removeAllMemoriesByUserIds(userIds: UUID[]): Promise { - const result = await this.runtime.supabase.rpc("remove_memories", { - query_table_name: this.tableName, - query_user_ids: userIds, - }); - - if (result.error) { - throw new Error(JSON.stringify(result.error)); - } + await this.runtime.databaseAdapter.removeAllMemoriesByUserIds( + userIds, + this.tableName, + ); } /** @@ -213,17 +166,10 @@ export class MemoryManager { userIds: UUID[], unique = true, ): Promise { - const query = { - query_table_name: this.tableName, - query_user_ids: userIds, - query_unique: !!unique, - }; - const result = await this.runtime.supabase.rpc("count_memories", query); - - if (result.error) { - throw new Error(JSON.stringify(result.error)); - } - - return result.data; + return await this.runtime.databaseAdapter.countMemoriesByUserIds( + userIds, + unique, + this.tableName, + ); } } diff --git a/src/lib/messages.ts b/src/lib/messages.ts index 74c3a32..49a57a6 100644 --- a/src/lib/messages.ts +++ b/src/lib/messages.ts @@ -12,25 +12,7 @@ export async function getActorDetails({ runtime: BgentRuntime; userIds: UUID[]; }) { - const response = await runtime.supabase - .from("accounts") - .select("*") - .in("id", userIds); - if (response.error) { - console.error(response.error); - return []; - } - - const { data } = response; - - const actors = data.map((actor: Actor) => { - const { name, details, id } = actor; - return { - name, - details, - id, - }; - }); + const actors = await runtime.databaseAdapter.getActorDetails({ userIds }); return actors as Actor[]; } diff --git a/src/lib/relationships.ts b/src/lib/relationships.ts index 859ff6f..74fd532 100644 --- a/src/lib/relationships.ts +++ b/src/lib/relationships.ts @@ -11,17 +11,10 @@ export async function createRelationship({ userA: UUID; userB: UUID; }): Promise { - const { error } = await runtime.supabase.from("relationships").upsert({ - user_a: userA, - user_b: userB, - user_id: userA, + return runtime.databaseAdapter.createRelationship({ + userA, + userB, }); - - if (error) { - throw new Error(error.message); - } - - return true; } export async function getRelationship({ @@ -30,19 +23,13 @@ export async function getRelationship({ userB, }: { runtime: BgentRuntime; - userA: string; - userB: string; + userA: UUID; + userB: UUID; }) { - const { data, error } = await runtime.supabase.rpc("get_relationship", { - usera: userA, - userb: userB, + return runtime.databaseAdapter.getRelationship({ + userA, + userB, }); - - if (error) { - throw new Error(error.message); - } - - return data[0]; } export async function getRelationships({ @@ -50,19 +37,9 @@ export async function getRelationships({ userId, }: { runtime: BgentRuntime; - userId: string; + userId: UUID; }) { - const { data, error } = await runtime.supabase - .from("relationships") - .select("*") - .or(`user_a.eq.${userId},user_b.eq.${userId}`) - .eq("status", "FRIENDS"); - - if (error) { - throw new Error(error.message); - } - - return data as Relationship[]; + return runtime.databaseAdapter.getRelationships({ userId }); } export async function formatRelationships({ @@ -70,7 +47,7 @@ export async function formatRelationships({ userId, }: { runtime: BgentRuntime; - userId: string; + userId: UUID; }) { const relationships = await getRelationships({ runtime, userId }); diff --git a/src/lib/runtime.ts b/src/lib/runtime.ts index c20caf5..30215ae 100644 --- a/src/lib/runtime.ts +++ b/src/lib/runtime.ts @@ -1,4 +1,3 @@ -import { type SupabaseClient } from "@supabase/supabase-js"; import { addHeader, composeContext } from "./context"; import { defaultEvaluators, @@ -34,6 +33,7 @@ import { formatLore, getLore } from "./lore"; import { formatActors, formatMessages, getActorDetails } from "./messages"; import { defaultProviders, getProviders } from "./providers"; import { type Actor, /*type Goal,*/ type Memory } from "./types"; +import { DatabaseAdapter } from "./database"; /** * Represents the runtime environment for an agent, handling message processing, @@ -50,6 +50,11 @@ export class BgentRuntime { */ serverUrl = "http://localhost:7998"; + /** + * The database adapter used for interacting with the database. + */ + databaseAdapter: DatabaseAdapter; + /** * Authentication token used for securing requests. */ @@ -60,11 +65,6 @@ export class BgentRuntime { */ debugMode: boolean; - /** - * The Supabase client used for database interactions. - */ - supabase: SupabaseClient; - /** * Custom actions that the agent can perform. */ @@ -127,7 +127,6 @@ export class BgentRuntime { * @param opts - The options for configuring the BgentRuntime. * @param opts.recentMessageCount - The number of messages to hold in the recent message cache. * @param opts.token - The JWT token, can be a JWT token if outside worker, or an OpenAI token if inside worker. - * @param opts.supabase - The Supabase client. * @param opts.debugMode - If true, debug messages will be logged. * @param opts.serverUrl - The URL of the worker. * @param opts.actions - Optional custom actions. @@ -135,11 +134,11 @@ export class BgentRuntime { * @param opts.providers - Optional context providers. * @param opts.model - The model to use for completion. * @param opts.embeddingModel - The model to use for embedding. + * @param opts.databaseAdapter - The database adapter used for interacting with the database. */ constructor(opts: { recentMessageCount?: number; // number of messages to hold in the recent message cache token: string; // JWT token, can be a JWT token if outside worker, or an OpenAI token if inside worker - supabase: SupabaseClient; // Supabase client debugMode?: boolean; // If true, will log debug messages serverUrl?: string; // The URL of the worker actions?: Action[]; // Optional custom actions @@ -147,11 +146,17 @@ export class BgentRuntime { providers?: Provider[]; model?: string; // The model to use for completion embeddingModel?: string; // The model to use for embedding + databaseAdapter: DatabaseAdapter; // The database adapter used for interacting with the database }) { this.#recentMessageCount = opts.recentMessageCount ?? this.#recentMessageCount; this.debugMode = opts.debugMode ?? false; - this.supabase = opts.supabase; + this.databaseAdapter = opts.databaseAdapter; + + if (!opts.databaseAdapter) { + throw new Error("No database adapter provided"); + } + this.serverUrl = opts.serverUrl ?? this.serverUrl; this.model = opts.model ?? this.model; this.embeddingModel = opts.embeddingModel ?? this.embeddingModel; diff --git a/src/lib/types.ts b/src/lib/types.ts index 2fdd0fb..cf52802 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -199,3 +199,14 @@ export interface Relationship { status: string; created_at?: string; } + +/** + * Represents a user, including their name, details, and a unique identifier. + */ +export type Account = { + id: UUID; + name: string; + details?: string; + email?: string; + avatar_url?: string; +}; diff --git a/src/test/createRuntime.ts b/src/test/createRuntime.ts index 8f64916..e2b554c 100644 --- a/src/test/createRuntime.ts +++ b/src/test/createRuntime.ts @@ -7,6 +7,8 @@ import { TEST_EMAIL, TEST_PASSWORD, } from "./constants"; +import { DatabaseAdapter } from "../lib/database"; +import { SupabaseDatabaseAdapter } from "../lib/adapters/supabase"; export async function createRuntime({ env, @@ -14,12 +16,14 @@ export async function createRuntime({ evaluators = [], actions = [], providers = [], + databaseAdapter, }: { env?: Record | NodeJS.ProcessEnv; recentMessageCount?: number; evaluators?: Evaluator[]; actions?: Action[]; providers?: Provider[]; + databaseAdapter?: DatabaseAdapter; }) { const supabase = createClient( env?.SUPABASE_URL ?? SUPABASE_URL, @@ -54,12 +58,17 @@ export async function createRuntime({ const runtime = new BgentRuntime({ debugMode: false, serverUrl: "https://api.openai.com/v1", - supabase, recentMessageCount, token: env!.OPENAI_API_KEY!, actions: actions ?? [], evaluators: evaluators ?? [], providers: providers ?? [], + databaseAdapter: + databaseAdapter ?? + new SupabaseDatabaseAdapter( + env?.SUPABASE_URL ?? SUPABASE_URL, + env?.SUPABASE_SERVICE_API_KEY ?? SUPABASE_ANON_KEY, + ), }); return { user, session, runtime };