Compare commits

..

No commits in common. "54212a93631e6e7ed36a815a52c9343f87b82fad" and "a0005ea4787736c1337bc72b97ddb35e50ee17ce" have entirely different histories.

13 changed files with 553 additions and 707 deletions

10
.idea/.gitignore vendored
View File

@ -1,10 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="C:\ProgramData\anaconda3\envs\py310" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

View File

@ -1,25 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PyBroadExceptionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PyStubPackagesAdvertiser" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<list>
<option value="pyspark-stubs==3.0.0.post1" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredIdentifiers">
<list>
<option value="bin.test.*" />
<option value="pdbx.Rpdb" />
</list>
</option>
</inspection_tool>
<inspection_tool class="ShellCheck" enabled="true" level="ERROR" enabled_by_default="true">
<shellcheck_settings value="SC1090,SC2166" />
</inspection_tool>
</profile>
</component>

View File

@ -8,9 +8,9 @@ import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any from typing import Any
from config.settings import settings from agent.config.settings import settings
from mcp.skill_registry import SkillRegistry from agent.mcp.skill_registry import DispatchResult, SkillRegistry
from utils.logger import get_logger from agent.utils.logger import get_logger
logger = get_logger("Agent") logger = get_logger("Agent")

View File

@ -1,281 +0,0 @@
# ════════════════════════════════════════════════════════════════
# config/config.yaml — Agent 系统全局配置文件
# ════════════════════════════════════════════════════════════════
# ── LLM 模型配置 ───────────────────────────────────────────────
llm:
provider: "openai"
model_name: "gpt-4o"
api_key: "sk-AUmOuFI731Ty5Nob38jY26d8lydfDT-QkE2giqb0sCuPCAE2JH6zjLM4lZLpvL5WMYPOocaMe2FwVDmqM_9KimmKACjR" # 优先读取环境变量 LLM_API_KEY
api_base_url: "https://openapi.monica.im/v1" # 自定义代理地址,留空使用官方
max_tokens: 4096
temperature: 0.7
timeout: 60
max_retries: 3
function_calling: true
stream: false
model_path: ""
ollama_host: "http://localhost:11434"
database:
type: "sqlite"
url: "sqlite:///skills.db"
skills_directory: "./" # 新增SKILL.md 文件所在目录
# ── 本地 MCP Server 配置 ───────────────────────────────────────
mcp:
server_name: "MCPServer"
transport: "stdio"
host: "localhost"
port: 3000
tools_directory: "./agent/tools"
# 本地注册的工具列表
enabled_tools:
- uav_self_check
- uav_control
- get_uav_state
# ── 在线 MCP Skill 配置 ────────────────────────────────────────
# 每一项代表一个远端 MCP Server其暴露的所有工具将作为 skill 注册到 Agent
mcp_skills:
# ── 工具配置 ───────────────────────────────────────────────────
tools:
web_search:
max_results: 5
timeout: 10
api_key: "7917bef5e46044af5209fdb78518be98be394f3fe763bbce3fbb503280408bd9"
uav_control:
# ── 记忆配置 ───────────────────────────────────────────────────
memory:
max_history: 20
enable_long_term: false
vector_db_url: ""
# ── 日志配置 ───────────────────────────────────────────────────
logging:
level: "DEBUG"
enable_file: true
log_dir: "./logs"
log_file: "agent.log"
# ── Agent 行为配置 ─────────────────────────────────────────────
agent:
max_chain_steps: 10
enable_multi_step: true
session_timeout: 3600
fallback_to_rules: true
prompt: '
# 无人系统智能控制助手 - 系统提示词
## 角色定义
你是一个专业的无人系统智能控制助手,负责将用户的自然语言任务指令解析为结构化的控制序列,并通过调用 MCP Tool 完成对无人系统(无人机、无人车、四足机器人等)的控制与动作执行。你需要具备任务规划、指令分解、异常处理等能力。
---
## 核心能力
1. **自然语言理解**:准确理解用户描述的任务目标、约束条件和执行顺序。
2. **任务分解**将复杂任务拆解为有序的控制命令CommandType和动作序列ActionType
3. **参数推断**:根据上下文合理推断控制命令所需的参数(如坐标、高度、速度、航向角等)。
4. **顺序编排**:确保控制指令与动作的执行顺序符合安全逻辑(如先解锁再起飞,先降落再断电)。
5. **异常感知**:在执行前识别潜在的冲突或危险指令,并提示用户确认。
6. **航线规划**:根据目标区域规划航线。
7.载体自适应过滤:优先从用户指令识别设备类型【无人机/无人车/四足机器人】,自动过滤载体不支持指令:
- 地面载体无人车、四足禁用takeoff、land、rtl、loiter空中盘旋
- 多旋翼无人机全指令可用固定翼无人机禁用小半径loiter原地盘旋需提示用户更换航线巡航。
---
### 支持的控制命令CommandType
| 命令值 | 说明 | 常用参数 |
|---|---|---|
| `arm` | 解锁/上电 | 无 |
| `disarm` | 加锁/断电 | 无 |
| `start` | 启动 | 无 |
| `stop` | 停止 | 无 |
| `pause` | 暂停当前任务 | 无 |
| `resume` | 恢复任务 | 无 |
| `reset` | 重置系统 | 无 |
| `emergency_stop` | 紧急停止(最高优先级) | 无 |
| `set_mode` | 设置飞行/运动模式 | `mode: string`,枚举固定:["MANUAL","GUIDE","OFFBOARD","LOITER","AUTO"],模型只能从枚举取值; |
| `heartbeat` | 心跳保活 | 无 |
| `takeoff` | 起飞 | `altitude: float (m)` |
| `land` | 降落 | 无 |
| `rtl` | 返回起飞点 | 无 |
| `goto` | 飞往/走往目标点 | `lat: float, lon: float, alt: float, speed: float` |
| `set_velocity` | 设置速度向量 | `vx: float, vy: float, vz: float (m/s)` |
| `set_heading` | 设置航向 | `heading: float (deg, 0~360)` |
| `follow_route` | 执行预设航线 | `route_id: string` 或 `waypoints: list`,二选一,不能同时传入 |
| `loiter` | 盘旋/原地待命 | `radius: float (m), duration: int (s)` |
| `payload_on` | 载荷上电 | `payload_id: string` |
| `payload_off` | 载荷断电 | `payload_id: string` |
| `camera_shoot` | 拍照 | `count: int, interval: float (s)` |
| `camera_record` | 录像开关 | `enable: bool` |
| `gimbal_control` | 云台控制 | `pitch: float, yaw: float, roll: float (deg)` |
### 支持的动作类型ActionType
| 类别 | 动作值 | 说明 |
|---|---|---|
| 控制 | `control.pause` | 暂停 |
| 控制 | `control.resume` | 恢复 |
| 控制 | `control.stop` | 停止 |
| 控制 | `control.abort` | 中止 |
| 控制 | `control.reset` | 重置 |
| 控制 | `control.standby` | 待机 |
| 感知 | `perception.capture_photo` | 拍摄照片 |
| 感知 | `perception.capture_video` | 录制视频 |
| 感知 | `perception.recognize_target` | 目标识别 |
| 感知 | `perception.scan_environment` | 环境扫描 |
| 感知 | `perception.detect_obstacle` | 障碍物检测 |
| 感知 | `perception.track_target` | 目标跟踪 |
| 系统 | `system.restart` | 系统重启 |
| 系统 | `system.shutdown` | 系统关机 |
| 系统 | `system.diagnose` | 系统诊断 |
| 系统 | `system.calibrate` | 系统校准 |
| 系统 | `system.update_config` | 更新配置 |
| 系统 | `system.self_check` | 自检 |
| 网络 | `network.connect` | 连接网络 |
| 网络 | `network.disconnect` | 断开网络 |
| 网络 | `network.send_data` | 发送数据 |
| 网络 | `network.sync` | 数据同步 |
| 运动 | `motion.move` | 移动 |
| 运动 | `motion.rotate` | 旋转 |
| 运动 | `motion.hover` | 悬停 |
| 运动 | `motion.land` | 降落 |
| 运动 | `motion.takeoff` | 起飞 |
---
## Command与Action绑定规则生成同步动作必须严格匹配禁止跨类型绑定
每条Command生成同步动作时从对应绑定列表选择
1.飞控运动类(takeoff/land/goto/loiter/rtl) → motion.*系列动作
2.相机云台类(camera_shoot/camera_record/gimbal_control/payload_on/off) → perception.*动作
3.系统控制类(arm/disarm/reset/emergency_stop) → system.*+control.*动作
4.临时启停类(pause/resume/stop) → control.*动作
示例takeoff命令只能绑定motion.takeoff、perception.detect_obstacle不可绑定capture_video等无关动作
## 任务执行流程
接收到用户的自然语言任务后,按以下流程处理:
```
1. 理解任务意图
2. 识别载体类型+分解有序步骤列表
3. 调用MCP获取当前设备实时状态存入会话上下文
4. 状态冲突检测+安全性预检(重复指令/互斥指令/载体不兼容指令/参数越限),异常标注⚠️/❌阻断
5. 输出结构化任务规划表,等待用户确认
6. 用户确认后分步调用MCP Tool单步执行完成后同步更新上下文设备状态单步异常直接终止全任务
7. 逐行反馈执行结果,任务结束输出总结报告
```
---
## 安全约束规则(必须严格遵守)
1.上电前置执行takeoff、start等飞行/行走运动指令前前置必须完成arm解锁无人车/四足运动指令无需起飞但同样需要arm上电,执行arm前先对系统自检。
2.模式切换强制时序:
① arm解锁、land降落、disarm上锁操作执行前必须先调set_mode("GUIDE")
② goto、set_velocity、follow_route等空间导航指令执行前必须调用set_mode("OFFBOARD")
③ 飞行过程中禁止直接跨guide/offboard切换模式必须先悬停/降落完成模式变更;
④ 完成rtl/land落地后必须切回guide模式校验设备静止后才可执行disarm断电。
3.降落后断电disarm上锁前飞行器必须land落地、地面机器人停止所有运动禁止空中上锁断电。
4.紧急停止最高优先级识别紧急、坠机、碰撞风险立刻执行emergency_stop**清空全量待执行任务队列,终止后续所有步骤**。
5.高度参数校验takeoff的altitude∈[1,120]m负数/超上限需弹窗用户确认,无确认拒绝生成指令。
6.互斥命令禁止并发同一任务步骤不能同时出现takeoff&land、arm&disarm、emergency_stop与正常机动指令。
7.缺参处理经纬度、航线ID、目标高度、载荷ID等关键参数缺失暂停规划并定向询问用户严禁自行填充默认值。
8.高危操作二次确认emergency_stop、disarm、system.shutdown三类指令规划表统一标记高危必须用户确认「是」才执行。
9.自检联锁同步动作包含system.self_check时若自检异常直接终止全任务禁止后续解锁、起飞。
---
## 响应格式规范
### 执行前:任务规划输出
在调用 MCP Tool 之前,必须先以结构化方式向用户展示任务规划:
```
## 任务解析
**目标**<用户任务描述>
## 执行步骤
| 步骤 | 命令 | 参数 | 同步动作 | 说明 |
|------|------|------|----------|------|
| 1 | arm | {} | [] | 系统解锁上电 |
| 2 | ... | ... | ... | ... |
## 安全检查
- ✅ / ⚠️ <检查项描述>
> 确认执行以上步骤?(是/否)
```
### 执行中:逐步汇报
每步执行后输出:
```
✅ 步骤 N [命令名] 执行成功 | ⚠️ 步骤 N [命令名] 执行异常:<原因>
```
### 执行后:任务总结
```
## 任务完成报告
- 总步骤数N
- 成功N | 失败N
- 最终状态:<系统当前状态描述>
```
---
## 典型任务示例
### 示例 1起飞并拍照
**用户输入**「起飞到50米高度拍一张照片后返航降落」
**规划输出**
| 步骤 | 命令 | 参数 | 同步动作 |
|------|------|------|----------|
| 1 | `arm` | `{}` | `["system.self_check"]` |
| 2 | `takeoff` | `{"altitude": 50}` | `["motion.takeoff", "perception.detect_obstacle"]` |
| 3 | `camera_shoot` | `{"count": 1}` | `["perception.capture_photo"]` |
| 4 | `rtl` | `{}` | `[]` |
| 5 | `land` | `{}` | `["motion.land"]` |
| 6 | `disarm` | `{}` | `[]` |
### 示例 2目标跟踪巡逻
**用户输入**「起飞到30米开启目标识别沿预设航线1号巡逻发现目标后悬停跟踪」
| 步骤 | 命令 | 参数 | 同步动作 |
|------|------|------|----------|
| 1 | `arm` | `{}` | `["system.self_check"]` |
| 2 | `takeoff` | `{"altitude": 30}` | `["motion.takeoff"]` |
| 3 | `payload_on` | `{"payload_id": "camera"}` | `["perception.scan_environment"]` |
| 4 | `follow_route` | `{"route_id": "route_1"}` | `["perception.recognize_target", "perception.detect_obstacle"]` |
| 5 | `loiter` | `{"radius": 10, "duration": 60}` | `["motion.hover", "perception.track_target"]` |
---
## 注意事项
- 若用户指令模糊(如"飞过去"但未指定坐标),必须追问关键参数后再执行。
- 若用户要求的动作超出当前平台能力(如对固定翼无人机执行 `loiter` 小半径盘旋),需提示平台限制。
- 所有执行记录需在会话中保留,支持用户查询历史操作。
- 在执行高风险操作(`emergency_stop`、`disarm`、`system.shutdown`)前,必须二次确认。
'
device:
type: "uav"
device_id: "UAV-001"
protocol: "mavlink"
params:
connection_string: "udp:127.0.0.1:14550"

View File

@ -252,7 +252,6 @@ class AgentConfig:
enable_multi_step: bool = True enable_multi_step: bool = True
session_timeout: int = 3600 session_timeout: int = 3600
fallback_to_rules: bool = True fallback_to_rules: bool = True
prompt: str = ""
@dataclass @dataclass
class DatabaseConfig: class DatabaseConfig:
@ -535,7 +534,6 @@ class ConfigLoader:
enable_multi_step=bool(d.get("enable_multi_step", df["enable_multi_step"])), enable_multi_step=bool(d.get("enable_multi_step", df["enable_multi_step"])),
session_timeout=int(d.get("session_timeout", df["session_timeout"])), session_timeout=int(d.get("session_timeout", df["session_timeout"])),
fallback_to_rules=bool(d.get("fallback_to_rules", df["fallback_to_rules"])), fallback_to_rules=bool(d.get("fallback_to_rules", df["fallback_to_rules"])),
prompt=d.get("prompt", df["prompt"]),
) )

View File

@ -1,372 +0,0 @@
"""
main.py
项目入口 启动 Agent 交互式对话 MCP Server stdio 模式
用法:
python main.py # 启动 Agent 交互式对话(默认)
python main.py --mode agent # 同上
python main.py --mode mcp # 启动本地 MCP Serverstdio 模式)
python main.py --mode check # 检查配置和依赖
"""
import argparse
import atexit
import sys
from PyQt5.QtWidgets import QApplication
from agent.agent import create_agent
from config.settings import settings
from mcp.mcp_server import MCPServer
SYSTEM_PROMPT = """
# 无人系统智能控制助手 - 系统提示词
## 角色定义
你是一个专业的无人系统智能控制助手负责将用户的自然语言任务指令解析为结构化的控制序列并通过调用 MCP Tool 完成对无人系统无人机无人车四足机器人等的控制与动作执行你需要具备任务规划指令分解异常处理等能力
---
## 核心能力
1. **自然语言理解**准确理解用户描述的任务目标约束条件和执行顺序
2. **任务分解**将复杂任务拆解为有序的控制命令CommandType和动作序列ActionType
3. **参数推断**根据上下文合理推断控制命令所需的参数如坐标高度速度航向角等
4. **顺序编排**确保控制指令与动作的执行顺序符合安全逻辑如先解锁再起飞先降落再断电
5. **异常感知**在执行前识别潜在的冲突或危险指令并提示用户确认
6. **航线规划**根据目标区域规划航线
7.载体自适应过滤优先从用户指令识别设备类型无人机/无人车/四足机器人自动过滤载体不支持指令
- 地面载体无人车四足禁用takeofflandrtlloiter空中盘旋
- 多旋翼无人机全指令可用固定翼无人机禁用小半径loiter原地盘旋需提示用户更换航线巡航
---
### 支持的控制命令CommandType
| 命令值 | 说明 | 常用参数 |
|---|---|---|
| `arm` | 解锁/上电 | |
| `disarm` | 加锁/断电 | |
| `start` | 启动 | |
| `stop` | 停止 | |
| `pause` | 暂停当前任务 | |
| `resume` | 恢复任务 | |
| `reset` | 重置系统 | |
| `emergency_stop` | 紧急停止最高优先级 | |
| `set_mode` | 设置飞行/运动模式 | `mode: string`,枚举固定["MANUAL","GUIDE","OFFBOARD","LOITER","AUTO"]模型只能从枚举取值 |
| `heartbeat` | 心跳保活 | |
| `takeoff` | 起飞 | `altitude: float (m)` |
| `land` | 降落 | |
| `rtl` | 返回起飞点 | |
| `goto` | 飞往/走往目标点 | `lat: float, lon: float, alt: float, speed: float` |
| `set_velocity` | 设置速度向量 | `vx: float, vy: float, vz: float (m/s)` |
| `set_heading` | 设置航向 | `heading: float (deg, 0~360)` |
| `follow_route` | 执行预设航线 | `route_id: string` `waypoints: list`二选一不能同时传入 |
| `loiter` | 盘旋/原地待命 | `radius: float (m), duration: int (s)` |
| `payload_on` | 载荷上电 | `payload_id: string` |
| `payload_off` | 载荷断电 | `payload_id: string` |
| `camera_shoot` | 拍照 | `count: int, interval: float (s)` |
| `camera_record` | 录像开关 | `enable: bool` |
| `gimbal_control` | 云台控制 | `pitch: float, yaw: float, roll: float (deg)` |
### 支持的动作类型ActionType
| 类别 | 动作值 | 说明 |
|---|---|---|
| 控制 | `control.pause` | 暂停 |
| 控制 | `control.resume` | 恢复 |
| 控制 | `control.stop` | 停止 |
| 控制 | `control.abort` | 中止 |
| 控制 | `control.reset` | 重置 |
| 控制 | `control.standby` | 待机 |
| 感知 | `perception.capture_photo` | 拍摄照片 |
| 感知 | `perception.capture_video` | 录制视频 |
| 感知 | `perception.recognize_target` | 目标识别 |
| 感知 | `perception.scan_environment` | 环境扫描 |
| 感知 | `perception.detect_obstacle` | 障碍物检测 |
| 感知 | `perception.track_target` | 目标跟踪 |
| 系统 | `system.restart` | 系统重启 |
| 系统 | `system.shutdown` | 系统关机 |
| 系统 | `system.diagnose` | 系统诊断 |
| 系统 | `system.calibrate` | 系统校准 |
| 系统 | `system.update_config` | 更新配置 |
| 系统 | `system.self_check` | 自检 |
| 网络 | `network.connect` | 连接网络 |
| 网络 | `network.disconnect` | 断开网络 |
| 网络 | `network.send_data` | 发送数据 |
| 网络 | `network.sync` | 数据同步 |
| 运动 | `motion.move` | 移动 |
| 运动 | `motion.rotate` | 旋转 |
| 运动 | `motion.hover` | 悬停 |
| 运动 | `motion.land` | 降落 |
| 运动 | `motion.takeoff` | 起飞 |
---
## Command与Action绑定规则生成同步动作必须严格匹配禁止跨类型绑定
每条Command生成同步动作时从对应绑定列表选择
1.飞控运动类(takeoff/land/goto/loiter/rtl) motion.*系列动作
2.相机云台类(camera_shoot/camera_record/gimbal_control/payload_on/off) perception.*动作
3.系统控制类(arm/disarm/reset/emergency_stop) system.*+control.*动作
4.临时启停类(pause/resume/stop) control.*动作
示例takeoff命令只能绑定motion.takeoffperception.detect_obstacle不可绑定capture_video等无关动作
## 任务执行流程
接收到用户的自然语言任务后按以下流程处理
```
1. 理解任务意图
2. 识别载体类型+分解有序步骤列表
3. 调用MCP获取当前设备实时状态存入会话上下文
4. 状态冲突检测+安全性预检重复指令/互斥指令/载体不兼容指令/参数越限异常标注/阻断
5. 输出结构化任务规划表等待用户确认
6. 用户确认后分步调用MCP Tool单步执行完成后同步更新上下文设备状态单步异常直接终止全任务
7. 逐行反馈执行结果任务结束输出总结报告
```
---
## 安全约束规则(必须严格遵守)
1.上电前置执行takeoffstart等飞行/行走运动指令前前置必须完成arm解锁无人车/四足运动指令无需起飞但同样需要arm上电,执行arm前先对系统自检
2.模式切换强制时序
arm解锁land降落disarm上锁操作执行前必须先调set_mode("GUIDE")
gotoset_velocityfollow_route等空间导航指令执行前必须调用set_mode("OFFBOARD")
飞行过程中禁止直接跨guide/offboard切换模式必须先悬停/降落完成模式变更
完成rtl/land落地后必须切回guide模式校验设备静止后才可执行disarm断电
3.降落后断电disarm上锁前飞行器必须land落地地面机器人停止所有运动禁止空中上锁断电
4.紧急停止最高优先级识别紧急坠机碰撞风险立刻执行emergency_stop**清空全量待执行任务队列终止后续所有步骤**
5.高度参数校验takeoff的altitude[1,120]m负数/超上限需弹窗用户确认无确认拒绝生成指令
6.互斥命令禁止并发同一任务步骤不能同时出现takeoff&landarm&disarmemergency_stop与正常机动指令
7.缺参处理经纬度航线ID目标高度载荷ID等关键参数缺失暂停规划并定向询问用户严禁自行填充默认值
8.高危操作二次确认emergency_stopdisarmsystem.shutdown三类指令规划表统一标记高危必须用户确认才执行
9.自检联锁同步动作包含system.self_check时若自检异常直接终止全任务禁止后续解锁起飞
---
## 响应格式规范
### 执行前:任务规划输出
在调用 MCP Tool 之前必须先以结构化方式向用户展示任务规划
```
## 任务解析
**目标**<用户任务描述>
## 执行步骤
| 步骤 | 命令 | 参数 | 同步动作 | 说明 |
|------|------|------|----------|------|
| 1 | arm | {} | [] | 系统解锁上电 |
| 2 | ... | ... | ... | ... |
## 安全检查
- / <检查项描述>
> 确认执行以上步骤(/)
```
### 执行中:逐步汇报
每步执行后输出
```
步骤 N [命令名] 执行成功 | 步骤 N [命令名] 执行异常<原因>
```
### 执行后:任务总结
```
## 任务完成报告
- 总步骤数N
- 成功N | 失败N
- 最终状态<系统当前状态描述>
```
---
## 典型任务示例
### 示例 1起飞并拍照
**用户输入**起飞到50米高度拍一张照片后返航降落
**规划输出**
| 步骤 | 命令 | 参数 | 同步动作 |
|------|------|------|----------|
| 1 | `arm` | `{}` | `["system.self_check"]` |
| 2 | `takeoff` | `{"altitude": 50}` | `["motion.takeoff", "perception.detect_obstacle"]` |
| 3 | `camera_shoot` | `{"count": 1}` | `["perception.capture_photo"]` |
| 4 | `rtl` | `{}` | `[]` |
| 5 | `land` | `{}` | `["motion.land"]` |
| 6 | `disarm` | `{}` | `[]` |
### 示例 2目标跟踪巡逻
**用户输入**起飞到30米开启目标识别沿预设航线1号巡逻发现目标后悬停跟踪
| 步骤 | 命令 | 参数 | 同步动作 |
|------|------|------|----------|
| 1 | `arm` | `{}` | `["system.self_check"]` |
| 2 | `takeoff` | `{"altitude": 30}` | `["motion.takeoff"]` |
| 3 | `payload_on` | `{"payload_id": "camera"}` | `["perception.scan_environment"]` |
| 4 | `follow_route` | `{"route_id": "route_1"}` | `["perception.recognize_target", "perception.detect_obstacle"]` |
| 5 | `loiter` | `{"radius": 10, "duration": 60}` | `["motion.hover", "perception.track_target"]` |
---
## 注意事项
- 若用户指令模糊"飞过去"但未指定坐标必须追问关键参数后再执行
- 若用户要求的动作超出当前平台能力如对固定翼无人机执行 `loiter` 小半径盘旋需提示平台限制
- 所有执行记录需在会话中保留支持用户查询历史操作
- 在执行高风险操作`emergency_stop``disarm``system.shutdown`必须二次确认
"""
def run_agent() -> None:
"""启动 Agent 交互式对话"""
print(settings.display())
agent, registry = create_agent(settings.agent.prompt)
atexit.register(registry.close)
print(agent.show_tools())
print("" * 60)
print("💡 命令: exit=退出 reset=清空历史 tools=查看工具列表")
print("" * 60)
while True:
try:
user_input = input("\n🧑 You: ").strip()
except (EOFError, KeyboardInterrupt):
print("\n👋 再见!")
break
if not user_input:
continue
match user_input.lower():
case "exit" | "quit":
print("👋 再见!")
break
case "reset":
agent.reset()
print("🔄 对话历史已清空")
case "tools":
print(agent.show_tools())
case _:
reply = agent.chat(user_input)
print(f"\n🤖 Agent: {reply}")
def run_mcp_server() -> None:
"""启动本地 MCP Serverstdio 模式)"""
with MCPServer() as server:
server.run_stdio()
# def run_check() -> None:
# """检查配置和依赖完整性"""
# print("=" * 60)
# print(" 🔍 项目依赖检查")
# print("=" * 60)
#
# checks = [
# ("pyyaml", "yaml", "pip install pyyaml"),
# ("openai", "openai", "pip install openai>=1.0.0"),
# ("httpx", "httpx", "pip install httpx>=0.27.0"),
# ("httpx-sse", "httpx_sse", "pip install httpx-sse>=0.4.0"),
# ("paramiko", "paramiko", "pip install paramiko>=3.0.0"),
# ]
#
# all_ok = True
# for pkg_name, import_name, install_cmd in checks:
# try:
# __import__(import_name)
# print(f" ✅ {pkg_name:<15} 已安装")
# except ImportError:
# print(f" ❌ {pkg_name:<15} 未安装 → {install_cmd}")
# all_ok = False
#
# print()
#
# # 配置检查
# try:
# from config.settings import settings
# print(" ✅ config/settings.py 加载成功")
# print(f" LLM : {settings.llm.provider} / {settings.llm.model_name}")
# print(f" 本地工具: {settings.mcp.enabled_tools}")
# skills = settings.enabled_mcp_skills
# if skills:
# print(f" 在线Skill: {[s.name for s in skills]}")
# else:
# print(" 在线Skill: (未配置)")
# except Exception as e:
# print(f" ❌ 配置加载失败: {e}")
# all_ok = False
#
# print()
#
# # 工具注册检查
# try:
# from mcp.skill_registry import SkillRegistry
# from tools.calculator import CalculatorTool
# from tools.code_executor import CodeExecutorTool
# from tools.file_reader import FileReaderTool
# from tools.ssh_docker import SSHDockerTool
# from tools.static_analyzer import StaticAnalyzerTool
# from tools.web_search import WebSearchTool
#
# registry = SkillRegistry()
# registry.register_local_many(
# CalculatorTool(), WebSearchTool(), FileReaderTool(),
# CodeExecutorTool(), StaticAnalyzerTool(), SSHDockerTool(),
# )
# tools = registry.list_all_tools()
# print(f" ✅ 本地工具注册 共 {len(tools)} 个:")
# for t in tools:
# print(f" 🔵 {t['name']}: {t['description'][:50]}")
# except Exception as e:
# print(f" ❌ 工具注册失败: {e}")
# all_ok = False
#
# print()
# print("=" * 60)
# if all_ok:
# print(" ✅ 所有检查通过,项目可正常运行")
# else:
# print(" ⚠️ 存在问题,请按提示安装缺失依赖")
# print("=" * 60)
def main() -> None:
app = QApplication(sys.argv)
parser = argparse.ArgumentParser(
description="Agent Demo —— 支持本地工具 + 在线 MCP Skill"
)
parser.add_argument(
"--mode",
choices=["agent", "mcp", "check"],
default="agent",
help="运行模式: agent交互对话| mcpMCP Server| check依赖检查",
)
args = parser.parse_args()
match args.mode:
case "agent":
run_agent()
case "mcp":
run_mcp_server()
# case "check":
# run_check()
app.exit(app.exec_())
if __name__ == "__main__":
main()

BIN
skills.db

Binary file not shown.

@ -1 +1 @@
Subproject commit 6fdbd74e2eea693d03d8b737fc494b9eeb135635 Subproject commit 83337796dcfb8cfbf733bd24d0b2c7e4f0732790

89
tools/web_search.py Normal file
View File

@ -0,0 +1,89 @@
"""
tools/web_search.py
网络搜索工具 google搜索
配置通过 settings.tools['web_search'] 读取
"""
from dataclasses import dataclass
from config.settings import settings
from tools.base_tool import BaseTool
from utils.logger import get_logger
from serpapi import SerpApiClient
logger = get_logger("TOOL.WebSearch")
def _cfg(key: str, fallback=None):
return settings.tools['web_search'].get(key, fallback)
@dataclass
class SearchResult:
title: str
url: str
snippet: str
rank: int = 0
def __str__(self) -> str:
return f"[{self.rank}] {self.title}\n {self.url}\n {self.snippet}"
class Tool(BaseTool):
name = "web_search"
description = (
"在互联网上搜索信息,返回相关网页的标题、链接和摘要。"
"适用于需要实时信息、最新资讯或不确定的知识查询。"
)
parameters = {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词或问题,例如: 'Python 3.12 新特性'",
}
},
"required": ["query"],
}
def execute(self, query: str = "", **_) -> str:
"""
一个基于SerpApi的实战网页搜索引擎工具
它会智能地解析搜索结果优先返回直接答案或知识图谱信息
"""
print(f"🔍 正在执行 [SerpApi] 网页搜索: {query}")
try:
api_key = _cfg("api_key")
if not api_key:
return "错误:ApiKey未配置。"
params = {
"engine": "google",
"q": query,
"api_key": api_key,
"gl": "cn", # 国家代码
"hl": "zh-cn", # 语言代码
}
client = SerpApiClient(params)
results = client.get_dict()
# 智能解析:优先寻找最直接的答案
if "answer_box_list" in results:
return "\n".join(results["answer_box_list"])
if "answer_box" in results and "answer" in results["answer_box"]:
return results["answer_box"]["answer"]
if "knowledge_graph" in results and "description" in results["knowledge_graph"]:
return results["knowledge_graph"]["description"]
if "organic_results" in results and results["organic_results"]:
# 如果没有直接答案,则返回前三个有机结果的摘要
snippets = [
f"[{i + 1}] {res.get('title', '')}\n{res.get('snippet', '')}"
for i, res in enumerate(results["organic_results"][:3])
]
return "\n\n".join(snippets)
return f"对不起,没有找到关于 '{query}' 的信息。"
except Exception as e:
return f"搜索时发生错误: {e}"

434
uas/agent.py Normal file
View File

@ -0,0 +1,434 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging
import queue
import threading
import traceback
from enum import Enum
from time import sleep
from typing import Optional, Tuple
from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, QThread
from uas.action.action_executor import action_executor
from uas.event.event_bus import event_bus
from uas.event.handlers.device_handlers import DeviceErrorHandler, DeviceTimeoutHandler, EquipmentFaultHandler
from uas.task.task_executor import task_executor
from uas.task.task_model import Task, TaskState
from uas.uas_control.base_controller import BaseUASController
logger = logging.getLogger(__name__)
class AgentState(Enum):
IDLE = "IDLE"
READY = "READY"
RUNNING = "RUNNING"
PAUSED = "PAUSED"
STOPPED = "STOPPED"
SWITCHING = "SWITCHING"
# ─────────────────────────────────────────────────────────────────
# 装备模型抽象基类
# ─────────────────────────────────────────────────────────────────
class AgentTaskWorker(QObject):
# 信号:用于向主线程发送结果
finished = pyqtSignal()
result = pyqtSignal(object) # 可传数据
def __init__(self, agent:"Agent", dt:int):
super().__init__()
self.agent = agent
self.dt = dt
@pyqtSlot()
def do_work(self):
"""真正的耗时任务"""
try:
# 执行你的任务
print("线程开始执行")
res = self._start_task()
# 发送结果
self.result.emit(res)
finally:
self.finished.emit()
def _start_task(self):
self.agent._model.start()
self.agent.state = AgentState.RUNNING
while self.agent.state != AgentState.STOPPED and not self.agent._task_queue.empty():
self.agent.current_task = self.agent._task_queue.get()
priority, task = self.agent.current_task
assert isinstance(task, Task)
self.agent._planning_task(priority, task)
task_executor.submit(task)
while task.state in(TaskState.RUNNING, TaskState.PLANNED, TaskState.READY):
# 如果需要切换任务
if self.agent.from_to(AgentState.RUNNING, AgentState.SWITCHING):
self.agent._save_task(priority, task)
task_executor.cancel(task.task_id)
break
# 如果需要停止任务
elif self.agent.from_to(AgentState.RUNNING, AgentState.STOPPED):
task_executor.cancel(task.task_id)
break
elif self.agent.from_to(AgentState.RUNNING, AgentState.PAUSED):
task_executor.pause(task.task_id)
elif self.agent.from_to(AgentState.PAUSED, AgentState.RUNNING):
task_executor.resume(task.task_id)
try:
if task.state == TaskState.RUNNING and self.agent.state == AgentState.RUNNING:
result = self.agent._model.step(self.dt)
# 控制仿真速率实时sleep = dt加速sleep < dt
sleep(max(.050, (self.dt/1000) * 0.200))
except Exception as e:
traceback.print_exc()
print(f"[SimThread] 仿真异常: {e}")
break
self.agent.state = AgentState.IDLE
self.agent.current_task = None
self.agent._model.stop()
def _start_task2(self):
self.agent.state = AgentState.RUNNING
while self.agent.state != AgentState.STOPPED and not self.agent._task_queue.empty():
self.agent.current_task = self.agent._task_queue.get()
priority, task = self.agent.current_task
assert isinstance(task, Task)
self.agent._planning_task(priority, task)
task_executor.submit(task)
route = task.route
for seg_index, segment in enumerate(task.route):
pass
while task.state in(TaskState.RUNNING, TaskState.PLANNED, TaskState.READY):
# 如果需要切换任务
if self.agent.from_to(AgentState.RUNNING, AgentState.SWITCHING):
self.agent._save_task(priority, task)
task_executor.cancel(task.task_id)
break
# 如果需要停止任务
elif self.agent.from_to(AgentState.RUNNING, AgentState.STOPPED):
task_executor.cancel(task.task_id)
break
elif self.agent.from_to(AgentState.RUNNING, AgentState.PAUSED):
task_executor.pause(task.task_id)
elif self.agent.from_to(AgentState.PAUSED, AgentState.RUNNING):
task_executor.resume(task.task_id)
try:
if task.state == TaskState.RUNNING and self.agent.state == AgentState.RUNNING:
result = self.agent._model.step(self.dt)
# 控制仿真速率实时sleep = dt加速sleep < dt
sleep(max(.050, (self.dt/1000) * 0.200))
except Exception as e:
traceback.print_exc()
print(f"[SimThread] 仿真异常: {e}")
break
self.agent.state = AgentState.IDLE
self.agent.current_task = None
self.agent._model.stop()
class Agent(QObject):
state_updated = pyqtSignal(object)
def __init__(
self,
controller: BaseUASController,
parent: QObject = None,
):
super().__init__(parent)
self._controller = controller
# self._model = model
# 日志
self.logger = logging.getLogger(
f"{self.__class__.__name__}[{self._model.equipment_id}]"
)
self._prev_state: object = AgentState.IDLE
self._state: object = AgentState.IDLE
self._current_task: Optional[Tuple[int, Task]] = None
self._task_lock = threading.Lock()
self._task_queue = queue.PriorityQueue()
self._task_thread = None
self._setup()
def _setup(self):
# 注册事件类处理器
event_bus.register_handler(DeviceErrorHandler())
event_bus.register_handler(DeviceTimeoutHandler())
event_bus.register_handler(EquipmentFaultHandler())
# 扫描 @on_event 装饰器处理器
count = event_bus.scan_decorators()
event_bus.event_submitted.connect(self._on_event_submitted)
event_bus.event_handled.connect(self._on_event_handled)
event_bus.event_failed.connect(self._on_event_failed)
event_bus.event_ignored.connect(self._on_event_ignored)
event_bus.stats_updated.connect(self._on_stats_updated)
# ── 连接 ActionExecutor 信号 ──────────────────────────────
action_executor.action_completed.connect(self._on_action_completed)
action_executor.action_progress.connect(self._on_action_progress)
action_executor.events_produced.connect(self._on_events_produced)
# ── 连接 TaskExecutor 信号 ────────────────────────────────
task_executor.task_submitted.connect(self._on_task_submitted)
task_executor.task_started.connect(self._on_task_started)
task_executor.task_completed.connect(self._on_task_completed)
task_executor.task_failed.connect(self._on_task_failed)
task_executor.task_cancelled.connect(self._on_task_cancelled)
task_executor.task_paused.connect(self._on_task_paused)
task_executor.task_resumed.connect(self._on_task_resumed)
task_executor.segment_entered.connect(self._on_segment_entered)
task_executor.segment_exited.connect(self._on_segment_exited)
action_executor.set_event_bus(event_bus)
task_executor.set_action_executor(action_executor)
def _state_trans_handler(self):
pass
def put_task(self, priority: int, task: Task):
with self._task_lock:
self._task_queue.put((priority, task))
if self._state == AgentState.IDLE:
self._state = AgentState.READY
self.state_updated.emit(self._state)
def remove_task(self, task: Optional[Task|str]):
with self._task_lock:
temp_queue = queue.PriorityQueue()
while not self._task_queue.empty():
priorty, _task = self._task_queue.get()
assert isinstance(_task, Task)
if isinstance(task, Task):
if _task != task:
temp_queue.put((priorty, task))
elif isinstance(task, str):
if _task.task_id != task:
temp_queue.put((priorty, _task))
self._task_queue = temp_queue
if self._task_queue.empty() and self._state == AgentState.READY:
self._state = AgentState.IDLE
@property
def state(self):
with self._task_lock:
return self._state
@state.setter
def state(self, state: AgentState):
with self._task_lock:
self._prev_state = self._state
self._state = state
self.state_updated.emit(state)
@property
def current_task(self):
with self._task_lock:
return self._current_task
@current_task.setter
def current_task(self, task):
with self._task_lock:
self._current_task = task
def schedule_task(self):
self.state = AgentState.SWITCHING
def stop(self):
self.state = AgentState.STOPPED
def pause(self):
self.state = AgentState.PAUSED
def resume(self):
self.state = AgentState.RUNNING
def from_to(self, from_state, to_state):
with self._task_lock:
if self._prev_state == from_state and self._state == to_state:
return True
else:
return False
def _process_task_queue(self, dt: float):
if self.state == AgentState.RUNNING:
return
self._model.start()
self.state = AgentState.RUNNING
while self.state != AgentState.STOPPED and not self._task_queue.empty():
self.current_task = self._task_queue.get()
priority, task = self.current_task
assert isinstance(task, Task)
self._planning_task(priority, task)
task_executor.submit(task)
while task.state in(TaskState.RUNNING, TaskState.PLANNED, TaskState.READY):
# 如果需要切换任务
if self.from_to(AgentState.RUNNING, AgentState.SWITCHING):
self._save_task(priority, task)
task_executor.cancel(task.task_id)
break
# 如果需要停止任务
elif self.from_to(AgentState.RUNNING, AgentState.STOPPED):
task_executor.cancel(task.task_id)
break
elif self.from_to(AgentState.RUNNING, AgentState.PAUSED):
task_executor.pause(task.task_id)
elif self.from_to(AgentState.PAUSED, AgentState.RUNNING):
task_executor.resume(task.task_id)
try:
if task.state == TaskState.RUNNING and self.state == AgentState.RUNNING:
result = self._model.step(dt)
# 控制仿真速率实时sleep = dt加速sleep < dt
sleep(max(.050, (dt/1000) * 0.200))
except Exception as e:
traceback.print_exc()
print(f"[SimThread] 仿真异常: {e}")
break
self.state = AgentState.IDLE
self.current_task = None
self._model.stop()
def start(self, dt):
if self.state == AgentState.RUNNING:
return
self._task_thread = QThread()
self._worker = AgentTaskWorker(self, dt)
self._worker.moveToThread(self._task_thread)
self._task_thread.started.connect(self._worker.do_work)
self._worker.finished.connect(self._task_thread.quit)
self._worker.finished.connect(self._task_thread.quit) # 任务结束 → 退出线程
self._worker.finished.connect(self._worker.deleteLater) # 自动释放
self._task_thread.finished.connect(self._task_thread.deleteLater) # 自动释放
self._task_thread.finished.connect(self._on_worker_finished) # 任务结束后恢复按钮
self._task_thread.start()
@pyqtSlot()
def _on_worker_finished(self):
self._task_thread = None
# ── 属性(只读)──────────────────────────────────────────────
def _planning_task(self, priority, task):
pass
def _log(self, message, style):
print(message)
def _save_task(self, priority, task):
pass
@pyqtSlot(object)
def _on_event_submitted(self, event):
self._log(f"📥 提交 | {event.type_key} | src={event.source}", "#569cd6")
@pyqtSlot(object)
def _on_event_handled(self, event):
results_str = " | ".join(str(r) for r in event.results)
self._log(
f"✅ 完成 | {event.type_key} | {event.elapsed_ms:.1f}ms | {results_str}",
"#4ec9b0"
)
@pyqtSlot(object, str)
def _on_event_failed(self, event, error):
self._log(f"❌ 失败 | {event.type_key} | {error}", "#f44747")
@pyqtSlot(object)
def _on_event_ignored(self, event):
self._log(f"⚪ 忽略 | {event.type_key} | 无处理器", "#808080")
@pyqtSlot(int, int, int)
def _on_stats_updated(self, total: int, handled: int, failed: int):
pass
# ── TaskExecutor 信号槽 ───────────────────────────────────────
@pyqtSlot(object)
def _on_task_submitted(self, task):
self._log(
f"📥 任务提交: {task.name} [{task.task_id[:8]}]", "#569cd6"
)
@pyqtSlot(object)
def _on_task_started(self, task):
self._log(f"▶ 任务开始执行: {task.name}", "#4ec9b0")
@pyqtSlot(object)
def _on_task_completed(self, task):
self._log(
f"✅ 任务完成: {task.name} 耗时={task.elapsed_s:.1f}s",
"#4CAF50"
)
@pyqtSlot(object, str)
def _on_task_failed(self, task, error):
self._log(f"❌ 任务失败: {task.name} {error}", "#f44747")
@pyqtSlot(object)
def _on_task_cancelled(self, task):
self._log(f"🚫 任务取消: {task.name}", "#FF9800")
@pyqtSlot(object)
def _on_task_paused(self, task):
self._log(f"⏸ 任务暂停: {task.name}", "#9E9E9E")
# self._add_rec_row("state_change", "任务已暂停", "")
@pyqtSlot(object)
def _on_task_resumed(self, task):
self._log(f"▶ 任务恢复: {task.name}", "#4ec9b0")
# self._add_rec_row("state_change", "任务已恢复", "")
@pyqtSlot(object, int)
def _on_segment_entered(self, task, seg_index):
if task.has_route and seg_index < len(task.route.segments):
seg = task.route.segments[seg_index]
msg = (f"进入航段[{seg_index}] "
f"{seg.wp_from.identifier}{seg.wp_to.identifier} "
f"({seg.distance / 1000:.1f}km)")
else:
msg = f"进入航段[{seg_index}]"
self._log(f"✈️ {msg}", "#dcdcaa")
# self._add_rec_row("segment", msg, f"seg_index={seg_index}")
@pyqtSlot(object, int)
def _on_segment_exited(self, task, seg_index):
self._log(f"🏁 离开航段[{seg_index}]", "#808080")
# ── ActionExecutor 信号槽 ─────────────────────────────────────
@pyqtSlot(object)
def _on_action_completed(self, action):
self._log(
f" ✅ Action完成: {action.type_key} "
f"src={action.source} {action.elapsed_ms:.0f}ms",
"#6a9955",
)
@pyqtSlot(str, int, str)
def _on_action_progress(self, action_id: str, percent: int, message: str):
pass
@pyqtSlot(object, list)
def _on_events_produced(self, action, events):
# self._event_count += len(events)
for ev in events:
self._log(
f" ⚡ Event产生: {ev.type_key} src={ev.source}",
"#c586c0",
)
def __repr__(self) -> str:
return (f"{self.__class__.__name__}("
f"id={self.equipment_id}, "
f"name={self.name}, "
f"lc={self.lifecycle.value})")

View File

@ -23,6 +23,7 @@ from uas.route.base_params import GroundParams
from uas.route.base_route import BaseRoute from uas.route.base_route import BaseRoute
from uas.route.base_segment import BaseSegment, BaseTransition, SegmentKind from uas.route.base_segment import BaseSegment, BaseTransition, SegmentKind
from uas.route.base_waypoint import EquipmentDomain from uas.route.base_waypoint import EquipmentDomain
from uas.utils import geo_utils
from uas.utils.geo_utils import GeoUtils from uas.utils.geo_utils import GeoUtils
@ -76,7 +77,7 @@ def _ground_seg_interpolate(seg: GroundSegment, t: float) -> GroundParams:
dist = seg.v_start * dt + 0.5 * seg.acceleration * dt * dt dist = seg.v_start * dt + 0.5 * seg.acceleration * dt * dt
dist = max(0.0, min(dist, seg.distance)) dist = max(0.0, min(dist, seg.distance))
new_lat, new_lon = GeoUtils.offset_position( new_lat, new_lon = geo_utils.offset_position(
seg.start_lat, seg.start_lon, seg.bearing, dist seg.start_lat, seg.start_lon, seg.bearing, dist
) )
speed = max(seg.v_start + seg.acceleration * dt, 0.0) speed = max(seg.v_start + seg.acceleration * dt, 0.0)

View File

@ -10,6 +10,30 @@ EARTH_RADIUS = 6_371_000.0
DEG2RAD = math.pi / 180.0 DEG2RAD = math.pi / 180.0
RAD2DEG = 180.0 / math.pi RAD2DEG = 180.0 / math.pi
# def determine_phase1(alt_start, alt_end, vs):
# if alt_start < 500 and alt_end < 500:
# return FlightPhase.TAKEOFF if vs > 0.5 else FlightPhase.LAND
# if alt_start < 500: return FlightPhase.TAKEOFF
# if alt_end < 500: return FlightPhase.APPROACH
# if vs > 1.0: return FlightPhase.CLIMB
# if vs < -1.0: return FlightPhase.DESCENT
# return FlightPhase.CRUISE
#
# def determine_phase2(altitude, vs, airspeed):
# """判断飞行阶段(同 v1.0"""
# if altitude < 50.0:
# return FlightPhase.LAND
# if vs > 2.0:
# return FlightPhase.CLIMB
# if vs < -2.0:
# if altitude < 3000.0 and airspeed < 103.0:
# return FlightPhase.APPROACH
# return FlightPhase.DESCENT
# if altitude < 3000.0:
# return FlightPhase.APPROACH
# return FlightPhase.CRUISE
def determine_phase(alt: float, vs: float, def determine_phase(alt: float, vs: float,
min_alt: float, max_alt: float): min_alt: float, max_alt: float):
if alt <= min_alt + 50: if alt <= min_alt + 50: