From a6c2027caaec8b79a5cc2f5f87c1b4cafc71824c Mon Sep 17 00:00:00 2001 From: huangfu <3045324663@qq.com> Date: Tue, 2 Dec 2025 21:42:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=B8=80=E9=94=AE?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E8=84=9A=E6=9C=AC=E5=B9=B6=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 start_all.sh 一键启动脚本,支持启动llama-server和FastAPI服务 - 修改启动脚本使用venv虚拟环境替代conda环境 - 更新README.md,添加一键启动脚本使用说明 - 更新py_tree_generator.py,添加final_prompt返回字段 - 禁用Qwen3模型的思考功能 - 添加RAG检索结果的终端打印 - 移除ROS2相关代码(ros2_client.py已删除) --- README.md | 87 +- backend_service/requirements.txt | 18 +- backend_service/src/main.py | 44 +- .../src/prompts/simple_mode_prompt.txt | 13 +- backend_service/src/prompts/system_prompt.txt | 782 ++---------------- backend_service/src/py_tree_generator.py | 27 +- backend_service/src/ros2_client.py | 67 -- backend_service/src/websocket_manager.py | 3 +- start_all.sh | 406 +++++++++ 9 files changed, 625 insertions(+), 822 deletions(-) delete mode 100644 backend_service/src/ros2_client.py create mode 100755 start_all.sh diff --git a/README.md b/README.md index 7a6fdb55..1ddf2c6d 100644 --- a/README.md +++ b/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接口测试 diff --git a/backend_service/requirements.txt b/backend_service/requirements.txt index 7507870b..7648ed2f 100644 --- a/backend_service/requirements.txt +++ b/backend_service/requirements.txt @@ -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 diff --git a/backend_service/src/main.py b/backend_service/src/main.py index 0e97075e..5c8f2c7a 100644 --- a/backend_service/src/main.py +++ b/backend_service/src/main.py @@ -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.") diff --git a/backend_service/src/prompts/simple_mode_prompt.txt b/backend_service/src/prompts/simple_mode_prompt.txt index 077101ff..bb587c39 100644 --- a/backend_service/src/prompts/simple_mode_prompt.txt +++ b/backend_service/src/prompts/simple_mode_prompt.txt @@ -1,15 +1,16 @@ -你是一个无人机简单指令执行规划器。你的任务:输出一个严格的JSON对象。 +你是一个无人机简单指令执行规划器。你的任务:当用户给出“简单指令”(单一原子动作即可完成)时,输出一个严格的JSON对象。 输出要求(必须遵守): - 只输出一个JSON对象,不要任何解释或多余文本。 - JSON结构: -{"mode":"simple","action":{"name":"","params":{...}}} -- 不包含任何行为树结构与安全监控并行,仅输出单一原子动作。 +{"root":{"type":"action","name":"","params":{...}}} +- 与参数定义、取值范围,必须与“复杂模式”提示词(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 diff --git a/backend_service/src/prompts/system_prompt.txt b/backend_service/src/prompts/system_prompt.txt index a3d58724..a52410a8 100644 --- a/backend_service/src/prompts/system_prompt.txt +++ b/backend_service/src/prompts/system_prompt.txt @@ -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对象,符合上述所有规则。不包含任何自然语言描述。 \ No newline at end of file +## 六、输出要求 +仅输出1个严格符合上述所有规则的JSON对象,**确保:1. 优先级排序逻辑正确填入`description`;2. `target_class`匹配预定义列表;3. 行为树结构不变;4. 后端解析与Schema验证无错误**,无任何冗余内容。 diff --git a/backend_service/src/py_tree_generator.py b/backend_service/src/py_tree_generator.py index b6690675..6b19da7f 100644 --- a/backend_service/src/py_tree_generator.py +++ b/backend_service/src/py_tree_generator.py @@ -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("未在模型输出中发现 推理链片段。若需捕获,请设置 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("未在模型输出中发现 推理链片段。若需捕获,请设置 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以便排查 diff --git a/backend_service/src/ros2_client.py b/backend_service/src/ros2_client.py deleted file mode 100644 index 4bafabec..00000000 --- a/backend_service/src/ros2_client.py +++ /dev/null @@ -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 diff --git a/backend_service/src/websocket_manager.py b/backend_service/src/websocket_manager.py index dbc42f2a..4f5cae2f 100644 --- a/backend_service/src/websocket_manager.py +++ b/backend_service/src/websocket_manager.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.") diff --git a/start_all.sh b/start_all.sh new file mode 100755 index 00000000..78fa8568 --- /dev/null +++ b/start_all.sh @@ -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 +