Skip to content

Enable Dynamic Agent Configuration Through Admin UI ​

Context ​

The Swiss AI Hub platform supports multiple agent types (RAG agents, specialized assistants, etc.) that each have runtime configuration requirements. Previously, modifying agent behavior required:

  • Code Changes: Developers had to modify AgentConfig classes and redeploy services to change configuration values.
  • Technical Barrier: Non-technical users (administrators, business analysts) could not customize agent behavior to meet specific use cases without developer involvement.

While agents already had a configuration system through Pydantic-based AgentConfig classes, there was no way to expose these configurations to end users through the UI or to persist user-customized configurations separately from the default values.

Agent Blueprint vs Agent Profile ​

The platform now distinguishes between two concepts:

  • Agent Blueprint (Agent Class): The code-level definition containing workflow steps, form schema, event specifications, and default configuration. Discovered automatically when agents come online and stored in the agent_classes collection.
  • Agent Profile (Agent Instance): A user-created configuration of a blueprint. Has a unique ID, name, description, icon, and specific settings. Multiple profiles can be created from one blueprint. Stored in the agent_configs collection.

This separation enables self-service deployment: developers define blueprints, while administrators create and configure profiles through the UI without code changes.

Decision Drivers ​

  • User Self-Service: Enable administrators and business users to customize agent configurations without code changes or developer involvement.
  • Type Safety Preservation: Configuration changes should still be validated against the agent's expected schema, preventing invalid configurations.
  • Single Source of Truth: Form schema and data model should be defined once to avoid desynchronization.
  • Developer Experience: Minimal boilerplate for exposing configuration through the UI.

Decision ​

We will implement dynamic agent configuration through the Admin UI using the Form Duality Pattern:

1. Form Duality Pattern ​

A single Pydantic model serves two purposes through type unions:

  • Form Mode: Fields contain FormkitElement instances (e.g., InputText, InputNumber, Select) for UI rendering
  • Data Mode: Fields contain primitive values (e.g., str, float, bool) for validated configuration
python
class MyAgentConfig(AgentConfig):
    # Form duality: float for data mode, InputNumber for form mode
    temperature: Annotated[float | InputNumber, Field(description="LLM temperature"), Ge(0.0), Le(1.0)] = 0.7

    @classmethod
    def as_form(cls) -> "MyAgentConfig":
        """Create form-mode config with FormKit elements for UI rendering."""
        base = AgentConfig.as_form()
        return cls(
            agent_id=base.agent_id,
            name=base.name,
            description=base.description,
            icon=base.icon,
            temperature=InputNumber(label=LocaleString(en="Temperature"), min=0.0, max=1.0),
        )

2. FormKit Element Aliases ​

FormKit elements use Pydantic aliases to handle MongoDB's $ prefix restriction:

  • formkit stored in DB → $formkit in JSON response
  • condition_if stored in DB → if in JSON response (for conditional visibility)
  • ref stored in DB → id in JSON response

This enables storing form schemas in MongoDB while generating valid FormKit schemas for the frontend.

3. Non-Configurable vs Configurable Fields ​

Fields can be marked as non-configurable (deployment-specific) by omitting the FormKit element alternative in the type union:

python
class MyAgentConfig(AgentConfig):
    # Configurable (appears in form) - has FormKit alternative
    model_name: str | InputText = "gpt-4"

    # Non-configurable (set at deployment) - no FormKit alternative
    channel_config: TeamsConfig

Non-configurable values are merged with user-submitted configuration at runtime.

4. Form-Safe Constraints ​

Custom constraint validators (Ge, Le, Pattern, MinLen, MaxLen) that skip validation when the field contains a FormkitElement, enabling Pydantic validation to work with the duality pattern.

5. Agent Discovery and Registration ​

When agents register via AgentRunner, the form-mode config is used:

python
runner = AgentRunner(
    agent_type=MyAgent,
    agent_config=MyAgentConfig.as_form(),  # Form mode for discovery
)

The discovery response includes:

  • Form schema (extracted via to_formkit_form())
  • JSON schema for validation (via AgentConfigSpecs.from_model())
  • Start/stop event specifications
  • Workflow network graph

6. Configuration Injection ​

At runtime, the AgentDispatcher fetches the configuration for the specific agent profile via RPC, merges it with non-configurable defaults, validates it against the Pydantic model, and injects it into step methods via type annotation:

python
@step()
async def my_step(self, event: MyEvent, agent_config: MyAgentConfig):
    model = agent_config.model_name  # Validated configuration value

7. Two-Collection Persistence ​

  • agent_classes (AgentClassEntity): One document per agent type, updated by discovery. Contains form schema, event specs, workflow graph, online status.
  • agent_configs (AgentConfigEntityDocument): Many documents per agent type. Contains instance-specific configuration values, name, description, icon.

8. REST API Endpoints ​

Agent Classes (blueprints):

  • GET /agents/classes - List all agent classes
  • GET /agents/classes/{agent_class} - Get specific class with form schema

Agent Instances (profiles):

  • GET /agents/classes/{agent_class}/instances - List instances of a class
  • POST /agents/classes/{agent_class}/instances - Create new instance
  • GET /agents/classes/{agent_class}/instances/{agent_id} - Get instance
  • PUT /agents/classes/{agent_class}/instances/{agent_id} - Update instance configuration
  • DELETE /agents/classes/{agent_class}/instances/{agent_id} - Delete instance

Cross-class:

  • GET /agents/instances - List all instances across all classes

Consequences ​

Positive Outcomes ​

  • Self-Service Configuration: Administrators can create and customize agent profiles (prompts, RAG parameters, model selection) directly through the UI without developer involvement.
  • Single Source of Truth: Form schema and data model are defined in one place, preventing desynchronization.
  • Type-Safe End-to-End: Pydantic validates both form submission and runtime configuration access.
  • Conditional Field Visibility: FormKit's condition_if (aliased from if) enables dynamic form sections based on other field values.
  • Multi-Language Support: LocaleInput element enables multi-language names and descriptions.
  • Nested Configuration: Forms can contain nested Group elements and Repeater arrays for complex configuration structures.
  • Dynamic Model Selection: ModelSelect element populates from the LiteLLM model registry at runtime.

Trade-offs and Considerations ​

  • Form Definition Required: Agents must implement as_form() class method and use type unions to expose configuration in the UI. This provides full control but requires understanding the duality pattern.
  • Schema Evolution: When agent developers change AgentConfig fields, existing persisted configurations may use stale schemas until agents re-register.
  • Discovery Dependency: Form schemas are captured at agent discovery time; if an agent never comes online after deployment, its configuration form won't be available.
  • Constraint Validators: Standard Pydantic constraints (ge=, le=) don't work with duality pattern; developers must use SDK-provided constraints (Ge(), Le()).

Built with ❤️ in Switzerland 🇨🇭