Spaces:
Running
Running
fix(NEW-A3 part1): replace em-dashes/smart-quotes in 56+ Python files -- CI requires pure ASCII source
8a0fba4 | 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 | |
| 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(), | |
| } | |
| 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(), | |
| } | |
| 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. | |
| 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. | |
| 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(), | |
| } | |