File size: 10,433 Bytes
a671976
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
---
title: RICS Report Genius
emoji: 🏠
colorFrom: blue
colorTo: indigo
sdk: docker
app_port: 7860
pinned: false
license: mit
short_description: RICS v2 report generator from past reports + notes.
---
# Report Genius AI

**Active baseline:** `e561e67` + **standard RAG/LLM generate** (see [CODEBASE_BASELINE.md](CODEBASE_BASELINE.md)).
Confirm with `GET /health``codebase_baseline` and `primary_generate_pipeline: "standard"`.

A production-quality **Retrieval-Augmented Generation (RAG)** system for producing
RICS-style property survey reports. Each user's uploaded documents are indexed in a
single pooled vector store with strict `tenant_id` isolation — your data never leaks
into another user's results.

---

## Architecture

```
Upload (.docx/.pdf)


  Parser → Normaliser → Chunker


  Embeddings (OpenAI text-embedding-3-small)


  Vector Store (FAISS, on-disk) ← metadata: tenant_id, doc_id, chunk_id

       ▼ (query + tenant_id filter)
  Retriever (top-10) → Reranker (top-3) → ≤400 token context


  LLM Adapter (gpt-4o-mini, edit/adapt prompt)


  Post-processor ([VERIFY] enforcement)


  Section Cache (SHA-256 keyed) → Response JSON
```

---

## Quick start

### 1. Prerequisites

- Python 3.11+
- Docker & Docker Compose (for full stack)
- An OpenAI API key

### 2. Clone and configure

```bash
git clone https://github.com/My-Report-AI/Report-genius-ai.git
cd Report-genius-ai
cp .env.example .env
# Edit .env and set OPENAI_API_KEY=sk-...
```

### 3a. Run locally (without Docker)

```bash
python -m venv .venv
# Windows:
.venv\Scripts\activate
# macOS/Linux:
source .venv/bin/activate

pip install -e ".[dev]"

# FAISS runs inside the API process (index under ~/.report_genius/faiss_index by default).
uvicorn app.main:app --reload --port 8000
```

### 3b. Run with Docker Compose

```bash
docker-compose up --build
```

API available at `http://localhost:8000`.  
Interactive docs at `http://localhost:8000/docs`.

---

## API endpoints

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/upload` | Upload a `.docx` or `.pdf`; triggers ingestion |
| `POST` | `/reports/{report_id}/generate` | Run retrieval + adapt pipeline |
| `GET` | `/reports/{report_id}/sections` | Fetch generated sections |
| `GET` | `/reports/{report_id}/status` | Poll ingestion / generation status |
| `POST` | `/test/ingest` | **Dev only** — ingest a file from `/mnt/data/samples/` |

### Example: upload a file

```bash
curl -X POST http://localhost:8000/upload \
  -H "X-Tenant-ID: tenant_abc" \
  -F "file=@/path/to/survey.docx" \
  -F "tenant_id=tenant_abc"
```

### Example: generate a section

```bash
curl -X POST http://localhost:8000/reports/{report_id}/generate \
  -H "X-Tenant-ID: tenant_abc" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "B1",
    "bullets": [
      "Property is a semi-detached house built circa 1965",
      "Floor area approx 95 sqm",
      "Located in NW3 postcode"
    ]
  }'
```

### Similar content, deduplication, and refreshing the knowledge library

When surveyors add or revise inspection notes, the app can **surface similar material** already in the tenant’s workspace:

1. **Indexed uploads** (`.docx` / `.pdf` ingested into the FAISS index) — semantic (embedding) similarity.
2. **Other RICS sections’ draft notes** (sent from the browser) — lexical overlap (token Jaccard) to catch duplicate lines across sections.

**API**

```bash
curl -X POST http://localhost:8000/content/similar \
  -H "X-Tenant-ID: tenant_abc" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "DPC minimum 150mm above ground per current guidance",
    "section_code": "E4",
    "peer_sections": { "I1": "…other section bullets…" },
    "exclude_document_ids": [],
    "limit": 8,
    "min_relevance_percent": 28
  }'
```

- **`exclude_document_ids`**: omit UUIDs of uploads you do not want in the match list (the UI passes the **current survey file** so hits focus on separate reference/guidance documents).
- **`DELETE /documents/{document_id}`**: removes that file from disk, the database, and the vector index. **HTTP 409** if a report still references the file — start a new report or keep the file for traceability.

**UI**: On the configure step, each section’s **Inspection Notes** toolbar has **Check similar**. The modal supports keeping notes, merging library or peer wording, replacing notes, or **removing an outdated upload** from the library.

This is **RAG corpus hygiene**, not LLM fine-tuning: updating or removing indexed documents changes what retrieval sees on the next generation.

---

## Environment variables

| Variable | Default | Description |
|----------|---------|-------------|
| `OPENAI_API_KEY` | *(required)* | OpenAI secret key |
| `FAISS_INDEX_PATH` | `~/.report_genius/faiss_index` | Directory for the persisted FAISS index |
| `DATABASE_URL` | `sqlite+aiosqlite:///./dev.db` | SQLAlchemy async DB URL |
| `UPLOAD_DIR` | `/mnt/data/uploads` | Where uploaded files are stored |
| `CACHE_DIR` | `/tmp/section_cache` | Section output cache directory |
| `MAX_CONTEXT_TOKENS` | `400` | Max tokens fed to LLM as context |
| `MAX_OUTPUT_TOKENS` | `300` | Max tokens in LLM response |
| `RETRIEVAL_TOP_K` | `10` | Vector search top-k |
| `RERANK_TOP_N` | `3` | Snippets kept after reranking |
| `CHUNK_SIZE` | `500` | Target chunk size in tokens |
| `CHUNK_OVERLAP` | `75` | Overlap between consecutive chunks |
| `DEV_MODE` | `false` | Enables SQL echo and dev endpoint |
| `TENANT_SECRET_KEY` | *(required in prod)* | Used to sign tenant tokens |

---

## Running tests

```bash
# All tests with coverage
pytest

# Specific acceptance tests
pytest app/tests/test_isolation.py -v   # tenant isolation
pytest app/tests/test_generator.py -v  # non-invention + token limit
pytest app/tests/test_cache.py -v      # cache prevents duplicate LLM calls
pytest app/tests/test_content_similar_api.py -v  # similar-content API
```

Tests run in < 2 minutes (all LLM and embedding calls are mocked).

---

## Linting & type-checks

```bash
black --check .
ruff check .
mypy app/
```

---

## Incremental milestones

| Milestone | Description |
|-----------|-------------|
| M1 | Upload + DOCX/PDF parser → chunks saved to disk |
| M2 | Chunker + FAISS index + mock embeddings |
| M3 | Retriever + reranker + generator (mock OpenAI) |
| M4 | Real OpenAI calls + section cache |
| M5 | CI, security middleware, acceptance tests |

---

## Non-invention guarantee

The LLM is always prompted with:

> "Do not invent facts. Use only facts present in the bullets or retrieved_snippets.
>  If a fact is missing or unverifiable, mark it **[VERIFY]**."

A post-processor additionally scans the output for any numeric fact or named entity
not present in the source bullets or retrieved snippets, and automatically wraps it
with `[VERIFY]`. These items are flagged for human review before the report is
finalised.

### Inline section editing

The results grid renders each generated section in a `.result-text`
`contenteditable="plaintext-only"` block. Edits are persisted server-side via
`PATCH /reports/{report_id}/sections/{section_code}` (body
`{ "text": "..." }`, max 200 KB UTF-8, empty string allowed). Saves are
debounced **700 ms** after the last keystroke, fire immediately on **blur**,
and on **Cmd/Ctrl+S** (the browser's save-page dialog is suppressed). An
in-flight request is aborted if the user resumes typing before it completes.

The card header carries a small status pill that walks through:
`✎ Editable → ⏳ Saving… → ✓ Saved` (or `⚠ Save failed` on a non-2xx response).

When a save succeeds the backend stamps `meta.user_edited = true` inside the
section's persisted `provenance` JSON; existing `provenance.sources`, `mode`,
`confidence`, `ai_level`, `ai_percent`, `ai_transparency`, `style_profile`,
`pipeline`, `fallback_used`, and `inspector` metadata are preserved. The
client also updates `state.results[code].text` in place, so a subsequent
**Re-Generate** of the same section sends the edited body as
`draft_paragraph` rather than the original LLM output.

---

## Personalisation guarantees (per user / tenant)

This system personalises output **per tenant** and **per report request**.

### 1) User isolation (who the model learns from)

- Retrieval is always filtered by `tenant_id`.
- Writing-style analysis samples only that tenant's indexed content.
- Style profile cache is keyed by tenant, so one user's style never leaks to another.

### 2) Style personalisation (how it writes)

For generation, the pipeline detects and applies a `WritingStyleProfile`:

- tone
- formality level
- sentence complexity
- vocabulary level
- common phrases
- structural patterns

These fields are injected into prompts so sections match the user's own writing voice.

### 3) Preferences and choices (how much AI is allowed)

The frontend AI scale (`ai_level` 1..5) controls behaviour:

- **1 (RAG only)**: minimal rewrite, no notes-expansion, no style-profile injection.
- **2..4 (light to strong)**: increasing rewrite strength and style matching.
- **5 (full AI)**: strongest style adaptation and paraphrasing.

`ai_level` is applied in all three modes:

- `generate`: controls temperature + creativity hint + expansion behaviour.
- `proofread`: controls edit intensity (conservative at low levels, stronger at high).
- `enhance`: controls technical-expansion intensity and prose freedom.

### 4) Priorities (what the model should preserve first)

Prompt and post-processing priorities are fixed in this order:

1. Preserve user facts (especially numbers/measurements/dates/addresses).
2. Use retrieved evidence from the same tenant.
3. Match user style/profile.
4. Mark uncertain/unsupported claims with `[VERIFY]`.

This means user-provided facts and evidence are prioritised above stylistic flourish.

### 5) Current scope

Current personalisation is driven by:

- uploaded reference documents (style + evidence),
- section bullets / extracted notes (user intent),
- selected mode (`generate`, `proofread`, `enhance`),
- selected AI scale (`ai_level`).

There is currently no separate long-term "user preference profile" store beyond
tenant style analysis and request-level controls above.

---

## Licence

MIT — see `LICENSE`.