Skip to content

Commit e651c41

Browse files
committed
feat: enhance tool input streaming and agent action handling for improved chat experience
1 parent a688895 commit e651c41

15 files changed

Lines changed: 1857 additions & 545 deletions

File tree

surfsense_backend/app/services/new_streaming_service.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -595,8 +595,17 @@ def format_tool_input_start(
595595
Format the start of tool input streaming.
596596
597597
Args:
598-
tool_call_id: The unique tool call identifier (synthetic, derived
599-
from LangGraph ``run_id`` so the frontend has a stable card id).
598+
tool_call_id: The unique tool call identifier. May be EITHER the
599+
synthetic ``call_<run_id>`` id derived from LangGraph
600+
``run_id`` (legacy / ``SURFSENSE_ENABLE_STREAM_PARITY_V2``
601+
OFF, or the unmatched-fallback path under parity_v2) OR
602+
the authoritative LangChain ``tool_call.id`` (parity_v2
603+
path: when the provider streams ``tool_call_chunks`` we
604+
register the ``index`` and reuse the lc-id as the card
605+
id so live ``tool-input-delta`` events can be routed
606+
without a downstream join). Either way, the same id is
607+
preserved across ``tool-input-start`` / ``-delta`` /
608+
``-available`` / ``tool-output-available`` for one call.
600609
tool_name: The name of the tool being called.
601610
langchain_tool_call_id: Optional authoritative LangChain
602611
``tool_call.id``. When set, surfaces as

surfsense_backend/app/tasks/chat/stream_new_chat.py

Lines changed: 201 additions & 55 deletions
Large diffs are not rendered by default.

surfsense_backend/tests/unit/tasks/chat/test_extract_chunk_parts.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,46 @@ def test_invalid_chunk_returns_empty_parts(self, chunk_value: Any) -> None:
183183
assert out["text"] == ""
184184
assert out["reasoning"] == ""
185185
assert out["tool_call_chunks"] == []
186+
187+
188+
class TestIdlessContinuationChunks:
189+
"""Per LangChain ``ToolCallChunk`` semantics, the FIRST chunk for a
190+
tool call carries id+name; later chunks for the same call have
191+
``id=None, name=None`` and only ``args`` + ``index``. Live tool-call
192+
argument streaming relies on those idless continuation chunks
193+
flowing through ``_extract_chunk_parts`` UNTOUCHED so the upstream
194+
chunk-emission loop can still route them by ``index``.
195+
"""
196+
197+
def test_idless_continuation_chunk_preserved_verbatim(self) -> None:
198+
chunk = _FakeChunk(
199+
tool_call_chunks=[
200+
{"id": None, "name": None, "args": '_path":"/x"}', "index": 0}
201+
]
202+
)
203+
out = _extract_chunk_parts(chunk)
204+
assert len(out["tool_call_chunks"]) == 1
205+
tcc = out["tool_call_chunks"][0]
206+
assert tcc.get("id") is None
207+
assert tcc.get("name") is None
208+
assert tcc.get("args") == '_path":"/x"}'
209+
assert tcc.get("index") == 0
210+
211+
def test_first_then_idless_sequence_preserves_index(self) -> None:
212+
"""Both chunks for the same call share an ``index`` key — the
213+
index-routing loop in ``stream_new_chat`` depends on it."""
214+
first = _FakeChunk(
215+
tool_call_chunks=[
216+
{"id": "lc-1", "name": "write_file", "args": '{"file', "index": 0}
217+
]
218+
)
219+
cont = _FakeChunk(
220+
tool_call_chunks=[
221+
{"id": None, "name": None, "args": '_path":"/x"}', "index": 0}
222+
]
223+
)
224+
out_first = _extract_chunk_parts(first)
225+
out_cont = _extract_chunk_parts(cont)
226+
assert out_first["tool_call_chunks"][0]["index"] == 0
227+
assert out_cont["tool_call_chunks"][0]["index"] == 0
228+
assert out_cont["tool_call_chunks"][0].get("id") is None

0 commit comments

Comments
 (0)