Spaces:
Sleeping
Sleeping
Commit ·
c4cc55b
1
Parent(s): f818de0
feat(ai/forensics):complete — Revolving Door + TBML
Browse files- ai/forensics/revolving_door.py: career transition detector
Cooling-off violation: government role → private board < 365 days
Pre-employment benefit: company received contracts before appointment
Fallback-safe with sample data when DB unavailable
- ai/forensics/tbml_detector.py: trade-based transfer indicators
Price anomaly: contracts > 2.5 std-dev from entity mean
Subcontract loop: circular re-award chain detection via Neo4j cycles
Director-change window: directorship change within 90 days of contract
- api/routes/conflict.py: GET /conflict/revolving-door/{entity_id},
GET /conflict/tbml/{entity_id}
- api/main.py: conflict router registered
- ai/forensics/revolving_door.py +157 -0
- ai/forensics/tbml_detector.py +193 -0
- api/main.py +2 -1
- api/routes/conflict.py +30 -0
ai/forensics/revolving_door.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, sys
|
| 2 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
| 3 |
+
|
| 4 |
+
from datetime import datetime, date
|
| 5 |
+
from loguru import logger
|
| 6 |
+
|
| 7 |
+
COOLING_OFF_DAYS = 365 # 1 year minimum expected gap
|
| 8 |
+
BENEFIT_WINDOW = 730 # 2 years pre-appointment benefit window
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class RevolvingDoorDetector:
|
| 12 |
+
"""
|
| 13 |
+
Detects career transitions from regulatory/government roles
|
| 14 |
+
to private-sector boards and companies that were regulated
|
| 15 |
+
or benefited from the official's decisions.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
def analyze(self, entity_id: str, entity_name: str,
|
| 19 |
+
driver=None) -> dict:
|
| 20 |
+
logger.info(f"[RevolvingDoor] Analyzing {entity_name}")
|
| 21 |
+
|
| 22 |
+
transitions = self._fetch_transitions(entity_id, driver)
|
| 23 |
+
findings = []
|
| 24 |
+
positive = []
|
| 25 |
+
|
| 26 |
+
for t in transitions:
|
| 27 |
+
gap_days = self._day_gap(
|
| 28 |
+
t.get("left_date",""), t.get("joined_date","")
|
| 29 |
+
)
|
| 30 |
+
if gap_days is None:
|
| 31 |
+
continue
|
| 32 |
+
|
| 33 |
+
if gap_days < COOLING_OFF_DAYS:
|
| 34 |
+
findings.append({
|
| 35 |
+
"type": "cooling_off_violation",
|
| 36 |
+
"severity": "HIGH" if gap_days < 180 else "MODERATE",
|
| 37 |
+
"description": (
|
| 38 |
+
f"{entity_name} moved from {t.get('from_role','?')} "
|
| 39 |
+
f"at {t.get('from_org','?')} to {t.get('to_role','?')} "
|
| 40 |
+
f"at {t.get('to_org','?')} in {gap_days} days — "
|
| 41 |
+
f"below the expected {COOLING_OFF_DAYS}-day cooling-off period."
|
| 42 |
+
),
|
| 43 |
+
"evidence": [
|
| 44 |
+
f"Left: {t.get('from_org')} on {t.get('left_date')}",
|
| 45 |
+
f"Joined: {t.get('to_org')} on {t.get('joined_date')}",
|
| 46 |
+
f"Gap: {gap_days} days",
|
| 47 |
+
],
|
| 48 |
+
})
|
| 49 |
+
|
| 50 |
+
pre_benefit = self._check_pre_employment_benefit(
|
| 51 |
+
t, entity_id, driver
|
| 52 |
+
)
|
| 53 |
+
if pre_benefit:
|
| 54 |
+
findings.append(pre_benefit)
|
| 55 |
+
|
| 56 |
+
if not findings:
|
| 57 |
+
positive.append(
|
| 58 |
+
"No cooling-off violations or pre-employment benefit patterns "
|
| 59 |
+
"detected in available career transition data."
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
logger.success(
|
| 63 |
+
f"[RevolvingDoor] {entity_name}: "
|
| 64 |
+
f"{len(transitions)} transitions, {len(findings)} findings"
|
| 65 |
+
)
|
| 66 |
+
return {
|
| 67 |
+
"entity_id": entity_id,
|
| 68 |
+
"entity_name": entity_name,
|
| 69 |
+
"transitions_found": len(transitions),
|
| 70 |
+
"findings": findings,
|
| 71 |
+
"positive": positive,
|
| 72 |
+
"analyzed_at": datetime.now().isoformat(),
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
def _fetch_transitions(self, entity_id: str, driver) -> list:
|
| 76 |
+
if not driver:
|
| 77 |
+
return [
|
| 78 |
+
{
|
| 79 |
+
"from_org": "Ministry of Finance",
|
| 80 |
+
"from_role": "Joint Secretary",
|
| 81 |
+
"left_date": "2021-03-31",
|
| 82 |
+
"to_org": "HDFC Bank",
|
| 83 |
+
"to_role": "Independent Director",
|
| 84 |
+
"joined_date": "2021-07-15",
|
| 85 |
+
"entity_type": "regulator_to_private",
|
| 86 |
+
},
|
| 87 |
+
]
|
| 88 |
+
try:
|
| 89 |
+
with driver.session() as s:
|
| 90 |
+
rows = s.run(
|
| 91 |
+
"""
|
| 92 |
+
MATCH (p {id:$id})-[:WORKED_AT]->(org)
|
| 93 |
+
RETURN org.name AS from_org, org.role AS from_role,
|
| 94 |
+
org.left_date AS left_date, org.type AS org_type
|
| 95 |
+
LIMIT 20
|
| 96 |
+
""", id=entity_id
|
| 97 |
+
).data()
|
| 98 |
+
return [dict(r) for r in rows]
|
| 99 |
+
except Exception:
|
| 100 |
+
return []
|
| 101 |
+
|
| 102 |
+
def _check_pre_employment_benefit(self, transition: dict,
|
| 103 |
+
entity_id: str, driver) -> dict | None:
|
| 104 |
+
if not driver:
|
| 105 |
+
return None
|
| 106 |
+
try:
|
| 107 |
+
to_org = transition.get("to_org","")
|
| 108 |
+
with driver.session() as s:
|
| 109 |
+
row = s.run(
|
| 110 |
+
"""
|
| 111 |
+
MATCH (c:Company {name:$name})-[:WON_CONTRACT]->(ct:Contract)
|
| 112 |
+
WHERE ct.order_date >= $start AND ct.order_date <= $end
|
| 113 |
+
RETURN count(ct) AS n, sum(ct.amount_crore) AS total
|
| 114 |
+
""",
|
| 115 |
+
name=to_org,
|
| 116 |
+
start=transition.get("left_date","2000-01-01"),
|
| 117 |
+
end=transition.get("joined_date","2099-01-01"),
|
| 118 |
+
).single()
|
| 119 |
+
if row and row.get("n",0) >= 2:
|
| 120 |
+
return {
|
| 121 |
+
"type": "pre_employment_benefit",
|
| 122 |
+
"severity": "HIGH",
|
| 123 |
+
"description": (
|
| 124 |
+
f"{to_org} received {row['n']} government contracts "
|
| 125 |
+
f"worth Rs {row.get('total',0):.1f} Cr during the "
|
| 126 |
+
f"period between the official's departure and board appointment."
|
| 127 |
+
),
|
| 128 |
+
"evidence": [
|
| 129 |
+
f"Contracts: {row['n']}",
|
| 130 |
+
f"Total: Rs {row.get('total',0):.1f} Cr",
|
| 131 |
+
f"Window: {transition.get('left_date')} → {transition.get('joined_date')}",
|
| 132 |
+
],
|
| 133 |
+
}
|
| 134 |
+
except Exception:
|
| 135 |
+
pass
|
| 136 |
+
return None
|
| 137 |
+
|
| 138 |
+
def _day_gap(self, date_a: str, date_b: str):
|
| 139 |
+
try:
|
| 140 |
+
d1 = date.fromisoformat(date_a[:10])
|
| 141 |
+
d2 = date.fromisoformat(date_b[:10])
|
| 142 |
+
return abs((d2 - d1).days)
|
| 143 |
+
except Exception:
|
| 144 |
+
return None
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
if __name__ == "__main__":
|
| 148 |
+
print("=" * 55)
|
| 149 |
+
print("BharatGraph — Revolving Door Test")
|
| 150 |
+
print("=" * 55)
|
| 151 |
+
r = RevolvingDoorDetector()
|
| 152 |
+
result = r.analyze("pol_001", "Test Official", driver=None)
|
| 153 |
+
print(f"\n Transitions: {result['transitions_found']}")
|
| 154 |
+
print(f" Findings: {len(result['findings'])}")
|
| 155 |
+
for f in result["findings"]:
|
| 156 |
+
print(f" [{f['severity']}] {f['type']}: {f['description'][:70]}")
|
| 157 |
+
print("\nDone!")
|
ai/forensics/tbml_detector.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, sys, statistics
|
| 2 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
| 3 |
+
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from loguru import logger
|
| 6 |
+
|
| 7 |
+
PRICE_ANOMALY_SIGMA = 2.5 # std dev threshold for price anomaly
|
| 8 |
+
SUBCONTRACT_DEPTH = 3 # max re-award chain considered suspicious
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class TBMLDetector:
|
| 12 |
+
"""
|
| 13 |
+
Trade-Based indicators of value transfer:
|
| 14 |
+
1. Contract price anomaly vs category median
|
| 15 |
+
2. Subcontract loop detection (A awards to B, B to C, C to A)
|
| 16 |
+
3. Award-to-director-change window (director changed shortly after contract)
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
def analyze(self, entity_id: str, driver=None) -> dict:
|
| 20 |
+
logger.info(f"[TBML] Analyzing {entity_id}")
|
| 21 |
+
|
| 22 |
+
findings = []
|
| 23 |
+
positive = []
|
| 24 |
+
|
| 25 |
+
price_findings = self._price_anomaly(entity_id, driver)
|
| 26 |
+
loop_findings = self._subcontract_loop(entity_id, driver)
|
| 27 |
+
window_findings = self._award_director_window(entity_id, driver)
|
| 28 |
+
|
| 29 |
+
findings.extend(price_findings)
|
| 30 |
+
findings.extend(loop_findings)
|
| 31 |
+
findings.extend(window_findings)
|
| 32 |
+
|
| 33 |
+
if not findings:
|
| 34 |
+
positive.append(
|
| 35 |
+
"Trade-based transfer analysis found no significant anomalies "
|
| 36 |
+
"in contract pricing, subcontracting patterns, or director changes."
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
logger.success(
|
| 40 |
+
f"[TBML] {entity_id}: {len(findings)} findings"
|
| 41 |
+
)
|
| 42 |
+
return {
|
| 43 |
+
"entity_id": entity_id,
|
| 44 |
+
"findings": findings,
|
| 45 |
+
"positive": positive,
|
| 46 |
+
"analyzed_at":datetime.now().isoformat(),
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
def _price_anomaly(self, entity_id: str, driver) -> list:
|
| 50 |
+
contracts = self._fetch_contracts(entity_id, driver)
|
| 51 |
+
if len(contracts) < 3:
|
| 52 |
+
return []
|
| 53 |
+
|
| 54 |
+
amounts = [float(c.get("amount_crore") or 0) for c in contracts]
|
| 55 |
+
amounts = [a for a in amounts if a > 0]
|
| 56 |
+
if len(amounts) < 3:
|
| 57 |
+
return []
|
| 58 |
+
|
| 59 |
+
mean = statistics.mean(amounts)
|
| 60 |
+
stdev = statistics.stdev(amounts)
|
| 61 |
+
if stdev == 0:
|
| 62 |
+
return []
|
| 63 |
+
|
| 64 |
+
outliers = [
|
| 65 |
+
c for c in contracts
|
| 66 |
+
if abs(float(c.get("amount_crore") or 0) - mean) > PRICE_ANOMALY_SIGMA * stdev
|
| 67 |
+
]
|
| 68 |
+
if not outliers:
|
| 69 |
+
return []
|
| 70 |
+
|
| 71 |
+
return [{
|
| 72 |
+
"type": "contract_price_anomaly",
|
| 73 |
+
"severity": "HIGH" if len(outliers) >= 2 else "MODERATE",
|
| 74 |
+
"description": (
|
| 75 |
+
f"{len(outliers)} contract(s) have values more than "
|
| 76 |
+
f"{PRICE_ANOMALY_SIGMA} standard deviations from the "
|
| 77 |
+
f"entity's contract average (Rs {mean:.1f} Cr). "
|
| 78 |
+
f"Abnormal contract pricing may indicate value inflation."
|
| 79 |
+
),
|
| 80 |
+
"evidence": [
|
| 81 |
+
f"Contract Rs {float(c.get('amount_crore',0)):.1f} Cr "
|
| 82 |
+
f"vs mean Rs {mean:.1f} Cr (z={abs(float(c.get('amount_crore',0))-mean)/stdev:.1f}σ)"
|
| 83 |
+
for c in outliers[:3]
|
| 84 |
+
],
|
| 85 |
+
}]
|
| 86 |
+
|
| 87 |
+
def _subcontract_loop(self, entity_id: str, driver) -> list:
|
| 88 |
+
if not driver:
|
| 89 |
+
return []
|
| 90 |
+
try:
|
| 91 |
+
with driver.session() as s:
|
| 92 |
+
rows = s.run(
|
| 93 |
+
"""
|
| 94 |
+
MATCH path = (c1:Company)-[:SUBCONTRACTS_TO*2..4]->(c1)
|
| 95 |
+
WHERE any(n IN nodes(path) WHERE n.id = $id)
|
| 96 |
+
RETURN length(path) AS depth,
|
| 97 |
+
[n IN nodes(path) | n.name] AS loop_nodes
|
| 98 |
+
LIMIT 5
|
| 99 |
+
""", id=entity_id
|
| 100 |
+
).data()
|
| 101 |
+
if rows:
|
| 102 |
+
return [{
|
| 103 |
+
"type": "subcontract_loop",
|
| 104 |
+
"severity": "HIGH",
|
| 105 |
+
"description": (
|
| 106 |
+
f"Circular subcontracting detected: "
|
| 107 |
+
f"{' → '.join((rows[0].get('loop_nodes') or [])[:4])}. "
|
| 108 |
+
f"Contract re-award loops are a structural indicator "
|
| 109 |
+
f"of artificial transaction chains."
|
| 110 |
+
),
|
| 111 |
+
"evidence": [
|
| 112 |
+
f"Loop depth: {rows[0].get('depth')} hops"
|
| 113 |
+
],
|
| 114 |
+
}]
|
| 115 |
+
except Exception:
|
| 116 |
+
pass
|
| 117 |
+
return []
|
| 118 |
+
|
| 119 |
+
def _award_director_window(self, entity_id: str, driver) -> list:
|
| 120 |
+
if not driver:
|
| 121 |
+
return []
|
| 122 |
+
try:
|
| 123 |
+
with driver.session() as s:
|
| 124 |
+
rows = s.run(
|
| 125 |
+
"""
|
| 126 |
+
MATCH (p {id:$id})-[:DIRECTOR_OF]->(c:Company)
|
| 127 |
+
-[:WON_CONTRACT]->(ct:Contract)
|
| 128 |
+
WHERE ct.order_date IS NOT NULL
|
| 129 |
+
AND c.director_change_date IS NOT NULL
|
| 130 |
+
AND abs(duration.inDays(
|
| 131 |
+
date(ct.order_date),
|
| 132 |
+
date(c.director_change_date)
|
| 133 |
+
).days) <= 90
|
| 134 |
+
RETURN c.name AS company, ct.order_date AS award,
|
| 135 |
+
c.director_change_date AS change_date
|
| 136 |
+
LIMIT 5
|
| 137 |
+
""", id=entity_id
|
| 138 |
+
).data()
|
| 139 |
+
if rows:
|
| 140 |
+
return [{
|
| 141 |
+
"type": "director_change_near_award",
|
| 142 |
+
"severity": "MODERATE",
|
| 143 |
+
"description": (
|
| 144 |
+
f"Director change at {rows[0].get('company','?')} "
|
| 145 |
+
f"occurred within 90 days of contract award on "
|
| 146 |
+
f"{rows[0].get('award','?')}. Director substitution "
|
| 147 |
+
f"near contract award is a structural risk indicator."
|
| 148 |
+
),
|
| 149 |
+
"evidence": [
|
| 150 |
+
f"Award: {r.get('award')} | Director change: {r.get('change_date')}"
|
| 151 |
+
for r in rows[:3]
|
| 152 |
+
],
|
| 153 |
+
}]
|
| 154 |
+
except Exception:
|
| 155 |
+
pass
|
| 156 |
+
return []
|
| 157 |
+
|
| 158 |
+
def _fetch_contracts(self, entity_id: str, driver) -> list:
|
| 159 |
+
if not driver:
|
| 160 |
+
return [
|
| 161 |
+
{"id":"c1","amount_crore":12.0,"buyer_org":"MoRTH"},
|
| 162 |
+
{"id":"c2","amount_crore":11.5,"buyer_org":"MoRTH"},
|
| 163 |
+
{"id":"c3","amount_crore":89.0,"buyer_org":"MoRTH"},
|
| 164 |
+
{"id":"c4","amount_crore":13.0,"buyer_org":"MoRTH"},
|
| 165 |
+
]
|
| 166 |
+
try:
|
| 167 |
+
with driver.session() as s:
|
| 168 |
+
rows = s.run(
|
| 169 |
+
"""
|
| 170 |
+
MATCH (p {id:$id})-[:DIRECTOR_OF]->(c:Company)
|
| 171 |
+
-[:WON_CONTRACT]->(ct:Contract)
|
| 172 |
+
RETURN ct.id AS id, ct.amount_crore AS amount_crore,
|
| 173 |
+
ct.buyer_org AS buyer_org, ct.order_date AS date
|
| 174 |
+
LIMIT 50
|
| 175 |
+
""", id=entity_id
|
| 176 |
+
).data()
|
| 177 |
+
return [dict(r) for r in rows]
|
| 178 |
+
except Exception:
|
| 179 |
+
return []
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
if __name__ == "__main__":
|
| 183 |
+
print("=" * 55)
|
| 184 |
+
print("BharatGraph — TBML Detector Test")
|
| 185 |
+
print("=" * 55)
|
| 186 |
+
t = TBMLDetector()
|
| 187 |
+
r = t.analyze("pol_001", driver=None)
|
| 188 |
+
print(f"\n Findings: {len(r['findings'])}")
|
| 189 |
+
for f in r["findings"]:
|
| 190 |
+
print(f" [{f['severity']}] {f['type']}: {f['description'][:70]}")
|
| 191 |
+
if r["positive"]:
|
| 192 |
+
print(f" Positive: {r['positive'][0][:70]}")
|
| 193 |
+
print("\nDone!")
|
api/main.py
CHANGED
|
@@ -10,7 +10,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
| 10 |
from loguru import logger
|
| 11 |
|
| 12 |
from api.dependencies import get_driver, close_driver
|
| 13 |
-
from api.routes import search, profile, graph, risk, multilingual, export, admin, investigation, affidavit, biography, benami, sources, procurement
|
| 14 |
from api.models import HealthResponse, StatsResponse
|
| 15 |
|
| 16 |
|
|
@@ -65,6 +65,7 @@ app.include_router(biography.router, tags=["Biography"])
|
|
| 65 |
app.include_router(benami.router, tags=["Benami"])
|
| 66 |
app.include_router(sources.router, tags=["Sources"])
|
| 67 |
app.include_router(procurement.router, tags=["Procurement"])
|
|
|
|
| 68 |
|
| 69 |
|
| 70 |
@app.get("/health", response_model=HealthResponse)
|
|
|
|
| 10 |
from loguru import logger
|
| 11 |
|
| 12 |
from api.dependencies import get_driver, close_driver
|
| 13 |
+
from api.routes import search, profile, graph, risk, multilingual, export, admin, investigation, affidavit, biography, benami, sources, procurement, conflict
|
| 14 |
from api.models import HealthResponse, StatsResponse
|
| 15 |
|
| 16 |
|
|
|
|
| 65 |
app.include_router(benami.router, tags=["Benami"])
|
| 66 |
app.include_router(sources.router, tags=["Sources"])
|
| 67 |
app.include_router(procurement.router, tags=["Procurement"])
|
| 68 |
+
app.include_router(conflict.router, tags=["Conflict"])
|
| 69 |
|
| 70 |
|
| 71 |
@app.get("/health", response_model=HealthResponse)
|
api/routes/conflict.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, sys
|
| 2 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
| 3 |
+
|
| 4 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 5 |
+
from loguru import logger
|
| 6 |
+
from api.dependencies import get_db
|
| 7 |
+
from ai.forensics.revolving_door import RevolvingDoorDetector
|
| 8 |
+
from ai.forensics.tbml_detector import TBMLDetector
|
| 9 |
+
|
| 10 |
+
router = APIRouter()
|
| 11 |
+
rev_door = RevolvingDoorDetector()
|
| 12 |
+
tbml = TBMLDetector()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@router.get("/conflict/revolving-door/{entity_id}")
|
| 16 |
+
def revolving_door(entity_id: str, driver=Depends(get_db)):
|
| 17 |
+
with driver.session() as s:
|
| 18 |
+
row = s.run(
|
| 19 |
+
"MATCH (n {id:$id}) RETURN n.name AS name", id=entity_id
|
| 20 |
+
).single()
|
| 21 |
+
if not row:
|
| 22 |
+
raise HTTPException(status_code=404,
|
| 23 |
+
detail=f"Entity {entity_id} not found")
|
| 24 |
+
name = row.get("name") or entity_id
|
| 25 |
+
return rev_door.analyze(entity_id, name, driver=driver)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@router.get("/conflict/tbml/{entity_id}")
|
| 29 |
+
def tbml_analysis(entity_id: str, driver=Depends(get_db)):
|
| 30 |
+
return tbml.analyze(entity_id, driver=driver)
|