Skip to content

Commit e6d7aa5

Browse files
committed
update mofa mcp
1 parent 99648db commit e6d7aa5

File tree

17 files changed

+601
-58
lines changed

17 files changed

+601
-58
lines changed

examples/mofa/agent-hub/openra-copilot-agent/.env.example

Lines changed: 0 additions & 14 deletions
This file was deleted.
Binary file not shown.

examples/mofa/agent-hub/openra-copilot-agent/openra_copilot_agent/main.py

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,52 @@ def get_available_tools(self) -> List[Dict[str, Any]]:
141141
"required": ["unit_name"]
142142
}
143143
}
144+
},
145+
{
146+
"type": "function",
147+
"function": {
148+
"name": "deploy_units",
149+
"description": "部署单位(如展开基地车、部署攻城单位等)",
150+
"parameters": {
151+
"type": "object",
152+
"properties": {
153+
"actor_ids": {"type": "array", "items": {"type": "integer"}, "description": "要部署的单位ID列表,如果不提供则自动寻找基地车"}
154+
},
155+
"required": []
156+
}
157+
}
158+
},
159+
{
160+
"type": "function",
161+
"function": {
162+
"name": "start_production",
163+
"description": "开始生产单位或建筑,适用于建造电厂等建筑",
164+
"parameters": {
165+
"type": "object",
166+
"properties": {
167+
"unit_type": {"type": "string", "description": "要生产的单位或建筑类型,如 '电厂', '兵营', '步兵' 等"},
168+
"quantity": {"type": "integer", "description": "生产数量", "default": 1},
169+
"auto_place_building": {"type": "boolean", "description": "是否自动放置建筑", "default": True}
170+
},
171+
"required": ["unit_type"]
172+
}
173+
}
174+
},
175+
{
176+
"type": "function",
177+
"function": {
178+
"name": "place_building",
179+
"description": "放置已生产完成的建筑",
180+
"parameters": {
181+
"type": "object",
182+
"properties": {
183+
"queue_type": {"type": "string", "description": "生产队列类型: 'Building', 'Defense', 'Infantry', 'Vehicle', 'Aircraft', 'Naval'"},
184+
"x": {"type": "integer", "description": "放置X坐标(可选,不指定则自动选址)"},
185+
"y": {"type": "integer", "description": "放置Y坐标(可选,不指定则自动选址)"}
186+
},
187+
"required": ["queue_type"]
188+
}
189+
}
144190
}
145191
]
146192

@@ -162,18 +208,23 @@ def process_command_with_ai(self, user_input: str) -> str:
162208
messages = [
163209
{
164210
"role": "system",
165-
"content": """你是 OpenRA 游戏的 AI 助手。用户会用自然语言描述他们想要执行的游戏操作,你需要:
211+
"content": """你是 OpenRA 游戏的 AI 助手。用户用自然语言描述游戏操作,你必须:
212+
213+
1. **每次都先调用 get_game_state 工具**来获取当前游戏状态、资源和可用单位信息
214+
2. 根据获取的信息,选择合适的工具执行用户请求
215+
3. 只能使用真实的 actor_id(整数),不能使用 "MCV"、"actor_1" 这种字符串
166216
167-
1. 理解用户意图
168-
2. 调用相应的工具函数来执行操作
169-
3. 返回操作结果
217+
核心规则:
218+
- 你每次都先 get_game_state 来刷新状态,获取可以操作的 actor_id
219+
- 如果不知道 actor 的 ID,必须先调用 get_game_state 获取所有信息
220+
- 绝不能传入虚构的 ID,只能使用从工具返回的真实 actor_id
170221
171-
可用的主要操作:
172-
- 生产单位:步兵、电厂、重坦、矿车、兵营等
173-
- 查询状态:游戏状态、单位信息、资源信息
174-
- 单位控制:移动、攻击等
222+
**重要:展开基地车的特殊规则:**
223+
- 当用户要求"展开基地车"时,即使 get_game_state 显示没有可见单位,也要调用 deploy_units() 工具(不传参数)
224+
- deploy_units() 工具会自动寻找并部署基地车,不需要提前知道基地车ID
225+
- 不要因为查询不到单位就放弃执行部署命令
175226
176-
请根据用户指令调用合适的工具。"""
227+
可用操作:生产单位、查询状态、移动攻击、部署展开等。"""
177228
},
178229
{"role": "user", "content": user_input}
179230
]
@@ -190,7 +241,8 @@ def process_command_with_ai(self, user_input: str) -> str:
190241

191242
# 如果 AI 选择了工具调用
192243
if message.tool_calls:
193-
results = []
244+
# 构建工具响应消息
245+
tool_messages = [message] # 先添加 assistant 的工具调用消息
194246

195247
for tool_call in message.tool_calls:
196248
tool_name = tool_call.function.name
@@ -200,20 +252,16 @@ def process_command_with_ai(self, user_input: str) -> str:
200252

201253
# 调用工具
202254
result = self.call_tool(tool_name, arguments)
203-
results.append({
204-
"tool": tool_name,
205-
"arguments": arguments,
206-
"result": result
255+
256+
# 添加标准的 tool 消息
257+
tool_messages.append({
258+
"role": "tool",
259+
"tool_call_id": tool_call.id,
260+
"content": json.dumps(result, ensure_ascii=False) if isinstance(result, (dict, list)) else str(result)
207261
})
208262

209-
# 让 AI 总结结果
210-
summary_messages = messages + [
211-
message,
212-
{
213-
"role": "user",
214-
"content": f"工具执行结果:{json.dumps(results, ensure_ascii=False)}。请用简洁的中文总结执行情况。"
215-
}
216-
]
263+
# 让 AI 总结结果 - 使用正确的消息格式
264+
summary_messages = messages + tool_messages
217265

218266
summary_response = self.client.chat.completions.create(
219267
model=self.model,

examples/mofa/agent-hub/openra-copilot-agent/openra_copilot_agent/openra_tools.py

Lines changed: 104 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,45 @@ def get_game_state(self) -> Dict[str, Any]:
2222
"""返回玩家资源、电力和可见单位列表"""
2323
# 1) 玩家基础信息
2424
info = self.api.player_base_info_query()
25-
# 2) 屏幕内可见单位
26-
units = self.api.query_actor(
27-
TargetsQueryParam(
28-
type=[], faction=["任意"], range="screen", restrain=[{"visible": True}]
29-
)
30-
)
31-
visible = [
32-
{
33-
"actor_id": u.actor_id,
34-
"type": u.type,
35-
"faction": u.faction,
36-
"position": {"x": u.position.x, "y": u.position.y},
37-
}
38-
for u in units if u.faction != "中立"
39-
]
25+
26+
# 2) 由于 query_actor API 在当前游戏版本中似乎有问题,我们采用间接方法
27+
visible = []
28+
29+
# 方法1: 先选择所有单位,然后尝试查询(虽然查询会失败,但能确认有单位存在)
30+
game_status = "游戏正在运行"
31+
try:
32+
# 尝试选择屏幕中的所有单位
33+
select_response = self.api._send_request('select_unit', {
34+
'targets': {},
35+
'isCombine': 0
36+
})
37+
if "Selected" in select_response.get('response', ''):
38+
game_status += ",检测到可选择的单位存在"
39+
else:
40+
game_status += ",未检测到可选择的单位"
41+
except:
42+
game_status += ",选择操作失败"
43+
44+
# 方法2: 尝试通过ID暴力查找(这个方法有时能工作)
45+
for actor_id in range(1, 30): # 扩大搜索范围
46+
try:
47+
actor = self.api.get_actor_by_id(actor_id)
48+
if actor:
49+
visible.append({
50+
"actor_id": actor.actor_id,
51+
"type": actor.type,
52+
"faction": actor.faction,
53+
"position": {"x": actor.position.x, "y": actor.position.y},
54+
})
55+
except:
56+
pass
4057

4158
return {
4259
"cash": info.Cash,
4360
"resources": info.Resources,
4461
"power": info.Power,
45-
"visible_units": visible
62+
"visible_units": visible,
63+
"game_status": game_status # 添加游戏状态信息
4664
}
4765

4866
def visible_units(self, type: List[str], faction: str, range: str, restrain: List[dict]) -> List[Dict[str, Any]]:
@@ -187,11 +205,49 @@ def update_actor(self, actor_id: int) -> Optional[Dict[str, Any]]:
187205
"hpPercent": getattr(actor, "hp_percent", None)
188206
}
189207

190-
def deploy_units(self, actor_ids: List[int]) -> str:
191-
"""展开或部署指定单位列表"""
192-
actors = [Actor(i) for i in actor_ids]
193-
self.api.deploy_units(actors)
194-
return "ok"
208+
def deploy_units(self, actor_ids: List[int] = None) -> str:
209+
"""展开或部署指定单位列表,如果没有指定ID则尝试寻找和部署基地车"""
210+
if actor_ids:
211+
# 指定了ID,正常部署
212+
actors = [Actor(i) for i in actor_ids]
213+
self.api.deploy_units(actors)
214+
return "ok"
215+
else:
216+
# 没有指定ID,尝试直接使用 API 调用部署基地车
217+
# 1. 先尝试使用内置的 deploy_mcv_and_wait 方法
218+
try:
219+
self.api.deploy_mcv_and_wait(1.0)
220+
return "成功部署基地车 (使用内置方法)"
221+
except Exception as e:
222+
print(f"内置方法失败: {e}")
223+
224+
# 2. 尝试直接调用 deploy API
225+
deploy_attempts = [
226+
{'targets': {'type': ['基地车'], 'faction': '己方'}},
227+
{'targets': {'type': ['mcv'], 'faction': '己方'}},
228+
{'targets': {'type': ['基地车']}},
229+
{'targets': {'type': ['mcv']}},
230+
{'targets': {'faction': '己方'}}, # 部署所有己方可部署单位
231+
]
232+
233+
for i, params in enumerate(deploy_attempts):
234+
try:
235+
response = self.api._send_request('deploy', params)
236+
return f"成功部署 (尝试{i+1}): {response.get('response', '')}"
237+
except Exception as e:
238+
continue
239+
240+
# 3. 最后尝试通过ID暴力查找MCV
241+
for actor_id in range(1, 50):
242+
try:
243+
actor = self.api.get_actor_by_id(actor_id)
244+
if actor and ('mcv' in actor.type.lower() or 'construction' in actor.type.lower() or '基地车' in actor.type):
245+
self.api.deploy_units([actor])
246+
return f"找到并部署基地车 ID: {actor_id}"
247+
except:
248+
continue
249+
250+
return "未找到基地车可以部署,所有尝试都失败了"
195251

196252
def move_camera_to_actor(self, actor_id: int) -> str:
197253
"""将镜头移动到指定 Actor 的位置"""
@@ -303,4 +359,30 @@ def set_rally_point(self, actor_ids: List[int], x: int, y: int) -> str:
303359
"""为指定建筑设置集结点"""
304360
actors = [Actor(i) for i in actor_ids]
305361
self.api.set_rally_point(actors, Location(x, y))
306-
return "ok"
362+
return "ok"
363+
364+
def start_production(self, unit_type: str, quantity: int = 1, auto_place_building: bool = True) -> int:
365+
"""开始生产单位或建筑,返回等待ID"""
366+
try:
367+
# 直接调用 API
368+
response = self.api._send_request('start_production', {
369+
'units': [{'unit_type': unit_type, 'quantity': quantity}],
370+
'autoPlaceBuilding': auto_place_building
371+
})
372+
result = response.get('data', {})
373+
return result.get('waitId', -1)
374+
except Exception as e:
375+
print(f"生产失败: {e}")
376+
return -1
377+
378+
def place_building(self, queue_type: str, x: int = None, y: int = None) -> str:
379+
"""放置已生产完成的建筑"""
380+
try:
381+
params = {'queueType': queue_type}
382+
if x is not None and y is not None:
383+
params['location'] = {'x': x, 'y': y}
384+
385+
response = self.api._send_request('place_building', params)
386+
return response.get('response', '建筑放置完成')
387+
except Exception as e:
388+
return f"放置建筑失败: {e}"

examples/mofa/examples/openra-single-controller/out/01989498-14b9-7771-beb7-6af494c97b7a/log_openra-copilot-agent-node.txt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,34 @@
55
🎮 收到用户指令: 全部
66
🤖 AI 调用工具: get_game_state 参数: {}
77
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_VVxSjvYZK1bVjhRQrliUeNY6", 'type': 'invalid_request_error', 'param': 'messages.[3].role', 'code': None}}
8+
🎮 收到用户指令: None
9+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "Invalid value for 'content': expected a string, got null.", 'type': 'invalid_request_error', 'param': 'messages.[1].content', 'code': None}}
10+
🎮 收到用户指令: None
11+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "Invalid value for 'content': expected a string, got null.", 'type': 'invalid_request_error', 'param': 'messages.[1].content', 'code': None}}
12+
🎮 收到用户指令: None
13+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "Invalid value for 'content': expected a string, got null.", 'type': 'invalid_request_error', 'param': 'messages.[1].content', 'code': None}}
14+
🎮 收到用户指令: None
15+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "Invalid value for 'content': expected a string, got null.", 'type': 'invalid_request_error', 'param': 'messages.[1].content', 'code': None}}
16+
🎮 收到用户指令: None
17+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "Invalid value for 'content': expected a string, got null.", 'type': 'invalid_request_error', 'param': 'messages.[1].content', 'code': None}}
18+
🎮 收到用户指令: None
19+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "Invalid value for 'content': expected a string, got null.", 'type': 'invalid_request_error', 'param': 'messages.[1].content', 'code': None}}
20+
🎮 收到用户指令: None
21+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "Invalid value for 'content': expected a string, got null.", 'type': 'invalid_request_error', 'param': 'messages.[1].content', 'code': None}}
22+
🎮 收到用户指令: None
23+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "Invalid value for 'content': expected a string, got null.", 'type': 'invalid_request_error', 'param': 'messages.[1].content', 'code': None}}
24+
🎮 收到用户指令: None
25+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "Invalid value for 'content': expected a string, got null.", 'type': 'invalid_request_error', 'param': 'messages.[1].content', 'code': None}}
26+
🎮 收到用户指令: None
27+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "Invalid value for 'content': expected a string, got null.", 'type': 'invalid_request_error', 'param': 'messages.[1].content', 'code': None}}
28+
🎮 收到用户指令: None
29+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "Invalid value for 'content': expected a string, got null.", 'type': 'invalid_request_error', 'param': 'messages.[1].content', 'code': None}}
30+
🎮 收到用户指令: None
31+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "Invalid value for 'content': expected a string, got null.", 'type': 'invalid_request_error', 'param': 'messages.[1].content', 'code': None}}
32+
🎮 收到用户指令: None
33+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "Invalid value for 'content': expected a string, got null.", 'type': 'invalid_request_error', 'param': 'messages.[1].content', 'code': None}}
34+
🎮 收到用户指令: None
35+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "Invalid value for 'content': expected a string, got null.", 'type': 'invalid_request_error', 'param': 'messages.[1].content', 'code': None}}
36+
🎮 收到用户指令: None
37+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "Invalid value for 'content': expected a string, got null.", 'type': 'invalid_request_error', 'param': 'messages.[1].content', 'code': None}}
38+
🎮 收到用户指令: None
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
🎮 收到用户指令: hi
2+
📤 AI 处理结果: Hello! How can I assist you with your OpenRA game today?
3+
🎮 收到用户指令: 展开基地车
4+
🤖 AI 调用工具: get_game_state 参数: {}
5+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_MwwUxYUQdUeBOjzUzIQtWfMA", 'type': 'invalid_request_error', 'param': 'messages.[3].role', 'code': None}}
6+
🎮 收到用户指令: 展初始的基地车
7+
📤 AI 处理结果: ❌ AI 处理失败: Connection error.
8+
🎮 收到用户指令: hi
9+
📤 AI 处理结果: Hello! How can I assist you with your OpenRA game today?
10+
🎮 收到用户指令: 展开初试基地车
11+
🤖 AI 调用工具: get_game_state 参数: {}
12+
📤 AI 处理结果: ❌ AI 处理失败: Error code: 400 - {'error': {'message': "An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_XNi5rUviwNYdQYF2tvI6Gfrg", 'type': 'invalid_request_error', 'param': 'messages.[3].role', 'code': None}}
13+
🎮 收到用户指令: None

0 commit comments

Comments
 (0)