updated the van inventory to use the api

This commit is contained in:
2026-03-09 17:09:24 -04:00
parent a6fbbd245f
commit 6365dd8067
4 changed files with 100 additions and 124 deletions

View File

@@ -1,4 +1,4 @@
"""Van inventory command — parse receipts and item lists via LLM."""
"""Van inventory command — parse receipts and item lists via LLM, push to API."""
from __future__ import annotations
@@ -6,11 +6,11 @@ import json
import logging
from typing import TYPE_CHECKING, Any
import httpx
from python.signal_bot.models import InventoryItem, InventoryUpdate
if TYPE_CHECKING:
from pathlib import Path
from python.signal_bot.llm_client import LLMClient
from python.signal_bot.models import SignalMessage
from python.signal_bot.signal_client import SignalClient
@@ -21,12 +21,13 @@ SYSTEM_PROMPT = """\
You are an inventory assistant. Extract items from the input and return ONLY
a JSON array. Each element must have these fields:
- "name": item name (string)
- "quantity": integer count (default 1)
- "quantity": numeric count or amount (default 1)
- "unit": unit of measure (e.g. "each", "lb", "oz", "gallon", "bag", "box")
- "category": category like "food", "tools", "supplies", etc.
- "notes": any extra detail (empty string if none)
Example output:
[{"name": "water bottles", "quantity": 6, "category": "supplies", "notes": "1 gallon each"}]
[{"name": "water bottles", "quantity": 6, "unit": "gallon", "category": "supplies", "notes": "1 gallon each"}]
Return ONLY the JSON array, no other text.\
"""
@@ -48,31 +49,47 @@ def parse_llm_response(raw: str) -> list[InventoryItem]:
return [InventoryItem.model_validate(item) for item in items_data]
def load_inventory(path: Path) -> list[InventoryItem]:
"""Load existing inventory from disk."""
if not path.exists():
return []
data: list[dict[str, Any]] = json.loads(path.read_text())
return [InventoryItem.model_validate(item) for item in data]
def _upsert_item(api_url: str, item: InventoryItem) -> None:
"""Create or update an item via the van_inventory API.
Fetches existing items, and if one with the same name exists,
patches its quantity (summing). Otherwise creates a new item.
"""
base = api_url.rstrip("/")
response = httpx.get(f"{base}/api/items", timeout=10)
response.raise_for_status()
existing: list[dict[str, Any]] = response.json()
def save_inventory(path: Path, items: list[InventoryItem]) -> None:
"""Save inventory to disk."""
path.parent.mkdir(parents=True, exist_ok=True)
data = [item.model_dump() for item in items]
path.write_text(json.dumps(data, indent=2) + "\n")
match = next((e for e in existing if e["name"].lower() == item.name.lower()), None)
if match:
new_qty = match["quantity"] + item.quantity
patch = {"quantity": new_qty}
if item.category:
patch["category"] = item.category
response = httpx.patch(f"{base}/api/items/{match['id']}", json=patch, timeout=10)
response.raise_for_status()
return
payload = {
"name": item.name,
"quantity": item.quantity,
"unit": item.unit,
"category": item.category or None,
}
response = httpx.post(f"{base}/api/items", json=payload, timeout=10)
response.raise_for_status()
def handle_inventory_update(
message: SignalMessage,
signal: SignalClient,
llm: LLMClient,
inventory_path: Path,
api_url: str,
) -> InventoryUpdate:
"""Process an inventory update from a Signal message.
Accepts either an image (receipt photo) or text list.
Uses the LLM to extract structured items, then merges into inventory.
Uses the LLM to extract structured items, then pushes to the van_inventory API.
"""
try:
if message.attachments:
@@ -94,9 +111,9 @@ def handle_inventory_update(
return InventoryUpdate()
new_items = parse_llm_response(raw_response)
existing = load_inventory(inventory_path)
merged = _merge_items(existing, new_items)
save_inventory(inventory_path, merged)
for item in new_items:
_upsert_item(api_url, item)
summary = _format_summary(new_items)
signal.reply(message, f"Inventory updated with {len(new_items)} item(s):\n{summary}")
@@ -109,26 +126,7 @@ def handle_inventory_update(
return InventoryUpdate()
def _merge_items(existing: list[InventoryItem], new: list[InventoryItem]) -> list[InventoryItem]:
"""Merge new items into existing inventory, summing quantities for matches."""
by_name: dict[str, InventoryItem] = {item.name.lower(): item for item in existing}
for item in new:
key = item.name.lower()
if key in by_name:
current = by_name[key]
by_name[key] = current.model_copy(
update={
"quantity": current.quantity + item.quantity,
"category": item.category or current.category,
"notes": item.notes or current.notes,
},
)
else:
by_name[key] = item
return list(by_name.values())
def _format_summary(items: list[InventoryItem]) -> str:
"""Format items into a readable summary."""
lines = [f" - {item.name} x{item.quantity} [{item.category}]" for item in items]
lines = [f" - {item.name} x{item.quantity} {item.unit} [{item.category}]" for item in items]
return "\n".join(lines)