Skip to content

Show the chat run: show_process weaving and the ChatStream events view - #454

Merged
rejojer merged 4 commits into
mainfrom
feat/chat-process-display
Sep 1, 2026
Merged

Show the chat run: show_process weaving and the ChatStream events view#454
rejojer merged 4 commits into
mainfrom
feat/chat-process-display

Conversation

@rejojer

@rejojer rejojer commented Sep 1, 2026

Copy link
Copy Markdown
Member

What

chat(stream=True) now shows the run. It returns a ChatStream with two views over one run:

# Text view — ordinary iteration, process woven in by default
for piece in client.chat("What status?", doc_id=doc, stream=True):
    print(piece, end="")
# [thinking] I need to check the report first.
#
# [tool_call] get_document {"doc_name": "report.pdf"}
# [tool_result] get_document: {"success": true, ... (+304 chars)
#
# The project is on track: ...

# Typed view — machine-readable, full data, never clipped
for ev in client.chat("What status?", doc_id=doc, stream=True).events:
    ...  # {"type": "thinking"|"answer", "delta": ...}
         # {"type": "tool_call", "call_id", "name", "arguments"}
         # {"type": "tool_result", "call_id", "name", "output"}

API

  • show_process on chat()on by default ("on where available"). False = bare answer stream; a dict (pageindex.ChatProcessOptions) selects parts: thinking / tool_call / tool_result (bools, default on) and max_chars (line cap, default 200). Explicit True without stream=True raises.
  • ChatStream: iterating yields text (next() works; existing for piece in client.chat(..., stream=True) code is unchanged in shape), .events yields typed dicts. One run serves one view; close() kills the stream like a closed generator.
  • Non-stream chat() and the protocol surfaces (chat_completions / responses / messages) are untouched.

Behavior changes (release-notes material)

  1. Streamed chat() output now includes the process by default — pass show_process=False for the old bare answer stream (and when joining a stream back into conversation history).
  2. On managed-cloud clients, the stream weaves what the endpoint actually serves: tool-call lines parsed from its block_metadata chunk tags (that wire carries no thinking and no tool results). Old-wire chunks (no block_metadata) remain plain answer text. As a consequence, the bare answer view no longer leaks the tool-argument JSON the endpoint interleaves into delta.contentchat_completions()'s raw text mode is deliberately left as-is.

Engine

One typed-event primitive per lane (_chat_events_agen for the in-process agent, _cloud_chunk_events for managed chunks) with _weave as a pure renderer over it — both views, both lanes, one code path. The chat lane's prologue is shared via _chat_agent (pure refactor; chat_completions behavior unchanged).

Verification

  • 456 tests green — 19 new, red-verified TDD (weave shape, dict config incl. the falsy-{} trap, events sequence with parsed arguments and unclipped output, one-view claim, next() compat, closed-stream deadness, managed weave / clean-off / old-wire compat).
  • Review fixes (42e8bbd): a woven tool result pairs with its call by call_id — standalone with its arguments echoed when parallel calls separate them; empty deltas dropped at the event source; show_process validated before the managed request is sent.
  • Without-frameworks CI leg simulated (agents blocked): managed weaving and all validation paths run without openai-agents.
  • Pyright flat vs main (touched files net −2).

https://claude.ai/code/session_014GN8u3zdH3RpeftHZChavP

…nts view

chat(stream=True) now returns a ChatStream: iterating it yields the
answer text with the run woven in by default — "[thinking] " sections,
one "[tool] name arguments" line per call with its clipped result —
and .events yields the run as typed dicts (thinking/answer deltas,
tool_call with parsed arguments, tool_result with the full output).
One run serves one view; close() kills it like a closed generator.

- show_process: on by default ("on where available"); False for the
  bare answer stream; a dict (ChatProcessOptions: thinking, tool_calls,
  tool_results, max_chars) selects the parts. Explicit True without
  stream=True raises.
- Managed clients weave what the endpoint serves: tool-call lines
  parsed from its block_metadata chunk tags (that wire carries no
  thinking and no tool results); old-wire chunks stay plain answer
  text, and the bare answer view no longer leaks tool-argument JSON.
- Engine: one typed-event primitive (_chat_events_agen /
  _cloud_chunk_events) with _weave as a pure renderer over it; the
  chat lane's prologue is shared via _chat_agent, behavior unchanged
  on chat_completions/responses/messages.

453 tests green (16 new, red-verified), no-openai-agents leg simulated,
pyright flat vs main.

Claude-Session: https://claude.ai/code/session_014GN8u3zdH3RpeftHZChavP
Comment thread pageindex/local_chat.py
arguments = getattr(raw, "arguments", "") or ""
try:
arguments = json.loads(arguments)
except (ValueError, TypeError):
Comment thread pageindex/local_chat.py
arguments = "".join(pieces)
try:
arguments = json.loads(arguments)
except ValueError:
@rejojer

rejojer commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

…idate show_process before the managed request

Three review findings on the show_process weave, all red-verified:

- _weave nested every tool result under the most recent [tool] line,
  which misattributes results when a turn makes parallel calls (the SDK
  streams all calls, then all results). A result now nests only when the
  line above is its own call (by call_id); otherwise it stands alone
  with its call's clipped arguments echoed, so same-name parallel calls
  stay tellable apart. Hidden-call mode falls out unchanged (no stored
  arguments, no echo).

- Empty deltas now stop at the event source. chat_completions and the
  managed chunk lane both filter them; the new local lane did not, so a
  mid-stream "" (litellm forwards annotated/provider-field empties)
  leaked into the bare view and flipped _weave sections, splitting one
  thinking burst into repeated labels.

- The managed streaming lane sent the billed request before
  _process_options ran, so a config typo cost a real chat call. chat()
  now chokes on bad show_process before dispatching, matching the local
  lane's validate-first order.

Two docstring truths: close()'s "a run never consumed never starts"
holds only for own-model chat (the managed request is already on the
wire), and the module docstring now covers the managed chunk weave.

456 tests green; the three new ones red-verified; managed-lane paths
re-run with the agents package blocked; pyright adds nothing on touched
lines.

Claude-Session: https://claude.ai/code/session_01XeeD2214z6Vd6qKcAi9ZvJ
…am overloads

Four review fixes plus two coverage gaps, each red- or
mutation-verified:

- _cloud_chunk_events treated any non-tool_use tag inside an open tool
  block as answer text, so argument JSON leaked into the
  show_process=False answer — the one meant to be appended back as
  conversation history. Inside an open block nothing is answer:
  argument chunks now accumulate under any tag. And non-string
  argument pieces stringify at the join instead of killing the whole
  stream with a raw TypeError.

- _process_options sorted unknown keys before repr-ing them, so
  mixed-type keys ({1: True, "foo": 1}) raised a bare TypeError past
  the caller's `except PageIndexAPIError`; sorting the reprs keeps the
  single error type.

- chat() gains @overload on stream, so the docstring's own `.events`
  usage type-checks for py.typed consumers (previously pyright ruled
  `Cannot access attribute "events" for class "str"` on the exact
  documented snippet). pageindex/ error count unchanged (234).

- Coverage: the streaming lane's whole `finally` could be deleted with
  the suite still green — the new abandonment test pins the teardown
  (pump exits, turn 2 emits nothing, _aclose_backend closes the
  per-call client). And FakeModel emitted only the reasoning event
  production never sends (litellm folds reasoning into
  reasoning_content, which arrives as summary deltas); it now
  alternates variants, so dropping either from the isinstance tuple
  goes red.

459 tests green; managed-path tests re-run with the agents package
blocked; flake8 parity on every touched file.

Claude-Session: https://claude.ai/code/session_01DWBCCTDzwuVamBf5MvQ4eP
Comment thread pageindex/client.py
model: Optional[str] = None,
reasoning_effort: Optional[str] = None,
show_process: Union[bool, Mapping[str, Any], None] = None,
) -> str: ...
Comment thread pageindex/client.py
model: Optional[str] = None,
reasoning_effort: Optional[str] = None,
show_process: Union[bool, Mapping[str, Any], None] = None,
) -> "ChatStream": ...
Comment thread pageindex/client.py
reasoning_effort: Optional[str] = None,
) -> Union[str, Iterator[str]]:
show_process: Union[bool, Mapping[str, Any], None] = None,
) -> Union[str, "ChatStream"]: ...
…sy show_process message

ChatStream.events was a property whose getter latched the stream's one
view on mere attribute access: a debugger variable pane, hasattr, or
getattr(stream, "events", None) — which PageIndexAPIError escapes, as
getattr only swallows AttributeError — was enough to make a later
`for chunk in stream:` refuse, with nothing consumed. The getter now
returns a lazy generator: the managed refusal, the view claim and the
run start all happen on first consumption, so introspection is
side-effect free and the text view stays usable after a probe.

And the stream=False guard's message told falsy-but-not-False values
("show_process=0", "") that they passed show_process=True; the check
itself is the ruled falsy-{} trap and stands, but the message now
names the off values and echoes what was got.

461 tests green (2 red-verified new: inert read on both lanes, plus
the falsy-message case); changed managed-path tests re-run with the
agents package blocked; pyright pageindex/ 234 -> 234.

Claude-Session: https://claude.ai/code/session_01DWBCCTDzwuVamBf5MvQ4eP
@rejojer
rejojer merged commit 555c370 into main Sep 1, 2026
9 checks passed
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.

1 participant