Skip to content

SingleSelectField clash between Settings and Form #1653

@jpwsutton

Description

@jpwsutton

Pre-requisities

  • I am using the newest version of the platform (agentstack version shows that CLI and platform are the same version, and there is no newer version available)

Describe the bug
If building an agent that has both a settings element containing a agentstack_sdk.a2a.extensions.ui.settings.SingleSelectField and then requests a form mid conversation that also includes a agentstack_sdk.a2a.extensions.ui.form.SingleSelectField, you get an import clash (Assuming this is all in the same file) resulting in pydantic validation errors for the agentstack_sdk.a2a.extensions.ui.form.SingleSelectField.

agentstack_sdk.a2a.extensions.ui.settings.OptionItem and agentstack_sdk.a2a.extensions.ui.form.OptionItem are similarly affected.

The equivalent types are similar enough that when defining them, mypy complains about things like id or value missing from the OptionItem. You can try and resolve this by adding the missing field in, but upon execution, an exception will be thrown when attempting to render the mid chat form.

A quick workaround is just to alias one of the pair on import like so:

from agentstack_sdk.a2a.extensions.ui.settings import (
    OptionItem as SettingsOptionItem,
    SingleSelectField as SettingsSingleSelectField,
)

However, I think longer term, it might be worth combining the Settings and Form classes into a single common form class as there is so much overlap between the two.

To Reproduce
Create a simple agent that uses a agentstack_sdk.a2a.extensions.ui.settings.SingleSelectField in it's settings, and then requests a form mid chat using agentstack_sdk.a2a.extensions.ui.form.SingleSelectField.

Expected behavior
Both the settings and the mid chat form should render correctly.

Logs / Screenshots / Code snippets
Example to re-produce:

from typing import Annotated

from a2a.types import Message
from a2a.utils.message import get_message_text
from agentstack_sdk.a2a.extensions.ui.form import (
    FormExtensionServer,
    FormExtensionSpec,
    FormRender,
    OptionItem,
    SingleSelectField,
)
from agentstack_sdk.a2a.extensions.ui.settings import (
    OptionItem,
    SettingsExtensionServer,
    SettingsExtensionSpec,
    SettingsRender,
    SingleSelectField,
)
from agentstack_sdk.server import Server
from pydantic import BaseModel

server = Server()



class ContactInfo(BaseModel):
    contact_method: str | None
   

vector_store_doctype_select = SingleSelectField(
    id="vector_store_doc_type",
    options=[
        OptionItem(value="sql_query", label="SQL Queries"),
        OptionItem(value="question", label="Questions"),
        OptionItem(value="both", label="Both"),
    ],
    default_value="sql_queries",
    label="Vector Store Doc Type"
)


@server.agent()
async def dynamic_form_agent(
    message: Message,
    form_request: Annotated[FormExtensionServer, FormExtensionSpec(params=None)],
    settings: Annotated[SettingsExtensionServer, SettingsExtensionSpec(
        params=SettingsRender(
            fields=[vector_store_doctype_select]
        )
    )]
):
    """Agent that requests forms dynamically during conversation"""

    user_input = get_message_text(message)

    # Check if user wants to provide contact information
    if "contact" in user_input.lower() or "reach" in user_input.lower():
        # Request contact form dynamically
        contact_info = await form_request.request_form(
            form=FormRender(
                title="Please provide your contact information",
                columns=2,
                fields=[
                    SingleSelectField(
                        id="contact_method",
                        label="Preferred Contact Method",
                        col_span=2,
                        required=False,
                        options=[
                            OptionItem(id="email", label="Email"),
                            OptionItem(id="phone", label="Phone"),
                            OptionItem(id="sms", label="SMS"),
                            OptionItem(id="none", label="Do not contact"),
                        ],
                        default_value="email",
                    )
                ],
                model=ContactInfo
            )
        )

        if contact_info is None:
            yield "No contact information received."
        else:
            yield f"Thank you! I'll contact you at {contact_info.email} or {contact_info.phone} regarding {contact_info.company}."
    else:
        yield "Hello! If you'd like me to contact you, just let me know and I'll ask for your details."


if __name__ == "__main__":
    server.run()

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingui

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions