update documentation
This commit is contained in:
@@ -1,283 +0,0 @@
|
||||
# API Routes
|
||||
|
||||
All HTTP endpoints across NexusAI services. Clients communicate only with
|
||||
the orchestration service (port 4000) — memory service routes are listed
|
||||
here for reference and direct debugging use.
|
||||
|
||||
---
|
||||
|
||||
## Orchestration Service — port 4000
|
||||
|
||||
### Health
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | /health | Service health check |
|
||||
|
||||
### Chat
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| POST | /chat | Send a message, receive full response |
|
||||
| POST | /chat/stream | Send a message, receive SSE token stream |
|
||||
|
||||
**POST /chat and POST /chat/stream — request body:**
|
||||
```json
|
||||
{
|
||||
"sessionId": "your-session-uuid",
|
||||
"message": "Hello, my name is Tim.",
|
||||
"model": "gemma-4-26B-A4B-Claude-Distill-APEX-I-Mini.gguf",
|
||||
"temperature": 0.7
|
||||
}
|
||||
```
|
||||
`model` and `temperature` are optional.
|
||||
|
||||
**POST /chat — response:**
|
||||
```json
|
||||
{
|
||||
"sessionId": "your-session-uuid",
|
||||
"response": "Hello Tim! How can I help you today?",
|
||||
"model": "gemma-4-26B-A4B-Claude-Distill-APEX-I-Mini.gguf",
|
||||
"tokenCount": 87
|
||||
}
|
||||
```
|
||||
|
||||
**POST /chat/stream — response (SSE):**
|
||||
```
|
||||
data: {"text":"Hello"}
|
||||
data: {"text":" Tim"}
|
||||
data: {"done":true,"model":"gemma-4-26B...gguf","tokenCount":87}
|
||||
```
|
||||
|
||||
### Sessions
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | /sessions | Paginated session list |
|
||||
| GET | /sessions/:sessionId/history | Paginated episode history for a session |
|
||||
| PATCH | /sessions/:sessionId | Update session name and/or project assignment |
|
||||
| DELETE | /sessions/:sessionId | Delete session and all its episodes |
|
||||
|
||||
**GET /sessions — query params:**
|
||||
|
||||
| Param | Default | Description |
|
||||
|---|---|---|
|
||||
| limit | 20 | Sessions per page |
|
||||
| offset | 0 | Pagination offset |
|
||||
| projectId | — | Filter by project (integer ID) |
|
||||
|
||||
**PATCH /sessions/:sessionId — body:**
|
||||
```json
|
||||
{ "name": "My Session", "projectId": 3 }
|
||||
```
|
||||
Either `name` or `projectId` is required. Both can be sent together.
|
||||
Returns the updated session object.
|
||||
|
||||
**GET /sessions/:sessionId/history — query params:**
|
||||
|
||||
| Param | Default | Description |
|
||||
|---|---|---|
|
||||
| limit | 20 | Episodes per page |
|
||||
| offset | 0 | Pagination offset |
|
||||
|
||||
Returns `{ sessionId, episodes: [...] }`. Episodes ordered newest first.
|
||||
|
||||
### Projects
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | /projects | Get all projects |
|
||||
| POST | /projects | Create a new project |
|
||||
| PATCH | /projects/:id | Update a project |
|
||||
| DELETE | /projects/:id | Delete a project (nulls session assignments) |
|
||||
|
||||
**POST /projects — body:**
|
||||
```json
|
||||
{
|
||||
"name": "My Project",
|
||||
"description": "Optional description",
|
||||
"colour": "#3d3a79",
|
||||
"icon": null,
|
||||
"isolated": 0
|
||||
}
|
||||
```
|
||||
`name` is required. All other fields optional. `isolated` is `0` or `1`.
|
||||
Returns `201` with the created project object.
|
||||
|
||||
**PATCH /projects/:id — body:** same fields as POST, all optional.
|
||||
|
||||
### Models
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | /models | Available models from `models.json` manifest |
|
||||
|
||||
Returns array: `[{ "value": "model-name.gguf", "label": "Display Name" }]`
|
||||
|
||||
---
|
||||
|
||||
## Memory Service — port 3002
|
||||
|
||||
Direct access is for debugging only. All client traffic goes through
|
||||
orchestration.
|
||||
|
||||
### Health
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | /health | Service health check |
|
||||
|
||||
### Sessions
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| POST | /sessions | Create a new session |
|
||||
| GET | /sessions | Paginated session list with optional projectId filter |
|
||||
| GET | /sessions/:id | Get session by internal ID |
|
||||
| GET | /sessions/by-external/:externalId | Get session by external ID |
|
||||
| PATCH | /sessions/by-external/:externalId | Update session fields |
|
||||
| DELETE | /sessions/by-external/:externalId | Delete session (cascades to episodes) |
|
||||
|
||||
> Route ordering: `by-external/:externalId` must be defined before `/:id`
|
||||
> to prevent `by-external` being captured as an ID param.
|
||||
|
||||
**POST /sessions — body:**
|
||||
```json
|
||||
{ "externalId": "unique-uuid", "metadata": {} }
|
||||
```
|
||||
|
||||
**PATCH /sessions/by-external/:externalId — body:**
|
||||
```json
|
||||
{ "name": "Session Name", "projectId": 3 }
|
||||
```
|
||||
Both fields are optional. Only provided fields are updated — other fields
|
||||
are not touched.
|
||||
|
||||
### Episodes
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| POST | /episodes | Create episode + auto-embed into Qdrant |
|
||||
| GET | /episodes/search?q=&limit= | FTS keyword search across all episodes |
|
||||
| GET | /episodes/:id | Get episode by ID |
|
||||
| GET | /sessions/:id/episodes?limit=&offset= | Paginated episodes for a session |
|
||||
| DELETE | /episodes/:id | Delete an episode |
|
||||
|
||||
> Route ordering: `/episodes/search` must be defined before `/episodes/:id`.
|
||||
|
||||
**POST /episodes — body:**
|
||||
```json
|
||||
{
|
||||
"sessionId": 1,
|
||||
"userMessage": "Hello",
|
||||
"aiResponse": "Hi there!",
|
||||
"tokenCount": 10
|
||||
}
|
||||
```
|
||||
|
||||
### Projects
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| POST | /projects | Create a new project |
|
||||
| GET | /projects | Get all projects |
|
||||
| GET | /projects/:id | Get project by ID |
|
||||
| PATCH | /projects/:id | Update a project |
|
||||
| DELETE | /projects/:id | Delete project + null session assignments |
|
||||
|
||||
Same request/response shape as orchestration `/projects` above.
|
||||
|
||||
### Entities
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| POST | /entities | Upsert entity (creates or updates by name + type) |
|
||||
| GET | /entities/by-type/:type | All entities of a given type |
|
||||
| GET | /entities/:id | Get entity by ID |
|
||||
| DELETE | /entities/:id | Delete entity (cascades to relationships) |
|
||||
|
||||
> Route ordering: `/entities/by-type/:type` must be before `/entities/:id`.
|
||||
|
||||
**POST /entities — body:**
|
||||
```json
|
||||
{
|
||||
"name": "NexusAI",
|
||||
"type": "project",
|
||||
"notes": "My AI memory project",
|
||||
"metadata": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Relationships
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| POST | /relationships | Upsert a relationship between two entities |
|
||||
| GET | /entities/:id/relationships | All relationships for an entity |
|
||||
| DELETE | /relationships | Delete a specific relationship |
|
||||
|
||||
**POST /relationships — body:**
|
||||
```json
|
||||
{ "fromId": 1, "toId": 2, "label": "uses", "metadata": {} }
|
||||
```
|
||||
|
||||
**DELETE /relationships — body:**
|
||||
```json
|
||||
{ "fromId": 1, "toId": 2, "label": "uses" }
|
||||
```
|
||||
|
||||
Relationships are identified by the composite key `(fromId, toId, label)`.
|
||||
Delete uses request body rather than URL params since this three-part key
|
||||
is awkward to encode in a path.
|
||||
|
||||
---
|
||||
|
||||
## Embedding Service — port 3003
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | /health | Service health check |
|
||||
| POST | /embed | Embed a single text string |
|
||||
| POST | /embed/batch | Embed an array of text strings |
|
||||
|
||||
**POST /embed — body:**
|
||||
```json
|
||||
{ "text": "Hello from NexusAI" }
|
||||
```
|
||||
|
||||
**POST /embed — response:**
|
||||
```json
|
||||
{ "embedding": [0.123, -0.456, ...], "model": "nomic-embed-text", "dimensions": 768 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Inference Service — port 3001
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | /health | Health check — reports active provider and model |
|
||||
| POST | /complete | Full completion — awaits entire response |
|
||||
| POST | /complete/stream | Streaming completion via SSE |
|
||||
|
||||
**POST /complete — body:**
|
||||
```json
|
||||
{
|
||||
"prompt": "What is the capital of France?",
|
||||
"model": "gemma-4-26B-A4B-Claude-Distill-APEX-I-Mini.gguf",
|
||||
"temperature": 0.7,
|
||||
"maxTokens": 1024
|
||||
}
|
||||
```
|
||||
All fields except `prompt` are optional.
|
||||
|
||||
**POST /complete — response:**
|
||||
```json
|
||||
{
|
||||
"text": "The capital of France is Paris.",
|
||||
"model": "gemma-4-26B...gguf",
|
||||
"done": true,
|
||||
"evalCount": 8,
|
||||
"promptEvalCount": 41
|
||||
}
|
||||
```
|
||||
@@ -1,128 +0,0 @@
|
||||
# Memory Isolation
|
||||
|
||||
NexusAI implements project-scoped memory — sessions belonging to the same
|
||||
project can share semantic context, and isolated projects can be restricted
|
||||
from drawing on memory outside the project. This document describes how the
|
||||
system works end-to-end.
|
||||
|
||||
## Concepts
|
||||
|
||||
**Session** — a single conversation thread. Identified by `external_id`.
|
||||
|
||||
**Project** — a named grouping of sessions. Has an `isolated` flag (0 or 1).
|
||||
|
||||
**Semantic search** — at inference time, the user's message is embedded and
|
||||
compared against past episodes in Qdrant to surface relevant context. The
|
||||
scope of this search is controlled by the project context.
|
||||
|
||||
## Semantic Search Scope
|
||||
|
||||
| Session state | Semantic search scope |
|
||||
|---|---|
|
||||
| No project | Own session's episodes only |
|
||||
| Assigned to a non-isolated project | All episodes across all sessions in the project |
|
||||
| Assigned to an isolated project | All episodes within the project only |
|
||||
| Removed from a project | Own session's episodes only (from that point) |
|
||||
|
||||
Sessions with no project assigned behave the same as they always have —
|
||||
only their own past episodes are searched.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Step 1 — Project context resolution (orchestration)
|
||||
|
||||
In `chat/index.js`, immediately after session resolution:
|
||||
|
||||
```js
|
||||
let projectSessionIds = null;
|
||||
if (session.project_id) {
|
||||
const project = await memory.getProject(session.project_id);
|
||||
if (project) {
|
||||
const projectSessions = await memory.getProjectSessions(session.project_id);
|
||||
projectSessionIds = projectSessions.map(s => s.id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If the session belongs to any project (isolated or not), `projectSessionIds`
|
||||
is populated with the internal integer IDs of all sessions in that project.
|
||||
|
||||
For **non-isolated projects**, this expands the search to all project sessions.
|
||||
For **isolated projects**, the same set is used but the intent is restriction
|
||||
— since `projectSessionIds` only contains project sessions, no external
|
||||
episodes can appear.
|
||||
|
||||
Both cases use the same code path — the `isolated` flag does not change the
|
||||
query logic, only the conceptual meaning.
|
||||
|
||||
### Step 2 — Qdrant filter construction
|
||||
|
||||
In `services/qdrant.js`, `searchEpisodes` builds the filter:
|
||||
|
||||
```js
|
||||
if (projectSessionIds) {
|
||||
body.filter = {
|
||||
should: projectSessionIds.map(id => ({
|
||||
key: 'sessionId', match: { value: id }
|
||||
}))
|
||||
};
|
||||
} else if (sessionId) {
|
||||
body.filter = { must: [{ key: 'sessionId', match: { value: sessionId } }] };
|
||||
}
|
||||
```
|
||||
|
||||
`should` is Qdrant's "match any of" operator — equivalent to SQL
|
||||
`WHERE sessionId IN (...)`. When `projectSessionIds` is set, the single-session
|
||||
filter is not used.
|
||||
|
||||
### Step 3 — Episode payloads
|
||||
|
||||
Every episode upserted into Qdrant carries `{ sessionId, createdAt }` in its
|
||||
payload. `sessionId` here is the **internal integer ID** from SQLite. This
|
||||
is what the Qdrant filter matches against.
|
||||
|
||||
This means the filter works correctly regardless of when episodes were created
|
||||
or when a session was added to a project — the payload is immutable.
|
||||
|
||||
## Important Behaviours
|
||||
|
||||
**Pre-existing episodes are included immediately.** When a session is added
|
||||
to a project and a new message is sent, Qdrant can match all of that session's
|
||||
existing episodes since the filter only requires the `sessionId` to be in the
|
||||
project's session list.
|
||||
|
||||
**Removing a session from a project takes effect immediately.** On the next
|
||||
message, `getProjectSessions` will not include that session's ID, so its
|
||||
episodes disappear from the semantic search scope.
|
||||
|
||||
**New sessions created from ProjectView are assigned after the first message.**
|
||||
The `useChat` hook writes the `project_id` assignment via `updateSession` after
|
||||
`onDone` fires. There is a brief window during the first message where the
|
||||
session has no project assigned. The project is correctly applied from the
|
||||
second message onward.
|
||||
|
||||
## Isolated vs Non-Isolated
|
||||
|
||||
The `isolated` flag is stored on the project but does not currently change the
|
||||
query logic — both isolated and non-isolated projects result in a
|
||||
`projectSessionIds` filter. The distinction is semantic and enforced by
|
||||
the project's membership:
|
||||
|
||||
- **Non-isolated** — intentionally draws from all sessions in the project,
|
||||
creating a shared memory pool for related conversations
|
||||
- **Isolated** — by design contains only sessions explicitly added to it,
|
||||
so the same filter naturally restricts context to project-only episodes
|
||||
|
||||
If cross-project contamination became a concern (e.g. a session accidentally
|
||||
added to the wrong project), removing it from the project immediately restores
|
||||
isolation.
|
||||
|
||||
## Qdrant Payload Structure
|
||||
|
||||
Episodes are stored with this payload:
|
||||
```json
|
||||
{ "sessionId": 42, "createdAt": 1776080188 }
|
||||
```
|
||||
|
||||
`sessionId` is the SQLite `sessions.id` integer, not the `external_id` UUID.
|
||||
This is important when building filters — always use internal IDs.
|
||||
Reference in New Issue
Block a user