Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/publish typescript sdk #250

Merged
merged 1 commit into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Changesets

Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)

We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
11 changes: 11 additions & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
5 changes: 5 additions & 0 deletions .changeset/rare-cougars-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@latitude-data/sdk-js': patch
---

First release! Introduces `run` and `chat` commands in order interact with Latitude's Gateway
2 changes: 2 additions & 0 deletions apps/gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@
"drizzle-orm": "^0.33.0",
"hono": "^4.5.3",
"jet-paths": "^1.0.6",
"lodash-es": "^4.17.21",
"zod": "^3.23.8"
},
"devDependencies": {
"@latitude-data/eslint-config": "workspace:^",
"@latitude-data/typescript-config": "workspace:^",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.5.1",
"@types/uuid": "^10.0.0",
"tsup": "^8.2.4",
Expand Down
2 changes: 1 addition & 1 deletion apps/gateway/src/common/parseSSEEvent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export function parseSSEEvent(data?: string) {
export function parseSSEvent(data?: string) {
if (!data) return

const event = data
Expand Down
16 changes: 8 additions & 8 deletions apps/gateway/src/routes/api/v1/chats/handlers/addMessage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { database } from '@latitude-data/core/client'
import { createProject } from '@latitude-data/core/factories'
import { Result } from '@latitude-data/core/lib/Result'
import { apiKeys } from '@latitude-data/core/schema'
import { parseSSEEvent } from '$/common/parseSSEEvent'
import { parseSSEvent } from '$/common/parseSSEEvent'
import app from '$/routes/app'
import { eq } from 'drizzle-orm'
import { testConsumeStream } from 'test/helpers'
Expand Down Expand Up @@ -77,7 +77,7 @@ describe('POST /add-message', () => {
const res = await app.request('/api/v1/chats/add-message', {
method: 'POST',
body: JSON.stringify({
documentPath: '/path/to/document',
path: '/path/to/document',
}),
})

Expand All @@ -101,8 +101,7 @@ describe('POST /add-message', () => {
route = '/api/v1/chats/add-message'
body = JSON.stringify({
messages: [],
source: LogSources.Playground,
documentLogUuid: 'fake-document-log-uuid',
uuid: 'fake-document-log-uuid',
})
headers = {
Authorization: `Bearer ${token}`,
Expand All @@ -118,13 +117,14 @@ describe('POST /add-message', () => {
})

let { done, value } = await testConsumeStream(res.body as ReadableStream)
value = parseSSEEvent(value!)!.data
const event = parseSSEvent(value!)

expect(mocks.queues)
expect(res.status).toBe(200)
expect(res.body).toBeInstanceOf(ReadableStream)
expect(done).toBe(true)
expect(value).toEqual({
expect(event).toEqual({
id: 0,
event: StreamEventTypes.Latitude,
data: {
type: ChainEventTypes.Complete,
Expand All @@ -136,14 +136,14 @@ describe('POST /add-message', () => {
})
})

it('calls addMessages provider', async () => {
it('calls chat provider', async () => {
await app.request(route, { method: 'POST', body, headers })

expect(mocks.addMessages).toHaveBeenCalledWith({
workspace,
documentLogUuid: 'fake-document-log-uuid',
messages: [],
source: LogSources.Playground,
source: LogSources.API,
})
})
})
Expand Down
24 changes: 17 additions & 7 deletions apps/gateway/src/routes/api/v1/chats/handlers/addMessage.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { zValidator } from '@hono/zod-validator'
import { LogSources } from '@latitude-data/core/browser'
import { streamToGenerator } from '@latitude-data/core/lib/streamToGenerator'
import { addMessages } from '@latitude-data/core/services/documentLogs/index'
import { captureException } from '@sentry/node'
import { messageSchema } from '$/common/messageSchema'
import { pipeToStream } from '$/common/pipeToStream'
import { Factory } from 'hono/factory'
import { streamSSE } from 'hono/streaming'
import { z } from 'zod'

import { chainEventPresenter } from '../../projects/:projectId/commits/:commitUuid/documents/handlers/_shared'

const factory = new Factory()

const schema = z.object({
messages: z.array(messageSchema),
documentLogUuid: z.string(),
source: z.nativeEnum(LogSources).optional().default(LogSources.API),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public API has to be the simplest possible

uuid: z.string(),
})

export const addMessageHandler = factory.createHandlers(
Expand All @@ -22,17 +23,26 @@ export const addMessageHandler = factory.createHandlers(
return streamSSE(
c,
async (stream) => {
const { documentLogUuid, messages, source } = c.req.valid('json')
const { uuid, messages } = c.req.valid('json')
const workspace = c.get('workspace')

const result = await addMessages({
workspace,
documentLogUuid,
documentLogUuid: uuid,
messages,
source,
source: LogSources.API,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is not technically correct because we use ts sdk in playground but honestly i don't care

}).then((r) => r.unwrap())

await pipeToStream(stream, result.stream)
let id = 0
for await (const event of streamToGenerator(result.stream)) {
const data = chainEventPresenter(event)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internally nothing has really changed but from the public API perspective I don't want to expose users to concepts such as providerlogs or document logs, and thus I'm introducing event presenters to filter this information out


stream.writeSSE({
id: String(id++),
event: event.event,
data: typeof data === 'string' ? data : JSON.stringify(data),
})
}
},
(error: Error) => {
captureException(error)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import type { Workspace } from '@latitude-data/core/browser'
import { omit } from 'lodash-es'

import { Message } from '@latitude-data/compiler'
import {
ChainEventDto,
ChainEventTypes,
LatitudeEventData,
StreamEventTypes,
type ChainEvent,
type Workspace,
} from '@latitude-data/core/browser'
import { BadRequestError } from '@latitude-data/core/lib/errors'
import { Result } from '@latitude-data/core/lib/Result'
import {
CommitsRepository,
DocumentVersionsRepository,
ProjectsRepository,
} from '@latitude-data/core/repositories'
import { Config } from '@latitude-data/core/services/ai/helpers'

export const getData = async ({
workspace,
Expand All @@ -19,16 +32,65 @@ export const getData = async ({
const projectsScope = new ProjectsRepository(workspace.id)
const commitsScope = new CommitsRepository(workspace.id)
const docsScope = new DocumentVersionsRepository(workspace.id)
const project = await projectsScope
.getProjectById(projectId)
.then((r) => r.unwrap())
const commit = await commitsScope
.getCommitByUuid({ projectId: project.id, uuid: commitUuid })
.then((r) => r.unwrap())

const document = await docsScope
.getDocumentByPath({ commit, path: documentPath })
.then((r) => r.unwrap())

return { project, commit, document }

const projectResult = await projectsScope.getProjectById(projectId)
if (projectResult.error) return projectResult
const project = projectResult.value

const commitResult = await commitsScope.getCommitByUuid({
projectId: project.id,
uuid: commitUuid,
})
if (commitResult.error) return commitResult
const commit = commitResult.value

const documentResult = await docsScope.getDocumentByPath({
commit,
path: documentPath,
})
if (documentResult.error) return documentResult
const document = documentResult.value

return Result.ok({ project, commit, document })
}

export function chainEventPresenter(event: ChainEvent) {
switch (event.event) {
case StreamEventTypes.Provider:
return event.data
case StreamEventTypes.Latitude:
return latitudeEventPresenter(event)
}
}

function latitudeEventPresenter(event: {
data: LatitudeEventData
event: StreamEventTypes.Latitude
}): ChainEventDto | string {
switch (event.data.type) {
case ChainEventTypes.Step:
case ChainEventTypes.StepComplete:
return {
...omit(event.data, 'documentLogUuid'),
uuid: event.data.documentLogUuid!,
} as {
type: ChainEventTypes.Step
config: Config
isLastStep: boolean
messages: Message[]
uuid?: string
}
case ChainEventTypes.Complete:
return {
...omit(event.data, 'documentLogUuid'),
uuid: event.data.response.documentLogUuid!,
response: omit(event.data.response, 'providerLog', 'documentLogUuid'),
}
case ChainEventTypes.Error:
return event.data
default:
throw new BadRequestError(
`Unknown event type in chainEventPresenter ${JSON.stringify(event)}`,
)
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this probably can be further simplified in the future

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const getHandler = factory.createHandlers(async (c) => {
projectId: Number(projectId!),
commitUuid: commitUuid!,
documentPath: documentPath!,
})
}).then((r) => r.unwrap())

return c.json(document)
})
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import { Result } from '@latitude-data/core/lib/Result'
import { apiKeys } from '@latitude-data/core/schema'
import { mergeCommit } from '@latitude-data/core/services/commits/merge'
import { parseSSEEvent } from '$/common/parseSSEEvent'
import { parseSSEvent } from '$/common/parseSSEEvent'
import app from '$/routes/app'
import { eq } from 'drizzle-orm'
import { testConsumeStream } from 'test/helpers'
Expand Down Expand Up @@ -104,7 +104,7 @@ describe('POST /run', () => {

route = `/api/v1/projects/${project!.id}/commits/${commit!.uuid}/documents/run`
body = JSON.stringify({
documentPath: document.documentVersion.path,
path: document.documentVersion.path,
parameters: {},
})
headers = {
Expand Down Expand Up @@ -153,12 +153,13 @@ describe('POST /run', () => {
})

let { done, value } = await testConsumeStream(res.body as ReadableStream)
value = parseSSEEvent(value)!.data
const event = parseSSEvent(value)
expect(mocks.queues)
expect(res.status).toBe(200)
expect(res.body).toBeInstanceOf(ReadableStream)
expect(done).toBe(true)
expect(value).toEqual({
expect(event).toEqual({
id: 0,
event: StreamEventTypes.Latitude,
data: {
type: ChainEventTypes.Complete,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { zValidator } from '@hono/zod-validator'
import { LogSources } from '@latitude-data/core/browser'
import { streamToGenerator } from '@latitude-data/core/lib/streamToGenerator'
import { runDocumentAtCommit } from '@latitude-data/core/services/commits/runDocumentAtCommit'
import { pipeToStream } from '$/common/pipeToStream'
import { captureException } from '$/common/sentry'
import { Factory } from 'hono/factory'
import { streamSSE } from 'hono/streaming'
import { z } from 'zod'

import { getData } from './_shared'
import { chainEventPresenter, getData } from './_shared'

const factory = new Factory()

const runSchema = z.object({
documentPath: z.string(),
path: z.string(),
parameters: z.record(z.any()).optional().default({}),
source: z.nativeEnum(LogSources).optional().default(LogSources.API),
})

export const runHandler = factory.createHandlers(
Expand All @@ -24,23 +23,32 @@ export const runHandler = factory.createHandlers(
c,
async (stream) => {
const { projectId, commitUuid } = c.req.param()
const { documentPath, parameters, source } = c.req.valid('json')
const { path, parameters } = c.req.valid('json')
const workspace = c.get('workspace')
const { document, commit } = await getData({
workspace,
projectId: Number(projectId!),
commitUuid: commitUuid!,
documentPath: documentPath!,
})
documentPath: path!,
}).then((r) => r.unwrap())
const result = await runDocumentAtCommit({
workspace,
document,
commit,
parameters,
source,
source: LogSources.API,
}).then((r) => r.unwrap())

await pipeToStream(stream, result.stream)
let id = 0
for await (const event of streamToGenerator(result.stream)) {
const data = chainEventPresenter(event)

stream.writeSSE({
id: String(id++),
event: event.event,
data: typeof data === 'string' ? data : JSON.stringify(data),
})
}
},
(error: Error) => {
captureException(error)
Expand Down
10 changes: 10 additions & 0 deletions apps/gateway/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { serve } from '@hono/node-server'
import env from '$/common/env'
import app from '$/routes/app'

import { captureException, captureMessage } from './common/sentry'

serve(
{
fetch: app.fetch,
Expand All @@ -21,3 +23,11 @@ function gracefulShutdown() {

process.on('SIGTERM', gracefulShutdown)
process.on('SIGINT', gracefulShutdown)

process.on('uncaughtException', function (err) {
captureException(err)
})

process.on('unhandledRejection', (reason: string) => {
captureMessage(reason)
})
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

important otherwise gateway doesn't recover from any unexpected error (hono is a bit crap honestly)

Loading
Loading