diff --git a/scanfiler/ai/prompt.py b/scanfiler/ai/prompt.py index 9416272..f7f7672 100644 --- a/scanfiler/ai/prompt.py +++ b/scanfiler/ai/prompt.py @@ -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." diff --git a/scanfiler/ai/schema.py b/scanfiler/ai/schema.py index 74883f3..add91a1 100644 --- a/scanfiler/ai/schema.py +++ b/scanfiler/ai/schema.py @@ -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", diff --git a/tests/test_schema.py b/tests/test_schema.py index fe97a29..82d5625 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -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"]