Metadata-Version: 2.4
Name: pykamirsl
Version: 0.1.2
Summary: Robot Scripting Language (RSL) - 카미봇 미니 DSL: 파서/검증기/async 인터프리터
Home-page: https://github.com/kamibot-rsl
Author: Kamibot RSL contributors
License: MIT
Project-URL: Homepage, https://github.com/kamibot-rsl
Project-URL: Source, https://github.com/kamibot-rsl
Keywords: rsl,dsl,robot,kamibot,langgraph,mcp
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Education
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Education
Classifier: Topic :: Software Development :: Interpreters
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.3; extra == "langchain"
Dynamic: home-page
Dynamic: requires-python

## pykamrsl — Kamibot Pi 미니 DSL (RSL)

**RSL (Robot Scripting Language)** — 카미봇 파이(Kamibot Pi) 로봇을 제어하기 위한
작은 도메인 특화 언어와 그 실행 도구체인. `parse → validate → interpret` 3 단
파이프라인을 **무의존성**(`dependencies = []`) 으로 제공한다.

`pykamrsl` 자체는 시리얼 포트를 열지 않는다 — 인터프리터는 사용자가 주입한
**비동기 도구 사전**(`dict[str, AsyncTool]`) 을 디스패치한다.
실제 카미봇 조작은 LangChain `BaseTool`, MCP 서버, 또는 직접 만든 stub 어느
백엔드로도 갈아 끼울 수 있다.

### 설치

```bash
pip install pykamirsl
```

| 이름 | 용도 |
|---|---|
| 배포명 (`pykamirsl`) | `pip install` 시 입력 |
| 임포트명 (`pykamrsl`) | 코드에서 `import pykamrsl` |

LangChain `BaseTool` 어댑터와 같이 쓸 거라면 선택 의존성을 함께 설치:

```bash
pip install "pykamirsl[langchain]"
```

### 5 분 시작 — 하드웨어 없이

`FakeTool` 만 있으면 카미봇이 없어도 전 파이프라인을 그대로 돌려볼 수 있다.

```python
import asyncio
from pykamrsl import parse, validate, RSLInterpreter

# 1) RSL 스크립트
source = """
BEGIN MISSION "장애물 회피"
  SET threshold = 30
  SENSE DISTANCE AS d
  IF d > threshold:
    MOVE BACKWARD CM=10
    BEEP
    SIGNAL "장애물 감지 — 후진 d={d}"
  ELSE:
    MOVE FORWARD CM=20
  END IF
END MISSION
"""

# 2) parse: 텍스트 → Mission AST
mission = parse(source)

# 3) validate: 값 범위 + 변수 정의 정적 검증
errors = validate(mission)
print("validation errors:", errors)   # [] = 통과

# 4) 도구 stub (실제 환경에선 LangChain BaseTool / MCP 어댑터로 교체)
class FakeTool:
    def __init__(self, name, ret="ok"):
        self.name = name
        self._ret = ret
    async def ainvoke(self, args):
        print(f"  → {self.name}({args})")
        return self._ret

tools = {
    "move_forward_cm":          FakeTool("move_forward_cm"),
    "move_backward_cm":         FakeTool("move_backward_cm"),
    "play_beep":                FakeTool("play_beep"),
    # SENSE 결과는 dict 또는 JSON 문자열로 반환 — 인터프리터가 알아서 디코드.
    # DISTANCE 는 min(left, right) 가 변수에 들어감 → 여기선 d=50 → IF 분기.
    "read_object_distance_raw": FakeTool("read_object_distance_raw",
                                         {"left": 50, "right": 80}),
}

# 5) 인터프리터 실행
log = asyncio.run(RSLInterpreter(tools).arun(mission))
print("\n".join(log))
```

출력 예:

```
validation errors: []
[CMD] read_object_distance_raw()
[CMD] move_backward_cm(distance_cm=10)
[CMD] play_beep()

=== 미션 시작: 장애물 회피 ===
센서 DISTANCE → d=50
ok
ok
SIGNAL: 장애물 감지 — 후진 d=50
=== 미션 종료 ===
```

- `[CMD]` 줄은 인터프리터가 콘솔로 echo 하는 호출 시그니처. 끄려면
  `RSLInterpreter(tools, echo_commands=False)`.
- `ok` 두 줄은 `FakeTool` 의 반환값 — 실제 환경에서는 도구가 돌려준 한국어
  안내 문자열이 그대로 로그에 들어간다.
- `SENSE DISTANCE` 는 `min(left, right)` 를 변수에 저장한다 (`d=50`).

### 패키지 표면

```python
from pykamrsl import (
    parse, ParseError,             # 텍스트 → Mission AST
    validate, ValidationError,     # AST 의미·범위 검증
    RSLInterpreter, RuntimeRSLError, MAX_LOOP_ITERS,
    SYSTEM_PROMPT, build_retry_message,
)
```

`pykamrsl.ast_nodes` 도 export 됨: `Mission`, `Assign`, `Move`, `Turn`, `Sense`,
`Signal`, `If`, `Loop`, `Condition`, `IntLit`, `StrLit`, `VarRef` 등.

### RSL 문법 한눈에 보기

```text
mission     := BEGIN MISSION "title" ... END MISSION
statement   := assign | move | turn | stop | wait | led | sound
             | draw | sense | signal | if_block | loop_block | repeat_block
assign      := SET name = value
move        := MOVE FORWARD|BACKWARD  CM = v
            |  MOVE FORWARD|BACKWARD  SECONDS = v
            |  MOVE FORWARD|BACKWARD  SPEED = v        # 연속 이동 — STOP 전까지
turn        := TURN LEFT|RIGHT ANGLE = v
stop        := STOP
wait        := WAIT SECONDS = v
led         := LED RGB r, g, b
            |  LED PRESET n
sound       := BEEP | MELODY SCALE = s SECONDS = t
draw        := DRAW TRIANGLE|RECTANGLE|PENTAGON|HEXAGON|STAR|CIRCLE SIZE = v
sense       := SENSE DISTANCE|LINE_LEFT|LINE_CENTER|LINE_RIGHT|COLOR|BATTERY AS var
signal      := SIGNAL "message with {var} interpolation"
if_block    := IF cond:
                 ...
               (ELSE:
                 ...)?
               END IF
loop_block  := LOOP UNTIL|WHILE cond:
                 ...
               END LOOP
repeat_block := REPEAT count TIMES:
                 ...
               END REPEAT
cond        := value <|>|<=|>=|==|!= value
            |  var                           # 단일 변수 → truthy 검사
value       := 정수 | "문자열" | var
```

- 키워드는 모두 영어 대문자. 변수명은 `[a-z0-9_]+`.
- 코멘트는 `# ...` (라인 끝까지).
- 블록 종료는 명시적 `END IF` / `END LOOP` / `END MISSION` — 들여쓰기 의존 없음.

### 수치 허용 범위 (검증기)

| 매개변수 | 범위 |
|---|---|
| `speed` | 0 ~ 100 |
| `cm` | 1 ~ 200 |
| `seconds` (이동/대기) | 1 ~ 30 |
| `degrees` | 1 ~ 360 |
| RGB 채널 | 0 ~ 255 |
| `LED PRESET` | 0 ~ 8 (0=꺼짐, 1=빨강, 2=주황, 3=노랑, 4=초록, 5=파랑, 6=하늘, 7=보라, 8=흰색) |
| `DRAW SIZE` | 5 ~ 50 |
| `MELODY SCALE` / `SECONDS` | 1 ~ 200 / 1 ~ 60 |
| `REPEAT count` | 1 ~ 100 (= `MAX_LOOP_ITERS`) |

리터럴이 범위를 벗어나면 `validate()` 가 에러 메시지를 리스트로 반환한다.
변수에 담긴 값은 런타임에 인터프리터가 검사한다.

### 도구 인터페이스 (인터프리터 백엔드)

`RSLInterpreter(tools_by_name)` 의 `tools_by_name: dict[str, AsyncTool]` 은
다음 구조적 인터페이스만 만족하면 된다. `langchain_core.BaseTool` 인스턴스가
자동으로 만족한다 — langchain-mcp-adapters 가 돌려주는 도구도 그대로 주입 가능.

```python
from typing import Protocol, Any

class AsyncTool(Protocol):
    name: str
    async def ainvoke(self, args: dict) -> Any: ...
```

#### 인터프리터가 호출하는 도구 이름

| RSL 문장 | 도구 이름 | 인자 | 비고 |
|---|---|---|---|
| `MOVE FORWARD CM=v`  | `move_forward_cm`  | `distance_cm` | |
| `MOVE BACKWARD CM=v` | `move_backward_cm` | `distance_cm` | |
| `MOVE * SECONDS=v`   | `move_*_seconds`   | `seconds` | |
| `MOVE * SPEED=v`     | `drive_*_continuous` | `speed` | 연속 이동 |
| `TURN LEFT/RIGHT`    | `turn_left_degrees` / `turn_right_degrees` | `degrees` | |
| `STOP`               | `stop_now` | — | |
| `WAIT SECONDS=v`     | `wait_seconds` | `seconds` | |
| `LED RGB r, g, b`    | `set_led_rgb` | `r, g, b` | |
| `LED PRESET n`       | `set_led_preset` | `preset_index` | |
| `BEEP`               | `play_beep` | — | |
| `MELODY SCALE=s SECONDS=t` | `play_melody_note` | `scale, seconds` | |
| `DRAW <shape> SIZE=v` | `draw_<shape>` | `size` | shape ∈ {tri,rect,penta,hexa,star,circle} 의 풀네임 |
| `SENSE DISTANCE`     | `read_object_distance_raw` | — | 반환 `{"left": int, "right": int}` |
| `SENSE LINE_*`       | `read_line_sensors_raw`    | — | 반환 `{"left": int, "center": int, "right": int}` |
| `SENSE COLOR`        | `read_color_index_raw`     | — | 반환 정수 |
| `SENSE BATTERY`      | `read_battery_level_raw`   | — | 반환 정수 |

센서 도구의 dict / int 는 그대로 돌려줘도 되고 JSON 문자열로 직렬화해서
돌려줘도 된다 — 인터프리터의 `_to_text` / `_coerce_dict` / `_coerce_int` 가
양쪽을 모두 처리한다.

### LLM 으로 RSL 자동 생성 (선택)

자연어 한국어 명령 → RSL 변환을 LLM 에 맡길 때 쓰는 시스템 프롬프트가
패키지에 포함돼 있다. few-shot 예시 3 개와 출력 규칙이 들어 있다.

```python
from pykamrsl import SYSTEM_PROMPT, build_retry_message
from openai import OpenAI                                    # 예시 — 다른 모델 OK

client = OpenAI()
resp = client.chat.completions.create(
    model="gpt-4o-mini",
    temperature=0,
    messages=[
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user",   "content": "앞으로 20cm 가고 LED 를 파란색으로 켜줘"},
    ],
)
source = resp.choices[0].message.content
# 이후 parse → validate → RSLInterpreter.arun 로 흘리면 끝.
# validate 가 에러를 돌려주면 build_retry_message(errors) 로 LLM 에 재시도 요청 가능.
```

### SIGNAL 의 `{var}` 보간

`SIGNAL "장애물 감지 — d={d}, threshold={threshold}"` 는 실행 시점의 변수
값으로 치환된다. 정의되지 않은 변수는 원본 그대로 남는다.

### V1 알려진 제약

- 산술/논리 연산자 없음 — `+ - * / AND OR NOT` 미지원. 비교 6 종 `< > <= >= == !=` 만.
- 변수에 상수만 대입 가능 (`SET x = 30` ✓, `SET x = y + 1` ✗).
- `LOOP` 본문은 최대 **100 회** 실행 후 자동 종료 — `SIGNAL: loop_iteration_cap_reached` 가 로그에 남음.
- 단순 N 회 반복은 `REPEAT N TIMES: … END REPEAT` 를 사용한다. 본문은 카운터 변수를 노출하지 않으며 `N` 은 1~100. 변수 `N` 이 100 을 넘으면 런타임에 100 으로 잘리고 `SIGNAL: repeat_count_clamped` 가 로그에 남음.

### 더 큰 예제

저장소의 [examples/](https://github.com/kamibot-rsl) 디렉토리에는 다음을 포함한
end-to-end LangGraph 에이전트 예제가 들어 있다:

- **LLM → RSL → MCP 서버 → 카미봇** 통합 흐름
- MCP 서버 (`kamibot_mcp_server.py`) — 카미봇 도구 24 종을 stdio 로 노출
- `KAMIBOT_MOCK=true` 로 하드웨어 없이 통합 테스트

```bash
pip install "pykamirsl[langchain]" langchain-openai langgraph langchain-mcp-adapters
python -m examples.main
```

### 라이선스

MIT. 학습용으로 자유롭게 수정·재배포 가능.
