diff --git a/README.md b/README.md index e16c212..08f87fc 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Reference implementations provided in this repository demonstrate common use-cas - [**Get User Info Server**](servers/get-user-info) - Access and return enriched user profile information from authentication providers or internal systems. - [**SQL Chat Server**](servers/sql) - Connect to SQL databases and automatically generate, execute, and optimize queries based on your database schema and natural language input. Enables chat-based data exploration, leveraging external Retrieval-Augmented Generation (RAG) for advanced query assistance. - [**External RAG Tool Server**](servers/external-rag) - Connect and execute your own Retrieval-Augmented Generation (RAG) pipelines as callable API tools. Easily integrate custom or third-party RAG flows, providing structured access and modular composition for knowledge-intensive applications. +- [**Bilig WorkPaper Formula Readback Server**](servers/bilig-workpaper) - Write spreadsheet model inputs, recalculate formulas, and return verified before/after readback proof without Excel or browser automation. (More examples and reference implementations will be actively developed and continually updated.) diff --git a/compose.yaml b/compose.yaml index 0d106e9..d0b5421 100644 --- a/compose.yaml +++ b/compose.yaml @@ -16,6 +16,13 @@ services: context: ./servers/time ports: - 8083:8000 + bilig-workpaper-server: + build: + context: ./servers/bilig-workpaper + ports: + - 8084:8000 + environment: + BILIG_BASE_URL: ${BILIG_BASE_URL:-https://bilig.proompteng.ai} volumes: memory: diff --git a/servers/bilig-workpaper/Dockerfile b/servers/bilig-workpaper/Dockerfile new file mode 100644 index 0000000..18ed63f --- /dev/null +++ b/servers/bilig-workpaper/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY main.py . + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/servers/bilig-workpaper/README.md b/servers/bilig-workpaper/README.md new file mode 100644 index 0000000..2e1ce5f --- /dev/null +++ b/servers/bilig-workpaper/README.md @@ -0,0 +1,85 @@ +# Bilig WorkPaper OpenAPI Tool Server + +A FastAPI tool server for spreadsheet-backed formula calculations through Bilig WorkPaper. + +Use it when an agent or OpenAPI-compatible client needs to write one workbook input, recalculate formulas, and verify computed readback without opening Excel, Google Sheets, or a browser. + +## Quickstart + +```bash +git clone https://github.com/open-webui/openapi-servers +cd openapi-servers/servers/bilig-workpaper + +pip install -r requirements.txt +uvicorn main:app --host 0.0.0.0 --reload +``` + +Open the docs: + +```text +http://localhost:8000/docs +``` + +## Docker + +```bash +docker compose up +``` + +## Configuration + +By default, this server calls the hosted Bilig demo: + +```text +https://bilig.proompteng.ai +``` + +Set `BILIG_BASE_URL` to point at a self-hosted Bilig app: + +```bash +export BILIG_BASE_URL=http://localhost:4321 +``` + +## Endpoint + +```text +POST /forecast/readback +``` + +Request: + +```json +{ + "sheetName": "Inputs", + "address": "B3", + "value": 0.4 +} +``` + +Response: + +```json +{ + "verified": true, + "editedCell": "Inputs!B3", + "before": { + "expectedArr": 60000, + "targetGap": -34000 + }, + "after": { + "expectedArr": 96000, + "targetGap": 5600 + }, + "checks": { + "formulasPersisted": true, + "restoredMatchesAfter": true, + "computedOutputChanged": true + }, + "source": "Bilig WorkPaper", + "github": "https://github.com/proompteng/bilig" +} +``` + +## Why This Exists + +Spreadsheet logic often drives pricing, forecasts, approvals, and payout rules. UI automation can click the wrong cell, and cached XLSX values can be stale. This server gives OpenAPI-compatible agents a narrow, typed API that returns proof that formulas recalculated after the edit. diff --git a/servers/bilig-workpaper/compose.yaml b/servers/bilig-workpaper/compose.yaml new file mode 100644 index 0000000..595e206 --- /dev/null +++ b/servers/bilig-workpaper/compose.yaml @@ -0,0 +1,7 @@ +services: + bilig-workpaper-server: + build: . + ports: + - "8084:8000" + environment: + BILIG_BASE_URL: ${BILIG_BASE_URL:-https://bilig.proompteng.ai} diff --git a/servers/bilig-workpaper/main.py b/servers/bilig-workpaper/main.py new file mode 100644 index 0000000..cdee0e8 --- /dev/null +++ b/servers/bilig-workpaper/main.py @@ -0,0 +1,162 @@ +import os +from typing import Any + +import requests +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field + + +DEFAULT_BILIG_BASE_URL = "https://bilig.proompteng.ai" + +app = FastAPI( + title="Bilig WorkPaper Formula Readback API", + version="0.1.0", + description=( + "OpenAPI tool server for writing one Bilig WorkPaper input cell, " + "recalculating formulas, and returning verified computed readback." + ), +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +class FormulaReadbackRequest(BaseModel): + sheetName: str = Field( + "Inputs", + description="Worksheet containing the editable forecast input.", + examples=["Inputs"], + ) + address: str = Field( + "B3", + description="A1-style input cell address to edit before recalculation.", + examples=["B3"], + ) + value: float = Field( + 0.4, + description="Numeric value to write before formula readback.", + examples=[0.4], + ) + + +class FormulaSnapshot(BaseModel): + expectedArr: float | None = Field(None, description="Computed expected ARR value.") + targetGap: float | None = Field(None, description="Computed gap to target value.") + + +class FormulaChecks(BaseModel): + formulasPersisted: bool = Field( + ..., description="Formula definitions persisted after serialization." + ) + restoredMatchesAfter: bool = Field( + ..., description="Restored workbook readback matched post-edit values." + ) + computedOutputChanged: bool = Field( + ..., description="Dependent formula output changed after the input edit." + ) + + +class FormulaReadbackResponse(BaseModel): + verified: bool = Field( + ..., description="True only when Bilig verified the formula readback proof." + ) + editedCell: str | None = Field( + None, description="Edited worksheet cell, for example Inputs!B3." + ) + before: FormulaSnapshot + after: FormulaSnapshot + checks: FormulaChecks + source: str = Field("Bilig WorkPaper", description="Readback source.") + github: str = Field( + "https://github.com/proompteng/bilig", description="Bilig project URL." + ) + + +def bilig_base_url() -> str: + base_url = os.getenv("BILIG_BASE_URL", DEFAULT_BILIG_BASE_URL).rstrip("/") + if not base_url.startswith(("http://", "https://")): + raise HTTPException( + status_code=500, detail="BILIG_BASE_URL must start with http:// or https://" + ) + return base_url + + +def compact_proof(proof: dict[str, Any]) -> FormulaReadbackResponse: + if proof.get("verified") is not True: + raise HTTPException( + status_code=502, + detail={"message": "Bilig returned an unverified response", "proof": proof}, + ) + + before = proof.get("before") if isinstance(proof.get("before"), dict) else {} + after = proof.get("after") if isinstance(proof.get("after"), dict) else {} + checks = proof.get("checks") if isinstance(proof.get("checks"), dict) else {} + + return FormulaReadbackResponse( + verified=True, + editedCell=proof.get("editedCell"), + before=FormulaSnapshot( + expectedArr=before.get("expectedArr"), + targetGap=before.get("targetGap"), + ), + after=FormulaSnapshot( + expectedArr=after.get("expectedArr"), + targetGap=after.get("targetGap"), + ), + checks=FormulaChecks( + formulasPersisted=checks.get("formulasPersisted") is True, + restoredMatchesAfter=checks.get("restoredMatchesAfter") is True, + computedOutputChanged=checks.get("computedOutputChanged") is True, + ), + ) + + +@app.get("/health", summary="Health check") +def health() -> dict[str, str]: + return {"status": "ok"} + + +@app.post( + "/forecast/readback", + response_model=FormulaReadbackResponse, + summary="Write one forecast input and return verified formula readback", +) +def forecast_readback(request: FormulaReadbackRequest) -> FormulaReadbackResponse: + try: + response = requests.post( + f"{bilig_base_url()}/api/workpaper/n8n/forecast", + json={ + "sheetName": request.sheetName, + "address": request.address.upper(), + "value": request.value, + }, + headers={ + "Accept": "application/json", + "User-Agent": "OpenWebUI-Bilig-WorkPaper-Server/0.1", + }, + timeout=30, + ) + response.raise_for_status() + proof = response.json() + except requests.exceptions.RequestException as error: + raise HTTPException( + status_code=503, detail=f"Error connecting to Bilig: {error}" + ) from error + except ValueError as error: + raise HTTPException( + status_code=502, detail=f"Bilig returned invalid JSON: {error}" + ) from error + + if not isinstance(proof, dict): + raise HTTPException( + status_code=502, + detail=f"Expected JSON object from Bilig, received {type(proof).__name__}", + ) + + return compact_proof(proof) diff --git a/servers/bilig-workpaper/requirements.txt b/servers/bilig-workpaper/requirements.txt new file mode 100644 index 0000000..e1de28b --- /dev/null +++ b/servers/bilig-workpaper/requirements.txt @@ -0,0 +1,4 @@ +fastapi +uvicorn[standard] +pydantic +requests