#!/usr/bin/env python3
"""
Form-to-JSON Extraction Agent — runnable demo (Agent Blueprint from AgentKits)

This runs a REAL agent loop: it sends the kit's actual system prompt and a
realistic sample input to the model, prints the structured decision, and then
executes any proposed actions against MOCK tools (labelled [MOCK]).

The model/reasoning is real. The tools are stubs that return fake data and do
NOT touch any real system. Wiring the mock tools to your own integrations
(CRM, database, GitHub, etc.) is the final implementation step — see README.md.

Works with Anthropic OR OpenAI — it auto-detects whichever API key is set.

© 2026 AgentKits · www.agent-kits.com · Licensed CC BY 4.0 (attribution required)
Original blueprint: https://agent-kits.com/kit/form-to-json-extractor

Setup (pick one):
    pip install anthropic   &&  export ANTHROPIC_API_KEY=sk-ant-...
    pip install openai      &&  export OPENAI_API_KEY=sk-...
Then:
    python run.py
"""
import os, json, sys

ANTHROPIC_MODEL = "claude-sonnet-4-6"
OPENAI_MODEL = "gpt-4o"

SYSTEM_PROMPT = """You are a Form-to-JSON Extraction Agent. You read a filled-in form/document and output schema-valid JSON of the fields, with per-field confidence. You are judged on accurate, faithful extraction and on NEVER guessing, inventing, or inferring a field value that isn't actually on the document.

== CORE PRINCIPLES ==
1. Extract only what's present. Output a value only if it's actually on the document. If a field is blank, missing, or unreadable, mark it as such — never fill it with a guess or an inferred value.
2. Confidence per field. Attach a confidence to every extracted value. Low-confidence, ambiguous, or poor-OCR fields are flagged for human review.
3. Faithful to the document. Capture what the form says. Don't normalize away meaning, don't "correct" values, and don't merge or split fields in ways that change them.

== HARD RULES (NON-NEGOTIABLE) ==
- NO FABRICATED FIELDS: Never invent, infer, or guess a value. Not on the document = null + "missing"/"illegible" flag. This is absolute — a confidently wrong value is worse than a flagged blank.
- NO SENSITIVE INFERENCE: Never derive a sensitive value (ID number, DOB, etc.) that isn't explicitly present.
- CONFIDENCE + FLAGS: Every field carries a confidence; low-confidence/ambiguous/illegible -> review_required.
- SCHEMA-VALID: Output must validate against the provided schema (types, required, enums). Report fields that don't fit rather than coercing silently.
- FAITHFUL: Preserve values as written; flag rather than "fix" suspicious values.

== METHOD ==
- Read the document. For each schema field, locate the value, extract it as written, score confidence, and flag missing/illegible/ambiguous. Validate against the schema. Return JSON + a review list.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "document_type": "<as identified>",
  "fields": { "<field>": { "value": "<as written|null>", "confidence": "high|medium|low", "flag": "ok|low_confidence|illegible|missing|ambiguous" } },
  "schema_valid": <bool>,
  "review_required": ["<fields needing human review and why>"],
  "note": "Extracted only values present on the document. No field was guessed or inferred."
}
Never guess a value. Flag missing/illegible fields. Validate the schema.
"""

SAMPLE_INPUT = """A typed application form with Name: Jane Smith, DOB: 1990-04-12, State: active — all legible.
"""

# ---- MOCK tools (stubs — replace with real integrations to deploy) ----
def get_document(args=None):
    print(f"  [MOCK] get_document(" + str(args) + ")")
    return {"status": "mock", "tool": "get_document"}

def detect_fields(args=None):
    print(f"  [MOCK] detect_fields(" + str(args) + ")")
    return {"status": "mock", "tool": "detect_fields"}

def extract_to_schema(args=None):
    print(f"  [MOCK] extract_to_schema(" + str(args) + ")")
    return {"status": "mock", "tool": "extract_to_schema"}

def confidence_score(args=None):
    print(f"  [MOCK] confidence_score(" + str(args) + ")")
    return {"status": "mock", "tool": "confidence_score"}

def validate_schema(args=None):
    print(f"  [MOCK] validate_schema(" + str(args) + ")")
    return {"status": "mock", "tool": "validate_schema"}

def flag_low_confidence(args=None):
    print(f"  [MOCK] flag_low_confidence(" + str(args) + ")")
    return {"status": "mock", "tool": "flag_low_confidence"}

def flag_missing(args=None):
    print(f"  [MOCK] flag_missing(" + str(args) + ")")
    return {"status": "mock", "tool": "flag_missing"}

def ocr_quality_check(args=None):
    print(f"  [MOCK] ocr_quality_check(" + str(args) + ")")
    return {"status": "mock", "tool": "ocr_quality_check"}

TOOLS = {
    "get_document": get_document,
    "detect_fields": detect_fields,
    "extract_to_schema": extract_to_schema,
    "confidence_score": confidence_score,
    "validate_schema": validate_schema,
    "flag_low_confidence": flag_low_confidence,
    "flag_missing": flag_missing,
    "ocr_quality_check": ocr_quality_check,
}


def call_model(system, user):
    """Dispatch to whichever provider's key is configured."""
    if os.environ.get("ANTHROPIC_API_KEY"):
        try:
            from anthropic import Anthropic
        except ImportError:
            sys.exit("Run: pip install anthropic")
        msg = Anthropic().messages.create(
            model=ANTHROPIC_MODEL,
            max_tokens=1600,
            system=system,
            messages=[{"role": "user", "content": user}],
        )
        return "".join(b.text for b in msg.content if getattr(b, "type", "") == "text")
    if os.environ.get("OPENAI_API_KEY"):
        try:
            from openai import OpenAI
        except ImportError:
            sys.exit("Run: pip install openai")
        resp = OpenAI().chat.completions.create(
            model=OPENAI_MODEL,
            max_tokens=1600,
            messages=[
                {"role": "system", "content": system},
                {"role": "user", "content": user},
            ],
        )
        return resp.choices[0].message.content
    sys.exit("Set ANTHROPIC_API_KEY or OPENAI_API_KEY first.")


def extract_json(text):
    a, b = text.find("{"), text.rfind("}")
    if a == -1 or b == -1:
        return None
    try:
        return json.loads(text[a : b + 1])
    except json.JSONDecodeError:
        return None


def main():
    print("=" * 70)
    print("INPUT:\n" + SAMPLE_INPUT)
    print("=" * 70)

    text = call_model(SYSTEM_PROMPT, SAMPLE_INPUT)
    print("AGENT DECISION:\n" + text)
    print("=" * 70)

    data = extract_json(text)
    if not data:
        print("(No JSON object parsed — the model returned prose.)")
        return

    actions = data.get("actions") or []
    if actions:
        print("EXECUTING PROPOSED ACTIONS (mock):")
        for a in actions:
            name = a.get("tool")
            args = a.get("args", {})
            if name in TOOLS:
                TOOLS[name](args)
            else:
                print(f"  [skip] no mock tool for '{name}'")
    else:
        print("This blueprint returned a structured result (no side-effecting "
              "actions in this sample). Available mock tools: "
              + ", ".join(TOOLS.keys()))


if __name__ == "__main__":
    main()
