Skip to content

Adds the ability to pass files to ramalama run #1570

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 29, 2025

Conversation

ieaves
Copy link
Collaborator

@ieaves ieaves commented Jun 19, 2025

Summary by Sourcery

Enable users to pass files or directories into the ramalama commands by introducing a new --input flag, implementing a file upload loader with supported file-type handlers, integrating file contents into chat prompts, and covering the functionality with extensive tests.

New Features:

  • Add --input option to accept single file or directory for ramalama run/chat commands
  • Implement FileUpLoader and BaseFileUploader with pluggable file-type handlers (.txt, .md, .json, .yaml/.yml, .toml, .csv, .pdf) to load and prefix file contents into prompts

Enhancements:

  • Handle file extension mapping case-insensitively and warn on unsupported types

Tests:

  • Add comprehensive unit tests and sample data fixtures for file loading, type-specific handlers, and chat integration with file inputs

Copy link
Contributor

sourcery-ai bot commented Jun 19, 2025

Reviewer's Guide

Implements file input support for ramalama run and chat by adding a FileUpLoader and file handlers for supported formats, integrating a new --input CLI flag and chat logic to inject file contents into prompts, and bolstering coverage with extensive unit and integration tests.

Sequence diagram for CLI file upload flow

sequenceDiagram
    actor User
    participant CLI as CLI
    participant FileUpLoader
    participant FileHandlers as File Handlers
    participant Model
    User->>CLI: run ramalama run --input <file/dir>
    CLI->>FileUpLoader: Initialize with file/dir path
    FileUpLoader->>FileHandlers: Load and concatenate file contents
    FileHandlers-->>FileUpLoader: File contents
    FileUpLoader-->>CLI: Concatenated file contents
    CLI->>Model: Inject file contents into prompt and run model
    Model-->>CLI: Model output
    CLI-->>User: Display output
Loading

Sequence diagram for chat file upload flow

sequenceDiagram
    actor User
    participant Chat as Chat Command
    participant FileUpLoader
    participant FileHandlers as File Handlers
    participant Model
    User->>Chat: Start chat with --input <file/dir>
    Chat->>FileUpLoader: Initialize with file/dir path
    FileUpLoader->>FileHandlers: Load and concatenate file contents
    FileHandlers-->>FileUpLoader: File contents
    FileUpLoader-->>Chat: Concatenated file contents
    Chat->>Model: Inject file contents into chat prompt and run model
    Model-->>Chat: Model output
    Chat-->>User: Display output
Loading

Entity relationship diagram for supported file types

erDiagram
    FILE_UPLOADER ||--o{ FILE : loads
    FILE {
        string file_path
        string extension
    }
    FILE_UPLOADER {
        string document_delimiter
    }
    TXT_FILE_UPLOAD ||--|| FILE : is_a
    PDF_FILE_UPLOAD ||--|| FILE : is_a
    FILE_UPLOADER ||--o{ TXT_FILE_UPLOAD : uses
    FILE_UPLOADER ||--o{ PDF_FILE_UPLOAD : uses
Loading

Class diagram for file upload handlers and loader

classDiagram
    class BaseFileUpload {
        <<abstract>>
        +file
        +load() str
    }
    class TXTFileUpload {
        +load() str
    }
    class PDFFileUpload {
        +load() str
    }
    class BaseFileUploader {
        +files
        +document_delimiter
        +load() str
    }
    class FileUpLoader {
        +__init__(file_path: str)
    }
    BaseFileUpload <|-- TXTFileUpload
    BaseFileUpload <|-- PDFFileUpload
    BaseFileUploader <|-- FileUpLoader
    FileUpLoader o-- BaseFileUpload : uses
    FileUpLoader o-- TXTFileUpload : uses
    FileUpLoader o-- PDFFileUpload : uses
Loading

File-Level Changes

Change Details Files
Introduce --input option and integrate file loading in chat
  • Import and invoke FileUpLoader in chat to append loaded file data to prompts
  • Add --input argument to the CLI parser for run/chat commands
  • Conditionally extend args.ARGS with file contents when input is provided
ramalama/chat.py
ramalama/cli.py
Add file upload infrastructure with extension-based handlers
  • Define SUPPORTED_EXTENSIONS mapping and implement FileUpLoader for single files or directories
  • Create BaseFileUploader to concatenate document-delimited content
  • Define abstract BaseFileUpload and concrete TXTFileUpload and PDFFileUpload handlers
ramalama/file_upload/file_loader.py
ramalama/file_upload/file_types/base.py
ramalama/file_upload/file_types/txt.py
ramalama/file_upload/file_types/pdf.py
Add comprehensive unit and integration tests for file upload and chat integration
  • Unit tests for upload handlers (BaseFileUpload, TXTFileUpload, PDFFileUpload)
  • Unit tests for BaseFileUploader, FileUpLoader, supported extensions and edge cases
  • End-to-end integration tests for chat with file inputs
  • Data-driven tests using sample files in a new test data directory
test/unit/test_file_upload.py
test/unit/test_file_upload_integration.py
test/unit/test_file_upload_with_data.py
test/unit/data/test_file_upload/*

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Changes

Hello @ieaves, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant enhancement to the ramalama CLI by enabling the direct ingestion of file contents into the model's prompt. This functionality is implemented through a new, modular file upload system that supports various text-based formats and is designed for future extensibility. The changes empower users to provide rich context to the model directly from their local files or project directories.

Highlights

  • New Feature: File Input for ramalama run: I've added a new --input argument to the ramalama run command, allowing users to pass the content of specified files or entire directories as part of the prompt to the model.
  • Modular File Upload System: A new ramalama.file_upload package has been introduced, featuring a modular design with BaseFileUpload and BaseFileUploader classes. This architecture supports easy extension for new file types.
  • Supported File Types: Initial support for various text-based file types (e.g., .txt, .md, .json, .yaml, .csv, .toml, .sh) and a placeholder for .pdf files has been implemented. Unsupported file types are gracefully ignored with a warning.
  • Automated Content Inclusion: The chat function now automatically loads content from the specified input files or directories and appends it to the ARGS (prompt) that is sent to the model, delimited for clarity.
  • Comprehensive Testing: Extensive unit and integration tests have been added to validate the new file upload system, covering file loading, directory handling, unsupported types, and the integration with the chat command.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments to provide feedback.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @ieaves - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `ramalama/file_upload/file_types/pdf.py:10` </location>
<code_context>
+        Load the content of the PDF file.
+        This method should be implemented to handle PDF file reading.
+        """
+        return ""
</code_context>

<issue_to_address>
PDFFileUpload.load currently returns an empty string, which may be misleading.

Raising NotImplementedError or issuing a warning would make it clear that PDF loading is not yet implemented, avoiding confusion with an empty PDF result.
</issue_to_address>

### Comment 2
<location> `test/unit/test_file_upload.py:187` </location>
<code_context>
+            expected = f"\n<!--start_document {tmp_file.name}-->\nTest content\nwith multiple lines"
+            assert result == expected
+
+    def test_file_uploader_load_multiple_files(self):
+        """Test loading multiple files from a directory."""
+        with tempfile.TemporaryDirectory() as tmp_dir:
</code_context>

<issue_to_address>
Consider adding a test for deeply nested directories.

Please clarify whether files in subdirectories should be processed. Add a test to verify this behavior, and update the implementation if needed.

Suggested implementation:

```python
    def test_file_uploader_load_multiple_files(self):
        """Test loading multiple files from a directory."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            txt_file = os.path.join(tmp_dir, "test.txt")
            with open(txt_file, "w") as f:
                f.write("Text content")

            md_file = os.path.join(tmp_dir, "test.md")
            with open(md_file, "w") as f:
                f.write("# Markdown content")

            uploader = FileUpLoader(tmp_dir)
            assert len(uploader.files) == 2

    def test_file_uploader_ignores_nested_directories(self):
        """Test that files in deeply nested directories are not processed (current behavior)."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            # Create a file in the root directory
            root_file = os.path.join(tmp_dir, "root.txt")
            with open(root_file, "w") as f:
                f.write("Root file")

            # Create a nested directory structure
            nested_dir = os.path.join(tmp_dir, "level1", "level2", "level3")
            os.makedirs(nested_dir)
            nested_file = os.path.join(nested_dir, "nested.txt")
            with open(nested_file, "w") as f:
                f.write("Nested file")

            uploader = FileUpLoader(tmp_dir)
            # Only the root file should be processed if subdirectories are ignored
            assert root_file in uploader.files
            assert nested_file not in uploader.files
            assert len(uploader.files) == 1

```

If the intended behavior is to process files in subdirectories, you will need to update the `FileUpLoader` implementation to recursively find files. If so, let me know and I can provide the necessary implementation changes.
</issue_to_address>

### Comment 3
<location> `test/unit/test_file_upload.py:207` </location>
<code_context>
+            assert "test.txt" in result
+            assert "test.md" in result
+
+    def test_file_uploader_unsupported_file_types(self):
+        """Test handling of unsupported file types."""
+        with tempfile.TemporaryDirectory() as tmp_dir:
</code_context>

<issue_to_address>
Add a test to verify that a warning is emitted for unsupported file types.

Please add a test that asserts a warning is raised when unsupported file types are present, for example using pytest.warns.

Suggested implementation:

```python
    def test_file_uploader_unsupported_file_types(self):
        """Test handling of unsupported file types."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            # Create a supported file
            txt_file = os.path.join(tmp_dir, "test.txt")
            with open(txt_file, "w") as f:
                f.write("Text content")

            # Create an unsupported file
            unsupported_file = os.path.join(tmp_dir, "test.exe")
            with open(unsupported_file, "w") as f:
                f.write("This should trigger a warning.")

            import pytest
            # Assuming the warning is a UserWarning; adjust if your code uses a custom warning
            with pytest.warns(UserWarning, match="unsupported file type"):
                uploader = FileUpLoader(tmp_dir)
                uploader.load()

```

- Ensure that the `FileUpLoader` class emits a `UserWarning` (or a custom warning) when encountering unsupported file types. If it does not, you will need to add this behavior in the implementation.
- Adjust the warning type and match string in `pytest.warns` if your code uses a different warning class or message.
- Make sure `import pytest` is present at the top of your test file if not already imported.
</issue_to_address>

### Comment 4
<location> `test/unit/test_file_upload.py:175` </location>
<code_context>
+        with pytest.raises(ValueError, match="does not exist"):
+            FileUpLoader("/nonexistent/directory")
+
+    def test_file_uploader_load_single_text_file(self):
+        """Test loading a single text file."""
+        with tempfile.NamedTemporaryFile(suffix=".txt") as tmp_file:
</code_context>

<issue_to_address>
Consider adding a test for files with no extension.

Please add a test to confirm how files without extensions are handled—whether they are ignored or raise an error.

Suggested implementation:

```python
    def test_file_uploader_load_single_text_file(self):
        """Test loading a single text file."""
        with tempfile.NamedTemporaryFile(suffix=".txt") as tmp_file:
            with open(tmp_file.name, "w") as f:
                f.write("Test content")

            uploader = FileUpLoader(tmp_file.name)
            assert len(uploader.files) == 1
            assert isinstance(uploader.files[0], TXTFileUpload)
            assert uploader.files[0].file == tmp_file.name

    def test_file_uploader_file_without_extension(self):
        """Test loading a file with no extension."""
        with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_file:
            tmp_file.write(b"No extension content")
            tmp_file.flush()
            file_path = tmp_file.name

        try:
            uploader = FileUpLoader(file_path)
            # If files without extension are ignored, assert that uploader.files is empty
            assert len(uploader.files) == 0
        except Exception as e:
            # If an error is expected, assert the type of error
            assert isinstance(e, ValueError)
        finally:
            os.remove(file_path)

```

- If `FileUpLoader` is expected to behave differently (e.g., raise a specific error or handle files without extension in a custom way), adjust the assertions accordingly.
- Ensure `os` is imported at the top of the file if not already present.
</issue_to_address>

### Comment 5
<location> `test/unit/test_file_upload_with_data.py:172` </location>
<code_context>
+            assert "sample.md" in result
+            assert "sample.json" in result
+
+    def test_unsupported_file_handling(self, data_dir):
+        """Test that unsupported files are handled correctly."""
+
</code_context>

<issue_to_address>
Consider adding a test for a file with a supported extension but unreadable permissions.

Add a test to verify that the loader handles unreadable files with supported extensions by failing gracefully or raising the correct error.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    def test_unsupported_file_handling(self, data_dir):
        """Test that unsupported files are handled correctly."""
=======
    def test_unsupported_file_handling(self, data_dir):
        """Test that unsupported files are handled correctly."""

    def test_unreadable_supported_file(self, tmp_path):
        """Test that unreadable files with supported extensions are handled gracefully."""
        import os
        from ramalama.file_upload.file_loader import FileUpLoader

        # Create a supported file (e.g., .txt)
        unreadable_file = tmp_path / "unreadable.txt"
        unreadable_file.write_text("This file should not be readable.")

        # Remove read permissions
        unreadable_file.chmod(0o000)

        try:
            loader = FileUpLoader()
            with pytest.raises(Exception):
                loader.load_file(str(unreadable_file))
        finally:
            # Restore permissions so pytest can clean up the file
            unreadable_file.chmod(0o644)
>>>>>>> REPLACE

</suggested_fix>

### Comment 6
<location> `ramalama/file_upload/file_loader.py:1` </location>
<code_context>
+import itertools
+import os
+from string import Template
</code_context>

<issue_to_address>
Consider removing unused imports, simplifying delimiter handling, and collapsing file discovery and instantiation logic into concise comprehensions.

```suggestion
# 1) Drop unused imports and the Template/Type machinery
- Remove `import itertools`, `from string import Template`, `from typing import Type`.
- Update BaseFileUploader to store a plain string prefix and use an f-string in load().

--- a/ramalama/file_upload/uploader.py
@@
- import itertools
- from string import Template
- from typing import Type
+ import os
+ from warnings import warn
+ from ramalama.file_upload.file_types import base, pdf, txt

@@
- class BaseFileUploader:
+ class BaseFileUploader:
@@
-     def __init__(self, files: list[Type[base.BaseFileUpload]], delim_string: str = "<!--start_document $name-->"):
-         self.files = files
-         self.document_delimiter: Template = Template(delim_string)
+     def __init__(self, files: list[base.BaseFileUpload], delimiter_prefix: str = "<!--start_document"):
+         self.files = files
+         self.delimiter_prefix = delimiter_prefix
@@
-     def load(self) -> str:
-         """Generate the output string by concatenating the processed files."""
-         output = (f"\n{self.document_delimiter.substitute(name=f.file)}\n{f.load()}" for f in self.files)
-         return "".join(output)
+     def load(self) -> str:
+         """Generate the output string by concatenating the processed files."""
+         return "".join(
+             f"\n{self.delimiter_prefix} {f.file}-->\n{f.load()}"
+             for f in self.files
+         )

# 2) Collapse discovery, filtering, warning, instantiation in FileUpLoader
--- a/ramalama/file_upload/uploader.py
@@ class FileUpLoader(BaseFileUploader):
-     def __init__(self, file_path: str):
-         if not os.path.exists(file_path):
-             raise ValueError(f"{file_path} does not exist.")
-
-         if not os.path.isdir(file_path):
-             files = [file_path]
-         else:
-             files = [
-                 os.path.join(file_path, f) for f in os.listdir(file_path)
-                 if os.path.isfile(os.path.join(file_path, f))
-             ]
-
-         extensions = [os.path.splitext(f)[1].lower() for f in files]
-
-         if set(extensions) - set(SUPPORTED_EXTENSIONS):
-             warning_message = (
-                 f"Unsupported file types found: {set(extensions) - set(SUPPORTED_EXTENSIONS)}\n"
-                 f"Supported types are: {set(SUPPORTED_EXTENSIONS.keys())}"
-             )
-             warn(warning_message)
-
-         files = [SUPPORTED_EXTENSIONS[ext](file=f)
-                  for ext, f in zip(extensions, files)
-                  if ext in SUPPORTED_EXTENSIONS]
-         super().__init__(files=files)
+     def __init__(self, path: str):
+         if not os.path.exists(path):
+             raise ValueError(f"{path} does not exist.")
+
+         # collect all candidate files
+         candidates = [path] if os.path.isfile(path) else [
+             os.path.join(path, name) for name in os.listdir(path)
+             if os.path.isfile(os.path.join(path, name))
+         ]
+
+         # warn on unsupported extensions
+         exts = [os.path.splitext(p)[1].lower() for p in candidates]
+         unsupported = set(exts) - set(SUPPORTED_EXTENSIONS)
+         if unsupported:
+             warn(f"Unsupported file types: {unsupported}. Supported: {set(SUPPORTED_EXTENSIONS)}")
+
+         # instantiate only supported uploads
+         instances = [
+             SUPPORTED_EXTENSIONS[ext](file=p)
+             for p in candidates
+             if (ext := os.path.splitext(p)[1].lower()) in SUPPORTED_EXTENSIONS
+         ]
+         super().__init__(instances)
```

This removes indirection, drops unused imports, replaces `Template` with a simple f-string, and collapses listing/filtering/instantiation into concise comprehensions while preserving all behavior.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

expected = f"\n<!--start_document {tmp_file.name}-->\nTest content\nwith multiple lines"
assert result == expected

def test_file_uploader_load_multiple_files(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider adding a test for deeply nested directories.

Please clarify whether files in subdirectories should be processed. Add a test to verify this behavior, and update the implementation if needed.

Suggested implementation:

    def test_file_uploader_load_multiple_files(self):
        """Test loading multiple files from a directory."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            txt_file = os.path.join(tmp_dir, "test.txt")
            with open(txt_file, "w") as f:
                f.write("Text content")

            md_file = os.path.join(tmp_dir, "test.md")
            with open(md_file, "w") as f:
                f.write("# Markdown content")

            uploader = FileUpLoader(tmp_dir)
            assert len(uploader.files) == 2

    def test_file_uploader_ignores_nested_directories(self):
        """Test that files in deeply nested directories are not processed (current behavior)."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            # Create a file in the root directory
            root_file = os.path.join(tmp_dir, "root.txt")
            with open(root_file, "w") as f:
                f.write("Root file")

            # Create a nested directory structure
            nested_dir = os.path.join(tmp_dir, "level1", "level2", "level3")
            os.makedirs(nested_dir)
            nested_file = os.path.join(nested_dir, "nested.txt")
            with open(nested_file, "w") as f:
                f.write("Nested file")

            uploader = FileUpLoader(tmp_dir)
            # Only the root file should be processed if subdirectories are ignored
            assert root_file in uploader.files
            assert nested_file not in uploader.files
            assert len(uploader.files) == 1

If the intended behavior is to process files in subdirectories, you will need to update the FileUpLoader implementation to recursively find files. If so, let me know and I can provide the necessary implementation changes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this tool should walk the subdirectories, please suggest a implementation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! Since you want the tool to walk subdirectories, your current implementation in FileUpLoader already uses os.walk, which recursively finds all files in the given directory and its subdirectories:

files = [os.path.join(root, name) for root, _, files in os.walk(file_path) for name in files]

This means all files, including those in deeply nested subdirectories, will be processed if their extensions are supported.

To ensure this behavior is tested and clear, you should add a test like the following:

def test_file_uploader_processes_nested_directories(self):
    """Test that files in deeply nested directories are processed."""
    with tempfile.TemporaryDirectory() as tmp_dir:
        # Create a file in the root directory
        root_file = os.path.join(tmp_dir, "root.txt")
        with open(root_file, "w") as f:
            f.write("Root file")

        # Create a nested directory structure
        nested_dir = os.path.join(tmp_dir, "level1", "level2", "level3")
        os.makedirs(nested_dir)
        nested_file = os.path.join(nested_dir, "nested.txt")
        with open(nested_file, "w") as f:
            f.write("Nested file")

        uploader = FileUpLoader(tmp_dir)
        file_paths = {f.file for f in uploader.files}
        assert root_file in file_paths
        assert nested_file in file_paths
        assert len(file_paths) == 2

This test will confirm that files in all subdirectories are included. You can add it to your TestFileUpLoader class.

Let me know if you need a test for filtering or for unsupported file types in subdirectories as well!

assert "test.txt" in result
assert "test.md" in result

def test_file_uploader_unsupported_file_types(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a test to verify that a warning is emitted for unsupported file types.

Please add a test that asserts a warning is raised when unsupported file types are present, for example using pytest.warns.

Suggested implementation:

    def test_file_uploader_unsupported_file_types(self):
        """Test handling of unsupported file types."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            # Create a supported file
            txt_file = os.path.join(tmp_dir, "test.txt")
            with open(txt_file, "w") as f:
                f.write("Text content")

            # Create an unsupported file
            unsupported_file = os.path.join(tmp_dir, "test.exe")
            with open(unsupported_file, "w") as f:
                f.write("This should trigger a warning.")

            import pytest
            # Assuming the warning is a UserWarning; adjust if your code uses a custom warning
            with pytest.warns(UserWarning, match="unsupported file type"):
                uploader = FileUpLoader(tmp_dir)
                uploader.load()
  • Ensure that the FileUpLoader class emits a UserWarning (or a custom warning) when encountering unsupported file types. If it does not, you will need to add this behavior in the implementation.
  • Adjust the warning type and match string in pytest.warns if your code uses a different warning class or message.
  • Make sure import pytest is present at the top of your test file if not already imported.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add this test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! To add this test, simply copy the suggested implementation into your TestFileUpLoader class in test_file_upload.py. Make sure that your FileUpLoader implementation actually emits a UserWarning (or another warning type) when it encounters unsupported file types—otherwise, the test will fail and you may need to update the implementation accordingly.

If you need the warning to be more specific or want to use a custom warning class, adjust the pytest.warns parameters as needed. Also, ensure import pytest is present at the top of your test file (it already is in your current file).

Let me know if you need help updating the implementation to emit the warning, or if you want to see a full code block for the test!

with pytest.raises(ValueError, match="does not exist"):
FileUpLoader("/nonexistent/directory")

def test_file_uploader_load_single_text_file(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider adding a test for files with no extension.

Please add a test to confirm how files without extensions are handled—whether they are ignored or raise an error.

Suggested implementation:

    def test_file_uploader_load_single_text_file(self):
        """Test loading a single text file."""
        with tempfile.NamedTemporaryFile(suffix=".txt") as tmp_file:
            with open(tmp_file.name, "w") as f:
                f.write("Test content")

            uploader = FileUpLoader(tmp_file.name)
            assert len(uploader.files) == 1
            assert isinstance(uploader.files[0], TXTFileUpload)
            assert uploader.files[0].file == tmp_file.name

    def test_file_uploader_file_without_extension(self):
        """Test loading a file with no extension."""
        with tempfile.NamedTemporaryFile(suffix="", delete=False) as tmp_file:
            tmp_file.write(b"No extension content")
            tmp_file.flush()
            file_path = tmp_file.name

        try:
            uploader = FileUpLoader(file_path)
            # If files without extension are ignored, assert that uploader.files is empty
            assert len(uploader.files) == 0
        except Exception as e:
            # If an error is expected, assert the type of error
            assert isinstance(e, ValueError)
        finally:
            os.remove(file_path)
  • If FileUpLoader is expected to behave differently (e.g., raise a specific error or handle files without extension in a custom way), adjust the assertions accordingly.
  • Ensure os is imported at the top of the file if not already present.

@ieaves ieaves force-pushed the feat/file-upload branch from 3d35e08 to 0b88cfc Compare June 19, 2025 20:03
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the ability to pass files to ramalama run, enhancing its utility by allowing file contents to be directly included in prompts. Key feedback points include correcting the handling of args.ARGS in ramalama/chat.py to prevent TypeErrors, refining a type hint in BaseFileUploader for better clarity, and suggesting a default encoding (utf-8) for reading text files to improve robustness.

This class should be extended by specific file type handlers.
"""

def __init__(self, files: list[Type[base.BaseFileUpload]], delim_string: str = "<!--start_document $name-->"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The type hint for the files parameter in BaseFileUploader.__init__ is list[Type[base.BaseFileUpload]]. This suggests it expects a list of classes, but a list of instances is being passed. To accurately reflect that files is a list of instances, the type hint should be list[base.BaseFileUpload].

Suggested change
def __init__(self, files: list[Type[base.BaseFileUpload]], delim_string: str = "<!--start_document $name-->"):
def __init__(self, files: list[base.BaseFileUpload], delim_string: str = "<!--start_document $name-->"):

"""

# TODO: Support for non-default encodings?
with open(self.file, 'r') as f:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The TODO comment on line 10 mentions supporting non-default encodings. Opening files without specifying an encoding uses the system's default, which can lead to UnicodeDecodeError. Using encoding='utf-8' is a robust default for text files.

Suggested change
with open(self.file, 'r') as f:
with open(self.file, 'r', encoding='utf-8') as f: # Specify UTF-8 encoding as a robust default

@ieaves
Copy link
Collaborator Author

ieaves commented Jun 19, 2025

Hey @rhatdan and @ericcurtin, this PR came about by way of reference to #867 and I'd love some directional feedback. The group over at pymupdf has a pure python implementation of a pdf reader with a permissive, copy-left license. If this direction looked right, we could either add it as an optional dependency or copy their current implementation over to support a pdf reader.

@ericcurtin
Copy link
Member

ericcurtin commented Jun 20, 2025

I'm not sure we want this added dependancy, what we can do is:

cat some_file.txt | ramalama run somemodel

@ieaves
Copy link
Collaborator Author

ieaves commented Jun 20, 2025

I'm not sure we want this added dependancy, what we can do is:


cat some_file.txt | ramalama run somemodel

Do you mean the pdf reader as a dependency or the ability to pass file content? If the former, as I mentioned there are a few easy paths

  1. Leave it an optional dependency which doesn't affect the rest of the application at all

  2. Use the code from the repository which is itself pure python and has a permissive copy left license. This means we don't get upstream changes but removes any dependencies.

  3. Just not include pdf readers

@rhatdan
Copy link
Member

rhatdan commented Jun 20, 2025

Should we just use -v and support container volumes or --mount? Does this option make sense in the --nocontainer use case?

@rhatdan
Copy link
Member

rhatdan commented Jun 20, 2025

BTW Love to keep some of these tests, but need to understand why I need to move files around at all, if the issue is just to provide file/directory access to the Model Engines within the containers, then volume mounts can do that.

@ericcurtin
Copy link
Member

I'm not sure we want this added dependancy, what we can do is:


cat some_file.txt | ramalama run somemodel

Do you mean the pdf reader as a dependency or the ability to pass file content? If the former, as I mentioned there are a few easy paths

  1. Leave it an optional dependency which doesn't affect the rest of the application at all
  2. Use the code from the repository which is itself pure python and has a permissive copy left license. This means we don't get upstream changes but removes any dependencies.
  3. Just not include pdf readers
  1. we've had a couple of optional dependancies in the past, huggingface cli being one, it's been a big source of bugs.

We could consider 4. , installing it inside the container and executing it there.

@ieaves
Copy link
Collaborator Author

ieaves commented Jun 20, 2025

Just as a little preamble, the way I understood the initial request is that the user was looking for a way to perform one off analysis. Something like

ramalama run --input my_document.pdf tinyllama Please summarize this document for me:

Would spin up an instance of tinyllama, consume the prompt and document, respond with the requested summary, and wind down. This workflow can already be accomplished for a single raw text based file by piping the content of the file into the prompt but gets a little more complicated for more complicated document formats or a directory of documents.


We can definitely mount the user specified file or directory into the container but what are you thinking from there? Is the idea that we would move all of the reader code into a separate dependency that we bake into every image build and somehow trigger when the user provides the --input flag? Something else?

@ericcurtin
Copy link
Member

ericcurtin commented Jun 20, 2025

I recommend doing something like this:

pdf2txt my_document.pdf | ramalama run tinyllama Please summarize this document for me:

I used to do things like this with RamaLama before, worked great. The biggest problem is the new dependancy. But if you do it this way, we avoid that. pdf2txt is obviously a made up thing, but put a tool of your choice there.

Just tried to test this with:

cat README.md | ramalama run smollm:135m Summarise this file

doesn't seem to work anymore, that's a bug.

@ieaves
Copy link
Collaborator Author

ieaves commented Jun 20, 2025

Okay, it sounds like you aren't interested in Ramalama itself being able to support either multi-file upload or file parsing itself? I can close this PR in that case, do you want to close out #867 as not being of interest as well?

@ericcurtin
Copy link
Member

It may be possible to add dependancies inside the container did you look at our rag solution? That could be similar to what you are looking for.

@ieaves
Copy link
Collaborator Author

ieaves commented Jun 20, 2025

The biggest problem is the new dependancy.

Just to be clear, this PR doesn't add a new dependency. It does include an example of how optional dependencies can be included for users who want functionality not available in a default installation but, as discussed, even that isn't strictly necessary.

It may be possible to add dependancies inside the container did you look at our rag solution?

I previously asked whether I was thinking about this potential implementation correctly because it just seemed like an awfully heavy solution to a fairly trivial problem and I wanted to make sure I hadn't missed something.

The user is ultimately just trying to append documents to their prompt but following the RAG model they would need to do something like this:

  1. Mounting the file / directory into the container
  2. Running a modified build script to pull additional dependencies on every container boot
  3. Ramalama builds a new image with the vector database available to the llm and pushes the new image to a repository
  4. The user runs this image to finally perform a query.

This is asking a lot from users to perform what could otherwise be a fairly simple task.

@rhatdan
Copy link
Member

rhatdan commented Jun 21, 2025

Lets setup a google meet to further discuss the PR/Issue you are trying to solve. Once we get the requirements down we can decide if this is something to be added to RamaLama or something to be done externally and then piped into the ramalama run/ramalama chat interface.

@ieaves
Copy link
Collaborator Author

ieaves commented Jun 23, 2025

Sounds good, I don't have your email @rhatdan but I'll shoot @ericcurtin an email to see if we can find some time for a quick call this week!

@rhatdan
Copy link
Member

rhatdan commented Jun 24, 2025

My email is [email protected]

@rhatdan
Copy link
Member

rhatdan commented Jun 25, 2025

After meeting today, we believe the first version of this should be

ramalama chat --rag /tmp/myrag --url http://localhost:8080

Where ramalama will read the markdown file in the --rag directory and add it to the context before querying the Model Runtime.

Nest step would be to change

ramalama rag a.md b.pdf c.docx /tmp/mynewrag

Which would tell ramalama to use the docling-rag container image to convert the specified files into markdown language to be used in a new command:

ramalama chat --rag /tmp/mynewrag

Since ramalama run is now ramalama serve; ramalama chat, this should quickly get us the same support into ramalama run command.

We still want to support:

ramalama serve --rag OCI:quay.io/ramalama/myrag:1.2 MODEL for server side support.

@rhatdan
Copy link
Member

rhatdan commented Jun 25, 2025

You need to sign all of your commits, but it might be that we need a total rewrite. of this PR.

@ieaves
Copy link
Collaborator Author

ieaves commented Jun 25, 2025

Tell me more, I can definitely change the argument from --context to --rag which would be the phase 1 we discussed (and is fairly trivial at this point). If we are simply adding the documents to the user's query, I'm not sure it's the best idea to cross contaminate the --rag argument but happy to defer to your judgement if you feel strongly on the point.

Do you want to include a migration to docling in this PR as well though? If so, how do you feel about first building a new minimal image with docling for document processing and then when a user calls the chat endpoint with --rag (or w/e)

  1. Load the document processing container with a mount point for the users documents and a temporary output directory
  2. Running the docs processing container outputting a single markdown file to the output directory
  3. Spinning up the users chat container
  4. Loading the output file from (2) and appending it to the users query.
  5. Proceeding with chat as usual

@rhatdan
Copy link
Member

rhatdan commented Jun 26, 2025

Lets do the ramalama chat --rag/context first. Then we can do ramalama rag second PR.

I am not wedded to --rag, but --context was not as clear to me what you were doing.

Issue with

ramama run/chat --rag is that this is on the client side.
Where as
ramalama server --rag would be to the service.

Having --context and --rag in ramalama run would be confusing.

@ieaves
Copy link
Collaborator Author

ieaves commented Jun 26, 2025

@rhatdan if we are putting docling off for the moment, can we move forwards on this PR? Perhaps after changing from --context to --rag?

@rhatdan
Copy link
Member

rhatdan commented Jun 26, 2025

Yes

@ieaves ieaves force-pushed the feat/file-upload branch 5 times, most recently from 6bad636 to 81a8b86 Compare June 27, 2025 06:12
@ieaves
Copy link
Collaborator Author

ieaves commented Jun 27, 2025

This should be good to do using --rag now @rhatdan. Once this is merged I'll start putting a draft together for us the phase 2 docling changes.

@ericcurtin
Copy link
Member

Build failure with:

1000.1.20250627061305165889.pr1570.49.g81a8b86.el10.x86_64/usr/lib/python3.12/site-packages
+ _PYTHONSITE=/builddir/build/BUILDROOT/python-ramalama-0.9.3-1000.1.20250627061305165889.pr1570.49.g81a8b86.el10.x86_64/usr/lib64/python3.12/site-packages:/builddir/build/BUILDROOT/python-ramalama-0.9.3-1000.1.20250627061305165889.pr1570.49.g81a8b86.el10.x86_64/usr/lib/python3.12/site-packages
+ PYTHONDONTWRITEBYTECODE=1
+ /usr/bin/python3 -sP /usr/lib/rpm/redhat/import_all_modules.py -f /builddir/build/BUILD/python-ramalama-0.9.3-1000.1.20250627061305165889.pr1570.49.g81a8b86.el10.x86_64-pyproject-modules
Check import: ramalama
Traceback (most recent call last):
  File "/usr/lib/rpm/redhat/import_all_modules.py", line 171, in <module>
    main()
  File "/usr/lib/rpm/redhat/import_all_modules.py", line 167, in main
    import_modules(modules)
  File "/usr/lib/rpm/redhat/import_all_modules.py", line 100, in import_modules
    importlib.import_module(module)
  File "/usr/lib64/python3.12/importlib/__init__.py", line 90, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 999, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "/builddir/build/BUILDROOT/python-ramalama-0.9.3-1000.1.20250627061305165889.pr1570.49.g81a8b86.el10.x86_64/usr/lib/python3.12/site-packages/ramalama/__init__.py", line 6, in <module>
    from ramalama.cli import HelpException, init_cli, print_version
  File "/builddir/build/BUILDROOT/python-ramalama-0.9.3-1000.1.20250627061305165889.pr1570.49.g81a8b86.el10.x86_64/usr/lib/python3.12/site-packages/ramalama/cli.py", line 22, in <module>
    import ramalama.chat as chat
  File "/builddir/build/BUILDROOT/python-ramalama-0.9.3-1000.1.20250627061305165889.pr1570.49.g81a8b86.el10.x86_64/usr/lib/python3.12/site-packages/ramalama/chat.py", line 17, in <module>
    from ramalama.file_upload.file_loader import FileUpLoader
ModuleNotFoundError: No module named 'ramalama.file_upload'

RPM build errors:
error: Bad exit status from /var/tmp/rpm-tmp.uIE4dW (%check)
    Bad exit status from /var/tmp/rpm-tmp.uIE4dW (%check)
Finish: rpmbuild python-ramalama-0.9.3-1000.1.202506270613051

anyway we could fix that?

@ieaves
Copy link
Collaborator Author

ieaves commented Jun 27, 2025

Hey @ericcurtin should be good now, looks like submodules weren't being included in stricter build environments. Since I was looking at the build I threw in a fix for the deprecated license declaration behavior as well, hope that's okay!

files = [file_path]
else:
files = [
os.path.join(file_path, f) for f in os.listdir(file_path) if os.path.isfile(os.path.join(file_path, f))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a Walk rather then a single layer?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure! Switched it over just now :)

@rhatdan
Copy link
Member

rhatdan commented Jun 28, 2025

Great test suite, this looks very good, I have a few comments and then we can merge. Love to have a demo of this or a blog once it works and is released.

Signed-off-by: Ian Eaves <[email protected]>
@ieaves ieaves force-pushed the feat/file-upload branch from df876af to 1f03de0 Compare June 29, 2025 02:18
@ieaves
Copy link
Collaborator Author

ieaves commented Jun 29, 2025

Great test suite, this looks very good, I have a few comments and then we can merge. Love to have a demo of this or a blog once it works and is released.

Awesome, thanks Dan! Once it's merged I'll put something together we can use for both. It might be a good tie in to some of the RAG features y'all have been working on as well since this could be a sort of low stakes entree to those capabilities. I didn't see any other comments on the PR though, did you want to throw those into a review or ping me another way? Happy to address whatever you're thinking about.

@rhatdan
Copy link
Member

rhatdan commented Jun 29, 2025

LGTM

I swear I reviewed a version of this where you were not using the walk function, and I said to walk the specified directory. Now I see that walk is here. There is a sourcery suggestion to add a couple more tests, but this is ready to merge, so I will merge.

BTW Did you use AI to generate the tests suite?

@rhatdan rhatdan merged commit bc561d2 into containers:main Jun 29, 2025
25 checks passed
@ieaves
Copy link
Collaborator Author

ieaves commented Jun 30, 2025

BTW Did you use AI to generate the tests suite?

Definitely yes for the scaffolding, I've found it's very helpful generating much of the boilerplate and ideating on potential failure paths but much, much worse at actually implementing meaningful tests without supervision. For this PR I, asked it to generate a test suite but found the test implementations somewhat trivial. I had to go back and provide targeted guidance on a better approach and with those additional instructions it mostly fixed things through in the subsequent pass. After that I individually reviewed each and made targeted changes as needed. It's a huge time saver though for generating all of the tedious mocks and what not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants