@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
273 lines (234 loc) • 8.58 kB
text/typescript
import { EventEmitter } from 'events';
import { ToolCall } from '../ai/manager';
import { UserContext } from '../components/mcpOutput/MCPArgumentProcessor';
interface InlineToolApprovalRequest {
requestId: string;
toolCalls: ToolCall[];
resolve: (approvedToolIds: string[]) => void;
reject: (error: Error) => void;
// Reference to the AI manager for adding messages to history
aiManager?: any;
// Callback to update the specific message with loading state
updateMessage?: (loading: boolean) => void;
}
export class InlineToolApprovalManager extends EventEmitter {
private static instance: InlineToolApprovalManager | null = null;
private pendingRequest: InlineToolApprovalRequest | null = null;
private autoApproveSettings: Map<string, boolean> = new Map();
private sessionAutoApproval: boolean = false;
private constructor() {
super();
}
public static getInstance(): InlineToolApprovalManager {
if (!InlineToolApprovalManager.instance) {
InlineToolApprovalManager.instance = new InlineToolApprovalManager();
}
return InlineToolApprovalManager.instance;
}
/**
* Extract user context from AI manager
*/
private extractUserContext(aiManager: any): UserContext {
const userContext: UserContext = {
timeContext: new Date(),
};
try {
// Extract user message from history
const history = aiManager.history || [];
const lastUserMessage = history.filter((msg: any) => msg.role === 'user').pop();
if (lastUserMessage) {
userContext.userMessage = lastUserMessage.content;
}
// Extract conversation history (last 5 messages for context)
userContext.conversationHistory = history.slice(-5).map((msg: any) => ({
role: msg.role,
content: msg.content,
}));
// Extract kubernetes context if available
if (aiManager.toolManager?.kubernetesContext) {
userContext.kubernetesContext = {
selectedClusters: aiManager.toolManager.kubernetesContext.selectedClusters,
namespace: aiManager.toolManager.kubernetesContext.namespace,
currentResource: aiManager.toolManager.kubernetesContext.currentResource,
};
}
// Extract last tool results
const lastToolResults = history.filter((msg: any) => msg.role === 'tool').slice(-3);
if (lastToolResults.length > 0) {
userContext.lastToolResults = {};
lastToolResults.forEach((toolMsg: any, index: number) => {
try {
const parsed = JSON.parse(toolMsg.content);
userContext.lastToolResults![`tool_${index}`] = parsed;
} catch {
userContext.lastToolResults![`tool_${index}`] = toolMsg.content;
}
});
}
} catch (error) {
console.warn('Failed to extract user context:', error);
}
return userContext;
}
/**
* Request approval for tool execution via inline chat message
*/
async requestApproval(toolCalls: any[], aiManager: any): Promise<string[]> {
// Check if session auto-approval is enabled
if (this.sessionAutoApproval) {
return toolCalls.map(tool => tool.id);
}
// Check for individual tool auto-approvals
const autoApprovedTools: string[] = [];
const needsApprovalTools: ToolCall[] = [];
for (const tool of toolCalls) {
if (this.autoApproveSettings.get(tool.name)) {
autoApprovedTools.push(tool.id);
} else {
needsApprovalTools.push(tool);
}
}
// If all tools are auto-approved, return them
if (needsApprovalTools.length === 0) {
return autoApprovedTools;
}
// If there's already a pending request, reject the previous one
if (this.pendingRequest) {
this.pendingRequest.reject(new Error('Request superseded by new tool approval request'));
}
// Extract user context for intelligent argument processing
const userContext = this.extractUserContext(aiManager);
return new Promise<string[]>((resolve, reject) => {
const requestId = `tool-approval-${Date.now()}-${Math.random()}`;
const handleApprove = (approvedToolIds: string[]) => {
// Combine auto-approved and manually approved tools
const allApprovedIds = [...autoApprovedTools, ...approvedToolIds];
// Update the message to show loading state
if (this.pendingRequest?.updateMessage) {
this.pendingRequest.updateMessage(true);
}
this.pendingRequest = null;
resolve(allApprovedIds);
};
const handleDeny = () => {
this.pendingRequest = null;
reject(new Error('User denied tool execution'));
};
const updateMessage = (loading: boolean) => {
// Emit an event to update the UI
if (this.pendingRequest) {
this.emit('update-confirmation', {
requestId: this.pendingRequest.requestId,
toolConfirmation: {
tools: this.pendingRequest.toolCalls,
onApprove: handleApprove,
onDeny: handleDeny,
loading: loading,
requestId: this.pendingRequest.requestId, // Include requestId
userContext: userContext, // Include user context
},
});
}
};
this.pendingRequest = {
requestId,
toolCalls: needsApprovalTools,
resolve: handleApprove,
reject: handleDeny,
aiManager,
updateMessage,
};
// Emit event to add the tool confirmation message to chat history
this.emit('request-confirmation', {
requestId,
toolConfirmation: {
tools: needsApprovalTools,
onApprove: handleApprove,
onDeny: handleDeny,
loading: false,
requestId: requestId, // Include requestId in the tool confirmation
userContext: userContext, // Pass user context for intelligent argument processing
},
aiManager,
});
});
}
/**
* Approve tools (called from inline confirmation component)
*/
public approveTools(requestId: string, approvedToolIds: string[], rememberChoice = false): void {
if (!this.pendingRequest || this.pendingRequest.requestId !== requestId) {
console.warn('No matching pending request for approval:', requestId);
return;
}
// Handle remember choice
if (rememberChoice) {
// If all tools were approved, enable session auto-approval
const allToolIds = this.pendingRequest.toolCalls.map(tool => tool.id);
if (approvedToolIds.length === allToolIds.length) {
this.sessionAutoApproval = true;
} else {
// Remember individual tool approvals
for (const toolCall of this.pendingRequest.toolCalls) {
if (approvedToolIds.includes(toolCall.id)) {
this.autoApproveSettings.set(toolCall.name, true);
}
}
}
}
this.pendingRequest.resolve(approvedToolIds);
}
/**
* Deny all tools (called from inline confirmation component)
*/
public denyTools(requestId: string): void {
if (!this.pendingRequest || this.pendingRequest.requestId !== requestId) {
console.warn('No matching pending request for denial:', requestId);
return;
}
this.pendingRequest.reject(new Error('User denied tool execution'));
}
/**
* Get current pending request
*/
public getPendingRequest(): InlineToolApprovalRequest | null {
return this.pendingRequest;
}
/**
* Clear session settings (called when user explicitly clears or starts new session)
*/
public clearSession(): void {
this.sessionAutoApproval = false;
this.autoApproveSettings.clear();
}
/**
* Set session auto-approval
*/
public setSessionAutoApproval(enabled: boolean): void {
this.sessionAutoApproval = enabled;
}
/**
* Get session auto-approval status
*/
public isSessionAutoApprovalEnabled(): boolean {
return this.sessionAutoApproval;
}
/**
* Set auto-approval for a specific tool
*/
public setToolAutoApproval(toolName: string, enabled: boolean): void {
if (enabled) {
this.autoApproveSettings.set(toolName, true);
} else {
this.autoApproveSettings.delete(toolName);
}
}
/**
* Check if a tool has auto-approval enabled
*/
public isToolAutoApprovalEnabled(toolName: string): boolean {
return this.autoApproveSettings.get(toolName) === true;
}
}
// Export singleton instance
export const inlineToolApprovalManager = InlineToolApprovalManager.getInstance();