Skip to content

Commit 7430945

Browse files
authored
feat(ReadFiles): simplify content output for single file reads (#33)
Improve ReadFiles executor to return simplified content when reading a single file instead of YAML-wrapped structure. Changes: - Single file: Return file content directly (string) - Multiple files: Keep existing YAML structure with files array - All other outputs unchanged (files, total_files, etc.) Benefits: - More intuitive for common single-file use case - Easier to consume in workflow templates - Backward compatible for multiple file scenarios Updated test snapshots to reflect new behavior.
2 parents 9d58b7f + 84dd805 commit 7430945

14 files changed

Lines changed: 60 additions & 107 deletions

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,10 @@ Read files with glob patterns, multiple output modes, and automatic outline extr
327327
- Gitignore integration and file filtering
328328
- Size limits and file count limits
329329
- Multi-file reading in single block
330-
- YAML-formatted output
330+
- Smart output format:
331+
- Single file: Direct content (string)
332+
- Multiple files: YAML-formatted structure
333+
- No files: Empty string
331334

332335
**See examples:** `tests/workflows/core/file-operations/readfiles-test.yaml`
333336

src/workflows_mcp/engine/executors_file.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,11 +302,22 @@ class ReadFilesOutput(BlockOutput):
302302
@computed_field # type: ignore[prop-decorator]
303303
@property
304304
def content(self) -> str:
305-
"""YAML-formatted output (backward compatible).
305+
"""Content output - simplified for single file, YAML for multiple files.
306306
307-
Returns structured YAML with literal block scalars for file content.
307+
No files: Returns empty string.
308+
Single file: Returns the file content directly (string).
309+
Multiple files: Returns YAML-formatted output with file list.
308310
Single source of truth: files list.
309311
"""
312+
# No files: return empty string
313+
if not self.files:
314+
return ""
315+
316+
# Single file: return content directly
317+
if len(self.files) == 1:
318+
return self.files[0].content
319+
320+
# Multiple files: use YAML format
310321
return self._format_as_yaml()
311322

312323
def _format_as_yaml(self) -> str:

tests/snapshots/editfile-interpolation-test.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"outputs": {
33
"all_operations_succeeded": true,
44
"delete_lines_count": 2,
5-
"final_content": "files:\n- path: test.py\n content: |\n # Test completed successfully: true\n # Lines added: 1\n # Modified by /tmp/editfile-interpolation-test\n # Interpolated header\n def test():\n value = \"OPERATIONS_INTERPOLATED\"\n logger.info(\"Hello, World!\")\n # Extra line 1\n # Extra line 2\n size_bytes: 235",
5+
"final_content": "# Test completed successfully: true\n# Lines added: 1\n# Modified by test_multiple_operations_interpolation\n# Interpolated header\ndef test():\n value = \"OPERATIONS_INTERPOLATED\"\n logger.info(\"Hello, World!\")\n# Extra line 1\n# Extra line 2",
66
"insert_lines_count": 1,
77
"operations_field_interpolation_success": true,
88
"replace_text_lines_modified": 1

tests/snapshots/editfile-operations-test.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"outputs": {
33
"delete_lines_success": true,
44
"dry_run_success": true,
5-
"final_content": "files:\n- path: test.py\n content: |\n \"\"\"Test module for EditFile operations.\"\"\"\n def hello():\n logger.info(\"Hello, Python!\")\n return 0\n\n def goodbye():\n size_bytes: 119",
5+
"final_content": "\"\"\"Test module for EditFile operations.\"\"\"\ndef hello():\n logger.info(\"Hello, Python!\")\n return 0\n\ndef goodbye():",
66
"insert_lines_success": true,
77
"lines_added": 1,
88
"lines_removed": 2,

tests/snapshots/file-operations-create-read.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"outputs": {
3-
"cleanup_done": true,
43
"executable_file_created": true,
54
"file_content": "# Testing file OPS\n\nGenerated: TIMESTAMP\nShell Output: data_from_shell\nUTF8 Content: \u4f60\u597d\u4e16\u754c\n\n## Features\n- Feature 1\n- Feature 2\n- Feature 3",
65
"file_created": true,

tests/snapshots/filters-chaining.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{
22
"outputs": {
33
"all_tests_passed": true,
4-
"cleanup_done": true,
54
"test_complex_chain_output": "Complex chain: 'hello_world' (length: 11)\nSUCCESS: trim -> lower -> replace -> length chain worked",
65
"test_number_chain_output": "Number chain: '42' (length: 2)\nSUCCESS: abs -> string -> length chain worked",
76
"test_output_chain_output": "SUCCESS: outputs -> field access -> filter chain worked\nFile size: 12 bytes, created: true",

tests/snapshots/readfiles-test.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@
1313
"size_bytes": 464
1414
}
1515
],
16-
"md_outline_content": "files:\n- path: README.md\n content: |-\n --- README.md (11 lines) ---\n \u251c\u2500\u2500 Test Project [1]\n \u251c\u2500\u2500 Overview [3]\n \u251c\u2500\u2500 Features [6]\n \u251c\u2500\u2500 Usage [10]\n size_bytes: 134",
16+
"md_outline_content": "--- README.md (11 lines) ---\n \u251c\u2500\u2500 Test Project [1]\n \u251c\u2500\u2500 Overview [3]\n \u251c\u2500\u2500 Features [6]\n \u251c\u2500\u2500 Usage [10]",
1717
"multi_full_content": "files:\n- path: README.md\n content: |\n # Test Project\n\n ## Overview\n This is a test project for ReadFiles.\n\n ### Features\n - Feature 1\n - Feature 2\n\n ## Usage\n See examples below.\n size_bytes: 134\n- path: example.py\n content: |\n \"\"\"Example Python module for testing.\"\"\"\n\n def calculate_sum(a: int, b: int) -> int:\n '''Add two numbers together.'''\n return a + b\n\n def calculate_product(a: int, b: int) -> int:\n '''Multiply two numbers.'''\n return a * b\n\n class Calculator:\n '''A simple calculator class.'''\n\n def divide(self, a: float, b: float) -> float:\n '''Divide a by b.'''\n if b == 0:\n raise ValueError(\"Cannot divide by zero\")\n return a / b\n size_bytes: 464\n",
1818
"multi_full_count": 2,
1919
"multi_full_size": 0,
20-
"py_outline_content": "files:\n- path: example.py\n content: |-\n --- example.py (18 lines) ---\n \u251c\u2500\u2500 def calculate_sum(a: int, b: int) -> int [3-5]\n \u251c\u2500\u2500 def calculate_product(a: int, b: int) -> int [7-9]\n \u251c\u2500\u2500 class Calculator [11-18]\n \u2502 \u251c\u2500\u2500 def divide(self, a: float, b: float) -> float [14-18]\n size_bytes: 464",
20+
"py_outline_content": "--- example.py (18 lines) ---\n \u251c\u2500\u2500 def calculate_sum(a: int, b: int) -> int [3-5]\n \u251c\u2500\u2500 def calculate_product(a: int, b: int) -> int [7-9]\n \u251c\u2500\u2500 class Calculator [11-18]\n \u2502 \u251c\u2500\u2500 def divide(self, a: float, b: float) -> float [14-18]",
2121
"py_outline_count": 1,
22-
"py_summary_content": "files:\n- path: example.py\n content: |-\n --- example.py (18 lines) ---\n \u251c\u2500\u2500 def calculate_sum(a: int, b: int) -> int [3-5]\n \u2502 \u2514\u2500\u2500 \"Add two numbers together....\"\n \u251c\u2500\u2500 def calculate_product(a: int, b: int) -> int [7-9]\n \u2502 \u2514\u2500\u2500 \"Multiply two numbers....\"\n \u251c\u2500\u2500 class Calculator [11-18]\n \u2502 \u2514\u2500\u2500 \"A simple calculator class....\"\n \u2502 \u251c\u2500\u2500 def divide(self, a: float, b: float) -> float [14-18]\n \u2502 \u2502 \u2514\u2500\u2500 \"Divide a by b....\"\n size_bytes: 464",
23-
"single_full_content": "files:\n- path: example.py\n content: |\n \"\"\"Example Python module for testing.\"\"\"\n\n def calculate_sum(a: int, b: int) -> int:\n '''Add two numbers together.'''\n return a + b\n\n def calculate_product(a: int, b: int) -> int:\n '''Multiply two numbers.'''\n return a * b\n\n class Calculator:\n '''A simple calculator class.'''\n\n def divide(self, a: float, b: float) -> float:\n '''Divide a by b.'''\n if b == 0:\n raise ValueError(\"Cannot divide by zero\")\n return a / b\n size_bytes: 464"
22+
"py_summary_content": "--- example.py (18 lines) ---\n \u251c\u2500\u2500 def calculate_sum(a: int, b: int) -> int [3-5]\n \u2502 \u2514\u2500\u2500 \"Add two numbers together....\"\n \u251c\u2500\u2500 def calculate_product(a: int, b: int) -> int [7-9]\n \u2502 \u2514\u2500\u2500 \"Multiply two numbers....\"\n \u251c\u2500\u2500 class Calculator [11-18]\n \u2502 \u2514\u2500\u2500 \"A simple calculator class....\"\n \u2502 \u251c\u2500\u2500 def divide(self, a: float, b: float) -> float [14-18]\n \u2502 \u2502 \u2514\u2500\u2500 \"Divide a by b....\"",
23+
"single_full_content": "\"\"\"Example Python module for testing.\"\"\"\n\ndef calculate_sum(a: int, b: int) -> int:\n '''Add two numbers together.'''\n return a + b\n\ndef calculate_product(a: int, b: int) -> int:\n '''Multiply two numbers.'''\n return a * b\n\nclass Calculator:\n '''A simple calculator class.'''\n\n def divide(self, a: float, b: float) -> float:\n '''Divide a by b.'''\n if b == 0:\n raise ValueError(\"Cannot divide by zero\")\n return a / b"
2424
},
2525
"status": "success"
2626
}

tests/workflows/core/file-operations/create-read.yaml

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@ name: file-operations-create-read
22
description: Test CreateFile with Jinja2 templates and variables
33
tags: [test, core, files, create, read]
44
inputs:
5-
path:
6-
type: str
7-
description: "Temporary workspace"
8-
default: "/tmp/test-create-read"
9-
105
features:
116
description: Feature list
127
type: list
@@ -24,7 +19,7 @@ blocks:
2419
- id: create_executable
2520
type: CreateFile
2621
inputs:
27-
path: "{{inputs.path}}/script.sh"
22+
path: "{{tmp}}/script.sh"
2823
create_parents: true
2924
mode: "755"
3025
content: |
@@ -34,14 +29,14 @@ blocks:
3429
- id: verify_executable
3530
type: Shell
3631
inputs:
37-
working_dir: "{{inputs.path}}"
32+
working_dir: "{{tmp}}"
3833
command: test -x "script.sh" && echo "yes"
3934
depends_on: [create_executable]
4035

4136
- id: create_file
4237
type: CreateFile
4338
inputs:
44-
path: "{{inputs.path}}/README.md"
39+
path: "{{tmp}}/README.md"
4540
create_parents: true
4641
overwrite: true
4742
encoding: "utf-8"
@@ -62,42 +57,26 @@ blocks:
6257
type: ReadFiles
6358
inputs:
6459
patterns: ["README.md"]
65-
base_path: "{{inputs.path}}"
60+
base_path: "{{tmp}}"
6661
depends_on: [create_file]
6762

6863
- id: read_nonexistent
6964
type: ReadFiles
7065
inputs:
7166
patterns: ["nonexistent.txt"]
72-
base_path: "{{inputs.path}}"
67+
base_path: "{{tmp}}"
7368
depends_on: [shell_data]
7469

75-
- id: cleanup
76-
type: Shell
77-
inputs:
78-
command: |
79-
# Safe cleanup with validation
80-
DIR="{{inputs.path}}"
81-
if [[ "$DIR" == /tmp/* ]] && [ -d "$DIR" ]; then
82-
rm -rf "$DIR"
83-
fi
84-
depends_on:
85-
- block: read_file
86-
required: false
87-
8870
outputs:
8971
file_created:
9072
value: "{{blocks.create_file.succeeded}}"
9173
type: bool
9274
file_content:
93-
value: "{{get(blocks.read_file.outputs.files, 0, {}).content | default('Error: Failed to read file content')}}"
75+
value: "{{blocks.read_file.content}}"
9476
type: str
9577
nonexistent_file_found:
9678
value: "{{blocks.read_nonexistent.succeeded}}"
9779
type: bool
9880
executable_file_created:
9981
value: "{{blocks.verify_executable.succeeded}}"
10082
type: bool
101-
cleanup_done:
102-
value: "{{blocks.cleanup.succeeded}}"
103-
type: bool

tests/workflows/core/file-operations/editfile-interpolation-test.yaml

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@ description: Test EditFile operation field interpolation support
1313
tags: [test, editfile, interpolation]
1414

1515
inputs:
16-
test_dir:
17-
type: str
18-
description: Test directory path
19-
default: "/tmp/editfile-interpolation-test"
2016
# String inputs for operation field interpolation
2117
search_text:
2218
type: str
@@ -53,7 +49,7 @@ blocks:
5349
- id: create_test_file
5450
type: CreateFile
5551
inputs:
56-
path: "{{inputs.test_dir}}/test.py"
52+
path: "{{tmp}}/test.py"
5753
content: |
5854
def test():
5955
value = "PLACEHOLDER"
@@ -68,7 +64,7 @@ blocks:
6864
- id: test_replace_text_interpolation
6965
type: EditFile
7066
inputs:
71-
path: "{{inputs.test_dir}}/test.py"
67+
path: "{{tmp}}/test.py"
7268
operations:
7369
- type: replace_text
7470
old_text: "{{inputs.search_text}}"
@@ -81,7 +77,7 @@ blocks:
8177
- id: test_insert_lines_interpolation
8278
type: EditFile
8379
inputs:
84-
path: "{{inputs.test_dir}}/test.py"
80+
path: "{{tmp}}/test.py"
8581
operations:
8682
- type: insert_lines
8783
line_start: 1
@@ -93,7 +89,7 @@ blocks:
9389
- id: test_regex_replace_interpolation
9490
type: EditFile
9591
inputs:
96-
path: "{{inputs.test_dir}}/test.py"
92+
path: "{{tmp}}/test.py"
9793
operations:
9894
- type: regex_replace
9995
pattern: "{{inputs.regex_pattern}}"
@@ -106,7 +102,7 @@ blocks:
106102
- id: test_delete_lines_interpolation
107103
type: EditFile
108104
inputs:
109-
path: "{{inputs.test_dir}}/test.py"
105+
path: "{{tmp}}/test.py"
110106
operations:
111107
- type: delete_lines
112108
line_start: "{{inputs.delete_start}}"
@@ -118,7 +114,7 @@ blocks:
118114
- id: test_multiple_operations_interpolation
119115
type: EditFile
120116
inputs:
121-
path: "{{inputs.test_dir}}/test.py"
117+
path: "{{tmp}}/test.py"
122118
operations:
123119
# Operation 1: Use variable for old_text
124120
- type: replace_text
@@ -128,15 +124,15 @@ blocks:
128124
# Operation 2: Use variable for content
129125
- type: insert_lines
130126
line_start: 1
131-
content: "# Modified by {{inputs.test_dir}}"
127+
content: "# Modified by test_multiple_operations_interpolation"
132128
backup: false
133129
depends_on: [test_delete_lines_interpolation]
134130

135131
# Test 6: Nested variable references (block output interpolation)
136132
- id: test_nested_interpolation
137133
type: EditFile
138134
inputs:
139-
path: "{{inputs.test_dir}}/test.py"
135+
path: "{{tmp}}/test.py"
140136
operations:
141137
- type: insert_lines
142138
line_start: 1
@@ -150,7 +146,7 @@ blocks:
150146
- id: store_operations
151147
type: WriteJSONState
152148
inputs:
153-
path: "{{inputs.test_dir}}/operations.json"
149+
path: "{{tmp}}/operations.json"
154150
data:
155151
operations:
156152
- type: "replace_text"
@@ -163,15 +159,15 @@ blocks:
163159
- id: read_operations
164160
type: ReadJSONState
165161
inputs:
166-
path: "{{inputs.test_dir}}/operations.json"
162+
path: "{{tmp}}/operations.json"
167163
required: true
168164
depends_on: [store_operations]
169165

170166
# Test 9: Interpolate entire operations field from state
171167
- id: test_operations_interpolation
172168
type: EditFile
173169
inputs:
174-
path: "{{inputs.test_dir}}/test.py"
170+
path: "{{tmp}}/test.py"
175171
operations: "{{blocks.read_operations.outputs.data.operations}}"
176172
backup: false
177173
depends_on: [read_operations]
@@ -181,7 +177,7 @@ blocks:
181177
type: ReadFiles
182178
inputs:
183179
patterns: ["test.py"]
184-
base_path: "{{inputs.test_dir}}"
180+
base_path: "{{tmp}}"
185181
mode: full
186182
depends_on: [test_operations_interpolation]
187183

tests/workflows/core/file-operations/editfile-operations-test.yaml

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,12 @@ name: editfile-operations-test
1212
description: Test EditFile executor with all operation types
1313
tags: [test, editfile, files]
1414

15-
inputs:
16-
test_dir:
17-
type: str
18-
description: Test directory path
19-
default: "/tmp/editfile-test"
20-
2115
blocks:
2216
# Setup: Create test file
2317
- id: create_test_file
2418
type: CreateFile
2519
inputs:
26-
path: "{{inputs.test_dir}}/test.py"
20+
path: "{{tmp}}/test.py"
2721
content: |
2822
def hello():
2923
print("Hello, World!")
@@ -38,7 +32,7 @@ blocks:
3832
- id: test_replace_text
3933
type: EditFile
4034
inputs:
41-
path: "{{inputs.test_dir}}/test.py"
35+
path: "{{tmp}}/test.py"
4236
operations:
4337
- type: replace_text
4438
old_text: "Hello, World!"
@@ -51,7 +45,7 @@ blocks:
5145
- id: test_insert_lines
5246
type: EditFile
5347
inputs:
54-
path: "{{inputs.test_dir}}/test.py"
48+
path: "{{tmp}}/test.py"
5549
operations:
5650
- type: insert_lines
5751
line_start: 1
@@ -64,7 +58,7 @@ blocks:
6458
- id: test_delete_lines
6559
type: EditFile
6660
inputs:
67-
path: "{{inputs.test_dir}}/test.py"
61+
path: "{{tmp}}/test.py"
6862
operations:
6963
- type: delete_lines
7064
line_start: 7
@@ -76,7 +70,7 @@ blocks:
7670
- id: test_regex_replace
7771
type: EditFile
7872
inputs:
79-
path: "{{inputs.test_dir}}/test.py"
73+
path: "{{tmp}}/test.py"
8074
operations:
8175
- type: regex_replace
8276
pattern: 'print\("([^"]+)"\)'
@@ -89,7 +83,7 @@ blocks:
8983
- id: test_dry_run
9084
type: EditFile
9185
inputs:
92-
path: "{{inputs.test_dir}}/test.py"
86+
path: "{{tmp}}/test.py"
9387
operations:
9488
- type: replace_text
9589
old_text: "logger.info"
@@ -103,7 +97,7 @@ blocks:
10397
type: ReadFiles
10498
inputs:
10599
patterns: ["test.py"]
106-
base_path: "{{inputs.test_dir}}"
100+
base_path: "{{tmp}}"
107101
mode: full
108102
depends_on: [test_dry_run]
109103

0 commit comments

Comments
 (0)