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
15 changes: 14 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,21 @@ FRAUDLENS_SEED_PASSWORD=
# docker-compose.yml, and the same password here.
# FRAUDLENS_REDIS_URL=redis://:<password>@127.0.0.1:6380/0

# Case notes written by Claude instead of the template (needs ANTHROPIC_API_KEY).
# Case notes written by a language model instead of the template. Providers with a
# key are tried in this order; the first gets several attempts per note, the others
# one each (docs/DECISION_POLICY.md §7). With no key below, ANTHROPIC_API_KEY (Claude).
# FRAUDLENS_LLM_NOTES=true
# FRAUDLENS_LLM_PROVIDERS=["nova","gemini","groq","openrouter"]
# FRAUDLENS_NOVA_API_KEY=
# FRAUDLENS_GEMINI_API_KEY=
# FRAUDLENS_GROQ_API_KEY=
# FRAUDLENS_OPENROUTER_API_KEY=
# FRAUDLENS_LLM_PRIMARY_ATTEMPTS=3
# FRAUDLENS_LLM_ATTEMPT_TIMEOUT_SECONDS=25
# FRAUDLENS_LLM_DEADLINE_SECONDS=120
# Models (defaults shown): FRAUDLENS_NOVA_MODEL=nova_g, FRAUDLENS_GEMINI_MODEL=gemini-3.8-flash,
# FRAUDLENS_GROQ_MODEL=openai/gpt-oss-120b,
# FRAUDLENS_OPENROUTER_MODEL=nvidia/nemotron-3-super-120b-a12b:free

# A challenger that scores every transaction next to the served model and decides
# nothing (`make retrain` registers one). Compare them on the console's model page.
Expand Down
10 changes: 7 additions & 3 deletions backend/src/fraudlens/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,14 @@ def build_platform(settings: Settings) -> Platform:
redis = Redis.from_url(settings.redis_url, decode_responses=True)
scorer = Scorer(settings, sessions, redis)
narrator = None
if settings.llm_notes and os.environ.get("ANTHROPIC_API_KEY"):
from ..decision.narrative import LLMNarrator
if settings.llm_notes:
from ..decision.llm import build_chain

narrator = LLMNarrator()
narrator = build_chain(settings)
if narrator is None and os.environ.get("ANTHROPIC_API_KEY"):
from ..decision.narrative import LLMNarrator

narrator = LLMNarrator()
return Platform(
settings=settings,
db=db,
Expand Down
2 changes: 2 additions & 0 deletions backend/src/fraudlens/api/routes/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ def narrative(
if p.narrator is None:
return narrate(evidence, None, lang)
key = f"fraudlens:note:{txn_id}:{lang}"
if tag := getattr(p.narrator, "cache_tag", None):
key += f":{tag}"
try:
if cached := p.redis.get(key):
return json.loads(cached)
Expand Down
26 changes: 24 additions & 2 deletions backend/src/fraudlens/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pathlib import Path
from typing import Literal

from pydantic import SecretStr
from pydantic import Field, SecretStr
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict

from .secret_sources import SecretFilesSource
Expand Down Expand Up @@ -73,8 +73,30 @@ class Settings(BaseSettings):
# only you can reach; ignored in production, where the API also refuses to start.
demo_login: bool = False

# Case notes from the language model. Off unless enabled and ANTHROPIC_API_KEY is set.
# Case notes from language models (decision/llm). Off unless enabled. Providers with
# a key are asked in `llm_providers` order; the first gets `llm_primary_attempts`
# tries per note, each other one try. With no provider key set, ANTHROPIC_API_KEY
# (Claude) is used as before. Nothing here is on the scoring path.
llm_notes: bool = False
llm_providers: list[Literal["nova", "gemini", "groq", "openrouter"]] = [
"nova",
"gemini",
"groq",
"openrouter",
]
llm_primary_attempts: int = Field(3, ge=1, le=5)
llm_attempt_timeout_seconds: float = Field(25.0, gt=0, le=120)
llm_deadline_seconds: float = Field(120.0, gt=0, le=600) # for one note, all providers
nova_api_key: SecretStr | None = None
nova_url: str = "https://nova.zohirrayhan.me/api/chat"
nova_model: Literal["nova_g", "nova_c"] = "nova_g"
gemini_api_key: SecretStr | None = None
gemini_model: str = "gemini-3.8-flash"
gemini_thinking_level: str | None = "low" # unset for models without thinking levels
groq_api_key: SecretStr | None = None
groq_model: str = "openai/gpt-oss-120b"
openrouter_api_key: SecretStr | None = None
openrouter_model: str = "nvidia/nemotron-3-super-120b-a12b:free"

# Where the console is served from in development (`make console`).
cors_origins: list[str] = ["http://localhost:3100", "http://127.0.0.1:3100"]
Expand Down
3 changes: 2 additions & 1 deletion backend/src/fraudlens/decision/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"""Decision layer: policy, tiers, reasons and case notes on top of the model scores."""

from .engine import Decision, DecisionEngine, context_from_engine, display_score
from .narrative import LLMNarrator, check_grounding, mask_id, narrate, template
from .narrative import Draft, LLMNarrator, check_grounding, mask_id, narrate, template
from .policy import TIERS, Policy, PolicyError, apply_policy, load_policy
from .similar import SimilarCases

__all__ = [
"TIERS",
"Decision",
"DecisionEngine",
"Draft",
"LLMNarrator",
"Policy",
"PolicyError",
Expand Down
39 changes: 39 additions & 0 deletions backend/src/fraudlens/decision/llm/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Case notes from a chain of language models, with a bilingual knowledge base."""

from .chain import (
LABELS,
PROVIDERS,
ProviderChain,
Step,
build_chain,
build_prompt,
check_facts,
check_note,
)
from .knowledge import Knowledge, KnowledgeError, load_knowledge
from .providers import (
ChatCompletionsProvider,
GeminiProvider,
NovaProvider,
Prompt,
ProviderError,
)

__all__ = [
"LABELS",
"PROVIDERS",
"ChatCompletionsProvider",
"GeminiProvider",
"Knowledge",
"KnowledgeError",
"NovaProvider",
"Prompt",
"ProviderChain",
"ProviderError",
"Step",
"build_chain",
"build_prompt",
"check_facts",
"check_note",
"load_knowledge",
]
Loading
Loading