Skip to content

Added the route which will detect fraud using graph db#19

Open
pruthviraj-cpu wants to merge 1 commit intoparthakadam2007:mainfrom
pruthviraj-cpu:main
Open

Added the route which will detect fraud using graph db#19
pruthviraj-cpu wants to merge 1 commit intoparthakadam2007:mainfrom
pruthviraj-cpu:main

Conversation

@pruthviraj-cpu
Copy link
Collaborator

No description provided.

@vercel
Copy link

vercel bot commented Feb 25, 2026

@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.

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

Walkthrough

This 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)

Check name Status Explanation Resolution
Description check ❓ Inconclusive No pull request description was provided by the author. Without any descriptive content, it is unclear if the changeset rationale and implementation details have been adequately communicated. Add a detailed description explaining the purpose of the fraud detection route, the investigation workflow, and how the graph database tools work together to identify suspicious patterns.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a new route for fraud detection using graph database functionality. It is specific and directly related to the primary changeset objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🔴 Critical

File 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 .jsonl format 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 | 🔴 Critical

Invalid 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 | 🟠 Major

Synchronous blocking call inside an async def handler will block the event loop.

run_full_investigation performs multiple synchronous Neo4j queries (via execute_read_query). Calling it directly from an async def endpoint 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 from run_full_investigation will surface as raw 500 errors.

If a Neo4j query fails (connection error, bad query, etc.), the exception will propagate uncaught. The if not result check 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 entire run_full_investigation call 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: datetime and mean.

Neither datetime nor mean are referenced anywhere in this file.

-from datetime import datetime
-from statistics import mean

169-174: AML_TOOLS registry 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

📥 Commits

Reviewing files that changed from the base of the PR and between 872c365 and 3973798.

📒 Files selected for processing (7)
  • backend/python_ai/check.json
  • backend/python_ai/output.json
  • backend/python_ai/routes/graph_route.py
  • backend/python_ai/services/graphtools/ai_graph_tools.py
  • backend/python_ai/services/graphtools/aml_engine.py
  • backend/python_ai/services/graphtools/explainablity_engine.py
  • backend/python_ai/test.py
💤 Files with no reviewable changes (1)
  • backend/python_ai/test.py

Comment on lines +458 to +535
{
"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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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)

Comment on lines +322 to +385
///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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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)

Comment on lines +11 to +19
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()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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]

Comment on lines +62 to +94
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
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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:


🌐 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 r1 is OK, 1r is not). [1]
  • After the first character, use letters/digits/underscore; symbols aren’t allowed (underscore is the main exception). [1]
  • Case-sensitive (r and R are 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.

Suggested change
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Filename typo: explainablity_engine.pyexplainability_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.

Comment on lines +3 to +6
def format_datetime(dt):
if hasattr(dt, "iso_format"):
return dt.iso_format()
return str(dt)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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() and self.timetz().iso_format() with sep between 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:


🏁 Script executed:

cat -n backend/python_ai/services/graphtools/explainablity_engine.py | head -100

Repository: 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.py

Repository: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant