Spaces:
Sleeping
Sleeping
Aissugur Kurganbekov
Claude Opus 4.8
Add two-stage retrieval: TF-IDF -> optional FlashRank reranking
ba9f2db |
Download ARCHITECTURE.md from financeflow/ai: direct link, hf CLI and curl.
- Browser
- Download file 5.18 kB
-
https://huggingface.co/spaces/financeflow/ai/resolve/main/ARCHITECTURE.md
- Command line
-
hf download hf://spaces/financeflow/ai/ARCHITECTURE.md
-
curl -L -o ARCHITECTURE.md https://huggingface.co/spaces/financeflow/ai/resolve/main/ARCHITECTURE.md
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. | |
| ```mermaid | |
| 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 | |
| ```mermaid | |
| 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 | |
| ```mermaid | |
| 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 | |
| ```mermaid | |
| 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 | |
| ```mermaid | |
| 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 | |
| ```mermaid | |
| 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. | |