diff --git a/docs/design/server_client/prompt.md b/docs/design/server_client/prompt.md
index d21e7d31..5f494656 100644
--- a/docs/design/server_client/prompt.md
+++ b/docs/design/server_client/prompt.md
@@ -33,7 +33,7 @@ openviking/service/
- `FSService`: ls, tree, stat, mkdir, rm, mv, read, abstract, overview, grep, glob
- `ResourceService`: add_resource, add_skill, wait_processed
- `SearchService`: find, search
-- `SessionService`: session, sessions, add_message, compress, extract
+- `SessionService`: session, sessions, add_message, commit
- `RelationService`: link, unlink, relations
- `DebugService`: observer (ObserverService)
- `PackService`: export_ovpack, import_ovpack
@@ -118,14 +118,14 @@ class Response(BaseModel):
- 调试: GET /api/v1/debug/status, GET /api/v1/debug/health
5. **实现服务启动器** (bootstrap.py):
-- 加载配置文件 `~/.openviking/server.yaml`
+- 加载配置文件 `ov.conf`(通过 `--config` 参数或 `OPENVIKING_CONFIG_FILE` 环境变量指定)
- 初始化 OpenVikingService
- 启动 uvicorn 服务器
### 验收标准
```bash
# 启动服务
-openviking serve --path ./data --port 8000
+openviking serve --config ./ov.conf --port 8000
# 验证 API
curl http://localhost:8000/health
@@ -178,7 +178,7 @@ cli = [
3. **实现核心命令**:
```bash
# 服务管理
-openviking serve --path
[--port 8000] [--host 0.0.0.0]
+openviking serve [--config ] [--port 1933] [--host 0.0.0.0]
# 调试命令
openviking status # 系统整体状态(包含 queue/vikingdb/vlm 组件状态)
@@ -199,9 +199,11 @@ openviking overview
openviking find [] [--limit N] [--threshold F]
```
-4. **实现配置优先级**:
-- 命令行参数 > 环境变量 > 配置文件
-- 环境变量: OPENVIKING_URL, OPENVIKING_API_KEY, OPENVIKING_USER, OPENVIKING_AGENT
+4. **实现配置管理**:
+- CLI 连接信息通过 `ovcli.conf` 配置文件管理(url/api_key/user)
+- 配置文件路径通过 `OPENVIKING_CLI_CONFIG_FILE` 环境变量指定
+- 命令行参数(`--config`、`--host`、`--port`)优先于配置文件
+- 不使用 `--url`、`--api-key` 等全局命令行选项
5. **实现输出格式化**:
- 支持 `--output json` 输出 JSON
@@ -210,7 +212,7 @@ openviking find [] [--limit N] [--threshold F]
### 验收标准
```bash
openviking --help
-openviking serve --path ./data --port 8000
+openviking serve --config ./ov.conf --port 1933
openviking add-resource ./docs/ --wait
openviking ls viking://resources/
openviking find "how to use"
@@ -285,7 +287,6 @@ class OpenViking:
url: Optional[str] = None, # HTTP 模式
api_key: Optional[str] = None,
user: Optional[str] = None,
- agent: Optional[str] = None,
):
if url:
self._client = HTTPClient(url, api_key)
@@ -328,8 +329,7 @@ results = client.find("how to use")
openviking session new [--user ]
openviking session list
openviking session get
-openviking session compress
-openviking session extract
+openviking session commit
openviking session delete
```
@@ -531,7 +531,7 @@ async def test_full_workflow():
### 启动服务
\`\`\`bash
-openviking serve --path ./data --port 8000
+openviking serve --config ./ov.conf --port 8000
\`\`\`
### 添加资源
@@ -624,9 +624,9 @@ services:
- "8000:8000"
volumes:
- openviking-data:/data
+ - ./ov.conf:/etc/openviking/ov.conf
environment:
- - OPENVIKING_PATH=/data
- - OPENVIKING_API_KEY=${OPENVIKING_API_KEY}
+ - OPENVIKING_CONFIG_FILE=/etc/openviking/ov.conf
- OPENAI_API_KEY=${OPENAI_API_KEY}
restart: unless-stopped
diff --git a/docs/design/server_client/server-cli-design.md b/docs/design/server_client/server-cli-design.md
index 630c0f20..477b8bc6 100644
--- a/docs/design/server_client/server-cli-design.md
+++ b/docs/design/server_client/server-cli-design.md
@@ -200,12 +200,12 @@ OpenViking 提供三种接口,面向不同使用场景:
| 接口 | 使用者 | 场景 | 特点 |
|------|--------|------|------|
| **Python SDK** | Agent 开发者 | Agent 代码中调用 | 对象化 API,支持异步,client 实例维护状态 |
-| **Bash CLI** | Agent / 运维人员 | Agent subprocess 调用、运维脚本 | 非交互式,通过环境变量管理状态 |
+| **Bash CLI** | Agent / 运维人员 | Agent subprocess 调用、运维脚本 | 非交互式,通过 ovcli.conf 配置文件管理连接状态 |
| **HTTP API** | 任意语言客户端 | 跨语言集成、微服务调用 | RESTful,无状态,每次请求带身份信息 |
**选型建议**:
- **Agent 开发**:优先使用 Python SDK,可以维护 client 实例状态
-- **Agent 调用外部工具**:使用 Bash CLI,通过环境变量传递身份信息
+- **Agent 调用外部工具**:使用 Bash CLI,通过 ovcli.conf 配置文件传递连接和身份信息
- **非 Python 环境**:使用 HTTP API
### 3.1 分层架构
@@ -341,37 +341,55 @@ OpenViking 提供三种接口,面向不同使用场景:
#### Client 配置
-客户端 SDK 通过构造函数参数或环境变量配置,不使用配置文件(参考 Weaviate/ChromaDB/Qdrant 等主流产品的设计):
+客户端 SDK 通过构造函数参数配置:
```python
-# 方式一:构造函数参数
-client = OpenViking(url="http://localhost:1933", api_key="your-api-key")
+# 构造函数参数
+client = OpenViking(url="http://localhost:1933", api_key="your-api-key", user="alice")
+```
+
+SDK 构造函数只接受 `url`、`api_key`、`path`、`user` 参数。不支持 `config` 参数,也不支持 `vectordb_url`/`agfs_url` 参数。
+
+#### CLI 配置
+
+CLI 通过 `ovcli.conf` 配置文件管理连接信息,不使用 `--url`、`--api-key` 等全局命令行选项:
-# 方式二:环境变量(OPENVIKING_URL / OPENVIKING_API_KEY)
-client = OpenViking()
+```json
+{
+ "url": "http://localhost:1933",
+ "api_key": "sk-xxx",
+ "user": "alice",
+ "output": "table"
+}
```
+配置文件路径通过 `OPENVIKING_CLI_CONFIG_FILE` 环境变量指定。
+
#### 环境变量
+只保留 2 个环境变量:
+
| 环境变量 | 说明 | 示例 |
|----------|------|------|
-| `OPENVIKING_URL` | Server URL | `http://localhost:1933` |
-| `OPENVIKING_API_KEY` | API Key | `sk-xxx` |
-| `OPENVIKING_USER` | 用户标识 | `alice` |
-| `OPENVIKING_AGENT` | Agent 标识 | `agent-a1` |
-| `OPENVIKING_PATH` | 本地数据路径(嵌入式模式) | `./data` |
+| `OPENVIKING_CONFIG_FILE` | ov.conf 配置文件路径(SDK 嵌入式 + Server) | `~/.openviking/ov.conf` |
+| `OPENVIKING_CLI_CONFIG_FILE` | ovcli.conf 配置文件路径(CLI 连接配置) | `~/.openviking/ovcli.conf` |
+
+不再使用单字段环境变量(`OPENVIKING_URL`、`OPENVIKING_API_KEY`、`OPENVIKING_HOST`、`OPENVIKING_PORT`、`OPENVIKING_PATH`、`OPENVIKING_VECTORDB_URL`、`OPENVIKING_AGFS_URL` 均已移除)。
-**为什么 `user`/`agent` 用环境变量而不是配置文件**:
-1. **进程隔离**:环境变量是进程级的,多个 Agent 进程互不干扰
-2. **无并发冲突**:不会出现多个进程同时写配置文件的问题
-3. **易于设置**:Agent 启动时设置一次,后续调用自动继承
+#### 配置文件
+
+系统使用 2 个配置文件:
+
+| 配置文件 | 用途 | 环境变量 |
+|----------|------|----------|
+| `ov.conf` | SDK 嵌入式模式 + Server 配置 | `OPENVIKING_CONFIG_FILE` |
+| `ovcli.conf` | CLI 连接配置(url/api_key/user) | `OPENVIKING_CLI_CONFIG_FILE` |
#### 配置优先级
从高到低:
-1. 命令行参数 / 构造函数参数(`--url`, `--user` 等)
-2. 环境变量(`OPENVIKING_URL`, `OPENVIKING_API_KEY` 等)
-3. 配置文件(`OPENVIKING_CONFIG_FILE`,仅服务端)
+1. 构造函数参数(SDK)/ 命令行参数(`--config`、`--host`、`--port`)
+2. 配置文件(`ov.conf` 或 `ovcli.conf`)
---
@@ -424,7 +442,7 @@ openviking/
├── cli/ # CLI 模块
│ ├── __init__.py
│ ├── main.py # CLI 入口
-│ └── output.py # 输出格式化
+│ └── output.py # 输出格式化(JSON/table/脚本模式)
│
├── client/ # Client 层
│ ├── __init__.py
@@ -491,8 +509,7 @@ dependencies = [
| 方法 | 参数 | 说明 |
|------|------|------|
| `add_message` | `role, content` | 添加消息 |
-| `compress` | - | 压缩会话 |
-| `extract` | - | 提取记忆 |
+| `commit` | - | 提交会话(归档消息、提取记忆) |
| `delete` | - | 删除会话 |
### 5.2 统一返回值格式
@@ -840,30 +857,30 @@ OpenViking 支持多 user、多 agent,CLI 需要处理多进程并发场景:
└─────────┘ └─────────┘ └─────────┘
```
-多个 Agent 进程可能同时运行,各自有不同的 user/agent 身份。因此 `user`/`agent` 通过环境变量管理(详见 3.4 配置管理)。
+多个 Agent 进程可能同时运行,各自有不同的 user/agent 身份。各进程通过各自的 `ovcli.conf` 配置文件管理连接信息和身份(详见 3.4 配置管理)。
#### 使用示例
**Agent 调用场景**:
```bash
-# Agent 启动时设置环境变量
-export OPENVIKING_URL=http://localhost:1933
-export OPENVIKING_API_KEY=sk-xxx
-export OPENVIKING_USER=alice
-export OPENVIKING_AGENT=agent-a1
-
-# 后续 CLI 调用自动使用环境变量,无需重复指定
+# CLI 连接信息通过 ovcli.conf 配置文件管理
+# 配置文件路径通过 OPENVIKING_CLI_CONFIG_FILE 环境变量指定
+export OPENVIKING_CLI_CONFIG_FILE=~/.openviking/ovcli.conf
+
+# ovcli.conf 内容示例:
+# {
+# "url": "http://localhost:1933",
+# "api_key": "sk-xxx",
+# "user": "alice",
+# "output": "table"
+# }
+
+# 后续 CLI 调用自动读取 ovcli.conf,无需重复指定
openviking find "how to use openviking"
openviking ls viking://resources/
openviking read viking://resources/doc.md
```
-**临时覆盖**:
-```bash
-# 命令行参数覆盖环境变量
-openviking --user bob find "query"
-```
-
#### 设计原则
1. **与接口一致**:命令名、参数与接口定义保持一致
@@ -874,8 +891,7 @@ openviking --user bob find "query"
```bash
# 服务管理
-openviking serve --path [--port 1933] [--host 0.0.0.0]
-openviking serve --port 1933 --agfs-url --vectordb-url
+openviking serve [--config ] [--port 1933] [--host 0.0.0.0]
openviking status
# 资源导入
@@ -910,8 +926,7 @@ openviking relations
openviking session new [--user ]
openviking session list
openviking session get
-openviking session compress
-openviking session extract
+openviking session commit
# 导入导出
openviking export
@@ -956,11 +971,9 @@ def main():
# serve command
serve_parser = subparsers.add_parser("serve", help="Start OpenViking HTTP Server")
+ serve_parser.add_argument("--config", type=str, default=None, help="Config file path (ov.conf)")
serve_parser.add_argument("--host", type=str, default=None, help="Host to bind to")
serve_parser.add_argument("--port", type=int, default=None, help="Port to bind to")
- serve_parser.add_argument("--path", type=str, default=None, help="Storage path")
- serve_parser.add_argument("--config", type=str, default=None, help="Config file path")
- serve_parser.add_argument("--api-key", type=str, default=None, help="API key")
args = parser.parse_args()
@@ -1051,6 +1064,7 @@ class OpenViking:
path: Optional[str] = None,
url: Optional[str] = None,
api_key: Optional[str] = None,
+ user: Optional[str] = None,
user: Optional[UserIdentifier] = None
):
if url:
@@ -1077,9 +1091,9 @@ class OpenViking:
pip install openviking
```
-#### 环境变量
+#### 配置文件
-详见 3.4 配置管理中的环境变量定义。
+详见 3.4 配置管理中的配置文件定义。
#### viking:// 目录结构
@@ -1145,15 +1159,13 @@ Authorization: Bearer your-api-key
- 如果 `config.api_key` 有值 → 验证请求中的 Key
**配置方式**:
-```yaml
-# ~/.openviking/server.yaml
-server:
- api_key: your-secret-key # 设置后启用认证,不设置则跳过认证
-```
-
-```bash
-# 或通过环境变量
-export OPENVIKING_API_KEY=your-secret-key
+```json
+// ~/.openviking/ov.conf
+{
+ "server": {
+ "api_key": "your-secret-key" // 设置后启用认证,不设置则跳过认证
+ }
+}
```
### 7.3 API 端点设计
@@ -1224,8 +1236,7 @@ GET /api/v1/sessions # session list
GET /api/v1/sessions/{id} # session get
DELETE /api/v1/sessions/{id} # session delete
-POST /api/v1/sessions/{id}/compress # session compress
-POST /api/v1/sessions/{id}/extract # session extract
+POST /api/v1/sessions/{id}/commit # session commit
POST /api/v1/sessions/{id}/messages # add message
Body: {"role": "user", "content": "..."}
```
@@ -1347,7 +1358,7 @@ T1 (Service层)
**验收标准**:
```bash
# 启动服务
-openviking serve --path ./data --port 1933
+openviking serve --config ./ov.conf --port 1933
# 验证 API
curl http://localhost:1933/health
@@ -1369,7 +1380,7 @@ curl http://localhost:1933/api/v1/fs/ls?uri=viking://
**验收标准**:
```bash
-openviking serve --path ./data --port 1933
+openviking serve --config ./ov.conf --port 1933
openviking add-resource ./docs/ --wait
openviking ls viking://resources/
openviking find "how to use"
@@ -1389,8 +1400,7 @@ openviking find "how to use"
**验收标准**:
```python
# HTTP 模式
-client = OpenViking(server_url="http://localhost:1933", api_key="test")
-client.initialize()
+client = OpenViking(url="http://localhost:1933", api_key="test")
results = client.find("how to use")
```
diff --git a/docs/en/api/01-overview.md b/docs/en/api/01-overview.md
index 6ec8848b..0fcde97b 100644
--- a/docs/en/api/01-overview.md
+++ b/docs/en/api/01-overview.md
@@ -6,11 +6,11 @@ This page covers how to connect to OpenViking and the conventions shared across
OpenViking supports three connection modes:
-| Mode | Use Case | Singleton |
-|------|----------|-----------|
-| **Embedded** | Local development, single process | Yes |
-| **Service** | Remote VectorDB + AGFS infrastructure | No |
-| **HTTP** | Connect to OpenViking Server | No |
+| Mode | Use Case | Description |
+|------|----------|-------------|
+| **Embedded** | Local development, single process | Runs locally with local data storage |
+| **HTTP** | Connect to OpenViking Server | Connects to a remote server via HTTP API |
+| **CLI** | Shell scripting, agent tool-use | Connects to server via CLI commands |
### Embedded Mode
@@ -21,16 +21,6 @@ client = ov.OpenViking(path="./data")
client.initialize()
```
-### Service Mode
-
-```python
-client = ov.OpenViking(
- vectordb_url="http://vectordb.example.com:8000",
- agfs_url="http://agfs.example.com:1833",
-)
-client.initialize()
-```
-
### HTTP Mode
```python
@@ -48,6 +38,42 @@ curl http://localhost:1933/api/v1/fs/ls?uri=viking:// \
-H "X-API-Key: your-key"
```
+### CLI Mode
+
+The CLI connects to an OpenViking server and exposes all operations as shell commands.
+
+**Configuration**
+
+Create `~/.openviking/ovcli.conf` (or set `OPENVIKING_CLI_CONFIG_FILE` environment variable):
+
+```json
+{
+ "url": "http://localhost:1933",
+ "api_key": "your-key"
+}
+```
+
+**Basic Usage**
+
+```bash
+openviking [global options] [arguments] [command options]
+```
+
+**Global Options** (must be placed before the command name)
+
+| Option | Description |
+|--------|-------------|
+| `--output`, `-o` | Output format: `table` (default), `json` |
+| `--json` | Compact JSON with `{ok, result}` wrapper (for scripts) |
+| `--version` | Show CLI version |
+
+Example:
+
+```bash
+openviking --json ls viking://resources/
+openviking -o json ls viking://resources/
+```
+
## Client Lifecycle
```python
@@ -95,6 +121,66 @@ All HTTP API responses follow a unified format:
}
```
+## CLI Output Format
+
+### Table Mode (default)
+
+List data is rendered as tables; non-list data falls back to formatted JSON:
+
+```bash
+openviking ls viking://resources/
+# name size mode isDir uri
+# .abstract.md 100 420 False viking://resources/.abstract.md
+```
+
+### JSON Mode (`--output json`)
+
+All commands output formatted JSON matching the API response `result` structure:
+
+```bash
+openviking -o json ls viking://resources/
+# [{ "name": "...", "size": 100, ... }, ...]
+```
+
+The default output format can be set in `ovcli.conf`:
+
+```json
+{
+ "url": "http://localhost:1933",
+ "output": "json"
+}
+```
+
+### Script Mode (`--json`)
+
+Compact JSON with status wrapper, suitable for scripting. Overrides `--output`:
+
+**Success**
+
+```json
+{"ok": true, "result": ...}
+```
+
+**Error**
+
+```json
+{"ok": false, "error": {"code": "NOT_FOUND", "message": "Resource not found", "details": {}}}
+```
+
+### Special Cases
+
+- **String results** (`read`, `abstract`, `overview`): printed directly as plain text
+- **None results** (`mkdir`, `rm`, `mv`): no output
+
+### Exit Codes
+
+| Code | Meaning |
+|------|---------|
+| 0 | Success |
+| 1 | General error |
+| 2 | Configuration error |
+| 3 | Connection error |
+
## Error Codes
| Code | HTTP Status | Description |
@@ -179,8 +265,7 @@ All HTTP API responses follow a unified format:
| GET | `/api/v1/sessions` | List sessions |
| GET | `/api/v1/sessions/{id}` | Get session |
| DELETE | `/api/v1/sessions/{id}` | Delete session |
-| POST | `/api/v1/sessions/{id}/compress` | Compress session |
-| POST | `/api/v1/sessions/{id}/extract` | Extract memories |
+| POST | `/api/v1/sessions/{id}/commit` | Commit session |
| POST | `/api/v1/sessions/{id}/messages` | Add message |
### Observer
diff --git a/docs/en/api/02-resources.md b/docs/en/api/02-resources.md
index f193c176..33ec8f04 100644
--- a/docs/en/api/02-resources.md
+++ b/docs/en/api/02-resources.md
@@ -80,6 +80,12 @@ curl -X POST http://localhost:1933/api/v1/resources \
}'
```
+**CLI**
+
+```bash
+openviking add-resource ./documents/guide.md --reason "User guide documentation"
+```
+
**Response**
```json
@@ -122,6 +128,12 @@ curl -X POST http://localhost:1933/api/v1/resources \
}'
```
+**CLI**
+
+```bash
+openviking add-resource https://example.com/api-docs.md --to viking://resources/external/ --reason "External API documentation"
+```
+
**Example: Wait for Processing**
**Python SDK**
@@ -156,6 +168,12 @@ curl -X POST http://localhost:1933/api/v1/system/wait \
-d '{}'
```
+**CLI**
+
+```bash
+openviking add-resource ./documents/guide.md --wait
+```
+
---
### export_ovpack()
@@ -202,6 +220,12 @@ curl -X POST http://localhost:1933/api/v1/pack/export \
}'
```
+**CLI**
+
+```bash
+openviking export viking://resources/my-project/ ./exports/my-project.ovpack
+```
+
**Response**
```json
@@ -267,6 +291,12 @@ curl -X POST http://localhost:1933/api/v1/pack/import \
}'
```
+**CLI**
+
+```bash
+openviking import ./exports/my-project.ovpack viking://resources/imported/ --force
+```
+
**Response**
```json
@@ -324,6 +354,19 @@ curl -X GET "http://localhost:1933/api/v1/fs/ls?uri=viking://resources/&recursiv
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+# List all resources
+openviking ls viking://resources/
+
+# Simple path list
+openviking ls viking://resources/ --simple
+
+# Recursive listing
+openviking ls viking://resources/ --recursive
+```
+
**Response**
```json
@@ -374,6 +417,19 @@ curl -X GET "http://localhost:1933/api/v1/content/read?uri=viking://resources/do
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+# L0: Abstract
+openviking abstract viking://resources/docs/
+
+# L1: Overview
+openviking overview viking://resources/docs/
+
+# L2: Full content
+openviking read viking://resources/docs/api.md
+```
+
**Response**
```json
@@ -413,6 +469,12 @@ curl -X POST http://localhost:1933/api/v1/fs/mv \
}'
```
+**CLI**
+
+```bash
+openviking mv viking://resources/old-project/ viking://resources/new-project/
+```
+
**Response**
```json
@@ -456,6 +518,16 @@ curl -X DELETE "http://localhost:1933/api/v1/fs?uri=viking://resources/old-proje
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+# Delete single file
+openviking rm viking://resources/docs/old.md
+
+# Delete directory recursively
+openviking rm viking://resources/old-project/ --recursive
+```
+
**Response**
```json
@@ -521,6 +593,12 @@ curl -X POST http://localhost:1933/api/v1/relations/link \
}'
```
+**CLI**
+
+```bash
+openviking link viking://resources/docs/auth/ viking://resources/docs/security/ --reason "Security best practices"
+```
+
**Response**
```json
@@ -557,6 +635,12 @@ curl -X GET "http://localhost:1933/api/v1/relations?uri=viking://resources/docs/
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking relations viking://resources/docs/auth/
+```
+
**Response**
```json
@@ -599,6 +683,12 @@ curl -X DELETE http://localhost:1933/api/v1/relations/link \
}'
```
+**CLI**
+
+```bash
+openviking unlink viking://resources/docs/auth/ viking://resources/docs/security/
+```
+
**Response**
```json
diff --git a/docs/en/api/03-filesystem.md b/docs/en/api/03-filesystem.md
index fe123ad1..a4dac8ca 100644
--- a/docs/en/api/03-filesystem.md
+++ b/docs/en/api/03-filesystem.md
@@ -40,6 +40,12 @@ curl -X GET "http://localhost:1933/api/v1/content/abstract?uri=viking://resource
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking abstract viking://resources/docs/
+```
+
**Response**
```json
@@ -87,6 +93,12 @@ curl -X GET "http://localhost:1933/api/v1/content/overview?uri=viking://resource
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking overview viking://resources/docs/
+```
+
**Response**
```json
@@ -134,6 +146,12 @@ curl -X GET "http://localhost:1933/api/v1/content/read?uri=viking://resources/do
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking read viking://resources/docs/api.md
+```
+
**Response**
```json
@@ -208,6 +226,12 @@ curl -X GET "http://localhost:1933/api/v1/fs/ls?uri=viking://resources/&recursiv
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking ls viking://resources/ [--simple] [--recursive]
+```
+
**Response**
```json
@@ -266,6 +290,12 @@ curl -X GET "http://localhost:1933/api/v1/fs/tree?uri=viking://resources/" \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking tree viking://resources/my-project/
+```
+
**Response**
```json
@@ -329,6 +359,12 @@ curl -X GET "http://localhost:1933/api/v1/fs/stat?uri=viking://resources/docs/ap
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking stat viking://resources/my-project/docs/api.md
+```
+
**Response**
```json
@@ -386,6 +422,12 @@ curl -X POST http://localhost:1933/api/v1/fs/mkdir \
}'
```
+**CLI**
+
+```bash
+openviking mkdir viking://resources/new-project/
+```
+
**Response**
```json
@@ -444,6 +486,12 @@ curl -X DELETE "http://localhost:1933/api/v1/fs?uri=viking://resources/old-proje
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking rm viking://resources/old.md [--recursive]
+```
+
**Response**
```json
@@ -501,6 +549,12 @@ curl -X POST http://localhost:1933/api/v1/fs/mv \
}'
```
+**CLI**
+
+```bash
+openviking mv viking://resources/old-name/ viking://resources/new-name/
+```
+
**Response**
```json
@@ -567,6 +621,12 @@ curl -X POST http://localhost:1933/api/v1/search/grep \
}'
```
+**CLI**
+
+```bash
+openviking grep viking://resources/ "authentication" [--ignore-case]
+```
+
**Response**
```json
@@ -636,6 +696,12 @@ curl -X POST http://localhost:1933/api/v1/search/glob \
}'
```
+**CLI**
+
+```bash
+openviking glob "**/*.md" [--uri viking://resources/]
+```
+
**Response**
```json
@@ -722,6 +788,12 @@ curl -X POST http://localhost:1933/api/v1/relations/link \
}'
```
+**CLI**
+
+```bash
+openviking link viking://resources/docs/auth/ viking://resources/docs/security/ --reason "Security best practices"
+```
+
**Response**
```json
@@ -774,6 +846,12 @@ curl -X GET "http://localhost:1933/api/v1/relations?uri=viking://resources/docs/
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking relations viking://resources/docs/auth/
+```
+
**Response**
```json
@@ -832,6 +910,12 @@ curl -X DELETE http://localhost:1933/api/v1/relations/link \
}'
```
+**CLI**
+
+```bash
+openviking unlink viking://resources/docs/auth/ viking://resources/docs/security/
+```
+
**Response**
```json
diff --git a/docs/en/api/04-skills.md b/docs/en/api/04-skills.md
index de2cd10b..de3f0516 100644
--- a/docs/en/api/04-skills.md
+++ b/docs/en/api/04-skills.md
@@ -104,6 +104,12 @@ curl -X POST http://localhost:1933/api/v1/skills \
}'
```
+**CLI**
+
+```bash
+openviking add-skill ./my-skill/ [--wait]
+```
+
**Response**
```json
diff --git a/docs/en/api/05-sessions.md b/docs/en/api/05-sessions.md
index f7170b8d..1c8d266d 100644
--- a/docs/en/api/05-sessions.md
+++ b/docs/en/api/05-sessions.md
@@ -13,7 +13,6 @@ Create a new session.
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| session_id | str | No | None | Session ID. Creates new session with auto-generated ID if None |
-| user | str | No | None | User identifier (HTTP API only) |
**Python SDK**
@@ -39,10 +38,13 @@ POST /api/v1/sessions
```bash
curl -X POST http://localhost:1933/api/v1/sessions \
-H "Content-Type: application/json" \
- -H "X-API-Key: your-key" \
- -d '{
- "user": "alice"
- }'
+ -H "X-API-Key: your-key"
+```
+
+**CLI**
+
+```bash
+openviking session new
```
**Response**
@@ -90,6 +92,12 @@ curl -X GET http://localhost:1933/api/v1/sessions \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking session list
+```
+
**Response**
```json
@@ -142,6 +150,12 @@ curl -X GET http://localhost:1933/api/v1/sessions/a1b2c3d4 \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking session get a1b2c3d4
+```
+
**Response**
```json
@@ -192,6 +206,12 @@ curl -X DELETE http://localhost:1933/api/v1/sessions/a1b2c3d4 \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking session delete a1b2c3d4
+```
+
**Response**
```json
@@ -294,6 +314,12 @@ curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/messages \
}'
```
+**CLI**
+
+```bash
+openviking session add-message a1b2c3d4 --role user --content "How do I authenticate users?"
+```
+
**Response**
```json
@@ -309,15 +335,15 @@ curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/messages \
---
-### compress()
+### commit()
-Compress a session by archiving messages and generating summaries.
+Commit a session by archiving messages and extracting memories.
**Parameters**
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
-| session_id | str | Yes | - | Session ID to compress |
+| session_id | str | Yes | - | Session ID to commit |
**Python SDK**
@@ -341,69 +367,19 @@ client.close()
**HTTP API**
```
-POST /api/v1/sessions/{session_id}/compress
+POST /api/v1/sessions/{session_id}/commit
```
```bash
-curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/compress \
+curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/commit \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key"
```
-**Response**
-
-```json
-{
- "status": "ok",
- "result": {
- "session_id": "a1b2c3d4",
- "status": "compressed",
- "archived": true
- },
- "time": 0.1
-}
-```
-
----
-
-### extract()
-
-Extract memories from a session.
-
-**Parameters**
-
-| Parameter | Type | Required | Default | Description |
-|-----------|------|----------|---------|-------------|
-| session_id | str | Yes | - | Session ID to extract memories from |
-
-**Python SDK**
-
-```python
-import openviking as ov
-
-client = ov.OpenViking(path="./data")
-client.initialize()
-
-session = client.session(session_id="a1b2c3d4")
-session.load()
-
-# Commit includes memory extraction
-result = session.commit()
-print(f"Memories extracted: {result['memories_extracted']}")
-
-client.close()
-```
-
-**HTTP API**
-
-```
-POST /api/v1/sessions/{session_id}/extract
-```
+**CLI**
```bash
-curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/extract \
- -H "Content-Type: application/json" \
- -H "X-API-Key: your-key"
+openviking session commit a1b2c3d4
```
**Response**
@@ -413,7 +389,8 @@ curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/extract \
"status": "ok",
"result": {
"session_id": "a1b2c3d4",
- "memories_extracted": 3
+ "status": "committed",
+ "archived": true
},
"time": 0.1
}
@@ -517,9 +494,8 @@ client.close()
# Step 1: Create session
curl -X POST http://localhost:1933/api/v1/sessions \
-H "Content-Type: application/json" \
- -H "X-API-Key: your-key" \
- -d '{"user": "alice"}'
-# Returns: {"status": "ok", "result": {"session_id": "a1b2c3d4", "user": "alice"}}
+ -H "X-API-Key: your-key"
+# Returns: {"status": "ok", "result": {"session_id": "a1b2c3d4"}}
# Step 2: Add user message
curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/messages \
@@ -539,8 +515,8 @@ curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/messages \
-H "X-API-Key: your-key" \
-d '{"role": "assistant", "content": "Based on the documentation, you can configure embedding..."}'
-# Step 5: Extract memories
-curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/extract \
+# Step 5: Commit session
+curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/commit \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key"
```
diff --git a/docs/en/api/06-retrieval.md b/docs/en/api/06-retrieval.md
index 041fbdfb..8685e915 100644
--- a/docs/en/api/06-retrieval.md
+++ b/docs/en/api/06-retrieval.md
@@ -90,6 +90,12 @@ curl -X POST http://localhost:1933/api/v1/search/find \
}'
```
+**CLI**
+
+```bash
+openviking find "how to authenticate users" [--uri viking://resources/] [--limit 10]
+```
+
**Response**
```json
@@ -234,6 +240,12 @@ curl -X POST http://localhost:1933/api/v1/search/search \
}'
```
+**CLI**
+
+```bash
+openviking search "best practices" [--session-id abc123] [--limit 10]
+```
+
**Response**
```json
@@ -340,6 +352,12 @@ curl -X POST http://localhost:1933/api/v1/search/grep \
}'
```
+**CLI**
+
+```bash
+openviking grep viking://resources/ "authentication" [--ignore-case]
+```
+
**Response**
```json
@@ -409,6 +427,12 @@ curl -X POST http://localhost:1933/api/v1/search/glob \
}'
```
+**CLI**
+
+```bash
+openviking glob "**/*.md" [--uri viking://resources/]
+```
+
**Response**
```json
diff --git a/docs/en/api/07-system.md b/docs/en/api/07-system.md
index 6b705211..acfe80b8 100644
--- a/docs/en/api/07-system.md
+++ b/docs/en/api/07-system.md
@@ -33,6 +33,12 @@ GET /health
curl -X GET http://localhost:1933/health
```
+**CLI**
+
+```bash
+openviking health
+```
+
**Response**
```json
@@ -71,6 +77,12 @@ curl -X GET http://localhost:1933/api/v1/system/status \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking status
+```
+
**Response**
```json
@@ -129,6 +141,12 @@ curl -X POST http://localhost:1933/api/v1/system/wait \
}'
```
+**CLI**
+
+```bash
+openviking wait [--timeout 60]
+```
+
**Response**
```json
@@ -184,6 +202,12 @@ curl -X GET http://localhost:1933/api/v1/observer/queue \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking observer queue
+```
+
**Response**
```json
@@ -231,6 +255,12 @@ curl -X GET http://localhost:1933/api/v1/observer/vikingdb \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking observer vikingdb
+```
+
**Response**
```json
@@ -274,6 +304,12 @@ curl -X GET http://localhost:1933/api/v1/observer/vlm \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking observer vlm
+```
+
**Response**
```json
@@ -323,6 +359,12 @@ curl -X GET http://localhost:1933/api/v1/observer/system \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking observer system
+```
+
**Response**
```json
diff --git a/docs/en/concepts/01-architecture.md b/docs/en/concepts/01-architecture.md
index 3cf7275a..1c88186f 100644
--- a/docs/en/concepts/01-architecture.md
+++ b/docs/en/concepts/01-architecture.md
@@ -27,7 +27,7 @@ OpenViking is a context database designed for AI Agents, unifying all context ty
│ │ (Context │ │ (Session │ │ (Context │ │
│ │ Retrieval) │ │ Management)│ │ Extraction)│ │
│ │ search/find │ │ add/used │ │ Doc parsing │ │
-│ │ Intent │ │ compress │ │ L0/L1/L2 │ │
+│ │ Intent │ │ commit │ │ L0/L1/L2 │ │
│ │ Rerank │ │ commit │ │ Tree build │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
@@ -69,7 +69,7 @@ The Service layer decouples business logic from the transport layer, enabling re
|---------|----------------|-------------|
| **FSService** | File system operations | ls, mkdir, rm, mv, tree, stat, read, abstract, overview, grep, glob |
| **SearchService** | Semantic search | search, find |
-| **SessionService** | Session management | session, sessions, compress, extract, delete |
+| **SessionService** | Session management | session, sessions, commit, delete |
| **ResourceService** | Resource import | add_resource, add_skill, wait_processed |
| **RelationService** | Relation management | relations, link, unlink |
| **PackService** | Import/export | export_ovpack, import_ovpack |
@@ -134,24 +134,9 @@ client = OpenViking(path="./data")
- Uses local vector index
- Singleton pattern
-### Service Mode
-
-For production distributed deployment:
-
-```python
-client = OpenViking(
- vectordb_url="http://vectordb:5000",
- agfs_url="http://agfs:8080"
-)
-```
-
-- Connects to remote services
-- Supports multiple concurrent instances
-- Independently scalable
-
### HTTP Mode
-For team sharing and cross-language integration:
+For team sharing, production deployment, and cross-language integration:
```python
# Python SDK connects to OpenViking Server
diff --git a/docs/en/faq/faq.md b/docs/en/faq/faq.md
index d62d032e..f5b41558 100644
--- a/docs/en/faq/faq.md
+++ b/docs/en/faq/faq.md
@@ -100,7 +100,7 @@ Create an `~/.openviking/ov.conf` configuration file in your project directory:
}
```
-Configuration priority: Constructor parameters > Config object > Config file > Environment variables > Default values
+Config files at the default path `~/.openviking/ov.conf` are loaded automatically; you can also specify a different path via the `OPENVIKING_CONFIG_FILE` environment variable or `--config` flag. See [Configuration Guide](../guides/01-configuration.md) for details.
### What Embedding providers are supported?
@@ -119,17 +119,17 @@ Supports Dense, Sparse, and Hybrid embedding modes.
```python
import openviking as ov
-# Async client (recommended)
+# Async client - embedded mode (recommended)
client = ov.AsyncOpenViking(path="./my_data")
await client.initialize()
-# Or use a config object
-from openviking.utils.config import OpenVikingConfig
-config = OpenVikingConfig(...)
-client = ov.AsyncOpenViking(config=config)
+# Async client - HTTP client mode
+client = ov.AsyncOpenViking(url="http://localhost:1933", api_key="your-key")
await client.initialize()
```
+The SDK constructor only accepts `url`, `api_key`, `path`, and `user` parameters. Other configuration (embedding, vlm, etc.) is managed through the `ov.conf` config file.
+
### What file formats are supported?
| Type | Supported Formats |
@@ -362,11 +362,8 @@ This strategy finds semantically matching fragments while understanding the comp
# Embedded mode
client = ov.AsyncOpenViking(path="./data")
-# Service mode
-client = ov.AsyncOpenViking(
- vectordb_url="http://vectordb:5000",
- agfs_url="http://agfs:8080"
-)
+# HTTP client mode (connects to a remote server)
+client = ov.AsyncOpenViking(url="http://localhost:1933", api_key="your-key")
```
### Is OpenViking open source?
diff --git a/docs/en/getting-started/02-quickstart.md b/docs/en/getting-started/02-quickstart.md
index 1fa0bfa4..373751c6 100644
--- a/docs/en/getting-started/02-quickstart.md
+++ b/docs/en/getting-started/02-quickstart.md
@@ -105,12 +105,14 @@ Create a configuration file `~/.openviking/ov.conf`:
-### Set Environment Variable
+### Environment Variables
-After creating the configuration file, set the environment variable to point to it:
+When the config file is at the default path `~/.openviking/ov.conf`, no additional setup is needed — OpenViking loads it automatically.
+
+If the config file is at a different location, specify it via environment variable:
```bash
-export OPENVIKING_CONFIG_FILE=~/.openviking/ov.conf
+export OPENVIKING_CONFIG_FILE=/path/to/your/ov.conf
```
## Run Your First Example
diff --git a/docs/en/getting-started/03-quickstart-server.md b/docs/en/getting-started/03-quickstart-server.md
index 751ce541..3876045e 100644
--- a/docs/en/getting-started/03-quickstart-server.md
+++ b/docs/en/getting-started/03-quickstart-server.md
@@ -9,8 +9,17 @@ Run OpenViking as a standalone HTTP server and connect from any client.
## Start the Server
+Make sure you have a config file at `~/.openviking/ov.conf` with your model and storage settings (see [Configuration](../guides/01-configuration.md)).
+
```bash
-python -m openviking serve --path ./data
+# Config file at default path ~/.openviking/ov.conf — just start
+python -m openviking serve
+
+# Config file at a different location — specify with --config
+python -m openviking serve --config /path/to/ov.conf
+
+# Override host/port
+python -m openviking serve --port 8000
```
You should see:
@@ -34,18 +43,12 @@ import openviking as ov
client = ov.OpenViking(url="http://localhost:1933")
```
-Or use environment variables:
-
-```bash
-export OPENVIKING_URL="http://localhost:1933"
-export OPENVIKING_API_KEY="your-key" # if authentication is enabled
-```
+If the server has authentication enabled, pass the API key:
```python
import openviking as ov
-# url and api_key are read from environment variables automatically
-client = ov.OpenViking()
+client = ov.OpenViking(url="http://localhost:1933", api_key="your-key")
```
**Full example:**
@@ -76,6 +79,29 @@ finally:
client.close()
```
+## Connect with CLI
+
+Create a CLI config file `~/.openviking/ovcli.conf` that points to your server:
+
+```json
+{
+ "url": "http://localhost:1933"
+}
+```
+
+Then use CLI commands to interact with the server:
+
+```bash
+python -m openviking health
+python -m openviking find "what is openviking"
+```
+
+If the config file is at a different location, specify it via environment variable:
+
+```bash
+export OPENVIKING_CLI_CONFIG_FILE=/path/to/ovcli.conf
+```
+
## Connect with curl
```bash
diff --git a/docs/en/guides/01-configuration.md b/docs/en/guides/01-configuration.md
index ca968677..6049f0a1 100644
--- a/docs/en/guides/01-configuration.md
+++ b/docs/en/guides/01-configuration.md
@@ -43,7 +43,9 @@ Create `~/.openviking/ov.conf` in your project directory:
### embedding
-Embedding model configuration for vector search.
+Embedding model configuration for vector search, supporting dense, sparse, and hybrid modes.
+
+#### Dense Embedding
```json
{
@@ -79,6 +81,81 @@ Embedding model configuration for vector search.
With `input: "multimodal"`, OpenViking can embed text, images (PNG, JPG, etc.), and mixed content.
+**Supported providers:**
+- `openai`: OpenAI Embedding API
+- `volcengine`: Volcengine Embedding API
+- `vikingdb`: VikingDB Embedding API
+
+**vikingdb provider example:**
+
+```json
+{
+ "embedding": {
+ "dense": {
+ "provider": "vikingdb",
+ "model": "bge_large_zh",
+ "ak": "your-access-key",
+ "sk": "your-secret-key",
+ "region": "cn-beijing",
+ "dimension": 1024
+ }
+ }
+}
+```
+
+#### Sparse Embedding
+
+```json
+{
+ "embedding": {
+ "sparse": {
+ "provider": "volcengine",
+ "api_key": "your-api-key",
+ "model": "bm25-sparse-v1"
+ }
+ }
+}
+```
+
+#### Hybrid Embedding
+
+Two approaches are supported:
+
+**Option 1: Single hybrid model**
+
+```json
+{
+ "embedding": {
+ "hybrid": {
+ "provider": "volcengine",
+ "api_key": "your-api-key",
+ "model": "doubao-embedding-hybrid",
+ "dimension": 1024
+ }
+ }
+}
+```
+
+**Option 2: Combine dense + sparse**
+
+```json
+{
+ "embedding": {
+ "dense": {
+ "provider": "volcengine",
+ "api_key": "your-api-key",
+ "model": "doubao-embedding-vision-250615",
+ "dimension": 1024
+ },
+ "sparse": {
+ "provider": "volcengine",
+ "api_key": "your-api-key",
+ "model": "bm25-sparse-v1"
+ }
+ }
+}
+```
+
### vlm
Vision Language Model for semantic extraction (L0/L1 generation).
@@ -157,58 +234,77 @@ Storage backend configuration.
}
```
-## Environment Variables
+## Config Files
+
+OpenViking uses two config files:
+
+| File | Purpose | Default Path |
+|------|---------|-------------|
+| `ov.conf` | SDK embedded mode + server config | `~/.openviking/ov.conf` |
+| `ovcli.conf` | CLI connection to remote server | `~/.openviking/ovcli.conf` |
+
+When config files are at the default path, OpenViking loads them automatically — no additional setup needed.
+
+If config files are at a different location, there are two ways to specify:
```bash
-export VOLCENGINE_API_KEY="your-api-key"
-export OPENVIKING_DATA_PATH="./data"
+# Option 1: Environment variable
+export OPENVIKING_CONFIG_FILE=/path/to/ov.conf
+export OPENVIKING_CLI_CONFIG_FILE=/path/to/ovcli.conf
+
+# Option 2: Command-line argument (serve command only)
+python -m openviking serve --config /path/to/ov.conf
+```
+
+### ov.conf
+
+The config sections documented above (embedding, vlm, rerank, storage) all belong to `ov.conf`. SDK embedded mode and server share this file.
+
+### ovcli.conf
+
+Config file for CLI to connect to a remote server:
+
+```json
+{
+ "url": "http://localhost:1933",
+ "api_key": "your-secret-key",
+ "output": "table"
+}
```
-## Configuration Priority
-
-1. Constructor parameters (highest)
-2. Config object
-3. Configuration file (`~/.openviking/ov.conf`)
-4. Environment variables
-5. Default values (lowest)
-
-## Programmatic Configuration
-
-```python
-from openviking.utils.config import (
- OpenVikingConfig,
- StorageConfig,
- AGFSConfig,
- VectorDBBackendConfig,
- EmbeddingConfig,
- DenseEmbeddingConfig
-)
-
-config = OpenVikingConfig(
- storage=StorageConfig(
- agfs=AGFSConfig(
- backend="local",
- path="./custom_data",
- ),
- vectordb=VectorDBBackendConfig(
- backend="local",
- path="./custom_data",
- )
- ),
- embedding=EmbeddingConfig(
- dense=DenseEmbeddingConfig(
- provider="volcengine",
- api_key="your-api-key",
- model="doubao-embedding-vision-250615",
- dimension=1024
- )
- )
-)
-
-client = ov.AsyncOpenViking(config=config)
+| Field | Description | Default |
+|-------|-------------|---------|
+| `url` | Server address | (required) |
+| `api_key` | API key for authentication | `null` (no auth) |
+| `output` | Default output format: `"table"` or `"json"` | `"table"` |
+
+See [Deployment](./03-deployment.md) for details.
+
+## server Section
+
+When running OpenViking as an HTTP service, add a `server` section to `ov.conf`:
+
+```json
+{
+ "server": {
+ "host": "0.0.0.0",
+ "port": 1933,
+ "api_key": "your-secret-key",
+ "cors_origins": ["*"]
+ }
+}
```
-## Full Configuration Schema
+| Field | Type | Description | Default |
+|-------|------|-------------|---------|
+| `host` | str | Bind address | `0.0.0.0` |
+| `port` | int | Bind port | `1933` |
+| `api_key` | str | API Key auth, disabled if not set | `null` |
+| `cors_origins` | list | Allowed CORS origins | `["*"]` |
+
+For startup and deployment details see [Deployment](./03-deployment.md), for authentication see [Authentication](./04-authentication.md).
+
+## Full Schema
```json
{
@@ -245,41 +341,18 @@ client = ov.AsyncOpenViking(config=config)
"url": "string"
}
},
- "user": "string"
-}
-```
-
-Notes:
-- `storage.vectordb.sparse_weight` controls hybrid (dense + sparse) indexing/search. It only takes effect when you use a hybrid index; set it > 0 to enable sparse signals.
-
-## Server Configuration
-
-When running OpenViking as an HTTP server, the server reads its configuration from the same JSON config file (via `--config` or `OPENVIKING_CONFIG_FILE`):
-
-```json
-{
"server": {
"host": "0.0.0.0",
"port": 1933,
- "api_key": "your-secret-key",
+ "api_key": "string",
"cors_origins": ["*"]
},
- "storage": {
- "path": "/data/openviking"
- }
+ "user": "string"
}
```
-Server configuration can also be set via environment variables:
-
-| Variable | Description |
-|----------|-------------|
-| `OPENVIKING_HOST` | Server host |
-| `OPENVIKING_PORT` | Server port |
-| `OPENVIKING_API_KEY` | API key for authentication |
-| `OPENVIKING_PATH` | Storage path |
-
-See [Server Deployment](./03-deployment.md) for full details.
+Notes:
+- `storage.vectordb.sparse_weight` controls hybrid (dense + sparse) indexing/search. It only takes effect when you use a hybrid index; set it > 0 to enable sparse signals.
## Troubleshooting
diff --git a/docs/en/guides/03-deployment.md b/docs/en/guides/03-deployment.md
index f21a279c..b44547ec 100644
--- a/docs/en/guides/03-deployment.md
+++ b/docs/en/guides/03-deployment.md
@@ -5,8 +5,11 @@ OpenViking can run as a standalone HTTP server, allowing multiple clients to con
## Quick Start
```bash
-# Start server with local storage
-python -m openviking serve --path ./data
+# Start server (reads ~/.openviking/ov.conf by default)
+python -m openviking serve
+
+# Or specify a custom config path
+python -m openviking serve --config /path/to/ov.conf
# Verify it's running
curl http://localhost:1933/health
@@ -17,43 +20,28 @@ curl http://localhost:1933/health
| Option | Description | Default |
|--------|-------------|---------|
+| `--config` | Path to ov.conf file | `~/.openviking/ov.conf` |
| `--host` | Host to bind to | `0.0.0.0` |
| `--port` | Port to bind to | `1933` |
-| `--path` | Local storage path (embedded mode) | None |
-| `--vectordb-url` | Remote VectorDB URL (service mode) | None |
-| `--agfs-url` | Remote AGFS URL (service mode) | None |
-| `--api-key` | API key for authentication | None (auth disabled) |
-| `--config` | Path to config file | `OPENVIKING_CONFIG_FILE` env var |
**Examples**
```bash
-# Embedded mode with custom port
-python -m openviking serve --path ./data --port 8000
+# With default config
+python -m openviking serve
-# With authentication
-python -m openviking serve --path ./data --api-key "your-secret-key"
+# With custom port
+python -m openviking serve --port 8000
-# Service mode (remote storage)
-python -m openviking serve \
- --vectordb-url http://vectordb:8000 \
- --agfs-url http://agfs:1833
+# With custom config, host, and port
+python -m openviking serve --config /path/to/ov.conf --host 127.0.0.1 --port 8000
```
## Configuration
-### Config File
-
-Server configuration is read from the JSON config file specified by `--config` or the `OPENVIKING_CONFIG_FILE` environment variable (the same file used for `OpenVikingConfig`):
-
-```bash
-python -m openviking serve --config ~/.openviking/ov.conf
-# or
-export OPENVIKING_CONFIG_FILE=~/.openviking/ov.conf
-python -m openviking serve
-```
+The server reads all configuration from `ov.conf`. See [Configuration Guide](./01-configuration.md) for full details on config file format.
-The `server` section in the config file:
+The `server` section in `ov.conf` controls server behavior:
```json
{
@@ -64,48 +52,46 @@ The `server` section in the config file:
"cors_origins": ["*"]
},
"storage": {
- "path": "/data/openviking"
+ "agfs": { "backend": "local", "path": "/data/openviking" },
+ "vectordb": { "backend": "local", "path": "/data/openviking" }
}
}
```
-### Environment Variables
-
-| Variable | Description | Example |
-|----------|-------------|---------|
-| `OPENVIKING_HOST` | Server host | `0.0.0.0` |
-| `OPENVIKING_PORT` | Server port | `1933` |
-| `OPENVIKING_API_KEY` | API key | `sk-xxx` |
-| `OPENVIKING_PATH` | Storage path | `./data` |
-| `OPENVIKING_VECTORDB_URL` | Remote VectorDB URL | `http://vectordb:8000` |
-| `OPENVIKING_AGFS_URL` | Remote AGFS URL | `http://agfs:1833` |
-
-### Configuration Priority
-
-From highest to lowest:
-
-1. **Command line arguments** (`--port 8000`)
-2. **Environment variables** (`OPENVIKING_PORT=8000`)
-3. **Config file** (`OPENVIKING_CONFIG_FILE`)
-
## Deployment Modes
### Standalone (Embedded Storage)
-Server manages local AGFS and VectorDB:
+Server manages local AGFS and VectorDB. Configure the storage path in `ov.conf`:
+
+```json
+{
+ "storage": {
+ "agfs": { "backend": "local", "path": "/data/openviking" },
+ "vectordb": { "backend": "local", "path": "/data/openviking" }
+ }
+}
+```
```bash
-python -m openviking serve --path ./data
+python -m openviking serve
```
### Hybrid (Remote Storage)
-Server connects to remote AGFS and VectorDB services:
+Server connects to remote AGFS and VectorDB services. Configure remote URLs in `ov.conf`:
+
+```json
+{
+ "storage": {
+ "agfs": { "backend": "remote", "url": "http://agfs:1833" },
+ "vectordb": { "backend": "remote", "url": "http://vectordb:8000" }
+ }
+}
+```
```bash
-python -m openviking serve \
- --vectordb-url http://vectordb:8000 \
- --agfs-url http://agfs:1833
+python -m openviking serve
```
## Connecting Clients
@@ -122,19 +108,27 @@ results = client.find("how to use openviking")
client.close()
```
-Or use environment variables:
+### CLI
+
+The CLI reads connection settings from `ovcli.conf`. Create `~/.openviking/ovcli.conf`:
+
+```json
+{
+ "url": "http://localhost:1933",
+ "api_key": "your-key"
+}
+```
+
+Or set the config path via environment variable:
```bash
-export OPENVIKING_URL="http://localhost:1933"
-export OPENVIKING_API_KEY="your-key"
+export OPENVIKING_CLI_CONFIG_FILE=/path/to/ovcli.conf
```
-```python
-import openviking as ov
+Then use the CLI:
-# url and api_key are read from environment variables automatically
-client = ov.OpenViking()
-client.initialize()
+```bash
+python -m openviking ls viking://resources/
```
### curl
diff --git a/docs/en/guides/04-authentication.md b/docs/en/guides/04-authentication.md
index 1fe5ae10..b91e6433 100644
--- a/docs/en/guides/04-authentication.md
+++ b/docs/en/guides/04-authentication.md
@@ -6,20 +6,7 @@ OpenViking Server supports API key authentication to secure access.
### Setting Up (Server Side)
-**Option 1: Command line**
-
-```bash
-python -m openviking serve --path ./data --api-key "your-secret-key"
-```
-
-**Option 2: Environment variable**
-
-```bash
-export OPENVIKING_API_KEY="your-secret-key"
-python -m openviking serve --path ./data
-```
-
-**Option 3: Config file** (via `OPENVIKING_CONFIG_FILE`)
+Configure the API key in the `server` section of `ov.conf` (`~/.openviking/ov.conf`):
```json
{
@@ -29,6 +16,12 @@ python -m openviking serve --path ./data
}
```
+Then start the server:
+
+```bash
+python -m openviking serve
+```
+
### Using API Key (Client Side)
OpenViking accepts API keys via two headers:
@@ -58,27 +51,33 @@ client = ov.OpenViking(
)
```
-Or use the `OPENVIKING_API_KEY` environment variable:
+**CLI (via ovcli.conf)**
-```bash
-export OPENVIKING_URL="http://localhost:1933"
-export OPENVIKING_API_KEY="your-secret-key"
-```
+Configure the API key in `~/.openviking/ovcli.conf`:
-```python
-import openviking as ov
-
-# api_key is read from OPENVIKING_API_KEY automatically
-client = ov.OpenViking()
+```json
+{
+ "url": "http://localhost:1933",
+ "api_key": "your-secret-key"
+}
```
## Development Mode
-When no API key is configured, authentication is disabled. All requests are accepted without credentials.
+When no API key is configured in `ov.conf`, authentication is disabled. All requests are accepted without credentials.
+
+```json
+{
+ "server": {
+ "host": "0.0.0.0",
+ "port": 1933
+ }
+}
+```
```bash
-# No --api-key flag = auth disabled
-python -m openviking serve --path ./data
+# No api_key in ov.conf = auth disabled
+python -m openviking serve
```
## Unauthenticated Endpoints
diff --git a/docs/zh/api/01-overview.md b/docs/zh/api/01-overview.md
index 56685b3b..560afc6b 100644
--- a/docs/zh/api/01-overview.md
+++ b/docs/zh/api/01-overview.md
@@ -6,11 +6,11 @@
OpenViking 支持三种连接模式:
-| 模式 | 使用场景 | 单例 |
+| 模式 | 使用场景 | 说明 |
|------|----------|------|
-| **嵌入式** | 本地开发,单进程 | 是 |
-| **服务模式** | 远程 VectorDB + AGFS 基础设施 | 否 |
-| **HTTP** | 连接 OpenViking Server | 否 |
+| **嵌入式** | 本地开发,单进程 | 本地运行,数据存储在本地 |
+| **HTTP** | 连接 OpenViking Server | 通过 HTTP API 连接远程服务 |
+| **CLI** | Shell 脚本、Agent 工具调用 | 通过 CLI 命令连接服务端 |
### 嵌入式模式
@@ -21,16 +21,6 @@ client = ov.OpenViking(path="./data")
client.initialize()
```
-### 服务模式
-
-```python
-client = ov.OpenViking(
- vectordb_url="http://vectordb.example.com:8000",
- agfs_url="http://agfs.example.com:1833",
-)
-client.initialize()
-```
-
### HTTP 模式
```python
@@ -48,6 +38,42 @@ curl http://localhost:1933/api/v1/fs/ls?uri=viking:// \
-H "X-API-Key: your-key"
```
+### CLI 模式
+
+CLI 连接到 OpenViking 服务端,将所有操作暴露为 Shell 命令。
+
+**配置**
+
+创建 `~/.openviking/ovcli.conf`(或设置 `OPENVIKING_CLI_CONFIG_FILE` 环境变量):
+
+```json
+{
+ "url": "http://localhost:1933",
+ "api_key": "your-key"
+}
+```
+
+**基本用法**
+
+```bash
+openviking [全局选项] [参数] [命令选项]
+```
+
+**全局选项**(必须放在命令名之前)
+
+| 选项 | 说明 |
+|------|------|
+| `--output`, `-o` | 输出格式:`table`(默认)、`json` |
+| `--json` | 紧凑 JSON + `{ok, result}` 包装(用于脚本) |
+| `--version` | 显示 CLI 版本 |
+
+示例:
+
+```bash
+openviking --json ls viking://resources/
+openviking -o json ls viking://resources/
+```
+
## 客户端生命周期
```python
@@ -95,6 +121,66 @@ client.close() # Release resources
}
```
+## CLI 输出格式
+
+### Table 模式(默认)
+
+列表数据渲染为表格,非列表数据 fallback 到格式化 JSON:
+
+```bash
+openviking ls viking://resources/
+# name size mode isDir uri
+# .abstract.md 100 420 False viking://resources/.abstract.md
+```
+
+### JSON 模式(`--output json`)
+
+所有命令输出格式化 JSON,与 API 响应的 `result` 结构一致:
+
+```bash
+openviking -o json ls viking://resources/
+# [{ "name": "...", "size": 100, ... }, ...]
+```
+
+可在 `ovcli.conf` 中设置默认输出格式:
+
+```json
+{
+ "url": "http://localhost:1933",
+ "output": "json"
+}
+```
+
+### 脚本模式(`--json`)
+
+紧凑 JSON + 状态包装,适用于脚本。覆盖 `--output`:
+
+**成功**
+
+```json
+{"ok": true, "result": ...}
+```
+
+**错误**
+
+```json
+{"ok": false, "error": {"code": "NOT_FOUND", "message": "Resource not found", "details": {}}}
+```
+
+### 特殊情况
+
+- **字符串结果**(`read`、`abstract`、`overview`):直接打印原文
+- **None 结果**(`mkdir`、`rm`、`mv`):无输出
+
+### 退出码
+
+| 退出码 | 含义 |
+|--------|------|
+| 0 | 成功 |
+| 1 | 一般错误 |
+| 2 | 配置错误 |
+| 3 | 连接错误 |
+
## 错误码
| 错误码 | HTTP 状态码 | 说明 |
@@ -179,8 +265,7 @@ client.close() # Release resources
| GET | `/api/v1/sessions` | 列出会话 |
| GET | `/api/v1/sessions/{id}` | 获取会话 |
| DELETE | `/api/v1/sessions/{id}` | 删除会话 |
-| POST | `/api/v1/sessions/{id}/compress` | 压缩会话 |
-| POST | `/api/v1/sessions/{id}/extract` | 提取记忆 |
+| POST | `/api/v1/sessions/{id}/commit` | 提交会话 |
| POST | `/api/v1/sessions/{id}/messages` | 添加消息 |
### Observer
diff --git a/docs/zh/api/02-resources.md b/docs/zh/api/02-resources.md
index 00d18a27..5887e6fc 100644
--- a/docs/zh/api/02-resources.md
+++ b/docs/zh/api/02-resources.md
@@ -80,6 +80,12 @@ curl -X POST http://localhost:1933/api/v1/resources \
}'
```
+**CLI**
+
+```bash
+openviking add-resource ./documents/guide.md --reason "User guide documentation"
+```
+
**响应**
```json
@@ -122,6 +128,12 @@ curl -X POST http://localhost:1933/api/v1/resources \
}'
```
+**CLI**
+
+```bash
+openviking add-resource https://example.com/api-docs.md --to viking://resources/external/ --reason "External API documentation"
+```
+
**示例:等待处理完成**
**Python SDK**
@@ -156,6 +168,12 @@ curl -X POST http://localhost:1933/api/v1/system/wait \
-d '{}'
```
+**CLI**
+
+```bash
+openviking add-resource ./documents/guide.md --wait
+```
+
---
### export_ovpack()
@@ -202,6 +220,12 @@ curl -X POST http://localhost:1933/api/v1/pack/export \
}'
```
+**CLI**
+
+```bash
+openviking export viking://resources/my-project/ ./exports/my-project.ovpack
+```
+
**响应**
```json
@@ -267,6 +291,12 @@ curl -X POST http://localhost:1933/api/v1/pack/import \
}'
```
+**CLI**
+
+```bash
+openviking import ./exports/my-project.ovpack viking://resources/imported/ --force
+```
+
**响应**
```json
@@ -324,6 +354,19 @@ curl -X GET "http://localhost:1933/api/v1/fs/ls?uri=viking://resources/&recursiv
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+# 列出所有资源
+openviking ls viking://resources/
+
+# 简单路径列表
+openviking ls viking://resources/ --simple
+
+# 递归列出
+openviking ls viking://resources/ --recursive
+```
+
**响应**
```json
@@ -374,6 +417,19 @@ curl -X GET "http://localhost:1933/api/v1/content/read?uri=viking://resources/do
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+# L0:摘要
+openviking abstract viking://resources/docs/
+
+# L1:概览
+openviking overview viking://resources/docs/
+
+# L2:完整内容
+openviking read viking://resources/docs/api.md
+```
+
**响应**
```json
@@ -413,6 +469,12 @@ curl -X POST http://localhost:1933/api/v1/fs/mv \
}'
```
+**CLI**
+
+```bash
+openviking mv viking://resources/old-project/ viking://resources/new-project/
+```
+
**响应**
```json
@@ -456,6 +518,16 @@ curl -X DELETE "http://localhost:1933/api/v1/fs?uri=viking://resources/old-proje
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+# 删除单个文件
+openviking rm viking://resources/docs/old.md
+
+# 递归删除目录
+openviking rm viking://resources/old-project/ --recursive
+```
+
**响应**
```json
@@ -521,6 +593,12 @@ curl -X POST http://localhost:1933/api/v1/relations/link \
}'
```
+**CLI**
+
+```bash
+openviking link viking://resources/docs/auth/ viking://resources/docs/security/ --reason "Security best practices"
+```
+
**响应**
```json
@@ -557,6 +635,12 @@ curl -X GET "http://localhost:1933/api/v1/relations?uri=viking://resources/docs/
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking relations viking://resources/docs/auth/
+```
+
**响应**
```json
@@ -599,6 +683,12 @@ curl -X DELETE http://localhost:1933/api/v1/relations/link \
}'
```
+**CLI**
+
+```bash
+openviking unlink viking://resources/docs/auth/ viking://resources/docs/security/
+```
+
**响应**
```json
diff --git a/docs/zh/api/03-filesystem.md b/docs/zh/api/03-filesystem.md
index 771c750b..871cc8ed 100644
--- a/docs/zh/api/03-filesystem.md
+++ b/docs/zh/api/03-filesystem.md
@@ -40,6 +40,12 @@ curl -X GET "http://localhost:1933/api/v1/content/abstract?uri=viking://resource
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking abstract viking://resources/docs/
+```
+
**响应**
```json
@@ -87,6 +93,12 @@ curl -X GET "http://localhost:1933/api/v1/content/overview?uri=viking://resource
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking overview viking://resources/docs/
+```
+
**响应**
```json
@@ -134,6 +146,12 @@ curl -X GET "http://localhost:1933/api/v1/content/read?uri=viking://resources/do
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking read viking://resources/docs/api.md
+```
+
**响应**
```json
@@ -208,6 +226,12 @@ curl -X GET "http://localhost:1933/api/v1/fs/ls?uri=viking://resources/&recursiv
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking ls viking://resources/ [--simple] [--recursive]
+```
+
**响应**
```json
@@ -266,6 +290,12 @@ curl -X GET "http://localhost:1933/api/v1/fs/tree?uri=viking://resources/" \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking tree viking://resources/my-project/
+```
+
**响应**
```json
@@ -329,6 +359,12 @@ curl -X GET "http://localhost:1933/api/v1/fs/stat?uri=viking://resources/docs/ap
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking stat viking://resources/my-project/docs/api.md
+```
+
**响应**
```json
@@ -386,6 +422,12 @@ curl -X POST http://localhost:1933/api/v1/fs/mkdir \
}'
```
+**CLI**
+
+```bash
+openviking mkdir viking://resources/new-project/
+```
+
**响应**
```json
@@ -444,6 +486,12 @@ curl -X DELETE "http://localhost:1933/api/v1/fs?uri=viking://resources/old-proje
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking rm viking://resources/old.md [--recursive]
+```
+
**响应**
```json
@@ -501,6 +549,12 @@ curl -X POST http://localhost:1933/api/v1/fs/mv \
}'
```
+**CLI**
+
+```bash
+openviking mv viking://resources/old-name/ viking://resources/new-name/
+```
+
**响应**
```json
@@ -567,6 +621,12 @@ curl -X POST http://localhost:1933/api/v1/search/grep \
}'
```
+**CLI**
+
+```bash
+openviking grep viking://resources/ "authentication" [--ignore-case]
+```
+
**响应**
```json
@@ -636,6 +696,12 @@ curl -X POST http://localhost:1933/api/v1/search/glob \
}'
```
+**CLI**
+
+```bash
+openviking glob "**/*.md" [--uri viking://resources/]
+```
+
**响应**
```json
@@ -722,6 +788,12 @@ curl -X POST http://localhost:1933/api/v1/relations/link \
}'
```
+**CLI**
+
+```bash
+openviking link viking://resources/docs/auth/ viking://resources/docs/security/ --reason "Security best practices"
+```
+
**响应**
```json
@@ -774,6 +846,12 @@ curl -X GET "http://localhost:1933/api/v1/relations?uri=viking://resources/docs/
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking relations viking://resources/docs/auth/
+```
+
**响应**
```json
@@ -832,6 +910,12 @@ curl -X DELETE http://localhost:1933/api/v1/relations/link \
}'
```
+**CLI**
+
+```bash
+openviking unlink viking://resources/docs/auth/ viking://resources/docs/security/
+```
+
**响应**
```json
diff --git a/docs/zh/api/04-skills.md b/docs/zh/api/04-skills.md
index bad2afbe..cc7ca644 100644
--- a/docs/zh/api/04-skills.md
+++ b/docs/zh/api/04-skills.md
@@ -104,6 +104,12 @@ curl -X POST http://localhost:1933/api/v1/skills \
}'
```
+**CLI**
+
+```bash
+openviking add-skill ./my-skill/ [--wait]
+```
+
**响应**
```json
diff --git a/docs/zh/api/05-sessions.md b/docs/zh/api/05-sessions.md
index be9e3ca2..8c64e074 100644
--- a/docs/zh/api/05-sessions.md
+++ b/docs/zh/api/05-sessions.md
@@ -13,7 +13,6 @@
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| session_id | str | 否 | None | 会话 ID。如果为 None,则创建一个自动生成 ID 的新会话 |
-| user | str | 否 | None | 用户标识符(仅 HTTP API) |
**Python SDK**
@@ -39,10 +38,13 @@ POST /api/v1/sessions
```bash
curl -X POST http://localhost:1933/api/v1/sessions \
-H "Content-Type: application/json" \
- -H "X-API-Key: your-key" \
- -d '{
- "user": "alice"
- }'
+ -H "X-API-Key: your-key"
+```
+
+**CLI**
+
+```bash
+openviking session new
```
**响应**
@@ -90,6 +92,12 @@ curl -X GET http://localhost:1933/api/v1/sessions \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking session list
+```
+
**响应**
```json
@@ -142,6 +150,12 @@ curl -X GET http://localhost:1933/api/v1/sessions/a1b2c3d4 \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking session get a1b2c3d4
+```
+
**响应**
```json
@@ -192,6 +206,12 @@ curl -X DELETE http://localhost:1933/api/v1/sessions/a1b2c3d4 \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking session delete a1b2c3d4
+```
+
**响应**
```json
@@ -294,6 +314,12 @@ curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/messages \
}'
```
+**CLI**
+
+```bash
+openviking session add-message a1b2c3d4 --role user --content "How do I authenticate users?"
+```
+
**响应**
```json
@@ -309,15 +335,15 @@ curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/messages \
---
-### compress()
+### commit()
-压缩会话,归档消息并生成摘要。
+提交会话,归档消息并提取记忆。
**参数**
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
-| session_id | str | 是 | - | 要压缩的会话 ID |
+| session_id | str | 是 | - | 要提交的会话 ID |
**Python SDK**
@@ -341,69 +367,19 @@ client.close()
**HTTP API**
```
-POST /api/v1/sessions/{session_id}/compress
+POST /api/v1/sessions/{session_id}/commit
```
```bash
-curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/compress \
+curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/commit \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key"
```
-**响应**
-
-```json
-{
- "status": "ok",
- "result": {
- "session_id": "a1b2c3d4",
- "status": "compressed",
- "archived": true
- },
- "time": 0.1
-}
-```
-
----
-
-### extract()
-
-从会话中提取记忆。
-
-**参数**
-
-| 参数 | 类型 | 必填 | 默认值 | 说明 |
-|------|------|------|--------|------|
-| session_id | str | 是 | - | 要提取记忆的会话 ID |
-
-**Python SDK**
-
-```python
-import openviking as ov
-
-client = ov.OpenViking(path="./data")
-client.initialize()
-
-session = client.session(session_id="a1b2c3d4")
-session.load()
-
-# commit 包含记忆提取
-result = session.commit()
-print(f"Memories extracted: {result['memories_extracted']}")
-
-client.close()
-```
-
-**HTTP API**
-
-```
-POST /api/v1/sessions/{session_id}/extract
-```
+**CLI**
```bash
-curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/extract \
- -H "Content-Type: application/json" \
- -H "X-API-Key: your-key"
+openviking session commit a1b2c3d4
```
**响应**
@@ -413,7 +389,8 @@ curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/extract \
"status": "ok",
"result": {
"session_id": "a1b2c3d4",
- "memories_extracted": 3
+ "status": "committed",
+ "archived": true
},
"time": 0.1
}
@@ -517,9 +494,8 @@ client.close()
# 步骤 1:创建会话
curl -X POST http://localhost:1933/api/v1/sessions \
-H "Content-Type: application/json" \
- -H "X-API-Key: your-key" \
- -d '{"user": "alice"}'
-# 返回:{"status": "ok", "result": {"session_id": "a1b2c3d4", "user": "alice"}}
+ -H "X-API-Key: your-key"
+# 返回:{"status": "ok", "result": {"session_id": "a1b2c3d4"}}
# 步骤 2:添加用户消息
curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/messages \
@@ -539,8 +515,8 @@ curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/messages \
-H "X-API-Key: your-key" \
-d '{"role": "assistant", "content": "Based on the documentation, you can configure embedding..."}'
-# 步骤 5:提取记忆
-curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/extract \
+# 步骤 5:提交会话
+curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/commit \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key"
```
diff --git a/docs/zh/api/06-retrieval.md b/docs/zh/api/06-retrieval.md
index b6329ce3..c7adee11 100644
--- a/docs/zh/api/06-retrieval.md
+++ b/docs/zh/api/06-retrieval.md
@@ -90,6 +90,12 @@ curl -X POST http://localhost:1933/api/v1/search/find \
}'
```
+**CLI**
+
+```bash
+openviking find "how to authenticate users" [--uri viking://resources/] [--limit 10]
+```
+
**响应**
```json
@@ -234,6 +240,12 @@ curl -X POST http://localhost:1933/api/v1/search/search \
}'
```
+**CLI**
+
+```bash
+openviking search "best practices" [--session-id abc123] [--limit 10]
+```
+
**响应**
```json
@@ -340,6 +352,12 @@ curl -X POST http://localhost:1933/api/v1/search/grep \
}'
```
+**CLI**
+
+```bash
+openviking grep viking://resources/ "authentication" [--ignore-case]
+```
+
**响应**
```json
@@ -409,6 +427,12 @@ curl -X POST http://localhost:1933/api/v1/search/glob \
}'
```
+**CLI**
+
+```bash
+openviking glob "**/*.md" [--uri viking://resources/]
+```
+
**响应**
```json
diff --git a/docs/zh/api/07-system.md b/docs/zh/api/07-system.md
index eda9ce53..f637eb9f 100644
--- a/docs/zh/api/07-system.md
+++ b/docs/zh/api/07-system.md
@@ -33,6 +33,12 @@ GET /health
curl -X GET http://localhost:1933/health
```
+**CLI**
+
+```bash
+openviking health
+```
+
**响应**
```json
@@ -71,6 +77,12 @@ curl -X GET http://localhost:1933/api/v1/system/status \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking status
+```
+
**响应**
```json
@@ -129,6 +141,12 @@ curl -X POST http://localhost:1933/api/v1/system/wait \
}'
```
+**CLI**
+
+```bash
+openviking wait [--timeout 60]
+```
+
**响应**
```json
@@ -184,6 +202,12 @@ curl -X GET http://localhost:1933/api/v1/observer/queue \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking observer queue
+```
+
**响应**
```json
@@ -231,6 +255,12 @@ curl -X GET http://localhost:1933/api/v1/observer/vikingdb \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking observer vikingdb
+```
+
**响应**
```json
@@ -274,6 +304,12 @@ curl -X GET http://localhost:1933/api/v1/observer/vlm \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking observer vlm
+```
+
**响应**
```json
@@ -323,6 +359,12 @@ curl -X GET http://localhost:1933/api/v1/observer/system \
-H "X-API-Key: your-key"
```
+**CLI**
+
+```bash
+openviking observer system
+```
+
**响应**
```json
diff --git a/docs/zh/concepts/01-architecture.md b/docs/zh/concepts/01-architecture.md
index 14710c72..2ec4aea5 100644
--- a/docs/zh/concepts/01-architecture.md
+++ b/docs/zh/concepts/01-architecture.md
@@ -27,7 +27,7 @@ OpenViking 是为 AI Agent 设计的上下文数据库,将所有上下文(Me
│ │ (上下文检索) │ │ (会话管理) │ │ (上下文提取) │ │
│ │ │ │ │ │ │ │
│ │ search/find │ │ add/used │ │ 文档解析 │ │
-│ │ 意图分析 │ │ compress │ │ L0/L1/L2 │ │
+│ │ 意图分析 │ │ commit │ │ L0/L1/L2 │ │
│ │ Rerank │ │ commit │ │ 树构建 │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
@@ -68,7 +68,7 @@ Service 层将业务逻辑与传输层解耦,便于 HTTP Server 和 CLI 复用
|---------|------|----------|
| **FSService** | 文件系统操作 | ls, mkdir, rm, mv, tree, stat, read, abstract, overview, grep, glob |
| **SearchService** | 语义搜索 | search, find |
-| **SessionService** | 会话管理 | session, sessions, compress, extract, delete |
+| **SessionService** | 会话管理 | session, sessions, commit, delete |
| **ResourceService** | 资源导入 | add_resource, add_skill, wait_processed |
| **RelationService** | 关联管理 | relations, link, unlink |
| **PackService** | 导入导出 | export_ovpack, import_ovpack |
@@ -133,24 +133,9 @@ client = OpenViking(path="./data")
- 使用本地向量索引
- 单例模式
-### 服务模式
-
-用于生产环境的分布式部署:
-
-```python
-client = OpenViking(
- vectordb_url="http://vectordb:5000",
- agfs_url="http://agfs:8080"
-)
-```
-
-- 连接远程服务
-- 支持多实例并发
-- 可独立扩展
-
### HTTP 模式
-用于团队共享和跨语言集成:
+用于团队共享、生产环境和跨语言集成:
```python
# Python SDK 连接 OpenViking Server
diff --git a/docs/zh/faq/faq.md b/docs/zh/faq/faq.md
index a4331a07..c05dcf5c 100644
--- a/docs/zh/faq/faq.md
+++ b/docs/zh/faq/faq.md
@@ -100,7 +100,7 @@ pip install openviking
}
```
-配置优先级:构造函数参数 > Config 对象 > 配置文件 > 环境变量 > 默认值
+配置文件放在默认路径 `~/.openviking/ov.conf` 时自动加载;也可通过环境变量 `OPENVIKING_CONFIG_FILE` 或命令行 `--config` 指定其他路径。详见 [配置指南](../guides/01-configuration.md)。
### 支持哪些 Embedding Provider?
@@ -119,17 +119,17 @@ pip install openviking
```python
import openviking as ov
-# 异步客户端(推荐)
+# 异步客户端(推荐)- 嵌入模式
client = ov.AsyncOpenViking(path="./my_data")
await client.initialize()
-# 或使用配置对象
-from openviking.utils.config import OpenVikingConfig
-config = OpenVikingConfig(...)
-client = ov.AsyncOpenViking(config=config)
+# 异步客户端 - 服务模式
+client = ov.AsyncOpenViking(url="http://localhost:1933", api_key="your-key")
await client.initialize()
```
+SDK 构造函数仅接受 `url`、`api_key`、`path`、`user` 参数。其他配置(embedding、vlm 等)通过 `ov.conf` 配置文件管理。
+
### 支持哪些文件格式?
| 类型 | 支持格式 |
@@ -363,10 +363,7 @@ OpenViking 使用分数传播机制:
client = ov.AsyncOpenViking(path="./data")
# 服务模式
-client = ov.AsyncOpenViking(
- vectordb_url="http://vectordb:5000",
- agfs_url="http://agfs:8080"
-)
+client = ov.AsyncOpenViking(url="http://localhost:1933", api_key="your-key")
```
### OpenViking 是开源的吗?
diff --git a/docs/zh/getting-started/02-quickstart.md b/docs/zh/getting-started/02-quickstart.md
index 2f18c493..9ed53b81 100644
--- a/docs/zh/getting-started/02-quickstart.md
+++ b/docs/zh/getting-started/02-quickstart.md
@@ -107,10 +107,12 @@ OpenViking 支持多种模型服务:
### 设置环境变量
-创建好配置文件后,设置环境变量指向配置文件:
+配置文件放在默认路径 `~/.openviking/ov.conf` 时,无需额外设置,OpenViking 会自动加载。
+
+如果配置文件放在其他位置,需要通过环境变量指定:
```bash
-export OPENVIKING_CONFIG_FILE=~/.openviking/ov.conf
+export OPENVIKING_CONFIG_FILE=/path/to/your/ov.conf
```
## 运行第一个示例
diff --git a/docs/zh/getting-started/03-quickstart-server.md b/docs/zh/getting-started/03-quickstart-server.md
index 5fdd8f24..7542cfe6 100644
--- a/docs/zh/getting-started/03-quickstart-server.md
+++ b/docs/zh/getting-started/03-quickstart-server.md
@@ -9,8 +9,17 @@
## 启动服务
+确保 `ov.conf` 已配置好存储路径和模型信息(参见 [快速开始](02-quickstart.md)),然后启动服务:
+
```bash
-python -m openviking serve --path ./data
+# 配置文件在默认路径 ~/.openviking/ov.conf 时,直接启动
+python -m openviking serve
+
+# 配置文件在其他位置时,通过 --config 指定
+python -m openviking serve --config /path/to/ov.conf
+
+# 覆盖 host/port
+python -m openviking serve --port 8000
```
你应该看到:
@@ -34,18 +43,12 @@ import openviking as ov
client = ov.OpenViking(url="http://localhost:1933")
```
-或使用环境变量:
-
-```bash
-export OPENVIKING_URL="http://localhost:1933"
-export OPENVIKING_API_KEY="your-key" # 如果启用了认证
-```
+如果服务端启用了认证,需要传入 `api_key`:
```python
import openviking as ov
-# url 和 api_key 自动从环境变量读取
-client = ov.OpenViking()
+client = ov.OpenViking(url="http://localhost:1933", api_key="your-key")
```
**完整示例:**
@@ -76,6 +79,29 @@ finally:
client.close()
```
+## 使用 CLI 连接
+
+创建 CLI 连接配置文件 `~/.openviking/ovcli.conf`:
+
+```json
+{
+ "url": "http://localhost:1933"
+}
+```
+
+然后直接使用 CLI 命令:
+
+```bash
+python -m openviking health
+python -m openviking find "what is openviking"
+```
+
+如果配置文件在其他位置,通过环境变量指定:
+
+```bash
+export OPENVIKING_CLI_CONFIG_FILE=/path/to/ovcli.conf
+```
+
## 使用 curl 连接
```bash
diff --git a/docs/zh/guides/01-configuration.md b/docs/zh/guides/01-configuration.md
index 9a375202..96ddfce4 100644
--- a/docs/zh/guides/01-configuration.md
+++ b/docs/zh/guides/01-configuration.md
@@ -240,57 +240,76 @@ OpenViking 使用 JSON 配置文件(`ov.conf`)进行设置。配置文件支
}
```
-## 环境变量
+## 配置文件
+
+OpenViking 使用两个配置文件:
+
+| 配置文件 | 用途 | 默认路径 |
+|---------|------|---------|
+| `ov.conf` | SDK 嵌入模式 + 服务端配置 | `~/.openviking/ov.conf` |
+| `ovcli.conf` | CLI 连接远程服务端 | `~/.openviking/ovcli.conf` |
+
+配置文件放在默认路径时,OpenViking 自动加载,无需额外设置。
+
+如果配置文件在其他位置,有两种指定方式:
```bash
-export VOLCENGINE_API_KEY="your-api-key"
-export OPENVIKING_DATA_PATH="./data"
+# 方式一:环境变量
+export OPENVIKING_CONFIG_FILE=/path/to/ov.conf
+export OPENVIKING_CLI_CONFIG_FILE=/path/to/ovcli.conf
+
+# 方式二:命令行参数(仅 serve 命令)
+python -m openviking serve --config /path/to/ov.conf
+```
+
+### ov.conf
+
+本文档上方各配置段(embedding、vlm、rerank、storage)均属于 `ov.conf`。SDK 嵌入模式和服务端共用此文件。
+
+### ovcli.conf
+
+CLI 工具连接远程服务端的配置文件:
+
+```json
+{
+ "url": "http://localhost:1933",
+ "api_key": "your-secret-key",
+ "output": "table"
+}
```
-## 配置优先级
-
-1. 构造函数参数(最高)
-2. Config 对象
-3. 配置文件(`~/.openviking/ov.conf`)
-4. 环境变量
-5. 默认值(最低)
-
-## 编程式配置
-
-```python
-from openviking.utils.config import (
- OpenVikingConfig,
- StorageConfig,
- AGFSConfig,
- VectorDBBackendConfig,
- EmbeddingConfig,
- DenseEmbeddingConfig
-)
-
-config = OpenVikingConfig(
- storage=StorageConfig(
- agfs=AGFSConfig(
- backend="local",
- path="./custom_data",
- ),
- vectordb=VectorDBBackendConfig(
- backend="local",
- path="./custom_data",
- )
- ),
- embedding=EmbeddingConfig(
- dense=DenseEmbeddingConfig(
- provider="volcengine",
- api_key="your-api-key",
- model="doubao-embedding-vision-250615",
- dimension=1024
- )
- )
-)
-
-client = ov.AsyncOpenViking(config=config)
+| 字段 | 说明 | 默认值 |
+|------|------|--------|
+| `url` | 服务端地址 | (必填) |
+| `api_key` | API Key 认证 | `null`(无认证) |
+| `output` | 默认输出格式:`"table"` 或 `"json"` | `"table"` |
+
+详见 [服务部署](./03-deployment.md)。
+
+## server 段
+
+将 OpenViking 作为 HTTP 服务运行时,在 `ov.conf` 中添加 `server` 段:
+
+```json
+{
+ "server": {
+ "host": "0.0.0.0",
+ "port": 1933,
+ "api_key": "your-secret-key",
+ "cors_origins": ["*"]
+ }
+}
```
+| 字段 | 类型 | 说明 | 默认值 |
+|------|------|------|--------|
+| `host` | str | 绑定地址 | `0.0.0.0` |
+| `port` | int | 绑定端口 | `1933` |
+| `api_key` | str | API Key 认证,不设则禁用认证 | `null` |
+| `cors_origins` | list | CORS 允许的来源 | `["*"]` |
+
+启动方式和部署详情见 [服务部署](./03-deployment.md),认证详情见 [认证](./04-authentication.md)。
+
## 完整 Schema
```json
@@ -328,41 +347,18 @@ client = ov.AsyncOpenViking(config=config)
"url": "string"
}
},
- "user": "string"
-}
-```
-
-说明:
-- `storage.vectordb.sparse_weight` 用于混合(dense + sparse)索引/检索的权重,仅在使用 hybrid 索引时生效;设置为 > 0 才会启用 sparse 信号。
-
-## Server 配置
-
-将 OpenViking 作为 HTTP 服务运行时,服务端从同一个 JSON 配置文件中读取配置(通过 `--config` 或 `OPENVIKING_CONFIG_FILE`):
-
-```json
-{
"server": {
- "host": "0.0.0.0",
+ "host": "string",
"port": 1933,
- "api_key": "your-secret-key",
- "cors_origins": ["*"]
+ "api_key": "string",
+ "cors_origins": ["string"]
},
- "storage": {
- "path": "/data/openviking"
- }
+ "user": "string"
}
```
-Server 配置也可以通过环境变量设置:
-
-| 变量 | 说明 |
-|------|------|
-| `OPENVIKING_HOST` | 服务主机地址 |
-| `OPENVIKING_PORT` | 服务端口 |
-| `OPENVIKING_API_KEY` | 用于认证的 API Key |
-| `OPENVIKING_PATH` | 存储路径 |
-
-详见 [服务部署](./03-deployment.md)。
+说明:
+- `storage.vectordb.sparse_weight` 用于混合(dense + sparse)索引/检索的权重,仅在使用 hybrid 索引时生效;设置为 > 0 才会启用 sparse 信号。
## 故障排除
@@ -402,7 +398,7 @@ Error: Rate limit exceeded
## 相关文档
-- [火山引擎购买指南](./volcengine-purchase-guide.md) - API Key 获取
+- [火山引擎购买指南](./02-volcengine-purchase-guide.md) - API Key 获取
- [API 概览](../api/01-overview.md) - 客户端初始化
- [服务部署](./03-deployment.md) - Server 配置
- [上下文层级](../concepts/03-context-layers.md) - L0/L1/L2
diff --git a/docs/zh/guides/03-deployment.md b/docs/zh/guides/03-deployment.md
index 41fd062c..d0185eb0 100644
--- a/docs/zh/guides/03-deployment.md
+++ b/docs/zh/guides/03-deployment.md
@@ -5,8 +5,11 @@ OpenViking 可以作为独立的 HTTP 服务器运行,允许多个客户端通
## 快速开始
```bash
-# 使用本地存储启动服务器
-python -m openviking serve --path ./data
+# 配置文件在默认路径 ~/.openviking/ov.conf 时,直接启动
+python -m openviking serve
+
+# 配置文件在其他位置时,通过 --config 指定
+python -m openviking serve --config /path/to/ov.conf
# 验证服务器是否运行
curl http://localhost:1933/health
@@ -17,43 +20,28 @@ curl http://localhost:1933/health
| 选项 | 描述 | 默认值 |
|------|------|--------|
+| `--config` | 配置文件路径 | `~/.openviking/ov.conf` |
| `--host` | 绑定的主机地址 | `0.0.0.0` |
| `--port` | 绑定的端口 | `1933` |
-| `--path` | 本地存储路径(嵌入模式) | 无 |
-| `--vectordb-url` | 远程 VectorDB URL(服务模式) | 无 |
-| `--agfs-url` | 远程 AGFS URL(服务模式) | 无 |
-| `--api-key` | 用于认证的 API Key | 无(禁用认证) |
-| `--config` | 配置文件路径 | `OPENVIKING_CONFIG_FILE` 环境变量 |
**示例**
```bash
-# 嵌入模式,使用自定义端口
-python -m openviking serve --path ./data --port 8000
+# 使用默认配置
+python -m openviking serve
-# 启用认证
-python -m openviking serve --path ./data --api-key "your-secret-key"
+# 使用自定义端口
+python -m openviking serve --port 8000
-# 服务模式(远程存储)
-python -m openviking serve \
- --vectordb-url http://vectordb:8000 \
- --agfs-url http://agfs:1833
+# 指定配置文件、主机地址和端口
+python -m openviking serve --config /path/to/ov.conf --host 127.0.0.1 --port 8000
```
## 配置
-### 配置文件
-
-服务端配置从 `--config` 或 `OPENVIKING_CONFIG_FILE` 环境变量指定的 JSON 配置文件中读取(与 `OpenVikingConfig` 共用同一个文件):
-
-```bash
-python -m openviking serve --config ~/.openviking/ov.conf
-# 或
-export OPENVIKING_CONFIG_FILE=~/.openviking/ov.conf
-python -m openviking serve
-```
+服务端从 `ov.conf` 读取所有配置。配置文件各段详情见 [配置指南](01-configuration.md)。
-配置文件中的 `server` 段:
+`ov.conf` 中的 `server` 段控制服务端行为:
```json
{
@@ -64,48 +52,46 @@ python -m openviking serve
"cors_origins": ["*"]
},
"storage": {
- "path": "/data/openviking"
+ "agfs": { "backend": "local", "path": "/data/openviking" },
+ "vectordb": { "backend": "local", "path": "/data/openviking" }
}
}
```
-### 环境变量
-
-| 变量 | 描述 | 示例 |
-|------|------|------|
-| `OPENVIKING_HOST` | 服务器主机地址 | `0.0.0.0` |
-| `OPENVIKING_PORT` | 服务器端口 | `1933` |
-| `OPENVIKING_API_KEY` | API Key | `sk-xxx` |
-| `OPENVIKING_PATH` | 存储路径 | `./data` |
-| `OPENVIKING_VECTORDB_URL` | 远程 VectorDB URL | `http://vectordb:8000` |
-| `OPENVIKING_AGFS_URL` | 远程 AGFS URL | `http://agfs:1833` |
-
-### 配置优先级
-
-从高到低:
-
-1. **命令行参数** (`--port 8000`)
-2. **环境变量** (`OPENVIKING_PORT=8000`)
-3. **配置文件** (`OPENVIKING_CONFIG_FILE`)
-
## 部署模式
### 独立模式(嵌入存储)
-服务器管理本地 AGFS 和 VectorDB:
+服务器管理本地 AGFS 和 VectorDB。在 `ov.conf` 中配置本地存储路径:
+
+```json
+{
+ "storage": {
+ "agfs": { "backend": "local", "path": "./data" },
+ "vectordb": { "backend": "local", "path": "./data" }
+ }
+}
+```
```bash
-python -m openviking serve --path ./data
+python -m openviking serve
```
### 混合模式(远程存储)
-服务器连接到远程 AGFS 和 VectorDB 服务:
+服务器连接到远程 AGFS 和 VectorDB 服务。在 `ov.conf` 中配置远程地址:
+
+```json
+{
+ "storage": {
+ "agfs": { "backend": "remote", "url": "http://agfs:1833" },
+ "vectordb": { "backend": "remote", "url": "http://vectordb:8000" }
+ }
+}
+```
```bash
-python -m openviking serve \
- --vectordb-url http://vectordb:8000 \
- --agfs-url http://agfs:1833
+python -m openviking serve
```
## 连接客户端
@@ -122,19 +108,21 @@ results = client.find("how to use openviking")
client.close()
```
-或使用环境变量:
+### CLI
-```bash
-export OPENVIKING_URL="http://localhost:1933"
-export OPENVIKING_API_KEY="your-key"
+CLI 从 `ovcli.conf` 读取连接配置。在 `~/.openviking/ovcli.conf` 中配置:
+
+```json
+{
+ "url": "http://localhost:1933",
+ "api_key": "your-key"
+}
```
-```python
-import openviking as ov
+也可通过 `OPENVIKING_CLI_CONFIG_FILE` 环境变量指定配置文件路径:
-# url 和 api_key 自动从环境变量读取
-client = ov.OpenViking()
-client.initialize()
+```bash
+export OPENVIKING_CLI_CONFIG_FILE=/path/to/ovcli.conf
```
### curl
diff --git a/docs/zh/guides/04-authentication.md b/docs/zh/guides/04-authentication.md
index d228cb4d..635d801a 100644
--- a/docs/zh/guides/04-authentication.md
+++ b/docs/zh/guides/04-authentication.md
@@ -6,20 +6,7 @@ OpenViking Server 支持 API Key 认证以保护访问安全。
### 设置(服务端)
-**方式一:命令行**
-
-```bash
-python -m openviking serve --path ./data --api-key "your-secret-key"
-```
-
-**方式二:环境变量**
-
-```bash
-export OPENVIKING_API_KEY="your-secret-key"
-python -m openviking serve --path ./data
-```
-
-**方式三:配置文件**(通过 `OPENVIKING_CONFIG_FILE`)
+在 `ov.conf` 配置文件中设置 `server.api_key`:
```json
{
@@ -29,6 +16,12 @@ python -m openviking serve --path ./data
}
```
+启动服务时通过 `--config` 指定配置文件:
+
+```bash
+python -m openviking serve
+```
+
### 使用 API Key(客户端)
OpenViking 通过以下两种请求头接受 API Key:
@@ -58,27 +51,33 @@ client = ov.OpenViking(
)
```
-或使用 `OPENVIKING_API_KEY` 环境变量:
+**CLI**
-```bash
-export OPENVIKING_URL="http://localhost:1933"
-export OPENVIKING_API_KEY="your-secret-key"
-```
+CLI 从 `ovcli.conf` 配置文件读取连接信息。在 `~/.openviking/ovcli.conf` 中配置 `api_key`:
-```python
-import openviking as ov
-
-# api_key 自动从 OPENVIKING_API_KEY 环境变量读取
-client = ov.OpenViking()
+```json
+{
+ "url": "http://localhost:1933",
+ "api_key": "your-secret-key"
+}
```
## 开发模式
-当未配置 API Key 时,认证功能将被禁用。所有请求无需凭证即可被接受。
+当 `ov.conf` 中未配置 `server.api_key` 时,认证功能将被禁用。所有请求无需凭证即可被接受。
+
+```json
+{
+ "server": {
+ "host": "0.0.0.0",
+ "port": 1933
+ }
+}
+```
```bash
-# 不指定 --api-key 参数 = 禁用认证
-python -m openviking serve --path ./data
+# 未配置 api_key = 禁用认证
+python -m openviking serve
```
## 无需认证的端点
diff --git a/examples/ov.conf.example b/examples/ov.conf.example
index 0d54a673..fc97ecc2 100644
--- a/examples/ov.conf.example
+++ b/examples/ov.conf.example
@@ -1,4 +1,10 @@
{
+ "server": {
+ "host": "0.0.0.0",
+ "port": 1933,
+ "api_key": null,
+ "cors_origins": ["*"]
+ },
"storage": {
"vectordb": {
"name": "context",
diff --git a/examples/ovcli.conf.example b/examples/ovcli.conf.example
new file mode 100644
index 00000000..d1d44532
--- /dev/null
+++ b/examples/ovcli.conf.example
@@ -0,0 +1,5 @@
+{
+ "url": "http://localhost:1933",
+ "api_key": null,
+ "output": "table"
+}
diff --git a/examples/server_client/README.md b/examples/server_client/README.md
index c9ceddc7..c4ab6046 100644
--- a/examples/server_client/README.md
+++ b/examples/server_client/README.md
@@ -17,56 +17,98 @@
# 0. 安装依赖
uv sync
-# 1. 启动 Server
-uv run server.py
-
-# 2. 另一个终端,运行 Client 示例
+# 1. 配置 Server(ov.conf)
+# Server 读取配置的优先级:
+# $OPENVIKING_CONFIG_FILE > ~/.openviking/ov.conf
+# 参见 ov.conf.example 了解完整配置项
+
+# 2. 启动 Server(另开终端)
+openviking serve # 从默认路径读取 ov.conf
+openviking serve --config ./ov.conf # 指定配置文件
+openviking serve --host 0.0.0.0 --port 1933 # 覆盖 host/port
+
+# 3. 配置 CLI 连接(ovcli.conf)
+# CLI 读取配置的优先级:
+# $OPENVIKING_CLI_CONFIG_FILE > ~/.openviking/ovcli.conf
+# 示例内容:
+# {"url": "http://localhost:1933", "api_key": null, "output": "table"}
+
+# 4. 运行 Client 示例
uv run client_sync.py # 同步客户端
uv run client_async.py # 异步客户端
+bash client_cli.sh # CLI 使用示例
```
## 文件说明
```
-server.py # Server 启动示例(含 API Key 认证)
client_sync.py # 同步客户端示例(SyncOpenViking HTTP mode)
client_async.py # 异步客户端示例(AsyncOpenViking HTTP mode)
-ov.conf.example # 配置文件模板
+client_cli.sh # CLI 使用示例(覆盖所有命令和参数)
+ov.conf.example # Server/SDK 配置文件模板(ov.conf)
+ovcli.conf.example # CLI 连接配置文件模板(ovcli.conf)
pyproject.toml # 项目依赖
```
+## 配置文件
+
+新的配置系统使用两个配置文件,不再支持单字段环境变量(如 OPENVIKING_URL、OPENVIKING_API_KEY、OPENVIKING_HOST、OPENVIKING_PORT、OPENVIKING_PATH、OPENVIKING_VECTORDB_URL、OPENVIKING_AGFS_URL 均已移除)。
+
+仅保留 2 个环境变量:
+
+| 环境变量 | 说明 | 默认路径 |
+|---------|------|---------|
+| `OPENVIKING_CONFIG_FILE` | ov.conf 配置文件路径 | `~/.openviking/ov.conf` |
+| `OPENVIKING_CLI_CONFIG_FILE` | ovcli.conf 配置文件路径 | `~/.openviking/ovcli.conf` |
+
+### ov.conf(SDK 嵌入 + Server)
+
+用于 SDK 嵌入模式和 Server 启动,包含 `server` 段配置。参见 `ov.conf.example`。
+
+### ovcli.conf(CLI 连接配置)
+
+用于 CLI 连接远程 Server:
+
+| 字段 | 说明 | 默认值 |
+|------|------|--------|
+| `url` | Server 地址 | (必填) |
+| `api_key` | API Key 认证 | `null`(无认证) |
+| `user` | 默认用户名 | `"default"` |
+| `output` | 默认输出格式:`"table"` 或 `"json"` | `"table"` |
+
+```json
+{
+ "url": "http://localhost:1933",
+ "api_key": null,
+ "user": "default",
+ "output": "table"
+}
+```
+
## Server 启动方式
-### 方式一:CLI 命令
+### CLI 命令
```bash
-# 基本启动
-python -m openviking serve --path ./data --port 1933
-
-# 带 API Key 认证
-python -m openviking serve --path ./data --port 1933 --api-key your-secret-key
+# 基本启动(从 ~/.openviking/ov.conf 或 $OPENVIKING_CONFIG_FILE 读取配置)
+openviking serve
# 指定配置文件
-python -m openviking serve --path ./data --config ./ov.conf
+openviking serve --config ./ov.conf
+
+# 覆盖 host/port
+openviking serve --host 0.0.0.0 --port 1933
```
-### 方式二:Python 脚本
+`serve` 命令支持 `--config`、`--host`、`--port` 三个选项。认证密钥等其他配置通过 ov.conf 的 `server` 段设置。
+
+### Python 脚本
```python
from openviking.server.bootstrap import main
main()
```
-### 方式三:环境变量
-
-```bash
-export OPENVIKING_CONFIG_FILE=./ov.conf # or ~/.openviking/ov.conf
-export OPENVIKING_PATH=./data
-export OPENVIKING_PORT=1933
-export OPENVIKING_API_KEY=your-secret-key
-python -m openviking serve
-```
-
## Client 使用方式
### 同步客户端
@@ -99,6 +141,31 @@ results = await client.find("search query")
await client.close()
```
+### CLI
+
+```bash
+# CLI 从 ~/.openviking/ovcli.conf 或 $OPENVIKING_CLI_CONFIG_FILE 读取连接配置
+
+# 基本操作
+openviking health
+openviking add-resource ./document.md
+openviking wait
+openviking find "search query"
+
+# 输出格式(全局选项,须放在子命令之前)
+openviking -o table find "query" # 表格输出(默认)
+openviking -o json find "query" # 格式化 JSON 输出
+openviking --json find "query" # 紧凑 JSON + {"ok":true} 包装(脚本用)
+
+# Session 操作
+openviking session new
+openviking session add-message --role user --content "hello"
+openviking session commit
+openviking session delete
+```
+
+完整 CLI 使用示例参见 `client_cli.sh`。
+
## API 端点一览
| 方法 | 路径 | 说明 |
@@ -129,6 +196,7 @@ await client.close()
| GET | `/api/v1/sessions/{id}` | 获取 Session |
| DELETE | `/api/v1/sessions/{id}` | 删除 Session |
| POST | `/api/v1/sessions/{id}/messages` | 添加消息 |
+| POST | `/api/v1/sessions/{id}/commit` | 提交 Session(归档消息、提取记忆) |
| POST | `/api/v1/pack/export` | 导出 ovpack |
| POST | `/api/v1/pack/import` | 导入 ovpack |
| GET | `/api/v1/observer/system` | 系统监控 |
@@ -139,7 +207,7 @@ await client.close()
## 认证
-Server 支持可选的 API Key 认证。启动时通过 `--api-key` 或配置文件设置。
+Server 支持可选的 API Key 认证。通过 ov.conf 的 `server.api_key` 字段设置。
Client 请求时通过以下任一方式传递:
@@ -148,4 +216,6 @@ X-API-Key: your-secret-key
Authorization: Bearer your-secret-key
```
+CLI 的 API Key 通过 ovcli.conf 的 `api_key` 字段配置。
+
`/health` 端点始终免认证。
diff --git a/examples/server_client/client_async.py b/examples/server_client/client_async.py
index 7284062d..fc2093eb 100644
--- a/examples/server_client/client_async.py
+++ b/examples/server_client/client_async.py
@@ -5,7 +5,7 @@
使用 AsyncOpenViking 通过 HTTP 连接远程 Server,演示完整 API。
前置条件:
- 先启动 Server: uv run server.py
+ 先启动 Server: openviking serve
运行:
uv run client_async.py
@@ -146,7 +146,7 @@ async def main():
# ── Session + Context Search ──
console.print(Panel("Session & Context Search", style="bold magenta", width=PANEL_WIDTH))
session = client.session()
- console.print(f" Created session: [bold]{session.id}[/bold]")
+ console.print(f" Created session: [bold]{session.session_id}[/bold]")
await session.add_message(role="user", content="Tell me about OpenViking")
await session.add_message(
@@ -169,7 +169,7 @@ async def main():
console.print(" [dim]No context search results[/dim]")
await session.delete()
- console.print(f" Deleted session: [dim]{session.id}[/dim]")
+ console.print(f" Deleted session: [dim]{session.session_id}[/dim]")
console.print()
# ── Relations ──
diff --git a/examples/server_client/client_cli.sh b/examples/server_client/client_cli.sh
new file mode 100755
index 00000000..6d345548
--- /dev/null
+++ b/examples/server_client/client_cli.sh
@@ -0,0 +1,231 @@
+#!/usr/bin/env bash
+# ============================================================================
+# OpenViking CLI Usage Examples
+#
+# This script demonstrates the OpenViking CLI commands and options.
+# It walks through a typical workflow: health check → add resource → browse →
+# search → session management → cleanup.
+#
+# Prerequisites:
+# 1. Configure & start the server:
+# Server reads ov.conf from (priority high → low):
+# a) $OPENVIKING_CONFIG_FILE # env var, highest priority
+# b) ~/.openviking/ov.conf # default path
+# See ov.conf.example for template.
+#
+# openviking serve # default: localhost:1933
+# openviking serve --port 8080 # custom port
+# openviking serve --config /path/to/ov.conf # explicit config path
+#
+# 2. Configure CLI connection (ovcli.conf):
+# CLI reads ovcli.conf from (priority high → low):
+# a) $OPENVIKING_CLI_CONFIG_FILE # env var, highest priority
+# b) ~/.openviking/ovcli.conf # default path
+#
+# Example ovcli.conf:
+# {
+# "url": "http://localhost:1933",
+# "api_key": null,
+# "output": "table"
+# }
+#
+# Fields:
+# url - Server address (required)
+# api_key - API key for authentication (null = no auth)
+# output - Default output format: "table" or "json" (default: "table")
+#
+# Usage:
+# bash client_cli.sh
+# ============================================================================
+
+set -euo pipefail
+
+
+section() { printf '\n\033[1;36m── %s ──\033[0m\n' "$1"; }
+
+# ============================================================================
+# Global Options
+# ============================================================================
+#
+# --output, -o Output format: table (default) or json
+# --json Compact JSON with {"ok": true, "result": ...} wrapper
+# --version Show version and exit
+#
+# Global options MUST be placed before the subcommand:
+# openviking -o json ls viking:// ✓
+# openviking --json find "query" ✓
+# openviking ls viking:// -o json ✗ (won't work)
+
+printf '\033[1m=== OpenViking CLI Usage Examples ===\033[0m\n'
+
+openviking --version
+
+# ============================================================================
+# 1. Health & Status
+# ============================================================================
+
+section "1.1 Health Check"
+openviking health # table: {"healthy": true}
+# openviking -o json health # json: {"healthy": true}
+# openviking --json health # script: {"ok": true, "result": {"healthy": true}}
+
+section "1.2 System Status"
+openviking status # table: component status with ASCII tables
+
+section "1.3 Observer (per-component)"
+openviking observer queue # queue processor status
+# openviking observer vikingdb # VikingDB connection status
+# openviking observer vlm # VLM service status
+# openviking observer system # all components (same as `status`)
+
+# ============================================================================
+# 2. Resource Management
+# ============================================================================
+
+section "2.1 Add Resource"
+# Add from URL (use --json to capture root_uri for later commands)
+ROOT_URI=$(openviking --json add-resource \
+ "https://raw.githubusercontent.com/volcengine/OpenViking/refs/heads/main/README.md" \
+ | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['root_uri'])")
+echo " root_uri: $ROOT_URI"
+
+# Other add-resource options:
+# openviking add-resource ./file --to viking://dst # specify target URI
+# openviking add-resource ./file --reason "..." # attach import reason
+# openviking add-resource ./file --wait # block until processing done
+# openviking add-resource ./file --wait --timeout 60
+
+section "2.2 Add Skill"
+# openviking add-skill ./my_skill/SKILL.md # from SKILL.md file
+# openviking add-skill ./skill_dir # from directory
+# openviking add-skill "raw skill content" # from inline text
+# openviking add-skill ./skill --wait --timeout 30
+
+section "2.3 Wait for Processing"
+openviking wait # block until all queues are idle
+# openviking wait --timeout 120 # with timeout (seconds)
+
+# ============================================================================
+# 3. File System
+# ============================================================================
+
+section "3.1 List Directory"
+openviking ls viking://resources/ # table: name, size, mode, ...
+# openviking ls viking://resources/ --simple # simple: paths only
+# openviking ls viking://resources/ --recursive # recursive listing
+# openviking -o json ls viking://resources/ # json output
+
+section "3.2 Directory Tree"
+openviking tree "$ROOT_URI"
+
+section "3.3 File Metadata"
+openviking stat "$ROOT_URI" # table: single-row with all metadata
+
+section "3.4 File Operations"
+# openviking mkdir viking://resources/new_dir
+# openviking mv viking://resources/old viking://resources/new
+# openviking rm viking://resources/file
+# openviking rm viking://resources/dir --recursive
+
+# ============================================================================
+# 4. Content Reading (3 levels of detail)
+# ============================================================================
+
+section "4.1 Abstract (L0 - shortest summary)"
+openviking abstract "$ROOT_URI"
+
+section "4.2 Overview (L1 - structured overview)"
+openviking overview "$ROOT_URI"
+
+section "4.3 Read (L2 - full content)"
+# openviking read "$ROOT_URI" # prints full file content
+
+# ============================================================================
+# 5. Search
+# ============================================================================
+
+section "5.1 Semantic Search (find)"
+openviking find "what is openviking" --limit 3
+# openviking find "auth" --uri viking://resources/docs/ # search within URI
+# openviking find "auth" --limit 5 --threshold 0.3 # with score threshold
+# openviking -o json find "auth" # json output
+
+section "5.2 Pattern Search (grep)"
+openviking grep "viking://" "OpenViking"
+# openviking grep "viking://resources/" "pattern" --ignore-case
+
+section "5.3 File Glob"
+openviking glob "**/*.md"
+# openviking glob "*.py" --uri viking://resources/src/ # search within URI
+
+# ============================================================================
+# 6. Relations
+# ============================================================================
+
+section "6.1 List Relations"
+openviking relations "$ROOT_URI"
+
+section "6.2 Link / Unlink"
+# openviking link viking://a viking://b viking://c --reason "related docs"
+# openviking unlink viking://a viking://b
+
+# ============================================================================
+# 7. Session Management
+# ============================================================================
+
+section "7.1 Create Session"
+SESSION_ID=$(openviking --json session new | python3 -c "
+import sys, json; print(json.load(sys.stdin)['result']['session_id'])
+")
+echo " session_id: $SESSION_ID"
+# openviking session new --user "alice" # with custom user
+
+section "7.2 Add Messages"
+openviking session add-message "$SESSION_ID" \
+ --role user --content "Tell me about OpenViking"
+openviking session add-message "$SESSION_ID" \
+ --role assistant --content "OpenViking is an agent-native context database."
+
+section "7.3 Get Session Details"
+openviking session get "$SESSION_ID"
+
+section "7.4 Context-Aware Search"
+# search uses session history for better relevance
+openviking search "how to use it" --session-id "$SESSION_ID" --limit 3
+# openviking search "query" --session-id "$SESSION_ID" --threshold 0.3
+
+section "7.5 List All Sessions"
+openviking session list
+
+section "7.6 Commit Session (archive + extract memories)"
+# openviking session commit "$SESSION_ID"
+
+section "7.7 Delete Session"
+openviking session delete "$SESSION_ID"
+
+# ============================================================================
+# 8. Import / Export
+# ============================================================================
+
+section "8.1 Export"
+# openviking export viking://resources/docs ./docs.ovpack
+
+section "8.2 Import"
+# openviking import ./docs.ovpack viking://resources/imported
+# openviking import ./docs.ovpack viking://resources/imported --force
+# openviking import ./docs.ovpack viking://resources/imported --no-vectorize
+
+# ============================================================================
+# Output Format Comparison
+# ============================================================================
+
+section "Output: table (default)"
+openviking ls viking://resources/
+
+section "Output: json"
+openviking -o json ls viking://resources/
+
+section "Output: --json (for scripts)"
+openviking --json ls viking://resources/
+
+printf '\n\033[1m=== Done ===\033[0m\n'
diff --git a/examples/server_client/client_sync.py b/examples/server_client/client_sync.py
index 5e299c69..12ed42ab 100644
--- a/examples/server_client/client_sync.py
+++ b/examples/server_client/client_sync.py
@@ -5,7 +5,7 @@
使用 SyncOpenViking 通过 HTTP 连接远程 Server,演示完整 API。
前置条件:
- 先启动 Server: uv run server.py
+ 先启动 Server: openviking serve
运行:
uv run client_sync.py
@@ -169,7 +169,7 @@ def main():
# ── Session + Context Search ──
console.print(Panel("Session & Context Search", style="bold magenta", width=PANEL_WIDTH))
session = client.session()
- console.print(f" Created session: [bold]{session.id}[/bold]")
+ console.print(f" Created session: [bold]{session.session_id}[/bold]")
run_async(session.add_message(
role="user", content="Tell me about OpenViking",
@@ -194,7 +194,7 @@ def main():
console.print(" [dim]No context search results[/dim]")
run_async(session.delete())
- console.print(f" Deleted session: [dim]{session.id}[/dim]")
+ console.print(f" Deleted session: [dim]{session.session_id}[/dim]")
console.print()
# ── Relations ──
diff --git a/examples/server_client/ovcli.conf.example b/examples/server_client/ovcli.conf.example
new file mode 100644
index 00000000..d1d44532
--- /dev/null
+++ b/examples/server_client/ovcli.conf.example
@@ -0,0 +1,5 @@
+{
+ "url": "http://localhost:1933",
+ "api_key": null,
+ "output": "table"
+}
diff --git a/examples/server_client/server.py b/examples/server_client/server.py
deleted file mode 100644
index a3f4f3a9..00000000
--- a/examples/server_client/server.py
+++ /dev/null
@@ -1,77 +0,0 @@
-#!/usr/bin/env python3
-"""
-OpenViking Server 启动示例
-
-启动方式:
- uv run server.py
- uv run server.py --api-key your-secret-key
- uv run server.py --port 8000 --path ./my_data
-"""
-
-import argparse
-import os
-import sys
-
-from rich.console import Console
-from rich.panel import Panel
-from rich.table import Table
-
-console = Console()
-
-
-def main():
- parser = argparse.ArgumentParser(
- description="Start OpenViking HTTP Server",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog="""
-Examples:
- uv run server.py
- uv run server.py --api-key my-secret-key
- uv run server.py --port 8000 --path ./my_data
- uv run server.py --config ./ov.conf
- """,
- )
- parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
- parser.add_argument("--port", type=int, default=1933, help="Port to bind to")
- parser.add_argument("--path", default="./data", help="Storage path")
- parser.add_argument("--config", default=None, help="Config file path")
- parser.add_argument("--api-key", default=None, help="API key for authentication")
- args = parser.parse_args()
-
- # Set config file
- if args.config:
- os.environ["OPENVIKING_CONFIG_FILE"] = args.config
- elif os.path.exists("./ov.conf"):
- os.environ.setdefault("OPENVIKING_CONFIG_FILE", "./ov.conf")
- elif os.path.exists("~/.openviking/ov.conf"):
- os.environ.setdefault("OPENVIKING_CONFIG_FILE", "~/.openviking/ov.conf")
-
- # Display server info
- info = Table(show_header=False, box=None, padding=(0, 2))
- info.add_column("Key", style="bold cyan")
- info.add_column("Value", style="white")
- info.add_row("Host", args.host)
- info.add_row("Port", str(args.port))
- info.add_row("Storage", args.path)
- info.add_row("Config", args.config or os.environ.get("OPENVIKING_CONFIG_FILE", "(default)"))
- info.add_row("Auth", "enabled" if args.api_key else "[dim]disabled[/dim]")
-
- console.print()
- console.print(Panel(info, title="OpenViking Server", style="bold green", padding=(1, 2)))
- console.print()
-
- # Rebuild sys.argv for the bootstrap module
- sys.argv = ["openviking-server"]
- sys.argv.extend(["--host", args.host])
- sys.argv.extend(["--port", str(args.port)])
- sys.argv.extend(["--path", args.path])
- if args.api_key:
- sys.argv.extend(["--api-key", args.api_key])
-
- from openviking.server.bootstrap import main as serve_main
-
- serve_main()
-
-
-if __name__ == "__main__":
- main()
diff --git a/examples/server_client/uv.lock b/examples/server_client/uv.lock
index 88dc62ff..8d7c7999 100644
--- a/examples/server_client/uv.lock
+++ b/examples/server_client/uv.lock
@@ -666,14 +666,18 @@ dependencies = [
{ name = "markdownify" },
{ name = "nest-asyncio" },
{ name = "openai" },
+ { name = "pdfminer-six" },
{ name = "pdfplumber" },
+ { name = "protobuf" },
{ name = "pyagfs" },
{ name = "pydantic" },
{ name = "pyyaml" },
{ name = "readabilipy" },
{ name = "requests" },
{ name = "tabulate" },
+ { name = "typer" },
{ name = "typing-extensions" },
+ { name = "urllib3" },
{ name = "uvicorn" },
{ name = "volcengine" },
{ name = "volcengine-python-sdk", extra = ["ark"] },
@@ -692,7 +696,9 @@ requires-dist = [
{ name = "myst-parser", marker = "extra == 'doc'", specifier = ">=2.0.0" },
{ name = "nest-asyncio", specifier = ">=1.5.0" },
{ name = "openai", specifier = ">=1.0.0" },
+ { name = "pdfminer-six", specifier = ">=20251230" },
{ name = "pdfplumber", specifier = ">=0.10.0" },
+ { name = "protobuf", specifier = ">=6.33.5" },
{ name = "pyagfs", specifier = ">=1.4.0" },
{ name = "pydantic", specifier = ">=2.0.0" },
{ name = "pytest", marker = "extra == 'test'", specifier = ">=7.0.0" },
@@ -705,9 +711,11 @@ requires-dist = [
{ name = "sphinx", marker = "extra == 'doc'", specifier = ">=7.0.0" },
{ name = "sphinx-rtd-theme", marker = "extra == 'doc'", specifier = ">=1.3.0" },
{ name = "tabulate", specifier = ">=0.9.0" },
+ { name = "typer", specifier = ">=0.12.0" },
{ name = "typing-extensions", specifier = ">=4.5.0" },
+ { name = "urllib3", specifier = ">=2.6.3" },
{ name = "uvicorn", specifier = ">=0.39.0" },
- { name = "volcengine", specifier = ">=1.0.212" },
+ { name = "volcengine", specifier = ">=1.0.216" },
{ name = "volcengine-python-sdk", extras = ["ark"], specifier = ">=5.0.3" },
{ name = "xxhash", specifier = ">=3.0.0" },
]
@@ -1234,6 +1242,15 @@ requires-dist = [
{ name = "rich", specifier = ">=13.0.0" },
]
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+]
+
[[package]]
name = "six"
version = "1.17.0"
@@ -1295,6 +1312,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
]
+[[package]]
+name = "typer"
+version = "0.21.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "rich" },
+ { name = "shellingham" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
+]
+
[[package]]
name = "typing-extensions"
version = "4.15.0"
diff --git a/openviking/__main__.py b/openviking/__main__.py
index e0260f3d..2d394404 100644
--- a/openviking/__main__.py
+++ b/openviking/__main__.py
@@ -1,58 +1,14 @@
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: Apache-2.0
-"""Main entry point for `python -m openviking` command."""
+"""Main entry point for `python -m openviking`."""
-import argparse
-import sys
+from openviking.cli.main import app
-def main():
- """Main CLI entry point."""
- parser = argparse.ArgumentParser(
- description="OpenViking - An Agent-native context database",
- formatter_class=argparse.ArgumentDefaultsHelpFormatter,
- )
- subparsers = parser.add_subparsers(dest="command", help="Available commands")
- # serve command
- serve_parser = subparsers.add_parser("serve", help="Start OpenViking HTTP Server")
- serve_parser.add_argument("--host", type=str, default=None, help="Host to bind to")
- serve_parser.add_argument("--port", type=int, default=None, help="Port to bind to")
- serve_parser.add_argument("--path", type=str, default=None, help="Storage path")
- serve_parser.add_argument("--config", type=str, default=None, help="Config file path")
- serve_parser.add_argument("--api-key", type=str, default=None, help="API key")
-
- # viewer command
- viewer_parser = subparsers.add_parser("viewer", help="Start OpenViking Viewer")
- viewer_parser.add_argument("--port", type=int, default=8501, help="Viewer port")
-
- args = parser.parse_args()
-
- if args.command == "serve":
- from openviking.server.bootstrap import main as serve_main
-
- # Rebuild sys.argv for serve command
- sys.argv = ["openviking-server"]
- if args.host:
- sys.argv.extend(["--host", args.host])
- if args.port:
- sys.argv.extend(["--port", str(args.port)])
- if args.path:
- sys.argv.extend(["--path", args.path])
- if args.config:
- sys.argv.extend(["--config", args.config])
- if args.api_key:
- sys.argv.extend(["--api-key", args.api_key])
- serve_main()
-
- elif args.command == "viewer":
- from openviking.tools.viewer import main as viewer_main
-
- viewer_main()
-
- else:
- parser.print_help()
- sys.exit(1)
+def main() -> None:
+ """Run the OpenViking CLI."""
+ app()
if __name__ == "__main__":
diff --git a/openviking/async_client.py b/openviking/async_client.py
index 89d0c93d..ea3aa83c 100644
--- a/openviking/async_client.py
+++ b/openviking/async_client.py
@@ -6,7 +6,6 @@
Supports both embedded mode (LocalClient) and HTTP mode (HTTPClient).
"""
-import os
import threading
from typing import Any, Dict, List, Optional, Union
@@ -15,7 +14,6 @@
from openviking.service.debug_service import SystemStatus
from openviking.session.user_id import UserIdentifier
from openviking.utils import get_logger
-from openviking.utils.config import OpenVikingConfig
logger = get_logger(__name__)
@@ -24,51 +22,22 @@ class AsyncOpenViking:
"""
OpenViking main client class (Asynchronous).
- Supports three deployment modes:
- - Embedded mode: Uses local VikingVectorIndex storage and auto-starts AGFS subprocess (singleton)
- - Service mode: Connects to remote VikingVectorIndex and AGFS services (not singleton)
+ Supports two deployment modes:
+ - Embedded mode: Uses local storage and auto-starts services (singleton)
- HTTP mode: Connects to remote OpenViking Server via HTTP API (not singleton)
Examples:
- # 1. Embedded mode (auto-starts local services)
+ # 1. Embedded mode (loads config from ov.conf)
client = AsyncOpenViking(path="./data")
await client.initialize()
- # 2. Service mode (connects to remote services)
- client = AsyncOpenViking(
- vectordb_url="http://localhost:5000",
- agfs_url="http://localhost:8080",
- user="alice"
- )
- await client.initialize()
-
- # 3. HTTP mode (connects to OpenViking Server)
+ # 2. HTTP mode (connects to OpenViking Server)
client = AsyncOpenViking(
url="http://localhost:8000",
api_key="your-api-key",
user="alice"
)
await client.initialize()
-
- # 4. Using Config Object for advanced configuration
- from openviking.utils.config import OpenVikingConfig
- from openviking.utils.config import StorageConfig, AGFSConfig, VectorDBBackendConfig
-
- config = OpenVikingConfig(
- storage=StorageConfig(
- agfs=AGFSConfig(
- backend="local",
- path="./custom_data",
- ),
- vectordb=VectorDBBackendConfig(
- backend="local",
- path="./custom_data",
- )
- )
- )
-
- client = AsyncOpenViking(config=config)
- await client.initialize()
"""
_instance: Optional["AsyncOpenViking"] = None
@@ -76,16 +45,10 @@ class AsyncOpenViking:
def __new__(cls, *args, **kwargs):
# HTTP mode: no singleton
- url = kwargs.get("url") or os.environ.get("OPENVIKING_URL")
+ url = kwargs.get("url")
if url:
return object.__new__(cls)
- # Service mode: no singleton
- vectordb_url = kwargs.get("vectordb_url")
- agfs_url = kwargs.get("agfs_url")
- if vectordb_url and agfs_url:
- return object.__new__(cls)
-
# Embedded mode: use singleton
if cls._instance is None:
with cls._lock:
@@ -98,23 +61,17 @@ def __init__(
path: Optional[str] = None,
url: Optional[str] = None,
api_key: Optional[str] = None,
- vectordb_url: Optional[str] = None,
- agfs_url: Optional[str] = None,
user: Optional[UserIdentifier] = None,
- config: Optional[OpenVikingConfig] = None,
**kwargs,
):
"""
Initialize OpenViking client.
Args:
- path: Local storage path for embedded mode.
+ path: Local storage path for embedded mode (overrides ov.conf storage path).
url: OpenViking Server URL for HTTP mode.
api_key: API key for HTTP mode authentication.
- vectordb_url: Remote VectorDB service URL for service mode.
- agfs_url: Remote AGFS service URL for service mode.
user: UserIdentifier object for session management.
- config: OpenVikingConfig object for advanced configuration.
**kwargs: Additional configuration parameters.
"""
# Singleton guard for repeated initialization
@@ -127,22 +84,15 @@ def __init__(
self._initialized = False
self._singleton_initialized = True
- # Environment variable fallback for HTTP mode
- url = url or os.environ.get("OPENVIKING_URL")
- api_key = api_key or os.environ.get("OPENVIKING_API_KEY")
-
- # Create the appropriate client - only _client, no _service
+ # Create the appropriate client
if url:
# HTTP mode
self._client: BaseClient = HTTPClient(url=url, api_key=api_key, user=self.user)
else:
- # Local/Service mode - LocalClient creates and owns the OpenVikingService
+ # Embedded mode - LocalClient loads config from ov.conf singleton
self._client: BaseClient = LocalClient(
path=path,
- vectordb_url=vectordb_url,
- agfs_url=agfs_url,
user=self.user,
- config=config,
)
# ============= Lifecycle methods =============
@@ -184,6 +134,36 @@ def session(self, session_id: Optional[str] = None) -> Session:
"""
return self._client.session(session_id)
+ async def create_session(self) -> Dict[str, Any]:
+ """Create a new session."""
+ await self._ensure_initialized()
+ return await self._client.create_session()
+
+ async def list_sessions(self) -> List[Any]:
+ """List all sessions."""
+ await self._ensure_initialized()
+ return await self._client.list_sessions()
+
+ async def get_session(self, session_id: str) -> Dict[str, Any]:
+ """Get session details."""
+ await self._ensure_initialized()
+ return await self._client.get_session(session_id)
+
+ async def delete_session(self, session_id: str) -> None:
+ """Delete a session."""
+ await self._ensure_initialized()
+ await self._client.delete_session(session_id)
+
+ async def add_message(self, session_id: str, role: str, content: str) -> Dict[str, Any]:
+ """Add a message to a session."""
+ await self._ensure_initialized()
+ return await self._client.add_message(session_id=session_id, role=role, content=content)
+
+ async def commit_session(self, session_id: str) -> Dict[str, Any]:
+ """Commit a session (archive and extract memories)."""
+ await self._ensure_initialized()
+ return await self._client.commit_session(session_id)
+
# ============= Resource methods =============
async def add_resource(
@@ -242,6 +222,7 @@ async def search(
query: str,
target_uri: str = "",
session: Optional[Union["Session", Any]] = None,
+ session_id: Optional[str] = None,
limit: int = 10,
score_threshold: Optional[float] = None,
filter: Optional[Dict] = None,
@@ -253,6 +234,7 @@ async def search(
query: Query string
target_uri: Target directory URI
session: Session object for context
+ session_id: Session ID string (alternative to session object)
limit: Max results
filter: Metadata filters
@@ -260,11 +242,11 @@ async def search(
FindResult
"""
await self._ensure_initialized()
- session_id = session.session_id if session else None
+ sid = session_id or (session.session_id if session else None)
return await self._client.search(
query=query,
target_uri=target_uri,
- session_id=session_id,
+ session_id=sid,
limit=limit,
score_threshold=score_threshold,
filter=filter,
diff --git a/openviking/cli/__init__.py b/openviking/cli/__init__.py
new file mode 100644
index 00000000..b90ea3d0
--- /dev/null
+++ b/openviking/cli/__init__.py
@@ -0,0 +1,7 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""OpenViking CLI package."""
+
+from openviking.cli.main import app
+
+__all__ = ["app"]
diff --git a/openviking/cli/commands/__init__.py b/openviking/cli/commands/__init__.py
new file mode 100644
index 00000000..551a6941
--- /dev/null
+++ b/openviking/cli/commands/__init__.py
@@ -0,0 +1,34 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""Command registration for OpenViking CLI."""
+
+import typer
+
+from openviking.cli.commands import (
+ content,
+ debug,
+ filesystem,
+ observer,
+ pack,
+ relations,
+ resources,
+ search,
+ serve,
+ session,
+ system,
+)
+
+
+def register_commands(app: typer.Typer) -> None:
+ """Register all supported commands into the root CLI app."""
+ serve.register(app)
+ resources.register(app)
+ filesystem.register(app)
+ content.register(app)
+ search.register(app)
+ relations.register(app)
+ pack.register(app)
+ system.register(app)
+ debug.register(app)
+ observer.register(app)
+ session.register(app)
diff --git a/openviking/cli/commands/content.py b/openviking/cli/commands/content.py
new file mode 100644
index 00000000..dee30973
--- /dev/null
+++ b/openviking/cli/commands/content.py
@@ -0,0 +1,36 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""Content reading commands."""
+
+import typer
+
+from openviking.cli.errors import run
+
+
+
+def register(app: typer.Typer) -> None:
+ """Register content commands."""
+
+ @app.command("read")
+ def read_command(
+ ctx: typer.Context,
+ uri: str = typer.Argument(..., help="Viking URI"),
+ ) -> None:
+ """Read full file content (L2)."""
+ run(ctx, lambda client: client.read(uri))
+
+ @app.command("abstract")
+ def abstract_command(
+ ctx: typer.Context,
+ uri: str = typer.Argument(..., help="Viking URI"),
+ ) -> None:
+ """Read abstract content (L0)."""
+ run(ctx, lambda client: client.abstract(uri))
+
+ @app.command("overview")
+ def overview_command(
+ ctx: typer.Context,
+ uri: str = typer.Argument(..., help="Viking URI"),
+ ) -> None:
+ """Read overview content (L1)."""
+ run(ctx, lambda client: client.overview(uri))
diff --git a/openviking/cli/commands/debug.py b/openviking/cli/commands/debug.py
new file mode 100644
index 00000000..62bca6ce
--- /dev/null
+++ b/openviking/cli/commands/debug.py
@@ -0,0 +1,27 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""Debug and health commands."""
+
+import typer
+
+from openviking.cli.context import get_cli_context
+from openviking.cli.errors import execute_client_command, run
+from openviking.cli.output import output_success
+
+
+def register(app: typer.Typer) -> None:
+ """Register debug commands."""
+
+ @app.command("status")
+ def status_command(ctx: typer.Context) -> None:
+ """Show OpenViking component status."""
+ run(ctx, lambda client: client.get_status())
+
+ @app.command("health")
+ def health_command(ctx: typer.Context) -> None:
+ """Quick health check."""
+ cli_ctx = get_cli_context(ctx)
+ result = execute_client_command(cli_ctx, lambda client: client.is_healthy())
+ output_success(cli_ctx, {"healthy": result})
+ if not result:
+ raise typer.Exit(1)
diff --git a/openviking/cli/commands/filesystem.py b/openviking/cli/commands/filesystem.py
new file mode 100644
index 00000000..c6a2093d
--- /dev/null
+++ b/openviking/cli/commands/filesystem.py
@@ -0,0 +1,68 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""Filesystem commands."""
+
+import typer
+
+from openviking.cli.errors import run
+
+
+def register(app: typer.Typer) -> None:
+ """Register filesystem commands."""
+
+ @app.command("ls")
+ def ls_command(
+ ctx: typer.Context,
+ uri: str = typer.Argument("viking://", help="Viking URI"),
+ simple: bool = typer.Option(False, "--simple", "-s", help="Simple path output"),
+ recursive: bool = typer.Option(
+ False,
+ "--recursive",
+ "-r",
+ help="List all subdirectories recursively",
+ ),
+ ) -> None:
+ """List directory contents."""
+ run(ctx, lambda client: client.ls(uri, simple=simple, recursive=recursive))
+
+ @app.command("tree")
+ def tree_command(
+ ctx: typer.Context,
+ uri: str = typer.Argument(..., help="Viking URI"),
+ ) -> None:
+ """Get directory tree."""
+ run(ctx, lambda client: client.tree(uri))
+
+ @app.command("mkdir")
+ def mkdir_command(
+ ctx: typer.Context,
+ uri: str = typer.Argument(..., help="Directory URI to create"),
+ ) -> None:
+ """Create a directory."""
+ run(ctx, lambda client: client.mkdir(uri))
+
+ @app.command("rm")
+ def rm_command(
+ ctx: typer.Context,
+ uri: str = typer.Argument(..., help="Viking URI to remove"),
+ recursive: bool = typer.Option(False, "--recursive", "-r", help="Remove recursively"),
+ ) -> None:
+ """Remove a resource."""
+ run(ctx, lambda client: client.rm(uri, recursive=recursive))
+
+ @app.command("mv")
+ def mv_command(
+ ctx: typer.Context,
+ from_uri: str = typer.Argument(..., help="Source URI"),
+ to_uri: str = typer.Argument(..., help="Target URI"),
+ ) -> None:
+ """Move or rename a resource."""
+ run(ctx, lambda client: client.mv(from_uri, to_uri))
+
+ @app.command("stat")
+ def stat_command(
+ ctx: typer.Context,
+ uri: str = typer.Argument(..., help="Viking URI"),
+ ) -> None:
+ """Get resource metadata and status."""
+ run(ctx, lambda client: client.stat(uri))
diff --git a/openviking/cli/commands/observer.py b/openviking/cli/commands/observer.py
new file mode 100644
index 00000000..a825b6d7
--- /dev/null
+++ b/openviking/cli/commands/observer.py
@@ -0,0 +1,38 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""Observer commands."""
+
+import typer
+
+from openviking.cli.errors import run
+
+observer_app = typer.Typer(help="Observer status commands")
+
+
+@observer_app.command("queue")
+def observer_queue_command(ctx: typer.Context) -> None:
+ """Get queue status."""
+ run(ctx, lambda client: client.observer.queue)
+
+
+@observer_app.command("vikingdb")
+def observer_vikingdb_command(ctx: typer.Context) -> None:
+ """Get VikingDB status."""
+ run(ctx, lambda client: client.observer.vikingdb)
+
+
+@observer_app.command("vlm")
+def observer_vlm_command(ctx: typer.Context) -> None:
+ """Get VLM status."""
+ run(ctx, lambda client: client.observer.vlm)
+
+
+@observer_app.command("system")
+def observer_system_command(ctx: typer.Context) -> None:
+ """Get overall system status."""
+ run(ctx, lambda client: client.observer.system)
+
+
+def register(app: typer.Typer) -> None:
+ """Register observer command group."""
+ app.add_typer(observer_app, name="observer")
diff --git a/openviking/cli/commands/pack.py b/openviking/cli/commands/pack.py
new file mode 100644
index 00000000..011b84f7
--- /dev/null
+++ b/openviking/cli/commands/pack.py
@@ -0,0 +1,45 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""Import/export pack commands."""
+
+import typer
+
+from openviking.cli.errors import run
+
+
+def register(app: typer.Typer) -> None:
+ """Register pack commands."""
+
+ @app.command("export")
+ def export_command(
+ ctx: typer.Context,
+ uri: str = typer.Argument(..., help="Source URI"),
+ to: str = typer.Argument(..., help="Output .ovpack file path"),
+ ) -> None:
+ """Export context as .ovpack."""
+ run(ctx, lambda client: {"file": client.export_ovpack(uri, to)})
+
+ @app.command("import")
+ def import_command(
+ ctx: typer.Context,
+ file_path: str = typer.Argument(..., help="Input .ovpack file path"),
+ target_uri: str = typer.Argument(..., help="Target parent URI"),
+ force: bool = typer.Option(False, "--force", help="Overwrite when conflicts exist"),
+ no_vectorize: bool = typer.Option(
+ False,
+ "--no-vectorize",
+ help="Disable vectorization after import",
+ ),
+ ) -> None:
+ """Import .ovpack into target URI."""
+ run(
+ ctx,
+ lambda client: {
+ "uri": client.import_ovpack(
+ file_path=file_path,
+ target=target_uri,
+ force=force,
+ vectorize=not no_vectorize,
+ )
+ },
+ )
diff --git a/openviking/cli/commands/relations.py b/openviking/cli/commands/relations.py
new file mode 100644
index 00000000..3cd33f30
--- /dev/null
+++ b/openviking/cli/commands/relations.py
@@ -0,0 +1,58 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""Relation management commands."""
+
+from typing import List
+
+import typer
+
+from openviking.cli.context import get_cli_context
+from openviking.cli.errors import execute_client_command, run
+from openviking.cli.output import output_success
+
+
+def register(app: typer.Typer) -> None:
+ """Register relation commands."""
+
+ @app.command("relations")
+ def relations_command(
+ ctx: typer.Context,
+ uri: str = typer.Argument(..., help="Viking URI"),
+ ) -> None:
+ """List relations of a resource."""
+ run(ctx, lambda client: client.relations(uri))
+
+ @app.command("link")
+ def link_command(
+ ctx: typer.Context,
+ from_uri: str = typer.Argument(..., help="Source URI"),
+ to_uris: List[str] = typer.Argument(..., help="One or more target URIs"),
+ reason: str = typer.Option("", "--reason", help="Reason for linking"),
+ ) -> None:
+ """Create relation links from one URI to one or more targets."""
+ cli_ctx = get_cli_context(ctx)
+ result = execute_client_command(
+ cli_ctx,
+ lambda client: client.link(from_uri, to_uris, reason),
+ )
+ output_success(
+ cli_ctx,
+ result if result is not None else {"from": from_uri, "to": to_uris, "reason": reason},
+ )
+
+ @app.command("unlink")
+ def unlink_command(
+ ctx: typer.Context,
+ from_uri: str = typer.Argument(..., help="Source URI"),
+ to_uri: str = typer.Argument(..., help="Target URI to unlink"),
+ ) -> None:
+ """Remove a relation link."""
+ cli_ctx = get_cli_context(ctx)
+ result = execute_client_command(
+ cli_ctx,
+ lambda client: client.unlink(from_uri, to_uri),
+ )
+ output_success(
+ cli_ctx,
+ result if result is not None else {"from": from_uri, "to": to_uri},
+ )
diff --git a/openviking/cli/commands/resources.py b/openviking/cli/commands/resources.py
new file mode 100644
index 00000000..efa75eff
--- /dev/null
+++ b/openviking/cli/commands/resources.py
@@ -0,0 +1,54 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""Resource management commands."""
+
+from typing import Optional
+
+import typer
+
+from openviking.cli.errors import run
+
+
+
+def register(app: typer.Typer) -> None:
+ """Register resource commands."""
+
+ @app.command("add-resource")
+ def add_resource_command(
+ ctx: typer.Context,
+ path: str = typer.Argument(..., help="Local path or URL to import"),
+ to: Optional[str] = typer.Option(None, "--to", help="Target URI"),
+ reason: str = typer.Option("", help="Reason for import"),
+ instruction: str = typer.Option("", help="Additional instruction"),
+ wait: bool = typer.Option(False, "--wait", help="Wait until processing is complete"),
+ timeout: Optional[float] = typer.Option(None, help="Wait timeout in seconds"),
+ ) -> None:
+ """Add resources into OpenViking."""
+ run(
+ ctx,
+ lambda client: client.add_resource(
+ path=path,
+ target=to,
+ reason=reason,
+ instruction=instruction,
+ wait=wait,
+ timeout=timeout,
+ ),
+ )
+
+ @app.command("add-skill")
+ def add_skill_command(
+ ctx: typer.Context,
+ data: str = typer.Argument(..., help="Skill directory, SKILL.md, or raw content"),
+ wait: bool = typer.Option(False, "--wait", help="Wait until processing is complete"),
+ timeout: Optional[float] = typer.Option(None, help="Wait timeout in seconds"),
+ ) -> None:
+ """Add a skill into OpenViking."""
+ run(
+ ctx,
+ lambda client: client.add_skill(
+ data=data,
+ wait=wait,
+ timeout=timeout,
+ ),
+ )
diff --git a/openviking/cli/commands/search.py b/openviking/cli/commands/search.py
new file mode 100644
index 00000000..a752af56
--- /dev/null
+++ b/openviking/cli/commands/search.py
@@ -0,0 +1,86 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""Search commands."""
+
+from typing import Optional
+
+import typer
+
+from openviking.cli.errors import run
+
+
+def register(app: typer.Typer) -> None:
+ """Register search commands."""
+
+ @app.command("find")
+ def find_command(
+ ctx: typer.Context,
+ query: str = typer.Argument(..., help="Search query"),
+ uri: str = typer.Option("", "--uri", "-u", help="Target URI"),
+ limit: int = typer.Option(10, "--limit", "-n", help="Maximum number of results"),
+ threshold: Optional[float] = typer.Option(
+ None,
+ "--threshold",
+ "-t",
+ help="Score threshold",
+ ),
+ ) -> None:
+ """Run semantic retrieval."""
+ run(
+ ctx,
+ lambda client: client.find(
+ query=query,
+ target_uri=uri,
+ limit=limit,
+ score_threshold=threshold,
+ ),
+ )
+
+ @app.command("search")
+ def search_command(
+ ctx: typer.Context,
+ query: str = typer.Argument(..., help="Search query"),
+ uri: str = typer.Option("", "--uri", "-u", help="Target URI"),
+ session_id: Optional[str] = typer.Option(
+ None,
+ "--session-id",
+ help="Session ID for context-aware search",
+ ),
+ limit: int = typer.Option(10, "--limit", "-n", help="Maximum number of results"),
+ threshold: Optional[float] = typer.Option(
+ None,
+ "--threshold",
+ "-t",
+ help="Score threshold",
+ ),
+ ) -> None:
+ """Run context-aware retrieval."""
+ run(
+ ctx,
+ lambda client: client.search(
+ query=query,
+ target_uri=uri,
+ session_id=session_id,
+ limit=limit,
+ score_threshold=threshold,
+ ),
+ )
+
+ @app.command("grep")
+ def grep_command(
+ ctx: typer.Context,
+ uri: str = typer.Argument(..., help="Target URI"),
+ pattern: str = typer.Argument(..., help="Search pattern"),
+ ignore_case: bool = typer.Option(False, "--ignore-case", "-i", help="Case insensitive"),
+ ) -> None:
+ """Run content pattern search."""
+ run(ctx, lambda client: client.grep(uri, pattern, case_insensitive=ignore_case))
+
+ @app.command("glob")
+ def glob_command(
+ ctx: typer.Context,
+ pattern: str = typer.Argument(..., help="Glob pattern"),
+ uri: str = typer.Option("viking://", "--uri", "-u", help="Search root URI"),
+ ) -> None:
+ """Run file glob pattern search."""
+ run(ctx, lambda client: client.glob(pattern, uri=uri))
diff --git a/openviking/cli/commands/serve.py b/openviking/cli/commands/serve.py
new file mode 100644
index 00000000..d80f29ad
--- /dev/null
+++ b/openviking/cli/commands/serve.py
@@ -0,0 +1,45 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""`serve` command implementation."""
+
+import os
+from typing import Optional
+
+import typer
+
+from openviking.cli.context import get_cli_context
+from openviking.cli.errors import handle_command_error
+
+
+def register(app: typer.Typer) -> None:
+ """Register `serve` command."""
+
+ @app.command("serve")
+ def serve_command(
+ ctx: typer.Context,
+ host: Optional[str] = typer.Option(None, help="Host to bind to"),
+ port: Optional[int] = typer.Option(None, help="Port to bind to"),
+ config: Optional[str] = typer.Option(None, help="Path to ov.conf config file"),
+ ) -> None:
+ """Start OpenViking HTTP server."""
+ cli_ctx = get_cli_context(ctx)
+
+ try:
+ import uvicorn
+
+ from openviking.server.app import create_app
+ from openviking.server.config import load_server_config
+
+ if config is not None:
+ os.environ["OPENVIKING_CONFIG_FILE"] = config
+
+ server_config = load_server_config(config)
+ if host is not None:
+ server_config.host = host
+ if port is not None:
+ server_config.port = port
+
+ app_instance = create_app(server_config)
+ uvicorn.run(app_instance, host=server_config.host, port=server_config.port)
+ except Exception as exc: # noqa: BLE001
+ handle_command_error(cli_ctx, exc)
diff --git a/openviking/cli/commands/session.py b/openviking/cli/commands/session.py
new file mode 100644
index 00000000..355e96a8
--- /dev/null
+++ b/openviking/cli/commands/session.py
@@ -0,0 +1,71 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""Session commands."""
+
+import typer
+
+from openviking.cli.context import get_cli_context
+from openviking.cli.errors import execute_client_command, run
+from openviking.cli.output import output_success
+
+session_app = typer.Typer(help="Session management commands")
+
+
+@session_app.command("new")
+def session_new_command(
+ ctx: typer.Context,
+) -> None:
+ """Create a new session."""
+ run(ctx, lambda client: client.create_session())
+
+
+@session_app.command("list")
+def session_list_command(ctx: typer.Context) -> None:
+ """List sessions."""
+ run(ctx, lambda client: client.list_sessions())
+
+
+@session_app.command("get")
+def session_get_command(
+ ctx: typer.Context,
+ session_id: str = typer.Argument(..., help="Session ID"),
+) -> None:
+ """Get session details."""
+ run(ctx, lambda client: client.get_session(session_id))
+
+
+@session_app.command("delete")
+def session_delete_command(
+ ctx: typer.Context,
+ session_id: str = typer.Argument(..., help="Session ID"),
+) -> None:
+ """Delete a session."""
+ cli_ctx = get_cli_context(ctx)
+ result = execute_client_command(cli_ctx, lambda client: client.delete_session(session_id))
+ output_success(cli_ctx, result if result is not None else {"session_id": session_id})
+
+
+@session_app.command("add-message")
+def session_add_message_command(
+ ctx: typer.Context,
+ session_id: str = typer.Argument(..., help="Session ID"),
+ role: str = typer.Option(..., "--role", help="Message role, e.g. user/assistant"),
+ content: str = typer.Option(..., "--content", help="Message content"),
+) -> None:
+ """Add one message to a session."""
+ run(ctx, lambda client: client.add_message(session_id=session_id, role=role, content=content))
+
+
+@session_app.command("commit")
+def session_commit_command(
+ ctx: typer.Context,
+ session_id: str = typer.Argument(..., help="Session ID"),
+) -> None:
+ """Commit a session (archive messages and extract memories)."""
+ run(ctx, lambda client: client.commit_session(session_id))
+
+
+
+def register(app: typer.Typer) -> None:
+ """Register session command group."""
+ app.add_typer(session_app, name="session")
diff --git a/openviking/cli/commands/system.py b/openviking/cli/commands/system.py
new file mode 100644
index 00000000..63672672
--- /dev/null
+++ b/openviking/cli/commands/system.py
@@ -0,0 +1,21 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""System utility commands."""
+
+from typing import Optional
+
+import typer
+
+from openviking.cli.errors import run
+
+
+def register(app: typer.Typer) -> None:
+ """Register system utility commands."""
+
+ @app.command("wait")
+ def wait_command(
+ ctx: typer.Context,
+ timeout: Optional[float] = typer.Option(None, help="Wait timeout in seconds"),
+ ) -> None:
+ """Wait for queued async processing to complete."""
+ run(ctx, lambda client: client.wait_processed(timeout))
diff --git a/openviking/cli/context.py b/openviking/cli/context.py
new file mode 100644
index 00000000..989b8df1
--- /dev/null
+++ b/openviking/cli/context.py
@@ -0,0 +1,88 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""Runtime context and client factory for CLI commands."""
+
+from dataclasses import dataclass, field
+from typing import TYPE_CHECKING, Optional
+
+import typer
+
+from openviking.utils.config.config_loader import (
+ DEFAULT_CONFIG_DIR,
+ DEFAULT_OVCLI_CONF,
+ OPENVIKING_CLI_CONFIG_ENV,
+ require_config,
+)
+
+if TYPE_CHECKING:
+ from openviking.sync_client import SyncOpenViking
+
+
+class CliConfigError(ValueError):
+ """Raised when required CLI configuration is missing or invalid."""
+
+
+def build_sync_client(**kwargs) -> "SyncOpenViking":
+ """Create SyncOpenViking lazily to keep CLI import lightweight."""
+ from openviking.sync_client import SyncOpenViking
+
+ return SyncOpenViking(**kwargs)
+
+
+@dataclass
+class CLIContext:
+ """Shared state for one CLI invocation."""
+
+ json_output: bool = False
+ output_format: str = "table"
+ _client: Optional["SyncOpenViking"] = field(default=None, init=False, repr=False)
+
+ def get_client_http_only(self) -> "SyncOpenViking":
+ """Create an HTTP client from ovcli.conf."""
+ if self._client is not None:
+ return self._client
+
+ try:
+ cli_config = require_config(
+ None,
+ OPENVIKING_CLI_CONFIG_ENV,
+ DEFAULT_OVCLI_CONF,
+ "CLI",
+ )
+ except FileNotFoundError:
+ default_path = DEFAULT_CONFIG_DIR / DEFAULT_OVCLI_CONF
+ raise CliConfigError(
+ f"CLI configuration file not found.\n"
+ f"Please create {default_path} or set {OPENVIKING_CLI_CONFIG_ENV}.\n"
+ f"Example content: "
+ f'{{"url": "http://localhost:1933", "api_key": null}}'
+ )
+
+ url = cli_config.get("url")
+ if not url:
+ default_path = DEFAULT_CONFIG_DIR / DEFAULT_OVCLI_CONF
+ raise CliConfigError(
+ f'"url" is required in {default_path}.\n'
+ f'Example: {{"url": "http://localhost:1933"}}'
+ )
+
+ self._client = build_sync_client(
+ url=url,
+ api_key=cli_config.get("api_key"),
+ user=cli_config.get("user"),
+ )
+ return self._client
+
+ def close_client(self) -> None:
+ """Close the client if it has been created."""
+ if self._client is None:
+ return
+ self._client.close()
+ self._client = None
+
+
+def get_cli_context(ctx: typer.Context) -> CLIContext:
+ """Return a typed CLI context from Typer context."""
+ if not isinstance(ctx.obj, CLIContext):
+ raise RuntimeError("CLI context is not initialized")
+ return ctx.obj
diff --git a/openviking/cli/errors.py b/openviking/cli/errors.py
new file mode 100644
index 00000000..4b701fba
--- /dev/null
+++ b/openviking/cli/errors.py
@@ -0,0 +1,81 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""Exception handling helpers for CLI commands."""
+
+from typing import Any, Callable
+
+import httpx
+import typer
+
+from openviking.cli.context import CLIContext, CliConfigError, get_cli_context
+from openviking.cli.output import output_error, output_success
+from openviking.exceptions import OpenVikingError
+
+
+def handle_command_error(ctx: CLIContext, exc: Exception) -> None:
+ """Normalize command exceptions into user-facing output and exit codes."""
+ if isinstance(exc, typer.Exit):
+ raise exc
+
+ if isinstance(exc, CliConfigError):
+ output_error(
+ ctx,
+ message=str(exc),
+ code="CLI_CONFIG",
+ details={"config_file": "ovcli.conf"},
+ exit_code=2,
+ )
+
+ elif isinstance(exc, OpenVikingError):
+ output_error(
+ ctx,
+ message=exc.message,
+ code=exc.code,
+ details=exc.details,
+ exit_code=1,
+ )
+
+ elif isinstance(exc, (httpx.ConnectError, httpx.ConnectTimeout, httpx.ReadTimeout)):
+ output_error(
+ ctx,
+ message=(
+ "Failed to connect to OpenViking server. "
+ "Check the url in ovcli.conf and ensure the server is running."
+ ),
+ code="CONNECTION_ERROR",
+ exit_code=3,
+ details={"exception": str(exc)},
+ )
+
+ else:
+ output_error(
+ ctx,
+ message=str(exc),
+ code="CLI_ERROR",
+ exit_code=1,
+ details={"exception": type(exc).__name__},
+ )
+
+
+def execute_client_command(
+ ctx: CLIContext,
+ operation: Callable[[Any], Any],
+) -> Any:
+ """Run a client command with consistent error handling and cleanup."""
+ try:
+ client = ctx.get_client_http_only()
+ return operation(client)
+ except Exception as exc: # noqa: BLE001
+ handle_command_error(ctx, exc)
+ finally:
+ ctx.close_client()
+
+
+def run(
+ ctx: typer.Context,
+ fn: Callable[[Any], Any],
+) -> None:
+ """Execute a client command with boilerplate: context → execute → output."""
+ cli_ctx = get_cli_context(ctx)
+ result = execute_client_command(cli_ctx, fn)
+ output_success(cli_ctx, result)
diff --git a/openviking/cli/main.py b/openviking/cli/main.py
new file mode 100644
index 00000000..2f74cc43
--- /dev/null
+++ b/openviking/cli/main.py
@@ -0,0 +1,70 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""Typer entrypoint for OpenViking CLI."""
+
+from typing import Optional
+
+import typer
+
+from openviking.cli.commands import register_commands
+from openviking.cli.context import CLIContext
+from openviking.utils.config.config_loader import (
+ DEFAULT_OVCLI_CONF,
+ OPENVIKING_CLI_CONFIG_ENV,
+ resolve_config_path,
+ load_json_config,
+)
+
+app = typer.Typer(
+ help="OpenViking - An Agent-native context database",
+ no_args_is_help=True,
+ add_completion=False,
+)
+
+
+def _version_callback(value: bool) -> None:
+ if value:
+ from openviking import __version__
+
+ typer.echo(f"openviking {__version__}")
+ raise typer.Exit()
+
+
+@app.callback()
+def main(
+ ctx: typer.Context,
+ json_output: bool = typer.Option(
+ False, "--json", help="Compact JSON with {ok, result} wrapper (for scripts)"
+ ),
+ output_format: Optional[str] = typer.Option(
+ None, "--output", "-o", help="Output format: table (default), json"
+ ),
+ version: Optional[bool] = typer.Option(
+ None,
+ "--version",
+ callback=_version_callback,
+ is_eager=True,
+ help="Show version and exit",
+ ),
+) -> None:
+ """Configure shared CLI options."""
+ # Priority: --output CLI arg > ovcli.conf "output" field > default "table"
+ if output_format is None:
+ config_path = resolve_config_path(None, OPENVIKING_CLI_CONFIG_ENV, DEFAULT_OVCLI_CONF)
+ if config_path is not None:
+ try:
+ cfg = load_json_config(config_path)
+ output_format = cfg.get("output")
+ except (ValueError, FileNotFoundError):
+ pass
+ if output_format is None:
+ output_format = "table"
+
+ ctx.obj = CLIContext(json_output=json_output, output_format=output_format)
+
+
+register_commands(app)
+
+
+if __name__ == "__main__":
+ app()
diff --git a/openviking/cli/output.py b/openviking/cli/output.py
new file mode 100644
index 00000000..b93193d7
--- /dev/null
+++ b/openviking/cli/output.py
@@ -0,0 +1,166 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""CLI output helpers."""
+
+import json
+from dataclasses import asdict, is_dataclass
+from typing import Any, Dict, List, Optional
+
+import typer
+from tabulate import tabulate
+
+from openviking.cli.context import CLIContext
+
+_MAX_COL_WIDTH = 80
+
+
+def _to_serializable(value: Any) -> Any:
+ """Convert rich Python values to JSON-serializable primitives."""
+ if value is None or isinstance(value, (str, int, float, bool)):
+ return value
+ if hasattr(value, "to_dict"):
+ return _to_serializable(value.to_dict())
+ if is_dataclass(value):
+ return _to_serializable(asdict(value))
+ if isinstance(value, dict):
+ return {str(k): _to_serializable(v) for k, v in value.items()}
+ if isinstance(value, (list, tuple, set)):
+ return [_to_serializable(item) for item in value]
+ return str(value)
+
+
+def _truncate(val: Any) -> Any:
+ """Truncate a value to _MAX_COL_WIDTH for table display."""
+ s = str(val) if not isinstance(val, str) else val
+ return s[: _MAX_COL_WIDTH - 3] + "..." if len(s) > _MAX_COL_WIDTH else val
+
+
+def _format_list_table(rows: List[Dict[str, Any]]) -> Optional[str]:
+ """Render a list of dict rows as a table with truncation."""
+ if not rows:
+ return None
+ headers: List[str] = []
+ for row in rows:
+ for key in row.keys():
+ key_str = str(key)
+ if key_str not in headers:
+ headers.append(key_str)
+ if not headers:
+ return None
+ values = [[_truncate(row.get(h, "")) for h in headers] for row in rows]
+ return tabulate(values, headers=headers, tablefmt="plain")
+
+
+def _is_primitive_list(v: Any) -> bool:
+ return isinstance(v, list) and v and all(isinstance(r, (str, int, float, bool)) for r in v)
+
+
+def _is_dict_list(v: Any) -> bool:
+ return isinstance(v, list) and v and all(isinstance(r, dict) for r in v)
+
+
+def _to_table(data: Any) -> Optional[str]:
+ """Try to render data as a table. Returns None if not possible."""
+ # Rule 1: list[dict] -> multi-row table
+ if isinstance(data, list) and data and all(isinstance(r, dict) for r in data):
+ return _format_list_table(data)
+
+ if not isinstance(data, dict):
+ return None
+
+ # Rule 5: ComponentStatus (name + is_healthy + status)
+ if {"name", "is_healthy", "status"}.issubset(data):
+ health = "healthy" if data["is_healthy"] else "unhealthy"
+ return f"[{data['name']}] ({health})\n{data['status']}"
+
+ # Rule 6: SystemStatus (is_healthy + components)
+ if "components" in data and "is_healthy" in data:
+ lines: List[str] = []
+ for comp in data["components"].values():
+ table = _to_table(comp)
+ if table:
+ lines.append(table)
+ lines.append("")
+ health = "healthy" if data["is_healthy"] else "unhealthy"
+ lines.append(f"[system] ({health})")
+ if data.get("errors"):
+ lines.append(f"Errors: {', '.join(data['errors'])}")
+ return "\n".join(lines)
+
+ # Extract list fields
+ dict_lists = {k: v for k, v in data.items() if _is_dict_list(v)}
+ prim_lists = {k: v for k, v in data.items() if _is_primitive_list(v)}
+
+ # Rule 3a: single list[primitive] -> one item per line
+ if not dict_lists and len(prim_lists) == 1:
+ key, items = next(iter(prim_lists.items()))
+ col = key.rstrip("es") if key.endswith("es") else key.rstrip("s")
+ return _format_list_table([{col: item} for item in items])
+
+ # Rule 3b: single list[dict] -> render directly
+ if len(dict_lists) == 1 and not prim_lists:
+ return _format_list_table(next(iter(dict_lists.values())))
+
+ # Rule 2: multiple list[dict] -> flatten with type column
+ if dict_lists:
+ merged: List[Dict[str, Any]] = []
+ for key, items in dict_lists.items():
+ for item in items:
+ merged.append({"type": key.rstrip("s"), **item})
+ if merged:
+ return _format_list_table(merged)
+
+ # Rule 4: plain dict (no expandable lists) -> single-row horizontal table
+ if not dict_lists and not prim_lists:
+ return tabulate(
+ [[_truncate(v) for v in data.values()]], headers=data.keys(), tablefmt="plain"
+ )
+
+ return None
+
+
+def output_success(ctx: CLIContext, result: Any) -> None:
+ """Print successful command result."""
+ serializable = _to_serializable(result)
+
+ if ctx.json_output:
+ typer.echo(json.dumps({"ok": True, "result": serializable}, ensure_ascii=False))
+ return
+ if serializable is None:
+ return
+ if isinstance(serializable, str):
+ typer.echo(serializable)
+ return
+
+ if ctx.output_format == "table":
+ table = _to_table(serializable)
+ if table is not None:
+ typer.echo(table)
+ return
+
+ typer.echo(json.dumps(serializable, ensure_ascii=False, indent=2))
+
+
+def output_error(
+ ctx: CLIContext,
+ *,
+ message: str,
+ code: str,
+ exit_code: int,
+ details: Optional[Dict[str, Any]] = None,
+) -> None:
+ """Print error in JSON or plain format then exit."""
+ details = details or {}
+ if ctx.json_output:
+ payload = {
+ "ok": False,
+ "error": {
+ "code": code,
+ "message": message,
+ "details": _to_serializable(details),
+ },
+ }
+ typer.echo(json.dumps(payload, ensure_ascii=False), err=True)
+ else:
+ typer.echo(f"ERROR[{code}]: {message}", err=True)
+ raise typer.Exit(exit_code)
diff --git a/openviking/client/base.py b/openviking/client/base.py
index 57d125c1..08430452 100644
--- a/openviking/client/base.py
+++ b/openviking/client/base.py
@@ -169,7 +169,7 @@ async def unlink(self, from_uri: str, to_uri: str) -> None:
# ============= Sessions =============
@abstractmethod
- async def create_session(self, user: Optional[str] = None) -> Dict[str, Any]:
+ async def create_session(self) -> Dict[str, Any]:
"""Create a new session."""
...
@@ -189,13 +189,8 @@ async def delete_session(self, session_id: str) -> None:
...
@abstractmethod
- async def compress_session(self, session_id: str) -> Dict[str, Any]:
- """Compress a session (commit and archive)."""
- ...
-
- @abstractmethod
- async def extract_session(self, session_id: str) -> List[Any]:
- """Extract memories from a session."""
+ async def commit_session(self, session_id: str) -> Dict[str, Any]:
+ """Commit a session (archive and extract memories)."""
...
@abstractmethod
diff --git a/openviking/client/http.py b/openviking/client/http.py
index f483a825..d464a105 100644
--- a/openviking/client/http.py
+++ b/openviking/client/http.py
@@ -5,7 +5,6 @@
Implements BaseClient interface using HTTP calls to OpenViking Server.
"""
-import os
from typing import Any, Dict, List, Optional, Union
import httpx
@@ -123,7 +122,7 @@ def __init__(
user: User name for session management
"""
self._url = url.rstrip("/")
- self._api_key = api_key or os.environ.get("OPENVIKING_API_KEY")
+ self._api_key = api_key
self._user = user or UserIdentifier.the_default_user()
self._http: Optional[httpx.AsyncClient] = None
self._observer: Optional[_HTTPObserver] = None
@@ -418,11 +417,11 @@ async def unlink(self, from_uri: str, to_uri: str) -> None:
# ============= Sessions =============
- async def create_session(self, user: Optional[str] = None) -> Dict[str, Any]:
+ async def create_session(self) -> Dict[str, Any]:
"""Create a new session."""
response = await self._http.post(
"/api/v1/sessions",
- json={"user": user or self._user.to_dict()},
+ json={},
)
return self._handle_response(response)
@@ -441,14 +440,9 @@ async def delete_session(self, session_id: str) -> None:
response = await self._http.delete(f"/api/v1/sessions/{session_id}")
self._handle_response(response)
- async def compress_session(self, session_id: str) -> Dict[str, Any]:
- """Compress a session (commit)."""
- response = await self._http.post(f"/api/v1/sessions/{session_id}/compress")
- return self._handle_response(response)
-
- async def extract_session(self, session_id: str) -> List[Any]:
- """Extract memories from a session."""
- response = await self._http.post(f"/api/v1/sessions/{session_id}/extract")
+ async def commit_session(self, session_id: str) -> Dict[str, Any]:
+ """Commit a session (archive and extract memories)."""
+ response = await self._http.post(f"/api/v1/sessions/{session_id}/commit")
return self._handle_response(response)
async def add_message(self, session_id: str, role: str, content: str) -> Dict[str, Any]:
diff --git a/openviking/client/local.py b/openviking/client/local.py
index f1983b44..3a7a541b 100644
--- a/openviking/client/local.py
+++ b/openviking/client/local.py
@@ -22,26 +22,17 @@ class LocalClient(BaseClient):
def __init__(
self,
path: Optional[str] = None,
- vectordb_url: Optional[str] = None,
- agfs_url: Optional[str] = None,
user: Optional[UserIdentifier] = None,
- config: Optional[OpenVikingConfig] = None,
):
"""Initialize LocalClient.
Args:
- path: Local storage path
- vectordb_url: Remote VectorDB service URL for service mode
- agfs_url: Remote AGFS service URL for service mode
+ path: Local storage path (overrides ov.conf storage path)
user: User name for session management
- config: OpenVikingConfig object for advanced configuration
"""
self._service = OpenVikingService(
path=path,
- vectordb_url=vectordb_url,
- agfs_url=agfs_url,
user=user or UserIdentifier.the_default_user(),
- config=config,
)
self._user = self._service.user
@@ -204,7 +195,7 @@ async def unlink(self, from_uri: str, to_uri: str) -> None:
# ============= Sessions =============
- async def create_session(self, user: Optional[str] = None) -> Dict[str, Any]:
+ async def create_session(self) -> Dict[str, Any]:
"""Create a new session."""
session = self._service.sessions.session()
return {
@@ -230,13 +221,9 @@ async def delete_session(self, session_id: str) -> None:
"""Delete a session."""
await self._service.sessions.delete(session_id)
- async def compress_session(self, session_id: str) -> Dict[str, Any]:
- """Compress a session (commit)."""
- return await self._service.sessions.compress(session_id)
-
- async def extract_session(self, session_id: str) -> List[Any]:
- """Extract memories from a session."""
- return await self._service.sessions.extract(session_id)
+ async def commit_session(self, session_id: str) -> Dict[str, Any]:
+ """Commit a session (archive and extract memories)."""
+ return await self._service.sessions.commit(session_id)
async def add_message(self, session_id: str, role: str, content: str) -> Dict[str, Any]:
"""Add a message to a session."""
diff --git a/openviking/client/session.py b/openviking/client/session.py
index bb99b643..0dc035e2 100644
--- a/openviking/client/session.py
+++ b/openviking/client/session.py
@@ -5,7 +5,7 @@
Session delegates all operations to the underlying Client (LocalClient or HTTPClient).
"""
-from typing import TYPE_CHECKING, Any, Dict, List
+from typing import TYPE_CHECKING, Any, Dict
from openviking.session.user_id import UserIdentifier
@@ -29,7 +29,7 @@ def __init__(self, client: "BaseClient", session_id: str, user: UserIdentifier):
user: User name
"""
self._client = client
- self.id = session_id
+ self.session_id = session_id
self.user = user
async def add_message(self, role: str, content: str) -> Dict[str, Any]:
@@ -42,35 +42,19 @@ async def add_message(self, role: str, content: str) -> Dict[str, Any]:
Returns:
Result dict with session_id and message_count
"""
- return await self._client.add_message(self.id, role, content)
-
- async def compress(self) -> Dict[str, Any]:
- """Compress the session (commit and archive).
-
- Returns:
- Compression result
- """
- return await self._client.compress_session(self.id)
+ return await self._client.add_message(self.session_id, role, content)
async def commit(self) -> Dict[str, Any]:
- """Commit the session (alias for compress).
+ """Commit the session (archive messages and extract memories).
Returns:
Commit result
"""
- return await self._client.compress_session(self.id)
-
- async def extract(self) -> List[Any]:
- """Extract memories from the session.
-
- Returns:
- List of extracted memories
- """
- return await self._client.extract_session(self.id)
+ return await self._client.commit_session(self.session_id)
async def delete(self) -> None:
"""Delete the session."""
- await self._client.delete_session(self.id)
+ await self._client.delete_session(self.session_id)
async def load(self) -> Dict[str, Any]:
"""Load session data.
@@ -78,7 +62,7 @@ async def load(self) -> Dict[str, Any]:
Returns:
Session details
"""
- return await self._client.get_session(self.id)
+ return await self._client.get_session(self.session_id)
def __repr__(self) -> str:
- return f"Session(id={self.id}, user={self.user.__str__()})"
+ return f"Session(id={self.session_id}, user={self.user.__str__()})"
diff --git a/openviking/models/embedder/openai_embedders.py b/openviking/models/embedder/openai_embedders.py
index 34f3b593..61c5c998 100644
--- a/openviking/models/embedder/openai_embedders.py
+++ b/openviking/models/embedder/openai_embedders.py
@@ -2,7 +2,6 @@
# SPDX-License-Identifier: Apache-2.0
"""OpenAI Embedder Implementation"""
-import os
from typing import Any, Dict, List, Optional
import openai
@@ -53,17 +52,12 @@ def __init__(
"""
super().__init__(model_name, config)
- self.api_key = (
- api_key or os.getenv("OPENVIKING_EMBEDDING_API_KEY") or os.getenv("OPENAI_API_KEY")
- )
- self.api_base = api_base or os.getenv("OPENVIKING_EMBEDDING_API_BASE")
+ self.api_key = api_key
+ self.api_base = api_base
self.dimension = dimension
if not self.api_key:
- raise ValueError(
- "api_key is required. Set via parameter or "
- "OPENVIKING_EMBEDDING_API_KEY/OPENAI_API_KEY env var"
- )
+ raise ValueError("api_key is required")
# Initialize OpenAI client
client_kwargs = {"api_key": self.api_key}
diff --git a/openviking/models/embedder/vikingdb_embedders.py b/openviking/models/embedder/vikingdb_embedders.py
index 8ba507b9..e7b96be2 100644
--- a/openviking/models/embedder/vikingdb_embedders.py
+++ b/openviking/models/embedder/vikingdb_embedders.py
@@ -2,7 +2,6 @@
# SPDX-License-Identifier: Apache-2.0
"""VikingDB Embedder Implementation via HTTP API"""
-import os
from typing import Any, Dict, List, Optional
from openviking.models.embedder.base import (
@@ -25,9 +24,9 @@ def _init_vikingdb_client(
region: Optional[str] = None,
host: Optional[str] = None,
):
- self.ak = ak or os.getenv("OPENVIKING_EMBEDDING_API_KEY") or os.getenv("VOLC_ACCESSKEY")
- self.sk = sk or os.getenv("OPENVIKING_EMBEDDING_API_SECRET") or os.getenv("VOLC_SECRETKEY")
- self.region = region or os.getenv("VOLC_REGION", "cn-beijing")
+ self.ak = ak
+ self.sk = sk
+ self.region = region or "cn-beijing"
self.host = host
if not self.ak or not self.sk:
diff --git a/openviking/models/embedder/volcengine_embedders.py b/openviking/models/embedder/volcengine_embedders.py
index 5c3ca0d2..a7c1b677 100644
--- a/openviking/models/embedder/volcengine_embedders.py
+++ b/openviking/models/embedder/volcengine_embedders.py
@@ -2,7 +2,6 @@
# SPDX-License-Identifier: Apache-2.0
"""Volcengine Embedder Implementation"""
-import os
from typing import Any, Dict, List, Optional
import volcenginesdkarkruntime
@@ -81,14 +80,8 @@ def __init__(
"""
super().__init__(model_name, config)
- self.api_key = (
- api_key or os.getenv("OPENVIKING_EMBEDDING_API_KEY") or os.getenv("VOLC_API_KEY")
- )
- self.api_base = (
- api_base
- or os.getenv("OPENVIKING_EMBEDDING_API_BASE")
- or "https://ark.cn-beijing.volces.com/api/v3"
- )
+ self.api_key = api_key
+ self.api_base = api_base or "https://ark.cn-beijing.volces.com/api/v3"
self.dimension = dimension
self.input_type = input_type
diff --git a/openviking/models/vlm/backends/openai_vlm.py b/openviking/models/vlm/backends/openai_vlm.py
index 128da40b..c6c5b230 100644
--- a/openviking/models/vlm/backends/openai_vlm.py
+++ b/openviking/models/vlm/backends/openai_vlm.py
@@ -46,7 +46,7 @@ def _update_token_usage_from_response(self, response):
completion_tokens = response.usage.completion_tokens
self.update_token_usage(
model_name=self.model or "gpt-4o-mini",
- provider="openai",
+ provider=self.provider,
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
)
diff --git a/openviking/server/app.py b/openviking/server/app.py
index 611e9780..0ed38a57 100644
--- a/openviking/server/app.py
+++ b/openviking/server/app.py
@@ -53,12 +53,8 @@ async def lifespan(app: FastAPI):
"""Application lifespan handler."""
nonlocal service
if service is None:
- # Create and initialize service
- service = OpenVikingService(
- path=config.storage_path,
- vectordb_url=config.vectordb_url,
- agfs_url=config.agfs_url,
- )
+ # Create and initialize service (reads config from ov.conf singleton)
+ service = OpenVikingService()
await service.initialize()
logger.info("OpenVikingService initialized")
diff --git a/openviking/server/bootstrap.py b/openviking/server/bootstrap.py
index de3592d5..d9f22030 100644
--- a/openviking/server/bootstrap.py
+++ b/openviking/server/bootstrap.py
@@ -8,7 +8,7 @@
import uvicorn
from openviking.server.app import create_app
-from openviking.server.config import ServerConfig, load_server_config
+from openviking.server.config import load_server_config
def main():
@@ -29,45 +29,21 @@ def main():
default=None,
help="Port to bind to",
)
- parser.add_argument(
- "--path",
- type=str,
- default=None,
- help="Storage path for embedded mode",
- )
- parser.add_argument(
- "--vectordb-url",
- type=str,
- default=None,
- help="VectorDB service URL for service mode",
- )
- parser.add_argument(
- "--agfs-url",
- type=str,
- default=None,
- help="AGFS service URL for service mode",
- )
- parser.add_argument(
- "--api-key",
- type=str,
- default=None,
- help="API key for authentication (if not set, authentication is disabled)",
- )
parser.add_argument(
"--config",
type=str,
default=None,
- help="Path to config file",
+ help="Path to ov.conf config file",
)
args = parser.parse_args()
# Set OPENVIKING_CONFIG_FILE environment variable if --config is provided
- # This allows OpenVikingConfig to load from the specified config file
+ # This allows OpenVikingConfigSingleton to load from the specified config file
if args.config is not None:
os.environ["OPENVIKING_CONFIG_FILE"] = args.config
- # Load config from file and environment
+ # Load server config from ov.conf
config = load_server_config(args.config)
# Override with command line arguments
@@ -75,14 +51,6 @@ def main():
config.host = args.host
if args.port is not None:
config.port = args.port
- if args.path is not None:
- config.storage_path = args.path
- if args.vectordb_url is not None:
- config.vectordb_url = args.vectordb_url
- if args.agfs_url is not None:
- config.agfs_url = args.agfs_url
- if args.api_key is not None:
- config.api_key = args.api_key
# Create and run app
app = create_app(config)
diff --git a/openviking/server/config.py b/openviking/server/config.py
index e3031dae..d15718db 100644
--- a/openviking/server/config.py
+++ b/openviking/server/config.py
@@ -2,74 +2,66 @@
# SPDX-License-Identifier: Apache-2.0
"""Server configuration for OpenViking HTTP Server."""
-import json
-import os
from dataclasses import dataclass, field
-from pathlib import Path
from typing import List, Optional
+from openviking.utils.config.config_loader import (
+ DEFAULT_OV_CONF,
+ OPENVIKING_CONFIG_ENV,
+ load_json_config,
+ resolve_config_path,
+)
+
@dataclass
class ServerConfig:
- """Server configuration."""
+ """Server configuration (from the ``server`` section of ov.conf)."""
host: str = "0.0.0.0"
port: int = 1933
api_key: Optional[str] = None
- storage_path: Optional[str] = None
- vectordb_url: Optional[str] = None
- agfs_url: Optional[str] = None
cors_origins: List[str] = field(default_factory=lambda: ["*"])
def load_server_config(config_path: Optional[str] = None) -> ServerConfig:
- """Load server configuration from file and environment variables.
+ """Load server configuration from ov.conf.
- Priority: command line args > environment variables > config file
+ Reads the ``server`` section of ov.conf and also ensures the full
+ ov.conf is loaded into the OpenVikingConfigSingleton so that model
+ and storage settings are available.
- Config file lookup:
- 1. Explicit config_path (from --config)
- 2. OPENVIKING_CONFIG_FILE environment variable
+ Resolution chain:
+ 1. Explicit ``config_path`` (from --config)
+ 2. OPENVIKING_CONFIG_FILE environment variable
+ 3. ~/.openviking/ov.conf
Args:
- config_path: Path to config file.
+ config_path: Explicit path to ov.conf.
Returns:
- ServerConfig instance
- """
- config = ServerConfig()
-
- # Load from config file
- if config_path is None:
- config_path = os.environ.get("OPENVIKING_CONFIG_FILE")
-
- if config_path and Path(config_path).exists():
- with open(config_path) as f:
- data = json.load(f) or {}
+ ServerConfig instance with defaults for missing fields.
- server_data = data.get("server", {})
- config.host = server_data.get("host", config.host)
- config.port = server_data.get("port", config.port)
- config.api_key = server_data.get("api_key", config.api_key)
- config.cors_origins = server_data.get("cors_origins", config.cors_origins)
-
- storage_data = data.get("storage", {})
- config.storage_path = storage_data.get("path", config.storage_path)
- config.vectordb_url = storage_data.get("vectordb_url", config.vectordb_url)
- config.agfs_url = storage_data.get("agfs_url", config.agfs_url)
-
- # Override with environment variables
- if os.environ.get("OPENVIKING_HOST"):
- config.host = os.environ["OPENVIKING_HOST"]
- if os.environ.get("OPENVIKING_PORT"):
- config.port = int(os.environ["OPENVIKING_PORT"])
- if os.environ.get("OPENVIKING_API_KEY"):
- config.api_key = os.environ["OPENVIKING_API_KEY"]
- if os.environ.get("OPENVIKING_PATH"):
- config.storage_path = os.environ["OPENVIKING_PATH"]
- if os.environ.get("OPENVIKING_VECTORDB_URL"):
- config.vectordb_url = os.environ["OPENVIKING_VECTORDB_URL"]
- if os.environ.get("OPENVIKING_AGFS_URL"):
- config.agfs_url = os.environ["OPENVIKING_AGFS_URL"]
+ Raises:
+ FileNotFoundError: If no config file is found.
+ """
+ path = resolve_config_path(config_path, OPENVIKING_CONFIG_ENV, DEFAULT_OV_CONF)
+ if path is None:
+ from openviking.utils.config.config_loader import DEFAULT_CONFIG_DIR
+ default_path = DEFAULT_CONFIG_DIR / DEFAULT_OV_CONF
+ raise FileNotFoundError(
+ f"OpenViking configuration file not found.\n"
+ f"Please create {default_path} or set {OPENVIKING_CONFIG_ENV}.\n"
+ f"See: https://openviking.dev/docs/guides/configuration"
+ )
+
+ data = load_json_config(path)
+ server_data = data.get("server", {})
+
+ config = ServerConfig(
+ host=server_data.get("host", "0.0.0.0"),
+ port=server_data.get("port", 1933),
+ api_key=server_data.get("api_key"),
+ cors_origins=server_data.get("cors_origins", ["*"]),
+ )
return config
diff --git a/openviking/server/routers/sessions.py b/openviking/server/routers/sessions.py
index 0fa17382..b36b7657 100644
--- a/openviking/server/routers/sessions.py
+++ b/openviking/server/routers/sessions.py
@@ -3,7 +3,6 @@
"""Sessions endpoints for OpenViking HTTP Server."""
from typing import Any, Optional
-
from fastapi import APIRouter, Depends, Path
from pydantic import BaseModel
@@ -15,12 +14,6 @@
router = APIRouter(prefix="/api/v1/sessions", tags=["sessions"])
-class CreateSessionRequest(BaseModel):
- """Request model for creating a session."""
-
- user: Optional[str] = None
-
-
class AddMessageRequest(BaseModel):
"""Request model for adding a message."""
@@ -42,7 +35,6 @@ def _to_jsonable(value: Any) -> Any:
@router.post("")
async def create_session(
- request: CreateSessionRequest,
_: bool = Depends(verify_api_key),
):
"""Create a new session."""
@@ -52,7 +44,7 @@ async def create_session(
status="ok",
result={
"session_id": session.session_id,
- "user": session.user,
+ "user": session.user.to_dict(),
},
)
@@ -80,7 +72,7 @@ async def get_session(
status="ok",
result={
"session_id": session.session_id,
- "user": session.user,
+ "user": session.user.to_dict(),
"message_count": len(session.messages),
},
)
@@ -97,14 +89,14 @@ async def delete_session(
return Response(status="ok", result={"session_id": session_id})
-@router.post("/{session_id}/compress")
-async def compress_session(
+@router.post("/{session_id}/commit")
+async def commit_session(
session_id: str = Path(..., description="Session ID"),
_: bool = Depends(verify_api_key),
):
- """Compress a session."""
+ """Commit a session (archive and extract memories)."""
service = get_service()
- result = await service.sessions.compress(session_id)
+ result = await service.sessions.commit(session_id)
return Response(status="ok", result=result)
diff --git a/openviking/service/core.py b/openviking/service/core.py
index 7077528a..7f5ac143 100644
--- a/openviking/service/core.py
+++ b/openviking/service/core.py
@@ -43,27 +43,18 @@ class OpenVikingService:
def __init__(
self,
path: Optional[str] = None,
- vectordb_url: Optional[str] = None,
- agfs_url: Optional[str] = None,
user: Optional[UserIdentifier] = None,
- config: Optional[OpenVikingConfig] = None,
):
"""Initialize OpenViking service.
Args:
- path: Local storage path for embedded mode.
- vectordb_url: Remote VectorDB service URL for service mode.
- agfs_url: Remote AGFS service URL for service mode.
+ path: Local storage path (overrides ov.conf storage path).
user: Username for session management.
- config: OpenVikingConfig object for advanced configuration.
"""
- # Initialize config
+ # Initialize config from ov.conf
config = initialize_openviking_config(
- config=config,
user=user,
path=path,
- vectordb_url=vectordb_url,
- agfs_url=agfs_url,
)
self._config = config
self._user = user or UserIdentifier(
@@ -187,6 +178,9 @@ async def initialize(self) -> None:
# Create context collection
await init_context_collection(self._vikingdb_manager)
+ # Start queues after collections are ready
+ self._vikingdb_manager.start_queues()
+
# Initialize VikingFS
self._viking_fs = init_viking_fs(
agfs_url=self._agfs_url or "http://localhost:8080",
diff --git a/openviking/service/resource_service.py b/openviking/service/resource_service.py
index a86103dd..11982328 100644
--- a/openviking/service/resource_service.py
+++ b/openviking/service/resource_service.py
@@ -8,7 +8,7 @@
from typing import Any, Dict, Optional
-from openviking.exceptions import InvalidArgumentError, NotInitializedError
+from openviking.exceptions import DeadlineExceededError, InvalidArgumentError, NotInitializedError
from openviking.session.user_id import UserIdentifier
from openviking.storage import VikingDBManager
from openviking.storage.queuefs import get_queue_manager
@@ -104,7 +104,10 @@ async def add_resource(
if wait:
qm = get_queue_manager()
- status = await qm.wait_complete(timeout=timeout)
+ try:
+ status = await qm.wait_complete(timeout=timeout)
+ except TimeoutError as exc:
+ raise DeadlineExceededError("queue processing", timeout) from exc
result["queue_status"] = {
name: {
"processed": s.processed,
@@ -142,7 +145,10 @@ async def add_skill(
if wait:
qm = get_queue_manager()
- status = await qm.wait_complete(timeout=timeout)
+ try:
+ status = await qm.wait_complete(timeout=timeout)
+ except TimeoutError as exc:
+ raise DeadlineExceededError("queue processing", timeout) from exc
result["queue_status"] = {
name: {
"processed": s.processed,
@@ -164,7 +170,10 @@ async def wait_processed(self, timeout: Optional[float] = None) -> Dict[str, Any
Queue status
"""
qm = get_queue_manager()
- status = await qm.wait_complete(timeout=timeout)
+ try:
+ status = await qm.wait_complete(timeout=timeout)
+ except TimeoutError as exc:
+ raise DeadlineExceededError("queue processing", timeout) from exc
return {
name: {
"processed": s.processed,
diff --git a/openviking/service/session_service.py b/openviking/service/session_service.py
index 682c3c3f..a905fedf 100644
--- a/openviking/service/session_service.py
+++ b/openviking/service/session_service.py
@@ -3,7 +3,7 @@
"""
Session Service for OpenViking.
-Provides session management operations: session, sessions, add_message, compress, extract, delete.
+Provides session management operations: session, sessions, add_message, commit, delete.
"""
from typing import Any, Dict, List, Optional
@@ -117,14 +117,14 @@ async def delete(self, session_id: str) -> bool:
logger.error(f"Failed to delete session {session_id}: {e}")
raise NotFoundError(session_id, "session")
- async def compress(self, session_id: str) -> Dict[str, Any]:
- """Compress a session (commit and archive).
+ async def commit(self, session_id: str) -> Dict[str, Any]:
+ """Commit a session (archive messages and extract memories).
Args:
- session_id: Session ID to compress
+ session_id: Session ID to commit
Returns:
- Compression result
+ Commit result
"""
self._ensure_initialized()
session = self.session(session_id)
diff --git a/openviking/storage/vikingdb_manager.py b/openviking/storage/vikingdb_manager.py
index 59f9b8f3..9d96105f 100644
--- a/openviking/storage/vikingdb_manager.py
+++ b/openviking/storage/vikingdb_manager.py
@@ -59,12 +59,11 @@ def __init__(
self._embedding_handler = None
self._semantic_processor = None
- # Initialize queue manager if AGFS URL is provided
+ # Initialize queue manager (but don't start yet — collections must be created first)
self._init_queue_manager()
if self._queue_manager:
self._init_embedding_queue()
self._init_semantic_queue()
- self._queue_manager.start()
def _init_queue_manager(self):
"""Initialize queue manager for background processing."""
@@ -112,6 +111,12 @@ def _init_semantic_queue(self):
)
logger.info("Semantic queue initialized with SemanticProcessor")
+ def start_queues(self):
+ """Start the queue manager. Call after collections are created."""
+ if self._queue_manager:
+ self._queue_manager.start()
+ logger.info("Queue manager started")
+
async def close(self) -> None:
"""Close storage connection and release resources, including queue manager."""
try:
diff --git a/openviking/sync_client.py b/openviking/sync_client.py
index 548be956..756a5f26 100644
--- a/openviking/sync_client.py
+++ b/openviking/sync_client.py
@@ -32,6 +32,30 @@ def session(self, session_id: Optional[str] = None) -> "Session":
"""Create new session or load existing session."""
return self._async_client.session(session_id)
+ def create_session(self) -> Dict[str, Any]:
+ """Create a new session."""
+ return run_async(self._async_client.create_session())
+
+ def list_sessions(self) -> List[Any]:
+ """List all sessions."""
+ return run_async(self._async_client.list_sessions())
+
+ def get_session(self, session_id: str) -> Dict[str, Any]:
+ """Get session details."""
+ return run_async(self._async_client.get_session(session_id))
+
+ def delete_session(self, session_id: str) -> None:
+ """Delete a session."""
+ run_async(self._async_client.delete_session(session_id))
+
+ def add_message(self, session_id: str, role: str, content: str) -> Dict[str, Any]:
+ """Add a message to a session."""
+ return run_async(self._async_client.add_message(session_id, role, content))
+
+ def commit_session(self, session_id: str) -> Dict[str, Any]:
+ """Commit a session (archive and extract memories)."""
+ return run_async(self._async_client.commit_session(session_id))
+
def add_resource(
self,
path: str,
@@ -60,13 +84,16 @@ def search(
query: str,
target_uri: str = "",
session: Optional["Session"] = None,
+ session_id: Optional[str] = None,
limit: int = 10,
score_threshold: Optional[float] = None,
filter: Optional[Dict] = None,
):
"""Execute complex retrieval (intent analysis, hierarchical retrieval)."""
return run_async(
- self._async_client.search(query, target_uri, session, limit, score_threshold, filter)
+ self._async_client.search(
+ query, target_uri, session, session_id, limit, score_threshold, filter
+ )
)
def find(
@@ -132,7 +159,7 @@ def rm(self, uri: str, recursive: bool = False) -> None:
"""Delete resource"""
return run_async(self._async_client.rm(uri, recursive))
- def wait_processed(self, timeout: float = None) -> None:
+ def wait_processed(self, timeout: float = None) -> Dict[str, Any]:
"""Wait for all async operations to complete"""
return run_async(self._async_client.wait_processed(timeout))
@@ -166,6 +193,8 @@ def get_status(self):
Returns:
SystemStatus containing health status of all components.
"""
+ if not self._initialized:
+ self.initialize()
return self._async_client.get_status()
def is_healthy(self) -> bool:
@@ -174,11 +203,15 @@ def is_healthy(self) -> bool:
Returns:
True if all components are healthy, False otherwise.
"""
+ if not self._initialized:
+ self.initialize()
return self._async_client.is_healthy()
@property
def observer(self):
"""Get observer service for component status."""
+ if not self._initialized:
+ self.initialize()
return self._async_client.observer
@classmethod
diff --git a/openviking/utils/config/__init__.py b/openviking/utils/config/__init__.py
index e6a81141..c28c3316 100644
--- a/openviking/utils/config/__init__.py
+++ b/openviking/utils/config/__init__.py
@@ -1,6 +1,16 @@
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: Apache-2.0
from .agfs_config import AGFSConfig
+from .config_loader import (
+ DEFAULT_CONFIG_DIR,
+ DEFAULT_OV_CONF,
+ DEFAULT_OVCLI_CONF,
+ OPENVIKING_CLI_CONFIG_ENV,
+ OPENVIKING_CONFIG_ENV,
+ load_json_config,
+ require_config,
+ resolve_config_path,
+)
from .embedding_config import EmbeddingConfig
from .open_viking_config import (
OpenVikingConfig,
@@ -30,7 +40,12 @@
__all__ = [
"AGFSConfig",
+ "DEFAULT_CONFIG_DIR",
+ "DEFAULT_OV_CONF",
+ "DEFAULT_OVCLI_CONF",
"EmbeddingConfig",
+ "OPENVIKING_CLI_CONFIG_ENV",
+ "OPENVIKING_CONFIG_ENV",
"OpenVikingConfig",
"OpenVikingConfigSingleton",
"RerankConfig",
@@ -50,6 +65,9 @@
"load_parser_configs_from_dict",
"PARSER_CONFIG_REGISTRY",
"get_openviking_config",
+ "load_json_config",
+ "require_config",
+ "resolve_config_path",
"set_openviking_config",
"is_valid_openviking_config",
]
diff --git a/openviking/utils/config/config_loader.py b/openviking/utils/config/config_loader.py
new file mode 100644
index 00000000..177763ff
--- /dev/null
+++ b/openviking/utils/config/config_loader.py
@@ -0,0 +1,114 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""Configuration file loading utilities.
+
+Provides a three-level resolution chain for locating config files:
+ 1. Explicit path (constructor parameter / --config)
+ 2. Environment variable
+ 3. Default path (~/.openviking/)
+"""
+
+import json
+import os
+from pathlib import Path
+from typing import Any, Dict, Optional
+
+DEFAULT_CONFIG_DIR = Path.home() / ".openviking"
+
+OPENVIKING_CONFIG_ENV = "OPENVIKING_CONFIG_FILE"
+OPENVIKING_CLI_CONFIG_ENV = "OPENVIKING_CLI_CONFIG_FILE"
+
+DEFAULT_OV_CONF = "ov.conf"
+DEFAULT_OVCLI_CONF = "ovcli.conf"
+
+
+def resolve_config_path(
+ explicit_path: Optional[str],
+ env_var: str,
+ default_filename: str,
+) -> Optional[Path]:
+ """Resolve a config file path using the three-level chain.
+
+ Resolution order:
+ 1. ``explicit_path`` (if provided and exists)
+ 2. Path from environment variable ``env_var``
+ 3. ``~/.openviking/``
+
+ Returns:
+ Path to the config file, or None if not found at any level.
+ """
+ # Level 1: explicit path
+ if explicit_path:
+ p = Path(explicit_path).expanduser()
+ if p.exists():
+ return p
+ return None
+
+ # Level 2: environment variable
+ env_val = os.environ.get(env_var)
+ if env_val:
+ p = Path(env_val).expanduser()
+ if p.exists():
+ return p
+ return None
+
+ # Level 3: default directory
+ p = DEFAULT_CONFIG_DIR / default_filename
+ if p.exists():
+ return p
+
+ return None
+
+
+def load_json_config(path: Path) -> Dict[str, Any]:
+ """Load and parse a JSON config file.
+
+ Args:
+ path: Path to the JSON config file.
+
+ Returns:
+ Parsed configuration dictionary.
+
+ Raises:
+ FileNotFoundError: If the file does not exist.
+ ValueError: If the file contains invalid JSON.
+ """
+ if not path.exists():
+ raise FileNotFoundError(f"Config file does not exist: {path}")
+
+ with open(path, "r", encoding="utf-8") as f:
+ try:
+ return json.load(f)
+ except json.JSONDecodeError as e:
+ raise ValueError(f"Invalid JSON in config file {path}: {e}") from e
+
+
+def require_config(
+ explicit_path: Optional[str],
+ env_var: str,
+ default_filename: str,
+ purpose: str,
+) -> Dict[str, Any]:
+ """Resolve and load a config file, raising a clear error if not found.
+
+ Args:
+ explicit_path: Explicitly provided config file path.
+ env_var: Environment variable name for the config path.
+ default_filename: Default filename under ~/.openviking/.
+ purpose: Human-readable description for error messages.
+
+ Returns:
+ Parsed configuration dictionary.
+
+ Raises:
+ FileNotFoundError: With a clear message if the config file is not found.
+ """
+ path = resolve_config_path(explicit_path, env_var, default_filename)
+ if path is None:
+ default_path = DEFAULT_CONFIG_DIR / default_filename
+ raise FileNotFoundError(
+ f"OpenViking {purpose} configuration file not found.\n"
+ f"Please create {default_path} or set {env_var}.\n"
+ f"See: https://openviking.dev/docs/guides/configuration"
+ )
+ return load_json_config(path)
diff --git a/openviking/utils/config/embedding_config.py b/openviking/utils/config/embedding_config.py
index ae3ef8c7..e0d9897c 100644
--- a/openviking/utils/config/embedding_config.py
+++ b/openviking/utils/config/embedding_config.py
@@ -1,6 +1,5 @@
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: Apache-2.0
-import os
from typing import Any, Optional
from pydantic import BaseModel, Field, model_validator
@@ -98,56 +97,6 @@ class EmbeddingConfig(BaseModel):
sparse: Optional[EmbeddingModelConfig] = Field(default=None)
hybrid: Optional[EmbeddingModelConfig] = Field(default=None)
- @model_validator(mode="before")
- @classmethod
- def apply_env_defaults(cls, data):
- if not isinstance(data, dict):
- return data
-
- config_sections = [
- ("dense", "OPENVIKING_EMBEDDING_DENSE_"),
- ("sparse", "OPENVIKING_EMBEDDING_SPARSE_"),
- ("hybrid", "OPENVIKING_EMBEDDING_HYBRID_"),
- ]
-
- env_fields = {
- "model": str,
- "api_key": str,
- "api_base": str,
- "dimension": int,
- "batch_size": int,
- "input": str,
- "provider": str,
- "backend": str,
- "version": str,
- "ak": str,
- "sk": str,
- "region": str,
- "host": str,
- }
-
- for section, prefix in config_sections:
- section_env_data = {}
- for field, field_type in env_fields.items():
- env_key = f"{prefix}{field.upper()}"
- val = os.getenv(env_key)
- if val is not None:
- try:
- section_env_data[field] = field_type(val)
- except ValueError:
- continue
-
- if section_env_data:
- if data.get(section) is None:
- data[section] = {}
-
- if isinstance(data[section], dict):
- for k, v in section_env_data.items():
- if data[section].get(k) is None:
- data[section][k] = v
-
- return data
-
@model_validator(mode="after")
def validate_config(self):
"""Validate configuration completeness and consistency"""
diff --git a/openviking/utils/config/open_viking_config.py b/openviking/utils/config/open_viking_config.py
index 7857951d..7acb56a9 100644
--- a/openviking/utils/config/open_viking_config.py
+++ b/openviking/utils/config/open_viking_config.py
@@ -1,15 +1,19 @@
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: Apache-2.0
import json
-import os
from pathlib import Path
from threading import Lock
from typing import Any, Dict, Optional
from pydantic import BaseModel, Field
+from .config_loader import (
+ DEFAULT_OV_CONF,
+ OPENVIKING_CONFIG_ENV,
+ resolve_config_path,
+ load_json_config,
+)
from openviking.session.user_id import UserIdentifier
-
from .embedding_config import EmbeddingConfig
from .parser_config import (
AudioConfig,
@@ -151,38 +155,67 @@ def to_dict(self) -> Dict[str, Any]:
class OpenVikingConfigSingleton:
- """Global singleton for OpenVikingConfig."""
+ """Global singleton for OpenVikingConfig.
+
+ Resolution chain for ov.conf:
+ 1. Explicit path passed to initialize()
+ 2. OPENVIKING_CONFIG_FILE environment variable
+ 3. ~/.openviking/ov.conf
+ 4. Error with clear guidance
+ """
_instance: Optional[OpenVikingConfig] = None
_lock: Lock = Lock()
@classmethod
def get_instance(cls) -> OpenVikingConfig:
- """Get the global singleton instance."""
+ """Get the global singleton instance.
+
+ Raises FileNotFoundError if no config file is found.
+ """
if cls._instance is None:
with cls._lock:
if cls._instance is None:
- # Check if environment variable specifies a config file
- config_file = os.getenv("OPENVIKING_CONFIG_FILE")
- if config_file and Path(config_file).exists():
- cls._instance = cls._load_from_file(config_file)
+ config_path = resolve_config_path(None, OPENVIKING_CONFIG_ENV, DEFAULT_OV_CONF)
+ if config_path is not None:
+ cls._instance = cls._load_from_file(str(config_path))
else:
- cls._instance = OpenVikingConfig()
+ from .config_loader import DEFAULT_CONFIG_DIR
+ default_path = DEFAULT_CONFIG_DIR / DEFAULT_OV_CONF
+ raise FileNotFoundError(
+ f"OpenViking configuration file not found.\n"
+ f"Please create {default_path} or set {OPENVIKING_CONFIG_ENV}.\n"
+ f"See: https://openviking.dev/docs/guides/configuration"
+ )
return cls._instance
@classmethod
- def initialize(cls, config: Optional[Dict[str, Any]] = None) -> OpenVikingConfig:
- """Initialize the global singleton with optional config dict."""
+ def initialize(
+ cls,
+ config_dict: Optional[Dict[str, Any]] = None,
+ config_path: Optional[str] = None,
+ ) -> OpenVikingConfig:
+ """Initialize the global singleton.
+
+ Args:
+ config_dict: Direct config dictionary (highest priority).
+ config_path: Explicit path to ov.conf file.
+ """
with cls._lock:
- if config is None:
- # Check if environment variable specifies a config file
- config_file = os.getenv("OPENVIKING_CONFIG_FILE")
- if config_file and Path(config_file).exists():
- cls._instance = cls._load_from_file(config_file)
- else:
- cls._instance = OpenVikingConfig()
+ if config_dict is not None:
+ cls._instance = OpenVikingConfig.from_dict(config_dict)
else:
- cls._instance = OpenVikingConfig.from_dict(config)
+ path = resolve_config_path(config_path, OPENVIKING_CONFIG_ENV, DEFAULT_OV_CONF)
+ if path is not None:
+ cls._instance = cls._load_from_file(str(path))
+ else:
+ from .config_loader import DEFAULT_CONFIG_DIR
+ default_path = DEFAULT_CONFIG_DIR / DEFAULT_OV_CONF
+ raise FileNotFoundError(
+ f"OpenViking configuration file not found.\n"
+ f"Please create {default_path} or set {OPENVIKING_CONFIG_ENV}.\n"
+ f"See: https://openviking.dev/docs/guides/configuration"
+ )
return cls._instance
@classmethod
@@ -217,7 +250,7 @@ def get_openviking_config() -> OpenVikingConfig:
def set_openviking_config(config: OpenVikingConfig) -> None:
"""Set the global OpenVikingConfig instance."""
- OpenVikingConfigSingleton.initialize(config.to_dict())
+ OpenVikingConfigSingleton.initialize(config_dict=config.to_dict())
def is_valid_openviking_config(config: OpenVikingConfig) -> bool:
@@ -259,34 +292,27 @@ def is_valid_openviking_config(config: OpenVikingConfig) -> bool:
def initialize_openviking_config(
- config: Optional[OpenVikingConfig] = None,
user: Optional[UserIdentifier] = None,
path: Optional[str] = None,
- vectordb_url: Optional[str] = None,
- agfs_url: Optional[str] = None,
) -> OpenVikingConfig:
"""
Initialize OpenViking configuration with provided parameters.
+ Loads ov.conf from the standard resolution chain, then applies
+ parameter overrides.
+
Args:
- config: Optional OpenVikingConfig object to use as base configuration
user: UserIdentifier for session management
path: Local storage path for embedded mode
- vectordb_url: Remote VectorDB service URL for service mode
- agfs_url: Remote AGFS service URL for service mode
Returns:
Configured OpenVikingConfig instance
Raises:
ValueError: If the resulting configuration is invalid
+ FileNotFoundError: If no config file is found
"""
- # Initialize config
- if config is not None:
- set_openviking_config(config)
- config = get_openviking_config()
- else:
- config = get_openviking_config()
+ config = get_openviking_config()
if user:
# Set user if provided, like a email address or a account_id
@@ -301,12 +327,6 @@ def initialize_openviking_config(
config.storage.agfs.path = path
config.storage.vectordb.backend = config.storage.vectordb.backend or "local"
config.storage.vectordb.path = path
- elif vectordb_url and agfs_url:
- # Service mode: remote services
- config.storage.agfs.backend = "local"
- config.storage.agfs.url = agfs_url
- config.storage.vectordb.backend = "http"
- config.storage.vectordb.url = vectordb_url
# Ensure vector dimension is synced if not set in storage
if config.storage.vectordb.dimension == 0:
diff --git a/openviking/utils/config/parser_config.py b/openviking/utils/config/parser_config.py
index fbfb794d..14931b6e 100644
--- a/openviking/utils/config/parser_config.py
+++ b/openviking/utils/config/parser_config.py
@@ -8,7 +8,6 @@
and can be loaded from ov.conf files.
"""
-import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, Optional, Union
@@ -86,42 +85,6 @@ def from_yaml(cls, yaml_path: Union[str, Path]) -> "ParserConfig":
return cls.from_dict(data)
- @classmethod
- def from_env(cls, prefix: str = "OPENVIKING_PARSER") -> "ParserConfig":
- """
- Load configuration from environment variables.
-
- Environment variables should be prefixed with the given prefix
- and use uppercase field names. For example:
- - OPENVIKING_PARSER_MAX_CONTENT_LENGTH=50000
-
- Args:
- prefix: Environment variable prefix
-
- Returns:
- ParserConfig instance
-
- Examples:
- >>> config = ParserConfig.from_env("OPENVIKING_PARSER")
- """
- data = {}
- for field_name in cls.__dataclass_fields__:
- env_var = f"{prefix}_{field_name.upper()}"
- if env_var in os.environ:
- value = os.environ[env_var]
- # Type conversion
- field_type = cls.__dataclass_fields__[field_name].type
- if field_type is bool:
- data[field_name] = value.lower() in ("true", "1", "yes")
- elif field_type is int:
- data[field_name] = int(value)
- elif field_type is float:
- data[field_name] = float(value)
- else:
- data[field_name] = value
-
- return cls.from_dict(data)
-
def validate(self) -> None:
"""
Validate configuration.
@@ -206,30 +169,6 @@ def validate(self) -> None:
if self.mineru_timeout <= 0:
raise ValueError("mineru_timeout must be positive")
- @classmethod
- def from_env(cls, prefix: str = "OPENVIKING_PDF") -> "PDFConfig":
- """
- Load PDF configuration from environment variables.
-
- Environment variables:
- - OPENVIKING_PDF_STRATEGY=auto
- - OPENVIKING_PDF_MINERU_ENDPOINT=https://...
- - OPENVIKING_PDF_MINERU_API_KEY=your-key
-
- Args:
- prefix: Environment variable prefix
-
- Returns:
- PDFConfig instance
-
- Examples:
- >>> # Set environment variables first:
- >>> # export OPENVIKING_PDF_STRATEGY=mineru
- >>> # export OPENVIKING_PDF_MINERU_API_KEY=your-key
- >>> config = PDFConfig.from_env()
- """
- return super().from_env(prefix)
-
@dataclass
class CodeConfig(ParserConfig):
@@ -283,24 +222,6 @@ def validate(self) -> None:
"Must be 'head', 'tail', or 'balanced'"
)
- @classmethod
- def from_env(cls, prefix: str = "OPENVIKING_CODE") -> "CodeConfig":
- """
- Load code configuration from environment variables.
-
- Environment variables:
- - OPENVIKING_CODE_ENABLE_AST=true
- - OPENVIKING_CODE_EXTRACT_FUNCTIONS=true
- - OPENVIKING_CODE_MAX_TOKEN_LIMIT=50000
-
- Args:
- prefix: Environment variable prefix
-
- Returns:
- CodeConfig instance
- """
- return super().from_env(prefix)
-
@dataclass
class ImageConfig(ParserConfig):
@@ -335,24 +256,6 @@ def validate(self) -> None:
if self.max_dimension <= 0:
raise ValueError("max_dimension must be positive")
- @classmethod
- def from_env(cls, prefix: str = "OPENVIKING_IMAGE") -> "ImageConfig":
- """
- Load image configuration from environment variables.
-
- Environment variables:
- - OPENVIKING_IMAGE_ENABLE_OCR=false
- - OPENVIKING_IMAGE_ENABLE_VLM=true
- - OPENVIKING_IMAGE_MAX_DIMENSION=2048
-
- Args:
- prefix: Environment variable prefix
-
- Returns:
- ImageConfig instance
- """
- return super().from_env(prefix)
-
@dataclass
class AudioConfig(ParserConfig):
@@ -385,23 +288,6 @@ def validate(self) -> None:
if not self.transcription_model:
raise ValueError("transcription_model cannot be empty")
- @classmethod
- def from_env(cls, prefix: str = "OPENVIKING_AUDIO") -> "AudioConfig":
- """
- Load audio configuration from environment variables.
-
- Environment variables:
- - OPENVIKING_AUDIO_ENABLE_TRANSCRIPTION=true
- - OPENVIKING_AUDIO_TRANSCRIPTION_MODEL=whisper-large-v3
-
- Args:
- prefix: Environment variable prefix
-
- Returns:
- AudioConfig instance
- """
- return super().from_env(prefix)
-
@dataclass
class VideoConfig(ParserConfig):
@@ -439,24 +325,6 @@ def validate(self) -> None:
if self.max_duration <= 0:
raise ValueError("max_duration must be positive")
- @classmethod
- def from_env(cls, prefix: str = "OPENVIKING_VIDEO") -> "VideoConfig":
- """
- Load video configuration from environment variables.
-
- Environment variables:
- - OPENVIKING_VIDEO_EXTRACT_FRAMES=true
- - OPENVIKING_VIDEO_FRAME_INTERVAL=10.0
- - OPENVIKING_VIDEO_MAX_DURATION=3600.0
-
- Args:
- prefix: Environment variable prefix
-
- Returns:
- VideoConfig instance
- """
- return super().from_env(prefix)
-
@dataclass
class MarkdownConfig(ParserConfig):
@@ -489,21 +357,6 @@ def validate(self) -> None:
if self.max_heading_depth < 1:
raise ValueError("max_heading_depth must be at least 1")
- @classmethod
- def from_env(cls, prefix: str = "OPENVIKING_MARKDOWN") -> "MarkdownConfig":
- """
- Load Markdown configuration from environment variables.
-
- Environment variables:
- - OPENVIKING_MARKDOWN_PRESERVE_LINKS=true
- Args:
- prefix: Environment variable prefix
-
- Returns:
- MarkdownConfig instance
- """
- return super().from_env(prefix)
-
@dataclass
class HTMLConfig(ParserConfig):
@@ -534,22 +387,6 @@ def validate(self) -> None:
# No additional validation needed for HTML config
- @classmethod
- def from_env(cls, prefix: str = "OPENVIKING_HTML") -> "HTMLConfig":
- """
- Load HTML configuration from environment variables.
-
- Environment variables:
- - OPENVIKING_HTML_EXTRACT_TEXT_ONLY=false
-
- Args:
- prefix: Environment variable prefix
-
- Returns:
- HTMLConfig instance
- """
- return super().from_env(prefix)
-
@dataclass
class TextConfig(ParserConfig):
@@ -582,22 +419,6 @@ def validate(self) -> None:
if self.max_paragraph_length <= 0:
raise ValueError("max_paragraph_length must be positive")
- @classmethod
- def from_env(cls, prefix: str = "OPENVIKING_TEXT") -> "TextConfig":
- """
- Load text configuration from environment variables.
-
- Environment variables:
- - OPENVIKING_TEXT_SPLIT_BY_PARAGRAPHS=true
-
- Args:
- prefix: Environment variable prefix
-
- Returns:
- TextConfig instance
- """
- return super().from_env(prefix)
-
# Configuration registry for dynamic loading
PARSER_CONFIG_REGISTRY = {
diff --git a/openviking/utils/config/rerank_config.py b/openviking/utils/config/rerank_config.py
index c3f5c73f..64f2271d 100644
--- a/openviking/utils/config/rerank_config.py
+++ b/openviking/utils/config/rerank_config.py
@@ -1,9 +1,8 @@
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: Apache-2.0
-import os
from typing import Optional
-from pydantic import BaseModel, Field, model_validator
+from pydantic import BaseModel, Field
class RerankConfig(BaseModel):
@@ -20,23 +19,6 @@ class RerankConfig(BaseModel):
default=0.1, description="Relevance threshold (score > threshold is relevant)"
)
- @model_validator(mode="before")
- @classmethod
- def apply_env_defaults(cls, data):
- """Apply environment variable defaults if manual config not provided."""
- if isinstance(data, dict):
- env_mapping = {
- "ak": "OPENVIKING_RERANK_AK",
- "sk": "OPENVIKING_RERANK_SK",
- "host": "OPENVIKING_RERANK_HOST",
- }
- for field, env_var in env_mapping.items():
- if data.get(field) is None:
- env_val = os.getenv(env_var)
- if env_val is not None:
- data[field] = env_val
- return data
-
def is_available(self) -> bool:
"""Check if rerank is configured."""
return self.ak is not None and self.sk is not None
diff --git a/openviking/utils/config/vlm_config.py b/openviking/utils/config/vlm_config.py
index 6511b9e6..8b1d84fb 100644
--- a/openviking/utils/config/vlm_config.py
+++ b/openviking/utils/config/vlm_config.py
@@ -1,6 +1,5 @@
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: Apache-2.0
-import os
from typing import Any, Literal, Optional
from pydantic import BaseModel, Field, model_validator
@@ -39,25 +38,6 @@ def sync_provider_backend(cls, data: Any) -> Any:
data["provider"] = backend
return data
- @model_validator(mode="before")
- @classmethod
- def apply_env_defaults(cls, data):
- """Read default values from environment variables."""
- if isinstance(data, dict):
- env_mapping = {
- "api_key": "OPENVIKING_VLM_API_KEY",
- "model": "OPENVIKING_VLM_MODEL",
- "api_base": "OPENVIKING_VLM_API_BASE",
- "provider": "OPENVIKING_VLM_PROVIDER",
- "backend": "OPENVIKING_VLM_BACKEND",
- }
- for field, env_var in env_mapping.items():
- if data.get(field) is None:
- env_val = os.getenv(env_var)
- if env_val is not None:
- data[field] = env_val
- return data
-
@model_validator(mode="after")
def validate_config(self):
"""Validate configuration completeness and consistency"""
diff --git a/pyproject.toml b/pyproject.toml
index f3058701..5556a74c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -52,6 +52,7 @@ dependencies = [
"urllib3>=2.6.3",
"protobuf>=6.33.5",
"pdfminer-six>=20251230",
+ "typer>=0.12.0",
]
[project.optional-dependencies]
@@ -78,7 +79,7 @@ Repository = "https://github.com/volcengine/openviking"
Issues = "https://github.com/volcengine/openviking/issues"
[project.scripts]
-openviking-viewer = "openviking.tools.viewer:main"
+openviking = "openviking.cli.main:app"
openviking-server = "openviking.server.bootstrap:main"
[tool.setuptools_scm]
diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py
new file mode 100644
index 00000000..a28f9968
--- /dev/null
+++ b/tests/cli/conftest.py
@@ -0,0 +1,101 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""CLI fixtures that run against a real OpenViking server process."""
+
+import json
+import os
+import socket
+import subprocess
+import sys
+import time
+from pathlib import Path
+from typing import Generator
+
+import httpx
+import pytest
+
+
+def _get_free_port() -> int:
+ """Reserve a free port for the test server."""
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.bind(("127.0.0.1", 0))
+ port = sock.getsockname()[1]
+ sock.close()
+ return port
+
+
+def _wait_for_health(url: str, timeout_s: float = 20.0) -> None:
+ """Poll the health endpoint until the server is ready."""
+ deadline = time.time() + timeout_s
+ last_error = None
+ while time.time() < deadline:
+ try:
+ response = httpx.get(f"{url}/health", timeout=1.0)
+ if response.status_code == 200:
+ return
+ except Exception as exc: # noqa: BLE001
+ last_error = exc
+ time.sleep(0.25)
+ raise RuntimeError(f"OpenViking server failed to start: {last_error}")
+
+
+@pytest.fixture(scope="session")
+def openviking_server(tmp_path_factory: pytest.TempPathFactory) -> Generator[str, None, None]:
+ """Start a real OpenViking server for CLI tests."""
+ storage_dir = tmp_path_factory.mktemp("openviking_cli_data")
+ port = _get_free_port()
+
+ # Load the base example config and override storage path + server port
+ base_conf_path = Path("examples/ov.conf").resolve()
+ with open(base_conf_path) as f:
+ conf_data = json.load(f)
+
+ conf_data.setdefault("server", {})
+ conf_data["server"]["host"] = "127.0.0.1"
+ conf_data["server"]["port"] = port
+
+ conf_data.setdefault("storage", {})
+ conf_data["storage"].setdefault("vectordb", {})
+ conf_data["storage"]["vectordb"]["backend"] = "local"
+ conf_data["storage"]["vectordb"]["path"] = str(storage_dir)
+ conf_data["storage"].setdefault("agfs", {})
+ conf_data["storage"]["agfs"]["backend"] = "local"
+ conf_data["storage"]["agfs"]["path"] = str(storage_dir)
+
+ # Write temporary ov.conf
+ tmp_conf = storage_dir / "ov.conf"
+ with open(tmp_conf, "w") as f:
+ json.dump(conf_data, f)
+
+ env = os.environ.copy()
+ env["OPENVIKING_CONFIG_FILE"] = str(tmp_conf)
+
+ cmd = [
+ sys.executable,
+ "-m",
+ "openviking",
+ "serve",
+ "--config",
+ str(tmp_conf),
+ ]
+
+ proc = subprocess.Popen(
+ cmd,
+ env=env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ )
+
+ url = f"http://127.0.0.1:{port}"
+
+ try:
+ _wait_for_health(url)
+ yield url
+ finally:
+ proc.terminate()
+ try:
+ proc.wait(timeout=10)
+ except subprocess.TimeoutExpired:
+ proc.kill()
+ proc.wait(timeout=10)
diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py
new file mode 100644
index 00000000..42c014cc
--- /dev/null
+++ b/tests/cli/test_cli.py
@@ -0,0 +1,243 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""CLI tests that use real HTTP requests."""
+
+import json
+import os
+import tempfile
+from pathlib import Path
+
+import pytest
+from typer.testing import CliRunner
+
+from openviking.cli.main import app
+
+runner = CliRunner()
+
+
+def _make_ovcli_conf(url: str, tmp_dir: str) -> str:
+ """Create a temporary ovcli.conf and return its path."""
+ conf_path = os.path.join(tmp_dir, "ovcli.conf")
+ with open(conf_path, "w") as f:
+ json.dump({"url": url, "api_key": None}, f)
+ return conf_path
+
+
+def _run_cli(args, server_url, env=None, expected_exit_code=0):
+ """Run a CLI command and optionally parse JSON output.
+
+ Args:
+ args: CLI arguments
+ server_url: OpenViking server URL
+ env: Extra environment variables
+ expected_exit_code: Expected exit code (default 0)
+
+ Returns:
+ Parsed JSON payload if exit_code is 0 and output is valid JSON,
+ otherwise the raw CliRunner Result.
+ """
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ conf_path = _make_ovcli_conf(server_url, tmp_dir)
+ merged_env = {"OPENVIKING_CLI_CONFIG_FILE": conf_path}
+ if env:
+ merged_env.update(env)
+ result = runner.invoke(app, ["--json", *args], env=merged_env)
+ assert result.exit_code == expected_exit_code, (
+ f"Expected exit_code={expected_exit_code}, got {result.exit_code}\n"
+ f"args={args}\n{result.output}"
+ )
+ if expected_exit_code != 0:
+ return result
+ try:
+ payload = json.loads(result.output)
+ except json.JSONDecodeError:
+ return result
+ assert payload["ok"] is True
+ return payload
+
+
+def test_requires_ovcli_conf():
+ result = runner.invoke(app, ["find", "hello"], env={})
+
+ assert result.exit_code == 2
+ assert "ovcli.conf" in result.output
+
+
+def test_cli_version():
+ """--version should print version string and exit 0."""
+ result = runner.invoke(app, ["--version"], env={})
+ assert result.exit_code == 0
+ assert "openviking" in result.output
+
+
+def test_cli_help_smoke():
+ """All commands should print help without errors."""
+ commands = [
+ # root
+ ["--help"],
+ # content
+ ["read", "--help"],
+ ["abstract", "--help"],
+ ["overview", "--help"],
+ # debug
+ ["status", "--help"],
+ ["health", "--help"],
+ # filesystem
+ ["ls", "--help"],
+ ["tree", "--help"],
+ ["mkdir", "--help"],
+ ["rm", "--help"],
+ ["mv", "--help"],
+ ["stat", "--help"],
+ # pack
+ ["export", "--help"],
+ ["import", "--help"],
+ # relations
+ ["relations", "--help"],
+ ["link", "--help"],
+ ["unlink", "--help"],
+ # resources
+ ["add-resource", "--help"],
+ ["add-skill", "--help"],
+ # search
+ ["find", "--help"],
+ ["search", "--help"],
+ ["grep", "--help"],
+ ["glob", "--help"],
+ # serve
+ ["serve", "--help"],
+ # system
+ ["wait", "--help"],
+ # observer group
+ ["observer", "--help"],
+ ["observer", "queue", "--help"],
+ ["observer", "vikingdb", "--help"],
+ ["observer", "vlm", "--help"],
+ ["observer", "system", "--help"],
+ # session group
+ ["session", "--help"],
+ ["session", "new", "--help"],
+ ["session", "list", "--help"],
+ ["session", "get", "--help"],
+ ["session", "delete", "--help"],
+ ["session", "add-message", "--help"],
+ ["session", "commit", "--help"],
+ ]
+
+ for args in commands:
+ result = runner.invoke(app, args, env={})
+ assert result.exit_code == 0, "command failed: {}\n{}".format(
+ " ".join(args), result.output
+ )
+
+
+def test_cli_connection_refused():
+ """Connecting to a non-existent server should exit with code 3."""
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ conf_path = _make_ovcli_conf("http://127.0.0.1:19999", tmp_dir)
+ result = runner.invoke(
+ app,
+ ["--json", "health"],
+ env={"OPENVIKING_CLI_CONFIG_FILE": conf_path},
+ )
+ assert result.exit_code == 3
+ payload = json.loads(result.output)
+ assert payload["ok"] is False
+ assert payload["error"]["code"] == "CONNECTION_ERROR"
+
+
+def test_cli_real_requests(openviking_server, tmp_path):
+ server_url = openviking_server
+
+ sample_file = tmp_path / "sample.txt"
+ sample_file.write_text("OpenViking CLI real request test")
+
+ # -- add resource --
+ add_payload = _run_cli(["add-resource", str(sample_file)], server_url)
+ root_uri = add_payload["result"].get("root_uri")
+ assert root_uri
+ assert isinstance(root_uri, str)
+
+ file_uri = root_uri
+ if file_uri.endswith("/"):
+ file_uri = f"{file_uri}{sample_file.name}"
+
+ # -- debug / system --
+ health_payload = _run_cli(["health"], server_url)
+ assert "result" in health_payload
+
+ _run_cli(["status"], server_url)
+ _run_cli(["observer", "queue"], server_url)
+ _run_cli(["observer", "vikingdb"], server_url)
+ _run_cli(["observer", "vlm"], server_url)
+ _run_cli(["observer", "system"], server_url)
+
+ # -- filesystem --
+ ls_payload = _run_cli(["ls", "viking://resources/"], server_url)
+ assert isinstance(ls_payload["result"], list)
+
+ _run_cli(["tree", "viking://resources/"], server_url)
+ _run_cli(["stat", file_uri], server_url)
+ _run_cli(["read", "viking://resources/.abstract.md"], server_url)
+ _run_cli(["abstract", "viking://resources"], server_url)
+ _run_cli(["overview", "viking://resources"], server_url)
+
+ # -- search --
+ _run_cli(["grep", root_uri, "OpenViking"], server_url)
+ _run_cli(["glob", "*.txt", "--uri", root_uri], server_url)
+ _run_cli(["search", "OpenViking", "--uri", root_uri], server_url)
+
+ # -- mkdir / mv / rm --
+ mkdir_uri = "viking://resources/cli-temp-dir"
+ moved_uri = "viking://resources/cli-temp-dir-moved"
+ _run_cli(["mkdir", mkdir_uri], server_url)
+ _run_cli(["mv", mkdir_uri, moved_uri], server_url)
+ _run_cli(["rm", moved_uri], server_url)
+
+ # -- relations --
+ _run_cli(["relations", file_uri], server_url)
+ _run_cli(["link", file_uri, root_uri, "--reason", "ref"], server_url)
+ _run_cli(["unlink", file_uri, root_uri], server_url)
+
+ # -- pack --
+ export_path = tmp_path / "openviking_cli_test.ovpack"
+ _run_cli(["export", file_uri, str(export_path)], server_url)
+ _run_cli(
+ ["import", str(export_path), "viking://resources/ovpack-import", "--no-vectorize"],
+ server_url,
+ )
+
+ # -- session lifecycle --
+ session_payload = _run_cli(["session", "new"], server_url)
+ session_id = session_payload["result"]["session_id"]
+ assert isinstance(session_id, str) and len(session_id) > 0
+
+ list_payload = _run_cli(["session", "list"], server_url)
+ assert isinstance(list_payload["result"], list)
+
+ get_payload = _run_cli(["session", "get", session_id], server_url)
+ assert get_payload["result"]["session_id"] == session_id
+
+ _run_cli(
+ [
+ "session",
+ "add-message",
+ session_id,
+ "--role",
+ "user",
+ "--content",
+ "hello",
+ ],
+ server_url,
+ )
+ _run_cli(["session", "delete", session_id], server_url)
+
+
+def test_cli_nonexistent_resource(openviking_server):
+ """Accessing a non-existent resource should fail with exit code 1."""
+ result = _run_cli(
+ ["stat", "viking://resources/does-not-exist-xyz"],
+ openviking_server,
+ expected_exit_code=1,
+ )
+ assert result.exit_code == 1
diff --git a/tests/client/test_lifecycle.py b/tests/client/test_lifecycle.py
index 0cdb2fe5..09d6193a 100644
--- a/tests/client/test_lifecycle.py
+++ b/tests/client/test_lifecycle.py
@@ -6,6 +6,7 @@
from pathlib import Path
from openviking import AsyncOpenViking
+from openviking.session.user_id import UserIdentifier
class TestClientInitialization:
@@ -34,7 +35,7 @@ class TestClientClose:
async def test_close_success(self, test_data_dir: Path):
"""Test normal close"""
await AsyncOpenViking.reset()
- client = AsyncOpenViking(path=str(test_data_dir), user="test")
+ client = AsyncOpenViking(path=str(test_data_dir), user=UserIdentifier.the_default_user("test"))
await client.initialize()
await client.close()
@@ -45,7 +46,7 @@ async def test_close_success(self, test_data_dir: Path):
async def test_close_idempotent(self, test_data_dir: Path):
"""Test repeated close is safe"""
await AsyncOpenViking.reset()
- client = AsyncOpenViking(path=str(test_data_dir), user="test")
+ client = AsyncOpenViking(path=str(test_data_dir), user=UserIdentifier.the_default_user("test"))
await client.initialize()
await client.close()
@@ -61,12 +62,12 @@ async def test_reset_clears_singleton(self, test_data_dir: Path):
"""Test reset clears singleton"""
await AsyncOpenViking.reset()
- client1 = AsyncOpenViking(path=str(test_data_dir), user="test")
+ client1 = AsyncOpenViking(path=str(test_data_dir), user=UserIdentifier.the_default_user("test"))
await client1.initialize()
await AsyncOpenViking.reset()
- client2 = AsyncOpenViking(path=str(test_data_dir), user="test")
+ client2 = AsyncOpenViking(path=str(test_data_dir), user=UserIdentifier.the_default_user("test"))
# Should be new instance after reset
assert client1 is not client2
@@ -80,8 +81,8 @@ async def test_embedded_mode_singleton(self, test_data_dir: Path):
"""Test embedded mode uses singleton"""
await AsyncOpenViking.reset()
- client1 = AsyncOpenViking(path=str(test_data_dir), user="test")
- client2 = AsyncOpenViking(path=str(test_data_dir), user="test")
+ client1 = AsyncOpenViking(path=str(test_data_dir), user=UserIdentifier.the_default_user("test"))
+ client2 = AsyncOpenViking(path=str(test_data_dir), user=UserIdentifier.the_default_user("test"))
assert client1 is client2
diff --git a/tests/conftest.py b/tests/conftest.py
index cff8681c..41a88d63 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -12,6 +12,7 @@
import pytest_asyncio
from openviking import AsyncOpenViking
+from openviking.session.user_id import UserIdentifier
# Test data root directory
TEST_ROOT = Path(__file__).parent
@@ -149,7 +150,7 @@ async def client(test_data_dir: Path) -> AsyncGenerator[AsyncOpenViking, None]:
"""Create initialized OpenViking client"""
await AsyncOpenViking.reset()
- client = AsyncOpenViking(path=str(test_data_dir), user="test_user")
+ client = AsyncOpenViking(path=str(test_data_dir), user=UserIdentifier.the_default_user("test_user"))
await client.initialize()
yield client
@@ -163,7 +164,7 @@ async def uninitialized_client(test_data_dir: Path) -> AsyncGenerator[AsyncOpenV
"""Create uninitialized OpenViking client (for testing initialization flow)"""
await AsyncOpenViking.reset()
- client = AsyncOpenViking(path=str(test_data_dir), user="test_user")
+ client = AsyncOpenViking(path=str(test_data_dir), user=UserIdentifier.the_default_user("test_user"))
yield client
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
index 8b4bbb6b..756bfae8 100644
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -23,6 +23,7 @@
from openviking.server.app import create_app
from openviking.server.config import ServerConfig
from openviking.service.core import OpenVikingService
+from openviking.session.user_id import UserIdentifier
TEST_ROOT = Path(__file__).parent
@@ -50,7 +51,7 @@ def server_url(temp_dir):
loop = asyncio.new_event_loop()
svc = OpenVikingService(
- path=str(temp_dir / "data"), user="test_user"
+ path=str(temp_dir / "data"), user=UserIdentifier.the_default_user("test_user")
)
loop.run_until_complete(svc.initialize())
diff --git a/tests/integration/test_full_workflow.py b/tests/integration/test_full_workflow.py
index 5c3816f9..e60db321 100644
--- a/tests/integration/test_full_workflow.py
+++ b/tests/integration/test_full_workflow.py
@@ -9,6 +9,7 @@
from openviking import AsyncOpenViking
from openviking.message import TextPart
+from openviking.session.user_id import UserIdentifier
@pytest_asyncio.fixture(scope="function")
@@ -16,7 +17,7 @@ async def integration_client(test_data_dir: Path):
"""Integration test client"""
await AsyncOpenViking.reset()
- client = AsyncOpenViking(path=str(test_data_dir), user="integration_test_user")
+ client = AsyncOpenViking(path=str(test_data_dir), user=UserIdentifier.the_default_user("integration_test_user"))
await client.initialize()
yield client
diff --git a/tests/integration/test_http_integration.py b/tests/integration/test_http_integration.py
index 07123800..90f9a2fe 100644
--- a/tests/integration/test_http_integration.py
+++ b/tests/integration/test_http_integration.py
@@ -12,6 +12,7 @@
from openviking import AsyncOpenViking
from openviking.client import HTTPClient
from openviking.exceptions import NotFoundError
+from openviking.session.user_id import UserIdentifier
class TestHTTPClientIntegration:
@@ -20,7 +21,7 @@ class TestHTTPClientIntegration:
@pytest_asyncio.fixture
async def client(self, server_url):
"""Create and initialize HTTPClient."""
- client = HTTPClient(url=server_url, user="test_user")
+ client = HTTPClient(url=server_url, user=UserIdentifier.the_default_user("test_user"))
await client.initialize()
yield client
await client.close()
@@ -83,7 +84,7 @@ class TestSessionIntegration:
@pytest_asyncio.fixture
async def client(self, server_url):
"""Create and initialize HTTPClient."""
- client = HTTPClient(url=server_url, user="test_user")
+ client = HTTPClient(url=server_url, user=UserIdentifier.the_default_user("test_user"))
await client.initialize()
yield client
await client.close()
@@ -92,7 +93,7 @@ async def client(self, server_url):
async def test_session_lifecycle(self, client):
"""Test session create, add message, and delete."""
# Create session
- result = await client.create_session(user="test_user")
+ result = await client.create_session()
assert "session_id" in result
session_id = result["session_id"]
@@ -124,7 +125,7 @@ class TestAsyncOpenVikingHTTPMode:
@pytest_asyncio.fixture
async def ov(self, server_url):
"""Create AsyncOpenViking in HTTP mode."""
- client = AsyncOpenViking(url=server_url, user="test_user")
+ client = AsyncOpenViking(url=server_url, user=UserIdentifier.the_default_user("test_user"))
await client.initialize()
yield client
await client.close()
diff --git a/tests/server/conftest.py b/tests/server/conftest.py
index 55373cad..9d749957 100644
--- a/tests/server/conftest.py
+++ b/tests/server/conftest.py
@@ -18,6 +18,7 @@
from openviking.server.app import create_app
from openviking.server.config import ServerConfig
from openviking.service.core import OpenVikingService
+from openviking.session.user_id import UserIdentifier
# ---------------------------------------------------------------------------
# Paths
@@ -67,7 +68,7 @@ def sample_markdown_file(temp_dir: Path) -> Path:
@pytest_asyncio.fixture(scope="function")
async def service(temp_dir: Path):
"""Create and initialize an OpenVikingService in embedded mode."""
- svc = OpenVikingService(path=str(temp_dir / "data"), user="test_user")
+ svc = OpenVikingService(path=str(temp_dir / "data"), user=UserIdentifier.the_default_user("test_user"))
await svc.initialize()
yield svc
await svc.close()
@@ -117,7 +118,7 @@ async def running_server(temp_dir: Path):
await AsyncOpenViking.reset()
svc = OpenVikingService(
- path=str(temp_dir / "sdk_data"), user="sdk_test_user"
+ path=str(temp_dir / "sdk_data"), user=UserIdentifier.the_default_user("sdk_test_user")
)
await svc.initialize()
diff --git a/tests/server/test_api_sessions.py b/tests/server/test_api_sessions.py
index 78ec8251..ed97639f 100644
--- a/tests/server/test_api_sessions.py
+++ b/tests/server/test_api_sessions.py
@@ -8,7 +8,7 @@
async def test_create_session(client: httpx.AsyncClient):
resp = await client.post(
- "/api/v1/sessions", json={"user": "test_user"}
+ "/api/v1/sessions", json={}
)
assert resp.status_code == 200
body = resp.json()
@@ -18,7 +18,7 @@ async def test_create_session(client: httpx.AsyncClient):
async def test_list_sessions(client: httpx.AsyncClient):
# Create a session first
- await client.post("/api/v1/sessions", json={"user": "test"})
+ await client.post("/api/v1/sessions", json={})
resp = await client.get("/api/v1/sessions")
assert resp.status_code == 200
body = resp.json()
@@ -28,7 +28,7 @@ async def test_list_sessions(client: httpx.AsyncClient):
async def test_get_session(client: httpx.AsyncClient):
create_resp = await client.post(
- "/api/v1/sessions", json={"user": "test"}
+ "/api/v1/sessions", json={}
)
session_id = create_resp.json()["result"]["session_id"]
@@ -41,7 +41,7 @@ async def test_get_session(client: httpx.AsyncClient):
async def test_add_message(client: httpx.AsyncClient):
create_resp = await client.post(
- "/api/v1/sessions", json={"user": "test"}
+ "/api/v1/sessions", json={}
)
session_id = create_resp.json()["result"]["session_id"]
@@ -57,7 +57,7 @@ async def test_add_message(client: httpx.AsyncClient):
async def test_add_multiple_messages(client: httpx.AsyncClient):
create_resp = await client.post(
- "/api/v1/sessions", json={"user": "test"}
+ "/api/v1/sessions", json={}
)
session_id = create_resp.json()["result"]["session_id"]
@@ -123,7 +123,7 @@ async def test_add_message_persistence_regression(
async def test_delete_session(client: httpx.AsyncClient):
create_resp = await client.post(
- "/api/v1/sessions", json={"user": "test"}
+ "/api/v1/sessions", json={}
)
session_id = create_resp.json()["result"]["session_id"]
@@ -133,7 +133,7 @@ async def test_delete_session(client: httpx.AsyncClient):
json={"role": "user", "content": "ensure persisted"},
)
# Compress to persist
- await client.post(f"/api/v1/sessions/{session_id}/compress")
+ await client.post(f"/api/v1/sessions/{session_id}/commit")
resp = await client.delete(f"/api/v1/sessions/{session_id}")
assert resp.status_code == 200
@@ -142,36 +142,18 @@ async def test_delete_session(client: httpx.AsyncClient):
async def test_compress_session(client: httpx.AsyncClient):
create_resp = await client.post(
- "/api/v1/sessions", json={"user": "test"}
+ "/api/v1/sessions", json={}
)
session_id = create_resp.json()["result"]["session_id"]
- # Add some messages before compressing
+ # Add some messages before committing
await client.post(
f"/api/v1/sessions/{session_id}/messages",
json={"role": "user", "content": "Hello"},
)
resp = await client.post(
- f"/api/v1/sessions/{session_id}/compress"
- )
- assert resp.status_code == 200
- assert resp.json()["status"] == "ok"
-
-
-async def test_extract_session(client: httpx.AsyncClient):
- create_resp = await client.post(
- "/api/v1/sessions", json={"user": "test"}
- )
- session_id = create_resp.json()["result"]["session_id"]
-
- await client.post(
- f"/api/v1/sessions/{session_id}/messages",
- json={"role": "user", "content": "Remember this fact"},
- )
-
- resp = await client.post(
- f"/api/v1/sessions/{session_id}/extract"
+ f"/api/v1/sessions/{session_id}/commit"
)
assert resp.status_code == 200
assert resp.json()["status"] == "ok"
diff --git a/tests/server/test_auth.py b/tests/server/test_auth.py
index 77350f8d..a8b6f030 100644
--- a/tests/server/test_auth.py
+++ b/tests/server/test_auth.py
@@ -11,6 +11,7 @@
from openviking.server.config import ServerConfig
from openviking.server.dependencies import set_service
from openviking.service.core import OpenVikingService
+from openviking.session.user_id import UserIdentifier
TEST_API_KEY = "test-secret-key-12345"
@@ -19,7 +20,7 @@
@pytest_asyncio.fixture(scope="function")
async def auth_service(temp_dir):
"""Service for auth tests."""
- svc = OpenVikingService(path=str(temp_dir / "auth_data"), user="auth_user")
+ svc = OpenVikingService(path=str(temp_dir / "auth_data"), user=UserIdentifier.the_default_user("auth_user"))
await svc.initialize()
yield svc
await svc.close()
diff --git a/tests/server/test_http_client_sdk.py b/tests/server/test_http_client_sdk.py
index 3e5d5021..d918bed9 100644
--- a/tests/server/test_http_client_sdk.py
+++ b/tests/server/test_http_client_sdk.py
@@ -8,6 +8,7 @@
import pytest_asyncio
from openviking.client.http import HTTPClient
+from openviking.session.user_id import UserIdentifier
from tests.server.conftest import SAMPLE_MD_CONTENT, TEST_TMP_DIR
@@ -17,7 +18,7 @@ async def http_client(running_server):
port, svc = running_server
client = HTTPClient(
url=f"http://127.0.0.1:{port}",
- user="sdk_test_user",
+ user=UserIdentifier.the_default_user("sdk_test_user"),
)
await client.initialize()
yield client, svc
@@ -91,7 +92,7 @@ async def test_sdk_session_lifecycle(http_client):
client, _ = http_client
# Create
- session_info = await client.create_session(user="sdk_user")
+ session_info = await client.create_session()
session_id = session_info["session_id"]
assert session_id
diff --git a/tests/test_config_loader.py b/tests/test_config_loader.py
new file mode 100644
index 00000000..83e0646b
--- /dev/null
+++ b/tests/test_config_loader.py
@@ -0,0 +1,103 @@
+# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
+# SPDX-License-Identifier: Apache-2.0
+"""Tests for config_loader utilities."""
+
+import json
+import os
+
+import pytest
+
+from openviking.utils.config.config_loader import (
+ load_json_config,
+ require_config,
+ resolve_config_path,
+)
+
+
+class TestResolveConfigPath:
+ """Tests for resolve_config_path."""
+
+ def test_explicit_path_exists(self, tmp_path):
+ conf = tmp_path / "test.conf"
+ conf.write_text("{}")
+ result = resolve_config_path(str(conf), "UNUSED_ENV", "unused.conf")
+ assert result == conf
+
+ def test_explicit_path_not_exists(self, tmp_path):
+ result = resolve_config_path(
+ str(tmp_path / "nonexistent.conf"), "UNUSED_ENV", "unused.conf"
+ )
+ assert result is None
+
+ def test_env_var_path(self, tmp_path, monkeypatch):
+ conf = tmp_path / "env.conf"
+ conf.write_text("{}")
+ monkeypatch.setenv("TEST_CONFIG_ENV", str(conf))
+ result = resolve_config_path(None, "TEST_CONFIG_ENV", "unused.conf")
+ assert result == conf
+
+ def test_env_var_path_not_exists(self, monkeypatch):
+ monkeypatch.setenv("TEST_CONFIG_ENV", "/nonexistent/path.conf")
+ result = resolve_config_path(None, "TEST_CONFIG_ENV", "unused.conf")
+ assert result is None
+
+ def test_default_path(self, tmp_path, monkeypatch):
+ import openviking.utils.config.config_loader as loader
+
+ conf = tmp_path / "ov.conf"
+ conf.write_text("{}")
+ monkeypatch.setattr(loader, "DEFAULT_CONFIG_DIR", tmp_path)
+ monkeypatch.delenv("TEST_CONFIG_ENV", raising=False)
+ result = resolve_config_path(None, "TEST_CONFIG_ENV", "ov.conf")
+ assert result == conf
+
+ def test_nothing_found(self, monkeypatch):
+ monkeypatch.delenv("TEST_CONFIG_ENV", raising=False)
+ result = resolve_config_path(None, "TEST_CONFIG_ENV", "nonexistent.conf")
+ # May or may not be None depending on whether ~/.openviking/nonexistent.conf exists
+ # but for a random filename it should be None
+ assert result is None
+
+ def test_explicit_takes_priority_over_env(self, tmp_path, monkeypatch):
+ explicit = tmp_path / "explicit.conf"
+ explicit.write_text('{"source": "explicit"}')
+ env_conf = tmp_path / "env.conf"
+ env_conf.write_text('{"source": "env"}')
+ monkeypatch.setenv("TEST_CONFIG_ENV", str(env_conf))
+ result = resolve_config_path(str(explicit), "TEST_CONFIG_ENV", "unused.conf")
+ assert result == explicit
+
+
+class TestLoadJsonConfig:
+ """Tests for load_json_config."""
+
+ def test_valid_json(self, tmp_path):
+ conf = tmp_path / "test.conf"
+ conf.write_text('{"key": "value", "num": 42}')
+ data = load_json_config(conf)
+ assert data == {"key": "value", "num": 42}
+
+ def test_file_not_found(self, tmp_path):
+ with pytest.raises(FileNotFoundError):
+ load_json_config(tmp_path / "nonexistent.conf")
+
+ def test_invalid_json(self, tmp_path):
+ conf = tmp_path / "bad.conf"
+ conf.write_text("not valid json {{{")
+ with pytest.raises(ValueError, match="Invalid JSON"):
+ load_json_config(conf)
+
+
+class TestRequireConfig:
+ """Tests for require_config."""
+
+ def test_loads_existing_config(self, tmp_path):
+ conf = tmp_path / "test.conf"
+ conf.write_text('{"url": "http://localhost:1933"}')
+ data = require_config(str(conf), "UNUSED_ENV", "unused.conf", "test")
+ assert data["url"] == "http://localhost:1933"
+
+ def test_raises_on_missing(self, monkeypatch):
+ monkeypatch.delenv("TEST_MISSING_ENV", raising=False)
+ with pytest.raises(FileNotFoundError, match="configuration file not found"):
+ require_config(None, "TEST_MISSING_ENV", "nonexistent_file.conf", "test")
diff --git a/uv.lock b/uv.lock
index 8c50561b..f3d5188a 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1209,6 +1209,7 @@ dependencies = [
{ name = "readabilipy" },
{ name = "requests" },
{ name = "tabulate" },
+ { name = "typer" },
{ name = "typing-extensions" },
{ name = "urllib3" },
{ name = "uvicorn" },
@@ -1263,6 +1264,7 @@ requires-dist = [
{ name = "sphinx", marker = "extra == 'doc'", specifier = ">=7.0.0" },
{ name = "sphinx-rtd-theme", marker = "extra == 'doc'", specifier = ">=1.3.0" },
{ name = "tabulate", specifier = ">=0.9.0" },
+ { name = "typer", specifier = ">=0.12.0" },
{ name = "typing-extensions", specifier = ">=4.5.0" },
{ name = "urllib3", specifier = ">=2.6.3" },
{ name = "uvicorn", specifier = ">=0.39.0" },
@@ -1956,6 +1958,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/0d/53aea75710af4528a25ed6837d71d117602b01946b307a3912cb3cfcbcba/retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606", size = 7986, upload-time = "2016-05-11T13:58:39.925Z" },
]
+[[package]]
+name = "rich"
+version = "14.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" },
+]
+
[[package]]
name = "roman-numerals"
version = "4.1.0"
@@ -2003,6 +2018,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
]
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+]
+
[[package]]
name = "s3transfer"
version = "0.16.0"
@@ -2278,6 +2302,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
]
+[[package]]
+name = "typer"
+version = "0.21.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "rich" },
+ { name = "shellingham" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
+]
+
[[package]]
name = "typing-extensions"
version = "4.15.0"