Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,9 @@ export async function POST(req: Request) {
wideEvent.status = "error";
wideEvent.error = error instanceof Error ? error.message : String(error);
if (error instanceof HttpError) {
return new Response(error.body ?? error.message, { status: error.status });
return new Response(error.body ?? error.message, {
status: error.status
});
}
return new Response("Internal Server Error", { status: 500 });
} finally {
Expand Down
42 changes: 40 additions & 2 deletions src/components/Playbooks/Runs/Actions/PlaybookRunsActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import FormatDuration from "@flanksource-ui/ui/Dates/FormatDuration";
import VerticalDescription from "@flanksource-ui/ui/description/VerticalDescription";
import { Menu } from "@flanksource-ui/ui/Menu";
import dayjs from "dayjs";
import { useMemo, useState } from "react";
import { lazy, Suspense, useMemo, useState } from "react";
import { VscFileCode } from "react-icons/vsc";
import { FaCog } from "react-icons/fa";
import { Link } from "react-router-dom";
Expand All @@ -31,13 +31,31 @@ import PlaybooksRunActionsResults from "./PlaybooksActionsResults";
import ViewPlaybookSpecModal from "./ViewPlaybookSpecModal";
import ViewPlaybookParamsModal from "./ShowParamaters/ViewPlaybookParamsModal";
import FailedChildRunComponent from "./FailedChildRunComponent";
import { Sparkles } from "lucide-react";
import { AiFeatureRequest } from "@flanksource-ui/ui/Layout/AiFeatureLoader";
import { useFeatureFlagsContext } from "@flanksource-ui/context/FeatureFlagsContext";
import { features } from "@flanksource-ui/services/permissions/features";
import { Button } from "@flanksource-ui/ui/Buttons/Button";

const LazyDiagnoseButton = lazy(() =>
import("../DiagnosePlaybookFailureButton").then((module) => ({
default: module.DiagnosePlaybookFailureButton
}))
);

type PlaybookRunActionsProps = {
data: PlaybookRunWithActions;
refetch?: () => void;
};

export default function PlaybookRunsActions({
/**
* Displays the detail view for a single playbook run, including run metadata
* (status, duration, triggered by, etc.), an action sidebar listing each step,
* and a result panel showing the selected action's output. For failed runs,
* an AI "Diagnose Failure" button is rendered to open the navbar AI chat
* pre-populated with run context.
*/
export default function PlaybookRunDetailView({
data,
refetch = () => {}
}: PlaybookRunActionsProps) {
Expand Down Expand Up @@ -70,6 +88,9 @@ export default function PlaybookRunsActions({

const resource = getResourceForRun(data);

const { isFeatureDisabled } = useFeatureFlagsContext();
const isAiDisabled = isFeatureDisabled(features.ai);

// if the playbook run failed, create an action for the initialization step
// that shows the error message
const initializationAction = useMemo(() => {
Expand Down Expand Up @@ -216,6 +237,8 @@ export default function PlaybookRunsActions({
/>
)}
</div>

{/* Run action buttons: approve, cancel, AI diagnose (failed only), rerun, and kebab menu */}
<div className="ml-auto flex h-auto flex-col justify-center gap-2">
<div className="flex flex-row items-center gap-2">
<ApprovePlaybookButton
Expand All @@ -232,6 +255,21 @@ export default function PlaybookRunsActions({
status={data.status}
/>

{!isAiDisabled && data.status === "failed" && (
<AiFeatureRequest>
<Suspense
fallback={
<Button className="btn-white min-w-max space-x-1" disabled>
<Sparkles className="h-5 w-5 animate-pulse" />
<span>Diagnose Failure</span>
</Button>
}
>
<LazyDiagnoseButton data={data} />
</Suspense>
</AiFeatureRequest>
)}

<ReRunPlaybookWithParamsButton
playbook={data.playbooks!}
params={data.parameters}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import PlaybookRunsActions from "./../PlaybookRunsActions";
import PlaybookRunDetailView from "./../PlaybookRunsActions";
import * as playbooksApi from "../../../../../api/services/playbooks";
import { AiFeatureLoaderProvider } from "@flanksource-ui/ui/Layout/AiFeatureLoader";

const mockAction: PlaybookRunAction = {
playbook_run_id: "1",
Expand Down Expand Up @@ -45,7 +46,7 @@ afterEach(() => {
jest.clearAllMocks();
});

describe("PlaybookRunsActions", () => {
describe("PlaybookRunDetailView", () => {
it("should have playbook metadaba", () => {
const queryClient = createQueryClient();
const mockData: PlaybookRunWithActions = {
Expand Down Expand Up @@ -76,7 +77,9 @@ describe("PlaybookRunsActions", () => {
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<PlaybookRunsActions data={mockData} />
<AiFeatureLoaderProvider>
<PlaybookRunDetailView data={mockData} />
</AiFeatureLoaderProvider>
</MemoryRouter>
</QueryClientProvider>
);
Expand Down Expand Up @@ -135,7 +138,9 @@ describe("PlaybookRunsActions", () => {
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<PlaybookRunsActions data={mockData} />
<AiFeatureLoaderProvider>
<PlaybookRunDetailView data={mockData} />
</AiFeatureLoaderProvider>
</MemoryRouter>
</QueryClientProvider>
);
Expand Down Expand Up @@ -183,7 +188,9 @@ describe("PlaybookRunsActions", () => {
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<PlaybookRunsActions data={mockData} />
<AiFeatureLoaderProvider>
<PlaybookRunDetailView data={mockData} />
</AiFeatureLoaderProvider>
</MemoryRouter>
</QueryClientProvider>
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down
78 changes: 78 additions & 0 deletions src/components/Playbooks/Runs/DiagnosePlaybookFailureButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useCallback } from "react";
import { type PlaybookRunWithActions } from "@flanksource-ui/api/types/playbooks";
import { useAiChatPopover } from "@flanksource-ui/components/ai/AiChatPopover";
import { Button } from "@flanksource-ui/ui/Buttons/Button";
import { Sparkles } from "lucide-react";

type DiagnosePlaybookFailureButtonProps = {
data: PlaybookRunWithActions;
};

export function DiagnosePlaybookFailureButton({
data
}: DiagnosePlaybookFailureButtonProps) {
const { setOpen, setChatMessages } = useAiChatPopover();

const handleDiagnoseFailure = useCallback(() => {
const systemPrompt = `You are helping diagnose why a Playbook run failed in Mission Control.

Context on Playbooks:
- A Playbook is an automated workflow that executes a sequence of Actions
- Each Action in the playbook has a status (scheduled, running, completed, failed, skipped, etc.)
- A PlaybookRun is an instance/execution of a Playbook
- PlaybookRuns can have parameters and child runs

When analyzing a failed run:
1. Check the overall run status and error message
2. Identify which actions failed and their error details
3. Look at action results to understand what went wrong
4. Consider the parameters that were passed in
5. Look at the playbook spec to understand the intended flow
6. Suggest specific fixes based on the error

Be concise and actionable in your diagnosis.`;

const failedActions =
data.actions?.filter((a) => a.status === "failed") || [];
const userPrompt = `Please diagnose why this playbook run failed.${
data.error ? ` Run error: ${data.error}` : ""
}${
failedActions.length > 0
? ` ${failedActions.length} action(s) failed.`
: ""
} What went wrong and how do we fix it?`;

const text = JSON.stringify(data, null, 2);

setChatMessages([
{
id: `system-${Date.now()}`,
role: "system",
parts: [{ type: "text", text: systemPrompt }]
},
{
id: `user-${Date.now()}`,
role: "user",
parts: [
{
type: "text",
text: `${userPrompt}\n\nPlaybook Run Data:\n\`\`\`json\n${text}\n\`\`\``
}
]
}
]);

setOpen(true);
}, [data, setChatMessages, setOpen]);

return (
<Button
onClick={handleDiagnoseFailure}
className="btn-white min-w-max space-x-1"
title="Diagnose playbook failure with AI"
>
<Sparkles className="h-5 w-5" />
<span>Diagnose Failure</span>
</Button>
);
}
16 changes: 15 additions & 1 deletion src/components/ai/AiChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import { formatTick, parseTimestamp } from "@flanksource-ui/lib/timeseries";
import { cn } from "@flanksource-ui/lib/utils";
import type { FileUIPart, ReasoningUIPart, UIMessage } from "ai";
import { getToolName, isToolUIPart } from "ai";
import { useCallback, useMemo } from "react";
import { useCallback, useEffect, useMemo, useRef } from "react";
import { ErrorViewer } from "@flanksource-ui/components/ErrorViewer";
import { CartesianGrid, Line, ComposedChart, XAxis, YAxis } from "recharts";

Expand Down Expand Up @@ -213,6 +213,20 @@ export function AIChat({
id
});

// Auto-send when chat mounts with a pre-seeded user message (e.g. from setChatMessages).
const hasSentOnMount = useRef(false);
useEffect(() => {
if (
!hasSentOnMount.current &&
messages.length > 0 &&
messages[messages.length - 1].role === "user" &&
status === "ready"
) {
hasSentOnMount.current = true;
sendMessage();
}
}, [messages, status, sendMessage]);

const handleToolApproval = useCallback(
async (approvalId: string, approved: boolean) => {
await addToolApprovalResponse({
Expand Down
1 change: 0 additions & 1 deletion src/components/ai/AiChatPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export function AiChatPopoverProvider({
const [quickPrompts, setQuickPrompts] = useState<string[] | undefined>(
undefined
);

const setOpen = useCallback(
(open: boolean) => {
setOpenState(open);
Expand Down
4 changes: 2 additions & 2 deletions src/pages/playbooks/PlaybookRunsDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getPlaybookRunsWithActions } from "@flanksource-ui/api/services/playbooks";
import { CheckLink } from "@flanksource-ui/components/Canary/HealthChecks/CheckLink";
import ConfigLink from "@flanksource-ui/components/Configs/ConfigLink/ConfigLink";
import PlaybookRunsActions from "@flanksource-ui/components/Playbooks/Runs/Actions/PlaybookRunsActions";
import PlaybookRunDetailView from "@flanksource-ui/components/Playbooks/Runs/Actions/PlaybookRunsActions";
import { playbookRunsPageTabs } from "@flanksource-ui/components/Playbooks/Runs/PlaybookRunsPageTabs";
import PlaybookSpecIcon from "@flanksource-ui/components/Playbooks/Settings/PlaybookSpecIcon";
import { TopologyLink } from "@flanksource-ui/components/Topology/TopologyLink";
Expand Down Expand Up @@ -117,7 +117,7 @@ export default function PlaybookRunsDetailsPage() {
<TabbedLinks activeTabName={`Runs`} tabLinks={playbookRunsPageTabs}>
<div className={`mx-auto flex h-full w-full flex-col p-4`}>
{playbookRunsWithActions ? (
<PlaybookRunsActions
<PlaybookRunDetailView
data={playbookRunsWithActions}
refetch={refetch}
/>
Expand Down
Loading