diff --git a/apps/gateway/.eslintrc.json b/apps/gateway/.eslintrc.json index bda6f2fdb..74ddb8f2d 100644 --- a/apps/gateway/.eslintrc.json +++ b/apps/gateway/.eslintrc.json @@ -2,5 +2,8 @@ "extends": ["./node_modules/@latitude-data/eslint-config/library.js"], "env": { "node": true + }, + "rules": { + "no-constant-condition": "off" } } diff --git a/apps/gateway/package.json b/apps/gateway/package.json index 78fe61a75..51b62132e 100644 --- a/apps/gateway/package.json +++ b/apps/gateway/package.json @@ -3,7 +3,7 @@ "type": "module", "scripts": { "build": "tsc --build tsconfig.prod.json --verbose", - "dev": "tsx watch src", + "dev": "tsx watch src/server", "lint": "eslint src/", "prettier": "prettier --write \"**/*.{ts,tsx,md}\"", "start": "node -r module-alias/register ./dist --env=production", @@ -13,9 +13,12 @@ }, "dependencies": { "@hono/node-server": "^1.12.0", + "@hono/zod-validator": "^0.2.2", "@latitude-data/core": "workspace:^", "@latitude-data/env": "workspace:^", - "hono": "^4.5.3" + "@t3-oss/env-core": "^0.10.1", + "hono": "^4.5.3", + "zod": "^3.23.8" }, "devDependencies": { "@latitude-data/eslint-config": "workspace:^", diff --git a/apps/gateway/src/common/env.ts b/apps/gateway/src/common/env.ts new file mode 100644 index 000000000..4fa552ff0 --- /dev/null +++ b/apps/gateway/src/common/env.ts @@ -0,0 +1,26 @@ +import '@latitude-data/env' + +import { createEnv } from '@t3-oss/env-core' +import { z } from 'zod' + +let env +if (process.env.NODE_ENV === 'development') { + env = await import('./env/development').then((r) => r.default) +} else if (process.env.NODE_ENV === 'test') { + env = await import('./env/test').then((r) => r.default) +} else { + env = process.env as { + GATEWAY_PORT: string + GATEWAY_HOST: string + } +} + +export default createEnv({ + skipValidation: + process.env.BUILDING_CONTAINER == 'true' || process.env.NODE_ENV === 'test', + server: { + GATEWAY_PORT: z.string().optional().default('8787'), + GATEWAY_HOST: z.string().optional().default('localhost'), + }, + runtimeEnv: env, +}) diff --git a/apps/gateway/src/common/env/development.ts b/apps/gateway/src/common/env/development.ts new file mode 100644 index 000000000..5368e3739 --- /dev/null +++ b/apps/gateway/src/common/env/development.ts @@ -0,0 +1,4 @@ +export default { + GATEWAY_PORT: '8787', + GATEWAY_HOST: 'localhost', +} diff --git a/apps/gateway/src/common/env/test.ts b/apps/gateway/src/common/env/test.ts new file mode 100644 index 000000000..021a76e2b --- /dev/null +++ b/apps/gateway/src/common/env/test.ts @@ -0,0 +1,6 @@ +import devEnv from './development' + +export default { + ...devEnv, + GATEWAY_PORT: '8788', +} diff --git a/apps/gateway/src/common/routes.ts b/apps/gateway/src/common/routes.ts index 781f4b3c4..55b5f145c 100644 --- a/apps/gateway/src/common/routes.ts +++ b/apps/gateway/src/common/routes.ts @@ -7,6 +7,7 @@ const ROUTES = { Documents: { Base: '/projects/:projectId/commits/:commitUuid/documents', Get: '/:documentPath{.+}', + Run: '/run', }, }, }, diff --git a/apps/gateway/src/index.ts b/apps/gateway/src/index.ts index 54b572d20..3dfb08478 100644 --- a/apps/gateway/src/index.ts +++ b/apps/gateway/src/index.ts @@ -1,6 +1,3 @@ -import '@latitude-data/env' - -import { serve } from '@hono/node-server' import { Hono } from 'hono' import { logger } from 'hono/logger' import jetPaths from 'jet-paths' @@ -24,16 +21,4 @@ app.route(jetPaths(ROUTES).Api.V1.Documents.Base, documentsRouter) // Must be the last one! app.use(errorHandlerMiddleware()) -serve( - { - fetch: app.fetch, - overrideGlobalObjects: undefined, - port: parseInt(process.env.GATEWAY_PORT || '4000', 10), - hostname: process.env.GATEWAY_HOSTNAME || 'localhost', - }, - (info) => { - console.log(`Listening on http://localhost:${info.port}`) - }, -) - export default app diff --git a/apps/gateway/src/middlewares/auth.ts b/apps/gateway/src/middlewares/auth.ts index 95f655332..128baa51e 100644 --- a/apps/gateway/src/middlewares/auth.ts +++ b/apps/gateway/src/middlewares/auth.ts @@ -3,7 +3,6 @@ import { unsafelyGetApiKeyByToken, } from '@latitude-data/core' import type { Workspace } from '@latitude-data/core/browser' -import { Context } from 'hono' import { bearerAuth } from 'hono/bearer-auth' declare module 'hono' { @@ -14,7 +13,7 @@ declare module 'hono' { const authMiddleware = () => bearerAuth({ - verifyToken: async (token: string, c: Context) => { + verifyToken: async (token: string, c) => { const apiKeyResult = await unsafelyGetApiKeyByToken({ token }) if (apiKeyResult.error) return false diff --git a/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/handlers/_shared.ts b/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/handlers/_shared.ts new file mode 100644 index 000000000..aa83b45d2 --- /dev/null +++ b/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/handlers/_shared.ts @@ -0,0 +1,45 @@ +import { + CommitsRepository, + DocumentVersionsRepository, + ProjectsRepository, +} from '@latitude-data/core' +import type { Workspace } from '@latitude-data/core/browser' + +const toDocumentPath = (path: string) => { + if (path.startsWith('/')) { + return path + } + + return `/${path}` +} + +export const getData = async ({ + workspace, + projectId, + commitUuid, + documentPath, +}: { + workspace: Workspace + projectId: number + commitUuid: string + documentPath: string +}) => { + const projectsScope = new ProjectsRepository(workspace.id) + const commitsScope = new CommitsRepository(workspace.id) + const docsScope = new DocumentVersionsRepository(workspace.id) + + const project = await projectsScope + .getProjectById(projectId) + .then((r) => r.unwrap()) + const commit = await commitsScope + .getCommitByUuid({ project, uuid: commitUuid }) + .then((r) => r.unwrap()) + const document = await docsScope + .getDocumentByPath({ + commit, + path: toDocumentPath(documentPath), + }) + .then((r) => r.unwrap()) + + return { project, commit, document } +} diff --git a/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/index.test.ts b/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/handlers/get.test.ts similarity index 100% rename from apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/index.test.ts rename to apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/handlers/get.test.ts diff --git a/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/handlers/get.ts b/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/handlers/get.ts new file mode 100644 index 000000000..7099cc45e --- /dev/null +++ b/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/handlers/get.ts @@ -0,0 +1,19 @@ +import { createFactory } from 'hono/factory' + +import { getData } from './_shared' + +const factory = createFactory() + +export const getHandler = factory.createHandlers(async (c) => { + const workspace = c.get('workspace') + const { projectId, commitUuid, documentPath } = c.req.param() + + const { document } = await getData({ + workspace, + projectId: Number(projectId!), + commitUuid: commitUuid!, + documentPath: documentPath!, + }) + + return c.json(document) +}) diff --git a/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/handlers/run.test.ts b/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/handlers/run.test.ts new file mode 100644 index 000000000..b78d66742 --- /dev/null +++ b/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/handlers/run.test.ts @@ -0,0 +1,144 @@ +import { + apiKeys, + ChainEventTypes, + database, + factories, + LATITUDE_EVENT, + mergeCommit, + Result, +} from '@latitude-data/core' +import app from '$/index' +import { eq } from 'drizzle-orm' +import { describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + streamText: vi.fn(), +})) + +vi.mock('@latitude-data/core', async (importOriginal) => { + const original = (await importOriginal()) as typeof importOriginal + + return { + ...original, + streamText: mocks.streamText, + } +}) + +describe('POST /run', () => { + describe('unauthorized', () => { + it('fails', async () => { + const res = await app.request( + '/api/v1/projects/1/commits/asldkfjhsadl/documents/run', + { + method: 'POST', + body: JSON.stringify({ + documentPath: '/path/to/document', + }), + }, + ) + + expect(res.status).toBe(401) + }) + }) + + describe('authorized', () => { + it('succeeds', async () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue({ + event: LATITUDE_EVENT, + data: { + type: ChainEventTypes.Complete, + response: { + text: 'Hello', + usage: {}, + }, + }, + }) + + controller.close() + }, + }) + + const response = new Promise((resolve) => { + resolve({ text: 'Hello', usage: {} }) + }) + + mocks.streamText.mockReturnValue( + new Promise((resolve) => { + resolve( + Result.ok({ + stream, + response, + }), + ) + }), + ) + + const { workspace, user, project } = await factories.createProject() + const apikey = await database.query.apiKeys.findFirst({ + where: eq(apiKeys.workspaceId, workspace.id), + }) + const path = '/path/to/document' + const { commit } = await factories.createDraft({ + project, + user, + }) + const document = await factories.createDocumentVersion({ + commit, + path, + content: ` + --- + provider: openai + model: gpt-4o + --- + + Ignore all the rest and just return "Hello". + `, + }) + + await mergeCommit(commit).then((r) => r.unwrap()) + + const route = `/api/v1/projects/${project!.id}/commits/${commit!.uuid}/documents/run` + const body = JSON.stringify({ + documentPath: document.documentVersion.path, + parameters: {}, + }) + const res = await app.request(route, { + method: 'POST', + body, + headers: { + Authorization: `Bearer ${apikey!.token}`, + 'Content-Type': 'application/json', + }, + }) + + expect(res.status).toBe(200) + expect(res.body).toBeInstanceOf(ReadableStream) + + const responseStream = res.body as ReadableStream + const reader = responseStream.getReader() + + let done = false + let value + while (!done) { + const { done: _done, value: _value } = await reader.read() + done = _done + if (_value) value = new TextDecoder().decode(_value) + } + + expect(done).toBe(true) + expect(JSON.parse(value!)).toEqual({ + event: LATITUDE_EVENT, + data: { + type: ChainEventTypes.Complete, + response: { + text: 'Hello', + usage: {}, + }, + }, + id: '0', + }) + }) + }) +}) diff --git a/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/handlers/run.ts b/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/handlers/run.ts new file mode 100644 index 000000000..db3d95300 --- /dev/null +++ b/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/handlers/run.ts @@ -0,0 +1,60 @@ +import { zValidator } from '@hono/zod-validator' +import { streamText } from '@latitude-data/core' +import { Factory } from 'hono/factory' +import { SSEStreamingApi, streamSSE } from 'hono/streaming' +import { z } from 'zod' + +import { getData } from './_shared' + +const factory = new Factory() + +const runSchema = z.object({ + documentPath: z.string(), + parameters: z.record(z.any()).optional().default({}), +}) + +export const runHandler = factory.createHandlers( + zValidator('json', runSchema), + async (c) => { + return streamSSE(c, async (stream) => { + const { projectId, commitUuid } = c.req.param() + const { documentPath, parameters } = c.req.valid('json') + + const workspace = c.get('workspace') + + const { document } = await getData({ + workspace, + projectId: Number(projectId!), + commitUuid: commitUuid!, + documentPath: documentPath!, + }) + + const result = await streamText({ + document, + parameters, + }).then((r) => r.unwrap()) + + await pipeToStream(stream, result.stream) + }) + }, +) + +async function pipeToStream( + stream: SSEStreamingApi, + readableStream: ReadableStream, +) { + let id = 0 + const reader = readableStream.getReader() + + while (true) { + const { done, value } = await reader.read() + if (done) break + + stream.write( + JSON.stringify({ + ...value, + id: String(id++), + }), + ) + } +} diff --git a/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/index.ts b/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/index.ts index 98f3cd4ec..7f0cc80a5 100644 --- a/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/index.ts +++ b/apps/gateway/src/routes/api/v1/projects/:projectId/commits/:commitUuid/documents/index.ts @@ -1,39 +1,12 @@ import ROUTES from '$/common/routes' -import { - CommitsRepository, - DocumentVersionsRepository, - ProjectsRepository, -} from '$core/repositories' import { Hono } from 'hono' -const router = new Hono() - -router.get(ROUTES.Api.V1.Documents.Get, async (c) => { - const workspace = c.get('workspace') - // @ts-expect-error - hono cannot infer the params type from the route path - // unless you explicitely write it in the handler - const { projectId, commitUuid, documentPath } = c.req.param() +import { getHandler } from './handlers/get' +import { runHandler } from './handlers/run' - // scopes - const projectsScope = new ProjectsRepository(workspace.id) - const commitsScope = new CommitsRepository(workspace.id) - const docsScope = new DocumentVersionsRepository(workspace.id) - - // get project, commit, and document - const project = await projectsScope - .getProjectById(projectId) - .then((r) => r.unwrap()) - const commit = await commitsScope - .getCommitByUuid({ project, uuid: commitUuid }) - .then((r) => r.unwrap()) - const document = await docsScope - .getDocumentByPath({ - commit, - path: '/' + documentPath, - }) - .then((r) => r.unwrap()) +const router = new Hono() - return c.json(document) -}) +router.get(ROUTES.Api.V1.Documents.Get, ...getHandler) +router.post(ROUTES.Api.V1.Documents.Run, ...runHandler) export { router as documentsRouter } diff --git a/apps/gateway/src/server.ts b/apps/gateway/src/server.ts new file mode 100644 index 000000000..227cbc1cf --- /dev/null +++ b/apps/gateway/src/server.ts @@ -0,0 +1,16 @@ +import { serve } from '@hono/node-server' + +import app from '.' +import env from './common/env' + +serve( + { + fetch: app.fetch, + overrideGlobalObjects: undefined, + port: Number(env.GATEWAY_PORT), + hostname: env.GATEWAY_HOST, + }, + (info) => { + console.log(`Listening on http://localhost:${info.port}`) + }, +) diff --git a/apps/gateway/tsconfig.json b/apps/gateway/tsconfig.json index 46a901ad0..c88767b2a 100644 --- a/apps/gateway/tsconfig.json +++ b/apps/gateway/tsconfig.json @@ -2,17 +2,22 @@ "extends": "@latitude-data/typescript-config/base.json", "compilerOptions": { "moduleResolution": "Bundler", + + "strict": true, + "outDir": "./dist", + + // Path aliases "baseUrl": ".", "paths": { + "@latitude-data/core": ["../../packages/core/src/*"], + "@latitude-data/jobs": ["../../packages/jobs/src/*"], + "@latitude-data/web-ui": ["../../packages/web-ui/src/*"], "$/*": ["./src/*"], "$compiler/*": ["../../packages/compiler/src/*"], "$core/*": ["../../packages/core/src/*"], "$jobs/*": ["../../packages/jobs/src/*"], "$ui/*": ["../../packages/web-ui/src/*"], - "@latitude-data/core": ["../../packages/core/src/*"], - "@latitude-data/jobs": ["../../packages/jobs/src/*"], - "@latitude-data/web-ui": ["../../packages/web-ui/src/*"], "acorn": ["node_modules/@latitude-data/typescript-config/types/acorn"] } }, diff --git a/packages/compiler/src/compiler/base/nodes/tags/message.ts b/packages/compiler/src/compiler/base/nodes/tags/message.ts index 40ecf18c7..eb1ef21c2 100644 --- a/packages/compiler/src/compiler/base/nodes/tags/message.ts +++ b/packages/compiler/src/compiler/base/nodes/tags/message.ts @@ -95,7 +95,7 @@ function buildMessage( if (role === MessageRole.system) { return { role, - content, + content: content[0]?.value, } as SystemMessage } diff --git a/packages/compiler/src/compiler/chain.test.ts b/packages/compiler/src/compiler/chain.test.ts index 267ab7de4..6df741657 100644 --- a/packages/compiler/src/compiler/chain.test.ts +++ b/packages/compiler/src/compiler/chain.test.ts @@ -1,6 +1,12 @@ import { CHAIN_STEP_TAG } from '$compiler/constants' import CompileError from '$compiler/error/error' -import { Conversation, MessageRole } from '$compiler/types' +import { + AssistantMessage, + Conversation, + MessageContent, + MessageRole, + UserMessage, +} from '$compiler/types' import { describe, expect, it, vi } from 'vitest' import { Chain } from './chain' @@ -48,7 +54,7 @@ describe('chain', async () => { it('computes in a single iteration when there is no step tag', async () => { const prompt = removeCommonIndent(` {{ foo = 'foo' }} - System messate + System message {{#each [1, 2, 3] as element}} @@ -95,21 +101,21 @@ describe('chain', async () => { const systemMessage = conversation.messages[0]! expect(systemMessage.role).toBe('system') - expect(systemMessage.content[0]!.value).toBe('System message') + expect(systemMessage.content).toBe('System message') - const userMessage = conversation.messages[1]! + const userMessage = conversation.messages[1]! as UserMessage expect(userMessage.role).toBe('user') expect(userMessage.content[0]!.value).toBe('User message: 1') - const userMessage2 = conversation.messages[2]! + const userMessage2 = conversation.messages[2]! as UserMessage expect(userMessage2.role).toBe('user') expect(userMessage2.content[0]!.value).toBe('User message: 2') - const userMessage3 = conversation.messages[3]! + const userMessage3 = conversation.messages[3]! as UserMessage expect(userMessage3.role).toBe('user') expect(userMessage3.content[0]!.value).toBe('User message: 3') - const assistantMessage = conversation.messages[4]! + const assistantMessage = conversation.messages[4]! as AssistantMessage expect(assistantMessage.role).toBe('assistant') expect(assistantMessage.content[0]!.value).toBe('Assistant message: foo') }) @@ -133,16 +139,18 @@ describe('chain', async () => { expect(completed1).toBe(false) expect(conversation1.messages.length).toBe(1) - expect(conversation1.messages[0]!.content[0]!.value).toBe('Message 1') + expect(conversation1.messages[0]!.content).toBe('Message 1') const { completed: completed2, conversation: conversation2 } = await chain.step('response') expect(completed2).toBe(true) expect(conversation2.messages.length).toBe(3) - expect(conversation2.messages[0]!.content[0]!.value).toBe('Message 1') - expect(conversation2.messages[1]!.content[0]!.value).toBe('response') - expect(conversation2.messages[2]!.content[0]!.value).toBe('Message 2') + expect(conversation2.messages[0]!.content).toBe('Message 1') + expect( + (conversation2.messages[1]!.content[0]! as MessageContent).value, + ).toBe('response') + expect(conversation2.messages[2]!.content).toBe('Message 2') }) it('fails when an assistant message is not provided in followup steps', async () => { @@ -227,9 +235,11 @@ describe('chain', async () => { }) const conversation = await complete({ chain }) - expect(conversation.messages[0]!.content[0]!.value).toBe('1') - expect(conversation.messages[1]!.content[0]!.value).toBe('') - expect(conversation.messages[2]!.content[0]!.value).toBe('2') + expect(conversation.messages[0]!.content).toBe('1') + expect( + (conversation.messages[1]!.content[0]! as MessageContent).value, + ).toBe('') + expect(conversation.messages[2]!.content).toBe('2') expect(func1).toHaveBeenCalledTimes(1) expect(func2).toHaveBeenCalledTimes(1) }) @@ -254,8 +264,7 @@ describe('chain', async () => { const conversation = await complete({ chain }) expect( - conversation.messages[conversation.messages.length - 1]!.content[0]! - .value, + conversation.messages[conversation.messages.length - 1]!.content, ).toBe('6') }) @@ -289,8 +298,7 @@ describe('chain', async () => { const conversation = await complete({ chain: correctChain }) expect( - conversation.messages[conversation.messages.length - 1]!.content[0]! - .value, + conversation.messages[conversation.messages.length - 1]!.content, ).toBe('6') const incorrectChain = new Chain({ @@ -327,10 +335,16 @@ describe('chain', async () => { const conversation = await complete({ chain, maxSteps: 5 }) expect(conversation.messages.length).toBe(7) - expect(conversation.messages[0]!.content[0]!.value).toBe('0') - expect(conversation.messages[2]!.content[0]!.value).toBe('1') - expect(conversation.messages[4]!.content[0]!.value).toBe('2') - expect(conversation.messages[6]!.content[0]!.value).toBe('3') + expect( + (conversation.messages[0]!.content[0]! as MessageContent).value, + ).toBe('0') + expect( + (conversation.messages[2]!.content[0]! as MessageContent).value, + ).toBe('1') + expect( + (conversation.messages[4]!.content[0]! as MessageContent).value, + ).toBe('2') + expect(conversation.messages[6]!.content).toBe('3') }) it('cannot access variables created in a loop outside its scope', async () => { @@ -399,8 +413,7 @@ describe('chain', async () => { `), ) expect( - conversation.messages[conversation.messages.length - 1]!.content[0]! - .value, + conversation.messages[conversation.messages.length - 1]!.content, ).toBe('9') }) @@ -420,8 +433,10 @@ describe('chain', async () => { const { conversation } = await chain.step('foo') expect(conversation.messages.length).toBe(2) - expect(conversation.messages[0]!.content[0]!.value).toBe('foo') - expect(conversation.messages[1]!.content[0]!.value).toBe('foo') + expect( + (conversation.messages[0]!.content[0]! as MessageContent).value, + ).toBe('foo') + expect(conversation.messages[1]!.content).toBe('foo') }) it('returns the correct configuration in all steps', async () => { diff --git a/packages/compiler/src/compiler/compile.test.ts b/packages/compiler/src/compiler/compile.test.ts index 6d3a035f4..907ab6614 100644 --- a/packages/compiler/src/compiler/compile.test.ts +++ b/packages/compiler/src/compiler/compile.test.ts @@ -1,6 +1,13 @@ import { CUSTOM_TAG_END, CUSTOM_TAG_START } from '$compiler/constants' import CompileError from '$compiler/error/error' -import { Message } from '$compiler/types' +import { + AssistantMessage, + Message, + MessageContent, + SystemMessage, + ToolMessage, + UserMessage, +} from '$compiler/types' import { describe, expect, it, vi } from 'vitest' import { render } from '.' @@ -29,7 +36,12 @@ async function getCompiledText( }) return result.messages.reduce((acc: string, message: Message) => { - return acc + message.content.map((c) => c.value).join('') + const content = + typeof message.content === 'string' + ? message.content + : (message.content as MessageContent[]).map((c) => c.value).join('') + + return acc + content }, '') } @@ -89,10 +101,7 @@ describe('comments', async () => { expect(result.messages.length).toBe(1) const message = result.messages[0]! - expect(message.content.length).toBe(1) - expect(message.content[0]!.type).toBe('text') - - const text = message.content[0]!.value + const text = message.content expect(text).toBe('anna\nbob\n\ncharlie') }) @@ -109,11 +118,9 @@ describe('comments', async () => { }) expect(result.messages.length).toBe(1) - const message = result.messages[0]! - expect(message.content.length).toBe(1) - expect(message.content[0]!.type).toBe('text') - expect(message.content[0]!.value).toBe('Test message') + const message = result.messages[0]! + expect(message.content).toBe('Test message') }) }) @@ -132,12 +139,12 @@ describe('messages', async () => { expect(result.messages.length).toBe(4) const systemMessage = result.messages[0]! - const userMessage = result.messages[1]! - const assistantMessage = result.messages[2]! - const toolMessage = result.messages[3]! + const userMessage = result.messages[1]! as UserMessage + const assistantMessage = result.messages[2]! as AssistantMessage + const toolMessage = result.messages[3]! as ToolMessage expect(systemMessage.role).toBe('system') - expect(systemMessage.content[0]!.value).toBe('system message') + expect(systemMessage.content).toBe('system message') expect(userMessage.role).toBe('user') expect(userMessage.content[0]!.value).toBe('user message') @@ -182,12 +189,12 @@ describe('messages', async () => { expect(result1.messages.length).toBe(1) const message1 = result1.messages[0]! expect(message1.role).toBe('system') - expect(message1.content[0]!.value).toBe('message') + expect(message1.content).toBe('message') expect(result2.messages.length).toBe(1) const message2 = result2.messages[0]! expect(message2.role).toBe('user') - expect(message2.content[0]!.value).toBe('message') + expect((message2.content[0] as MessageContent)!.value).toBe('message') }) it('raises an error when using an invalid message role', async () => { @@ -229,11 +236,11 @@ describe('messages', async () => { }) expect(result.messages.length).toBe(2) - const systemMessage = result.messages[0]! - const userMessage = result.messages[1]! + const systemMessage = result.messages[0]! as SystemMessage + const userMessage = result.messages[1]! as UserMessage expect(systemMessage.role).toBe('system') - expect(systemMessage.content[0]!.value).toBe('Test message') + expect(systemMessage.content).toBe('Test message') expect(userMessage.role).toBe('user') expect(userMessage.content[0]!.value).toBe('user message') @@ -243,11 +250,11 @@ describe('messages', async () => { describe('message contents', async () => { it('all messages can have multiple content tags', async () => { const prompt = ` - + text content image content another text content - + ` const result = await render({ prompt: removeCommonIndent(prompt), @@ -255,7 +262,7 @@ describe('message contents', async () => { }) expect(result.messages.length).toBe(1) - const message = result.messages[0]! + const message = result.messages[0]! as UserMessage expect(message.content.length).toBe(3) expect(message.content[0]!.type).toBe('text') @@ -296,10 +303,8 @@ describe('message contents', async () => { expect(result.messages.length).toBe(1) const message = result.messages[0]! - expect(message.content.length).toBe(1) - expect(message.content[0]!.type).toBe('text') - expect(message.content[0]!.value).toBe('Test message') + expect(message.content).toBe('Test message') }) }) @@ -554,14 +559,14 @@ describe('conditional expressions', async () => { }) expect(result1.messages.length).toBe(1) - const message1 = result1.messages[0]! + const message1 = result1.messages[0]! as UserMessage expect(message1.role).toBe('user') expect(message1.content.length).toBe(1) expect(message1.content[0]!.type).toBe('text') expect(message1.content[0]!.value).toBe('Foo!') expect(result2.messages.length).toBe(1) - const message2 = result2.messages[0]! + const message2 = result2.messages[0]! as AssistantMessage expect(message2.role).toBe('assistant') expect(message2.content.length).toBe(1) expect(message2.content[0]!.type).toBe('text') diff --git a/packages/compiler/src/compiler/compile.ts b/packages/compiler/src/compiler/compile.ts index 51a7c2c5f..6fd7654b9 100644 --- a/packages/compiler/src/compiler/compile.ts +++ b/packages/compiler/src/compiler/compile.ts @@ -159,7 +159,7 @@ export class Compile { if (content.length > 0) { const message = { role: MessageRole.system, - content, + content: content[0]!.value, } as SystemMessage this.addMessage(message) diff --git a/packages/compiler/src/compiler/index.ts b/packages/compiler/src/compiler/index.ts index d2f1b4eac..1aff26b8a 100644 --- a/packages/compiler/src/compiler/index.ts +++ b/packages/compiler/src/compiler/index.ts @@ -9,10 +9,10 @@ import { export async function render({ prompt, - parameters, + parameters = {}, }: { prompt: string - parameters: Record + parameters?: Record }): Promise { const iterator = new Chain({ prompt, parameters }) const { conversation, completed } = await iterator.step() diff --git a/packages/compiler/src/serializer/index.ts b/packages/compiler/src/serializer/index.ts index 4e1597d31..0e1a2e9a0 100644 --- a/packages/compiler/src/serializer/index.ts +++ b/packages/compiler/src/serializer/index.ts @@ -37,15 +37,19 @@ export function serialize(conversation: Conversation): string { } output += '>\n' - for (const content of message.content) { - if (content.type === 'text') { - output += addIndent(content.value, 2) + '\n' - continue - } + if (typeof message.content === 'string') { + output += addIndent(message.content, 2) + '\n' + } else { + for (const content of message.content) { + if (content.type === 'text') { + output += addIndent(content.value, 2) + '\n' + continue + } - output += addIndent(`<${content.type}>`, 2) + '\n' - output += addIndent(content.value, 4) + '\n' - output += addIndent(``, 2) + '\n' + output += addIndent(`<${content.type}>`, 2) + '\n' + output += addIndent(content.value, 4) + '\n' + output += addIndent(``, 2) + '\n' + } } if (message.role === MessageRole.assistant) { diff --git a/packages/compiler/src/types/index.ts b/packages/compiler/src/types/index.ts index 8dbfcfa21..c78e9dc36 100644 --- a/packages/compiler/src/types/index.ts +++ b/packages/compiler/src/types/index.ts @@ -2,6 +2,7 @@ import CompileError from '$compiler/error/error' import { Message } from './message' +// TODO: Improve this type export type Config = Record export type Conversation = { diff --git a/packages/compiler/src/types/message.ts b/packages/compiler/src/types/message.ts index b34a2b7c5..900214945 100644 --- a/packages/compiler/src/types/message.ts +++ b/packages/compiler/src/types/message.ts @@ -37,7 +37,10 @@ interface IMessage { content: MessageContent[] } -export type SystemMessage = IMessage & { role: MessageRole.system } +export type SystemMessage = { + role: MessageRole.system + content: string +} export type UserMessage = IMessage & { role: MessageRole.user diff --git a/packages/core/package.json b/packages/core/package.json index 7378781e5..1fea8a6b7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,9 +23,11 @@ "prettier": "prettier --write src/**/*.ts" }, "dependencies": { + "@ai-sdk/openai": "^0.0.40", "@latitude-data/compiler": "workspace:^", "@latitude-data/env": "workspace:^", "@t3-oss/env-core": "^0.10.1", + "ai": "^3.2.42", "bcrypt": "^5.1.1", "drizzle-orm": "0.31.4", "lodash-es": "^4.17.21", diff --git a/packages/core/src/ai/index.ts b/packages/core/src/ai/index.ts new file mode 100644 index 000000000..c34e316ef --- /dev/null +++ b/packages/core/src/ai/index.ts @@ -0,0 +1,72 @@ +import { createOpenAI } from '@ai-sdk/openai' +import { OpenAICompletionModelId } from '@ai-sdk/openai/internal' +import { Message } from '@latitude-data/compiler' +import { Providers } from '$core/browser' +import { + CallWarning, + CompletionTokenUsage, + CoreMessage, + FinishReason, + streamText, +} from 'ai' + +type FinishCallbackEvent = { + finishReason: FinishReason + usage: CompletionTokenUsage + text: string + toolCalls?: + | { + type: 'tool-call' + toolCallId: string + toolName: string + args: any + }[] + | undefined + toolResults?: never[] | undefined + rawResponse?: { + headers?: Record + } + warnings?: CallWarning[] +} + +type FinishCallback = (event: FinishCallbackEvent) => void + +function getProvider(provider: Providers) { + switch (provider) { + case Providers.OpenAI: + return createOpenAI + default: + throw new Error(`Provider ${provider} not supported`) + } +} + +export default async function ai( + { + prompt, + messages, + apiKey, + model, + provider, + }: { + prompt?: string + messages: Message[] + apiKey: string + model: OpenAICompletionModelId + provider: Providers + }, + { + onFinish, + }: { + onFinish?: FinishCallback + } = {}, +) { + const p = getProvider(provider)({ apiKey }) + const m = p(model) + + return await streamText({ + model: m, + prompt, + messages: messages as CoreMessage[], + onFinish, + }) +} diff --git a/packages/core/src/data-access/workspaces.ts b/packages/core/src/data-access/workspaces.ts index 6a3c270b5..579d8d9ec 100644 --- a/packages/core/src/data-access/workspaces.ts +++ b/packages/core/src/data-access/workspaces.ts @@ -1,7 +1,7 @@ -import { type Commit, type Workspace } from '$core/browser' +import { DocumentVersion, type Commit, type Workspace } from '$core/browser' import { database } from '$core/client' import { NotFoundError, Result, TypedResult } from '$core/lib' -import { commits, projects, workspaces } from '$core/schema' +import { commits, documentVersions, projects, workspaces } from '$core/schema' import { eq, getTableColumns } from 'drizzle-orm' export async function unsafelyFindWorkspace( @@ -30,3 +30,19 @@ export async function findWorkspaceFromCommit(commit: Commit, db = database) { return results[0] } + +export async function findWorkspaceFromDocument( + document: DocumentVersion, + db = database, +) { + const results = await db + .select(getTableColumns(workspaces)) + .from(workspaces) + .innerJoin(projects, eq(projects.workspaceId, workspaces.id)) + .innerJoin(commits, eq(commits.projectId, projects.id)) + .innerJoin(documentVersions, eq(documentVersions.commitId, commits.id)) + .where(eq(documentVersions.id, document.id)) + .limit(1) + + return results[0] +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 40f6a0b8e..07fc6d560 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,5 +7,6 @@ export * from './lib' export * from './schema' export * from './services' export * from './repositories' +export * from './ai' export { factories } diff --git a/packages/core/src/lib/Transaction.ts b/packages/core/src/lib/Transaction.ts index b51a1eefc..cc2461f2e 100644 --- a/packages/core/src/lib/Transaction.ts +++ b/packages/core/src/lib/Transaction.ts @@ -53,7 +53,7 @@ export default class Transaction { const code = (error as DatabaseError)?.code switch (code) { case DB_ERROR_CODES.UNIQUE_VIOLATION: - return Result.error(new ConflictError('Database conflict')) + return Result.error(new ConflictError((error as DatabaseError).message)) case DB_ERROR_CODES.INPUT_SYTAXT_ERROR: return Result.error(new UnprocessableEntityError('Invalid input', {})) default: diff --git a/packages/core/src/repositories/providerApiKeysRepository.ts b/packages/core/src/repositories/providerApiKeysRepository.ts index 0a1b9e5a3..26cc57167 100644 --- a/packages/core/src/repositories/providerApiKeysRepository.ts +++ b/packages/core/src/repositories/providerApiKeysRepository.ts @@ -29,4 +29,17 @@ export class ProviderApiKeysRepository extends Repository { const result = await this.db.select().from(this.scope) return Result.ok(result) } + + async findByName(name: string) { + const result = await this.db + .select() + .from(this.scope) + .where(eq(this.scope.name, name)) + + if (!result.length) { + return Result.error(new NotFoundError('ProviderApiKey not found')) + } + + return Result.ok(result[0]!) + } } diff --git a/packages/core/src/services/documents/index.ts b/packages/core/src/services/documents/index.ts index fd66bc7c5..62bf7382c 100644 --- a/packages/core/src/services/documents/index.ts +++ b/packages/core/src/services/documents/index.ts @@ -3,3 +3,4 @@ export * from './update' export * from './destroyDocument' export * from './destroyFolder' export * from './recomputeChanges' +export * from './streamText' diff --git a/packages/core/src/services/documents/streamText.ts b/packages/core/src/services/documents/streamText.ts new file mode 100644 index 000000000..f6223322c --- /dev/null +++ b/packages/core/src/services/documents/streamText.ts @@ -0,0 +1,172 @@ +import { Chain, Config, createChain, Message } from '@latitude-data/compiler' +import ai from '$core/ai' +import { DocumentVersion, ProviderApiKey } from '$core/browser' +import { findWorkspaceFromDocument } from '$core/data-access' +import { Result } from '$core/lib' +import { ProviderApiKeysRepository } from '$core/repositories' +import { CompletionTokenUsage } from 'ai' +import { z } from 'zod' + +export const PROVIDER_EVENT = 'provider-event' +export const LATITUDE_EVENT = 'latitude-event' + +export enum ChainEventTypes { + Step = 'chain-step', + Complete = 'chain-complete', +} + +type ChainEvent = { + data: { + type: ChainEventTypes + config: Config + messages: Message[] + response?: { text: string; usage: CompletionTokenUsage } + } + event: typeof LATITUDE_EVENT +} + +export async function streamText({ + document, + parameters, +}: { + document: DocumentVersion + parameters: Record +}) { + const workspace = await findWorkspaceFromDocument(document) + if (!workspace) throw new Error('Workspace not found') + + const scope = new ProviderApiKeysRepository(workspace.id) + const chain = createChain({ prompt: document.content, parameters }) + + let stream: ReadableStream + let response: Promise<{ text: string; usage: Record }> + + await new Promise((resolve) => { + stream = new ReadableStream({ + start(controller) { + response = iterate({ chain, scope, controller }) + + resolve() + }, + }) + }) + + return Result.ok({ + stream: stream!, + response: response!, + }) +} + +async function iterate({ + chain, + apiKey, + scope, + controller, + sentCount = 0, + lastResponse, +}: { + chain: Chain + scope: ProviderApiKeysRepository + controller: ReadableStreamDefaultController + sentCount?: number + apiKey?: ProviderApiKey + lastResponse?: { + text: string + usage: Record + } +}) { + try { + const { completed, conversation } = await chain.step(lastResponse?.text) + const config = validateConfig(conversation.config) + if (!apiKey || apiKey?.name !== conversation.config.apikey) { + apiKey = await findApiKey({ scope, name: config.apiKey }) + } + + const msgs = conversation.messages.slice(sentCount) + sentCount += msgs.length + + controller.enqueue({ + data: { + type: ChainEventTypes.Step, + config: conversation.config, + messages: msgs, + }, + event: LATITUDE_EVENT, + }) + + const result = await ai({ + messages: conversation.messages, + apiKey: apiKey.token, + provider: apiKey.provider, + model: config.model, + }) + + const reader = result.fullStream.getReader() + while (true) { + const { done, value } = await reader.read() + if (done) break + + controller.enqueue({ + data: value, + event: PROVIDER_EVENT, + }) + } + + const response = { + text: await result.text, + usage: await result.usage, + } + if (completed) { + controller.enqueue({ + event: LATITUDE_EVENT, + data: { + type: ChainEventTypes.Complete, + response, + }, + }) + + controller.close() + + return response + } else { + return iterate({ + chain, + scope, + controller, + apiKey, + sentCount, + lastResponse: response, + }) + } + } catch (error) { + controller.error(error) + controller.close() + + return { + text: `ERROR: ${(error as Error).message}`, + usage: {}, + } + } +} + +function validateConfig(config: Record) { + const configSchema = z.object({ + model: z.string(), + apiKey: z.string(), + }) + + return configSchema.parse(config) +} + +async function findApiKey({ + scope, + name, +}: { + scope: ProviderApiKeysRepository + name: string +}) { + const apiKeyResult = await scope.findByName(name) + if (apiKeyResult.error) throw apiKeyResult.error + + return apiKeyResult.value! +} diff --git a/packages/web-ui/src/ds/molecules/Chat/Message/index.tsx b/packages/web-ui/src/ds/molecules/Chat/Message/index.tsx index a1030e7ae..700212c79 100644 --- a/packages/web-ui/src/ds/molecules/Chat/Message/index.tsx +++ b/packages/web-ui/src/ds/molecules/Chat/Message/index.tsx @@ -29,7 +29,7 @@ const MessageVariants = { export type MessageProps = { role: string - content: MessageContent[] + content: MessageContent[] | string className?: string variant?: keyof typeof MessageVariants animatePulse?: boolean @@ -54,18 +54,35 @@ export function Message({
- {content.map((c, contentIndex) => - c.value.split('\n').map((line, lineIndex) => ( - + ) : ( + content.map((c, idx) => ( + - {line} - - )), + value={c.value} + /> + )) )}
) } + +const ContentValue = ({ + index = 0, + color, + value, +}: { + index?: number + color: TextColor + value: string +}) => { + return value.split('\n').map((line, lineIndex) => ( + + {line} + + )) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97b516ff5..522546f2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,15 +54,24 @@ importers: '@hono/node-server': specifier: ^1.12.0 version: 1.12.0 + '@hono/zod-validator': + specifier: ^0.2.2 + version: 0.2.2(hono@4.5.3)(zod@3.23.8) '@latitude-data/core': specifier: workspace:^ version: link:../../packages/core '@latitude-data/env': specifier: workspace:^ version: link:../../packages/env + '@t3-oss/env-core': + specifier: ^0.10.1 + version: 0.10.1(typescript@5.5.4)(zod@3.23.8) hono: specifier: ^4.5.3 version: 4.5.3 + zod: + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@latitude-data/eslint-config': specifier: workspace:^ @@ -255,6 +264,9 @@ importers: packages/core: dependencies: + '@ai-sdk/openai': + specifier: ^0.0.40 + version: 0.0.40(zod@3.23.8) '@latitude-data/compiler': specifier: workspace:^ version: link:../compiler @@ -264,6 +276,9 @@ importers: '@t3-oss/env-core': specifier: ^0.10.1 version: 0.10.1(typescript@5.5.3)(zod@3.23.8) + ai: + specifier: ^3.2.42 + version: 3.2.42(zod@3.23.8) bcrypt: specifier: ^5.1.1 version: 5.1.1 @@ -552,6 +567,120 @@ importers: packages: + /@ai-sdk/openai@0.0.40(zod@3.23.8): + resolution: {integrity: sha512-9Iq1UaBHA5ZzNv6j3govuKGXrbrjuWvZIgWNJv4xzXlDMHu9P9hnqlBr/Aiay54WwCuTVNhTzAUTfFgnTs2kbQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + dependencies: + '@ai-sdk/provider': 0.0.14 + '@ai-sdk/provider-utils': 1.0.5(zod@3.23.8) + zod: 3.23.8 + dev: false + + /@ai-sdk/provider-utils@1.0.5(zod@3.23.8): + resolution: {integrity: sha512-XfOawxk95X3S43arn2iQIFyWGMi0DTxsf9ETc6t7bh91RPWOOPYN1tsmS5MTKD33OGJeaDQ/gnVRzXUCRBrckQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + peerDependenciesMeta: + zod: + optional: true + dependencies: + '@ai-sdk/provider': 0.0.14 + eventsource-parser: 1.1.2 + nanoid: 3.3.6 + secure-json-parse: 2.7.0 + zod: 3.23.8 + dev: false + + /@ai-sdk/provider@0.0.14: + resolution: {integrity: sha512-gaQ5Y033nro9iX1YUjEDFDRhmMcEiCk56LJdIUbX5ozEiCNCfpiBpEqrjSp/Gp5RzBS2W0BVxfG7UGW6Ezcrzg==} + engines: {node: '>=18'} + dependencies: + json-schema: 0.4.0 + dev: false + + /@ai-sdk/react@0.0.33(zod@3.23.8): + resolution: {integrity: sha512-HUDRx5iKxdSnfx9RoqNCL4bzO7FMBBWndjOhH+N/PU4cid5Xx+LRlI+rJGakv85nDAl8Y0mVYel22vP1UZ031g==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || >=18.x + zod: ^3.0.0 + peerDependenciesMeta: + react: + optional: true + zod: + optional: true + dependencies: + '@ai-sdk/provider-utils': 1.0.5(zod@3.23.8) + '@ai-sdk/ui-utils': 0.0.23(zod@3.23.8) + swr: 2.2.5 + zod: 3.23.8 + dev: false + + /@ai-sdk/solid@0.0.26(zod@3.23.8): + resolution: {integrity: sha512-kY9CJXYaZS57qi5Yc1hjkBtGwgEubkfn7P1CXYbY/wMpTkvlUaHw5JY2twMgG9qdq+uNWp1omZUVlVEkqZMqbA==} + engines: {node: '>=18'} + peerDependencies: + solid-js: ^1.7.7 + peerDependenciesMeta: + solid-js: + optional: true + dependencies: + '@ai-sdk/provider-utils': 1.0.5(zod@3.23.8) + '@ai-sdk/ui-utils': 0.0.23(zod@3.23.8) + transitivePeerDependencies: + - zod + dev: false + + /@ai-sdk/svelte@0.0.28(zod@3.23.8): + resolution: {integrity: sha512-n5MOV7UF/3wQvtGMUccMnoV+Xk524fVWzmPF9C0h1ZcS1oskutNqVbfZzFFoB2otD0/DBj2IS+rmHH4Dqc6D0g==} + engines: {node: '>=18'} + peerDependencies: + svelte: ^3.0.0 || ^4.0.0 + peerDependenciesMeta: + svelte: + optional: true + dependencies: + '@ai-sdk/provider-utils': 1.0.5(zod@3.23.8) + '@ai-sdk/ui-utils': 0.0.23(zod@3.23.8) + sswr: 2.1.0 + transitivePeerDependencies: + - zod + dev: false + + /@ai-sdk/ui-utils@0.0.23(zod@3.23.8): + resolution: {integrity: sha512-9KONrxwnoV9VyW9I3m9+cEi5IANvADeLuCe+oB3JzOobNKASwYwcQZ4X7no28DckfiJUmHk4gmPnsC3yfRoU5Q==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + peerDependenciesMeta: + zod: + optional: true + dependencies: + '@ai-sdk/provider': 0.0.14 + '@ai-sdk/provider-utils': 1.0.5(zod@3.23.8) + secure-json-parse: 2.7.0 + zod: 3.23.8 + dev: false + + /@ai-sdk/vue@0.0.27(zod@3.23.8): + resolution: {integrity: sha512-pYksYRUtMdAceZRKg9M6Rh1M4uSKJpa+cm0CC3Q2CMufE9Tgs3eEMB5ZqdSlP00uOifL3h36O14Y9vQwW4x7uw==} + engines: {node: '>=18'} + peerDependencies: + vue: ^3.3.4 + peerDependenciesMeta: + vue: + optional: true + dependencies: + '@ai-sdk/provider-utils': 1.0.5(zod@3.23.8) + '@ai-sdk/ui-utils': 0.0.23(zod@3.23.8) + swrv: 1.0.4 + transitivePeerDependencies: + - zod + dev: false + /@alloc/quick-lru@5.2.0: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -1584,6 +1713,16 @@ packages: engines: {node: '>=18.14.1'} dev: false + /@hono/zod-validator@0.2.2(hono@4.5.3)(zod@3.23.8): + resolution: {integrity: sha512-dSDxaPV70Py8wuIU2QNpoVEIOSzSXZ/6/B/h4xA7eOMz7+AarKTSGV8E6QwrdcCbBLkpqfJ4Q2TmBO0eP1tCBQ==} + peerDependencies: + hono: '>=3.9.0' + zod: ^3.19.1 + dependencies: + hono: 4.5.3 + zod: 3.23.8 + dev: false + /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -3337,6 +3476,19 @@ packages: zod: 3.23.8 dev: false + /@t3-oss/env-core@0.10.1(typescript@5.5.4)(zod@3.23.8): + resolution: {integrity: sha512-GcKZiCfWks5CTxhezn9k5zWX3sMDIYf6Kaxy2Gx9YEQftFcz8hDRN56hcbylyAO3t4jQnQ5ifLawINsNgCDpOg==} + peerDependencies: + typescript: '>=5.0.0' + zod: ^3.0.0 + peerDependenciesMeta: + typescript: + optional: true + dependencies: + typescript: 5.5.4 + zod: 3.23.8 + dev: false + /@t3-oss/env-nextjs@0.10.1(typescript@5.5.3)(zod@3.23.8): resolution: {integrity: sha512-iy2qqJLnFh1RjEWno2ZeyTu0ufomkXruUsOZludzDIroUabVvHsrSjtkHqwHp1/pgPUzN3yBRHMILW162X7x2Q==} peerDependencies: @@ -3442,6 +3594,10 @@ packages: '@types/node': 20.14.10 dev: true + /@types/diff-match-patch@1.0.36: + resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} + dev: false + /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -4124,6 +4280,47 @@ packages: humanize-ms: 1.2.1 dev: true + /ai@3.2.42(zod@3.23.8): + resolution: {integrity: sha512-7tQels82AgIq8aj9Oj88RBMH/md7dOvZqoz2+08Q5WMlBt3atE6HdUTlhg5OdRH6YWfzDxplN/zjxKEYdtwqlg==} + engines: {node: '>=18'} + peerDependencies: + openai: ^4.42.0 + react: ^18 || ^19 || >=18.x + sswr: ^2.1.0 + svelte: ^3.0.0 || ^4.0.0 + zod: ^3.0.0 + peerDependenciesMeta: + openai: + optional: true + react: + optional: true + sswr: + optional: true + svelte: + optional: true + zod: + optional: true + dependencies: + '@ai-sdk/provider': 0.0.14 + '@ai-sdk/provider-utils': 1.0.5(zod@3.23.8) + '@ai-sdk/react': 0.0.33(zod@3.23.8) + '@ai-sdk/solid': 0.0.26(zod@3.23.8) + '@ai-sdk/svelte': 0.0.28(zod@3.23.8) + '@ai-sdk/ui-utils': 0.0.23(zod@3.23.8) + '@ai-sdk/vue': 0.0.27(zod@3.23.8) + '@opentelemetry/api': 1.9.0 + eventsource-parser: 1.1.2 + json-schema: 0.4.0 + jsondiffpatch: 0.6.0 + nanoid: 3.3.6 + secure-json-parse: 2.7.0 + zod: 3.23.8 + zod-to-json-schema: 3.22.5(zod@3.23.8) + transitivePeerDependencies: + - solid-js + - vue + dev: false + /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: @@ -4144,6 +4341,7 @@ packages: /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} + requiresBuild: true dependencies: color-convert: 1.9.3 dev: true @@ -4531,6 +4729,11 @@ packages: supports-color: 7.2.0 dev: true + /chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: false + /check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} dependencies: @@ -4610,6 +4813,7 @@ packages: /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + requiresBuild: true dependencies: color-name: 1.1.3 dev: true @@ -4622,6 +4826,7 @@ packages: /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + requiresBuild: true dev: true /color-name@1.1.4: @@ -4904,6 +5109,10 @@ packages: /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + /diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + dev: false + /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5854,6 +6063,11 @@ packages: engines: {node: '>=6'} dev: true + /eventsource-parser@1.1.2: + resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==} + engines: {node: '>=14.18'} + dev: false + /execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} @@ -6237,6 +6451,7 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} + requiresBuild: true dev: true /has-flag@4.0.0: @@ -6757,6 +6972,10 @@ packages: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true + /json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + dev: false + /json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true @@ -6774,6 +6993,16 @@ packages: hasBin: true dev: true + /jsondiffpatch@0.6.0: + resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + '@types/diff-match-patch': 1.0.36 + chalk: 5.3.0 + diff-match-patch: 1.0.5 + dev: false + /jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -7118,6 +7347,12 @@ packages: object-assign: 4.1.1 thenify-all: 1.6.0 + /nanoid@3.3.6: + resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: false + /nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -8378,6 +8613,14 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + /sswr@2.1.0: + resolution: {integrity: sha512-Cqc355SYlTAaUt8iDPaC/4DPPXK925PePLMxyBKuWd5kKc5mwsG3nT9+Mq2tyguL5s7b4Jg+IRMpTRsNTAfpSQ==} + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + dependencies: + swrev: 4.0.0 + dev: false + /stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true @@ -8580,6 +8823,7 @@ packages: /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} + requiresBuild: true dependencies: has-flag: 3.0.0 dev: true @@ -8595,6 +8839,15 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + /swr@2.2.5: + resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || >=18.x + dependencies: + client-only: 0.0.1 + use-sync-external-store: 1.2.2 + dev: false + /swr@2.2.5(react@19.0.0-rc-378b305958-20240710): resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} peerDependencies: @@ -8605,6 +8858,16 @@ packages: use-sync-external-store: 1.2.2(react@19.0.0-rc-378b305958-20240710) dev: false + /swrev@4.0.0: + resolution: {integrity: sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==} + dev: false + + /swrv@1.0.4: + resolution: {integrity: sha512-zjEkcP8Ywmj+xOJW3lIT65ciY/4AL4e/Or7Gj0MzU3zBJNMdJiT8geVZhINavnlHRMMCcJLHhraLTAiDOTmQ9g==} + peerDependencies: + vue: '>=3.2.26 < 4' + dev: false + /symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} dev: true @@ -8948,7 +9211,6 @@ packages: resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} hasBin: true - dev: true /ufo@1.5.3: resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} @@ -9092,6 +9354,12 @@ packages: react: 18.3.0 dev: false + /use-sync-external-store@1.2.2: + resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=18.x + dev: false + /use-sync-external-store@1.2.2(react@19.0.0-rc-378b305958-20240710): resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} peerDependencies: @@ -9685,6 +9953,14 @@ packages: engines: {node: '>=12.20'} dev: true + /zod-to-json-schema@3.22.5(zod@3.23.8): + resolution: {integrity: sha512-+akaPo6a0zpVCCseDed504KBJUQpEW5QZw7RMneNmKw+fGaML1Z9tUNLnHHAC8x6dzVRO1eB2oEMyZRnuBZg7Q==} + peerDependencies: + zod: ^3.22.4 + dependencies: + zod: 3.23.8 + dev: false + /zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} dev: false diff --git a/tools/typescript/base.json b/tools/typescript/base.json index 9699f3b37..0208bda24 100644 --- a/tools/typescript/base.json +++ b/tools/typescript/base.json @@ -2,24 +2,24 @@ "$schema": "https://json.schemastore.org/tsconfig", "display": "Default", "compilerOptions": { + "allowJs": true, "declaration": false, "declarationMap": false, - "forceConsistentCasingInFileNames": true, - "allowJs": true, - "noEmit": true, "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, "incremental": false, "isolatedModules": true, "lib": ["es2022", "DOM", "DOM.Iterable", "esnext.asynciterable"], - "moduleDetection": "force", "module": "ESNext", + "moduleDetection": "force", "moduleResolution": "node", - "noUncheckedIndexedAccess": true, + "noEmit": true, "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, "resolveJsonModule": true, "skipLibCheck": true, "strict": true, - "noUnusedLocals": true, "target": "ES2022" } }