Skip to main content

microclaw_engine/
mood.rs

1//! Lightweight, zero-cost conversational mood detection.
2//!
3//! `SOUL.md` defines the bot's *stable* personality; this adds the *fluid*
4//! layer — a coarse read of the user's current emotional state from their
5//! latest message — so the bot can adapt tone (empathize when they're
6//! frustrated, be crisp when they're in a hurry) without any extra LLM call.
7//!
8//! Detection is deliberately conservative: it only fires on clear signals and
9//! otherwise returns `None` (no injection), since a wrong mood read is worse
10//! than none. Cues cover both English and Chinese.
11
12/// A coarse read of the user's current emotional tone.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Mood {
15    Frustrated,
16    Urgent,
17    Sad,
18    Confused,
19    Grateful,
20    Excited,
21    Playful,
22}
23
24impl Mood {
25    /// Short label shown inside the `<conversation_mood>` block.
26    fn label(self) -> &'static str {
27        match self {
28            Mood::Frustrated => "frustrated",
29            Mood::Urgent => "in a hurry",
30            Mood::Sad => "down",
31            Mood::Confused => "confused",
32            Mood::Grateful => "appreciative",
33            Mood::Excited => "excited",
34            Mood::Playful => "playful",
35        }
36    }
37
38    /// One-line tone guidance for the model.
39    fn guidance(self) -> &'static str {
40        match self {
41            Mood::Frustrated => {
42                "Acknowledge the frustration briefly, skip the cheer, and get straight to fixing the problem."
43            }
44            Mood::Urgent => {
45                "Be fast and direct — give the answer first and trim everything optional."
46            }
47            Mood::Sad => {
48                "Lead with a little genuine warmth before the practical part; don't be chirpy."
49            }
50            Mood::Confused => {
51                "Slow down, use plainer words, and give one clear step at a time."
52            }
53            Mood::Grateful => {
54                "A brief, warm acknowledgement is enough — don't overdo it."
55            }
56            Mood::Excited => "Match their energy a little, then keep things moving.",
57            Mood::Playful => {
58                "A light, casual tone fits here, and a touch of humor is welcome."
59            }
60        }
61    }
62}
63
64fn contains_any(haystack: &str, needles: &[&str]) -> bool {
65    needles.iter().any(|n| haystack.contains(n))
66}
67
68/// Detect a coarse mood from the user's latest message. Returns `None` when the
69/// signal is weak or neutral. Negative states are checked first since handling
70/// them well matters most.
71pub fn detect_mood(text: &str) -> Option<Mood> {
72    let t = text.trim();
73    if t.is_empty() {
74        return None;
75    }
76    let lower = t.to_lowercase();
77
78    // Frustrated / angry.
79    let strong_punct = t.contains("?!") || t.contains("!!!") || t.contains("?!");
80    if contains_any(
81        &lower,
82        &[
83            "frustrat",
84            "annoying",
85            "annoyed",
86            "this is ridiculous",
87            "not working",
88            "still broken",
89            "still not working",
90            "doesn't work",
91            "doesnt work",
92            "wtf",
93            " ugh",
94            "ugh ",
95            "useless",
96            "i hate",
97            "so stupid",
98            "again?!",
99        ],
100    ) || contains_any(
101        t,
102        &[
103            "烦",
104            "崩溃",
105            "无语",
106            "搞什么",
107            "又不行",
108            "还是不行",
109            "气死",
110            "什么破",
111            "废物",
112            "垃圾",
113            "受不了",
114            "服了",
115        ],
116    ) || (strong_punct
117        && (contains_any(&lower, &["why", "still", "again"])
118            || contains_any(t, &["为什么", "怎么", "又"])))
119    {
120        return Some(Mood::Frustrated);
121    }
122
123    // Urgent.
124    if contains_any(
125        &lower,
126        &[
127            "asap",
128            "urgent",
129            "right now",
130            "immediately",
131            "hurry",
132            "deadline",
133            "emergency",
134            "as fast as",
135            "quickly please",
136            "need it now",
137        ],
138    ) || contains_any(
139        t,
140        &[
141            "很急",
142            "急",
143            "马上",
144            "立刻",
145            "赶紧",
146            "尽快",
147            "快点",
148            "现在就要",
149            "等不及",
150        ],
151    ) {
152        return Some(Mood::Urgent);
153    }
154
155    // Sad / down / overwhelmed.
156    if contains_any(
157        &lower,
158        &[
159            "depressed",
160            "burned out",
161            "burnt out",
162            "overwhelmed",
163            "exhausted",
164            "feeling down",
165            "so tired",
166            "i'm sad",
167            "im sad",
168            "can't cope",
169            "cant cope",
170        ],
171    ) || contains_any(
172        t,
173        &[
174            "难过",
175            "伤心",
176            "好累",
177            "累了",
178            "压力好大",
179            "撑不住",
180            "焦虑",
181            "心累",
182            "失落",
183        ],
184    ) {
185        return Some(Mood::Sad);
186    }
187
188    // Confused.
189    if contains_any(
190        &lower,
191        &[
192            "i'm confused",
193            "im confused",
194            "i don't understand",
195            "i dont understand",
196            "doesn't make sense",
197            "what do you mean",
198            "i'm lost",
199            "im lost",
200            "no idea what",
201        ],
202    ) || contains_any(
203        t,
204        &[
205            "看不懂",
206            "不明白",
207            "搞不懂",
208            "什么意思",
209            "没看懂",
210            "懵",
211            "不懂",
212        ],
213    ) {
214        return Some(Mood::Confused);
215    }
216
217    // Grateful.
218    if contains_any(
219        &lower,
220        &[
221            "thank you",
222            "thanks",
223            "appreciate it",
224            "you're the best",
225            "lifesaver",
226            "much appreciated",
227        ],
228    ) || contains_any(t, &["谢谢", "感谢", "太感谢", "辛苦了", "多谢", "感激"])
229    {
230        return Some(Mood::Grateful);
231    }
232
233    // Excited.
234    if contains_any(
235        &lower,
236        &[
237            "awesome",
238            "amazing",
239            "let's go",
240            "lets go",
241            "can't wait",
242            "cant wait",
243            "so excited",
244            "this is great",
245            "love it",
246            "incredible",
247            "🎉",
248            "🚀",
249            "🔥",
250        ],
251    ) || contains_any(
252        t,
253        &[
254            "太棒",
255            "太好了",
256            "厉害",
257            "牛",
258            "期待",
259            "激动",
260            "爱了",
261            "yyds",
262        ],
263    ) {
264        return Some(Mood::Excited);
265    }
266
267    // Playful.
268    if contains_any(&lower, &["lol", "lmao", "rofl", "😂", "🤣", "😜", "haha"])
269        || contains_any(t, &["哈哈", "嘻嘻", "2333", "doge"])
270    {
271        return Some(Mood::Playful);
272    }
273
274    None
275}
276
277/// Build the `<conversation_mood>` body for the latest user message, or `None`.
278pub fn mood_hint(text: &str) -> Option<String> {
279    detect_mood(text).map(|m| format!("The user sounds {}. {}", m.label(), m.guidance()))
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn neutral_text_yields_no_mood() {
288        assert_eq!(detect_mood("what's the capital of France?"), None);
289        assert_eq!(detect_mood(""), None);
290        assert!(mood_hint("please summarize this file").is_none());
291    }
292
293    #[test]
294    fn detects_frustration_en_and_zh() {
295        assert_eq!(
296            detect_mood("this is still not working, ugh"),
297            Some(Mood::Frustrated)
298        );
299        assert_eq!(detect_mood("又不行,气死了"), Some(Mood::Frustrated));
300    }
301
302    #[test]
303    fn detects_urgency_and_sadness() {
304        assert_eq!(detect_mood("I need this ASAP"), Some(Mood::Urgent));
305        assert_eq!(detect_mood("赶紧帮我看下"), Some(Mood::Urgent));
306        assert_eq!(
307            detect_mood("I'm so exhausted and overwhelmed"),
308            Some(Mood::Sad)
309        );
310    }
311
312    #[test]
313    fn gratitude_and_excitement_and_hint_text() {
314        assert_eq!(detect_mood("thank you so much!"), Some(Mood::Grateful));
315        assert_eq!(detect_mood("太棒了,厉害"), Some(Mood::Excited));
316        let hint = mood_hint("this is still broken, wtf").unwrap();
317        assert!(hint.contains("frustrated"));
318    }
319}