feat: standardize parameters for mine_resource action in AIPlanner to improve error handling and compatibility with LM Studio responses
This commit is contained in:
@@ -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` 프롬프트를 덧붙여 모델이 스키마를 다시 따르도록 유도합니다.
|
||||
|
||||
Binary file not shown.
@@ -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]:
|
||||
"""
|
||||
|
||||
17
docs/plan.md
17
docs/plan.md
@@ -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 반복 설정 제거 (우클릭 유지)
|
||||
|
||||
### 문제
|
||||
|
||||
Reference in New Issue
Block a user