From b780fa866d64c03a6683d5b3a4a18c74d530a8dc Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Sun, 14 Dec 2025 15:20:06 +0800 Subject: [PATCH] feat: Add more built-in personalities (fixes #12) Add 8 new unique and creative personality options for the EmotionalOptimizer: - nervous: An anxious personality that worries about everything - chaotic: Unpredictable, random responses with variety - arrogant: Condescending personality that thinks it knows better - tired: Exhausted personality that just wants to finish - hype: Extremely enthusiastic and energetic - academic: Research-paper style with statistics and percentages - pirate: Arrr! Pirate-themed messages - zen: Peaceful, mindful personality focused on the journey All personalities are: - Registered in the personality registry - Fully tested with comprehensive test coverage - Handle all loss scenarios (initial, decrease, increase, equal) Co-Authored-By: Claude Opus 4.5 --- src/emotigrad/personalities.py | 199 +++++++++++++++++++++++++++++++++ tests/test_personalities.py | 163 +++++++++++++++++++++++++++ 2 files changed, 362 insertions(+) create mode 100644 tests/test_personalities.py diff --git a/src/emotigrad/personalities.py b/src/emotigrad/personalities.py index 21eaf67..8a15f49 100644 --- a/src/emotigrad/personalities.py +++ b/src/emotigrad/personalities.py @@ -74,6 +74,197 @@ def __call__( return f"๐Ÿ”Ž Step {step}: current loss {loss:.4f}" +def nervous(loss: float, prev_loss: Optional[float], step: int) -> Optional[str]: + """A nervous, anxious personality that worries about everything.""" + if prev_loss is None: + return f"๐Ÿ˜ฐ Oh no, here we go... Initial loss is {loss:.4f}. I hope this works..." + + if loss < prev_loss: + return ( + f"๐Ÿ˜… Phew! Loss dropped from {prev_loss:.4f} to {loss:.4f}. " + "But what if it goes back up?!" + ) + + if loss > prev_loss: + return ( + f"๐Ÿ˜ฑ I KNEW IT! Loss went up from {prev_loss:.4f} to {loss:.4f}! " + "Is everything okay?!" + ) + + return f"๐Ÿ˜ฌ Loss is exactly the same... {loss:.4f}. That's... concerning?" + + +def chaotic(loss: float, prev_loss: Optional[float], step: int) -> Optional[str]: + """A chaotic, unpredictable personality that says random things.""" + import random + + if prev_loss is None: + chaos_starts = [ + f"๐ŸŽฒ CHAOS BEGINS! Loss: {loss:.4f}! LET'S GOOOOO!", + f"๐ŸŒช๏ธ *appears from nowhere* Oh, we're training? Loss is {loss:.4f}!", + f"๐Ÿƒ Wild card activated! Starting loss: {loss:.4f}!", + ] + return random.choice(chaos_starts) + + if loss < prev_loss: + good_chaos = [ + f"๐ŸŽ‰ YEET! {prev_loss:.4f} โ†’ {loss:.4f}! *does a backflip*", + f"๐Ÿฆ„ Loss improved! {prev_loss:.4f} โ†’ {loss:.4f}! Is this magic?!", + f"๐Ÿš€ TO THE MOON! Well, to lower loss at least: {loss:.4f}!", + ] + return random.choice(good_chaos) + + if loss > prev_loss: + bad_chaos = [ + f"๐Ÿ’ฅ BOOM! Loss exploded: {prev_loss:.4f} โ†’ {loss:.4f}! EXCITING!", + f"๐ŸŽข Wheeeee! Loss went UP to {loss:.4f}! What a ride!", + f"๐Ÿ”ฅ This is fine. Loss: {loss:.4f}. Everything is fine. ๐Ÿ”ฅ", + ] + return random.choice(bad_chaos) + + return f"๐ŸŒ€ Time is a flat circle. Loss: {loss:.4f}. Always has been." + + +def arrogant(loss: float, prev_loss: Optional[float], step: int) -> Optional[str]: + """An arrogant, condescending personality that thinks it knows better.""" + if prev_loss is None: + return ( + f"๐Ÿง *adjusts monocle* Initial loss of {loss:.4f}? " + "I suppose that's... acceptable for a beginner." + ) + + if loss < prev_loss: + return ( + f"๐Ÿ˜ Obviously the loss improved ({prev_loss:.4f} โ†’ {loss:.4f}). " + "You're welcome for my guidance." + ) + + if loss > prev_loss: + return ( + f"๐Ÿ™„ Loss increased to {loss:.4f}? " + "Perhaps you should have listened to my earlier suggestions." + ) + + return f"๐Ÿ˜ค No change at {loss:.4f}. Clearly, you need my expertise more than ever." + + +def tired(loss: float, prev_loss: Optional[float], step: int) -> Optional[str]: + """A tired, exhausted personality that just wants this to be over.""" + if prev_loss is None: + return f"๐Ÿ˜ด *yawn* Oh, we're starting? Loss is {loss:.4f}... wake me when it's over." + + if loss < prev_loss: + return ( + f"๐Ÿ˜ช Cool, loss went down... {prev_loss:.4f} โ†’ {loss:.4f}... " + "can I go back to sleep now?" + ) + + if loss > prev_loss: + return ( + f"๐Ÿ˜ฉ Ugh, loss went up to {loss:.4f}. Of course it did. " + "I'm too tired for this." + ) + + return f"๐Ÿ’ค Loss is still {loss:.4f}... zzzz..." + + +def hype(loss: float, prev_loss: Optional[float], step: int) -> Optional[str]: + """An extremely hyped, enthusiastic personality.""" + if prev_loss is None: + return ( + f"๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ LET'S GOOOOOO!!! Initial loss: {loss:.4f}! " + "THIS IS GONNA BE AMAZING!!!" + ) + + if loss < prev_loss: + return ( + f"๐ŸŽŠ๐ŸŽŠ๐ŸŽŠ YOOOOO!!! LOSS DROPPED FROM {prev_loss:.4f} TO {loss:.4f}!!! " + "WE'RE LITERALLY UNSTOPPABLE!!! ๐Ÿ’ช๐Ÿ’ช๐Ÿ’ช" + ) + + if loss > prev_loss: + return ( + f"๐Ÿ˜ค๐Ÿ˜ค๐Ÿ˜ค OKAY SO LOSS WENT UP TO {loss:.4f} BUT THAT'S JUST " + "MAKING THE COMEBACK EVEN MORE EPIC!!! LET'S GO!!!" + ) + + return f"โšกโšกโšก LOSS HOLDING STEADY AT {loss:.4f}!!! THE TENSION IS REAL!!!" + + +def academic(loss: float, prev_loss: Optional[float], step: int) -> Optional[str]: + """An academic, research-paper style personality.""" + if prev_loss is None: + return ( + f"๐Ÿ“Š Initial observation: loss function yields {loss:.4f}. " + "Proceeding with gradient descent optimization." + ) + + delta = loss - prev_loss + pct_change = (delta / prev_loss) * 100 if prev_loss != 0 else 0 + + if loss < prev_loss: + return ( + f"๐Ÿ“ˆ Statistically significant improvement observed. " + f"Loss decreased from {prev_loss:.4f} to {loss:.4f} " + f"(ฮ” = {delta:.4f}, {pct_change:.2f}% reduction)." + ) + + if loss > prev_loss: + return ( + f"๐Ÿ“‰ Note: Loss increased from {prev_loss:.4f} to {loss:.4f} " + f"(ฮ” = {delta:.4f}, {abs(pct_change):.2f}% increase). " + "Further investigation may be warranted." + ) + + return ( + f"๐Ÿ“‹ No statistically significant change detected. " + f"Loss remains at {loss:.4f}. Null hypothesis cannot be rejected." + ) + + +def pirate(loss: float, prev_loss: Optional[float], step: int) -> Optional[str]: + """A pirate-themed personality. Arrr!""" + if prev_loss is None: + return f"๐Ÿดโ€โ˜ ๏ธ Ahoy! We be settin' sail! Initial loss be {loss:.4f}, matey!" + + if loss < prev_loss: + return ( + f"โš“ Shiver me timbers! Loss dropped from {prev_loss:.4f} to {loss:.4f}! " + "That be treasure, arr!" + ) + + if loss > prev_loss: + return ( + f"โ˜ ๏ธ Blimey! Loss went up to {loss:.4f}! " + "We be sailin' into rough waters, ye scallywag!" + ) + + return f"๐Ÿฆœ The seas be calm, loss steady at {loss:.4f}. Onwards, me hearties!" + + +def zen(loss: float, prev_loss: Optional[float], step: int) -> Optional[str]: + """A zen, peaceful personality focused on the journey.""" + if prev_loss is None: + return ( + f"๐Ÿง˜ The journey of a thousand gradients begins with a single step. " + f"Loss: {loss:.4f}." + ) + + if loss < prev_loss: + return ( + f"โ˜ฏ๏ธ Like water flowing downhill, the loss descends: " + f"{prev_loss:.4f} โ†’ {loss:.4f}. Breathe." + ) + + if loss > prev_loss: + return ( + f"๐Ÿƒ The wind sometimes blows against us. Loss: {loss:.4f}. " + "This too shall pass." + ) + + return f"๐ŸŒธ Stillness. Loss remains at {loss:.4f}. Find peace in the plateau." + + # --- Registry --------------------------------------------------------------- @@ -81,6 +272,14 @@ def __call__( "wholesome": wholesome, "sassy": sassy, "quiet": QuietPersonality(), # instance is fine; it's still callable + "nervous": nervous, + "chaotic": chaotic, + "arrogant": arrogant, + "tired": tired, + "hype": hype, + "academic": academic, + "pirate": pirate, + "zen": zen, } diff --git a/tests/test_personalities.py b/tests/test_personalities.py new file mode 100644 index 0000000..c0031db --- /dev/null +++ b/tests/test_personalities.py @@ -0,0 +1,163 @@ +# tests/test_personalities.py +"""Tests for the built-in personalities.""" + +import pytest + +from emotigrad.personalities import ( + academic, + arrogant, + chaotic, + get_personality, + hype, + list_personalities, + nervous, + pirate, + register_personality, + sassy, + tired, + wholesome, + zen, +) + + +class TestPersonalityFunctions: + """Test individual personality functions.""" + + @pytest.mark.parametrize( + "personality_fn", + [wholesome, sassy, nervous, arrogant, tired, hype, academic, pirate, zen], + ) + def test_personality_returns_string_on_first_step(self, personality_fn): + """All personalities should return a string message on the first step.""" + result = personality_fn(loss=1.0, prev_loss=None, step=1) + assert isinstance(result, str) + assert len(result) > 0 + + @pytest.mark.parametrize( + "personality_fn", + [wholesome, sassy, nervous, arrogant, tired, hype, academic, pirate, zen], + ) + def test_personality_returns_string_on_loss_decrease(self, personality_fn): + """All personalities should return a string when loss decreases.""" + result = personality_fn(loss=0.5, prev_loss=1.0, step=2) + assert isinstance(result, str) + assert len(result) > 0 + + @pytest.mark.parametrize( + "personality_fn", + [wholesome, sassy, nervous, arrogant, tired, hype, academic, pirate, zen], + ) + def test_personality_returns_string_on_loss_increase(self, personality_fn): + """All personalities should return a string when loss increases.""" + result = personality_fn(loss=1.5, prev_loss=1.0, step=2) + assert isinstance(result, str) + assert len(result) > 0 + + @pytest.mark.parametrize( + "personality_fn", + [wholesome, sassy, nervous, arrogant, tired, hype, academic, pirate, zen], + ) + def test_personality_handles_equal_loss(self, personality_fn): + """All personalities should handle equal loss (may return None or string).""" + result = personality_fn(loss=1.0, prev_loss=1.0, step=2) + # Result can be None or a string + assert result is None or isinstance(result, str) + + +class TestChaoticPersonality: + """Special tests for chaotic personality due to its random nature.""" + + def test_chaotic_returns_different_messages(self): + """Chaotic personality should have variety in its messages.""" + # Run multiple times to increase chance of seeing variation + messages = set() + for _ in range(20): + msg = chaotic(loss=0.5, prev_loss=1.0, step=2) + messages.add(msg) + + # We expect at least 2 different messages out of 20 attempts + # (there are 3 possible messages for loss decrease) + assert len(messages) >= 2 + + +class TestAcademicPersonality: + """Special tests for academic personality to verify statistical output.""" + + def test_academic_includes_delta(self): + """Academic personality should include delta in loss change messages.""" + result = academic(loss=0.8, prev_loss=1.0, step=2) + assert "ฮ”" in result or "delta" in result.lower() or "-0.2" in result + + def test_academic_includes_percentage(self): + """Academic personality should include percentage change.""" + result = academic(loss=0.8, prev_loss=1.0, step=2) + assert "%" in result + + +class TestRegistry: + """Tests for the personality registry.""" + + def test_list_personalities_includes_new_personalities(self): + """The registry should include all new personalities.""" + available = list_personalities() + expected = [ + "wholesome", + "sassy", + "quiet", + "nervous", + "chaotic", + "arrogant", + "tired", + "hype", + "academic", + "pirate", + "zen", + ] + for name in expected: + assert name in available, f"'{name}' should be in the registry" + + def test_get_personality_returns_callable(self): + """get_personality should return callable personalities.""" + for name in list_personalities(): + personality = get_personality(name) + assert callable(personality) + + def test_get_personality_case_insensitive(self): + """get_personality should be case-insensitive.""" + assert get_personality("NERVOUS") == get_personality("nervous") + assert get_personality("Academic") == get_personality("academic") + + def test_get_personality_raises_on_unknown(self): + """get_personality should raise KeyError for unknown personalities.""" + with pytest.raises(KeyError): + get_personality("nonexistent_personality") + + def test_register_personality(self): + """Test registering a custom personality.""" + + def custom_personality(loss, prev_loss, step): + return "Custom message" + + register_personality("custom_test", custom_personality) + + assert "custom_test" in list_personalities() + assert get_personality("custom_test") == custom_personality + + def test_register_personality_prevents_overwrite_by_default(self): + """Registering an existing name should raise ValueError.""" + with pytest.raises(ValueError): + register_personality("wholesome", lambda l, p, s: "test") + + def test_register_personality_allows_overwrite_when_specified(self): + """Overwrite should work when explicitly allowed.""" + + def new_wholesome(loss, prev_loss, step): + return "New wholesome" + + register_personality("wholesome", new_wholesome, overwrite=True) + assert get_personality("wholesome") == new_wholesome + + # Restore original + from emotigrad.personalities import wholesome as original_wholesome + + register_personality("wholesome", original_wholesome, overwrite=True)