glam/src/glam_extractor/api/sparql_lsp_server.py

303 lines
7.9 KiB
Python

"""
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)