""" SPARQL-LSP HTTP Server FastAPI wrapper that exposes the SPARQL Language Server Protocol over HTTP/JSON-RPC for use by AI agents, IDEs, and web clients. Endpoints: POST /jsonrpc - JSON-RPC 2.0 endpoint for all LSP methods GET /health - Health check endpoint GET /capabilities - Server capabilities (convenience) POST /validate - Quick validation endpoint (convenience) POST /complete - Quick completion endpoint (convenience) POST /execute - Quick execute endpoint (convenience) Author: Heritage Custodian Ontology Project Date: 2025-12-27 """ import logging import os from typing import Any, Dict, List, Optional from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel try: from glam_extractor.api.sparql_lsp import SPARQLLanguageServer, create_lsp_request except ImportError: from sparql_lsp import SPARQLLanguageServer, create_lsp_request # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Get the root path from environment (for reverse proxy setups) ROOT_PATH = os.getenv("ROOT_PATH", "/api/lsp") # Create FastAPI app app = FastAPI( title="SPARQL-LSP Server", description="Language Server Protocol for SPARQL queries against Heritage Custodian ontology", version="1.0.0", docs_url="/docs", redoc_url="/redoc", openapi_url="/openapi.json", root_path=ROOT_PATH, ) # Add CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Create LSP server instance lsp_server = SPARQLLanguageServer( sparql_endpoint=os.getenv("SPARQL_ENDPOINT", "https://bronhouder.nl/sparql"), qdrant_host=os.getenv("QDRANT_HOST"), typedb_host=os.getenv("TYPEDB_HOST"), ) # Initialize the server lsp_server.handle_message({ "jsonrpc": "2.0", "id": 0, "method": "initialize", "params": {}, }) # ============================================================================= # Pydantic Models # ============================================================================= class JSONRPCRequest(BaseModel): """JSON-RPC 2.0 request.""" jsonrpc: str = "2.0" method: str params: Dict[str, Any] = {} id: Optional[int] = None class ValidationRequest(BaseModel): """Quick validation request.""" query: str class CompletionRequest(BaseModel): """Quick completion request.""" query: str line: int character: int class ExecuteRequest(BaseModel): """Quick execute request.""" query: str class ExplainRequest(BaseModel): """Quick explain request.""" query: str class SuggestRequest(BaseModel): """Quick suggest request.""" query: str context: str = "" # ============================================================================= # Endpoints # ============================================================================= @app.get("/health") async def health_check(): """Health check endpoint.""" return { "status": "healthy", "service": "sparql-lsp", "version": "1.0.0", "initialized": lsp_server.initialized, } @app.get("/capabilities") async def get_capabilities(): """Get server capabilities.""" response = lsp_server.handle_message({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}, }) if response: return response.get("result", {}) return {} @app.post("/jsonrpc") async def jsonrpc_endpoint(request: JSONRPCRequest): """ JSON-RPC 2.0 endpoint for all LSP methods. Supported methods: - initialize - textDocument/didOpen - textDocument/didChange - textDocument/didClose - textDocument/completion - textDocument/hover - textDocument/signatureHelp - sparql/validate - sparql/execute - sparql/explain - sparql/suggest """ message = { "jsonrpc": request.jsonrpc, "method": request.method, "params": request.params, } if request.id is not None: message["id"] = request.id response = lsp_server.handle_message(message) if response is None: # Notification - no response expected return {"status": "ok"} return response @app.post("/validate") async def validate_query(request: ValidationRequest): """ Quick validation endpoint. Validates a SPARQL query and returns diagnostics. """ response = lsp_server.handle_message({ "jsonrpc": "2.0", "id": 1, "method": "sparql/validate", "params": {"text": request.query}, }) if response and "result" in response: return response["result"] elif response and "error" in response: raise HTTPException(status_code=400, detail=response["error"]) return {"diagnostics": []} @app.post("/complete") async def get_completions(request: CompletionRequest): """ Quick completion endpoint. Returns completions for a position in a SPARQL query. """ # Open document doc_uri = "inmemory://query.sparql" lsp_server.handle_message({ "jsonrpc": "2.0", "method": "textDocument/didOpen", "params": { "textDocument": { "uri": doc_uri, "languageId": "sparql", "version": 1, "text": request.query, } } }) # Get completions response = lsp_server.handle_message({ "jsonrpc": "2.0", "id": 1, "method": "textDocument/completion", "params": { "textDocument": {"uri": doc_uri}, "position": {"line": request.line, "character": request.character}, }, }) if response and "result" in response: return response["result"] elif response and "error" in response: raise HTTPException(status_code=400, detail=response["error"]) return {"isIncomplete": False, "items": []} @app.post("/execute") async def execute_query(request: ExecuteRequest): """ Execute a SPARQL query against the endpoint. """ response = lsp_server.handle_message({ "jsonrpc": "2.0", "id": 1, "method": "sparql/execute", "params": {"query": request.query}, }) if response and "result" in response: return response["result"] elif response and "error" in response: raise HTTPException(status_code=400, detail=response["error"]) return {"success": False, "error": "Unknown error"} @app.post("/explain") async def explain_query(request: ExplainRequest): """ Explain what a SPARQL query does. """ response = lsp_server.handle_message({ "jsonrpc": "2.0", "id": 1, "method": "sparql/explain", "params": {"query": request.query}, }) if response and "result" in response: return response["result"] elif response and "error" in response: raise HTTPException(status_code=400, detail=response["error"]) return {"summary": "Unable to explain query", "steps": []} @app.post("/suggest") async def suggest_connections(request: SuggestRequest): """ Suggest novel connections from vector DB. """ response = lsp_server.handle_message({ "jsonrpc": "2.0", "id": 1, "method": "sparql/suggest", "params": {"query": request.query, "context": request.context}, }) if response and "result" in response: return response["result"] elif response and "error" in response: raise HTTPException(status_code=400, detail=response["error"]) return {"suggestions": []} # ============================================================================= # Main # ============================================================================= if __name__ == "__main__": import uvicorn uvicorn.run(app, host="127.0.0.1", port=8011)