Skip to content

Commit d65d2b5

Browse files
authored
Misc. cleanup - use a .widget file for metro map; handle selection; add tool call descriptions (#34)
* Add metro-map node select state * use .widget file for line select widget * Add tool call param descriptions
1 parent 41c6939 commit d65d2b5

File tree

13 files changed

+504
-105
lines changed

13 files changed

+504
-105
lines changed

examples/cat-lounge/backend/app/cat_agent.py

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,11 @@ async def _add_hidden_context(ctx: RunContextWrapper[CatAgentContext], content:
115115
)
116116

117117

118-
@function_tool(description_override="Read the cat's current stats before deciding what to do next.")
118+
@function_tool(
119+
description_override=(
120+
"Read the cat's current stats before deciding what to do next. No parameters."
121+
)
122+
)
119123
async def get_cat_status(
120124
ctx: RunContextWrapper[CatAgentContext],
121125
) -> dict[str, Any]:
@@ -125,7 +129,12 @@ async def get_cat_status(
125129
return state.to_payload(ctx.context.thread.id)
126130

127131

128-
@function_tool(description_override="Feed the cat to replenish energy and keep moods stable.")
132+
@function_tool(
133+
description_override=(
134+
"Feed the cat to replenish energy and keep moods stable.\n"
135+
"- `meal`: Meal or snack description to include in the update."
136+
)
137+
)
129138
async def feed_cat(
130139
ctx: RunContextWrapper[CatAgentContext],
131140
meal: str | None = None,
@@ -138,7 +147,12 @@ async def feed_cat(
138147
# No need to return payload for a client tool call; agent must be configured to stop after this tool call.
139148

140149

141-
@function_tool(description_override="Play with the cat to boost happiness and create fun moments.")
150+
@function_tool(
151+
description_override=(
152+
"Play with the cat to boost happiness and create fun moments.\n"
153+
"- `activity`: Toy or activity used during playtime."
154+
)
155+
)
142156
async def play_with_cat(
143157
ctx: RunContextWrapper[CatAgentContext],
144158
activity: str | None = None,
@@ -151,7 +165,12 @@ async def play_with_cat(
151165
# No need to return payload for a client tool call; agent must be configured to stop after this tool call.
152166

153167

154-
@function_tool(description_override="Clean the cat to tidy up and improve cleanliness.")
168+
@function_tool(
169+
description_override=(
170+
"Clean the cat to tidy up and improve cleanliness.\n"
171+
"- `method`: Cleaning method or item used."
172+
)
173+
)
155174
async def clean_cat(
156175
ctx: RunContextWrapper[CatAgentContext],
157176
method: str | None = None,
@@ -165,7 +184,10 @@ async def clean_cat(
165184

166185

167186
@function_tool(
168-
description_override="Give the cat a permanent name and update the thread title to match."
187+
description_override=(
188+
"Give the cat a permanent name and update the thread title to match.\n"
189+
"- `name`: Desired name for the cat."
190+
)
169191
)
170192
async def set_cat_name(
171193
ctx: RunContextWrapper[CatAgentContext],
@@ -206,7 +228,13 @@ async def set_cat_name(
206228
# No need to return payload for a client tool call; agent must be configured to stop after this tool call.
207229

208230

209-
@function_tool(description_override="Show the cat's profile card with avatar and age.")
231+
@function_tool(
232+
description_override=(
233+
"Show the cat's profile card with avatar and age.\n"
234+
"- `age`: Cat age (in years) to display and persist.\n"
235+
"- `favorite_toy`: Favorite toy label to include."
236+
)
237+
)
210238
async def show_cat_profile(
211239
ctx: RunContextWrapper[CatAgentContext],
212240
age: int | None = None,
@@ -250,13 +278,17 @@ def mutate(state: CatState) -> None:
250278
)
251279

252280

253-
@function_tool(description_override="Speak as the cat so a bubble appears in the dashboard.")
281+
@function_tool(
282+
description_override=(
283+
"Speak as the cat so a bubble appears in the dashboard.\n"
284+
"- `line`: The text the cat should say."
285+
)
286+
)
254287
async def speak_as_cat(
255288
ctx: RunContextWrapper[CatAgentContext],
256289
line: str,
257-
mood: str | None = None,
258290
):
259-
print(f"[TOOL CALL] speak_as_cat({line}, {str(mood)})")
291+
print(f"[TOOL CALL] speak_as_cat({line})")
260292
message = line.strip()
261293
if not message:
262294
raise ValueError("A line is required for the cat to speak.")
@@ -266,14 +298,14 @@ async def speak_as_cat(
266298
arguments={
267299
"state": state.to_payload(ctx.context.thread.id),
268300
"message": message,
269-
"mood": mood or "playful",
270301
},
271302
)
272303

273304

274305
@function_tool(
275306
description_override=(
276-
"Render up to three creative cat name options provided in the `suggestions` argument."
307+
"Render up to three creative cat name options provided in the `suggestions` argument.\n"
308+
"- `suggestions`: List of name suggestions with a `name` and `reason` for each."
277309
)
278310
)
279311
async def suggest_cat_names(

examples/metro-map/backend/app/agents/metro_map_agent.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,12 @@ class StationDetailResult(BaseModel):
9696
lines: list[Line]
9797

9898

99-
@function_tool(description_override="Show a clickable widget listing metro lines.")
99+
@function_tool(
100+
description_override=(
101+
"Show a clickable widget listing metro lines.\n"
102+
"- `message`: Text shown above the widget to prompt the user."
103+
)
104+
)
100105
async def show_line_selector(ctx: RunContextWrapper[MetroAgentContext], message: str):
101106
widget = build_line_select_widget(ctx.context.metro.list_lines())
102107
await ctx.context.stream(
@@ -112,27 +117,39 @@ async def show_line_selector(ctx: RunContextWrapper[MetroAgentContext], message:
112117
await ctx.context.stream_widget(widget)
113118

114119

115-
@function_tool(description_override="Load the latest metro map with lines and stations.")
120+
@function_tool(
121+
description_override=("Load the latest metro map with lines and stations. No parameters.")
122+
)
116123
async def get_map(ctx: RunContextWrapper[MetroAgentContext]) -> MapResult:
117124
print("[TOOL CALL] get_map")
118125
metro_map = ctx.context.metro.get_map()
119126
await ctx.context.stream(ProgressUpdateEvent(text="Retrieving the latest metro map..."))
120127
return MapResult(map=metro_map)
121128

122129

123-
@function_tool(description_override="List all metro lines with their colors and endpoints.")
130+
@function_tool(
131+
description_override=("List all metro lines with their colors and endpoints. No parameters.")
132+
)
124133
async def list_lines(ctx: RunContextWrapper[MetroAgentContext]) -> LineListResult:
125134
print("[TOOL CALL] list_lines")
126135
return LineListResult(lines=ctx.context.metro.list_lines())
127136

128137

129-
@function_tool(description_override="List all stations and which lines serve them.")
138+
@function_tool(
139+
description_override=("List all stations and which lines serve them. No parameters.")
140+
)
130141
async def list_stations(ctx: RunContextWrapper[MetroAgentContext]) -> StationListResult:
131142
print("[TOOL CALL] list_stations")
132143
return StationListResult(stations=ctx.context.metro.list_stations())
133144

134145

135-
@function_tool(description_override="Show the user the planned route.")
146+
@function_tool(
147+
description_override=(
148+
"Show the user the planned route.\n"
149+
"- `route`: Ordered list of stations in the journey.\n"
150+
"- `message`: One-sentence description of the itinerary."
151+
)
152+
)
136153
async def plan_route(
137154
ctx: RunContextWrapper[MetroAgentContext],
138155
route: list[Station],
@@ -167,7 +184,12 @@ async def plan_route(
167184
)
168185

169186

170-
@function_tool(description_override="Look up a single station and the lines serving it.")
187+
@function_tool(
188+
description_override=(
189+
"Look up a single station and the lines serving it.\n"
190+
"- `station_id`: Station identifier to retrieve."
191+
)
192+
)
171193
async def get_station(
172194
ctx: RunContextWrapper[MetroAgentContext],
173195
station_id: str,
@@ -251,6 +273,7 @@ async def add_station(
251273
tool_use_behavior=StopAtTools(
252274
stop_at_tool_names=[
253275
add_station.name,
276+
plan_route.name,
254277
show_line_selector.name,
255278
]
256279
),

examples/metro-map/backend/app/data/metro_map_store.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from __future__ import annotations
22

33
import json
4-
from dataclasses import dataclass
54
import re
5+
from dataclasses import dataclass
66
from pathlib import Path
77
from typing import Literal
88

examples/metro-map/backend/app/server.py

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,7 @@
3535
from .memory_store import MemoryStore
3636
from .request_context import RequestContext
3737
from .thread_item_converter import MetroMapThreadItemConverter
38-
from .widgets.line_select_widget import (
39-
LINE_SELECT_ACTION_TYPE,
40-
LineSelectPayload,
41-
build_line_select_widget,
42-
)
38+
from .widgets.line_select_widget import build_line_select_widget
4339

4440

4541
class MetroMapServer(ChatKitServer[RequestContext]):
@@ -108,11 +104,12 @@ async def action(
108104
sender: WidgetItem | None,
109105
context: RequestContext,
110106
) -> AsyncIterator[ThreadStreamEvent]:
111-
if action.type == LINE_SELECT_ACTION_TYPE:
112-
payload = self._parse_line_select_payload(action)
113-
if payload is None:
107+
if action.type == "line.select":
108+
if action.payload is None:
114109
return
115-
async for event in self._handle_line_select_action(thread, payload, sender, context):
110+
async for event in self._handle_line_select_action(
111+
thread, action.payload, sender, context
112+
):
116113
yield event
117114
return
118115

@@ -122,24 +119,19 @@ async def to_message_content(self, _input: Attachment) -> ResponseInputContentPa
122119
raise RuntimeError("File attachments are not supported in this demo.")
123120

124121
# -- Helpers ----------------------------------------------------
125-
def _parse_line_select_payload(self, action: Action[str, Any]) -> LineSelectPayload | None:
126-
try:
127-
return LineSelectPayload.model_validate(action.payload or {})
128-
except ValidationError as exc:
129-
print(f"[WARN] Invalid line.select payload: {exc}")
130-
return None
131-
132122
async def _handle_line_select_action(
133123
self,
134124
thread: ThreadMetadata,
135-
payload: LineSelectPayload,
125+
payload: dict[str, Any],
136126
sender: WidgetItem | None,
137127
context: RequestContext,
138128
) -> AsyncIterator[ThreadStreamEvent]:
129+
line_id = payload["id"]
130+
139131
# Update the widget to show the selected line and disable further clicks.
140132
updated_widget = build_line_select_widget(
141133
self.metro_map_store.list_lines(),
142-
selected=payload.id,
134+
selected=line_id,
143135
)
144136

145137
if sender:
@@ -155,7 +147,7 @@ async def _handle_line_select_action(
155147
id=self.store.generate_item_id("message", thread, context),
156148
thread_id=thread.id,
157149
created_at=datetime.now(),
158-
content=f"<LINE_SELECTED>{payload.id}</LINE_SELECTED>",
150+
content=f"<LINE_SELECTED>{line_id}</LINE_SELECTED>",
159151
),
160152
context=context,
161153
)
@@ -178,7 +170,7 @@ async def _handle_line_select_action(
178170
id=self.store.generate_item_id("tool_call", thread, context),
179171
thread_id=thread.id,
180172
name="location_select_mode",
181-
arguments={"lineId": payload.id},
173+
arguments={"lineId": line_id},
182174
created_at=datetime.now(),
183175
call_id=self.store.generate_item_id("tool_call", thread, context),
184176
),

0 commit comments

Comments
 (0)