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"