diff --git a/README.md b/README.md index 4713cc8..3801d1e 100644 --- a/README.md +++ b/README.md @@ -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` 프롬프트를 덧붙여 모델이 스키마를 다시 따르도록 유도합니다. diff --git a/__pycache__/ai_planner.cpython-311.pyc b/__pycache__/ai_planner.cpython-311.pyc index 18bfa87..3bfaa9b 100644 Binary files a/__pycache__/ai_planner.cpython-311.pyc and b/__pycache__/ai_planner.cpython-311.pyc differ diff --git a/ai_planner.py b/ai_planner.py index 40e3d64..f305637 100644 --- a/ai_planner.py +++ b/ai_planner.py @@ -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]: """ diff --git a/docs/plan.md b/docs/plan.md index 7122604..ff3d338 100644 --- a/docs/plan.md +++ b/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 반복 설정 제거 (우클릭 유지) ### 문제