mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-12 06:55:19 +00:00
feat: add AI log analysis component and integrate into deployment views
- Introduced the AnalyzeLogs component for analyzing logs using AI, allowing users to select AI providers and view analysis results. - Integrated AnalyzeLogs into the ShowDeployment and DockerLogsId components, enabling log analysis for both build and runtime contexts. - Updated the AI router to include a new endpoint for log analysis, which processes logs and returns structured insights. - Enhanced the AI provider selection logic to support new providers, including Z.AI and MiniMax. This feature enhances the user experience by providing actionable insights from logs, improving troubleshooting and operational efficiency.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import copy from "copy-to-clipboard";
|
||||
import { Check, Copy, Loader2 } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AnalyzeLogs } from "@/components/dashboard/docker/logs/analyze-logs";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@@ -165,6 +166,7 @@ export const ShowDeployment = ({
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
<AnalyzeLogs logs={filteredLogs} context="build" />
|
||||
|
||||
{serverId && (
|
||||
<div className="flex items-center space-x-2">
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
"use client";
|
||||
import { Bot, Loader2, RotateCcw, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import type { LogLine } from "./utils";
|
||||
|
||||
interface Props {
|
||||
logs: LogLine[];
|
||||
context: "build" | "runtime";
|
||||
}
|
||||
|
||||
const MAX_LOG_LINES = 200;
|
||||
|
||||
export function AnalyzeLogs({ logs, context }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [aiId, setAiId] = useState<string>("");
|
||||
const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, {
|
||||
enabled: open,
|
||||
});
|
||||
const { mutate, isPending, data, reset } =
|
||||
api.ai.analyzeLogs.useMutation({
|
||||
onError: (error) => {
|
||||
toast.error("Analysis failed", {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleAnalyze = () => {
|
||||
if (!aiId || logs.length === 0) return;
|
||||
|
||||
const logsText = logs
|
||||
.slice(-MAX_LOG_LINES)
|
||||
.map((l) => l.message)
|
||||
.join("\n");
|
||||
|
||||
mutate({ aiId, logs: logsText, context });
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
setOpen(isOpen);
|
||||
if (!isOpen) {
|
||||
reset();
|
||||
setAiId("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9"
|
||||
disabled={logs.length === 0}
|
||||
title="Analyze logs with AI"
|
||||
>
|
||||
<Bot className="mr-2 h-4 w-4" />
|
||||
AI
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[550px] p-0" align="end">
|
||||
<div className="flex items-center justify-between border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bot className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">Log Analysis</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
{!data?.analysis ? (
|
||||
<>
|
||||
<Select value={aiId} onValueChange={setAiId}>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="Select AI provider..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{providers?.map((p) => (
|
||||
<SelectItem key={p.aiId} value={p.aiId}>
|
||||
{p.name} ({p.model})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full"
|
||||
disabled={!aiId || isPending || logs.length === 0}
|
||||
onClick={handleAnalyze}
|
||||
>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />
|
||||
Analyzing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Bot className="mr-2 h-3.5 w-3.5" />
|
||||
Analyze {logs.length > MAX_LOG_LINES ? `last ${MAX_LOG_LINES}` : logs.length} lines
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="max-h-[400px] overflow-y-auto">
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none text-sm break-words">
|
||||
<ReactMarkdown>{data.analysis}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
reset();
|
||||
handleAnalyze();
|
||||
}}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<RotateCcw className="mr-2 h-3.5 w-3.5" />
|
||||
)}
|
||||
Re-analyze
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
reset();
|
||||
setAiId("");
|
||||
}}
|
||||
title="Change provider"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { AnalyzeLogs } from "./analyze-logs";
|
||||
import { LineCountFilter } from "./line-count-filter";
|
||||
import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter";
|
||||
import { StatusLogsFilter } from "./status-logs-filter";
|
||||
@@ -377,6 +378,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
<DownloadIcon className="mr-2 h-4 w-4" />
|
||||
Download logs
|
||||
</Button>
|
||||
<AnalyzeLogs logs={filteredLogs} context="runtime" />
|
||||
</div>
|
||||
</div>
|
||||
{isPaused && (
|
||||
|
||||
@@ -298,7 +298,19 @@ export const TemplateGenerator = ({ environmentId }: Props) => {
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2 w-full justify-end">
|
||||
<Button
|
||||
onClick={stepper.prev}
|
||||
onClick={() => {
|
||||
if (
|
||||
stepper.current.id === "variant" &&
|
||||
templateInfo.details
|
||||
) {
|
||||
setTemplateInfo((prev) => ({
|
||||
...prev,
|
||||
details: null,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
stepper.prev();
|
||||
}}
|
||||
disabled={stepper.isFirst}
|
||||
variant="secondary"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
"use client";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { Check, ChevronDown, PenBoxIcon, PlusIcon } from "lucide-react";
|
||||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
Loader2,
|
||||
PenBoxIcon,
|
||||
Plug,
|
||||
PlusIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -37,10 +44,34 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const AI_PROVIDERS = [
|
||||
{ name: "OpenAI", apiUrl: "https://api.openai.com/v1" },
|
||||
{ name: "Anthropic", apiUrl: "https://api.anthropic.com/v1" },
|
||||
{
|
||||
name: "Google Gemini",
|
||||
apiUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||
},
|
||||
{ name: "Mistral", apiUrl: "https://api.mistral.ai/v1" },
|
||||
{ name: "Cohere", apiUrl: "https://api.cohere.ai/v2" },
|
||||
{ name: "Perplexity", apiUrl: "https://api.perplexity.ai" },
|
||||
{ name: "DeepInfra", apiUrl: "https://api.deepinfra.com/v1/openai" },
|
||||
{ name: "Ollama", apiUrl: "http://localhost:11434" },
|
||||
{ name: "OpenRouter", apiUrl: "https://openrouter.ai/api/v1" },
|
||||
{ name: "Z.AI", apiUrl: "https://api.z.ai/api/paas/v4/" },
|
||||
{ name: "MiniMax", apiUrl: "https://api.minimax.io/v1" },
|
||||
] as const;
|
||||
|
||||
const Schema = z.object({
|
||||
name: z.string().min(1, { message: "Name is required" }),
|
||||
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
|
||||
@@ -103,7 +134,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
const isOllama = apiUrl.includes(":11434") || apiUrl.includes("ollama");
|
||||
const {
|
||||
data: models,
|
||||
isPending: isLoadingServerModels,
|
||||
isFetching: isLoadingServerModels,
|
||||
error: modelsError,
|
||||
} = api.ai.getModels.useQuery(
|
||||
{
|
||||
@@ -172,6 +203,34 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
<AlertBlock type="error">{modelsError.message}</AlertBlock>
|
||||
)}
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<FormLabel>Provider</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
const provider = AI_PROVIDERS.find((p) => p.apiUrl === value);
|
||||
if (provider) {
|
||||
form.setValue("name", provider.name);
|
||||
form.setValue("apiUrl", provider.apiUrl);
|
||||
form.setValue("model", "");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a provider preset..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{AI_PROVIDERS.map((provider) => (
|
||||
<SelectItem key={provider.apiUrl} value={provider.apiUrl}>
|
||||
{provider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
Quick-fill provider name and URL, or configure manually below
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -253,101 +312,129 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!isLoadingServerModels && !models?.length && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
No models available
|
||||
</span>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="model"
|
||||
render={({ field }) => {
|
||||
const hasModels =
|
||||
!isLoadingServerModels && models && models.length > 0;
|
||||
const selectedModel = models?.find((m) => m.id === field.value);
|
||||
const filteredModels = (models ?? []).filter((model) =>
|
||||
model.id.toLowerCase().includes(modelSearch.toLowerCase()),
|
||||
);
|
||||
|
||||
{!isLoadingServerModels && models && models.length > 0 && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="model"
|
||||
render={({ field }) => {
|
||||
const selectedModel = models.find(
|
||||
(m) => m.id === field.value,
|
||||
);
|
||||
const filteredModels = models.filter((model) =>
|
||||
model.id.toLowerCase().includes(modelSearch.toLowerCase()),
|
||||
);
|
||||
const displayModels =
|
||||
field.value &&
|
||||
!filteredModels.find((m) => m.id === field.value) &&
|
||||
selectedModel
|
||||
? [selectedModel, ...filteredModels]
|
||||
: filteredModels;
|
||||
|
||||
// Ensure selected model is always in the filtered list
|
||||
const displayModels =
|
||||
field.value &&
|
||||
!filteredModels.find((m) => m.id === field.value) &&
|
||||
selectedModel
|
||||
? [selectedModel, ...filteredModels]
|
||||
: filteredModels;
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Model</FormLabel>
|
||||
<Popover
|
||||
open={modelPopoverOpen}
|
||||
onOpenChange={setModelPopoverOpen}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Model</FormLabel>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
{hasModels ? (
|
||||
<Popover
|
||||
open={modelPopoverOpen}
|
||||
onOpenChange={setModelPopoverOpen}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{field.value
|
||||
? (selectedModel?.id ?? field.value)
|
||||
: "Select a model"}
|
||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[400px] p-0"
|
||||
align="start"
|
||||
>
|
||||
{field.value
|
||||
? (selectedModel?.id ?? field.value)
|
||||
: "Select a model"}
|
||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search models..."
|
||||
value={modelSearch}
|
||||
onValueChange={setModelSearch}
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search or type a custom model..."
|
||||
value={modelSearch}
|
||||
onValueChange={setModelSearch}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{modelSearch ? (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full cursor-pointer px-2 py-1.5 text-left text-sm hover:bg-accent"
|
||||
onClick={() => {
|
||||
field.onChange(modelSearch);
|
||||
setModelPopoverOpen(false);
|
||||
setModelSearch("");
|
||||
}}
|
||||
>
|
||||
Use custom model: "{modelSearch}"
|
||||
</button>
|
||||
) : (
|
||||
"No models found."
|
||||
)}
|
||||
</CommandEmpty>
|
||||
{displayModels.map((model) => {
|
||||
const isSelected = field.value === model.id;
|
||||
return (
|
||||
<CommandItem
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
onSelect={() => {
|
||||
field.onChange(model.id);
|
||||
setModelPopoverOpen(false);
|
||||
setModelSearch("");
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
isSelected
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{model.id}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={
|
||||
isLoadingServerModels
|
||||
? "Loading models..."
|
||||
: "Enter model name (e.g. gpt-4o)"
|
||||
}
|
||||
disabled={isLoadingServerModels}
|
||||
{...field}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No models found.</CommandEmpty>
|
||||
{displayModels.map((model) => {
|
||||
const isSelected = field.value === model.id;
|
||||
return (
|
||||
<CommandItem
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
onSelect={() => {
|
||||
field.onChange(model.id);
|
||||
setModelPopoverOpen(false);
|
||||
setModelSearch("");
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
isSelected
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{model.id}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormDescription>
|
||||
Select an AI model to use
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</FormControl>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FormDescription>
|
||||
Select a model from the list or type a custom model name
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -372,7 +459,12 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<TestConnectionButton
|
||||
apiUrl={apiUrl}
|
||||
apiKey={apiKey}
|
||||
model={form.watch("model")}
|
||||
/>
|
||||
<Button type="submit" isLoading={isPending}>
|
||||
{aiId ? "Update" : "Create"}
|
||||
</Button>
|
||||
@@ -383,3 +475,42 @@ export const HandleAi = ({ aiId }: Props) => {
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
function TestConnectionButton({
|
||||
apiUrl,
|
||||
apiKey,
|
||||
model,
|
||||
}: {
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
model: string;
|
||||
}) {
|
||||
const { mutate, isPending } = api.ai.testConnection.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success("Connection successful");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Connection failed", {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const isDisabled = !apiUrl || !model;
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={isDisabled || isPending}
|
||||
onClick={() => mutate({ apiUrl, apiKey, model })}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Plug className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Test Connection
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,9 +25,11 @@ import { findProjectById } from "@dokploy/server/services/project";
|
||||
import {
|
||||
getProviderHeaders,
|
||||
getProviderName,
|
||||
selectAIProvider,
|
||||
type Model,
|
||||
} from "@dokploy/server/utils/ai/select-ai-provider";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { generateText } from "ai";
|
||||
import { z } from "zod";
|
||||
import { slugify } from "@/lib/slug";
|
||||
import {
|
||||
@@ -95,6 +97,30 @@ export const aiRouter = createTRPCRouter({
|
||||
owned_by: "perplexity",
|
||||
},
|
||||
] as Model[];
|
||||
case "zai":
|
||||
return [
|
||||
{
|
||||
id: "glm-5",
|
||||
object: "model",
|
||||
created: Date.now(),
|
||||
owned_by: "zai",
|
||||
},
|
||||
{
|
||||
id: "glm-4.7",
|
||||
object: "model",
|
||||
created: Date.now(),
|
||||
owned_by: "zai",
|
||||
},
|
||||
] as Model[];
|
||||
case "minimax":
|
||||
return [
|
||||
{
|
||||
id: "MiniMax-M2.7",
|
||||
object: "model",
|
||||
created: Date.now(),
|
||||
owned_by: "minimax",
|
||||
},
|
||||
] as Model[];
|
||||
default:
|
||||
if (!input.apiKey)
|
||||
throw new TRPCError({
|
||||
@@ -174,6 +200,96 @@ export const aiRouter = createTRPCRouter({
|
||||
return await deleteAiSettings(input.aiId);
|
||||
}),
|
||||
|
||||
getEnabledProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
const settings = await getAiSettingsByOrganizationId(
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
return settings
|
||||
.filter((s) => s.isEnabled)
|
||||
.map((s) => ({ aiId: s.aiId, name: s.name, model: s.model }));
|
||||
}),
|
||||
|
||||
analyzeLogs: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
aiId: z.string().min(1),
|
||||
logs: z.string().min(1),
|
||||
context: z.enum(["build", "runtime"]),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const aiSettings = await getAiSettingById(input.aiId);
|
||||
if (!aiSettings?.isEnabled) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "AI provider is not enabled",
|
||||
});
|
||||
}
|
||||
|
||||
const provider = selectAIProvider(aiSettings);
|
||||
const model = provider(aiSettings.model);
|
||||
|
||||
const contextLabel =
|
||||
input.context === "build" ? "build/deployment" : "runtime/container";
|
||||
|
||||
const result = await generateText({
|
||||
model,
|
||||
prompt: `You are a DevOps engineer analyzing ${contextLabel} logs. Analyze the following logs and provide:
|
||||
|
||||
1. **Summary**: A brief summary of what's happening
|
||||
2. **Issues Found**: Any errors, warnings, or problems detected
|
||||
3. **Root Cause**: The most likely root cause if there are errors
|
||||
4. **Suggested Fix**: Actionable steps to resolve the issues
|
||||
|
||||
Be concise and practical. Focus on the most important issues. If the logs look healthy, say so briefly.
|
||||
|
||||
Logs:
|
||||
${input.logs}`,
|
||||
});
|
||||
|
||||
return { analysis: result.text };
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
error instanceof Error ? error.message : `Analysis failed: ${error}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
testConnection: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
apiUrl: z.string().min(1),
|
||||
apiKey: z.string(),
|
||||
model: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const provider = selectAIProvider({
|
||||
apiUrl: input.apiUrl,
|
||||
apiKey: input.apiKey,
|
||||
});
|
||||
const model = provider(input.model);
|
||||
const result = await generateText({
|
||||
model,
|
||||
prompt: "Reply with 'ok'",
|
||||
});
|
||||
if (!result.text) {
|
||||
throw new Error("No response received from the model");
|
||||
}
|
||||
return { success: true, message: "Connection successful" };
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
error instanceof Error ? error.message : `Connection failed: ${error}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
suggest: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
|
||||
Reference in New Issue
Block a user