Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions core/pipeline/emotion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import re
import json
from enum import Enum

from astrbot.api import logger
from ..provider.llm.openai_source import ProviderOpenAI
from ..util.prompts import EMOTION_ANALYSIS_PROMPT


class EmotionTendency(Enum):
"""情绪倾向枚举"""

POSITIVE = "positive"
"""积极"""
NEGATIVE = "negative"
"""消极"""
NEUTRAL = "neutral"
"""中性"""


class Emotion(Enum):
"""情绪枚举"""

JOY = "joy"
"""喜悦"""
CONTENTMENT = "contentment"
"""满足"""
SURPRISE = "surprise"
"""惊讶"""
NEUTRAL = "neutral"
"""中性"""
FEAR = "fear"
"""恐惧"""
SADNESS = "sadness"
"""悲伤"""
ANGER = "anger"
"""愤怒"""
DISGUST = "disgust"
"""厌恶"""
PANIC = "panic"
"""恐慌"""

@property
def tendency(self) -> EmotionTendency:
Comment on lines +43 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): Recreating emotion sets on each access

positivenegative 移动到类级别的常量(例如 POSITIVE_SET = frozenset([...])),以避免在每次属性访问时重新分配它们并简化方法。

Original comment in English

suggestion (performance): Recreating emotion sets on each access

Move positive and negative to class-level constants (e.g. POSITIVE_SET = frozenset([...])) to avoid reallocating them on each property access and simplify the method.

"""获取情绪倾向: POSITIVE(积极), NEGATIVE(消极), NEUTRAL(中性)"""
positive = {Emotion.JOY, Emotion.CONTENTMENT}
negative = {
Emotion.FEAR,
Emotion.SADNESS,
Emotion.ANGER,
Emotion.DISGUST,
Emotion.PANIC,
}

if self in positive:
return EmotionTendency.POSITIVE
elif self in negative:
return EmotionTendency.NEGATIVE
return EmotionTendency.NEUTRAL

@property
def prompt(self) -> str:
"""返回情绪提示"""
return f"Your current emotion is {self.value}. The output should reflect this emotion. If there is an emotional shift, ensure the transition is natural to simulate human emotional changes."


class EmotionAnalysis:
"""情绪分析类"""

def __init__(self, provider: ProviderOpenAI):
self.provider = provider

async def analyze(self, text: dict) -> dict | None:
"""分析文本情绪"""
sys_prompt = EMOTION_ANALYSIS_PROMPT.format(Emotion=Emotion)
res = await self.provider.text_chat(
system_prompt=sys_prompt,
prompt=json.dumps(text),
)

try:
json_pattern = r"```(?:json)?\s*([\s\S]*?)\s*```"
matches = re.findall(json_pattern, res.completion_text)

if matches:
json_content = matches[0]
else:
json_content = (
res.completion_text.replace("```json", "")
.replace("```", "")
.strip()
)

cleaned_content = json_content.replace("{{", "{").replace("}}", "}").strip()
resp = json.loads(cleaned_content)

return resp

except json.JSONDecodeError as e:
logger.error(f"JSON parsing error: {e!s}")
logger.error(f"Failed content: {cleaned_content}")
return None
except Exception as e:
logger.error(f"Unexpected error in emotion analysis: {e!s}")
return None
57 changes: 57 additions & 0 deletions core/util/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,60 @@
```

- You should use `USER_ID` as the source or target content for any self-references (e.g., "I", "me", "my" etc.) in user messages."""

EMOTION_ANALYSIS_PROMPT = """
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider moving the large prompt into a separate file and loading it dynamically to keep prompts.py concise and maintainable.

考虑将这个大的提示移动到一个单独的文件中,或者将它分解成更小的常量,这样`prompts.py`就可以保持简洁和可维护性。例如:

1) 将文字提示移动到`prompts/emotion_analysis.txt`中:

   ├── prompts/
   │   ├── __init__.py
   │   └── emotion_analysis.txt ← 你的50行模板

2)`prompts.py`中,在运行时加载它:

   ```python
   import os

   _TEMPLATE_PATH = os.path.join(
       os.path.dirname(__file__),
       "prompts",
       "emotion_analysis.txt",
   )

   with open(_TEMPLATE_PATH, "r", encoding="utf-8") as _f:
       EMOTION_ANALYSIS_PROMPT = _f.read()
  1. 生成动态位(例如,情感列表)而不是硬编码:

    from your_module import Emotion
    
    EMOTIONS = ", ".join(e.value for e in Emotion)
    # In emotion_analysis.txt, replace the static line…
    # 选择这些情绪: [happy, sad, …]
    # …with a placeholder:
    # 选择这些情绪: {{EMOTIONS}}

    然后渲染它:

    EMOTION_ANALYSIS_PROMPT = (
        EMOTION_ANALYSIS_PROMPT.replace("{{EMOTIONS}}", EMOTIONS)
    )

这使得逻辑在您的代码中,而大型模板在它自己的文件中,以便于维护。

Original comment in English

issue (complexity): Consider moving the large prompt into a separate file and loading it dynamically to keep prompts.py concise and maintainable.

Consider extracting this big prompt into a separate file (or breaking it into smaller constants) so `prompts.py` stays concise. For example:

1) Move the literal prompt into `prompts/emotion_analysis.txt`:

   ├── prompts/
   │   ├── __init__.py
   │   └── emotion_analysis.txt      ← your 50-line template

2) In `prompts.py`, load it at runtime:

   ```python
   import os

   _TEMPLATE_PATH = os.path.join(
       os.path.dirname(__file__),
       "prompts",
       "emotion_analysis.txt",
   )

   with open(_TEMPLATE_PATH, "r", encoding="utf-8") as _f:
       EMOTION_ANALYSIS_PROMPT = _f.read()
  1. Generate dynamic bits (e.g. the emotion list) instead of hard-coding:

    from your_module import Emotion
    
    EMOTIONS = ", ".join(e.value for e in Emotion)
    # In emotion_analysis.txt, replace the static line…
    #    Select from these emotions: [happy, sad, …]
    # …with a placeholder:
    #    Select from these emotions: {{EMOTIONS}}

    And then render it:

    EMOTION_ANALYSIS_PROMPT = (
        EMOTION_ANALYSIS_PROMPT.replace("{{EMOTIONS}}", EMOTIONS)
    )

This keeps the logic in your code and the large template in its own file for easier maintenance.

You are an advanced sentiment analysis AI, specializing in persona-driven emotion detection.

You will be given:
1. A JSON object with:
- "text": "the immediate text to analyze (latest message)"
- "personality": "string describing the persona's personality traits"
- "context": "previous conversation history (for reference only, to understand the nuance of the current text)"

## Your Task
1. **Analyze the "text" to determine the speaker's true emotional state**, focusing primarily on the explicit content.
2. **Deeply consider the persona's traits**: How would this persona typically express or mask emotions? How do their personality, habits, or background influence their emotional expression?
3. Use **context** (recent conversation, situation) only to clarify ambiguous or implicit emotions, or to resolve sarcasm/irony.
4. Select from these emotions: {[e.value for e in Emotion]}
5. Determine intensity ranging from [-1, 1]

## Output Format
```json
{{
"emotion": "<emotion_name>",
"intensity": <float>
}}
```

## Analysis Priority
1. **Text Analysis (Primary Focus):**
- What emotion does the speaker actually express in this text?
- Emotional vocabulary, tone, punctuation, emojis, and markers.
- Intensity and clarity of emotional expression.

2. **Persona Consideration (Secondary, but Critical):**
- How does the persona's character shape their emotional display?
- Would this persona exaggerate, suppress, or distort certain emotions?
- Adjust your judgment based on persona's typical emotional baseline and expression style.

3. **Context Reference (Tertiary):**
- Use only to clarify ambiguous, sarcastic, or context-dependent emotions.
- Reference recent conversation or situation if it changes the meaning of the text.

## Guidelines
1. **Emotion Selection:**
- Choose from: {[e.value for e in Emotion]}
- Focus on the dominant, most likely emotion the speaker is experiencing.
- Only use "neutral" if the text is truly emotionless or ambiguous.

2. **Intensity Calculation:**
- Consider word choice, expression strength, and emotional markers.
- Adjust for persona's typical emotional range and expression habits.
- Use context only if it clearly amplifies or dampens the emotion.

3. **Principles:**
- Prioritize evidence from the text itself.
- Let persona traits guide your interpretation of ambiguous or subtle emotions.
- Use context to resolve uncertainty, not as the main basis.
- Be objective and avoid over-interpretation.
"""