# How Hermes Agent Helped Me Ship an Indonesian NLP Parser in One Week

> Source: <https://dev.to/iyop666/how-hermes-agent-helped-me-ship-an-indonesian-nlp-parser-in-one-week-2gpo>
> Published: 2026-05-27 20:18:04+00:00

*This is a submission for the Hermes Agent Challenge: Build With Hermes Agent*

Warung MiMo is an AI-powered assistant for Indonesian warungs, small neighborhood shops that run a huge part of daily commerce in Indonesia. The assistant lets shop owners manage inventory, track debts, and log sales using natural Indonesian. Voice input, text input, receipt scanning. Whatever feels natural to them.

The core challenge was language.

Indonesian warung owners do not speak in clean, structured commands. They say things like:

That one sentence contains multiple actions, numbers written as Indonesian words, product names in shorthand, and a debt instruction. Generic NLP cannot handle this. I needed a custom parser built from scratch.

And I needed Hermes Agent to make it happen.

**Live:** [warung-mimo.vercel.app](https://warung-mimo.vercel.app)

**Source:** [github.com/iyop666/warung-mimo](https://github.com/iyop666/warung-mimo)

Try these inputs in the assistant page:

The assistant parses each sentence, identifies products, extracts numbers (even written as Indonesian words), and generates structured actions like stock updates or debt records.

The full source is at [github.com/iyop666/warung-mimo](https://github.com/iyop666/warung-mimo).

Here are the key modules that Hermes Agent helped build:

This was the hardest part. Indonesian compound numbers work differently from English. "Empat puluh dua" means forty-two (4 x 10 + 2). The parser needed to handle the "puluh" (tens) multiplier logic.

``` js
const NUMBER_WORDS: Record<string, number> = {
  nol: 0, satu: 1, se: 1, dua: 2, tiga: 3, empat: 4,
  lima: 5, enam: 6, tujuh: 7, delapan: 8, sembilan: 9,
  sepuluh: 10, sebelas: 11,
  "dua belas": 12, "tiga belas": 13, "empat belas": 14,
  "lima belas": 15, "enam belas": 16, "tujuh belas": 17,
  "delapan belas": 18, "sembilan belas": 19,
  "dua puluh": 20, "tiga puluh": 30, "empat puluh": 40,
  setengah: 0.5, seperempat: 0.25,
  "1/2": 0.5, "1/4": 0.25, "3/4": 0.75,
};

function parseIndonesianNumber(text: string): number | null {
  const lower = text.toLowerCase().trim();
  const sortedKeys = Object.keys(NUMBER_WORDS).sort((a, b) => b.length - a.length);

  for (const key of sortedKeys) {
    if (lower === key) return NUMBER_WORDS[key];
  }

  let compound = 0;
  let matched = false;
  const parts = lower.split(/\s+/);
  for (const part of parts) {
    if (NUMBER_WORDS[part] !== undefined) {
      if (part === "puluh") continue;
      const idx = parts.indexOf(part);
      if (idx < parts.length - 1 && parts[idx + 1] === "puluh") {
        compound += NUMBER_WORDS[part] * 10;
        matched = true;
      } else if (idx > 0 && parts[idx - 1] === "puluh") {
        compound += NUMBER_WORDS[part];
        matched = true;
      } else {
        compound += NUMBER_WORDS[part];
        matched = true;
      }
    }
  }
  if (matched && compound > 0) return compound;

  const digitMatch = lower.match(/^(\d+(?:[.,]\d+)?)$/);
  if (digitMatch) return parseFloat(digitMatch[1].replace(",", "."));
  return null;
}
```

Warung owners rarely use canonical product names. They say "Aqua" instead of "Aqua 600ml", "gula" instead of "Gula Pasir", "rokok" instead of "Rokok Sampoerna Mild". The matcher needed 30+ aliases for 8 products.

``` js
const PRODUCT_MAPPINGS: ProductMapping[] = [
  { keywords: ["indomie", "mie goreng", "mi goreng", "mie instan"], canonical: "Indomie Goreng", unit: "bungkus" },
  { keywords: ["aqua", "air mineral", "air botol"], canonical: "Aqua 600ml", unit: "botol" },
  { keywords: ["gula", "gula pasir"], canonical: "Gula Pasir", unit: "kg" },
  { keywords: ["telur", "telor", "telur ayam"], canonical: "Telur", unit: "kg" },
  { keywords: ["minyak", "minyak goreng", "minyak 1l"], canonical: "Minyak Goreng 1L", unit: "liter" },
  { keywords: ["kopi", "kopi kapal api", "kapal api"], canonical: "Kopi Kapal Api", unit: "sachet" },
  { keywords: ["rokok", "sampoerna", "sampoerna mild"], canonical: "Rokok Sampoerna Mild", unit: "bungkus" },
  { keywords: ["teh botol", "sosro", "teh sosro"], canonical: "Teh Botol Sosro", unit: "botol" },
];
```

Four patterns for recording debt, four for settling. Each handles a different way Indonesians talk about money.

``` js
const debtPatterns = [
  // "bu sari utang 25 ribu"
  /(.+?)\s+(?:utang|hutang|ngutang)\s+(\d+(?:[.,]\d+)?)\s*(ribu|rb|jt|juta)?/i,
  // "catat utang bu sari 25000"
  /(?:catat|tulis|tambah)\s+(?:utang|hutang)\s+(.+?)\s+(\d+(?:[.,]\d+)?)/i,
  // "pak budi ngutang 15 ribu"
  /(\w+(?:\s+\w+)?)\s+ngutang\s+(\d+(?:[.,]\d+)?)\s*(ribu|rb|jt|juta)?/i,
  // "utang bu sari 50rb"
  /(?:utang|hutang)\s+(.+?)\s+(\d+(?:[.,]\d+)?)\s*(ribu|rb|jt|juta)?/i,
];

const settlePatterns = [
  /bayar\s+(?:utang|hutang)\s+(.+)/i,
  /(.+?)\s+(?:bayar|lunasi|lunas)\s+(?:utang|hutang)/i,
  /(.+?)\s+(?:sudah|udah)\s+bayar/i,
  /(?:utang|hutang)\s+(.+?)\s+(?:lunas|lunasi|sudah\s*dibayar)/i,
];
```

One sentence can contain a sale, a stock update, and a debt note. The parser splits by commas, "dan", "terus", "lalu", "juga".

``` js
const segments = lower.split(/[,;]|\bdan\b|\bterus\b|\blalu\b|\bjuga\b/);

for (const segment of segments) {
  const trimmed = segment.trim();
  if (trimmed.length < 3) continue;

  const product = matchProduct(trimmed);
  if (!product) continue;

  const remaining = parseRemainingFromContext(trimmed);
  if (remaining !== null) {
    actions.push({ type: "update_stock", item: product.name, remaining, unit: product.unit });
  }
}
```

Hermes Agent was not just a coding assistant. It was the operational backbone of the entire project. Here is exactly how it powered the build.

When I needed the debt tracking regex patterns, I did not write them from scratch. I told Hermes:

"I need regex patterns for Indonesian debt recording. Handle variations like 'bu sari utang 25 ribu', 'catat utang bu sari 25000', 'pak budi ngutang 15 ribu'."

Hermes generated four patterns covering different Indonesian phrasings. I reviewed them, adjusted one edge case, and shipped. What would have taken 30 minutes of regex debugging took 5 minutes.

The same happened with the number parser. The compound logic for "empat puluh dua" (42) was tricky. I described the problem to Hermes, it suggested the "puluh" multiplier approach, and I implemented it.

Every deployment followed the same pattern:

"Deploy Warung MiMo to Vercel."

Hermes would:

No terminal switching. No build log reading. No Vercel dashboard. One Telegram message, one response with the live URL.

When I added new product aliases or changed regex patterns, I asked Hermes to test them:

"Test these inputs against the parser: 'empat puluh dua ribu', 'setengah dus', '42rb', '1/2'."

Hermes ran the parser against each input, reported which ones passed and which failed, and suggested fixes for the failures. This feedback loop was faster than writing unit tests for every edge case.

After building the core features, I needed to write about it. Hermes pulled code directly from the project repository, structured it into article format, and published drafts to Dev.to via API.

The article for the GitHub Finish-Up-A-Thon Challenge was written this way. Hermes extracted real code snippets from the codebase, organized them into the submission template, and published. I reviewed and edited, but the heavy lifting was automated.

The project runs on a VPS for backend operations. Hermes manages the entire infrastructure:

All of this happens through conversational commands. No manual SSH sessions. No remembering which port or which directory.

The key was **context persistence**.

Hermes remembered:

That meant every conversation started where the last one ended. No re-explaining. No context rebuilding. Just continuous development.

For a solo developer building a domain-specific project like Indonesian NLP, that continuity was the difference between shipping and abandoning.

**Project Stats:**

**Links:**
