Skip to content
Draft
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: 2 additions & 2 deletions src/agent/agents/subagents/tangentResearcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
*/
import { Agent } from "@openai/agents";

import { requireOrchestratorModel } from "../../config";
import { getAgentModelConfig } from "../../config";
import { attachObservabilityHooks } from "../../middleware/observability";
import tangentResearcherPrompt from "../../prompts/tangentResearcher.md?raw";
import type { AgentSession, RecentPipelineRun } from "../../session";
Expand Down Expand Up @@ -44,7 +44,7 @@ export function createTangentResearcherAgent(session: AgentSession): Agent {
hyperparameter-tuning and experiment ideas. Read-only — cannot edit the pipeline or submit runs.`,
instructions,
tools: [runTools.getRunStatus, csom.getPipelineState],
model: requireOrchestratorModel(),
...getAgentModelConfig(session.aiConfig),
modelSettings: { reasoning: { effort: "high" } },
});
attachObservabilityHooks(agent, session.emitStatus);
Expand Down
20 changes: 20 additions & 0 deletions src/routes/v2/pages/RunView/components/RunDetailsContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@ import { CopyText } from "@/components/shared/CopyText/CopyText";
import PipelineIO from "@/components/shared/Execution/PipelineIO";
import { InfoBox } from "@/components/shared/InfoBox";
import { LoadingScreen } from "@/components/shared/LoadingScreen";
import { useFlagValue } from "@/components/shared/Settings/useFlags";
import { StatusBar } from "@/components/shared/Status";
import { TagList } from "@/components/shared/Tags/TagList";
import { Button } from "@/components/ui/button";
import { Icon } from "@/components/ui/icon";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import { Paragraph, Text } from "@/components/ui/typography";
import { useUserDetails } from "@/hooks/useUserDetails";
import type { ComponentSpec } from "@/models/componentSpec";
import { useBackend } from "@/providers/BackendProvider";
import { useExecutionData } from "@/providers/ExecutionDataProvider";
import { useStartOptimizationChat } from "@/routes/v2/pages/RunView/hooks/useStartOptimizationChat";
import { useSpec } from "@/routes/v2/shared/providers/SpecContext";
import {
PIPELINE_NOTES_ANNOTATION,
Expand Down Expand Up @@ -121,6 +125,8 @@ function RunDetailsContentLoaded({
{spec.name ?? "Unnamed Pipeline"}
</CopyText>

<OptimizationButton />

{metadata && <RunInfoSection metadata={metadata} />}

{spec.description && (
Expand Down Expand Up @@ -155,6 +161,20 @@ function RunDetailsContentLoaded({
);
}

function OptimizationButton() {
const aiEnabled = useFlagValue("ai-assistant");
const startOptimizationChat = useStartOptimizationChat();

if (!aiEnabled) return null;

return (
<Button variant="secondary" onClick={startOptimizationChat}>
<Icon name="Sparkles" />
Suggest optimization
</Button>
);
}

function RunInfoSection({ metadata }: { metadata: PipelineRunResponse }) {
return (
<KeyValueList
Expand Down
2 changes: 1 addition & 1 deletion src/routes/v2/pages/RunView/hooks/useAiChatWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createRunViewToolBridge } from "@/routes/v2/pages/RunView/toolBridge/ru
import { AiChatContent } from "@/routes/v2/shared/components/AiChat/AiChatContent";
import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";

const AI_CHAT_WINDOW_ID = "run-ai-assistant-chat";
export const AI_CHAT_WINDOW_ID = "run-ai-assistant-chat";

export function useAiChatWindow(enabled: boolean) {
const { windows } = useSharedStores();
Expand Down
28 changes: 28 additions & 0 deletions src/routes/v2/pages/RunView/hooks/useStartOptimizationChat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useAiChatStore } from "@/routes/v2/shared/components/AiChat/AiChatStoreContext";
import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";

import { AI_CHAT_WINDOW_ID } from "./useAiChatWindow";

const OPTIMIZATION_PROMPT = "Suggest optimization scenario for this pipeline";

/**
* Surfaces the AI Assistant window, starts a fresh chat thread, and
* queues an optimization prompt for it. The send itself happens inside
* the chat window (which owns the tool bridge).
*/
export function useStartOptimizationChat() {
const { windows } = useSharedStores();
const aiChat = useAiChatStore();

return function startOptimizationChat() {
const win = windows.getWindowById(AI_CHAT_WINDOW_ID);
if (win) {
if (win.state === "hidden" || win.isMinimized) {
win.restore();
} else {
win.bringToFront();
}
}
aiChat.startThreadWithPrompt(OPTIMIZATION_PROMPT);
};
}
10 changes: 10 additions & 0 deletions src/routes/v2/shared/components/AiChat/AiChatContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ export const AiChatContent = observer(function AiChatContent({
});
}

// Dispatch a prompt queued externally (e.g. a "suggest optimization"
// button on another panel). This component owns the tool bridge, so it
// is the right place to actually send the message.
const pendingPrompt = aiChat.pendingPrompt;
useEffect(() => {
if (!pendingPrompt || !thread) return;
const prompt = aiChat.consumePendingPrompt();
if (prompt) handleSend(prompt);
}, [pendingPrompt, thread, aiChat]);

if (!isAiConfigured) {
return <AiProviderSetup />;
}
Expand Down
25 changes: 25 additions & 0 deletions src/routes/v2/shared/components/AiChat/aiChatStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export interface AiChatStoreConfig {
export class AiChatStore {
@observable.shallow accessor threads: AgentThread[] = [];
@observable accessor activeThreadId: string | null = null;
/**
* Prompt queued by a consumer (e.g. an external "ask the assistant"
* button) to be sent on the active thread. {@link AiChatContent} owns
* the tool bridge, so it observes this and dispatches the send.
*/
@observable accessor pendingPrompt: string | null = null;

constructor(private readonly config: AiChatStoreConfig) {
makeObservable(this);
Expand Down Expand Up @@ -62,11 +68,30 @@ export class AiChatStore {
return thread;
}

/**
* Starts a fresh thread and queues a prompt to be sent on it. The
* dispatch happens in {@link AiChatContent}, which holds the tool
* bridge required to fulfil tool calls.
*/
@action startThreadWithPrompt(prompt: string): AgentThread {
const thread = this.newThread();
this.pendingPrompt = prompt;
return thread;
}

/** Returns and clears the queued prompt, guarding against double-sends. */
@action consumePendingPrompt(): string | null {
const prompt = this.pendingPrompt;
this.pendingPrompt = null;
return prompt;
}

@action disposeAll() {
for (const thread of this.threads) {
thread.dispose();
}
this.threads = [];
this.activeThreadId = null;
this.pendingPrompt = null;
}
}
Loading