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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Reference implementations demonstrating core Agent Stack capabilities.
- [Form Agent](https://github.com/i-am-bee/agentstack/tree/main/agents/form) - Single-turn form interaction using Form Extension with multiple field types, customizable layouts, file uploads, validation, and structured output.
- [RAG Agent](https://github.com/i-am-bee/agentstack/tree/main/agents/rag) - Retrieval-Augmented Generation agent supporting 12+ file formats, dynamic vector stores, semantic search (VectorSearchTool), document summaries (FileReaderTool), intelligent tool selection, and citation tracking with document URLs.
- [OAuth Agent](https://github.com/i-am-bee/agentstack/blob/main/apps/agentstack-sdk-py/examples/oauth.py) - OAuth Extension demo with MCP integration, browser-based authorization, secure token management, and Stripe MCP server access.
- [Dynamic Form Request Agent](https://github.com/i-am-bee/agentstack/blob/main/apps/agentstack-sdk-py/examples/request_form_agent.py) - Multi-step form workflow showing both static and dynamic form generation, where the agent conditionally requests additional input mid-conversation.
- [Dynamic Form Request Agent](https://github.com/i-am-bee/agentstack/blob/main/apps/agentstack-sdk-py/examples/form_request_agent.py) - Multi-step form workflow showing both static and dynamic form generation, where the agent conditionally requests additional input mid-conversation.
- [Flight Search and Visualization Agent](https://github.com/jezekra1/agentstack-workshop) - Agent that queries the Kiwi.com MCP API for flight results, requests missing parameters through the Form Extension, and optionally generates PNG or HTML route visualizations using geospatial helpers. It uses RequirementAgent to orchestrate tool calls (data validation and visualization) and streams a final answer with any generated files and citations.

### Community Agents
Expand Down
29 changes: 19 additions & 10 deletions agents/form/src/form/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import os
from typing import Annotated

from pydantic import BaseModel

import a2a.server.agent_execution
import a2a.server.apps
Expand All @@ -20,17 +20,20 @@
from agentstack_sdk.server.context import RunContext

import agentstack_sdk.a2a.extensions
from agentstack_sdk.a2a.extensions.ui.form import (
from agentstack_sdk.a2a.extensions.common.form import (
DateField,
TextField,
FileField,
FileInfo,
CheckboxField,
MultiSelectField,
OptionItem,
FormExtensionServer,
FormExtensionSpec,
FormRender,
)
from agentstack_sdk.a2a.extensions.services.form import (
FormServiceExtensionServer,
FormServiceExtensionSpec,
)

agent_detail_extension_spec = agentstack_sdk.a2a.extensions.AgentDetailExtensionSpec(
params=agentstack_sdk.a2a.extensions.AgentDetail(
Expand Down Expand Up @@ -65,15 +68,21 @@
)

form_render = FormRender(
id="adventure_form",
title="Let’s go on an adventure",
columns=2,
fields=[location, date_from, date_to, notes, flexible, interests],
)
form_extension_spec = FormExtensionSpec(form_render)
form_extension_spec = FormServiceExtensionSpec.demand(initial_form=form_render)

server = Server()

class FormData(BaseModel):
location: str | None
date_from: str | None
date_to: str | None
notes: list[FileInfo] | None
flexible: bool | None
interests: list[str] | None

@server.agent(
name="Single-turn Form Agent",
Expand All @@ -97,17 +106,17 @@
],
)
async def agent(
input: Message,
_message: Message,
form: Annotated[
FormExtensionServer,
FormServiceExtensionServer,
form_extension_spec,
],
):
"""Example demonstrating a single-turn agent using a form to collect user input."""

form_data = form.parse_form_response(message=input)
form_data = form.parse_initial_form(model=FormData)

yield f"Hello {form_data.values['location'].value}"
yield f"Hello {form_data.location}"


def serve():
Expand Down
2 changes: 1 addition & 1 deletion agents/form/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 13 additions & 8 deletions apps/agentstack-cli/src/agentstack_cli/commands/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
EmbeddingFulfillment,
EmbeddingServiceExtensionClient,
EmbeddingServiceExtensionSpec,
FormRequestExtensionSpec,
FormServiceExtensionSpec,
LLMFulfillment,
LLMServiceExtensionClient,
LLMServiceExtensionSpec,
Expand All @@ -44,12 +46,11 @@
TrajectoryExtensionClient,
TrajectoryExtensionSpec,
)
from agentstack_sdk.a2a.extensions.ui.form import (
from agentstack_sdk.a2a.extensions.common.form import (
CheckboxField,
CheckboxFieldValue,
DateField,
DateFieldValue,
FormExtensionSpec,
FormFieldValue,
FormRender,
FormResponse,
Expand Down Expand Up @@ -319,7 +320,7 @@ async def _ask_form_questions(form_render: FormRender) -> FormResponse:
).execute_async()
form_values[field.id] = CheckboxFieldValue(value=answer)
console.print()
return FormResponse(id=form_render.id, values=form_values)
return FormResponse(values=form_values)


async def _run_agent(
Expand Down Expand Up @@ -384,7 +385,11 @@ async def _run_agent(
else {}
)
| (
{FormExtensionSpec.URI: typing.cast(FormResponse, input).model_dump(mode="json")}
{
FormServiceExtensionSpec.URI: {
"form_fulfillments": {"initial_form": typing.cast(FormResponse, input).model_dump(mode="json")}
}
}
if isinstance(input, FormResponse)
else {}
)
Expand Down Expand Up @@ -470,7 +475,7 @@ async def _run_agent(
raise ValueError("Agent requires input but no input handler provided")

if form_metadata := (
message.metadata.get(FormExtensionSpec.URI) if message and message.metadata else None
message.metadata.get(FormRequestExtensionSpec.URI) if message and message.metadata else None
):
stream = client.send_message(
Message(
Expand All @@ -480,7 +485,7 @@ async def _run_agent(
task_id=task_id,
context_id=context_token.context_id,
metadata={
FormExtensionSpec.URI: (
FormRequestExtensionSpec.URI: (
await _ask_form_questions(FormRender.model_validate(form_metadata))
).model_dump(mode="json")
},
Expand Down Expand Up @@ -803,9 +808,9 @@ async def run_agent(

initial_form_render = next(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We should probably refactor this and use the extension client and parse as service extension cc @jezekra1

(
FormRender.model_validate(ext.params)
FormRender.model_validate(ext.params["form_demands"]["initial_form"])
for ext in agent.capabilities.extensions or ()
if ext.uri == FormExtensionSpec.URI and ext.params
if ext.uri == FormServiceExtensionSpec.URI and ext.params
),
None,
)
Expand Down
44 changes: 44 additions & 0 deletions apps/agentstack-sdk-py/examples/form_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0
from typing import Annotated

from a2a.types import Message
from pydantic import BaseModel

from agentstack_sdk.a2a.extensions.common.form import FormRender, TextField
from agentstack_sdk.a2a.extensions.services.form import (
FormServiceExtensionServer,
FormServiceExtensionSpec,
)
from agentstack_sdk.server import Server

server = Server()


class FormData(BaseModel):
mood: str | None


@server.agent()
async def form_agent(
_message: Message,
form: Annotated[
FormServiceExtensionServer,
FormServiceExtensionSpec.demand(
initial_form=FormRender(
title="How are you?",
fields=[TextField(id="mood", label="Mood", type="text", col_span=1)],
)
),
],
):
"""Initial form agent"""
initial_form = form.parse_initial_form(model=FormData)
if initial_form is None:
yield "No form data received."
else:
yield f"Your mood is {initial_form.mood}"


if __name__ == "__main__":
server.run()
86 changes: 86 additions & 0 deletions apps/agentstack-sdk-py/examples/form_request_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0
from typing import Annotated

from a2a.types import Message
from pydantic import BaseModel

from agentstack_sdk.a2a.extensions.common.form import (
CheckboxField,
DateField,
FileField,
FileInfo,
FormRender,
MultiSelectField,
OptionItem,
SingleSelectField,
TextField,
)
from agentstack_sdk.a2a.extensions.ui.form_request import FormRequestExtensionServer, FormRequestExtensionSpec
from agentstack_sdk.server import Server

server = Server()


class KitchenSink(BaseModel):
text_field: str | None
date_field: str | None
file_field: list[FileInfo] | None
singleselect_field: str | None
multiselect_field: list[str] | None
checkbox_field: bool | None


@server.agent()
async def form_request_agent(
_message: Message,
form_request: Annotated[
FormRequestExtensionServer,
FormRequestExtensionSpec(),
],
):
"""Request form agent"""
user_info = await form_request.request_form(
form=FormRender(
title="Kitchen Sink Form",
columns=2,
fields=[
TextField(id="text_field", label="Text Field", col_span=1),
DateField(id="date_field", label="Date Field", col_span=1),
FileField(id="file_field", label="File Field", accept=["*/*"], col_span=2),
SingleSelectField(
id="singleselect_field",
label="Single-Select Field",
options=[
OptionItem(id="option1", label="Option 1"),
OptionItem(id="option2", label="Option 2"),
],
col_span=2,
),
MultiSelectField(
id="multiselect_field",
label="Multi-Select Field",
options=[
OptionItem(id="option1", label="Option 1"),
OptionItem(id="option2", label="Option 2"),
],
col_span=2,
),
CheckboxField(
id="checkbox_field",
label="Checkbox Field",
content="I agree to the terms and conditions.",
col_span=2,
),
],
),
model=KitchenSink,
)
if user_info is None:
yield "No user info received."
else:
yield user_info.model_dump_json()


if __name__ == "__main__":
server.run()
91 changes: 0 additions & 91 deletions apps/agentstack-sdk-py/examples/request_form_agent.py

This file was deleted.

Loading
Loading