From 243e1121f25c1b5f72f1bd968ee3fc3e724ccefb Mon Sep 17 00:00:00 2001 From: Gerard Clos Date: Mon, 16 Sep 2024 15:25:37 +0200 Subject: [PATCH] feature: structured outputs Adds support for structured outputs responses from providers and implements it in evaluations and across our UI --- apps/web/src/actions/providerLogs/fetch.ts | 8 +- .../Editor/Playground/index.tsx | 2 +- .../editor/import-logs/page.tsx | 8 +- .../DocumentEditor/Editor/Playground/Chat.tsx | 18 +- .../EvaluationResultInfo/Messages.tsx | 6 +- .../EvaluationResultInfo/Metadata.tsx | 4 +- .../EvaluationResultInfo/index.tsx | 4 +- .../DocumentLogs/DocumentLogInfo/Messages.tsx | 6 +- .../DocumentLogs/DocumentLogInfo/Metadata.tsx | 4 +- .../DocumentLogs/DocumentLogInfo/index.tsx | 4 +- .../src/presenters/providerLogPresenter.ts | 16 + apps/web/src/stores/providerLogs.ts | 15 +- .../drizzle/0052_famous_daimon_hellstrom.sql | 3 + packages/core/drizzle/meta/0052_snapshot.json | 2194 +++++++++++++++++ packages/core/drizzle/meta/_journal.json | 7 + packages/core/package.json | 7 +- packages/core/src/browser.ts | 1 + packages/core/src/constants.ts | 32 +- .../handlers/createEvaluationResultJob.ts | 3 +- .../events/handlers/createProviderLogJob.ts | 15 +- packages/core/src/events/handlers/index.ts | 38 +- packages/core/src/helpers.ts | 7 + .../core/src/schema/models/providerLogs.ts | 5 +- packages/core/src/schema/types.ts | 13 +- packages/core/src/services/ai/helpers.ts | 96 + packages/core/src/services/ai/index.ts | 249 +- packages/core/src/services/chains/run.test.ts | 293 +++ packages/core/src/services/chains/run.ts | 264 +- .../documentLogs/addMessages/index.ts | 44 +- .../services/evaluationResults/create.test.ts | 23 +- .../src/services/evaluationResults/create.ts | 8 +- packages/core/src/services/evaluations/run.ts | 57 +- .../core/src/services/providerLogs/create.ts | 12 +- .../providerLogs/formatForEvaluation.ts | 30 +- .../src/tests/factories/evaluationResults.ts | 5 +- .../batchEvaluations/runEvaluationJob.ts | 4 +- packages/jobs/src/queues/index.ts | 8 +- packages/sdks/typescript/src/index.ts | 15 +- pnpm-lock.yaml | 6 + 39 files changed, 3145 insertions(+), 389 deletions(-) create mode 100644 apps/web/src/presenters/providerLogPresenter.ts create mode 100644 packages/core/drizzle/0052_famous_daimon_hellstrom.sql create mode 100644 packages/core/drizzle/meta/0052_snapshot.json create mode 100644 packages/core/src/helpers.ts create mode 100644 packages/core/src/services/ai/helpers.ts create mode 100644 packages/core/src/services/chains/run.test.ts diff --git a/apps/web/src/actions/providerLogs/fetch.ts b/apps/web/src/actions/providerLogs/fetch.ts index fa81a1beb..e8645f71a 100644 --- a/apps/web/src/actions/providerLogs/fetch.ts +++ b/apps/web/src/actions/providerLogs/fetch.ts @@ -1,6 +1,7 @@ 'use server' import { ProviderLogsRepository } from '@latitude-data/core/repositories' +import providerLogPresenter from '$/presenters/providerLogPresenter' import { z } from 'zod' import { authProcedure } from '../procedures' @@ -30,7 +31,7 @@ export const getProviderLogsAction = authProcedure result = await scope.findAll({ limit: 1000 }).then((r) => r.unwrap()) } - return result + return result.map(providerLogPresenter) }) export const getProviderLogAction = authProcedure @@ -39,5 +40,8 @@ export const getProviderLogAction = authProcedure .handler(async ({ input, ctx }) => { const { providerLogId } = input const scope = new ProviderLogsRepository(ctx.workspace.id) - return await scope.find(providerLogId).then((r) => r.unwrap()) + return await scope + .find(providerLogId) + .then((r) => r.unwrap()) + .then(providerLogPresenter) }) diff --git a/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/editor/_components/EvaluationEditor/Editor/Playground/index.tsx b/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/editor/_components/EvaluationEditor/Editor/Playground/index.tsx index fb3a1bf15..0a88fd8ab 100644 --- a/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/editor/_components/EvaluationEditor/Editor/Playground/index.tsx +++ b/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/editor/_components/EvaluationEditor/Editor/Playground/index.tsx @@ -50,7 +50,7 @@ export default function Playground({ setInputs({ messages: JSON.stringify(formatConversation(providerLog)), context: JSON.stringify(formatContext(providerLog)), - response: providerLog.responseText, + response: providerLog.response, prompt: '', parameters: '', config: '', diff --git a/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/editor/import-logs/page.tsx b/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/editor/import-logs/page.tsx index 10e03fbe1..95634cf28 100644 --- a/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/editor/import-logs/page.tsx +++ b/apps/web/src/app/(private)/evaluations/(evaluation)/[evaluationUuid]/editor/import-logs/page.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' import { capitalize } from 'lodash-es' import { MessageContent, TextContent } from '@latitude-data/compiler' -import { HEAD_COMMIT } from '@latitude-data/core/browser' +import { HEAD_COMMIT, ProviderLogDto } from '@latitude-data/core/browser' import { Badge, Button, @@ -193,7 +193,9 @@ const ProviderLogMessages = ({ providerLogUuid?: string }) => { const { data } = useProviderLogs({ documentUuid }) - const providerLog = data?.find((log) => log.uuid === providerLogUuid) + const providerLog = data?.find( + (log) => log.uuid === providerLogUuid, + ) as ProviderLogDto if (!providerLog) { return (
@@ -225,7 +227,7 @@ const ProviderLogMessages = ({ Assistant
- {printMessageContent(providerLog.responseText)} + {printMessageContent(providerLog.response)}
diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/Playground/Chat.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/Playground/Chat.tsx index 34c0fc149..cd3ec4c7f 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/Playground/Chat.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/Playground/Chat.tsx @@ -1,6 +1,7 @@ import { useCallback, useContext, useEffect, useRef, useState } from 'react' import { + AssistantMessage, ContentType, Conversation, Message as ConversationMessage, @@ -95,8 +96,8 @@ export default function Chat({ const { event, data } = serverEvent const hasMessages = 'messages' in data if (hasMessages) { - data.messages.forEach(addMessageToConversation) - messagesCount += data.messages.length + data.messages!.forEach(addMessageToConversation) + messagesCount += data.messages!.length } switch (event) { @@ -109,6 +110,7 @@ export default function Chat({ } else if (data.type === ChainEventTypes.Error) { setError(new Error(data.error.message)) } + break } @@ -161,17 +163,17 @@ export default function Chat({ const { event, data } = serverEvent - const hasMessages = 'messages' in data - - if (hasMessages) { - data.messages.forEach(addMessageToConversation) - } - switch (event) { case StreamEventTypes.Latitude: { if (data.type === ChainEventTypes.Error) { setError(new Error(data.error.message)) + } else if (data.type === ChainEventTypes.Complete) { + addMessageToConversation({ + role: MessageRole.assistant, + content: data.response.text, + } as AssistantMessage) } + break } diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultInfo/Messages.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultInfo/Messages.tsx index cd28fe705..3d76def6c 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultInfo/Messages.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultInfo/Messages.tsx @@ -1,20 +1,20 @@ import { useMemo } from 'react' import { AssistantMessage, Message, MessageRole } from '@latitude-data/compiler' -import { ProviderLog } from '@latitude-data/core/browser' +import { ProviderLogDto } from '@latitude-data/core/browser' import { MessageList } from '@latitude-data/web-ui' export function EvaluationResultMessages({ providerLog, }: { - providerLog?: ProviderLog + providerLog?: ProviderLogDto }) { const messages = useMemo(() => { if (!providerLog) return [] as Message[] const responseMessage = { role: MessageRole.assistant, - content: providerLog.responseText, + content: providerLog.response, toolCalls: providerLog.toolCalls, } as AssistantMessage diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultInfo/Metadata.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultInfo/Metadata.tsx index a27034b27..0b8f29bec 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultInfo/Metadata.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultInfo/Metadata.tsx @@ -1,6 +1,6 @@ import { ReactNode } from 'react' -import { ProviderLog } from '@latitude-data/core/browser' +import { ProviderLogDto } from '@latitude-data/core/browser' import { EvaluationResultWithMetadata } from '@latitude-data/core/repositories' import { ClickToCopy, @@ -48,7 +48,7 @@ export function EvaluationResultMetadata({ providerLog, }: { evaluationResult: EvaluationResultWithMetadata - providerLog?: ProviderLog + providerLog?: ProviderLogDto }) { const { data: providers, isLoading: providersLoading } = useProviderApiKeys() diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultInfo/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultInfo/index.tsx index 3f9d98873..0dfa5b007 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultInfo/index.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/evaluations/[evaluationId]/_components/EvaluationResults/EvaluationResultInfo/index.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' -import { ProviderLog } from '@latitude-data/core/browser' +import { ProviderLogDto } from '@latitude-data/core/browser' import { EvaluationResultWithMetadata } from '@latitude-data/core/repositories' import { TabSelector } from '@latitude-data/web-ui' @@ -14,7 +14,7 @@ export function EvaluationResultInfo({ providerLog, }: { evaluationResult: EvaluationResultWithMetadata - providerLog?: ProviderLog + providerLog?: ProviderLogDto }) { const [selectedTab, setSelectedTab] = useState('metadata') return ( diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/Messages.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/Messages.tsx index 899844a56..02ea269fb 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/Messages.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/Messages.tsx @@ -1,13 +1,13 @@ import { useMemo } from 'react' import { AssistantMessage, Message, MessageRole } from '@latitude-data/compiler' -import { ProviderLog } from '@latitude-data/core/browser' +import { ProviderLogDto } from '@latitude-data/core/browser' import { MessageList } from '@latitude-data/web-ui' export function DocumentLogMessages({ providerLogs, }: { - providerLogs?: ProviderLog[] + providerLogs?: ProviderLogDto[] }) { const messages = useMemo(() => { const lastLog = providerLogs?.[providerLogs.length - 1] @@ -15,7 +15,7 @@ export function DocumentLogMessages({ const responseMessage = { role: MessageRole.assistant, - content: lastLog.responseText, + content: lastLog.response, toolCalls: lastLog.toolCalls, } as AssistantMessage diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/Metadata.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/Metadata.tsx index 070786779..f439d6c50 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/Metadata.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/Metadata.tsx @@ -1,6 +1,6 @@ import { ReactNode, useMemo } from 'react' -import { ProviderLog } from '@latitude-data/core/browser' +import { ProviderLogDto } from '@latitude-data/core/browser' import { DocumentLogWithMetadata } from '@latitude-data/core/repositories' import { ClickToCopy, @@ -48,7 +48,7 @@ export function DocumentLogMetadata({ providerLogs, }: { documentLog: DocumentLogWithMetadata - providerLogs?: ProviderLog[] + providerLogs?: ProviderLogDto[] }) { const { data: providers, isLoading: providersLoading } = useProviderApiKeys() const lastProviderLog = useMemo( diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/index.tsx index 5e8380a5f..8a5eb676b 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/index.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/logs/_components/DocumentLogs/DocumentLogInfo/index.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' -import { ProviderLog } from '@latitude-data/core/browser' +import { ProviderLogDto } from '@latitude-data/core/browser' import { DocumentLogWithMetadata } from '@latitude-data/core/repositories' import { TabSelector } from '@latitude-data/web-ui' @@ -14,7 +14,7 @@ export function DocumentLogInfo({ providerLogs, }: { documentLog: DocumentLogWithMetadata - providerLogs?: ProviderLog[] + providerLogs?: ProviderLogDto[] }) { const [selectedTab, setSelectedTab] = useState('metadata') return ( diff --git a/apps/web/src/presenters/providerLogPresenter.ts b/apps/web/src/presenters/providerLogPresenter.ts new file mode 100644 index 000000000..1e7ebaaf4 --- /dev/null +++ b/apps/web/src/presenters/providerLogPresenter.ts @@ -0,0 +1,16 @@ +import { omit } from 'lodash-es' + +import type { ProviderLog, ProviderLogDto } from '@latitude-data/core/browser' + +export default function providerLogPresenter( + providerLog: ProviderLog, +): ProviderLogDto { + return { + ...omit(providerLog, 'responseText', 'responseObject'), + response: + providerLog.responseText || + (providerLog.responseObject + ? JSON.stringify(providerLog.responseObject) + : ''), + } +} diff --git a/apps/web/src/stores/providerLogs.ts b/apps/web/src/stores/providerLogs.ts index dda476c97..d835a2277 100644 --- a/apps/web/src/stores/providerLogs.ts +++ b/apps/web/src/stores/providerLogs.ts @@ -2,7 +2,7 @@ import { compact } from 'lodash-es' -import type { ProviderLog } from '@latitude-data/core/browser' +import { ProviderLogDto } from '@latitude-data/core/browser' import { useToast } from '@latitude-data/web-ui' import { getProviderLogAction, @@ -19,10 +19,10 @@ export default function useProviderLogs( ) { const { toast } = useToast() const { - data = undefined, + data = [], isLoading, error: swrError, - } = useSWR( + } = useSWR( compact(['providerLogs', documentUuid, documentLogUuid]), async () => { const [data, error] = await getProviderLogsAction({ @@ -38,10 +38,11 @@ export default function useProviderLogs( description: error.formErrors?.[0] || error.message, variant: 'destructive', }) - return undefined + + return [] } - return data as ProviderLog[] + return data }, opts, ) @@ -62,7 +63,7 @@ export function useProviderLog( data = undefined, isLoading, error: swrError, - } = useSWR( + } = useSWR( compact(['providerLog', providerLogId]), async () => { if (!providerLogId) return undefined @@ -82,7 +83,7 @@ export function useProviderLog( return undefined } - return data as ProviderLog + return data }, opts, ) diff --git a/packages/core/drizzle/0052_famous_daimon_hellstrom.sql b/packages/core/drizzle/0052_famous_daimon_hellstrom.sql new file mode 100644 index 000000000..891e4c380 --- /dev/null +++ b/packages/core/drizzle/0052_famous_daimon_hellstrom.sql @@ -0,0 +1,3 @@ +ALTER TABLE "latitude"."provider_logs" ALTER COLUMN "response_text" DROP DEFAULT;--> statement-breakpoint +ALTER TABLE "latitude"."provider_logs" ALTER COLUMN "response_text" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "latitude"."provider_logs" ADD COLUMN "response_object" jsonb; \ No newline at end of file diff --git a/packages/core/drizzle/meta/0052_snapshot.json b/packages/core/drizzle/meta/0052_snapshot.json new file mode 100644 index 000000000..7760ae01f --- /dev/null +++ b/packages/core/drizzle/meta/0052_snapshot.json @@ -0,0 +1,2194 @@ +{ + "id": "9b5a6c7c-2af0-4e63-b502-4f7ae011066d", + "prevId": "eb7ce322-fe62-4bd3-b181-5386c6db6c01", + "version": "7", + "dialect": "postgresql", + "tables": { + "latitude.users": { + "name": "users", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "latitude.sessions": { + "name": "sessions", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "latitude", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.workspaces": { + "name": "workspaces", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspaces_creator_id_users_id_fk": { + "name": "workspaces_creator_id_users_id_fk", + "tableFrom": "workspaces", + "tableTo": "users", + "schemaTo": "latitude", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.memberships": { + "name": "memberships", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invitation_token": { + "name": "invitation_token", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "memberships_workspace_id_user_id_index": { + "name": "memberships_workspace_id_user_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memberships_invitation_token_index": { + "name": "memberships_invitation_token_index", + "columns": [ + { + "expression": "invitation_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memberships_workspace_id_workspaces_id_fk": { + "name": "memberships_workspace_id_workspaces_id_fk", + "tableFrom": "memberships", + "tableTo": "workspaces", + "schemaTo": "latitude", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_user_id_users_id_fk": { + "name": "memberships_user_id_users_id_fk", + "tableFrom": "memberships", + "tableTo": "users", + "schemaTo": "latitude", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "memberships_invitation_token_unique": { + "name": "memberships_invitation_token_unique", + "nullsNotDistinct": false, + "columns": [ + "invitation_token" + ] + } + } + }, + "latitude.api_keys": { + "name": "api_keys", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_id_idx": { + "name": "workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_workspace_id_workspaces_id_fk": { + "name": "api_keys_workspace_id_workspaces_id_fk", + "tableFrom": "api_keys", + "tableTo": "workspaces", + "schemaTo": "latitude", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_token_unique": { + "name": "api_keys_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + } + }, + "latitude.projects": { + "name": "projects", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_idx": { + "name": "workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_workspace_id_workspaces_id_fk": { + "name": "projects_workspace_id_workspaces_id_fk", + "tableFrom": "projects", + "tableTo": "workspaces", + "schemaTo": "latitude", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.commits": { + "name": "commits", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "merged_at": { + "name": "merged_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_commit_order_idx": { + "name": "project_commit_order_idx", + "columns": [ + { + "expression": "merged_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "commits_project_id_projects_id_fk": { + "name": "commits_project_id_projects_id_fk", + "tableFrom": "commits", + "tableTo": "projects", + "schemaTo": "latitude", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "commits_user_id_users_id_fk": { + "name": "commits_user_id_users_id_fk", + "tableFrom": "commits", + "tableTo": "users", + "schemaTo": "latitude", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "commits_uuid_unique": { + "name": "commits_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + }, + "unique_commit_version": { + "name": "unique_commit_version", + "nullsNotDistinct": false, + "columns": [ + "version", + "project_id" + ] + } + } + }, + "latitude.document_versions": { + "name": "document_versions", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "document_uuid": { + "name": "document_uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "resolved_content": { + "name": "resolved_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "commit_id": { + "name": "commit_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "document_versions_commit_id_commits_id_fk": { + "name": "document_versions_commit_id_commits_id_fk", + "tableFrom": "document_versions", + "tableTo": "commits", + "schemaTo": "latitude", + "columnsFrom": [ + "commit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_document_uuid_commit_id": { + "name": "unique_document_uuid_commit_id", + "nullsNotDistinct": false, + "columns": [ + "document_uuid", + "commit_id" + ] + }, + "unique_path_commit_id_deleted_at": { + "name": "unique_path_commit_id_deleted_at", + "nullsNotDistinct": false, + "columns": [ + "path", + "commit_id", + "deleted_at" + ] + } + } + }, + "latitude.provider_api_keys": { + "name": "provider_api_keys", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "provider", + "typeSchema": "latitude", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "provider_apikeys_workspace_id_idx": { + "name": "provider_apikeys_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "provider_apikeys_name_idx": { + "name": "provider_apikeys_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "provider_apikeys_user_id_idx": { + "name": "provider_apikeys_user_id_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_api_keys_author_id_users_id_fk": { + "name": "provider_api_keys_author_id_users_id_fk", + "tableFrom": "provider_api_keys", + "tableTo": "users", + "schemaTo": "latitude", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "provider_api_keys_workspace_id_workspaces_id_fk": { + "name": "provider_api_keys_workspace_id_workspaces_id_fk", + "tableFrom": "provider_api_keys", + "tableTo": "workspaces", + "schemaTo": "latitude", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_apikeys_token_provider_unique": { + "name": "provider_apikeys_token_provider_unique", + "nullsNotDistinct": false, + "columns": [ + "token", + "provider", + "workspace_id" + ] + } + } + }, + "latitude.document_logs": { + "name": "document_logs", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_uuid": { + "name": "document_uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "commit_id": { + "name": "commit_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "resolved_content": { + "name": "resolved_content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parameters": { + "name": "parameters", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "custom_identifier": { + "name": "custom_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_log_uuid_idx": { + "name": "document_log_uuid_idx", + "columns": [ + { + "expression": "document_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "commit_id_idx": { + "name": "commit_id_idx", + "columns": [ + { + "expression": "commit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_logs_commit_id_commits_id_fk": { + "name": "document_logs_commit_id_commits_id_fk", + "tableFrom": "document_logs", + "tableTo": "commits", + "schemaTo": "latitude", + "columnsFrom": [ + "commit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "document_logs_uuid_unique": { + "name": "document_logs_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + } + }, + "latitude.provider_logs": { + "name": "provider_logs", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_log_uuid": { + "name": "document_log_uuid", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "messages": { + "name": "messages", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "response_object": { + "name": "response_object", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_text": { + "name": "response_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_calls": { + "name": "tool_calls", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'::json" + }, + "tokens": { + "name": "tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cost_in_millicents": { + "name": "cost_in_millicents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "duration": { + "name": "duration", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "log_source", + "typeSchema": "latitude", + "primaryKey": false, + "notNull": true + }, + "apiKeyId": { + "name": "apiKeyId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "generated_at": { + "name": "generated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "provider_logs_provider_id_provider_api_keys_id_fk": { + "name": "provider_logs_provider_id_provider_api_keys_id_fk", + "tableFrom": "provider_logs", + "tableTo": "provider_api_keys", + "schemaTo": "latitude", + "columnsFrom": [ + "provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "provider_logs_apiKeyId_api_keys_id_fk": { + "name": "provider_logs_apiKeyId_api_keys_id_fk", + "tableFrom": "provider_logs", + "tableTo": "api_keys", + "schemaTo": "latitude", + "columnsFrom": [ + "apiKeyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_logs_uuid_unique": { + "name": "provider_logs_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + } + }, + "latitude.datasets": { + "name": "datasets", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "csv_delimiter": { + "name": "csv_delimiter", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_key": { + "name": "file_key", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "file_metadata": { + "name": "file_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "datasets_workspace_idx": { + "name": "datasets_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "datasets_author_idx": { + "name": "datasets_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "datasets_workspace_id_name_index": { + "name": "datasets_workspace_id_name_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "datasets_workspace_id_workspaces_id_fk": { + "name": "datasets_workspace_id_workspaces_id_fk", + "tableFrom": "datasets", + "tableTo": "workspaces", + "schemaTo": "latitude", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "datasets_author_id_users_id_fk": { + "name": "datasets_author_id_users_id_fk", + "tableFrom": "datasets", + "tableTo": "users", + "schemaTo": "latitude", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.evaluations": { + "name": "evaluations", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata_id": { + "name": "metadata_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "configuration": { + "name": "configuration", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata_type": { + "name": "metadata_type", + "type": "metadata_type", + "typeSchema": "latitude", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "evaluation_workspace_idx": { + "name": "evaluation_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "evaluation_metadata_idx": { + "name": "evaluation_metadata_idx", + "columns": [ + { + "expression": "metadata_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metadata_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "evaluations_workspace_id_workspaces_id_fk": { + "name": "evaluations_workspace_id_workspaces_id_fk", + "tableFrom": "evaluations", + "tableTo": "workspaces", + "schemaTo": "latitude", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "evaluations_uuid_unique": { + "name": "evaluations_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + } + }, + "latitude.llm_as_judge_evaluation_metadatas": { + "name": "llm_as_judge_evaluation_metadatas", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "metadata_type": { + "name": "metadata_type", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "default": "'llm_as_judge'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "llm_as_judge_evaluation_metadatas_template_id_idx": { + "name": "llm_as_judge_evaluation_metadatas_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "llm_as_judge_evaluation_metadatas_template_id_evaluations_templates_id_fk": { + "name": "llm_as_judge_evaluation_metadatas_template_id_evaluations_templates_id_fk", + "tableFrom": "llm_as_judge_evaluation_metadatas", + "tableTo": "evaluations_templates", + "schemaTo": "latitude", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.connected_evaluations": { + "name": "connected_evaluations", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "document_uuid": { + "name": "document_uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "evaluation_mode": { + "name": "evaluation_mode", + "type": "evaluation_mode_enum", + "typeSchema": "latitude", + "primaryKey": false, + "notNull": true + }, + "evaluation_id": { + "name": "evaluation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "connected_evaluations_evaluation_idx": { + "name": "connected_evaluations_evaluation_idx", + "columns": [ + { + "expression": "evaluation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "connected_evaluations_evaluation_id_evaluations_id_fk": { + "name": "connected_evaluations_evaluation_id_evaluations_id_fk", + "tableFrom": "connected_evaluations", + "tableTo": "evaluations", + "schemaTo": "latitude", + "columnsFrom": [ + "evaluation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "connected_evaluations_unique_idx": { + "name": "connected_evaluations_unique_idx", + "nullsNotDistinct": false, + "columns": [ + "document_uuid", + "evaluation_id" + ] + } + } + }, + "latitude.evaluation_results": { + "name": "evaluation_results", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "evaluation_id": { + "name": "evaluation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "document_log_id": { + "name": "document_log_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "provider_log_id": { + "name": "provider_log_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "resultable_type": { + "name": "resultable_type", + "type": "evaluation_result_types", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resultable_id": { + "name": "resultable_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "evaluation_idx": { + "name": "evaluation_idx", + "columns": [ + { + "expression": "evaluation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_log_idx": { + "name": "document_log_idx", + "columns": [ + { + "expression": "document_log_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "provider_log_idx": { + "name": "provider_log_idx", + "columns": [ + { + "expression": "provider_log_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resultable_idx": { + "name": "resultable_idx", + "columns": [ + { + "expression": "resultable_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resultable_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "evaluation_results_evaluation_id_evaluations_id_fk": { + "name": "evaluation_results_evaluation_id_evaluations_id_fk", + "tableFrom": "evaluation_results", + "tableTo": "evaluations", + "schemaTo": "latitude", + "columnsFrom": [ + "evaluation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "evaluation_results_document_log_id_document_logs_id_fk": { + "name": "evaluation_results_document_log_id_document_logs_id_fk", + "tableFrom": "evaluation_results", + "tableTo": "document_logs", + "schemaTo": "latitude", + "columnsFrom": [ + "document_log_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "evaluation_results_provider_log_id_provider_logs_id_fk": { + "name": "evaluation_results_provider_log_id_provider_logs_id_fk", + "tableFrom": "evaluation_results", + "tableTo": "provider_logs", + "schemaTo": "latitude", + "columnsFrom": [ + "provider_log_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.evaluations_templates": { + "name": "evaluations_templates", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "configuration": { + "name": "configuration", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "evaluations_templates_category_evaluations_template_categories_id_fk": { + "name": "evaluations_templates_category_evaluations_template_categories_id_fk", + "tableFrom": "evaluations_templates", + "tableTo": "evaluations_template_categories", + "schemaTo": "latitude", + "columnsFrom": [ + "category" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.evaluations_template_categories": { + "name": "evaluations_template_categories", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.magic_link_tokens": { + "name": "magic_link_tokens", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expired_at": { + "name": "expired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "magic_link_tokens_user_id_users_id_fk": { + "name": "magic_link_tokens_user_id_users_id_fk", + "tableFrom": "magic_link_tokens", + "tableTo": "users", + "schemaTo": "latitude", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "magic_link_tokens_token_unique": { + "name": "magic_link_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + } + }, + "latitude.events": { + "name": "events", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "event_type_idx": { + "name": "event_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.evaluation_resultable_numbers": { + "name": "evaluation_resultable_numbers", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.evaluation_resultable_texts": { + "name": "evaluation_resultable_texts", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.evaluation_resultable_booleans": { + "name": "evaluation_resultable_booleans", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "latitude.provider": { + "name": "provider", + "schema": "latitude", + "values": [ + "openai", + "anthropic", + "groq", + "mistral", + "azure", + "google" + ] + }, + "latitude.log_source": { + "name": "log_source", + "schema": "latitude", + "values": [ + "playground", + "api", + "evaluation" + ] + }, + "latitude.metadata_type": { + "name": "metadata_type", + "schema": "latitude", + "values": [ + "llm_as_judge" + ] + }, + "public.evaluation_result_types": { + "name": "evaluation_result_types", + "schema": "public", + "values": [ + "evaluation_resultable_booleans", + "evaluation_resultable_texts", + "evaluation_resultable_numbers" + ] + } + }, + "schemas": { + "latitude": "latitude" + }, + "_meta": { + "columns": {}, + "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 2b594cf46..d25411418 100644 --- a/packages/core/drizzle/meta/_journal.json +++ b/packages/core/drizzle/meta/_journal.json @@ -365,6 +365,13 @@ "when": 1726148717688, "tag": "0051_colorful_lightspeed", "breakpoints": true + }, + { + "idx": 52, + "version": "7", + "when": 1726227047167, + "tag": "0052_famous_daimon_hellstrom", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index 9101c9ee1..25b6c2e5e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -26,7 +26,7 @@ "db:migrate:test": "NODE_ENV=test pnpm run db:migrate", "db:drop": "drizzle-kit drop", "db:check": "drizzle-kit check", - "db:studio": "drizzle-kit studio --port 3002", + "db:studio": "drizzle-kit studio --port 3003", "lint": "eslint src", "tc": "tsc --noEmit", "test": "pnpm run db:migrate:test && vitest run --pool=forks", @@ -48,6 +48,7 @@ "@latitude-data/typescript-config": "workspace:*", "@sindresorhus/slugify": "^2.2.1", "@t3-oss/env-core": "^0.11.1", + "@types/json-schema": "^7.0.15", "@types/lodash-es": "^4.17.12", "@types/node": "^22.5.0", "@types/pg": "^8.11.6", @@ -61,6 +62,7 @@ "eslint-plugin-drizzle": "^0.2.3", "flydrive": "^1.1.0", "jose": "^5.8.0", + "json-schema": "^0.4.0", "lodash-es": "^4.17.21", "pg": "^8.12.0", "pg-transactional-tests": "^1.0.9", @@ -88,12 +90,13 @@ "@latitude-data/mailers": "workspace:^", "@sindresorhus/slugify": "^2.2.1", "@t3-oss/env-core": "^0.11.1", - "ai": "^3.2.42", + "ai": "^3.3.3", "argon2": "^0.41.0", "csv-parse": "^5.5.6", "drizzle-orm": "^0.33.0", "flydrive": "^1.1.0", "jose": "^5.8.0", + "json-schema": "^0.4.0", "lodash-es": "^4.17.21", "pg": "^8.12.0", "react": "^18.3.1", diff --git a/packages/core/src/browser.ts b/packages/core/src/browser.ts index d89cd8aa5..4c4f5accb 100644 --- a/packages/core/src/browser.ts +++ b/packages/core/src/browser.ts @@ -1,3 +1,4 @@ export * from './constants' export * from './schema/types' export * from './websockets/constants' +export * from './helpers' diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 911d063f3..d2d02f29b 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -2,7 +2,12 @@ import type { Message as CompilerMessage, ToolCall, } from '@latitude-data/compiler' -import { CompletionTokenUsage, CoreTool, TextStreamPart } from 'ai' +import { + CompletionTokenUsage, + CoreTool, + ObjectStreamPart, + TextStreamPart, +} from 'ai' import { Config } from './services/ai' @@ -34,14 +39,24 @@ export const HELP_CENTER = { commitVersions: `${LATITUDE_DOCS_URL}/not-found`, } -export type ChainStepCallResponse = { +export type ChainStepTextResponse = { text: string usage: CompletionTokenUsage + toolCalls: ToolCall[] +} +export type ChainStepObjectResponse = { + object: any + text: string + usage: CompletionTokenUsage +} + +export type ChainTextResponse = ChainStepTextResponse & { + documentLogUuid: string } -export type ChainCallResponse = ChainStepCallResponse & { +export type ChainObjectResponse = ChainStepObjectResponse & { documentLogUuid: string - toolCalls: ToolCall[] } +export type ChainCallResponse = ChainTextResponse | ChainObjectResponse export enum Providers { OpenAI = 'openai', @@ -69,7 +84,9 @@ export enum ChainEventTypes { StepComplete = 'chain-step-complete', } -type ProviderData = TextStreamPart> +export type ProviderData = + | TextStreamPart> + | ObjectStreamPart> export type ProviderDataType = ProviderData['type'] type LatitudeEventData = @@ -81,12 +98,13 @@ type LatitudeEventData = } | { type: ChainEventTypes.StepComplete - response: ChainStepCallResponse + response: ChainCallResponse } | { type: ChainEventTypes.Complete config: Config - messages: Message[] + messages?: Message[] + object?: any response: ChainCallResponse } | { diff --git a/packages/core/src/events/handlers/createEvaluationResultJob.ts b/packages/core/src/events/handlers/createEvaluationResultJob.ts index 09ffa92cc..7126e4ccc 100644 --- a/packages/core/src/events/handlers/createEvaluationResultJob.ts +++ b/packages/core/src/events/handlers/createEvaluationResultJob.ts @@ -1,4 +1,5 @@ import { EvaluationRunEvent } from '.' +import { ChainObjectResponse } from '../../constants' import { unsafelyFindDocumentLogByUuid, unsafelyFindEvaluation, @@ -28,6 +29,6 @@ export const createEvaluationResultJob = async ({ evaluation, documentLog, providerLog, - result: response.text, + result: (response as ChainObjectResponse).object, }).then((r) => r.unwrap()) } diff --git a/packages/core/src/events/handlers/createProviderLogJob.ts b/packages/core/src/events/handlers/createProviderLogJob.ts index 37dce0bbc..de923aca5 100644 --- a/packages/core/src/events/handlers/createProviderLogJob.ts +++ b/packages/core/src/events/handlers/createProviderLogJob.ts @@ -1,10 +1,15 @@ -import { AIProviderCallCompleted } from '.' -import { createProviderLog } from '../../services/providerLogs' +import { + createProviderLog, + CreateProviderLogProps, +} from '../../services/providerLogs' export const createProviderLogJob = async ({ - data: event, + data, }: { - data: AIProviderCallCompleted + data: Omit & { generatedAt: string } }) => { - await createProviderLog(event.data) + return await createProviderLog({ + ...data, + generatedAt: new Date(data.generatedAt), + }).then((r) => r.unwrap()) } diff --git a/packages/core/src/events/handlers/index.ts b/packages/core/src/events/handlers/index.ts index ca7d86c70..8bcab9afe 100644 --- a/packages/core/src/events/handlers/index.ts +++ b/packages/core/src/events/handlers/index.ts @@ -1,18 +1,11 @@ -import { ToolCall } from '@latitude-data/compiler' -import { CompletionTokenUsage } from 'ai' - import { ChainCallResponse, - LogSources, MagicLinkToken, Membership, - Message, - Providers, + ProviderLog, User, } from '../../browser' -import { PartialConfig } from '../../services/ai' import { createEvaluationResultJob } from './createEvaluationResultJob' -import { createProviderLogJob } from './createProviderLogJob' import { createDocumentLogJob } from './documentLogs/createJob' import { sendInvitationToUserJob } from './sendInvitationToUser' import { sendMagicLinkJob } from './sendMagicLinkHandler' @@ -31,24 +24,6 @@ export type EventHandler = ({ data: E }) => void -export type AIProviderCallCompleted = LatitudeEventGeneric< - 'aiProviderCallCompleted', - { - uuid: string - source: LogSources - generatedAt: Date - documentLogUuid?: string - providerId: number - providerType: Providers - model: string - config: PartialConfig - messages: Message[] - responseText: string - toolCalls?: ToolCall[] - usage: CompletionTokenUsage - duration: number - } -> export type MagicLinkTokenCreated = LatitudeEventGeneric< 'magicLinkTokenCreated', MagicLinkToken @@ -83,28 +58,33 @@ export type DocumentRunEvent = LatitudeEventGeneric< } > +export type ProviderLogCreatedEvent = LatitudeEventGeneric< + 'providerLogCreated', + ProviderLog +> + export type LatitudeEvent = | MembershipCreatedEvent | UserCreatedEvent | MagicLinkTokenCreated - | AIProviderCallCompleted | EvaluationRunEvent | DocumentRunEvent + | ProviderLogCreatedEvent export interface IEventsHandlers { - aiProviderCallCompleted: EventHandler[] magicLinkTokenCreated: EventHandler[] membershipCreated: EventHandler[] userCreated: EventHandler[] evaluationRun: EventHandler[] documentRun: EventHandler[] + providerLogCreated: EventHandler[] } export const EventHandlers: IEventsHandlers = { magicLinkTokenCreated: [sendMagicLinkJob], membershipCreated: [sendInvitationToUserJob], userCreated: [], - aiProviderCallCompleted: [createProviderLogJob], evaluationRun: [createEvaluationResultJob], documentRun: [createDocumentLogJob], + providerLogCreated: [], } as const diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts new file mode 100644 index 000000000..f679c2309 --- /dev/null +++ b/packages/core/src/helpers.ts @@ -0,0 +1,7 @@ +export function objectToString(object: any) { + try { + return JSON.stringify(object) + } catch (error) { + return 'Error: Provider returned an object that could not be stringified' + } +} diff --git a/packages/core/src/schema/models/providerLogs.ts b/packages/core/src/schema/models/providerLogs.ts index 0118367b1..097348cb8 100644 --- a/packages/core/src/schema/models/providerLogs.ts +++ b/packages/core/src/schema/models/providerLogs.ts @@ -4,11 +4,13 @@ import { bigserial, integer, json, + jsonb, text, timestamp, uuid, varchar, } from 'drizzle-orm/pg-core' +import { JSONSchema7 } from 'json-schema' import { LogSources } from '../../constants' import { PartialConfig } from '../../services/ai' @@ -36,7 +38,8 @@ export const providerLogs = latitudeSchema.table('provider_logs', { model: varchar('model'), config: json('config').$type().notNull(), messages: json('messages').$type().notNull(), - responseText: text('response_text').$type().notNull().default(''), + responseObject: jsonb('response_object').$type(), + responseText: text('response_text').$type(), toolCalls: json('tool_calls').$type().notNull().default([]), tokens: bigint('tokens', { mode: 'number' }).notNull(), costInMillicents: integer('cost_in_millicents').notNull().default(0), diff --git a/packages/core/src/schema/types.ts b/packages/core/src/schema/types.ts index a3009e2e1..5f51862e7 100644 --- a/packages/core/src/schema/types.ts +++ b/packages/core/src/schema/types.ts @@ -1,4 +1,3 @@ -import { ToolCall } from '@latitude-data/compiler' import { type InferSelectModel } from 'drizzle-orm' import { EvaluationResultableType } from '../constants' @@ -38,12 +37,7 @@ export type ApiKey = InferSelectModel export type Commit = InferSelectModel export type DocumentVersion = InferSelectModel export type Project = InferSelectModel -export type ProviderLog = InferSelectModel & { - // Typescript thinks these 2 are optional because they are in the schema - // but we add a default empty string and empty array to them - responseText: string - toolCalls: ToolCall[] -} +export type ProviderLog = InferSelectModel export type DocumentLog = InferSelectModel export type Evaluation = InferSelectModel export type ConnectedEvaluation = InferSelectModel @@ -80,3 +74,8 @@ export type EvaluationResultConfiguration = { export type EvaluationTemplateWithCategory = EvaluationTemplate & { category: string } + +export type ProviderLogDto = Omit< + ProviderLog, + 'responseText' | 'responseObject' +> & { response: string } diff --git a/packages/core/src/services/ai/helpers.ts b/packages/core/src/services/ai/helpers.ts new file mode 100644 index 000000000..fd5ffebc0 --- /dev/null +++ b/packages/core/src/services/ai/helpers.ts @@ -0,0 +1,96 @@ +import { createAnthropic } from '@ai-sdk/anthropic' +import { createAzure } from '@ai-sdk/azure' +import { createGoogleGenerativeAI } from '@ai-sdk/google' +import { createMistral } from '@ai-sdk/mistral' +import { createOpenAI } from '@ai-sdk/openai' +import { z } from 'zod' + +import { Providers } from '../../constants' + +export type Config = { + [key: string]: any + provider: string + model: string + azure?: { resourceName: string } +} + +export type PartialConfig = Omit + +const GROQ_API_URL = 'https://api.groq.com/openai/v1' + +export function createProvider({ + provider, + apiKey, + config, +}: { + provider: Providers + apiKey: string + config?: PartialConfig +}) { + switch (provider) { + case Providers.OpenAI: + return createOpenAI({ + apiKey, + // Needed for OpenAI to return token usage + compatibility: 'strict', + }) + case Providers.Groq: + return createOpenAI({ + apiKey, + compatibility: 'compatible', + baseURL: GROQ_API_URL, + }) + case Providers.Anthropic: + return createAnthropic({ + apiKey, + }) + case Providers.Mistral: + return createMistral({ + apiKey, + }) + case Providers.Azure: + return createAzure({ + apiKey, + ...(config?.azure ?? {}), + }) + case Providers.Google: + return createGoogleGenerativeAI({ + apiKey, + ...(config?.google ?? {}), + }) + default: + throw new Error(`Provider ${provider} not supported`) + } +} + +export function validateConfig(config: Record): Config { + const configSchema = z + .object({ + model: z.string(), + provider: z.string(), + google: z + .object({ + structuredOutputs: z.boolean().optional(), + cachedContent: z.string().optional(), + safetySettings: z + .array( + z + .object({ + category: z.string().optional(), // TODO: can be an enum + threshold: z.string().optional(), // TODO: can be an enum + }) + .optional(), + ) + .optional(), + }) + .optional(), + azure: z + .object({ + resourceName: z.string(), + }) + .optional(), + }) + .catchall(z.unknown()) + + return configSchema.parse(config) +} diff --git a/packages/core/src/services/ai/index.ts b/packages/core/src/services/ai/index.ts index ed2627ddb..96904c102 100644 --- a/packages/core/src/services/ai/index.ts +++ b/packages/core/src/services/ai/index.ts @@ -1,22 +1,23 @@ -import { createAnthropic } from '@ai-sdk/anthropic' -import { createAzure } from '@ai-sdk/azure' -import { createGoogleGenerativeAI } from '@ai-sdk/google' -import { createMistral } from '@ai-sdk/mistral' -import { createOpenAI } from '@ai-sdk/openai' import { Message } from '@latitude-data/compiler' +import { setupJobs } from '@latitude-data/jobs' import { CallWarning, CompletionTokenUsage, CoreMessage, FinishReason, + jsonSchema, + streamObject, streamText, } from 'ai' +import { JSONSchema7 } from 'json-schema' import { v4 } from 'uuid' -import { z } from 'zod' -import { LogSources, ProviderApiKey, Providers } from '../../browser' -import { publisher } from '../../events/publisher' -import { CreateProviderLogProps } from '../providerLogs/create' +import { LogSources, ProviderApiKey } from '../../browser' +import { + createProviderLog, + CreateProviderLogProps, +} from '../providerLogs/create' +import { createProvider, PartialConfig } from './helpers' export type FinishCallbackEvent = { finishReason: FinishReason @@ -38,85 +39,30 @@ export type FinishCallbackEvent = { } export type FinishCallback = (event: FinishCallbackEvent) => void -export type Config = { - [key: string]: any - provider: string - model: string - azure?: { resourceName: string } -} - -export type PartialConfig = Omit - -const GROQ_API_URL = 'https://api.groq.com/openai/v1' - -function createProvider({ - provider, - apiKey, +export type AILog = Omit +export async function ai({ + provider: apiProvider, + prompt, + messages, config, + documentLogUuid, + source, + schema = config.schema, + output = config.schema?.type || 'no-schema', + transactionalLogs = false, + onFinish, }: { - provider: Providers - apiKey: string - config?: PartialConfig + provider: ProviderApiKey + config: PartialConfig + messages: Message[] + documentLogUuid?: string + prompt?: string + source: LogSources + schema?: JSONSchema7 + output?: 'object' | 'array' | 'no-schema' + transactionalLogs?: boolean + onFinish?: FinishCallback }) { - switch (provider) { - case Providers.OpenAI: - return createOpenAI({ - apiKey, - // Needed for OpenAI to return token usage - compatibility: 'strict', - }) - case Providers.Groq: - return createOpenAI({ - apiKey, - compatibility: 'compatible', - baseURL: GROQ_API_URL, - }) - case Providers.Anthropic: - return createAnthropic({ - apiKey, - }) - case Providers.Mistral: - return createMistral({ - apiKey, - }) - case Providers.Azure: - return createAzure({ - apiKey, - ...(config?.azure ?? {}), - }) - case Providers.Google: - return createGoogleGenerativeAI({ - apiKey, - ...(config?.google ?? {}), - }) - default: - throw new Error(`Provider ${provider} not supported`) - } -} - -export type AILog = Omit -export async function ai( - { - provider: apiProvider, - prompt, - messages, - config, - documentLogUuid, - source, - }: { - provider: ProviderApiKey - config: PartialConfig - messages: Message[] - documentLogUuid?: string - prompt?: string - source: LogSources - }, - { - onFinish, - }: { - onFinish?: FinishCallback - } = {}, -) { const startTime = Date.now() const { provider, @@ -127,76 +73,87 @@ export async function ai( const model = config.model const m = createProvider({ provider, apiKey, config })(model) - const result = await streamText({ + const commonOptions = { model: m, prompt, messages: messages as CoreMessage[], - onFinish: (event) => { - publisher.publish({ - type: 'aiProviderCallCompleted', - data: { - uuid: v4(), - source, - generatedAt: new Date(), - documentLogUuid, - providerId, - providerType, - model, - config, - messages, - responseText: event.text, - toolCalls: event.toolCalls?.map((t) => ({ - id: t.toolCallId, - name: t.toolName, - arguments: t.args, - })), - usage: event.usage, - duration: Date.now() - startTime, - }, - }) + } + + const createFinishHandler = (isStructured: boolean) => async (event: any) => { + const commonData = { + uuid: v4(), + source, + generatedAt: new Date(), + documentLogUuid, + providerId, + providerType, + model, + config, + messages, + toolCalls: event.toolCalls?.map((t: any) => ({ + id: t.toolCallId, + name: t.toolName, + arguments: t.args, + })), + usage: event.usage, + duration: Date.now() - startTime, + } + + const payload = { + type: 'aiProviderCallCompleted', + data: { + ...commonData, + responseText: event.text, + responseObject: isStructured ? event.object : undefined, + }, + } - onFinish?.(event) - }, - }) + let providerLogUuid + if (transactionalLogs) { + const providerLog = await createProviderLog(payload.data).then((r) => + r.unwrap(), + ) + providerLogUuid = providerLog.uuid + } else { + await setupJobs().defaultQueue.jobs.enqueueCreateProviderLogJob( + payload.data, + ) + } - return { - fullStream: result.fullStream, - text: result.text, - usage: result.usage, - toolCalls: result.toolCalls, + onFinish?.({ ...event, providerLogUuid }) } -} -export function validateConfig(config: Record): Config { - const configSchema = z - .object({ - model: z.string(), - provider: z.string(), - google: z - .object({ - structuredOutputs: z.boolean().optional(), - cachedContent: z.string().optional(), - safetySettings: z - .array( - z - .object({ - category: z.string().optional(), // TODO: can be an enum - threshold: z.string().optional(), // TODO: can be an enum - }) - .optional(), - ) - .optional(), - }) - .optional(), - azure: z - .object({ - resourceName: z.string(), - }) - .optional(), + if (schema && output) { + const result = await streamObject({ + ...commonOptions, + schema: jsonSchema(schema), + // @ts-expect-error - output is vale but depending on the type of schema + // there might be a mismatch (e.g you pass an object schema but the + // output is "array"). Not really an issue we need to defend atm + output, + onFinish: createFinishHandler(true), + }) + + return { + fullStream: result.fullStream, + object: result.object, + usage: result.usage, + } + } else { + const result = await streamText({ + ...commonOptions, + onFinish: createFinishHandler(false), }) - .catchall(z.unknown()) - return configSchema.parse(config) + return { + fullStream: result.fullStream, + text: result.text, + usage: result.usage, + toolCalls: result.toolCalls, + } + } } export { estimateCost } from './estimateCost' +export { validateConfig } from './helpers' +export type { PartialConfig, Config } from './helpers' diff --git a/packages/core/src/services/chains/run.test.ts b/packages/core/src/services/chains/run.test.ts new file mode 100644 index 000000000..cfc1a0e07 --- /dev/null +++ b/packages/core/src/services/chains/run.test.ts @@ -0,0 +1,293 @@ +import { Chain, ContentType, MessageRole } from '@latitude-data/compiler' +import { v4 as uuid } from 'uuid' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { LogSources, Providers } from '../../constants' +import * as factories from '../../tests/factories' +import * as aiModule from '../ai' +import { runChain } from './run' + +// Mock other dependencies +vi.mock('@latitude-data/compiler') +vi.mock('uuid') + +describe('runChain', () => { + const mockChain: Partial = { + step: vi.fn(), + rawText: 'Test raw text', + } + + const mockUUID = '12345678-1234-1234-1234-123456789012' + let apikeys: Map + + function createMockAiResponse(text: string, totalTokens: number) { + return { + text: Promise.resolve(text), + usage: Promise.resolve({ totalTokens }), + toolCalls: Promise.resolve([]), + fullStream: new ReadableStream({ + start(controller) { + controller.enqueue({ type: 'text', text }) + controller.close() + }, + }), + } + } + + beforeEach(async () => { + vi.resetAllMocks() + vi.mocked(uuid).mockReturnValue(mockUUID) + + const { providers } = await factories.createProject({ + providers: [{ name: 'openai', type: Providers.OpenAI }], + }) + apikeys = new Map(providers.map((p) => [p.name, p])) + }) + + it('runs a chain without schema override', async () => { + const mockAiResponse = createMockAiResponse('AI response', 10) + vi.spyOn(aiModule, 'ai').mockResolvedValue(mockAiResponse as any) + + vi.mocked(mockChain.step!).mockResolvedValue({ + completed: true, + conversation: { + messages: [ + { + role: MessageRole.user, + content: [{ type: ContentType.text, text: 'Test message' }], + }, + ], + config: { provider: 'openai', model: 'gpt-3.5-turbo' }, + }, + }) + + const result = await runChain({ + chain: mockChain as Chain, + apikeys, + source: LogSources.API, + }) + + expect(result.ok).toBe(true) + if (!result.ok) return + + const response = await result.value.response + expect(response).toEqual({ + documentLogUuid: expect.any(String), + text: 'AI response', + usage: { totalTokens: 10 }, + toolCalls: [], + }) + + expect(aiModule.ai).toHaveBeenCalledWith( + expect.objectContaining({ + config: { provider: 'openai', model: 'gpt-3.5-turbo' }, + schema: undefined, + output: undefined, + }), + ) + }) + + it('runs a chain with schema override', async () => { + const mockSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'number' }, + }, + } as const + + const mockAiResponse = { + object: Promise.resolve({ name: 'John', age: 30 }), + usage: Promise.resolve({ totalTokens: 15 }), + fullStream: new ReadableStream({ + start(controller) { + controller.enqueue({ + type: 'object', + object: { name: 'John', age: 30 }, + }) + controller.close() + }, + }), + } + + vi.spyOn(aiModule, 'ai').mockResolvedValue(mockAiResponse as any) + + vi.mocked(mockChain.step!).mockResolvedValue({ + completed: true, + conversation: { + messages: [ + { + role: MessageRole.user, + content: [{ type: ContentType.text, text: 'Test message' }], + }, + ], + config: { provider: 'openai', model: 'gpt-3.5-turbo' }, + }, + }) + + const result = await runChain({ + chain: mockChain as Chain, + apikeys, + source: LogSources.API, + configOverrides: { + schema: mockSchema, + output: 'object', + }, + }) + + expect(result.ok).toBe(true) + if (!result.ok) return + + const response = await result.value.response + expect(response).toEqual({ + documentLogUuid: expect.any(String), + object: { name: 'John', age: 30 }, + text: '{"name":"John","age":30}', + usage: { totalTokens: 15 }, + }) + + expect(aiModule.ai).toHaveBeenCalledWith( + expect.objectContaining({ + config: { provider: 'openai', model: 'gpt-3.5-turbo' }, + schema: mockSchema, + output: 'object', + }), + ) + }) + + it('handles errors during chain execution', async () => { + vi.mocked(mockChain.step!).mockRejectedValue( + new Error('Chain execution failed'), + ) + + const result = await runChain({ + chain: mockChain as Chain, + apikeys, + source: LogSources.API, + }) + + expect(result.ok).toBe(true) + if (!result.ok) return + + await expect(result.value.response).rejects.toThrow( + 'Chain execution failed', + ) + await expect(result.value.duration).rejects.toThrow( + 'Chain execution failed', + ) + }) + + it('handles multiple steps in a chain', async () => { + const mockAiResponse1 = createMockAiResponse('AI response 1', 10) + const mockAiResponse2 = createMockAiResponse('AI response 2', 15) + + vi.spyOn(aiModule, 'ai') + .mockResolvedValueOnce(mockAiResponse1 as any) + .mockResolvedValueOnce(mockAiResponse2 as any) + + vi.mocked(mockChain.step!) + .mockResolvedValueOnce({ + completed: false, + conversation: { + messages: [ + { + role: MessageRole.user, + content: [{ type: ContentType.text, text: 'Step 1' }], + }, + ], + config: { provider: 'openai', model: 'gpt-3.5-turbo' }, + }, + }) + .mockResolvedValueOnce({ + completed: true, + conversation: { + messages: [ + { + role: MessageRole.user, + content: [{ type: ContentType.text, text: 'Step 1' }], + }, + { + role: MessageRole.assistant, + content: 'AI response 1', + toolCalls: [], + }, + { + role: MessageRole.user, + content: [{ type: ContentType.text, text: 'Step 2' }], + }, + ], + config: { provider: 'openai', model: 'gpt-3.5-turbo' }, + }, + }) + + const result = await runChain({ + chain: mockChain as Chain, + apikeys, + source: LogSources.API, + }) + + expect(result.ok).toBe(true) + if (!result.ok) return + + const response = await result.value.response + expect(response).toEqual({ + documentLogUuid: expect.any(String), + text: 'AI response 2', + usage: { totalTokens: 15 }, + toolCalls: [], + }) + + expect(aiModule.ai).toHaveBeenCalledTimes(2) + }) + + it('handles system messages correctly', async () => { + const mockAiResponse = createMockAiResponse('AI response', 10) + vi.spyOn(aiModule, 'ai').mockResolvedValue(mockAiResponse as any) + + vi.mocked(mockChain.step!).mockResolvedValue({ + completed: true, + conversation: { + messages: [ + { + role: MessageRole.system, + content: 'System instruction', + }, + { + role: MessageRole.user, + content: [{ type: ContentType.text, text: 'User message' }], + }, + ], + config: { provider: 'openai', model: 'gpt-3.5-turbo' }, + }, + }) + + const result = await runChain({ + chain: mockChain as Chain, + apikeys, + source: LogSources.API, + }) + + expect(result.ok).toBe(true) + if (!result.ok) return + + const response = await result.value.response + expect(response).toEqual({ + documentLogUuid: expect.any(String), + text: 'AI response', + usage: { totalTokens: 10 }, + toolCalls: [], + }) + + expect(aiModule.ai).toHaveBeenCalledWith( + expect.objectContaining({ + messages: [ + { role: MessageRole.system, content: 'System instruction' }, + { + role: MessageRole.user, + content: [{ type: ContentType.text, text: 'User message' }], + }, + ], + }), + ) + }) +}) diff --git a/packages/core/src/services/chains/run.ts b/packages/core/src/services/chains/run.ts index d5e8d67da..276117d19 100644 --- a/packages/core/src/services/chains/run.ts +++ b/packages/core/src/services/chains/run.ts @@ -1,13 +1,18 @@ import { Chain, MessageRole } from '@latitude-data/compiler' +import { CoreTool, ObjectStreamPart, TextStreamPart } from 'ai' +import { JSONSchema7 } from 'json-schema' import { v4 } from 'uuid' import { ZodError } from 'zod' -import { ProviderApiKey } from '../../browser' +import { objectToString, ProviderApiKey } from '../../browser' import { ChainCallResponse, ChainEvent, ChainEventTypes, + ChainObjectResponse, + ChainTextResponse, LogSources, + ProviderData, StreamEventTypes, } from '../../constants' import { NotFoundError, Result, UnprocessableEntityError } from '../../lib' @@ -21,11 +26,16 @@ export async function runChain({ apikeys, source, generateUUID = v4, + configOverrides, }: { chain: Chain generateUUID?: () => string source: LogSources apikeys: CachedApiKeys + configOverrides?: { + schema: JSONSchema7 + output: 'object' | 'array' | 'no-schema' + } }) { const documentLogUuid = generateUUID() @@ -46,6 +56,7 @@ export async function runChain({ apikeys, controller, documentLogUuid, + configOverrides, }) .then(responseResolve) .catch(responseReject) @@ -70,6 +81,7 @@ async function iterate({ previousCount = 0, previousResponse, documentLogUuid, + configOverrides, }: { source: LogSources chain: Chain @@ -78,20 +90,14 @@ async function iterate({ previousCount?: number previousApiKey?: ProviderApiKey documentLogUuid: string - previousResponse?: { - text: string - usage: Record + previousResponse?: ChainTextResponse + configOverrides?: { + schema: JSONSchema7 + output: 'object' | 'array' | 'no-schema' } }) { try { - const { - newMessagesInStep, - conversation, - completed, - config, - apiKey, - sentCount, - } = await doChainStep({ + const stepResult = await computeStepData({ chain, previousResponse, apikeys, @@ -99,35 +105,91 @@ async function iterate({ sentCount: previousCount, }) - enqueueChainEvent(controller, { - data: { - type: ChainEventTypes.Step, - isLastStep: completed, - config: conversation.config as Config, - messages: newMessagesInStep, - }, - event: StreamEventTypes.Latitude, - }) + publishStepStartEvent(controller, stepResult) - const result = await ai({ + const aiResult = await ai({ source, documentLogUuid, - messages: conversation.messages, - config: config, - provider: apiKey, + messages: stepResult.conversation.messages, + config: stepResult.config, + provider: stepResult.apiKey, + schema: configOverrides?.schema, + output: configOverrides?.output, + transactionalLogs: stepResult.completed, }) - for await (const value of streamToGenerator(result.fullStream)) { - enqueueChainEvent(controller, { - event: StreamEventTypes.Provider, - data: value, + await streamAIResult(controller, aiResult) + + const response = await createChainResponse(aiResult, documentLogUuid) + + if (stepResult.completed) { + await handleCompletedChain(controller, stepResult, response) + return response + } else { + publishStepCompleteEvent(controller, response) + + return iterate({ + source, + chain, + documentLogUuid, + apikeys, + controller, + previousApiKey: stepResult.apiKey, + previousCount: stepResult.sentCount, + previousResponse: response as ChainTextResponse, + configOverrides, }) } + } catch (error) { + handleIterationError(controller, error) + throw error + } +} + +// Helper functions - // TODO: The type on `ai` package return a different definition for `toolCalls` - // than `@latitude-data/compiler` package. We have to unify the naming. - // For now no toolCalls - const response: ChainCallResponse = { +function publishStepStartEvent( + controller: ReadableStreamDefaultController, + stepResult: Awaited>, +) { + enqueueChainEvent(controller, { + data: { + type: ChainEventTypes.Step, + isLastStep: stepResult.completed, + config: stepResult.conversation.config as Config, + messages: stepResult.newMessagesInStep, + }, + event: StreamEventTypes.Latitude, + }) +} + +async function streamAIResult( + controller: ReadableStreamDefaultController, + result: Awaited>, +) { + for await (const value of streamToGenerator< + TextStreamPart> | ObjectStreamPart + >(result.fullStream)) { + enqueueChainEvent(controller, { + event: StreamEventTypes.Provider, + data: value as unknown as ProviderData, + }) + } +} + +async function createChainResponse( + result: Awaited>, + documentLogUuid: string, +): Promise { + if (result.object) { + return { + text: objectToString(await result.object), + object: await result.object, + usage: await result.usage, + documentLogUuid, + } + } else { + return { documentLogUuid, text: await result.text, usage: await result.usage, @@ -137,80 +199,90 @@ async function iterate({ arguments: t.args, })), } + } +} - if (completed) { - const completedResponse = { - ...response, - documentLogUuid, - } - enqueueChainEvent(controller, { - event: StreamEventTypes.Latitude, - data: { - type: ChainEventTypes.Complete, - config: conversation.config as Config, - messages: [ - { - role: MessageRole.assistant, - toolCalls: response.toolCalls, - content: response.text, - }, - ], - response: completedResponse, +async function handleCompletedChain( + controller: ReadableStreamDefaultController, + stepResult: Awaited>, + response: ChainCallResponse, +) { + const eventData = { + type: ChainEventTypes.Complete, + config: stepResult.conversation.config as Config, + response, + } as const + + if ('text' in response) { + Object.assign(eventData, { + messages: [ + { + role: MessageRole.assistant, + toolCalls: (response as ChainTextResponse).toolCalls || [], + content: response.text || '', }, - }) + ], + }) + } else if ('object' in response) { + Object.assign(eventData, { + object: (response as ChainObjectResponse).object, + text: objectToString((response as ChainObjectResponse).object), + }) + } - controller.close() + enqueueChainEvent(controller, { + event: StreamEventTypes.Latitude, + data: eventData, + }) - return completedResponse - } else { - enqueueChainEvent(controller, { - event: StreamEventTypes.Latitude, - data: { - type: ChainEventTypes.StepComplete, - response, - }, - }) - return iterate({ - source, - chain, - documentLogUuid, - apikeys, - controller, - previousApiKey: apiKey, - previousCount: sentCount, - previousResponse: response, - }) - } - } catch (e) { - const error = e as Error - enqueueChainEvent(controller, { - event: StreamEventTypes.Latitude, - data: { - type: ChainEventTypes.Error, - error: { - name: error.name, - message: error.message, - stack: error.stack, - }, + controller.close() +} + +function publishStepCompleteEvent( + controller: ReadableStreamDefaultController, + response: ChainCallResponse, +) { + enqueueChainEvent(controller, { + event: StreamEventTypes.Latitude, + data: { + type: ChainEventTypes.StepComplete, + response: response, + }, + }) +} + +function handleIterationError( + controller: ReadableStreamDefaultController, + error: unknown, +) { + const chainError = + error instanceof Error ? error : new Error('An unknown error occurred') + + enqueueChainEvent(controller, { + event: StreamEventTypes.Latitude, + data: { + type: ChainEventTypes.Error, + error: { + name: chainError.name, + message: chainError.message, + stack: chainError.stack, }, - }) - controller.close() + }, + }) + controller.close() - if (error instanceof ZodError) { - throw new UnprocessableEntityError( - 'Error validating document configuration', - error.formErrors.fieldErrors, - ) - } else { - throw error - } + if (error instanceof ZodError) { + throw new UnprocessableEntityError( + 'Error validating document configuration', + error.formErrors.fieldErrors, + ) } } /** * Performs some common operations needed for processing an iteration step **/ -async function doChainStep({ +async function computeStepData({ chain, apikeys, previousResponse, @@ -219,7 +291,7 @@ async function doChainStep({ }: { chain: Chain apikeys: CachedApiKeys - previousResponse?: { text: string; usage: Record } + previousResponse?: ChainTextResponse apiKey?: ProviderApiKey sentCount: number }) { diff --git a/packages/core/src/services/documentLogs/addMessages/index.ts b/packages/core/src/services/documentLogs/addMessages/index.ts index 8409ce12b..53c01e9be 100644 --- a/packages/core/src/services/documentLogs/addMessages/index.ts +++ b/packages/core/src/services/documentLogs/addMessages/index.ts @@ -1,4 +1,5 @@ import { Message, MessageRole } from '@latitude-data/compiler' +import { CoreTool, ObjectStreamPart, TextStreamPart } from 'ai' import { ChainCallResponse, @@ -6,7 +7,9 @@ import { ChainEventTypes, DocumentLog, LogSources, + objectToString, ProviderApiKey, + ProviderData, StreamEventTypes, Workspace, } from '../../../browser' @@ -64,7 +67,9 @@ export async function addMessages({ ...providerLog.messages, { role: MessageRole.assistant, - content: providerLog.responseText, + content: + providerLog.responseText || + objectToString(providerLog.responseObject), toolCalls: providerLog.toolCalls, }, ...messages, @@ -75,10 +80,6 @@ export async function addMessages({ }, }) - // Dummy handling of the response - // This is helpful for not throwing the error - // when no one is listening to the promise - response.then(() => {}).catch(() => {}) return Result.ok({ stream, response }) } @@ -106,22 +107,27 @@ async function streamMessageResponse({ source, }) - for await (const value of streamToGenerator(result.fullStream)) { + for await (const value of streamToGenerator< + TextStreamPart> | ObjectStreamPart + >(result.fullStream)) { enqueueChainEvent(controller, { event: StreamEventTypes.Provider, - data: value, + data: value as unknown as ProviderData, }) } const response = { documentLogUuid, + object: await result.object, text: await result.text, usage: await result.usage, - toolCalls: (await result.toolCalls).map((t) => ({ - id: t.toolCallId, - name: t.toolName, - arguments: t.args, - })), + toolCalls: result.toolCalls + ? (await result.toolCalls).map((t) => ({ + id: t.toolCallId, + name: t.toolName, + arguments: t.args, + })) + : [], } enqueueChainEvent(controller, { event: StreamEventTypes.Latitude, @@ -132,14 +138,22 @@ async function streamMessageResponse({ { role: MessageRole.assistant, toolCalls: response.toolCalls, - content: response.text, + content: response.text || objectToString(response.object), }, ], - response, + response: { + ...response, + text: response.text || objectToString(response.object), + }, }, }) + controller.close() - return response + + return { + ...response, + text: response.text || objectToString(response.object), + } } catch (e) { const error = e as Error enqueueChainEvent(controller, { diff --git a/packages/core/src/services/evaluationResults/create.test.ts b/packages/core/src/services/evaluationResults/create.test.ts index 5b683494d..89cd74667 100644 --- a/packages/core/src/services/evaluationResults/create.test.ts +++ b/packages/core/src/services/evaluationResults/create.test.ts @@ -59,7 +59,10 @@ describe('createEvaluationResult', () => { evaluation, documentLog, providerLog, - result: true, + result: { + result: true, + reason: 'This is a boolean result', + }, }) expect(result.ok).toBe(true) @@ -70,7 +73,6 @@ describe('createEvaluationResult', () => { expect(result.value.providerLogId).toBe(providerLog.id) expect(result.value.resultableType).toBe(EvaluationResultableType.Boolean) expect(result.value.result).toBe(true) - // Verify in database const dbResult = await database.query.evaluationResults.findFirst({ where: eq(evaluationResults.id, result.value.id), @@ -95,7 +97,10 @@ describe('createEvaluationResult', () => { evaluation, documentLog, providerLog, - result: 75, + result: { + result: 75, + reason: 'This is a number result', + }, }) expect(result.ok).toBe(true) @@ -106,7 +111,6 @@ describe('createEvaluationResult', () => { expect(result.value.providerLogId).toBe(providerLog.id) expect(result.value.resultableType).toBe(EvaluationResultableType.Number) expect(result.value.result).toBe(75) - // Verify in database const dbResult = await database.query.evaluationResults.findFirst({ where: eq(evaluationResults.id, result.value.id), @@ -131,7 +135,10 @@ describe('createEvaluationResult', () => { evaluation, documentLog, providerLog, - result: 'This is a text result', + result: { + result: 'This is a text result', + reason: 'Explanation for the text result', + }, }) expect(result.ok).toBe(true) @@ -142,7 +149,6 @@ describe('createEvaluationResult', () => { expect(result.value.providerLogId).toBe(providerLog.id) expect(result.value.resultableType).toBe(EvaluationResultableType.Text) expect(result.value.result).toBe('This is a text result') - // Verify in database const dbResult = await database.query.evaluationResults.findFirst({ where: eq(evaluationResults.id, result.value.id), @@ -168,7 +174,10 @@ describe('createEvaluationResult', () => { evaluation, documentLog, providerLog, - result: 'Some result', + result: { + result: 'Some result', + reason: 'Some reason', + }, }) expect(result.ok).toBe(false) diff --git a/packages/core/src/services/evaluationResults/create.ts b/packages/core/src/services/evaluationResults/create.ts index bcc4d871d..566978ed3 100644 --- a/packages/core/src/services/evaluationResults/create.ts +++ b/packages/core/src/services/evaluationResults/create.ts @@ -17,7 +17,7 @@ export type CreateEvaluationResultProps = { evaluation: Evaluation | EvaluationDto documentLog: DocumentLog providerLog: ProviderLog - result: number | string | boolean + result: { result: number | string | boolean; reason: string } } export async function createEvaluationResult( @@ -44,7 +44,11 @@ export async function createEvaluationResult( ) } - const metadata = await trx.insert(table).values({ result }).returning() + // TODO: store the reason + const metadata = await trx + .insert(table) + .values({ result: result.result }) + .returning() const inserts = await trx .insert(evaluationResults) .values({ diff --git a/packages/core/src/services/evaluations/run.ts b/packages/core/src/services/evaluations/run.ts index fb80db647..7c351e258 100644 --- a/packages/core/src/services/evaluations/run.ts +++ b/packages/core/src/services/evaluations/run.ts @@ -1,6 +1,12 @@ import { createChain, readMetadata } from '@latitude-data/compiler' +import { JSONSchema7 } from 'json-schema' -import { DocumentLog, EvaluationDto, LogSources } from '../../browser' +import { + DocumentLog, + EvaluationDto, + EvaluationResultableType, + LogSources, +} from '../../browser' import { database } from '../../client' import { findLastProviderLogFromDocumentLogUuid } from '../../data-access' import { publisher } from '../../events/publisher' @@ -10,6 +16,20 @@ import { computeDocumentLogWithMetadata } from '../documentLogs' import { buildProviderApikeysMap } from '../providerApiKeys/buildMap' import { formatContext, formatConversation } from '../providerLogs' +// Helper function to get the result schema based on evaluation type +const getResultSchema = (type: EvaluationResultableType): JSONSchema7 => { + switch (type) { + case EvaluationResultableType.Boolean: + return { type: 'boolean' } + case EvaluationResultableType.Number: + return { type: 'number' } + case EvaluationResultableType.Text: + return { type: 'string' } + default: + throw new Error(`Unsupported evaluation type: ${type}`) + } +} + export const runEvaluation = async ( { documentLog, @@ -32,12 +52,11 @@ export const runEvaluation = async ( ) } - const rezult = await computeDocumentLogWithMetadata(documentLog) - if (rezult.error) return rezult - const documentLogWithMetadata = rezult.value + const documentLogWithMetadataResult = + await computeDocumentLogWithMetadata(documentLog) + if (documentLogWithMetadataResult.error) return documentLogWithMetadataResult + const documentLogWithMetadata = documentLogWithMetadataResult.value - // TODO: This will need to become polymorphic in the future given different - // types of evaluations const metadata = await readMetadata({ prompt: documentLog.resolvedContent }) const chain = createChain({ prompt: evaluation.metadata.prompt, @@ -50,21 +69,37 @@ export const runEvaluation = async ( config: metadata.config, duration: documentLogWithMetadata.duration, cost: documentLogWithMetadata.costInMillicents - ? documentLogWithMetadata.costInMillicents * 1000 + ? documentLogWithMetadata.costInMillicents / 1000 : 0, }, }) - const result = await runChain({ + // Use the helper function to get the result schema + const resultSchema = getResultSchema(evaluation.configuration.type) + + const schema: JSONSchema7 = { + type: 'object', + properties: { + result: resultSchema, + reason: { type: 'string' }, + }, + required: ['result', 'reason'], + } + + const chainResult = await runChain({ chain, source: LogSources.Evaluation, apikeys: await buildProviderApikeysMap({ workspaceId: evaluation.workspaceId, }), + configOverrides: { + schema, + output: 'object', + }, }) - if (result.error) return result + if (chainResult.error) return chainResult - result.value.response.then((response) => { + chainResult.value.response.then((response) => { publisher.publish({ type: 'evaluationRun', data: { @@ -76,5 +111,5 @@ export const runEvaluation = async ( }) }) - return result + return chainResult } diff --git a/packages/core/src/services/providerLogs/create.ts b/packages/core/src/services/providerLogs/create.ts index 73a4cb057..a6856a236 100644 --- a/packages/core/src/services/providerLogs/create.ts +++ b/packages/core/src/services/providerLogs/create.ts @@ -1,8 +1,10 @@ import { Message, ToolCall } from '@latitude-data/compiler' import { CompletionTokenUsage } from 'ai' +import { JSONSchema7 } from 'json-schema' import { LogSources, ProviderLog, Providers } from '../../browser' import { database } from '../../client' +import { publisher } from '../../events/publisher' import { Result, Transaction } from '../../lib' import { providerLogs } from '../../schema' import { estimateCost, PartialConfig } from '../ai' @@ -19,7 +21,8 @@ export type CreateProviderLogProps = { model: string config: PartialConfig messages: Message[] - responseText: string + responseText?: string + responseObject?: JSONSchema7 toolCalls?: ToolCall[] usage: CompletionTokenUsage duration: number @@ -38,6 +41,7 @@ export async function createProviderLog( config, messages, responseText, + responseObject, toolCalls, usage, duration, @@ -67,6 +71,7 @@ export async function createProviderLog( config, messages, responseText, + responseObject, toolCalls, tokens: isNaN(usage.totalTokens) ? 0 : (usage.totalTokens ?? 0), costInMillicents: cost, @@ -80,6 +85,11 @@ export async function createProviderLog( await touchProviderApiKey(providerId, trx) if (apiKeyId) await touchApiKey(apiKeyId, trx) + publisher.publishLater({ + type: 'providerLogCreated', + data: log, + }) + return Result.ok(log) }, db) } diff --git a/packages/core/src/services/providerLogs/formatForEvaluation.ts b/packages/core/src/services/providerLogs/formatForEvaluation.ts index 7eb76b0a9..a1defb37a 100644 --- a/packages/core/src/services/providerLogs/formatForEvaluation.ts +++ b/packages/core/src/services/providerLogs/formatForEvaluation.ts @@ -1,23 +1,39 @@ import { MessageRole } from '@latitude-data/compiler' -import { Message, ProviderLog } from '../../browser' - -export function formatConversation(providerLog: ProviderLog) { +import { + Message, + objectToString, + ProviderLog, + ProviderLogDto, +} from '../../browser' + +export function formatConversation(providerLog: ProviderLogDto | ProviderLog) { const messages: Message[] = [...(providerLog.messages || [])] - // Add the response as an assistant message if it exists - if (providerLog.responseText) { + if ((providerLog as ProviderLogDto).response) { + messages.push({ + role: MessageRole.assistant, + content: (providerLog as ProviderLogDto).response, + toolCalls: providerLog.toolCalls, + }) + } else if ((providerLog as ProviderLog).responseText) { messages.push({ role: MessageRole.assistant, - content: providerLog.responseText, + content: (providerLog as ProviderLog).responseText!, toolCalls: providerLog.toolCalls, }) + } else if ((providerLog as ProviderLog).responseObject) { + messages.push({ + role: MessageRole.assistant, + content: objectToString((providerLog as ProviderLog).responseObject), + toolCalls: [], + }) } return formatMessages(messages) } -export function formatContext(providerLog: ProviderLog) { +export function formatContext(providerLog: ProviderLog | ProviderLogDto) { return formatMessages(providerLog.messages) } diff --git a/packages/core/src/tests/factories/evaluationResults.ts b/packages/core/src/tests/factories/evaluationResults.ts index b0555b468..8f7a9fc8e 100644 --- a/packages/core/src/tests/factories/evaluationResults.ts +++ b/packages/core/src/tests/factories/evaluationResults.ts @@ -88,7 +88,10 @@ export async function createEvaluationResult({ evaluation, documentLog, providerLog: providerLogs[providerLogs.length - 1]!, - result: mockedResponse, + result: { + result: mockedResponse, + reason: 'I do not even know to be honest.', + }, }) return { diff --git a/packages/jobs/src/job-definitions/batchEvaluations/runEvaluationJob.ts b/packages/jobs/src/job-definitions/batchEvaluations/runEvaluationJob.ts index 745f37b81..019033330 100644 --- a/packages/jobs/src/job-definitions/batchEvaluations/runEvaluationJob.ts +++ b/packages/jobs/src/job-definitions/batchEvaluations/runEvaluationJob.ts @@ -40,13 +40,11 @@ export const runEvaluationJob = async (job: Job) => { .find(evaluationId) .then((r) => r.unwrap()) - const { response } = await runEvaluation({ + await runEvaluation({ documentLog, evaluation, }).then((r) => r.unwrap()) - await response - await progressTracker.incrementCompleted() } catch (error) { await progressTracker.incrementErrors() diff --git a/packages/jobs/src/queues/index.ts b/packages/jobs/src/queues/index.ts index ce1f902df..3b9f85c06 100644 --- a/packages/jobs/src/queues/index.ts +++ b/packages/jobs/src/queues/index.ts @@ -1,3 +1,4 @@ +import { createProviderLogJob } from '@latitude-data/core/events/handlers/createProviderLogJob' import { EventHandlers } from '@latitude-data/core/events/handlers/index' import { Job, JobsOptions, Queue, QueueEvents } from 'bullmq' @@ -66,7 +67,12 @@ function setupQueue({ export const QUEUES = { [Queues.defaultQueue]: { name: Queues.defaultQueue, - jobs: [runBatchEvaluationJob, runDocumentJob, runEvaluationJob], + jobs: [ + runBatchEvaluationJob, + runDocumentJob, + runEvaluationJob, + createProviderLogJob, + ], }, [Queues.eventsQueue]: { name: Queues.eventsQueue, diff --git a/packages/sdks/typescript/src/index.ts b/packages/sdks/typescript/src/index.ts index 1f957edae..74f0f0c9c 100644 --- a/packages/sdks/typescript/src/index.ts +++ b/packages/sdks/typescript/src/index.ts @@ -1,5 +1,5 @@ import type { Message } from '@latitude-data/compiler' -import { LogSources } from '@latitude-data/core/browser' +import { ChainCallResponse, LogSources } from '@latitude-data/core/browser' import env from '$sdk/env' import { GatewayApiConfig, RouteResolver } from '$sdk/utils' import { @@ -9,15 +9,6 @@ import { UrlParams, } from '$sdk/utils/types' -type ChainCallResponse = { - documentLogUuid: string - text: string - usage: { - promptTokens: number - completionTokens: number - totalTokens: number - } -} export type StreamChainResponse = { conversation: Message[] response: ChainCallResponse @@ -115,7 +106,7 @@ export class LatitudeSdk { const reader = body.getReader() const conversation: Message[] = [] - let chainResponse: ChainCallResponse | null = null + let chainResponse: ChainCallResponse while (true) { const { done, value } = await reader.read() @@ -131,7 +122,7 @@ export class LatitudeSdk { // in a new package. Now they are in `core` but I don't want to depend on core // just for this. We need these enums in production not only as a dev dependency. if (chunk.event === 'latitude-event') { - const messages = 'messages' in chunk.data ? chunk.data.messages : [] + const messages = 'messages' in chunk.data ? chunk.data.messages! : [] if (messages.length > 0) { // At the moment all message.content should be a string // but in the future this could be an image or other type diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a957174d5..8d4afa1b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -615,6 +615,9 @@ importers: '@t3-oss/env-core': specifier: ^0.11.1 version: 0.11.1(typescript@5.6.2)(zod@3.23.8) + '@types/json-schema': + specifier: ^7.0.15 + version: 7.0.15 '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 @@ -654,6 +657,9 @@ importers: jose: specifier: ^5.8.0 version: 5.9.2 + json-schema: + specifier: ^0.4.0 + version: 0.4.0 lodash-es: specifier: ^4.17.21 version: 4.17.21