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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions sdk/python/openviking_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,11 @@ async def find(
tags: Optional[List[str]] = None,
telemetry: Any = False,
image: Any = None,
since: Optional[str] = None,
until: Optional[str] = None,
time_field: Optional[str] = None,
level: Optional[List[int]] = None,
include_provenance: Optional[bool] = None,
) -> Dict[str, Any]:
actual_limit = node_limit if node_limit is not None else limit
payload = {
Expand All @@ -1208,6 +1213,11 @@ async def find(
"filter": filter,
"context_type": self._normalize_context_type(context_type),
"tags": tags,
"since": since,
"until": until,
"time_field": time_field,
"level": level,
"include_provenance": include_provenance,
"telemetry": telemetry,
}
payload = self._compact_request_body(payload)
Expand All @@ -1228,6 +1238,11 @@ async def search(
tags: Optional[List[str]] = None,
telemetry: Any = False,
image: Any = None,
since: Optional[str] = None,
until: Optional[str] = None,
time_field: Optional[str] = None,
level: Optional[List[int]] = None,
include_provenance: Optional[bool] = None,
) -> Dict[str, Any]:
actual_limit = node_limit if node_limit is not None else limit
sid = session_id or (session.session_id if session else None)
Expand All @@ -1241,6 +1256,11 @@ async def search(
"filter": filter,
"context_type": self._normalize_context_type(context_type),
"tags": tags,
"since": since,
"until": until,
"time_field": time_field,
"level": level,
"include_provenance": include_provenance,
"telemetry": telemetry,
}
payload = self._compact_request_body(payload)
Expand Down Expand Up @@ -2220,6 +2240,11 @@ def find(
tags: Optional[List[str]] = None,
telemetry: Any = False,
image: Any = None,
since: Optional[str] = None,
until: Optional[str] = None,
time_field: Optional[str] = None,
level: Optional[List[int]] = None,
include_provenance: Optional[bool] = None,
) -> Dict[str, Any]:
return run_async(
self._async_client.find(
Expand All @@ -2233,6 +2258,11 @@ def find(
tags=tags,
telemetry=telemetry,
image=image,
since=since,
until=until,
time_field=time_field,
level=level,
include_provenance=include_provenance,
)
)

Expand All @@ -2250,6 +2280,11 @@ def search(
tags: Optional[List[str]] = None,
telemetry: Any = False,
image: Any = None,
since: Optional[str] = None,
until: Optional[str] = None,
time_field: Optional[str] = None,
level: Optional[List[int]] = None,
include_provenance: Optional[bool] = None,
) -> Dict[str, Any]:
actual_session_id = session_id
if actual_session_id is None and session is not None:
Expand All @@ -2267,6 +2302,11 @@ def search(
tags=tags,
telemetry=telemetry,
image=image,
since=since,
until=until,
time_field=time_field,
level=level,
include_provenance=include_provenance,
)
)

Expand Down
92 changes: 89 additions & 3 deletions sdk/python/tests/test_async_client_behaviors.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,47 @@ def test_add_resource_signatures_keep_telemetry_position():
assert params.index("telemetry") < params.index("tag_mode")


@pytest.mark.parametrize(
("method_name", "expected_extra"),
[
("find", {}),
("search", {"session_id": None}),
],
)
def test_sync_http_client_forwards_retrieval_contract_fields(method_name, expected_extra):
client = SyncHTTPClient(url="http://localhost:1933")
retrieval_options = {
"since": "2026-08-01T00:00:00Z",
"until": "2026-08-02T00:00:00Z",
"time_field": "updated_at",
"level": [1, 2],
"include_provenance": True,
}

with patch.object(
client._async_client,
method_name,
return_value={"total": 0, "resources": []},
) as mock_retrieval:
result = getattr(client, method_name)(query="sample", **retrieval_options)

assert result == {"total": 0, "resources": []}
mock_retrieval.assert_called_once_with(
query="sample",
target_uri="",
limit=10,
node_limit=None,
score_threshold=None,
filter=None,
context_type=None,
tags=None,
telemetry=False,
image=None,
**retrieval_options,
**expected_extra,
)


@pytest.mark.asyncio
async def test_async_http_client_initialize_forwards_event_hooks():
async def request_hook(_request):
Expand Down Expand Up @@ -728,7 +769,7 @@ async def test_add_resource_sends_tags_and_tag_mode():


@pytest.mark.asyncio
async def test_find_uses_node_limit_as_http_limit_and_normalizes_target_uri_list():
async def test_find_sends_exact_retrieval_body_when_optional_fields_are_set():
client = AsyncHTTPClient(url="http://localhost:1933")
fake_http = SimpleNamespace(post=AsyncMock(return_value=object()))
client._http = fake_http
Expand All @@ -744,6 +785,11 @@ async def test_find_uses_node_limit_as_http_limit_and_normalizes_target_uri_list
context_type="resource",
tags=["k:v"],
telemetry={"enabled": True},
since="2026-08-01T00:00:00Z",
until="2026-08-02T00:00:00Z",
time_field="created_at",
level=[0, 2],
include_provenance=True,
)

fake_http.post.assert_awaited_once_with(
Expand All @@ -756,20 +802,35 @@ async def test_find_uses_node_limit_as_http_limit_and_normalizes_target_uri_list
"filter": {"type": "resource"},
"context_type": "resource",
"tags": ["k:v"],
"since": "2026-08-01T00:00:00Z",
"until": "2026-08-02T00:00:00Z",
"time_field": "created_at",
"level": [0, 2],
"include_provenance": True,
"telemetry": {"enabled": True},
},
)


@pytest.mark.asyncio
async def test_search_uses_session_wrapper_session_id_in_payload():
async def test_search_sends_exact_retrieval_body_with_session_context():
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": {"total": 0, "resources": []}}

session = Session(client, "thread-123")
await client.search(query="sample", target_uri="/resources/demo", session=session, limit=5)
await client.search(
query="sample",
target_uri="/resources/demo",
session=session,
limit=5,
since="7d",
until="now",
time_field="updated_at",
level=[1],
include_provenance=False,
)

fake_http.post.assert_awaited_once_with(
"/api/v1/search/search",
Expand All @@ -778,6 +839,31 @@ async def test_search_uses_session_wrapper_session_id_in_payload():
"target_uri": "viking://resources/demo",
"session_id": "thread-123",
"limit": 5,
"since": "7d",
"until": "now",
"time_field": "updated_at",
"level": [1],
"include_provenance": False,
"telemetry": False,
},
)


@pytest.mark.asyncio
async def test_find_omits_unset_retrieval_fields_from_exact_body():
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": {"total": 0, "resources": []}}

await client.find(query="sample")

fake_http.post.assert_awaited_once_with(
"/api/v1/search/find",
json={
"query": "sample",
"target_uri": "",
"limit": 10,
"telemetry": False,
},
)
Expand Down
1 change: 1 addition & 0 deletions sdk/typescript/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ export class OpenVikingClient {
until: options.until,
time_field: options.timeField,
level: options.level,
include_provenance: options.includeProvenance,
tags: options.tags,
}),
});
Expand Down
1 change: 1 addition & 0 deletions sdk/typescript/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export interface SearchOptions {
until?: string;
timeField?: string;
level?: number[];
includeProvenance?: boolean;
tags?: string[];
}
/** Content grep options. */
Expand Down
25 changes: 20 additions & 5 deletions sdk/typescript/tests/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe("OpenVikingClient", () => {
);
});

it("sends identity headers and the Python/Go compatible search body", async () => {
it("sends the exact retrieval body when optional fields are set", async () => {
const fetcher = vi
.fn<typeof fetch>()
.mockResolvedValue(ok({ resources: [] }));
Expand All @@ -34,20 +34,33 @@ describe("OpenVikingClient", () => {
actorPeerId: "peer",
fetch: fetcher,
});
await client.find("hello", { targetUri: "viking://resources", limit: 5 });
await client.find("hello", {
targetUri: "viking://resources",
limit: 5,
since: "2026-08-01T00:00:00Z",
until: "2026-08-02T00:00:00Z",
timeField: "created_at",
level: [0, 2],
includeProvenance: true,
});
const [url, init] = fetcher.mock.calls[0]!;
expect(String(url)).toBe("https://example.com/api/v1/search/find");
expect(new Headers(init?.headers).get("X-OpenViking-Actor-Peer")).toBe(
"peer",
);
expect(JSON.parse(String(init?.body))).toMatchObject({
expect(JSON.parse(String(init?.body))).toEqual({
query: "hello",
target_uri: "viking://resources",
limit: 5,
since: "2026-08-01T00:00:00Z",
until: "2026-08-02T00:00:00Z",
time_field: "created_at",
level: [0, 2],
include_provenance: true,
});
});

it("uses the Python/Go empty default retrieval target", async () => {
it("omits unset retrieval options from the exact body", async () => {
const fetcher = vi
.fn<typeof fetch>()
.mockResolvedValue(ok({ resources: [] }));
Expand All @@ -58,8 +71,10 @@ describe("OpenVikingClient", () => {

await client.search("hello");

expect(JSON.parse(String(fetcher.mock.calls[0]![1]?.body))).toMatchObject({
expect(JSON.parse(String(fetcher.mock.calls[0]![1]?.body))).toEqual({
query: "hello",
target_uri: "",
limit: 10,
});
});

Expand Down