# 无人机自然语言控制项目

本项目构建了一个完整的无人机自然语言控制系统，集成了检索增强生成（RAG）知识库、大型语言模型（LLM）、FastAPI后端服务和ROS2通信，最终实现通过自然语言指令控制无人机执行复杂任务。

## 项目结构

项目被清晰地划分为几个核心模块：

```
.
├── backend_service/
│   ├── src/                      # FastAPI应用核心代码
│   │   ├── __init__.py
│   │   ├── main.py               # 应用主入口，提供Web API
│   │   ├── py_tree_generator.py  # RAG与LLM集成，生成py_tree
│   │   ├── prompts/              # LLM 提示词
│   │   │   ├── system_prompt.txt         # 复杂模式提示词（行为树与安全监控）
│   │   │   ├── simple_mode_prompt.txt    # 简单模式提示词（单一原子动作JSON）
│   │   │   └── classifier_prompt.txt     # 指令简单/复杂分类提示词
│   │   ├── ...
│   ├── generated_visualizations/ # 存放最新生成的py_tree可视化图像
│   └── requirements.txt          # 后端服务的Python依赖
│
├── tools/
│   ├── map/                      # 【数据源】存放原始地图文件(如.world, .json)
│   ├── knowledge_base/           # 【处理后】存放build_knowledge_base.py生成的.ndjson文件
│   ├── vector_store/             # 【数据库】存放最终的ChromaDB向量数据库
│   ├── build_knowledge_base.py   # 【步骤1】用于将原始数据转换为自然语言知识
│   └── ingest.py                 # 【步骤2】用于将自然语言知识摄入向量数据库
│
├── /             # ROS2接口定义 (保持不变)
└── docs/
    └── README.md                 # 本说明文件
```

## 核心配置：Orin IP 地址

**重要提示：** 本项目的后端服务和知识库工具需要与在NVIDIA Jetson Orin设备上运行的服务进行通信（嵌入模型和LLM推理服务），**默认的IP地址为localhost**，所以使用电脑本地部署的模型服务同样可以，但是需要注意指定模型的端口。

在使用前，您必须配置正确的Orin设备IP地址。您可以通过以下两种方式之一进行设置：

1.  **设置环境变量 (推荐)**:
    在您的终端中设置一个名为 `ORIN_IP` 的环境变量。
    ```bash
    export ORIN_IP="192.168.1.100" # 请替换为您的Orin设备的实际IP地址
    ```
    脚本会优先使用这个环境变量。

2.  **直接修改脚本**:
    如果您不想设置环境变量，可以打开 `tools/ingest.py` 和 `backend_service/src/py_tree_generator.py` 文件，找到 `orin_ip = os.getenv("ORIN_IP", "...")` 这样的行，并将默认的IP地址修改为您的Orin设备的实际IP地址。

**在继续后续步骤之前，请务必完成此项配置。**

## 模型端口启动

本项目启动依赖于后端的模型推理服务，即`ORIN_IP`所指向的设备的模型服务端口，目前项目使用instruct模型与embedding模型实现流程，分别部署在8081端口与8090端口。

1. **推理模型部署**:

    在`/llama.cpp/build/bin`路径下执行以下命令启动模型
    ```bash
    ./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
    ```

2. **Embedding模型部署**

    在`/llama.cpp/build/bin`路径下执行以下命令启动模型
    ```bash
    ./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
    ```

---

## 指令分类与分流

后端在生成任务前会先对用户指令进行“简单/复杂”分类，并分流到不同提示词与模型：

- 分类提示词：`backend_service/src/prompts/classifier_prompt.txt`
- 简单模式提示词：`backend_service/src/prompts/simple_mode_prompt.txt`
- 复杂模式提示词：`backend_service/src/prompts/system_prompt.txt`

分类仅输出如下JSON之一：`{"mode":"simple"}` 或 `{"mode":"complex"}`。两种模式都会执行检索增强（RAG），将参考知识拼接到用户指令后再进行推理。

当为简单模式时，LLM仅输出：
`{"mode":"simple","action":{"name":"<action>","params":{...}}}`。
后端不会再自动封装为复杂行为树；将直接返回简单JSON，并附加 `plan_id` 与 `visualization_url`（单动作可视化）。

### 环境变量（可选）

支持为“分类/简单/复杂”三类调用分别配置模型与Base URL（未设置时回退到默认本地配置）：

- `CLASSIFIER_MODEL`, `CLASSIFIER_BASE_URL`
- `SIMPLE_MODEL`, `SIMPLE_BASE_URL`
- `COMPLEX_MODEL`, `COMPLEX_BASE_URL`

通用API Key：`OPENAI_API_KEY`

示例：
```bash
export CLASSIFIER_MODEL="qwen2.5-1.8b-instruct"
export SIMPLE_MODEL="qwen2.5-1.8b-instruct"
export COMPLEX_MODEL="qwen2.5-7b-instruct"
export CLASSIFIER_BASE_URL="http://$ORIN_IP:8081/v1"
export SIMPLE_BASE_URL="http://$ORIN_IP:8081/v1"
export COMPLEX_BASE_URL="http://$ORIN_IP:8081/v1"
export OPENAI_API_KEY="sk-no-key-required"
```

### 测试简单模式

启动服务后，运行内置测试脚本：

```bash
cd tools
python test_api.py
```

示例输入：“简单模式，起飞” 或 “起飞到10米”。返回结果为简单JSON（无 `root`）：包含 `mode`、`action`、`plan_id`、`visualization_url`。

---

## 工作流程

整个系统的工作流程分为两个主要阶段：

1.  **知识库构建（一次性设置）**: 将环境信息、无人机能力等知识加工并存入向量数据库。
2.  **后端服务运行与交互**: 启动主服务，通过API接收指令、生成并执行任务。

### 阶段一：环境设置与编译

此阶段为项目准备好运行环境，仅需在初次配置或依赖变更时执行。一个稳定、隔离且兼容的环境是所有后续步骤成功的基础。

#### 1. 创建Conda环境 (关键步骤)

为了从根源上避免本地Python环境与系统ROS 2环境的库版本冲突（特别是Python版本和C++标准库），我们**必须**使用Conda创建一个干净、隔离且版本精确的虚拟环境。

```bash
# 1. 创建一个使用Python 3.10的新环境。
#    --name backend: 指定环境名称。
#    python=3.10:    指定Python版本，必须与ROS 2 Humble要求的版本一致。
#    --channel conda-forge: 使用conda-forge社区源，其包通常有更好的兼容性。
#    --no-default-packages: 关键！不安装Conda默认的包（如libgcc），避免与系统ROS 2的C++库冲突。
conda create --name backend --channel conda-forge --no-default-packages python=3.10

# 2. 激活新创建的环境
conda activate backend
```

#### 2. 安装所有Python依赖

在激活`backend`环境后，使用`pip`一次性安装所有依赖。`requirements.txt`已包含**运行时**（如fastapi, rclpy）和**编译时**（如empy, catkin-pkg, lark）所需的所有库。

```bash
# 确保在项目根目录 (drone/) 下执行
pip install -r backend_service/requirements.txt
```

#### 3. 编译ROS 2接口

为了让后端服务能够像导入普通Python包一样导入我们自定义的Action接口 (`drone_interfaces`)，你需要先使用`colcon`对其进行编译。

```bash
# 确保在项目根目录 (drone/) 下执行
colcon build
```
成功后，您会看到`build/`, `install/`, `log/`三个新目录。这一步会将`.action`文件转换为Python和C++代码。

---

### 阶段二：数据处理流水线

此阶段为RAG系统准备数据，让LLM能够理解任务环境。

#### 1. 准备原始数据

将你的原始数据文件（例如，`.world`, `.json` 文件等）放入 `tools/map/` 目录中。

#### 2. 数据预处理

运行脚本将原始数据“翻译”成自然语言知识。

```bash
# 确保在项目根目录 (drone/) 下，并已激活backend环境
cd tools
python build_knowledge_base.py
```
该脚本会扫描 `tools/map/` 目录，并在 `tools/knowledge_base/` 目录下生成对应的 `_knowledge.ndjson` 文件。

#### 3. 数据入库（Ingestion）

运行脚本将处理好的知识加载到向量数据库中。

```bash
# 仍在tools/目录下执行
python ingest.py
```
该脚本会自动扫描 `tools/knowledge_base/` 目录，并将数据存入 `tools/vector_store/` 目录中。

---

### 阶段三：服务启动与测试

完成前两个阶段后，即可启动并测试后端服务。

#### 1. 启动后端服务

启动服务的关键在于**按顺序激活环境**：先激活ROS 2工作空间，再激活Conda环境。

```bash
# 1. 切换到项目根目录
cd /path/to/your/drone

# 2. 激活ROS 2编译环境
#    作用：将我们编译好的`drone_interfaces`包的路径告知系统，否则Python会报`ModuleNotFoundError`。
#    注意：此命令必须在每次打开新终端时执行一次。
source install/setup.bash

# 3. 激活Conda Python环境
conda activate backend

# 4. 启动FastAPI服务
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接口测试

我们提供了一个脚本来验证核心的“任务生成”功能。

**打开一个新的终端**，并执行以下命令：

```bash
# 1. 切换到项目根目录
cd /path/to/your/drone

# 2. 激活Conda环境
conda activate backend

# 3. 运行测试脚本
cd tools/
python test_api.py
```
如果一切正常，您将在终端看到一系列 `PASS` 信息，以及从服务器返回的Pytree JSON。

#### 3. API接口使用说明

---

## 故障排除 / 常见问题 (FAQ)

以下是在配置和运行此项目时可能遇到的一些常见问题及其解决方案。

#### **Q1: 启动服务时报错 `ModuleNotFoundError: No module named 'drone_interfaces'`**

- **原因**: 您当前的终端环境没有加载ROS 2工作空间的路径。仅仅激活Conda环境是不够的。
- **解决方案**: 严格遵循“启动后端服务”章节的说明，在激活Conda环境**之前**，必须先运行 `source install/setup.bash` 命令。

#### **Q2: `colcon build` 编译失败，提示 `ModuleNotFoundError: No module named 'em'`, `'catkin_pkg'`, 或 `'lark'`**

- **原因**: 您的Python环境中缺少ROS 2编译代码时所必需的依赖包。
- **解决方案**: 我们已将所有已知的编译时依赖（`empy`, `catkin-pkg`, `lark`等）添加到了`requirements.txt`中。请确保您已激活正确的Conda环境，然后运行 `pip install -r backend_service/requirements.txt` 来安装它们。

#### **Q3: 启动服务时报错 `ImportError: ... GLIBCXX_... not found` 或 `ModuleNotFoundError: No module named 'rclpy._rclpy_pybind11'`**

- **原因**: 您的Conda环境与系统ROS 2环境存在核心库冲突。最常见的原因是Python版本不匹配（例如，Conda是Python 3.11而ROS 2 Humble需要3.10），或者Conda自带的C++库与系统库冲突。
- **解决方案**: 这是最棘手的环境问题。最可靠的解决方法是彻底删除当前的Conda环境 (`conda env remove --name backend`)，然后严格按照本文档「环境设置」章节的说明，用正确的命令 (`conda create --name backend --channel conda-forge --no-default-packages python=3.10`) 重建一个干净、兼容的环境。

#### **Q4: 服务启动时，日志显示正在从网络上下载模型（例如 `all-MiniLM-L6-v2`）**

- **原因**: 后端服务在连接向量数据库时，没有正确指定使用远程嵌入模型，导致ChromaDB退回到默认的、需要下载模型的本地嵌入函数。
- **解决方案**: 此问题在当前代码中**已被修复**。`backend_service/src/py_tree_generator.py`现在会正确地将远程嵌入函数实例传递给ChromaDB。如果您在自己的代码中遇到此问题，请检查您的`get_collection`调用。

#### **Q5: 服务启动时，日志停在 `waiting for action server...`，无法访问API**

- **原因**: 代码中存在阻塞式的`wait_for_server()`调用，它会一直等待直到无人机端的Action服务器上线，从而卡住了Web服务的启动流程。
- **解决方案**: 此问题在当前代码中**已被修复**。`backend_service/src/ros2_client.py`现在使用非阻塞的方式初始化，并在发送任务时检查服务器是否可用。

##### **A. 生成任务计划**

接收自然语言指令，返回生成的行为树（py_tree）JSON。

- **Endpoint**: `POST /generate_plan`
- **Request Body**:
  ```json
  {
    "user_prompt": "无人机起飞到10米，然后前往机库，最后降落。"
  }
  ```
- **Success Response（复杂模式）**:
  ```json
  {
    "root": { ... },
    "plan_id": "some-unique-id",
    "visualization_url": "/static/py_tree.png"
  }
  ```
- **Success Response（简单模式）**:
  ```json
  {
    "mode": "simple",
    "action": { "name": "takeoff", "params": { "altitude": 10.0 } },
    "plan_id": "some-unique-id",
    "visualization_url": "/static/py_tree.png"
  }
  ```

##### **B. 查看任务可视化**

获取最新生成的行为树的可视化图像。

- **Endpoint**: `GET /static/py_tree.png`
- **Usage**: 在浏览器中直接打开 `http://<服务器IP>:8000/static/py_tree.png` 即可查看。每次成功调用 `/generate_plan` 后，该图像都会被更新。

##### **C. 执行任务**

接收一个py_tree JSON，下发给无人机执行（当前为模拟执行）。

- **Endpoint**: `POST /execute_mission`
- **Request Body**: (使用 `/generate_plan` 返回的 `root` 对象)
  ```json
  {
    "py_tree": {
      "root": { ... }
    }
  }
  ```
- **Response**:
  ```json
  {
    "status": "execution_started"
  }
  ```

##### **D. 接收实时状态**

通过WebSocket连接，实时接收无人机在执行任务时的状态反馈。

- **Endpoint**: `WS /ws/status`
- **Usage**: 使用任意WebSocket客户端连接到 `ws://<服务器IP>:8000/ws/status`。当任务执行时，服务器会主动推送JSON消息，例如：
  ```json
  {"node_id": "takeoff_node_1", "status": 0} // 0: RUNNING
  {"node_id": "takeoff_node_1", "status": 1} // 1: SUCCESS
  ```
 