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
6 changes: 6 additions & 0 deletions index.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
218 changes: 218 additions & 0 deletions notebooks/bilig_workpaper_formula_readback.ipynb
Original file line number Diff line number Diff line change
@@ -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
}