agent-brain-rag 1.1.0__py3-none-any.whl
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.
- agent_brain_rag-1.1.0.dist-info/METADATA +202 -0
- agent_brain_rag-1.1.0.dist-info/RECORD +31 -0
- agent_brain_rag-1.1.0.dist-info/WHEEL +4 -0
- agent_brain_rag-1.1.0.dist-info/entry_points.txt +3 -0
- doc_serve_server/__init__.py +3 -0
- doc_serve_server/api/__init__.py +5 -0
- doc_serve_server/api/main.py +332 -0
- doc_serve_server/api/routers/__init__.py +11 -0
- doc_serve_server/api/routers/health.py +100 -0
- doc_serve_server/api/routers/index.py +208 -0
- doc_serve_server/api/routers/query.py +96 -0
- doc_serve_server/config/__init__.py +5 -0
- doc_serve_server/config/settings.py +92 -0
- doc_serve_server/indexing/__init__.py +19 -0
- doc_serve_server/indexing/bm25_index.py +166 -0
- doc_serve_server/indexing/chunking.py +831 -0
- doc_serve_server/indexing/document_loader.py +506 -0
- doc_serve_server/indexing/embedding.py +274 -0
- doc_serve_server/locking.py +133 -0
- doc_serve_server/models/__init__.py +18 -0
- doc_serve_server/models/health.py +126 -0
- doc_serve_server/models/index.py +157 -0
- doc_serve_server/models/query.py +191 -0
- doc_serve_server/project_root.py +85 -0
- doc_serve_server/runtime.py +112 -0
- doc_serve_server/services/__init__.py +11 -0
- doc_serve_server/services/indexing_service.py +476 -0
- doc_serve_server/services/query_service.py +414 -0
- doc_serve_server/storage/__init__.py +5 -0
- doc_serve_server/storage/vector_store.py +320 -0
- doc_serve_server/storage_paths.py +72 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: agent-brain-rag
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: RAG-based document indexing and semantic search server for AI agents
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: rag,semantic-search,documentation,indexing,llama-index,chromadb,ai-agent,brain
|
|
7
|
+
Author: Spillwave Solutions
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
18
|
+
Classifier: Topic :: Text Processing :: Indexing
|
|
19
|
+
Requires-Dist: anthropic (>=0.40.0,<0.41.0)
|
|
20
|
+
Requires-Dist: chromadb (>=0.5.0,<0.6.0)
|
|
21
|
+
Requires-Dist: click (>=8.1.0,<9.0.0)
|
|
22
|
+
Requires-Dist: fastapi (>=0.115.0,<0.116.0)
|
|
23
|
+
Requires-Dist: llama-index-core (>=0.14.0,<0.15.0)
|
|
24
|
+
Requires-Dist: llama-index-embeddings-openai (>=0.5.0,<0.6.0)
|
|
25
|
+
Requires-Dist: llama-index-llms-openai (>=0.6.12,<0.7.0)
|
|
26
|
+
Requires-Dist: llama-index-readers-file (>=0.5.0,<0.6.0)
|
|
27
|
+
Requires-Dist: llama-index-retrievers-bm25 (>=0.6.0,<0.7.0)
|
|
28
|
+
Requires-Dist: openai (>=1.57.0,<2.0.0)
|
|
29
|
+
Requires-Dist: pydantic (>=2.10.0,<3.0.0)
|
|
30
|
+
Requires-Dist: pydantic-settings (>=2.6.0,<3.0.0)
|
|
31
|
+
Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
|
|
32
|
+
Requires-Dist: rank-bm25 (>=0.2.2,<0.3.0)
|
|
33
|
+
Requires-Dist: tiktoken (>=0.8.0,<0.9.0)
|
|
34
|
+
Requires-Dist: tree-sitter-language-pack (>=0.7.3,<0.8.0)
|
|
35
|
+
Requires-Dist: uvicorn[standard] (>=0.32.0,<0.33.0)
|
|
36
|
+
Project-URL: Documentation, https://github.com/SpillwaveSolutions/doc-serve#readme
|
|
37
|
+
Project-URL: Homepage, https://github.com/SpillwaveSolutions/doc-serve
|
|
38
|
+
Project-URL: Repository, https://github.com/SpillwaveSolutions/doc-serve
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# Doc-Serve Server
|
|
42
|
+
|
|
43
|
+
RAG-based document indexing and semantic search REST API service.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install doc-serve
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
1. Set environment variables:
|
|
54
|
+
```bash
|
|
55
|
+
export OPENAI_API_KEY=your-key
|
|
56
|
+
export ANTHROPIC_API_KEY=your-key
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
2. Start the server:
|
|
60
|
+
```bash
|
|
61
|
+
doc-serve
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The server will start at `http://127.0.0.1:8000`.
|
|
65
|
+
|
|
66
|
+
## Features
|
|
67
|
+
|
|
68
|
+
- **Document Indexing**: Load and index documents from folders (PDF, Markdown, TXT, DOCX, HTML)
|
|
69
|
+
- **Context-Aware Chunking**: Smart text splitting with configurable chunk sizes and overlap
|
|
70
|
+
- **Semantic Search**: Query indexed documents using natural language
|
|
71
|
+
- **OpenAI Embeddings**: Uses `text-embedding-3-large` for high-quality embeddings
|
|
72
|
+
- **Chroma Vector Store**: Persistent, thread-safe vector database
|
|
73
|
+
- **FastAPI**: Modern, high-performance REST API with OpenAPI documentation
|
|
74
|
+
|
|
75
|
+
## Quick Start
|
|
76
|
+
|
|
77
|
+
### Prerequisites
|
|
78
|
+
|
|
79
|
+
- Python 3.10+
|
|
80
|
+
- Poetry
|
|
81
|
+
- OpenAI API key
|
|
82
|
+
|
|
83
|
+
### Installation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
cd doc-serve-server
|
|
87
|
+
poetry install
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Configuration
|
|
91
|
+
|
|
92
|
+
Copy the environment template and configure:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
cp ../.env.example .env
|
|
96
|
+
# Edit .env with your API keys
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Required environment variables:
|
|
100
|
+
- `OPENAI_API_KEY`: Your OpenAI API key for embeddings
|
|
101
|
+
|
|
102
|
+
### Running the Server
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Development mode
|
|
106
|
+
poetry run uvicorn doc_serve_server.api.main:app --reload
|
|
107
|
+
|
|
108
|
+
# Or use the entry point
|
|
109
|
+
poetry run doc-serve
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The server will start at `http://127.0.0.1:8000`.
|
|
113
|
+
|
|
114
|
+
### API Documentation
|
|
115
|
+
|
|
116
|
+
Once running, visit:
|
|
117
|
+
- Swagger UI: http://127.0.0.1:8000/docs
|
|
118
|
+
- ReDoc: http://127.0.0.1:8000/redoc
|
|
119
|
+
- OpenAPI JSON: http://127.0.0.1:8000/openapi.json
|
|
120
|
+
|
|
121
|
+
## API Endpoints
|
|
122
|
+
|
|
123
|
+
### Health
|
|
124
|
+
|
|
125
|
+
- `GET /health` - Server health status
|
|
126
|
+
- `GET /health/status` - Detailed indexing status
|
|
127
|
+
|
|
128
|
+
### Indexing
|
|
129
|
+
|
|
130
|
+
- `POST /index` - Start indexing documents from a folder
|
|
131
|
+
- `POST /index/add` - Add documents to existing index
|
|
132
|
+
- `DELETE /index` - Reset the index
|
|
133
|
+
|
|
134
|
+
### Querying
|
|
135
|
+
|
|
136
|
+
- `POST /query` - Semantic search query
|
|
137
|
+
- `GET /query/count` - Get indexed document count
|
|
138
|
+
|
|
139
|
+
## Example Usage
|
|
140
|
+
|
|
141
|
+
### Index Documents
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
curl -X POST http://localhost:8000/index \
|
|
145
|
+
-H "Content-Type: application/json" \
|
|
146
|
+
-d '{"folder_path": "/path/to/docs"}'
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Query Documents
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
curl -X POST http://localhost:8000/query \
|
|
153
|
+
-H "Content-Type: application/json" \
|
|
154
|
+
-d '{"query": "How do I configure authentication?", "top_k": 5}'
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Architecture
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
doc_serve_server/
|
|
161
|
+
├── api/
|
|
162
|
+
│ ├── main.py # FastAPI application
|
|
163
|
+
│ └── routers/ # Endpoint handlers
|
|
164
|
+
├── config/
|
|
165
|
+
│ └── settings.py # Configuration management
|
|
166
|
+
├── models/ # Pydantic request/response models
|
|
167
|
+
├── indexing/
|
|
168
|
+
│ ├── document_loader.py # Document loading
|
|
169
|
+
│ ├── chunking.py # Text chunking
|
|
170
|
+
│ └── embedding.py # Embedding generation
|
|
171
|
+
├── services/
|
|
172
|
+
│ ├── indexing_service.py # Indexing orchestration
|
|
173
|
+
│ └── query_service.py # Query execution
|
|
174
|
+
└── storage/
|
|
175
|
+
└── vector_store.py # Chroma vector store
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Development
|
|
179
|
+
|
|
180
|
+
### Running Tests
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
poetry run pytest
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Code Formatting
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
poetry run black doc_serve_server/
|
|
190
|
+
poetry run ruff check doc_serve_server/
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Type Checking
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
poetry run mypy doc_serve_server/
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|
|
202
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
doc_serve_server/__init__.py,sha256=CWV76BF1YvkQVzexuEm1sl4Zn4z27wRd_9dWbAZNWa0,95
|
|
2
|
+
doc_serve_server/api/__init__.py,sha256=nvTvO_ahHAAsRDlV3dL_JlNruSdan4kav_P5sT_1PFk,93
|
|
3
|
+
doc_serve_server/api/main.py,sha256=a3g_N9VEY-VAG-HvNXMW7u-xWvwjP3P9BtPlxNgC900,10628
|
|
4
|
+
doc_serve_server/api/routers/__init__.py,sha256=Z3PUKDwxeI8T88xMQeTcQ8nq1bQnu0nYjRnqNIsDUyY,254
|
|
5
|
+
doc_serve_server/api/routers/health.py,sha256=cbiBooeKMKoTpje8xd2Yywpr7fSrCJZP9ubl8EJOjEo,3442
|
|
6
|
+
doc_serve_server/api/routers/index.py,sha256=M4zOlXX_pmMYDcmfK6fh9PX5qqw-7xHuwG-Mp-0CZw0,6754
|
|
7
|
+
doc_serve_server/api/routers/query.py,sha256=zHyzQBPOz2Xz8MAF1rU2reWopYJsXCBWFW2kQe_f4hg,2787
|
|
8
|
+
doc_serve_server/config/__init__.py,sha256=zzDErZGUBwUm5Fk43OHJN7eWpeIw_1kWdnhsN6QQqSc,84
|
|
9
|
+
doc_serve_server/config/settings.py,sha256=ePpGSJ2VVov_LOtQtaOedi_HM-Yf8YcpJQzSyLHfyZQ,2629
|
|
10
|
+
doc_serve_server/indexing/__init__.py,sha256=7P1zcYAQFO4ME6uLtc75LL-GT7dEzXk2yY0JAcP0kRc,587
|
|
11
|
+
doc_serve_server/indexing/bm25_index.py,sha256=0JZ4_T1d9H7FLUDqnnXHcoO2_qrztzXrtNj7HNMg2WQ,5266
|
|
12
|
+
doc_serve_server/indexing/chunking.py,sha256=uGeCRRT19eVcFPpK9KkxI0_1OXjFRQZZI6B5CwCVR-4,29846
|
|
13
|
+
doc_serve_server/indexing/document_loader.py,sha256=D6U3mDl_2jFfT_JxD-5yudtqlJW8Fh0zj5lhz0V0tPs,16090
|
|
14
|
+
doc_serve_server/indexing/embedding.py,sha256=MtL9SLpBQxh99hJ_dufah0xyIGwznHGwnfnqcdyhSCc,9053
|
|
15
|
+
doc_serve_server/locking.py,sha256=yRpswpBem4964gTh1VH4VUPT-vBIRnNEVgWdorJA4Hg,3365
|
|
16
|
+
doc_serve_server/models/__init__.py,sha256=JLVsiVet-JDVC9s6ZefBcTXWZT5TVXrbI0E2B3ejpWM,478
|
|
17
|
+
doc_serve_server/models/health.py,sha256=-NWwyPI0cUK1Sqhf5x0WO-ilyJbwJxvROzeaFn12WDc,3661
|
|
18
|
+
doc_serve_server/models/index.py,sha256=pjDv7phLS6dpiHLlEtcAuXQN_aHIfA_4lMkAZ-NkXZQ,5400
|
|
19
|
+
doc_serve_server/models/query.py,sha256=phl-bJFxhu83k1iP4Cx3GH5qJUQ12Y1Ah_bAwXOJVZM,6054
|
|
20
|
+
doc_serve_server/project_root.py,sha256=HIY5NMRDYWYIT7K6B_UMOGo1zXn9zwAdGI6ApViKI_8,2194
|
|
21
|
+
doc_serve_server/runtime.py,sha256=bcchfDBt_tn2_r-lFhxqKp0RncKrz152D-oM5RcKGrU,3076
|
|
22
|
+
doc_serve_server/services/__init__.py,sha256=E4VPN9Rqa2mxGQQEQn-5IYj63LSPTrA8aIx8ENO5xcc,296
|
|
23
|
+
doc_serve_server/services/indexing_service.py,sha256=rOs0xlMy_1RQuoPt62w428-CshPWj91CFSpkL251TEI,18555
|
|
24
|
+
doc_serve_server/services/query_service.py,sha256=elyKWl02v8BzjdBr8S7KOiUUEdS2Rp3V85ifUU5Bi-g,14852
|
|
25
|
+
doc_serve_server/storage/__init__.py,sha256=T4N0DhDT3av7zn3Ra6uLhMsjL9N5wakqC4cl-QXWvmM,222
|
|
26
|
+
doc_serve_server/storage/vector_store.py,sha256=9pdEPvlf2JAUnwxDpb9CwY_A7sIqv2iu5ddy5SQm-ys,10881
|
|
27
|
+
doc_serve_server/storage_paths.py,sha256=aHSsTwELwIk1VOLO0L6O-RsbRJ1U20jvJ-EQsFKG1a8,1761
|
|
28
|
+
agent_brain_rag-1.1.0.dist-info/METADATA,sha256=dOzH_3y00uehax1LiI5QBHToTwnN09tGyoRygBw_uVA,5230
|
|
29
|
+
agent_brain_rag-1.1.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
30
|
+
agent_brain_rag-1.1.0.dist-info/entry_points.txt,sha256=DOPhlYyScH9BTPXr9_FJzgRplzHe6bR9q2pTCCg2QOI,59
|
|
31
|
+
agent_brain_rag-1.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""FastAPI application entry point."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import socket
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
import uvicorn
|
|
13
|
+
from fastapi import FastAPI
|
|
14
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
15
|
+
|
|
16
|
+
from doc_serve_server import __version__
|
|
17
|
+
from doc_serve_server.config import settings
|
|
18
|
+
from doc_serve_server.indexing.bm25_index import BM25IndexManager
|
|
19
|
+
from doc_serve_server.locking import acquire_lock, cleanup_stale, is_stale, release_lock
|
|
20
|
+
from doc_serve_server.project_root import resolve_project_root
|
|
21
|
+
from doc_serve_server.runtime import RuntimeState, delete_runtime, write_runtime
|
|
22
|
+
from doc_serve_server.services import IndexingService, QueryService
|
|
23
|
+
from doc_serve_server.storage import VectorStoreManager
|
|
24
|
+
from doc_serve_server.storage_paths import resolve_state_dir, resolve_storage_paths
|
|
25
|
+
|
|
26
|
+
from .routers import health_router, index_router, query_router
|
|
27
|
+
|
|
28
|
+
# Configure logging
|
|
29
|
+
logging.basicConfig(
|
|
30
|
+
level=logging.DEBUG if settings.DEBUG else logging.INFO,
|
|
31
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
32
|
+
)
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# Module-level state for multi-instance mode
|
|
36
|
+
_runtime_state: Optional[RuntimeState] = None
|
|
37
|
+
_state_dir: Optional[Path] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@asynccontextmanager
|
|
41
|
+
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
42
|
+
"""Application lifespan manager.
|
|
43
|
+
|
|
44
|
+
Initializes services and stores them on app.state for dependency
|
|
45
|
+
injection via request.app.state in route handlers.
|
|
46
|
+
|
|
47
|
+
In per-project mode:
|
|
48
|
+
- Resolves project root and state directory
|
|
49
|
+
- Acquires lock (with stale detection)
|
|
50
|
+
- Writes runtime.json with server info
|
|
51
|
+
- Cleans up on shutdown
|
|
52
|
+
"""
|
|
53
|
+
global _runtime_state, _state_dir
|
|
54
|
+
|
|
55
|
+
logger.info("Starting Doc-Serve server...")
|
|
56
|
+
|
|
57
|
+
if settings.OPENAI_API_KEY:
|
|
58
|
+
os.environ["OPENAI_API_KEY"] = settings.OPENAI_API_KEY
|
|
59
|
+
|
|
60
|
+
# Determine mode and resolve paths
|
|
61
|
+
mode = settings.DOC_SERVE_MODE
|
|
62
|
+
state_dir = _state_dir # May be set by CLI
|
|
63
|
+
storage_paths: Optional[dict[str, Path]] = None
|
|
64
|
+
|
|
65
|
+
if state_dir is not None:
|
|
66
|
+
# Per-project mode with explicit state directory
|
|
67
|
+
mode = "project"
|
|
68
|
+
|
|
69
|
+
# Check for stale locks and clean up
|
|
70
|
+
if is_stale(state_dir):
|
|
71
|
+
logger.info(f"Cleaning stale lock in {state_dir}")
|
|
72
|
+
cleanup_stale(state_dir)
|
|
73
|
+
|
|
74
|
+
# Acquire exclusive lock
|
|
75
|
+
if not acquire_lock(state_dir):
|
|
76
|
+
raise RuntimeError(
|
|
77
|
+
f"Another doc-serve instance is already running for {state_dir}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Resolve storage paths (creates directories)
|
|
81
|
+
storage_paths = resolve_storage_paths(state_dir)
|
|
82
|
+
logger.info(f"State directory: {state_dir}")
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# Determine persistence directories
|
|
86
|
+
chroma_dir = (
|
|
87
|
+
str(storage_paths["chroma_db"])
|
|
88
|
+
if storage_paths
|
|
89
|
+
else settings.CHROMA_PERSIST_DIR
|
|
90
|
+
)
|
|
91
|
+
bm25_dir = (
|
|
92
|
+
str(storage_paths["bm25_index"])
|
|
93
|
+
if storage_paths
|
|
94
|
+
else settings.BM25_INDEX_PATH
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Initialize services and store on app.state for DI
|
|
98
|
+
vector_store = VectorStoreManager(
|
|
99
|
+
persist_dir=chroma_dir,
|
|
100
|
+
)
|
|
101
|
+
await vector_store.initialize()
|
|
102
|
+
app.state.vector_store = vector_store
|
|
103
|
+
logger.info("Vector store initialized")
|
|
104
|
+
|
|
105
|
+
bm25_manager = BM25IndexManager(
|
|
106
|
+
persist_dir=bm25_dir,
|
|
107
|
+
)
|
|
108
|
+
bm25_manager.initialize()
|
|
109
|
+
app.state.bm25_manager = bm25_manager
|
|
110
|
+
logger.info("BM25 index manager initialized")
|
|
111
|
+
|
|
112
|
+
# Create indexing service with injected deps
|
|
113
|
+
indexing_service = IndexingService(
|
|
114
|
+
vector_store=vector_store,
|
|
115
|
+
bm25_manager=bm25_manager,
|
|
116
|
+
)
|
|
117
|
+
app.state.indexing_service = indexing_service
|
|
118
|
+
|
|
119
|
+
# Create query service with injected deps
|
|
120
|
+
query_service = QueryService(
|
|
121
|
+
vector_store=vector_store,
|
|
122
|
+
bm25_manager=bm25_manager,
|
|
123
|
+
)
|
|
124
|
+
app.state.query_service = query_service
|
|
125
|
+
|
|
126
|
+
# Set multi-instance metadata on app.state for health endpoint
|
|
127
|
+
app.state.mode = mode
|
|
128
|
+
app.state.instance_id = _runtime_state.instance_id if _runtime_state else None
|
|
129
|
+
app.state.project_id = _runtime_state.project_id if _runtime_state else None
|
|
130
|
+
app.state.active_projects = None # For shared mode (future)
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.error(f"Failed to initialize services: {e}")
|
|
134
|
+
# Clean up lock if we acquired it
|
|
135
|
+
if state_dir is not None:
|
|
136
|
+
release_lock(state_dir)
|
|
137
|
+
raise
|
|
138
|
+
|
|
139
|
+
yield
|
|
140
|
+
|
|
141
|
+
logger.info("Shutting down Doc-Serve server...")
|
|
142
|
+
|
|
143
|
+
# Cleanup for per-project mode
|
|
144
|
+
if state_dir is not None:
|
|
145
|
+
delete_runtime(state_dir)
|
|
146
|
+
release_lock(state_dir)
|
|
147
|
+
logger.info(f"Released lock and cleaned up state in {state_dir}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# Create FastAPI application
|
|
151
|
+
app = FastAPI(
|
|
152
|
+
title="Doc-Serve API",
|
|
153
|
+
description=(
|
|
154
|
+
"RAG-based document indexing and semantic search API. "
|
|
155
|
+
"Index documents from folders and query them using natural language."
|
|
156
|
+
),
|
|
157
|
+
version="1.1.0",
|
|
158
|
+
lifespan=lifespan,
|
|
159
|
+
docs_url="/docs",
|
|
160
|
+
redoc_url="/redoc",
|
|
161
|
+
openapi_url="/openapi.json",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Add CORS middleware
|
|
165
|
+
app.add_middleware(
|
|
166
|
+
CORSMiddleware,
|
|
167
|
+
allow_origins=["*"], # Configure appropriately for production
|
|
168
|
+
allow_credentials=True,
|
|
169
|
+
allow_methods=["*"],
|
|
170
|
+
allow_headers=["*"],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Include routers
|
|
174
|
+
app.include_router(health_router, prefix="/health", tags=["Health"])
|
|
175
|
+
app.include_router(index_router, prefix="/index", tags=["Indexing"])
|
|
176
|
+
app.include_router(query_router, prefix="/query", tags=["Querying"])
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@app.get("/", include_in_schema=False)
|
|
180
|
+
async def root() -> dict[str, str]:
|
|
181
|
+
"""Root endpoint redirects to docs."""
|
|
182
|
+
return {
|
|
183
|
+
"name": "Doc-Serve API",
|
|
184
|
+
"version": "1.1.0",
|
|
185
|
+
"docs": "/docs",
|
|
186
|
+
"health": "/health",
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _find_free_port() -> int:
|
|
191
|
+
"""Find a free port by binding to port 0.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
An available port number.
|
|
195
|
+
"""
|
|
196
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
197
|
+
s.bind(("", 0))
|
|
198
|
+
s.listen(1)
|
|
199
|
+
port = s.getsockname()[1]
|
|
200
|
+
return port # type: ignore[no-any-return]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def run(
|
|
204
|
+
host: Optional[str] = None,
|
|
205
|
+
port: Optional[int] = None,
|
|
206
|
+
reload: Optional[bool] = None,
|
|
207
|
+
state_dir: Optional[str] = None,
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Run the server using uvicorn.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
host: Host to bind to (default: from settings)
|
|
213
|
+
port: Port to bind to (default: from settings, 0 = auto-assign)
|
|
214
|
+
reload: Enable auto-reload (default: from DEBUG setting)
|
|
215
|
+
state_dir: State directory for per-project mode (enables locking)
|
|
216
|
+
"""
|
|
217
|
+
global _runtime_state, _state_dir
|
|
218
|
+
|
|
219
|
+
resolved_host = host or settings.API_HOST
|
|
220
|
+
resolved_port = port if port is not None else settings.API_PORT
|
|
221
|
+
|
|
222
|
+
# Handle port 0: find a free port
|
|
223
|
+
if resolved_port == 0:
|
|
224
|
+
resolved_port = _find_free_port()
|
|
225
|
+
logger.info(f"Auto-assigned port: {resolved_port}")
|
|
226
|
+
|
|
227
|
+
# Set up per-project mode if state_dir specified
|
|
228
|
+
if state_dir:
|
|
229
|
+
_state_dir = Path(state_dir).resolve()
|
|
230
|
+
|
|
231
|
+
# Create runtime state
|
|
232
|
+
_runtime_state = RuntimeState(
|
|
233
|
+
mode="project",
|
|
234
|
+
project_root=str(_state_dir.parent.parent.parent), # .claude/doc-serve
|
|
235
|
+
bind_host=resolved_host,
|
|
236
|
+
port=resolved_port,
|
|
237
|
+
pid=os.getpid(),
|
|
238
|
+
base_url=f"http://{resolved_host}:{resolved_port}",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Write runtime.json before starting server
|
|
242
|
+
# Note: Lock is acquired in lifespan, but we write runtime early
|
|
243
|
+
# for port discovery by CLI tools
|
|
244
|
+
_state_dir.mkdir(parents=True, exist_ok=True)
|
|
245
|
+
write_runtime(_state_dir, _runtime_state)
|
|
246
|
+
logger.info(f"Per-project mode enabled: {_state_dir}")
|
|
247
|
+
|
|
248
|
+
uvicorn.run(
|
|
249
|
+
"doc_serve_server.api.main:app",
|
|
250
|
+
host=resolved_host,
|
|
251
|
+
port=resolved_port,
|
|
252
|
+
reload=reload if reload is not None else settings.DEBUG,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@click.command()
|
|
257
|
+
@click.version_option(version=__version__, prog_name="doc-serve")
|
|
258
|
+
@click.option(
|
|
259
|
+
"--host",
|
|
260
|
+
"-h",
|
|
261
|
+
default=None,
|
|
262
|
+
help=f"Host to bind to (default: {settings.API_HOST})",
|
|
263
|
+
)
|
|
264
|
+
@click.option(
|
|
265
|
+
"--port",
|
|
266
|
+
"-p",
|
|
267
|
+
type=int,
|
|
268
|
+
default=None,
|
|
269
|
+
help=f"Port to bind to (default: {settings.API_PORT}, 0 = auto-assign)",
|
|
270
|
+
)
|
|
271
|
+
@click.option(
|
|
272
|
+
"--reload/--no-reload",
|
|
273
|
+
default=None,
|
|
274
|
+
help=f"Enable auto-reload (default: {'enabled' if settings.DEBUG else 'disabled'})",
|
|
275
|
+
)
|
|
276
|
+
@click.option(
|
|
277
|
+
"--state-dir",
|
|
278
|
+
"-s",
|
|
279
|
+
default=None,
|
|
280
|
+
help="State directory for per-project mode (enables locking and runtime.json)",
|
|
281
|
+
)
|
|
282
|
+
@click.option(
|
|
283
|
+
"--project-dir",
|
|
284
|
+
"-d",
|
|
285
|
+
default=None,
|
|
286
|
+
help="Project directory (auto-resolves state-dir to .claude/doc-serve)",
|
|
287
|
+
)
|
|
288
|
+
def cli(
|
|
289
|
+
host: Optional[str],
|
|
290
|
+
port: Optional[int],
|
|
291
|
+
reload: Optional[bool],
|
|
292
|
+
state_dir: Optional[str],
|
|
293
|
+
project_dir: Optional[str],
|
|
294
|
+
) -> None:
|
|
295
|
+
"""Doc-Serve Server - RAG-based document indexing and semantic search API.
|
|
296
|
+
|
|
297
|
+
Start the FastAPI server for document indexing and querying.
|
|
298
|
+
|
|
299
|
+
\b
|
|
300
|
+
Examples:
|
|
301
|
+
doc-serve # Start with default settings
|
|
302
|
+
doc-serve --port 8080 # Start on port 8080
|
|
303
|
+
doc-serve --port 0 # Auto-assign an available port
|
|
304
|
+
doc-serve --host 0.0.0.0 # Bind to all interfaces
|
|
305
|
+
doc-serve --reload # Enable auto-reload
|
|
306
|
+
doc-serve --project-dir /my/project # Per-project mode (auto state-dir)
|
|
307
|
+
doc-serve --state-dir /path/.claude/doc-serve # Explicit state directory
|
|
308
|
+
|
|
309
|
+
\b
|
|
310
|
+
Environment Variables:
|
|
311
|
+
API_HOST Server host (default: 127.0.0.1)
|
|
312
|
+
API_PORT Server port (default: 8000)
|
|
313
|
+
DEBUG Enable debug mode (default: false)
|
|
314
|
+
DOC_SERVE_STATE_DIR Override state directory
|
|
315
|
+
DOC_SERVE_MODE Instance mode: 'project' or 'shared'
|
|
316
|
+
"""
|
|
317
|
+
# Resolve state directory from options
|
|
318
|
+
resolved_state_dir = state_dir
|
|
319
|
+
|
|
320
|
+
if project_dir and not state_dir:
|
|
321
|
+
# Auto-resolve state-dir from project directory
|
|
322
|
+
project_root = resolve_project_root(Path(project_dir))
|
|
323
|
+
resolved_state_dir = str(resolve_state_dir(project_root))
|
|
324
|
+
elif settings.DOC_SERVE_STATE_DIR and not state_dir:
|
|
325
|
+
# Use environment variable if set
|
|
326
|
+
resolved_state_dir = settings.DOC_SERVE_STATE_DIR
|
|
327
|
+
|
|
328
|
+
run(host=host, port=port, reload=reload, state_dir=resolved_state_dir)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
if __name__ == "__main__":
|
|
332
|
+
cli()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""API routers for different endpoint groups."""
|
|
2
|
+
|
|
3
|
+
from .health import router as health_router
|
|
4
|
+
from .index import router as index_router
|
|
5
|
+
from .query import router as query_router
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"health_router",
|
|
9
|
+
"index_router",
|
|
10
|
+
"query_router",
|
|
11
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Health check endpoints."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Request
|
|
7
|
+
|
|
8
|
+
from doc_serve_server.models import HealthStatus, IndexingStatus
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get(
|
|
14
|
+
"/",
|
|
15
|
+
response_model=HealthStatus,
|
|
16
|
+
summary="Health Check",
|
|
17
|
+
description="Returns the current server health status.",
|
|
18
|
+
)
|
|
19
|
+
async def health_check(request: Request) -> HealthStatus:
|
|
20
|
+
"""Check server health status.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
HealthStatus with current status:
|
|
24
|
+
- healthy: Server is running and ready for queries
|
|
25
|
+
- indexing: Server is currently indexing documents
|
|
26
|
+
- degraded: Server is up but some services are unavailable
|
|
27
|
+
- unhealthy: Server is not operational
|
|
28
|
+
"""
|
|
29
|
+
indexing_service = request.app.state.indexing_service
|
|
30
|
+
vector_store = request.app.state.vector_store
|
|
31
|
+
|
|
32
|
+
# Determine status
|
|
33
|
+
status: Literal["healthy", "indexing", "degraded", "unhealthy"]
|
|
34
|
+
if indexing_service.is_indexing:
|
|
35
|
+
status = "indexing"
|
|
36
|
+
message = f"Indexing in progress: {indexing_service.state.folder_path}"
|
|
37
|
+
elif not vector_store.is_initialized:
|
|
38
|
+
status = "degraded"
|
|
39
|
+
message = "Vector store not initialized"
|
|
40
|
+
elif indexing_service.state.error:
|
|
41
|
+
status = "degraded"
|
|
42
|
+
message = f"Last indexing failed: {indexing_service.state.error}"
|
|
43
|
+
else:
|
|
44
|
+
status = "healthy"
|
|
45
|
+
message = "Server is running and ready for queries"
|
|
46
|
+
|
|
47
|
+
# Multi-instance metadata
|
|
48
|
+
mode = getattr(request.app.state, "mode", "project")
|
|
49
|
+
instance_id = getattr(request.app.state, "instance_id", None)
|
|
50
|
+
project_id = getattr(request.app.state, "project_id", None)
|
|
51
|
+
active_projects = getattr(request.app.state, "active_projects", None)
|
|
52
|
+
|
|
53
|
+
return HealthStatus(
|
|
54
|
+
status=status,
|
|
55
|
+
message=message,
|
|
56
|
+
timestamp=datetime.now(timezone.utc),
|
|
57
|
+
version="1.1.0",
|
|
58
|
+
mode=mode,
|
|
59
|
+
instance_id=instance_id,
|
|
60
|
+
project_id=project_id,
|
|
61
|
+
active_projects=active_projects,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@router.get(
|
|
66
|
+
"/status",
|
|
67
|
+
response_model=IndexingStatus,
|
|
68
|
+
summary="Indexing Status",
|
|
69
|
+
description="Returns detailed indexing status information.",
|
|
70
|
+
)
|
|
71
|
+
async def indexing_status(request: Request) -> IndexingStatus:
|
|
72
|
+
"""Get detailed indexing status.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
IndexingStatus with:
|
|
76
|
+
- total_documents: Number of documents indexed
|
|
77
|
+
- total_chunks: Number of chunks in vector store
|
|
78
|
+
- indexing_in_progress: Boolean indicating active indexing
|
|
79
|
+
- last_indexed_at: Timestamp of last indexing operation
|
|
80
|
+
- indexed_folders: List of folders that have been indexed
|
|
81
|
+
"""
|
|
82
|
+
indexing_service = request.app.state.indexing_service
|
|
83
|
+
status = await indexing_service.get_status()
|
|
84
|
+
|
|
85
|
+
return IndexingStatus(
|
|
86
|
+
total_documents=status["total_documents"],
|
|
87
|
+
total_chunks=status["total_chunks"],
|
|
88
|
+
total_doc_chunks=status.get("total_doc_chunks", 0),
|
|
89
|
+
total_code_chunks=status.get("total_code_chunks", 0),
|
|
90
|
+
indexing_in_progress=status["is_indexing"],
|
|
91
|
+
current_job_id=status["current_job_id"],
|
|
92
|
+
progress_percent=status["progress_percent"],
|
|
93
|
+
last_indexed_at=(
|
|
94
|
+
datetime.fromisoformat(status["completed_at"])
|
|
95
|
+
if status["completed_at"]
|
|
96
|
+
else None
|
|
97
|
+
),
|
|
98
|
+
indexed_folders=status["indexed_folders"],
|
|
99
|
+
supported_languages=status.get("supported_languages", []),
|
|
100
|
+
)
|