From f48facde942f938111ffdf2622f1c77f7a3fd512 Mon Sep 17 00:00:00 2001 From: Cx330 <1487537121@qq.com> Date: Mon, 20 Apr 2026 14:19:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Eagent=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/CSS/agent.css | 175 ++++++++++++++ main/CSS/control.css | 3 +- main/CSS/record.css | 6 +- main/CSS/style.css | 10 - main/Javascript/agent.js | 180 +++++++++++++++ main/Javascript/script.js | 57 ----- main/Path/轨迹.json | 21 ++ main/__pycache__/agent.cpython-38.pyc | Bin 0 -> 6704 bytes main/__pycache__/motor.cpython-313.pyc | Bin 0 -> 5628 bytes main/__pycache__/motor.cpython-38.pyc | Bin 3564 -> 3564 bytes main/agent.py | 308 +++++++++++++++++++++++++ main/agent/skills.md | 108 +++++++++ main/index.html | 46 +++- main/server.py | 33 +-- 14 files changed, 856 insertions(+), 91 deletions(-) create mode 100644 main/CSS/agent.css create mode 100644 main/Javascript/agent.js create mode 100644 main/Path/轨迹.json create mode 100644 main/__pycache__/agent.cpython-38.pyc create mode 100644 main/__pycache__/motor.cpython-313.pyc create mode 100644 main/agent.py create mode 100644 main/agent/skills.md diff --git a/main/CSS/agent.css b/main/CSS/agent.css new file mode 100644 index 0000000..3e6d5e9 --- /dev/null +++ b/main/CSS/agent.css @@ -0,0 +1,175 @@ +/* Agent聊天区域样式 */ +.agent-chat { + margin-top: 30px; + padding: 20px; + background: linear-gradient(145deg, #e6e6e6, #ffffff); + border-radius: 15px; + box-shadow: 10px 10px 20px rgba(0, 0, 0, 0.1), -10px -10px 20px rgba(255, 255, 255, 0.7); + width: 100%; + max-width: 600px; +} + +.agent-chat h3 { + margin-top: 0; + color: #333; + text-align: center; +} + +/* LLM配置区域 */ +.llm-config { + margin-bottom: 20px; + padding: 15px; + background: #f5f5f5; + border-radius: 10px; + box-shadow: inset 3px 3px 6px rgba(0, 0, 0, 0.1), inset -3px -3px 6px rgba(255, 255, 255, 0.7); +} + +.llm-config h4 { + margin: 0 0 10px 0; + color: #555; + font-size: 14px; +} + +.config-row { + display: flex; + gap: 10px; + margin-bottom: 10px; + align-items: center; +} + +.config-row:last-child { + margin-bottom: 0; +} + +.config-row label { + width: 70px; + color: #666; + font-size: 14px; + flex-shrink: 0; +} + +.config-row input { + flex: 1; + padding: 8px; + border: none; + border-radius: 8px; + background: #ffffff; + box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.1), inset -2px -2px 4px rgba(255, 255, 255, 0.7); + font-size: 14px; +} + +.config-row input:focus { + outline: none; + box-shadow: inset 3px 3px 6px rgba(0, 0, 0, 0.15), inset -3px -3px 6px rgba(255, 255, 255, 0.8); +} + +.btn-config { + background: linear-gradient(145deg, #2196F3, #1976D2); + color: white; + border: none; + border-radius: 8px; + padding: 8px 16px; + font-size: 14px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 3px 3px 6px rgba(0, 0, 0, 0.1), -3px -3px 6px rgba(255, 255, 255, 0.7); + margin-left: 80px; +} + +.btn-config:hover { + transform: translateY(-2px); + box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.15), -5px -5px 10px rgba(255, 255, 255, 0.8); +} + +/* 聊天输入区域 */ +.chat-input { + display: flex; + gap: 10px; + margin-bottom: 20px; + justify-content: center; +} + +.chat-input input { + padding: 10px; + border: none; + border-radius: 10px; + background: #f5f5f5; + box-shadow: inset 5px 5px 10px rgba(0, 0, 0, 0.1), inset -5px -5px 10px rgba(255, 255, 255, 0.7); + width: 250px; + font-size: 16px; +} + +.chat-history { + max-height: 300px; + overflow-y: auto; + background: #f5f5f5; + border-radius: 10px; + padding: 15px; + box-shadow: inset 5px 5px 10px rgba(0, 0, 0, 0.1), inset -5px -5px 10px rgba(255, 255, 255, 0.7); +} + +.message { + margin-bottom: 10px; + padding: 10px; + border-radius: 10px; + max-width: 80%; +} + +.message.user { + background: linear-gradient(145deg, #E3F2FD, #BBDEFB); + margin-left: auto; + box-shadow: 3px 3px 6px rgba(0, 0, 0, 0.1), -3px -3px 6px rgba(255, 255, 255, 0.7); +} + +.message.agent { + background: linear-gradient(145deg, #E8F5E8, #C8E6C9); + margin-right: auto; + box-shadow: 3px 3px 6px rgba(0, 0, 0, 0.1), -3px -3px 6px rgba(255, 255, 255, 0.7); +} + +.message p { + margin: 0; + color: #333; +} + +/* 响应式设计 - Agent聊天 */ +@media (max-width: 650px) { + .chat-input { + flex-direction: column; + align-items: center; + } + + .chat-input input { + width: 100%; + max-width: 300px; + } + + .agent-chat { + padding: 15px; + } + + .message { + max-width: 90%; + } + + .config-row { + flex-direction: column; + align-items: flex-start; + } + + .config-row label { + width: auto; + margin-bottom: 5px; + } + + .config-row input { + width: 100%; + } + + .btn-config { + margin-left: 0; + width: 100%; + margin-top: 10px; + } +} \ No newline at end of file diff --git a/main/CSS/control.css b/main/CSS/control.css index 5830af6..36561a9 100644 --- a/main/CSS/control.css +++ b/main/CSS/control.css @@ -268,4 +268,5 @@ .title { font-size: 20px; } -} \ No newline at end of file +} + diff --git a/main/CSS/record.css b/main/CSS/record.css index 4d46d72..53df625 100644 --- a/main/CSS/record.css +++ b/main/CSS/record.css @@ -120,17 +120,17 @@ flex-direction: column; align-items: center; } - + .path-input input { width: 100%; max-width: 300px; } - + .btn-save { width: 100%; max-width: 300px; } - + .path-management { padding: 15px; } diff --git a/main/CSS/style.css b/main/CSS/style.css index 533da69..1e43f28 100644 --- a/main/CSS/style.css +++ b/main/CSS/style.css @@ -56,16 +56,6 @@ h1 { color: white; } -.btn-record { - background-color: #ff9800; - color: white; -} - -.btn-playback { - background-color: #2196f3; - color: white; -} - .btn:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); diff --git a/main/Javascript/agent.js b/main/Javascript/agent.js new file mode 100644 index 0000000..8903416 --- /dev/null +++ b/main/Javascript/agent.js @@ -0,0 +1,180 @@ +// Agent聊天功能 +document.addEventListener('DOMContentLoaded', function() { + const agentInput = document.getElementById('agentInput'); + const sendAgentBtn = document.getElementById('sendAgentBtn'); + const chatHistory = document.getElementById('chatHistory'); + const status = document.getElementById('status'); + + // LLM配置相关元素 + const apiUrlInput = document.getElementById('apiUrlInput'); + const apiKeyInput = document.getElementById('apiKeyInput'); + const modelInput = document.getElementById('modelInput'); + const saveConfigBtn = document.getElementById('saveConfigBtn'); + + // 添加清除缓存按钮 + const clearCacheBtn = document.createElement('button'); + clearCacheBtn.className = 'btn btn-config'; + clearCacheBtn.id = 'clearCacheBtn'; + clearCacheBtn.textContent = '清除缓存'; + saveConfigBtn.parentNode.appendChild(clearCacheBtn); + + // 从本地存储加载配置 + function loadConfigFromLocalStorage() { + const savedConfig = localStorage.getItem('llmConfig'); + if (savedConfig) { + try { + const config = JSON.parse(savedConfig); + apiUrlInput.value = config.api_url || ''; + apiKeyInput.value = config.api_key || ''; + modelInput.value = config.model || ''; + return true; + } catch (error) { + console.log('解析本地存储配置失败:', error); + } + } + return false; + } + + // 保存配置到本地存储 + function saveConfigToLocalStorage(config) { + localStorage.setItem('llmConfig', JSON.stringify(config)); + } + + // 清除本地存储 + function clearLocalStorage() { + localStorage.removeItem('llmConfig'); + apiUrlInput.value = ''; + apiKeyInput.value = ''; + modelInput.value = ''; + status.innerHTML = '

缓存已清除

'; + } + + // 加载配置 + function loadConfig() { + // 优先从本地存储加载 + if (!loadConfigFromLocalStorage()) { + // 如果本地存储没有,从后端加载 + fetch('/agent_config') + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + apiUrlInput.value = data.api_url || ''; + apiKeyInput.value = data.api_key || ''; + modelInput.value = data.model || ''; + // 保存到本地存储 + saveConfigToLocalStorage({ + api_url: data.api_url || '', + api_key: data.api_key || '', + model: data.model || '' + }); + } + }) + .catch(error => { + console.log('加载配置失败:', error); + status.innerHTML = `

加载配置失败: ${error.message}

`; + }); + } + } + + // 保存配置 + function saveConfig() { + const config = { + api_url: apiUrlInput.value.trim(), + api_key: apiKeyInput.value.trim(), + model: modelInput.value.trim() + }; + + console.log('保存配置:', config); + + // 保存到本地存储 + saveConfigToLocalStorage(config); + + fetch('/agent_config', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(config) + }) + .then(response => response.json()) + .then(data => { + console.log('保存配置响应:', data); + if (data.status === 'success') { + status.innerHTML = '

配置已保存

'; + } else { + status.innerHTML = `

配置保存失败: ${data.message}

`; + } + }) + .catch(error => { + console.error('配置保存失败:', error); + status.innerHTML = `

配置保存失败: ${error.message}

`; + }); + } + + // 发送消息 + function sendMessage() { + const text = agentInput.value.trim(); + if (!text) return; + + addMessage('user', text); + agentInput.value = ''; + status.innerHTML = '

处理中...

'; + + fetch('/agent_chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ text: text }) + }) + .then(response => response.json()) + .then(data => { + console.log('聊天响应:', data); + addMessage('agent', data.message || '处理完成'); + + // 更新状态 + status.innerHTML = `

${data.message || '处理完成'}

`; + }) + .catch(error => { + console.error('通信错误:', error); + addMessage('agent', '通信错误,请重试'); + status.innerHTML = `

通信错误: ${error.message}

`; + }); + } + + // 添加消息到聊天历史 + function addMessage(type, content) { + const messageDiv = document.createElement('div'); + messageDiv.className = `message ${type}`; + messageDiv.innerHTML = `

${content}

`; + chatHistory.appendChild(messageDiv); + chatHistory.scrollTop = chatHistory.scrollHeight; + } + + // 绑定发送按钮点击事件 + if (sendAgentBtn) { + sendAgentBtn.addEventListener('click', sendMessage); + } + + // 绑定回车键发送 + if (agentInput) { + agentInput.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + sendMessage(); + } + }); + } + + // 绑定保存配置按钮 + if (saveConfigBtn) { + saveConfigBtn.addEventListener('click', saveConfig); + } + + // 绑定清除缓存按钮 + if (clearCacheBtn) { + clearCacheBtn.addEventListener('click', clearLocalStorage); + } + + // 页面加载时获取配置 + loadConfig(); +}); \ No newline at end of file diff --git a/main/Javascript/script.js b/main/Javascript/script.js index fadfe1e..502ec25 100644 --- a/main/Javascript/script.js +++ b/main/Javascript/script.js @@ -1,25 +1,12 @@ // 获取按钮和状态元素 const forwardBtn = document.getElementById('forwardBtn'); const backwardBtn = document.getElementById('backwardBtn'); -const recordBtn = document.getElementById('recordBtn'); -const playbackBtn = document.getElementById('playbackBtn'); const status = document.getElementById('status'); -// 录制相关变量 -let isRecording = false; -let recordedActions = []; -let recordingStartTime = 0; - // 实际控制函数 function controlMotor(direction) { status.innerHTML = `

正在${direction === 'forward' ? '前进' : '后退'}...

`; - // 记录操作(如果正在录制) - if (isRecording) { - const timestamp = Date.now() - recordingStartTime; - recordedActions.push({ direction, timestamp }); - } - // 发送AJAX请求到后端服务器 fetch('/control', { method: 'POST', @@ -41,50 +28,6 @@ function controlMotor(direction) { }); } -// 录制按钮点击事件 -recordBtn.addEventListener('click', () => { - if (isRecording) { - // 停止录制 - isRecording = false; - recordBtn.textContent = '开始录制'; - status.innerHTML = `

录制完成,共记录 ${recordedActions.length} 个操作

`; - } else { - // 开始录制 - isRecording = true; - recordedActions = []; - recordingStartTime = Date.now(); - recordBtn.textContent = '停止录制'; - status.innerHTML = `

开始录制...

`; - } -}); - -// 回放按钮点击事件 -playbackBtn.addEventListener('click', () => { - if (recordedActions.length === 0) { - status.innerHTML = `

没有可回放的操作

`; - return; - } - - status.innerHTML = `

开始回放...

`; - - // 按时间顺序回放操作 - let lastTimestamp = 0; - recordedActions.forEach((action, index) => { - setTimeout(() => { - controlMotor(action.direction); - - // 最后一个操作完成后显示回放完成 - if (index === recordedActions.length - 1) { - setTimeout(() => { - status.innerHTML = `

回放完成

`; - }, 2000); - } - }, action.timestamp - lastTimestamp); - - lastTimestamp = action.timestamp; - }); -}); - // 按钮点击事件 forwardBtn.addEventListener('click', () => { controlMotor('forward'); diff --git a/main/Path/轨迹.json b/main/Path/轨迹.json new file mode 100644 index 0000000..0ab9e04 --- /dev/null +++ b/main/Path/轨迹.json @@ -0,0 +1,21 @@ +{ + "name": "轨迹", + "actions": [ + { + "direction": "forward", + "timestamp": 858 + }, + { + "direction": "stop", + "timestamp": 1166 + }, + { + "direction": "backward", + "timestamp": 2327 + }, + { + "direction": "stop", + "timestamp": 2642 + } + ] +} \ No newline at end of file diff --git a/main/__pycache__/agent.cpython-38.pyc b/main/__pycache__/agent.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..543fd137568c39baf8cacc20db6b264489879056 GIT binary patch literal 6704 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 literal 0 HcmV?d00001 diff --git a/main/__pycache__/motor.cpython-313.pyc b/main/__pycache__/motor.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4f49b9b924ce79b311d5d611fe315427434d29f GIT binary patch literal 5628 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 literal 0 HcmV?d00001 diff --git a/main/__pycache__/motor.cpython-38.pyc b/main/__pycache__/motor.cpython-38.pyc index 5f6e650dd6fd9b02780135fd3098d0acc5d5ccf0..86c66f9d84467d9e01cc42eac3469d0121d28f84 100644 GIT binary patch delta 41 scmaDO{YIKEl$V!_0SJWJpJcA*-^iE1%3%x!kL@Q1a!75Sz#78=0PV92YybcN delta 41 tcmaDO{YIKEl$V!_0SHV@?`OsdY~)K|<(M@S1YX)t4&;#9Jb^Wa0{{c+4O0LB diff --git a/main/agent.py b/main/agent.py new file mode 100644 index 0000000..0bc9f73 --- /dev/null +++ b/main/agent.py @@ -0,0 +1,308 @@ +import time +import os +import json +import requests +import re +import threading +from queue import Queue + +task_queue = Queue() + +llm_config = { + "api_url": "", + "api_key": "", + "model": "" +} + +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": "路径名称不能为空"} + return {"status": "success", "message": f"播放轨迹{path_name}"} + + elif action == "list_paths": + return {"status": "success", "message": "获取轨迹列表"} + + elif action == "delete_path": + path_name = args.get("name") + if not path_name: + return {"status": "error", "message": "路径名称不能为空"} + return {"status": "success", "message": f"删除轨迹{path_name}"} + + 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}'}) \ No newline at end of file diff --git a/main/agent/skills.md b/main/agent/skills.md new file mode 100644 index 0000000..8659724 --- /dev/null +++ b/main/agent/skills.md @@ -0,0 +1,108 @@ +# 垃圾桶控制技能文档 + +你是一个智能垃圾桶控制助手,可以调用以下技能控制垃圾桶运动。你必须而且只能返回JSON格式的指令,不要返回任何其他内容。 + +--- + +## 一、基础运动技能 + +### 1. move_forward +向前移动垃圾桶 + +参数: +- duration: 持续时间(秒) + +### 2. move_backward +向后移动垃圾桶 + +参数: +- duration: 秒 + +### 3. turn_left +左转 + +参数: +- duration: 秒 + +### 4. turn_right +右转 + +参数: +- duration: 秒 + +### 5. stop +停止所有运动 + +参数:无 + +--- + +## 二、轨迹技能(重要) + +### 6. play_path +播放已保存轨迹 + +参数: +- name: 轨迹名称 + +说明: +调用系统中已经录制好的路径,让垃圾桶自动执行。 + +### 7. save_path +保存当前录制轨迹 + +参数: +- name: 轨迹名称 + +### 8. list_paths +获取所有轨迹列表 + +参数:无 + +### 9. delete_path +删除轨迹 + +参数: +- name: 轨迹名称 + +--- + +## 三、规则(必须严格遵守) + +1. **只能返回JSON格式,不要返回任何解释或额外文字** +2. 用户说"停"、"停止" → stop +3. 用户说"前进X秒" → move_forward,duration为X +4. 用户说"后退X秒" → move_backward,duration为X +5. 用户说"左转X秒" → turn_left,duration为X +6. 用户说"右转X秒" → turn_right,duration为X +7. 用户说"执行路径xxx" → play_path,name为xxx +8. 用户说"列出路径" → list_paths +9. 用户说"删除路径xxx" → delete_path,name为xxx +10. **用户说"动作1之后动作2"或"动作1然后动作2"等复合指令时,返回包含多个动作的JSON数组** + 例如:"前进1秒之后回退1秒" → [{"action": "move_forward", "args": {"duration": 1}}, {"action": "move_backward", "args": {"duration": 1}}] +11. 如果不确定运动时间,duration默认1秒 +12. 如果无法理解用户意图,返回:{"action": "stop", "args": {}} + +--- + +## 四、输出格式示例 + +用户说:前进3秒 +输出: +{"action": "move_forward", "args": {"duration": 3}} + +用户说:停止 +输出: +{"action": "stop", "args": {}} + +用户说:执行路径test1 +输出: +{"action": "play_path", "args": {"name": "test1"}} + +用户说:前进1秒之后回退1秒 +输出: +[{"action": "move_forward", "args": {"duration": 1}}, {"action": "move_backward", "args": {"duration": 1}}] + +--- + +**重要:你只能返回JSON格式的指令,不要包含任何其他文字、解释或格式符号(如markdown代码块)。** \ No newline at end of file diff --git a/main/index.html b/main/index.html index f2a6732..810c15e 100644 --- a/main/index.html +++ b/main/index.html @@ -6,11 +6,12 @@ 垃圾桶控制界面 +

垃圾桶控制界面

- +
@@ -32,13 +33,13 @@
- +
- +
@@ -46,7 +47,7 @@
- +

轨迹管理

@@ -59,15 +60,48 @@
    - +

    就绪

    + + +
    +

    智能控制助手

    + + +
    +

    LLM模型配置

    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + + +
    + + +
    +
    + +
    +
    - + + \ No newline at end of file diff --git a/main/server.py b/main/server.py index 8c5bcca..c2b0d88 100644 --- a/main/server.py +++ b/main/server.py @@ -3,6 +3,9 @@ import motor import time import os import json +import sys +sys.path.insert(0, os.path.dirname(__file__)) +import agent app = Flask(__name__) @@ -94,19 +97,19 @@ def save_path(): data = request.get_json() path_name = data.get('name') recorded_actions = data.get('actions') - + if not path_name or not recorded_actions: return jsonify({'status': 'error', 'message': '路径名称和轨迹数据不能为空'}) - + try: # 确保Path目录存在 os.makedirs('Path', exist_ok=True) - + # 保存为JSON文件 file_path = os.path.join('Path', f'{path_name}.json') with open(file_path, 'w', encoding='utf-8') as f: json.dump({'name': path_name, 'actions': recorded_actions}, f, ensure_ascii=False, indent=2) - + return jsonify({'status': 'success', 'message': '轨迹保存成功'}) except Exception as e: print(f"保存轨迹出错: {e}") @@ -118,10 +121,10 @@ def list_paths(): try: # 确保Path目录存在 os.makedirs('Path', exist_ok=True) - + # 列出所有轨迹文件 path_files = [f.replace('.json', '') for f in os.listdir('Path') if f.endswith('.json')] - + return jsonify({'status': 'success', 'paths': path_files}) except Exception as e: print(f"列出轨迹出错: {e}") @@ -131,18 +134,18 @@ def list_paths(): @app.route('/load_path', methods=['GET']) def load_path(): path_name = request.args.get('name') - + if not path_name: return jsonify({'status': 'error', 'message': '路径名称不能为空'}) - + try: file_path = os.path.join('Path', f'{path_name}.json') if not os.path.exists(file_path): return jsonify({'status': 'error', 'message': '轨迹文件不存在'}) - + with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) - + return jsonify({'status': 'success', 'data': data}) except Exception as e: print(f"加载轨迹出错: {e}") @@ -153,20 +156,22 @@ def load_path(): def delete_path(): data = request.get_json() path_name = data.get('name') - + if not path_name: return jsonify({'status': 'error', 'message': '路径名称不能为空'}) - + try: file_path = os.path.join('Path', f'{path_name}.json') if not os.path.exists(file_path): return jsonify({'status': 'error', 'message': '轨迹文件不存在'}) - + os.remove(file_path) return jsonify({'status': 'success', 'message': '轨迹删除成功'}) except Exception as e: print(f"删除轨迹出错: {e}") return jsonify({'status': 'error', 'message': f'删除轨迹出错: {e}'}) +agent.create_agent_routes(app, motor) + if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, debug=True) + app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file