Skip to content

UI tools#178

Draft
torchiaf wants to merge 4 commits intorancher:mainfrom
torchiaf:feature-ui-tools
Draft

UI tools#178
torchiaf wants to merge 4 commits intorancher:mainfrom
torchiaf:feature-ui-tools

Conversation

@torchiaf
Copy link
Copy Markdown
Member

@torchiaf torchiaf commented Mar 30, 2026

Related issue: rancher/rancher-ai-ui#190
Related issue: #179

UI Tools Feature

Overview

This PR introduces UI Tools, It enables the AI agent to dynamically select Ui tools declared by the UI. The agent can recommend appropriate UI tools based on the conversation context, and the LLM intelligently selects which tools to invoke with their respective parameters.

Key Features

  1. Kubernetes-Native Configuration: UI tools are defined as UIToolsConfig Custom Resource Definitions (CRDs) in Kubernetes
  2. Per-Config Tool Isolation: Tools are scoped to their configuration, preventing namespace collisions across multiple tool sets
  3. Configurable Tool Limits: Each configuration can specify a maximum number of unique tools the LLM can select per response

Architecture

Components

1. Registry (app/services/ui_tools/registry.py)

Central registry for managing UI tools with config-scoped namespacing.

Key Classes:

  • UITool: Represents a single UI tool with metadata and schema
  • UIToolSchema: Defines input validation schema for tools
  • UIToolCall: Represents a tool invocation by the agent
  • UIToolsConfig: Configuration for a set of UI tools
  • UIToolsRegistry: Manages tools in a config-scoped dictionary tools_by_config[config_name][tool_name]

2. Loader (app/services/ui_tools/loader.py)

Loads UI tool definitions from Kubernetes CRDs into the registry.

Key Functions:

  • reload_ui_tools_config(): Reload the tools from watcher updates.

3. Selector (app/services/ui_tools/selector.py)

Uses LLM to intelligently select appropriate UI tools for a given context.

Key Classes:

  • UIToolsSelector: Main selector class that invokes LLM for tool selection

Features:

  • Works with the LLM provider
  • Includes tool schema information in LLM prompt for accurate selection
  • Deduplicates tool calls
  • Enforces maxTools limit by unique tool name:
    • LLM can select up to maxTools different tools (by unique name)
    • Same tool can be called multiple times with different parameters
    • Example with maxTools=2: tool_A(params1), tool_A(params2), tool_B ✓ but tool_C

Configuration

Kubernetes CRD: UIToolsConfig

apiVersion: ai.cattle.io/v1alpha1
kind: UIToolsConfig
metadata:
  name: my-tools
spec:
  config:
    enabled: true                    # Enable/disable this tool set
    revision: 1                      # For change detection/gating
    systemPrompt: "..."              # Custom instructions for tool selection
    maxTools: 2                       # Max unique tools per response (0 = unlimited)
  
  tools:
    - name: chart_tool
      category: visualization
      description: "Renders data charts"
      enabled: true
      schema:
        type: object
        properties:
          chart_type:
            type: string
            description: "Type of chart (bar, line, pie)"
          data:
            type: object
            description: "Chart data"
        required: [chart_type, data]
      metadata:
        icon: "chart"
      revision: 1
    
    - name: table_tool
      category: data-display
      description: "Renders data in table format"
      enabled: true
      schema:
        type: object
        properties:
          columns:
            type: array
            description: "Column definitions"
          rows:
            type: array
            description: "Row data"
        required: [columns, rows]
      metadata:
        icon: "table"
      revision: 1

Configuration Fields

Config-Level Settings

Field Type Default Description
enabled boolean true Whether this tool set is active
revision integer 0 Revision number for change tracking
systemPrompt string null Custom instructions for tool selection LLM
maxTools integer 5 Maximum unique tools per response (0 = unlimited)

Tool-Level Settings

Field Type Required Description
name string Unique tool identifier
category string Tool classification (e.g., "visualization", "data-display")
description string What the tool does (UI usage)
prompt string System Prompt for the llm to select the tool
schema object JSON Schema for input validation
enabled boolean Enable/disable specific tool
metadata object Custom metadata (e.g., icon, display hints)
revision integer Tool definition version

Usage

1. Define Tools in Kubernetes

kubectl apply -f - <<EOF
apiVersion: ai.cattle.io/v1alpha1
kind: UIToolsConfig
metadata:
  name: dashboard
spec:
  config:
    enabled: true
    revision: 1
    maxTools: 2
    systemPrompt: "Select tools to visualize data effectively"
  
  tools:
    - name: bar_chart
      category: chart
      description: "Visualize data with a bar chart"
      enabled: true
      schema:
        type: object
        properties:
          title: { type: string }
          values: { type: array, items: { type: number } }
        required: [title, values]
EOF

2. Agent Uses Tools

The agent automatically:

  1. Loads available tools for the configuration
  2. Calls the LLM with tool descriptions
  3. LLM selects appropriate tools with parameters
  4. Agent validates inputs against schemas
  5. Sends tool calls to frontend for rendering

Implementation Details

Tool Selection Flow

┌─────────────────────┐
│  Agent Response     │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────────────────────┐
│  Load Tools from Registry           │
│  (config-scoped retrieval)          │
└──────────┬──────────────────────────┘
           │
           ▼
┌─────────────────────────────────────┐
│  Format Tools for LLM Prompt        │
│  (include schemas and descriptions) │
└──────────┬──────────────────────────┘
           │
           ▼
┌──────────────────────────────────────┐
│  LLM Selection                       │
│  - Analyze context                   │
│  - Select at most maxTools unique    │
│    tools (same tool allowed with     │
│    different params)                 │
└──────────┬───────────────────────────┘
           │
           ▼
┌──────────────────────────────────────┐
│  Extract Selections       │
│  - Parse JSON tool calls from LLM    │
│  - Validate inputs vs schema         │
│  - Deduplicate identical calls       │
│  - Enforce maxTools limit by         │
│    unique tool name                  │
└──────────┬───────────────────────────┘
           │
           ▼
┌──────────────────────────────────────┐
│  Return to Frontend for Rendering    │
└──────────────────────────────────────┘

Config-Scoped Namespacing

Problem Solved: When multiple UIToolsConfig resources exist, tools with the same name could collide.

Solution: Nested dictionary structure

registry.tools_by_config = {
  "config_a": {
    "chart_tool": UITool(...),
    "table_tool": UITool(...),
  },
  "config_b": {
    "chart_tool": UITool(...),  # Different from config_a's chart_tool
    "custom_tool": UITool(...),
  }
}

Revision-Gated Updates

Prevents race conditions when multiple clients update configurations:

if provided_revision > current_revision:
    # Update all revision-gated fields
    resource['spec']['config']['systemPrompt'] = provided_system_prompt
    resource['spec']['config']['enabled'] = provided_enabled
    resource['spec']['config']['maxTools'] = provided_max_tools
else:
    # Preserve current values
    resource['spec']['config']['systemPrompt'] = current_system_prompt
    resource['spec']['config']['enabled'] = current_enabled
    resource['spec']['config']['maxTools'] = current_max_tools

resource['spec']['config']['revision'] = provided_revision

Integration Points

1. Agent State (base.py)

  • Calls _dispatch_ui_tools_event() to select tools
  • Passes context and available tools to selector
  • Retrieves max_tools from registry config
  • Saves tool calls to LangGraph state (TODO)

2. WebSocket Handler (websocket.py)

  • Sends tool calls to connected clients in real-time
  • Clients render tools based on tool_name and input parameters

3. Memory Service (memory.py)

  • TODO

4. Webhook validation

  • TODO

Testing

TODO


Future Enhancements

  • Convert existing built-in tools events (mcp-response, confirmation, suggestions) into built-in UI tools for better integration.Those will not use LLM selection.
  • Tool dependency management (tool A requires tool B)
  • Dynamic tool plugin loading from external sources
  • Tool execution feedback loop to LLM
  • Tool invocation history and analytics

@torchiaf torchiaf requested a review from raulcabello March 30, 2026 07:40
@torchiaf torchiaf changed the title temp UI tools Mar 30, 2026
@torchiaf torchiaf force-pushed the feature-ui-tools branch 3 times, most recently from 9c75205 to 34a54c2 Compare March 31, 2026 10:03

# Fetch the resource from Kubernetes
try:
resource = api.get_namespaced_custom_object(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should check the user have permissions to fetch UITools

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe even consider modifying the CRD from the UI instead of adding this new endpoints? what do you think?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the logic for the CRD updates to the UI and removed the endpoints. Let's postpone the Webhook validation to a dedicate PR.

api = _init_k8s_client()

try:
current_resource = api.get_namespaced_custom_object(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here. We should check user can update UITools

detail="Missing required field: 'tools'",
)

tools = body.get("tools")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all these validation should be moved to a webhook. Otherwise, a user can create an invalid UITool with kubectl

"continue": "tools",
"summarize_conversation": "summarize_conversation",
"end": END,
"summarize_conversation": "ui_tools", # Route all endings to ui_tools
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

summarize_conversation won't always be called as it depends on the number of messages. We should change this to make sure ui_tools is always called.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed the workflow in parent and root nodes.
Now the ui-tools happens always before the summarize switch.

{
"continue": "agent",
"end": END,
"end": "ui_tools", # Route to ui_tools before summarize
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be called when human validation is rejected, so we should END

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

torchiaf added 2 commits April 2, 2026 18:51
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
@torchiaf torchiaf requested a review from raulcabello April 2, 2026 17:00
@torchiaf
Copy link
Copy Markdown
Member Author

torchiaf commented Apr 2, 2026

Thanks for the review @raulcabello .

  • I addressed reported issues, please take a look.
  • We removed the API ui-tools API -> when updating the UI tools the registry is no more reloaded. I created a watcher for the ui-tools-config for this reason. The alternative is to fetch the ui-tools from the cluster on each user request, for this reason the watcher is more efficent.

I still keep this in draft as the tests are TODO.

kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.17.1
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this generated with kubebuilder? Can you add the go code like we do for AIAgentConfig https://github.com/rancher/rancher-ai-agent/tree/main/crd-generation?

Copy link
Copy Markdown
Member Author

@torchiaf torchiaf Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it was generated manually. I just pushed the uitoolsconfig_types.go generator.


Args:
tool: The UITool to register
config_name: The config this tool belongs to (default: 'default')
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we want multiple configs?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allow us to specify multiple ui-tools configs - not required now, but in the future we want to define different tools and dynamically select them when sending the requests from the UI, basically it's a flexible approach.
It's also possible to define multiple tools for different UIs...

description: UIToolsConfigSpec defines the desired state of UITools
properties:
tools:
description: List of UI tools
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are the benefits of having a list instead of a single tool per UIToolConfig resource? Have you consider only having one tool per UIToolConfig resource? I beleive that would make it easier to manage all tools

Copy link
Copy Markdown
Member Author

@torchiaf torchiaf Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, my first approach was to have a list of UITools crd, this seems reasonable code-wise, but:

  • The UI needs to check if the ui tools definition has changes before initiating a new chat. When the definition has changes compared with the values in the cluster (this can happen when you install a new UI version), they need to be updated in atomic way.
  • Fetching only 1 resource it's more efficient.

4. Include all REQUIRED fields marked with (REQUIRED) in your input
5. Optional fields can be omitted if not needed
6. Output a valid JSON array at the very end with this format:
[
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if all models would be able to create a valid JSON array. We've seen in the past that small models like gpt-oss:20b fails to create a valid json for creating k8s resources.
Have you consider using something like MCP UI to prevent this problem? https://mcpui.dev/guide/introduction

else:
logging.debug(f"UI tool '{tool_name}' not found in available tools: {[t.name for t in available_tools]}")
except json.JSONDecodeError as e:
logging.debug(f"Failed to parse JSON array: {json_str[:100]}... Error: {e}")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would log an error if it is not a valid JSON. That would mean the UITools won't work

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, thanks

self.last_updated = datetime.now()
self._register_default_tools()

def _register_default_tools(self) -> None:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why empty? can we remove it ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo from previous implementation - removed

config=self.config,
)

def validate_tool_input(self, tool_name: str, input_data: Dict[str, Any], config_name: str = None) -> tuple[bool, Optional[str]]:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not used, can we remove it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo from previous implementation - removed


# Dispatch UI tools before the interrupt, so they're available to the client
if config is not None:
logging.info(f"Dispatching UI tools before confirmation interrupt")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we want to dispatch UI tools before confirming? We will dispatch UI tools at the end, so wouldn't this dispatch it twice?

Copy link
Copy Markdown
Member Author

@torchiaf torchiaf Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The confirmation process generates 2 different messages in the UI:

  • Confirmation ask: The agent sends a confirmation message - <confirmation-response> - to the UI; in this context we are dispatching the ui-tools -> the code you posted is executed.

    image
  • The user confirms/cancels the action: The agent performs the action and return a message in case of confirmation - There are no duplicates, the agent drops the ongoing LangGraph workflow after confirmation so the UI tools node is not executed. So we are not currently sending any ui-tools after confirmation/cancel, but we should. Notice that there are no ui tools elements in the second confirmation chunk:

    image

torchiaf added 2 commits April 8, 2026 10:51
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
Signed-off-by: Francesco Torchia <francesco.torchia@suse.com>
@torchiaf torchiaf requested a review from raulcabello April 8, 2026 09:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants