mirror of
https://github.com/kennethnym/freya
synced 2026-07-04 15:11:15 +01:00
feat: add conversation schemas
This commit is contained in:
@@ -1,14 +1,24 @@
|
||||
import type { Context, Hono } from "hono"
|
||||
|
||||
import { ConversationEntryVisibility } from "@freya/core"
|
||||
import {
|
||||
AssistantMessagePayload,
|
||||
AttachmentPayload,
|
||||
ConversationEntryKind,
|
||||
ConversationEntryVisibility,
|
||||
ContextSummaryPayload,
|
||||
GenericObjectPayload,
|
||||
ToolCallPayload,
|
||||
ToolResultPayload,
|
||||
UserMessagePayload,
|
||||
type Conversation,
|
||||
type ConversationEntry,
|
||||
} from "@freya/core"
|
||||
import { type } from "arktype"
|
||||
import type { Context, Hono } from "hono"
|
||||
import { createMiddleware } from "hono/factory"
|
||||
|
||||
import type { AuthSessionMiddleware } from "../auth/session-middleware.ts"
|
||||
import type { Database } from "../db/index.ts"
|
||||
import type { ConversationRow } from "./storage.ts"
|
||||
|
||||
import { ConversationNotFoundError } from "./errors.ts"
|
||||
import type { ConversationEntryRow, ConversationRow } from "./storage.ts"
|
||||
import { conversations } from "./storage.ts"
|
||||
|
||||
/** Hono environment populated by the conversations route middleware. */
|
||||
@@ -18,19 +28,20 @@ type Env = {
|
||||
}
|
||||
}
|
||||
|
||||
/** Serialized conversation summary returned by the list endpoint. */
|
||||
interface ConversationSummaryResponse {
|
||||
id: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/** Dependencies required to register conversation HTTP handlers. */
|
||||
interface ConversationsHttpHandlersDeps {
|
||||
db: Database
|
||||
authSessionMiddleware: AuthSessionMiddleware
|
||||
}
|
||||
|
||||
interface ListConversationsResponse {
|
||||
conversations: Conversation[]
|
||||
}
|
||||
|
||||
interface ListConversationEntriesResponse {
|
||||
entries: ConversationEntry[]
|
||||
}
|
||||
|
||||
const ConversationIdParam = type("string.uuid")
|
||||
|
||||
export function registerConversationsHttpHandlers(
|
||||
@@ -49,12 +60,13 @@ export function registerConversationsHttpHandlers(
|
||||
async function handleListConversations(c: Context<Env>) {
|
||||
const user = c.get("user")!
|
||||
const db = c.get("db")
|
||||
|
||||
return c.json({
|
||||
const response: ListConversationsResponse = {
|
||||
conversations: (await conversations(db, user.id).listConversations()).map(
|
||||
serializeConversation,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return c.json(response)
|
||||
}
|
||||
|
||||
async function handleListEntries(c: Context<Env>) {
|
||||
@@ -73,20 +85,11 @@ async function handleListEntries(c: Context<Env>) {
|
||||
const entries = await conversations(db, user.id).listEntries(parsedConversationId, {
|
||||
visibility: ConversationEntryVisibility.UserVisible,
|
||||
})
|
||||
const response: ListConversationEntriesResponse = {
|
||||
entries: entries.map(serializeConversationEntry),
|
||||
}
|
||||
|
||||
return c.json({
|
||||
entries: entries.map((row) => ({
|
||||
id: row.id,
|
||||
conversationId: row.conversationId,
|
||||
sequence: row.sequence,
|
||||
kind: row.kind,
|
||||
visibility: row.visibility,
|
||||
fileId: row.fileId,
|
||||
payload: row.payload,
|
||||
metadata: row.metadata,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
})),
|
||||
})
|
||||
return c.json(response)
|
||||
} catch (err) {
|
||||
if (err instanceof ConversationNotFoundError) {
|
||||
return c.json({ error: "Conversation not found" }, 404)
|
||||
@@ -95,10 +98,89 @@ async function handleListEntries(c: Context<Env>) {
|
||||
}
|
||||
}
|
||||
|
||||
function serializeConversation(row: ConversationRow): ConversationSummaryResponse {
|
||||
function serializeConversation(row: ConversationRow): Conversation {
|
||||
return {
|
||||
id: row.id,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
function serializeConversationEntry(row: ConversationEntryRow): ConversationEntry {
|
||||
const base = {
|
||||
id: row.id,
|
||||
conversationId: row.conversationId,
|
||||
sequence: row.sequence,
|
||||
visibility: row.visibility,
|
||||
metadata: row.metadata,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
}
|
||||
|
||||
switch (row.kind) {
|
||||
case ConversationEntryKind.UserMessage:
|
||||
return {
|
||||
...base,
|
||||
kind: row.kind,
|
||||
fileId: nullFileId(row),
|
||||
payload: UserMessagePayload.assert(row.payload),
|
||||
}
|
||||
case ConversationEntryKind.AssistantMessage:
|
||||
return {
|
||||
...base,
|
||||
kind: row.kind,
|
||||
fileId: nullFileId(row),
|
||||
payload: AssistantMessagePayload.assert(row.payload),
|
||||
}
|
||||
case ConversationEntryKind.Attachment:
|
||||
return {
|
||||
...base,
|
||||
kind: row.kind,
|
||||
fileId: requireFileId(row),
|
||||
payload: AttachmentPayload.assert(row.payload),
|
||||
}
|
||||
case ConversationEntryKind.ToolCall:
|
||||
return {
|
||||
...base,
|
||||
kind: row.kind,
|
||||
fileId: nullFileId(row),
|
||||
payload: ToolCallPayload.assert(row.payload),
|
||||
}
|
||||
case ConversationEntryKind.ToolResult:
|
||||
return {
|
||||
...base,
|
||||
kind: row.kind,
|
||||
fileId: nullFileId(row),
|
||||
payload: ToolResultPayload.assert(row.payload),
|
||||
}
|
||||
case ConversationEntryKind.ContextSummary:
|
||||
return {
|
||||
...base,
|
||||
kind: row.kind,
|
||||
fileId: nullFileId(row),
|
||||
payload: ContextSummaryPayload.assert(row.payload),
|
||||
}
|
||||
case ConversationEntryKind.SystemNote:
|
||||
return {
|
||||
...base,
|
||||
kind: row.kind,
|
||||
fileId: nullFileId(row),
|
||||
payload: GenericObjectPayload.assert(row.payload),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function requireFileId(row: ConversationEntryRow): string {
|
||||
if (!row.fileId) {
|
||||
throw new Error(`Conversation attachment entry "${row.id}" is missing a file id`)
|
||||
}
|
||||
|
||||
return row.fileId
|
||||
}
|
||||
|
||||
function nullFileId(row: ConversationEntryRow): null {
|
||||
if (row.fileId !== null) {
|
||||
throw new Error(`Conversation entry "${row.id}" unexpectedly references a file`)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AssistantMessagePayload, UserMessagePayload } from "@freya/core"
|
||||
import { AssistantMessagePayload, ConversationEntry, UserMessagePayload } from "@freya/core"
|
||||
import { FlashList } from "@shopify/flash-list"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { View, ViewStyle } from "react-native"
|
||||
@@ -6,7 +6,6 @@ import tw from "twrnc"
|
||||
|
||||
import { SansSerifText } from "@/components/ui/sans-serif-text"
|
||||
|
||||
import { ConversationEntry } from "./conversations"
|
||||
import { useListConversationEntriesQuery, useDefaultConversationQuery } from "./queries"
|
||||
|
||||
type ConversationListProps = Omit<
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import {
|
||||
ConversationEntryKind,
|
||||
ConversationEntryPayload,
|
||||
ConversationEntryVisibility,
|
||||
} from "@freya/core"
|
||||
import { type } from "arktype"
|
||||
|
||||
export const Conversation = type({
|
||||
id: "string.uuid",
|
||||
createdAt: "string.date.iso",
|
||||
updatedAt: "string.date.iso",
|
||||
})
|
||||
|
||||
export const ConversationEntry = type({
|
||||
id: "string.uuid",
|
||||
sequence: "number",
|
||||
kind: type.enumerated(...Object.values(ConversationEntryKind)),
|
||||
visibility: type.enumerated(...Object.values(ConversationEntryVisibility)),
|
||||
fileId: "string | null",
|
||||
payload: ConversationEntryPayload,
|
||||
})
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Conversation, ConversationEntry } from "@freya/core"
|
||||
import { queryOptions, skipToken } from "@tanstack/react-query"
|
||||
import { type } from "arktype"
|
||||
|
||||
import { useApiClient } from "@/api/client"
|
||||
|
||||
import { Conversation, ConversationEntry } from "./conversations"
|
||||
|
||||
const ListConversationsResponse = type({
|
||||
"+": "reject",
|
||||
conversations: Conversation.array(),
|
||||
})
|
||||
|
||||
const ConversationEntriesResponse = type({
|
||||
"+": "reject",
|
||||
entries: ConversationEntry.array(),
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user