This commit is contained in:
2026-02-20 23:04:09 +08:00
parent 43d69d5a99
commit 5d8412bcb6
244 changed files with 9304 additions and 1463 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -2165,3 +2165,7 @@
[2025-12-06 13:42:34] ✅ Test completed successfully
[2025-12-06 13:42:34] ================================================================================
[2025-12-06 13:42:34]
[2026-02-03 22:26:39] ================================================================================
[2026-02-03 22:26:39] --- API Test: Generate Plan ---
[2026-02-03 22:26:39] URL: http://127.0.0.1:8000/generate_plan
[2026-02-03 22:26:39] Sending Prompt: "无人机起飞到80米高度后先移动至搜索区搜索并锁定任一红色车辆跟踪接近距离目标车辆10m后进行拍照完成拍照后返回。"

View File

@@ -1,157 +0,0 @@
import xml.etree.ElementTree as ET
import json
from pathlib import Path
import logging
import os
# --- 配置日志 ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def process_osm_json(input_path: Path) -> list[str]:
"""
处理OpenStreetMap的JSON文件返回描述性句子列表。
"""
logging.info(f"正在以OSM JSON格式处理文件: {input_path.name}")
descriptions = []
try:
with open(input_path, 'r', encoding='utf-8') as f:
data = json.load(f)
except (json.JSONDecodeError, IOError) as e:
logging.error(f"读取或解析 {input_path.name} 时出错: {e}")
return []
elements = data.get('elements', [])
if not elements:
return []
nodes_map = {node['id']: node for node in elements if node.get('type') == 'node'}
ways = [elem for elem in elements if elem.get('type') == 'way']
for way in ways:
tags = way.get('tags', {})
if 'name' not in tags:
continue
way_name = tags.get('name')
way_nodes_ids = way.get('nodes', [])
if not way_nodes_ids:
continue
total_lat, total_lon, node_count = 0, 0, 0
for node_id in way_nodes_ids:
node_info = nodes_map.get(node_id)
if node_info:
total_lat += node_info.get('lat', 0)
total_lon += node_info.get('lon', 0)
node_count += 1
if node_count == 0:
continue
center_lat = total_lat / node_count
center_lon = total_lon / node_count
sentence = f"在地图上有一个名为 '{way_name}' 的地点或区域"
other_tags = {k: v for k, v in tags.items() if k != 'name'}
if other_tags:
tag_descs = [f"{key}'{value}'" for key, value in other_tags.items()]
sentence += f",它的{ ''.join(tag_descs) }"
sentence += f",其中心位置坐标大约在 ({center_lat:.6f}, {center_lon:.6f})。"
descriptions.append(sentence)
logging.info(f"{input_path.name} 提取了 {len(descriptions)} 条位置描述。")
return descriptions
def process_gazebo_world(input_path: Path) -> list[str]:
"""
处理Gazebo的.world文件返回描述性句子列表。
"""
logging.info(f"正在以Gazebo World格式处理文件: {input_path.name}")
descriptions = []
try:
tree = ET.parse(input_path)
root = tree.getroot()
except ET.ParseError as e:
logging.error(f"解析XML文件 {input_path.name} 失败: {e}")
return []
models = root.findall('.//model')
for model in models:
model_name = model.get('name')
pose_element = model.find('pose')
if model_name and pose_element is not None and pose_element.text:
try:
pose_values = [float(p) for p in pose_element.text.strip().split()]
sentence = (
f"仿真环境中有一个名为 '{model_name}' 的物体,"
f"其位置和姿态(x, y, z, roll, pitch, yaw)为: {pose_values}"
)
descriptions.append(sentence)
except (ValueError, IndexError):
logging.warning(f"跳过模型 '{model_name}'因其pose格式不正确。")
logging.info(f"{input_path.name} 提取了 {len(descriptions)} 个物体信息。")
return descriptions
def main():
"""
主函数扫描源数据目录为每个文件生成独立的NDJSON知识库。
"""
script_dir = Path(__file__).resolve().parent
# 输入源: tools/map/
source_data_dir = script_dir / 'map'
# 输出目录: tools/knowledge_base/
output_knowledge_base_dir = script_dir / 'knowledge_base'
if not source_data_dir.exists():
logging.error(f"源数据目录不存在: {source_data_dir}")
return
# 确保输出目录存在
output_knowledge_base_dir.mkdir(parents=True, exist_ok=True)
total_files_processed = 0
logging.info(f"--- 开始扫描源数据目录: {source_data_dir} ---")
for file_path in source_data_dir.iterdir():
if not file_path.is_file():
continue
descriptions = []
if file_path.suffix == '.json':
descriptions = process_osm_json(file_path)
elif file_path.suffix == '.world':
descriptions = process_gazebo_world(file_path)
else:
logging.warning(f"跳过不支持的文件类型: {file_path.name}")
continue
if not descriptions:
logging.warning(f"未能从 {file_path.name} 提取有效信息,跳过生成文件。")
continue
output_filename = file_path.stem + '_knowledge.ndjson'
output_path = output_knowledge_base_dir / output_filename
try:
with open(output_path, 'w', encoding='utf-8') as f:
for sentence in descriptions:
json_record = {"text": sentence}
f.write(json.dumps(json_record, ensure_ascii=False) + '\n')
logging.info(f"成功为 '{file_path.name}' 生成知识库文件: {output_path.name}")
total_files_processed += 1
except IOError as e:
logging.error(f"写入输出文件 '{output_path.name}' 失败: {e}")
logging.info("--- 数据处理完成 ---")
if total_files_processed > 0:
logging.info(f"共为 {total_files_processed} 个源文件生成了知识库。")
else:
logging.warning("未生成任何知识库文件。")
if __name__ == "__main__":
main()

View File

@@ -1,194 +0,0 @@
# 该代码用于将本地知识库中的文档导入到ChromaDB中并使用远程嵌入模型进行向量化
import os
from pathlib import Path
import chromadb
# from chromadb.utils import embedding_functions - 不再需要
from chromadb.api.types import Documents, EmbeddingFunction, Embeddings, Embeddable
from unstructured.partition.auto import partition
from rich.progress import track
import logging
import requests # 导入requests
import json # 导入json模块
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# --- 配置 ---
# 获取脚本所在目录,确保路径的正确性
SCRIPT_DIR = Path(__file__).resolve().parent
KNOWLEDGE_BASE_DIR = SCRIPT_DIR / "knowledge_base"
VECTOR_STORE_DIR = SCRIPT_DIR / "vector_store"
COLLECTION_NAME = "drone_docs"
# EMBEDDING_MODEL_NAME = "bge-small-zh-v1.5" # 不再需要,模型名在函数内部处理
# --- 自定义远程嵌入函数 ---
class RemoteEmbeddingFunction(EmbeddingFunction[Embeddable]):
"""
一个使用远程、兼容OpenAI API的嵌入服务的嵌入函数。
"""
def __init__(self, api_url: str):
self._api_url = api_url
logging.info(f"自定义嵌入函数已初始化,将连接到: {self._api_url}")
def __call__(self, input: Embeddable) -> Embeddings:
"""
对输入的文档进行嵌入。
"""
# 我们的服务只能处理文本,所以检查输入是否为字符串列表
if not isinstance(input, list) or not all(isinstance(doc, str) for doc in input):
logging.error("此嵌入函数仅支持字符串列表(文档)作为输入。")
return []
try:
# 移除 "model" 参数因为embedding服务可能不需要它
response = requests.post(
self._api_url,
json={"input": input},
headers={"Content-Type": "application/json"}
)
response.raise_for_status() # 如果请求失败则抛出HTTPError
# 按照OpenAI API的格式解析返回的嵌入向量
data = response.json().get("data", [])
if not data:
raise ValueError("API响应中没有找到'data'字段或'data'为空")
embeddings = [item['embedding'] for item in data]
return embeddings
except requests.RequestException as e:
logging.error(f"调用嵌入API失败: {e}")
# 返回一个空列表或根据需要处理错误
return []
except (ValueError, KeyError) as e:
logging.error(f"解析API响应失败: {e}")
logging.error(f"收到的响应内容: {response.text}")
return []
def get_documents(directory: Path):
"""从知识库目录加载所有文档并进行切分"""
documents = []
logging.info(f"'{directory}' 加载文档...")
for file_path in directory.rglob("*"):
if file_path.is_file() and not file_path.name.startswith('.'):
try:
# 对简单文本文件直接读取
if file_path.suffix in ['.txt', '.md']:
text = file_path.read_text(encoding='utf-8')
documents.append({
"text": text,
"metadata": {"source": str(file_path.name)}
})
logging.info(f"成功处理文本文件: {file_path.name}")
# 特别处理常规的JSON文件
elif file_path.suffix == '.json':
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
if 'elements' in data and isinstance(data['elements'], list):
for element in data['elements']:
documents.append({
"text": json.dumps(element, ensure_ascii=False),
"metadata": {"source": str(file_path.name)}
})
logging.info(f"成功处理JSON文件: {file_path.name}, 提取了 {len(data['elements'])} 个元素。")
else:
documents.append({
"text": json.dumps(data, ensure_ascii=False),
"metadata": {"source": str(file_path.name)}
})
logging.info(f"成功处理JSON文件: {file_path.name} (作为单个文档)")
# 新增:专门处理我们生成的 NDJSON 文件
elif file_path.suffix == '.ndjson':
with open(file_path, 'r', encoding='utf-8') as f:
count = 0
for line in f:
try:
record = json.loads(line)
if 'text' in record and isinstance(record['text'], str):
documents.append({
"text": record['text'],
"metadata": {"source": str(file_path.name)}
})
count += 1
except json.JSONDecodeError:
logging.warning(f"跳过无效的JSON行: {line.strip()}")
if count > 0:
logging.info(f"成功处理NDJSON文件: {file_path.name}, 提取了 {count} 个文档。")
# 对其他所有文件类型使用unstructured
else:
elements = partition(filename=str(file_path))
for element in elements:
documents.append({
"text": element.text,
"metadata": {"source": str(file_path.name)}
})
logging.info(f"成功处理文件: {file_path.name} (使用unstructured)")
except Exception as e:
logging.error(f"处理文件 {file_path.name} 失败: {e}")
return documents
def main():
"""主函数,执行文档入库流程"""
if not KNOWLEDGE_BASE_DIR.exists():
KNOWLEDGE_BASE_DIR.mkdir(parents=True)
logging.warning(f"知识库目录不存在,已自动创建: {KNOWLEDGE_BASE_DIR}")
logging.warning("请向该目录中添加您的知识文件(如 .txt, .pdf, .md)。")
return
# 1. 加载并切分文档
docs_to_ingest = get_documents(KNOWLEDGE_BASE_DIR)
if not docs_to_ingest:
logging.warning("在知识库中未找到可处理的文档。")
return
# 2. 初始化ChromaDB客户端和远程嵌入函数
orin_ip = os.getenv("ORIN_IP", "localhost")
embedding_api_url = f"http://{orin_ip}:8090/v1/embeddings"
logging.info(f"正在初始化远程嵌入函数,目标服务地址: {embedding_api_url}")
embedding_func = RemoteEmbeddingFunction(api_url=embedding_api_url)
client = chromadb.PersistentClient(path=str(VECTOR_STORE_DIR))
# 3. 创建或获取集合
logging.info(f"正在访问ChromaDB集合: {COLLECTION_NAME}")
collection = client.get_or_create_collection(
name=COLLECTION_NAME,
embedding_function=embedding_func
)
# 4. 将文档向量化并存入数据库
logging.info(f"开始将 {len(docs_to_ingest)} 个文档块入库...")
# 为了避免重复添加,可以先检查
# (这里为了简单,我们每次都重新添加,生产环境需要更复杂的逻辑)
doc_texts = [doc['text'] for doc in docs_to_ingest]
metadatas = [doc['metadata'] for doc in docs_to_ingest]
ids = [f"doc_{KNOWLEDGE_BASE_DIR.name}_{i}" for i in range(len(doc_texts))]
try:
# ChromaDB的add方法会自动处理嵌入
collection.add(
documents=doc_texts,
metadatas=metadatas,
ids=ids
)
logging.info("所有文档块已成功入库!")
except Exception as e:
logging.error(f"向ChromaDB添加文档时出错: {e}")
# 验证一下
count = collection.count()
logging.info(f"数据库中现在有 {count} 个条目。")
print("\n✅ 数据入库完成!")
print(f"知识库位于: {KNOWLEDGE_BASE_DIR}")
print(f"向量数据库位于: {VECTOR_STORE_DIR}")
if __name__ == "__main__":
main()

View File

@@ -1,39 +1,62 @@
# RAG & Map Tools
# RAG 工具说明
该目录包含了地图构建、知识库生成和向量数据库管理的相关工具
该目录负责知识构建与向量入库
## 目录结构
- **knowledge_base/**: 存放源文档数据。
- 支持格式: `.txt`, `.md`, `.pdf`
- 生成格式: `.json`, `.ndjson` (由 `build_knowledge_base.py` 生成)
- **map/**: 存放地图原始数据。
- `.osm` (OpenStreetMap 数据)
- `.world` (Gazebo 仿真环境数据)
- **vector_store/**: ChromaDB 向量数据库的持久化存储目录
## 脚本说明
### 1. `build_knowledge_base.py`
**功能**: 处理 `map/` 目录下的地图文件,提取地理信息和语义描述,生成知识库文件到 `knowledge_base/` 目录。
**使用方法**:
```bash
python build_knowledge_base.py
```text
tools/rag/
├── map/ # 原始地图数据
├── knowledge_base/
│ ├── location/ # 地点知识(可选分目录)
├── pattern/ # 模式知识(任务模板)
├── rules/ # 规则知识
└── *.ndjson
├── vector_store/ # Chroma 持久化目录
├── build_knowledge_base.py # 从 map 构建 ndjson
└── ingest.py # 入库到 Chroma多集合 + 兼容集合)
```
### 2. `ingest.py`
**功能**: 读取 `knowledge_base/` 中的所有文档调用嵌入模型Embedding Model将其向量化并存入 `vector_store/` 中的 ChromaDB 数据库。
**使用方法**:
## 运行前准备
```bash
python ingest.py
cd /home/huangfukk/DronePlanning
source backend_service/venv/bin/activate
export ORIN_IP="localhost"
```
**依赖**: 需要确保后端嵌入服务(如 `llama-server`)已启动,或者配置正确的 `ORIN_IP` 环境变量。
## 工作流
1. 将地图文件放入 `map/`
2. 运行 `build_knowledge_base.py` 生成文本描述。
3. 将其他补充文档放入 `knowledge_base/`
4. 运行 `ingest.py` 构建向量索引。
并确保 embedding 服务可用(默认 `8090`
```bash
curl -s http://localhost:8090/v1/embeddings \
-H "Content-Type: application/json" \
-d '{"input":["test"]}'
```
## 完整流程(可直接执行)
```bash
cd /home/huangfukk/DronePlanning
source backend_service/venv/bin/activate
# 1) 由 map 构建知识文本
python tools/rag/build_knowledge_base.py
# 2) 入库
python tools/rag/ingest.py
```
## 入库结果
`ingest.py` 会同时写入:
- 兼容集合:`drone_docs`
- 新集合:`location_kb``pattern_kb``rules_kb`
并在 metadata 中写入 `kb_type`,支持检索时按知识域筛选。
## 常见问题
- **Chroma 初始化异常**:先确认使用的是项目 venv再检查 `tools/rag/vector_store/` 是否损坏(备份后重建)。
- **连接 embedding 失败**:检查 `ORIN_IP`、端口 8090、以及模型服务是否已启动。

Binary file not shown.

View File

@@ -5,7 +5,6 @@ import chromadb
# from chromadb.utils import embedding_functions - 不再需要
from chromadb.api.types import Documents, EmbeddingFunction, Embeddings, Embeddable
from unstructured.partition.auto import partition
from rich.progress import track
import logging
import requests # 导入requests
import json # 导入json模块
@@ -19,6 +18,11 @@ SCRIPT_DIR = Path(__file__).resolve().parent
KNOWLEDGE_BASE_DIR = SCRIPT_DIR / "knowledge_base"
VECTOR_STORE_DIR = SCRIPT_DIR / "vector_store"
COLLECTION_NAME = "drone_docs"
COLLECTIONS_BY_KB_TYPE = {
"location": "location_kb",
"pattern": "pattern_kb",
"rules": "rules_kb",
}
# EMBEDDING_MODEL_NAME = "bge-small-zh-v1.5" # 不再需要,模型名在函数内部处理
# --- 自定义远程嵌入函数 ---
@@ -66,6 +70,18 @@ class RemoteEmbeddingFunction(EmbeddingFunction[Embeddable]):
return []
def _infer_kb_type(file_path: Path, base_dir: Path) -> str:
try:
rel = file_path.relative_to(base_dir)
except ValueError:
return "location"
if len(rel.parts) > 1:
first = rel.parts[0].lower()
if first in COLLECTIONS_BY_KB_TYPE:
return first
return "location"
def get_documents(directory: Path):
"""从知识库目录加载所有文档并进行切分"""
documents = []
@@ -74,11 +90,12 @@ def get_documents(directory: Path):
if file_path.is_file() and not file_path.name.startswith('.'):
try:
# 对简单文本文件直接读取
kb_type = _infer_kb_type(file_path, directory)
if file_path.suffix in ['.txt', '.md']:
text = file_path.read_text(encoding='utf-8')
documents.append({
"text": text,
"metadata": {"source": str(file_path.name)}
"metadata": {"source": str(file_path.name), "kb_type": kb_type}
})
logging.info(f"成功处理文本文件: {file_path.name}")
# 特别处理常规的JSON文件
@@ -89,13 +106,13 @@ def get_documents(directory: Path):
for element in data['elements']:
documents.append({
"text": json.dumps(element, ensure_ascii=False),
"metadata": {"source": str(file_path.name)}
"metadata": {"source": str(file_path.name), "kb_type": kb_type}
})
logging.info(f"成功处理JSON文件: {file_path.name}, 提取了 {len(data['elements'])} 个元素。")
else:
documents.append({
"text": json.dumps(data, ensure_ascii=False),
"metadata": {"source": str(file_path.name)}
"metadata": {"source": str(file_path.name), "kb_type": kb_type}
})
logging.info(f"成功处理JSON文件: {file_path.name} (作为单个文档)")
# 新增:专门处理我们生成的 NDJSON 文件
@@ -115,7 +132,7 @@ def get_documents(directory: Path):
text = json.dumps(record, ensure_ascii=False)
documents.append({
"text": text,
"metadata": {"source": str(file_path.name)}
"metadata": {"source": str(file_path.name), "kb_type": kb_type}
})
count += 1
except json.JSONDecodeError:
@@ -128,7 +145,7 @@ def get_documents(directory: Path):
for element in elements:
documents.append({
"text": element.text,
"metadata": {"source": str(file_path.name)}
"metadata": {"source": str(file_path.name), "kb_type": kb_type}
})
logging.info(f"成功处理文件: {file_path.name} (使用unstructured)")
except Exception as e:
@@ -159,38 +176,48 @@ def main():
client = chromadb.PersistentClient(path=str(VECTOR_STORE_DIR))
# 3. 创建或获取集合
logging.info(f"正在访问ChromaDB集合: {COLLECTION_NAME}")
collection = client.get_or_create_collection(
name=COLLECTION_NAME,
embedding_function=embedding_func
)
# 3. 创建或获取集合(兼容旧集合 + 新多知识库集合)
collections = {
"fallback": client.get_or_create_collection(name=COLLECTION_NAME, embedding_function=embedding_func)
}
for kb_type, coll_name in COLLECTIONS_BY_KB_TYPE.items():
collections[kb_type] = client.get_or_create_collection(name=coll_name, embedding_function=embedding_func)
# 4. 将文档向量化并存入数据库
logging.info(f"开始将 {len(docs_to_ingest)} 个文档块入库...")
# 为了避免重复添加,可以先检查
# (这里为了简单,我们每次都重新添加,生产环境需要更复杂的逻辑)
doc_texts = [doc['text'] for doc in docs_to_ingest]
metadatas = [doc['metadata'] for doc in docs_to_ingest]
ids = [f"doc_{KNOWLEDGE_BASE_DIR.name}_{i}" for i in range(len(doc_texts))]
grouped_docs = {}
for doc in docs_to_ingest:
kb_type = doc["metadata"].get("kb_type", "location")
grouped_docs.setdefault(kb_type, []).append(doc)
# 先入旧集合,保持兼容
all_texts = [doc['text'] for doc in docs_to_ingest]
all_metadatas = [doc['metadata'] for doc in docs_to_ingest]
all_ids = [f"doc_{KNOWLEDGE_BASE_DIR.name}_{i}" for i in range(len(all_texts))]
try:
# ChromaDB的add方法会自动处理嵌入
collection.add(
documents=doc_texts,
metadatas=metadatas,
ids=ids
)
logging.info("所有文档块已成功入库!")
collections["fallback"].add(documents=all_texts, metadatas=all_metadatas, ids=all_ids)
logging.info("兼容集合 drone_docs 入库完成。")
except Exception as e:
logging.error(f"ChromaDB添加文档时出错: {e}")
logging.error(f"兼容集合添加文档时出错: {e}")
for kb_type, docs in grouped_docs.items():
if kb_type not in collections:
continue
doc_texts = [doc["text"] for doc in docs]
metadatas = [doc["metadata"] for doc in docs]
ids = [f"doc_{kb_type}_{i}" for i in range(len(doc_texts))]
try:
collections[kb_type].add(documents=doc_texts, metadatas=metadatas, ids=ids)
logging.info(f"{kb_type} 集合入库完成,条目数: {len(doc_texts)}")
except Exception as e:
logging.error(f"{kb_type} 集合添加文档时出错: {e}")
# 验证一下
count = collection.count()
logging.info(f"数据库中现在有 {count} 个条目。")
for name, collection in collections.items():
try:
count = collection.count()
logging.info(f"集合 {name} 当前条目数: {count}")
except Exception:
pass
print("\n✅ 数据入库完成!")
print(f"知识库位于: {KNOWLEDGE_BASE_DIR}")

View File

@@ -0,0 +1,6 @@
{"scene_id":"scene1_perimeter_window_ground","intent_type":"patrol_or_monitor","trigger_terms":["面前大楼","12米","外围","窗户"],"text":"地面起飞后到面前大楼约12米高度沿外围巡查打开窗户如发现窗户则拍照回传。","pattern_json":{"root":{"type":"Sequence","name":"Sequence","children":[{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},{"type":"action","name":"takeoff","params":{"altitude":12}},{"type":"action","name":"rotate_search","params":{"target_class":"window"}},{"type":"condition","name":"object_detected","params":{"target_class":"window"}},{"type":"action","name":"take_photos","params":{"target_class":"window","track_time":10}}]}}}
{"scene_id":"scene1_perimeter_person_air","intent_type":"patrol_or_monitor","trigger_terms":["这栋楼","空中","外围","人"],"text":"无人机已在空中,先调整高度后沿建筑外围巡查人员,发现目标后拍照。","pattern_json":{"root":{"type":"Sequence","name":"Sequence","children":[{"type":"action","name":"move_direction","params":{"direction":"up","distance":3}},{"type":"action","name":"rotate_search","params":{"target_class":"person"}},{"type":"condition","name":"object_detected","params":{"target_class":"person"}},{"type":"action","name":"take_photos","params":{"target_class":"person","track_time":10}}]}}}
{"scene_id":"scene1_perimeter_trash_ground","intent_type":"patrol_or_monitor","trigger_terms":["面前大楼","杂物","外围"],"text":"地面起飞至指定高度后沿楼体外围侦察杂物堆积并拍照。","pattern_json":{"root":{"type":"Sequence","name":"Sequence","children":[{"type":"action","name":"takeoff","params":{"altitude":12}},{"type":"action","name":"rotate_search","params":{"target_class":"garbage"}},{"type":"condition","name":"object_detected","params":{"target_class":"garbage"}},{"type":"action","name":"take_photos","params":{"target_class":"garbage","track_time":8}}]}}}
{"scene_id":"scene4_named_place_search_photo","intent_type":"search_and_photo","trigger_terms":["广场","查找","拍照"],"text":"前往命名地点后搜索目标并拍照。","pattern_json":{"root":{"type":"Sequence","name":"Sequence","children":[{"type":"action","name":"fly_to_waypoint","params":{"x":0,"y":0,"z":10,"acceptance_radius":2}},{"type":"action","name":"rotate_search","params":{"target_class":"person"}},{"type":"condition","name":"object_detected","params":{"target_class":"person"}},{"type":"action","name":"take_photos","params":{"target_class":"person","track_time":8}}]}}}
{"scene_id":"scene4_named_place_monitor_return","intent_type":"return_or_land","trigger_terms":["广场南边","监控","返航"],"text":"先到达命名地点偏移区域执行监控,到时后返航。","pattern_json":{"root":{"type":"Sequence","name":"Sequence","children":[{"type":"action","name":"fly_to_waypoint","params":{"x":40,"y":80,"z":15,"acceptance_radius":2}},{"type":"action","name":"loiter","params":{"duration":300}},{"type":"action","name":"return_emergency","params":{"reason":"mission_complete"}}]}}}
{"scene_id":"scene4_named_place_confirm_then_action","intent_type":"search_and_photo","trigger_terms":["确认后","拍照","返航"],"text":"在命名区域发现目标后等待人工确认,再执行拍照或返航。","pattern_json":{"root":{"type":"Sequence","name":"Sequence","children":[{"type":"action","name":"fly_to_waypoint","params":{"x":10,"y":10,"z":10,"acceptance_radius":2}},{"type":"action","name":"rotate_search","params":{"target_class":"car"}},{"type":"condition","name":"object_detected","params":{"target_class":"car"}},{"type":"action","name":"manual_confirmation","params":{}},{"type":"action","name":"take_photos","params":{"target_class":"car","track_time":8}}]}}}

View File

@@ -0,0 +1,3 @@
{"rule_id":"rules_manual_confirmation","intent_type":"search_and_photo","trigger_terms":["我确认","等待确认","经允许"],"text":"仅当用户明确要求人工确认时才注入 manual_confirmation 节点;否则禁止主动添加。"}
{"rule_id":"rules_direction_priority","intent_type":"generic_mission","trigger_terms":["东边","西边","北边","南边","米"],"text":"当只有方向+距离且无具体地点名词时,优先使用 move_direction不要生成 fly_to_waypoint。"}
{"rule_id":"rules_return_emergency_scope","intent_type":"return_or_land","trigger_terms":["返航","回到","去"],"text":"有明确目的地时使用 fly_to_waypoint无明确目的地的立即返航才使用 return_emergency。"}

View File

@@ -1,57 +1,51 @@
# Test & Validation Tools (Unified)
# 测试与验证工具
该目录包含用于测试无人机规划系统、API 接口及 LLM 服务的集成验证工具集
该目录用于接口回归、批量稳定性测试和结果导出
## 🚀 快速开始
使用统一入口脚本启动交互式菜单:
## 快速开始(完整命令)
```bash
python run_tests.py
cd /home/huangfukk/DronePlanning
source backend_service/venv/bin/activate
python tools/test_validate/run_tests.py
```
## 🛠️ 测试模式
### 1. 交互式单次测试 (Mode 1)
- **场景**: 快速验证单条指令,调试 Prompt。
- **操作**: 在终端输入自然语言指令,即时获取结果。
- **输出**: `validation/temporary/{指令名}/`
- `response.json`: 完整 API 响应
- `plan.png`: 可视化任务树
- `process.log`: 请求与响应日志
### 2. 批量/场景测试 (Mode 2)
- **场景**:
- **场景测试**: 验证一组预定义指令的正确性(默认运行 1 次)。
- **稳定性测试**: 对同一组指令进行高频重复测试(如运行 10 次),检测成功率和延迟抖动。
- **操作**:
1. 选择指令文件(位于 `instructions/` 目录)。
2. 输入每条指令的运行次数(默认 1
- **输出**: `validation/{时间戳}/`
- `test_summary.csv`: 统计摘要(成功率、平均耗时)
- `test_details.csv`: 每次运行的详细记录
- `instructions_backup.txt`: 本次测试使用的指令备份
- `{指令名}/`: 包含所有运行的 `.json``.png` 产物
## 📂 目录结构
## 目录说明
```text
tools/test_validate/
├── instructions/ # 指令集文件 (.txt)
├── modules/ # 功能模块
│ ├── api_client.py # API 客户端核心
│ ├── interactive_test.py # 交互式测试逻辑
│ ├── batch_runner.py # 批量测试逻辑
│ ├── visualizer.py # 可视化工具库
│ ├── llm_tester.py # LLM 连接测试
│ └── drone_uploader.py # 任务上传工具
├── validation/ # 测试产物输出
│ ├── temporary/ # 交互式测试结果
│ └── {时间戳}/ # 批量测试结果
└── run_tests.py # 主程序入口
├── instructions/ # 指令集.txt
├── modules/ # API 客户端、批量执行、可视化等
├── validation/ # 输出结果目录
└── run_tests.py # 统一入口
```
## 📄 配置文件
## 使用方式
- **instructions/validate_instructions.txt**: 默认的预定义场景指令集。
- 您可以在 `instructions/` 下添加任意 `.txt` 文件,测试时会在菜单中自动列出供选择。
- Mode 1交互式单条测试快速调试
- Mode 2批量测试场景回归/稳定性)
输出默认在 `tools/test_validate/validation/`,包括:
- `test_summary.csv`
- `test_details.csv`
- 每条指令的 `response.json``plan.png`
## 典型回归流程
```bash
cd /home/huangfukk/DronePlanning
source backend_service/venv/bin/activate
# 先确保后端已启动
./start_all.sh status
# 运行测试菜单
python tools/test_validate/run_tests.py
```
## 常见问题
- 后端未启动:先执行 `./start_all.sh start``./start_all_vllm.sh start`
- API 超时:查看 `logs/fastapi.log` 与模型日志
- 结果目录为空:确认菜单中已实际执行测试轮次

View File

@@ -1,10 +1,10 @@
无人机当前在地面,到广场查找穿色衣服的男子,找到后近距离拍照。
无人机当前在空中,回到广场,对戴帽子的人进行拍照。
无人机当前在空中,去广场西边200米对过往的公交车拍张照,然后返航。
无人机当前在地面,到广场查找绿色公交车,看见了拍个照片。
无人机当前在空中,搜索小汽车,搜索到了我确认后再决定要不要拍照
无人机当前在空中,往广场西边飞200米持续监控5分钟发现人就拍照告诉我到时间可以返航。
无人机当前在地面,到广场边上的施工区域内,发现未戴安全帽的飞近后拍照。
无人机当前在空中,紧急回到广场,看见了红绿灯之后直接降落。
无人机当前在地面,快速去往东边60米身穿白色衣服,头戴帽子在挟持他人的人进行拍照。
无人机当前在空中,离色衣服戴帽子的人太远了照片看不清贴近到3米距离拍拍完可以直接返航。
无人机当前在地面,到飞行场地查找穿色衣服的男子,找到后近距离拍照。
无人机当前在空中,回到飞行场地,对戴帽子的人进行拍照。
无人机当前在空中,去飞行场地西边50米对过往穿红色衣服的人拍张照,然后返航。
无人机当前在地面,到飞行场地查找长头发的人,看见了拍个照片。
无人机当前在空中,搜索穿红色衣服的人,搜索到了拍张照,我确认后再决定要不要返航
无人机当前在空中,往飞行场地西边飞50米持续监控5分钟发现人就拍照告诉我到时间可以返航。
无人机当前在地面,到飞行场地,发现未带帽子的飞近后拍照。
无人机当前在空中,紧急回到飞行场地,看见了之后直接降落。
无人机当前在地面,快速去往东边30米身穿白色衣服,头戴帽子的男子在挟持他人,对其进行拍照。
无人机当前在空中,离色衣服戴帽子的人太远了照片看不清贴近到3米距离拍拍完可以直接返航。

View File

@@ -0,0 +1,10 @@
无人机当前在地面,到飞行场地查找穿红色衣服的男子,找到后近距离拍照。
无人机当前在空中,回到飞行场地,对戴帽子的人进行拍照。
无人机当前在空中去飞行场地西边50米对过往穿红色衣服的人拍张照然后返航。
无人机当前在地面,到飞行场地查找长头发的人,看见了拍个照片。
无人机当前在空中,搜索穿红色衣服的人,搜索到了拍张照,我确认后再决定要不要返航。
无人机当前在空中往飞行场地西边飞50米持续监控5分钟发现人就拍照告诉我到时间可以返航。
无人机当前在地面,到飞行场地,发现未带帽子的飞近后拍照。
无人机当前在空中,紧急回到飞行场地,看见了树之后直接降落。
无人机当前在地面快速去往东边30米有身穿白色衣服头戴帽子的男子在挟持他人对其进行拍照。
无人机当前在空中离白色衣服戴帽子的人太远了照片看不清贴近到3米距离拍拍完可以直接返航。

View File

@@ -0,0 +1,11 @@
instruction,run_id,success,latency,error
无人机当前在地面,到飞行场地查找穿红色衣服的男子,找到后近距离拍照。,1,True,13.293935775756836,
无人机当前在空中,回到飞行场地,对戴帽子的人进行拍照。,1,True,10.266583442687988,
无人机当前在空中去飞行场地西边50米对过往穿红色衣服的人拍张照然后返航。,1,True,11.274949550628662,
无人机当前在地面,到飞行场地查找长头发的人,看见了拍个照片。,1,True,1.6364917755126953,
无人机当前在空中,搜索穿红色衣服的人,搜索到了拍张照,我确认后再决定要不要返航。,1,True,10.27147102355957,
无人机当前在空中往飞行场地西边飞50米持续监控5分钟发现人就拍照告诉我到时间可以返航。,1,True,13.89813232421875,
无人机当前在地面,到飞行场地,发现未带帽子的飞近后拍照。,1,True,3.24064302444458,
无人机当前在空中,紧急回到飞行场地,看见了树之后直接降落。,1,True,19.873026847839355,
无人机当前在地面快速去往东边30米有身穿白色衣服头戴帽子的男子在挟持他人对其进行拍照。,1,True,13.099174499511719,
无人机当前在空中离白色衣服戴帽子的人太远了照片看不清贴近到3米距离拍拍完可以直接返航。,1,True,8.792617082595825,
1 instruction run_id success latency error
2 无人机当前在地面,到飞行场地查找穿红色衣服的男子,找到后近距离拍照。 1 True 13.293935775756836
3 无人机当前在空中,回到飞行场地,对戴帽子的人进行拍照。 1 True 10.266583442687988
4 无人机当前在空中,去飞行场地西边50米,对过往穿红色衣服的人拍张照,然后返航。 1 True 11.274949550628662
5 无人机当前在地面,到飞行场地查找长头发的人,看见了拍个照片。 1 True 1.6364917755126953
6 无人机当前在空中,搜索穿红色衣服的人,搜索到了拍张照,我确认后再决定要不要返航。 1 True 10.27147102355957
7 无人机当前在空中,往飞行场地西边飞50米,持续监控5分钟,发现人就拍照告诉我,到时间可以返航。 1 True 13.89813232421875
8 无人机当前在地面,到飞行场地,发现未带帽子的飞近后拍照。 1 True 3.24064302444458
9 无人机当前在空中,紧急回到飞行场地,看见了树之后直接降落。 1 True 19.873026847839355
10 无人机当前在地面,快速去往东边30米,有身穿白色衣服,头戴帽子的男子在挟持他人,对其进行拍照。 1 True 13.099174499511719
11 无人机当前在空中,离白色衣服戴帽子的人太远了照片看不清,贴近到3米距离拍,拍完可以直接返航。 1 True 8.792617082595825

View File

@@ -0,0 +1,11 @@
Instruction,Total Runs,Success Runs,Success Rate,Avg Latency
无人机当前在地面,到飞行场地查找穿红色衣服的男子,找到后近距离拍照。,1,1,100.0%,13.29s
无人机当前在空中,回到飞行场地,对戴帽子的人进行拍照。,1,1,100.0%,10.27s
无人机当前在空中去飞行场地西边50米对过往穿红色衣服的人拍张照然后返航。,1,1,100.0%,11.27s
无人机当前在地面,到飞行场地查找长头发的人,看见了拍个照片。,1,1,100.0%,1.64s
无人机当前在空中,搜索穿红色衣服的人,搜索到了拍张照,我确认后再决定要不要返航。,1,1,100.0%,10.27s
无人机当前在空中往飞行场地西边飞50米持续监控5分钟发现人就拍照告诉我到时间可以返航。,1,1,100.0%,13.90s
无人机当前在地面,到飞行场地,发现未带帽子的飞近后拍照。,1,1,100.0%,3.24s
无人机当前在空中,紧急回到飞行场地,看见了树之后直接降落。,1,1,100.0%,19.87s
无人机当前在地面快速去往东边30米有身穿白色衣服头戴帽子的男子在挟持他人对其进行拍照。,1,1,100.0%,13.10s
无人机当前在空中离白色衣服戴帽子的人太远了照片看不清贴近到3米距离拍拍完可以直接返航。,1,1,100.0%,8.79s
1 Instruction Total Runs Success Runs Success Rate Avg Latency
2 无人机当前在地面,到飞行场地查找穿红色衣服的男子,找到后近距离拍照。 1 1 100.0% 13.29s
3 无人机当前在空中,回到飞行场地,对戴帽子的人进行拍照。 1 1 100.0% 10.27s
4 无人机当前在空中,去飞行场地西边50米,对过往穿红色衣服的人拍张照,然后返航。 1 1 100.0% 11.27s
5 无人机当前在地面,到飞行场地查找长头发的人,看见了拍个照片。 1 1 100.0% 1.64s
6 无人机当前在空中,搜索穿红色衣服的人,搜索到了拍张照,我确认后再决定要不要返航。 1 1 100.0% 10.27s
7 无人机当前在空中,往飞行场地西边飞50米,持续监控5分钟,发现人就拍照告诉我,到时间可以返航。 1 1 100.0% 13.90s
8 无人机当前在地面,到飞行场地,发现未带帽子的飞近后拍照。 1 1 100.0% 3.24s
9 无人机当前在空中,紧急回到飞行场地,看见了树之后直接降落。 1 1 100.0% 19.87s
10 无人机当前在地面,快速去往东边30米,有身穿白色衣服,头戴帽子的男子在挟持他人,对其进行拍照。 1 1 100.0% 13.10s
11 无人机当前在空中,离白色衣服戴帽子的人太远了照片看不清,贴近到3米距离拍,拍完可以直接返航。 1 1 100.0% 8.79s

View File

@@ -0,0 +1,3 @@
{
"error": "在3次尝试后仍未能生成一个有效的Pytree。"
}

View File

@@ -0,0 +1,10 @@
无人机当前在地面,到飞行场地查找穿红色衣服的男子,找到后近距离拍照。
无人机当前在空中,回到飞行场地,对戴帽子的人进行拍照。
无人机当前在空中去飞行场地西边50米对过往穿红色衣服的人拍张照然后返航。
无人机当前在地面,到飞行场地查找长头发的人,看见了拍个照片。
无人机当前在空中,搜索穿红色衣服的人,搜索到了拍张照,我确认后再决定要不要返航。
无人机当前在空中往飞行场地西边飞50米持续监控5分钟发现人就拍照告诉我到时间可以返航。
无人机当前在地面,到飞行场地,发现未带帽子的飞近后拍照。
无人机当前在空中,紧急回到飞行场地,看见了树之后直接降落。
无人机当前在地面快速去往东边30米有身穿白色衣服头戴帽子的男子在挟持他人对其进行拍照。
无人机当前在空中离白色衣服戴帽子的人太远了照片看不清贴近到3米距离拍拍完可以直接返航。

View File

@@ -0,0 +1,11 @@
instruction,run_id,success,latency,error
无人机当前在地面,到飞行场地查找穿红色衣服的男子,找到后近距离拍照。,1,True,14.206302165985107,
无人机当前在空中,回到飞行场地,对戴帽子的人进行拍照。,1,True,9.870139360427856,
无人机当前在空中去飞行场地西边50米对过往穿红色衣服的人拍张照然后返航。,1,True,11.775025844573975,
无人机当前在地面,到飞行场地查找长头发的人,看见了拍个照片。,1,True,10.768753051757812,
无人机当前在空中,搜索穿红色衣服的人,搜索到了拍张照,我确认后再决定要不要返航。,1,True,10.050657749176025,
无人机当前在空中往飞行场地西边飞50米持续监控5分钟发现人就拍照告诉我到时间可以返航。,1,True,13.982521057128906,
无人机当前在地面,到飞行场地,发现未带帽子的飞近后拍照。,1,True,12.104306697845459,
无人机当前在空中,紧急回到飞行场地,看见了树之后直接降落。,1,True,21.10497808456421,
无人机当前在地面快速去往东边30米有身穿白色衣服头戴帽子的男子在挟持他人对其进行拍照。,1,True,11.49827790260315,
无人机当前在空中离白色衣服戴帽子的人太远了照片看不清贴近到3米距离拍拍完可以直接返航。,1,True,9.288247346878052,
1 instruction run_id success latency error
2 无人机当前在地面,到飞行场地查找穿红色衣服的男子,找到后近距离拍照。 1 True 14.206302165985107
3 无人机当前在空中,回到飞行场地,对戴帽子的人进行拍照。 1 True 9.870139360427856
4 无人机当前在空中,去飞行场地西边50米,对过往穿红色衣服的人拍张照,然后返航。 1 True 11.775025844573975
5 无人机当前在地面,到飞行场地查找长头发的人,看见了拍个照片。 1 True 10.768753051757812
6 无人机当前在空中,搜索穿红色衣服的人,搜索到了拍张照,我确认后再决定要不要返航。 1 True 10.050657749176025
7 无人机当前在空中,往飞行场地西边飞50米,持续监控5分钟,发现人就拍照告诉我,到时间可以返航。 1 True 13.982521057128906
8 无人机当前在地面,到飞行场地,发现未带帽子的飞近后拍照。 1 True 12.104306697845459
9 无人机当前在空中,紧急回到飞行场地,看见了树之后直接降落。 1 True 21.10497808456421
10 无人机当前在地面,快速去往东边30米,有身穿白色衣服,头戴帽子的男子在挟持他人,对其进行拍照。 1 True 11.49827790260315
11 无人机当前在空中,离白色衣服戴帽子的人太远了照片看不清,贴近到3米距离拍,拍完可以直接返航。 1 True 9.288247346878052

View File

@@ -0,0 +1,11 @@
Instruction,Total Runs,Success Runs,Success Rate,Avg Latency
无人机当前在地面,到飞行场地查找穿红色衣服的男子,找到后近距离拍照。,1,1,100.0%,14.21s
无人机当前在空中,回到飞行场地,对戴帽子的人进行拍照。,1,1,100.0%,9.87s
无人机当前在空中去飞行场地西边50米对过往穿红色衣服的人拍张照然后返航。,1,1,100.0%,11.78s
无人机当前在地面,到飞行场地查找长头发的人,看见了拍个照片。,1,1,100.0%,10.77s
无人机当前在空中,搜索穿红色衣服的人,搜索到了拍张照,我确认后再决定要不要返航。,1,1,100.0%,10.05s
无人机当前在空中往飞行场地西边飞50米持续监控5分钟发现人就拍照告诉我到时间可以返航。,1,1,100.0%,13.98s
无人机当前在地面,到飞行场地,发现未带帽子的飞近后拍照。,1,1,100.0%,12.10s
无人机当前在空中,紧急回到飞行场地,看见了树之后直接降落。,1,1,100.0%,21.10s
无人机当前在地面快速去往东边30米有身穿白色衣服头戴帽子的男子在挟持他人对其进行拍照。,1,1,100.0%,11.50s
无人机当前在空中离白色衣服戴帽子的人太远了照片看不清贴近到3米距离拍拍完可以直接返航。,1,1,100.0%,9.29s
1 Instruction Total Runs Success Runs Success Rate Avg Latency
2 无人机当前在地面,到飞行场地查找穿红色衣服的男子,找到后近距离拍照。 1 1 100.0% 14.21s
3 无人机当前在空中,回到飞行场地,对戴帽子的人进行拍照。 1 1 100.0% 9.87s
4 无人机当前在空中,去飞行场地西边50米,对过往穿红色衣服的人拍张照,然后返航。 1 1 100.0% 11.78s
5 无人机当前在地面,到飞行场地查找长头发的人,看见了拍个照片。 1 1 100.0% 10.77s
6 无人机当前在空中,搜索穿红色衣服的人,搜索到了拍张照,我确认后再决定要不要返航。 1 1 100.0% 10.05s
7 无人机当前在空中,往飞行场地西边飞50米,持续监控5分钟,发现人就拍照告诉我,到时间可以返航。 1 1 100.0% 13.98s
8 无人机当前在地面,到飞行场地,发现未带帽子的飞近后拍照。 1 1 100.0% 12.10s
9 无人机当前在空中,紧急回到飞行场地,看见了树之后直接降落。 1 1 100.0% 21.10s
10 无人机当前在地面,快速去往东边30米,有身穿白色衣服,头戴帽子的男子在挟持他人,对其进行拍照。 1 1 100.0% 11.50s
11 无人机当前在空中,离白色衣服戴帽子的人太远了照片看不清,贴近到3米距离拍,拍完可以直接返航。 1 1 100.0% 9.29s

View File

@@ -0,0 +1,3 @@
{
"error": "在3次尝试后仍未能生成一个有效的Pytree。"
}

View File

@@ -0,0 +1,54 @@
{%- if tools %}
<|im_start|>system
{%- if messages[0].role == 'system' -%}
{{ messages[0].content }}
{%- else -%}
You are a helpful assistant.
{%- endif -%}
# Tools
You may call one or more functions to assist with the user query.
The function signatures are provided below:
<tools>
{%- for tool in tools %}
{{ tool | tojson }}
{%- endfor %}
</tools>
When you need to call a tool, respond with XML tags in this exact format:
<tool_call>
<function=tool_name>
<parameter=param1>value1</parameter>
<parameter=param2>value2</parameter>
</function>
</tool_call>
<|im_end|>
{%- else %}
{%- if messages[0].role == 'system' -%}
<|im_start|>system
{{ messages[0].content }}<|im_end|>
{%- endif -%}
{%- endif %}
{%- for message in messages %}
{%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
<|im_start|>{{ message.role }}
{{ message.content }}<|im_end|>
{%- elif message.role == "assistant" %}
<|im_start|>assistant
{%- if message.content %}
{{ message.content }}
{%- endif %}
<|im_end|>
{%- elif message.role == "tool" %}
<|im_start|>user
<tool_response>
{{ message.content }}
</tool_response>
<|im_end|>
{%- endif %}
{%- endfor %}
{%- if add_generation_prompt %}
<|im_start|>assistant
{%- endif %}