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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions docs/ramalama-chat.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ Show this help message and exit
#### **--prefix**
Prefix for the user prompt (default: 🦭 > )

#### **--rag**=path
A file or directory of files to be loaded and provided as local context in the chat history.

#### **--url**=URL
The host to send requests to (default: http://127.0.0.1:8080)


## EXAMPLES

Communicate with the default local OpenAI REST API. (http://127.0.0.1:8080)
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ version = "0.9.3"
description = "RamaLama is a command line tool for working with AI LLM models."
readme = "README.md"
requires-python = ">=3.10"
license = { file = "LICENSE" }
keywords = ["ramalama", "llama", "AI"]
dependencies = [
"argcomplete",
Expand All @@ -18,6 +17,8 @@ maintainers = [
{ name="Eric Curtin", email = "[email protected]" },
]

[project.license]
text = "MIT"

[project.optional-dependencies]
dev = [
Expand Down Expand Up @@ -56,6 +57,7 @@ ramalama = "ramalama.cli:main"

[tool.setuptools]
include-package-data = true
license-files = ["LICENSE"]

[tool.black]
line-length = 120
Expand Down Expand Up @@ -93,7 +95,7 @@ log_cli_date_format = "%Y-%m-%d %H:%M:%S"


[tool.setuptools.packages.find]
include = ["ramalama"]
include = ["ramalama", "ramalama.*"]

[tool.setuptools.data-files]
"share/ramalama" = ["shortnames/shortnames.conf"]
Expand Down
11 changes: 11 additions & 0 deletions ramalama/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from ramalama.config import CONFIG
from ramalama.console import EMOJI, should_colorize
from ramalama.engine import dry_run, stop_container
from ramalama.file_upload.file_loader import FileUpLoader
from ramalama.logger import logger


Expand Down Expand Up @@ -72,6 +73,16 @@ def __init__(self, args):
self.prompt = args.prefix

self.url = f"{args.url}/chat/completions"
self.prep_rag_message()

def prep_rag_message(self):
if (context := getattr(self.args, "rag", None)) is None:
return

if not (message_content := FileUpLoader(context).load()):
return

self.conversation_history.append({"role": "system", "content": message_content})

def handle_args(self):
prompt = " ".join(self.args.ARGS) if self.args.ARGS else None
Expand Down
2 changes: 2 additions & 0 deletions ramalama/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,7 @@ def chat_parser(subparsers):
help='possible values are "never", "always" and "auto".',
)
parser.add_argument("--prefix", type=str, help="prefix for the user prompt", default=default_prefix())
parser.add_argument("--rag", type=str, help="a file or directory to use as context for the chat")
parser.add_argument("--url", type=str, default="http://127.0.0.1:8080/v1", help="the url to send requests to")
parser.add_argument("MODEL", completer=local_models) # positional argument
parser.add_argument(
Expand All @@ -925,6 +926,7 @@ def run_parser(subparsers):
)
parser.add_argument("--prefix", type=str, help="prefix for the user prompt", default=default_prefix())
parser.add_argument("MODEL", completer=local_models) # positional argument

parser.add_argument(
"ARGS",
nargs="*",
Expand Down
3 changes: 3 additions & 0 deletions ramalama/file_upload/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ramalama.file_upload import file_loader, file_types

__all__ = ["file_loader", "file_types"]
57 changes: 57 additions & 0 deletions ramalama/file_upload/file_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import os
from string import Template
from warnings import warn

from ramalama.file_upload.file_types import base, txt

SUPPORTED_EXTENSIONS = {
'.txt': txt.TXTFileUpload,
'.sh': txt.TXTFileUpload,
'.md': txt.TXTFileUpload,
'.yaml': txt.TXTFileUpload,
'.yml': txt.TXTFileUpload,
'.json': txt.TXTFileUpload,
'.csv': txt.TXTFileUpload,
'.toml': txt.TXTFileUpload,
}


class BaseFileUploader:
"""
Base class for file upload handlers.
This class should be extended by specific file type handlers.
"""

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

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)


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(root, name) for root, _, files in os.walk(file_path) for name in files]

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)
3 changes: 3 additions & 0 deletions ramalama/file_upload/file_types/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ramalama.file_upload.file_types import base, txt

__all__ = ["base", "txt"]
19 changes: 19 additions & 0 deletions ramalama/file_upload/file_types/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from abc import ABC, abstractmethod


class BaseFileUpload(ABC):
"""
Base class for file upload handlers.
This class should be extended by specific file type handlers.
"""

def __init__(self, file):
self.file = file

@abstractmethod
def load(self) -> str:
"""
Load the content of the file.
This method should be implemented by subclasses to handle specific file types.
"""
pass
10 changes: 10 additions & 0 deletions ramalama/file_upload/file_types/pdf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from ramalama.file_upload.file_types.base import BaseFileUpload


class PDFFileUpload(BaseFileUpload):
def load(self) -> str:
"""
Load the content of the PDF file.
This method should be implemented to handle PDF file reading.
"""
return ""
12 changes: 12 additions & 0 deletions ramalama/file_upload/file_types/txt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from ramalama.file_upload.file_types.base import BaseFileUpload


class TXTFileUpload(BaseFileUpload):
def load(self) -> str:
"""
Load the content of the text file.
"""

# 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

return f.read()
6 changes: 6 additions & 0 deletions test/unit/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# # tests/conftest.py
# import pytest

# @pytest.fixture(autouse=True)
# def set_container_engine_env(monkeypatch):
# monkeypatch.setenv("RAMALAMA_CONTAINER_ENGINE", "docker")
6 changes: 6 additions & 0 deletions test/unit/data/test_file_upload/sample.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name,age,city,occupation
John Doe,30,New York,Engineer
Jane Smith,25,San Francisco,Designer
Bob Johnson,35,Chicago,Manager
Alice Brown,28,Boston,Developer
Charlie Wilson,32,Seattle,Analyst
20 changes: 20 additions & 0 deletions test/unit/data/test_file_upload/sample.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "test_data",
"version": "1.0.0",
"description": "Sample JSON data for testing file upload functionality",
"features": [
"text_processing",
"file_upload",
"chat_integration"
],
"metadata": {
"author": "Test User",
"created": "2024-01-01",
"tags": ["test", "sample", "json"]
},
"config": {
"enabled": true,
"max_file_size": 1048576,
"supported_formats": [".txt", ".md", ".json", ".yaml", ".yml", ".csv", ".toml", ".pdf"]
}
}
20 changes: 20 additions & 0 deletions test/unit/data/test_file_upload/sample.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Sample Markdown File

This is a sample markdown file for testing the file upload functionality.

## Features

- **Bold text** and *italic text*
- Lists with bullets
- Code blocks: `inline code`
- Links: [Example](https://example.com)

## Code Example

```python
def hello_world():
print("Hello, World!")
return "Success"
```

This file should be processed by the TXTFileUpload class since markdown is treated as text.
36 changes: 36 additions & 0 deletions test/unit/data/test_file_upload/sample.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/bin/bash

# Sample shell script for testing file upload functionality

echo "Hello, World! This is a test script."

# Function to demonstrate script functionality
test_function() {
local message="$1"
echo "Testing: $message"
return 0
}

# Variables
NAME="Test Script"
VERSION="1.0.0"

# Main execution
echo "Running $NAME version $VERSION"

# Test the function
test_function "file upload functionality"

# Conditional logic
if [ -f "test_file.txt" ]; then
echo "Test file exists"
else
echo "Test file does not exist"
fi

# Loop example
for i in {1..3}; do
echo "Iteration $i"
done

echo "Script completed successfully!"
23 changes: 23 additions & 0 deletions test/unit/data/test_file_upload/sample.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Sample TOML configuration file
name = "test_config"
version = "1.0.0"
description = "Sample TOML data for testing file upload functionality"

[metadata]
author = "Test User"
created = "2024-01-01"
tags = ["test", "sample", "toml"]

[config]
enabled = true
max_file_size = 1048576
supported_formats = [".txt", ".md", ".json", ".yaml", ".yml", ".csv", ".toml", ".pdf"]

[features]
text_processing = true
file_upload = true
chat_integration = true
toml_support = true

[nested.structure]
with_deep_nesting = true
9 changes: 9 additions & 0 deletions test/unit/data/test_file_upload/sample.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
This is a sample text file for testing the file upload functionality.

It contains multiple lines of text to test how the system handles:
- Plain text content
- Multiple lines
- Special characters like: !@#$%^&*()
- Numbers: 1234567890

This file should be processed by the TXTFileUpload class.
37 changes: 37 additions & 0 deletions test/unit/data/test_file_upload/sample.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Sample YAML configuration file
name: test_config
version: 1.0.0
description: Sample YAML data for testing file upload functionality

features:
- text_processing
- file_upload
- chat_integration
- yaml_support

metadata:
author: Test User
created: 2024-01-01
tags:
- test
- sample
- yaml

config:
enabled: true
max_file_size: 1048576
supported_formats:
- .txt
- .md
- .json
- .yaml
- .yml
- .csv
- .toml
- .pdf

nested:
structure:
with:
deep:
nesting: true
3 changes: 2 additions & 1 deletion test/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ def test_get_default_engine_with_toolboxenv(self):
def test_get_default_engine_with_podman_available(self, platform, expected):
with patch("ramalama.config.available", side_effect=lambda x: x == "podman"):
with patch("sys.platform", platform):
assert get_default_engine() == expected
with patch("ramalama.config.apple_vm", return_value=False):
assert get_default_engine() == expected

def test_get_default_engine_with_podman_available_osx_apple_vm_has_podman(self):
with patch("ramalama.config.available", side_effect=lambda x: x == "podman"):
Expand Down
Loading
Loading