From 4885709bcdb9ee66877591ce56765ba2a5a968aa Mon Sep 17 00:00:00 2001 From: Gerard Clos Date: Mon, 28 Oct 2024 12:05:27 +0100 Subject: [PATCH] feat(core): user logs - Introduced a new job `uploadDocumentLogsJob` to handle bulk uploads of document logs from CSV files. - Added `createDocumentLogJob` to create individual document logs from parsed CSV data. - Updated `messageSchema` and `messagesSchema` to validate message structures. - Implemented `bulkUploadDocumentLogs` service to parse CSV files and enqueue jobs for processing. - Modified `ProviderLogsRepository` to include additional joins for better data retrieval. - Enhanced `createProviderLog` to handle optional fields and avoid errors when certain data is missing. - Updated various components to handle nullable fields and provide default values where necessary. - Added new constants for delimiter values and maximum upload size. - Updated SDK to include a new `log` method for logging messages and responses. These changes enable users to upload large sets of document logs in bulk, improving efficiency and user experience. The new job definitions and services ensure that the logs are processed asynchronously, reducing the load on the main application. --- apps/gateway/package.json | 1 - apps/gateway/src/common/messageSchema.ts | 73 ----- apps/gateway/src/common/routes.ts | 32 -- apps/gateway/src/middlewares/errorHandler.ts | 4 +- .../[conversationUuid]/handlers/chat.ts | 5 +- .../conversations/[conversationUuid]/index.ts | 3 +- .../versions/[versionUuid]/documents/index.ts | 7 +- .../[versionUuid]/documents/logs/index.ts | 8 + .../conversations/[conversationUuid]/index.ts | 3 +- .../versions/[versionUuid]/documents/index.ts | 7 +- .../documents/logs/handlers/post.ts | 50 +++ .../[versionUuid]/documents/logs/index.ts | 9 + apps/gateway/src/routes/app.ts | 19 +- apps/gateway/test/support/paths.ts | 6 - apps/web/src/actions/datasets/create.ts | 22 +- apps/web/src/actions/documentLogs/upload.ts | 50 +++ .../_components/DelimiterSelector/index.tsx | 26 +- .../src/app/(private)/datasets/new/page.tsx | 1 - .../EvaluationResultInfo/Metadata.tsx | 5 +- .../evaluations/[evaluationId]/layout.tsx | 11 +- .../DocumentLogs/DocumentLogInfo/Messages.tsx | 1 + .../DocumentLogs/DocumentLogInfo/Metadata.tsx | 145 +++++---- .../documents/[documentUuid]/logs/page.tsx | 18 +- .../logs/upload/UploadLogModal.tsx | 125 ++++++++ .../[documentUuid]/logs/upload/page.tsx | 19 ++ apps/web/src/app/_lib/formatUtils.ts | 3 +- apps/web/src/services/routes.ts | 1 + docs/assets/upload_logs_1.png | Bin 0 -> 52748 bytes .../evaluations/running-evaluations.mdx | 2 +- docs/guides/logs/upload-logs.mdx | 44 +++ docs/guides/sdk/javascript-typescript-sdk.mdx | 48 ++- docs/mint.json | 5 +- examples/sdks/typescript/package.json | 17 + examples/sdks/typescript/push_logs.ts | 30 ++ examples/sdks/typescript/tsconfig.json | 6 + packages/constants/src/errors.ts | 2 +- .../core/drizzle/0082_sparkling_redwing.sql | 6 + .../core/drizzle/0083_neat_johnny_storm.sql | 1 + .../core/drizzle/0084_special_whistler.sql | 1 + packages/core/drizzle/0085_silky_warstar.sql | 2 + .../drizzle/0086_polite_forgotten_one.sql | 8 + packages/core/drizzle/meta/0086_snapshot.json | 302 +++++------------- packages/core/drizzle/meta/_journal.json | 2 +- packages/core/src/constants.ts | 103 +++++- packages/core/src/jobs/constants.ts | 3 + .../documentLogs/createDocumentLogJob.ts | 46 +++ .../documentLogs/uploadDocumentLogsJob.ts | 42 +++ .../core/src/jobs/job-definitions/index.ts | 2 + .../core/src/jobs/job-definitions/types.d.ts | 2 + packages/core/src/lib/readCsv.ts | 8 +- .../getConnectedDocumentsWithMetadata.test.ts | 3 +- .../repositories/providerLogsRepository.ts | 4 - packages/core/src/schema/messages.ts | 0 .../core/src/schema/models/documentLogs.ts | 2 +- .../core/src/schema/models/providerLogs.ts | 20 +- .../src/services/documentLogs/bulkUpload.ts | 32 ++ .../computeDocumentLogWithMetadata.test.ts | 10 +- .../core/src/services/documentLogs/create.ts | 35 +- .../core/src/services/documentLogs/index.ts | 1 + .../run/handleEvaluationResponse.ts | 2 +- .../src/services/providerLogs/addMessages.ts | 17 +- .../core/src/services/providerLogs/create.ts | 34 +- .../core/src/tests/factories/documentLogs.ts | 2 +- packages/sdks/typescript/package.json | 20 +- packages/sdks/typescript/src/index.ts | 45 ++- packages/sdks/typescript/src/utils/errors.ts | 2 +- packages/sdks/typescript/src/utils/index.ts | 4 + packages/sdks/typescript/src/utils/types.ts | 15 +- .../src/ds/atoms/DropzoneInput/index.tsx | 2 +- .../web-ui/src/ds/atoms/FormField/index.tsx | 16 +- .../src/ds/atoms/FormFieldGroup/index.tsx | 6 +- packages/web-ui/src/ds/atoms/Switch/index.tsx | 4 +- pnpm-lock.yaml | 100 +++++- 73 files changed, 1157 insertions(+), 555 deletions(-) delete mode 100644 apps/gateway/src/common/messageSchema.ts delete mode 100644 apps/gateway/src/common/routes.ts create mode 100644 apps/gateway/src/routes/api/v1/projects/[projectId]/versions/[versionUuid]/documents/logs/index.ts create mode 100644 apps/gateway/src/routes/api/v2/projects/[projectId]/versions/[versionUuid]/documents/logs/handlers/post.ts create mode 100644 apps/gateway/src/routes/api/v2/projects/[projectId]/versions/[versionUuid]/documents/logs/index.ts delete mode 100644 apps/gateway/test/support/paths.ts create mode 100644 apps/web/src/actions/documentLogs/upload.ts create mode 100644 apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/upload/UploadLogModal.tsx create mode 100644 apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/upload/page.tsx create mode 100644 docs/assets/upload_logs_1.png create mode 100644 docs/guides/logs/upload-logs.mdx create mode 100644 examples/sdks/typescript/package.json create mode 100644 examples/sdks/typescript/push_logs.ts create mode 100644 examples/sdks/typescript/tsconfig.json create mode 100644 packages/core/drizzle/0082_sparkling_redwing.sql create mode 100644 packages/core/drizzle/0083_neat_johnny_storm.sql create mode 100644 packages/core/drizzle/0084_special_whistler.sql create mode 100644 packages/core/drizzle/0085_silky_warstar.sql create mode 100644 packages/core/drizzle/0086_polite_forgotten_one.sql create mode 100644 packages/core/src/jobs/job-definitions/documentLogs/createDocumentLogJob.ts create mode 100644 packages/core/src/jobs/job-definitions/documentLogs/uploadDocumentLogsJob.ts create mode 100644 packages/core/src/schema/messages.ts create mode 100644 packages/core/src/services/documentLogs/bulkUpload.ts diff --git a/apps/gateway/package.json b/apps/gateway/package.json index ce8f4287d..15e567a0e 100644 --- a/apps/gateway/package.json +++ b/apps/gateway/package.json @@ -27,7 +27,6 @@ "@t3-oss/env-core": "^0.10.1", "drizzle-orm": "^0.33.0", "hono": "^4.5.3", - "jet-paths": "^1.0.6", "lodash-es": "^4.17.21", "zod": "^3.23.8", "rate-limiter-flexible": "^5.0.3" diff --git a/apps/gateway/src/common/messageSchema.ts b/apps/gateway/src/common/messageSchema.ts deleted file mode 100644 index 5e2e3c3c6..000000000 --- a/apps/gateway/src/common/messageSchema.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { ContentType, MessageRole } from '@latitude-data/compiler' -import { z } from 'zod' - -const userContentSchema = z.array( - z - .object({ - type: z.literal(ContentType.text), - text: z.string(), - }) - .or( - z.object({ - type: z.literal(ContentType.image), - image: z - .string() - .or(z.instanceof(Uint8Array)) - .or(z.instanceof(Buffer)) - .or(z.instanceof(ArrayBuffer)) - .or(z.instanceof(URL)), - }), - ), -) - -// TODO: We could unify the way this schema is build. -// We could have this zod schema as base in a common package and -// infer compiler's types from it -export const messageSchema = z - .object({ - role: z.literal(MessageRole.system), - content: z.string(), - }) - .or( - z.object({ - role: z.literal(MessageRole.user), - name: z.string().optional(), - content: userContentSchema, - }), - ) - .or( - z.object({ - role: z.literal(MessageRole.assistant), - content: z.string().or( - z.array( - z.object({ - type: z.literal(ContentType.toolCall), - toolCallId: z.string(), - toolName: z.string(), - args: z.record(z.any()), - }), - ), - ), - toolCalls: z.array( - z.object({ - id: z.string(), - name: z.string(), - arguments: z.record(z.any()), - }), - ), - }), - ) - .or( - z.object({ - role: z.literal(MessageRole.tool), - content: z.array( - z.object({ - type: z.literal(ContentType.toolResult), - toolCallId: z.string(), - toolName: z.string(), - result: z.string(), - isError: z.boolean().optional(), - }), - ), - }), - ) diff --git a/apps/gateway/src/common/routes.ts b/apps/gateway/src/common/routes.ts deleted file mode 100644 index 61b659ecf..000000000 --- a/apps/gateway/src/common/routes.ts +++ /dev/null @@ -1,32 +0,0 @@ -const ROUTES = { - Base: '', - Api: { - Base: '/api', - V1: { - Base: '/v1', - Conversations: { - Base: '/conversations', - Chat: '/:conversationUuid/chat', - }, - Documents: { - Base: '/projects/:projectId/versions/:versionUuid/documents', - Get: '/:documentPath{.+}', - Run: '/run', - }, - }, - V2: { - Base: '/v2', - Conversations: { - Base: '/conversations', - Chat: '/:conversationUuid/chat', - }, - Documents: { - Base: '/projects/:projectId/versions/:versionUuid/documents', - Get: '/:documentPath{.+}', - Run: '/run', - }, - }, - }, -} - -export default ROUTES diff --git a/apps/gateway/src/middlewares/errorHandler.ts b/apps/gateway/src/middlewares/errorHandler.ts index 4fa2ea6d2..e0d8fd69f 100644 --- a/apps/gateway/src/middlewares/errorHandler.ts +++ b/apps/gateway/src/middlewares/errorHandler.ts @@ -40,8 +40,8 @@ const errorHandlerMiddleware = (err: Error) => { if (err instanceof HTTPException) { return Response.json( { - name: ApiErrorCodes.HTTPExeption, - errorCode: ApiErrorCodes.HTTPExeption, + name: ApiErrorCodes.HTTPException, + errorCode: ApiErrorCodes.HTTPException, message: err.message, details: { cause: err.cause }, }, diff --git a/apps/gateway/src/routes/api/v1/conversations/[conversationUuid]/handlers/chat.ts b/apps/gateway/src/routes/api/v1/conversations/[conversationUuid]/handlers/chat.ts index dd5497b15..ef6c39af1 100644 --- a/apps/gateway/src/routes/api/v1/conversations/[conversationUuid]/handlers/chat.ts +++ b/apps/gateway/src/routes/api/v1/conversations/[conversationUuid]/handlers/chat.ts @@ -1,9 +1,8 @@ import { zValidator } from '@hono/zod-validator' -import { LogSources } from '@latitude-data/core/browser' +import { LogSources, messagesSchema } from '@latitude-data/core/browser' import { streamToGenerator } from '@latitude-data/core/lib/streamToGenerator' import { addMessages } from '@latitude-data/core/services/documentLogs/index' import { captureException } from '@sentry/node' -import { messageSchema } from '$/common/messageSchema' import { Factory } from 'hono/factory' import { streamSSE } from 'hono/streaming' import { z } from 'zod' @@ -13,7 +12,7 @@ import { chainEventPresenter } from '../../../projects/[projectId]/versions/[ver const factory = new Factory() const schema = z.object({ - messages: z.array(messageSchema), + messages: messagesSchema, __internal: z .object({ source: z.nativeEnum(LogSources).optional(), diff --git a/apps/gateway/src/routes/api/v1/conversations/[conversationUuid]/index.ts b/apps/gateway/src/routes/api/v1/conversations/[conversationUuid]/index.ts index 0097fb7db..fd5d47fb1 100644 --- a/apps/gateway/src/routes/api/v1/conversations/[conversationUuid]/index.ts +++ b/apps/gateway/src/routes/api/v1/conversations/[conversationUuid]/index.ts @@ -1,8 +1,7 @@ -import ROUTES from '$/common/routes' import { Hono } from 'hono' import { chatHandler } from './handlers/chat' export const chatsRouter = new Hono() -chatsRouter.post(ROUTES.Api.V1.Conversations.Chat, ...chatHandler) +chatsRouter.post('/:conversationUuid/chat', ...chatHandler) diff --git a/apps/gateway/src/routes/api/v1/projects/[projectId]/versions/[versionUuid]/documents/index.ts b/apps/gateway/src/routes/api/v1/projects/[projectId]/versions/[versionUuid]/documents/index.ts index 7f0cc80a5..d7b0488ce 100644 --- a/apps/gateway/src/routes/api/v1/projects/[projectId]/versions/[versionUuid]/documents/index.ts +++ b/apps/gateway/src/routes/api/v1/projects/[projectId]/versions/[versionUuid]/documents/index.ts @@ -1,12 +1,13 @@ -import ROUTES from '$/common/routes' import { Hono } from 'hono' import { getHandler } from './handlers/get' import { runHandler } from './handlers/run' +import { logsRouterV1 } from './logs' const router = new Hono() -router.get(ROUTES.Api.V1.Documents.Get, ...getHandler) -router.post(ROUTES.Api.V1.Documents.Run, ...runHandler) +router.get('/:documentPath{.+}', ...getHandler) +router.post('/run', ...runHandler) +router.route('/logs', logsRouterV1) export { router as documentsRouter } diff --git a/apps/gateway/src/routes/api/v1/projects/[projectId]/versions/[versionUuid]/documents/logs/index.ts b/apps/gateway/src/routes/api/v1/projects/[projectId]/versions/[versionUuid]/documents/logs/index.ts new file mode 100644 index 000000000..c86c159b8 --- /dev/null +++ b/apps/gateway/src/routes/api/v1/projects/[projectId]/versions/[versionUuid]/documents/logs/index.ts @@ -0,0 +1,8 @@ +import { postHandler } from '$/routes/api/v2/projects/[projectId]/versions/[versionUuid]/documents/logs/handlers/post' +import { Hono } from 'hono' + +const router = new Hono() + +router.post('/', ...postHandler) + +export { router as logsRouterV1 } diff --git a/apps/gateway/src/routes/api/v2/conversations/[conversationUuid]/index.ts b/apps/gateway/src/routes/api/v2/conversations/[conversationUuid]/index.ts index e36f7679c..1472edaa0 100644 --- a/apps/gateway/src/routes/api/v2/conversations/[conversationUuid]/index.ts +++ b/apps/gateway/src/routes/api/v2/conversations/[conversationUuid]/index.ts @@ -1,7 +1,6 @@ -import ROUTES from '$/common/routes' import { chatHandler } from '$/routes/api/v1/conversations/[conversationUuid]/handlers/chat' import { Hono } from 'hono' export const chatsRouter = new Hono() -chatsRouter.post(ROUTES.Api.V2.Conversations.Chat, ...chatHandler) +chatsRouter.post('/:conversationUuid/chat', ...chatHandler) diff --git a/apps/gateway/src/routes/api/v2/projects/[projectId]/versions/[versionUuid]/documents/index.ts b/apps/gateway/src/routes/api/v2/projects/[projectId]/versions/[versionUuid]/documents/index.ts index c1c08c800..0ac69e9c6 100644 --- a/apps/gateway/src/routes/api/v2/projects/[projectId]/versions/[versionUuid]/documents/index.ts +++ b/apps/gateway/src/routes/api/v2/projects/[projectId]/versions/[versionUuid]/documents/index.ts @@ -1,12 +1,13 @@ -import ROUTES from '$/common/routes' import { getHandler } from '$/routes/api/v1/projects/[projectId]/versions/[versionUuid]/documents/handlers/get' import { Hono } from 'hono' import { runHandler } from './handlers/run' +import { logsRouterV2 } from './logs' const router = new Hono() -router.get(ROUTES.Api.V2.Documents.Get, ...getHandler) -router.post(ROUTES.Api.V2.Documents.Run, ...runHandler) +router.get('/:documentPath{.+}', ...getHandler) +router.post('/run', ...runHandler) +router.route('/logs', logsRouterV2) export { router as documentsRouter } diff --git a/apps/gateway/src/routes/api/v2/projects/[projectId]/versions/[versionUuid]/documents/logs/handlers/post.ts b/apps/gateway/src/routes/api/v2/projects/[projectId]/versions/[versionUuid]/documents/logs/handlers/post.ts new file mode 100644 index 000000000..265b69c07 --- /dev/null +++ b/apps/gateway/src/routes/api/v2/projects/[projectId]/versions/[versionUuid]/documents/logs/handlers/post.ts @@ -0,0 +1,50 @@ +import { zValidator } from '@hono/zod-validator' +import { LogSources, messagesSchema } from '@latitude-data/core/browser' +import { generateUUIDIdentifier } from '@latitude-data/core/lib/generateUUID' +import { createDocumentLog } from '@latitude-data/core/services/documentLogs/create' +import { getData } from '$/routes/api/v1/projects/[projectId]/versions/[versionUuid]/documents/handlers/_shared' +import { Factory } from 'hono/factory' +import { z } from 'zod' + +const factory = new Factory() + +const schema = z.object({ + path: z.string(), + messages: messagesSchema, + response: z.string().optional(), +}) + +export const postHandler = factory.createHandlers( + zValidator('json', schema), + async (c) => { + const { projectId, versionUuid } = c.req.param() + const { path, messages, response } = c.req.valid('json') + const workspace = c.get('workspace') + const { document, commit } = await getData({ + workspace, + projectId: Number(projectId!), + commitUuid: versionUuid!, + documentPath: path!, + }).then((r) => r.unwrap()) + + const documentLog = await createDocumentLog({ + data: { + uuid: generateUUIDIdentifier(), + documentUuid: document.documentUuid, + resolvedContent: document.content, + source: LogSources.API, + parameters: {}, + providerLog: { + messages, + responseText: + response ?? + (messages[messages.length - 1].content?.text || + messages[messages.length - 1].content), + }, + }, + commit, + }).then((r) => r.unwrap()) + + return c.json(documentLog) + }, +) diff --git a/apps/gateway/src/routes/api/v2/projects/[projectId]/versions/[versionUuid]/documents/logs/index.ts b/apps/gateway/src/routes/api/v2/projects/[projectId]/versions/[versionUuid]/documents/logs/index.ts new file mode 100644 index 000000000..94d12e74d --- /dev/null +++ b/apps/gateway/src/routes/api/v2/projects/[projectId]/versions/[versionUuid]/documents/logs/index.ts @@ -0,0 +1,9 @@ +import { Hono } from 'hono' + +import { postHandler } from './handlers/post' + +const router = new Hono() + +router.post('/', ...postHandler) + +export { router as logsRouterV2 } diff --git a/apps/gateway/src/routes/app.ts b/apps/gateway/src/routes/app.ts index 3d614340f..5ec73525c 100644 --- a/apps/gateway/src/routes/app.ts +++ b/apps/gateway/src/routes/app.ts @@ -1,10 +1,8 @@ -import ROUTES from '$/common/routes' import authMiddleware from '$/middlewares/auth' import errorHandlerMiddleware from '$/middlewares/errorHandler' import rateLimitMiddleware from '$/middlewares/rateLimit' import { Hono } from 'hono' import { logger } from 'hono/logger' -import jetPaths from 'jet-paths' import { chatsRouter as chatsRouterV1 } from './api/v1/conversations/[conversationUuid]' import { documentsRouter as documentsRouterV1 } from './api/v1/projects/[projectId]/versions/[versionUuid]/documents' @@ -26,10 +24,19 @@ app.use(rateLimitMiddleware()) app.use(authMiddleware()) // Routers -app.route(jetPaths(ROUTES).Api.V1.Documents.Base, documentsRouterV1) -app.route(jetPaths(ROUTES).Api.V1.Conversations.Base, chatsRouterV1) -app.route(jetPaths(ROUTES).Api.V2.Documents.Base, documentsRouterV2) -app.route(jetPaths(ROUTES).Api.V2.Conversations.Base, chatsRouterV2) +// v1 +app.route( + '/api/v1/projects/:projectId/versions/:versionUuid/documents', + documentsRouterV1, +) +app.route('/api/v1/conversations', chatsRouterV1) + +// v2 +app.route( + '/api/v2/projects/:projectId/versions/:versionUuid/documents', + documentsRouterV2, +) +app.route('/api/v2/conversations', chatsRouterV2) // Must be the last one! app.onError(errorHandlerMiddleware) diff --git a/apps/gateway/test/support/paths.ts b/apps/gateway/test/support/paths.ts deleted file mode 100644 index c9c3e80af..000000000 --- a/apps/gateway/test/support/paths.ts +++ /dev/null @@ -1,6 +0,0 @@ -import ROUTES from '$/common/routes' -import jetPaths from 'jet-paths' - -const paths = jetPaths(ROUTES) - -export default paths diff --git a/apps/web/src/actions/datasets/create.ts b/apps/web/src/actions/datasets/create.ts index c4034acf6..62da648bf 100644 --- a/apps/web/src/actions/datasets/create.ts +++ b/apps/web/src/actions/datasets/create.ts @@ -1,27 +1,17 @@ 'use server' +import { + DELIMITER_VALUES, + DELIMITERS_KEYS, + MAX_SIZE, + MAX_UPLOAD_SIZE_IN_MB, +} from '@latitude-data/core/browser' import { DatasetsRepository } from '@latitude-data/core/repositories' import { createDataset } from '@latitude-data/core/services/datasets/create' import { z } from 'zod' import { authProcedure } from '../procedures' -const DELIMITERS_KEYS = [ - 'comma', - 'semicolon', - 'tab', - 'space', - 'custom', -] as const -const DELIMITER_VALUES = { - comma: ',', - semicolon: ';', - tab: '\t', - space: ' ', -} - -const MAX_SIZE = 15 -const MAX_UPLOAD_SIZE_IN_MB = MAX_SIZE * 1024 * 1024 export const createDatasetAction = authProcedure .createServerAction() .input( diff --git a/apps/web/src/actions/documentLogs/upload.ts b/apps/web/src/actions/documentLogs/upload.ts new file mode 100644 index 000000000..9473dea21 --- /dev/null +++ b/apps/web/src/actions/documentLogs/upload.ts @@ -0,0 +1,50 @@ +'use server' + +import { + DELIMITERS_KEYS, + MAX_SIZE, + MAX_UPLOAD_SIZE_IN_MB, +} from '@latitude-data/core/browser' +import { CommitsRepository } from '@latitude-data/core/repositories' +import { bulkUploadDocumentLogs } from '@latitude-data/core/services/documentLogs/bulkUpload' +import { z } from 'zod' + +import { withDocument } from '../procedures' + +export const uploadDocumentLogsAction = withDocument + .createServerAction() + .input( + z.object({ + csvDelimiter: z.enum(DELIMITERS_KEYS, { + message: 'Choose a valid delimiter option', + }), + logsFile: z + .instanceof(File) + .refine((file) => { + return !file || file.size <= MAX_UPLOAD_SIZE_IN_MB + }, `Your file must be less than ${MAX_SIZE}MB in size`) + .refine( + (file) => file.type === 'text/csv', + 'Your file must be a CSV file', + ), + }), + ) + .handler(async ({ ctx, input }) => { + const commitsScope = new CommitsRepository(ctx.workspace.id) + const commit = await commitsScope + .getCommitByUuid({ + projectId: ctx.project.id, + uuid: ctx.currentCommitUuid, + }) + .then((r) => r.unwrap()) + + await bulkUploadDocumentLogs({ + workspace: ctx.workspace, + document: ctx.document, + commit, + csvDelimiter: input.csvDelimiter, + logsFile: input.logsFile, + }) + + return { success: true } + }) diff --git a/apps/web/src/app/(private)/datasets/new/_components/DelimiterSelector/index.tsx b/apps/web/src/app/(private)/datasets/new/_components/DelimiterSelector/index.tsx index 59dac5e29..56788c781 100644 --- a/apps/web/src/app/(private)/datasets/new/_components/DelimiterSelector/index.tsx +++ b/apps/web/src/app/(private)/datasets/new/_components/DelimiterSelector/index.tsx @@ -13,16 +13,22 @@ export enum DelimiterEnum { } const DELIMITERS = [ - { value: DelimiterEnum.Comma, label: 'Comma (ex.: column1,column2,column3)' }, + { + value: DelimiterEnum.Comma, + label: 'Comma (e.g.: column1,column2,column3)', + }, { value: DelimiterEnum.Semicolon, - label: 'Semicolon (ex.: column1;column2;column3)', + label: 'Semicolon (e.g.: column1;column2;column3)', }, { value: DelimiterEnum.Tab, - label: 'Tab (ex.: column1 \\t column2 \\t column3)', + label: 'Tab (e.g.: column1 \\t column2 \\t column3)', + }, + { + value: DelimiterEnum.Space, + label: 'Space (e.g.: column1 column2 column3)', }, - { value: DelimiterEnum.Space, label: 'Space (ex.: column1 column2 column3)' }, { value: DelimiterEnum.Custom, label: 'Custom' }, ] export const DELIMITER_KEYS = DELIMITERS.map(({ value }) => value) @@ -32,15 +38,15 @@ export default function DelimiterSelector({ customDelimiterInputName, delimiterErrors, customDelimiterErrors, - delimiterValue, + delimiterDefaultValue, customDelimiterValue, }: { delimiterInputName: string - delimiterValue: string | undefined - delimiterErrors: string[] | undefined + delimiterDefaultValue?: string | undefined + delimiterErrors?: string[] | undefined customDelimiterInputName: string - customDelimiterValue: string - customDelimiterErrors: string[] | undefined + customDelimiterValue?: string + customDelimiterErrors?: string[] | undefined }) { const inputRef = useRef(null) const [isCustom, setIsCustom] = useState(false) @@ -60,7 +66,7 @@ export default function DelimiterSelector({ + + + +
+ +
+ + Upload a csv file with your prompt logs.{' '} + + Learn more + + +
+
+ + + + + ) +} diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/upload/page.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/upload/page.tsx new file mode 100644 index 000000000..906855598 --- /dev/null +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/upload/page.tsx @@ -0,0 +1,19 @@ +import UploadLogModal from './UploadLogModal' + +export default function UploadLogModalPage({ + params, +}: { + params: { + documentUuid: string + projectId: string + commitUuid: string + } +}) { + return ( + + ) +} diff --git a/apps/web/src/app/_lib/formatUtils.ts b/apps/web/src/app/_lib/formatUtils.ts index 4d1c473f6..39dc1c665 100644 --- a/apps/web/src/app/_lib/formatUtils.ts +++ b/apps/web/src/app/_lib/formatUtils.ts @@ -15,7 +15,8 @@ export function relativeTime(date: Date | null) { return format(date, 'PPpp') } -export function formatDuration(duration: number) { +export function formatDuration(duration?: number | null) { + if (!duration) return '-' if (duration < MINUTES) return `${(duration / SECONDS).toFixed(3)}s` const hours = Math.floor(duration / HOURS) diff --git a/apps/web/src/services/routes.ts b/apps/web/src/services/routes.ts index 443415e20..ab7addb85 100644 --- a/apps/web/src/services/routes.ts +++ b/apps/web/src/services/routes.ts @@ -134,6 +134,7 @@ export const ROUTES = { }, [DocumentRoutes.logs]: { root: `${root}/${DocumentRoutes.logs}`, + upload: `${root}/${DocumentRoutes.logs}/upload`, }, } }, diff --git a/docs/assets/upload_logs_1.png b/docs/assets/upload_logs_1.png new file mode 100644 index 0000000000000000000000000000000000000000..64897efcd7920babccd833ccb7d75b3efb805608 GIT binary patch literal 52748 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/guides/evaluations/running-evaluations.mdx b/docs/guides/evaluations/running-evaluations.mdx index 97a98a58b..9f5461458 100644 --- a/docs/guides/evaluations/running-evaluations.mdx +++ b/docs/guides/evaluations/running-evaluations.mdx @@ -42,4 +42,4 @@ Click the "Run evaluation" button to start the evaluation process. You'll see th Evaluations running in live mode will run on all new logs generated in your project. This is useful if you want to monitor the performance of your prompts in real-time. -We recommend keeping a few key evaluations running in live mode to spot degradations in response quality as soon as they happen. Sometimes new model releases or changes in parameters can lead to a drop in response quality, so this is a good way to catch those issues early. \ No newline at end of file +We recommend keeping a few key evaluations running in live mode to spot degradations in response quality as soon as they happen. Sometimes new model releases or changes in parameters can lead to a drop in response quality, so this is a good way to catch those issues early. diff --git a/docs/guides/logs/upload-logs.mdx b/docs/guides/logs/upload-logs.mdx new file mode 100644 index 000000000..53392b187 --- /dev/null +++ b/docs/guides/logs/upload-logs.mdx @@ -0,0 +1,44 @@ +--- +title: Upload logs +description: 'Learn how to upload logs to Latitude and start evaluating your production systems.' +--- + +## Upload logs to Latitude + +Most users use Latitude's SDK to run their prompts via Latitude's AI Gateway. However, if you're using a different method to run your prompts, you can still upload logs to Latitude and evalute them in order to assess the quality of your outputs. + +### Upload logs from Latitude's Dashboard + +Navigate to your prompt's logs section and click on the **Upload logs** button. The following screen will appear: + +![Upload logs](/images/guides/logs/upload_logs_1.png) + +You can upload a list of logs in a single csv file. The CSV file should be a single column csv where each row is a log, i.e a user-assitant interaction. [Here's an example](https://docs.google.com/spreadsheets/d/1uxmUW2XhcqRB_cK0SBmHzUfa9xMqVzKZ0eT8umO8pr8/edit?usp=sharing) of a valid CSV file: + +```csv +"[{""role"":""system"",""content"":""Tell me a joke about School""},{""role"": ""assistant"", ""content"": ""Why did the math book look sad? Because it had too many problems!""}]" +"[{""role"":""system"",""content"":""Tell me a joke about Travel""}],{""role"": ""assistant"", ""content"": ""Why did the hiker refuse to eat pizza on the way to the top of the mountain? Because he was afraid of heights and the pizza was too big!""}]" +"[{""role"":""system"",""content"":""Tell me a joke about Work""}],{""role"": ""assistant"", ""content"": ""Why did the worker get fired from his job? Because he was not properly training for it!""}]" +"[{""role"":""system"",""content"":""Tell me a joke about Sports""}],{""role"": ""assistant"", ""content"": ""Why did the sports star quit his job? Because he was not properly training for it!""}]" +``` + + + Logs follow OpenAI's [ChatCompletion + format](https://platform.openai.com/docs/guides/text-generation/building-prompts). + If you're using a different method to run your prompts, you'll need to format + your logs accordingly. + + +Do not include headers in your CSV file. + +Once you've uploaded your logs, they will quickly appear in your prompt's logs section. + +### Evaluating uploaded logs + +To evaluate your uploaded logs, make sure to first create and connect an evaluation to your prompt. Also, make sure the evaluation is configured to [evaluate live logs](/guides/evaluations/running-evaluations). + +Once you've correctly configured your evaluation, just upload your logs as described above and they will be automatically evaluated in real time. + +### Upload logs from the SDK + +You can push logs to Latitude with Latitude's SDK. Check out Latitude' SDK [`log` method](/guides/sdk/javascript-typescript-sdk). diff --git a/docs/guides/sdk/javascript-typescript-sdk.mdx b/docs/guides/sdk/javascript-typescript-sdk.mdx index d15812c30..a9db3cc36 100644 --- a/docs/guides/sdk/javascript-typescript-sdk.mdx +++ b/docs/guides/sdk/javascript-typescript-sdk.mdx @@ -125,7 +125,53 @@ onError: (error) => { ``` **IMPORTANT**: If you don't provide `onError` callback `await sdk.run` will throw an error. -## More examples +## Pushing a log to Latitude + +You can push a log to Latitude in order to evaluate it, using the `log` method: + +```typescript +const sdk = new Latitude(process.env.LATITUDE_API_KEY, { + projectId: 1, +}) +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}) + +const messages = [ + { + role: 'user' as MessageRole.user, + content: [ + { + type: 'text' as ContentType.text, + text: 'Please tell me a joke about doctors', + }, + ], + }, +] +const chatCompletion = await openai.chat.completions.create({ + messages, + model: 'gpt-4o-mini', +}) + +// Push the log to the live version of our prompt called 'joker' +sdk.log('joker', messages, { + response: chatCompletion.choices[0].message.content, +}) +``` + + + Logs follow OpenAI's + [format](https://platform.openai.com/docs/guides/text-generation/building-prompts). + If you're using a different method to run your prompts, you'll need to format + your logs accordingly. + + + + If you include the assistant response in the optional `response` parameter, + make sure to not include it in the log so it isn't included twice. + + +## Common examples ### Executing tool calls diff --git a/docs/mint.json b/docs/mint.json index 46c2b518a..6c6892366 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -65,7 +65,10 @@ }, { "group": "Logs", - "pages": ["guides/logs/overview"] + "pages": [ + "guides/logs/overview", + "guides/logs/upload-logs" + ] }, { "group": "Datasets", diff --git a/examples/sdks/typescript/package.json b/examples/sdks/typescript/package.json new file mode 100644 index 000000000..c9e72f6fd --- /dev/null +++ b/examples/sdks/typescript/package.json @@ -0,0 +1,17 @@ +{ + "name": "typescript-examples", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@latitude-data/sdk": "workspace:^", + "openai": "^4.68.4" + } +} diff --git a/examples/sdks/typescript/push_logs.ts b/examples/sdks/typescript/push_logs.ts new file mode 100644 index 000000000..6f232149b --- /dev/null +++ b/examples/sdks/typescript/push_logs.ts @@ -0,0 +1,30 @@ +import { ContentType, Latitude, MessageRole } from '@latitude-data/sdk' +import OpenAI from 'openai' + +const sdk = new Latitude(process.env.LATITUDE_API_KEY, { + projectId: 1, +}) +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}) + +const messages = [ + { + role: 'user' as MessageRole.user, + content: [ + { + type: 'text' as ContentType.text, + text: 'Please tell me a joke about doctors', + }, + ], + }, +] +const chatCompletion = await openai.chat.completions.create({ + messages, + model: 'gpt-4o-mini', +}) + +// Push the log to the live version of our prompt called 'joker' +sdk.log('joker', messages, { + response: chatCompletion.choices[0].message.content, +}) diff --git a/examples/sdks/typescript/tsconfig.json b/examples/sdks/typescript/tsconfig.json new file mode 100644 index 000000000..41254be94 --- /dev/null +++ b/examples/sdks/typescript/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "module": "node16", + "moduleResolution": "node16" + } +} diff --git a/packages/constants/src/errors.ts b/packages/constants/src/errors.ts index f5628aea2..e8d6485a9 100644 --- a/packages/constants/src/errors.ts +++ b/packages/constants/src/errors.ts @@ -33,7 +33,7 @@ export type RunErrorDetails = : never export enum ApiErrorCodes { - HTTPExeption = 'http_exception', + HTTPException = 'http_exception', InternalServerError = 'internal_server_error', } diff --git a/packages/core/drizzle/0082_sparkling_redwing.sql b/packages/core/drizzle/0082_sparkling_redwing.sql new file mode 100644 index 000000000..6e242d5c0 --- /dev/null +++ b/packages/core/drizzle/0082_sparkling_redwing.sql @@ -0,0 +1,6 @@ +ALTER TABLE "latitude"."provider_logs" ALTER COLUMN "provider_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "latitude"."provider_logs" ALTER COLUMN "finish_reason" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "latitude"."provider_logs" ALTER COLUMN "config" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "latitude"."provider_logs" ALTER COLUMN "messages" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "latitude"."provider_logs" ALTER COLUMN "duration" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "latitude"."provider_logs" ALTER COLUMN "generated_at" DROP NOT NULL; \ No newline at end of file diff --git a/packages/core/drizzle/0083_neat_johnny_storm.sql b/packages/core/drizzle/0083_neat_johnny_storm.sql new file mode 100644 index 000000000..231b3f26d --- /dev/null +++ b/packages/core/drizzle/0083_neat_johnny_storm.sql @@ -0,0 +1 @@ +ALTER TABLE "latitude"."provider_logs" ALTER COLUMN "tokens" DROP NOT NULL; \ No newline at end of file diff --git a/packages/core/drizzle/0084_special_whistler.sql b/packages/core/drizzle/0084_special_whistler.sql new file mode 100644 index 000000000..2b48a204e --- /dev/null +++ b/packages/core/drizzle/0084_special_whistler.sql @@ -0,0 +1 @@ +ALTER TABLE "latitude"."document_logs" ALTER COLUMN "duration" DROP NOT NULL; \ No newline at end of file diff --git a/packages/core/drizzle/0085_silky_warstar.sql b/packages/core/drizzle/0085_silky_warstar.sql new file mode 100644 index 000000000..a58eadb2f --- /dev/null +++ b/packages/core/drizzle/0085_silky_warstar.sql @@ -0,0 +1,2 @@ +ALTER TYPE "latitude"."log_source" ADD VALUE 'user';--> statement-breakpoint +ALTER TABLE "latitude"."provider_logs" ALTER COLUMN "messages" SET NOT NULL; \ No newline at end of file diff --git a/packages/core/drizzle/0086_polite_forgotten_one.sql b/packages/core/drizzle/0086_polite_forgotten_one.sql new file mode 100644 index 000000000..ed54411b0 --- /dev/null +++ b/packages/core/drizzle/0086_polite_forgotten_one.sql @@ -0,0 +1,8 @@ +ALTER TYPE "latitude"."log_source" ADD VALUE 'user';--> statement-breakpoint +ALTER TABLE "latitude"."document_logs" ALTER COLUMN "duration" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "latitude"."provider_logs" ALTER COLUMN "provider_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "latitude"."provider_logs" ALTER COLUMN "finish_reason" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "latitude"."provider_logs" ALTER COLUMN "config" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "latitude"."provider_logs" ALTER COLUMN "tokens" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "latitude"."provider_logs" ALTER COLUMN "duration" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "latitude"."provider_logs" ALTER COLUMN "generated_at" DROP NOT NULL; \ No newline at end of file diff --git a/packages/core/drizzle/meta/0086_snapshot.json b/packages/core/drizzle/meta/0086_snapshot.json index 8c3e7054d..b6dc1913f 100644 --- a/packages/core/drizzle/meta/0086_snapshot.json +++ b/packages/core/drizzle/meta/0086_snapshot.json @@ -61,9 +61,7 @@ "users_email_unique": { "name": "users_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } } }, @@ -111,12 +109,8 @@ "tableFrom": "sessions", "tableTo": "users", "schemaTo": "latitude", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -174,12 +168,8 @@ "tableFrom": "workspaces", "tableTo": "subscriptions", "schemaTo": "latitude", - "columnsFrom": [ - "current_subscription_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["current_subscription_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -188,12 +178,8 @@ "tableFrom": "workspaces", "tableTo": "users", "schemaTo": "latitude", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -369,12 +355,8 @@ "tableFrom": "memberships", "tableTo": "workspaces", "schemaTo": "latitude", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -383,12 +365,8 @@ "tableFrom": "memberships", "tableTo": "users", "schemaTo": "latitude", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -398,9 +376,7 @@ "memberships_invitation_token_unique": { "name": "memberships_invitation_token_unique", "nullsNotDistinct": false, - "columns": [ - "invitation_token" - ] + "columns": ["invitation_token"] } } }, @@ -483,12 +459,8 @@ "tableFrom": "api_keys", "tableTo": "workspaces", "schemaTo": "latitude", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -498,9 +470,7 @@ "api_keys_token_unique": { "name": "api_keys_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } } }, @@ -589,12 +559,8 @@ "tableFrom": "claimed_rewards", "tableTo": "workspaces", "schemaTo": "latitude", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -603,12 +569,8 @@ "tableFrom": "claimed_rewards", "tableTo": "users", "schemaTo": "latitude", - "columnsFrom": [ - "creator_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -697,12 +659,8 @@ "tableFrom": "projects", "tableTo": "workspaces", "schemaTo": "latitude", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -894,12 +852,8 @@ "tableFrom": "commits", "tableTo": "projects", "schemaTo": "latitude", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -908,12 +862,8 @@ "tableFrom": "commits", "tableTo": "users", "schemaTo": "latitude", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -923,9 +873,7 @@ "commits_uuid_unique": { "name": "commits_uuid_unique", "nullsNotDistinct": false, - "columns": [ - "uuid" - ] + "columns": ["uuid"] } } }, @@ -1099,12 +1047,8 @@ "tableFrom": "document_versions", "tableTo": "commits", "schemaTo": "latitude", - "columnsFrom": [ - "commit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["commit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1284,12 +1228,8 @@ "tableFrom": "provider_api_keys", "tableTo": "users", "schemaTo": "latitude", - "columnsFrom": [ - "author_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["author_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1298,12 +1238,8 @@ "tableFrom": "provider_api_keys", "tableTo": "workspaces", "schemaTo": "latitude", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1313,21 +1249,12 @@ "provider_api_keys_name_workspace_id_deleted_at_unique": { "name": "provider_api_keys_name_workspace_id_deleted_at_unique", "nullsNotDistinct": true, - "columns": [ - "name", - "workspace_id", - "deleted_at" - ] + "columns": ["name", "workspace_id", "deleted_at"] }, "provider_api_keys_token_provider_workspace_id_deleted_at_unique": { "name": "provider_api_keys_token_provider_workspace_id_deleted_at_unique", "nullsNotDistinct": true, - "columns": [ - "token", - "provider", - "workspace_id", - "deleted_at" - ] + "columns": ["token", "provider", "workspace_id", "deleted_at"] } } }, @@ -1494,12 +1421,8 @@ "tableFrom": "document_logs", "tableTo": "commits", "schemaTo": "latitude", - "columnsFrom": [ - "commit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["commit_id"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" } @@ -1509,9 +1432,7 @@ "document_logs_uuid_unique": { "name": "document_logs_uuid_unique", "nullsNotDistinct": false, - "columns": [ - "uuid" - ] + "columns": ["uuid"] } } }, @@ -1783,12 +1704,8 @@ "tableFrom": "provider_logs", "tableTo": "provider_api_keys", "schemaTo": "latitude", - "columnsFrom": [ - "provider_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["provider_id"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" }, @@ -1797,12 +1714,8 @@ "tableFrom": "provider_logs", "tableTo": "api_keys", "schemaTo": "latitude", - "columnsFrom": [ - "apiKeyId" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["apiKeyId"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" } @@ -1812,9 +1725,7 @@ "provider_logs_uuid_unique": { "name": "provider_logs_uuid_unique", "nullsNotDistinct": false, - "columns": [ - "uuid" - ] + "columns": ["uuid"] } } }, @@ -1938,12 +1849,8 @@ "tableFrom": "datasets", "tableTo": "workspaces", "schemaTo": "latitude", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1952,12 +1859,8 @@ "tableFrom": "datasets", "tableTo": "users", "schemaTo": "latitude", - "columnsFrom": [ - "author_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["author_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -2099,12 +2002,8 @@ "tableFrom": "evaluations", "tableTo": "workspaces", "schemaTo": "latitude", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2114,9 +2013,7 @@ "evaluations_uuid_unique": { "name": "evaluations_uuid_unique", "nullsNotDistinct": false, - "columns": [ - "uuid" - ] + "columns": ["uuid"] } } }, @@ -2186,12 +2083,8 @@ "tableFrom": "llm_as_judge_evaluation_metadatas", "tableTo": "evaluations_templates", "schemaTo": "latitude", - "columnsFrom": [ - "template_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["template_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2272,12 +2165,8 @@ "tableFrom": "connected_evaluations", "tableTo": "evaluations", "schemaTo": "latitude", - "columnsFrom": [ - "evaluation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["evaluation_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2287,10 +2176,7 @@ "connected_evaluations_unique_idx": { "name": "connected_evaluations_unique_idx", "nullsNotDistinct": false, - "columns": [ - "document_uuid", - "evaluation_id" - ] + "columns": ["document_uuid", "evaluation_id"] } } }, @@ -2452,12 +2338,8 @@ "tableFrom": "evaluation_results", "tableTo": "evaluations", "schemaTo": "latitude", - "columnsFrom": [ - "evaluation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["evaluation_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2466,12 +2348,8 @@ "tableFrom": "evaluation_results", "tableTo": "document_logs", "schemaTo": "latitude", - "columnsFrom": [ - "document_log_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["document_log_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2480,12 +2358,8 @@ "tableFrom": "evaluation_results", "tableTo": "provider_logs", "schemaTo": "latitude", - "columnsFrom": [ - "provider_log_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["provider_log_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2495,9 +2369,7 @@ "evaluation_results_uuid_unique": { "name": "evaluation_results_uuid_unique", "nullsNotDistinct": false, - "columns": [ - "uuid" - ] + "columns": ["uuid"] } } }, @@ -2563,12 +2435,8 @@ "tableFrom": "evaluations_templates", "tableTo": "evaluations_template_categories", "schemaTo": "latitude", - "columnsFrom": [ - "category" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["category"], + "columnsTo": ["id"], "onDelete": "restrict", "onUpdate": "cascade" } @@ -2663,12 +2531,8 @@ "tableFrom": "magic_link_tokens", "tableTo": "users", "schemaTo": "latitude", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2678,9 +2542,7 @@ "magic_link_tokens_token_unique": { "name": "magic_link_tokens_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } } }, @@ -2765,12 +2627,8 @@ "tableFrom": "events", "tableTo": "workspaces", "schemaTo": "latitude", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2891,12 +2749,7 @@ "latitude.subscription_plans": { "name": "subscription_plans", "schema": "latitude", - "values": [ - "hobby_v1", - "hobby_v2", - "team_v1", - "enterprise_v1" - ] + "values": ["hobby_v1", "hobby_v2", "team_v1", "enterprise_v1"] }, "latitude.reward_types": { "name": "reward_types", @@ -2945,26 +2798,17 @@ "latitude.run_error_entity_enum": { "name": "run_error_entity_enum", "schema": "latitude", - "values": [ - "document_log", - "evaluation_result" - ] + "values": ["document_log", "evaluation_result"] }, "latitude.log_source": { "name": "log_source", "schema": "latitude", - "values": [ - "playground", - "api", - "evaluation" - ] + "values": ["playground", "api", "evaluation"] }, "latitude.metadata_type": { "name": "metadata_type", "schema": "latitude", - "values": [ - "llm_as_judge" - ] + "values": ["llm_as_judge"] }, "public.evaluation_result_types": { "name": "evaluation_result_types", @@ -2984,4 +2828,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/core/drizzle/meta/_journal.json b/packages/core/drizzle/meta/_journal.json index c8e60fda6..89c1ec879 100644 --- a/packages/core/drizzle/meta/_journal.json +++ b/packages/core/drizzle/meta/_journal.json @@ -619,4 +619,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index b8811524d..29bb73ec5 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -1,9 +1,9 @@ -import type { - AssistantMessage, - Message as CompilerMessage, - SystemMessage, - ToolCall, - UserMessage, +import { + type AssistantMessage, + type Message as CompilerMessage, + type SystemMessage, + type ToolCall, + type UserMessage, } from '@latitude-data/compiler' import { CoreTool, @@ -11,6 +11,7 @@ import { ObjectStreamPart, TextStreamPart, } from 'ai' +import { z } from 'zod' import { ProviderLog } from './browser' import { Config } from './services/ai' @@ -78,6 +79,7 @@ export enum LogSources { API = 'api', Playground = 'playground', Evaluation = 'evaluation', + User = 'user', } export enum ErrorableEntity { @@ -264,8 +266,8 @@ export type SerializedProviderLog = { messages: SerializedConversation context: string response: string | null - config: object - duration: number + config: object | null + duration: number | null cost: number } @@ -296,3 +298,88 @@ export type SerializedEvaluationResult = Omit< } export const ULTRA_LARGE_PAGE_SIZE = 1000 +export const DELIMITER_VALUES = { + comma: ',', + semicolon: ';', + tab: '\t', + space: ' ', +} +export const DELIMITERS_KEYS = [ + 'comma', + 'semicolon', + 'tab', + 'space', + 'custom', +] as const +export const MAX_SIZE = 15 +export const MAX_UPLOAD_SIZE_IN_MB = MAX_SIZE * 1024 * 1024 + +const userContentSchema = z.array( + z + .object({ + type: z.literal('text'), + text: z.string(), + }) + .or( + z.object({ + type: z.literal('image'), + image: z + .string() + .or(z.instanceof(Uint8Array)) + .or(z.instanceof(Buffer)) + .or(z.instanceof(ArrayBuffer)) + .or(z.instanceof(URL)), + }), + ), +) + +export const messageSchema = z + .object({ + role: z.literal('system'), + content: z.string(), + }) + .or( + z.object({ + role: z.literal('user'), + name: z.string().optional(), + content: userContentSchema, + }), + ) + .or( + z.object({ + role: z.literal('assistant'), + content: z.string().or( + z.array( + z.object({ + type: z.literal('tool-call'), + toolCallId: z.string(), + toolName: z.string(), + args: z.record(z.any()), + }), + ), + ), + toolCalls: z.array( + z.object({ + id: z.string(), + name: z.string(), + arguments: z.record(z.any()), + }), + ), + }), + ) + .or( + z.object({ + role: z.literal('tool'), + content: z.array( + z.object({ + type: z.literal('tool-result'), + toolCallId: z.string(), + toolName: z.string(), + result: z.string(), + isError: z.boolean().optional(), + }), + ), + }), + ) + +export const messagesSchema = z.array(z.any(messageSchema)) diff --git a/packages/core/src/jobs/constants.ts b/packages/core/src/jobs/constants.ts index 2d4f2e8ef..e2bd656db 100644 --- a/packages/core/src/jobs/constants.ts +++ b/packages/core/src/jobs/constants.ts @@ -19,6 +19,7 @@ export enum Jobs { runEvaluationJob = 'runEvaluationJob', publishToAnalyticsJob = 'publishToAnalyticsJob', runLiveEvaluationJob = 'runLiveEvaluationJob', + uploadDocumentLogsJob = 'uploadDocumentLogsJob', } export const QUEUES = { @@ -31,6 +32,8 @@ export const QUEUES = { 'runDocumentInBatchJob', 'runDocumentJob', 'runEvaluationJob', + 'uploadDocumentLogsJob', + 'createDocumentLogJob', ], }, [Queues.eventsQueue]: { diff --git a/packages/core/src/jobs/job-definitions/documentLogs/createDocumentLogJob.ts b/packages/core/src/jobs/job-definitions/documentLogs/createDocumentLogJob.ts new file mode 100644 index 000000000..1ca954c37 --- /dev/null +++ b/packages/core/src/jobs/job-definitions/documentLogs/createDocumentLogJob.ts @@ -0,0 +1,46 @@ +import { Message } from '@latitude-data/compiler' +import { Job } from 'bullmq' + +import { Commit } from '../../../browser' +import { LogSources } from '../../../constants' +import { generateUUIDIdentifier } from '../../../lib' +import { DocumentVersionsRepository } from '../../../repositories' +import { createDocumentLog } from '../../../services/documentLogs' + +export type CreateDocumentLogJobProps = { + workspaceId: number + documentUuid: string + commit: Commit + source: LogSources + messages: Message[] + responseText: string +} + +export const createDocumentLogJob = async ( + job: Job, +) => { + const { workspaceId, documentUuid, commit, source, messages, responseText } = + job.data + const docsRepo = new DocumentVersionsRepository(workspaceId) + const document = await docsRepo + .getDocumentByUuid({ + documentUuid, + commit, + }) + .then((r) => r.unwrap()) + + await createDocumentLog({ + data: { + uuid: generateUUIDIdentifier(), + documentUuid: document.documentUuid, + resolvedContent: document.resolvedContent || '', + source, + parameters: {}, + providerLog: { + messages, + responseText, + }, + }, + commit, + }).then((r) => r.unwrap()) +} diff --git a/packages/core/src/jobs/job-definitions/documentLogs/uploadDocumentLogsJob.ts b/packages/core/src/jobs/job-definitions/documentLogs/uploadDocumentLogsJob.ts new file mode 100644 index 000000000..2e9906366 --- /dev/null +++ b/packages/core/src/jobs/job-definitions/documentLogs/uploadDocumentLogsJob.ts @@ -0,0 +1,42 @@ +import { Job } from 'bullmq' + +import { setupJobs } from '../..' +import { Commit } from '../../../browser' +import { LogSources, messagesSchema } from '../../../constants' + +export type UploadDocumentLogsJobData = { + workspaceId: number + documentUuid: string + commit: Commit + source: LogSources + csv: { data: { record: string[] }[] } +} + +export const uploadDocumentLogsJob = async ( + job: Job, +) => { + const { workspaceId, source, documentUuid, commit, csv } = job.data + const jobs = await setupJobs() + + csv.data.forEach((row) => { + const messages = JSON.parse(row.record[0]!) + const response = row.record[1] + const result = messagesSchema.safeParse(messages) + if (!result.success) { + // TODO: notify client of invalid log format + return + } + + jobs.defaultQueue.jobs.enqueueCreateDocumentLogJob({ + workspaceId, + documentUuid, + commit, + source, + messages: response ? messages : messages.slice(0, -1), + responseText: + response ?? + (messages[messages.length - 1].content?.text || + messages[messages.length - 1].content), + }) + }) +} diff --git a/packages/core/src/jobs/job-definitions/index.ts b/packages/core/src/jobs/job-definitions/index.ts index 037c67bbc..4dcbc7f1d 100644 --- a/packages/core/src/jobs/job-definitions/index.ts +++ b/packages/core/src/jobs/job-definitions/index.ts @@ -9,3 +9,5 @@ export * from '../../events/handlers/sendMagicLinkHandler' export * from '../../events/handlers/sendInvitationToUser' export * from '../../events/handlers/sendReferralInvitation' export * from '../../events/handlers/createLoopsContact' +export * from './documentLogs/uploadDocumentLogsJob' +export * from './documentLogs/createDocumentLogJob' diff --git a/packages/core/src/jobs/job-definitions/types.d.ts b/packages/core/src/jobs/job-definitions/types.d.ts index ee5a3a8dc..64465e5ba 100644 --- a/packages/core/src/jobs/job-definitions/types.d.ts +++ b/packages/core/src/jobs/job-definitions/types.d.ts @@ -9,6 +9,7 @@ import type { RunBatchEvaluationJobParams } from './batchEvaluations/runBatchEva import type { RunEvaluationJobData } from './batchEvaluations/runEvaluationJob' import type { RunDocumentInBatchJobProps } from './documents/runDocumentInBatchJob' import type { RunDocumentJobData } from './documents/runDocumentJob' +import type { UploadDocumentLogsJobData } from './documents/uploadDocumentLogsJob' import type { RunLiveEvaluationJobData } from './liveEvaluations/runLiveEvaluationJob' export type JobDataMap = { @@ -23,6 +24,7 @@ export type JobDataMap = { [Jobs.runEvaluationJob]: RunEvaluationJobData [Jobs.publishToAnalyticsJob]: LatitudeEvent [Jobs.runLiveEvaluationJob]: RunLiveEvaluationJobData + [Jobs.uploadDocumentLogsJob]: UploadDocumentLogsJobData } type JobData = J extends keyof JobDataMap diff --git a/packages/core/src/lib/readCsv.ts b/packages/core/src/lib/readCsv.ts index bcafbd424..93bc223bf 100644 --- a/packages/core/src/lib/readCsv.ts +++ b/packages/core/src/lib/readCsv.ts @@ -16,6 +16,7 @@ type ParseCsvOptions = { // https://csv.js.org/parse/options/to_line/ toLine?: number fromLine?: number + columns?: boolean } type ParseResult = { record: Record @@ -28,7 +29,7 @@ export type CsvParsedData = { } export async function syncReadCsv( file: File | string, - { delimiter, toLine, fromLine }: ParseCsvOptions, + { delimiter, toLine, fromLine, columns = true }: ParseCsvOptions, ) { try { const data = await getData(file) @@ -37,7 +38,7 @@ export async function syncReadCsv( relax_column_count: true, skip_empty_lines: true, relax_quotes: true, - columns: true, + columns, trim: true, info: true, } @@ -58,7 +59,8 @@ export async function syncReadCsv( return Result.ok({ headers: [], rowCount: 0, data: [] }) const firstRecord = records[0]! - const headers = firstRecord.info.columns.map((column) => column.name) + const headers = + firstRecord.info?.columns?.map?.((column) => column.name) ?? [] return Result.ok({ rowCount: records.length, headers, data: records }) } catch (e) { const error = e as CsvError diff --git a/packages/core/src/repositories/connectedEvaluationsRepository/getConnectedDocumentsWithMetadata.test.ts b/packages/core/src/repositories/connectedEvaluationsRepository/getConnectedDocumentsWithMetadata.test.ts index 9593dffef..424e87575 100644 --- a/packages/core/src/repositories/connectedEvaluationsRepository/getConnectedDocumentsWithMetadata.test.ts +++ b/packages/core/src/repositories/connectedEvaluationsRepository/getConnectedDocumentsWithMetadata.test.ts @@ -231,7 +231,8 @@ describe('getConnectedDocumentsWithMetadata', () => { ) const totalTokens = results.reduce( - (acc, r) => acc + r.providerLogs.reduce((acc2, l) => acc2 + l.tokens, 0), + (acc, r) => + acc + r.providerLogs.reduce((acc2, l) => acc2 + (l?.tokens ?? 0), 0), 0, ) const totalCost = results.reduce( diff --git a/packages/core/src/repositories/providerLogsRepository.ts b/packages/core/src/repositories/providerLogsRepository.ts index 97a8ded9a..2e79eac1e 100644 --- a/packages/core/src/repositories/providerLogsRepository.ts +++ b/packages/core/src/repositories/providerLogsRepository.ts @@ -29,10 +29,6 @@ export class ProviderLogsRepository extends Repository { async findByDocumentUuid(documentUuid: string, opts: QueryOptions = {}) { const query = this.scope - .innerJoin( - documentLogs, - eq(documentLogs.uuid, providerLogs.documentLogUuid), - ) .where(eq(documentLogs.documentUuid, documentUuid)) .orderBy(asc(providerLogs.generatedAt)) diff --git a/packages/core/src/schema/messages.ts b/packages/core/src/schema/messages.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/core/src/schema/models/documentLogs.ts b/packages/core/src/schema/models/documentLogs.ts index e7db96f24..e5f97e7eb 100644 --- a/packages/core/src/schema/models/documentLogs.ts +++ b/packages/core/src/schema/models/documentLogs.ts @@ -28,7 +28,7 @@ export const documentLogs = latitudeSchema.table( contentHash: text('content_hash').notNull(), parameters: jsonb('parameters').$type>().notNull(), customIdentifier: text('custom_identifier'), - duration: bigint('duration', { mode: 'number' }).notNull(), + duration: bigint('duration', { mode: 'number' }), source: logSourcesEnum('source'), ...timestamps(), }, diff --git a/packages/core/src/schema/models/providerLogs.ts b/packages/core/src/schema/models/providerLogs.ts index 6137c87e8..c3f5d407b 100644 --- a/packages/core/src/schema/models/providerLogs.ts +++ b/packages/core/src/schema/models/providerLogs.ts @@ -23,6 +23,7 @@ export const logSourcesEnum = latitudeSchema.enum('log_source', [ LogSources.Playground, LogSources.API, LogSources.Evaluation, + LogSources.User, ]) export const providerLogs = latitudeSchema.table( @@ -32,22 +33,23 @@ export const providerLogs = latitudeSchema.table( workspaceId: bigint('workspace_id', { mode: 'number' }), uuid: uuid('uuid').notNull().unique(), documentLogUuid: uuid('document_log_uuid'), - providerId: bigint('provider_id', { mode: 'number' }) - .notNull() - .references(() => providerApiKeys.id, { + providerId: bigint('provider_id', { mode: 'number' }).references( + () => providerApiKeys.id, + { onDelete: 'restrict', onUpdate: 'cascade', - }), + }, + ), model: varchar('model'), - finishReason: varchar('finish_reason').notNull().default('stop'), - config: json('config').$type().notNull(), + finishReason: varchar('finish_reason').default('stop'), + config: json('config').$type(), messages: json('messages').$type().notNull(), responseObject: jsonb('response_object').$type(), responseText: text('response_text').$type(), toolCalls: json('tool_calls').$type().notNull().default([]), - tokens: bigint('tokens', { mode: 'number' }).notNull(), + tokens: bigint('tokens', { mode: 'number' }), costInMillicents: integer('cost_in_millicents').notNull().default(0), - duration: bigint('duration', { mode: 'number' }).notNull(), // in milliseconds! + duration: bigint('duration', { mode: 'number' }), // in milliseconds! source: logSourcesEnum('source').notNull(), apiKeyId: bigint('apiKeyId', { mode: 'number' }).references( () => apiKeys.id, @@ -56,7 +58,7 @@ export const providerLogs = latitudeSchema.table( onUpdate: 'cascade', }, ), - generatedAt: timestamp('generated_at', { mode: 'date' }).notNull(), + generatedAt: timestamp('generated_at', { mode: 'date' }), ...timestamps(), }, (table) => ({ diff --git a/packages/core/src/services/documentLogs/bulkUpload.ts b/packages/core/src/services/documentLogs/bulkUpload.ts new file mode 100644 index 000000000..380155dc7 --- /dev/null +++ b/packages/core/src/services/documentLogs/bulkUpload.ts @@ -0,0 +1,32 @@ +import { Commit, DocumentVersion, Workspace } from '../../browser' +import { LogSources } from '../../constants' +import { setupJobs } from '../../jobs' +import { syncReadCsv } from '../../lib/readCsv' + +export async function bulkUploadDocumentLogs({ + csvDelimiter, + logsFile, + workspace, + document, + commit, +}: { + csvDelimiter: string + logsFile: File + workspace: Workspace + document: DocumentVersion + commit: Commit +}) { + const csv = await syncReadCsv(logsFile, { + delimiter: csvDelimiter, + columns: false, + }).then((r) => r.unwrap()) + const queues = await setupJobs() + + queues.defaultQueue.jobs.enqueueUploadDocumentLogsJob({ + workspaceId: workspace.id, + documentUuid: document.documentUuid, + commit, + csv, + source: LogSources.User, + }) +} diff --git a/packages/core/src/services/documentLogs/computeDocumentLogWithMetadata.test.ts b/packages/core/src/services/documentLogs/computeDocumentLogWithMetadata.test.ts index f28ca1c4f..8b4522de8 100644 --- a/packages/core/src/services/documentLogs/computeDocumentLogWithMetadata.test.ts +++ b/packages/core/src/services/documentLogs/computeDocumentLogWithMetadata.test.ts @@ -68,8 +68,14 @@ describe('computeDocumentLogWithMetadata', () => { id: documentLog.id, uuid: documentLog.uuid, documentUuid: documentLog.documentUuid, - tokens: totalProviderLogs.reduce((acc, log) => acc + log.tokens, 0), - duration: totalProviderLogs.reduce((acc, log) => acc + log.duration, 0), + tokens: totalProviderLogs.reduce( + (acc, log) => acc + (log?.tokens ?? 0), + 0, + ), + duration: totalProviderLogs.reduce( + (acc, log) => acc + (log?.duration ?? 0), + 0, + ), costInMillicents: totalProviderLogs.reduce( (acc, log) => acc + log.costInMillicents, 0, diff --git a/packages/core/src/services/documentLogs/create.ts b/packages/core/src/services/documentLogs/create.ts index 67a85d5fc..8bd23abce 100644 --- a/packages/core/src/services/documentLogs/create.ts +++ b/packages/core/src/services/documentLogs/create.ts @@ -1,8 +1,18 @@ +import { Message } from '@latitude-data/compiler' + import { Commit, DocumentLog, LogSources } from '../../browser' import { database } from '../../client' +import { findWorkspaceFromCommit } from '../../data-access' import { publisher } from '../../events/publisher' -import { hashContent, Result, Transaction } from '../../lib' +import { + generateUUIDIdentifier, + hashContent, + NotFoundError, + Result, + Transaction, +} from '../../lib' import { documentLogs } from '../../schema' +import { createProviderLog } from '../providerLogs' export type CreateDocumentLogProps = { commit: Commit @@ -11,10 +21,14 @@ export type CreateDocumentLogProps = { documentUuid: string parameters: Record resolvedContent: string - duration: number + duration?: number source: LogSources customIdentifier?: string createdAt?: Date + providerLog?: { + messages: Message[] + responseText?: string + } } } @@ -29,6 +43,7 @@ export async function createDocumentLog( duration, source, createdAt, + providerLog, }, commit, }: CreateDocumentLogProps, @@ -52,6 +67,22 @@ export async function createDocumentLog( .returning() const documentLog = inserts[0]! + if (providerLog) { + const workspace = await findWorkspaceFromCommit(commit, trx) + if (!workspace) { + throw new NotFoundError('Workspace not found') + } + + await createProviderLog({ + uuid: generateUUIDIdentifier(), + documentLogUuid: documentLog.uuid, + messages: providerLog.messages, + responseText: providerLog.responseText, + generatedAt: new Date(), + source, + workspace, + }).then((r) => r.unwrap()) + } publisher.publishLater({ type: 'documentLogCreated', diff --git a/packages/core/src/services/documentLogs/index.ts b/packages/core/src/services/documentLogs/index.ts index b90681ae4..b3db43969 100644 --- a/packages/core/src/services/documentLogs/index.ts +++ b/packages/core/src/services/documentLogs/index.ts @@ -3,3 +3,4 @@ export * from './addMessages' export * from './computeDocumentLogWithMetadata' export * from './computeDocumentLogsWithMetadata' export * from './fetchDocumentLogWithMetadata' +export * from './bulkUpload' diff --git a/packages/core/src/services/evaluations/run/handleEvaluationResponse.ts b/packages/core/src/services/evaluations/run/handleEvaluationResponse.ts index b20539482..6ba618141 100644 --- a/packages/core/src/services/evaluations/run/handleEvaluationResponse.ts +++ b/packages/core/src/services/evaluations/run/handleEvaluationResponse.ts @@ -82,7 +82,7 @@ export async function handleEvaluationResponse({ if (!response.object) { error = new ChainError({ code: RunErrorCodes.EvaluationRunResponseJsonFormatError, - message: `Provider with model [${providerLog.config.model}] did not return a valid JSON object`, + message: `Provider with model [${providerLog?.config?.model ?? 'unknown'}] did not return a valid JSON object`, }) } else { result = response.object diff --git a/packages/core/src/services/providerLogs/addMessages.ts b/packages/core/src/services/providerLogs/addMessages.ts index 563fe13be..2df31f1ae 100644 --- a/packages/core/src/services/providerLogs/addMessages.ts +++ b/packages/core/src/services/providerLogs/addMessages.ts @@ -38,6 +38,21 @@ export async function addMessages({ messages: Message[] source: LogSources }) { + if (!providerLog.providerId) { + return Result.error( + new NotFoundError( + `Cannot add messages to a conversation that has no associated provider`, + ), + ) + } + if (!providerLog.config) { + return Result.error( + new NotFoundError( + `Cannot add messages to a conversation that has no associated configuration`, + ), + ) + } + const provider = await unsafelyFindProviderApiKey(providerLog.providerId) if (!provider) { return Result.error( @@ -78,7 +93,7 @@ export async function addMessages({ iterate({ workspace, source, - config: providerLog.config, + config: providerLog.config!, provider, controller, documentLogUuid: providerLog.documentLogUuid!, diff --git a/packages/core/src/services/providerLogs/create.ts b/packages/core/src/services/providerLogs/create.ts index 4b13c048d..925c6b1ee 100644 --- a/packages/core/src/services/providerLogs/create.ts +++ b/packages/core/src/services/providerLogs/create.ts @@ -17,13 +17,13 @@ export type CreateProviderLogProps = { workspace: Workspace uuid: string generatedAt: Date - providerId: number - providerType: Providers - model: string - config: PartialConfig + providerId?: number + providerType?: Providers + model?: string + config?: PartialConfig messages: Message[] - usage: LanguageModelUsage - duration: number + usage?: LanguageModelUsage + duration?: number source: LogSources finishReason?: StreamConsumeReturn['finishReason'] apiKeyId?: number @@ -60,10 +60,15 @@ export async function createProviderLog( return await Transaction.call(async (trx) => { const cost = costInMillicents ?? - Math.floor( - estimateCost({ provider: providerType, model, usage }) * - TO_MILLICENTS_FACTOR, - ) + (providerType && model && usage + ? Math.floor( + estimateCost({ + provider: providerType!, + model: model!, + usage: usage!, + }) * TO_MILLICENTS_FACTOR, + ) + : undefined) const inserts = await trx .insert(providerLogs) .values({ @@ -78,7 +83,11 @@ export async function createProviderLog( responseText, responseObject, toolCalls, - tokens: isNaN(usage.totalTokens) ? 0 : (usage.totalTokens ?? 0), + tokens: usage + ? isNaN(usage.totalTokens) + ? 0 + : (usage.totalTokens ?? 0) + : undefined, costInMillicents: cost, duration, source, @@ -88,8 +97,7 @@ export async function createProviderLog( .returning() const log = inserts[0]! as ProviderLog - await touchProviderApiKey(providerId, trx) - + if (providerId) await touchProviderApiKey(providerId, trx) if (apiKeyId) await touchApiKey(apiKeyId, trx) publisher.publishLater({ diff --git a/packages/core/src/tests/factories/documentLogs.ts b/packages/core/src/tests/factories/documentLogs.ts index 00f5e0d89..02a6e9cdc 100644 --- a/packages/core/src/tests/factories/documentLogs.ts +++ b/packages/core/src/tests/factories/documentLogs.ts @@ -120,7 +120,7 @@ export async function createDocumentLog({ const duration = Math.floor(Math.random() * 100) + - providerLogs.reduce((acc, log) => acc + log.duration, 0) + providerLogs.reduce((acc, log) => acc + (log?.duration ?? 0), 0) const documentLog = await ogCreateDocumentLog({ commit, diff --git a/packages/sdks/typescript/package.json b/packages/sdks/typescript/package.json index 299eb8bb9..624e781be 100644 --- a/packages/sdks/typescript/package.json +++ b/packages/sdks/typescript/package.json @@ -19,16 +19,24 @@ "prettier": "prettier --write \"**/*.{ts,tsx,md}\"" }, "type": "module", - "files": [ - "dist" - ], + "files": ["dist"], + "main": "./dist/index.cjs", + "module": "./dist/index.js", "exports": { ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.cjs" + } } }, + "types": { + "index.d.ts": "./dist/index.d.ts" + }, "dependencies": { "@t3-oss/env-core": "^0.10.1", "eventsource-parser": "^2.0.1", diff --git a/packages/sdks/typescript/src/index.ts b/packages/sdks/typescript/src/index.ts index 47ba6147f..31dfe8c9d 100644 --- a/packages/sdks/typescript/src/index.ts +++ b/packages/sdks/typescript/src/index.ts @@ -7,7 +7,11 @@ import { } from '@latitude-data/core/browser' import env from '$sdk/env' import { GatewayApiConfig, RouteResolver } from '$sdk/utils' -import { ApiErrorJsonResponse, LatitudeApiError } from '$sdk/utils/errors' +import { + ApiErrorCodes, + ApiErrorJsonResponse, + LatitudeApiError, +} from '$sdk/utils/errors' import { handleStream } from '$sdk/utils/handleStream' import { makeRequest } from '$sdk/utils/request' import { streamRun } from '$sdk/utils/streamRun' @@ -111,6 +115,45 @@ class Latitude { return syncRun(path, options) } + async log( + path: string, + messages: Message[], + { + response, + projectId, + versionUuid, + }: { + projectId?: number + versionUuid?: string + response?: string + } = {}, + ) { + projectId = projectId ?? this.options.projectId + if (!projectId) { + throw new Error('Project ID is required') + } + versionUuid = versionUuid ?? this.options.versionUuid + + const httpResponse = await makeRequest({ + method: 'POST', + handler: HandlerType.Log, + params: { projectId, versionUuid }, + body: { path, messages, response }, + options: this.options, + }) + + if (!httpResponse.ok) { + throw new LatitudeApiError({ + status: httpResponse.status, + message: httpResponse.statusText, + serverResponse: await httpResponse.text(), + errorCode: ApiErrorCodes.HTTPException, + }) + } + + return await httpResponse.json() + } + async chat( uuid: string, messages: Message[], diff --git a/packages/sdks/typescript/src/utils/errors.ts b/packages/sdks/typescript/src/utils/errors.ts index 5fc6dc5a8..cb2e9e2b2 100644 --- a/packages/sdks/typescript/src/utils/errors.ts +++ b/packages/sdks/typescript/src/utils/errors.ts @@ -16,7 +16,7 @@ function getErrorMessage({ message: string errorCode: ApiResponseCode }) { - const httpExeception = ApiErrorCodes.HTTPExeption + const httpExeception = ApiErrorCodes.HTTPException const internalServerError = ApiErrorCodes.InternalServerError const isUnexpectedError = errorCode === httpExeception || errorCode === internalServerError diff --git a/packages/sdks/typescript/src/utils/index.ts b/packages/sdks/typescript/src/utils/index.ts index 4e67203ff..6c5b58e9b 100644 --- a/packages/sdks/typescript/src/utils/index.ts +++ b/packages/sdks/typescript/src/utils/index.ts @@ -2,6 +2,7 @@ import { ChatUrlParams, GetDocumentUrlParams, HandlerType, + LogUrlParams, RunUrlParams, UrlParams, } from '$sdk/utils/types' @@ -46,6 +47,8 @@ export class RouteResolver { return this.conversations().chat( (params as ChatUrlParams).conversationUuid, ) + case HandlerType.Log: + return this.documents(params as LogUrlParams).log default: throw new Error(`Unknown handler: ${handler satisfies never}`) } @@ -63,6 +66,7 @@ export class RouteResolver { return { run: `${base}/run`, document: (path: string) => `${base}/${path}`, + log: `${base}/logs`, } } diff --git a/packages/sdks/typescript/src/utils/types.ts b/packages/sdks/typescript/src/utils/types.ts index d04e4ceb7..d0c6015bd 100644 --- a/packages/sdks/typescript/src/utils/types.ts +++ b/packages/sdks/typescript/src/utils/types.ts @@ -27,6 +27,12 @@ type RunDocumentBodyParam = { type ChatBodyParams = { messages: Message[] } +export type LogUrlParams = RunUrlParams +type LogBodyParams = { + path: string + messages: Message[] + response?: string +} export type GetDocumentUrlParams = { projectId: number @@ -38,6 +44,7 @@ export enum HandlerType { Chat = 'chat', GetDocument = 'get-document', RunDocument = 'run-document', + Log = 'log', } export type UrlParams = T extends HandlerType.RunDocument @@ -46,14 +53,18 @@ export type UrlParams = T extends HandlerType.RunDocument ? GetDocumentUrlParams : T extends HandlerType.Chat ? ChatUrlParams - : never + : T extends HandlerType.Log + ? LogUrlParams + : never export type BodyParams = T extends HandlerType.RunDocument ? RunDocumentBodyParam : T extends HandlerType.Chat ? ChatBodyParams - : never + : T extends HandlerType.Log + ? LogBodyParams + : never export type StreamChainResponse = { conversation: Message[] diff --git a/packages/web-ui/src/ds/atoms/DropzoneInput/index.tsx b/packages/web-ui/src/ds/atoms/DropzoneInput/index.tsx index 979171007..eb71a14d3 100644 --- a/packages/web-ui/src/ds/atoms/DropzoneInput/index.tsx +++ b/packages/web-ui/src/ds/atoms/DropzoneInput/index.tsx @@ -56,7 +56,7 @@ export function DropzoneInput({
) { +}) { if (typeof children !== 'string') return children - return ( -

- {children} -

- ) + return {children} } function TooltipMessage({ error }: { error: string | undefined }) { @@ -155,10 +148,7 @@ function FormField({ {children} - {description && ( - {description} - )} - + {description && {description}} {errorStyle === 'inline' ? ( ) : null} diff --git a/packages/web-ui/src/ds/atoms/FormFieldGroup/index.tsx b/packages/web-ui/src/ds/atoms/FormFieldGroup/index.tsx index 9ac4ad069..20ca1d054 100644 --- a/packages/web-ui/src/ds/atoms/FormFieldGroup/index.tsx +++ b/packages/web-ui/src/ds/atoms/FormFieldGroup/index.tsx @@ -34,11 +34,7 @@ export function FormFieldGroup({ > {children}
- {description ? ( - - {description} - - ) : null} + {description ? {description} : null} ) } diff --git a/packages/web-ui/src/ds/atoms/Switch/index.tsx b/packages/web-ui/src/ds/atoms/Switch/index.tsx index 241462c84..ab9fde748 100644 --- a/packages/web-ui/src/ds/atoms/Switch/index.tsx +++ b/packages/web-ui/src/ds/atoms/Switch/index.tsx @@ -96,9 +96,7 @@ function SwitchInput({ ) : null} - {description && ( - {description} - )} + {description && {description}} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44e71b3e9..853339f35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,7 +79,7 @@ importers: version: 0.11.1(typescript@5.6.3)(zod@3.23.8) ai: specifier: ^3.2.42 - version: 3.4.23(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.12(typescript@5.6.3))(zod@3.23.8) + version: 3.4.23(openai@4.68.4(encoding@0.1.13)(zod@3.23.8))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.12(typescript@5.6.3))(zod@3.23.8) argon2: specifier: ^0.41.0 version: 0.41.1 @@ -250,9 +250,6 @@ importers: hono: specifier: ^4.5.3 version: 4.6.7 - jet-paths: - specifier: ^1.0.6 - version: 1.0.9 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -353,7 +350,7 @@ importers: version: 0.10.1(typescript@5.6.3)(zod@3.23.8) ai: specifier: ^3.2.42 - version: 3.4.23(react@19.0.0-rc-5d19e1c8-20240923)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.12(typescript@5.6.3))(zod@3.23.8) + version: 3.4.23(openai@4.68.4(encoding@0.1.13)(zod@3.23.8))(react@19.0.0-rc-5d19e1c8-20240923)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.12(typescript@5.6.3))(zod@3.23.8) bullmq: specifier: ^5.8.5 version: 5.21.2 @@ -736,7 +733,7 @@ importers: version: 10.0.0 ai: specifier: ^3.4.7 - version: 3.4.23(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.12(typescript@5.6.3))(zod@3.23.8) + version: 3.4.23(openai@4.68.4(encoding@0.1.13)(zod@3.23.8))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.12(typescript@5.6.3))(zod@3.23.8) argon2: specifier: ^0.41.0 version: 0.41.1 @@ -5220,6 +5217,10 @@ packages: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -5255,6 +5256,10 @@ packages: resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} engines: {node: '>= 14'} + agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} + engines: {node: '>= 8.0.0'} + aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -6867,6 +6872,10 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -7047,6 +7056,9 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + form-data-encoder@2.1.4: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} engines: {node: '>= 14.17'} @@ -7059,6 +7071,10 @@ packages: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -7475,6 +7491,9 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -7825,9 +7844,6 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} - jet-paths@1.0.9: - resolution: {integrity: sha512-XxYKHYMwwhFr2azKPGXAV80RIdFwueJOilBTXH2ICwXSxD5vo6PUZsfufL+DVk+nZKf6Z2y+19kpwh8lJ81Gng==} - jiti@1.21.6: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true @@ -8955,6 +8971,15 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + openai@4.68.4: + resolution: {integrity: sha512-LRinV8iU9VQplkr25oZlyrsYGPGasIwYN8KFMAAFTHHLHjHhejtJ5BALuLFrkGzY4wfbKhOhuT+7lcHZ+F3iEA==} + hasBin: true + peerDependencies: + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} @@ -10956,6 +10981,10 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + web-vitals@4.2.4: resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} @@ -16501,6 +16530,11 @@ snapshots: abbrev@2.0.0: {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + optional: true + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -16534,6 +16568,11 @@ snapshots: transitivePeerDependencies: - supports-color + agentkeepalive@4.5.0: + dependencies: + humanize-ms: 1.2.1 + optional: true + aggregate-error@3.1.0: dependencies: clean-stack: 2.2.0 @@ -16544,7 +16583,7 @@ snapshots: clean-stack: 4.2.0 indent-string: 5.0.0 - ai@3.4.23(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.12(typescript@5.6.3))(zod@3.23.8): + ai@3.4.23(openai@4.68.4(encoding@0.1.13)(zod@3.23.8))(react@18.3.1)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.12(typescript@5.6.3))(zod@3.23.8): dependencies: '@ai-sdk/provider': 0.0.26 '@ai-sdk/provider-utils': 1.0.22(zod@3.23.8) @@ -16560,6 +16599,7 @@ snapshots: secure-json-parse: 2.7.0 zod-to-json-schema: 3.23.5(zod@3.23.8) optionalDependencies: + openai: 4.68.4(encoding@0.1.13)(zod@3.23.8) react: 18.3.1 sswr: 2.1.0(svelte@4.2.19) svelte: 4.2.19 @@ -16568,7 +16608,7 @@ snapshots: - solid-js - vue - ai@3.4.23(react@19.0.0-rc-5d19e1c8-20240923)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.12(typescript@5.6.3))(zod@3.23.8): + ai@3.4.23(openai@4.68.4(encoding@0.1.13)(zod@3.23.8))(react@19.0.0-rc-5d19e1c8-20240923)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.5.12(typescript@5.6.3))(zod@3.23.8): dependencies: '@ai-sdk/provider': 0.0.26 '@ai-sdk/provider-utils': 1.0.22(zod@3.23.8) @@ -16584,6 +16624,7 @@ snapshots: secure-json-parse: 2.7.0 zod-to-json-schema: 3.23.5(zod@3.23.8) optionalDependencies: + openai: 4.68.4(encoding@0.1.13)(zod@3.23.8) react: 19.0.0-rc-5d19e1c8-20240923 sswr: 2.1.0(svelte@4.2.19) svelte: 4.2.19 @@ -18253,6 +18294,9 @@ snapshots: etag@1.8.1: {} + event-target-shim@5.0.1: + optional: true + eventemitter3@4.0.7: {} events@1.1.1: {} @@ -18474,6 +18518,9 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 + form-data-encoder@1.7.2: + optional: true + form-data-encoder@2.1.4: {} form-data@4.0.1: @@ -18484,6 +18531,12 @@ snapshots: format@0.2.2: {} + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + optional: true + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -19110,6 +19163,11 @@ snapshots: human-signals@5.0.0: {} + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + optional: true + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -19426,8 +19484,6 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jet-paths@1.0.9: {} - jiti@1.21.6: {} jju@1.4.0: {} @@ -21191,6 +21247,21 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + openai@4.68.4(encoding@0.1.13)(zod@3.23.8): + dependencies: + '@types/node': 18.19.59 + '@types/node-fetch': 2.6.11 + abort-controller: 3.0.0 + agentkeepalive: 4.5.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0(encoding@0.1.13) + optionalDependencies: + zod: 3.23.8 + transitivePeerDependencies: + - encoding + optional: true + openapi-types@12.1.3: {} optionator@0.9.4: @@ -23886,6 +23957,9 @@ snapshots: web-streams-polyfill@3.3.3: {} + web-streams-polyfill@4.0.0-beta.3: + optional: true + web-vitals@4.2.4: {} webidl-conversions@3.0.1: {}