Metadata-Version: 2.4
Name: lora-easy
Version: 2.0
Summary: A tiny OOP wrapper around PEFT for LoRA fine-tuning of causal LMs.
Author: Freakwill
License: MIT
Project-URL: Homepage, https://github.com/Freakwill/lora-easy
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: torch>=2.0
Requires-Dist: peft>=0.19
Requires-Dist: transformers>=4.45

# lora-easy

<p align="center">
  <img src="assets/banner.svg" alt="lora-easy — LoRA fine-tuning" width="700">
</p>

<p align="center">
  <img src="https://img.shields.io/badge/python-3.9%2B-blue" alt="python">
  <img src="https://img.shields.io/badge/peft-0.19%2B-orange" alt="peft">
  <img src="https://img.shields.io/badge/license-MIT-green" alt="license">
</p>

A tiny, object-oriented wrapper around 🤗 **PEFT** for LoRA fine-tuning of causal
language models. One `LoraModel` class hides the `from_pretrained` boilerplate,
and `Agent` adds web / file search capabilities on top.

## Key concepts

- **LoRA (Low-Rank Adaptation)** — instead of updating all of a model's weights,
  LoRA freezes the base model and trains two small low-rank matrices (`A` and `B`)
  injected into the attention layers. Typically **~0.1% of parameters** are
  trainable, so a checkpoint is a few MB instead of GB.
- **Base vs. adapter** — the large pretrained weights never change. The tiny
  adapter carries the new personality you trained. `enable_lora()` /
  `disable_lora()` just switch which one `self.model` points at, so toggling never
  loses your trained weights.
- **Chat template** — training and inference must format text the same way.
  Training data is rendered with `add_generation_prompt=False`; inference uses
  `True` so the model knows to start generating.
- **Label masking** — `labels` mirror `input_ids`, but padding positions are set to
  `-100` so they are ignored in the loss.
- **System prompt** — set `system_prompt=` on `LoraModel` or `Agent(description=…)`
  to give the model a persona. Change it at runtime with `/system-prompt` in an
  interactive session.
- **Slash commands** — register your own with `@command("/name")` and use them
  during `chat_session().run()`.

## Requirements

```
torch>=2.0
peft>=0.19
transformers>=4.45
```

```bash
pip install lora-easy
```

Runs on CUDA, Apple Silicon (MPS), or CPU.

## Quick start

### LoraModel

```python
from lora_ez import LoraModel

m = LoraModel("Qwen/Qwen2.5-0.5B-Instruct", name="cat",
              system_prompt="you are a sassy house cat")

# ----- fine-tune -----
m.enable_lora(r=8, alpha=16)
m.train(data, epochs=30)
m.save()                   # -> ./lora-cat/

# ----- single-turn chat -----
print(m.chat("hello!"))

# ----- multi-turn with memory -----
with m.chat_session("./chat.json", auto_save=True) as s:
    s.chat("I'm back")
    s.chat("how are you?")
    s.run()                # interactive REPL, /exit to quit
```

### Agent (web + file search)

```python
from lora_ez import Agent

m = LoraModel("Qwen/Qwen2.5-0.5B-Instruct")
a = Agent(m, description="you are a data analyst",
          web_enabled=True, file_enabled=True)

a.chat("what Python packages are installed?")
a.web_fetch("https://example.com")
a.disable_web()
```

### API

**LoraModel**

| Method | What it does |
|--------|--------------|
| `LoraModel(model_id, name, system_prompt, device)` | Load base model + tokenizer |
| `enable_lora(r, alpha, dropout)` | Attach a LoRA adapter |
| `disable_lora()` | Point back to the frozen base model |
| `train(conversations, **kwargs)` | Fine-tune on ShareGPT-format data |
| `chat(prompt, history, system_prompt)` | Generate a reply |
| `chat_session(save_path, auto_save, system_prompt)` | Multi-turn session context manager |
| `save(path)` / `load(path)` | Persist / restore the adapter |

**Agent** — wraps a `LoraModel` with tools

| Method | What it does |
|--------|--------------|
| `Agent(model, description, web_enabled, …)` | Wrap a model with search tools |
| `chat(prompt)` | Auto-injects web / file context, then delegates to model |
| `enable_web()` / `disable_web()` | Toggle web search |
| `enable_files()` / `disable_files()` | Toggle local file search |
| `web_fetch(url)` | Fetch a URL, respecting allowlists / blocklists |
| `file_read(path)` | Read a file inside allowed directories |
| `chat_session(…)` | Multi-turn session (delegated to the model) |

**Slash commands** — build your own with the `@command` decorator

```python
from lora_ez import command

@command("/greet")
def greet(session, *args):
    return f"Hello, {' '.join(args)}!" if args else "Hello!"
```

Built-in: `/exit`, `/help`, `/system-prompt`.

## Demo

The [`demo/`](demo/) folder trains `Qwen2.5-0.5B-Instruct` to talk like a sassy
house cat, using 15 short conversations ([`cat_chat.json`](demo/cat_chat.json)).

```bash
cd demo
python3 lora-cat.py          # full training + before/after comparison
python3 test-session.py      # multi-turn chat session with commands
```

## Links

- [🤗 PEFT documentation](https://huggingface.co/docs/peft)
- [LoRA paper (Hu et al., 2021)](https://arxiv.org/abs/2106.09685)
- [Qwen2.5 models](https://huggingface.co/Qwen)

## License

MIT
