Workspace/Mini projects
Loading progress
All mini projects
MODULE 06 · 7 HOUR BUILD

Catalog agent workbench

Build a small local interface for a bounded catalog assistant, with a shared controller usable from a CLI and HTTP API and a visible evidence-backed run result.

Build evidence Record your actual checks, results, and limitations.

Build it in stages

  1. Run the seed in CLI mode, then start its optional localhost form.
  2. Replace the direct lookup with an injected policy and controller using the lab lifecycle.
  3. Add a run store, stable IDs, and explicit progress and cancellation semantics.
  4. Render final results and partial status safely, with correlated tool evidence.
  5. Add request ownership and authentication before any deployment.
  6. Evaluate a fixed workflow and a model-driven policy on the same fixture set, recording success, calls, latency, and failures.

Your acceptance criteria

Use these as your project review. Record commands, outputs, and failure cases in your repository.

  • A7 returns found=true and available=8; B2 returns found=true and available=0; an unknown SKU returns found=false and available=null.
  • Malformed input receives a clear client error and cannot trigger a lookup.
  • The UI distinguishes missing items, zero stock, partial runs, and completed results.
  • Provider credentials never reach browser code or API responses.
  • A controller budget and completion check are enforced on the backend.
  • CLI and HTTP paths invoke the same core behavior and have reproducible fixtures.

A working starting point

The seed runs as supplied. Extend it to satisfy the full brief. It is a teaching starting point, not a finished portfolio submission.

main.py
python
import json
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer

STOCK = {"A7":8, "B2":0}
PAGE = """<!doctype html><meta charset='utf-8'><title>Catalog workbench seed</title>
<h1>Catalog lookup</h1><form id='form'><label>SKU <input id='sku' value='A7'></label>
<button>Run lookup</button></form><pre id='result'>Ready</pre>
<script>
const form = document.getElementById('form');
const result = document.getElementById('result');
form.addEventListener('submit', async (event) => {
  event.preventDefault(); result.textContent = 'Running';
  try {
    const response = await fetch('/runs', {method:'POST', headers:{'Content-Type':'application/json'},
      body:JSON.stringify({sku:document.getElementById('sku').value})});
    const data = await response.json(); result.textContent = JSON.stringify(data, null, 2);
  } catch (error) { result.textContent = 'Request failed'; }
});
</script>"""

def run(payload):
    if not isinstance(payload,dict) or set(payload) != {"sku"}:
        raise ValueError("expected exactly one sku field")
    if not isinstance(payload["sku"],str) or not payload["sku"].strip() or len(payload["sku"]) > 40:
        raise ValueError("sku must be nonempty text of at most 40 characters")
    sku = payload["sku"].strip()
    observation = {"sku":sku, "found":sku in STOCK, "available":STOCK.get(sku)}
    return {"status":"completed", "result":observation,
            "evidence":{"call_id":"local-lookup-1", "tool":"get_stock"}, "calls":1}

class Handler(BaseHTTPRequestHandler):
    def send(self, status, body, content_type):
        raw = body.encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", content_type)
        self.send_header("Content-Length", str(len(raw)))
        self.end_headers()
        self.wfile.write(raw)
    def do_GET(self):
        self.send(200, PAGE, "text/html; charset=utf-8") if self.path == "/" else self.send(404,"Not found","text/plain")
    def do_POST(self):
        if self.path != "/runs":
            return self.send(404,"Not found","text/plain")
        try:
            size = int(self.headers.get("Content-Length", "0"))
            if not 0 < size <= 4096 or self.headers.get("Content-Type", "").split(";")[0] != "application/json":
                raise ValueError("small JSON body required")
            result = run(json.loads(self.rfile.read(size)))
            self.send(200,json.dumps(result),"application/json")
        except (ValueError, UnicodeError):
            self.send(400,json.dumps({"status":"failed","error":"invalid_request"}),"application/json")

if __name__ == "__main__":
    if "--serve" in sys.argv:
        print("Local teaching demo: http://127.0.0.1:8765")
        HTTPServer(("127.0.0.1",8765),Handler).serve_forever()
    else:
        for sku in ("A7","B2","missing"):
            print(json.dumps(run({"sku":sku}),sort_keys=True))

Push it further

Add a second read-only tool for supplier lead times and evaluate whether model-selected tool use improves ambiguous requests relative to a deterministic router, including unnecessary-call rate.