feat: enhance ZAKU AI parser — better amount parsing & category mapping - #107
Conversation
- Add amount parsing rules: rb, k, jt, juta, koma, typo handling - Add context-aware type detection: transfer ke/dari, bayar/dapat utang - Add priority category mapping: grab=TRANSPORTASI, pulsa=TELEKOMUNIKASI, etc. - Add example outputs for better AI understanding - Add edge cases handling Closes #106
Reviewer's GuideEnhances the AI parser’s system prompt with Indonesian-specific amount normalization, contextual transaction-type detection, prioritized category mapping, structured edge-case handling, and example-driven JSON output guidance. Flow diagram for enhanced ZAKU AI transaction parsingflowchart TD
A["Indonesian user message"] --> B["ZAKU AI systemPrompt"]
B --> C["Normalize amount\n13rb → 13000; 1,5jt → 1500000"]
C --> D["Detect transaction type\ntransfer ke → expense\ntransfer dari → income"]
D --> E["Apply priority category mapping\nGrab → TRANSPORTASI\nPulsa → TELEKOMUNIKASI\nNetflix → HIBURAN"]
E --> F["Handle typos and edge cases"]
F --> G["Return JSON transaction\nor request missing amount/item"]
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 4 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="app/Services/AiTransactionParserService.php" line_range="211" />
<code_context>
-Use "expense" if the message contains: beli, bayar, belanja, isi, sewa, tagihan, potong, hutang.
-category should be one of: {$categories}.
-If the message has no amount, set description, amount, category, and type to null.
+ return <<<'PROMPT'
+You are ZAKU AI, an Indonesian personal finance assistant.
+Your task: Parse user message into one finance transaction.
+
+## CATEGORIES AVAILABLE
+{$categories}
+
+## AMOUNT PARSING RULES
</code_context>
<issue_to_address>
**issue (bug_risk):** The nowdoc does not interpolate `{$categories}`, so every provider receives the literal placeholder instead of the database category list. The model therefore cannot reliably restrict its category output to categories that `resolveCategory()` can resolve.
**Suggested fix:** Use an interpolating heredoc (`<<<PROMPT`) or concatenate the category list into the nowdoc.
```suggestion
return <<<PROMPT
```
</issue_to_address>
### Comment 2
<location path="app/Services/AiTransactionParserService.php" line_range="239-248" />
<code_context>
+- "dapat utang dari [orang]" → INCOME
+
+## CATEGORY MAPPING (priority order)
+1. TELEKOMUNIKASI: pulsa, axis, telkomsel, xl, indosat, paket data
+2. TRANSPORTASI: grab, gojek, bjrt, taxi, parkir, tol, bensin, ojol
+3. HIBURAN: netflix, spotify, youtube, film, nonton, game
+4. KESEHATAN: obat, apotek, dokter, rs, vitamin
+5. KECANTIKAN: shampoo, sabun, parfum, skincare, salon
+6. PENDIDIKAN: buku, les, kursus, sekolah
+7. MAKANAN: makan, minum, kopi, teh, roti, gorengan, jajan, warteg
+8. TAGIHAN: listrik, air, internet, bpjs
+9. GAJI: salary, gajian, paycheck
+10. LAINNYA: default (fallback)
</code_context>
<issue_to_address>
**issue (bug_risk):** The prompt instructs the model to return categories such as `TELEKOMUNIKASI`, `KESEHATAN`, `KECANTIKAN`, and `PENDIDIKAN`, but the seeded categories are `TAGIHAN`, `HIBURAN`, `MAKANAN`, `TRANSPORT`, `GAJI`, and `LAINNYA`. For example, `Beli pulsa 10rb` produces `TELEKOMUNIKASI`, which `resolveCategory()` cannot find and therefore stores as `LAINNYA` instead of the intended category.
**Triggers:** When the AI returns one of the newly introduced category names that is not present in the database.
**Suggested fix:** Align the prompt mappings with the actual category names, or add and migrate the new categories before asking the model to return them.
```suggestion
1. TAGIHAN: pulsa, axis, telkomsel, xl, indosat, paket data, listrik, air, internet, bpjs
2. TRANSPORT: grab, gojek, bjrt, taxi, parkir, tol, bensin, ojol
3. HIBURAN: netflix, spotify, youtube, film, nonton, game
4. MAKANAN: makan, minum, kopi, teh, roti, gorengan, jajan, warteg
5. GAJI: salary, gajian, paycheck
6. LAINNYA: obat, apotek, dokter, rs, vitamin, shampoo, sabun, parfum, skincare, salon, buku, les, kursus, sekolah, default (fallback)
```
</issue_to_address>
### Comment 3
<location path="app/Services/AiTransactionParserService.php" line_range="234" />
<code_context>
+
+Context rules:
+- "transfer ke [orang]" → EXPENSE
+- "transfer dari [orang]" → INCOME
+- "bayar utang ke [orang]" → EXPENSE
+- "dapat utang dari [orang]" → INCOME
</code_context>
<issue_to_address>
**issue (broader_impact):** The prompt declares every `transfer dari [orang]` message to be income, but `aiChat()` applies a later keyword override that does not recognize `transfer dari` and can classify the same message as expense when it also contains the existing `untuk` keyword. Thus an input such as `transfer dari ibu untuk beli laptop 200rb` is persisted with the wrong type.
**Triggers:** When a `transfer dari` message also contains an expense override keyword such as `untuk`.
**Suggested fix:** Apply the context-aware transfer rules in the controller override as well, or remove the conflicting post-processing and centralize type detection.
</issue_to_address>
### Comment 4
<location path="app/Services/AiTransactionParserService.php" line_range="261" />
<code_context>
+}
+
+## EDGE CASES
+- No amount → {"description":null,"amount":null,"category":null,"type":null,"response":" berapa jumlahnya?"}
+- No clear item → {"description":null,"amount":null,"category":null,"type":null,"response":" beli/bayar apa?"}
+- Typo → still parse (don't fail)
</code_context>
<issue_to_address>
**nitpick:** The no-amount response begins with a leading space (`" berapa jumlahnya?"`), so the API returns a visibly malformed response whenever this edge case is selected.
**Triggers:** When the model follows the no-amount edge-case example.
**Suggested fix:** Remove the leading space from the response string.
```suggestion
- No amount → {"description":null,"amount":null,"category":null,"type":null,"response":"berapa jumlahnya?"}
```
</issue_to_address>Sourcery assessment
Needs a human reviewer. 3 findings to address first, and the prompt can cause incorrect amounts, transaction types, or categories to be written to users’ financial records; changing the heredoc to a nowdoc also leaves {$categories} uninterpolated, so category validation may fail or produce mismatches. Reverting stops future bad parses but does not repair records already created, which must be identified and corrected or rerun.
Blocking findings: app/Services/AiTransactionParserService.php:211, app/Services/AiTransactionParserService.php:248, app/Services/AiTransactionParserService.php:234
| Use "expense" if the message contains: beli, bayar, belanja, isi, sewa, tagihan, potong, hutang. | ||
| category should be one of: {$categories}. | ||
| If the message has no amount, set description, amount, category, and type to null. | ||
| return <<<'PROMPT' |
There was a problem hiding this comment.
issue (bug_risk): The nowdoc does not interpolate {$categories}, so every provider receives the literal placeholder instead of the database category list. The model therefore cannot reliably restrict its category output to categories that resolveCategory() can resolve.
Suggested fix: Use an interpolating heredoc (<<<PROMPT) or concatenate the category list into the nowdoc.
| return <<<'PROMPT' | |
| return <<<PROMPT |
| 1. TELEKOMUNIKASI: pulsa, axis, telkomsel, xl, indosat, paket data | ||
| 2. TRANSPORTASI: grab, gojek, bjrt, taxi, parkir, tol, bensin, ojol | ||
| 3. HIBURAN: netflix, spotify, youtube, film, nonton, game | ||
| 4. KESEHATAN: obat, apotek, dokter, rs, vitamin | ||
| 5. KECANTIKAN: shampoo, sabun, parfum, skincare, salon | ||
| 6. PENDIDIKAN: buku, les, kursus, sekolah | ||
| 7. MAKANAN: makan, minum, kopi, teh, roti, gorengan, jajan, warteg | ||
| 8. TAGIHAN: listrik, air, internet, bpjs | ||
| 9. GAJI: salary, gajian, paycheck | ||
| 10. LAINNYA: default (fallback) |
There was a problem hiding this comment.
issue (bug_risk): The prompt instructs the model to return categories such as TELEKOMUNIKASI, KESEHATAN, KECANTIKAN, and PENDIDIKAN, but the seeded categories are TAGIHAN, HIBURAN, MAKANAN, TRANSPORT, GAJI, and LAINNYA. For example, Beli pulsa 10rb produces TELEKOMUNIKASI, which resolveCategory() cannot find and therefore stores as LAINNYA instead of the intended category.
Triggers: When the AI returns one of the newly introduced category names that is not present in the database.
Suggested fix: Align the prompt mappings with the actual category names, or add and migrate the new categories before asking the model to return them.
| 1. TELEKOMUNIKASI: pulsa, axis, telkomsel, xl, indosat, paket data | |
| 2. TRANSPORTASI: grab, gojek, bjrt, taxi, parkir, tol, bensin, ojol | |
| 3. HIBURAN: netflix, spotify, youtube, film, nonton, game | |
| 4. KESEHATAN: obat, apotek, dokter, rs, vitamin | |
| 5. KECANTIKAN: shampoo, sabun, parfum, skincare, salon | |
| 6. PENDIDIKAN: buku, les, kursus, sekolah | |
| 7. MAKANAN: makan, minum, kopi, teh, roti, gorengan, jajan, warteg | |
| 8. TAGIHAN: listrik, air, internet, bpjs | |
| 9. GAJI: salary, gajian, paycheck | |
| 10. LAINNYA: default (fallback) | |
| 1. TAGIHAN: pulsa, axis, telkomsel, xl, indosat, paket data, listrik, air, internet, bpjs | |
| 2. TRANSPORT: grab, gojek, bjrt, taxi, parkir, tol, bensin, ojol | |
| 3. HIBURAN: netflix, spotify, youtube, film, nonton, game | |
| 4. MAKANAN: makan, minum, kopi, teh, roti, gorengan, jajan, warteg | |
| 5. GAJI: salary, gajian, paycheck | |
| 6. LAINNYA: obat, apotek, dokter, rs, vitamin, shampoo, sabun, parfum, skincare, salon, buku, les, kursus, sekolah, default (fallback) |
|
|
||
| Context rules: | ||
| - "transfer ke [orang]" → EXPENSE | ||
| - "transfer dari [orang]" → INCOME |
There was a problem hiding this comment.
issue (broader_impact): The prompt declares every transfer dari [orang] message to be income, but aiChat() applies a later keyword override that does not recognize transfer dari and can classify the same message as expense when it also contains the existing untuk keyword. Thus an input such as transfer dari ibu untuk beli laptop 200rb is persisted with the wrong type.
Triggers: When a transfer dari message also contains an expense override keyword such as untuk.
Suggested fix: Apply the context-aware transfer rules in the controller override as well, or remove the conflicting post-processing and centralize type detection.
| } | ||
|
|
||
| ## EDGE CASES | ||
| - No amount → {"description":null,"amount":null,"category":null,"type":null,"response":" berapa jumlahnya?"} |
There was a problem hiding this comment.
nitpick: The no-amount response begins with a leading space (" berapa jumlahnya?"), so the API returns a visibly malformed response whenever this edge case is selected.
Triggers: When the model follows the no-amount edge-case example.
Suggested fix: Remove the leading space from the response string.
| - No amount → {"description":null,"amount":null,"category":null,"type":null,"response":" berapa jumlahnya?"} | |
| - No amount → {"description":null,"amount":null,"category":null,"type":null,"response":"berapa jumlahnya?"} |
Summary
Enhance
AiTransactionParserService::systemPrompt()untuk parsing transaksi Indonesia yang lebih akurat.Changes
System Prompt Enhancement
13rb,13k,1jt,1,5jt, typo (bayarrr,rugiii)transfer ke(expense) vstransfer dari(income)Test Cases
Beli kopi 13rbGrab 18rbBeli pulsa 10rbTransfer ke ibu 200rbNetflix 109rbRelated
Closes #106
Summary by Sourcery
Enhance the ZAKU AI transaction parser to produce more accurate structured transactions from Indonesian user messages.
New Features:
Enhancements: