KZNGBB-API / main.py
rairo's picture
Update main.py
f3dcd54 verified
import os
import re
import json
import logging
import traceback
from datetime import datetime, timedelta
from flask import Flask, request, jsonify
from flask_cors import CORS
from google import genai
from google.genai import types
# -----------------------------------------------------------------------------
# 0. LOGGING
# -----------------------------------------------------------------------------
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger("KZNGBB_AI")
# -----------------------------------------------------------------------------
# 1. CONFIGURATION & INITIALIZATION
# -----------------------------------------------------------------------------
app = Flask(__name__)
CORS(app)
# --- Google GenAI Client ---
try:
logger.info("Initializing KZNGBB AI Intelligence Layer...")
api_key = os.environ.get("GEMINI_API_KEY")
if not api_key:
raise ValueError("GEMINI_API_KEY environment variable is not set.")
client = genai.Client(api_key=api_key)
logger.info("Gemini client initialized successfully.")
except Exception as e:
logger.error(f"FATAL: Could not initialize Gemini: {e}")
exit(1)
GEMINI_FLASH = "gemini-3.1-flash-lite"
GEMINI_PRO = "gemini-3.1-flash-lite"
# -----------------------------------------------------------------------------
# 2. HELPERS
# -----------------------------------------------------------------------------
def _strip_json_fences(s: str) -> str:
s = (s or "").strip()
if "```" in s:
m = re.search(r"```(?:json)?\s*(.*?)\s*```", s, re.DOTALL)
if m:
return m.group(1).strip()
return s
def _now_iso() -> str:
return datetime.utcnow().isoformat() + "Z"
def _gemini_json(prompt: str, model: str = GEMINI_FLASH) -> dict:
"""Call Gemini and parse a JSON response. Raises on failure."""
res = client.models.generate_content(
model=model,
contents=prompt,
config=types.GenerateContentConfig(response_mime_type="application/json")
)
raw = _strip_json_fences(res.text)
return json.loads(raw)
def _gemini_text(prompt: str, model: str = GEMINI_FLASH) -> str:
"""Call Gemini and return plain text."""
res = client.models.generate_content(model=model, contents=prompt)
return res.text.strip()
def _gemini_text_with_search(prompt: str, model: str = GEMINI_FLASH) -> str:
"""Call Gemini with Google Search grounding."""
res = client.models.generate_content(
model=model,
contents=prompt,
config=types.GenerateContentConfig(tools=[{"google_search": {}}])
)
return res.text.strip()
# -----------------------------------------------------------------------------
# 3. AI FEATURE ENDPOINTS
# -----------------------------------------------------------------------------
# ── 3.1 OPERATOR RISK INTELLIGENCE ──────────────────────────────────────────
@app.route("/api/ai/operator-risk", methods=["POST"])
def operator_risk():
"""
Analyses an operator record and returns a structured risk report.
Input JSON:
operator_name str required
license_number str required
operator_type str required (e.g. "Land-Based Casino", "Online", "Bookmaker")
compliance_score float required (0-100)
violations int required
last_audit_date str required (ISO date)
revenue_ggr_zar float optional
district str optional
Output JSON:
risk_level str ("Low" | "Medium" | "High" | "Critical")
risk_score int (0-100)
flags list of str
recommendations list of str
predicted_next_violation_days int | null
summary str
generated_at str ISO timestamp
"""
data = request.get_json(silent=True) or {}
required = ["operator_name", "license_number", "operator_type", "compliance_score", "violations", "last_audit_date"]
missing = [f for f in required if f not in data]
if missing:
return jsonify({"error": f"Missing fields: {', '.join(missing)}"}), 400
prompt = f"""
You are the KZNGBB (KwaZulu-Natal Gaming and Betting Board) AI Risk Intelligence Engine.
Analyse the following operator record and produce a structured risk assessment.
OPERATOR RECORD:
- Name: {data['operator_name']}
- License: {data['license_number']}
- Type: {data['operator_type']}
- Compliance Score: {data['compliance_score']}%
- Violations to date: {data['violations']}
- Last Audit: {data['last_audit_date']}
- GGR (ZAR): {data.get('revenue_ggr_zar', 'Not provided')}
- District: {data.get('district', 'Not specified')}
TASK:
1. Assign a risk_level: "Low" (score 0-25), "Medium" (26-55), "High" (56-80), or "Critical" (81-100).
2. Assign a numeric risk_score (0-100).
3. List up to 5 specific compliance flags based on the data.
4. Provide 3-5 actionable regulatory recommendations.
5. Predict how many days until the next probable violation (null if Low risk).
6. Write a 2-sentence executive summary.
Respond ONLY with this JSON schema:
{{
"risk_level": "...",
"risk_score": 0,
"flags": ["..."],
"recommendations": ["..."],
"predicted_next_violation_days": null,
"summary": "..."
}}
"""
try:
result = _gemini_json(prompt)
result["generated_at"] = _now_iso()
logger.info(f"Risk assessment complete for {data['operator_name']}: {result.get('risk_level')}")
return jsonify(result), 200
except Exception as e:
logger.error(f"operator-risk error: {e}\n{traceback.format_exc()}")
return jsonify({"error": str(e)}), 500
# ── 3.2 APPLICATION SCREENING ────────────────────────────────────────────────
@app.route("/api/ai/screen-application", methods=["POST"])
def screen_application():
"""
Screens a new license application against Regulation 3 requirements.
Input JSON:
applicant_name str required
applicant_type str required ("Individual" | "Company")
application_type str required (e.g. "Land-Based Casino", "Online Betting")
business_plan str required (text description of the business plan)
id_verified bool required
fee_paid bool required
supporting_docs list optional list of document names submitted
proposed_district str optional
Output JSON:
decision str ("APPROVE" | "CONDITIONAL" | "REJECT")
confidence float (0.0-1.0)
regulation_3_gaps list of str (missing Regulation 3 requirements)
conditions list of str (conditions if CONDITIONAL)
rejection_reasons list of str (reasons if REJECT)
estimated_processing_days int
summary str
generated_at str
"""
data = request.get_json(silent=True) or {}
required = ["applicant_name", "applicant_type", "application_type", "business_plan", "id_verified", "fee_paid"]
missing = [f for f in required if f not in data]
if missing:
return jsonify({"error": f"Missing fields: {', '.join(missing)}"}), 400
prompt = f"""
You are the KZNGBB (KwaZulu-Natal Gaming and Betting Board) Automated Application Screening System, operating under the KwaZulu-Natal
Gambling and Betting Act, No. 4 of 2008, specifically Regulation 3.
Regulation 3 requires:
- Submission on prescribed application form
- Payment of prescribed fee
- Certified ID or company registration documents
- Detailed business plan
- Description of proposed gambling/betting activities
APPLICATION DETAILS:
- Applicant: {data['applicant_name']} ({data['applicant_type']})
- License Type Sought: {data['application_type']}
- ID/Company Docs Verified: {data['id_verified']}
- Fee Paid: {data['fee_paid']}
- Supporting Documents Submitted: {data.get('supporting_docs', [])}
- Proposed District: {data.get('proposed_district', 'Not specified')}
- Business Plan Summary: {data['business_plan']}
TASK:
1. Identify any Regulation 3 compliance gaps.
2. Recommend a decision: APPROVE, CONDITIONAL (approval with conditions), or REJECT.
3. If CONDITIONAL, list conditions to be met.
4. If REJECT, list specific rejection reasons.
5. Estimate processing days (standard is 14 days, complex cases may be 30+).
6. Provide confidence score (0.0-1.0) in your decision.
7. Write a 2-sentence summary for the applicant file.
Respond ONLY with this JSON:
{{
"decision": "...",
"confidence": 0.0,
"regulation_3_gaps": ["..."],
"conditions": ["..."],
"rejection_reasons": ["..."],
"estimated_processing_days": 14,
"summary": "..."
}}
"""
try:
result = _gemini_json(prompt)
result["generated_at"] = _now_iso()
logger.info(f"Application screening for {data['applicant_name']}: {result.get('decision')}")
return jsonify(result), 200
except Exception as e:
logger.error(f"screen-application error: {e}")
return jsonify({"error": str(e)}), 500
# ── 3.3 COMPLIANCE NARRATIVE GENERATOR ──────────────────────────────────────
@app.route("/api/ai/compliance-narrative", methods=["POST"])
def compliance_narrative():
"""
Generates a formal compliance report narrative for a given operator audit.
Input JSON:
operator_name str required
audit_date str required
compliance_score float required
violations_detail list required list of violation description strings
previous_score float optional
auditor_notes str optional
Output JSON:
executive_summary str
findings_narrative str
corrective_actions str
board_recommendation str
formal_report_text str (full formal narrative, ready to paste into a report)
generated_at str
"""
data = request.get_json(silent=True) or {}
required = ["operator_name", "audit_date", "compliance_score", "violations_detail"]
missing = [f for f in required if f not in data]
if missing:
return jsonify({"error": f"Missing fields: {', '.join(missing)}"}), 400
prompt = f"""
You are the KZNGBB (KwaZulu-Natal Gaming and Betting Board) Chief Compliance Officer's AI writing assistant.
Produce a formal regulatory compliance narrative for an operator audit.
AUDIT DATA:
- Operator: {data['operator_name']}
- Audit Date: {data['audit_date']}
- Compliance Score: {data['compliance_score']}%
- Previous Score: {data.get('previous_score', 'N/A')}%
- Violations Found: {json.dumps(data['violations_detail'])}
- Auditor Notes: {data.get('auditor_notes', 'None')}
TASK:
Generate formal, professional regulatory language suitable for official KZNGBB records.
Respond ONLY with this JSON:
{{
"executive_summary": "2-3 sentences for the board.",
"findings_narrative": "Detailed paragraph describing each violation with regulatory references.",
"corrective_actions": "Bulleted list as prose: specific actions the operator must take.",
"board_recommendation": "One clear recommendation: Maintain License | Issue Warning | Suspend | Revoke.",
"formal_report_text": "The complete formal report text combining all sections, ready for official use."
}}
"""
try:
result = _gemini_json(prompt, model=GEMINI_PRO)
result["generated_at"] = _now_iso()
return jsonify(result), 200
except Exception as e:
logger.error(f"compliance-narrative error: {e}")
return jsonify({"error": str(e)}), 500
# ── 3.4 CSI IMPACT ANALYSER ─────────────────────────────────────────────────
@app.route("/api/ai/csi-impact", methods=["POST"])
def csi_impact():
"""
Evaluates a Corporate Social Investment (CSI) project and predicts community impact.
Input JSON:
project_name str required
operator_sponsor str required
category str required (e.g. "Education", "Health", "Sports")
budget_zar float required
target_beneficiaries int required
district str required
description str required
duration_months int optional (default 12)
Output JSON:
impact_score int (0-100)
impact_rating str ("Excellent" | "Good" | "Adequate" | "Poor")
beneficiary_reach str (qualitative: "High" | "Medium" | "Low")
roi_narrative str (social return on investment assessment)
alignment_score int (alignment with KZNGBB CSI mandate, 0-100)
risks list of str
recommendations list of str
projected_outcomes list of str
generated_at str
"""
data = request.get_json(silent=True) or {}
required = ["project_name", "operator_sponsor", "category", "budget_zar", "target_beneficiaries", "district", "description"]
missing = [f for f in required if f not in data]
if missing:
return jsonify({"error": f"Missing fields: {', '.join(missing)}"}), 400
prompt = f"""
You are the KZNGBB(KwaZulu-Natal Gaming and Betting Board) CSI (Corporate Social Investment) Evaluation Engine.
Gambling operators in KwaZulu-Natal are required to reinvest in community development.
CSI PROJECT SUBMISSION:
- Project: {data['project_name']}
- Sponsor: {data['operator_sponsor']}
- Category: {data['category']}
- Budget: R {data['budget_zar']:,.0f}
- Target Beneficiaries: {data['target_beneficiaries']:,}
- District: {data['district']}
- Duration: {data.get('duration_months', 12)} months
- Description: {data['description']}
TASK:
1. Score the project impact (0-100) based on budget efficiency, reach, and community need.
2. Rate the impact as Excellent (80+), Good (60-79), Adequate (40-59), or Poor (<40).
3. Assess beneficiary reach (High/Medium/Low).
4. Write a social ROI narrative.
5. Score alignment with KZNGBB CSI mandate (0-100).
6. List 3 risks and 3 recommendations.
7. List 3 projected measurable outcomes.
Respond ONLY with this JSON:
{{
"impact_score": 0,
"impact_rating": "...",
"beneficiary_reach": "...",
"roi_narrative": "...",
"alignment_score": 0,
"risks": ["..."],
"recommendations": ["..."],
"projected_outcomes": ["..."]
}}
"""
try:
result = _gemini_json(prompt)
result["generated_at"] = _now_iso()
return jsonify(result), 200
except Exception as e:
logger.error(f"csi-impact error: {e}")
return jsonify({"error": str(e)}), 500
# ── 3.5 REVENUE ANOMALY DETECTION ───────────────────────────────────────────
@app.route("/api/ai/revenue-anomaly", methods=["POST"])
def revenue_anomaly():
"""
Detects anomalies and patterns in an operator's monthly GGR submissions.
Input JSON:
operator_name str required
operator_type str required
monthly_ggr list required list of {month: str, ggr_zar: float, tax_zar: float}
industry_avg_growth float optional (e.g. 0.08 for 8%)
Output JSON:
anomalies list of {month, type, severity, detail}
trend str ("Growing" | "Declining" | "Stable" | "Volatile")
tax_compliance_rate float (0.0-1.0)
underreporting_flag bool
underreporting_confidence float (0.0-1.0)
narrative str
audit_recommendation str ("Routine" | "Priority Review" | "Immediate Investigation")
generated_at str
"""
data = request.get_json(silent=True) or {}
required = ["operator_name", "operator_type", "monthly_ggr"]
missing = [f for f in required if f not in data]
if missing:
return jsonify({"error": f"Missing fields: {', '.join(missing)}"}), 400
if not isinstance(data["monthly_ggr"], list) or len(data["monthly_ggr"]) < 2:
return jsonify({"error": "monthly_ggr must be a list with at least 2 months of data"}), 400
prompt = f"""
You are the KZNGBB(KwaZulu-Natal Gaming and Betting Board) Financial Intelligence Unit AI.
Analyse the following Gross Gaming Revenue (GGR) data for anomalies, underreporting,
and compliance patterns. The standard tax rate is 20% of GGR.
OPERATOR: {data['operator_name']} ({data['operator_type']})
INDUSTRY AVG GROWTH: {data.get('industry_avg_growth', 0.08) * 100:.1f}% per month
MONTHLY GGR DATA:
{json.dumps(data['monthly_ggr'], indent=2)}
TASK:
1. Identify anomalies (sudden drops >30%, spikes >50%, irregular tax ratios, flat reporting).
For each anomaly: month, type, severity (Low/Medium/High/Critical), detail.
2. Classify the overall trend.
3. Calculate average tax compliance rate (tax_zar / ggr_zar should be ~0.20).
4. Flag potential underreporting if tax ratio is consistently below 0.18.
5. Write a 3-sentence narrative for the financial analytics report.
6. Recommend audit action level.
Respond ONLY with this JSON:
{{
"anomalies": [{{"month": "...", "type": "...", "severity": "...", "detail": "..."}}],
"trend": "...",
"tax_compliance_rate": 0.0,
"underreporting_flag": false,
"underreporting_confidence": 0.0,
"narrative": "...",
"audit_recommendation": "..."
}}
"""
try:
result = _gemini_json(prompt)
result["generated_at"] = _now_iso()
return jsonify(result), 200
except Exception as e:
logger.error(f"revenue-anomaly error: {e}")
return jsonify({"error": str(e)}), 500
# ── 3.6 REGULATORY QUERY ASSISTANT ──────────────────────────────────────────
@app.route("/api/ai/regulatory-query", methods=["POST"])
def regulatory_query():
"""
Natural language Q&A on KwaZulu-Natal gambling regulation.
Uses Gemini with Google Search grounding for current regulatory context.
Input JSON:
question str required
context str optional (e.g. "Online Casino", "Bookmaker license renewal")
user_role str optional ("Operator" | "Applicant" | "Board Member" | "Public")
Output JSON:
answer str
regulatory_refs list of str (Act sections, Regulation numbers)
confidence str ("High" | "Medium" | "Low")
disclaimer str
follow_up_questions list of str
generated_at str
"""
data = request.get_json(silent=True) or {}
if not data.get("question"):
return jsonify({"error": "question is required"}), 400
prompt = f"""
You are the KZNGBB(KwaZulu-Natal Gaming and Betting Board) AI Regulatory Assistant, an expert in the KwaZulu-Natal Gambling and Betting Act No. 4 of 2008 and all associated Regulations.
USER ROLE: {data.get('user_role', 'General')}
CONTEXT: {data.get('context', 'General regulatory query')}
QUESTION: {data['question']}
Search for the most current information on this regulatory topic and provide a clear, accurate answer.
TASK:
1. Answer the question clearly and accurately.
2. Reference specific Act sections or Regulation numbers where applicable.
3. Rate your confidence (High/Medium/Low) based on how specific and current the information is.
4. Write a brief regulatory disclaimer.
5. Suggest 3 follow-up questions the user might find helpful.
Respond ONLY with this JSON:
{{
"answer": "...",
"regulatory_refs": ["Act No. 4 of 2008, Section X", "Regulation 3", "..."],
"confidence": "...",
"disclaimer": "...",
"follow_up_questions": ["...", "...", "..."]
}}
"""
try:
res = client.models.generate_content(
model=GEMINI_FLASH,
contents=prompt,
config=types.GenerateContentConfig(
tools=[{"google_search": {}}],
response_mime_type="application/json"
)
)
raw = _strip_json_fences(res.text)
result = json.loads(raw)
result["generated_at"] = _now_iso()
return jsonify(result), 200
except Exception as e:
logger.error(f"regulatory-query error: {e}")
return jsonify({"error": str(e)}), 500
# ── 3.7 GEOGRAPHIC INTELLIGENCE SUMMARY ─────────────────────────────────────
@app.route("/api/ai/geographic-intel", methods=["POST"])
def geographic_intel():
"""
Produces a strategic intelligence summary for a KZN district or municipality.
Input JSON:
district str required (e.g. "eThekwini", "Umgungundlovu")
operators_in_district list required list of {name, type, compliance_score, violations}
population int optional
include_recommendations bool optional (default true)
Output JSON:
district_risk_level str ("Low" | "Medium" | "High" | "Critical")
operator_density str
dominant_type str (most common operator type)
compliance_overview str
hotspots list of str
strategic_recommendations list of str
resource_allocation_advice str
narrative str
generated_at str
"""
data = request.get_json(silent=True) or {}
required = ["district", "operators_in_district"]
missing = [f for f in required if f not in data]
if missing:
return jsonify({"error": f"Missing fields: {', '.join(missing)}"}), 400
prompt = f"""
You are the KZNGBB(KwaZulu-Natal Gaming and Betting Board) Geographic Intelligence Analyst AI.
Produce a strategic intelligence brief for a KwaZulu-Natal district.
DISTRICT: {data['district']}
POPULATION: {data.get('population', 'Unknown')}
OPERATORS IN DISTRICT:
{json.dumps(data['operators_in_district'], indent=2)}
TASK:
1. Assess the overall district risk level.
2. Describe operator density relative to district context.
3. Identify the dominant operator type.
4. Summarise the compliance landscape.
5. List 3-5 hotspot concerns (areas or operators needing attention).
6. Provide 3-5 strategic recommendations for board resource allocation.
7. Advise on resource allocation priorities.
8. Write a 3-sentence intelligence brief narrative.
Respond ONLY with this JSON:
{{
"district_risk_level": "...",
"operator_density": "...",
"dominant_type": "...",
"compliance_overview": "...",
"hotspots": ["..."],
"strategic_recommendations": ["..."],
"resource_allocation_advice": "...",
"narrative": "..."
}}
"""
try:
result = _gemini_json(prompt)
result["generated_at"] = _now_iso()
return jsonify(result), 200
except Exception as e:
logger.error(f"geographic-intel error: {e}")
return jsonify({"error": str(e)}), 500
# ── 3.8 EXPORT REPORT GENERATOR ─────────────────────────────────────────────
@app.route("/api/ai/generate-report", methods=["POST"])
def generate_report():
"""
Generates a formatted executive report narrative for board export.
Input JSON:
report_type str required ("Monthly" | "Quarterly" | "Annual" | "Incident")
period str required (e.g. "Q1 2025", "May 2025")
total_licenses int required
revenue_ggr_zar float required
tax_collected_zar float required
compliance_rate float required (0-100)
pending_applications int required
violations_this_period int required
csi_spend_zar float optional
key_events list optional list of str (notable events in the period)
board_chair str optional
Output JSON:
report_title str
executive_summary str
performance_highlights list of str
compliance_section str
financial_section str
outlook str
full_report_text str
generated_at str
"""
data = request.get_json(silent=True) or {}
required = ["report_type", "period", "total_licenses", "revenue_ggr_zar",
"tax_collected_zar", "compliance_rate", "pending_applications", "violations_this_period"]
missing = [f for f in required if f not in data]
if missing:
return jsonify({"error": f"Missing fields: {', '.join(missing)}"}), 400
prompt = f"""
You are the KZNGBB(KwaZulu-Natal Gaming and Betting Board) AI Report Drafting Engine.
Draft a professional {data['report_type']} board report for the period: {data['period']}.
DATA:
- Total Active Licenses: {data['total_licenses']}
- Gross Gaming Revenue: R {data['revenue_ggr_zar']:,.0f}
- Tax Collected: R {data['tax_collected_zar']:,.0f}
- Compliance Rate: {data['compliance_rate']}%
- Pending Applications: {data['pending_applications']}
- Violations This Period: {data['violations_this_period']}
- CSI Spend: R {data.get('csi_spend_zar', 0):,.0f}
- Key Events: {json.dumps(data.get('key_events', []))}
- Board Chair: {data.get('board_chair', 'The Chairperson')}
TASK:
Generate a formal, professional board-quality report with:
1. A formal report title.
2. Executive summary (3 sentences).
3. 4-6 performance highlights as bullet points (prose list).
4. Compliance section paragraph.
5. Financial analytics section paragraph.
6. Forward-looking outlook paragraph.
7. A full_report_text that combines all sections into a polished, ready-to-use narrative.
Respond ONLY with this JSON:
{{
"report_title": "...",
"executive_summary": "...",
"performance_highlights": ["...", "..."],
"compliance_section": "...",
"financial_section": "...",
"outlook": "...",
"full_report_text": "..."
}}
"""
try:
result = _gemini_json(prompt, model=GEMINI_PRO)
result["generated_at"] = _now_iso()
return jsonify(result), 200
except Exception as e:
logger.error(f"generate-report error: {e}")
return jsonify({"error": str(e)}), 500
# ── 3.9 VIOLATION PATTERN ANALYSIS ─────────────────────────────────────────
@app.route("/api/ai/violation-patterns", methods=["POST"])
def violation_patterns():
"""
Analyses historical violation records across operators to identify systemic patterns.
Input JSON:
violations list required list of {operator_name, type, violation_type, date, district, severity}
period_label str optional (e.g. "2025 YTD")
Output JSON:
most_common_violation str
highest_risk_type str (operator type with most violations)
district_hotspot str
seasonal_pattern str or null
systemic_issues list of str
enforcement_priorities list of str
board_action_items list of str
statistical_summary dict
generated_at str
"""
data = request.get_json(silent=True) or {}
if not data.get("violations") or not isinstance(data["violations"], list):
return jsonify({"error": "violations list is required"}), 400
prompt = f"""
You are the KZNGBB(KwaZulu-Natal Gaming and Betting Board) Enforcement Intelligence AI.
Analyse the following violation records and identify systemic patterns.
PERIOD: {data.get('period_label', 'Historical')}
VIOLATION RECORDS:
{json.dumps(data['violations'], indent=2)}
TASK:
1. Identify the most common violation type.
2. Identify which operator type has the highest violation rate.
3. Identify the district with the most violations (hotspot).
4. Detect any seasonal or temporal patterns.
5. List 3-5 systemic issues revealed by the data.
6. List 3-5 specific enforcement priorities for the board.
7. List 3 concrete board action items.
8. Produce a statistical_summary dict with: total_violations, by_type (dict), by_district (dict), by_severity (dict).
Respond ONLY with this JSON:
{{
"most_common_violation": "...",
"highest_risk_type": "...",
"district_hotspot": "...",
"seasonal_pattern": null,
"systemic_issues": ["..."],
"enforcement_priorities": ["..."],
"board_action_items": ["..."],
"statistical_summary": {{
"total_violations": 0,
"by_type": {{}},
"by_district": {{}},
"by_severity": {{}}
}}
}}
"""
try:
result = _gemini_json(prompt)
result["generated_at"] = _now_iso()
return jsonify(result), 200
except Exception as e:
logger.error(f"violation-patterns error: {e}")
return jsonify({"error": str(e)}), 500
# ── 3.10 LICENCE RENEWAL ADVISOR ────────────────────────────────────────────
@app.route("/api/ai/renewal-advisory", methods=["POST"])
def renewal_advisory():
"""
Produces an AI renewal recommendation for an expiring operator license.
Input JSON:
operator_name str required
license_number str required
operator_type str required
expiry_date str required (ISO date)
compliance_history list required list of {year, score, violations}
ggr_trend str optional ("growing" | "declining" | "stable")
csi_compliant bool optional
Output JSON:
recommendation str ("RENEW" | "RENEW_WITH_CONDITIONS" | "DEFER" | "DECLINE")
confidence float
rationale str
conditions list of str
risk_flags list of str
renewal_fee_category str ("Standard" | "Elevated" | "Premium Review")
next_audit_priority str ("Routine" | "Priority" | "Immediate")
generated_at str
"""
data = request.get_json(silent=True) or {}
required = ["operator_name", "license_number", "operator_type", "expiry_date", "compliance_history"]
missing = [f for f in required if f not in data]
if missing:
return jsonify({"error": f"Missing fields: {', '.join(missing)}"}), 400
prompt = f"""
You are the KZNGBB(KwaZulu-Natal Gaming and Betting Board) License Renewal Advisory AI.
Assess whether an operator's license should be renewed under the KZN Gambling and Betting Act.
OPERATOR: {data['operator_name']} | {data['license_number']} | {data['operator_type']}
EXPIRY DATE: {data['expiry_date']}
GGR TREND: {data.get('ggr_trend', 'Unknown')}
CSI COMPLIANT: {data.get('csi_compliant', 'Unknown')}
COMPLIANCE HISTORY (last {len(data['compliance_history'])} years):
{json.dumps(data['compliance_history'], indent=2)}
TASK:
1. Recommend: RENEW, RENEW_WITH_CONDITIONS, DEFER (pending investigation), or DECLINE.
2. Provide confidence (0.0-1.0).
3. Write a clear rationale paragraph.
4. List any renewal conditions (if RENEW_WITH_CONDITIONS).
5. List risk flags identified from the compliance history.
6. Categorise renewal fee complexity (Standard/Elevated/Premium Review).
7. Set next audit priority.
Respond ONLY with this JSON:
{{
"recommendation": "...",
"confidence": 0.0,
"rationale": "...",
"conditions": ["..."],
"risk_flags": ["..."],
"renewal_fee_category": "...",
"next_audit_priority": "..."
}}
"""
try:
result = _gemini_json(prompt)
result["generated_at"] = _now_iso()
return jsonify(result), 200
except Exception as e:
logger.error(f"renewal-advisory error: {e}")
return jsonify({"error": str(e)}), 500
# -----------------------------------------------------------------------------
# 4. SYSTEM ENDPOINTS
# -----------------------------------------------------------------------------
@app.route("/health", methods=["GET"])
def health():
return jsonify({
"status": "operational",
"service": "KZNGBB AI Intelligence Layer",
"version": "1.0.0",
"model": GEMINI_FLASH,
"endpoints": [
"/api/ai/operator-risk",
"/api/ai/screen-application",
"/api/ai/compliance-narrative",
"/api/ai/csi-impact",
"/api/ai/revenue-anomaly",
"/api/ai/regulatory-query",
"/api/ai/geographic-intel",
"/api/ai/generate-report",
"/api/ai/violation-patterns",
"/api/ai/renewal-advisory"
],
"timestamp": _now_iso()
}), 200
@app.route("/", methods=["GET"])
def root():
return jsonify({
"name": "KZNGBB AI Intelligence Layer",
"description": "Gemini-powered regulatory intelligence for the KwaZulu-Natal Gambling and Betting Board",
"status": "operational",
"docs": "/health"
}), 200
# -----------------------------------------------------------------------------
# 5. MAIN
# -----------------------------------------------------------------------------
if __name__ == "__main__":
logger.info("KZNGBB AI Intelligence Layer starting on port 7860...")
app.run(debug=False, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))