mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(skills): preserve Markdown table cells in data-analysis exports (#7163)
* fix(skills): preserve Markdown table cells in data-analysis exports * docs(skills): document Markdown cell escaping contract Clarify handling of Markdown exports and escaping rules. * docs: describe data-analysis Markdown export escaping Clarify data-analysis skill features and behavior in README. * test(skills): cover Markdown export headers and multiline cells Add a test for markdown export functionality in data analysis.
This commit is contained in:
parent
2c880a3133
commit
d288935865
@ -1530,6 +1530,8 @@ uses neither `main` nor `master` reports its current tree instead of a stale bra
|
||||
|
||||
Users can explicitly activate an enabled skill for a single turn by starting the request with `/skill-name`, for example `/data-analysis analyze uploads/foo.csv`. DeerFlow loads that skill's `SKILL.md` as hidden current-turn context while leaving the base prompt limited to skill metadata. Slash activation respects disabled skills, custom-agent skill whitelists, and existing channel commands such as `/new` and `/help`.
|
||||
|
||||
Data-analysis Markdown exports escape pipes and backslashes in column names and values, and render cell line breaks as `<br>` so multiline data stays within its table cell.
|
||||
|
||||
The **data-analysis** skill preserves each CSV file or Excel sheet when names collide across inputs. [Prepare its Python environment](skills/public/data-analysis/SKILL.md#environment-setup) explicitly with DuckDB and openpyxl; missing packages report setup instructions instead of installing themselves during analysis. Use the SQL names printed by `inspect` (such as `report`, `report_1`) for queries and summaries. Its cache follows ordered input basenames and contents, so renaming or reordering inputs rebuilds their table map instead of silently selecting a different source. Quoted original names also resolve without rewriting query text; apostrophes in upload paths and double quotes in column names are preserved through loading, inspection, queries, and summaries.
|
||||
|
||||
After an answer loads skills, its toolbar includes **Skills used**. Hover over or click the icon to see the skills and their sources; hovering a skill name underlines it. Select a skill to inspect its `SKILL.md` in the resizable side panel (a drawer on mobile). The view uses snapshots captured during successful configured read-tool loads or explicit slash activation, so later edits or removal of a skill do not rewrite its history. Repeated loads appear once per run, in first-load order. Range reads and bounded snapshots are labeled as partial; older conversations without captured evidence do not show this menu. Copy returns the captured Markdown, including YAML frontmatter. Package-relative links and images remain readable references rather than navigating away from the conversation. Skill loads performed through other tools, such as shell commands, are not inferred from answer text.
|
||||
|
||||
@ -23,3 +23,7 @@ Keep these views connection-local and preserve normal CTE shadowing.
|
||||
Prepare DuckDB and openpyxl explicitly in the execution environment. Missing
|
||||
packages produce setup instructions without launching installers; keep those
|
||||
import-failure regressions offline in the root skill tests.
|
||||
|
||||
Markdown exports use the same cell escaping for headers and values. Escape
|
||||
backslashes before pipes and normalize CRLF, CR, and LF to inline `<br>` breaks;
|
||||
raw newlines must not split one query-result row across Markdown table rows.
|
||||
|
||||
@ -337,6 +337,18 @@ def _format_table(columns: list[str], rows: list[tuple]) -> str:
|
||||
return result
|
||||
|
||||
|
||||
def _markdown_cell(value: object) -> str:
|
||||
"""Keep table delimiters and line breaks inside a single Markdown cell."""
|
||||
return (
|
||||
str(value)
|
||||
.replace("\\", "\\\\")
|
||||
.replace("|", "\\|")
|
||||
.replace("\r\n", "\n")
|
||||
.replace("\r", "\n")
|
||||
.replace("\n", "<br>")
|
||||
)
|
||||
|
||||
|
||||
def _export_results(columns: list[str], rows: list[tuple], output_file: str) -> str:
|
||||
"""Export query results to a file (CSV, JSON, or Markdown)."""
|
||||
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
||||
@ -369,13 +381,11 @@ def _export_results(columns: list[str], rows: list[tuple], output_file: str) ->
|
||||
elif ext == ".md":
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
# Header
|
||||
f.write("| " + " | ".join(columns) + " |\n")
|
||||
f.write("| " + " | ".join(_markdown_cell(c) for c in columns) + " |\n")
|
||||
f.write("| " + " | ".join("---" for _ in columns) + " |\n")
|
||||
# Rows
|
||||
for row in rows:
|
||||
f.write(
|
||||
"| " + " | ".join(str(v).replace("|", "\\|") for v in row) + " |\n"
|
||||
)
|
||||
f.write("| " + " | ".join(_markdown_cell(v) for v in row) + " |\n")
|
||||
else:
|
||||
msg = f"Unsupported output format: {ext}. Use .csv, .json, or .md"
|
||||
print(msg)
|
||||
|
||||
39
tests/skills/test_data_analysis_markdown_export.py
Normal file
39
tests/skills/test_data_analysis_markdown_export.py
Normal file
@ -0,0 +1,39 @@
|
||||
"""Markdown exports preserve the shape of real DuckDB query results."""
|
||||
|
||||
import runpy
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
duckdb = pytest.importorskip("duckdb")
|
||||
pytest.importorskip("openpyxl")
|
||||
SCRIPT = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "skills/public/data-analysis/scripts/analyze.py"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"header, value, expected_header, expected_value",
|
||||
[
|
||||
("left|right", "a|b", r"left\|right", r"a\|b"),
|
||||
("notes", "first\nsecond", "notes", "first<br>second"),
|
||||
("first\r\nsecond", "a\rb\r\nc", "first<br>second", "a<br>b<br>c"),
|
||||
("path", r"a\|b", "path", r"a\\\|b"),
|
||||
("数量", 12, "数量", "12"),
|
||||
],
|
||||
)
|
||||
def test_query_markdown_export_keeps_each_cell_on_one_line(
|
||||
tmp_path, header, value, expected_header, expected_value
|
||||
):
|
||||
analysis = runpy.run_path(str(SCRIPT))
|
||||
output = tmp_path / "result.md"
|
||||
with duckdb.connect() as con:
|
||||
con.execute('CREATE TABLE input ("' + header.replace('"', '""') + '" VARCHAR)')
|
||||
con.execute("INSERT INTO input VALUES (?)", [value])
|
||||
analysis["action_query"](con, "SELECT * FROM input", {}, str(output))
|
||||
assert output.read_text(encoding="utf-8").splitlines() == [
|
||||
f"| {expected_header} |",
|
||||
"| --- |",
|
||||
f"| {expected_value} |",
|
||||
]
|
||||
Loading…
x
Reference in New Issue
Block a user