diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b08c71ada8..63b2c62fed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,17 +83,8 @@ non-default path. ### 4. Verify Installation -```python -import asyncio -import openviking as ov - -async def main(): - client = ov.AsyncOpenViking(path="./test_data") - await client.initialize() - print("OpenViking initialized successfully!") - await client.close() - -asyncio.run(main()) +```bash +python -c "import openviking; print(openviking.__version__)" ``` ### 5. Build Rust CLI (Optional) @@ -121,7 +112,7 @@ openviking/ ├── pyproject.toml # Python project and tooling configuration ├── Cargo.toml # Rust workspace configuration ├── openviking/ # Python SDK and server implementation -│ ├── client/ # Local and HTTP client implementations +│ ├── client/ # HTTP client compatibility exports │ ├── connector/ # Data connectors │ ├── core/ # Core data models and directory abstractions │ ├── ingest/ # Ingestion pipeline @@ -206,10 +197,10 @@ pytest tests/server/ -v pytest tests/parse/ -v # Run specific test file -pytest tests/client/test_lifecycle.py +pytest tests/client/test_http_client_config.py # Run specific test -pytest tests/client/test_lifecycle.py::TestClientInitialization::test_initialize_success +pytest tests/client/test_http_client_config.py # Run by keyword pytest -k "search" -v @@ -223,26 +214,19 @@ pytest --cov=openviking --cov-report=term-missing Tests are organized in subdirectories under `tests/`. The project uses `asyncio_mode = "auto"`, so async tests do **not** need the `@pytest.mark.asyncio` decorator: ```python -# tests/client/test_example.py -from openviking import AsyncOpenViking - - -class TestAsyncOpenViking: - async def test_initialize(self, uninitialized_client: AsyncOpenViking): - await uninitialized_client.initialize() - assert uninitialized_client._service is not None - await uninitialized_client.close() - - async def test_add_resource(self, client: AsyncOpenViking, sample_markdown_file): - result = await client.add_resource( +# tests/service/test_example.py +class TestResourceService: + async def test_add_resource(self, service, request_context, sample_markdown_file): + result = await service.resources.add_resource( path=str(sample_markdown_file), - reason="test document" + ctx=request_context, + reason="test document", ) assert "root_uri" in result assert result["root_uri"].startswith("viking://") ``` -Common fixtures are defined in `tests/conftest.py`, including `client` (initialized `AsyncOpenViking`), `uninitialized_client`, `temp_dir`, `sample_markdown_file`, and more. +Common fixtures are defined in `tests/conftest.py`, including the initialized `service`, `request_context`, `temp_dir`, and sample files. --- diff --git a/CONTRIBUTING_CN.md b/CONTRIBUTING_CN.md index 35251fbdaf..528fd30d00 100644 --- a/CONTRIBUTING_CN.md +++ b/CONTRIBUTING_CN.md @@ -98,17 +98,8 @@ export OPENVIKING_CONFIG_FILE=~/.openviking/ov.conf ### 4. 验证安装 -```python -import asyncio -import openviking as ov - -async def main(): - client = ov.AsyncOpenViking(path="./test_data") - await client.initialize() - print("OpenViking initialized successfully!") - await client.close() - -asyncio.run(main()) +```bash +python -c "import openviking; print(openviking.__version__)" ``` ### 5. 构建 Rust CLI(可选) @@ -138,10 +129,8 @@ openviking/ ├── third_party/ # 第三方依赖 │ └── agfs/ # AGFS 文件系统 │ -├── openviking/ # Python SDK -│ ├── async_client.py # AsyncOpenViking 客户端 -│ ├── sync_client.py # SyncOpenViking 客户端 -│ ├── client/ # 本地与 HTTP 客户端实现 +├── openviking/ # Python 服务端与核心实现 +│ ├── client/ # HTTP 客户端兼容导出 │ ├── console/ # 独立 console UI 与代理服务 │ ├── core/ # 核心数据模型与目录抽象 │ ├── message/ # 会话消息与 part 模型 @@ -233,10 +222,10 @@ pytest tests/server/ -v pytest tests/parse/ -v # 运行特定测试文件 -pytest tests/client/test_lifecycle.py +pytest tests/client/test_http_client_config.py # 运行特定测试 -pytest tests/client/test_lifecycle.py::TestClientInitialization::test_initialize_success +pytest tests/client/test_http_client_config.py # 按关键字运行 pytest -k "search" -v @@ -250,26 +239,19 @@ pytest --cov=openviking --cov-report=term-missing 测试按模块组织在 `tests/` 的子目录中。项目使用 `asyncio_mode = "auto"`,异步测试**不需要** `@pytest.mark.asyncio` 装饰器: ```python -# tests/client/test_example.py -from openviking import AsyncOpenViking - - -class TestAsyncOpenViking: - async def test_initialize(self, uninitialized_client: AsyncOpenViking): - await uninitialized_client.initialize() - assert uninitialized_client._service is not None - await uninitialized_client.close() - - async def test_add_resource(self, client: AsyncOpenViking, sample_markdown_file): - result = await client.add_resource( +# tests/service/test_example.py +class TestResourceService: + async def test_add_resource(self, service, request_context, sample_markdown_file): + result = await service.resources.add_resource( path=str(sample_markdown_file), - reason="test document" + ctx=request_context, + reason="test document", ) assert "root_uri" in result assert result["root_uri"].startswith("viking://") ``` -常用 fixture 定义在 `tests/conftest.py` 中,包括 `client`(已初始化的 `AsyncOpenViking`)、`uninitialized_client`、`temp_dir`、`sample_markdown_file` 等。 +常用 fixture 定义在 `tests/conftest.py` 中,包括已初始化的 `service`、`request_context`、`temp_dir` 和示例文件等。 --- diff --git a/CONTRIBUTING_JA.md b/CONTRIBUTING_JA.md index dcd526074e..062d67d8bc 100644 --- a/CONTRIBUTING_JA.md +++ b/CONTRIBUTING_JA.md @@ -98,17 +98,8 @@ export OPENVIKING_CONFIG_FILE=~/.openviking/ov.conf ### 4. インストールの確認 -```python -import asyncio -import openviking as ov - -async def main(): - client = ov.AsyncOpenViking(path="./test_data") - await client.initialize() - print("OpenViking initialized successfully!") - await client.close() - -asyncio.run(main()) +```bash +python -c "import openviking; print(openviking.__version__)" ``` ### 5. Rust CLIのビルド(オプション) @@ -140,10 +131,8 @@ openviking/ ├── third_party/ # サードパーティ依存関係 │ └── agfs/ # AGFSファイルシステム │ -├── openviking/ # Python SDK -│ ├── async_client.py # AsyncOpenVikingクライアント -│ ├── sync_client.py # SyncOpenVikingクライアント -│ ├── client/ # ローカル / HTTP クライアント実装 +├── openviking/ # Pythonサーバーとコア実装 +│ ├── client/ # HTTPクライアント互換エクスポート │ ├── console/ # スタンドアロン console UI とプロキシサービス │ ├── core/ # コアデータモデルとディレクトリ抽象 │ ├── message/ # セッションメッセージと part モデル @@ -235,10 +224,10 @@ pytest tests/server/ -v pytest tests/parse/ -v # 特定のテストファイルの実行 -pytest tests/client/test_lifecycle.py +pytest tests/client/test_http_client_config.py # 特定のテストの実行 -pytest tests/client/test_lifecycle.py::TestClientInitialization::test_initialize_success +pytest tests/client/test_http_client_config.py # キーワードで実行 pytest -k "search" -v @@ -252,26 +241,19 @@ pytest --cov=openviking --cov-report=term-missing テストは`tests/`配下のサブディレクトリに整理されています。プロジェクトは`asyncio_mode = "auto"`を使用しているため、非同期テストに`@pytest.mark.asyncio`デコレーターは**不要**です: ```python -# tests/client/test_example.py -from openviking import AsyncOpenViking - - -class TestAsyncOpenViking: - async def test_initialize(self, uninitialized_client: AsyncOpenViking): - await uninitialized_client.initialize() - assert uninitialized_client._service is not None - await uninitialized_client.close() - - async def test_add_resource(self, client: AsyncOpenViking, sample_markdown_file): - result = await client.add_resource( +# tests/service/test_example.py +class TestResourceService: + async def test_add_resource(self, service, request_context, sample_markdown_file): + result = await service.resources.add_resource( path=str(sample_markdown_file), - reason="test document" + ctx=request_context, + reason="test document", ) assert "root_uri" in result assert result["root_uri"].startswith("viking://") ``` -共通フィクスチャは`tests/conftest.py`に定義されており、`client`(初期化済み`AsyncOpenViking`)、`uninitialized_client`、`temp_dir`、`sample_markdown_file` などが含まれます。 +共通フィクスチャは`tests/conftest.py`に定義されており、初期化済みの`service`、`request_context`、`temp_dir`、サンプルファイルなどが含まれます。 --- diff --git a/benchmark/RAG/README.md b/benchmark/RAG/README.md index a0fde97d49..ad2239966a 100644 --- a/benchmark/RAG/README.md +++ b/benchmark/RAG/README.md @@ -277,7 +277,6 @@ RAG uses YAML configuration files to control the evaluation process. Each datase 4. **Path Configuration**: - `dataset_dir`: Path to dataset file or directory - `doc_output_dir`: Directory for processed documents - - `vector_store`: Directory for vector index storage - `output_dir`: Directory for evaluation results - `log_file`: Path to log file 5. **LLM Configuration**: @@ -330,12 +329,8 @@ Output/ └── benchmark.log # Log file ``` -**Vector Store Database Location:** -The vector index (document database) is stored in the path specified by `vector_store` in the configuration file. By default, this is: - -``` -datasets/{dataset_name}/viking_store_index_dir -``` +**OpenViking Storage:** +The benchmark uses the OpenViking Server configured for the Python HTTP SDK. Storage and vector-index locations are owned by that Server rather than by the benchmark process. #### File descriptions and examples @@ -638,8 +633,8 @@ FinanceBench has 3 question types: This project integrates with OpenViking through: -- Using `openviking` client for data ingestion and retrieval -- Configuring OpenViking connection via `ov.conf` +- Using the OpenViking Python HTTP SDK for data ingestion and retrieval +- Configuring the OpenViking connection via `ovcli.conf` or SDK environment variables - Supporting dynamic loading of OpenViking's latest features ### Frequently Asked Questions (FAQ) diff --git a/benchmark/RAG/README_zh.md b/benchmark/RAG/README_zh.md index b324145f0d..ef6093ac80 100644 --- a/benchmark/RAG/README_zh.md +++ b/benchmark/RAG/README_zh.md @@ -277,7 +277,6 @@ RAG 使用 YAML 配置文件来控制评估过程。每个数据集在 `config/` 4. **路径配置**: - `dataset_dir`:数据集文件或目录的路径 - `doc_output_dir`:处理文档的目录 - - `vector_store`:向量索引存储的目录 - `output_dir`:评估结果的目录 - `log_file`:日志文件的路径 5. **LLM 配置**: @@ -330,12 +329,8 @@ Output/ └── benchmark.log # 日志文件 ``` -**向量存储数据库位置:** -向量索引(文档数据库)存储在配置文件中 `vector_store` 指定的路径中。默认情况下,这是: - -``` -datasets/{dataset_name}/viking_store_index_dir -``` +**OpenViking 存储:** +Benchmark 使用 Python HTTP SDK 所配置的 OpenViking Server。内容和向量索引的存储位置由 Server 管理,而不是由 Benchmark 进程管理。 #### 文件描述和示例 @@ -638,8 +633,8 @@ FinanceBench 有 3 种问题类型: 本项目通过以下方式与 OpenViking 集成: -- 使用 `openviking` 客户端进行数据摄取和检索 -- 通过 `ov.conf` 配置 OpenViking 连接 +- 使用 OpenViking Python HTTP SDK 进行数据摄取和检索 +- 通过 `ovcli.conf` 或 SDK 环境变量配置 OpenViking 连接 - 支持动态加载 OpenViking 的最新功能 ### 常见问题(FAQ) diff --git a/benchmark/RAG/config/config.yaml b/benchmark/RAG/config/config.yaml index dfa51affdb..e6509601ee 100644 --- a/benchmark/RAG/config/config.yaml +++ b/benchmark/RAG/config/config.yaml @@ -40,8 +40,6 @@ paths: dataset_path: "datasets/{dataset_name}" # Output directory for processed documents doc_output_dir: "datasets/{dataset_name}/{dataset_name}_processed_docs" - # Vector index storage directory - vector_store: "datasets/{dataset_name}/viking_store_index_dir" # Results output directory output_dir: "Output/{dataset_name}/experiment_dev" # Log file path diff --git a/benchmark/RAG/config/financebench_config.yaml b/benchmark/RAG/config/financebench_config.yaml index e059002776..f6524d6c3b 100644 --- a/benchmark/RAG/config/financebench_config.yaml +++ b/benchmark/RAG/config/financebench_config.yaml @@ -20,7 +20,6 @@ execution: paths: dataset_path: "datasets/{dataset_name}/financebench_open_source.jsonl" doc_output_dir: "ov_storage/{dataset_name}/{dataset_name}_processed_docs" - vector_store: "ov_storage/{dataset_name}/{dataset_name}_viking_store_index" output_dir: "Output/{dataset_name}/experiment_test_top_{retrieval_topk}" log_file: "Output/{dataset_name}/experiment_test_top_{retrieval_topk}/benchmark.log" diff --git a/benchmark/RAG/config/locomo_config.yaml b/benchmark/RAG/config/locomo_config.yaml index ac5c986209..a594b94ec0 100644 --- a/benchmark/RAG/config/locomo_config.yaml +++ b/benchmark/RAG/config/locomo_config.yaml @@ -20,7 +20,6 @@ execution: paths: dataset_path: "datasets/{dataset_name}/locomo10.json" doc_output_dir: "ov_storage/{dataset_name}/{dataset_name}_processed_docs" - vector_store: "ov_storage/{dataset_name}/{dataset_name}_viking_store_index" output_dir: "Output/{dataset_name}/experiment_test_top_{retrieval_topk}" log_file: "Output/{dataset_name}/experiment_test_top_{retrieval_topk}/benchmark.log" diff --git a/benchmark/RAG/config/qasper_config.yaml b/benchmark/RAG/config/qasper_config.yaml index 0ba0c3b77c..b07120aeaa 100644 --- a/benchmark/RAG/config/qasper_config.yaml +++ b/benchmark/RAG/config/qasper_config.yaml @@ -20,7 +20,6 @@ execution: paths: dataset_path: "datasets/{dataset_name}" doc_output_dir: "ov_storage/{dataset_name}/{dataset_name}_processed_docs" - vector_store: "ov_storage/{dataset_name}/{dataset_name}_viking_store_index" output_dir: "Output/{dataset_name}/experiment_test_top_{retrieval_topk}" log_file: "Output/{dataset_name}/experiment_test_top_{retrieval_topk}/benchmark.log" diff --git a/benchmark/RAG/config/syllabusqa_config.yaml b/benchmark/RAG/config/syllabusqa_config.yaml index 07244a19bb..2a285ebb42 100644 --- a/benchmark/RAG/config/syllabusqa_config.yaml +++ b/benchmark/RAG/config/syllabusqa_config.yaml @@ -20,7 +20,6 @@ execution: paths: dataset_path: "datasets/{dataset_name}" doc_output_dir: "ov_storage/{dataset_name}/{dataset_name}_processed_docs" - vector_store: "ov_storage/{dataset_name}/{dataset_name}_viking_store_index" output_dir: "Output/{dataset_name}/experiment_test_top_{retrieval_topk}" log_file: "Output/{dataset_name}/experiment_test_top_{retrieval_topk}/benchmark.log" diff --git a/benchmark/RAG/run.py b/benchmark/RAG/run.py index 0d2d0a57b4..74fd2a189a 100644 --- a/benchmark/RAG/run.py +++ b/benchmark/RAG/run.py @@ -88,7 +88,7 @@ def main(): 'retrieval_topk': retrieval_topk } - path_keys = ['dataset_path', 'output_dir', 'vector_store', 'log_file', 'doc_output_dir'] + path_keys = ['dataset_path', 'output_dir', 'log_file', 'doc_output_dir'] for key in path_keys: if key in config.get('paths', {}): original = config['paths'][key] @@ -122,7 +122,7 @@ def main(): raise e # 2. Vector Store - vector_store = VikingStoreWrapper(store_path=config['paths']['vector_store']) + vector_store = VikingStoreWrapper() # 3. LLM Client api_key = os.environ.get( diff --git a/benchmark/RAG/src/core/vector_store.py b/benchmark/RAG/src/core/vector_store.py index feb06fe210..44a30a3c55 100644 --- a/benchmark/RAG/src/core/vector_store.py +++ b/benchmark/RAG/src/core/vector_store.py @@ -1,24 +1,21 @@ import os -import time -from typing import List import sys +import time from pathlib import Path +from typing import List sys.path.append(str(Path(__file__).parent.parent)) -from adapters.base import StandardDoc, StandardSample import tiktoken -import openviking as ov +from adapters.base import StandardDoc +from openviking_sdk import SyncHTTPClient class VikingStoreWrapper: - def __init__(self, store_path: str): - self.store_path = store_path - if not os.path.exists(store_path): - os.makedirs(store_path) - - self.client = ov.SyncOpenViking(path=store_path) - + def __init__(self): + self.client = SyncHTTPClient() + self.client.initialize() + try: self.enc = tiktoken.get_encoding("cl100k_base") except Exception as e: diff --git a/benchmark/RAG/src/pipeline.py b/benchmark/RAG/src/pipeline.py index 54bd67937f..f4306a8e7b 100644 --- a/benchmark/RAG/src/pipeline.py +++ b/benchmark/RAG/src/pipeline.py @@ -48,7 +48,7 @@ def run_generation(self): doc_dir = os.path.join(self.output_dir, "docs") if skip_ingestion: - self.logger.info(f"Skipping ingestion. Reusing existing vector index at: {self.db.store_path}") + self.logger.info("Skipping ingestion. Reusing the configured OpenViking Server") self.metrics_summary["insertion"] = {"time": 0, "input_tokens": 0, "output_tokens": 0, "embedding_tokens": 0} else: try: @@ -238,9 +238,14 @@ def _process_generation_task(self, task): retrieved_uris = [] context_blocks = [] - for r in search_res.resources: - retrieved_uris.append(r.uri) - content = self.db.read_resource(r.uri) if getattr(r, 'level', 2) == 2 else f"{getattr(r, 'abstract', '')}\n{getattr(r, 'overview', '')}" + for result in search_res.get("resources", []): + uri = result["uri"] + retrieved_uris.append(uri) + content = ( + self.db.read_resource(uri) + if result.get("level", 2) == 2 + else f"{result.get('abstract', '')}\n{result.get('overview', '')}" + ) retrieved_texts.append(content) clean = content[:8000] context_blocks.append(clean) diff --git a/benchmark/retrieval/grep/vikingdb_bm25/effectiveness/step1_add_resource.py b/benchmark/retrieval/grep/vikingdb_bm25/effectiveness/step1_add_resource.py index e8e18c08b4..4669cd377a 100644 --- a/benchmark/retrieval/grep/vikingdb_bm25/effectiveness/step1_add_resource.py +++ b/benchmark/retrieval/grep/vikingdb_bm25/effectiveness/step1_add_resource.py @@ -1,33 +1,22 @@ #!/usr/bin/env python3 -"""Step 1 (Effectiveness): Import real code repos into OpenViking (with indexing). - -Imports the entire source directory as a single resource via -SyncOpenViking.add_resource (wait=True, build_index=True, summarize=True). -add_resource handles recursive traversal internally. - -After import, run step2_quality.py to evaluate retrieval quality. - -Prerequisites: - - Download code repos and place them under the source directory manually. - -Usage: - python3 step1_add_resource.py - python3 step1_add_resource.py --source ~/.openviking/data/benchmark/OpenViking-main -""" +"""Import real code repos through the shared Service layer with indexing enabled.""" from __future__ import annotations import argparse +import asyncio import os import time -from openviking.sync_client import SyncOpenViking +from openviking.server.identity import RequestContext, Role +from openviking.service.core import OpenVikingService +from openviking_cli.session.user_id import UserIdentifier DEFAULT_SOURCE = os.path.expanduser("~/.openviking/data/benchmark/OpenViking-main") BENCHMARK_PARENT = "viking://resources/benchmark/effectiveness" -def main(): +async def main(): parser = argparse.ArgumentParser( description="Step 1 (Effectiveness): Import real code repos (with indexing)" ) @@ -56,13 +45,16 @@ def main(): print(" Indexing: ENABLED (build_index=True, summarize=True)") print() - client = SyncOpenViking() - client.initialize() + user = UserIdentifier.the_default_user() + service = OpenVikingService(user=user) + ctx = RequestContext(user=user, role=Role.USER) + await service.initialize() t0 = time.monotonic() try: - result = client.add_resource( + result = await service.resources.add_resource( path=source, + ctx=ctx, parent=args.parent, reason="benchmark effectiveness", wait=True, @@ -76,12 +68,12 @@ def main(): print() print("Import completed successfully.") print("Next step: run step2_quality.py to evaluate retrieval quality") - except Exception as e: + except Exception as exc: elapsed = time.monotonic() - t0 - print(f"FAILED ({elapsed:.1f}s): {e}") - - client.close() + print(f"FAILED ({elapsed:.1f}s): {exc}") + finally: + await service.close() if __name__ == "__main__": - main() + asyncio.run(main()) diff --git a/benchmark/retrieval/grep/vikingdb_bm25/performance/step1_add_resource.py b/benchmark/retrieval/grep/vikingdb_bm25/performance/step1_add_resource.py index 3d5a56a08b..8e56198e81 100644 --- a/benchmark/retrieval/grep/vikingdb_bm25/performance/step1_add_resource.py +++ b/benchmark/retrieval/grep/vikingdb_bm25/performance/step1_add_resource.py @@ -1,25 +1,16 @@ #!/usr/bin/env python3 -"""Step 1 (Performance): Import synthetic data into OpenViking WITHOUT indexing. - -Imports each directory recursively via SyncOpenViking.add_resource with -build_index=False and summarize=False, to skip slow VLM/embedding steps. -Progress is saved after each directory for resumability. - -After all imports are done, run step2_reindex.py to build vector indexes, -then step3_benchmark.py to measure performance. - -Usage: - python3 step1_add_resource.py - python3 step1_add_resource.py --source ~/.openviking/data/benchmark/synthetic -""" +"""Import synthetic data through the shared Service layer without indexing.""" from __future__ import annotations import argparse +import asyncio import os import time -from openviking.sync_client import SyncOpenViking +from openviking.server.identity import RequestContext, Role +from openviking.service.core import OpenVikingService +from openviking_cli.session.user_id import UserIdentifier DEFAULT_SOURCE = os.path.expanduser("~/.openviking/data/benchmark/synthetic") PROGRESS_FILE = os.path.expanduser("~/.openviking/data/benchmark/.perf-import-progress") @@ -29,18 +20,17 @@ def load_progress() -> set[str]: if not os.path.exists(PROGRESS_FILE): return set() - with open(PROGRESS_FILE) as f: - return {line.strip() for line in f if line.strip()} + with open(PROGRESS_FILE) as file: + return {line.strip() for line in file if line.strip()} def save_progress(rel_dir: str) -> None: os.makedirs(os.path.dirname(PROGRESS_FILE), exist_ok=True) - with open(PROGRESS_FILE, "a") as f: - f.write(rel_dir + "\n") + with open(PROGRESS_FILE, "a") as file: + file.write(rel_dir + "\n") def scan_subdirs_recursive(root: str) -> list[str]: - """Return sorted list of all subdirectory relative paths (deterministic order).""" result: list[str] = [] def _walk(dir_path: str, rel_prefix: str) -> None: @@ -62,7 +52,7 @@ def _walk(dir_path: str, rel_prefix: str) -> None: return result -def main(): +async def main(): parser = argparse.ArgumentParser( description="Step 1 (Performance): Import synthetic data (no indexing)" ) @@ -96,73 +86,76 @@ def main(): total = len(subdirs) print(f" Total directories to import: {total}") print() - if total == 0: print("No subdirectories found. Nothing to import.") return completed = load_progress() if completed: - already_done = [d for d in subdirs if d in completed] + already_done = [directory for directory in subdirs if directory in completed] print(f" Resuming: {len(already_done)} directories already imported") print() - client = SyncOpenViking() - client.initialize() + user = UserIdentifier.the_default_user() + service = OpenVikingService(user=user) + ctx = RequestContext(user=user, role=Role.USER) + await service.initialize() results = [] - for i, rel_dir in enumerate(subdirs, 1): - if rel_dir in completed: - print(f" [{i}/{total}] SKIP (already done): {rel_dir}") - continue - - dir_path = os.path.join(source, rel_dir) - parent_rel = os.path.dirname(rel_dir) - parent_uri = f"{args.parent}/{parent_rel}" if parent_rel else args.parent - print(f" [{i}/{total}] Importing: {rel_dir} ...", end="", flush=True) + try: + for index, rel_dir in enumerate(subdirs, 1): + if rel_dir in completed: + print(f" [{index}/{total}] SKIP (already done): {rel_dir}") + continue - t0 = time.monotonic() - try: - result = client.add_resource( - path=dir_path, - parent=parent_uri, - reason=f"benchmark perf: {rel_dir}", - wait=True, - create_parent=True, - build_index=False, - summarize=False, - ) - elapsed = time.monotonic() - t0 - root_uri = result.get("root_uri", "?") - print(f" OK ({elapsed:.1f}s) -> {root_uri}") - save_progress(rel_dir) - results.append({"dir": rel_dir, "status": "ok", "elapsed_s": round(elapsed, 1)}) - except Exception as e: - elapsed = time.monotonic() - t0 - print(f" FAILED ({elapsed:.1f}s): {e}") - results.append( - { - "dir": rel_dir, - "status": "failed", - "elapsed_s": round(elapsed, 1), - "error": str(e)[:500], - } - ) - - client.close() + dir_path = os.path.join(source, rel_dir) + parent_rel = os.path.dirname(rel_dir) + parent_uri = f"{args.parent}/{parent_rel}" if parent_rel else args.parent + print(f" [{index}/{total}] Importing: {rel_dir} ...", end="", flush=True) + + t0 = time.monotonic() + try: + result = await service.resources.add_resource( + path=dir_path, + ctx=ctx, + parent=parent_uri, + reason=f"benchmark perf: {rel_dir}", + wait=True, + create_parent=True, + build_index=False, + summarize=False, + ) + elapsed = time.monotonic() - t0 + root_uri = result.get("root_uri", "?") + print(f" OK ({elapsed:.1f}s) -> {root_uri}") + save_progress(rel_dir) + results.append({"dir": rel_dir, "status": "ok", "elapsed_s": round(elapsed, 1)}) + except Exception as exc: + elapsed = time.monotonic() - t0 + print(f" FAILED ({elapsed:.1f}s): {exc}") + results.append( + { + "dir": rel_dir, + "status": "failed", + "elapsed_s": round(elapsed, 1), + "error": str(exc)[:500], + } + ) + finally: + await service.close() print() print("Summary:") - ok_count = sum(1 for r in results if r["status"] == "ok") - failed_count = sum(1 for r in results if r["status"] == "failed") - skipped_count = sum(1 for d in subdirs if d in completed) + ok_count = sum(1 for result in results if result["status"] == "ok") + failed_count = sum(1 for result in results if result["status"] == "failed") + skipped_count = sum(1 for directory in subdirs if directory in completed) total_done = skipped_count + ok_count - for r in results: - status = r["status"] - line = f" {status.upper():>7s} {r['dir']} ({r['elapsed_s']}s)" + for result in results: + status = result["status"] + line = f" {status.upper():>7s} {result['dir']} ({result['elapsed_s']}s)" if status == "failed": - line += f" -- {r.get('error', '')}" + line += f" -- {result.get('error', '')}" print(line) print() @@ -179,4 +172,4 @@ def main(): if __name__ == "__main__": - main() + asyncio.run(main()) diff --git a/bot/docs/zh/design/vikingbot-openviking-context-plan.md b/bot/docs/zh/design/vikingbot-openviking-context-plan.md index 282b21acfb..6af08e778c 100644 --- a/bot/docs/zh/design/vikingbot-openviking-context-plan.md +++ b/bot/docs/zh/design/vikingbot-openviking-context-plan.md @@ -347,22 +347,15 @@ OpenViking 的 `get_session_context()` 返回的不是纯摘要,而是: - `commit_token_threshold`:触发 commit 的阈值 - `commit_keep_recent_count`:commit 后保留的 recent live messages 数量 -## OpenViking Python Client 需要补的能力 +## OpenViking Python HTTP SDK 需要补的能力 -虽然 OpenViking 服务端已支持 `keep_recent_count`,但当前 Python client wrapper 还没有把这个参数完整透出给 VikingBot。 - -需要修改: - -- `openviking/async_client.py` -- `openviking/client/session.py` - -建议补齐以下调用能力: +OpenViking 服务端和 Python HTTP SDK 需要完整透出以下调用能力: - `commit_session(session_id, keep_recent_count=0, telemetry=False)` - `Session.commit(keep_recent_count=0, telemetry=False)` - `Session.commit_async(keep_recent_count=0, telemetry=False)` -否则 VikingBot 在 `AgentsConfig` 配置了 `commit_keep_recent_count` 也无法真正生效。 +对应实现位于 `sdk/python/openviking_sdk/client.py`,VikingBot 通过 HTTP 服务调用这些接口。 ## 与旧链路的共存策略 diff --git a/bot/tests/conftest.py b/bot/tests/conftest.py index 03b2d7435c..44102903ef 100644 --- a/bot/tests/conftest.py +++ b/bot/tests/conftest.py @@ -6,12 +6,9 @@ import asyncio import shutil from pathlib import Path -from typing import AsyncGenerator, Generator +from typing import Generator import pytest -import pytest_asyncio - -from openviking import AsyncOpenViking # Test data root directory PROJECT_ROOT = Path(__file__).parent.parent @@ -138,59 +135,3 @@ def sample_files(temp_dir: Path) -> list[Path]: ) files.append(file_path) return files - - -# ============ Client Fixtures ============ - - -@pytest_asyncio.fixture(scope="function") -async def client(test_data_dir: Path) -> AsyncGenerator[AsyncOpenViking, None]: - """Create initialized OpenViking client""" - await AsyncOpenViking.reset() - - client = AsyncOpenViking(path=str(test_data_dir)) - await client.initialize() - - yield client - - await client.close() - await AsyncOpenViking.reset() - - -@pytest_asyncio.fixture(scope="function") -async def uninitialized_client(test_data_dir: Path) -> AsyncGenerator[AsyncOpenViking, None]: - """Create uninitialized OpenViking client (for testing initialization flow)""" - await AsyncOpenViking.reset() - - client = AsyncOpenViking(path=str(test_data_dir)) - - yield client - - try: - await client.close() - except Exception: - pass - await AsyncOpenViking.reset() - - -@pytest_asyncio.fixture(scope="function") -async def client_with_resource_sync( - client: AsyncOpenViking, sample_markdown_file: Path -) -> AsyncGenerator[tuple[AsyncOpenViking, str], None]: - """Create client with resource (sync mode, wait for vectorization)""" - result = await client.add_resource( - path=str(sample_markdown_file), reason="Test resource", wait=True - ) - uri = result.get("root_uri", "") - - yield client, uri - - -@pytest_asyncio.fixture(scope="function") -async def client_with_resource( - client: AsyncOpenViking, sample_markdown_file: Path -) -> AsyncGenerator[tuple[AsyncOpenViking, str], None]: - """Create client with resource (async mode, no wait for vectorization)""" - result = await client.add_resource(path=str(sample_markdown_file), reason="Test resource") - uri = result.get("root_uri", "") - yield client, uri diff --git a/bot/tests/example.py b/bot/tests/example.py deleted file mode 100644 index 4817bfe8f5..0000000000 --- a/bot/tests/example.py +++ /dev/null @@ -1,47 +0,0 @@ -import openviking as ov - -# Initialize OpenViking client with data directory -client = ov.SyncOpenViking(path="./data") - -try: - # Initialize the client - client.initialize() - - # Add resource (supports URL, file, or directory) - add_result = client.add_resource( - path="/Users/bytedance/Downloads/exp/experience_data_mini.json", - resource_type="json", # 明确指定类型 - tags=["large_data", "agent_context", "structured"], - ) - root_uri = add_result["root_uri"] - - # Explore the resource tree structure - ls_result = client.ls(root_uri) - print(f"Directory structure:\n{ls_result}\n") - - # Use glob to find markdown files - glob_result = client.glob(pattern="**/*.md", uri=root_uri) - if glob_result["matches"]: - content = client.read(glob_result["matches"][0]) - print(f"Content preview: {content[:200]}...\n") - - # Wait for semantic processing to complete - print("Wait for semantic processing...") - client.wait_processed() - - # Get abstract and overview of the resource - abstract = client.abstract(root_uri) - overview = client.overview(root_uri) - print(f"Abstract:\n{abstract}\n\nOverview:\n{overview}\n") - - # Perform semantic search - results = client.find("what is openviking", target_uri=root_uri) - print("Search results:") - for r in results.resources: - print(f" {r.uri} (score: {r.score:.4f})") - - # Close the client - client.close() - -except Exception as e: - print(f"Error: {e}") diff --git a/bot/vikingbot/openviking_mount/README.md b/bot/vikingbot/openviking_mount/README.md index 25213dbc83..2ce2a304a3 100644 --- a/bot/vikingbot/openviking_mount/README.md +++ b/bot/vikingbot/openviking_mount/README.md @@ -24,7 +24,7 @@ from pathlib import Path # 创建挂载配置 config = MountConfig( mount_point=Path("./my_openviking_mount"), - openviking_data_path=Path("./my_openviking_data"), + openviking_data_path=Path("./my_openviking_cache"), scope=MountScope.RESOURCES, auto_init=True, read_only=False @@ -65,13 +65,13 @@ manager = get_mount_manager() # 创建资源挂载 mount = manager.create_resources_mount( mount_id="my_resources", - openviking_data_path=Path("./ov_data") + openviking_data_path=Path("./ov_cache") ) # 为会话创建挂载 session_mount = manager.create_session_mount( session_id="session_123", - openviking_data_path=Path("./ov_data") + openviking_data_path=Path("./ov_cache") ) # 列出所有挂载 @@ -112,7 +112,7 @@ vikingbot/openviking_mount/ | 字段 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `mount_point` | `Path` | 必填 | 挂载点路径 | -| `openviking_data_path` | `Path` | 必填 | OpenViking 数据存储路径 | +| `openviking_data_path` | `Path` | 必填 | FUSE 本地缓存路径 | | `session_id` | `Optional[str]` | `None` | 会话 ID(session 作用域时需要) | | `scope` | `MountScope` | `RESOURCES` | 挂载作用域 | | `auto_init` | `bool` | `True` | 是否自动初始化 | diff --git a/bot/vikingbot/openviking_mount/mount.py b/bot/vikingbot/openviking_mount/mount.py index 653050b91b..411ad22aea 100644 --- a/bot/vikingbot/openviking_mount/mount.py +++ b/bot/vikingbot/openviking_mount/mount.py @@ -31,7 +31,7 @@ class MountConfig: """挂载配置""" mount_point: Path # 挂载点路径 - openviking_data_path: Path # OpenViking数据存储路径 + openviking_data_path: Path # FUSE 本地缓存路径 session_id: Optional[str] = None # 会话ID(如果是session作用域) scope: MountScope = MountScope.RESOURCES # 挂载作用域 auto_init: bool = True # 是否自动初始化 @@ -67,7 +67,7 @@ def __init__(self, config: MountConfig): config: 挂载配置 """ self.config = config - self._client: Optional[ov.SyncOpenViking] = None + self._client: Optional[ov.SyncHTTPClient] = None self._initialized = False self._mount_point_created = False @@ -89,10 +89,9 @@ def initialize(self) -> None: if ov is None: raise ImportError("openviking module is not available") - logger.info(f"Initializing OpenViking at: {self.config.openviking_data_path}") + logger.info("Connecting to the configured OpenViking Server") - # 初始化OpenViking客户端 - self._client = ov.SyncOpenViking(path=str(self.config.openviking_data_path)) + self._client = ov.SyncHTTPClient() self._client.initialize() self._initialized = True @@ -107,7 +106,7 @@ def _ensure_client(self) -> None: raise RuntimeError("OpenViking client not initialized. Call initialize() first.") @property - def client(self) -> Optional[ov.SyncOpenViking]: + def client(self) -> Optional[ov.SyncHTTPClient]: """获取底层OpenViking客户端""" return self._client @@ -321,14 +320,16 @@ def search(self, query: str, target_path: Optional[Union[str, Path]] = None) -> results = self._client.find(query, target_uri=target_uri) file_infos = [] - for r in results.resources: + for r in results.get("resources", []): + uri = r.get("uri", "") if isinstance(r, dict) else r.uri file_info = FileInfo( - uri=r.uri, - name=Path(r.uri).name, + uri=uri, + name=Path(uri).name, is_dir=False, # 需要根据实际结果判断 ) - if hasattr(r, "score"): - file_info.score = r.score + score = r.get("score") if isinstance(r, dict) else getattr(r, "score", None) + if score is not None: + file_info.score = score file_infos.append(file_info) return file_infos @@ -365,7 +366,7 @@ def add_resource( logger.debug(f"Adding resource: {source_path} -> {target_uri} (wait={wait})") try: - result = self._client.add_resource(path=str(source_path), target=target_uri, wait=wait) + result = self._client.add_resource(path=str(source_path), to=target_uri, wait=wait) return result.get("root_uri", "") except Exception as e: logger.error(f"Failed to add resource: {e}") diff --git a/bot/vikingbot/sandbox/manager.py b/bot/vikingbot/sandbox/manager.py index 20774b7c7e..d4fe9ab66f 100644 --- a/bot/vikingbot/sandbox/manager.py +++ b/bot/vikingbot/sandbox/manager.py @@ -2,7 +2,8 @@ from pathlib import Path -from openviking.async_client import logger +from loguru import logger + from vikingbot.config.schema import Config, SessionKey from vikingbot.sandbox.backends import get_backend from vikingbot.sandbox.base import SandboxBackend, UnsupportedBackendError diff --git a/docs/design/agent-evolution-global-switch-design.md b/docs/design/agent-evolution-global-switch-design.md index c1a8746d21..ff746face8 100644 --- a/docs/design/agent-evolution-global-switch-design.md +++ b/docs/design/agent-evolution-global-switch-design.md @@ -36,9 +36,9 @@ The default is `false`. defaults such as add targets. Agent Evolution is no longer part of active `UserConfig` resolution. -This setting belongs to the HTTP server deployment surface. Embedded/local SDK -clients do not load `ServerConfig`, so they preserve the historical enabled -behavior instead of becoming permanently unable to produce Agent memory. +This setting belongs to the HTTP server deployment surface. A directly +constructed `SessionService` retains its enabled default for internal service +callers that do not load `ServerConfig`. ## Commit Behavior @@ -76,7 +76,6 @@ Remove the user-level management surfaces introduced by the current branch: - `GET /api/v1/user-settings/memory` - `PATCH /api/v1/user-settings/memory` - Python SDK memory-setting methods -- Embedded client memory-setting methods - `ov user-settings memory` - `ov user-settings set-memory` - Agent Evolution fields accepted during account or user creation @@ -87,8 +86,8 @@ override the deployment-level setting. ## Compatibility - Existing experiences remain readable and searchable. -- Embedded/local SDK clients preserve their historical enabled behavior because - they do not have the HTTP server configuration surface. +- Directly constructed services preserve their enabled default because they do + not have the HTTP server configuration surface. - Existing user config files containing `agent_evolution` continue to parse, preventing an upgrade from breaking users that already wrote the branch-era configuration. diff --git a/docs/design/agent-evolution-global-switch-implementation-plan.md b/docs/design/agent-evolution-global-switch-implementation-plan.md index 0f7481cbff..f79ebb3242 100644 --- a/docs/design/agent-evolution-global-switch-implementation-plan.md +++ b/docs/design/agent-evolution-global-switch-implementation-plan.md @@ -4,7 +4,7 @@ **Goal:** Replace the per-user Agent Evolution setting with one deployment-level switch shared by every account and user in an OpenViking server instance. -**Architecture:** `ServerConfig.agent_evolution.enabled` is the only active Agent Evolution setting for HTTP server deployments. `SessionService` snapshots it into each `Session`; commit Phase 1 stores the effective value in archive metadata and Phase 2 consumes that snapshot. Embedded/local SDK clients preserve the historical enabled behavior because they do not load `ServerConfig`. The former user field remains parse-only for compatibility, while user-facing management APIs, clients, and CLI commands are removed. +**Architecture:** `ServerConfig.agent_evolution.enabled` is the only active Agent Evolution setting for HTTP server deployments. `SessionService` snapshots it into each `Session`; commit Phase 1 stores the effective value in archive metadata and Phase 2 consumes that snapshot. Directly constructed services retain the enabled default because they do not load `ServerConfig`. The former user field remains parse-only for compatibility, while user-facing management APIs, clients, and CLI commands are removed. **Tech Stack:** Python 3.10+, Pydantic v2, FastAPI, pytest, Rust/clap CLI. @@ -168,7 +168,7 @@ Expected: failures because sessions still resolve per-user settings. In `SessionService`, replace `_user_config_defaults` with: ```python -# Embedded/local compatibility default. HTTP app setup overrides it from +# Direct-service default. HTTP app setup overrides it from # ServerConfig, whose default is false. self._agent_evolution_enabled = True @@ -234,17 +234,12 @@ git commit -m "feat(agent-evolution): apply global commit switch" **Files:** - Modify: `openviking/server/routers/admin.py` - Modify: `openviking/server/routers/user_settings.py` -- Modify: `openviking/async_client.py` -- Modify: `openviking/sync_client.py` -- Modify: `openviking/client/local.py` -- Modify: `openviking_cli/client/base.py` - Modify: `sdk/python/openviking_sdk/client.py` - Modify: `crates/ov_cli/src/commands/mod.rs` - Delete: `crates/ov_cli/src/commands/user_settings.rs` - Modify: `crates/ov_cli/src/main.rs` - Modify: `crates/ov_cli/src/help_ui.rs` - Delete: `tests/client/test_user_memory_settings.py` -- Modify: `tests/client/test_base_client_compatibility.py` - Modify: `sdk/python/tests/test_async_client_behaviors.py` - Modify: `tests/server/test_admin_api.py` @@ -267,7 +262,6 @@ Run: ```bash uv run pytest -q --no-cov --tb=short \ tests/server/test_agent_evolution_global_setting.py \ - tests/client/test_base_client_compatibility.py \ sdk/python/tests/test_async_client_behaviors.py ``` @@ -276,8 +270,8 @@ Expected: the endpoint and client methods still exist. - [ ] **Step 3: Remove the Python and HTTP surfaces** Delete the memory request model and `/user-settings/memory` routes. Remove -`get_memory_settings()` and `patch_memory_settings()` from embedded, async, -sync, CLI-base, and SDK clients. Remove Agent Evolution handling from account +`get_memory_settings()` and `patch_memory_settings()` from HTTP and SDK +clients. Remove Agent Evolution handling from account and user creation; deprecated user input remains accepted by `UserConfig` but is ignored. diff --git a/docs/design/memory-link-design.md b/docs/design/memory-link-design.md index 7b957db488..3350b55146 100644 --- a/docs/design/memory-link-design.md +++ b/docs/design/memory-link-design.md @@ -1892,7 +1892,7 @@ PPR 传播(多种子叠加,按 3.2.7.4 配置表): | `session/compressor_v2.py` | `_create_relations()` 使用新 `link()` 签名 | | `session/session.py` | `_run_memory_extraction()` 使用新 `link()` 签名 | | `server/routers/relations.py` | `LinkRequest` 扩展 `direction`/`link_type`/`weight` 等字段 | -| `openviking_cli/client/base.py` | `link()` 方法签名扩展 | +| `sdk/python/openviking_sdk/client.py` | `link()` 方法签名扩展 | | YAML templates | 新增 `report` + `report_candidate` memory_type 定义 + `dream_tasks` 配置;现有模板默认 link_enabled=true | | 新增模块 | `memory/dream_context_provider.py` — 整理上下文提供者;`utils/links_merge.py` — links/backlinks 合并逻辑;`retrieve/ppr.py` — PPR 算法 | diff --git a/docs/en/about/03-roadmap.md b/docs/en/about/03-roadmap.md index 167b8130e4..09733bd52d 100644 --- a/docs/en/about/03-roadmap.md +++ b/docs/en/about/03-roadmap.md @@ -64,7 +64,7 @@ This document outlines the development roadmap for OpenViking. - HTTP Server (FastAPI) - Native MCP endpoint built into openviking-server - Python HTTP Client -- Client abstraction layer (LocalClient / HTTPClient) +- Python HTTP client SDK - Web Console ### CLI diff --git a/docs/en/agent-integrations/07-langchain-langgraph.md b/docs/en/agent-integrations/07-langchain-langgraph.md index 7d071cc424..20f7b0ae75 100644 --- a/docs/en/agent-integrations/07-langchain-langgraph.md +++ b/docs/en/agent-integrations/07-langchain-langgraph.md @@ -2,8 +2,7 @@ Wire OpenViking into your LangChain or LangGraph agent as the context backend. The standalone integration package provides a retriever, chat history, context wrapper, -agent tools, LangGraph store, and middleware for HTTP-backed or embedded OpenViking -deployments. +agent tools, LangGraph store, and middleware for OpenViking HTTP deployments. ## Install @@ -28,7 +27,7 @@ tools = create_openviking_tools( ) ``` -When both `url` and `path` are omitted, adapters use the HTTP connection settings from the OpenViking CLI config. Pass `path` to use an embedded workspace through OpenViking's synchronous client; embedded mode also requires the full `openviking` package. Embedding and VLM providers are configured in OpenViking, not in your app. +When `url` is omitted, adapters use the HTTP connection settings from the OpenViking CLI config. Embedding and VLM providers are configured in OpenViking, not in your app. ### Async applications @@ -44,13 +43,12 @@ result = await chain.ainvoke( ) ``` -Async adapters support three client modes: +Async adapters support two client modes: | Configuration | Async interface | Ownership | |---------------|-----------------|-----------| | `client=` or `async_client=` | The injected client is returned unchanged | Caller | -| `url=`, or neither `url` nor `path` | One recovery-capable HTTP handle per event loop | Adapter | -| `path=` | A synchronous embedded client invoked in a worker thread | Adapter | +| `url=`, or omitted | One recovery-capable HTTP handle per event loop | Adapter | Long-lived applications can initialize one caller-owned async client and reuse it across adapters running on the same event loop: @@ -73,14 +71,6 @@ not share one injected async client across event loops; create and manage one client per loop instead. An injected synchronous client remains safe to use from async adapter methods because its calls run in a worker thread. -For embedded `path=` adapters, the synchronous fallback is intentional: -`SyncOpenViking` keeps the stateful embedded engine on OpenViking's shared -background loop while the application event loop remains non-blocking. To use -native embedded async methods, construct and initialize `AsyncOpenViking` -yourself, inject it with `async_client=`, use it from that same event loop, and -close it yourself. Only one embedded workspace can be live per process; close -or reset it before selecting another workspace. - `OpenVikingChatMessageHistory` provides `aget_messages()`, `aadd_messages()`, and `aclear()`. `OpenVikingSessionRecorder` provides `arecord()`, `aflush()`, and `aclose()`. Async LangGraph runs select `awrap_model_call()` and @@ -200,8 +190,7 @@ remain bound to the API key or OAuth credential, so multi-user applications must select a credential-bound client before invoking the middleware. Resolve the actor peer only from authenticated, server-owned runtime fields; do not trust model state or client-controlled configurable values. Runtime actor-peer -resolution is available only for HTTP-backed middleware, not embedded `path=` -clients. An injected custom client must set +resolution is available only for HTTP-backed middleware. An injected custom client must set `supports_request_actor_peer = True` and honor the `openviking_sdk` actor-peer scope. Upgrade `openviking-sdk` together with `openviking` before enabling this feature in an existing environment. diff --git a/docs/en/api/01-overview.md b/docs/en/api/01-overview.md index a876481ca0..62a7f2dcda 100644 --- a/docs/en/api/01-overview.md +++ b/docs/en/api/01-overview.md @@ -4,69 +4,23 @@ This page covers how to connect to OpenViking and the conventions shared across ## Connection Modes -OpenViking supports two usage modes: **Embedded Mode** (direct Python API calls) and **Client-Server Mode** (via HTTP API). - -This API documentation primarily focuses on the HTTP API usage in **Client-Server Mode**. Embedded mode is available but will not be covered separately in subsequent documentation. +OpenViking clients connect to an OpenViking Server over HTTP. | 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 (Brief Overview) - -Embedded mode allows direct OpenViking API calls within a Python process without starting a separate server process. - -```python -import openviking as ov - -client = ov.OpenViking(path="./data") -client.initialize() -``` - -Embedded mode uses `ov.conf` to configure embedding, vlm, storage, and other modules. Default configuration path: `~/.openviking/ov.conf`. You can also specify the path via environment variable: - -```bash -export OPENVIKING_CONFIG_FILE=/path/to/ov.conf -``` - -Minimal configuration example: - -```json -{ - "embedding": { - "dense": { - "api_base": "", - "api_key": "", - "provider": "", - "dimension": 1024, - "model": "" - } - }, - "vlm": { - "api_base": "", - "api_key": "", - "provider": "", - "model": "" - } -} -``` - -For `provider: "openai-codex"`, `vlm.api_key` is optional once Codex OAuth is available through `openviking-server init`. - -For full configuration options and provider-specific examples, see the [Configuration Guide](../guides/01-configuration.md). - -### Client-Server Mode (Main Focus) +### Client-Server Mode Client-Server mode connects to an OpenViking server via HTTP API, supporting multi-tenancy, remote access, and other features. See the deployment documentation for how to start the OpenViking server. #### Python SDK Client ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -client = ov.SyncHTTPClient( +client = SyncHTTPClient( url="http://localhost:1933", api_key="your-key", timeout=120.0, @@ -107,7 +61,7 @@ For normal `api_key` deployments, `APIKey` is enough because the server derives tenant identity from the key. Set `Account` and `User` only for trusted deployments or gateways that explicitly forward tenant identity. -It does not implement Python embedded mode or legacy `agent_id` compatibility. +It does not implement legacy `agent_id` compatibility. See [`sdk/go/README.md`](../../../sdk/go/README.md) for package-level examples. #### JavaScript/TypeScript SDK Client @@ -229,19 +183,6 @@ openviking -o json ls viking://resources/ ## Lifecycle -### Embedded Mode - -```python -import openviking as ov - -client = ov.OpenViking(path="./data") -client.initialize() - -# ... use client ... - -client.close() -``` - ### Client-Server Mode ```python diff --git a/docs/en/api/02-resources.md b/docs/en/api/02-resources.md index 2305e724b7..b850beee45 100644 --- a/docs/en/api/02-resources.md +++ b/docs/en/api/02-resources.md @@ -151,8 +151,7 @@ This endpoint is the core entry point for resource management, supporting adding 8. Set up scheduled update task if `watch_interval` is specified **Code Entry Points**: -- `openviking/client/local.py:LocalClient.add_resource` - SDK entry (embedded) -- `openviking_cli/client/http.py:AsyncHTTPClient.add_resource` - SDK entry (HTTP) +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.add_resource` - Python SDK entry - `openviking/server/routers/resources.py:add_resource` - HTTP router - `openviking/service/resource_service.py` - Core service implementation - `crates/ov_cli/src/handlers.rs:handle_add_resource` - CLI handler @@ -305,14 +304,9 @@ curl -X POST http://localhost:1933/api/v1/resources \ **Python SDK** ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -# Using embedded mode -client = ov.OpenViking(path="./data") -client.initialize() - -# Or using HTTP client -client = ov.SyncHTTPClient(url="http://localhost:1933", api_key="your-key") +client = SyncHTTPClient(url="http://localhost:1933", api_key="your-key") client.initialize() # Add local file diff --git a/docs/en/api/03-filesystem.md b/docs/en/api/03-filesystem.md index eeda198230..d972e69851 100644 --- a/docs/en/api/03-filesystem.md +++ b/docs/en/api/03-filesystem.md @@ -41,7 +41,7 @@ List directory contents. ``` -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python entries = client.ls( @@ -138,7 +138,7 @@ Get directory tree structure. | level_limit | int | No | 3 | Maximum directory depth to traverse | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python entries = client.tree("viking://resources/") @@ -222,7 +222,7 @@ Get file or directory status information. For directories, returns the count of | uri | str | Yes | - | Viking URI | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python info = client.stat("viking://resources/docs/api.md") @@ -432,7 +432,7 @@ Create a directory. | description | str | No | `null` | Initial directory description. When provided, it is written to `.abstract.md` and queued for L0 vectorization. | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python client.mkdir("viking://resources/new-project/") @@ -506,7 +506,7 @@ Invalid URI formats, unsupported schemes, and non-public scopes return `INVALID_ | recursive | bool | No | False | Remove directory recursively | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python # Remove single file @@ -599,7 +599,7 @@ Move file or directory. | to_uri | str | Yes | - | Destination Viking URI | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python client.mv( diff --git a/docs/en/api/04-skills.md b/docs/en/api/04-skills.md index 8d0bfe9302..fa2db26a35 100644 --- a/docs/en/api/04-skills.md +++ b/docs/en/api/04-skills.md @@ -158,7 +158,7 @@ Skills are a special type of resource that define actions or tools agents can pe 5. If `wait=true`, wait for vectorization to complete **Code Entry Points**: -- `openviking/client/local.py:LocalClient.add_skill` - SDK entry point (embedded) +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.add_skill` - Python SDK entry point - `openviking_cli/client/http.py:AsyncHTTPClient.add_skill` - SDK entry point (HTTP) - `openviking/server/routers/resources.py:add_skill` - HTTP router - `openviking/service/resource_service.py:ResourceService.add_skill` - Core service implementation diff --git a/docs/en/api/05-sessions.md b/docs/en/api/05-sessions.md index a2f28878f5..151aef167f 100644 --- a/docs/en/api/05-sessions.md +++ b/docs/en/api/05-sessions.md @@ -32,7 +32,7 @@ Create a new session. Sessions are containers for conversations, storing message **Code Entries:** - `openviking/session/session.py:Session.__init__()` - Core Session class - `openviking/server/routers/sessions.py:create_session()` - HTTP route -- `openviking_cli/client/base.py:BaseClient.create_session()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.create_session()` - Python SDK - `crates/ov_cli/src/commands/session.rs:new_session()` - CLI command #### 2. Interface and Parameter Description @@ -134,7 +134,7 @@ List all sessions for the current user. Returns session IDs and URI info for fur **Code Entries:** - `openviking/server/routers/sessions.py:list_sessions()` - HTTP route -- `openviking_cli/client/base.py:BaseClient.list_sessions()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.list_sessions()` - Python SDK - `crates/ov_cli/src/commands/session.rs:list_sessions()` - CLI command #### 2. Interface and Parameter Description @@ -231,7 +231,7 @@ Get session details including metadata, message statistics, commit history, etc. **Code Entries:** - `openviking/session/session.py:Session.load()` - Session loading - `openviking/server/routers/sessions.py:get_session()` - HTTP route -- `openviking_cli/client/base.py:BaseClient.get_session()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.get_session()` - Python SDK - `crates/ov_cli/src/commands/session.rs:get_session()` - CLI command #### 2. Interface and Parameter Description @@ -510,7 +510,7 @@ Get the assembled session context used for LLM context building. This endpoint r **Code Entries:** - `openviking/session/session.py:Session.get_session_context()` - Core implementation - `openviking/server/routers/sessions.py:get_session_context()` - HTTP route -- `openviking_cli/client/base.py:BaseClient.get_session_context()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.get_session_context()` - Python SDK - `crates/ov_cli/src/commands/session.rs:get_session_context()` - CLI command #### 2. Interface and Parameter Description @@ -619,7 +619,7 @@ Get the full contents of one completed archive for a session. This endpoint is t **Code Entries:** - `openviking/session/session.py:Session.get_session_archive()` - Core implementation - `openviking/server/routers/sessions.py:get_session_archive()` - HTTP route -- `openviking_cli/client/base.py:BaseClient.get_session_archive()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.get_session_archive()` - Python SDK - `crates/ov_cli/src/commands/session.rs:get_session_archive()` - CLI command #### 2. Interface and Parameter Description @@ -734,7 +734,7 @@ Delete a session and all its data, including messages, archive history, memories **Code Entries:** - `openviking/server/routers/sessions.py:delete_session()` - HTTP route -- `openviking_cli/client/base.py:BaseClient.delete_session()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.delete_session()` - Python SDK - `crates/ov_cli/src/commands/session.rs:delete_session()` - CLI command #### 2. Interface and Parameter Description @@ -818,7 +818,7 @@ Add a message to the session. Supports two modes: simple text mode and Parts mod **Code Entries:** - `openviking/session/session.py:Session.add_message()` - Core implementation - `openviking/server/routers/sessions.py:add_message()` - HTTP route -- `openviking_cli/client/base.py:BaseClient.add_message()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.add_message()` - Python SDK - `crates/ov_cli/src/commands/session.rs:add_message()` - CLI command #### 2. Interface and Parameter Description @@ -1023,7 +1023,7 @@ Add multiple messages to a session in a single request. Suitable for scenarios t **Code Entry Points**: - `openviking/session/session.py:Session.add_messages()` - Core implementation - `openviking/server/routers/sessions.py:batch_add_messages()` - HTTP route -- `openviking_cli/client/base.py:BaseClient.batch_add_messages()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.batch_add_messages()` - Python SDK - `crates/ov_cli/src/commands/session.rs:add_messages()` - CLI command #### 2. Interface and Parameter Description @@ -1206,7 +1206,7 @@ Commit a session. Message archiving (Phase 1) completes immediately. Summary gen **Code Entries:** - `openviking/session/session.py:Session.commit_async()` - Core implementation - `openviking/server/routers/sessions.py:commit_session()` - HTTP route -- `openviking_cli/client/base.py:BaseClient.commit_session()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.commit_session()` - Python SDK - `crates/ov_cli/src/commands/session.rs:commit_session()` - CLI command #### 2. Interface and Parameter Description diff --git a/docs/en/api/11-snapshot.md b/docs/en/api/11-snapshot.md index 332092cfaa..de8972f041 100644 --- a/docs/en/api/11-snapshot.md +++ b/docs/en/api/11-snapshot.md @@ -26,7 +26,7 @@ In addition, account-level `.ovgitignore` exclusion rules can be managed (`get`/ ## Implementation - HTTP routes: [snapshot.py](https://github.com/volcengine/OpenViking/blob/main/openviking/server/routers/snapshot.py), prefix `/api/v1/snapshot`. -- SDK namespace: [snapshot_namespace.py](https://github.com/volcengine/OpenViking/blob/main/openviking/snapshot_namespace.py), exposed as `client.snapshot.*`. +- SDK namespace: [client.py](https://github.com/volcengine/OpenViking/blob/main/sdk/python/openviking_sdk/client.py), exposed as `client.snapshot.*`. - Underlying semantics: `commit` / `restore` / `show` / `log` / `diff` in [viking_fs.py](https://github.com/volcengine/OpenViking/blob/main/openviking/storage/viking_fs.py). - CLI: the `SnapshotCmd` in [main.rs](https://github.com/volcengine/OpenViking/blob/main/crates/ov_cli/src/main.rs), subcommands in [snapshot.rs](https://github.com/volcengine/OpenViking/blob/main/crates/ov_cli/src/commands/snapshot.rs). @@ -46,7 +46,7 @@ Save the current workspace state as a new snapshot. | author_name | str | No | null | Override the default author name (default `viking-bot`) | | author_email | str | No | null | Override the default author email | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python result = client.snapshot.commit( @@ -131,7 +131,7 @@ Filtering happens before the result limit is applied, so `limit=10` with `paths= To bound storage work, a filtered request inspects at most 1,000 commits. If the requested number of matches has not been collected and older uninspected history remains, the request returns an `INVALID_ARGUMENT` error instead of a partial history list. Unfiltered history is not subject to this scan budget because every inspected commit advances the result limit. -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python history = client.snapshot.log( @@ -221,7 +221,7 @@ View a commit's metadata; if `path` is given, return that file's content from th | target_ref | str | Yes | - | Commit OID (abbreviated prefix allowed), branch name, or tag | | path | str | No | null | `viking://` URI of a single file; omit to return commit metadata | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python # View commit metadata @@ -238,7 +238,7 @@ blob = client.snapshot.show("3f2a1b9c", path="viking://resources/my_project/guid console.log(await client.gitShow("main", "viking://resources/docs/api.md")); ``` -> Note: when reading a file (`path` given), the **Embedded (local) client** returns raw `bytes`, while the **HTTP client** returns a `{"oid": str, "size": int, "bytes": bytes}` dict. +> Note: when reading a file (`path` given), the Python client returns a `{"oid": str, "size": int, "bytes": bytes}` dict. **HTTP API** @@ -303,7 +303,7 @@ ov snapshot show 3f2a1b9c --path viking://resources/my_project/guide.md --out-fi Compare one UTF-8 file between two snapshot refs and return a unified diff. `to_ref` is required. When `from_ref` is omitted, the older side is treated as an empty file, which is useful for displaying the initial version. -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python result = client.snapshot.diff( @@ -384,7 +384,7 @@ This is a **forward-commit restore**: it computes the diff between `source_commi | author_name | str | No | null | Override the default author name | | author_email | str | No | null | Override the default author email | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python result = client.snapshot.restore( @@ -512,7 +512,7 @@ Three methods are provided: `get_gitignore` (read, empty string when absent), `s Reads the account `.ovgitignore` content; returns an empty string when the file is absent. -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python content = client.snapshot.get_gitignore() @@ -562,7 +562,7 @@ Writes the account `.ovgitignore` content (overwrites). The size limit (64 KiB) |-----------|------|----------|---------|-------------| | content | str | Yes | - | The `.ovgitignore` content (UTF-8) | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python client.snapshot.set_gitignore(content="*.log\n") @@ -608,7 +608,7 @@ ov snapshot ignore-set --file ./my-rules -o json Deletes the account `.ovgitignore`. Missing is success (idempotent). -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python client.snapshot.delete_gitignore() @@ -651,9 +651,9 @@ ov snapshot ignore-delete -o json A complete "commit → modify → restore" flow (Python SDK): ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -client = ov.OpenViking() +client = SyncHTTPClient(url="http://localhost:1933", api_key="your-key") client.initialize() root = "viking://resources/my_project" diff --git a/docs/en/api/12-content.md b/docs/en/api/12-content.md index d094ce311e..28f72f7ec6 100644 --- a/docs/en/api/12-content.md +++ b/docs/en/api/12-content.md @@ -572,7 +572,6 @@ This API operates on existing `viking://...` content. It does not import new fil **Authentication** - HTTP endpoint: requires admin/root role when authentication is enabled. In `api_key` mode, use an admin key for tenant content; a raw root key cannot access tenant-scoped data. -- Python embedded mode: uses the current service context - Python HTTP client / CLI: sends the current authenticated identity **Parameters** diff --git a/docs/en/api/17-tasks.md b/docs/en/api/17-tasks.md index 6d84835d42..7862e6ed23 100644 --- a/docs/en/api/17-tasks.md +++ b/docs/en/api/17-tasks.md @@ -244,7 +244,7 @@ List background tasks visible to the current caller, supporting filtering by typ **Code Entries:** - `openviking/server/routers/tasks.py:list_tasks()` - HTTP route -- `openviking_cli/client/base.py:BaseClient.list_tasks()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.list_tasks()` - Python SDK #### 2. Interface and Parameter Description diff --git a/docs/en/api/99-api-doc-writing-guide.md b/docs/en/api/99-api-doc-writing-guide.md index 26a411dffc..1a9f6da6bd 100644 --- a/docs/en/api/99-api-doc-writing-guide.md +++ b/docs/en/api/99-api-doc-writing-guide.md @@ -148,7 +148,7 @@ Example tabs are generated from bold labels. Put each invocation label in its ow paragraph and use one of these fixed base forms: `**Python SDK**`, `**TypeScript SDK**`, `**Go SDK**`, `**HTTP API**`, or `**CLI**`. When a transport qualifier is useful, put it inside the same bold label with ASCII parentheses, for example -`**Python SDK (Embedded / HTTP)**`. Do not put the qualifier after the bold label +`**Python HTTP SDK**`. Do not put the qualifier after the bold label or use full-width parentheses. Show only surfaces that are actually implemented. If an SDK or CLI does not expose the capability, omit that tab and briefly identify the available alternative. Do not wrap a handwritten HTTP request @@ -180,8 +180,8 @@ Add resources to the knowledge base, supporting various sources such as local fi 5. Build vector index **Code Entry**: -- `openviking/async_client.py:AsyncOpenViking.add_resource()` - Async SDK entry -- `openviking/sync_client.py:SyncOpenViking.add_resource()` - Sync SDK entry +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.add_resource()` - Async SDK entry +- `sdk/python/openviking_sdk/client.py:SyncHTTPClient.add_resource()` - Sync SDK entry - `openviking/service/resource_service.py:ResourceService.add_resource()` - Core implementation - `openviking/server/routers/resources.py:add_resource()` - HTTP router - `crates/ov_cli/src/handlers.rs:handle_add_resource()` - CLI handler @@ -224,10 +224,9 @@ curl -X POST http://localhost:1933/api/v1/resources \ **Python SDK** ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -client = ov.OpenViking(path="./data") -client.initialize() +client = SyncHTTPClient(url="http://localhost:1933", api_key="your-key") result = client.add_resource( "./documents/guide.md", diff --git a/docs/en/concepts/01-architecture.md b/docs/en/concepts/01-architecture.md index 0e777d3524..03e4aac29d 100644 --- a/docs/en/concepts/01-architecture.md +++ b/docs/en/concepts/01-architecture.md @@ -120,19 +120,7 @@ Messages → Compress → Archive → Memory Extraction → Storage 4. **Memory Extraction**: Extract memories from messages according to the memory policy and MemoryType schemas 5. **Storage**: Write to AGFS + vector index -## Deployment Modes - -### Embedded Mode - -For local development and single-process applications: - -```python -client = OpenViking(path="./data") -``` - -- Auto-starts AGFS subprocess -- Uses local vector index -- Singleton pattern +## Deployment Mode ### HTTP Mode diff --git a/docs/en/configuration/01-server.md b/docs/en/configuration/01-server.md index d1ebad2d0b..9496bd87f6 100644 --- a/docs/en/configuration/01-server.md +++ b/docs/en/configuration/01-server.md @@ -2,7 +2,7 @@ For initial setup, run `openviking-server init`, then run `openviking-server doctor` after saving the configuration. -The OpenViking server and embedded Python SDK mode read `ov.conf`. The default path is: +The OpenViking server reads `ov.conf`. The default path is: ```text ~/.openviking/ov.conf @@ -42,8 +42,8 @@ Optional sections use their defaults when omitted. Unknown fields are rejected. | Setting | Type / values | Default | Purpose | |---|---|---|---| -| `default_account` | string | `"default"` | Default account in embedded SDK mode | -| `default_user` | string | `"default"` | Default user in embedded SDK mode | +| `default_account` | string | `"default"` | Default account for the service context | +| `default_user` | string | `"default"` | Default user for the service context | | `embedding` | object | built-in local dense model | Dense, sparse, and hybrid embedding; defaults to `local` / `bge-small-zh-v1.5-f16` | | `vlm` | object | empty config | Content understanding, summaries, and memory extraction; configure a working model before using these capabilities | | `query_planner` | object / `null` | `null` | Retrieval intent model; falls back to `vlm` | diff --git a/docs/en/faq/faq.md b/docs/en/faq/faq.md index ce63d4dc0b..555c51c838 100644 --- a/docs/en/faq/faq.md +++ b/docs/en/faq/faq.md @@ -145,18 +145,13 @@ Supports Dense, Sparse, and Hybrid embedding modes. ### How do I initialize the client? ```python -import openviking as ov +from openviking_sdk import AsyncHTTPClient -# Async client - embedded mode (recommended) -client = ov.AsyncOpenViking(path="./my_data") -await client.initialize() - -# Async client - HTTP client mode -client = ov.AsyncHTTPClient(url="http://localhost:1933", api_key="your-key") +client = AsyncHTTPClient(url="http://localhost:1933", api_key="your-key") await client.initialize() ``` -The SDK constructor only accepts `url`, `api_key`, and `path` parameters. Other configuration (embedding, vlm, etc.) is managed through the `ov.conf` config file. +Embedding, VLM, storage, and other service configuration is managed by the OpenViking Server through `ov.conf`. ### What file formats are supported? @@ -368,25 +363,10 @@ This strategy finds semantically matching fragments while understanding the comp 1. **Batch processing**: Adding multiple resources at once is more efficient than one by one 2. **Set appropriate `batch_size`**: Adjust batch processing size in Embedding configuration 3. **Use local storage**: Use `local` backend during development to reduce network latency -4. **Async operations**: Fully utilize `AsyncOpenViking` / `AsyncHTTPClient`'s async capabilities +4. **Async operations**: Fully utilize `AsyncHTTPClient`'s async capabilities ## Deployment -### What's the difference between embedded mode and service mode? - -| Mode | Use Case | Characteristics | -|------|----------|-----------------| -| **Embedded** | Local development, single-process apps | Auto-starts AGFS subprocess, uses local vector index | -| **Service Mode** | Production, distributed deployment | Connects to remote services, supports multi-instance concurrency, independently scalable | - -```python -# Embedded mode -client = ov.AsyncOpenViking(path="./data") - -# HTTP client mode (connects to a remote server) -client = ov.AsyncHTTPClient(url="http://localhost:1933", api_key="your-key") -``` - ### Is OpenViking open source? Yes, OpenViking main project is open source under the AGPL-3.0 license, and examples/ and crates/ov_cli are licensed under the Apache 2.0 license. diff --git a/docs/en/getting-started/02-quickstart.md b/docs/en/getting-started/02-quickstart.md index 392b112041..4d87ac3b1b 100644 --- a/docs/en/getting-started/02-quickstart.md +++ b/docs/en/getting-started/02-quickstart.md @@ -159,13 +159,13 @@ export OPENVIKING_CONFIG_FILE=/path/to/your/ov.conf Create `example.py`: ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -# Initialize OpenViking client with data directory -client = ov.OpenViking(path="./data") +# Connect to OpenViking Server +client = SyncHTTPClient(url="http://localhost:1933", api_key="your-key") try: - # Initialize the client + # Check the connection client.initialize() # Add resource (supports URL, file, or directory) @@ -196,8 +196,8 @@ try: # Perform semantic search results = client.find("what is openviking", target_uri=root_uri) print("Search results:") - for r in results.resources: - print(f" {r.uri} (score: {r.score:.4f})") + for result in results.get("resources", []): + print(f" {result['uri']} (score: {result.get('score', 0.0):.4f})") # Close the client client.close() diff --git a/docs/en/guides/01-configuration.md b/docs/en/guides/01-configuration.md index 80b0e9dd9c..524384eb1a 100644 --- a/docs/en/guides/01-configuration.md +++ b/docs/en/guides/01-configuration.md @@ -1312,7 +1312,7 @@ OpenViking uses two config files: | File | Purpose | Default Path | |------|---------|-------------| -| `ov.conf` | SDK embedded mode + server config | `~/.openviking/ov.conf` | +| `ov.conf` | OpenViking Server configuration | `~/.openviking/ov.conf` | | `ovcli.conf` | HTTP client and CLI connection to remote server | `~/.openviking/ovcli.conf` | When config files are at the default path, OpenViking loads them automatically — no additional setup needed. @@ -1350,7 +1350,7 @@ openviking-server --config /path/to/ov.conf ### ov.conf -The config sections documented above (embedding, vlm, rerank, retrieval, grep, storage) all belong to `ov.conf`. SDK embedded mode and server share this file. +The config sections documented above (embedding, vlm, rerank, retrieval, grep, storage) all belong to the server's `ov.conf`. For memory-related settings, add a `memory` section in `ov.conf`: diff --git a/docs/en/guides/02-volcengine-purchase-guide.md b/docs/en/guides/02-volcengine-purchase-guide.md index 11faa215fe..24b039fdba 100644 --- a/docs/en/guides/02-volcengine-purchase-guide.md +++ b/docs/en/guides/02-volcengine-purchase-guide.md @@ -171,11 +171,11 @@ Save the following content as `~/.openviking/ov.conf`: ### Test Connection ```python -import openviking as ov import asyncio +from openviking_sdk import AsyncHTTPClient async def test(): - client = ov.AsyncOpenViking(path="./test_data") + client = AsyncHTTPClient(url="http://localhost:1933", api_key="your-key") await client.initialize() # Test adding a simple resource diff --git a/docs/en/guides/05-observability.md b/docs/en/guides/05-observability.md index b0cfeb7468..c829b2dbbe 100644 --- a/docs/en/guides/05-observability.md +++ b/docs/en/guides/05-observability.md @@ -36,7 +36,7 @@ curl http://localhost:1933/health ### Overall system status -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python status = client.get_status() @@ -89,7 +89,7 @@ curl http://localhost:1933/api/v1/observer/queue \ ### Quick health check -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python if client.is_healthy(): diff --git a/docs/en/guides/07-operation-telemetry.md b/docs/en/guides/07-operation-telemetry.md index a8c55a217c..5b6407eb15 100644 --- a/docs/en/guides/07-operation-telemetry.md +++ b/docs/en/guides/07-operation-telemetry.md @@ -338,9 +338,9 @@ curl -X POST http://localhost:1933/api/v1/resources \ ### Python SDK ```python -from openviking import AsyncOpenVikingClient +from openviking_sdk import AsyncHTTPClient -client = AsyncOpenVikingClient(config_path="/path/to/config.yaml") +client = AsyncHTTPClient(url="http://localhost:1933", api_key="your-key") await client.initialize() result = await client.find("memory dedup", telemetry=True) diff --git a/docs/en/guides/08-encryption.md b/docs/en/guides/08-encryption.md index 5700b632ef..d566096e22 100644 --- a/docs/en/guides/08-encryption.md +++ b/docs/en/guides/08-encryption.md @@ -56,13 +56,13 @@ Edit `~/.openviking/ov.conf`: ### 3. Verify ```python -import openviking as ov import asyncio from pathlib import Path +from openviking_sdk import AsyncHTTPClient async def test(): - client = ov.AsyncOpenViking(path="./data") + client = AsyncHTTPClient(url="http://localhost:1933", api_key="your-key") await client.initialize() # add_resource expects a file path or URL diff --git a/docs/en/guides/09-ovpack.md b/docs/en/guides/09-ovpack.md index d0deb4b60d..ffc61a2d46 100644 --- a/docs/en/guides/09-ovpack.md +++ b/docs/en/guides/09-ovpack.md @@ -160,11 +160,11 @@ them. ## Python SDK ```python -from openviking import AsyncOpenViking +from openviking_sdk import AsyncHTTPClient async def migrate_project(): - client = AsyncOpenViking() + client = AsyncHTTPClient(url="http://localhost:1933", api_key="your-key") await client.initialize() try: await client.export_ovpack( diff --git a/docs/en/guides/15-snapshot.md b/docs/en/guides/15-snapshot.md index 6e59a500c4..51e07ded05 100644 --- a/docs/en/guides/15-snapshot.md +++ b/docs/en/guides/15-snapshot.md @@ -129,9 +129,9 @@ Once enabled, all three surfaces expose snapshot commands. The examples below sh Snapshot methods live under the `client.snapshot.*` namespace. ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -client = ov.OpenViking() +client = SyncHTTPClient(url="http://localhost:1933", api_key="your-key") client.initialize() root = "viking://resources/my_project" diff --git a/docs/zh/about/03-roadmap.md b/docs/zh/about/03-roadmap.md index ae2b56c4bf..d2193d02be 100644 --- a/docs/zh/about/03-roadmap.md +++ b/docs/zh/about/03-roadmap.md @@ -64,7 +64,7 @@ - HTTP Server(FastAPI) - 内置 MCP 端点 - Python HTTP Client -- 客户端抽象层(LocalClient / HTTPClient) +- Python HTTP Client SDK - Web 控制台 ### CLI diff --git a/docs/zh/agent-integrations/07-langchain-langgraph.md b/docs/zh/agent-integrations/07-langchain-langgraph.md index 7e68ede25a..b521db86be 100644 --- a/docs/zh/agent-integrations/07-langchain-langgraph.md +++ b/docs/zh/agent-integrations/07-langchain-langgraph.md @@ -2,7 +2,7 @@ 把 OpenViking 接入你的 LangChain 或 LangGraph Agent 作为上下文后端。独立集成包提供 retriever、chat history、context wrapper、agent tools、LangGraph store 和 middleware, -可连接 HTTP 服务或嵌入式 OpenViking。 +统一连接 OpenViking HTTP 服务。 ## 安装 @@ -26,9 +26,8 @@ tools = create_openviking_tools( ) ``` -同时省略 `url` 和 `path` 时,适配器会使用 OpenViking CLI 配置中的 HTTP 连接信息。传入 -`path` 时,通过 OpenViking 同步 client 使用嵌入式 workspace,该模式还需要安装完整的 -`openviking` 包。Embedding 和 VLM 在 OpenViking 侧配置,不在你的应用中。 +省略 `url` 时,适配器会使用 OpenViking CLI 配置中的 HTTP 连接信息。Embedding 和 VLM +在 OpenViking 侧配置,不在你的应用中。 ### 异步应用 @@ -44,13 +43,12 @@ result = await chain.ainvoke( ) ``` -异步适配器支持三种 client 模式: +异步适配器支持两种 client 模式: | 配置 | 异步接口 | 所有权 | |------|----------|--------| | `client=` 或 `async_client=` | 原样返回注入的 client | 调用方 | -| `url=`,或同时省略 `url` 和 `path` | 每个 event loop 一个支持恢复的 HTTP handle | Adapter | -| `path=` | 在 worker thread 中调用同步嵌入式 client | Adapter | +| `url=`,或省略 | 每个 event loop 一个支持恢复的 HTTP handle | Adapter | 长期运行的应用可以初始化一个由调用方管理的异步 client,并在同一 event loop 内的多个适配器之间复用: @@ -72,12 +70,6 @@ finally: 注入异步 client;应为每个 loop 分别创建并管理 client。注入的同步 client 仍可安全地 用于异步 adapter 方法,因为调用会在 worker thread 中执行。 -`path=` 嵌入式 adapter 使用同步 fallback 是有意设计:`SyncOpenViking` 会让有状态的 -嵌入式引擎保持在 OpenViking 的共享后台 loop 上,同时不阻塞应用 event loop。若要使用 -原生嵌入式异步方法,请自行创建并初始化 `AsyncOpenViking`,通过 `async_client=` 注入, -在同一个 event loop 中使用,并由调用方自行关闭。每个进程同时只能运行一个嵌入式 -workspace;切换 workspace 前应先关闭或 reset 当前 client。 - `OpenVikingChatMessageHistory` 提供 `aget_messages()`、`aadd_messages()` 和 `aclear()`;`OpenVikingSessionRecorder` 提供 `arecord()`、`aflush()` 和 `aclose()`。异步 LangGraph 运行会自动选择 `awrap_model_call()` 和 @@ -187,7 +179,7 @@ middleware = OpenVikingContextMiddleware( 凭证决定。因此,多用户应用必须先选择绑定对应用户凭证的 client,再调用 middleware。 Actor peer 只能从已经认证、由服务端控制的 runtime 字段中解析;不要信任 model state 或客户端可控的 configurable 值。运行时 actor-peer 解析仅支持 HTTP-backed -middleware,不支持 embedded `path=` client。注入的自定义 client 必须设置 +middleware。注入的自定义 client 必须设置 `supports_request_actor_peer = True`,并遵循 `openviking_sdk` 的 actor-peer 作用域。在已有环境中启用该能力前,应同时升级 `openviking-sdk` 和 `openviking`。 diff --git a/docs/zh/api/01-overview.md b/docs/zh/api/01-overview.md index 905d7132ed..d619ab53d6 100644 --- a/docs/zh/api/01-overview.md +++ b/docs/zh/api/01-overview.md @@ -4,69 +4,23 @@ ## 连接模式 -OpenViking 支持两种使用模式:**嵌入式模式**(直接调用 Python API)和 **Client-Server 模式**(通过 HTTP API 连接)。 - -本 API 文档主要介绍 **Client-Server 模式**的 HTTP API 使用方式。嵌入式模式虽然可用,但后续文档将不单独展开介绍。 +OpenViking 客户端通过 HTTP 连接 OpenViking Server。 | 模式 | 适用场景 | 说明 | |------|----------|------| -| **嵌入式模式** | 本地开发、单进程 | 使用本地数据存储运行 | | **HTTP** | 连接 OpenViking 服务器 | 通过 HTTP API 连接远程服务器 | | **CLI** | Shell 脚本、Agent 工具使用 | 通过 CLI 命令连接服务器 | -### 嵌入式模式(简要说明) - -嵌入式模式允许在 Python 进程内直接调用 OpenViking API,无需启动独立的服务器进程。 - -```python -import openviking as ov - -client = ov.OpenViking(path="./data") -client.initialize() -``` - -嵌入式模式通过 `ov.conf` 配置 embedding、vlm、storage 等模块。默认配置路径为 `~/.openviking/ov.conf`,也可通过环境变量指定: - -```bash -export OPENVIKING_CONFIG_FILE=/path/to/ov.conf -``` - -最小配置示例: - -```json -{ - "embedding": { - "dense": { - "api_base": "", - "api_key": "", - "provider": "", - "dimension": 1024, - "model": "" - } - }, - "vlm": { - "api_base": "", - "api_key": "", - "provider": "", - "model": "" - } -} -``` - -对于 `provider: "openai-codex"`,通过 `openviking-server init` 配置 Codex OAuth 后,`vlm.api_key` 是可选的。 - -完整的配置选项和 provider 特定示例,请参见 [配置指南](../guides/01-configuration.md)。 - -### Client-Server 模式(主要介绍) +### Client-Server 模式 Client-Server 模式通过 HTTP API 连接 OpenViking 服务器,支持多租户、远程访问等特性。OpenViking 的服务器启动方式请参见相关部署文档。 #### Python SDK 客户端 ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -client = ov.SyncHTTPClient( +client = SyncHTTPClient( url="http://localhost:1933", api_key="your-key", timeout=120.0, @@ -104,7 +58,7 @@ Go SDK 发送的身份请求头与 Python HTTP client 一致: 普通 `api_key` 部署下只需要设置 `APIKey`,服务端会从 API key 推导租户身份。只有在 trusted 部署或网关显式透传租户身份时,才需要设置 `Account` 和 `User`。 -Go SDK 不支持 Python embedded 模式,也不保留旧 `agent_id` 兼容路径。更多示例见 [`sdk/go/README_CN.md`](../../../sdk/go/README_CN.md)。 +Go SDK 不保留旧 `agent_id` 兼容路径。更多示例见 [`sdk/go/README_CN.md`](../../../sdk/go/README_CN.md)。 #### JavaScript/TypeScript SDK 客户端 @@ -224,19 +178,6 @@ openviking -o json ls viking://resources/ ## 生命周期 -### 嵌入式模式 - -```python -import openviking as ov - -client = ov.OpenViking(path="./data") -client.initialize() - -# ... 使用 client ... - -client.close() -``` - ### Client-Server 模式 ```python diff --git a/docs/zh/api/02-resources.md b/docs/zh/api/02-resources.md index 7c3d284c74..0e963ee21c 100644 --- a/docs/zh/api/02-resources.md +++ b/docs/zh/api/02-resources.md @@ -144,8 +144,7 @@ URL/文件 Parser TreeBuilder AGFS Summarizer/Vector 8. 如指定 `--watch-interval`,设置定时更新任务 **代码入口**: -- `openviking/client/local.py:LocalClient.add_resource` - SDK 入口(嵌入式) -- `openviking_cli/client/http.py:AsyncHTTPClient.add_resource` - SDK 入口(HTTP) +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.add_resource` - Python SDK 入口 - `openviking/server/routers/resources.py:add_resource` - HTTP 路由 - `openviking/service/resource_service.py` - 核心服务实现 - `crates/ov_cli/src/handlers.rs:handle_add_resource` - CLI 处理 @@ -310,14 +309,9 @@ curl -X POST http://localhost:1933/api/v1/resources \ **Python SDK** ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -# 使用嵌入式模式(以后不再推荐和详细介绍) -client = ov.OpenViking(path="./data") -client.initialize() - -# 使用 HTTP 客户端模式 -client = ov.SyncHTTPClient(url="http://localhost:1933", api_key="your-key") +client = SyncHTTPClient(url="http://localhost:1933", api_key="your-key") client.initialize() ## 添加本地文件 diff --git a/docs/zh/api/03-filesystem.md b/docs/zh/api/03-filesystem.md index 7793b7a67e..eca71f0db4 100644 --- a/docs/zh/api/03-filesystem.md +++ b/docs/zh/api/03-filesystem.md @@ -42,7 +42,7 @@ OpenViking 提供类 Unix 的文件系统操作来管理上下文。 ``` -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python entries = client.ls( @@ -139,7 +139,7 @@ openviking ls viking://resources/ [--simple] [--recursive] | level_limit | int | 否 | 3 | 最大目录遍历深度 | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python entries = client.tree("viking://resources/") @@ -223,7 +223,7 @@ openviking tree viking://resources/my-project/ | uri | str | 是 | - | Viking URI | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python info = client.stat("viking://resources/docs/api.md") @@ -433,7 +433,7 @@ openviking attrs set-tags viking://resources/docs --tags team=search --mode appe | description | str | 否 | `null` | 目录初始说明。传入后会写入 `.abstract.md`,并进入目录 L0 向量化队列。 | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python client.mkdir("viking://resources/new-project/") @@ -507,7 +507,7 @@ URI 格式非法、scheme 不支持或使用非公开作用域时返回 `INVALID | recursive | bool | 否 | False | 递归删除目录 | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python # 删除单个文件 @@ -600,7 +600,7 @@ openviking rm viking://resources/old.md [--recursive] | to_uri | str | 是 | - | 目标 Viking URI | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python client.mv( diff --git a/docs/zh/api/04-skills.md b/docs/zh/api/04-skills.md index cd75b579cf..e796457933 100644 --- a/docs/zh/api/04-skills.md +++ b/docs/zh/api/04-skills.md @@ -157,7 +157,7 @@ This tool wraps the MCP tool `search-web`. Call this when the user needs functio 5. 如指定 `wait=True`,等待向量化完成 **代码入口**: -- `openviking/client/local.py:LocalClient.add_skill` - SDK 入口(嵌入式) +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.add_skill` - Python SDK 入口 - `openviking_cli/client/http.py:AsyncHTTPClient.add_skill` - SDK 入口(HTTP) - `openviking/server/routers/resources.py:add_skill` - HTTP 路由 - `openviking/service/resource_service.py:ResourceService.add_skill` - 核心服务实现 diff --git a/docs/zh/api/05-sessions.md b/docs/zh/api/05-sessions.md index 2c4c945720..8f5f9a4030 100644 --- a/docs/zh/api/05-sessions.md +++ b/docs/zh/api/05-sessions.md @@ -32,7 +32,7 @@ Session API 按认证用户作用域访问会话,并返回 canonical user sess **代码入口**: - `openviking/session/session.py:Session.__init__()` - Session 核心类 - `openviking/server/routers/sessions.py:create_session()` - HTTP 路由 -- `openviking_cli/client/base.py:BaseClient.create_session()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.create_session()` - Python SDK - `crates/ov_cli/src/commands/session.rs:new_session()` - CLI 命令 #### 2. 接口和参数说明 @@ -134,7 +134,7 @@ ov session new **代码入口**: - `openviking/server/routers/sessions.py:list_sessions()` - HTTP 路由 -- `openviking_cli/client/base.py:BaseClient.list_sessions()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.list_sessions()` - Python SDK - `crates/ov_cli/src/commands/session.rs:list_sessions()` - CLI 命令 #### 2. 接口和参数说明 @@ -231,7 +231,7 @@ ov session list **代码入口**: - `openviking/session/session.py:Session.load()` - 会话加载 - `openviking/server/routers/sessions.py:get_session()` - HTTP 路由 -- `openviking_cli/client/base.py:BaseClient.get_session()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.get_session()` - Python SDK - `crates/ov_cli/src/commands/session.rs:get_session()` - CLI 命令 #### 2. 接口和参数说明 @@ -510,7 +510,7 @@ curl --get http://localhost:1933/api/v1/sessions/session-id/tool-results/tool-re **代码入口**: - `openviking/session/session.py:Session.get_session_context()` - 核心实现 - `openviking/server/routers/sessions.py:get_session_context()` - HTTP 路由 -- `openviking_cli/client/base.py:BaseClient.get_session_context()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.get_session_context()` - Python SDK - `crates/ov_cli/src/commands/session.rs:get_session_context()` - CLI 命令 #### 2. 接口和参数说明 @@ -619,7 +619,7 @@ ov session get-session-context a1b2c3d4 --token-budget 128000 **代码入口**: - `openviking/session/session.py:Session.get_session_archive()` - 核心实现 - `openviking/server/routers/sessions.py:get_session_archive()` - HTTP 路由 -- `openviking_cli/client/base.py:BaseClient.get_session_archive()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.get_session_archive()` - Python SDK - `crates/ov_cli/src/commands/session.rs:get_session_archive()` - CLI 命令 #### 2. 接口和参数说明 @@ -734,7 +734,7 @@ ov session get-session-archive a1b2c3d4 archive_002 **代码入口**: - `openviking/server/routers/sessions.py:delete_session()` - HTTP 路由 -- `openviking_cli/client/base.py:BaseClient.delete_session()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.delete_session()` - Python SDK - `crates/ov_cli/src/commands/session.rs:delete_session()` - CLI 命令 #### 2. 接口和参数说明 @@ -817,7 +817,7 @@ ov session delete a1b2c3d4 **代码入口**: - `openviking/session/session.py:Session.add_message()` - 核心实现 - `openviking/server/routers/sessions.py:add_message()` - HTTP 路由 -- `openviking_cli/client/base.py:BaseClient.add_message()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.add_message()` - Python SDK - `crates/ov_cli/src/commands/session.rs:add_message()` - CLI 命令 #### 2. 接口和参数说明 @@ -997,7 +997,7 @@ ov session add-message a1b2c3d4 --role user --content "How do I authenticate use **代码入口**: - `openviking/session/session.py:Session.add_messages()` - 核心实现 - `openviking/server/routers/sessions.py:batch_add_messages()` - HTTP 路由 -- `openviking_cli/client/base.py:BaseClient.batch_add_messages()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.batch_add_messages()` - Python SDK - `crates/ov_cli/src/commands/session.rs:add_messages()` - CLI 命令 #### 2. 接口和参数说明 @@ -1180,7 +1180,7 @@ curl -X POST http://localhost:1933/api/v1/sessions/a1b2c3d4/used \ **代码入口**: - `openviking/session/session.py:Session.commit_async()` - 核心实现 - `openviking/server/routers/sessions.py:commit_session()` - HTTP 路由 -- `openviking_cli/client/base.py:BaseClient.commit_session()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.commit_session()` - Python SDK - `crates/ov_cli/src/commands/session.rs:commit_session()` - CLI 命令 #### 2. 接口和参数说明 diff --git a/docs/zh/api/11-snapshot.md b/docs/zh/api/11-snapshot.md index 1a4ff5cfc8..b665586e56 100644 --- a/docs/zh/api/11-snapshot.md +++ b/docs/zh/api/11-snapshot.md @@ -26,7 +26,7 @@ OpenViking 在 VikingFS 之上提供了一套基于 Git 的多版本管理能力 ## API 实现介绍 - HTTP 路由:[snapshot.py](https://github.com/volcengine/OpenViking/blob/main/openviking/server/routers/snapshot.py),前缀 `/api/v1/snapshot`。 -- 命名空间(SDK):[snapshot_namespace.py](https://github.com/volcengine/OpenViking/blob/main/openviking/snapshot_namespace.py),暴露为 `client.snapshot.*`。 +- 命名空间(SDK):[client.py](https://github.com/volcengine/OpenViking/blob/main/sdk/python/openviking_sdk/client.py),暴露为 `client.snapshot.*`。 - 底层语义实现:[viking_fs.py](https://github.com/volcengine/OpenViking/blob/main/openviking/storage/viking_fs.py) 的 `commit` / `restore` / `show` / `log` / `diff`。 - CLI 命令:[main.rs](https://github.com/volcengine/OpenViking/blob/main/crates/ov_cli/src/main.rs) 的 `SnapshotCmd`,子命令 [snapshot.rs](https://github.com/volcengine/OpenViking/blob/main/crates/ov_cli/src/commands/snapshot.rs)。 @@ -46,7 +46,7 @@ OpenViking 在 VikingFS 之上提供了一套基于 Git 的多版本管理能力 | author_name | str | 否 | null | 覆盖默认的提交者名字(默认 `viking-bot`) | | author_email | str | 否 | null | 覆盖默认的提交者邮箱 | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python result = client.snapshot.commit( @@ -131,7 +131,7 @@ ov snapshot commit -m "v1 initial import" --paths viking://resources/my_md.md -o 为限制存储开销,过滤请求最多检查 1,000 条提交。如果尚未收集到请求数量的匹配结果,并且仍存在未检查的更早历史,接口将返回 `INVALID_ARGUMENT` 错误,而不是返回不完整的历史列表。非过滤请求不受该扫描预算限制,因为每检查一条提交都会推进返回数量限制。 -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python history = client.snapshot.log( @@ -221,7 +221,7 @@ ov snapshot log --limit 10 \ | target_ref | str | 是 | - | 提交 OID(支持缩写前缀)、分支名或标签 | | path | str | 否 | null | 某个文件的 `viking://` URI;省略时返回提交元数据 | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python # 查看提交元数据 @@ -238,7 +238,7 @@ blob = client.snapshot.show("3f2a1b9c", path="viking://resources/my_project/guid console.log(await client.gitShow("main", "viking://resources/docs/api.md")); ``` -> 注意:带 `path` 读取文件内容时,**Embedded(本地)客户端**直接返回原始 `bytes`;**HTTP 客户端**返回 `{"oid": str, "size": int, "bytes": bytes}` 字典。 +> 注意:带 `path` 读取文件内容时,Python 客户端返回 `{"oid": str, "size": int, "bytes": bytes}` 字典。 **HTTP API** @@ -303,7 +303,7 @@ ov snapshot show 3f2a1b9c --path viking://resources/my_project/guide.md --out-fi 对比一个 UTF-8 文件在两个快照引用中的内容,并返回 unified diff。`to_ref` 必填;省略 `from_ref` 时,旧版本按空文件处理,可用于展示文件的初始版本。 -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python result = client.snapshot.diff( @@ -384,7 +384,7 @@ ov snapshot diff viking://resources/my_project/guide.md \ | author_name | str | 否 | null | 覆盖默认的提交者名字 | | author_email | str | 否 | null | 覆盖默认的提交者邮箱 | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python result = client.snapshot.restore( @@ -512,7 +512,7 @@ ov snapshot restore 3f2a1b9c viking://resources/my_project --dry-run -o json 读取账号 `.ovgitignore` 内容;文件不存在时返回空字符串。 -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python content = client.snapshot.get_gitignore() @@ -562,7 +562,7 @@ ov snapshot ignore-get -o json |------|------|------|--------|------| | content | str | 是 | - | `.ovgitignore` 文件内容(UTF-8) | -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python client.snapshot.set_gitignore(content="*.log\n") @@ -608,7 +608,7 @@ ov snapshot ignore-set --file ./my-rules -o json 删除账号 `.ovgitignore`。文件不存在也视为成功(幂等)。 -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python client.snapshot.delete_gitignore() @@ -651,9 +651,9 @@ ov snapshot ignore-delete -o json 下面演示一个"提交 → 修改 → 恢复"的完整流程(Python SDK): ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -client = ov.OpenViking() +client = SyncHTTPClient(url="http://localhost:1933", api_key="your-key") client.initialize() root = "viking://resources/my_project" diff --git a/docs/zh/api/12-content.md b/docs/zh/api/12-content.md index 74fa0e24fd..d1e6dbdf5d 100644 --- a/docs/zh/api/12-content.md +++ b/docs/zh/api/12-content.md @@ -572,7 +572,6 @@ ov set-tags viking://resources/project/ \ **认证** - HTTP 端点:在开启认证时要求 admin/root 角色。`api_key` 模式下,租户内容重建请使用 admin key;裸 root key 不能访问租户级数据。 -- Python embedded 模式:使用当前 service context - Python HTTP client / CLI:使用当前认证身份发起请求 **参数** diff --git a/docs/zh/api/17-tasks.md b/docs/zh/api/17-tasks.md index 1726c478df..61fbd129f0 100644 --- a/docs/zh/api/17-tasks.md +++ b/docs/zh/api/17-tasks.md @@ -242,7 +242,7 @@ ov task cancel uuid-xxx **代码入口**: - `openviking/server/routers/tasks.py:list_tasks()` - HTTP 路由 -- `openviking_cli/client/base.py:BaseClient.list_tasks()` - Python SDK +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.list_tasks()` - Python SDK #### 2. 接口和参数说明 diff --git a/docs/zh/api/99-api-doc-writing-guide.md b/docs/zh/api/99-api-doc-writing-guide.md index 6d486112a0..49188296e2 100644 --- a/docs/zh/api/99-api-doc-writing-guide.md +++ b/docs/zh/api/99-api-doc-writing-guide.md @@ -146,7 +146,7 @@ API 文档按模块组织,每个模块一个文件,使用两位数字序号 示例切换由加粗标签自动生成。调用方式标签必须单独成段,并使用以下固定基础写法: `**Python SDK**`、`**TypeScript SDK**`、`**Go SDK**`、`**HTTP API**`、`**CLI**`。 需要区分调用形态时,可以在同一个加粗标签内追加半角括号限定词,例如 -`**Python SDK (Embedded / HTTP)**`;不要把限定词写在加粗标签外,也不要使用全角括号。 +`**Python HTTP SDK**`;不要把限定词写在加粗标签外,也不要使用全角括号。 只展示实现中真实存在的调用方式;某个 SDK 或 CLI 没有对应能力时应省略该 Tab,并简短说明 可用的替代入口。不要把手写 HTTP 请求包装成不存在的 SDK 方法。 @@ -174,8 +174,8 @@ API 文档应按 API 模块和具体接口组织,而不是按客户端语言 5. 建立向量索引 **代码入口**: -- `openviking/async_client.py:AsyncOpenViking.add_resource()` - 异步 SDK 入口 -- `openviking/sync_client.py:SyncOpenViking.add_resource()` - 同步 SDK 入口 +- `sdk/python/openviking_sdk/client.py:AsyncHTTPClient.add_resource()` - 异步 SDK 入口 +- `sdk/python/openviking_sdk/client.py:SyncHTTPClient.add_resource()` - 同步 SDK 入口 - `openviking/service/resource_service.py:ResourceService.add_resource()` - 核心实现 - `openviking/server/routers/resources.py:add_resource()` - HTTP 路由 - `crates/ov_cli/src/handlers.rs:handle_add_resource()` - CLI 处理函数 @@ -218,10 +218,9 @@ curl -X POST http://localhost:1933/api/v1/resources \ **Python SDK** ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -client = ov.OpenViking(path="./data") -client.initialize() +client = SyncHTTPClient(url="http://localhost:1933", api_key="your-key") result = client.add_resource( "./documents/guide.md", diff --git a/docs/zh/concepts/01-architecture.md b/docs/zh/concepts/01-architecture.md index cb5be02892..6ee69a9ed3 100644 --- a/docs/zh/concepts/01-architecture.md +++ b/docs/zh/concepts/01-architecture.md @@ -121,18 +121,6 @@ OpenViking 采用双层存储架构,实现内容与索引分离(详见 [存 ## 部署模式 -### 嵌入式模式 - -用于本地开发和单进程应用: - -```python -client = OpenViking(path="./data") -``` - -- 自动启动 AGFS 子进程 -- 使用本地向量索引 -- 单例模式 - ### HTTP 模式 用于团队共享、生产环境和跨语言集成: diff --git a/docs/zh/configuration/01-server.md b/docs/zh/configuration/01-server.md index f87bd22feb..53a776d636 100644 --- a/docs/zh/configuration/01-server.md +++ b/docs/zh/configuration/01-server.md @@ -2,7 +2,7 @@ 首次配置建议使用 `openviking-server init`,保存后运行 `openviking-server doctor`。 -OpenViking 服务端和 Python SDK 嵌入模式读取 `ov.conf`。默认路径是: +OpenViking 服务端读取 `ov.conf`。默认路径是: ```text ~/.openviking/ov.conf @@ -42,8 +42,8 @@ openviking-server --config /path/to/ov.conf | 配置项 | 类型 / 可选值 | 默认值 | 作用 | |---|---|---|---| -| `default_account` | string | `"default"` | SDK 嵌入模式使用的默认账号 | -| `default_user` | string | `"default"` | SDK 嵌入模式使用的默认用户 | +| `default_account` | string | `"default"` | Service context 使用的默认账号 | +| `default_user` | string | `"default"` | Service context 使用的默认用户 | | `embedding` | object | 内置本地 Dense 模型 | 向量化模型和稀疏/混合检索配置;默认使用 `local` / `bge-small-zh-v1.5-f16` | | `vlm` | object | 空配置 | 内容理解、摘要和记忆抽取使用的模型;使用相关能力前需要配置可用模型 | | `query_planner` | object / `null` | `null` | 检索意图分析模型;未配置时回退到 `vlm` | diff --git a/docs/zh/faq/faq.md b/docs/zh/faq/faq.md index 740e977c8d..6cc27d47a2 100644 --- a/docs/zh/faq/faq.md +++ b/docs/zh/faq/faq.md @@ -138,18 +138,13 @@ pip install openviking --upgrade --force-reinstall ### 如何初始化客户端? ```python -import openviking as ov +from openviking_sdk import AsyncHTTPClient -# 异步客户端(推荐)- 嵌入模式 -client = ov.AsyncOpenViking(path="./my_data") -await client.initialize() - -# 异步客户端 - 服务模式 -client = ov.AsyncHTTPClient(url="http://localhost:1933", api_key="your-key") +client = AsyncHTTPClient(url="http://localhost:1933", api_key="your-key") await client.initialize() ``` -SDK 构造函数仅接受 `url`、`api_key`、`path` 参数。其他配置(embedding、vlm 等)通过 `ov.conf` 配置文件管理。 +Embedding、VLM、存储等服务配置由 OpenViking Server 通过 `ov.conf` 管理。 ### 支持哪些文件格式? @@ -361,25 +356,10 @@ OpenViking 使用分数传播机制: 1. **批量处理**:一次添加多个资源比逐个添加更高效 2. **合理设置 `batch_size`**:Embedding 配置中调整批处理大小 3. **使用本地存储**:开发阶段使用 `local` 后端减少网络延迟 -4. **异步操作**:充分利用 `AsyncOpenViking` / `AsyncHTTPClient` 的异步特性 +4. **异步操作**:充分利用 `AsyncHTTPClient` 的异步特性 ## 部署相关 -### 嵌入式模式和服务模式有什么区别? - -| 模式 | 适用场景 | 特点 | -|------|----------|------| -| **嵌入式** | 本地开发、单进程应用 | 自动启动 AGFS 子进程,使用本地向量索引 | -| **服务模式** | 生产环境、分布式部署 | 连接远程服务,支持多实例并发,可独立扩展 | - -```python -# 嵌入式模式 -client = ov.AsyncOpenViking(path="./data") - -# 服务模式 -client = ov.AsyncHTTPClient(url="http://localhost:1933", api_key="your-key") -``` - ### OpenViking 是开源的吗? 是的,OpenViking 完全开源,主体采用 AGPLv3 许可证,详见 README.md 说明。 diff --git a/docs/zh/getting-started/02-quickstart.md b/docs/zh/getting-started/02-quickstart.md index bc68b31dfa..5c34b027ef 100644 --- a/docs/zh/getting-started/02-quickstart.md +++ b/docs/zh/getting-started/02-quickstart.md @@ -159,13 +159,13 @@ export OPENVIKING_CONFIG_FILE=/path/to/your/ov.conf 创建 `example.py`: ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -# Initialize OpenViking client with data directory -client = ov.OpenViking(path="./data") +# 连接 OpenViking Server +client = SyncHTTPClient(url="http://localhost:1933", api_key="your-key") try: - # Initialize the client + # 检查连接 client.initialize() # Add resource (supports URL, file, or directory) @@ -196,8 +196,8 @@ try: # Perform semantic search results = client.find("what is openviking", target_uri=root_uri) print("Search results:") - for r in results.resources: - print(f" {r.uri} (score: {r.score:.4f})") + for result in results.get("resources", []): + print(f" {result['uri']} (score: {result.get('score', 0.0):.4f})") # Close the client client.close() diff --git a/docs/zh/guides/01-configuration.md b/docs/zh/guides/01-configuration.md index b16ed5fb4d..8f242b9097 100644 --- a/docs/zh/guides/01-configuration.md +++ b/docs/zh/guides/01-configuration.md @@ -1282,7 +1282,7 @@ OpenViking 使用两个配置文件: | 配置文件 | 用途 | 默认路径 | |---------|------|---------| -| `ov.conf` | SDK 嵌入模式 + 服务端配置 | `~/.openviking/ov.conf` | +| `ov.conf` | OpenViking Server 配置 | `~/.openviking/ov.conf` | | `ovcli.conf` | HTTP 客户端和 CLI 连接远程服务端 | `~/.openviking/ovcli.conf` | 配置文件放在默认路径时,OpenViking 自动加载,无需额外设置。 @@ -1316,7 +1316,7 @@ openviking-server --config /path/to/ov.conf ### ov.conf -本文档上方各配置段(embedding、vlm、rerank、storage)均属于 `ov.conf`。SDK 嵌入模式和服务端共用此文件。 +本文档上方各配置段(embedding、vlm、rerank、storage)均属于服务端的 `ov.conf`。 如需配置 memory 相关行为,可在 `ov.conf` 中添加 `memory` 段: diff --git a/docs/zh/guides/02-volcengine-purchase-guide.md b/docs/zh/guides/02-volcengine-purchase-guide.md index 03ac29efc6..6ec62791bf 100644 --- a/docs/zh/guides/02-volcengine-purchase-guide.md +++ b/docs/zh/guides/02-volcengine-purchase-guide.md @@ -173,11 +173,11 @@ OpenViking 需要以下模型服务: ### 测试连接 ```python -import openviking as ov import asyncio +from openviking_sdk import AsyncHTTPClient async def test(): - client = ov.AsyncOpenViking(path="./test_data") + client = AsyncHTTPClient(url="http://localhost:1933", api_key="your-key") await client.initialize() # 添加简单资源测试 diff --git a/docs/zh/guides/05-observability.md b/docs/zh/guides/05-observability.md index 31c235d90c..a62842d423 100644 --- a/docs/zh/guides/05-observability.md +++ b/docs/zh/guides/05-observability.md @@ -36,7 +36,7 @@ curl http://localhost:1933/health ### 整体系统状态 -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python status = client.get_status() @@ -89,7 +89,7 @@ curl http://localhost:1933/api/v1/observer/queue \ ### 快速健康检查 -**Python SDK (Embedded / HTTP)** +**Python HTTP SDK** ```python if client.is_healthy(): diff --git a/docs/zh/guides/07-operation-telemetry.md b/docs/zh/guides/07-operation-telemetry.md index ef44541e91..7cbe57a0d9 100644 --- a/docs/zh/guides/07-operation-telemetry.md +++ b/docs/zh/guides/07-operation-telemetry.md @@ -332,9 +332,9 @@ curl -X POST http://localhost:1933/api/v1/resources \ ### Python SDK ```python -from openviking import AsyncOpenVikingClient +from openviking_sdk import AsyncHTTPClient -client = AsyncOpenVikingClient(config_path="/path/to/config.yaml") +client = AsyncHTTPClient(url="http://localhost:1933", api_key="your-key") await client.initialize() result = await client.find("memory dedup", telemetry=True) diff --git a/docs/zh/guides/08-encryption.md b/docs/zh/guides/08-encryption.md index 166ee5aeb8..0aefe88f6a 100644 --- a/docs/zh/guides/08-encryption.md +++ b/docs/zh/guides/08-encryption.md @@ -56,13 +56,13 @@ ov system crypto init-key --output-file ~/.openviking/master.key ### 3. 验证 ```python -import openviking as ov import asyncio from pathlib import Path +from openviking_sdk import AsyncHTTPClient async def test(): - client = ov.AsyncOpenViking(path="./data") + client = AsyncHTTPClient(url="http://localhost:1933", api_key="your-key") await client.initialize() # add_resource 接收文件路径或 URL diff --git a/docs/zh/guides/09-ovpack.md b/docs/zh/guides/09-ovpack.md index d26837702b..6005514259 100644 --- a/docs/zh/guides/09-ovpack.md +++ b/docs/zh/guides/09-ovpack.md @@ -141,11 +141,11 @@ ov restore ./backups/openviking.ovpack --on-conflict overwrite ## Python SDK ```python -from openviking import AsyncOpenViking +from openviking_sdk import AsyncHTTPClient async def migrate_project(): - client = AsyncOpenViking() + client = AsyncHTTPClient(url="http://localhost:1933", api_key="your-key") await client.initialize() try: await client.export_ovpack( diff --git a/docs/zh/guides/15-snapshot.md b/docs/zh/guides/15-snapshot.md index aaa512c8c6..5efa4755e6 100644 --- a/docs/zh/guides/15-snapshot.md +++ b/docs/zh/guides/15-snapshot.md @@ -129,9 +129,9 @@ data/ # storage.workspace 快照方法挂在 `client.snapshot.*` 命名空间下。 ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -client = ov.OpenViking() +client = SyncHTTPClient(url="http://localhost:1933", api_key="your-key") client.initialize() root = "viking://resources/my_project" diff --git a/examples/basic-usage/README.md b/examples/basic-usage/README.md index 11f029749a..694ab12cde 100644 --- a/examples/basic-usage/README.md +++ b/examples/basic-usage/README.md @@ -9,8 +9,7 @@ integration, use this example as the foundation and then move to the server and ## What This Example Covers -- Embedded SDK usage for local exploration -- HTTP client usage for server mode +- HTTP SDK usage - Resource ingestion from a remote URL - Filesystem-style access with `ls`, `tree`, and `read` - Retrieval with `find`, `abstract`, `overview`, and `grep` @@ -18,15 +17,14 @@ integration, use this example as the foundation and then move to the server and ## Choose the Right Mode -OpenViking currently has three common integration paths: +OpenViking has two common integration paths: | Mode | Best for | Recommended? | |------|----------|--------------| -| Embedded SDK | Single-process local experimentation | Yes, for first contact | | HTTP server + SDK/CLI | Shared service, multi-session, multi-agent workloads | Yes, preferred for real deployments | | MCP | Claude Code, Cursor, Claude Desktop, OpenClaw, and other MCP hosts | Yes, for tool-based client integration | -If you are building anything beyond a one-process local demo, prefer HTTP server mode over spawning isolated local processes repeatedly. For MCP specifically, follow the dedicated [MCP Integration Guide](../../docs/en/guides/06-mcp-integration.md). +For MCP specifically, follow the dedicated [MCP Integration Guide](../../docs/en/guides/06-mcp-integration.md). ## Prerequisites @@ -34,10 +32,10 @@ If you are building anything beyond a one-process local demo, prefer HTTP server 2. OpenViking installed: ```bash -pip install openviking --upgrade --force-reinstall +pip install openviking-sdk --upgrade ``` -3. A valid config file at `~/.openviking/ov.conf` +3. A running OpenViking server ## Quick Start @@ -49,21 +47,12 @@ cd OpenViking/examples/basic-usage python basic_usage.py ``` -The script uses embedded mode by default: +The script connects to a local OpenViking server: ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -client = ov.OpenViking(path="./data") -client.initialize() -``` - -To point the same flow at a running server instead, switch to: - -```python -import openviking as ov - -client = ov.SyncHTTPClient(url="http://localhost:1933") +client = SyncHTTPClient(url="http://localhost:1933") client.initialize() ``` @@ -86,28 +75,19 @@ See the dedicated [Server Mode Quick Start](../../docs/en/getting-started/03-qui ### Initialization -Use embedded mode for a local first run: - -```python -import openviking as ov - -client = ov.OpenViking(path="./data") -client.initialize() -``` - -Use HTTP client mode when OpenViking runs as a separate service: +Use the HTTP client when OpenViking runs as a separate service: ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -client = ov.SyncHTTPClient(url="http://localhost:1933") +client = SyncHTTPClient(url="http://localhost:1933") client.initialize() ``` If server authentication is enabled, use a `user_key` for normal data access: ```python -client = ov.SyncHTTPClient( +client = SyncHTTPClient( url="http://localhost:1933", api_key="", ) diff --git a/examples/basic-usage/README_CN.md b/examples/basic-usage/README_CN.md index b24dcebddb..1492173967 100644 --- a/examples/basic-usage/README_CN.md +++ b/examples/basic-usage/README_CN.md @@ -9,8 +9,7 @@ ## 这个示例覆盖什么 -- 本地快速试用时的嵌入式 SDK 用法 -- 服务端模式下的 HTTP 客户端用法 +- HTTP SDK 用法 - 从远程 URL 导入资源 - 使用 `ls`、`tree`、`read` 浏览 `viking://` 文件系统 - 使用 `find`、`abstract`、`overview`、`grep` 做检索和加载 @@ -18,15 +17,13 @@ ## 先选对接入方式 -目前 OpenViking 常见有三种接入路径: +目前 OpenViking 常见有两种接入路径: | 模式 | 适合场景 | 是否推荐 | |------|----------|----------| -| 嵌入式 SDK | 单进程、本地试用、快速验证 | 是,适合第一次上手 | | HTTP 服务端 + SDK/CLI | 共享服务、多会话、多 Agent | 是,正式使用优先 | | MCP | Claude Code、Cursor、Claude Desktop、OpenClaw 等 MCP 宿主 | 是,工具化集成优先 | -如果不是单进程本地 demo,而是要长期运行或多端接入,优先使用 HTTP 服务端模式。 如果你是给 Claude Code、Cursor 这类客户端接入,请直接看 [MCP 集成指南](../../docs/zh/guides/06-mcp-integration.md)。 ## 前置条件 @@ -35,10 +32,10 @@ 2. 安装 OpenViking: ```bash -pip install openviking --upgrade --force-reinstall +pip install openviking-sdk --upgrade ``` -3. 准备好 `~/.openviking/ov.conf` +3. 启动 OpenViking Server ## 快速开始 @@ -50,21 +47,12 @@ cd OpenViking/examples/basic-usage python basic_usage.py ``` -脚本默认使用嵌入式模式: +脚本默认连接本地 OpenViking Server: ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -client = ov.OpenViking(path="./data") -client.initialize() -``` - -如果你想把同样的流程切到服务端模式,改成: - -```python -import openviking as ov - -client = ov.SyncHTTPClient(url="http://localhost:1933") +client = SyncHTTPClient(url="http://localhost:1933") client.initialize() ``` @@ -87,28 +75,19 @@ client.initialize() ### 初始化 -本地首次试用建议先用嵌入式模式: - -```python -import openviking as ov - -client = ov.OpenViking(path="./data") -client.initialize() -``` - -如果 OpenViking 作为独立服务运行,则使用 HTTP 客户端: +使用 HTTP 客户端连接 OpenViking Server: ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -client = ov.SyncHTTPClient(url="http://localhost:1933") +client = SyncHTTPClient(url="http://localhost:1933") client.initialize() ``` 如果服务端启用了认证,普通数据访问请优先使用 `user_key`: ```python -client = ov.SyncHTTPClient( +client = SyncHTTPClient( url="http://localhost:1933", api_key="", ) diff --git a/examples/basic-usage/basic_usage.py b/examples/basic-usage/basic_usage.py index 21af4ffc25..6bba82c4a6 100644 --- a/examples/basic-usage/basic_usage.py +++ b/examples/basic-usage/basic_usage.py @@ -3,7 +3,7 @@ OpenViking Basic Usage Example This script demonstrates the core features of OpenViking: -1. Initialization (embedded mode and HTTP client mode) +1. HTTP client initialization 2. Adding resources (URLs, files, directories) 3. Browsing the virtual filesystem 4. Semantic search and retrieval @@ -12,7 +12,7 @@ Requirements: - pip install openviking --upgrade -- Configuration file at ~/.openviking/ov.conf +- A running OpenViking server at http://localhost:1933 """ import os @@ -35,18 +35,13 @@ def main(): print("-" * 40) try: - import openviking as ov + from openviking_sdk import SyncHTTPClient except ImportError as e: - print(f" Error: Failed to import openviking: {e}") - print(" Please install: pip install openviking --upgrade") + print(f" Error: Failed to import openviking_sdk: {e}") + print(" Please install: pip install openviking-sdk --upgrade") sys.exit(1) - # Embedded mode (local development) - # Option A: Embedded mode with local path - client = ov.OpenViking(path="./data") - - # Option B: HTTP client mode (connect to remote server) - # client = ov.SyncHTTPClient(url="http://localhost:1933") + client = SyncHTTPClient(url="http://localhost:1933") try: client.initialize() @@ -60,7 +55,7 @@ def main(): except Exception as e: print(f" Error during initialization: {e}") - print(" Make sure you have configured ~/.openviking/ov.conf") + print(" Make sure the OpenViking server is running") sys.exit(1) print() @@ -193,10 +188,11 @@ def main(): results = client.find(query=query, target_uri=root_uri, limit=5) - if hasattr(results, "resources") and results.resources: - for r in results.resources: - print(f" - {r.uri}") - print(f" Score: {r.score:.4f}") + resources = results.get("resources", []) + if resources: + for resource in resources: + print(f" - {resource['uri']}") + print(f" Score: {resource.get('score', 0.0):.4f}") else: print(" No results found") diff --git a/examples/common/boring_logging_config.py b/examples/common/boring_logging_config.py index 33f19d1828..220e530838 100644 --- a/examples/common/boring_logging_config.py +++ b/examples/common/boring_logging_config.py @@ -85,7 +85,7 @@ "handlers": ["null"], "propagate": False, }, - "openviking.async_client": { + "openviking_sdk.client": { "level": "CRITICAL", "handlers": ["null"], "propagate": False, diff --git a/examples/common/recipe.py b/examples/common/recipe.py index 8a1f9b539b..4f6d9f5211 100644 --- a/examples/common/recipe.py +++ b/examples/common/recipe.py @@ -9,9 +9,7 @@ from typing import Any, Dict, List, Optional import requests - -import openviking as ov -from openviking_cli.utils.config.open_viking_config import OpenVikingConfig +from openviking_sdk import SyncHTTPClient class Recipe: @@ -24,13 +22,17 @@ class Recipe: 3. Return generated answer with sources """ - def __init__(self, config_path: str = "./ov.conf", data_path: str = "./data"): + def __init__( + self, + config_path: str = "./ov.conf", + server_url: str = "http://127.0.0.1:1933", + ): """ Initialize RAG pipeline Args: config_path: Path to config file with LLM settings - data_path: Path to OpenViking data directory + server_url: OpenViking HTTP server URL """ # Load configuration with open(config_path, "r") as f: @@ -43,8 +45,7 @@ def __init__(self, config_path: str = "./ov.conf", data_path: str = "./data"): self.model = self.vlm_config.get("model") # Initialize OpenViking client - config = OpenVikingConfig.from_dict(self.config_dict) - self.client = ov.SyncOpenViking(path=data_path, config=config) + self.client = SyncHTTPClient(url=server_url) self.client.initialize() def search( @@ -74,33 +75,35 @@ def search( # Extract top results search_results = [] - for _i, resource in enumerate( - results.resources[:top_k] + results.memories[:top_k] - ): # ignore SKILLs for mvp + resources = results.get("resources", [])[:top_k] + memories = results.get("memories", [])[:top_k] + for _i, resource in enumerate(resources + memories): # ignore SKILLs for mvp + uri = resource["uri"] + score = resource.get("score", 0.0) try: - content = self.client.read(resource.uri) + content = self.client.read(uri) search_results.append( { - "uri": resource.uri, - "score": resource.score, + "uri": uri, + "score": score, "content": content, } ) - # print(f" {i + 1}. {resource.uri} (score: {resource.score:.4f})") + # print(f" {i + 1}. {uri} (score: {score:.4f})") except Exception as e: # Handle directories - read their abstract instead if "is a directory" in str(e): try: - abstract = self.client.abstract(resource.uri) + abstract = self.client.abstract(uri) search_results.append( { - "uri": resource.uri, - "score": resource.score, + "uri": uri, + "score": score, "content": f"[Directory Abstract] {abstract}", } ) - # print(f" {i + 1}. {resource.uri} (score: {resource.score:.4f}) [directory]") + # print(f" {i + 1}. {uri} (score: {score:.4f}) [directory]") except: # Skip if we can't get abstract continue diff --git a/examples/common/resource_manager.py b/examples/common/resource_manager.py index 03bfc2a4f1..95d1cd8eb6 100644 --- a/examples/common/resource_manager.py +++ b/examples/common/resource_manager.py @@ -3,39 +3,31 @@ Resource Manager - Shared utilities for adding resources to OpenViking """ -import json from pathlib import Path from typing import Optional +from openviking_sdk import SyncHTTPClient from rich.console import Console -import openviking as ov -from openviking_cli.utils.config.open_viking_config import OpenVikingConfig - -def create_client(config_path: str = "./ov.conf", data_path: str = "./data") -> ov.SyncOpenViking: +def create_client(server_url: str = "http://127.0.0.1:1933") -> SyncHTTPClient: """ Create and initialize OpenViking client Args: - config_path: Path to config file - data_path: Path to data directory + server_url: OpenViking HTTP server URL Returns: - Initialized SyncOpenViking client + Initialized HTTP client """ - with open(config_path, "r") as f: - config_dict = json.load(f) - - config = OpenVikingConfig.from_dict(config_dict) - client = ov.SyncOpenViking(path=data_path, config=config) + client = SyncHTTPClient(url=server_url) client.initialize() return client def add_resource( - client: ov.SyncOpenViking, + client: SyncHTTPClient, resource_path: str, console: Optional[Console] = None, show_output: bool = True, @@ -44,7 +36,7 @@ def add_resource( Add a resource to OpenViking database Args: - client: Initialized SyncOpenViking client + client: Initialized HTTP client resource_path: Path to file/directory or URL console: Rich Console for output (creates new if None) show_output: Whether to print status messages diff --git a/examples/k8s-helm/README.md b/examples/k8s-helm/README.md index 41d53ed63f..8eb48c16bb 100644 --- a/examples/k8s-helm/README.md +++ b/examples/k8s-helm/README.md @@ -208,12 +208,12 @@ openviking health ### Python Client ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient # Get service endpoint # kubectl get svc openviking -client = ov.OpenViking(url="http://:1933", api_key="your-key") +client = SyncHTTPClient(url="http://:1933", api_key="your-key") client.initialize() # Add a resource diff --git a/examples/k8s-helm/README_CN.md b/examples/k8s-helm/README_CN.md index 776a61beb3..ca1f7f5e69 100644 --- a/examples/k8s-helm/README_CN.md +++ b/examples/k8s-helm/README_CN.md @@ -208,12 +208,12 @@ openviking health ### Python 客户端 ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient # 获取服务端点 # kubectl get svc openviking -client = ov.OpenViking(url="http://:1933", api_key="your-key") +client = SyncHTTPClient(url="http://:1933", api_key="your-key") client.initialize() # 添加资源 @@ -269,4 +269,4 @@ kubectl delete pvc openviking-data ## 许可证 -此 Helm Chart 采用 Apache License 2.0 许可证,与 OpenViking 项目许可证一致。 \ No newline at end of file +此 Helm Chart 采用 Apache License 2.0 许可证,与 OpenViking 项目许可证一致。 diff --git a/examples/quick_start.py b/examples/quick_start.py index 4fcb8da40e..8ccb932d9d 100644 --- a/examples/quick_start.py +++ b/examples/quick_start.py @@ -1,7 +1,6 @@ -import openviking as ov +from openviking_sdk import SyncHTTPClient -client = ov.OpenViking(path="./data") -# client = ov.SyncHTTPClient(url="http://localhost:1933") # HTTP mode: connect to OpenViking Server +client = SyncHTTPClient(url="http://localhost:1933") try: client.initialize() @@ -27,8 +26,8 @@ results = client.find("what is openviking", target_uri=root_uri) # Semantic search print("Search results:") - for r in results.resources: - print(f" {r.uri} (score: {r.score:.4f})") + for result in results.get("resources", []): + print(f" {result['uri']} (score: {result.get('score', 0.0):.4f})") client.close() diff --git a/examples/snapshot/snapshot_example.py b/examples/snapshot/snapshot_example.py index 968ff694ea..ac20aedcb3 100644 --- a/examples/snapshot/snapshot_example.py +++ b/examples/snapshot/snapshot_example.py @@ -1,12 +1,10 @@ from __future__ import annotations -import os import time import uuid -from pathlib import Path from typing import Any -OV_CONFIG_FILE = "/home/byteide/.openviking/ov.conf" +OPENVIKING_URL = "http://127.0.0.1:1933" WORKSPACE_URI = "viking://resources/snapshot_sdk_demo" WAIT_TIMEOUT = 180.0 @@ -45,12 +43,13 @@ def remove_resource(client: Any, uri: str) -> None: def print_find(client: Any, query: str, root_uri: str) -> None: results = client.find(query, target_uri=root_uri, limit=10) - if not results.resources: + resources = results.get("resources", []) + if not resources: print(f"find {query!r}: (no matches)") return - print(f"find {query!r}: {len(results.resources)} match(es)") - for r in results.resources: - print(f" {r.uri} (score: {r.score:.4f})") + print(f"find {query!r}: {len(resources)} match(es)") + for resource in resources: + print(f" {resource['uri']} (score: {resource.get('score', 0.0):.4f})") def print_read(client: Any, uri: str) -> None: @@ -61,7 +60,9 @@ def print_read(client: Any, uri: str) -> None: def commit_snapshot(client: Any, message: str, paths: list[str] | None = None) -> dict[str, Any]: result = client.snapshot.commit(message=message, paths=paths) - print(f"commit {message!r}: result={result.get('result')} oid={short_oid(result.get('commit_oid'))}") + print( + f"commit {message!r}: result={result.get('result')} oid={short_oid(result.get('commit_oid'))}" + ) return result @@ -95,9 +96,7 @@ def wait_for_task( def main() -> None: - os.environ["OPENVIKING_CONFIG_FILE"] = str(Path(OV_CONFIG_FILE).resolve()) - - import openviking as ov + from openviking_sdk import SyncHTTPClient run_id, root_uri = unique_run_uri() uris = resource_uris(root_uri) @@ -108,26 +107,32 @@ def main() -> None: gamma = f"gamma_{run_id}" archive = f"archive_{run_id}" - client = ov.OpenViking(path="./data") + client = SyncHTTPClient(url=OPENVIKING_URL) client.initialize() try: print_section("setup") - print(f"config: {Path(OV_CONFIG_FILE).resolve()}") + print(f"server: {OPENVIKING_URL}") print(f"workspace: {root_uri}") client.mkdir(root_uri) client.mkdir(f"{root_uri}/notes") print(f"mkdir: {root_uri}, {root_uri}/notes") print_section("v1 initial import") - write_text(client, uris["guide"], f"# Guide\n\nInitial SDK content with {alpha}.\n", mode="create") + write_text( + client, uris["guide"], f"# Guide\n\nInitial SDK content with {alpha}.\n", mode="create" + ) write_text(client, uris["todo"], f"# Todo\n\nRemember {todo}.\n", mode="create") v1 = commit_snapshot(client, "sdk v1 initial import", paths=[root_uri]) print_find(client, alpha, root_uri) print_section("v2 modify delete add") - write_text(client, uris["guide"], f"# Guide\n\nUpdated SDK content with {beta}.\n", mode="replace") + write_text( + client, uris["guide"], f"# Guide\n\nUpdated SDK content with {beta}.\n", mode="replace" + ) remove_resource(client, uris["todo"]) - write_text(client, uris["changelog"], f"# Changelog\n\nCreated {changelog}.\n", mode="create") + write_text( + client, uris["changelog"], f"# Changelog\n\nCreated {changelog}.\n", mode="create" + ) v2 = commit_snapshot(client, "sdk v2 modify delete add", paths=[root_uri]) print_find(client, beta, root_uri) print_find(client, todo, root_uri) @@ -136,8 +141,15 @@ def main() -> None: print_section("v3 second changes") client.mkdir(f"{root_uri}/archive") print(f"mkdir: {root_uri}/archive") - write_text(client, uris["changelog"], f"# Changelog\n\nCreated {changelog}. Added {gamma}.\n", mode="replace") - write_text(client, uris["archive"], f"# Archive\n\nArchived marker {archive}.\n", mode="create") + write_text( + client, + uris["changelog"], + f"# Changelog\n\nCreated {changelog}. Added {gamma}.\n", + mode="replace", + ) + write_text( + client, uris["archive"], f"# Archive\n\nArchived marker {archive}.\n", mode="create" + ) v3 = commit_snapshot(client, "sdk v3 second changes", paths=[root_uri]) print_find(client, gamma, root_uri) print_find(client, archive, root_uri) @@ -147,7 +159,9 @@ def main() -> None: print(f" {short_oid(commit.get('oid'))} {commit.get('message', '')}") for label, snap in (("v1", v1), ("v2", v2), ("v3", v3)): meta = client.snapshot.show(snap["commit_oid"]) - print(f"snapshot show {label}: oid={short_oid(meta.get('oid'))} message={meta.get('message', '')!r}") + print( + f"snapshot show {label}: oid={short_oid(meta.get('oid'))} message={meta.get('message', '')!r}" + ) print_section("restore to v1") restore = client.snapshot.restore( diff --git a/examples/watch_resource_example.py b/examples/watch_resource_example.py index fdf5c31b55..88f945e7d8 100644 --- a/examples/watch_resource_example.py +++ b/examples/watch_resource_example.py @@ -18,12 +18,11 @@ import asyncio from pathlib import Path -from openviking import AsyncOpenViking -from openviking_cli.exceptions import ConflictError +from openviking_sdk import AsyncHTTPClient, ConflictError async def example_basic_watch(): - client = AsyncOpenViking(path="./data_watch_example") + client = AsyncHTTPClient(url="http://localhost:1933") await client.initialize() try: @@ -57,7 +56,7 @@ async def example_basic_watch(): async def example_update_watch_interval(): - client = AsyncOpenViking(path="./data_watch_example") + client = AsyncHTTPClient(url="http://localhost:1933") await client.initialize() try: @@ -82,7 +81,7 @@ async def example_update_watch_interval(): async def example_cancel_watch(): - client = AsyncOpenViking(path="./data_watch_example") + client = AsyncHTTPClient(url="http://localhost:1933") await client.initialize() try: @@ -101,7 +100,7 @@ async def example_cancel_watch(): async def example_handle_conflict(): - client = AsyncOpenViking(path="./data_watch_example") + client = AsyncHTTPClient(url="http://localhost:1933") await client.initialize() try: @@ -148,4 +147,3 @@ async def main(): if __name__ == "__main__": asyncio.run(main()) - diff --git a/integrations/langchain/README.md b/integrations/langchain/README.md index a87e4e68fa..854204957a 100644 --- a/integrations/langchain/README.md +++ b/integrations/langchain/README.md @@ -50,8 +50,6 @@ The package also provides `OpenVikingSessionRecorder`, - A client supplied through `client=` or `async_client=` remains caller-owned. - Clients created from `url=` are managed by the adapter and can be closed with `close()` or `aclose()` as documented by each adapter. -- Embedded `path=` mode requires the full `openviking` package. It is kept for - compatibility, while the standalone package's default boundary is HTTP. The previous `openviking.integrations.langchain` import path remains available from the full `openviking` distribution as a compatibility shim. diff --git a/integrations/langchain/README_CN.md b/integrations/langchain/README_CN.md index bc9897c1e6..da04dffe23 100644 --- a/integrations/langchain/README_CN.md +++ b/integrations/langchain/README_CN.md @@ -41,7 +41,6 @@ finally: ``` 外部传入的 client 仍由调用方管理。通过 `url=` 创建的 client 由适配器管理。 -`path=` 嵌入模式需要另外安装完整的 `openviking` 包。 完整 `openviking` 包会继续保留原有的 `openviking.integrations.langchain` 导入路径,并转发到本包,方便现有应用平滑迁移。 diff --git a/integrations/langchain/src/langchain_openviking/client.py b/integrations/langchain/src/langchain_openviking/client.py index 3c58f9fcc7..f54fb0d1e0 100644 --- a/integrations/langchain/src/langchain_openviking/client.py +++ b/integrations/langchain/src/langchain_openviking/client.py @@ -11,8 +11,7 @@ import logging import threading from dataclasses import dataclass -from importlib import import_module -from typing import Any, Callable, Iterable, Literal +from typing import Any, Iterable, Literal from langchain_openviking._async_client_cache import LoopScopedAsyncClientCache @@ -74,7 +73,6 @@ class OpenVikingConnection: user: str | None = None user_id: str | None = None actor_peer_id: str | None = None - path: str | None = None timeout: float = 60.0 extra_headers: dict[str, str] | None = None auto_initialize: bool = True @@ -92,7 +90,6 @@ def __deepcopy__(self, memo: dict[int, Any]) -> OpenVikingConnection: user=self.user, user_id=self.user_id, actor_peer_id=self.actor_peer_id, - path=self.path, timeout=self.timeout, extra_headers=copy.deepcopy(self.extra_headers, memo), auto_initialize=self.auto_initialize, @@ -121,7 +118,7 @@ def __init__(self, connection: OpenVikingConnection): def supports_request_actor_peer(self) -> bool: """Return whether this handle supports request-scoped actor peers.""" - return _uses_http_client(self._connection) + return True @property def _initialized(self) -> bool: @@ -223,7 +220,7 @@ def __init__(self, connection: OpenVikingConnection): def supports_request_actor_peer(self) -> bool: """Return whether this handle supports request-scoped actor peers.""" - return _uses_http_client(self._connection) + return True @property def _initialized(self) -> bool: @@ -368,12 +365,10 @@ def ensure_client(connection: OpenVikingConnection) -> Any: client = connection.client if client is None: - if connection.url or connection.path is None: - handle = OpenVikingClientHandle(connection) - if connection.auto_initialize: - handle.get() - return handle - return _create_client_from_connection(connection) + handle = OpenVikingClientHandle(connection) + if connection.auto_initialize: + handle.get() + return handle if connection.auto_initialize and hasattr(client, "initialize"): if not getattr(client, "_initialized", False): client.initialize() @@ -384,40 +379,26 @@ async def ensure_async_client( connection: OpenVikingConnection, *, client_cache: LoopScopedAsyncClientCache | None = None, - embedded_client_factory: Callable[[], Any] | None = None, ) -> Any: """Return a client suitable for non-blocking OpenViking calls. An explicitly supplied async client is preferred. Existing synchronous clients remain supported and are dispatched through a worker thread by :func:`acall_openviking`. Internally created HTTP handles can be scoped to - the running loop with ``client_cache``. Embedded ``path=`` connections - intentionally use the synchronous client so their stateful async internals - remain on OpenViking's shared background loop. + the running loop with ``client_cache``. """ client = connection.async_client if client is None and connection.client is not None: client = connection.client if client is None: - if _uses_http_client(connection): - if client_cache is None: - handle = OpenVikingAsyncClientHandle(connection) - else: - handle = client_cache.get(lambda: OpenVikingAsyncClientHandle(connection)) - if connection.auto_initialize: - await handle.get() - return handle - if embedded_client_factory is not None: - if client_cache is not None: - # The thread hop is load-bearing: without a running event loop, - # the embedded singleton occupies the cache's shared fallback slot. - return await asyncio.to_thread( - client_cache.get, - embedded_client_factory, - ) - return await asyncio.to_thread(embedded_client_factory) - return await asyncio.to_thread(_create_client_from_connection, connection) + if client_cache is None: + handle = OpenVikingAsyncClientHandle(connection) + else: + handle = client_cache.get(lambda: OpenVikingAsyncClientHandle(connection)) + if connection.auto_initialize: + await handle.get() + return handle if connection.auto_initialize and hasattr(client, "initialize"): await _ainitialize_client(client) return client @@ -563,32 +544,18 @@ async def acall_openviking(client: Any, method_name: str, /, **kwargs: Any) -> A def _create_client_from_connection(connection: OpenVikingConnection) -> Any: - client: Any - if connection.url or connection.path is None: - from openviking_sdk import SyncHTTPClient - - client = SyncHTTPClient( - url=connection.url, - api_key=connection.api_key, - account=connection.account, - user=connection.user, - user_id=connection.user_id, - actor_peer_id=connection.actor_peer_id, - timeout=connection.timeout, - extra_headers=connection.extra_headers, - ) - else: - try: - openviking = import_module("openviking") - sync_openviking = openviking.SyncOpenViking - except ImportError as exc: - raise OptionalDependencyError( - "Embedded path= connections require the full openviking package. " - 'Install it with `pip install "openviking"`, or configure an ' - "OpenViking server URL for lightweight HTTP usage." - ) from exc + from openviking_sdk import SyncHTTPClient - client = sync_openviking(path=connection.path, actor_peer_id=connection.actor_peer_id) + client: Any = SyncHTTPClient( + url=connection.url, + api_key=connection.api_key, + account=connection.account, + user=connection.user, + user_id=connection.user_id, + actor_peer_id=connection.actor_peer_id, + timeout=connection.timeout, + extra_headers=connection.extra_headers, + ) if connection.auto_initialize and hasattr(client, "initialize"): if not getattr(client, "_initialized", False): @@ -597,9 +564,6 @@ def _create_client_from_connection(connection: OpenVikingConnection) -> Any: async def _create_async_client_from_connection(connection: OpenVikingConnection) -> Any: - if not _uses_http_client(connection): - raise ValueError("Native async clients are created automatically only for HTTP connections") - from openviking_sdk import AsyncHTTPClient client: Any = AsyncHTTPClient( @@ -720,12 +684,6 @@ def _async_client_initialization_lock(client: Any) -> asyncio.Lock: return locks.get(asyncio.Lock) -def _uses_http_client(connection: OpenVikingConnection) -> bool: - """Return whether connection settings select the HTTP transport.""" - - return bool(connection.url) or connection.path is None - - def _async_client_is_initialized(client: Any) -> bool: for name in ("_initialized", "_http"): try: diff --git a/integrations/langchain/src/langchain_openviking/context.py b/integrations/langchain/src/langchain_openviking/context.py index b06e949582..8959da9ec4 100644 --- a/integrations/langchain/src/langchain_openviking/context.py +++ b/integrations/langchain/src/langchain_openviking/context.py @@ -179,7 +179,6 @@ def __init__( user: str | None = None, user_id: str | None = None, actor_peer_id: str | None = None, - path: str | None = None, timeout: float = 60.0, extra_headers: dict[str, str] | None = None, auto_initialize: bool = True, @@ -201,7 +200,6 @@ def __init__( user=user, user_id=user_id, actor_peer_id=actor_peer_id, - path=path, timeout=timeout, extra_headers=extra_headers, auto_initialize=auto_initialize, @@ -215,7 +213,6 @@ def __init__( user=user, user_id=user_id, actor_peer_id=actor_peer_id, - path=path, timeout=timeout, extra_headers=extra_headers, auto_initialize=auto_initialize, @@ -327,15 +324,12 @@ async def get_async_client(self) -> Any: whose method calls support recovery. Direct attributes on that handle are best-effort during recovery; use ``await handle.get()`` only to read raw properties immediately because recovery may replace that snapshot. - Embedded ``path=`` connections return an adapter-owned synchronous - client whose calls are dispatched through a worker thread. """ self._raise_if_closed() client = await ensure_async_client( self._connection, client_cache=self._async_clients, - embedded_client_factory=self._get_client, ) self._raise_if_closed() return client @@ -632,7 +626,6 @@ def with_openviking_context( account: str | None = None, user: str | None = None, user_id: str | None = None, - path: str | None = None, timeout: float = 60.0, extra_headers: dict[str, str] | None = None, auto_initialize: bool = True, @@ -666,7 +659,6 @@ def with_openviking_context( user=user, user_id=user_id, actor_peer_id=actor_peer_id, - path=path, timeout=timeout, extra_headers=extra_headers, auto_initialize=auto_initialize, @@ -686,7 +678,6 @@ def with_openviking_context( user=user, user_id=user_id, actor_peer_id=actor_peer_id, - path=path, timeout=timeout, extra_headers=extra_headers, auto_initialize=auto_initialize, diff --git a/integrations/langchain/src/langchain_openviking/history.py b/integrations/langchain/src/langchain_openviking/history.py index 8af1fb8f79..38a8a0dab9 100644 --- a/integrations/langchain/src/langchain_openviking/history.py +++ b/integrations/langchain/src/langchain_openviking/history.py @@ -56,7 +56,6 @@ def __init__( user: str | None = None, user_id: str | None = None, actor_peer_id: str | None = None, - path: str | None = None, timeout: float = 60.0, extra_headers: dict[str, str] | None = None, auto_initialize: bool = True, @@ -89,7 +88,6 @@ def __init__( user=user, user_id=user_id, actor_peer_id=actor_peer_id, - path=path, timeout=timeout, extra_headers=extra_headers, auto_initialize=auto_initialize, diff --git a/integrations/langchain/src/langchain_openviking/middleware.py b/integrations/langchain/src/langchain_openviking/middleware.py index 52eacd3bf7..ca57bb650e 100644 --- a/integrations/langchain/src/langchain_openviking/middleware.py +++ b/integrations/langchain/src/langchain_openviking/middleware.py @@ -92,7 +92,6 @@ def __init__( user: str | None = None, user_id: str | None = None, actor_peer_id: str | None = None, - path: str | None = None, target_uri: str | list[str] = "", limit: int = 5, peer_id: str | None = None, @@ -113,7 +112,6 @@ def __init__( client=client, async_client=async_client, retriever=retriever, - path=path, ) self.recorder = OpenVikingSessionRecorder( client=client, @@ -124,7 +122,6 @@ def __init__( user=user, user_id=user_id, actor_peer_id=actor_peer_id, - path=path, commit_policy=None, ) self._owns_retriever = retriever is None @@ -137,7 +134,6 @@ def __init__( user=user, user_id=user_id, actor_peer_id=actor_peer_id, - path=path, target_uri=target_uri, limit=limit, score_threshold=score_threshold, @@ -153,7 +149,6 @@ def __init__( user=user, user_id=user_id, actor_peer_id=actor_peer_id, - path=path, target_uri=target_uri, limit=limit, score_threshold=score_threshold, @@ -497,13 +492,10 @@ def _validate_actor_peer_transport( client: Any, async_client: Any, retriever: OpenVikingRetriever | None, - path: str | None, ) -> None: if actor_peer_resolver is None: return require_request_actor_peer_support() - if path is not None or (retriever is not None and retriever.path is not None): - raise ValueError("actor_peer_resolver requires an OpenViking HTTP connection") transports = [client, async_client] if retriever is not None: transports.extend([retriever.client, retriever.async_client]) diff --git a/integrations/langchain/src/langchain_openviking/recording.py b/integrations/langchain/src/langchain_openviking/recording.py index 21c92f0e3d..b146cb069b 100644 --- a/integrations/langchain/src/langchain_openviking/recording.py +++ b/integrations/langchain/src/langchain_openviking/recording.py @@ -202,7 +202,6 @@ def __init__( user: str | None = None, user_id: str | None = None, actor_peer_id: str | None = None, - path: str | None = None, timeout: float = 60.0, extra_headers: dict[str, str] | None = None, auto_initialize: bool = True, @@ -220,7 +219,6 @@ def __init__( user=user, user_id=user_id, actor_peer_id=actor_peer_id, - path=path, timeout=timeout, extra_headers=extra_headers, auto_initialize=auto_initialize, @@ -545,15 +543,12 @@ async def get_async_client(self) -> Any: whose method calls support recovery. Direct attributes on that handle are best-effort during recovery; use ``await handle.get()`` only to read raw properties immediately because recovery may replace that snapshot. - Embedded ``path=`` connections return an adapter-owned synchronous - client whose calls are dispatched through a worker thread. """ self._raise_if_closed() client = await ensure_async_client( self._connection, client_cache=self._async_clients, - embedded_client_factory=lambda: self.client, ) self._raise_if_closed() return client @@ -572,14 +567,11 @@ def close(self) -> None: if self._closed: return - uses_http_client = bool(self._connection.url) or self._connection.path is None - if uses_http_client and self._async_clients.has_clients(): + if self._async_clients.has_clients(): raise RuntimeError( "OpenVikingSessionRecorder has an active async client; " "use `await recorder.aclose()`" ) - if not uses_http_client: - self._async_clients.pop_all() self._closed = True self._clear_pending_commits() with self._client_cache_lock: diff --git a/integrations/langchain/src/langchain_openviking/retrievers.py b/integrations/langchain/src/langchain_openviking/retrievers.py index 06d127cbdb..f961d01aea 100644 --- a/integrations/langchain/src/langchain_openviking/retrievers.py +++ b/integrations/langchain/src/langchain_openviking/retrievers.py @@ -74,7 +74,6 @@ class OpenVikingRetriever(BaseRetriever): user: str | None = None user_id: str | None = None actor_peer_id: str | None = None - path: str | None = None timeout: float = 60.0 extra_headers: dict[str, str] | None = None auto_initialize: bool = True @@ -132,7 +131,6 @@ def _get_client(self) -> Any: user=self.user, user_id=self.user_id, actor_peer_id=self.actor_peer_id, - path=self.path, timeout=self.timeout, extra_headers=self.extra_headers, auto_initialize=self.auto_initialize, @@ -149,8 +147,6 @@ async def get_async_client(self) -> Any: whose method calls support recovery. Direct attributes on that handle are best-effort during recovery; use ``await handle.get()`` only to read raw properties immediately because recovery may replace that snapshot. - Embedded ``path=`` connections return an adapter-owned synchronous - client whose calls are dispatched through a worker thread. """ self._raise_if_closed() @@ -164,13 +160,11 @@ async def get_async_client(self) -> Any: user=self.user, user_id=self.user_id, actor_peer_id=self.actor_peer_id, - path=self.path, timeout=self.timeout, extra_headers=self.extra_headers, auto_initialize=self.auto_initialize, ), client_cache=self._async_clients, - embedded_client_factory=self._get_client, ) self._raise_if_closed() return client diff --git a/integrations/langchain/src/langchain_openviking/store.py b/integrations/langchain/src/langchain_openviking/store.py index 00f231421b..bc02f4915b 100644 --- a/integrations/langchain/src/langchain_openviking/store.py +++ b/integrations/langchain/src/langchain_openviking/store.py @@ -71,7 +71,6 @@ def __init__( user: str | None = None, user_id: str | None = None, actor_peer_id: str | None = None, - path: str | None = None, root_uri: str = "viking://user/memories/langgraph_store", index: bool | list[str] | None = None, wait: bool = True, @@ -90,7 +89,6 @@ def __init__( user=user, user_id=user_id, actor_peer_id=actor_peer_id, - path=path, auto_initialize=auto_initialize, ) self.root_uri: str = root_uri.rstrip("/") diff --git a/integrations/langchain/src/langchain_openviking/tools.py b/integrations/langchain/src/langchain_openviking/tools.py index 9fad578bd3..6fad3f54ac 100644 --- a/integrations/langchain/src/langchain_openviking/tools.py +++ b/integrations/langchain/src/langchain_openviking/tools.py @@ -53,7 +53,6 @@ def create_openviking_tools( user: str | None = None, user_id: str | None = None, actor_peer_id: str | None = None, - path: str | None = None, timeout: float = 60.0, extra_headers: dict[str, str] | None = None, auto_initialize: bool = True, @@ -83,7 +82,6 @@ def get_client() -> Any: user=user, user_id=user_id, actor_peer_id=actor_peer_id, - path=path, timeout=timeout, extra_headers=extra_headers, auto_initialize=auto_initialize, diff --git a/openviking/__init__.py b/openviking/__init__.py index f906a9d306..99a81db76c 100644 --- a/openviking/__init__.py +++ b/openviking/__init__.py @@ -6,8 +6,6 @@ Data in, Context out. """ -from typing import TYPE_CHECKING - try: from ._version import version as __version__ except ImportError: @@ -18,42 +16,8 @@ except ImportError: __version__ = "0.0.0+unknown" -try: - from openviking.pyagfs import get_binding_client -except ImportError as exc: - raise ImportError( - "Bundled OpenViking AGFS client is unavailable. " - "Reinstall openviking or run 'pip install -e .' from the project root." - ) from exc - -if TYPE_CHECKING: - from openviking.async_client import AsyncOpenViking - from openviking.session import Session - from openviking.sync_client import SyncOpenViking - from openviking_cli.client.http import AsyncHTTPClient - from openviking_cli.client.sync_http import SyncHTTPClient - from openviking_cli.session.user_id import UserIdentifier - - OpenViking = SyncOpenViking - def __getattr__(name: str): - if name == "AsyncOpenViking": - from openviking.async_client import AsyncOpenViking - - return AsyncOpenViking - if name == "SyncOpenViking": - from openviking.sync_client import SyncOpenViking - - return SyncOpenViking - if name == "OpenViking": - from openviking.sync_client import SyncOpenViking - - return SyncOpenViking - if name == "Session": - from openviking.session import Session - - return Session if name == "AsyncHTTPClient": from openviking_cli.client.http import AsyncHTTPClient @@ -62,19 +26,10 @@ def __getattr__(name: str): from openviking_cli.client.sync_http import SyncHTTPClient return SyncHTTPClient - if name == "UserIdentifier": - from openviking_cli.session.user_id import UserIdentifier - - return UserIdentifier raise AttributeError(name) __all__ = [ - "OpenViking", - "SyncOpenViking", - "AsyncOpenViking", "SyncHTTPClient", "AsyncHTTPClient", - "Session", - "UserIdentifier", ] diff --git a/openviking/async_client.py b/openviking/async_client.py deleted file mode 100644 index 1d98c40af9..0000000000 --- a/openviking/async_client.py +++ /dev/null @@ -1,959 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 -""" -Async OpenViking client implementation (embedded mode only). - -For HTTP mode, use AsyncHTTPClient or SyncHTTPClient. -""" - -from __future__ import annotations - -import threading -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union - -from openviking.client import LocalClient, Session -from openviking.service.debug_service import SystemStatus -from openviking.telemetry import TelemetryRequest -from openviking.utils.search_filters import SearchContextTypeInput -from openviking_cli.client.base import BaseClient -from openviking_cli.session.user_id import UserIdentifier -from openviking_cli.utils import get_logger - -if TYPE_CHECKING: - from openviking.snapshot_namespace import AsyncSnapshotNamespace - -logger = get_logger(__name__) - -if TYPE_CHECKING: - from openviking.snapshot_namespace import AsyncSnapshotNamespace - - -class AsyncOpenViking: - """ - OpenViking main client class (Asynchronous, embedded mode only). - - Uses local storage and auto-starts services (singleton). - For HTTP mode, use AsyncHTTPClient or SyncHTTPClient instead. - - Examples: - client = AsyncOpenViking(path="./data") - await client.initialize() - """ - - _instance: Optional["AsyncOpenViking"] = None - _lock = threading.Lock() - - def __new__(cls, *args, **kwargs): - if cls._instance is None: - with cls._lock: - if cls._instance is None: - cls._instance = object.__new__(cls) - return cls._instance - - def __init__( - self, - path: Optional[str] = None, - actor_peer_id: Optional[str] = None, - agent_id: Optional[str] = None, - ): - """ - Initialize OpenViking client (embedded mode). - - Args: - path: Local storage path (overrides ov.conf storage path). - actor_peer_id: Optional view filter for the current user's peer collection. - agent_id: Legacy alias for actor_peer_id. - """ - # Singleton guard for repeated initialization - if hasattr(self, "_singleton_initialized") and self._singleton_initialized: - return - - self.user = UserIdentifier.the_default_user() - self._initialized = False - self._snapshot: Optional["AsyncSnapshotNamespace"] = None - # Mark initialized only after LocalClient is successfully constructed. - self._singleton_initialized = False - - self._client: BaseClient = LocalClient( - path=path, - actor_peer_id=actor_peer_id, - agent_id=agent_id, - ) - self._singleton_initialized = True - - # ============= Lifecycle methods ============= - - async def initialize(self) -> None: - """Initialize OpenViking storage and indexes.""" - await self._client.initialize() - self._initialized = True - - async def _ensure_initialized(self): - """Ensure storage collections are initialized.""" - if not self._initialized: - await self.initialize() - - async def close(self) -> None: - """Close OpenViking and release resources.""" - client = getattr(self, "_client", None) - if client is not None: - await client.close() - self._initialized = False - self._singleton_initialized = False - - @classmethod - async def reset(cls) -> None: - """Reset the singleton instance (mainly for testing).""" - with cls._lock: - if cls._instance is not None: - await cls._instance.close() - cls._instance = None - - # ============= Session methods ============= - - def session(self, session_id: Optional[str] = None, must_exist: bool = False) -> Session: - """ - Create a new session or load an existing one. - - Args: - session_id: Session ID, creates a new session (auto-generated ID) if None - must_exist: If True and session_id is provided, raises NotFoundError - when the session does not exist. - If session_id is None, must_exist is ignored. - """ - return self._client.session(session_id, must_exist=must_exist) - - async def session_exists(self, session_id: str) -> bool: - """Check whether a session exists in storage. - - Args: - session_id: Session ID to check - - Returns: - True if the session exists, False otherwise - """ - await self._ensure_initialized() - return await self._client.session_exists(session_id) - - async def create_session( - self, - session_id: Optional[str] = None, - telemetry: TelemetryRequest = False, - memory_policy: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: - """Create a new session. - - Args: - session_id: Optional session ID. If provided, creates a session with the given ID. - If None, creates a new session with auto-generated ID. - """ - await self._ensure_initialized() - return await self._client.create_session( - session_id, - telemetry=telemetry, - memory_policy=memory_policy, - ) - - 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, *, auto_create: bool = False) -> Dict[str, Any]: - """Get session details.""" - await self._ensure_initialized() - return await self._client.get_session(session_id, auto_create=auto_create) - - async def get_session_context( - self, session_id: str, token_budget: int = 128_000 - ) -> Dict[str, Any]: - """Get assembled session context.""" - await self._ensure_initialized() - return await self._client.get_session_context(session_id, token_budget=token_budget) - - async def get_session_archive(self, session_id: str, archive_id: str) -> Dict[str, Any]: - """Get one completed archive for a session.""" - await self._ensure_initialized() - return await self._client.get_session_archive(session_id, archive_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 | None = None, - parts: list[dict] | None = None, - created_at: str | None = None, - peer_id: str | None = None, - telemetry: TelemetryRequest = False, - turn_id: str | None = None, - message_kind: str | None = None, - source_message_ids: list[str] | None = None, - ) -> Dict[str, Any]: - """Add a message to a session. - - Args: - session_id: Session ID - role: Message role ("user" or "assistant") - content: Text content (simple mode) - parts: Parts array (full Part support: TextPart, ContextPart, ImagePart, ToolPart) - created_at: Message creation time (ISO format string) - peer_id: Optional stable interaction peer identity. - - If both content and parts are provided, parts takes precedence. - """ - await self._ensure_initialized() - semantic_kwargs = { - key: value - for key, value in { - "turn_id": turn_id, - "message_kind": message_kind, - "source_message_ids": source_message_ids, - }.items() - if value is not None - } - return await self._client.add_message( - session_id=session_id, - role=role, - content=content, - parts=parts, - created_at=created_at, - peer_id=peer_id, - telemetry=telemetry, - **semantic_kwargs, - ) - - async def batch_add_messages( - self, - session_id: str, - messages: list[dict], - telemetry: TelemetryRequest = False, - ) -> Dict[str, Any]: - """Add multiple messages to a session in a single request.""" - await self._ensure_initialized() - return await self._client.batch_add_messages( - session_id=session_id, - messages=messages, - telemetry=telemetry, - ) - - async def commit_session( - self, - session_id: str, - telemetry: TelemetryRequest = False, - *, - keep_recent_count: int = 0, - retention_mode: str | None = None, - keep_recent_turn_count: int | None = None, - retained_message_token_budget: int | None = None, - min_raw_tail_steps: int | None = None, - ) -> Dict[str, Any]: - """Commit a session (archive and extract memories).""" - await self._ensure_initialized() - optional_retention = { - key: value - for key, value in { - "retention_mode": retention_mode, - "keep_recent_turn_count": keep_recent_turn_count, - "retained_message_token_budget": retained_message_token_budget, - "min_raw_tail_steps": min_raw_tail_steps, - }.items() - if value is not None - } - return await self._client.commit_session( - session_id, - telemetry=telemetry, - keep_recent_count=keep_recent_count, - **optional_retention, - ) - - async def get_task(self, task_id: str) -> Optional[Dict[str, Any]]: - """Query background task status.""" - await self._ensure_initialized() - return await self._client.get_task(task_id) - - async def cancel_task(self, task_id: str) -> Optional[Dict[str, Any]]: - """Cancel a background task.""" - await self._ensure_initialized() - return await self._client.cancel_task(task_id) - - async def list_tasks( - self, - task_type: Optional[str] = None, - status: Optional[str] = None, - resource_id: Optional[str] = None, - limit: int = 50, - ) -> list[dict[str, Any]]: - """List background tasks visible to the current caller.""" - await self._ensure_initialized() - return await self._client.list_tasks( - task_type=task_type, - status=status, - resource_id=resource_id, - limit=limit, - ) - - async def reindex( - self, - uri: str, - mode: str = "vectors_only", - wait: bool = True, - dry_run: bool = False, - ) -> Dict[str, Any]: - """Reindex semantic/vector artifacts for a URI.""" - await self._ensure_initialized() - return await self._client.reindex( - uri=uri, - mode=mode, - wait=wait, - dry_run=dry_run, - ) - - # ============= Resource methods ============= - - async def add_resource( - self, - path: str, - to: Optional[str] = None, - parent: Optional[str] = None, - reason: str = "", - instruction: str = "", - wait: bool = False, - timeout: float = None, - build_index: bool = True, - summarize: bool = False, - watch_interval: float = 0, - args: Optional[Dict[str, Any]] = None, - telemetry: TelemetryRequest = False, - processing_mode: str = "semantic_and_vectors", - add_type: Optional[str] = None, - tags: Optional[List[str]] = None, - tag_mode: str = "replace", - **kwargs, - ) -> Dict[str, Any]: - """ - Add a resource (file/URL) to OpenViking. - - Args: - path: Local file path or URL. A sitemap / RSS / Atom URL ingests the - whole site as one resource tree; pass ``args={"site": True}`` to - force whole-site ingestion from a bare domain. - add_type: Explicit Connector source type. Requires an exact ``to`` - target and cannot be combined with ``parent``. The source - ``path`` is forwarded verbatim. - reason: Context/reason for adding this resource. - instruction: Specific instruction for processing. - wait: If True, wait for processing to complete. - to: Exact target URI. Existing targets keep the add_resource incremental-update behavior. - parent: Target parent URI (must already exist). - build_index: Whether to build vector index immediately (default: True). - summarize: Whether to generate summary (default: False). - processing_mode: "semantic_and_vectors" for normal semantic processing, - or "vectors_only" to only build vector indexes. - watch_interval: Auto-refresh interval in minutes (>0 enables a watch). - On a sitemap/feed URL this keeps the whole site refreshed. - args: Parser/accessor-specific options (e.g. ``site``, ``max_pages``). - telemetry: Whether to attach operation telemetry data to the result. - """ - await self._ensure_initialized() - - if add_type is not None: - add_type = add_type.strip() or None - if add_type and parent: - raise ValueError("'add_type' cannot be combined with 'parent'.") - if add_type and not to: - raise ValueError("'add_type' requires an exact 'to' target.") - if to and parent: - raise ValueError("Cannot specify both 'to' and 'parent' at the same time.") - - return await self._client.add_resource( - path=path, - add_type=add_type, - to=to, - parent=parent, - reason=reason, - instruction=instruction, - wait=wait, - timeout=timeout, - build_index=build_index, - summarize=summarize, - processing_mode=processing_mode, - telemetry=telemetry, - watch_interval=watch_interval, - args=args, - tags=tags, - tag_mode=tag_mode, - **kwargs, - ) - - @property - def _service(self): - return self._client.service - - @property - def snapshot(self) -> "AsyncSnapshotNamespace": - """Snapshot version control namespace. - - Lazy-initialized on first access so importing the client does not - pull in the snapshot module when it's not needed. - """ - if getattr(self, "_snapshot", None) is None: - from openviking.snapshot_namespace import AsyncSnapshotNamespace - - self._snapshot = AsyncSnapshotNamespace(self) - return self._snapshot - - async def wait_processed(self, timeout: float = None) -> Dict[str, Any]: - """Wait for all queued processing to complete.""" - await self._ensure_initialized() - return await self._client.wait_processed(timeout=timeout) - - async def build_index(self, resource_uris: Union[str, List[str]], **kwargs) -> Dict[str, Any]: - """ - Manually trigger index building for resources. - - Args: - resource_uris: Single URI or list of URIs to index. - """ - await self._ensure_initialized() - return await self._client.build_index(resource_uris, **kwargs) - - async def summarize(self, resource_uris: Union[str, List[str]], **kwargs) -> Dict[str, Any]: - """ - Manually trigger summarization for resources. - - Args: - resource_uris: Single URI or list of URIs to summarize. - """ - await self._ensure_initialized() - return await self._client.summarize(resource_uris, **kwargs) - - async def add_skill( - self, - data: Any, - wait: bool = False, - timeout: float = None, - telemetry: TelemetryRequest = False, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Add skill to OpenViking. - - Args: - wait: Whether to wait for vectorization to complete - timeout: Wait timeout in seconds - target_uri: Optional target root URI override. Defaults to the - user's private ``viking://user/{user_id}/skills`` directory. - Pass ``viking://agent/skills`` to install a shared skill. - """ - await self._ensure_initialized() - return await self._client.add_skill( - data=data, - wait=wait, - timeout=timeout, - telemetry=telemetry, - target_uri=target_uri, - ) - - async def list_skills( - self, - node_limit: int = 1000, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """List installed skills.""" - await self._ensure_initialized() - return await self._client.list_skills( - node_limit=node_limit, - target_uri=target_uri, - ) - - async def find_skills( - self, - query: str, - limit: int = 10, - score_threshold: Optional[float] = None, - level: Optional[List[int]] = None, - telemetry: TelemetryRequest = False, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Find skills by semantic search.""" - await self._ensure_initialized() - return await self._client.find_skills( - query=query, - limit=limit, - score_threshold=score_threshold, - level=level, - telemetry=telemetry, - target_uri=target_uri, - ) - - async def get_skill( - self, - skill_name: str, - include_content: Optional[bool] = None, - include_files: bool = True, - include_source: bool = False, - level: Optional[int] = None, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Get a skill by name.""" - await self._ensure_initialized() - return await self._client.get_skill( - skill_name=skill_name, - include_content=include_content, - include_files=include_files, - include_source=include_source, - level=level, - target_uri=target_uri, - ) - - async def update_skill( - self, - skill_name: str, - data: Any, - wait: bool = False, - timeout: Optional[float] = None, - source_metadata: Optional[Dict[str, Any]] = None, - telemetry: TelemetryRequest = False, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Update an existing skill.""" - await self._ensure_initialized() - return await self._client.update_skill( - skill_name=skill_name, - data=data, - wait=wait, - timeout=timeout, - source_metadata=source_metadata, - telemetry=telemetry, - target_uri=target_uri, - ) - - async def delete_skill( - self, - skill_name: str, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Delete a skill.""" - await self._ensure_initialized() - return await self._client.delete_skill( - skill_name=skill_name, - target_uri=target_uri, - ) - - async def validate_skill( - self, - data: Any, - strict: bool = False, - source_path: Optional[str] = None, - skill_dir_name: Optional[str] = None, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Validate skill data.""" - await self._ensure_initialized() - return await self._client.validate_skill( - data=data, - strict=strict, - source_path=source_path, - skill_dir_name=skill_dir_name, - target_uri=target_uri, - ) - - # ============= Search methods ============= - - async def search( - self, - query: str = "", - target_uri: Union[str, List[str]] = "", - session: Optional[Union["Session", Any]] = None, - session_id: Optional[str] = None, - limit: int = 10, - score_threshold: Optional[float] = None, - filter: Optional[Dict] = None, - context_type: Optional[SearchContextTypeInput] = None, - tags: Optional[List[str]] = None, - telemetry: TelemetryRequest = False, - since: Optional[str] = None, - until: Optional[str] = None, - time_field: Optional[str] = None, - level: Optional[List[int]] = None, - image: Optional[Any] = None, - ): - """ - Complex search with session context. - - Args: - 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 - - Returns: - FindResult - """ - await self._ensure_initialized() - sid = session_id or (session.session_id if session else None) - return await self._client.search( - query=query, - target_uri=target_uri, - session_id=sid, - limit=limit, - score_threshold=score_threshold, - filter=filter, - context_type=context_type, - tags=tags, - telemetry=telemetry, - since=since, - until=until, - time_field=time_field, - level=level, - image=image, - ) - - async def find( - self, - query: str = "", - target_uri: Union[str, List[str]] = "", - limit: int = 10, - score_threshold: Optional[float] = None, - filter: Optional[Dict] = None, - context_type: Optional[SearchContextTypeInput] = None, - tags: Optional[List[str]] = None, - telemetry: TelemetryRequest = False, - since: Optional[str] = None, - until: Optional[str] = None, - time_field: Optional[str] = None, - level: Optional[List[int]] = None, - image: Optional[Any] = None, - ): - """Semantic search""" - await self._ensure_initialized() - return await self._client.find( - query=query, - target_uri=target_uri, - limit=limit, - score_threshold=score_threshold, - filter=filter, - context_type=context_type, - tags=tags, - telemetry=telemetry, - since=since, - until=until, - time_field=time_field, - level=level, - image=image, - ) - - # ============= FS methods ============= - - async def abstract(self, uri: str) -> str: - """Read L0 abstract (.abstract.md)""" - await self._ensure_initialized() - return await self._client.abstract(uri) - - async def overview(self, uri: str) -> str: - """Read L1 overview (.overview.md)""" - await self._ensure_initialized() - return await self._client.overview(uri) - - async def read(self, uri: str, offset: int = 0, limit: int = -1) -> str: - """Read file content""" - await self._ensure_initialized() - return await self._client.read(uri, offset=offset, limit=limit) - - async def read_raw(self, uri: str, offset: int = 0, limit: int = -1) -> str: - """Read raw file content, including hidden MEMORY_FIELDS metadata.""" - await self._ensure_initialized() - read_raw = getattr(self._client, "read_raw", None) - if read_raw is not None: - return await read_raw(uri, offset=offset, limit=limit) - return await self._client.read(uri, offset=offset, limit=limit) - - async def write( - self, - uri: str, - content: str, - mode: str = "replace", - wait: bool = False, - timeout: Optional[float] = None, - telemetry: TelemetryRequest = False, - ) -> Dict[str, Any]: - """Write text content to an existing file and refresh semantics/vectors.""" - await self._ensure_initialized() - return await self._client.write( - uri=uri, - content=content, - mode=mode, - wait=wait, - timeout=timeout, - telemetry=telemetry, - ) - - async def set_tags( - self, - uri: str, - tags: List[str], - mode: str = "replace", - recursive: bool = False, - telemetry: TelemetryRequest = False, - ) -> Dict[str, Any]: - """Replace explicit retrieval tags for a file or directory.""" - await self._ensure_initialized() - return await self._client.set_tags( - uri=uri, - tags=tags, - mode=mode, - recursive=recursive, - telemetry=telemetry, - ) - - async def ls(self, uri: str, **kwargs) -> List[Any]: - """ - List directory contents. - - Args: - uri: Viking URI - simple: Return only relative path list (bool, default: False) - recursive: List all subdirectories recursively (bool, default: False) - node_limit: Maximum number of entries to return (int, default: 1000) - sort_by: Optional sort field, "name" or "mtime" - sort_order: Sort direction, "asc" or "desc" - """ - await self._ensure_initialized() - recursive = kwargs.get("recursive", False) - simple = kwargs.get("simple", False) - output = kwargs.get("output", "original") - abs_limit = kwargs.get("abs_limit", 256) - show_all_hidden = kwargs.get("show_all_hidden", True) - node_limit = kwargs.get("node_limit", 1000) - sort_by = kwargs.get("sort_by") - sort_order = kwargs.get("sort_order", "asc") - return await self._client.ls( - uri, - recursive=recursive, - simple=simple, - output=output, - abs_limit=abs_limit, - show_all_hidden=show_all_hidden, - node_limit=node_limit, - sort_by=sort_by, - sort_order=sort_order, - ) - - async def rm( - self, - uri: str, - recursive: bool = False, - wait: bool = False, - timeout: Optional[float] = None, - ) -> None: - """Remove resource""" - await self._ensure_initialized() - await self._client.rm(uri, recursive=recursive, wait=wait, timeout=timeout) - - async def grep( - self, - uri: str, - pattern: str, - case_insensitive: bool = False, - node_limit: Optional[int] = None, - exclude_uri: Optional[str] = None, - level_limit: int = 5, - ) -> Dict: - """Content search""" - await self._ensure_initialized() - return await self._client.grep( - uri, - pattern, - case_insensitive=case_insensitive, - node_limit=node_limit, - exclude_uri=exclude_uri, - level_limit=level_limit, - ) - - async def glob(self, pattern: str, uri: str = "viking://") -> Dict: - """File pattern matching""" - await self._ensure_initialized() - return await self._client.glob(pattern, uri=uri) - - async def mv(self, from_uri: str, to_uri: str) -> None: - """Move resource""" - await self._ensure_initialized() - await self._client.mv(from_uri, to_uri) - - async def tree(self, uri: str, **kwargs) -> Dict: - """Get directory tree""" - await self._ensure_initialized() - output = kwargs.get("output", "original") - abs_limit = kwargs.get("abs_limit", 128) - show_all_hidden = kwargs.get("show_all_hidden", True) - node_limit = kwargs.get("node_limit", 1000) - return await self._client.tree( - uri, - output=output, - abs_limit=abs_limit, - show_all_hidden=show_all_hidden, - node_limit=node_limit, - ) - - async def mkdir(self, uri: str, description: Optional[str] = None) -> None: - """Create directory""" - await self._ensure_initialized() - await self._client.mkdir(uri, description=description) - - async def stat(self, uri: str) -> Dict: - """Get resource status""" - await self._ensure_initialized() - return await self._client.stat(uri) - - # ============= Relation methods ============= - - async def relations(self, uri: str) -> List[Dict[str, Any]]: - """Get relations (returns [{"uri": "...", "reason": "..."}, ...])""" - await self._ensure_initialized() - return await self._client.relations(uri) - - async def link(self, from_uri: str, uris: Any, reason: str = "") -> None: - """ - Create link (single or multiple). - - Args: - from_uri: Source URI - uris: Target URI or list of URIs - reason: Reason for linking - """ - await self._ensure_initialized() - await self._client.link(from_uri, uris, reason) - - async def unlink(self, from_uri: str, uri: str) -> None: - """ - Remove link (remove specified URI from uris). - - Args: - from_uri: Source URI - uri: Target URI to remove - """ - await self._ensure_initialized() - await self._client.unlink(from_uri, uri) - - # ============= Pack methods ============= - - async def export_ovpack( - self, - uri: str, - to: str, - include_vectors: bool = False, - ) -> str: - """ - Export specified context path as .ovpack file. - - Args: - uri: Viking URI - to: Target file path - - Returns: - Exported file path - """ - await self._ensure_initialized() - return await self._client.export_ovpack( - uri, - to, - include_vectors=include_vectors, - ) - - async def backup_ovpack(self, to: str, include_vectors: bool = False) -> str: - """ - Back up public OpenViking scopes as a restore-only .ovpack file. - - Args: - to: Target file path - - Returns: - Exported backup file path - """ - await self._ensure_initialized() - return await self._client.backup_ovpack(to, include_vectors=include_vectors) - - async def import_ovpack( - self, - file_path: str, - parent: str, - on_conflict: Optional[str] = None, - vector_mode: Optional[str] = None, - ) -> str: - """ - Import local .ovpack file to specified parent path. - - Args: - file_path: Local .ovpack file path - parent: Target parent URI (e.g., viking://user/alice/resources/references/) - on_conflict: One of "fail", "overwrite", or "skip" - vector_mode: One of "auto", "recompute", or "require" - - Returns: - Imported root resource URI - """ - await self._ensure_initialized() - return await self._client.import_ovpack( - file_path, - parent, - on_conflict=on_conflict, - vector_mode=vector_mode, - ) - - async def restore_ovpack( - self, - file_path: str, - on_conflict: Optional[str] = None, - vector_mode: Optional[str] = None, - ) -> str: - """ - Restore a backup .ovpack file to its original public scope roots. - - Args: - file_path: Local backup .ovpack file path - on_conflict: One of "fail", "overwrite", or "skip" - vector_mode: One of "auto", "recompute", or "require" - - Returns: - Restored root URI - """ - await self._ensure_initialized() - return await self._client.restore_ovpack( - file_path, - on_conflict=on_conflict, - vector_mode=vector_mode, - ) - - # ============= Debug methods ============= - - async def check_consistency(self, uri: str) -> Dict[str, Any]: - """Check filesystem/vector-index consistency for a URI subtree.""" - await self._ensure_initialized() - return await self._client.check_consistency(uri) - - def get_status(self) -> Union[SystemStatus, Dict[str, Any]]: - """Get system status. - - Returns: - SystemStatus containing health status of all components. - """ - return self._client.get_status() - - def is_healthy(self) -> bool: - """Quick health check. - - Returns: - True if all components are healthy, False otherwise. - """ - return self._client.is_healthy() - - @property - def observer(self): - """Get observer service for component status.""" - return self._client.observer diff --git a/openviking/client.py b/openviking/client.py index 9492911efd..2d39ae1ffe 100644 --- a/openviking/client.py +++ b/openviking/client.py @@ -1,32 +1,19 @@ # Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. # SPDX-License-Identifier: AGPL-3.0 -""" -OpenViking client. -This module provides both synchronous and asynchronous clients. -""" +"""HTTP client compatibility exports for the main OpenViking package.""" from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: - from openviking.async_client import AsyncOpenViking - from openviking.sync_client import SyncOpenViking from openviking_cli.client.http import AsyncHTTPClient from openviking_cli.client.sync_http import SyncHTTPClient -__all__ = ["SyncOpenViking", "AsyncOpenViking", "SyncHTTPClient", "AsyncHTTPClient"] +__all__ = ["SyncHTTPClient", "AsyncHTTPClient"] def __getattr__(name: str): - if name == "AsyncOpenViking": - from openviking.async_client import AsyncOpenViking - - return AsyncOpenViking - if name == "SyncOpenViking": - from openviking.sync_client import SyncOpenViking - - return SyncOpenViking if name == "AsyncHTTPClient": from openviking_cli.client.http import AsyncHTTPClient diff --git a/openviking/client/__init__.py b/openviking/client/__init__.py index c6ccc08f8a..2da56dcb72 100644 --- a/openviking/client/__init__.py +++ b/openviking/client/__init__.py @@ -1,27 +1,18 @@ # Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. # SPDX-License-Identifier: AGPL-3.0 -"""OpenViking Client module. - -Provides client implementations for embedded (LocalClient) and HTTP (AsyncHTTPClient/SyncHTTPClient) modes. -""" +"""HTTP client compatibility exports for the main OpenViking package.""" from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: - from openviking.client.local import LocalClient - from openviking.client.session import Session - from openviking_cli.client.base import BaseClient from openviking_cli.client.http import AsyncHTTPClient from openviking_cli.client.sync_http import SyncHTTPClient __all__ = [ - "BaseClient", "AsyncHTTPClient", "SyncHTTPClient", - "LocalClient", - "Session", ] @@ -34,16 +25,4 @@ def __getattr__(name: str): from openviking_cli.client.sync_http import SyncHTTPClient return SyncHTTPClient - if name == "LocalClient": - from openviking.client.local import LocalClient - - return LocalClient - if name == "Session": - from openviking.client.session import Session - - return Session - if name == "BaseClient": - from openviking_cli.client.base import BaseClient - - return BaseClient raise AttributeError(name) diff --git a/openviking/client/local.py b/openviking/client/local.py deleted file mode 100644 index a9baba1f34..0000000000 --- a/openviking/client/local.py +++ /dev/null @@ -1,1362 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 -"""Local Client for OpenViking. - -Implements BaseClient interface using direct service calls (embedded mode). -""" - -from typing import Any, Dict, List, Optional, Union - -from openviking.core.peer_id import normalize_peer_id -from openviking.core.skill_loader import validate_skill_format -from openviking.server.identity import RequestContext, Role -from openviking.server.routers.skills import ( - _list_skill_files, - _list_skills_from_root, - _require_skill, - _restore_skill_privacy, - _skill_summary_from_hit, -) -from openviking.service import OpenVikingService -from openviking.service.task_tracker import get_task_tracker -from openviking.telemetry import TelemetryRequest -from openviking.telemetry.execution import ( - attach_telemetry_payload, - run_with_telemetry, -) -from openviking.utils.image_search import normalize_client_image_input -from openviking.utils.search_filters import SearchContextTypeInput, merge_search_filter -from openviking.utils.tags import build_search_tags_filter -from openviking_cli.client.base import BaseClient -from openviking_cli.exceptions import InvalidArgumentError, NotFoundError, PermissionDeniedError -from openviking_cli.session.user_id import UserIdentifier -from openviking_cli.utils import run_async - - -def _to_jsonable(value: Any) -> Any: - """Convert internal objects into JSON-serializable values.""" - to_dict = getattr(value, "to_dict", None) - if callable(to_dict): - return to_dict() - if isinstance(value, list): - return [_to_jsonable(item) for item in value] - if isinstance(value, dict): - return {k: _to_jsonable(v) for k, v in value.items()} - return value - - -def _resolve_search_filter( - filter: Optional[Dict[str, Any]], - context_type: Optional[SearchContextTypeInput], - since: Optional[str], - until: Optional[str], - time_field: Optional[str], - tags: Optional[List[str]] = None, -) -> Optional[Dict[str, Any]]: - """Merge public retrieval filter shortcuts into the metadata filter.""" - merged = merge_search_filter( - filter, - context_type=context_type, - since=since, - until=until, - time_field=time_field, - ) - tag_filter = build_search_tags_filter(tags) - if not tag_filter: - return merged - if merged: - if tag_filter.get("op") == "and" and isinstance(tag_filter.get("conds"), list): - return {"op": "and", "conds": [merged, *tag_filter["conds"]]} - return {"op": "and", "conds": [merged, tag_filter]} - return tag_filter - - -class LocalClient(BaseClient): - """Local Client for OpenViking (embedded mode). - - Implements BaseClient interface using direct service calls. - """ - - def __init__( - self, - path: Optional[str] = None, - user: Optional[UserIdentifier] = None, - actor_peer_id: Optional[str] = None, - agent_id: Optional[str] = None, - ): - """Initialize LocalClient. - - Args: - path: Local storage path (overrides ov.conf storage path) - user: Explicit account/user identity for embedded mode - actor_peer_id: Optional view filter for the current user's peer collection. - agent_id: Legacy alias that marks the actor peer scope as legacy agent mode. - """ - if actor_peer_id is not None and agent_id is not None: - raise ValueError("actor_peer_id cannot be used with legacy agent_id") - effective_actor_peer_id = actor_peer_id or agent_id - self._service = OpenVikingService( - path=path, - user=user or UserIdentifier.the_default_user(), - ) - self._user = self._service.user - self._ctx = RequestContext( - user=self._user, - role=Role.USER, - actor_peer_id=normalize_peer_id(effective_actor_peer_id), - ) - - @property - def service(self) -> OpenVikingService: - """Get the underlying service instance.""" - return self._service - - # ============= Lifecycle ============= - - async def initialize(self) -> None: - """Initialize the local client.""" - await self._service.initialize() - - async def close(self) -> None: - """Close the local client.""" - await self._service.close() - - # ============= Resource Management ============= - - async def add_resource( - self, - path: str, - to: Optional[str] = None, - parent: Optional[str] = None, - reason: str = "", - instruction: str = "", - wait: bool = False, - timeout: Optional[float] = None, - build_index: bool = True, - summarize: bool = False, - telemetry: TelemetryRequest = False, - watch_interval: float = 0, - args: Optional[Dict[str, Any]] = None, - processing_mode: str = "semantic_and_vectors", - add_type: Optional[str] = None, - tags: Optional[List[str]] = None, - tag_mode: str = "replace", - **kwargs, - ) -> Dict[str, Any]: - """Add resource to OpenViking. - - ``add_type`` declares a Connector source and requires an exact ``to`` - target; it cannot be combined with ``parent``. - """ - if add_type is not None: - add_type = add_type.strip() or None - if add_type and parent: - raise ValueError("'add_type' cannot be combined with 'parent'.") - if add_type and not to: - raise ValueError("'add_type' requires an exact 'to' target.") - if to and parent: - raise ValueError("Cannot specify both 'to' and 'parent' at the same time.") - - execution = await run_with_telemetry( - operation="resources.add_resource", - telemetry=telemetry, - fn=lambda: self._service.resources.add_resource( - path=path, - ctx=self._ctx, - add_type=add_type, - to=to, - parent=parent, - reason=reason, - instruction=instruction, - wait=wait, - timeout=timeout, - build_index=build_index, - summarize=summarize, - processing_mode=processing_mode, - watch_interval=watch_interval, - args=args, - tags=tags, - tag_mode=tag_mode, - **kwargs, - ), - ) - return attach_telemetry_payload( - execution.result, - execution.telemetry, - ) - - async def add_skill( - self, - data: Any, - wait: bool = False, - timeout: Optional[float] = None, - telemetry: TelemetryRequest = False, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Add skill to OpenViking.""" - execution = await run_with_telemetry( - operation="resources.add_skill", - telemetry=telemetry, - fn=lambda: self._service.resources.add_skill( - data=data, - ctx=self._ctx, - wait=wait, - timeout=timeout, - target_uri=target_uri, - ), - ) - return attach_telemetry_payload( - execution.result, - execution.telemetry, - ) - - async def list_skills( - self, - node_limit: int = 1000, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """List installed skills.""" - from openviking.core.namespace import canonical_user_root - - service = self._service - ctx = self._ctx - if target_uri: - skills = await _list_skills_from_root(service, ctx, target_uri) - return {"root_uri": target_uri, "skills": skills, "total": len(skills)} - else: - user_skills = await _list_skills_from_root( - service, ctx, f"{canonical_user_root(ctx)}/skills" - ) - agent_skills = await _list_skills_from_root(service, ctx, "viking://agent/skills") - merged = [*user_skills, *agent_skills] - return { - "root_uris": [f"{canonical_user_root(ctx)}/skills", "viking://agent/skills"], - "skills": merged, - "total": len(merged), - } - - async def find_skills( - self, - query: str, - limit: int = 10, - score_threshold: Optional[float] = None, - level: Optional[List[int]] = None, - telemetry: TelemetryRequest = False, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Find skills by semantic search.""" - from openviking.core.namespace import canonical_user_root - - service = self._service - ctx = self._ctx - - async def _search_at(uri: str) -> list: - result = await service.search.find( - query=query, - ctx=ctx, - target_uri=uri, - limit=limit, - score_threshold=score_threshold, - level=level, - ) - result_dict = result.to_dict() if hasattr(result, "to_dict") else dict(result or {}) - return [_skill_summary_from_hit(hit) for hit in result_dict.get("skills", [])] - - if target_uri: - execution = await run_with_telemetry( - operation="skills.find", - telemetry=telemetry, - fn=lambda: _search_at(target_uri), - ) - hits = execution.result - return { - "root_uri": target_uri, - "skills": hits, - "total": len(hits), - } - else: - user_root = f"{canonical_user_root(ctx)}/skills" - agent_root = "viking://agent/skills" - - user_execution = await run_with_telemetry( - operation="skills.find", - telemetry=telemetry, - fn=lambda: _search_at(user_root), - ) - user_hits = user_execution.result - - agent_hits = await _search_at(agent_root) - - merged = [*user_hits, *agent_hits] - if merged and "score" in merged[0]: - merged.sort(key=lambda x: x.get("score", 0), reverse=True) - - return { - "root_uris": [user_root, agent_root], - "skills": merged, - "total": len(merged), - } - - async def get_skill( - self, - skill_name: str, - include_content: Optional[bool] = None, - include_files: bool = True, - include_source: bool = False, - level: Optional[int] = None, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Get a skill by name.""" - from openviking.server.routers.skills import ( - SOURCE_METADATA_FILENAME, - _parse_abstract_meta, - _relative_skill_path, - _skill_file_kind, - _skill_summary_from_meta, - ) - from openviking.server.skill_source_metadata import read_skill_source_metadata - - if level is not None and level not in {0, 1, 2}: - raise InvalidArgumentError( - "Skill show level must be 0, 1, or 2", - details={"field": "level", "allowed": [0, 1, 2]}, - ) - - service = self._service - ctx = self._ctx - root_uri = await _require_skill(service, ctx, skill_name, target_uri) - - abstract = await service.fs.abstract(root_uri, ctx=ctx) - result = _skill_summary_from_meta(skill_name, root_uri, _parse_abstract_meta(abstract)) - - if level is None or level == 0: - result["abstract"] = abstract - if level is None or level == 1: - result["overview"] = await service.fs.overview(root_uri, ctx=ctx) - if ( - level == 2 - or include_content is True - or (level is None and include_content is not False) - ): - from openviking.server.routers.skills import _skill_md_uri - - result["content"] = await service.fs.read(_skill_md_uri(root_uri), ctx=ctx) - - if include_files: - entries = await _list_skill_files(service, ctx, root_uri) - result["files"] = [ - { - "name": entry.get("name") or skill_name, - "uri": entry.get("uri", ""), - "path": _relative_skill_path(root_uri, entry.get("uri", "")), - "is_dir": entry.get("isDir", False), - "kind": _skill_file_kind( - _relative_skill_path(root_uri, entry.get("uri", "")), - entry.get("isDir", False), - ), - } - for entry in entries - if isinstance(entry, dict) - and _relative_skill_path(root_uri, entry.get("uri", "")) != SOURCE_METADATA_FILENAME - ] - - if include_source: - result["source"] = await read_skill_source_metadata(service, ctx, root_uri) - - return result - - async def update_skill( - self, - skill_name: str, - data: Any, - wait: bool = False, - timeout: Optional[float] = None, - source_metadata: Optional[Dict[str, Any]] = None, - telemetry: TelemetryRequest = False, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Update an existing skill.""" - service = self._service - ctx = self._ctx - - # Verify the skill exists and determine its root URI - root_uri = await _require_skill(service, ctx, skill_name, target_uri) - skill_root_parent = root_uri.rsplit("/", 1)[0] - - execution = await run_with_telemetry( - operation="skills.update", - telemetry=telemetry, - fn=lambda: self._update_skill_impl( - skill_name, data, root_uri, skill_root_parent, wait, timeout, source_metadata - ), - ) - return attach_telemetry_payload( - execution.result, - execution.telemetry, - ) - - async def _update_skill_impl( - self, - skill_name: str, - data: Any, - root_uri: str, - skill_root_parent: str, - wait: bool, - timeout: Optional[float], - source_metadata: Optional[Dict[str, Any]], - ) -> Dict[str, Any]: - import shutil - import uuid - - from openviking.server.skill_source_metadata import persist_skill_source_metadata - - service = self._service - ctx = self._ctx - backup_uri = f"{skill_root_parent}/.{skill_name}.update-backup-{uuid.uuid4().hex}" - backup_created = False - privacy_update_attempted = False - previous_privacy = None - preparation = None - privacy = service.privacy_configs - effective_source_metadata = source_metadata or { - "type": "embedded", - "source": "inline_content", - "operation": "update", - } - try: - if privacy is not None: - previous_privacy = await privacy.get_current(ctx, "skill", skill_name) - preparation = await service.resources._skill_processor.prepare_skill_processing( # noqa: SLF001 - data, - ctx=ctx, - allow_local_path_resolution=False, - ) - expected_name = skill_name - if preparation.skill_dict.get("name") != expected_name: - raise InvalidArgumentError( - f"Skill name mismatch: path name is '{expected_name}', content name is '{preparation.skill_dict.get('name')}'", - details={ - "expected": expected_name, - "actual": preparation.skill_dict.get("name"), - }, - ) - await service.fs.mv(root_uri, backup_uri, ctx=ctx) - backup_created = True - result = await service.resources.add_skill( - data=preparation, - ctx=ctx, - wait=wait, - timeout=timeout, - allow_local_path_resolution=False, - apply_privacy=False, - privacy_change_reason="auto-extracted from update_skill", - target_uri=skill_root_parent, - ) - await persist_skill_source_metadata(service, ctx, result, effective_source_metadata) - privacy_update_attempted = True - await service.resources._skill_processor.apply_skill_privacy( # noqa: SLF001 - preparation.skill_dict, - preparation.privacy_values, - ctx, - change_reason="auto-extracted from update_skill", - delete_if_empty=True, - ) - except Exception: - if backup_created: - try: - await service.fs.rm(root_uri, ctx=ctx, recursive=True) - except Exception: - pass - try: - await service.fs.mv(backup_uri, root_uri, ctx=ctx) - except Exception: - pass - if privacy_update_attempted: - try: - await _restore_skill_privacy(service, ctx, skill_name, previous_privacy) - except Exception: - pass - raise - else: - if backup_created: - try: - await service.fs.rm(backup_uri, ctx=ctx, recursive=True) - except Exception: - pass - result["action"] = "update" - return result - finally: - if preparation and preparation.cleanup_path: - shutil.rmtree(preparation.cleanup_path, ignore_errors=True) - - async def delete_skill( - self, - skill_name: str, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Delete a skill.""" - service = self._service - ctx = self._ctx - root_uri = await _require_skill(service, ctx, skill_name, target_uri) - await service.fs.rm(root_uri, ctx=ctx, recursive=True) - return {"name": skill_name, "uri": root_uri, "root_uri": root_uri, "deleted": True} - - async def validate_skill( - self, - data: Any, - strict: bool = False, - source_path: Optional[str] = None, - skill_dir_name: Optional[str] = None, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Validate skill data.""" - result = validate_skill_format( - data, - strict=strict, - skill_dir_name=skill_dir_name, - source_path=source_path, - ) - return result - - async def wait_processed(self, timeout: Optional[float] = None) -> Dict[str, Any]: - """Wait for all processing to complete.""" - return await self._service.resources.wait_processed(timeout=timeout) - - async def reindex( - self, - uri: str, - mode: str = "vectors_only", - wait: bool = True, - dry_run: bool = False, - ) -> Dict[str, Any]: - """Reindex semantic/vector artifacts for a URI.""" - return await self._service.reindex( - uri=uri, - mode=mode, - wait=wait, - dry_run=dry_run, - ) - - async def build_index(self, resource_uris: Union[str, List[str]], **kwargs) -> Dict[str, Any]: - """Manually trigger index building.""" - if isinstance(resource_uris, str): - resource_uris = [resource_uris] - return await self._service.resources.build_index(resource_uris, ctx=self._ctx, **kwargs) - - async def summarize(self, resource_uris: Union[str, List[str]], **kwargs) -> Dict[str, Any]: - """Manually trigger summarization.""" - if isinstance(resource_uris, str): - resource_uris = [resource_uris] - return await self._service.resources.summarize(resource_uris, ctx=self._ctx, **kwargs) - - # ============= File System ============= - - async def ls( - self, - uri: str, - simple: bool = False, - recursive: bool = False, - output: str = "original", - abs_limit: int = 256, - show_all_hidden: bool = False, - node_limit: int = 1000, - sort_by: Optional[str] = None, - sort_order: str = "asc", - ) -> List[Any]: - """List directory contents.""" - return await self._service.fs.ls( - uri, - ctx=self._ctx, - simple=simple, - recursive=recursive, - output=output, - abs_limit=abs_limit, - show_all_hidden=show_all_hidden, - node_limit=node_limit, - sort_by=sort_by, - sort_order=sort_order, - ) - - async def tree( - self, - uri: str, - output: str = "original", - abs_limit: int = 128, - show_all_hidden: bool = False, - node_limit: int = 1000, - ) -> List[Dict[str, Any]]: - """Get directory tree.""" - return await self._service.fs.tree( - uri, - ctx=self._ctx, - output=output, - abs_limit=abs_limit, - show_all_hidden=show_all_hidden, - node_limit=node_limit, - ) - - async def stat(self, uri: str) -> Dict[str, Any]: - """Get resource status.""" - return await self._service.fs.stat(uri, ctx=self._ctx) - - async def mkdir(self, uri: str, description: Optional[str] = None) -> None: - """Create directory.""" - await self._service.fs.mkdir(uri, ctx=self._ctx, description=description) - - async def rm( - self, - uri: str, - recursive: bool = False, - wait: bool = False, - timeout: Optional[float] = None, - ) -> None: - """Remove resource.""" - await self._service.fs.rm( - uri, - ctx=self._ctx, - recursive=recursive, - wait=wait, - timeout=timeout, - ) - - async def mv(self, from_uri: str, to_uri: str) -> None: - """Move resource.""" - await self._service.fs.mv(from_uri, to_uri, ctx=self._ctx) - - # ============= Content Reading ============= - - async def read(self, uri: str, offset: int = 0, limit: int = -1) -> str: - """Read file content. - - Args: - uri: Viking URI - offset: Starting line number (0-indexed). Default 0. - limit: Number of lines to read. -1 means read to end. Default -1. - """ - return await self._service.fs.read(uri, ctx=self._ctx, offset=offset, limit=limit) - - async def read_raw(self, uri: str, offset: int = 0, limit: int = -1) -> str: - """Read raw file content, including hidden MEMORY_FIELDS metadata.""" - return await self._service.fs.read(uri, ctx=self._ctx, offset=offset, limit=limit) - - async def abstract(self, uri: str) -> str: - """Read L0 abstract.""" - return await self._service.fs.abstract(uri, ctx=self._ctx) - - async def overview(self, uri: str) -> str: - """Read L1 overview.""" - return await self._service.fs.overview(uri, ctx=self._ctx) - - async def write( - self, - uri: str, - content: str, - mode: str = "replace", - wait: bool = False, - timeout: Optional[float] = None, - telemetry: TelemetryRequest = False, - ) -> Dict[str, Any]: - """Write text content to an existing file and refresh semantics/vectors.""" - execution = await run_with_telemetry( - operation="content.write", - telemetry=telemetry, - fn=lambda: self._service.fs.write( - uri=uri, - content=content, - ctx=self._ctx, - mode=mode, - wait=wait, - timeout=timeout, - ), - ) - return attach_telemetry_payload( - execution.result, - execution.telemetry, - ) - - async def set_tags( - self, - uri: str, - tags: List[str], - mode: str = "replace", - recursive: bool = False, - telemetry: TelemetryRequest = False, - ) -> Dict[str, Any]: - """Replace explicit retrieval tags for a file or directory.""" - execution = await run_with_telemetry( - operation="content.set_tags", - telemetry=telemetry, - fn=lambda: self._service.fs.set_tags( - uri=uri, - tags=tags, - mode=mode, - recursive=recursive, - ctx=self._ctx, - ), - ) - return attach_telemetry_payload( - execution.result, - execution.telemetry, - ) - - # ============= Search ============= - - async def find( - self, - query: str = "", - target_uri: Union[str, List[str]] = "", - limit: int = 10, - score_threshold: Optional[float] = None, - filter: Optional[Dict[str, Any]] = None, - context_type: Optional[SearchContextTypeInput] = None, - tags: Optional[List[str]] = None, - telemetry: TelemetryRequest = False, - since: Optional[str] = None, - until: Optional[str] = None, - time_field: Optional[str] = None, - level: Optional[List[int]] = None, - image: Optional[Any] = None, - ) -> Any: - """Semantic search without session context.""" - resolved_filter = _resolve_search_filter( - filter, context_type, since, until, time_field, tags - ) - image_url = normalize_client_image_input(image) - execution = await run_with_telemetry( - operation="search.find", - telemetry=telemetry, - fn=lambda: self._service.search.find( - query=query, - ctx=self._ctx, - target_uri=target_uri, - limit=limit, - score_threshold=score_threshold, - filter=resolved_filter, - level=level, - image_url=image_url, - ), - ) - return attach_telemetry_payload( - execution.result, - execution.telemetry, - ) - - async def search( - self, - query: str = "", - target_uri: Union[str, List[str]] = "", - session_id: Optional[str] = None, - limit: int = 10, - score_threshold: Optional[float] = None, - filter: Optional[Dict[str, Any]] = None, - context_type: Optional[SearchContextTypeInput] = None, - tags: Optional[List[str]] = None, - telemetry: TelemetryRequest = False, - since: Optional[str] = None, - until: Optional[str] = None, - time_field: Optional[str] = None, - level: Optional[List[int]] = None, - image: Optional[Any] = None, - ) -> Any: - """Semantic search with optional session context.""" - resolved_filter = _resolve_search_filter( - filter, context_type, since, until, time_field, tags - ) - image_url = normalize_client_image_input(image) - - async def _search(): - session = None - # Intent off: skip session.load — SearchService will not scan session either. - if session_id and self._service.search.is_intent_enabled(): - session = self._service.sessions.session(self._ctx, session_id) - await session.load() - return await self._service.search.search( - query=query, - ctx=self._ctx, - target_uri=target_uri, - session=session, - limit=limit, - score_threshold=score_threshold, - filter=resolved_filter, - level=level, - image_url=image_url, - ) - - execution = await run_with_telemetry( - operation="search.search", - telemetry=telemetry, - fn=_search, - ) - return attach_telemetry_payload( - execution.result, - execution.telemetry, - ) - - async def grep( - self, - uri: str, - pattern: str, - case_insensitive: bool = False, - node_limit: Optional[int] = None, - exclude_uri: Optional[str] = None, - level_limit: int = 5, - ) -> Dict[str, Any]: - """Content search with pattern.""" - return await self._service.fs.grep( - uri, - pattern, - ctx=self._ctx, - case_insensitive=case_insensitive, - node_limit=node_limit, - exclude_uri=exclude_uri, - level_limit=level_limit, - ) - - async def glob(self, pattern: str, uri: str = "viking://") -> Dict[str, Any]: - """File pattern matching.""" - return await self._service.fs.glob(pattern, ctx=self._ctx, uri=uri) - - # ============= Relations ============= - - async def relations(self, uri: str) -> List[Any]: - """Get relations for a resource.""" - return await self._service.relations.relations(uri, ctx=self._ctx) - - async def link(self, from_uri: str, to_uris: Union[str, List[str]], reason: str = "") -> None: - """Create link between resources.""" - await self._service.relations.link(from_uri, to_uris, ctx=self._ctx, reason=reason) - - async def unlink(self, from_uri: str, to_uri: str) -> None: - """Remove link between resources.""" - await self._service.relations.unlink(from_uri, to_uri, ctx=self._ctx) - - # ============= Sessions ============= - - async def create_session( - self, - session_id: Optional[str] = None, - telemetry: TelemetryRequest = False, - memory_policy: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: - """Create a new session. - - Args: - session_id: Optional session ID. If provided, creates a session with the given ID. - If None, creates a new session with auto-generated ID. - """ - execution = await run_with_telemetry( - operation="session.create", - telemetry=telemetry, - fn=lambda: self._create_session_impl(session_id, memory_policy), - ) - return attach_telemetry_payload( - execution.result, - execution.telemetry, - ) - - async def _create_session_impl( - self, - session_id: Optional[str], - memory_policy: Optional[Dict[str, Any]], - ) -> Dict[str, Any]: - await self._service.initialize_user_directories(self._ctx) - session = await self._service.sessions.create( - self._ctx, - session_id, - memory_policy=memory_policy, - ) - return { - "session_id": session.session_id, - "uri": session.uri, - "user": session.user.to_dict(), - } - - async def list_sessions(self) -> List[Any]: - """List all sessions.""" - return await self._service.sessions.sessions(self._ctx) - - async def get_session(self, session_id: str, *, auto_create: bool = False) -> Dict[str, Any]: - """Get session details.""" - session = await self._service.sessions.get(session_id, self._ctx, auto_create=auto_create) - result = session.meta.to_dict() - result["uri"] = session.uri - result["user"] = session.user.to_dict() - return result - - async def get_session_context( - self, session_id: str, token_budget: int = 128_000 - ) -> Dict[str, Any]: - """Get assembled session context.""" - session = await self._service.sessions.get(session_id, self._ctx, auto_create=False) - result = await session.get_session_context(token_budget=token_budget) - return _to_jsonable(result) - - async def get_session_archive(self, session_id: str, archive_id: str) -> Dict[str, Any]: - """Get one completed archive for a session.""" - session = await self._service.sessions.get(session_id, self._ctx, auto_create=False) - result = await session.get_session_archive(archive_id) - return _to_jsonable(result) - - async def delete_session(self, session_id: str) -> None: - """Delete a session.""" - await self._service.sessions.delete(session_id, self._ctx) - - async def commit_session( - self, - session_id: str, - telemetry: TelemetryRequest = False, - *, - keep_recent_count: int = 0, - retention_mode: Optional[str] = None, - keep_recent_turn_count: Optional[int] = None, - retained_message_token_budget: Optional[int] = None, - min_raw_tail_steps: Optional[int] = None, - ) -> Dict[str, Any]: - """Commit a session (archive and extract memories).""" - commit_kwargs: Dict[str, Any] = {"keep_recent_count": keep_recent_count} - optional_retention = { - "retention_mode": retention_mode, - "keep_recent_turn_count": keep_recent_turn_count, - "retained_message_token_budget": retained_message_token_budget, - "min_raw_tail_steps": min_raw_tail_steps, - } - commit_kwargs.update( - {key: value for key, value in optional_retention.items() if value is not None} - ) - execution = await run_with_telemetry( - operation="session.commit", - telemetry=telemetry, - fn=lambda: self._service.sessions.commit( - session_id, - self._ctx, - **commit_kwargs, - ), - ) - return attach_telemetry_payload( - execution.result, - execution.telemetry, - ) - - async def get_task(self, task_id: str) -> Optional[Dict[str, Any]]: - """Query background task status.""" - return await self._service.sessions.get_commit_task(task_id, self._ctx) - - async def cancel_task(self, task_id: str) -> Optional[Dict[str, Any]]: - """Cancel a background task.""" - if self._ctx.role == Role.ROOT: - raise PermissionDeniedError("ROOT may not cancel tasks") - task = await get_task_tracker().cancel( - task_id, - account_id=self._ctx.account_id, - user_id=self._ctx.user.user_id, - ) - return task.to_dict() if task else None - - async def list_tasks( - self, - task_type: Optional[str] = None, - status: Optional[str] = None, - resource_id: Optional[str] = None, - limit: int = 50, - ) -> List[Dict[str, Any]]: - """List background tasks visible to the current caller.""" - tasks = await get_task_tracker().list_tasks( - task_type=task_type, - status=status, - resource_id=resource_id, - limit=limit, - account_id=self._ctx.account_id, - user_id=self._ctx.user.user_id, - ) - return [task.to_dict() for task in tasks] - - async def add_message( - self, - session_id: str, - role: str, - content: Optional[str] = None, - parts: Optional[List[Dict[str, Any]]] = None, - created_at: Optional[str] = None, - peer_id: Optional[str] = None, - telemetry: TelemetryRequest = False, - turn_id: Optional[str] = None, - message_kind: Optional[str] = None, - source_message_ids: Optional[List[str]] = None, - ) -> Dict[str, Any]: - """Add a message to a session. - - Args: - session_id: Session ID - role: Message role ("user" or "assistant") - content: Text content (simple mode, backward compatible) - parts: Parts array (full Part support mode) - created_at: Message creation time (ISO format string) - peer_id: Optional stable interaction peer identity. - - If both content and parts are provided, parts takes precedence. - """ - execution = await run_with_telemetry( - operation="session.add_message", - telemetry=telemetry, - fn=lambda: self._add_message_impl( - session_id, - role, - content, - parts, - created_at, - peer_id, - turn_id, - message_kind, - source_message_ids, - ), - ) - return attach_telemetry_payload( - execution.result, - execution.telemetry, - ) - - async def _add_message_impl( - self, - session_id: str, - role: str, - content: Optional[str], - parts: Optional[List[Dict[str, Any]]], - created_at: Optional[str], - peer_id: Optional[str], - turn_id: Optional[str], - message_kind: Optional[str], - source_message_ids: Optional[List[str]], - ) -> Dict[str, Any]: - from openviking.message.part import Part, TextPart, part_from_dict - - session = await self._service.sessions.get(session_id, self._ctx, auto_create=True) - - message_parts: list[Part] - if parts is not None: - message_parts = [part_from_dict(p) for p in parts] - elif content is not None: - message_parts = [TextPart(text=content)] - else: - raise ValueError("Either content or parts must be provided") - - semantic_kwargs = { - key: value - for key, value in { - "turn_id": turn_id, - "message_kind": message_kind, - "source_message_ids": source_message_ids, - }.items() - if value is not None - } - add_async = getattr(session, "add_message_async", None) - add_kwargs = { - "peer_id": normalize_peer_id(peer_id), - "created_at": created_at, - **semantic_kwargs, - } - if callable(add_async): - await add_async(role, message_parts, **add_kwargs) - else: - session.add_message(role, message_parts, **add_kwargs) - return { - "session_id": session_id, - "message_count": len(session.messages), - # Post-write value so a commit policy can decide without a - # follow-up get_session round trip. - "pending_tokens": self._session_pending_tokens(session), - } - - async def batch_add_messages( - self, - session_id: str, - messages: List[Dict[str, Any]], - telemetry: TelemetryRequest = False, - ) -> Dict[str, Any]: - """Add multiple messages to a session in one batch.""" - execution = await run_with_telemetry( - operation="session.batch_add_messages", - telemetry=telemetry, - fn=lambda: self._batch_add_messages_impl(session_id, messages), - ) - return attach_telemetry_payload( - execution.result, - execution.telemetry, - ) - - async def _batch_add_messages_impl( - self, - session_id: str, - messages: List[Dict[str, Any]], - ) -> Dict[str, Any]: - from openviking.message.part import Part, TextPart, part_from_dict - - session = await self._service.sessions.get(session_id, self._ctx, auto_create=True) - specs: list[dict[str, Any]] = [] - - for index, message in enumerate(messages): - role = message.get("role") - if not role: - raise ValueError(f"messages[{index}]: missing required key 'role'") - - message_parts: list[Part] - if message.get("parts") is not None: - message_parts = [part_from_dict(part) for part in message["parts"]] - elif message.get("content") is not None: - message_parts = [TextPart(text=str(message["content"]))] - else: - raise ValueError(f"messages[{index}]: Either content or parts must be provided") - - specs.append( - { - "role": role, - "parts": message_parts, - "peer_id": normalize_peer_id(message.get("peer_id")), - "created_at": message.get("created_at"), - "turn_id": message.get("turn_id"), - "message_kind": message.get("message_kind"), - "source_message_ids": message.get("source_message_ids"), - } - ) - - add_many_async = getattr(session, "add_messages_async", None) - if callable(add_many_async): - added = await add_many_async(specs) - else: - added = session.add_messages(specs) - return { - "session_id": session_id, - "message_count": len(session.messages), - "added": len(added), - # Post-write value so a commit policy can decide without a - # follow-up get_session round trip. - "pending_tokens": self._session_pending_tokens(session), - } - - @staticmethod - def _session_pending_tokens(session: Any) -> int: - """Read the post-write pending-token count from a session. - - Returns 0 when the session object does not expose ``meta`` so callers - keep working against lightweight or legacy session implementations. - """ - meta = getattr(session, "meta", None) - try: - return max(0, int(getattr(meta, "pending_tokens", 0) or 0)) - except (TypeError, ValueError): - return 0 - - # ============= Pack ============= - - async def export_ovpack( - self, - uri: str, - to: str, - include_vectors: bool = False, - ) -> str: - """Export context as .ovpack file.""" - return await self._service.pack.export_ovpack( - uri, - to, - ctx=self._ctx, - include_vectors=include_vectors, - ) - - async def backup_ovpack(self, to: str, include_vectors: bool = False) -> str: - """Back up public scopes as a restore-only .ovpack file.""" - return await self._service.pack.backup_ovpack( - to, - ctx=self._ctx, - include_vectors=include_vectors, - ) - - async def import_ovpack( - self, - file_path: str, - parent: str, - on_conflict: Optional[str] = None, - vector_mode: Optional[str] = None, - ) -> str: - """Import .ovpack file.""" - return await self._service.pack.import_ovpack( - file_path, - parent, - ctx=self._ctx, - on_conflict=on_conflict, - vector_mode=vector_mode, - ) - - async def restore_ovpack( - self, - file_path: str, - on_conflict: Optional[str] = None, - vector_mode: Optional[str] = None, - ) -> str: - """Restore backup .ovpack file.""" - return await self._service.pack.restore_ovpack( - file_path, - ctx=self._ctx, - on_conflict=on_conflict, - vector_mode=vector_mode, - ) - - # ============= Git Version Control ============= - - async def git_commit( - self, - *, - message: str, - paths: Optional[List[str]] = None, - branch: str = "main", - author_name: Optional[str] = None, - author_email: Optional[str] = None, - ) -> Dict[str, Any]: - """Create a git snapshot. See VikingFS.commit for semantics.""" - return await self._service.fs.commit( - message=message, - paths=paths, - branch=branch, - author_name=author_name, - author_email=author_email, - ctx=self._ctx, - ) - - async def git_restore( - self, - *, - project_dir: Optional[str] = None, - source_commit: str, - branch: str = "main", - dry_run: bool = False, - message: Optional[str] = None, - author_name: Optional[str] = None, - author_email: Optional[str] = None, - ) -> Dict[str, Any]: - """Restore a subtree, or the full account tree when project_dir is omitted.""" - return await self._service.fs.restore( - project_dir=project_dir, - source_commit=source_commit, - branch=branch, - dry_run=dry_run, - message=message, - author_name=author_name, - author_email=author_email, - ctx=self._ctx, - ) - - async def git_show( - self, - target_ref: str, - *, - path: Optional[str] = None, - ) -> Any: - """Read a commit's metadata or a single blob.""" - return await self._service.fs.show(target_ref, path=path, ctx=self._ctx) - - async def git_log( - self, - *, - branch: str = "main", - limit: int = 20, - paths: Optional[List[str]] = None, - ) -> List[Dict[str, Any]]: - """Walk back along parents[0] up to limit commits.""" - return await self._service.fs.log(branch=branch, limit=limit, paths=paths, ctx=self._ctx) - - async def git_diff( - self, - path: str, - *, - to_ref: str, - from_ref: Optional[str] = None, - ) -> Dict[str, Any]: - """Compare one file between two snapshot refs.""" - return await self._service.fs.diff( - path=path, - from_ref=from_ref, - to_ref=to_ref, - ctx=self._ctx, - ) - - async def git_get_ignore(self) -> str: - """Return the account .ovgitignore content (empty string if absent).""" - return await self._service.fs.get_gitignore(ctx=self._ctx) - - async def git_set_ignore(self, *, content: str) -> None: - """Write the account .ovgitignore control file.""" - await self._service.fs.set_gitignore(content=content, ctx=self._ctx) - - async def git_delete_ignore(self) -> None: - """Delete the account .ovgitignore control file (missing is success).""" - await self._service.fs.delete_gitignore(ctx=self._ctx) - - # ============= Debug ============= - - async def check_consistency(self, uri: str) -> Dict[str, Any]: - """Check filesystem/vector-index consistency for a URI subtree.""" - return await self._service.check_consistency( - uri=uri, - ctx=self._ctx, - ) - - async def health(self) -> bool: - """Check service health.""" - return True # Local service is always healthy if initialized - - def session(self, session_id: Optional[str] = None, must_exist: bool = False) -> Any: - """Create a new session or load an existing one. - - Args: - session_id: Session ID, creates a new session if None. - must_exist: Whether to raise an error if the session does not exist. Default False. - Returns: - Session object if exists, None otherwise. - """ - - if session_id: - try: - return run_async( - self._service.sessions.get(session_id, self._ctx, auto_create=False) - ) - except NotFoundError: - if must_exist: - raise NotFoundError(session_id, "session") - - session = self._service.sessions.session(self._ctx, session_id) - run_async(session.ensure_exists()) - return session - - async def session_exists(self, session_id: str) -> bool: - """Check whether a session exists in storage. - - Args: - session_id: Session ID to check - - Returns: - True if the session exists, False otherwise - """ - try: - await self._service.sessions.get(session_id, self._ctx, auto_create=False) - return True - except NotFoundError: - return False - - def get_status(self) -> Any: - """Get system status. - - Returns: - SystemStatus containing health status of all components. - """ - return self._service.debug.observer.system() - - def is_healthy(self) -> bool: - """Quick health check (synchronous). - - Returns: - True if all components are healthy, False otherwise. - """ - return self._service.debug.observer.is_healthy() - - @property - def observer(self) -> Any: - """Get observer service for component status.""" - return self._service.debug.observer diff --git a/openviking/client/session.py b/openviking/client/session.py deleted file mode 100644 index 5286bfb697..0000000000 --- a/openviking/client/session.py +++ /dev/null @@ -1,183 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 -"""Lightweight Session class for OpenViking client. - -Session delegates all operations to the underlying Client (LocalClient or AsyncHTTPClient). -""" - -from dataclasses import asdict -from typing import TYPE_CHECKING, Any, Dict, List, Optional - -from openviking.message.part import Part -from openviking.telemetry import TelemetryRequest -from openviking_cli.session.user_id import UserIdentifier - -if TYPE_CHECKING: - from openviking_cli.client.base import BaseClient - - -class Session: - """Lightweight Session wrapper that delegates operations to Client. - - This class provides a convenient OOP interface for session operations. - All actual work is delegated to the underlying client. - """ - - def __init__(self, client: "BaseClient", session_id: str, user: UserIdentifier): - """Initialize Session. - - Args: - client: The underlying client (LocalClient or AsyncHTTPClient) - session_id: Session ID - user: User name - """ - self._client = client - self.session_id = session_id - self.user = user - - async def add_message( - self, - role: str, - content: Optional[str] = None, - parts: Optional[List[Part]] = None, - created_at: Optional[str] = None, - peer_id: Optional[str] = None, - turn_id: Optional[str] = None, - message_kind: Optional[str] = None, - source_message_ids: Optional[List[str]] = None, - ) -> Dict[str, Any]: - """Add a message to the session. - - Args: - role: Message role (e.g., "user", "assistant") - content: Text content (simple mode) - parts: Parts list (TextPart, ContextPart, ImagePart, ToolPart) - created_at: Message creation time (ISO format string). If not provided, current time is used. - peer_id: Optional stable interaction peer identity. - - If both content and parts are provided, parts takes precedence. - - Returns: - Result dict with session_id and message_count - """ - semantic_kwargs = { - key: value - for key, value in { - "turn_id": turn_id, - "message_kind": message_kind, - "source_message_ids": source_message_ids, - }.items() - if value is not None - } - if parts is not None: - parts_dicts = [asdict(p) for p in parts] - return await self._client.add_message( - self.session_id, - role, - parts=parts_dicts, - created_at=created_at, - peer_id=peer_id, - **semantic_kwargs, - ) - return await self._client.add_message( - self.session_id, - role, - content=content, - created_at=created_at, - peer_id=peer_id, - **semantic_kwargs, - ) - - async def batch_add_messages( - self, - messages: List[Dict[str, Any]], - ) -> Dict[str, Any]: - """Add multiple messages to the session in a single request. - - Args: - messages: List of dicts, each with "role" and optionally "content", - "parts", "created_at", "peer_id". - - Returns: - Result dict with session_id, message_count, and added count. - """ - return await self._client.batch_add_messages( - self.session_id, - messages=messages, - ) - - async def commit( - self, - telemetry: TelemetryRequest = False, - *, - keep_recent_count: int = 0, - retention_mode: Optional[str] = None, - keep_recent_turn_count: Optional[int] = None, - retained_message_token_budget: Optional[int] = None, - min_raw_tail_steps: Optional[int] = None, - ) -> Dict[str, Any]: - """Commit the session (archive messages and extract memories). - - Returns: - Commit result - """ - kwargs: Dict[str, Any] = { - "telemetry": telemetry, - "keep_recent_count": keep_recent_count, - } - optional = { - "retention_mode": retention_mode, - "keep_recent_turn_count": keep_recent_turn_count, - "retained_message_token_budget": retained_message_token_budget, - "min_raw_tail_steps": min_raw_tail_steps, - } - kwargs.update({key: value for key, value in optional.items() if value is not None}) - return await self._client.commit_session(self.session_id, **kwargs) - - async def commit_async( - self, - telemetry: TelemetryRequest = False, - *, - keep_recent_count: int = 0, - retention_mode: Optional[str] = None, - keep_recent_turn_count: Optional[int] = None, - retained_message_token_budget: Optional[int] = None, - min_raw_tail_steps: Optional[int] = None, - ) -> Dict[str, Any]: - """Commit the session asynchronously (archive messages and extract memories). - Used in viking bot for committing. - - Returns: - Commit result - """ - return await self.commit( - telemetry=telemetry, - keep_recent_count=keep_recent_count, - retention_mode=retention_mode, - keep_recent_turn_count=keep_recent_turn_count, - retained_message_token_budget=retained_message_token_budget, - min_raw_tail_steps=min_raw_tail_steps, - ) - - async def delete(self) -> None: - """Delete the session.""" - await self._client.delete_session(self.session_id) - - async def load(self) -> Dict[str, Any]: - """Load session data. - - Returns: - Session details - """ - return await self._client.get_session(self.session_id) - - async def get_session_context(self, token_budget: int = 128_000) -> Dict[str, Any]: - """Get assembled session context.""" - return await self._client.get_session_context(self.session_id, token_budget=token_budget) - - async def get_archive(self, archive_id: str) -> Dict[str, Any]: - """Get one completed archive for the session.""" - return await self._client.get_session_archive(self.session_id, archive_id) - - def __repr__(self) -> str: - return f"Session(id={self.session_id}, user={self.user.__str__()})" diff --git a/openviking/eval/ragas/pipeline.py b/openviking/eval/ragas/pipeline.py index 49d181384a..84bf8aff2a 100644 --- a/openviking/eval/ragas/pipeline.py +++ b/openviking/eval/ragas/pipeline.py @@ -4,7 +4,6 @@ RAG Query Pipeline for OpenViking evaluation. """ -import json from pathlib import Path from typing import Any, Dict, List, Union @@ -26,31 +25,26 @@ class RAGQueryPipeline: def __init__( self, config_path: str = "./ov.conf", - data_path: str = "./data", + server_url: str = "http://127.0.0.1:1933", ): """ Initialize the RAG pipeline. Args: config_path: Path to OpenViking config file - data_path: Path to OpenViking data directory + server_url: OpenViking HTTP server URL """ self.config_path = config_path - self.data_path = data_path + self.server_url = server_url self._client = None self._llm = None def _get_client(self): """Lazy initialization of OpenViking client.""" if self._client is None: - import openviking as ov - from openviking_cli.utils.config.open_viking_config import OpenVikingConfig + from openviking_sdk import SyncHTTPClient - with open(self.config_path, "r") as f: - config_dict = json.load(f) - - config = OpenVikingConfig.from_dict(config_dict) - self._client = ov.SyncOpenViking(path=self.data_path, config=config) + self._client = SyncHTTPClient(url=self.server_url) self._client.initialize() logger.info("OpenViking client initialized") return self._client @@ -155,7 +149,9 @@ def query( retrieved_uris = [] items = ( - search_result.get("results", []) if isinstance(search_result, dict) else search_result + search_result.get("memories", []) + + search_result.get("resources", []) + + search_result.get("skills", []) ) for item in items or []: if isinstance(item, dict): diff --git a/openviking/eval/ragas/rag_eval.py b/openviking/eval/ragas/rag_eval.py index 3af2c829e5..3d1466f3f3 100644 --- a/openviking/eval/ragas/rag_eval.py +++ b/openviking/eval/ragas/rag_eval.py @@ -19,7 +19,7 @@ from pathlib import Path from typing import Any, Dict, List -from openviking_cli.utils.config import OPENVIKING_CONFIG_ENV, OPENVIKING_ENABLE_RECORDER_ENV +from openviking_cli.utils.config import OPENVIKING_CONFIG_ENV logging.basicConfig( level=logging.INFO, @@ -65,8 +65,7 @@ def __init__( docs_dirs: List[str], code_dirs: List[str], config_path: str = "./ov.conf", - data_path: str = "./data", - enable_recorder: bool = False, + server_url: str = "http://127.0.0.1:1933", ): """ Initialize the RAG evaluator. @@ -75,38 +74,28 @@ def __init__( docs_dirs: List of document directories or files code_dirs: List of code repository paths config_path: Path to OpenViking config file - data_path: Path to OpenViking data directory - enable_recorder: Whether to enable IO recording + server_url: OpenViking HTTP server URL """ self.docs_dirs = docs_dirs self.code_dirs = code_dirs self.config_path = config_path - self.data_path = data_path - self.enable_recorder = enable_recorder + self.server_url = server_url self._client = None self._initialized = False - if enable_recorder: - from openviking.eval.recorder import init_recorder - - init_recorder(enabled=True) - logger.info("IO Recorder enabled") - def _get_client(self): """Get or create OpenViking client.""" if self._client is None: try: - from openviking import OpenViking + from openviking_sdk import SyncHTTPClient config_path = Path(self.config_path).expanduser() if config_path.exists(): os.environ[OPENVIKING_CONFIG_ENV] = str(config_path) logger.info(f"Using config file: {config_path}") - if self.enable_recorder: - os.environ[OPENVIKING_ENABLE_RECORDER_ENV] = "true" - - self._client = OpenViking(path=self.data_path) + self._client = SyncHTTPClient(url=self.server_url) + self._client.initialize() except Exception as e: logger.error(f"Failed to create OpenViking client: {e}") raise @@ -176,12 +165,17 @@ async def retrieve(self, query: str, top_k: int = 5) -> Dict[str, Any]: contexts = [] if result: - for ctx in result: + items = ( + result.get("memories", []) + + result.get("resources", []) + + result.get("skills", []) + ) + for ctx in items: contexts.append( { - "uri": getattr(ctx, "uri", ""), - "content": getattr(ctx, "abstract", "") or getattr(ctx, "overview", ""), - "score": getattr(ctx, "score", 0.0), + "uri": ctx.get("uri", ""), + "content": ctx.get("abstract", "") or ctx.get("overview", ""), + "score": ctx.get("score", 0.0), } ) @@ -363,8 +357,7 @@ async def main_async(args): docs_dirs=args.docs_dir, code_dirs=args.code_dir, config_path=args.config, - data_path=args.data_path, - enable_recorder=args.recorder, + server_url=args.url, ) print("\nRunning RAG evaluation...") @@ -381,32 +374,6 @@ async def main_async(args): if args.ragas: await run_ragas_evaluation(eval_results) - if args.recorder: - from openviking.eval.recorder import get_recorder - from openviking.storage.viking_fs import get_viking_fs - - recorder = get_recorder() - - viking_fs = get_viking_fs() - if hasattr(viking_fs.agfs, "stop_recording"): - viking_fs.agfs.stop_recording() - - stats = recorder.get_stats() - print("\n" + "=" * 60) - print("IO Recorder Statistics") - print("=" * 60) - print(f"Total Records: {stats['total_count']}") - print(f"FS Operations: {stats['fs_count']}") - print(f"VikingDB Operations: {stats['vikingdb_count']}") - print(f"Total Latency: {stats['total_latency_ms']:.2f} ms") - print(f"Errors: {stats['errors']}") - if stats["operations"]: - print("\nOperations Breakdown:") - for op, data in stats["operations"].items(): - avg_latency = data["total_latency_ms"] / data["count"] if data["count"] > 0 else 0 - print(f" {op}: {data['count']} calls, avg {avg_latency:.2f} ms") - print(f"\nRecord file: {recorder.record_file}") - def main(): """Main entry point.""" @@ -453,9 +420,9 @@ def main(): ) parser.add_argument( - "--data_path", - default="./data", - help="Path to OpenViking data directory (default: ./data)", + "--url", + default="http://127.0.0.1:1933", + help="OpenViking server URL (default: http://127.0.0.1:1933)", ) parser.add_argument( @@ -476,12 +443,6 @@ def main(): help="Run RAGAS evaluation (requires ragas package)", ) - parser.add_argument( - "--recorder", - action="store_true", - help="Enable IO recording for storage layer evaluation", - ) - args = parser.parse_args() asyncio.run(main_async(args)) diff --git a/openviking/service/core.py b/openviking/service/core.py index 5953e7543a..ef826b2c2d 100644 --- a/openviking/service/core.py +++ b/openviking/service/core.py @@ -440,8 +440,8 @@ async def initialize(self) -> None: # Register as the process-wide service so flows that resolve the # service via the dependency global (e.g. background reindex tasks - # triggered by git restore) work in embedded mode, not just under the - # HTTP server which calls set_service() during bootstrap. + # triggered by git restore) work for explicitly constructed service + # instances as well as server-owned instances. from openviking.server.dependencies import set_service set_service(self) diff --git a/openviking/service/session_service.py b/openviking/service/session_service.py index d428ca7b52..c4d4b0f295 100644 --- a/openviking/service/session_service.py +++ b/openviking/service/session_service.py @@ -46,9 +46,8 @@ def __init__( self._viking_fs = viking_fs self._session_compressor = session_compressor self._tool_output_externalization_config = ToolOutputExternalizationConfig() - # Embedded clients do not load ServerConfig. Preserve their historical - # Agent memory behavior; HTTP servers always override this from - # server.agent_evolution during app setup. + # Directly constructed services default to enabled. HTTP servers override + # this from server.agent_evolution during app setup. self._agent_evolution_enabled = True self._agent_evolution_config_provider: Optional[AgentEvolutionConfigProvider] = None self._usage_reporter: Optional["UsageReporter"] = None diff --git a/openviking/snapshot_namespace.py b/openviking/snapshot_namespace.py deleted file mode 100644 index 7198d2f7b4..0000000000 --- a/openviking/snapshot_namespace.py +++ /dev/null @@ -1,211 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 -"""Snapshot (multi-version) namespace for OpenViking clients. - -Exposes the snapshot/versioning methods on BaseClient under a -`client.snapshot.*` namespace so the user-facing API reads as -`client.snapshot.commit(...)` rather than the flat `client.git_commit(...)` -underneath. -""" -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Dict, List, Optional - -from openviking_cli.utils import run_async - -if TYPE_CHECKING: - from openviking.async_client import AsyncOpenViking - from openviking.sync_client import SyncOpenViking - - -class AsyncSnapshotNamespace: - """Snapshot version control methods on the async client. - - Forwards to the underlying BaseClient's git_* methods. - """ - - def __init__(self, client: "AsyncOpenViking"): - self._client = client - - async def commit( - self, - *, - message: str, - paths: Optional[List[str]] = None, - branch: str = "main", - author_name: Optional[str] = None, - author_email: Optional[str] = None, - ) -> Dict[str, Any]: - await self._client._ensure_initialized() - return await self._client._client.git_commit( - message=message, - paths=paths, - branch=branch, - author_name=author_name, - author_email=author_email, - ) - - async def restore( - self, - *, - project_dir: Optional[str] = None, - source_commit: str, - branch: str = "main", - dry_run: bool = False, - message: Optional[str] = None, - author_name: Optional[str] = None, - author_email: Optional[str] = None, - ) -> Dict[str, Any]: - await self._client._ensure_initialized() - return await self._client._client.git_restore( - project_dir=project_dir, - source_commit=source_commit, - branch=branch, - dry_run=dry_run, - message=message, - author_name=author_name, - author_email=author_email, - ) - - async def show( - self, - target_ref: str, - *, - path: Optional[str] = None, - ) -> Any: - await self._client._ensure_initialized() - return await self._client._client.git_show(target_ref, path=path) - - async def log( - self, - *, - branch: str = "main", - limit: int = 20, - paths: Optional[List[str]] = None, - ) -> List[Dict[str, Any]]: - await self._client._ensure_initialized() - return await self._client._client.git_log(branch=branch, limit=limit, paths=paths) - - async def diff( - self, - path: str, - *, - to_ref: str, - from_ref: Optional[str] = None, - ) -> Dict[str, Any]: - """Compare one file between two snapshot refs.""" - await self._client._ensure_initialized() - return await self._client._client.git_diff( - path, - from_ref=from_ref, - to_ref=to_ref, - ) - - async def get_gitignore(self) -> str: - """Return the account .ovgitignore content (empty string if absent).""" - await self._client._ensure_initialized() - return await self._client._client.git_get_ignore() - - async def set_gitignore(self, *, content: str) -> None: - """Write the account .ovgitignore control file.""" - await self._client._ensure_initialized() - await self._client._client.git_set_ignore(content=content) - - async def delete_gitignore(self) -> None: - """Delete the account .ovgitignore control file (missing is success).""" - await self._client._ensure_initialized() - await self._client._client.git_delete_ignore() - - -class SyncSnapshotNamespace: - """Synchronous wrapper around AsyncSnapshotNamespace. - - Each method calls into the SyncOpenViking's underlying async client - via run_async, matching the rest of the SyncOpenViking surface. - """ - - def __init__(self, client: "SyncOpenViking"): - self._client = client - - def _ns(self) -> AsyncSnapshotNamespace: - return self._client._async_client.snapshot - - def commit( - self, - *, - message: str, - paths: Optional[List[str]] = None, - branch: str = "main", - author_name: Optional[str] = None, - author_email: Optional[str] = None, - ) -> Dict[str, Any]: - return run_async( - self._ns().commit( - message=message, - paths=paths, - branch=branch, - author_name=author_name, - author_email=author_email, - ) - ) - - def restore( - self, - *, - project_dir: Optional[str] = None, - source_commit: str, - branch: str = "main", - dry_run: bool = False, - message: Optional[str] = None, - author_name: Optional[str] = None, - author_email: Optional[str] = None, - ) -> Dict[str, Any]: - return run_async( - self._ns().restore( - project_dir=project_dir, - source_commit=source_commit, - branch=branch, - dry_run=dry_run, - message=message, - author_name=author_name, - author_email=author_email, - ) - ) - - def show( - self, - target_ref: str, - *, - path: Optional[str] = None, - ) -> Any: - return run_async(self._ns().show(target_ref, path=path)) - - def log( - self, - *, - branch: str = "main", - limit: int = 20, - paths: Optional[List[str]] = None, - ) -> List[Dict[str, Any]]: - return run_async(self._ns().log(branch=branch, limit=limit, paths=paths)) - - def diff( - self, - path: str, - *, - to_ref: str, - from_ref: Optional[str] = None, - ) -> Dict[str, Any]: - """Compare one file between two snapshot refs.""" - return run_async( - self._ns().diff(path, from_ref=from_ref, to_ref=to_ref) - ) - - def get_gitignore(self) -> str: - return run_async(self._ns().get_gitignore()) - - def set_gitignore(self, *, content: str) -> None: - return run_async(self._ns().set_gitignore(content=content)) - - def delete_gitignore(self) -> None: - return run_async(self._ns().delete_gitignore()) diff --git a/openviking/storage/observers/README.md b/openviking/storage/observers/README.md index 97fa240abb..0f7fe9b274 100644 --- a/openviking/storage/observers/README.md +++ b/openviking/storage/observers/README.md @@ -35,10 +35,11 @@ Monitors queue system status (Embedding, Semantic, and custom queues). **Usage:** ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -client = ov.OpenViking(path="./data") -print(client.observer.queue) +client = SyncHTTPClient(url="http://127.0.0.1:1933") +client.initialize() +print(client.observer.queue()) # Output: # Queue Pending In Progress Processed Errors Total # Embedding 5 2 100 0 107 @@ -55,9 +56,10 @@ Monitors VikingDB collection status (index count and vector count per collection **Usage:** ```python -import openviking as ov +from openviking_sdk import SyncHTTPClient -client = ov.OpenViking(path="./data") +client = SyncHTTPClient(url="http://127.0.0.1:1933") +client.initialize() print(client.observer.vikingdb()) # Output: # Collection Index Count Vector Count Status @@ -69,7 +71,7 @@ print(client.observer.vikingdb()) 1. **Use `get_status_table()` for human-readable output**: Provides clean, formatted tables 2. **Check the table output**: Look at "Errors" column to detect issues early -3. **Use with sync or async client**: Works seamlessly with both `OpenViking` and `AsyncOpenViking` +3. **Use the HTTP SDK**: Both `SyncHTTPClient` and `AsyncHTTPClient` expose observer endpoints ## See Also diff --git a/openviking/sync_client.py b/openviking/sync_client.py deleted file mode 100644 index 657b2ebb3c..0000000000 --- a/openviking/sync_client.py +++ /dev/null @@ -1,717 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 -""" -Synchronous OpenViking client implementation. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union - -if TYPE_CHECKING: - from openviking.session import Session - from openviking.snapshot_namespace import SyncSnapshotNamespace - -from openviking.async_client import AsyncOpenViking -from openviking.telemetry import TelemetryRequest -from openviking.utils.search_filters import SearchContextTypeInput -from openviking_cli.utils import run_async - - -class SyncOpenViking: - """ - SyncOpenViking main client class (Synchronous). - Wraps AsyncOpenViking with synchronous methods. - """ - - def __init__( - self, - path: Optional[str] = None, - actor_peer_id: Optional[str] = None, - agent_id: Optional[str] = None, - ): - self._async_client = AsyncOpenViking( - path=path, - actor_peer_id=actor_peer_id, - agent_id=agent_id, - ) - self._initialized = False - self._snapshot: Optional["SyncSnapshotNamespace"] = None - - def initialize(self) -> None: - """Initialize OpenViking storage and indexes.""" - run_async(self._async_client.initialize()) - self._initialized = True - - def session(self, session_id: Optional[str] = None, must_exist: bool = False) -> "Session": - """Create new session or load existing session.""" - return self._async_client.session(session_id, must_exist=must_exist) - - def session_exists(self, session_id: str) -> bool: - """Check whether a session exists in storage.""" - return run_async(self._async_client.session_exists(session_id)) - - def create_session( - self, - session_id: Optional[str] = None, - telemetry: TelemetryRequest = False, - memory_policy: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: - """Create a new session. - - Args: - session_id: Optional session ID. If provided, creates a session with the given ID. - If None, creates a new session with auto-generated ID. - """ - return run_async( - self._async_client.create_session( - session_id, - telemetry=telemetry, - memory_policy=memory_policy, - ) - ) - - def list_sessions(self) -> List[Any]: - """List all sessions.""" - return run_async(self._async_client.list_sessions()) - - def get_session(self, session_id: str, *, auto_create: bool = False) -> Dict[str, Any]: - """Get session details.""" - return run_async(self._async_client.get_session(session_id, auto_create=auto_create)) - - def get_session_context(self, session_id: str, token_budget: int = 128_000) -> Dict[str, Any]: - """Get assembled session context.""" - return run_async( - self._async_client.get_session_context(session_id, token_budget=token_budget) - ) - - def get_session_archive(self, session_id: str, archive_id: str) -> Dict[str, Any]: - """Get one completed archive for a session.""" - return run_async(self._async_client.get_session_archive(session_id, archive_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 | None = None, - parts: list[dict] | None = None, - created_at: str | None = None, - peer_id: str | None = None, - telemetry: TelemetryRequest = False, - turn_id: str | None = None, - message_kind: str | None = None, - source_message_ids: list[str] | None = None, - ) -> Dict[str, Any]: - """Add a message to a session. - - Args: - session_id: Session ID - role: Message role ("user" or "assistant") - content: Text content (simple mode) - parts: Parts array (full Part support: TextPart, ContextPart, ImagePart, ToolPart) - created_at: Message creation time (ISO format string). If not provided, current time is used. - peer_id: Optional stable interaction peer identity. - - If both content and parts are provided, parts takes precedence. - """ - semantic_kwargs = { - key: value - for key, value in { - "turn_id": turn_id, - "message_kind": message_kind, - "source_message_ids": source_message_ids, - }.items() - if value is not None - } - return run_async( - self._async_client.add_message( - session_id=session_id, - role=role, - content=content, - parts=parts, - created_at=created_at, - peer_id=peer_id, - telemetry=telemetry, - **semantic_kwargs, - ) - ) - - def batch_add_messages( - self, - session_id: str, - messages: list[dict], - telemetry: TelemetryRequest = False, - ) -> Dict[str, Any]: - """Add multiple messages to a session in a single request.""" - return run_async( - self._async_client.batch_add_messages( - session_id, - messages, - telemetry, - ) - ) - - def commit_session( - self, - session_id: str, - telemetry: TelemetryRequest = False, - *, - keep_recent_count: int = 0, - retention_mode: str | None = None, - keep_recent_turn_count: int | None = None, - retained_message_token_budget: int | None = None, - min_raw_tail_steps: int | None = None, - ) -> Dict[str, Any]: - """Commit a session (archive and extract memories).""" - optional_retention = { - key: value - for key, value in { - "retention_mode": retention_mode, - "keep_recent_turn_count": keep_recent_turn_count, - "retained_message_token_budget": retained_message_token_budget, - "min_raw_tail_steps": min_raw_tail_steps, - }.items() - if value is not None - } - return run_async( - self._async_client.commit_session( - session_id, - telemetry=telemetry, - keep_recent_count=keep_recent_count, - **optional_retention, - ) - ) - - def get_task(self, task_id: str) -> Optional[Dict[str, Any]]: - """Query background task status.""" - return run_async(self._async_client.get_task(task_id)) - - def cancel_task(self, task_id: str) -> Optional[Dict[str, Any]]: - """Cancel a background task.""" - return run_async(self._async_client.cancel_task(task_id)) - - def list_tasks( - self, - task_type: Optional[str] = None, - status: Optional[str] = None, - resource_id: Optional[str] = None, - limit: int = 50, - ) -> list[dict[str, Any]]: - """List background tasks visible to the current caller.""" - return run_async( - self._async_client.list_tasks( - task_type=task_type, - status=status, - resource_id=resource_id, - limit=limit, - ) - ) - - def reindex( - self, - uri: str, - mode: str = "vectors_only", - wait: bool = True, - dry_run: bool = False, - ) -> Dict[str, Any]: - """Reindex semantic/vector artifacts for a URI.""" - return run_async( - self._async_client.reindex( - uri=uri, - mode=mode, - wait=wait, - dry_run=dry_run, - ) - ) - - def add_resource( - self, - path: str, - to: Optional[str] = None, - parent: Optional[str] = None, - reason: str = "", - instruction: str = "", - wait: bool = False, - timeout: float = None, - build_index: bool = True, - summarize: bool = False, - args: Optional[Dict[str, Any]] = None, - telemetry: TelemetryRequest = False, - processing_mode: str = "semantic_and_vectors", - add_type: Optional[str] = None, - tags: Optional[List[str]] = None, - tag_mode: str = "replace", - **kwargs, - ) -> Dict[str, Any]: - """Add resource to OpenViking (resources scope only) - - A sitemap / RSS / Atom URL ingests the whole site as one resource tree; - pass ``args={"site": True}`` to force whole-site ingestion from a bare - domain. A ``watch_interval`` on a sitemap/feed URL keeps the whole site - refreshed. - - Args: - add_type: Explicit Connector source type. Requires an exact ``to`` - target and cannot be combined with ``parent``. The source - ``path`` is forwarded verbatim. - to: Exact target URI. Existing targets keep the add_resource incremental-update behavior. - parent: Target parent URI for automatic child naming. - build_index: Whether to build vector index immediately (default: True). - summarize: Whether to generate summary (default: False). - **kwargs: Extra options forwarded to the parser chain, e.g. - ``strict``, ``ignore_dirs``, ``include``, ``exclude``. - """ - if add_type is not None: - add_type = add_type.strip() or None - if add_type and parent: - raise ValueError("'add_type' cannot be combined with 'parent'.") - if add_type and not to: - raise ValueError("'add_type' requires an exact 'to' target.") - if to and parent: - raise ValueError("Cannot specify both 'to' and 'parent' at the same time.") - return run_async( - self._async_client.add_resource( - path=path, - add_type=add_type, - to=to, - parent=parent, - reason=reason, - instruction=instruction, - wait=wait, - timeout=timeout, - build_index=build_index, - summarize=summarize, - processing_mode=processing_mode, - args=args, - tags=tags, - tag_mode=tag_mode, - telemetry=telemetry, - **kwargs, - ) - ) - - def add_skill( - self, - data: Any, - wait: bool = False, - timeout: float = None, - telemetry: TelemetryRequest = False, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Add skill to OpenViking.""" - return run_async( - self._async_client.add_skill( - data, - wait=wait, - timeout=timeout, - telemetry=telemetry, - target_uri=target_uri, - ) - ) - - def list_skills( - self, - node_limit: int = 1000, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """List installed skills.""" - return run_async( - self._async_client.list_skills( - node_limit=node_limit, - target_uri=target_uri, - ) - ) - - def find_skills( - self, - query: str, - limit: int = 10, - score_threshold: Optional[float] = None, - level: Optional[List[int]] = None, - telemetry: TelemetryRequest = False, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Find skills by semantic search.""" - return run_async( - self._async_client.find_skills( - query=query, - limit=limit, - score_threshold=score_threshold, - level=level, - telemetry=telemetry, - target_uri=target_uri, - ) - ) - - def get_skill( - self, - skill_name: str, - include_content: Optional[bool] = None, - include_files: bool = True, - include_source: bool = False, - level: Optional[int] = None, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Get a skill by name.""" - return run_async( - self._async_client.get_skill( - skill_name=skill_name, - include_content=include_content, - include_files=include_files, - include_source=include_source, - level=level, - target_uri=target_uri, - ) - ) - - def update_skill( - self, - skill_name: str, - data: Any, - wait: bool = False, - timeout: Optional[float] = None, - source_metadata: Optional[Dict[str, Any]] = None, - telemetry: TelemetryRequest = False, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Update an existing skill.""" - return run_async( - self._async_client.update_skill( - skill_name=skill_name, - data=data, - wait=wait, - timeout=timeout, - source_metadata=source_metadata, - telemetry=telemetry, - target_uri=target_uri, - ) - ) - - def delete_skill( - self, - skill_name: str, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Delete a skill.""" - return run_async( - self._async_client.delete_skill( - skill_name=skill_name, - target_uri=target_uri, - ) - ) - - def validate_skill( - self, - data: Any, - strict: bool = False, - source_path: Optional[str] = None, - skill_dir_name: Optional[str] = None, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Validate skill data.""" - return run_async( - self._async_client.validate_skill( - data=data, - strict=strict, - source_path=source_path, - skill_dir_name=skill_dir_name, - target_uri=target_uri, - ) - ) - - def search( - self, - query: str = "", - target_uri: Union[str, List[str]] = "", - session: Optional["Session"] = None, - session_id: Optional[str] = None, - limit: int = 10, - score_threshold: Optional[float] = None, - filter: Optional[Dict] = None, - context_type: Optional[SearchContextTypeInput] = None, - tags: Optional[List[str]] = None, - telemetry: TelemetryRequest = False, - since: Optional[str] = None, - until: Optional[str] = None, - time_field: Optional[str] = None, - level: Optional[List[int]] = None, - image: Optional[Any] = None, - ): - """Execute complex retrieval (intent analysis, hierarchical retrieval).""" - return run_async( - self._async_client.search( - query=query, - target_uri=target_uri, - session=session, - session_id=session_id, - limit=limit, - score_threshold=score_threshold, - filter=filter, - context_type=context_type, - tags=tags, - telemetry=telemetry, - since=since, - until=until, - time_field=time_field, - level=level, - image=image, - ) - ) - - def find( - self, - query: str = "", - target_uri: Union[str, List[str]] = "", - limit: int = 10, - score_threshold: Optional[float] = None, - filter: Optional[Dict] = None, - context_type: Optional[SearchContextTypeInput] = None, - tags: Optional[List[str]] = None, - telemetry: TelemetryRequest = False, - since: Optional[str] = None, - until: Optional[str] = None, - time_field: Optional[str] = None, - level: Optional[List[int]] = None, - image: Optional[Any] = None, - ): - """Quick retrieval""" - return run_async( - self._async_client.find( - query, - target_uri, - limit, - score_threshold, - filter, - context_type, - tags, - telemetry, - since, - until, - time_field, - level, - image, - ) - ) - - def abstract(self, uri: str) -> str: - """Read L0 abstract""" - return run_async(self._async_client.abstract(uri)) - - def overview(self, uri: str) -> str: - """Read L1 overview""" - return run_async(self._async_client.overview(uri)) - - def read(self, uri: str, offset: int = 0, limit: int = -1) -> str: - """Read file""" - return run_async(self._async_client.read(uri, offset=offset, limit=limit)) - - def write( - self, - uri: str, - content: str, - mode: str = "replace", - wait: bool = False, - timeout: Optional[float] = None, - telemetry: TelemetryRequest = False, - ) -> Dict[str, Any]: - """Write text content to an existing file and refresh semantics/vectors.""" - return run_async( - self._async_client.write( - uri=uri, - content=content, - mode=mode, - wait=wait, - timeout=timeout, - telemetry=telemetry, - ) - ) - - def set_tags( - self, - uri: str, - tags: List[str], - mode: str = "replace", - recursive: bool = False, - telemetry: TelemetryRequest = False, - ) -> Dict[str, Any]: - """Replace explicit retrieval tags for a file or directory.""" - return run_async( - self._async_client.set_tags( - uri=uri, - tags=tags, - mode=mode, - recursive=recursive, - telemetry=telemetry, - ) - ) - - def ls(self, uri: str, **kwargs) -> List[Any]: - """ - List directory contents. - - Args: - uri: Viking URI - simple: Return only relative path list (bool, default: False) - recursive: List all subdirectories recursively (bool, default: False) - """ - return run_async(self._async_client.ls(uri, **kwargs)) - - def link(self, from_uri: str, uris: Any, reason: str = "") -> None: - """Create relation""" - return run_async(self._async_client.link(from_uri, uris, reason)) - - def unlink(self, from_uri: str, uri: str) -> None: - """Delete relation""" - return run_async(self._async_client.unlink(from_uri, uri)) - - def export_ovpack(self, uri: str, to: str, include_vectors: bool = False) -> str: - """Export .ovpack file""" - return run_async(self._async_client.export_ovpack(uri, to, include_vectors=include_vectors)) - - def backup_ovpack(self, to: str, include_vectors: bool = False) -> str: - """Back up public scopes as a restore-only .ovpack file.""" - return run_async(self._async_client.backup_ovpack(to, include_vectors=include_vectors)) - - def import_ovpack( - self, - file_path: str, - target: str, - on_conflict: Optional[str] = None, - vector_mode: Optional[str] = None, - ) -> str: - """Import .ovpack file (triggers vectorization by default)""" - return run_async( - self._async_client.import_ovpack( - file_path, - target, - on_conflict=on_conflict, - vector_mode=vector_mode, - ) - ) - - def restore_ovpack( - self, - file_path: str, - on_conflict: Optional[str] = None, - vector_mode: Optional[str] = None, - ) -> str: - """Restore backup .ovpack file.""" - return run_async( - self._async_client.restore_ovpack( - file_path, - on_conflict=on_conflict, - vector_mode=vector_mode, - ) - ) - - def check_consistency(self, uri: str) -> Dict[str, Any]: - """Check filesystem/vector-index consistency for a URI subtree.""" - return run_async(self._async_client.check_consistency(uri)) - - def close(self) -> None: - """Close OpenViking and release resources.""" - return run_async(self._async_client.close()) - - def relations(self, uri: str) -> List[Dict[str, Any]]: - """Get relations""" - return run_async(self._async_client.relations(uri)) - - def rm( - self, - uri: str, - recursive: bool = False, - wait: bool = False, - timeout: float = None, - ) -> None: - """Delete resource""" - return run_async(self._async_client.rm(uri, recursive, wait=wait, timeout=timeout)) - - 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)) - - def grep( - self, - uri: str, - pattern: str, - case_insensitive: bool = False, - node_limit: Optional[int] = None, - exclude_uri: Optional[str] = None, - level_limit: int = 5, - ) -> Dict: - """Content search""" - return run_async( - self._async_client.grep( - uri, - pattern, - case_insensitive, - node_limit, - exclude_uri, - level_limit, - ) - ) - - def glob(self, pattern: str, uri: str = "viking://") -> Dict: - """File pattern matching""" - return run_async(self._async_client.glob(pattern, uri)) - - def mv(self, from_uri: str, to_uri: str) -> None: - """Move resource""" - return run_async(self._async_client.mv(from_uri, to_uri)) - - def tree(self, uri: str, **kwargs) -> Dict: - """Get directory tree""" - return run_async(self._async_client.tree(uri, **kwargs)) - - def stat(self, uri: str) -> Dict: - """Get resource status""" - return run_async(self._async_client.stat(uri)) - - def mkdir(self, uri: str, description: Optional[str] = None) -> None: - """Create directory""" - return run_async(self._async_client.mkdir(uri, description=description)) - - def get_status(self): - """Get system status. - - 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: - """Quick health check. - - 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 - - @property - def snapshot(self) -> "SyncSnapshotNamespace": - """Snapshot version control namespace (synchronous).""" - if getattr(self, "_snapshot", None) is None: - from openviking.snapshot_namespace import SyncSnapshotNamespace - - self._snapshot = SyncSnapshotNamespace(self) - return self._snapshot - - @classmethod - def reset(cls) -> None: - """Reset singleton (for testing).""" - return run_async(AsyncOpenViking.reset()) diff --git a/openviking/utils/process_lock.py b/openviking/utils/process_lock.py index db27995452..6db6f54cb4 100644 --- a/openviking/utils/process_lock.py +++ b/openviking/utils/process_lock.py @@ -17,7 +17,7 @@ LOCK_FILENAME = ".openviking.pid" -# A PID file protects the whole process, while multiple embedded services may +# A PID file protects the whole process, while multiple service instances may # legitimately share that process and workspace. Keep process-local ownership # counts so closing one service cannot expose another live service to a second # process. The file remains the cross-process source of truth. diff --git a/openviking_cli/client/__init__.py b/openviking_cli/client/__init__.py index 7f5eb37f7f..9485b98753 100644 --- a/openviking_cli/client/__init__.py +++ b/openviking_cli/client/__init__.py @@ -1,16 +1,11 @@ # Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. # SPDX-License-Identifier: AGPL-3.0 -"""OpenViking Client module. +"""OpenViking HTTP client compatibility exports.""" -Provides client implementations for embedded (LocalClient) and HTTP (AsyncHTTPClient/SyncHTTPClient) modes. -""" - -from openviking_cli.client.base import BaseClient from openviking_cli.client.http import AsyncHTTPClient from openviking_cli.client.sync_http import SyncHTTPClient __all__ = [ - "BaseClient", "AsyncHTTPClient", "SyncHTTPClient", ] diff --git a/openviking_cli/client/base.py b/openviking_cli/client/base.py deleted file mode 100644 index 1f2568f51d..0000000000 --- a/openviking_cli/client/base.py +++ /dev/null @@ -1,640 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 -"""Base client interface for OpenViking. - -Defines the abstract base class that both LocalClient and AsyncHTTPClient implement. -""" - -from abc import ABC, abstractmethod -from typing import Any, Dict, List, Optional, Union - -from openviking.telemetry import TelemetryRequest -from openviking.utils.search_filters import SearchContextTypeInput - - -class BaseClient(ABC): - """Abstract base class for OpenViking clients. - - Both LocalClient (embedded mode) and AsyncHTTPClient (HTTP mode) implement this interface. - """ - - # ============= Lifecycle ============= - - @abstractmethod - async def initialize(self) -> None: - """Initialize the client.""" - ... - - @abstractmethod - async def close(self) -> None: - """Close the client and release resources.""" - ... - - # ============= Resource Management ============= - - @abstractmethod - async def add_resource( - self, - path: str, - to: Optional[str] = None, - parent: Optional[str] = None, - reason: str = "", - instruction: str = "", - wait: bool = False, - timeout: Optional[float] = None, - watch_interval: float = 0, - processing_mode: str = "semantic_and_vectors", - args: Optional[Dict[str, Any]] = None, - telemetry: TelemetryRequest = False, - add_type: Optional[str] = None, - tags: Optional[List[str]] = None, - tag_mode: str = "replace", - ) -> Dict[str, Any]: - """Add resource to OpenViking. - - ``add_type`` declares a Connector source and requires an exact ``to`` - target; it cannot be combined with ``parent``. - """ - ... - - @abstractmethod - async def add_skill( - self, - data: Any, - wait: bool = False, - timeout: Optional[float] = None, - telemetry: TelemetryRequest = False, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Add skill to OpenViking.""" - ... - - @abstractmethod - async def list_skills( - self, - node_limit: int = 1000, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """List skills.""" - ... - - @abstractmethod - async def find_skills( - self, - query: str, - limit: int = 10, - score_threshold: Optional[float] = None, - level: Optional[List[int]] = None, - telemetry: TelemetryRequest = False, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Find skills by semantic search.""" - ... - - @abstractmethod - async def get_skill( - self, - skill_name: str, - include_content: Optional[bool] = None, - include_files: bool = True, - include_source: bool = False, - level: Optional[int] = None, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Get a skill by name.""" - ... - - @abstractmethod - async def update_skill( - self, - skill_name: str, - data: Any, - wait: bool = False, - timeout: Optional[float] = None, - source_metadata: Optional[Dict[str, Any]] = None, - telemetry: TelemetryRequest = False, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Update an existing skill.""" - ... - - @abstractmethod - async def delete_skill( - self, - skill_name: str, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Delete a skill.""" - ... - - @abstractmethod - async def validate_skill( - self, - data: Any, - strict: bool = False, - source_path: Optional[str] = None, - skill_dir_name: Optional[str] = None, - target_uri: Optional[str] = None, - ) -> Dict[str, Any]: - """Validate skill data.""" - ... - - @abstractmethod - async def wait_processed(self, timeout: Optional[float] = None) -> Dict[str, Any]: - """Wait for all processing to complete.""" - ... - - @abstractmethod - async def reindex( - self, - uri: str, - mode: str = "vectors_only", - wait: bool = True, - dry_run: bool = False, - ) -> Dict[str, Any]: - """Reindex semantic/vector artifacts for a URI.""" - ... - - # ============= File System ============= - - @abstractmethod - async def ls( - self, - uri: str, - simple: bool = False, - recursive: bool = False, - output: str = "original", - abs_limit: int = 256, - show_all_hidden: bool = False, - node_limit: int = 1000, - sort_by: Optional[str] = None, - sort_order: str = "asc", - ) -> List[Any]: - """List directory contents.""" - ... - - @abstractmethod - async def tree( - self, - uri: str, - output: str = "original", - abs_limit: int = 128, - show_all_hidden: bool = False, - node_limit: int = 1000, - ) -> List[Dict[str, Any]]: - """Get directory tree.""" - ... - - @abstractmethod - async def stat(self, uri: str) -> Dict[str, Any]: - """Get resource status.""" - ... - - @abstractmethod - async def mkdir(self, uri: str, description: Optional[str] = None) -> None: - """Create directory.""" - ... - - @abstractmethod - async def rm( - self, - uri: str, - recursive: bool = False, - wait: bool = False, - timeout: Optional[float] = None, - ) -> None: - """Remove resource.""" - ... - - @abstractmethod - async def mv(self, from_uri: str, to_uri: str) -> None: - """Move resource.""" - ... - - # ============= Content Reading ============= - - @abstractmethod - async def read(self, uri: str, offset: int = 0, limit: int = -1) -> str: - """Read file content (L2). - - Args: - uri: Viking URI - offset: Starting line number (0-indexed). Default 0. - limit: Number of lines to read. -1 means read to end. Default -1. - """ - ... - - @abstractmethod - async def abstract(self, uri: str) -> str: - """Read L0 abstract (.abstract.md).""" - ... - - @abstractmethod - async def overview(self, uri: str) -> str: - """Read L1 overview (.overview.md).""" - ... - - @abstractmethod - async def write( - self, - uri: str, - content: str, - mode: str = "replace", - wait: bool = False, - timeout: Optional[float] = None, - telemetry: TelemetryRequest = False, - ) -> Dict[str, Any]: - """Write text content to an existing file and refresh semantics/vectors.""" - ... - - @abstractmethod - async def set_tags( - self, - uri: str, - tags: List[str], - mode: str = "replace", - recursive: bool = False, - telemetry: TelemetryRequest = False, - ) -> Dict[str, Any]: - """Update explicit retrieval tags metadata for a file or directory.""" - ... - - # ============= Search ============= - - @abstractmethod - async def find( - self, - query: str = "", - target_uri: Union[str, List[str]] = "", - limit: int = 10, - score_threshold: Optional[float] = None, - filter: Optional[Dict] = None, - context_type: Optional[SearchContextTypeInput] = None, - tags: Optional[List[str]] = None, - telemetry: TelemetryRequest = False, - image: Optional[Any] = None, - ) -> Any: - """Semantic search without session context.""" - ... - - @abstractmethod - async def search( - self, - query: str = "", - target_uri: Union[str, List[str]] = "", - session_id: Optional[str] = None, - limit: int = 10, - score_threshold: Optional[float] = None, - filter: Optional[Dict] = None, - context_type: Optional[SearchContextTypeInput] = None, - tags: Optional[List[str]] = None, - telemetry: TelemetryRequest = False, - image: Optional[Any] = None, - ) -> Any: - """Semantic search with optional session context.""" - ... - - @abstractmethod - async def grep( - self, - uri: str, - pattern: str, - case_insensitive: bool = False, - exclude_uri: Optional[str] = None, - node_limit: Optional[int] = None, - level_limit: int = 5, - ) -> Dict[str, Any]: - """Content search with pattern.""" - ... - - @abstractmethod - async def glob(self, pattern: str, uri: str = "viking://") -> Dict[str, Any]: - """File pattern matching.""" - ... - - # ============= Relations ============= - - @abstractmethod - async def relations(self, uri: str) -> List[Dict[str, Any]]: - """Get relations for a resource.""" - ... - - @abstractmethod - async def link(self, from_uri: str, to_uris: Union[str, List[str]], reason: str = "") -> None: - """Create link between resources.""" - ... - - @abstractmethod - async def unlink(self, from_uri: str, to_uri: str) -> None: - """Remove link between resources.""" - ... - - # ============= Sessions ============= - - @abstractmethod - async def create_session( - self, - session_id: Optional[str] = None, - telemetry: TelemetryRequest = False, - memory_policy: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: - """Create a new session. - - Args: - session_id: Optional session ID. If provided, creates a session with the given ID. - If None, creates a new session with auto-generated ID. - telemetry: Whether to attach operation telemetry data to the result. - memory_policy: Optional default memory extraction policy. - """ - ... - - @abstractmethod - async def list_sessions(self) -> List[Dict[str, Any]]: - """List all sessions.""" - ... - - @abstractmethod - async def get_session(self, session_id: str, *, auto_create: bool = False) -> Dict[str, Any]: - """Get session details.""" - ... - - @abstractmethod - async def get_session_context( - self, session_id: str, token_budget: int = 128_000 - ) -> Dict[str, Any]: - """Get assembled session context for a session.""" - ... - - @abstractmethod - async def get_session_archive(self, session_id: str, archive_id: str) -> Dict[str, Any]: - """Get one completed archive for a session.""" - ... - - @abstractmethod - async def delete_session(self, session_id: str) -> None: - """Delete a session.""" - ... - - @abstractmethod - async def commit_session( - self, - session_id: str, - telemetry: TelemetryRequest = False, - *, - keep_recent_count: int = 0, - retention_mode: str | None = None, - keep_recent_turn_count: int | None = None, - retained_message_token_budget: int | None = None, - min_raw_tail_steps: int | None = None, - ) -> Dict[str, Any]: - """Commit a session (archive and extract memories). - - Args: - session_id: Session ID - telemetry: Whether to attach operation telemetry data to the result. - keep_recent_count: Number of recent live messages to retain after commit. - """ - ... - - @abstractmethod - async def add_message( - self, - session_id: str, - role: str, - content: str | None = None, - parts: list[dict] | None = None, - created_at: str | None = None, - peer_id: str | None = None, - telemetry: TelemetryRequest = False, - turn_id: str | None = None, - message_kind: str | None = None, - source_message_ids: list[str] | None = None, - ) -> Dict[str, Any]: - """Add a message to a session. - - Args: - session_id: Session ID - role: Message role ("user" or "assistant") - content: Text content (simple mode) - parts: Parts array (full Part support: TextPart, ContextPart, ImagePart, ToolPart) - created_at: Message creation time (ISO format string) - peer_id: Optional stable interaction peer identity. - telemetry: Whether to attach operation telemetry data to the result. - - If both content and parts are provided, parts takes precedence. - """ - ... - - @abstractmethod - async def batch_add_messages( - self, - session_id: str, - messages: list[dict], - telemetry: TelemetryRequest = False, - ) -> Dict[str, Any]: - """Add multiple messages to a session in a single request. - - Args: - session_id: Session ID - messages: List of message dicts, each with "role" and optionally - "content", "parts", "created_at", "peer_id". - telemetry: Whether to attach operation telemetry data to the result. - - Returns: - Result dict with session_id, message_count, and added count. - """ - ... - - @abstractmethod - async def get_task(self, task_id: str) -> Optional[Dict[str, Any]]: - """Query background task status.""" - ... - - @abstractmethod - async def cancel_task(self, task_id: str) -> Optional[Dict[str, Any]]: - """Cancel a background task.""" - ... - - @abstractmethod - async def list_tasks( - self, - task_type: Optional[str] = None, - status: Optional[str] = None, - resource_id: Optional[str] = None, - limit: int = 50, - ) -> List[Dict[str, Any]]: - """List background tasks visible to the current caller.""" - ... - - # ============= Pack ============= - - @abstractmethod - async def export_ovpack(self, uri: str, to: str, include_vectors: bool = False) -> str: - """Export as .ovpack file.""" - ... - - @abstractmethod - async def backup_ovpack(self, to: str, include_vectors: bool = False) -> str: - """Back up public scopes as a restore-only .ovpack file.""" - ... - - @abstractmethod - async def import_ovpack( - self, - file_path: str, - parent: str, - on_conflict: Optional[str] = None, - vector_mode: Optional[str] = None, - ) -> str: - """Import .ovpack file.""" - ... - - @abstractmethod - async def restore_ovpack( - self, - file_path: str, - on_conflict: Optional[str] = None, - vector_mode: Optional[str] = None, - ) -> str: - """Restore backup .ovpack file.""" - ... - - # ============= Debug ============= - - @abstractmethod - async def check_consistency(self, uri: str) -> Dict[str, Any]: - """Check filesystem/vector-index consistency for a URI subtree.""" - ... - - @abstractmethod - async def health(self) -> bool: - """Quick health check.""" - ... - - @abstractmethod - def session(self, session_id: Optional[str] = None, must_exist: bool = False) -> Any: - """Create a new session or load an existing one. - - Args: - session_id: Session ID, creates a new session if None - must_exist: If True and session_id is provided, raises NotFoundError - when the session does not exist instead of silently - returning a fresh empty session. - If session_id is None, must_exist is ignored. - - Returns: - Session object - - Raises: - NotFoundError: If must_exist=True and the session does not exist. - """ - ... - - @abstractmethod - async def session_exists(self, session_id: str) -> bool: - """Check whether a session exists in storage. - - Args: - session_id: Session ID to check - - Returns: - True if the session exists, False otherwise - """ - ... - - @abstractmethod - def get_status(self) -> Any: - """Get system status. - - Returns: - SystemStatus or Dict containing health status of all components. - """ - ... - - @abstractmethod - def is_healthy(self) -> bool: - """Quick health check (synchronous). - - Returns: - True if all components are healthy, False otherwise. - """ - ... - - @property - @abstractmethod - def observer(self) -> Any: - """Get observer service for component status.""" - ... - - # ============= Git Version Control ============= - - @abstractmethod - async def git_commit( - self, - *, - message: str, - paths: Optional[List[str]] = None, - branch: str = "main", - author_name: Optional[str] = None, - author_email: Optional[str] = None, - ) -> Dict[str, Any]: - """Create a git snapshot. See VikingFS.commit for semantics.""" - - @abstractmethod - async def git_restore( - self, - *, - project_dir: Optional[str] = None, - source_commit: str, - branch: str = "main", - dry_run: bool = False, - message: Optional[str] = None, - author_name: Optional[str] = None, - author_email: Optional[str] = None, - ) -> Dict[str, Any]: - """Restore a subtree, or the full account tree when project_dir is omitted.""" - - @abstractmethod - async def git_show( - self, - target_ref: str, - *, - path: Optional[str] = None, - ) -> Any: - """Read a commit's metadata or a single blob.""" - - @abstractmethod - async def git_log( - self, - *, - branch: str = "main", - limit: int = 20, - paths: Optional[List[str]] = None, - ) -> List[Dict[str, Any]]: - """Walk back along parents[0] up to limit commits.""" - - async def git_diff( - self, - path: str, - *, - to_ref: str, - from_ref: Optional[str] = None, - ) -> Dict[str, Any]: - """Compare one file between two snapshot refs. - - The default keeps third-party subclasses written against older - OpenViking releases instantiable while making unsupported use explicit. - """ - raise NotImplementedError("snapshot diff is not supported by this client") - - @abstractmethod - async def git_get_ignore(self) -> str: - """Return the account .ovgitignore content (empty string if absent).""" - - @abstractmethod - async def git_set_ignore(self, *, content: str) -> None: - """Write the account .ovgitignore control file.""" - - @abstractmethod - async def git_delete_ignore(self) -> None: - """Delete the account .ovgitignore control file (missing is success).""" diff --git a/openviking_cli/utils/config/open_viking_config.py b/openviking_cli/utils/config/open_viking_config.py index 65363b9e16..23762b87bd 100644 --- a/openviking_cli/utils/config/open_viking_config.py +++ b/openviking_cli/utils/config/open_viking_config.py @@ -665,7 +665,7 @@ def initialize_openviking_config( Args: user: UserIdentifier for session management - path: Local storage path (workspace) for embedded mode + path: Optional local workspace override for the service Returns: Configured OpenVikingConfig instance @@ -683,7 +683,7 @@ def initialize_openviking_config( # Configure storage based on provided parameters if path: - # Embedded mode: local storage + # Explicit local workspace override config.storage.agfs.backend = config.storage.agfs.backend or "local" config.storage.vectordb.backend = config.storage.vectordb.backend or "local" # Resolve and update workspace + dependent paths (model_validator won't diff --git a/sdk/go/README.md b/sdk/go/README.md index 619a60da35..0cb44a869f 100644 --- a/sdk/go/README.md +++ b/sdk/go/README.md @@ -56,8 +56,7 @@ derives account and user identity from the key. Set `Account` and `User` only for trusted deployments or gateways where the upstream explicitly forwards tenant identity through OpenViking headers. -This SDK is HTTP-only. It does not implement Python embedded mode or legacy -`agent_id` compatibility. +This SDK does not implement legacy `agent_id` compatibility. ## Common Operations @@ -136,7 +135,6 @@ Not implemented in Go SDK v1: | Area | Reason | |------|--------| -| Python embedded mode | Go SDK is HTTP-only. | | Legacy `agent_id` compatibility | New SDKs use `ActorPeerID` only. | | Privacy config routes | Server-only management surface today; not in Python HTTP client. | | Metrics endpoint | Prometheus text scrape endpoint, not a JSON SDK API. | diff --git a/sdk/go/README_CN.md b/sdk/go/README_CN.md index 784eb01327..e2c3ceee24 100644 --- a/sdk/go/README_CN.md +++ b/sdk/go/README_CN.md @@ -31,7 +31,7 @@ Go SDK 发送的身份请求头与 Python HTTP client 一致: 普通 `api_key` 部署下只需要设置 `APIKey`,服务端会从 API key 推导 account/user 身份。只有在 trusted 部署或网关显式透传租户身份时,才需要设置 `Account` 和 `User`。 -Go SDK 仅支持 HTTP 模式,不支持 Python embedded 模式,也不保留旧 `agent_id` 兼容路径。 +Go SDK 不保留旧 `agent_id` 兼容路径。 ## 图片检索示例 @@ -82,7 +82,6 @@ Go SDK v1 的边界是对齐当前 Python HTTP client,不覆盖所有 server | 模块 | 原因 | |------|------| -| Python embedded 模式 | Go SDK 是纯 HTTP SDK。 | | 旧 `agent_id` 兼容 | 新 SDK 只使用 `ActorPeerID`。 | | Privacy config 路由 | 当前属于 server-only 管理面,Python HTTP client 未公开。 | | Metrics endpoint | Prometheus 文本抓取端点,不是标准 JSON SDK API。 | diff --git a/sdk/python/tests/test_main_package_exports.py b/sdk/python/tests/test_main_package_exports.py index ef8c2332da..36bb14f1db 100644 --- a/sdk/python/tests/test_main_package_exports.py +++ b/sdk/python/tests/test_main_package_exports.py @@ -26,6 +26,9 @@ def test_openviking_top_level_exports_http_clients(): assert openviking.AsyncHTTPClient is LegacyAsyncHTTPClient assert openviking.SyncHTTPClient is LegacySyncHTTPClient + assert not hasattr(openviking, "AsyncOpenViking") + assert not hasattr(openviking, "SyncOpenViking") + assert not hasattr(openviking, "OpenViking") def test_openviking_client_module_exports_http_clients(): @@ -56,12 +59,11 @@ def test_openviking_client_module_can_fallback_to_repo_local_sdk(): sys.path[:] = original_sys_path -def test_openviking_client_module_import_is_lazy_for_local_client_stack(): +def test_openviking_client_module_does_not_import_service_stack(): _purge_openviking_modules() import openviking.client as client_module - assert "openviking.client.local" not in sys.modules assert "openviking.service" not in sys.modules assert client_module.AsyncHTTPClient.__module__ == "openviking_cli.client._http_compat" diff --git a/tests/README.md b/tests/README.md index 2098b953d4..f05522ec2b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -50,36 +50,36 @@ pytest tests/client tests/server tests/session tests/vectordb tests/misc tests/i ```bash # Run a specific test module -pytest tests/client/test_lifecycle.py -v +pytest tests/client/test_http_client_config.py -v # Run a specific test class -pytest tests/client/test_lifecycle.py::TestClientInitialization -v +pytest tests/client/test_http_client_config.py::test_async_http_client_explicit_values_override_ovcli_config -v # Run a specific test function -pytest tests/client/test_lifecycle.py::TestClientInitialization::test_initialize_success -v +pytest tests/client/test_http_client_config.py -v # Run tests matching a keyword pytest tests/ -k "lifecycle" -v pytest tests/ -k "initialize" -v # Run tests with print output visible -pytest tests/client/test_lifecycle.py -v -s +pytest tests/client/test_http_client_config.py -v -s ``` ### Common Test Scenarios ```bash -# Test client lifecycle (init, close, reset) -pytest tests/client/test_lifecycle.py -v +# Test HTTP client configuration +pytest tests/client/test_http_client_config.py -v # Test resource add and processing -pytest tests/client/test_resource_management.py -v +pytest tests/server/test_api_resources.py -v # Test skill management -pytest tests/client/test_skill_management.py -v +pytest tests/server/test_api_skills.py -v # Test semantic search -pytest tests/client/test_search.py -v +pytest tests/server/test_api_search.py -v # Test server HTTP API pytest tests/server/ -v @@ -111,18 +111,15 @@ make ### client/ -Tests for the OpenViking client API (`AsyncOpenViking` / `SyncOpenViking`). +Tests for the Python HTTP client API. | File | Description | Key Test Cases | |------|-------------|----------------| -| `test_lifecycle.py` | Client lifecycle management | `initialize()` success and idempotency, `close()` cleanup, `reset()` singleton clearing, embedded mode singleton behavior | -| `test_resource_management.py` | Resource operations | `add_resource()` with sync/async modes, custom target URI, file not found handling; `wait_processed()` for single and batch resources | -| `test_skill_management.py` | Skill operations | `add_skill()` from SKILL.md file, YAML string, MCP tool dict, skill directory with auxiliary files; skill search | -| `test_filesystem.py` | Virtual filesystem | `ls()` with simple/recursive modes; `read()` file content; `abstract()` L0 summary; `overview()` L1 overview; `tree()` directory structure | -| `test_search.py` | Semantic search | `find()` fast vector search with limit/threshold/target_uri; `search()` with intent analysis and session context | -| `test_relations.py` | Resource linking | `link()` single/multiple URIs with reason; `unlink()` existing/nonexistent; `relations()` query | -| `test_file_operations.py` | File manipulation | `rm()` file/directory with recursive; `mv()` rename/move; `grep()` content search with case sensitivity; `glob()` pattern matching | -| `test_import_export.py` | Import/Export | `export_ovpack()` file/directory; `import_ovpack()` with conflict policy; roundtrip verification | +| `test_http_client_config.py` | Connection and identity configuration | URL, API key, headers, timeout, and compatibility behavior | +| `test_http_client_local_upload.py` | Local uploads | File and directory upload behavior | +| `test_http_client_snapshot.py` | Snapshot operations | Snapshot namespace and response handling | +| `test_http_error_mapping.py` | Error mapping | Server, network, timeout, and conflict errors | +| `test_rebuild_clients.py` | Reindex and message operations | Async and sync HTTP request forwarding | ### server/ diff --git a/tests/client/test_add_resource_signature_compat.py b/tests/client/test_add_resource_signature_compat.py deleted file mode 100644 index a339abc8de..0000000000 --- a/tests/client/test_add_resource_signature_compat.py +++ /dev/null @@ -1,139 +0,0 @@ -import inspect - -from openviking_sdk.client import AsyncHTTPClient, SyncHTTPClient - -from openviking import AsyncOpenViking, SyncOpenViking -from openviking.client.local import LocalClient - - -def test_python_add_resource_clients_accept_add_type_keyword(): - for client_type in ( - AsyncOpenViking, - SyncOpenViking, - LocalClient, - AsyncHTTPClient, - SyncHTTPClient, - ): - parameters = inspect.signature(client_type.add_resource).parameters - assert "add_type" in parameters, client_type.__name__ - - -def test_async_openviking_add_resource_preserves_positional_watch_args(): - bound = inspect.signature(AsyncOpenViking.add_resource).bind_partial( - object(), - "doc.md", - None, - None, - "", - "", - False, - None, - True, - False, - 1440, - {"site": True}, - False, - ) - - assert bound.arguments["watch_interval"] == 1440 - assert bound.arguments["args"] == {"site": True} - assert bound.arguments["telemetry"] is False - assert "processing_mode" not in bound.arguments - - -def test_sync_openviking_add_resource_preserves_positional_args_and_telemetry(): - bound = inspect.signature(SyncOpenViking.add_resource).bind_partial( - object(), - "doc.md", - None, - None, - "", - "", - False, - None, - True, - False, - {"site": True}, - False, - ) - - assert bound.arguments["args"] == {"site": True} - assert bound.arguments["telemetry"] is False - assert "processing_mode" not in bound.arguments - - -def test_local_client_add_resource_preserves_positional_watch_args(): - bound = inspect.signature(LocalClient.add_resource).bind_partial( - object(), - "doc.md", - None, - None, - "", - "", - False, - None, - True, - False, - False, - 1440, - {"site": True}, - ) - - assert bound.arguments["telemetry"] is False - assert bound.arguments["watch_interval"] == 1440 - assert bound.arguments["args"] == {"site": True} - assert "processing_mode" not in bound.arguments - - -def test_async_http_client_add_resource_preserves_positional_watch_args(): - bound = inspect.signature(AsyncHTTPClient.add_resource).bind_partial( - object(), - "doc.md", - None, - None, - "", - "", - False, - None, - False, - None, - None, - None, - True, - None, - 1440, - {"site": True}, - False, - ) - - assert bound.arguments["watch_interval"] == 1440 - assert bound.arguments["args"] == {"site": True} - assert bound.arguments["telemetry"] is False - assert "processing_mode" not in bound.arguments - - -def test_sync_http_client_add_resource_preserves_positional_watch_args(): - bound = inspect.signature(SyncHTTPClient.add_resource).bind_partial( - object(), - "doc.md", - None, - None, - "", - "", - False, - None, - False, - None, - None, - None, - True, - None, - 1440, - {"site": True}, - False, - ) - - assert bound.arguments["watch_interval"] == 1440 - assert bound.arguments["args"] == {"site": True} - assert bound.arguments["telemetry"] is False - assert "processing_mode" not in bound.arguments diff --git a/tests/client/test_base_client_compatibility.py b/tests/client/test_base_client_compatibility.py deleted file mode 100644 index 687d42e3e3..0000000000 --- a/tests/client/test_base_client_compatibility.py +++ /dev/null @@ -1,11 +0,0 @@ -from openviking_cli.client.base import BaseClient - - -def test_legacy_base_client_subclass_without_git_diff_remains_instantiable(): - async def noop(self, *args, **kwargs): - return None - - implementations = {name: noop for name in BaseClient.__abstractmethods__ if name != "git_diff"} - legacy_client_type = type("LegacyClient", (BaseClient,), implementations) - - legacy_client_type() diff --git a/tests/client/test_file_operations.py b/tests/client/test_file_operations.py deleted file mode 100644 index 6d2edb64c1..0000000000 --- a/tests/client/test_file_operations.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 - -"""File operation tests""" - -from pathlib import Path - -import pytest - -from openviking import AsyncOpenViking - - -class TestRm: - """Test rm delete operation""" - - async def test_rm_file(self, client: AsyncOpenViking, sample_markdown_file: Path): - """Test deleting file""" - # Add resource first - print(f"Add resource: {sample_markdown_file}") - result = await client.add_resource( - path=str(sample_markdown_file), - reason="Test rm", - ) - - uris = await client.tree(result["root_uri"]) - for data in uris: - if not data["isDir"]: - await client.rm(data["uri"]) - with pytest.raises(Exception): # noqa: B017 - await client.read(data["uri"]) - - async def test_rm_directory_recursive(self, client: AsyncOpenViking, sample_directory: Path): - """Test recursive directory deletion""" - # Add files from directory first - for f in sample_directory.glob("**/*.txt"): - await client.add_resource(path=str(f), reason="Test rm dir") - - entries = await client.ls("viking://resources/") - for data in entries: - if data["isDir"]: - dir_uri = data["uri"] - await client.rm(dir_uri, recursive=True) - with pytest.raises(Exception): # noqa: B017 - await client.stat(dir_uri) - - -class TestMv: - """Test mv move operation""" - - async def test_mv_file(self, client: AsyncOpenViking, sample_markdown_file: Path): - """Test moving file""" - # Add resource first - result = await client.add_resource( - path=str(sample_markdown_file), - reason="Test mv", - ) - uri = result["root_uri"] - new_uri = "viking://resources/moved/" - await client.mv(uri, new_uri) - # Verify original location does not exist - with pytest.raises(Exception): # noqa: B017 - await client.stat(uri) - - await client.stat(new_uri) - - -class TestGrep: - """Test grep content search""" - - async def test_grep_basic(self, client_with_resource): - """Test basic content search""" - client, uri = client_with_resource - - result = await client.grep(uri, pattern="Sample") - - assert isinstance(result, dict) - - assert "matches" in result and result["count"] > 0 - - async def test_grep_case_insensitive(self, client_with_resource): - """Test case insensitive search""" - client, uri = client_with_resource - - result = await client.grep(uri, pattern="SAMPLE", case_insensitive=True) - print(result) - assert isinstance(result, dict) - assert "matches" in result and result["count"] > 0 - - async def test_grep_no_match(self, client_with_resource): - """Test no matching results""" - client, uri = client_with_resource - - result = await client.grep(uri, pattern="nonexistent_pattern_xyz123") - assert isinstance(result, dict) - matches = result.get("matches", []) - assert len(matches) == 0 - - -class TestGlob: - """Test glob file pattern matching""" - - async def test_glob_basic(self, client_with_resource): - """Test basic pattern matching""" - client, _ = client_with_resource - - result = await client.glob(pattern="**/*.md") - assert isinstance(result, dict) - assert "matches" in result and result["count"] > 0 - - async def test_glob_with_uri(self, client_with_resource): - """Test pattern matching with specified URI""" - client, uri = client_with_resource - parent_uri = "/".join(uri.split("/")[:-1]) + "/" - - result = await client.glob(pattern="**/*.md", uri=parent_uri) - assert isinstance(result, dict) - assert "matches" in result and result["count"] > 0 - - async def test_glob_txt_files(self, client: AsyncOpenViking, sample_text_file: Path): - """Test matching txt files""" - # Add txt file - await client.add_resource( - path=str(sample_text_file), - reason="Test glob txt", - ) - - result = await client.glob(pattern="**/*.md") - assert isinstance(result, dict) and result["count"] > 0 diff --git a/tests/client/test_filesystem.py b/tests/client/test_filesystem.py deleted file mode 100644 index 966d87b760..0000000000 --- a/tests/client/test_filesystem.py +++ /dev/null @@ -1,206 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 - -"""Filesystem operation tests""" - -from types import SimpleNamespace -from unittest.mock import AsyncMock - -import pytest - -from openviking import AsyncOpenViking, OpenViking -from openviking.client import LocalClient -from openviking.server.identity import RequestContext, Role -from openviking.telemetry import get_current_telemetry -from openviking_cli.session.user_id import UserIdentifier - - -class TestLs: - """Test ls operation""" - - async def test_ls_directory(self, client_with_resource): - """Test listing directory contents""" - client, uri = client_with_resource - # Get parent directory - parent_uri = "/".join(uri.split("/")[:-1]) + "/" - - entries = await client.ls(parent_uri) - - assert isinstance(entries, list) - assert len(entries) > 0 - - async def test_ls_simple_mode(self, client_with_resource): - """Test simple mode listing returns non-empty URI strings (fixes #218)""" - client, uri = client_with_resource - parent_uri = "/".join(uri.split("/")[:-1]) + "/" - - entries = await client.ls(parent_uri, simple=True) - - assert isinstance(entries, list) - assert all(isinstance(e, str) for e in entries) - assert all(e.startswith("viking://") for e in entries) - - async def test_ls_recursive(self, client_with_resource): - """Test recursive listing""" - client, _ = client_with_resource - - entries = await client.ls("viking://", recursive=True) - - assert isinstance(entries, list) - - async def test_ls_root(self, client: AsyncOpenViking): - """Test listing root directory""" - entries = await client.ls("viking://") - - assert isinstance(entries, list) - - -class TestRead: - """Test read operation""" - - async def test_read_file(self, client_with_resource): - """Test reading file content""" - client, uri = client_with_resource - entries = await client.tree(uri) - content = "" - for e in entries: - if not e["isDir"]: - content = await client.read(e["uri"]) - assert isinstance(content, str) - assert len(content) > 0 - assert "Sample Document" in content - - async def test_read_nonexistent_file(self, client: AsyncOpenViking): - """Test reading nonexistent file""" - with pytest.raises(Exception): # noqa: B017 - await client.read("viking://nonexistent/file.txt") - - async def test_write_with_wait_returns_queue_status(self): - """Test local SDK write(wait=True) preserves queue_status and binds telemetry.""" - queue_status = { - "Semantic": {"processed": 1, "error_count": 0, "errors": []}, - "Embedding": {"processed": 0, "error_count": 0, "errors": []}, - } - seen: dict[str, object] = {} - - async def _fake_write(**kwargs): - telemetry = get_current_telemetry() - seen["enabled"] = telemetry.enabled - seen["telemetry_id"] = telemetry.telemetry_id - seen["kwargs"] = kwargs - return {"uri": kwargs["uri"], "queue_status": queue_status} - - client = LocalClient.__new__(LocalClient) - client._ctx = RequestContext(user=UserIdentifier.the_default_user(), role=Role.USER) - client._service = SimpleNamespace(fs=SimpleNamespace(write=_fake_write)) - - result = await LocalClient.write( - client, - uri="viking://resources/demo.md", - content="Updated from client test", - wait=True, - telemetry=False, - ) - - assert result["uri"] == "viking://resources/demo.md" - assert result["queue_status"] == queue_status - assert seen["enabled"] is True - assert str(seen["telemetry_id"]).startswith("tm_") - assert seen["kwargs"]["wait"] is True - - -class TestAbstract: - """Test abstract operation""" - - async def test_abstract_directory(self, client_with_resource): - """Test reading directory abstract""" - client, uri = client_with_resource - # Get parent directory - parent_uri = "/".join(uri.split("/")[:-1]) + "/" - - abstract = await client.abstract(parent_uri) - - assert isinstance(abstract, str) - - -class TestOverview: - """Test overview operation""" - - async def test_overview_directory(self, client_with_resource): - """Test reading directory overview""" - client, uri = client_with_resource - parent_uri = "/".join(uri.split("/")[:-1]) + "/" - - overview = await client.overview(parent_uri) - - assert isinstance(overview, str) - - -class TestTree: - """Test tree operation""" - - async def test_tree_success(self, client_with_resource): - """Test getting directory tree""" - client, _ = client_with_resource - - tree = await client.tree("viking://") - - assert isinstance(tree, (list, dict)) - - async def test_tree_specific_directory(self, client_with_resource): - """Test getting tree of specific directory""" - client, uri = client_with_resource - parent_uri = "/".join(uri.split("/")[:-1]) + "/" - - tree = await client.tree(parent_uri) - - assert isinstance(tree, (list, dict)) - - -async def test_local_client_mkdir_forwards_description(): - client = LocalClient.__new__(LocalClient) - client._ctx = RequestContext(user=UserIdentifier.the_default_user(), role=Role.USER) - client._service = SimpleNamespace(fs=SimpleNamespace(mkdir=AsyncMock())) - - await LocalClient.mkdir( - client, - "viking://resources/demo-dir/", - description="Demo directory", - ) - - client._service.fs.mkdir.assert_awaited_once_with( - "viking://resources/demo-dir/", - ctx=client._ctx, - description="Demo directory", - ) - - -async def test_sync_openviking_write_updates_existing_file(test_data_dir, sample_markdown_file): - """Sync OpenViking exposes write() and delegates to the async client.""" - await AsyncOpenViking.reset() - client = OpenViking(path=str(test_data_dir)) - - try: - client._async_client.write = AsyncMock(return_value={"uri": "viking://resources/demo.md"}) - - write_result = client.write( - "viking://resources/demo.md", - "updated content", - mode="append", - wait=True, - timeout=3.0, - telemetry=False, - ) - - assert write_result == {"uri": "viking://resources/demo.md"} - client._async_client.write.assert_awaited_once_with( - uri="viking://resources/demo.md", - content="updated content", - mode="append", - wait=True, - timeout=3.0, - telemetry=False, - ) - finally: - client.close() - await AsyncOpenViking.reset() diff --git a/tests/client/test_git_versioning.py b/tests/client/test_git_versioning.py deleted file mode 100644 index 4a8ba850ee..0000000000 --- a/tests/client/test_git_versioning.py +++ /dev/null @@ -1,416 +0,0 @@ -"""End-to-end tests for the OpenViking.snapshot namespace. - -These exercise the user-facing namespace path: -OpenViking -> LocalClient -> FSService -> VikingFS -> RAGFSBindingClient -> Rust GitService. -""" - -from __future__ import annotations - -import re -import shutil -import tempfile -from pathlib import Path -from typing import NamedTuple, Tuple -from unittest.mock import MagicMock - -import pytest - -from openviking.async_client import AsyncOpenViking -from openviking.client.local import LocalClient -from openviking.pyagfs.exceptions import AGFSNotFoundError, AGFSNotSupportedError -from openviking.server.identity import RequestContext, Role -from openviking.service.fs_service import FSService -from openviking.storage.viking_fs import VikingFS -from openviking.sync_client import SyncOpenViking -from openviking_cli.exceptions import InvalidURIError -from openviking_cli.session.user_id import UserIdentifier - -ragfs_python = pytest.importorskip("ragfs_python") - - -OID_RE = re.compile(r"^[0-9a-f]{40}$") -DEFAULT_AUTHOR_NAME = VikingFS._DEFAULT_GIT_AUTHOR_NAME - - -class ClientHarness(NamedTuple): - client: SyncOpenViking - async_client: AsyncOpenViking - vfs: VikingFS - ctx: RequestContext - - -def _make_ctx(account: str = "acct_t", user: str = "user1") -> RequestContext: - return RequestContext(user=UserIdentifier(account, user), role=Role.ROOT) - - -def _write_workspace(tmp_root: Path) -> Tuple[Path, Path]: - """Create ragfs config and backing localfs root for git-enabled tests.""" - fs_root = tmp_root / "fs" - git_root = tmp_root / "git" - fs_root.mkdir(parents=True, exist_ok=True) - git_root.mkdir(parents=True, exist_ok=True) - cfg = tmp_root / "ragfs.toml" - cfg.write_text( - f""" -[git] -enabled = true -backend = "local" -default_branch = "main" -author_name = "test-bot" -author_email = "test@example.com" - -[git.local] -base_dir = "{git_root}" -""" - ) - return cfg, fs_root - - -def _write_disabled_workspace(tmp_root: Path) -> Tuple[Path, Path]: - fs_root = tmp_root / "fs" - fs_root.mkdir(parents=True, exist_ok=True) - cfg = tmp_root / "ragfs.toml" - cfg.write_text( - """ -[git] -enabled = false -""" - ) - return cfg, fs_root - - -def _build_binding_client(config_path: Path, fs_root: Path): - client = ragfs_python.RAGFSBindingClient(git_config_path=str(config_path)) - client.mount("localfs", "/local", {"local_dir": str(fs_root)}) - return client - - -def _build_harness(config_path: Path, fs_root: Path) -> ClientHarness: - ctx = _make_ctx() - binding_client = _build_binding_client(config_path, fs_root) - vfs = VikingFS(agfs=binding_client) - - fs_service = FSService() - fs_service.set_dependencies(viking_fs=vfs) - - local_client = object.__new__(LocalClient) - local_client._service = MagicMock() - local_client._service.fs = fs_service - local_client._ctx = ctx - - async_client = object.__new__(AsyncOpenViking) - async_client._client = local_client - async_client._initialized = True - async_client._singleton_initialized = True - async_client._snapshot = None - - sync_client = object.__new__(SyncOpenViking) - sync_client._async_client = async_client - sync_client._initialized = True - sync_client._snapshot = None - - return ClientHarness( - client=sync_client, - async_client=async_client, - vfs=vfs, - ctx=ctx, - ) - - -@pytest.fixture -def workspace(): - root = Path(tempfile.mkdtemp(prefix="ov-client-git-")) - try: - yield root - finally: - shutil.rmtree(root, ignore_errors=True) - - -@pytest.fixture -def git_harness(workspace) -> ClientHarness: - cfg, fs_root = _write_workspace(workspace) - try: - yield _build_harness(cfg, fs_root) - finally: - pass - - -@pytest.fixture -def git_disabled_harness(workspace) -> ClientHarness: - cfg, fs_root = _write_disabled_workspace(workspace) - try: - yield _build_harness(cfg, fs_root) - finally: - pass - - -async def test_write_commit_show_roundtrip(git_harness): - await git_harness.vfs.write_file( - "viking://resources/a.md", - b"hello", - ctx=git_harness.ctx, - ) - - commit = git_harness.client.snapshot.commit( - message="initial", - paths=["viking://resources/a.md"], - ) - - assert commit["result"] == "created" - assert OID_RE.match(commit["commit_oid"]) - assert ( - git_harness.client.snapshot.show( - "main", - path="viking://resources/a.md", - ) - == b"hello" - ) - - -async def test_show_metadata_without_path(git_harness): - await git_harness.vfs.write_file( - "viking://resources/meta.md", - b"metadata", - ctx=git_harness.ctx, - ) - commit = git_harness.client.snapshot.commit( - message="metadata commit", - paths=["viking://resources/meta.md"], - ) - - metadata = git_harness.client.snapshot.show("main") - - assert metadata["oid"] == commit["commit_oid"] - assert metadata["message"].startswith("metadata commit") - assert metadata["author"]["name"] == DEFAULT_AUTHOR_NAME - assert metadata["parents"] == [] - - -async def test_log_walks_parents(git_harness): - commits = [] - for idx, body in enumerate((b"v1", b"v2", b"v3"), start=1): - await git_harness.vfs.write_file( - "viking://resources/log.md", - body, - ctx=git_harness.ctx, - ) - commits.append( - git_harness.client.snapshot.commit( - message=f"c{idx}", - paths=["viking://resources/log.md"], - ) - ) - - history = git_harness.client.snapshot.log(limit=10) - limited = git_harness.client.snapshot.log(limit=2) - - assert [item["oid"] for item in history] == [ - commits[2]["commit_oid"], - commits[1]["commit_oid"], - commits[0]["commit_oid"], - ] - assert [item["oid"] for item in limited] == [ - commits[2]["commit_oid"], - commits[1]["commit_oid"], - ] - - -async def test_log_filters_multiple_paths_end_to_end(git_harness): - target_uri = "viking://resources/log_paths/a.md" - directory_uri = "viking://resources/log_paths/docs" - child_uri = f"{directory_uri}/guide.md" - unrelated_uri = "viking://resources/log_paths_other.md" - - await git_harness.vfs.write_file(target_uri, b"target", ctx=git_harness.ctx) - target_commit = git_harness.client.snapshot.commit( - message="add target", - paths=[target_uri], - ) - - await git_harness.vfs.write_file(unrelated_uri, b"unrelated", ctx=git_harness.ctx) - git_harness.client.snapshot.commit( - message="add unrelated", - paths=[unrelated_uri], - ) - - await git_harness.vfs.write_file(child_uri, b"guide", ctx=git_harness.ctx) - directory_commit = git_harness.client.snapshot.commit( - message="add directory child", - paths=[child_uri], - ) - - history = git_harness.client.snapshot.log( - limit=2, - paths=[target_uri, directory_uri], - ) - - assert [item["oid"] for item in history] == [ - directory_commit["commit_oid"], - target_commit["commit_oid"], - ] - - -async def test_restore_reverts_file_and_advances_head(git_harness): - await git_harness.vfs.write_file( - "viking://resources/proj/a.md", - b"v1", - ctx=git_harness.ctx, - ) - v1 = git_harness.client.snapshot.commit( - message="v1", - paths=["viking://resources/proj/a.md"], - ) - - await git_harness.vfs.write_file( - "viking://resources/proj/a.md", - b"v2", - ctx=git_harness.ctx, - ) - v2 = git_harness.client.snapshot.commit( - message="v2", - paths=["viking://resources/proj/a.md"], - ) - - restore = git_harness.client.snapshot.restore( - project_dir="viking://resources/proj", - source_commit=v1["commit_oid"], - ) - - assert restore["result"] == "applied" - assert restore["source_commit"] == v1["commit_oid"] - assert restore["parent_commit"] == v2["commit_oid"] - assert restore["new_commit_oid"] != v2["commit_oid"] - assert ( - await git_harness.vfs.read( - "viking://resources/proj/a.md", - ctx=git_harness.ctx, - ) - == b"v1" - ) - assert git_harness.client.snapshot.show("main")["parents"] == [v2["commit_oid"]] - - -async def test_restore_dry_run_does_not_mutate(git_harness): - await git_harness.vfs.write_file( - "viking://resources/proj/a.md", - b"v1", - ctx=git_harness.ctx, - ) - v1 = git_harness.client.snapshot.commit( - message="v1", - paths=["viking://resources/proj/a.md"], - ) - await git_harness.vfs.write_file( - "viking://resources/proj/a.md", - b"v2", - ctx=git_harness.ctx, - ) - git_harness.client.snapshot.commit( - message="v2", - paths=["viking://resources/proj/a.md"], - ) - before_log = git_harness.client.snapshot.log() - - dry_run = git_harness.client.snapshot.restore( - project_dir="viking://resources/proj", - source_commit=v1["commit_oid"], - dry_run=True, - ) - - assert dry_run["result"] == "dry_run" - assert any(item["path"] == "a.md" for item in dry_run["diff"]["to_write"]) - assert ( - await git_harness.vfs.read( - "viking://resources/proj/a.md", - ctx=git_harness.ctx, - ) - == b"v2" - ) - assert len(git_harness.client.snapshot.log()) == len(before_log) - - -async def test_restore_internal_scope_rejected(git_harness): - await git_harness.vfs.write_file( - "viking://resources/a.md", - b"content", - ctx=git_harness.ctx, - ) - commit = git_harness.client.snapshot.commit( - message="commit", - paths=["viking://resources/a.md"], - ) - - # Client-level calls cross FSService first; its URI validator rejects - # internal scopes before VikingFS.restore can raise ValueError. - with pytest.raises(InvalidURIError): - git_harness.client.snapshot.restore( - project_dir="viking://temp/x", - source_commit=commit["commit_oid"], - ) - - -async def test_disabled_raises_not_supported(git_disabled_harness): - with pytest.raises(AGFSNotSupportedError): - git_disabled_harness.client.snapshot.commit(message="disabled") - with pytest.raises(AGFSNotSupportedError): - git_disabled_harness.client.snapshot.show("main") - with pytest.raises(AGFSNotSupportedError): - git_disabled_harness.client.snapshot.restore( - project_dir="viking://resources/proj", - source_commit="main", - ) - with pytest.raises(AGFSNotSupportedError): - git_disabled_harness.client.snapshot.log() - - -async def test_async_api_parity(git_harness): - await git_harness.vfs.write_file( - "viking://resources/async.md", - b"async hello", - ctx=git_harness.ctx, - ) - - commit = await git_harness.async_client.snapshot.commit( - message="async initial", - paths=["viking://resources/async.md"], - ) - body = await git_harness.async_client.snapshot.show( - "main", - path="viking://resources/async.md", - ) - - assert commit["result"] == "created" - assert OID_RE.match(commit["commit_oid"]) - assert body == b"async hello" - - -async def test_snapshot_namespace_gitignore_roundtrip(git_harness): - """End-to-end: snapshot namespace -> LocalClient -> FSService -> VikingFS. - - get/set/delete_gitignore must flow through the full local-mode stack and - the written rules must take effect at commit time (ignored count). - """ - client = git_harness.async_client - - # Absent -> empty string. - assert await client.snapshot.get_gitignore() == "" - - # Set via the namespace, then read back. - await client.snapshot.set_gitignore(content="*.log\n") - assert await client.snapshot.get_gitignore() == "*.log\n" - - # The rule must affect commits: a .log file is skipped, a .md file kept. - await git_harness.vfs.write_file("viking://resources/keep.md", b"keep", ctx=git_harness.ctx) - await git_harness.vfs.write_file("viking://resources/skip.log", b"skip", ctx=git_harness.ctx) - - commit = await client.snapshot.commit(message="with ignore") - assert commit["result"] == "created" - assert commit["ignored"] == 1 - assert await client.snapshot.show("main", path="viking://resources/keep.md") == b"keep" - with pytest.raises(AGFSNotFoundError): - await client.snapshot.show("main", path="viking://resources/skip.log") - - # Delete is idempotent and makes the rule vanish. - await client.snapshot.delete_gitignore() - await client.snapshot.delete_gitignore() - assert await client.snapshot.get_gitignore() == "" diff --git a/tests/client/test_git_versioning_http.py b/tests/client/test_git_versioning_http.py index fbc4bf30e1..5d606b7cff 100644 --- a/tests/client/test_git_versioning_http.py +++ b/tests/client/test_git_versioning_http.py @@ -2,9 +2,8 @@ # SPDX-License-Identifier: AGPL-3.0 """End-to-end parity tests for client.snapshot.* over HTTP. -These exercise the AsyncHTTPClient.snapshot namespace surface that mirrors -the LocalClient.snapshot surface covered by tests/client/test_git_versioning.py, -routed through AsyncHTTPClient -> real FastAPI server (via httpx +These exercise the AsyncHTTPClient.snapshot namespace surface, routed through +AsyncHTTPClient -> real FastAPI server (via httpx ASGITransport) -> real OpenVikingService -> real VikingFS. The full stack is genuine: real httpx response parsing, real envelope diff --git a/tests/client/test_import_export.py b/tests/client/test_import_export.py deleted file mode 100644 index 5fd7356178..0000000000 --- a/tests/client/test_import_export.py +++ /dev/null @@ -1,191 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 - -"""Import/export tests""" - -import hashlib -import io -import json -import zipfile -from pathlib import Path - -import pytest - -from openviking import AsyncOpenViking - - -class TestExportOvpack: - """Test export_ovpack""" - - async def test_export_success(self, client_with_resource, temp_dir: Path): - """Test successful export""" - client, uri = client_with_resource - export_path = temp_dir / "export.ovpack" - - result = await client.export_ovpack(uri, str(export_path)) - - assert isinstance(result, str) - assert Path(result).exists() - - async def test_export_directory( - self, client: AsyncOpenViking, sample_directory: Path, temp_dir: Path - ): - """Test exporting directory""" - # Add files from directory - for f in sample_directory.glob("**/*.txt"): - await client.add_resource(path=str(f), reason="Test export dir") - - # Export entire resource directory - export_path = temp_dir / "dir_export.ovpack" - result = await client.export_ovpack("viking://resources/", str(export_path)) - - assert isinstance(result, str) - - -class TestImportOvpack: - """Test import_ovpack""" - - async def test_import_success(self, client_with_resource, temp_dir: Path): - """Test successful import""" - client, uri = client_with_resource - - # Export first - export_path = temp_dir / "import_test.ovpack" - await client.export_ovpack(uri, str(export_path)) - - # Import to new location - import_uri = await client.import_ovpack(str(export_path), "viking://resources/imported/") - - assert isinstance(import_uri, str) - assert "imported" in import_uri - - async def test_import_with_on_conflict_overwrite(self, client_with_resource, temp_dir: Path): - """Test overwrite import.""" - client, uri = client_with_resource - - # Export first - export_path = temp_dir / "overwrite_test.ovpack" - await client.export_ovpack(uri, str(export_path)) - - # First import - await client.import_ovpack(str(export_path), "viking://resources/overwrite_test/") - - # Second import overwrites the existing root. - import_uri = await client.import_ovpack( - str(export_path), - "viking://resources/overwrite_test/", - on_conflict="overwrite", - ) - - assert isinstance(import_uri, str) - - async def test_import_export_roundtrip( - self, client: AsyncOpenViking, sample_markdown_file: Path, temp_dir: Path - ): - """Test export-import roundtrip""" - # Add resource - result = await client.add_resource(path=str(sample_markdown_file), reason="Roundtrip test") - original_uri = result["root_uri"] - - # Read original content - original_content = "" - entries = await client.tree(original_uri) - for e in entries: - if not e["isDir"]: - original_content = await client.read(e["uri"]) - - # Export - export_path = temp_dir / "roundtrip.ovpack" - await client.export_ovpack(original_uri, str(export_path)) - - # Delete original resource - await client.rm(original_uri, recursive=True) - - # Import - import_uri = await client.import_ovpack(str(export_path), "viking://resources/roundtrip/") - - # Read imported content - imported_content = "" - entries = await client.tree(import_uri) - for e in entries: - if not e["isDir"]: - imported_content = await client.read(e["uri"]) - - # Verify content consistency - assert original_content == imported_content - - @staticmethod - def _build_ovpack(zip_path: Path, entries: dict[str, str]) -> None: - index_records = b"" - manifest = { - "kind": "openviking.ovpack", - "format_version": 2, - "root": { - "name": "pkg", - "uri": "viking://resources/pkg", - "scope": "resources", - }, - "entries": [{"path": "", "kind": "directory"}], - "content_sha256": hashlib.sha256(b"[]").hexdigest(), - "index": { - "records": { - "path": "_ovpack/index_records.jsonl", - "count": 0, - "sha256": hashlib.sha256(index_records).hexdigest(), - } - }, - } - buffer = io.BytesIO() - with zipfile.ZipFile(buffer, "w") as zf: - zf.writestr("pkg/", "") - zf.writestr("pkg/files/", "") - zf.writestr("pkg/_ovpack/", "") - zf.writestr("pkg/_ovpack/index_records.jsonl", index_records) - zf.writestr("pkg/_ovpack/manifest.json", json.dumps(manifest)) - for name, content in entries.items(): - zf.writestr(name, content) - zip_path.write_bytes(buffer.getvalue()) - - @pytest.mark.parametrize( - "entries,error_pattern", - [ - ( - { - "pkg/../../escape.txt": "pwned", - }, - "Unsafe ovpack entry path", - ), - ( - { - "/abs/path.txt": "pwned", - }, - "Unsafe ovpack entry path", - ), - ( - { - "C:/drive/path.txt": "pwned", - }, - "Unsafe ovpack entry path", - ), - ( - { - "pkg\\windows\\path.txt": "pwned", - }, - "Unsafe ovpack entry path", - ), - ( - { - "other/file.txt": "pwned", - }, - "Invalid ovpack entry root", - ), - ], - ) - async def test_import_rejects_unsafe_entries( - self, client: AsyncOpenViking, temp_dir: Path, entries: dict[str, str], error_pattern: str - ): - ovpack_path = temp_dir / "malicious.ovpack" - self._build_ovpack(ovpack_path, entries) - - with pytest.raises(ValueError, match=error_pattern): - await client.import_ovpack(str(ovpack_path), "viking://resources/security/") diff --git a/tests/client/test_lifecycle.py b/tests/client/test_lifecycle.py deleted file mode 100644 index c213c3fa9c..0000000000 --- a/tests/client/test_lifecycle.py +++ /dev/null @@ -1,113 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 - -"""Client lifecycle tests""" - -from pathlib import Path - -from openviking import AsyncOpenViking - - -class TestClientInitialization: - """Test Client initialization""" - - async def test_initialize_success(self, uninitialized_client: AsyncOpenViking): - """Test normal initialization""" - await uninitialized_client.initialize() - assert uninitialized_client._initialized is True - - async def test_initialize_idempotent(self, client: AsyncOpenViking): - """Test repeated initialization is idempotent""" - await client.initialize() - await client.initialize() - assert client._initialized is True - - async def test_initialize_creates_client(self, uninitialized_client: AsyncOpenViking): - """Test initialization creates client""" - await uninitialized_client.initialize() - assert uninitialized_client._client is not None - - async def test_agent_id_alias_sets_actor_peer_scope(self, test_data_dir: Path): - await AsyncOpenViking.reset() - - client = AsyncOpenViking(path=str(test_data_dir), agent_id="legacy-agent") - - assert client._client._ctx.actor_peer_id == "legacy-agent" - - await AsyncOpenViking.reset() - - async def test_agent_id_alias_must_match_actor_peer_id(self, test_data_dir: Path): - await AsyncOpenViking.reset() - - try: - try: - AsyncOpenViking( - path=str(test_data_dir), - actor_peer_id="actor-a", - agent_id="actor-b", - ) - except ValueError as exc: - assert "actor_peer_id cannot be used with legacy agent_id" in str(exc) - else: - raise AssertionError("mismatched agent_id should fail") - finally: - await AsyncOpenViking.reset() - -class TestClientClose: - """Test Client close""" - - async def test_close_success(self, test_data_dir: Path): - """Test normal close""" - await AsyncOpenViking.reset() - client = AsyncOpenViking(path=str(test_data_dir)) - await client.initialize() - - await client.close() - assert client._initialized is False - - await AsyncOpenViking.reset() - - 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)) - await client.initialize() - - await client.close() - await client.close() # Should not raise exception - - await AsyncOpenViking.reset() - - -class TestClientReset: - """Test Client reset""" - - 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)) - await client1.initialize() - - await AsyncOpenViking.reset() - - client2 = AsyncOpenViking(path=str(test_data_dir)) - # Should be new instance after reset - assert client1 is not client2 - - await AsyncOpenViking.reset() - - -class TestClientSingleton: - """Test Client singleton pattern""" - - 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)) - client2 = AsyncOpenViking(path=str(test_data_dir)) - - assert client1 is client2 - - await AsyncOpenViking.reset() diff --git a/tests/client/test_ls_forwarding.py b/tests/client/test_ls_forwarding.py deleted file mode 100644 index 101eea1b0a..0000000000 --- a/tests/client/test_ls_forwarding.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 - -"""Filesystem option forwarding at the public embedded-client boundary.""" - -from types import SimpleNamespace -from unittest.mock import AsyncMock - -from openviking.async_client import AsyncOpenViking - - -async def test_async_openviking_ls_forwards_ordering_and_limit_options(): - client = object.__new__(AsyncOpenViking) - client._ensure_initialized = AsyncMock() - client._client = SimpleNamespace(ls=AsyncMock(return_value=[])) - - await client.ls( - "viking://session", - node_limit=200, - sort_by="mtime", - sort_order="desc", - ) - - client._client.ls.assert_awaited_once_with( - "viking://session", - recursive=False, - simple=False, - output="original", - abs_limit=256, - show_all_hidden=True, - node_limit=200, - sort_by="mtime", - sort_order="desc", - ) diff --git a/tests/client/test_rebuild_clients.py b/tests/client/test_rebuild_clients.py index ecd32761d3..db78c7ca05 100644 --- a/tests/client/test_rebuild_clients.py +++ b/tests/client/test_rebuild_clients.py @@ -9,9 +9,6 @@ import openviking_cli.client.http as http_module import openviking_cli.utils.async_utils as async_utils -from openviking import AsyncOpenViking, SyncOpenViking -from openviking.client.local import LocalClient -from openviking.message import ImagePart, TextPart from openviking_cli.client.http import AsyncHTTPClient from openviking_cli.client.sync_http import SyncHTTPClient from openviking_cli.utils.config import OPENVIKING_CLI_CONFIG_ENV @@ -78,233 +75,6 @@ def test_async_http_client_zip_directory_warns_when_archive_is_empty(tmp_path): ) -async def test_async_openviking_reindex_forwards_to_local_client(tmp_path): - client = AsyncOpenViking(path=str(tmp_path)) - with patch.object(client, "_ensure_initialized", new_callable=AsyncMock) as mock_init: - with patch.object(client._client, "reindex", new_callable=AsyncMock) as mock_reindex: - mock_reindex.return_value = {"status": "completed"} - - result = await client.reindex( - "viking://resources/demo", - mode="prune_orphans", - wait=False, - dry_run=True, - ) - - assert result == {"status": "completed"} - mock_init.assert_awaited_once() - mock_reindex.assert_awaited_once_with( - uri="viking://resources/demo", - mode="prune_orphans", - wait=False, - dry_run=True, - ) - - -async def test_async_openviking_forwards_turn_retention_and_message_semantics(tmp_path): - client = AsyncOpenViking(path=str(tmp_path)) - with patch.object(client, "_ensure_initialized", new_callable=AsyncMock): - with patch.object( - client._client, - "add_message", - new_callable=AsyncMock, - return_value={"message_count": 1}, - ) as mock_add: - with patch.object( - client._client, - "commit_session", - new_callable=AsyncMock, - return_value={"status": "accepted"}, - ) as mock_commit: - await client.add_message( - "session-1", - "assistant", - parts=[{"type": "text", "text": "checking"}], - turn_id="turn-1", - message_kind="assistant_step", - source_message_ids=["u1"], - ) - await client.commit_session( - "session-1", - retention_mode="turn_budget", - keep_recent_turn_count=3, - retained_message_token_budget=12_000, - min_raw_tail_steps=1, - ) - - mock_add.assert_awaited_once_with( - session_id="session-1", - role="assistant", - content=None, - parts=[{"type": "text", "text": "checking"}], - created_at=None, - peer_id=None, - telemetry=False, - turn_id="turn-1", - message_kind="assistant_step", - source_message_ids=["u1"], - ) - mock_commit.assert_awaited_once_with( - "session-1", - telemetry=False, - keep_recent_count=0, - retention_mode="turn_budget", - keep_recent_turn_count=3, - retained_message_token_budget=12_000, - min_raw_tail_steps=1, - ) - - -def test_sync_openviking_reindex_forwards_to_async_client(): - client = SyncOpenViking() - with patch.object( - client._async_client, - "reindex", - new_callable=Mock, - return_value={"status": "completed"}, - ) as mock_reindex: - with patch( - "openviking.sync_client.run_async", return_value={"status": "completed"} - ) as mock_run: - result = client.reindex( - "viking://resources/demo", - mode="prune_orphans", - wait=True, - dry_run=True, - ) - - assert result == {"status": "completed"} - assert mock_run.called - mock_reindex.assert_called_once_with( - uri="viking://resources/demo", - mode="prune_orphans", - wait=True, - dry_run=True, - ) - - -async def test_local_client_reindex_forwards_to_service(): - client = LocalClient.__new__(LocalClient) - client._service = SimpleNamespace(reindex=AsyncMock(return_value={"status": "completed"})) - - result = await LocalClient.reindex( - client, - uri="viking://resources/demo", - mode="prune_orphans", - wait=False, - dry_run=True, - ) - - assert result == {"status": "completed"} - client._service.reindex.assert_awaited_once_with( - uri="viking://resources/demo", - mode="prune_orphans", - wait=False, - dry_run=True, - ) - - -async def test_local_client_batch_add_messages_forwards_to_session(): - class FakeSession: - def __init__(self): - self.messages = [] - - def add_messages(self, specs): - self.messages.extend(specs) - return specs - - fake_session = FakeSession() - - class FakeSessions: - async def get(self, session_id, ctx, auto_create=False): - assert session_id == "batch-session" - assert ctx is client._ctx - assert auto_create is True - return fake_session - - client = LocalClient.__new__(LocalClient) - client._service = SimpleNamespace(sessions=FakeSessions()) - client._ctx = SimpleNamespace(user=SimpleNamespace(user_id="user-1")) - - result = await LocalClient.batch_add_messages( - client, - "batch-session", - [ - { - "role": "user", - "content": "hello", - "peer_id": "explicit-user", - "created_at": "2026-05-28T00:00:00+00:00", - }, - {"role": "assistant", "parts": [{"type": "text", "text": "hi"}]}, - ], - ) - - # A lightweight fake session has no ``meta``, so pending_tokens degrades to 0 - # instead of raising; the field is always present for commit-policy callers. - assert result == { - "session_id": "batch-session", - "message_count": 2, - "added": 2, - "pending_tokens": 0, - } - assert fake_session.messages[0]["role"] == "user" - assert fake_session.messages[0]["peer_id"] == "explicit-user" - assert fake_session.messages[0]["created_at"] == "2026-05-28T00:00:00+00:00" - assert fake_session.messages[0]["parts"][0].text == "hello" - assert fake_session.messages[1]["role"] == "assistant" - assert fake_session.messages[1]["peer_id"] is None - assert fake_session.messages[1]["parts"][0].text == "hi" - - -async def test_local_client_add_message_accepts_image_parts(): - class FakeSession: - def __init__(self): - self.messages = [] - - def add_message(self, role, parts, peer_id=None, created_at=None): - self.messages.append( - { - "role": role, - "parts": parts, - "peer_id": peer_id, - "created_at": created_at, - } - ) - - fake_session = FakeSession() - - class FakeSessions: - async def get(self, session_id, ctx, auto_create=False): - assert session_id == "image-session" - assert ctx is client._ctx - assert auto_create is True - return fake_session - - client = LocalClient.__new__(LocalClient) - client._service = SimpleNamespace(sessions=FakeSessions()) - client._ctx = SimpleNamespace(user=SimpleNamespace(user_id="user-1")) - - result = await LocalClient.add_message( - client, - "image-session", - "user", - parts=[ - {"type": "text", "text": "Look at this"}, - {"type": "image_url", "image_url": {"url": "https://example.com/image.png"}}, - ], - ) - - assert result == { - "session_id": "image-session", - "message_count": 1, - "pending_tokens": 0, - } - assert isinstance(fake_session.messages[0]["parts"][0], TextPart) - assert isinstance(fake_session.messages[0]["parts"][1], ImagePart) - assert fake_session.messages[0]["parts"][1].url == "https://example.com/image.png" - - async def test_async_http_client_batch_add_messages_posts_batch_payload(): client = AsyncHTTPClient(url="http://localhost:1933") fake_http = SimpleNamespace(post=AsyncMock(return_value=object())) diff --git a/tests/client/test_relations.py b/tests/client/test_relations.py deleted file mode 100644 index c968f5bb74..0000000000 --- a/tests/client/test_relations.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 - -"""Relation tests""" - - -class TestLink: - """Test link creating relations""" - - async def test_link_single_uri(self, client_with_resource): - """Test creating single relation""" - client, uri = client_with_resource - target_uri = "viking://resources/target/" - - await client.link(from_uri=uri, uris=target_uri, reason="Test link") - - relations = await client.relations(uri) - assert any(r.get("uri") == target_uri for r in relations) - - async def test_link_multiple_uris(self, client_with_resource): - """Test creating multiple relations""" - client, uri = client_with_resource - target_uris = ["viking://resources/target1/", "viking://resources/target2/"] - - await client.link(from_uri=uri, uris=target_uris, reason="Test multiple links") - - relations = await client.relations(uri) - for target in target_uris: - assert any(r.get("uri") == target for r in relations) - - async def test_link_with_reason(self, client_with_resource): - """Test creating relation with reason""" - client, uri = client_with_resource - target_uri = "viking://resources/reason_test/" - reason = "This is a test reason for the link" - - await client.link(from_uri=uri, uris=target_uri, reason=reason) - - relations = await client.relations(uri) - link = next((r for r in relations if r.get("uri") == target_uri), None) - assert link is not None - assert link.get("reason") == reason - - -class TestUnlink: - """Test unlink deleting relations""" - - async def test_unlink_success(self, client_with_resource): - """Test successful relation deletion""" - client, uri = client_with_resource - target_uri = "viking://resources/unlink_test/" - - # Create relation first - await client.link(from_uri=uri, uris=target_uri, reason="Test") - - # Verify relation exists - relations = await client.relations(uri) - assert any(r.get("uri") == target_uri for r in relations) - - # Delete relation - await client.unlink(from_uri=uri, uri=target_uri) - - # Verify relation deleted - relations = await client.relations(uri) - assert not any(r.get("uri") == target_uri for r in relations) - - async def test_unlink_nonexistent(self, client_with_resource): - """Test deleting nonexistent relation""" - client, uri = client_with_resource - - # Should not raise exception - await client.unlink(from_uri=uri, uri="viking://nonexistent/") - - -class TestRelations: - """Test relations getting relations""" - - async def test_relations_empty(self, client_with_resource): - """Test getting empty relation list""" - client, uri = client_with_resource - - relations = await client.relations(uri) - - assert isinstance(relations, list) - - async def test_relations_with_data(self, client_with_resource): - """Test getting relation list with data""" - client, uri = client_with_resource - target_uri = "viking://resources/relations_test/" - - await client.link(from_uri=uri, uris=target_uri, reason="Test reason") - - relations = await client.relations(uri) - - assert len(relations) > 0 - link = next((r for r in relations if r.get("uri") == target_uri), None) - assert link is not None - assert link.get("reason") == "Test reason" diff --git a/tests/client/test_resource_management.py b/tests/client/test_resource_management.py deleted file mode 100644 index 3efd2df439..0000000000 --- a/tests/client/test_resource_management.py +++ /dev/null @@ -1,268 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 - -"""Resource management tests""" - -from pathlib import Path -from types import SimpleNamespace -from unittest.mock import AsyncMock, patch - -from openviking import AsyncOpenViking -from openviking.client import LocalClient -from openviking.server.identity import RequestContext, Role -from openviking.telemetry import get_current_telemetry -from openviking_cli.session.user_id import UserIdentifier - - -class TestAddResource: - """Test add_resource""" - - async def test_add_resource_success(self, client: AsyncOpenViking, sample_markdown_file: Path): - """Test successful resource addition""" - result = await client.add_resource(path=str(sample_markdown_file), reason="Test resource") - - assert "root_uri" in result - assert result["root_uri"].startswith("viking://") - - async def test_add_resource_with_wait( - self, client: AsyncOpenViking, sample_markdown_file: Path - ): - """Test adding resource and waiting for processing""" - result = await client.add_resource( - path=str(sample_markdown_file), - reason="Test resource", - wait=True, - ) - - print(result) - assert "root_uri" in result - - async def test_local_client_add_resource_with_wait_preserves_queue_status(self): - """Local SDK add_resource(wait=True) should keep queue_status and internal telemetry.""" - queue_status = { - "Semantic": {"processed": 1, "error_count": 0, "errors": []}, - "Embedding": {"processed": 2, "error_count": 0, "errors": []}, - } - seen: dict[str, object] = {} - - async def _fake_add_resource(**kwargs): - telemetry = get_current_telemetry() - seen["enabled"] = telemetry.enabled - seen["telemetry_id"] = telemetry.telemetry_id - seen["kwargs"] = kwargs - return { - "root_uri": "viking://resources/demo", - "queue_status": queue_status, - } - - client = LocalClient.__new__(LocalClient) - client._ctx = RequestContext(user=UserIdentifier.the_default_user(), role=Role.USER) - client._service = SimpleNamespace( - resources=SimpleNamespace(add_resource=_fake_add_resource) - ) - - result = await LocalClient.add_resource( - client, - path="/tmp/demo.md", - reason="Test resource", - wait=True, - telemetry=False, - ) - - assert result["root_uri"] == "viking://resources/demo" - assert result["queue_status"] == queue_status - assert seen["enabled"] is True - assert str(seen["telemetry_id"]).startswith("tm_") - assert seen["kwargs"]["wait"] is True - - async def test_local_client_forwards_declared_add_type(self): - seen: dict[str, object] = {} - - async def _fake_add_resource(**kwargs): - seen.update(kwargs) - return {"root_uri": "viking://resources/feishu"} - - client = LocalClient.__new__(LocalClient) - client._ctx = RequestContext(user=UserIdentifier.the_default_user(), role=Role.USER) - client._service = SimpleNamespace( - resources=SimpleNamespace(add_resource=_fake_add_resource) - ) - - result = await LocalClient.add_resource( - client, - path="space:home", - add_type=" feishu ", - to="viking://resources/feishu", - ) - - assert result["root_uri"] == "viking://resources/feishu" - assert seen["path"] == "space:home" - assert seen["add_type"] == "feishu" - assert seen["to"] == "viking://resources/feishu" - - async def test_async_openviking_forwards_declared_add_type(self): - backend = SimpleNamespace(add_resource=AsyncMock(return_value={"root_uri": "ok"})) - client = AsyncOpenViking.__new__(AsyncOpenViking) - client._initialized = True - client._client = backend - - result = await AsyncOpenViking.add_resource( - client, - path="space:home", - add_type="feishu", - to="viking://resources/feishu", - ) - - assert result == {"root_uri": "ok"} - assert backend.add_resource.await_args.kwargs["add_type"] == "feishu" - assert backend.add_resource.await_args.kwargs["to"] == "viking://resources/feishu" - - async def test_add_resource_without_wait( - self, client: AsyncOpenViking, sample_markdown_file: Path - ): - """Test adding resource without waiting (async mode)""" - result = await client.add_resource( - path=str(sample_markdown_file), reason="Test resource", wait=False - ) - - assert "root_uri" in result - # In async mode, status can be monitored via observer - observer = client.observer - assert observer.queue is not None - - async def test_add_resource_with_to(self, client: AsyncOpenViking, sample_markdown_file: Path): - """Test adding resource to specified target""" - result = await client.add_resource( - path=str(sample_markdown_file), - to="viking://resources/custom/sample", - reason="Test resource", - ) - - assert "root_uri" in result - assert "custom" in result["root_uri"] - - async def test_add_resource_file_not_found(self, client: AsyncOpenViking): - """Test adding nonexistent file""" - - res = await client.add_resource(path="/nonexistent/file.txt", reason="Test") - - assert "errors" in res and len(res["errors"]) > 0 - - -class TestWaitProcessed: - """Test wait_processed""" - - async def test_wait_processed_success( - self, client: AsyncOpenViking, sample_markdown_file: Path - ): - """Test waiting for processing to complete""" - await client.add_resource(path=str(sample_markdown_file), reason="Test") - - status = await client.wait_processed() - - assert isinstance(status, dict) - - async def test_wait_processed_empty_queue(self, client: AsyncOpenViking): - """Test waiting on empty queue""" - status = await client.wait_processed() - - assert isinstance(status, dict) - - async def test_wait_processed_multiple_resources( - self, client: AsyncOpenViking, sample_files: list[Path] - ): - """Test waiting for multiple resources to complete""" - for f in sample_files: - await client.add_resource(path=str(f), reason="Batch test") - - status = await client.wait_processed() - - assert isinstance(status, dict) - - -class TestWatchIntervalParameter: - """Test watch_interval parameter propagation""" - - async def test_watch_interval_default_value( - self, client: AsyncOpenViking, sample_markdown_file: Path - ): - """Test that watch_interval defaults to 0""" - with patch.object( - client._client, "add_resource", new_callable=AsyncMock - ) as mock_add_resource: - mock_add_resource.return_value = {"root_uri": "viking://test"} - - await client.add_resource(path=str(sample_markdown_file), reason="Test") - - call_kwargs = mock_add_resource.call_args[1] - assert call_kwargs.get("watch_interval") == 0 - - async def test_watch_interval_custom_value( - self, client: AsyncOpenViking, sample_markdown_file: Path - ): - """Test that custom watch_interval value is propagated""" - with patch.object( - client._client, "add_resource", new_callable=AsyncMock - ) as mock_add_resource: - mock_add_resource.return_value = {"root_uri": "viking://test"} - - await client.add_resource( - path=str(sample_markdown_file), - reason="Test", - watch_interval=5.0, - ) - - call_kwargs = mock_add_resource.call_args[1] - assert call_kwargs.get("watch_interval") == 5.0 - - async def test_watch_interval_propagates_to_local_client( - self, sample_markdown_file: Path, test_data_dir: Path - ): - """Test that watch_interval propagates from AsyncOpenViking to LocalClient""" - from openviking.client import LocalClient - - with patch.object(LocalClient, "add_resource", new_callable=AsyncMock) as mock_add_resource: - mock_add_resource.return_value = {"root_uri": "viking://test"} - - from openviking import AsyncOpenViking - - await AsyncOpenViking.reset() - client = AsyncOpenViking(path=str(test_data_dir)) - await client.initialize() - - try: - await client.add_resource( - path=str(sample_markdown_file), - reason="Test", - watch_interval=10.0, - ) - - call_kwargs = mock_add_resource.call_args[1] - assert call_kwargs.get("watch_interval") == 10.0 - finally: - await client.close() - await AsyncOpenViking.reset() - - async def test_watch_interval_zero_means_disabled( - self, client: AsyncOpenViking, sample_markdown_file: Path - ): - """Test that watch_interval=0 means monitoring is disabled""" - result = await client.add_resource( - path=str(sample_markdown_file), - reason="Test", - watch_interval=0, - ) - - assert "root_uri" in result - - async def test_watch_interval_positive_value( - self, client: AsyncOpenViking, sample_markdown_file: Path - ): - """Test that positive watch_interval value is accepted""" - result = await client.add_resource( - path=str(sample_markdown_file), - reason="Test", - watch_interval=2.5, - ) - - assert "root_uri" in result diff --git a/tests/client/test_search.py b/tests/client/test_search.py deleted file mode 100644 index f24e161e84..0000000000 --- a/tests/client/test_search.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 - -"""Search tests""" - -from openviking.message import TextPart - - -class TestFind: - """Test find quick search""" - - async def test_find(self, client_with_resource_sync): - """Test basic search""" - client, uri = client_with_resource_sync - - result = await client.find(query="sample document") - - assert hasattr(result, "resources") - assert hasattr(result, "memories") - assert hasattr(result, "skills") - assert hasattr(result, "total") - - """Test limiting result count""" - result = await client.find(query="test", limit=5) - - assert len(result.resources) <= 5 - - """Test search with target URI""" - result = await client.find(query="sample", target_uri=uri) - - assert hasattr(result, "resources") - - """Test score threshold filtering""" - result = await client.find(query="sample document", score_threshold=0.1) - - # Verify all results have score >= threshold - for res in result.resources: - assert res.score >= 0.1 - - """Test no matching results""" - result = await client.find(query="completely_random_nonexistent_query_xyz123") - - assert result.total >= 0 - - -class TestSearch: - """Test search complex search""" - - async def test_search(self, client_with_resource_sync): - """Test basic complex search""" - client, uri = client_with_resource_sync - - result = await client.search(query="sample document") - - assert hasattr(result, "resources") - - """Test search with session context""" - session = client.session() - # Add some messages to establish context - session.add_message("user", [TextPart("I need help with testing")]) - - result = await client.search(query="testing help", session=session) - - assert hasattr(result, "resources") - - """Test limiting result count""" - result = await client.search(query="sample", limit=3) - - assert len(result.resources) <= 3 - - """Test complex search with target URI""" - parent_uri = "/".join(uri.split("/")[:-1]) + "/" - - result = await client.search(query="sample", target_uri=parent_uri) - - assert hasattr(result, "resources") diff --git a/tests/client/test_skill_management.py b/tests/client/test_skill_management.py deleted file mode 100644 index c1607ba11f..0000000000 --- a/tests/client/test_skill_management.py +++ /dev/null @@ -1,264 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 - -"""Skill management tests""" - -from pathlib import Path -from types import SimpleNamespace - -from openviking import AsyncOpenViking -from openviking.client import LocalClient -from openviking.core.namespace import canonical_user_root -from openviking.server.identity import RequestContext, Role -from openviking.storage.expr import PathScope -from openviking.telemetry import get_current_telemetry -from openviking_cli.session.user_id import UserIdentifier - - -def _user_skills_root(client: AsyncOpenViking) -> str: - return f"{canonical_user_root(client._client._ctx)}/skills" - - -class TestAddSkill: - """Test add_skill""" - - async def test_add_skill_from_file(self, client: AsyncOpenViking, temp_dir: Path): - """Test adding skill from file""" - # Create skill file in SKILL.md format - skill_file = temp_dir / "test_skill.md" - skill_file.write_text( - """--- -name: test-skill -description: A test skill for unit testing -tags: - - test - - unit-test ---- - -# Test Skill - -## Description -This is a test skill for unit testing OpenViking skill management. - -## Usage -Use this skill when you need to test skill functionality. - -## Instructions -1. Step one: Initialize the skill -2. Step two: Execute the skill -3. Step three: Verify the result -""" - ) - - result = await client.add_skill(data=skill_file) - - assert "root_uri" in result - assert "uri" in result - assert result["root_uri"] == result["uri"] - assert result["uri"].startswith(f"{_user_skills_root(client)}/") - - abstract = await client.abstract(result["uri"]) - assert "name: test-skill" in abstract - assert "description: A test skill for unit testing" in abstract - assert "tags:" in abstract - assert "test" in abstract - assert "unit-test" in abstract - - async def test_add_skill_from_string(self, client: AsyncOpenViking): - """Test adding skill from string""" - skill_content = """--- -name: string-skill -description: A skill created from string -tags: - - test -allowed-tools: - - shell ---- - -# String Skill - -## Instructions -This skill was created from a string. -""" - result = await client.add_skill(data=skill_content) - - assert "uri" in result - assert result["uri"].startswith(f"{_user_skills_root(client)}/") - - abstract = await client.abstract(result["uri"]) - assert "name: string-skill" in abstract - assert "description: A skill created from string" in abstract - assert "tags:" in abstract - assert "test" in abstract - assert "allowed_tools:" in abstract - assert "shell" in abstract - - async def test_add_skill_with_wait_returns_queue_status(self, client: AsyncOpenViking): - """Test local SDK add_skill(wait=True) preserves queue_status and binds telemetry.""" - del client - queue_status = { - "Semantic": {"processed": 0, "error_count": 0, "errors": []}, - "Embedding": {"processed": 1, "error_count": 0, "errors": []}, - } - seen: dict[str, object] = {} - - async def _fake_add_skill(**kwargs): - telemetry = get_current_telemetry() - seen["enabled"] = telemetry.enabled - seen["telemetry_id"] = telemetry.telemetry_id - seen["kwargs"] = kwargs - return { - "uri": "viking://user/default/skills/waited-skill", - "queue_status": queue_status, - } - - local_client = LocalClient.__new__(LocalClient) - local_client._ctx = RequestContext( - user=UserIdentifier.the_default_user(), - role=Role.USER, - ) - local_client._service = SimpleNamespace( - resources=SimpleNamespace(add_skill=_fake_add_skill) - ) - - result = await LocalClient.add_skill( - local_client, - data={"name": "waited-skill", "content": "# Waited Skill"}, - wait=True, - telemetry=False, - ) - - assert result["uri"] == "viking://user/default/skills/waited-skill" - assert result["queue_status"] == queue_status - assert seen["enabled"] is True - assert str(seen["telemetry_id"]).startswith("tm_") - assert seen["kwargs"]["wait"] is True - - async def test_add_skill_from_mcp_tool(self, client: AsyncOpenViking): - """Test adding skill from MCP Tool format""" - mcp_tool = { - "name": "mcp_test_tool", - "description": "A test MCP tool", - "inputSchema": { - "type": "object", - "properties": {"query": {"type": "string", "description": "The search query"}}, - "required": ["query"], - }, - } - result = await client.add_skill(data=mcp_tool) - - assert "uri" in result - assert result["uri"].startswith(f"{_user_skills_root(client)}/") - - async def test_add_skill_from_directory(self, client: AsyncOpenViking, temp_dir: Path): - """Test adding skill from directory""" - # Create skill directory - skill_dir = temp_dir / "dir_skill" - skill_dir.mkdir() - - # Create SKILL.md - (skill_dir / "SKILL.md").write_text( - """--- -name: dir-skill -description: A skill from directory -tags: - - directory ---- - -# Directory Skill - -## Instructions -This skill was loaded from a directory. -""" - ) - - # Create auxiliary file - (skill_dir / "reference.md").write_text("# Reference\nAdditional reference content.") - - result = await client.add_skill(data=skill_dir) - - assert "uri" in result - assert result["uri"].startswith(f"{_user_skills_root(client)}/") - - -class TestSkillSearch: - """Test skill search""" - - async def test_find_skill(self, client: AsyncOpenViking, temp_dir: Path): - """Test searching skills""" - # Add skill first - skill_file = temp_dir / "search_skill.md" - skill_file.write_text( - """--- -name: search-test-skill -description: A skill for testing search functionality -tags: - - search - - test ---- - -# Search Test Skill - -## Instructions -Use this skill to test search functionality. -""" - ) - await client.add_skill(data=skill_file) - - # Search skills - result = await client.find(query="search functionality") - - assert hasattr(result, "skills") - - async def test_add_skill_indexes_canonical_user_uri( - self, client: AsyncOpenViking, temp_dir: Path - ): - """Skill vectors should be stored under the canonical user root.""" - skill_file = temp_dir / "canonical_scope_skill.md" - skill_file.write_text( - """--- -name: canonical-scope-skill -description: A skill for testing canonical vector scope -tags: - - search - - test ---- - -# Canonical Scope Skill - -## Instructions -Use this skill to test canonical URI vector indexing. -""" - ) - - result = await client.add_skill(data=skill_file, wait=True) - canonical_uri = f"{_user_skills_root(client)}/canonical-scope-skill" - short_uri = "viking://user/skills/canonical-scope-skill" - - assert result["uri"] == canonical_uri - - vikingdb = client._service.vikingdb_manager - canonical_count = await vikingdb.count( - filter=PathScope("uri", canonical_uri, depth=0), - ctx=client._client._ctx, - ) - short_count = await vikingdb.count( - filter=PathScope("uri", short_uri, depth=0), - ctx=client._client._ctx, - ) - - assert canonical_count == 1 - assert short_count == 0 - - records = await vikingdb.filter( - filter=PathScope("uri", canonical_uri, depth=0), - limit=10, - output_fields=["uri", "abstract"], - ctx=client._client._ctx, - ) - assert len(records) == 1 - assert records[0]["uri"] == canonical_uri - assert "name: canonical-scope-skill" in records[0]["abstract"] - assert "description: A skill for testing canonical vector scope" in records[0]["abstract"] - assert "tags:" in records[0]["abstract"] - assert "search" in records[0]["abstract"] diff --git a/tests/conftest.py b/tests/conftest.py index 1594feab77..fc7c0ad756 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,15 @@ import pytest import pytest_asyncio -from openviking import AsyncOpenViking +from openviking.models.embedder.base import DenseEmbedderBase, EmbedResult +from openviking.server.identity import RequestContext, Role +from openviking.service.core import OpenVikingService +from openviking.service.task_tracker import set_task_tracker +from openviking.storage import viking_fs as viking_fs_module +from openviking_cli.session.user_id import UserIdentifier +from openviking_cli.utils.config.embedding_config import EmbeddingConfig +from openviking_cli.utils.config.open_viking_config import OpenVikingConfigSingleton +from tests.utils.mock_agfs import MockLocalAGFS # ── Workaround: local .so may lack AGFS_Grep symbol (new in latest source) ── @@ -200,57 +208,69 @@ def sample_files(temp_dir: Path) -> list[Path]: return files -# ============ Client Fixtures ============ +# ============ Service Fixtures ============ @pytest_asyncio.fixture(scope="function") -async def client(test_data_dir: Path) -> AsyncGenerator[AsyncOpenViking, None]: - """Create initialized OpenViking client""" - await AsyncOpenViking.reset() +async def service( + test_data_dir: Path, + monkeypatch, +) -> AsyncGenerator[OpenVikingService, None]: + """Create an initialized service for domain-level tests.""" - client = AsyncOpenViking(path=str(test_data_dir)) - await client.initialize() + previous_viking_fs = viking_fs_module._instance - yield client + class FakeEmbedder(DenseEmbedderBase): + def __init__(self): + super().__init__(model_name="test-fake-embedder") - await client.close() - await AsyncOpenViking.reset() + def embed(self, text: str, is_query: bool = False) -> EmbedResult: + return EmbedResult(dense_vector=[0.1] * 1024) + def get_dimension(self) -> int: + return 1024 -@pytest_asyncio.fixture(scope="function") -async def uninitialized_client(test_data_dir: Path) -> AsyncGenerator[AsyncOpenViking, None]: - """Create uninitialized OpenViking client (for testing initialization flow)""" - await AsyncOpenViking.reset() - - client = AsyncOpenViking(path=str(test_data_dir)) - - yield client - + monkeypatch.setattr(EmbeddingConfig, "get_embedder", lambda self: FakeEmbedder()) + mock_agfs = MockLocalAGFS(root_path=test_data_dir / "mock_agfs_root") + monkeypatch.setattr( + "openviking.utils.agfs_utils.create_agfs_client", + lambda *args, **kwargs: mock_agfs, + ) + OpenVikingConfigSingleton.reset_instance() + OpenVikingConfigSingleton.initialize( + config_dict={ + "storage": { + "workspace": str(test_data_dir), + "agfs": {"backend": "local"}, + "vectordb": {"backend": "local"}, + }, + "embedding": { + "dense": { + "provider": "openai", + "model": "test-embedder", + "api_key": "test-key", + "dimension": 1024, + } + }, + } + ) + instance = OpenVikingService( + path=str(test_data_dir), + user=UserIdentifier.the_default_user(), + ) try: - await client.close() - except Exception: - pass - await AsyncOpenViking.reset() + await instance.initialize() + yield instance + finally: + await instance.close() + set_task_tracker(None) + viking_fs_module._instance = previous_viking_fs + OpenVikingConfigSingleton.reset_instance() -@pytest_asyncio.fixture(scope="function") -async def client_with_resource_sync( - client: AsyncOpenViking, sample_markdown_file: Path -) -> AsyncGenerator[tuple[AsyncOpenViking, str], None]: - """Create client with resource (sync mode, wait for vectorization)""" - result = await client.add_resource( - path=str(sample_markdown_file), reason="Test resource", wait=True +@pytest.fixture(scope="function") +def request_context() -> RequestContext: + return RequestContext( + user=UserIdentifier.the_default_user(), + role=Role.USER, ) - uri = result.get("root_uri", "") - - yield client, uri - - -@pytest_asyncio.fixture(scope="function") -async def client_with_resource( - client: AsyncOpenViking, sample_markdown_file: Path -) -> AsyncGenerator[tuple[AsyncOpenViking, str], None]: - """Create client with resource (async mode, no wait for vectorization)""" - result = await client.add_resource(path=str(sample_markdown_file), reason="Test resource") - uri = result.get("root_uri", "") - yield client, uri diff --git a/tests/eval/test_ragas_basic.py b/tests/eval/test_ragas_basic.py index e0deb33be7..69f6fc0f3f 100644 --- a/tests/eval/test_ragas_basic.py +++ b/tests/eval/test_ragas_basic.py @@ -11,7 +11,6 @@ from openviking.eval.ragas.pipeline import RAGQueryPipeline from openviking.eval.ragas.types import EvalDataset, EvalSample from openviking.eval.recorder.async_writer import AsyncRecordWriter -from openviking_cli.retrieve.types import ContextType, FindResult, MatchedContext def test_eval_types(): @@ -34,9 +33,9 @@ def test_generator_initialization(): def test_pipeline_initialization(): - pipeline = RAGQueryPipeline(config_path="./test.conf", data_path="./test_data/test_ragas") + pipeline = RAGQueryPipeline(config_path="./test.conf", server_url="http://openviking.test") assert pipeline.config_path == "./test.conf" - assert pipeline.data_path == "./test_data/test_ragas" + assert pipeline.server_url == "http://openviking.test" assert pipeline._client is None @@ -58,26 +57,24 @@ def test_async_record_writer_drains_records_before_stop_sentinel(): assert flushed == [{"id": 1}, {"id": 2}] -def test_pipeline_query_consumes_find_result_and_generates_answer(): +def test_pipeline_query_consumes_http_result_and_generates_answer(): class Client: def search(self, **kwargs): - return FindResult( - memories=[ - MatchedContext( - uri="viking://user/memories/profile.md", - context_type=ContextType.MEMORY, - overview="Profile overview", - ) + return { + "memories": [ + { + "uri": "viking://user/memories/profile.md", + "overview": "Profile overview", + } ], - resources=[ - MatchedContext( - uri="viking://resources/guide.md", - context_type=ContextType.RESOURCE, - abstract="Guide abstract", - ) + "resources": [ + { + "uri": "viking://resources/guide.md", + "abstract": "Guide abstract", + } ], - skills=[], - ) + "skills": [], + } class LLM: def get_completion(self, prompt): diff --git a/tests/eval/test_ragas_validation.py b/tests/eval/test_ragas_validation.py index bcec15d7fb..3ef4a44172 100644 --- a/tests/eval/test_ragas_validation.py +++ b/tests/eval/test_ragas_validation.py @@ -105,15 +105,15 @@ def test_pipeline_initialization(): from openviking.eval.ragas.pipeline import RAGQueryPipeline - pipeline = RAGQueryPipeline(config_path="./test.conf", data_path="./test_data/test_ragas") + pipeline = RAGQueryPipeline(config_path="./test.conf", server_url="http://openviking.test") assert pipeline.config_path == "./test.conf" - assert pipeline.data_path == "./test_data/test_ragas" + assert pipeline.server_url == "http://openviking.test" assert pipeline._client is None print(" ✅ RAGQueryPipeline initialized successfully") print(f" ✅ Config path: {pipeline.config_path}") - print(f" ✅ Data path: {pipeline.data_path}") + print(f" ✅ Server URL: {pipeline.server_url}") def test_question_loader(): diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 8f98d3d92c..2f379e4e39 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -22,9 +22,9 @@ import pytest_asyncio import uvicorn -from openviking import AsyncOpenViking from openviking.server.app import create_app from openviking.server.config import ServerConfig +from openviking.server.identity import RequestContext, Role from openviking.service.core import OpenVikingService from openviking_cli.session.user_id import UserIdentifier from openviking_cli.utils.config.open_viking_config import OpenVikingConfigSingleton @@ -128,7 +128,7 @@ def gemini_config_dict( query_param: str | None = None, doc_param: str | None = None, ) -> dict: - """Build a minimal embedded-mode config for Gemini-backed integration tests.""" + """Build a minimal service config for Gemini-backed integration tests.""" return { "storage": { "workspace": str(TEST_TMP_DIR / "gemini"), @@ -148,14 +148,16 @@ def gemini_config_dict( } -async def teardown_ov_client() -> None: - """Reset singleton client/config state used by embedded integration tests.""" - await AsyncOpenViking.reset() +async def teardown_ov_service(service: OpenVikingService | None = None) -> None: + if service is not None: + await service.close() OpenVikingConfigSingleton.reset_instance() -async def make_ov_client(config_dict: dict, data_path: str) -> AsyncOpenViking: - """Create an AsyncOpenViking client from an explicit config dict.""" +async def make_ov_service( + config_dict: dict, + data_path: str, +) -> tuple[OpenVikingService, RequestContext]: if not GOOGLE_API_KEY: pytest.skip("GOOGLE_API_KEY not set") try: @@ -163,8 +165,7 @@ async def make_ov_client(config_dict: dict, data_path: str) -> AsyncOpenViking: except (ImportError, ModuleNotFoundError, AttributeError): pytest.skip("google-genai not installed") - await teardown_ov_client() - + OpenVikingConfigSingleton.reset_instance() workspace = Path(data_path) shutil.rmtree(workspace, ignore_errors=True) workspace.mkdir(parents=True, exist_ok=True) @@ -174,31 +175,32 @@ async def make_ov_client(config_dict: dict, data_path: str) -> AsyncOpenViking: storage["workspace"] = str(workspace) storage.setdefault("agfs", {"backend": "local"}) storage.setdefault("vectordb", {"name": "test", "backend": "local", "project": "default"}) - OpenVikingConfigSingleton.initialize(config_dict=effective_config) - client = AsyncOpenViking(path=str(workspace)) - await client.initialize() - return client + user = UserIdentifier.the_default_user("gemini_test") + service = OpenVikingService(path=str(workspace), user=user) + await service.initialize() + return service, RequestContext(user=user, role=Role.USER) def sample_markdown(base_dir: Path, slug: str, content: str) -> Path: - """Write a markdown file for an integration test case.""" path = base_dir / f"{slug}.md" path.write_text(content, encoding="utf-8") return path @pytest_asyncio.fixture(scope="function") -async def gemini_ov_client(tmp_path): - """Provide a Gemini-backed OpenViking client and its model metadata.""" +async def gemini_ov_service(tmp_path): model = "gemini-embedding-2-preview" dim = 768 - client = await make_ov_client(gemini_config_dict(model, dim), str(tmp_path / "ov_gemini")) + service, ctx = await make_ov_service( + gemini_config_dict(model, dim), + str(tmp_path / "ov_gemini"), + ) try: - yield client, model, dim + yield service, ctx, model, dim finally: - await teardown_ov_client() + await teardown_ov_service(service) @pytest.fixture(scope="session") diff --git a/tests/integration/test_add_resource_index.py b/tests/integration/test_add_resource_index.py index be2386aebe..7723a89481 100644 --- a/tests/integration/test_add_resource_index.py +++ b/tests/integration/test_add_resource_index.py @@ -1,91 +1,21 @@ -import json -import os from unittest.mock import AsyncMock, MagicMock, patch import pytest -from openviking.async_client import AsyncOpenViking -from openviking_cli.utils.config import OPENVIKING_CONFIG_ENV -from openviking_cli.utils.config.open_viking_config import OpenVikingConfigSingleton -from tests.utils.mock_agfs import MockLocalAGFS - - -@pytest.fixture -def test_config(tmp_path): - """Create a temporary config file.""" - config_path = tmp_path / "ov.conf" - workspace = tmp_path / "workspace" - workspace.mkdir() - - config_content = { - "storage": { - "workspace": str(workspace), - "agfs": {"backend": "local", "port": 1833}, - "vectordb": {"backend": "local"}, - }, - "embedding": { - "dense": {"provider": "openai", "api_key": "fake", "model": "text-embedding-3-small"} - }, - "vlm": {"provider": "openai", "api_key": "fake", "model": "gpt-4-vision-preview"}, - } - config_path.write_text(json.dumps(config_content)) - return config_path - - -@pytest.fixture -async def client(test_config, tmp_path): - """Initialize AsyncOpenViking client with mocks.""" - - # Set config env var - os.environ[OPENVIKING_CONFIG_ENV] = str(test_config) - - # Reset Singletons - OpenVikingConfigSingleton._instance = None - await AsyncOpenViking.reset() - - mock_agfs = MockLocalAGFS(root_path=tmp_path / "mock_agfs_root") - - # Mock LLM/VLM services AND AGFS - with ( - patch("openviking.utils.summarizer.Summarizer.summarize") as mock_summarize, - patch("openviking.utils.index_builder.IndexBuilder.build_index") as mock_build_index, - patch("openviking.utils.agfs_utils.create_agfs_client", return_value=mock_agfs), - ): - # Make mocks return success - mock_summarize.return_value = {"status": "success"} - mock_build_index.return_value = {"status": "success"} - - client = AsyncOpenViking(path=str(test_config.parent)) - await client.initialize() - - yield client - - await client.close() - - # Cleanup - OpenVikingConfigSingleton._instance = None - if OPENVIKING_CONFIG_ENV in os.environ: - del os.environ[OPENVIKING_CONFIG_ENV] +from openviking.server.identity import RequestContext +from openviking.service.core import OpenVikingService @pytest.mark.asyncio -async def test_add_resource_indexing_logic(test_config, tmp_path): - """ - Integration-like test for add_resource indexing logic. - Uses Mock AGFS but tests the client logic. - """ - # Set config env var - os.environ[OPENVIKING_CONFIG_ENV] = str(test_config) - OpenVikingConfigSingleton._instance = None - await AsyncOpenViking.reset() - - # Create dummy resource +async def test_add_resource_indexing_logic( + service: OpenVikingService, + request_context: RequestContext, + tmp_path, +): + """The resource service must honor build_index and summarize independently.""" resource_file = tmp_path / "test_doc.md" resource_file.write_text("# Test Document\n\nThis is a test document.", encoding="utf-8") - mock_agfs = MockLocalAGFS(root_path=tmp_path / "mock_agfs_root") - - # Create mock parse result for Phase 1 (media processor) mock_parse_result = MagicMock() mock_parse_result.source_path = str(resource_file) mock_parse_result.meta = {} @@ -93,18 +23,16 @@ async def test_add_resource_indexing_logic(test_config, tmp_path): mock_parse_result.warnings = [] mock_parse_result.source_format = "markdown" - # Create mock context tree for Phase 2/3 (tree builder) mock_context_tree = MagicMock() mock_context_tree.root = MagicMock() mock_context_tree.root.uri = "viking://resources/test_doc" mock_context_tree.root.temp_uri = None - # Patch the Summarizer and IndexBuilder to verify calls with ( patch( - "openviking.utils.summarizer.Summarizer.summarize", new_callable=AsyncMock + "openviking.utils.summarizer.Summarizer.summarize", + new_callable=AsyncMock, ) as mock_summarize, - patch("openviking.utils.agfs_utils.create_agfs_client", return_value=mock_agfs), patch( "openviking.utils.media_processor.UnifiedResourceProcessor.process", new_callable=AsyncMock, @@ -118,42 +46,32 @@ async def test_add_resource_indexing_logic(test_config, tmp_path): ): mock_summarize.return_value = {"status": "success"} - client = AsyncOpenViking(path=str(test_config.parent)) - await client.initialize() - - try: - # 1. Test with build_index=True - await client.add_resource(path=str(resource_file), build_index=True, wait=True) - - # Verify summarizer called with skip_vectorization=False - assert mock_summarize.call_count == 1 - call_kwargs = mock_summarize.call_args.kwargs - assert call_kwargs.get("skip_vectorization") is False - - mock_summarize.reset_mock() - - # 2. Test with build_index=False, summarize=True - await client.add_resource( - path=str(resource_file), build_index=False, summarize=True, wait=True - ) - - # Verify summarizer called with skip_vectorization=True - assert mock_summarize.call_count == 1 - call_kwargs = mock_summarize.call_args.kwargs - assert call_kwargs.get("skip_vectorization") is True - - mock_summarize.reset_mock() - - # 3. Test with build_index=False, summarize=False - await client.add_resource( - path=str(resource_file), build_index=False, summarize=False, wait=True - ) - - # Verify summarizer NOT called - mock_summarize.assert_not_called() - - finally: - await client.close() - OpenVikingConfigSingleton._instance = None - if OPENVIKING_CONFIG_ENV in os.environ: - del os.environ[OPENVIKING_CONFIG_ENV] + await service.resources._execute_resource_ingestion( + path=str(resource_file), + ctx=request_context, + defer_post_processing=False, + build_index=True, + ) + assert mock_summarize.call_count == 1 + assert mock_summarize.call_args.kwargs.get("skip_vectorization") is False + + mock_summarize.reset_mock() + await service.resources._execute_resource_ingestion( + path=str(resource_file), + ctx=request_context, + defer_post_processing=False, + build_index=False, + summarize=True, + ) + assert mock_summarize.call_count == 1 + assert mock_summarize.call_args.kwargs.get("skip_vectorization") is True + + mock_summarize.reset_mock() + await service.resources._execute_resource_ingestion( + path=str(resource_file), + ctx=request_context, + defer_post_processing=False, + build_index=False, + summarize=False, + ) + mock_summarize.assert_not_called() diff --git a/tests/integration/test_agent_memory_e2e.py b/tests/integration/test_agent_memory_e2e.py index 87307d22ef..3fecdddf4c 100644 --- a/tests/integration/test_agent_memory_e2e.py +++ b/tests/integration/test_agent_memory_e2e.py @@ -33,8 +33,10 @@ import pytest -from openviking.client.local import LocalClient +from openviking.message import TextPart from openviking.server.config import load_server_config +from openviking.server.identity import RequestContext, Role +from openviking.service.core import OpenVikingService from openviking.session.memory.session_extract_context_provider import SessionExtractContextProvider from openviking.session.memory.utils import MemoryFileUtils from openviking.telemetry import tracer @@ -143,10 +145,15 @@ def _flush_tracer_provider() -> None: # ── Helpers ─────────────────────────────────────────────────────────────────── -def _wait_for_task(client: LocalClient, task_id: str, timeout_s: int = 600) -> None: +def _wait_for_task( + service: OpenVikingService, + ctx: RequestContext, + task_id: str, + timeout_s: int = 600, +) -> None: deadline = time.time() + timeout_s while time.time() < deadline: - task = run_async(client.get_task(task_id)) or {} + task = run_async(service.sessions.get_commit_task(task_id, ctx)) or {} status = task.get("status") if isinstance(task, dict) else getattr(task, "status", None) if status in {"completed", "failed", "cancelled"}: if status != "completed": @@ -156,25 +163,33 @@ def _wait_for_task(client: LocalClient, task_id: str, timeout_s: int = 600) -> N raise TimeoutError(f"Task timed out: {task_id}") -def _run_conversation(client: LocalClient, turns: List[Tuple[str, str]]) -> None: - session = run_async(client.create_session()) - session_id = session["session_id"] +def _run_conversation( + service: OpenVikingService, + ctx: RequestContext, + turns: List[Tuple[str, str]], +) -> None: + session = run_async(service.sessions.create(ctx)) + session_id = session.session_id logger.info(f" session_id = {session_id[:8]}...") for role, content in turns: - run_async(client.add_message(session_id=session_id, role=role, content=content)) + run_async(session.add_message_async(role, [TextPart(content)])) logger.info(f" Committing {len(turns)} messages...") - result = run_async(client.commit_session(session_id=session_id)) + result = run_async(service.sessions.commit(session_id, ctx)) task_id = ( result.get("task_id") if isinstance(result, dict) else getattr(result, "task_id", None) ) if task_id: - _wait_for_task(client, task_id) + _wait_for_task(service, ctx, task_id) logger.info(f" Done (task {task_id[:8]})") -def _list_non_overview_entries(client: LocalClient, uri: str) -> List[dict]: +def _list_non_overview_entries( + service: OpenVikingService, + ctx: RequestContext, + uri: str, +) -> List[dict]: try: - entries = run_async(client.ls(uri, simple=False)) or [] + entries = run_async(service.fs.ls(uri, ctx=ctx, simple=False)) or [] except Exception: return [] _INTERNAL_SUFFIXES = (".overview.md", ".abstract.md") @@ -194,14 +209,18 @@ def _entry_uri(entry: dict) -> str: return str(getattr(entry, "uri", "")) -def _collect_source_trajectories(client: LocalClient, exp_entries: List[dict]) -> List[str]: +def _collect_source_trajectories( + service: OpenVikingService, + ctx: RequestContext, + exp_entries: List[dict], +) -> List[str]: """Collect traj URIs from experience forward links (exp→traj, derived_from).""" all_uris: List[str] = [] for entry in exp_entries: exp_uri = _entry_uri(entry) if not exp_uri: continue - raw = run_async(client.read(exp_uri)) or "" + raw = run_async(service.fs.read(exp_uri, ctx=ctx)) or "" mf = MemoryFileUtils.read(raw) if raw else None if not mf: continue @@ -219,23 +238,17 @@ def _collect_source_trajectories(client: LocalClient, exp_entries: List[dict]) - def local_test_env() -> Iterator[Dict[str, object]]: local_path = Path.cwd() / ".tmp_agent_memory_e2e" / uuid.uuid4().hex[:8] local_path.mkdir(parents=True, exist_ok=True) - try: - yield { - "path": str(local_path), - "account_id": "default", - } - finally: - pass - # shutil.rmtree(local_path, ignore_errors=True) - - -def _build_client(env: Dict[str, object], user_id: str) -> LocalClient: - client = LocalClient( - path=str(env["path"]), - user=UserIdentifier(str(env["account_id"]), user_id), - ) - run_async(client.initialize()) - return client + yield {"path": str(local_path), "account_id": "default"} + + +def _build_service( + env: Dict[str, object], + user_id: str, +) -> tuple[OpenVikingService, RequestContext]: + user = UserIdentifier(str(env["account_id"]), user_id) + service = OpenVikingService(path=str(env["path"]), user=user) + run_async(service.initialize()) + return service, RequestContext(user=user, role=Role.USER) # ── Tests ───────────────────────────────────────────────────────────────────── @@ -263,46 +276,48 @@ def test_trajectory_and_experience_extraction( pytest.importorskip("opentelemetry") initialized = init_tracer_from_server_config(load_server_config()) if initialized is None or not tracer.is_enabled(): - pytest.fail( - "failed to initialize tracer; please check server.observability.traces" - ) + pytest.fail("failed to initialize tracer; please check server.observability.traces") trajectories_dir = "viking://user/alice/memories/trajectories" experiences_dir = "viking://user/alice/memories/experiences" - client = None + service = None try: with tracer.start_as_current_span( "tests.integration.test_trajectory_and_experience_extraction" ): print(f"\n[TEST] trace_id: {tracer.get_trace_id()}") - client = _build_client(local_test_env, user_id="alice") + service, ctx = _build_service(local_test_env, user_id="alice") logger.info("Round 1: flight booking duplicate (expect CREATE experience)") - _run_conversation(client, CONV_A_FLIGHT_DUPLICATE) + _run_conversation(service, ctx, CONV_A_FLIGHT_DUPLICATE) - traj_after_r1 = _list_non_overview_entries(client, trajectories_dir) - exp_after_r1 = _list_non_overview_entries(client, experiences_dir) + traj_after_r1 = _list_non_overview_entries(service, ctx, trajectories_dir) + exp_after_r1 = _list_non_overview_entries(service, ctx, experiences_dir) assert traj_after_r1, "should have trajectory memories after round 1" assert len(exp_after_r1) >= 1, ( "should have at least 1 experience after round 1 (CREATE path)" ) logger.info("Round 2: booking conflict extra cases (expect EDIT experience)") - _run_conversation(client, CONV_B_FLIGHT_DUPLICATE_EXTRA) + _run_conversation(service, ctx, CONV_B_FLIGHT_DUPLICATE_EXTRA) - traj_after_r2 = _list_non_overview_entries(client, trajectories_dir) - exp_after_r2 = _list_non_overview_entries(client, experiences_dir) + traj_after_r2 = _list_non_overview_entries(service, ctx, trajectories_dir) + exp_after_r2 = _list_non_overview_entries(service, ctx, experiences_dir) traj_uris_r2 = {_entry_uri(e) for e in traj_after_r2 if _entry_uri(e)} - source_trajectories = _collect_source_trajectories(client, exp_after_r2) + source_trajectories = _collect_source_trajectories( + service, + ctx, + exp_after_r2, + ) assert source_trajectories, "experience metadata should include source_trajectories" assert any(uri in traj_uris_r2 for uri in source_trajectories), ( "source_trajectories should reference extracted trajectories" ) finally: - if client is not None: - run_async(client.close()) + if service is not None: + run_async(service.close()) _flush_tracer_provider() diff --git a/tests/integration/test_encryption_integration.py b/tests/integration/test_encryption_integration.py index 80aa11b2e1..11cc4c71e9 100644 --- a/tests/integration/test_encryption_integration.py +++ b/tests/integration/test_encryption_integration.py @@ -14,7 +14,6 @@ import pytest import pytest_asyncio -from openviking import AsyncOpenViking from openviking.crypto.config import bootstrap_encryption from openviking.crypto.encryptor import FileEncryptor from openviking.crypto.providers import LocalFileProvider @@ -51,44 +50,6 @@ async def file_encryptor(tmp_path): return FileEncryptor(provider) -@pytest_asyncio.fixture(scope="function") -async def openviking_client_with_encryption(test_data_dir: Path, encryption_config): - """Fixture that provides an OpenViking client with encryption enabled""" - await AsyncOpenViking.reset() - OpenVikingConfigSingleton.reset_instance() - - # Clean data directory - if test_data_dir.exists(): - import shutil - - shutil.rmtree(test_data_dir) - test_data_dir.mkdir(parents=True, exist_ok=True) - - # Create config dict with encryption enabled - config_dict = {} - config_dict.update(encryption_config) - config_dict["storage"] = { - "workspace": str(test_data_dir / "workspace"), - "vectordb": {"name": "test", "backend": "local", "project": "default"}, - } - config_dict["embedding"] = { - "dense": {"provider": "openai", "api_key": "fake", "model": "text-embedding-3-small"} - } - config_dict["vlm"] = {"provider": "openai", "api_key": "fake", "model": "gpt-4-vision-preview"} - - # Initialize config singleton - OpenVikingConfigSingleton.initialize(config_dict=config_dict) - - client = AsyncOpenViking(path=str(test_data_dir)) - await client.initialize() - - yield client - - await client.close() - await AsyncOpenViking.reset() - OpenVikingConfigSingleton.reset_instance() - - class TestEncryptionBootstrap: """Tests for encryption module bootstrap""" @@ -154,81 +115,32 @@ async def test_encrypt_empty_data(self, file_encryptor): class TestEncryptionDisabled: - """Tests for behavior when encryption is disabled""" - - @pytest_asyncio.fixture(scope="function") - async def openviking_client_without_encryption(self, test_data_dir: Path): - """Fixture that provides an OpenViking client without encryption""" - await AsyncOpenViking.reset() - OpenVikingConfigSingleton.reset_instance() - - if test_data_dir.exists(): - import shutil - - shutil.rmtree(test_data_dir) - test_data_dir.mkdir(parents=True, exist_ok=True) - - # Create config dict with encryption disabled - config_dict = { - "encryption": {"enabled": False}, - "storage": { - "workspace": str(test_data_dir / "workspace"), - "vectordb": {"name": "test", "backend": "local", "project": "default"}, - }, - "embedding": { - "dense": { - "provider": "openai", - "api_key": "fake", - "model": "text-embedding-3-small", - } - }, - "vlm": {"provider": "openai", "api_key": "fake", "model": "gpt-4-vision-preview"}, - } - - # Initialize config singleton - OpenVikingConfigSingleton.initialize(config_dict=config_dict) - - client = AsyncOpenViking(path=str(test_data_dir)) - await client.initialize() - - yield client - - await client.close() - await AsyncOpenViking.reset() - OpenVikingConfigSingleton.reset_instance() + """Normal resource I/O remains available when encryption is disabled.""" async def test_read_write_without_encryption( - self, openviking_client_without_encryption: AsyncOpenViking, tmp_path: Path + self, + service: OpenVikingService, + request_context, + tmp_path: Path, ): - """Test normal file operations when encryption is disabled""" - client = openviking_client_without_encryption - test_file = tmp_path / "normal_file.txt" test_content = "Normal content without encryption" test_file.write_text(test_content) - result = await client.add_resource( - path=str(test_file), reason="Normal operation test", wait=True + result = await service.resources._execute_resource_ingestion( + path=str(test_file), + ctx=request_context, + defer_post_processing=False, + reason="Normal operation test", + build_index=False, ) - root_uri = result["root_uri"] - - # Get tree structure to find the actual file - uris = await client.tree(root_uri) - assert len(uris) > 0 - - # Find the actual file (skip .abstract.md and .overview.md) - found = False - for data in uris: - if not data["isDir"]: - filename = data["name"] - # Skip auto-generated files - if filename not in [".abstract.md", ".overview.md"]: - file_uri = data["uri"] - content = await client.read(file_uri) - assert content == test_content - found = True - break - assert found, "Could not find the test file" + entries = await service.fs.ls(result["root_uri"], ctx=request_context) + contents = [ + await service.fs.read(data["uri"], ctx=request_context) + for data in entries + if not data["isDir"] and data["name"] not in {".abstract.md", ".overview.md"} + ] + assert test_content in contents class TestVikingFSEncryptionWithAccounts: @@ -285,7 +197,6 @@ async def openviking_service_with_encryption(self, test_data_dir: Path, encrypti yield {"service": svc, "api_key_manager": api_key_manager, "test_data_dir": test_data_dir} await svc.close() - await AsyncOpenViking.reset() OpenVikingConfigSingleton.reset_instance() def _is_file_encrypted(self, file_path: Path) -> bool: diff --git a/tests/integration/test_full_workflow.py b/tests/integration/test_full_workflow.py deleted file mode 100644 index 285addf234..0000000000 --- a/tests/integration/test_full_workflow.py +++ /dev/null @@ -1,251 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 - -"""Full workflow integration tests""" - -import shutil -from pathlib import Path - -import pytest_asyncio - -from openviking import AsyncOpenViking -from openviking.message import TextPart - - -@pytest_asyncio.fixture(scope="function") -async def integration_client(test_data_dir: Path): - """Integration test client""" - await AsyncOpenViking.reset() - - # Clean data directory to avoid AGFS "directory already exists" errors - shutil.rmtree(test_data_dir, ignore_errors=True) - test_data_dir.mkdir(parents=True, exist_ok=True) - - client = AsyncOpenViking(path=str(test_data_dir)) - await client.initialize() - - yield client - - await client.close() - await AsyncOpenViking.reset() - - -class TestResourceToSearchWorkflow: - """Full workflow from resource addition to search""" - - async def test_add_and_search( - self, integration_client: AsyncOpenViking, sample_files: list[Path] - ): - """Test: add resource -> vectorize -> search""" - client = integration_client - - # 1. Add multiple resources - uris = [] - for f in sample_files: - result = await client.add_resource(path=str(f), reason="Integration test") - uris.append(result["root_uri"]) - - # 2. Wait for vectorization to complete - await client.wait_processed() - - # 3. Verify search - result = await client.find(query="batch file content") - - assert result.total >= 0 - - async def test_add_search_read_workflow( - self, integration_client: AsyncOpenViking, sample_markdown_file: Path - ): - """Test: add -> search -> read""" - client = integration_client - - # 1. Add resource - await client.add_resource(path=str(sample_markdown_file), reason="Workflow test", wait=True) - - # 2. Search - search_result = await client.find(query="sample document") - - # 3. Read searched resource - if search_result.resources: - uri = search_result.resources[0].uri - info = await client.stat(uri) - if info.get("isDir"): - res = await client.tree(uri) - for data in res: - if not data["isDir"]: - content = await client.read(data["uri"]) - assert len(content) > 0 - else: - content = await client.read(uri) - assert len(content) > 0 - - -class TestSessionWorkflow: - """Session management full workflow""" - - async def test_session_conversation_workflow( - self, integration_client: AsyncOpenViking, sample_markdown_file: Path - ): - """Test: session create -> multi-turn conversation -> commit -> memory extraction""" - client = integration_client - - # 1. Add resource - await client.add_resource( - path=str(sample_markdown_file), reason="Session workflow test", wait=True - ) - - # 2. Create session - session = client.session(session_id="workflow_test_session") - - # 3. Multi-turn conversation - session.add_message("user", [TextPart("Hello, I need help with testing.")]) - - # 4. Search and use context - search_result = await client.search(query="testing", session=session) - if search_result.resources: - session.used(contexts=[search_result.resources[0].uri]) - - session.add_message("assistant", [TextPart("I can help you with testing.")]) - - session.add_message("user", [TextPart("What features are available?")]) - session.add_message("assistant", [TextPart("There are many features available.")]) - - # 5. Commit - commit_result = session.commit() - assert commit_result["status"] == "accepted" - assert commit_result["task_id"] is not None - - # 6. Wait for memory extraction - await client.wait_processed() - - async def test_session_reload_workflow(self, integration_client: AsyncOpenViking): - """Test: session create -> commit -> reload -> continue conversation""" - client = integration_client - session_id = "reload_test_session" - - # 1. Create session and add messages - session1 = client.session(session_id=session_id) - session1.add_message("user", [TextPart("First message")]) - session1.add_message("assistant", [TextPart("First response")]) - commit_result1 = session1.commit() - assert commit_result1["status"] == "accepted" - assert commit_result1["task_id"] is not None - - # 2. Reload session - session2 = client.session(session_id=session_id) - await session2.load() - - # 3. Continue conversation - session2.add_message("user", [TextPart("Second message")]) - session2.add_message("assistant", [TextPart("Second response")]) - - # 4. Commit again - commit_result = session2.commit() - assert commit_result["status"] == "accepted" - assert commit_result["task_id"] is not None - - -class TestImportExportWorkflow: - """Import/export full workflow""" - - async def test_export_import_roundtrip( - self, integration_client: AsyncOpenViking, sample_markdown_file: Path, temp_dir: Path - ): - """Test: export -> delete -> import -> verify""" - client = integration_client - - # 1. Add resource - result = await client.add_resource( - path=str(sample_markdown_file), - reason="Export test", - ) - print(result) - original_uri = result["root_uri"] - - # 2. Read original content - original_content = "" - entries = await client.tree(original_uri) - for data in entries: - if not data["isDir"]: - original_content += await client.read(data["uri"]) - - # 3. Export - export_path = temp_dir / "workflow_export.ovpack" - await client.export_ovpack(original_uri, str(export_path)) - assert export_path.exists() - - # 4. Delete original resource - await client.rm(original_uri, recursive=True) - - # 5. Import - import_uri = await client.import_ovpack(str(export_path), "viking://resources/imported/") - - # 6. Verify content consistency - imported_content = "" - entries = await client.tree(import_uri) - for data in entries: - if not data["isDir"]: - imported_content += await client.read(data["uri"]) - assert original_content == imported_content - - -class TestFullEndToEndWorkflow: - """Full end-to-end workflow""" - - async def test_complete_workflow( - self, integration_client: AsyncOpenViking, sample_files: list[Path], temp_dir: Path - ): - """Test complete end-to-end workflow""" - client = integration_client - - # ===== Phase 1: Resource Management ===== - # Add multiple resources - resource_uris = [] - for f in sample_files: - result = await client.add_resource(path=str(f), reason="E2E test") - resource_uris.append(result["root_uri"]) - - # Wait for processing to complete - await client.wait_processed() - - # ===== Phase 2: Search Verification ===== - # Quick search - find_result = await client.find(query="batch file") - assert find_result.total >= 0 - - # ===== Phase 3: Session Management ===== - session = client.session(session_id="e2e_test_session") - - # Multi-turn conversation - session.add_message("user", [TextPart("I need information about batch files.")]) - - # Search with session context - search_result = await client.search(query="batch", session=session) - if search_result.resources: - session.used(contexts=[search_result.resources[0].uri]) - - session.add_message("assistant", [TextPart("Here is information about batch files.")]) - - # Commit session - commit_result = session.commit() - assert commit_result["status"] == "accepted" - assert commit_result["task_id"] is not None - - # ===== Phase 4: Import/Export ===== - if resource_uris: - # Export - export_path = temp_dir / "e2e_export.ovpack" - await client.export_ovpack(resource_uris[0], str(export_path)) - - # Import to new location - import_uri = await client.import_ovpack( - str(export_path), "viking://resources/e2e_imported/" - ) - - # Verify import success - await client.stat(import_uri) - - # ===== Phase 5: Cleanup Verification ===== - # List all resources - entries = await client.ls("viking://", recursive=True) - assert isinstance(entries, list) diff --git a/tests/integration/test_gemini_openviking_it.py b/tests/integration/test_gemini_openviking_it.py index a6946b75a3..210b861484 100644 --- a/tests/integration/test_gemini_openviking_it.py +++ b/tests/integration/test_gemini_openviking_it.py @@ -1,16 +1,6 @@ # Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. # SPDX-License-Identifier: AGPL-3.0 -""" -End-to-end integration tests for OpenViking add-memory + search using Gemini embeddings. - -Exercises the full workflow: inject Gemini config → add_resource → wait_processed → find/search. -No mocking — real Gemini API calls. Auto-skipped when GOOGLE_API_KEY is not set. - -Run: - GOOGLE_API_KEY= pytest tests/integration/test_gemini_openviking_it.py -v - -NOTE: provider MUST be "gemini" — "google" is not a valid provider value. -""" +"""End-to-end Service tests for local VectorDB with Gemini embeddings.""" from pathlib import Path @@ -18,69 +8,54 @@ from tests.integration.conftest import ( gemini_config_dict, - make_ov_client, + make_ov_service, requires_api_key, requires_engine, sample_markdown, - teardown_ov_client, + teardown_ov_service, ) pytestmark = [requires_api_key, requires_engine] -# --------------------------------------------------------------------------- -# Test 1: Basic add-memory + search -# --------------------------------------------------------------------------- - - -async def test_add_and_search_basic(gemini_ov_client, tmp_path): - """Add a single markdown document and verify it is returned by find().""" - client, model, dim = gemini_ov_client - +async def test_add_and_search_basic(gemini_ov_service, tmp_path): + service, ctx, _model, _dim = gemini_ov_service doc = sample_markdown( tmp_path, "ml_intro", "# Machine Learning\n\nMachine learning is a field of AI that uses statistical methods.", ) - result = await client.add_resource(path=str(doc), reason="IT test basic", wait=True) - assert result.get("root_uri"), "add_resource should return a root_uri" - - found = await client.find(query="machine learning AI statistical") - assert found.total > 0, f"Expected search results for ML doc, got total={found.total}" - scores = [r.score for r in found.resources] - assert any(s > 0.0 for s in scores), f"Expected non-zero similarity scores, got {scores}" - - -# --------------------------------------------------------------------------- -# Test 2: Batch — multiple documents, search returns relevant one -# --------------------------------------------------------------------------- + result = await service.resources.add_resource( + path=str(doc), + ctx=ctx, + reason="IT test basic", + wait=True, + ) + assert result.get("root_uri") + found = await service.search.find(query="machine learning AI statistical", ctx=ctx) + assert found.total > 0 + scores = [resource.score for resource in found.resources] + assert any(score > 0.0 for score in scores) -async def test_batch_documents_search(gemini_ov_client, tmp_path): - """Add 5 documents on different topics; search returns the relevant one first.""" - client, model, dim = gemini_ov_client +async def test_batch_documents_search(gemini_ov_service, tmp_path): + service, ctx, _model, _dim = gemini_ov_service docs = { "python_types": "Python supports dynamic typing and type hints via the typing module.", - "quantum_physics": "Quantum mechanics describes the behavior of particles at atomic scale.", - "cooking_pasta": "To cook pasta: boil salted water, add pasta, cook 8-12 minutes, drain.", - "git_branching": "Git branches allow parallel development. Use git checkout -b to create.", - "solar_system": "The solar system has 8 planets. Jupiter is the largest planet.", + "quantum_physics": "Quantum mechanics describes particles at atomic scale.", + "cooking_pasta": "To cook pasta: boil salted water, add pasta, cook, and drain.", + "git_branching": "Git branches allow parallel development.", + "solar_system": "The solar system has 8 planets. Jupiter is the largest.", } for slug, content in docs.items(): - doc_path = sample_markdown(tmp_path, slug, f"# {slug}\n\n{content}") - await client.add_resource(path=str(doc_path), reason="IT batch test") + doc = sample_markdown(tmp_path, slug, f"# {slug}\n\n{content}") + await service.resources.add_resource(path=str(doc), ctx=ctx, reason="IT batch test") - await client.wait_processed() - - found = await client.find(query="how to cook pasta boil water") - assert found.total > 0, "Expected at least one result for pasta query" - - -# --------------------------------------------------------------------------- -# Test 3: Large text chunking -# --------------------------------------------------------------------------- + await service.resources.wait_processed() + found = await service.search.find(query="how to cook pasta boil water", ctx=ctx) + assert found.total > 0 @pytest.mark.parametrize( @@ -91,29 +66,27 @@ async def test_batch_documents_search(gemini_ov_client, tmp_path): ], ) async def test_large_text_add_and_search(model, dim, token_limit, tmp_path): - """Add a document exceeding the model's token limit; verify chunking and searchability.""" data_path = str(tmp_path / "ov_large") Path(data_path).mkdir(parents=True, exist_ok=True) - - client = await make_ov_client(gemini_config_dict(model, dim), data_path) + service, ctx = await make_ov_service(gemini_config_dict(model, dim), data_path) try: phrase = "Neural networks are computational models inspired by the brain. " repeats = (token_limit * 2) // len(phrase.split()) + 10 - large_content = f"# Large Document\n\n{phrase * repeats}" - - doc = sample_markdown(tmp_path, "large_doc", large_content) - result = await client.add_resource(path=str(doc), reason="large text IT", wait=True) - assert result.get("root_uri"), "Large doc should index without error" - - found = await client.find(query="neural networks computational brain") - assert found.total > 0, "Chunked large doc should be findable" + doc = sample_markdown(tmp_path, "large_doc", f"# Large Document\n\n{phrase * repeats}") + result = await service.resources.add_resource( + path=str(doc), + ctx=ctx, + reason="large text IT", + wait=True, + ) + assert result.get("root_uri") + found = await service.search.find( + query="neural networks computational brain", + ctx=ctx, + ) + assert found.total > 0 finally: - await teardown_ov_client() - - -# --------------------------------------------------------------------------- -# Test 4: RETRIEVAL_QUERY / RETRIEVAL_DOCUMENT routing via EmbeddingConfig -# --------------------------------------------------------------------------- + await teardown_ov_service(service) @pytest.mark.parametrize( @@ -124,15 +97,14 @@ async def test_large_text_add_and_search(model, dim, token_limit, tmp_path): ], ) async def test_retrieval_routing_workflow(query_param, doc_param, tmp_path): - """Verify add+search works with non-symmetric task-type routing.""" - data_path = str(tmp_path / "ov_routing") - Path(data_path).mkdir(parents=True, exist_ok=True) - - client = await make_ov_client( + service, ctx = await make_ov_service( gemini_config_dict( - "gemini-embedding-2-preview", 768, query_param=query_param, doc_param=doc_param + "gemini-embedding-2-preview", + 768, + query_param=query_param, + doc_param=doc_param, ), - data_path, + str(tmp_path / "ov_routing"), ) try: doc = sample_markdown( @@ -140,70 +112,64 @@ async def test_retrieval_routing_workflow(query_param, doc_param, tmp_path): "routing_doc", "# Retrieval Test\n\nOpenViking provides memory management for AI agents.", ) - result = await client.add_resource(path=str(doc), reason="routing IT", wait=True) + result = await service.resources.add_resource( + path=str(doc), + ctx=ctx, + reason="routing IT", + wait=True, + ) assert result.get("root_uri") - - found = await client.find(query="memory management AI agents") - assert found.total > 0, f"Routing {query_param}/{doc_param}: expected search results" + found = await service.search.find(query="memory management AI agents", ctx=ctx) + assert found.total > 0 finally: - await teardown_ov_client() - - -# --------------------------------------------------------------------------- -# Test 5: Dimension variants — verify index schema uses requested dim -# --------------------------------------------------------------------------- + await teardown_ov_service(service) @pytest.mark.parametrize("dim", [512, 768, 1536, 3072]) async def test_dimension_variant_add_search(dim, tmp_path): - """Each dimension variant should index and search without errors.""" - data_path = str(tmp_path / f"ov_dim_{dim}") - Path(data_path).mkdir(parents=True, exist_ok=True) - - client = await make_ov_client(gemini_config_dict("gemini-embedding-2-preview", dim), data_path) + service, ctx = await make_ov_service( + gemini_config_dict("gemini-embedding-2-preview", dim), + str(tmp_path / f"ov_dim_{dim}"), + ) from openviking_cli.utils.config.open_viking_config import OpenVikingConfigSingleton - assert OpenVikingConfigSingleton.get_instance().embedding.dimension == dim, ( - f"Expected embedder dimension={dim}, got {OpenVikingConfigSingleton.get_instance().embedding.dimension}" - ) + assert OpenVikingConfigSingleton.get_instance().embedding.dimension == dim try: doc = sample_markdown( tmp_path, f"dim_doc_{dim}", - f"# Dimension {dim} Test\n\nThis document is indexed with embedding dimension {dim}.", + f"# Dimension {dim} Test\n\nThis document uses embedding dimension {dim}.", ) - result = await client.add_resource(path=str(doc), reason=f"dim={dim} IT", wait=True) - assert result.get("root_uri"), f"dim={dim}: add_resource should succeed" - - found = await client.find(query=f"embedding dimension {dim}") - assert found.total > 0, f"dim={dim}: should find the indexed doc" + result = await service.resources.add_resource( + path=str(doc), + ctx=ctx, + reason=f"dim={dim} IT", + wait=True, + ) + assert result.get("root_uri") + found = await service.search.find(query=f"embedding dimension {dim}", ctx=ctx) + assert found.total > 0 finally: - await teardown_ov_client() - + await teardown_ov_service(service) -# --------------------------------------------------------------------------- -# Test 6: Multi-turn session + search (smoke test) -# --------------------------------------------------------------------------- - -async def test_session_search_smoke(gemini_ov_client, tmp_path): - """Session construction + embedding-based find works with Gemini embeddings. - - Uses find() (pure embedding path) rather than search() which requires a VLM. - """ +async def test_session_search_smoke(gemini_ov_service, tmp_path): from openviking.message import TextPart - client, model, dim = gemini_ov_client - + service, ctx, _model, _dim = gemini_ov_service doc = sample_markdown( tmp_path, "session_doc", "# Python Testing\n\nPytest is a mature full-featured Python testing tool.", ) - await client.add_resource(path=str(doc), reason="session IT", wait=True) - - session = client.session(session_id="gemini_it_session") + await service.resources.add_resource( + path=str(doc), + ctx=ctx, + reason="session IT", + wait=True, + ) + session = await service.sessions.create(ctx, session_id="gemini_it_session") session.add_message("user", [TextPart("Tell me about Python testing.")]) - result = await client.find(query="pytest testing tool") - assert result.total > 0, "Embedding-based find should return the indexed pytest doc" + result = await service.search.find(query="pytest testing tool", ctx=ctx) + assert result.total > 0 diff --git a/tests/integration/test_vault_encryption_integration.py b/tests/integration/test_vault_encryption_integration.py index c080ce9330..403967e141 100644 --- a/tests/integration/test_vault_encryption_integration.py +++ b/tests/integration/test_vault_encryption_integration.py @@ -15,7 +15,6 @@ import pytest import pytest_asyncio -from openviking import AsyncOpenViking from openviking.crypto.config import bootstrap_encryption from openviking.crypto.encryptor import FileEncryptor from openviking.crypto.exceptions import AuthenticationFailedError, ConfigError @@ -212,47 +211,6 @@ async def vault_file_encryptor(): return FileEncryptor(provider) -@pytest_asyncio.fixture(scope="function") -async def openviking_client_with_vault_encryption(test_data_dir: Path, vault_encryption_config): - """Fixture that provides an OpenViking client with Vault encryption enabled""" - await AsyncOpenViking.reset() - OpenVikingConfigSingleton.reset_instance() - - # Clean data directory - if test_data_dir.exists(): - import shutil - - shutil.rmtree(test_data_dir) - test_data_dir.mkdir(parents=True, exist_ok=True) - - # Create config dict with encryption enabled - config_dict = {} - config_dict.update(vault_encryption_config) - config_dict["storage"] = { - "workspace": str(test_data_dir / "workspace"), - "vectordb": {"name": "test", "backend": "local", "project": "default"}, - } - config_dict["embedding"] = { - "dense": { - "provider": "openai", - "api_key": "fake", - "model": "text-embedding-3-small", - } - } - - # Initialize config singleton - OpenVikingConfigSingleton.initialize(config_dict=config_dict) - - client = AsyncOpenViking(path=str(test_data_dir)) - await client.initialize() - - yield client - - await client.close() - await AsyncOpenViking.reset() - OpenVikingConfigSingleton.reset_instance() - - class TestVaultEncryptionBootstrap: """Tests for encryption module bootstrap with Vault provider""" @@ -367,7 +325,6 @@ async def openviking_service_with_vault_encryption( yield {"service": svc, "api_key_manager": api_key_manager, "test_data_dir": test_data_dir} await svc.close() - await AsyncOpenViking.reset() OpenVikingConfigSingleton.reset_instance() def _is_file_encrypted(self, file_path: Path) -> bool: diff --git a/tests/integration/test_volcengine_kms_encryption_integration.py b/tests/integration/test_volcengine_kms_encryption_integration.py index 51b2f76544..83c822863a 100644 --- a/tests/integration/test_volcengine_kms_encryption_integration.py +++ b/tests/integration/test_volcengine_kms_encryption_integration.py @@ -17,7 +17,6 @@ import pytest import pytest_asyncio -from openviking import AsyncOpenViking from openviking.crypto.encryptor import FileEncryptor from openviking.crypto.providers import VolcengineKMSProvider from openviking.server.api_keys import APIKeyManager, is_new_format_key @@ -174,44 +173,6 @@ async def volcengine_file_encryptor(): return FileEncryptor(provider) -@pytest_asyncio.fixture(scope="function") -async def openviking_client_with_volcengine_encryption(test_data_dir: Path, volcengine_kms_config): - """Fixture that provides an OpenViking client with Volcengine KMS encryption""" - await AsyncOpenViking.reset() - OpenVikingConfigSingleton.reset_instance() - - if test_data_dir.exists(): - import shutil - - shutil.rmtree(test_data_dir) - test_data_dir.mkdir(parents=True, exist_ok=True) - - config_dict = {} - config_dict.update(volcengine_kms_config) - config_dict["storage"] = { - "workspace": str(test_data_dir / "workspace"), - "vectordb": {"name": "test", "backend": "local", "project": "default"}, - } - config_dict["embedding"] = { - "dense": { - "provider": "openai", - "api_key": "fake", - "model": "text-embedding-3-small", - } - } - - OpenVikingConfigSingleton.initialize(config_dict=config_dict) - - client = AsyncOpenViking(path=str(test_data_dir)) - await client.initialize() - - yield client - - await client.close() - await AsyncOpenViking.reset() - OpenVikingConfigSingleton.reset_instance() - - class TestVolcengineKMSEncryptionBootstrap: """Tests for encryption module bootstrap with Volcengine KMS""" @@ -274,81 +235,31 @@ async def test_encrypt_empty_data(self, volcengine_file_encryptor): class TestVolcengineKMSEncryptionDisabled: - """Tests for behavior when encryption is disabled""" - - @pytest_asyncio.fixture(scope="function") - async def openviking_client_without_encryption(self, test_data_dir: Path): - """Fixture that provides an OpenViking client without encryption""" - await AsyncOpenViking.reset() - OpenVikingConfigSingleton.reset_instance() - - if test_data_dir.exists(): - import shutil - - shutil.rmtree(test_data_dir) - test_data_dir.mkdir(parents=True, exist_ok=True) - - # Create config dict with encryption disabled - config_dict = { - "encryption": {"enabled": False}, - "storage": { - "workspace": str(test_data_dir / "workspace"), - "vectordb": {"name": "test", "backend": "local", "project": "default"}, - }, - "embedding": { - "dense": { - "provider": "openai", - "api_key": "fake", - "model": "text-embedding-3-small", - } - }, - } + """Normal resource I/O remains available when encryption is disabled.""" - # Initialize config singleton - OpenVikingConfigSingleton.initialize(config_dict=config_dict) - - client = AsyncOpenViking(path=str(test_data_dir)) - await client.initialize() - - yield client - - await client.close() - await AsyncOpenViking.reset() - OpenVikingConfigSingleton.reset_instance() - - @pytest.mark.asyncio async def test_read_write_without_encryption( - self, openviking_client_without_encryption: AsyncOpenViking, tmp_path: Path + self, + service: OpenVikingService, + request_context, + tmp_path: Path, ): - """Test normal file operations when encryption is disabled""" - client = openviking_client_without_encryption - test_file = tmp_path / "normal_file.txt" test_content = "Normal content without encryption" test_file.write_text(test_content) - result = await client.add_resource( - path=str(test_file), reason="Normal operation test", wait=True + result = await service.resources.add_resource( + path=str(test_file), + ctx=request_context, + reason="Normal operation test", + wait=True, ) - root_uri = result["root_uri"] - - # Get tree structure to find the actual file - uris = await client.tree(root_uri) - assert len(uris) > 0 - - # Find the actual file (skip .abstract.md and .overview.md) - found = False - for data in uris: - if not data["isDir"]: - filename = data["name"] - # Skip auto-generated files - if filename not in [".abstract.md", ".overview.md"]: - file_uri = data["uri"] - content = await client.read(file_uri) - assert content == test_content - found = True - break - assert found, "Could not find the test file" + entries = await service.fs.tree(result["root_uri"], ctx=request_context) + contents = [ + await service.fs.read(data["uri"], ctx=request_context) + for data in entries + if not data["isDir"] and data["name"] not in {".abstract.md", ".overview.md"} + ] + assert test_content in contents class TestVikingFSEncryptionWithVolcengineKMS: @@ -361,7 +272,6 @@ class TestVikingFSEncryptionWithVolcengineKMS: @pytest_asyncio.fixture(scope="function") async def openviking_service_with_volcengine_encryption(self, test_data_dir: Path): """Fixture that provides OpenVikingService with Volcengine KMS encryption""" - await AsyncOpenViking.reset() OpenVikingConfigSingleton.reset_instance() if test_data_dir.exists(): @@ -381,7 +291,6 @@ async def openviking_service_with_volcengine_encryption(self, test_data_dir: Pat yield {"service": svc, "api_key_manager": api_key_manager, "test_data_dir": test_data_dir} await svc.close() - await AsyncOpenViking.reset() OpenVikingConfigSingleton.reset_instance() def _is_file_encrypted(self, file_path: Path) -> bool: diff --git a/tests/integration/test_watch_e2e.py b/tests/integration/test_watch_e2e.py index db89ca71cb..11e430ae65 100644 --- a/tests/integration/test_watch_e2e.py +++ b/tests/integration/test_watch_e2e.py @@ -2,44 +2,40 @@ # SPDX-License-Identifier: AGPL-3.0 """End-to-end tests for resource watch functionality.""" -import shutil +from functools import partial from pathlib import Path import pytest import pytest_asyncio -from openviking import AsyncOpenViking from openviking.server.identity import RequestContext, Role from openviking.service.resource_service import ResourceService from openviking_cli.exceptions import ConflictError from openviking_cli.session.user_id import UserIdentifier -async def get_watch_task(client: AsyncOpenViking, to_uri: str): - watch_manager = client._service.resources._watch_scheduler.watch_manager +async def get_watch_task(service, ctx: RequestContext, to_uri: str): + watch_manager = service.resources._watch_scheduler.watch_manager return await watch_manager.get_task_by_uri( to_uri=to_uri, - account_id=client._service.user.account_id, - user_id=client._service.user.user_id, - role=str(Role.USER), + account_id=ctx.account_id, + user_id=ctx.user.user_id, + role=str(ctx.role), ) @pytest_asyncio.fixture(scope="function") -async def e2e_client(test_data_dir: Path): - """End-to-end test client with watch support.""" - await AsyncOpenViking.reset() - - shutil.rmtree(test_data_dir, ignore_errors=True) - test_data_dir.mkdir(parents=True, exist_ok=True) - - client = AsyncOpenViking(path=str(test_data_dir)) - await client.initialize() - - yield client - - await client.close() - await AsyncOpenViking.reset() +async def e2e_service(service, request_context: RequestContext, monkeypatch): + """End-to-end service with watch support.""" + monkeypatch.setattr( + service.resources, + "add_resource", + partial( + service.resources._execute_resource_ingestion, + defer_post_processing=False, + ), + ) + yield service, request_context @pytest_asyncio.fixture(scope="function") @@ -64,15 +60,14 @@ class TestWatchE2EBasicFlow: """End-to-end tests for basic watch flow.""" @pytest.mark.asyncio - async def test_create_resource_with_watch( - self, e2e_client: AsyncOpenViking, watch_test_file: Path - ): + async def test_create_resource_with_watch(self, e2e_service, watch_test_file: Path): """Test creating a resource with watch enabled.""" - client = e2e_client + service, ctx = e2e_service to_uri = "viking://resources/watch_e2e_test" - result = await client.add_resource( + result = await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=to_uri, reason="E2E watch test", @@ -84,7 +79,7 @@ async def test_create_resource_with_watch( assert "root_uri" in result assert result["root_uri"] == to_uri - task = await get_watch_task(client, to_uri) + task = await get_watch_task(service, ctx, to_uri) assert task is not None assert task.is_active is True assert task.watch_interval == 60.0 @@ -92,93 +87,100 @@ async def test_create_resource_with_watch( assert task.next_execution_time is not None @pytest.mark.asyncio - async def test_query_watch_status(self, e2e_client: AsyncOpenViking, watch_test_file: Path): + async def test_query_watch_status(self, e2e_service, watch_test_file: Path): """Test querying watch status for resources.""" - client = e2e_client + service, ctx = e2e_service watched_uri = "viking://resources/watched_resource" unwatched_uri = "viking://resources/unwatched_resource" - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=watched_uri, watch_interval=30.0, ) - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=unwatched_uri, watch_interval=0, ) - watched_task = await get_watch_task(client, watched_uri) + watched_task = await get_watch_task(service, ctx, watched_uri) assert watched_task is not None assert watched_task.is_active is True assert watched_task.watch_interval == 30.0 - unwatched_task = await get_watch_task(client, unwatched_uri) + unwatched_task = await get_watch_task(service, ctx, unwatched_uri) assert unwatched_task is None @pytest.mark.asyncio - async def test_update_watch_interval(self, e2e_client: AsyncOpenViking, watch_test_file: Path): + async def test_update_watch_interval(self, e2e_service, watch_test_file: Path): """Test updating watch interval.""" - client = e2e_client + service, ctx = e2e_service to_uri = "viking://resources/update_interval_test" - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=to_uri, watch_interval=30.0, ) - task = await get_watch_task(client, to_uri) + task = await get_watch_task(service, ctx, to_uri) assert task is not None assert task.watch_interval == 30.0 task_id = task.task_id - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=to_uri, watch_interval=0, ) - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=to_uri, watch_interval=120.0, ) - task = await get_watch_task(client, to_uri) + task = await get_watch_task(service, ctx, to_uri) assert task is not None assert task.is_active is True assert task.watch_interval == 120.0 assert task.task_id == task_id @pytest.mark.asyncio - async def test_cancel_watch(self, e2e_client: AsyncOpenViking, watch_test_file: Path): + async def test_cancel_watch(self, e2e_service, watch_test_file: Path): """Test cancelling watch by setting interval to 0 or negative.""" - client = e2e_client + service, ctx = e2e_service to_uri = "viking://resources/cancel_test" - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=to_uri, watch_interval=30.0, ) - task = await get_watch_task(client, to_uri) + task = await get_watch_task(service, ctx, to_uri) assert task is not None assert task.is_active is True - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=to_uri, watch_interval=0, ) - task = await get_watch_task(client, to_uri) + task = await get_watch_task(service, ctx, to_uri) assert task is not None assert task.is_active is False @@ -187,22 +189,22 @@ class TestWatchE2EConflictDetection: """End-to-end tests for conflict detection.""" @pytest.mark.asyncio - async def test_conflict_when_active_watch_exists( - self, e2e_client: AsyncOpenViking, watch_test_file: Path - ): + async def test_conflict_when_active_watch_exists(self, e2e_service, watch_test_file: Path): """Test that conflict is raised when trying to watch an already watched URI.""" - client = e2e_client + service, ctx = e2e_service to_uri = "viking://resources/conflict_test" - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=to_uri, watch_interval=30.0, ) with pytest.raises(ConflictError) as exc_info: - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=to_uri, watch_interval=60.0, @@ -212,43 +214,44 @@ async def test_conflict_when_active_watch_exists( assert to_uri in str(exc_info.value) @pytest.mark.asyncio - async def test_reactivate_inactive_watch( - self, e2e_client: AsyncOpenViking, watch_test_file: Path - ): + async def test_reactivate_inactive_watch(self, e2e_service, watch_test_file: Path): """Test reactivating an inactive watch task.""" - client = e2e_client + service, ctx = e2e_service to_uri = "viking://resources/reactivate_test" - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=to_uri, reason="Initial reason", watch_interval=30.0, ) - task = await get_watch_task(client, to_uri) + task = await get_watch_task(service, ctx, to_uri) assert task is not None task_id = task.task_id - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=to_uri, watch_interval=0, ) - task = await get_watch_task(client, to_uri) + task = await get_watch_task(service, ctx, to_uri) assert task is not None assert task.is_active is False - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=to_uri, reason="Reactivated reason", watch_interval=45.0, ) - task = await get_watch_task(client, to_uri) + task = await get_watch_task(service, ctx, to_uri) assert task is not None assert task.is_active is True assert task.watch_interval == 45.0 @@ -258,15 +261,14 @@ async def test_reactivate_inactive_watch( class TestWatchE2ESchedulerExecution: """End-to-end tests for scheduler execution.""" + class TestWatchE2EMultipleResources: """End-to-end tests for multiple resources.""" @pytest.mark.asyncio - async def test_multiple_watched_resources( - self, e2e_client: AsyncOpenViking, watch_test_file: Path - ): + async def test_multiple_watched_resources(self, e2e_service, watch_test_file: Path): """Test managing multiple watched resources.""" - client = e2e_client + service, ctx = e2e_service uris = [ "viking://resources/multi_test_1", @@ -277,66 +279,69 @@ async def test_multiple_watched_resources( intervals = [30.0, 60.0, 120.0] for uri, interval in zip(uris, intervals, strict=True): - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=uri, watch_interval=interval, ) for uri, expected_interval in zip(uris, intervals, strict=True): - task = await get_watch_task(client, uri) + task = await get_watch_task(service, ctx, uri) assert task is not None assert task.is_active is True assert task.watch_interval == expected_interval for uri in uris: - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=uri, watch_interval=0, ) for uri in uris: - task = await get_watch_task(client, uri) + task = await get_watch_task(service, ctx, uri) assert task is not None assert task.is_active is False @pytest.mark.asyncio - async def test_independent_watch_tasks( - self, e2e_client: AsyncOpenViking, watch_test_file: Path - ): + async def test_independent_watch_tasks(self, e2e_service, watch_test_file: Path): """Test that watch tasks are independent.""" - client = e2e_client + service, ctx = e2e_service uri1 = "viking://resources/independent_1" uri2 = "viking://resources/independent_2" - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=uri1, watch_interval=30.0, ) - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=uri2, watch_interval=60.0, ) - task1 = await get_watch_task(client, uri1) - task2 = await get_watch_task(client, uri2) + task1 = await get_watch_task(service, ctx, uri1) + task2 = await get_watch_task(service, ctx, uri2) assert task1 is not None assert task2 is not None assert task1.task_id != task2.task_id - await client.add_resource( + await service.resources.add_resource( + ctx=ctx, path=str(watch_test_file), to=uri1, watch_interval=0, ) - task1_after = await get_watch_task(client, uri1) - task2_after = await get_watch_task(client, uri2) + task1_after = await get_watch_task(service, ctx, uri1) + task2_after = await get_watch_task(service, ctx, uri2) assert task1_after is not None assert task1_after.is_active is False assert task2_after is not None @@ -371,9 +376,10 @@ async def process_skill(self, **kwargs): role=Role.USER, ) - result = await resource_service.add_resource( + result = await resource_service._execute_resource_ingestion( path=str(watch_test_file), ctx=ctx, + defer_post_processing=False, to="viking://resources/no_watch_test", watch_interval=30.0, ) @@ -382,7 +388,7 @@ async def process_skill(self, **kwargs): assert "root_uri" in result @pytest.mark.asyncio - async def test_watch_task_nonexistent_resource(self, e2e_client: AsyncOpenViking): - client = e2e_client - task = await get_watch_task(client, "viking://resources/nonexistent") + async def test_watch_task_nonexistent_resource(self, e2e_service): + service, ctx = e2e_service + task = await get_watch_task(service, ctx, "viking://resources/nonexistent") assert task is None diff --git a/tests/misc/test_vikingdb_observer.py b/tests/misc/test_vikingdb_observer.py deleted file mode 100644 index aff41d6d42..0000000000 --- a/tests/misc/test_vikingdb_observer.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 -""" -Test VikingDBObserver functionality -""" - -import asyncio - -import openviking as ov -from openviking.async_client import AsyncOpenViking - - -async def test_vikingdb_observer(): - """Test VikingDBObserver functionality""" - print("=== Test VikingDBObserver ===") - - # Reset singleton to ensure clean state from previous tests - await AsyncOpenViking.reset() - - client = ov.AsyncOpenViking(path="./test_data/test_vikingdb_observer") - - try: - # Initialize client - await client.initialize() - print("Client initialized successfully") - - # Test observer access - print("\n1. Test observer access:") - print(f"Observer service: {client.observer}") - - # Test QueueObserver - print("\n2. Test QueueObserver:") - queue_status = client.observer.queue - print(f"Type: {type(queue_status)}") - print(f"Is healthy: {queue_status.is_healthy}") - print(f"Has errors: {queue_status.has_errors}") - - # Test direct print - print("\n3. Test direct print QueueObserver:") - print(queue_status) - - # Test VikingDBObserver - print("\n4. Test VikingDBObserver:") - vikingdb_status = client.observer.vikingdb() - print(f"Type: {type(vikingdb_status)}") - print(f"Is healthy: {vikingdb_status.is_healthy}") - print(f"Has errors: {vikingdb_status.has_errors}") - - # Test direct print - print("\n5. Test direct print VikingDBObserver:") - print(vikingdb_status) - - # Test status string - print("\n6. Test status string:") - print(f"Status type: {type(vikingdb_status.status)}") - print(f"Status length: {len(vikingdb_status.status)}") - - # Test system status - print("\n7. Test system status:") - system_status = client.observer.system() - print(f"System is_healthy: {system_status.is_healthy}") - for name, component in system_status.components.items(): - print(f"\n{name}:") - print(f" is_healthy: {component.is_healthy}") - print(f" has_errors: {component.has_errors}") - print(f" status: {component.status[:100]}...") - - print("\n=== All tests completed ===") - - except Exception as e: - print(f"Error during test: {e}") - import traceback - - traceback.print_exc() - - finally: - await AsyncOpenViking.reset() - print("Client closed") - - -async def test_sync_client(): - """Test sync client""" - print("\n=== Test sync client ===") - - # Reset singleton to ensure clean state from previous tests - await AsyncOpenViking.reset() - - client = ov.OpenViking(path="./test_data/test_vikingdb_observer") - - try: - # Initialize - client.initialize() - print("Sync client initialized successfully") - - # Test observer access - print(f"Observer service: {client.observer}") - - # Test QueueObserver - print("\nQueueObserver status:") - print(client.observer.queue) - - # Test VikingDBObserver - print("\nVikingDBObserver status:") - print(client.observer.vikingdb()) - - print("\n=== Sync client test completed ===") - - except Exception as e: - print(f"Sync client test error: {e}") - import traceback - - traceback.print_exc() - - finally: - client.close() - await AsyncOpenViking.reset() - print("Sync client closed") - - -if __name__ == "__main__": - # Run async test - asyncio.run(test_vikingdb_observer()) - - # Run sync test - asyncio.run(test_sync_client()) diff --git a/tests/server/conftest.py b/tests/server/conftest.py index 43a921a66e..c5028fc688 100644 --- a/tests/server/conftest.py +++ b/tests/server/conftest.py @@ -17,7 +17,6 @@ import pytest_asyncio import uvicorn -from openviking import AsyncOpenViking from openviking.models.embedder.base import DenseEmbedderBase, EmbedResult from openviking.server.app import create_app from openviking.server.config import ServerConfig @@ -165,7 +164,7 @@ def upload_temp_dir(temp_dir: Path, monkeypatch) -> Path: @pytest_asyncio.fixture(scope="function") async def service(temp_dir: Path, monkeypatch): - """Create and initialize an OpenVikingService in embedded mode.""" + """Create and initialize an OpenVikingService for in-process API tests.""" fake_embedder_cls = _install_fake_embedder(monkeypatch) _install_fake_vlm(monkeypatch) svc = OpenVikingService( @@ -228,7 +227,6 @@ async def client_with_resource(client, service, sample_markdown_file): @pytest_asyncio.fixture(scope="function") async def running_server(temp_dir: Path, monkeypatch): """Start a real uvicorn server in a background thread.""" - await AsyncOpenViking.reset() fake_embedder_cls = _install_fake_embedder(monkeypatch) _install_fake_vlm(monkeypatch) @@ -289,4 +287,3 @@ async def _noop_mcp_lifespan(): server.should_exit = True thread.join(timeout=5) await svc.close() - await AsyncOpenViking.reset() diff --git a/tests/server/test_agent_evolution_global_setting.py b/tests/server/test_agent_evolution_global_setting.py index 3c191a9a71..f7632b6e6a 100644 --- a/tests/server/test_agent_evolution_global_setting.py +++ b/tests/server/test_agent_evolution_global_setting.py @@ -27,7 +27,7 @@ def test_agent_evolution_can_be_enabled_for_the_server(): assert config.agent_evolution.enabled is True -def test_embedded_session_service_preserves_agent_evolution_default( +def test_direct_session_service_preserves_agent_evolution_default( tmp_path, monkeypatch, ): diff --git a/tests/session/conftest.py b/tests/session/conftest.py index efba44e699..6488489e99 100644 --- a/tests/session/conftest.py +++ b/tests/session/conftest.py @@ -4,52 +4,130 @@ """Session test fixtures""" import asyncio +from functools import partial from typing import AsyncGenerator import pytest_asyncio -from openviking import AsyncOpenViking from openviking.message import TextPart, ToolPart -from openviking.service.task_tracker import TaskStatus, get_task_tracker, set_task_tracker +from openviking.server.identity import RequestContext +from openviking.service.core import OpenVikingService from openviking.session import Session +from openviking.storage.queuefs import QueueManager, SessionCommitMsg, get_queue_manager +from openviking.utils.time_utils import get_current_timestamp -@pytest_asyncio.fixture(autouse=True) -async def _drain_background_tasks(client: AsyncOpenViking): - """Wait for background commit tasks to finish before client teardown.""" - yield - # Drain asyncio.create_task() background tasks BEFORE client.close() - tracker = get_task_tracker() - for _ in range(100): # up to 10s - pending = [ - t - for t in await tracker.list_tasks() - if t.status in (TaskStatus.PENDING, TaskStatus.RUNNING) - ] - if not pending: - break - await asyncio.sleep(0.1) - set_task_tracker(None) +@pytest_asyncio.fixture(scope="function") +async def client( + service: OpenVikingService, + request_context: RequestContext, + monkeypatch, +) -> partial: + """Bind the shared service's session factory to the test request context.""" + + queue_manager = get_queue_manager() + original_enqueue = queue_manager.enqueue + commit_tasks = [] + + async def enqueue_with_session_commit_fallback(queue_name, data): + if queue_name != QueueManager.SESSION_COMMIT: + return await original_enqueue(queue_name, data) + + async def process_commit(): + message = SessionCommitMsg(**data) + queued_session = service.sessions.session( + request_context, + session_id=message.session_id, + session_uri=message.session_uri, + ) + while True: + phase1 = await queued_session._read_phase1_meta(message.archive_uri) + if phase1.get("status") == "ready" or await queued_session._archive_file_exists( + message.archive_uri, + ".failed.json", + ): + break + await asyncio.sleep(0) + await queued_session.load() + await queued_session.resume_queued_commit(message) + + commit_tasks.append(asyncio.create_task(process_commit())) + return data["task_id"] + + monkeypatch.setattr(queue_manager, "enqueue", enqueue_with_session_commit_fallback) + yield partial(service.sessions.session, request_context) + if commit_tasks: + await asyncio.gather(*commit_tasks, return_exceptions=True) + + +@pytest_asyncio.fixture(scope="function") +async def client_with_resource_sync( + client, + service: OpenVikingService, + request_context: RequestContext, +): + uri = "viking://resources/session-active-count.md" + timestamp = get_current_timestamp() + vector = service.vikingdb_manager.get_embedder().embed("active count test").dense_vector + await service.vikingdb_manager.upsert( + { + "uri": uri, + "parent_uri": "viking://resources", + "is_leaf": True, + "abstract": "Session active count test resource", + "context_type": "resource", + "category": "", + "created_at": timestamp, + "updated_at": timestamp, + "active_count": 0, + "vector": vector, + "meta": {}, + "related_uri": [], + "account_id": request_context.account_id, + "owner_space": "", + "level": 2, + }, + ctx=request_context, + ) + return service, request_context, uri @pytest_asyncio.fixture(scope="function") -async def session(client: AsyncOpenViking) -> AsyncGenerator[Session, None]: +async def session( + client, + service: OpenVikingService, + request_context: RequestContext, +) -> AsyncGenerator[Session, None]: """Create new Session""" - session = client.session() + session = await service.sessions.create(request_context) yield session @pytest_asyncio.fixture(scope="function") -async def session_with_id(client: AsyncOpenViking) -> AsyncGenerator[Session, None]: +async def session_with_id( + client, + service: OpenVikingService, + request_context: RequestContext, +) -> AsyncGenerator[Session, None]: """Create Session with specified ID""" - session = client.session(session_id="test_session_001") + session = await service.sessions.create( + request_context, + session_id="test_session_001", + ) yield session @pytest_asyncio.fixture(scope="function") -async def session_with_messages(client: AsyncOpenViking) -> AsyncGenerator[Session, None]: +async def session_with_messages( + client, + service: OpenVikingService, + request_context: RequestContext, +) -> AsyncGenerator[Session, None]: """Create Session with existing messages""" - session = client.session(session_id="test_session_with_messages") + session = await service.sessions.create( + request_context, + session_id="test_session_with_messages", + ) session.add_message("user", [TextPart("Hello, this is a test message.")]) session.add_message("assistant", [TextPart("Hello! How can I help you today?")]) @@ -61,10 +139,15 @@ async def session_with_messages(client: AsyncOpenViking) -> AsyncGenerator[Sessi @pytest_asyncio.fixture(scope="function") async def session_with_tool_call( - client: AsyncOpenViking, + client, + service: OpenVikingService, + request_context: RequestContext, ) -> AsyncGenerator[tuple[Session, str, str], None]: """Create Session with tool call""" - session = client.session(session_id="test_session_with_tool") + session = await service.sessions.create( + request_context, + session_id="test_session_with_tool", + ) tool_id = "test_tool_001" tool_part = ToolPart( diff --git a/tests/session/test_session_commit.py b/tests/session/test_session_commit.py index 90d8ce8a58..150afe3bbd 100644 --- a/tests/session/test_session_commit.py +++ b/tests/session/test_session_commit.py @@ -8,9 +8,9 @@ from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock -from openviking import AsyncOpenViking -from openviking.client.session import Session as ClientSession from openviking.message import TextPart +from openviking.server.identity import RequestContext +from openviking.service.core import OpenVikingService from openviking.service.task_tracker import get_task_tracker from openviking.session import Session @@ -49,9 +49,29 @@ async def test_commit_success(self, session_with_messages: Session): assert "memories_extracted" not in result async def test_commit_extracts_memories( - self, session_with_messages: Session, client: AsyncOpenViking + self, + session_with_messages: Session, + service: OpenVikingService, ): """Test commit kicks off background memory extraction""" + + async def extract_long_term_memories(**kwargs): + archive_uri = kwargs["archive_uri"] + await session_with_messages._viking_fs.write_file( + uri=f"{archive_uri}/memory_diff.json", + content=json.dumps({"archive_uri": archive_uri}), + ctx=session_with_messages.ctx, + ) + return [] + + session_with_messages._session_compressor.extract_long_term_memories = AsyncMock( + side_effect=extract_long_term_memories + ) + if hasattr(session_with_messages._session_compressor, "extract_execution_memories"): + session_with_messages._session_compressor.extract_execution_memories = AsyncMock( + return_value={"contexts": [], "session_skills": []} + ) + result = await session_with_messages.commit_async() task_id = result["task_id"] @@ -74,7 +94,7 @@ async def test_commit_extracts_memories( assert isinstance(memory_counts, dict) # Wait for semantic/embedding queues - await client.wait_processed(timeout=60.0) + await service.resources.wait_processed(timeout=60.0) async def test_commit_default_disables_agent_memory_but_keeps_archive( self, session_with_messages: Session @@ -167,6 +187,7 @@ async def test_commit_reports_session_skills_separately( config = MagicMock() config.memory.extraction_enabled = True config.memory.session_skill_extraction_enabled = True + config.vlm = SimpleNamespace(is_available=lambda: False) monkeypatch.setattr("openviking.session.session.get_openviking_config", lambda: config) session_with_messages._session_compressor.extract_long_term_memories = AsyncMock( @@ -199,12 +220,16 @@ async def test_commit_reports_session_skills_separately( assert task_result["result"]["session_skills_extracted"] == 0 assert task_result["result"]["session_skill_uris"] == [] assert "memory_diff_uri" not in task_result["result"] - session_with_messages._session_compressor.extract_long_term_memories.assert_not_awaited() if hasattr(session_with_messages._session_compressor, "extract_execution_memories"): + session_with_messages._session_compressor.extract_long_term_memories.assert_not_awaited() session_with_messages._session_compressor.extract_execution_memories.assert_awaited_once() call_kwargs = session_with_messages._session_compressor.extract_execution_memories.call_args.kwargs assert call_kwargs["allowed_memory_types"] == {"trajectories"} assert call_kwargs["include_session_skills"] is True + else: + session_with_messages._session_compressor.extract_long_term_memories.assert_awaited_once() + call_kwargs = session_with_messages._session_compressor.extract_long_term_memories.call_args.kwargs + assert call_kwargs["allowed_memory_types"] == {"trajectories"} async def test_commit_skips_session_skills_without_execution_memory_type( self, session_with_messages: Session, monkeypatch @@ -314,7 +339,7 @@ async def fake_extract(*args, **kwargs): async def test_commit_routes_peer_memory_with_single_full_context_pass( self, - client: AsyncOpenViking, + client, monkeypatch, ): """Peer memory uses one full-context extraction and operation-level routing.""" @@ -323,7 +348,8 @@ async def test_commit_routes_peer_memory_with_single_full_context_pass( config.memory.session_skill_extraction_enabled = True monkeypatch.setattr("openviking.session.session.get_openviking_config", lambda: config) - session = client.session(session_id="peer_memory_role_routing_test") + session = client(session_id="peer_memory_role_routing_test") + await session.ensure_exists() long_term_calls: list[dict] = [] execution_calls: list[dict] = [] @@ -430,9 +456,10 @@ async def test_commit_empty_session(self, session: Session): assert isinstance(result, dict) assert result.get("archived") is False - async def test_commit_multiple_times(self, client: AsyncOpenViking): + async def test_commit_multiple_times(self, client): """Test multiple commits""" - session = client.session(session_id="multi_commit_test") + session = client(session_id="multi_commit_test") + await session.ensure_exists() # First round of conversation session.add_message("user", [TextPart("First round message")]) @@ -452,7 +479,11 @@ async def test_commit_multiple_times(self, client: AsyncOpenViking): assert result2.get("task_id") is not None async def test_commit_keep_recent_count_retains_live_tail_and_resets_pending_tokens( - self, client: AsyncOpenViking, monkeypatch + self, + client, + service: OpenVikingService, + request_context: RequestContext, + monkeypatch, ): config = MagicMock() config.memory.extraction_enabled = True @@ -460,7 +491,9 @@ async def test_commit_keep_recent_count_retains_live_tail_and_resets_pending_tok config.vlm = SimpleNamespace(is_available=lambda: False) monkeypatch.setattr("openviking.session.session.get_openviking_config", lambda: config) - session = client.session(session_id="commit_keep_recent_count_test") + session = client(session_id="commit_keep_recent_count_test") + await session.ensure_exists() + session._session_compressor.extract_long_term_memories = AsyncMock(return_value=[]) session.add_message("user", [TextPart("Round 1 user")]) session.add_message("assistant", [TextPart("Round 1 assistant")]) @@ -477,8 +510,8 @@ async def test_commit_keep_recent_count_retains_live_tail_and_resets_pending_tok "Round 2 assistant", ] - session_info = await client.get_session(session.session_id) - assert session_info["pending_tokens"] == 0 + persisted = await service.sessions.get(session.session_id, request_context) + assert persisted.meta.pending_tokens == 0 context = await session.get_session_context() assert context["latest_archive_overview"] @@ -487,74 +520,8 @@ async def test_commit_keep_recent_count_retains_live_tail_and_resets_pending_tok "Round 2 assistant", ] - async def test_session_commit_keeps_telemetry_as_first_positional_argument(self): - calls = [] - - class _FakeClient: - async def commit_session( - self, - session_id, - telemetry=False, - *, - keep_recent_count=0, - ): - calls.append( - { - "session_id": session_id, - "telemetry": telemetry, - "keep_recent_count": keep_recent_count, - } - ) - return {"task_id": "task-1"} - - session = ClientSession(_FakeClient(), "s1", "user-1") - - result = await session.commit(True) - - assert result == {"task_id": "task-1"} - assert calls == [ - { - "session_id": "s1", - "telemetry": True, - "keep_recent_count": 0, - } - ] - - async def test_session_commit_async_keeps_telemetry_as_first_positional_argument(self): - calls = [] - - class _FakeClient: - async def commit_session( - self, - session_id, - telemetry=False, - *, - keep_recent_count=0, - ): - calls.append( - { - "session_id": session_id, - "telemetry": telemetry, - "keep_recent_count": keep_recent_count, - } - ) - return {"task_id": "task-1"} - - session = ClientSession(_FakeClient(), "s1", "user-1") - - result = await session.commit_async(True) - - assert result == {"task_id": "task-1"} - assert calls == [ - { - "session_id": "s1", - "telemetry": True, - "keep_recent_count": 0, - } - ] - async def test_commit_uses_latest_archive_overview_for_summary_and_extraction( - self, client: AsyncOpenViking, monkeypatch + self, client, monkeypatch ): """Second commit should pass the latest completed archive overview into Phase 2.""" config = MagicMock() @@ -563,11 +530,13 @@ async def test_commit_uses_latest_archive_overview_for_summary_and_extraction( config.vlm = SimpleNamespace(is_available=lambda: False) monkeypatch.setattr("openviking.session.session.get_openviking_config", lambda: config) - session = client.session(session_id="latest_overview_threading_test") + session = client(session_id="latest_overview_threading_test") + await session.ensure_exists() session._meta.memory_policy = { "peer": {"enabled": False}, "memory_types": ["profile"], } + session._session_compressor.extract_long_term_memories = AsyncMock(return_value=[]) session.add_message("user", [TextPart("First round message")]) session.add_message("assistant", [TextPart("First round response")]) @@ -608,10 +577,8 @@ async def capture_extract(*args, **kwargs): assert seen["extract"] == previous_overview async def test_active_count_incremented_after_commit(self, client_with_resource_sync: tuple): - client, uri = client_with_resource_sync - vikingdb = client._client.service.vikingdb_manager - # Use the client's own context to match the account_id used when adding the resource - client_ctx = client._client._ctx + service, client_ctx, uri = client_with_resource_sync + vikingdb = service.vikingdb_manager # Look up the record by URI records_before = await vikingdb.get_context_by_uri( @@ -623,7 +590,12 @@ async def test_active_count_incremented_after_commit(self, client_with_resource_ count_before = records_before[0].get("active_count") or 0 # Mark as used and commit - session = client.session(session_id="active_count_regression_test") + session = service.sessions.session( + client_ctx, + session_id="active_count_regression_test", + ) + await session.ensure_exists() + session._session_compressor.extract_long_term_memories = AsyncMock(return_value=[]) session.add_message("user", [TextPart("Query")]) session.used(contexts=[uri]) session.add_message("assistant", [TextPart("Answer")]) @@ -646,14 +618,13 @@ async def test_active_count_incremented_after_commit(self, client_with_resource_ f"active_count not incremented: before={count_before}, after={count_after}" ) - async def test_commit_failed_after_long_term_extraction_failure_does_not_block( - self, client: AsyncOpenViking - ): + async def test_commit_failed_after_long_term_extraction_failure_does_not_block(self, client): """Binary archive outcome: if long-term extraction fails (after retries), the whole archive is marked .failed.json and skipped — there is no partial state — but a failed archive must not block the next commit. """ - session = client.session(session_id="failed_archive_does_not_block_commit") + session = client(session_id="failed_archive_does_not_block_commit") + await session.ensure_exists() async def failing_extract(*args, **kwargs): del args, kwargs diff --git a/tests/session/test_session_commit_race.py b/tests/session/test_session_commit_race.py index 25e2ab44f4..5baf62498c 100644 --- a/tests/session/test_session_commit_race.py +++ b/tests/session/test_session_commit_race.py @@ -5,16 +5,16 @@ import asyncio -from openviking import AsyncOpenViking from openviking.message import TextPart class TestCommitRace: """Test concurrent commit safety.""" - async def test_concurrent_commit_no_duplicate(self, client: AsyncOpenViking): + async def test_concurrent_commit_no_duplicate(self, client): """Two concurrent commits on the same session: only one should archive.""" - session = client.session(session_id="race_test_dedup") + session = client(session_id="race_test_dedup") + await session.ensure_exists() session.add_message("user", [TextPart("Hello")]) session.add_message("assistant", [TextPart("Hi there")]) @@ -34,11 +34,12 @@ async def test_concurrent_commit_no_duplicate(self, client: AsyncOpenViking): async def test_message_added_during_commit_not_lost( self, - client: AsyncOpenViking, + client, monkeypatch, ): """Messages added while commit is running should not be lost.""" - session = client.session(session_id="race_test_msg_safety") + session = client(session_id="race_test_msg_safety") + await session.ensure_exists() session.add_message("user", [TextPart("Original message")]) # Use an Event for deterministic synchronization instead of sleeps diff --git a/tests/session/test_session_context.py b/tests/session/test_session_context.py index 9777af485a..1a3a17a303 100644 --- a/tests/session/test_session_context.py +++ b/tests/session/test_session_context.py @@ -5,36 +5,19 @@ import asyncio import json -from unittest.mock import patch +from functools import partial import pytest import pytest_asyncio -from openviking import AsyncOpenViking from openviking.message import Message, TextPart -from openviking.models.embedder.base import DenseEmbedderBase, EmbedResult +from openviking.server.identity import RequestContext +from openviking.service.core import OpenVikingService from openviking.service.task_tracker import get_task_tracker from openviking.session import Session from openviking.storage.queuefs import QueueManager, SessionCommitMsg, get_queue_manager -from openviking_cli.utils.config import OPENVIKING_CONFIG_ENV -from openviking_cli.utils.config.embedding_config import EmbeddingConfig -from openviking_cli.utils.config.open_viking_config import OpenVikingConfigSingleton +from openviking_cli.utils.config import get_openviking_config from openviking_cli.utils.config.vlm_config import VLMConfig -from tests.utils.mock_agfs import MockLocalAGFS - - -def _install_fake_embedder(monkeypatch): - class FakeEmbedder(DenseEmbedderBase): - def __init__(self): - super().__init__(model_name="test-fake-embedder") - - def embed(self, text: str, is_query: bool = False) -> EmbedResult: - return EmbedResult(dense_vector=[0.1] * 1024) - - def get_dimension(self) -> int: - return 1024 - - monkeypatch.setattr(EmbeddingConfig, "get_embedder", lambda self: FakeEmbedder()) def _install_fake_vlm(monkeypatch): @@ -49,74 +32,39 @@ async def _fake_get_vision_completion(self, prompt, images, thinking=False): monkeypatch.setattr(VLMConfig, "get_vision_completion_async", _fake_get_vision_completion) -def _write_test_config(tmp_path): - config_path = tmp_path / "ov.conf" - config_path.write_text( - json.dumps( - { - "storage": { - "workspace": str(tmp_path / "workspace"), - "agfs": {"backend": "local"}, - "vectordb": {"backend": "local"}, - }, - "embedding": { - "dense": { - "provider": "openai", - "model": "test-embedder", - "api_base": "http://127.0.0.1:11434/v1", - "dimension": 1024, - } - }, - "encryption": {"enabled": False}, - "memory": {"extraction_enabled": False}, - } - ), - encoding="utf-8", - ) - return config_path - - @pytest_asyncio.fixture(scope="function") -async def client(test_data_dir, monkeypatch, tmp_path): - config_path = _write_test_config(tmp_path) - mock_agfs = MockLocalAGFS(root_path=tmp_path / "mock_agfs_root") - - OpenVikingConfigSingleton.reset_instance() - await AsyncOpenViking.reset() - monkeypatch.setenv(OPENVIKING_CONFIG_ENV, str(config_path)) - _install_fake_embedder(monkeypatch) +async def client( + service: OpenVikingService, + request_context: RequestContext, + monkeypatch, +): _install_fake_vlm(monkeypatch) - - with patch("openviking.utils.agfs_utils.create_agfs_client", return_value=mock_agfs): - client = AsyncOpenViking(path=str(test_data_dir)) - await client.initialize() - - # MockLocalAGFS provides ordinary file operations but not QueueFS's - # virtual enqueue/dequeue endpoints. Execute SessionCommit jobs through - # the real resume path so these context tests remain about context - # assembly rather than the storage mock's missing queue protocol. - queue_manager = get_queue_manager() - original_enqueue = queue_manager.enqueue - - async def enqueue_with_session_commit_fallback(queue_name, data): - if queue_name != QueueManager.SESSION_COMMIT: - return await original_enqueue(queue_name, data) - - async def process_commit(): + monkeypatch.setattr(get_openviking_config().memory, "extraction_enabled", False) + client = partial(service.sessions.session, request_context) + + queue_manager = get_queue_manager() + original_enqueue = queue_manager.enqueue + + async def enqueue_with_session_commit_fallback(queue_name, data): + if queue_name != QueueManager.SESSION_COMMIT: + return await original_enqueue(queue_name, data) + + async def process_commit(): + message = SessionCommitMsg(**data) + queued_session = client(session_id=message.session_id) + while True: + phase1 = await queued_session._read_phase1_meta(message.archive_uri) + if phase1.get("status") == "ready": + break await asyncio.sleep(0) - queued_session = client.session(session_id=data["session_id"]) - await queued_session.load() - await queued_session.resume_queued_commit(SessionCommitMsg(**data)) + await queued_session.load() + await queued_session.resume_queued_commit(message) - asyncio.create_task(process_commit()) - return data["task_id"] + asyncio.create_task(process_commit()) + return data["task_id"] - monkeypatch.setattr(queue_manager, "enqueue", enqueue_with_session_commit_fallback) - yield client - await client.close() - - OpenVikingConfigSingleton.reset_instance() - await AsyncOpenViking.reset() + monkeypatch.setattr(queue_manager, "enqueue", enqueue_with_session_commit_fallback) + yield client def _estimate_tokens(text: str) -> int: @@ -134,9 +82,10 @@ async def _wait_for_task(task_id: str, timeout: float = 30.0) -> dict: async def test_oversized_legacy_assistant_only_commit_completes_without_checkpoint( - client: AsyncOpenViking, + client: partial, ): - session = client.session(session_id="legacy_assistant_only_turn_test") + session = client(session_id="legacy_assistant_only_turn_test") + await session.ensure_exists() for index in range(3): session.add_message("assistant", [TextPart(str(index) * 1000)]) @@ -181,9 +130,10 @@ async def test_get_context_with_max_messages(self, session_with_messages: Sessio assert isinstance(context, dict) assert len(context["current_messages"]) <= 2 - async def test_get_context_returns_latest_completed_archive_only(self, client: AsyncOpenViking): + async def test_get_context_returns_latest_completed_archive_only(self, client: partial): """Current context should expose only the latest completed archive overview.""" - session = client.session(session_id="archive_context_test") + session = client(session_id="archive_context_test") + await session.ensure_exists() session.add_message("user", [TextPart("First message")]) session.add_message("assistant", [TextPart("First response")]) @@ -207,9 +157,10 @@ async def test_get_context_returns_latest_completed_archive_only(self, client: A assert context["latest_archive_overview"] == latest_overview assert len(context["current_messages"]) == 1 - async def test_get_context_skips_incomplete_latest_archive(self, client: AsyncOpenViking): + async def test_get_context_skips_incomplete_latest_archive(self, client: partial): """Incomplete archives without .done must not replace the latest completed overview.""" - session = client.session(session_id="archive_context_incomplete_test") + session = client(session_id="archive_context_incomplete_test") + await session.ensure_exists() session.add_message("user", [TextPart("First message")]) session.add_message("assistant", [TextPart("First response")]) @@ -230,9 +181,10 @@ async def test_get_context_skips_incomplete_latest_archive(self, client: AsyncOp assert context["latest_archive_overview"] == completed_overview - async def test_get_context_includes_incomplete_archive_messages(self, client: AsyncOpenViking): + async def test_get_context_includes_incomplete_archive_messages(self, client: partial): """Pending archive messages should be merged with current live messages.""" - session = client.session(session_id="archive_context_pending_messages_test") + session = client(session_id="archive_context_pending_messages_test") + await session.ensure_exists() session.add_message("user", [TextPart("First message")]) result = await session.commit_async() @@ -261,11 +213,10 @@ async def test_get_context_includes_incomplete_archive_messages(self, client: As "Current live message", ] - async def test_get_context_max_messages_applies_after_pending_merge( - self, client: AsyncOpenViking - ): + async def test_get_context_max_messages_applies_after_pending_merge(self, client: partial): """max_messages should trim the merged pending + live message sequence.""" - session = client.session(session_id="archive_context_pending_max_messages_test") + session = client(session_id="archive_context_pending_max_messages_test") + await session.ensure_exists() session.add_message("user", [TextPart("First message")]) result = await session.commit_async() @@ -300,9 +251,10 @@ async def test_get_context_empty_session(self, session: Session): assert context["latest_archive_overview"] == "" assert context["current_messages"] == [] - async def test_get_context_after_commit(self, client: AsyncOpenViking): + async def test_get_context_after_commit(self, client: partial): """Test getting context after commit""" - session = client.session(session_id="post_commit_context_test") + session = client(session_id="post_commit_context_test") + await session.ensure_exists() session.add_message("user", [TextPart("Test message before commit")]) session.add_message("assistant", [TextPart("Response before commit")]) @@ -319,10 +271,11 @@ async def test_get_context_after_commit(self, client: AsyncOpenViking): assert len(context["current_messages"]) == 1 async def test_get_context_tracks_multiple_rapid_commits_by_done_boundary( - self, client: AsyncOpenViking, monkeypatch + self, client: partial, monkeypatch ): """Context should only advance latest overview when the earlier archive is .done.""" - session = client.session(session_id="archive_context_done_boundary_test") + session = client(session_id="archive_context_done_boundary_test") + await session.ensure_exists() first_gate = asyncio.Event() second_gate = asyncio.Event() second_started = asyncio.Event() @@ -386,9 +339,10 @@ class TestGetSessionContext: """Test get_session_context""" async def test_get_session_context_returns_latest_archive_overview_and_history( - self, client: AsyncOpenViking, monkeypatch + self, client: partial, monkeypatch ): - session = client.session(session_id="assemble_trim_test") + session = client(session_id="assemble_trim_test") + await session.ensure_exists() summaries = [ "# Session Summary\n\n" + ("A" * 80), "# Session Summary\n\n" + ("B" * 20), @@ -446,10 +400,11 @@ async def test_get_session_context_counts_active_tool_parts( assert context["stats"]["activeTokens"] > _estimate_tokens("Executing tool...") async def test_get_session_context_stops_at_newest_terminal_without_abstracts( - self, client: AsyncOpenViking, monkeypatch + self, client: partial, monkeypatch ): """Newest completed supplies overview; abstracts are never read for context.""" - session = client.session(session_id="assemble_lazy_read_test") + session = client(session_id="assemble_lazy_read_test") + await session.ensure_exists() summaries = [ "# Summary\n\n" + ("A" * 80), "# Summary\n\n" + ("B" * 80), @@ -494,13 +449,15 @@ async def tracking_read_file(*args, **kwargs): abstract_reads = [u for u in read_uris if u.endswith(".abstract.md")] assert abstract_reads == [], f"Old abstracts must not be read, got: {abstract_reads}" overview_reads = [u for u in read_uris if u.endswith(".overview.md")] - assert overview_reads, f"Newest terminal overview should be available, got: {overview_reads}" + assert overview_reads, ( + f"Newest terminal overview should be available, got: {overview_reads}" + ) assert any("archive_003" in u for u in overview_reads), ( f"Newest completed overview should be read, got: {overview_reads}" ) async def test_get_session_context_does_not_touch_archives_older_than_terminal( - self, client: AsyncOpenViking, monkeypatch + self, client: partial, monkeypatch ): """Scan cost must not grow with history length. @@ -508,7 +465,8 @@ async def test_get_session_context_does_not_touch_archives_older_than_terminal( terminal marker: no marker, overview, meta or messages read may reference an older archive. """ - session = client.session(session_id="assemble_terminal_stop_cost_test") + session = client(session_id="assemble_terminal_stop_cost_test") + await session.ensure_exists() summaries = [f"# Summary\n\narchive {index}" for index in range(1, 5)] async def fake_generate(self, _messages, latest_archive_overview="", **kwargs): @@ -546,15 +504,15 @@ async def tracking_exists(*args, **kwargs): stale = [ uri for uri in touched - if isinstance(uri, str) - and any(f"archive_{index:03d}" in uri for index in (1, 2, 3)) + if isinstance(uri, str) and any(f"archive_{index:03d}" in uri for index in (1, 2, 3)) ] assert stale == [], f"Archives older than the terminal must not be read: {stale}" async def test_get_session_context_pre_archive_abstracts_always_empty( - self, client: AsyncOpenViking, monkeypatch + self, client: partial, monkeypatch ): - session = client.session(session_id="assemble_trim_oldest_abstracts_test") + session = client(session_id="assemble_trim_oldest_abstracts_test") + await session.ensure_exists() summaries = [ "# Summary\n\n" + ("A" * 80), "# Summary\n\n" + ("B" * 80), @@ -583,17 +541,16 @@ async def fake_generate(self, _messages, latest_archive_overview="", **kwargs): assert context["latest_archive_overview"] == newest_summary assert context["pre_archive_abstracts"] == [] - assert context["estimatedTokens"] == ( - active_tokens + _estimate_tokens(newest_summary) - ) + assert context["estimatedTokens"] == (active_tokens + _estimate_tokens(newest_summary)) assert context["stats"]["totalArchives"] == 3 assert context["stats"]["includedArchives"] == 0 assert context["stats"]["droppedArchives"] == 3 async def test_get_session_context_newest_done_unreadable_overview_does_not_fallback( - self, client: AsyncOpenViking, monkeypatch + self, client: partial, monkeypatch ): - session = client.session(session_id="assemble_failed_archive_test") + session = client(session_id="assemble_failed_archive_test") + await session.ensure_exists() summaries = [ "# Session Summary\n\narchive one", "# Session Summary\n\narchive two", @@ -633,9 +590,10 @@ async def flaky_read_file(*args, **kwargs): assert context["stats"]["failedArchives"] == 1 async def test_get_session_context_newest_failed_skips_overview( - self, client: AsyncOpenViking, monkeypatch + self, client: partial, monkeypatch ): - session = client.session(session_id="assemble_newest_failed_test") + session = client(session_id="assemble_newest_failed_test") + await session.ensure_exists() async def fake_generate(self, _messages, latest_archive_overview="", **kwargs): del self, latest_archive_overview, kwargs @@ -675,9 +633,10 @@ async def fake_generate(self, _messages, latest_archive_overview="", **kwargs): assert context["stats"]["failedArchives"] == 1 async def test_get_session_context_budget_trim_drops_latest_archive_abstract( - self, client: AsyncOpenViking, monkeypatch + self, client: partial, monkeypatch ): - session = client.session(session_id="assemble_trim_id_test") + session = client(session_id="assemble_trim_id_test") + await session.ensure_exists() async def fake_generate(self, _messages, latest_archive_overview="", **kwargs): del self, latest_archive_overview, kwargs @@ -697,7 +656,7 @@ async def fake_generate(self, _messages, latest_archive_overview="", **kwargs): assert context["stats"]["droppedArchives"] == 1 async def test_get_session_context_returns_pending_messages_while_commit_running( - self, client: AsyncOpenViking + self, client: partial ): """Regression for #3129: when an archive has been written (Phase 1 done, commit_count advanced) but its ``.done`` marker has not been written yet @@ -710,7 +669,8 @@ async def test_get_session_context_returns_pending_messages_while_commit_running through ``commit_async``, so it is deterministic regardless of the queue-worker architecture.""" session_id = "deterministic_pending_archive_test" - session = client.session(session_id=session_id) + session = client(session_id=session_id) + await session.ensure_exists() # Add messages to the session (in-memory only at this point). session.add_message("user", [TextPart("Pending user message")]) @@ -749,7 +709,7 @@ async def test_get_session_context_returns_pending_messages_while_commit_running # running, so the archive must be treated as pending. # Load a fresh session from the filesystem state we just wrote. - fresh_session = client.session(session_id=session_id) + fresh_session = client(session_id=session_id) # The pending archive's messages must be visible. context = await fresh_session.get_session_context() @@ -763,9 +723,10 @@ class TestGetSessionArchive: """Test get_session_archive""" async def test_get_session_archive_returns_messages_and_summary( - self, client: AsyncOpenViking, monkeypatch + self, client: partial, monkeypatch ): - session = client.session(session_id="session_archive_expand_test") + session = client(session_id="session_archive_expand_test") + await session.ensure_exists() summaries = [ "# Session Summary\n\narchive one", "# Session Summary\n\narchive two", @@ -793,8 +754,9 @@ async def fake_generate(self, _messages, latest_archive_overview="", **kwargs): assert archive["overview"] == "# Session Summary\n\narchive one" assert [m["parts"][0]["text"] for m in archive["messages"]] == ["turn one", "reply one"] - async def test_get_session_archive_raises_for_missing_archive(self, client: AsyncOpenViking): - session = client.session(session_id="missing_session_archive_test") + async def test_get_session_archive_raises_for_missing_archive(self, client: partial): + session = client(session_id="missing_session_archive_test") + await session.ensure_exists() with pytest.raises(Exception, match="Session archive not found: archive_999"): await session.get_session_archive("archive_999") diff --git a/tests/session/test_session_lifecycle.py b/tests/session/test_session_lifecycle.py index 3eb343b324..2b1f908a43 100644 --- a/tests/session/test_session_lifecycle.py +++ b/tests/session/test_session_lifecycle.py @@ -5,32 +5,45 @@ import re -from openviking import AsyncOpenViking +from openviking.server.identity import RequestContext +from openviking.service.core import OpenVikingService from openviking.session import Session class TestSessionCreate: """Test Session creation""" - async def test_create_new_session(self, client: AsyncOpenViking): + async def test_create_new_session( + self, + service: OpenVikingService, + request_context: RequestContext, + ): """Test creating new session""" - session = client.session() + session = service.sessions.session(request_context) assert session is not None assert session.session_id is not None assert re.fullmatch(r"\d{8}-\d{6}-[0-9a-f]{16}", session.session_id) - async def test_create_with_id(self, client: AsyncOpenViking): + async def test_create_with_id( + self, + service: OpenVikingService, + request_context: RequestContext, + ): """Test creating session with specified ID""" session_id = "custom_session_id_123" - session = client.session(session_id=session_id) + session = service.sessions.session(request_context, session_id=session_id) assert session.session_id == session_id - async def test_create_multiple_sessions(self, client: AsyncOpenViking): + async def test_create_multiple_sessions( + self, + service: OpenVikingService, + request_context: RequestContext, + ): """Test creating multiple sessions""" - session1 = client.session(session_id="session_1") - session2 = client.session(session_id="session_2") + session1 = service.sessions.session(request_context, session_id="session_1") + session2 = service.sessions.session(request_context, session_id="session_2") assert session1.session_id != session2.session_id @@ -47,21 +60,31 @@ class TestSessionLoad: """Test Session loading""" async def test_load_existing_session( - self, session_with_messages: Session, client: AsyncOpenViking + self, + session_with_messages: Session, + service: OpenVikingService, + request_context: RequestContext, ): """Test loading existing session""" session_id = session_with_messages.session_id # Create new session instance and load - new_session = client.session(session_id=session_id) + new_session = service.sessions.session(request_context, session_id=session_id) await new_session.load() # Verify messages loaded assert len(new_session.messages) > 0 - async def test_load_nonexistent_session(self, client: AsyncOpenViking): + async def test_load_nonexistent_session( + self, + service: OpenVikingService, + request_context: RequestContext, + ): """Test loading nonexistent session""" - session = client.session(session_id="nonexistent_session_xyz") + session = service.sessions.session( + request_context, + session_id="nonexistent_session_xyz", + ) await session.load() # Nonexistent session should be empty after loading @@ -74,49 +97,72 @@ async def test_session_properties(self, session: Session): assert hasattr(session, "session_id") -class TestSessionMustExist: - """Test session(must_exist=True) raises when session does not exist.""" +class TestSessionServiceGet: + """Test persisted-session lookup semantics.""" - async def test_must_exist_raises_for_nonexistent(self, client: AsyncOpenViking): - """must_exist=True should raise NotFoundError for an unknown session_id.""" + async def test_get_raises_for_nonexistent( + self, + service: OpenVikingService, + request_context: RequestContext, + ): + """SessionService.get should reject an unknown session ID.""" import pytest from openviking_cli.exceptions import NotFoundError with pytest.raises(NotFoundError): - client.session(session_id="definitely_not_a_real_session", must_exist=True) + await service.sessions.get("definitely_not_a_real_session", request_context) - async def test_must_exist_succeeds_after_create(self, client: AsyncOpenViking): - """must_exist=True should succeed for a session created via create_session().""" - result = await client.create_session() - existing_id = result["session_id"] + async def test_get_succeeds_after_create( + self, + service: OpenVikingService, + request_context: RequestContext, + ): + """SessionService.get should return a persisted session.""" + created = await service.sessions.create(request_context) + existing_id = created.session_id - session = client.session(session_id=existing_id, must_exist=True) + session = await service.sessions.get(existing_id, request_context) assert session.session_id == existing_id - async def test_must_exist_false_default_accepts_unknown_id(self, client: AsyncOpenViking): - """Default must_exist=False should silently accept any session_id (backward compat).""" - session = client.session(session_id="fabricated_id_abc") + async def test_session_factory_accepts_unpersisted_id( + self, + service: OpenVikingService, + request_context: RequestContext, + ): + session = service.sessions.session(request_context, session_id="fabricated_id_abc") await session.load() assert session.session_id == "fabricated_id_abc" class TestSessionExists: - """Test session_exists() convenience method.""" + """Test persisted session existence.""" - async def test_session_exists_true_after_create(self, client: AsyncOpenViking): + async def test_session_exists_true_after_create( + self, + service: OpenVikingService, + request_context: RequestContext, + ): """session_exists() should return True for a created session.""" - result = await client.create_session() - session_id = result["session_id"] + session = await service.sessions.create(request_context) - assert await client.session_exists(session_id) is True + assert await session.exists() is True - async def test_session_exists_false_for_unknown(self, client: AsyncOpenViking): + async def test_session_exists_false_for_unknown( + self, + service: OpenVikingService, + request_context: RequestContext, + ): """session_exists() should return False for an unknown session_id.""" - assert await client.session_exists("definitely_not_a_real_session") is False + session = service.sessions.session( + request_context, + session_id="definitely_not_a_real_session", + ) + assert await session.exists() is False async def test_session_exists_true_after_add_message( - self, session_with_messages: Session, client: AsyncOpenViking + self, + session_with_messages: Session, ): """session_exists() should return True for a session that has messages.""" - assert await client.session_exists(session_with_messages.session_id) is True + assert await session_with_messages.exists() is True diff --git a/tests/session/test_session_messages.py b/tests/session/test_session_messages.py index e0f27507a7..4f228c9b6e 100644 --- a/tests/session/test_session_messages.py +++ b/tests/session/test_session_messages.py @@ -5,7 +5,6 @@ import pytest -from openviking import AsyncOpenViking from openviking.message import ContextPart, TextPart, ToolPart from openviking.session import Session from openviking_cli.exceptions import InvalidArgumentError @@ -95,28 +94,22 @@ async def test_messages_list_updated(self, session: Session): assert len(session.messages) == initial_count + 2 - async def test_batch_add_messages_preserves_peer_id_created_at_and_parts( - self, client: AsyncOpenViking - ): - session_id = "batch_message_preservation_test" - created = await client.create_session(session_id=session_id) - session_uri = created["uri"] - - result = await client.batch_add_messages( - session_id, + async def test_batch_add_messages_preserves_peer_id_created_at_and_parts(self, client): + session = client(session_id="batch_message_preservation_test") + await session.ensure_exists() + await session.add_messages_async( [ { "role": "user", "peer_id": "user-123", "created_at": "2026-05-01T12:00:00Z", "parts": [ - {"type": "text", "text": "Hello batch"}, - { - "type": "context", - "uri": "viking://resources/test-doc", - "context_type": "resource", - "abstract": "Test document", - }, + TextPart("Hello batch"), + ContextPart( + uri="viking://resources/test-doc", + context_type="resource", + abstract="Test document", + ), ], }, { @@ -124,64 +117,43 @@ async def test_batch_add_messages_preserves_peer_id_created_at_and_parts( "peer_id": "assistant-123", "created_at": "2026-05-01T12:00:05Z", "parts": [ - {"type": "text", "text": "Executing tool"}, - { - "type": "tool", - "tool_id": "tool_123", - "tool_name": "search_tool", - "tool_uri": f"{session_uri}/tools/tool_123", - "skill_uri": "viking://user/skills/search", - "tool_status": "completed", - "tool_output": "Found a result", - }, + TextPart("Executing tool"), + ToolPart( + tool_id="tool_123", + tool_name="search_tool", + tool_uri=f"{session.uri}/tools/tool_123", + skill_uri="viking://user/skills/search", + tool_status="completed", + tool_output="Found a result", + ), ], }, - ], + ] ) - assert result["added"] == 2 - - context = await client.get_session_context(session_id) + fresh = client(session_id=session.session_id) + await fresh.load() + context = await fresh.get_session_context() assert [message["role"] for message in context["messages"]] == ["user", "assistant"] assert context["messages"][0]["peer_id"] == "user-123" assert context["messages"][0]["created_at"] == "2026-05-01T12:00:00Z" - assert context["messages"][0]["parts"][1] == { - "type": "context", - "uri": "viking://resources/test-doc", - "context_type": "resource", - "abstract": "Test document", - } + assert context["messages"][0]["parts"][1]["uri"] == "viking://resources/test-doc" assert context["messages"][1]["peer_id"] == "assistant-123" - assert context["messages"][1]["created_at"] == "2026-05-01T12:00:05Z" - assert context["messages"][1]["parts"][1]["type"] == "tool" assert context["messages"][1]["parts"][1]["tool_status"] == "completed" assert context["messages"][1]["parts"][1]["tool_output"] == "Found a result" - async def test_batch_add_messages_is_atomic_when_later_message_is_invalid( - self, client: AsyncOpenViking - ): - session_id = "batch_message_atomicity_test" - await client.create_session(session_id=session_id) + async def test_batch_add_messages_is_atomic_when_later_message_is_invalid(self, client): + session = client(session_id="batch_message_atomicity_test") + await session.ensure_exists() - with pytest.raises(ValueError, match="Either content or parts must be provided"): - await client.batch_add_messages( - session_id, + with pytest.raises(ValueError, match="missing required key 'parts'"): + await session.add_messages_async( [ - {"role": "user", "content": "first valid message"}, + {"role": "user", "parts": [TextPart("first valid message")]}, {"role": "assistant"}, - ], + ] ) - context = await client.get_session_context(session_id) - assert context["messages"] == [] - - result = await client.batch_add_messages( - session_id, - [{"role": "user", "content": "first valid message"}], - ) - - assert result["added"] == 1 - context = await client.get_session_context(session_id) - assert [message["parts"][0]["text"] for message in context["messages"]] == [ - "first valid message" - ] + fresh = client(session_id=session.session_id) + await fresh.load() + assert fresh.messages == [] diff --git a/tests/session/test_session_retention_integration.py b/tests/session/test_session_retention_integration.py index 2660a8f60a..596ef0310e 100644 --- a/tests/session/test_session_retention_integration.py +++ b/tests/session/test_session_retention_integration.py @@ -8,7 +8,6 @@ import pytest -from openviking import AsyncOpenViking from openviking.message import Message, TextPart, ToolPart from openviking.models.vlm.base import ToolCall, VLMResponse from openviking.service.task_tracker import get_task_tracker @@ -80,9 +79,9 @@ def test_checkpoint_record_respects_remaining_retained_budget(): async def test_two_pending_archives_are_visible_independent_of_commit_count( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="two_pending_directory_state_test") + session = client(session_id="two_pending_directory_state_test") await session.ensure_exists() await _write_archive(session, 1, [_text_message("u1", "user", "one")]) await _write_archive(session, 2, [_text_message("u2", "user", "two")]) @@ -96,9 +95,9 @@ async def test_two_pending_archives_are_visible_independent_of_commit_count( async def test_session_context_enforces_hard_budget_without_mutating_archive_raw( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="session_context_hard_budget_test") + session = client(session_id="session_context_hard_budget_test") await session.ensure_exists() archived = _text_message("u1", "user", "A" * 4000) await _write_archive(session, 1, [archived]) @@ -116,14 +115,14 @@ async def test_session_context_enforces_hard_budget_without_mutating_archive_raw async def test_context_stops_at_newest_terminal_without_replaying_older_failed_raw( - client: AsyncOpenViking, + client, ): """The read path stops at archive_002 and never replays archive_001 raw. Phase 2 still treats the uncovered failed archive as replayable; only ``get_session_context`` stops at the newest terminal. """ - session = client.session(session_id="failed_and_wm_disabled_archive_test") + session = client(session_id="failed_and_wm_disabled_archive_test") await session.ensure_exists() await _write_archive( session, @@ -148,10 +147,10 @@ async def test_context_stops_at_newest_terminal_without_replaying_older_failed_r async def test_done_with_missing_required_overview_reports_failed_without_raw( - client: AsyncOpenViking, + client, ): """A required overview that is unreadable yields no overview and no raw.""" - session = client.session(session_id="missing_required_overview_test") + session = client(session_id="missing_required_overview_test") await session.ensure_exists() await _write_archive( session, @@ -172,9 +171,9 @@ async def test_done_with_missing_required_overview_reports_failed_without_raw( async def test_legacy_done_marker_covers_only_its_own_archive( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="legacy_done_self_coverage_test") + session = client(session_id="legacy_done_self_coverage_test") await session.ensure_exists() await _write_archive( session, @@ -198,9 +197,9 @@ async def test_legacy_done_marker_covers_only_its_own_archive( async def test_coverage_metadata_cannot_hide_a_pending_archive( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="pending_not_explicitly_covered_test") + session = client(session_id="pending_not_explicitly_covered_test") await session.ensure_exists() await _write_archive( session, @@ -233,9 +232,9 @@ async def test_coverage_metadata_cannot_hide_a_pending_archive( async def test_later_coverage_absorbs_failed_raw_and_stable_deduplicates_root( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="failed_coverage_roll_forward_test") + session = client(session_id="failed_coverage_roll_forward_test") await session.ensure_exists() duplicate = _text_message("u1", "user", "failed raw") await _write_archive(session, 1, [duplicate], failed=True) @@ -262,9 +261,9 @@ async def test_later_coverage_absorbs_failed_raw_and_stable_deduplicates_root( async def test_phase2_replays_failed_but_not_completed_without_overview( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="phase2_failed_replay_test") + session = client(session_id="phase2_failed_replay_test") await session.ensure_exists() await _write_archive( session, @@ -297,9 +296,9 @@ async def test_phase2_replays_failed_but_not_completed_without_overview( async def test_phase2_never_replays_an_earlier_pending_archive( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="phase2_pending_not_replayed_test") + session = client(session_id="phase2_pending_not_replayed_test") await session.ensure_exists() await _write_archive(session, 1, [_text_message("u1", "user", "pending one")]) await _write_archive( @@ -326,10 +325,10 @@ async def test_phase2_never_replays_an_earlier_pending_archive( async def test_phase2_waits_for_all_earlier_pending_archives( - client: AsyncOpenViking, + client, monkeypatch, ): - session = client.session(session_id="phase2_wait_all_pending_test") + session = client(session_id="phase2_wait_all_pending_test") await session.ensure_exists() first_uri = await _write_archive( session, @@ -358,9 +357,9 @@ async def test_phase2_waits_for_all_earlier_pending_archives( async def test_missing_previous_archive_directory_does_not_block_phase2( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="missing_previous_archive_test") + session = client(session_id="missing_previous_archive_test") await session.ensure_exists() await _write_archive( session, @@ -375,10 +374,10 @@ async def test_missing_previous_archive_directory_does_not_block_phase2( async def test_phase2_roll_forward_writes_coverage_and_calls_existing_summary_once( - client: AsyncOpenViking, + client, monkeypatch, ): - session = client.session(session_id="phase2_roll_forward_end_to_end_test") + session = client(session_id="phase2_roll_forward_end_to_end_test") await session.ensure_exists() await _write_archive( session, @@ -442,10 +441,10 @@ async def fake_summary(messages, latest_archive_overview=""): async def test_phase2_rolls_forward_done_archive_with_missing_required_overview( - client: AsyncOpenViking, + client, monkeypatch, ): - session = client.session(session_id="invalid_done_roll_forward_test") + session = client(session_id="invalid_done_roll_forward_test") await session.ensure_exists() await _write_archive( session, @@ -503,10 +502,10 @@ async def fake_summary(messages, latest_archive_overview=""): async def test_roll_forward_does_not_repeat_completed_memory_step_messages( - client: AsyncOpenViking, + client, monkeypatch, ): - session = client.session(session_id="phase2_memory_step_idempotency_test") + session = client(session_id="phase2_memory_step_idempotency_test") await session.ensure_exists() first = _text_message("u1", "user", "already extracted") await _write_archive( @@ -574,9 +573,9 @@ async def fake_long_term(*, messages, **_kwargs): async def test_completed_partial_turn_inserts_checkpoint_after_user_anchor( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="partial_turn_checkpoint_test") + session = client(session_id="partial_turn_checkpoint_test") await session.ensure_exists() anchor = _text_message("u1", "user", "investigate the outage") early = _text_message("a1", "assistant", "checking the first signal") @@ -648,9 +647,9 @@ async def test_completed_partial_turn_inserts_checkpoint_after_user_anchor( async def test_legacy_partial_turn_does_not_derive_checkpoint_from_overview( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="legacy_partial_turn_without_checkpoint_test") + session = client(session_id="legacy_partial_turn_without_checkpoint_test") await session.ensure_exists() anchor = _text_message("u1", "user", "investigate the outage") early = _text_message("a1", "assistant", "checking the first signal") @@ -680,10 +679,10 @@ async def test_legacy_partial_turn_does_not_derive_checkpoint_from_overview( async def test_phase2_persists_checkpoint_from_same_summary_call( - client: AsyncOpenViking, + client, monkeypatch, ): - session = client.session(session_id="phase2_checkpoint_product_test") + session = client(session_id="phase2_checkpoint_product_test") await session.ensure_exists() anchor = _text_message("u1", "user", "investigate the outage") early = _text_message("a1", "assistant", "checking the first signal") @@ -773,10 +772,10 @@ async def fake_summary( async def test_wm_creation_returns_two_products_in_one_model_call( - client: AsyncOpenViking, + client, monkeypatch, ): - session = client.session(session_id="single_call_checkpoint_generation_test") + session = client(session_id="single_call_checkpoint_generation_test") calls: list[dict] = [] class FakeVLM: @@ -834,10 +833,10 @@ async def get_completion_async(self, **kwargs): async def test_wm_update_returns_two_products_in_one_model_call( - client: AsyncOpenViking, + client, monkeypatch, ): - session = client.session(session_id="single_call_checkpoint_update_test") + session = client(session_id="single_call_checkpoint_update_test") calls: list[dict] = [] class FakeVLM: @@ -930,9 +929,9 @@ async def get_completion_async(self, **kwargs): async def test_roll_forward_collects_multiple_checkpoint_requests( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="multiple_roll_forward_checkpoint_test") + session = client(session_id="multiple_roll_forward_checkpoint_test") await session.ensure_exists() first_anchor = _text_message("u1", "user", "first investigation") first_source = _text_message("a1", "assistant", "first archived step") @@ -986,9 +985,9 @@ async def test_roll_forward_collects_multiple_checkpoint_requests( async def test_phase2_rolls_previous_cumulative_checkpoint_into_v2_record( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="cumulative_checkpoint_roll_forward_test") + session = client(session_id="cumulative_checkpoint_roll_forward_test") await session.ensure_exists() anchor = _text_message("u1", "user", "investigate the outage") await _write_archive( @@ -1051,9 +1050,9 @@ async def test_phase2_rolls_previous_cumulative_checkpoint_into_v2_record( async def test_phase2_migrates_legacy_checkpoint_deltas_to_cumulative_v2( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="legacy_checkpoint_migration_test") + session = client(session_id="legacy_checkpoint_migration_test") await session.ensure_exists() anchor = _text_message("u1", "user", "investigate the outage") for index, source_id, abstract in ( @@ -1115,9 +1114,9 @@ async def test_phase2_migrates_legacy_checkpoint_deltas_to_cumulative_v2( async def test_checkpoint_request_rejects_user_or_cross_turn_sources( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="invalid_checkpoint_source_test") + session = client(session_id="invalid_checkpoint_source_test") await session.ensure_exists() anchor = _text_message("u1", "user", "first query") assistant = _text_message("a1", "assistant", "first response") @@ -1145,10 +1144,10 @@ async def test_checkpoint_request_rejects_user_or_cross_turn_sources( async def test_missing_required_checkpoint_keeps_archive_raw_uncovered( - client: AsyncOpenViking, + client, monkeypatch, ): - session = client.session(session_id="missing_required_checkpoint_test") + session = client(session_id="missing_required_checkpoint_test") await session.ensure_exists() anchor = _text_message("u1", "user", "investigate the outage") early = _text_message("a1", "assistant", "checking the first signal") @@ -1216,10 +1215,10 @@ async def fake_summary(*_args, **_kwargs): async def test_working_memory_disabled_does_not_generate_or_restore_checkpoint( - client: AsyncOpenViking, + client, monkeypatch, ): - session = client.session(session_id="wm_disabled_partial_checkpoint_test") + session = client(session_id="wm_disabled_partial_checkpoint_test") await session.ensure_exists() anchor = _text_message("u1", "user", "investigate the outage") early = _text_message("a1", "assistant", "checking the first signal") @@ -1282,9 +1281,9 @@ async def unexpected_summary(*_args, **_kwargs): async def test_covered_failed_partial_turn_checkpoint_points_to_covering_overview( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="covered_failed_partial_checkpoint_test") + session = client(session_id="covered_failed_partial_checkpoint_test") await session.ensure_exists() anchor = _text_message("u1", "user", "investigate the outage") early = _text_message("a1", "assistant", "checking an early signal") @@ -1340,9 +1339,9 @@ async def test_covered_failed_partial_turn_checkpoint_points_to_covering_overvie async def test_failed_terminal_checkpoint_metadata_is_never_restored( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="failed_terminal_checkpoint_test") + session = client(session_id="failed_terminal_checkpoint_test") await session.ensure_exists() anchor = _text_message("u1", "user", "investigate the outage") tail = _text_message("a3", "assistant", "latest raw step") @@ -1389,10 +1388,10 @@ async def test_failed_terminal_checkpoint_metadata_is_never_restored( async def test_repeated_legacy_partial_commits_merge_checkpoint_deltas( - client: AsyncOpenViking, + client, ): """Legacy records have no version marker, so older deltas remain readable.""" - session = client.session(session_id="repeated_partial_checkpoint_merge_test") + session = client(session_id="repeated_partial_checkpoint_merge_test") await session.ensure_exists() anchor = _text_message("u1", "user", "investigate the outage") tail = _text_message("a3", "assistant", "latest raw step") @@ -1449,11 +1448,11 @@ async def test_repeated_legacy_partial_commits_merge_checkpoint_deltas( async def test_repeated_v2_partial_commits_restore_newest_cumulative_checkpoint_only( - client: AsyncOpenViking, + client, monkeypatch, ): """The newest v2 record is complete, so context does not read older archives.""" - session = client.session(session_id="repeated_cumulative_checkpoint_test") + session = client(session_id="repeated_cumulative_checkpoint_test") await session.ensure_exists() anchor = _text_message("u1", "user", "investigate the outage") tail = _text_message("a3", "assistant", "latest raw step") @@ -1517,9 +1516,10 @@ async def tracking_read_file(*args, **kwargs): async def test_commit_externalizes_tool_outputs_across_the_whole_turn( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="turn_wide_externalization_test") + session = client(session_id="turn_wide_externalization_test") + await session.ensure_exists() session.add_message("user", [TextPart("inspect all files")]) for index in range(10): session.add_message( @@ -1551,9 +1551,10 @@ async def test_commit_externalizes_tool_outputs_across_the_whole_turn( async def test_turn_budget_commit_archives_complete_old_turn_and_keeps_latest_user( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="turn_budget_phase1_boundary_test") + session = client(session_id="turn_budget_phase1_boundary_test") + await session.ensure_exists() session.add_message("user", [TextPart("first query")]) session.add_message("assistant", [TextPart("first answer")]) session.add_message("user", [TextPart("latest query")]) @@ -1578,11 +1579,12 @@ async def test_turn_budget_commit_archives_complete_old_turn_and_keeps_latest_us async def test_concurrent_stale_session_instances_use_one_authoritative_phase1_snapshot( - client: AsyncOpenViking, + client, ): - session_a = client.session(session_id="multi_worker_phase1_snapshot_test") + session_a = client(session_id="multi_worker_phase1_snapshot_test") + await session_a.ensure_exists() session_a.add_message("user", [TextPart("only once")]) - session_b = client.session(session_id=session_a.session_id) + session_b = client(session_id=session_a.session_id) await session_b.load() disabled_policy = { "working_memory": {"enabled": False}, @@ -1601,11 +1603,11 @@ async def test_concurrent_stale_session_instances_use_one_authoritative_phase1_s async def test_concurrent_stale_workers_append_without_losing_messages( - client: AsyncOpenViking, + client, ): - session_a = client.session(session_id="multi_worker_append_lock_test") + session_a = client(session_id="multi_worker_append_lock_test") await session_a.ensure_exists() - session_b = client.session(session_id=session_a.session_id) + session_b = client(session_id=session_a.session_id) await session_b.load() await asyncio.gather( @@ -1613,7 +1615,7 @@ async def test_concurrent_stale_workers_append_without_losing_messages( asyncio.to_thread(session_b.add_message, "user", [TextPart("from worker b")]), ) - fresh = client.session(session_id=session_a.session_id) + fresh = client(session_id=session_a.session_id) await fresh.load() assert sorted(message.content for message in fresh.messages) == [ "from worker a", @@ -1622,14 +1624,15 @@ async def test_concurrent_stale_workers_append_without_losing_messages( async def test_phase2_meta_merge_serializes_with_concurrent_append( - client: AsyncOpenViking, + client, monkeypatch, ): - initial = client.session(session_id="phase2_meta_append_lock_test") + initial = client(session_id="phase2_meta_append_lock_test") + await initial.ensure_exists() await initial.add_message_async("user", [TextPart("first")]) - phase2 = client.session(session_id=initial.session_id) - appending = client.session(session_id=initial.session_id) + phase2 = client(session_id=initial.session_id) + appending = client(session_id=initial.session_id) await phase2.load() await appending.load() @@ -1637,10 +1640,10 @@ async def test_phase2_meta_merge_serializes_with_concurrent_append( allow_phase2_save = asyncio.Event() original_save_meta = phase2._save_meta - async def delayed_save_meta(): + async def delayed_save_meta(*, lease_ref=None): phase2_inside_save.set() await allow_phase2_save.wait() - await original_save_meta() + await original_save_meta(lease_ref=lease_ref) monkeypatch.setattr(phase2, "_save_meta", delayed_save_meta) merge_task = asyncio.create_task( @@ -1661,7 +1664,7 @@ async def delayed_save_meta(): allow_phase2_save.set() await asyncio.gather(merge_task, append_task) - fresh = client.session(session_id=initial.session_id) + fresh = client(session_id=initial.session_id) await fresh.load() assert [message.content for message in fresh.messages] == ["first", "second"] assert fresh.meta.message_count == 2 @@ -1670,21 +1673,22 @@ async def delayed_save_meta(): async def test_add_waits_for_commit_root_rewrite_and_remains_live( - client: AsyncOpenViking, + client, monkeypatch, ): - committing = client.session(session_id="add_during_commit_lock_test") + committing = client(session_id="add_during_commit_lock_test") + await committing.ensure_exists() committing.add_message("user", [TextPart("archive me")]) - adding = client.session(session_id=committing.session_id) + adding = client(session_id=committing.session_id) await adding.load() commit_inside_rewrite = asyncio.Event() allow_commit_rewrite = asyncio.Event() original_write = committing._write_to_agfs_async - async def delayed_root_write(messages): + async def delayed_root_write(messages, *, lease_ref=None): commit_inside_rewrite.set() await allow_commit_rewrite.wait() - await original_write(messages) + await original_write(messages, lease_ref=lease_ref) class CapturingQueueManager: async def enqueue(self, *_args, **_kwargs): @@ -1711,7 +1715,7 @@ async def enqueue(self, *_args, **_kwargs): allow_commit_rewrite.set() await asyncio.gather(commit_task, add_task) - fresh = client.session(session_id=committing.session_id) + fresh = client(session_id=committing.session_id) await fresh.load() context = await fresh.get_session_context() assert [message.content for message in fresh.messages] == ["keep me live"] @@ -1722,7 +1726,7 @@ async def enqueue(self, *_args, **_kwargs): async def test_queue_enqueue_failure_marks_archive_failed_and_keeps_raw_durable( - client: AsyncOpenViking, + client, monkeypatch, ): """The failed marker is authoritative and the raw file stays durable. @@ -1730,7 +1734,8 @@ async def test_queue_enqueue_failure_marks_archive_failed_and_keeps_raw_durable( Terminal-stop means ``get_session_context`` no longer replays that raw as logical live; recovery goes through Phase 2 roll-forward instead. """ - session = client.session(session_id="queue_enqueue_failure_recovery_test") + session = client(session_id="queue_enqueue_failure_recovery_test") + await session.ensure_exists() session.add_message("user", [TextPart("do not lose me")]) class FailingQueueManager: @@ -1764,16 +1769,17 @@ async def enqueue(self, *_args, **_kwargs): async def test_phase1_root_rewrite_failure_marks_orphan_archive_failed( - client: AsyncOpenViking, + client, monkeypatch, ): - session = client.session(session_id="phase1_root_failure_recovery_test") + session = client(session_id="phase1_root_failure_recovery_test") + await session.ensure_exists() session.add_message("user", [TextPart("archive candidate")]) session.add_message("assistant", [TextPart("retained tail")]) original_write = session._write_to_agfs_async - async def write_then_fail(messages): - await original_write(messages) + async def write_then_fail(messages, *, lease_ref=None): + await original_write(messages, lease_ref=lease_ref) raise RuntimeError("synthetic root rewrite failure") monkeypatch.setattr(session, "_write_to_agfs_async", write_then_fail) @@ -1788,7 +1794,7 @@ async def write_then_fail(messages): ctx=session.ctx, ) ) - fresh = client.session(session_id=session.session_id) + fresh = client(session_id=session.session_id) await fresh.load() context = await fresh.get_session_context() assert states[0].state == "failed" @@ -1802,10 +1808,11 @@ async def write_then_fail(messages): async def test_phase1_enqueues_before_root_rewrite_and_publishes_ready_last( - client: AsyncOpenViking, + client, monkeypatch, ): - session = client.session(session_id="phase1_publish_order_test") + session = client(session_id="phase1_publish_order_test") + await session.ensure_exists() session.add_message("user", [TextPart("archive me")]) observations: list[tuple[list[str], str]] = [] @@ -1833,9 +1840,10 @@ async def enqueue(self, _queue_name, data): async def test_interrupted_phase1_recovers_when_root_rewrite_is_durable( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="phase1_reconcile_durable_root_test") + session = client(session_id="phase1_reconcile_durable_root_test") + await session.ensure_exists() original = [ _text_message("u1", "user", "archive me"), _text_message("a1", "assistant", "retain me"), @@ -1863,9 +1871,10 @@ async def test_interrupted_phase1_recovers_when_root_rewrite_is_durable( async def test_interrupted_phase1_before_root_rewrite_becomes_failed( - client: AsyncOpenViking, + client, ): - session = client.session(session_id="phase1_reconcile_original_root_test") + session = client(session_id="phase1_reconcile_original_root_test") + await session.ensure_exists() original = [_text_message("u1", "user", "still live")] session._messages = original await session._write_to_agfs_async(messages=original) @@ -1891,12 +1900,14 @@ async def test_interrupted_phase1_before_root_rewrite_becomes_failed( async def test_stale_worker_uses_lock_snapshot_memory_policy_for_queue_message( - client: AsyncOpenViking, + client, monkeypatch, ): - stale_session = client.session(session_id="stale_memory_policy_snapshot_test") + stale_session = client(session_id="stale_memory_policy_snapshot_test") + await stale_session.ensure_exists() + stale_session._agent_evolution_enabled_provider = lambda: False stale_session.add_message("user", [TextPart("archive with persisted policy")]) - updater = client.session(session_id=stale_session.session_id) + updater = client(session_id=stale_session.session_id) await updater.load() updater.meta.memory_policy = { "working_memory": {"enabled": False}, diff --git a/tests/session/test_tool_result_externalization.py b/tests/session/test_tool_result_externalization.py index 195b292594..9639c86e8e 100644 --- a/tests/session/test_tool_result_externalization.py +++ b/tests/session/test_tool_result_externalization.py @@ -18,7 +18,7 @@ class MemoryVikingFS: def __init__(self): self.files = {} - async def write_file(self, uri, content, *, ctx=None): # noqa: ANN001 + async def write_file(self, uri, content, *, ctx=None, lease_ref=None): # noqa: ANN001 self.files[uri] = content async def append_file(self, uri, content, *, ctx=None): # noqa: ANN001 diff --git a/tests/test_session_async_commit.py b/tests/test_session_async_commit.py index e747537654..75e935e126 100644 --- a/tests/test_session_async_commit.py +++ b/tests/test_session_async_commit.py @@ -9,8 +9,6 @@ import httpx import pytest_asyncio -from openviking import AsyncOpenViking -from openviking.message import TextPart from openviking.server.app import create_app from openviking.server.config import ServerConfig from openviking.server.dependencies import set_service @@ -32,19 +30,6 @@ async def api_client(temp_dir) -> AsyncGenerator[Tuple[httpx.AsyncClient, OpenVi yield client, service await service.close() - await AsyncOpenViking.reset() - set_task_tracker(None) - - -@pytest_asyncio.fixture -async def ov_client(temp_dir) -> AsyncGenerator[AsyncOpenViking, None]: - """Create AsyncOpenViking client for unit tests.""" - set_task_tracker(None) - client = AsyncOpenViking(path=str(temp_dir / "ov_data")) - await client.initialize() - yield client - await client.close() - await AsyncOpenViking.reset() set_task_tracker(None) @@ -61,19 +46,6 @@ async def _new_session_with_one_message(client: httpx.AsyncClient) -> str: return session_id -async def test_commit_async_returns_accepted_with_task_id(ov_client: AsyncOpenViking): - """commit_async should return status=accepted with a task_id.""" - session = ov_client.session(session_id="async-shape-test") - session.add_message("user", [TextPart("first")]) - result = await session.commit_async() - - assert result["status"] == "accepted" - assert result["task_id"] is not None - assert result["archived"] is True - assert "session_id" in result - assert "archive_uri" in result - - async def test_commit_endpoint_returns_accepted_with_task_id(api_client): """Commit endpoint should return status=accepted with a task_id.""" client, service = api_client diff --git a/tests/test_session_task_tracking.py b/tests/test_session_task_tracking.py index 101c270dfc..37ade5221e 100644 --- a/tests/test_session_task_tracking.py +++ b/tests/test_session_task_tracking.py @@ -9,7 +9,6 @@ import httpx import pytest_asyncio -from openviking import AsyncOpenViking from openviking.core.namespace import canonical_session_uri from openviking.server.app import create_app from openviking.server.config import ServerConfig @@ -32,7 +31,6 @@ async def api_client(temp_dir) -> AsyncGenerator[Tuple[httpx.AsyncClient, OpenVi yield client, service await service.close() - await AsyncOpenViking.reset() set_task_tracker(None) diff --git a/tests/unit/test_add_resource_client_signatures.py b/tests/unit/test_add_resource_client_signatures.py deleted file mode 100644 index 6835ce5202..0000000000 --- a/tests/unit/test_add_resource_client_signatures.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. -# SPDX-License-Identifier: AGPL-3.0 - -import inspect - -from openviking.async_client import AsyncOpenViking -from openviking.client.local import LocalClient -from openviking.sync_client import SyncOpenViking -from openviking_cli.client.base import BaseClient - - -def _assert_add_resource_keeps_telemetry_before_tags(func): - params = list(inspect.signature(func).parameters) - assert params.index("telemetry") < params.index("tags") - assert params.index("telemetry") < params.index("tag_mode") - - -def test_top_level_add_resource_signatures_keep_telemetry_position(): - _assert_add_resource_keeps_telemetry_before_tags(AsyncOpenViking.add_resource) - _assert_add_resource_keeps_telemetry_before_tags(SyncOpenViking.add_resource) - _assert_add_resource_keeps_telemetry_before_tags(LocalClient.add_resource) - _assert_add_resource_keeps_telemetry_before_tags(BaseClient.add_resource) diff --git a/tests/unit/test_langchain_async_integration.py b/tests/unit/test_langchain_async_integration.py index b62a35ddd3..f719889624 100644 --- a/tests/unit/test_langchain_async_integration.py +++ b/tests/unit/test_langchain_async_integration.py @@ -4,7 +4,6 @@ import copy import sys import threading -from types import SimpleNamespace from typing import Any import pytest @@ -841,74 +840,6 @@ def find(self, query: str) -> dict[str, Any]: assert call_thread_ids[0] != main_thread_id -@pytest.mark.asyncio -async def test_embedded_path_async_uses_owned_sync_client_in_worker(monkeypatch, tmp_path): - main_thread_id = threading.get_ident() - instances: list[Any] = [] - - class FakeSyncOpenViking: - def __init__(self, *, path: str, actor_peer_id: str | None = None): - self.path = path - self.actor_peer_id = actor_peer_id - self._initialized = False - self.initialize_thread_id: int | None = None - self.find_thread_id: int | None = None - self.closed = False - instances.append(self) - - def initialize(self) -> None: - self.initialize_thread_id = threading.get_ident() - self._initialized = True - - def find(self, **_kwargs: Any) -> dict[str, Any]: - self.find_thread_id = threading.get_ident() - return { - "memories": [ - { - "uri": "viking://user/memories/example", - "abstract": "Embedded result.", - "level": 1, - } - ], - "resources": [], - "skills": [], - } - - def close(self) -> None: - self.closed = True - - import langchain_openviking.client as client_module - - monkeypatch.setattr( - client_module, - "import_module", - lambda name: SimpleNamespace(SyncOpenViking=FakeSyncOpenViking), - ) - retriever = OpenVikingRetriever(path=str(tmp_path), actor_peer_id="assistant-a") - - documents = await retriever.ainvoke("embedded") - client = await retriever.get_async_client() - - assert [document.page_content for document in documents] == ["Embedded result."] - assert len(instances) == 1 - assert client is instances[0] - assert client is retriever._get_client() - assert client.actor_peer_id == "assistant-a" - assert client.initialize_thread_id != main_thread_id - assert client.find_thread_id != main_thread_id - - await retriever.aclose() - assert client.closed is True - - recorder = OpenVikingSessionRecorder(path=str(tmp_path / "recorder")) - recorder_client = await recorder.get_async_client() - - assert recorder_client is recorder.client - recorder.close() - assert recorder._closed is True - assert recorder_client.closed is True - - @pytest.mark.asyncio async def test_async_retriever_uses_native_client_for_search_and_read(): backing = InMemoryOpenVikingClient( diff --git a/tests/unit/test_langchain_integration.py b/tests/unit/test_langchain_integration.py index 46389d1e7c..0e20fdcb4c 100644 --- a/tests/unit/test_langchain_integration.py +++ b/tests/unit/test_langchain_integration.py @@ -1,7 +1,6 @@ from __future__ import annotations import json -from types import SimpleNamespace from typing import Any import pytest @@ -456,32 +455,6 @@ def initialize(self): assert created["url"] is None -def test_ensure_client_keeps_local_path_clients_direct(monkeypatch, tmp_path): - created = {} - - class FakeLocalClient: - def __init__(self, path, actor_peer_id=None): - created["path"] = path - created["actor_peer_id"] = actor_peer_id - self._initialized = False - - def initialize(self): - self._initialized = True - - monkeypatch.setattr( - client_helpers, - "import_module", - lambda name: SimpleNamespace(SyncOpenViking=FakeLocalClient), - ) - - client = ensure_client(OpenVikingConnection(path=str(tmp_path))) - - assert isinstance(client, FakeLocalClient) - assert client._initialized is True - assert created["path"] == str(tmp_path) - assert created["actor_peer_id"] is None - - def test_openviking_client_retries_recoverable_read_with_fresh_client(monkeypatch): instances = [] diff --git a/tests/unit/test_langchain_package_boundary.py b/tests/unit/test_langchain_package_boundary.py index 646872dc5d..4701329d99 100644 --- a/tests/unit/test_langchain_package_boundary.py +++ b/tests/unit/test_langchain_package_boundary.py @@ -46,8 +46,7 @@ def test_standalone_package_has_no_server_imports_at_module_scope(): module = node.args[0].value is_full_package = module == "openviking" or module.startswith("openviking.") is_cli_package = module == "openviking_cli" or module.startswith("openviking_cli.") - is_embedded_client_import = source_file.name == "client.py" and module == "openviking" - if (is_full_package or is_cli_package) and not is_embedded_client_import: + if is_full_package or is_cli_package: violations.append(f"{source_file.name}:{node.lineno}: import_module({module!r})") assert violations == [] diff --git a/tests/unit/test_langchain_runtime_actor_peer.py b/tests/unit/test_langchain_runtime_actor_peer.py index 96a54fce2e..dcab5d33fe 100644 --- a/tests/unit/test_langchain_runtime_actor_peer.py +++ b/tests/unit/test_langchain_runtime_actor_peer.py @@ -60,17 +60,6 @@ def test_langchain_star_import_remains_usable_with_older_sdk( assert not namespace["has_request_actor_peer_support"]() -def test_middleware_rejects_actor_peer_resolver_for_embedded_client(tmp_path): - with pytest.raises( - ValueError, - match="actor_peer_resolver requires an OpenViking HTTP connection", - ): - OpenVikingContextMiddleware( - path=str(tmp_path), - actor_peer_resolver=lambda _state, _runtime: "runtime-agent", - ) - - def test_middleware_remains_usable_with_older_sdk_when_actor_peer_is_unused( monkeypatch: pytest.MonkeyPatch, ): @@ -106,21 +95,6 @@ def test_middleware_rejects_custom_client_without_actor_peer_support(): ) -def test_middleware_rejects_embedded_client_handle_with_actor_peer_resolver( - tmp_path, -): - client = OpenVikingClientHandle(OpenVikingConnection(path=str(tmp_path))) - - with pytest.raises( - ValueError, - match="clients that support request-scoped actor peers", - ): - OpenVikingContextMiddleware( - client=client, - actor_peer_resolver=lambda _state, _runtime: "runtime-agent", - ) - - def test_middleware_accepts_official_request_actor_peer_client(): client = _openviking_sdk.AsyncHTTPClient(url="http://openviking.test") diff --git a/tests/unit/test_local_client_git.py b/tests/unit/test_local_client_git.py deleted file mode 100644 index d5817552fb..0000000000 --- a/tests/unit/test_local_client_git.py +++ /dev/null @@ -1,160 +0,0 @@ -"""Tests for LocalClient git version control methods. - -Verifies that LocalClient git methods forward the right kwargs to FSService. -""" -from unittest.mock import AsyncMock, MagicMock - -import pytest - -from openviking.client.local import LocalClient -from openviking.server.identity import RequestContext, Role -from openviking_cli.session.user_id import UserIdentifier - - -@pytest.fixture -def mock_fs(): - m = MagicMock() - m.commit = AsyncMock(return_value={"result": "created", "commit_oid": "a" * 40}) - m.restore = AsyncMock(return_value={"result": "applied", "commit_oid": "b" * 40}) - m.show = AsyncMock(return_value={"oid": "c" * 40, "message": "m", "parents": []}) - m.log = AsyncMock(return_value=[{"oid": "c" * 40, "message": "m"}]) - m.diff = AsyncMock(return_value={"change_type": "modified"}) - return m - - -@pytest.fixture -def local_client(mock_fs): - """Build a LocalClient with a mocked FSService, bypassing __init__.""" - ctx = RequestContext( - user=UserIdentifier(account_id="acc", user_id="u"), - role=Role.ROOT, - ) - client = object.__new__(LocalClient) - client._service = MagicMock() - client._service.fs = mock_fs - client._ctx = ctx - return client - - -@pytest.mark.asyncio -async def test_commit_forwards_kwargs(local_client, mock_fs): - out = await local_client.git_commit( - message="snapshot", - paths=["viking://resources/a.md"], - branch="main", - author_name="me", - author_email="me@x", - ) - mock_fs.commit.assert_awaited_once_with( - message="snapshot", - paths=["viking://resources/a.md"], - branch="main", - author_name="me", - author_email="me@x", - ctx=local_client._ctx, - ) - assert out["commit_oid"] == "a" * 40 - - -@pytest.mark.asyncio -async def test_commit_defaults(local_client, mock_fs): - await local_client.git_commit(message="m") - kwargs = mock_fs.commit.await_args.kwargs - assert kwargs["paths"] is None - assert kwargs["branch"] == "main" - assert kwargs["author_name"] is None - assert kwargs["author_email"] is None - - -@pytest.mark.asyncio -async def test_restore_forwards_kwargs(local_client, mock_fs): - out = await local_client.git_restore( - project_dir="viking://resources/proj", - source_commit="d" * 40, - branch="main", - dry_run=True, - message="rollback", - author_name="me", - author_email="me@x", - ) - mock_fs.restore.assert_awaited_once_with( - project_dir="viking://resources/proj", - source_commit="d" * 40, - branch="main", - dry_run=True, - message="rollback", - author_name="me", - author_email="me@x", - ctx=local_client._ctx, - ) - assert out["result"] == "applied" - - -@pytest.mark.asyncio -async def test_restore_defaults_project_dir_none(local_client, mock_fs): - await local_client.git_restore(source_commit="d" * 40) - mock_fs.restore.assert_awaited_once_with( - project_dir=None, - source_commit="d" * 40, - branch="main", - dry_run=False, - message=None, - author_name=None, - author_email=None, - ctx=local_client._ctx, - ) - - -@pytest.mark.asyncio -async def test_show_metadata(local_client, mock_fs): - out = await local_client.git_show("main") - mock_fs.show.assert_awaited_once_with("main", path=None, ctx=local_client._ctx) - assert out["oid"] == "c" * 40 - - -@pytest.mark.asyncio -async def test_show_with_path(local_client, mock_fs): - mock_fs.show = AsyncMock(return_value=b"blob data") - out = await local_client.git_show("main", path="viking://resources/a.md") - mock_fs.show.assert_awaited_once_with("main", path="viking://resources/a.md", ctx=local_client._ctx) - assert out == b"blob data" - - -@pytest.mark.asyncio -async def test_log_defaults(local_client, mock_fs): - out = await local_client.git_log() - mock_fs.log.assert_awaited_once_with( - branch="main", limit=20, paths=None, ctx=local_client._ctx - ) - assert len(out) == 1 - - -@pytest.mark.asyncio -async def test_log_overrides(local_client, mock_fs): - await local_client.git_log( - branch="dev", - limit=5, - paths=["viking://resources/a.md", "viking://resources/docs"], - ) - mock_fs.log.assert_awaited_once_with( - branch="dev", - limit=5, - paths=["viking://resources/a.md", "viking://resources/docs"], - ctx=local_client._ctx, - ) - - -@pytest.mark.asyncio -async def test_diff_forwards_kwargs(local_client, mock_fs): - out = await local_client.git_diff( - "viking://resources/a.md", - from_ref="old", - to_ref="new", - ) - mock_fs.diff.assert_awaited_once_with( - path="viking://resources/a.md", - from_ref="old", - to_ref="new", - ctx=local_client._ctx, - ) - assert out["change_type"] == "modified" diff --git a/tests/unit/test_search_tags_filter.py b/tests/unit/test_search_tags_filter.py index fbddfe2974..2a6f4be5e0 100644 --- a/tests/unit/test_search_tags_filter.py +++ b/tests/unit/test_search_tags_filter.py @@ -1,7 +1,6 @@ # Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. # SPDX-License-Identifier: AGPL-3.0 -from openviking.client.local import _resolve_search_filter as _resolve_local_search_filter from openviking.server.routers.search import _resolve_search_filter from openviking.utils.tags import build_search_tags_filter @@ -63,22 +62,3 @@ def test_find_tags_filter_ands_all_tags_with_existing_filter(): {"op": "must", "field": "search_tags", "conds": ["team=search"]}, ], } - - -def test_local_client_tags_filter_requires_all_tags(): - result = _resolve_local_search_filter( - filter=None, - context_type=None, - since=None, - until=None, - time_field=None, - tags=["env=prod", "team=search"], - ) - - assert result == { - "op": "and", - "conds": [ - {"op": "must", "field": "search_tags", "conds": ["env=prod"]}, - {"op": "must", "field": "search_tags", "conds": ["team=search"]}, - ], - } diff --git a/tests/unit/test_snapshot_namespace.py b/tests/unit/test_snapshot_namespace.py deleted file mode 100644 index 72c265b1ad..0000000000 --- a/tests/unit/test_snapshot_namespace.py +++ /dev/null @@ -1,210 +0,0 @@ -"""Unit tests for AsyncSnapshotNamespace and SyncSnapshotNamespace. - -These tests verify the namespace classes forward to the underlying -client's git_* methods correctly. They don't exercise real git. -""" -from unittest.mock import AsyncMock, MagicMock - -import pytest - -from openviking.snapshot_namespace import AsyncSnapshotNamespace, SyncSnapshotNamespace - - -@pytest.fixture -def fake_async_client(): - """A fake AsyncOpenViking with a mocked _client (BaseClient).""" - parent = MagicMock() - parent._ensure_initialized = AsyncMock(return_value=None) - parent._client = MagicMock() - parent._client.git_commit = AsyncMock(return_value={"result": "created", "commit_oid": "a" * 40}) - parent._client.git_restore = AsyncMock(return_value={"result": "applied", "commit_oid": "b" * 40}) - parent._client.git_show = AsyncMock(return_value={"oid": "c" * 40, "parents": []}) - parent._client.git_log = AsyncMock(return_value=[{"oid": "c" * 40}]) - parent._client.git_diff = AsyncMock( - return_value={ - "path": "viking://resources/a.md", - "from_commit": "a" * 40, - "to_commit": "b" * 40, - "change_type": "modified", - "diff_text": "@@ -1 +1 @@\n-old\n+new\n", - } - ) - return parent - - -@pytest.fixture -def async_ns(fake_async_client): - return AsyncSnapshotNamespace(fake_async_client) - - -# -------- AsyncSnapshotNamespace -------- - - -@pytest.mark.asyncio -async def test_async_commit_forwards(async_ns, fake_async_client): - out = await async_ns.commit(message="m", paths=["viking://x/a"], branch="dev", - author_name="me", author_email="me@x") - fake_async_client._ensure_initialized.assert_awaited() - fake_async_client._client.git_commit.assert_awaited_once_with( - message="m", paths=["viking://x/a"], branch="dev", - author_name="me", author_email="me@x", - ) - assert out["commit_oid"] == "a" * 40 - - -@pytest.mark.asyncio -async def test_async_commit_defaults(async_ns, fake_async_client): - await async_ns.commit(message="m") - kwargs = fake_async_client._client.git_commit.await_args.kwargs - assert kwargs == { - "message": "m", "paths": None, "branch": "main", - "author_name": None, "author_email": None, - } - - -@pytest.mark.asyncio -async def test_async_restore_forwards(async_ns, fake_async_client): - out = await async_ns.restore( - project_dir="viking://resources/proj", - source_commit="d" * 40, - dry_run=True, - message="rollback", - ) - fake_async_client._client.git_restore.assert_awaited_once_with( - project_dir="viking://resources/proj", - source_commit="d" * 40, - branch="main", - dry_run=True, - message="rollback", - author_name=None, - author_email=None, - ) - assert out["result"] == "applied" - - -@pytest.mark.asyncio -async def test_async_restore_defaults_project_dir_none(async_ns, fake_async_client): - await async_ns.restore(source_commit="d" * 40) - fake_async_client._client.git_restore.assert_awaited_once_with( - project_dir=None, - source_commit="d" * 40, - branch="main", - dry_run=False, - message=None, - author_name=None, - author_email=None, - ) - - -@pytest.mark.asyncio -async def test_async_show_no_path(async_ns, fake_async_client): - out = await async_ns.show("main") - fake_async_client._client.git_show.assert_awaited_once_with("main", path=None) - assert out["oid"] == "c" * 40 - - -@pytest.mark.asyncio -async def test_async_show_with_path(async_ns, fake_async_client): - fake_async_client._client.git_show = AsyncMock(return_value=b"data") - out = await async_ns.show("main", path="viking://x/a") - fake_async_client._client.git_show.assert_awaited_once_with("main", path="viking://x/a") - assert out == b"data" - - -@pytest.mark.asyncio -async def test_async_log_defaults(async_ns, fake_async_client): - out = await async_ns.log() - fake_async_client._client.git_log.assert_awaited_once_with( - branch="main", limit=20, paths=None - ) - assert len(out) == 1 - - -@pytest.mark.asyncio -async def test_async_log_overrides(async_ns, fake_async_client): - await async_ns.log(branch="dev", limit=5, paths=["viking://resources/a.md"]) - fake_async_client._client.git_log.assert_awaited_once_with( - branch="dev", limit=5, paths=["viking://resources/a.md"] - ) - - -@pytest.mark.asyncio -async def test_async_diff_forwards(async_ns, fake_async_client): - out = await async_ns.diff( - "viking://resources/a.md", - from_ref="a" * 40, - to_ref="b" * 40, - ) - fake_async_client._client.git_diff.assert_awaited_once_with( - "viking://resources/a.md", - from_ref="a" * 40, - to_ref="b" * 40, - ) - assert out["change_type"] == "modified" - - -@pytest.mark.asyncio -async def test_async_ensures_initialized_before_every_call(async_ns, fake_async_client): - await async_ns.commit(message="m") - await async_ns.show("main") - await async_ns.log() - assert fake_async_client._ensure_initialized.await_count == 3 - - -# -------- SyncSnapshotNamespace -------- - - -def test_sync_namespace_delegates_through_async(monkeypatch): - """SyncSnapshotNamespace.commit() runs the async equivalent via run_async.""" - # Build a fake SyncOpenViking exposing an async_client with a snapshot namespace. - sync_parent = MagicMock() - inner_async_ns = MagicMock() - inner_async_ns.commit = AsyncMock(return_value={"commit_oid": "z" * 40}) - inner_async_ns.restore = AsyncMock(return_value={"result": "applied"}) - inner_async_ns.show = AsyncMock(return_value=b"blob") - inner_async_ns.log = AsyncMock(return_value=[]) - inner_async_ns.diff = AsyncMock(return_value={"change_type": "modified"}) - sync_parent._async_client.snapshot = inner_async_ns - - sync_ns = SyncSnapshotNamespace(sync_parent) - - out = sync_ns.commit(message="m") - assert out["commit_oid"] == "z" * 40 - inner_async_ns.commit.assert_awaited_once_with( - message="m", paths=None, branch="main", - author_name=None, author_email=None, - ) - - sync_ns.show("main", path="viking://x/a") - inner_async_ns.show.assert_awaited_once_with("main", path="viking://x/a") - - sync_ns.log(branch="dev", limit=3) - inner_async_ns.log.assert_awaited_once_with(branch="dev", limit=3, paths=None) - - sync_ns.diff("viking://x/a", from_ref="old", to_ref="new") - inner_async_ns.diff.assert_awaited_once_with( - "viking://x/a", from_ref="old", to_ref="new" - ) - - -def test_async_client_snapshot_property_is_lazy_and_cached(): - """Accessing .snapshot twice returns the same instance and doesn't construct early.""" - from openviking.async_client import AsyncOpenViking - # Avoid real construction by faking the singleton. - inst = object.__new__(AsyncOpenViking) - # Patch the lazy attribute machinery - assert not hasattr(inst, "_snapshot") - ns1 = inst.snapshot - assert isinstance(ns1, AsyncSnapshotNamespace) - ns2 = inst.snapshot - assert ns1 is ns2 - - -def test_sync_client_snapshot_property_is_lazy_and_cached(): - from openviking.sync_client import SyncOpenViking - inst = object.__new__(SyncOpenViking) - assert not hasattr(inst, "_snapshot") - ns1 = inst.snapshot - assert isinstance(ns1, SyncSnapshotNamespace) - ns2 = inst.snapshot - assert ns1 is ns2 diff --git a/tests/utils/mock_agfs.py b/tests/utils/mock_agfs.py index f73ad121f9..198758ec21 100644 --- a/tests/utils/mock_agfs.py +++ b/tests/utils/mock_agfs.py @@ -1,4 +1,6 @@ import shutil +import threading +import uuid from pathlib import Path from unittest.mock import MagicMock @@ -14,6 +16,9 @@ def __init__(self, config=None, root_path=None): self.config = config self.root = Path(root_path) if root_path else Path("/tmp/viking_data") self.root.mkdir(parents=True, exist_ok=True) + self._pathlocks_guard = threading.Lock() + self._pathlocks = {} + self._pathlock_leases = {} def _resolve(self, path): if str(path).startswith("viking://"): @@ -46,6 +51,41 @@ def ls(self, path, ctx=None, **kwargs): ) return res + def glob_directory( + self, + path, + pattern, + show_hidden=False, + page_size=None, + level_limit=None, + continuation_token=None, + ctx=None, + ): + del ctx + root = self._resolve(path) + matches = [] + for item in sorted(root.glob(pattern)): + relative = item.relative_to(root) + if not show_hidden and any(part.startswith(".") for part in relative.parts): + continue + if level_limit is not None and len(relative.parts) > level_limit: + continue + matches.append( + { + "path": f"{str(path).rstrip('/')}/{relative.as_posix()}", + "rel_path": relative.as_posix(), + "name": item.name, + "is_dir": item.is_dir(), + } + ) + + start = int(continuation_token or 0) + end = len(matches) if not page_size else start + page_size + return { + "entries": matches[start:end], + "next_token": str(end) if end < len(matches) else None, + } + def writeto(self, path, content, ctx=None, **kwargs): p = self._resolve(path) p.parent.mkdir(parents=True, exist_ok=True) @@ -70,6 +110,9 @@ def read_file(self, path, ctx=None, **kwargs): def read(self, path, ctx=None, **kwargs): return self.read_file(path, ctx, **kwargs) + def cat(self, path, ctx=None, **kwargs): + return self.read_file(path, ctx, **kwargs) + def rm(self, path, recursive=False, ctx=None): p = self._resolve(path) if p.exists(): @@ -95,7 +138,45 @@ def stat(self, path, ctx=None): if not p.exists(): raise FileNotFoundError(path) s = p.stat() - return {"size": s.st_size, "mtime": s.st_mtime, "is_dir": p.is_dir()} + return { + "size": s.st_size, + "mtime": s.st_mtime, + "isDir": p.is_dir(), + "is_dir": p.is_dir(), + } def bind_request_context(self, ctx): return MagicMock(__enter__=lambda x: None, __exit__=lambda x, y, z: None) + + def pathlock_acquire_tree( + self, + ctx, + path, + timeout_secs=0.0, + owner_lease_ref=None, + ): + del ctx, owner_lease_ref + with self._pathlocks_guard: + lock = self._pathlocks.setdefault(path, threading.Lock()) + + acquired = lock.acquire(timeout=timeout_secs) + if not acquired: + raise TimeoutError(f"timed out acquiring test path lock: {path}") + + lease_ref = str(uuid.uuid4()) + lease = { + "lease_ref": lease_ref, + "ownership_ref": str(uuid.uuid4()), + "owner_id": "mock-local-agfs", + "owned": True, + } + with self._pathlocks_guard: + self._pathlock_leases[lease_ref] = lock + return lease + + def pathlock_release(self, ctx, owned_lease_ref): + del ctx + lease_ref = owned_lease_ref["lease_ref"] + with self._pathlocks_guard: + lock = self._pathlock_leases.pop(lease_ref) + lock.release()