diff --git a/apps/chat/app/new/editor.tsx b/apps/chat/app/new/editor.tsx index ce478728f..c4e9a92de 100644 --- a/apps/chat/app/new/editor.tsx +++ b/apps/chat/app/new/editor.tsx @@ -6,7 +6,8 @@ import Editor, { useMonaco } from '@monaco-editor/react'; import { Button } from '@/components/ui/button'; import { deployEndpointUrl, r2EndpointUrl } from '@/utils/const/endpoints'; import { getJWT } from '@/lib/jwt'; -import { getUserIdForJwt, getUserForJwt } from '@/utils/supabase/supabase-client'; +import { getUserForJwt } from '@/utils/supabase/supabase-client'; +import { PGliteStorage } from 'react-agents/storage/pglite-storage.mjs' import type { StoreItem, SubscriptionProps, @@ -42,7 +43,6 @@ import { currencies, intervals } from 'react-agents/constants.mjs'; import { buildAgentSrc } from 'react-agents-builder'; import { ReactAgentsWorker } from 'react-agents-browser'; import type { FetchableWorker } from 'react-agents-browser/types'; -// import { IconButton } from 'ucom'; import { BackButton } from '@/components/back'; // @@ -355,6 +355,7 @@ export default function AgentEditor({ agentJson, agentModuleSrc, env, + storageAdapter: 'pglite', }); setWorker(newWorker); diff --git a/packages/usdk/packages/upstreet-agent/packages/react-agents-browser/browser-runtime.ts b/packages/usdk/packages/upstreet-agent/packages/react-agents-browser/browser-runtime.ts index 30a4a6ff0..112e9dab0 100644 --- a/packages/usdk/packages/upstreet-agent/packages/react-agents-browser/browser-runtime.ts +++ b/packages/usdk/packages/upstreet-agent/packages/react-agents-browser/browser-runtime.ts @@ -8,10 +8,12 @@ export class ReactAgentsWorker { agentJson, agentModuleSrc, env, + storageAdapter, }: { agentJson: any, agentModuleSrc: string, env: any, + storageAdapter?: string, }) { if ( !agentJson || @@ -34,6 +36,7 @@ export class ReactAgentsWorker { agentJson, env, agentModuleSrc, + storageAdapter, }, }); this.worker.addEventListener('error', e => { diff --git a/packages/usdk/packages/upstreet-agent/packages/react-agents-browser/worker.tsx b/packages/usdk/packages/upstreet-agent/packages/react-agents-browser/worker.tsx index d60db39c8..4e18da0c9 100644 --- a/packages/usdk/packages/upstreet-agent/packages/react-agents-browser/worker.tsx +++ b/packages/usdk/packages/upstreet-agent/packages/react-agents-browser/worker.tsx @@ -25,7 +25,7 @@ globalThis.onmessage = (event: any) => { if (!rootPromise) { rootPromise = (async () => { const { args } = event.data; - const { agentJson, env, agentModuleSrc } = args; + const { agentJson, env, agentModuleSrc, storageAdapter } = args; if (typeof agentModuleSrc !== 'string') { throw new Error('agent worker: missing agentModuleSrc'); } @@ -38,6 +38,7 @@ globalThis.onmessage = (event: any) => { agentJson, codecs, env, + storageAdapter, }; const root = createRoot(rootOpts); root.render(); diff --git a/packages/usdk/packages/upstreet-agent/packages/react-agents/classes/agent-renderer.tsx b/packages/usdk/packages/upstreet-agent/packages/react-agents/classes/agent-renderer.tsx index 2816b51de..85ce3e5cf 100644 --- a/packages/usdk/packages/upstreet-agent/packages/react-agents/classes/agent-renderer.tsx +++ b/packages/usdk/packages/upstreet-agent/packages/react-agents/classes/agent-renderer.tsx @@ -95,6 +95,7 @@ export class AgentRenderer { env: any; config: any; chatsSpecification: ChatsSpecification; + supabase: any; codecs: any; registry: RenderRegistry; @@ -114,17 +115,20 @@ export class AgentRenderer { env, config, chatsSpecification, + supabase, codecs, }: { env: any; config: any; chatsSpecification: ChatsSpecification; + supabase: any; codecs: any; }) { // latch arguments this.env = env; this.config = config; this.chatsSpecification = chatsSpecification; + this.supabase = supabase; this.codecs = codecs; // create the app context @@ -151,9 +155,7 @@ export class AgentRenderer { return this.env.AGENT_TOKEN; }; const useSupabase = () => { - const jwt = useAuthToken(); - const supabase = new SupabaseStorage({ jwt }); - return supabase; + return this.supabase; }; const useConversationManager = () => { return this.conversationManager; diff --git a/packages/usdk/packages/upstreet-agent/packages/react-agents/hooks.ts b/packages/usdk/packages/upstreet-agent/packages/react-agents/hooks.ts index 7eadc2243..ab477a21b 100644 --- a/packages/usdk/packages/upstreet-agent/packages/react-agents/hooks.ts +++ b/packages/usdk/packages/upstreet-agent/packages/react-agents/hooks.ts @@ -27,9 +27,9 @@ import { ConversationContext, } from './context'; import { ExtendableMessageEvent } from './util/extendable-message-event'; -import { - supabaseSubscribe, -} from './util/supabase-utils.mjs'; +// import { +// supabaseSubscribe, +// } from './util/supabase-utils.mjs'; import { QueueManager, } from 'queue-manager'; @@ -404,23 +404,23 @@ export const usePurchases = () => { live = false; }; }, []); - // subscribe to webhooks - useEffect(() => { - const channel = supabaseSubscribe({ - supabase, - table: 'webhooks', - userId: ownerId, - }, (payload: any) => { - // console.log('subscription payload', payload); - const webhook = payload.new; - queueManager.waitForTurn(async () => { - await handleWebhook(webhook); - }); - }); - return () => { - supabase.removeChannel(channel); - }; - }, []); + // // subscribe to webhooks + // useEffect(() => { + // const channel = supabaseSubscribe({ + // supabase, + // table: 'webhooks', + // userId: ownerId, + // }, (payload: any) => { + // // console.log('subscription payload', payload); + // const webhook = payload.new; + // queueManager.waitForTurn(async () => { + // await handleWebhook(webhook); + // }); + // }); + // return () => { + // supabase.removeChannel(channel); + // }; + // }, []); const purchases = agentWebhooksState.webhooks.map((webhook) => { const { diff --git a/packages/usdk/packages/upstreet-agent/packages/react-agents/root.ts b/packages/usdk/packages/upstreet-agent/packages/react-agents/root.ts index b399ac03a..df43bf94c 100644 --- a/packages/usdk/packages/upstreet-agent/packages/react-agents/root.ts +++ b/packages/usdk/packages/upstreet-agent/packages/react-agents/root.ts @@ -1,6 +1,7 @@ -import React, { ReactNode } from 'react'; +import { ReactNode } from 'react'; import { headers } from './constants.mjs'; import { SupabaseStorage } from './storage/supabase-storage.mjs'; +import { PGliteStorage } from './storage/pglite-storage.mjs'; import { AgentRenderer } from './classes/agent-renderer.tsx'; import { ChatsSpecification } from './classes/chats-specification.ts'; import { multiplayerEndpointUrl } from './util/endpoints.mjs'; @@ -32,11 +33,30 @@ export class Root extends EventTarget { const { agentJson = {}, env = {}, - storageAdapter = new SupabaseStorage({ - jwt: env.AGENT_TOKEN, - }), codecs = {}, } = opts; + const storageAdapter = (() => { + const _storageAdapter = opts.storageAdapter ?? 'supabase'; + if (typeof _storageAdapter === 'string') { + switch (_storageAdapter) { + case 'supabase': { + return new SupabaseStorage({ + jwt: env.AGENT_TOKEN, + }); + } + case 'pglite': { + return new PGliteStorage({ + path: env.PGLITE_PATH, + }); + } + default: { + throw new Error('unknown storage adapter type: ' + _storageAdapter); + } + } + } else { + return _storageAdapter; + } + })(); this.chatsSpecification = new ChatsSpecification({ agentId: agentJson.id, @@ -46,6 +66,7 @@ export class Root extends EventTarget { env, config: agentJson, codecs, + supabase: storageAdapter, chatsSpecification: this.chatsSpecification, }); // const bindAlarm = () => { diff --git a/packages/usdk/packages/upstreet-agent/packages/react-agents/storage/pglite-storage.mjs b/packages/usdk/packages/upstreet-agent/packages/react-agents/storage/pglite-storage.mjs index 698f67668..833c6f662 100644 --- a/packages/usdk/packages/upstreet-agent/packages/react-agents/storage/pglite-storage.mjs +++ b/packages/usdk/packages/upstreet-agent/packages/react-agents/storage/pglite-storage.mjs @@ -1,137 +1,292 @@ import { PostgrestClient } from '@supabase/postgrest-js'; import { PGlite } from '@electric-sql/pglite'; +import { vector } from '@electric-sql/pglite/vector'; +import dedent from 'dedent'; +import { QueueManager } from 'queue-manager'; const defaultSchema = 'public'; +const initQueries = [ + dedent`\ + CREATE EXTENSION vector; + `, + dedent`\ + create table + chat_specifications ( + id text not null, + created_at timestamp with time zone not null default now(), + user_id uuid not null, + data jsonb null, + uid uuid not null default gen_random_uuid (), + constraint chat_specifications_pkey primary key (uid), + constraint chat_specifications_uid_key unique (uid) + ); + `, + dedent`\ + create table + keys_values ( + created_at timestamp with time zone not null default now(), + value jsonb null, + agent_id uuid null, + key text not null, + constraint keys_values_pkey primary key (key) + ); + `, + dedent`\ + create table + agent_messages ( + user_id uuid not null default gen_random_uuid (), + created_at timestamp with time zone not null default now(), + method text not null, + src_user_id uuid null, + src_name text null, + text text null, + args jsonb not null, + id uuid not null default gen_random_uuid (), + embedding vector(3072) not null, + conversation_id text null, + attachments jsonb null, + constraint agent_messages_pkey primary key (id) + ); + `, + dedent`\ + create table + webhooks ( + id uuid not null default gen_random_uuid (), + user_id uuid null, + type text not null, + data jsonb not null, + created_at timestamp with time zone not null default now(), + dev boolean null, + constraint stripe_connect_payments_pkey primary key (id) + ); + `, + dedent`\ + create table + pings ( + user_id uuid not null default gen_random_uuid (), + timestamp timestamp with time zone not null default now(), + constraint pings_pkey primary key (user_id) + ); + `, +]; + export class PGliteStorage { + pglite; + postgrestClient; + queueManager = new QueueManager(); + constructor({ path, - }) { - const pglite = new PGlite(path); + } = {}) { + // console.log('vector extension', vector); + // const vector2 = { + // name: 'vector', + // setup: (...args) => { + // console.log('vector setup', args, new Error().stack); + // return vector.setup(...args); + // }, + // }; + const pglite = new PGlite({ + dataDir: path, + extensions: { + vector, + // vector: vector2, + }, + }); this.pglite = pglite; - // await p.waitReady; + // (async () => { + // this.queueManager.waitForTurn(async () => { + // await pglite.waitReady; + // }); + // })(); // Hook up PostgrestQueryBuilder to PGlite by implementing fetch const fetch = async (url, init = {}) => { - // console.log('fetch', url, init); - - // Parse the SQL query from the URL and body - const urlObj = new URL(url); - const path = urlObj.pathname; - const searchParams = urlObj.searchParams; - const method = init.method || 'GET'; - - // // Get table name from headers - // const tableName = (init.headers?.['Accept-Profile'] || init.headers?.['Content-Profile'] || '').replace(/[^a-zA-Z0-9_]/g, ''); - // Get table name from the basename of the path - const tableName = path.split('/').pop(); - - // Convert Postgrest request to SQL query - let query; - if (method === 'GET') { - // Handle SELECT - const select = searchParams.get('select') || '*'; - const filter = Object.fromEntries(searchParams); - delete filter.select; - - query = `SELECT ${select} FROM ${tableName}`; - if (Object.keys(filter).length > 0) { - const conditions = Object.entries(filter) - .map(([key, value]) => `${key} = '${value}'`) - .join(' AND '); - query += ` WHERE ${conditions}`; - } - } else if (method === 'POST') { - // Handle INSERT or CREATE DATABASE - // if (path === '/rpc') { - // // read the query from the request body - // const body = JSON.parse(init.body); - // query = body.query; - // } else { - // Regular INSERT + return await this.queueManager.waitForTurn(async () => { + // Parse the SQL query from the URL and body + const urlObj = new URL(url); + const path = urlObj.pathname; + const searchParams = urlObj.searchParams; + const method = init.method || 'GET'; + + // Get table name from the basename of the path + const tableName = path.split('/').pop(); + + const parseConditions = (params) => { + const conditions = []; + for (const [key, value] of params) { + if (key === 'select' || key === 'order' || key === 'limit' || key === 'on_conflict') { + continue; + } + const [operator, filterValue] = value.split('.'); + switch(operator) { + case 'eq': + conditions.push(`${key} = '${filterValue}'`); + break; + case 'gt': + conditions.push(`${key} > '${filterValue}'`); + break; + case 'lt': + conditions.push(`${key} < '${filterValue}'`); + break; + case 'gte': + conditions.push(`${key} >= '${filterValue}'`); + break; + case 'lte': + conditions.push(`${key} <= '${filterValue}'`); + break; + case 'neq': + conditions.push(`${key} != '${filterValue}'`); + break; + default: + conditions.push(`${key} = '${value}'`); + } + } + return conditions.join(' AND '); + }; + + const stringifyValue = (value) => { + if (value === null) return 'NULL'; + if (typeof value === 'string') return `'${value.replace(/'/g, "''")}'`; + if (typeof value === 'object') return `'${JSON.stringify(value).replace(/'/g, "''")}'`; + return value; + }; + + // Convert Postgrest request to SQL query + let query; + if (method === 'GET') { + // Handle SELECT + const select = searchParams.get('select') || '*'; + const filter = Object.fromEntries(searchParams); + + query = `SELECT ${select} FROM ${tableName}`; + + // Handle PostgREST filter operators + const conditions = parseConditions(searchParams); + if (conditions) { + query += ` WHERE ${conditions}`; + } + + // Handle order + const order = searchParams.get('order'); + if (order) { + const [column, direction] = order.split('.'); + query += ` ORDER BY ${column} ${direction.toUpperCase()}`; + } + + // Handle limit + const limit = searchParams.get('limit'); + if (limit) { + query += ` LIMIT ${limit}`; + } + + } else if (method === 'POST') { + // Handle INSERT const body = JSON.parse(init.body); const columns = Object.keys(body).join(', '); const values = Object.values(body) - .map(v => typeof v === 'string' ? `'${v}'` : v) + .map(stringifyValue) .join(', '); query = `INSERT INTO ${tableName} (${columns}) VALUES (${values})`; - // } - } else if (method === 'PATCH') { - // Handle UPDATE - const body = JSON.parse(init.body); - const updates = Object.entries(body) - .map(([key, value]) => `${key} = '${value}'`) - .join(', '); - query = `UPDATE ${tableName} SET ${updates}`; - // Add WHERE clause from search params - if (searchParams.toString()) { - const conditions = Array.from(searchParams) - .map(([key, value]) => `${key} = '${value}'`) - .join(' AND '); - query += ` WHERE ${conditions}`; - } - } else if (method === 'DELETE') { - // Handle DELETE - if (path.startsWith('/database/')) { - // Handle DROP DATABASE - const dbName = path.slice('/database/'.length).replace(/[^a-zA-Z0-9_]/g, ''); - query = `DROP DATABASE ${dbName}`; - } else { - // Regular DELETE + + // Handle ON CONFLICT + const onConflict = searchParams.get('on_conflict'); + if (onConflict) { + query += ` ON CONFLICT (${onConflict}) DO UPDATE SET `; + const updates = Object.entries(body) + .filter(([key]) => key !== onConflict) // Exclude the conflict column + .map(([key, value]) => `${key} = ${stringifyValue(value)}`) + .join(', '); + query += updates; + } + } else if (method === 'PATCH') { + // Handle UPDATE + const body = JSON.parse(init.body); + const updates = Object.entries(body) + .map(([key, value]) => `${key} = ${stringifyValue(value)}`) + .join(', '); + query = `UPDATE ${tableName} SET ${updates}`; + + // Add WHERE clause from search params with operators + const conditions = parseConditions(searchParams); + if (conditions) { + query += ` WHERE ${conditions}`; + } + } else if (method === 'DELETE') { + // Handle DELETE query = `DELETE FROM ${tableName}`; - if (searchParams.toString()) { - const conditions = Array.from(searchParams) - .map(([key, value]) => `${key} = '${value}'`) - .join(' AND '); + const conditions = parseConditions(searchParams); + if (conditions) { query += ` WHERE ${conditions}`; } } - } - - try { - // Execute the protocol message - // console.log('executing query', query); - const result = await pglite.query(query); - // console.log('result', result); // , new TextDecoder().decode(result.data)); - - // Parse result and convert to JSON response - return new Response(JSON.stringify(result?.rows ?? null), { - status: 200, - headers: { - 'Content-Type': 'application/vnd.pgrst.object+json', - } - }); - - } catch (err) { - console.warn('error', err); - return new Response(JSON.stringify({ - error: err.message - }), { - status: 500, - headers: { - 'Content-Type': 'application/json' - } - }); - } + + try { + // console.log('execute query', query, { + // searchParams: Object.fromEntries(searchParams), + // init, + // }); + const result = await pglite.query(query); + return new Response(JSON.stringify(result?.rows ?? null), { + status: 200, + headers: { + 'Content-Type': 'application/vnd.pgrst.object+json', + } + }); + } catch (err) { + console.warn('error', err); + return new Response(JSON.stringify({ + error: err.message + }), { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + }); + } + }); }; this.postgrestClient = new PostgrestClient(new URL('http://localhost'), { fetch, schema: defaultSchema, }); + + (async () => { + await this.#init(); + })(); } - query(query) { - return this.pglite.query(query); + async #init() { + // console.log('init 1'); + const initPromises = []; + for (const initQuery of initQueries) { + const p = this.query(initQuery); + initPromises.push(p); + } + const initResults = await Promise.all(initPromises); + // console.log('init 2', initResults); } - sql(strings, ...values) { - const query = strings.reduce((acc, str, i) => { - acc.push(str); - if (i < values.length) { - acc.push(values[i]); - } - return acc; - }, []).join(''); - return this.query(query); + async query(query) { + return await this.queueManager.waitForTurn(async () => { + // console.log('init query', query); + const result = await this.pglite.query(query); + // console.log('init result', { + // query, + // result, + // }); + return result; + }); } + // sql(strings, ...values) { + // const query = strings.reduce((acc, str, i) => { + // acc.push(str); + // if (i < values.length) { + // acc.push(values[i]); + // } + // return acc; + // }, []).join(''); + // return this.query(query); + // } from(...args) { return this.postgrestClient.from(...args); }