Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions functions/tools/bilig_workpaper_formula_readback/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Bilig WorkPaper Formula Readback

Use a Bilig WorkPaper endpoint from Open WebUI to write an input cell, recalculate formulas, and return verified JSON readback.

This is useful when an assistant needs spreadsheet-style business logic without opening Excel, driving a browser, or trusting stale cached formula values.

## What it does

- Sends `sheetName`, `address`, and `value` to a Bilig WorkPaper formula endpoint.
- Confirms the endpoint reports `verified: true`.
- Returns the edited cell, recalculated values, and persistence/readback fields as compact JSON.

## Default endpoint

The default valve points at the public Bilig forecast demo:

```text
https://bilig.proompteng.ai/api/workpaper/n8n/forecast
```

The default call writes `0.5` to `Inputs!B3`. The demo recalculates expected ARR from the workbook formulas.

## Install

1. In Open WebUI, go to `Workspace -> Tools`.
2. Create a new tool.
3. Paste `main.py`.
4. Save and enable it for a model that supports tool calls.

## Example prompt

```text
Set Inputs!B3 to 0.5 and show the verified formula readback.
```

The tool should return a JSON object with `verified: true`, the edited cell, and recalculated output fields.

## Configuration

Use valves to point the tool at your own Bilig endpoint:

- `base_url`: Bilig service origin.
- `formula_path`: endpoint path that accepts a formula-readback POST.
- `timeout_seconds`: request timeout.

The endpoint must accept JSON:

```json
{
"sheetName": "Inputs",
"address": "B3",
"value": 0.5
}
```

`value` can be passed as a native JSON value or as a string containing a JSON
literal. For example, `0.5`, `"0.5"`, `true`, and `"SKU-1"` are accepted.
147 changes: 147 additions & 0 deletions functions/tools/bilig_workpaper_formula_readback/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""
title: Bilig WorkPaper Formula Readback
author: Greg Konush
author_url: https://github.com/gregkonush
git_url: https://github.com/proompteng/bilig
description: Write an input cell to a Bilig WorkPaper endpoint, recalculate formulas, and return verified JSON readback.
required_open_webui_version: 0.4.0
version: 0.1.0
license: MIT
"""

import json
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.parse import urljoin
from urllib.request import Request, urlopen

from pydantic import BaseModel, Field


class Tools:
class Valves(BaseModel):
base_url: str = Field(
default="https://bilig.proompteng.ai",
description="Base URL for the Bilig WorkPaper service.",
)
formula_path: str = Field(
default="/api/workpaper/n8n/forecast",
description="Path for the formula readback endpoint.",
)
timeout_seconds: int = Field(
default=20,
ge=1,
le=120,
description="HTTP timeout for formula readback requests.",
)

def __init__(self):
self.valves = self.Valves()

async def verify_formula_readback(
self,
sheet_name: str = "Inputs",
address: str = "B3",
value: Any = "0.5",
) -> str:
"""
Write one input cell to a Bilig WorkPaper endpoint and return verified
formula readback.

:param sheet_name: Worksheet name, for example "Inputs".
:param address: A1-style cell address, for example "B3".
:param value: Cell value. JSON literals such as 0.5, true, or "SKU-1"
are accepted. Native JSON values from tool calls are also accepted.
"""

payload = {
"sheetName": sheet_name,
"address": address,
"value": self._parse_value(value),
}
endpoint = urljoin(
self.valves.base_url.rstrip("/") + "/",
self.valves.formula_path.lstrip("/"),
)

try:
result = self._post_json(endpoint, payload)
except HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
return json.dumps(
{
"ok": False,
"error": "bilig_http_error",
"status": exc.code,
"body": body[:1200],
},
indent=2,
sort_keys=True,
)
except URLError as exc:
return json.dumps(
{
"ok": False,
"error": "bilig_network_error",
"reason": str(exc.reason),
},
indent=2,
sort_keys=True,
)
except Exception as exc:
return json.dumps(
{
"ok": False,
"error": "bilig_unexpected_error",
"reason": str(exc),
},
indent=2,
sort_keys=True,
)

return json.dumps(self._summarize_result(result), indent=2, sort_keys=True)

def _post_json(self, endpoint: str, payload: dict[str, Any]) -> dict[str, Any]:
request = Request(
endpoint,
data=json.dumps(payload).encode("utf-8"),
headers={
"content-type": "application/json",
"accept": "application/json",
"user-agent": "open-webui-bilig-workpaper-tool/0.1.0",
},
method="POST",
)
with urlopen(request, timeout=self.valves.timeout_seconds) as response:
return json.loads(response.read().decode("utf-8"))

def _summarize_result(self, result: dict[str, Any]) -> dict[str, Any]:
after = result.get("after") if isinstance(result.get("after"), dict) else {}
before = result.get("before") if isinstance(result.get("before"), dict) else {}

return {
"ok": bool(result.get("verified")),
"verified": result.get("verified"),
"editedCell": result.get("editedCell"),
"persisted": result.get("persisted"),
"restoredReadbackMatches": result.get("restoredReadbackMatches"),
"before": {
"input": before.get("input"),
"expectedArr": before.get("expectedArr"),
"expectedCustomers": before.get("expectedCustomers"),
},
"after": {
"input": after.get("input"),
"expectedArr": after.get("expectedArr"),
"expectedCustomers": after.get("expectedCustomers"),
},
}

def _parse_value(self, raw_value: Any) -> Any:
if not isinstance(raw_value, str):
return raw_value

try:
return json.loads(raw_value)
except json.JSONDecodeError:
return raw_value