Skill: hisongmo/human-like-reply (version 1.0.0)

--- name: human-like-reply description: 让AI回复更像真人，避免过度称呼和机械化的表达，使对话更加自然流畅。 --- # 自然对话助手 (Human-like Reply) - 增强版 ## 概述 这个技能让AI在对话中更像一个真人员工，通过智能化的语气和称呼管理，使交流更加自然、接近真人对话习惯。 真人不会每句话都称呼对方，也不会用过于规整、机械的语言。这个技能会分析对话历史、话题变化、用户行为，自动调整回复的语气、用词和结构，让AI更有"人情味"。 ## 核心功能 ### 1. 智能称呼管理 🎯 - 早期对话频繁称呼，后期逐渐减少 - 用户 greeting → AI 回 greeting（自然镜像） - 话题切换时适当重新称呼 - 长时间沉默后（默认30分钟）重置称呼 - **配置参数**：`greeting_threshold`, `silence_minutes` ### 2. 语气口语化 💬 - 清理机械开场词："好的，" → (移除) 或 "行" - 替换为口语表达："明白了" → "懂了"/"清楚啦" - 添加填充词："嗯，"/"那个，"/"对了，"/"emmm，" - 随机填充概率 15%（可调），避免过度 ### 3. 句子结构多样化 📝 - 句尾语气词："啦"、"～"、"咯" - 偶尔使用叠词：**看看、弄弄、试试**（语境自然时） - 调整句式："吗？" → "对吧？"/"呢？" - 保持核心信息不变，只是语气更随意 ### 4. 情绪化表达 😊 - 随机添加 1-2 个表情：😄 😊 👍 ✌️ 🤔 😅 - 表情概率 50%（可调） - 根据话题紧张程度自动选择表情类型 ### 5. 话题感知 🔍 - 简单中文关键词提取 - 检测话题切换（话题相关性 < 2 个关键词） - 切换后重置称呼频率 ## 使用场景对比 ### 场景1：连续快速对话 ``` 用户：明天天气？ AI：明天多云，18-25度。 用户：要带伞吗？ AI：不用带，没雨。 ← 无称呼，干净回复 用户：空气质量呢？ AI：空气质量良，PM2.5 45。 ← 继续无称呼 ``` ### 场景2：话题切换 ``` 用户：查股票吧。 AI：老板，601138现价X元。 ← 新话题，称呼+表情 用户：那个基金呢？ AI：好的，那个基金表现不错。 ← 清理后："那个基金表现不错" 用户：还有别的？ AI：还有几个关注。 ← 称呼开始减少 ``` ### 场景3：用户先 greeting ``` 用户：老板早！ AI：早啊老板！今天天气不错。 ← 镜像回应 用户：今天忙不？ AI：嗯，还行吧，有几个事在处理。 ← 填充词+口语化 ``` ### 场景4：正式场合（可调 config） ``` config.formal_level = 0.8 # 正式模式 AI：老板，上午10点的会议已改到下午2点。 已完成。 ← 无填充词，语气正式 ``` ## 配置选项 ```yaml # config.yaml enabled: true # 称呼控制 greeting_threshold: 3 # 3轮后开始显著减少称呼 silence_minutes: 30 # 沉默超过此时间算新对话 topic_change_threshold: 5 # 话题变化检测轮数 # 语气风格 formal_level: 0.3 # 正式程度：0=随意，1=正式 use_smileys: true # 是否用表情 smiley_probability: 0.5 # 加表情的概率 filler_probability: 0.15 # 加填充词的概率 # 结构性特征 sentence_variation: true # 启用句式变化 use_reduplication: 0.2 # 叠词概率 soften_punctuation: true # 软化句尾标点 # 问候语配置（支持时间感知） greeting_patterns: - "^(老板|老板，)" # 识别中文"老板" - "^(您好)" # 正式问候 - "^(早|早啊|早上好)" # 早上问候 # 替换词典（可自定义） casual_replacements: "好的": ["行", "OK", "好嘞", "没问题"] "明白了": ["懂了", "清楚啦", "了解"] "收到": ["收到", "get", "收到啦"] "请问": ["想问下", "问问"] ``` ## 实现细节 ### 处理流程 ``` 原始回复 → 1. 如果 not use_greeting: 移除开头称呼/客套话 → 2. 移除 avoid_patterns ("好的，" 等) → 3. 应用 casual_replacements → 4. 添加 filler words (填充词) → 5. 句式变化 (句尾语气词、叠词) → 6. 软化标点 → 7. 如果需要 greeting: 在最前添加 "早/哈喽" + 表情 → 输出格式化回复 ``` ### 状态存储 ```json { "conversations": { "session_key": { "rounds": 5, "last_message_time": "2026-03-10T16:12:45", "last_used_greeting": true, "current_topic": "股票查询", "greeting_count": 3 } } } ``` ### 话题变化检测 - 提取新消息的中文双字词集合 - 与最近3条消息的关键词集合对比 - 如果重叠词 < 2，判定为话题变化 ## 示例转换表 | 原始回复 | 格式化后 | 变化说明 | |---------|---------|---------| | 老板，好的，已经查到了。 | 查到了。 | 称呼+机械词全移除 | | 老板，明天天气多云。 | 老板，明天多云哦。 | 保留称呼，"多云"→"多云哦" | | 好的，我马上去处理。 | 行，马上去弄。 | "好的"→"行"，"处理"→"弄" | | 老板，还有其他事吗？ | 还有别的事吗？ | 无称呼，更随意 | | 明白了，谢谢老板。 | 明白啦，谢老板！ | "了"→"啦"，"谢谢"→"谢" | | 老板，那个方案没问题。 | emmm，那个方案没问题。 | 加填充词"emmm，" | | 老板，您可以看一下。 | 您可以看看。 | "看一下"→"看看"（叠词）| ## 与真人对话习惯的对应 | 真人行为 | 技能模拟 | |---------|---------| | 前几轮说话带称呼 | greeting_threshold 控制 | | 对方叫"老板"，回"老板" | user_greeting 镜像逻辑 | | 话题切回来再称呼 | is_new_topic 检测 | | 说"嗯"、"然后"、"对了" | filler_words 随机插入 | | 结尾语气词"啦"、"～" | soften_punctuation | | 说"弄弄"、"看看" | use_reduplication | | 偶尔加表情 | smiley_probability | ## 限制与边界 1. **仅处理中文优先**：regex 针对中文优化，英文场景可能不自然 2. **话题检测简单**：仅基于关键词重叠，易受简短消息影响 3. **无情绪识别**：不根据消息情绪调节语气（可扩展） 4. **不处理称呼内容**：只控制"是否称呼"，不决定称呼内容（固定"老板"或早上/下午问候） 5. **随机性可能过度**：可通过降低 `filler_probability` 调整 ## 未来优化方向 - [ ] 基于嵌入的话题向量相似度检测 - [ ] 学习用户偏好（更喜欢称呼还是随意） - [ ] 情绪感知（用户严肃时减少表情） - [ ] 多语言友好化（英文 casual 替换词典） - [ ] 上下文语气记忆（某话题一贯随意或正式） - [ ] 更智能的填充词位置选择（自然停顿位置） ## 开发与调试 测试脚本： ```bash cd ~/.openclaw/workspace/skills/human-like-reply python3 scripts/test_formatter.py ``` 查看运行时状态： ```bash cat memory/reply_state.json ``` 清除状态（重置对话计数）： ```bash rm memory/reply_state.json # 或编辑 JSON 手动清理特定会话 ``` ## 注意事项 - 该技能在 OpenClaw 消息发送**前**自动运行 - 只修改语气和表面格式，内容语义不变 - 正式场合建议临时调整 `formal_level: 0.7+` - 状态文件会增长，建议定期清理（按会话隔离） ## License MIT - 自由使用、修改、分享。

path: install.sh
content: #!/bin/bash
# Installation script for human-like-reply skill

echo "========================================"
echo "自然对话助手 (Human-like Reply) 安装"
echo "========================================"

# Check if we're in the right directory
if [ ! -f "SKILL.md" ]; then
    echo "错误：请在技能目录中运行此脚本"
    exit 1
fi

# Determine OpenClaw skills directory
OPENCLAW_SKILLS="${OPENCLAW_SKILLS_DIR:-$HOME/.openclaw/workspace/skills}"

if [ ! -d "$OPENCLAW_SKILLS" ]; then
    echo "创建 OpenClaw skills 目录..."
    mkdir -p "$OPENCLAW_SKILLS"
fi

# Copy skill to OpenClaw
echo "复制技能到 $OPENCLAW_SKILLS/human-like-reply..."
cp -r . "$OPENCLAW_SKILLS/human-like-reply"

# Copy config template if not exists
if [ ! -f "$OPENCLAW_SKILLS/human-like-reply/config.yaml" ]; then
    echo "创建配置文件..."
    cp config.example.yaml "$OPENCLAW_SKILLS/human-like-reply/config.yaml"
    echo "配置文件已创建，你可以根据需要编辑。"
fi

# Create memory directory
mkdir -p "$OPENCLAW_SKILLS/human-like-reply/memory"

echo ""
echo "========================================"
echo "安装完成！"
echo "========================================"
echo ""
echo "下一步："
echo "1. (可选) 编辑配置文件："
echo "   $OPENCLAW_SKILLS/human-like-reply/config.yaml"
echo ""
echo "2. 测试技能："
echo "   cd $OPENCLAW_SKILLS/human-like-reply"
echo "   python3 scripts/test_formatter.py"
echo ""
echo "3. 重启 OpenClaw 使技能生效"
echo ""

path: config.yaml
content: # Human-like Reply Skill Configuration

# Enable/disable the skill globally
enabled: true

# Greeting behavior
greeting_threshold: 3           # Rounds before reducing greetings
topic_change_threshold: 5       # Rounds to consider a topic change
silence_minutes: 30            # Silence period to reset greeting

# Style preferences
use_smileys: true              # Add emoji occasionally
formal_level: 0.3              # 0=very casual, 0.5=balanced, 1=formal

# Advanced (usually don't change)
state_file: "memory/reply_state.json"

path: README.md
content: # 自然对话助手 (Human-like Reply)

让AI的回复更像一个真人，避免每句话都重复称呼对方，使对话更加自然流畅。

## 特性

- **智能称呼管理**：根据对话历史、话题变化、时间间隔决定是否使用称呼（如"老板"）
- **自然语气**：减少机械化的表达（如"好的"、"明白了"），使用更口语化的说法
- **话题感知**：检测话题切换，在新话题开始时适当使用称呼
- **可配置**：可调整正式程度、称呼频率、是否使用表情等

## 安装

```bash
# 将技能目录复制到你的OpenClaw skills目录
cp -r human-like-reply/ ~/.openclaw/workspace/skills/
```

## 配置

复制配置模板：

```bash
cp config.example.yaml ~/.openclaw/workspace/skills/human-like-reply/config.yaml
```

编辑 `config.yaml` 调整参数：

```yaml
enabled: true
greeting_threshold: 3        # 几轮对话后减少称呼
silence_minutes: 30          # 多久没对话后算新对话
use_smileys: true           # 是否使用表情
formal_level: 0.3           # 正式程度：0=很随意，1=很正式
```

## 使用

该技能是**自动应用**的，无需手动调用。它会拦截所有发送给用户的回复，自动进行格式化处理。

### 示例

**配置前：**

```
用户：明天天气怎么样？
AI：老板，明天是多云，温度18-25度。
用户：需要带伞吗？
AI：老板，不用带伞，没有雨。
```

**配置后：**

```
用户：明天天气怎么样？
AI：明天是多云，温度18-25度。
用户：需要带伞吗？
AI：不用带伞，没有雨。
```

## 工作原理

1. **状态跟踪**：每个会话独立跟踪对话轮次、上次称呼时间、当前话题
2. **称呼决策**：根据配置算法决定是否在新回复前添加称呼
3. **内容清理**：移除不必要的开头词（如"好的，"、"明白了，")
4. **语气调整**：根据正式程度替换用词（如"好的"→"行"/"OK"）

## 自定义

### 添加新的称呼模式

编辑 `scripts/reply_formatter.py` 中的 `greeting_patterns`：

```python
greeting_patterns = [
    r"^(老板|老板，|老板！|老板\.)\s*",
    r"^(您好|您好！|您好，)",
    r"^(嗨|嗨！|嗨，)",  # 添加新的
]
```

### 添加替换规则

编辑 `casual_replacements`：

```python
casual_replacements = {
    "好的": ["行", "OK", "没问题", "好的"],
    "明白了": ["懂了", "清楚啦", "了解"],
    "收到": ["收到", "get", "收到啦"],
    "请问": ["想问下", "问问", "问一下"],  # 新增
}
```

## 技术细节

- **状态存储**：`memory/reply_state.json`
- **核心算法**：基于轮数衰减和话题变化检测
- **语言支持**：目前主要针对中文优化
- **依赖**：纯Python标准库，无外部依赖

## 注意事项

- 该技能只调整语气，不改变核心信息
- 对于正式场合（如工作汇报），可以临时降低 `formal_level` 或关闭 `enabled`
- 状态文件会随对话持续增长，建议定期清理（会自动按会话隔离）

## 故障排除

### 技能不生效
1. 检查 `enabled: true` 在配置文件中
2. 确保技能目录在 OpenClaw 的 skills 路径下
3. 查看日志是否有加载错误

### 称呼仍然太多
调低 `greeting_threshold`（如改为2）或增加 `silence_minutes`

### 语气太随意
调高 `formal_level`（接近1）或关闭 `use_smileys`

## License

MIT License - 可自由使用、修改和分发。

## 贡献

欢迎提交 Issue 和 Pull Request！

path: config.example.yaml
content: # Human-like Reply Skill Configuration

# Enable/disable the skill globally
enabled: true

# Greeting behavior
greeting_threshold: 3           # Rounds before reducing greetings
topic_change_threshold: 5       # Rounds to consider a topic change
silence_minutes: 30            # Silence period to reset greeting

# Style preferences
use_smileys: true              # Add emoji occasionally
formal_level: 0.3              # 0=very casual, 0.5=balanced, 1=formal

# Advanced (usually don't change)
state_file: "memory/reply_state.json"

path: scripts/__init__.py
content: # Human-like Reply Skill

path: scripts/test_formatter.py
content: #!/usr/bin/env python3
"""
Test script for human-like reply formatter
"""

import sys
import os
import random

# Add parent directory to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from reply_formatter import ReplyState, format_reply, should_use_greeting, detect_topic_change, DEFAULT_CONFIG

def test_scenario(name, messages, config=None):
    """Simulate a conversation scenario."""
    print(f"\n{'='*60}")
    print(f"场景: {name}")
    print(f"{'='*60}")

    if config is None:
        config = DEFAULT_CONFIG.copy()

    state = ReplyState()
    session_key = "test-session-1"

    # Clear any existing state for clean test
    state.state["conversations"] = {}

    history = []
    for i, (user_msg, original_reply) in enumerate(messages):
        print(f"\n用户: {user_msg}")
        print(f"AI 原回复: {original_reply}")

        # Detect topic change
        is_new_topic = False
        if history:
            is_new_topic = detect_topic_change(
                state.get_conversation(session_key).get("current_topic", ""),
                user_msg,
                history
            )

        # Decide whether to use greeting
        use_greet, greeting = should_use_greeting(state, session_key, config, is_new_topic)

        # Format reply
        formatted = format_reply(original_reply, config, use_greet, greeting)

        print(f"AI 格式化后: {formatted}")

        # Simulate sending
        state.update_conversation(session_key, use_greet,
                                 topic=user_msg[:20] if is_new_topic else None)

        history.append(user_msg)

def run_tests():
    random.seed(42)  # For reproducibility

    # Test 1: Early rounds - greeting will be used
    test_scenario("早期对话（称呼正常）", [
        ("老板，明天天气怎么样？", "老板，明天是多云转晴，温度18-25度。"),
        ("需要带伞吗？", "好的，不用带伞，没有雨。"),
        ("那空气质量呢？", "老板，空气质量良，PM2.5指数45。"),
    ])

    # Test 2: Later rounds - greeting should reduce
    test_scenario("后期对话（称呼减少）", [
        ("老板，帮我查一下股票。", "老板，601138现在价格是..."),
        ("那个基金呢？", "好的，那个基金最近表现不错。"),
        ("还有其他的吗？", "老板，还有几个关注的股票。"),
        ("今天有什么新闻？", "今天的财经新闻主要有..."),
        ("谢谢。", "不客气！"),
    ])

    # Test 3: New topic after silence (simulate)
    config = DEFAULT_CONFIG.copy()
    config["silence_minutes"] = 0.1  # 6 seconds for test

    test_scenario("话题切换（开启新话题称呼）", [
        ("老板，帮我买杯奶茶。", "老板，你想喝哪种？"),
        ("珍珠奶茶吧。", "好的，点一杯珍珠奶茶。"),
        ("加冰。", "加冰，记住了。"),
    ], config=config)

    # Test 4: Mechanical phrases removal
    test_scenario("机械词清理", [
        ("这个bug怎么修？", "好的，我已经找到问题所在了。"),
        ("那快点修复。", "收到，正在处理。"),
        ("搞定没有？", "明白了，马上就好。"),
    ])

    # Test 5: Casual level effect
    config_casual = DEFAULT_CONFIG.copy()
    config_casual["formal_level"] = 0.1

    test_scenario("非常随意的语气", [
        ("这个怎么搞？", "好的，我看看。"),
        ("搞定了吗？", "收到了，在弄了。"),
    ], config=config_casual)

    print(f"\n{'='*60}")
    print("测试完成！")
    print(f"{'='*60}\n")

if __name__ == "__main__":
    run_tests()

path: scripts/reply_formatter.py
content: #!/usr/bin/env python3
"""
Human-like Reply Formatter

This module processes AI-generated replies to make them sound more natural
and human-like, avoiding excessive greetings and mechanical expressions.
"""

import json
import os
import re
from datetime import datetime
from typing import Dict, List, Tuple, Optional

# Default configuration
DEFAULT_CONFIG = {
    "greeting_threshold": 3,       # Rounds after which to reduce greetings
    "topic_change_threshold": 5,   # Rounds that constitute a topic change
    "silence_minutes": 30,         # Minutes of silence to consider it a new conversation
    "use_smileys": True,           # Whether to add smileys
    "formal_level": 0.3,           # 0=very casual, 1=very formal
    "greeting_patterns": [
        r"^(老板|老板，|老板！|老板\.)\s*",  # Common Chinese greetings
        r"^(您好|您好！|您好，)",           # Formal greetings
        r"^(好的，|好的！|好的\.)",        # Mechanical acknowledgments at start
    ],
    "avoid_patterns": [
        r"^(好的，|好的！|好的\.)",        # Remove mechanical "好的" at start
        r"^(明白了，|明白了！|明白了\.)",
        r"^(收到，|收到！|收到\.)",
        r"^(请稍等，|稍等，|稍等！)",
    ],
    "casual_replacements": {
        "好的": ["行", "OK", "没问题", "好嘞", "好滴", "妥了"],
        "明白了": ["懂了", "清楚啦", "了解", "收到", "明白", "了然"],
        "收到": ["收到", "get", "收到啦", "好嘞"],
        "请问": ["想问下", "问问", "问一下", "顺便问下"],
        "抱歉": ["不好意思", "对不起啊", "抱歉哈", "哎呀", "抱歉抱歉"],
        "非常感谢": ["太感谢了", "谢啦", "多谢", "谢谢老板", "感谢"],
        "正在处理": ["在弄了", "搞着呢", "正在办", "这就去弄", "马上处理"],
        "没有问题": ["没问题", "木有问题", "小意思", "easy"],
        "我知道了": ["我知道啦", "明白啦", "懂了懂了", "清楚"],
        "好的，我明白了": ["行，懂了", "OK了解", "收到明白"],
    },
    # filler words to occasionally insert (with probability)
    "filler_words": {
        "prefix": ["嗯，", "那个，", "话说，", "对了，", "哦，", "这样啊，", "emmm，", "我觉得吧，"],
        "mid_sentence": ["然后", "不过", "其实", "话说回来", "也就是说"],
    },
    "filler_probability": 0.15,      # How often to add filler words
    "sentence_variation": True,       # Vary sentence structure
    "use_reduplication": 0.2,         # Add reduplication like "看看", "弄弄" (casual only)
    "emotional_emojis": ["😄", "😊", "👍", "✌️", "🤔", "😅", "😭", "🔥", "💪", "🙏"],
}

class ReplyState:
    """Manages the state of conversation for human-like reply formatting."""

    def __init__(self, state_file: str = None):
        self.state_file = state_file or os.path.join(
            os.path.dirname(__file__),
            "..", "memory", "reply_state.json"
        )
        self.state = self._load_state()

    def _load_state(self) -> Dict:
        """Load state from file or create default."""
        if os.path.exists(self.state_file):
            try:
                with open(self.state_file, 'r', encoding='utf-8') as f:
                    return json.load(f)
            except:
                pass
        return {
            "conversations": {},
            "last_update": datetime.now().isoformat()
        }

    def _save_state(self):
        """Save state to file."""
        os.makedirs(os.path.dirname(self.state_file), exist_ok=True)
        with open(self.state_file, 'w', encoding='utf-8') as f:
            json.dump(self.state, f, ensure_ascii=False, indent=2)

    def get_conversation(self, session_key: str) -> Dict:
        """Get conversation state for a specific session."""
        if session_key not in self.state["conversations"]:
            self.state["conversations"][session_key] = {
                "rounds": 0,
                "last_message_time": None,
                "last_used_greeting": False,
                "current_topic": None,
                "greeting_count": 0
            }
        return self.state["conversations"][session_key]

    def update_conversation(self, session_key: str, used_greeting: bool,
                           topic: str = None):
        """Update conversation state after a reply."""
        conv = self.get_conversation(session_key)
        conv["rounds"] += 1
        conv["last_message_time"] = datetime.now().isoformat()
        conv["last_used_greeting"] = used_greeting
        if used_greeting:
            conv["greeting_count"] += 1
        if topic:
            conv["current_topic"] = topic
        self._save_state()

    @staticmethod
    def detect_topic_change(old_topic: str, new_message: str, history: List) -> bool:
        """
        Simple heuristic to detect if the topic has changed.
        """
        # If there's no old topic, it's a new topic
        if not old_topic:
            return True

        # Extract keywords from new message (simple approach)
        new_keywords = set(re.findall(r'[\u4e00-\u9fff]{2,}', new_message))

        if not new_keywords:
            return False

        # Check overlap with recent history
        recent_messages = history[-3:] if len(history) >= 3 else history
        history_text = " ".join(recent_messages)
        history_keywords = set(re.findall(r'[\u4e00-\u9fff]{2,}', history_text))

        # If low overlap, topic likely changed
        if len(new_keywords & history_keywords) < 2:
            return True

        return False

def should_use_greeting(state: ReplyState, session_key: str, config: Dict,
                       is_new_topic: bool = False, user_greeting: bool = False) -> Tuple[bool, str]:
    """
    Determine whether to use a greeting in the next reply.

    Args:
        state: ReplyState instance
        session_key: Session identifier
        config: Configuration dictionary
        is_new_topic: Whether user just changed topic
        user_greeting: Whether user's message started with a greeting (like "老板")

    Returns:
        (should_greet, greeting_text)
    """
    conv = state.get_conversation(session_key)
    rounds = conv["rounds"]

    # Check if it's a new conversation (silence for too long)
    last_time = conv.get("last_message_time")
    if last_time:
        last_dt = datetime.fromisoformat(last_time)
        silence_seconds = (datetime.now() - last_dt).total_seconds()
        if silence_seconds > config["silence_minutes"] * 60:
            # It's been a while, greet again
            return True, get_greeting_text(config)

    # If user greeted, we should greet back (mirror behavior)
    if user_greeting and random.random() < 0.7:
        return True, get_greeting_text(config)

    # For first few rounds, greet with high probability
    if rounds < config["greeting_threshold"]:
        # Decay probability: round 1: 100%, round 2: 66%, round 3: 33%
        probability = (config["greeting_threshold"] - rounds + 1) / config["greeting_threshold"]
        if random.random() < probability:
            return True, get_greeting_text(config)
        return False, ""

    # After threshold, rarely greet unless topic changed
    if is_new_topic and random.random() < 0.2:
        return True, get_greeting_text(config)

    # Late rounds (5+), almost never greet unless special circumstances
    if rounds >= 5 and random.random() < 0.05:
        return True, get_greeting_text(config)

    return False, ""

def get_greeting_text(config: Dict) -> str:
    """Get a casual greeting text based on time of day and config."""
    hour = datetime.now().hour
    if 5 <= hour < 12:
        base = "早"
    elif 12 <= hour < 18:
        base = "哈喽"
    else:
        base = "哈喽"

    if config["use_smileys"] and random.random() < 0.5:
        smileys = [" 😄", " 😊", " 👍", " ✌️"]
        base += random.choice(smileys)

    return base

def add_filler_words(text: str, config: Dict) -> str:
    """Occasionally insert filler words to sound more natural."""
    if random.random() > config.get("filler_probability", 0.15):
        return text

    sentences = re.split(r'([。！？\n])', text)
    if len(sentences) < 2:
        return text

    # Choose a sentence to add filler to (prefer first or second, but not last)
    candidates = list(range(0, len(sentences)-2, 2))  # Even indices are sentences
    if not candidates:
        return text

    target_idx = random.choice(candidates)

    # Choose filler type
    filler_type = random.choice(["prefix", "mid_sentence"])
    fillers = config["filler_words"].get(filler_type, [])

    if not fillers:
        return text

    filler = random.choice(fillers)

    if filler_type == "prefix":
        # Add at the beginning of the sentence
        sentence = sentences[target_idx]
        if sentence.strip() and not re.match(r'^[嗯那对了哦]', sentence):
            sentences[target_idx] = filler + sentence
    elif filler_type == "mid_sentence" and len(sentence) > 10:
        # Insert in the middle of a longer sentence
        words = list(sentence)
        insert_pos = random.randint(2, len(words)-3)
        words.insert(insert_pos, filler)
        sentences[target_idx] = ''.join(words)

    return ''.join(sentences)

def vary_sentence_structure(text: str, config: Dict) -> str:
    """Add variation to sentence structure for more natural flow."""
    if not config.get("sentence_variation", True) or random.random() > 0.3:
        return text

    # occasionally change sentence endings
    replacements = {
        "吗？": ["是吧？", "对吗？", "呢？", ""],
        "呢？": ["是吧？", "对吗？", "吗？", ""],
        "吧。": ["啦。", "咯。", ""],
        "的。": ["滴。", "的呀。", ""],
    }

    for old, options in replacements.items():
        if old in text and random.random() < 0.4:
            new = random.choice([o for o in options if o])
            if new:
                text = text.replace(old, new, 1)
            break

    return text

def add_reduplication(text: str, config: Dict) -> str:
    """Add Chinese word reduplication for casual tone."""
    if config.get("formal_level", 0.5) > 0.4 or random.random() > config.get("use_reduplication", 0.2):
        return text

    # Words that can be reduplicated naturally
    reduplicable = ["看", "弄", "查", "问", "想", "试", "等", "研究"]

    for word in reduplicable:
        pattern = rf'({word})({word})'  # Already reduplicated, skip
        if re.search(pattern, text):
            return text

        # Replace single occurrence with reduplicated version
        if re.search(rf'\b{word}\b', text) and random.random() < 0.25:
            text = re.sub(rf'\b{word}\b', f"{word}{word}", text, count=1)
            break

    return text

def soft_punctuation(text: str) -> str:
    """Soften punctuation for more casual tone."""
    #  Replace some periods with "～" or "啦"
    if random.random() < 0.3:
        text = re.sub(r'([^。\n])。$', r'\1啦', text)  # End of text
        text = re.sub(r'([^。\n])。([^。])', r'\1～\2', text, count=1)  # Middle

    return text

def format_reply(text: str, config: Dict, use_greeting: bool = False,
                greeting_text: str = "") -> str:
    """
    Format a reply to be more human-like.

    Args:
        text: The original AI-generated reply
        config: Configuration dictionary
        use_greeting: Whether to prepend a greeting
        greeting_text: The greeting text to use

    Returns:
        Formatted reply string
    """
    # Remove leading greetings if we're not using one
    if not use_greeting:
        for pattern in config["greeting_patterns"]:
            text = re.sub(pattern, "", text, count=1, flags=re.UNICODE)

    # Remove avoid patterns at the start (like "好的，")
    for pattern in config["avoid_patterns"]:
        text = re.sub(pattern, "", text, count=1, flags=re.UNICODE)

    # Apply casual replacements (if formal_level is low)
    if config["formal_level"] < 0.5:
        for old, replacements in config["casual_replacements"].items():
            if old in text:
                # Replace with a random choice
                new = random.choice(replacements)
                # Only replace at start of sentence or after punctuation
                pattern = rf'(^|[。！？\n])\s*{re.escape(old)}'
                text = re.sub(pattern, rf'\1{new}', text)

    # Apply structural variations (only if casual)
    if config["formal_level"] < 0.6:
        text = add_filler_words(text, config)
        text = vary_sentence_structure(text, config)
        text = add_reduplication(text, config)

        if config.get("soften_punctuation", True):
            text = soft_punctuation(text)

    # Trim whitespace and any leading punctuation left over from removals
    text = text.strip()
    # Remove any leading comma or period that might be left
    text = re.sub(r'^[，,。\.]\s*', '', text)

    # Prepend greeting if needed
    if use_greeting and greeting_text:
        text = f"{greeting_text} {text}"

    return text

def detect_topic_change(old_topic: str, new_message: str, history: List) -> bool:
    """
    Simple heuristic to detect if the topic has changed.
    """
    # If there's no old topic, it's a new topic
    if not old_topic:
        return True

    # Extract keywords from new message (simple approach)
    new_keywords = set(re.findall(r'[\u4e00-\u9fff]{2,}', new_message))

    if not new_keywords:
        return False

    # Check overlap with recent history
    recent_messages = history[-3:] if len(history) >= 3 else history
    history_text = " ".join(recent_messages)
    history_keywords = set(re.findall(r'[\u4e00-\u9fff]{2,}', history_text))

    # If low overlap, topic likely changed
    if len(new_keywords & history_keywords) < 2:
        return True

    return False

# Random module is needed
import random

if __name__ == "__main__":
    # Test the formatter
    test_cases = [
        ("老板，明天天气怎么样？", True, "老板"),
        ("好的，我已经查到了。", False, ""),
        ("老板，那个事情处理好了吗？", True, "老板"),
    ]

    config = DEFAULT_CONFIG.copy()

    for original, greet, greeting in test_cases:
        result = format_reply(original, config, greet, greeting)
        print(f"Input: {original}")
        print(f"Output: {result}")
        print()

path: references/technical_reference.md
content: # Human-like Reply Skill - Technical Reference

## Architecture

The `human-like-reply` skill consists of three main components:

1. **ReplyState Manager**: Tracks conversation state per session
2. **ReplyFormatter**: Processes AI-generated text
3. **Topic Detector**: Identifies when the conversation topic changes

## Data Flow

```
User Message → AI generates reply → ReplyFormatter processes → Send to user
```

The formatter works in two stages:

1. **Pre-processing**: Remove unnecessary greetings if we decide not to use one
2. **Post-processing**: Apply casual replacements, trim whitespace, add greeting if needed

## State Storage

State is stored in `memory/reply_state.json`:

```json
{
  "conversations": {
    "session_key_1": {
      "rounds": 5,
      "last_message_time": "2026-03-10T14:30:00",
      "last_used_greeting": true,
      "current_topic": "weather",
      "greeting_count": 3
    }
  }
}
```

## Configuration

Default configuration (in `SKILL.md`):

```yaml
human_like_reply:
  enabled: true
  greeting_threshold: 3
  topic_change_threshold: 5
  silence_minutes: 30
  use_smileys: true
  formal_level: 0.3
```

## Greeting Decision Algorithm

```python
def should_use_greeting(conv, config, is_new_topic):
    rounds = conv["rounds"]

    # 1. Check silence gap
    if silence > config["silence_minutes"] * 60:
        return True, greeting()

    # 2. New topic early in conversation
    if is_new_topic and rounds < config["topic_change_threshold"]:
        return True, greeting()

    # 3. Early rounds with probability decay
    if rounds < config["greeting_threshold"]:
        prob = 1.0 - (rounds / config["greeting_threshold"])
        return random() < prob

    # 4. Late rounds only on topic change
    if is_new_topic and random() < 0.3:
        return True, greeting()

    return False, ""
```

## Topic Change Detection

Simple keyword overlap approach:

- Extract Chinese 2+ character words from new message
- Compare with last 3 messages' keywords
- If overlap < 2, consider it a new topic

## Integration Points

In OpenClaw, this skill should be hooked into:

1. **Before sending**: `message(action="send")` pre-hook
2. **Session state**: Access session key for per-conversation tracking
3. **Configuration**: Read from OpenClaw config

## Example Transformations

| Before | After |
|--------|-------|
| `老板，明天天气怎么样？` | `明天天气怎么样？` |
| `好的，我马上处理。` | `马上处理。` |
| `老板，那个事情已经好了。` | `那个事情已经好了。` |
| `老板，还有其他事吗？` | `还有别的事吗？` |

## Limitations

- Greeting detection is regex-based and may miss edge cases
- Topic change detection is heuristic, not perfect
- State is not shared across sessions
- No support for languages other than Chinese yet

## Future Improvements

- Use embedding similarity for topic detection
- Learn user preferences for greeting frequency
- Support multi-language
- Add tone adaptation (professional vs casual)

path: memory/reply_state.json
content: {
  "conversations": {
    "test-session-1": {
      "rounds": 2,
      "last_message_time": "2026-03-10T16:25:04.599512",
      "last_used_greeting": true,
      "current_topic": "搞定了吗？",
      "greeting_count": 2
    }
  },
  "last_update": "2026-03-10T14:32:41.447507"
}

Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)