19 Commits

Author SHA1 Message Date
fb473dcf1a 删除自动降落 2026-01-08 16:09:48 +08:00
10c5bb5a8a 增加环绕侦察场景适配 2026-01-08 15:44:38 +08:00
3eba1f962b 解决冲突:以本地版本为准更新 system_prompt.txt 和 py_tree_generator.py 2026-01-07 16:38:40 +08:00
6f990e645d 优化交互式测试验证脚本,针对场景4修改提示词以及代码 2026-01-02 16:28:58 +08:00
c08cdfb339 修改简单模式验证 2025-12-03 17:13:59 +08:00
43a0636913 修改简单模式验证 2025-12-03 17:13:47 +08:00
c4f851d387 chore: 添加虚拟环境到仓库
- 添加 backend_service/venv 虚拟环境
- 包含所有Python依赖包
- 注意:虚拟环境约393MB,包含12655个文件
2025-12-03 10:19:25 +08:00
a6c2027caa feat: 添加一键启动脚本并更新项目配置
- 添加 start_all.sh 一键启动脚本,支持启动llama-server和FastAPI服务
- 修改启动脚本使用venv虚拟环境替代conda环境
- 更新README.md,添加一键启动脚本使用说明
- 更新py_tree_generator.py,添加final_prompt返回字段
- 禁用Qwen3模型的思考功能
- 添加RAG检索结果的终端打印
- 移除ROS2相关代码(ros2_client.py已删除)
2025-12-02 21:42:26 +08:00
ab6e09423b 去除ROS2相关内容,新增一键启动脚本 2025-12-02 21:41:18 +08:00
d32520d83f 增加输出数量约束 2025-09-21 22:33:54 +08:00
afd170c451 优化提示词 2025-09-21 22:12:21 +08:00
fd89745950 新增说明 2025-09-21 01:16:33 +08:00
8e333ac03f 优化提示词 2025-09-15 22:23:49 +08:00
7b9d05b306 优化提示词 2025-09-15 22:09:12 +08:00
781b490cdc 增加口令支持 2025-09-15 21:52:13 +08:00
ce963ed7d6 修改README.md 2025-09-14 21:15:42 +08:00
9703f7cc10 简单模式增加查询 2025-09-14 21:09:54 +08:00
7bf8210b80 优化简单模式支持 2025-09-14 21:03:30 +08:00
3adf3985cb 增加简单/复杂指令判断环节,对简单、复杂指令提供支持 2025-09-14 20:57:50 +08:00
11376 changed files with 2748915 additions and 69510 deletions

178
README.md
View File

@@ -13,8 +13,13 @@
│ │ ├── __init__.py
│ │ ├── main.py # 应用主入口提供Web API
│ │ ├── py_tree_generator.py # RAG与LLM集成生成py_tree
│ │ ├── prompts/ # LLM 提示词
│ │ │ ├── system_prompt.txt # 复杂模式提示词(行为树与安全监控)
│ │ │ ├── simple_mode_prompt.txt # 简单模式提示词单一原子动作JSON
│ │ │ └── classifier_prompt.txt # 指令简单/复杂分类提示词
│ │ ├── ...
│ ├── generated_visualizations/ # 存放最新生成的py_tree可视化图像
│ ├── generated_reasoning_content/ # 存放最新推理链Markdown<plan_id>.md
│ └── requirements.txt # 后端服务的Python依赖
├── tools/
@@ -22,7 +27,8 @@
│ ├── knowledge_base/ # 【处理后】存放build_knowledge_base.py生成的.ndjson文件
│ ├── vector_store/ # 【数据库】存放最终的ChromaDB向量数据库
│ ├── build_knowledge_base.py # 【步骤1】用于将原始数据转换为自然语言知识
── ingest.py # 【步骤2】用于将自然语言知识摄入向量数据库
── ingest.py # 【步骤2】用于将自然语言知识摄入向量数据库
│ └── test_llama_server.py # 直接调用本地8081端口llama-server支持 --system / --system-file
├── / # ROS2接口定义 (保持不变)
└── docs/
@@ -67,6 +73,78 @@
---
## 指令分类与分流
后端在生成任务前会先对用户指令进行“简单/复杂”分类,并分流到不同提示词与模型:
- 分类提示词:`backend_service/src/prompts/classifier_prompt.txt`
- 简单模式提示词:`backend_service/src/prompts/simple_mode_prompt.txt`
- 复杂模式提示词:`backend_service/src/prompts/system_prompt.txt`
分类仅输出如下JSON之一`{"mode":"simple"}` 或 `{"mode":"complex"}`。两种模式都会执行检索增强RAG将参考知识拼接到用户指令后再进行推理。
当为简单模式时LLM仅输出
`{"mode":"simple","action":{"name":"<action>","params":{...}}}`。
后端不会再自动封装为复杂行为树将直接返回简单JSON并附加 `plan_id` 与 `visualization_url`(单动作可视化)。
### 环境变量(可选)
支持为“分类/简单/复杂”三类调用分别配置模型与Base URL未设置时回退到默认本地配置
- `CLASSIFIER_MODEL`, `CLASSIFIER_BASE_URL`
- `SIMPLE_MODEL`, `SIMPLE_BASE_URL`
- `COMPLEX_MODEL`, `COMPLEX_BASE_URL`
通用API Key`OPENAI_API_KEY`
推理链捕获相关:
- `ENABLE_REASONING_CAPTURE`:是否允许模型返回含有 <think> 的原文以便捕获推理链;默认 true。
- `REASONING_PREVIEW_LINES`:在后端日志中打印推理链预览的行数;默认 20。
示例:
```bash
export CLASSIFIER_MODEL="qwen2.5-1.8b-instruct"
export SIMPLE_MODEL="qwen2.5-1.8b-instruct"
export COMPLEX_MODEL="qwen2.5-7b-instruct"
export CLASSIFIER_BASE_URL="http://$ORIN_IP:8081/v1"
export SIMPLE_BASE_URL="http://$ORIN_IP:8081/v1"
export COMPLEX_BASE_URL="http://$ORIN_IP:8081/v1"
export OPENAI_API_KEY="sk-no-key-required"
# 推理链捕获(可选)
export ENABLE_REASONING_CAPTURE=true # 默认已为true如需关闭设置为 false
export REASONING_PREVIEW_LINES=30 # 调整日志预览行数
```
### 测试简单模式
启动服务后,运行内置测试脚本:
```bash
cd tools
python test_api.py
```
示例输入:“简单模式,起飞” 或 “起飞到10米”。返回结果为简单JSON无 `root`):包含 `mode`、`action`、`plan_id`、`visualization_url`。
### 直接调用 llama-server绕过后端
当仅需测试本地 8081 端口的推理服务OpenAI 兼容接口)时,可使用内置脚本:
```bash
python tools/test_llama_server.py \
--system-file backend_service/src/prompts/system_prompt.txt \
--user "起飞到10米然后降落" \
--base-url "http://127.0.0.1:8081/v1" \
--verbose
```
说明:
- 支持 `--system` 或 `--system-file` 自定义提示词文件;`--system-file` 优先。
- 默认解析 OpenAI 风格返回,若包含 `<think>` 推理内容会显示在输出中(具体取决于模型和服务配置)。
---
## 工作流程
整个系统的工作流程分为两个主要阶段:
@@ -150,14 +228,96 @@ python ingest.py
完成前两个阶段后,即可启动并测试后端服务。
#### 1. 启动后端服务
#### 1. 启动所有服务(推荐方式:一键启动脚本)
启动服务的关键在于**按顺序激活环境**先激活ROS 2工作空间再激活Conda环境。
我们提供了一个一键启动脚本 `start_all.sh`,可以自动启动所有必需的服务:
```bash
# 1. 切换到项目根目录
cd /path/to/your/drone
# 2. 使用一键启动脚本(推荐)
./start_all.sh start
# 或者直接运行start是默认命令
./start_all.sh
```
**脚本功能:**
- 自动启动推理模型服务llama-server端口8081
- 自动启动Embedding模型服务llama-server端口8090
- 自动启动FastAPI后端服务端口8000
- 自动检查端口占用、模型文件、环境配置等
- 自动等待服务就绪
- 统一管理日志文件(保存在 `logs/` 目录)
**环境变量配置(可选):**
在运行脚本前,可以通过环境变量自定义配置:
```bash
# 设置llama-server路径如果不在默认位置
export LLAMA_SERVER_DIR="/path/to/llama.cpp/build/bin"
# 设置模型路径(如果不在默认位置)
export INFERENCE_MODEL="~/models/gguf/Qwen/Qwen3-8B-GGUF/Qwen3-8B-Q4_K_M.gguf"
export EMBEDDING_MODEL="~/models/gguf/Qwen/Qwen3-embedding-4B/Qwen3-Embedding-4B-Q4_K_M.gguf"
# 设置Conda环境名称如果使用不同的环境名
export CONDA_ENV="backend"
# 然后运行脚本
./start_all.sh
```
**脚本命令:**
```bash
./start_all.sh start # 启动所有服务(默认)
./start_all.sh stop # 停止所有服务
./start_all.sh restart # 重启所有服务
./start_all.sh status # 查看服务状态
```
**日志查看:**
所有服务的日志都保存在 `logs/` 目录下:
```bash
# 查看所有日志
tail -f logs/*.log
# 查看特定服务日志
tail -f logs/inference_model.log # 推理模型
tail -f logs/embedding_model.log # Embedding模型
tail -f logs/fastapi.log # FastAPI服务
```
#### 2. 手动启动服务(备选方式)
如果您需要手动控制每个服务的启动,可以按照以下步骤操作:
**启动推理模型服务:**
```bash
cd /llama.cpp/build/bin
./llama-server -m ~/models/gguf/Qwen/Qwen3-8B-GGUF/Qwen3-8B-Q4_K_M.gguf --port 8081 --gpu-layers 36 --host 0.0.0.0 -c 8192
```
**启动Embedding模型服务**
在另一个终端中:
```bash
cd /llama.cpp/build/bin
./llama-server -m ~/models/gguf/Qwen/Qwen3-embedding-4B/Qwen3-Embedding-4B-Q4_K_M.gguf --gpu-layers 36 --port 8090 --embeddings --pooling last --host 0.0.0.0
```
**启动FastAPI后端服务**
在第三个终端中:
```bash
# 1. 切换到项目根目录
cd /path/to/your/drone
# 2. 激活ROS 2编译环境
# 作用:将我们编译好的`drone_interfaces`包的路径告知系统否则Python会报`ModuleNotFoundError`。
# 注意:此命令必须在每次打开新终端时执行一次。
@@ -170,6 +330,7 @@ conda activate backend
cd backend_service/
uvicorn src.main:app --host 0.0.0.0 --port 8000
```
当您看到日志中出现 `Uvicorn running on http://0.0.0.0:8000` 时,表示服务已成功启动。
#### 2. 运行API接口测试
@@ -235,7 +396,7 @@ python test_api.py
"user_prompt": "无人机起飞到10米然后前往机库最后降落。"
}
```
- **Success Response**:
- **Success Response(复杂模式)**:
```json
{
"root": { ... },
@@ -243,6 +404,15 @@ python test_api.py
"visualization_url": "/static/py_tree.png"
}
```
- **Success Response简单模式**:
```json
{
"mode": "simple",
"action": { "name": "takeoff", "params": { "altitude": 10.0 } },
"plan_id": "some-unique-id",
"visualization_url": "/static/py_tree.png"
}
```
##### **B. 查看任务可视化**

View File

@@ -0,0 +1,85 @@
好的我现在需要处理用户的任务指令生成一个符合要求的行为树JSON。首先我得仔细阅读用户的指令和参考知识确保理解所有要求。
用户的目标是搜索并锁定危险性最高的气球(红色>蓝色>绿色然后跟踪30秒后进行打击。参考知识中提到了三个地点但用户可能希望搜索的区域是这些地点附近或者需要根据这些坐标来规划路径。
首先,我需要确定无人机的起降点。参考知识中的地点坐标可能作为搜索区域的中心。比如,用户提到的“学生宿舍”坐标是(5,3,2)但可能需要选择一个合适的中心点进行搜索。不过用户没有明确指定搜索区域可能需要使用search_pattern来覆盖这些区域或者直接在某个中心点周围搜索。
接下来,任务流程应该是:起飞→飞往搜索区域→搜索目标→检测到目标后跟踪→打击。同时必须包含安全监控。
根据用户提供的参考知识,可能需要将搜索区域设置为某个中心点。比如,用户提到的“跷跷板”在(x:15, y:-8.5, z:1.2)但z坐标需要调整到至少1米所以可能设置为z=2。或者选择其他地点作为搜索中心。但用户没有明确说明可能需要假设搜索区域是这些地点的附近或者使用其中一个作为中心。
另外用户提到要搜索并锁定危险性最高的气球所以需要使用search_pattern或者object_detect。但因为是未知区域可能更适合使用search_pattern或者先飞到某个区域再进行检测。
根据任务范式可能需要先飞到某个坐标点然后进行搜索。例如使用fly_to_waypoint飞到某个中心点然后执行search_pattern。或者直接使用search_pattern覆盖多个区域。
不过参考知识中的三个地点可能作为搜索区域的中心,可能需要将搜索区域设置为这些点的附近。但用户没有明确说明,可能需要选择一个中心点,比如“学生宿舍”的坐标,或者综合考虑。
另外用户提到要锁定危险性最高的气球所以需要检测到红色气球。因此在search_pattern中target_class应为balloon并且description为“红色气球危险性高于蓝色气球高于绿色气球”或者在object_detect中设置description参数为“红>蓝>绿”。
但根据搜索模式search_pattern的参数需要target_class和description。所以可能需要设置target_class为balloondescription为“红>蓝>绿”。
接下来,流程应该是:
1. 起飞到一定高度。
2. 飞往搜索区域的中心点比如学生宿舍的坐标调整z到2
3. 执行search_pattern搜索红色气球。
4. 一旦检测到目标,进行跟踪。
5. 跟踪30秒后打击。
同时安全监控必须包含电池和GPS检查以及紧急处理。
现在需要构建行为树结构。根节点是Parallel包含主任务和安全监控。主任务是Sequence包含起飞、飞往搜索点、搜索、跟踪、打击、降落。
在搜索部分可能需要使用search_pattern或者先飞到某个点再进行object_detect。但根据用户指令可能更适合使用search_pattern因为需要在未知区域搜索。
所以,主任务步骤:
- preflight_checks
- takeoff
- fly_to_waypoint到某个中心点比如学生宿舍的坐标调整z为2
- search_pattern参数为balloondescription为“红>蓝>绿”pattern_type可能是spiral或grid假设用spiral中心点为学生宿舍的坐标半径适当比如50米。
- 然后当检测到目标后进行track_object跟踪30秒。
- 然后strike_target打击。
- 最后降落。
但需要确保在search_pattern之后检测到目标然后进入跟踪。或者可能需要使用Selector来选择直接检测或搜索。
但根据用户指令可能需要先飞到某个点再执行search_pattern。或者直接使用search_pattern覆盖多个区域。
参考知识中的三个地点可能作为搜索区域的中心,但用户没有明确说明,可能需要选择其中一个作为搜索中心。比如学生宿舍的坐标(x:5, y:3, z:2)调整z为2。
所以fly_to_waypoint的参数是x=5, y=3, z=2, acceptance_radius=2.0。
然后执行search_pattern参数为balloondescription为“红>蓝>绿”pattern_type为spiral中心点为学生宿舍的坐标半径比如50米。
之后当检测到目标后进行track_object参数为balloondescription为“红>蓝>绿”track_time=30.0min_confidence=0.7safe_distance=15.0。
然后strike_target参数为balloondescription为“红>蓝>绿”count=1。
最后降落。
同时安全监控需要包含电池和GPS检查以及紧急处理。
现在需要构建JSON结构确保所有参数符合约束并且流程正确。
可能的搜索中心是学生宿舍的坐标调整z为2。然后search_pattern的参数为balloondescription为“红>蓝>绿”pattern_type为spiralradius为50米center_x=5, center_y=3, center_z=2。
然后当检测到目标后进行track_object跟踪30秒然后打击。
在行为树中主任务Sequence的步骤包括
- preflight_checks
- takeoff (altitude=2.0)
- fly_to_waypoint到学生宿舍的坐标调整z为2
- search_pattern参数为balloondescription为“红>蓝>绿”pattern_type为spiralradius=50center_x=5, center_y=3, center_z=2
- 然后当检测到目标后进行track_object
- strike_target
- land
同时安全监控的Selector包含电池和GPS条件以及紧急处理。
现在需要检查所有参数是否符合约束例如z=2符合≥1。
其他参数如radius=50符合[5,1000]。
所以生成的JSON结构应该符合这些要求并且流程正确。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 169 KiB

View File

@@ -15,8 +15,8 @@ chromadb>=0.4.0
# Visualization
graphviz>=0.20.0
# ROS 2 Python Client
rclpy>=0.0.1
# ROS 2 Python Client - 已注释项目已与ROS2解耦
# rclpy>=0.0.1
# Document Processing
unstructured[all]>=0.11.0
@@ -30,10 +30,10 @@ rich>=13.7.0
# Type Hints Support
typing-extensions>=4.8.0
# ROS 2 Build Dependencies
empy==3.3.4
catkin-pkg>=0.4.0
lark>=1.1.0
colcon-common-extensions>=0.3.0
vcstool>=0.2.0
rosdep>=0.22.0
# ROS 2 Build Dependencies - 已注释项目已与ROS2解耦
# empy==3.3.4
# catkin-pkg>=0.4.0
# lark>=1.1.0
# colcon-common-extensions>=0.3.0
# vcstool>=0.2.0
# rosdep>=0.22.0

Binary file not shown.

View File

@@ -3,13 +3,13 @@ import os
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles
import logging
import threading
import rclpy
# import threading # ROS2相关已注释
# import rclpy # ROS2相关已注释
from .models import GeneratePlanRequest, ExecuteMissionRequest
from .websocket_manager import websocket_manager
from .py_tree_generator import py_tree_generator
from .ros2_client import MissionActionClient
# from .ros2_client import MissionActionClient # ROS2相关已注释
# --- Application Setup ---
app = FastAPI(
@@ -23,14 +23,15 @@ static_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'gene
app.mount("/static", StaticFiles(directory=static_dir), name="static")
# --- ROS2 Node and Client Initialization ---
rclpy.init()
ros2_client = MissionActionClient()
# ROS2相关代码已注释项目已与ROS2解耦
# rclpy.init()
# ros2_client = MissionActionClient()
def run_ros2_node():
"""Spins the ROS2 node in a dedicated thread."""
logging.info("Starting to spin ROS2 node...")
rclpy.spin(ros2_client)
logging.info("ROS2 node has stopped spinning.")
# def run_ros2_node():
# """Spins the ROS2 node in a dedicated thread."""
# logging.info("Starting to spin ROS2 node...")
# rclpy.spin(ros2_client)
# logging.info("ROS2 node has stopped spinning.")
# --- API Endpoints ---
@@ -49,9 +50,12 @@ async def generate_plan_endpoint(request: GeneratePlanRequest):
async def execute_mission_endpoint(request: ExecuteMissionRequest):
"""
Receives a `py_tree.json` and sends it to the drone for execution.
ROS2相关功能已注释项目已与ROS2解耦。
"""
ros2_client.send_goal(request.py_tree)
return {"status": "execution_started"}
# ROS2相关代码已注释
# ros2_client.send_goal(request.py_tree)
logging.warning("execute_mission endpoint called but ROS2 is disabled. Mission execution is not available.")
return {"status": "execution_disabled", "message": "ROS2 integration is disabled. Mission execution is not available."}
@app.websocket("/ws/status")
async def websocket_endpoint(websocket: WebSocket):
@@ -73,21 +77,23 @@ async def websocket_endpoint(websocket: WebSocket):
async def startup_event():
"""
On startup, get the current asyncio event loop and pass it to the websocket manager.
Also, start the ROS2 node in a background thread.
ROS2相关功能已注释项目已与ROS2解耦。
"""
# Configure WebSocket Manager
loop = asyncio.get_running_loop()
websocket_manager.set_loop(loop)
logging.info("WebSocket event loop configured.")
# ROS2相关代码已注释
# Start ROS2 node in a background thread
ros2_thread = threading.Thread(target=run_ros2_node, daemon=True)
ros2_thread.start()
logging.info("ROS2 node thread started.")
# ros2_thread = threading.Thread(target=run_ros2_node, daemon=True)
# ros2_thread.start()
# logging.info("ROS2 node thread started.")
@app.on_event("shutdown")
async def shutdown_event():
logging.info("Backend service shutting down.")
ros2_client.destroy_node()
rclpy.shutdown()
logging.info("ROS2 node shut down successfully.")
# ROS2相关代码已注释
# ros2_client.destroy_node()
# rclpy.shutdown()
# logging.info("ROS2 node shut down successfully.")

View File

@@ -0,0 +1,24 @@
你是一个严格的任务分类器。只输出一个JSON对象不要输出解释或多余文本。
根据用户指令与下述可用节点定义,判断其为“简单”或“复杂”。
- 简单:单一原子动作即可完成(例如"起飞""飞机自检""移动到某地(已给定坐标)""对着某点环绕XY圈如'对着学生宿舍环绕三十两圈'"等),且无需行为树。
- 复杂:需要多步流程、搜索/检测/跟踪/评估、战损确认、或需要模板化任务结构。
输出格式(严格遵守):
{"mode":"simple"} 或 {"mode":"complex"}
—— 可用节点定义——
```json
{
"actions": [
{"name": "takeoff"}, {"name": "land"}, {"name": "fly_to_waypoint"}, {"name": "move_direction"}, {"name": "orbit_around_point"}, {"name": "orbit_around_target"}, {"name": "loiter"},
{"name": "object_detect"}, {"name": "strike_target"}, {"name": "battle_damage_assessment"},
{"name": "search_pattern"}, {"name": "track_object"}, {"name": "deliver_payload"},
{"name": "preflight_checks"}, {"name": "take_picture"}
],
"conditions": [
{"name": "at_waypoint"}, {"name": "object_detected"},
{"name": "target_destroyed"}, {"name": "time_elapsed"}
]
}
```

View File

@@ -0,0 +1,63 @@
你是一个无人机简单指令执行规划器。你的任务当用户给出“简单指令”单一原子动作即可完成输出一个严格的JSON对象。
输出要求(必须遵守):
- 只输出一个JSON对象不要任何解释或多余文本。
- JSON结构
{"root":{"type":"action","name":"<action_name>","params":{...}}}
- root节点必须是action类型节点不能是控制流节点。
示例:
- “起飞到10米” → {"root":{"type":"action","name":"takeoff","params":{"altitude":10.0}}}
- “移动到(120,80,20)” → {"root":{"type":"action","name":"fly_to_waypoint","params":{"x":120.0,"y":80.0,"z":20.0,"acceptance_radius":2.0}}}
- “飞机自检” → {"root":{"type":"action","name":"preflight_checks","params":{"check_level":"comprehensive"}}}
—— 可用节点定义——
```json
{
"actions": [
{"name": "takeoff", "description": "无人机从当前位置垂直起飞到指定的海拔高度。", "params": {"altitude": "float, 目标海拔高度(米),范围[1, 100]默认为2"}},
{"name": "land", "description": "降落无人机。可选择当前位置或返航点降落。", "params": {"mode": "string, 可选值: 'current'(当前位置), 'home'(返航点)"}},
{"name": "fly_to_waypoint", "description": "导航至一个指定坐标点。使用相对坐标系x,y,z单位为米。", "params": {"x": "float", "y": "float", "z": "float", "acceptance_radius": "float, 可选默认2.0"}},
{"name": "move_direction", "description": "按指定方向直线移动。方向可为绝对方位或相对机体朝向。", "params": {"direction": "string: north|south|east|west|forward|backward|left|right", "distance": "float[1,10000], 可选, 不指定则持续移动", "speed": "float, 可选"}},
{"name": "approach_target", "description": "快速趋近目标至固定距离。", "params": {"target_class": "string, 要趋近的目标类别", "description": "string, 可选", "stop_distance": "float, 期望的最终停止距离", "speed": "float, 可选"}},
{"name": "rotate", "description": "旋转固定角度。", "params": {"angle": "float, 旋转角度(正数逆时针, 负数顺时针)", "angular_velocity": "rad/s, 旋转角速度"}},
{"name": "rotate_search", "description": "原地旋转搜索目标。", "params": {"target_class": "string, 要搜寻的目标类别", "description": "string, 可选", "step_angle": "float, 可选, 每一步旋转的角度", "total_rotation": "float, 可选, 总共旋转搜索的角度"}},
{"name": "manual_confirmation", "description": "前端弹窗是否继续执行后续任务。", "params": {}},
{"name": "orbit_around_point", "description": "以给定中心点为中心,等速圆周飞行指定圈数。", "params": {"center_x": "float", "center_y": "float", "center_z": "float", "radius": "float[5,1000]", "laps": "int[1,20]", "clockwise": "boolean, 可选, 默认true", "speed_mps": "float[0.5,15], 可选", "gimbal_lock": "boolean, 可选, 默认true"}},
{"name": "orbit_around_target", "description": "以目标为中心,等速圆周飞行指定圈数(需已有目标)。", "params": {"target_class": "string, 取值同object_detect列表", "description": "string, 可选", "radius": "float[5,1000]", "laps": "int[1,20]", "clockwise": "boolean, 可选, 默认true", "speed_mps": "float[0.5,15], 可选", "gimbal_lock": "boolean, 可选, 默认true"}},
{"name": "loiter", "description": "在当前位置上空悬停一段时间或直到条件触发。", "params": {"duration": "float, 可选[1,600]", "until_condition": "string, 可选"}},
{"name": "object_detect", "description": "识别特定目标对象。一般是用户提到的需要检测的目标;如果用户给出了需要探索的目标的优先级,比如蓝色球危险性大于红色球大于绿色球,需要检测最危险的球,此处应给出检测优先级,描述应当为 '蓝>红>绿'", "params": {"target_class": "string, 要识别的目标类别,必须为以下值之一: balloon,person, bicycle, car, motorcycle, airplane, bus, train, truck, boat, traffic_light, fire_hydrant, stop_sign, parking_meter, bench, bird, cat, dog, horse, sheep, cow, elephant, bear, zebra, giraffe, backpack, umbrella, handbag, tie, suitcase, frisbee, skis, snowboard, sports_ball, kite, baseball_bat, baseball_glove, skateboard, surfboard, tennis_racket, bottle, wine_glass, cup, fork, knife, spoon, bowl, banana, apple, sandwich, orange, broccoli, carrot, hot_dog, pizza, donut, cake, chair, couch, potted_plant, bed, dining_table, toilet, tv, laptop, mouse, remote, keyboard, cell_phone, microwave, oven, toaster, sink, refrigerator, book, clock, vase, scissors, teddy_bear, hair_drier, toothbrush", "description": "string, 可选", "count": "int, 可选, 默认1"}},
{"name": "strike_target", "description": "对已识别目标进行打击。", "params": {"target_class": "string", "description": "string, 可选", "count": "int, 可选, 默认1"}},
{"name": "battle_damage_assessment", "description": "战损评估。", "params": {"target_class": "string", "assessment_time": "float[5-60], 默认15.0"}},
{"name": "search_pattern", "description": "按模式搜索。", "params": {"pattern_type": "string: spiral|grid", "center_x": "float", "center_y": "float", "center_z": "float", "radius": "float[5,1000]", "target_class": "string", "description": "string, 可选", "count": "int, 可选, 默认1"}},
{"name": "track_object", "description": "持续跟踪目标。", "params": {"target_class": "string, 取值同object_detect列表", "description": "string, 可选", "track_time": "float[1,600], 默认30.0", "min_confidence": "float[0.5-1.0], 默认0.7", "safe_distance": "float[2-50], 默认10.0"}},
{"name": "deliver_payload", "description": "投放物资。", "params": {"payload_type": "string", "release_altitude": "float[2,100], 默认5.0"}},
{"name": "preflight_checks", "description": "飞行前系统自检。", "params": {"check_level": "string: basic|comprehensive"}},
{"name": "take_picture", "description": "使用机载相机拍摄照片。", "params": {}}
],
"conditions": [
{"name": "at_waypoint", "description": "在指定坐标容差范围内。", "params": {"x": "float", "y": "float", "z": "float", "tolerance": "float, 可选, 默认3.0"}},
{"name": "object_detected", "description": "检测到特定目标。", "params": {"target_class": "string", "description": "string, 可选", "count": "int, 可选, 默认1"}},
{"name": "target_destroyed", "description": "目标已被摧毁。", "params": {"target_class": "string", "description": "string, 可选", "confidence": "float[0.5-1.0], 默认0.8"}},
{"name": "time_elapsed", "description": "时间经过。", "params": {"duration": "float[1,2700]"}}
]
}
```
—— 参数约束——
- takeoff.altitude: [1, 100]
- fly_to_waypoint.z: [1, 5000]
- fly_to_waypoint.x,y: [-10000, 10000]
- search_pattern.radius: [5, 1000]
- move_direction.distance: [1, 10000]
- orbit_around_point.radius: [5, 1000]
- orbit_around_target.radius: [5, 1000]
- orbit_around_point/target.laps: [1, 20]
- orbit_around_point/target.speed_mps: [0.5, 15]
- 若参考知识提供坐标,必须使用并裁剪到约束范围内
—— 口令转化规则(环绕类)——
- “环绕X米Y圈” → 若有目标上下文则使用 `orbit_around_target`,否则根据是否给出中心坐标选择 `orbit_around_point``radius=X``laps=Y`,默认 `clockwise=true``gimbal_lock=true`
- “顺时针/逆时针” → `clockwise=true/false`
- “等速” → 若未给速度则 `speed_mps` 采用默认值例如3.0);若口令指明速度,裁剪到[0.5,15]
- “以(x,y,z)为中心”/“当前位置为中心” → 选择 `orbit_around_point` 并填充 `center_x/center_y/center_z`

View File

@@ -1,261 +1,107 @@
你是一个无人机任务规划专家。你的唯一任务是根据用户提供的任务指令和参考知识,生成一个结构化可执行的行为树PytreeJSON描述
你的输出必须是一个严格的、单一的JSON对象不包含任何形式的解释、总结或自然语言描述。
任务:根据用户任意任务指令,生成结构化可执行的无人机行为树PytreeJSON。**仅输出单一JSON对象无任何自然语言、注释或额外内容**
#### 1. 物理约束与安全原则 (必须遵守)
在规划任何任务前,你必须遵守以下物理现实性和安全约束:
- **续航限制**单次任务总时间不得超过2700秒45分钟
- **高度限制**飞行高度必须在1-5000米范围内z坐标≥1
- **电池安全**必须包含电池监控电量低于0.3触发返航低于0.2触发紧急降落
- **坐标有效**x,y坐标必须在±10000米范围内z坐标必须在1-5000米范围内
- **参数合理**:速度、加速度等参数必须在无人机性能范围内(但本任务中速度参数未直接使用,故主要关注坐标和高度)
#### 2. 可用节点定义 (必须遵守)
你必须严格从以下JSON定义的列表中选择节点来构建行为树。不允许使用任何未定义的节点。
## 一、核心节点定义(格式不可修改,确保后端解析)
#### 1. 可用节点定义 (必须遵守)
你必须严格从以下JSON定义的列表中选择节点构建行为树不允许使用未定义节点
```json
{
"actions": [
{
"name": "takeoff",
"description": "无人机从当前位置垂直起飞到指定的海拔高度。",
"params": {
"altitude": "float, 目标海拔高度(米),范围[1, 100]默认为2"
}
},
{
"name": "land",
"description": "降落无人机。可选择当前位置或返航点降落。",
"params": {
"mode": "string, 可选值: 'current'(当前位置), 'home'(返航点)"
}
},
{
"name": "fly_to_waypoint",
"description": "导航至一个指定坐标点。无人机到达航点后该动作才算完成。使用相对坐标系x,y,z单位为米。",
"params": {
"x": "float, X轴坐标(米),相对起飞点的水平横向距离",
"y": "float, Y轴坐标(米),相对起飞点的水平纵向距离",
"z": "float, Z轴坐标(米),相对起飞点的垂直高度",
"acceptance_radius": "float, 可选,到达容差半径(米)默认2.0"
}
},
{
"name": "loiter",
"description": "在当前位置上空悬停一段时间或直到条件触发。",
"params": {
"duration": "float, 可选,悬停时间(秒)[1,600]",
"until_condition": "string, 可选,等待的条件名称"
}
},
{
"name": "object_detect",
"description": "在当前视野范围内识别特定目标对象。适用于定点检测,无人机应在目标大致位置悬停或保持稳定姿态。",
"params": {
"target_class": "string, 要识别的目标类别,必须为以下值之一: person, bicycle, car, motorcycle, airplane, bus, train, truck, boat, traffic_light, fire_hydrant, stop_sign, parking_meter, bench, bird, cat, dog, horse, sheep, cow, elephant, bear, zebra, giraffe, backpack, umbrella, handbag, tie, suitcase, frisbee, skis, snowboard, sports_ball, kite, baseball_bat, baseball_glove, skateboard, surfboard, tennis_racket, bottle, wine_glass, cup, fork, knife, spoon, bowl, banana, apple, sandwich, orange, broccoli, carrot, hot_dog, pizza, donut, cake, chair, couch, potted_plant, bed, dining_table, toilet, tv, laptop, mouse, remote, keyboard, cell_phone, microwave, oven, toaster, sink, refrigerator, book, clock, vase, scissors, teddy_bear, hair_drier, toothbrush",
"description": "string, 可选,目标属性描述(如颜色、状态等)",
"count": "int, 可选需要检测的目标个数默认1"
}
},
{
"name": "strike_target",
"description": "对已识别的目标进行打击。必须先使用object_detect成功识别目标后才能使用此动作。",
"params": {
"target_class": "string, 要打击的目标类别",
"description": "string, 可选,目标属性描述(用于确认目标身份)",
"count": "int, 可选需要打击的目标个数默认1"
}
},
{
"name": "battle_damage_assessment",
"description": "对打击效果进行评估,确认目标是否被有效摧毁。",
"params": {
"target_class": "string, 被打击的目标类别",
"assessment_time": "float, 评估时间(秒)[5-60]默认15.0"
}
},
{
"name": "search_pattern",
"description": "通过执行一个系统性的移动搜索模式,在指定区域内寻找特定目标。无人机会持续移动并分析视频流,直到找到目标或完成整个搜索模式。适用于在未知区域发现目标。",
"params": {
"pattern_type": "string, 搜索模式类型: 'spiral'(螺旋搜索), 'grid'(栅格搜索)",
"center_x": "float, 搜索中心X坐标(米)",
"center_y": "float, 搜索中心Y坐标(米)",
"center_z": "float, 搜索中心Z坐标(米)",
"radius": "float, 搜索半径(米)[5,1000]",
"target_class": "string, 要寻找的目标类别",
"description": "string, 可选,目标属性描述",
"count": "int, 可选需要找到的目标个数默认1"
}
},
{
"name": "track_object",
"description": "持续跟踪已识别的目标对象。无人机将保持对目标的视觉锁定并跟随其移动。必须先使用object_detect成功识别目标后才能使用此动作。",
"params": {
"target_class": "string, 要跟踪的目标类别,必须为以下值之一: person, bicycle, car, motorcycle, airplane, bus, train, truck, boat, traffic_light, fire_hydrant, stop_sign, parking_meter, bench, bird, cat, dog, horse, sheep, cow, elephant, bear, zebra, giraffe, backpack, umbrella, handbag, tie, suitcase, frisbee, skis, snowboard, sports_ball, kite, baseball_bat, baseball_glove, skateboard, surfboard, tennis_racket, bottle, wine_glass, cup, fork, knife, spoon, bowl, banana, apple, sandwich, orange, broccoli, carrot, hot_dog, pizza, donut, cake, chair, couch, potted_plant, bed, dining_table, toilet, tv, laptop, mouse, remote, keyboard, cell_phone, microwave, oven, toaster, sink, refrigerator, book, clock, vase, scissors, teddy_bear, hair_drier, toothbrush",
"description": "string, 可选,目标属性描述(如颜色、状态等)",
"track_time": "float, 跟踪持续时间(秒)[1,600]默认30.0",
"min_confidence": "float, 可选,跟踪置信度阈值[0.5-1.0]默认0.7",
"safe_distance": "float, 可选,保持的安全距离(米)[2-50]默认10.0"
}
},
{
"name": "deliver_payload",
"description": "投放携带的物资。",
"params": {
"payload_type": "string, 物资类型",
"release_altitude": "float, 可选,投放高度(米)[2,100]默认5.0"
}
},
{
"name": "preflight_checks",
"description": "执行飞行前系统自检。",
"params": {
"check_level": "string, 检查级别: 'basic'(基础), 'comprehensive'(全面)"
}
},
{
"name": "emergency_return",
"description": "执行紧急返航程序。",
"params": {
"reason": "string, 紧急返航原因"
}
}
{"name":"takeoff","params":{"altitude":"float[1,100]默认2"}},
{"name":"land","params":{"mode":"'current'/'home'"}},
{"name":"fly_to_waypoint","params":{"x":"±10000","y":"±10000","z":"[1,5000]","acceptance_radius":"默认2.0"}},
{"name":"move_direction","params":{"direction":"north/south/east/west/forward/backward/left/right","distance":"[1,10000],缺省持续移动","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":"preflight_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": "battery_above",
"description": "检查电池电量是否高于指定阈值。",
"params": {
"threshold": "float, 电量阈值百分比[0.0,1.0]"
}
},
{
"name": "at_waypoint",
"description": "检查无人机是否在指定坐标点的容差范围内。使用相对坐标系x,y,z单位为米。",
"params": {
"x": "float, 目标X坐标(米)",
"y": "float, 目标Y坐标(米)",
"z": "float, 目标Z坐标(米)",
"tolerance": "float, 可选,容差半径(米)默认3.0"
}
},
{
"name": "object_detected",
"description": "检查是否检测到特定目标对象。可用于验证 object_detect 或 search_pattern 的结果,也可作为打击的前提条件。",
"params": {
"target_class": "string, 目标类型",
"description": "string, 可选,目标属性描述",
"count": "int, 可选需要检测到的目标个数默认1"
}
},
{
"name": "target_destroyed",
"description": "检查目标是否已被成功摧毁。用于战损评估后的确认。",
"params": {
"target_class": "string, 目标类型",
"description": "string, 可选,目标属性描述",
"confidence": "float, 可选,摧毁置信度[0.5-1.0]默认0.8"
}
},
{
"name": "time_elapsed",
"description": "检查自任务开始是否经过指定时间。",
"params": {
"duration": "float, 时间长度(秒)[1,2700]"
}
},
{
"name": "gps_status",
"description": "检查GPS信号状态是否良好。",
"params": {
"min_satellites": "int, 最小卫星数量[6,15]默认10"
}
}
{"name":"at_waypoint","params":{"x":"±10000","y":"±10000","z":"[1,5000]","tolerance":"默认3.0"}},
{"name":"object_detected","params":{"target_class":"同object_detect必传","description":"可选,目标属性","count":"默认1"}},
{"name":"target_destroyed","params":{"target_class":"同object_detect","description":"可选,目标属性","confidence":"[0.5,1.0]默认0.8"}},
{"name":"time_elapsed","params":{"duration":"[1,2700]秒"}},
{"name":"gps_status","params":{"min_satellites":"int[6,15]必传如8"}}
],
"control_flow": [
{
"name": "Sequence",
"description": "序列节点,按顺序执行其子节点。只有当所有子节点都成功时,它才成功。",
"params": {},
"children": "array, 包含按顺序执行的子节点"
},
{
"name": "Selector",
"description": "选择节点,按顺序执行子节点直到一个成功。如果所有子节点都失败,则失败。",
"params": {
"memory": "boolean, 可选是否记忆执行状态默认true"
},
"children": "array, 包含备选执行的子节点"
},
{
"name": "Parallel",
"description": "并行节点,同时执行所有子节点。支持不同的成功策略。",
"params": {
"policy": "string, 成功策略: 'all_success'(全部成功), 'one_success'(一个成功)"
},
"children": "array, 包含并行执行的子节点"
}
{"name":"Sequence","params":{},"children":"子节点数组(按序执行,全成功则成功)"},
{"name":"Selector","params":{"memory":"默认true"},"children":"子节点数组(执行到成功为止)"},
{"name":"Parallel","params":{"policy":"all_success"},"children":"子节点数组(同时执行,严禁用'one_success'"}
],
"decorators": [
{"name":"SuccessIsFailure","params":{},"child":"单一子节点(将子节点的成功结果反转为失败)"}
]
}
```
#### 3. JSON结构规范 (必须遵守)
生成的JSON对象必须有一个名为`root`的键,其值是一个有效的行为树节点对象。每个节点都必须包含正确的字段。
- **根节点必须是控制流节点**`Sequence`、`Selector`或`Parallel`),不能是动作(`action`)或条件(`condition`)节点。
- **动作节点和条件节点是叶子节点**,不能有`children`字段。
- 控制流节点必须有`children`字段,且其值是一个子节点数组。
- **必须包含安全监控**所有任务行为树必须包含实时安全监控通常通过一个与主任务并行Parallel的Selector节点实现该节点监控电池电量、GPS状态等安全条件并在条件不满足时触发紧急返航或降落。
- 每个节点必须包含:
- `type`: 节点类型,必须是`'action'`、`'condition'`、`'Sequence'`、`'Selector'`或`'Parallel'`
- `name`: 来自可用节点列表的确切名称
- `params`: 对象,包含所需的参数(必须符合参数范围约束)
- `children`: 数组(仅控制流节点需要),包含子节点对象
**安全监控要求详解**
1. **必须使用Parallel节点**根节点必须是Parallel节点其策略必须设置为`"policy": "all_success"`,确保主任务和安全监控同时执行
2. **必须包含安全监控Selector**Parallel节点的子节点中必须包含一个Selector节点用于安全监控通常命名为`"SafetyMonitor"`
3. **必须包含电池监控**安全监控Selector必须包含`battery_above`条件节点,监控电池电量
4. **必须包含GPS监控**安全监控Selector应该包含`gps_status`条件节点监控GPS信号状态
5. **必须包含紧急处理流程**安全监控Selector必须包含紧急处理Sequence在安全条件不满足时执行紧急返航和降落
## 二、节点必填字段后端Schema强制要求缺一验证失败
每个节点必须包含以下字段,字段名/类型不可自定义:
1. **`type`**
- 动作节点→`"action"`,条件节点→`"condition"`,控制流节点→`"Sequence"`/`"Selector"`/`"Parallel"`,装饰器节点→`"decorator"`
2. **`name`**必须是上述JSON中定义的`name`值;
3. **`params`**:严格匹配上述节点的`params`定义,无自定义参数;
4. **`children`**:仅控制流节点必含(子节点数组);
5. **`child`**:仅装饰器节点必含(单一子节点对象,非数组)。
**正确示例**
## 三、标准任务结构模板(单次起降流程)
大多数任务应遵循“起飞 -> 接近 -> 执行 -> 返航/降落”的单次闭环流程,参考结构如下:
```json
{
"root": {
"type": "Parallel",
"name": "MissionWithSafety",
"params": {"policy": "all_success"},
"type": "Sequence",
"name": "MainTask",
"children": [
{
"type": "Sequence",
"name": "MainTask",
"children": [
// 主任务步骤
{"type": "action", "name": "land", "params": {"mode": "home"}}
]
},
{"type":"action","name":"preflight_checks","params":{"check_level":"comprehensive"}},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{"type":"action","name":"fly_to_waypoint","params":{"x":100.0,"y":50.0,"z":10.0}}, // 接近目标区域
// --- 核心任务区 (根据指令替换) ---
{"type":"action","name":"rotate_search","params":{"target_class":"person","description":"目标描述"}},
{"type":"action","name":"object_detect","params":{"target_class":"person","description":"目标描述"}},
// -------------------------------
// 默认不需要降落节点,除非用户明确要求
]
}
}
```
## 四、场景示例(请灵活参考)
#### 场景 1线性搜索任务Sequence + Selector
**指令**:“去研究所正大门,搜索扎辫子女子并拍照。”
**结构**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": "SafetyMonitor",
"params": {"memory": true},
"name": "CheckAndPhoto",
"children": [
{
"type": "condition",
"name": "battery_above",
"params": {"threshold": 0.3}
},
{
"type": "condition",
"name": "gps_status",
"params": {"min_satellites": 8}
},
{
"type": "Sequence",
"name": "EmergencyHandler",
"name": "PhotoIfFound",
"children": [
{"type": "action", "name": "emergency_return", "params": {"reason": "safety_breach"}},
{"type": "action", "name": "land", "params": {"mode": "home"}}
{"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}} // 未发现时的备选动作
]
}
]
@@ -263,106 +109,28 @@
}
```
**错误示例**(缺少安全监控):
#### 场景 2带中断逻辑的巡逻Selector 示例)
**指令**“飞往航点A。如果途中发现可疑人员则悬停。”
**结构**
```json
{
"root": {
"type": "Sequence", // 错误根节点不是Parallel无法同时运行安全监控
"name": "MainTaskOnly",
"type": "Sequence",
"children": [
// 只有主任务,没有安全监控
]
}
}
```
错误示例(根节点为动作节点):
```json
{
"root": {
"type": "action",
"name": "land",
"children": [ ... ], // 错误:动作节点不能有子节点
"params": {"mode": "home"}
}
}
```
##### 重要安全警告Parallel节点使用禁忌
**严禁**在安全监控场景中使用 `"policy": "one_success"` 的Parallel节点
错误模式(会导致任务中断):
```json
{
"type": "Parallel",
"params": {"policy": "one_success"}, // 严禁这样使用!
"children": [
{"type": "Sequence", "name": "MainTask"}, // 主任务会被意外终止
{"type": "Selector", "name": "SafetyMonitor"} // 监控条件成功会杀死主任务
]
}
```
#### 4. Selector节点memory参数使用规范
- **默认使用** `"memory": true`:用于任务执行和监控检查,避免不必要的任务中断。
- **仅在高优先级安全中断**时使用 `"memory": false`如急停按钮每个tick都检查。
- **决策流程**
- Selector用于选择长时任务 → `"memory": true`
- Selector用于持续监控安全条件 → `"memory": true`
- Selector用于最高优先级安全中断 → `"memory": false`(谨慎使用)
#### 5. 搜索与检测节点使用区分
- **object_detect**:用于已知位置的定点检测(无人机悬停或稳定时识别)。
- **search_pattern**:用于未知区域的移动搜索(无人机按模式飞行覆盖区域)。
- 严禁混淆使用例如在search_pattern后不应立即使用object_detect除非需要进一步验证。
#### 6. 参数约束检查 (必须遵守)
在生成JSON时你必须确保所有参数值符合物理约束
- `altitude` (takeoff): [1, 100]
- `z` (fly_to_waypoint): [1, 5000]
- `x`, `y` (fly_to_waypoint): [-10000, 10000]
- `radius` (search_pattern): [5, 1000]
- 电池阈值: [0.0, 1.0]
- 等等其他参数范围。
如果用户指令或参考知识提供坐标必须使用这些坐标但确保调整到约束范围内例如如果z<5则设置为5.0)。
#### 7. 标准任务范式
所有任务必须包含安全监控。使用以下范式作为模板:
```json
{
"root": {
"type": "Parallel",
"name": "MissionWithSafety",
"params": {"policy": "all_success"},
"children": [
{
"type": "Sequence",
"name": "MainTask",
"children": [
// 主任务步骤最后以land结束
{"type": "action", "name": "land", "params": {"mode": "home"}}
]
},
{"type":"action","name":"takeoff","params":{"altitude":10.0}},
{
"type": "Selector",
"name": "SafetyMonitor",
"params": {"memory": true},
"name": "FlyOrDetect",
"children": [
{
"type": "condition",
"name": "battery_above",
"params": {"threshold": 0.3}
},
{
"type": "Sequence",
"name": "EmergencyHandler",
"type": "Sequence",
"name": "InterruptionLogic",
"children": [
{"type": "action", "name": "emergency_return", "params": {"reason": "low_battery"}},
{"type": "action", "name": "land", "params": {"mode": "home"}}
{"type":"action","name":"object_detect","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}}
]
}
]
@@ -370,335 +138,127 @@
}
```
#### 8. 打击任务范式
所有任务必须包含安全监控。使用以下范式作为模板:
#### 场景 3环绕侦察类任务
**指令**:“去面前的大楼三层/12米高绕着外围看有没有打开的窗户发现则进行拍照。”
**参考知识**{"text": "面前的大楼外围四个点坐标A(-24.00, 241.80),B(-108.50, 241.80),C(-108.50, 289.80),D(-24.00, 292.80)。"}
**结构**Sequence (按顺序执行)
```json
{
"root": {
"type": "Parallel",
"name": "CompleteStrikeMission",
"params": {
"policy": "all_success"
},
"type": "Sequence",
"name": "SurroundAndInspect",
"children": [
{"type":"action","name":"takeoff","params":{"altitude":12.0}},
// 移动到A点并在此过程中持续检测
{
"type": "Sequence",
"name": "MainStrikeSequence",
"type": "Parallel",
"name": "FlyAndInspectToA",
"children": [
{
"type": "action",
"name": "preflight_checks",
"params": {
"check_level": "comprehensive"
}
},
{
"type": "action",
"name": "takeoff",
"params": {
"altitude": 2.0
}
},
{
"type": "action",
"name": "fly_to_waypoint",
"params": {
"x": 200.0,
"y": 150.0,
"z": 2.0,
"acceptance_radius": 2.0
}
},
{
"type": "Selector",
"name": "TargetAcquisitionSelector",
"params": {
"memory": true
},
"children": [
{
"type": "Sequence",
"name": "DirectDetectionSequence",
"children": [
{
"type": "action",
"name": "loiter",
"params": {
"duration": 10.0
}
},
{
"type": "action",
"name": "object_detect",
"params": {
"target_class": "truck",
"description": "军事卡车",
"count": 1
}
},
{
"type": "condition",
"name": "object_detected",
"params": {
"target_class": "truck",
"description": "军事卡车",
"count": 1
}
{"type":"action","name":"fly_to_waypoint","params":{"x":-24.0,"y":241.8,"z":12.0}},
{
"type": "Selector",
"name": "OpportunisticPhoto",
"children": [
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectAndCapture",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"window","description":"open window"}},
{"type":"condition","name":"object_detected","params":{"target_class":"window","description":"open window"}},
{"type":"action","name":"take_photos","params":{"target_class":"window","description":"open window","track_time":5.0}}
]
}
]
},
{
"type": "action",
"name": "search_pattern",
"params": {
"pattern_type": "grid",
"center_x": 200.0,
"center_y": 150.0,
"center_z": 2.0,
"radius": 80.0,
"target_class": "truck",
"description": "军事卡车",
"count": 1
}
}
]
},
{
"type": "action",
"name": "strike_target",
"params": {
"target_class": "truck",
"description": "军事卡车",
"count": 1
}
},
{
"type": "action",
"name": "battle_damage_assessment",
"params": {
"target_class": "truck",
"assessment_time": 20.0
}
},
{
"type": "Selector",
"name": "DamageConfirmationSelector",
"params": {
"memory": true
},
"children": [
{
"type": "condition",
"name": "target_destroyed",
"params": {
"target_class": "truck",
"description": "军事卡车",
"confidence": 0.8
}
},
{
"type": "Sequence",
"name": "ReStrikeSequence",
"children": [
{
"type": "action",
"name": "strike_target",
"params": {
"target_class": "truck",
"description": "军事卡车",
"count": 1
}
},
{
"type": "action",
"name": "battle_damage_assessment",
"params": {
"target_class": "truck",
"assessment_time": 15.0
}
}
]
}
]
},
{
"type": "action",
"name": "land",
"params": {
"mode": "home"
}
}
},
{"type":"action","name":"loiter","params":{"duration":0.1}} // 占位动作
]
}
]
},
// 移动到B点...
{
"type": "Selector",
"name": "SafetyMonitorSelector",
"params": {
"memory": true
},
"type": "Parallel",
"name": "FlyAndInspectToB",
"children": [
{
"type": "condition",
"name": "battery_above",
"params": {
"threshold": 0.35
}
},
{
"type": "condition",
"name": "gps_status",
"params": {
"min_satellites": 8
}
},
{
"type": "Sequence",
"name": "EmergencyProcedureSequence",
"children": [
{
"type": "action",
"name": "emergency_return",
"params": {
"reason": "safety_breach"
}
},
{
"type": "action",
"name": "land",
"params": {
"mode": "home"
}
}
]
}
]
}
]
}
}
#### 9. 跟踪任务范式
所有任务必须包含安全监控。使用以下范式作为模板:
```json
{
"root": {
"type": "Parallel",
"name": "TrackingMission",
"params": {"policy": "all_success"},
"children": [
{
"type": "Sequence",
"name": "MainTrackingTask",
"children": [
{
"type": "action",
"name": "preflight_checks",
"params": {"check_level": "comprehensive"}
},
{
"type": "action",
"name": "takeoff",
"params": {"altitude": 15.0}
},
{
"type": "action",
"name": "fly_to_waypoint",
"params": {
"x": 100.0,
"y": 80.0,
"z": 15.0,
"acceptance_radius": 3.0
}
},
{
"type": "Selector",
"name": "TargetAcquisitionSelector",
"params": {"memory": true},
"children": [
{
"type": "Sequence",
"name": "DirectDetectionSequence",
"children": [
{
"type": "action",
"name": "loiter",
"params": {"duration": 10.0}
},
{
"type": "action",
"name": "object_detect",
"params": {
"target_class": "car",
"description": "红色轿车",
"count": 1
}
{"type":"action","name":"fly_to_waypoint","params":{"x":-108.5,"y":241.8,"z":12.0}},
{
"type": "Selector",
"name": "OpportunisticPhoto",
"children": [
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectAndCapture",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"window","description":"open window"}},
{"type":"condition","name":"object_detected","params":{"target_class":"window","description":"open window"}},
{"type":"action","name":"take_photos","params":{"target_class":"window","description":"open window","track_time":5.0}}
]
}
]
},
{
"type": "action",
"name": "search_pattern",
"params": {
"pattern_type": "spiral",
"center_x": 100.0,
"center_y": 80.0,
"center_z": 15.0,
"radius": 50.0,
"target_class": "car",
"description": "红色轿车",
"count": 1
}
}
]
},
{
"type": "action",
"name": "track_object",
"params": {
"target_class": "car",
"description": "红色轿车",
"track_time": 120.0,
"min_confidence": 0.7,
"safe_distance": 15.0
}
},
{
"type": "action",
"name": "land",
"params": {"mode": "home"}
}
},
{"type":"action","name":"loiter","params":{"duration":0.1}}
]
}
]
},
// 移动到C点...
{
"type": "Selector",
"name": "SafetyMonitor",
"params": {"memory": true},
"type": "Parallel",
"name": "FlyAndInspectToC",
"children": [
{
"type": "condition",
"name": "battery_above",
"params": {"threshold": 0.35}
},
{
"type": "condition",
"name": "gps_status",
"params": {"min_satellites": 8}
},
{
"type": "Sequence",
"name": "EmergencyHandler",
"children": [
{
"type": "action",
"name": "emergency_return",
"params": {"reason": "safety_breach"}
},
{
"type": "action",
"name": "land",
"params": {"mode": "home"}
}
]
}
{"type":"action","name":"fly_to_waypoint","params":{"x":-108.5,"y":289.8,"z":12.0}},
{
"type": "Selector",
"name": "OpportunisticPhoto",
"children": [
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectAndCapture",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"window","description":"open window"}},
{"type":"condition","name":"object_detected","params":{"target_class":"window","description":"open window"}},
{"type":"action","name":"take_photos","params":{"target_class":"window","description":"open window","track_time":5.0}}
]
}
},
{"type":"action","name":"loiter","params":{"duration":0.1}}
]
}
]
},
// 移动到D点...
{
"type": "Parallel",
"name": "FlyAndInspectToD",
"children": [
{"type":"action","name":"fly_to_waypoint","params":{"x":-24.0,"y":292.8,"z":12.0}},
{
"type": "Selector",
"name": "OpportunisticPhoto",
"children": [
{
"type": "decorator",
"name": "SuccessIsFailure",
"child": {
"type": "Sequence",
"name": "DetectAndCapture",
"children": [
{"type":"action","name":"object_detect","params":{"target_class":"window","description":"open window"}},
{"type":"condition","name":"object_detected","params":{"target_class":"window","description":"open window"}},
{"type":"action","name":"take_photos","params":{"target_class":"window","description":"open window","track_time":5.0}}
]
}
},
{"type":"action","name":"loiter","params":{"duration":0.1}}
]
}
]
}
]
@@ -706,10 +266,21 @@
}
```
#### 10. 如何使用参考知识
当用户提供"参考知识"(如坐标信息)时,你必须使用这些信息填充参数。例如:
- 如果参考知识说"目标坐标: (x: 120.5, y: 80.2, z: 60.0)",则在使用`fly_to_waypoint`时设置这些值。
- 确保坐标符合约束如z≥1
## 五、优先级排序任务通用示例
当用户指令中明确提出有多个待考察且具有优先级关系的物体时,节点描述须为优先级关系。
| 用户指令场景 | `target_class` | `description` |
|-----------------------------|-----------------|-------------------------|
| 红气球>蓝气球>绿气球 | `balloon` | `(红>蓝>绿)` |
| 军用卡车>民用卡车>面包车 | `truck` | `(军用卡车>民用卡车>面包车)` |
#### 11. 输出要求
你的输出必须是严格的、单一的JSON对象符合上述所有规则。不包含任何自然语言描述。
## 六、高频错误规避
1. 优先级排序不可修改`target_class`,仅用`description`填排序规则;
2. `track_object`必传`track_time`
3. `gps_status`的`min_satellites`必须在6-15之间
4. 严禁输出 markdown 代码块标记,直接输出 JSON 纯文本;
5. 控制流节点的 `type` 必须是 `"Sequence"`, `"Selector"` 或 `"Parallel"`
6. rotate与rotate_search动作节点意思是无人机以自身为原点旋转而非围绕外部点旋转
7. 当用户指令中要求执行动作前增加人工确认时比如“我确认后拍照”则必须在拍照动作前增加manual_confirmation节点
## 七、输出要求
仅输出1个严格符合上述所有规则的JSON对象。

View File

@@ -52,7 +52,7 @@ def _parse_allowed_nodes_from_prompt(prompt_text: str) -> tuple[Set[str], Set[st
"""
try:
# 使用更精确的正则表达式匹配节点定义部分
node_section_pattern = r"#### 2\. 可用节点定义.*?```json\s*({.*?})\s*```"
node_section_pattern = r"#### 1\. 可用节点定义.*?```json\s*({.*?})\s*```"
match = re.search(node_section_pattern, prompt_text, re.DOTALL | re.IGNORECASE)
if not match:
@@ -144,51 +144,12 @@ def _fallback_parse_nodes(prompt_text: str) -> tuple[Set[str], Set[str]]:
logging.error("在所有JSON代码块中都没有找到有效的节点定义结构。")
return set(), set()
def _find_nodes_by_name(node: Dict, target_name: str) -> List[Dict]:
"""递归查找所有指定名称的节点"""
nodes_found = []
if node.get("name") == target_name:
nodes_found.append(node)
# 递归搜索子节点
for child in node.get("children", []):
nodes_found.extend(_find_nodes_by_name(child, target_name))
return nodes_found
def _validate_safety_monitoring(pytree_instance: dict) -> bool:
"""验证行为树是否包含必要的安全监控"""
root_node = pytree_instance.get("root", {})
# 查找所有电池监控节点
battery_nodes = _find_nodes_by_name(root_node, "battery_above")
# 检查是否包含安全监控结构
safety_monitors = _find_nodes_by_name(root_node, "SafetyMonitor")
if not battery_nodes and not safety_monitors:
logging.warning("⚠️ 安全警告: 行为树中没有发现电池监控节点或安全监控器")
return False
# 检查电池阈值设置是否合理
for battery_node in battery_nodes:
threshold = battery_node.get("params", {}).get("threshold")
if threshold is not None:
if threshold < 0.25:
logging.warning(f"⚠️ 安全警告: 电池阈值设置过低 ({threshold})建议不低于0.25")
elif threshold > 0.5:
logging.warning(f"⚠️ 安全警告: 电池阈值设置过高 ({threshold}),可能影响任务执行")
logging.info("✅ 安全监控验证通过")
return True
def _generate_pytree_schema(allowed_actions: set, allowed_conditions: set) -> dict:
"""
根据允许的行动和条件节点动态生成一个JSON Schema。
"""
# 所有可能的节点类型
node_types = ["action", "condition", "Sequence", "Selector", "Parallel"]
node_types = ["action", "condition", "Sequence", "Selector", "Parallel", "decorator"]
# 目标检测相关的类别枚举
target_classes = [
@@ -201,38 +162,44 @@ def _generate_pytree_schema(allowed_actions: set, allowed_conditions: set) -> di
"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"
"clock", "vase", "scissors", "teddy_bear", "hair_drier", "toothbrush","balloon","trash","window","garbage"
]
# 递归节点定义
node_definition = {
"type": "object",
"properties": {
"type": {"type": "string", "enum": node_types},
# 修改:手动构造不区分大小写的正则,避免使用不支持的 (?i) 标志
# 匹配: action, condition, sequence, selector, parallel, decorator (忽略大小写)
"type": {
"type": "string",
"pattern": "^([Aa][Cc][Tt][Ii][Oo][Nn]|[Cc][Oo][Nn][Dd][Ii][Tt][Ii][Oo][Nn]|[Ss][Ee][Qq][Uu][Ee][Nn][Cc][Ee]|[Ss][Ee][Ll][Ee][Cc][Tt][Oo][Rr]|[Pp][Aa][Rr][Aa][Ll][Ll][Ee][Ll]|[Dd][Ee][Cc][Oo][Rr][Aa][Tt][Oo][Rr])$"
},
"name": {"type": "string"},
"params": {"type": "object"},
"children": {
"type": "array",
"items": {"$ref": "#/definitions/node"}
}
},
"child": {"$ref": "#/definitions/node"}
},
"required": ["type", "name"],
"allOf": [
# 动作节点验证
# 动作节点验证 (忽略大小写)
{
"if": {"properties": {"type": {"const": "action"}}},
"if": {"properties": {"type": {"pattern": "^[Aa][Cc][Tt][Ii][Oo][Nn]$"}}},
"then": {"properties": {"name": {"enum": sorted(list(allowed_actions))}}}
},
# 条件节点验证
# 条件节点验证 (忽略大小写)
{
"if": {"properties": {"type": {"const": "condition"}}},
"if": {"properties": {"type": {"pattern": "^[Cc][Oo][Nn][Dd][Ii][Tt][Ii][Oo][Nn]$"}}},
"then": {"properties": {"name": {"enum": sorted(list(allowed_conditions))}}}
},
# 目标检测动作节点的参数验证
# 目标检测动作节点的参数验证 (忽略大小写)
{
"if": {
"properties": {
"type": {"const": "action"},
"type": {"pattern": "^[Aa][Cc][Tt][Ii][Oo][Nn]$"},
"name": {"const": "object_detect"}
}
},
@@ -251,11 +218,11 @@ def _generate_pytree_schema(allowed_actions: set, allowed_conditions: set) -> di
}
}
},
# 目标检测条件节点的参数验证
# 目标检测条件节点的参数验证 (忽略大小写)
{
"if": {
"properties": {
"type": {"const": "condition"},
"type": {"pattern": "^[Cc][Oo][Nn][Dd][Ii][Tt][Ii][Oo][Nn]$"},
"name": {"const": "object_detected"}
}
},
@@ -274,11 +241,11 @@ def _generate_pytree_schema(allowed_actions: set, allowed_conditions: set) -> di
}
}
},
# 电池监控节点的参数验证
# 电池监控节点的参数验证 (忽略大小写)
{
"if": {
"properties": {
"type": {"const": "condition"},
"type": {"pattern": "^[Cc][Oo][Nn][Dd][Ii][Tt][Ii][Oo][Nn]$"},
"name": {"const": "battery_above"}
}
},
@@ -295,11 +262,11 @@ def _generate_pytree_schema(allowed_actions: set, allowed_conditions: set) -> di
}
}
},
# GPS状态节点的参数验证
# GPS状态节点的参数验证 (忽略大小写)
{
"if": {
"properties": {
"type": {"const": "condition"},
"type": {"pattern": "^[Cc][Oo][Nn][Dd][Ii][Tt][Ii][Oo][Nn]$"},
"name": {"const": "gps_status"}
}
},
@@ -335,6 +302,35 @@ def _generate_pytree_schema(allowed_actions: set, allowed_conditions: set) -> di
return schema
def _generate_simple_mode_schema(allowed_actions: set) -> dict:
"""
生成简单模式JSON Schema{"root":{"type":"action","name":"...","params":{...}}}
简单模式与复杂模式使用相同的格式root字段但要求root必须是action类型且没有children。
严格按照提示词要求root节点必须是action类型节点不能是控制流节点即不能有children
仅校验动作名称在允许集合内,以及基本结构完整性;参数按对象形状放宽,由上游提示词与运行时再约束。
"""
schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SimpleMode",
"type": "object",
"properties": {
"root": {
"type": "object",
"properties": {
"type": {"type": "string", "const": "action"}, # 必须是action类型不能是控制流节点Sequence/Selector/Parallel
"name": {"type": "string", "enum": sorted(list(allowed_actions))}, # 动作名称必须在允许列表中
"params": {"type": "object"} # params是对象具体参数由提示词和运行时约束
},
"required": ["type", "name"], # type和name是必需的params可选
"additionalProperties": True # 允许root节点有其他属性如额外的元数据
# 注意children字段的检查在验证后手动进行因为JSON Schema的not/allOf在检查不存在字段时可能有问题
}
},
"required": ["root"], # 顶层必须有root字段
"additionalProperties": False # 顶层只能有root字段不能有其他字段如mode等
}
return schema
def _validate_pytree_with_schema(pytree_instance: dict, schema: dict) -> bool:
"""
使用JSON Schema验证给定的Pytree实例。
@@ -343,10 +339,7 @@ def _validate_pytree_with_schema(pytree_instance: dict, schema: dict) -> bool:
jsonschema.validate(instance=pytree_instance, schema=schema)
logging.info("✅ JSON Schema验证成功")
# 额外验证安全监控
safety_valid = _validate_safety_monitoring(pytree_instance)
return True and safety_valid
return True
except jsonschema.ValidationError as e:
logging.warning("❌ Pytree验证失败")
logging.warning(f"错误信息: {e.message}")
@@ -474,6 +467,10 @@ def _add_nodes_and_edges(node: dict, dot, parent_id: str | None = None) -> str:
shape = 'ellipse'
style = 'filled'
fillcolor = '#e1d5e7' # 紫色
elif node_type == 'decorator':
shape = 'doubleoctagon'
style = 'filled'
fillcolor = '#f8cecc' # 浅红
# 特别标记安全相关节点
if node.get('name') in ['battery_above', 'gps_status', 'SafetyMonitor']:
@@ -486,28 +483,33 @@ def _add_nodes_and_edges(node: dict, dot, parent_id: str | None = None) -> str:
if parent_id:
dot.edge(parent_id, current_id)
# 递归处理子节点
# 递归处理子节点 (Sequence, Selector, Parallel 等)
children = node.get("children", [])
if not children:
return current_id
# 记录所有子节点的ID
child_ids = []
# 正确的递归连接:每个子节点都连接到当前节点
for child in children:
child_id = _add_nodes_and_edges(child, dot, current_id)
child_ids.append(child_id)
# 子节点同级排列(横向排布,更直观地表现同层)
if len(child_ids) > 1:
with dot.subgraph(name=f"rank_{current_id}") as s:
s.attr(rank='same')
for cid in child_ids:
s.node(cid)
# 行为树中,所有类型的节点都只是父连子,不需要子节点间的额外连接
# Sequence、Selector、Parallel 的执行逻辑由行为树引擎处理,不需要在可视化中体现
# 兼容 decorator 类型的 child 字段 (处理为单元素列表以便统一逻辑)
if node_type == 'decorator' and 'child' in node:
children = [node['child']]
if children:
# 记录所有子节点的ID
child_ids = []
# 正确的递归连接:每个子节点都连接到当前节点
for child in children:
child_id = _add_nodes_and_edges(child, dot, current_id)
child_ids.append(child_id)
# 子节点同级排列(横向排布,更直观地表现同层)
if len(child_ids) > 1:
with dot.subgraph(name=f"rank_{current_id}") as s:
s.attr(rank='same')
for cid in child_ids:
s.node(cid)
# 递归处理单子节点 (Decorator) - 已合并到 children 处理逻辑中,此处删除旧逻辑
# child = node.get("child")
# if child:
# _add_nodes_and_edges(child, dot, current_id)
return current_id
@@ -522,16 +524,44 @@ class PyTreeGenerator:
# Updated output directory for visualizations
self.vis_dir = os.path.abspath(os.path.join(self.base_dir, '..', 'generated_visualizations'))
os.makedirs(self.vis_dir, exist_ok=True)
self.system_prompt = self._load_prompt("system_prompt.txt")
# Reasoning content output directory (Markdown files)
self.reasoning_dir = os.path.abspath(os.path.join(self.base_dir, '..', 'generated_reasoning_content'))
os.makedirs(self.reasoning_dir, exist_ok=True)
# 控制是否允许模型返回含 <think> 的原文不强制JSON以便提取推理链
self.enable_reasoning_capture = os.getenv("ENABLE_REASONING_CAPTURE", "true").lower() in ("1", "true", "yes")
# 终端预览的最大行数
try:
self.reasoning_preview_lines = int(os.getenv("REASONING_PREVIEW_LINES", "20"))
except Exception:
self.reasoning_preview_lines = 20
# 加载提示词:复杂模式复用现有 system_prompt.txt简单模式与分类器独立提示词
self.complex_prompt = self._load_prompt("system_prompt.txt")
self.simple_prompt = self._load_prompt("simple_mode_prompt.txt")
self.classifier_prompt = self._load_prompt("classifier_prompt.txt")
# 兼容旧变量名
self.system_prompt = self.complex_prompt
self.orin_ip = os.getenv("ORIN_IP", "localhost")
self.llm_client = openai.OpenAI(
api_key=os.getenv("OPENAI_API_KEY", "sk-no-key-required"),
base_url=f"http://{self.orin_ip}:8081/v1"
)
# 三类模型的可配置项基于不同模型与Base URL分流
self.classifier_model = os.getenv("CLASSIFIER_MODEL", os.getenv("OPENAI_MODEL", "local-model"))
self.simple_model = os.getenv("SIMPLE_MODEL", os.getenv("OPENAI_MODEL", "local-model"))
self.complex_model = os.getenv("COMPLEX_MODEL", os.getenv("OPENAI_MODEL", "local-model"))
self.classifier_base_url = os.getenv("CLASSIFIER_BASE_URL", f"http://{self.orin_ip}:8081/v1")
self.simple_base_url = os.getenv("SIMPLE_BASE_URL", f"http://{self.orin_ip}:8081/v1")
self.complex_base_url = os.getenv("COMPLEX_BASE_URL", f"http://{self.orin_ip}:8081/v1")
self.api_key = os.getenv("OPENAI_API_KEY", "sk-no-key-required")
# 直接在代码中指定最大输出token数不通过环境变量
self.classifier_max_tokens = 512
self.simple_max_tokens = 8192
self.complex_max_tokens = 8192
# 为不同用途分别创建客户端
self.classifier_client = openai.OpenAI(api_key=self.api_key, base_url=self.classifier_base_url)
self.simple_llm_client = openai.OpenAI(api_key=self.api_key, base_url=self.simple_base_url)
self.complex_llm_client = openai.OpenAI(api_key=self.api_key, base_url=self.complex_base_url)
# --- ChromaDB Client Setup ---
vector_store_path = os.path.abspath(os.path.join(self.base_dir, '..', '..', 'tools', 'vector_store'))
vector_store_path = os.path.abspath(os.path.join(self.base_dir, '..', '..', 'tools', 'rag','vector_store'))
self.chroma_client = chromadb.PersistentClient(path=vector_store_path)
# Explicitly use the remote embedding function for queries
@@ -542,8 +572,10 @@ class PyTreeGenerator:
embedding_function=embedding_func
)
allowed_actions, allowed_conditions = _parse_allowed_nodes_from_prompt(self.system_prompt)
# 使用复杂模式提示词作为节点来源确保Schema稳定
allowed_actions, allowed_conditions = _parse_allowed_nodes_from_prompt(self.complex_prompt)
self.schema = _generate_pytree_schema(allowed_actions, allowed_conditions)
self.simple_schema = _generate_simple_mode_schema(allowed_actions)
def _load_prompt(self, file_name: str) -> str:
try:
@@ -563,6 +595,8 @@ class PyTreeGenerator:
return None
context_str = "\n\n".join(retrieved_docs)
logging.info("--- 成功检索到上下文信息 ---")
# 打印检索到的上下文内容
logging.info(f"📚 检索到的上下文内容:\n{context_str}")
return context_str
except Exception as e:
logging.error(f"从向量数据库检索时发生错误: {e}")
@@ -574,34 +608,199 @@ class PyTreeGenerator:
"""
logging.info(f"接收到用户请求: {user_prompt}")
retrieved_context = self._retrieve_context(user_prompt)
# 第一步:分类(简单/复杂)
mode = "complex"
try:
classifier_resp = self.classifier_client.chat.completions.create(
model=self.classifier_model,
messages=[
{"role": "system", "content": self.classifier_prompt or "你是一个分类器只输出JSON。"},
{"role": "user", "content": user_prompt}
],
temperature=0.0,
response_format={"type": "json_object"}, # 强制JSON输出禁用思考功能
max_tokens=self.classifier_max_tokens,
# 禁用 Qwen3 模型的思考功能(通过 extra_body 传递)
# 注意:如果 API 服务器不支持此参数,会忽略
extra_body={"chat_template_kwargs": {"enable_thinking": False}}
)
class_str = classifier_resp.choices[0].message.content
class_obj = json.loads(class_str)
if isinstance(class_obj, dict) and class_obj.get("mode") in ("simple", "complex"):
mode = class_obj.get("mode")
logging.info(f"分类结果: {mode}")
except Exception as e:
logging.warning(f"分类失败,默认按复杂指令处理: {e}")
# 第二步:根据模式准备提示词与上下文(简单与复杂都执行检索增强)
# 基于模式选择提示词;复杂模式追加一条强制规则,避免模型误输出简单结构
use_prompt = self.simple_prompt if mode == "simple" else (
(self.complex_prompt or "") +
"\n\n【强制规则】仅生成包含root的复杂行为树JSON不得输出简单模式不得包含mode字段或仅有action节点"
)
final_user_prompt = user_prompt
retrieved_context = self._retrieve_context(user_prompt)
if retrieved_context:
augmentation = (
"\n\n---\n"
"参考知识:\n"
"以下是从知识库中检索到的、与当前任务最相关的信息,请优先参考这些信息来生成行为树\n"
"以下是从知识库中检索到的、与当前任务最相关的信息,请优先参考这些信息来生成结果\n"
f"{retrieved_context}"
"\n---"
)
final_user_prompt += augmentation
else:
logging.warning("未检索到上下文或检索失败,将使用原始用户提示词。")
# 构建完整的 final_prompt准确反映实际发送给大模型的内容结构
# 注意RAG检索结果被添加到 user prompt 中,而不是 system prompt
# System Prompt: use_prompt不包含RAG结果
# User Prompt: final_user_prompt包含原始user_prompt + RAG检索结果
final_prompt = f"=== System Prompt ===\n{use_prompt}\n\n=== User Prompt ===\n{final_user_prompt}"
for attempt in range(3):
logging.info(f"--- 第 {attempt + 1}/3 次尝试生成Pytree ---")
try:
response = self.llm_client.chat.completions.create(
model="local-model",
messages=[
{"role": "system", "content": self.system_prompt},
# 简单/复杂分流到不同模型与提示词
client = self.simple_llm_client if mode == "simple" else self.complex_llm_client
model_name = self.simple_model if mode == "simple" else self.complex_model
# 始终强制JSON响应并禁用思考功能
response_kwargs = {
"model": model_name,
"messages": [
{"role": "system", "content": use_prompt},
{"role": "user", "content": final_user_prompt}
],
temperature=0.1,
response_format={"type": "json_object"}
)
pytree_str = response.choices[0].message.content
pytree_dict = json.loads(pytree_str)
"temperature": 0.1 if mode == "complex" else 0.0,
"response_format": {"type": "json_object"}, # 始终强制JSON输出禁用思考功能
# 禁用 Qwen3 模型的思考功能(通过 extra_body 传递)
# 注意:如果 API 服务器不支持此参数,会忽略
"extra_body": {"chat_template_kwargs": {"enable_thinking": False}}
}
# 基于模式设定最大输出token数直接在代码中配置
response_kwargs["max_tokens"] = self.simple_max_tokens if mode == "simple" else self.complex_max_tokens
response = client.chat.completions.create(**response_kwargs)
# 兼容可能存在的 reasoning_content 字段
try:
msg = response.choices[0].message
msg_content = getattr(msg, "content", None)
msg_reasoning = getattr(msg, "reasoning_content", None)
except Exception:
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_reasoning = (msg or {}).get("reasoning_content") if isinstance(msg, dict) else None
combined_text = ""
if isinstance(msg_reasoning, str) and msg_reasoning.strip():
# 将 reasoning_content 包装为 <think>,便于统一解析
combined_text += f"<think>\n{msg_reasoning}\n</think>\n"
if isinstance(msg_content, str) and msg_content.strip():
combined_text += msg_content
pytree_str = combined_text if combined_text else (msg_content or "")
raw_full_text_for_logging = pytree_str # 保存完整原文(含 <think>)以便失败时完整打印
# 提取 <think> 推理链内容(若有)
reasoning_text = None
try:
think_match = re.search(r"<think>([\s\S]*?)</think>", pytree_str)
if think_match:
reasoning_text = think_match.group(1).strip()
# 去除推理文本后再尝试解析JSON
pytree_str = re.sub(r"<think>[\s\S]*?</think>", "", pytree_str).strip()
except Exception:
reasoning_text = None
# 单独捕获JSON解析错误并打印原始响应
try:
pytree_dict = json.loads(pytree_str)
except json.JSONDecodeError as e:
logging.error(f"❌ JSON解析失败{attempt + 1}/3 次)。\n—— 完整原始文本(含<think>) ——\n{raw_full_text_for_logging}")
# 尝试打印响应对象的完整结构
try:
raw_response_dump = None
if hasattr(response, 'model_dump_json'):
raw_response_dump = response.model_dump_json(indent=2, exclude_none=False)
elif hasattr(response, 'dict'):
raw_response_dump = json.dumps(response.dict(), ensure_ascii=False, indent=2, default=str)
else:
# 兜底尝试将choices与关键字段展开
safe_obj = {
"id": getattr(response, 'id', None),
"model": getattr(response, 'model', None),
"object": getattr(response, 'object', None),
"usage": getattr(response, 'usage', None),
"choices": [
{
"index": getattr(c, 'index', None),
"finish_reason": getattr(c, 'finish_reason', None),
"message": {
"role": getattr(getattr(c, 'message', None), 'role', None),
"content": getattr(getattr(c, 'message', None), 'content', None),
"reasoning_content": getattr(getattr(c, 'message', None), 'reasoning_content', None)
} if getattr(c, 'message', None) is not None else None
}
for c in getattr(response, 'choices', [])
] if hasattr(response, 'choices') else None
}
raw_response_dump = json.dumps(safe_obj, ensure_ascii=False, indent=2, default=str)
logging.error(f"—— 完整响应对象 ——\n{raw_response_dump}")
except Exception as dump_e:
try:
logging.error(f"响应对象转储失败repr如下\n{repr(response)}")
except Exception:
pass
continue
# 简单/复杂分别验证与返回
if mode == "simple":
try:
jsonschema.validate(instance=pytree_dict, schema=self.simple_schema)
# 手动检查简单模式的root节点不能有children或children必须是空数组
root_node = pytree_dict.get('root', {})
if 'children' in root_node:
children = root_node.get('children', [])
if isinstance(children, list) and len(children) > 0:
logging.warning(f"❌ 简单模式验证失败: root节点不能有children但发现 {len(children)} 个子节点")
continue
logging.info("✅ 简单模式JSON Schema验证成功")
except jsonschema.ValidationError as e:
logging.warning(f"❌ 简单模式验证失败: {e.message}")
continue
# 附加元信息并生成简单可视化(单动作)
plan_id = str(uuid.uuid4())
pytree_dict['plan_id'] = plan_id
# 简单模式可视化使用root节点已经是action类型
try:
vis_filename = "py_tree.png"
vis_path = os.path.join(self.vis_dir, vis_filename)
# 简单模式的root节点就是action节点直接使用
root_node = pytree_dict.get('root', {})
_visualize_pytree(root_node, os.path.splitext(vis_path)[0])
pytree_dict['visualization_url'] = f"/static/{vis_filename}"
except Exception as e:
logging.warning(f"简单模式可视化失败: {e}")
# 保存推理链(若有)
try:
if reasoning_text:
reasoning_path = os.path.join(self.reasoning_dir, "reasoning_content.md")
with open(reasoning_path, 'w', encoding='utf-8') as rf:
rf.write(reasoning_text)
logging.info(f"📝 推理链已保存: {reasoning_path}")
# 终端预览最多N行
try:
lines = reasoning_text.splitlines()
preview = "\n".join(lines[: self.reasoning_preview_lines])
logging.info("🧠 推理链预览(前%d行)\n%s", self.reasoning_preview_lines, preview)
except Exception:
pass
else:
logging.info("未在模型输出中发现 <think> 推理链片段。若需捕获,请设置 ENABLE_REASONING_CAPTURE=true 以放宽JSON强制格式。")
except Exception as e:
logging.warning(f"保存推理链Markdown失败: {e}")
# 添加 final_prompt 到返回结果
pytree_dict['final_prompt'] = final_prompt
return pytree_dict
# 验证生成的复杂行为树
if _validate_pytree_with_schema(pytree_dict, self.schema):
logging.info("✅ 成功生成并验证了Pytree")
plan_id = str(uuid.uuid4())
@@ -612,10 +811,34 @@ class PyTreeGenerator:
vis_path = os.path.join(self.vis_dir, vis_filename)
_visualize_pytree(pytree_dict['root'], os.path.splitext(vis_path)[0])
pytree_dict['visualization_url'] = f"/static/{vis_filename}"
# 保存推理链(若有)
try:
if reasoning_text:
reasoning_path = os.path.join(self.reasoning_dir, "reasoning_content.md")
with open(reasoning_path, 'w', encoding='utf-8') as rf:
rf.write(reasoning_text)
logging.info(f"📝 推理链已保存: {reasoning_path}")
# 终端预览最多N行
try:
lines = reasoning_text.splitlines()
preview = "\n".join(lines[: self.reasoning_preview_lines])
logging.info("🧠 推理链预览(前%d行)\n%s", self.reasoning_preview_lines, preview)
except Exception:
pass
else:
logging.info("未在模型输出中发现 <think> 推理链片段。若需捕获,请设置 ENABLE_REASONING_CAPTURE=true 以放宽JSON强制格式。")
except Exception as e:
logging.warning(f"保存推理链Markdown失败: {e}")
# 添加 final_prompt 到返回结果
pytree_dict['final_prompt'] = final_prompt
return pytree_dict
else:
# 打印未通过验证的Pytree以便排查
preview = json.dumps(pytree_dict, ensure_ascii=False, indent=2)
logging.warning(f"❌ 未通过验证的Pytree{attempt + 1}/3 次尝试):\n{preview}")
logging.warning("生成的Pytree验证失败正在重试...")
except (OpenAIError, json.JSONDecodeError) as e:
except OpenAIError as e:
logging.error(f"生成Pytree时发生错误: {e}")
raise RuntimeError("在3次尝试后仍未能生成一个有效的Pytree。")

View File

@@ -1,67 +0,0 @@
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
import json
from typing import Dict, Any
import logging
from drone_interfaces.action import ExecuteMission
from .websocket_manager import websocket_manager
class MissionActionClient(Node):
"""
Interfaces with the drone's `ExecuteMission` ROS2 Action Server.
"""
def __init__(self):
super().__init__('mission_action_client')
self._action_client = ActionClient(self, ExecuteMission, 'execute_mission')
self.get_logger().info("MissionActionClient initialized.")
def send_goal(self, py_tree: Dict[str, Any]):
"""
Sends the mission (py_tree) to the action server.
"""
if not self._action_client.server_is_ready():
self.get_logger().error("Action server not available, goal not sent.")
# Optionally, you could broadcast a status update to the frontend here
return
self.get_logger().info("Received request to send goal to drone.")
goal_msg = ExecuteMission.Goal()
goal_msg.py_tree_json = json.dumps(py_tree)
self.get_logger().info(f"Sending goal to action server...")
send_goal_future = self._action_client.send_goal_async(
goal_msg,
feedback_callback=self.feedback_callback
)
send_goal_future.add_done_callback(self.goal_response_callback)
def goal_response_callback(self, future):
goal_handle = future.result()
if not goal_handle.accepted:
self.get_logger().info('Goal rejected :(')
return
self.get_logger().info('Goal accepted :)')
self._get_result_future = goal_handle.get_result_async()
self._get_result_future.add_done_callback(self.get_result_callback)
def get_result_callback(self, future):
result = future.result().result
self.get_logger().info(f'Result: {{success: {result.success}, message: {result.message}}}')
# Optionally, you can broadcast the final result via WebSocket here
def feedback_callback(self, feedback_msg):
"""
This callback is triggered by the action server.
It forwards the status to the QGC plugin via the WebSocket manager in a thread-safe manner.
"""
feedback = feedback_msg.feedback
feedback_payload = json.dumps({"node_id": feedback.node_id, "status": feedback.status})
self.get_logger().info(f"Received feedback: {feedback_payload}")
websocket_manager.broadcast(feedback_payload)
# Note: The rclpy.init() and spinning of the node will be handled in main.py

View File

@@ -22,7 +22,8 @@ class ConnectionManager:
def broadcast(self, message: str):
"""
Thread-safely broadcasts a message to all active WebSocket connections.
This method is designed to be called from a different thread (e.g., a ROS2 callback).
This method is designed to be called from a different thread.
(Note: ROS2 callback support has been removed as the project is decoupled from ROS2)
"""
if not self.loop:
logging.error("Event loop not set in ConnectionManager. Cannot broadcast.")

View File

@@ -0,0 +1,248 @@
<#
.Synopsis
Activate a Python virtual environment for the current PowerShell session.
.Description
Pushes the python executable for a virtual environment to the front of the
$Env:PATH environment variable and sets the prompt to signify that you are
in a Python virtual environment. Makes use of the command line switches as
well as the `pyvenv.cfg` file values present in the virtual environment.
.Parameter VenvDir
Path to the directory that contains the virtual environment to activate. The
default value for this is the parent of the directory that the Activate.ps1
script is located within.
.Parameter Prompt
The prompt prefix to display when this virtual environment is activated. By
default, this prompt is the name of the virtual environment folder (VenvDir)
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
.Example
Activate.ps1
Activates the Python virtual environment that contains the Activate.ps1 script.
.Example
Activate.ps1 -Verbose
Activates the Python virtual environment that contains the Activate.ps1 script,
and shows extra information about the activation as it executes.
.Example
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
Activates the Python virtual environment located in the specified location.
.Example
Activate.ps1 -Prompt "MyPython"
Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#>
Param(
[Parameter(Mandatory = $false)]
[String]
$VenvDir,
[Parameter(Mandatory = $false)]
[String]
$Prompt
)
<# Function declarations --------------------------------------------------- #>
<#
.Synopsis
Remove all shell session elements added by the Activate script, including the
addition of the virtual environment's Python executable from the beginning of
the PATH variable.
.Parameter NonDestructive
If present, do not remove this function from the global namespace for the
session.
#>
function global:deactivate ([switch]$NonDestructive) {
# Revert to original values
# The prior prompt:
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
}
# The prior PYTHONHOME:
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
}
# The prior PATH:
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
}
# Just remove the VIRTUAL_ENV altogether:
if (Test-Path -Path Env:VIRTUAL_ENV) {
Remove-Item -Path env:VIRTUAL_ENV
}
# Just remove VIRTUAL_ENV_PROMPT altogether.
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
}
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
}
# Leave deactivate function in the global namespace if requested:
if (-not $NonDestructive) {
Remove-Item -Path function:deactivate
}
}
<#
.Description
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
given folder, and returns them in a map.
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
two strings separated by `=` (with any amount of whitespace surrounding the =)
then it is considered a `key = value` line. The left hand string is the key,
the right hand is the value.
If the value starts with a `'` or a `"` then the first and last character is
stripped from the value before being captured.
.Parameter ConfigDir
Path to the directory that contains the `pyvenv.cfg` file.
#>
function Get-PyVenvConfig(
[String]
$ConfigDir
) {
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
# An empty map will be returned if no config file is found.
$pyvenvConfig = @{ }
if ($pyvenvConfigPath) {
Write-Verbose "File exists, parse `key = value` lines"
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
$pyvenvConfigContent | ForEach-Object {
$keyval = $PSItem -split "\s*=\s*", 2
if ($keyval[0] -and $keyval[1]) {
$val = $keyval[1]
# Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0, 1))) {
$val = $val.Substring(1, $val.Length - 2)
}
$pyvenvConfig[$keyval[0]] = $val
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
}
}
}
return $pyvenvConfig
}
<# Begin Activate script --------------------------------------------------- #>
# Determine the containing directory of this script
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$VenvExecDir = Get-Item -Path $VenvExecPath
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
# Set values required in priority: CmdLine, ConfigFile, Default
# First, get the location of the virtual environment, it might not be
# VenvExecDir if specified on the command line.
if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
}
else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
Write-Verbose "VenvDir=$VenvDir"
}
# Next, read the `pyvenv.cfg` file to determine any required value such
# as `prompt`.
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
# Next, set the prompt from the command line, or the config file, or
# just use the name of the virtual environment folder.
if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'"
}
else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
$Prompt = $pyvenvCfg['prompt'];
}
else {
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
$Prompt = Split-Path -Path $venvDir -Leaf
}
}
Write-Verbose "Prompt = '$Prompt'"
Write-Verbose "VenvDir='$VenvDir'"
# Deactivate any currently active virtual environment, but leave the
# deactivate function in place.
deactivate -nondestructive
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
# that there is an activated venv.
$env:VIRTUAL_ENV = $VenvDir
$env:VIRTUAL_ENV_PROMPT = $Prompt
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
Write-Verbose "Setting prompt to '$Prompt'"
# Set the prompt to include the env name
# Make sure _OLD_VIRTUAL_PROMPT is global
function global:_OLD_VIRTUAL_PROMPT { "" }
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
function global:prompt {
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
_OLD_VIRTUAL_PROMPT
}
}
# Clear PYTHONHOME
if (Test-Path -Path Env:PYTHONHOME) {
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
Remove-Item -Path Env:PYTHONHOME
}
# Add the venv to the PATH
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"

View File

@@ -0,0 +1,76 @@
# This file must be used with "source bin/activate" *from bash*
# You cannot run it directly
deactivate () {
# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# Call hash to forget past locations. Without forgetting
# past locations the $PATH changes we made may not be respected.
# See "man bash" for more details. hash is usually a builtin of your shell
hash -r 2> /dev/null
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
unset VIRTUAL_ENV_PROMPT
if [ ! "${1:-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
# on Windows, a path can contain colons and backslashes and has to be converted:
case "$(uname)" in
CYGWIN*|MSYS*|MINGW*)
# transform D:\path\to\venv to /d/path/to/venv on MSYS and MINGW
# and to /cygdrive/d/path/to/venv on Cygwin
VIRTUAL_ENV=$(cygpath /home/a/DronePlanning/backend_service/venv)
export VIRTUAL_ENV
;;
*)
# use the path as-is
export VIRTUAL_ENV=/home/a/DronePlanning/backend_service/venv
;;
esac
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/"bin":$PATH"
export PATH
VIRTUAL_ENV_PROMPT=venv
export VIRTUAL_ENV_PROMPT
# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1:-}"
PS1="("venv") ${PS1:-}"
export PS1
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null

View File

@@ -0,0 +1,27 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <davidedb@gmail.com>.
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
# Unset irrelevant variables.
deactivate nondestructive
setenv VIRTUAL_ENV /home/a/DronePlanning/backend_service/venv
set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
setenv VIRTUAL_ENV_PROMPT venv
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
set prompt = "("venv") $prompt:q"
endif
alias pydoc python -m pydoc
rehash

View File

@@ -0,0 +1,69 @@
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
# (https://fishshell.com/). You cannot run it directly.
function deactivate -d "Exit virtual environment and return to normal shell environment"
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
set -e _OLD_VIRTUAL_PYTHONHOME
end
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
set -e _OLD_FISH_PROMPT_OVERRIDE
# prevents error when using nested fish instances (Issue #93858)
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
end
set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV /home/a/DronePlanning/backend_service/venv
set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
set -gx VIRTUAL_ENV_PROMPT venv
# Unset PYTHONHOME if set.
if set -q PYTHONHOME
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
set -e PYTHONHOME
end
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# fish uses a function instead of an env var to generate the prompt.
# Save the current fish_prompt function as the function _old_fish_prompt.
functions -c fish_prompt _old_fish_prompt
# With the original prompt function renamed, we can override with our own.
function fish_prompt
# Save the return status of the last command.
set -l old_status $status
# Output the venv prompt; color taken from the blue of the Python logo.
printf "%s(%s)%s " (set_color 4B8BBE) venv (set_color normal)
# Restore the return status of the previous command.
echo "exit $old_status" | .
# Output the original/"old" prompt.
_old_fish_prompt
end
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
end

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
python3

View File

@@ -0,0 +1 @@
/home/a/miniconda3/bin/python3

View File

@@ -0,0 +1 @@
python3

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
# This is a stub package designed to roughly emulate the _yaml
# extension module, which previously existed as a standalone module
# and has been moved into the `yaml` package namespace.
# It does not perfectly mimic its old counterpart, but should get
# close enough for anyone who's relying on it even when they shouldn't.
import yaml
# in some circumstances, the yaml module we imoprted may be from a different version, so we need
# to tread carefully when poking at it here (it may not have the attributes we expect)
if not getattr(yaml, '__with_libyaml__', False):
from sys import version_info
exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError
raise exc("No module named '_yaml'")
else:
from yaml._yaml import *
import warnings
warnings.warn(
'The _yaml extension module is now located at yaml._yaml'
' and its location is subject to change. To use the'
' LibYAML-based parser and emitter, import from `yaml`:'
' `from yaml import CLoader as Loader, CDumper as Dumper`.',
DeprecationWarning
)
del warnings
# Don't `del yaml` here because yaml is actually an existing
# namespace member of _yaml.
__name__ = '_yaml'
# If the module is top-level (i.e. not a part of any specific package)
# then the attribute should be set to ''.
# https://docs.python.org/3.8/library/types.html
__package__ = ''

View File

@@ -0,0 +1,145 @@
Metadata-Version: 2.4
Name: annotated-doc
Version: 0.0.4
Summary: Document parameters, class attributes, return types, and variables inline, with Annotated.
Author-Email: =?utf-8?q?Sebasti=C3=A1n_Ram=C3=ADrez?= <tiangolo@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python
Classifier: Topic :: Internet
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development
Classifier: Typing :: Typed
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
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
Project-URL: Homepage, https://github.com/fastapi/annotated-doc
Project-URL: Documentation, https://github.com/fastapi/annotated-doc
Project-URL: Repository, https://github.com/fastapi/annotated-doc
Project-URL: Issues, https://github.com/fastapi/annotated-doc/issues
Project-URL: Changelog, https://github.com/fastapi/annotated-doc/release-notes.md
Requires-Python: >=3.8
Description-Content-Type: text/markdown
# Annotated Doc
Document parameters, class attributes, return types, and variables inline, with `Annotated`.
<a href="https://github.com/fastapi/annotated-doc/actions?query=workflow%3ATest+event%3Apush+branch%3Amain" target="_blank">
<img src="https://github.com/fastapi/annotated-doc/actions/workflows/test.yml/badge.svg?event=push&branch=main" alt="Test">
</a>
<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/fastapi/annotated-doc" target="_blank">
<img src="https://coverage-badge.samuelcolvin.workers.dev/fastapi/annotated-doc.svg" alt="Coverage">
</a>
<a href="https://pypi.org/project/annotated-doc" target="_blank">
<img src="https://img.shields.io/pypi/v/annotated-doc?color=%2334D058&label=pypi%20package" alt="Package version">
</a>
<a href="https://pypi.org/project/annotated-doc" target="_blank">
<img src="https://img.shields.io/pypi/pyversions/annotated-doc.svg?color=%2334D058" alt="Supported Python versions">
</a>
## Installation
```bash
pip install annotated-doc
```
Or with `uv`:
```Python
uv add annotated-doc
```
## Usage
Import `Doc` and pass a single literal string with the documentation for the specific parameter, class attribute, return type, or variable.
For example, to document a parameter `name` in a function `hi` you could do:
```Python
from typing import Annotated
from annotated_doc import Doc
def hi(name: Annotated[str, Doc("Who to say hi to")]) -> None:
print(f"Hi, {name}!")
```
You can also use it to document class attributes:
```Python
from typing import Annotated
from annotated_doc import Doc
class User:
name: Annotated[str, Doc("The user's name")]
age: Annotated[int, Doc("The user's age")]
```
The same way, you could document return types and variables, or anything that could have a type annotation with `Annotated`.
## Who Uses This
`annotated-doc` was made for:
* [FastAPI](https://fastapi.tiangolo.com/)
* [Typer](https://typer.tiangolo.com/)
* [SQLModel](https://sqlmodel.tiangolo.com/)
* [Asyncer](https://asyncer.tiangolo.com/)
`annotated-doc` is supported by [griffe-typingdoc](https://github.com/mkdocstrings/griffe-typingdoc), which powers reference documentation like the one in the [FastAPI Reference](https://fastapi.tiangolo.com/reference/).
## Reasons not to use `annotated-doc`
You are already comfortable with one of the existing docstring formats, like:
* Sphinx
* numpydoc
* Google
* Keras
Your team is already comfortable using them.
You prefer having the documentation about parameters all together in a docstring, separated from the code defining them.
You care about a specific set of users, using one specific editor, and that editor already has support for the specific docstring format you use.
## Reasons to use `annotated-doc`
* No micro-syntax to learn for newcomers, its **just Python** syntax.
* **Editing** would be already fully supported by default by any editor (current or future) supporting Python syntax, including syntax errors, syntax highlighting, etc.
* **Rendering** would be relatively straightforward to implement by static tools (tools that don't need runtime execution), as the information can be extracted from the AST they normally already create.
* **Deduplication of information**: the name of a parameter would be defined in a single place, not duplicated inside of a docstring.
* **Elimination** of the possibility of having **inconsistencies** when removing a parameter or class variable and **forgetting to remove** its documentation.
* **Minimization** of the probability of adding a new parameter or class variable and **forgetting to add its documentation**.
* **Elimination** of the possibility of having **inconsistencies** between the **name** of a parameter in the **signature** and the name in the docstring when it is renamed.
* **Access** to the documentation string for each symbol at **runtime**, including existing (older) Python versions.
* A more formalized way to document other symbols, like type aliases, that could use Annotated.
* **Support** for apps using FastAPI, Typer and others.
* **AI Accessibility**: AI tools will have an easier way understanding each parameter as the distance from documentation to parameter is much closer.
## History
I ([@tiangolo](https://github.com/tiangolo)) originally wanted for this to be part of the Python standard library (in [PEP 727](https://peps.python.org/pep-0727/)), but the proposal was withdrawn as there was a fair amount of negative feedback and opposition.
The conclusion was that this was better done as an external effort, in a third-party library.
So, here it is, with a simpler approach, as a third-party library, in a way that can be used by others, starting with FastAPI and friends.
## License
This project is licensed under the terms of the MIT license.

View File

@@ -0,0 +1,11 @@
annotated_doc-0.0.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
annotated_doc-0.0.4.dist-info/METADATA,sha256=Irm5KJua33dY2qKKAjJ-OhKaVBVIfwFGej_dSe3Z1TU,6566
annotated_doc-0.0.4.dist-info/RECORD,,
annotated_doc-0.0.4.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
annotated_doc-0.0.4.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
annotated_doc-0.0.4.dist-info/licenses/LICENSE,sha256=__Fwd5pqy_ZavbQFwIfxzuF4ZpHkqWpANFF-SlBKDN8,1086
annotated_doc/__init__.py,sha256=VuyxxUe80kfEyWnOrCx_Bk8hybo3aKo6RYBlkBBYW8k,52
annotated_doc/__pycache__/__init__.cpython-313.pyc,,
annotated_doc/__pycache__/main.cpython-313.pyc,,
annotated_doc/main.py,sha256=5Zfvxv80SwwLqpRW73AZyZyiM4bWma9QWRbp_cgD20s,1075
annotated_doc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0

View File

@@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: pdm-backend (2.4.5)
Root-Is-Purelib: true
Tag: py3-none-any

View File

@@ -0,0 +1,4 @@
[console_scripts]
[gui_scripts]

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2025 Sebastián Ramírez
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.

View File

@@ -0,0 +1,3 @@
from .main import Doc as Doc
__version__ = "0.0.4"

View File

@@ -0,0 +1,36 @@
class Doc:
"""Define the documentation of a type annotation using `Annotated`, to be
used in class attributes, function and method parameters, return values,
and variables.
The value should be a positional-only string literal to allow static tools
like editors and documentation generators to use it.
This complements docstrings.
The string value passed is available in the attribute `documentation`.
Example:
```Python
from typing import Annotated
from annotated_doc import Doc
def hi(name: Annotated[str, Doc("Who to say hi to")]) -> None:
print(f"Hi, {name}!")
```
"""
def __init__(self, documentation: str, /) -> None:
self.documentation = documentation
def __repr__(self) -> str:
return f"Doc({self.documentation!r})"
def __hash__(self) -> int:
return hash(self.documentation)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Doc):
return NotImplemented
return self.documentation == other.documentation

View File

@@ -0,0 +1,295 @@
Metadata-Version: 2.3
Name: annotated-types
Version: 0.7.0
Summary: Reusable constraint types to use with typing.Annotated
Project-URL: Homepage, https://github.com/annotated-types/annotated-types
Project-URL: Source, https://github.com/annotated-types/annotated-types
Project-URL: Changelog, https://github.com/annotated-types/annotated-types/releases
Author-email: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Samuel Colvin <s@muelcolvin.com>, Zac Hatfield-Dodds <zac@zhd.dev>
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Environment :: MacOS X
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: Unix
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
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: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.8
Requires-Dist: typing-extensions>=4.0.0; python_version < '3.9'
Description-Content-Type: text/markdown
# annotated-types
[![CI](https://github.com/annotated-types/annotated-types/workflows/CI/badge.svg?event=push)](https://github.com/annotated-types/annotated-types/actions?query=event%3Apush+branch%3Amain+workflow%3ACI)
[![pypi](https://img.shields.io/pypi/v/annotated-types.svg)](https://pypi.python.org/pypi/annotated-types)
[![versions](https://img.shields.io/pypi/pyversions/annotated-types.svg)](https://github.com/annotated-types/annotated-types)
[![license](https://img.shields.io/github/license/annotated-types/annotated-types.svg)](https://github.com/annotated-types/annotated-types/blob/main/LICENSE)
[PEP-593](https://peps.python.org/pep-0593/) added `typing.Annotated` as a way of
adding context-specific metadata to existing types, and specifies that
`Annotated[T, x]` _should_ be treated as `T` by any tool or library without special
logic for `x`.
This package provides metadata objects which can be used to represent common
constraints such as upper and lower bounds on scalar values and collection sizes,
a `Predicate` marker for runtime checks, and
descriptions of how we intend these metadata to be interpreted. In some cases,
we also note alternative representations which do not require this package.
## Install
```bash
pip install annotated-types
```
## Examples
```python
from typing import Annotated
from annotated_types import Gt, Len, Predicate
class MyClass:
age: Annotated[int, Gt(18)] # Valid: 19, 20, ...
# Invalid: 17, 18, "19", 19.0, ...
factors: list[Annotated[int, Predicate(is_prime)]] # Valid: 2, 3, 5, 7, 11, ...
# Invalid: 4, 8, -2, 5.0, "prime", ...
my_list: Annotated[list[int], Len(0, 10)] # Valid: [], [10, 20, 30, 40, 50]
# Invalid: (1, 2), ["abc"], [0] * 20
```
## Documentation
_While `annotated-types` avoids runtime checks for performance, users should not
construct invalid combinations such as `MultipleOf("non-numeric")` or `Annotated[int, Len(3)]`.
Downstream implementors may choose to raise an error, emit a warning, silently ignore
a metadata item, etc., if the metadata objects described below are used with an
incompatible type - or for any other reason!_
### Gt, Ge, Lt, Le
Express inclusive and/or exclusive bounds on orderable values - which may be numbers,
dates, times, strings, sets, etc. Note that the boundary value need not be of the
same type that was annotated, so long as they can be compared: `Annotated[int, Gt(1.5)]`
is fine, for example, and implies that the value is an integer x such that `x > 1.5`.
We suggest that implementors may also interpret `functools.partial(operator.le, 1.5)`
as being equivalent to `Gt(1.5)`, for users who wish to avoid a runtime dependency on
the `annotated-types` package.
To be explicit, these types have the following meanings:
* `Gt(x)` - value must be "Greater Than" `x` - equivalent to exclusive minimum
* `Ge(x)` - value must be "Greater than or Equal" to `x` - equivalent to inclusive minimum
* `Lt(x)` - value must be "Less Than" `x` - equivalent to exclusive maximum
* `Le(x)` - value must be "Less than or Equal" to `x` - equivalent to inclusive maximum
### Interval
`Interval(gt, ge, lt, le)` allows you to specify an upper and lower bound with a single
metadata object. `None` attributes should be ignored, and non-`None` attributes
treated as per the single bounds above.
### MultipleOf
`MultipleOf(multiple_of=x)` might be interpreted in two ways:
1. Python semantics, implying `value % multiple_of == 0`, or
2. [JSONschema semantics](https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.2.1),
where `int(value / multiple_of) == value / multiple_of`.
We encourage users to be aware of these two common interpretations and their
distinct behaviours, especially since very large or non-integer numbers make
it easy to cause silent data corruption due to floating-point imprecision.
We encourage libraries to carefully document which interpretation they implement.
### MinLen, MaxLen, Len
`Len()` implies that `min_length <= len(value) <= max_length` - lower and upper bounds are inclusive.
As well as `Len()` which can optionally include upper and lower bounds, we also
provide `MinLen(x)` and `MaxLen(y)` which are equivalent to `Len(min_length=x)`
and `Len(max_length=y)` respectively.
`Len`, `MinLen`, and `MaxLen` may be used with any type which supports `len(value)`.
Examples of usage:
* `Annotated[list, MaxLen(10)]` (or `Annotated[list, Len(max_length=10))`) - list must have a length of 10 or less
* `Annotated[str, MaxLen(10)]` - string must have a length of 10 or less
* `Annotated[list, MinLen(3))` (or `Annotated[list, Len(min_length=3))`) - list must have a length of 3 or more
* `Annotated[list, Len(4, 6)]` - list must have a length of 4, 5, or 6
* `Annotated[list, Len(8, 8)]` - list must have a length of exactly 8
#### Changed in v0.4.0
* `min_inclusive` has been renamed to `min_length`, no change in meaning
* `max_exclusive` has been renamed to `max_length`, upper bound is now **inclusive** instead of **exclusive**
* The recommendation that slices are interpreted as `Len` has been removed due to ambiguity and different semantic
meaning of the upper bound in slices vs. `Len`
See [issue #23](https://github.com/annotated-types/annotated-types/issues/23) for discussion.
### Timezone
`Timezone` can be used with a `datetime` or a `time` to express which timezones
are allowed. `Annotated[datetime, Timezone(None)]` must be a naive datetime.
`Timezone[...]` ([literal ellipsis](https://docs.python.org/3/library/constants.html#Ellipsis))
expresses that any timezone-aware datetime is allowed. You may also pass a specific
timezone string or [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects)
object such as `Timezone(timezone.utc)` or `Timezone("Africa/Abidjan")` to express that you only
allow a specific timezone, though we note that this is often a symptom of fragile design.
#### Changed in v0.x.x
* `Timezone` accepts [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects) objects instead of
`timezone`, extending compatibility to [`zoneinfo`](https://docs.python.org/3/library/zoneinfo.html) and third party libraries.
### Unit
`Unit(unit: str)` expresses that the annotated numeric value is the magnitude of
a quantity with the specified unit. For example, `Annotated[float, Unit("m/s")]`
would be a float representing a velocity in meters per second.
Please note that `annotated_types` itself makes no attempt to parse or validate
the unit string in any way. That is left entirely to downstream libraries,
such as [`pint`](https://pint.readthedocs.io) or
[`astropy.units`](https://docs.astropy.org/en/stable/units/).
An example of how a library might use this metadata:
```python
from annotated_types import Unit
from typing import Annotated, TypeVar, Callable, Any, get_origin, get_args
# given a type annotated with a unit:
Meters = Annotated[float, Unit("m")]
# you can cast the annotation to a specific unit type with any
# callable that accepts a string and returns the desired type
T = TypeVar("T")
def cast_unit(tp: Any, unit_cls: Callable[[str], T]) -> T | None:
if get_origin(tp) is Annotated:
for arg in get_args(tp):
if isinstance(arg, Unit):
return unit_cls(arg.unit)
return None
# using `pint`
import pint
pint_unit = cast_unit(Meters, pint.Unit)
# using `astropy.units`
import astropy.units as u
astropy_unit = cast_unit(Meters, u.Unit)
```
### Predicate
`Predicate(func: Callable)` expresses that `func(value)` is truthy for valid values.
Users should prefer the statically inspectable metadata above, but if you need
the full power and flexibility of arbitrary runtime predicates... here it is.
For some common constraints, we provide generic types:
* `IsLower = Annotated[T, Predicate(str.islower)]`
* `IsUpper = Annotated[T, Predicate(str.isupper)]`
* `IsDigit = Annotated[T, Predicate(str.isdigit)]`
* `IsFinite = Annotated[T, Predicate(math.isfinite)]`
* `IsNotFinite = Annotated[T, Predicate(Not(math.isfinite))]`
* `IsNan = Annotated[T, Predicate(math.isnan)]`
* `IsNotNan = Annotated[T, Predicate(Not(math.isnan))]`
* `IsInfinite = Annotated[T, Predicate(math.isinf)]`
* `IsNotInfinite = Annotated[T, Predicate(Not(math.isinf))]`
so that you can write e.g. `x: IsFinite[float] = 2.0` instead of the longer
(but exactly equivalent) `x: Annotated[float, Predicate(math.isfinite)] = 2.0`.
Some libraries might have special logic to handle known or understandable predicates,
for example by checking for `str.isdigit` and using its presence to both call custom
logic to enforce digit-only strings, and customise some generated external schema.
Users are therefore encouraged to avoid indirection like `lambda s: s.lower()`, in
favor of introspectable methods such as `str.lower` or `re.compile("pattern").search`.
To enable basic negation of commonly used predicates like `math.isnan` without introducing introspection that makes it impossible for implementers to introspect the predicate we provide a `Not` wrapper that simply negates the predicate in an introspectable manner. Several of the predicates listed above are created in this manner.
We do not specify what behaviour should be expected for predicates that raise
an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
skip invalid constraints, or statically raise an error; or it might try calling it
and then propagate or discard the resulting
`TypeError: descriptor 'isdigit' for 'str' objects doesn't apply to a 'int' object`
exception. We encourage libraries to document the behaviour they choose.
### Doc
`doc()` can be used to add documentation information in `Annotated`, for function and method parameters, variables, class attributes, return types, and any place where `Annotated` can be used.
It expects a value that can be statically analyzed, as the main use case is for static analysis, editors, documentation generators, and similar tools.
It returns a `DocInfo` class with a single attribute `documentation` containing the value passed to `doc()`.
This is the early adopter's alternative form of the [`typing-doc` proposal](https://github.com/tiangolo/fastapi/blob/typing-doc/typing_doc.md).
### Integrating downstream types with `GroupedMetadata`
Implementers may choose to provide a convenience wrapper that groups multiple pieces of metadata.
This can help reduce verbosity and cognitive overhead for users.
For example, an implementer like Pydantic might provide a `Field` or `Meta` type that accepts keyword arguments and transforms these into low-level metadata:
```python
from dataclasses import dataclass
from typing import Iterator
from annotated_types import GroupedMetadata, Ge
@dataclass
class Field(GroupedMetadata):
ge: int | None = None
description: str | None = None
def __iter__(self) -> Iterator[object]:
# Iterating over a GroupedMetadata object should yield annotated-types
# constraint metadata objects which describe it as fully as possible,
# and may include other unknown objects too.
if self.ge is not None:
yield Ge(self.ge)
if self.description is not None:
yield Description(self.description)
```
Libraries consuming annotated-types constraints should check for `GroupedMetadata` and unpack it by iterating over the object and treating the results as if they had been "unpacked" in the `Annotated` type. The same logic should be applied to the [PEP 646 `Unpack` type](https://peps.python.org/pep-0646/), so that `Annotated[T, Field(...)]`, `Annotated[T, Unpack[Field(...)]]` and `Annotated[T, *Field(...)]` are all treated consistently.
Libraries consuming annotated-types should also ignore any metadata they do not recongize that came from unpacking a `GroupedMetadata`, just like they ignore unrecognized metadata in `Annotated` itself.
Our own `annotated_types.Interval` class is a `GroupedMetadata` which unpacks itself into `Gt`, `Lt`, etc., so this is not an abstract concern. Similarly, `annotated_types.Len` is a `GroupedMetadata` which unpacks itself into `MinLen` (optionally) and `MaxLen`.
### Consuming metadata
We intend to not be prescriptive as to _how_ the metadata and constraints are used, but as an example of how one might parse constraints from types annotations see our [implementation in `test_main.py`](https://github.com/annotated-types/annotated-types/blob/f59cf6d1b5255a0fe359b93896759a180bec30ae/tests/test_main.py#L94-L103).
It is up to the implementer to determine how this metadata is used.
You could use the metadata for runtime type checking, for generating schemas or to generate example data, amongst other use cases.
## Design & History
This package was designed at the PyCon 2022 sprints by the maintainers of Pydantic
and Hypothesis, with the goal of making it as easy as possible for end-users to
provide more informative annotations for use by runtime libraries.
It is deliberately minimal, and following PEP-593 allows considerable downstream
discretion in what (if anything!) they choose to support. Nonetheless, we expect
that staying simple and covering _only_ the most common use-cases will give users
and maintainers the best experience we can. If you'd like more constraints for your
types - follow our lead, by defining them and documenting them downstream!

View File

@@ -0,0 +1,10 @@
annotated_types-0.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
annotated_types-0.7.0.dist-info/METADATA,sha256=7ltqxksJJ0wCYFGBNIQCWTlWQGeAH0hRFdnK3CB895E,15046
annotated_types-0.7.0.dist-info/RECORD,,
annotated_types-0.7.0.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
annotated_types-0.7.0.dist-info/licenses/LICENSE,sha256=_hBJiEsaDZNCkB6I4H8ykl0ksxIdmXK2poBfuYJLCV0,1083
annotated_types/__init__.py,sha256=RynLsRKUEGI0KimXydlD1fZEfEzWwDo0Uon3zOKhG1Q,13819
annotated_types/__pycache__/__init__.cpython-313.pyc,,
annotated_types/__pycache__/test_cases.cpython-313.pyc,,
annotated_types/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
annotated_types/test_cases.py,sha256=zHFX6EpcMbGJ8FzBYDbO56bPwx_DYIVSKbZM-4B3_lg,6421

View File

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

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2022 the contributors
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.

View File

@@ -0,0 +1,432 @@
import math
import sys
import types
from dataclasses import dataclass
from datetime import tzinfo
from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, SupportsFloat, SupportsIndex, TypeVar, Union
if sys.version_info < (3, 8):
from typing_extensions import Protocol, runtime_checkable
else:
from typing import Protocol, runtime_checkable
if sys.version_info < (3, 9):
from typing_extensions import Annotated, Literal
else:
from typing import Annotated, Literal
if sys.version_info < (3, 10):
EllipsisType = type(Ellipsis)
KW_ONLY = {}
SLOTS = {}
else:
from types import EllipsisType
KW_ONLY = {"kw_only": True}
SLOTS = {"slots": True}
__all__ = (
'BaseMetadata',
'GroupedMetadata',
'Gt',
'Ge',
'Lt',
'Le',
'Interval',
'MultipleOf',
'MinLen',
'MaxLen',
'Len',
'Timezone',
'Predicate',
'LowerCase',
'UpperCase',
'IsDigits',
'IsFinite',
'IsNotFinite',
'IsNan',
'IsNotNan',
'IsInfinite',
'IsNotInfinite',
'doc',
'DocInfo',
'__version__',
)
__version__ = '0.7.0'
T = TypeVar('T')
# arguments that start with __ are considered
# positional only
# see https://peps.python.org/pep-0484/#positional-only-arguments
class SupportsGt(Protocol):
def __gt__(self: T, __other: T) -> bool:
...
class SupportsGe(Protocol):
def __ge__(self: T, __other: T) -> bool:
...
class SupportsLt(Protocol):
def __lt__(self: T, __other: T) -> bool:
...
class SupportsLe(Protocol):
def __le__(self: T, __other: T) -> bool:
...
class SupportsMod(Protocol):
def __mod__(self: T, __other: T) -> T:
...
class SupportsDiv(Protocol):
def __div__(self: T, __other: T) -> T:
...
class BaseMetadata:
"""Base class for all metadata.
This exists mainly so that implementers
can do `isinstance(..., BaseMetadata)` while traversing field annotations.
"""
__slots__ = ()
@dataclass(frozen=True, **SLOTS)
class Gt(BaseMetadata):
"""Gt(gt=x) implies that the value must be greater than x.
It can be used with any type that supports the ``>`` operator,
including numbers, dates and times, strings, sets, and so on.
"""
gt: SupportsGt
@dataclass(frozen=True, **SLOTS)
class Ge(BaseMetadata):
"""Ge(ge=x) implies that the value must be greater than or equal to x.
It can be used with any type that supports the ``>=`` operator,
including numbers, dates and times, strings, sets, and so on.
"""
ge: SupportsGe
@dataclass(frozen=True, **SLOTS)
class Lt(BaseMetadata):
"""Lt(lt=x) implies that the value must be less than x.
It can be used with any type that supports the ``<`` operator,
including numbers, dates and times, strings, sets, and so on.
"""
lt: SupportsLt
@dataclass(frozen=True, **SLOTS)
class Le(BaseMetadata):
"""Le(le=x) implies that the value must be less than or equal to x.
It can be used with any type that supports the ``<=`` operator,
including numbers, dates and times, strings, sets, and so on.
"""
le: SupportsLe
@runtime_checkable
class GroupedMetadata(Protocol):
"""A grouping of multiple objects, like typing.Unpack.
`GroupedMetadata` on its own is not metadata and has no meaning.
All of the constraints and metadata should be fully expressable
in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`.
Concrete implementations should override `GroupedMetadata.__iter__()`
to add their own metadata.
For example:
>>> @dataclass
>>> class Field(GroupedMetadata):
>>> gt: float | None = None
>>> description: str | None = None
...
>>> def __iter__(self) -> Iterable[object]:
>>> if self.gt is not None:
>>> yield Gt(self.gt)
>>> if self.description is not None:
>>> yield Description(self.gt)
Also see the implementation of `Interval` below for an example.
Parsers should recognize this and unpack it so that it can be used
both with and without unpacking:
- `Annotated[int, Field(...)]` (parser must unpack Field)
- `Annotated[int, *Field(...)]` (PEP-646)
""" # noqa: trailing-whitespace
@property
def __is_annotated_types_grouped_metadata__(self) -> Literal[True]:
return True
def __iter__(self) -> Iterator[object]:
...
if not TYPE_CHECKING:
__slots__ = () # allow subclasses to use slots
def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:
# Basic ABC like functionality without the complexity of an ABC
super().__init_subclass__(*args, **kwargs)
if cls.__iter__ is GroupedMetadata.__iter__:
raise TypeError("Can't subclass GroupedMetadata without implementing __iter__")
def __iter__(self) -> Iterator[object]: # noqa: F811
raise NotImplementedError # more helpful than "None has no attribute..." type errors
@dataclass(frozen=True, **KW_ONLY, **SLOTS)
class Interval(GroupedMetadata):
"""Interval can express inclusive or exclusive bounds with a single object.
It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which
are interpreted the same way as the single-bound constraints.
"""
gt: Union[SupportsGt, None] = None
ge: Union[SupportsGe, None] = None
lt: Union[SupportsLt, None] = None
le: Union[SupportsLe, None] = None
def __iter__(self) -> Iterator[BaseMetadata]:
"""Unpack an Interval into zero or more single-bounds."""
if self.gt is not None:
yield Gt(self.gt)
if self.ge is not None:
yield Ge(self.ge)
if self.lt is not None:
yield Lt(self.lt)
if self.le is not None:
yield Le(self.le)
@dataclass(frozen=True, **SLOTS)
class MultipleOf(BaseMetadata):
"""MultipleOf(multiple_of=x) might be interpreted in two ways:
1. Python semantics, implying ``value % multiple_of == 0``, or
2. JSONschema semantics, where ``int(value / multiple_of) == value / multiple_of``
We encourage users to be aware of these two common interpretations,
and libraries to carefully document which they implement.
"""
multiple_of: Union[SupportsDiv, SupportsMod]
@dataclass(frozen=True, **SLOTS)
class MinLen(BaseMetadata):
"""
MinLen() implies minimum inclusive length,
e.g. ``len(value) >= min_length``.
"""
min_length: Annotated[int, Ge(0)]
@dataclass(frozen=True, **SLOTS)
class MaxLen(BaseMetadata):
"""
MaxLen() implies maximum inclusive length,
e.g. ``len(value) <= max_length``.
"""
max_length: Annotated[int, Ge(0)]
@dataclass(frozen=True, **SLOTS)
class Len(GroupedMetadata):
"""
Len() implies that ``min_length <= len(value) <= max_length``.
Upper bound may be omitted or ``None`` to indicate no upper length bound.
"""
min_length: Annotated[int, Ge(0)] = 0
max_length: Optional[Annotated[int, Ge(0)]] = None
def __iter__(self) -> Iterator[BaseMetadata]:
"""Unpack a Len into zone or more single-bounds."""
if self.min_length > 0:
yield MinLen(self.min_length)
if self.max_length is not None:
yield MaxLen(self.max_length)
@dataclass(frozen=True, **SLOTS)
class Timezone(BaseMetadata):
"""Timezone(tz=...) requires a datetime to be aware (or ``tz=None``, naive).
``Annotated[datetime, Timezone(None)]`` must be a naive datetime.
``Timezone[...]`` (the ellipsis literal) expresses that the datetime must be
tz-aware but any timezone is allowed.
You may also pass a specific timezone string or tzinfo object such as
``Timezone(timezone.utc)`` or ``Timezone("Africa/Abidjan")`` to express that
you only allow a specific timezone, though we note that this is often
a symptom of poor design.
"""
tz: Union[str, tzinfo, EllipsisType, None]
@dataclass(frozen=True, **SLOTS)
class Unit(BaseMetadata):
"""Indicates that the value is a physical quantity with the specified unit.
It is intended for usage with numeric types, where the value represents the
magnitude of the quantity. For example, ``distance: Annotated[float, Unit('m')]``
or ``speed: Annotated[float, Unit('m/s')]``.
Interpretation of the unit string is left to the discretion of the consumer.
It is suggested to follow conventions established by python libraries that work
with physical quantities, such as
- ``pint`` : <https://pint.readthedocs.io/en/stable/>
- ``astropy.units``: <https://docs.astropy.org/en/stable/units/>
For indicating a quantity with a certain dimensionality but without a specific unit
it is recommended to use square brackets, e.g. `Annotated[float, Unit('[time]')]`.
Note, however, ``annotated_types`` itself makes no use of the unit string.
"""
unit: str
@dataclass(frozen=True, **SLOTS)
class Predicate(BaseMetadata):
"""``Predicate(func: Callable)`` implies `func(value)` is truthy for valid values.
Users should prefer statically inspectable metadata, but if you need the full
power and flexibility of arbitrary runtime predicates... here it is.
We provide a few predefined predicates for common string constraints:
``IsLower = Predicate(str.islower)``, ``IsUpper = Predicate(str.isupper)``, and
``IsDigits = Predicate(str.isdigit)``. Users are encouraged to use methods which
can be given special handling, and avoid indirection like ``lambda s: s.lower()``.
Some libraries might have special logic to handle certain predicates, e.g. by
checking for `str.isdigit` and using its presence to both call custom logic to
enforce digit-only strings, and customise some generated external schema.
We do not specify what behaviour should be expected for predicates that raise
an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
skip invalid constraints, or statically raise an error; or it might try calling it
and then propagate or discard the resulting exception.
"""
func: Callable[[Any], bool]
def __repr__(self) -> str:
if getattr(self.func, "__name__", "<lambda>") == "<lambda>":
return f"{self.__class__.__name__}({self.func!r})"
if isinstance(self.func, (types.MethodType, types.BuiltinMethodType)) and (
namespace := getattr(self.func.__self__, "__name__", None)
):
return f"{self.__class__.__name__}({namespace}.{self.func.__name__})"
if isinstance(self.func, type(str.isascii)): # method descriptor
return f"{self.__class__.__name__}({self.func.__qualname__})"
return f"{self.__class__.__name__}({self.func.__name__})"
@dataclass
class Not:
func: Callable[[Any], bool]
def __call__(self, __v: Any) -> bool:
return not self.func(__v)
_StrType = TypeVar("_StrType", bound=str)
LowerCase = Annotated[_StrType, Predicate(str.islower)]
"""
Return True if the string is a lowercase string, False otherwise.
A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string.
""" # noqa: E501
UpperCase = Annotated[_StrType, Predicate(str.isupper)]
"""
Return True if the string is an uppercase string, False otherwise.
A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string.
""" # noqa: E501
IsDigit = Annotated[_StrType, Predicate(str.isdigit)]
IsDigits = IsDigit # type: ignore # plural for backwards compatibility, see #63
"""
Return True if the string is a digit string, False otherwise.
A string is a digit string if all characters in the string are digits and there is at least one character in the string.
""" # noqa: E501
IsAscii = Annotated[_StrType, Predicate(str.isascii)]
"""
Return True if all characters in the string are ASCII, False otherwise.
ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too.
"""
_NumericType = TypeVar('_NumericType', bound=Union[SupportsFloat, SupportsIndex])
IsFinite = Annotated[_NumericType, Predicate(math.isfinite)]
"""Return True if x is neither an infinity nor a NaN, and False otherwise."""
IsNotFinite = Annotated[_NumericType, Predicate(Not(math.isfinite))]
"""Return True if x is one of infinity or NaN, and False otherwise"""
IsNan = Annotated[_NumericType, Predicate(math.isnan)]
"""Return True if x is a NaN (not a number), and False otherwise."""
IsNotNan = Annotated[_NumericType, Predicate(Not(math.isnan))]
"""Return True if x is anything but NaN (not a number), and False otherwise."""
IsInfinite = Annotated[_NumericType, Predicate(math.isinf)]
"""Return True if x is a positive or negative infinity, and False otherwise."""
IsNotInfinite = Annotated[_NumericType, Predicate(Not(math.isinf))]
"""Return True if x is neither a positive or negative infinity, and False otherwise."""
try:
from typing_extensions import DocInfo, doc # type: ignore [attr-defined]
except ImportError:
@dataclass(frozen=True, **SLOTS)
class DocInfo: # type: ignore [no-redef]
""" "
The return value of doc(), mainly to be used by tools that want to extract the
Annotated documentation at runtime.
"""
documentation: str
"""The documentation string passed to doc()."""
def doc(
documentation: str,
) -> DocInfo:
"""
Add documentation to a type annotation inside of Annotated.
For example:
>>> def hi(name: Annotated[int, doc("The name of the user")]) -> None: ...
"""
return DocInfo(documentation)

View File

@@ -0,0 +1,151 @@
import math
import sys
from datetime import date, datetime, timedelta, timezone
from decimal import Decimal
from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Set, Tuple
if sys.version_info < (3, 9):
from typing_extensions import Annotated
else:
from typing import Annotated
import annotated_types as at
class Case(NamedTuple):
"""
A test case for `annotated_types`.
"""
annotation: Any
valid_cases: Iterable[Any]
invalid_cases: Iterable[Any]
def cases() -> Iterable[Case]:
# Gt, Ge, Lt, Le
yield Case(Annotated[int, at.Gt(4)], (5, 6, 1000), (4, 0, -1))
yield Case(Annotated[float, at.Gt(0.5)], (0.6, 0.7, 0.8, 0.9), (0.5, 0.0, -0.1))
yield Case(
Annotated[datetime, at.Gt(datetime(2000, 1, 1))],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
[datetime(2000, 1, 1), datetime(1999, 12, 31)],
)
yield Case(
Annotated[datetime, at.Gt(date(2000, 1, 1))],
[date(2000, 1, 2), date(2000, 1, 3)],
[date(2000, 1, 1), date(1999, 12, 31)],
)
yield Case(
Annotated[datetime, at.Gt(Decimal('1.123'))],
[Decimal('1.1231'), Decimal('123')],
[Decimal('1.123'), Decimal('0')],
)
yield Case(Annotated[int, at.Ge(4)], (4, 5, 6, 1000, 4), (0, -1))
yield Case(Annotated[float, at.Ge(0.5)], (0.5, 0.6, 0.7, 0.8, 0.9), (0.4, 0.0, -0.1))
yield Case(
Annotated[datetime, at.Ge(datetime(2000, 1, 1))],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
[datetime(1998, 1, 1), datetime(1999, 12, 31)],
)
yield Case(Annotated[int, at.Lt(4)], (0, -1), (4, 5, 6, 1000, 4))
yield Case(Annotated[float, at.Lt(0.5)], (0.4, 0.0, -0.1), (0.5, 0.6, 0.7, 0.8, 0.9))
yield Case(
Annotated[datetime, at.Lt(datetime(2000, 1, 1))],
[datetime(1999, 12, 31), datetime(1999, 12, 31)],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
)
yield Case(Annotated[int, at.Le(4)], (4, 0, -1), (5, 6, 1000))
yield Case(Annotated[float, at.Le(0.5)], (0.5, 0.0, -0.1), (0.6, 0.7, 0.8, 0.9))
yield Case(
Annotated[datetime, at.Le(datetime(2000, 1, 1))],
[datetime(2000, 1, 1), datetime(1999, 12, 31)],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
)
# Interval
yield Case(Annotated[int, at.Interval(gt=4)], (5, 6, 1000), (4, 0, -1))
yield Case(Annotated[int, at.Interval(gt=4, lt=10)], (5, 6), (4, 10, 1000, 0, -1))
yield Case(Annotated[float, at.Interval(ge=0.5, le=1)], (0.5, 0.9, 1), (0.49, 1.1))
yield Case(
Annotated[datetime, at.Interval(gt=datetime(2000, 1, 1), le=datetime(2000, 1, 3))],
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
[datetime(2000, 1, 1), datetime(2000, 1, 4)],
)
yield Case(Annotated[int, at.MultipleOf(multiple_of=3)], (0, 3, 9), (1, 2, 4))
yield Case(Annotated[float, at.MultipleOf(multiple_of=0.5)], (0, 0.5, 1, 1.5), (0.4, 1.1))
# lengths
yield Case(Annotated[str, at.MinLen(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
yield Case(Annotated[str, at.Len(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
yield Case(Annotated[List[int], at.MinLen(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
yield Case(Annotated[List[int], at.Len(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
yield Case(Annotated[str, at.MaxLen(4)], ('', '1234'), ('12345', 'x' * 10))
yield Case(Annotated[str, at.Len(0, 4)], ('', '1234'), ('12345', 'x' * 10))
yield Case(Annotated[List[str], at.MaxLen(4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
yield Case(Annotated[List[str], at.Len(0, 4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
yield Case(Annotated[str, at.Len(3, 5)], ('123', '12345'), ('', '1', '12', '123456', 'x' * 10))
yield Case(Annotated[str, at.Len(3, 3)], ('123',), ('12', '1234'))
yield Case(Annotated[Dict[int, int], at.Len(2, 3)], [{1: 1, 2: 2}], [{}, {1: 1}, {1: 1, 2: 2, 3: 3, 4: 4}])
yield Case(Annotated[Set[int], at.Len(2, 3)], ({1, 2}, {1, 2, 3}), (set(), {1}, {1, 2, 3, 4}))
yield Case(Annotated[Tuple[int, ...], at.Len(2, 3)], ((1, 2), (1, 2, 3)), ((), (1,), (1, 2, 3, 4)))
# Timezone
yield Case(
Annotated[datetime, at.Timezone(None)], [datetime(2000, 1, 1)], [datetime(2000, 1, 1, tzinfo=timezone.utc)]
)
yield Case(
Annotated[datetime, at.Timezone(...)], [datetime(2000, 1, 1, tzinfo=timezone.utc)], [datetime(2000, 1, 1)]
)
yield Case(
Annotated[datetime, at.Timezone(timezone.utc)],
[datetime(2000, 1, 1, tzinfo=timezone.utc)],
[datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
)
yield Case(
Annotated[datetime, at.Timezone('Europe/London')],
[datetime(2000, 1, 1, tzinfo=timezone(timedelta(0), name='Europe/London'))],
[datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
)
# Quantity
yield Case(Annotated[float, at.Unit(unit='m')], (5, 4.2), ('5m', '4.2m'))
# predicate types
yield Case(at.LowerCase[str], ['abc', 'foobar'], ['', 'A', 'Boom'])
yield Case(at.UpperCase[str], ['ABC', 'DEFO'], ['', 'a', 'abc', 'AbC'])
yield Case(at.IsDigit[str], ['123'], ['', 'ab', 'a1b2'])
yield Case(at.IsAscii[str], ['123', 'foo bar'], ['£100', '😊', 'whatever 👀'])
yield Case(Annotated[int, at.Predicate(lambda x: x % 2 == 0)], [0, 2, 4], [1, 3, 5])
yield Case(at.IsFinite[float], [1.23], [math.nan, math.inf, -math.inf])
yield Case(at.IsNotFinite[float], [math.nan, math.inf], [1.23])
yield Case(at.IsNan[float], [math.nan], [1.23, math.inf])
yield Case(at.IsNotNan[float], [1.23, math.inf], [math.nan])
yield Case(at.IsInfinite[float], [math.inf], [math.nan, 1.23])
yield Case(at.IsNotInfinite[float], [math.nan, 1.23], [math.inf])
# check stacked predicates
yield Case(at.IsInfinite[Annotated[float, at.Predicate(lambda x: x > 0)]], [math.inf], [-math.inf, 1.23, math.nan])
# doc
yield Case(Annotated[int, at.doc("A number")], [1, 2], [])
# custom GroupedMetadata
class MyCustomGroupedMetadata(at.GroupedMetadata):
def __iter__(self) -> Iterator[at.Predicate]:
yield at.Predicate(lambda x: float(x).is_integer())
yield Case(Annotated[float, MyCustomGroupedMetadata()], [0, 2.0], [0.01, 1.5])

View File

@@ -0,0 +1,111 @@
from __future__ import annotations
from ._core._contextmanagers import AsyncContextManagerMixin as AsyncContextManagerMixin
from ._core._contextmanagers import ContextManagerMixin as ContextManagerMixin
from ._core._eventloop import current_time as current_time
from ._core._eventloop import get_all_backends as get_all_backends
from ._core._eventloop import get_available_backends as get_available_backends
from ._core._eventloop import get_cancelled_exc_class as get_cancelled_exc_class
from ._core._eventloop import run as run
from ._core._eventloop import sleep as sleep
from ._core._eventloop import sleep_forever as sleep_forever
from ._core._eventloop import sleep_until as sleep_until
from ._core._exceptions import BrokenResourceError as BrokenResourceError
from ._core._exceptions import BrokenWorkerInterpreter as BrokenWorkerInterpreter
from ._core._exceptions import BrokenWorkerProcess as BrokenWorkerProcess
from ._core._exceptions import BusyResourceError as BusyResourceError
from ._core._exceptions import ClosedResourceError as ClosedResourceError
from ._core._exceptions import ConnectionFailed as ConnectionFailed
from ._core._exceptions import DelimiterNotFound as DelimiterNotFound
from ._core._exceptions import EndOfStream as EndOfStream
from ._core._exceptions import IncompleteRead as IncompleteRead
from ._core._exceptions import NoEventLoopError as NoEventLoopError
from ._core._exceptions import RunFinishedError as RunFinishedError
from ._core._exceptions import TypedAttributeLookupError as TypedAttributeLookupError
from ._core._exceptions import WouldBlock as WouldBlock
from ._core._fileio import AsyncFile as AsyncFile
from ._core._fileio import Path as Path
from ._core._fileio import open_file as open_file
from ._core._fileio import wrap_file as wrap_file
from ._core._resources import aclose_forcefully as aclose_forcefully
from ._core._signals import open_signal_receiver as open_signal_receiver
from ._core._sockets import TCPConnectable as TCPConnectable
from ._core._sockets import UNIXConnectable as UNIXConnectable
from ._core._sockets import as_connectable as as_connectable
from ._core._sockets import connect_tcp as connect_tcp
from ._core._sockets import connect_unix as connect_unix
from ._core._sockets import create_connected_udp_socket as create_connected_udp_socket
from ._core._sockets import (
create_connected_unix_datagram_socket as create_connected_unix_datagram_socket,
)
from ._core._sockets import create_tcp_listener as create_tcp_listener
from ._core._sockets import create_udp_socket as create_udp_socket
from ._core._sockets import create_unix_datagram_socket as create_unix_datagram_socket
from ._core._sockets import create_unix_listener as create_unix_listener
from ._core._sockets import getaddrinfo as getaddrinfo
from ._core._sockets import getnameinfo as getnameinfo
from ._core._sockets import notify_closing as notify_closing
from ._core._sockets import wait_readable as wait_readable
from ._core._sockets import wait_socket_readable as wait_socket_readable
from ._core._sockets import wait_socket_writable as wait_socket_writable
from ._core._sockets import wait_writable as wait_writable
from ._core._streams import create_memory_object_stream as create_memory_object_stream
from ._core._subprocesses import open_process as open_process
from ._core._subprocesses import run_process as run_process
from ._core._synchronization import CapacityLimiter as CapacityLimiter
from ._core._synchronization import (
CapacityLimiterStatistics as CapacityLimiterStatistics,
)
from ._core._synchronization import Condition as Condition
from ._core._synchronization import ConditionStatistics as ConditionStatistics
from ._core._synchronization import Event as Event
from ._core._synchronization import EventStatistics as EventStatistics
from ._core._synchronization import Lock as Lock
from ._core._synchronization import LockStatistics as LockStatistics
from ._core._synchronization import ResourceGuard as ResourceGuard
from ._core._synchronization import Semaphore as Semaphore
from ._core._synchronization import SemaphoreStatistics as SemaphoreStatistics
from ._core._tasks import TASK_STATUS_IGNORED as TASK_STATUS_IGNORED
from ._core._tasks import CancelScope as CancelScope
from ._core._tasks import create_task_group as create_task_group
from ._core._tasks import current_effective_deadline as current_effective_deadline
from ._core._tasks import fail_after as fail_after
from ._core._tasks import move_on_after as move_on_after
from ._core._tempfile import NamedTemporaryFile as NamedTemporaryFile
from ._core._tempfile import SpooledTemporaryFile as SpooledTemporaryFile
from ._core._tempfile import TemporaryDirectory as TemporaryDirectory
from ._core._tempfile import TemporaryFile as TemporaryFile
from ._core._tempfile import gettempdir as gettempdir
from ._core._tempfile import gettempdirb as gettempdirb
from ._core._tempfile import mkdtemp as mkdtemp
from ._core._tempfile import mkstemp as mkstemp
from ._core._testing import TaskInfo as TaskInfo
from ._core._testing import get_current_task as get_current_task
from ._core._testing import get_running_tasks as get_running_tasks
from ._core._testing import wait_all_tasks_blocked as wait_all_tasks_blocked
from ._core._typedattr import TypedAttributeProvider as TypedAttributeProvider
from ._core._typedattr import TypedAttributeSet as TypedAttributeSet
from ._core._typedattr import typed_attribute as typed_attribute
# Re-export imports so they look like they live directly in this package
for __value in list(locals().values()):
if getattr(__value, "__module__", "").startswith("anyio."):
__value.__module__ = __name__
del __value
def __getattr__(attr: str) -> type[BrokenWorkerInterpreter]:
"""Support deprecated aliases."""
if attr == "BrokenWorkerIntepreter":
import warnings
warnings.warn(
"The 'BrokenWorkerIntepreter' alias is deprecated, use 'BrokenWorkerInterpreter' instead.",
DeprecationWarning,
stacklevel=2,
)
return BrokenWorkerInterpreter
raise AttributeError(f"module {__name__!r} has no attribute {attr!r}")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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