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

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

How the Trigger Chain Processes Natural Language

Every incoming message passes through a predefined trigger chain defined in hoshino/trigger.py. This chain is a list of BaseTrigger subclasses executed in strict order:

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

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

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

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

Practical Implementation Examples

Example 1: Simple Keyword-Based NLP Trigger

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

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

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

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 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, 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: 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →