Skip to content

Give each chat step fields the interface words in the reader's language, and let a query step carry its SQL, rows and failure - #957

Merged
WaylandYang merged 2 commits into
deeplethe:devfrom
Maya-Kid:fix/chat-step-trail-fields
Sep 26, 2026
Merged

WaylandYang merged 2 commits into
deeplethe:devfrom
Maya-Kid:fix/chat-step-trail-fields

Conversation

@Maya-Kid

Copy link
Copy Markdown
Contributor

Closes #942. Also does item (1) of #936, the query step. With #954 done, #936 stays open only for (2), retry.

Problem

  • The step trail speaks English in the Chinese interface #942. Each step in the chat trail is a finished English phrase written on the server: 6 sources, 3 of 10 linked, 40+ changes, failed, missing query. Chat.tsx shows label · detail as it comes. In the Chinese interface the trail stays in English, and moments appear as raw RFC 3339 stamps, e.g. 12 facts at 2024-08-01T00:00:00Z, as recorded before ….
  • A chat answer cannot be checked, retried or copied #936 (1). A query_data step shows only the source name and the purpose. The SQL behind the number cannot be seen, and a query that failed shows the same step as one that returned rows.

Change

Server

The three writers (api/tools.rs, api/tools_graph.rs, and check_call in api/chat.rs) now build every step with one small builder, tools::Step, instead of each assembling its own json!.

Every step keeps kind, label and detail unchanged, and gains these fields:

field meaning
status ok, failed, not_found or invalid. Successful steps carry ok too, so the client can tell a new step from a stored one.
count, total how many were listed, and how many there are when fewer were listed
more the list stopped at its limit and the total is unknown: 40+ changes, the 10-path cap, a query cut at 200 rows
valid_at, as_of, before the moments, RFC 3339. When before is given it is written instead of the as_of derived from it.
since, until the changes window as asked (YYYY-MM-DD). No until means the window runs to now.
hops the length of the shortest path
param, missing which argument was wrong, and whether it was missing rather than malformed
sql the SQL a query step ran

Where this differs from the fields agreed in #942, and why:

  • The world moment is valid_at, not at. Every stored step already has at: the offset in the answer where the step happened, in UTF-16 units. Replay uses it to thread the trail back into the text. The chat loop sets it after the tool returns, so a world-time at would be overwritten.
  • more, since/until, hops, param/missing and sql are additions. Without them the client would have to parse detail for "40+", for the window's "now", for the hop count and for the argument name.

Statuses:

  • failed: every read that failed. These tool results were already errors.
  • not_found: an unknown document, an entity the graph tools cannot resolve, an unknown data source, or an unknown tool.
    • entity_facts on a name that matches nothing used to write invalid id. Its status is now not_found, like neighbors, timeline and paths_between. Its detail is unchanged.
  • invalid: check_call refusals, and the tools' own argument checks (document_id, rule_id, since, and remember's text).

query_data:

  • run_query is split so that the chat tool gets the rows and the truncation flag directly. run_query keeps its signature and output, because the mapping preview uses it.
  • The step carries sql, count (rows) and more (cut at the 200-row cap).
  • A failed query is status: failed. A source that is not mounted is not_found.
  • label is still the source name and detail is still the purpose.
  • The text the model reads is unchanged.

Web

  • ChatStep gains the fields. kind gains document, which the server already sent.
  • New steps.ts: stepLabel and stepDetail word a step from its kind and fields in S.ask.step.
    • A step without status, i.e. one stored before this change, shows its detail as before.
    • The world moment (valid_at) shows as a calendar date read in UTC, as fmtTime reads valid_from. It shows a time of day only when one was given.
    • Record moments (as_of, before) show in the reader's time zone through localDateTime, as other recorded times do.
    • What belongs to the user or the model stays as written: the query, entity and source names, a query's purpose, a remembered sentence.
    • The changes window is worded from since and until. The stored label ends in an English "now".
  • Chat.tsx:
    • The trail and the Thinking line use stepLabel and stepDetail.
    • A query step has an "SQL" toggle that shows the statement.
    • A failed step shows in the danger colour.
  • Wording is added to en.ts and zh.ts. zh is typed typeof S, so a missing entry fails the build.

Docs

  • The status line of 0004.
  • The loop paragraph in chat-and-mcp.md.

Verification

Run in containers on a Linux host: Rust 1.98.1, Postgres 16 with pgvector, Node 20 with pnpm 10.2.1, and Playwright 1.58.2 with Chromium.

New and extended tests

  • mcp_tests.rs

    • each_step_carries_the_fields_the_interface_words dispatches each tool against the fixture and checks its step:

      • search, docs and find_entities;
      • document: ok, invalid and not_found;
      • entity_facts: world and record moments, before over as_of, no at, and an unknown name;
      • neighbors and timeline, including an unknown name;
      • paths_between: hops, and an unknown id;
      • changes: a closed window, an open window, and an invalid since;
      • list_rules, and rule_matches (invalid id, and a rule that marks nothing);
      • an unknown tool, and remember with only NUL.

      Where the fixture does not fix a count, the test ties it to the stored detail, which is unchanged.

    • a_query_step_carries_its_sql_rows_and_failure registers a postgres data source pointing at the test database, then runs:

      • SELECT 1 AS x: ok, 1 row, and the SQL back as written;
      • a query on a table that does not exist: failed;
      • an unknown source: not_found.

      It deletes the source afterwards.

    • a_list_cut_at_its_limit_says_so also checks the steps:

      • 40+ changes (more and the window);
      • 8 of 10 entities (total);
      • 10+ paths (more and hops);
      • the uncut counterparts of each.
    • failed_reads_do_not_become_successful_empty_results checks status: failed for every failed read.

    • rule_matches_keep_materialized_intervals_and_count_rows checks count.

  • chat.rs: a_refused_call_names_its_parameter_for_the_interface.

  • steps.test.ts (vitest, 29 cases):

    • every kind in English and Chinese;
    • failed, not_found and invalid;
    • data kept as written, and the changes window;
    • stored steps unchanged.

    The moments case pins TZ to America/New_York, where UTC and local readings differ, and checks first that the zone took effect.

  • chat-view.test.mjs (browser) replays a stored conversation with two query steps. It checks:

    • the row count;
    • the failed step's danger colour;
    • that "SQL" opens the statement.

On dev (6f74a08) with only the new tests

This run used dev with only the new server tests, the check_call test and the browser test added. The vitest file imports the new module, so it was left out.

  • All 6 server tests fail on the missing fields. For example, each_step_carries_the_fields_the_interface_words stops at its first step: `status` should be "ok" in {"detail":"1 sources","kind":"search","label":"orchard"}.
  • The new browser case fails waiting for · revenue by month · 12 rows: 23 tests, 21 pass, 2 fail (the case and its parent).

With this change

  • cargo fmt --all --check passes.
  • cargo clippy --workspace --all-targets -- -D warnings passes.
  • cargo test -p utopia-server api::: 181 passed.
  • cargo test --workspace with UTOPIA_TEST_REQUIRE_DB=1 and UTOPIA_TEST_REQUIRE_PDFTOTEXT=1: 1199 passed, 0 failed, 5 ignored (the existing #[ignore] tests).
  • cargo build --workspace passes.
  • pnpm install --frozen-lockfile passes.
  • pnpm test: 187 passed.
  • pnpm build passes, including style-guard and tsc.
  • The browser test: 23 of 23 pass.

Limits

  • Messages stored before this change keep their English detail. Rewording them by parsing it was not attempted; this was the second question in The step trail speaks English in the Chinese interface #942.
  • On a tool step the label is the tool's name, such as list_rules, rule_matches, or the tool a refused call named. It stays an identifier.
  • A world moment given as a year or a month (at: "2024") arrives as its first instant and shows as a date (2024-01-01), as detail did. The step does not carry the precision.
  • The result the model reads from query_data is unchanged. That includes a failed query not being marked as a tool error.

…ge, and let a query step carry its SQL, rows and failure

Signed-off-by: dada-yan <BinjunYann@gmail.com>

@WaylandYang WaylandYang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, this is a careful cut. Reviewed the builder, the three writers, the wording module and the tests; the decision to keep the English detail for stored messages and to write status on successful steps is right, and the note on why the world moment cannot be called at is exactly the kind of thing that saves the next reader. Landing it now; #942 closes with it and #936 stays open for retry only. Thanks again.

@WaylandYang WaylandYang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainer review: sound, landing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The step trail speaks English in the Chinese interface

2 participants