Download test_live_space.py from piyush-mk/invoiceguard-code: direct link, hf CLI and curl.
- Browser
- Download file 8.94 kB
-
https://huggingface.co/piyush-mk/invoiceguard-code/resolve/main/test_live_space.py
- Command line
-
hf download hf://piyush-mk/invoiceguard-code/test_live_space.py
-
curl -L -o test_live_space.py https://huggingface.co/piyush-mk/invoiceguard-code/resolve/main/test_live_space.py
8.94 kB
| """ | |
| Simulate judge evaluation against the live HF Space with LLM agent. | |
| Connects to the deployed Space and runs all tasks using OpenAI API. | |
| """ | |
| import asyncio | |
| import json | |
| import os | |
| import sys | |
| import time | |
| from typing import List, Optional | |
| from dotenv import load_dotenv | |
| from openai import OpenAI | |
| from client import InvoiceGuardEnv | |
| from models import ( | |
| ActionType, DecisionType, ExceptionType, InvoiceGuardAction, | |
| ) | |
| load_dotenv() | |
| SPACE_URL = "https://piyush-mk-invoice-guard.hf.space" | |
| API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1") | |
| MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4.1-mini") | |
| API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY") or os.getenv("OPENAI_API_KEY") or "" | |
| TASKS = [ | |
| "task_1_clean_match", | |
| "task_2_partial_receipt", | |
| "task_3_price_variance", | |
| "task_4_duplicate_invoice", | |
| "task_5_mixed_discrepancy", | |
| "task_6_false_positive_duplicate", | |
| "task_7_retroactive_price", | |
| "task_8_split_invoice_pattern", | |
| "task_9_clean_from_risky_vendor", | |
| "task_10_rounding_false_alarm", | |
| "task_11_authorized_overship", | |
| "task_12_corrected_resubmission", | |
| ] | |
| _MODELS_MAX_COMPLETION_TOKENS = {"gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano", "gpt-5", "gpt-5-mini", "gpt-5.1"} | |
| def _tok_kwarg(limit=512): | |
| for p in _MODELS_MAX_COMPLETION_TOKENS: | |
| if MODEL_NAME.startswith(p): | |
| return {"max_completion_tokens": limit} | |
| return {"max_tokens": limit} | |
| SYSTEM_PROMPT = """You are a senior accounts payable analyst. You will be given an invoice case to investigate and resolve. | |
| The environment tells you your goal, available actions, and decision options. Read the goal carefully. | |
| WORKFLOW: | |
| 1. Investigate: inspect documents (PO, GRN, vendor profile, policy rules), run comparisons (quantity, price, totals), check for duplicates. | |
| 2. Resolve: submit_final_resolution with your decision, exception type, evidence references, and explanation. | |
| Complete a thorough investigation before resolving. Inspect at least: purchase order, goods receipt note, compare quantity, compare price, policy rules, duplicate check, and vendor profile. | |
| RESPONSE FORMAT: | |
| - Respond with ONLY a valid JSON object. No markdown, no commentary. | |
| - Investigation example: {"action_type": "inspect_purchase_order"} | |
| - Resolution example: {"action_type": "submit_final_resolution", "final_decision": "approve_for_payment", "exception_type": "clean_match", "evidence_references": ["inspect_purchase_order", "compare_quantity"], "explanation": "All documents match within tolerance."} | |
| RULES: | |
| - Pay close attention to POLICY findings -- they tell you when escalation is required. | |
| - When multiple issues exist, escalation takes priority over hold. | |
| - Check PO references carefully before concluding an invoice is a duplicate. | |
| - Include all investigation actions you performed in evidence_references. | |
| - Cite specific numbers in your explanation. | |
| - NEVER repeat an action you already took. | |
| - When remaining_steps is 3 or fewer, submit immediately with what you have. | |
| """ | |
| def build_observation_prompt(obs, is_first=False): | |
| parts = [ | |
| f"Case: {obs.case_id} | Difficulty: {obs.difficulty} | Steps remaining: {obs.remaining_steps}", | |
| f"Invoice: {obs.invoice_summary}", | |
| ] | |
| if is_first and obs.goal: | |
| parts.append(f"\n{obs.goal}") | |
| if obs.revealed_documents: | |
| parts.append(f"Documents reviewed: {', '.join(obs.revealed_documents)}") | |
| if obs.findings: | |
| parts.append("Findings:") | |
| for i, f in enumerate(obs.findings, 1): | |
| parts.append(f" {i}. {f}") | |
| if obs.last_action_result: | |
| parts.append(f"Last result: {obs.last_action_result}") | |
| if obs.warnings: | |
| parts.append(f"Warnings: {'; '.join(obs.warnings)}") | |
| if obs.remaining_steps <= 2: | |
| parts.append(">>> YOU MUST submit_final_resolution NOW. Decide based on what you have. <<<") | |
| return "\n".join(parts) | |
| def parse_llm_response(text): | |
| text = text.strip() | |
| if "```json" in text: | |
| text = text.split("```json")[1].split("```")[0].strip() | |
| elif "```" in text: | |
| text = text.split("```")[1].split("```")[0].strip() | |
| try: | |
| return json.loads(text) | |
| except json.JSONDecodeError: | |
| pass | |
| for line in text.split("\n"): | |
| line = line.strip() | |
| if line.startswith("{"): | |
| try: | |
| return json.loads(line) | |
| except json.JSONDecodeError: | |
| continue | |
| return {"action_type": "summarize_findings"} | |
| def build_action(params): | |
| action_type = params.get("action_type", "summarize_findings") | |
| try: | |
| ActionType(action_type) | |
| except ValueError: | |
| action_type = "summarize_findings" | |
| kwargs = {"action_type": action_type} | |
| if params.get("final_decision"): | |
| try: | |
| kwargs["final_decision"] = DecisionType(params["final_decision"]) | |
| except ValueError: | |
| pass | |
| if params.get("exception_type"): | |
| try: | |
| kwargs["exception_type"] = ExceptionType(params["exception_type"]) | |
| except ValueError: | |
| pass | |
| if params.get("evidence_references"): | |
| kwargs["evidence_references"] = list(params["evidence_references"]) | |
| if params.get("explanation"): | |
| kwargs["explanation"] = str(params["explanation"]) | |
| return InvoiceGuardAction(**kwargs) | |
| async def run_task(env, llm, task_id): | |
| result = await env.reset(task_id=task_id) | |
| obs = result.observation | |
| obs.done = result.done | |
| obs.reward = result.reward | |
| messages = [{"role": "system", "content": SYSTEM_PROMPT}] | |
| steps = 0 | |
| rewards = [] | |
| print(f" [RESET] case={obs.case_id} difficulty={obs.difficulty} steps={obs.remaining_steps}") | |
| while not obs.done: | |
| user_msg = build_observation_prompt(obs, is_first=(steps == 0)) | |
| messages.append({"role": "user", "content": user_msg}) | |
| try: | |
| api_kwargs = {"model": MODEL_NAME, "messages": messages, "temperature": 0.0, **_tok_kwarg()} | |
| try: | |
| api_kwargs["response_format"] = {"type": "json_object"} | |
| response = llm.chat.completions.create(**api_kwargs) | |
| except Exception: | |
| del api_kwargs["response_format"] | |
| response = llm.chat.completions.create(**api_kwargs) | |
| assistant_msg = response.choices[0].message.content or "" | |
| except Exception as e: | |
| print(f" [LLM ERROR] {e}") | |
| assistant_msg = '{"action_type": "summarize_findings"}' | |
| messages.append({"role": "assistant", "content": assistant_msg}) | |
| params = parse_llm_response(assistant_msg) | |
| action = build_action(params) | |
| result = await env.step(action) | |
| obs = result.observation | |
| obs.done = result.done | |
| obs.reward = result.reward | |
| reward = result.reward or 0.0 | |
| rewards.append(reward) | |
| steps += 1 | |
| status = "ERR" if obs.last_action_error else "ok" | |
| print(f" [STEP {steps:2d}] {action.action_type.value:35s} reward={reward:+.2f} remain={obs.remaining_steps} {status}") | |
| grader = getattr(obs, "grader_result", {}) or {} | |
| score = grader.get("score", 0.0) if isinstance(grader, dict) else 0.0 | |
| decision = params.get("final_decision", "none") | |
| return {"task_id": task_id, "steps": steps, "score": score, "decision": decision, "rewards": rewards} | |
| async def main(): | |
| print("=" * 65) | |
| print(" JUDGE SIMULATION -- Live HF Space + LLM Agent") | |
| print(f" Space: {SPACE_URL}") | |
| print(f" Model: {MODEL_NAME}") | |
| print(f" API: {API_BASE_URL}") | |
| print("=" * 65) | |
| llm = OpenAI(base_url=API_BASE_URL, api_key=API_KEY) | |
| env = InvoiceGuardEnv(base_url=SPACE_URL) | |
| results = [] | |
| try: | |
| async with env: | |
| for task_id in TASKS: | |
| print(f"\n--- {task_id} ---") | |
| start = time.time() | |
| try: | |
| r = await run_task(env, llm, task_id) | |
| r["time"] = time.time() - start | |
| results.append(r) | |
| print(f" >> score={r['score']:.4f} decision={r['decision']} time={r['time']:.1f}s") | |
| except Exception as e: | |
| print(f" >> FAILED: {e}") | |
| results.append({"task_id": task_id, "score": 0.0, "steps": 0, "error": str(e)}) | |
| except Exception as e: | |
| print(f"\nConnection error: {e}") | |
| sys.exit(1) | |
| print(f"\n\n{'='*65}") | |
| print(" RESULTS SUMMARY") | |
| print(f"{'='*65}") | |
| scores = [] | |
| for r in results: | |
| s = r.get("score", 0.0) | |
| scores.append(s) | |
| dec = r.get("decision", "n/a") | |
| print(f" {r['task_id']:40s} score={s:.4f} decision={dec}") | |
| avg = sum(scores) / len(scores) if scores else 0.0 | |
| print(f"\n Average score: {avg:.4f}") | |
| print(f" Tasks passed: {sum(1 for s in scores if s >= 0.5)}/{len(scores)}") | |
| print(f"{'='*65}") | |
| if __name__ == "__main__": | |
| asyncio.run(main()) | |