bharatgraph / api /routes /multilingual.py
abinazebinoy's picture
fix(NEW-A3 part1): replace em-dashes/smart-quotes in 56+ Python files -- CI requires pure ASCII source
8a0fba4
Raw
History Blame Contribute Delete
10.5 kB
import os, sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, Query, Body
from loguru import logger
from api.dependencies import get_db
from config.languages import (
SCHEDULED_LANGUAGES,
get_language_name,
get_risk_translation,
get_ui_label,
)
from ai.translator import Translator
from ai.transliteration import Transliterator
router = APIRouter()
_translator = None
_transliterator = None
def get_translator() -> Translator:
global _translator
if _translator is None:
_translator = Translator()
return _translator
def get_transliterator() -> Transliterator:
global _transliterator
if _transliterator is None:
_transliterator = Transliterator()
return _transliterator
@router.get("/languages")
def list_languages():
return {
"supported_languages": [
{"code": code, "name": info["name"],
"native": info["native"], "script": info["script"]}
for code, info in SCHEDULED_LANGUAGES.items()
],
"total": len(SCHEDULED_LANGUAGES),
"default": "en",
"generated_at": datetime.now().isoformat(),
}
@router.get("/search/multilingual")
def multilingual_search(
q: str = Query(..., min_length=1),
lang: Optional[str] = Query("en"),
driver = Depends(get_db),
):
logger.info(f"[Multilingual] Search: '{q}' lang={lang}")
translator = get_translator()
transliterator = get_transliterator()
detected_lang = translator.detect_language(q)
if detected_lang != "en":
lang = detected_lang
search_variants = transliterator.normalize_for_search(q)
# BUG-31 FIX: added 6 previously missing node types:
# ParliamentQuestion, VigilanceCircular, ICIJEntity,
# SanctionedEntity, CourtCase, LocalBody
INDEXED_QUERIES = [
("Politician", "MATCH (n:Politician) WHERE toLower(n.name) CONTAINS toLower($q) OR any(a IN coalesce(n.aliases,[]) WHERE toLower(a) CONTAINS toLower($q)) RETURN n.id AS id, 'Politician' AS type, n.name AS name, n.state AS state, n.source AS source LIMIT 5"),
("Company", "MATCH (n:Company) WHERE toLower(n.name) CONTAINS toLower($q) OR toLower(coalesce(n.cin,'')) CONTAINS toLower($q) RETURN n.id AS id, 'Company' AS type, n.name AS name, n.state AS state, n.source AS source LIMIT 5"),
("AuditReport", "MATCH (n:AuditReport) WHERE toLower(coalesce(n.title,'')) CONTAINS toLower($q) OR toLower(coalesce(n.ministry,'')) CONTAINS toLower($q) RETURN n.id AS id, 'AuditReport' AS type, n.title AS name, n.state AS state, n.source AS source LIMIT 3"),
("Contract", "MATCH (n:Contract) WHERE toLower(coalesce(n.item_desc,'')) CONTAINS toLower($q) OR toLower(coalesce(n.buyer_org,'')) CONTAINS toLower($q) RETURN n.id AS id, 'Contract' AS type, coalesce(n.item_desc,n.order_id) AS name, null AS state, n.source AS source LIMIT 3"),
("Ministry", "MATCH (n:Ministry) WHERE toLower(n.name) CONTAINS toLower($q) RETURN n.id AS id, 'Ministry' AS type, n.name AS name, null AS state, n.source AS source LIMIT 3"),
("Party", "MATCH (n:Party) WHERE toLower(n.name) CONTAINS toLower($q) RETURN n.id AS id, 'Party' AS type, n.name AS name, null AS state, n.source AS source LIMIT 3"),
("PressRelease", "MATCH (n:PressRelease) WHERE toLower(coalesce(n.title,'')) CONTAINS toLower($q) RETURN n.id AS id, 'PressRelease' AS type, n.title AS name, null AS state, n.source AS source LIMIT 3"),
("ElectoralBond", "MATCH (n:ElectoralBond) WHERE toLower(coalesce(n.purchaser_name,'')) CONTAINS toLower($q) OR toLower(coalesce(n.redeemed_by,'')) CONTAINS toLower($q) RETURN n.id AS id, 'ElectoralBond' AS type, coalesce(n.purchaser_name,n.redeemed_by) AS name, null AS state, n.source AS source LIMIT 3"),
("NGO", "MATCH (n:NGO) WHERE toLower(coalesce(n.ngo_name,'')) CONTAINS toLower($q) RETURN n.id AS id, 'NGO' AS type, n.ngo_name AS name, n.state AS state, n.source AS source LIMIT 3"),
# BUG-31 FIX: 6 new types added below
("ParliamentQuestion","MATCH (n:ParliamentQuestion) WHERE toLower(coalesce(n.subject,'')) CONTAINS toLower($q) OR toLower(coalesce(n.member_name,'')) CONTAINS toLower($q) RETURN n.id AS id, 'ParliamentQuestion' AS type, n.subject AS name, null AS state, n.source AS source LIMIT 3"),
("VigilanceCircular", "MATCH (n:VigilanceCircular) WHERE toLower(coalesce(n.title,'')) CONTAINS toLower($q) OR toLower(coalesce(n.subject,'')) CONTAINS toLower($q) RETURN n.id AS id, 'VigilanceCircular' AS type, n.title AS name, null AS state, n.source AS source LIMIT 3"),
("ICIJEntity", "MATCH (n:ICIJEntity) WHERE toLower(coalesce(n.name,'')) CONTAINS toLower($q) OR toLower(coalesce(n.jurisdiction,'')) CONTAINS toLower($q) RETURN n.id AS id, 'ICIJEntity' AS type, n.name AS name, null AS state, n.source AS source LIMIT 3"),
("SanctionedEntity", "MATCH (n:SanctionedEntity) WHERE toLower(coalesce(n.name,'')) CONTAINS toLower($q) RETURN n.id AS id, 'SanctionedEntity' AS type, n.name AS name, null AS state, n.source AS source LIMIT 3"),
("CourtCase", "MATCH (n:CourtCase) WHERE toLower(coalesce(n.court_name,'')) CONTAINS toLower($q) OR toLower(coalesce(n.state,'')) CONTAINS toLower($q) RETURN n.id AS id, 'CourtCase' AS type, n.court_name AS name, n.state AS state, n.source AS source LIMIT 3"),
("LocalBody", "MATCH (n:LocalBody) WHERE toLower(coalesce(n.name,'')) CONTAINS toLower($q) OR toLower(coalesce(n.district,'')) CONTAINS toLower($q) RETURN n.id AS id, 'LocalBody' AS type, n.name AS name, n.state AS state, n.source AS source LIMIT 3"),
]
results = []
seen_ids = set()
with driver.session() as session:
for variant in search_variants[:3]:
for label, cypher in INDEXED_QUERIES:
try:
rows = session.run(cypher, q=variant).data()
for r in rows:
rid = r.get("id", "")
if rid and rid not in seen_ids:
seen_ids.add(rid)
results.append(r)
except Exception as qe:
logger.debug(f"[Multilingual] {label} query skipped: {qe}")
disclaimer = get_ui_label("legal_disclaimer", lang)
return {
"query": q,
"detected_language": detected_lang,
"display_language": lang,
"search_variants": search_variants,
"results": results,
"total": len(results),
"disclaimer": disclaimer,
"generated_at": datetime.now().isoformat(),
}
@router.get("/risk/multilingual/{entity_id}")
def multilingual_risk(
entity_id: str,
lang: Optional[str] = Query("en"),
driver = Depends(get_db),
):
logger.info(f"[Multilingual] Risk for {entity_id} in {lang}")
translator = get_translator()
with driver.session() as session:
row = session.run(
"MATCH (n {id: $id}) RETURN n.name AS name, "
"n.risk_score AS score, n.risk_level AS level",
id=entity_id
).single()
if not row:
return {
"error": "Entity not found",
"message": translator.get_no_data_message(lang),
}
name = row["name"] or entity_id
score = row["score"] or 0
level = row["level"] or "UNKNOWN"
translated_level = get_risk_translation(level, lang)
explanation = translator.translate_risk_explanation(name, score, translated_level, lang)
risk_label = get_ui_label("risk_indicator", lang)
disclaimer = get_ui_label("legal_disclaimer", lang)
return {
"entity_id": entity_id,
"entity_name": name,
"risk_score": score,
"risk_level": level,
"risk_level_display": translated_level,
"risk_label": risk_label,
"explanation": explanation,
"language": lang,
"language_name": get_language_name(lang),
"disclaimer": disclaimer,
"generated_at": datetime.now().isoformat(),
}
# BUG-7 FIX: translate now accepts text from EITHER query param (short text)
# OR JSON request body (long text). The frontend sends query params for text
# < 400 chars and a JSON body for longer text to avoid 414 URI Too Long.
@router.post("/translate")
def translate_text(
source_lang: str = Query("en"),
target_lang: str = Query("hi"),
text: str = Query(None), # short text via query string
body: dict = Body(default={}), # long text via JSON body
):
# Prefer body.text if present, fall back to query param
actual_text = (body.get("text") or text or "").strip()
if not actual_text:
return {"error": "No text provided", "original": "", "translated": ""}
logger.info(f"[Multilingual] Translate {source_lang}->{target_lang} "
f"({len(actual_text)} chars)")
translator = get_translator()
translated = translator.translate(actual_text, source_lang, target_lang)
return {
"original": actual_text,
"translated": translated,
"source_lang": source_lang,
"target_lang": target_lang,
"lang_name": get_language_name(target_lang),
"generated_at": datetime.now().isoformat(),
}
# BUG-32 FIX: duplicate @router.get("/ui-labels") definition -- Python
# silently registers only the SECOND one, so the first implementation
# (using get_ui_label) was always shadowed. Merged into one handler using
# the more complete get_all_labels_for_lang implementation.
@router.get("/ui-labels")
def get_ui_labels(lang: str = "en"):
"""Return all UI labels translated into the requested language."""
from config.languages import get_all_labels_for_lang, SCHEDULED_LANGUAGES
labels = get_all_labels_for_lang(lang)
return {
"language": lang,
"language_name": SCHEDULED_LANGUAGES.get(lang, {}).get("name", "English"),
"native_name": SCHEDULED_LANGUAGES.get(lang, {}).get("native", "English"),
"labels": labels,
"generated_at": datetime.now().isoformat(),
}