Added the route which will detect fraud using graph db#19
Added the route which will detect fraud using graph db#19pruthviraj-cpu wants to merge 1 commit intoparthakadam2007:mainfrom
Conversation
|
@pruthviraj-cpu is attempting to deploy a commit to the parthakadam2007's projects Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughThis pull request introduces an end-to-end anti-money laundering (AML) investigation pipeline. The changes add four fraud detection tools that execute graph database queries (detecting layering, pass-through accounts, cross-border transfers, and velocity spikes). A new orchestration module aggregates results from these tools into risk scores and triggered patterns. An explanation engine generates text narratives from investigation outcomes. A new API endpoint exposes the investigation workflow. Test data and a prompt template for case generation are added. The old database connectivity test is removed. Together, these changes establish a complete deterministic investigation system that processes case IDs and outputs structured risk assessments. 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
backend/python_ai/check.json (1)
1-535:⚠️ Potential issue | 🔴 CriticalFile is not valid JSON — multiple top-level objects will fail parsing.
This file contains multiple JSON objects concatenated together (separated by blank lines), which is neither valid JSON nor JSONL. Any standard
json.load()call will fail. Either wrap all cases in a JSON array ([{...}, {...}, ...]) or use.jsonlformat with one object per line.Additionally:
- Duplicate
case_id: The case at line 369 ("e6a58469-2c0d-4447-a5c8-a1451a6acd66") is identical to the one at line 243. The case at line 305 (...6ecd66) is a near-duplicate with almost identical data.- Excessive blank lines between objects (e.g., lines 356–367) add noise — clean up.
Suggested structure: wrap in a JSON array
-{ - "case_id": "461523f3-d013-4c7f-b5a2-56191a97c8e6", - ... -} - - -{ - "case_id": "d2108626-493b-4590-a16a-2b86a42b22d0", - ... -} +[ + { + "case_id": "461523f3-d013-4c7f-b5a2-56191a97c8e6", + ... + }, + { + "case_id": "d2108626-493b-4590-a16a-2b86a42b22d0", + ... + } +]backend/python_ai/output.json (1)
158-158:⚠️ Potential issue | 🔴 CriticalInvalid JSON:
///comment separator is not valid JSON syntax.Line 158 uses
///////////////////////////as a visual separator between two JSON objects. JSON does not support comments. This, combined with having multiple top-level objects, makes the file unparseable. Wrap in an array or split into separate files.backend/python_ai/routes/graph_route.py (1)
18-26:⚠️ Potential issue | 🟠 MajorSynchronous blocking call inside an
async defhandler will block the event loop.
run_full_investigationperforms multiple synchronous Neo4j queries (viaexecute_read_query). Calling it directly from anasync defendpoint blocks the entire event loop, stalling all concurrent requests.Either drop
async(FastAPI will run it in a threadpool automatically) or offload explicitly:Option 1 (simplest): Use a synchronous handler
-@router.get("/case/{case_id}/investigate") -async def investigate_case(case_id: str): +@router.get("/case/{case_id}/investigate") +def investigate_case(case_id: str):Option 2: Keep async and offload to thread
+import asyncio + `@router.get`("/case/{case_id}/investigate") async def investigate_case(case_id: str): - result = run_full_investigation(case_id) + result = await asyncio.to_thread(run_full_investigation, case_id)
🧹 Nitpick comments (5)
backend/python_ai/routes/graph_route.py (2)
21-24: Unhandled exceptions fromrun_full_investigationwill surface as raw 500 errors.If a Neo4j query fails (connection error, bad query, etc.), the exception will propagate uncaught. The
if not resultcheck only guards against falsy returns, not exceptions. Consider wrapping in a try/except to return a meaningful error response.Suggested improvement
+ try: + result = run_full_investigation(case_id) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Investigation failed: {str(e)}") - result = run_full_investigation(case_id) if not result: raise HTTPException(status_code=404, detail="Case not found")
4-4: Remove commented-out import.Dead code — either remove it or restore it if still needed.
backend/python_ai/services/graphtools/aml_engine.py (1)
10-64: No error handling around tool execution — a single failing query takes down the entire investigation.Each
detect_*call runs a Neo4j query. If any one fails (network issue, syntax error, missing data), the entirerun_full_investigationcall raises an unhandled exception. Consider catching per-tool exceptions so partial results can still be returned or at least the error is attributed to the specific tool.Suggested resilient pattern
tool_results = {} + for tool_name, tool_fn in [ + ("detect_layering", detect_layering), + ("detect_pass_through", detect_pass_through), + ("detect_cross_border", detect_cross_border), + ("detect_velocity_spike", detect_velocity_spike), + ]: + try: + tool_results[tool_name] = tool_fn(case_id) + except Exception as e: + tool_results[tool_name] = { + "tool": tool_name, + "case_id": case_id, + "matches_found": 0, + "details": [], + "error": str(e), + } - tool_results = { - "detect_layering": detect_layering(case_id), - "detect_pass_through": detect_pass_through(case_id), - "detect_cross_border": detect_cross_border(case_id), - "detect_velocity_spike": detect_velocity_spike(case_id), - }backend/python_ai/services/graphtools/ai_graph_tools.py (2)
4-5: Unused imports:datetimeandmean.Neither
datetimenormeanare referenced anywhere in this file.-from datetime import datetime -from statistics import mean
169-174:AML_TOOLSregistry is dead code.The four detection functions are imported and invoked directly in
aml_engine.py(lines 1-6), making this registry unused. Remove it unless it's reserved for future dynamic dispatch, in which case add a clarifying comment explaining the intent.
ℹ️ Review info
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge base: Disabled due to data retention organization setting
📒 Files selected for processing (7)
backend/python_ai/check.jsonbackend/python_ai/output.jsonbackend/python_ai/routes/graph_route.pybackend/python_ai/services/graphtools/ai_graph_tools.pybackend/python_ai/services/graphtools/aml_engine.pybackend/python_ai/services/graphtools/explainablity_engine.pybackend/python_ai/test.py
💤 Files with no reviewable changes (1)
- backend/python_ai/test.py
| { | ||
| "case_id": "9f3c2a1b-7d44-4e2f-9b77-2ac91f6e8c10", | ||
| "generated_at": "2026-02-25T14:10:00Z", | ||
| "institution": "Suryodaya National Bank", | ||
| "customer_kyc": { | ||
| "customer_id": "CUST774512", | ||
| "full_name": "Amit Kulkarni", | ||
| "dob": "1990-08-14", | ||
| "pan": "AKLPK8291R", | ||
| "aadhaar_last4": "4821", | ||
| "occupation": "Mobile Accessories Shop Owner", | ||
| "declared_annual_income": 480000, | ||
| "risk_category": "Medium", | ||
| "address": "Karve Nagar, Pune, Maharashtra, India", | ||
| "kyc_last_updated": "2025-05-12" | ||
| }, | ||
| "account_profile": { | ||
| "account_number": "SBIN77451209", | ||
| "account_type": "Savings", | ||
| "opened_date": "2018-03-22", | ||
| "average_monthly_balance": 72000, | ||
| "average_monthly_credit": 95000, | ||
| "average_monthly_debit": 91000, | ||
| "usual_transaction_pattern": "Small retail UPI and NEFT deposits below INR 20,000" | ||
| }, | ||
| "alerts": [ | ||
| { | ||
| "alert_id": "ALERT_31001", | ||
| "type": "Structured Deposits", | ||
| "description": "Multiple credits just below reporting threshold within short duration", | ||
| "trigger_time": "2026-02-24T10:15:00Z", | ||
| "severity": "High" | ||
| }, | ||
| { | ||
| "alert_id": "ALERT_31002", | ||
| "type": "Layering Pattern", | ||
| "description": "Funds routed through multiple intermediary and shell entities", | ||
| "trigger_time": "2026-02-24T11:05:00Z", | ||
| "severity": "Critical" | ||
| }, | ||
| { | ||
| "alert_id": "ALERT_31003", | ||
| "type": "Rapid Movement", | ||
| "description": "High-value funds transferred within minutes of receipt", | ||
| "trigger_time": "2026-02-24T11:20:00Z", | ||
| "severity": "High" | ||
| }, | ||
| { | ||
| "alert_id": "ALERT_31004", | ||
| "type": "Cross Border Escalation", | ||
| "description": "Funds moved to Hong Kong and UAE through layered transactions", | ||
| "trigger_time": "2026-02-24T12:10:00Z", | ||
| "severity": "Critical" | ||
| } | ||
| ], | ||
| "transactions": [ | ||
| { "tx_id": "T01", "timestamp": "2026-02-24T09:05:00Z", "from_account": "RETAIL_CUST_01", "to_account": "SBIN77451209", "amount": 12500, "currency": "INR", "channel": "UPI", "country": "India" }, | ||
| { "tx_id": "T02", "timestamp": "2026-02-24T09:22:00Z", "from_account": "RETAIL_CUST_02", "to_account": "SBIN77451209", "amount": 18200, "currency": "INR", "channel": "UPI", "country": "India" }, | ||
| { "tx_id": "T03", "timestamp": "2026-02-24T10:00:00Z", "from_account": "CASH_DEPOSIT_BRANCH_PUNE", "to_account": "SBIN77451209", "amount": 299800, "currency": "INR", "channel": "Cash", "country": "India" }, | ||
| { "tx_id": "T04", "timestamp": "2026-02-24T10:07:00Z", "from_account": "CASH_DEPOSIT_BRANCH_PUNE", "to_account": "SBIN77451209", "amount": 300200, "currency": "INR", "channel": "Cash", "country": "India" }, | ||
| { "tx_id": "T05", "timestamp": "2026-02-24T10:14:00Z", "from_account": "CASH_DEPOSIT_BRANCH_PUNE", "to_account": "SBIN77451209", "amount": 298900, "currency": "INR", "channel": "Cash", "country": "India" }, | ||
| { "tx_id": "T06", "timestamp": "2026-02-24T10:20:00Z", "from_account": "SBIN77451209", "to_account": "SHELL_ALPHA_TRADERS", "amount": 299750, "currency": "INR", "channel": "IMPS", "country": "India" }, | ||
| { "tx_id": "T07", "timestamp": "2026-02-24T10:26:00Z", "from_account": "SBIN77451209", "to_account": "SHELL_BETA_EXPORTS", "amount": 300150, "currency": "INR", "channel": "IMPS", "country": "India" }, | ||
| { "tx_id": "T08", "timestamp": "2026-02-24T10:31:00Z", "from_account": "SBIN77451209", "to_account": "SHELL_GAMMA_SERVICES", "amount": 298700, "currency": "INR", "channel": "IMPS", "country": "India" }, | ||
| { "tx_id": "T09", "timestamp": "2026-02-24T10:36:00Z", "from_account": "SHELL_ALPHA_TRADERS", "to_account": "MULE_ACCOUNT_DELHI", "amount": 299750, "currency": "INR", "channel": "NEFT", "country": "India" }, | ||
| { "tx_id": "T10", "timestamp": "2026-02-24T10:42:00Z", "from_account": "SHELL_BETA_EXPORTS", "to_account": "MULE_ACCOUNT_DELHI", "amount": 300150, "currency": "INR", "channel": "NEFT", "country": "India" }, | ||
| { "tx_id": "T11", "timestamp": "2026-02-24T10:48:00Z", "from_account": "SHELL_GAMMA_SERVICES", "to_account": "MULE_ACCOUNT_DELHI", "amount": 298700, "currency": "INR", "channel": "NEFT", "country": "India" }, | ||
| { "tx_id": "T12", "timestamp": "2026-02-24T11:00:00Z", "from_account": "MULE_ACCOUNT_DELHI", "to_account": "HK_BRIDGE_8821", "amount": 898600, "currency": "INR", "channel": "SWIFT", "country": "Hong Kong" }, | ||
| { "tx_id": "T13", "timestamp": "2026-02-24T11:25:00Z", "from_account": "HK_BRIDGE_8821", "to_account": "UAE_NODE_4411", "amount": 890000, "currency": "INR", "channel": "SWIFT", "country": "United Arab Emirates" }, | ||
| { "tx_id": "T14", "timestamp": "2026-02-24T11:50:00Z", "from_account": "UAE_NODE_4411", "to_account": "SHELL_ALPHA_TRADERS", "amount": 882500, "currency": "INR", "channel": "SWIFT", "country": "India" }, | ||
| { "tx_id": "T15", "timestamp": "2026-02-24T12:05:00Z", "from_account": "SHELL_ALPHA_TRADERS", "to_account": "SBIN77451209", "amount": 440000, "currency": "INR", "channel": "RTGS", "country": "India" }, | ||
| { "tx_id": "T16", "timestamp": "2026-02-24T12:12:00Z", "from_account": "SBIN77451209", "to_account": "LOCAL_SUPPLIER_PUNE", "amount": 38000, "currency": "INR", "channel": "NEFT", "country": "India" }, | ||
| { "tx_id": "T17", "timestamp": "2026-02-24T12:20:00Z", "from_account": "SBIN77451209", "to_account": "MULE_ACCOUNT_DELHI", "amount": 435000, "currency": "INR", "channel": "IMPS", "country": "India" }, | ||
| { "tx_id": "T18", "timestamp": "2026-02-24T12:27:00Z", "from_account": "MULE_ACCOUNT_DELHI", "to_account": "HK_BRIDGE_8821", "amount": 435000, "currency": "INR", "channel": "SWIFT", "country": "Hong Kong" }, | ||
| { "tx_id": "T19", "timestamp": "2026-02-24T13:10:00Z", "from_account": "RETAIL_CUST_03", "to_account": "SBIN77451209", "amount": 15600, "currency": "INR", "channel": "UPI", "country": "India" }, | ||
| { "tx_id": "T20", "timestamp": "2026-02-24T14:05:00Z", "from_account": "SHELL_BETA_EXPORTS", "to_account": "SBIN77451209", "amount": 210000, "currency": "INR", "channel": "RTGS", "country": "India" } | ||
| ] | ||
| } No newline at end of file |
There was a problem hiding this comment.
Sensitive-looking data in test fixtures — confirm it is fully synthetic.
This file contains PAN numbers, Aadhaar (last 4), full names, DOBs, and addresses. Even for test data, ensure none of these correspond to real individuals. Consider adding a comment at the top of the file stating the data is entirely fictitious.
🧰 Tools
🪛 Biome (2.4.4)
[error] 458-535: End of file expected
(parse)
| ///Prompt | ||
| Generate a highly realistic AML investigation case in STRICT JSON format. | ||
|
|
||
| ⚠️ Output ONLY valid JSON. No explanation text. | ||
|
|
||
| Follow EXACTLY this schema: | ||
|
|
||
| { | ||
| "case_id": "...", | ||
| "generated_at": "...", | ||
| "institution": "...", | ||
| "customer_kyc": { ... }, | ||
| "account_profile": { ... }, | ||
| "alerts": [ ... ], | ||
| "transactions": [ ... ] | ||
| } | ||
|
|
||
| Requirements: | ||
|
|
||
| 1. Make transaction network COMPLEX and GRAPH-DEPENDENT: | ||
| - Use at least 15–25 transactions. | ||
| - Include layering (3–5 hops). | ||
| - Include circular fund routing (money eventually returns to origin via different path). | ||
| - Include split-and-merge structuring (1 amount → split into 3 → merged again). | ||
| - Include pass-through behavior (same amount within 10 minutes). | ||
| - Include cross-border transfers via 2 jurisdictions. | ||
| - Include at least 2 intermediary shell accounts. | ||
| - Include 1 account that acts as both receiver and sender multiple times. | ||
|
|
||
| 2. Make it HARD for rule-based detection: | ||
| - Slightly vary amounts (299,800; 300,200; etc). | ||
| - Spread transactions across multiple hours. | ||
| - Use different channels (NEFT, IMPS, SWIFT, RTGS, Online). | ||
| - Mix legitimate-looking retail deposits with suspicious activity. | ||
| - Add noise transactions that look normal. | ||
|
|
||
| 3. Ensure the suspicious pattern is ONLY clearly visible if someone: | ||
| - Traverses graph paths | ||
| - Detects multi-hop fund flow | ||
| - Detects circular return to origin | ||
| - Detects structuring across accounts | ||
|
|
||
| 4. Customer profile: | ||
| - Medium income individual | ||
| - Declared income much lower than total suspicious volume | ||
| - Realistic Indian KYC details | ||
|
|
||
| 5. Alerts: | ||
| - Add 3–4 alerts: | ||
| - Layering Pattern | ||
| - Structured Deposits | ||
| - Rapid Movement | ||
| - Cross Border Escalation | ||
|
|
||
| 6. Ensure: | ||
| - All accounts referenced in transactions exist in flow | ||
| - Timestamps are chronological | ||
| - At least 1 transaction chain is 4 hops deep | ||
| - Total suspicious flow > 15x declared annual income | ||
|
|
||
| 7. Currency: INR | ||
| 8. Use realistic names and Indian geography. | ||
|
|
||
| Output ONLY JSON. No newline at end of file |
There was a problem hiding this comment.
Prompt template embedded as raw text in a .json file — move to a dedicated file.
The AML case generation prompt (lines 322–385) is appended as plain text after JSON content, making the entire file invalid JSON. This prompt should live in its own file (e.g., prompts/aml_case_generation.txt or .md) or be stored as a properly escaped string value within a JSON structure.
🧰 Tools
🪛 Biome (2.4.4)
[error] 322-327: End of file expected
(parse)
[error] 323-323: unexpected character .
(parse)
[error] 325-325: unexpected character !
(parse)
[error] 325-325: unexpected character ️
(parse)
[error] 325-325: unexpected character .
(parse)
[error] 325-325: unexpected character .
(parse)
[error] 333-333: unexpected character .
(parse)
[error] 333-333: unexpected character .
(parse)
[error] 333-333: unexpected character .
(parse)
[error] 334-334: unexpected character .
(parse)
[error] 334-334: unexpected character .
(parse)
[error] 334-334: unexpected character .
(parse)
[error] 335-335: unexpected character .
(parse)
[error] 335-335: unexpected character .
(parse)
[error] 335-335: unexpected character .
(parse)
[error] 336-336: unexpected character .
(parse)
[error] 336-336: unexpected character .
(parse)
[error] 329-337: End of file expected
(parse)
[error] 336-336: unexpected character .
(parse)
[error] 339-339: String values must be double quoted.
(parse)
[error] 339-342: End of file expected
(parse)
[error] 341-341: Missing fraction
(parse)
[error] 341-341: Minus must be followed by a digit
(parse)
[error] 342-342: Minus must be followed by a digit
(parse)
[error] 342-342: End of file expected
(parse)
[error] 342-342: unexpected character –
(parse)
[error] 342-342: End of file expected
(parse)
[error] 342-342: String values must be double quoted.
(parse)
[error] 342-342: unexpected character .
(parse)
[error] 343-343: Minus must be followed by a digit
(parse)
[error] 343-343: String values must be double quoted.
(parse)
[error] 343-343: String values must be double quoted.
(parse)
[error] 343-343: unexpected character (
(parse)
[error] 343-343: End of file expected
(parse)
[error] 343-343: unexpected character –
(parse)
[error] 343-343: End of file expected
(parse)
[error] 343-343: String values must be double quoted.
(parse)
[error] 343-343: unexpected character )
(parse)
[error] 343-343: unexpected character .
(parse)
[error] 344-344: Minus must be followed by a digit
(parse)
[error] 344-344: String values must be double quoted.
(parse)
[error] 344-344: String values must be double quoted.
(parse)
[error] 344-344: String values must be double quoted.
(parse)
[error] 344-344: String values must be double quoted.
(parse)
[error] 344-344: unexpected character (
(parse)
[error] 344-344: String values must be double quoted.
(parse)
[error] 344-344: String values must be double quoted.
(parse)
[error] 344-344: String values must be double quoted.
(parse)
[error] 344-344: String values must be double quoted.
(parse)
[error] 344-344: String values must be double quoted.
(parse)
[error] 344-344: String values must be double quoted.
(parse)
[error] 344-344: String values must be double quoted.
(parse)
[error] 344-344: String values must be double quoted.
(parse)
[error] 344-344: unexpected character )
(parse)
[error] 344-344: unexpected character .
(parse)
[error] 345-345: Minus must be followed by a digit
(parse)
[error] 345-345: String values must be double quoted.
(parse)
[error] 345-345: String values must be double quoted.
(parse)
[error] 345-345: Minus must be followed by a digit
(parse)
[error] 345-345: String values must be double quoted.
(parse)
[error] 345-345: Minus must be followed by a digit
(parse)
[error] 345-345: String values must be double quoted.
(parse)
[error] 345-345: String values must be double quoted.
(parse)
[error] 345-345: unexpected character (
(parse)
[error] 345-345: End of file expected
(parse)
[error] 345-345: String values must be double quoted.
(parse)
[error] 345-345: unexpected character →
(parse)
[error] 345-345: String values must be double quoted.
(parse)
[error] 345-345: String values must be double quoted.
(parse)
[error] 345-345: End of file expected
(parse)
[error] 345-345: unexpected character →
(parse)
[error] 345-345: String values must be double quoted.
(parse)
[error] 345-345: String values must be double quoted.
(parse)
[error] 345-345: unexpected character )
(parse)
[error] 345-345: unexpected character .
(parse)
[error] 346-346: Minus must be followed by a digit
(parse)
[error] 346-346: String values must be double quoted.
(parse)
[error] 346-346: String values must be double quoted.
(parse)
[error] 346-346: Minus must be followed by a digit
(parse)
[error] 346-346: String values must be double quoted.
(parse)
[error] 346-346: String values must be double quoted.
(parse)
[error] 346-346: unexpected character (
(parse)
[error] 346-346: String values must be double quoted.
(parse)
[error] 346-346: String values must be double quoted.
(parse)
[error] 346-346: String values must be double quoted.
(parse)
[error] 346-346: End of file expected
(parse)
[error] 346-346: String values must be double quoted.
(parse)
[error] 346-346: unexpected character )
(parse)
[error] 346-346: unexpected character .
(parse)
[error] 347-347: Minus must be followed by a digit
(parse)
[error] 347-347: String values must be double quoted.
(parse)
[error] 347-347: String values must be double quoted.
(parse)
[error] 347-347: Minus must be followed by a digit
(parse)
[error] 347-347: String values must be double quoted.
(parse)
[error] 347-347: String values must be double quoted.
(parse)
[error] 347-347: String values must be double quoted.
(parse)
[error] 347-347: End of file expected
(parse)
[error] 347-347: String values must be double quoted.
(parse)
[error] 347-347: unexpected character .
(parse)
[error] 348-348: Minus must be followed by a digit
(parse)
[error] 348-348: String values must be double quoted.
(parse)
[error] 348-348: String values must be double quoted.
(parse)
[error] 348-348: String values must be double quoted.
(parse)
[error] 348-348: End of file expected
(parse)
[error] 348-348: String values must be double quoted.
(parse)
[error] 348-348: String values must be double quoted.
(parse)
[error] 348-348: String values must be double quoted.
(parse)
[error] 348-348: unexpected character .
(parse)
[error] 349-349: Minus must be followed by a digit
(parse)
[error] 349-349: String values must be double quoted.
(parse)
[error] 349-349: End of file expected
(parse)
[error] 349-349: String values must be double quoted.
(parse)
[error] 349-349: String values must be double quoted.
(parse)
[error] 349-349: String values must be double quoted.
(parse)
[error] 349-349: String values must be double quoted.
(parse)
[error] 349-349: String values must be double quoted.
(parse)
[error] 349-349: String values must be double quoted.
(parse)
[error] 349-349: String values must be double quoted.
(parse)
[error] 349-349: String values must be double quoted.
(parse)
[error] 349-349: String values must be double quoted.
(parse)
[error] 349-349: String values must be double quoted.
(parse)
[error] 349-349: unexpected character .
(parse)
[error] 351-351: End of file expected
(parse)
[error] 351-351: Missing fraction
(parse)
[error] 351-351: String values must be double quoted.
(parse)
[error] 351-351: String values must be double quoted.
(parse)
[error] 351-351: String values must be double quoted.
(parse)
[error] 351-351: String values must be double quoted.
(parse)
[error] 351-351: String values must be double quoted.
(parse)
[error] 351-351: Minus must be followed by a digit
(parse)
[error] 351-351: String values must be double quoted.
(parse)
[error] 351-351: String values must be double quoted.
(parse)
[error] 351-352: End of file expected
(parse)
[error] 352-352: Minus must be followed by a digit
(parse)
[error] 352-352: unexpected character (
(parse)
[error] 352-352: End of file expected
(parse)
[error] 352-352: End of file expected
(parse)
[error] 352-352: End of file expected
(parse)
[error] 352-352: unexpected character ;
(parse)
[error] 352-352: End of file expected
(parse)
[error] 352-352: End of file expected
(parse)
[error] 352-352: End of file expected
(parse)
[error] 352-352: unexpected character ;
(parse)
[error] 352-352: String values must be double quoted.
(parse)
[error] 352-352: unexpected character )
(parse)
[error] 352-352: unexpected character .
(parse)
[error] 353-353: Minus must be followed by a digit
(parse)
[error] 353-353: String values must be double quoted.
(parse)
[error] 353-353: String values must be double quoted.
(parse)
[error] 353-353: String values must be double quoted.
(parse)
[error] 353-353: String values must be double quoted.
(parse)
[error] 353-353: String values must be double quoted.
(parse)
[error] 353-353: unexpected character .
(parse)
[error] 354-354: Minus must be followed by a digit
(parse)
[error] 354-354: String values must be double quoted.
(parse)
[error] 354-354: String values must be double quoted.
(parse)
[error] 354-354: String values must be double quoted.
(parse)
[error] 354-354: unexpected character (
(parse)
[error] 354-354: String values must be double quoted.
(parse)
[error] 354-370: End of file expected
(parse)
[error] 354-354: unexpected character )
(parse)
[error] 354-354: unexpected character .
(parse)
[error] 355-355: Minus must be followed by a digit
(parse)
[error] 355-355: Minus must be followed by a digit
(parse)
[error] 355-355: unexpected character .
(parse)
[error] 356-356: Minus must be followed by a digit
(parse)
[error] 356-356: unexpected character .
(parse)
[error] 358-358: Missing fraction
(parse)
[error] 359-359: Minus must be followed by a digit
(parse)
[error] 360-360: Minus must be followed by a digit
(parse)
[error] 360-360: Minus must be followed by a digit
(parse)
[error] 361-361: Minus must be followed by a digit
(parse)
[error] 362-362: Minus must be followed by a digit
(parse)
[error] 364-364: Missing fraction
(parse)
[error] 365-365: Minus must be followed by a digit
(parse)
[error] 366-366: Minus must be followed by a digit
(parse)
[error] 367-367: Minus must be followed by a digit
(parse)
[error] 369-369: Missing fraction
(parse)
[error] 370-370: Minus must be followed by a digit
(parse)
[error] 370-370: End of file expected
(parse)
[error] 370-370: unexpected character –
(parse)
[error] 370-370: End of file expected
(parse)
[error] 370-370: String values must be double quoted.
(parse)
[error] 370-379: End of file expected
(parse)
[error] 371-371: Minus must be followed by a digit
(parse)
[error] 372-372: Minus must be followed by a digit
(parse)
[error] 373-373: Minus must be followed by a digit
(parse)
[error] 374-374: Minus must be followed by a digit
(parse)
[error] 376-376: Missing fraction
(parse)
[error] 377-377: Minus must be followed by a digit
(parse)
[error] 378-378: Minus must be followed by a digit
(parse)
[error] 379-379: Minus must be followed by a digit
(parse)
[error] 379-379: End of file expected
(parse)
[error] 379-379: String values must be double quoted.
(parse)
[error] 379-379: String values must be double quoted.
(parse)
[error] 379-379: String values must be double quoted.
(parse)
[error] 379-379: End of file expected
(parse)
[error] 379-379: String values must be double quoted.
(parse)
[error] 379-379: String values must be double quoted.
(parse)
[error] 380-380: Minus must be followed by a digit
(parse)
[error] 380-380: String values must be double quoted.
(parse)
[error] 380-380: String values must be double quoted.
(parse)
[error] 380-380: String values must be double quoted.
(parse)
[error] 380-380: unexpected character >
(parse)
[error] 380-380: End of file expected
(parse)
[error] 380-380: String values must be double quoted.
(parse)
[error] 380-380: String values must be double quoted.
(parse)
[error] 380-380: String values must be double quoted.
(parse)
[error] 380-380: String values must be double quoted.
(parse)
[error] 382-382: End of file expected
(parse)
[error] 382-382: Missing fraction
(parse)
[error] 382-382: String values must be double quoted.
(parse)
[error] 382-385: End of file expected
(parse)
[error] 383-383: Missing fraction
(parse)
[error] 383-383: unexpected character .
(parse)
[error] 385-385: unexpected character .
(parse)
| def execute_read_query(query: str, params: dict): | ||
| driver = get_driver() | ||
|
|
||
| try: | ||
| with driver.session() as session: | ||
| result = session.run(query, **params) | ||
| return [record.data() for record in result] | ||
| finally: | ||
| driver.close() |
There was a problem hiding this comment.
Critical: Creates a new Neo4j driver (+ connectivity check) on every single query.
get_driver() calls GraphDatabase.driver() and verify_connectivity() each time. Since run_full_investigation calls 4 tools, this creates 4 separate TCP connections + 4 auth handshakes + 4 connectivity pings per investigation request. This is extremely wasteful and fragile.
The Neo4j driver should be created once and reused (singleton or dependency-injected). At minimum, accept a driver/session parameter or use a module-level singleton.
Suggested approach: module-level singleton driver
-def execute_read_query(query: str, params: dict):
- driver = get_driver()
-
- try:
- with driver.session() as session:
- result = session.run(query, **params)
- return [record.data() for record in result]
- finally:
- driver.close()
+from config.database import get_driver
+
+_driver = None
+
+def _get_shared_driver():
+ global _driver
+ if _driver is None:
+ _driver = get_driver()
+ return _driver
+
+def execute_read_query(query: str, params: dict):
+ driver = _get_shared_driver()
+ with driver.session() as session:
+ result = session.run(query, **params)
+ return [record.data() for record in result]| def detect_pass_through(case_id: str): | ||
|
|
||
| query = """ | ||
| MATCH (c:Case {case_id: $case_id}) | ||
| MATCH (c)-[:INVOLVES_CUSTOMER]->(:Customer)-[:OWNS]->(acc:Account) | ||
|
|
||
| MATCH (acc)<-[in:TRANSFERRED]-(src) | ||
| MATCH (acc)-[out:TRANSFERRED]->(dest) | ||
|
|
||
| WHERE in.amount = out.amount | ||
| AND src <> dest | ||
| AND abs(duration.between(in.timestamp, out.timestamp).minutes) <= 30 | ||
|
|
||
| WITH acc.account_number AS account, | ||
| in.amount AS mirrored_amount, | ||
| in.timestamp AS incoming_time, | ||
| out.timestamp AS outgoing_time | ||
|
|
||
| RETURN DISTINCT account, | ||
| mirrored_amount, | ||
| incoming_time, | ||
| outgoing_time | ||
| ORDER BY incoming_time | ||
| """ | ||
|
|
||
| results = execute_read_query(query, {"case_id": case_id}) | ||
|
|
||
| return { | ||
| "tool": "detect_pass_through", | ||
| "case_id": case_id, | ||
| "matches_found": len(results), | ||
| "details": results | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Cypher reserved keywords Neo4j official documentation
💡 Result:
Neo4j documents Cypher “reserved keywords” in the Cypher Manual under Syntax → Keywords (and, in older manual versions, Syntax → Reserved keywords). (neo4j.com)
Key points from the official docs:
- Reserved/keywords have special meaning in Cypher and shouldn’t be used as identifiers (e.g., variable/function names; the docs also caution against using them for labels/relationship types/procedures). (neo4j.com)
- You can still use them as identifiers by escaping with backticks, e.g.
`MATCH`. (neo4j.com) - The docs provide the authoritative, version-specific lists (the set can differ by Cypher/Neo4j version). (neo4j.com)
Citations:
- 1: https://neo4j.com/docs/cypher-manual/current/syntax/keywords/?utm_source=openai
- 2: https://neo4j.com/docs/cypher-manual/current/syntax/keywords/?utm_source=openai
- 3: https://neo4j.com/docs/cypher-manual/4.4/syntax/reserved/?utm_source=openai
- 4: https://neo4j.com/docs/cypher-manual/current/syntax/keywords/?utm_source=openai
🌐 Web query:
Cypher relationship variable naming restrictions Neo4j
💡 Result:
In Cypher, a relationship variable (the identifier inside -[ ... ]-, e.g. -[r:KNOWS]->) follows the standard identifier/variable naming rules:
- Must start with a letter (Unicode letters allowed), not a digit (so
r1is OK,1ris not). [1] - After the first character, use letters/digits/underscore; symbols aren’t allowed (underscore is the main exception). [1]
- Case-sensitive (
randRare different). [1] - Must be unique within the same query scope (you can’t reuse the same variable name for a node and a relationship in the same scope, e.g.
CREATE (a)-[a]->(b)is invalid). [1] - If you need spaces, punctuation, or reserved words, you can use backticks:
MATCH ()-[my rel]->(). To include a backtick inside the name, escape it by doubling it (`myrel` ``). [2]
Examples
- Valid:
MATCH (a)-[rel]->(b) RETURN rel - Invalid:
MATCH (a)-[1rel]->(b)[1] - Valid with quoting:
MATCH (a)-[1rel]->(b)[2]
Sources: Neo4j Cypher Manual “Naming rules and recommendations” [1]; Neo4j KB on identifier quoting/escaping [2].
🌐 Web query:
Cypher reserved keywords list "in" Neo4j 5
💡 Result:
In Neo4j 5 (Cypher 5), IN is a Cypher keyword (used as the list-membership operator) and appears in Neo4j’s official “Keywords” list. If you try to use IN as an unquoted identifier (e.g., a variable name), Cypher will interpret it as the keyword; to use it as an identifier you must escape it with backticks (`IN`). [1]
Source (full keyword list): Neo4j Cypher Manual → Syntax → Keywords (includes IN). [1]
References:
[1] Neo4j Cypher Manual (current) — Syntax → Keywords https://neo4j.com/docs/cypher-manual/current/syntax/keywords/
Rename reserved keyword in to avoid parse error.
In the pass-through detection query, in is used as a relationship variable name ([in:TRANSFERRED]). In Cypher, in is a reserved keyword (used as the list-membership operator). Using it unquoted as a variable name will cause a parse error in Neo4j.
Rename to something like r_in or inTx:
Suggested fix
- MATCH (acc)<-[in:TRANSFERRED]-(src)
- MATCH (acc)-[out:TRANSFERRED]->(dest)
+ MATCH (acc)<-[r_in:TRANSFERRED]-(src)
+ MATCH (acc)-[r_out:TRANSFERRED]->(dest)
- WHERE in.amount = out.amount
+ WHERE r_in.amount = r_out.amount
AND src <> dest
- AND abs(duration.between(in.timestamp, out.timestamp).minutes) <= 30
+ AND abs(duration.between(r_in.timestamp, r_out.timestamp).minutes) <= 30
- WITH acc.account_number AS account,
- in.amount AS mirrored_amount,
- in.timestamp AS incoming_time,
- out.timestamp AS outgoing_time
+ WITH acc.account_number AS account,
+ r_in.amount AS mirrored_amount,
+ r_in.timestamp AS incoming_time,
+ r_out.timestamp AS outgoing_time📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def detect_pass_through(case_id: str): | |
| query = """ | |
| MATCH (c:Case {case_id: $case_id}) | |
| MATCH (c)-[:INVOLVES_CUSTOMER]->(:Customer)-[:OWNS]->(acc:Account) | |
| MATCH (acc)<-[in:TRANSFERRED]-(src) | |
| MATCH (acc)-[out:TRANSFERRED]->(dest) | |
| WHERE in.amount = out.amount | |
| AND src <> dest | |
| AND abs(duration.between(in.timestamp, out.timestamp).minutes) <= 30 | |
| WITH acc.account_number AS account, | |
| in.amount AS mirrored_amount, | |
| in.timestamp AS incoming_time, | |
| out.timestamp AS outgoing_time | |
| RETURN DISTINCT account, | |
| mirrored_amount, | |
| incoming_time, | |
| outgoing_time | |
| ORDER BY incoming_time | |
| """ | |
| results = execute_read_query(query, {"case_id": case_id}) | |
| return { | |
| "tool": "detect_pass_through", | |
| "case_id": case_id, | |
| "matches_found": len(results), | |
| "details": results | |
| } | |
| def detect_pass_through(case_id: str): | |
| query = """ | |
| MATCH (c:Case {case_id: $case_id}) | |
| MATCH (c)-[:INVOLVES_CUSTOMER]->(:Customer)-[:OWNS]->(acc:Account) | |
| MATCH (acc)<-[r_in:TRANSFERRED]-(src) | |
| MATCH (acc)-[r_out:TRANSFERRED]->(dest) | |
| WHERE r_in.amount = r_out.amount | |
| AND src <> dest | |
| AND abs(duration.between(r_in.timestamp, r_out.timestamp).minutes) <= 30 | |
| WITH acc.account_number AS account, | |
| r_in.amount AS mirrored_amount, | |
| r_in.timestamp AS incoming_time, | |
| r_out.timestamp AS outgoing_time | |
| RETURN DISTINCT account, | |
| mirrored_amount, | |
| incoming_time, | |
| outgoing_time | |
| ORDER BY incoming_time | |
| """ | |
| results = execute_read_query(query, {"case_id": case_id}) | |
| return { | |
| "tool": "detect_pass_through", | |
| "case_id": case_id, | |
| "matches_found": len(results), | |
| "details": results | |
| } |
| @@ -0,0 +1,95 @@ | |||
| from datetime import datetime | |||
There was a problem hiding this comment.
Filename typo: explainablity_engine.py → explainability_engine.py.
Missing the second 'i' in "explainability". This will propagate to all import statements (from .explainablity_engine import ...). Worth fixing now before more code depends on it.
| def format_datetime(dt): | ||
| if hasattr(dt, "iso_format"): | ||
| return dt.iso_format() | ||
| return str(dt) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Neo4j Python driver DateTime iso_format method API
💡 Result:
In the Neo4j Python Driver, DateTime is neo4j.time.DateTime (not datetime.datetime). Its ISO formatter is:
- API:
DateTime.iso_format(sep: str = "T") -> str - Behavior: returns an ISO-8601 formatted string by joining
self.date().iso_format()andself.timetz().iso_format()withsepbetween them. - Example:
from neo4j.time import DateTime dt = DateTime(2018, 4, 30, 12, 34, 56, 789123456) s = dt.iso_format() # default separator "T" s2 = dt.iso_format(" ") # custom separator
[1]
If you’re retrieving temporal values from query results, Neo4j will automatically convert Cypher temporal values to these neo4j.time.* types (including neo4j.time.DateTime). [2]
Sources: [1] Neo4j Python Driver docs (Temporal Data Types / DateTime.iso_format) (neo4j.com), [2] Neo4j GraphAcademy (Python drivers: dates & times in results) (graphacademy.neo4j.com)
Citations:
- 1: https://neo4j.com/docs/api/python-driver/current/types/temporal.html?utm_source=openai
- 2: https://graphacademy.neo4j.com/courses/drivers-python/2-handling-results/3-dates-and-times/?utm_source=openai
🏁 Script executed:
cat -n backend/python_ai/services/graphtools/explainablity_engine.py | head -100Repository: parthakadam2007/SAR-Generator
Length of output: 3927
🏁 Script executed:
cd backend/python_ai/services/graphtools && grep -n "import datetime" explainablity_engine.py && grep -n "datetime\." explainablity_engine.pyRepository: parthakadam2007/SAR-Generator
Length of output: 104
Remove unused datetime import and consider defensive format handling for both Neo4j and stdlib datetime objects.
The datetime import on line 1 is unused and should be removed.
Regarding format_datetime(): Neo4j's DateTime type does expose iso_format() (verified in official Neo4j Python Driver docs), and your current code correctly checks for it. However, if the function might ever receive stdlib datetime objects (which use isoformat() without underscore), the current check will miss them. Consider this defensive fix:
Suggested fix
def format_datetime(dt):
- if hasattr(dt, "iso_format"):
+ if hasattr(dt, "isoformat"):
+ return dt.isoformat()
+ elif hasattr(dt, "iso_format"):
return dt.iso_format()
return str(dt)This prioritizes stdlib datetime while preserving Neo4j DateTime support.
No description provided.