Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c62df86
Support tool mode in dynamic outputs
erichare Nov 6, 2025
10f5932
[autofix.ci] apply automated fixes
autofix-ci[bot] Nov 6, 2025
b764428
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Nov 6, 2025
f35eac4
[autofix.ci] apply automated fixes (attempt 3/3)
autofix-ci[bot] Nov 6, 2025
e77cf5c
Tool mode and ruff fixes
erichare Nov 6, 2025
86f8fcb
Template updates
erichare Nov 6, 2025
cc300f9
[autofix.ci] apply automated fixes
autofix-ci[bot] Nov 6, 2025
b508b17
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Nov 6, 2025
b18ecd5
[autofix.ci] apply automated fixes (attempt 3/3)
autofix-ci[bot] Nov 6, 2025
ec7f30c
Merge branch 'main' into fix-file-component-toolmode
erichare Nov 12, 2025
22dc1fa
[autofix.ci] apply automated fixes
autofix-ci[bot] Nov 12, 2025
2d1310a
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Nov 12, 2025
8766a6e
[autofix.ci] apply automated fixes (attempt 3/3)
autofix-ci[bot] Nov 12, 2025
25f24d0
Update test_mcp_servers_file.py
erichare Nov 18, 2025
78c0a8f
Merge branch 'main' into fix-file-component-toolmode
erichare Nov 18, 2025
17d6e6f
Revert "Update test_mcp_servers_file.py"
erichare Nov 18, 2025
e84ccc9
[autofix.ci] apply automated fixes
autofix-ci[bot] Nov 18, 2025
c224f64
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Nov 18, 2025
7eb7557
[autofix.ci] apply automated fixes (attempt 3/3)
autofix-ci[bot] Nov 18, 2025
7d45999
add possibility for the agent to access the processed output file
Cristhianzl Nov 24, 2025
46f462a
merge fix
Cristhianzl Nov 24, 2025
59459d0
[autofix.ci] apply automated fixes
autofix-ci[bot] Nov 24, 2025
5d7e885
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Nov 24, 2025
3c13d86
[autofix.ci] apply automated fixes (attempt 3/3)
autofix-ci[bot] Nov 24, 2025
1730e21
Merge branch 'main' into fix-file-component-toolmode
carlosrcoelho Nov 24, 2025
363f088
Merge branch 'main' into fix-file-component-toolmode
erichare Nov 24, 2025
9f8c9b5
[autofix.ci] apply automated fixes
autofix-ci[bot] Nov 24, 2025
01e2578
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Nov 24, 2025
f3e74a9
[autofix.ci] apply automated fixes (attempt 3/3)
autofix-ci[bot] Nov 24, 2025
0ac1338
Merge branch 'main' into fix-file-component-toolmode
erichare Nov 24, 2025
6bdcf1c
[autofix.ci] apply automated fixes
autofix-ci[bot] Nov 24, 2025
32b9506
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Nov 24, 2025
6a87a83
[autofix.ci] apply automated fixes (attempt 3/3)
autofix-ci[bot] Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,134 @@ def test_process_docling_subprocess_success(self, mock_subprocess):
assert result is not None
assert result.data["doc"] == mock_result["doc"]
assert result.data["file_path"] == "test.pdf"

def test_dynamic_outputs_have_tool_mode_enabled(self):
"""Test that all dynamically created outputs have tool_mode=True."""
component = FileComponent()

# Test single CSV file
frontend_node = {"outputs": [], "template": {"path": {"file_path": ["test.csv"]}}}
result = component.update_outputs(frontend_node, "path", ["test.csv"])
for output in result["outputs"]:
assert output.tool_mode is True, f"Output {output.name} should have tool_mode=True"

# Test single JSON file
frontend_node = {"outputs": [], "template": {"path": {"file_path": ["data.json"]}}}
result = component.update_outputs(frontend_node, "path", ["data.json"])
for output in result["outputs"]:
assert output.tool_mode is True, f"Output {output.name} should have tool_mode=True"

# Test multiple files
frontend_node = {"outputs": [], "template": {"path": {"file_path": ["file1.txt", "file2.txt"]}}}
result = component.update_outputs(frontend_node, "path", ["file1.txt", "file2.txt"])
for output in result["outputs"]:
assert output.tool_mode is True, f"Output {output.name} should have tool_mode=True"

# Test advanced mode enabled
frontend_node = {
"outputs": [],
"template": {
"path": {"file_path": ["document.pdf"]},
"advanced_mode": {"value": True},
},
}
result = component.update_outputs(frontend_node, "advanced_mode", field_value=True)
for output in result["outputs"]:
assert output.tool_mode is True, f"Output {output.name} should have tool_mode=True"

# Test advanced mode disabled
frontend_node = {
"outputs": [],
"template": {
"path": {"file_path": ["document.pdf"]},
"advanced_mode": {"value": False},
},
}
result = component.update_outputs(frontend_node, "advanced_mode", field_value=False)
for output in result["outputs"]:
assert output.tool_mode is True, f"Output {output.name} should have tool_mode=True"

def test_file_path_str_input_exists_for_tool_mode(self):
"""Test that file_path_str input exists for tool mode."""
component = FileComponent()

# Find the file_path_str input
file_path_str_input = None
for input_field in component.inputs:
if input_field.name == "file_path_str":
file_path_str_input = input_field
break

assert file_path_str_input is not None, "file_path_str input should exist"
assert file_path_str_input.tool_mode is True, "file_path_str should have tool_mode=True"

# Check that the path FileInput has tool_mode=False
path_input = None
for input_field in component.inputs:
if input_field.name == "path":
path_input = input_field
break

assert path_input is not None, "path input should exist"
assert path_input.tool_mode is False, "path FileInput should have tool_mode=False"

def test_read_file_using_file_path_str(self, tmp_path):
"""Test reading a file using file_path_str parameter (tool mode)."""
# Create a test file
test_file = tmp_path / "test.txt"
test_content = "Hello from tool mode!"
test_file.write_text(test_content)

# Create component and set file_path_str
component = FileComponent()
component.file_path_str = str(test_file)

# Load the file
result = component.load_files_message()

assert result.text == test_content, f"Expected '{test_content}', got '{result.text}'"

def test_read_file_using_path_when_file_path_str_not_provided(self, tmp_path):
"""Test that component falls back to uploaded file when file_path_str is not provided.

This simulates the scenario where a file is uploaded via UI and then the Agent
calls the component as a tool without providing file_path_str.
When a file is uploaded via UI, the FileInput populates the file_path attribute.
"""
# Create a test file
test_file = tmp_path / "test_from_ui.txt"
test_content = "Hello from uploaded file!"
test_file.write_text(test_content)

# Create component and simulate uploaded file
component = FileComponent()
# When user uploads file via UI, the FileInput sets the path attribute
# which populates the file_path list (from FileMixin)
component.path = str(test_file) # Simulate FileInput value

# DO NOT set file_path_str (simulating Agent calling tool without this parameter)
# component.file_path_str should be None or empty

# Load the file - should use path since file_path_str is not provided
result = component.load_files_message()

assert result.text == test_content, f"Expected '{test_content}', got '{result.text}'"

def test_file_path_str_takes_priority_over_path(self, tmp_path):
"""Test that file_path_str takes priority when both are provided."""
# Create two test files
file1 = tmp_path / "file1.txt"
file1.write_text("Content from path")

file2 = tmp_path / "file2.txt"
file2.write_text("Content from file_path_str")

# Create component with both inputs set
component = FileComponent()
component.path = str(file1) # Uploaded file via UI
component.file_path_str = str(file2) # Provided by Agent tool call

# Load the file - should use file_path_str (priority)
result = component.load_files_message()

assert result.text == "Content from file_path_str", "file_path_str should take priority over path"
2 changes: 1 addition & 1 deletion src/lfx/src/lfx/_assets/component_index.json

Large diffs are not rendered by default.

77 changes: 68 additions & 9 deletions src/lfx/src/lfx/components/files_and_knowledge/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,24 @@ class FileComponent(BaseFileComponent):
for input_item in _base_inputs:
if isinstance(input_item, FileInput) and input_item.name == "path":
input_item.real_time_refresh = True
input_item.tool_mode = False # Disable tool mode for file upload input
input_item.required = False # Make it optional so it doesn't error in tool mode
break

inputs = [
*_base_inputs,
StrInput(
name="file_path_str",
display_name="File Path",
info=(
"Path to the file to read. Used when component is called as a tool. "
"If not provided, will use the uploaded file from 'path' input."
),
show=False,
advanced=True,
tool_mode=True,
required=False,
),
BoolInput(
name="advanced_mode",
display_name="Advanced Parser",
Expand Down Expand Up @@ -152,7 +166,7 @@ class FileComponent(BaseFileComponent):
]

outputs = [
Output(display_name="Raw Content", name="message", method="load_files_message"),
Output(display_name="Raw Content", name="message", method="load_files_message", tool_mode=True),
]

# ------------------------------ UI helpers --------------------------------------
Expand Down Expand Up @@ -213,39 +227,84 @@ def update_outputs(self, frontend_node: dict[str, Any], field_name: str, field_v
file_path = paths[0] if field_name == "path" else frontend_node["template"]["path"]["file_path"][0]
if file_path.endswith((".csv", ".xlsx", ".parquet")):
frontend_node["outputs"].append(
Output(display_name="Structured Content", name="dataframe", method="load_files_structured"),
Output(
display_name="Structured Content",
name="dataframe",
method="load_files_structured",
tool_mode=True,
),
)
elif file_path.endswith(".json"):
frontend_node["outputs"].append(
Output(display_name="Structured Content", name="json", method="load_files_json"),
Output(display_name="Structured Content", name="json", method="load_files_json", tool_mode=True),
)

advanced_mode = frontend_node.get("template", {}).get("advanced_mode", {}).get("value", False)
if advanced_mode:
frontend_node["outputs"].append(
Output(display_name="Structured Output", name="advanced_dataframe", method="load_files_dataframe"),
Output(
display_name="Structured Output",
name="advanced_dataframe",
method="load_files_dataframe",
tool_mode=True,
),
)
frontend_node["outputs"].append(
Output(display_name="Markdown", name="advanced_markdown", method="load_files_markdown"),
Output(
display_name="Markdown", name="advanced_markdown", method="load_files_markdown", tool_mode=True
),
)
frontend_node["outputs"].append(
Output(display_name="File Path", name="path", method="load_files_path"),
Output(display_name="File Path", name="path", method="load_files_path", tool_mode=True),
)
else:
frontend_node["outputs"].append(
Output(display_name="Raw Content", name="message", method="load_files_message"),
Output(display_name="Raw Content", name="message", method="load_files_message", tool_mode=True),
)
frontend_node["outputs"].append(
Output(display_name="File Path", name="path", method="load_files_path"),
Output(display_name="File Path", name="path", method="load_files_path", tool_mode=True),
)
else:
# Multiple files => DataFrame output; advanced parser disabled
frontend_node["outputs"].append(Output(display_name="Files", name="dataframe", method="load_files"))
frontend_node["outputs"].append(
Output(display_name="Files", name="dataframe", method="load_files", tool_mode=True)
)

return frontend_node

# ------------------------------ Core processing ----------------------------------

def _validate_and_resolve_paths(self) -> list[BaseFileComponent.BaseFile]:
"""Override to handle file_path_str input from tool mode.

When called as a tool, the file_path_str parameter can be set.
If not provided, it will fall back to using the path FileInput (uploaded file).
Priority:
1. file_path_str (if provided by the tool call)
2. path (uploaded file from UI)
"""
# Check if file_path_str is provided (from tool mode)
file_path_str = getattr(self, "file_path_str", None)
if file_path_str:
# Use the string path from tool mode
from pathlib import Path

from lfx.schema.data import Data

resolved_path = Path(self.resolve_path(file_path_str))
if not resolved_path.exists():
msg = f"File or directory not found: {file_path_str}"
self.log(msg)
if not self.silent_errors:
raise ValueError(msg)
return []

data_obj = Data(data={self.SERVER_FILE_PATH_FIELDNAME: str(resolved_path)})
return [BaseFileComponent.BaseFile(data_obj, resolved_path, delete_after_processing=False)]

# Otherwise use the default implementation (uses path FileInput)
return super()._validate_and_resolve_paths()

def _is_docling_compatible(self, file_path: str) -> bool:
"""Lightweight extension gate for Docling-compatible types."""
docling_exts = (
Expand Down
Loading