diff --git a/.gitignore b/.gitignore index 7fa2022..37a352c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ .idea venv .venv -*.db \ No newline at end of file +*.db +*.egg-info/ +src/inputs/*_template.pdf diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/Dockerfile b/Dockerfile index 833fcc3..587610e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,15 +8,14 @@ RUN apt-get update && apt-get install -y \ curl \ && rm -rf /var/lib/apt/lists/* -# Copy and install Python dependencies -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - # Copy application code COPY . . +# Install Python dependencies after the source is present so pip can install the package itself. +RUN pip install --no-cache-dir -r requirements.txt -r requirements-ai.txt + # Set Python path so imports work correctly -ENV PYTHONPATH=/app/src +ENV PYTHONPATH=/app # Keep container running for interactive use CMD ["tail", "-f", "/dev/null"] diff --git a/Makefile b/Makefile index 53eb56a..c0d7a61 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,7 @@ pull-model: docker compose exec ollama ollama pull mistral test: - docker compose exec app python3 -m pytest src/test/ + docker compose exec app python3 -m pytest -q tests/ clean: docker compose down -v diff --git a/README.md b/README.md index 42862e3..0f68b3b 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,24 @@ FireForm is a centralized "report once, file everywhere" system. The result is hours of time saved per shift, per firefighter. +## Local Setup + +FireForm targets Python 3.11. The repository includes a `.python-version` file to make that explicit. + +For local development, install the package in editable mode with the dev dependencies: +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements-dev.txt +python -m api.db.init_db +pytest -q +``` + +For the full PDF template and Ollama-backed workflow, install the optional AI extras as well: +```bash +pip install -r requirements-ai.txt +``` + ### ✨ Key Features - **Agnostic:** Works with any department's existing fillable PDF forms. - **AI-Powered:** Uses open-source, locally-run LLMs (Mistral) to extract data from natural language. No data ever needs to leave the local machine. diff --git a/docker-compose.yml b/docker-compose.yml index 203c36c..53f2189 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,7 +27,7 @@ services: - .:/app environment: - PYTHONUNBUFFERED=1 - - PYTHONPATH=/app/src + - PYTHONPATH=/app - OLLAMA_HOST=http://ollama:11434 networks: - fireform-network diff --git a/docs/db.md b/docs/db.md index 4d702be..2a593cf 100644 --- a/docs/db.md +++ b/docs/db.md @@ -7,7 +7,12 @@ This guide explains how to set up, initialize, and manage the FireForm database. > [!IMPORTANT] > Ensure you have installed all dependencies before proceeding: > ```bash -> pip install -r requirements.txt +> pip install -r requirements-dev.txt +> ``` +> +> Install the optional AI/PDF extras only if you want template creation and the Ollama-backed fill flow: +> ```bash +> pip install -r requirements-ai.txt > ``` ## Database Setup @@ -32,6 +37,14 @@ uvicorn api.main:app --reload If successful, you will see: `INFO: Uvicorn running on http://127.0.0.1:8000` +## Running Tests + +The package is installed in editable mode by `requirements-dev.txt`, so tests can be run from the project root without setting `PYTHONPATH` manually: + +```bash +pytest -q +``` + ## Testing Endpoints 1. Open your browser and go to [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs). diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e26237f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "fireform" +version = "0.1.0" +description = "FireForm backend and PDF processing pipeline" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "fastapi", + "pdfrw", + "pydantic", + "requests", + "sqlmodel", + "uvicorn", +] + +[tool.setuptools.packages.find] +include = ["api*", "src*"] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..5ee6477 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +testpaths = tests diff --git a/requirements-ai.txt b/requirements-ai.txt new file mode 100644 index 0000000..7c252cf --- /dev/null +++ b/requirements-ai.txt @@ -0,0 +1,3 @@ +commonforms +numpy<2 +ollama diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..b04f8e3 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +-e . +httpx +pytest diff --git a/requirements.txt b/requirements.txt index eaa6c81..9c558e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1 @@ -requests -pdfrw -flask -commonforms -fastapi -uvicorn -pydantic -sqlmodel -pytest -httpx -numpy<2 -ollama \ No newline at end of file +. diff --git a/src/file_manipulator.py b/src/file_manipulator.py index b7815cc..8bf03b3 100644 --- a/src/file_manipulator.py +++ b/src/file_manipulator.py @@ -1,7 +1,6 @@ import os from src.filler import Filler from src.llm import LLM -from commonforms import prepare_form class FileManipulator: @@ -13,6 +12,14 @@ def create_template(self, pdf_path: str): """ By using commonforms, we create an editable .pdf template and we store it. """ + try: + from commonforms import prepare_form + except ImportError as exc: + raise RuntimeError( + "Template creation requires optional AI dependencies. " + "Install them with `pip install -r requirements-ai.txt`." + ) from exc + template_path = pdf_path[:-4] + "_template.pdf" prepare_form(pdf_path, template_path) return template_path diff --git a/tests/conftest.py b/tests/conftest.py index 7cb4db3..be79dee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,34 +6,37 @@ from api.main import app from api.deps import get_db -from api.db.models import Template, FormSubmission # In-memory SQLite database for tests TEST_DATABASE_URL = "sqlite://" -engine = create_engine( - TEST_DATABASE_URL, - connect_args={"check_same_thread": False}, - poolclass=StaticPool, -) +@pytest.fixture +def engine(): + engine = create_engine( + TEST_DATABASE_URL, + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + SQLModel.metadata.create_all(engine) + yield engine + SQLModel.metadata.drop_all(engine) -def override_get_db(): +@pytest.fixture +def db_session(engine): with Session(engine) as session: yield session -# Apply dependency override -app.dependency_overrides[get_db] = override_get_db - +@pytest.fixture +def client(engine): + def override_get_db(): + with Session(engine) as session: + yield session -@pytest.fixture(scope="session", autouse=True) -def create_test_db(): - SQLModel.metadata.create_all(engine) - yield - SQLModel.metadata.drop_all(engine) + app.dependency_overrides[get_db] = override_get_db + with TestClient(app) as test_client: + yield test_client -@pytest.fixture -def client(): - return TestClient(app) + app.dependency_overrides.clear() diff --git a/tests/test_forms.py b/tests/test_forms.py index 8f432bf..cab356c 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -1,25 +1,31 @@ -def test_submit_form(client): - pass - # First create a template - # form_payload = { - # "template_id": 3, - # "input_text": "Hi. The employee's name is John Doe. His job title is managing director. His department supervisor is Jane Doe. His phone number is 123456. His email is jdoe@ucsc.edu. The signature is , and the date is 01/02/2005", - # } +from api.db.models import Template +from src.controller import Controller - # template_res = client.post("/templates/", json=template_payload) - # template_id = template_res.json()["id"] - # # Submit a form - # form_payload = { - # "template_id": template_id, - # "data": {"rating": 5, "comment": "Great service"}, - # } +def test_submit_form(client, db_session, monkeypatch): + template = Template( + name="Template 1", + fields={"Incident": "string"}, + pdf_path="src/inputs/file_template.pdf", + ) + db_session.add(template) + db_session.commit() + db_session.refresh(template) - # response = client.post("/forms/", json=form_payload) + monkeypatch.setattr( + Controller, + "fill_form", + lambda self, user_input, fields, pdf_form_path: "src/outputs/generated.pdf", + ) - # assert response.status_code == 200 + response = client.post( + "/forms/fill", + json={ + "template_id": template.id, + "input_text": "Incident details", + }, + ) - # data = response.json() - # assert data["id"] is not None - # assert data["template_id"] == template_id - # assert data["data"] == form_payload["data"] + assert response.status_code == 200 + assert response.json()["template_id"] == template.id + assert response.json()["output_pdf_path"] == "src/outputs/generated.pdf" diff --git a/tests/test_templates.py b/tests/test_templates.py index bbced2b..9745d23 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -1,4 +1,14 @@ -def test_create_template(client): +import sys +import types + + +def test_create_template(client, monkeypatch): + monkeypatch.setitem( + sys.modules, + "commonforms", + types.SimpleNamespace(prepare_form=lambda src, dest: dest), + ) + payload = { "name": "Template 1", "pdf_path": "src/inputs/file.pdf", @@ -16,3 +26,5 @@ def test_create_template(client): response = client.post("/templates/create", json=payload) assert response.status_code == 200 + assert response.json()["name"] == payload["name"] + assert response.json()["pdf_path"].endswith("_template.pdf")