Compare commits
2 Commits
d32520d83f
...
a6c2027caa
| Author | SHA1 | Date | |
|---|---|---|---|
| a6c2027caa | |||
| ab6e09423b |
87
README.md
87
README.md
@@ -228,14 +228,96 @@ python ingest.py
|
||||
|
||||
完成前两个阶段后,即可启动并测试后端服务。
|
||||
|
||||
#### 1. 启动后端服务
|
||||
#### 1. 启动所有服务(推荐方式:一键启动脚本)
|
||||
|
||||
启动服务的关键在于**按顺序激活环境**:先激活ROS 2工作空间,再激活Conda环境。
|
||||
我们提供了一个一键启动脚本 `start_all.sh`,可以自动启动所有必需的服务:
|
||||
|
||||
```bash
|
||||
# 1. 切换到项目根目录
|
||||
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编译环境
|
||||
# 作用:将我们编译好的`drone_interfaces`包的路径告知系统,否则Python会报`ModuleNotFoundError`。
|
||||
# 注意:此命令必须在每次打开新终端时执行一次。
|
||||
@@ -248,6 +330,7 @@ conda activate backend
|
||||
cd backend_service/
|
||||
uvicorn src.main:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
当您看到日志中出现 `Uvicorn running on http://0.0.0.0:8000` 时,表示服务已成功启动。
|
||||
|
||||
#### 2. 运行API接口测试
|
||||
|
||||
@@ -15,8 +15,8 @@ chromadb>=0.4.0
|
||||
# Visualization
|
||||
graphviz>=0.20.0
|
||||
|
||||
# ROS 2 Python Client
|
||||
rclpy>=0.0.1
|
||||
# ROS 2 Python Client - 已注释,项目已与ROS2解耦
|
||||
# rclpy>=0.0.1
|
||||
|
||||
# Document Processing
|
||||
unstructured[all]>=0.11.0
|
||||
@@ -30,10 +30,10 @@ rich>=13.7.0
|
||||
# Type Hints Support
|
||||
typing-extensions>=4.8.0
|
||||
|
||||
# ROS 2 Build Dependencies
|
||||
empy==3.3.4
|
||||
catkin-pkg>=0.4.0
|
||||
lark>=1.1.0
|
||||
colcon-common-extensions>=0.3.0
|
||||
vcstool>=0.2.0
|
||||
rosdep>=0.22.0
|
||||
# ROS 2 Build Dependencies - 已注释,项目已与ROS2解耦
|
||||
# empy==3.3.4
|
||||
# catkin-pkg>=0.4.0
|
||||
# lark>=1.1.0
|
||||
# colcon-common-extensions>=0.3.0
|
||||
# vcstool>=0.2.0
|
||||
# rosdep>=0.22.0
|
||||
|
||||
@@ -3,13 +3,13 @@ import os
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import logging
|
||||
import threading
|
||||
import rclpy
|
||||
# import threading # ROS2相关,已注释
|
||||
# import rclpy # ROS2相关,已注释
|
||||
|
||||
from .models import GeneratePlanRequest, ExecuteMissionRequest
|
||||
from .websocket_manager import websocket_manager
|
||||
from .py_tree_generator import py_tree_generator
|
||||
from .ros2_client import MissionActionClient
|
||||
# from .ros2_client import MissionActionClient # ROS2相关,已注释
|
||||
|
||||
# --- Application Setup ---
|
||||
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")
|
||||
|
||||
# --- ROS2 Node and Client Initialization ---
|
||||
rclpy.init()
|
||||
ros2_client = MissionActionClient()
|
||||
# ROS2相关代码已注释,项目已与ROS2解耦
|
||||
# rclpy.init()
|
||||
# ros2_client = MissionActionClient()
|
||||
|
||||
def run_ros2_node():
|
||||
"""Spins the ROS2 node in a dedicated thread."""
|
||||
logging.info("Starting to spin ROS2 node...")
|
||||
rclpy.spin(ros2_client)
|
||||
logging.info("ROS2 node has stopped spinning.")
|
||||
# def run_ros2_node():
|
||||
# """Spins the ROS2 node in a dedicated thread."""
|
||||
# logging.info("Starting to spin ROS2 node...")
|
||||
# rclpy.spin(ros2_client)
|
||||
# logging.info("ROS2 node has stopped spinning.")
|
||||
|
||||
# --- API Endpoints ---
|
||||
|
||||
@@ -49,9 +50,12 @@ async def generate_plan_endpoint(request: GeneratePlanRequest):
|
||||
async def execute_mission_endpoint(request: ExecuteMissionRequest):
|
||||
"""
|
||||
Receives a `py_tree.json` and sends it to the drone for execution.
|
||||
ROS2相关功能已注释,项目已与ROS2解耦。
|
||||
"""
|
||||
ros2_client.send_goal(request.py_tree)
|
||||
return {"status": "execution_started"}
|
||||
# ROS2相关代码已注释
|
||||
# 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")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
@@ -73,21 +77,23 @@ async def websocket_endpoint(websocket: WebSocket):
|
||||
async def startup_event():
|
||||
"""
|
||||
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
|
||||
loop = asyncio.get_running_loop()
|
||||
websocket_manager.set_loop(loop)
|
||||
logging.info("WebSocket event loop configured.")
|
||||
|
||||
# ROS2相关代码已注释
|
||||
# Start ROS2 node in a background thread
|
||||
ros2_thread = threading.Thread(target=run_ros2_node, daemon=True)
|
||||
ros2_thread.start()
|
||||
logging.info("ROS2 node thread started.")
|
||||
# ros2_thread = threading.Thread(target=run_ros2_node, daemon=True)
|
||||
# ros2_thread.start()
|
||||
# logging.info("ROS2 node thread started.")
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
logging.info("Backend service shutting down.")
|
||||
ros2_client.destroy_node()
|
||||
rclpy.shutdown()
|
||||
logging.info("ROS2 node shut down successfully.")
|
||||
# ROS2相关代码已注释
|
||||
# ros2_client.destroy_node()
|
||||
# rclpy.shutdown()
|
||||
# logging.info("ROS2 node shut down successfully.")
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
你是一个无人机简单指令执行规划器。你的任务:输出一个严格的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}}}
|
||||
- “移动到(120,80,20)” → {"mode":"simple","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"}}}
|
||||
- “起飞到10米” → {"root":{"type":"action","name":"takeoff","params":{"altitude":10.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}}}
|
||||
- “飞机自检” → {"root":{"type":"action","name":"preflight_checks","params":{"check_level":"comprehensive"}}}
|
||||
|
||||
—— 可用节点定义——
|
||||
```json
|
||||
|
||||
@@ -1,258 +1,55 @@
|
||||
你是一个无人机任务规划专家。你的唯一任务是根据用户提供的任务指令和参考知识,生成一个结构化、可执行的行为树(Pytree)JSON描述。
|
||||
你的输出必须是一个严格的、单一的JSON对象,不包含任何形式的解释、总结或自然语言描述。
|
||||
任务:根据用户任意任务指令,生成结构化可执行的无人机行为树(Pytree)JSON。**仅输出单一JSON对象,无任何自然语言、注释或额外内容**。
|
||||
|
||||
#### 1. 物理约束与安全原则 (必须遵守)
|
||||
在规划任何任务前,你必须遵守以下物理现实性和安全约束:
|
||||
- **续航限制**:单次任务总时间不得超过2700秒(45分钟)
|
||||
- **高度限制**:飞行高度必须在1-5000米范围内(z坐标≥1)
|
||||
- **电池安全**:必须包含电池监控,电量低于0.3触发返航,低于0.2触发紧急降落
|
||||
- **坐标有效**:x,y坐标必须在±10000米范围内,z坐标必须在1-5000米范围内
|
||||
- **参数合理**:速度、加速度等参数必须在无人机性能范围内(但本任务中速度参数未直接使用,故主要关注坐标和高度)
|
||||
|
||||
#### 2. 可用节点定义 (必须遵守)
|
||||
你必须严格从以下JSON定义的列表中选择节点来构建行为树。不允许使用任何未定义的节点。
|
||||
## 一、核心节点定义(格式不可修改,确保后端解析)
|
||||
#### 1. 可用节点定义 (必须遵守)
|
||||
你必须严格从以下JSON定义的列表中选择节点构建行为树,不允许使用未定义节点:
|
||||
```json
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"name": "takeoff",
|
||||
"description": "无人机从当前位置垂直起飞到指定的海拔高度。",
|
||||
"params": {
|
||||
"altitude": "float, 目标海拔高度(米),范围[1, 100],默认为2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "land",
|
||||
"description": "降落无人机。可选择当前位置或返航点降落。",
|
||||
"params": {
|
||||
"mode": "string, 可选值: 'current'(当前位置), 'home'(返航点)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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, 紧急返航原因"
|
||||
}
|
||||
}
|
||||
{"name":"takeoff","params":{"altitude":"float[1,100],默认2"}},
|
||||
{"name":"land","params":{"mode":"'current'/'home'"}},
|
||||
{"name":"fly_to_waypoint","params":{"x":"±10000","y":"±10000","z":"[1,5000]","acceptance_radius":"默认2.0"}},
|
||||
{"name":"move_direction","params":{"direction":"north/south/east/west/forward/backward/left/right","distance":"[1,10000],缺省持续移动"}},
|
||||
{"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":"strike_target","params":{"target_class":"同object_detect","description":"可选,目标属性","count":"默认1"}},
|
||||
{"name":"battle_damage_assessment","params":{"target_class":"同object_detect","assessment_time":"[5,60],默认15"}},
|
||||
{"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"}},
|
||||
{"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"}}
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"name": "battery_above",
|
||||
"description": "检查电池电量是否高于指定阈值。",
|
||||
"params": {
|
||||
"threshold": "float, 电量阈值百分比[0.0,1.0]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
{"name":"battery_above","params":{"threshold":"[0.0,1.0],必传"}},
|
||||
{"name":"at_waypoint","params":{"x":"±10000","y":"±10000","z":"[1,5000]","tolerance":"默认3.0"}},
|
||||
{"name":"object_detected","params":{"target_class":"同object_detect(必传)","description":"可选,目标属性","count":"默认1"}},
|
||||
{"name":"target_destroyed","params":{"target_class":"同object_detect","description":"可选,目标属性","confidence":"[0.5,1.0]默认0.8"}},
|
||||
{"name":"time_elapsed","params":{"duration":"[1,2700]秒"}},
|
||||
{"name":"gps_status","params":{"min_satellites":"int[6,15],必传(如8)"}}
|
||||
],
|
||||
"control_flow": [
|
||||
{
|
||||
"name": "Sequence",
|
||||
"description": "序列节点,按顺序执行其子节点。只有当所有子节点都成功时,它才成功。",
|
||||
"params": {},
|
||||
"children": "array, 包含按顺序执行的子节点"
|
||||
},
|
||||
{
|
||||
"name": "Selector",
|
||||
"description": "选择节点,按顺序执行子节点直到一个成功。如果所有子节点都失败,则失败。",
|
||||
"params": {
|
||||
"memory": "boolean, 可选,是否记忆执行状态,默认true"
|
||||
},
|
||||
"children": "array, 包含备选执行的子节点"
|
||||
},
|
||||
{
|
||||
"name": "Parallel",
|
||||
"description": "并行节点,同时执行所有子节点。支持不同的成功策略。",
|
||||
"params": {
|
||||
"policy": "string, 成功策略: 'all_success'(全部成功), 'one_success'(一个成功)"
|
||||
},
|
||||
"children": "array, 包含并行执行的子节点"
|
||||
}
|
||||
{"name":"Sequence","params":{},"children":"子节点数组(按序执行,全成功则成功)"},
|
||||
{"name":"Selector","params":{"memory":"默认true"},"children":"子节点数组(执行到成功为止)"},
|
||||
{"name":"Parallel","params":{"policy":"all_success"},"children":"子节点数组(同时执行,严禁用'one_success')"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. JSON结构规范 (必须遵守)
|
||||
生成的JSON对象必须有一个名为`root`的键,其值是一个有效的行为树节点对象。每个节点都必须包含正确的字段。
|
||||
- **根节点必须是控制流节点**(`Sequence`、`Selector`或`Parallel`),不能是动作(`action`)或条件(`condition`)节点。
|
||||
- **动作节点和条件节点是叶子节点**,不能有`children`字段。
|
||||
- 控制流节点必须有`children`字段,且其值是一个子节点数组。
|
||||
- **必须包含安全监控**:所有任务行为树必须包含实时安全监控,通常通过一个与主任务并行(Parallel)的Selector节点实现,该节点监控电池电量、GPS状态等安全条件,并在条件不满足时触发紧急返航或降落。
|
||||
- 每个节点必须包含:
|
||||
- `type`: 节点类型,必须是`'action'`、`'condition'`、`'Sequence'`、`'Selector'`或`'Parallel'`
|
||||
- `name`: 来自可用节点列表的确切名称
|
||||
- `params`: 对象,包含所需的参数(必须符合参数范围约束)
|
||||
- `children`: 数组(仅控制流节点需要),包含子节点对象
|
||||
|
||||
**安全监控要求详解**:
|
||||
1. **必须使用Parallel节点**:根节点必须是Parallel节点,其策略必须设置为`"policy": "all_success"`,确保主任务和安全监控同时执行
|
||||
2. **必须包含安全监控Selector**:Parallel节点的子节点中必须包含一个Selector节点用于安全监控,通常命名为`"SafetyMonitor"`
|
||||
3. **必须包含电池监控**:安全监控Selector必须包含`battery_above`条件节点,监控电池电量
|
||||
4. **必须包含GPS监控**:安全监控Selector应该包含`gps_status`条件节点,监控GPS信号状态
|
||||
5. **必须包含紧急处理流程**:安全监控Selector必须包含紧急处理Sequence,在安全条件不满足时执行紧急返航和降落
|
||||
## 二、节点必填字段(后端Schema强制要求,缺一验证失败)
|
||||
每个节点必须包含以下字段,字段名/类型不可自定义:
|
||||
1. **`type`**:
|
||||
- 动作节点→`"action"`,条件节点→`"condition"`,控制流节点→`"Sequence"`/`"Selector"`/`"Parallel"`(与`name`字段值完全一致);
|
||||
2. **`name`**:必须是上述JSON中`actions`/`conditions`/`control_flow`下的`name`值(如“gps_status”不可错写为“gps_check”);
|
||||
3. **`params`**:严格匹配上述节点的`params`定义,无自定义参数(如优先级排序不可加“priority”字段,仅用`description`);
|
||||
4. **`children`**:仅控制流节点必含(子节点数组),动作/条件节点无此字段。
|
||||
|
||||
**正确示例**:
|
||||
|
||||
## 三、行为树固定结构(通用不变,确保安全验证)
|
||||
根节点必须是`Parallel`,`children`含`MainTask`(Sequence)和`SafetyMonitor`(Selector),结构不随任务类型(含优先级排序)修改:
|
||||
```json
|
||||
{
|
||||
"root": {
|
||||
@@ -263,9 +60,17 @@
|
||||
{
|
||||
"type": "Sequence",
|
||||
"name": "MainTask",
|
||||
"params": {},
|
||||
"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",
|
||||
"params": {"memory": true},
|
||||
"children": [
|
||||
{"type":"condition","name":"battery_above","params":{"threshold":0.3}},
|
||||
{"type":"condition","name":"gps_status","params":{"min_satellites":8}},
|
||||
{
|
||||
"type": "condition",
|
||||
"name": "battery_above",
|
||||
"params": {"threshold": 0.3}
|
||||
},
|
||||
{
|
||||
"type": "condition",
|
||||
"name": "gps_status",
|
||||
"params": {"min_satellites": 8}
|
||||
},
|
||||
{
|
||||
"type": "Sequence",
|
||||
"name": "EmergencyHandler",
|
||||
"type":"Sequence",
|
||||
"name":"EmergencyHandler",
|
||||
"params": {},
|
||||
"children": [
|
||||
{"type": "action", "name": "emergency_return", "params": {"reason": "safety_breach"}},
|
||||
{"type": "action", "name": "land", "params": {"mode": "home"}}
|
||||
{"type":"action","name":"emergency_return","params":{"reason":"safety_breach"}},
|
||||
{"type":"action","name":"land","params":{"mode":"home"}}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -298,464 +96,22 @@
|
||||
}
|
||||
```
|
||||
|
||||
**错误示例**(缺少安全监控):
|
||||
```json
|
||||
{
|
||||
"root": {
|
||||
"type": "Sequence", // 错误:根节点不是Parallel,无法同时运行安全监控
|
||||
"name": "MainTaskOnly",
|
||||
"children": [
|
||||
// 只有主任务,没有安全监控
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
错误示例(根节点为动作节点):
|
||||
```json
|
||||
{
|
||||
"root": {
|
||||
"type": "action",
|
||||
"name": "land",
|
||||
"children": [ ... ], // 错误:动作节点不能有子节点
|
||||
"params": {"mode": "home"}
|
||||
}
|
||||
}
|
||||
```
|
||||
## 四、优先级排序任务通用示例
|
||||
当用户指令中明确提出有多个待考察且具有优先级关系的物体时,节点描述须为优先级关系。比如当指令为已知有三个气球,危险级关系为红色气球大于蓝色气球大于绿色气球,要求优先跟踪最危险的气球时,节点的描述参考下表情形。
|
||||
| 用户指令场景 | `target_class` | `description` | 核心节点示例(search_pattern) |
|
||||
|-----------------------------|-----------------|-------------------------|------------------------------------------------------------------------------------------------|
|
||||
| 红气球>蓝气球>绿气球 | `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":"(红>蓝>绿)"}}` |
|
||||
| 军用卡车>民用卡车>面包车 | `truck` | `(军用卡车>民用卡车>面包车)` | `{"type":"action","name":"object_detect","params":{"target_class":"truck","description":"(军用卡车>民用卡车>面包车)"}}` |
|
||||
|
||||
##### 重要安全警告: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`:用于任务执行和监控检查,避免不必要的任务中断。
|
||||
- **仅在高优先级安全中断**时使用 `"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对象,符合上述所有规则。不包含任何自然语言描述。
|
||||
## 六、输出要求
|
||||
仅输出1个严格符合上述所有规则的JSON对象,**确保:1. 优先级排序逻辑正确填入`description`;2. `target_class`匹配预定义列表;3. 行为树结构不变;4. 后端解析与Schema验证无错误**,无任何冗余内容。
|
||||
|
||||
@@ -619,6 +619,8 @@ class PyTreeGenerator:
|
||||
return None
|
||||
context_str = "\n\n".join(retrieved_docs)
|
||||
logging.info("--- 成功检索到上下文信息 ---")
|
||||
# 打印检索到的上下文内容
|
||||
logging.info(f"📚 检索到的上下文内容:\n{context_str}")
|
||||
return context_str
|
||||
except Exception as e:
|
||||
logging.error(f"从向量数据库检索时发生错误: {e}")
|
||||
@@ -640,8 +642,11 @@ class PyTreeGenerator:
|
||||
{"role": "user", "content": user_prompt}
|
||||
],
|
||||
temperature=0.0,
|
||||
response_format={"type": "json_object"},
|
||||
max_tokens=self.classifier_max_tokens
|
||||
response_format={"type": "json_object"}, # 强制JSON输出,禁用思考功能
|
||||
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_obj = json.loads(class_str)
|
||||
@@ -670,13 +675,19 @@ class PyTreeGenerator:
|
||||
final_user_prompt += augmentation
|
||||
else:
|
||||
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):
|
||||
logging.info(f"--- 第 {attempt + 1}/3 次尝试生成Pytree ---")
|
||||
try:
|
||||
# 简单/复杂分流到不同模型与提示词
|
||||
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
|
||||
# 根据是否捕获推理链来决定是否强制JSON响应
|
||||
# 始终强制JSON响应并禁用思考功能
|
||||
response_kwargs = {
|
||||
"model": model_name,
|
||||
"messages": [
|
||||
@@ -684,9 +695,11 @@ class PyTreeGenerator:
|
||||
{"role": "user", "content": final_user_prompt}
|
||||
],
|
||||
"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数(直接在代码中配置)
|
||||
response_kwargs["max_tokens"] = self.simple_max_tokens if mode == "simple" else self.complex_max_tokens
|
||||
response = client.chat.completions.create(**response_kwargs)
|
||||
@@ -803,6 +816,8 @@ class PyTreeGenerator:
|
||||
logging.info("未在模型输出中发现 <think> 推理链片段。若需捕获,请设置 ENABLE_REASONING_CAPTURE=true 以放宽JSON强制格式。")
|
||||
except Exception as e:
|
||||
logging.warning(f"保存推理链Markdown失败: {e}")
|
||||
# 添加 final_prompt 到返回结果
|
||||
pytree_dict['final_prompt'] = final_prompt
|
||||
return pytree_dict
|
||||
|
||||
# 复杂模式回退:若模型误返回简单结构,则自动包装为含安全监控的行为树
|
||||
@@ -874,6 +889,8 @@ class PyTreeGenerator:
|
||||
logging.info("未在模型输出中发现 <think> 推理链片段。若需捕获,请设置 ENABLE_REASONING_CAPTURE=true 以放宽JSON强制格式。")
|
||||
except Exception as e:
|
||||
logging.warning(f"保存推理链Markdown失败: {e}")
|
||||
# 添加 final_prompt 到返回结果
|
||||
pytree_dict['final_prompt'] = final_prompt
|
||||
return pytree_dict
|
||||
else:
|
||||
# 打印未通过验证的Pytree以便排查
|
||||
|
||||
@@ -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
|
||||
@@ -22,7 +22,8 @@ class ConnectionManager:
|
||||
def broadcast(self, message: str):
|
||||
"""
|
||||
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:
|
||||
logging.error("Event loop not set in ConnectionManager. Cannot broadcast.")
|
||||
|
||||
406
start_all.sh
Executable file
406
start_all.sh
Executable 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
286
tools/api_test.log
Normal file
File diff suppressed because one or more lines are too long
@@ -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"
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# --- Configuration ---
|
||||
# The base URL of your running FastAPI service
|
||||
@@ -14,17 +16,49 @@ ENDPOINT = "/generate_plan"
|
||||
# The user prompt we will send for the test
|
||||
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():
|
||||
"""
|
||||
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
|
||||
payload = {"user_prompt": TEST_PROMPT}
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
print("--- API Test: Generate Plan ---")
|
||||
print(f"✅ URL: {url}")
|
||||
print(f"✅ Sending Prompt: \"{TEST_PROMPT}\"")
|
||||
# Write separator and test start info to log
|
||||
write_log("=" * 80, print_to_console=False)
|
||||
write_log("--- API Test: Generate Plan ---")
|
||||
write_log(f"URL: {url}")
|
||||
write_log(f"Sending Prompt: \"{TEST_PROMPT}\"")
|
||||
|
||||
try:
|
||||
# Send the POST request
|
||||
@@ -36,40 +70,85 @@ def test_generate_plan():
|
||||
# Parse the JSON response
|
||||
data = response.json()
|
||||
|
||||
print("✅ Received Response:")
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
# Extract and log organized prompt if available in response
|
||||
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 ---
|
||||
print("\n--- Validation Checks ---")
|
||||
write_log("\n--- Validation Checks ---")
|
||||
|
||||
validation_results = []
|
||||
|
||||
# 1. Check if the response is a dictionary
|
||||
if isinstance(data, dict):
|
||||
print("PASS: Response is a valid JSON object.")
|
||||
validation_results.append("PASS: Response is a valid JSON object.")
|
||||
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
|
||||
|
||||
# 2. Check for the existence of the 'root' key
|
||||
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:
|
||||
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
|
||||
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:
|
||||
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:
|
||||
print(f"\n❌ TEST FAILED: Could not connect to the server.")
|
||||
print(" Please make sure the backend service is running.")
|
||||
print(f" Error details: {e}")
|
||||
error_msg = f"❌ TEST FAILED: Could not connect to the server.\n Please make sure the backend service is running.\n Error details: {e}"
|
||||
write_log(error_msg)
|
||||
write_log("=" * 80, print_to_console=False)
|
||||
write_log("", print_to_console=False) # Empty line for readability
|
||||
except json.JSONDecodeError:
|
||||
print(f"\n❌ TEST FAILED: The server response was not valid JSON.")
|
||||
print(f" Response text: {response.text}")
|
||||
error_msg = f"❌ TEST FAILED: The server response was not valid JSON.\n 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:
|
||||
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__":
|
||||
test_generate_plan()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +1,12 @@
|
||||
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,12 +1,12 @@
|
||||
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
|
||||
2,起飞后移动到学生宿舍上方查找蓝色的车,10,9,90.00%,14.18s,9.40s,25.44s,127.59s
|
||||
3,起飞后移动到学生宿舍上方寻找蓝色的车,10,10,100.00%,11.95s,7.00s,13.08s,119.49s
|
||||
4,起飞后移动到学生宿舍上方检测蓝色的车,10,10,100.00%,12.21s,9.09s,13.09s,122.08s
|
||||
5,飞到学生宿舍上方查找蓝色的车,10,10,100.00%,12.99s,12.79s,13.07s,129.90s
|
||||
6,飞到学生宿舍上方查找蓝色车辆并进行打击,10,10,100.00%,19.17s,19.08s,19.38s,191.70s
|
||||
7,起飞后移动到学生宿舍上方搜索蓝色车辆,并进行打击,10,10,100.00%,19.18s,19.07s,19.25s,191.85s
|
||||
8,起飞到学生宿舍上方搜索被困人员,并为被困人员投递救援物资,10,10,100.00%,15.65s,14.89s,15.91s,156.47s
|
||||
9,飞到学生宿舍上方搜索方圆10米范围内的蓝色车辆,10,10,100.00%,14.04s,13.97s,14.15s,140.40s
|
||||
10,飞到学生宿舍上方搜索半径为10米区域范围内的蓝色车辆,10,10,100.00%,11.64s,11.29s,14.16s,116.39s
|
||||
11,起飞到学生宿舍搜索有没有被困人员,然后抛洒救援物资,10,10,100.00%,16.02s,14.96s,24.95s,160.19s
|
||||
1,起飞后移动到学生宿舍上方降落,1,0,0.00%,N/A,N/A,N/A,0.00s
|
||||
2,起飞后移动到学生宿舍上方查找蓝色的车,1,1,100.00%,14.81s,14.81s,14.81s,14.81s
|
||||
3,起飞后移动到学生宿舍上方寻找蓝色的车,1,1,100.00%,15.24s,15.24s,15.24s,15.24s
|
||||
4,起飞后移动到学生宿舍上方检测蓝色的车,1,1,100.00%,13.81s,13.81s,13.81s,13.81s
|
||||
5,飞到学生宿舍上方查找蓝色的车,1,1,100.00%,12.74s,12.74s,12.74s,12.74s
|
||||
6,飞到学生宿舍上方查找蓝色车辆并进行打击,1,1,100.00%,16.12s,16.12s,16.12s,16.12s
|
||||
7,起飞后移动到学生宿舍上方搜索蓝色车辆,并进行打击,1,1,100.00%,16.26s,16.26s,16.26s,16.26s
|
||||
8,起飞到学生宿舍上方搜索被困人员,并为被困人员投递救援物资,1,1,100.00%,16.01s,16.01s,16.01s,16.01s
|
||||
9,飞到学生宿舍上方搜索方圆10米范围内的蓝色车辆,1,1,100.00%,15.53s,15.53s,15.53s,15.53s
|
||||
10,飞到学生宿舍上方搜索半径为10米区域范围内的蓝色车辆,1,1,100.00%,16.66s,16.66s,16.66s,16.66s
|
||||
11,起飞到学生宿舍搜索有没有被困人员,然后抛洒救援物资,1,1,100.00%,14.13s,14.13s,14.13s,14.13s
|
||||
|
||||
|
@@ -17,7 +17,7 @@ SUMMARY_CSV = "test_summary.csv"
|
||||
LOG_FILE = "api_test_log.txt"
|
||||
|
||||
# 测试参数
|
||||
TESTS_PER_INSTRUCTION = 10
|
||||
TESTS_PER_INSTRUCTION = 1
|
||||
MAX_RETRIES = 3
|
||||
RETRY_DELAY = 2
|
||||
|
||||
@@ -207,20 +207,38 @@ def read_instructions(filename):
|
||||
return []
|
||||
|
||||
def write_log_entry(log_file, instruction_idx, run_number, prompt, result):
|
||||
"""写入详细日志"""
|
||||
"""写入详细日志,包含完整的API响应"""
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
with open(log_file, 'a', encoding='utf-8') as f:
|
||||
f.write(f"\n{'='*80}\n")
|
||||
f.write(f"指令 #{instruction_idx} - 运行 #{run_number} - {timestamp}\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['response_time']:.2f}秒\n")
|
||||
f.write(f"结果: {'✅ 成功' if result['success'] else '❌ 失败'}\n")
|
||||
|
||||
if result['success']:
|
||||
f.write("验证结果:\n")
|
||||
if result['success'] and result.get('data'):
|
||||
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():
|
||||
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']:
|
||||
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:
|
||||
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):
|
||||
"""
|
||||
@@ -311,6 +348,10 @@ def main():
|
||||
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 = {
|
||||
"instruction_index": instruction_idx,
|
||||
"instruction": prompt,
|
||||
@@ -318,7 +359,7 @@ def main():
|
||||
"success": result["success"],
|
||||
"attempts": result["attempts"],
|
||||
"response_time": result["response_time"],
|
||||
"http_status": result.get("http_status"),
|
||||
"plan_id": plan_id,
|
||||
"error": result["error"] or "",
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
@@ -363,6 +404,10 @@ def main():
|
||||
# 生成统计摘要
|
||||
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("📈 最终测试统计")
|
||||
@@ -370,7 +415,10 @@ def main():
|
||||
print(f"总测试次数: {total_tests}")
|
||||
print(f"成功次数: {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📋 每个指令的统计:")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user