Compare commits
2 Commits
dae78fffc3
...
f48facde94
| Author | SHA1 | Date | |
|---|---|---|---|
| f48facde94 | |||
| d9ec92220e |
175
main/CSS/agent.css
Normal file
175
main/CSS/agent.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
@ -268,4 +268,5 @@
|
||||
.title {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
180
main/Javascript/agent.js
Normal file
180
main/Javascript/agent.js
Normal file
@ -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 = '<p>缓存已清除</p>';
|
||||
}
|
||||
|
||||
// 加载配置
|
||||
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 = `<p>加载配置失败: ${error.message}</p>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
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 = '<p>配置已保存</p>';
|
||||
} else {
|
||||
status.innerHTML = `<p>配置保存失败: ${data.message}</p>`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('配置保存失败:', error);
|
||||
status.innerHTML = `<p>配置保存失败: ${error.message}</p>`;
|
||||
});
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
function sendMessage() {
|
||||
const text = agentInput.value.trim();
|
||||
if (!text) return;
|
||||
|
||||
addMessage('user', text);
|
||||
agentInput.value = '';
|
||||
status.innerHTML = '<p>处理中...</p>';
|
||||
|
||||
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 = `<p>${data.message || '处理完成'}</p>`;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('通信错误:', error);
|
||||
addMessage('agent', '通信错误,请重试');
|
||||
status.innerHTML = `<p>通信错误: ${error.message}</p>`;
|
||||
});
|
||||
}
|
||||
|
||||
// 添加消息到聊天历史
|
||||
function addMessage(type, content) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message ${type}`;
|
||||
messageDiv.innerHTML = `<p>${content}</p>`;
|
||||
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();
|
||||
});
|
||||
@ -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 = `<p>正在${direction === 'forward' ? '前进' : '后退'}...</p>`;
|
||||
|
||||
// 记录操作(如果正在录制)
|
||||
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 = `<p>录制完成,共记录 ${recordedActions.length} 个操作</p>`;
|
||||
} else {
|
||||
// 开始录制
|
||||
isRecording = true;
|
||||
recordedActions = [];
|
||||
recordingStartTime = Date.now();
|
||||
recordBtn.textContent = '停止录制';
|
||||
status.innerHTML = `<p>开始录制...</p>`;
|
||||
}
|
||||
});
|
||||
|
||||
// 回放按钮点击事件
|
||||
playbackBtn.addEventListener('click', () => {
|
||||
if (recordedActions.length === 0) {
|
||||
status.innerHTML = `<p>没有可回放的操作</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
status.innerHTML = `<p>开始回放...</p>`;
|
||||
|
||||
// 按时间顺序回放操作
|
||||
let lastTimestamp = 0;
|
||||
recordedActions.forEach((action, index) => {
|
||||
setTimeout(() => {
|
||||
controlMotor(action.direction);
|
||||
|
||||
// 最后一个操作完成后显示回放完成
|
||||
if (index === recordedActions.length - 1) {
|
||||
setTimeout(() => {
|
||||
status.innerHTML = `<p>回放完成</p>`;
|
||||
}, 2000);
|
||||
}
|
||||
}, action.timestamp - lastTimestamp);
|
||||
|
||||
lastTimestamp = action.timestamp;
|
||||
});
|
||||
});
|
||||
|
||||
// 按钮点击事件
|
||||
forwardBtn.addEventListener('click', () => {
|
||||
controlMotor('forward');
|
||||
|
||||
21
main/Path/轨迹.json
Normal file
21
main/Path/轨迹.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "轨迹",
|
||||
"actions": [
|
||||
{
|
||||
"direction": "forward",
|
||||
"timestamp": 858
|
||||
},
|
||||
{
|
||||
"direction": "stop",
|
||||
"timestamp": 1166
|
||||
},
|
||||
{
|
||||
"direction": "backward",
|
||||
"timestamp": 2327
|
||||
},
|
||||
{
|
||||
"direction": "stop",
|
||||
"timestamp": 2642
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
main/__pycache__/agent.cpython-38.pyc
Normal file
BIN
main/__pycache__/agent.cpython-38.pyc
Normal file
Binary file not shown.
BIN
main/__pycache__/motor.cpython-313.pyc
Normal file
BIN
main/__pycache__/motor.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
308
main/agent.py
Normal file
308
main/agent.py
Normal file
@ -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}'})
|
||||
108
main/agent/skills.md
Normal file
108
main/agent/skills.md
Normal file
@ -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代码块)。**
|
||||
@ -99,7 +99,7 @@ while True:
|
||||
if area < 75: # 过滤噪声,增大阈值
|
||||
continue
|
||||
|
||||
if area > 8000: # 过滤大面积假目标(如整个画面),减小阈值
|
||||
if area > 5000: # 过滤大面积假目标(如整个画面),减小阈值
|
||||
continue
|
||||
|
||||
# 计算轮廓的宽高比,过滤不符合垃圾特征的目标
|
||||
@ -117,7 +117,7 @@ while True:
|
||||
best_match = None
|
||||
min_distance = float('inf')
|
||||
|
||||
search_radius = 100 # 目标锁定区域半径
|
||||
search_radius = 80 # 目标锁定区域半径
|
||||
|
||||
for area, cnt, x, y, w, h in valid_contours:
|
||||
cx = x + w // 2
|
||||
@ -170,7 +170,7 @@ while True:
|
||||
cy = int(alpha * prev_cy + (1 - alpha) * cy)
|
||||
|
||||
# 绘制画面中心
|
||||
center_x = 50 # 画面中心x坐标
|
||||
center_x = 90 # 画面中心x坐标
|
||||
center_y = 120 # 画面中心y坐标
|
||||
cv2.circle(frame, (center_x, center_y), 5, (255, 0, 0), -1)
|
||||
|
||||
@ -197,7 +197,7 @@ while True:
|
||||
tracking_target = None
|
||||
|
||||
# 控制逻辑:根据x轴和y轴坐标控制垃圾桶移动
|
||||
center_x = 160 # 画面中心x坐标
|
||||
center_x = 90 # 画面中心x坐标
|
||||
center_y = 120 # 画面中心y坐标
|
||||
threshold = 15 # 阈值范围
|
||||
|
||||
|
||||
@ -6,11 +6,12 @@
|
||||
<title>垃圾桶控制界面</title>
|
||||
<link rel="stylesheet" href="CSS/control.css">
|
||||
<link rel="stylesheet" href="CSS/record.css">
|
||||
<link rel="stylesheet" href="CSS/agent.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="title">垃圾桶控制界面</h1>
|
||||
|
||||
|
||||
<!-- 手柄风格控制器 -->
|
||||
<div class="controller">
|
||||
<div class="controller-layout">
|
||||
@ -32,13 +33,13 @@
|
||||
<button class="btn btn-backward-right" id="backwardRightBtn"><span>↘</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 中间:录制回放控制 -->
|
||||
<div class="center-controls">
|
||||
<button class="btn btn-record" id="recordBtn"><span>录制</span></button>
|
||||
<button class="btn btn-playback" id="playbackBtn"><span>回放</span></button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 右侧:旋转控制 -->
|
||||
<div class="rotate-controls">
|
||||
<button class="btn btn-rotate-left" id="rotateLeftBtn"><span>↶</span></button>
|
||||
@ -46,7 +47,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 轨迹管理区域 -->
|
||||
<div class="path-management">
|
||||
<h3>轨迹管理</h3>
|
||||
@ -59,15 +60,48 @@
|
||||
<ul id="pathList"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 状态显示区域 -->
|
||||
<div class="status" id="status">
|
||||
<p>就绪</p>
|
||||
</div>
|
||||
|
||||
<!-- Agent聊天区域 -->
|
||||
<div class="agent-chat">
|
||||
<h3>智能控制助手</h3>
|
||||
|
||||
<!-- LLM配置区域 -->
|
||||
<div class="llm-config">
|
||||
<h4>LLM模型配置</h4>
|
||||
<div class="config-row">
|
||||
<label for="apiUrlInput">API URL:</label>
|
||||
<input type="text" id="apiUrlInput" placeholder="例如: https://api.openai.com/v1">
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label for="apiKeyInput">API Key:</label>
|
||||
<input type="password" id="apiKeyInput" placeholder="输入API密钥">
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label for="modelInput">模型:</label>
|
||||
<input type="text" id="modelInput" placeholder="例如: gpt-3.5-turbo">
|
||||
</div>
|
||||
<button class="btn btn-config" id="saveConfigBtn">保存配置</button>
|
||||
</div>
|
||||
|
||||
<!-- 聊天输入区域 -->
|
||||
<div class="chat-input">
|
||||
<input type="text" id="agentInput" placeholder="输入命令,例如:前进3秒、左转、执行路径test1">
|
||||
<button class="btn btn-save" id="sendAgentBtn">发送</button>
|
||||
</div>
|
||||
<div class="chat-history" id="chatHistory">
|
||||
<!-- 聊天历史记录 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="Javascript/control.js"></script>
|
||||
<script src="Javascript/record.js"></script>
|
||||
<script src="Javascript/joycon.js"></script>
|
||||
<script src="Javascript/agent.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -72,25 +72,25 @@ def motor_drive(name, speed):
|
||||
# ======================
|
||||
# 基础移动
|
||||
# ======================
|
||||
def forward(speed=0.8):
|
||||
def forward(speed=0.6):
|
||||
motor_drive("M1", -speed)
|
||||
motor_drive("M2", -speed)
|
||||
motor_drive("M3", speed)
|
||||
motor_drive("M4", speed)
|
||||
|
||||
def backward(speed=0.8):
|
||||
def backward(speed=0.6):
|
||||
motor_drive("M1", speed)
|
||||
motor_drive("M2", speed)
|
||||
motor_drive("M3", -speed)
|
||||
motor_drive("M4", -speed)
|
||||
|
||||
def move_left(speed=0.8):
|
||||
def move_left(speed=0.6):
|
||||
motor_drive("M1", -speed)
|
||||
motor_drive("M2", speed)
|
||||
motor_drive("M3", speed)
|
||||
motor_drive("M4", -speed)
|
||||
|
||||
def move_right(speed=0.8):
|
||||
def move_right(speed=0.6):
|
||||
motor_drive("M1", speed)
|
||||
motor_drive("M2", -speed)
|
||||
motor_drive("M3", -speed)
|
||||
@ -99,13 +99,13 @@ def move_right(speed=0.8):
|
||||
# ======================
|
||||
# 旋转
|
||||
# ======================
|
||||
def rotate_left(speed=0.8):
|
||||
def rotate_left(speed=0.6):
|
||||
motor_drive("M1", -speed)
|
||||
motor_drive("M2", -speed)
|
||||
motor_drive("M3", -speed)
|
||||
motor_drive("M4", -speed)
|
||||
|
||||
def rotate_right(speed=0.8):
|
||||
def rotate_right(speed=0.6):
|
||||
motor_drive("M1", speed)
|
||||
motor_drive("M2", speed)
|
||||
motor_drive("M3", speed)
|
||||
@ -114,25 +114,25 @@ def rotate_right(speed=0.8):
|
||||
# ======================
|
||||
# 斜向移动
|
||||
# ======================
|
||||
def move_left_forward(speed=0.8):
|
||||
def move_left_forward(speed=0.6):
|
||||
motor_drive("M1", 0)
|
||||
motor_drive("M2", speed)
|
||||
motor_drive("M3", 0)
|
||||
motor_drive("M4", -speed)
|
||||
|
||||
def move_right_forward(speed=0.8):
|
||||
def move_right_forward(speed=0.6):
|
||||
motor_drive("M1", speed)
|
||||
motor_drive("M2", 0)
|
||||
motor_drive("M3", -speed)
|
||||
motor_drive("M4", 0)
|
||||
|
||||
def move_left_backward(speed=0.8):
|
||||
def move_left_backward(speed=0.6):
|
||||
motor_drive("M1", -speed)
|
||||
motor_drive("M2", 0)
|
||||
motor_drive("M3", speed)
|
||||
motor_drive("M4", 0)
|
||||
|
||||
def move_right_backward(speed=0.8):
|
||||
def move_right_backward(speed=0.6):
|
||||
motor_drive("M1", 0)
|
||||
motor_drive("M2", -speed)
|
||||
motor_drive("M3", 0)
|
||||
|
||||
@ -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)
|
||||
Loading…
x
Reference in New Issue
Block a user