Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 28 additions & 12 deletions crates/ov_cli/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ impl HttpClient {
Ok(temp_file)
}

/// Upload a temporary file and return the temp_path
/// Upload a temporary file and return the temp_file_id
async fn upload_temp_file(&self, file_path: &Path) -> Result<String> {
let url = format!("{}/api/v1/resources/temp_upload", self.base_url);
let file_name = file_path
Expand Down Expand Up @@ -110,10 +110,10 @@ impl HttpClient {

let result: Value = self.handle_response(response).await?;
result
.get("temp_path")
.get("temp_file_id")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| Error::Parse("Missing temp_path in response".to_string()))
.ok_or_else(|| Error::Parse("Missing temp_file_id in response".to_string()))
}

fn build_headers(&self) -> reqwest::header::HeaderMap {
Expand Down Expand Up @@ -536,10 +536,10 @@ impl HttpClient {
if path_obj.exists() {
if path_obj.is_dir() {
let zip_file = self.zip_directory(path_obj)?;
let temp_path = self.upload_temp_file(zip_file.path()).await?;
let temp_file_id = self.upload_temp_file(zip_file.path()).await?;

let body = serde_json::json!({
"temp_path": temp_path,
"temp_file_id": temp_file_id,
"to": to,
"parent": parent,
"reason": reason,
Expand All @@ -556,10 +556,10 @@ impl HttpClient {

self.post("/api/v1/resources", &body).await
} else if path_obj.is_file() {
let temp_path = self.upload_temp_file(path_obj).await?;
let temp_file_id = self.upload_temp_file(path_obj).await?;

let body = serde_json::json!({
"temp_path": temp_path,
"temp_file_id": temp_file_id,
"to": to,
"parent": parent,
"reason": reason,
Expand Down Expand Up @@ -626,19 +626,19 @@ impl HttpClient {
if path_obj.exists() {
if path_obj.is_dir() {
let zip_file = self.zip_directory(path_obj)?;
let temp_path = self.upload_temp_file(zip_file.path()).await?;
let temp_file_id = self.upload_temp_file(zip_file.path()).await?;

let body = serde_json::json!({
"temp_path": temp_path,
"temp_file_id": temp_file_id,
"wait": wait,
"timeout": timeout,
});
self.post("/api/v1/skills", &body).await
} else if path_obj.is_file() {
let temp_path = self.upload_temp_file(path_obj).await?;
let temp_file_id = self.upload_temp_file(path_obj).await?;

let body = serde_json::json!({
"temp_path": temp_path,
"temp_file_id": temp_file_id,
"wait": wait,
"timeout": timeout,
});
Expand Down Expand Up @@ -707,8 +707,24 @@ impl HttpClient {
force: bool,
vectorize: bool,
) -> Result<serde_json::Value> {
let file_path_obj = Path::new(file_path);

if !file_path_obj.exists() {
return Err(Error::Client(format!(
"Local ovpack file not found: {}",
file_path
)));
}
if !file_path_obj.is_file() {
return Err(Error::Client(format!(
"Path is not a file: {}",
file_path
)));
}

let temp_file_id = self.upload_temp_file(file_path_obj).await?;
let body = serde_json::json!({
"file_path": file_path,
"temp_file_id": temp_file_id,
"parent": parent,
"force": force,
"vectorize": vectorize,
Expand Down
7 changes: 7 additions & 0 deletions docs/en/api/01-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ export OPENVIKING_CLI_CONFIG_FILE=/path/to/ovcli.conf

See the [Configuration Guide](../guides/01-configuration.md#ovcliconf) for details.

**Local files in HTTP mode**

- CLI, `SyncHTTPClient`, and `AsyncHTTPClient` automatically upload local files and directories before calling the server API.
- Raw HTTP callers do not get this convenience layer. When using `curl` or another HTTP client, upload the file with `POST /api/v1/resources/temp_upload` first, then call the target API with the returned `temp_file_id`.
- For local directories in raw HTTP mode, zip the directory first and upload the `.zip` file; the server does not accept direct host directory paths.
- `POST /api/v1/resources` accepts remote URLs directly, but does not accept direct host filesystem paths such as `./doc.md` or `/tmp/doc.md`.

### Direct HTTP (curl)

```bash
Expand Down
102 changes: 91 additions & 11 deletions docs/en/api/02-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,24 @@ Add a resource to the knowledge base.

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| path | str | Yes | - | Local file path, directory path, or URL |
| path | str | Yes | - | SDK/CLI: local path, directory path, or URL. Raw HTTP: remote URL only |
| temp_file_id | str | No | None | Upload ID returned by `POST /api/v1/resources/temp_upload` for raw HTTP local file ingestion |
| target | str | No | None | Target Viking URI (must be in `resources` scope) |
| reason | str | No | "" | Why this resource is being added (improves search relevance) |
| instruction | str | No | "" | Special processing instructions |
| wait | bool | No | False | Wait for semantic processing to complete |
| timeout | float | No | None | Timeout in seconds (only used when wait=True) |
| watch_interval | float | No | 0 | Watch interval (minutes). >0 enables/updates watch; <=0 disables watch. Only takes effect when target is provided |

**How local files and directories work**

- Python SDK and CLI accept local file and directory paths directly. In HTTP mode they automatically upload local files before calling the server API.
- Raw HTTP callers should think in two categories:
- Remote source: pass `path` directly, for example `https://example.com/doc.pdf`
- Local file: call `POST /api/v1/resources/temp_upload` first, then pass the returned `temp_file_id`
- Local directory: zip it first, upload the `.zip` file, then pass the returned `temp_file_id`
- `POST /api/v1/resources` does not accept direct host filesystem paths such as `./guide.md`, `/tmp/guide.md`, or `/tmp/my-dir/`.

**Incremental Updates**

When you call `add_resource()` repeatedly for the same resource URI, the system performs an incremental update instead of rebuilding everything from scratch:
Expand Down Expand Up @@ -83,7 +93,7 @@ curl -X POST http://localhost:1933/api/v1/resources \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{
"path": "./documents/guide.md",
"path": "https://example.com/guide.md",
"reason": "User guide documentation"
}'
```
Expand Down Expand Up @@ -142,6 +152,58 @@ curl -X POST http://localhost:1933/api/v1/resources \
openviking add-resource https://example.com/api-docs.md --to viking://resources/external/ --reason "External API documentation"
```

**Example: Add a Local File with Raw HTTP**

When you call the HTTP API directly, upload local files first and then use `temp_file_id`.

```bash
# Step 1: upload the local file
TEMP_FILE_ID=$(
curl -sS -X POST http://localhost:1933/api/v1/resources/temp_upload \
-H "X-API-Key: your-key" \
-F 'file=@./documents/guide.md' \
| jq -r '.result.temp_file_id'
)

# Step 2: add the uploaded file
curl -X POST http://localhost:1933/api/v1/resources \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d "{
\"temp_file_id\": \"$TEMP_FILE_ID\",
\"reason\": \"User guide documentation\",
\"wait\": true
}"
```

**Example: Add a Local Directory with Raw HTTP**

When you call the HTTP API directly, zip the directory yourself first. CLI and SDK do this automatically for you.

```bash
# Step 1: zip the local directory
cd ./documents
zip -r /tmp/guide.zip ./guide

# Step 2: upload the zip file
TEMP_FILE_ID=$(
curl -sS -X POST http://localhost:1933/api/v1/resources/temp_upload \
-H "X-API-Key: your-key" \
-F 'file=@/tmp/guide.zip' \
| jq -r '.result.temp_file_id'
)

# Step 3: add the uploaded directory archive
curl -X POST http://localhost:1933/api/v1/resources \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d "{
\"temp_file_id\": \"$TEMP_FILE_ID\",
\"reason\": \"Import local directory\",
\"wait\": true
}"
```

**Example: Add Feishu/Lark Cloud Documents**

[Feishu](https://www.feishu.cn) (飞书) and its international version [Lark](https://www.larksuite.com) are widely used for documentation in Chinese tech companies. OpenViking can directly import cloud documents by URL.
Expand Down Expand Up @@ -218,7 +280,7 @@ print(f"All processed: {status}")
curl -X POST http://localhost:1933/api/v1/resources \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{"path": "./documents/guide.md", "wait": true}'
-d '{"path": "https://example.com/guide.md", "wait": true}'

# Wait separately after batch
curl -X POST http://localhost:1933/api/v1/system/wait \
Expand Down Expand Up @@ -260,7 +322,7 @@ curl -X POST http://localhost:1933/api/v1/resources \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{
"path": "./documents/guide.md",
"path": "https://example.com/guide.md",
"target": "viking://resources/documents/guide.md",
"watch_interval": 60
}'
Expand Down Expand Up @@ -338,7 +400,7 @@ openviking export viking://resources/my-project/ ./exports/my-project.ovpack

Import a `.ovpack` file.

**Parameters**
**SDK / CLI parameters**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
Expand All @@ -347,6 +409,15 @@ Import a `.ovpack` file.
| force | bool | No | False | Overwrite existing resources |
| vectorize | bool | No | True | Trigger vectorization after import |

**Raw HTTP request body**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| temp_file_id | str | Yes | - | Upload ID returned by `POST /api/v1/resources/temp_upload` |
| parent | str | Yes | - | Target parent URI |
| force | bool | No | False | Overwrite existing resources |
| vectorize | bool | No | True | Trigger vectorization after import |

**Python SDK (Embedded / HTTP)**

```python
Expand All @@ -368,15 +439,24 @@ POST /api/v1/pack/import
```

```bash
# Step 1: upload the local ovpack file
TEMP_FILE_ID=$(
curl -sS -X POST http://localhost:1933/api/v1/resources/temp_upload \
-H "X-API-Key: your-key" \
-F 'file=@./exports/my-project.ovpack' \
| jq -r '.result.temp_file_id'
)

# Step 2: import using temp_file_id
curl -X POST http://localhost:1933/api/v1/pack/import \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{
"file_path": "./exports/my-project.ovpack",
"parent": "viking://resources/imported/",
"force": true,
"vectorize": true
}'
-d "{
\"temp_file_id\": \"$TEMP_FILE_ID\",
\"parent\": \"viking://resources/imported/\",
\"force\": true,
\"vectorize\": true
}"
```

**CLI**
Expand Down
30 changes: 26 additions & 4 deletions docs/en/api/04-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,21 @@ Add a skill to the knowledge base.

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| data | Any | Yes | - | Skill data (dict, string, or path) |
| data | Any | Yes | - | Skill data. Raw HTTP accepts structured data or raw `SKILL.md` content, not direct host paths |
| temp_file_id | str | No | None | Upload ID returned by `POST /api/v1/resources/temp_upload` for raw HTTP local file ingestion |
| wait | bool | No | False | Wait for vectorization to complete |
| timeout | float | No | None | Timeout in seconds |

**How local skill files work**

- Python SDK and CLI accept local `SKILL.md` files or directories directly. In HTTP mode they automatically upload local files before calling the server API.
- Raw HTTP callers should either:
- send structured skill data directly in `data`
- send raw `SKILL.md` content in `data`
- upload a local `SKILL.md` file first with `POST /api/v1/resources/temp_upload`, then call `POST /api/v1/skills` with `temp_file_id`
- zip a local skill directory first, upload the `.zip` file, then call `POST /api/v1/skills` with `temp_file_id`
- `POST /api/v1/skills` does not accept direct host filesystem paths in `data`.

**Supported Data Formats**

1. **Dict (Skill format)**:
Expand Down Expand Up @@ -185,14 +196,25 @@ print(f"Auxiliary files: {result['auxiliary_files']}")
**HTTP API**

```bash
# Step 1: upload the local SKILL.md file
TEMP_FILE_ID=$(
curl -sS -X POST http://localhost:1933/api/v1/resources/temp_upload \
-H "X-API-Key: your-key" \
-F 'file=@./skills/search-web/SKILL.md' \
| jq -r '.result.temp_file_id'
)

# Step 2: add the uploaded skill
curl -X POST http://localhost:1933/api/v1/skills \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{
"data": "./skills/search-web/SKILL.md"
}'
-d "{
\"temp_file_id\": \"$TEMP_FILE_ID\"
}"
```

For a local skill directory, zip the directory first, upload the `.zip` file, then call the same `POST /api/v1/skills` request with the returned `temp_file_id`.

---

## SKILL.md Format
Expand Down
2 changes: 2 additions & 0 deletions docs/en/getting-started/03-quickstart-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ export OPENVIKING_CLI_CONFIG_FILE=/path/to/ovcli.conf

## Connect with curl

Use direct `path` for remote URLs. For local files, upload first with `POST /api/v1/resources/temp_upload`, then call the target API with the returned `temp_file_id`. For local directories in raw HTTP mode, zip the directory first and upload the `.zip` file.

```bash
# Add a resource
curl -X POST http://localhost:1933/api/v1/resources \
Expand Down
7 changes: 7 additions & 0 deletions docs/zh/api/01-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ export OPENVIKING_CLI_CONFIG_FILE=/path/to/ovcli.conf

详见 [配置指南](../guides/01-configuration.md#ovcliconf)。

**HTTP 模式下的本地文件**

- CLI、`SyncHTTPClient`、`AsyncHTTPClient` 遇到本地文件或目录时,会先自动上传,再调用服务端 API。
- 裸 HTTP 调用没有这层封装。使用 `curl` 或其他 HTTP 客户端时,需要先调用 `POST /api/v1/resources/temp_upload`,再把返回的 `temp_file_id` 传给目标 API。
- 裸 HTTP 如果导入本地目录,需要先自行打成 `.zip` 再上传;服务端不接受直接传宿主机目录路径。
- `POST /api/v1/resources` 可以直接接收远端 URL,但不接受 `./doc.md`、`/tmp/doc.md` 这类宿主机本地路径。

### 直接 HTTP(curl)

```bash
Expand Down
Loading
Loading