Skip to content

Commit 9feeb78

Browse files
committed
feat(pdf): enforce strict boolean typing for checkbox/radio fields
Per maintainer feedback in fireform-core#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
1 parent 15122f6 commit 9feeb78

3 files changed

Lines changed: 61 additions & 21 deletions

File tree

‎src/filler.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,13 @@ def fill_form(self, pdf_form: str, llm: LLM):
4444
# Check if the field type is a Button (Checkbox/Radio)
4545
field_type = annot.FT if annot.FT else (annot.Parent.FT if annot.Parent else None)
4646
if str(field_type) == "/Btn":
47-
is_truthy = str(answer).lower() in ["yes", "true", "1", "x", "on"]
47+
# The LLM pipeline guarantees Python bool for boolean fields.
48+
# We check isinstance(answer, bool) so only an explicit True
49+
# activates the button — no fuzzy string matching needed.
50+
is_truthy = isinstance(answer, bool) and answer
4851

4952
# Find the 'ON' state from the appearance dictionary
50-
on_state = "/Yes" # Default assumption
53+
on_state = "/Yes" # Default assumption
5154
if annot.AP and annot.AP.N:
5255
keys = [k for k in annot.AP.N.keys() if k != "/Off"]
5356
if keys:

‎src/llm.py‎

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,30 @@ def __init__(self, transcript_text: str=None, target_fields: list=None, json_dic
1010
self._target_fields = target_fields
1111
self._json = json_dict if json_dict is not None else {}
1212

13-
def build_prompt(self, current_field: str):
13+
def build_prompt(self, current_field: str, field_type: type = str):
1414
"""
15-
This method is in charge of the prompt engineering. It creates a specific prompt for each target field.
16-
@params: current_field -> represents the current element of the json that is being prompted.
15+
This method is in charge of the prompt engineering. It creates a specific prompt
16+
for each target field, taking into account the expected field type.
17+
18+
If the field type is `bool`, the LLM is explicitly instructed to return only
19+
the literal string `True` or `False` — no fuzzy values like 'yes' or '1'.
20+
21+
@params:
22+
current_field -> the name of the JSON field to extract.
23+
field_type -> the expected Python type (e.g. str, bool).
1724
"""
1825
prompt_path = os.path.join(os.path.dirname(__file__), "prompt.txt")
1926
with open(prompt_path, "r") as f:
2027
template = f.read()
2128

29+
if field_type is bool:
30+
bool_instruction = (
31+
"\nIMPORTANT: This field is a boolean. "
32+
"You MUST respond with ONLY the literal word True or False. "
33+
"Do not use 'yes', 'no', '1', '0', or any other value."
34+
)
35+
return template.format(field=current_field, text=self._transcript_text) + bool_instruction
36+
2237
return template.format(field=current_field, text=self._transcript_text)
2338

2439
def main_loop(self):
@@ -27,7 +42,8 @@ def main_loop(self):
2742

2843
total_fields = len(self._target_fields)
2944
for i, field in enumerate(self._target_fields.keys(), 1):
30-
prompt = self.build_prompt(field)
45+
field_type = self._target_fields[field] if isinstance(self._target_fields[field], type) else str
46+
prompt = self.build_prompt(field, field_type=field_type)
3147
ollama_host = os.getenv("OLLAMA_HOST", "http://localhost:11434").rstrip("/")
3248
ollama_url = f"{ollama_host}/api/generate"
3349

@@ -73,17 +89,35 @@ def main_loop(self):
7389

7490
def add_response_to_json(self, field: str, value: str):
7591
"""
76-
this method adds the following value under the specified field,
77-
or under a new field if the field doesn't exist, to the json dict
92+
Adds the LLM response under the specified field in the JSON dict.
93+
94+
If the field type in _target_fields is `bool`, the response is strictly
95+
coerced: only the literal strings 'True' and 'False' (case-insensitive)
96+
are accepted. Any other value is treated as None (unanswered).
7897
"""
7998
value = value.strip().replace('"', "")
8099
parsed_value = None
81100

82-
if value != "-1":
83-
parsed_value = value
101+
# Determine expected type for this field
102+
field_type = self._target_fields.get(field) if isinstance(self._target_fields, dict) else str
103+
if not isinstance(field_type, type):
104+
field_type = str
105+
106+
if field_type is bool:
107+
# Strictly enforce True/False — no fuzzy matching
108+
if value.lower() == "true":
109+
parsed_value = True
110+
elif value.lower() == "false":
111+
parsed_value = False
112+
else:
113+
print(f"[WARN]: Boolean field '{field}' received unexpected value '{value}'. Defaulting to None.")
114+
parsed_value = None
115+
else:
116+
if value != "-1":
117+
parsed_value = value
84118

85-
if ";" in value:
86-
parsed_value = self.handle_plural_values(value)
119+
if ";" in value:
120+
parsed_value = self.handle_plural_values(value)
87121

88122
if field in self._json.keys():
89123
self._json[field].append(parsed_value)

‎src/main.py‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,18 @@ def run_pdf_fill_process(user_input: str, definitions: list, pdf_form_path: Unio
6464
if __name__ == "__main__":
6565
file = "./src/inputs/file.pdf"
6666
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 <Mamañema>, and the date is 01/02/2005"
67-
fields = [
68-
"Employee's name",
69-
"Employee's job title",
70-
"Employee's department supervisor",
71-
"Employee's phone number",
72-
"Employee's email",
73-
"Signature",
74-
"Date",
75-
]
67+
# Fields dict maps each field name to its expected Python type.
68+
# Use `bool` for checkbox/radio fields so the LLM is instructed to
69+
# return exactly True or False instead of fuzzy strings like "yes".
70+
fields = {
71+
"Employee's name": str,
72+
"Employee's job title": str,
73+
"Employee's department supervisor": str,
74+
"Employee's phone number": str,
75+
"Employee's email": str,
76+
"Signature": str,
77+
"Date": str,
78+
}
7679
prepared_pdf = "temp_outfile.pdf"
7780
prepare_form(file, prepared_pdf)
7881

0 commit comments

Comments
 (0)