ai / ARCHITECTURE.md
Aissugur Kurganbekov
Claude Opus 4.8
Add two-stage retrieval: TF-IDF -> optional FlashRank reranking
ba9f2db
|
Raw History Blame Contribute Delete
5.18 kB

Architecture β€” FinanceFlow AI

System overview

A two-tier app: a React/Vite single-page frontend talks to a FastAPI backend over a small REST/JSON API. The backend parses documents into chunks, ranks them with a TF-IDF retriever, builds a grounded answer with citations, and persists everything in SQLite. No external services are required to run it.

flowchart LR
    U[Browser - React/Vite] -->|/api JSON| API[FastAPI backend]
    API --> ING[Ingestion: parse -> chunk]
    API --> RET[Retrieval: TF-IDF rank]
    API --> ANS[Answering: abstain + extract + cite]
    ANS -. optional .-> LLM[(OpenAI / Anthropic)]
    ING --> DB[(SQLite)]
    RET --> DB
    API --> DB

In Docker, the frontend is served by nginx which proxies /api to the backend container; in local dev, Vite proxies /api to http://127.0.0.1:8000.

Backend modules (backend/app/)

Module Responsibility
main.py FastAPI app, all endpoints, CORS, logging setup, global exception handler
config.py Env-driven settings with safe defaults (answer mode, thresholds, limits)
database.py SQLAlchemy engine/session + SQLite PRAGMA foreign_keys=ON
models.py ORM models: Document, Chunk, QA
schemas.py Pydantic request/response models (typed I/O, Swagger)
parsing.py Text extraction: TXT (whole file) and PDF (per page, via pypdf)
chunking.py Overlapping word-window chunking
retrieval.py Tokenizer + TF-IDF ranker (rank_chunks) β€” offline first stage
reranking.py Optional cross-encoder reranking (FlashRank) of the TF-IDF candidates; graceful fallback to TF-IDF order when unavailable
answering.py Abstention gates, focused extractive answer, citation builder
llm.py Optional OpenAI/Anthropic synthesis with graceful fallback
ingest.py Shared parse β†’ chunk β†’ persist pipeline
evaluation.py Golden-dataset runner + metrics
eval_cli.py python -m app.eval_cli
reset_db.py python -m app.reset_db (drop + recreate)
logging_config.py One-line logging setup

Frontend modules (frontend/src/)

Area Files
Shell App.jsx (sidebar + view routing + demo orchestration)
Navigation components/Sidebar.jsx
Views components/views/Overview.jsx, Ask.jsx, Evaluation.jsx
Feature components DocumentCard.jsx, EvidencePanel.jsx, MetricCard.jsx, Upload.jsx
UI primitives components/ui/Button.jsx, Card.jsx, Badge.jsx, Field.jsx, States.jsx
Misc lib/icons.jsx, api.js, styles.css

Database schema

erDiagram
    DOCUMENTS ||--o{ CHUNKS : has
    DOCUMENTS ||--o{ QA_HISTORY : has

    DOCUMENTS {
        int id PK
        string filename
        string file_type
        string status
        text error
        int num_chunks
        datetime created_at
    }
    CHUNKS {
        int id PK
        int document_id FK
        int chunk_index
        int page
        text text
    }
    QA_HISTORY {
        int id PK
        int document_id FK
        text question
        text answer
        int abstained
        text citations_json
        datetime created_at
    }

Deleting a Document cascades to its Chunk and QA rows (ORM cascade + SQLite foreign-key pragma).

Document processing flow

flowchart TD
    A[POST /documents/upload] --> B{Validate type + size}
    B -- invalid --> E[400 / 413 error]
    B -- ok --> C[Write to temp file]
    C --> D[parse: extract pages]
    D --> F[chunk: overlapping word windows]
    F --> G{Any chunks?}
    G -- no --> H[status = failed, error stored]
    G -- yes --> I[Store chunks, status = processed]
    C --> J[Delete temp file in finally]

The raw upload is parsed in a temp file and then discarded β€” only extracted chunks are persisted.

Retrieval flow

flowchart LR
    Q[Question] --> T[Tokenize - drop stopwords]
    C[Document chunks] --> IDF[Build IDF over chunks]
    T --> S[Score chunk = sum tf*idf]
    IDF --> S
    S --> N[Normalize by sqrt chunk length]
    N --> K[Take top-K above threshold]

Ask β†’ answer β†’ citation flow

flowchart TD
    A[POST /ask] --> V{Document processed?}
    V -- no --> ERR[400/404]
    V -- yes --> R[rank_chunks top-K]
    R --> G1{score above threshold?}
    G1 -- no --> AB[Abstain message, no citations]
    G1 -- yes --> G2{at least 2 query terms matched?}
    G2 -- no --> AB
    G2 -- yes --> CIT[Build focused citations]
    CIT --> M{LLM mode + key?}
    M -- yes --> LLM[Synthesize from context, strict prompt]
    M -- no --> EX[Focused extractive span]
    LLM --> SAVE[Persist QA + return]
    EX --> SAVE
    AB --> SAVE

Evaluation flow

flowchart LR
    GD[golden_dataset.json] --> SEED[Ensure sample docs ingested]
    SEED --> LOOP[For each question: answer_question]
    LOOP --> CLS{Expectation vs outcome}
    CLS --> MET[Metrics: coverage, abstentions, unsupported, evidence-match]
    MET --> OUT[UI cards + CLI report]

unsupported_answer_count > 0 makes the CLI exit non-zero, so CI can gate on it.