This commit is contained in:
liusongtao 2026-06-18 11:50:33 +08:00
parent a0005ea478
commit c157710a32
10 changed files with 121 additions and 550 deletions

10
.idea/.gitignore vendored Normal file
View File

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

12
.idea/base_agent.iml Normal file
View File

@ -0,0 +1,12 @@
<?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

@ -0,0 +1,25 @@
<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>

72
config.yaml Normal file
View File

@ -0,0 +1,72 @@
# ════════════════════════════════════════════════════════════════
# 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
device:
type: "uav"
device_id: "UAV-001"
protocol: "mavlink"
params:
connection_string: "udp:127.0.0.1:14550"

BIN
skills.db Normal file

Binary file not shown.

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

View File

@ -1,89 +0,0 @@
"""
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}"

View File

@ -1,434 +0,0 @@
# -*- 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,7 +23,6 @@ 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
@ -77,7 +76,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 = geo_utils.offset_position( new_lat, new_lon = GeoUtils.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,30 +10,6 @@ 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: