Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ graph TB
style API fill:#2563eb,color:#fff
style NEO fill:#4581C3,color:#fff
style OUT fill:#059669,color:#fff
fallback (no LLM) (Cypher DDL) invocations
```

---
Expand All @@ -45,7 +44,7 @@ graph TB
```
fingraph-sentinel/
├── src/
│ ├── api/main.py # FastAPI: /solve, /agent, /schema, /domains, /health
│ ├── api/main.py # FastAPI: /solve, /playbooks, /schemas, /schema/{domain}, /algorithms, /algorithm/{name}, /health
│ ├── agent/sentinel.py # LangChain agent → loads system prompt, invokes LLM
│ ├── graph/
│ │ ├── schema.py # Cypher DDL for 4 BFSI domains
Expand All @@ -60,7 +59,7 @@ fingraph-sentinel/
├── prompts/system_prompt.md # FinGraph Sentinel persona (1,200+ words)
├── data/seed_cypher.cql # Sample BFSI graph (parties, accounts, ownership chain)
├── examples/solved_aml.json # Fully worked AML solution (1,800+ words)
├── tests/ # 15 tests (API + graph integration)
├── tests/ # 17 tests (15 API + 2 graph integration)
├── configs/model_config.yaml
├── docker-compose.yml # API + Neo4j + optional Ollama
├── Dockerfile # Multi-stage Python 3.11-slim
Expand Down Expand Up @@ -106,22 +105,26 @@ make seed

### Mode 1: Deterministic (default — no LLM required)

The agent matches your problem against its knowledge base of solved BFSI playbooks (4 domains × full 4-section solutions). Returns a comprehensive, production-ready answer in under 100ms — ideal for CI/CD and quick prototyping.
The agent matches your problem against its knowledge base of solved BFSI playbooks (4 domains × full 8-section solutions). Returns a comprehensive, production-ready answer in under 100ms — ideal for CI/CD and quick prototyping.

### Mode 2: LLM-powered (Ollama or OpenAI-compatible)

Set `FS_LLM_PROVIDER=ollama` in `.env`. The agent loads the full FinGraph Sentinel system prompt, invokes the LLM, and parses the output into the canonical 4-section structure. Use `docker-compose --profile llm up` to also start Ollama.
Set `FS_LLM_PROVIDER=ollama` in `.env`. The agent loads the full FinGraph Sentinel system prompt, invokes the LLM, and parses the output into the canonical 8-section structure. Use `docker-compose --profile llm up` to also start Ollama.

### The 4-Section Canon
### The 8-Section Canon

Every solution follows this exact structure:
Every solution follows this exact structure (mirroring the system prompt and the `SolutionResponse` model):

| Section | Content |
|:--------|:--------|
| **Problem Decomposition** | Current-state failure analysis, why tabular/SQL approaches fail, graph-theoretic problem framing |
| **Knowledge Graph Schema** | Complete Cypher DDL (constraints, indexes, node taxonomies, relationship types) with GDS projections |
| **Algorithms & Methodology** | Streaming + batch pipelines, specific graph algorithms with runnable Cypher, GNN architectures (layers, dims, loss) |
| **KPIs & Compliance** | Numeric KPI targets with measurement methodology, regulatory alignment matrix (GDPR/6AMLD/Basel/FATF citations), audit trail specification |
| **1. Problem Decomposition** | Current-state failure analysis, why tabular/SQL approaches fail, graph-theoretic problem framing, explicit assumptions |
| **2. Knowledge Graph Schema** | Complete Cypher DDL (constraints, indexes, node taxonomies, relationship types) with GDS projections |
| **3. Graph Algorithm Selection** | Specific algorithms with parameters & complexity, runnable Cypher, GNN architectures (layers, dims, loss, train regime) |
| **4. Gen AI Integration** | Graph RAG pipeline: subgraph retrieval, serialization, prompt templates, anti-hallucination guardrails |
| **5. Architecture Diagram** | Ingestion → graph storage → ML inference → Gen AI reasoning → API, with latency budgets |
| **6. Implementation Roadmap** | Phased plan (weeks, deliverables, exit criteria) from graph construction to production hardening |
| **7. Success Metrics** | Numeric KPI targets (baseline → target) with measurement methodology |
| **8. Risk & Mitigation** | GDPR/PII, LLM hallucination, scalability/sharding, adversarial attacks, model drift, legacy integration |

---

Expand Down Expand Up @@ -196,4 +199,3 @@ poetry run pytest tests/ -v --cov=src --cov-report=term-missing
## 📄 License

MIT — see [LICENSE](LICENSE).
# Community Update
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ build-backend = "poetry.core.masonry.api"

[tool.ruff]
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
ignore = ["E501"]

Expand Down
28 changes: 19 additions & 9 deletions src/agent/sentinel.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import json
import os
import re
from dataclasses import dataclass, field
from dataclasses import dataclass, field, replace
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -152,19 +152,27 @@ def _llm_solve(problem: str, domain: str = "") -> SentinelSolution:
try:
if model_provider == "openai":
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model=model_name, base_url=api_base or None, api_key=api_key, temperature=0.2)
from pydantic import SecretStr

llm = ChatOpenAI(
model=model_name,
base_url=api_base or None,
api_key=SecretStr(api_key),
temperature=0.2,
)
else:
from langchain_ollama import ChatOllama
from langchain_ollama import ChatOllama # type: ignore[import-not-found]

llm = ChatOllama(model=model_name, base_url=api_base or "http://localhost:11434", temperature=0.2)

response = llm.invoke(messages)
raw = response.content if hasattr(response, "content") else str(response)
content = response.content if hasattr(response, "content") else str(response)
raw = content if isinstance(content, str) else str(content)
except Exception as exc:
# Fall back to rule-based on any LLM error
sol = _rule_match(problem)
sol.raw = f"(LLM unavailable: {exc})\n\n{_rule_match(problem).raw}"
# Fall back to rule-based on any LLM error. Copy so the cached KB
# singleton is not mutated, and keep the caller's domain/problem.
sol = replace(_rule_match(problem, domain), problem=problem)
sol.raw = f"(LLM unavailable: {exc})\n\n{sol.raw}"
return sol

return _parse_llm_output(raw, problem, domain)
Expand Down Expand Up @@ -251,7 +259,9 @@ def solve(problem: str, domain: str = "", use_llm: bool | None = None) -> Sentin

if use_llm:
return _llm_solve(problem, domain)
return _rule_match(problem, domain)
# Return a copy so the cached KB singleton is not mutated; always echo the
# caller's problem rather than the canned example problem.
return replace(_rule_match(problem, domain), problem=problem)


def get_playbooks() -> list[dict[str, str]]:
Expand Down
2 changes: 1 addition & 1 deletion src/graph/neo4j_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async def get_driver():
global _DRIVER
if _DRIVER is None:
try:
from neo4j import AsyncGraphDatabase
from neo4j import AsyncGraphDatabase # type: ignore[import-not-found]

uri, (user, pwd) = _get_uri(), _get_auth()
_DRIVER = AsyncGraphDatabase.driver(uri, auth=(user, pwd))
Expand Down
13 changes: 12 additions & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"FinGraph Sentinel — BFSI Graph + GenAI Agent API tests."
"""FinGraph Sentinel — BFSI Graph + GenAI Agent API tests."""

from fastapi.testclient import TestClient

Expand Down Expand Up @@ -90,6 +90,17 @@ def test_solve_auto_detect_domain():
assert data["domain"] in ("aml_fraud", "general_bfsi")


def test_solve_echoes_submitted_problem():
"""The response must echo the caller's problem, not the canned playbook example."""
problem = (
"Detect money-mule networks across 2M accounts with real-time transaction "
"screening for layering rings and smurfing patterns across 4 jurisdictions."
)
resp = client.post("/api/v1/solve", json={"problem": problem, "domain": "aml_fraud"})
assert resp.status_code == 200
assert resp.json()["problem"] == problem


def test_solve_rejects_short_problem():
resp = client.post("/api/v1/solve", json={"problem": "too short"})
assert resp.status_code == 422
Expand Down
Loading