mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
fix(tool-output): count logical CSV and TSV records (#6369)
This commit is contained in:
parent
b33e82e392
commit
a9b1675923
@ -2129,6 +2129,8 @@ request the binary capability retain the legacy JSON/base64 frame protocol.
|
||||
|
||||
### Context Engineering
|
||||
|
||||
**Table Tool-Output Synopses**: CSV/TSV row counts reflect logical records, so newlines inside quoted fields do not add rows. If parsing fails or exceeds the field limit, the synopsis reports an undetermined count. Outputs beyond the input-size limit still skip structured parsing.
|
||||
|
||||
**Isolated Sub-Agent Context**: Each sub-agent runs in its own isolated context. This means that the sub-agent will not be able to see the context of the main agent or other sub-agents. This is important to ensure that the sub-agent is able to focus on the task at hand and not be distracted by the context of the main agent or other sub-agents.
|
||||
|
||||
**Summarization**: Within a session, DeerFlow manages context aggressively — summarizing completed sub-tasks, offloading intermediate results to the filesystem, compressing what's no longer immediately relevant. This lets it stay sharp across long, multi-step tasks without blowing the context window.
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
### Middleware Chain
|
||||
|
||||
Table synopses count nonblank CSV/TSV logical records, excluding the header.
|
||||
Preserve the recognition sample and 5,000,000 UTF-8-byte guard. Count incrementally;
|
||||
on parsing or field-limit errors, report an undetermined count rather than a
|
||||
physical-line or sample total.
|
||||
|
||||
Compaction keeps state `SystemMessage`s; transient instructions use request
|
||||
wrappers, and fully rescued partitions skip compaction. If latest-user rescue
|
||||
empties an AI/Tool-only window, use `_build_summary_input_text(strategy="last")`;
|
||||
|
||||
@ -454,8 +454,14 @@ def _try_table(content: str, *, delimiter: str, kind: Literal["csv", "tsv"]) ->
|
||||
return None
|
||||
|
||||
columns = [cell.strip() or f"column_{idx + 1}" for idx, cell in enumerate(raw_header)]
|
||||
total_nonempty_lines = sum(1 for line in content.splitlines() if line.strip())
|
||||
data_rows = max(0, total_nonempty_lines - 1)
|
||||
# Count logical records from the full input, preserving quoted newlines.
|
||||
# Keep blank-row filtering and the process-wide CSV field limit unchanged.
|
||||
try:
|
||||
with io.StringIO(content, newline="") as stream:
|
||||
record_count = sum(1 for row in csv.reader(stream, delimiter=delimiter, strict=True) if any(cell.strip() for cell in row))
|
||||
row_count_text = str(max(0, record_count - 1))
|
||||
except csv.Error:
|
||||
row_count_text = "an undetermined number of"
|
||||
# Render the first data row as a key=value list so quoted cells that
|
||||
# contain the delimiter do not get rejoined into a comma-separated
|
||||
# string that misleads the model about column count.
|
||||
@ -468,7 +474,7 @@ def _try_table(content: str, *, delimiter: str, kind: Literal["csv", "tsv"]) ->
|
||||
return ToolOutputSynopsis(
|
||||
kind=kind,
|
||||
title=title,
|
||||
summary=[f"{label} table with {data_rows} data rows and {width} columns."],
|
||||
summary=[f"{label} table with {row_count_text} data rows and {width} columns."],
|
||||
structure=[
|
||||
f"columns: {', '.join(columns[:_TABLE_COLUMN_LIMIT])}",
|
||||
f"first data row: {' | '.join(first_data_pairs) or '(none)'}",
|
||||
|
||||
@ -9,7 +9,9 @@ per-tool overrides, edge cases, and both sync/async code paths.
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import csv
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
@ -627,6 +629,57 @@ class TestToolOutputSynopsis:
|
||||
# The re-joined comma-broken row is the failure mode we are guarding.
|
||||
assert "Ada,a fine, brilliant" not in first_row
|
||||
|
||||
@pytest.mark.parametrize("delimiter,kind", [(",", "csv"), ("\t", "tsv")])
|
||||
@pytest.mark.parametrize("newline", ["\n", "\r\n"])
|
||||
@pytest.mark.parametrize("count", [6, 60])
|
||||
def test_table_synopsis_counts_logical_records(self, delimiter, kind, newline, count):
|
||||
"""Count logical records even beyond the physical-line recognition sample."""
|
||||
buffer = io.StringIO(newline="")
|
||||
writer = csv.writer(buffer, delimiter=delimiter, lineterminator=newline)
|
||||
writer.writerow(["id", "description", "score"])
|
||||
for index in range(count):
|
||||
writer.writerow([index, f"first{delimiter}part{newline}second part", 90])
|
||||
content = buffer.getvalue()
|
||||
synopsis = build_tool_output_synopsis(content)
|
||||
assert synopsis.kind == kind
|
||||
assert synopsis.summary == [f"{kind.upper()} table with {count} data rows and 3 columns."]
|
||||
preview = _build_preview(content, tool_name="bash", virtual_path="/mnt/test/table", head_chars=100, tail_chars=100)
|
||||
assert synopsis.summary[0] in preview
|
||||
|
||||
@pytest.mark.parametrize("delimiter,kind", [(",", "csv"), ("\t", "tsv")])
|
||||
def test_table_synopsis_ignores_blank_records(self, delimiter, kind):
|
||||
"""Ignore blank records while preserving blank lines inside quoted fields."""
|
||||
buffer = io.StringIO(newline="")
|
||||
writer = csv.writer(buffer, delimiter=delimiter)
|
||||
writer.writerow(["id", "description"])
|
||||
for index in range(6):
|
||||
writer.writerow([index, "first\n\nlast"])
|
||||
writer.writerow([])
|
||||
writer.writerow([" ", ""])
|
||||
synopsis = build_tool_output_synopsis(buffer.getvalue())
|
||||
assert synopsis.kind == kind
|
||||
assert synopsis.summary == [f"{kind.upper()} table with 6 data rows and 2 columns."]
|
||||
|
||||
@pytest.mark.parametrize("delimiter,kind", [(",", "csv"), ("\t", "tsv")])
|
||||
@pytest.mark.parametrize("suffix", ['"unterminated', '"closed"invalid', "field_limit"])
|
||||
def test_table_synopsis_does_not_invent_count_after_parse_failure(self, delimiter, kind, suffix):
|
||||
"""Do not report an exact total when parsing fails beyond the sample."""
|
||||
content = f"id{delimiter}description\n" + "".join(f"{index}{delimiter}ok\n" for index in range(60))
|
||||
original_limit = csv.field_size_limit()
|
||||
if suffix == "field_limit":
|
||||
suffix = "x" * (original_limit + 1)
|
||||
synopsis = build_tool_output_synopsis(content + f"61{delimiter}{suffix}")
|
||||
assert synopsis.kind == kind
|
||||
assert synopsis.summary == [f"{kind.upper()} table with an undetermined number of data rows and 2 columns."]
|
||||
assert csv.field_size_limit() == original_limit
|
||||
|
||||
def test_table_synopsis_preserves_oversized_input_guard(self):
|
||||
"""Skip structured parsing when the input exceeds the byte budget."""
|
||||
content = "id,description\n" + "1,ok\n" * 6 + "x" * 5_000_000
|
||||
synopsis = build_tool_output_synopsis(content)
|
||||
assert synopsis.kind == "unknown"
|
||||
assert "Parsing skipped due to size limit" in synopsis.summary[0]
|
||||
|
||||
def test_review_9_tsv_detector_rejects_tab_indented_bash(self):
|
||||
# Tab-indented output (ls -l, tree, indented logs) used to be
|
||||
# accepted as TSV because _try_table only checked that the
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user