Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions scanfiler/ai/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
- subdir: PREFER one of the existing subfolders below. Only set is_new_subdir=true and
invent a new one when none genuinely fits.{new_rule}
- date: the document's own date as ISO (YYYY, YYYY-MM, or YYYY-MM-DD); null if none.
- doc_type: a short lowercase noun (receipt, invoice, medical_record, drawing, letter...).
- summary: 1-2 sentences capturing what this document is, for later search.
- doc_type: REQUIRED, never empty — a short lowercase noun (receipt, invoice,
medical_record, drawing, letter, statement...). Use "unknown" only as a last resort.
- summary: REQUIRED, never empty — 1-2 sentences capturing what this document is, for
later search.
- tags: 2-6 short lowercase keywords.
- confidence: 0..1, your certainty in the filename+subdir. Be honest; low is fine.

Existing subfolders: {subdirs}
Every field is required. Existing subfolders: {subdirs}
"""

_NO_NEW = "\n- New subfolders are DISABLED: you MUST pick from the existing list."
Expand Down
28 changes: 17 additions & 11 deletions scanfiler/ai/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,26 @@ def build_response_format(existing_subdirs: list[str], allow_new: bool) -> dict:
else:
subdir_schema = {"type": "string"}

properties = {
"filename": {"type": "string", "minLength": 1},
"subdir": subdir_schema,
"is_new_subdir": {"type": "boolean"},
# minLength forces non-empty: the model can't satisfy the constraint by
# emitting "" for doc_type/summary (which it did when they were optional).
"doc_type": {"type": "string", "minLength": 1},
"date": {"type": ["string", "null"]},
"summary": {"type": "string", "minLength": 1},
"tags": {"type": "array", "items": {"type": "string"}},
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
}
schema = {
"type": "object",
"additionalProperties": False,
"required": ["filename", "subdir", "confidence"],
"properties": {
"filename": {"type": "string", "minLength": 1},
"subdir": subdir_schema,
"is_new_subdir": {"type": "boolean"},
"doc_type": {"type": "string"},
"date": {"type": ["string", "null"]},
"summary": {"type": "string"},
"tags": {"type": "array", "items": {"type": "string"}},
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
},
# All fields required: forces doc_type/summary to be populated and makes the
# schema OpenAI strict-mode compliant (portable to cloud providers). `date`
# stays nullable; `tags` may be an empty array.
"required": list(properties),
"properties": properties,
}
return {
"type": "json_schema",
Expand Down
17 changes: 17 additions & 0 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,20 @@ def test_decision_defaults():
assert d.is_new_subdir is False
assert d.tags == []
assert d.date is None


def test_all_fields_required():
schema = build_response_format(["A"], allow_new=True)["json_schema"]["schema"]
# Every property is required (forces doc_type/summary; OpenAI strict-compliant).
assert set(schema["required"]) == set(schema["properties"])
assert "doc_type" in schema["required"]
assert "summary" in schema["required"]


def test_doctype_and_summary_are_non_empty():
props = build_response_format([], allow_new=True)["json_schema"]["schema"]["properties"]
assert props["doc_type"]["minLength"] == 1
assert props["summary"]["minLength"] == 1
# date stays nullable, tags may be empty
assert props["date"] == {"type": ["string", "null"]}
assert "minItems" not in props["tags"]