Skip to content

Commit 92d8ede

Browse files
committed
feat: ✨ Allow users to choose AI model
1 parent 66f8da2 commit 92d8ede

4 files changed

Lines changed: 49 additions & 1 deletion

File tree

app/api/routes/forms.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
FormFillResponse,
1111
ModelsResponse,
1212
TranscriptionResponse,
13+
ModelPullRequest,
1314
)
1415
from app.core.config import OLLAMA_HOST, OLLAMA_MODEL, BASE_DIR, RETENTION_PERIOD_DAYS
1516
from app.services.whisper import call_whisper_asr
@@ -86,6 +87,16 @@ def list_models():
8687
return ModelsResponse(models=models, default=default_model)
8788

8889

90+
@router.post("/pull")
91+
def pull_model(req: ModelPullRequest):
92+
try:
93+
resp = requests.post(f"{OLLAMA_HOST}/api/pull", json={"name": req.model, "stream": False}, timeout=600)
94+
resp.raise_for_status()
95+
return {"status": "success", "message": f"Model {req.model} pulled successfully"}
96+
except requests.exceptions.RequestException as e:
97+
raise AppError(f"Failed to pull model: {e}", status_code=500, error_code="MODEL_PULL_ERROR")
98+
99+
89100
@router.post("/transcribe", response_model=TranscriptionResponse)
90101
def transcribe(audio: UploadFile = File(...)):
91102
"""Forward recorded audio to the local Whisper ASR sidecar and return text.

app/api/schemas/forms.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,8 @@ class Config:
7474

7575

7676
class AsyncFormFillResponse(BaseModel):
77-
jobs: list[AsyncJobSubmitResponse]
77+
jobs: list[AsyncJobSubmitResponse]
78+
79+
80+
class ModelPullRequest(BaseModel):
81+
model: str

docs/1. SETUP.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,19 @@ Check `make logs-app` for the actual error. The entrypoint runs database migrati
9191
**Want a clean slate**
9292
`make super-clean` stops everything and **deletes all volumes** database, uploads, and downloaded model weights. Only use it when you intend to wipe all local data.
9393

94+
## AI Model Selection
95+
96+
FireForm allows you to choose which AI model to run during form extraction directly from the dropdown in the frontend "Fill Form" UI.
97+
98+
The supported recommended models are:
99+
- `qwen2.5:1.5b` (default, lightweight)
100+
- `qwen2.5:3b`
101+
- `qwen2.5:7b`
102+
- `llama3.2:3b`
103+
- `mistral:7b`
104+
105+
If you select a model that is not yet installed (pulled) in your local Ollama instance, the app will automatically request the backend to download it via the `POST /api/v1/forms/pull` endpoint. During installation, form submission is temporarily disabled, and status progress is displayed. Once downloaded, the model is cached in Ollama's Docker volume for future use.
106+
94107
## Where to go next
95108

96109
- **Join our [Discord](https://discord.gg/nBv5b6kF68)** — ask questions and coordinate with other contributors

tests/test_api.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,26 @@ def boom(*a, **k):
283283
assert resp.status_code == 200
284284
assert resp.json()["models"] == ["qwen2.5:1.5b"]
285285

286+
def test_pull_model_success(self, client, monkeypatch):
287+
from unittest.mock import MagicMock
288+
fake_response = MagicMock()
289+
fake_response.raise_for_status.return_value = None
290+
monkeypatch.setattr("app.api.routes.forms.requests.post", lambda *a, **k: fake_response)
291+
292+
resp = client.post(f"{API_PREFIX}/forms/pull", json={"model": "llama3.2:3b"})
293+
assert resp.status_code == 200
294+
assert resp.json()["status"] == "success"
295+
296+
def test_pull_model_failure(self, client, monkeypatch):
297+
import requests
298+
def boom(*a, **k):
299+
raise requests.exceptions.RequestException("pull failed")
300+
monkeypatch.setattr("app.api.routes.forms.requests.post", boom)
301+
302+
resp = client.post(f"{API_PREFIX}/forms/pull", json={"model": "llama3.2:3b"})
303+
assert resp.status_code == 500
304+
assert resp.json()["error_code"] == "MODEL_PULL_ERROR"
305+
286306
def test_fill_form_passes_model_override(self, client, mock_controller):
287307
"""A `model` in the request reaches Controller.fill_form but isn't persisted."""
288308
tpl_id = self._seed_template(client, mock_controller)

0 commit comments

Comments
 (0)