From e5ed6ed2f702bfd7b1eee99c8d84fd99f24656bc Mon Sep 17 00:00:00 2001 From: Agung Hariadi Tedja Date: Wed, 22 Oct 2025 11:52:44 +0700 Subject: [PATCH 01/10] feature: identifier tanya --- tanya/specs.go | 105 ++++++++++++++++++++++++++++++++++++++++++ tanya/tanya.go | 110 ++++++++++++++++++++++++++++++++++++++++++++ tanya/tanya_test.go | 109 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 324 insertions(+) create mode 100644 tanya/specs.go create mode 100644 tanya/tanya.go create mode 100644 tanya/tanya_test.go diff --git a/tanya/specs.go b/tanya/specs.go new file mode 100644 index 0000000..e5dcdd5 --- /dev/null +++ b/tanya/specs.go @@ -0,0 +1,105 @@ +package tanya + +type ( + Intent string + MatchType string +) + +const ( + IntentUpdate Intent = "update" + IntentExplain Intent = "explain" + IntentHowTo Intent = "how_to" + IntentDefinition Intent = "definition" + IntentComparison Intent = "comparison" + IntentRecommendation Intent = "recommendation" + IntentTroubleshoot Intent = "troubleshoot" + IntentLocation Intent = "location" + IntentTime Intent = "time" + IntentPrice Intent = "price" + IntentContact Intent = "contact" + IntentQuestion Intent = "question" // general fallback + IntentOther Intent = "other" + + MatchTypeContains MatchType = "contains" + MatchTypeStarts MatchType = "starts" + MatchTypeEnds MatchType = "ends" + MatchTypeEndsTokenSuffix MatchType = "ends_token_suffix" +) + +type ( + Rule struct { + Terms []string + Weight int + MatchType MatchType + } + + IntentSpec struct { + Intent Intent + Priority int + Rules []Rule + } +) + +func terms(ss ...string) []string { return ss } + +var intentTable = []IntentSpec{ + {IntentUpdate, 95, []Rule{ + {terms("update", "perkembangan", "terbaru", "terkini", "progress", "lanjutan", "pembaruan"), 3, "contains"}, + {terms("hari ini", "sekarang", "terkini banget"), 1, "contains"}, + }}, + {IntentExplain, 90, []Rule{ + {terms("jelaskan", "jelasin", "penjelasan", "uraikan", "explain"), 3, "contains"}, + {terms("arti", "artinya", "maksud", "makna", "definisi"), 2, "contains"}, + }}, + {IntentHowTo, 80, []Rule{ + {terms("bagaimana cara ", "gimana cara "), 3, "starts"}, + {terms("cara "), 3, "starts"}, + {terms(" cara ", " langkah ", " step "), 1, "contains"}, + }}, + {IntentDefinition, 75, []Rule{ + {terms("apa itu "), 3, "starts"}, + {terms("apa arti", "apa maksud"), 2, "contains"}, + }}, + {IntentComparison, 70, []Rule{ + {terms(" vs ", " versus "), 2, "contains"}, + {terms("perbedaan ", "beda "), 2, "contains"}, + {terms("bagusan mana", "lebih bagus mana", "pilih mana"), 2, "contains"}, + }}, + {IntentRecommendation, 65, []Rule{ + {terms("rekomendasi", "rekom", "saran"), 2, "contains"}, + {terms("bagusan mana", "pilih mana", "cocok yang mana"), 2, "contains"}, + }}, + {IntentTroubleshoot, 60, []Rule{ + {terms("kenapa", "mengapa"), 2, "contains"}, + {terms("kok "), 2, "starts"}, + {terms("error", "gagal", "bug", "crash", "macet", "hang"), 2, "contains"}, + {terms("solusi ", "fix ", "gimana sih", "kenapa sih"), 1, "contains"}, + }}, + {IntentLocation, 55, []Rule{ + {terms("dimana", "di mana", "kemana", "ke mana", "lokasi", "alamat"), 2, "contains"}, + {terms(" kemana", " dimana", " alamat", " lokasi"), 2, "ends"}, + }}, + {IntentTime, 55, []Rule{ + {terms("kapan", "jadwal", "jam berapa", "pukul berapa"), 2, "contains"}, + {terms("hari ini", "minggu ini", "sekarang", "besok", "nanti sore", "malam ini"), 1, "contains"}, + }}, + {IntentPrice, 50, []Rule{ + {terms("harga", "biaya", "tarif", "fee", "ongkir"), 2, "contains"}, + }}, + {IntentContact, 50, []Rule{ + {terms("kontak", "contact", "telepon", "telp", "nomor", "email", "whatsapp", "wa"), 2, "contains"}, + }}, + // fallback tanya umum + {IntentQuestion, 10, []Rule{ + {terms("apa", "apakah", "bagaimana", "gimana", "kapan", "siapa", "dimana", "di mana", "kemana", "ke mana", "berapa", "mana"), 2, "contains"}, + {terms(" vs ", " versus "), 1, "contains"}, + {terms("kah"), 1, "ends_token_suffix"}, + {terms("ya ga sih", "ya gak sih", "ya nggak sih", "ya kan", "apa sih", "gimana sih", "kenapa sih"), 2, "contains"}, + {terms(" kok "), 2, "contains"}, + {terms("?"), 3, "contains"}, + }}, +} + +var abbrevMap = map[string]string{ + " gmn ": " gimana ", " knp ": " kenapa ", " dmn ": " dimana ", " brp ": " berapa ", +} diff --git a/tanya/tanya.go b/tanya/tanya.go new file mode 100644 index 0000000..6d1e735 --- /dev/null +++ b/tanya/tanya.go @@ -0,0 +1,110 @@ +package tanya + +import ( + "sort" + "strings" + "unicode" +) + +// IsQuestion returns true if a query is a question +func IsQuestion(q string) bool { + intent := ClassifyIntent(q) + switch intent { + case IntentPrice, IntentContact, IntentOther: + return false + default: + return true + } +} + +// ClassifyIntent returns the most likely intent for the given query +func ClassifyIntent(q string) Intent { + q = normalize(q) + if q == "" { + return IntentOther + } + type scored struct { + intent Intent + score, prio int + } + var candidates []scored + + for _, spec := range intentTable { + score := 0 + for _, r := range spec.Rules { + if matchByType(q, r) { + score += r.Weight + } + } + if score != 0 { + candidates = append(candidates, scored{spec.Intent, score, spec.Priority}) + } + } + if len(candidates) == 0 { + return IntentOther + } + + sort.Slice(candidates, func(i, j int) bool { + if candidates[i].score == candidates[j].score { + return candidates[i].prio > candidates[j].prio + } + return candidates[i].score > candidates[j].score + }) + return candidates[0].intent +} + +func matchByType(q string, r Rule) bool { + switch r.MatchType { + case MatchTypeContains: + for _, t := range r.Terms { + if strings.Contains(q, t) { + return true + } + } + case MatchTypeStarts: + for _, t := range r.Terms { + if strings.HasPrefix(q, t) { + return true + } + } + case MatchTypeEnds: + for _, t := range r.Terms { + if strings.HasSuffix(q, t) { + return true + } + } + case MatchTypeEndsTokenSuffix: + for _, tok := range strings.Split(q, " ") { + if len(tok) > 3 && strings.HasSuffix(tok, "kah") { + return true + } + } + } + return false +} + +func normalize(s string) string { + s = strings.ToLower(strings.TrimSpace(collapseSpaces(s))) + s = " " + s + " " + for k, v := range abbrevMap { + s = strings.ReplaceAll(s, k, v) + } + return strings.TrimSpace(collapseSpaces(s)) +} + +func collapseSpaces(s string) string { + var b strings.Builder + sp := false + for _, r := range s { + if unicode.IsSpace(r) { + if !sp { + b.WriteByte(' ') + sp = true + } + } else { + b.WriteRune(r) + sp = false + } + } + return strings.TrimSpace(b.String()) +} diff --git a/tanya/tanya_test.go b/tanya/tanya_test.go new file mode 100644 index 0000000..dd104cf --- /dev/null +++ b/tanya/tanya_test.go @@ -0,0 +1,109 @@ +package tanya + +import "testing" + +func TestIsQuestion(t *testing.T) { + t.Parallel() + + cases := []struct { + q string + want bool + }{ + // questions + {"apa itu knowledge graph", true}, + {"bagaimana cara reset password gmail", true}, + {"perbedaan redux vs zustand", true}, + {"kapan sidang mk hari ini", true}, + {"si andi pergi kemana ya", true}, + {"kok servernya error pas deploy", true}, + {"ya nggak sih performanya drop", true}, + {"jelasin cara mukbang", true}, + {"update kematian mahasiswa unud", true}, + + // abbreviations + {"gmn cara scrape instagram", true}, + {"knp server down semalem", true}, + {"dmn lokasi konser", true}, + {"brp harga langganan", false}, + + // non-question + {"toyota", false}, + {"harga paket premium", false}, + {"kontak cs kumparan", false}, + {"download aplikasi android", false}, + {"", false}, + {" \t ", false}, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.q, func(t *testing.T) { + t.Parallel() + got := IsQuestion(tc.q) + if got != tc.want { + t.Fatalf("IsQuestion(%q) = %v, want %v", tc.q, got, tc.want) + } + }) + } +} + +func TestClassifyIntent(t *testing.T) { + t.Parallel() + + cases := []struct { + q string + want Intent + }{ + {"update kematian mahasiswa unud", IntentUpdate}, + {"jelasin cara mukbang", IntentExplain}, + {"arti overfitting", IntentExplain}, + {"bagaimana cara reset password gmail", IntentHowTo}, + {"apa itu knowledge graph", IntentDefinition}, + {"perbedaan redux vs zustand", IntentComparison}, + {"rekomendasi laptop 10 jutaan untuk desain", IntentRecommendation}, + {"kok servernya error pas deploy", IntentTroubleshoot}, + {"alamat kantor kumparan dimana ya", IntentLocation}, + {"mau makan kemana siang ini", IntentLocation}, + {"kapan jadwal konser hari ini", IntentTime}, + {"berapa harga paket premium", IntentPrice}, + {"kontak cs atau nomor wa resmi", IntentContact}, + {"toyota", IntentOther}, + {"download aplikasi android", IntentOther}, + + // mixed signals + {"update berita gempa vs banjir hari ini", IntentUpdate}, + {"apa sih lebih bagus mana A vs B", IntentQuestion}, + {"gmn cara beli tiket konser", IntentHowTo}, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.q, func(t *testing.T) { + t.Parallel() + got := ClassifyIntent(tc.q) + if got != tc.want { + t.Fatalf("ClassifyIntent(%q) = %s, want one of %v", tc.q, got, tc.want) + } + }) + } +} + +func BenchmarkIsQuestion(b *testing.B) { + queries := []string{ + "apa itu knowledge graph", + "bagaimana cara reset password gmail", + "update kematian mahasiswa unud", + "jelasin cara mukbang", + "perbedaan redux vs zustand", + "toyota", + "harga paket premium", + "kontak cs kumparan", + "download aplikasi android", + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, q := range queries { + _ = IsQuestion(q) + } + } +} From 790910976583676e607b2e90a3d9be40e6844760 Mon Sep 17 00:00:00 2001 From: Agung Hariadi Tedja Date: Wed, 22 Oct 2025 12:45:48 +0700 Subject: [PATCH 02/10] fix lint issue --- tanya/specs.go | 9 +++++++-- tanya/tanya.go | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tanya/specs.go b/tanya/specs.go index e5dcdd5..3da3050 100644 --- a/tanya/specs.go +++ b/tanya/specs.go @@ -1,10 +1,13 @@ package tanya type ( - Intent string + // Intent is the intent of a query + Intent string + // MatchType is the type of match MatchType string ) +// Intent and MatchType constants const ( IntentUpdate Intent = "update" IntentExplain Intent = "explain" @@ -23,16 +26,18 @@ const ( MatchTypeContains MatchType = "contains" MatchTypeStarts MatchType = "starts" MatchTypeEnds MatchType = "ends" - MatchTypeEndsTokenSuffix MatchType = "ends_token_suffix" + MatchTypeEndsTokenSuffix MatchType = "ends_token_suffix" // nolint:gosec ) type ( + // Rule is a rule for matching a query to intent Rule struct { Terms []string Weight int MatchType MatchType } + // IntentSpec is a specification for intent IntentSpec struct { Intent Intent Priority int diff --git a/tanya/tanya.go b/tanya/tanya.go index 6d1e735..1e11007 100644 --- a/tanya/tanya.go +++ b/tanya/tanya.go @@ -9,7 +9,7 @@ import ( // IsQuestion returns true if a query is a question func IsQuestion(q string) bool { intent := ClassifyIntent(q) - switch intent { + switch intent { // nolint:exhaustive case IntentPrice, IntentContact, IntentOther: return false default: From 82d32f5fad18c20eba73d1b20c095d5e427961d4 Mon Sep 17 00:00:00 2001 From: Agung Hariadi Tedja Date: Wed, 22 Oct 2025 13:28:59 +0700 Subject: [PATCH 03/10] remove end suffix --- tanya/specs.go | 7 +++---- tanya/tanya.go | 6 ------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/tanya/specs.go b/tanya/specs.go index 3da3050..e6669b6 100644 --- a/tanya/specs.go +++ b/tanya/specs.go @@ -23,10 +23,9 @@ const ( IntentQuestion Intent = "question" // general fallback IntentOther Intent = "other" - MatchTypeContains MatchType = "contains" - MatchTypeStarts MatchType = "starts" - MatchTypeEnds MatchType = "ends" - MatchTypeEndsTokenSuffix MatchType = "ends_token_suffix" // nolint:gosec + MatchTypeContains MatchType = "contains" + MatchTypeStarts MatchType = "starts" + MatchTypeEnds MatchType = "ends" ) type ( diff --git a/tanya/tanya.go b/tanya/tanya.go index 1e11007..99d7c80 100644 --- a/tanya/tanya.go +++ b/tanya/tanya.go @@ -73,12 +73,6 @@ func matchByType(q string, r Rule) bool { return true } } - case MatchTypeEndsTokenSuffix: - for _, tok := range strings.Split(q, " ") { - if len(tok) > 3 && strings.HasSuffix(tok, "kah") { - return true - } - } } return false } From 20c6ac845e2069e55294a9cd541fd3c49330a194 Mon Sep 17 00:00:00 2001 From: Agung Hariadi Tedja Date: Wed, 22 Oct 2025 14:00:46 +0700 Subject: [PATCH 04/10] fix fix --- tanya/specs.go | 69 ++++++++++++++++++++++--------------------- tanya/tanya.go | 7 +++++ tanya/tanya_test.go | 72 +++++++++++++++++++++++++++++++++++++++------ 3 files changed, 106 insertions(+), 42 deletions(-) diff --git a/tanya/specs.go b/tanya/specs.go index e6669b6..0e8353d 100644 --- a/tanya/specs.go +++ b/tanya/specs.go @@ -23,9 +23,10 @@ const ( IntentQuestion Intent = "question" // general fallback IntentOther Intent = "other" - MatchTypeContains MatchType = "contains" - MatchTypeStarts MatchType = "starts" - MatchTypeEnds MatchType = "ends" + MatchTypeContains MatchType = "contains" + MatchTypeStarts MatchType = "starts" + MatchTypeEnds MatchType = "ends" + MatchTypeEndsTokenSuffix MatchType = "ends_token_suffix" // nolint:gosec ) type ( @@ -48,59 +49,61 @@ func terms(ss ...string) []string { return ss } var intentTable = []IntentSpec{ {IntentUpdate, 95, []Rule{ - {terms("update", "perkembangan", "terbaru", "terkini", "progress", "lanjutan", "pembaruan"), 3, "contains"}, - {terms("hari ini", "sekarang", "terkini banget"), 1, "contains"}, + {terms("update", "perkembangan", "terbaru", "terkini", "progress", "lanjutan", "pembaruan"), 3, MatchTypeContains}, + {terms("hari ini", "sekarang", "terkini banget"), 1, MatchTypeContains}, }}, {IntentExplain, 90, []Rule{ - {terms("jelaskan", "jelasin", "penjelasan", "uraikan", "explain"), 3, "contains"}, - {terms("arti", "artinya", "maksud", "makna", "definisi"), 2, "contains"}, + {terms("jelaskan", "jelasin", "penjelasan", "uraikan", "explain"), 3, MatchTypeContains}, + {terms("arti", "artinya", "maksud", "makna", "definisi"), 2, MatchTypeContains}, }}, {IntentHowTo, 80, []Rule{ - {terms("bagaimana cara ", "gimana cara "), 3, "starts"}, - {terms("cara "), 3, "starts"}, - {terms(" cara ", " langkah ", " step "), 1, "contains"}, + {terms("bagaimana cara ", "gimana cara "), 3, MatchTypeStarts}, + {terms("cara "), 3, MatchTypeStarts}, + {terms(" cara ", " langkah ", " step "), 1, MatchTypeContains}, }}, {IntentDefinition, 75, []Rule{ - {terms("apa itu "), 3, "starts"}, - {terms("apa arti", "apa maksud"), 2, "contains"}, + {terms("apa itu "), 3, MatchTypeStarts}, + {terms("apa arti", "apa maksud"), 2, MatchTypeContains}, }}, {IntentComparison, 70, []Rule{ - {terms(" vs ", " versus "), 2, "contains"}, - {terms("perbedaan ", "beda "), 2, "contains"}, - {terms("bagusan mana", "lebih bagus mana", "pilih mana"), 2, "contains"}, + {terms(" vs ", " versus "), 2, MatchTypeContains}, + {terms("perbedaan ", "beda "), 2, MatchTypeContains}, + {terms("bagusan mana", "lebih bagus mana", "pilih mana"), 2, MatchTypeContains}, }}, {IntentRecommendation, 65, []Rule{ - {terms("rekomendasi", "rekom", "saran"), 2, "contains"}, - {terms("bagusan mana", "pilih mana", "cocok yang mana"), 2, "contains"}, + {terms("rekomendasi", "rekom", "saran"), 2, MatchTypeContains}, + {terms("bagusan mana", "pilih mana", "cocok yang mana"), 2, MatchTypeContains}, }}, {IntentTroubleshoot, 60, []Rule{ - {terms("kenapa", "mengapa"), 2, "contains"}, - {terms("kok "), 2, "starts"}, - {terms("error", "gagal", "bug", "crash", "macet", "hang"), 2, "contains"}, - {terms("solusi ", "fix ", "gimana sih", "kenapa sih"), 1, "contains"}, + {terms("kenapa", "mengapa"), 2, MatchTypeContains}, + {terms("kok "), 2, MatchTypeStarts}, + {terms("error", "gagal", "bug", "crash", "macet", "hang"), 2, MatchTypeContains}, + {terms("solusi ", "fix ", "gimana sih", "kenapa sih"), 1, MatchTypeContains}, }}, {IntentLocation, 55, []Rule{ - {terms("dimana", "di mana", "kemana", "ke mana", "lokasi", "alamat"), 2, "contains"}, - {terms(" kemana", " dimana", " alamat", " lokasi"), 2, "ends"}, + {terms("dimana", "di mana", "kemana", "ke mana", "lokasi", "alamat"), 2, MatchTypeContains}, + {terms(" kemana", " dimana", " alamat", " lokasi"), 2, MatchTypeEnds}, }}, {IntentTime, 55, []Rule{ - {terms("kapan", "jadwal", "jam berapa", "pukul berapa"), 2, "contains"}, - {terms("hari ini", "minggu ini", "sekarang", "besok", "nanti sore", "malam ini"), 1, "contains"}, + {terms("kapan", "jadwal", "jam berapa", "pukul berapa"), 2, MatchTypeContains}, + {terms("hari ini", "minggu ini", "sekarang", "besok", "nanti sore", "malam ini"), 1, MatchTypeContains}, }}, {IntentPrice, 50, []Rule{ - {terms("harga", "biaya", "tarif", "fee", "ongkir"), 2, "contains"}, + {terms("harga", "biaya", "tarif", "fee", "ongkir"), 2, MatchTypeContains}, }}, {IntentContact, 50, []Rule{ - {terms("kontak", "contact", "telepon", "telp", "nomor", "email", "whatsapp", "wa"), 2, "contains"}, + {terms("kontak", "contact", "telepon", "telp", "nomor", "email", "whatsapp", "wa"), 2, MatchTypeContains}, }}, // fallback tanya umum {IntentQuestion, 10, []Rule{ - {terms("apa", "apakah", "bagaimana", "gimana", "kapan", "siapa", "dimana", "di mana", "kemana", "ke mana", "berapa", "mana"), 2, "contains"}, - {terms(" vs ", " versus "), 1, "contains"}, - {terms("kah"), 1, "ends_token_suffix"}, - {terms("ya ga sih", "ya gak sih", "ya nggak sih", "ya kan", "apa sih", "gimana sih", "kenapa sih"), 2, "contains"}, - {terms(" kok "), 2, "contains"}, - {terms("?"), 3, "contains"}, + {terms("apa", "apakah", "bagaimana", "gimana", "kapan", "siapa", "dimana", "di mana", "kemana", "ke mana", "berapa"), 2, MatchTypeContains}, + {terms(" vs ", " versus "), 1, MatchTypeContains}, + {terms(" yang mana "), 2, MatchTypeContains}, + {terms(" mana"), 2, MatchTypeEnds}, + {terms("kah"), 1, MatchTypeEndsTokenSuffix}, + {terms("ya ga sih", "ya gak sih", "ya nggak sih", "ya kan", "apa sih", "gimana sih", "kenapa sih"), 2, MatchTypeContains}, + {terms(" kok "), 2, MatchTypeContains}, + {terms("?"), 3, MatchTypeContains}, }}, } diff --git a/tanya/tanya.go b/tanya/tanya.go index 99d7c80..9235c67 100644 --- a/tanya/tanya.go +++ b/tanya/tanya.go @@ -73,7 +73,14 @@ func matchByType(q string, r Rule) bool { return true } } + case MatchTypeEndsTokenSuffix: + for _, tok := range strings.Split(q, " ") { + if len(tok) > 3 && strings.HasSuffix(tok, "kah") { + return true + } + } } + return false } diff --git a/tanya/tanya_test.go b/tanya/tanya_test.go index dd104cf..0279371 100644 --- a/tanya/tanya_test.go +++ b/tanya/tanya_test.go @@ -1,6 +1,9 @@ package tanya -import "testing" +import ( + "strings" + "testing" +) func TestIsQuestion(t *testing.T) { t.Parallel() @@ -9,7 +12,7 @@ func TestIsQuestion(t *testing.T) { q string want bool }{ - // questions + // --- explicit / canonical questions {"apa itu knowledge graph", true}, {"bagaimana cara reset password gmail", true}, {"perbedaan redux vs zustand", true}, @@ -20,24 +23,75 @@ func TestIsQuestion(t *testing.T) { {"jelasin cara mukbang", true}, {"update kematian mahasiswa unud", true}, - // abbreviations - {"gmn cara scrape instagram", true}, - {"knp server down semalem", true}, - {"dmn lokasi konser", true}, - {"brp harga langganan", false}, + // --- abbreviations / slang normalization + {"gmn cara scrape instagram", true}, // gmn -> gimana + {"knp server down semalem", true}, // knp -> kenapa + {"dmn lokasi konser", true}, // dmn -> dimana + {"brp harga langganan", false}, // brp -> berapa -> price intent => non-question - // non-question + // --- particles at the end (colloquial endings) + {"mau makan kemana siang ini", true}, + {"dia tadi ke kantor dimana", true}, + {"ini kenapa ya", true}, + {"ini apa sih", true}, + {"performanya turun ya kan", true}, + + // --- -kah suffix + {"bisakah presiden diganti", true}, + {"mungkinkah ini berhasil", true}, + {"adakah solusi cepatnya", true}, + + // --- how-to variants + {"cara deploy ke production docker", true}, + {"bagaimana cara memperbaiki error 500", true}, + {"cara cepat push ke github ", true}, // extra spaces + + // --- comparison signals + {"bagusan mana mirrorless atau dslr", true}, + {"A vs B untuk data pipeline", true}, + {"versus airflow vs dagster", true}, + + // --- definition / explain variants + {"apa arti resilien", true}, + {"apa maksud zero copy", true}, + {"explain RAG pls", true}, + {"penjelasan implementasi RAG", true}, // explanation intent + + // --- update/time intent + {"terkini erupsi bromo", true}, + {"perkembangan kasus x sekarang", true}, + + // --- punctuation / emoji / casing + {"KENAPA SERVER LEMOT", true}, + {"Kenapa server lemot?", true}, // explicit '?' + {"kenapa server lemot 🤔", true}, + {" Bagaimana Cara Reset Password ", true}, + + // --- URL / noise in a query + {"cara setting oauth di https://example.com/docs", true}, + {"update api rate limit v2 (lihat changelog)", true}, + + // --- obvious non-questions {"toyota", false}, {"harga paket premium", false}, {"kontak cs kumparan", false}, {"download aplikasi android", false}, + {"grab promo kupon", false}, {"", false}, {" \t ", false}, + + // --- tricky near-misses / should remain non-question + {"vs code extensions", false}, // 'vs' as product word, not comparison + {"mana store", false}, // 'mana' as noun chunk; intended info/browse } for _, tc := range cases { tc := tc - t.Run(tc.q, func(t *testing.T) { + name := tc.q + if strings.TrimSpace(name) == "" { + name = "" + } + t.Run(name, func(t *testing.T) { t.Parallel() got := IsQuestion(tc.q) if got != tc.want { From 698c539f8602940c475d8d82f160a68747d31703 Mon Sep 17 00:00:00 2001 From: Agung Hariadi Tedja Date: Wed, 22 Oct 2025 14:08:48 +0700 Subject: [PATCH 05/10] fix fix --- tanya/specs.go | 79 +++++++++++++++++++++++++------------------------- tanya/tanya.go | 54 +++++++++++++++++++++++++++++++--- 2 files changed, 90 insertions(+), 43 deletions(-) diff --git a/tanya/specs.go b/tanya/specs.go index 0e8353d..6e513b6 100644 --- a/tanya/specs.go +++ b/tanya/specs.go @@ -23,18 +23,19 @@ const ( IntentQuestion Intent = "question" // general fallback IntentOther Intent = "other" - MatchTypeContains MatchType = "contains" - MatchTypeStarts MatchType = "starts" - MatchTypeEnds MatchType = "ends" - MatchTypeEndsTokenSuffix MatchType = "ends_token_suffix" // nolint:gosec + MatchTypeContains MatchType = "contains" + MatchTypeStarts MatchType = "starts" + MatchTypeEnds MatchType = "ends" + MatchTypeTokenSuffix MatchType = "token_suffix" // nolint:gosec ) type ( // Rule is a rule for matching a query to intent Rule struct { - Terms []string - Weight int - MatchType MatchType + Terms []string + Weight int + MatchType MatchType + MinTokenLen int // optional: for token_suffix; <=0 => default 4 } // IntentSpec is a specification for intent @@ -49,61 +50,61 @@ func terms(ss ...string) []string { return ss } var intentTable = []IntentSpec{ {IntentUpdate, 95, []Rule{ - {terms("update", "perkembangan", "terbaru", "terkini", "progress", "lanjutan", "pembaruan"), 3, MatchTypeContains}, - {terms("hari ini", "sekarang", "terkini banget"), 1, MatchTypeContains}, + {terms("update", "perkembangan", "terbaru", "terkini", "progress", "lanjutan", "pembaruan"), 3, MatchTypeContains, 0}, + {terms("hari ini", "sekarang", "terkini banget"), 1, MatchTypeContains, 0}, }}, {IntentExplain, 90, []Rule{ - {terms("jelaskan", "jelasin", "penjelasan", "uraikan", "explain"), 3, MatchTypeContains}, - {terms("arti", "artinya", "maksud", "makna", "definisi"), 2, MatchTypeContains}, + {terms("jelaskan", "jelasin", "penjelasan", "uraikan", "explain"), 3, MatchTypeContains, 0}, + {terms("arti", "artinya", "maksud", "makna", "definisi"), 2, MatchTypeContains, 0}, }}, {IntentHowTo, 80, []Rule{ - {terms("bagaimana cara ", "gimana cara "), 3, MatchTypeStarts}, - {terms("cara "), 3, MatchTypeStarts}, - {terms(" cara ", " langkah ", " step "), 1, MatchTypeContains}, + {terms("bagaimana cara ", "gimana cara "), 3, MatchTypeStarts, 0}, + {terms("cara "), 3, MatchTypeStarts, 0}, + {terms(" cara ", " langkah ", " step "), 1, MatchTypeContains, 0}, }}, {IntentDefinition, 75, []Rule{ - {terms("apa itu "), 3, MatchTypeStarts}, - {terms("apa arti", "apa maksud"), 2, MatchTypeContains}, + {terms("apa itu "), 3, MatchTypeStarts, 0}, + {terms("apa arti", "apa maksud"), 2, MatchTypeContains, 0}, }}, {IntentComparison, 70, []Rule{ - {terms(" vs ", " versus "), 2, MatchTypeContains}, - {terms("perbedaan ", "beda "), 2, MatchTypeContains}, - {terms("bagusan mana", "lebih bagus mana", "pilih mana"), 2, MatchTypeContains}, + {terms(" vs ", " versus "), 2, MatchTypeContains, 0}, + {terms("perbedaan ", "beda "), 2, MatchTypeContains, 0}, + {terms("bagusan mana", "lebih bagus mana", "pilih mana"), 2, MatchTypeContains, 0}, }}, {IntentRecommendation, 65, []Rule{ - {terms("rekomendasi", "rekom", "saran"), 2, MatchTypeContains}, - {terms("bagusan mana", "pilih mana", "cocok yang mana"), 2, MatchTypeContains}, + {terms("rekomendasi", "rekom", "saran"), 2, MatchTypeContains, 0}, + {terms("bagusan mana", "pilih mana", "cocok yang mana"), 2, MatchTypeContains, 0}, }}, {IntentTroubleshoot, 60, []Rule{ - {terms("kenapa", "mengapa"), 2, MatchTypeContains}, - {terms("kok "), 2, MatchTypeStarts}, - {terms("error", "gagal", "bug", "crash", "macet", "hang"), 2, MatchTypeContains}, - {terms("solusi ", "fix ", "gimana sih", "kenapa sih"), 1, MatchTypeContains}, + {terms("kenapa", "mengapa"), 2, MatchTypeContains, 0}, + {terms("kok "), 2, MatchTypeStarts, 0}, + {terms("error", "gagal", "bug", "crash", "macet", "hang"), 2, MatchTypeContains, 0}, + {terms("solusi ", "fix ", "gimana sih", "kenapa sih"), 1, MatchTypeContains, 0}, }}, {IntentLocation, 55, []Rule{ - {terms("dimana", "di mana", "kemana", "ke mana", "lokasi", "alamat"), 2, MatchTypeContains}, - {terms(" kemana", " dimana", " alamat", " lokasi"), 2, MatchTypeEnds}, + {terms("dimana", "di mana", "kemana", "ke mana", "lokasi", "alamat"), 2, MatchTypeContains, 0}, + {terms(" kemana", " dimana", " alamat", " lokasi"), 2, MatchTypeEnds, 0}, }}, {IntentTime, 55, []Rule{ - {terms("kapan", "jadwal", "jam berapa", "pukul berapa"), 2, MatchTypeContains}, - {terms("hari ini", "minggu ini", "sekarang", "besok", "nanti sore", "malam ini"), 1, MatchTypeContains}, + {terms("kapan", "jadwal", "jam berapa", "pukul berapa"), 2, MatchTypeContains, 0}, + {terms("hari ini", "minggu ini", "sekarang", "besok", "nanti sore", "malam ini"), 1, MatchTypeContains, 0}, }}, {IntentPrice, 50, []Rule{ - {terms("harga", "biaya", "tarif", "fee", "ongkir"), 2, MatchTypeContains}, + {terms("harga", "biaya", "tarif", "fee", "ongkir"), 2, MatchTypeContains, 0}, }}, {IntentContact, 50, []Rule{ - {terms("kontak", "contact", "telepon", "telp", "nomor", "email", "whatsapp", "wa"), 2, MatchTypeContains}, + {terms("kontak", "contact", "telepon", "telp", "nomor", "email", "whatsapp", "wa"), 2, MatchTypeContains, 0}, }}, // fallback tanya umum {IntentQuestion, 10, []Rule{ - {terms("apa", "apakah", "bagaimana", "gimana", "kapan", "siapa", "dimana", "di mana", "kemana", "ke mana", "berapa"), 2, MatchTypeContains}, - {terms(" vs ", " versus "), 1, MatchTypeContains}, - {terms(" yang mana "), 2, MatchTypeContains}, - {terms(" mana"), 2, MatchTypeEnds}, - {terms("kah"), 1, MatchTypeEndsTokenSuffix}, - {terms("ya ga sih", "ya gak sih", "ya nggak sih", "ya kan", "apa sih", "gimana sih", "kenapa sih"), 2, MatchTypeContains}, - {terms(" kok "), 2, MatchTypeContains}, - {terms("?"), 3, MatchTypeContains}, + {terms("apa", "apakah", "bagaimana", "gimana", "kapan", "siapa", "dimana", "di mana", "kemana", "ke mana", "berapa"), 2, MatchTypeContains, 0}, + {terms(" vs ", " versus "), 1, MatchTypeContains, 0}, + {terms(" yang mana "), 2, MatchTypeContains, 0}, + {terms(" mana"), 2, MatchTypeEnds, 0}, + {terms("kah"), 1, MatchTypeTokenSuffix, 4}, + {terms("ya ga sih", "ya gak sih", "ya nggak sih", "ya kan", "apa sih", "gimana sih", "kenapa sih"), 2, MatchTypeContains, 0}, + {terms(" kok "), 2, MatchTypeContains, 0}, + {terms("?"), 3, MatchTypeContains, 0}, }}, } diff --git a/tanya/tanya.go b/tanya/tanya.go index 9235c67..0813739 100644 --- a/tanya/tanya.go +++ b/tanya/tanya.go @@ -4,6 +4,7 @@ import ( "sort" "strings" "unicode" + "unicode/utf8" ) // IsQuestion returns true if a query is a question @@ -73,10 +74,19 @@ func matchByType(q string, r Rule) bool { return true } } - case MatchTypeEndsTokenSuffix: - for _, tok := range strings.Split(q, " ") { - if len(tok) > 3 && strings.HasSuffix(tok, "kah") { - return true + case MatchTypeTokenSuffix: + minLen := r.MinTokenLen + if minLen <= 0 { + minLen = 4 + } + for _, tok := range tokenize(q) { + if len(tok) < minLen { + continue + } + for _, suf := range r.Terms { + if strings.HasSuffix(tok, suf) { + return true + } } } } @@ -109,3 +119,39 @@ func collapseSpaces(s string) string { } return strings.TrimSpace(b.String()) } + +// tokenize splits on whitespace and trims leading/trailing non-letters/digits per token. +// keeps tokens simple & fast (no regex). +func tokenize(s string) []string { + raw := strings.Fields(s) + out := make([]string, 0, len(raw)) + for _, t := range raw { + t = trimNonAlphaNum(t) + if t != "" { + out = append(out, t) + } + } + return out +} + +func trimNonAlphaNum(s string) string { + start, end := 0, len(s) + for start < end { + r := rune(s[start]) + if isAlphaNum(r) { + break + } + _, w := utf8.DecodeRuneInString(s[start:]) + start += w + } + for end > start { + r, w := utf8.DecodeLastRuneInString(s[:end]) + if isAlphaNum(r) { + break + } + end -= w + } + return s[start:end] +} + +func isAlphaNum(r rune) bool { return unicode.IsLetter(r) || unicode.IsDigit(r) } From 5d83c783e6c3f58c932771b6245182ec33738270 Mon Sep 17 00:00:00 2001 From: Agung Hariadi Tedja Date: Wed, 22 Oct 2025 14:33:05 +0700 Subject: [PATCH 06/10] updated dikid --- tanya/specs.go | 25 +++++++++++++++++++-- tanya/tanya.go | 22 ++++++++++++++++--- tanya/tanya_test.go | 53 ++++++++++++++++++++++++++++++++++----------- 3 files changed, 82 insertions(+), 18 deletions(-) diff --git a/tanya/specs.go b/tanya/specs.go index 6e513b6..e603774 100644 --- a/tanya/specs.go +++ b/tanya/specs.go @@ -61,6 +61,9 @@ var intentTable = []IntentSpec{ {terms("bagaimana cara ", "gimana cara "), 3, MatchTypeStarts, 0}, {terms("cara "), 3, MatchTypeStarts, 0}, {terms(" cara ", " langkah ", " step "), 1, MatchTypeContains, 0}, + {terms("resep "), 3, MatchTypeStarts, 0}, + {terms(" panduan ", "panduan "), 2, MatchTypeContains, 0}, + {terms(" tutorial ", "tutorial "), 2, MatchTypeContains, 0}, }}, {IntentDefinition, 75, []Rule{ {terms("apa itu "), 3, MatchTypeStarts, 0}, @@ -74,6 +77,8 @@ var intentTable = []IntentSpec{ {IntentRecommendation, 65, []Rule{ {terms("rekomendasi", "rekom", "saran"), 2, MatchTypeContains, 0}, {terms("bagusan mana", "pilih mana", "cocok yang mana"), 2, MatchTypeContains, 0}, + {terms("menu "), 2, MatchTypeStarts, 0}, + {terms(" ide ", "ide "), 1, MatchTypeContains, 0}, }}, {IntentTroubleshoot, 60, []Rule{ {terms("kenapa", "mengapa"), 2, MatchTypeContains, 0}, @@ -100,8 +105,9 @@ var intentTable = []IntentSpec{ {terms("apa", "apakah", "bagaimana", "gimana", "kapan", "siapa", "dimana", "di mana", "kemana", "ke mana", "berapa"), 2, MatchTypeContains, 0}, {terms(" vs ", " versus "), 1, MatchTypeContains, 0}, {terms(" yang mana "), 2, MatchTypeContains, 0}, + {terms("yang mana "), 2, MatchTypeStarts, 0}, {terms(" mana"), 2, MatchTypeEnds, 0}, - {terms("kah"), 1, MatchTypeTokenSuffix, 4}, + {terms("kah"), 1, MatchTypeTokenSuffix, 5}, {terms("ya ga sih", "ya gak sih", "ya nggak sih", "ya kan", "apa sih", "gimana sih", "kenapa sih"), 2, MatchTypeContains, 0}, {terms(" kok "), 2, MatchTypeContains, 0}, {terms("?"), 3, MatchTypeContains, 0}, @@ -109,5 +115,20 @@ var intentTable = []IntentSpec{ } var abbrevMap = map[string]string{ - " gmn ": " gimana ", " knp ": " kenapa ", " dmn ": " dimana ", " brp ": " berapa ", + "gmn": "gimana", + "gmna": "gimana", + "bgmn": "bagaimana", + "knp": "kenapa", + "knpa": "kenapa", + "dmn": "dimana", + "dmna": "dimana", + "dimn": "dimana", + "kmn": "kemana", + "kmna": "kemana", + "brp": "berapa", + "brpa": "berapa", + "kpn": "kapan", + "kpan": "kapan", + "sapa": "siapa", + "sp": "siapa", } diff --git a/tanya/tanya.go b/tanya/tanya.go index 0813739..e635af0 100644 --- a/tanya/tanya.go +++ b/tanya/tanya.go @@ -97,9 +97,7 @@ func matchByType(q string, r Rule) bool { func normalize(s string) string { s = strings.ToLower(strings.TrimSpace(collapseSpaces(s))) s = " " + s + " " - for k, v := range abbrevMap { - s = strings.ReplaceAll(s, k, v) - } + s = expandAbbreviations(s) return strings.TrimSpace(collapseSpaces(s)) } @@ -120,6 +118,24 @@ func collapseSpaces(s string) string { return strings.TrimSpace(b.String()) } +// normalize abbreviations anywhere (start/mid/end) +func expandAbbreviations(s string) string { + words := strings.Fields(s) + for i, w := range words { + if repl, ok := abbrevMap[w]; ok { + words[i] = repl + continue + } + // handle punctuation like "knp?" or "dmn," etc. + base := strings.TrimRight(w, "?.!,") + suffix := w[len(base):] + if repl, ok := abbrevMap[base]; ok { + words[i] = repl + suffix + } + } + return strings.Join(words, " ") +} + // tokenize splits on whitespace and trims leading/trailing non-letters/digits per token. // keeps tokens simple & fast (no regex). func tokenize(s string) []string { diff --git a/tanya/tanya_test.go b/tanya/tanya_test.go index 0279371..59a6eb3 100644 --- a/tanya/tanya_test.go +++ b/tanya/tanya_test.go @@ -23,11 +23,24 @@ func TestIsQuestion(t *testing.T) { {"jelasin cara mukbang", true}, {"update kematian mahasiswa unud", true}, - // --- abbreviations / slang normalization + // --- abbreviations / slang normalization (start/mid/end + punct) {"gmn cara scrape instagram", true}, // gmn -> gimana + {"gmna cara beli tiket", true}, // gmna -> gimana + {"bgmn cara install docker", true}, // bgmn -> bagaimana {"knp server down semalem", true}, // knp -> kenapa + {"knpa servernya lambat", true}, // knpa -> kenapa {"dmn lokasi konser", true}, // dmn -> dimana - {"brp harga langganan", false}, // brp -> berapa -> price intent => non-question + {"dmna lokasi vaksin", true}, // dmna -> dimana + {"ini ada di dimn", true}, // dimn -> dimana + {"kmn mau makan siang?", true}, // kmn -> kemana + {"kita kmna abis ini", true}, // kmna -> kemana + {"brp harga langganan", false}, // brp -> berapa -> price => non-question + {"kpn rilis update?", true}, // kpn -> kapan + {"kpan meetingnya", true}, // kpan -> kapan + {"sapa yang ikut", true}, // sapa -> siapa + {"sp aja yang hadir", true}, // sp -> siapa + {"knp?", true}, // knp at start + punctuation + {"server down dmn,", true}, // trailing punctuation handled // --- particles at the end (colloquial endings) {"mau makan kemana siang ini", true}, @@ -36,53 +49,67 @@ func TestIsQuestion(t *testing.T) { {"ini apa sih", true}, {"performanya turun ya kan", true}, - // --- -kah suffix + // --- -kah suffix (via token_suffix) including punctuation {"bisakah presiden diganti", true}, {"mungkinkah ini berhasil", true}, {"adakah solusi cepatnya", true}, + {"mungkinkah ini berhasil!!!", true}, + {"akah", false}, // too short to be meaningful (guard by MinTokenLen) // --- how-to variants {"cara deploy ke production docker", true}, {"bagaimana cara memperbaiki error 500", true}, {"cara cepat push ke github ", true}, // extra spaces + {"cara setting oauth di https://example.com/docs", true}, + {"resep bubur bayi 6 bulan", true}, + {"resep mpasi tanpa gula garam", true}, + {"menu mpasi 6 bulan", true}, + {"ide mpasi murah meriah", true}, + {"tutorial docker", true}, + {"panduan upgrade postgres", true}, // --- comparison signals {"bagusan mana mirrorless atau dslr", true}, {"A vs B untuk data pipeline", true}, {"versus airflow vs dagster", true}, + {"pilih mana A atau B", true}, + {"lebih bagus mana iphone atau pixel", true}, // --- definition / explain variants {"apa arti resilien", true}, {"apa maksud zero copy", true}, {"explain RAG pls", true}, - {"penjelasan implementasi RAG", true}, // explanation intent + {"penjelasan implementasi RAG", true}, - // --- update/time intent + // --- update / time / location intent {"terkini erupsi bromo", true}, {"perkembangan kasus x sekarang", true}, + {"lokasi kantor jakarta selatan", true}, // location -> question-like + {"jadwal konser jakarta", true}, // time -> question-like + + // --- “yang mana” (keep as question), but “mana store” should not + {"yang mana yang benar", true}, + {"ini pilih yang mana", true}, + {"mana store", false}, // 'mana' as noun chunk; intended info/browse // --- punctuation / emoji / casing {"KENAPA SERVER LEMOT", true}, - {"Kenapa server lemot?", true}, // explicit '?' + {"Kenapa server lemot?", true}, {"kenapa server lemot 🤔", true}, {" Bagaimana Cara Reset Password ", true}, - // --- URL / noise in a query - {"cara setting oauth di https://example.com/docs", true}, - {"update api rate limit v2 (lihat changelog)", true}, + // --- tricky “vs” that is not comparison (product name) + {"vs code extensions", false}, // treat 'vs' here as product word, not comparison // --- obvious non-questions {"toyota", false}, + {"jakarta", false}, {"harga paket premium", false}, {"kontak cs kumparan", false}, {"download aplikasi android", false}, {"grab promo kupon", false}, {"", false}, {" \t ", false}, - - // --- tricky near-misses / should remain non-question - {"vs code extensions", false}, // 'vs' as product word, not comparison - {"mana store", false}, // 'mana' as noun chunk; intended info/browse } for _, tc := range cases { From 423c1650a1c9f1464ca5e40c0e976a5f58ad703a Mon Sep 17 00:00:00 2001 From: Agung Hariadi Tedja Date: Wed, 22 Oct 2025 14:50:56 +0700 Subject: [PATCH 07/10] gefi nambahin kealayan --- tanya/specs.go | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/tanya/specs.go b/tanya/specs.go index e603774..8daee0e 100644 --- a/tanya/specs.go +++ b/tanya/specs.go @@ -115,20 +115,24 @@ var intentTable = []IntentSpec{ } var abbrevMap = map[string]string{ - "gmn": "gimana", - "gmna": "gimana", - "bgmn": "bagaimana", - "knp": "kenapa", - "knpa": "kenapa", - "dmn": "dimana", - "dmna": "dimana", - "dimn": "dimana", - "kmn": "kemana", - "kmna": "kemana", - "brp": "berapa", - "brpa": "berapa", - "kpn": "kapan", - "kpan": "kapan", - "sapa": "siapa", - "sp": "siapa", + "gmn": "gimana", + "gmna": "gimana", + "bgmn": "bagaimana", + "knp": "kenapa", + "knpa": "kenapa", + "dmn": "dimana", + "dmna": "dimana", + "dimn": "dimana", + "kmn": "kemana", + "kmna": "kemana", + "brp": "berapa", + "brpa": "berapa", + "kpn": "kapan", + "kpan": "kapan", + "sapa": "siapa", + "sp": "siapa", + "syp": "siapa", + "sypa": "siapa", + "apkh": "apakah", + "apakh": "apakah", } From c7aba51541921d1aee84389af469adb27260b052 Mon Sep 17 00:00:00 2001 From: Agung Hariadi Tedja Date: Wed, 22 Oct 2025 15:13:01 +0700 Subject: [PATCH 08/10] kaidah puebi --- tanya/specs.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tanya/specs.go b/tanya/specs.go index 8daee0e..604cf23 100644 --- a/tanya/specs.go +++ b/tanya/specs.go @@ -88,7 +88,7 @@ var intentTable = []IntentSpec{ }}, {IntentLocation, 55, []Rule{ {terms("dimana", "di mana", "kemana", "ke mana", "lokasi", "alamat"), 2, MatchTypeContains, 0}, - {terms(" kemana", " dimana", " alamat", " lokasi"), 2, MatchTypeEnds, 0}, + {terms(" kemana", " dimana", "di mana", " alamat", " lokasi"), 2, MatchTypeEnds, 0}, }}, {IntentTime, 55, []Rule{ {terms("kapan", "jadwal", "jam berapa", "pukul berapa"), 2, MatchTypeContains, 0}, @@ -120,11 +120,11 @@ var abbrevMap = map[string]string{ "bgmn": "bagaimana", "knp": "kenapa", "knpa": "kenapa", - "dmn": "dimana", - "dmna": "dimana", - "dimn": "dimana", - "kmn": "kemana", - "kmna": "kemana", + "dmn": "d imana", + "dmna": "di mana", + "dimn": "di mana", + "kmn": "ke mana", + "kmna": "ke mana", "brp": "berapa", "brpa": "berapa", "kpn": "kapan", From 76d43640b9a7e92a939d87acc3aeb5d2429f1ade Mon Sep 17 00:00:00 2001 From: Agung Hariadi Tedja Date: Wed, 22 Oct 2025 15:15:27 +0700 Subject: [PATCH 09/10] kaidah puebi --- tanya/specs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tanya/specs.go b/tanya/specs.go index 604cf23..8d294bb 100644 --- a/tanya/specs.go +++ b/tanya/specs.go @@ -120,7 +120,7 @@ var abbrevMap = map[string]string{ "bgmn": "bagaimana", "knp": "kenapa", "knpa": "kenapa", - "dmn": "d imana", + "dmn": "di mana", "dmna": "di mana", "dimn": "di mana", "kmn": "ke mana", From 5fb00af4dcf771643ee9bb6f92bb3d5a4dce3585 Mon Sep 17 00:00:00 2001 From: Agung Hariadi Tedja Date: Wed, 22 Oct 2025 15:16:04 +0700 Subject: [PATCH 10/10] bump: CHANGELOG.md --- CHANGELOG.md | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d231b46..407d907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,15 @@ # go-utils + +## [v1.42.0] - 2025-10-22 +### New Features +- identifier tanya + + -## [v1.41.0] - 2025-09-11 +## [v1.41.0] - 2025-09-17 ### New Features -- reverse slice +- reverse slice ([#71](https://github.com/kumparan/go-utils/issues/71)) @@ -207,6 +213,9 @@ - fix marshal issue on gorm.DeletedAt empty value ([#32](https://github.com/kumparan/go-utils/issues/32)) + +## [v.1.20.0] - 2022-03-11 + ## [v1.20.0] - 2022-03-11 ### New Features @@ -306,11 +315,11 @@ - add money formatter for multiple currencies ([#13](https://github.com/kumparan/go-utils/issues/13)) - -## [v1.8.0] - 2020-12-10 - ## [v1.7.1] - 2020-12-10 + + +## [v1.8.0] - 2020-12-10 ### New Features - add formatter for indonesian money and date @@ -375,7 +384,8 @@ - init go-utils -[Unreleased]: https://github.com/kumparan/go-utils/compare/v1.41.0...HEAD +[Unreleased]: https://github.com/kumparan/go-utils/compare/v1.42.0...HEAD +[v1.42.0]: https://github.com/kumparan/go-utils/compare/v1.41.0...v1.42.0 [v1.41.0]: https://github.com/kumparan/go-utils/compare/v1.40.2...v1.41.0 [v1.40.2]: https://github.com/kumparan/go-utils/compare/v1.40.1...v1.40.2 [v1.40.1]: https://github.com/kumparan/go-utils/compare/v1.40.0...v1.40.1 @@ -408,7 +418,8 @@ [v1.23.0]: https://github.com/kumparan/go-utils/compare/v1.22.0...v1.23.0 [v1.22.0]: https://github.com/kumparan/go-utils/compare/v1.21.0...v1.22.0 [v1.21.0]: https://github.com/kumparan/go-utils/compare/v1.20.1...v1.21.0 -[v1.20.1]: https://github.com/kumparan/go-utils/compare/v1.20.0...v1.20.1 +[v1.20.1]: https://github.com/kumparan/go-utils/compare/v.1.20.0...v1.20.1 +[v.1.20.0]: https://github.com/kumparan/go-utils/compare/v1.20.0...v.1.20.0 [v1.20.0]: https://github.com/kumparan/go-utils/compare/v1.19.3...v1.20.0 [v1.19.3]: https://github.com/kumparan/go-utils/compare/v1.19.2...v1.19.3 [v1.19.2]: https://github.com/kumparan/go-utils/compare/v1.19.1...v1.19.2 @@ -425,9 +436,9 @@ [v1.12.0]: https://github.com/kumparan/go-utils/compare/v1.11.0...v1.12.0 [v1.11.0]: https://github.com/kumparan/go-utils/compare/v1.10.0...v1.11.0 [v1.10.0]: https://github.com/kumparan/go-utils/compare/v1.9.0...v1.10.0 -[v1.9.0]: https://github.com/kumparan/go-utils/compare/v1.8.0...v1.9.0 -[v1.8.0]: https://github.com/kumparan/go-utils/compare/v1.7.1...v1.8.0 -[v1.7.1]: https://github.com/kumparan/go-utils/compare/v1.7.0...v1.7.1 +[v1.9.0]: https://github.com/kumparan/go-utils/compare/v1.7.1...v1.9.0 +[v1.7.1]: https://github.com/kumparan/go-utils/compare/v1.8.0...v1.7.1 +[v1.8.0]: https://github.com/kumparan/go-utils/compare/v1.7.0...v1.8.0 [v1.7.0]: https://github.com/kumparan/go-utils/compare/v1.6.0...v1.7.0 [v1.6.0]: https://github.com/kumparan/go-utils/compare/v1.5.0...v1.6.0 [v1.5.0]: https://github.com/kumparan/go-utils/compare/v1.4.0...v1.5.0