Skip to content
Open
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
40 changes: 22 additions & 18 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,34 +23,33 @@
"clean": "rm -rf ./dist"
},
"dependencies": {
"@ai-sdk/google": "^2.0.51",
"@ai-sdk/openai": "^2.0.23",
"@ai-sdk/openai-compatible": "^1.0.18",
"@ai-sdk/google": "^3.0.33",
"@ai-sdk/openai": "^3.0.35",
"@ai-sdk/openai-compatible": "^2.0.30",
"@azimutt/inspector-core": "file:../packages/core",
"@azimutt/utils": "^0.1.8",
"@modelcontextprotocol/sdk": "^1.21.1",
"@nestjs/common": "^11.0.10",
"@nestjs/core": "^11.0.10",
"@nestjs/event-emitter": "^3.0.0",
"@nestjs/platform-express": "^11.0.10",
"@nestjs/schedule": "^5.0.1",
"@nestjs/schedule": "^6.1.1",
"@nestjs/serve-static": "^5.0.3",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/resources": "^2.0.1",
"@opentelemetry/sdk-metrics": "^2.0.1",
"@opentelemetry/sdk-node": "^0.201.0",
"@opentelemetry/resources": "^2.5.1",
"@opentelemetry/sdk-metrics": "^2.5.1",
"@opentelemetry/sdk-node": "^0.212.0",
"@opentelemetry/semantic-conventions": "^1.33.0",
"ai": "^5.0.29",
"ai": "^6.0.103",
"bcryptjs": "^3.0.2",
"better-sqlite3": "^11.9.1",
"blocked-at": "^1.2.0",
"cookie-parser": "^1.4.7",
"dotenv": "^16.4.7",
"dotenv": "^17.3.1",
"gray-matter": "^4.0.3",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"mysql2": "^3.14.1",
"ollama-ai-provider-v2": "^1.3.1",
"ollama-ai-provider-v2": "^3.3.1",
"openai": "^4.91.1",
"openid-client": "^6.8.2",
"pg": "^8.13.3",
Expand All @@ -68,29 +67,30 @@
"@nestjs/cli": "^11.0.4",
"@nestjs/schematics": "^11.0.1",
"@nestjs/testing": "^11.0.10",
"@swc/cli": "^0.6.0",
"@swc/cli": "^0.8.0",
"@swc/core": "^1.10.18",
"@swc/jest": "^0.2.39",
"@types/bcryptjs": "^2.4.6",
"@types/better-sqlite3": "^7.6.13",
"@types/blocked-at": "^1.0.4",
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/jest": "^30.0.0",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^22.13.5",
"@types/pg": "^8.11.11",
"@types/supertest": "^6.0.2",
"@types/supertest": "^7.2.0",
"@types/uuid": "^10.0.0",
"better-sqlite3": "^11.9.1",
"eslint": "^9.21.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.3",
"globals": "^16.0.0",
"jest": "^29.7.0",
"jest": "^30.2.0",
"prettier": "^3.5.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"supertest": "^7.2.2",
"ts-jest": "^29.4.6",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
Expand All @@ -114,6 +114,10 @@
"node_modules/(?!(openid-client|oauth4webapi|jose)/)"
],
"testEnvironment": "node",
"maxWorkers": 1
"maxWorkers": 1,
"moduleDirectories": [
"node_modules",
"<rootDir>/../node_modules"
]
}
}
8 changes: 4 additions & 4 deletions backend/src/conversations/ai.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
import * as dotenv from 'dotenv'
import * as z from 'zod'
import { generateText, ModelMessage, streamText } from 'ai'
import { LanguageModelV2 } from '@ai-sdk/provider'
import { LanguageModelV3 } from '@ai-sdk/provider'
import {
AssistantModelMessage,
ToolCallPart,
ToolModelMessage,
ToolResultPart,
UserModelMessage,
} from '@ai-sdk/provider-utils'
} from 'ai'
import { createOpenAI } from '@ai-sdk/openai'
import {
Actor,
Expand All @@ -34,8 +34,8 @@ describe.skip('AiService', () => {
dotenv.config()
const apiKey = process.env.OPENAI_API_KEY || 'missing'
const by = Actor.anon()
const model: LanguageModelV2 = createOpenAI({ apiKey })('gpt-4o-mini')
// const model: LanguageModelV2 = createGoogleGenerativeAI({ apiKey: process.env.GOOGLE_API_KEY })('gemini-2.5-flash-lite')
const model: LanguageModelV3 = createOpenAI({ apiKey })('gpt-4o-mini')
// const model: LanguageModelV3 = createGoogleGenerativeAI({ apiKey: process.env.GOOGLE_API_KEY })('gemini-2.5-flash-lite')
const currentDatetime: AiTool = {
id: 'current_datetime',
description: 'Get the current date and time',
Expand Down
21 changes: 12 additions & 9 deletions backend/src/conversations/ai.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
streamText,
ToolSet,
} from 'ai'
import { LanguageModelV2, LanguageModelV2ToolResultOutput } from '@ai-sdk/provider'
import { tool, Tool, ToolCallPart, ToolResultPart } from '@ai-sdk/provider-utils'
import { LanguageModelV3, LanguageModelV3ToolCallPart, LanguageModelV3ToolResultOutput, LanguageModelV3ToolResultPart } from '@ai-sdk/provider'
import { tool, Tool } from 'ai'
import { createOpenAI, openai } from '@ai-sdk/openai'
import { createGoogleGenerativeAI, google } from '@ai-sdk/google'
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
Expand Down Expand Up @@ -106,6 +106,8 @@ export class AiService {
case 'abort': yield { type: 'stream_end', reason: LlmFinishReason.enum.user_cancelled }; break
case 'error': yield { type: 'stream_end', reason: LlmFinishReason.enum.error, error: errorToString(part.error) }; break
case 'raw': break
case 'tool-approval-request': break // tool approval not supported, ignore
case 'tool-output-denied': break // tool output denial not supported, ignore
case 'finish-step': yield { type: 'step_end', responseId: part.response.id, reason: part.finishReason, tokens: removeUndefined({ input: part.usage.inputTokens, reasoning: part.usage.reasoningTokens, output: part.usage.outputTokens }) }; break
case 'finish': yield { type: 'stream_end', reason: part.finishReason, tokens: removeUndefined({ input: part.totalUsage.inputTokens, reasoning: part.totalUsage.reasoningTokens, output: part.totalUsage.outputTokens }) }; break
// does API errors like Quota exceeded are handled by 'error' ?
Expand Down Expand Up @@ -168,8 +170,8 @@ function buildContext(model: LlmModel, req: LlmRequest): GenContext {
}
}

const modelCache: Record<LlmModelName, LanguageModelV2> = {}
function getModel(model: LlmModel): LanguageModelV2 {
const modelCache: Record<LlmModelName, LanguageModelV3> = {}
function getModel(model: LlmModel): LanguageModelV3 {
// cf https://ai-sdk.dev/providers/ai-sdk-providers
if (model.id in modelCache) return modelCache[model.id]
// TODO: add more providers:
Expand Down Expand Up @@ -211,7 +213,7 @@ function getModel(model: LlmModel): LanguageModelV2 {

function buildMessageHistory(messages: MessageWithRelations[]): ModelMessage[] {
const result: ModelMessage[] = []
const toolCalls: Record<LlmResponseId, { calls: ToolCallPart[], results: ToolResultPart[] }> = {}
const toolCalls: Record<LlmResponseId, { calls: LanguageModelV3ToolCallPart[], results: LanguageModelV3ToolResultPart[] }> = {}
messages.forEach(msg => {
if (msg.role === LlmRole.enum.system) {
msg.parts?.length && result.push({ role: 'system', content: msg.parts.filter(p => p.type === LlmPartType.enum.text).map(p => p.content).join('\n\n') })
Expand All @@ -222,8 +224,8 @@ function buildMessageHistory(messages: MessageWithRelations[]): ModelMessage[] {
if (p.type === LlmPartType.enum.text) {
result.push({ role: 'assistant', content: p.content })
} else if (p.type === LlmPartType.enum.tool) {
const toolCall: ToolCallPart = { type: 'tool-call', toolCallId: p.callId, toolName: p.name, input: p.input }
const toolRes: ToolResultPart = { type: 'tool-result', toolCallId: p.callId, toolName: p.name, output: formatToolResult(p.errorType, p.output) }
const toolCall: LanguageModelV3ToolCallPart = { type: 'tool-call', toolCallId: p.callId, toolName: p.name, input: p.input }
const toolRes: LanguageModelV3ToolResultPart = { type: 'tool-result', toolCallId: p.callId, toolName: p.name, output: formatToolResult(p.errorType, p.output) }
const existing = msg.responseId && toolCalls[msg.responseId]
if (existing) {
existing.calls.push(toolCall)
Expand Down Expand Up @@ -286,12 +288,13 @@ function buildTool(model: LlmModel, t: AiTool, hasFunctionTools: boolean): Tool
: tool({ description: t.description, inputSchema: t.input })
}

function formatToolResult(errorType: LlmToolCallErrorKind | undefined, output: LlmToolCallOutput | undefined): LanguageModelV2ToolResultOutput {
function formatToolResult(errorType: LlmToolCallErrorKind | undefined, output: LlmToolCallOutput | undefined): LanguageModelV3ToolResultOutput {
if (output === undefined) return errorType ? { type: 'error-json', value: null } : { type: 'json', value: null }
return errorType ? { type: 'error-text', value: output } : { type: 'text', value: output }
}

export function formatTextResponse<TOOLS extends ToolSet = any, OUTPUT = never>(res: GenerateTextResult<TOOLS, OUTPUT>): LlmResponse {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function formatTextResponse<TOOLS extends ToolSet = any>(res: GenerateTextResult<TOOLS, any>): LlmResponse {
return removeUndefined({
responseId: res.response.id,
finishReason: res.finishReason,
Expand Down
28 changes: 14 additions & 14 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
"clean": "rm -rf ./dist"
},
"dependencies": {
"@ai-sdk/openai": "^2.0.30",
"@ai-sdk/react": "^2.0.29",
"@ai-sdk/openai": "^3.0.35",
"@ai-sdk/react": "^3.0.105",
"@azimutt/inspector-core": "file:../packages/core",
"@azimutt/utils": "^0.1.8",
"@hookform/resolvers": "^4.1.2",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-avatar": "^1.1.10",
Expand Down Expand Up @@ -51,29 +51,29 @@
"@tsparticles/engine": "^3.8.1",
"@tsparticles/react": "^3.0.0",
"@tsparticles/slim": "^3.8.1",
"ai": "^5.0.44",
"ai": "^6.0.103",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.6.5",
"lucide-react": "^0.475.0",
"lucide-react": "^0.575.0",
"mermaid": "^11.11.0",
"nanoid": "^5.1.5",
"next-themes": "^0.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^2.1.7",
"react-resizable-panels": "^4.6.5",
"react-router-dom": "^7.2.0",
"react-syntax-highlighter": "^15.6.6",
"react-syntax-highlighter": "^16.1.0",
"reactflow": "^11.11.4",
"recharts": "^2.15.1",
"recharts": "^3.7.0",
"remark-gfm": "^4.0.1",
"sonner": "^2.0.1",
"sql-formatter": "^15.6.10",
"streamdown": "^1.2.0",
"streamdown": "^2.3.0",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7",
"use-stick-to-bottom": "^1.1.1",
Expand All @@ -92,18 +92,18 @@
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-react": "^5.1.4",
"eslint": "^9.21.0",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"eslint-plugin-storybook": "^9.1.5",
"globals": "^16.0.0",
"storybook": "^9.1.5",
"tailwindcss": "^4.0.8",
"typescript": "~5.7.3",
"typescript-eslint": "^8.24.1",
"vite": "^6.1.1",
"vitest": "^3.0.5"
"vite": "^7.3.1",
"vitest": "^4.0.18"
}
}
14 changes: 10 additions & 4 deletions frontend/src/components/ai-elements/tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,25 @@ export type ToolHeaderProps = {
};

const getStatusBadge = (status: ToolUIPart["state"]) => {
const labels = {
const labels: Record<ToolUIPart["state"], string> = {
"input-streaming": "Pending",
"input-available": "Running",
"output-available": "Completed",
"output-error": "Error",
} as const;
"approval-requested": "Approval",
"approval-responded": "Approved",
"output-denied": "Denied",
};

const icons = {
const icons: Record<ToolUIPart["state"], ReactNode> = {
"input-streaming": <CircleIcon className="size-4" />,
"input-available": <ClockIcon className="size-4 animate-pulse" />,
"output-available": <CheckCircleIcon className="size-4 text-green-600" />,
"output-error": <XCircleIcon className="size-4 text-red-600" />,
} as const;
"approval-requested": <ClockIcon className="size-4 text-yellow-600" />,
"approval-responded": <CheckCircleIcon className="size-4 text-yellow-600" />,
"output-denied": <XCircleIcon className="size-4 text-orange-600" />,
};

return (
<Badge className="gap-1.5 rounded-full text-xs" variant="secondary">
Expand Down
30 changes: 18 additions & 12 deletions frontend/src/components/ui/chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,20 @@ function ChartTooltipContent({
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}) {
}: React.ComponentProps<"div"> & {
active?: boolean
payload?: ReadonlyArray<RechartsPrimitive.TooltipPayloadEntry<any, any>>
label?: string | number
labelClassName?: string
labelFormatter?: (label: any, payload: ReadonlyArray<any>) => React.ReactNode
formatter?: (value: any, name: any, item: any, index: number, payload: ReadonlyArray<any>) => React.ReactNode | [React.ReactNode, React.ReactNode]
color?: string
nameKey?: string
labelKey?: string
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
}) {
const { config } = useChart()

const tooltipLabel = React.useMemo(() => {
Expand Down Expand Up @@ -180,14 +186,14 @@ function ChartTooltipContent({
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
{payload.map((item: RechartsPrimitive.TooltipPayloadEntry<any, any>, index: number) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color

return (
<div
key={item.dataKey}
key={item.dataKey as React.Key}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
Expand Down Expand Up @@ -258,7 +264,7 @@ function ChartLegendContent({
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
Pick<RechartsPrimitive.DefaultLegendContentProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}) {
Expand All @@ -276,7 +282,7 @@ function ChartLegendContent({
className
)}
>
{payload.map((item) => {
{payload.map((item: RechartsPrimitive.LegendPayload) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)

Expand Down
10 changes: 5 additions & 5 deletions frontend/src/components/ui/resizable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { cn } from "@/lib/utils"
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
}: React.ComponentProps<typeof ResizablePrimitive.Group>) {
return (
<ResizablePrimitive.PanelGroup
<ResizablePrimitive.Group
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
Expand All @@ -33,11 +33,11 @@ function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
}: React.ComponentProps<typeof ResizablePrimitive.Separator> & {
withHandle?: boolean
}) {
return (
<ResizablePrimitive.PanelResizeHandle
<ResizablePrimitive.Separator
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
Expand All @@ -50,7 +50,7 @@ function ResizableHandle({
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
</ResizablePrimitive.Separator>
)
}

Expand Down
Loading
Loading