Skip to content
Open
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
16 changes: 12 additions & 4 deletions openviking/core/retrieval_targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ class ResolvedRetrievalTargets:
def resolve_retrieval_targets(
target_uri: Union[str, List[str]],
ctx: RequestContext,
*,
context_type: Optional[ContextType] = None,
) -> ResolvedRetrievalTargets:
"""Resolve search/find target directories."""
target_uris = _dedupe_target_uris(target_uri)

if not target_uris:
return ResolvedRetrievalTargets(
target_directories=default_target_directories(ctx),
target_directories=default_target_directories(ctx, context_type=context_type),
)

target_directories: List[str] = []
Expand Down Expand Up @@ -147,11 +149,17 @@ def _actor_peer_targets(ctx: RequestContext) -> List[str]:
f"{peer_root}/memories",
f"{peer_root}/resources",
]
def _is_agent_scope_uri(target_uri: str) -> bool:
parts = target_uri[len("viking://"):].strip("/").split("/")
return parts and parts[0] == "agent" and len(parts) >= 2 and parts[1] in {"skills", "endpoints", "tools", "payments"}


def _is_agent_scope_uri(target_uri: str) -> bool:
parts = target_uri[len("viking://") :].strip("/").split("/")
return (
parts
and parts[0] == "agent"
and len(parts) >= 2
and parts[1] in {"skills", "endpoints", "tools", "payments"}
)


def _resolve_peer_target(
target_uri: str,
Expand Down
14 changes: 14 additions & 0 deletions openviking/server/routers/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@
SearchContextTypeInput,
_resolve_levels,
merge_search_filter,
resolve_context_types,
)
from openviking.utils.tags import build_search_tags_filter
from openviking_cli.exceptions import InvalidArgumentError, NotFoundError
from openviking_cli.retrieve import ContextType


def _sanitize_floats(obj: Any) -> Any:
Expand Down Expand Up @@ -95,6 +97,16 @@ def _resolve_search_filter(
raise InvalidArgumentError(str(exc)) from exc


def _resolve_scope_context_type(
context_type: Optional[SearchContextTypeInput],
) -> Optional[ContextType]:
"""Return a single context type suitable for narrowing default target scope."""
resolved = resolve_context_types(context_type)
if len(resolved) != 1:
return None
return ContextType(resolved[0])


def _resolve_uri_or_uris(uri: Union[str, List[str]], ctx: RequestContext) -> Union[str, List[str]]:
"""Resolve path variables in a single URI or list of URIs."""
if isinstance(uri, list):
Expand Down Expand Up @@ -353,6 +365,7 @@ async def find(
filter=effective_filter,
level=_resolve_levels(request.level) or None,
image_url=resolved_image_url,
context_type=_resolve_scope_context_type(request.context_type),
),
)
result = execution.result
Expand Down Expand Up @@ -467,6 +480,7 @@ async def _search():
filter=effective_filter,
level=_resolve_levels(request.level) or None,
image_url=resolved_image_url,
context_type=_resolve_scope_context_type(request.context_type),
)

execution = await run_operation(
Expand Down
7 changes: 7 additions & 0 deletions openviking/service/search_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
is_viking_uri,
)
from openviking_cli.exceptions import InvalidArgumentError, NotInitializedError
from openviking_cli.retrieve import ContextType
from openviking_cli.utils import get_logger

if TYPE_CHECKING:
Expand Down Expand Up @@ -85,6 +86,7 @@ async def search(
filter: Optional[Dict] = None,
level: Optional[List[int]] = None,
image_url: Optional[str] = None,
context_type: Optional[ContextType] = None,
) -> Any:
"""Complex search with session context.

Expand All @@ -96,6 +98,7 @@ async def search(
score_threshold: Score threshold
filter: Metadata filters
level: Filter by level (0=abstract, 1=overview, 2=file)
context_type: Restrict default target scope when target_uri is empty

Returns:
FindResult
Expand All @@ -119,6 +122,7 @@ async def search(
filter=filter,
level=level,
image_url=resolved_image_url,
context_type=context_type,
)
return result

Expand All @@ -132,6 +136,7 @@ async def find(
filter: Optional[Dict] = None,
level: Optional[List[int]] = None,
image_url: Optional[str] = None,
context_type: Optional[ContextType] = None,
) -> Any:
"""Semantic search without session context.

Expand All @@ -142,6 +147,7 @@ async def find(
score_threshold: Score threshold
filter: Metadata filters
level: Filter by level (0=abstract, 1=overview, 2=file)
context_type: Restrict default target scope when target_uri is empty

Returns:
FindResult
Expand All @@ -158,5 +164,6 @@ async def find(
filter=filter,
level=level,
image_url=resolved_image_url,
context_type=context_type,
)
return result
13 changes: 9 additions & 4 deletions openviking/storage/viking_fs/_semantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from openviking.telemetry import get_current_telemetry
from openviking.utils.image_search import build_multimodal_embedding_input
from openviking_cli.exceptions import NotFoundError
from openviking_cli.retrieve import ContextType


class _SemanticMixin:
Expand Down Expand Up @@ -191,6 +192,7 @@ async def find(
ctx: Optional[RequestContext] = None,
level: Optional[List[int]] = None,
image_url: Optional[str] = None,
context_type: Optional[ContextType] = None,
):
"""Semantic search.

Expand All @@ -211,13 +213,14 @@ async def find(
RetrieverMode,
)
from openviking_cli.retrieve import (
ContextType,
FindResult,
TypedQuery,
)

real_ctx = self._ctx_or_default(ctx)
retrieval_targets = resolve_retrieval_targets(target_uri, real_ctx)
retrieval_targets = resolve_retrieval_targets(
target_uri, real_ctx, context_type=context_type
)

for target_dir in retrieval_targets.target_directories:
await self._ensure_retrieval_scope(target_dir, ctx)
Expand Down Expand Up @@ -292,6 +295,7 @@ async def search(
ctx: Optional[RequestContext] = None,
level: Optional[List[int]] = None,
image_url: Optional[str] = None,
context_type: Optional[ContextType] = None,
):
"""Complex search with session context.

Expand All @@ -310,14 +314,15 @@ async def search(
from openviking.retrieve.hierarchical_retriever import HierarchicalRetriever
from openviking.retrieve.intent_analyzer import IntentAnalyzer
from openviking_cli.retrieve import (
ContextType,
FindResult,
QueryPlan,
TypedQuery,
)

real_ctx = self._ctx_or_default(ctx)
retrieval_targets = resolve_retrieval_targets(target_uri, real_ctx)
retrieval_targets = resolve_retrieval_targets(
target_uri, real_ctx, context_type=context_type
)
primary_target_uri = retrieval_targets.first_explicit_directory

session_summary = (
Expand Down
9 changes: 9 additions & 0 deletions tests/server/test_actor_peer_retrieval_targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ def _ctx(
def _target_dirs(
target_uri="",
actor_peer_id: str | None = None,
context_type: ContextType | None = None,
):
return resolve_retrieval_targets(
target_uri,
_ctx(actor_peer_id),
context_type=context_type,
).target_directories


Expand Down Expand Up @@ -109,6 +111,13 @@ def test_actor_skill_defaults_include_user_and_shared_agent_skills():
]


def test_empty_target_uri_respects_skill_context_type():
assert _target_dirs(context_type=ContextType.SKILL) == [
"viking://user/support_bot/skills",
"viking://agent/skills",
]


def test_actor_default_resource_targets_global_and_actor_peer_resources():
assert default_target_directories(
_ctx("web-visitor-alice"), context_type=ContextType.RESOURCE
Expand Down
51 changes: 51 additions & 0 deletions tests/server/test_api_search_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
# SPDX-License-Identifier: AGPL-3.0

import json
from types import SimpleNamespace

import httpx

from openviking.server.identity import RequestContext, Role
from openviking.server.routers import search as search_router
from openviking.server.routers.search import FindRequest, SearchRequest
from openviking_cli.retrieve import ContextType, MatchedContext
from openviking_cli.session.user_id import UserIdentifier

Expand Down Expand Up @@ -90,6 +93,54 @@ async def fake_find(**kwargs):
assert "viking://agent/skills" in targets


async def test_find_forwards_context_type_to_search_service(
monkeypatch,
):
calls = []

async def fake_find(**kwargs):
calls.append(kwargs)
return _FakeFindResult()

service = SimpleNamespace(search=SimpleNamespace(find=fake_find))
monkeypatch.setattr(search_router, "get_service", lambda: service)

response = await search_router.find(
FindRequest(query="company brand", context_type="skill"),
_ctx=RequestContext(
user=UserIdentifier("acct", "test_user"),
role=Role.USER,
),
)

assert response["status"] == "ok"
assert calls[0]["context_type"] == ContextType.SKILL


async def test_search_forwards_context_type_to_search_service(
monkeypatch,
):
calls = []

async def fake_search(**kwargs):
calls.append(kwargs)
return _FakeFindResult()

service = SimpleNamespace(search=SimpleNamespace(search=fake_search))
monkeypatch.setattr(search_router, "get_service", lambda: service)

response = await search_router.search(
SearchRequest(query="company brand", context_type="skill"),
_ctx=RequestContext(
user=UserIdentifier("acct", "test_user"),
role=Role.USER,
),
)

assert response["status"] == "ok"
assert calls[0]["context_type"] == ContextType.SKILL


async def test_coding_purpose_searches_all_domains_and_actor_resource(
client: httpx.AsyncClient,
service,
Expand Down