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