|
42 | 42 | from langchain_core.tools import tool |
43 | 43 | from langgraph_sdk import get_client |
44 | 44 |
|
| 45 | +from src.streaming.envelope_tool import render_a2ui_surface |
| 46 | +from src.streaming.envelope_normalizer import normalize_envelope_args |
| 47 | +from src.schemas.a2ui_v1 import A2UI_V1_SCHEMA_PROMPT |
| 48 | + |
45 | 49 |
|
46 | 50 | # Module-level singleton client; created lazily on first thread-title write. |
47 | 51 | _threads_client = None |
@@ -156,7 +160,7 @@ async def _maybe_write_thread_title(state: "State", config: RunnableConfig) -> N |
156 | 160 | "'render Y', 'show a Z', 'create a form for', 'make a card with' — " |
157 | 161 | "you MUST IMMEDIATELY dispatch the schema-generation tool bound " |
158 | 162 | "to the conversation. Exactly ONE such tool is bound per request: " |
159 | | - "either `generate_a2ui_schema` or `generate_json_render_spec`. " |
| 163 | + "either `render_a2ui_surface` or `generate_json_render_spec`. " |
160 | 164 | "Do NOT ask clarifying questions about platform, framework, fields, " |
161 | 165 | "validation, styling, or anything else. Do NOT describe the UI in " |
162 | 166 | "prose. Do NOT request more details from the user. The tool ITSELF " |
@@ -326,23 +330,6 @@ async def research(topic: str, subagent_type: str = "research") -> str: |
326 | 330 | A2UI_PREFIX = "---a2ui_JSON---" |
327 | 331 |
|
328 | 332 |
|
329 | | -@tool |
330 | | -async def generate_a2ui_schema(request: str) -> str: |
331 | | - """Dispatch the A2UI schema sub-agent to render a UI surface in A2UI |
332 | | - v1 wire format. Use this when the user asks for UI/forms/cards and |
333 | | - state.gen_ui_mode is 'a2ui'. Pass the user's request verbatim as the |
334 | | - `request` argument. The sub-agent returns a JSON array of v1 |
335 | | - envelopes (surfaceUpdate, optional dataModelUpdate, beginRendering) |
336 | | - that the post-process node wraps for the chat composition.""" |
337 | | - from src.schemas.a2ui_v1 import A2UI_V1_SCHEMA_PROMPT |
338 | | - llm = ChatOpenAI(model="gpt-5-mini", temperature=0) |
339 | | - response = await llm.ainvoke([ |
340 | | - SystemMessage(content=A2UI_V1_SCHEMA_PROMPT), |
341 | | - HumanMessage(content=request), |
342 | | - ]) |
343 | | - return _as_text(response.content).strip() |
344 | | - |
345 | | - |
346 | 333 | @tool |
347 | 334 | async def generate_json_render_spec(request: str) -> str: |
348 | 335 | """Dispatch the json-render schema sub-agent to render a UI surface |
@@ -403,13 +390,26 @@ async def generate(state: State, config: RunnableConfig) -> dict: |
403 | 390 | # side of the conditional resolves at execution time. |
404 | 391 | gen_ui_mode = state.get("gen_ui_mode") or "a2ui" |
405 | 392 | gen_ui_tool = ( |
406 | | - generate_a2ui_schema if gen_ui_mode == "a2ui" |
| 393 | + render_a2ui_surface if gen_ui_mode == "a2ui" |
407 | 394 | else generate_json_render_spec |
408 | 395 | ) |
409 | | - llm = ChatOpenAI(**kwargs).bind_tools([ |
410 | | - search_documents, request_approval, research, gen_ui_tool, |
411 | | - ]) |
412 | | - messages = [SystemMessage(content=SYSTEM_PROMPT)] + state["messages"] |
| 396 | + # Strict mode is enabled for the envelope-emission tool so OpenAI enforces |
| 397 | + # the canonical {envelopes: [...]} argument shape; the JS bridge and Python |
| 398 | + # normalizer treat the non-canonical shapes as safety nets. |
| 399 | + llm = ChatOpenAI(**kwargs).bind_tools( |
| 400 | + [search_documents, request_approval, research, gen_ui_tool], |
| 401 | + strict=True if gen_ui_mode == "a2ui" else False, |
| 402 | + ) |
| 403 | + # Append A2UI v1 schema to system prompt when in a2ui mode, so the parent |
| 404 | + # LLM knows how to construct the envelopes directly. |
| 405 | + system = SYSTEM_PROMPT |
| 406 | + if gen_ui_mode == "a2ui": |
| 407 | + system = SYSTEM_PROMPT + "\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 | + messages = [SystemMessage(content=system)] + state["messages"] |
413 | 413 | response = await llm.ainvoke(messages) |
414 | 414 | return {"messages": [response]} |
415 | 415 |
|
@@ -437,7 +437,7 @@ def after_tools(state: State) -> Literal["emit_generated_surface", "generate"]: |
437 | 437 | if isinstance(prior, AIMessage) and prior.tool_calls: |
438 | 438 | for tc in prior.tool_calls: |
439 | 439 | if tc.get("id") == m.tool_call_id and tc.get("name") in ( |
440 | | - "generate_a2ui_schema", "generate_json_render_spec", |
| 440 | + "render_a2ui_surface", "generate_json_render_spec", |
441 | 441 | ): |
442 | 442 | return "emit_generated_surface" |
443 | 443 | break |
@@ -480,7 +480,7 @@ async def emit_generated_surface(state: State) -> dict: |
480 | 480 | if not payload: |
481 | 481 | return {} |
482 | 482 |
|
483 | | - if tool_name == "generate_a2ui_schema": |
| 483 | + if tool_name == "render_a2ui_surface": |
484 | 484 | # Sub-LLM returns a JSON array of v1 envelopes. Convert to JSONL |
485 | 485 | # (one envelope per line) and prepend the classifier sentinel. |
486 | 486 | try: |
@@ -645,7 +645,7 @@ async def attach_citations(state: State) -> dict: |
645 | 645 | _builder.add_node("generate", generate) |
646 | 646 | _builder.add_node("tools", ToolNode([ |
647 | 647 | search_documents, request_approval, research, |
648 | | - generate_a2ui_schema, generate_json_render_spec, |
| 648 | + render_a2ui_surface, generate_json_render_spec, |
649 | 649 | ])) |
650 | 650 | _builder.add_node("emit_generated_surface", emit_generated_surface) |
651 | 651 | _builder.add_node("attach_citations", attach_citations) |
|
0 commit comments