Merge pull request #356 from EverMind-AI/feat/api-v2-alias
feat(api): serve endpoints under /api/v2, retain /api/v1 as alias
This commit is contained in:
commit
a59d385d23
|
|
@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **`/api/v2` API prefix** — every business endpoint (`memory/*`, `ome/*`,
|
||||
`knowledge/*`) is now served under `/api/v2`, aligning the open-source API
|
||||
with the EverOS Cloud contract. `/api/v1` is retained as a permanent,
|
||||
backward-compatible alias: both prefixes resolve to the same handlers with
|
||||
identical request/response contracts, so existing integrations keep working
|
||||
unchanged. Infrastructure endpoints (`/health`, `/metrics`) stay unversioned.
|
||||
|
||||
## [1.1.4] - 2026-07-20
|
||||
|
||||
### Added
|
||||
|
|
|
|||
71
docs/api.md
71
docs/api.md
|
|
@ -1,4 +1,4 @@
|
|||
# EverOS HTTP API (v1)
|
||||
# EverOS HTTP API (v2)
|
||||
|
||||
Human-readable reference for the EverOS HTTP API. Schema names, types
|
||||
and validation constraints mirror the OpenAPI spec served at
|
||||
|
|
@ -26,11 +26,11 @@ business semantics the raw spec does not carry.
|
|||
- [SearchMethod](#searchmethod)
|
||||
- [GetMemoryType](#getmemorytype)
|
||||
- [Endpoints](#endpoints)
|
||||
- [POST /api/v1/memory/add](#post-apiv1memoryadd)
|
||||
- [POST /api/v1/memory/flush](#post-apiv1memoryflush)
|
||||
- [POST /api/v1/memory/search](#post-apiv1memorysearch)
|
||||
- [POST /api/v1/memory/get](#post-apiv1memoryget)
|
||||
- [POST /api/v1/ome/trigger](#post-apiv1ometrigger)
|
||||
- [POST /api/v2/memory/add](#post-apiv1memoryadd)
|
||||
- [POST /api/v2/memory/flush](#post-apiv1memoryflush)
|
||||
- [POST /api/v2/memory/search](#post-apiv1memorysearch)
|
||||
- [POST /api/v2/memory/get](#post-apiv1memoryget)
|
||||
- [POST /api/v2/ome/trigger](#post-apiv1ometrigger)
|
||||
- [Knowledge endpoints](#knowledge-endpoints)
|
||||
- [OpenAPI spec source](#openapi-spec-source)
|
||||
|
||||
|
|
@ -42,15 +42,22 @@ business semantics the raw spec does not carry.
|
|||
|---|---|---|
|
||||
| Host | `127.0.0.1` (loopback only) | `EVEROS_API__HOST` env var or `--host` flag |
|
||||
| Port | `8000` | `EVEROS_API__PORT` env var or `--port` flag |
|
||||
| Version prefix | `/api/v1` | — |
|
||||
| Version prefix | `/api/v2` | — |
|
||||
|
||||
Business endpoints live under `/api/v1/memory/`, `/api/v1/ome/`, and
|
||||
`/api/v1/knowledge/`. Knowledge endpoints have their own dedicated
|
||||
Business endpoints live under `/api/v2/memory/`, `/api/v2/ome/`, and
|
||||
`/api/v2/knowledge/`. Knowledge endpoints have their own dedicated
|
||||
reference at [docs/knowledge.md](knowledge.md) and are cross-referenced
|
||||
below. The operational endpoints `GET /health` and `GET /metrics` exist
|
||||
but are intentionally outside this reference — they are runtime probes
|
||||
for deployment, not part of the application contract.
|
||||
|
||||
`/api/v2` is the canonical prefix, aligned with the EverOS Cloud API. Every
|
||||
business endpoint is **also** served under `/api/v1`, which is retained as a
|
||||
permanent, backward-compatible alias: the two prefixes resolve to the same
|
||||
handlers with identical request/response contracts. Existing `/api/v1`
|
||||
integrations keep working unchanged; new integrations should use `/api/v2`.
|
||||
Swap the prefix in any example below to reach the same endpoint under v1.
|
||||
|
||||
### Content type
|
||||
|
||||
All `POST` endpoints require `Content-Type: application/json`. Request
|
||||
|
|
@ -133,7 +140,7 @@ storage. This is the same rule users see when reading rendered output:
|
|||
e.g. `alice_ep_20260528_00000001` for an episode, `alice_af_...`
|
||||
for an atomic fact. See
|
||||
[storage_layout.md §4](storage_layout.md) for the encoding.
|
||||
- **All endpoints are POST** for `/api/v1/memory/*` even when the
|
||||
- **All endpoints are POST** for `/api/v2/memory/*` even when the
|
||||
semantics look like a read (`/search`, `/get`) — the request bodies
|
||||
are too rich (filters, methods, paging) to encode in a query string.
|
||||
|
||||
|
|
@ -173,7 +180,7 @@ the top level (mirroring the success envelope) alongside a nested
|
|||
"code": "NOT_FOUND",
|
||||
"message": "Document 'abc123' not found",
|
||||
"timestamp": "2026-06-01T12:24:46+00:00",
|
||||
"path": "/api/v1/knowledge/documents/abc123"
|
||||
"path": "/api/v2/knowledge/documents/abc123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -204,7 +211,7 @@ parsing the human-readable `message` field.
|
|||
| `code` | `string` | One of the `ErrorCode` values listed above |
|
||||
| `message` | `string` | Human-readable reason. For `INVALID_INPUT` from request validation, **only the first** validation error is surfaced, formatted `"<msg>: <dotted-loc>"` with the leading `body` segment stripped (e.g. `"Field required: messages"`); a model-level validator with no field location surfaces just `"<msg>"` (e.g. `"Value error, exactly one of user_id / agent_id must be provided"`) |
|
||||
| `timestamp` | `string` | ISO-8601 with timezone offset (display tz) |
|
||||
| `path` | `string` | Request path, e.g. `/api/v1/memory/add` |
|
||||
| `path` | `string` | Request path, e.g. `/api/v2/memory/add` |
|
||||
|
||||
> Unlike FastAPI's default, the full per-field validation array is **not**
|
||||
> returned — only the first error's message. A client that needs the
|
||||
|
|
@ -478,7 +485,7 @@ require `agent_id`. The mismatching combinations are rejected with
|
|||
|
||||
## Endpoints
|
||||
|
||||
### POST /api/v1/memory/add
|
||||
### POST /api/v2/memory/add
|
||||
|
||||
Append a batch of messages to a session buffer. The server
|
||||
accumulates messages until the boundary detector decides the session
|
||||
|
|
@ -535,7 +542,7 @@ correlation.
|
|||
|
||||
```bash
|
||||
TS=$(( $(date +%s) * 1000 ))
|
||||
curl -X POST http://127.0.0.1:8000/api/v1/memory/add \
|
||||
curl -X POST http://127.0.0.1:8000/api/v2/memory/add \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{
|
||||
\"session_id\": \"demo-002\",
|
||||
|
|
@ -561,7 +568,7 @@ Response (real capture):
|
|||
}
|
||||
```
|
||||
|
||||
### POST /api/v1/memory/flush
|
||||
### POST /api/v2/memory/flush
|
||||
|
||||
Force the boundary detector to decide **now** for the given session
|
||||
buffer. The LLM runs extraction (one call) regardless of whether the
|
||||
|
|
@ -603,7 +610,7 @@ sync is still asynchronous — see
|
|||
#### cURL example
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8000/api/v1/memory/flush \
|
||||
curl -X POST http://127.0.0.1:8000/api/v2/memory/flush \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"session_id":"demo-002","app_id":"default","project_id":"default"}'
|
||||
```
|
||||
|
|
@ -620,7 +627,7 @@ extraction LLM call):
|
|||
}
|
||||
```
|
||||
|
||||
### POST /api/v1/memory/search
|
||||
### POST /api/v2/memory/search
|
||||
|
||||
Hybrid retrieval over the memory store. Combines BM25, dense vector
|
||||
ANN, optional scalar filtering, optional cross-encoder rerank, and
|
||||
|
|
@ -823,7 +830,7 @@ attribution, so `session_id` is the only meaningful query dimension.
|
|||
#### cURL example
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8000/api/v1/memory/search \
|
||||
curl -X POST http://127.0.0.1:8000/api/v2/memory/search \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"user_id": "alice",
|
||||
|
|
@ -871,7 +878,7 @@ Response (real capture):
|
|||
}
|
||||
```
|
||||
|
||||
### POST /api/v1/memory/get
|
||||
### POST /api/v2/memory/get
|
||||
|
||||
Paginated listing of memory records of a given kind for a single
|
||||
owner. No ranking — ordering is `sort_by` × `sort_order` only. Used
|
||||
|
|
@ -1003,7 +1010,7 @@ Same shape as [SearchAgentSkillItem](#searchagentskillitem) **minus**
|
|||
#### cURL example
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8000/api/v1/memory/get \
|
||||
curl -X POST http://127.0.0.1:8000/api/v2/memory/get \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"user_id": "alice",
|
||||
|
|
@ -1043,7 +1050,7 @@ Response (real capture):
|
|||
}
|
||||
```
|
||||
|
||||
### POST /api/v1/ome/trigger
|
||||
### POST /api/v2/ome/trigger
|
||||
|
||||
Manually trigger a registered OME strategy.
|
||||
|
||||
|
|
@ -1071,7 +1078,7 @@ Manually trigger a registered OME strategy.
|
|||
#### cURL example
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8000/api/v1/ome/trigger \
|
||||
curl -X POST http://127.0.0.1:8000/api/v2/ome/trigger \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"name": "reflect_episodes", "force": true}'
|
||||
```
|
||||
|
|
@ -1080,7 +1087,7 @@ curl -X POST http://127.0.0.1:8000/api/v1/ome/trigger \
|
|||
|
||||
### Knowledge endpoints
|
||||
|
||||
The knowledge base subsystem (`/api/v1/knowledge/*`) provides document
|
||||
The knowledge base subsystem (`/api/v2/knowledge/*`) provides document
|
||||
upload, CRUD, and hybrid search. These endpoints are fully documented
|
||||
in their own reference: **[docs/knowledge.md](knowledge.md)**.
|
||||
|
||||
|
|
@ -1088,15 +1095,15 @@ Summary of available routes:
|
|||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| `POST` | `/api/v1/knowledge/documents` | Upload and extract a document |
|
||||
| `GET` | `/api/v1/knowledge/documents` | List documents (paginated) |
|
||||
| `GET` | `/api/v1/knowledge/documents/{doc_id}` | Get a single document |
|
||||
| `PUT` | `/api/v1/knowledge/documents/{doc_id}` | Replace a document |
|
||||
| `PATCH` | `/api/v1/knowledge/documents/{doc_id}` | Partial update |
|
||||
| `DELETE` | `/api/v1/knowledge/documents/{doc_id}` | Delete a document |
|
||||
| `GET` | `/api/v1/knowledge/topics/{topic_id}` | Get a single topic |
|
||||
| `POST` | `/api/v1/knowledge/search` | Hybrid search over topics |
|
||||
| `GET` | `/api/v1/knowledge/categories` | List taxonomy categories |
|
||||
| `POST` | `/api/v2/knowledge/documents` | Upload and extract a document |
|
||||
| `GET` | `/api/v2/knowledge/documents` | List documents (paginated) |
|
||||
| `GET` | `/api/v2/knowledge/documents/{doc_id}` | Get a single document |
|
||||
| `PUT` | `/api/v2/knowledge/documents/{doc_id}` | Replace a document |
|
||||
| `PATCH` | `/api/v2/knowledge/documents/{doc_id}` | Partial update |
|
||||
| `DELETE` | `/api/v2/knowledge/documents/{doc_id}` | Delete a document |
|
||||
| `GET` | `/api/v2/knowledge/topics/{topic_id}` | Get a single topic |
|
||||
| `POST` | `/api/v2/knowledge/search` | Hybrid search over topics |
|
||||
| `GET` | `/api/v2/knowledge/categories` | List taxonomy categories |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -811,6 +811,766 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/memory/add": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"memory"
|
||||
],
|
||||
"summary": "Add Memory",
|
||||
"description": "Add messages into the user-memory + agent-memory pipelines.",
|
||||
"operationId": "add_memory_api_v2_memory_add_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MemorizeAddRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope_AddResponseData_"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/memory/flush": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"memory"
|
||||
],
|
||||
"summary": "Flush Memory",
|
||||
"description": "Force boundary detection over the current ``session_id`` buffer.\n\n[OSS-only] — cloud edition decides boundary timing server-side and\ndoes not expose this endpoint.",
|
||||
"operationId": "flush_memory_api_v2_memory_flush_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MemorizeFlushRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope_FlushResponseData_"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/memory/search": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"memory"
|
||||
],
|
||||
"summary": "Post Search",
|
||||
"description": "Hybrid retrieval across the configured memory backends.",
|
||||
"operationId": "post_search_api_v2_memory_search_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SearchRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SearchResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/memory/get": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"memory"
|
||||
],
|
||||
"summary": "Post Get",
|
||||
"description": "Paginated listing over the requested ``memory_type``.",
|
||||
"operationId": "post_get_api_v2_memory_get_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/ome/trigger": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"ome"
|
||||
],
|
||||
"summary": "Trigger",
|
||||
"description": "Manually trigger a registered OME strategy and wait for completion.",
|
||||
"operationId": "trigger_api_v2_ome_trigger_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TriggerRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TriggerResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/knowledge/documents": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"knowledge"
|
||||
],
|
||||
"summary": "Create Document Route",
|
||||
"description": "Upload a new knowledge document.",
|
||||
"operationId": "create_document_route_api_v2_knowledge_documents_post",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_create_document_route_api_v2_knowledge_documents_post"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope_DocumentCreateResponse_"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"tags": [
|
||||
"knowledge"
|
||||
],
|
||||
"summary": "List Documents Route",
|
||||
"description": "Paginated document listing.",
|
||||
"operationId": "list_documents_route_api_v2_knowledge_documents_get",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "app_id",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"title": "App Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"title": "Project Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "category_id",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Category Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"default": 1,
|
||||
"title": "Page"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "page_size",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"maximum": 100,
|
||||
"minimum": 1,
|
||||
"default": 20,
|
||||
"title": "Page Size"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sort_by",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"enum": [
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"title"
|
||||
],
|
||||
"type": "string",
|
||||
"default": "created_at",
|
||||
"title": "Sort By"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sort_order",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string",
|
||||
"default": "desc",
|
||||
"title": "Sort Order"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope_DocumentListResponse_"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/knowledge/documents/{doc_id}": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"knowledge"
|
||||
],
|
||||
"summary": "Replace Document Route",
|
||||
"description": "Replace an existing knowledge document (atomic backup/restore on failure).",
|
||||
"operationId": "replace_document_route_api_v2_knowledge_documents__doc_id__put",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "doc_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"pattern": "^d_[a-f0-9]{12,32}$",
|
||||
"title": "Doc Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Body_replace_document_route_api_v2_knowledge_documents__doc_id__put"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope_DocumentCreateResponse_"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"knowledge"
|
||||
],
|
||||
"summary": "Delete Document Route",
|
||||
"description": "Remove a knowledge document.",
|
||||
"operationId": "delete_document_route_api_v2_knowledge_documents__doc_id__delete",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "doc_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"pattern": "^d_[a-f0-9]{12,32}$",
|
||||
"title": "Doc Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "app_id",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"title": "App Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"title": "Project Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"tags": [
|
||||
"knowledge"
|
||||
],
|
||||
"summary": "Get Document Route",
|
||||
"description": "Fetch a single document with its topic list.",
|
||||
"operationId": "get_document_route_api_v2_knowledge_documents__doc_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "doc_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"pattern": "^d_[a-f0-9]{12,32}$",
|
||||
"title": "Doc Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "app_id",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"title": "App Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"title": "Project Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope_DocumentDetailResponse_"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"knowledge"
|
||||
],
|
||||
"summary": "Patch Document Route",
|
||||
"description": "Update mutable document metadata fields.",
|
||||
"operationId": "patch_document_route_api_v2_knowledge_documents__doc_id__patch",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "doc_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"pattern": "^d_[a-f0-9]{12,32}$",
|
||||
"title": "Doc Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DocumentPatchRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope_DocumentPatchResponse_"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/knowledge/topics/{topic_id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"knowledge"
|
||||
],
|
||||
"summary": "Get Topic Route",
|
||||
"description": "Fetch a single topic with full content.",
|
||||
"operationId": "get_topic_route_api_v2_knowledge_topics__topic_id__get",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "topic_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"pattern": "^d_[a-f0-9]{12,32}_\\d+$",
|
||||
"title": "Topic Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "app_id",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"title": "App Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"title": "Project Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope_TopicDetailResponse_"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/knowledge/search": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"knowledge"
|
||||
],
|
||||
"summary": "Search Knowledge Route",
|
||||
"description": "Knowledge retrieval (keyword / vector / hybrid).",
|
||||
"operationId": "search_knowledge_route_api_v2_knowledge_search_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/KnowledgeSearchRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope_KnowledgeSearchResponse_"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v2/knowledge/categories": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"knowledge"
|
||||
],
|
||||
"summary": "List Categories Route",
|
||||
"description": "List taxonomy categories from ``.taxonomy.md``.",
|
||||
"operationId": "list_categories_route_api_v2_knowledge_categories_get",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "app_id",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"title": "App Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "project_id",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"title": "Project Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SuccessEnvelope_CategoryListResponse_"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
|
@ -890,6 +1650,59 @@
|
|||
],
|
||||
"title": "Body_create_document_route_api_v1_knowledge_documents_post"
|
||||
},
|
||||
"Body_create_document_route_api_v2_knowledge_documents_post": {
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
"title": "File"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\w",
|
||||
"title": "Title"
|
||||
},
|
||||
"source_type": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Source Type"
|
||||
},
|
||||
"category_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Category Id"
|
||||
},
|
||||
"app_id": {
|
||||
"type": "string",
|
||||
"title": "App Id",
|
||||
"default": "default"
|
||||
},
|
||||
"project_id": {
|
||||
"type": "string",
|
||||
"title": "Project Id",
|
||||
"default": "default"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"file",
|
||||
"title"
|
||||
],
|
||||
"title": "Body_create_document_route_api_v2_knowledge_documents_post"
|
||||
},
|
||||
"Body_replace_document_route_api_v1_knowledge_documents__doc_id__put": {
|
||||
"properties": {
|
||||
"file": {
|
||||
|
|
@ -943,6 +1756,59 @@
|
|||
],
|
||||
"title": "Body_replace_document_route_api_v1_knowledge_documents__doc_id__put"
|
||||
},
|
||||
"Body_replace_document_route_api_v2_knowledge_documents__doc_id__put": {
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "string",
|
||||
"contentMediaType": "application/octet-stream",
|
||||
"title": "File"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "\\w",
|
||||
"title": "Title"
|
||||
},
|
||||
"source_type": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Source Type"
|
||||
},
|
||||
"category_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Category Id"
|
||||
},
|
||||
"app_id": {
|
||||
"type": "string",
|
||||
"title": "App Id",
|
||||
"default": "default"
|
||||
},
|
||||
"project_id": {
|
||||
"type": "string",
|
||||
"title": "Project Id",
|
||||
"default": "default"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"file",
|
||||
"title"
|
||||
],
|
||||
"title": "Body_replace_document_route_api_v2_knowledge_documents__doc_id__put"
|
||||
},
|
||||
"CategoryDTO": {
|
||||
"properties": {
|
||||
"category_id": {
|
||||
|
|
@ -1819,7 +2685,7 @@
|
|||
"memory_type"
|
||||
],
|
||||
"title": "GetRequest",
|
||||
"description": "Request body for ``POST /api/v1/memory/get``.\n\nCallers identify the memory owner via ``user_id`` XOR ``agent_id`` —\nexactly one must be set. Internally the manager keeps using\n``owner_id`` / ``owner_type`` (the storage tables' columns); those\nare exposed as derived properties so the rename only affects the\nwire contract."
|
||||
"description": "Request body for ``POST /api/v2/memory/get``.\n\nCallers identify the memory owner via ``user_id`` XOR ``agent_id`` —\nexactly one must be set. Internally the manager keeps using\n``owner_id`` / ``owner_type`` (the storage tables' columns); those\nare exposed as derived properties so the rename only affects the\nwire contract."
|
||||
},
|
||||
"GetResponse": {
|
||||
"properties": {
|
||||
|
|
@ -2642,7 +3508,7 @@
|
|||
"query"
|
||||
],
|
||||
"title": "SearchRequest",
|
||||
"description": "Request body for ``POST /api/v1/memory/search``.\n\nCallers identify the memory owner via ``user_id`` XOR ``agent_id`` —\nexactly one must be set. Internally the manager + compile_filters keep\nusing ``owner_id`` / ``owner_type`` (the storage tables' columns);\nthose are exposed as derived properties so the rename only affects\nthe wire contract, not the internal recall plumbing."
|
||||
"description": "Request body for ``POST /api/v2/memory/search``.\n\nCallers identify the memory owner via ``user_id`` XOR ``agent_id`` —\nexactly one must be set. Internally the manager + compile_filters keep\nusing ``owner_id`` / ``owner_type`` (the storage tables' columns);\nthose are exposed as derived properties so the rename only affects\nthe wire contract, not the internal recall plumbing."
|
||||
},
|
||||
"SearchResponse": {
|
||||
"properties": {
|
||||
|
|
@ -3001,7 +3867,7 @@
|
|||
"name"
|
||||
],
|
||||
"title": "TriggerRequest",
|
||||
"description": "Request body for ``POST /api/v1/ome/trigger``."
|
||||
"description": "Request body for ``POST /api/v2/ome/trigger``."
|
||||
},
|
||||
"TriggerResponse": {
|
||||
"properties": {
|
||||
|
|
@ -3020,7 +3886,7 @@
|
|||
"name"
|
||||
],
|
||||
"title": "TriggerResponse",
|
||||
"description": "Response body for ``POST /api/v1/ome/trigger``."
|
||||
"description": "Response body for ``POST /api/v2/ome/trigger``."
|
||||
},
|
||||
"UnprocessedMessageDTO": {
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -42,18 +42,27 @@ _SKIP_PATHS = frozenset({"/metrics", "/health", "/healthz", "/favicon.ico"})
|
|||
|
||||
|
||||
def _normalize_path(request: Request) -> str:
|
||||
"""Resolve the route template (e.g. ``/users/{user_id}``) for stable labels."""
|
||||
"""Resolve the full route template (e.g. ``/api/v1/users/{user_id}``).
|
||||
|
||||
Built from the concrete request path with matched path params folded
|
||||
back to ``{name}`` placeholders — NOT from ``route.path``. A route
|
||||
mounted under a prefix (the ``/api/vN`` version aliases) only carries
|
||||
its router-relative path on the route object (e.g. ``/memory/get``);
|
||||
the prefix lives on the mounted router. Using ``route.path`` would drop
|
||||
the version prefix and collapse v1/v2 traffic into one label, so the
|
||||
label is rebuilt from ``url.path`` instead. Unmatched requests (no
|
||||
``route`` in scope) fold to a single bucket to bound cardinality.
|
||||
"""
|
||||
scope = getattr(request, "scope", {})
|
||||
route = scope.get("route") if isinstance(scope, dict) else None
|
||||
if route is not None and hasattr(route, "path"):
|
||||
return route.path
|
||||
if request.path_params:
|
||||
path = request.url.path
|
||||
for name, value in request.path_params.items():
|
||||
if str(value) in path:
|
||||
path = path.replace(str(value), f"{{{name}}}")
|
||||
return path
|
||||
return "{unmatched}"
|
||||
if route is None:
|
||||
return "{unmatched}"
|
||||
path = request.url.path
|
||||
for name, value in request.path_params.items():
|
||||
value_str = str(value)
|
||||
if value_str:
|
||||
path = path.replace(value_str, f"{{{name}}}")
|
||||
return path
|
||||
|
||||
|
||||
class PrometheusMiddleware(BaseHTTPMiddleware):
|
||||
|
|
|
|||
|
|
@ -121,14 +121,28 @@ def create_app(
|
|||
# or handler runs, so all logs + the response header carry it.
|
||||
app.add_middleware(RequestIdMiddleware)
|
||||
|
||||
# Routes.
|
||||
# Infra endpoints — deliberately unversioned.
|
||||
app.include_router(health.router)
|
||||
app.include_router(metrics.router)
|
||||
app.include_router(memorize.router)
|
||||
app.include_router(search.router)
|
||||
app.include_router(get.router)
|
||||
app.include_router(ome.router)
|
||||
app.include_router(knowledge.router)
|
||||
|
||||
# Business API — served under both /api/v2 (cloud-aligned name) and
|
||||
# /api/v1 (retained as a permanent backward-compatible alias). The same
|
||||
# router object is mounted twice, so both prefixes resolve to the exact
|
||||
# same handlers; FastAPI's default operationId embeds the path, so the
|
||||
# two copies get distinct OpenAPI ids automatically (no collision).
|
||||
# v1 and v2 stay identical by construction — see test_api_versioning.
|
||||
# v1 first — retained alias, behavior identical.
|
||||
app.include_router(memorize.router, prefix="/api/v1")
|
||||
app.include_router(search.router, prefix="/api/v1")
|
||||
app.include_router(get.router, prefix="/api/v1")
|
||||
app.include_router(ome.router, prefix="/api/v1")
|
||||
app.include_router(knowledge.router, prefix="/api/v1")
|
||||
# v2 — cloud-aligned name, same routers.
|
||||
app.include_router(memorize.router, prefix="/api/v2")
|
||||
app.include_router(search.router, prefix="/api/v2")
|
||||
app.include_router(get.router, prefix="/api/v2")
|
||||
app.include_router(ome.router, prefix="/api/v2")
|
||||
app.include_router(knowledge.router, prefix="/api/v2")
|
||||
|
||||
logger.info("app_created", docs_enabled=enable_docs)
|
||||
return app
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""POST /api/v1/memory/get — paginated listing endpoint.
|
||||
"""POST /api/v2/memory/get — paginated listing endpoint.
|
||||
|
||||
Thin adapter: validate the request DTO, dispatch to the service layer,
|
||||
return the envelope verbatim. ``request_id`` is generated inside the
|
||||
|
|
@ -12,7 +12,7 @@ from fastapi import APIRouter
|
|||
from everos.memory.get import GetRequest, GetResponse
|
||||
from everos.service import get as get_service
|
||||
|
||||
router = APIRouter(prefix="/api/v1/memory", tags=["memory"])
|
||||
router = APIRouter(prefix="/memory", tags=["memory"])
|
||||
|
||||
|
||||
@router.post("/get", response_model=GetResponse)
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ from everos.service import (
|
|||
# a shared module would be cleaner but is out of scope for this PR.
|
||||
from .memorize import PathSafeId, SuccessEnvelope
|
||||
|
||||
router = APIRouter(prefix="/api/v1/knowledge", tags=["knowledge"])
|
||||
router = APIRouter(prefix="/knowledge", tags=["knowledge"])
|
||||
|
||||
|
||||
# ── Annotated param types (satisfies B008) ──────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""POST /api/v1/memory/add and /api/v1/memory/flush.
|
||||
"""POST /api/v2/memory/add and /api/v2/memory/flush.
|
||||
|
||||
DTOs follow the v1 API brief (01_v1_api_brief.md §2 / §3). Routes are
|
||||
thin adapters: validate the DTO, dump to dict, hand to service. No
|
||||
|
|
@ -19,7 +19,7 @@ from pydantic import AfterValidator, BaseModel, ConfigDict, Field
|
|||
from everos.entrypoints.api.utils import extract_request_id
|
||||
from everos.service import memorize
|
||||
|
||||
router = APIRouter(prefix="/api/v1/memory", tags=["memory"])
|
||||
router = APIRouter(prefix="/memory", tags=["memory"])
|
||||
|
||||
|
||||
# ── Path-safe identifier ────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ from pydantic import BaseModel
|
|||
from everos.core.errors import NotFoundError
|
||||
from everos.core.observability.logging import get_logger
|
||||
|
||||
router = APIRouter(prefix="/api/v1/ome", tags=["ome"])
|
||||
router = APIRouter(prefix="/ome", tags=["ome"])
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class TriggerRequest(BaseModel):
|
||||
"""Request body for ``POST /api/v1/ome/trigger``."""
|
||||
"""Request body for ``POST /api/v2/ome/trigger``."""
|
||||
|
||||
name: str
|
||||
timeout: float = 120.0
|
||||
|
|
@ -22,7 +22,7 @@ class TriggerRequest(BaseModel):
|
|||
|
||||
|
||||
class TriggerResponse(BaseModel):
|
||||
"""Response body for ``POST /api/v1/ome/trigger``."""
|
||||
"""Response body for ``POST /api/v2/ome/trigger``."""
|
||||
|
||||
status: str
|
||||
name: str
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""POST /api/v1/memory/search — hybrid retrieval endpoint.
|
||||
"""POST /api/v2/memory/search — hybrid retrieval endpoint.
|
||||
|
||||
Thin adapter: validate the request DTO, dispatch to the service layer,
|
||||
return the envelope verbatim. ``request_id`` is generated inside the
|
||||
|
|
@ -13,7 +13,7 @@ from fastapi import APIRouter
|
|||
from everos.memory.search import SearchRequest, SearchResponse
|
||||
from everos.service import search
|
||||
|
||||
router = APIRouter(prefix="/api/v1/memory", tags=["memory"])
|
||||
router = APIRouter(prefix="/memory", tags=["memory"])
|
||||
|
||||
|
||||
@router.post("/search", response_model=SearchResponse)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""memory.get — read path: paginated listing over LanceDB.
|
||||
|
||||
This subpackage owns the dispatch + shape layer for ``POST
|
||||
/api/v1/memory/get``. Unlike :mod:`memory.search`, /get does no
|
||||
/api/v2/memory/get``. Unlike :mod:`memory.search`, /get does no
|
||||
ranking — it is a pure offset/limit + scalar-filter listing,
|
||||
partitioned by ``(owner_type, memory_type)``.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Public DTOs for ``POST /api/v1/memory/get``.
|
||||
"""Public DTOs for ``POST /api/v2/memory/get``.
|
||||
|
||||
Contract per the final design (mirrors :mod:`memory.search.dto` shape,
|
||||
minus ``score`` because /get is a paginated listing rather than a
|
||||
|
|
@ -54,7 +54,7 @@ class GetMemoryType(StrEnum):
|
|||
|
||||
|
||||
class GetRequest(BaseModel):
|
||||
"""Request body for ``POST /api/v1/memory/get``.
|
||||
"""Request body for ``POST /api/v2/memory/get``.
|
||||
|
||||
Callers identify the memory owner via ``user_id`` XOR ``agent_id`` —
|
||||
exactly one must be set. Internally the manager keeps using
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""GetManager — top-level orchestrator for ``POST /api/v1/memory/get``.
|
||||
"""GetManager — top-level orchestrator for ``POST /api/v2/memory/get``.
|
||||
|
||||
Hard partition by ``(owner_type, memory_type)`` (validated by
|
||||
:class:`GetRequest`):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""memory.search — read path: hybrid retrieval over LanceDB.
|
||||
|
||||
This subpackage owns the recall + adapter layer for ``POST
|
||||
/api/v1/memory/search``. All fusion / rerank / agentic algorithms are
|
||||
/api/v2/memory/search``. All fusion / rerank / agentic algorithms are
|
||||
delegated to :mod:`everalgo.rank`; this layer is responsible for:
|
||||
|
||||
* compiling the Filters DSL into a LanceDB ``where`` string,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Public DTOs for ``POST /api/v1/memory/search``.
|
||||
"""Public DTOs for ``POST /api/v2/memory/search``.
|
||||
|
||||
Contract per the final design:
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ class FilterNode(BaseModel):
|
|||
|
||||
|
||||
class SearchRequest(BaseModel):
|
||||
"""Request body for ``POST /api/v1/memory/search``.
|
||||
"""Request body for ``POST /api/v2/memory/search``.
|
||||
|
||||
Callers identify the memory owner via ``user_id`` XOR ``agent_id`` —
|
||||
exactly one must be set. Internally the manager + compile_filters keep
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""SearchManager — top-level orchestrator for ``POST /api/v1/memory/search``.
|
||||
"""SearchManager — top-level orchestrator for ``POST /api/v2/memory/search``.
|
||||
|
||||
Hard partition by ``owner_type``:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Get use case — lazy singleton wiring for ``POST /api/v1/memory/get``.
|
||||
"""Get use case — lazy singleton wiring for ``POST /api/v2/memory/get``.
|
||||
|
||||
Mirrors :mod:`everos.service.search`: the :class:`GetManager` and its
|
||||
LanceDB repo singletons are built on first call so the FastAPI module
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
End-to-end orchestration:
|
||||
|
||||
POST /api/v1/memory/add { session_id, messages[] }
|
||||
POST /api/v2/memory/add { session_id, messages[] }
|
||||
→ ingest.process → IngestResult
|
||||
→ _boundary.prepare_cells(mode=settings.memorize.mode) → cells
|
||||
→ asyncio.gather(
|
||||
|
|
|
|||
|
|
@ -119,8 +119,32 @@ async def test_path_params_normalized(client: AsyncClient) -> None:
|
|||
# ── _normalize_path direct tests (defensive fallback branches) ─────────
|
||||
|
||||
|
||||
def test_normalize_path_uses_path_params_fallback() -> None:
|
||||
"""When scope has no ``route`` but ``path_params`` is set, substitute names."""
|
||||
def test_normalize_path_uses_full_request_path_not_route_path() -> None:
|
||||
"""Label is built from the full request path (keeps the mount prefix) with
|
||||
path params folded to ``{name}`` — NOT from the route's router-relative
|
||||
path. A versioned alias mounts the router under ``/api/vN``, so the route
|
||||
object only carries ``/memory/{id}``; the label must keep the prefix.
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from everos.core.middleware.prometheus import _normalize_path
|
||||
|
||||
fake_req = SimpleNamespace(
|
||||
scope={"route": SimpleNamespace(path="/memory/{id}")},
|
||||
url=SimpleNamespace(path="/api/v1/memory/abc"),
|
||||
path_params={"id": "abc"},
|
||||
)
|
||||
# type: ignore[arg-type] — helper accepts anything duck-typed.
|
||||
assert _normalize_path(fake_req) == "/api/v1/memory/{id}" # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_normalize_path_unmatched_even_with_path_params() -> None:
|
||||
"""No matched ``route`` in scope → ``{unmatched}``, even if params are set.
|
||||
|
||||
``path_params`` is only populated by route matching, so their presence
|
||||
without a route means no real match — we do not trust them to build a
|
||||
label (that would risk unbounded cardinality from unmatched paths).
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from everos.core.middleware.prometheus import _normalize_path
|
||||
|
|
@ -130,8 +154,7 @@ def test_normalize_path_uses_path_params_fallback() -> None:
|
|||
url=SimpleNamespace(path="/x/abc/y"),
|
||||
path_params={"id": "abc"},
|
||||
)
|
||||
# type: ignore[arg-type] — helper accepts anything duck-typed.
|
||||
assert _normalize_path(fake_req) == "/x/{id}/y" # type: ignore[arg-type]
|
||||
assert _normalize_path(fake_req) == "{unmatched}" # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_normalize_path_unmatched_fallback() -> None:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
"""API version aliasing — every business route is served under v1 and v2.
|
||||
|
||||
The ``/api/v2`` prefix is the cloud-aligned name; ``/api/v1`` is retained as a
|
||||
permanent backward-compatible alias pointing to the *same* endpoint. These
|
||||
tests are the completeness guard: they fail if any versioned route is exposed
|
||||
under one prefix but not the other, or if the two prefixes ever diverge to
|
||||
different handlers. Infrastructure endpoints (``/health``, ``/metrics``) are
|
||||
deliberately unversioned and must NOT be mirrored.
|
||||
|
||||
Assertions run against ``app.openapi()["paths"]`` — the authoritative,
|
||||
fully-resolved public surface — rather than ``app.routes`` (which FastAPI keeps
|
||||
as lazy ``_IncludedRouter`` wrappers, so leaf paths are not directly readable).
|
||||
Same-handler identity is proved via the operationId, which FastAPI derives from
|
||||
the endpoint function name + path: twin routes must share an operationId that
|
||||
differs only by the version segment.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from everos.entrypoints.api.app import create_app
|
||||
|
||||
_V1 = "/api/v1/"
|
||||
_V2 = "/api/v2/"
|
||||
|
||||
|
||||
def _openapi_paths() -> dict[str, dict]:
|
||||
app = create_app(lifespan_providers=[])
|
||||
return app.openapi()["paths"]
|
||||
|
||||
|
||||
def test_every_v1_route_has_identical_v2_twin() -> None:
|
||||
paths = _openapi_paths()
|
||||
v1 = {p: ops for p, ops in paths.items() if p.startswith(_V1)}
|
||||
assert v1, "expected at least one /api/v1 route"
|
||||
|
||||
for path, ops in v1.items():
|
||||
twin = _V2 + path[len(_V1) :]
|
||||
assert twin in paths, f"{path} has no v2 twin at {twin}"
|
||||
assert set(paths[twin]) == set(ops), f"{twin} verbs differ from {path}"
|
||||
# Same handler: operationId differs only by the version token.
|
||||
for method, op in ops.items():
|
||||
v1_id = op["operationId"]
|
||||
v2_id = paths[twin][method]["operationId"]
|
||||
assert v1_id.replace("_v1_", "_v2_") == v2_id, (
|
||||
f"{twin} [{method}] resolves to a different handler: "
|
||||
f"{v1_id!r} vs {v2_id!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_every_v2_route_has_v1_twin() -> None:
|
||||
paths = _openapi_paths()
|
||||
v2 = {p for p in paths if p.startswith(_V2)}
|
||||
assert v2, "expected at least one /api/v2 route"
|
||||
|
||||
for path in v2:
|
||||
twin = _V1 + path[len(_V2) :]
|
||||
assert twin in paths, f"{path} has no v1 twin at {twin}"
|
||||
|
||||
|
||||
def test_infra_endpoints_are_not_versioned() -> None:
|
||||
paths = set(_openapi_paths())
|
||||
assert "/health" in paths
|
||||
assert "/metrics" in paths
|
||||
# No accidental versioned mirror of infra endpoints.
|
||||
assert "/api/v1/health" not in paths
|
||||
assert "/api/v2/health" not in paths
|
||||
assert "/api/v1/metrics" not in paths
|
||||
assert "/api/v2/metrics" not in paths
|
||||
|
|
@ -112,6 +112,26 @@ async def test_metrics_counter_increments_on_request(client: AsyncClient) -> Non
|
|||
assert after - before == 1.0, f"counter not bumped: {before} → {after}"
|
||||
|
||||
|
||||
async def test_v1_and_v2_recorded_under_distinct_path_labels(
|
||||
client: AsyncClient,
|
||||
) -> None:
|
||||
"""v1 and v2 alias hits must NOT collapse into one metric label.
|
||||
|
||||
Both prefixes resolve to the same handler, but the ``path`` label must
|
||||
keep the ``/api/vN`` prefix so existing dashboards keep working and
|
||||
per-version traffic stays distinguishable. (Regression guard: the leaf
|
||||
route only carries its router-relative path, so the label must be built
|
||||
from the full request path, not ``route.path``.)
|
||||
"""
|
||||
await client.post("/api/v1/memory/get", json={})
|
||||
await client.post("/api/v2/memory/get", json={})
|
||||
|
||||
dump = (await client.get("/metrics")).text
|
||||
recorded = _all_recorded_paths(dump)
|
||||
assert "/api/v1/memory/get" in recorded, recorded
|
||||
assert "/api/v2/memory/get" in recorded, recorded
|
||||
|
||||
|
||||
async def test_metrics_skip_paths_not_recorded(client: AsyncClient) -> None:
|
||||
"""``_SKIP_PATHS`` (``/metrics``, ``/health``) never appear in the counter."""
|
||||
# Hit both endpoints. If they were *not* skipped, they'd show up in
|
||||
|
|
|
|||
Loading…
Reference in New Issue