Spaces:
Build error
Build error
Merge pull request #105 from abinaze/fix/r5-wire-cache-ttl
Browse filesWire get_graph_depth() into 5 internal investigator queries
- ai/deep_investigator.py +17 -6
- ai/investigators/historical_investigator.py +7 -2
- api/routes/risk.py +7 -1
ai/deep_investigator.py
CHANGED
|
@@ -3,6 +3,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
| 3 |
|
| 4 |
from datetime import datetime
|
| 5 |
from loguru import logger
|
|
|
|
| 6 |
|
| 7 |
|
| 8 |
LAYER_NAMES = [
|
|
@@ -204,6 +205,15 @@ class DeepInvestigator:
|
|
| 204 |
|
| 205 |
def _layer_3_patterns(self, entity_id: str, session) -> dict:
|
| 206 |
findings = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
# Pattern 1: Company with multiple directors that won contracts
|
| 209 |
rows = session.run(
|
|
@@ -223,8 +233,8 @@ class DeepInvestigator:
|
|
| 223 |
|
| 224 |
# Pattern 2: Audit flags on connected schemes
|
| 225 |
rows = session.run(
|
| 226 |
-
"""
|
| 227 |
-
MATCH (n {id:$id})-[*1..
|
| 228 |
RETURN a.title AS title, a.year AS year, a.amount_crore AS amount LIMIT 5
|
| 229 |
""", id=entity_id
|
| 230 |
).data()
|
|
@@ -237,8 +247,8 @@ class DeepInvestigator:
|
|
| 237 |
|
| 238 |
# Pattern 3: Electoral bond connections
|
| 239 |
rows = session.run(
|
| 240 |
-
"""
|
| 241 |
-
MATCH (n {id:$id})-[*1..
|
| 242 |
RETURN e.donor_name AS donor, e.amount AS amount, e.party AS party LIMIT 5
|
| 243 |
""", id=entity_id
|
| 244 |
).data()
|
|
@@ -258,11 +268,12 @@ class DeepInvestigator:
|
|
| 258 |
|
| 259 |
def _layer_4_timeline(self, entity_id: str, session) -> dict:
|
| 260 |
events = []
|
|
|
|
| 261 |
|
| 262 |
# Timeline events from multiple sources
|
| 263 |
rows = session.run(
|
| 264 |
-
"""
|
| 265 |
-
MATCH (n {id:$id})-[:DIRECTOR_OF|WON_CONTRACT|FLAGS|MENTIONED_IN|AUDITS*1..
|
| 266 |
WHERE m.order_date IS NOT NULL OR m.year IS NOT NULL OR m.scraped_at IS NOT NULL
|
| 267 |
RETURN coalesce(m.name,m.title,m.id) AS name,
|
| 268 |
labels(m)[0] AS type,
|
|
|
|
| 3 |
|
| 4 |
from datetime import datetime
|
| 5 |
from loguru import logger
|
| 6 |
+
from config.model_selector import get_graph_depth
|
| 7 |
|
| 8 |
|
| 9 |
LAYER_NAMES = [
|
|
|
|
| 205 |
|
| 206 |
def _layer_3_patterns(self, entity_id: str, session) -> dict:
|
| 207 |
findings = []
|
| 208 |
+
# R-5 fix: was a hardcoded *1..2 in both queries below. Safe to wire
|
| 209 |
+
# tier-aware here specifically because 2 IS the current hardcoded
|
| 210 |
+
# value AND PROFILE["low"]'s value -- so a low-tier/desktop install
|
| 211 |
+
# sees zero change, while medium/high tiers get a deeper (more
|
| 212 |
+
# thorough, not less) traversal than today's shared baseline. This
|
| 213 |
+
# is NOT true of every graph_depth call site in this codebase (see
|
| 214 |
+
# this commit's message), which is why each one was checked
|
| 215 |
+
# individually rather than assuming they all behave the same way.
|
| 216 |
+
depth = int(get_graph_depth())
|
| 217 |
|
| 218 |
# Pattern 1: Company with multiple directors that won contracts
|
| 219 |
rows = session.run(
|
|
|
|
| 233 |
|
| 234 |
# Pattern 2: Audit flags on connected schemes
|
| 235 |
rows = session.run(
|
| 236 |
+
f"""
|
| 237 |
+
MATCH (n {{id:$id}})-[*1..{depth}]-(a:AuditReport)
|
| 238 |
RETURN a.title AS title, a.year AS year, a.amount_crore AS amount LIMIT 5
|
| 239 |
""", id=entity_id
|
| 240 |
).data()
|
|
|
|
| 247 |
|
| 248 |
# Pattern 3: Electoral bond connections
|
| 249 |
rows = session.run(
|
| 250 |
+
f"""
|
| 251 |
+
MATCH (n {{id:$id}})-[*1..{depth}]-(e:ElectoralBond)
|
| 252 |
RETURN e.donor_name AS donor, e.amount AS amount, e.party AS party LIMIT 5
|
| 253 |
""", id=entity_id
|
| 254 |
).data()
|
|
|
|
| 268 |
|
| 269 |
def _layer_4_timeline(self, entity_id: str, session) -> dict:
|
| 270 |
events = []
|
| 271 |
+
depth = int(get_graph_depth()) # R-5 fix -- see _layer_3_patterns
|
| 272 |
|
| 273 |
# Timeline events from multiple sources
|
| 274 |
rows = session.run(
|
| 275 |
+
f"""
|
| 276 |
+
MATCH (n {{id:$id}})-[:DIRECTOR_OF|WON_CONTRACT|FLAGS|MENTIONED_IN|AUDITS*1..{depth}]-(m)
|
| 277 |
WHERE m.order_date IS NOT NULL OR m.year IS NOT NULL OR m.scraped_at IS NOT NULL
|
| 278 |
RETURN coalesce(m.name,m.title,m.id) AS name,
|
| 279 |
labels(m)[0] AS type,
|
ai/investigators/historical_investigator.py
CHANGED
|
@@ -3,6 +3,7 @@ import sys
|
|
| 3 |
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
| 4 |
from datetime import datetime
|
| 5 |
from loguru import logger
|
|
|
|
| 6 |
|
| 7 |
|
| 8 |
class HistoricalInvestigator:
|
|
@@ -28,9 +29,13 @@ class HistoricalInvestigator:
|
|
| 28 |
|
| 29 |
entity_type = node_row["type"] if node_row else "Unknown"
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
contract_rows = session.run(
|
| 32 |
-
"""
|
| 33 |
-
MATCH (n {id: $id})-[:DIRECTOR_OF|WON_CONTRACT*1..
|
| 34 |
RETURN ct.order_date AS date, ct.amount_crore AS amount,
|
| 35 |
ct.buyer_org AS buyer
|
| 36 |
ORDER BY ct.order_date
|
|
|
|
| 3 |
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
| 4 |
from datetime import datetime
|
| 5 |
from loguru import logger
|
| 6 |
+
from config.model_selector import get_graph_depth
|
| 7 |
|
| 8 |
|
| 9 |
class HistoricalInvestigator:
|
|
|
|
| 29 |
|
| 30 |
entity_type = node_row["type"] if node_row else "Unknown"
|
| 31 |
|
| 32 |
+
# R-5 fix: was a hardcoded *1..2, which is exactly PROFILE["low"]'s
|
| 33 |
+
# value -- zero change for a low-tier/desktop install, deeper
|
| 34 |
+
# (not shallower) traversal for medium/high.
|
| 35 |
+
depth = int(get_graph_depth())
|
| 36 |
contract_rows = session.run(
|
| 37 |
+
f"""
|
| 38 |
+
MATCH (n {{id: $id}})-[:DIRECTOR_OF|WON_CONTRACT*1..{depth}]->(ct:Contract)
|
| 39 |
RETURN ct.order_date AS date, ct.amount_crore AS amount,
|
| 40 |
ct.buyer_org AS buyer
|
| 41 |
ORDER BY ct.order_date
|
api/routes/risk.py
CHANGED
|
@@ -11,6 +11,7 @@ from ai.benfords_analyzer import BenfordsAnalyzer
|
|
| 11 |
from ai.ghost_company import GhostCompanyDetector
|
| 12 |
from ai.shadow_director import ShadowDirectorDetector
|
| 13 |
from ai.explainer import generate_explanation
|
|
|
|
| 14 |
|
| 15 |
router = APIRouter()
|
| 16 |
|
|
@@ -204,8 +205,13 @@ def get_risk(entity_id: str, driver=Depends(get_db)):
|
|
| 204 |
|
| 205 |
# Phase 34: ghost company detection
|
| 206 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
co_rows = session.run(
|
| 208 |
-
"MATCH (co:Company)-[:DIRECTOR_OF|:LINKED_TO*1..
|
| 209 |
" OPTIONAL MATCH (co)-[:WON_CONTRACT]->(ct:Contract)"
|
| 210 |
" WITH co, min(ct.order_date) AS first_contract,"
|
| 211 |
" count(ct) AS contract_count,"
|
|
|
|
| 11 |
from ai.ghost_company import GhostCompanyDetector
|
| 12 |
from ai.shadow_director import ShadowDirectorDetector
|
| 13 |
from ai.explainer import generate_explanation
|
| 14 |
+
from config.model_selector import get_graph_depth
|
| 15 |
|
| 16 |
router = APIRouter()
|
| 17 |
|
|
|
|
| 205 |
|
| 206 |
# Phase 34: ghost company detection
|
| 207 |
try:
|
| 208 |
+
# R-5 fix: was a hardcoded *1..2, matching PROFILE["low"]'s value
|
| 209 |
+
# exactly -- zero change for low tier, deeper traversal for
|
| 210 |
+
# medium/high (see ai/deep_investigator.py's _layer_3_patterns
|
| 211 |
+
# for the full reasoning, identical here).
|
| 212 |
+
depth = int(get_graph_depth())
|
| 213 |
co_rows = session.run(
|
| 214 |
+
f"MATCH (co:Company)-[:DIRECTOR_OF|:LINKED_TO*1..{depth}]-(n {{id: $id}})"
|
| 215 |
" OPTIONAL MATCH (co)-[:WON_CONTRACT]->(ct:Contract)"
|
| 216 |
" WITH co, min(ct.order_date) AS first_contract,"
|
| 217 |
" count(ct) AS contract_count,"
|