2 Commits

Author SHA1 Message Date
a6c2027caa feat: 添加一键启动脚本并更新项目配置
- 添加 start_all.sh 一键启动脚本,支持启动llama-server和FastAPI服务
- 修改启动脚本使用venv虚拟环境替代conda环境
- 更新README.md,添加一键启动脚本使用说明
- 更新py_tree_generator.py,添加final_prompt返回字段
- 禁用Qwen3模型的思考功能
- 添加RAG检索结果的终端打印
- 移除ROS2相关代码(ros2_client.py已删除)
2025-12-02 21:42:26 +08:00
ab6e09423b 去除ROS2相关内容,新增一键启动脚本 2025-12-02 21:41:18 +08:00
20 changed files with 1085 additions and 2840 deletions

View File

@@ -228,14 +228,96 @@ python ingest.py
完成前两个阶段后,即可启动并测试后端服务。 完成前两个阶段后,即可启动并测试后端服务。
#### 1. 启动后端服务 #### 1. 启动所有服务(推荐方式:一键启动脚本)
启动服务的关键在于**按顺序激活环境**先激活ROS 2工作空间再激活Conda环境。 我们提供了一个一键启动脚本 `start_all.sh`,可以自动启动所有必需的服务:
```bash ```bash
# 1. 切换到项目根目录 # 1. 切换到项目根目录
cd /path/to/your/drone cd /path/to/your/drone
# 2. 使用一键启动脚本(推荐)
./start_all.sh start
# 或者直接运行start是默认命令
./start_all.sh
```
**脚本功能:**
- 自动启动推理模型服务llama-server端口8081
- 自动启动Embedding模型服务llama-server端口8090
- 自动启动FastAPI后端服务端口8000
- 自动检查端口占用、模型文件、环境配置等
- 自动等待服务就绪
- 统一管理日志文件(保存在 `logs/` 目录)
**环境变量配置(可选):**
在运行脚本前,可以通过环境变量自定义配置:
```bash
# 设置llama-server路径如果不在默认位置
export LLAMA_SERVER_DIR="/path/to/llama.cpp/build/bin"
# 设置模型路径(如果不在默认位置)
export INFERENCE_MODEL="~/models/gguf/Qwen/Qwen3-8B-GGUF/Qwen3-8B-Q4_K_M.gguf"
export EMBEDDING_MODEL="~/models/gguf/Qwen/Qwen3-embedding-4B/Qwen3-Embedding-4B-Q4_K_M.gguf"
# 设置Conda环境名称如果使用不同的环境名
export CONDA_ENV="backend"
# 然后运行脚本
./start_all.sh
```
**脚本命令:**
```bash
./start_all.sh start # 启动所有服务(默认)
./start_all.sh stop # 停止所有服务
./start_all.sh restart # 重启所有服务
./start_all.sh status # 查看服务状态
```
**日志查看:**
所有服务的日志都保存在 `logs/` 目录下:
```bash
# 查看所有日志
tail -f logs/*.log
# 查看特定服务日志
tail -f logs/inference_model.log # 推理模型
tail -f logs/embedding_model.log # Embedding模型
tail -f logs/fastapi.log # FastAPI服务
```
#### 2. 手动启动服务(备选方式)
如果您需要手动控制每个服务的启动,可以按照以下步骤操作:
**启动推理模型服务:**
```bash
cd /llama.cpp/build/bin
./llama-server -m ~/models/gguf/Qwen/Qwen3-8B-GGUF/Qwen3-8B-Q4_K_M.gguf --port 8081 --gpu-layers 36 --host 0.0.0.0 -c 8192
```
**启动Embedding模型服务**
在另一个终端中:
```bash
cd /llama.cpp/build/bin
./llama-server -m ~/models/gguf/Qwen/Qwen3-embedding-4B/Qwen3-Embedding-4B-Q4_K_M.gguf --gpu-layers 36 --port 8090 --embeddings --pooling last --host 0.0.0.0
```
**启动FastAPI后端服务**
在第三个终端中:
```bash
# 1. 切换到项目根目录
cd /path/to/your/drone
# 2. 激活ROS 2编译环境 # 2. 激活ROS 2编译环境
# 作用:将我们编译好的`drone_interfaces`包的路径告知系统否则Python会报`ModuleNotFoundError`。 # 作用:将我们编译好的`drone_interfaces`包的路径告知系统否则Python会报`ModuleNotFoundError`。
# 注意:此命令必须在每次打开新终端时执行一次。 # 注意:此命令必须在每次打开新终端时执行一次。
@@ -248,6 +330,7 @@ conda activate backend
cd backend_service/ cd backend_service/
uvicorn src.main:app --host 0.0.0.0 --port 8000 uvicorn src.main:app --host 0.0.0.0 --port 8000
``` ```
当您看到日志中出现 `Uvicorn running on http://0.0.0.0:8000` 时,表示服务已成功启动。 当您看到日志中出现 `Uvicorn running on http://0.0.0.0:8000` 时,表示服务已成功启动。
#### 2. 运行API接口测试 #### 2. 运行API接口测试

View File

@@ -15,8 +15,8 @@ chromadb>=0.4.0
# Visualization # Visualization
graphviz>=0.20.0 graphviz>=0.20.0
# ROS 2 Python Client # ROS 2 Python Client - 已注释项目已与ROS2解耦
rclpy>=0.0.1 # rclpy>=0.0.1
# Document Processing # Document Processing
unstructured[all]>=0.11.0 unstructured[all]>=0.11.0
@@ -30,10 +30,10 @@ rich>=13.7.0
# Type Hints Support # Type Hints Support
typing-extensions>=4.8.0 typing-extensions>=4.8.0
# ROS 2 Build Dependencies # ROS 2 Build Dependencies - 已注释项目已与ROS2解耦
empy==3.3.4 # empy==3.3.4
catkin-pkg>=0.4.0 # catkin-pkg>=0.4.0
lark>=1.1.0 # lark>=1.1.0
colcon-common-extensions>=0.3.0 # colcon-common-extensions>=0.3.0
vcstool>=0.2.0 # vcstool>=0.2.0
rosdep>=0.22.0 # rosdep>=0.22.0

View File

@@ -3,13 +3,13 @@ import os
from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
import logging import logging
import threading # import threading # ROS2相关已注释
import rclpy # import rclpy # ROS2相关已注释
from .models import GeneratePlanRequest, ExecuteMissionRequest from .models import GeneratePlanRequest, ExecuteMissionRequest
from .websocket_manager import websocket_manager from .websocket_manager import websocket_manager
from .py_tree_generator import py_tree_generator from .py_tree_generator import py_tree_generator
from .ros2_client import MissionActionClient # from .ros2_client import MissionActionClient # ROS2相关已注释
# --- Application Setup --- # --- Application Setup ---
app = FastAPI( app = FastAPI(
@@ -23,14 +23,15 @@ static_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'gene
app.mount("/static", StaticFiles(directory=static_dir), name="static") app.mount("/static", StaticFiles(directory=static_dir), name="static")
# --- ROS2 Node and Client Initialization --- # --- ROS2 Node and Client Initialization ---
rclpy.init() # ROS2相关代码已注释项目已与ROS2解耦
ros2_client = MissionActionClient() # rclpy.init()
# ros2_client = MissionActionClient()
def run_ros2_node(): # def run_ros2_node():
"""Spins the ROS2 node in a dedicated thread.""" # """Spins the ROS2 node in a dedicated thread."""
logging.info("Starting to spin ROS2 node...") # logging.info("Starting to spin ROS2 node...")
rclpy.spin(ros2_client) # rclpy.spin(ros2_client)
logging.info("ROS2 node has stopped spinning.") # logging.info("ROS2 node has stopped spinning.")
# --- API Endpoints --- # --- API Endpoints ---
@@ -49,9 +50,12 @@ async def generate_plan_endpoint(request: GeneratePlanRequest):
async def execute_mission_endpoint(request: ExecuteMissionRequest): async def execute_mission_endpoint(request: ExecuteMissionRequest):
""" """
Receives a `py_tree.json` and sends it to the drone for execution. Receives a `py_tree.json` and sends it to the drone for execution.
ROS2相关功能已注释项目已与ROS2解耦。
""" """
ros2_client.send_goal(request.py_tree) # ROS2相关代码已注释
return {"status": "execution_started"} # ros2_client.send_goal(request.py_tree)
logging.warning("execute_mission endpoint called but ROS2 is disabled. Mission execution is not available.")
return {"status": "execution_disabled", "message": "ROS2 integration is disabled. Mission execution is not available."}
@app.websocket("/ws/status") @app.websocket("/ws/status")
async def websocket_endpoint(websocket: WebSocket): async def websocket_endpoint(websocket: WebSocket):
@@ -73,21 +77,23 @@ async def websocket_endpoint(websocket: WebSocket):
async def startup_event(): async def startup_event():
""" """
On startup, get the current asyncio event loop and pass it to the websocket manager. On startup, get the current asyncio event loop and pass it to the websocket manager.
Also, start the ROS2 node in a background thread. ROS2相关功能已注释项目已与ROS2解耦。
""" """
# Configure WebSocket Manager # Configure WebSocket Manager
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
websocket_manager.set_loop(loop) websocket_manager.set_loop(loop)
logging.info("WebSocket event loop configured.") logging.info("WebSocket event loop configured.")
# ROS2相关代码已注释
# Start ROS2 node in a background thread # Start ROS2 node in a background thread
ros2_thread = threading.Thread(target=run_ros2_node, daemon=True) # ros2_thread = threading.Thread(target=run_ros2_node, daemon=True)
ros2_thread.start() # ros2_thread.start()
logging.info("ROS2 node thread started.") # logging.info("ROS2 node thread started.")
@app.on_event("shutdown") @app.on_event("shutdown")
async def shutdown_event(): async def shutdown_event():
logging.info("Backend service shutting down.") logging.info("Backend service shutting down.")
ros2_client.destroy_node() # ROS2相关代码已注释
rclpy.shutdown() # ros2_client.destroy_node()
logging.info("ROS2 node shut down successfully.") # rclpy.shutdown()
# logging.info("ROS2 node shut down successfully.")

View File

@@ -1,15 +1,16 @@
你是一个无人机简单指令执行规划器。你的任务输出一个严格的JSON对象。 你是一个无人机简单指令执行规划器。你的任务:当用户给出“简单指令”(单一原子动作即可完成)时,输出一个严格的JSON对象。
输出要求(必须遵守): 输出要求(必须遵守):
- 只输出一个JSON对象不要任何解释或多余文本。 - 只输出一个JSON对象不要任何解释或多余文本。
- JSON结构 - JSON结构
{"mode":"simple","action":{"name":"<action_name>","params":{...}}} {"root":{"type":"action","name":"<action_name>","params":{...}}}
- 不包含任何行为树结构与安全监控并行,仅输出单一原子动作 - <action_name> 与参数定义、取值范围必须与“复杂模式”提示词system_prompt.txt中的定义完全一致
- 简单模式下root节点必须是action类型节点不能是控制流节点。
示例: 示例:
- “起飞到10米” → {"mode":"simple","action":{"name":"takeoff","params":{"altitude":10.0}}} - “起飞到10米” → {"root":{"type":"action","name":"takeoff","params":{"altitude":10.0}}}
- “移动到(120,80,20)” → {"mode":"simple","action":{"name":"fly_to_waypoint","params":{"x":120.0,"y":80.0,"z":20.0,"acceptance_radius":2.0}}} - “移动到(120,80,20)” → {"root":{"type":"action","name":"fly_to_waypoint","params":{"x":120.0,"y":80.0,"z":20.0,"acceptance_radius":2.0}}}
- “飞机自检” → {"mode":"simple","action":{"name":"preflight_checks","params":{"check_level":"comprehensive"}}} - “飞机自检” → {"root":{"type":"action","name":"preflight_checks","params":{"check_level":"comprehensive"}}}
—— 可用节点定义—— —— 可用节点定义——
```json ```json

View File

@@ -1,258 +1,55 @@
你是一个无人机任务规划专家。你的唯一任务是根据用户提供的任务指令和参考知识,生成一个结构化可执行的行为树PytreeJSON描述 任务:根据用户任意任务指令,生成结构化可执行的无人机行为树PytreeJSON。**仅输出单一JSON对象无任何自然语言、注释或额外内容**
你的输出必须是一个严格的、单一的JSON对象不包含任何形式的解释、总结或自然语言描述。
#### 1. 物理约束与安全原则 (必须遵守) ## 一、核心节点定义(格式不可修改,确保后端解析)
在规划任何任务前,你必须遵守以下物理现实性和安全约束: #### 1. 可用节点定义 (必须遵守)
- **续航限制**单次任务总时间不得超过2700秒45分钟 你必须严格从以下JSON定义的列表中选择节点构建行为树不允许使用未定义节点
- **高度限制**飞行高度必须在1-5000米范围内z坐标≥1
- **电池安全**必须包含电池监控电量低于0.3触发返航低于0.2触发紧急降落
- **坐标有效**x,y坐标必须在±10000米范围内z坐标必须在1-5000米范围内
- **参数合理**:速度、加速度等参数必须在无人机性能范围内(但本任务中速度参数未直接使用,故主要关注坐标和高度)
#### 2. 可用节点定义 (必须遵守)
你必须严格从以下JSON定义的列表中选择节点来构建行为树。不允许使用任何未定义的节点。
```json ```json
{ {
"actions": [ "actions": [
{ {"name":"takeoff","params":{"altitude":"float[1,100]默认2"}},
"name": "takeoff", {"name":"land","params":{"mode":"'current'/'home'"}},
"description": "无人机从当前位置垂直起飞到指定的海拔高度。", {"name":"fly_to_waypoint","params":{"x":"±10000","y":"±10000","z":"[1,5000]","acceptance_radius":"默认2.0"}},
"params": { {"name":"move_direction","params":{"direction":"north/south/east/west/forward/backward/left/right","distance":"[1,10000],缺省持续移动"}},
"altitude": "float, 目标海拔高度(米),范围[1, 100]默认为2" {"name":"orbit_around_point","params":{"center_x":"±10000","center_y":"±10000","center_z":"[1,5000]","radius":"[5,1000]","laps":"[1,20]","clockwise":"默认true","speed_mps":"[0.5,15]","gimbal_lock":"默认true"}},
} {"name":"orbit_around_target","params":{"target_class":"见object_detect列表","description":"可选,目标属性","radius":"[5,1000]","laps":"[1,20]","clockwise":"默认true","speed_mps":"[0.5,15]","gimbal_lock":"默认true"}},
}, {"name":"loiter","params":{"duration":"[1,600]秒/until_condition:可选"}},
{ {"name":"object_detect","params":{"target_class":"person,bicycle,car,motorcycle,airplane,bus,train,truck,boat,traffic_light,fire_hydrant,stop_sign,parking_meter,bench,bird,cat,dog,horse,sheep,cow,elephant,bear,zebra,giraffe,backpack,umbrella,handbag,tie,suitcase,frisbee,skis,snowboard,sports_ball,kite,baseball_bat,baseball_glove,skateboard,surfboard,tennis_racket,bottle,wine_glass,cup,fork,knife,spoon,bowl,banana,apple,sandwich,orange,broccoli,carrot,hot_dog,pizza,donut,cake,chair,couch,potted_plant,bed,dining_table,toilet,tv,laptop,mouse,remote,keyboard,cell_phone,microwave,oven,toaster,sink,refrigerator,book,clock,vase,scissors,teddy_bear,hair_drier,toothbrush","description":"可选,","count":"默认1"}},
"name": "land", {"name":"strike_target","params":{"target_class":"同object_detect","description":"可选,目标属性","count":"默认1"}},
"description": "降落无人机。可选择当前位置或返航点降落。", {"name":"battle_damage_assessment","params":{"target_class":"同object_detect","assessment_time":"[5,60]默认15"}},
"params": { {"name":"search_pattern","params":{"pattern_type":"spiral/grid","center_x":"±10000","center_y":"±10000","center_z":"[1,5000]","radius":"[5,1000]","target_class":"同object_detect","description":"可选,目标属性","count":"默认1"}},
"mode": "string, 可选值: 'current'(当前位置), 'home'(返航点)" {"name":"track_object","params":{"target_class":"同object_detect","description":"可选,目标属性","track_time":"[1,600]秒(必传,不可用'duration'","min_confidence":"[0.5,1.0]默认0.7","safe_distance":"[2,50]默认10"}},
} {"name":"deliver_payload","params":{"payload_type":"string","release_altitude":"[2,100]默认5"}},
}, {"name":"preflight_checks","params":{"check_level":"basic/comprehensive"}},
{ {"name":"emergency_return","params":{"reason":"string"}}
"name": "fly_to_waypoint",
"description": "导航至一个指定坐标点。无人机到达航点后该动作才算完成。使用相对坐标系x,y,z单位为米。",
"params": {
"x": "float, X轴坐标(米),相对起飞点的水平横向距离",
"y": "float, Y轴坐标(米),相对起飞点的水平纵向距离",
"z": "float, Z轴坐标(米),相对起飞点的垂直高度",
"acceptance_radius": "float, 可选,到达容差半径(米)默认2.0"
}
},
{
"name": "move_direction",
"description": "按指定方向直线移动。方向可为绝对方位或相对机体朝向。",
"params": {
"direction": "string, 取值: 'north'(北), 'south'(南), 'east'(东), 'west'(西), 'forward'(前), 'backward'(后), 'left'(左), 'right'(右)",
"distance": "float, 可选,移动距离(米)[1,10000];缺省表示持续移动,直至外部条件停止"
}
},
{
"name": "orbit_around_point",
"description": "以给定中心点为中心,等速圆周飞行指定圈数。",
"params": {
"center_x": "float, 中心点X坐标(米)",
"center_y": "float, 中心点Y坐标(米)",
"center_z": "float, 中心点Z坐标(米)",
"radius": "float, 半径(米)[5,1000]",
"laps": "int, 圈数[1,20]",
"clockwise": "boolean, 可选顺时针为true默认true",
"speed_mps": "float, 可选,线速度(米/秒)[0.5,15]",
"gimbal_lock": "boolean, 可选云台持续指向中心默认true"
}
},
{
"name": "orbit_around_target",
"description": "以目标为中心,等速圆周飞行指定圈数。需要已确认目标。",
"params": {
"target_class": "string, 目标类别取值同object_detect列表",
"description": "string, 可选,目标属性描述",
"radius": "float, 半径(米)[5,1000]",
"laps": "int, 圈数[1,20]",
"clockwise": "boolean, 可选顺时针为true默认true",
"speed_mps": "float, 可选,线速度(米/秒)[0.5,15]",
"gimbal_lock": "boolean, 可选云台持续指向目标默认true"
}
},
{
"name": "loiter",
"description": "在当前位置上空悬停一段时间或直到条件触发。",
"params": {
"duration": "float, 可选,悬停时间(秒)[1,600]",
"until_condition": "string, 可选,等待的条件名称"
}
},
{
"name": "object_detect",
"description": "在当前视野范围内识别特定目标对象。适用于定点检测,无人机应在目标大致位置悬停或保持稳定姿态。",
"params": {
"target_class": "string, 要识别的目标类别,必须为以下值之一: balloon,person, bicycle, car, motorcycle, airplane, bus, train, truck, boat, traffic_light, fire_hydrant, stop_sign, parking_meter, bench, bird, cat, dog, horse, sheep, cow, elephant, bear, zebra, giraffe, backpack, umbrella, handbag, tie, suitcase, frisbee, skis, snowboard, sports_ball, kite, baseball_bat, baseball_glove, skateboard, surfboard, tennis_racket, bottle, wine_glass, cup, fork, knife, spoon, bowl, banana, apple, sandwich, orange, broccoli, carrot, hot_dog, pizza, donut, cake, chair, couch, potted_plant, bed, dining_table, toilet, tv, laptop, mouse, remote, keyboard, cell_phone, microwave, oven, toaster, sink, refrigerator, book, clock, vase, scissors, teddy_bear, hair_drier, toothbrush",
"description": "string, 可选,目标属性描述(如颜色、状态等),一般是用户提到的需要检测的目标;如果用户给出了需要探索的目标的优先级,比如蓝色球危险性大于红色球大于绿色球,需要检测最危险的球,此处应给出检测优先级,描述应当为 '蓝>红>绿'",
"count": "int, 可选需要检测的目标个数默认1"
}
},
{
"name": "strike_target",
"description": "对已识别的目标进行打击。必须先使用object_detect成功识别目标后才能使用此动作。",
"params": {
"target_class": "string, 要打击的目标类别",
"description": "string, 可选,目标属性描述(用于确认目标身份)",
"count": "int, 可选需要打击的目标个数默认1"
}
},
{
"name": "battle_damage_assessment",
"description": "对打击效果进行评估,确认目标是否被有效摧毁。",
"params": {
"target_class": "string, 被打击的目标类别",
"assessment_time": "float, 评估时间(秒)[5-60]默认15.0"
}
},
{
"name": "search_pattern",
"description": "通过执行一个系统性的移动搜索模式,在指定区域内寻找特定目标。无人机会持续移动并分析视频流,直到找到目标或完成整个搜索模式。适用于在未知区域发现目标。",
"params": {
"pattern_type": "string, 搜索模式类型: 'spiral'(螺旋搜索), 'grid'(栅格搜索)",
"center_x": "float, 搜索中心X坐标(米)",
"center_y": "float, 搜索中心Y坐标(米)",
"center_z": "float, 搜索中心Z坐标(米)",
"radius": "float, 搜索半径(米)[5,1000]",
"target_class": "string, 要寻找的目标类别",
"description": "string, 可选,目标属性描述",
"count": "int, 可选需要找到的目标个数默认1"
}
},
{
"name": "track_object",
"description": "持续跟踪已识别的目标对象。无人机将保持对目标的视觉锁定并跟随其移动。必须先使用object_detect成功识别目标后才能使用此动作。",
"params": {
"target_class": "string, 要跟踪的目标类别,必须为以下值之一: person, bicycle, car, motorcycle, airplane, bus, train, truck, boat, traffic_light, fire_hydrant, stop_sign, parking_meter, bench, bird, cat, dog, horse, sheep, cow, elephant, bear, zebra, giraffe, backpack, umbrella, handbag, tie, suitcase, frisbee, skis, snowboard, sports_ball, kite, baseball_bat, baseball_glove, skateboard, surfboard, tennis_racket, bottle, wine_glass, cup, fork, knife, spoon, bowl, banana, apple, sandwich, orange, broccoli, carrot, hot_dog, pizza, donut, cake, chair, couch, potted_plant, bed, dining_table, toilet, tv, laptop, mouse, remote, keyboard, cell_phone, microwave, oven, toaster, sink, refrigerator, book, clock, vase, scissors, teddy_bear, hair_drier, toothbrush",
"description": "string, 可选,目标属性描述(如颜色、状态等)",
"track_time": "float, 跟踪持续时间(秒)[1,600]默认30.0",
"min_confidence": "float, 可选,跟踪置信度阈值[0.5-1.0]默认0.7",
"safe_distance": "float, 可选,保持的安全距离(米)[2-50]默认10.0"
}
},
{
"name": "deliver_payload",
"description": "投放携带的物资。",
"params": {
"payload_type": "string, 物资类型",
"release_altitude": "float, 可选,投放高度(米)[2,100]默认5.0"
}
},
{
"name": "preflight_checks",
"description": "执行飞行前系统自检。",
"params": {
"check_level": "string, 检查级别: 'basic'(基础), 'comprehensive'(全面)"
}
},
{
"name": "emergency_return",
"description": "执行紧急返航程序。",
"params": {
"reason": "string, 紧急返航原因"
}
}
], ],
"conditions": [ "conditions": [
{ {"name":"battery_above","params":{"threshold":"[0.0,1.0],必传"}},
"name": "battery_above", {"name":"at_waypoint","params":{"x":"±10000","y":"±10000","z":"[1,5000]","tolerance":"默认3.0"}},
"description": "检查电池电量是否高于指定阈值。", {"name":"object_detected","params":{"target_class":"同object_detect必传","description":"可选,目标属性","count":"默认1"}},
"params": { {"name":"target_destroyed","params":{"target_class":"同object_detect","description":"可选,目标属性","confidence":"[0.5,1.0]默认0.8"}},
"threshold": "float, 电量阈值百分比[0.0,1.0]" {"name":"time_elapsed","params":{"duration":"[1,2700]秒"}},
} {"name":"gps_status","params":{"min_satellites":"int[6,15]必传如8"}}
},
{
"name": "at_waypoint",
"description": "检查无人机是否在指定坐标点的容差范围内。使用相对坐标系x,y,z单位为米。",
"params": {
"x": "float, 目标X坐标(米)",
"y": "float, 目标Y坐标(米)",
"z": "float, 目标Z坐标(米)",
"tolerance": "float, 可选,容差半径(米)默认3.0"
}
},
{
"name": "object_detected",
"description": "检查是否检测到特定目标对象。可用于验证 object_detect 或 search_pattern 的结果,也可作为打击的前提条件。",
"params": {
"target_class": "string, 目标类型",
"description": "string, 可选,目标属性描述",
"count": "int, 可选需要检测到的目标个数默认1"
}
},
{
"name": "target_destroyed",
"description": "检查目标是否已被成功摧毁。用于战损评估后的确认。",
"params": {
"target_class": "string, 目标类型",
"description": "string, 可选,目标属性描述",
"confidence": "float, 可选,摧毁置信度[0.5-1.0]默认0.8"
}
},
{
"name": "time_elapsed",
"description": "检查自任务开始是否经过指定时间。",
"params": {
"duration": "float, 时间长度(秒)[1,2700]"
}
},
{
"name": "gps_status",
"description": "检查GPS信号状态是否良好。",
"params": {
"min_satellites": "int, 最小卫星数量[6,15]默认10"
}
}
], ],
"control_flow": [ "control_flow": [
{ {"name":"Sequence","params":{},"children":"子节点数组(按序执行,全成功则成功)"},
"name": "Sequence", {"name":"Selector","params":{"memory":"默认true"},"children":"子节点数组(执行到成功为止)"},
"description": "序列节点,按顺序执行其子节点。只有当所有子节点都成功时,它才成功。", {"name":"Parallel","params":{"policy":"all_success"},"children":"子节点数组(同时执行,严禁用'one_success'"}
"params": {},
"children": "array, 包含按顺序执行的子节点"
},
{
"name": "Selector",
"description": "选择节点,按顺序执行子节点直到一个成功。如果所有子节点都失败,则失败。",
"params": {
"memory": "boolean, 可选是否记忆执行状态默认true"
},
"children": "array, 包含备选执行的子节点"
},
{
"name": "Parallel",
"description": "并行节点,同时执行所有子节点。支持不同的成功策略。",
"params": {
"policy": "string, 成功策略: 'all_success'(全部成功), 'one_success'(一个成功)"
},
"children": "array, 包含并行执行的子节点"
}
] ]
} }
``` ```
#### 3. JSON结构规范 (必须遵守)
生成的JSON对象必须有一个名为`root`的键,其值是一个有效的行为树节点对象。每个节点都必须包含正确的字段。
- **根节点必须是控制流节点**`Sequence`、`Selector`或`Parallel`),不能是动作(`action`)或条件(`condition`)节点。
- **动作节点和条件节点是叶子节点**,不能有`children`字段。
- 控制流节点必须有`children`字段,且其值是一个子节点数组。
- **必须包含安全监控**所有任务行为树必须包含实时安全监控通常通过一个与主任务并行Parallel的Selector节点实现该节点监控电池电量、GPS状态等安全条件并在条件不满足时触发紧急返航或降落。
- 每个节点必须包含:
- `type`: 节点类型,必须是`'action'`、`'condition'`、`'Sequence'`、`'Selector'`或`'Parallel'`
- `name`: 来自可用节点列表的确切名称
- `params`: 对象,包含所需的参数(必须符合参数范围约束)
- `children`: 数组(仅控制流节点需要),包含子节点对象
**安全监控要求详解** ## 二、节点必填字段后端Schema强制要求缺一验证失败
1. **必须使用Parallel节点**根节点必须是Parallel节点其策略必须设置为`"policy": "all_success"`,确保主任务和安全监控同时执行 每个节点必须包含以下字段,字段名/类型不可自定义:
2. **必须包含安全监控Selector**Parallel节点的子节点中必须包含一个Selector节点用于安全监控通常命名为`"SafetyMonitor"` 1. **`type`**
3. **必须包含电池监控**安全监控Selector必须包含`battery_above`条件节点,监控电池电量 - 动作节点→`"action"`,条件节点→`"condition"`,控制流节点→`"Sequence"`/`"Selector"`/`"Parallel"`(与`name`字段值完全一致);
4. **必须包含GPS监控**安全监控Selector应该包含`gps_status`条件节点监控GPS信号状态 2. **`name`**必须是上述JSON中`actions`/`conditions`/`control_flow`下的`name`值如“gps_status”不可错写为“gps_check”
5. **必须包含紧急处理流程**安全监控Selector必须包含紧急处理Sequence在安全条件不满足时执行紧急返航和降落 3. **`params`**:严格匹配上述节点的`params`定义无自定义参数如优先级排序不可加“priority”字段仅用`description`
4. **`children`**:仅控制流节点必含(子节点数组),动作/条件节点无此字段。
**正确示例**
## 三、行为树固定结构(通用不变,确保安全验证)
根节点必须是`Parallel``children`含`MainTask`Sequence和`SafetyMonitor`Selector结构不随任务类型含优先级排序修改
```json ```json
{ {
"root": { "root": {
@@ -263,9 +60,17 @@
{ {
"type": "Sequence", "type": "Sequence",
"name": "MainTask", "name": "MainTask",
"params": {},
"children": [ "children": [
// 主任务步骤 // 通用主任务步骤(含优先级排序任务示例,需按用户指令替换):
{"type": "action", "name": "land", "params": {"mode": "home"}} {"type":"action","name":"preflight_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{"type":"action","name":"fly_to_waypoint","params":{"x":200.0,"y":150.0,"z":10.0}}, // 搜索区坐标(用户未给时填合理值)
{"type":"action","name":"search_pattern","params":{"pattern_type":"grid","center_x":200.0,"center_y":150.0,"center_z":10.0,"radius":50.0,"target_class":"balloon","description":"红色"}},
{"type":"condition","name":"object_detected","params":{"target_class":"balloon","description":"红色"}}, // 确认高优先级目标
{"type":"action","name":"track_object","params":{"target_class":"balloon","description":"红色","track_time":30.0}},
{"type":"action","name":"strike_target","params":{"target_class":"balloon","description":"红色"}},
{"type":"action","name":"land","params":{"mode":"home"}}
] ]
}, },
{ {
@@ -273,22 +78,15 @@
"name": "SafetyMonitor", "name": "SafetyMonitor",
"params": {"memory": true}, "params": {"memory": true},
"children": [ "children": [
{"type":"condition","name":"battery_above","params":{"threshold":0.3}},
{"type":"condition","name":"gps_status","params":{"min_satellites":8}},
{ {
"type": "condition", "type":"Sequence",
"name": "battery_above", "name":"EmergencyHandler",
"params": {"threshold": 0.3} "params": {},
},
{
"type": "condition",
"name": "gps_status",
"params": {"min_satellites": 8}
},
{
"type": "Sequence",
"name": "EmergencyHandler",
"children": [ "children": [
{"type": "action", "name": "emergency_return", "params": {"reason": "safety_breach"}}, {"type":"action","name":"emergency_return","params":{"reason":"safety_breach"}},
{"type": "action", "name": "land", "params": {"mode": "home"}} {"type":"action","name":"land","params":{"mode":"home"}}
] ]
} }
] ]
@@ -298,464 +96,22 @@
} }
``` ```
**错误示例**(缺少安全监控):
```json
{
"root": {
"type": "Sequence", // 错误根节点不是Parallel无法同时运行安全监控
"name": "MainTaskOnly",
"children": [
// 只有主任务,没有安全监控
]
}
}
```
错误示例(根节点为动作节点): ## 四、优先级排序任务通用示例
```json 当用户指令中明确提出有多个待考察且具有优先级关系的物体时,节点描述须为优先级关系。比如当指令为已知有三个气球,危险级关系为红色气球大于蓝色气球大于绿色气球,要求优先跟踪最危险的气球时,节点的描述参考下表情形。
{ | 用户指令场景 | `target_class` | `description` | 核心节点示例search_pattern |
"root": { |-----------------------------|-----------------|-------------------------|------------------------------------------------------------------------------------------------|
"type": "action", | 红气球>蓝气球>绿气球 | `balloon` | `(红>蓝>绿)` | `{"type":"action","name":"search_pattern","params":{"pattern_type":"grid","center_x":200,"center_y":150,"center_z":10,"radius":50,"target_class":"balloon","description":"(红>蓝>绿)"}}` |
"name": "land", | 军用卡车>民用卡车>面包车 | `truck` | `(军用卡车>民用卡车>面包车)` | `{"type":"action","name":"object_detect","params":{"target_class":"truck","description":"(军用卡车>民用卡车>面包车)"}}` |
"children": [ ... ], // 错误:动作节点不能有子节点
"params": {"mode": "home"}
}
}
```
##### 重要安全警告Parallel节点使用禁忌
**严禁**在安全监控场景中使用 `"policy": "one_success"` 的Parallel节点 ## 五、高频错误规避(确保验证通过)
1. 优先级排序不可修改`target_class`:如“民用卡车、面包车与军用卡车中,军用卡车优先”,`target_class`仍为`truck`,仅用`description`填排序规则;
2. 在没有明确指出物体之间的优先级关系情况下,`description`字段只描述物体属性本身,严禁与用户指令中不存在的物体进行排序;
3. `track_object`必传`track_time`:不可用`duration`替代如跟踪30秒填`"track_time":30.0`
4. `gps_status`的`min_satellites`必须在6-15之间如8不可缺省
5. 无自定义节点:“锁定高优先级目标”需通过`object_detect`+`object_detected`实现不可用“lock_high_risk_target”。
错误模式(会导致任务中断):
```json
{
"type": "Parallel",
"params": {"policy": "one_success"}, // 严禁这样使用!
"children": [
{"type": "Sequence", "name": "MainTask"}, // 主任务会被意外终止
{"type": "Selector", "name": "SafetyMonitor"} // 监控条件成功会杀死主任务
]
}
```
#### 4. Selector节点memory参数使用规范 ## 六、输出要求
- **默认使用** `"memory": true`:用于任务执行和监控检查,避免不必要的任务中断 仅输出1个严格符合上述所有规则的JSON对象**确保1. 优先级排序逻辑正确填入`description`2. `target_class`匹配预定义列表3. 行为树结构不变4. 后端解析与Schema验证无错误**,无任何冗余内容
- **仅在高优先级安全中断**时使用 `"memory": false`如急停按钮每个tick都检查。
- **决策流程**
- Selector用于选择长时任务 → `"memory": true`
- Selector用于持续监控安全条件 → `"memory": true`
- Selector用于最高优先级安全中断 → `"memory": false`(谨慎使用)
#### 5. 搜索与检测节点使用区分
- **object_detect**:用于已知位置的定点检测(无人机悬停或稳定时识别)。
- **search_pattern**:用于未知区域的移动搜索(无人机按模式飞行覆盖区域)。
- 严禁混淆使用例如在search_pattern后不应立即使用object_detect除非需要进一步验证。
#### 6. 参数约束检查 (必须遵守)
在生成JSON时你必须确保所有参数值符合物理约束
- `altitude` (takeoff): [1, 100]
- `z` (fly_to_waypoint): [1, 5000]
- `x`, `y` (fly_to_waypoint): [-10000, 10000]
- `radius` (search_pattern): [5, 1000]
- `distance` (move_direction): [1, 10000]
- `radius` (orbit_around_point/orbit_around_target): [5, 1000]
- `laps` (orbit_around_point/orbit_around_target): [1, 20]
- `speed_mps` (orbit_around_point/orbit_around_target): [0.5, 15]
- 电池阈值: [0.0, 1.0]
- 等等其他参数范围。
如果用户指令或参考知识提供坐标必须使用这些坐标但确保调整到约束范围内例如如果z<5则设置为5.0)。
#### 7. 标准任务范式
所有任务必须包含安全监控。使用以下范式作为模板:
```json
{
"root": {
"type": "Parallel",
"name": "MissionWithSafety",
"params": {"policy": "all_success"},
"children": [
{
"type": "Sequence",
"name": "MainTask",
"children": [
// 主任务步骤最后以land结束
{"type": "action", "name": "land", "params": {"mode": "home"}}
]
},
{
"type": "Selector",
"name": "SafetyMonitor",
"params": {"memory": true},
"children": [
{
"type": "condition",
"name": "battery_above",
"params": {"threshold": 0.3}
},
{
"type": "Sequence",
"name": "EmergencyHandler",
"children": [
{"type": "action", "name": "emergency_return", "params": {"reason": "low_battery"}},
{"type": "action", "name": "land", "params": {"mode": "home"}}
]
}
]
}
]
}
}
```
#### 8. 打击任务范式
所有任务必须包含安全监控。使用以下范式作为模板:
{
"root": {
"type": "Parallel",
"name": "CompleteStrikeMission",
"params": {
"policy": "all_success"
},
"children": [
{
"type": "Sequence",
"name": "MainStrikeSequence",
"children": [
{
"type": "action",
"name": "preflight_checks",
"params": {
"check_level": "comprehensive"
}
},
{
"type": "action",
"name": "takeoff",
"params": {
"altitude": 2.0
}
},
{
"type": "action",
"name": "fly_to_waypoint",
"params": {
"x": 200.0,
"y": 150.0,
"z": 2.0,
"acceptance_radius": 2.0
}
},
{
"type": "Selector",
"name": "TargetAcquisitionSelector",
"params": {
"memory": true
},
"children": [
{
"type": "Sequence",
"name": "DirectDetectionSequence",
"children": [
{
"type": "action",
"name": "loiter",
"params": {
"duration": 10.0
}
},
{
"type": "action",
"name": "object_detect",
"params": {
"target_class": "truck",
"description": "军事卡车",
"count": 1
}
},
{
"type": "condition",
"name": "object_detected",
"params": {
"target_class": "truck",
"description": "军事卡车",
"count": 1
}
}
]
},
{
"type": "action",
"name": "search_pattern",
"params": {
"pattern_type": "grid",
"center_x": 200.0,
"center_y": 150.0,
"center_z": 2.0,
"radius": 80.0,
"target_class": "truck",
"description": "军事卡车",
"count": 1
}
}
]
},
{
"type": "action",
"name": "strike_target",
"params": {
"target_class": "truck",
"description": "军事卡车",
"count": 1
}
},
{
"type": "action",
"name": "battle_damage_assessment",
"params": {
"target_class": "truck",
"assessment_time": 20.0
}
},
{
"type": "Selector",
"name": "DamageConfirmationSelector",
"params": {
"memory": true
},
"children": [
{
"type": "condition",
"name": "target_destroyed",
"params": {
"target_class": "truck",
"description": "军事卡车",
"confidence": 0.8
}
},
{
"type": "Sequence",
"name": "ReStrikeSequence",
"children": [
{
"type": "action",
"name": "strike_target",
"params": {
"target_class": "truck",
"description": "军事卡车",
"count": 1
}
},
{
"type": "action",
"name": "battle_damage_assessment",
"params": {
"target_class": "truck",
"assessment_time": 15.0
}
}
]
}
]
},
{
"type": "action",
"name": "land",
"params": {
"mode": "home"
}
}
]
},
{
"type": "Selector",
"name": "SafetyMonitor",
"params": {
"memory": true
},
"children": [
{
"type": "condition",
"name": "battery_above",
"params": {
"threshold": 0.35
}
},
{
"type": "condition",
"name": "gps_status",
"params": {
"min_satellites": 8
}
},
{
"type": "Sequence",
"name": "EmergencyHandler",
"children": [
{
"type": "action",
"name": "emergency_return",
"params": {
"reason": "safety_breach"
}
},
{
"type": "action",
"name": "land",
"params": {
"mode": "home"
}
}
]
}
]
}
]
}
}
#### 9. 跟踪任务范式
所有任务必须包含安全监控。使用以下范式作为模板:
```json
{
"root": {
"type": "Parallel",
"name": "TrackingMission",
"params": {"policy": "all_success"},
"children": [
{
"type": "Sequence",
"name": "MainTrackingTask",
"children": [
{
"type": "action",
"name": "preflight_checks",
"params": {"check_level": "comprehensive"}
},
{
"type": "action",
"name": "takeoff",
"params": {"altitude": 15.0}
},
{
"type": "action",
"name": "fly_to_waypoint",
"params": {
"x": 100.0,
"y": 80.0,
"z": 15.0,
"acceptance_radius": 3.0
}
},
{
"type": "Selector",
"name": "TargetAcquisitionSelector",
"params": {"memory": true},
"children": [
{
"type": "Sequence",
"name": "DirectDetectionSequence",
"children": [
{
"type": "action",
"name": "loiter",
"params": {"duration": 10.0}
},
{
"type": "action",
"name": "object_detect",
"params": {
"target_class": "car",
"description": "红色轿车",
"count": 1
}
}
]
},
{
"type": "action",
"name": "search_pattern",
"params": {
"pattern_type": "spiral",
"center_x": 100.0,
"center_y": 80.0,
"center_z": 15.0,
"radius": 50.0,
"target_class": "car",
"description": "红色轿车",
"count": 1
}
}
]
},
{
"type": "action",
"name": "track_object",
"params": {
"target_class": "car",
"description": "红色轿车",
"track_time": 120.0,
"min_confidence": 0.7,
"safe_distance": 15.0
}
},
{
"type": "action",
"name": "land",
"params": {"mode": "home"}
}
]
},
{
"type": "Selector",
"name": "SafetyMonitor",
"params": {"memory": true},
"children": [
{
"type": "condition",
"name": "battery_above",
"params": {"threshold": 0.35}
},
{
"type": "condition",
"name": "gps_status",
"params": {"min_satellites": 8}
},
{
"type": "Sequence",
"name": "EmergencyHandler",
"children": [
{
"type": "action",
"name": "emergency_return",
"params": {"reason": "safety_breach"}
},
{
"type": "action",
"name": "land",
"params": {"mode": "home"}
}
]
}
]
}
]
}
}
```
#### 10. 如何使用参考知识
当用户提供"参考知识"(如坐标信息)时,你必须使用这些信息填充参数。例如:
- 如果参考知识说"目标坐标: (x: 120.5, y: 80.2, z: 60.0)",则在使用`fly_to_waypoint`时设置这些值。
- 确保坐标符合约束如z≥1
环绕口令到参数的映射规则(当口令涉及“环绕/绕圈”等):
- “环绕XY圈” → `radius=X`, `laps=Y`,默认 `clockwise=true`, `gimbal_lock=true`比如环绕三十两圈意思就是以目标点为圆心30米为半径绕2圈
- 明确“顺时针/逆时针”时 → 设置 `clockwise=true/false`
- 出现“等速”时 → 若未给速度则 `speed_mps` 使用默认值如3.0);若口令给出速度,裁剪到[0.5,15]
- “以(中心坐标)为中心/当前位置为中心” → 使用 `orbit_around_point` 并填写 `center_x/center_y/center_z`
- “以目标为中心/围绕目标” → 使用 `orbit_around_target`;若任务未提供目标来源,则需要在主任务中先行确认目标(通过检测/跟踪或参考知识)
#### 11. 输出要求
你的输出必须是严格的、单一的JSON对象符合上述所有规则。不包含任何自然语言描述。

View File

@@ -619,6 +619,8 @@ class PyTreeGenerator:
return None return None
context_str = "\n\n".join(retrieved_docs) context_str = "\n\n".join(retrieved_docs)
logging.info("--- 成功检索到上下文信息 ---") logging.info("--- 成功检索到上下文信息 ---")
# 打印检索到的上下文内容
logging.info(f"📚 检索到的上下文内容:\n{context_str}")
return context_str return context_str
except Exception as e: except Exception as e:
logging.error(f"从向量数据库检索时发生错误: {e}") logging.error(f"从向量数据库检索时发生错误: {e}")
@@ -640,8 +642,11 @@ class PyTreeGenerator:
{"role": "user", "content": user_prompt} {"role": "user", "content": user_prompt}
], ],
temperature=0.0, temperature=0.0,
response_format={"type": "json_object"}, response_format={"type": "json_object"}, # 强制JSON输出禁用思考功能
max_tokens=self.classifier_max_tokens max_tokens=self.classifier_max_tokens,
# 禁用 Qwen3 模型的思考功能(通过 extra_body 传递)
# 注意:如果 API 服务器不支持此参数,会忽略
extra_body={"chat_template_kwargs": {"enable_thinking": False}}
) )
class_str = classifier_resp.choices[0].message.content class_str = classifier_resp.choices[0].message.content
class_obj = json.loads(class_str) class_obj = json.loads(class_str)
@@ -670,13 +675,19 @@ class PyTreeGenerator:
final_user_prompt += augmentation final_user_prompt += augmentation
else: else:
logging.warning("未检索到上下文或检索失败,将使用原始用户提示词。") logging.warning("未检索到上下文或检索失败,将使用原始用户提示词。")
# 构建完整的 final_prompt准确反映实际发送给大模型的内容结构
# 注意RAG检索结果被添加到 user prompt 中,而不是 system prompt
# System Prompt: use_prompt不包含RAG结果
# User Prompt: final_user_prompt包含原始user_prompt + RAG检索结果
final_prompt = f"=== System Prompt ===\n{use_prompt}\n\n=== User Prompt ===\n{final_user_prompt}"
for attempt in range(3): for attempt in range(3):
logging.info(f"--- 第 {attempt + 1}/3 次尝试生成Pytree ---") logging.info(f"--- 第 {attempt + 1}/3 次尝试生成Pytree ---")
try: try:
# 简单/复杂分流到不同模型与提示词 # 简单/复杂分流到不同模型与提示词
client = self.simple_llm_client if mode == "simple" else self.complex_llm_client client = self.simple_llm_client if mode == "simple" else self.complex_llm_client
model_name = self.simple_model if mode == "simple" else self.complex_model model_name = self.simple_model if mode == "simple" else self.complex_model
# 根据是否捕获推理链来决定是否强制JSON响应 # 始终强制JSON响应并禁用思考功能
response_kwargs = { response_kwargs = {
"model": model_name, "model": model_name,
"messages": [ "messages": [
@@ -684,9 +695,11 @@ class PyTreeGenerator:
{"role": "user", "content": final_user_prompt} {"role": "user", "content": final_user_prompt}
], ],
"temperature": 0.1 if mode == "complex" else 0.0, "temperature": 0.1 if mode == "complex" else 0.0,
"response_format": {"type": "json_object"}, # 始终强制JSON输出禁用思考功能
# 禁用 Qwen3 模型的思考功能(通过 extra_body 传递)
# 注意:如果 API 服务器不支持此参数,会忽略
"extra_body": {"chat_template_kwargs": {"enable_thinking": False}}
} }
if not self.enable_reasoning_capture:
response_kwargs["response_format"] = {"type": "json_object"}
# 基于模式设定最大输出token数直接在代码中配置 # 基于模式设定最大输出token数直接在代码中配置
response_kwargs["max_tokens"] = self.simple_max_tokens if mode == "simple" else self.complex_max_tokens response_kwargs["max_tokens"] = self.simple_max_tokens if mode == "simple" else self.complex_max_tokens
response = client.chat.completions.create(**response_kwargs) response = client.chat.completions.create(**response_kwargs)
@@ -803,6 +816,8 @@ class PyTreeGenerator:
logging.info("未在模型输出中发现 <think> 推理链片段。若需捕获,请设置 ENABLE_REASONING_CAPTURE=true 以放宽JSON强制格式。") logging.info("未在模型输出中发现 <think> 推理链片段。若需捕获,请设置 ENABLE_REASONING_CAPTURE=true 以放宽JSON强制格式。")
except Exception as e: except Exception as e:
logging.warning(f"保存推理链Markdown失败: {e}") logging.warning(f"保存推理链Markdown失败: {e}")
# 添加 final_prompt 到返回结果
pytree_dict['final_prompt'] = final_prompt
return pytree_dict return pytree_dict
# 复杂模式回退:若模型误返回简单结构,则自动包装为含安全监控的行为树 # 复杂模式回退:若模型误返回简单结构,则自动包装为含安全监控的行为树
@@ -874,6 +889,8 @@ class PyTreeGenerator:
logging.info("未在模型输出中发现 <think> 推理链片段。若需捕获,请设置 ENABLE_REASONING_CAPTURE=true 以放宽JSON强制格式。") logging.info("未在模型输出中发现 <think> 推理链片段。若需捕获,请设置 ENABLE_REASONING_CAPTURE=true 以放宽JSON强制格式。")
except Exception as e: except Exception as e:
logging.warning(f"保存推理链Markdown失败: {e}") logging.warning(f"保存推理链Markdown失败: {e}")
# 添加 final_prompt 到返回结果
pytree_dict['final_prompt'] = final_prompt
return pytree_dict return pytree_dict
else: else:
# 打印未通过验证的Pytree以便排查 # 打印未通过验证的Pytree以便排查

View File

@@ -1,67 +0,0 @@
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
import json
from typing import Dict, Any
import logging
from drone_interfaces.action import ExecuteMission
from .websocket_manager import websocket_manager
class MissionActionClient(Node):
"""
Interfaces with the drone's `ExecuteMission` ROS2 Action Server.
"""
def __init__(self):
super().__init__('mission_action_client')
self._action_client = ActionClient(self, ExecuteMission, 'execute_mission')
self.get_logger().info("MissionActionClient initialized.")
def send_goal(self, py_tree: Dict[str, Any]):
"""
Sends the mission (py_tree) to the action server.
"""
if not self._action_client.server_is_ready():
self.get_logger().error("Action server not available, goal not sent.")
# Optionally, you could broadcast a status update to the frontend here
return
self.get_logger().info("Received request to send goal to drone.")
goal_msg = ExecuteMission.Goal()
goal_msg.py_tree_json = json.dumps(py_tree)
self.get_logger().info(f"Sending goal to action server...")
send_goal_future = self._action_client.send_goal_async(
goal_msg,
feedback_callback=self.feedback_callback
)
send_goal_future.add_done_callback(self.goal_response_callback)
def goal_response_callback(self, future):
goal_handle = future.result()
if not goal_handle.accepted:
self.get_logger().info('Goal rejected :(')
return
self.get_logger().info('Goal accepted :)')
self._get_result_future = goal_handle.get_result_async()
self._get_result_future.add_done_callback(self.get_result_callback)
def get_result_callback(self, future):
result = future.result().result
self.get_logger().info(f'Result: {{success: {result.success}, message: {result.message}}}')
# Optionally, you can broadcast the final result via WebSocket here
def feedback_callback(self, feedback_msg):
"""
This callback is triggered by the action server.
It forwards the status to the QGC plugin via the WebSocket manager in a thread-safe manner.
"""
feedback = feedback_msg.feedback
feedback_payload = json.dumps({"node_id": feedback.node_id, "status": feedback.status})
self.get_logger().info(f"Received feedback: {feedback_payload}")
websocket_manager.broadcast(feedback_payload)
# Note: The rclpy.init() and spinning of the node will be handled in main.py

View File

@@ -22,7 +22,8 @@ class ConnectionManager:
def broadcast(self, message: str): def broadcast(self, message: str):
""" """
Thread-safely broadcasts a message to all active WebSocket connections. Thread-safely broadcasts a message to all active WebSocket connections.
This method is designed to be called from a different thread (e.g., a ROS2 callback). This method is designed to be called from a different thread.
(Note: ROS2 callback support has been removed as the project is decoupled from ROS2)
""" """
if not self.loop: if not self.loop:
logging.error("Event loop not set in ConnectionManager. Cannot broadcast.") logging.error("Event loop not set in ConnectionManager. Cannot broadcast.")

406
start_all.sh Executable file
View File

@@ -0,0 +1,406 @@
#!/bin/bash
# ==============================================================================
# 无人机自然语言控制项目 - 一键启动脚本
# ==============================================================================
# 功能启动所有必需的服务llama-server推理模型、embedding模型、FastAPI后端
# 用法:./start_all.sh [选项]
# ==============================================================================
set -e # 遇到错误立即退出
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 默认配置(可通过环境变量覆盖)
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LLAMA_SERVER_DIR="${LLAMA_SERVER_DIR:-~/llama.cpp/build/bin}"
INFERENCE_MODEL="${INFERENCE_MODEL:-~/models/gguf/Qwen/Qwen3-4B/Qwen3-4B-Q5_K_M.gguf}"
EMBEDDING_MODEL="${EMBEDDING_MODEL:-~/models/gguf/Qwen/Qwen3-Embedding-4B/Qwen3-Embedding-4B-Q4_K_M.gguf}"
VENV_PATH="${VENV_PATH:-${PROJECT_ROOT}/backend_service/venv}"
LOG_DIR="${PROJECT_ROOT}/logs"
PID_FILE="${LOG_DIR}/services.pid"
# 端口配置
INFERENCE_PORT=8081
EMBEDDING_PORT=8090
API_PORT=8000
# 创建日志目录
mkdir -p "${LOG_DIR}"
# ==============================================================================
# 辅助函数
# ==============================================================================
print_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 检查命令是否存在
check_command() {
if ! command -v "$1" &> /dev/null; then
print_error "$1 命令未找到,请先安装"
return 1
fi
return 0
}
# 检查端口是否被占用
check_port() {
local port=$1
if lsof -Pi :${port} -sTCP:LISTEN -t >/dev/null 2>&1 ; then
return 0 # 端口被占用
else
return 1 # 端口空闲
fi
}
# 等待服务就绪
wait_for_service() {
local url=$1
local service_name=$2
local max_attempts=30
local attempt=0
print_info "等待 ${service_name} 启动..."
while [ $attempt -lt $max_attempts ]; do
if curl -s "${url}" > /dev/null 2>&1; then
print_success "${service_name} 已就绪"
return 0
fi
attempt=$((attempt + 1))
sleep 1
done
print_error "${service_name} 启动超时"
return 1
}
# 停止所有服务
stop_services() {
print_info "正在停止所有服务..."
if [ -f "${PID_FILE}" ]; then
while read pid; do
if ps -p $pid > /dev/null 2>&1; then
print_info "停止进程 PID: $pid"
kill $pid 2>/dev/null || true
fi
done < "${PID_FILE}"
rm -f "${PID_FILE}"
fi
# 尝试通过端口停止服务
for port in ${INFERENCE_PORT} ${EMBEDDING_PORT} ${API_PORT}; do
if check_port ${port}; then
local pid=$(lsof -ti:${port})
if [ ! -z "$pid" ]; then
print_info "停止占用端口 ${port} 的进程 (PID: $pid)"
kill $pid 2>/dev/null || true
fi
fi
done
print_success "所有服务已停止"
}
# 清理函数(脚本退出时调用)
cleanup() {
if [ "$?" -ne 0 ]; then
print_error "启动过程中发生错误,正在清理..."
fi
# 注意:这里不自动停止服务,让用户手动控制
}
trap cleanup EXIT
# ==============================================================================
# 主函数
# ==============================================================================
start_services() {
print_info "=========================================="
print_info " 无人机自然语言控制项目 - 服务启动"
print_info "=========================================="
echo ""
# 检查必要的命令
print_info "检查必要的命令..."
check_command "python3" || exit 1
check_command "curl" || exit 1
check_command "lsof" || print_warning "lsof 未安装,将无法检查端口占用"
echo ""
# 检查端口占用
print_info "检查端口占用..."
if check_port ${INFERENCE_PORT}; then
print_warning "端口 ${INFERENCE_PORT} 已被占用,推理模型可能已在运行"
fi
if check_port ${EMBEDDING_PORT}; then
print_warning "端口 ${EMBEDDING_PORT} 已被占用Embedding模型可能已在运行"
fi
if check_port ${API_PORT}; then
print_error "端口 ${API_PORT} 已被占用,请先停止占用该端口的服务"
exit 1
fi
echo ""
# 检查llama-server展开路径中的 ~
local llama_server_dir_expanded=$(eval echo "${LLAMA_SERVER_DIR}")
local llama_server="${llama_server_dir_expanded}/llama-server"
if [ ! -f "${llama_server}" ]; then
print_error "llama-server 未找到: ${llama_server}"
print_info "请设置 LLAMA_SERVER_DIR 环境变量指向正确的路径"
print_info "当前路径: ${LLAMA_SERVER_DIR}"
print_info "展开后路径: ${llama_server_dir_expanded}"
exit 1
fi
print_success "找到 llama-server: ${llama_server}"
echo ""
# 检查模型文件
local inference_model_expanded=$(eval echo "${INFERENCE_MODEL}")
local embedding_model_expanded=$(eval echo "${EMBEDDING_MODEL}")
if [ ! -f "${inference_model_expanded}" ]; then
print_error "推理模型文件未找到: ${inference_model_expanded}"
print_info "请设置 INFERENCE_MODEL 环境变量指向正确的模型路径"
exit 1
fi
print_success "找到推理模型: ${inference_model_expanded}"
if [ ! -f "${embedding_model_expanded}" ]; then
print_error "Embedding模型文件未找到: ${embedding_model_expanded}"
print_info "请设置 EMBEDDING_MODEL 环境变量指向正确的模型路径"
exit 1
fi
print_success "找到Embedding模型: ${embedding_model_expanded}"
echo ""
# 检查ROS2环境
local ros2_setup="${PROJECT_ROOT}/install/setup.bash"
if [ ! -f "${ros2_setup}" ]; then
print_warning "ROS2 setup文件未找到: ${ros2_setup}"
print_warning "如果项目已与ROS2解耦可以忽略此警告"
else
print_success "找到ROS2 setup文件: ${ros2_setup}"
fi
echo ""
# 检查venv虚拟环境
local venv_path_expanded=$(eval echo "${VENV_PATH}")
print_info "检查venv虚拟环境: ${venv_path_expanded}"
if [ ! -d "${venv_path_expanded}" ]; then
print_error "venv虚拟环境目录不存在: ${venv_path_expanded}"
print_info "请先创建venv环境: python3 -m venv ${venv_path_expanded}"
print_info "然后安装依赖: ${venv_path_expanded}/bin/pip install -r backend_service/requirements.txt"
exit 1
fi
if [ ! -f "${venv_path_expanded}/bin/activate" ]; then
print_error "venv激活脚本不存在: ${venv_path_expanded}/bin/activate"
print_error "这看起来不是一个有效的venv环境"
exit 1
fi
print_success "venv虚拟环境存在: ${venv_path_expanded}"
echo ""
# 初始化PID文件
> "${PID_FILE}"
# ==========================================================================
# 启动推理模型服务
# ==========================================================================
print_info "启动推理模型服务 (端口 ${INFERENCE_PORT})..."
cd "${llama_server_dir_expanded}"
nohup ./llama-server \
-m "${inference_model_expanded}" \
--port ${INFERENCE_PORT} \
--gpu-layers 36 \
--host 0.0.0.0 \
-c 8192 \
> "${LOG_DIR}/inference_model.log" 2>&1 &
local inference_pid=$!
echo $inference_pid >> "${PID_FILE}"
print_success "推理模型服务已启动 (PID: $inference_pid)"
print_info "日志文件: ${LOG_DIR}/inference_model.log"
echo ""
# ==========================================================================
# 启动Embedding模型服务
# ==========================================================================
print_info "启动Embedding模型服务 (端口 ${EMBEDDING_PORT})..."
nohup ./llama-server \
-m "${embedding_model_expanded}" \
--gpu-layers 36 \
--port ${EMBEDDING_PORT} \
--embeddings \
--pooling last \
--host 0.0.0.0 \
> "${LOG_DIR}/embedding_model.log" 2>&1 &
local embedding_pid=$!
echo $embedding_pid >> "${PID_FILE}"
print_success "Embedding模型服务已启动 (PID: $embedding_pid)"
print_info "日志文件: ${LOG_DIR}/embedding_model.log"
echo ""
# ==========================================================================
# 等待模型服务就绪
# ==========================================================================
print_info "等待模型服务就绪..."
sleep 3 # 给服务一些启动时间
# 等待推理模型服务
if ! wait_for_service "http://localhost:${INFERENCE_PORT}/health" "推理模型服务"; then
# 如果health端点不存在尝试检查根路径
if ! wait_for_service "http://localhost:${INFERENCE_PORT}/v1/models" "推理模型服务"; then
print_warning "推理模型服务可能未完全就绪,但将继续启动"
fi
fi
# 等待Embedding模型服务
if ! wait_for_service "http://localhost:${EMBEDDING_PORT}/health" "Embedding模型服务"; then
if ! wait_for_service "http://localhost:${EMBEDDING_PORT}/v1/models" "Embedding模型服务"; then
print_warning "Embedding模型服务可能未完全就绪但将继续启动"
fi
fi
echo ""
# ==========================================================================
# 启动FastAPI后端服务
# ==========================================================================
print_info "启动FastAPI后端服务 (端口 ${API_PORT})..."
cd "${PROJECT_ROOT}"
# 激活venv虚拟环境并启动FastAPI服务
# 使用bash -c来在新的shell中激活venv环境
bash -c "
# 激活ROS2环境如果存在
if [ -f '${ros2_setup}' ]; then
source '${ros2_setup}'
fi
# 激活venv虚拟环境
source '${venv_path_expanded}/bin/activate' && \
cd '${PROJECT_ROOT}/backend_service' && \
uvicorn src.main:app --host 0.0.0.0 --port ${API_PORT}
" > "${LOG_DIR}/fastapi.log" 2>&1 &
local api_pid=$!
echo $api_pid >> "${PID_FILE}"
print_success "FastAPI服务已启动 (PID: $api_pid)"
print_info "日志文件: ${LOG_DIR}/fastapi.log"
echo ""
# 等待FastAPI服务就绪
sleep 3
if wait_for_service "http://localhost:${API_PORT}/docs" "FastAPI服务"; then
print_success "所有服务已成功启动!"
else
print_warning "FastAPI服务可能未完全就绪请检查日志: ${LOG_DIR}/fastapi.log"
fi
echo ""
# 显示服务访问信息
print_info "=========================================="
print_info " 服务启动完成!"
print_info "=========================================="
print_info "推理模型API: http://localhost:${INFERENCE_PORT}/v1"
print_info "Embedding模型API: http://localhost:${EMBEDDING_PORT}/v1"
print_info "FastAPI后端: http://localhost:${API_PORT}"
print_info "API文档: http://localhost:${API_PORT}/docs"
print_info ""
print_info "日志文件位置:"
print_info " - 推理模型: ${LOG_DIR}/inference_model.log"
print_info " - Embedding模型: ${LOG_DIR}/embedding_model.log"
print_info " - FastAPI服务: ${LOG_DIR}/fastapi.log"
print_info ""
print_info "按 Ctrl+C 停止所有服务"
print_info "=========================================="
echo ""
# 设置信号处理确保Ctrl+C时能清理
trap 'print_info "\n正在停止服务..."; stop_services; exit 0' INT TERM
# 等待所有后台进程(保持脚本运行)
print_info "所有服务正在运行中,查看日志请使用:"
print_info " tail -f ${LOG_DIR}/*.log"
echo ""
# 等待所有后台进程
wait
}
# ==============================================================================
# 脚本入口
# ==============================================================================
case "${1:-start}" in
start)
start_services
;;
stop)
stop_services
;;
restart)
stop_services
sleep 2
start_services
;;
status)
print_info "检查服务状态..."
if [ -f "${PID_FILE}" ]; then
print_info "已记录的服务进程:"
while read pid; do
if ps -p $pid > /dev/null 2>&1; then
print_success "PID $pid: 运行中"
else
print_warning "PID $pid: 已停止"
fi
done < "${PID_FILE}"
else
print_info "未找到PID文件服务可能未启动"
fi
echo ""
print_info "端口占用情况:"
for port in ${INFERENCE_PORT} ${EMBEDDING_PORT} ${API_PORT}; do
if check_port ${port}; then
local pid=$(lsof -ti:${port})
print_success "端口 ${port}: 被占用 (PID: $pid)"
else
print_warning "端口 ${port}: 空闲"
fi
done
;;
*)
echo "用法: $0 {start|stop|restart|status}"
echo ""
echo "命令说明:"
echo " start - 启动所有服务(默认)"
echo " stop - 停止所有服务"
echo " restart - 重启所有服务"
echo " status - 查看服务状态"
echo ""
echo "环境变量配置:"
echo " LLAMA_SERVER_DIR - llama-server所在目录 (默认: ~/llama.cpp/build/bin)"
echo " INFERENCE_MODEL - 推理模型路径 (默认: ~/models/gguf/Qwen/Qwen3-4B/Qwen3-4B-Q5_K_M.gguf)"
echo " EMBEDDING_MODEL - Embedding模型路径 (默认: ~/models/gguf/Qwen/Qwen3-Embedding-4B/Qwen3-Embedding-4B-Q4_K_M.gguf)"
echo " VENV_PATH - venv虚拟环境路径 (默认: \${PROJECT_ROOT}/backend_service/venv)"
exit 1
;;
esac

286
tools/api_test.log Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,149 +0,0 @@
{
"plan_id": "9f743b03-8ba7-4a06-9260-337463887fc2",
"root": {
"children": [
{
"children": [
{
"name": "preflight_checks",
"params": {
"check_level": "comprehensive"
},
"type": "action"
},
{
"name": "takeoff",
"params": {
"altitude": 2
},
"type": "action"
},
{
"name": "fly_to_waypoint",
"params": {
"acceptance_radius": 2,
"x": 5,
"y": 3,
"z": 2
},
"type": "action"
},
{
"children": [
{
"children": [
{
"name": "loiter",
"params": {
"duration": 10
},
"type": "action"
},
{
"name": "object_detect",
"params": {
"count": 1,
"description": "学生",
"target_class": "person"
},
"type": "action"
}
],
"name": "DirectDetectionSequence",
"type": "Sequence"
},
{
"name": "search_pattern",
"params": {
"center_x": 5,
"center_y": 3,
"center_z": 2,
"count": 1,
"description": "学生",
"pattern_type": "spiral",
"radius": 50,
"target_class": "person"
},
"type": "action"
}
],
"name": "TargetAcquisitionSelector",
"params": {
"memory": true
},
"type": "Selector"
},
{
"name": "track_object",
"params": {
"description": "学生",
"min_confidence": 0.7,
"safe_distance": 10,
"target_class": "person",
"track_time": 20
},
"type": "action"
},
{
"name": "land",
"params": {
"mode": "home"
},
"type": "action"
}
],
"name": "MainTrackingTask",
"type": "Sequence"
},
{
"children": [
{
"name": "battery_above",
"params": {
"threshold": 0.35
},
"type": "condition"
},
{
"name": "gps_status",
"params": {
"min_satellites": 8
},
"type": "condition"
},
{
"children": [
{
"name": "emergency_return",
"params": {
"reason": "safety_breach"
},
"type": "action"
},
{
"name": "land",
"params": {
"mode": "home"
},
"type": "action"
}
],
"name": "EmergencyHandler",
"type": "Sequence"
}
],
"name": "SafetyMonitor",
"params": {
"memory": true
},
"type": "Selector"
}
],
"name": "TrackingMission",
"params": {
"policy": "all_success"
},
"type": "Parallel"
},
"visualization_url": "/static/py_tree.png"
}

View File

@@ -3,6 +3,8 @@
import requests import requests
import json import json
import os
from datetime import datetime
# --- Configuration --- # --- Configuration ---
# The base URL of your running FastAPI service # The base URL of your running FastAPI service
@@ -14,17 +16,49 @@ ENDPOINT = "/generate_plan"
# The user prompt we will send for the test # The user prompt we will send for the test
TEST_PROMPT = "已知目标检测红色气球危险性高于蓝色气球高于绿色气球飞往搜索区搜索并锁定危险性最高的气球对其跟踪30秒后进行打击操作" TEST_PROMPT = "已知目标检测红色气球危险性高于蓝色气球高于绿色气球飞往搜索区搜索并锁定危险性最高的气球对其跟踪30秒后进行打击操作"
# Log file path (will be created in the same directory as this script)
LOG_FILE = os.path.join(os.path.dirname(__file__), "api_test.log")
def write_log(message, print_to_console=True):
"""
Write a message to the log file in append mode.
Supports multi-line messages - only the first line gets timestamp.
Args:
message: The message to write (can be multi-line)
print_to_console: Whether to also print to console (default: True)
"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Split message into lines and add timestamp to first line only
lines = message.split('\n')
log_entries = [f"[{timestamp}] {lines[0]}\n"]
for line in lines[1:]:
log_entries.append(f"{' ' * (len(timestamp) + 3)}{line}\n") # Indent continuation lines
try:
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.writelines(log_entries)
except Exception as e:
print(f"⚠️ Warning: Failed to write to log file: {e}")
if print_to_console:
print(message)
def test_generate_plan(): def test_generate_plan():
""" """
Sends a request to the /generate_plan endpoint and validates the response. Sends a request to the /generate_plan endpoint and validates the response.
All results are logged to the log file for continuous tracking.
""" """
url = BASE_URL + ENDPOINT url = BASE_URL + ENDPOINT
payload = {"user_prompt": TEST_PROMPT} payload = {"user_prompt": TEST_PROMPT}
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
print("--- API Test: Generate Plan ---") # Write separator and test start info to log
print(f"✅ URL: {url}") write_log("=" * 80, print_to_console=False)
print(f"✅ Sending Prompt: \"{TEST_PROMPT}\"") write_log("--- API Test: Generate Plan ---")
write_log(f"URL: {url}")
write_log(f"Sending Prompt: \"{TEST_PROMPT}\"")
try: try:
# Send the POST request # Send the POST request
@@ -36,40 +70,85 @@ def test_generate_plan():
# Parse the JSON response # Parse the JSON response
data = response.json() data = response.json()
print("✅ Received Response:") # Extract and log organized prompt if available in response
print(json.dumps(data, indent=2, ensure_ascii=False)) organized_prompt = None
if isinstance(data, dict):
# Check for various possible field names for organized prompt
organized_prompt = data.get("organized_prompt") or \
data.get("processed_prompt") or \
data.get("final_prompt") or \
data.get("enhanced_prompt") or \
data.get("user_prompt_enhanced")
write_log("✅ Received Response:")
# Log organized prompt if found
if organized_prompt:
write_log("\n📝 组织后的Prompt:")
write_log(organized_prompt)
else:
# If not in response, log the original prompt for reference
write_log("\n📝 原始Prompt:")
write_log(f" {TEST_PROMPT}")
write_log(" (注: 组织后的prompt未在API响应中返回如需查看请检查后端日志)")
response_json = json.dumps(data, indent=2, ensure_ascii=False)
write_log("\n完整响应内容:")
write_log(response_json)
# --- Validation --- # --- Validation ---
print("\n--- Validation Checks ---") write_log("\n--- Validation Checks ---")
validation_results = []
# 1. Check if the response is a dictionary # 1. Check if the response is a dictionary
if isinstance(data, dict): if isinstance(data, dict):
print("PASS: Response is a valid JSON object.") validation_results.append("PASS: Response is a valid JSON object.")
else: else:
print("FAIL: Response is not a valid JSON object.") validation_results.append("FAIL: Response is not a valid JSON object.")
# Write all validation results to log before returning
for result in validation_results:
write_log(result)
write_log("=" * 80, print_to_console=False)
write_log("", print_to_console=False) # Empty line for readability
return return
# 2. Check for the existence of the 'root' key # 2. Check for the existence of the 'root' key
if "root" in data and isinstance(data['root'], dict): if "root" in data and isinstance(data['root'], dict):
print("PASS: Response contains a valid 'root' key.") validation_results.append("PASS: Response contains a valid 'root' key.")
else: else:
print("FAIL: Response does not contain a valid 'root' key.") validation_results.append("FAIL: Response does not contain a valid 'root' key.")
# 3. Check for the existence and format of the 'visualization_url' key # 3. Check for the existence and format of the 'visualization_url' key
if "visualization_url" in data and data["visualization_url"].endswith(".png"): if "visualization_url" in data and data["visualization_url"].endswith(".png"):
print(f"PASS: Response contains a valid 'visualization_url': {data['visualization_url']}") validation_results.append(f"PASS: Response contains a valid 'visualization_url': {data['visualization_url']}")
else: else:
print("FAIL: Response does not contain a valid 'visualization_url'.") validation_results.append("FAIL: Response does not contain a valid 'visualization_url'.")
# Write all validation results to log
for result in validation_results:
write_log(result)
# Write test completion marker
write_log("✅ Test completed successfully")
write_log("=" * 80, print_to_console=False)
write_log("", print_to_console=False) # Empty line for readability
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
print(f"\n❌ TEST FAILED: Could not connect to the server.") error_msg = f"❌ TEST FAILED: Could not connect to the server.\n Please make sure the backend service is running.\n Error details: {e}"
print(" Please make sure the backend service is running.") write_log(error_msg)
print(f" Error details: {e}") write_log("=" * 80, print_to_console=False)
write_log("", print_to_console=False) # Empty line for readability
except json.JSONDecodeError: except json.JSONDecodeError:
print(f"\n❌ TEST FAILED: The server response was not valid JSON.") error_msg = f"❌ TEST FAILED: The server response was not valid JSON.\n Response text: {response.text}"
print(f" Response text: {response.text}") write_log(error_msg)
write_log("=" * 80, print_to_console=False)
write_log("", print_to_console=False) # Empty line for readability
except Exception as e: except Exception as e:
print(f"\n❌ TEST FAILED: An unexpected error occurred: {e}") error_msg = f"❌ TEST FAILED: An unexpected error occurred: {e}"
write_log(error_msg)
write_log("=" * 80, print_to_console=False)
write_log("", print_to_console=False) # Empty line for readability
if __name__ == "__main__": if __name__ == "__main__":
test_generate_plan() test_generate_plan()

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,12 @@
instruction_index,instruction,run_number,success,attempts,response_time,plan_id,error,timestamp instruction_index,instruction,run_number,success,attempts,response_time,plan_id,error,timestamp
1,起飞后移动到学生宿舍上方降落,1,False,1,2.048215866088867,,,2025-12-02 20:44:56
2,起飞后移动到学生宿舍上方查找蓝色的车,1,True,1,14.806509971618652,8a6f282e-c306-4249-962c-d47d48c31bad,,2025-12-02 20:45:12
3,起飞后移动到学生宿舍上方寻找蓝色的车,1,True,1,15.240672826766968,f298e2f4-9295-4ffd-8fff-0d0eb9a0ee6c,,2025-12-02 20:45:28
4,起飞后移动到学生宿舍上方检测蓝色的车,1,True,1,13.8105788230896,31733491-2030-43b1-a5e4-eb1300b8d23f,,2025-12-02 20:45:43
5,飞到学生宿舍上方查找蓝色的车,1,True,1,12.74257755279541,4c855ef4-c251-48cd-b464-4816bc62fbb5,,2025-12-02 20:45:57
6,飞到学生宿舍上方查找蓝色车辆并进行打击,1,True,1,16.117226600646973,63d0e7c3-dcbb-40f0-b76b-6f0191c6512f,,2025-12-02 20:46:14
7,起飞后移动到学生宿舍上方搜索蓝色车辆,并进行打击,1,True,1,16.25989079475403,1b4a537e-c1be-4abf-897e-c21b677b83b7,,2025-12-02 20:46:31
8,起飞到学生宿舍上方搜索被困人员,并为被困人员投递救援物资,1,True,1,16.014280796051025,f88ea46f-5e0b-48fb-b1da-326d287af3d6,,2025-12-02 20:46:48
9,飞到学生宿舍上方搜索方圆10米范围内的蓝色车辆,1,True,1,15.530286073684692,f56c811a-8304-4c68-8260-01643928bf3e,,2025-12-02 20:47:05
10,飞到学生宿舍上方搜索半径为10米区域范围内的蓝色车辆,1,True,1,16.660754919052124,07a13346-3026-4dce-a976-4e0faa132248,,2025-12-02 20:47:23
11,起飞到学生宿舍搜索有没有被困人员,然后抛洒救援物资,1,True,1,14.128317832946777,16426d41-4f02-4e27-a05e-f4eb84d6c935,,2025-12-02 20:47:38
1 instruction_index instruction run_number success attempts response_time plan_id error timestamp
2 1 起飞后移动到学生宿舍上方降落 1 False 1 2.048215866088867 2025-12-02 20:44:56
3 2 起飞后移动到学生宿舍上方查找蓝色的车 1 True 1 14.806509971618652 8a6f282e-c306-4249-962c-d47d48c31bad 2025-12-02 20:45:12
4 3 起飞后移动到学生宿舍上方寻找蓝色的车 1 True 1 15.240672826766968 f298e2f4-9295-4ffd-8fff-0d0eb9a0ee6c 2025-12-02 20:45:28
5 4 起飞后移动到学生宿舍上方检测蓝色的车 1 True 1 13.8105788230896 31733491-2030-43b1-a5e4-eb1300b8d23f 2025-12-02 20:45:43
6 5 飞到学生宿舍上方查找蓝色的车 1 True 1 12.74257755279541 4c855ef4-c251-48cd-b464-4816bc62fbb5 2025-12-02 20:45:57
7 6 飞到学生宿舍上方查找蓝色车辆并进行打击 1 True 1 16.117226600646973 63d0e7c3-dcbb-40f0-b76b-6f0191c6512f 2025-12-02 20:46:14
8 7 起飞后移动到学生宿舍上方搜索蓝色车辆,并进行打击 1 True 1 16.25989079475403 1b4a537e-c1be-4abf-897e-c21b677b83b7 2025-12-02 20:46:31
9 8 起飞到学生宿舍上方搜索被困人员,并为被困人员投递救援物资 1 True 1 16.014280796051025 f88ea46f-5e0b-48fb-b1da-326d287af3d6 2025-12-02 20:46:48
10 9 飞到学生宿舍上方搜索方圆10米范围内的蓝色车辆 1 True 1 15.530286073684692 f56c811a-8304-4c68-8260-01643928bf3e 2025-12-02 20:47:05
11 10 飞到学生宿舍上方搜索半径为10米区域范围内的蓝色车辆 1 True 1 16.660754919052124 07a13346-3026-4dce-a976-4e0faa132248 2025-12-02 20:47:23
12 11 起飞到学生宿舍搜索有没有被困人员,然后抛洒救援物资 1 True 1 14.128317832946777 16426d41-4f02-4e27-a05e-f4eb84d6c935 2025-12-02 20:47:38

View File

@@ -1,12 +1,12 @@
instruction_index,instruction,total_runs,successful_runs,success_rate,avg_response_time,min_response_time,max_response_time,total_response_time instruction_index,instruction,total_runs,successful_runs,success_rate,avg_response_time,min_response_time,max_response_time,total_response_time
1,起飞后移动到学生宿舍上方降落,10,10,100.00%,7.91s,7.73s,8.96s,79.07s 1,起飞后移动到学生宿舍上方降落,1,0,0.00%,N/A,N/A,N/A,0.00s
2,起飞后移动到学生宿舍上方查找蓝色的车,10,9,90.00%,14.18s,9.40s,25.44s,127.59s 2,起飞后移动到学生宿舍上方查找蓝色的车,1,1,100.00%,14.81s,14.81s,14.81s,14.81s
3,起飞后移动到学生宿舍上方寻找蓝色的车,10,10,100.00%,11.95s,7.00s,13.08s,119.49s 3,起飞后移动到学生宿舍上方寻找蓝色的车,1,1,100.00%,15.24s,15.24s,15.24s,15.24s
4,起飞后移动到学生宿舍上方检测蓝色的车,10,10,100.00%,12.21s,9.09s,13.09s,122.08s 4,起飞后移动到学生宿舍上方检测蓝色的车,1,1,100.00%,13.81s,13.81s,13.81s,13.81s
5,飞到学生宿舍上方查找蓝色的车,10,10,100.00%,12.99s,12.79s,13.07s,129.90s 5,飞到学生宿舍上方查找蓝色的车,1,1,100.00%,12.74s,12.74s,12.74s,12.74s
6,飞到学生宿舍上方查找蓝色车辆并进行打击,10,10,100.00%,19.17s,19.08s,19.38s,191.70s 6,飞到学生宿舍上方查找蓝色车辆并进行打击,1,1,100.00%,16.12s,16.12s,16.12s,16.12s
7,起飞后移动到学生宿舍上方搜索蓝色车辆,并进行打击,10,10,100.00%,19.18s,19.07s,19.25s,191.85s 7,起飞后移动到学生宿舍上方搜索蓝色车辆,并进行打击,1,1,100.00%,16.26s,16.26s,16.26s,16.26s
8,起飞到学生宿舍上方搜索被困人员,并为被困人员投递救援物资,10,10,100.00%,15.65s,14.89s,15.91s,156.47s 8,起飞到学生宿舍上方搜索被困人员,并为被困人员投递救援物资,1,1,100.00%,16.01s,16.01s,16.01s,16.01s
9,飞到学生宿舍上方搜索方圆10米范围内的蓝色车辆,10,10,100.00%,14.04s,13.97s,14.15s,140.40s 9,飞到学生宿舍上方搜索方圆10米范围内的蓝色车辆,1,1,100.00%,15.53s,15.53s,15.53s,15.53s
10,飞到学生宿舍上方搜索半径为10米区域范围内的蓝色车辆,10,10,100.00%,11.64s,11.29s,14.16s,116.39s 10,飞到学生宿舍上方搜索半径为10米区域范围内的蓝色车辆,1,1,100.00%,16.66s,16.66s,16.66s,16.66s
11,起飞到学生宿舍搜索有没有被困人员,然后抛洒救援物资,10,10,100.00%,16.02s,14.96s,24.95s,160.19s 11,起飞到学生宿舍搜索有没有被困人员,然后抛洒救援物资,1,1,100.00%,14.13s,14.13s,14.13s,14.13s
1 instruction_index instruction total_runs successful_runs success_rate avg_response_time min_response_time max_response_time total_response_time
2 1 起飞后移动到学生宿舍上方降落 10 1 10 0 100.00% 0.00% 7.91s N/A 7.73s N/A 8.96s N/A 79.07s 0.00s
3 2 起飞后移动到学生宿舍上方查找蓝色的车 10 1 9 1 90.00% 100.00% 14.18s 14.81s 9.40s 14.81s 25.44s 14.81s 127.59s 14.81s
4 3 起飞后移动到学生宿舍上方寻找蓝色的车 10 1 10 1 100.00% 11.95s 15.24s 7.00s 15.24s 13.08s 15.24s 119.49s 15.24s
5 4 起飞后移动到学生宿舍上方检测蓝色的车 10 1 10 1 100.00% 12.21s 13.81s 9.09s 13.81s 13.09s 13.81s 122.08s 13.81s
6 5 飞到学生宿舍上方查找蓝色的车 10 1 10 1 100.00% 12.99s 12.74s 12.79s 12.74s 13.07s 12.74s 129.90s 12.74s
7 6 飞到学生宿舍上方查找蓝色车辆并进行打击 10 1 10 1 100.00% 19.17s 16.12s 19.08s 16.12s 19.38s 16.12s 191.70s 16.12s
8 7 起飞后移动到学生宿舍上方搜索蓝色车辆,并进行打击 10 1 10 1 100.00% 19.18s 16.26s 19.07s 16.26s 19.25s 16.26s 191.85s 16.26s
9 8 起飞到学生宿舍上方搜索被困人员,并为被困人员投递救援物资 10 1 10 1 100.00% 15.65s 16.01s 14.89s 16.01s 15.91s 16.01s 156.47s 16.01s
10 9 飞到学生宿舍上方搜索方圆10米范围内的蓝色车辆 10 1 10 1 100.00% 14.04s 15.53s 13.97s 15.53s 14.15s 15.53s 140.40s 15.53s
11 10 飞到学生宿舍上方搜索半径为10米区域范围内的蓝色车辆 10 1 10 1 100.00% 11.64s 16.66s 11.29s 16.66s 14.16s 16.66s 116.39s 16.66s
12 11 起飞到学生宿舍搜索有没有被困人员,然后抛洒救援物资 10 1 10 1 100.00% 16.02s 14.13s 14.96s 14.13s 24.95s 14.13s 160.19s 14.13s

View File

@@ -17,7 +17,7 @@ SUMMARY_CSV = "test_summary.csv"
LOG_FILE = "api_test_log.txt" LOG_FILE = "api_test_log.txt"
# 测试参数 # 测试参数
TESTS_PER_INSTRUCTION = 10 TESTS_PER_INSTRUCTION = 1
MAX_RETRIES = 3 MAX_RETRIES = 3
RETRY_DELAY = 2 RETRY_DELAY = 2
@@ -207,20 +207,38 @@ def read_instructions(filename):
return [] return []
def write_log_entry(log_file, instruction_idx, run_number, prompt, result): def write_log_entry(log_file, instruction_idx, run_number, prompt, result):
"""写入详细日志""" """写入详细日志包含完整的API响应"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(log_file, 'a', encoding='utf-8') as f: with open(log_file, 'a', encoding='utf-8') as f:
f.write(f"\n{'='*80}\n") f.write(f"\n{'='*80}\n")
f.write(f"指令 #{instruction_idx} - 运行 #{run_number} - {timestamp}\n") f.write(f"指令 #{instruction_idx} - 运行 #{run_number} - {timestamp}\n")
f.write(f"HTTP状态: {result.get('http_status', 'N/A')}\n") f.write(f"HTTP状态: {result.get('http_status', 'N/A')}\n")
f.write(f"指令: {prompt}\n") f.write(f"原始指令: {prompt}\n")
f.write(f"尝试次数: {result['attempts']}\n") f.write(f"尝试次数: {result['attempts']}\n")
f.write(f"响应时间: {result['response_time']:.2f}\n") f.write(f"响应时间: {result['response_time']:.2f}\n")
f.write(f"结果: {'✅ 成功' if result['success'] else '❌ 失败'}\n") f.write(f"结果: {'✅ 成功' if result['success'] else '❌ 失败'}\n")
if result['success']: if result['success'] and result.get('data'):
f.write("验证结果:\n") data = result['data']
# 提取并记录组织后的prompt如果存在
organized_prompt = None
if isinstance(data, dict):
organized_prompt = data.get("organized_prompt") or \
data.get("processed_prompt") or \
data.get("final_prompt") or \
data.get("enhanced_prompt") or \
data.get("user_prompt_enhanced")
if organized_prompt:
f.write(f"\n📝 组织后的Prompt:\n")
f.write(f"{organized_prompt}\n")
else:
f.write(f"\n📝 组织后的Prompt: (未在响应中返回)\n")
# 记录验证结果
f.write("\n验证结果:\n")
for check_name, check_result in result['validation_checks'].items(): for check_name, check_result in result['validation_checks'].items():
f.write(f" {check_name}: {'' if check_result else ''}\n") f.write(f" {check_name}: {'' if check_result else ''}\n")
@@ -229,8 +247,27 @@ def write_log_entry(log_file, instruction_idx, run_number, prompt, result):
if result['invalid_conditions']: if result['invalid_conditions']:
f.write(f"⚠️ 无效条件节点: {result['invalid_conditions']}\n") f.write(f"⚠️ 无效条件节点: {result['invalid_conditions']}\n")
# 记录完整的API响应
f.write(f"\n完整API响应:\n")
try:
response_json = json.dumps(data, indent=2, ensure_ascii=False)
f.write(response_json)
f.write("\n")
except Exception as e:
f.write(f"⚠️ 无法序列化响应数据: {e}\n")
f.write(f"原始数据: {str(data)}\n")
else: else:
f.write(f"错误信息: {result['error']}\n") f.write(f"错误信息: {result['error']}\n")
# 即使失败也尝试记录响应数据(如果有)
if result.get('data'):
f.write(f"\n部分响应数据:\n")
try:
response_json = json.dumps(result['data'], indent=2, ensure_ascii=False)
f.write(response_json)
f.write("\n")
except Exception:
f.write(f"原始数据: {str(result['data'])}\n")
def generate_summary_report(instructions, results_summary): def generate_summary_report(instructions, results_summary):
""" """
@@ -311,6 +348,10 @@ def main():
write_log_entry(LOG_FILE, instruction_idx, run_number, prompt, result) write_log_entry(LOG_FILE, instruction_idx, run_number, prompt, result)
# 记录结果 # 记录结果
plan_id = ""
if result.get("success") and result.get("data") and isinstance(result["data"], dict):
plan_id = result["data"].get("plan_id", "")
detailed_result = { detailed_result = {
"instruction_index": instruction_idx, "instruction_index": instruction_idx,
"instruction": prompt, "instruction": prompt,
@@ -318,7 +359,7 @@ def main():
"success": result["success"], "success": result["success"],
"attempts": result["attempts"], "attempts": result["attempts"],
"response_time": result["response_time"], "response_time": result["response_time"],
"http_status": result.get("http_status"), "plan_id": plan_id,
"error": result["error"] or "", "error": result["error"] or "",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
} }
@@ -363,6 +404,10 @@ def main():
# 生成统计摘要 # 生成统计摘要
generate_summary_report(instructions, results_summary) generate_summary_report(instructions, results_summary)
# 计算总统计
total_tests = len(instructions) * TESTS_PER_INSTRUCTION
total_successful = sum(summary['success_count'] for summary in results_summary)
# 打印最终统计 # 打印最终统计
print(f"\n{'='*60}") print(f"\n{'='*60}")
print("📈 最终测试统计") print("📈 最终测试统计")
@@ -370,7 +415,10 @@ def main():
print(f"总测试次数: {total_tests}") print(f"总测试次数: {total_tests}")
print(f"成功次数: {total_successful}") print(f"成功次数: {total_successful}")
print(f"失败次数: {total_tests - total_successful}") print(f"失败次数: {total_tests - total_successful}")
print(f"总成功率: {(total_successful / total_tests * 100):.2f}%") if total_tests > 0:
print(f"总成功率: {(total_successful / total_tests * 100):.2f}%")
else:
print(f"总成功率: N/A")
# 打印每个指令的统计 # 打印每个指令的统计
print(f"\n📋 每个指令的统计:") print(f"\n📋 每个指令的统计:")

Binary file not shown.