feat: standardize parameters for mine_resource action in AIPlanner to improve error handling and compatibility with LM Studio responses

This commit is contained in:
21in7
2026-03-27 20:23:22 +09:00
parent d9801ee457
commit 1d0522dc33
4 changed files with 71 additions and 0 deletions

View File

@@ -146,6 +146,7 @@ planner.set_goal(
- LM Studio `input-only` 모드에서 `output[].type=reasoning` 형태로 응답이 오는 경우도 파서가 처리하며, 응답 텍스트에 JSON 앞뒤 설명이 섞이면 `{...}` 구간을 복구 추출해 파싱을 시도합니다(복구 불가 시 기존 휴리스틱 폴백).
- 파싱된 계획은 `actions` 스키마와 지원 action 화이트리스트를 통과한 경우에만 채택합니다. 유효 액션이 없거나 형식이 깨지면 LLM 출력을 버리고 상태 기반 휴리스틱 폴백으로 전환해 `알 수 없는 행동` 루프를 줄입니다.
- LM Studio가 표준과 다른 동의어를 반환할 때(예: `place_building`, `target_x/target_y`, `position_x/position_y`), planner가 표준 스키마(`place_entity`, `x/y`, `name`)로 자동 정규화한 뒤 검증합니다.
- 특히 `mine_resource`는 실행기 시그니처에 맞게 `ore`, `count`만 남기도록 강제 정규화합니다. `resource`/`item`은 `ore`로 매핑하고 `tile`/`target` 같은 비표준 키는 제거해 `unexpected keyword argument` 오류를 방지합니다.
- `ai_planner.py`는 LLM이 move 순서를 놓쳐 `place_entity`/`insert_to_entity`/`set_recipe`가 건설 거리 제한으로 실패하는 경우를 줄이기 위해, 해당 액션 직전에 최근 `move`가 같은 좌표가 아니면 자동으로 `move`를 끼워 넣습니다
- `agent_log.jsonl`에 모든 행동과 타임스탬프가 기록됩니다
- `ai_planner.py`는 GLM 응답이 잘리거나(중괄호/대괄호 불일치) 마크다운이 섞여도 JSON 파싱을 복구하도록 `{} / []` 균형 추적과 보정 로직을 사용합니다. 또한 최상위에서 JSON 배열(`[...]`)로 답하는 경우도 `actions`로 래핑해 처리합니다. (추가) `finish_reason=length` 등으로 `message.content`가 비거나(또는 JSON 형태가 아니면) `message.reasoning_content`를 우선 사용합니다. 그리고 응답 안에 여러 개의 `{...}`가 섞여 있어도 그중 `actions`를 포함한 계획 객체를 우선 선택합니다. 또한 JSON 파싱 실패가 감지되면 다음 재시도에는 `JSON-only repair` 프롬프트를 덧붙여 모델이 스키마를 다시 따르도록 유도합니다.

View File

@@ -43,6 +43,16 @@ ACTION_ALIASES = {
"mine": "mine_resource",
"research": "start_research",
}
ORE_ALIASES = {
"iron": "iron-ore",
"iron ore": "iron-ore",
"iron-ore": "iron-ore",
"copper": "copper-ore",
"copper ore": "copper-ore",
"copper-ore": "copper-ore",
"coal": "coal",
"stone": "stone",
}
SYSTEM_PROMPT = """당신은 팩토리오 게임을 순수하게 플레이하는 AI 에이전트입니다.
@@ -500,6 +510,7 @@ class AIPlanner:
p["x"] = p.get("target_x")
if "y" not in p and "target_y" in p:
p["y"] = p.get("target_y")
return AIPlanner._keep_allowed_keys(p, {"x", "y"})
elif action == "place_entity":
if "name" not in p and "item" in p:
p["name"] = p.get("item")
@@ -507,8 +518,50 @@ class AIPlanner:
p["x"] = p.get("position_x")
if "y" not in p and "position_y" in p:
p["y"] = p.get("position_y")
return AIPlanner._keep_allowed_keys(p, {"name", "x", "y", "direction"})
elif action == "mine_resource":
if "ore" not in p:
if "resource" in p:
p["ore"] = p.get("resource")
elif "item" in p:
p["ore"] = p.get("item")
ore = AIPlanner._normalize_ore_name(p.get("ore", "iron-ore"))
try:
count = int(p.get("count", 35))
except Exception:
count = 35
if count <= 0:
count = 35
return {"ore": ore, "count": count}
elif action == "craft_item":
return AIPlanner._keep_allowed_keys(p, {"item", "count"})
elif action == "explore":
return AIPlanner._keep_allowed_keys(p, {"direction", "max_steps", "wanted_ores"})
elif action == "build_smelting_line":
return AIPlanner._keep_allowed_keys(p, {"ore", "x", "y", "furnace_count"})
elif action == "place_belt_line":
return AIPlanner._keep_allowed_keys(p, {"from_x", "from_y", "to_x", "to_y"})
elif action == "insert_to_entity":
return AIPlanner._keep_allowed_keys(p, {"x", "y", "item", "count"})
elif action == "set_recipe":
return AIPlanner._keep_allowed_keys(p, {"x", "y", "recipe"})
elif action == "start_research":
return AIPlanner._keep_allowed_keys(p, {"tech"})
elif action == "wait":
return AIPlanner._keep_allowed_keys(p, {"seconds"})
return p
@staticmethod
def _keep_allowed_keys(params: dict, allowed: set[str]) -> dict:
return {k: v for k, v in params.items() if k in allowed}
@staticmethod
def _normalize_ore_name(value: object) -> str:
if not isinstance(value, str):
return "iron-ore"
key = value.strip().lower()
return ORE_ALIASES.get(key, "iron-ore")
@staticmethod
def _ensure_move_before_build_actions(actions: list[dict]) -> list[dict]:
"""

View File

@@ -87,6 +87,23 @@
---
## 2026-03-27 mine_resource 파라미터 강제 표준화 계획
### 문제 요약
- LM Studio가 `mine_resource``tile`, `target`, `resource` 같은 비표준 키를 보내면서 실행기(`mine_resource(ore, count)`)에서 TypeError 발생.
### 구현 계획
1. `ai_planner.py`에서 `mine_resource``ore`, `count`만 남기도록 필터링한다.
2. `resource`/`item` 동의어를 `ore`로 매핑하고, `iron`/`copper` 같은 축약값도 정규 ore 이름으로 보정한다.
3. `count`가 없거나 비정상이면 기본값(35)으로 보정한다.
4. README에 `mine_resource` 파라미터 표준화 동작을 반영한다.
### 검증 계획
- `python -m py_compile ai_planner.py`
- 런타임에서 `unexpected keyword argument 'tile'/'target'`가 사라지는지 확인
---
## 채굴 시 mining_state 반복 설정 제거 (우클릭 유지)
### 문제