Skip to content

Commit c2e6085

Browse files
tomkisedengilbert
authored andcommitted
chore: form extension rework (i-am-bee#1481)
Signed-off-by: Eden Gilbert <[email protected]>
1 parent 14134fb commit c2e6085

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+611
-396
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ Reference implementations demonstrating core Agent Stack capabilities.
102102
- [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.
103103
- [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.
104104
- [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.
105-
- [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.
105+
- [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.
106106
- [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.
107107

108108
### Community Agents

agents/form/src/form/agent.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import os
55
from typing import Annotated
6-
6+
from pydantic import BaseModel
77

88
import a2a.server.agent_execution
99
import a2a.server.apps
@@ -20,17 +20,20 @@
2020
from agentstack_sdk.server.context import RunContext
2121

2222
import agentstack_sdk.a2a.extensions
23-
from agentstack_sdk.a2a.extensions.ui.form import (
23+
from agentstack_sdk.a2a.extensions.common.form import (
2424
DateField,
2525
TextField,
2626
FileField,
27+
FileInfo,
2728
CheckboxField,
2829
MultiSelectField,
2930
OptionItem,
30-
FormExtensionServer,
31-
FormExtensionSpec,
3231
FormRender,
3332
)
33+
from agentstack_sdk.a2a.extensions.services.form import (
34+
FormServiceExtensionServer,
35+
FormServiceExtensionSpec,
36+
)
3437

3538
agent_detail_extension_spec = agentstack_sdk.a2a.extensions.AgentDetailExtensionSpec(
3639
params=agentstack_sdk.a2a.extensions.AgentDetail(
@@ -65,15 +68,21 @@
6568
)
6669

6770
form_render = FormRender(
68-
id="adventure_form",
6971
title="Let’s go on an adventure",
7072
columns=2,
7173
fields=[location, date_from, date_to, notes, flexible, interests],
7274
)
73-
form_extension_spec = FormExtensionSpec(form_render)
75+
form_extension_spec = FormServiceExtensionSpec.demand(initial_form=form_render)
7476

7577
server = Server()
7678

79+
class FormData(BaseModel):
80+
location: str | None
81+
date_from: str | None
82+
date_to: str | None
83+
notes: list[FileInfo] | None
84+
flexible: bool | None
85+
interests: list[str] | None
7786

7887
@server.agent(
7988
name="Single-turn Form Agent",
@@ -97,17 +106,17 @@
97106
],
98107
)
99108
async def agent(
100-
input: Message,
109+
_message: Message,
101110
form: Annotated[
102-
FormExtensionServer,
111+
FormServiceExtensionServer,
103112
form_extension_spec,
104113
],
105114
):
106115
"""Example demonstrating a single-turn agent using a form to collect user input."""
107116

108-
form_data = form.parse_form_response(message=input)
117+
form_data = form.parse_initial_form(model=FormData)
109118

110-
yield f"Hello {form_data.values['location'].value}"
119+
yield f"Hello {form_data.location}"
111120

112121

113122
def serve():

agents/form/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/agentstack-cli/src/agentstack_cli/commands/agent.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
EmbeddingFulfillment,
3737
EmbeddingServiceExtensionClient,
3838
EmbeddingServiceExtensionSpec,
39+
FormRequestExtensionSpec,
40+
FormServiceExtensionSpec,
3941
LLMFulfillment,
4042
LLMServiceExtensionClient,
4143
LLMServiceExtensionSpec,
@@ -44,12 +46,11 @@
4446
TrajectoryExtensionClient,
4547
TrajectoryExtensionSpec,
4648
)
47-
from agentstack_sdk.a2a.extensions.ui.form import (
49+
from agentstack_sdk.a2a.extensions.common.form import (
4850
CheckboxField,
4951
CheckboxFieldValue,
5052
DateField,
5153
DateFieldValue,
52-
FormExtensionSpec,
5354
FormFieldValue,
5455
FormRender,
5556
FormResponse,
@@ -319,7 +320,7 @@ async def _ask_form_questions(form_render: FormRender) -> FormResponse:
319320
).execute_async()
320321
form_values[field.id] = CheckboxFieldValue(value=answer)
321322
console.print()
322-
return FormResponse(id=form_render.id, values=form_values)
323+
return FormResponse(values=form_values)
323324

324325

325326
async def _run_agent(
@@ -384,7 +385,11 @@ async def _run_agent(
384385
else {}
385386
)
386387
| (
387-
{FormExtensionSpec.URI: typing.cast(FormResponse, input).model_dump(mode="json")}
388+
{
389+
FormServiceExtensionSpec.URI: {
390+
"form_fulfillments": {"initial_form": typing.cast(FormResponse, input).model_dump(mode="json")}
391+
}
392+
}
388393
if isinstance(input, FormResponse)
389394
else {}
390395
)
@@ -470,7 +475,7 @@ async def _run_agent(
470475
raise ValueError("Agent requires input but no input handler provided")
471476

472477
if form_metadata := (
473-
message.metadata.get(FormExtensionSpec.URI) if message and message.metadata else None
478+
message.metadata.get(FormRequestExtensionSpec.URI) if message and message.metadata else None
474479
):
475480
stream = client.send_message(
476481
Message(
@@ -480,7 +485,7 @@ async def _run_agent(
480485
task_id=task_id,
481486
context_id=context_token.context_id,
482487
metadata={
483-
FormExtensionSpec.URI: (
488+
FormRequestExtensionSpec.URI: (
484489
await _ask_form_questions(FormRender.model_validate(form_metadata))
485490
).model_dump(mode="json")
486491
},
@@ -803,9 +808,9 @@ async def run_agent(
803808

804809
initial_form_render = next(
805810
(
806-
FormRender.model_validate(ext.params)
811+
FormRender.model_validate(ext.params["form_demands"]["initial_form"])
807812
for ext in agent.capabilities.extensions or ()
808-
if ext.uri == FormExtensionSpec.URI and ext.params
813+
if ext.uri == FormServiceExtensionSpec.URI and ext.params
809814
),
810815
None,
811816
)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
from typing import Annotated
4+
5+
from a2a.types import Message
6+
from pydantic import BaseModel
7+
8+
from agentstack_sdk.a2a.extensions.common.form import FormRender, TextField
9+
from agentstack_sdk.a2a.extensions.services.form import (
10+
FormServiceExtensionServer,
11+
FormServiceExtensionSpec,
12+
)
13+
from agentstack_sdk.server import Server
14+
15+
server = Server()
16+
17+
18+
class FormData(BaseModel):
19+
mood: str | None
20+
21+
22+
@server.agent()
23+
async def form_agent(
24+
_message: Message,
25+
form: Annotated[
26+
FormServiceExtensionServer,
27+
FormServiceExtensionSpec.demand(
28+
initial_form=FormRender(
29+
title="How are you?",
30+
fields=[TextField(id="mood", label="Mood", type="text", col_span=1)],
31+
)
32+
),
33+
],
34+
):
35+
"""Initial form agent"""
36+
initial_form = form.parse_initial_form(model=FormData)
37+
if initial_form is None:
38+
yield "No form data received."
39+
else:
40+
yield f"Your mood is {initial_form.mood}"
41+
42+
43+
if __name__ == "__main__":
44+
server.run()
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
from typing import Annotated
4+
5+
from a2a.types import Message
6+
from pydantic import BaseModel
7+
8+
from agentstack_sdk.a2a.extensions.common.form import (
9+
CheckboxField,
10+
DateField,
11+
FileField,
12+
FileInfo,
13+
FormRender,
14+
MultiSelectField,
15+
OptionItem,
16+
SingleSelectField,
17+
TextField,
18+
)
19+
from agentstack_sdk.a2a.extensions.ui.form_request import FormRequestExtensionServer, FormRequestExtensionSpec
20+
from agentstack_sdk.server import Server
21+
22+
server = Server()
23+
24+
25+
class KitchenSink(BaseModel):
26+
text_field: str | None
27+
date_field: str | None
28+
file_field: list[FileInfo] | None
29+
singleselect_field: str | None
30+
multiselect_field: list[str] | None
31+
checkbox_field: bool | None
32+
33+
34+
@server.agent()
35+
async def form_request_agent(
36+
_message: Message,
37+
form_request: Annotated[
38+
FormRequestExtensionServer,
39+
FormRequestExtensionSpec(),
40+
],
41+
):
42+
"""Request form agent"""
43+
user_info = await form_request.request_form(
44+
form=FormRender(
45+
title="Kitchen Sink Form",
46+
columns=2,
47+
fields=[
48+
TextField(id="text_field", label="Text Field", col_span=1),
49+
DateField(id="date_field", label="Date Field", col_span=1),
50+
FileField(id="file_field", label="File Field", accept=["*/*"], col_span=2),
51+
SingleSelectField(
52+
id="singleselect_field",
53+
label="Single-Select Field",
54+
options=[
55+
OptionItem(id="option1", label="Option 1"),
56+
OptionItem(id="option2", label="Option 2"),
57+
],
58+
col_span=2,
59+
),
60+
MultiSelectField(
61+
id="multiselect_field",
62+
label="Multi-Select Field",
63+
options=[
64+
OptionItem(id="option1", label="Option 1"),
65+
OptionItem(id="option2", label="Option 2"),
66+
],
67+
col_span=2,
68+
),
69+
CheckboxField(
70+
id="checkbox_field",
71+
label="Checkbox Field",
72+
content="I agree to the terms and conditions.",
73+
col_span=2,
74+
),
75+
],
76+
),
77+
model=KitchenSink,
78+
)
79+
if user_info is None:
80+
yield "No user info received."
81+
else:
82+
yield user_info.model_dump_json()
83+
84+
85+
if __name__ == "__main__":
86+
server.run()

apps/agentstack-sdk-py/examples/request_form_agent.py

Lines changed: 0 additions & 91 deletions
This file was deleted.

0 commit comments

Comments
 (0)