From d798bfbe023f380590f4e7c9f3a0510a10e4afb0 Mon Sep 17 00:00:00 2001 From: Dushyant Acharya Date: Tue, 31 Mar 2026 23:51:36 +0530 Subject: [PATCH 1/8] fix: import Union in main.py and correct pytest directory in Makefile --- Makefile | 2 +- src/main.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 53eb56a..a0d5555 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 tests/ clean: docker compose down -v diff --git a/src/main.py b/src/main.py index 5bb632b..a17ecb1 100644 --- a/src/main.py +++ b/src/main.py @@ -1,4 +1,5 @@ import os +from typing import Union # from backend import Fill from commonforms import prepare_form from pypdf import PdfReader From 6fd3496758575afec3d3f1759998d3efd985672d Mon Sep 17 00:00:00 2001 From: Dushyant Acharya Date: Wed, 1 Apr 2026 00:28:48 +0530 Subject: [PATCH 2/8] feat(pdf): Map Boolean Checkbox and Radio States & Fix main.py NameError Implemented PDF /Btn dictionary parsing in filler.py to extract and dynamically map truthy LLM outputs to their specific 'ON' Appearance Mode instead of blindly appending strings. Also resolved broken backend pipeline in main.py by initializing the base Controller instead of the removed Fill class. --- src/filler.py | 28 ++++++++++++++++++++++++++-- src/main.py | 7 ++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/filler.py b/src/filler.py index e31e535..4073799 100644 --- a/src/filler.py +++ b/src/filler.py @@ -39,8 +39,32 @@ def fill_form(self, pdf_form: str, llm: LLM): for annot in sorted_annots: if annot.Subtype == "/Widget" and annot.T: if i < len(answers_list): - annot.V = f"{answers_list[i]}" - annot.AP = None + answer = answers_list[i] + + # Check if the field type is a Button (Checkbox/Radio) + field_type = annot.FT if annot.FT else (annot.Parent.FT if annot.Parent else None) + if str(field_type) == "/Btn": + is_truthy = str(answer).lower() in ["yes", "true", "1", "x", "on"] + + # Find the 'ON' state from the appearance dictionary + on_state = "/Yes" # Default assumption + if annot.AP and annot.AP.N: + keys = [k for k in annot.AP.N.keys() if k != "/Off"] + if keys: + on_state = keys[0] + + if is_truthy: + from pdfrw import PdfName + annot.V = PdfName(on_state.strip("/")) + annot.AS = PdfName(on_state.strip("/")) + else: + from pdfrw import PdfName + annot.V = PdfName("Off") + annot.AS = PdfName("Off") + else: + annot.V = f"{answer}" + annot.AP = None + i += 1 else: # Stop if we run out of answers diff --git a/src/main.py b/src/main.py index a17ecb1..8ed7e9e 100644 --- a/src/main.py +++ b/src/main.py @@ -31,10 +31,11 @@ def run_pdf_fill_process(user_input: str, definitions: list, pdf_form_path: Unio print("[3] Starting extraction and PDF filling process...") try: - output_name = Fill.fill_form( + controller = Controller() + output_name = controller.fill_form( user_input=user_input, - definitions=definitions, - pdf_form=pdf_form_path + fields=definitions, + pdf_form_path=pdf_form_path ) print("\n----------------------------------") From 9feeb786dee88cfe95ae17c831f19f039c4a0377 Mon Sep 17 00:00:00 2001 From: Dushyant Acharya Date: Sat, 16 May 2026 04:20:11 +0530 Subject: [PATCH 3/8] feat(pdf): enforce strict boolean typing for checkbox/radio fields Per maintainer feedback in #407: - The fields dict now accepts Python types as values (e.g. {'is awake': bool}) - build_prompt() detects bool fields and explicitly instructs the LLM to return only the literal string 'True' or 'False', not fuzzy values - add_response_to_json() strictly coerces LLM output to Python bool for bool fields, logging a warning if an unexpected value is returned - filler.py now uses isinstance(answer, bool) instead of string matching so only a guaranteed Python True activates a checkbox/radio button - Updated example in main.py to demonstrate the new typed fields dict --- src/filler.py | 7 +++++-- src/llm.py | 54 +++++++++++++++++++++++++++++++++++++++++---------- src/main.py | 21 +++++++++++--------- 3 files changed, 61 insertions(+), 21 deletions(-) diff --git a/src/filler.py b/src/filler.py index 154f4da..cb374e7 100644 --- a/src/filler.py +++ b/src/filler.py @@ -44,10 +44,13 @@ def fill_form(self, pdf_form: str, llm: LLM): # Check if the field type is a Button (Checkbox/Radio) field_type = annot.FT if annot.FT else (annot.Parent.FT if annot.Parent else None) if str(field_type) == "/Btn": - is_truthy = str(answer).lower() in ["yes", "true", "1", "x", "on"] + # The LLM pipeline guarantees Python bool for boolean fields. + # We check isinstance(answer, bool) so only an explicit True + # activates the button — no fuzzy string matching needed. + is_truthy = isinstance(answer, bool) and answer # Find the 'ON' state from the appearance dictionary - on_state = "/Yes" # Default assumption + on_state = "/Yes" # Default assumption if annot.AP and annot.AP.N: keys = [k for k in annot.AP.N.keys() if k != "/Off"] if keys: diff --git a/src/llm.py b/src/llm.py index 1d5985f..b724727 100644 --- a/src/llm.py +++ b/src/llm.py @@ -10,15 +10,30 @@ def __init__(self, transcript_text: str=None, target_fields: list=None, json_dic self._target_fields = target_fields self._json = json_dict if json_dict is not None else {} - def build_prompt(self, current_field: str): + def build_prompt(self, current_field: str, field_type: type = str): """ - This method is in charge of the prompt engineering. It creates a specific prompt for each target field. - @params: current_field -> represents the current element of the json that is being prompted. + This method is in charge of the prompt engineering. It creates a specific prompt + for each target field, taking into account the expected field type. + + If the field type is `bool`, the LLM is explicitly instructed to return only + the literal string `True` or `False` — no fuzzy values like 'yes' or '1'. + + @params: + current_field -> the name of the JSON field to extract. + field_type -> the expected Python type (e.g. str, bool). """ prompt_path = os.path.join(os.path.dirname(__file__), "prompt.txt") with open(prompt_path, "r") as f: template = f.read() + if field_type is bool: + bool_instruction = ( + "\nIMPORTANT: This field is a boolean. " + "You MUST respond with ONLY the literal word True or False. " + "Do not use 'yes', 'no', '1', '0', or any other value." + ) + return template.format(field=current_field, text=self._transcript_text) + bool_instruction + return template.format(field=current_field, text=self._transcript_text) def main_loop(self): @@ -27,7 +42,8 @@ def main_loop(self): total_fields = len(self._target_fields) for i, field in enumerate(self._target_fields.keys(), 1): - prompt = self.build_prompt(field) + field_type = self._target_fields[field] if isinstance(self._target_fields[field], type) else str + prompt = self.build_prompt(field, field_type=field_type) ollama_host = os.getenv("OLLAMA_HOST", "http://localhost:11434").rstrip("/") ollama_url = f"{ollama_host}/api/generate" @@ -73,17 +89,35 @@ def main_loop(self): def add_response_to_json(self, field: str, value: str): """ - this method adds the following value under the specified field, - or under a new field if the field doesn't exist, to the json dict + Adds the LLM response under the specified field in the JSON dict. + + If the field type in _target_fields is `bool`, the response is strictly + coerced: only the literal strings 'True' and 'False' (case-insensitive) + are accepted. Any other value is treated as None (unanswered). """ value = value.strip().replace('"', "") parsed_value = None - if value != "-1": - parsed_value = value + # Determine expected type for this field + field_type = self._target_fields.get(field) if isinstance(self._target_fields, dict) else str + if not isinstance(field_type, type): + field_type = str + + if field_type is bool: + # Strictly enforce True/False — no fuzzy matching + if value.lower() == "true": + parsed_value = True + elif value.lower() == "false": + parsed_value = False + else: + print(f"[WARN]: Boolean field '{field}' received unexpected value '{value}'. Defaulting to None.") + parsed_value = None + else: + if value != "-1": + parsed_value = value - if ";" in value: - parsed_value = self.handle_plural_values(value) + if ";" in value: + parsed_value = self.handle_plural_values(value) if field in self._json.keys(): self._json[field].append(parsed_value) diff --git a/src/main.py b/src/main.py index 3a2277b..8cfdd22 100644 --- a/src/main.py +++ b/src/main.py @@ -64,15 +64,18 @@ def run_pdf_fill_process(user_input: str, definitions: list, pdf_form_path: Unio if __name__ == "__main__": file = "./src/inputs/file.pdf" user_input = "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" - fields = [ - "Employee's name", - "Employee's job title", - "Employee's department supervisor", - "Employee's phone number", - "Employee's email", - "Signature", - "Date", - ] + # Fields dict maps each field name to its expected Python type. + # Use `bool` for checkbox/radio fields so the LLM is instructed to + # return exactly True or False instead of fuzzy strings like "yes". + fields = { + "Employee's name": str, + "Employee's job title": str, + "Employee's department supervisor": str, + "Employee's phone number": str, + "Employee's email": str, + "Signature": str, + "Date": str, + } prepared_pdf = "temp_outfile.pdf" prepare_form(file, prepared_pdf) From a10663d8f24ced0370e6c6b089c137678be4c00b Mon Sep 17 00:00:00 2001 From: Dushyant Acharya Date: Tue, 26 May 2026 09:20:30 +0530 Subject: [PATCH 4/8] fix: add strict boolean schema validation and retry loop --- src/llm.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/llm.py b/src/llm.py index 2efbd7d..40a8fd5 100644 --- a/src/llm.py +++ b/src/llm.py @@ -30,9 +30,9 @@ def build_prompt(self, current_field: str, field_type: type = str): if field_type is bool: bool_instruction = ( - "\nIMPORTANT: This field is a boolean. " + "\nIMPORTANT: This field is a boolean (checkbox or radio button). " "You MUST respond with ONLY the literal word True or False. " - "Do not use 'yes', 'no', '1', '0', or any other value." + "Do not use 'Checked', 'yes', 'no', '1', '0', 'X', or any other value." ) return template.format(field=current_field, type=current_type, text=self._transcript_text) + bool_instruction @@ -61,7 +61,17 @@ def main_loop(self): try: response = requests.post(ollama_url, json=payload, timeout=timeout) response.raise_for_status() - json_data = response.json() + + temp_json_data = response.json() + parsed_response = temp_json_data["response"].strip().replace('"', "") + + if field_type is bool: + if parsed_response.lower() not in ["true", "false"]: + print(f"[WARN]: LLM returned unexpected boolean value '{parsed_response}' for field '{field}' (attempt {attempt+1}). Retrying...") + payload["prompt"] += "\nERROR: Your previous response was invalid. You MUST respond with ONLY the literal word True or False." + continue + + json_data = temp_json_data break except Timeout: print(f"[LOG]: Ollama request timed out (attempt {attempt+1}) for field '{field}'. Retrying...") From b27e5b794f3bc931588023ce7a118c125a9b7692 Mon Sep 17 00:00:00 2001 From: Dushyant Acharya Date: Sat, 4 Jul 2026 14:12:48 +0530 Subject: [PATCH 5/8] refactor: replace Union with modern syntax and remove unused method call --- src/llm.py | 2 +- src/main.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/llm.py b/src/llm.py index ec1a3e6..8348868 100644 --- a/src/llm.py +++ b/src/llm.py @@ -132,7 +132,7 @@ def add_response_to_json(self, field: str, value: str): parsed_value = value if ";" in value: - parsed_value = self.handle_plural_values(value) + pass if field in self._json.keys(): self._json[field].append(parsed_value) else: diff --git a/src/main.py b/src/main.py index 8cfdd22..36d6efa 100644 --- a/src/main.py +++ b/src/main.py @@ -1,4 +1,3 @@ -from typing import Union import os os.environ["CUDA_VISIBLE_DEVICES"] = "" @@ -25,7 +24,7 @@ def input_fields(num_fields: int): fields.append(field) return fields -def run_pdf_fill_process(user_input: str, definitions: list, pdf_form_path: Union[str, os.PathLike]): +def run_pdf_fill_process(user_input: str, definitions: list, pdf_form_path: str | os.PathLike): """ This function is called by the frontend server. It receives the raw data, runs the PDF filling logic, From 0a959b04e595c52f16af78b7777fb35d1af8af59 Mon Sep 17 00:00:00 2001 From: Dushyant Acharya Date: Sat, 4 Jul 2026 14:47:44 +0530 Subject: [PATCH 6/8] fix: remove trailing whitespace in main.py --- src/main.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main.py b/src/main.py index 36d6efa..e9c79ae 100644 --- a/src/main.py +++ b/src/main.py @@ -13,7 +13,7 @@ def patched_ensure(model_ctx): except ImportError: pass -from commonforms import prepare_form +from commonforms import prepare_form from pypdf import PdfReader from controller import Controller @@ -30,13 +30,13 @@ def run_pdf_fill_process(user_input: str, definitions: list, pdf_form_path: str It receives the raw data, runs the PDF filling logic, and returns the path to the newly created file. """ - + print("[1] Received request from frontend.") print(f"[2] PDF template path: {pdf_form_path}") - + # Normalize Path/PathLike to a plain string for downstream code pdf_form_path = os.fspath(pdf_form_path) - + if not os.path.exists(pdf_form_path): print(f"Error: PDF template not found at {pdf_form_path}") return None # Or raise an exception @@ -49,16 +49,16 @@ def run_pdf_fill_process(user_input: str, definitions: list, pdf_form_path: str fields=definitions, pdf_form_path=pdf_form_path ) - + print("\n----------------------------------") print(f"✅ Process Complete.") print(f"Output saved to: {output_name}") - + return output_name - + except Exception as e: print(f"An error occurred during PDF generation: {e}") - # Re-raise the exception so the frontend can handle it + # Re-raise the exception so the frontend can handle i raise e if __name__ == "__main__": file = "./src/inputs/file.pdf" From 19b16c2a1736c95396f8464b7bd27d6e090add14 Mon Sep 17 00:00:00 2001 From: Dushyant Acharya Date: Mon, 13 Jul 2026 22:04:34 +0530 Subject: [PATCH 7/8] chore: fix f-string lint error --- .gitignore | 3 ++- src/main.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index b5a9a90..8a405e8 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ src/inputs/*.pdf frontend/release/ # Local Claude Code instructions -CLAUDE.md \ No newline at end of file +CLAUDE.md +.aider* diff --git a/src/main.py b/src/main.py index e9c79ae..ed7f6dd 100644 --- a/src/main.py +++ b/src/main.py @@ -51,7 +51,7 @@ def run_pdf_fill_process(user_input: str, definitions: list, pdf_form_path: str ) print("\n----------------------------------") - print(f"✅ Process Complete.") + print("✅ Process Complete.") print(f"Output saved to: {output_name}") return output_name From f4a21eb79002651e0691916eae8be70f8d66be64 Mon Sep 17 00:00:00 2001 From: Dushyant Acharya Date: Tue, 28 Jul 2026 12:37:24 +0530 Subject: [PATCH 8/8] fix: resolve ruff linter errors --- src/controller.py | 3 ++- src/file_manipulator.py | 5 +++-- src/filler.py | 9 ++++++--- src/llm.py | 8 ++++---- src/main.py | 9 ++++----- src/test/test_model.py | 2 +- tests/conftest.py | 8 ++++---- tests/test_api.py | 4 ++-- 8 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/controller.py b/src/controller.py index 0e19290..26c8ba6 100644 --- a/src/controller.py +++ b/src/controller.py @@ -1,10 +1,11 @@ from src.file_manipulator import FileManipulator + class Controller: def __init__(self): self.file_manipulator = FileManipulator() - def fill_form(self, user_input: str, fields: list, pdf_form_path: str, model: str = None): + def fill_form(self, user_input: str, fields: list, pdf_form_path: str, model: str | None = None): return self.file_manipulator.fill_form(user_input, fields, pdf_form_path, model=model) def prepare_fillable(self, pdf_path: str): diff --git a/src/file_manipulator.py b/src/file_manipulator.py index 8d1f3a0..71aad22 100644 --- a/src/file_manipulator.py +++ b/src/file_manipulator.py @@ -1,4 +1,5 @@ import os + from src.filler import Filler from src.llm import LLM @@ -34,7 +35,7 @@ def patched_ensure(model_ctx): prepare_form(pdf_path, template_path) return template_path - def fill_form(self, user_input: str, fields: list, pdf_form_path: str, model: str = None): + def fill_form(self, user_input: str, fields: list, pdf_form_path: str, model: str | None = None): """ It receives the raw data, runs the PDF filling logic, and returns the path to the newly created file. @@ -60,4 +61,4 @@ def fill_form(self, user_input: str, fields: list, pdf_form_path: str, model: st except Exception as e: print(f"An error occurred during PDF generation: {e}") - raise e + raise diff --git a/src/filler.py b/src/filler.py index cb374e7..88157ed 100644 --- a/src/filler.py +++ b/src/filler.py @@ -1,6 +1,9 @@ +from datetime import datetime, timezone + + from pdfrw import PdfReader, PdfWriter + from src.llm import LLM -from datetime import datetime class Filler: @@ -15,7 +18,7 @@ def fill_form(self, pdf_form: str, llm: LLM): output_pdf = ( pdf_form[:-4] + "_" - + datetime.now().strftime("%Y%m%d_%H%M%S") + + datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") + "_filled.pdf" ) @@ -52,7 +55,7 @@ def fill_form(self, pdf_form: str, llm: LLM): # Find the 'ON' state from the appearance dictionary on_state = "/Yes" # Default assumption if annot.AP and annot.AP.N: - keys = [k for k in annot.AP.N.keys() if k != "/Off"] + keys = [k for k in annot.AP.N if k != "/Off"] if keys: on_state = keys[0] diff --git a/src/llm.py b/src/llm.py index 8348868..b7798a2 100644 --- a/src/llm.py +++ b/src/llm.py @@ -1,11 +1,12 @@ import json import os + import requests -from requests.exceptions import Timeout, RequestException +from requests.exceptions import RequestException, Timeout class LLM: - def __init__(self, transcript_text: str=None, target_fields: list=None, json_dict: dict=None, model: str=None): + def __init__(self, transcript_text: str | None=None, target_fields: list | None=None, json_dict: dict | None=None, model: str | None=None): self._transcript_text = transcript_text self._target_fields = target_fields self._json = json_dict if json_dict is not None else {} @@ -133,12 +134,11 @@ def add_response_to_json(self, field: str, value: str): if ";" in value: pass - if field in self._json.keys(): + if field in self._json: self._json[field].append(parsed_value) else: self._json[field] = parsed_value - return def get_data(self): diff --git a/src/main.py b/src/main.py index ed7f6dd..9d1704e 100644 --- a/src/main.py +++ b/src/main.py @@ -15,8 +15,10 @@ def patched_ensure(model_ctx): from commonforms import prepare_form from pypdf import PdfReader + from controller import Controller + def input_fields(num_fields: int): fields = [] for i in range(num_fields): @@ -59,7 +61,7 @@ def run_pdf_fill_process(user_input: str, definitions: list, pdf_form_path: str except Exception as e: print(f"An error occurred during PDF generation: {e}") # Re-raise the exception so the frontend can handle i - raise e + raise if __name__ == "__main__": file = "./src/inputs/file.pdf" user_input = "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" @@ -80,10 +82,7 @@ def run_pdf_fill_process(user_input: str, definitions: list, pdf_form_path: str reader = PdfReader(prepared_pdf) fields = reader.get_fields() - if fields: - num_fields = len(fields) - else: - num_fields = 0 + num_fields = len(fields) if fields else 0 controller = Controller() controller.fill_form(user_input, fields, file) diff --git a/src/test/test_model.py b/src/test/test_model.py index 6de314f..3fcfb0a 100644 --- a/src/test/test_model.py +++ b/src/test/test_model.py @@ -8,5 +8,5 @@ {'role': 'user', 'content': 'Say hello in Spanish'} ]) print("Success! Response:", response['message']['content']) -except Exception as e: +except Exception as e: # noqa: BLE001 print("Error:", e) diff --git a/tests/conftest.py b/tests/conftest.py index 5f1eb3f..21cab5c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,16 +5,16 @@ """ import io -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest from fastapi.testclient import TestClient from sqlalchemy.pool import StaticPool -from sqlmodel import SQLModel, Session, create_engine +from sqlmodel import Session, SQLModel, create_engine -from api.main import app +from api.db.models import FormSubmission, Template from api.deps import get_db -from api.db.models import Template, FormSubmission # noqa: F401 — registers tables +from api.main import app # --------------------------------------------------------------------------- # In-memory database diff --git a/tests/test_api.py b/tests/test_api.py index 89a9b25..d91dd3d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -7,8 +7,7 @@ import pytest from sqlmodel import select -from api.db.models import Template, FormSubmission - +from api.db.models import FormSubmission, Template # ═══════════════════════════════════════════════════════════════════════════ # DB model sanity @@ -292,6 +291,7 @@ def test_fill_form_passes_model_override(self, client, mock_controller): def test_transcribe_service_unavailable(self, client, monkeypatch): """A down whisper service surfaces as a 503, not a 500.""" import io + import requests def fake_post(*args, **kwargs):