From 74a7fcbeded899aaa3042c558821aeae9bb76ffa Mon Sep 17 00:00:00 2001 From: Gowtham Rao MD PhD Date: Sat, 16 May 2026 10:35:10 -0400 Subject: [PATCH] feat: add CLI application and discovery API router for semantic tool search --- src/coreason_runtime/api/discovery_router.py | 50 ++++++++++++++++ src/coreason_runtime/cli.py | 2 + tests/api/test_discovery_router.py | 62 ++++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 src/coreason_runtime/api/discovery_router.py create mode 100644 tests/api/test_discovery_router.py diff --git a/src/coreason_runtime/api/discovery_router.py b/src/coreason_runtime/api/discovery_router.py new file mode 100644 index 00000000..5c2fa54d --- /dev/null +++ b/src/coreason_runtime/api/discovery_router.py @@ -0,0 +1,50 @@ +# Copyright (c) 2026 CoReason, Inc. +# +# This software is proprietary and dual-licensed +# Licensed under the Prosperity Public License 3.0 (the "License") +# A copy of the license is available at +# For details, see the LICENSE file +# Commercial use beyond a 30-day trial requires a separate license +# +# Source Code: + +from typing import Any + +from fastapi import APIRouter +from pydantic import BaseModel, Field + +from coreason_runtime.execution_plane.discovery_indexer import DiscoveryIndexer + +discovery_router = APIRouter(prefix="/api/v1/discovery", tags=["Semantic Discovery"]) + + +class DiscoverySearchRequest(BaseModel): + query: str = Field(..., description="The semantic query to search for capabilities.") + limit: int = Field(5, description="Maximum number of results to return.") + tenant_cid: str = Field( + "889955217295c2bfef2d6812071b633b0819477e67f57853febf116f69f30531", + description="Tenant CID for segregation.", + ) + + +@discovery_router.post("/search") +async def search_capabilities(request: DiscoverySearchRequest) -> list[dict[str, Any]]: + """AGENT INSTRUCTION: Perform semantic discovery of URN-addressable capabilities. + + CAUSAL AFFORDANCE: Resolves a natural language intent into a ranked list of available tools. + + EPISTEMIC BOUNDS: Limited to the capabilities indexed in the tenant's vector store. + + MCP ROUTING TRIGGERS: Semantic Search, Tool Discovery, Vector Retrieval, Intent Matching + """ + try: + indexer = DiscoveryIndexer(tenant_cid=request.tenant_cid) + # Ensure local WASM tools are indexed + indexer.sync_local_wasm() + # Note: Remote MCP sync is typically handled by the NemoClaw bridge out-of-process, + # but search_capabilities will find whatever is already in the LanceDB table. + return indexer.search_capabilities(query=request.query, limit=request.limit, tenant_cid=request.tenant_cid) + except Exception as e: + from fastapi import HTTPException + + raise HTTPException(status_code=500, detail=f"Discovery search failed: {e}") from e diff --git a/src/coreason_runtime/cli.py b/src/coreason_runtime/cli.py index a9b30781..0c860840 100644 --- a/src/coreason_runtime/cli.py +++ b/src/coreason_runtime/cli.py @@ -67,6 +67,7 @@ def create_app() -> Any: FastAPIInstrumentor.instrument_app(api_app) + from coreason_runtime.api.discovery_router import discovery_router from coreason_runtime.api.oracle import router as oracle_router from coreason_runtime.api.predict_router import predict_router from coreason_runtime.api.schema import router as schema_router @@ -78,6 +79,7 @@ def create_app() -> Any: api_app.include_router(oracle_router) api_app.include_router(predict_router) api_app.include_router(shocks_router) + api_app.include_router(discovery_router) return api_app diff --git a/tests/api/test_discovery_router.py b/tests/api/test_discovery_router.py new file mode 100644 index 00000000..2fb6a8c0 --- /dev/null +++ b/tests/api/test_discovery_router.py @@ -0,0 +1,62 @@ +# Copyright (c) 2026 CoReason, Inc. +# +# This software is proprietary and dual-licensed +# Licensed under the Prosperity Public License 3.0 (the "License") +# A copy of the license is available at +# For details, see the LICENSE file +# Commercial use beyond a 30-day trial requires a separate license +# +# Source Code: + +import pytest +from fastapi import FastAPI +from httpx import ASGITransport, AsyncClient +from unittest.mock import AsyncMock, patch, MagicMock + +from coreason_runtime.api.discovery_router import discovery_router + +app = FastAPI() +app.include_router(discovery_router) + +@pytest.fixture +async def client(): + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as c: + yield c + +@pytest.mark.asyncio +async def test_search_discovery_success(client: AsyncClient): + mock_results = [ + { + "urn": "urn:coreason:actionspace:solver:test:v1", + "score": 0.95 + } + ] + + with patch("coreason_runtime.api.discovery_router.DiscoveryIndexer") as MockIndexer: + # Configure the mock instance + mock_instance = MockIndexer.return_value + mock_instance.search_capabilities = MagicMock(return_value=mock_results) + + payload = {"query": "find test tools", "limit": 5} + response = await client.post("/api/v1/discovery/search", json=payload) + + assert response.status_code == 200 + assert response.json() == mock_results + mock_instance.search_capabilities.assert_called_once_with( + query="find test tools", + limit=5, + tenant_cid="889955217295c2bfef2d6812071b633b0819477e67f57853febf116f69f30531" + ) + +@pytest.mark.asyncio +async def test_search_discovery_error(client: AsyncClient): + with patch("coreason_runtime.api.discovery_router.DiscoveryIndexer") as MockIndexer: + mock_instance = MockIndexer.return_value + mock_instance.search_capabilities = MagicMock(side_effect=Exception("Database error")) + + payload = {"query": "fail query"} + response = await client.post("/api/v1/discovery/search", json=payload) + + assert response.status_code == 500 + assert "Discovery search failed" in response.json()["detail"]