From 6efa88eaba1dfb6a7283c7d5f1665caa8c053a73 Mon Sep 17 00:00:00 2001 From: Greg Konush <12027037+gregkonush@users.noreply.github.com> Date: Sat, 23 May 2026 11:10:46 -0700 Subject: [PATCH] add Bilig WorkPaper formula readback cookbook --- index.toml | 6 + .../bilig_workpaper_formula_readback.ipynb | 218 ++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 notebooks/bilig_workpaper_formula_readback.ipynb diff --git a/index.toml b/index.toml index 7eeaf19..8a0ddfe 100644 --- a/index.toml +++ b/index.toml @@ -221,6 +221,12 @@ title = "Define & Run Tools " notebook = "tools_support.ipynb" topics = ["Function Calling", "Chat", "Agents"] +[[cookbook]] +title = "Bilig WorkPaper Formula Readback with Haystack" +notebook = "bilig_workpaper_formula_readback.ipynb" +topics = ["Function Calling", "Agents", "Customization"] +new = true + [[cookbook]] title = "Agentic RAG with Llama 3.2 3B" notebook = "llama32_agentic_rag.ipynb" diff --git a/notebooks/bilig_workpaper_formula_readback.ipynb b/notebooks/bilig_workpaper_formula_readback.ipynb new file mode 100644 index 0000000..5225d96 --- /dev/null +++ b/notebooks/bilig_workpaper_formula_readback.ipynb @@ -0,0 +1,218 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Bilig WorkPaper Formula Readback With Haystack\n", + "\n", + "This cookbook shows how to expose spreadsheet-backed business logic to Haystack without opening Excel, Google Sheets, or a browser. The example writes one input cell in a Bilig WorkPaper forecast model, recalculates dependent formulas, verifies readback, and returns a compact proof object.\n", + "\n", + "The first path uses a Haystack `Tool`. The second path wraps the same operation as a reusable Haystack pipeline component." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Install Dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "! pip install -q -U haystack-ai" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define The WorkPaper Call\n", + "\n", + "The helper below calls the public Bilig demo API. If the response is not verified, it raises instead of letting an agent or pipeline continue with stale formula values." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import annotations\n", + "\n", + "import json\n", + "import os\n", + "from typing import Any\n", + "from urllib.parse import urljoin\n", + "from urllib.request import Request, urlopen\n", + "\n", + "from haystack import Pipeline, component\n", + "from haystack.tools import Tool\n", + "\n", + "\n", + "DEFAULT_BASE_URL = \"https://bilig.proompteng.ai\"\n", + "\n", + "\n", + "def call_bilig_forecast(\n", + " sheet_name: str = \"Inputs\",\n", + " address: str = \"B3\",\n", + " value: float = 0.4,\n", + " base_url: str | None = None,\n", + " timeout: int = 30,\n", + ") -> dict[str, Any]:\n", + " resolved_base_url = base_url or os.getenv(\"BILIG_BASE_URL\", DEFAULT_BASE_URL)\n", + " if not resolved_base_url.startswith((\"http://\", \"https://\")):\n", + " raise ValueError(\"BILIG_BASE_URL must start with http:// or https://\")\n", + "\n", + " endpoint = urljoin(resolved_base_url.rstrip(\"/\") + \"/\", \"api/workpaper/n8n/forecast\")\n", + " body = json.dumps({\"sheetName\": sheet_name, \"address\": address.upper(), \"value\": value}).encode(\"utf-8\")\n", + " request = Request(\n", + " endpoint,\n", + " data=body,\n", + " headers={\n", + " \"accept\": \"application/json\",\n", + " \"content-type\": \"application/json\",\n", + " \"user-agent\": \"Haystack-Bilig-WorkPaper-Cookbook/0.1\",\n", + " },\n", + " method=\"POST\",\n", + " )\n", + "\n", + " with urlopen(request, timeout=timeout) as response:\n", + " parsed = json.loads(response.read().decode(\"utf-8\"))\n", + "\n", + " if not isinstance(parsed, dict):\n", + " raise ValueError(f\"Expected JSON object response, received {type(parsed).__name__}\")\n", + " if parsed.get(\"verified\") is not True:\n", + " raise ValueError(f\"Bilig returned an unverified WorkPaper response: {parsed}\")\n", + "\n", + " return compact_proof(parsed)\n", + "\n", + "\n", + "def compact_proof(proof: dict[str, Any]) -> dict[str, Any]:\n", + " before = proof.get(\"before\") if isinstance(proof.get(\"before\"), dict) else {}\n", + " after = proof.get(\"after\") if isinstance(proof.get(\"after\"), dict) else {}\n", + " checks = proof.get(\"checks\") if isinstance(proof.get(\"checks\"), dict) else {}\n", + "\n", + " return {\n", + " \"verified\": proof.get(\"verified\") is True,\n", + " \"editedCell\": proof.get(\"editedCell\"),\n", + " \"before\": {\"expectedArr\": before.get(\"expectedArr\"), \"targetGap\": before.get(\"targetGap\")},\n", + " \"after\": {\"expectedArr\": after.get(\"expectedArr\"), \"targetGap\": after.get(\"targetGap\")},\n", + " \"checks\": {\n", + " \"formulasPersisted\": checks.get(\"formulasPersisted\") is True,\n", + " \"restoredMatchesAfter\": checks.get(\"restoredMatchesAfter\") is True,\n", + " \"computedOutputChanged\": checks.get(\"computedOutputChanged\") is True,\n", + " },\n", + " \"source\": \"Bilig WorkPaper\",\n", + " \"github\": \"https://github.com/proompteng/bilig\",\n", + " }" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use It As A Haystack Tool\n", + "\n", + "This tool can be passed to Haystack agent workflows that support tool/function calling. The direct `invoke()` call below is deterministic and does not require an LLM key." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bilig_workpaper_tool = Tool(\n", + " name=\"bilig_workpaper_formula_readback\",\n", + " description=(\n", + " \"Edit one Bilig WorkPaper forecast input, recalculate formulas, \"\n", + " \"and return a verified before/after readback proof.\"\n", + " ),\n", + " parameters={\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"sheet_name\": {\"type\": \"string\", \"default\": \"Inputs\"},\n", + " \"address\": {\"type\": \"string\", \"default\": \"B3\"},\n", + " \"value\": {\"type\": \"number\", \"default\": 0.4},\n", + " },\n", + " \"required\": [\"sheet_name\", \"address\", \"value\"],\n", + " },\n", + " function=call_bilig_forecast,\n", + ")\n", + "\n", + "proof = bilig_workpaper_tool.invoke(sheet_name=\"Inputs\", address=\"B3\", value=0.4)\n", + "print(json.dumps(proof, indent=2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use It As A Pipeline Component\n", + "\n", + "The same verified operation can also run inside a Haystack pipeline when the workbook calculation is part of a larger deterministic workflow." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "@component\n", + "class BiligWorkPaperFormulaReadback:\n", + " @component.output_types(proof=dict)\n", + " def run(self, sheet_name: str = \"Inputs\", address: str = \"B3\", value: float = 0.4) -> dict[str, dict[str, Any]]:\n", + " return {\"proof\": call_bilig_forecast(sheet_name=sheet_name, address=address, value=value)}\n", + "\n", + "\n", + "pipeline = Pipeline()\n", + "pipeline.add_component(\"workpaper\", BiligWorkPaperFormulaReadback())\n", + "\n", + "result = pipeline.run({\"workpaper\": {\"sheet_name\": \"Inputs\", \"address\": \"B3\", \"value\": 0.4}})\n", + "result[\"workpaper\"][\"proof\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Self-Hosted Bilig\n", + "\n", + "Set `BILIG_BASE_URL` if you run Bilig yourself:\n", + "\n", + "```bash\n", + "export BILIG_BASE_URL=http://localhost:4321\n", + "```\n", + "\n", + "The cookbook uses `POST /api/workpaper/n8n/forecast` with `sheetName`, `address`, and `value` in the JSON payload." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}