diff --git a/apps/website/content/docs/chat/components/chat-debug.mdx b/apps/website/content/docs/chat/components/chat-debug.mdx index ca357cbfd..879eb7d44 100644 --- a/apps/website/content/docs/chat/components/chat-debug.mdx +++ b/apps/website/content/docs/chat/components/chat-debug.mdx @@ -22,19 +22,25 @@ Before the first turn the Timeline tab shows its empty state: "No checkpoints ye ## How it is built -Three files carry the example: the graph that produces the checkpoints, the provider that points Angular at it, and the component that mounts the panel. Open the Code tab to read them in place. +The example is three files: the graph that produces the checkpoints, the provider that points Angular at it, and the component that mounts the panel. Open the Code tab to read them in place. ### A graph with several nodes per turn -The backend is deliberately multi-step, because one node per turn produces one checkpoint and very little to look at. `generate` answers with the model, `process` appends a synthetic message derived from the answer, and `summarize` asks the model for a one-sentence summary of the conversation so far. The system prompt read by `generate` frames the assistant as an aviation helper; the graph binds no tools, so every answer comes from the model alone. +The backend is deliberately multi-step, because one node per turn produces one checkpoint and very little to look at. `generate` answers with the model, `process` measures that answer and records the result, and `summarize` asks the model for a one-sentence summary of the conversation so far. The system prompt read by `generate` frames the assistant as an aviation helper; the graph binds no tools, so every answer comes from the model alone. Each node returns a partial state update, and each of those updates becomes a checkpoint on the thread. +### State the inspector can show + +`agent.state()` is the LangGraph values bag with `messages` projected out into the transcript, so a graph that carries nothing but its messages leaves the State tab printing an empty object. This graph widens `MessagesState` with the metrics `process` computes, which is what gives the second tab something to inspect. + + + ### Wiring the nodes into a linear pipeline -The nodes are registered on a `StateGraph` over `MessagesState` and chained: `generate` to `process` to `summarize` to `generate_title`, then to the end. `generate_title` is a background node that summarizes the first user message into a thread title; it returns an empty update, so it changes the message list not at all while still adding a step to the run. +The nodes are registered on a `StateGraph` over `DebugState` and chained: `generate` to `process` to `summarize` to `generate_title`, then to the end. `generate_title` is a background node that summarizes the first user message into a thread title; it returns an empty update, so it changes the message list not at all while still adding a step to the run. diff --git a/cockpit/chat/debug/angular/e2e/c-debug.spec.ts b/cockpit/chat/debug/angular/e2e/c-debug.spec.ts index 69662d4ed..857e624c0 100644 --- a/cockpit/chat/debug/angular/e2e/c-debug.spec.ts +++ b/cockpit/chat/debug/angular/e2e/c-debug.spec.ts @@ -88,12 +88,11 @@ test('c-debug: the State tab swaps in the live state inspector', async ({ page } // The tab owns the panel body — the timeline is torn down, not stacked. await expect(page.locator('chat-debug-checkpoint-card')).toHaveCount(0); // `agent.state()` is the LangGraph values bag with `messages` projected out - // into the transcript, so on this MessagesState graph the inspector renders - // an empty object today. Assert the shape the JsonPipe produces rather than - // that exact literal: the claim is that the inspector is mounted and bound - // to the agent, and a graph that carries state beyond its messages should - // widen this tab's coverage, not fail it. - await expect(stateTab.locator('chat-debug-state-inspector pre')).toHaveText( - /^\{[\s\S]*\}$/, - ); + // into the transcript. This graph's DebugState also carries `analysis`, the + // metrics the `process` node computes, so the inspector has real run state + // to print — that is what makes this tab worth opening. + const inspector = stateTab.locator('chat-debug-state-inspector pre'); + await expect(inspector).toContainText('analysis'); + await expect(inspector).toContainText('characters'); + await expect(inspector).toContainText('words'); }); diff --git a/cockpit/chat/debug/angular/e2e/fixtures/c-debug.json b/cockpit/chat/debug/angular/e2e/fixtures/c-debug.json index be0972183..33d8cd90e 100644 --- a/cockpit/chat/debug/angular/e2e/fixtures/c-debug.json +++ b/cockpit/chat/debug/angular/e2e/fixtures/c-debug.json @@ -2,30 +2,381 @@ "fixtures": [ { "match": { - "systemMessage": "Aviation Assistant", - "userMessage": "What is a jet bridge?" + "userMessage": "What is a jet bridge?", + "model": "gpt-5-mini", + "turnIndex": 0, + "hasToolResult": false }, "response": { - "content": "A jet bridge is the enclosed, movable walkway that connects an airport gate to an aircraft door, so passengers board without crossing the ramp." - } - }, - { - "match": { - "systemMessage": "brief one-sentence summary", - "userMessage": "What is a jet bridge?" + "content": "A jet bridge is an enclosed, movable connector that links an airport terminal gate directly to an aircraft door so passengers can board and disembark without going outdoors. It\u2019s sometimes called a jetway, airbridge, passenger boarding bridge (PBB) or boarding bridge.\n\nKey points\n- Construction: usually a telescoping, pivoting metal-and-glass tube mounted on wheels or a rotatable plaza so it can extend, retract, raise and lower to match different aircraft doors and heights.\n- Operation: a trained operator maneuvers the bridge from the terminal end or a control panel, aligns it with the aircraft door, and seals the interface with a flexible canopy or curtain.\n- Benefits: protects passengers from weather, improves security and accessibility (no stairs), speeds boarding and deplaning, and reduces ground handling complexity.\n- Variations: single bridges for most jets; dual or double bridges (one at upper deck, one at main deck) for very large aircraft like the A380; some gates lack jet bridges and use mobile stairs or buses to remote stands.\n- Limitations: not all airports or remote stands have them; very large or unusual door positions may require special bridges; improper docking can damage aircraft so precise operation and safety checks are required.\n\nIn short: a jet bridge is the enclosed walkway that provides a dry, secure, and accessible path between the terminal and an airplane." }, - "response": { - "content": "The traveler asked what a jet bridge is and received a short definition of the boarding walkway." + "recordedTimings": { + "ttftMs": 4731, + "interChunkDelaysMs": [ + 14, + 0, + 16, + 0, + 15, + 0, + 18, + 0, + 15, + 0, + 29, + 0, + 15, + 0, + 15, + 0, + 18, + 0, + 16, + 0, + 17, + 0, + 28, + 0, + 10, + 0, + 14, + 0, + 15, + 0, + 15, + 0, + 17, + 0, + 14, + 0, + 17, + 0, + 19, + 0, + 16, + 0, + 16, + 0, + 17, + 0, + 34, + 0, + 4, + 0, + 13, + 0, + 15, + 0, + 4, + 0, + 33, + 0, + 25, + 0, + 11, + 0, + 13, + 0, + 29, + 0, + 14, + 0, + 18, + 0, + 16, + 0, + 20, + 0, + 14, + 1, + 13, + 0, + 16, + 0, + 14, + 0, + 27, + 0, + 8, + 0, + 16, + 0, + 18, + 0, + 16, + 0, + 21, + 0, + 9, + 0, + 29, + 0, + 5, + 0, + 15, + 0, + 19, + 0, + 17, + 0, + 15, + 0, + 23, + 0, + 16, + 0, + 23, + 0, + 16, + 0, + 15, + 0, + 65, + 0, + 35, + 0, + 5, + 0, + 41, + 0, + 6, + 0, + 5, + 0, + 7, + 0, + 7, + 0, + 1, + 0, + 5, + 0, + 11, + 0, + 17, + 0, + 17, + 0, + 16, + 0, + 97, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 10, + 0, + 5, + 0, + 7, + 0, + 18, + 0, + 7, + 0, + 31, + 0, + 13, + 0, + 14, + 0, + 17, + 0, + 18, + 0, + 19, + 0, + 17, + 0, + 21, + 0, + 16, + 0, + 18, + 0, + 34, + 0, + 16, + 0, + 1, + 0, + 13, + 0, + 18, + 0, + 14, + 0, + 17, + 0, + 21, + 0, + 21, + 0, + 23, + 0, + 13, + 0, + 3, + 0, + 19, + 0, + 34, + 0, + 44, + 0, + 4, + 0, + 8, + 0, + 5, + 0, + 15, + 0, + 16, + 0, + 13, + 0, + 18, + 0, + 18, + 0, + 36, + 0, + 11, + 0, + 40, + 0, + 17, + 0, + 32, + 0, + 6, + 0, + 3, + 0, + 5, + 0, + 3, + 0, + 5, + 0, + 21, + 0, + 21, + 0, + 11, + 0, + 20, + 0, + 13, + 0, + 15, + 0, + 16, + 0, + 18, + 0, + 16, + 0, + 17, + 0, + 17, + 0, + 14, + 0, + 17, + 0, + 18, + 0, + 39, + 0, + 13 + ], + "totalDurationMs": 7131 + }, + "metadata": { + "systemHash": "b3d7aaf7" } }, { "match": { - "systemMessage": "In 3-5 words", - "userMessage": "What is a jet bridge?" + "userMessage": "What is a jet bridge?", + "model": "gpt-5-mini", + "turnIndex": 2, + "hasToolResult": false }, "response": { - "content": "Jet bridge basics" + "content": "The user asked what a jet bridge is, and the assistant explained it\u2019s an enclosed, movable connector between an airport terminal gate and an aircraft (covering construction, operation, benefits, variations, and limitations) and then posted a brief processing note with character and word counts." + }, + "recordedTimings": { + "ttftMs": 3377, + "interChunkDelaysMs": [ + 14, + 0, + 20, + 0, + 31, + 0, + 74, + 0, + 25, + 0, + 52, + 0, + 1, + 0, + 14, + 0, + 11, + 0, + 106, + 0, + 107, + 0, + 40, + 0, + 5, + 0, + 9, + 0, + 4, + 0, + 11, + 0, + 3, + 0, + 0, + 4, + 0, + 7, + 0, + 9, + 0, + 5, + 0, + 5, + 1, + 2, + 0, + 8, + 0, + 6, + 0, + 5, + 0, + 72, + 0, + 9, + 0 + ], + "totalDurationMs": 4037 + }, + "metadata": { + "systemHash": "05dfe622" } } ] -} +} \ No newline at end of file diff --git a/cockpit/chat/debug/python/src/graph.py b/cockpit/chat/debug/python/src/graph.py index 82e866945..abe4a3cb5 100644 --- a/cockpit/chat/debug/python/src/graph.py +++ b/cockpit/chat/debug/python/src/graph.py @@ -66,6 +66,21 @@ async def generate_title(state: MessagesState, config) -> dict: return {} +# region debug-state +class DebugState(MessagesState): + """MessagesState plus the metrics `process` computes. + + The devtools State tab pretty-prints whatever the graph keeps in state. + A graph that only carries `messages` has nothing to show there, because + the transcript is rendered as the conversation rather than as state. + """ + + analysis: dict + + +# endregion + + def build_debug_graph(): """ Constructs a multi-step graph with generate, process, and summarize @@ -74,21 +89,25 @@ def build_debug_graph(): llm = ChatOpenAI(model="gpt-5-mini", streaming=True) # region pipeline-nodes - async def generate(state: MessagesState) -> dict: + async def generate(state: DebugState) -> dict: system_prompt = (PROMPTS_DIR / "debug.md").read_text() messages = [SystemMessage(content=system_prompt)] + state["messages"] response = await llm.ainvoke(messages) return {"messages": [response]} - async def process(state: MessagesState) -> dict: + async def process(state: DebugState) -> dict: last = state["messages"][-1].content + analysis = { + "characters": len(last), + "words": last.count(" ") + 1, + } processed = AIMessage( - content=f"[Processing] Analyzed {len(last)} characters. " - f"Found {last.count(' ') + 1} words. Processing complete." + content=f"[Processing] Analyzed {analysis['characters']} characters. " + f"Found {analysis['words']} words. Processing complete." ) - return {"messages": [processed]} + return {"messages": [processed], "analysis": analysis} - async def summarize(state: MessagesState) -> dict: + async def summarize(state: DebugState) -> dict: messages = [ SystemMessage(content="Provide a brief one-sentence summary of the conversation so far.") ] + state["messages"] @@ -98,7 +117,7 @@ async def summarize(state: MessagesState) -> dict: # endregion # region graph-wiring - graph = StateGraph(MessagesState) + graph = StateGraph(DebugState) graph.add_node("generate", generate) graph.add_node("process", process) graph.add_node("summarize", summarize)