REPORT
ONCE.
+ Describe any incident in plain language. FireForm uses a locally-running AI to extract every relevant detail and auto-fill all required agency forms — instantly and privately.
+No data leaves your machine.
diff --git a/.gitignore b/.gitignore index 7fa2022..e81a1db 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .idea venv .venv -*.db \ No newline at end of file +*.db +src/inputs/*.pdf \ No newline at end of file diff --git a/api/db/repositories.py b/api/db/repositories.py index 6608718..7686510 100644 --- a/api/db/repositories.py +++ b/api/db/repositories.py @@ -16,4 +16,10 @@ def create_form(session: Session, form: FormSubmission) -> FormSubmission: session.add(form) session.commit() session.refresh(form) - return form \ No newline at end of file + return form + +def get_all_templates(session: Session, limit: int = 100, offset: int = 0) -> list[Template]: + return session.exec(select(Template).offset(offset).limit(limit)).all() + +def get_form(session: Session, submission_id: int) -> FormSubmission | None: + return session.get(FormSubmission, submission_id) \ No newline at end of file diff --git a/api/main.py b/api/main.py index d0b8c79..0a7d8e7 100644 --- a/api/main.py +++ b/api/main.py @@ -1,7 +1,25 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse from api.routes import templates, forms +from api.errors.base import AppError +from typing import Union app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +@app.exception_handler(AppError) +def app_error_handler(request: Request, exc: AppError): + return JSONResponse( + status_code=exc.status_code, + content={"detail": exc.message} + ) + app.include_router(templates.router) app.include_router(forms.router) \ No newline at end of file diff --git a/api/routes/forms.py b/api/routes/forms.py index f3430ed..3491d4e 100644 --- a/api/routes/forms.py +++ b/api/routes/forms.py @@ -1,25 +1,82 @@ +import os from fastapi import APIRouter, Depends +from fastapi.responses import FileResponse from sqlmodel import Session from api.deps import get_db from api.schemas.forms import FormFill, FormFillResponse -from api.db.repositories import create_form, get_template +from api.db.repositories import create_form, get_template, get_form from api.db.models import FormSubmission from api.errors.base import AppError from src.controller import Controller router = APIRouter(prefix="/forms", tags=["forms"]) + @router.post("/fill", response_model=FormFillResponse) def fill_form(form: FormFill, db: Session = Depends(get_db)): - if not get_template(db, form.template_id): + # Single DB query (fixes issue #149 - redundant query) + template = get_template(db, form.template_id) + if not template: raise AppError("Template not found", status_code=404) - fetched_template = get_template(db, form.template_id) + try: + controller = Controller() + # FileManipulator.fill_form expects fields as a list of key strings + fields_list = list(template.fields.keys()) if isinstance(template.fields, dict) else template.fields + path = controller.fill_form( + user_input=form.input_text, + fields=fields_list, + pdf_form_path=template.pdf_path + ) + except ConnectionError: + raise AppError( + "Could not connect to Ollama. Make sure ollama serve is running.", + status_code=503 + ) + except Exception as e: + raise AppError(f"PDF filling failed: {str(e)}", status_code=500) + + # Guard: controller returned None instead of a file path + if not path: + raise AppError( + "PDF generation failed — no output file was produced. " + "Check that the PDF template is a valid fillable form and Ollama is running.", + status_code=500 + ) - controller = Controller() - path = controller.fill_form(user_input=form.input_text, fields=fetched_template.fields, pdf_form_path=fetched_template.pdf_path) + if not os.path.exists(path): + raise AppError( + f"PDF was generated but file not found at: {path}", + status_code=500 + ) - submission = FormSubmission(**form.model_dump(), output_pdf_path=path) + submission = FormSubmission( + **form.model_dump(), + output_pdf_path=path + ) return create_form(db, submission) +@router.get("/{submission_id}", response_model=FormFillResponse) +def get_submission(submission_id: int, db: Session = Depends(get_db)): + submission = get_form(db, submission_id) + if not submission: + raise AppError("Submission not found", status_code=404) + return submission + + +@router.get("/download/{submission_id}") +def download_filled_pdf(submission_id: int, db: Session = Depends(get_db)): + submission = get_form(db, submission_id) + if not submission: + raise AppError("Submission not found", status_code=404) + + file_path = submission.output_pdf_path + if not os.path.exists(file_path): + raise AppError("PDF file not found on server", status_code=404) + + return FileResponse( + path=file_path, + media_type="application/pdf", + filename=os.path.basename(file_path) + ) \ No newline at end of file diff --git a/api/routes/templates.py b/api/routes/templates.py index 5c2281b..9419ae6 100644 --- a/api/routes/templates.py +++ b/api/routes/templates.py @@ -1,16 +1,89 @@ -from fastapi import APIRouter, Depends +import os +import shutil +import uuid +from fastapi import APIRouter, Depends, UploadFile, File, Form from sqlmodel import Session from api.deps import get_db -from api.schemas.templates import TemplateCreate, TemplateResponse -from api.db.repositories import create_template +from api.schemas.templates import TemplateResponse +from api.db.repositories import create_template, get_all_templates from api.db.models import Template -from src.controller import Controller +from api.errors.base import AppError router = APIRouter(prefix="/templates", tags=["templates"]) +# Save directly into src/inputs/ — stable location, won't get wiped +TEMPLATES_DIR = os.path.join("src", "inputs") +os.makedirs(TEMPLATES_DIR, exist_ok=True) + + @router.post("/create", response_model=TemplateResponse) -def create(template: TemplateCreate, db: Session = Depends(get_db)): - controller = Controller() - template_path = controller.create_template(template.pdf_path) - tpl = Template(**template.model_dump(exclude={"pdf_path"}), pdf_path=template_path) - return create_template(db, tpl) \ No newline at end of file +async def create( + name: str = Form(...), + file: UploadFile = File(...), + db: Session = Depends(get_db) +): + # Validate PDF + if not file.filename.endswith(".pdf"): + raise AppError("Only PDF files are allowed", status_code=400) + + # Save uploaded file with unique name into src/inputs/ + unique_name = f"{uuid.uuid4().hex}_{file.filename}" + save_path = os.path.join(TEMPLATES_DIR, unique_name) + + with open(save_path, "wb") as f: + shutil.copyfileobj(file.file, f) + + # Extract fields using commonforms + pypdf + # Store as simple list of field name strings — what Filler expects + try: + from commonforms import prepare_form + from pypdf import PdfReader + + # Read real field names directly from original PDF + # Use /T (internal name) as both key and label + # Real names like "JobTitle", "Phone Number" are already human-readable + reader = PdfReader(save_path) + raw_fields = reader.get_fields() or {} + + fields = {} + for internal_name, field_data in raw_fields.items(): + # Use /TU tooltip if available, otherwise prettify /T name + label = None + if isinstance(field_data, dict): + label = field_data.get("/TU") + if not label: + # Prettify: "JobTitle" → "Job Title", "DATE7_af_date" → "Date" + import re + label = re.sub(r'([a-z])([A-Z])', r'\1 \2', internal_name) + label = re.sub(r'_af_.*$', '', label) # strip "_af_date" suffix + label = label.replace('_', ' ').strip().title() + fields[internal_name] = label + + except Exception as e: + print(f"Field extraction failed: {e}") + fields = [] + + # Save to DB + tpl = Template(name=name, pdf_path=save_path, fields=fields) + return create_template(db, tpl) + + +@router.get("", response_model=list[TemplateResponse]) +def list_templates( + limit: int = 100, + offset: int = 0, + db: Session = Depends(get_db) +): + return get_all_templates(db, limit=limit, offset=offset) + + +@router.get("/{template_id}", response_model=TemplateResponse) +def get_template_by_id( + template_id: int, + db: Session = Depends(get_db) +): + from api.db.repositories import get_template + tpl = get_template(db, template_id) + if not tpl: + raise AppError("Template not found", status_code=404) + return tpl \ No newline at end of file diff --git a/docs/SETUP.md b/docs/SETUP.md new file mode 100644 index 0000000..cf47642 --- /dev/null +++ b/docs/SETUP.md @@ -0,0 +1,228 @@ +# 🔥 FireForm — Setup & Usage Guide + +This guide covers how to install, run, and use FireForm locally on Windows, Linux, and macOS. + +--- + +## 📋 Prerequisites + +| Tool | Version | Purpose | +|------|---------|---------| +| Python | 3.11+ | Backend runtime | +| Ollama | 0.17.7+ | Local LLM server | +| Mistral 7B | latest | AI extraction model | +| Git | any | Clone the repository | + +--- + +## 🪟 Windows + +### 1. Clone the repository +```cmd +git clone https://github.com/fireform-core/FireForm.git +cd FireForm +``` + +### 2. Create and activate virtual environment +```cmd +python -m venv venv +venv\Scripts\activate +``` + +### 3. Install dependencies +```cmd +pip install -r requirements.txt +``` + +### 4. Install and start Ollama +Download Ollama from https://ollama.com/download/windows + +Then pull the Mistral model: +```cmd +ollama pull mistral +ollama serve +``` + +> Ollama runs on `http://localhost:11434` by default. Keep this terminal open. + +### 5. Initialize the database +```cmd +python -m api.db.init_db +``` + +### 6. Start the API server +```cmd +uvicorn api.main:app --reload +``` + +API is now running at `http://127.0.0.1:8000` + +### 7. Start the frontend +Open a new terminal: +```cmd +cd frontend +python -m http.server 3000 +``` + +Open `http://localhost:3000` in your browser. + +--- + + +## 🍎 macOS + +### 1. Clone and enter the repository +```bash +git clone https://github.com/fireform-core/FireForm.git +cd FireForm +``` + +### 2. Create and activate virtual environment +```bash +python3 -m venv venv +source venv/bin/activate +``` + +### 3. Install dependencies +```bash +pip install -r requirements.txt +``` + +### 4. Install and start Ollama +Download from https://ollama.com/download/mac or: +```bash +brew install ollama +ollama pull mistral +ollama serve & +``` + +### 5. Initialize the database +```bash +python -m api.db.init_db +``` + +### 6. Start the API server +```bash +uvicorn api.main:app --reload +``` + +### 7. Start the frontend +```bash +cd frontend +python3 -m http.server 3000 +``` + +--- + +## 🖥️ Using the Frontend + +Once everything is running, open `http://localhost:3000` in your browser. + +### Step 1 — Upload a PDF template +- Click **"Choose File"** and select any fillable PDF form +- Enter a name for the template +- Click **"Upload Template"** + +FireForm will automatically extract all form field names and their human-readable labels. + +### Step 2 — Fill the form +- Select your uploaded template from the dropdown +- In the text box, describe the incident or enter the information in natural language: + +``` +Employee name is John Smith. Employee ID is EMP-2024-789. +Job title is Firefighter Paramedic. Location is Station 12 Sacramento. +Department is Emergency Medical Services. Supervisor is Captain Rodriguez. +Phone number is 916-555-0147. +``` + +- Click **"Fill Form"** + +FireForm sends one request to Ollama (Mistral) which extracts all fields at once and returns structured JSON. + +### Step 3 — Download the filled PDF +- Click **"Download PDF"** to save the completed form + +--- + +## 🤖 How AI Extraction Works + +FireForm uses a **batch extraction** approach: + +``` +Traditional approach (slow): FireForm approach (fast): + Field 1 → Ollama call All fields → 1 Ollama call + Field 2 → Ollama call Mistral returns JSON with all values + Field 3 → Ollama call Parse → fill PDF + ...N calls total 1 call total (O(1)) +``` + +Field names are automatically read from the PDF's annotations and converted to human-readable labels before being sent to Mistral — so the model understands what each field means regardless of internal PDF naming conventions like `textbox_0_0`. + +**Example extraction:** +```json +{ + "NAME/SID": "John Smith", + "JobTitle": "Firefighter Paramedic", + "Department": "Emergency Medical Services", + "Phone Number": "916-555-0147", + "email": null +} +``` + +--- + +## 🧪 Running Tests + +```bash +python -m pytest tests/ -v +``` + +Expected output: **52 passed** + +See [TESTING.md](TESTING.md) for full test coverage details. + +--- + +## 🔧 Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `OLLAMA_HOST` | `http://localhost:11434` | Ollama server URL | + +To use a remote Ollama instance: +```bash +export OLLAMA_HOST=http://your-server:11434 # Linux/Mac +set OLLAMA_HOST=http://your-server:11434 # Windows +``` + +--- + +## 🐳 Docker (Coming Soon) + +Docker support is in progress. See [docker.md](docker.md) for current status. + +--- + +## ❓ Troubleshooting + +**`Form data requires python-multipart`** +```bash +pip install python-multipart +``` + +**`ModuleNotFoundError: No module named 'pypdf'`** +```bash +pip install pypdf +``` + +**`Could not connect to Ollama`** +- Make sure `ollama serve` is running +- Check Ollama is on port 11434: `curl http://localhost:11434` + +**`NameError: name 'Union' is not defined`** +- Pull latest changes: `git pull origin main` +- This bug is fixed in the current version + +**Tests fail with `ModuleNotFoundError: No module named 'api'`** +- Use `python -m pytest` instead of `pytest` \ No newline at end of file diff --git a/docs/demo/filled_form_output.pdf b/docs/demo/filled_form_output.pdf new file mode 100644 index 0000000..6587e43 Binary files /dev/null and b/docs/demo/filled_form_output.pdf differ diff --git a/docs/demo/frontend_ui.png b/docs/demo/frontend_ui.png new file mode 100644 index 0000000..856c696 Binary files /dev/null and b/docs/demo/frontend_ui.png differ diff --git a/docs/demo/frontend_ui02.png b/docs/demo/frontend_ui02.png new file mode 100644 index 0000000..ca84a72 Binary files /dev/null and b/docs/demo/frontend_ui02.png differ diff --git a/docs/frontend.md b/docs/frontend.md new file mode 100644 index 0000000..22d2b55 --- /dev/null +++ b/docs/frontend.md @@ -0,0 +1,218 @@ +# Frontend UI Guide + +This guide explains how to set up and use the FireForm browser-based frontend interface. + +## Overview + +The FireForm frontend is a single-page web application (`frontend/index.html`) that provides a user-friendly interface for non-technical first responders to: + +- Upload and save fillable PDF form templates +- Describe incidents in plain language +- Auto-fill forms using local AI (Mistral via Ollama) +- Download completed PDF forms instantly + +> [!IMPORTANT] +> The frontend communicates with the FastAPI backend at `http://127.0.0.1:8000`. Make sure both Ollama and the API server are running before opening the frontend. + +--- + +## Prerequisites + +Before running the frontend, ensure the following are set up: + +> [!IMPORTANT] +> Complete the database setup described in [db.md](db.md) first. + +1. **Ollama** installed and running — [https://ollama.com/download](https://ollama.com/download) +2. **Mistral model** pulled: + ```bash + ollama pull mistral + ``` +3. **Dependencies** installed: + ```bash + pip install -r requirements.txt + ``` + +--- + +## Running the Frontend + +### Step 1 — Start Ollama + +In a terminal, run: + +```bash +ollama serve +``` + +> [!TIP] +> Leave this terminal open. Ollama must stay running for AI extraction to work. + +### Step 2 — Initialize the Database + +```bash +python -m api.db.init_db +``` + +### Step 3 — Start the API Server + +In a new terminal, from the project root: + +```bash +uvicorn api.main:app --reload +``` + +If successful, you will see: +`INFO: Uvicorn running on http://127.0.0.1:8000` + +### Step 4 — Open the Frontend + +Open `frontend/index.html` directly in your browser by double-clicking it, or navigate to it in your file explorer. + +> [!NOTE] +> No additional server is required for the frontend. It is a static HTML file that communicates directly with the FastAPI backend. + +--- + +## Using the Frontend + +The interface guides you through 4 steps: + +### Step 1 — Upload a Template + +1. Click **"Click to upload"** or drag and drop a fillable PDF form +2. Enter a name for the template (e.g. `Cal Fire Incident Report`) +3. Click **"SAVE TEMPLATE →"** + +The template is saved to the database and will appear in the **Saved Templates** list. + +> [!TIP] +> Any fillable PDF form works. The system automatically detects all form fields. + +### Step 2 — Select a Template + +Click any saved template from the **Saved Templates** list in the sidebar. The selected template will be highlighted in red. + +### Step 3 — Describe the Incident + +Type or paste a plain-language description of the incident in the text area. For best results, include all relevant details that match your form's fields. + +**Example for an employee form:** +``` +The employee's name is John Smith. His employee ID is EMP-2024-789. +His job title is Firefighter Paramedic. His location is Station 12, +Sacramento. His department is Emergency Medical Services. His supervisor +is Captain Jane Rodriguez. His phone number is 916-555-0147. +His email is jsmith@calfire.ca.gov. +``` + +**Example for an incident report form:** +``` +Officer Hernandez responding to a structure fire at 742 Evergreen Terrace. +Two occupants evacuated safely. Minor smoke inhalation treated on scene +by EMS. Unit 7 on scene at 14:32, cleared at 16:45. +Handed off to Deputy Martinez. +``` + +### Step 4 — Fill and Download + +Click **"⚡ FILL FORM"**. The system will: + +1. Send the description to Mistral (running locally via Ollama) +2. Extract all relevant field values +3. Fill the PDF template automatically +4. Provide a **"⬇ Download PDF"** button + +> [!NOTE] +> Processing time depends on your hardware. Typically 10–30 seconds with Mistral on a standard machine. + +--- + +## API Endpoints + +The frontend uses the following API endpoints: + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/templates/create` | Upload a new PDF template | +| `GET` | `/templates` | List all saved templates | +| `GET` | `/templates/{id}` | Get a specific template | +| `POST` | `/forms/fill` | Fill a form with incident text | +| `GET` | `/forms/{id}` | Get a submission record | +| `GET` | `/forms/download/{id}` | Download a filled PDF | + +For full API documentation, visit [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) while the server is running. + +--- + +## API Status Indicator + +The top-right corner of the frontend shows the API connection status: + +- 🟢 **api online** — Backend is reachable, ready to use +- 🔴 **api offline** — Backend is not running, check uvicorn + +--- + +## Troubleshooting + +### "api offline" shown in the top bar + +The FastAPI server is not running. Start it with: +```bash +uvicorn api.main:app --reload +``` + +### Form fills with null or incorrect values + +This happens when the incident description does not contain information matching the PDF form fields. Ensure your description includes the specific data your form requires (names, dates, locations, etc.). + +See [Issue #113](https://github.com/fireform-core/FireForm/issues/113) for context on matching input to templates. + +### "Could not connect to Ollama" error + +Ollama is not running. Start it with: +```bash +ollama serve +``` + +Then verify Mistral is available: +```bash +ollama list +``` + +If Mistral is not listed, pull it: +```bash +ollama pull mistral +``` + +### Port conflict on 11434 + +Something else is using Ollama's port. On Linux/Mac: +```bash +sudo lsof -i :11434 +``` +On Windows: +```cmd +netstat -ano | findstr :11434 +``` + +--- + +## Privacy + +> [!IMPORTANT] +> FireForm is designed to be fully private. All AI processing happens locally via Ollama. No incident data, form content, or personal information is ever sent to external servers. + +--- + +## Docker Usage + +To run the full stack including the frontend API via Docker: + +```bash +chmod +x container-init.sh +./container-init.sh +``` + +See [docker.md](docker.md) for full Docker setup instructions. diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..a3b0083 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,467 @@ + + +
+ + +Describe any incident in plain language. FireForm uses a locally-running AI to extract every relevant detail and auto-fill all required agency forms — instantly and privately.
+