Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5d85536
fix: make rust CLI (ov) commands match python CLI (openviking) exactl…
Feb 16, 2026
0e08354
fix: ov cli plays same as py cli (ls, tree)
Feb 16, 2026
7376d61
Refactor media parsers to subdirectory structure with validation
Feb 16, 2026
2f3ec8c
Enhance CLI robustness: validate add-resource path exists and detect …
Feb 16, 2026
c4b5960
Fix unescaped spaces in paths by replacing \ with space
Feb 16, 2026
da4386a
Sanitize URI components to replace spaces and special chars with unde…
Feb 16, 2026
b2fdfc0
feat: auto organize audio and image and video files
Feb 16, 2026
26ba487
Merge branch 'volcengine:main' into main
MaojiaSheng Feb 16, 2026
1118919
Update media parsers to use original filenames and folder names with …
Feb 16, 2026
900e8f6
Merge branch 'volcengine:main' into main
MaojiaSheng Feb 17, 2026
b1fc591
Optimize MediaParser section for readability
Feb 16, 2026
3e3483b
feat: vlm optimization for image
Feb 17, 2026
b264e8e
Merge branch 'main' into main
MaojiaSheng Feb 19, 2026
5b818f5
feat: vlm optimization for image
Feb 19, 2026
0974f3b
feat: vlm optimization for image
Feb 19, 2026
25d52b9
refactor: move media content understanding to SemanticProcessor
Feb 20, 2026
4590c02
refactor: split _generate_single_file_summary to add _generate_text_s…
Feb 20, 2026
dd3e64e
feat: vlm optimization for image
Feb 20, 2026
d167ff1
feat: vlm optimization for image
Feb 20, 2026
b20a0a8
Merge branch 'volcengine:main' into main
MaojiaSheng Feb 21, 2026
9c561a4
Implement smart dual-mode for add-resource and import-ovpack, and con…
Feb 21, 2026
b100a2b
feat: support local upload
Feb 22, 2026
08c9ef7
Merge branch 'main' into main
qin-ctx Feb 23, 2026
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
2 changes: 2 additions & 0 deletions crates/ov_cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ curl -fsSL https://raw.githubusercontent.com/volcengine/OpenViking/main/crates/o
### From Source

```bash
# openviking need rust >= 1.88, please upgrade it if necessary
# brew upgrade rust
cargo install --path crates/ov_cli
```

Expand Down
3 changes: 1 addition & 2 deletions examples/ov.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
"cors_origins": ["*"]
},
"storage": {
"workspace": "./data",
"vectordb": {
"name": "context",
"backend": "local",
"path": "./data",
"project": "default",
"volcengine": {
"region": "cn-beijing",
Expand All @@ -20,7 +20,6 @@
"agfs": {
"port": 1833,
"log_level": "warn",
"path": "./data",
"backend": "local",
"timeout": 10,
"retry_times": 3,
Expand Down
5 changes: 2 additions & 3 deletions examples/server_client/ov.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@
"cors_origins": ["*"]
},
"storage": {
"workspace": "./data",
"vectordb": {
"name": "context",
"backend": "local",
"path": "./data"
"backend": "local"
},
"agfs": {
"port": 1833,
"log_level": "warn",
"path": "./data",
"backend": "local"
}
},
Expand Down
5 changes: 4 additions & 1 deletion openviking/parse/directory_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def _classify_file(
def scan_directory(
root: Union[str, Path],
registry: Optional[ParserRegistry] = None,
strict: bool = True,
strict: bool = False,
ignore_dirs: Optional[Set[str]] = None,
include: Optional[str] = None,
exclude: Optional[str] = None,
Expand Down Expand Up @@ -272,7 +272,10 @@ def scan_directory(
f"Unsupported: {unsupported_paths[:10]}{'...' if len(unsupported_paths) > 10 else ''}"
)
if strict:
logger.error(msg)
raise UnsupportedDirectoryFilesError(msg, unsupported_paths)
else:
logger.warning(msg)
result.warnings.append(msg)
for rel in unsupported_paths:
result.warnings.append(f" - {rel}")
Expand Down
12 changes: 10 additions & 2 deletions openviking/server/routers/pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# SPDX-License-Identifier: Apache-2.0
"""Pack endpoints for OpenViking HTTP Server."""

from typing import Optional

from fastapi import APIRouter, Depends
from pydantic import BaseModel

Expand All @@ -22,7 +24,8 @@ class ExportRequest(BaseModel):
class ImportRequest(BaseModel):
"""Request model for import."""

file_path: str
file_path: Optional[str] = None
temp_path: Optional[str] = None
parent: str
force: bool = False
vectorize: bool = True
Expand All @@ -46,8 +49,13 @@ async def import_ovpack(
):
"""Import .ovpack file."""
service = get_service()

file_path = request.file_path
if request.temp_path:
file_path = request.temp_path

result = await service.pack.import_ovpack(
request.file_path,
file_path,
request.parent,
force=request.force,
vectorize=request.vectorize,
Expand Down
54 changes: 51 additions & 3 deletions openviking/server/routers/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,27 @@
# SPDX-License-Identifier: Apache-2.0
"""Resource endpoints for OpenViking HTTP Server."""

import time
import uuid
from pathlib import Path
from typing import Any, Optional

from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, File, UploadFile
from pydantic import BaseModel

from openviking.server.auth import verify_api_key
from openviking.server.dependencies import get_service
from openviking.server.models import Response
from openviking_cli.utils.config.open_viking_config import get_openviking_config

router = APIRouter(prefix="/api/v1", tags=["resources"])


class AddResourceRequest(BaseModel):
"""Request model for add_resource."""

path: str
path: Optional[str] = None
temp_path: Optional[str] = None
target: Optional[str] = None
reason: str = ""
instruction: str = ""
Expand All @@ -33,15 +38,58 @@ class AddSkillRequest(BaseModel):
timeout: Optional[float] = None


def _cleanup_temp_files(temp_dir: Path, max_age_hours: int = 1):
"""Clean up temporary files older than max_age_hours."""
if not temp_dir.exists():
return

now = time.time()
max_age_seconds = max_age_hours * 3600

for file_path in temp_dir.iterdir():
if file_path.is_file():
file_age = now - file_path.stat().st_mtime
if file_age > max_age_seconds:
file_path.unlink(missing_ok=True)


@router.post("/resources/temp_upload")
async def temp_upload(
file: UploadFile = File(...),
_: bool = Depends(verify_api_key),
):
"""Upload a temporary file for add_resource or import_ovpack."""
config = get_openviking_config()
temp_dir = config.storage.get_upload_temp_dir()

# Clean up old temporary files
_cleanup_temp_files(temp_dir)

# Save the uploaded file
file_ext = Path(file.filename).suffix if file.filename else ".tmp"
temp_filename = f"upload_{uuid.uuid4().hex}{file_ext}"
temp_file_path = temp_dir / temp_filename

with open(temp_file_path, "wb") as f:
f.write(await file.read())

return Response(status="ok", result={"temp_path": str(temp_file_path)})


@router.post("/resources")
async def add_resource(
request: AddResourceRequest,
_: bool = Depends(verify_api_key),
):
"""Add resource to OpenViking."""
service = get_service()

path = request.path
if request.temp_path:
path = request.temp_path

result = await service.resources.add_resource(
path=request.path,
path=path,
target=request.target,
reason=request.reason,
instruction=request.instruction,
Expand Down
11 changes: 11 additions & 0 deletions openviking/utils/media_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# SPDX-License-Identifier: Apache-2.0
"""Unified resource processor with strategy-based routing."""

import tempfile
import zipfile
from pathlib import Path
from typing import TYPE_CHECKING, Optional

Expand Down Expand Up @@ -103,6 +105,15 @@ async def _process_file(
instruction: str,
) -> ParseResult:
"""Process file with unified parsing."""
# Check if it's a zip file
if zipfile.is_zipfile(file_path):
temp_dir = Path(tempfile.mkdtemp())
try:
with zipfile.ZipFile(file_path, "r") as zipf:
zipf.extractall(temp_dir)
return await self._process_directory(temp_dir, instruction)
finally:
pass # Don't delete temp_dir yet, it will be used by TreeBuilder
return await parse(
str(file_path),
instruction=instruction,
Expand Down
88 changes: 74 additions & 14 deletions openviking_cli/client/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
Implements BaseClient interface using HTTP calls to OpenViking Server.
"""

import tempfile
import uuid
import zipfile
from pathlib import Path
from typing import Any, Dict, List, Optional, Union

import httpx
Expand Down Expand Up @@ -219,6 +223,42 @@ def _raise_exception(self, error: Dict[str, Any]) -> None:
else:
raise exc_class(message)

def _is_local_server(self) -> bool:
"""Check if the server URL is localhost or 127.0.0.1."""
from urllib.parse import urlparse

parsed_url = urlparse(self._url)
hostname = parsed_url.hostname
return hostname in ("localhost", "127.0.0.1")

def _zip_directory(self, dir_path: str) -> str:
"""Create a temporary zip file from a directory."""
dir_path = Path(dir_path)
if not dir_path.is_dir():
raise ValueError(f"Path {dir_path} is not a directory")

temp_dir = tempfile.gettempdir()
zip_path = Path(temp_dir) / f"temp_upload_{uuid.uuid4().hex}.zip"

with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
for file_path in dir_path.rglob("*"):
if file_path.is_file():
arcname = file_path.relative_to(dir_path)
zipf.write(file_path, arcname=arcname)

return str(zip_path)

async def _upload_temp_file(self, file_path: str) -> str:
"""Upload a file to /api/v1/resources/temp_upload and return the temp_path."""
with open(file_path, "rb") as f:
files = {"file": (Path(file_path).name, f, "application/octet-stream")}
response = await self._http.post(
"/api/v1/resources/temp_upload",
files=files,
)
result = self._handle_response(response)
return result.get("temp_path", "")

# ============= Resource Management =============

async def add_resource(
Expand All @@ -231,16 +271,28 @@ async def add_resource(
timeout: Optional[float] = None,
) -> Dict[str, Any]:
"""Add resource to OpenViking."""
request_data = {
"target": target,
"reason": reason,
"instruction": instruction,
"wait": wait,
"timeout": timeout,
}

path_obj = Path(path)
if path_obj.exists() and path_obj.is_dir() and not self._is_local_server():
zip_path = self._zip_directory(path)
try:
temp_path = await self._upload_temp_file(zip_path)
request_data["temp_path"] = temp_path
finally:
Path(zip_path).unlink(missing_ok=True)
else:
request_data["path"] = path

response = await self._http.post(
"/api/v1/resources",
json={
"path": path,
"target": target,
"reason": reason,
"instruction": instruction,
"wait": wait,
"timeout": timeout,
},
json=request_data,
)
return self._handle_response(response)

Expand Down Expand Up @@ -554,14 +606,22 @@ async def import_ovpack(
) -> str:
"""Import .ovpack file."""
parent = VikingURI.normalize(parent)
request_data = {
"parent": parent,
"force": force,
"vectorize": vectorize,
}

file_path_obj = Path(file_path)
if file_path_obj.exists() and file_path_obj.is_file() and not self._is_local_server():
temp_path = await self._upload_temp_file(file_path)
request_data["temp_path"] = temp_path
else:
request_data["file_path"] = file_path

response = await self._http.post(
"/api/v1/pack/import",
json={
"file_path": file_path,
"parent": parent,
"force": force,
"vectorize": vectorize,
},
json=request_data,
)
result = self._handle_response(response)
return result.get("uri", "")
Expand Down
8 changes: 5 additions & 3 deletions openviking_cli/utils/config/agfs_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ def validate_config(self):
class AGFSConfig(BaseModel):
"""Configuration for AGFS (Agent Global File System)."""

path: str = Field(default="./data", description="AGFS data storage path")
path: Optional[str] = Field(
default=None,
description="[Deprecated in favor of `storage.workspace`] AGFS data storage path. This will be ignored if `storage.workspace` is set.",
)

port: int = Field(default=1833, description="AGFS service port")

Expand Down Expand Up @@ -110,8 +113,7 @@ def validate_config(self):
)

if self.backend == "local":
if not self.path:
raise ValueError("AGFS local backend requires 'path' to be set")
pass

elif self.backend == "s3":
# Validate S3 configuration
Expand Down
7 changes: 3 additions & 4 deletions openviking_cli/utils/config/open_viking_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def from_dict(cls, config: Dict[str, Any]) -> "OpenVikingConfig":

# Remove sections managed by other loaders (e.g. server config)
config_copy.pop("server", None)

# Handle parser configurations from nested "parsers" section
parser_configs = {}
if "parsers" in config_copy:
Expand Down Expand Up @@ -316,7 +316,7 @@ def initialize_openviking_config(

Args:
user: UserIdentifier for session management
path: Local storage path for embedded mode
path: Local storage path (workspace) for embedded mode

Returns:
Configured OpenVikingConfig instance
Expand All @@ -337,9 +337,8 @@ def initialize_openviking_config(
if path:
# Embedded mode: local storage
config.storage.agfs.backend = config.storage.agfs.backend or "local"
config.storage.agfs.path = path
config.storage.vectordb.backend = config.storage.vectordb.backend or "local"
config.storage.vectordb.path = path
config.storage.workspace = path

# Ensure vector dimension is synced if not set in storage
if config.storage.vectordb.dimension == 0:
Expand Down
Loading
Loading