303 lines
7.9 KiB
Python
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)
|