harborrag-app 2.0.0a1__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.
- harborrag_app/__init__.py +14 -0
- harborrag_app/api/README.md +470 -0
- harborrag_app/api/__init__.py +0 -0
- harborrag_app/api/app.py +193 -0
- harborrag_app/api/auth/__init__.py +1 -0
- harborrag_app/api/auth/base.py +20 -0
- harborrag_app/api/auth/dependencies.py +143 -0
- harborrag_app/api/auth/hmac.py +74 -0
- harborrag_app/api/auth/principal.py +37 -0
- harborrag_app/api/capacity.py +237 -0
- harborrag_app/api/capacity_dependency.py +47 -0
- harborrag_app/api/dependencies.py +21 -0
- harborrag_app/api/errors.py +179 -0
- harborrag_app/api/export_openapi.py +40 -0
- harborrag_app/api/metrics.py +113 -0
- harborrag_app/api/middleware.py +127 -0
- harborrag_app/api/router.py +32 -0
- harborrag_app/api/routes/__init__.py +34 -0
- harborrag_app/api/routes/activity.py +50 -0
- harborrag_app/api/routes/health.py +29 -0
- harborrag_app/api/routes/legacy_diagnostics.py +59 -0
- harborrag_app/api/routes/legacy_ingestions.py +186 -0
- harborrag_app/api/routes/metrics.py +60 -0
- harborrag_app/api/routes/projects.py +79 -0
- harborrag_app/api/routes/settings.py +24 -0
- harborrag_app/api/routes/sources.py +148 -0
- harborrag_app/api/schemas.py +60 -0
- harborrag_app/api/settings.py +80 -0
- harborrag_app/api/v1/__init__.py +1 -0
- harborrag_app/api/v1/admin/__init__.py +5 -0
- harborrag_app/api/v1/admin/dependencies.py +29 -0
- harborrag_app/api/v1/admin/routes.py +113 -0
- harborrag_app/api/v1/admin/schemas.py +28 -0
- harborrag_app/api/v1/agent/__init__.py +5 -0
- harborrag_app/api/v1/agent/dependencies.py +62 -0
- harborrag_app/api/v1/agent/routes.py +180 -0
- harborrag_app/api/v1/agent/schemas.py +73 -0
- harborrag_app/api/v1/chat/__init__.py +5 -0
- harborrag_app/api/v1/chat/dependencies.py +53 -0
- harborrag_app/api/v1/chat/routes.py +147 -0
- harborrag_app/api/v1/chat/schemas.py +70 -0
- harborrag_app/api/v1/connections/__init__.py +5 -0
- harborrag_app/api/v1/connections/dependencies.py +23 -0
- harborrag_app/api/v1/connections/routes.py +42 -0
- harborrag_app/api/v1/connections/schemas.py +27 -0
- harborrag_app/api/v1/ingestion/__init__.py +5 -0
- harborrag_app/api/v1/ingestion/commands.py +18 -0
- harborrag_app/api/v1/ingestion/dependencies.py +62 -0
- harborrag_app/api/v1/ingestion/routes.py +262 -0
- harborrag_app/api/v1/ingestion/schemas.py +196 -0
- harborrag_app/api/v1/retrieval/__init__.py +5 -0
- harborrag_app/api/v1/retrieval/dependencies.py +64 -0
- harborrag_app/api/v1/retrieval/routes.py +161 -0
- harborrag_app/api/v1/retrieval/schemas.py +218 -0
- harborrag_app/cli/README.md +385 -0
- harborrag_app/cli/__init__.py +0 -0
- harborrag_app/cli/banner.py +25 -0
- harborrag_app/cli/commands/__init__.py +0 -0
- harborrag_app/cli/commands/chat.py +75 -0
- harborrag_app/cli/commands/doctor.py +37 -0
- harborrag_app/cli/commands/ingest.py +309 -0
- harborrag_app/cli/commands/retrieve.py +117 -0
- harborrag_app/cli/dashboard/README.md +17 -0
- harborrag_app/cli/dashboard/__init__.py +4 -0
- harborrag_app/cli/dashboard/dialogs.py +42 -0
- harborrag_app/cli/dashboard/rendering.py +127 -0
- harborrag_app/cli/dashboard/schemas.py +82 -0
- harborrag_app/cli/dashboard/screen.py +215 -0
- harborrag_app/cli/dashboard/styles.py +143 -0
- harborrag_app/cli/environment.py +58 -0
- harborrag_app/cli/main.py +99 -0
- harborrag_app/cli/rendering.py +326 -0
- harborrag_app/cli/rendering_values.py +80 -0
- harborrag_app/cli/runner.py +252 -0
- harborrag_app/cli/stages.py +172 -0
- harborrag_app/workflow_control/README.md +25 -0
- harborrag_app/workflow_control/__init__.py +31 -0
- harborrag_app/workflow_control/agent/__init__.py +7 -0
- harborrag_app/workflow_control/agent/client.py +87 -0
- harborrag_app/workflow_control/agent/options.py +15 -0
- harborrag_app/workflow_control/agent/service.py +274 -0
- harborrag_app/workflow_control/chat/__init__.py +7 -0
- harborrag_app/workflow_control/chat/client.py +74 -0
- harborrag_app/workflow_control/chat/options.py +17 -0
- harborrag_app/workflow_control/chat/presenters.py +61 -0
- harborrag_app/workflow_control/chat/service.py +264 -0
- harborrag_app/workflow_control/composition/__init__.py +1 -0
- harborrag_app/workflow_control/composition/factories.py +60 -0
- harborrag_app/workflow_control/composition/resources.py +105 -0
- harborrag_app/workflow_control/composition/selection.py +25 -0
- harborrag_app/workflow_control/composition/service.py +303 -0
- harborrag_app/workflow_control/control_plane/__init__.py +1 -0
- harborrag_app/workflow_control/control_plane/effect_recovery.py +160 -0
- harborrag_app/workflow_control/control_plane/metrics.py +44 -0
- harborrag_app/workflow_control/control_plane/reads.py +72 -0
- harborrag_app/workflow_control/control_plane/writes.py +260 -0
- harborrag_app/workflow_control/errors.py +109 -0
- harborrag_app/workflow_control/ingestion/__init__.py +1 -0
- harborrag_app/workflow_control/ingestion/client.py +91 -0
- harborrag_app/workflow_control/ingestion/connections.py +44 -0
- harborrag_app/workflow_control/ingestion/models.py +25 -0
- harborrag_app/workflow_control/ingestion/ports.py +114 -0
- harborrag_app/workflow_control/ingestion/presenters.py +264 -0
- harborrag_app/workflow_control/ingestion/progress_bridge.py +151 -0
- harborrag_app/workflow_control/ingestion/recovery.py +114 -0
- harborrag_app/workflow_control/ingestion/service.py +344 -0
- harborrag_app/workflow_control/ingestion/task_pages.py +91 -0
- harborrag_app/workflow_control/ingestion/temporal.py +194 -0
- harborrag_app/workflow_control/memory/__init__.py +6 -0
- harborrag_app/workflow_control/memory/composition.py +56 -0
- harborrag_app/workflow_control/memory/sessions.py +59 -0
- harborrag_app/workflow_control/ports.py +269 -0
- harborrag_app/workflow_control/retrieval/__init__.py +1 -0
- harborrag_app/workflow_control/retrieval/client.py +85 -0
- harborrag_app/workflow_control/retrieval/graph.py +125 -0
- harborrag_app/workflow_control/retrieval/presentation.py +49 -0
- harborrag_app/workflow_control/retrieval/query.py +63 -0
- harborrag_app/workflow_control/schemas.py +13 -0
- harborrag_app-2.0.0a1.dist-info/METADATA +134 -0
- harborrag_app-2.0.0a1.dist-info/RECORD +123 -0
- harborrag_app-2.0.0a1.dist-info/WHEEL +4 -0
- harborrag_app-2.0.0a1.dist-info/entry_points.txt +2 -0
- harborrag_app-2.0.0a1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
from harborrag_app.workflow_control import AppResponse, BaseAppService
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("harborrag-app")
|
|
7
|
+
except PackageNotFoundError: # pragma: no cover - source checkout without installation
|
|
8
|
+
__version__ = "0+unknown"
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AppResponse",
|
|
12
|
+
"BaseAppService",
|
|
13
|
+
"__version__",
|
|
14
|
+
]
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# Control Plane API
|
|
2
|
+
|
|
3
|
+
The `harborrag_app.api` package is HarborRAG's FastAPI boundary. It validates
|
|
4
|
+
HTTP input, authenticates callers, delegates workflow operations to the
|
|
5
|
+
transport-neutral application service, and maps results onto stable HTTP
|
|
6
|
+
responses. Connector execution, parsing, chunking, indexing, and Temporal
|
|
7
|
+
workflow rules remain outside this package.
|
|
8
|
+
|
|
9
|
+
Operational health and documentation routes remain under `/api/v1`. Stable
|
|
10
|
+
public ingestion, retrieval, chat, and agent resources are mounted under `/v1`.
|
|
11
|
+
|
|
12
|
+
For browser convenience, `/` and `/docs` redirect to `/api/v1/docs` while
|
|
13
|
+
documentation is enabled. When documentation is disabled, `/docs` remains
|
|
14
|
+
closed and `/` redirects to `/api/v1/health`.
|
|
15
|
+
|
|
16
|
+
## Install and run
|
|
17
|
+
|
|
18
|
+
Install the API dependencies from a source checkout:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv sync --package harborrag-app --extra api
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Start the application factory directly:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv run uvicorn harborrag_app.api.app:create_fastapi_app \
|
|
28
|
+
--factory \
|
|
29
|
+
--host 127.0.0.1 \
|
|
30
|
+
--port 8000
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
For the supported local Docker topology, use:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
scripts/deployment/dev.sh up
|
|
37
|
+
curl --fail http://127.0.0.1:8000/api/v1/health
|
|
38
|
+
curl --fail -H "Authorization: Bearer $ADMIN_TOKEN" \
|
|
39
|
+
http://127.0.0.1:8000/api/v1/metrics
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The development helper starts the data services, Temporal, an ingestion
|
|
43
|
+
worker, and the API, then waits for its process health check. If those dependencies are
|
|
44
|
+
already running, `scripts/deployment/dev.sh api` starts only the API. API process
|
|
45
|
+
and authentication values belong in the ignored `env/.env.api`; embedding
|
|
46
|
+
credentials come from `env/.env.models` for synchronous vector retrieval.
|
|
47
|
+
Connector credentials remain isolated in the worker. See the repository
|
|
48
|
+
[deployment guide](../../../../../docs/developers/deployment/README.md) for
|
|
49
|
+
environment preparation and shutdown.
|
|
50
|
+
|
|
51
|
+
The local API container listens on `0.0.0.0` inside Docker but Compose publishes
|
|
52
|
+
port 8000 only on host loopback. Accordingly, the development environment file
|
|
53
|
+
sets `HARBORRAG_ALLOW_INSECURE_DEV=true` alongside `HARBORRAG_AUTH_MODE=none`.
|
|
54
|
+
Do not publish that configuration on a non-loopback host address; use HMAC auth
|
|
55
|
+
before exposing the API outside the trusted local development network.
|
|
56
|
+
|
|
57
|
+
## Composition modes
|
|
58
|
+
|
|
59
|
+
The application lifespan constructs one `AppService` and closes it on process
|
|
60
|
+
shutdown.
|
|
61
|
+
|
|
62
|
+
| Condition | Composition |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| `HARBORRAG_ENV=dev` without `HARBORRAG_CONTROL_DB_URL` | Development control-database stub plus the real Temporal client |
|
|
65
|
+
| A control-database URL is set | Migrated runtime control-plane repositories plus the real Temporal client |
|
|
66
|
+
| `HARBORRAG_ENV=prod` | Requires an explicit control-database URL and authenticated API mode |
|
|
67
|
+
|
|
68
|
+
Development mode changes only control-plane repository composition. Ingestion
|
|
69
|
+
requests still go to the configured Temporal service.
|
|
70
|
+
|
|
71
|
+
## HTTP routes
|
|
72
|
+
|
|
73
|
+
| Method | Path | Minimum role | Behavior |
|
|
74
|
+
| --- | --- | --- | --- |
|
|
75
|
+
The API serves two prefixes, and the distinction matters:
|
|
76
|
+
|
|
77
|
+
- **`/v1/...` is the public contract** - ingestion, retrieval, chat, agent, admin. This is
|
|
78
|
+
what applications integrate against.
|
|
79
|
+
- **`/api/v1/...` is the operational surface** - health, readiness, metrics, and the
|
|
80
|
+
console-facing project/source/activity/settings routes. Treat it as deployment
|
|
81
|
+
plumbing, not an application API.
|
|
82
|
+
|
|
83
|
+
`/api/v1/docs` and `/api/v1/openapi.json` are served when `HARBORRAG_DOCS_ENABLED` is set;
|
|
84
|
+
`GET /` and `GET /docs` redirect there (or to `/api/v1/health` when docs are disabled).
|
|
85
|
+
The live schema is always authoritative - prefer it to this table.
|
|
86
|
+
|
|
87
|
+
### Public contract (`/v1`)
|
|
88
|
+
|
|
89
|
+
| Method | Path | Minimum role | Behavior |
|
|
90
|
+
| --- | --- | --- | --- |
|
|
91
|
+
| `POST` | `/v1/ingestions` | `editor` | Submit a durable ingestion task |
|
|
92
|
+
| `GET` | `/v1/ingestions` | `reader` | List tasks newest first, cursor-paginated |
|
|
93
|
+
| `GET` | `/v1/ingestions/{task_id}` | `reader` | Read Postgres-authoritative task progress |
|
|
94
|
+
| `GET` | `/v1/ingestions/{task_id}/documents` | `reader` | Read cursor-paginated document outcomes |
|
|
95
|
+
| `GET` | `/v1/ingestions/{task_id}/stream` | `reader` | Stream task progress as Server-Sent Events |
|
|
96
|
+
| `POST` | `/v1/ingestions/{task_id}/cancel` | `editor` | Request graceful cancellation |
|
|
97
|
+
| `POST` | `/v1/ingestions/{task_id}/retry-failures` | `editor` | Retry selected or all retryable failures |
|
|
98
|
+
| `GET` | `/v1/connections` | `reader` | List enabled connections submittable by `connection_id` |
|
|
99
|
+
| `POST` | `/v1/chat/sessions` | `reader` | Create a persisted chat session and greeting (`201`) |
|
|
100
|
+
| `POST` | `/v1/chat/completions` | `reader` | Retrieval-grounded JSON or SSE chat completion |
|
|
101
|
+
| `POST` | `/v1/agent/sessions` | `reader` | Create a persisted agent session and greeting (`201`) |
|
|
102
|
+
| `POST` | `/v1/agent/completions` | `reader` | Bounded multi-turn model/tool completion |
|
|
103
|
+
| `POST` | `/v1/agent/runs/{run_id}/resume` | `reader` | Continue a run that stopped before finishing |
|
|
104
|
+
| `POST` | `/v1/retrieval/vector` | `reader` | Dense, sparse, or hybrid vector search |
|
|
105
|
+
| `POST` | `/v1/retrieval/graph/triplets` | `reader` | Match subject-predicate-object records |
|
|
106
|
+
| `POST` | `/v1/retrieval/graph/paths` | `reader` | Find bounded paths between graph nodes |
|
|
107
|
+
| `POST` | `/v1/retrieval/graph/subgraphs` | `reader` | Expand a bounded graph neighborhood |
|
|
108
|
+
| `GET` | `/v1/admin/projections/{tenant}` | `admin` | Inspect projection state for a tenant |
|
|
109
|
+
| `DELETE` | `/v1/admin/projections/{tenant}` | `admin` | Drop a tenant's rebuildable projections |
|
|
110
|
+
|
|
111
|
+
### Operational surface (`/api/v1`)
|
|
112
|
+
|
|
113
|
+
| Method | Path | Minimum role | Behavior |
|
|
114
|
+
| --- | --- | --- | --- |
|
|
115
|
+
| `GET` | `/api/v1/health` | Public | Process liveness; does not probe dependencies |
|
|
116
|
+
| `GET` | `/api/v1/readyz` | Public | Dependency-aware readiness; `503` when runtime composition is unavailable |
|
|
117
|
+
| `GET` | `/api/v1/metrics` | `admin` | Prometheus API, Python runtime, and process metrics |
|
|
118
|
+
| `GET` | `/api/v1/metrics/ingestion` | `reader` | Ingestion-specific metrics |
|
|
119
|
+
| `GET` | `/api/v1/projects` | `reader` | List projects |
|
|
120
|
+
| `GET` | `/api/v1/projects/{project_id}` | `reader` | Read one project |
|
|
121
|
+
| `GET` | `/api/v1/sources` | `reader` | List configured sources |
|
|
122
|
+
| `POST` | `/api/v1/sources` | `editor` | Create a source |
|
|
123
|
+
| `GET` | `/api/v1/sources/{source_id}` | `reader` | Read one source |
|
|
124
|
+
| `PATCH` | `/api/v1/sources/{source_id}` | `editor` | Update a source |
|
|
125
|
+
| `DELETE` | `/api/v1/sources/{source_id}` | `editor` | Delete a source |
|
|
126
|
+
| `GET` | `/api/v1/activity` | `reader` | Recent activity feed |
|
|
127
|
+
| `GET` | `/api/v1/settings` | `reader` | Effective non-secret settings |
|
|
128
|
+
|
|
129
|
+
### Deprecated
|
|
130
|
+
|
|
131
|
+
These predate the `/v1` contract, still appear in the generated schema, and will be
|
|
132
|
+
removed. Use the `/v1/ingestions*` routes instead.
|
|
133
|
+
|
|
134
|
+
| Method | Path | Minimum role |
|
|
135
|
+
| --- | --- | --- |
|
|
136
|
+
| `GET` | `/api/v1/diagnostics` | `admin` |
|
|
137
|
+
| `POST` | `/api/v1/ingestions` | `editor` |
|
|
138
|
+
| `GET` | `/api/v1/ingestions/{run_id}` | `reader` |
|
|
139
|
+
| `GET` | `/api/v1/ingestions/{run_id}/result` | `reader` |
|
|
140
|
+
| `POST` | `/api/v1/ingestions/{run_id}/actions` | `editor` |
|
|
141
|
+
|
|
142
|
+
The API authenticates the principal and accepts tenant as one explicit,
|
|
143
|
+
top-level request field. It defaults to `DEFAULT`; callers cannot bypass tenant
|
|
144
|
+
isolation through retrieval filters.
|
|
145
|
+
|
|
146
|
+
### Generate a chat completion
|
|
147
|
+
|
|
148
|
+
Chat uses the `chat` section of `config/models.yaml`, resolves credentials from
|
|
149
|
+
`env/.env.models`, and retrieves tenant-scoped evidence before calling the
|
|
150
|
+
model. Callers may choose per-request graph observation and streaming, but
|
|
151
|
+
cannot submit a system prompt, provider
|
|
152
|
+
credentials, model overrides, custom tools, or adapter-specific parameters.
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
curl --fail-with-body \
|
|
156
|
+
--request POST \
|
|
157
|
+
--header 'Content-Type: application/json' \
|
|
158
|
+
--data '{"tenant":"DEFAULT"}' \
|
|
159
|
+
http://127.0.0.1:8000/v1/chat/sessions
|
|
160
|
+
|
|
161
|
+
curl --fail-with-body --request POST \
|
|
162
|
+
--header 'Content-Type: application/json' \
|
|
163
|
+
--data '{"session_id":"session-...","prompt":"What is HarborRAG?"}' \
|
|
164
|
+
http://127.0.0.1:8000/v1/chat/completions
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The session response contains a generated `session_id` and greeting. The completion POST
|
|
168
|
+
requires that session ID and a prompt in its JSON body. `stream=true` changes the response to
|
|
169
|
+
SSE. The two latest PostgreSQL-backed turns are recalled under the tenant,
|
|
170
|
+
authenticated principal, and session identity. Requests are marked sensitive
|
|
171
|
+
so model-response caching remains disabled. Completion endpoints accept POST
|
|
172
|
+
only, keeping prompts and other sensitive content out of request URLs and access logs.
|
|
173
|
+
|
|
174
|
+
### Run a multi-turn agent
|
|
175
|
+
|
|
176
|
+
The agent uses the same model and memory identity but can execute multiple
|
|
177
|
+
model/tool hops before synthesizing an answer. Only runtime-owned read tools
|
|
178
|
+
are exposed. `graph_search` controls whether graph triplet, path, and subgraph
|
|
179
|
+
tools are available, and `max_steps` bounds model/tool rounds from 1 to 8.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
curl --fail-with-body \
|
|
183
|
+
--request POST \
|
|
184
|
+
--header 'Content-Type: application/json' \
|
|
185
|
+
--data '{"tenant":"DEFAULT"}' \
|
|
186
|
+
http://127.0.0.1:8000/v1/agent/sessions
|
|
187
|
+
|
|
188
|
+
curl --fail-with-body --request POST \
|
|
189
|
+
--header 'Content-Type: application/json' \
|
|
190
|
+
--data '{"session_id":"session-...","prompt":"Connect this release policy to its owner.","graph_search":true,"max_steps":4}' \
|
|
191
|
+
http://127.0.0.1:8000/v1/agent/completions
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The POST returns a `session_id` and greeting. Agent completions return
|
|
195
|
+
aggregate token usage, turn count, and a safe tool trace containing tool names
|
|
196
|
+
and success status. Raw tool arguments and results are not returned.
|
|
197
|
+
|
|
198
|
+
The former `POST /v1/retrieval/search` route has been removed. Retrieval
|
|
199
|
+
operations are intentionally not exposed as GET: query text and nested filters
|
|
200
|
+
belong in a validated request body and should not be copied into URLs, access
|
|
201
|
+
logs, browser history, or intermediary cache keys. A future GET endpoint should
|
|
202
|
+
represent a persisted retrieval resource; searches are not persisted today.
|
|
203
|
+
|
|
204
|
+
### Retrieve evidence
|
|
205
|
+
|
|
206
|
+
Vector search has one contract for both simple and advanced callers. Omitting
|
|
207
|
+
controls uses hybrid retrieval; advanced callers may select `dense`, `sparse`,
|
|
208
|
+
or `hybrid`, apply metadata filters, observe graph context, set a score
|
|
209
|
+
threshold, and control content or metadata projection.
|
|
210
|
+
|
|
211
|
+
`filters` is optional. Omit it for an unfiltered search; when supplied, use
|
|
212
|
+
real indexed metadata keys such as `{"category": "architecture"}`. The
|
|
213
|
+
`additionalProp1` names shown by generic JSON-object schema renderers are
|
|
214
|
+
placeholders, not fields recognized by HarborRAG.
|
|
215
|
+
|
|
216
|
+
This is a synchronous `POST`: a successful response contains at most `top_k`
|
|
217
|
+
ranked results immediately. It never returns an ingestion-style `task_id` and
|
|
218
|
+
does not require a follow-up GET. The optional `request_id` in the response is
|
|
219
|
+
only a trace/correlation identifier. A retrieval GET should be added only if
|
|
220
|
+
search results become persisted resources.
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
curl --fail-with-body \
|
|
224
|
+
--request POST \
|
|
225
|
+
--header 'Content-Type: application/json' \
|
|
226
|
+
--data '{
|
|
227
|
+
"query": "publication policy",
|
|
228
|
+
"tenant": "DEFAULT",
|
|
229
|
+
"top_k": 10,
|
|
230
|
+
"lane": "hybrid",
|
|
231
|
+
"filters": {"category": "architecture"},
|
|
232
|
+
"observe_graph": true,
|
|
233
|
+
"score_threshold": 0.2,
|
|
234
|
+
"include_content": true,
|
|
235
|
+
"include_metadata": true
|
|
236
|
+
}' \
|
|
237
|
+
http://127.0.0.1:8000/v1/retrieval/vector
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Graph operations use portable graph records rather than FalkorDB query syntax.
|
|
241
|
+
Triplets accept any combination containing at least one of `subject`,
|
|
242
|
+
`predicate`, or `object`; paths require distinct start and end nodes; subgraph
|
|
243
|
+
expansion requires one start node. Path depth is capped at 8 and all graph
|
|
244
|
+
result counts are capped at 100.
|
|
245
|
+
|
|
246
|
+
### Submit ingestion
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
curl --fail-with-body \
|
|
250
|
+
--request POST \
|
|
251
|
+
--header 'Content-Type: application/json' \
|
|
252
|
+
--header 'Idempotency-Key: local-release-notes-2026-08' \
|
|
253
|
+
--data '{
|
|
254
|
+
"connection_id": "harborrag-workspace",
|
|
255
|
+
"tenant": "DEFAULT",
|
|
256
|
+
"mode": "incremental"
|
|
257
|
+
}' \
|
|
258
|
+
http://127.0.0.1:8000/v1/ingestions
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The application generates a public UUIDv4 task ID. Before Temporal is
|
|
262
|
+
called, it persists the source scope and a `PENDING` task in Postgres. Reusing
|
|
263
|
+
an idempotency key with the same public request returns the existing task;
|
|
264
|
+
reusing it for a different request returns a conflict.
|
|
265
|
+
|
|
266
|
+
Task paths intentionally continue to accept opaque strings, so tasks created
|
|
267
|
+
by older versions with `ing_...` or `retry_...` identifiers remain queryable.
|
|
268
|
+
|
|
269
|
+
The connection must be enabled in the same connector configuration used by the
|
|
270
|
+
worker. Provider, scope, paths, attachment/comment policy, limits, and
|
|
271
|
+
credentials come from `config/connectors.yaml`; the API does not duplicate or
|
|
272
|
+
override them. The API resolves only connector identity and processing
|
|
273
|
+
fingerprints; connector calls and model execution remain worker activities.
|
|
274
|
+
|
|
275
|
+
`mode` defaults to `incremental`. It discovers the configured scope and skips
|
|
276
|
+
unchanged documents. `force` fetches and evaluates unchanged documents again,
|
|
277
|
+
but it does not delete data, force a new deterministic document version, or
|
|
278
|
+
rebuild an already-active projection merely because its storage collection was
|
|
279
|
+
renamed. See [Ingestion modes](../../../../../docs/users/ingestion-modes.md) for
|
|
280
|
+
the complete behavior and reindex guidance.
|
|
281
|
+
|
|
282
|
+
### List connections and tasks
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
curl --fail-with-body http://127.0.0.1:8000/v1/connections
|
|
286
|
+
|
|
287
|
+
curl --fail-with-body 'http://127.0.0.1:8000/v1/ingestions?status=RUNNING&limit=25'
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`/v1/connections` returns the `connection_id` and `source_type` of every
|
|
291
|
+
connection enabled in `config/connectors.yaml`, which is the only place those
|
|
292
|
+
names are defined. Nothing else about a connector is exposed: settings,
|
|
293
|
+
environment references, and credential references stay server-side. A
|
|
294
|
+
definition the catalog could not parse is omitted rather than failing the
|
|
295
|
+
listing; submitting it still reports its own configuration error.
|
|
296
|
+
|
|
297
|
+
`/v1/ingestions` lists tasks newest submission first and pages with an opaque
|
|
298
|
+
`cursor`, exactly like `/v1/ingestions/{task_id}/documents`. Each item carries
|
|
299
|
+
the same fields as the single-task response. `tenant` narrows the listing to
|
|
300
|
+
one tenant the caller may read; omitting it reads every tenant in the caller's
|
|
301
|
+
own scope, so a tenant-scoped token never sees another tenant's tasks and an
|
|
302
|
+
out-of-scope `tenant` is rejected rather than answered with an empty page.
|
|
303
|
+
|
|
304
|
+
### Inspect and control a task
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
curl --fail-with-body \
|
|
308
|
+
http://127.0.0.1:8000/v1/ingestions/2f47e0c9-398b-4b87-ae72-c6778f08a18a
|
|
309
|
+
|
|
310
|
+
curl --fail-with-body \
|
|
311
|
+
'http://127.0.0.1:8000/v1/ingestions/2f47e0c9-398b-4b87-ae72-c6778f08a18a/documents?limit=50'
|
|
312
|
+
|
|
313
|
+
curl --fail-with-body \
|
|
314
|
+
--request POST \
|
|
315
|
+
http://127.0.0.1:8000/v1/ingestions/2f47e0c9-398b-4b87-ae72-c6778f08a18a/cancel
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Cancellation is asynchronous and takes effect at safe workflow boundaries.
|
|
319
|
+
Published document versions remain active. The retry endpoint creates a new
|
|
320
|
+
UUIDv4 task and starts at the earliest reusable durable artifact; it does
|
|
321
|
+
not refetch a source when a raw or later artifact exists.
|
|
322
|
+
|
|
323
|
+
## Authentication and roles
|
|
324
|
+
|
|
325
|
+
`HARBORRAG_AUTH_MODE` supports:
|
|
326
|
+
|
|
327
|
+
- `none`: local development only; assigns an implicit `owner` principal;
|
|
328
|
+
- `hmac`: verifies HS256 bearer JWTs using `HARBORRAG_AUTH_SECRET`;
|
|
329
|
+
- `oidc`: reserved but not implemented; application creation fails fast.
|
|
330
|
+
|
|
331
|
+
Production refuses `auth_mode=none`. HMAC secrets must contain at least 32
|
|
332
|
+
UTF-8 bytes. Tokens must contain `sub`, `role`, `tenants`, `iat`, `exp`, `iss`,
|
|
333
|
+
and `aud` claims. `tenants` is a non-empty list of tenant identifiers the
|
|
334
|
+
principal may access; a global role does not grant access to other tenants.
|
|
335
|
+
The default issuer is `harborrag`, the default audience is `harborrag-api`, and
|
|
336
|
+
the role order is:
|
|
337
|
+
|
|
338
|
+
```text
|
|
339
|
+
reader < editor < admin < owner
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Send authenticated requests with:
|
|
343
|
+
|
|
344
|
+
```http
|
|
345
|
+
Authorization: Bearer JWT
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The liveness endpoint intentionally remains unauthenticated. Prometheus metrics
|
|
349
|
+
require an admin bearer token at `/api/v1/metrics`; configure a protected scrape
|
|
350
|
+
credential or a separate operational listener when the API is
|
|
351
|
+
not running on a trusted service network.
|
|
352
|
+
|
|
353
|
+
## Configuration
|
|
354
|
+
|
|
355
|
+
API process settings use the `HARBORRAG_` prefix.
|
|
356
|
+
|
|
357
|
+
| Variable | Default | Purpose |
|
|
358
|
+
| --- | --- | --- |
|
|
359
|
+
| `HARBORRAG_HOST` | `127.0.0.1` | Uvicorn bind host; containers override this to `0.0.0.0`, where disabled auth requires an explicit insecure-development opt-in |
|
|
360
|
+
| `HARBORRAG_PORT` | `8000` | Uvicorn bind port used by the container command |
|
|
361
|
+
| `HARBORRAG_ENV` | `dev` | `dev` or `prod` process policy |
|
|
362
|
+
| `HARBORRAG_AUTH_MODE` | `none` | `none`, `hmac`, or `oidc` |
|
|
363
|
+
| `HARBORRAG_ALLOW_INSECURE_DEV` | `false` | Explicitly acknowledge disabled auth on a non-loopback development listener |
|
|
364
|
+
| `HARBORRAG_AUTH_SECRET` | unset | Shared HS256 secret |
|
|
365
|
+
| `HARBORRAG_AUTH_ISSUER` | `harborrag` | Required JWT issuer |
|
|
366
|
+
| `HARBORRAG_AUTH_AUDIENCE` | `harborrag-api` | Required JWT audience |
|
|
367
|
+
| `HARBORRAG_CORS_ORIGINS` | `[]` | JSON list of allowed browser origins |
|
|
368
|
+
| `HARBORRAG_DOCS_ENABLED` | `true` in dev | Enables Swagger and the live OpenAPI route |
|
|
369
|
+
| `HARBORRAG_MAX_REQUEST_BODY_BYTES` | `1048576` | Maximum request body accepted before JSON parsing |
|
|
370
|
+
| `HARBORRAG_API_CAPACITY_REDIS_URL` | unset in dev; required in prod | Redis backend for cross-replica principal limits |
|
|
371
|
+
| `HARBORRAG_API_CAPACITY_ALLOW_INSECURE_REMOTE` | `false` | Development-only acknowledgement for remote plaintext Redis |
|
|
372
|
+
| `HARBORRAG_API_REQUESTS_PER_MINUTE` | `60` | Maximum expensive requests per principal per minute |
|
|
373
|
+
| `HARBORRAG_API_MAX_INFLIGHT_PER_PRINCIPAL` | `4` | Concurrent expensive request limit per principal |
|
|
374
|
+
| `HARBORRAG_API_REQUEST_TIMEOUT_SECONDS` | `120` | Server-owned wall-clock deadline for expensive requests |
|
|
375
|
+
| `HARBORRAG_CONTROL_DB_URL` | local SQLite runtime default | Control-plane SQLAlchemy URL |
|
|
376
|
+
| `HARBORRAG_INGESTION_TENANT_ID` | `DEFAULT` | Fallback tenant for non-HTTP runtime operations |
|
|
377
|
+
| `HARBORRAG_TEMPORAL_CONFIG_PATH` | `config/temporal.yaml` | Temporal connection, queues, retries, worker, ingestion, health, and workflow configuration |
|
|
378
|
+
| `HARBORRAG_TEMPORAL_TARGET` | value from YAML | Optional Temporal frontend override |
|
|
379
|
+
| `HARBORRAG_TEMPORAL_NAMESPACE` | value from YAML | Optional Temporal namespace override |
|
|
380
|
+
| `HARBORRAG_TEMPORAL_TLS` | value from YAML | Optional TLS override for a managed Temporal endpoint |
|
|
381
|
+
| `HARBORRAG_TEMPORAL_API_KEY` | unset | Secret API credential; keep it outside tracked YAML |
|
|
382
|
+
| `HARBORRAG_TEMPORAL_ALLOW_INSECURE_REMOTE` | `false` | Explicit opt-in for trusted plaintext remote targets |
|
|
383
|
+
| `HARBORRAG_TEMPORAL_INGESTION_BATCH_SIZE` | `200` | Default documents per `SourceBatchWorkflow` child; a CLI/API caller may override it per submission |
|
|
384
|
+
| `HARBORRAG_TEMPORAL_INGESTION_DOCUMENT_CONCURRENCY` | `8` | Default concurrent `DocumentIngestionWorkflow` children per wave; a CLI/API caller may override it per submission |
|
|
385
|
+
| `HARBORRAG_LOG_LEVEL` | `INFO` | HarborRAG namespace log level |
|
|
386
|
+
|
|
387
|
+
Credentialed CORS rejects wildcard origins. Swagger and the live OpenAPI route
|
|
388
|
+
default to disabled in production unless an operator explicitly enables them.
|
|
389
|
+
|
|
390
|
+
INFO logs report ingestion submission, discovery, document outcomes, failures,
|
|
391
|
+
and finalization. DEBUG additionally reports every Temporal activity boundary
|
|
392
|
+
and duration. Records include logger namespace, Python module, function, and
|
|
393
|
+
source line while omitting credentials and document/model content. See the
|
|
394
|
+
[troubleshooting guide](../../../../../docs/users/troubleshooting/README.md#an-accepted-ingestion-appears-to-do-nothing)
|
|
395
|
+
when an accepted task appears idle.
|
|
396
|
+
|
|
397
|
+
## Errors and request tracing
|
|
398
|
+
|
|
399
|
+
Every non-success response uses one error envelope:
|
|
400
|
+
|
|
401
|
+
```json
|
|
402
|
+
{
|
|
403
|
+
"error": {
|
|
404
|
+
"code": "harbor_validation_error",
|
|
405
|
+
"message": "Request validation failed",
|
|
406
|
+
"details": {},
|
|
407
|
+
"trace_id": "request-123"
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Send `X-Request-Id` to correlate a request with logs. The API echoes that value
|
|
413
|
+
on the response; when it is absent, middleware generates one. Unexpected
|
|
414
|
+
exceptions return a generic error while the complete traceback remains in
|
|
415
|
+
process logs.
|
|
416
|
+
|
|
417
|
+
## OpenAPI contract
|
|
418
|
+
|
|
419
|
+
In development:
|
|
420
|
+
|
|
421
|
+
```text
|
|
422
|
+
http://127.0.0.1:8000/api/v1/docs
|
|
423
|
+
http://127.0.0.1:8000/api/v1/openapi.json
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Export the deterministic contract without starting a server:
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
uv run python -m harborrag_app.api.export_openapi > openapi.json
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
## Package boundary
|
|
433
|
+
|
|
434
|
+
The package is organized by responsibility:
|
|
435
|
+
|
|
436
|
+
```text
|
|
437
|
+
api/
|
|
438
|
+
├── app.py, router.py, settings.py, middleware.py, metrics.py
|
|
439
|
+
├── auth/ # authentication and role dependencies
|
|
440
|
+
├── routes/ # operational surface: health, metrics, projects,
|
|
441
|
+
│ # sources, activity, settings, deprecated routes
|
|
442
|
+
├── schemas.py # contracts shared by API features
|
|
443
|
+
└── v1/ # the public /v1 contract
|
|
444
|
+
├── admin/ # projection inspection and teardown
|
|
445
|
+
├── agent/ # agent sessions, completions, run resume
|
|
446
|
+
├── chat/ # chat sessions and completions
|
|
447
|
+
├── connections/ # submittable connection listing
|
|
448
|
+
├── ingestion/ # ingestion routes, schemas, and command mapping
|
|
449
|
+
└── retrieval/ # retrieval routes, schemas, and service dependency
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
This package may:
|
|
453
|
+
|
|
454
|
+
- validate and serialize HTTP messages;
|
|
455
|
+
- authenticate principals and enforce route roles;
|
|
456
|
+
- manage API process lifecycle and middleware;
|
|
457
|
+
- delegate operations through `BaseAppService`.
|
|
458
|
+
|
|
459
|
+
It must not:
|
|
460
|
+
|
|
461
|
+
- instantiate connectors, parsers, model clients, or repositories;
|
|
462
|
+
- implement ingestion state transitions;
|
|
463
|
+
- expose Temporal SDK handles on the wire;
|
|
464
|
+
- return provider exception details or secrets.
|
|
465
|
+
|
|
466
|
+
Run its tests from the repository root:
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
pytest packages/harborrag-app/tests/test_api_*.py
|
|
470
|
+
```
|
|
File without changes
|
harborrag_app/api/app.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""FastAPI application factory for the HarborRAG Control Plane API (ST2).
|
|
2
|
+
|
|
3
|
+
`uvicorn harborrag_app.api.app:create_fastapi_app --factory` is the deploy
|
|
4
|
+
entrypoint (Dockerfile.api CMD). The factory wires settings, observability,
|
|
5
|
+
trace middleware, CORS, the error envelope, auth verifier, and versioned public
|
|
6
|
+
routers; the lifespan builds the app service through runtime composition.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import inspect
|
|
13
|
+
import logging
|
|
14
|
+
from collections.abc import AsyncIterator
|
|
15
|
+
from contextlib import asynccontextmanager
|
|
16
|
+
|
|
17
|
+
from fastapi import FastAPI, Request
|
|
18
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
19
|
+
from fastapi.responses import RedirectResponse
|
|
20
|
+
from starlette.routing import compile_path
|
|
21
|
+
|
|
22
|
+
from harborrag_app import __version__
|
|
23
|
+
from harborrag_app.api.auth.dependencies import build_token_verifier
|
|
24
|
+
from harborrag_app.api.capacity import build_api_capacity_limiter
|
|
25
|
+
from harborrag_app.api.errors import register_error_handlers
|
|
26
|
+
from harborrag_app.api.metrics import ApiMetrics, ApiMetricsMiddleware
|
|
27
|
+
from harborrag_app.api.middleware import RequestBodyLimitMiddleware, TraceIdMiddleware
|
|
28
|
+
from harborrag_app.api.router import OPERATIONAL_PREFIX, register_routes
|
|
29
|
+
from harborrag_app.api.settings import ApiSettings
|
|
30
|
+
from harborrag_app.workflow_control.composition.selection import select_app_service
|
|
31
|
+
from harborrag_core.contracts.errors import HarborConfigurationError
|
|
32
|
+
from harborrag_core.observability.process_logging import configure_logging
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger("harborrag.app.api.app")
|
|
35
|
+
_SUBMISSION_RECOVERY_INTERVAL_SECONDS = 5.0
|
|
36
|
+
_INGESTION_PROGRESS_INTERVAL_SECONDS = 2.0
|
|
37
|
+
_CONTROL_PLANE_EFFECT_RECOVERY_INTERVAL_SECONDS = 30.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _redirect_root(request: Request) -> RedirectResponse:
|
|
41
|
+
"""Send humans to docs when enabled, otherwise to the health endpoint."""
|
|
42
|
+
settings: ApiSettings = request.app.state.settings
|
|
43
|
+
destination = "docs" if settings.docs_enabled else "health"
|
|
44
|
+
return RedirectResponse(url=f"{OPERATIONAL_PREFIX}/{destination}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _redirect_docs() -> RedirectResponse:
|
|
48
|
+
"""Preserve the familiar Swagger path without duplicating the API route."""
|
|
49
|
+
return RedirectResponse(url=f"{OPERATIONAL_PREFIX}/docs")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def _recover_pending_submissions(app: FastAPI) -> None:
|
|
53
|
+
"""Continuously drain the durable workflow-submission outbox."""
|
|
54
|
+
|
|
55
|
+
while True:
|
|
56
|
+
try:
|
|
57
|
+
recovered = await app.state.app_service.recover_pending_submissions()
|
|
58
|
+
if recovered:
|
|
59
|
+
logger.info("Recovered %d pending ingestion submissions", recovered)
|
|
60
|
+
except asyncio.CancelledError:
|
|
61
|
+
raise
|
|
62
|
+
except Exception:
|
|
63
|
+
logger.exception("Pending ingestion submission recovery pass failed")
|
|
64
|
+
await asyncio.sleep(_SUBMISSION_RECOVERY_INTERVAL_SECONDS)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def _sync_ingestion_progress(app: FastAPI) -> None:
|
|
68
|
+
"""Continuously fan active ingestion tasks' progress out via the event bus."""
|
|
69
|
+
|
|
70
|
+
while True:
|
|
71
|
+
try:
|
|
72
|
+
await app.state.app_service.sync_ingestion_progress()
|
|
73
|
+
except asyncio.CancelledError:
|
|
74
|
+
raise
|
|
75
|
+
except Exception:
|
|
76
|
+
logger.exception("Ingestion progress sync tick failed")
|
|
77
|
+
await asyncio.sleep(_INGESTION_PROGRESS_INTERVAL_SECONDS)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def _recover_pending_control_plane_effects(app: FastAPI) -> None:
|
|
81
|
+
"""Continuously drain the durable secret-retirement/audit-logging outbox."""
|
|
82
|
+
|
|
83
|
+
while True:
|
|
84
|
+
try:
|
|
85
|
+
recovered = await app.state.app_service.recover_pending_control_plane_effects()
|
|
86
|
+
if recovered:
|
|
87
|
+
logger.info("Recovered %d pending control-plane effects", recovered)
|
|
88
|
+
except asyncio.CancelledError:
|
|
89
|
+
raise
|
|
90
|
+
except Exception:
|
|
91
|
+
logger.exception("Pending control-plane effect recovery pass failed")
|
|
92
|
+
await asyncio.sleep(_CONTROL_PLANE_EFFECT_RECOVERY_INTERVAL_SECONDS)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@asynccontextmanager
|
|
96
|
+
async def _lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
97
|
+
"""Compose the app service on startup (ST8 selection rule).
|
|
98
|
+
|
|
99
|
+
Runs in a worker thread: production composition executes Alembic
|
|
100
|
+
migrations and a DB probe, which drive their own event loops.
|
|
101
|
+
"""
|
|
102
|
+
service, mode = await asyncio.to_thread(select_app_service)
|
|
103
|
+
app.state.app_service = service
|
|
104
|
+
app.state.composition_mode = mode
|
|
105
|
+
logger.info("Application service composed in %s mode", mode)
|
|
106
|
+
recovery_task = asyncio.create_task(_recover_pending_submissions(app))
|
|
107
|
+
progress_task = asyncio.create_task(_sync_ingestion_progress(app))
|
|
108
|
+
control_plane_effect_recovery_task = asyncio.create_task(
|
|
109
|
+
_recover_pending_control_plane_effects(app)
|
|
110
|
+
)
|
|
111
|
+
try:
|
|
112
|
+
yield
|
|
113
|
+
finally:
|
|
114
|
+
recovery_task.cancel()
|
|
115
|
+
progress_task.cancel()
|
|
116
|
+
control_plane_effect_recovery_task.cancel()
|
|
117
|
+
await asyncio.gather(
|
|
118
|
+
recovery_task,
|
|
119
|
+
progress_task,
|
|
120
|
+
control_plane_effect_recovery_task,
|
|
121
|
+
return_exceptions=True,
|
|
122
|
+
)
|
|
123
|
+
logger.info("Closing the application service")
|
|
124
|
+
try:
|
|
125
|
+
close = getattr(service, "aclose", None)
|
|
126
|
+
if close is not None:
|
|
127
|
+
result = close()
|
|
128
|
+
if inspect.isawaitable(result):
|
|
129
|
+
await result
|
|
130
|
+
finally:
|
|
131
|
+
await app.state.api_capacity_limiter.aclose()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def create_fastapi_app(settings: ApiSettings | None = None) -> FastAPI:
|
|
135
|
+
"""Build the Control Plane API app from (env-derived) settings."""
|
|
136
|
+
settings = settings or ApiSettings()
|
|
137
|
+
# uvicorn configures the root logger, not the "harborrag" namespace; this
|
|
138
|
+
# attaches the namespace handler so runtime and route logs are emitted.
|
|
139
|
+
configure_logging()
|
|
140
|
+
app = FastAPI(
|
|
141
|
+
title="HarborRAG Control Plane API",
|
|
142
|
+
version=__version__,
|
|
143
|
+
openapi_url=(f"{OPERATIONAL_PREFIX}/openapi.json" if settings.docs_enabled else None),
|
|
144
|
+
docs_url=f"{OPERATIONAL_PREFIX}/docs" if settings.docs_enabled else None,
|
|
145
|
+
redoc_url=None,
|
|
146
|
+
lifespan=_lifespan,
|
|
147
|
+
)
|
|
148
|
+
app.state.settings = settings
|
|
149
|
+
app.state.token_verifier = build_token_verifier(settings)
|
|
150
|
+
redis_url = (
|
|
151
|
+
settings.api_capacity_redis_url.get_secret_value()
|
|
152
|
+
if settings.api_capacity_redis_url is not None
|
|
153
|
+
else None
|
|
154
|
+
)
|
|
155
|
+
app.state.api_capacity_limiter = build_api_capacity_limiter(
|
|
156
|
+
redis_url=redis_url,
|
|
157
|
+
requests_per_minute=settings.api_requests_per_minute,
|
|
158
|
+
max_inflight=settings.api_max_inflight_per_principal,
|
|
159
|
+
lease_seconds=settings.api_request_timeout_seconds + 5,
|
|
160
|
+
)
|
|
161
|
+
app.state.api_metrics = ApiMetrics(version=app.version)
|
|
162
|
+
app.add_middleware(
|
|
163
|
+
RequestBodyLimitMiddleware,
|
|
164
|
+
max_body_bytes=settings.max_request_body_bytes,
|
|
165
|
+
)
|
|
166
|
+
app.add_middleware(ApiMetricsMiddleware, metrics=app.state.api_metrics)
|
|
167
|
+
app.add_middleware(TraceIdMiddleware)
|
|
168
|
+
if settings.cors_origins:
|
|
169
|
+
if "*" in settings.cors_origins:
|
|
170
|
+
raise HarborConfigurationError(
|
|
171
|
+
"wildcard cors_origins is not allowed with credentialed CORS; list explicit origins"
|
|
172
|
+
)
|
|
173
|
+
app.add_middleware(
|
|
174
|
+
CORSMiddleware,
|
|
175
|
+
allow_origins=settings.cors_origins,
|
|
176
|
+
allow_credentials=True,
|
|
177
|
+
allow_methods=["*"],
|
|
178
|
+
allow_headers=["*"],
|
|
179
|
+
)
|
|
180
|
+
register_error_handlers(app)
|
|
181
|
+
app.add_api_route("/", _redirect_root, methods=["GET"], include_in_schema=False)
|
|
182
|
+
if settings.docs_enabled:
|
|
183
|
+
app.add_api_route(
|
|
184
|
+
"/docs",
|
|
185
|
+
_redirect_docs,
|
|
186
|
+
methods=["GET"],
|
|
187
|
+
include_in_schema=False,
|
|
188
|
+
)
|
|
189
|
+
register_routes(app)
|
|
190
|
+
app.state.api_metric_routes = tuple(
|
|
191
|
+
(path, compile_path(path)[0]) for path in app.openapi()["paths"]
|
|
192
|
+
)
|
|
193
|
+
return app
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Bearer-token auth for the Control Plane API (ST4)."""
|