|
|
|
|
|
""" |
|
|
Simple CLI client for testing the Kaanta Tax Assistant API. |
|
|
|
|
|
Example: |
|
|
python client_demo.py --question "Compute PAYE for 1500000 income" \ |
|
|
--input gross_income=1500000 |
|
|
""" |
|
|
|
|
|
from __future__ import annotations |
|
|
|
|
|
import argparse |
|
|
import json |
|
|
from typing import Dict, Optional |
|
|
|
|
|
import httpx |
|
|
|
|
|
|
|
|
def _parse_inputs(raw_pairs: Optional[list[str]]) -> Optional[Dict[str, float]]: |
|
|
if not raw_pairs: |
|
|
return None |
|
|
|
|
|
parsed: Dict[str, float] = {} |
|
|
for item in raw_pairs: |
|
|
if "=" not in item: |
|
|
raise argparse.ArgumentTypeError( |
|
|
f"Calculator input '{item}' must be in key=value form." |
|
|
) |
|
|
key, value = item.split("=", 1) |
|
|
key = key.strip() |
|
|
if not key: |
|
|
raise argparse.ArgumentTypeError("Input keys cannot be empty.") |
|
|
try: |
|
|
parsed[key] = float(value) |
|
|
except ValueError as exc: |
|
|
raise argparse.ArgumentTypeError( |
|
|
f"Value for '{key}' must be numeric." |
|
|
) from exc |
|
|
return parsed |
|
|
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser: |
|
|
parser = argparse.ArgumentParser( |
|
|
description="Send a test question to a running Kaanta Tax Assistant API." |
|
|
) |
|
|
parser.add_argument( |
|
|
"--base-url", |
|
|
default="https://eniiyanu-kaanta.hf.space", |
|
|
help="Base URL of the service (default: %(default)s).", |
|
|
) |
|
|
parser.add_argument( |
|
|
"--question", |
|
|
required=True, |
|
|
help="User question or task to send to the assistant.", |
|
|
) |
|
|
parser.add_argument( |
|
|
"--as-of", |
|
|
help="Optional YYYY-MM-DD date context for tax calculations.", |
|
|
) |
|
|
parser.add_argument( |
|
|
"--tax-type", |
|
|
default="PIT", |
|
|
help="Tax type for calculator runs (PIT, CIT, VAT).", |
|
|
) |
|
|
parser.add_argument( |
|
|
"--jurisdiction", |
|
|
default="state", |
|
|
help="Jurisdiction filter used by the rules engine.", |
|
|
) |
|
|
parser.add_argument( |
|
|
"--input", |
|
|
dest="inputs", |
|
|
action="append", |
|
|
metavar="key=value", |
|
|
help="Calculator input (repeatable). Example: --input gross_income=1500000", |
|
|
) |
|
|
parser.add_argument( |
|
|
"--rule-id", |
|
|
dest="rule_ids", |
|
|
action="append", |
|
|
help="Optional whitelist of rule IDs to evaluate (repeat flag for multiple).", |
|
|
) |
|
|
parser.add_argument( |
|
|
"--no-rag-quotes", |
|
|
action="store_true", |
|
|
help="Skip RAG enrichment when running the calculator.", |
|
|
) |
|
|
parser.add_argument( |
|
|
"--hf-token", |
|
|
help="Optional Hugging Face access token when querying a private Space.", |
|
|
) |
|
|
parser.add_argument( |
|
|
"--timeout", |
|
|
type=float, |
|
|
default=60.0, |
|
|
help="HTTP timeout in seconds (default: %(default)s).", |
|
|
) |
|
|
return parser |
|
|
|
|
|
|
|
|
def main() -> None: |
|
|
parser = build_parser() |
|
|
args = parser.parse_args() |
|
|
|
|
|
try: |
|
|
inputs = _parse_inputs(args.inputs) |
|
|
except argparse.ArgumentTypeError as exc: |
|
|
parser.error(str(exc)) |
|
|
return |
|
|
|
|
|
payload = { |
|
|
"question": args.question, |
|
|
"as_of": args.as_of, |
|
|
"tax_type": args.tax_type.upper() if args.tax_type else None, |
|
|
"jurisdiction": args.jurisdiction, |
|
|
"inputs": inputs, |
|
|
"with_rag_quotes_on_calc": not args.no_rag_quotes, |
|
|
"rule_ids_whitelist": args.rule_ids, |
|
|
} |
|
|
|
|
|
|
|
|
payload = {k: v for k, v in payload.items() if v is not None} |
|
|
|
|
|
url = args.base_url.rstrip("/") + "/v1/query" |
|
|
headers = {} |
|
|
if args.hf_token: |
|
|
headers["Authorization"] = f"Bearer {args.hf_token}" |
|
|
|
|
|
def do_request(target: str) -> httpx.Response: |
|
|
return httpx.post(target, json=payload, headers=headers, timeout=args.timeout) |
|
|
|
|
|
tried_urls = [url] |
|
|
try: |
|
|
response = do_request(url) |
|
|
if response.status_code == 404 and "/proxy" not in url: |
|
|
proxy_url = args.base_url.rstrip("/") + "/proxy/v1/query" |
|
|
response = do_request(proxy_url) |
|
|
tried_urls.append(proxy_url) |
|
|
response.raise_for_status() |
|
|
except httpx.TimeoutException: |
|
|
parser.exit(1, f"Request timed out after {args.timeout} seconds\n") |
|
|
except httpx.HTTPStatusError as exc: |
|
|
locations = " -> ".join(tried_urls) |
|
|
parser.exit( |
|
|
1, |
|
|
f"Server returned HTTP {exc.response.status_code} for {locations}:\n" |
|
|
f"{exc.response.text}\n", |
|
|
) |
|
|
except httpx.RequestError as exc: |
|
|
parser.exit(1, f"Request failed: {exc}\n") |
|
|
|
|
|
print(json.dumps(response.json(), indent=2)) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
main() |
|
|
|