1+ #!/usr/bin/env python3
2+ # -*- coding: utf-8 -*-
3+
4+ """OpenRA Copilot Agent - MoFA 单节点版本 with AI Tool Calling"""
5+
6+ import json
7+ import os
8+ import sys
9+ from typing import Any , Dict , List
10+ from dotenv import load_dotenv
11+ from openai import OpenAI
12+
13+ # 加载环境变量
14+ load_dotenv ()
15+
16+ # 添加 OpenRA 路径
17+ sys .path .append (os .getenv ('OPENRA_PATH' , '/Users/liyao/Code/mofa/OpenCodeAlert/Copilot/openra_ai' ))
18+
19+ from mofa .agent_build .base .base_agent import MofaAgent , run_agent
20+ from .openra_tools import OpenRATools
21+
22+
23+ class OpenRACopilotAgent :
24+ """OpenRA Copilot Agent - 真正的 MCP 风格 AI 工具调用"""
25+
26+ def __init__ (self ):
27+ self .tools = OpenRATools ()
28+ self .client = OpenAI (
29+ api_key = os .getenv ('OPENAI_API_KEY' ),
30+ base_url = os .getenv ('OPENAI_BASE_URL' , 'https://api.openai.com/v1' )
31+ )
32+ self .model = os .getenv ('OPENAI_MODEL' , 'gpt-4o-mini' )
33+
34+ def get_available_tools (self ) -> List [Dict [str , Any ]]:
35+ """返回可用工具的 OpenAI Function Calling 格式定义"""
36+ return [
37+ {
38+ "type" : "function" ,
39+ "function" : {
40+ "name" : "get_game_state" ,
41+ "description" : "获取游戏当前状态,包括资源、电力和可见单位" ,
42+ "parameters" : {"type" : "object" , "properties" : {}}
43+ }
44+ },
45+ {
46+ "type" : "function" ,
47+ "function" : {
48+ "name" : "produce" ,
49+ "description" : "生产指定类型和数量的单位" ,
50+ "parameters" : {
51+ "type" : "object" ,
52+ "properties" : {
53+ "unit_type" : {"type" : "string" , "description" : "单位类型,如 '步兵', '电厂', '重坦', '矿车' 等" },
54+ "quantity" : {"type" : "integer" , "description" : "生产数量" , "minimum" : 1 }
55+ },
56+ "required" : ["unit_type" , "quantity" ]
57+ }
58+ }
59+ },
60+ {
61+ "type" : "function" ,
62+ "function" : {
63+ "name" : "move_units" ,
64+ "description" : "移动一批单位到指定坐标" ,
65+ "parameters" : {
66+ "type" : "object" ,
67+ "properties" : {
68+ "actor_ids" : {"type" : "array" , "items" : {"type" : "integer" }, "description" : "单位ID列表" },
69+ "x" : {"type" : "integer" , "description" : "目标X坐标" },
70+ "y" : {"type" : "integer" , "description" : "目标Y坐标" },
71+ "attack_move" : {"type" : "boolean" , "description" : "是否攻击移动" , "default" : False }
72+ },
73+ "required" : ["actor_ids" , "x" , "y" ]
74+ }
75+ }
76+ },
77+ {
78+ "type" : "function" ,
79+ "function" : {
80+ "name" : "query_actor" ,
81+ "description" : "查询单位列表" ,
82+ "parameters" : {
83+ "type" : "object" ,
84+ "properties" : {
85+ "type" : {"type" : "array" , "items" : {"type" : "string" }, "description" : "单位类型过滤,空数组表示所有类型" },
86+ "faction" : {"type" : "string" , "description" : "阵营: '己方', '敌方', '任意'" , "default" : "己方" },
87+ "range" : {"type" : "string" , "description" : "范围: 'screen', 'all'" , "default" : "all" },
88+ "restrain" : {"type" : "array" , "items" : {"type" : "object" }, "description" : "约束条件" , "default" : []}
89+ },
90+ "required" : []
91+ }
92+ }
93+ },
94+ {
95+ "type" : "function" ,
96+ "function" : {
97+ "name" : "attack_target" ,
98+ "description" : "命令单位攻击目标" ,
99+ "parameters" : {
100+ "type" : "object" ,
101+ "properties" : {
102+ "attacker_id" : {"type" : "integer" , "description" : "攻击者单位ID" },
103+ "target_id" : {"type" : "integer" , "description" : "目标单位ID" }
104+ },
105+ "required" : ["attacker_id" , "target_id" ]
106+ }
107+ }
108+ },
109+ {
110+ "type" : "function" ,
111+ "function" : {
112+ "name" : "player_base_info_query" ,
113+ "description" : "查询玩家基地的资源、电力等基础信息" ,
114+ "parameters" : {"type" : "object" , "properties" : {}}
115+ }
116+ },
117+ {
118+ "type" : "function" ,
119+ "function" : {
120+ "name" : "can_produce" ,
121+ "description" : "检查是否可以生产某种单位" ,
122+ "parameters" : {
123+ "type" : "object" ,
124+ "properties" : {
125+ "unit_type" : {"type" : "string" , "description" : "单位类型" }
126+ },
127+ "required" : ["unit_type" ]
128+ }
129+ }
130+ },
131+ {
132+ "type" : "function" ,
133+ "function" : {
134+ "name" : "ensure_can_produce_unit" ,
135+ "description" : "确保能生产指定单位(会自动补齐依赖建筑并等待完成)" ,
136+ "parameters" : {
137+ "type" : "object" ,
138+ "properties" : {
139+ "unit_name" : {"type" : "string" , "description" : "单位名称" }
140+ },
141+ "required" : ["unit_name" ]
142+ }
143+ }
144+ }
145+ ]
146+
147+ def call_tool (self , tool_name : str , arguments : Dict [str , Any ]) -> Any :
148+ """调用具体的工具函数"""
149+ try :
150+ tool_method = getattr (self .tools , tool_name )
151+ if arguments :
152+ return tool_method (** arguments )
153+ else :
154+ return tool_method ()
155+ except Exception as e :
156+ return f"工具调用失败: { str (e )} "
157+
158+ def process_command_with_ai (self , user_input : str ) -> str :
159+ """使用 AI 解析用户指令并调用相应工具"""
160+ try :
161+ # 构建对话消息
162+ messages = [
163+ {
164+ "role" : "system" ,
165+ "content" : """你是 OpenRA 游戏的 AI 助手。用户会用自然语言描述他们想要执行的游戏操作,你需要:
166+
167+ 1. 理解用户意图
168+ 2. 调用相应的工具函数来执行操作
169+ 3. 返回操作结果
170+
171+ 可用的主要操作:
172+ - 生产单位:步兵、电厂、重坦、矿车、兵营等
173+ - 查询状态:游戏状态、单位信息、资源信息
174+ - 单位控制:移动、攻击等
175+
176+ 请根据用户指令调用合适的工具。"""
177+ },
178+ {"role" : "user" , "content" : user_input }
179+ ]
180+
181+ # 调用 OpenAI API
182+ response = self .client .chat .completions .create (
183+ model = self .model ,
184+ messages = messages ,
185+ tools = self .get_available_tools (),
186+ tool_choice = "auto"
187+ )
188+
189+ message = response .choices [0 ].message
190+
191+ # 如果 AI 选择了工具调用
192+ if message .tool_calls :
193+ results = []
194+
195+ for tool_call in message .tool_calls :
196+ tool_name = tool_call .function .name
197+ arguments = json .loads (tool_call .function .arguments )
198+
199+ print (f"🤖 AI 调用工具: { tool_name } 参数: { arguments } " )
200+
201+ # 调用工具
202+ result = self .call_tool (tool_name , arguments )
203+ results .append ({
204+ "tool" : tool_name ,
205+ "arguments" : arguments ,
206+ "result" : result
207+ })
208+
209+ # 让 AI 总结结果
210+ summary_messages = messages + [
211+ message ,
212+ {
213+ "role" : "user" ,
214+ "content" : f"工具执行结果:{ json .dumps (results , ensure_ascii = False )} 。请用简洁的中文总结执行情况。"
215+ }
216+ ]
217+
218+ summary_response = self .client .chat .completions .create (
219+ model = self .model ,
220+ messages = summary_messages
221+ )
222+
223+ return summary_response .choices [0 ].message .content
224+
225+ else :
226+ # AI 没有选择工具调用,直接返回回答
227+ return message .content
228+
229+ except Exception as e :
230+ return f"❌ AI 处理失败: { str (e )} "
231+
232+
233+ @run_agent
234+ def run (agent : MofaAgent ):
235+ """Agent 主运行函数"""
236+ copilot = OpenRACopilotAgent ()
237+
238+ # 接收用户命令
239+ user_input = agent .receive_parameter ('user_command' )
240+
241+ print (f"🎮 收到用户指令: { user_input } " )
242+
243+ # 使用 AI 处理命令
244+ result = copilot .process_command_with_ai (user_input )
245+
246+ print (f"📤 AI 处理结果: { result } " )
247+
248+ # 发送输出
249+ agent .send_output (agent_output_name = 'copilot_result' , agent_result = result )
250+
251+
252+ def main ():
253+ """主函数"""
254+ agent = MofaAgent (agent_name = 'openra-copilot-agent' )
255+ run (agent = agent )
256+
257+
258+ if __name__ == "__main__" :
259+ main ()
0 commit comments