Skip to content

Commit cdaeeea

Browse files
committed
feat(chat-graph): planner framing + itinerary context injection when client tools present
1 parent e23fb54 commit cdaeeea

2 files changed

Lines changed: 63 additions & 8 deletions

File tree

examples/chat/python/src/graph.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,16 @@ async def generate_title(state: "State", config: RunnableConfig) -> dict:
182182
"ask for a UI / form / card / interactive surface."
183183
)
184184

185+
_PLANNER_FRAMING = (
186+
"\n\n--- APP MODE: TRIP PLANNER ---\n"
187+
"You are a trip-planning assistant driving a live map cockpit. When the user "
188+
"asks for a plan, RECOMMEND concrete places (a short note each) grouped into "
189+
"days, and POPULATE the itinerary by calling `add_stop` for each recommendation "
190+
"(then `day_card` to recap a day). Revise with `move_stop`/`reorder_stop`/`clear_day`. "
191+
"Do NOT just describe the plan in prose — call the tools so the map and panel update. "
192+
"Only add stops that are not already present."
193+
)
194+
185195
# Reasoning-capable model prefixes. We only attach the ``reasoning``
186196
# parameter when the model name suggests reasoning support; setting it
187197
# on a non-reasoning model would be ignored anyway.
@@ -391,6 +401,25 @@ class State(TypedDict):
391401
itinerary: list[Stop]
392402

393403

404+
def build_system_prompt(gen_ui_mode: str, client_tools: list, itinerary: list) -> str:
405+
system = SYSTEM_PROMPT
406+
if gen_ui_mode == "a2ui":
407+
system = system + "\n\n--- A2UI v1 SCHEMA ---\n" + A2UI_V1_SCHEMA_PROMPT + (
408+
"\n\nWhen rendering UI in a2ui mode, emit envelopes in this order: "
409+
"surfaceUpdate FIRST, then beginRendering, then any dataModelUpdate "
410+
"entries. This lets the client mount the surface as early as possible."
411+
)
412+
if client_tools:
413+
system = system + _PLANNER_FRAMING
414+
if itinerary:
415+
import json as _json
416+
system = system + (
417+
"\n\nCURRENT ITINERARY (do not re-add existing stops):\n"
418+
+ _json.dumps(itinerary, ensure_ascii=False)
419+
)
420+
return system
421+
422+
394423
async def generate(state: State, config: RunnableConfig) -> dict:
395424
model_name = state.get("model") or "gpt-5-mini"
396425
kwargs = {"model": model_name, "streaming": True}
@@ -434,14 +463,14 @@ async def generate(state: State, config: RunnableConfig) -> dict:
434463
state,
435464
)
436465
# Append A2UI v1 schema to system prompt when in a2ui mode, so the parent
437-
# LLM knows how to construct the envelopes directly.
438-
system = SYSTEM_PROMPT
439-
if gen_ui_mode == "a2ui":
440-
system = SYSTEM_PROMPT + "\n\n--- A2UI v1 SCHEMA ---\n" + A2UI_V1_SCHEMA_PROMPT + (
441-
"\n\nWhen rendering UI in a2ui mode, emit envelopes in this order: "
442-
"surfaceUpdate FIRST, then beginRendering, then any dataModelUpdate "
443-
"entries. This lets the client mount the surface as early as possible."
444-
)
466+
# LLM knows how to construct the envelopes directly. When the frontend
467+
# client-tool catalog is present (App mode), also inject the trip-planner
468+
# framing + the current itinerary so the model populates state via the tools.
469+
system = build_system_prompt(
470+
gen_ui_mode=gen_ui_mode,
471+
client_tools=state.get("client_tools") or [],
472+
itinerary=state.get("itinerary") or [],
473+
)
445474
messages = [SystemMessage(content=system)] + state["messages"]
446475
# When in a2ui mode, attach the partial-envelope sideband handler so
447476
# the parent LLM's tool_call_chunks for render_a2ui_surface are
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import pytest
2+
from src.graph import build_system_prompt # helper introduced in Step 3
3+
4+
5+
@pytest.mark.smoke
6+
def test_planner_framing_only_when_client_tools_present():
7+
plain = build_system_prompt(gen_ui_mode="json-render", client_tools=[], itinerary=[])
8+
assert "trip-planning" not in plain.lower()
9+
10+
app = build_system_prompt(
11+
gen_ui_mode="json-render",
12+
client_tools=[{"name": "add_stop"}],
13+
itinerary=[],
14+
)
15+
assert "trip-planning" in app.lower()
16+
assert "add_stop" in app # instructs the model to populate state via the tool
17+
18+
19+
@pytest.mark.smoke
20+
def test_current_itinerary_is_injected_when_present():
21+
app = build_system_prompt(
22+
gen_ui_mode="json-render",
23+
client_tools=[{"name": "add_stop"}],
24+
itinerary=[{"id": "a", "day": 1, "place": "Louvre"}],
25+
)
26+
assert "Louvre" in app

0 commit comments

Comments
 (0)