| """The narrow tool surface the LLM brain may call. |
| |
| The brain decides WHICH items to add and WHEN it's done; pricing and math stay |
| in the deterministic tools (Facts-from-Tools, ADR-0004). The brain never emits |
| numbers itself. |
| """ |
|
|
| from quillwright.catalog import Catalog |
| from quillwright.tools import compute, draft_line_item, lookup_price |
|
|
| |
| BRAIN_TOOLS = [ |
| { |
| "type": "function", |
| "function": { |
| "name": "add_priced_item", |
| "description": ( |
| "Add one line item to the estimate. Provide the item name and how many " |
| "units/hours (quantity); the catalog price is applied automatically. Call " |
| "once per distinct item observed or mentioned (parts and labor)." |
| ), |
| "parameters": { |
| "type": "object", |
| "properties": { |
| "item": {"type": "string", "description": "item or labor name"}, |
| "quantity": { |
| "type": "number", |
| "description": "how many units or hours (default 1)", |
| }, |
| }, |
| "required": ["item"], |
| }, |
| }, |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "finish", |
| "description": "Call when every observed/mentioned item has been added.", |
| "parameters": {"type": "object", "properties": {}}, |
| }, |
| }, |
| ] |
|
|
|
|
| def dispatch(name: str, args: dict, catalog: Catalog) -> dict: |
| """Execute a brain tool call. Returns a status dict the loop interprets.""" |
| if name == "finish": |
| return {"status": "done"} |
| if name == "add_priced_item": |
| item = (args or {}).get("item", "") |
| res = lookup_price(item, catalog) |
| if not res["found"]: |
| return {"status": "need_price", "item": item} |
| qty = _coerce_qty((args or {}).get("quantity")) |
| |
| compute(f"{qty} * {res['rate']}") |
| line = draft_line_item( |
| res["description"], qty=qty, unit=res["unit"], rate=res["rate"], source="catalog" |
| ) |
| return {"status": "added", "line_item": line} |
| return {"status": "error", "tool": name} |
|
|
|
|
| def _coerce_qty(value) -> float: |
| """Quantity from possibly-noisy model output; fall back to 1 on anything invalid.""" |
| try: |
| q = float(value) |
| return q if q > 0 else 1 |
| except (TypeError, ValueError): |
| return 1 |
|
|