From 34ea140b12171eff4b2d0c90f43b04ed684c40af Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 17 Jul 2025 11:44:42 +0100 Subject: [PATCH 01/34] Queue in run table and filtering --- .../app/components/runs/v3/TaskRunsTable.tsx | 18 ++++++++++++++++++ .../app/services/runsRepository.server.ts | 9 +++++++++ 2 files changed, 27 insertions(+) diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx index 8abca02eef..022897d835 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx @@ -55,6 +55,7 @@ import { filterableTaskRunStatuses, TaskRunStatusCombo, } from "./TaskRunStatus"; +import { TaskIconSmall } from "~/assets/icons/TaskIcon"; type RunsTableProps = { total: number; @@ -209,6 +210,7 @@ export function TaskRunsTable({ Queue Test Created at + Queue @@ -406,6 +408,22 @@ export function TaskRunsTable({ {run.createdAt ? : "–"} + + + {run.queue.type === "task" ? ( + } + content={`This queue was automatically created from your "${run.queue.name}" task`} + /> + ) : ( + } + content={`This is a custom queue you added in your code.`} + /> + )} + {run.queue.name} + + {run.delayUntil ? : "–"} diff --git a/apps/webapp/app/services/runsRepository.server.ts b/apps/webapp/app/services/runsRepository.server.ts index 3196c436b3..916a4e74cb 100644 --- a/apps/webapp/app/services/runsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository.server.ts @@ -52,6 +52,11 @@ export type ParsedRunFilters = RunListInputFilters & { direction?: "forward" | "backward"; }; +export type ParsedRunFilters = RunListInputFilters & { + cursor?: string; + direction?: "forward" | "backward"; +}; + type FilterRunsOptions = Omit & { period: number | undefined; }; @@ -372,6 +377,10 @@ function applyRunFiltersToQueryBuilder( machines: options.machines, }); } + + if (options.queues && options.queues.length > 0) { + queryBuilder.where("queue IN {queues: Array(String)}", { queues: options.queues }); + } } export function parseRunListInputOptions(data: any): RunListInputOptions { From 471849aff610a1482600d71a4fbf397991c1d60c Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 17 Jul 2025 12:33:35 +0100 Subject: [PATCH 02/34] Debounce the filter changes --- apps/webapp/app/components/runs/v3/RunFilters.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index a44cd808a3..e698b995d0 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -9,7 +9,7 @@ import { XMarkIcon, } from "@heroicons/react/20/solid"; import { Form, useFetcher } from "@remix-run/react"; -import { IconToggleLeft, IconRotateClockwise2 } from "@tabler/icons-react"; +import { IconRotateClockwise2, IconToggleLeft } from "@tabler/icons-react"; import { MachinePresetName } from "@trigger.dev/core/v3"; import type { BulkActionType, TaskRunStatus, TaskTriggerSource } from "@trigger.dev/database"; import { ListFilterIcon } from "lucide-react"; @@ -26,6 +26,7 @@ import { machines, } from "~/components/MachineLabelCombo"; import { AppliedFilter } from "~/components/primitives/AppliedFilter"; +import { Badge } from "~/components/primitives/Badge"; import { DateTime } from "~/components/primitives/DateTime"; import { FormError } from "~/components/primitives/FormError"; import { Input } from "~/components/primitives/Input"; @@ -56,8 +57,8 @@ import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useSearchParams } from "~/hooks/useSearchParam"; import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues"; -import { type loader as tagsLoader } from "~/routes/resources.projects.$projectParam.runs.tags"; import { type loader as versionsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions"; +import { type loader as tagsLoader } from "~/routes/resources.projects.$projectParam.runs.tags"; import { Button } from "../../primitives/Buttons"; import { BulkActionTypeCombo } from "./BulkAction"; import { appliedSummary, FilterMenuProvider, TimeFilter } from "./SharedFilters"; @@ -69,7 +70,6 @@ import { TaskRunStatusCombo, } from "./TaskRunStatus"; import { TaskTriggerSourceIcon } from "./TaskTriggerSource"; -import { Badge } from "~/components/primitives/Badge"; export const RunStatus = z.enum(allTaskRunStatuses); From f84131d521710b8052a9928bad1e3b5e70ea1ecf Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 17 Jul 2025 12:38:36 +0100 Subject: [PATCH 03/34] Remove console log From a1aac6b15ed23e1f4adf7a08c3022efabcb02167 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 17 Jul 2025 15:58:14 +0100 Subject: [PATCH 04/34] Added machine filtering --- .../app/components/runs/v3/TaskRunsTable.tsx | 18 ---------- .../app/services/runsRepository.server.ts | 7 ++++ apps/webapp/app/utils/cn.ts | 35 +++++++++++++++++++ 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx index 022897d835..8abca02eef 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx @@ -55,7 +55,6 @@ import { filterableTaskRunStatuses, TaskRunStatusCombo, } from "./TaskRunStatus"; -import { TaskIconSmall } from "~/assets/icons/TaskIcon"; type RunsTableProps = { total: number; @@ -210,7 +209,6 @@ export function TaskRunsTable({ Queue Test Created at - Queue @@ -408,22 +406,6 @@ export function TaskRunsTable({ {run.createdAt ? : "–"} - - - {run.queue.type === "task" ? ( - } - content={`This queue was automatically created from your "${run.queue.name}" task`} - /> - ) : ( - } - content={`This is a custom queue you added in your code.`} - /> - )} - {run.queue.name} - - {run.delayUntil ? : "–"} diff --git a/apps/webapp/app/services/runsRepository.server.ts b/apps/webapp/app/services/runsRepository.server.ts index 916a4e74cb..1f8344c239 100644 --- a/apps/webapp/app/services/runsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository.server.ts @@ -8,6 +8,7 @@ import parseDuration from "parse-duration"; import { z } from "zod"; import { timeFilters } from "~/components/runs/v3/SharedFilters"; import { type PrismaClient } from "~/db.server"; +import { MachinePresetName } from "@trigger.dev/core/v3"; export type RunsRepositoryOptions = { clickhouse: ClickHouse; @@ -381,6 +382,12 @@ function applyRunFiltersToQueryBuilder( if (options.queues && options.queues.length > 0) { queryBuilder.where("queue IN {queues: Array(String)}", { queues: options.queues }); } + + if (options.machines && options.machines.length > 0) { + queryBuilder.where("machine_preset IN {machines: Array(String)}", { + machines: options.machines, + }); + } } export function parseRunListInputOptions(data: any): RunListInputOptions { diff --git a/apps/webapp/app/utils/cn.ts b/apps/webapp/app/utils/cn.ts index 842542049d..ef578364ce 100644 --- a/apps/webapp/app/utils/cn.ts +++ b/apps/webapp/app/utils/cn.ts @@ -64,6 +64,41 @@ const customTwMerge = extendTailwindMerge({ "96", "auto", "px", + "0.5", + "1", + "1.5", + "2", + "2.5", + "3", + "3.5", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "14", + "16", + "20", + "24", + "28", + "32", + "36", + "40", + "44", + "48", + "52", + "56", + "60", + "64", + "72", + "80", + "96", + "auto", + "px", "full", "min", "max", From f34f4c98d96c834daccb546392328bd855a5180a Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 17 Jul 2025 18:38:13 +0100 Subject: [PATCH 05/34] Added version filtering --- apps/webapp/app/services/runsRepository.server.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/webapp/app/services/runsRepository.server.ts b/apps/webapp/app/services/runsRepository.server.ts index 1f8344c239..bcd5dd582b 100644 --- a/apps/webapp/app/services/runsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository.server.ts @@ -8,7 +8,6 @@ import parseDuration from "parse-duration"; import { z } from "zod"; import { timeFilters } from "~/components/runs/v3/SharedFilters"; import { type PrismaClient } from "~/db.server"; -import { MachinePresetName } from "@trigger.dev/core/v3"; export type RunsRepositoryOptions = { clickhouse: ClickHouse; @@ -53,11 +52,6 @@ export type ParsedRunFilters = RunListInputFilters & { direction?: "forward" | "backward"; }; -export type ParsedRunFilters = RunListInputFilters & { - cursor?: string; - direction?: "forward" | "backward"; -}; - type FilterRunsOptions = Omit & { period: number | undefined; }; From 314ecbf785e0c8e9515a5372b1d3f9a38b48800e Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 18 Jul 2025 13:59:39 +0100 Subject: [PATCH 06/34] Filter by version in the db From 5c1d696b37fecea82821e7ac4033b12225e1f467 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 13:59:13 +0100 Subject: [PATCH 07/34] Removed duplicate classes From 4f601f3b90c78054535f1f747ec953414a073f48 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 14:01:12 +0100 Subject: [PATCH 08/34] Version filtering hasFilters consistency From 73ba30b87365e2b547f014b3751c0902a6179fae Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 14:07:40 +0100 Subject: [PATCH 09/34] Added queues and machines to the bulk action summary From 0d42fd1b39cebed4c377b6dda7e90a030ee65a0b Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 14:57:17 +0100 Subject: [PATCH 10/34] runs.list filtering for queue and machine --- .../app/presenters/v3/ApiRunListPresenter.server.ts | 8 ++++++++ packages/core/src/v3/apiClient/index.ts | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/apps/webapp/app/presenters/v3/ApiRunListPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiRunListPresenter.server.ts index a2e44969bf..254ec18d1c 100644 --- a/apps/webapp/app/presenters/v3/ApiRunListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ApiRunListPresenter.server.ts @@ -251,6 +251,14 @@ export class ApiRunListPresenter extends BasePresenter { options.batchId = searchParams["filter[batch]"]; } + if (searchParams["filter[queue]"]) { + options.queues = searchParams["filter[queue]"]; + } + + if (searchParams["filter[machine]"]) { + options.machines = searchParams["filter[machine]"]; + } + const presenter = new NextRunListPresenter(this._replica, clickhouseClient); logger.debug("Calling RunListPresenter", { options }); diff --git a/packages/core/src/v3/apiClient/index.ts b/packages/core/src/v3/apiClient/index.ts index 4eab7d0089..cebe9a6e34 100644 --- a/packages/core/src/v3/apiClient/index.ts +++ b/packages/core/src/v3/apiClient/index.ts @@ -1158,6 +1158,15 @@ function createSearchQueryForListRuns(query?: ListRunsQueryParams): URLSearchPar ); } + if (query.queue) { + searchParams.append( + "filter[queue]", + Array.isArray(query.queue) + ? query.queue.map((q) => queueNameFromQueueTypeName(q)).join(",") + : queueNameFromQueueTypeName(query.queue) + ); + } + if (query.machine) { searchParams.append( "filter[machine]", From 77e9c9383c91b7e2ff57876bd017d49900d52bd2 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 16:10:28 +0100 Subject: [PATCH 11/34] Fix for machine errors From 023d6fbec4dd8e5cc9ddf0d9f208e6f9526e57ab Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 18 Jul 2025 14:00:34 +0100 Subject: [PATCH 12/34] Input field now has accessory instead of shortcut --- .../app/components/primitives/Input.tsx | 18 ++++++--------- .../routes/storybook.input-fields/route.tsx | 21 +++++++++--------- .../routes/storybook.search-fields/route.tsx | 22 ++++++++++++++----- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/apps/webapp/app/components/primitives/Input.tsx b/apps/webapp/app/components/primitives/Input.tsx index 7cb4b8a32d..4ff01b608b 100644 --- a/apps/webapp/app/components/primitives/Input.tsx +++ b/apps/webapp/app/components/primitives/Input.tsx @@ -9,55 +9,51 @@ const containerBase = const inputBase = "h-full w-full text-text-bright bg-transparent file:border-0 file:bg-transparent file:text-base file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed outline-none ring-0 border-none"; -const shortcutBase = - "grid h-fit place-content-center border border-dimmed/40 font-normal text-text-dimmed"; - const variants = { large: { container: "px-1 w-full h-10 rounded-[3px] border border-charcoal-800 bg-charcoal-750 hover:border-charcoal-600 hover:bg-charcoal-650", input: "px-2 text-sm", iconSize: "size-4 ml-1", - shortcut: "mr-1 min-w-[22px] rounded-sm py-[3px] px-[5px] text-[0.6rem] select-none", + accessory: "pr-1", }, medium: { container: "px-1 h-8 w-full rounded border border-charcoal-800 bg-charcoal-750 hover:border-charcoal-600 hover:bg-charcoal-650", input: "px-1.5 rounded text-sm", iconSize: "size-4 ml-0.5", - shortcut: "min-w-[22px] rounded-sm py-[3px] px-[5px] text-[0.6rem]", + accessory: "pr-1", }, small: { container: "px-1 h-6 w-full rounded border border-charcoal-800 bg-charcoal-750 hover:border-charcoal-600 hover:bg-charcoal-650", input: "px-1 rounded text-xs", iconSize: "size-3 ml-0.5", - shortcut: "min-w-[22px] rounded-[2px] py-px px-[3px] text-[0.5rem]", + accessory: "pr-0.5", }, tertiary: { container: "px-1 h-6 w-full rounded hover:bg-charcoal-750", input: "px-1 rounded text-xs", iconSize: "size-3 ml-0.5", - shortcut: "min-w-[22px] rounded-[2px] py-px px-[3px] text-[0.5rem]", + accessory: "pr-0.5", }, }; export type InputProps = React.InputHTMLAttributes & { variant?: keyof typeof variants; icon?: RenderIcon; - shortcut?: string; + accessory?: React.ReactNode; fullWidth?: boolean; }; const Input = React.forwardRef( - ({ className, type, shortcut, fullWidth = true, variant = "medium", icon, ...props }, ref) => { + ({ className, type, accessory, fullWidth = true, variant = "medium", icon, ...props }, ref) => { const innerRef = useRef(null); useImperativeHandle(ref, () => innerRef.current as HTMLInputElement); const containerClassName = variants[variant].container; const inputClassName = variants[variant].input; const iconClassName = variants[variant].iconSize; - const shortcutClassName = variants[variant].shortcut; return (
( ref={innerRef} {...props} /> - {shortcut &&
{shortcut}
} + {accessory &&
{accessory}
}
); } diff --git a/apps/webapp/app/routes/storybook.input-fields/route.tsx b/apps/webapp/app/routes/storybook.input-fields/route.tsx index 6e5b95fe95..62794fab7b 100644 --- a/apps/webapp/app/routes/storybook.input-fields/route.tsx +++ b/apps/webapp/app/routes/storybook.input-fields/route.tsx @@ -1,6 +1,7 @@ import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; import { Input } from "~/components/primitives/Input"; +import { ShortcutKey } from "~/components/primitives/ShortcutKey"; export default function Story() { return ( @@ -26,28 +27,28 @@ function InputFieldSet({ disabled }: { disabled?: boolean }) { variant="large" placeholder="Search" icon={MagnifyingGlassIcon} - shortcut="⌘K" + accessory={} /> } /> } /> } />
@@ -56,42 +57,42 @@ function InputFieldSet({ disabled }: { disabled?: boolean }) { variant="large" placeholder="Search" icon={} - shortcut="⌘K" + accessory={} /> } - shortcut="⌘K" + accessory={} /> } - shortcut="⌘K" + accessory={} /> } - shortcut="⌘K" + accessory={} /> } - shortcut="⌘K" + accessory={} /> } - shortcut="⌘K" + accessory={} />
diff --git a/apps/webapp/app/routes/storybook.search-fields/route.tsx b/apps/webapp/app/routes/storybook.search-fields/route.tsx index 86bd7abe0b..53ae707486 100644 --- a/apps/webapp/app/routes/storybook.search-fields/route.tsx +++ b/apps/webapp/app/routes/storybook.search-fields/route.tsx @@ -5,6 +5,7 @@ import { Fieldset } from "~/components/primitives/Fieldset"; import { Input } from "~/components/primitives/Input"; import { InputGroup } from "~/components/primitives/InputGroup"; import { Label } from "~/components/primitives/Label"; +import { ShortcutKey } from "~/components/primitives/ShortcutKey"; export default function Story() { return ( @@ -18,9 +19,12 @@ export default function Story() { placeholder="Search" required={true} icon={MagnifyingGlassIcon} - shortcut="⌘K" + accessory={ + + } /> + + } fullWidth={false} /> @@ -48,7 +54,9 @@ export default function Story() { placeholder="Search" required={true} icon={MagnifyingGlassIcon} - shortcut="⌘K" + accessory={ + + } /> @@ -57,7 +65,9 @@ export default function Story() { placeholder="Search" required={true} icon={MagnifyingGlassIcon} - shortcut="⌘K" + accessory={ + + } /> @@ -66,7 +76,9 @@ export default function Story() { placeholder="Search" required={true} icon={MagnifyingGlassIcon} - shortcut="⌘K" + accessory={ + + } /> From dc70047555b5b63d63e564a106f325f08de55506 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 18 Jul 2025 14:02:49 +0100 Subject: [PATCH 13/34] First experiments with the UI --- .../app/components/runs/v3/AIFilterInput.tsx | 98 +++++++++++++++++++ .../app/components/runs/v3/RunFilters.tsx | 7 +- apps/webapp/app/hooks/useSearchParam.ts | 5 +- ...jectParam.env.$envParam.runs.ai-filter.tsx | 44 +++++++++ 4 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 apps/webapp/app/components/runs/v3/AIFilterInput.tsx create mode 100644 apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx diff --git a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx new file mode 100644 index 0000000000..1cec2e8e66 --- /dev/null +++ b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx @@ -0,0 +1,98 @@ +import { useFetcher } from "@remix-run/react"; +import { useState, useEffect } from "react"; +import { AISparkleIcon } from "~/assets/icons/AISparkleIcon"; +import { Button } from "~/components/primitives/Buttons"; +import { Input } from "~/components/primitives/Input"; +import { Spinner } from "~/components/primitives/Spinner"; +import { useSearchParams } from "~/hooks/useSearchParam"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { useEnvironment } from "~/hooks/useEnvironment"; +import { type TaskRunListSearchFilters } from "./RunFilters"; +import { objectToSearchParams } from "~/utils/searchParams"; +import { ShortcutKey } from "~/components/primitives/ShortcutKey"; + +type AIFilterResult = + | { + success: true; + filters: TaskRunListSearchFilters; + explanation?: string; + } + | { + success: false; + error: string; + suggestions?: string[]; + }; + +export function AIFilterInput() { + const [text, setText] = useState(""); + const { replace } = useSearchParams(); + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + + const fetcher = useFetcher(); + + useEffect(() => { + if (fetcher.data?.success) { + const searchParams = objectToSearchParams(fetcher.data.filters); + if (!searchParams) { + return; + } + + replace(searchParams); + + // Clear the input after successful application + setText(""); + + // TODO: Show success message with explanation + console.log(`AI applied filters: ${fetcher.data.explanation}`); + } else if (fetcher.data?.success === false) { + // TODO: Show error with suggestions + console.error(fetcher.data.error, fetcher.data.suggestions); + } + }, [fetcher.data, replace]); + + const isLoading = fetcher.state === "submitting"; + + return ( + +
+ setText(e.target.value)} + disabled={isLoading} + className="pr-10" + onKeyDown={(e) => { + if (e.key === "Enter" && text.trim() && !isLoading) { + e.preventDefault(); + const form = e.currentTarget.closest("form"); + if (form) { + form.requestSubmit(); + } + } + }} + icon={} + accessory={ + text.length > 0 ? ( + + ) : undefined + } + /> + {isLoading && ( +
+ +
+ )} +
+
+ ); +} diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index e698b995d0..2b81356dbf 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -62,6 +62,7 @@ import { type loader as tagsLoader } from "~/routes/resources.projects.$projectP import { Button } from "../../primitives/Buttons"; import { BulkActionTypeCombo } from "./BulkAction"; import { appliedSummary, FilterMenuProvider, TimeFilter } from "./SharedFilters"; +import { AIFilterInput } from "./AIFilterInput"; import { allTaskRunStatuses, descriptionForTaskRunStatus, @@ -305,6 +306,7 @@ export function RunsFilters(props: RunFiltersProps) { return (
+ @@ -355,9 +357,8 @@ function FilterMenu(props: RunFiltersProps) { variant={"secondary/small"} shortcut={shortcut} tooltipTitle={"Filter runs"} - > - Filter - + className="pr-0.5" + /> ); return ( diff --git a/apps/webapp/app/hooks/useSearchParam.ts b/apps/webapp/app/hooks/useSearchParam.ts index 3d0bb07e1b..821a726cdc 100644 --- a/apps/webapp/app/hooks/useSearchParam.ts +++ b/apps/webapp/app/hooks/useSearchParam.ts @@ -9,9 +9,8 @@ export function useSearchParams() { const location = useOptimisticLocation(); const replace = useCallback( - (values: Values) => { + (values: Values | URLSearchParams) => { const s = set(new URLSearchParams(location.search), values); - navigate(`${location.pathname}?${s.toString()}`, { replace: true }); }, [location, navigate] @@ -70,7 +69,7 @@ export function useSearchParams() { }; } -function set(searchParams: URLSearchParams, values: Values) { +function set(searchParams: URLSearchParams, values: Values | URLSearchParams) { const search = new URLSearchParams(searchParams); for (const [param, value] of Object.entries(values)) { if (value === undefined) { diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx new file mode 100644 index 0000000000..f899eabb9a --- /dev/null +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx @@ -0,0 +1,44 @@ +import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; +import { z } from "zod"; +import { requireUserId } from "~/services/session.server"; +import { EnvironmentParamSchema } from "~/utils/pathBuilder"; +import { type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; + +const RequestSchema = z.object({ + text: z.string().min(1), +}); + +export async function action({ request, params }: ActionFunctionArgs) { + const userId = await requireUserId(request); + const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); + + // Parse the request body + const formData = await request.formData(); + const submission = RequestSchema.safeParse(Object.fromEntries(formData)); + + if (!submission.success) { + return json<{ success: false; error: string }>( + { + success: false, + error: "Invalid request data", + }, + { status: 400 } + ); + } + + const { text } = submission.data; + + // TODO: Replace this with actual AI processing + // For now, return fake successful data + const fakeFilters: TaskRunListSearchFilters = { + statuses: ["COMPLETED_WITH_ERRORS"], + period: "7d", + tags: ["test-tag"], + }; + + return json<{ success: true; filters: TaskRunListSearchFilters; explanation: string }>({ + success: true, + filters: fakeFilters, + explanation: `Applied filters: failed status, last 7 days, with tag "test-tag"`, + }); +} From 8491caad154e31fbea7bb4a1e89290bb05e19dad Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 18 Jul 2025 14:13:17 +0100 Subject: [PATCH 14/34] Got the fake filtering working --- .../app/components/runs/v3/AIFilterInput.tsx | 30 +++++++++++-------- ...jectParam.env.$envParam.runs.ai-filter.tsx | 22 ++++++++++++-- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx index 1cec2e8e66..ee45473bc0 100644 --- a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx +++ b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx @@ -1,16 +1,15 @@ -import { useFetcher } from "@remix-run/react"; -import { useState, useEffect } from "react"; +import { useFetcher, useNavigate } from "@remix-run/react"; +import { useEffect, useState } from "react"; import { AISparkleIcon } from "~/assets/icons/AISparkleIcon"; -import { Button } from "~/components/primitives/Buttons"; import { Input } from "~/components/primitives/Input"; +import { ShortcutKey } from "~/components/primitives/ShortcutKey"; import { Spinner } from "~/components/primitives/Spinner"; -import { useSearchParams } from "~/hooks/useSearchParam"; +import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; -import { useEnvironment } from "~/hooks/useEnvironment"; -import { type TaskRunListSearchFilters } from "./RunFilters"; +import { useSearchParams } from "~/hooks/useSearchParam"; import { objectToSearchParams } from "~/utils/searchParams"; -import { ShortcutKey } from "~/components/primitives/ShortcutKey"; +import { type TaskRunListSearchFilters } from "./RunFilters"; type AIFilterResult = | { @@ -26,7 +25,7 @@ type AIFilterResult = export function AIFilterInput() { const [text, setText] = useState(""); - const { replace } = useSearchParams(); + const navigate = useNavigate(); const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); @@ -34,16 +33,21 @@ export function AIFilterInput() { const fetcher = useFetcher(); useEffect(() => { - if (fetcher.data?.success) { + if (fetcher.data?.success && fetcher.state === "loading") { + // Clear the input after successful application + setText(""); + const searchParams = objectToSearchParams(fetcher.data.filters); if (!searchParams) { return; } - replace(searchParams); + console.log("AI filter success", { + data: fetcher.data, + searchParams: searchParams.toString(), + }); - // Clear the input after successful application - setText(""); + navigate(`${location.pathname}?${searchParams.toString()}`, { replace: true }); // TODO: Show success message with explanation console.log(`AI applied filters: ${fetcher.data.explanation}`); @@ -51,7 +55,7 @@ export function AIFilterInput() { // TODO: Show error with suggestions console.error(fetcher.data.error, fetcher.data.suggestions); } - }, [fetcher.data, replace]); + }, [fetcher.data, navigate]); const isLoading = fetcher.state === "submitting"; diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx index f899eabb9a..96ecd85d8d 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx @@ -3,6 +3,8 @@ import { z } from "zod"; import { requireUserId } from "~/services/session.server"; import { EnvironmentParamSchema } from "~/utils/pathBuilder"; import { type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; +import { findProjectBySlug } from "~/models/project.server"; +import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; const RequestSchema = z.object({ text: z.string().min(1), @@ -26,14 +28,30 @@ export async function action({ request, params }: ActionFunctionArgs) { ); } + const project = await findProjectBySlug(organizationSlug, projectParam, userId); + if (!project) { + throw new Response(undefined, { + status: 404, + statusText: "Project not found", + }); + } + + const environment = await findEnvironmentBySlug(project.id, envParam, userId); + if (!environment) { + throw new Response(undefined, { + status: 404, + statusText: "Environment not found", + }); + } + const { text } = submission.data; // TODO: Replace this with actual AI processing // For now, return fake successful data const fakeFilters: TaskRunListSearchFilters = { - statuses: ["COMPLETED_WITH_ERRORS"], + statuses: ["COMPLETED_WITH_ERRORS", "COMPLETED_SUCCESSFULLY"], + machines: ["small-2x"], period: "7d", - tags: ["test-tag"], }; return json<{ success: true; filters: TaskRunListSearchFilters; explanation: string }>({ From c05b38c2441cd7a1c8b695ada8102a8c7309f602 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 18 Jul 2025 15:37:27 +0100 Subject: [PATCH 15/34] =?UTF-8?q?AI=20filtering=20is=20working=20pretty=20?= =?UTF-8?q?well=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/runs/v3/AIFilterInput.tsx | 34 ++++-- .../app/components/runs/v3/RunFilters.tsx | 93 +++++++++++----- ...jectParam.env.$envParam.runs.ai-filter.tsx | 23 ++-- .../v3/services/aiRunFilterService.server.ts | 100 ++++++++++++++++++ apps/webapp/package.json | 2 + pnpm-lock.yaml | 96 +++++++++++++++++ 6 files changed, 296 insertions(+), 52 deletions(-) create mode 100644 apps/webapp/app/v3/services/aiRunFilterService.server.ts diff --git a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx index ee45473bc0..ad3c97b2b2 100644 --- a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx +++ b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx @@ -10,6 +10,8 @@ import { useProject } from "~/hooks/useProject"; import { useSearchParams } from "~/hooks/useSearchParam"; import { objectToSearchParams } from "~/utils/searchParams"; import { type TaskRunListSearchFilters } from "./RunFilters"; +import { cn } from "~/utils/cn"; +import { motion } from "framer-motion"; type AIFilterResult = | { @@ -25,6 +27,7 @@ type AIFilterResult = export function AIFilterInput() { const [text, setText] = useState(""); + const [isFocused, setIsFocused] = useState(false); const navigate = useNavigate(); const organization = useOrganization(); const project = useProject(); @@ -65,7 +68,15 @@ export function AIFilterInput() { action={`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/runs/ai-filter`} method="post" > -
+ setText(e.target.value)} disabled={isLoading} - className="pr-10" + fullWidth onKeyDown={(e) => { if (e.key === "Enter" && text.trim() && !isLoading) { e.preventDefault(); @@ -84,19 +95,22 @@ export function AIFilterInput() { } } }} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} icon={} accessory={ - text.length > 0 ? ( - + isLoading ? ( + + ) : text.length > 0 ? ( + ) : undefined } /> - {isLoading && ( -
- -
- )} -
+ ); } diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index 2b81356dbf..c53f391f2a 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -112,37 +112,74 @@ export const MachinePresetOrMachinePresetArray = z.preprocess((value) => { }, MachinePresetName.array().optional()); export const TaskRunListSearchFilters = z.object({ - cursor: z.string().optional(), - direction: z.enum(["forward", "backward"]).optional(), - environments: StringOrStringArray, - tasks: StringOrStringArray, - versions: StringOrStringArray, - statuses: z.preprocess((value) => { - if (typeof value === "string") { - if (value.length > 0) { - return [value]; - } + cursor: z.string().optional().describe("Cursor for pagination - used internally for navigation"), + direction: z + .enum(["forward", "backward"]) + .optional() + .describe("Pagination direction - forward or backward. Used internally for navigation"), + environments: StringOrStringArray.describe( + "Environment names to filter by (DEVELOPMENT, STAGING, PREVIEW, PRODUCTION)" + ), + tasks: StringOrStringArray.describe( + "Task identifiers to filter by (these are user-defined names)" + ), + versions: StringOrStringArray.describe( + "Version identifiers to filter by (these are in this format 20250718.1). Needs to be looked up." + ), + statuses: z + .preprocess((value) => { + if (typeof value === "string") { + if (value.length > 0) { + return [value]; + } - return undefined; - } + return undefined; + } - if (Array.isArray(value)) { - return value.filter((v) => typeof v === "string" && v.length > 0); - } + if (Array.isArray(value)) { + return value.filter((v) => typeof v === "string" && v.length > 0); + } - return undefined; - }, RunStatus.array().optional()), - tags: StringOrStringArray, - bulkId: z.string().optional(), - period: z.preprocess((value) => (value === "all" ? undefined : value), z.string().optional()), - from: z.coerce.number().optional(), - to: z.coerce.number().optional(), - rootOnly: z.coerce.boolean().optional(), - batchId: z.string().optional(), - runId: StringOrStringArray, - scheduleId: z.string().optional(), - queues: StringOrStringArray, - machines: MachinePresetOrMachinePresetArray, + return undefined; + }, RunStatus.array().optional()) + .describe(`Run statuses to filter by (${filterableTaskRunStatuses.join(", ")})`), + tags: StringOrStringArray.describe("Tag names to filter by (these are user-defined names)"), + bulkId: z + .string() + .optional() + .describe("Bulk action ID to filter by - shows runs from a specific bulk operation"), + period: z + .preprocess((value) => (value === "all" ? undefined : value), z.string().optional()) + .describe("Time period string (e.g., '1h', '7d', '30d', '1y') for relative time filtering"), + from: z.coerce + .number() + .optional() + .describe("Unix timestamp for start of time range - absolute time filtering"), + to: z.coerce + .number() + .optional() + .describe("Unix timestamp for end of time range - absolute time filtering"), + rootOnly: z.coerce + .boolean() + .optional() + .describe("Show only root runs (not child runs) - set to true to exclude sub-runs"), + batchId: z + .string() + .optional() + .describe( + "Batch ID to filter by - shows runs from a specific batch operation. They start with batch_" + ), + runId: StringOrStringArray.describe("Specific run IDs to filter by. They start with run_"), + scheduleId: z + .string() + .optional() + .describe( + "Schedule ID to filter by - shows runs from a specific schedule. They start with sched_" + ), + queues: StringOrStringArray.describe("Queue names to filter by (these are user-defined names)"), + machines: MachinePresetOrMachinePresetArray.describe( + `Machine presets to filter by (${machines.join(", ")})` + ), }); export type TaskRunListSearchFilters = z.infer; diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx index 96ecd85d8d..78c4de5e1d 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx @@ -2,9 +2,10 @@ import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { z } from "zod"; import { requireUserId } from "~/services/session.server"; import { EnvironmentParamSchema } from "~/utils/pathBuilder"; -import { type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; +import { processAIFilter } from "~/v3/services/aiRunFilterService.server"; +import { type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; const RequestSchema = z.object({ text: z.string().min(1), @@ -46,17 +47,11 @@ export async function action({ request, params }: ActionFunctionArgs) { const { text } = submission.data; - // TODO: Replace this with actual AI processing - // For now, return fake successful data - const fakeFilters: TaskRunListSearchFilters = { - statuses: ["COMPLETED_WITH_ERRORS", "COMPLETED_SUCCESSFULLY"], - machines: ["small-2x"], - period: "7d", - }; - - return json<{ success: true; filters: TaskRunListSearchFilters; explanation: string }>({ - success: true, - filters: fakeFilters, - explanation: `Applied filters: failed status, last 7 days, with tag "test-tag"`, - }); + const result = await processAIFilter(text, environment.id); + + if (result.success) { + return json(result); + } else { + return json(result, { status: 400 }); + } } diff --git a/apps/webapp/app/v3/services/aiRunFilterService.server.ts b/apps/webapp/app/v3/services/aiRunFilterService.server.ts new file mode 100644 index 0000000000..9bcd041cee --- /dev/null +++ b/apps/webapp/app/v3/services/aiRunFilterService.server.ts @@ -0,0 +1,100 @@ +import { openai } from "@ai-sdk/openai"; +import { generateObject } from "ai"; +import { z } from "zod"; +import { TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; +import { env } from "~/env.server"; +import { logger } from "~/services/logger.server"; + +const AIFilterResponseSchema = z.object({ + filters: TaskRunListSearchFilters, + explanation: z + .string() + .describe("A short human-readable explanation of what filters were applied"), +}); + +export type AIFilterResult = + | { + success: true; + filters: TaskRunListSearchFilters; + explanation: string; + } + | { + success: false; + error: string; + suggestions?: string[]; + }; + +export async function processAIFilter( + text: string, + environmentId: string +): Promise { + if (!env.OPENAI_API_KEY) { + return { + success: false, + error: "OpenAI API key is not configured", + suggestions: ["Contact your administrator to configure AI features"], + }; + } + + try { + const result = await generateObject({ + model: openai("gpt-4o"), + schema: AIFilterResponseSchema, + prompt: `You are an AI assistant that converts natural language descriptions into structured filter parameters for a task run filtering system. + +Available filter options: +- statuses: Array of run statuses (PENDING, EXECUTING, COMPLETED_SUCCESSFULLY, COMPLETED_WITH_ERRORS, CANCELED, TIMED_OUT, CRASHED, etc.) +- period: Time period string (e.g., "1h", "7d", "30d", "1y") +- from/to: Unix ms timestamps for specific time ranges. You'll need to use a converter if they give you a date. Today's date is ${new Date().toISOString()}, if they only specify a day use the current month. If they don't specify a year use the current year. If they don't specify a time of day use midnight to midnight. +- tags: Array of tag names to filter by +- tasks: Array of task identifiers to filter by +- machines: Array of machine presets (micro, small, small-2x, medium, large, xlarge, etc.) +- queues: Array of queue names to filter by +- versions: Array of version identifiers to filter by +- rootOnly: Boolean to show only root runs (not child runs) +- runId: Array of specific run IDs to filter by +- batchId: Specific batch ID to filter by +- scheduleId: Specific schedule ID to filter by + +Common patterns to recognize: +- "failed runs" → statuses: ["COMPLETED_WITH_ERRORS", "CRASHED", "TIMED_OUT", "SYSTEM_FAILURE"]. +- If they say "only failed" then only use "COMPLETED_WITH_ERRORS". +- "successful runs" → statuses: ["COMPLETED_SUCCESSFULLY"] +- "running runs" → statuses: ["EXECUTING", "RETRYING_AFTER_FAILURE", "WAITING_TO_RESUME"] +- "pending runs" → statuses: ["PENDING", "PENDING_VERSION", "DELAYED"] +- "past 7 days" → period: "7d" +- "last hour" → period: "1h" +- "this month" → period: "30d" +- "with tag X" → tags: ["X"] +- "from task Y" → tasks: ["Y"] +- "using large machine" → machines: ["large-1x", "large-2x"] +- "root only" → rootOnly: true + +Unless they specify they only want root runs, set rootOnly to false. + +Convert the following natural language description into structured filters: + +"${text}" + +Return only the filters that are explicitly mentioned or can be reasonably inferred. If the description is unclear or doesn't match any known patterns, return an empty filters object and explain why in the explanation field.`, + }); + + return { + success: true, + filters: result.object.filters, + explanation: result.object.explanation, + }; + } catch (error) { + logger.error("AI filter processing failed", { error, text, environmentId }); + + return { + success: false, + error: "Failed to process AI filter request", + suggestions: [ + "Try being more specific about what you want to filter", + "Use common terms like 'failed runs', 'last 7 days', 'with tag X'", + "Check that your description is clear and unambiguous", + ], + }; + } +} diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 42a77d62e6..2c0713043e 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -31,6 +31,7 @@ "/public/build" ], "dependencies": { + "@ai-sdk/openai": "^1.3.23", "@ariakit/react": "^0.4.6", "@ariakit/react-core": "^0.4.6", "@aws-sdk/client-ecr": "^3.839.0", @@ -120,6 +121,7 @@ "@unkey/error": "^0.2.0", "@upstash/ratelimit": "^1.1.3", "@whatwg-node/fetch": "^0.9.14", + "ai": "^4.3.19", "assert-never": "^1.2.1", "aws4fetch": "^1.0.18", "class-variance-authority": "^0.5.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b403ee6c86..5df2a8808b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,6 +188,9 @@ importers: apps/webapp: dependencies: + '@ai-sdk/openai': + specifier: ^1.3.23 + version: 1.3.23(zod@3.23.8) '@ariakit/react': specifier: ^0.4.6 version: 0.4.6(react-dom@18.2.0)(react@18.2.0) @@ -455,6 +458,9 @@ importers: '@whatwg-node/fetch': specifier: ^0.9.14 version: 0.9.14 + ai: + specifier: ^4.3.19 + version: 4.3.19(react@18.2.0)(zod@3.23.8) assert-never: specifier: ^1.2.1 version: 1.2.1 @@ -2499,6 +2505,17 @@ packages: zod: 3.23.8 dev: false + /@ai-sdk/openai@1.3.23(zod@3.23.8): + resolution: {integrity: sha512-86U7rFp8yacUAOE/Jz8WbGcwMCqWvjK33wk5DXkfnAOEn3mx2r7tNSJdjukQFZbAK97VMXGPPHxF+aEARDXRXQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.23.8) + zod: 3.23.8 + dev: false + /@ai-sdk/openai@1.3.3(zod@3.23.8): resolution: {integrity: sha512-CH57tonLB4DwkwqwnMmTCoIOR7cNW3bP5ciyloI7rBGJS/Bolemsoo+vn5YnwkyT9O1diWJyvYeTh7A4UfiYOw==} engines: {node: '>=18'} @@ -2581,6 +2598,18 @@ packages: zod: 3.23.8 dev: false + /@ai-sdk/provider-utils@2.2.8(zod@3.23.8): + resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + dependencies: + '@ai-sdk/provider': 1.1.3 + nanoid: 3.3.8 + secure-json-parse: 2.7.0 + zod: 3.23.8 + dev: false + /@ai-sdk/provider@0.0.22: resolution: {integrity: sha512-smZ1/2jL/JSKnbhC6ama/PxI2D/psj+YAe0c0qpd5ComQCNFltg72VFf0rpUSFMmFuj1pCCNoBOCrvyl8HTZHQ==} engines: {node: '>=18'} @@ -2608,6 +2637,13 @@ packages: dependencies: json-schema: 0.4.0 + /@ai-sdk/provider@1.1.3: + resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} + engines: {node: '>=18'} + dependencies: + json-schema: 0.4.0 + dev: false + /@ai-sdk/react@0.0.53(react@19.0.0-rc.0)(zod@3.23.8): resolution: {integrity: sha512-sIsmTFoR/QHvUUkltmHwP4bPjwy2vko6j/Nj8ayxLhEHs04Ug+dwXQyfA7MwgimEE3BcDQpWL8ikVj0m3ZILWQ==} engines: {node: '>=18'} @@ -2667,6 +2703,24 @@ packages: zod: 3.23.8 dev: false + /@ai-sdk/react@1.2.12(react@18.2.0)(zod@3.23.8): + resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + dependencies: + '@ai-sdk/provider-utils': 2.2.8(zod@3.23.8) + '@ai-sdk/ui-utils': 1.2.11(zod@3.23.8) + react: 18.2.0 + swr: 2.2.5(react@18.2.0) + throttleit: 2.1.0 + zod: 3.23.8 + dev: false + /@ai-sdk/react@1.2.2(react@18.3.1)(zod@3.23.8): resolution: {integrity: sha512-rxyNTFjUd3IilVOJFuUJV5ytZBYAIyRi50kFS2gNmSEiG4NHMBBm31ddrxI/i86VpY8gzZVp1/igtljnWBihUA==} engines: {node: '>=18'} @@ -2827,6 +2881,18 @@ packages: zod: 3.23.8 zod-to-json-schema: 3.24.5(zod@3.23.8) + /@ai-sdk/ui-utils@1.2.11(zod@3.23.8): + resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.23.8) + zod: 3.23.8 + zod-to-json-schema: 3.24.5(zod@3.23.8) + dev: false + /@ai-sdk/vue@0.0.45(vue@3.5.16)(zod@3.23.8): resolution: {integrity: sha512-bqeoWZqk88TQmfoPgnFUKkrvhOIcOcSH5LMPgzZ8XwDqz5tHHrMHzpPfHCj7XyYn4ROTFK/2kKdC/ta6Ko0fMw==} engines: {node: '>=18'} @@ -20576,6 +20642,26 @@ packages: zod: 3.23.8 dev: false + /ai@4.3.19(react@18.2.0)(zod@3.23.8): + resolution: {integrity: sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + react: + optional: true + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.23.8) + '@ai-sdk/react': 1.2.12(react@18.2.0)(zod@3.23.8) + '@ai-sdk/ui-utils': 1.2.11(zod@3.23.8) + '@opentelemetry/api': 1.9.0 + jsondiffpatch: 0.6.0 + react: 18.2.0 + zod: 3.23.8 + dev: false + /ajv-formats@2.1.1(ajv@8.17.1): resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -33902,6 +33988,16 @@ packages: magic-string: 0.30.17 zimmerframe: 1.1.2 + /swr@2.2.5(react@18.2.0): + resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + dependencies: + client-only: 0.0.1 + react: 18.2.0 + use-sync-external-store: 1.2.2(react@18.2.0) + dev: false + /swr@2.2.5(react@18.3.1): resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} peerDependencies: From d86d165db9dd874b80ae944a4d06f3570b422d6e Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 18 Jul 2025 16:37:41 +0100 Subject: [PATCH 16/34] Started working on tool calling --- .../v3/QueueListPresenter.server.ts | 6 +- .../v3/VersionListPresenter.server.ts | 6 +- ...jectParam.env.$envParam.runs.ai-filter.tsx | 12 +- .../v3/services/aiRunFilterService.server.ts | 138 +++++++++++++++--- 4 files changed, 134 insertions(+), 28 deletions(-) diff --git a/apps/webapp/app/presenters/v3/QueueListPresenter.server.ts b/apps/webapp/app/presenters/v3/QueueListPresenter.server.ts index 8dc50e96e3..1d9b1e0255 100644 --- a/apps/webapp/app/presenters/v3/QueueListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/QueueListPresenter.server.ts @@ -1,9 +1,13 @@ +import { + TaskQueueType, + type RunEngineVersion, + type RuntimeEnvironmentType, +} from "@trigger.dev/database"; import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { determineEngineVersion } from "~/v3/engineVersion.server"; import { engine } from "~/v3/runEngine.server"; import { BasePresenter } from "./basePresenter.server"; import { toQueueItem } from "./QueueRetrievePresenter.server"; -import { TaskQueueType } from "@trigger.dev/database"; const DEFAULT_ITEMS_PER_PAGE = 25; const MAX_ITEMS_PER_PAGE = 100; diff --git a/apps/webapp/app/presenters/v3/VersionListPresenter.server.ts b/apps/webapp/app/presenters/v3/VersionListPresenter.server.ts index f8a4a36538..f541d884f8 100644 --- a/apps/webapp/app/presenters/v3/VersionListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/VersionListPresenter.server.ts @@ -1,6 +1,6 @@ -import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; -import { BasePresenter } from "./basePresenter.server"; import { CURRENT_DEPLOYMENT_LABEL } from "@trigger.dev/core/v3/isomorphic"; +import { type RuntimeEnvironment } from "@trigger.dev/database"; +import { BasePresenter } from "./basePresenter.server"; const DEFAULT_ITEMS_PER_PAGE = 25; const MAX_ITEMS_PER_PAGE = 100; @@ -17,7 +17,7 @@ export class VersionListPresenter extends BasePresenter { environment, query, }: { - environment: AuthenticatedEnvironment; + environment: Pick; query?: string; }) { const hasFilters = query !== undefined && query.length > 0; diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx index 78c4de5e1d..051b9c633d 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx @@ -6,6 +6,7 @@ import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { processAIFilter } from "~/v3/services/aiRunFilterService.server"; import { type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; +import { tryCatch } from "@trigger.dev/core"; const RequestSchema = z.object({ text: z.string().min(1), @@ -47,11 +48,10 @@ export async function action({ request, params }: ActionFunctionArgs) { const { text } = submission.data; - const result = await processAIFilter(text, environment.id); - - if (result.success) { - return json(result); - } else { - return json(result, { status: 400 }); + const [error, result] = await tryCatch(processAIFilter(text, environment)); + if (error) { + return json({ success: false, error: error.message }, { status: 400 }); } + + return json(result); } diff --git a/apps/webapp/app/v3/services/aiRunFilterService.server.ts b/apps/webapp/app/v3/services/aiRunFilterService.server.ts index 9bcd041cee..0e81eff14b 100644 --- a/apps/webapp/app/v3/services/aiRunFilterService.server.ts +++ b/apps/webapp/app/v3/services/aiRunFilterService.server.ts @@ -1,8 +1,14 @@ import { openai } from "@ai-sdk/openai"; -import { generateObject } from "ai"; +import { generateText, Output } from "ai"; import { z } from "zod"; import { TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; +import { $replica } from "~/db.server"; import { env } from "~/env.server"; +import { getAllTaskIdentifiers } from "~/models/task.server"; +import { QueueListPresenter } from "~/presenters/v3/QueueListPresenter.server"; +import { RunTagListPresenter } from "~/presenters/v3/RunTagListPresenter.server"; +import { VersionListPresenter } from "~/presenters/v3/VersionListPresenter.server"; +import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; const AIFilterResponseSchema = z.object({ @@ -21,36 +27,133 @@ export type AIFilterResult = | { success: false; error: string; - suggestions?: string[]; + suggestions: string; }; export async function processAIFilter( text: string, - environmentId: string + environment: AuthenticatedEnvironment ): Promise { if (!env.OPENAI_API_KEY) { return { success: false, error: "OpenAI API key is not configured", - suggestions: ["Contact your administrator to configure AI features"], + suggestions: "Contact your administrator to configure AI features", }; } try { - const result = await generateObject({ + // Create presenter instances for lookups + const tagPresenter = new RunTagListPresenter(); + const versionPresenter = new VersionListPresenter(); + const queuePresenter = new QueueListPresenter(); + + const result = await generateText({ model: openai("gpt-4o"), - schema: AIFilterResponseSchema, + experimental_output: Output.object({ schema: AIFilterResponseSchema }), + tools: { + lookupTags: { + description: "Look up available tags in the environment", + parameters: z.object({ + query: z.string().optional().describe("Optional search query to filter tags"), + }), + execute: async ({ query }) => { + const tags = await tagPresenter.call({ + projectId: environment.projectId, + name: query, + page: 1, + pageSize: 50, + }); + return { + tags: tags.tags.map((tag) => tag.name), + total: tags.tags.length, + }; + }, + }, + lookupVersions: { + description: + "Look up available versions in the environment. If you specify `isCurrent` it will return a single version string if it finds one. Otherwise it will return an array of version strings.", + parameters: z.object({ + isCurrent: z.boolean().optional().describe("If true, only return the current version"), + versionPrefix: z + .string() + .optional() + .describe( + "Optional version name to filter (e.g. 20250701.1), it uses contains to compare. Don't pass `latest` or `current`, the query has to be in the reverse date format specified. Leave out to get all recent versions." + ), + }), + execute: async ({ versionPrefix, isCurrent }) => { + const versions = await versionPresenter.call({ + environment, + query: versionPrefix ? versionPrefix : undefined, + }); + + if (isCurrent) { + const currentVersion = versions.versions.find((v) => v.isCurrent); + if (currentVersion) { + return { + version: currentVersion.version, + }; + } + + if (versions.versions.length > 0) { + return { + version: versions.versions.at(0)?.version, + }; + } + } + + return { + versions: versions.versions.map((v) => v.version), + }; + }, + }, + lookupQueues: { + description: "Look up available queues in the environment", + parameters: z.object({ + query: z.string().optional().describe("Optional search query to filter queues"), + type: z + .enum(["task", "custom"]) + .optional() + .describe("Filter by queue type, only do this if the user specifies it explicitly."), + }), + execute: async ({ query, type }) => { + const queues = await queuePresenter.call({ + environment, + query, + page: 1, + type, + }); + return { + queues: queues.success ? queues.queues.map((q) => q.name) : [], + total: queues.success ? queues.queues.length : 0, + }; + }, + }, + lookupTasks: { + description: + "Look up available tasks in the environment. It will return each one. The `slug` is used for the filtering. You also get the triggerSource which is either `STANDARD` or `SCHEDULED`", + parameters: z.object({}), + execute: async () => { + const tasks = await getAllTaskIdentifiers($replica, environment.id); + return { + tasks, + total: tasks.length, + }; + }, + }, + }, prompt: `You are an AI assistant that converts natural language descriptions into structured filter parameters for a task run filtering system. Available filter options: - statuses: Array of run statuses (PENDING, EXECUTING, COMPLETED_SUCCESSFULLY, COMPLETED_WITH_ERRORS, CANCELED, TIMED_OUT, CRASHED, etc.) - period: Time period string (e.g., "1h", "7d", "30d", "1y") - from/to: Unix ms timestamps for specific time ranges. You'll need to use a converter if they give you a date. Today's date is ${new Date().toISOString()}, if they only specify a day use the current month. If they don't specify a year use the current year. If they don't specify a time of day use midnight to midnight. -- tags: Array of tag names to filter by -- tasks: Array of task identifiers to filter by +- tags: Array of tag names to filter by. Use the lookupTags tool to get the tags. +- tasks: Array of task identifiers to filter by. Use the lookupTasks tool to get the tasks. - machines: Array of machine presets (micro, small, small-2x, medium, large, xlarge, etc.) -- queues: Array of queue names to filter by -- versions: Array of version identifiers to filter by +- queues: Array of queue names to filter by. Use the lookupQueues tool to get the queues. +- versions: Array of version identifiers to filter by. Use the lookupVersions tool to get the versions. The "latest" version will be the first returned. The "current" or "deployed" version will have isCurrent set to true. - rootOnly: Boolean to show only root runs (not child runs) - runId: Array of specific run IDs to filter by - batchId: Specific batch ID to filter by @@ -70,6 +173,8 @@ Common patterns to recognize: - "using large machine" → machines: ["large-1x", "large-2x"] - "root only" → rootOnly: true +Use the available tools to look up actual tags, versions, queues, and tasks in the environment when the user mentions them. This will help you provide accurate filter values. + Unless they specify they only want root runs, set rootOnly to false. Convert the following natural language description into structured filters: @@ -81,20 +186,17 @@ Return only the filters that are explicitly mentioned or can be reasonably infer return { success: true, - filters: result.object.filters, - explanation: result.object.explanation, + filters: result.experimental_output.filters, + explanation: result.experimental_output.explanation, }; } catch (error) { - logger.error("AI filter processing failed", { error, text, environmentId }); + logger.error("AI filter processing failed", { error, text, environmentId: environment.id }); return { success: false, error: "Failed to process AI filter request", - suggestions: [ - "Try being more specific about what you want to filter", - "Use common terms like 'failed runs', 'last 7 days', 'with tag X'", - "Check that your description is clear and unambiguous", - ], + suggestions: + "Try being more specific about what you want to filter. Use common terms like 'failed runs', 'last 7 days', 'with tag X'. Check that your description is clear and unambiguous", }; } } From e19f2ce8396c57402f89818ee9c322acca1f6eb2 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 18 Jul 2025 17:25:59 +0100 Subject: [PATCH 17/34] Tool calling is working --- .../v3/services/aiRunFilterService.server.ts | 81 ++++++++++++++++--- 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/apps/webapp/app/v3/services/aiRunFilterService.server.ts b/apps/webapp/app/v3/services/aiRunFilterService.server.ts index 0e81eff14b..a14fef406d 100644 --- a/apps/webapp/app/v3/services/aiRunFilterService.server.ts +++ b/apps/webapp/app/v3/services/aiRunFilterService.server.ts @@ -1,5 +1,5 @@ import { openai } from "@ai-sdk/openai"; -import { generateText, Output } from "ai"; +import { generateText, Output, tool } from "ai"; import { z } from "zod"; import { TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; import { $replica } from "~/db.server"; @@ -12,7 +12,7 @@ import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; const AIFilterResponseSchema = z.object({ - filters: TaskRunListSearchFilters, + filters: TaskRunListSearchFilters.omit({ environments: true }), explanation: z .string() .describe("A short human-readable explanation of what filters were applied"), @@ -49,10 +49,10 @@ export async function processAIFilter( const queuePresenter = new QueueListPresenter(); const result = await generateText({ - model: openai("gpt-4o"), + model: openai("gpt-4o-mini"), experimental_output: Output.object({ schema: AIFilterResponseSchema }), tools: { - lookupTags: { + lookupTags: tool({ description: "Look up available tags in the environment", parameters: z.object({ query: z.string().optional().describe("Optional search query to filter tags"), @@ -69,8 +69,8 @@ export async function processAIFilter( total: tags.tags.length, }; }, - }, - lookupVersions: { + }), + lookupVersions: tool({ description: "Look up available versions in the environment. If you specify `isCurrent` it will return a single version string if it finds one. Otherwise it will return an array of version strings.", parameters: z.object({ @@ -107,8 +107,8 @@ export async function processAIFilter( versions: versions.versions.map((v) => v.version), }; }, - }, - lookupQueues: { + }), + lookupQueues: tool({ description: "Look up available queues in the environment", parameters: z.object({ query: z.string().optional().describe("Optional search query to filter queues"), @@ -129,8 +129,8 @@ export async function processAIFilter( total: queues.success ? queues.queues.length : 0, }; }, - }, - lookupTasks: { + }), + lookupTasks: tool({ description: "Look up available tasks in the environment. It will return each one. The `slug` is used for the filtering. You also get the triggerSource which is either `STANDARD` or `SCHEDULED`", parameters: z.object({}), @@ -141,8 +141,9 @@ export async function processAIFilter( total: tasks.length, }; }, - }, + }), }, + maxSteps: 5, prompt: `You are an AI assistant that converts natural language descriptions into structured filter parameters for a task run filtering system. Available filter options: @@ -161,6 +162,7 @@ Available filter options: Common patterns to recognize: - "failed runs" → statuses: ["COMPLETED_WITH_ERRORS", "CRASHED", "TIMED_OUT", "SYSTEM_FAILURE"]. +- "runs not dequeued yet" → statuses: ["PENDING", "PENDING_VERSION", "DELAYED"] - If they say "only failed" then only use "COMPLETED_WITH_ERRORS". - "successful runs" → statuses: ["COMPLETED_SUCCESSFULLY"] - "running runs" → statuses: ["EXECUTING", "RETRYING_AFTER_FAILURE", "WAITING_TO_RESUME"] @@ -177,6 +179,18 @@ Use the available tools to look up actual tags, versions, queues, and tasks in t Unless they specify they only want root runs, set rootOnly to false. +IMPORTANT: Return ONLY the filters that are explicitly mentioned or can be reasonably inferred. If the description is unclear or doesn't match any known patterns, return an empty filters object {} and explain why in the explanation field. + +The filters object should only contain the fields that are actually being filtered. Do not include fields with empty arrays or undefined values. + +CRITICAL: The response must be a valid JSON object with exactly this structure: +{ + "filters": { + // only include fields that have actual values + }, + "explanation": "string explaining what filters were applied" +} + Convert the following natural language description into structured filters: "${text}" @@ -184,13 +198,54 @@ Convert the following natural language description into structured filters: Return only the filters that are explicitly mentioned or can be reasonably inferred. If the description is unclear or doesn't match any known patterns, return an empty filters object and explain why in the explanation field.`, }); + // Add debugging to see what the AI returned + logger.info("AI filter response", { + text, + environmentId: environment.id, + result: result.experimental_output, + filters: result.experimental_output.filters, + }); + + // Validate the filters against the schema to catch any issues + const validationResult = TaskRunListSearchFilters.omit({ environments: true }).safeParse( + result.experimental_output.filters + ); + if (!validationResult.success) { + logger.error("AI filter validation failed", { + errors: validationResult.error.errors, + filters: result.experimental_output.filters, + }); + + return { + success: false, + error: "AI response validation failed", + suggestions: + "The AI response contained invalid filter values. Try rephrasing your request.", + }; + } + return { success: true, - filters: result.experimental_output.filters, + filters: validationResult.data, explanation: result.experimental_output.explanation, }; } catch (error) { - logger.error("AI filter processing failed", { error, text, environmentId: environment.id }); + logger.error("AI filter processing failed", { + error, + errorMessage: error instanceof Error ? error.message : String(error), + text, + environmentId: environment.id, + }); + + // If it's a schema validation error, provide more specific feedback + if (error instanceof Error && error.message.includes("schema")) { + return { + success: false, + error: "AI response format error", + suggestions: + "The AI response didn't match the expected format. Try rephrasing your request or being more specific about what you want to filter.", + }; + } return { success: false, From 8b2441600aba7add0400402f18106cab09ed4dcc Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 18 Jul 2025 17:54:45 +0100 Subject: [PATCH 18/34] Styling progress --- .../app/components/primitives/Input.tsx | 9 ++- .../app/components/runs/v3/AIFilterInput.tsx | 57 +++++++++++++++++-- .../app/components/runs/v3/RunFilters.tsx | 4 +- .../v3/services/aiRunFilterService.server.ts | 53 ++++++++++------- apps/webapp/package.json | 1 + pnpm-lock.yaml | 24 ++++++++ 6 files changed, 122 insertions(+), 26 deletions(-) diff --git a/apps/webapp/app/components/primitives/Input.tsx b/apps/webapp/app/components/primitives/Input.tsx index 4ff01b608b..532341f1a6 100644 --- a/apps/webapp/app/components/primitives/Input.tsx +++ b/apps/webapp/app/components/primitives/Input.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { useImperativeHandle, useRef } from "react"; import { cn } from "~/utils/cn"; -import { Icon, RenderIcon } from "./Icon"; +import { Icon, type RenderIcon } from "./Icon"; const containerBase = "has-[:focus-visible]:outline-none has-[:focus-visible]:ring-1 has-[:focus-visible]:ring-charcoal-650 has-[:focus-visible]:ring-offset-0 has-[:focus]:border-ring has-[:focus]:outline-none has-[:focus]:ring-1 has-[:focus]:ring-ring has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 ring-offset-background transition cursor-text"; @@ -37,6 +37,13 @@ const variants = { iconSize: "size-3 ml-0.5", accessory: "pr-0.5", }, + "secondary-small": { + container: + "px-1 h-6 w-full rounded border border-charcoal-600 hover:border-charcoal-550 bg-secondary hover:bg-charcoal-650", + input: "px-1 rounded text-xs", + iconSize: "size-3 ml-0.5", + accessory: "pr-0.5", + }, }; export type InputProps = React.InputHTMLAttributes & { diff --git a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx index ad3c97b2b2..54ee789ffa 100644 --- a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx +++ b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx @@ -1,5 +1,6 @@ +import { Portal } from "@radix-ui/react-portal"; import { useFetcher, useNavigate } from "@remix-run/react"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { AISparkleIcon } from "~/assets/icons/AISparkleIcon"; import { Input } from "~/components/primitives/Input"; import { ShortcutKey } from "~/components/primitives/ShortcutKey"; @@ -32,13 +33,29 @@ export function AIFilterInput() { const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); - + const inputRef = useRef(null); const fetcher = useFetcher(); + // Calculate position for error message + const [errorPosition, setErrorPosition] = useState({ top: 0, left: 0, width: 0 }); + + useEffect(() => { + if (fetcher.data?.success === false && inputRef.current) { + const rect = inputRef.current.getBoundingClientRect(); + setErrorPosition({ + top: rect.bottom + window.scrollY, + left: rect.left + window.scrollX, + width: rect.width, + }); + } + }, [fetcher.data?.success]); + useEffect(() => { if (fetcher.data?.success && fetcher.state === "loading") { // Clear the input after successful application setText(""); + // Ensure focus is removed after successful submission + setIsFocused(false); const searchParams = objectToSearchParams(fetcher.data.filters); if (!searchParams) { @@ -52,6 +69,11 @@ export function AIFilterInput() { navigate(`${location.pathname}?${searchParams.toString()}`, { replace: true }); + //focus the input again + if (inputRef.current) { + inputRef.current.focus(); + } + // TODO: Show success message with explanation console.log(`AI applied filters: ${fetcher.data.explanation}`); } else if (fetcher.data?.success === false) { @@ -70,22 +92,28 @@ export function AIFilterInput() { > 0 ? "24rem" : "auto" }} transition={{ type: "spring", stiffness: 300, damping: 30, }} + className="animated-gradient-glow relative" > setText(e.target.value)} disabled={isLoading} fullWidth + ref={inputRef} + className={cn( + "placeholder:text-text-bright", + isFocused && "placeholder:text-text-dimmed" + )} onKeyDown={(e) => { if (e.key === "Enter" && text.trim() && !isLoading) { e.preventDefault(); @@ -96,7 +124,12 @@ export function AIFilterInput() { } }} onFocus={() => setIsFocused(true)} - onBlur={() => setIsFocused(false)} + onBlur={() => { + // Only blur if the text is empty or we're not loading + if (text.length === 0 || !isLoading) { + setIsFocused(false); + } + }} icon={} accessory={ isLoading ? ( @@ -110,6 +143,20 @@ export function AIFilterInput() { ) : undefined } /> + {fetcher.data?.success === false && ( + +
+ {fetcher.data.error} +
+
+ )}
); diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index c53f391f2a..6a9f332c41 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -395,7 +395,9 @@ function FilterMenu(props: RunFiltersProps) { shortcut={shortcut} tooltipTitle={"Filter runs"} className="pr-0.5" - /> + > + <> + ); return ( diff --git a/apps/webapp/app/v3/services/aiRunFilterService.server.ts b/apps/webapp/app/v3/services/aiRunFilterService.server.ts index a14fef406d..bc5d6b7972 100644 --- a/apps/webapp/app/v3/services/aiRunFilterService.server.ts +++ b/apps/webapp/app/v3/services/aiRunFilterService.server.ts @@ -11,12 +11,21 @@ import { VersionListPresenter } from "~/presenters/v3/VersionListPresenter.serve import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; -const AIFilterResponseSchema = z.object({ - filters: TaskRunListSearchFilters.omit({ environments: true }), - explanation: z - .string() - .describe("A short human-readable explanation of what filters were applied"), -}); +const AIFilterResponseSchema = z + .discriminatedUnion("success", [ + z.object({ + success: z.literal(true), + filters: TaskRunListSearchFilters.omit({ environments: true }), + explanation: z + .string() + .describe("A short human-readable explanation of what filters were applied"), + }), + z.object({ + success: z.literal(false), + error: z.string(), + }), + ]) + .describe("The response from the AI filter service"); export type AIFilterResult = | { @@ -27,7 +36,6 @@ export type AIFilterResult = | { success: false; error: string; - suggestions: string; }; export async function processAIFilter( @@ -38,7 +46,6 @@ export async function processAIFilter( return { success: false, error: "OpenAI API key is not configured", - suggestions: "Contact your administrator to configure AI features", }; } @@ -185,17 +192,25 @@ The filters object should only contain the fields that are actually being filter CRITICAL: The response must be a valid JSON object with exactly this structure: { + "success": true, "filters": { // only include fields that have actual values }, "explanation": "string explaining what filters were applied" } +or if you can't figure out the filters then return: +{ + "success": false, + "error": "" +} + +Make the error no more than 8 words. + Convert the following natural language description into structured filters: "${text}" - -Return only the filters that are explicitly mentioned or can be reasonably inferred. If the description is unclear or doesn't match any known patterns, return an empty filters object and explain why in the explanation field.`, +`, }); // Add debugging to see what the AI returned @@ -203,9 +218,15 @@ Return only the filters that are explicitly mentioned or can be reasonably infer text, environmentId: environment.id, result: result.experimental_output, - filters: result.experimental_output.filters, }); + if (!result.experimental_output.success) { + return { + success: false, + error: result.experimental_output.error, + }; + } + // Validate the filters against the schema to catch any issues const validationResult = TaskRunListSearchFilters.omit({ environments: true }).safeParse( result.experimental_output.filters @@ -219,8 +240,6 @@ Return only the filters that are explicitly mentioned or can be reasonably infer return { success: false, error: "AI response validation failed", - suggestions: - "The AI response contained invalid filter values. Try rephrasing your request.", }; } @@ -241,17 +260,13 @@ Return only the filters that are explicitly mentioned or can be reasonably infer if (error instanceof Error && error.message.includes("schema")) { return { success: false, - error: "AI response format error", - suggestions: - "The AI response didn't match the expected format. Try rephrasing your request or being more specific about what you want to filter.", + error: error.message, }; } return { success: false, - error: "Failed to process AI filter request", - suggestions: - "Try being more specific about what you want to filter. Use common terms like 'failed runs', 'last 7 days', 'with tag X'. Check that your description is clear and unambiguous", + error: error instanceof Error ? error.message : String(error), }; } } diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 2c0713043e..76b027b584 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -85,6 +85,7 @@ "@radix-ui/react-dialog": "^1.0.3", "@radix-ui/react-label": "^2.0.1", "@radix-ui/react-popover": "^1.0.5", + "@radix-ui/react-portal": "^1.1.9", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^1.2.1", "@radix-ui/react-slider": "^1.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5df2a8808b..685809c848 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -350,6 +350,9 @@ importers: '@radix-ui/react-popover': specifier: ^1.0.5 version: 1.0.5(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': + specifier: ^1.1.9 + version: 1.1.9(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-radio-group': specifier: ^1.1.3 version: 1.1.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0) @@ -12123,6 +12126,27 @@ packages: react-dom: 18.2.0(react@18.3.1) dev: false + /@radix-ui/react-portal@1.1.9(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.2.7)(@types/react@18.2.69)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.2.69)(react@18.2.0) + '@types/react': 18.2.69 + '@types/react-dom': 18.2.7 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-presence@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==} peerDependencies: From e7ac8f978407d4332add949a57152945b6de328d Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sat, 19 Jul 2025 14:17:28 +0100 Subject: [PATCH 19/34] Working on the error --- .../app/components/runs/v3/AIFilterInput.tsx | 170 +++++++++++------- 1 file changed, 104 insertions(+), 66 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx index 54ee789ffa..21c03555fe 100644 --- a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx +++ b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx @@ -13,6 +13,7 @@ import { objectToSearchParams } from "~/utils/searchParams"; import { type TaskRunListSearchFilters } from "./RunFilters"; import { cn } from "~/utils/cn"; import { motion } from "framer-motion"; +import { Popover, PopoverContent, PopoverTrigger } from "~/components/primitives/Popover"; type AIFilterResult = | { @@ -90,74 +91,111 @@ export function AIFilterInput() { action={`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/runs/ai-filter`} method="post" > - 0 ? "24rem" : "auto" }} - transition={{ - type: "spring", - stiffness: 300, - damping: 30, - }} - className="animated-gradient-glow relative" - > - setText(e.target.value)} - disabled={isLoading} - fullWidth - ref={inputRef} - className={cn( - "placeholder:text-text-bright", - isFocused && "placeholder:text-text-dimmed" - )} - onKeyDown={(e) => { - if (e.key === "Enter" && text.trim() && !isLoading) { - e.preventDefault(); - const form = e.currentTarget.closest("form"); - if (form) { - form.requestSubmit(); - } - } + + 0 ? "24rem" : "auto" }} + transition={{ + type: "spring", + stiffness: 300, + damping: 30, }} - onFocus={() => setIsFocused(true)} - onBlur={() => { - // Only blur if the text is empty or we're not loading - if (text.length === 0 || !isLoading) { - setIsFocused(false); + className="animated-gradient-glow relative" + > + setText(e.target.value)} + disabled={isLoading} + fullWidth + ref={inputRef} + className={cn( + "placeholder:text-text-bright", + isFocused && "placeholder:text-text-dimmed" + )} + onKeyDown={(e) => { + if (e.key === "Enter" && text.trim() && !isLoading) { + e.preventDefault(); + const form = e.currentTarget.closest("form"); + if (form) { + form.requestSubmit(); + } + } + }} + onFocus={() => setIsFocused(true)} + onBlur={() => { + // Only blur if the text is empty or we're not loading + if (text.length === 0 || !isLoading) { + setIsFocused(false); + } + }} + icon={} + accessory={ + isLoading ? ( + + ) : text.length > 0 ? ( + + ) : undefined } - }} - icon={} - accessory={ - isLoading ? ( - - ) : text.length > 0 ? ( - - ) : undefined - } - /> - {fetcher.data?.success === false && ( - -
- {fetcher.data.error} -
-
- )} -
+ /> + {fetcher.data?.success === false && ( + +
+ {fetcher.data.error} +
+
+ )} +
+ ); } + +function ErrorPopover({ + children, + error, + durationMs = 2_000, +}: { + children: React.ReactNode; + error?: string; + durationMs?: number; +}) { + const [isOpen, setIsOpen] = useState(false); + const timeout = useRef(); + + useEffect(() => { + if (timeout.current) { + clearTimeout(timeout.current); + } + timeout.current = setTimeout(() => { + setIsOpen((s) => true); + }, durationMs); + + return () => { + if (timeout.current) { + clearTimeout(timeout.current); + } + }; + }, [error, durationMs]); + + return ( + + {children} + {error} + + ); +} From 13735cc4bd02e414347a77cd6d1115e9eb4c2fb9 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sat, 19 Jul 2025 20:15:53 +0100 Subject: [PATCH 20/34] Errors work, improved the styling --- .../app/components/primitives/Input.tsx | 18 ++++- .../app/components/runs/v3/AIFilterInput.tsx | 71 +++++++++---------- 2 files changed, 51 insertions(+), 38 deletions(-) diff --git a/apps/webapp/app/components/primitives/Input.tsx b/apps/webapp/app/components/primitives/Input.tsx index 532341f1a6..d2671a1b1c 100644 --- a/apps/webapp/app/components/primitives/Input.tsx +++ b/apps/webapp/app/components/primitives/Input.tsx @@ -51,14 +51,27 @@ export type InputProps = React.InputHTMLAttributes & { icon?: RenderIcon; accessory?: React.ReactNode; fullWidth?: boolean; + containerClassName?: string; }; const Input = React.forwardRef( - ({ className, type, accessory, fullWidth = true, variant = "medium", icon, ...props }, ref) => { + ( + { + className, + type, + accessory, + fullWidth = true, + variant = "medium", + icon, + containerClassName, + ...props + }, + ref + ) => { const innerRef = useRef(null); useImperativeHandle(ref, () => innerRef.current as HTMLInputElement); - const containerClassName = variants[variant].container; + const variantContainerClassName = variants[variant].container; const inputClassName = variants[variant].input; const iconClassName = variants[variant].iconSize; @@ -67,6 +80,7 @@ const Input = React.forwardRef( className={cn( "flex items-center", containerBase, + variantContainerClassName, containerClassName, fullWidth ? "w-full" : "max-w-max" )} diff --git a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx index 21c03555fe..d839e2ac92 100644 --- a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx +++ b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx @@ -12,7 +12,7 @@ import { useSearchParams } from "~/hooks/useSearchParam"; import { objectToSearchParams } from "~/utils/searchParams"; import { type TaskRunListSearchFilters } from "./RunFilters"; import { cn } from "~/utils/cn"; -import { motion } from "framer-motion"; +import { motion, AnimatePresence } from "framer-motion"; import { Popover, PopoverContent, PopoverTrigger } from "~/components/primitives/Popover"; type AIFilterResult = @@ -37,20 +37,6 @@ export function AIFilterInput() { const inputRef = useRef(null); const fetcher = useFetcher(); - // Calculate position for error message - const [errorPosition, setErrorPosition] = useState({ top: 0, left: 0, width: 0 }); - - useEffect(() => { - if (fetcher.data?.success === false && inputRef.current) { - const rect = inputRef.current.getBoundingClientRect(); - setErrorPosition({ - top: rect.bottom + window.scrollY, - left: rect.left + window.scrollX, - width: rect.width, - }); - } - }, [fetcher.data?.success]); - useEffect(() => { if (fetcher.data?.success && fetcher.state === "loading") { // Clear the input after successful application @@ -100,8 +86,19 @@ export function AIFilterInput() { stiffness: 300, damping: 30, }} - className="animated-gradient-glow relative" + className="relative" > + + {isFocused && ( + + )} + { if (e.key === "Enter" && text.trim() && !isLoading) { e.preventDefault(); @@ -135,7 +133,13 @@ export function AIFilterInput() { icon={} accessory={ isLoading ? ( - + ) : text.length > 0 ? ( - {fetcher.data?.success === false && ( - -
- {fetcher.data.error} -
-
- )} @@ -168,7 +158,7 @@ export function AIFilterInput() { function ErrorPopover({ children, error, - durationMs = 2_000, + durationMs = 10_000, }: { children: React.ReactNode; error?: string; @@ -178,11 +168,14 @@ function ErrorPopover({ const timeout = useRef(); useEffect(() => { + if (error) { + setIsOpen(true); + } if (timeout.current) { clearTimeout(timeout.current); } timeout.current = setTimeout(() => { - setIsOpen((s) => true); + setIsOpen(false); }, durationMs); return () => { @@ -193,9 +186,15 @@ function ErrorPopover({ }, [error, durationMs]); return ( - + {children} - {error} + + {error} + ); } From 958395d1932371b1ddc17c922fd5d0a1e7c5c194 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 20 Jul 2025 12:05:30 +0100 Subject: [PATCH 21/34] Nice glow effect --- .../app/components/runs/v3/AIFilterInput.tsx | 102 +++++++++--------- apps/webapp/app/tailwind.css | 25 +++++ 2 files changed, 77 insertions(+), 50 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx index d839e2ac92..3379637eee 100644 --- a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx +++ b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx @@ -86,7 +86,7 @@ export function AIFilterInput() { stiffness: 300, damping: 30, }} - className="relative" + className="relative h-6 min-w-44" > {isFocused && ( @@ -95,60 +95,62 @@ export function AIFilterInput() { animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.2, ease: "linear" }} - className="animated-gradient-glow pointer-events-none absolute inset-0" + className="animated-gradient-glow-small pointer-events-none absolute inset-0 h-6" /> )} - setText(e.target.value)} - disabled={isLoading} - fullWidth - ref={inputRef} - className={cn( - "disabled:text-text-dimmed/50", - isFocused && "placeholder:text-text-dimmed" - )} - containerClassName="has-[:disabled]:opacity-100" - onKeyDown={(e) => { - if (e.key === "Enter" && text.trim() && !isLoading) { - e.preventDefault(); - const form = e.currentTarget.closest("form"); - if (form) { - form.requestSubmit(); +
+ setText(e.target.value)} + disabled={isLoading} + fullWidth + ref={inputRef} + className={cn( + "disabled:text-text-dimmed/50", + isFocused && "placeholder:text-text-dimmed" + )} + containerClassName="has-[:disabled]:opacity-100" + onKeyDown={(e) => { + if (e.key === "Enter" && text.trim() && !isLoading) { + e.preventDefault(); + const form = e.currentTarget.closest("form"); + if (form) { + form.requestSubmit(); + } } + }} + onFocus={() => setIsFocused(true)} + onBlur={() => { + // Only blur if the text is empty or we're not loading + if (text.length === 0 || !isLoading) { + setIsFocused(false); + } + }} + icon={} + accessory={ + isLoading ? ( + + ) : text.length > 0 ? ( + + ) : undefined } - }} - onFocus={() => setIsFocused(true)} - onBlur={() => { - // Only blur if the text is empty or we're not loading - if (text.length === 0 || !isLoading) { - setIsFocused(false); - } - }} - icon={} - accessory={ - isLoading ? ( - - ) : text.length > 0 ? ( - - ) : undefined - } - /> + /> +
diff --git a/apps/webapp/app/tailwind.css b/apps/webapp/app/tailwind.css index 860ffa0e74..3d68e87811 100644 --- a/apps/webapp/app/tailwind.css +++ b/apps/webapp/app/tailwind.css @@ -71,6 +71,31 @@ filter: blur(0.5rem); opacity: 0.1; } + + .animated-gradient-glow-small { + position: relative; + overflow: visible; + } + + .animated-gradient-glow-small::before { + content: ""; + position: absolute; + inset: -1px; + z-index: -1; + background: conic-gradient( + from var(--gradient-angle), + rgb(99 102 241), + rgb(245 158 11), + rgb(236 72 153), + rgb(245 158 11), + rgb(99 102 241) + ); + border-radius: inherit; + animation: gradient-rotation 3s linear infinite; + pointer-events: none; + filter: blur(0.2rem); + opacity: 0.3; + } } @keyframes gradient-rotation { From dfb0f86eff7aa1cab7a9b30fd022667fe6e6b986 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 20 Jul 2025 12:07:49 +0100 Subject: [PATCH 22/34] Tweak the darkness of the text field --- apps/webapp/app/components/primitives/Input.tsx | 2 +- apps/webapp/app/components/runs/v3/AIFilterInput.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/primitives/Input.tsx b/apps/webapp/app/components/primitives/Input.tsx index d2671a1b1c..5b421fd3b9 100644 --- a/apps/webapp/app/components/primitives/Input.tsx +++ b/apps/webapp/app/components/primitives/Input.tsx @@ -39,7 +39,7 @@ const variants = { }, "secondary-small": { container: - "px-1 h-6 w-full rounded border border-charcoal-600 hover:border-charcoal-550 bg-secondary hover:bg-charcoal-650", + "px-1 h-6 w-full rounded border border-charcoal-600 hover:border-charcoal-550 bg-grid-dimmed hover:bg-charcoal-650", input: "px-1 rounded text-xs", iconSize: "size-3 ml-0.5", accessory: "pr-0.5", diff --git a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx index 3379637eee..b0ba3b9698 100644 --- a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx +++ b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx @@ -112,7 +112,7 @@ export function AIFilterInput() { ref={inputRef} className={cn( "disabled:text-text-dimmed/50", - isFocused && "placeholder:text-text-dimmed" + isFocused && "placeholder:text-text-dimmed/70" )} containerClassName="has-[:disabled]:opacity-100" onKeyDown={(e) => { From d4e332aa116c4d417f44285980acc5ef6d1614cb Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 11:48:14 +0100 Subject: [PATCH 23/34] Re-ordered the UI, set AI settings to use system prompt and telemetry --- apps/webapp/app/components/runs/v3/RunFilters.tsx | 2 +- .../app/v3/services/aiRunFilterService.server.ts | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index 6a9f332c41..9eae1e1eb5 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -343,8 +343,8 @@ export function RunsFilters(props: RunFiltersProps) { return (
- + diff --git a/apps/webapp/app/v3/services/aiRunFilterService.server.ts b/apps/webapp/app/v3/services/aiRunFilterService.server.ts index bc5d6b7972..1aa74da96a 100644 --- a/apps/webapp/app/v3/services/aiRunFilterService.server.ts +++ b/apps/webapp/app/v3/services/aiRunFilterService.server.ts @@ -151,7 +151,7 @@ export async function processAIFilter( }), }, maxSteps: 5, - prompt: `You are an AI assistant that converts natural language descriptions into structured filter parameters for a task run filtering system. + system: `You are an AI assistant that converts natural language descriptions into structured filter parameters for a task run filtering system. Available filter options: - statuses: Array of run statuses (PENDING, EXECUTING, COMPLETED_SUCCESSFULLY, COMPLETED_WITH_ERRORS, CANCELED, TIMED_OUT, CRASHED, etc.) @@ -206,11 +206,16 @@ or if you can't figure out the filters then return: } Make the error no more than 8 words. - -Convert the following natural language description into structured filters: - -"${text}" `, + prompt: text, + experimental_telemetry: { + isEnabled: true, + metadata: { + environmentId: environment.id, + projectId: environment.projectId, + organizationId: environment.organizationId, + }, + }, }); // Add debugging to see what the AI returned From f156160de45674857e43e12b78306d0f0e83f6c6 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 12:52:28 +0100 Subject: [PATCH 24/34] Refactored to make it testable --- ...jectParam.env.$envParam.runs.ai-filter.tsx | 93 +++- .../v3/services/aiRunFilterService.server.ts | 437 +++++++++--------- 2 files changed, 307 insertions(+), 223 deletions(-) diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx index 051b9c633d..f11c7e1e9d 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx @@ -4,9 +4,21 @@ import { requireUserId } from "~/services/session.server"; import { EnvironmentParamSchema } from "~/utils/pathBuilder"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; -import { processAIFilter } from "~/v3/services/aiRunFilterService.server"; import { type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; import { tryCatch } from "@trigger.dev/core"; +import { + AIRunFilterService, + QueryQueues, + QueryTags, + QueryTasks, + QueryVersions, +} from "~/v3/services/aiRunFilterService.server"; +import { RunTagListPresenter } from "~/presenters/v3/RunTagListPresenter.server"; +import { QueueListPresenter } from "~/presenters/v3/QueueListPresenter.server"; +import { VersionListPresenter } from "~/presenters/v3/VersionListPresenter.server"; +import { TaskListPresenter } from "~/presenters/v3/TaskListPresenter.server"; +import { getAllTaskIdentifiers } from "~/models/task.server"; +import { $replica } from "~/db.server"; const RequestSchema = z.object({ text: z.string().min(1), @@ -48,7 +60,84 @@ export async function action({ request, params }: ActionFunctionArgs) { const { text } = submission.data; - const [error, result] = await tryCatch(processAIFilter(text, environment)); + //Tags querying + const queryTags: QueryTags = { + query: async (search) => { + const tagPresenter = new RunTagListPresenter(); + const tags = await tagPresenter.call({ + projectId: environment.projectId, + name: search, + page: 1, + pageSize: 50, + }); + return { + tags: tags.tags.map((t) => t.name), + }; + }, + }; + + const queryQueues: QueryQueues = { + query: async (query, type) => { + const queuePresenter = new QueueListPresenter(); + const queues = await queuePresenter.call({ + environment, + query, + page: 1, + type, + }); + return { + queues: queues.success ? queues.queues.map((q) => q.name) : [], + }; + }, + }; + + const queryVersions: QueryVersions = { + query: async (versionPrefix, isCurrent) => { + const versionPresenter = new VersionListPresenter(); + const versions = await versionPresenter.call({ + environment, + query: versionPrefix ? versionPrefix : undefined, + }); + + if (isCurrent) { + const currentVersion = versions.versions.find((v) => v.isCurrent); + if (currentVersion) { + return { + version: currentVersion.version, + }; + } + + const newestVersion = versions.versions.at(0)?.version; + if (newestVersion) { + return { + version: newestVersion, + }; + } + } + + return { + versions: versions.versions.map((v) => v.version), + }; + }, + }; + + const queryTasks: QueryTasks = { + query: async () => { + const tasks = await getAllTaskIdentifiers($replica, environment.id); + return { + tasks, + }; + }, + }; + + const service = new AIRunFilterService({ + queryTags, + queryVersions, + queryQueues, + queryTasks, + }); + + const [error, result] = await tryCatch(service.call(text, environment)); if (error) { return json({ success: false, error: error.message }, { status: 400 }); } diff --git a/apps/webapp/app/v3/services/aiRunFilterService.server.ts b/apps/webapp/app/v3/services/aiRunFilterService.server.ts index 1aa74da96a..284858c404 100644 --- a/apps/webapp/app/v3/services/aiRunFilterService.server.ts +++ b/apps/webapp/app/v3/services/aiRunFilterService.server.ts @@ -1,13 +1,9 @@ import { openai } from "@ai-sdk/openai"; +import { type TaskTriggerSource } from "@trigger.dev/database"; import { generateText, Output, tool } from "ai"; import { z } from "zod"; import { TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; -import { $replica } from "~/db.server"; import { env } from "~/env.server"; -import { getAllTaskIdentifiers } from "~/models/task.server"; -import { QueueListPresenter } from "~/presenters/v3/QueueListPresenter.server"; -import { RunTagListPresenter } from "~/presenters/v3/RunTagListPresenter.server"; -import { VersionListPresenter } from "~/presenters/v3/VersionListPresenter.server"; import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; @@ -22,11 +18,46 @@ const AIFilterResponseSchema = z }), z.object({ success: z.literal(false), - error: z.string(), + error: z.string().describe("A short human-readable error message"), }), ]) .describe("The response from the AI filter service"); +export interface QueryQueues { + query( + search: string | undefined, + type: "task" | "custom" | undefined + ): Promise<{ + queues: string[]; + }>; +} + +export interface QueryVersions { + query( + versionPrefix: string | undefined, + isCurrent: boolean | undefined + ): Promise< + | { + versions: string[]; + } + | { + version: string; + } + >; +} + +export interface QueryTags { + query(search: string | undefined): Promise<{ + tags: string[]; + }>; +} + +export interface QueryTasks { + query(): Promise<{ + tasks: { slug: string; triggerSource: TaskTriggerSource }[]; + }>; +} + export type AIFilterResult = | { success: true; @@ -38,240 +69,204 @@ export type AIFilterResult = error: string; }; -export async function processAIFilter( - text: string, - environment: AuthenticatedEnvironment -): Promise { - if (!env.OPENAI_API_KEY) { - return { - success: false, - error: "OpenAI API key is not configured", - }; - } +export class AIRunFilterService { + constructor( + private readonly queryFns: { + queryTags: QueryTags; + queryVersions: QueryVersions; + queryQueues: QueryQueues; + queryTasks: QueryTasks; + } + ) {} - try { - // Create presenter instances for lookups - const tagPresenter = new RunTagListPresenter(); - const versionPresenter = new VersionListPresenter(); - const queuePresenter = new QueueListPresenter(); + async call(text: string, environment: AuthenticatedEnvironment): Promise { + if (!env.OPENAI_API_KEY) { + return { + success: false, + error: "OpenAI API key is not configured", + }; + } - const result = await generateText({ - model: openai("gpt-4o-mini"), - experimental_output: Output.object({ schema: AIFilterResponseSchema }), - tools: { - lookupTags: tool({ - description: "Look up available tags in the environment", - parameters: z.object({ - query: z.string().optional().describe("Optional search query to filter tags"), + try { + const result = await generateText({ + model: openai("gpt-4o-mini"), + experimental_output: Output.object({ schema: AIFilterResponseSchema }), + tools: { + lookupTags: tool({ + description: "Look up available tags in the environment", + parameters: z.object({ + query: z.string().optional().describe("Optional search query to filter tags"), + }), + execute: async ({ query }) => { + return await this.queryFns.queryTags.query(query); + }, }), - execute: async ({ query }) => { - const tags = await tagPresenter.call({ - projectId: environment.projectId, - name: query, - page: 1, - pageSize: 50, - }); - return { - tags: tags.tags.map((tag) => tag.name), - total: tags.tags.length, - }; - }, - }), - lookupVersions: tool({ - description: - "Look up available versions in the environment. If you specify `isCurrent` it will return a single version string if it finds one. Otherwise it will return an array of version strings.", - parameters: z.object({ - isCurrent: z.boolean().optional().describe("If true, only return the current version"), - versionPrefix: z - .string() - .optional() - .describe( - "Optional version name to filter (e.g. 20250701.1), it uses contains to compare. Don't pass `latest` or `current`, the query has to be in the reverse date format specified. Leave out to get all recent versions." - ), + lookupVersions: tool({ + description: + "Look up available versions in the environment. If you specify `isCurrent` it will return a single version string if it finds one. Otherwise it will return an array of version strings.", + parameters: z.object({ + isCurrent: z + .boolean() + .optional() + .describe("If true, only return the current version"), + versionPrefix: z + .string() + .optional() + .describe( + "Optional version name to filter (e.g. 20250701.1), it uses contains to compare. Don't pass `latest` or `current`, the query has to be in the reverse date format specified. Leave out to get all recent versions." + ), + }), + execute: async ({ versionPrefix, isCurrent }) => { + return await this.queryFns.queryVersions.query(versionPrefix, isCurrent); + }, }), - execute: async ({ versionPrefix, isCurrent }) => { - const versions = await versionPresenter.call({ - environment, - query: versionPrefix ? versionPrefix : undefined, - }); - - if (isCurrent) { - const currentVersion = versions.versions.find((v) => v.isCurrent); - if (currentVersion) { - return { - version: currentVersion.version, - }; - } - - if (versions.versions.length > 0) { - return { - version: versions.versions.at(0)?.version, - }; - } - } - - return { - versions: versions.versions.map((v) => v.version), - }; - }, - }), - lookupQueues: tool({ - description: "Look up available queues in the environment", - parameters: z.object({ - query: z.string().optional().describe("Optional search query to filter queues"), - type: z - .enum(["task", "custom"]) - .optional() - .describe("Filter by queue type, only do this if the user specifies it explicitly."), + lookupQueues: tool({ + description: "Look up available queues in the environment", + parameters: z.object({ + query: z.string().optional().describe("Optional search query to filter queues"), + type: z + .enum(["task", "custom"]) + .optional() + .describe( + "Filter by queue type, only do this if the user specifies it explicitly." + ), + }), + execute: async ({ query, type }) => { + return await this.queryFns.queryQueues.query(query, type); + }, }), - execute: async ({ query, type }) => { - const queues = await queuePresenter.call({ - environment, - query, - page: 1, - type, - }); - return { - queues: queues.success ? queues.queues.map((q) => q.name) : [], - total: queues.success ? queues.queues.length : 0, - }; - }, - }), - lookupTasks: tool({ - description: - "Look up available tasks in the environment. It will return each one. The `slug` is used for the filtering. You also get the triggerSource which is either `STANDARD` or `SCHEDULED`", - parameters: z.object({}), - execute: async () => { - const tasks = await getAllTaskIdentifiers($replica, environment.id); - return { - tasks, - total: tasks.length, - }; + lookupTasks: tool({ + description: + "Look up available tasks in the environment. It will return each one. The `slug` is used for the filtering. You also get the triggerSource which is either `STANDARD` or `SCHEDULED`", + parameters: z.object({}), + execute: async () => { + return await this.queryFns.queryTasks.query(); + }, + }), + }, + maxSteps: 5, + system: `You are an AI assistant that converts natural language descriptions into structured filter parameters for a task run filtering system. + + Available filter options: + - statuses: Array of run statuses (PENDING, EXECUTING, COMPLETED_SUCCESSFULLY, COMPLETED_WITH_ERRORS, CANCELED, TIMED_OUT, CRASHED, etc.) + - period: Time period string (e.g., "1h", "7d", "30d", "1y") + - from/to: Unix ms timestamps for specific time ranges. You'll need to use a converter if they give you a date. Today's date is ${new Date().toISOString()}, if they only specify a day use the current month. If they don't specify a year use the current year. If they don't specify a time of day use midnight to midnight. + - tags: Array of tag names to filter by. Use the lookupTags tool to get the tags. + - tasks: Array of task identifiers to filter by. Use the lookupTasks tool to get the tasks. + - machines: Array of machine presets (micro, small, small-2x, medium, large, xlarge, etc.) + - queues: Array of queue names to filter by. Use the lookupQueues tool to get the queues. + - versions: Array of version identifiers to filter by. Use the lookupVersions tool to get the versions. The "latest" version will be the first returned. The "current" or "deployed" version will have isCurrent set to true. + - rootOnly: Boolean to show only root runs (not child runs) + - runId: Array of specific run IDs to filter by + - batchId: Specific batch ID to filter by + - scheduleId: Specific schedule ID to filter by + + Common patterns to recognize: + - "failed runs" → statuses: ["COMPLETED_WITH_ERRORS", "CRASHED", "TIMED_OUT", "SYSTEM_FAILURE"]. + - "runs not dequeued yet" → statuses: ["PENDING", "PENDING_VERSION", "DELAYED"] + - If they say "only failed" then only use "COMPLETED_WITH_ERRORS". + - "successful runs" → statuses: ["COMPLETED_SUCCESSFULLY"] + - "running runs" → statuses: ["EXECUTING", "RETRYING_AFTER_FAILURE", "WAITING_TO_RESUME"] + - "pending runs" → statuses: ["PENDING", "PENDING_VERSION", "DELAYED"] + - "past 7 days" → period: "7d" + - "last hour" → period: "1h" + - "this month" → period: "30d" + - "with tag X" → tags: ["X"] + - "from task Y" → tasks: ["Y"] + - "using large machine" → machines: ["large-1x", "large-2x"] + - "root only" → rootOnly: true + + Use the available tools to look up actual tags, versions, queues, and tasks in the environment when the user mentions them. This will help you provide accurate filter values. + + Unless they specify they only want root runs, set rootOnly to false. + + IMPORTANT: Return ONLY the filters that are explicitly mentioned or can be reasonably inferred. If the description is unclear or doesn't match any known patterns, return an empty filters object {} and explain why in the explanation field. + + The filters object should only contain the fields that are actually being filtered. Do not include fields with empty arrays or undefined values. + + CRITICAL: The response must be a valid JSON object with exactly this structure: + { + "success": true, + "filters": { + // only include fields that have actual values + }, + "explanation": "string explaining what filters were applied" + } + + or if you can't figure out the filters then return: + { + "success": false, + "error": "" + } + + Make the error no more than 8 words. + `, + prompt: text, + experimental_telemetry: { + isEnabled: true, + metadata: { + environmentId: environment.id, + projectId: environment.projectId, + organizationId: environment.organizationId, }, - }), - }, - maxSteps: 5, - system: `You are an AI assistant that converts natural language descriptions into structured filter parameters for a task run filtering system. - -Available filter options: -- statuses: Array of run statuses (PENDING, EXECUTING, COMPLETED_SUCCESSFULLY, COMPLETED_WITH_ERRORS, CANCELED, TIMED_OUT, CRASHED, etc.) -- period: Time period string (e.g., "1h", "7d", "30d", "1y") -- from/to: Unix ms timestamps for specific time ranges. You'll need to use a converter if they give you a date. Today's date is ${new Date().toISOString()}, if they only specify a day use the current month. If they don't specify a year use the current year. If they don't specify a time of day use midnight to midnight. -- tags: Array of tag names to filter by. Use the lookupTags tool to get the tags. -- tasks: Array of task identifiers to filter by. Use the lookupTasks tool to get the tasks. -- machines: Array of machine presets (micro, small, small-2x, medium, large, xlarge, etc.) -- queues: Array of queue names to filter by. Use the lookupQueues tool to get the queues. -- versions: Array of version identifiers to filter by. Use the lookupVersions tool to get the versions. The "latest" version will be the first returned. The "current" or "deployed" version will have isCurrent set to true. -- rootOnly: Boolean to show only root runs (not child runs) -- runId: Array of specific run IDs to filter by -- batchId: Specific batch ID to filter by -- scheduleId: Specific schedule ID to filter by - -Common patterns to recognize: -- "failed runs" → statuses: ["COMPLETED_WITH_ERRORS", "CRASHED", "TIMED_OUT", "SYSTEM_FAILURE"]. -- "runs not dequeued yet" → statuses: ["PENDING", "PENDING_VERSION", "DELAYED"] -- If they say "only failed" then only use "COMPLETED_WITH_ERRORS". -- "successful runs" → statuses: ["COMPLETED_SUCCESSFULLY"] -- "running runs" → statuses: ["EXECUTING", "RETRYING_AFTER_FAILURE", "WAITING_TO_RESUME"] -- "pending runs" → statuses: ["PENDING", "PENDING_VERSION", "DELAYED"] -- "past 7 days" → period: "7d" -- "last hour" → period: "1h" -- "this month" → period: "30d" -- "with tag X" → tags: ["X"] -- "from task Y" → tasks: ["Y"] -- "using large machine" → machines: ["large-1x", "large-2x"] -- "root only" → rootOnly: true - -Use the available tools to look up actual tags, versions, queues, and tasks in the environment when the user mentions them. This will help you provide accurate filter values. - -Unless they specify they only want root runs, set rootOnly to false. - -IMPORTANT: Return ONLY the filters that are explicitly mentioned or can be reasonably inferred. If the description is unclear or doesn't match any known patterns, return an empty filters object {} and explain why in the explanation field. - -The filters object should only contain the fields that are actually being filtered. Do not include fields with empty arrays or undefined values. + }, + }); -CRITICAL: The response must be a valid JSON object with exactly this structure: -{ - "success": true, - "filters": { - // only include fields that have actual values - }, - "explanation": "string explaining what filters were applied" -} + // Add debugging to see what the AI returned + logger.info("AI filter response", { + text, + environmentId: environment.id, + result: result.experimental_output, + }); -or if you can't figure out the filters then return: -{ - "success": false, - "error": "" -} + if (!result.experimental_output.success) { + return { + success: false, + error: result.experimental_output.error, + }; + } -Make the error no more than 8 words. -`, - prompt: text, - experimental_telemetry: { - isEnabled: true, - metadata: { - environmentId: environment.id, - projectId: environment.projectId, - organizationId: environment.organizationId, - }, - }, - }); + // Validate the filters against the schema to catch any issues + const validationResult = TaskRunListSearchFilters.omit({ environments: true }).safeParse( + result.experimental_output.filters + ); + if (!validationResult.success) { + logger.error("AI filter validation failed", { + errors: validationResult.error.errors, + filters: result.experimental_output.filters, + }); - // Add debugging to see what the AI returned - logger.info("AI filter response", { - text, - environmentId: environment.id, - result: result.experimental_output, - }); + return { + success: false, + error: "AI response validation failed", + }; + } - if (!result.experimental_output.success) { return { - success: false, - error: result.experimental_output.error, + success: true, + filters: validationResult.data, + explanation: result.experimental_output.explanation, }; - } - - // Validate the filters against the schema to catch any issues - const validationResult = TaskRunListSearchFilters.omit({ environments: true }).safeParse( - result.experimental_output.filters - ); - if (!validationResult.success) { - logger.error("AI filter validation failed", { - errors: validationResult.error.errors, - filters: result.experimental_output.filters, + } catch (error) { + logger.error("AI filter processing failed", { + error, + errorMessage: error instanceof Error ? error.message : String(error), + text, + environmentId: environment.id, }); - return { - success: false, - error: "AI response validation failed", - }; - } - - return { - success: true, - filters: validationResult.data, - explanation: result.experimental_output.explanation, - }; - } catch (error) { - logger.error("AI filter processing failed", { - error, - errorMessage: error instanceof Error ? error.message : String(error), - text, - environmentId: environment.id, - }); + // If it's a schema validation error, provide more specific feedback + if (error instanceof Error && error.message.includes("schema")) { + return { + success: false, + error: error.message, + }; + } - // If it's a schema validation error, provide more specific feedback - if (error instanceof Error && error.message.includes("schema")) { return { success: false, - error: error.message, + error: error instanceof Error ? error.message : String(error), }; } - - return { - success: false, - error: error instanceof Error ? error.message : String(error), - }; } } From be9351c4316e1239a3c9bf0ce74c0f651141ae05 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 13:28:11 +0100 Subject: [PATCH 25/34] Added basic evals --- ...jectParam.env.$envParam.runs.ai-filter.tsx | 10 +- .../v3/services/aiRunFilterService.server.ts | 24 +- apps/webapp/evals/aiRunFilter.eval.ts | 99 +++ apps/webapp/package.json | 5 +- pnpm-lock.yaml | 616 ++++++++++++++++-- 5 files changed, 692 insertions(+), 62 deletions(-) create mode 100644 apps/webapp/evals/aiRunFilter.eval.ts diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx index f11c7e1e9d..f947eb69b4 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx @@ -19,6 +19,7 @@ import { VersionListPresenter } from "~/presenters/v3/VersionListPresenter.serve import { TaskListPresenter } from "~/presenters/v3/TaskListPresenter.server"; import { getAllTaskIdentifiers } from "~/models/task.server"; import { $replica } from "~/db.server"; +import { env } from "~/env.server"; const RequestSchema = z.object({ text: z.string().min(1), @@ -130,6 +131,13 @@ export async function action({ request, params }: ActionFunctionArgs) { }, }; + if (!env.OPENAI_API_KEY) { + return { + success: false, + error: "OpenAI API key is not configured", + }; + } + const service = new AIRunFilterService({ queryTags, queryVersions, @@ -137,7 +145,7 @@ export async function action({ request, params }: ActionFunctionArgs) { queryTasks, }); - const [error, result] = await tryCatch(service.call(text, environment)); + const [error, result] = await tryCatch(service.call(text, environment.id)); if (error) { return json({ success: false, error: error.message }, { status: 400 }); } diff --git a/apps/webapp/app/v3/services/aiRunFilterService.server.ts b/apps/webapp/app/v3/services/aiRunFilterService.server.ts index 284858c404..f654e84b86 100644 --- a/apps/webapp/app/v3/services/aiRunFilterService.server.ts +++ b/apps/webapp/app/v3/services/aiRunFilterService.server.ts @@ -3,8 +3,6 @@ import { type TaskTriggerSource } from "@trigger.dev/database"; import { generateText, Output, tool } from "ai"; import { z } from "zod"; import { TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; -import { env } from "~/env.server"; -import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; const AIFilterResponseSchema = z @@ -12,9 +10,6 @@ const AIFilterResponseSchema = z z.object({ success: z.literal(true), filters: TaskRunListSearchFilters.omit({ environments: true }), - explanation: z - .string() - .describe("A short human-readable explanation of what filters were applied"), }), z.object({ success: z.literal(false), @@ -62,7 +57,6 @@ export type AIFilterResult = | { success: true; filters: TaskRunListSearchFilters; - explanation: string; } | { success: false; @@ -79,14 +73,7 @@ export class AIRunFilterService { } ) {} - async call(text: string, environment: AuthenticatedEnvironment): Promise { - if (!env.OPENAI_API_KEY) { - return { - success: false, - error: "OpenAI API key is not configured", - }; - } - + async call(text: string, environmentId: string): Promise { try { const result = await generateText({ model: openai("gpt-4o-mini"), @@ -205,9 +192,7 @@ export class AIRunFilterService { experimental_telemetry: { isEnabled: true, metadata: { - environmentId: environment.id, - projectId: environment.projectId, - organizationId: environment.organizationId, + environmentId, }, }, }); @@ -215,7 +200,7 @@ export class AIRunFilterService { // Add debugging to see what the AI returned logger.info("AI filter response", { text, - environmentId: environment.id, + environmentId, result: result.experimental_output, }); @@ -245,14 +230,13 @@ export class AIRunFilterService { return { success: true, filters: validationResult.data, - explanation: result.experimental_output.explanation, }; } catch (error) { logger.error("AI filter processing failed", { error, errorMessage: error instanceof Error ? error.message : String(error), text, - environmentId: environment.id, + environmentId, }); // If it's a schema validation error, provide more specific feedback diff --git a/apps/webapp/evals/aiRunFilter.eval.ts b/apps/webapp/evals/aiRunFilter.eval.ts new file mode 100644 index 0000000000..d1a4c9b2fd --- /dev/null +++ b/apps/webapp/evals/aiRunFilter.eval.ts @@ -0,0 +1,99 @@ +import { evalite } from "evalite"; +import { Levenshtein } from "autoevals"; +import { + AIRunFilterService, + type QueryQueues, + type QueryTags, + type QueryTasks, + type QueryVersions, +} from "~/v3/services/aiRunFilterService.server"; +import dotenv from "dotenv"; + +dotenv.config({ path: "../../.env" }); + +const queryTags: QueryTags = { + query: async (search) => { + return { + tags: ["user_1", "user_2", "org_1", "org_2"], + }; + }, +}; + +const queryVersions: QueryVersions = { + query: async (versionPrefix, isCurrent) => { + if (isCurrent) { + return { + version: "20250721.1", + }; + } + + return { + versions: ["20250721.1", "20250720.2", "20250720.1"], + }; + }, +}; + +const queryQueues: QueryQueues = { + query: async (query, type) => { + return { + queues: ["shared", "paid"], + }; + }, +}; + +const queryTasks: QueryTasks = { + query: async () => { + return { + tasks: [ + { slug: "task1", triggerSource: "STANDARD" }, + { slug: "task2", triggerSource: "SCHEDULED" }, + ], + }; + }, +}; + +evalite("AI Run Filter", { + data: async () => { + return [ + { + input: "Completed runs", + expected: JSON.stringify({ + success: true, + filters: { + statuses: ["COMPLETED_SUCCESSFULLY"], + }, + }), + }, + { + input: "Failed runs", + expected: JSON.stringify({ + success: true, + filters: { + statuses: ["COMPLETED_WITH_ERRORS", "CRASHED", "TIMED_OUT", "SYSTEM_FAILURE"], + }, + }), + }, + { + input: "Executing runs", + expected: JSON.stringify({ + success: true, + filters: { + statuses: ["EXECUTING", "RETRYING_AFTER_FAILURE", "WAITING_TO_RESUME"], + }, + }), + }, + ]; + }, + task: async (input) => { + const service = new AIRunFilterService({ + queryTags, + queryVersions, + queryQueues, + queryTasks, + }); + + const result = await service.call(input, "123456"); + return JSON.stringify(result); + }, + scorers: [Levenshtein], +}); diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 76b027b584..fc7c864f4a 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -23,7 +23,8 @@ "clean:sourcemaps": "run-s clean:sourcemaps:*", "clean:sourcemaps:public": "rimraf ./build/**/*.map", "clean:sourcemaps:build": "rimraf ./public/build/**/*.map", - "test": "vitest --no-file-parallelism" + "test": "vitest --no-file-parallelism", + "eval:dev": "evalite watch" }, "eslintIgnore": [ "/node_modules", @@ -248,6 +249,7 @@ "@types/ws": "^8.5.3", "@typescript-eslint/eslint-plugin": "^5.59.6", "@typescript-eslint/parser": "^5.59.6", + "autoevals": "^0.0.130", "autoprefixer": "^10.4.13", "css-loader": "^6.10.0", "datepicker": "link:@types/@react-aria/datepicker", @@ -258,6 +260,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-turbo": "^2.0.4", + "evalite": "^0.11.4", "npm-run-all": "^4.1.5", "postcss-import": "^16.0.1", "postcss-loader": "^8.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 685809c848..c8b4b4dc17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -834,6 +834,9 @@ importers: '@typescript-eslint/parser': specifier: ^5.59.6 version: 5.59.6(eslint@8.31.0)(typescript@5.5.4) + autoevals: + specifier: ^0.0.130 + version: 0.0.130(ws@8.12.0) autoprefixer: specifier: ^10.4.13 version: 10.4.13(postcss@8.5.4) @@ -864,6 +867,9 @@ importers: eslint-plugin-turbo: specifier: ^2.0.4 version: 2.0.5(eslint@8.31.0) + evalite: + specifier: ^0.11.4 + version: 0.11.4 npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -2076,7 +2082,7 @@ importers: version: link:../../packages/trigger-sdk openai: specifier: ^4.97.0 - version: 4.97.0(zod@3.23.8) + version: 4.97.0(ws@8.12.0)(zod@3.23.8) replicate: specifier: ^1.0.1 version: 1.0.1 @@ -7937,10 +7943,81 @@ packages: lodash.uniq: 4.5.0 dev: false + /@fastify/accept-negotiator@2.0.1: + resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} + dev: true + + /@fastify/ajv-compiler@4.0.2: + resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==} + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.0.6 + dev: true + /@fastify/busboy@2.0.0: resolution: {integrity: sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==} engines: {node: '>=14'} + /@fastify/error@4.2.0: + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + dev: true + + /@fastify/fast-json-stringify-compiler@5.0.3: + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + dependencies: + fast-json-stringify: 6.0.1 + dev: true + + /@fastify/forwarded@3.0.0: + resolution: {integrity: sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==} + dev: true + + /@fastify/merge-json-schemas@0.2.1: + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + dependencies: + dequal: 2.0.3 + dev: true + + /@fastify/proxy-addr@5.0.0: + resolution: {integrity: sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==} + dependencies: + '@fastify/forwarded': 3.0.0 + ipaddr.js: 2.2.0 + dev: true + + /@fastify/send@4.1.0: + resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==} + dependencies: + '@lukeed/ms': 2.0.2 + escape-html: 1.0.3 + fast-decode-uri-component: 1.0.1 + http-errors: 2.0.0 + mime: 3.0.0 + dev: true + + /@fastify/static@8.2.0: + resolution: {integrity: sha512-PejC/DtT7p1yo3p+W7LiUtLMsV8fEvxAK15sozHy9t8kwo5r0uLYmhV/inURmGz1SkHZFz/8CNtHLPyhKcx4SQ==} + dependencies: + '@fastify/accept-negotiator': 2.0.1 + '@fastify/send': 4.1.0 + content-disposition: 0.5.4 + fastify-plugin: 5.0.1 + fastq: 1.19.1 + glob: 11.0.0 + dev: true + + /@fastify/websocket@11.0.1: + resolution: {integrity: sha512-44yam5+t1I9v09hWBYO+ezV88+mb9Se2BjgERtzB/68+0mGeTfFkjBeDBe2y+ZdiPpeO2rhevhdnfrBm5mqH+Q==} + dependencies: + duplexify: 4.1.3 + fastify-plugin: 5.0.1 + ws: 8.18.0(bufferutil@4.0.9) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + /@fingerprintjs/fingerprintjs-pro-react@2.6.3: resolution: {integrity: sha512-/axCq/cfjZkIM+WFZM/05FQvqtNfdKbIFKU6b2yrwPKlgT8BqWkAq8XvFX6JCPlq8/udVLJjFEDCK+1JQh1L6g==} requiresBuild: true @@ -9050,6 +9127,11 @@ packages: '@lezer/common': 1.2.3 dev: false + /@lukeed/ms@2.0.2: + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + dev: true + /@manypkg/cli@0.19.2: resolution: {integrity: sha512-DXx/P1lyunNoFWwOj1MWBucUhaIJljoiAGOpO2fE0GKMBCI6EZBZD0Up1+fQZoXBecKXRgV9mGgLvIB2fOQ0KQ==} hasBin: true @@ -9546,7 +9628,7 @@ packages: requiresBuild: true dependencies: '@gar/promisify': 1.1.3 - semver: 7.6.3 + semver: 7.7.2 dev: false optional: true @@ -16865,6 +16947,10 @@ packages: resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} dev: true + /@sec-ant/readable-stream@0.4.1: + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + dev: true + /@selderee/plugin-htmlparser2@0.11.0: resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} dependencies: @@ -18232,6 +18318,17 @@ packages: resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} dev: false + /@stricli/auto-complete@1.2.0: + resolution: {integrity: sha512-r9/msiloVmTF95mdhe04Uzqei1B0ZofhYRLeiPqpJ1W1RMCC8p9iW7kqBZEbALl2aRL5ZK9OEW3Q1cIejH7KEQ==} + hasBin: true + dependencies: + '@stricli/core': 1.2.0 + dev: true + + /@stricli/core@1.2.0: + resolution: {integrity: sha512-5b+npntDY0TAB7wAw0daGlh3/R2sf0TDLyrB1By2jCNH+C+lmcSqMtJXOMLVtEGSkIOvqAgIWpLMSs1PXqzt3w==} + dev: true + /@sveltejs/acorn-typescript@1.0.5(acorn@8.14.1): resolution: {integrity: sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==} peerDependencies: @@ -18802,6 +18899,10 @@ packages: redent: 3.0.0 dev: false + /@tokenizer/token@0.3.0: + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + dev: true + /@tootallnate/once@1.1.2: resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} engines: {node: '>= 6'} @@ -19239,7 +19340,6 @@ packages: dependencies: '@types/node': 20.14.14 form-data: 4.0.0 - dev: false /@types/node-fetch@2.6.2: resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} @@ -19290,6 +19390,7 @@ packages: resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==} dependencies: undici-types: 6.20.0 + dev: false /@types/nodemailer@6.4.17: resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==} @@ -19982,12 +20083,25 @@ packages: vite: 5.2.7(@types/node@20.14.14) dev: true + /@vitest/pretty-format@2.1.9: + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + dependencies: + tinyrainbow: 1.2.0 + dev: true + /@vitest/pretty-format@3.1.4: resolution: {integrity: sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==} dependencies: tinyrainbow: 2.0.0 dev: true + /@vitest/runner@2.1.9: + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + dev: true + /@vitest/runner@3.1.4: resolution: {integrity: sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==} dependencies: @@ -20009,6 +20123,14 @@ packages: tinyspy: 3.0.2 dev: true + /@vitest/utils@2.1.9: + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.1.3 + tinyrainbow: 1.2.0 + dev: true + /@vitest/utils@3.1.4: resolution: {integrity: sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==} dependencies: @@ -20382,6 +20504,10 @@ packages: dependencies: event-target-shim: 5.0.1 + /abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + dev: true + /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -20496,7 +20622,6 @@ packages: engines: {node: '>= 8.0.0'} dependencies: humanize-ms: 1.2.1 - dev: false /aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} @@ -20697,6 +20822,17 @@ packages: ajv: 8.17.1 dev: true + /ajv-formats@3.0.1(ajv@8.17.1): + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.17.1 + dev: true + /ajv-keywords@3.5.2(ajv@6.12.6): resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -21113,6 +21249,28 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + /atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + dev: true + + /autoevals@0.0.130(ws@8.12.0): + resolution: {integrity: sha512-JS0T/YCEH13AAOGiWWGJDkIPP8LsDmRBYr3EazTukHxvd0nidOW7fGj0qVPFx2bARrSNO9AfCR6xoTP/5m3Bmw==} + dependencies: + ajv: 8.17.1 + compute-cosine-similarity: 1.1.0 + js-levenshtein: 1.1.6 + js-yaml: 4.1.0 + linear-sum-assignment: 1.0.7 + mustache: 4.2.0 + openai: 4.97.0(ws@8.12.0)(zod@3.23.8) + zod: 3.23.8 + zod-to-json-schema: 3.24.5(zod@3.23.8) + transitivePeerDependencies: + - encoding + - ws + dev: true + /autoprefixer@10.4.13(postcss@8.5.4): resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==} engines: {node: ^10 || ^12 || >=14} @@ -21169,6 +21327,13 @@ packages: possible-typed-array-names: 1.0.0 dev: true + /avvio@9.1.0: + resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==} + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.19.1 + dev: true + /aws-sign2@0.7.0: resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} dev: false @@ -21310,6 +21475,14 @@ packages: is-windows: 1.0.2 dev: false + /better-sqlite3@11.10.0: + resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} + requiresBuild: true + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + dev: true + /big.js@6.2.2: resolution: {integrity: sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==} dev: false @@ -21318,11 +21491,14 @@ packages: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} + /binary-search@1.3.6: + resolution: {integrity: sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==} + dev: true + /bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} dependencies: file-uri-to-path: 1.0.0 - dev: false /bintrees@1.0.2: resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} @@ -21494,7 +21670,7 @@ packages: /builtins@5.0.1: resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} dependencies: - semver: 7.6.3 + semver: 7.7.2 dev: true /bun-types@1.1.17: @@ -21825,6 +22001,10 @@ packages: - encoding dev: true + /cheminfo-types@1.8.1: + resolution: {integrity: sha512-FRcpVkox+cRovffgqNdDFQ1eUav+i/Vq/CUd1hcfEl2bevntFlzznL+jE8g4twl6ElB7gZjCko6pYpXyMn+6dA==} + dev: true + /chevrotain@10.5.0: resolution: {integrity: sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==} dependencies: @@ -22218,6 +22398,29 @@ packages: transitivePeerDependencies: - supports-color + /compute-cosine-similarity@1.1.0: + resolution: {integrity: sha512-FXhNx0ILLjGi9Z9+lglLzM12+0uoTnYkHm7GiadXDAr0HGVLm25OivUS1B/LPkbzzvlcXz/1EvWg9ZYyJSdhTw==} + dependencies: + compute-dot: 1.1.0 + compute-l2norm: 1.1.0 + validate.io-array: 1.0.6 + validate.io-function: 1.0.2 + dev: true + + /compute-dot@1.1.0: + resolution: {integrity: sha512-L5Ocet4DdMrXboss13K59OK23GXjiSia7+7Ukc7q4Bl+RVpIXK2W9IHMbWDZkh+JUEvJAwOKRaJDiFUa1LTnJg==} + dependencies: + validate.io-array: 1.0.6 + validate.io-function: 1.0.2 + dev: true + + /compute-l2norm@1.1.0: + resolution: {integrity: sha512-6EHh1Elj90eU28SXi+h2PLnTQvZmkkHWySpoFz+WOlVNLz3DQoC4ISUHSV9n5jMxPHtKGJ01F4uu2PsXBB8sSg==} + dependencies: + validate.io-array: 1.0.6 + validate.io-function: 1.0.2 + dev: true + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -22304,6 +22507,11 @@ packages: engines: {node: '>= 0.6'} dev: false + /cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + dev: true + /cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} dev: true @@ -22843,7 +23051,6 @@ packages: engines: {node: '>=10'} dependencies: mimic-response: 3.1.0 - dev: false /deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} @@ -23209,6 +23416,15 @@ packages: stream-shift: 1.0.1 dev: true + /duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + dependencies: + end-of-stream: 1.4.4 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + dev: true + /e2b@1.2.1: resolution: {integrity: sha512-ii/Bw55ecxgORqkArKNbuVTwqLgVZ0rH1X3J/NOe4LMZaVETm3qNpPBjoPkpQAsQjw2ew0Ad2sd54epqm9nLCw==} engines: {node: '>=18'} @@ -23239,7 +23455,7 @@ packages: '@one-ini/wasm': 0.1.1 commander: 10.0.1 minimatch: 9.0.1 - semver: 7.6.3 + semver: 7.7.2 dev: false /ee-first@1.1.1: @@ -24582,6 +24798,25 @@ packages: require-like: 0.1.2 dev: true + /evalite@0.11.4: + resolution: {integrity: sha512-t12sJlfkxo0Hon6MYCwOd2qliAjGObrnGL6hYXP9h8AiNAVQCiyGrFrqtOH8TIhM0kgaGrq3s/DeZ679Sr8ipw==} + hasBin: true + dependencies: + '@fastify/static': 8.2.0 + '@fastify/websocket': 11.0.1 + '@stricli/auto-complete': 1.2.0 + '@stricli/core': 1.2.0 + '@vitest/runner': 2.1.9 + better-sqlite3: 11.10.0 + fastify: 5.4.0 + file-type: 19.6.0 + table: 6.9.0 + tinyrainbow: 1.2.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + /event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -24663,7 +24898,6 @@ packages: /expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} - dev: false /expect-type@1.2.1: resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} @@ -24812,7 +25046,6 @@ packages: /fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} - dev: false /fast-deep-equal@2.0.1: resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} @@ -24842,6 +25075,17 @@ packages: /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + /fast-json-stringify@6.0.1: + resolution: {integrity: sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==} + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.0.6 + json-schema-ref-resolver: 2.0.1 + rfdc: 1.4.1 + dev: true + /fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} @@ -24853,7 +25097,11 @@ packages: resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} dependencies: fast-decode-uri-component: 1.0.1 - dev: false + + /fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + dev: true /fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} @@ -24891,11 +25139,41 @@ packages: resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==} dev: false + /fastify-plugin@5.0.1: + resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==} + dev: true + + /fastify@5.4.0: + resolution: {integrity: sha512-I4dVlUe+WNQAhKSyv15w+dwUh2EPiEl4X2lGYMmNSgF83WzTMAPKGdWEv5tPsCQOb+SOZwz8Vlta2vF+OeDgRw==} + dependencies: + '@fastify/ajv-compiler': 4.0.2 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.0.0 + abstract-logging: 2.0.1 + avvio: 9.1.0 + fast-json-stringify: 6.0.1 + find-my-way: 9.3.0 + light-my-request: 6.6.0 + pino: 9.7.0 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.0.0 + semver: 7.7.2 + toad-cache: 3.7.0 + dev: true + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: reusify: 1.0.4 + /fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + dependencies: + reusify: 1.0.4 + dev: true + /fault@2.0.1: resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} dependencies: @@ -24948,6 +25226,10 @@ packages: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} dev: true + /fft.js@4.0.4: + resolution: {integrity: sha512-f9c00hphOgeQTlDyavwTtu6RiK8AIFjD6+jvXkNkpeQ7rirK3uFWVpalkoS4LAwbdX7mfZ8aoBfFVQX1Re/8aw==} + dev: true + /figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -24968,9 +25250,18 @@ packages: tslib: 2.8.1 dev: false + /file-type@19.6.0: + resolution: {integrity: sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==} + engines: {node: '>=18'} + dependencies: + get-stream: 9.0.1 + strtok3: 9.1.1 + token-types: 6.0.3 + uint8array-extras: 1.4.0 + dev: true + /file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - dev: false /fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} @@ -25019,6 +25310,15 @@ packages: resolution: {integrity: sha512-4GOTMrpGQVzsCH2ruUn2vmwzV/02zF4q+ybhCIrw/Rkt3L8KWcycdC6aJMctJzwN4fXD4SD5F/4B9Sksh5rE0A==} dev: false + /find-my-way@9.3.0: + resolution: {integrity: sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==} + engines: {node: '>=20'} + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.0.0 + dev: true + /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -25095,7 +25395,6 @@ packages: /form-data-encoder@1.7.2: resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} - dev: false /form-data@2.3.3: resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} @@ -25142,7 +25441,6 @@ packages: dependencies: node-domexception: 1.0.0 web-streams-polyfill: 4.0.0-beta.3 - dev: false /formidable@3.5.1: resolution: {integrity: sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==} @@ -25425,6 +25723,14 @@ packages: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} + /get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + dev: true + /get-symbol-description@1.0.0: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} engines: {node: '>= 0.4'} @@ -25491,7 +25797,6 @@ packages: /github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - dev: false /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -26068,7 +26373,6 @@ packages: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} dependencies: ms: 2.1.3 - dev: false /hyphenate-style-name@1.0.4: resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==} @@ -26227,6 +26531,11 @@ packages: hasBin: true dev: false + /install@0.13.0: + resolution: {integrity: sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==} + engines: {node: '>= 0.10'} + dev: true + /internal-slot@1.0.4: resolution: {integrity: sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==} engines: {node: '>= 0.4'} @@ -26309,6 +26618,11 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + /ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + dev: true + /is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -26318,6 +26632,10 @@ packages: is-alphabetical: 2.0.1 is-decimal: 2.0.1 + /is-any-array@2.0.1: + resolution: {integrity: sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==} + dev: true + /is-arguments@1.1.1: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} engines: {node: '>= 0.4'} @@ -26577,6 +26895,11 @@ packages: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + /is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + dev: true + /is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} @@ -26755,7 +27078,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 22.13.9 + '@types/node': 20.14.14 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -26823,6 +27146,11 @@ packages: engines: {node: '>=14'} dev: false + /js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + dev: true + /js-sdsl@4.2.0: resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==} @@ -26889,6 +27217,12 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true + /json-schema-ref-resolver@2.0.1: + resolution: {integrity: sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q==} + dependencies: + dequal: 2.0.3 + dev: true + /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -27158,6 +27492,14 @@ packages: prelude-ls: 1.2.1 type-check: 0.4.0 + /light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + dependencies: + cookie: 1.0.2 + process-warning: 4.0.1 + set-cookie-parser: 2.6.0 + dev: true + /lightningcss-darwin-arm64@1.29.2: resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} engines: {node: '>= 12.0.0'} @@ -27274,6 +27616,15 @@ packages: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} + /linear-sum-assignment@1.0.7: + resolution: {integrity: sha512-jfLoSGwZNyjfY8eK4ayhjfcIu3BfWvP6sWieYzYI3AWldwXVoWEz1gtrQL10v/8YltYLBunqNjeVFXPMUs+MJg==} + dependencies: + cheminfo-types: 1.8.1 + install: 0.13.0 + ml-matrix: 6.12.1 + ml-spectra-processing: 14.13.0 + dev: true + /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -27411,6 +27762,10 @@ packages: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} dev: false + /lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + dev: true + /lodash.union@4.6.0: resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} dev: true @@ -27577,7 +27932,7 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} dependencies: - semver: 7.6.3 + semver: 7.7.2 dev: true /make-error@1.3.6: @@ -28495,7 +28850,6 @@ packages: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} hasBin: true - dev: false /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} @@ -28513,7 +28867,6 @@ packages: /mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - dev: false /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} @@ -28741,6 +29094,48 @@ packages: engines: {node: '>=10'} hasBin: true + /ml-array-max@1.2.4: + resolution: {integrity: sha512-BlEeg80jI0tW6WaPyGxf5Sa4sqvcyY6lbSn5Vcv44lp1I2GR6AWojfUvLnGTNsIXrZ8uqWmo8VcG1WpkI2ONMQ==} + dependencies: + is-any-array: 2.0.1 + dev: true + + /ml-array-min@1.2.3: + resolution: {integrity: sha512-VcZ5f3VZ1iihtrGvgfh/q0XlMobG6GQ8FsNyQXD3T+IlstDv85g8kfV0xUG1QPRO/t21aukaJowDzMTc7j5V6Q==} + dependencies: + is-any-array: 2.0.1 + dev: true + + /ml-array-rescale@1.3.7: + resolution: {integrity: sha512-48NGChTouvEo9KBctDfHC3udWnQKNKEWN0ziELvY3KG25GR5cA8K8wNVzracsqSW1QEkAXjTNx+ycgAv06/1mQ==} + dependencies: + is-any-array: 2.0.1 + ml-array-max: 1.2.4 + ml-array-min: 1.2.3 + dev: true + + /ml-matrix@6.12.1: + resolution: {integrity: sha512-TJ+8eOFdp+INvzR4zAuwBQJznDUfktMtOB6g/hUcGh3rcyjxbz4Te57Pgri8Q9bhSQ7Zys4IYOGhFdnlgeB6Lw==} + dependencies: + is-any-array: 2.0.1 + ml-array-rescale: 1.3.7 + dev: true + + /ml-spectra-processing@14.13.0: + resolution: {integrity: sha512-AZPE+XrBoRhSRwzUm0IbiQlAPbetDtndDnoq9VO/SzRkN82wrxJpU+urH4aaFVnxTJhxtGOI81FiAxjFe7xQtQ==} + dependencies: + binary-search: 1.3.6 + cheminfo-types: 1.8.1 + fft.js: 4.0.4 + is-any-array: 2.0.1 + ml-matrix: 6.12.1 + ml-xsadd: 3.0.1 + dev: true + + /ml-xsadd@3.0.1: + resolution: {integrity: sha512-Fz2q6dwgzGM8wYKGArTUTZDGa4lQFA2Vi6orjGeTVRy22ZnQFKlJuwS9n8NRviqz1KHAHAzdKJwbnYhdo38uYg==} + dev: true + /mlly@1.7.1: resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} dependencies: @@ -28838,7 +29233,6 @@ packages: /mustache@4.2.0: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true - dev: false /mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} @@ -28915,7 +29309,6 @@ packages: /napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - dev: false /napi_thread_safe_promise@1.2.6: resolution: {integrity: sha512-Magj7jWzspQiYlqBt4sieiOvWmNJgL/hCWeQaKFpFam82QZJ/etvOfAxlqhPtuUspMABTEgikULHaJA3aDVTNQ==} @@ -29139,8 +29532,7 @@ packages: resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==} engines: {node: '>=10'} dependencies: - semver: 7.6.3 - dev: false + semver: 7.7.2 /node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} @@ -29166,7 +29558,6 @@ packages: /node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} - dev: false /node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} @@ -29248,7 +29639,7 @@ packages: make-fetch-happen: 13.0.1 nopt: 7.2.0 proc-log: 4.2.0 - semver: 7.6.3 + semver: 7.7.2 tar: 6.2.1 which: 4.0.0 transitivePeerDependencies: @@ -29365,7 +29756,7 @@ packages: resolution: {integrity: sha512-744wat5wAAHsxa4590mWO0tJ8PKxR8ORZsH9wGpQc3nWTzozMAgBN/XyqYw7mg3yqLM8dLwEnwSfKMmXAjF69g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dependencies: - semver: 7.6.3 + semver: 7.7.2 dev: true /npm-normalize-package-bin@2.0.0: @@ -29384,7 +29775,7 @@ packages: dependencies: hosted-git-info: 6.1.1 proc-log: 3.0.0 - semver: 7.6.3 + semver: 7.7.2 validate-npm-package-name: 5.0.0 dev: true @@ -29406,7 +29797,7 @@ packages: npm-install-checks: 6.2.0 npm-normalize-package-bin: 3.0.1 npm-package-arg: 10.1.0 - semver: 7.6.3 + semver: 7.7.2 dev: true /npm-run-all@4.1.5: @@ -29610,6 +30001,11 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: false + /on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + dev: true + /on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -29728,7 +30124,7 @@ packages: - encoding dev: false - /openai@4.97.0(zod@3.23.8): + /openai@4.97.0(ws@8.12.0)(zod@3.23.8): resolution: {integrity: sha512-LRoiy0zvEf819ZUEJhgfV8PfsE8G5WpQi4AwA1uCV8SKvvtXQkoWUFkepD6plqyJQRghy2+AEPQ07FrJFKHZ9Q==} hasBin: true peerDependencies: @@ -29747,10 +30143,10 @@ packages: form-data-encoder: 1.7.2 formdata-node: 4.4.1 node-fetch: 2.6.12 + ws: 8.12.0 zod: 3.23.8 transitivePeerDependencies: - encoding - dev: false /openapi-fetch@0.9.8: resolution: {integrity: sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg==} @@ -30195,6 +30591,11 @@ packages: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} dev: false + /peek-readable@5.4.2: + resolution: {integrity: sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==} + engines: {node: '>=14.16'} + dev: true + /peek-stream@1.1.3: resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} dependencies: @@ -30379,6 +30780,33 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} + /pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + dependencies: + split2: 4.2.0 + dev: true + + /pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + dev: true + + /pino@9.7.0: + resolution: {integrity: sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==} + hasBin: true + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + dev: true + /pirates@4.0.5: resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} engines: {node: '>= 6'} @@ -30872,7 +31300,6 @@ packages: simple-get: 4.0.1 tar-fs: 2.1.3 tunnel-agent: 0.6.0 - dev: false /preferred-pm@3.0.3: resolution: {integrity: sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==} @@ -31060,6 +31487,14 @@ packages: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: true + /process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + dev: true + + /process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + dev: true + /process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -31202,7 +31637,6 @@ packages: dependencies: end-of-stream: 1.4.4 once: 1.4.0 - dev: false /pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -31316,6 +31750,10 @@ packages: /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + /quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + dev: true + /quick-lru@4.0.1: resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} engines: {node: '>=8'} @@ -32080,6 +32518,11 @@ packages: engines: {node: '>= 14.18.0'} dev: true + /real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + dev: true + /recharts-scale@0.4.5: resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} dependencies: @@ -32541,6 +32984,11 @@ packages: onetime: 5.1.2 signal-exit: 3.0.7 + /ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + dev: true + /retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} @@ -32557,6 +33005,10 @@ packages: resolution: {integrity: sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ==} dev: false + /rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + dev: true + /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -32737,6 +33189,17 @@ packages: is-regex: 1.1.4 dev: true + /safe-regex2@5.0.0: + resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==} + dependencies: + ret: 0.5.0 + dev: true + + /safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + dev: true + /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} requiresBuild: true @@ -32787,6 +33250,10 @@ packages: /secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + /secure-json-parse@4.0.0: + resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==} + dev: true + /seedrandom@3.0.5: resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} dev: false @@ -32833,7 +33300,6 @@ packages: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} hasBin: true - dev: false /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} @@ -33127,7 +33593,6 @@ packages: /simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - dev: false /simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} @@ -33135,7 +33600,6 @@ packages: decompress-response: 6.0.0 once: 1.4.0 simple-concat: 1.0.1 - dev: false /simple-oauth2@5.0.0: resolution: {integrity: sha512-8291lo/z5ZdpmiOFzOs1kF3cxn22bMj5FFH+DNUppLJrpoIlM1QnFiE7KpshHu3J3i21TVcx4yW+gXYjdCKDLQ==} @@ -33196,6 +33660,15 @@ packages: is-fullwidth-code-point: 3.0.0 dev: true + /slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + dev: true + /slug@6.1.0: resolution: {integrity: sha512-x6vLHCMasg4DR2LPiyFGI0gJJhywY6DTiGhCrOMzb3SOk/0JVLIaL4UhyFSHu04SD3uAavrKY/K3zZ3i6iRcgA==} dev: false @@ -33351,6 +33824,12 @@ packages: smart-buffer: 4.2.0 dev: false + /sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + dependencies: + atomic-sleep: 1.0.0 + dev: true + /sonner@1.0.3(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-hBoA2zKuYW3lUnpx4K0vAn8j77YuYiwvP9sLQfieNS2pd5FkT20sMyPTDJnl9S+5T27ZJbwQRPiujwvDBwhZQg==} peerDependencies: @@ -33448,7 +33927,6 @@ packages: /split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - dev: false /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -33623,6 +34101,10 @@ packages: resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==} dev: true + /stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + dev: true + /stream-slice@0.1.2: resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==} @@ -33801,6 +34283,14 @@ packages: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} dev: false + /strtok3@9.1.1: + resolution: {integrity: sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==} + engines: {node: '>=16'} + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 5.4.2 + dev: true + /style-loader@3.3.4(webpack@5.99.9): resolution: {integrity: sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==} engines: {node: '>= 12.13.0'} @@ -34081,6 +34571,17 @@ packages: tslib: 2.8.1 dev: true + /table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + /tailwind-merge@1.12.0: resolution: {integrity: sha512-Y17eDp7FtN1+JJ4OY0Bqv9OA41O+MS8c1Iyr3T6JFLnOgLg3EvcyMKZAnQ8AGyvB5Nxm3t9Xb5Mhe139m8QT/g==} dev: false @@ -34472,6 +34973,12 @@ packages: dependencies: any-promise: 1.3.0 + /thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + dependencies: + real-require: 0.2.0 + dev: true + /throttle-debounce@3.0.1: resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} engines: {node: '>=10'} @@ -34569,6 +35076,11 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} dev: true + /tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + dev: true + /tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -34629,6 +35141,11 @@ packages: dependencies: is-number: 7.0.0 + /toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + dev: true + /toggle-selection@1.0.6: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} dev: false @@ -34637,6 +35154,14 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + /token-types@6.0.3: + resolution: {integrity: sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ==} + engines: {node: '>=14.16'} + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + dev: true + /toml@3.0.0: resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} dev: true @@ -35018,7 +35543,6 @@ packages: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} dependencies: safe-buffer: 5.2.1 - dev: false /turbo-darwin-64@1.10.3: resolution: {integrity: sha512-IIB9IomJGyD3EdpSscm7Ip1xVWtYb7D0x7oH3vad3gjFcjHJzDz9xZ/iw/qItFEW+wGFcLSRPd+1BNnuLM8AsA==} @@ -35326,6 +35850,11 @@ packages: engines: {node: '>= 4.0.0'} dev: false + /uint8array-extras@1.4.0: + resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==} + engines: {node: '>=18'} + dev: true + /ulid@2.3.0: resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} hasBin: true @@ -35362,6 +35891,7 @@ packages: /undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + dev: false /undici@5.28.4: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} @@ -35866,6 +36396,14 @@ packages: builtins: 5.0.1 dev: true + /validate.io-array@1.0.6: + resolution: {integrity: sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==} + dev: true + + /validate.io-function@1.0.2: + resolution: {integrity: sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==} + dev: true + /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -36227,7 +36765,6 @@ packages: /web-streams-polyfill@4.0.0-beta.3: resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} engines: {node: '>= 14'} - dev: false /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -36573,7 +37110,6 @@ packages: optional: true utf-8-validate: optional: true - dev: false /ws@8.16.0: resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} From 80562a9da7e05a7c11df326a81961102b627975c Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 13:51:05 +0100 Subject: [PATCH 26/34] Better time inputs and evals --- .../v3/services/aiRunFilterService.server.ts | 34 ++-- apps/webapp/evals/aiRunFilter.eval.ts | 151 +++++++++++++++++- apps/webapp/package.json | 2 +- 3 files changed, 171 insertions(+), 16 deletions(-) diff --git a/apps/webapp/app/v3/services/aiRunFilterService.server.ts b/apps/webapp/app/v3/services/aiRunFilterService.server.ts index f654e84b86..87dece993f 100644 --- a/apps/webapp/app/v3/services/aiRunFilterService.server.ts +++ b/apps/webapp/app/v3/services/aiRunFilterService.server.ts @@ -5,11 +5,20 @@ import { z } from "zod"; import { TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; import { logger } from "~/services/logger.server"; +const AIFilters = TaskRunListSearchFilters.omit({ + environments: true, + from: true, + to: true, +}).extend({ + from: z.string().optional().describe("The ISO datetime to filter from"), + to: z.string().optional().describe("The ISO datetime to filter to"), +}); + const AIFilterResponseSchema = z .discriminatedUnion("success", [ z.object({ success: z.literal(true), - filters: TaskRunListSearchFilters.omit({ environments: true }), + filters: AIFilters, }), z.object({ success: z.literal(false), @@ -137,7 +146,7 @@ export class AIRunFilterService { Available filter options: - statuses: Array of run statuses (PENDING, EXECUTING, COMPLETED_SUCCESSFULLY, COMPLETED_WITH_ERRORS, CANCELED, TIMED_OUT, CRASHED, etc.) - period: Time period string (e.g., "1h", "7d", "30d", "1y") - - from/to: Unix ms timestamps for specific time ranges. You'll need to use a converter if they give you a date. Today's date is ${new Date().toISOString()}, if they only specify a day use the current month. If they don't specify a year use the current year. If they don't specify a time of day use midnight to midnight. + - from/to: ISO date string. Today's date is ${new Date().toISOString()}, if they only specify a day use the current month. If they don't specify a year use the current year. If they don't specify a time of day use midnight. - tags: Array of tag names to filter by. Use the lookupTags tool to get the tags. - tasks: Array of task identifiers to filter by. Use the lookupTasks tool to get the tasks. - machines: Array of machine presets (micro, small, small-2x, medium, large, xlarge, etc.) @@ -148,6 +157,7 @@ export class AIRunFilterService { - batchId: Specific batch ID to filter by - scheduleId: Specific schedule ID to filter by + Common patterns to recognize: - "failed runs" → statuses: ["COMPLETED_WITH_ERRORS", "CRASHED", "TIMED_OUT", "SYSTEM_FAILURE"]. - "runs not dequeued yet" → statuses: ["PENDING", "PENDING_VERSION", "DELAYED"] @@ -158,6 +168,7 @@ export class AIRunFilterService { - "past 7 days" → period: "7d" - "last hour" → period: "1h" - "this month" → period: "30d" + - "June 16" -> return a from/to filter. - "with tag X" → tags: ["X"] - "from task Y" → tasks: ["Y"] - "using large machine" → machines: ["large-1x", "large-2x"] @@ -197,13 +208,6 @@ export class AIRunFilterService { }, }); - // Add debugging to see what the AI returned - logger.info("AI filter response", { - text, - environmentId, - result: result.experimental_output, - }); - if (!result.experimental_output.success) { return { success: false, @@ -212,9 +216,7 @@ export class AIRunFilterService { } // Validate the filters against the schema to catch any issues - const validationResult = TaskRunListSearchFilters.omit({ environments: true }).safeParse( - result.experimental_output.filters - ); + const validationResult = AIFilters.safeParse(result.experimental_output.filters); if (!validationResult.success) { logger.error("AI filter validation failed", { errors: validationResult.error.errors, @@ -229,7 +231,13 @@ export class AIRunFilterService { return { success: true, - filters: validationResult.data, + filters: { + ...validationResult.data, + from: validationResult.data.from + ? new Date(validationResult.data.from).getTime() + : undefined, + to: validationResult.data.to ? new Date(validationResult.data.to).getTime() : undefined, + }, }; } catch (error) { logger.error("AI filter processing failed", { diff --git a/apps/webapp/evals/aiRunFilter.eval.ts b/apps/webapp/evals/aiRunFilter.eval.ts index d1a4c9b2fd..a0c16010a9 100644 --- a/apps/webapp/evals/aiRunFilter.eval.ts +++ b/apps/webapp/evals/aiRunFilter.eval.ts @@ -45,8 +45,8 @@ const queryTasks: QueryTasks = { query: async () => { return { tasks: [ - { slug: "task1", triggerSource: "STANDARD" }, - { slug: "task2", triggerSource: "SCHEDULED" }, + { slug: "email-sender", triggerSource: "STANDARD" }, + { slug: "email-sender-scheduled", triggerSource: "SCHEDULED" }, ], }; }, @@ -55,6 +55,7 @@ const queryTasks: QueryTasks = { evalite("AI Run Filter", { data: async () => { return [ + // Basic status filtering { input: "Completed runs", expected: JSON.stringify({ @@ -82,6 +83,152 @@ evalite("AI Run Filter", { }, }), }, + // Time filters + { + input: "Runs from the past 7 days", + expected: JSON.stringify({ + success: true, + filters: { + period: "7d", + }, + }), + }, + { + input: "Runs from the last hour", + expected: JSON.stringify({ + success: true, + filters: { + period: "1h", + }, + }), + }, + { + input: "Runs from this month", + expected: JSON.stringify({ + success: true, + filters: { + period: "30d", + }, + }), + }, + { + input: "June 16", + expected: JSON.stringify({ + success: true, + filters: { + from: new Date("2025-06-16").getTime(), + to: new Date("2025-06-17").getTime(), + }, + }), + }, + // Combined filters + { + input: "Failed runs from the past week", + expected: JSON.stringify({ + success: true, + filters: { + statuses: ["COMPLETED_WITH_ERRORS", "CRASHED", "TIMED_OUT", "SYSTEM_FAILURE"], + period: "7d", + }, + }), + }, + { + input: "Successful runs from the last 24 hours", + expected: JSON.stringify({ + success: true, + filters: { + statuses: ["COMPLETED_SUCCESSFULLY"], + period: "1d", + }, + }), + }, + // Root-only filtering + { + input: "Root runs only", + expected: JSON.stringify({ + success: true, + filters: { + rootOnly: true, + }, + }), + }, + { + input: "Failed root runs from yesterday", + expected: JSON.stringify({ + success: true, + filters: { + statuses: ["COMPLETED_WITH_ERRORS", "CRASHED", "TIMED_OUT", "SYSTEM_FAILURE"], + rootOnly: true, + period: "1d", + }, + }), + }, + // Machine filtering + { + input: "Runs using large machines", + expected: JSON.stringify({ + success: true, + filters: { + machines: ["large-1x", "large-2x"], + }, + }), + }, + // Edge cases and error handling + { + input: "Runs with tag production", + expected: JSON.stringify({ + success: true, + filters: { + tags: ["production"], + }, + }), + }, + { + input: "Runs from task email-sender", + expected: JSON.stringify({ + success: true, + filters: { + tasks: ["email-sender"], + }, + }), + }, + { + input: "Runs in the shared queue", + expected: JSON.stringify({ + success: true, + filters: { + queues: ["shared"], + }, + }), + }, + // Complex combinations + { + input: "Failed production runs from the past 3 days using large machines", + expected: JSON.stringify({ + success: true, + filters: { + statuses: ["COMPLETED_WITH_ERRORS", "CRASHED", "TIMED_OUT", "SYSTEM_FAILURE"], + tags: ["production"], + period: "3d", + machines: ["large-1x", "large-2x"], + }, + }), + }, + // Ambiguous cases that should return errors + { + input: "Show me something", + expected: JSON.stringify({ + success: false, + error: "Unclear what to filter", + }), + }, + { + input: "Runs with unknown status", + expected: JSON.stringify({ + success: false, + error: "Unknown status specified", + }), + }, ]; }, task: async (input) => { diff --git a/apps/webapp/package.json b/apps/webapp/package.json index fc7c864f4a..c0ed2e0c58 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -279,4 +279,4 @@ "engines": { "node": ">=16.0.0" } -} \ No newline at end of file +} From de42243504271d9c795e99834daba8514449c368 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 17:00:18 +0100 Subject: [PATCH 27/34] Removed some code comments --- .../app/components/runs/v3/AIFilterInput.tsx | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx index b0ba3b9698..ef8384894d 100644 --- a/apps/webapp/app/components/runs/v3/AIFilterInput.tsx +++ b/apps/webapp/app/components/runs/v3/AIFilterInput.tsx @@ -1,19 +1,17 @@ -import { Portal } from "@radix-ui/react-portal"; import { useFetcher, useNavigate } from "@remix-run/react"; +import { AnimatePresence, motion } from "framer-motion"; import { useEffect, useRef, useState } from "react"; import { AISparkleIcon } from "~/assets/icons/AISparkleIcon"; import { Input } from "~/components/primitives/Input"; +import { Popover, PopoverContent, PopoverTrigger } from "~/components/primitives/Popover"; import { ShortcutKey } from "~/components/primitives/ShortcutKey"; import { Spinner } from "~/components/primitives/Spinner"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; -import { useSearchParams } from "~/hooks/useSearchParam"; +import { cn } from "~/utils/cn"; import { objectToSearchParams } from "~/utils/searchParams"; import { type TaskRunListSearchFilters } from "./RunFilters"; -import { cn } from "~/utils/cn"; -import { motion, AnimatePresence } from "framer-motion"; -import { Popover, PopoverContent, PopoverTrigger } from "~/components/primitives/Popover"; type AIFilterResult = | { @@ -39,9 +37,7 @@ export function AIFilterInput() { useEffect(() => { if (fetcher.data?.success && fetcher.state === "loading") { - // Clear the input after successful application setText(""); - // Ensure focus is removed after successful submission setIsFocused(false); const searchParams = objectToSearchParams(fetcher.data.filters); @@ -49,23 +45,11 @@ export function AIFilterInput() { return; } - console.log("AI filter success", { - data: fetcher.data, - searchParams: searchParams.toString(), - }); - navigate(`${location.pathname}?${searchParams.toString()}`, { replace: true }); - //focus the input again if (inputRef.current) { inputRef.current.focus(); } - - // TODO: Show success message with explanation - console.log(`AI applied filters: ${fetcher.data.explanation}`); - } else if (fetcher.data?.success === false) { - // TODO: Show error with suggestions - console.error(fetcher.data.error, fetcher.data.suggestions); } }, [fetcher.data, navigate]); @@ -126,7 +110,6 @@ export function AIFilterInput() { }} onFocus={() => setIsFocused(true)} onBlur={() => { - // Only blur if the text is empty or we're not loading if (text.length === 0 || !isLoading) { setIsFocused(false); } From 0b578670d4a1695d3180143dcfc3d38777e92161 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 17:09:43 +0100 Subject: [PATCH 28/34] Remove unused useSearchParam change --- apps/webapp/app/hooks/useSearchParam.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/hooks/useSearchParam.ts b/apps/webapp/app/hooks/useSearchParam.ts index 821a726cdc..c619312428 100644 --- a/apps/webapp/app/hooks/useSearchParam.ts +++ b/apps/webapp/app/hooks/useSearchParam.ts @@ -9,7 +9,7 @@ export function useSearchParams() { const location = useOptimisticLocation(); const replace = useCallback( - (values: Values | URLSearchParams) => { + (values: Values) => { const s = set(new URLSearchParams(location.search), values); navigate(`${location.pathname}?${s.toString()}`, { replace: true }); }, @@ -69,7 +69,7 @@ export function useSearchParams() { }; } -function set(searchParams: URLSearchParams, values: Values | URLSearchParams) { +function set(searchParams: URLSearchParams, values: Values) { const search = new URLSearchParams(searchParams); for (const [param, value] of Object.entries(values)) { if (value === undefined) { From 56b075d0e46823036d782b4605cc1e91013568fc Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 17:14:44 +0100 Subject: [PATCH 29/34] Tidy imports --- apps/webapp/app/presenters/v3/QueueListPresenter.server.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/webapp/app/presenters/v3/QueueListPresenter.server.ts b/apps/webapp/app/presenters/v3/QueueListPresenter.server.ts index 1d9b1e0255..ec60db8b92 100644 --- a/apps/webapp/app/presenters/v3/QueueListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/QueueListPresenter.server.ts @@ -1,8 +1,4 @@ -import { - TaskQueueType, - type RunEngineVersion, - type RuntimeEnvironmentType, -} from "@trigger.dev/database"; +import { TaskQueueType } from "@trigger.dev/database"; import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { determineEngineVersion } from "~/v3/engineVersion.server"; import { engine } from "~/v3/runEngine.server"; From 619221314b363efd9bf92127fb3d94ef65f1c6ad Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 17:16:32 +0100 Subject: [PATCH 30/34] If no OpenAI API key send json back --- ...jectParam.env.$envParam.runs.ai-filter.tsx | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx index f947eb69b4..4b0c9cf837 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx @@ -1,25 +1,23 @@ import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; +import { tryCatch } from "@trigger.dev/core"; import { z } from "zod"; -import { requireUserId } from "~/services/session.server"; -import { EnvironmentParamSchema } from "~/utils/pathBuilder"; +import { $replica } from "~/db.server"; +import { env } from "~/env.server"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; -import { type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; -import { tryCatch } from "@trigger.dev/core"; +import { getAllTaskIdentifiers } from "~/models/task.server"; +import { QueueListPresenter } from "~/presenters/v3/QueueListPresenter.server"; +import { RunTagListPresenter } from "~/presenters/v3/RunTagListPresenter.server"; +import { VersionListPresenter } from "~/presenters/v3/VersionListPresenter.server"; +import { requireUserId } from "~/services/session.server"; +import { EnvironmentParamSchema } from "~/utils/pathBuilder"; import { AIRunFilterService, - QueryQueues, - QueryTags, - QueryTasks, - QueryVersions, + type QueryQueues, + type QueryTags, + type QueryTasks, + type QueryVersions, } from "~/v3/services/aiRunFilterService.server"; -import { RunTagListPresenter } from "~/presenters/v3/RunTagListPresenter.server"; -import { QueueListPresenter } from "~/presenters/v3/QueueListPresenter.server"; -import { VersionListPresenter } from "~/presenters/v3/VersionListPresenter.server"; -import { TaskListPresenter } from "~/presenters/v3/TaskListPresenter.server"; -import { getAllTaskIdentifiers } from "~/models/task.server"; -import { $replica } from "~/db.server"; -import { env } from "~/env.server"; const RequestSchema = z.object({ text: z.string().min(1), @@ -132,10 +130,13 @@ export async function action({ request, params }: ActionFunctionArgs) { }; if (!env.OPENAI_API_KEY) { - return { - success: false, - error: "OpenAI API key is not configured", - }; + return json( + { + success: false, + error: "OpenAI API key is not configured", + }, + { status: 400 } + ); } const service = new AIRunFilterService({ From 00394527a6ed5dd8486393a4c6aa87337617b20e Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 17:18:48 +0100 Subject: [PATCH 31/34] Fix for merge conflict with duplicate query filters --- apps/webapp/app/services/runsRepository.server.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/apps/webapp/app/services/runsRepository.server.ts b/apps/webapp/app/services/runsRepository.server.ts index bcd5dd582b..3196c436b3 100644 --- a/apps/webapp/app/services/runsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository.server.ts @@ -372,16 +372,6 @@ function applyRunFiltersToQueryBuilder( machines: options.machines, }); } - - if (options.queues && options.queues.length > 0) { - queryBuilder.where("queue IN {queues: Array(String)}", { queues: options.queues }); - } - - if (options.machines && options.machines.length > 0) { - queryBuilder.where("machine_preset IN {machines: Array(String)}", { - machines: options.machines, - }); - } } export function parseRunListInputOptions(data: any): RunListInputOptions { From beaadb97bc4db28237218339e1a6bbf75f74c038 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 17:20:20 +0100 Subject: [PATCH 32/34] Another conflict resolved --- apps/webapp/app/utils/cn.ts | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/apps/webapp/app/utils/cn.ts b/apps/webapp/app/utils/cn.ts index ef578364ce..842542049d 100644 --- a/apps/webapp/app/utils/cn.ts +++ b/apps/webapp/app/utils/cn.ts @@ -64,41 +64,6 @@ const customTwMerge = extendTailwindMerge({ "96", "auto", "px", - "0.5", - "1", - "1.5", - "2", - "2.5", - "3", - "3.5", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", - "14", - "16", - "20", - "24", - "28", - "32", - "36", - "40", - "44", - "48", - "52", - "56", - "60", - "64", - "72", - "80", - "96", - "auto", - "px", "full", "min", "max", From b467eba11624fe14b4e61c2c33121ac7bee29aa9 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 17:25:16 +0100 Subject: [PATCH 33/34] Another merge conflict resolved --- packages/core/src/v3/apiClient/index.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/core/src/v3/apiClient/index.ts b/packages/core/src/v3/apiClient/index.ts index cebe9a6e34..4eab7d0089 100644 --- a/packages/core/src/v3/apiClient/index.ts +++ b/packages/core/src/v3/apiClient/index.ts @@ -1158,15 +1158,6 @@ function createSearchQueryForListRuns(query?: ListRunsQueryParams): URLSearchPar ); } - if (query.queue) { - searchParams.append( - "filter[queue]", - Array.isArray(query.queue) - ? query.queue.map((q) => queueNameFromQueueTypeName(q)).join(",") - : queueNameFromQueueTypeName(query.queue) - ); - } - if (query.machine) { searchParams.append( "filter[machine]", From 178aa909ab5ee48a53bcafe8fbd94ebb8434c8a5 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 17:43:38 +0100 Subject: [PATCH 34/34] Pass the model in, allow changing it --- apps/webapp/app/env.server.ts | 3 +++ ...rojectParam.env.$envParam.runs.ai-filter.tsx | 16 ++++++++++------ .../v3/services/aiRunFilterService.server.ts | 7 ++++--- apps/webapp/evals/aiRunFilter.eval.ts | 17 +++++++++++------ 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 104b280c66..6fd895c456 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -975,6 +975,9 @@ const EnvironmentSchema = z.object({ BULK_ACTION_BATCH_SIZE: z.coerce.number().int().default(100), BULK_ACTION_BATCH_DELAY_MS: z.coerce.number().int().default(200), BULK_ACTION_SUBBATCH_CONCURRENCY: z.coerce.number().int().default(5), + + // AI Run Filter + AI_RUN_FILTER_MODEL: z.string().optional(), }); export type Environment = z.infer; diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx index 4b0c9cf837..9249d7afe9 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.ai-filter.tsx @@ -1,3 +1,4 @@ +import { openai } from "@ai-sdk/openai"; import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core"; import { z } from "zod"; @@ -139,12 +140,15 @@ export async function action({ request, params }: ActionFunctionArgs) { ); } - const service = new AIRunFilterService({ - queryTags, - queryVersions, - queryQueues, - queryTasks, - }); + const service = new AIRunFilterService( + { + queryTags, + queryVersions, + queryQueues, + queryTasks, + }, + openai(env.AI_RUN_FILTER_MODEL ?? "gpt-4o-mini") + ); const [error, result] = await tryCatch(service.call(text, environment.id)); if (error) { diff --git a/apps/webapp/app/v3/services/aiRunFilterService.server.ts b/apps/webapp/app/v3/services/aiRunFilterService.server.ts index 87dece993f..04a4c227c2 100644 --- a/apps/webapp/app/v3/services/aiRunFilterService.server.ts +++ b/apps/webapp/app/v3/services/aiRunFilterService.server.ts @@ -1,6 +1,6 @@ import { openai } from "@ai-sdk/openai"; import { type TaskTriggerSource } from "@trigger.dev/database"; -import { generateText, Output, tool } from "ai"; +import { generateText, LanguageModelV1, Output, tool } from "ai"; import { z } from "zod"; import { TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; import { logger } from "~/services/logger.server"; @@ -79,13 +79,14 @@ export class AIRunFilterService { queryVersions: QueryVersions; queryQueues: QueryQueues; queryTasks: QueryTasks; - } + }, + private readonly model: LanguageModelV1 = openai("gpt-4o-mini") ) {} async call(text: string, environmentId: string): Promise { try { const result = await generateText({ - model: openai("gpt-4o-mini"), + model: this.model, experimental_output: Output.object({ schema: AIFilterResponseSchema }), tools: { lookupTags: tool({ diff --git a/apps/webapp/evals/aiRunFilter.eval.ts b/apps/webapp/evals/aiRunFilter.eval.ts index a0c16010a9..68f3ac2068 100644 --- a/apps/webapp/evals/aiRunFilter.eval.ts +++ b/apps/webapp/evals/aiRunFilter.eval.ts @@ -8,6 +8,8 @@ import { type QueryVersions, } from "~/v3/services/aiRunFilterService.server"; import dotenv from "dotenv"; +import { traceAISDKModel } from "evalite/ai-sdk"; +import { openai } from "@ai-sdk/openai"; dotenv.config({ path: "../../.env" }); @@ -232,12 +234,15 @@ evalite("AI Run Filter", { ]; }, task: async (input) => { - const service = new AIRunFilterService({ - queryTags, - queryVersions, - queryQueues, - queryTasks, - }); + const service = new AIRunFilterService( + { + queryTags, + queryVersions, + queryQueues, + queryTasks, + }, + traceAISDKModel(openai("gpt-4o-mini")) + ); const result = await service.call(input, "123456"); return JSON.stringify(result);