diff --git a/sdk/python/openviking_sdk/client.py b/sdk/python/openviking_sdk/client.py index 9b2142b9bd..568029f34f 100644 --- a/sdk/python/openviking_sdk/client.py +++ b/sdk/python/openviking_sdk/client.py @@ -691,6 +691,8 @@ async def add_resource( add_type: Optional[str] = None, tags: Optional[List[str]] = None, tag_mode: str = "replace", + create_parent: bool = False, + source_name: Optional[str] = None, ) -> Dict[str, Any]: if add_type is not None: add_type = add_type.strip() or None @@ -725,18 +727,26 @@ async def add_resource( request_data["tag_mode"] = tag_mode if preserve_structure is not None: request_data["preserve_structure"] = preserve_structure + # Match CLI: only send create_parent when True so older servers that + # forbid unknown fields still accept the request. + if create_parent: + request_data["create_parent"] = True + if source_name is not None: + request_data["source_name"] = source_name path_obj = Path(path) if not add_type and path_obj.exists(): if path_obj.is_dir(): - request_data["source_name"] = path_obj.name + if source_name is None: + request_data["source_name"] = path_obj.name zip_path = self._zip_directory(path) try: request_data["temp_file_id"] = await self._upload_temp_file(zip_path) finally: Path(zip_path).unlink(missing_ok=True) elif path_obj.is_file(): - request_data["source_name"] = path_obj.name + if source_name is None: + request_data["source_name"] = path_obj.name request_data["temp_file_id"] = await self._upload_temp_file(path) else: request_data["path"] = path @@ -771,10 +781,13 @@ async def add_skill( timeout: Optional[float] = None, telemetry: Any = False, target_uri: Optional[str] = None, + source_metadata: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: request_data = {"wait": wait, "timeout": timeout, "telemetry": telemetry} if target_uri is not None: request_data["target_uri"] = target_uri + if source_metadata is not None: + request_data["source_metadata"] = source_metadata if isinstance(data, str): path_obj = Path(data) if path_obj.exists(): @@ -1946,6 +1959,8 @@ def add_resource( add_type: Optional[str] = None, tags: Optional[List[str]] = None, tag_mode: str = "replace", + create_parent: bool = False, + source_name: Optional[str] = None, ) -> Dict[str, Any]: return run_async( self._async_client.add_resource( @@ -1968,6 +1983,8 @@ def add_resource( args=args, tags=tags, tag_mode=tag_mode, + create_parent=create_parent, + source_name=source_name, telemetry=telemetry, ) ) @@ -1989,6 +2006,7 @@ def add_skill( timeout: Optional[float] = None, telemetry: Any = False, target_uri: Optional[str] = None, + source_metadata: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: return run_async( self._async_client.add_skill( @@ -1997,6 +2015,7 @@ def add_skill( timeout=timeout, telemetry=telemetry, target_uri=target_uri, + source_metadata=source_metadata, ) ) diff --git a/sdk/python/tests/test_async_client_behaviors.py b/sdk/python/tests/test_async_client_behaviors.py index 71be100729..66b77218df 100644 --- a/sdk/python/tests/test_async_client_behaviors.py +++ b/sdk/python/tests/test_async_client_behaviors.py @@ -804,6 +804,105 @@ async def test_add_resource_sends_tags_and_tag_mode(): ) +@pytest.mark.asyncio +async def test_add_resource_forwards_create_parent_and_source_name(): + client = AsyncHTTPClient(url="http://localhost:1933") + fake_http = SimpleNamespace(post=AsyncMock(return_value=object())) + client._http = fake_http + client._handle_response_data = lambda _response: { + "result": {"root_uri": "viking://user/resources/docs/demo"} + } + + await client.add_resource( + path="https://example.com/demo.md", + parent="viking://user/resources/docs", + create_parent=True, + source_name="custom-demo.md", + ) + + payload = fake_http.post.await_args.kwargs["json"] + assert payload["parent"] == "viking://user/resources/docs" + assert payload["create_parent"] is True + assert payload["source_name"] == "custom-demo.md" + + +@pytest.mark.asyncio +async def test_add_resource_omits_default_create_parent_for_legacy_servers(): + client = AsyncHTTPClient(url="http://localhost:1933") + fake_http = SimpleNamespace(post=AsyncMock(return_value=object())) + client._http = fake_http + client._handle_response_data = lambda _response: { + "result": {"root_uri": "viking://resources/demo"} + } + + await client.add_resource("https://example.com/demo.md") + + payload = fake_http.post.await_args.kwargs["json"] + assert "create_parent" not in payload + assert "source_name" not in payload + + +@pytest.mark.asyncio +async def test_add_resource_explicit_source_name_overrides_local_filename(tmp_path): + resource_file = tmp_path / "demo.md" + resource_file.write_text("# Demo\n") + + client = AsyncHTTPClient(url="http://127.0.0.1:1933") + fake_http = SimpleNamespace(post=AsyncMock(return_value=object())) + client._http = fake_http + client._upload_temp_file = AsyncMock(return_value="upload_resource.md") + client._handle_response_data = lambda _response: { + "result": {"root_uri": "viking://resources/demo"} + } + + await client.add_resource(str(resource_file), source_name="renamed.md") + + payload = fake_http.post.await_args.kwargs["json"] + assert payload["temp_file_id"] == "upload_resource.md" + assert payload["source_name"] == "renamed.md" + + +def test_sync_add_resource_accepts_and_forwards_create_parent(): + client = SyncHTTPClient(url="http://localhost:1933") + with patch.object( + client._async_client, + "add_resource", + new=AsyncMock(return_value={"root_uri": "viking://resources/demo"}), + ) as mock_add_resource: + result = client.add_resource( + "https://example.com/demo.md", + parent="viking://user/resources/docs", + create_parent=True, + source_name="custom-demo.md", + ) + + assert result == {"root_uri": "viking://resources/demo"} + assert mock_add_resource.await_args.kwargs["create_parent"] is True + assert mock_add_resource.await_args.kwargs["source_name"] == "custom-demo.md" + assert mock_add_resource.await_args.kwargs["parent"] == "viking://user/resources/docs" + + +@pytest.mark.asyncio +async def test_add_skill_forwards_source_metadata(): + client = AsyncHTTPClient(url="http://localhost:1933") + fake_http = SimpleNamespace(post=AsyncMock(return_value=object())) + client._http = fake_http + client._handle_response_data = lambda _response: {"result": {"status": "ok"}} + + await client.add_skill( + {"name": "demo", "description": "demo"}, + source_metadata={"type": "api", "source": "inline_content", "operation": "add"}, + ) + + payload = fake_http.post.await_args.kwargs["json"] + assert payload["source_metadata"] == { + "type": "api", + "source": "inline_content", + "operation": "add", + } + assert payload["data"] == {"name": "demo", "description": "demo"} + + @pytest.mark.asyncio async def test_find_uses_node_limit_as_http_limit_and_normalizes_target_uri_list(): client = AsyncHTTPClient(url="http://localhost:1933")