From 23ced6beab045007e6e2321fb2e771b196a0563d Mon Sep 17 00:00:00 2001 From: twomathematicians-code Date: Tue, 11 Aug 2026 23:03:22 +0200 Subject: [PATCH] Fix solve endpoint echo bug, type errors, and doc inconsistencies Correctness: - /solve now echoes the caller's submitted problem instead of the canned playbook example. Returns a copy via dataclasses.replace so the cached KB singleton is not mutated. The LLM-fallback branch also stopped dropping the domain hint and calling _rule_match twice. - Add regression test test_solve_echoes_submitted_problem. Type safety (mypy 4 -> 0): - Wrap ChatOpenAI api_key in SecretStr. - Coerce non-str response.content to str before parsing. - Mark optional neo4j/langchain_ollama imports type: ignore[import-not-found]. Config: - Move ruff select/ignore under [tool.ruff.lint] (removes deprecation warning). Docs (README): - Remove stray line corrupting the Mermaid architecture diagram. - Rewrite 4-Section Canon as 8-Section Canon (matches prompt/examples/model). - Fix endpoint list and test count (15 -> 17); drop stray heading. - Fix malformed single-quote docstring in test_api.py. Verified: pytest 16 passed/2 skipped, ruff clean, mypy clean. --- README.md | 26 ++++++++++++++------------ pyproject.toml | 2 ++ src/agent/sentinel.py | 28 +++++++++++++++++++--------- src/graph/neo4j_client.py | 2 +- tests/test_api.py | 13 ++++++++++++- 5 files changed, 48 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 69b640a..d5adb00 100644 --- a/README.md +++ b/README.md @@ -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 ``` --- @@ -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 @@ -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 @@ -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 | --- @@ -196,4 +199,3 @@ poetry run pytest tests/ -v --cov=src --cov-report=term-missing ## 📄 License MIT — see [LICENSE](LICENSE). -# Community Update diff --git a/pyproject.toml b/pyproject.toml index 8acacdf..03b960d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/src/agent/sentinel.py b/src/agent/sentinel.py index 1023516..a5ec3b8 100644 --- a/src/agent/sentinel.py +++ b/src/agent/sentinel.py @@ -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 @@ -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) @@ -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]]: diff --git a/src/graph/neo4j_client.py b/src/graph/neo4j_client.py index b12858e..1b5d113 100644 --- a/src/graph/neo4j_client.py +++ b/src/graph/neo4j_client.py @@ -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)) diff --git a/tests/test_api.py b/tests/test_api.py index 003805d..0e0c5dd 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -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 @@ -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