docsmind 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {docsmind-0.2.0 → docsmind-0.3.0}/CHANGELOG.md +29 -3
- {docsmind-0.2.0 → docsmind-0.3.0}/PKG-INFO +58 -11
- {docsmind-0.2.0 → docsmind-0.3.0}/README.md +54 -10
- {docsmind-0.2.0 → docsmind-0.3.0}/pyproject.toml +13 -3
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/adapters/storage/postgres/repository.py +125 -1
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/adapters/storage/postgres/tables.py +17 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/cli/app.py +3 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/cli/ingest.py +6 -1
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/cli/inspect.py +37 -0
- docsmind-0.3.0/src/docsmind/cli/serve.py +29 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/domain/documents.py +36 -7
- docsmind-0.3.0/src/docsmind/domain/errors.py +33 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/ingestion/chunking/page.py +53 -0
- docsmind-0.3.0/src/docsmind/ingestion/parsers/pdf.py +590 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/ingestion/pipeline.py +31 -1
- docsmind-0.3.0/src/docsmind/server/app.py +88 -0
- docsmind-0.3.0/src/docsmind/server/routes/documents.py +196 -0
- docsmind-0.3.0/src/docsmind/server/ui/assets/index-DSQOFo_H.css +1 -0
- docsmind-0.3.0/src/docsmind/server/ui/assets/index-QBP12Bcv.js +5385 -0
- docsmind-0.3.0/src/docsmind/server/ui/favicon.svg +1 -0
- docsmind-0.3.0/src/docsmind/server/ui/icons.svg +24 -0
- docsmind-0.3.0/src/docsmind/server/ui/index.html +14 -0
- docsmind-0.2.0/.env.example +0 -29
- docsmind-0.2.0/.github/workflows/publish.yml +0 -49
- docsmind-0.2.0/.github/workflows/test.yml +0 -59
- docsmind-0.2.0/.python-version +0 -1
- docsmind-0.2.0/alembic/env.py +0 -83
- docsmind-0.2.0/alembic/script.py.mako +0 -26
- docsmind-0.2.0/alembic/versions/0001_initial_schema.py +0 -173
- docsmind-0.2.0/alembic/versions/0002_full_text_and_stats.py +0 -36
- docsmind-0.2.0/alembic.ini +0 -43
- docsmind-0.2.0/docker-compose.yml +0 -20
- docsmind-0.2.0/examples/NIST.SP.800-171r2.pdf +0 -0
- docsmind-0.2.0/examples/NIST.SP.800-171r3.pdf +0 -0
- docsmind-0.2.0/examples/parse_pdf_demo.py +0 -77
- docsmind-0.2.0/examples/sample.pdf +0 -0
- docsmind-0.2.0/scripts/make_test_pdf.py +0 -118
- docsmind-0.2.0/src/docsmind/ingestion/parsers/pdf.py +0 -255
- docsmind-0.2.0/tests/unit/test_pdf_parser.py +0 -292
- docsmind-0.2.0/uv.lock +0 -5722
- {docsmind-0.2.0 → docsmind-0.3.0}/.gitignore +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/adapters/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/adapters/blobs/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/adapters/blobs/filesystem.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/adapters/embeddings/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/adapters/llm/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/adapters/storage/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/adapters/storage/postgres/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/adapters/storage/postgres/engine.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/adapters/tracing/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/adapters/vision/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/agent/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/agent/subagents/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/agent/subagents/change_analysis/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/agent/tools/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/cli/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/cli/chat.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/cli/db.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/cli/diff.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/cli/doctor.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/client.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/config.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/domain/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/domain/changes.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/domain/citations.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/domain/ports.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/domain/tables.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/ingestion/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/ingestion/chunking/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/ingestion/chunking/structural.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/ingestion/enrichment/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/ingestion/parsers/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/py.typed +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/retrieval/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/server/__init__.py +0 -0
- {docsmind-0.2.0 → docsmind-0.3.0}/src/docsmind/server/routes/__init__.py +0 -0
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2026-10-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Bundled frontend UI in the Python package: release UI assets are copied into
|
|
10
|
+
`src/docsmind/server/ui` and served by `docsmind serve` at `/`.
|
|
11
|
+
- `scripts/bundle_frontend.py` helper to copy `../frontend/dist` into the package
|
|
12
|
+
before publishing.
|
|
13
|
+
- `docsmind delete <version-id> [-y]` command to remove one ingested version and
|
|
14
|
+
its derived sections/chunks/images/tables.
|
|
15
|
+
- HTTP `DELETE /api/documents/{version_id}` endpoint.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `docsmind serve` now serves the bundled SPA (when present), in addition to
|
|
20
|
+
`/api/*` and `/blobs/*`.
|
|
21
|
+
- Ingestion now checks `content_hash` before parsing and raises a friendly
|
|
22
|
+
"already ingested" error instead of surfacing a raw Postgres unique-constraint
|
|
23
|
+
failure.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Image extraction de-duplicates repeated embedded images (e.g. page logo reused
|
|
28
|
+
across many pages) using perceptual hash, preventing inflated image counts.
|
|
29
|
+
- API duplicate-ingest responses now return HTTP 409 with structured details.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
5
33
|
## [0.2.0] - 2026-09-27
|
|
6
34
|
|
|
7
35
|
First functional release: documents can now be ingested from PDF into
|
|
@@ -70,9 +98,7 @@ implemented" below).
|
|
|
70
98
|
- Embeddings and hybrid vector/keyword search. Chunks are stored without
|
|
71
99
|
embeddings, and `hybrid_search` raises `NotImplementedError`.
|
|
72
100
|
- Image captioning with a vision model.
|
|
73
|
-
- The chat agent behind `docsmind chat
|
|
74
|
-
behind `docsmind diff`. Both commands exist but have no implementation
|
|
75
|
-
behind them yet.
|
|
101
|
+
- The chat agent behind `docsmind chat`.
|
|
76
102
|
- DOCX and plain-text parsers, S3/Supabase blob storage, and the server/UI.
|
|
77
103
|
- Multi-column reading order: PDFs with genuine multi-column layouts may have
|
|
78
104
|
their text interleaved in the saved full text.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: docsmind
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Chat with your documents. Version-aware retrieval, change analysis, and an editable table workspace, backed by Postgres/pgvector.
|
|
5
5
|
Project-URL: Homepage, https://github.com/yauheniya-ai/docsmind
|
|
6
6
|
Project-URL: Repository, https://github.com/yauheniya-ai/docsmind
|
|
@@ -29,11 +29,13 @@ Requires-Dist: pydantic>=2.6
|
|
|
29
29
|
Requires-Dist: pymupdf>=1.24
|
|
30
30
|
Requires-Dist: rich>=13.7
|
|
31
31
|
Requires-Dist: sqlalchemy[asyncio]>=2.0
|
|
32
|
+
Requires-Dist: tiktoken>=0.7
|
|
32
33
|
Requires-Dist: typer>=0.12
|
|
33
34
|
Provides-Extra: all
|
|
34
35
|
Requires-Dist: anthropic>=0.34; extra == 'all'
|
|
35
36
|
Requires-Dist: fastapi>=0.111; extra == 'all'
|
|
36
37
|
Requires-Dist: openai>=1.30; extra == 'all'
|
|
38
|
+
Requires-Dist: python-multipart>=0.0.9; extra == 'all'
|
|
37
39
|
Requires-Dist: uvicorn[standard]>=0.30; extra == 'all'
|
|
38
40
|
Provides-Extra: anthropic
|
|
39
41
|
Requires-Dist: anthropic>=0.34; extra == 'anthropic'
|
|
@@ -49,6 +51,7 @@ Provides-Extra: openai
|
|
|
49
51
|
Requires-Dist: openai>=1.30; extra == 'openai'
|
|
50
52
|
Provides-Extra: server
|
|
51
53
|
Requires-Dist: fastapi>=0.111; extra == 'server'
|
|
54
|
+
Requires-Dist: python-multipart>=0.0.9; extra == 'server'
|
|
52
55
|
Requires-Dist: uvicorn[standard]>=0.30; extra == 'server'
|
|
53
56
|
Description-Content-Type: text/markdown
|
|
54
57
|
|
|
@@ -58,8 +61,6 @@ Description-Content-Type: text/markdown
|
|
|
58
61
|
[](https://pypi.org/project/docsmind/)
|
|
59
62
|
[](https://pepy.tech/project/docsmind)
|
|
60
63
|
[](https://pepy.tech/project/docsmind)
|
|
61
|
-
[](https://github.com/yauheniya-ai/docsmind/actions/workflows/test.yml)
|
|
62
|
-
[](https://github.com/yauheniya-ai/docsmind/actions/workflows/test.yml)
|
|
63
64
|
[](https://github.com/yauheniya-ai/docsmind/blob/main/LICENSE)
|
|
64
65
|
[](https://github.com/astral-sh/ruff)
|
|
65
66
|
[](#status)
|
|
@@ -82,11 +83,13 @@ its compliance implications, with citations back to exact pages.
|
|
|
82
83
|
| ✅ Postgres + pgvector storage | Local Docker, or any Postgres with pgvector (Neon, Supabase) |
|
|
83
84
|
| ✅ Inspection CLI | `versions`, `fulltext`, `outline`, `chunks` read back exactly what was saved |
|
|
84
85
|
| ✅ `docsmind doctor` | Checks database, blob storage and API keys, and tells you what to fix |
|
|
85
|
-
|
|
|
86
|
-
|
|
|
86
|
+
| ✅ Table extraction | Extract and store tables skipping repeating ones (e.g. headers or footers) |
|
|
87
|
+
| ✅ server + UI | FastAPI backend (`docsmind serve`) + React UI (`frontend/`): add documents, browse indexed content, image and table galleries |
|
|
87
88
|
| 🚧 Chat agent (the Mind) | `docsmind chat` exists as a command, without an agent behind it yet |
|
|
89
|
+
| 🚧 Image captioning | Images are extracted and stored; vision captioning is next |
|
|
90
|
+
| 🚧 Embeddings + hybrid search | Chunks are stored without embeddings for now |
|
|
88
91
|
| 🚧 Change analysis (diffra) | `docsmind diff` exists as a command, without an implementation yet |
|
|
89
|
-
| 🚧 DOCX/text parsers, S3/Supabase blob storage
|
|
92
|
+
| 🚧 DOCX/text parsers, S3/Supabase blob storage | Planned |
|
|
90
93
|
|
|
91
94
|
## Install
|
|
92
95
|
|
|
@@ -96,7 +99,7 @@ pip install docsmind
|
|
|
96
99
|
pip install "docsmind[all]"
|
|
97
100
|
```
|
|
98
101
|
|
|
99
|
-
Extras: `docsmind[openai]`, `docsmind[anthropic]`, `docsmind[server]` (FastAPI,
|
|
102
|
+
Extras: `docsmind[openai]`, `docsmind[anthropic]`, `docsmind[server]` (FastAPI, serves the bundled UI),
|
|
100
103
|
`docsmind[mlflow]` (tracing/eval export). Requires Python 3.10+.
|
|
101
104
|
|
|
102
105
|
## Quickstart
|
|
@@ -158,7 +161,33 @@ docsmind chunks <version-id> --page 12 # the chunk(s) saved for one page
|
|
|
158
161
|
|
|
159
162
|
You can also inspect the tables directly with `docsmind db psql`.
|
|
160
163
|
|
|
161
|
-
### 4.
|
|
164
|
+
### 4. Browse it in the UI
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
pip install "docsmind[server]"
|
|
168
|
+
docsmind serve # FastAPI backend on http://127.0.0.1:8000
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Open `http://127.0.0.1:8000`.
|
|
172
|
+
|
|
173
|
+
The React UI is bundled into the Python package and served directly by `docsmind serve`
|
|
174
|
+
from `docsmind/src/docsmind/server/ui`.
|
|
175
|
+
|
|
176
|
+
Clicking **+ Add Document** in the UI runs the same `docsmind ingest` pipeline on the
|
|
177
|
+
uploaded PDF. Selecting a document shows its saved full text/outline (**Content**), and
|
|
178
|
+
galleries of everything extracted from it (**Images**, **Tables**).
|
|
179
|
+
|
|
180
|
+
For contributors refreshing bundled UI assets before a release:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
cd ../frontend
|
|
184
|
+
npm install
|
|
185
|
+
npm run build
|
|
186
|
+
cd ../docsmind
|
|
187
|
+
python scripts/bundle_frontend.py
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 5. Coming next
|
|
162
191
|
|
|
163
192
|
```python
|
|
164
193
|
print(dm.chat("What are our data retention obligations?")) # planned
|
|
@@ -180,6 +209,9 @@ heuristics that work on one PDF break on the next.
|
|
|
180
209
|
reconstruct structure.
|
|
181
210
|
- **Images** are extracted with their page and position, skipping tiny icons, and saved
|
|
182
211
|
under a readable path: `images/<collection>/<title>/<version>-<id>/`.
|
|
212
|
+
Visually repeated images (e.g. logo on every page) are de-duplicated by perceptual hash.
|
|
213
|
+
- **Tables** are detected with PyMuPDF and rendered as PNG snippets, saved under
|
|
214
|
+
`tables/<collection>/<title>/<version>-<id>/`.
|
|
183
215
|
|
|
184
216
|
## CLI reference
|
|
185
217
|
|
|
@@ -190,8 +222,10 @@ heuristics that work on one PDF break on the next.
|
|
|
190
222
|
| `docsmind fulltext <id> [--full]` | Print the saved full text |
|
|
191
223
|
| `docsmind outline <id>` | Print the saved outline |
|
|
192
224
|
| `docsmind chunks <id> [--page N] [--full]` | Print saved chunks |
|
|
225
|
+
| `docsmind delete <id> [-y]` | Delete one version and its derived chunks/images/tables |
|
|
193
226
|
| `docsmind db up` / `upgrade` / `psql` | Start local Postgres, apply migrations, open psql |
|
|
194
227
|
| `docsmind doctor` | Check your setup |
|
|
228
|
+
| `docsmind serve [--host] [--port] [--reload]` | Run the FastAPI backend for the `frontend/` UI |
|
|
195
229
|
| `docsmind chat`, `docsmind diff` | Planned |
|
|
196
230
|
|
|
197
231
|
## Architecture
|
|
@@ -223,11 +257,22 @@ src/docsmind/
|
|
|
223
257
|
├── cli/ # Typer app: ingest, versions, fulltext, outline, chunks, db, doctor
|
|
224
258
|
├── retrieval/ # hybrid vector + keyword search (planned)
|
|
225
259
|
├── agent/ # the Mind + diffra subagent (planned)
|
|
226
|
-
└── server/ # FastAPI +
|
|
260
|
+
└── server/ # FastAPI app + routes backing frontend/ (documents: ingest, read back content/images/tables)
|
|
227
261
|
|
|
228
|
-
alembic/ tests/ examples/ docs/
|
|
262
|
+
alembic/ tests/ examples/ docs/
|
|
229
263
|
```
|
|
230
264
|
|
|
265
|
+
The UI source lives one level up as a sibling project: `frontend/` (React + TypeScript +
|
|
266
|
+
TailwindCSS, see the repo-root `CLAUDE.md`). Release builds are bundled into
|
|
267
|
+
`src/docsmind/server/ui` and served by `server/app.py` at `/`. The UI talks to `server/`
|
|
268
|
+
over `/api/*`, and loads extracted images/tables straight from disk via `/blobs/*`.
|
|
269
|
+
|
|
270
|
+
## Artwork attribution
|
|
271
|
+
|
|
272
|
+
- Workspace 3D graph artwork inspiration: Angela Galliat,
|
|
273
|
+
"Neural nervous system of a brain" (CodePen):
|
|
274
|
+
https://codepen.io/agalliat/pen/vYGXJxQ
|
|
275
|
+
|
|
231
276
|
### Design notes
|
|
232
277
|
|
|
233
278
|
- **Ports and adapters, once.** `domain/ports.py` defines every external dependency as a
|
|
@@ -242,7 +287,9 @@ alembic/ tests/ examples/ docs/ ui/
|
|
|
242
287
|
Outline paths (e.g. `03.13.11`) give version comparison something stable to align on
|
|
243
288
|
where a document has a table of contents.
|
|
244
289
|
- **Tables are shared state, not a UI feature.** `AgentTable` is a domain object that
|
|
245
|
-
both the agent and the user write to (append-only revisions).
|
|
290
|
+
both the agent and the user write to (append-only revisions). This is distinct from
|
|
291
|
+
the `tables` storage table (extracted PDF table *regions*, rendered as images
|
|
292
|
+
alongside `images` — browsable read-only in the UI's **Tables** gallery).
|
|
246
293
|
|
|
247
294
|
## Development
|
|
248
295
|
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
[](https://pypi.org/project/docsmind/)
|
|
5
5
|
[](https://pepy.tech/project/docsmind)
|
|
6
6
|
[](https://pepy.tech/project/docsmind)
|
|
7
|
-
[](https://github.com/yauheniya-ai/docsmind/actions/workflows/test.yml)
|
|
8
|
-
[](https://github.com/yauheniya-ai/docsmind/actions/workflows/test.yml)
|
|
9
7
|
[](https://github.com/yauheniya-ai/docsmind/blob/main/LICENSE)
|
|
10
8
|
[](https://github.com/astral-sh/ruff)
|
|
11
9
|
[](#status)
|
|
@@ -28,11 +26,13 @@ its compliance implications, with citations back to exact pages.
|
|
|
28
26
|
| ✅ Postgres + pgvector storage | Local Docker, or any Postgres with pgvector (Neon, Supabase) |
|
|
29
27
|
| ✅ Inspection CLI | `versions`, `fulltext`, `outline`, `chunks` read back exactly what was saved |
|
|
30
28
|
| ✅ `docsmind doctor` | Checks database, blob storage and API keys, and tells you what to fix |
|
|
31
|
-
|
|
|
32
|
-
|
|
|
29
|
+
| ✅ Table extraction | Extract and store tables skipping repeating ones (e.g. headers or footers) |
|
|
30
|
+
| ✅ server + UI | FastAPI backend (`docsmind serve`) + React UI (`frontend/`): add documents, browse indexed content, image and table galleries |
|
|
33
31
|
| 🚧 Chat agent (the Mind) | `docsmind chat` exists as a command, without an agent behind it yet |
|
|
32
|
+
| 🚧 Image captioning | Images are extracted and stored; vision captioning is next |
|
|
33
|
+
| 🚧 Embeddings + hybrid search | Chunks are stored without embeddings for now |
|
|
34
34
|
| 🚧 Change analysis (diffra) | `docsmind diff` exists as a command, without an implementation yet |
|
|
35
|
-
| 🚧 DOCX/text parsers, S3/Supabase blob storage
|
|
35
|
+
| 🚧 DOCX/text parsers, S3/Supabase blob storage | Planned |
|
|
36
36
|
|
|
37
37
|
## Install
|
|
38
38
|
|
|
@@ -42,7 +42,7 @@ pip install docsmind
|
|
|
42
42
|
pip install "docsmind[all]"
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
Extras: `docsmind[openai]`, `docsmind[anthropic]`, `docsmind[server]` (FastAPI,
|
|
45
|
+
Extras: `docsmind[openai]`, `docsmind[anthropic]`, `docsmind[server]` (FastAPI, serves the bundled UI),
|
|
46
46
|
`docsmind[mlflow]` (tracing/eval export). Requires Python 3.10+.
|
|
47
47
|
|
|
48
48
|
## Quickstart
|
|
@@ -104,7 +104,33 @@ docsmind chunks <version-id> --page 12 # the chunk(s) saved for one page
|
|
|
104
104
|
|
|
105
105
|
You can also inspect the tables directly with `docsmind db psql`.
|
|
106
106
|
|
|
107
|
-
### 4.
|
|
107
|
+
### 4. Browse it in the UI
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install "docsmind[server]"
|
|
111
|
+
docsmind serve # FastAPI backend on http://127.0.0.1:8000
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Open `http://127.0.0.1:8000`.
|
|
115
|
+
|
|
116
|
+
The React UI is bundled into the Python package and served directly by `docsmind serve`
|
|
117
|
+
from `docsmind/src/docsmind/server/ui`.
|
|
118
|
+
|
|
119
|
+
Clicking **+ Add Document** in the UI runs the same `docsmind ingest` pipeline on the
|
|
120
|
+
uploaded PDF. Selecting a document shows its saved full text/outline (**Content**), and
|
|
121
|
+
galleries of everything extracted from it (**Images**, **Tables**).
|
|
122
|
+
|
|
123
|
+
For contributors refreshing bundled UI assets before a release:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
cd ../frontend
|
|
127
|
+
npm install
|
|
128
|
+
npm run build
|
|
129
|
+
cd ../docsmind
|
|
130
|
+
python scripts/bundle_frontend.py
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 5. Coming next
|
|
108
134
|
|
|
109
135
|
```python
|
|
110
136
|
print(dm.chat("What are our data retention obligations?")) # planned
|
|
@@ -126,6 +152,9 @@ heuristics that work on one PDF break on the next.
|
|
|
126
152
|
reconstruct structure.
|
|
127
153
|
- **Images** are extracted with their page and position, skipping tiny icons, and saved
|
|
128
154
|
under a readable path: `images/<collection>/<title>/<version>-<id>/`.
|
|
155
|
+
Visually repeated images (e.g. logo on every page) are de-duplicated by perceptual hash.
|
|
156
|
+
- **Tables** are detected with PyMuPDF and rendered as PNG snippets, saved under
|
|
157
|
+
`tables/<collection>/<title>/<version>-<id>/`.
|
|
129
158
|
|
|
130
159
|
## CLI reference
|
|
131
160
|
|
|
@@ -136,8 +165,10 @@ heuristics that work on one PDF break on the next.
|
|
|
136
165
|
| `docsmind fulltext <id> [--full]` | Print the saved full text |
|
|
137
166
|
| `docsmind outline <id>` | Print the saved outline |
|
|
138
167
|
| `docsmind chunks <id> [--page N] [--full]` | Print saved chunks |
|
|
168
|
+
| `docsmind delete <id> [-y]` | Delete one version and its derived chunks/images/tables |
|
|
139
169
|
| `docsmind db up` / `upgrade` / `psql` | Start local Postgres, apply migrations, open psql |
|
|
140
170
|
| `docsmind doctor` | Check your setup |
|
|
171
|
+
| `docsmind serve [--host] [--port] [--reload]` | Run the FastAPI backend for the `frontend/` UI |
|
|
141
172
|
| `docsmind chat`, `docsmind diff` | Planned |
|
|
142
173
|
|
|
143
174
|
## Architecture
|
|
@@ -169,11 +200,22 @@ src/docsmind/
|
|
|
169
200
|
├── cli/ # Typer app: ingest, versions, fulltext, outline, chunks, db, doctor
|
|
170
201
|
├── retrieval/ # hybrid vector + keyword search (planned)
|
|
171
202
|
├── agent/ # the Mind + diffra subagent (planned)
|
|
172
|
-
└── server/ # FastAPI +
|
|
203
|
+
└── server/ # FastAPI app + routes backing frontend/ (documents: ingest, read back content/images/tables)
|
|
173
204
|
|
|
174
|
-
alembic/ tests/ examples/ docs/
|
|
205
|
+
alembic/ tests/ examples/ docs/
|
|
175
206
|
```
|
|
176
207
|
|
|
208
|
+
The UI source lives one level up as a sibling project: `frontend/` (React + TypeScript +
|
|
209
|
+
TailwindCSS, see the repo-root `CLAUDE.md`). Release builds are bundled into
|
|
210
|
+
`src/docsmind/server/ui` and served by `server/app.py` at `/`. The UI talks to `server/`
|
|
211
|
+
over `/api/*`, and loads extracted images/tables straight from disk via `/blobs/*`.
|
|
212
|
+
|
|
213
|
+
## Artwork attribution
|
|
214
|
+
|
|
215
|
+
- Workspace 3D graph artwork inspiration: Angela Galliat,
|
|
216
|
+
"Neural nervous system of a brain" (CodePen):
|
|
217
|
+
https://codepen.io/agalliat/pen/vYGXJxQ
|
|
218
|
+
|
|
177
219
|
### Design notes
|
|
178
220
|
|
|
179
221
|
- **Ports and adapters, once.** `domain/ports.py` defines every external dependency as a
|
|
@@ -188,7 +230,9 @@ alembic/ tests/ examples/ docs/ ui/
|
|
|
188
230
|
Outline paths (e.g. `03.13.11`) give version comparison something stable to align on
|
|
189
231
|
where a document has a table of contents.
|
|
190
232
|
- **Tables are shared state, not a UI feature.** `AgentTable` is a domain object that
|
|
191
|
-
both the agent and the user write to (append-only revisions).
|
|
233
|
+
both the agent and the user write to (append-only revisions). This is distinct from
|
|
234
|
+
the `tables` storage table (extracted PDF table *regions*, rendered as images
|
|
235
|
+
alongside `images` — browsable read-only in the UI's **Tables** gallery).
|
|
192
236
|
|
|
193
237
|
## Development
|
|
194
238
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "docsmind"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Chat with your documents. Version-aware retrieval, change analysis, and an editable table workspace, backed by Postgres/pgvector."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -38,12 +38,13 @@ dependencies = [
|
|
|
38
38
|
"pillow>=10.0",
|
|
39
39
|
"imagehash>=4.3",
|
|
40
40
|
"rich>=13.7",
|
|
41
|
+
"tiktoken>=0.7",
|
|
41
42
|
]
|
|
42
43
|
|
|
43
44
|
[project.optional-dependencies]
|
|
44
45
|
openai = ["openai>=1.30"]
|
|
45
46
|
anthropic = ["anthropic>=0.34"]
|
|
46
|
-
server = ["fastapi>=0.111", "uvicorn[standard]>=0.30"]
|
|
47
|
+
server = ["fastapi>=0.111", "uvicorn[standard]>=0.30", "python-multipart>=0.0.9"]
|
|
47
48
|
mlflow = ["mlflow>=2.14"]
|
|
48
49
|
dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "testcontainers[postgres]>=4.4", "ruff>=0.5", "mypy>=1.10"]
|
|
49
50
|
all = ["docsmind[openai,anthropic,server]"]
|
|
@@ -61,6 +62,15 @@ Issues = "https://github.com/yauheniya-ai/docsmind/issues"
|
|
|
61
62
|
|
|
62
63
|
[tool.hatch.build.targets.wheel]
|
|
63
64
|
packages = ["src/docsmind"]
|
|
65
|
+
include = ["src/docsmind/server/ui/**"]
|
|
66
|
+
|
|
67
|
+
[tool.hatch.build.targets.sdist]
|
|
68
|
+
include = [
|
|
69
|
+
"src/docsmind/**",
|
|
70
|
+
"README.md",
|
|
71
|
+
"CHANGELOG.md",
|
|
72
|
+
"scripts/bundle_frontend.py",
|
|
73
|
+
]
|
|
64
74
|
|
|
65
75
|
[tool.pytest.ini_options]
|
|
66
76
|
asyncio_mode = "auto"
|
|
@@ -79,4 +89,4 @@ target-version = "py310"
|
|
|
79
89
|
# image is corrupt" — and a single malformed image in an otherwise-fine PDF
|
|
80
90
|
# shouldn't abort the whole ingest, so we deliberately catch broadly here and
|
|
81
91
|
# skip the one image rather than propagate.
|
|
82
|
-
"src/docsmind/ingestion/parsers/pdf.py" = ["BLE001"]
|
|
92
|
+
"src/docsmind/ingestion/parsers/pdf.py" = ["BLE001"]
|
|
@@ -14,7 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
from uuid import UUID
|
|
16
16
|
|
|
17
|
-
from sqlalchemy import insert, select
|
|
17
|
+
from sqlalchemy import delete, insert, select
|
|
18
18
|
|
|
19
19
|
from docsmind.adapters.storage.postgres.engine import get_sessionmaker
|
|
20
20
|
from docsmind.adapters.storage.postgres.tables import (
|
|
@@ -32,6 +32,9 @@ from docsmind.adapters.storage.postgres.tables import (
|
|
|
32
32
|
from docsmind.adapters.storage.postgres.tables import (
|
|
33
33
|
sections as sections_table,
|
|
34
34
|
)
|
|
35
|
+
from docsmind.adapters.storage.postgres.tables import (
|
|
36
|
+
tables as tables_table,
|
|
37
|
+
)
|
|
35
38
|
from docsmind.config import DocsmindConfig
|
|
36
39
|
from docsmind.domain.documents import (
|
|
37
40
|
Chunk,
|
|
@@ -137,6 +140,27 @@ class PostgresRepository:
|
|
|
137
140
|
insert(chunks_table),
|
|
138
141
|
[self._chunk_row(c) for c in parsed.chunks],
|
|
139
142
|
)
|
|
143
|
+
if parsed.tables:
|
|
144
|
+
await session.execute(
|
|
145
|
+
insert(tables_table),
|
|
146
|
+
[
|
|
147
|
+
{
|
|
148
|
+
"id": t.id,
|
|
149
|
+
"version_id": t.version_id,
|
|
150
|
+
"page_number": t.page_number,
|
|
151
|
+
"bbox_x0": t.bbox[0],
|
|
152
|
+
"bbox_y0": t.bbox[1],
|
|
153
|
+
"bbox_x1": t.bbox[2],
|
|
154
|
+
"bbox_y1": t.bbox[3],
|
|
155
|
+
"uri": t.uri,
|
|
156
|
+
"width": t.width,
|
|
157
|
+
"height": t.height,
|
|
158
|
+
"format": t.format,
|
|
159
|
+
"phash": t.phash,
|
|
160
|
+
}
|
|
161
|
+
for t in parsed.tables
|
|
162
|
+
],
|
|
163
|
+
)
|
|
140
164
|
await session.commit()
|
|
141
165
|
|
|
142
166
|
@staticmethod
|
|
@@ -205,6 +229,7 @@ class PostgresRepository:
|
|
|
205
229
|
async with self._sessionmaker() as session:
|
|
206
230
|
stmt = (
|
|
207
231
|
select(
|
|
232
|
+
documents_table.c.id.label("document_id"),
|
|
208
233
|
documents_table.c.title,
|
|
209
234
|
documents_table.c.collection,
|
|
210
235
|
document_versions_table.c.id,
|
|
@@ -223,6 +248,80 @@ class PostgresRepository:
|
|
|
223
248
|
result = await session.execute(stmt)
|
|
224
249
|
return [dict(row) for row in result.mappings().all()]
|
|
225
250
|
|
|
251
|
+
async def get_version(self, version_id: UUID) -> dict | None:
|
|
252
|
+
"""Metadata for one version, joined back to its parent `Document` —
|
|
253
|
+
everything the detail view needs besides full text/outline/chunks."""
|
|
254
|
+
async with self._sessionmaker() as session:
|
|
255
|
+
stmt = (
|
|
256
|
+
select(
|
|
257
|
+
documents_table.c.id.label("document_id"),
|
|
258
|
+
documents_table.c.title,
|
|
259
|
+
documents_table.c.collection,
|
|
260
|
+
document_versions_table.c.id,
|
|
261
|
+
document_versions_table.c.label,
|
|
262
|
+
document_versions_table.c.created_at,
|
|
263
|
+
document_versions_table.c.char_count,
|
|
264
|
+
document_versions_table.c.word_count,
|
|
265
|
+
document_versions_table.c.token_count_estimate,
|
|
266
|
+
document_versions_table.c.blob_uri,
|
|
267
|
+
)
|
|
268
|
+
.select_from(
|
|
269
|
+
document_versions_table.join(
|
|
270
|
+
documents_table, documents_table.c.id == document_versions_table.c.document_id
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
.where(document_versions_table.c.id == version_id)
|
|
274
|
+
)
|
|
275
|
+
result = await session.execute(stmt)
|
|
276
|
+
row = result.mappings().first()
|
|
277
|
+
return dict(row) if row else None
|
|
278
|
+
|
|
279
|
+
async def get_version_by_content_hash(self, content_hash: str) -> dict | None:
|
|
280
|
+
"""Look up an already-ingested version by its file content hash —
|
|
281
|
+
used *before* parsing to detect "this exact file is already here"
|
|
282
|
+
without wastefully re-extracting images/tables first. Returns the
|
|
283
|
+
same shape as `get_version` (joined back to the parent `Document`)."""
|
|
284
|
+
async with self._sessionmaker() as session:
|
|
285
|
+
stmt = (
|
|
286
|
+
select(
|
|
287
|
+
documents_table.c.id.label("document_id"),
|
|
288
|
+
documents_table.c.title,
|
|
289
|
+
documents_table.c.collection,
|
|
290
|
+
document_versions_table.c.id,
|
|
291
|
+
document_versions_table.c.label,
|
|
292
|
+
document_versions_table.c.created_at,
|
|
293
|
+
)
|
|
294
|
+
.select_from(
|
|
295
|
+
document_versions_table.join(
|
|
296
|
+
documents_table, documents_table.c.id == document_versions_table.c.document_id
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
.where(document_versions_table.c.content_hash == content_hash)
|
|
300
|
+
)
|
|
301
|
+
result = await session.execute(stmt)
|
|
302
|
+
row = result.mappings().first()
|
|
303
|
+
return dict(row) if row else None
|
|
304
|
+
|
|
305
|
+
async def delete_version(self, version_id: UUID) -> bool:
|
|
306
|
+
"""Delete one `DocumentVersion` and everything that hangs off it
|
|
307
|
+
(sections/images/tables/chunks all have `ON DELETE CASCADE` back to
|
|
308
|
+
`document_versions` — see migrations 0001-0003). Useful for clearing
|
|
309
|
+
out a version that was ingested before a pipeline fix (e.g. the
|
|
310
|
+
image-dedup / table-persistence fixes) so it can be cleanly
|
|
311
|
+
re-ingested. Does *not* delete the parent `Document` row, even if
|
|
312
|
+
this was its only version — `documents` rows are cheap and harmless
|
|
313
|
+
to leave orphaned, and deleting them too would cascade-delete any
|
|
314
|
+
sibling versions.
|
|
315
|
+
|
|
316
|
+
Returns True if a row was deleted, False if `version_id` didn't exist.
|
|
317
|
+
"""
|
|
318
|
+
async with self._sessionmaker() as session:
|
|
319
|
+
result = await session.execute(
|
|
320
|
+
delete(document_versions_table).where(document_versions_table.c.id == version_id)
|
|
321
|
+
)
|
|
322
|
+
await session.commit()
|
|
323
|
+
return result.rowcount > 0
|
|
324
|
+
|
|
226
325
|
async def get_full_text(self, version_id: UUID) -> str | None:
|
|
227
326
|
async with self._sessionmaker() as session:
|
|
228
327
|
result = await session.execute(
|
|
@@ -266,4 +365,29 @@ class PostgresRepository:
|
|
|
266
365
|
if page is not None:
|
|
267
366
|
stmt = stmt.where(chunks_table.c.page_start == page)
|
|
268
367
|
result = await session.execute(stmt)
|
|
368
|
+
return [dict(row) for row in result.mappings().all()]
|
|
369
|
+
|
|
370
|
+
async def get_images(self, version_id: UUID) -> list[dict]:
|
|
371
|
+
"""Saved images for one version, in page order — `uri` is a
|
|
372
|
+
`file://...` (or other `BlobStore`-specific) URI; the server layer
|
|
373
|
+
resolves these into servable URLs, not this repository."""
|
|
374
|
+
async with self._sessionmaker() as session:
|
|
375
|
+
stmt = (
|
|
376
|
+
select(images_table)
|
|
377
|
+
.where(images_table.c.version_id == version_id)
|
|
378
|
+
.order_by(images_table.c.page_number)
|
|
379
|
+
)
|
|
380
|
+
result = await session.execute(stmt)
|
|
381
|
+
return [dict(row) for row in result.mappings().all()]
|
|
382
|
+
|
|
383
|
+
async def get_tables(self, version_id: UUID) -> list[dict]:
|
|
384
|
+
"""Saved extracted tables (rendered as images) for one version, in
|
|
385
|
+
page order."""
|
|
386
|
+
async with self._sessionmaker() as session:
|
|
387
|
+
stmt = (
|
|
388
|
+
select(tables_table)
|
|
389
|
+
.where(tables_table.c.version_id == version_id)
|
|
390
|
+
.order_by(tables_table.c.page_number)
|
|
391
|
+
)
|
|
392
|
+
result = await session.execute(stmt)
|
|
269
393
|
return [dict(row) for row in result.mappings().all()]
|
|
@@ -96,6 +96,23 @@ images = Table(
|
|
|
96
96
|
Column("captioned_at", DateTime(timezone=True), nullable=True),
|
|
97
97
|
)
|
|
98
98
|
|
|
99
|
+
tables = Table(
|
|
100
|
+
"tables",
|
|
101
|
+
metadata,
|
|
102
|
+
Column("id", UUID(as_uuid=True), primary_key=True),
|
|
103
|
+
Column("version_id", UUID(as_uuid=True), ForeignKey("document_versions.id", ondelete="CASCADE"), nullable=False),
|
|
104
|
+
Column("page_number", Integer, nullable=False),
|
|
105
|
+
Column("bbox_x0", Float, nullable=False),
|
|
106
|
+
Column("bbox_y0", Float, nullable=False),
|
|
107
|
+
Column("bbox_x1", Float, nullable=False),
|
|
108
|
+
Column("bbox_y1", Float, nullable=False),
|
|
109
|
+
Column("uri", Text, nullable=False),
|
|
110
|
+
Column("width", Integer, nullable=False),
|
|
111
|
+
Column("height", Integer, nullable=False),
|
|
112
|
+
Column("format", Text, nullable=False),
|
|
113
|
+
Column("phash", Text, nullable=True),
|
|
114
|
+
)
|
|
115
|
+
|
|
99
116
|
_chunks_columns = [
|
|
100
117
|
Column("id", UUID(as_uuid=True), primary_key=True),
|
|
101
118
|
Column("version_id", UUID(as_uuid=True), ForeignKey("document_versions.id", ondelete="CASCADE"), nullable=False),
|
|
@@ -23,6 +23,7 @@ app.command("doctor")(doctor.doctor)
|
|
|
23
23
|
from docsmind.cli import chat as _chat
|
|
24
24
|
from docsmind.cli import diff as _diff
|
|
25
25
|
from docsmind.cli import inspect as _inspect
|
|
26
|
+
from docsmind.cli import serve as _serve
|
|
26
27
|
|
|
27
28
|
app.command("chat")(_chat.chat)
|
|
28
29
|
app.command("diff")(_diff.diff)
|
|
@@ -30,6 +31,8 @@ app.command("versions")(_inspect.versions)
|
|
|
30
31
|
app.command("fulltext")(_inspect.fulltext)
|
|
31
32
|
app.command("outline")(_inspect.outline)
|
|
32
33
|
app.command("chunks")(_inspect.chunks)
|
|
34
|
+
app.command("delete")(_inspect.delete)
|
|
35
|
+
app.command("serve")(_serve.serve)
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
def main() -> None:
|
|
@@ -16,7 +16,12 @@ def ingest(
|
|
|
16
16
|
),
|
|
17
17
|
) -> None:
|
|
18
18
|
from docsmind import Docsmind
|
|
19
|
+
from docsmind.domain.errors import DocumentAlreadyExistsError
|
|
19
20
|
|
|
20
21
|
dm = Docsmind()
|
|
21
|
-
|
|
22
|
+
try:
|
|
23
|
+
result = dm.ingest(path, collection=collection, version=version)
|
|
24
|
+
except DocumentAlreadyExistsError as exc:
|
|
25
|
+
console.print(f"[yellow]Already ingested.[/yellow] {exc}")
|
|
26
|
+
raise typer.Exit(code=1) from None
|
|
22
27
|
console.print(f"[green]Ingested[/green] {path.name} -> version {result.id} ({result.label})")
|
|
@@ -169,4 +169,41 @@ def chunks(
|
|
|
169
169
|
suffix = "..." if len(content) > 200 else ""
|
|
170
170
|
console.print(f"[{label}] TEXT ({word_count} words): {preview}{suffix}")
|
|
171
171
|
|
|
172
|
+
asyncio.run(_run())
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def delete(
|
|
176
|
+
version_id: UUID = typer.Argument(..., help="A version id from `docsmind versions`."),
|
|
177
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt."),
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Delete one ingested version (sections/images/tables/chunks cascade with it).
|
|
180
|
+
|
|
181
|
+
Useful for clearing out a version that was ingested before a pipeline
|
|
182
|
+
fix landed (e.g. the image-dedup or table-persistence fixes) so you can
|
|
183
|
+
re-ingest the same file cleanly afterwards — re-ingesting identical bytes
|
|
184
|
+
is otherwise a no-op (see `DocumentAlreadyExistsError`).
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
async def _run() -> None:
|
|
188
|
+
from docsmind.adapters.storage.postgres.repository import PostgresRepository
|
|
189
|
+
from docsmind.config import DocsmindConfig
|
|
190
|
+
|
|
191
|
+
repo = PostgresRepository(DocsmindConfig())
|
|
192
|
+
existing = await repo.get_version(version_id)
|
|
193
|
+
if existing is None:
|
|
194
|
+
console.print(f"[yellow]No version found with id {version_id}.[/yellow]")
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
if not yes:
|
|
198
|
+
confirm = typer.confirm(
|
|
199
|
+
f"Delete '{existing['title']}' ({existing['collection']} / {existing['label']}, "
|
|
200
|
+
f"version {version_id})? This also deletes its chunks, images and tables."
|
|
201
|
+
)
|
|
202
|
+
if not confirm:
|
|
203
|
+
console.print("Aborted.")
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
await repo.delete_version(version_id)
|
|
207
|
+
console.print(f"[green]Deleted[/green] version {version_id}.")
|
|
208
|
+
|
|
172
209
|
asyncio.run(_run())
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""`docsmind serve` — runs the FastAPI app (`server/app.py`) behind uvicorn,
|
|
2
|
+
for the `frontend/` UI to talk to. Requires the `server` extra:
|
|
3
|
+
`pip install "docsmind[server]"`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def serve(
|
|
15
|
+
host: str = typer.Option("127.0.0.1", "--host", help="Bind address."),
|
|
16
|
+
port: int = typer.Option(8000, "--port", "-p", help="Bind port."),
|
|
17
|
+
reload: bool = typer.Option(False, "--reload", help="Auto-reload on code changes (dev only)."),
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Start the docsmind API server used by the frontend UI."""
|
|
20
|
+
try:
|
|
21
|
+
import uvicorn
|
|
22
|
+
except ImportError as exc: # pragma: no cover - import guard
|
|
23
|
+
console.print(
|
|
24
|
+
"[red]uvicorn isn't installed.[/red] Run `pip install \"docsmind[server]\"` first."
|
|
25
|
+
)
|
|
26
|
+
raise typer.Exit(1) from exc
|
|
27
|
+
|
|
28
|
+
console.print(f"[green]Starting docsmind server[/green] on http://{host}:{port}")
|
|
29
|
+
uvicorn.run("docsmind.server.app:app", host=host, port=port, reload=reload)
|