Merge pull request #639 from cbcoutinho/fix/astrolabe-vector-sync-warning-637
fix: expose public status endpoints and handle vector sync gracefully
This commit is contained in:
+4
-4
@@ -90,9 +90,9 @@ services:
|
|||||||
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
||||||
|
|
||||||
# Semantic search configuration (ADR-007, ADR-021)
|
# Semantic search configuration (ADR-007, ADR-021)
|
||||||
#- ENABLE_SEMANTIC_SEARCH=true
|
- ENABLE_SEMANTIC_SEARCH=true
|
||||||
- VECTOR_SYNC_SCAN_INTERVAL=60
|
- VECTOR_SYNC_SCAN_INTERVAL=5
|
||||||
- VECTOR_SYNC_PROCESSOR_WORKERS=1
|
- VECTOR_SYNC_PROCESSOR_WORKERS=2
|
||||||
|
|
||||||
#- LOG_FORMAT=json
|
#- LOG_FORMAT=json
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ services:
|
|||||||
# 1. Network mode: Set QDRANT_URL=http://qdrant:6333 (requires qdrant service)
|
# 1. Network mode: Set QDRANT_URL=http://qdrant:6333 (requires qdrant service)
|
||||||
# 2. In-memory mode: Set QDRANT_LOCATION=:memory: (default if nothing set)
|
# 2. In-memory mode: Set QDRANT_LOCATION=:memory: (default if nothing set)
|
||||||
# 3. Persistent local: Set QDRANT_LOCATION=/app/data/qdrant (stored in mcp-data volume)
|
# 3. Persistent local: Set QDRANT_LOCATION=/app/data/qdrant (stored in mcp-data volume)
|
||||||
#- QDRANT_LOCATION=/app/data/qdrant # In-memory mode used if not set
|
- QDRANT_LOCATION=":memory:"
|
||||||
#- QDRANT_URL=http://qdrant:6333 # Uncomment for network mode
|
#- QDRANT_URL=http://qdrant:6333 # Uncomment for network mode
|
||||||
#- QDRANT_API_KEY=${QDRANT_API_KEY:-my_secret_api_key} # Only for network mode
|
#- QDRANT_API_KEY=${QDRANT_API_KEY:-my_secret_api_key} # Only for network mode
|
||||||
|
|
||||||
|
|||||||
@@ -2187,11 +2187,9 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
|
|||||||
logger.info("Test webhook endpoint enabled: /webhooks/nextcloud")
|
logger.info("Test webhook endpoint enabled: /webhooks/nextcloud")
|
||||||
|
|
||||||
# Add management API endpoints for Nextcloud PHP app
|
# Add management API endpoints for Nextcloud PHP app
|
||||||
# Available in: OAuth modes OR multi-user BasicAuth with offline access (for Astrolabe integration)
|
# Tier 1: Public endpoints (no auth required) - available in all non-Smithery modes
|
||||||
enable_management_apis = oauth_enabled or (
|
# These let Astrolabe show basic server status even in single-user BasicAuth mode
|
||||||
settings.enable_multi_user_basic_auth and settings.enable_offline_access
|
if deployment_mode != DeploymentMode.SMITHERY_STATELESS:
|
||||||
)
|
|
||||||
if enable_management_apis:
|
|
||||||
routes.append(Route("/api/v1/status", get_server_status, methods=["GET"]))
|
routes.append(Route("/api/v1/status", get_server_status, methods=["GET"]))
|
||||||
routes.append(
|
routes.append(
|
||||||
Route(
|
Route(
|
||||||
@@ -2200,6 +2198,16 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
|
|||||||
methods=["GET"],
|
methods=["GET"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
logger.info(
|
||||||
|
"Public management API endpoints enabled: /api/v1/status, /api/v1/vector-sync/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tier 2+: Authenticated management endpoints (OAuth required)
|
||||||
|
# Available in: OAuth modes OR multi-user BasicAuth with offline access
|
||||||
|
enable_authenticated_management_apis = oauth_enabled or (
|
||||||
|
settings.enable_multi_user_basic_auth and settings.enable_offline_access
|
||||||
|
)
|
||||||
|
if enable_authenticated_management_apis:
|
||||||
routes.append(
|
routes.append(
|
||||||
Route(
|
Route(
|
||||||
"/api/v1/users/{user_id}/session",
|
"/api/v1/users/{user_id}/session",
|
||||||
@@ -2270,7 +2278,7 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
|
|||||||
)
|
)
|
||||||
routes.append(Route("/api/v1/scopes", list_supported_scopes, methods=["GET"]))
|
routes.append(Route("/api/v1/scopes", list_supported_scopes, methods=["GET"]))
|
||||||
logger.info(
|
logger.info(
|
||||||
"Management API endpoints enabled: /api/v1/status, /api/v1/vector-sync/status, "
|
"Authenticated management API endpoints enabled: "
|
||||||
"/api/v1/users/{user_id}/session, /api/v1/users/{user_id}/revoke, "
|
"/api/v1/users/{user_id}/session, /api/v1/users/{user_id}/revoke, "
|
||||||
"/api/v1/users/{user_id}/app-password, /api/v1/users/{user_id}/access, "
|
"/api/v1/users/{user_id}/app-password, /api/v1/users/{user_id}/access, "
|
||||||
"/api/v1/users/{user_id}/scopes, /api/v1/scopes, "
|
"/api/v1/users/{user_id}/scopes, /api/v1/scopes, "
|
||||||
|
|||||||
@@ -23,6 +23,56 @@ from mcp.types import CreateMessageResult, TextContent
|
|||||||
pytestmark = pytest.mark.integration
|
pytestmark = pytest.mark.integration
|
||||||
|
|
||||||
|
|
||||||
|
async def wait_for_vector_sync(
|
||||||
|
nc_mcp_client,
|
||||||
|
*,
|
||||||
|
initial_indexed_count: int | None = None,
|
||||||
|
max_wait: int = 90,
|
||||||
|
wait_interval: int = 1,
|
||||||
|
) -> dict:
|
||||||
|
"""Wait for vector sync to complete, returning final status.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nc_mcp_client: MCP client to poll status with.
|
||||||
|
initial_indexed_count: If set, wait until indexed_count exceeds this
|
||||||
|
value and pending_count reaches 0. Otherwise wait for idle with
|
||||||
|
no pending work.
|
||||||
|
max_wait: Maximum seconds to wait before failing.
|
||||||
|
wait_interval: Seconds between status polls.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The last status dict from nc_get_vector_sync_status.
|
||||||
|
"""
|
||||||
|
waited = 0
|
||||||
|
status_data: dict = {}
|
||||||
|
while waited < max_wait:
|
||||||
|
sync_status = await nc_mcp_client.call_tool(
|
||||||
|
"nc_get_vector_sync_status", arguments={}
|
||||||
|
)
|
||||||
|
status_data = json.loads(sync_status.content[0].text)
|
||||||
|
|
||||||
|
if initial_indexed_count is not None:
|
||||||
|
# Wait for new document(s) to be indexed
|
||||||
|
if (
|
||||||
|
status_data["indexed_count"] > initial_indexed_count
|
||||||
|
and status_data["pending_count"] == 0
|
||||||
|
):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Wait for all pending work to complete
|
||||||
|
if status_data["status"] == "idle" and status_data["pending_count"] == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
await anyio.sleep(wait_interval)
|
||||||
|
waited += wait_interval
|
||||||
|
|
||||||
|
assert waited < max_wait, (
|
||||||
|
f"Vector sync did not complete within {max_wait} seconds. "
|
||||||
|
f"Last status: {status_data}"
|
||||||
|
)
|
||||||
|
return status_data
|
||||||
|
|
||||||
|
|
||||||
async def require_vector_sync_tools(nc_mcp_client):
|
async def require_vector_sync_tools(nc_mcp_client):
|
||||||
"""Skip test if vector sync tools are not available."""
|
"""Skip test if vector sync tools are not available."""
|
||||||
tools = await nc_mcp_client.list_tools()
|
tools = await nc_mcp_client.list_tools()
|
||||||
@@ -93,37 +143,8 @@ Avoid blocking operations in async code.""",
|
|||||||
print(f"Created note ID: {_note['id']}")
|
print(f"Created note ID: {_note['id']}")
|
||||||
|
|
||||||
# Wait for vector indexing to complete
|
# Wait for vector indexing to complete
|
||||||
max_wait = 30 # Maximum 30 seconds
|
status_data = await wait_for_vector_sync(
|
||||||
wait_interval = 1 # Check every 1 second
|
nc_mcp_client, initial_indexed_count=initial_indexed_count
|
||||||
waited = 0
|
|
||||||
|
|
||||||
while waited < max_wait:
|
|
||||||
sync_status = await nc_mcp_client.call_tool(
|
|
||||||
"nc_get_vector_sync_status", arguments={}
|
|
||||||
)
|
|
||||||
status_data = json.loads(sync_status.content[0].text)
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"Sync status at {waited}s: indexed={status_data['indexed_count']}, pending={status_data['pending_count']}, status={status_data['status']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if indexed count increased (new note was indexed)
|
|
||||||
if (
|
|
||||||
status_data["indexed_count"] > initial_indexed_count
|
|
||||||
and status_data["pending_count"] == 0
|
|
||||||
):
|
|
||||||
# Sync complete and new document indexed
|
|
||||||
print(
|
|
||||||
f"✓ Sync complete: {status_data['indexed_count']} documents indexed (was {initial_indexed_count})"
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
await anyio.sleep(wait_interval)
|
|
||||||
waited += wait_interval
|
|
||||||
|
|
||||||
# Verify sync completed
|
|
||||||
assert waited < max_wait, (
|
|
||||||
f"Vector sync did not complete within {max_wait} seconds. Last status: {status_data}"
|
|
||||||
)
|
)
|
||||||
assert status_data["indexed_count"] > initial_indexed_count, (
|
assert status_data["indexed_count"] > initial_indexed_count, (
|
||||||
f"New note was not indexed (count stayed at {initial_indexed_count})"
|
f"New note was not indexed (count stayed at {initial_indexed_count})"
|
||||||
@@ -247,24 +268,7 @@ async def test_semantic_search_answer_with_limit(nc_mcp_client, temporary_note_f
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Wait for vector indexing to complete
|
# Wait for vector indexing to complete
|
||||||
|
await wait_for_vector_sync(nc_mcp_client)
|
||||||
max_wait = 30
|
|
||||||
wait_interval = 1
|
|
||||||
waited = 0
|
|
||||||
|
|
||||||
while waited < max_wait:
|
|
||||||
sync_status = await nc_mcp_client.call_tool(
|
|
||||||
"nc_get_vector_sync_status", arguments={}
|
|
||||||
)
|
|
||||||
status_data = json.loads(sync_status.content[0].text)
|
|
||||||
|
|
||||||
if status_data["status"] == "idle" and status_data["pending_count"] == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
await anyio.sleep(wait_interval)
|
|
||||||
waited += wait_interval
|
|
||||||
|
|
||||||
assert waited < max_wait, f"Vector sync did not complete within {max_wait} seconds"
|
|
||||||
|
|
||||||
call_result = await nc_mcp_client.call_tool(
|
call_result = await nc_mcp_client.call_tool(
|
||||||
"nc_semantic_search_answer",
|
"nc_semantic_search_answer",
|
||||||
@@ -305,24 +309,7 @@ async def test_semantic_search_answer_score_threshold(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Wait for vector indexing to complete
|
# Wait for vector indexing to complete
|
||||||
|
await wait_for_vector_sync(nc_mcp_client)
|
||||||
max_wait = 30
|
|
||||||
wait_interval = 1
|
|
||||||
waited = 0
|
|
||||||
|
|
||||||
while waited < max_wait:
|
|
||||||
sync_status = await nc_mcp_client.call_tool(
|
|
||||||
"nc_get_vector_sync_status", arguments={}
|
|
||||||
)
|
|
||||||
status_data = json.loads(sync_status.content[0].text)
|
|
||||||
|
|
||||||
if status_data["status"] == "idle" and status_data["pending_count"] == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
await anyio.sleep(wait_interval)
|
|
||||||
waited += wait_interval
|
|
||||||
|
|
||||||
assert waited < max_wait, f"Vector sync did not complete within {max_wait} seconds"
|
|
||||||
|
|
||||||
# Query with exact match
|
# Query with exact match
|
||||||
call_result = await nc_mcp_client.call_tool(
|
call_result = await nc_mcp_client.call_tool(
|
||||||
@@ -369,24 +356,7 @@ async def test_semantic_search_answer_max_tokens(nc_mcp_client, temporary_note_f
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Wait for vector indexing to complete
|
# Wait for vector indexing to complete
|
||||||
|
await wait_for_vector_sync(nc_mcp_client)
|
||||||
max_wait = 30
|
|
||||||
wait_interval = 1
|
|
||||||
waited = 0
|
|
||||||
|
|
||||||
while waited < max_wait:
|
|
||||||
sync_status = await nc_mcp_client.call_tool(
|
|
||||||
"nc_get_vector_sync_status", arguments={}
|
|
||||||
)
|
|
||||||
status_data = json.loads(sync_status.content[0].text)
|
|
||||||
|
|
||||||
if status_data["status"] == "idle" and status_data["pending_count"] == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
await anyio.sleep(wait_interval)
|
|
||||||
waited += wait_interval
|
|
||||||
|
|
||||||
assert waited < max_wait, f"Vector sync did not complete within {max_wait} seconds"
|
|
||||||
|
|
||||||
call_result = await nc_mcp_client.call_tool(
|
call_result = await nc_mcp_client.call_tool(
|
||||||
"nc_semantic_search_answer",
|
"nc_semantic_search_answer",
|
||||||
|
|||||||
Vendored
+1
-1
Submodule third_party/astrolabe updated: af53f1c02f...d245ded7f8
Reference in New Issue
Block a user