# How to Implement Natural Language Processing (NLP) Triggers in HoshinoBot: Service Class and Trigger Chain Guide

> Learn to implement Natural Language Processing NLP triggers in HoshinoBot using decorators and regex. Enhance your bot's responsiveness with this guide.

- Repository: [ice9coffee/hoshinobot](https://github.com/ice9coffee/hoshinobot)
- Tags: how-to-guide
- Published: 2026-03-03

---

**Implement NLP triggers in HoshinoBot by decorating an async function with `@sv.on_natural_language()` and supplying either a `keywords` list or a compiled regex pattern, which registers the handler in the trigger chain for normalized message processing.**

HoshinoBot provides a robust framework for natural language processing through its `Service` class architecture. To implement natural language processing (NLP) triggers in HoshinoBot, developers leverage the `on_natural_language` decorator which integrates seamlessly with the bot's trigger chain and permission system. This guide explains the registration mechanics, matching algorithms, and validation layers using actual source code from the `ice9coffee/hoshinobot` repository.

## Registering NLP Handlers with Service.on_natural_language

Every feature module in HoshinoBot begins with the **`Service`** class, which serves as the entry point for registering natural language handlers. The **`Service.on_natural_language`** method creates a decorator that wraps your coroutine in a `ServiceFunc` object and registers it with the trigger subsystem.

When you apply `@sv.on_natural_language(...)` to a function, the decorator stores:
- The service reference (`sv`)
- The original coroutine to execute on match
- Configuration flags including `only_to_me` and `normalize`

This wrapper is then handed to the **`trigger.rex`** or **`trigger.keyword`** subsystem, depending on your decorator arguments. The natural language branch functions as a thin wrapper around Hoshino's generic trigger chain, maintaining consistency with command-based triggers while adding text normalization capabilities.

*Source:* [`Service.on_natural_language`](https://github.com/ice9coffee/hoshinobot/blob/master/hoshino/service.py#L19-L38)

## How the Trigger Chain Processes Natural Language

Every incoming message passes through a predefined **trigger chain** defined in [`hoshino/trigger.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/trigger.py). This chain is a list of `BaseTrigger` subclasses executed in strict order:

```python
chain: List[BaseTrigger] = [
    prefix,
    suffix,
    _TextNormalizer(),
    rex,
    keyword,
]

```

The **`_TextNormalizer`** extracts plain text from the CQMessage object and applies normalization rules, such as converting full-width characters to half-width. This normalized text becomes `event.norm_text`, which subsequent triggers use for pattern matching. When `normalize=False` is set in the decorator, the system uses `event.plain_text` instead, bypassing the normalization step.

*Source:* [`trigger.chain`](https://github.com/ice9coffee/hoshinobot/blob/master/hoshino/trigger.py#L69-L75)

## Keyword and Regex Matching Mechanisms

HoshinoBot supports two primary matching strategies for natural language processing, both operating against the normalized message text.

### KeywordTrigger Matching

When you provide a **`keywords`** list to the decorator, the `KeywordTrigger` class (referenced as `trigger.keyword` in the chain) iterates through each stored keyword and compares it against `event.norm_text`. The comparison looks for exact substring matches within the normalized message.

*Source:* [`KeywordTrigger.find_handler`](https://github.com/ice9coffee/hoshinobot/blob/master/hoshino/trigger.py#L24-L30)

### RexTrigger Matching

When you supply a compiled regular expression, the **`RexTrigger`** class (`trigger.rex`) executes `re.search` against the normalized text. This approach captures flexible patterns, variations in phrasing, and optional components within natural language queries.

*Source:* [`RexTrigger.find_handler`](https://github.com/ice9coffee/hoshinobot/blob/master/hoshino/trigger.py#L41-L48)

## Permission Validation and Service Checks

Before executing any natural language handler, HoshinoBot validates the request through the **`Service._check_all`** method. This verification ensures:
- The group is not blocked from using the service
- The user meets the required privilege level defined by `use_priv`
- The service is enabled in the current group context

Only when all three conditions pass does the dispatcher invoke the wrapped coroutine. This architecture ensures that NLP triggers respect the same permission boundaries as explicit commands.

*Source:* [`Service._check_all`](https://github.com/ice9coffee/hoshinobot/blob/master/hoshino/service.py#L61-L64)

## Practical Implementation Examples

### Example 1: Simple Keyword-Based NLP Trigger

```python
from hoshino import Service

sv = Service('greeting')

@sv.on_natural_language(keywords=['早上好', '早安'])
async def _(session):
    # `session` is a nonebot.NLPSession

    await session.send('早安呀~ 祝你今天精神满满！')

```

This decorator registers the coroutine with `keywords=['早上好', '早安']`. When any incoming group message contains either phrase after normalization, Hoshino routes the event to this handler immediately.

### Example 2: Regex-Based NLP Trigger

```python
import re
from hoshino import Service

sv = Service('weather')

# Matches "天气怎么样？" or "今天的天气如何"

@sv.on_natural_language(re.compile(r'天气.*[么吗]?'))
async def _(session):
    await session.send('今天天气晴朗，适合出门～')

```

Using a compiled regular expression allows the bot to recognize variations of weather-related queries. The matcher runs `re.search` against the normalized message text, capturing flexible natural language patterns.

### Example 3: Extracting Message Arguments with NLPSession

```python
from hoshino import Service

sv = Service('echo')

@sv.on_natural_language(keywords=['说', 'repeat'])
async def _(session):
    # Raw user message (without the trigger keyword)

    raw = session.current_arg_text.strip()
    if raw:
        await session.send(f'你说：{raw}')
    else:
        await session.send('请在后面跟上想要复读的内容。')

```

The **`NLPSession`** object provides `current_arg_text`, which contains the portion of the message following the matched keyword or regex. This property enables command-like functionality within natural language handlers.

### Example 4: Restricting Access with Privilege Levels

```python
from hoshino import Service, priv

sv = Service('admin_tool', use_priv=priv.SUPERUSER)

@sv.on_natural_language(keywords=['查看日志'])
async def _(session):
    # Only super-users can trigger this handler

    await session.send('正在发送最近 10 条日志…')

```

Setting **`use_priv`** on the `Service` instance restricts NLP trigger activation to users with the specified privilege level. The `_check_all` method enforces this restriction before executing the handler logic.

## Summary

- **Service Registration**: Use `@sv.on_natural_language()` from the `Service` class in [`hoshino/service.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/service.py) to register handlers, supplying either `keywords` or a compiled regex pattern.
- **Trigger Chain**: Messages flow through `prefix`, `suffix`, `_TextNormalizer()`, `rex`, and `keyword` triggers in [`hoshino/trigger.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/trigger.py), with normalization applied by default.
- **Matching Logic**: `KeywordTrigger` performs substring matching against `event.norm_text`, while `RexTrigger` uses `re.search` for pattern matching.
- **Permission System**: The `_check_all` method validates group blocks, user privileges, and service enablement before executing any NLP handler.
- **Session Data**: Access `session.current_arg_text` to retrieve message content following the trigger phrase for argument parsing.

## Frequently Asked Questions

### How do I make an NLP trigger respond only when mentioned?

Set the **`only_to_me=True`** parameter in the `@sv.on_natural_language()` decorator. This flag ensures the trigger chain only activates the handler when the bot is explicitly mentioned in the message, filtering out general conversation that happens to contain the keywords.

### What is the difference between `keywords` and regex patterns in NLP triggers?

**`keywords`** accepts a list of strings for exact substring matching against normalized text, suitable for fixed phrases like "早上好" or "help". **Regex patterns** (compiled via `re.compile()`) provide flexible pattern matching for variations in phrasing, optional words, or complex sentence structures, such as matching "天气怎么样", "今天天气好吗", and "天气如何" with a single pattern.

### Can I disable text normalization for specific NLP triggers?

Yes. Pass **`normalize=False`** to the `@sv.on_natural_language()` decorator. When disabled, the trigger uses `event.plain_text` instead of `event.norm_text`, preserving original character widths and formatting for cases where precise character matching matters more than semantic equivalence.

### How does HoshinoBot handle multiple NLP triggers matching the same message?

The trigger chain processes triggers in the order defined in [`hoshino/trigger.py`](https://github.com/ice9coffee/hoshinobot/blob/main/hoshino/trigger.py): `prefix`, `suffix`, `_TextNormalizer()`, `rex`, then `keyword`. Within each trigger type, handlers are typically checked in registration order. The first matching handler that passes all permission checks (`_check_all`) will execute, and subsequent matches are usually ignored unless the specific trigger implementation yields multiple handlers.