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
2 changes: 1 addition & 1 deletion .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { store } from "./store";

const preview: Preview = {
decorators: [
withRouter,
withRouter, // TODO: Fix the story crash on frequent rerendering
(Story: StoryFn, context): JSX.Element => {
const [isInitialized, setIsInitialized] = useState(false);
const theme = context.globals.theme as Theme;
Expand Down
30 changes: 18 additions & 12 deletions src/components/Agentic/IncidentDetails/IncidentSummary/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ import { IncidentSummaryRecord } from "./IncidentSummaryRecord";
import * as s from "./styles";
import type { IncidentSummaryProps } from "./types";

export const IncidentSummary = ({ records }: IncidentSummaryProps) => (
<s.Container>
{records.map((record) => (
<IncidentSummaryRecord
key={record.agent}
agent={record.agent_display_name}
datetime={record.timestamp}
text={record.text}
/>
))}
</s.Container>
);
export const IncidentSummary = ({ records }: IncidentSummaryProps) => {
if (records.length === 0) {
return null;
}

return (
<s.Container>
{records.map((record) => (
<IncidentSummaryRecord
key={record.agent}
agent={record.agent_display_name}
datetime={record.timestamp}
text={record.text}
/>
))}
</s.Container>
);
};
93 changes: 93 additions & 0 deletions src/components/Agentic/common/AgentChat/AgentChat.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { Meta, StoryObj } from "@storybook/react-webpack5";
import { useEffect, useState } from "react";
import { fn } from "storybook/test";
import { AgentChat } from ".";
import type { IncidentAgentEvent } from "../../../../redux/services/types";

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta<typeof AgentChat> = {
title: "Agentic/common/AgentChat",
component: AgentChat,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
layout: "fullscreen"
}
};

export default meta;

type Story = StoryObj<typeof AgentChat>;

// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
const mockedAgentEvents: IncidentAgentEvent[] = [
{
id: "1",
type: "human",
message: "Can you help me understand why my API response time is slow?",
agent_name: "user"
},
{
id: "2",
type: "token",
message: "Let me analyze your application's performance data...",
agent_name: "agent"
},
{
id: "3",
type: "token",
message:
"I've found several performance issues in your application:\n\n1. **Database Query Optimization**: Your user lookup queries are taking an average of 2.3 seconds\n2. **Memory Usage**: High memory allocation in the user service\n3. **Cache Misses**: 78% cache miss rate on user data\n\nWould you like me to suggest specific optimizations for any of these areas?",
agent_name: "agent"
},
{
id: "4",
type: "tool",
agent_name: "agent",
message:
'\n```json\n{\n "success": false,\n "blockers": "Limited tool access prevents thorough investigation of system-wide issues, infrastructure problems, service mesh configurations, and external dependencies. Need access to Kubernetes API, service mesh telemetry, network monitoring tools, and container logs.",\n "result": {\n "is_relevant": true,\n "objective_success": false,\n "blockers": "Limited tool access prevents thorough investigation of system-wide issues, infrastructure problems, service mesh configurations, and external dependencies. Need access to Kubernetes API, service mesh telemetry, network monitoring tools, and container logs.",\n "beyond_the_result": {\n "summary": "Unable to fully investigate alternative causes due to tool limitations, but analysis suggests potential issues in service mesh, network policies, or external dependencies",\n "description": "The investigation revealed a severe performance degradation (2650%) in the PipelineConnector Execute operation that has been ongoing for over 24 hours. While direct investigation was limited by tool access, the pattern and severity suggest potential issues with service mesh routing, network policies, cross-namespace communication, or external service dependencies rather than simple resource constraints.",\n "confidence_level": "30",\n "confidence_level_reason": "Limited tool access prevents thorough investigation of infrastructure and network-related causes. The assessment is based primarily on timing patterns and service impact analysis rather than direct evidence."\n },\n "next_steps_suggestions": "1. Request access to Kubernetes cluster information and API\\n2. Obtain access to service mesh telemetry and dashboard\\n3. Deploy network monitoring tools\\n4. Enable access to container logs\\n5. Once access is granted, conduct thorough analysis of namespace configurations, service mesh settings, network policies, and external service dependencies",\n "actions_taken": [\n {\n "action": "Gathered relevant objects",\n "action_execution_success": true,\n "action_command": "list_relevant_incident_objects",\n "resolution_success_status": "PARTIAL",\n "resolution_explanation": "Successfully identified the critical trace ID but couldn\'t gather infrastructure-related objects",\n "resolution_success_evidence": "Retrieved trace ID FB0C56FA98816BBBFBB934CCEDEA72E4 showing the performance degradation",\n "state_changes_confirmed_due_to_actions": "Confirmed existence of trace showing 2650% performance degradation in PipelineConnector Execute operation"\n },\n {\n "action": "Tracked relevant trace",\n "action_execution_success": true,\n "action_command": "track_incident_relevant_object",\n "resolution_success_status": "PARTIAL",\n "resolution_explanation": "Successfully tracked the critical trace ID for future reference",\n "resolution_success_evidence": "Trace ID FB0C56FA98816BBBFBB934CCEDEA72E4 was successfully tracked",\n "state_changes_confirmed_due_to_actions": "Added trace to tracked objects for future investigation"\n }\n ]\n }\n}\n```\n',
tool_name: "kubernetes_resolution_expert_tool",
mcp_name: "",
status: "success"
},
{
id: "5",
type: "token",
message:
"Here are my recommendations for optimizing your database queries:\n\n```sql\n-- Add an index on the email column\nCREATE INDEX idx_users_email ON users(email);\n\n-- Use prepared statements\nSELECT id, name, email FROM users WHERE email = ?\n```\n\nThis should reduce your query time from 2.3s to under 100ms.",
agent_name: "agent"
}
];

const EVENTS_CURSOR = 2;
const EVENTS_TIMEOUT = 2000;

export const Default: Story = {
args: {
data: mockedAgentEvents.slice(0, EVENTS_CURSOR),
isDataLoading: false,
onMessageSend: fn(),
typeInitialMessages: false
},
render: (args) => {
const [events, setEvents] = useState<IncidentAgentEvent[]>(args.data ?? []);

useEffect(() => {
// Simulate messages arriving progressively
const timeouts: number[] = [];

mockedAgentEvents.slice(EVENTS_CURSOR).forEach((event, index) => {
const timeout = window.setTimeout(() => {
setEvents((prev) => [...prev, event]);
}, index * EVENTS_TIMEOUT);

timeouts.push(timeout);
});

return () => {
timeouts.forEach((timeout) => clearTimeout(timeout));
};
}, []);

return <AgentChat {...args} data={events} />;
}
};
114 changes: 72 additions & 42 deletions src/components/Agentic/common/AgentEventsList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,101 @@
import { useEffect, useMemo, useState } from "react";
import { isNumber } from "../../../../typeGuards/isNumber";
import type { IncidentAgentEvent } from "../../../../redux/services/types";
import { AgentEvent } from "./AgentEvent";
import type { AgentEventsListProps } from "./types";
import type { AgentEventsListProps, RenderState } from "./types";

const isTypingEvent = (event: IncidentAgentEvent) =>
["ai", "token"].includes(event.type);

export const AgentEventsList = ({
events,
onNavigateToIncident,
typeInitialEvents
}: AgentEventsListProps) => {
const [initialEventsCount] = useState<number>(
typeInitialEvents ? 0 : events.length
);
const [eventsVisibleCount, setEventsVisibleCount] = useState<number>(
const [initialVisibleCount] = useState(() =>
typeInitialEvents ? 0 : events.length
);

const agentEventsIndexes = useMemo(
() =>
events.reduce((acc, event, index) => {
if (["ai", "token"].includes(event.type)) {
acc.push(index);
}
return acc;
}, [] as number[]),
[events]
);
const [renderState, setRenderState] = useState<RenderState>({
currentEventIndex: initialVisibleCount - 1,
isTyping: false
});

const handleEventTypingComplete = (id: string) => {
const i = events.findIndex((x) => x.id === id);
const completedEventIndex = events.findIndex((event) => event.id === id);

const nextAgentEventIndex = agentEventsIndexes.find((el) => el > i);
const nextTypingEventIndex = events.findIndex(
(event, index) => index > completedEventIndex && isTypingEvent(event)
);

if (isNumber(nextAgentEventIndex)) {
setEventsVisibleCount(nextAgentEventIndex + 1);
if (nextTypingEventIndex !== -1) {
setRenderState({
currentEventIndex: nextTypingEventIndex,
isTyping: true
});
} else {
setEventsVisibleCount(events.length);
setRenderState({
currentEventIndex: events.length - 1,
isTyping: false
});
}
};

const visibleEvents = useMemo(
() => events.slice(0, eventsVisibleCount),
[events, eventsVisibleCount]
);

const shouldShowTypingForEvent = (id: string) => {
const index = visibleEvents.findIndex((x) => x.id === id);
const eventIndex = events.findIndex((event) => event.id === id);
return (
eventIndex >= initialVisibleCount &&
eventIndex === renderState.currentEventIndex &&
renderState.isTyping
);
};

if (index < 0) {
return false;
// Handle new events
useEffect(() => {
if (renderState.currentEventIndex >= events.length - 1) {
return;
}

return index >= initialEventsCount;
};
if (renderState.isTyping) {
return;
}

useEffect(() => {
if (events.length > eventsVisibleCount) {
const nextAgentEventIndex = agentEventsIndexes.find(
(index) => index >= eventsVisibleCount
);
const nextEventIndex = renderState.currentEventIndex + 1;
const nextEvent = events[nextEventIndex];

const isInitialEvent = nextEventIndex < initialVisibleCount;

if (isNumber(nextAgentEventIndex)) {
setEventsVisibleCount(nextAgentEventIndex + 1);
} else {
setEventsVisibleCount(events.length);
}
// Start typing if
// either it's an initial event with typeInitialEvents=true
// or
// it's a new event
if (
isTypingEvent(nextEvent) &&
((isInitialEvent && typeInitialEvents) || !isInitialEvent)
) {
setRenderState({
currentEventIndex: nextEventIndex,
isTyping: true
});
return;
}
}, [events.length, eventsVisibleCount, agentEventsIndexes]);

// Otherwise, show the next event immediately
setRenderState((prev) => ({
...prev,
currentEventIndex: nextEventIndex
}));
}, [
events,
renderState.currentEventIndex,
renderState.isTyping,
initialVisibleCount,
typeInitialEvents
]);

const visibleEvents = useMemo(
() => events.slice(0, renderState.currentEventIndex + 1),
[events, renderState.currentEventIndex]
);

return visibleEvents.map((event) => (
<AgentEvent
Expand Down
5 changes: 5 additions & 0 deletions src/components/Agentic/common/AgentEventsList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ export interface AgentEventsListProps {
onNavigateToIncident?: () => void;
typeInitialEvents?: boolean;
}

export interface RenderState {
currentEventIndex: number;
isTyping: boolean;
}
44 changes: 32 additions & 12 deletions src/components/Agentic/common/AgentFlowChart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,28 @@ import type {
import { AgentFlowChartNodeToolbar } from "./AgentFlowChartNodeToolbar";
import type { AgentFlowChartProps, ExtendedAgent } from "./types";

const isAgentWaitingOrSkipped = (
agents: ExtendedAgent[],
agentName: string
) => {
const agent = agents.find((a) => a.name === agentName);
return agent ? ["waiting", "skipped"].includes(agent.status) : undefined;
};

const getFlowChartNodeData = ({
agent,
isSelected,
isInteractive,
isDisabled,
isEditMode,
onAddMCPServer,
onEditMCPServer,
onDeleteMCPServer
}: {
agent?: ExtendedAgent;
isInteractive?: boolean;
isSelected?: boolean;
isInteractive?: boolean;
isDisabled?: boolean;
isEditMode?: boolean;
onAddMCPServer?: (agentName: string) => void;
onEditMCPServer?: (agentName: string, server: string) => void;
Expand Down Expand Up @@ -69,7 +79,7 @@ const getFlowChartNodeData = ({
isPending: agent.status === "pending",
hasError: agent.status === "error",
isInteractive,
isDisabled: agent.status === "skipped",
isDisabled,
sideContainers: [
{
isVisible: Boolean(agent.mcp_servers.length > 0 || isEditMode),
Expand All @@ -86,8 +96,7 @@ const getFlowChartNodeData = ({
/>
)
}
],
isKebabMenuVisible: isEditMode
]
}
: {};
};
Expand Down Expand Up @@ -186,8 +195,11 @@ export const AgentFlowChartComponent = ({
agent: extendedAgents?.find((a) => a.name === "watchman"),
isSelected: "watchman" === selectedAgentId,
isInteractive:
extendedAgents?.find((a) => a.name === "watchman")?.status !==
"skipped",
Boolean(isEditMode) ||
!isAgentWaitingOrSkipped(extendedAgents, "watchman"),
isDisabled:
!isEditMode &&
isAgentWaitingOrSkipped(extendedAgents, "watchman"),
isEditMode,
onAddMCPServer,
onEditMCPServer,
Expand All @@ -204,8 +216,10 @@ export const AgentFlowChartComponent = ({
agent: extendedAgents?.find((a) => a.name === "triager"),
isSelected: "triager" === selectedAgentId,
isInteractive:
extendedAgents?.find((a) => a.name === "triager")?.status !==
"skipped",
Boolean(isEditMode) ||
!isAgentWaitingOrSkipped(extendedAgents, "triager"),
isDisabled:
!isEditMode && isAgentWaitingOrSkipped(extendedAgents, "triager"),
isEditMode,
onAddMCPServer,
onEditMCPServer,
Expand All @@ -222,8 +236,11 @@ export const AgentFlowChartComponent = ({
agent: extendedAgents?.find((a) => a.name === "infra_resolver"),
isSelected: "infra_resolver" === selectedAgentId,
isInteractive:
extendedAgents?.find((a) => a.name === "infra_resolver")
?.status !== "skipped",
Boolean(isEditMode) ||
!isAgentWaitingOrSkipped(extendedAgents, "infra_resolver"),
isDisabled:
!isEditMode &&
isAgentWaitingOrSkipped(extendedAgents, "infra_resolver"),
isEditMode,
onAddMCPServer,
onEditMCPServer,
Expand All @@ -240,8 +257,11 @@ export const AgentFlowChartComponent = ({
agent: extendedAgents?.find((a) => a.name === "code_resolver"),
isSelected: "code_resolver" === selectedAgentId,
isInteractive:
extendedAgents?.find((a) => a.name === "code_resolver")
?.status !== "skipped",
Boolean(isEditMode) ||
!isAgentWaitingOrSkipped(extendedAgents, "code_resolver"),
isDisabled:
!isEditMode &&
isAgentWaitingOrSkipped(extendedAgents, "code_resolver"),
isEditMode,
onAddMCPServer,
onEditMCPServer,
Expand Down
Loading
Loading