Skip to content

Commit

Permalink
feature: suggest evaluations form our templates with AI (#306)
Browse files Browse the repository at this point in the history
  • Loading branch information
geclos authored Sep 30, 2024
1 parent f13d64e commit bcaf89c
Show file tree
Hide file tree
Showing 12 changed files with 546 additions and 24 deletions.
5 changes: 5 additions & 0 deletions apps/infra/src/app/production/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,5 +128,10 @@ export const environment = pulumi
},
{ name: 'DATASET_GENERATOR_PROJECT_ID', value: '74' },
{ name: 'DATASET_GENERATOR_DOCUMENT_PATH', value: 'generator' },
{
name: 'TEMPLATES_SUGGESTION_PROMPT_PATH',
value: 'evaluation-template-suggestions',
},
{ name: 'TEMPLATES_SUGGESTION_PROJECT_ID', value: '60' },
]
})
48 changes: 27 additions & 21 deletions apps/infra/src/app/production/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,29 +137,35 @@ new aws.lb.ListenerRule('LatitudeLLMAppListenerRule', {

const cluster = coreStack.requireOutput('cluster') as pulumi.Output<Cluster>

const ecsService = new aws.ecs.Service('LatitudeLLMApp', {
cluster: cluster.arn,
taskDefinition: taskDefinition.arn,
desiredCount: 2,
launchType: 'FARGATE',
forceNewDeployment: true,
enableExecuteCommand: true,
deploymentController: {
type: 'CODE_DEPLOY',
const ecsService = new aws.ecs.Service(
'LatitudeLLMApp',
{
cluster: cluster.arn,
taskDefinition: taskDefinition.arn,
desiredCount: 2,
launchType: 'FARGATE',
forceNewDeployment: true,
enableExecuteCommand: true,
deploymentController: {
type: 'CODE_DEPLOY',
},
networkConfiguration: {
subnets: privateSubnets.ids,
assignPublicIp: false,
securityGroups: [ecsSecurityGroup],
},
loadBalancers: [
{
targetGroupArn: blueTargetGroup.arn,
containerName,
containerPort: 8080,
},
],
},
networkConfiguration: {
subnets: privateSubnets.ids,
assignPublicIp: false,
securityGroups: [ecsSecurityGroup],
{
ignoreChanges: ['taskDefinition'], // CodeDeploy controls the task definition that is deployed
},
loadBalancers: [
{
targetGroupArn: blueTargetGroup.arn,
containerName,
containerPort: 8080,
},
],
})
)

const codeDeployApp = new aws.codedeploy.Application(
'LatitudeLLMCodeDeployApp',
Expand Down
2 changes: 1 addition & 1 deletion apps/web/appspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: arn:aws:ecs:eu-central-1:442420265876:task-definition/LatitudeLLMTaskFamily:92
TaskDefinition: arn:aws:ecs:eu-central-1:442420265876:task-definition/LatitudeLLMTaskFamily:96
LoadBalancerInfo:
ContainerName: 'LatitudeLLMAppContainer'
ContainerPort: 8080
70 changes: 70 additions & 0 deletions apps/web/src/actions/evaluations/generateSuggestedEvaluations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use server'

import { createHash } from 'crypto'

import { ChainObjectResponse } from '@latitude-data/core/browser'
import { cache } from '@latitude-data/core/cache'
import { findAllEvaluationTemplates } from '@latitude-data/core/data-access'
import { BadRequestError } from '@latitude-data/core/lib/errors'
import { createSdk } from '$/app/(private)/_lib/createSdk'
import env from '$/env'
import { SuggestedEvaluation } from '$/stores/suggestedEvaluations'
import { z } from 'zod'

import { authProcedure } from '../procedures'

export const generateSuggestedEvaluationsAction = authProcedure
.createServerAction()
.input(
z.object({
documentContent: z.string(),
}),
)
.handler(async ({ input }) => {
if (!env.DATASET_GENERATOR_WORKSPACE_APIKEY) {
throw new BadRequestError('DATASET_GENERATOR_WORKSPACE_APIKEY is not set')
}
if (!env.TEMPLATES_SUGGESTION_PROJECT_ID) {
throw new BadRequestError('TEMPLATES_SUGGESTION_PROJECT_ID is not set')
}
if (!env.TEMPLATES_SUGGESTION_PROMPT_PATH) {
throw new BadRequestError('TEMPLATES_SUGGESTION_PROMPT_PATH is not set')
}

const cacheInstance = await cache()
const contentHash = createHash('sha1')
.update(input.documentContent)
.digest('hex')
const cacheKey = `suggested_evaluations:${contentHash}`

const cachedResult = await cacheInstance.get(cacheKey)
if (cachedResult) {
return JSON.parse(cachedResult) as SuggestedEvaluation[]
}

const templates = await findAllEvaluationTemplates().then((r) => r.unwrap())
const templateString = templates
.map((t) => `${t.id}\n${t.name}\n${t.description}\n`)
.join('\n')
const sdk = await createSdk({
apiKey: env.DATASET_GENERATOR_WORKSPACE_APIKEY,
projectId: env.TEMPLATES_SUGGESTION_PROJECT_ID,
}).then((r) => r.unwrap())
const result = await sdk.run(env.TEMPLATES_SUGGESTION_PROMPT_PATH, {
parameters: {
templates: templateString,
prompt: input.documentContent,
},
})

if (!result) return []

const res = result.response as ChainObjectResponse
if (!res.object) return []

const suggestedEvaluations = res.object[0] as SuggestedEvaluation[]

await cacheInstance.set(cacheKey, JSON.stringify(suggestedEvaluations))

return suggestedEvaluations
})
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,102 @@
import { EvaluationDto } from '@latitude-data/core/browser'
import {
BlankSlateStep,
BlankSlateStepSkeleton,
BlankSlateWithSteps,
Button,
Icon,
TableBlankSlate,
Text,
useCurrentCommit,
useCurrentDocument,
useCurrentProject,
} from '@latitude-data/web-ui'
import { connectEvaluationsAction } from '$/actions/evaluations/connect'
import useLatitudeAction from '$/hooks/useLatitudeAction'
import { useNavigate } from '$/hooks/useNavigate'
import { ROUTES } from '$/services/routes'
import useEvaluations from '$/stores/evaluations'
import useSuggestedEvaluations, {
SuggestedEvaluation,
} from '$/stores/suggestedEvaluations'
import Link from 'next/link'

import BatchEvaluationsTable from './BatchEvaluationsTable'

function SuggestedEvaluations() {
const document = useCurrentDocument()
const { project } = useCurrentProject()
const { commit } = useCurrentCommit()
const { data: suggestions, isLoading } = useSuggestedEvaluations(
document.content,
)
const navigate = useNavigate()
const { mutate } = useEvaluations()
const { execute, isPending } = useLatitudeAction(connectEvaluationsAction)
const onConnect = async (suggestion: SuggestedEvaluation) => {
const [data] = await execute({
projectId: project.id,
templateIds: [suggestion.id],
evaluationUuids: [],
documentUuid: document.documentUuid,
})

if (data) {
mutate()
const connectedEvaluation = data[0]!
navigate.push(
ROUTES.projects
.detail({ id: project.id })
.commits.detail({ uuid: commit.uuid })
.documents.detail({ uuid: document.documentUuid })
.evaluations.detail(connectedEvaluation.evaluationId).root,
)
}
}

if (isLoading) {
return (
<BlankSlateStepSkeleton className='w-[448px] animate-in fade-in duration-300 max-h-[360px] overflow-y-auto' />
)
}
if (!suggestions.length) return null

return (
<BlankSlateStep
number={3}
title='...Or try our suggested evaluations'
description='Our AI agent recommends starting with these evaluations based on the contents of your prompt.'
className='animate-in fade-in duration-300 max-h-[360px] over overflow-y-auto'
>
<div className='space-y-4'>
{suggestions.map((suggestion, index) => (
<div
key={index}
className='flex flex-row items-center justify-between gap-4 p-4 border border-border rounded-md'
>
<div className='flex flex-col gap-1'>
<Text.H5M>{suggestion.title}</Text.H5M>
<Text.H6 color='foregroundMuted'>
{suggestion.description}
</Text.H6>
</div>
<Button
fancy
variant='secondary'
disabled={isPending}
onClick={() => onConnect(suggestion)}
>
<Text.H5 noWrap color='foreground'>
Add Evaluation
</Text.H5>
</Button>
</div>
))}
</div>
</BlankSlateStep>
)
}

export default function EvaluationsLayoutClient({
evaluations: fallbackData,
}: {
Expand Down Expand Up @@ -71,6 +154,8 @@ export default function EvaluationsLayoutClient({
</Link>
</div>
</BlankSlateStep>

<SuggestedEvaluations />
</BlankSlateWithSteps>
)
}
6 changes: 6 additions & 0 deletions apps/web/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export default createEnv({
DATASET_GENERATOR_PROJECT_ID: z.coerce.number().optional(),
DATASET_GENERATOR_DOCUMENT_PATH: z.string().optional(),
DATASET_GENERATOR_WORKSPACE_APIKEY: z.string().optional(),
TEMPLATES_SUGGESTION_PROJECT_ID: z.coerce.number().optional(),
TEMPLATES_SUGGESTION_PROMPT_PATH: z.string().optional(),
},
runtimeEnv: {
NODE_ENV: process.env.NODE_ENV,
Expand All @@ -33,5 +35,9 @@ export default createEnv({
process.env.DATASET_GENERATOR_DOCUMENT_PATH,
DATASET_GENERATOR_WORKSPACE_APIKEY:
process.env.DATASET_GENERATOR_WORKSPACE_APIKEY,
TEMPLATES_SUGGESTION_PROJECT_ID:
process.env.TEMPLATES_SUGGESTION_PROJECT_ID,
TEMPLATES_SUGGESTION_PROMPT_PATH:
process.env.TEMPLATES_SUGGESTION_PROMPT_PATH,
},
})
40 changes: 40 additions & 0 deletions apps/web/src/stores/suggestedEvaluations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client'

import { generateSuggestedEvaluationsAction } from '$/actions/evaluations/generateSuggestedEvaluations'
import useSWR, { SWRConfiguration } from 'swr'

export interface SuggestedEvaluation {
id: number
title: string
description: string
}

export default function useSuggestedEvaluations(
documentContent?: string | null,
opts?: SWRConfiguration,
) {
const { data, error, isLoading } = useSWR<SuggestedEvaluation[]>(
[
'suggestedEvaluations',
documentContent ? documentContent.slice(-100) : null,
],
async () => {
if (!documentContent) return []

const [data, error] = await generateSuggestedEvaluationsAction({
documentContent,
})

if (error) return []

return data
},
opts,
)

return {
data: data || [],
isLoading,
error,
}
}
Loading

0 comments on commit bcaf89c

Please sign in to comment.