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
17 changes: 1 addition & 16 deletions docs/guides/agent-ui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,6 @@ GAIA Agent UI is a desktop interface for running AI agents **100% locally** on y
**Ready to install?** See the [Quickstart](/quickstart#agent-ui-fastest) for installation instructions.
</Info>

<Warning>
**Tested Configuration:** The Agent UI has been tested exclusively on **AMD Ryzen AI MAX+ 395** processors running the **Qwen3-Coder-30B-A3B-Instruct-GGUF** model via Lemonade Server. Other hardware or model combinations may work but are not officially verified.

If you encounter issues on a different configuration, please [open a GitHub issue](https://github.com/amd/gaia/issues/new) and include:
- Your processor model (e.g., Ryzen AI 9 HX 370, Ryzen AI MAX+ 395)
- RAM and available memory
- The LLM model you are using
- Operating system and version
- Steps to reproduce the issue
</Warning>

---

## What You Can Do
Expand Down Expand Up @@ -85,11 +74,7 @@ See the [Agent UI MCP Server guide](/guides/mcp/agent-ui) for setup instructions

<Accordion title="Port 4200 already in use">
```bash
# npm CLI
gaia-ui --port 8080

# Python CLI
gaia --ui-port 8080
gaia --ui --ui-port 8080
```
</Accordion>

Expand Down
22 changes: 8 additions & 14 deletions docs/sdk/sdks/agent-ui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ from gaia.ui.models import SystemStatus, ChatRequest, SessionResponse, DocumentR

**See also:** [User Guide](/guides/agent-ui) | [Agent SDK](/sdk/sdks/chat) | [API Specification](/spec/agent-ui-server)

<Warning>
**Tested Configuration:** The Agent UI has been tested on **AMD Ryzen AI MAX+ 395** with **Qwen3-Coder-30B-A3B-Instruct-GGUF**. Other configurations are not officially verified. See the [User Guide](/guides/agent-ui) for full details and how to report issues on other hardware.
</Warning>

---

## Overview
Expand Down Expand Up @@ -350,7 +346,6 @@ class MessageResponse(BaseModel):
content: str
created_at: str
rag_sources: Optional[List[SourceInfo]] = None
agent_steps: Optional[List[AgentStepResponse]] = None

class MessageListResponse(BaseModel):
messages: List[MessageResponse]
Expand All @@ -370,7 +365,6 @@ class DocumentResponse(BaseModel):
indexed_at: str
last_accessed_at: Optional[str] = None
sessions_using: int = 0
indexing_status: str = "complete" # pending | indexing | complete | failed | cancelled | missing

class DocumentListResponse(BaseModel):
documents: List[DocumentResponse]
Expand Down Expand Up @@ -865,8 +859,8 @@ from gaia.rag.sdk import RAGSDK, RAGConfig

config = RAGConfig()
rag = RAGSDK(config)
result = rag.index_document(filepath)
chunk_count = result.get("num_chunks", 0)
result = rag.index_file(filepath)
chunk_count = result.get("chunk_count", 0)
```

---
Expand All @@ -879,16 +873,16 @@ GAIA Agent UI is also available as an npm package for quick installation:
npm install -g @amd-gaia/agent-ui
```

This provides the `gaia-ui` CLI command:
This provides the `gaia` CLI command:

```bash
gaia-ui # Start Python backend + open browser
gaia-ui --serve # Serve frontend only (Node.js static server)
gaia-ui --port 8080 # Custom port
gaia-ui --version # Show version
gaia # Start Python backend + open browser
gaia --serve # Serve frontend only (Node.js static server)
gaia --port 8080 # Custom port
gaia --version # Show version
```

On first run, `gaia-ui` automatically installs the Python backend (uv, Python 3.12, amd-gaia) if not already present. On subsequent runs, it auto-updates if the version doesn't match.
On first run, `gaia` automatically installs the Python backend (uv, Python 3.12, amd-gaia) if not already present.

### Package Contents

Expand Down
15 changes: 15 additions & 0 deletions src/gaia/agents/base/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
CHUNK_TRUNCATION_THRESHOLD = 5000
CHUNK_TRUNCATION_SIZE = 2500

# Tools that require explicit user confirmation before execution.
# Adding a tool name here causes _execute_tool() to call
# console.confirm_tool_execution() and block until the user responds.
TOOLS_REQUIRING_CONFIRMATION = {"run_shell_command"}


class Agent(abc.ABC):
"""
Expand Down Expand Up @@ -1148,6 +1153,16 @@ def _execute_tool(self, tool_name: str, tool_args: Dict[str, Any]) -> Any:
logger.error(f"Tool '{tool_name}' not found in registry")
return {"status": "error", "error": f"Tool '{tool_name}' not found"}

# Guardrail: require explicit user confirmation for high-risk tools.
# The SSEOutputHandler overrides this to block until the frontend
# responds; the default implementation auto-approves (CLI path).
if tool_name in TOOLS_REQUIRING_CONFIRMATION:
if not self.console.confirm_tool_execution(tool_name, tool_args):
return {
"status": "denied",
"error": f"Tool '{tool_name}' was denied by the user.",
}

tool = _TOOL_REGISTRY[tool_name]["function"]
sig = inspect.signature(tool)

Expand Down
8 changes: 8 additions & 0 deletions src/gaia/agents/base/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,14 @@ def print_header(self, text: str): # pylint: disable=unused-argument
"""Print header. Optional - default no-op."""
...

def confirm_tool_execution(
self,
tool_name: str, # pylint: disable=unused-argument
tool_args: Dict[str, Any], # pylint: disable=unused-argument
) -> bool:
"""Request user confirmation before executing a tool. Returns True to proceed."""
return True

def print_separator(self, length: int = 50): # pylint: disable=unused-argument
"""Print separator. Optional - default no-op."""
...
Expand Down
41 changes: 41 additions & 0 deletions src/gaia/apps/webui/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { useEffect, useRef, useCallback, useState } from 'react';
import { Edit3, Paperclip, Download, Send, Upload, MessageSquare, Square, ArrowDown, Lock, FileText, FolderSearch, CheckCircle2, X, Link } from 'lucide-react';
import { MessageBubble } from './MessageBubble';
import { useChatStore } from '../stores/chatStore';
import { useNotificationStore, ALWAYS_ALLOW_TOOLS_KEY } from '../stores/notificationStore';
import type { GaiaNotification } from '../types/agent';
import * as api from '../services/api';
import { log } from '../utils/logger';
import { getSessionHash } from '../utils/format';
Expand Down Expand Up @@ -129,6 +131,8 @@ export function ChatView({ sessionId }: ChatViewProps) {
systemStatus,
} = useChatStore();

const { addNotification } = useNotificationStore();

const session = sessions.find((s) => s.id === sessionId);
const [input, setInput] = useState('');
const [editingTitle, setEditingTitle] = useState(false);
Expand Down Expand Up @@ -598,6 +602,43 @@ export function ChatView({ sessionId }: ChatViewProps) {
}
},
onAgentEvent: (event) => {
// ── Tool confirmation popup ──────────────────────────────
if (event.type === 'tool_confirm') {
if (!event.confirm_id) {
console.error('[ChatView] tool_confirm event missing confirm_id, ignoring');
return;
}
const toolName = event.tool || '';
const alwaysAllowed: string[] = JSON.parse(
localStorage.getItem(ALWAYS_ALLOW_TOOLS_KEY) || '[]'
);
if (alwaysAllowed.includes(toolName)) {
// Auto-approve without showing the modal
api.confirmToolExecution(sessionId, event.confirm_id, 'allow', false).catch(
(err) => console.error('[ChatView] auto-confirm failed:', err)
);
return;
}
// Show the PermissionPrompt modal via notificationStore
const notification: GaiaNotification = {
id: event.confirm_id,
type: 'permission_request',
agentId: 'chat',
agentName: 'GAIA',
title: `Allow ${toolName}?`,
message: `The agent wants to execute: ${toolName}`,
timestamp: Date.now(),
read: false,
dismissed: false,
priority: 'high',
tool: toolName,
toolArgs: event.args as Record<string, unknown> | undefined,
timeoutSeconds: event.timeout_seconds ?? 60,
};
addNotification(notification);
return;
}

// Tool completion updates the last TOOL step (not just the last step,
// since thinking/status events may have been interleaved during execution)
if (event.type === 'tool_end') {
Expand Down
14 changes: 13 additions & 1 deletion src/gaia/apps/webui/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export interface StreamCallbacks {
/** Agent event types that represent activity rather than content. */
const AGENT_EVENT_TYPES = new Set([
'status', 'step', 'thinking', 'plan',
'tool_start', 'tool_end', 'tool_result', 'tool_args', 'agent_error',
'tool_start', 'tool_end', 'tool_result', 'tool_args', 'tool_confirm', 'agent_error',
]);

export function sendMessageStream(
Expand Down Expand Up @@ -277,6 +277,18 @@ export function sendMessageStream(
return controller;
}

// -- Tool Confirmation ---------------------------------------------------------

/** Resolve a pending tool execution confirmation (Allow or Deny). */
export async function confirmToolExecution(
sessionId: string,
confirmId: string,
action: 'allow' | 'deny',
remember: boolean,
): Promise<void> {
return apiFetch('POST', '/chat/confirm', { session_id: sessionId, confirm_id: confirmId, action, remember });
}

// -- Documents -----------------------------------------------------------------

export async function listDocuments(): Promise<{ documents: Document[]; total: number; total_size_bytes: number; total_chunks: number }> {
Expand Down
36 changes: 32 additions & 4 deletions src/gaia/apps/webui/src/stores/notificationStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@

import { create } from 'zustand';
import type { GaiaNotification, NotificationType } from '../types/agent';
import { confirmToolExecution } from '../services/api';
import { useChatStore } from './chatStore';

// ── Constants ────────────────────────────────────────────────────────────

/** Maximum notifications kept in the center to prevent unbounded growth. */
const MAX_NOTIFICATIONS = 500;

/** localStorage key for the "always allow" tool list. */
export const ALWAYS_ALLOW_TOOLS_KEY = 'gaia_always_allow_tools';

// ── State Interface ──────────────────────────────────────────────────────

interface NotificationState {
Expand Down Expand Up @@ -78,18 +83,41 @@ export const useNotificationStore = create<NotificationState>((set, get) => ({
setTypeFilter: (type) => set({ typeFilter: type }),

respondToPermission: async (id, action, remember) => {
const api = window.gaiaAPI;
if (api) {
const electronApi = window.gaiaAPI;
if (electronApi) {
// Electron path: route via IPC
try {
await api.notification.respondPermission(id, action, remember);
await electronApi.notification.respondPermission(id, action, remember);
} catch (err) {
console.error('[notificationStore] Failed to send permission response via IPC:', err);
// Don't update local state — the agent didn't receive the response.
// The permission prompt remains actionable so the user can retry.
return;
}
} else {
// Web path: route via HTTP to /api/chat/confirm
const sessionId = useChatStore.getState().currentSessionId;
if (sessionId) {
try {
await confirmToolExecution(sessionId, id, action, remember);
} catch (err) {
console.error('[notificationStore] Failed to send permission response via HTTP:', err);
return;
}
}
}
// Persist "always allow" preference in localStorage
if (action === 'allow' && remember) {
const notification = get().notifications.find((n) => n.id === id);
if (notification?.tool) {
const existing: string[] = JSON.parse(localStorage.getItem(ALWAYS_ALLOW_TOOLS_KEY) || '[]');
if (!existing.includes(notification.tool)) {
existing.push(notification.tool);
localStorage.setItem(ALWAYS_ALLOW_TOOLS_KEY, JSON.stringify(existing));
}
}
}
// Update local state only after IPC succeeds (or if no IPC is available)
// Update local state after response is delivered
set((state) => ({
notifications: state.notifications.map((n) =>
n.id === id
Expand Down
17 changes: 17 additions & 0 deletions src/gaia/apps/webui/src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,23 @@ textarea:focus-visible {
line-height: 1.3;
}

/* ── Beta Badge ─────────────────────────────────────────────────── */

.beta-badge {
display: inline-block;
font-size: 10px;
font-weight: 800;
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 1px;
padding: 2px 6px;
border-radius: 3px;
background: var(--accent-yellow);
color: #111;
vertical-align: middle;
line-height: 1.3;
}

/* ── Modal Base ──────────────────────────────────────────────────── */

.modal-overlay {
Expand Down
31 changes: 18 additions & 13 deletions src/gaia/apps/webui/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,19 +197,20 @@ export interface AgentStep {

/** Extended SSE event types for agent communication. */
export type StreamEventType =
| 'chunk' // Text content chunk
| 'done' // Stream complete
| 'error' // Error
| 'status' // Agent state change
| 'step' // Step progress
| 'thinking' // Agent reasoning
| 'plan' // Agent plan
| 'tool_start' // Tool execution started
| 'tool_end' // Tool execution completed
| 'tool_result' // Tool result summary
| 'tool_args' // Tool arguments detail
| 'answer' // Final answer from agent
| 'agent_error';// Agent-level error (non-fatal)
| 'chunk' // Text content chunk
| 'done' // Stream complete
| 'error' // Error
| 'status' // Agent state change
| 'step' // Step progress
| 'thinking' // Agent reasoning
| 'plan' // Agent plan
| 'tool_start' // Tool execution started
| 'tool_end' // Tool execution completed
| 'tool_result' // Tool result summary
| 'tool_args' // Tool arguments detail
| 'tool_confirm' // Tool requires user confirmation (blocking)
| 'answer' // Final answer from agent
| 'agent_error'; // Agent-level error (non-fatal)

export interface StreamEvent {
type: StreamEventType;
Expand Down Expand Up @@ -243,6 +244,10 @@ export interface StreamEvent {
duration_seconds?: number;
truncated?: boolean;
};
/** Confirmation ID (for tool_confirm events). */
confirm_id?: string;
/** Timeout in seconds (for tool_confirm events). */
timeout_seconds?: number;
/** Structured result data (for tool_result with search results, file lists, etc.). */
result_data?: {
type: string;
Expand Down
Loading
Loading