Initial DocsMCP stack
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
"""WebUI FastAPI application."""
|
||||
import html
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import FastAPI, File, Form, Request, UploadFile
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from .api_client import DocsAPIClient
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="Context7 Docs WebUI",
|
||||
description="Web dashboard for managing documentation system",
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
templates = Jinja2Templates(directory=os.path.join(os.path.dirname(__file__), "templates"))
|
||||
templates.env.globals["escapeHtml"] = lambda value: html.escape(str(value or ""))
|
||||
app.mount("/static", StaticFiles(directory=os.path.join(os.path.dirname(__file__), "static")), name="static")
|
||||
|
||||
_client: Optional[DocsAPIClient] = None
|
||||
|
||||
|
||||
def get_client() -> DocsAPIClient:
|
||||
global _client
|
||||
if _client is None:
|
||||
_client = DocsAPIClient(
|
||||
os.environ.get("DOCS_API_URL", "http://docs-api:8787"),
|
||||
os.environ.get("WEBUI_API_KEY"),
|
||||
)
|
||||
return _client
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown() -> None:
|
||||
if _client is not None:
|
||||
await _client.close()
|
||||
|
||||
|
||||
def page(title: str, body: str) -> HTMLResponse:
|
||||
return HTMLResponse(
|
||||
f"""<!DOCTYPE html>
|
||||
<html><head><meta charset="UTF-8"><title>{html.escape(title)}</title></head>
|
||||
<body style="font-family:sans-serif;padding:20px;">{body}</body></html>"""
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def dashboard(request: Request):
|
||||
client = get_client()
|
||||
health = await client.health()
|
||||
|
||||
try:
|
||||
collections_data = await client.get("/collections")
|
||||
total_vectors = sum(
|
||||
item.get("vectors", 0)
|
||||
for item in collections_data.get("collections", {}).values()
|
||||
if isinstance(item, dict)
|
||||
)
|
||||
except Exception:
|
||||
total_vectors = 0
|
||||
|
||||
try:
|
||||
libs_data = await client.get("/libraries")
|
||||
libraries = libs_data.get("libraries", [])
|
||||
except Exception:
|
||||
libraries = []
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"dashboard.html",
|
||||
{"request": request, "health": health, "vectors": total_vectors, "libraries": libraries},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/actions/ingest-all")
|
||||
async def ingest_all():
|
||||
client = get_client()
|
||||
try:
|
||||
result = await client.post("/ingest/all")
|
||||
body = f"<h1>Ingestion Complete</h1><pre>{html.escape(str(result))}</pre><a href='/'>Back</a>"
|
||||
except Exception as e:
|
||||
body = f"<h1>Ingestion Failed</h1><pre>{html.escape(str(e))}</pre><a href='/'>Back</a>"
|
||||
return page("Ingestion", body)
|
||||
|
||||
|
||||
@app.post("/actions/sync-sources")
|
||||
async def sync_sources_action():
|
||||
client = get_client()
|
||||
try:
|
||||
result = await client.post("/sources/sync", json={"override": False})
|
||||
body = f"<h1>Git Sync Complete</h1><pre>{html.escape(str(result))}</pre><a href='/'>Back</a>"
|
||||
except Exception as e:
|
||||
body = f"<h1>Git Sync Failed</h1><pre>{html.escape(str(e))}</pre><a href='/'>Back</a>"
|
||||
return page("Git Sync", body)
|
||||
|
||||
|
||||
@app.get("/libraries")
|
||||
async def libraries(request: Request):
|
||||
client = get_client()
|
||||
try:
|
||||
data = await client.get("/libraries")
|
||||
libraries_data = data.get("libraries", [])
|
||||
except Exception:
|
||||
libraries_data = []
|
||||
return templates.TemplateResponse("libraries.html", {"request": request, "data": libraries_data})
|
||||
|
||||
|
||||
@app.post("/libraries/create")
|
||||
async def create_library(
|
||||
library_id: str = Form(...),
|
||||
name: str = Form(...),
|
||||
description: Optional[str] = Form(None),
|
||||
):
|
||||
client = get_client()
|
||||
try:
|
||||
result = await client.post(
|
||||
f"/api/v1/libraries/{library_id.strip()}",
|
||||
data={"name": name, "description": description or ""},
|
||||
)
|
||||
body = f"<h1>Library Created</h1><pre>{html.escape(str(result))}</pre><a href='/libraries'>Back</a>"
|
||||
except Exception as e:
|
||||
body = f"<h1>Create Failed</h1><pre>{html.escape(str(e))}</pre><a href='/libraries'>Back</a>"
|
||||
return page("Library Created", body)
|
||||
|
||||
|
||||
@app.post("/libraries/{library_id}/ingest")
|
||||
async def ingest_library(library_id: str):
|
||||
client = get_client()
|
||||
try:
|
||||
result = await client.post(f"/ingest/{library_id}")
|
||||
body = f"<h1>Ingestion Complete</h1><pre>{html.escape(str(result))}</pre><a href='/libraries'>Back</a>"
|
||||
except Exception as e:
|
||||
body = f"<h1>Ingestion Failed</h1><pre>{html.escape(str(e))}</pre><a href='/libraries'>Back</a>"
|
||||
return page("Ingest Library", body)
|
||||
|
||||
|
||||
@app.post("/libraries/{library_id}/delete")
|
||||
async def delete_library(library_id: str):
|
||||
client = get_client()
|
||||
try:
|
||||
result = await client.delete(f"/api/v1/libraries/{library_id}")
|
||||
body = f"<h1>Library Deleted</h1><pre>{html.escape(str(result))}</pre><a href='/libraries'>Back</a>"
|
||||
except Exception as e:
|
||||
body = f"<h1>Delete Failed</h1><pre>{html.escape(str(e))}</pre><a href='/libraries'>Back</a>"
|
||||
return page("Delete Library", body)
|
||||
|
||||
|
||||
@app.get("/libraries/{library_id}/docs")
|
||||
async def view_library_docs(library_id: str):
|
||||
client = get_client()
|
||||
try:
|
||||
result = await client.get(f"/docs/{library_id}")
|
||||
content = result.get("content", "")
|
||||
except Exception as e:
|
||||
content = str(e)
|
||||
return page(
|
||||
f"Docs: {library_id}",
|
||||
f"<h1>{html.escape(library_id)}</h1><pre>{html.escape(content)}</pre><a href='/libraries'>Back</a>",
|
||||
)
|
||||
|
||||
|
||||
@app.get("/upload")
|
||||
async def upload_form(request: Request):
|
||||
client = get_client()
|
||||
try:
|
||||
libs_data = await client.get("/libraries")
|
||||
libraries = libs_data.get("libraries", [])
|
||||
except Exception:
|
||||
libraries = []
|
||||
return templates.TemplateResponse("upload.html", {"request": request, "libraries": libraries})
|
||||
|
||||
|
||||
@app.post("/upload")
|
||||
async def upload_file(
|
||||
request: Request,
|
||||
library_id: str = Form(""),
|
||||
ingest_after_upload: Optional[str] = Form(None),
|
||||
files: List[UploadFile] = File(...),
|
||||
):
|
||||
client = get_client()
|
||||
results = []
|
||||
total_size = 0
|
||||
|
||||
for upload in files:
|
||||
filename = upload.filename or "upload.txt"
|
||||
target_library = library_id.strip()
|
||||
if not target_library:
|
||||
target_library = Path(filename).stem.lower().replace(" ", "-") or "uploaded"
|
||||
|
||||
try:
|
||||
contents = await upload.read()
|
||||
total_size += len(contents)
|
||||
result = await client.upload_file(target_library, filename, contents)
|
||||
results.append({"filename": filename, "status": "success", "message": result})
|
||||
except Exception as e:
|
||||
results.append({"filename": filename, "status": "error", "message": str(e)})
|
||||
|
||||
if ingest_after_upload == "on":
|
||||
for result in list(results):
|
||||
if result["status"] != "success":
|
||||
continue
|
||||
target_library = result["message"]["library_id"]
|
||||
try:
|
||||
ingest_result = await client.post(f"/ingest/{target_library}")
|
||||
results.append({"filename": "__INGEST__", "status": "success", "message": ingest_result})
|
||||
except Exception as e:
|
||||
results.append({"filename": "__INGEST__", "status": "error", "message": str(e)})
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"upload.html",
|
||||
{"request": request, "libraries": [], "results": results, "total_size_bytes": total_size},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/search")
|
||||
async def search_form(request: Request):
|
||||
return templates.TemplateResponse("search.html", {"request": request, "query": "", "results": []})
|
||||
|
||||
|
||||
@app.get("/search/results")
|
||||
async def search_results(request: Request, q: str = "", limit: int = 10):
|
||||
client = get_client()
|
||||
results = []
|
||||
if q:
|
||||
try:
|
||||
data = await client.post("/search", json={"query": q, "library_id": None, "limit": limit})
|
||||
results = data.get("results", [])
|
||||
except Exception:
|
||||
results = []
|
||||
return templates.TemplateResponse(
|
||||
"search.html",
|
||||
{"request": request, "query": q, "results": results, "limit": limit},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/sources")
|
||||
async def sources_page(request: Request):
|
||||
client = get_client()
|
||||
try:
|
||||
data = await client.get("/api/v1/sources")
|
||||
sources = data.get("sources", [])
|
||||
except Exception:
|
||||
sources = []
|
||||
return templates.TemplateResponse("sources.html", {"request": request, "sources": sources})
|
||||
|
||||
|
||||
@app.post("/sources/sync")
|
||||
async def sync_sources(override: bool = Form(False)):
|
||||
client = get_client()
|
||||
try:
|
||||
result = await client.post("/sources/sync", json={"override": override})
|
||||
body = f"<h1>Git Sync Complete</h1><pre>{html.escape(str(result))}</pre><a href='/sources'>Back</a>"
|
||||
except Exception as e:
|
||||
body = f"<h1>Git Sync Failed</h1><pre>{html.escape(str(e))}</pre><a href='/sources'>Back</a>"
|
||||
return page("Git Sync", body)
|
||||
Reference in New Issue
Block a user