diff --git a/Guardrails/bilig_workpaper_formula_readback.ipynb b/Guardrails/bilig_workpaper_formula_readback.ipynb new file mode 100644 index 0000000..3f3885c --- /dev/null +++ b/Guardrails/bilig_workpaper_formula_readback.ipynb @@ -0,0 +1,225 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Bilig WorkPaper Formula Readback With CrewAI\n", + "\n", + "This quickstart shows a CrewAI tool pattern for spreadsheet-backed business logic. The tool writes one input cell in a Bilig WorkPaper forecast model, recalculates dependent formulas, verifies readback, and returns a compact proof object.\n", + "\n", + "Use this when an agent needs spreadsheet logic without driving Excel, Google Sheets, or a browser." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Install Dependencies\n", + "\n", + "The deterministic smoke test only calls the Bilig API. The CrewAI agent run also needs an LLM key such as `OPENAI_API_KEY`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%pip install -U -q crewai pydantic" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define The Bilig WorkPaper Tool\n", + "\n", + "The tool fails closed: if Bilig does not return `verified: true`, the call raises instead of letting the agent continue with stale or guessed 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 crewai import Agent, Crew, Process, Task\n", + "from crewai.tools import BaseTool\n", + "from pydantic import BaseModel, Field\n", + "\n", + "\n", + "DEFAULT_BASE_URL = \"https://bilig.proompteng.ai\"\n", + "DEFAULT_SHEET_NAME = \"Inputs\"\n", + "DEFAULT_ADDRESS = \"B3\"\n", + "DEFAULT_VALUE = 0.4\n", + "\n", + "\n", + "class ForecastFormulaInput(BaseModel):\n", + " sheet_name: str = Field(default=DEFAULT_SHEET_NAME, description=\"Worksheet containing the editable forecast input.\")\n", + " address: str = Field(default=DEFAULT_ADDRESS, description=\"A1-style input cell address to edit before recalculation.\")\n", + " value: float = Field(default=DEFAULT_VALUE, description=\"Numeric value to write before formula readback.\")\n", + "\n", + "\n", + "class BiligWorkPaperForecastTool(BaseTool):\n", + " name: str = \"Bilig WorkPaper forecast formula readback\"\n", + " description: str = (\n", + " \"Edit one forecast workbook input, recalculate formulas, and return a \"\n", + " \"verified before/after readback proof from Bilig WorkPaper.\"\n", + " )\n", + " args_schema: type[BaseModel] = ForecastFormulaInput\n", + " base_url: str = Field(default=DEFAULT_BASE_URL)\n", + " timeout: int = Field(default=30)\n", + "\n", + " def _run(self, sheet_name: str = DEFAULT_SHEET_NAME, address: str = DEFAULT_ADDRESS, value: float = DEFAULT_VALUE) -> str:\n", + " proof = call_bilig_forecast(\n", + " base_url=self.base_url,\n", + " sheet_name=sheet_name,\n", + " address=address,\n", + " value=value,\n", + " timeout=self.timeout,\n", + " )\n", + " return json.dumps(compact_proof(proof), indent=2)\n", + "\n", + "\n", + "def call_bilig_forecast(*, base_url: str, sheet_name: str, address: str, value: float, timeout: int) -> dict[str, Any]:\n", + " if not base_url.startswith((\"http://\", \"https://\")):\n", + " raise ValueError(\"BILIG_BASE_URL must start with http:// or https://\")\n", + "\n", + " endpoint = urljoin(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\": \"CrewAI-Bilig-WorkPaper-Quickstart/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 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": [ + "## Run A Deterministic Tool Smoke Test\n", + "\n", + "This cell validates the workbook API and proof object without using an LLM." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "base_url = os.getenv(\"BILIG_BASE_URL\", DEFAULT_BASE_URL)\n", + "tool = BiligWorkPaperForecastTool(base_url=base_url)\n", + "\n", + "print(tool._run(sheet_name=\"Inputs\", address=\"B3\", value=0.4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Let A CrewAI Agent Use The Tool\n", + "\n", + "Run this section after setting your model provider key, for example `OPENAI_API_KEY`. The agent is instructed to answer only from computed readback returned by the tool." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "verifier = Agent(\n", + " role=\"Workbook formula verifier\",\n", + " goal=\"Use verified workbook readback to explain whether the edited forecast input changed the computed ARR output.\",\n", + " backstory=(\n", + " \"You check spreadsheet-backed business logic through API tools. \"\n", + " \"You do not infer formula results; you rely on computed readback.\"\n", + " ),\n", + " tools=[tool],\n", + " verbose=True,\n", + ")\n", + "\n", + "task = Task(\n", + " description=(\n", + " \"Set the forecast input cell to 0.4 with the Bilig WorkPaper tool. \"\n", + " \"Report the edited cell, before and after expected ARR, target gap, \"\n", + " \"and whether formulas persisted after restore.\"\n", + " ),\n", + " expected_output=\"A concise verification summary grounded only in the Bilig WorkPaper proof object returned by the tool.\",\n", + " agent=verifier,\n", + ")\n", + "\n", + "crew = Crew(agents=[verifier], tasks=[task], process=Process.sequential, verbose=True)\n", + "result = crew.kickoff()\n", + "print(result)" + ] + } + ], + "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 +} diff --git a/README.md b/README.md index 64b1176..37a35a8 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,10 @@ Each quickstart is self-contained and can be run independently: 3. Follow the instructions in the example 4. Experiment and modify the code to learn! +Featured guardrails quickstart: + +- [Bilig WorkPaper Formula Readback](Guardrails/bilig_workpaper_formula_readback.ipynb) - verify spreadsheet formula outputs through a CrewAI tool without Excel or browser automation. + --- ## Support @@ -73,4 +77,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file **Happy Learning! 🎉** -*Start your CrewAI journey today and build amazing multi-agent systems!* \ No newline at end of file +*Start your CrewAI journey today and build amazing multi-agent systems!*