Inspiration

Walk into any duka(Shop) in Kenya and ask how the shelves get restocked. The answer is almost always the same: the owner sends their wholesaler a WhatsApp text, makes a phone call, or snaps a photo of a handwritten list — "Rina 1L ctn 10, Jogoo 2kg bale 5, sukari…". On the other side, someone at the wholesaler retypes every order, checks stock by walking the store, and quotes prices from memory. Orders get lost, quantities get misread, and shortages are discovered only when the lorry is loaded.

We wanted to give every wholesaler an order desk that never sleeps: one that understands how shopkeepers actually write (English, Swahili and Sheng(Mixture of English and Swahili), often in the same sentence), knows the live catalogue and stock, and never makes a promise the business can't keep.

There was a second problem we kept running into. Kenya passes new laws, Finance Acts, levies, excise changes, standards and county by-laws all the time, and most business owners never hear about them until prices have already moved. Few people have the time to read a Finance Bill or a Gazette notice. When they do, the text is full of jargon such as "exempt supplies", "excisable value", "zero-rated" and "First Schedule to Cap 472". It says nothing about what the change means for their business. A shop owner should not need a tax lawyer to learn that a new sugar levy will raise the price of half the stock on their shelves. That became the Compliance Radar: it reads the notices, translates them into plain language, and shows each wholesaler exactly which of their products are affected, how much of their sales is at stake, and what to do about it.

What it does

  • Shop chat. A retailer opens a personal invite link (no account, no app) and types an order or sends a photo of a handwritten list. The agent:
    • finds the products, checks live stock, and suggests in-stock alternatives when something runs short;
    • replies with a priced quotation in the customer's own language;
    • creates an order only when the customer confirms with "sawa"(Okay), and the order still waits for the wholesaler's approval.
  • Wholesaler dashboard.
    • An Overview with sales charts and the orders waiting for approval.
    • Orders with one-click next steps and a full review: what the customer sent, and how the agent read it.
    • Products and stock with an audit trail, revocable chat links, and the live customer chats.
  • Compliance Radar. A second, proactive agent reads tax, levy and standards notices. It works out which of this wholesaler's products they touch and how much recent revenue is at stake, then turns each alert into costed next steps:
    • reprice;
    • stock up before the date;
    • draft a message to every affected customer in their language.

Nothing runs until the wholesaler clicks.

How we built it

Almost everything is written in Jac: the backend, both agents, the tests and the React front end. By non-blank lines of tracked source, the code is 97.4% Jac; the only other source is 231 lines of CSS for the theme.

  • The business is a graph. Nodes for the business, products, stock, customers, orders, quotations, conversations, notices and alerts, connected by typed edges, persisted by Jaseci.
  • Agents are walkers. CustomerAgent is spawned on a customer's Conversation node and walks back to the Business to gather context. ComplianceScout walks the business's notices and, for each one, matches products, computes the money at stake and plans actions.
  • by llm with tools. The order agent's decision step is a tool-using model call with typed output and sem instructions. Its tools are search_catalogue, check_stock, suggest_alternatives and recent_orders. Handwritten lists go through the same model's vision input.
  • Full-stack Jac. Every page is a Jac client component. Endpoints are def:pub (the four public chat endpoints) or def:protect (everything the wholesaler sees), using Jac's built-in auth.
  • Deterministic tests of a non-deterministic agent. MockLLM and MockToolCall script the model, so the agent's behaviour is tested without calling it. The suite has 130+ tests, run by CI on every pull request.

The model can read and suggest; only code sets prices, checks stock, builds quotations, creates orders, matches regulations to products and computes money.

For example, when a tax rate changes from a% to b%, the suggested selling price is a pure pass-through, rounded up to the nearest KSh 10:

$$p' = 10 \cdot \lceil \frac{p \cdot (100 + b)}{10 \cdot (100 + a)} \rceil$$

So a carton at KSh 2,800 under a change from 8% to 16% becomes KSh 3,010.

The stock-up suggestion before an effective date d days away uses u, the units sold in the last 30 days, capped at 30 days of cover:

$$q = \lceil \frac{u}{30} \cdot \min(d, 30) \rceil$$

An alert is high severity only if the rule is law and the affected products make up at least 10% of recent sales:

$$\text{share} = \frac{\text{sales of affected products, last 30 days}}{\text{all sales, last 30 days}} \geq 10\%$$

Challenges we ran into

  • Identity without accounts. Shopkeepers won't create accounts, but Jac isolates each user's graph by default.
    • We put the business graph on root.shared and identify customers only by a hashed, revocable invite token.
    • That means access control lives in our endpoints rather than in the framework. We wrote this trade-off down in docs/security.md and covered it with security tests, for example: a chat token never works as a wholesaler login.
  • The model being too helpful. In early real-model runs the agent asked about shortages instead of quoting, and once told a customer their order was placed when it wasn't.
    • We tightened the rules: the model cannot place orders, and any reply that mentions a price is thrown out unless a tool returned that price in the same run.
  • Memory that remembers too much. A customer's second order came back as a copy of the first, because the model kept copying lines from the chat history.
    • We now cut finished orders out of the history the model sees. New quote lines must also come from the customer's latest message, and a reply identical to the previous one is discarded.
  • Language. The agent first locked into Swahili for everyone.
    • Detection now looks only at function words and lets the newest clear message win, because product names like sukari or unga appear in English sentences too. It mirrors English, Swahili or Sheng.
  • Checking legal facts. Our first idea was to require the model to quote the source word for word. That proved brittle and unfriendly.
    • We switched to a two-layer check. Code looks up every rate, date and product name in the source text. A separate model pass, which sees only the source and the claims, judges each claim.
    • Unsupported facts are removed, and an unsupported main claim drops the notice. The "from the source" passage shown to the wholesaler is chosen by code, never written by the model.
  • Real-world data is messy. Some official sites refused automated requests, and news reports disagreed on figures; two give different previous rates for the imported-sugar excise.
    • The radar leaves an uncertain rate as UNKNOWN rather than guessing.
    • The demo runs from a reviewed snapshot of real notices, each with its source link, plus one clearly labelled sample notice so the repricing flow can be shown.

What we learned

  • Agentic depth comes from code as much as from prompts. The more we let code own the facts (prices, stock, totals, dates), the more freedom we could safely give the model.
  • Object-spatial programming fits business logic. "Walk from this conversation to its business, then to its products" is how the problem actually looks, and Jac lets us write it that way.
  • Tests make a model-driven product shippable. Scripting the model with MockLLM meant every guardrail had a test before it had a demo.

What's next

  • A WhatsApp adapter: the agent already takes a channel-independent message, so it is the natural next channel.
  • M-Pesa payment links on approved orders.
  • Live, scheduled regulation monitoring with alerts pushed to the wholesaler.
  • A proper evaluation on real handwritten lists.

Built With

Share this project:

Updates

Submission history