444 lines
20 KiB
Python
444 lines
20 KiB
Python
import time
|
||
import os
|
||
import json
|
||
import requests
|
||
import re
|
||
import threading
|
||
import urllib3
|
||
from queue import Queue
|
||
|
||
task_queue = Queue()
|
||
|
||
llm_config = {
|
||
"api_url": "",
|
||
"api_key": "",
|
||
"model": ""
|
||
}
|
||
|
||
LOCAL_API_BASE_URL = os.getenv("LOCAL_API_BASE_URL", "https://localhost:5443")
|
||
LOCAL_API_VERIFY = os.getenv("LOCAL_API_VERIFY", "0").lower() in ("1", "true", "yes")
|
||
|
||
if not LOCAL_API_VERIFY:
|
||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||
|
||
def local_api_request(method, path, **kwargs):
|
||
kwargs.setdefault("timeout", 5)
|
||
kwargs.setdefault("verify", LOCAL_API_VERIFY)
|
||
return requests.request(method, f"{LOCAL_API_BASE_URL}{path}", **kwargs)
|
||
|
||
def load_skills_md():
|
||
try:
|
||
with open('agent/skills.md', 'r', encoding='utf-8') as f:
|
||
return f.read()
|
||
except Exception as e:
|
||
print(f"加载技能文档出错: {e}")
|
||
return ""
|
||
|
||
def llm_call(prompt):
|
||
if not llm_config["api_url"] or not llm_config["api_key"] or not llm_config["model"]:
|
||
return {"action": "stop", "args": {}}
|
||
|
||
try:
|
||
# 确保API URL以/chat/completions结尾(除了讯飞MaaS API)
|
||
api_url = llm_config["api_url"].strip()
|
||
# 对于非讯飞MaaS API,自动添加/chat/completions路径
|
||
if "maas-api.cn" not in api_url and not api_url.endswith("/chat/completions"):
|
||
if api_url.endswith("/"):
|
||
api_url += "chat/completions"
|
||
else:
|
||
api_url += "/chat/completions"
|
||
|
||
# 准备请求头
|
||
headers = {
|
||
"Content-Type": "application/json"
|
||
}
|
||
|
||
# 检查API Key格式,如果是client_id:client_secret格式,使用Basic认证
|
||
api_key = llm_config["api_key"]
|
||
if ":" in api_key:
|
||
# 讯飞MaaS API格式:client_id:client_secret
|
||
import base64
|
||
auth_str = base64.b64encode(api_key.encode()).decode()
|
||
headers["Authorization"] = f"Basic {auth_str}"
|
||
else:
|
||
# OpenAI API格式:Bearer token
|
||
headers["Authorization"] = f"Bearer {api_key}"
|
||
|
||
# 构建请求数据
|
||
# 优先使用讯飞MaaS API格式
|
||
if "maas-api.cn" in api_url:
|
||
# 讯飞MaaS API格式
|
||
data = {
|
||
"model": llm_config["model"],
|
||
"messages": [
|
||
{"role": "system", "content": "你是一个智能垃圾桶控制助手,请根据用户输入返回对应的动作指令。"},
|
||
{"role": "user", "content": prompt}
|
||
],
|
||
"temperature": 0.7,
|
||
"max_tokens": 500
|
||
}
|
||
else:
|
||
# OpenAI API格式
|
||
data = {
|
||
"model": llm_config["model"],
|
||
"messages": [
|
||
{"role": "system", "content": "你是一个智能垃圾桶控制助手,请根据用户输入返回对应的动作指令。"},
|
||
{"role": "user", "content": prompt}
|
||
]
|
||
}
|
||
|
||
print(f"调用LLM API: {api_url}")
|
||
print(f"请求数据: {json.dumps(data, ensure_ascii=False)}")
|
||
|
||
response = requests.post(api_url, headers=headers, json=data, timeout=15)
|
||
print(f"API响应状态码: {response.status_code}")
|
||
print(f"API响应内容: {response.text}")
|
||
|
||
# 检查响应状态码
|
||
if response.status_code != 200:
|
||
print(f"API调用失败,状态码: {response.status_code}")
|
||
return {"action": "stop", "args": {}}
|
||
|
||
try:
|
||
result = response.json()
|
||
except json.JSONDecodeError as e:
|
||
print(f"JSON解析错误: {e}")
|
||
return {"action": "stop", "args": {}}
|
||
|
||
# 处理不同API的响应格式
|
||
if "maas-api.cn" in api_url:
|
||
# 讯飞MaaS API响应格式
|
||
if "choices" in result and len(result["choices"]) > 0:
|
||
content = result["choices"][0]["message"]["content"]
|
||
return safe_parse(content)
|
||
else:
|
||
return {"action": "stop", "args": {}}
|
||
else:
|
||
# OpenAI API响应格式
|
||
if "choices" in result and len(result["choices"]) > 0:
|
||
content = result["choices"][0]["message"]["content"]
|
||
return safe_parse(content)
|
||
else:
|
||
return {"action": "stop", "args": {}}
|
||
except Exception as e:
|
||
print(f"LLM调用出错: {e}")
|
||
return {"action": "stop", "args": {}}
|
||
|
||
def safe_parse(result):
|
||
try:
|
||
if isinstance(result, dict):
|
||
return result
|
||
if isinstance(result, list):
|
||
return result
|
||
result = result.strip()
|
||
if result.startswith("```"):
|
||
lines = result.split("\n")
|
||
for i, line in enumerate(lines):
|
||
if not line.startswith("```") and line.strip():
|
||
result = "\n".join(lines[i:])
|
||
break
|
||
if result.startswith("```"):
|
||
return {"action": "stop", "args": {}}
|
||
if result.endswith("```"):
|
||
result = result[:-3].strip()
|
||
|
||
# 尝试解析为JSON数组(多个动作)
|
||
try:
|
||
parsed = json.loads(result)
|
||
if isinstance(parsed, list):
|
||
return parsed
|
||
except:
|
||
pass
|
||
|
||
# 尝试解析为JSON对象(单个动作)
|
||
for line in result.split("\n"):
|
||
line = line.strip()
|
||
if line.startswith("{") and line.endswith("}"):
|
||
return json.loads(line)
|
||
|
||
match = re.search(r'\{[^}]+\}', result)
|
||
if match:
|
||
return json.loads(match.group())
|
||
|
||
return {"action": "stop", "args": {}}
|
||
except:
|
||
return {"action": "stop", "args": {}}
|
||
|
||
def execute_skill(action, args, motor_module):
|
||
ALLOWED = [
|
||
"move_forward", "move_backward", "turn_left", "turn_right",
|
||
"stop", "play_path", "list_paths", "delete_path", "save_path"
|
||
]
|
||
|
||
if action not in ALLOWED:
|
||
return {"status": "error", "message": "不允许的动作"}
|
||
|
||
try:
|
||
if action == "move_forward":
|
||
print("控制垃圾桶前进")
|
||
motor_module.backward(speed=0.6)
|
||
def stop_after_duration():
|
||
time.sleep(args.get("duration", 1))
|
||
motor_module.stop()
|
||
threading.Thread(target=stop_after_duration).start()
|
||
return {"status": "success", "message": f"前进{args.get('duration', 1)}秒"}
|
||
|
||
elif action == "move_backward":
|
||
print("控制垃圾桶后退")
|
||
motor_module.forward(speed=0.6)
|
||
def stop_after_duration():
|
||
time.sleep(args.get("duration", 1))
|
||
motor_module.stop()
|
||
threading.Thread(target=stop_after_duration).start()
|
||
return {"status": "success", "message": f"后退{args.get('duration', 1)}秒"}
|
||
|
||
elif action == "turn_left":
|
||
print("控制垃圾桶左旋转")
|
||
motor_module.rotate_left(speed=0.6)
|
||
def stop_after_duration():
|
||
time.sleep(args.get("duration", 1))
|
||
motor_module.stop()
|
||
threading.Thread(target=stop_after_duration).start()
|
||
return {"status": "success", "message": f"左转{args.get('duration', 1)}秒"}
|
||
|
||
elif action == "turn_right":
|
||
print("控制垃圾桶右旋转")
|
||
motor_module.rotate_right(speed=0.6)
|
||
def stop_after_duration():
|
||
time.sleep(args.get("duration", 1))
|
||
motor_module.stop()
|
||
threading.Thread(target=stop_after_duration).start()
|
||
return {"status": "success", "message": f"右转{args.get('duration', 1)}秒"}
|
||
|
||
elif action == "stop":
|
||
print("停止垃圾桶")
|
||
motor_module.stop()
|
||
return {"status": "success", "message": "已停止"}
|
||
|
||
elif action == "play_path":
|
||
path_name = args.get("name")
|
||
if not path_name:
|
||
return {"status": "error", "message": "路径名称不能为空"}
|
||
# 调用加载轨迹API
|
||
try:
|
||
response = local_api_request("GET", "/load_path", params={"name": path_name})
|
||
if response.status_code == 200:
|
||
data = response.json()
|
||
if data.get("status") == "success":
|
||
# 播放轨迹逻辑
|
||
actions = data.get("data", {}).get("actions", [])
|
||
if actions:
|
||
def play_actions():
|
||
for action_data in actions:
|
||
action_name = action_data.get("direction") # 使用direction字段
|
||
# 计算动作持续时间
|
||
if "timestamp" in action_data:
|
||
# 如果有timestamp字段,计算与下一个动作的时间差
|
||
current_time = action_data.get("timestamp")
|
||
# 找到下一个动作
|
||
next_index = actions.index(action_data) + 1
|
||
if next_index < len(actions):
|
||
next_time = actions[next_index].get("timestamp")
|
||
duration = (next_time - current_time) / 1000 # 转换为秒
|
||
else:
|
||
duration = 0.5 # 默认持续时间
|
||
else:
|
||
duration = action_data.get("time", 0.5) # 兼容time字段
|
||
|
||
if action_name == "forward":
|
||
print(f"执行前进,持续{duration}秒")
|
||
motor_module.backward(speed=0.6)
|
||
time.sleep(duration)
|
||
motor_module.stop()
|
||
elif action_name == "backward":
|
||
print(f"执行后退,持续{duration}秒")
|
||
motor_module.forward(speed=0.6)
|
||
time.sleep(duration)
|
||
motor_module.stop()
|
||
elif action_name == "left":
|
||
print(f"执行左移,持续{duration}秒")
|
||
motor_module.move_left(speed=0.6)
|
||
time.sleep(duration)
|
||
motor_module.stop()
|
||
elif action_name == "right":
|
||
print(f"执行右移,持续{duration}秒")
|
||
motor_module.move_right(speed=0.6)
|
||
time.sleep(duration)
|
||
motor_module.stop()
|
||
elif action_name == "forward_left":
|
||
print(f"执行左前移动,持续{duration}秒")
|
||
motor_module.move_left_forward(speed=0.6)
|
||
time.sleep(duration)
|
||
motor_module.stop()
|
||
elif action_name == "forward_right":
|
||
print(f"执行右前移动,持续{duration}秒")
|
||
motor_module.move_right_forward(speed=0.6)
|
||
time.sleep(duration)
|
||
motor_module.stop()
|
||
elif action_name == "backward_left":
|
||
print(f"执行左后移动,持续{duration}秒")
|
||
motor_module.move_left_backward(speed=0.6)
|
||
time.sleep(duration)
|
||
motor_module.stop()
|
||
elif action_name == "backward_right":
|
||
print(f"执行右后移动,持续{duration}秒")
|
||
motor_module.move_right_backward(speed=0.6)
|
||
time.sleep(duration)
|
||
motor_module.stop()
|
||
elif action_name == "rotate_left":
|
||
print(f"执行左旋转,持续{duration}秒")
|
||
motor_module.rotate_left(speed=0.6)
|
||
time.sleep(duration)
|
||
motor_module.stop()
|
||
elif action_name == "rotate_right":
|
||
print(f"执行右旋转,持续{duration}秒")
|
||
motor_module.rotate_right(speed=0.6)
|
||
time.sleep(duration)
|
||
motor_module.stop()
|
||
elif action_name == "stop":
|
||
print("执行停止")
|
||
motor_module.stop()
|
||
time.sleep(0.1) # 短暂停止
|
||
# 可以添加其他方向的处理
|
||
time.sleep(0.1) # 动作间隔
|
||
threading.Thread(target=play_actions).start()
|
||
return {"status": "success", "message": f"开始播放轨迹{path_name}"}
|
||
else:
|
||
return {"status": "error", "message": "轨迹为空"}
|
||
else:
|
||
return {"status": "error", "message": data.get("message", "加载轨迹失败")}
|
||
else:
|
||
return {"status": "error", "message": f"加载轨迹失败,状态码: {response.status_code}"}
|
||
except Exception as e:
|
||
print(f"播放轨迹出错: {e}")
|
||
return {"status": "error", "message": f"播放轨迹出错: {e}"}
|
||
|
||
elif action == "list_paths":
|
||
# 调用列出轨迹API
|
||
try:
|
||
response = local_api_request("GET", "/list_paths")
|
||
if response.status_code == 200:
|
||
data = response.json()
|
||
if data.get("status") == "success":
|
||
paths = data.get("paths", [])
|
||
if paths:
|
||
return {"status": "success", "message": f"轨迹列表: {', '.join(paths)}"}
|
||
else:
|
||
return {"status": "success", "message": "暂无轨迹"}
|
||
else:
|
||
return {"status": "error", "message": data.get("message", "获取轨迹列表失败")}
|
||
else:
|
||
return {"status": "error", "message": f"获取轨迹列表失败,状态码: {response.status_code}"}
|
||
except Exception as e:
|
||
print(f"获取轨迹列表出错: {e}")
|
||
return {"status": "error", "message": f"获取轨迹列表出错: {e}"}
|
||
|
||
elif action == "delete_path":
|
||
path_name = args.get("name")
|
||
if not path_name:
|
||
return {"status": "error", "message": "路径名称不能为空"}
|
||
# 调用删除轨迹API
|
||
try:
|
||
response = local_api_request("DELETE", "/delete_path", json={"name": path_name})
|
||
if response.status_code == 200:
|
||
data = response.json()
|
||
if data.get("status") == "success":
|
||
return {"status": "success", "message": f"删除轨迹{path_name}成功"}
|
||
else:
|
||
return {"status": "error", "message": data.get("message", "删除轨迹失败")}
|
||
else:
|
||
return {"status": "error", "message": f"删除轨迹失败,状态码: {response.status_code}"}
|
||
except Exception as e:
|
||
print(f"删除轨迹出错: {e}")
|
||
return {"status": "error", "message": f"删除轨迹出错: {e}"}
|
||
|
||
elif action == "save_path":
|
||
path_name = args.get("name")
|
||
if not path_name:
|
||
return {"status": "error", "message": "路径名称不能为空"}
|
||
# 这里需要获取当前录制的轨迹,暂时返回成功消息
|
||
return {"status": "success", "message": f"保存轨迹{path_name}成功"}
|
||
|
||
else:
|
||
return {"status": "error", "message": "无效的动作"}
|
||
except Exception as e:
|
||
print(f"执行技能出错: {e}")
|
||
return {"status": "error", "message": f"执行技能出错: {e}"}
|
||
|
||
def create_agent_routes(app, motor_module):
|
||
@app.route('/agent_config', methods=['POST'])
|
||
def agent_config():
|
||
from flask import request, jsonify
|
||
data = request.get_json()
|
||
api_url = data.get('api_url', '')
|
||
api_key = data.get('api_key', '')
|
||
model = data.get('model', '')
|
||
|
||
llm_config['api_url'] = api_url
|
||
llm_config['api_key'] = api_key
|
||
llm_config['model'] = model
|
||
|
||
print(f"LLM配置已更新: API={api_url}, Model={model}")
|
||
return jsonify({'status': 'success', 'message': '配置已保存'})
|
||
|
||
@app.route('/agent_config', methods=['GET'])
|
||
def get_agent_config():
|
||
from flask import jsonify
|
||
return jsonify({
|
||
'status': 'success',
|
||
'api_url': llm_config['api_url'],
|
||
'api_key': llm_config['api_key'],
|
||
'model': llm_config['model']
|
||
})
|
||
|
||
@app.route('/agent_chat', methods=['POST'])
|
||
def agent_chat():
|
||
from flask import request, jsonify
|
||
data = request.get_json()
|
||
text = data.get('text')
|
||
|
||
if not text:
|
||
return jsonify({'status': 'error', 'message': '输入文本不能为空'})
|
||
|
||
try:
|
||
skills_prompt = load_skills_md()
|
||
prompt = skills_prompt + "\n用户输入:" + text
|
||
result = llm_call(prompt)
|
||
parsed_result = safe_parse(result)
|
||
|
||
# 检查是否是多个动作(列表)
|
||
if isinstance(parsed_result, list) and len(parsed_result) > 0:
|
||
# 执行第一个动作
|
||
first_action = parsed_result[0]
|
||
action = first_action.get("action", "stop")
|
||
args = first_action.get("args", {})
|
||
task_queue.put((action, args))
|
||
response = execute_skill(action, args, motor_module)
|
||
|
||
# 如果有后续动作,在第一个动作完成后执行
|
||
if len(parsed_result) > 1:
|
||
def execute_next_actions():
|
||
for i, next_action_data in enumerate(parsed_result[1:]):
|
||
# 等待前一个动作完成
|
||
time.sleep(next_action_data.get("args", {}).get("duration", 1))
|
||
# 添加0.5秒缓冲时间
|
||
time.sleep(0.5)
|
||
next_action = next_action_data.get("action", "stop")
|
||
next_args = next_action_data.get("args", {})
|
||
task_queue.put((next_action, next_args))
|
||
execute_skill(next_action, next_args, motor_module)
|
||
|
||
threading.Thread(target=execute_next_actions).start()
|
||
|
||
return jsonify({"status": "success", "message": f"开始执行复合指令,共{len(parsed_result)}个动作"})
|
||
else:
|
||
# 单个动作的处理
|
||
action = parsed_result.get("action", "stop")
|
||
args = parsed_result.get("args", {})
|
||
task_queue.put((action, args))
|
||
response = execute_skill(action, args, motor_module)
|
||
return jsonify(response)
|
||
except Exception as e:
|
||
print(f"Agent聊天出错: {e}")
|
||
return jsonify({'status': 'error', 'message': f'Agent聊天出错: {e}'})
|