고급 규칙 작업: Python 스크립트 실행

이 문서에서는 사용자 정의 Python 스크립트를 실행하여 간단한 텍스트 교체 규칙의 기능을 확장하는 방법을 설명합니다. 이 강력한 기능을 사용하면 동적 응답을 만들고, 파일과 상호 작용하고, 외부 API를 호출하고, 음성 인식 워크플로 내에서 직접 복잡한 논리를 구현할 수 있습니다.

핵심 개념: on_match_exec

단순히 텍스트를 바꾸는 대신, 이제 패턴이 일치할 때 하나 이상의 Python 스크립트를 실행하도록 규칙에 지시할 수 있습니다. 이는 규칙의 옵션 사전에 on_match_exec 키를 추가하여 수행됩니다.

스크립트의 주요 작업은 일치 항목에 대한 정보를 받고, 작업을 수행하고, 새 텍스트로 사용될 최종 문자열을 반환하는 것입니다.

규칙 구조

스크립트 작업이 포함된 규칙은 다음과 같습니다.

# In your map file (e.g., config/maps/.../de-DE/my_rules.py)
from pathlib import Path

# It's best practice to define the directory path once at the top
CONFIG_DIR = Path(__file__).parent

FUZZY_MAP_pre = [
    (
        None,  # The replacement string is often None, as the script generates the final text.
        r'what time is it', # The regex pattern to match.
        95, # The confidence threshold.
        {
            'flags': re.IGNORECASE,
            # The new key: a list of script files to execute.
            'on_match_exec': [CONFIG_DIR / 'get_current_time.py']
        }
    ),
]

핵심 사항:

  • on_match_exec 값은 목록이어야 합니다.

  • 스크립트는 맵 파일과 동일한 디렉터리에 위치하므로 CONFIG_DIR / 'script_name.py'가 경로를 정의하는 데 권장되는 방법입니다.


실행 가능한 스크립트 만들기

시스템이 스크립트를 사용하려면 다음 두 가지 간단한 규칙을 따라야 합니다.

  1. 유효한 Python 파일(예: my_script.py)이어야 합니다.

  2. execute(match_data)라는 함수를 포함해야 합니다.

execute(match_data) 함수

이는 모든 실행 가능한 스크립트의 표준 진입점입니다. 규칙이 일치하면 시스템이 자동으로 이 함수를 호출합니다.

  • 일치_데이터(dict): 일치에 대한 모든 컨텍스트를 포함하는 사전입니다.

  • 반환 값(str): 함수는 반드시 문자열을 반환해야 합니다. 이 문자열은 새로 처리된 텍스트가 됩니다.

match_data 사전

이 사전은 기본 애플리케이션과 스크립트 사이의 다리 역할을 합니다. 여기에는 다음 키가 포함되어 있습니다.

  • ``original_text’` (str): 현재 규칙의 대체가 적용되기 전체 텍스트 문자열입니다.

  • ``text_after_replacement’(str): 규칙의 기본 대체 문자열이 적용된 *이후* 텍스트이지만 스크립트가 호출되기 *이전*입니다. (대체 항목이None 경우 original_text`와 동일합니다.)

  • ``regex_match_obj’(re.Match): 공식 Python 정규식 일치 개체입니다. 이는 **캡처 그룹**에 액세스하는 매우 강력합니다.match_obj.group(1), match_obj.group(2)` 등을 사용할 수 있습니다.

  • ``rule_options’`(dict): 스크립트를 트리거한 규칙에 대한 전체 옵션 사전입니다.


예시 1: 현재 시간 가져오기(동적 응답)

이 스크립트는 하루 중 시간을 기준으로 개인화된 인사말을 반환합니다.

1. 규칙(지도 파일에 있음):

(None, r'\b(what time is it|uhrzeit)\b', 95, {
    'flags': re.IGNORECASE,
    'on_match_exec': [CONFIG_DIR / 'get_current_time.py']
}),

2. 스크립트(get_current_time.py):

from datetime import datetime
import random

def execute(match_data):
    """Returns a friendly, time-aware response."""
    now = datetime.now()
    hour = now.hour
    time_str = now.strftime('%H:%M')

    if hour < 12:
        greeting = "Good morning!"
    elif hour < 18:
        greeting = "Good afternoon!"
    else:
        greeting = "Good evening!"
    
    responses = [
        f"{greeting} It's currently {time_str}.",
        f"Right now, the time is {time_str}. Hope you're having a great day!",
    ]
    return random.choice(responses)

용법:

입력: “지금은 몇 시야?” 출력: “안녕하세요! 현재 14시 30분입니다.”

예 2: 간단한 계산기(캡처 그룹 사용)

이 스크립트는 정규식의 캡처 그룹을 사용하여 계산을 수행합니다.

1. 규칙(지도 파일에 있음):

(None, r'calculate (\d+) (plus|minus) (\d+)', 98, {
    'flags': re.IGNORECASE,
    'on_match_exec': [CONFIG_DIR / 'calculator.py']
}),

2. 스크립트(calculator.py):

def execute(match_data):
    """Performs a simple calculation based on regex capture groups."""
    try:
        match_obj = match_data['regex_match_obj']
        
        num1 = int(match_obj.group(1))
        operator = match_obj.group(2).lower()
        num2 = int(match_obj.group(3))

        if operator == "plus":
            result = num1 + num2
        elif operator == "minus":
            result = num1 - num2
        else:
            return "I didn't understand that operator."
            
        return f"The result is {result}."
    except (ValueError, IndexError):
        return "I couldn't understand the numbers in your request."

용법:

입력: “55 더하기 10을 계산하세요” 출력: “결과는 65입니다.”

예시 3: 영구 쇼핑 목록(파일 I/O)

이 예에서는 사용자의 원본 텍스트를 검사하여 하나의 스크립트가 여러 명령(추가, 표시)을 처리하는 방법과 파일에 기록하여 데이터를 유지하는 방법을 보여줍니다.

1. 규칙(지도 파일에 있음):

# Rule for adding items
(None, r'add (.*) to the shopping list', 95, {
    'flags': re.IGNORECASE,
    'on_match_exec': [CONFIG_DIR / 'shopping_list.py']
}),

# Rule for showing the list
(None, r'show the shopping list', 95, {
    'flags': re.IGNORECASE,
    'on_match_exec': [CONFIG_DIR / 'shopping_list.py']
}),

2. 스크립트(shopping_list.py):

from pathlib import Path

LIST_FILE = Path(__file__).parent / "shopping_list.txt"

def execute(match_data):
    """Manages a shopping list stored in a text file."""
    original_text = match_data['original_text'].lower()
    
    # --- Add Item Command ---
    if "add" in original_text:
        item = match_data['regex_match_obj'].group(1).strip()
        with open(LIST_FILE, "a", encoding="utf-8") as f:
            f.write(f"{item}\n")
        return f"Okay, I've added '{item}' to the shopping list."
    
    # --- Show List Command ---
    elif "show" in original_text:
        if not LIST_FILE.exists() or LIST_FILE.stat().st_size == 0:
            return "The shopping list is empty."
        with open(LIST_FILE, "r", encoding="utf-8") as f:
            items = f.read().strip().splitlines()
        
        item_str = ", ".join(items)
        return f"On the list you have: {item_str}."
        
    return "I'm not sure what to do with the shopping list."

용법:

입력 1: “쇼핑 목록에 우유 추가” 출력 1: “알겠습니다. 쇼핑 목록에 ‘우유’를 추가했습니다.”

입력 2: “쇼핑 목록 표시” 출력 2: “당신이 가지고 있는 목록에는 우유가 있습니다.”


모범 사례

  • 스크립트당 하나의 작업: 스크립트의 초점을 단일 작업에 유지합니다(예: calculator.py는 계산만 합니다).

  • 오류 처리: 전체 애플리케이션이 충돌하는 것을 방지하려면 항상 스크립트의 논리를 ‘try…out’ 블록으로 감싸세요. 제외 블록에서 사용자에게 친숙한 오류 메시지를 반환합니다.

  • 외부 라이브러리: 외부 라이브러리(예: requests 또는 wikipedia-api)를 사용할 수 있지만 Python 환경(pip install <library-name>)에 설치되어 있는지 확인해야 합니다.

  • 보안: 이 기능은 모든 Python 코드를 실행할 수 있다는 점에 유의하세요. 신뢰할 수 있는 소스의 스크립트만 사용하세요.