"""
Pydantic schemas for the Government Fraud Hunter AI environment.
Architecture: Format-First Hierarchical Curriculum + RLVR
- Every action MUST include ... CoT before the JSON payload
- Format gate terminates episode on schema violation (prevents reward hacking)
- Per-kind fields are strictly validated; NPI requires exact checksum match
- Evidence graph accumulates across steps to enable causal chain scoring
Supports:
- Standard tool actions (query_corporate, query_medicare, etc.)
- CodeAct: agent submits Python code executed in a sandboxed environment
- SQL_QUERY: agent issues raw SQL (MCP-style tool call)
- Strict NPI validation (Luhn checksum, no partial credit)
- 7 fraud typologies with per-typology reward multipliers
"""
from __future__ import annotations
from enum import Enum
from typing import Any, Dict, List, Optional
from openenv.core.env_server.types import Action, Observation
from pydantic import Field, model_validator
# ─── Action Kinds ─────────────────────────────────────────────────────────────
class ActionKind(str, Enum):
QUERY_CORPORATE = "query_corporate" # lookup corporate registry
QUERY_MEDICARE = "query_medicare" # lookup beneficiary / claim
EXTRACT_ENTITY = "extract_entity" # flag an entity as fraudulent
LINK_SHELL = "link_shell" # assert UBO / shell relationship
CLAIM_CONTRADICTION = "claim_contradiction" # flag a billing anomaly
SQL_QUERY = "sql_query" # MCP-style: raw restricted SQL
CODE_ACT = "code_act" # CodeAct: sandboxed Python
OCR_DOCUMENT = "ocr_document" # OCR a scanned PDF into field/value text
COMPARE_DOC_VS_CLAIM = "compare_doc_vs_claim" # verify OCR extraction against DB
SUBMIT_CASE = "submit_case" # terminal: seek conviction
class EntityKind(str, Enum):
BENEFICIARY = "beneficiary"
PROVIDER = "provider"
CORPORATION = "corporation"
UBO = "ubo" # Ultimate Beneficial Owner
CONTRACTOR = "contractor" # government contracting vendor
class ContradictionKind(str, Enum):
# Healthcare fraud
DEAD_PATIENT_CLAIM = "dead_patient_claim"
DUPLICATE_BILL = "duplicate_bill"
UPCODING = "upcoding" # lower CPT billed as higher
UNBUNDLING = "unbundling" # bundled services split for excess billing
PHANTOM_BENEFICIARY = "phantom_beneficiary"
AKS_VIOLATION = "aks_violation" # Anti-Kickback Statute
OFF_LABEL_MARKETING = "off_label_marketing"
# Government contracting fraud
DOUBLE_BILLING = "double_billing" # same invoice line submitted twice
COST_PRICING_FRAUD = "cost_pricing_fraud" # inaccurate pricing data
PRODUCT_SUBSTITUTION = "product_substitution" # different item delivered
# PPP / pandemic fraud
PPP_FRAUD = "ppp_fraud" # misrepresented employee count
FOREIGN_AFFILIATION = "foreign_affiliation" # undisclosed foreign govt ties
# ─── Action Schema ─────────────────────────────────────────────────────────────
class FraudHunterAction(Action):
"""
Single polymorphic action. The LLM MUST emit:
I see beneficiary B001 has dod=2025-11-03. Claim C003 was filed 2026-03-01.
This is a dead patient billing contradiction.
{"kind": "claim_contradiction", "evidence_a": "beneficiary:B001",
"evidence_b": "claim:C003", "contradiction_kind": "dead_patient_claim"}
The block is parsed by the environment and scored by the CoT verifier.
Absence of a block applies COT_MISSING_PENALTY (soft, non-terminal).
Per-kind required fields are enforced by `_require_kind_fields`.
Schema violations (missing required fields, wrong types) trigger FORMAT_GATE_PENALTY
and terminate the episode immediately.
NPI validation: when extracting a provider, `npi_code` must match the ground-truth
NPI exactly. No partial credit; mismatch → NPI_MISMATCH_PENALTY.
"""
kind: ActionKind = Field(..., description="Action variant to dispatch")
# Reasoning trace (CoT) — parsed from ... wrapper
think_trace: Optional[str] = Field(
default=None,
description="Agent's chain-of-thought reasoning before acting"
)
# query_corporate
entity_name: Optional[str] = Field(default=None)
entity_id: Optional[str] = Field(default=None)
# query_medicare
beneficiary_id: Optional[str] = Field(default=None)
claim_id: Optional[str] = Field(default=None)
# extract_entity
extracted_name: Optional[str] = Field(default=None)
extracted_kind: Optional[EntityKind] = Field(default=None)
npi_code: Optional[str] = Field(
default=None, description="10-digit NPI (required for provider extraction)"
)
# link_shell
child_entity: Optional[str] = Field(default=None)
parent_entity: Optional[str] = Field(default=None)
# claim_contradiction
evidence_a: Optional[str] = Field(default=None)
evidence_b: Optional[str] = Field(default=None)
contradiction_kind: Optional[ContradictionKind] = Field(default=None)
cpt_code: Optional[str] = Field(default=None, description="CPT code involved")
icd10_code: Optional[str] = Field(default=None, description="ICD-10 diagnosis code")
# sql_query (MCP-style)
sql_statement: Optional[str] = Field(
default=None, description="Restricted SQL SELECT statement"
)
# code_act (CodeAct paradigm)
python_code: Optional[str] = Field(
default=None, description="Sandboxed Python code using `conn` and `pd`"
)
# ocr_document — path to a scanned claim PDF in evidence_documents.pdf_path
pdf_path: Optional[str] = Field(
default=None, description="Filesystem path to a scanned evidence PDF"
)
# compare_doc_vs_claim — agent's OCR-extracted fields for verification
extracted_fields: Optional[Dict[str, Any]] = Field(
default=None,
description="Fields the agent read off a scanned claim (e.g. {'hcpcs_code':'99215','amount':350.0})"
)
# submit_case
case_summary: Optional[str] = Field(default=None)
confidence: Optional[float] = Field(default=None, ge=0.0, le=1.0)
typologies: Optional[List[str]] = Field(default=None, description="List of fraud typologies alleged")
@model_validator(mode="after")
def _require_kind_fields(self) -> "FraudHunterAction":
if self.kind == ActionKind.QUERY_CORPORATE:
if not (self.entity_name or self.entity_id):
raise ValueError("query_corporate requires entity_name or entity_id")
elif self.kind == ActionKind.QUERY_MEDICARE:
if not (self.beneficiary_id or self.claim_id):
raise ValueError("query_medicare requires beneficiary_id or claim_id")
elif self.kind == ActionKind.EXTRACT_ENTITY:
if not self.extracted_name:
raise ValueError("extract_entity requires extracted_name")
if not self.extracted_kind:
raise ValueError("extract_entity requires extracted_kind")
if self.extracted_kind == EntityKind.PROVIDER and not self.npi_code:
raise ValueError("extract_entity for provider requires npi_code (strict NPI validation)")
elif self.kind == ActionKind.LINK_SHELL:
if not self.child_entity:
raise ValueError("link_shell requires child_entity")
if not self.parent_entity:
raise ValueError("link_shell requires parent_entity")
elif self.kind == ActionKind.CLAIM_CONTRADICTION:
if not self.evidence_a:
raise ValueError("claim_contradiction requires evidence_a")
if not self.evidence_b:
raise ValueError("claim_contradiction requires evidence_b")
if not self.contradiction_kind:
raise ValueError("claim_contradiction requires contradiction_kind")
elif self.kind == ActionKind.SQL_QUERY:
if not self.sql_statement:
raise ValueError("sql_query requires sql_statement")
stmt = self.sql_statement.strip().upper()
if not stmt.startswith("SELECT"):
raise ValueError("sql_query only allows SELECT statements")
elif self.kind == ActionKind.CODE_ACT:
if not self.python_code:
raise ValueError("code_act requires python_code")
elif self.kind == ActionKind.OCR_DOCUMENT:
if not self.pdf_path:
raise ValueError("ocr_document requires pdf_path")
elif self.kind == ActionKind.COMPARE_DOC_VS_CLAIM:
if not self.claim_id:
raise ValueError("compare_doc_vs_claim requires claim_id")
if not self.extracted_fields:
raise ValueError("compare_doc_vs_claim requires extracted_fields")
elif self.kind == ActionKind.SUBMIT_CASE:
if not self.case_summary:
raise ValueError("submit_case requires case_summary")
return self
# ─── Observation Schema ────────────────────────────────────────────────────────
class FraudHunterObservation(Observation):
"""
Environment → agent response.
On reset(): case_brief is populated with the whistleblower dossier.
On step(): tool_output carries query results or sandbox stdout.
grader_feedback carries the RLVR scoring rationale.
evidence_graph shows the accumulated entity/link/contradiction graph.
"""
case_brief: Optional[str] = Field(default=None)
tool_output: Optional[str] = Field(default=None)
base64_document: Optional[str] = Field(
default=None,
description="Base64-encoded document payload for OCR/vision-capable agents",
)
grader_feedback: Optional[str] = Field(default=None)
evidence_graph: Optional[Dict[str, Any]] = Field(
default=None, description="Accumulated evidence: entities, links, contradictions"
)
step_count: int = Field(default=0)
budget_remaining: int = Field(default=0)
difficulty_tier: int = Field(default=1, description="RLVE tier 1–5")
info: Optional[Dict[str, Any]] = Field(default=None)
# ─── Reward Schedule (RLVR — all numeric constants live here) ─────────────────
# Format gate
FORMAT_GATE_PENALTY = -10.0 # schema violation → episode ends
COT_MISSING_PENALTY = -2.0 # no block (soft, non-terminal)
COT_GROUNDED_BONUS = +1.0 # CoT facts verified against tool output
# Step costs
STEP_DECAY = -0.1 # mild time pressure per step
DUPLICATE_QUERY_PENALTY = -5.0 # same query issued twice
# Hallucination / NPI
HALLUCINATED_ENTITY_PENALTY = -50.0 # entity not in database
HALLUCINATED_LINK_PENALTY = -20.0 # shell link asserted with no DB-grounded relation
NPI_EXACT_MATCH_BONUS = +25.0 # provider NPI perfectly verified
NPI_MISMATCH_PENALTY = -20.0 # wrong NPI (zero partial credit)
# Per-action task rewards
EXTRACT_ENTITY_REWARD = +10.0 # correct entity flagged for first time
LINK_SHELL_REWARD = +50.0 # UBO relationship confirmed
CONTRADICTION_REWARD = +100.0 # billing anomaly confirmed
CODEACT_BONUS = +5.0 # per correct SQL result via CodeAct
# Length penalty (phases out after step 20 — forces concision early in training)
LENGTH_PENALTY_RATE = -0.005 # per excess token beyond 150 in CoT
LENGTH_PENALTY_PHASE_OUT_STEP = 20
# Causal chain bonus (process-based scoring)
PROOF_CHAIN_MULTIPLIER = 1.5 # applied to CASE_WON if full chain confirmed
# Unstructured evidence (OCR on scanned CMS-1500 PDFs)
OCR_RECALL_BONUS = +20.0 # per field the agent correctly extracts from a PDF
DOC_CLAIM_MATCH_BONUS = +30.0 # compare_doc_vs_claim finds a document ↔ DB mismatch
PDF_CHAIN_MULTIPLIER = 1.5 # applied to CONTRADICTION_REWARD when the claim
# cites a PDF-sourced field (multi-modal proof)
# Terminal rewards
CASE_WON_REWARD = +1000.0
CASE_PARTIAL_REWARD = +250.0 # won with incomplete evidence
CASE_DISMISSED_REWARD = 0.0
# Per-typology difficulty multipliers (harder typologies earn more)
TYPOLOGY_MULTIPLIERS: Dict[str, float] = {
"dead_patient_claim": 1.0,
"duplicate_bill": 1.0,
"upcoding": 1.2,
"unbundling": 1.3,
"phantom_beneficiary": 1.4,
"aks_violation": 1.6,
"off_label_marketing": 1.5,
"double_billing": 1.2,
"cost_pricing_fraud": 1.4,
"product_substitution": 1.5,
"ppp_fraud": 1.8,
"foreign_affiliation": 2.0,
}
MAX_EPISODE_STEPS = 60 # increased from 50 to give room for CoT + CodeAct