From d798bfbe023f380590f4e7c9f3a0510a10e4afb0 Mon Sep 17 00:00:00 2001 From: Dushyant Acharya Date: Tue, 31 Mar 2026 23:51:36 +0530 Subject: [PATCH 1/3] 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/3] 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/3] 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)