项目文件夹

文件
wehub-resource-sync 6c9c7fe7f3
CI / lint (push) Failing after 1s
CI / unit tests (push) Failing after 1s
CI / unit tests (3.13) (push) Failing after 1s
CI / integration tests (push) Failing after 1s
CI / integration tests (3.13) (push) Failing after 1s
CI / package build (push) Failing after 1s
Commit lint / pull request title (push) Has been skipped
Commit lint / commit messages (push) Failing after 1s
Docs / links (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:24:24 +08:00

333 行
12 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# Multimodal Memory
EverOS turns non-text content — images, PDFs, audio, office documents,
HTML, email — into the **same structured, searchable memory** as plain
text. You attach the asset to a message at ingest time; a vision/audio
capable LLM parses it into text, and from there it flows through the
identical extraction → markdown → index pipeline as any text turn. The
result is fully retrievable with the same `/search` stack.
## Table of contents
- [How it works](#how-it-works)
- [Prerequisites](#prerequisites)
- [Install the extra](#install-the-extra)
- [LibreOffice (office documents only)](#libreoffice-office-documents-only)
- [Configure the multimodal LLM](#configure-the-multimodal-llm)
- [Supported modalities](#supported-modalities)
- [Sending multimodal content](#sending-multimodal-content)
- [Payload: `uri` vs `base64`](#payload-uri-vs-base64)
- [Example: image by URL](#example-image-by-url)
- [Example: mixed text + image in one turn](#example-mixed-text--image-in-one-turn)
- [Example: inline PDF via base64](#example-inline-pdf-via-base64)
- [Example: local file via `file://`](#example-local-file-via-file)
- [Calling from Python (plain HTTP)](#calling-from-python-plain-http)
- [Configuration reference](#configuration-reference)
- [Errors and limits](#errors-and-limits)
- [Searching multimodal memory](#searching-multimodal-memory)
## How it works
```
POST /api/v1/memory/add
messages[].content = [ ContentItem, ContentItem, ... ]
│ text items → used verbatim
│ non-text items → multimodal LLM (everalgo-parser)
parsed text merged back into the session buffer (in original order)
boundary detector → extraction LLM → memory cell (MemCell)
markdown (truth) + SQLite (state) + LanceDB (vector + BM25)
retrievable via /search and /get like any text memory
```
Each **non-text** `ContentItem` is routed through the parser, which calls
a separate, vision/audio capable LLM (configured independently from the
main extraction `[llm]`, so parsing can target a multimodal endpoint
without changing boundary or extraction behaviour). Visual/audio formats
(image / pdf / audio / office) always go through that LLM; a few
text-bearing formats can be parsed without it (e.g. a plain email with no
inline images). The parser returns text; that text takes the place of the
asset in the message buffer. Nothing downstream of the parser
knows or cares that the content originated as an image or PDF — the raw
bytes are **not** persisted past extraction (the episode and memory cell (`MemCell`)
store only the parsed text).
## Prerequisites
### Install the extra
Multimodal parsing lives behind an optional dependency group so the base
install stays lean:
```bash
uv pip install 'everos[multimodal]' # or: pip install 'everos[multimodal]'
```
This pulls in `everalgo-parser[svg]` — the `[svg]` bundle adds `cairosvg`
so SVG works out of the box.
### LibreOffice (office documents only)
Office formats (`.doc` / `.docx` / `.ppt` / `.pptx` / `.xls` / `.xlsx`)
are converted to PDF before being fed to the multimodal LLM. The parser
shells out to `soffice`, LibreOffice's headless renderer, so LibreOffice
must be present on the **server** host:
```bash
brew install --cask libreoffice # macOS
sudo apt-get install -y libreoffice # Debian / Ubuntu
```
Without LibreOffice, **office uploads return `503`**
(`CAPABILITY_UNAVAILABLE`) with a clear error message; image / PDF /
audio / HTML / email parsing is unaffected.
### Configure the multimodal LLM
The parser uses its own LLM section (`[multimodal]` in `everos.toml`),
independent from `[llm]`. The model must accept OpenAI `image_url`
parts. Fill in three fields in `everos.toml`:
```toml
[multimodal]
model = "google/gemini-3-flash-preview" # must support image_url parts
base_url = "https://openrouter.ai/api/v1"
api_key = "<your key>"
```
See [Configuration reference](#configuration-reference) for the full
field list.
## Supported modalities
| `type` | Typical formats | Payload | Notes |
|---|---|---|---|
| `text` | — | `text` | Plain text; the string shorthand also maps here |
| `image` | PNG / JPG / GIF / WebP / SVG | `uri` or `base64` | SVG via the bundled `cairosvg` |
| `pdf` | PDF | `uri` or `base64` | — |
| `audio` | MP3 / WAV / … | `uri` or `base64` | Endpoint must accept audio parts |
| `doc` | DOC / DOCX / PPT / PPTX / XLS / XLSX | `uri` or `base64` | **Requires LibreOffice** (converted to PDF first) |
| `html` | HTML | `uri` or `base64` | To inline HTML as plain text instead, send it as `type: "text"` |
| `email` | EML / MSG | `uri` or `base64` | — |
A **non-text** item must carry a fetchable/decodable payload (`uri` or
`base64`). A non-text item that only carries `text` returns `415` — the
parser has nothing to parse.
## Sending multimodal content
Multimodal input is a `content` **array** of `ContentItem` objects on a
[MessageItem](api.md#messageitem). A bare string `content` is shorthand
for a single text item; switch to the array form when you mix text with
non-text assets. Field-level rules are in
[api.md → ContentItem](api.md#contentitem); the essentials:
| Field | Purpose |
|---|---|
| `type` | One of the modalities above |
| `text` | The literal text — **only** for `type: "text"` |
| `uri` | `http(s)://` (fetched server-side) or `file://` (read from the server fs) |
| `base64` | Inline payload, plain base64 (no `data:` prefix) |
| `ext` | Extension hint (`"pdf"`, `"png"`, …); effectively required for `base64` |
| `name` | Display filename for logs |
Carry the payload in exactly **one** of `text` / `uri` / `base64`.
### Payload: `uri` vs `base64`
| | `uri` (`http(s)://`) | `base64` |
|---|---|---|
| Where the bytes live | Fetched transiently at parse time | Held verbatim in the SQLite session buffer until flush |
| Wire size | URL only | ~4/3× the raw size (base64 inflation) |
| Best for | Large assets, S3/OSS presigned URLs | Small assets, or when no reachable URL exists |
**Prefer `uri` for anything large.** A multi-MB base64 blob becomes
multi-MB of SQLite buffer text for the buffer's lifetime and slows
request parsing. The bytes are never persisted past extraction either
way — only the parsed text is.
### Example: image by URL
```bash
TS=$(($(date +%s) * 1000)) # v1 contract: timestamp in ms
curl -X POST http://127.0.0.1:8000/api/v1/memory/add \
-H 'Content-Type: application/json' \
-d "{
\"session_id\": \"mm-001\",
\"messages\": [
{
\"sender_id\": \"alice\",
\"role\": \"user\",
\"timestamp\": $TS,
\"content\": [
{ \"type\": \"image\", \"uri\": \"https://example.com/whiteboard.png\" }
]
}
]
}"
```
### Example: mixed text + image in one turn
```json
{
"session_id": "mm-001",
"messages": [
{
"sender_id": "alice",
"role": "user",
"timestamp": 1748390400000,
"content": [
{ "type": "text", "text": "Here's the whiteboard from today's planning session." },
{ "type": "image", "uri": "https://example.com/whiteboard.png", "name": "whiteboard.png" }
]
}
]
}
```
### Example: inline PDF via base64
```json
{
"session_id": "mm-001",
"messages": [
{
"sender_id": "alice",
"role": "user",
"timestamp": 1748390400000,
"content": [
{ "type": "text", "text": "Quarterly report attached." },
{ "type": "pdf", "base64": "JVBERi0xLjQK...", "ext": "pdf", "name": "q3.pdf" }
]
}
]
}
```
`ext` is effectively **required** for `base64` payloads — it drives
modality dispatch. Without it the server falls back to MIME inference and
otherwise `415`s.
### Example: local file via `file://`
A `file://` URI is read from the **server's** local filesystem (the path
must be reachable by the server process), guardrailed by size and an
optional allowlist:
```json
{ "type": "pdf", "uri": "file:///srv/uploads/q3.pdf" }
```
Guardrails (a violation surfaces as `415`):
- the resolved path (symlinks followed) must be an existing regular file;
- size ≤ `EVEROS_MULTIMODAL__FILE_URI_MAX_BYTES` (default 50 MiB);
- if `EVEROS_MULTIMODAL__FILE_URI_ALLOW_DIRS` is set, the path must lie
within one of the listed roots (unset = any readable file, the
local-first default — confine this when exposing the API beyond
loopback).
### Calling from Python (plain HTTP)
There is no EverOS Python client; call the HTTP API directly with any
HTTP library:
```python
import httpx
httpx.post(
"http://127.0.0.1:8000/api/v1/memory/add",
json={
"session_id": "mm-001",
"messages": [
{
"sender_id": "alice",
"role": "user",
"timestamp": 1748390400000,
"content": [
{"type": "text", "text": "Here's the whiteboard from today's meeting."},
{"type": "image", "uri": "https://example.com/whiteboard.png"},
],
}
],
},
)
```
## Configuration reference
All fields live under `[multimodal]` in `everos.toml`. Each can also
be overridden via `EVEROS_MULTIMODAL__<FIELD>` env vars (useful for
containers and CI).
| Field | Default | Meaning |
|---|---|---|
| `model` | `google/gemini-3-flash-preview` | Parsing model; must accept `image_url` parts |
| `base_url` | `https://openrouter.ai/api/v1` | OpenAI-compatible base URL |
| `api_key` | — (required) | API key for the endpoint above |
| `max_concurrency` | `4` | Cap on parallel multimodal calls within one extraction |
| `file_uri_max_bytes` | `52428800` (50 MiB) | Max size of a `file://` asset |
| `file_uri_allow_dirs` | `[]` (any) | Allowlisted base dirs for `file://` URIs |
## Errors and limits
Three failure classes behave differently:
**Format errors** — the uploaded file format is invalid or not
recognized. These abort the batch with `415` (`UNSUPPORTED_FORMAT`):
| Condition | HTTP | `error.code` |
|---|---|---|
| Non-text item carries only `text` (no `uri` / `base64`) | `415` | `UNSUPPORTED_FORMAT` |
| Extension / modality the parser has no handler for | `415` | `UNSUPPORTED_FORMAT` |
| `base64` without a resolvable `ext` / MIME to dispatch on | `415` | `UNSUPPORTED_FORMAT` |
| `file://` fails a guardrail (missing / non-regular / too large / outside allowlist) | `415` | `UNSUPPORTED_FORMAT` |
**Capability errors** — the server is missing a required dependency.
These abort the batch with `503` (`CAPABILITY_UNAVAILABLE`). Unlike
transient errors, retrying will not help — admin action is required:
| Condition | HTTP | `error.code` |
|---|---|---|
| `everos[multimodal]` extra not installed | `503` | `CAPABILITY_UNAVAILABLE` |
| Office document but no LibreOffice (`soffice`) on host | `503` | `CAPABILITY_UNAVAILABLE` |
**Transient LLM errors** — the multimodal LLM call failed. These
degrade gracefully — the request still returns `200`, the affected
item is marked `parse_status="failed"` and contributes no text, and the
rest of the batch extracts normally:
| Condition | HTTP | Result |
|---|---|---|
| Multimodal LLM call fails (timeout / rate-limit / model rejects) | `200` | That item is skipped; the rest of the batch still extracts |
All error responses use the standard error envelope — see
[api.md → Errors](api.md#errors).
## Searching multimodal memory
Nothing special is required. Because parsed text is folded into the same
episodes and memory cells as text turns, every retrieval method works
across multimodal-derived memory unchanged:
```bash
curl -X POST http://127.0.0.1:8000/api/v1/memory/search \
-H 'Content-Type: application/json' \
-d '{
"user_id": "alice",
"query": "whiteboard from the planning session",
"method": "hybrid"
}'
```
`keyword`, `vector`, `hybrid` (default), and `agentic` all apply — see
[api.md → SearchMethod](api.md#searchmethod).