10 Commits

Author SHA1 Message Date
lulijing
e164148c5c 提示词分段编排解耦 2026-01-29 15:21:32 +08:00
lulijing
b12339ff73 新增方位解析function call 2026-01-29 14:27:56 +08:00
410d2e01e4 修改启动脚本 2026-01-29 10:27:36 +08:00
070e4f579d 提示词流程分类 2026-01-29 10:24:02 +08:00
dd066057b0 增加场景4示例 2026-01-27 16:38:06 +08:00
59c52f6b99 提示词修改 2026-01-22 17:38:24 +08:00
bce9203e01 进一步优化指令适配,指定温度采样参数 2026-01-21 13:23:49 +08:00
333fad40ac 修改为东南天坐标系 2026-01-20 09:49:52 +08:00
9538757047 修改坐标系 2026-01-19 15:20:15 +08:00
ffb9aee730 Add coordinate tool flow and tighten tool usage
Introduce ENU offset tool support with controlled tool-call handling, update prompts to gate coordinate tooling, and enable jinja tools in startup.
2026-01-19 15:01:04 +08:00
7219 changed files with 1032841 additions and 85733 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -1,54 +0,0 @@
你是一个严格的任务分类器。只输出一个JSON对象不要输出解释或多余文本。
根据用户指令与下述可用节点定义,判断其为“简单”或“复杂”。
- 简单:单一动作节点即可完成(例如"起飞""飞机自检""移动到某地(已给定坐标)"等),且无需行为树。
- 复杂:需要两个动作及以上、多步流程、搜索/检测/跟踪/评估、战损确认、或需要模板化任务结构。
输出格式(严格遵守):
{"mode":"simple"} 或 {"mode":"complex"}
—— 可用节点定义——
```json
{
"actions": [
{"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":"fly_sequence","params":{"waypoints":"list[dict] (e.g. [{'x':10,'y':20,'z':5}, ...])","coordinate_frame":"'global'/'local_enu'global:经纬度, local_enu:以起飞点为原点的东北天坐标系)","speed":"float,可选"}},
{"name":"move_direction","params":{"direction":"north/south/east/west/forward/backward/left/right","distance":"[1,10000],缺省持续移动","speed":"float,可选"}},
{"name":"approach_target","params":{"target_class":"string,要趋近的目标类别","description":"string,可选,目标属性描述","stop_distance":"float,期望的最终停止距离","speed":"float,可选,期望的逼近速度"}},
{"name":"rotate","params":{"angle":"float,无人机自身旋转角度(正数逆时针,负数顺时针)","angular_velocity":"rad/s,旋转角速度"}},
{"name":"rotate_search","params":{"target_class":"string,要搜寻的目标类别","description":"string,可选,目标属性描述","step_angle":"float,可选,每一步旋转的角度","total_rotation":"float,可选,总共旋转搜索的角度"}},
{"name":"manual_confirmation","params":{}},
{"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,garbage","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":"system_checks","params":{"check_level":"basic/comprehensive"}},
{"name":"emergency_return","params":{"reason":"string"}},
{"name":"take_photos","params":{"target_class":"同object_detect","description":"可选,目标属性","track_time":"[1,600]秒(必传,不可用'duration'","min_confidence":"[0.5,1.0]默认0.7","safe_distance":"[2,50]默认10"}}
],
"conditions": [
{"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","params":{},"children":"子节点数组(按序执行,全成功则成功)"},
{"name":"Selector","params":{"memory":"默认true"},"children":"子节点数组(执行到成功为止)"},
{"name":"Parallel","params":{"policy":"all_success"},"children":"子节点数组(同时执行,严禁用'one_success'"}
],
"decorators": [
{"name":"SuccessIsFailure","params":{},"child":"单一子节点(将子节点的成功结果反转为失败)"}
]
}
```
# 需要辨析的情况
1. "无人机当前在空中往东边飞50米,停个20s就好返航了。"这样的指令应该被判定为复杂,因为需要多步执行。
2. 到某地执行某动作,这样的任务应该是复杂。

View File

@@ -0,0 +1,41 @@
## 六、高频错误规避
1. 控制流节点的 `type` 必须是 `"Sequence"`, `"Selector"` 或 `"Parallel"`
2. **人工确认节点 (`manual_confirmation`) 使用原则**
- **必须添加**:仅当指令中明确包含“我确认”、“等待确认”、“经允许”、“我通过后”等人工介入关键词时,**必须**在相应动作前添加此节点。
- **严禁添加**:若指令未提及上述关键词,**严禁**主动添加此节点(即使是拍照、返航或降落等动作,只要用户没说要确认,就直接执行)。
3. 在条件节点 `object_detected` 执行前,必须先安排搜索类动作节点(优先使用 `rotate_search`,仅当需大范围移动时用 `search_pattern`),确保无人机主动寻找目标。
4. 当使用rotate_search或者object_detect节点时必须有object_detected节点
5. 用户指令中要求在当前位置执行任务时无需fly_to_waypoint节点
6. **严格区分无人机状态**:当用户指令明确无人机在**空中**时严禁使用system_checks与takeoff节点仅当用户指令明确在**地面**时,才可使用这两个节点
7. **重点关注**fly_to_waypoint与return_emergency节点辨析当指令包含具体目的地如“紧急回到广场”、“飞回大门”**必须**使用fly_to_waypoint节点**绝对禁止**使用return_emergency节点该节点仅用于无目的地的“返航”指令
8. 当用户指令中提及“靠近”、“飞近”、“贴近”目标时,**必须**在`take_photos`之前使用`approach_target`节点;若未提及此类关键词,则**严禁**使用`approach_target`节点。
9. **方向移动优先原则**当指令为“快速去往东边100米”直接使用 `move_direction` 节点+距离参数严禁使用fly_to_waypoint
## 七、坐标计算规则(东南天坐标系 ENU
本系统统一使用东南天ENU坐标系
- **X轴**:正方向为**东** (East),负方向为**西** (West)
- **Y轴**:正方向为**南** (South)向为**北** (North)
- **Z轴**:正方向为**天** (Up),负方向为**地** (Down)
**仅当指令涉及前往“具体地点”(如广场、大门)的偏移位置时,才需计算绝对坐标并使用`fly_to_waypoint`**
当指令包含“具体地点 + 方向 + 距离”的偏移时,按方向类型选择工具:
- **单一方向**(东/西/南/北/上/下如“广场西边200米”**必须**先调用工具`calc_offset_enu`计算绝对坐标,再使用`fly_to_waypoint`。工具参数:
- `base`: 参考地点的ENU坐标含x/y/z
- `direction`: east/west/north/south/up/down
- `distance`: 偏移距离(米)
- **中文复合方位**(东南/西北/东北/西南/南偏东10度/北偏西15度等**必须**先调用工具`calc_offset_esu_direction_text`计算绝对坐标,再使用`fly_to_waypoint`。工具参数:
- `base`: 参考地点的ENU坐标含x/y/z
- `direction_text`: 中文方位原文如“东南”“南偏东10度”
- `distance`: 偏移距离(米)
- **禁止简化**:当出现“东南/西北/东北/西南/南偏东/北偏西”等复合方位时,禁止将其简化为单一方向(如仅“东”或仅“南”)。
示例(必须遵守):
- “飞到广场东南方向100米” → 必须调用 `calc_offset_esu_direction_text`,参数 `direction_text` 为 `"东南"``distance` 为 `100`,再使用 `fly_to_waypoint`。
- “飞到广场南偏东10度100米” → 必须调用 `calc_offset_esu_direction_text`,参数 `direction_text` 为 `"南偏东10度"``distance` 为 `100`。
当指令只有“方向 + 距离”且**没有具体地点名词**时,**禁止**调用`calc_offset_enu`,必须使用`move_direction`。
当指令描述“附近/边上/区域内”等模糊位置且**无方向+距离**时,视为到该地点本身,不做偏移计算。
## 八、输出要求
仅输出1个严格符合上述所有规则的JSON对象。

View File

@@ -0,0 +1,185 @@
{
"actions": [
{
"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",
"desc": "仅当指令提及具体地点(如'去广场'、'去大门')或需计算明确坐标时使用"
}
},
{
"name": "fly_sequence",
"params": {
"waypoints": "list[dict] (e.g. [{'x':10,'y':20,'depth':5}, ...]depth可选不填则保持当前高度)",
"coordinate_frame": "'global'/'local_enu'global:经纬度, local_enu:以起飞点为原点的东南天坐标系)",
"speed": "float,可选"
}
},
{
"name": "move_direction",
"params": {
"direction": "north/south/east/west/forward/backward/left/right",
"distance": "[1,10000],缺省则持续移动",
"speed": "float,可选",
"desc": "当指令仅包含'往东/西...飞xx米'且无具体地点名词时,必须使用此节点"
}
},
{
"name": "approach_target",
"params": {
"target_class": "string,要趋近的目标类别",
"description": "string,可选,目标属性描述",
"stop_distance": "float,期望的最终停止距离",
"speed": "float,可选,期望的逼近速度"
}
},
{
"name": "rotate",
"params": {
"angle": "float,无人机自身旋转角度(正数逆时针,负数顺时针)",
"angular_velocity": "rad/s,旋转角速度"
}
},
{
"name": "rotate_search",
"params": {
"target_class": "同object_detect",
"description": "string,可选,目标属性描述",
"step_angle": "float,可选,每一步旋转的角度",
"total_rotation": "float,可选,总共旋转搜索的角度"
}
},
{
"name": "manual_confirmation",
"params": {}
},
{
"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,garbage",
"description": "可选,",
"count": "默认1"
}
},
{
"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": "system_checks",
"params": {
"check_level": "basic/comprehensive只能在起飞takeoff节点前使用空中无需使用该节点"
}
},
{
"name": "return_emergency",
"params": {
"reason": "string此节点仅用于【无明确目的地】的立即返航默认回起飞点。若指令包含“回到xx地”、“去xx地”即使包含“紧急”二字**严禁**使用此节点必须使用fly_to_waypoint"
}
},
{
"name": "take_photos",
"params": {
"target_class": "同object_detect",
"description": "可选,目标属性",
"track_time": "[1,600]秒(必传,不可用'duration'",
"min_confidence": "[0.5,1.0]默认0.7",
"safe_distance": "[2,50]默认10"
}
}
],
"conditions": [
{
"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"
}
}
],
"control_flow": [
{
"name": "Sequence",
"params": {},
"children": "子节点数组(按序执行,全成功则成功)"
},
{
"name": "Selector",
"params": {
"memory": "默认true"
},
"children": "子节点数组(执行到成功为止)"
},
{
"name": "Parallel",
"params": {
"policy": "all_success/success_on_one"
},
"children": "子节点数组同时执行默认all_success"
}
],
"decorators": [
{
"name": "SuccessIsFailure",
"params": {},
"child": "单一子节点(将子节点的成功结果反转为失败)"
}
]
}

View File

@@ -0,0 +1 @@
任务根据用户任意任务指令生成结构化可执行的无人机行为树PytreeJSON。**仅输出单一JSON对象无任何自然语言、注释或额外内容**。

View File

@@ -0,0 +1,8 @@
## 二、节点必填字段后端Schema强制要求缺一验证失败
每个节点必须包含以下字段,字段名/类型不可自定义:
1. **`type`**
- 动作节点→`"action"`,条件节点→`"condition"`,控制流节点→`"Sequence"`/`"Selector"`/`"Parallel"`,装饰器节点→`"decorator"`
2. **`name`**必须是上述JSON中定义的`name`值;
3. **`params`**:严格匹配上述节点的`params`定义,无自定义参数;
4. **`children`**:仅控制流节点必含(子节点数组);
5. **`child`**:仅装饰器节点必含(单一子节点对象,非数组)。

View File

@@ -0,0 +1,448 @@
## 四、场景示例
#### 场景 1地面到12米绕外围查看打开的窗户Sequence
**指令**“无人机当前在地面去面前大楼的12米高处绕着外围看有没有打开的窗户发现则进行拍照。”
**结构**
```json
{
"root": {
"type": "Sequence",
"name": "FlyPerimeterAndPhotoWindows",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{
"type": "Parallel",
"name": "PatrolAndDetectWindows",
"params": {"policy": "success_on_one"},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 289.8, "depth": 12.0},
{"x": -24.0, "y": 292.8, "depth": 12.0},
{"x": -24.0, "y": 241.8, "depth": 12.0}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectOpenWindowsWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"window","description":"打开的窗户"}},
{"type":"condition","name":"object_detected","params":{"target_class":"window","description":"打开的窗户"}},
{"type":"action","name":"take_photos","params":{"target_class":"window","description":"打开的窗户","track_time":10.0}}
]
}
}
]
}
]
}
}
```
#### 场景 2地面到12米沿外围查找打开的窗户Sequence
**指令**“无人机当前在地面去面前大楼的12米高处沿着外围查找所有打开的窗户并拍照。”
**结构**
```json
{
"root": {
"type": "Sequence",
"name": "FlyPerimeterAndPhotoOpenWindows",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{
"type": "Parallel",
"name": "PatrolAndDetectOpenWindows",
"params": {"policy": "success_on_one"},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 289.8, "depth": 12.0},
{"x": -24.0, "y": 292.8, "depth": 12.0},
{"x": -24.0, "y": 241.8, "depth": 12.0}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectAllOpenWindowsWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"window","description":"打开的窗户"}},
{"type":"condition","name":"object_detected","params":{"target_class":"window","description":"打开的窗户"}},
{"type":"action","name":"take_photos","params":{"target_class":"window","description":"打开的窗户","track_time":10.0}}
]
}
}
]
}
]
}
}
```
#### 场景 3空中上升后绕外围侦察打开窗户Sequence
**指令**“无人机当前在空中再往上飞3米接着绕这栋楼外围侦察有没有打开的窗户看到了就拍照传回来。”
**结构**
```json
{
"root": {
"type": "Sequence",
"name": "RiseAndDetectOpenWindows",
"children": [
{"type":"action","name":"move_direction","params":{"direction":"up","distance":3.0}},
{
"type": "Parallel",
"name": "PatrolAndDetectOpenWindows",
"params": {"policy": "success_on_one"},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8},
{"x": -108.5, "y": 241.8},
{"x": -108.5, "y": 289.8},
{"x": -24.0, "y": 292.8},
{"x": -24.0, "y": 241.8}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectOpenWindowsWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"window","description":"打开的窗户"}},
{"type":"condition","name":"object_detected","params":{"target_class":"window","description":"打开的窗户"}},
{"type":"action","name":"take_photos","params":{"target_class":"window","description":"打开的窗户","track_time":10.0}}
]
}
}
]
}
]
}
}
```
#### 场景 4地面到12米绕外围巡视杂物堆积Sequence
**指令**“无人机当前在地面去面前大楼的12米高处绕着外围巡视杂物堆积现象发现则进行拍照。”
**结构**
```json
{
"root": {
"type": "Sequence",
"name": "FlyPerimeterAndPhotoGarbageObserve",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{
"type": "Parallel",
"name": "PatrolAndDetectGarbageObserve",
"params": {"policy": "success_on_one"},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 289.8, "depth": 12.0},
{"x": -24.0, "y": 292.8, "depth": 12.0},
{"x": -24.0, "y": 241.8, "depth": 12.0}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectGarbageWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"garbage","description":"杂物堆积"}},
{"type":"condition","name":"object_detected","params":{"target_class":"garbage","description":"杂物堆积"}},
{"type":"action","name":"take_photos","params":{"target_class":"garbage","description":"杂物堆积","track_time":10.0}}
]
}
}
]
}
]
}
}
```
#### 场景 5地面到12米沿外围查找杂物堆积Sequence
**指令**“无人机当前在地面去面前大楼的12米高处沿着外围查找所有的杂物堆积并拍照。”
**结构**
```json
{
"root": {
"type": "Sequence",
"name": "FlyPerimeterAndPhotoGarbage",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{
"type": "Parallel",
"name": "PatrolAndDetectGarbage",
"params": {"policy": "success_on_one"},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 289.8, "depth": 12.0},
{"x": -24.0, "y": 292.8, "depth": 12.0},
{"x": -24.0, "y": 241.8, "depth": 12.0}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectGarbageWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"garbage","description":"杂物堆积"}},
{"type":"condition","name":"object_detected","params":{"target_class":"garbage","description":"杂物堆积"}},
{"type":"action","name":"take_photos","params":{"target_class":"garbage","description":"杂物堆积","track_time":10.0}}
]
}
}
]
}
]
}
}
```
#### 场景 6空中下降后绕外围侦察杂物堆积Sequence
**指令**“无人机当前在空中往下飞3米接着绕这栋楼外围侦察有没有杂物堆积看到了就拍照传回来。”
**结构**
```json
{
"root": {
"type": "Sequence",
"name": "DescendAndDetectGarbage",
"children": [
{"type":"action","name":"move_direction","params":{"direction":"down","distance":3.0}},
{
"type": "Parallel",
"name": "PatrolAndDetectGarbage",
"params": {"policy": "success_on_one"},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8},
{"x": -108.5, "y": 241.8},
{"x": -108.5, "y": 289.8},
{"x": -24.0, "y": 292.8},
{"x": -24.0, "y": 241.8}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectGarbageWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"garbage","description":"杂物堆积"}},
{"type":"condition","name":"object_detected","params":{"target_class":"garbage","description":"杂物堆积"}},
{"type":"action","name":"take_photos","params":{"target_class":"garbage","description":"杂物堆积","track_time":10.0}}
]
}
}
]
}
]
}
}
```
#### 场景 7地面到12米绕外围查看是否有人Sequence
**指令**“无人机当前在地面去面前大楼的12米高处绕着外围看有没有人发现则进行拍照。”
**结构**
```json
{
"root": {
"type": "Sequence",
"name": "FlyPerimeterAndPhotoPerson",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{
"type": "Parallel",
"name": "PatrolAndDetectPerson",
"params": {"policy": "success_on_one"},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 289.8, "depth": 12.0},
{"x": -24.0, "y": 292.8, "depth": 12.0},
{"x": -24.0, "y": 241.8, "depth": 12.0}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectPersonWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"person","description":"人员"}},
{"type":"condition","name":"object_detected","params":{"target_class":"person","description":"人员"}},
{"type":"action","name":"take_photos","params":{"target_class":"person","description":"人员","track_time":10.0}}
]
}
}
]
}
]
}
}
```
#### 场景 8地面到12米沿外围逆时针查找人员Sequence
**指令**“无人机当前在地面去面前大楼的12米高处沿着外围逆时针查找所有的人并拍照。”
**结构**
```json
{
"root": {
"type": "Sequence",
"name": "FlyPerimeterCounterClockwiseAndPhotoPerson",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{
"type": "Parallel",
"name": "PatrolCCWAndDetectPerson",
"params": {"policy": "success_on_one"},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8, "depth": 12.0},
{"x": -24.0, "y": 292.8, "depth": 12.0},
{"x": -108.5, "y": 289.8, "depth": 12.0},
{"x": -108.5, "y": 241.8, "depth": 12.0},
{"x": -24.0, "y": 241.8, "depth": 12.0}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectPersonWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"person","description":"人员"}},
{"type":"condition","name":"object_detected","params":{"target_class":"person","description":"人员"}},
{"type":"action","name":"take_photos","params":{"target_class":"person","description":"人员","track_time":10.0}}
]
}
}
]
}
]
}
}
```
#### 场景 9空中上升后沿建筑外围侦察人员Sequence
**指令**“无人机当前在空中再往上飞3米接着绕这栋楼外围侦察有没有人看到了就拍照传回来。”
**结构**
```json
{
"root": {
"type": "Sequence",
"name": "RiseAndPatrolPerimeter",
"children": [
{"type":"action","name":"move_direction","params":{"direction":"up","distance":3.0}},
{
"type": "Parallel",
"name": "PatrolAndDetect",
"params": {"policy": "success_on_one"},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8},
{"x": -108.5, "y": 241.8},
{"x": -108.5, "y": 289.8},
{"x": -24.0, "y": 292.8},
{"x": -24.0, "y": 241.8}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectAndPhotoWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"person","description":"建筑外围人员"}},
{"type":"condition","name":"object_detected","params":{"target_class":"person","description":"建筑外围人员"}},
{"type":"action","name":"take_photos","params":{"target_class":"person","description":"建筑外围人员","track_time":10.0}}
]
}
}
]
}
]
}
}
```

View File

@@ -0,0 +1,208 @@
## 四、场景示例(请灵活参考)
#### 场景 1线性搜索任务Sequence + Selector
**指令**:“无人机当前在地面,去研究所正大门,搜索扎辫子女子,找到后拍照。”
**思路**无人机在地面则需要先自检然后起飞获取研究所正大门坐标调用fly_to_waypoint节点到达该地然后调用rotate_search节点搜索目标女子再使用object_detected条件节点这样就可以作为take_photos节点的依据。
**结构**Sequence (按顺序执行)
```json
{
"root": {
"type": "Sequence",
"name": "MainSearchTask",
"children": [
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{"type":"action","name":"fly_to_waypoint","params":{"x":100.0,"y":50.0,"z":10.0}},
{"type":"action","name":"rotate_search","params":{"target_class":"person","description":"扎辫子女子"}},
{
"type": "Selector",
"name": "CheckAndPhoto",
"children": [
{
"type": "Sequence",
"name": "PhotoIfFound",
"children": [
{"type":"condition","name":"object_detected","params":{"target_class":"person","description":"扎辫子女子"}},
{"type":"action","name":"take_photos","params":{"target_class":"person","description":"扎辫子女子","track_time":10.0}}
]
},
{"type":"action","name":"loiter","params":{"duration":5.0}} // 未发现时的备选动作
]
}
]
}
}
```
#### 场景 2带中断逻辑的巡逻Selector 示例)
**指令**“无人机当前在地面飞往航点A。如果途中发现可疑人员则悬停。”
**参考知识**航点A的坐标x:100.0,y:50.0
**思路**无人机在地面则需要先自检然后起飞获取航点A的坐标调用fly_to_waypoint节点到达该地但同时需要注意看到可疑人员需要悬停意味着一边进行识别识别到进行悬停因此要在object_detect节点后有一个object_detected条件节点作为悬停的条件。
**结构**
```json
{
"root": {
"type": "Sequence",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"basic"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{
"type": "Selector",
"name": "FlyOrDetect",
"children": [
{
"type": "Sequence",
"name": "InterruptionLogic",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"person"}},
{"type":"condition","name":"object_detected","params":{"target_class":"person"}},
{"type":"action","name":"loiter","params":{"duration":5.0}}
]
},
{"type":"action","name":"fly_to_waypoint","params":{"x":100.0,"y":50.0,"z":10.0}}
]
}
]
}
}
```
#### 场景 3长期监控任务Parallel 示例)
**指令**“无人机当前在空中往广场西边飞200米持续监控5分钟发现人就拍照告诉我到时间可以返航。”
**参考知识**广场西边200米的坐标x:50.0,y:50.0,z:10.0
**思路**无人机在空中直接前往目标点到达后需要并行执行两件事1. 倒计时5分钟主控时间2. 持续检测人并拍照(从属任务)。
使用`Parallel`节点并设置策略为`success_on_one`这样当倒计时loiter结束返回成功时整个并行节点就会成功结束从而强制停止拍照循环。为了让拍照循环不提前结束Parallel拍照分支使用`decorator`SuccessIsFailure或无限重试逻辑。
**结构**
```json
{
"root": {
"type": "Sequence",
"name": "MainTask",
"children": [
{
"type": "action",
"name": "fly_to_waypoint",
"params": {
"x": 50.0,
"y": 50.0,
"z": 10.0
}
},
{
"type": "Parallel",
"name": "MonitorAndPhoto",
"params": {
"policy": "success_on_one"
},
"children": [
{
"type": "action",
"name": "loiter",
"params": {
"time": 300
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "CheckAndPhoto",
"children": [
{
"type": "action",
"name": "object_detect",
"params": {
"target_class": "person"
}
},
{
"type": "condition",
"name": "object_detected",
"params": {
"target_class": "person"
}
},
{
"type": "action",
"name": "take_photos",
"params": {
"target_class": "person"
}
}
]
}
}
]
},
{
"type": "action",
"name": "return_emergency",
"params": {
"reason": "任务完成"
}
}
]
}
}
```
#### 场景 4交互式确认任务Sequence + Manual Confirmation
**指令**:“无人机当前在空中,搜索小汽车,搜索到了我确认后再决定要不要拍照。”
**思路**:无人机已在空中,无需起飞,直接进行搜索。首先使用`rotate_search`主动搜索目标,配合`object_detected`条件节点确认目标是否被检测到。检测到后,执行`manual_confirmation`节点等待用户确认。只有用户确认通过返回Success才会继续执行后续的`take_photos`动作。
**结构**Sequence
```json
{
"root": {
"type": "Sequence",
"name": "SearchConfirmPhoto",
"children": [
{"type":"action","name":"rotate_search","params":{"target_class":"car","description":"小汽车"}},
{"type":"condition","name":"object_detected","params":{"target_class":"car","description":"小汽车"}},
{"type":"action","name":"manual_confirmation","params":{}},
{"type":"action","name":"take_photos","params":{"target_class":"car","description":"小汽车"}}
]
}
}
```
#### 场景 5后置确认任务Sequence + Manual Confirmation
**指令**:“无人机当前在地面,搜索小汽车,搜索到了拍张照,我确认后再决定要不要返航”
**思路**:无人机在地面,需起飞。搜索小汽车(`rotate_search` + `object_detected`)。搜索到后,先执行拍照(`take_photos`)。之后执行人工确认(`manual_confirmation`),确认通过后才执行返航(`return_emergency`)。
**结构**Sequence
```json
{
"root": {
"type": "Sequence",
"name": "SearchPhotoConfirmReturn",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{"type":"action","name":"rotate_search","params":{"target_class":"car","description":"小汽车"}},
{"type":"condition","name":"object_detected","params":{"target_class":"car","description":"小汽车"}},
{"type":"action","name":"take_photos","params":{"target_class":"car","description":"小汽车"}},
{"type":"action","name":"manual_confirmation","params":{}},
{"type":"action","name":"return_emergency","params":{"reason":"确认后返航"}}
]
}
}
```
#### 场景 6移动后条件降落Sequence
**指令**:“无人机当前在空中,紧急回到广场,看见了红绿灯之后直接降落。”
**参考知识**广场坐标x:0.0, y:0.0, z:10.0
**思路**:无人机在空中,无需起飞。首先飞往广场(`fly_to_waypoint`),严禁使用`return_emergency`。到达后,为了满足“看见红绿灯”的条件,必须先执行主动搜索(`rotate_search`)。一旦检测到红绿灯(`object_detected`),即执行降落(`land`)。
**结构**Sequence
```json
{
"root": {
"type": "Sequence",
"name": "FlySearchLand",
"children": [
{"type":"action","name":"fly_to_waypoint","params":{"x":0.0,"y":0.0,"z":10.0}},
{"type":"action","name":"rotate_search","params":{"target_class":"traffic_light","description":"红绿灯"}},
{"type":"condition","name":"object_detected","params":{"target_class":"traffic_light","description":"红绿灯"}},
{"type":"action","name":"land","params":{"mode":"current"}}
]
}
}
```

View File

@@ -0,0 +1,18 @@
## 三、标准任务结构模板(单次起降流程)
当无人机在地面时,大多数任务应遵循“起飞 -> 移动 -> 条件判断 -> 执行 -> 返航/降落”的单次闭环流程,参考结构如下:
```json
{
"root": {
"type": "Sequence",
"name": "MainTask",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{"type":"action","name":"fly_to_waypoint","params":{"x":100.0,"y":50.0,"z":10.0}}, // 接近目标区域
// --- 核心任务区 (根据指令替换) ---
// 默认不需要降落节点,除非用户明确要求
]
}
}
```
而当无人机在空中时则无需system_checks与takeoff环节直接执行用户任务即可

View File

@@ -0,0 +1,489 @@
#### 场景 7空中上升后沿建筑外围侦察Sequence + Selector
**指令**“无人机当前在空中再往上飞3米接着绕这栋楼外围侦察有没有人看到了就拍照传回来。”
**参考信息**面前的大楼外围四个点东南天坐标系坐标A(-24.00, 241.80),B(-108.50, 241.80),C(-108.50, 289.80),D(-24.00, 292.80)。
**思路**无人机在空中无需自检与起飞。先向上移动3米仅方向+距离,使用`move_direction`)。随后“边飞边判断”:用`Parallel`并行执行`fly_sequence`与持续检测分支。检测分支使用`object_detect` + `object_detected` + `take_photos`,再用`SuccessIsFailure`保证不会提前结束并行;并行策略为`success_on_one`,当巡查飞行完成即结束任务。
**结构**Sequence
```json
{
"root": {
"type": "Sequence",
"name": "RiseAndPatrolPerimeter",
"children": [
{
"type": "action",
"name": "move_direction",
"params": {
"direction": "up",
"distance": 3.0
}
},
{
"type": "Parallel",
"name": "PatrolAndDetect",
"params": {
"policy": "success_on_one"
},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8},
{"x": -108.5, "y": 241.8},
{"x": -108.5, "y": 289.8},
{"x": -24.0, "y": 292.8},
{"x": -24.0, "y": 241.8}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectAndPhotoWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"person","description":"建筑外围人员"}},
{"type":"condition","name":"object_detected","params":{"target_class":"person","description":"建筑外围人员"}},
{"type":"action","name":"take_photos","params":{"target_class":"person","description":"建筑外围人员","track_time":10.0}}
]
}
}
]
}
]
}
}
```
#### 场景 8地面起飞到楼前12米高度并沿外围查找杂物Sequence
**指令**“无人机当前在地面去面前大楼的12米高处沿着外围查找所有的杂物堆积并拍照。”
**参考信息**面前的大楼外围四个点东南天坐标系坐标A(-24.00, 241.80),B(-108.50, 241.80),C(-108.50, 289.80),D(-24.00, 292.80)。
**思路**:无人机在地面,需自检后起飞。指令是到具体地点且明确高度,使用`fly_sequence`沿外围巡查所有航点设置为12米高度`depth`=12。为满足“边飞边判断”使用`Parallel`并行飞行与检测,检测分支持续执行`object_detect` + `object_detected` + `take_photos`,用`SuccessIsFailure`避免提前结束。
**结构**Sequence
```json
{
"root": {
"type": "Sequence",
"name": "FlyPerimeterAndPhotoGarbage",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{
"type": "Parallel",
"name": "PatrolAndDetectGarbage",
"params": {
"policy": "success_on_one"
},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 289.8, "depth": 12.0},
{"x": -24.0, "y": 292.8, "depth": 12.0},
{"x": -24.0, "y": 241.8, "depth": 12.0}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectGarbageWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"garbage","description":"杂物堆积"}},
{"type":"condition","name":"object_detected","params":{"target_class":"garbage","description":"杂物堆积"}},
{"type":"action","name":"take_photos","params":{"target_class":"garbage","description":"杂物堆积","track_time":10.0}}
]
}
}
]
}
]
}
}
```
#### 场景 9地面到12米绕外围查看打开的窗户Sequence
**指令**“无人机当前在地面去面前大楼的12米高处绕着外围看有没有打开的窗户发现则进行拍照。”
**参考信息**面前的大楼外围四个点东南天坐标系坐标A(-24.00, 241.80),B(-108.50, 241.80),C(-108.50, 289.80),D(-24.00, 292.80)。
**思路**:无人机在地面,先自检起飞。随后使用`fly_sequence`沿外围绕飞航点高度为12米`depth`=12。同时并行执行检测分支发现打开窗户就拍照。
**结构**Sequence
```json
{
"root": {
"type": "Sequence",
"name": "FlyPerimeterAndPhotoWindows",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{
"type": "Parallel",
"name": "PatrolAndDetectWindows",
"params": {
"policy": "success_on_one"
},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 289.8, "depth": 12.0},
{"x": -24.0, "y": 292.8, "depth": 12.0},
{"x": -24.0, "y": 241.8, "depth": 12.0}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectOpenWindowsWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"window","description":"打开的窗户"}},
{"type":"condition","name":"object_detected","params":{"target_class":"window","description":"打开的窗户"}},
{"type":"action","name":"take_photos","params":{"target_class":"window","description":"打开的窗户","track_time":10.0}}
]
}
}
]
}
]
}
}
```
#### 场景 10地面到12米沿外围查找打开的窗户Sequence
**指令**“无人机当前在地面去面前大楼的12米高处沿着外围查找所有打开的窗户并拍照。”
**参考信息**面前的大楼外围四个点东南天坐标系坐标A(-24.00, 241.80),B(-108.50, 241.80),C(-108.50, 289.80),D(-24.00, 292.80)。
**思路**:无人机在地面,起飞后使用`fly_sequence`沿外围巡查(`depth`=12并行持续检测打开的窗户并拍照。
**结构**Sequence
```json
{
"root": {
"type": "Sequence",
"name": "FlyPerimeterAndPhotoOpenWindows",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{
"type": "Parallel",
"name": "PatrolAndDetectOpenWindows",
"params": {
"policy": "success_on_one"
},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 289.8, "depth": 12.0},
{"x": -24.0, "y": 292.8, "depth": 12.0},
{"x": -24.0, "y": 241.8, "depth": 12.0}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectAllOpenWindowsWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"window","description":"打开的窗户"}},
{"type":"condition","name":"object_detected","params":{"target_class":"window","description":"打开的窗户"}},
{"type":"action","name":"take_photos","params":{"target_class":"window","description":"打开的窗户","track_time":10.0}}
]
}
}
]
}
]
}
}
```
#### 场景 11空中上升后绕外围侦察打开窗户Sequence
**指令**“无人机当前在空中再往上飞3米接着绕这栋楼外围侦察有没有打开的窗户看到了就拍照传回来。”
**参考信息**面前的大楼外围四个点东南天坐标系坐标A(-24.00, 241.80),B(-108.50, 241.80),C(-108.50, 289.80),D(-24.00, 292.80)。
**思路**无人机在空中先上升3米。随后并行执行绕飞巡查与检测发现打开窗户立即拍照。
**结构**Sequence
```json
{
"root": {
"type": "Sequence",
"name": "RiseAndDetectOpenWindows",
"children": [
{"type":"action","name":"move_direction","params":{"direction":"up","distance":3.0}},
{
"type": "Parallel",
"name": "PatrolAndDetectOpenWindows",
"params": {
"policy": "success_on_one"
},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8},
{"x": -108.5, "y": 241.8},
{"x": -108.5, "y": 289.8},
{"x": -24.0, "y": 292.8},
{"x": -24.0, "y": 241.8}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectOpenWindowsWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"window","description":"打开的窗户"}},
{"type":"condition","name":"object_detected","params":{"target_class":"window","description":"打开的窗户"}},
{"type":"action","name":"take_photos","params":{"target_class":"window","description":"打开的窗户","track_time":10.0}}
]
}
}
]
}
]
}
}
```
#### 场景 12地面到12米绕外围巡视杂物堆积Sequence
**指令**“无人机当前在地面去面前大楼的12米高处绕着外围巡视杂物堆积现象发现则进行拍照。”
**参考信息**面前的大楼外围四个点东南天坐标系坐标A(-24.00, 241.80),B(-108.50, 241.80),C(-108.50, 289.80),D(-24.00, 292.80)。
**思路**:无人机在地面,起飞后沿外围绕飞(`depth`=12并行检测杂物堆积并拍照。
**结构**Sequence
```json
{
"root": {
"type": "Sequence",
"name": "FlyPerimeterAndPhotoGarbageObserve",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{
"type": "Parallel",
"name": "PatrolAndDetectGarbageObserve",
"params": {
"policy": "success_on_one"
},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 289.8, "depth": 12.0},
{"x": -24.0, "y": 292.8, "depth": 12.0},
{"x": -24.0, "y": 241.8, "depth": 12.0}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectGarbageWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"garbage","description":"杂物堆积"}},
{"type":"condition","name":"object_detected","params":{"target_class":"garbage","description":"杂物堆积"}},
{"type":"action","name":"take_photos","params":{"target_class":"garbage","description":"杂物堆积","track_time":10.0}}
]
}
}
]
}
]
}
}
```
#### 场景 13空中下降后绕外围侦察杂物堆积Sequence
**指令**“无人机当前在空中往下飞3米接着绕这栋楼外围侦察有没有杂物堆积看到了就拍照传回来。”
**参考信息**面前的大楼外围四个点东南天坐标系坐标A(-24.00, 241.80),B(-108.50, 241.80),C(-108.50, 289.80),D(-24.00, 292.80)。
**思路**无人机在空中先下降3米。随后并行执行绕飞巡查与检测发现杂物堆积立即拍照。
**结构**Sequence
```json
{
"root": {
"type": "Sequence",
"name": "DescendAndDetectGarbage",
"children": [
{"type":"action","name":"move_direction","params":{"direction":"down","distance":3.0}},
{
"type": "Parallel",
"name": "PatrolAndDetectGarbage",
"params": {
"policy": "success_on_one"
},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8},
{"x": -108.5, "y": 241.8},
{"x": -108.5, "y": 289.8},
{"x": -24.0, "y": 292.8},
{"x": -24.0, "y": 241.8}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectGarbageWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"garbage","description":"杂物堆积"}},
{"type":"condition","name":"object_detected","params":{"target_class":"garbage","description":"杂物堆积"}},
{"type":"action","name":"take_photos","params":{"target_class":"garbage","description":"杂物堆积","track_time":10.0}}
]
}
}
]
}
]
}
}
```
#### 场景 14地面到12米绕外围查看是否有人Sequence
**指令**“无人机当前在地面去面前大楼的12米高处绕着外围看有没有人发现则进行拍照。”
**参考信息**面前的大楼外围四个点东南天坐标系坐标A(-24.00, 241.80),B(-108.50, 241.80),C(-108.50, 289.80),D(-24.00, 292.80)。
**思路**:无人机在地面,起飞后沿外围绕飞(`depth`=12并行检测人员并拍照。
**结构**Sequence
```json
{
"root": {
"type": "Sequence",
"name": "FlyPerimeterAndPhotoPerson",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{
"type": "Parallel",
"name": "PatrolAndDetectPerson",
"params": {
"policy": "success_on_one"
},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 241.8, "depth": 12.0},
{"x": -108.5, "y": 289.8, "depth": 12.0},
{"x": -24.0, "y": 292.8, "depth": 12.0},
{"x": -24.0, "y": 241.8, "depth": 12.0}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectPersonWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"person","description":"人员"}},
{"type":"condition","name":"object_detected","params":{"target_class":"person","description":"人员"}},
{"type":"action","name":"take_photos","params":{"target_class":"person","description":"人员","track_time":10.0}}
]
}
}
]
}
]
}
}
```
#### 场景 15地面到12米沿外围逆时针查找人员Sequence
**指令**“无人机当前在地面去面前大楼的12米高处沿着外围逆时针查找所有的人并拍照。”
**参考信息**面前的大楼外围四个点东南天坐标系坐标A(-24.00, 241.80),B(-108.50, 241.80),C(-108.50, 289.80),D(-24.00, 292.80)。
**思路**无人机在地面起飞后按逆时针顺序巡查外围示例航点顺序A→D→C→B→A并行检测人员与拍照。
**结构**Sequence
```json
{
"root": {
"type": "Sequence",
"name": "FlyPerimeterCounterClockwiseAndPhotoPerson",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{
"type": "Parallel",
"name": "PatrolCCWAndDetectPerson",
"params": {
"policy": "success_on_one"
},
"children": [
{
"type": "action",
"name": "fly_sequence",
"params": {
"waypoints": [
{"x": -24.0, "y": 241.8, "depth": 12.0},
{"x": -24.0, "y": 292.8, "depth": 12.0},
{"x": -108.5, "y": 289.8, "depth": 12.0},
{"x": -108.5, "y": 241.8, "depth": 12.0},
{"x": -24.0, "y": 241.8, "depth": 12.0}
],
"coordinate_frame": "local_enu"
}
},
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectPersonWhileFlying",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"person","description":"人员"}},
{"type":"condition","name":"object_detected","params":{"target_class":"person","description":"人员"}},
{"type":"action","name":"take_photos","params":{"target_class":"person","description":"人员","track_time":10.0}}
]
}
}
]
}
]
}
}
```

View File

@@ -0,0 +1,24 @@
scenes:
system:
- header.txt
- core_nodes.json
- required_fields.txt
- standard_template.txt
- scene4_examples.txt
- system_extra_examples.txt
- common_rules.txt
scene1:
- header.txt
- core_nodes.json
- required_fields.txt
- standard_template.txt
- scene1_examples.txt
scene4:
- header.txt
- core_nodes.json
- required_fields.txt
- standard_template.txt
- scene4_examples.txt
- common_rules.txt
simple:
- simple_mode_prompt.txt

View File

@@ -0,0 +1,46 @@
你是一个严格的指令场景分类器。只输出一个JSON对象不要输出解释或多余文本。
根据用户指令与下述场景定义判断其属于“simple / scene1 / scene4”之一。
输出格式(严格遵守):
{"mode":"simple"} 或 {"mode":"scene1"} 或 {"mode":"scene4"}
判定规则(严格遵守):
1. 若指令符合 scene1 或 scene4 的特征,必须输出对应模式;
2. 若指令不符合 scene1 与 scene4输出 simple
3. 仅允许以上三种取值,禁止输出其他字段或文本。
—— 场景定义 ——
scene1大楼外围巡查类
- 关键词特征:面前大楼/这栋楼、外围/沿着外围/绕着外围、12米高处、逆时针等
- 任务类型:绕楼外围巡查、搜索窗户/杂物堆积/人员等并拍照;
- 包含上下/下降移动上升3米/下降3米后绕楼侦察的也属于 scene1。
scene4广场相关复合任务类
- 关键词特征:广场、广场边上、广场南边、施工区域、紧急回到广场、回到广场;
- 任务类型:在广场搜索/拍照/监控/返航/降落/靠近拍照等;
- 可能包含“确认后拍照/返航”、“持续监控5分钟”、“挟持”等描述。
—— 场景指令样例 ——
scene1 示例:
- 无人机当前在地面去面前大楼的12米高处绕着外围看有没有打开的窗户发现则进行拍照。
- 无人机当前在地面去面前大楼的12米高处沿着外围查找所有打开的窗户并拍照。
- 无人机当前在空中再往上飞3米接着绕这栋楼外围侦察有没有打开的窗户看到了就拍照传回来。
- 无人机当前在地面去面前大楼的12米高处绕着外围巡视杂物堆积现象发现则进行拍照。
- 无人机当前在地面去面前大楼的12米高处沿着外围查找所有的杂物堆积并拍照。
- 无人机当前在空中往下飞3米接着绕这栋楼外围侦察有没有杂物堆积看到了就拍照传回来。
- 无人机当前在地面去面前大楼的12米高处绕着外围看有没有人发现则进行拍照。
- 无人机当前在地面去面前大楼的12米高处沿着外围逆时针查找所有的人并拍照。
- 无人机当前在空中再往上飞3米接着绕这栋楼外围侦察有没有人看到了就拍照传回来。
scene4 示例:
- 无人机当前在地面,到广场查找穿红色衣服的人,找到后近距离拍照。
- 无人机当前在空中,回到广场,对戴帽子的人进行拍照。
- 无人机当前在空中去广场南边40米对过往的公交车拍张照然后返航。
- 无人机当前在地面,到广场查找绿色公交车,看见了拍个照片。
- 无人机当前在空中,搜索小汽车,搜索到了我确认后再决定要不要拍照。
- 无人机当前在空中,搜索小汽车,搜索到了拍张照,我确认后再决定要不要返航。
- 无人机当前在空中往广场南边飞40米持续监控5分钟发现人就拍照告诉我到时间可以返航。
- 无人机当前在地面,到广场边上的施工区域内,发现有没带安全帽的飞近后拍照。
- 无人机当前在空中,紧急回到广场,看见了红绿灯之后直接降落。
- 无人机当前在地面快速去往东边60米有身穿白色衣服头戴帽子的男子在挟持他人对其进行拍照。
- 无人机当前在空中离白色衣服戴帽子的人太远了照片看不清贴近到3米距离拍拍完可以直接返航。

View File

@@ -18,8 +18,8 @@
{"name":"takeoff","params":{"altitude":"float[1,100]默认2"}}, {"name":"takeoff","params":{"altitude":"float[1,100]默认2"}},
{"name":"land","params":{"mode":"'current'/'home'"}}, {"name":"land","params":{"mode":"'current'/'home'"}},
{"name":"fly_to_waypoint","params":{"x":"±10000","y":"±10000","z":"[1,5000]","acceptance_radius":"默认2.0"}}, {"name":"fly_to_waypoint","params":{"x":"±10000","y":"±10000","z":"[1,5000]","acceptance_radius":"默认2.0"}},
{"name":"fly_sequence","params":{"waypoints":"list[dict] (e.g. [{'x':10,'y':20,'z':5}, ...])","coordinate_frame":"'global'/'local_enu'global:经纬度, local_enu:以起飞点为原点的东天坐标系)","speed":"float,可选"}}, {"name":"fly_sequence","params":{"waypoints":"list[dict] (e.g. [{'x':10,'y':20,'depth':5}, ...]depth可选不填则保持当前高度)","coordinate_frame":"'global'/'local_enu'global:经纬度, local_enu:以起飞点为原点的东天坐标系)","speed":"float,可选"}},
{"name":"move_direction","params":{"direction":"north/south/east/west/forward/backward/left/right","distance":"[1,10000],缺省持续移动","speed":"float,可选"}}, {"name":"move_direction","params":{"direction":"north/south/east/west/forward/backward/left/right/up/down","distance":"[1,10000],缺省持续移动","speed":"float,可选"}},
{"name":"approach_target","params":{"target_class":"string,要趋近的目标类别","description":"string,可选,目标属性描述","stop_distance":"float,期望的最终停止距离","speed":"float,可选,期望的逼近速度"}}, {"name":"approach_target","params":{"target_class":"string,要趋近的目标类别","description":"string,可选,目标属性描述","stop_distance":"float,期望的最终停止距离","speed":"float,可选,期望的逼近速度"}},
{"name":"rotate","params":{"angle":"float,无人机自身旋转角度(正数逆时针,负数顺时针)","angular_velocity":"rad/s,旋转角速度"}}, {"name":"rotate","params":{"angle":"float,无人机自身旋转角度(正数逆时针,负数顺时针)","angular_velocity":"rad/s,旋转角速度"}},
{"name":"rotate_search","params":{"target_class":"string,要搜寻的目标类别","description":"string,可选,目标属性描述","step_angle":"float,可选,每一步旋转的角度","total_rotation":"float,可选,总共旋转搜索的角度"}}, {"name":"rotate_search","params":{"target_class":"string,要搜寻的目标类别","description":"string,可选,目标属性描述","step_angle":"float,可选,每一步旋转的角度","total_rotation":"float,可选,总共旋转搜索的角度"}},
@@ -32,7 +32,7 @@
{"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":"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":"deliver_payload","params":{"payload_type":"string","release_altitude":"[2,100]默认5"}},
{"name":"system_checks","params":{"check_level":"basic/comprehensive"}}, {"name":"system_checks","params":{"check_level":"basic/comprehensive"}},
{"name":"return","params":{"reason":"string"}}, {"name":"return_emergency","params":{"reason":"string此节点仅用于【无明确目的地】的立即返航。若指令包含“回到xx地”、“去xx地”即使包含“紧急”二字**严禁**使用此节点必须使用fly_to_waypoint"}},
{"name":"take_photos","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":"take_photos","params":{"target_class":"同object_detect","description":"可选,目标属性","track_time":"[1,600]秒(必传,不可用'duration'","min_confidence":"[0.5,1.0]默认0.7","safe_distance":"[2,50]默认10"}}
], ],
"conditions": [ "conditions": [

View File

@@ -1,237 +0,0 @@
任务根据用户任意任务指令生成结构化可执行的无人机行为树PytreeJSON。**仅输出单一JSON对象无任何自然语言、注释或额外内容**。
## 一、核心节点定义(格式不可修改,确保后端解析)
#### 1. 可用节点定义 (必须遵守)
你必须严格从以下JSON定义的列表中选择节点构建行为树不允许使用未定义节点
```json
{
"actions": [
{"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","desc":"仅当指令提及具体地点(如'去广场'、'去大门')或需计算明确坐标时使用"}},
{"name":"fly_sequence","params":{"waypoints":"list[dict] (e.g. [{'x':10,'y':20,'z':5}, ...])","coordinate_frame":"'global'/'local_enu'global:经纬度, local_enu:以起飞点为原点的东北天坐标系)","speed":"float,可选"}},
{"name":"move_direction","params":{"direction":"north/south/east/west/forward/backward/left/right","distance":"[1,10000],缺省则持续移动","speed":"float,可选","desc":"当指令仅包含'往东/西...飞xx米'且无具体地点名词时,必须使用此节点"}},
{"name":"approach_target","params":{"target_class":"string,要趋近的目标类别","description":"string,可选,目标属性描述","stop_distance":"float,期望的最终停止距离","speed":"float,可选,期望的逼近速度"}},
{"name":"rotate","params":{"angle":"float,无人机自身旋转角度(正数逆时针,负数顺时针)","angular_velocity":"rad/s,旋转角速度"}},
{"name":"rotate_search","params":{"target_class":"同object_detect","description":"string,可选,目标属性描述","step_angle":"float,可选,每一步旋转的角度","total_rotation":"float,可选,总共旋转搜索的角度"}},
{"name":"manual_confirmation","params":{}},
{"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,garbage","description":"可选,","count":"默认1"}},
{"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":"system_checks","params":{"check_level":"basic/comprehensive只能在起飞takeoff节点前使用空中无需使用该节点"}},
{"name":"return_emergency","params":{"reason":"string此节点仅用于【无明确目的地】的立即返航默认回起飞点。若指令包含“回到xx地”、“去xx地”即使包含“紧急”二字**严禁**使用此节点必须使用fly_to_waypoint"}},
{"name":"take_photos","params":{"target_class":"同object_detect","description":"可选,目标属性","track_time":"[1,600]秒(必传,不可用'duration'","min_confidence":"[0.5,1.0]默认0.7","safe_distance":"[2,50]默认10"}}
],
"conditions": [
{"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"}}
],
"control_flow": [
{"name":"Sequence","params":{},"children":"子节点数组(按序执行,全成功则成功)"},
{"name":"Selector","params":{"memory":"默认true"},"children":"子节点数组(执行到成功为止)"},
{"name":"Parallel","params":{"policy":"all_success/success_on_one"},"children":"子节点数组同时执行默认all_success"}
],
"decorators": [
{"name":"SuccessIsFailure","params":{},"child":"单一子节点(将子节点的成功结果反转为失败)"}
]
}
```
## 二、节点必填字段后端Schema强制要求缺一验证失败
每个节点必须包含以下字段,字段名/类型不可自定义:
1. **`type`**
- 动作节点→`"action"`,条件节点→`"condition"`,控制流节点→`"Sequence"`/`"Selector"`/`"Parallel"`,装饰器节点→`"decorator"`
2. **`name`**必须是上述JSON中定义的`name`值;
3. **`params`**:严格匹配上述节点的`params`定义,无自定义参数;
4. **`children`**:仅控制流节点必含(子节点数组);
5. **`child`**:仅装饰器节点必含(单一子节点对象,非数组)。
## 三、标准任务结构模板(单次起降流程)
当无人机在地面时,大多数任务应遵循“起飞 -> 移动 -> 条件判断 -> 执行 -> 返航/降落”的单次闭环流程,参考结构如下:
```json
{
"root": {
"type": "Sequence",
"name": "MainTask",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{"type":"action","name":"fly_to_waypoint","params":{"x":100.0,"y":50.0,"z":10.0}}, // 接近目标区域
// --- 核心任务区 (根据指令替换) ---
// --- 任务1执行---
{"type":"action","name":"rotate_search","params":{"target_class":"person","description":"目标描述"}},
{"type":"action","name":"object_detect","params":{"target_class":"person","description":"目标描述"}},
// ----条件判断(根据指令替换) -----
{"type":"condition","name":"object_detected","params":{"target_class":"person","description":"扎辫子女子"}},
// --- 任务2执行---
{"type":"action","name":"take_photos","params":{"target_class":"person","description":"扎辫子女子"}},
// 默认不需要降落节点,除非用户明确要求
]
}
}
```
## 四、场景示例(请灵活参考)
#### 场景 1线性搜索任务Sequence + Selector
**指令**:“无人机当前在地面,去研究所正大门,搜索扎辫子女子,找到后拍照。”
**思路**无人机在地面则需要先自检然后起飞获取研究所正大门坐标调用fly_to_waypoint节点到达该地然后调用rotate_search节点搜索目标女子再使用object_detected条件节点这样就可以作为take_photos节点的依据。
**结构**Sequence (按顺序执行)
```json
{
"root": {
"type": "Sequence",
"name": "MainSearchTask",
"children": [
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{"type":"action","name":"fly_to_waypoint","params":{"x":100.0,"y":50.0,"z":10.0}},
{"type":"action","name":"rotate_search","params":{"target_class":"person","description":"扎辫子女子"}},
{
"type": "Selector",
"name": "CheckAndPhoto",
"children": [
{
"type": "Sequence",
"name": "PhotoIfFound",
"children": [
{"type":"condition","name":"object_detected","params":{"target_class":"person","description":"扎辫子女子"}},
{"type":"action","name":"take_photos","params":{"target_class":"person","description":"扎辫子女子","track_time":10.0}}
]
},
{"type":"action","name":"loiter","params":{"duration":5.0}} // 未发现时的备选动作
]
}
]
}
}
```
#### 场景 2带中断逻辑的巡逻Selector 示例)
**指令**“无人机当前在地面飞往航点A。如果途中发现可疑人员则悬停。”
**参考知识**航点A的坐标x:100.0,y:50.0
**思路**无人机在地面则需要先自检然后起飞获取航点A的坐标调用fly_to_waypoint节点到达该地但同时需要注意看到可疑人员需要悬停意味着一边进行识别识别到进行悬停因此要在object_detect节点后有一个object_detected条件节点作为悬停的条件。
**结构**
```json
{
"root": {
"type": "Sequence",
"children": [
{"type":"action","name":"system_checks","params":{"check_level":"basic"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{
"type": "Selector",
"name": "FlyOrDetect",
"children": [
{
"type": "Sequence",
"name": "InterruptionLogic",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"person"}},
{"type":"condition","name":"object_detected","params":{"target_class":"person"}},
{"type":"action","name":"loiter","params":{"duration":5.0}}
]
},
{"type":"action","name":"fly_to_waypoint","params":{"x":100.0,"y":50.0,"z":10.0}}
]
}
]
}
}
```
#### 场景 3长期监控任务Parallel 示例)
**指令**“无人机当前在空中往广场西边飞200米持续监控5分钟发现人就拍照告诉我到时间可以返航。”
**参考知识**广场西边200米的坐标x:50.0,y:50.0,z:10.0
**思路**无人机在空中直接前往目标点到达后需要并行执行两件事1. 倒计时5分钟主控时间2. 持续检测人并拍照(从属任务)。
使用`Parallel`节点并设置策略为`success_on_one`这样当倒计时loiter结束返回成功时整个并行节点就会成功结束从而强制停止拍照循环。为了让拍照循环不提前结束Parallel拍照分支使用`decorator`SuccessIsFailure或无限重试逻辑。
**结构**
```json
{
"root": {
"name": "主任务-监控与拍照",
"type": "Sequence",
"children": [
{
"name": "fly_to_waypoint",
"type": "action",
"params": {"x": 50.0, "y": 50.0, "z": 10.0}
},
{
"name": "并行监控5分钟",
"type": "Parallel",
"params": { "policy": "success_on_one" },
"children": [
{
"name": "loiter",
"type": "action",
"params": { "duration": 300 }
},
{
"name": "SuccessIsFailure",
"type": "decorator",
"child": {
"name": "check_and_photo",
"type": "Sequence",
"children": [
{"name": "object_detect", "type": "action", "params": { "target_class": "person" }},
{"name": "object_detected", "type": "condition", "params": { "target_class": "person" }},
{"name": "take_photos", "type": "action", "params": { "target_class": "person" }}
]
}
}
]
},
{
"name": "return_emergency",
"type": "action",
"params": {"reason": "返航"} // 返航回起飞点
}
]
}
}
```
#### 场景 4交互式确认任务Sequence + Manual Confirmation
**指令**:“无人机当前在空中,搜索小汽车,搜索到了我确认后再决定要不要拍照。”
**思路**:无人机已在空中,无需起飞,直接进行搜索。首先使用`rotate_search`主动搜索目标,配合`object_detected`条件节点确认目标是否被检测到。检测到后,执行`manual_confirmation`节点等待用户确认。只有用户确认通过返回Success才会继续执行后续的`take_photos`动作。
**结构**Sequence
```json
{
"root": {
"type": "Sequence",
"name": "SearchConfirmPhoto",
"children": [
{"type":"action","name":"rotate_search","params":{"target_class":"car","description":"小汽车"}},
{"type":"condition","name":"object_detected","params":{"target_class":"car","description":"小汽车"}},
{"type":"action","name":"manual_confirmation","params":{}},
{"type":"action","name":"take_photos","params":{"target_class":"car","description":"小汽车"}}
]
}
}
```
## 六、高频错误规避
1. 控制流节点的 `type` 必须是 `"Sequence"`, `"Selector"` 或 `"Parallel"`
2. 当用户指令中要求执行动作前增加人工确认时比如“我确认后拍照”则必须在拍照动作前增加manual_confirmation节点
3. 在条件节点执行前必须有相应动作节点如object_detected节点前必须是rotate_search等搜索类节点
4. 当使用rotate_search或者object_detect节点时必须有object_detected节点
5. 用户指令中要求在当前位置执行任务时无需fly_to_waypoint节点
6. **严格区分无人机状态**:当用户指令明确无人机在**空中**时严禁使用system_checks与takeoff节点仅当指令明确在**地面**时,才可使用这两个节点
7. **重点关注**fly_to_waypoint与return_emergency节点辨析当指令包含具体目的地如“紧急回到广场”、“飞回大门”**必须**使用fly_to_waypoint节点**绝对禁止**使用return_emergency节点该节点仅用于无目的地的“紧急返航”指令
8. 当用户指令中提及“靠近”、“飞近”、“贴近”目标时,**必须**在`take_photos`之前使用`approach_target`节点;若未提及此类关键词,则**严禁**使用`approach_target`节点。
9. **方向移动优先原则**当指令为“快速去往东边100米”直接使用 `move_direction` 节点+距离参数严禁使用fly_to_waypoint
## 七、坐标计算规则(东北天坐标系 ENU
本系统统一使用东北天ENU坐标系
- **X轴**:正方向为**东** (East),负方向为**西** (West)
- **Y轴**:正方向为**北** (North),负方向为**南** (South)
- **Z轴**:正方向为**天** (Up),负方向为**地** (Down)
**仅当指令涉及前往“具体地点”(如广场、大门)的偏移位置时,才需计算绝对坐标并使用`fly_to_waypoint`**
## 八、输出要求
仅输出1个严格符合上述所有规则的JSON对象。

View File

@@ -10,6 +10,8 @@ from openai import OpenAIError
import jsonschema import jsonschema
import requests import requests
import platform # 新增:用于选择合适的中文字体 import platform # 新增:用于选择合适的中文字体
import yaml
from .tools.coordinate_tools import calc_offset_enu, calc_offset_esu_direction_text
# --- 自定义远程嵌入函数 (与ingest.py中定义一致) --- # --- 自定义远程嵌入函数 (与ingest.py中定义一致) ---
from chromadb.api.types import Documents, EmbeddingFunction, Embeddings, Embeddable from chromadb.api.types import Documents, EmbeddingFunction, Embeddings, Embeddable
@@ -303,9 +305,9 @@ def _generate_pytree_schema(allowed_actions: set, allowed_conditions: set) -> di
"properties": { "properties": {
"x": {"type": "number"}, "x": {"type": "number"},
"y": {"type": "number"}, "y": {"type": "number"},
"z": {"type": "number"} "depth": {"type": "number"}
}, },
"required": ["x", "y", "z"] "required": ["x", "y"]
}, },
"minItems": 1 "minItems": 1
}, },
@@ -579,6 +581,8 @@ class PyTreeGenerator:
def __init__(self): def __init__(self):
self.base_dir = os.path.dirname(os.path.abspath(__file__)) self.base_dir = os.path.dirname(os.path.abspath(__file__))
self.prompts_dir = os.path.join(self.base_dir, 'prompts') self.prompts_dir = os.path.join(self.base_dir, 'prompts')
self._prompt_manifest_cache = None
self._prompt_manifest_mtime = None
# Updated output directory for visualizations # Updated output directory for visualizations
self.vis_dir = os.path.abspath(os.path.join(self.base_dir, '..', 'generated_visualizations')) self.vis_dir = os.path.abspath(os.path.join(self.base_dir, '..', 'generated_visualizations'))
@@ -593,10 +597,13 @@ class PyTreeGenerator:
self.reasoning_preview_lines = int(os.getenv("REASONING_PREVIEW_LINES", "20")) self.reasoning_preview_lines = int(os.getenv("REASONING_PREVIEW_LINES", "20"))
except Exception: except Exception:
self.reasoning_preview_lines = 20 self.reasoning_preview_lines = 20
# 加载提示词:复杂模式复用现有 system_prompt.txt简单模式与分类器独立提示词 # 加载提示词:优先使用清单拼接,失败回退到单文件
self.complex_prompt = self._load_prompt("system_prompt.txt") self.complex_prompt = self._load_prompt_from_manifest("system", "system_prompt.txt")
self.simple_prompt = self._load_prompt("simple_mode_prompt.txt") self.scene1_prompt = self._load_prompt_from_manifest("scene1", "scene1_prompt.txt")
self.scene4_prompt = self._load_prompt_from_manifest("scene4", "scene4_prompt.txt")
self.simple_prompt = self._load_prompt_from_manifest("simple", "simple_mode_prompt.txt")
self.classifier_prompt = self._load_prompt("classifier_prompt.txt") self.classifier_prompt = self._load_prompt("classifier_prompt.txt")
self.scene_classifier_prompt = self._load_prompt("scene_classifier_prompt.txt")
# 兼容旧变量名 # 兼容旧变量名
self.system_prompt = self.complex_prompt self.system_prompt = self.complex_prompt
@@ -644,6 +651,219 @@ class PyTreeGenerator:
logging.error(f"提示词文件未找到 -> {file_name}") logging.error(f"提示词文件未找到 -> {file_name}")
return "" return ""
def _load_prompt_manifest(self) -> Optional[dict]:
manifest_path = os.path.join(self.prompts_dir, "prompt_manifest.yaml")
try:
manifest_mtime = os.path.getmtime(manifest_path)
except FileNotFoundError:
return None
if (
self._prompt_manifest_cache is not None
and self._prompt_manifest_mtime == manifest_mtime
):
return self._prompt_manifest_cache
try:
with open(manifest_path, "r", encoding="utf-8") as f:
manifest = yaml.safe_load(f) or {}
except Exception as exc:
logging.error(f"提示词清单加载失败 -> {exc}")
return None
if not isinstance(manifest, dict):
logging.error("提示词清单格式错误:顶层必须为映射")
return None
self._prompt_manifest_cache = manifest
self._prompt_manifest_mtime = manifest_mtime
return manifest
def _resolve_prompt_fragment(self, fragment: str) -> str:
if os.path.isabs(fragment):
return fragment
if "/" in fragment:
return os.path.join(self.prompts_dir, fragment)
partial_path = os.path.join(self.prompts_dir, "partials", fragment)
if os.path.exists(partial_path):
return partial_path
return os.path.join(self.prompts_dir, fragment)
def _load_fragment_text(self, fragment_path: str) -> str:
if fragment_path.lower().endswith(".json"):
try:
with open(fragment_path, "r", encoding="utf-8") as f:
payload = json.load(f)
except Exception as exc:
raise ValueError(f"JSON片段解析失败: {exc}") from exc
json_text = json.dumps(payload, ensure_ascii=False, indent=2)
if os.path.basename(fragment_path) == "core_nodes.json":
return "\n".join(
[
"## 一、核心节点定义(格式不可修改,确保后端解析)",
"#### 1. 可用节点定义 (必须遵守)",
"你必须严格从以下JSON定义的列表中选择节点构建行为树不允许使用未定义节点",
"```json",
json_text,
"```",
]
)
return "\n".join(["```json", json_text, "```"])
with open(fragment_path, "r", encoding="utf-8") as f:
return f.read()
def _load_prompt_from_manifest(self, scene_key: str, fallback_file: str) -> str:
manifest = self._load_prompt_manifest()
if not manifest:
return self._load_prompt(fallback_file)
scene_map = manifest.get("scenes", manifest)
fragments = scene_map.get(scene_key)
if not fragments:
logging.warning(f"提示词清单缺少场景配置 -> {scene_key}")
return self._load_prompt(fallback_file)
if not isinstance(fragments, list) or not all(isinstance(item, str) for item in fragments):
logging.warning(f"提示词清单场景配置非法 -> {scene_key}")
return self._load_prompt(fallback_file)
contents = []
for fragment in fragments:
fragment_path = self._resolve_prompt_fragment(fragment)
try:
contents.append(self._load_fragment_text(fragment_path).strip("\n"))
except FileNotFoundError:
logging.error(f"提示词片段未找到 -> {fragment_path}")
return self._load_prompt(fallback_file)
except ValueError as exc:
logging.error(f"提示词片段解析失败 -> {exc}")
return self._load_prompt(fallback_file)
return "\n\n".join(contents).strip("\n")
def _get_tool_definitions(self) -> list[dict]:
return [
{
"type": "function",
"function": {
"name": "calc_offset_enu",
"description": "根据ENU坐标系基准点、方向和距离计算偏移后的坐标。",
"parameters": {
"type": "object",
"properties": {
"base": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"z": {"type": "number"}
},
"required": ["x", "y", "z"]
},
"direction": {
"type": "string",
"enum": [
"east", "west", "north", "south",
"up", "down"
]
},
"distance": {"type": "number", "minimum": 0}
},
"required": ["base", "direction", "distance"]
}
}
},
{
"type": "function",
"function": {
"name": "calc_offset_esu_direction_text",
"description": "根据ESU坐标系基准点、中文方位与距离计算偏移后的坐标。",
"parameters": {
"type": "object",
"properties": {
"base": {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"z": {"type": "number"}
},
"required": ["x", "y", "z"]
},
"direction_text": {
"type": "string",
"description": "中文方位如“南偏东10度”“北偏东10度”“东南”“西北”等"
},
"distance": {"type": "number", "minimum": 0}
},
"required": ["base", "direction_text", "distance"]
}
}
}
]
def _normalize_tool_calls(self, tool_calls: list) -> list[dict]:
normalized = []
for call in tool_calls:
if hasattr(call, "function"):
normalized.append(
{
"id": getattr(call, "id", None),
"name": call.function.name,
"arguments": call.function.arguments,
}
)
elif isinstance(call, dict):
normalized.append(
{
"id": call.get("id"),
"name": (call.get("function") or {}).get("name"),
"arguments": (call.get("function") or {}).get("arguments"),
}
)
return normalized
def _execute_tool_calls(self, tool_calls: list) -> list[dict]:
tool_messages = []
normalized = self._normalize_tool_calls(tool_calls)
for call in normalized:
tool_name = call.get("name")
tool_args = call.get("arguments")
tool_id = call.get("id")
if not tool_name:
logging.warning("工具调用缺少名称,已忽略。")
continue
try:
args = json.loads(tool_args or "{}")
except json.JSONDecodeError:
logging.warning(f"工具调用参数解析失败: {tool_args}")
continue
try:
if tool_name == "calc_offset_enu":
result = calc_offset_enu(**args)
elif tool_name == "calc_offset_esu_direction_text":
result = calc_offset_esu_direction_text(**args)
else:
result = {"error": f"unsupported tool: {tool_name}"}
tool_messages.append(
{
"role": "tool",
"tool_call_id": tool_id,
"content": json.dumps(result, ensure_ascii=False)
}
)
except Exception as exc:
logging.warning(f"工具调用执行失败({tool_name}): {exc}")
tool_messages.append(
{
"role": "tool",
"tool_call_id": tool_id,
"content": json.dumps({"error": str(exc)}, ensure_ascii=False)
}
)
return tool_messages
def _retrieve_context(self, query: str) -> Optional[str]: def _retrieve_context(self, query: str) -> Optional[str]:
logging.info("--- 开始从向量数据库检索上下文 ---") logging.info("--- 开始从向量数据库检索上下文 ---")
try: try:
@@ -661,6 +881,35 @@ class PyTreeGenerator:
logging.error(f"从向量数据库检索时发生错误: {e}") logging.error(f"从向量数据库检索时发生错误: {e}")
return None return None
def _should_enable_tools(self, user_prompt: str, retrieved_context: Optional[str]) -> bool:
if not user_prompt:
return False
direction_patterns = [
"东边", "西边", "南边", "北边",
"东侧", "西侧", "南侧", "北侧",
"往东", "往西", "往南", "往北",
"向东", "向西", "向南", "向北",
"东南", "西南", "东北", "西北",
"南偏东", "南偏西", "北偏东", "北偏西",
]
has_direction = any(pat in user_prompt for pat in direction_patterns)
has_distance = re.search(r"\d+(\.\d+)?\s*(米|m)", user_prompt) is not None
if not (has_direction and has_distance):
return False
if not retrieved_context:
return False
place_candidates: list[str] = []
for line in retrieved_context.splitlines():
if "地点:" in line or "别名:" in line:
place_candidates.extend(re.findall(r"'([^']+)'", line))
if not place_candidates:
return False
return any(place in user_prompt for place in place_candidates)
def _save_history(self, prompt: str, result_dict: dict): def _save_history(self, prompt: str, result_dict: dict):
"""保存请求和响应历史记录""" """保存请求和响应历史记录"""
import datetime import datetime
@@ -691,13 +940,13 @@ class PyTreeGenerator:
""" """
logging.info(f"接收到用户请求: {user_prompt}") logging.info(f"接收到用户请求: {user_prompt}")
# 第一步:分类(简单/复杂 # 第一步:场景分类(simple/scene1/scene4
mode = "complex" scene_mode = "scene1"
try: try:
classifier_resp = self.classifier_client.chat.completions.create( classifier_resp = self.classifier_client.chat.completions.create(
model=self.classifier_model, model=self.classifier_model,
messages=[ messages=[
{"role": "system", "content": self.classifier_prompt or "你是一个分类器只输出JSON。"}, {"role": "system", "content": self.scene_classifier_prompt or "你是一个分类器只输出JSON。"},
{"role": "user", "content": user_prompt} {"role": "user", "content": user_prompt}
], ],
temperature=0.0, temperature=0.0,
@@ -709,16 +958,23 @@ class PyTreeGenerator:
) )
class_str = classifier_resp.choices[0].message.content class_str = classifier_resp.choices[0].message.content
class_obj = json.loads(class_str) class_obj = json.loads(class_str)
if isinstance(class_obj, dict) and class_obj.get("mode") in ("simple", "complex"): if isinstance(class_obj, dict) and class_obj.get("mode") in ("simple", "scene1", "scene4"):
mode = class_obj.get("mode") scene_mode = class_obj.get("mode")
logging.info(f"分类结果: {mode}") logging.info(f"场景分类结果: {scene_mode}")
except Exception as e: except Exception as e:
logging.warning(f"分类失败,默认按复杂指令处理: {e}") logging.warning(f"场景分类失败,默认按scene1处理: {e}")
# 第二步:根据模式准备提示词与上下文(简单与复杂都执行检索增强) # 第二步:根据模式准备提示词与上下文(简单与复杂都执行检索增强)
# 基于模式选择提示词;复杂模式追加一条强制规则,避免模型误输出简单结构 # 基于场景选择提示词;非simple时追加强制规则,避免模型误输出简单结构
use_prompt = self.simple_prompt if mode == "simple" else ( if scene_mode == "simple":
(self.complex_prompt or "") + use_prompt = self.simple_prompt
elif scene_mode == "scene4":
use_prompt = self.scene4_prompt
else:
use_prompt = self.scene1_prompt
if scene_mode != "simple":
use_prompt = (
(use_prompt or self.complex_prompt or "") +
"\n\n【强制规则】仅生成包含root的复杂行为树JSON不得输出简单模式不得包含mode字段或仅有action节点" "\n\n【强制规则】仅生成包含root的复杂行为树JSON不得输出简单模式不得包含mode字段或仅有action节点"
) )
final_user_prompt = user_prompt final_user_prompt = user_prompt
@@ -740,37 +996,92 @@ class PyTreeGenerator:
# System Prompt: use_prompt不包含RAG结果 # System Prompt: use_prompt不包含RAG结果
# User Prompt: final_user_prompt包含原始user_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}" final_prompt = f"=== System Prompt ===\n{use_prompt}\n\n=== User Prompt ===\n{final_user_prompt}"
tool_enabled = self._should_enable_tools(user_prompt, retrieved_context)
for attempt in range(3): for attempt in range(3):
logging.info(f"--- 第 {attempt + 1}/3 次尝试生成Pytree ---") logging.info(f"--- 第 {attempt + 1}/3 次尝试生成Pytree ---")
try: try:
# 简单/复杂分流到不同模型与提示词 # 简单/复杂分流到不同模型与提示词
client = self.simple_llm_client if mode == "simple" else self.complex_llm_client is_simple = scene_mode == "simple"
model_name = self.simple_model if mode == "simple" else self.complex_model client = self.simple_llm_client if is_simple else self.complex_llm_client
model_name = self.simple_model if is_simple else self.complex_model
messages = [
{"role": "system", "content": use_prompt},
{"role": "user", "content": final_user_prompt}
]
# 始终强制JSON响应并禁用思考功能 # 始终强制JSON响应并禁用思考功能
response_kwargs = { response_kwargs = {
"model": model_name, "model": model_name,
"messages": [ "messages": messages,
{"role": "system", "content": use_prompt}, "temperature": 0.0 if is_simple else 0.1,
{"role": "user", "content": final_user_prompt}
],
"temperature": 0.1 if mode == "complex" else 0.0,
"response_format": {"type": "json_object"}, # 始终强制JSON输出禁用思考功能 "response_format": {"type": "json_object"}, # 始终强制JSON输出禁用思考功能
# 禁用 Qwen3 模型的思考功能(通过 extra_body 传递)
# 注意:如果 API 服务器不支持此参数,会忽略
"extra_body": {"chat_template_kwargs": {"enable_thinking": False}} "extra_body": {"chat_template_kwargs": {"enable_thinking": False}}
} }
if tool_enabled:
response_kwargs["tools"] = self._get_tool_definitions()
response_kwargs["tool_choice"] = "auto"
# 基于模式设定最大输出token数直接在代码中配置 # 基于模式设定最大输出token数直接在代码中配置
response_kwargs["max_tokens"] = self.simple_max_tokens if mode == "simple" else self.complex_max_tokens response_kwargs["max_tokens"] = self.simple_max_tokens if is_simple else self.complex_max_tokens
response = client.chat.completions.create(**response_kwargs) response = client.chat.completions.create(**response_kwargs)
# 工具调用处理执行工具并回填后强制模型输出JSON
for tool_round in range(3):
try:
first_msg = response.choices[0].message
tool_calls = getattr(first_msg, "tool_calls", None)
except Exception:
first_msg = response.choices[0].get("message") if isinstance(response.choices[0], dict) else None
tool_calls = (first_msg or {}).get("tool_calls")
if not tool_calls:
break
logging.info("检测到工具调用,执行第 %d 轮工具回填。", tool_round + 1)
tool_messages = self._execute_tool_calls(tool_calls)
if not tool_messages:
break
tool_calls_for_messages = []
for call in self._normalize_tool_calls(tool_calls):
if not call.get("id") or not call.get("name"):
continue
tool_calls_for_messages.append(
{
"id": call["id"],
"type": "function",
"function": {
"name": call["name"],
"arguments": call.get("arguments", "")
}
}
)
messages = messages + [
{
"role": "assistant",
"content": getattr(first_msg, "content", "") if first_msg else "",
"tool_calls": tool_calls_for_messages
}
] + tool_messages
followup_kwargs = dict(response_kwargs)
followup_kwargs["messages"] = messages
followup_kwargs.pop("tools", None)
followup_kwargs.pop("tool_choice", None)
response = client.chat.completions.create(**followup_kwargs)
break
# 兼容可能存在的 reasoning_content 字段 # 兼容可能存在的 reasoning_content 字段
try: try:
msg = response.choices[0].message msg = response.choices[0].message
msg_content = getattr(msg, "content", None) msg_content = getattr(msg, "content", None)
msg_reasoning = getattr(msg, "reasoning_content", None) msg_reasoning = getattr(msg, "reasoning_content", None)
remaining_tool_calls = getattr(msg, "tool_calls", None)
except Exception: except Exception:
msg = response.choices[0]["message"] if isinstance(response.choices[0], dict) else None msg = response.choices[0]["message"] if isinstance(response.choices[0], dict) else None
msg_content = (msg or {}).get("content") if isinstance(msg, dict) else None msg_content = (msg or {}).get("content") if isinstance(msg, dict) else None
msg_reasoning = (msg or {}).get("reasoning_content") if isinstance(msg, dict) else None msg_reasoning = (msg or {}).get("reasoning_content") if isinstance(msg, dict) else None
remaining_tool_calls = (msg or {}).get("tool_calls") if isinstance(msg, dict) else None
if (msg_content is None or str(msg_content).strip() == "") and remaining_tool_calls:
logging.warning("模型仍在请求工具调用未返回JSON内容重试下一次。")
continue
combined_text = "" combined_text = ""
if isinstance(msg_reasoning, str) and msg_reasoning.strip(): if isinstance(msg_reasoning, str) and msg_reasoning.strip():
@@ -833,7 +1144,7 @@ class PyTreeGenerator:
continue continue
# 简单/复杂分别验证与返回 # 简单/复杂分别验证与返回
if mode == "simple": if scene_mode == "simple":
try: try:
jsonschema.validate(instance=pytree_dict, schema=self.simple_schema) jsonschema.validate(instance=pytree_dict, schema=self.simple_schema)
# 手动检查简单模式的root节点不能有children或children必须是空数组 # 手动检查简单模式的root节点不能有children或children必须是空数组

View File

@@ -0,0 +1 @@
"""工具模块包。"""

View File

@@ -0,0 +1,110 @@
from __future__ import annotations
import math
import re
from typing import Dict, Tuple
_DIRECTION_DELTAS: Dict[str, Tuple[int, int, int]] = {
"east": (1, 0, 0),
"west": (-1, 0, 0),
"north": (0, -1, 0),
"south": (0, 1, 0),
"up": (0, 0, 1),
"down": (0, 0, -1),
}
def calc_offset_enu(base: Dict[str, float], direction: str, distance: float) -> Dict[str, float]:
"""在ENU坐标系下按方向偏移固定距离。"""
if distance < 0:
raise ValueError("distance must be non-negative")
if not isinstance(base, dict):
raise ValueError("base must be an object with x/y/z")
direction_key = (direction or "").strip().lower()
if direction_key not in _DIRECTION_DELTAS:
raise ValueError(f"unsupported direction: {direction}")
try:
x = float(base["x"])
y = float(base["y"])
z = float(base["z"])
except Exception as exc:
raise ValueError("base must contain numeric x/y/z") from exc
dx, dy, dz = _DIRECTION_DELTAS[direction_key]
offset_x = x + dx * distance
offset_y = y + dy * distance
offset_z = z + dz * distance
return {"x": offset_x, "y": offset_y, "z": offset_z}
_DIRECTION_TEXT_DIAGONALS_ESU: Dict[str, Tuple[float, float]] = {
"东南": (1.0, 1.0),
"西南": (-1.0, 1.0),
"东北": (1.0, -1.0),
"西北": (-1.0, -1.0),
}
def _parse_direction_text_esu(direction_text: str) -> Tuple[float, float]:
text = (direction_text or "").strip().replace(" ", "")
if not text:
raise ValueError("direction_text must be a non-empty string")
for prefix in ("", ""):
if text.startswith(prefix):
text = text[len(prefix):]
if text in _DIRECTION_TEXT_DIAGONALS_ESU:
x, y = _DIRECTION_TEXT_DIAGONALS_ESU[text]
length = math.hypot(x, y)
return x / length, y / length
if text in ("", "西", "", ""):
if text == "":
return 1.0, 0.0
if text == "西":
return -1.0, 0.0
if text == "":
return 0.0, 1.0
return 0.0, -1.0
match = re.match(r"^(南|北)偏(东|西)(\d+(?:\.\d+)?)度?$", text)
if match:
base_dir, toward_dir, angle_str = match.groups()
angle_deg = float(angle_str)
if angle_deg < 0 or angle_deg > 90:
raise ValueError("angle must be between 0 and 90 degrees")
angle_rad = math.radians(angle_deg)
x_sign = 1.0 if toward_dir == "" else -1.0
if base_dir == "":
return math.sin(angle_rad) * x_sign, math.cos(angle_rad)
return math.sin(angle_rad) * x_sign, -math.cos(angle_rad)
raise ValueError(f"unsupported direction_text: {direction_text}")
def calc_offset_esu_direction_text(
base: Dict[str, float], direction_text: str, distance: float
) -> Dict[str, float]:
"""在ESU坐标系下按中文方位偏移固定距离。"""
if distance < 0:
raise ValueError("distance must be non-negative")
if not isinstance(base, dict):
raise ValueError("base must be an object with x/y/z")
try:
x = float(base["x"])
y = float(base["y"])
z = float(base["z"])
except Exception as exc:
raise ValueError("base must contain numeric x/y/z") from exc
dx_unit, dy_unit = _parse_direction_text_esu(direction_text)
offset_x = x + dx_unit * distance
offset_y = y + dy_unit * distance
return {"x": offset_x, "y": offset_y, "z": z}

View File

@@ -1,4 +1,4 @@
#!/home/a/DronePlanning/backend_service/venv/bin/python3 #!/home/huangfukk/DronePlanning/backend_service/venv/bin/python3
import sys import sys
from chromadb.cli.cli import app from chromadb.cli.cli import app
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,4 +1,4 @@
#!/home/a/DronePlanning/backend_service/venv/bin/python3 #!/home/huangfukk/DronePlanning/backend_service/venv/bin/python3
import sys import sys
from numpy.f2py.f2py2e import main from numpy.f2py.f2py2e import main
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -0,0 +1,7 @@
#!/home/huangfukk/DronePlanning/backend_service/venv/bin/python3
import sys
from filetype.__main__ import main
if __name__ == '__main__':
if sys.argv[0].endswith('.exe'):
sys.argv[0] = sys.argv[0][:-4]
sys.exit(main())

View File

@@ -1,4 +1,4 @@
#!/home/a/DronePlanning/backend_service/venv/bin/python3 #!/home/huangfukk/DronePlanning/backend_service/venv/bin/python3
import sys import sys
from huggingface_hub.cli.hf import main from huggingface_hub.cli.hf import main
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,4 +1,4 @@
#!/home/a/DronePlanning/backend_service/venv/bin/python3 #!/home/huangfukk/DronePlanning/backend_service/venv/bin/python3
import sys import sys
from jsonschema.cli import main from jsonschema.cli import main
if __name__ == '__main__': if __name__ == '__main__':

7
backend_service/venv/bin/nltk Executable file
View File

@@ -0,0 +1,7 @@
#!/home/huangfukk/DronePlanning/backend_service/venv/bin/python3
import sys
from nltk.cli import cli
if __name__ == '__main__':
if sys.argv[0].endswith('.exe'):
sys.argv[0] = sys.argv[0][:-4]
sys.exit(cli())

View File

@@ -1,4 +1,4 @@
#!/home/a/DronePlanning/backend_service/venv/bin/python3 #!/home/huangfukk/DronePlanning/backend_service/venv/bin/python3
import sys import sys
from numpy._configtool import main from numpy._configtool import main
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,4 +1,4 @@
#!/home/a/DronePlanning/backend_service/venv/bin/python3 #!/home/huangfukk/DronePlanning/backend_service/venv/bin/python3
import sys import sys
from openai.cli import main from openai.cli import main
if __name__ == '__main__': if __name__ == '__main__':

7
backend_service/venv/bin/oxmsg Executable file
View File

@@ -0,0 +1,7 @@
#!/home/huangfukk/DronePlanning/backend_service/venv/bin/python3
import sys
from oxmsg.cli import oxmsg
if __name__ == '__main__':
if sys.argv[0].endswith('.exe'):
sys.argv[0] = sys.argv[0][:-4]
sys.exit(oxmsg())

View File

@@ -1,4 +1,4 @@
#!/home/a/DronePlanning/backend_service/venv/bin/python3 #!/home/huangfukk/DronePlanning/backend_service/venv/bin/python3
import sys import sys
from pip._internal.cli.main import main from pip._internal.cli.main import main
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,4 +1,4 @@
#!/home/a/DronePlanning/backend_service/venv/bin/python3 #!/home/huangfukk/DronePlanning/backend_service/venv/bin/python3
import sys import sys
from pip._internal.cli.main import main from pip._internal.cli.main import main
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,4 +1,4 @@
#!/home/a/DronePlanning/backend_service/venv/bin/python3 #!/home/huangfukk/DronePlanning/backend_service/venv/bin/python3
import sys import sys
from pip._internal.cli.main import main from pip._internal.cli.main import main
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,4 +1,4 @@
#!/home/a/DronePlanning/backend_service/venv/bin/python3 #!/home/huangfukk/DronePlanning/backend_service/venv/bin/python3
import sys import sys
from pybase64.__main__ import main from pybase64.__main__ import main
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1 +1 @@
/home/a/miniconda3/bin/python3 /home/huangfukk/miniconda3/bin/python3

View File

@@ -1,4 +1,4 @@
#!/home/a/DronePlanning/backend_service/venv/bin/python3 #!/home/huangfukk/DronePlanning/backend_service/venv/bin/python3
import sys import sys
from huggingface_hub.inference._mcp.cli import app from huggingface_hub.inference._mcp.cli import app
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,4 +1,4 @@
#!/home/a/DronePlanning/backend_service/venv/bin/python3 #!/home/huangfukk/DronePlanning/backend_service/venv/bin/python3
import sys import sys
from typer.cli import main from typer.cli import main
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,4 +1,4 @@
#!/home/a/DronePlanning/backend_service/venv/bin/python3 #!/home/huangfukk/DronePlanning/backend_service/venv/bin/python3
import sys import sys
from uvicorn.main import main from uvicorn.main import main
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -0,0 +1,37 @@
#!/usr/local/bin/python
# -*- coding: latin-1 -*-
"""
olefile (formerly OleFileIO_PL)
Module to read/write Microsoft OLE2 files (also called Structured Storage or
Microsoft Compound Document File Format), such as Microsoft Office 97-2003
documents, Image Composer and FlashPix files, Outlook messages, ...
This version is compatible with Python 2.6+ and 3.x
Project website: http://www.decalage.info/olefile
olefile is copyright (c) 2005-2015 Philippe Lagadec (http://www.decalage.info)
olefile is based on the OleFileIO module from the PIL library v1.1.6
See: http://www.pythonware.com/products/pil/index.htm
The Python Imaging Library (PIL) is
Copyright (c) 1997-2005 by Secret Labs AB
Copyright (c) 1995-2005 by Fredrik Lundh
See source code and LICENSE.txt for information on usage and redistribution.
"""
# The OleFileIO_PL module is for backward compatibility
try:
# first try to import olefile for Python 2.6+/3.x
from olefile.olefile import *
# import metadata not covered by *:
from olefile.olefile import __version__, __author__, __date__
except:
# if it fails, fallback to the old version olefile2 for Python 2.x:
from olefile.olefile2 import *
# import metadata not covered by *:
from olefile.olefile2 import __doc__, __version__, __author__, __date__

View File

@@ -0,0 +1,209 @@
Metadata-Version: 2.4
Name: aiofiles
Version: 25.1.0
Summary: File support for asyncio.
Project-URL: Changelog, https://github.com/Tinche/aiofiles#history
Project-URL: Bug Tracker, https://github.com/Tinche/aiofiles/issues
Project-URL: Repository, https://github.com/Tinche/aiofiles
Author-email: Tin Tvrtkovic <tinchester@gmail.com>
License: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: AsyncIO
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Python: >=3.9
Description-Content-Type: text/markdown
# aiofiles: file support for asyncio
[![PyPI](https://img.shields.io/pypi/v/aiofiles.svg)](https://pypi.python.org/pypi/aiofiles)
[![Build](https://github.com/Tinche/aiofiles/workflows/CI/badge.svg)](https://github.com/Tinche/aiofiles/actions)
[![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/Tinche/882f02e3df32136c847ba90d2688f06e/raw/covbadge.json)](https://github.com/Tinche/aiofiles/actions/workflows/main.yml)
[![Supported Python versions](https://img.shields.io/pypi/pyversions/aiofiles.svg)](https://github.com/Tinche/aiofiles)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
**aiofiles** is an Apache2 licensed library, written in Python, for handling local
disk files in asyncio applications.
Ordinary local file IO is blocking, and cannot easily and portably be made
asynchronous. This means doing file IO may interfere with asyncio applications,
which shouldn't block the executing thread. aiofiles helps with this by
introducing asynchronous versions of files that support delegating operations to
a separate thread pool.
```python
async with aiofiles.open('filename', mode='r') as f:
contents = await f.read()
print(contents)
'My file contents'
```
Asynchronous iteration is also supported.
```python
async with aiofiles.open('filename') as f:
async for line in f:
...
```
Asynchronous interface to tempfile module.
```python
async with aiofiles.tempfile.TemporaryFile('wb') as f:
await f.write(b'Hello, World!')
```
## Features
- a file API very similar to Python's standard, blocking API
- support for buffered and unbuffered binary files, and buffered text files
- support for `async`/`await` ([PEP 492](https://peps.python.org/pep-0492/)) constructs
- async interface to tempfile module
## Installation
To install aiofiles, simply:
```shell
pip install aiofiles
```
## Usage
Files are opened using the `aiofiles.open()` coroutine, which in addition to
mirroring the builtin `open` accepts optional `loop` and `executor`
arguments. If `loop` is absent, the default loop will be used, as per the
set asyncio policy. If `executor` is not specified, the default event loop
executor will be used.
In case of success, an asynchronous file object is returned with an
API identical to an ordinary file, except the following methods are coroutines
and delegate to an executor:
- `close`
- `flush`
- `isatty`
- `read`
- `readall`
- `read1`
- `readinto`
- `readline`
- `readlines`
- `seek`
- `seekable`
- `tell`
- `truncate`
- `writable`
- `write`
- `writelines`
In case of failure, one of the usual exceptions will be raised.
`aiofiles.stdin`, `aiofiles.stdout`, `aiofiles.stderr`,
`aiofiles.stdin_bytes`, `aiofiles.stdout_bytes`, and
`aiofiles.stderr_bytes` provide async access to `sys.stdin`,
`sys.stdout`, `sys.stderr`, and their corresponding `.buffer` properties.
The `aiofiles.os` module contains executor-enabled coroutine versions of
several useful `os` functions that deal with files:
- `stat`
- `statvfs`
- `sendfile`
- `rename`
- `renames`
- `replace`
- `remove`
- `unlink`
- `mkdir`
- `makedirs`
- `rmdir`
- `removedirs`
- `link`
- `symlink`
- `readlink`
- `listdir`
- `scandir`
- `access`
- `getcwd`
- `path.abspath`
- `path.exists`
- `path.isfile`
- `path.isdir`
- `path.islink`
- `path.ismount`
- `path.getsize`
- `path.getatime`
- `path.getctime`
- `path.samefile`
- `path.sameopenfile`
### Tempfile
**aiofiles.tempfile** implements the following interfaces:
- TemporaryFile
- NamedTemporaryFile
- SpooledTemporaryFile
- TemporaryDirectory
Results return wrapped with a context manager allowing use with async with and async for.
```python
async with aiofiles.tempfile.NamedTemporaryFile('wb+') as f:
await f.write(b'Line1\n Line2')
await f.seek(0)
async for line in f:
print(line)
async with aiofiles.tempfile.TemporaryDirectory() as d:
filename = os.path.join(d, "file.ext")
```
### Writing tests for aiofiles
Real file IO can be mocked by patching `aiofiles.threadpool.sync_open`
as desired. The return type also needs to be registered with the
`aiofiles.threadpool.wrap` dispatcher:
```python
aiofiles.threadpool.wrap.register(mock.MagicMock)(
lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase(*args, **kwargs)
)
async def test_stuff():
write_data = 'data'
read_file_chunks = [
b'file chunks 1',
b'file chunks 2',
b'file chunks 3',
b'',
]
file_chunks_iter = iter(read_file_chunks)
mock_file_stream = mock.MagicMock(
read=lambda *args, **kwargs: next(file_chunks_iter)
)
with mock.patch('aiofiles.threadpool.sync_open', return_value=mock_file_stream) as mock_open:
async with aiofiles.open('filename', 'w') as f:
await f.write(write_data)
assert await f.read() == b'file chunks 1'
mock_file_stream.write.assert_called_once_with(write_data)
```
### Contributing
Contributions are very welcome. Tests can be run with `tox`, please ensure
the coverage at least stays the same before you submit a pull request.

View File

@@ -0,0 +1,26 @@
aiofiles-25.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
aiofiles-25.1.0.dist-info/METADATA,sha256=a5a5kHMVigDdsBKlFINLSMPsX3Ms4Fn_zecASBdZqLU,6291
aiofiles-25.1.0.dist-info/RECORD,,
aiofiles-25.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
aiofiles-25.1.0.dist-info/licenses/LICENSE,sha256=y16Ofl9KOYjhBjwULGDcLfdWBfTEZRXnduOspt-XbhQ,11325
aiofiles-25.1.0.dist-info/licenses/NOTICE,sha256=EExY0dRQvWR0wJ2LZLwBgnM6YKw9jCU-M0zegpRSD_E,55
aiofiles/__init__.py,sha256=DYqUwak6MVosBjbAsgyEnFFP-HUZCG5h7X4owoeyYHw,345
aiofiles/__pycache__/__init__.cpython-313.pyc,,
aiofiles/__pycache__/base.cpython-313.pyc,,
aiofiles/__pycache__/os.cpython-313.pyc,,
aiofiles/__pycache__/ospath.cpython-313.pyc,,
aiofiles/base.py,sha256=-fvh41PnictTZL3cg98HoN4h6jdebi5d7Mfh81zOBOc,2046
aiofiles/os.py,sha256=slJ5oUNHVW1xWVuuIWQiYjw30n3L48H7oX4CJvD_1d4,1078
aiofiles/ospath.py,sha256=c-Kqw4wMCZ-YRt8Jleb697cANwJQM9qux6lq97949C8,678
aiofiles/tempfile/__init__.py,sha256=twoC7vaQ-JjFzh2Bbd-3-o0hmExH3CYJUmQcuiVwZfg,10207
aiofiles/tempfile/__pycache__/__init__.cpython-313.pyc,,
aiofiles/tempfile/__pycache__/temptypes.cpython-313.pyc,,
aiofiles/tempfile/temptypes.py,sha256=3_hlc6l9r5wmino1fDrt4TpFlX4IKoR5IP_bBYVVuHg,2037
aiofiles/threadpool/__init__.py,sha256=-65UURmzUHsGTXUz0TARdSzyXIfkCFtbczAQLEPpEcU,3140
aiofiles/threadpool/__pycache__/__init__.cpython-313.pyc,,
aiofiles/threadpool/__pycache__/binary.cpython-313.pyc,,
aiofiles/threadpool/__pycache__/text.cpython-313.pyc,,
aiofiles/threadpool/__pycache__/utils.cpython-313.pyc,,
aiofiles/threadpool/binary.py,sha256=hp-km9VCRu0MLz_wAEUfbCz7OL7xtn9iGAawabpnp5U,2315
aiofiles/threadpool/text.py,sha256=fNmpw2PEkj0BZSldipJXAgZqVGLxALcfOMiuDQ54Eas,1223
aiofiles/threadpool/utils.py,sha256=VtIJ9KErbcIT9_Yz4V4rZgNEUjBH3cAYxzKQBMpEzik,1850

View File

@@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: hatchling 1.27.0
Root-Is-Purelib: true
Tag: py3-none-any

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,2 @@
Asyncio support for files
Copyright 2016 Tin Tvrtkovic

View File

@@ -0,0 +1,23 @@
"""Utilities for asyncio-friendly file handling."""
from . import tempfile
from .threadpool import (
open,
stderr,
stderr_bytes,
stdin,
stdin_bytes,
stdout,
stdout_bytes,
)
__all__ = [
"open",
"tempfile",
"stdin",
"stdout",
"stderr",
"stdin_bytes",
"stdout_bytes",
"stderr_bytes",
]

View File

@@ -0,0 +1,79 @@
from asyncio import get_running_loop
from collections.abc import Awaitable
from contextlib import AbstractAsyncContextManager
from functools import partial, wraps
def wrap(func):
@wraps(func)
async def run(*args, loop=None, executor=None, **kwargs):
if loop is None:
loop = get_running_loop()
pfunc = partial(func, *args, **kwargs)
return await loop.run_in_executor(executor, pfunc)
return run
class AsyncBase:
def __init__(self, file, loop, executor):
self._file = file
self._executor = executor
self._ref_loop = loop
@property
def _loop(self):
return self._ref_loop or get_running_loop()
def __aiter__(self):
"""We are our own iterator."""
return self
def __repr__(self):
return super().__repr__() + " wrapping " + repr(self._file)
async def __anext__(self):
"""Simulate normal file iteration."""
if line := await self.readline():
return line
raise StopAsyncIteration
class AsyncIndirectBase(AsyncBase):
def __init__(self, name, loop, executor, indirect):
self._indirect = indirect
self._name = name
super().__init__(None, loop, executor)
@property
def _file(self):
return self._indirect()
@_file.setter
def _file(self, v):
pass # discard writes
class AiofilesContextManager(Awaitable, AbstractAsyncContextManager):
"""An adjusted async context manager for aiofiles."""
__slots__ = ("_coro", "_obj")
def __init__(self, coro):
self._coro = coro
self._obj = None
def __await__(self):
if self._obj is None:
self._obj = yield from self._coro.__await__()
return self._obj
async def __aenter__(self):
return await self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await get_running_loop().run_in_executor(
None, self._obj._file.__exit__, exc_type, exc_val, exc_tb
)
self._obj = None

View File

@@ -0,0 +1,61 @@
"""Async executor versions of file functions from the os module."""
import os
from . import ospath as path
from .base import wrap
__all__ = [
"path",
"stat",
"rename",
"renames",
"replace",
"remove",
"unlink",
"mkdir",
"makedirs",
"rmdir",
"removedirs",
"symlink",
"readlink",
"listdir",
"scandir",
"access",
"wrap",
"getcwd",
]
access = wrap(os.access)
getcwd = wrap(os.getcwd)
listdir = wrap(os.listdir)
makedirs = wrap(os.makedirs)
mkdir = wrap(os.mkdir)
readlink = wrap(os.readlink)
remove = wrap(os.remove)
removedirs = wrap(os.removedirs)
rename = wrap(os.rename)
renames = wrap(os.renames)
replace = wrap(os.replace)
rmdir = wrap(os.rmdir)
scandir = wrap(os.scandir)
stat = wrap(os.stat)
symlink = wrap(os.symlink)
unlink = wrap(os.unlink)
if hasattr(os, "link"):
__all__ += ["link"]
link = wrap(os.link)
if hasattr(os, "sendfile"):
__all__ += ["sendfile"]
sendfile = wrap(os.sendfile)
if hasattr(os, "statvfs"):
__all__ += ["statvfs"]
statvfs = wrap(os.statvfs)

View File

@@ -0,0 +1,37 @@
"""Async executor versions of file functions from the os.path module."""
from os import path
from .base import wrap
__all__ = [
"abspath",
"getatime",
"getctime",
"getmtime",
"getsize",
"exists",
"isdir",
"isfile",
"islink",
"ismount",
"samefile",
"sameopenfile",
]
abspath = wrap(path.abspath)
getatime = wrap(path.getatime)
getctime = wrap(path.getctime)
getmtime = wrap(path.getmtime)
getsize = wrap(path.getsize)
exists = wrap(path.exists)
isdir = wrap(path.isdir)
isfile = wrap(path.isfile)
islink = wrap(path.islink)
ismount = wrap(path.ismount)
samefile = wrap(path.samefile)
sameopenfile = wrap(path.sameopenfile)

View File

@@ -0,0 +1,357 @@
import asyncio
import sys
from functools import partial, singledispatch
from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOBase
from tempfile import NamedTemporaryFile as syncNamedTemporaryFile
from tempfile import SpooledTemporaryFile as syncSpooledTemporaryFile
from tempfile import TemporaryDirectory as syncTemporaryDirectory
from tempfile import TemporaryFile as syncTemporaryFile
from tempfile import _TemporaryFileWrapper as syncTemporaryFileWrapper
from ..base import AiofilesContextManager
from ..threadpool.binary import AsyncBufferedIOBase, AsyncBufferedReader, AsyncFileIO
from ..threadpool.text import AsyncTextIOWrapper
from .temptypes import AsyncSpooledTemporaryFile, AsyncTemporaryDirectory
__all__ = [
"NamedTemporaryFile",
"TemporaryFile",
"SpooledTemporaryFile",
"TemporaryDirectory",
]
# ================================================================
# Public methods for async open and return of temp file/directory
# objects with async interface
# ================================================================
if sys.version_info >= (3, 12):
def NamedTemporaryFile(
mode="w+b",
buffering=-1,
encoding=None,
newline=None,
suffix=None,
prefix=None,
dir=None,
delete=True,
delete_on_close=True,
loop=None,
executor=None,
):
"""Async open a named temporary file"""
return AiofilesContextManager(
_temporary_file(
named=True,
mode=mode,
buffering=buffering,
encoding=encoding,
newline=newline,
suffix=suffix,
prefix=prefix,
dir=dir,
delete=delete,
delete_on_close=delete_on_close,
loop=loop,
executor=executor,
)
)
else:
def NamedTemporaryFile(
mode="w+b",
buffering=-1,
encoding=None,
newline=None,
suffix=None,
prefix=None,
dir=None,
delete=True,
loop=None,
executor=None,
):
"""Async open a named temporary file"""
return AiofilesContextManager(
_temporary_file(
named=True,
mode=mode,
buffering=buffering,
encoding=encoding,
newline=newline,
suffix=suffix,
prefix=prefix,
dir=dir,
delete=delete,
loop=loop,
executor=executor,
)
)
def TemporaryFile(
mode="w+b",
buffering=-1,
encoding=None,
newline=None,
suffix=None,
prefix=None,
dir=None,
loop=None,
executor=None,
):
"""Async open an unnamed temporary file"""
return AiofilesContextManager(
_temporary_file(
named=False,
mode=mode,
buffering=buffering,
encoding=encoding,
newline=newline,
suffix=suffix,
prefix=prefix,
dir=dir,
loop=loop,
executor=executor,
)
)
def SpooledTemporaryFile(
max_size=0,
mode="w+b",
buffering=-1,
encoding=None,
newline=None,
suffix=None,
prefix=None,
dir=None,
loop=None,
executor=None,
):
"""Async open a spooled temporary file"""
return AiofilesContextManager(
_spooled_temporary_file(
max_size=max_size,
mode=mode,
buffering=buffering,
encoding=encoding,
newline=newline,
suffix=suffix,
prefix=prefix,
dir=dir,
loop=loop,
executor=executor,
)
)
def TemporaryDirectory(suffix=None, prefix=None, dir=None, loop=None, executor=None):
"""Async open a temporary directory"""
return AiofilesContextManagerTempDir(
_temporary_directory(
suffix=suffix, prefix=prefix, dir=dir, loop=loop, executor=executor
)
)
# =========================================================
# Internal coroutines to open new temp files/directories
# =========================================================
if sys.version_info >= (3, 12):
async def _temporary_file(
named=True,
mode="w+b",
buffering=-1,
encoding=None,
newline=None,
suffix=None,
prefix=None,
dir=None,
delete=True,
delete_on_close=True,
loop=None,
executor=None,
max_size=0,
):
"""Async method to open a temporary file with async interface"""
if loop is None:
loop = asyncio.get_running_loop()
if named:
cb = partial(
syncNamedTemporaryFile,
mode=mode,
buffering=buffering,
encoding=encoding,
newline=newline,
suffix=suffix,
prefix=prefix,
dir=dir,
delete=delete,
delete_on_close=delete_on_close,
)
else:
cb = partial(
syncTemporaryFile,
mode=mode,
buffering=buffering,
encoding=encoding,
newline=newline,
suffix=suffix,
prefix=prefix,
dir=dir,
)
f = await loop.run_in_executor(executor, cb)
# Wrap based on type of underlying IO object
if type(f) is syncTemporaryFileWrapper:
# _TemporaryFileWrapper was used (named files)
result = wrap(f.file, f, loop=loop, executor=executor)
result._closer = f._closer
return result
# IO object was returned directly without wrapper
return wrap(f, f, loop=loop, executor=executor)
else:
async def _temporary_file(
named=True,
mode="w+b",
buffering=-1,
encoding=None,
newline=None,
suffix=None,
prefix=None,
dir=None,
delete=True,
loop=None,
executor=None,
max_size=0,
):
"""Async method to open a temporary file with async interface"""
if loop is None:
loop = asyncio.get_running_loop()
if named:
cb = partial(
syncNamedTemporaryFile,
mode=mode,
buffering=buffering,
encoding=encoding,
newline=newline,
suffix=suffix,
prefix=prefix,
dir=dir,
delete=delete,
)
else:
cb = partial(
syncTemporaryFile,
mode=mode,
buffering=buffering,
encoding=encoding,
newline=newline,
suffix=suffix,
prefix=prefix,
dir=dir,
)
f = await loop.run_in_executor(executor, cb)
# Wrap based on type of underlying IO object
if type(f) is syncTemporaryFileWrapper:
# _TemporaryFileWrapper was used (named files)
result = wrap(f.file, f, loop=loop, executor=executor)
# add delete property
result.delete = f.delete
return result
# IO object was returned directly without wrapper
return wrap(f, f, loop=loop, executor=executor)
async def _spooled_temporary_file(
max_size=0,
mode="w+b",
buffering=-1,
encoding=None,
newline=None,
suffix=None,
prefix=None,
dir=None,
loop=None,
executor=None,
):
"""Open a spooled temporary file with async interface"""
if loop is None:
loop = asyncio.get_running_loop()
cb = partial(
syncSpooledTemporaryFile,
max_size=max_size,
mode=mode,
buffering=buffering,
encoding=encoding,
newline=newline,
suffix=suffix,
prefix=prefix,
dir=dir,
)
f = await loop.run_in_executor(executor, cb)
# Single interface provided by SpooledTemporaryFile for all modes
return AsyncSpooledTemporaryFile(f, loop=loop, executor=executor)
async def _temporary_directory(
suffix=None, prefix=None, dir=None, loop=None, executor=None
):
"""Async method to open a temporary directory with async interface"""
if loop is None:
loop = asyncio.get_running_loop()
cb = partial(syncTemporaryDirectory, suffix, prefix, dir)
f = await loop.run_in_executor(executor, cb)
return AsyncTemporaryDirectory(f, loop=loop, executor=executor)
class AiofilesContextManagerTempDir(AiofilesContextManager):
"""With returns the directory location, not the object (matching sync lib)"""
async def __aenter__(self):
self._obj = await self._coro
return self._obj.name
@singledispatch
def wrap(base_io_obj, file, *, loop=None, executor=None):
"""Wrap the object with interface based on type of underlying IO"""
msg = f"Unsupported IO type: {base_io_obj}"
raise TypeError(msg)
@wrap.register(TextIOBase)
def _(base_io_obj, file, *, loop=None, executor=None):
return AsyncTextIOWrapper(file, loop=loop, executor=executor)
@wrap.register(BufferedWriter)
def _(base_io_obj, file, *, loop=None, executor=None):
return AsyncBufferedIOBase(file, loop=loop, executor=executor)
@wrap.register(BufferedReader)
@wrap.register(BufferedRandom)
def _(base_io_obj, file, *, loop=None, executor=None):
return AsyncBufferedReader(file, loop=loop, executor=executor)
@wrap.register(FileIO)
def _(base_io_obj, file, *, loop=None, executor=None):
return AsyncFileIO(file, loop=loop, executor=executor)

View File

@@ -0,0 +1,70 @@
"""Async wrappers for spooled temp files and temp directory objects"""
from functools import partial
from ..base import AsyncBase
from ..threadpool.utils import (
cond_delegate_to_executor,
delegate_to_executor,
proxy_property_directly,
)
@delegate_to_executor("fileno", "rollover")
@cond_delegate_to_executor(
"close",
"flush",
"isatty",
"read",
"readline",
"readlines",
"seek",
"tell",
"truncate",
)
@proxy_property_directly("closed", "encoding", "mode", "name", "newlines")
class AsyncSpooledTemporaryFile(AsyncBase):
"""Async wrapper for SpooledTemporaryFile class"""
async def _check(self):
if self._file._rolled:
return
max_size = self._file._max_size
if max_size and self._file.tell() > max_size:
await self.rollover()
async def write(self, s):
"""Implementation to anticipate rollover"""
if self._file._rolled:
cb = partial(self._file.write, s)
return await self._loop.run_in_executor(self._executor, cb)
file = self._file._file # reference underlying base IO object
rv = file.write(s)
await self._check()
return rv
async def writelines(self, iterable):
"""Implementation to anticipate rollover"""
if self._file._rolled:
cb = partial(self._file.writelines, iterable)
return await self._loop.run_in_executor(self._executor, cb)
file = self._file._file # reference underlying base IO object
rv = file.writelines(iterable)
await self._check()
return rv
@delegate_to_executor("cleanup")
@proxy_property_directly("name")
class AsyncTemporaryDirectory:
"""Async wrapper for TemporaryDirectory class"""
def __init__(self, file, loop, executor):
self._file = file
self._loop = loop
self._executor = executor
async def close(self):
await self.cleanup()

View File

@@ -0,0 +1,141 @@
"""Handle files using a thread pool executor."""
import asyncio
import sys
from functools import partial, singledispatch
from io import (
BufferedIOBase,
BufferedRandom,
BufferedReader,
BufferedWriter,
FileIO,
TextIOBase,
)
from ..base import AiofilesContextManager
from .binary import (
AsyncBufferedIOBase,
AsyncBufferedReader,
AsyncFileIO,
AsyncIndirectBufferedIOBase,
)
from .text import AsyncTextIndirectIOWrapper, AsyncTextIOWrapper
sync_open = open
__all__ = (
"open",
"stdin",
"stdout",
"stderr",
"stdin_bytes",
"stdout_bytes",
"stderr_bytes",
)
def open(
file,
mode="r",
buffering=-1,
encoding=None,
errors=None,
newline=None,
closefd=True,
opener=None,
*,
loop=None,
executor=None,
):
return AiofilesContextManager(
_open(
file,
mode=mode,
buffering=buffering,
encoding=encoding,
errors=errors,
newline=newline,
closefd=closefd,
opener=opener,
loop=loop,
executor=executor,
)
)
async def _open(
file,
mode="r",
buffering=-1,
encoding=None,
errors=None,
newline=None,
closefd=True,
opener=None,
*,
loop=None,
executor=None,
):
"""Open an asyncio file."""
if loop is None:
loop = asyncio.get_running_loop()
cb = partial(
sync_open,
file,
mode=mode,
buffering=buffering,
encoding=encoding,
errors=errors,
newline=newline,
closefd=closefd,
opener=opener,
)
f = await loop.run_in_executor(executor, cb)
return wrap(f, loop=loop, executor=executor)
@singledispatch
def wrap(file, *, loop=None, executor=None):
msg = f"Unsupported io type: {file}."
raise TypeError(msg)
@wrap.register(TextIOBase)
def _(file, *, loop=None, executor=None):
return AsyncTextIOWrapper(file, loop=loop, executor=executor)
@wrap.register(BufferedWriter)
@wrap.register(BufferedIOBase)
def _(file, *, loop=None, executor=None):
return AsyncBufferedIOBase(file, loop=loop, executor=executor)
@wrap.register(BufferedReader)
@wrap.register(BufferedRandom)
def _(file, *, loop=None, executor=None):
return AsyncBufferedReader(file, loop=loop, executor=executor)
@wrap.register(FileIO)
def _(file, *, loop=None, executor=None):
return AsyncFileIO(file, loop=loop, executor=executor)
stdin = AsyncTextIndirectIOWrapper("sys.stdin", None, None, indirect=lambda: sys.stdin)
stdout = AsyncTextIndirectIOWrapper(
"sys.stdout", None, None, indirect=lambda: sys.stdout
)
stderr = AsyncTextIndirectIOWrapper(
"sys.stderr", None, None, indirect=lambda: sys.stderr
)
stdin_bytes = AsyncIndirectBufferedIOBase(
"sys.stdin.buffer", None, None, indirect=lambda: sys.stdin.buffer
)
stdout_bytes = AsyncIndirectBufferedIOBase(
"sys.stdout.buffer", None, None, indirect=lambda: sys.stdout.buffer
)
stderr_bytes = AsyncIndirectBufferedIOBase(
"sys.stderr.buffer", None, None, indirect=lambda: sys.stderr.buffer
)

View File

@@ -0,0 +1,104 @@
from ..base import AsyncBase, AsyncIndirectBase
from .utils import delegate_to_executor, proxy_method_directly, proxy_property_directly
@delegate_to_executor(
"close",
"flush",
"isatty",
"read",
"read1",
"readinto",
"readline",
"readlines",
"seek",
"seekable",
"tell",
"truncate",
"writable",
"write",
"writelines",
)
@proxy_method_directly("detach", "fileno", "readable")
@proxy_property_directly("closed", "raw", "name", "mode")
class AsyncBufferedIOBase(AsyncBase):
"""The asyncio executor version of io.BufferedWriter and BufferedIOBase."""
@delegate_to_executor("peek")
class AsyncBufferedReader(AsyncBufferedIOBase):
"""The asyncio executor version of io.BufferedReader and Random."""
@delegate_to_executor(
"close",
"flush",
"isatty",
"read",
"readall",
"readinto",
"readline",
"readlines",
"seek",
"seekable",
"tell",
"truncate",
"writable",
"write",
"writelines",
)
@proxy_method_directly("fileno", "readable")
@proxy_property_directly("closed", "name", "mode")
class AsyncFileIO(AsyncBase):
"""The asyncio executor version of io.FileIO."""
@delegate_to_executor(
"close",
"flush",
"isatty",
"read",
"read1",
"readinto",
"readline",
"readlines",
"seek",
"seekable",
"tell",
"truncate",
"writable",
"write",
"writelines",
)
@proxy_method_directly("detach", "fileno", "readable")
@proxy_property_directly("closed", "raw", "name", "mode")
class AsyncIndirectBufferedIOBase(AsyncIndirectBase):
"""The indirect asyncio executor version of io.BufferedWriter and BufferedIOBase."""
@delegate_to_executor("peek")
class AsyncIndirectBufferedReader(AsyncIndirectBufferedIOBase):
"""The indirect asyncio executor version of io.BufferedReader and Random."""
@delegate_to_executor(
"close",
"flush",
"isatty",
"read",
"readall",
"readinto",
"readline",
"readlines",
"seek",
"seekable",
"tell",
"truncate",
"writable",
"write",
"writelines",
)
@proxy_method_directly("fileno", "readable")
@proxy_property_directly("closed", "name", "mode")
class AsyncIndirectFileIO(AsyncIndirectBase):
"""The indirect asyncio executor version of io.FileIO."""

View File

@@ -0,0 +1,64 @@
from ..base import AsyncBase, AsyncIndirectBase
from .utils import delegate_to_executor, proxy_method_directly, proxy_property_directly
@delegate_to_executor(
"close",
"flush",
"isatty",
"read",
"readable",
"readline",
"readlines",
"seek",
"seekable",
"tell",
"truncate",
"write",
"writable",
"writelines",
)
@proxy_method_directly("detach", "fileno", "readable")
@proxy_property_directly(
"buffer",
"closed",
"encoding",
"errors",
"line_buffering",
"newlines",
"name",
"mode",
)
class AsyncTextIOWrapper(AsyncBase):
"""The asyncio executor version of io.TextIOWrapper."""
@delegate_to_executor(
"close",
"flush",
"isatty",
"read",
"readable",
"readline",
"readlines",
"seek",
"seekable",
"tell",
"truncate",
"write",
"writable",
"writelines",
)
@proxy_method_directly("detach", "fileno", "readable")
@proxy_property_directly(
"buffer",
"closed",
"encoding",
"errors",
"line_buffering",
"newlines",
"name",
"mode",
)
class AsyncTextIndirectIOWrapper(AsyncIndirectBase):
"""The indirect asyncio executor version of io.TextIOWrapper."""

View File

@@ -0,0 +1,71 @@
import functools
def delegate_to_executor(*attrs):
def cls_builder(cls):
for attr_name in attrs:
setattr(cls, attr_name, _make_delegate_method(attr_name))
return cls
return cls_builder
def proxy_method_directly(*attrs):
def cls_builder(cls):
for attr_name in attrs:
setattr(cls, attr_name, _make_proxy_method(attr_name))
return cls
return cls_builder
def proxy_property_directly(*attrs):
def cls_builder(cls):
for attr_name in attrs:
setattr(cls, attr_name, _make_proxy_property(attr_name))
return cls
return cls_builder
def cond_delegate_to_executor(*attrs):
def cls_builder(cls):
for attr_name in attrs:
setattr(cls, attr_name, _make_cond_delegate_method(attr_name))
return cls
return cls_builder
def _make_delegate_method(attr_name):
async def method(self, *args, **kwargs):
cb = functools.partial(getattr(self._file, attr_name), *args, **kwargs)
return await self._loop.run_in_executor(self._executor, cb)
return method
def _make_proxy_method(attr_name):
def method(self, *args, **kwargs):
return getattr(self._file, attr_name)(*args, **kwargs)
return method
def _make_proxy_property(attr_name):
def proxy_property(self):
return getattr(self._file, attr_name)
return property(proxy_property)
def _make_cond_delegate_method(attr_name):
"""For spooled temp files, delegate only if rolled to file object"""
async def method(self, *args, **kwargs):
if self._file._rolled:
cb = functools.partial(getattr(self._file, attr_name), *args, **kwargs)
return await self._loop.run_in_executor(self._executor, cb)
return getattr(self._file, attr_name)(*args, **kwargs)
return method

View File

@@ -0,0 +1,96 @@
Metadata-Version: 2.4
Name: anyio
Version: 4.12.1
Summary: High-level concurrency and networking framework on top of asyncio or Trio
Author-email: Alex Grönholm <alex.gronholm@nextday.fi>
License-Expression: MIT
Project-URL: Documentation, https://anyio.readthedocs.io/en/latest/
Project-URL: Changelog, https://anyio.readthedocs.io/en/stable/versionhistory.html
Project-URL: Source code, https://github.com/agronholm/anyio
Project-URL: Issue tracker, https://github.com/agronholm/anyio/issues
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Framework :: AnyIO
Classifier: Typing :: Typed
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.9
Description-Content-Type: text/x-rst
License-File: LICENSE
Requires-Dist: exceptiongroup>=1.0.2; python_version < "3.11"
Requires-Dist: idna>=2.8
Requires-Dist: typing_extensions>=4.5; python_version < "3.13"
Provides-Extra: trio
Requires-Dist: trio>=0.32.0; python_version >= "3.10" and extra == "trio"
Requires-Dist: trio>=0.31.0; python_version < "3.10" and extra == "trio"
Dynamic: license-file
.. image:: https://github.com/agronholm/anyio/actions/workflows/test.yml/badge.svg
:target: https://github.com/agronholm/anyio/actions/workflows/test.yml
:alt: Build Status
.. image:: https://coveralls.io/repos/github/agronholm/anyio/badge.svg?branch=master
:target: https://coveralls.io/github/agronholm/anyio?branch=master
:alt: Code Coverage
.. image:: https://readthedocs.org/projects/anyio/badge/?version=latest
:target: https://anyio.readthedocs.io/en/latest/?badge=latest
:alt: Documentation
.. image:: https://badges.gitter.im/gitterHQ/gitter.svg
:target: https://gitter.im/python-trio/AnyIO
:alt: Gitter chat
AnyIO is an asynchronous networking and concurrency library that works on top of either asyncio_ or
Trio_. It implements Trio-like `structured concurrency`_ (SC) on top of asyncio and works in harmony
with the native SC of Trio itself.
Applications and libraries written against AnyIO's API will run unmodified on either asyncio_ or
Trio_. AnyIO can also be adopted into a library or application incrementally bit by bit, no full
refactoring necessary. It will blend in with the native libraries of your chosen backend.
To find out why you might want to use AnyIO's APIs instead of asyncio's, you can read about it
`here <https://anyio.readthedocs.io/en/stable/why.html>`_.
Documentation
-------------
View full documentation at: https://anyio.readthedocs.io/
Features
--------
AnyIO offers the following functionality:
* Task groups (nurseries_ in trio terminology)
* High-level networking (TCP, UDP and UNIX sockets)
* `Happy eyeballs`_ algorithm for TCP connections (more robust than that of asyncio on Python
3.8)
* async/await style UDP sockets (unlike asyncio where you still have to use Transports and
Protocols)
* A versatile API for byte streams and object streams
* Inter-task synchronization and communication (locks, conditions, events, semaphores, object
streams)
* Worker threads
* Subprocesses
* Subinterpreter support for code parallelization (on Python 3.13 and later)
* Asynchronous file I/O (using worker threads)
* Signal handling
* Asynchronous version of the functools_ module
AnyIO also comes with its own pytest_ plugin which also supports asynchronous fixtures.
It even works with the popular Hypothesis_ library.
.. _asyncio: https://docs.python.org/3/library/asyncio.html
.. _Trio: https://github.com/python-trio/trio
.. _structured concurrency: https://en.wikipedia.org/wiki/Structured_concurrency
.. _nurseries: https://trio.readthedocs.io/en/stable/reference-core.html#nurseries-and-spawning
.. _Happy eyeballs: https://en.wikipedia.org/wiki/Happy_Eyeballs
.. _pytest: https://docs.pytest.org/en/latest/
.. _functools: https://docs.python.org/3/library/functools.html
.. _Hypothesis: https://hypothesis.works/

View File

@@ -0,0 +1,92 @@
anyio-4.12.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
anyio-4.12.1.dist-info/METADATA,sha256=DfiDab9Tmmcfy802lOLTMEHJQShkOSbopCwqCYbLuJk,4277
anyio-4.12.1.dist-info/RECORD,,
anyio-4.12.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
anyio-4.12.1.dist-info/entry_points.txt,sha256=_d6Yu6uiaZmNe0CydowirE9Cmg7zUL2g08tQpoS3Qvc,39
anyio-4.12.1.dist-info/licenses/LICENSE,sha256=U2GsncWPLvX9LpsJxoKXwX8ElQkJu8gCO9uC6s8iwrA,1081
anyio-4.12.1.dist-info/top_level.txt,sha256=QglSMiWX8_5dpoVAEIHdEYzvqFMdSYWmCj6tYw2ITkQ,6
anyio/__init__.py,sha256=7iDVqMUprUuKNY91FuoKqayAhR-OY136YDPI6P78HHk,6170
anyio/__pycache__/__init__.cpython-313.pyc,,
anyio/__pycache__/from_thread.cpython-313.pyc,,
anyio/__pycache__/functools.cpython-313.pyc,,
anyio/__pycache__/lowlevel.cpython-313.pyc,,
anyio/__pycache__/pytest_plugin.cpython-313.pyc,,
anyio/__pycache__/to_interpreter.cpython-313.pyc,,
anyio/__pycache__/to_process.cpython-313.pyc,,
anyio/__pycache__/to_thread.cpython-313.pyc,,
anyio/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
anyio/_backends/__pycache__/__init__.cpython-313.pyc,,
anyio/_backends/__pycache__/_asyncio.cpython-313.pyc,,
anyio/_backends/__pycache__/_trio.cpython-313.pyc,,
anyio/_backends/_asyncio.py,sha256=xG6qv60mgGnL0mK82dxjH2b8hlkMlJ-x2BqIq3qv70Y,98863
anyio/_backends/_trio.py,sha256=30Rctb7lm8g63ZHljVPVnj5aH-uK6oQvphjwUBoAzuI,41456
anyio/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
anyio/_core/__pycache__/__init__.cpython-313.pyc,,
anyio/_core/__pycache__/_asyncio_selector_thread.cpython-313.pyc,,
anyio/_core/__pycache__/_contextmanagers.cpython-313.pyc,,
anyio/_core/__pycache__/_eventloop.cpython-313.pyc,,
anyio/_core/__pycache__/_exceptions.cpython-313.pyc,,
anyio/_core/__pycache__/_fileio.cpython-313.pyc,,
anyio/_core/__pycache__/_resources.cpython-313.pyc,,
anyio/_core/__pycache__/_signals.cpython-313.pyc,,
anyio/_core/__pycache__/_sockets.cpython-313.pyc,,
anyio/_core/__pycache__/_streams.cpython-313.pyc,,
anyio/_core/__pycache__/_subprocesses.cpython-313.pyc,,
anyio/_core/__pycache__/_synchronization.cpython-313.pyc,,
anyio/_core/__pycache__/_tasks.cpython-313.pyc,,
anyio/_core/__pycache__/_tempfile.cpython-313.pyc,,
anyio/_core/__pycache__/_testing.cpython-313.pyc,,
anyio/_core/__pycache__/_typedattr.cpython-313.pyc,,
anyio/_core/_asyncio_selector_thread.py,sha256=2PdxFM3cs02Kp6BSppbvmRT7q7asreTW5FgBxEsflBo,5626
anyio/_core/_contextmanagers.py,sha256=YInBCabiEeS-UaP_Jdxa1CaFC71ETPW8HZTHIM8Rsc8,7215
anyio/_core/_eventloop.py,sha256=c2EdcBX-xnKwxPcC4Pjn3_qG9I-x4IWFO2R9RqCGjM4,6448
anyio/_core/_exceptions.py,sha256=Y3aq-Wxd7Q2HqwSg7nZPvRsHEuGazv_qeet6gqEBdPk,4407
anyio/_core/_fileio.py,sha256=uc7t10Vb-If7GbdWM_zFf-ajUe6uek63fSt7IBLlZW0,25731
anyio/_core/_resources.py,sha256=NbmU5O5UX3xEyACnkmYX28Fmwdl-f-ny0tHym26e0w0,435
anyio/_core/_signals.py,sha256=mjTBB2hTKNPRlU0IhnijeQedpWOGERDiMjSlJQsFrug,1016
anyio/_core/_sockets.py,sha256=RBXHcUqZt5gg_-OOfgHVv8uq2FSKk1uVUzTdpjBoI1o,34977
anyio/_core/_streams.py,sha256=FczFwIgDpnkK0bODWJXMpsUJYdvAD04kaUaGzJU8DK0,1806
anyio/_core/_subprocesses.py,sha256=EXm5igL7dj55iYkPlbYVAqtbqxJxjU-6OndSTIx9SRg,8047
anyio/_core/_synchronization.py,sha256=MgVVqFzvt580tHC31LiOcq1G6aryut--xRG4Ff8KwxQ,20869
anyio/_core/_tasks.py,sha256=pVB7K6AAulzUM8YgXAeqNZG44nSyZ1bYJjH8GznC00I,5435
anyio/_core/_tempfile.py,sha256=lHb7CW4FyIlpkf5ADAf4VmLHCKwEHF9nxqNyBCFFUiA,19697
anyio/_core/_testing.py,sha256=u7MPqGXwpTxqI7hclSdNA30z2GH1Nw258uwKvy_RfBg,2340
anyio/_core/_typedattr.py,sha256=P4ozZikn3-DbpoYcvyghS_FOYAgbmUxeoU8-L_07pZM,2508
anyio/abc/__init__.py,sha256=6mWhcl_pGXhrgZVHP_TCfMvIXIOp9mroEFM90fYCU_U,2869
anyio/abc/__pycache__/__init__.cpython-313.pyc,,
anyio/abc/__pycache__/_eventloop.cpython-313.pyc,,
anyio/abc/__pycache__/_resources.cpython-313.pyc,,
anyio/abc/__pycache__/_sockets.cpython-313.pyc,,
anyio/abc/__pycache__/_streams.cpython-313.pyc,,
anyio/abc/__pycache__/_subprocesses.cpython-313.pyc,,
anyio/abc/__pycache__/_tasks.cpython-313.pyc,,
anyio/abc/__pycache__/_testing.cpython-313.pyc,,
anyio/abc/_eventloop.py,sha256=GlzgB3UJGgG6Kr7olpjOZ-o00PghecXuofVDQ_5611Q,10749
anyio/abc/_resources.py,sha256=DrYvkNN1hH6Uvv5_5uKySvDsnknGVDe8FCKfko0VtN8,783
anyio/abc/_sockets.py,sha256=ECTY0jLEF18gryANHR3vFzXzGdZ-xPwELq1QdgOb0Jo,13258
anyio/abc/_streams.py,sha256=005GKSCXGprxnhucILboSqc2JFovECZk9m3p-qqxXVc,7640
anyio/abc/_subprocesses.py,sha256=cumAPJTktOQtw63IqG0lDpyZqu_l1EElvQHMiwJgL08,2067
anyio/abc/_tasks.py,sha256=KC7wrciE48AINOI-AhPutnFhe1ewfP7QnamFlDzqesQ,3721
anyio/abc/_testing.py,sha256=tBJUzkSfOXJw23fe8qSJ03kJlShOYjjaEyFB6k6MYT8,1821
anyio/from_thread.py,sha256=L-0w1HxJ6BSb-KuVi57k5Tkc3yzQrx3QK5tAxMPcY-0,19141
anyio/functools.py,sha256=HWj7GBEmc0Z-mZg3uok7Z7ZJn0rEC_0Pzbt0nYUDaTQ,10973
anyio/lowlevel.py,sha256=AyKLVK3LaWSoK39LkCKxE4_GDMLKZBNqTrLUgk63y80,5158
anyio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
anyio/pytest_plugin.py,sha256=3jAFQn0jv_pyoWE2GBBlHaj9sqXj4e8vob0_hgrsXE8,10244
anyio/streams/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
anyio/streams/__pycache__/__init__.cpython-313.pyc,,
anyio/streams/__pycache__/buffered.cpython-313.pyc,,
anyio/streams/__pycache__/file.cpython-313.pyc,,
anyio/streams/__pycache__/memory.cpython-313.pyc,,
anyio/streams/__pycache__/stapled.cpython-313.pyc,,
anyio/streams/__pycache__/text.cpython-313.pyc,,
anyio/streams/__pycache__/tls.cpython-313.pyc,,
anyio/streams/buffered.py,sha256=2R3PeJhe4EXrdYqz44Y6-Eg9R6DrmlsYrP36Ir43-po,6263
anyio/streams/file.py,sha256=4WZ7XGz5WNu39FQHvqbe__TQ0HDP9OOhgO1mk9iVpVU,4470
anyio/streams/memory.py,sha256=F0zwzvFJKAhX_LRZGoKzzqDC2oMM-f-yyTBrEYEGOaU,10740
anyio/streams/stapled.py,sha256=T8Xqwf8K6EgURPxbt1N4i7A8BAk-gScv-GRhjLXIf_o,4390
anyio/streams/text.py,sha256=BcVAGJw1VRvtIqnv-o0Rb0pwH7p8vwlvl21xHq522ag,5765
anyio/streams/tls.py,sha256=Jpxy0Mfbcp1BxHCwE-YjSSFaLnIBbnnwur-excYThs4,15368
anyio/to_interpreter.py,sha256=_mLngrMy97TMR6VbW4Y6YzDUk9ZuPcQMPlkuyRh3C9k,7100
anyio/to_process.py,sha256=J7gAA_YOuoHqnpDAf5fm1Qu6kOmTzdFbiDNvnV755vk,9798
anyio/to_thread.py,sha256=menEgXYmUV7Fjg_9WqCV95P9MAtQS8BzPGGcWB_QnfQ,2687

View File

@@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: setuptools (80.9.0)
Root-Is-Purelib: true
Tag: py3-none-any

View File

@@ -0,0 +1,2 @@
[pytest11]
anyio = anyio.pytest_plugin

View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2018 Alex Grönholm
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Some files were not shown because too many files have changed in this diff Show More