first commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
__pycache__/*
|
||||
.DS_Store
|
||||
*/__pycache__/*
|
||||
54
llm_settings.json
Normal file
54
llm_settings.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"selected_provider": "Kimi",
|
||||
"provider_configs": {
|
||||
"Ollama": {
|
||||
"type": "ollama",
|
||||
"base_url": "http://localhost:11434",
|
||||
"models_endpoint": "/api/tags",
|
||||
"chat_endpoint": "/api/chat",
|
||||
"requires_api_key": false,
|
||||
"api_key": "",
|
||||
"encoding": "utf-8",
|
||||
"default_model": "gpt-oss",
|
||||
"default_models": [],
|
||||
"allow_endpoint_edit": false,
|
||||
"allow_api_toggle": false,
|
||||
"system_prompt": ""
|
||||
},
|
||||
"OpenAI": {
|
||||
"type": "openai-compatible",
|
||||
"base_url": "https://api.openai.com/v1",
|
||||
"models_endpoint": "/models",
|
||||
"chat_endpoint": "/chat/completions",
|
||||
"requires_api_key": true,
|
||||
"api_key": "",
|
||||
"encoding": "utf-8",
|
||||
"default_model": "gpt-4o-mini",
|
||||
"default_models": [
|
||||
"gpt-4o-mini",
|
||||
"gpt-4o",
|
||||
"gpt-4.1-mini",
|
||||
"gpt-3.5-turbo"
|
||||
],
|
||||
"allow_endpoint_edit": true,
|
||||
"allow_api_toggle": true,
|
||||
"system_prompt": ""
|
||||
},
|
||||
"Kimi": {
|
||||
"type": "openai-compatible",
|
||||
"base_url": "https://api.moonshot.cn/v1",
|
||||
"models_endpoint": "/v1/models",
|
||||
"chat_endpoint": "/v1/chat/completions",
|
||||
"requires_api_key": true,
|
||||
"api_key": "sk-2gCgINOEErD1ctdxIB7ALIPnHboZPrQRj1hvVJtEydT1JbXv",
|
||||
"encoding": "utf-8",
|
||||
"default_model": "kimi-k2-0711-preview",
|
||||
"default_models": [
|
||||
"kimi-k2-0711-preview"
|
||||
],
|
||||
"allow_endpoint_edit": true,
|
||||
"allow_api_toggle": true,
|
||||
"system_prompt": "You are a very friendly drone control agent. No matter what language I use to give you instructions, please call the tools to perform the task and then reply in English."
|
||||
}
|
||||
}
|
||||
}
|
||||
10
template/__init__.py
Normal file
10
template/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
UAV Agent Templates
|
||||
|
||||
This package contains prompt templates for the UAV control agent.
|
||||
"""
|
||||
|
||||
from .agent_prompt import AGENT_PROMPT
|
||||
from .parsing_error import PARSING_ERROR_TEMPLATE
|
||||
|
||||
__all__ = ["AGENT_PROMPT", "PARSING_ERROR_TEMPLATE"]
|
||||
92
template/agent_prompt.py
Normal file
92
template/agent_prompt.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
UAV Agent Prompt Template
|
||||
|
||||
This template defines the system prompt for the UAV control agent.
|
||||
It provides guidelines, safety rules, task types, and response format instructions.
|
||||
"""
|
||||
|
||||
AGENT_PROMPT = """You are an intelligent UAV (drone) control agent. Your job is to understand user intentions and control drones safely and efficiently.
|
||||
|
||||
IMPORTANT GUIDELINES:
|
||||
0. ALWAYS Respond [TASK DONE] as a signal of finish task at the end of response.
|
||||
1. ALWAYS check the current session status first to understand the mission task
|
||||
2. ALWAYS list available drones before attempting to control them
|
||||
3. ALWAYS check nearby entities of a drone before you control it, there are lot of obstacles.
|
||||
4. Check weather conditions regularly - the weather will influence the battery usage
|
||||
5. Be proactive in gathering information of obstacles and targets, by using nearby entities functions
|
||||
6. Remember the information of obstacles and targets, because they are not always available
|
||||
7. When visiting targets, get close enough within task_radius
|
||||
9. Land drones safely when tasks are complete or battery is low
|
||||
10. Monitor battery levels - if below 10%, consider charging before continuing
|
||||
|
||||
SAFETY RULES:
|
||||
- If you can not directly move the drone to a position, find a mediam waypoint to get there first, and then cosider the destination, repeat the process, until you can move directly to the destination.
|
||||
- Always verify drone status and nearby entities before commands
|
||||
|
||||
|
||||
AVAILABLE TOOLS:
|
||||
You have access to these tools to accomplish your tasks: {tool_names}
|
||||
|
||||
{tools}
|
||||
|
||||
RESPONSE FORMAT:
|
||||
Use this exact format for your responses:
|
||||
|
||||
Question: the input question or command you must respond to
|
||||
Thought: analyze what you need to do and what information you need
|
||||
Action: the specific tool to use from the list above
|
||||
Action Input: the input parameters for the tool (use proper JSON format)
|
||||
Observation: the result from running the tool
|
||||
... (repeat Thought/Action/Action Input/Observation as needed)
|
||||
Thought: I now have enough information to provide a final answer
|
||||
Final Answer: a clear, concise answer to the original question
|
||||
|
||||
ACTION INPUT FORMAT RULES:
|
||||
1. For tools with NO parameters (like list_drones, get_session_info):
|
||||
Action Input: {{}}
|
||||
|
||||
2. For tools with ONE string parameter (like get_drone_status):
|
||||
Action Input: {{"drone_id": "drone-abc123"}}
|
||||
|
||||
3. For tools with MULTIPLE parameters (like move_to):
|
||||
Action Input: {{"drone_id": "drone-abc123", "x": 100.0, "y": 50.0, "z": 20.0}}
|
||||
|
||||
CRITICAL:
|
||||
- ALWAYS use proper JSON format with double quotes for keys and string values
|
||||
- ALWAYS use curly braces for Action Input
|
||||
- For tools with no parameters, use empty braces
|
||||
- Numbers should NOT have quotes
|
||||
- Strings MUST have quotes
|
||||
|
||||
EXAMPLES:
|
||||
Question: What drones are available?
|
||||
Thought: I need to list all drones to see what's available
|
||||
Action: list_drones
|
||||
Action Input: {{}}
|
||||
Observation: [result will be returned here]
|
||||
|
||||
Question: Take off drone-001 to 15 meters
|
||||
Thought: I need to take off the drone to the specified altitude
|
||||
Action: take_off
|
||||
Action Input: {{"drone_id": "drone-001", "altitude": 15.0}}
|
||||
Observation: Drone took off successfully
|
||||
|
||||
Question: Move drone-001 to position x=100, y=50, z=20
|
||||
Thought: I need to move the drone to the specified coordinates
|
||||
Action: move_to
|
||||
Action Input: {{"drone_id": "drone-001", "x": 100.0, "y": 50.0, "z": 20.0}}
|
||||
Observation: Drone moved successfully
|
||||
|
||||
Tips for you to finish task in the most efficient way:
|
||||
|
||||
1. For a certain task, use the task-related API, do not get_session_info too many times.
|
||||
2. If you want to to get the position of a target, use GET /targets API. If you want to get the position of an obstacle, use GET /obstacles API. DO NOT USE get_nearby_entities!
|
||||
3. Move directly as much as possible. For example, when the task is moving from A to B via C, first try to Collision Detection between A to C, if there's no collision, then move directly to C, otherwise, detour.
|
||||
4. Getting entities nearby do not always effective. You have only limited sensor range. Using /targets API to get targets and /obstacles API to get obstacles is more effective.
|
||||
5. If battery is below 30, find the nerest waypoint, go there and land, then charge to 100.
|
||||
6. Reaching to a higher latitude can help you see targets, but do not exceed the drone's limit.
|
||||
|
||||
Begin!
|
||||
|
||||
Question: {input}
|
||||
Thought:{agent_scratchpad}"""
|
||||
18
template/parsing_error.py
Normal file
18
template/parsing_error.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Parsing Error Template
|
||||
|
||||
This template defines the error message shown to the LLM when it produces
|
||||
invalid JSON in the Action Input field.
|
||||
"""
|
||||
|
||||
PARSING_ERROR_TEMPLATE = """Parsing error: {error}
|
||||
|
||||
REMINDER - Action Input must be valid JSON:
|
||||
- Use double quotes for keys and string values
|
||||
- Use curly braces: {{}}
|
||||
- For no parameters: {{}}
|
||||
- For one parameter: {{"drone_id": "drone-001"}}
|
||||
- For multiple parameters: {{"drone_id": "drone-001", "altitude": 15.0}}
|
||||
- Numbers WITHOUT quotes, strings WITH quotes
|
||||
|
||||
Please try again with proper JSON format."""
|
||||
140
tp
Normal file
140
tp
Normal file
@@ -0,0 +1,140 @@
|
||||
These are all the APIs you can use.
|
||||
|
||||
Drone Management
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/drones` | List all drones |
|
||||
| POST | `/drones` | Register new drone |
|
||||
| GET | `/drones/{{id}}` | Get drone details |
|
||||
| PUT | `/drones/{{id}}` | Update drone properties (metadata, state, battery, position, home) |
|
||||
| PUT | `/drones/{{id}}/position` | Update drone position only |
|
||||
| DELETE | `/drones/{{id}}` | Delete drone |
|
||||
| POST | `/drones/{{id}}/battery` | Update battery level |
|
||||
|
||||
Command Management
|
||||
|
||||
Generic Command Endpoint
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| POST | `/drones/{{id}}/command` | Send any command |
|
||||
| GET | `/drones/{{id}}/commands` | Get command history |
|
||||
| GET | `/commands/{{command_id}}` | Get command status |
|
||||
|
||||
Direct Command Endpoints
|
||||
|
||||
All commands use **POST** method with `/drones/{{id}}/command/{{command_name}}`
|
||||
|
||||
Charge can only done at Waypoint target.
|
||||
|
||||
| Command | Parameters | Description |
|
||||
|---------|-----------|-------------|
|
||||
| `take_off` | `?altitude=10.0` | Takeoff to altitude |
|
||||
| `land` | - | Land at position |
|
||||
| `move_to` | `?x=50&y=50&z=15` | Move to coordinates |
|
||||
| `move_towards` | `?distance=20&heading=90` | Move distance in direction (uses current heading if not specified) |
|
||||
| `move_along_path` | Body: `{{waypoints:[...]}}` | Follow waypoints |
|
||||
| `change_altitude` | `?altitude=20.0` | Change altitude only |
|
||||
| `hover` | `duration` (optional) | Hold position |
|
||||
| `rotate` | `?heading=180.0` | Change heading/orientation |
|
||||
| `return_home` | - | Return to launch |
|
||||
| `set_home` | - | Set home position |
|
||||
| `calibrate` | - | Calibrate sensors |
|
||||
| `take_photo` | - | Capture image |
|
||||
| `send_message` | `?target_drone_id=X&message=Y` | Send to drone |
|
||||
| `broadcast` | `?message=text` | Send to all |
|
||||
| `charge` | `?charge_amount=30.0` | Charge battery |
|
||||
|
||||
Target Management
|
||||
|
||||
Type of target we have fixed, moving, waypoint, circle, polygon. - Note: fixed type can also represent points of interest
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/targets` | List all targets |
|
||||
| GET | `/targets/{{id}}` | Get target details |
|
||||
| GET | `/targets/type/{{type}}` | Get by type |
|
||||
|
||||
Waypoint Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/targets/waypoints` | List charging stations |
|
||||
| POST | `/targets/waypoints/{{id}}/check-drone` | Check if drone at waypoint |
|
||||
| GET | `/targets/waypoints/nearest` | Find nearest waypoint |
|
||||
|
||||
Obstacle Management
|
||||
|
||||
Type of obstacle we hvave point, circle, ellipse, polygon.
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/obstacles` | List all obstacles |
|
||||
| GET | `/obstacles/{{id}}` | Get obstacle |
|
||||
| GET | `/obstacles/type/{{type}}` | Get by type |
|
||||
|
||||
Collision Detection
|
||||
|
||||
**Endpoint:** `POST /obstacles/path_collision`
|
||||
|
||||
**Authentication:** Requires SYSTEM role (ADMIN inherits)
|
||||
|
||||
**Description:** Checks if a flight path from start to end collides with any obstacles. Returns the **first** obstacle that collides with the path.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| start | object | Yes | Start point {{x, y, z}} |
|
||||
| end | object | Yes | End point {{x, y, z}} |
|
||||
| safety_margin | float | No | Additional clearance distance (in meters) around the flight path (default: 0.0). Creates a corridor with specified width on each side. Use 0.0 for direct line path, or > 0.0 for safety corridor (e.g., 5.0 creates a 10m-wide corridor). Note: Drone movement commands use 0.0 by default |
|
||||
|
||||
**Height Logic:**
|
||||
- `height = 0`: Impassable at any altitude
|
||||
- `height > 0`: Collision only if max flight altitude <= obstacle.height
|
||||
|
||||
**Response:** Collision response object or null if no collision
|
||||
|
||||
**Example Request:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/obstacles/path_collision \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "X-API-Key: system_secret_key_change_in_production" \\
|
||||
-d '{{
|
||||
"start": {{"x": 0.0, "y": 0.0, "z": 10.0}},
|
||||
"end": {{"x": 200.0, "y": 300.0, "z": 10.0}},
|
||||
"safety_margin": 2.0
|
||||
}}'
|
||||
```
|
||||
|
||||
**Example Response (Collision):**
|
||||
```json
|
||||
{{
|
||||
"obstacle_id": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"obstacle_name": "Water Tower",
|
||||
"obstacle_type": "circle",
|
||||
"collision_type": "path_intersection",
|
||||
"distance": 5.0
|
||||
}}
|
||||
```
|
||||
|
||||
**Example Response (No Collision):**
|
||||
```json
|
||||
null
|
||||
```
|
||||
|
||||
| Method | Endpoint | Description | Auth |
|
||||
|--------|----------|-------------|------|
|
||||
| POST | `/obstacles/path_collision` | Check if flight path collides with obstacles | SYSTEM |
|
||||
| POST | `/obstacles/point_collision` | Check if point is inside any obstacles (returns all matches) | SYSTEM |
|
||||
|
||||
Proximity
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/drones/{{id}}/nearby` | Aggregated nearby drones, targets, obstacles (uses drone's perceived_radius) |
|
||||
| GET | `/drones/{{id}}/nearby/drones` | Nearby drones (uses drone's perceived_radius) |
|
||||
| GET | `/drones/{{id}}/nearby/targets` | Nearby targets (uses drone's perceived_radius) |
|
||||
| GET | `/drones/{{id}}/nearby/obstacles` | Nearby obstacles (uses drone's perceived_radius) |
|
||||
All proximity endpoints use the drone's `perceived_radius` to determine the search area.
|
||||
671
uav_agent.py
Normal file
671
uav_agent.py
Normal file
@@ -0,0 +1,671 @@
|
||||
"""
|
||||
UAV Control Agent
|
||||
An intelligent agent that understands natural language commands and controls drones using the UAV API
|
||||
Uses LangChain 1.0+ with modern @tool decorator pattern
|
||||
"""
|
||||
from langchain_classic.agents import create_react_agent
|
||||
from langchain_classic.agents import AgentExecutor
|
||||
from langchain_classic.prompts import PromptTemplate
|
||||
from langchain_ollama import ChatOllama
|
||||
from langchain_openai import ChatOpenAI
|
||||
from uav_api_client import UAVAPIClient
|
||||
from uav_langchain_tools import create_uav_tools
|
||||
from template.agent_prompt import AGENT_PROMPT
|
||||
from template.parsing_error import PARSING_ERROR_TEMPLATE
|
||||
from typing import Optional, Dict, Any
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_llm_settings(settings_path: str = "llm_settings.json") -> Optional[Dict[str, Any]]:
|
||||
"""Load LLM settings from JSON file"""
|
||||
try:
|
||||
path = Path(settings_path)
|
||||
if path.exists():
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not load LLM settings from {settings_path}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def prompt_user_for_llm_config() -> Dict[str, Any]:
|
||||
"""Prompt user to select LLM provider and model"""
|
||||
settings = load_llm_settings()
|
||||
|
||||
if not settings or 'provider_configs' not in settings:
|
||||
print("⚠️ No llm_settings.json found or invalid format. Using command line arguments.")
|
||||
return {}
|
||||
|
||||
provider_configs = settings['provider_configs']
|
||||
selected_provider = settings.get('selected_provider', '')
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("🤖 LLM Provider Configuration")
|
||||
print("="*60)
|
||||
|
||||
# Show available providers
|
||||
providers = list(provider_configs.keys())
|
||||
print("\nAvailable providers:")
|
||||
for i, provider in enumerate(providers, 1):
|
||||
config = provider_configs[provider]
|
||||
default_marker = " (selected in settings)" if provider == selected_provider else ""
|
||||
print(f" {i}. {provider}{default_marker}")
|
||||
print(f" Type: {config.get('type', 'unknown')}")
|
||||
print(f" Base URL: {config.get('base_url', 'N/A')}")
|
||||
print(f" Requires API Key: {config.get('requires_api_key', False)}")
|
||||
|
||||
# Prompt for provider selection
|
||||
print(f"\nSelect a provider (1-{len(providers)}) [default: {selected_provider or providers[0]}]: ", end='')
|
||||
provider_choice = input().strip()
|
||||
|
||||
if not provider_choice:
|
||||
# Use default
|
||||
if selected_provider and selected_provider in providers:
|
||||
chosen_provider = selected_provider
|
||||
else:
|
||||
chosen_provider = providers[0]
|
||||
else:
|
||||
try:
|
||||
idx = int(provider_choice) - 1
|
||||
if 0 <= idx < len(providers):
|
||||
chosen_provider = providers[idx]
|
||||
else:
|
||||
print(f"Invalid choice. Using default: {selected_provider or providers[0]}")
|
||||
chosen_provider = selected_provider or providers[0]
|
||||
except ValueError:
|
||||
print(f"Invalid input. Using default: {selected_provider or providers[0]}")
|
||||
chosen_provider = selected_provider or providers[0]
|
||||
|
||||
config = provider_configs[chosen_provider]
|
||||
print(f"\n✅ Selected provider: {chosen_provider}")
|
||||
|
||||
# Show available models
|
||||
default_models = config.get('default_models', [])
|
||||
default_model = config.get('default_model', '')
|
||||
|
||||
if default_models:
|
||||
print("\nAvailable models:")
|
||||
for i, model in enumerate(default_models, 1):
|
||||
default_marker = " (default)" if model == default_model else ""
|
||||
print(f" {i}. {model}{default_marker}")
|
||||
print(f" {len(default_models) + 1}. Custom model (enter manually)")
|
||||
|
||||
print(f"\nSelect a model (1-{len(default_models) + 1}) [default: {default_model}]: ", end='')
|
||||
model_choice = input().strip()
|
||||
|
||||
if not model_choice:
|
||||
chosen_model = default_model
|
||||
else:
|
||||
try:
|
||||
idx = int(model_choice) - 1
|
||||
if 0 <= idx < len(default_models):
|
||||
chosen_model = default_models[idx]
|
||||
elif idx == len(default_models):
|
||||
# Custom model
|
||||
print("Enter custom model name: ", end='')
|
||||
chosen_model = input().strip() or default_model
|
||||
else:
|
||||
print(f"Invalid choice. Using default: {default_model}")
|
||||
chosen_model = default_model
|
||||
except ValueError:
|
||||
print(f"Invalid input. Using default: {default_model}")
|
||||
chosen_model = default_model
|
||||
else:
|
||||
# No predefined models, ask for custom input
|
||||
print(f"\nEnter model name [default: {default_model}]: ", end='')
|
||||
chosen_model = input().strip() or default_model
|
||||
|
||||
print(f"✅ Selected model: {chosen_model}")
|
||||
|
||||
# Determine provider type
|
||||
provider_type = config.get('type', 'ollama')
|
||||
if provider_type == 'openai-compatible':
|
||||
if 'api.openai.com' in config.get('base_url', ''):
|
||||
llm_provider = 'openai'
|
||||
else:
|
||||
llm_provider = 'openai-compatible'
|
||||
else:
|
||||
llm_provider = provider_type
|
||||
|
||||
# Get API key if required
|
||||
api_key = config.get('api_key', '').strip()
|
||||
if config.get('requires_api_key', False) and not api_key:
|
||||
print("\n⚠️ This provider requires an API key.")
|
||||
print("Enter API key (or press Enter to use environment variable): ", end='')
|
||||
api_key = input().strip()
|
||||
|
||||
result = {
|
||||
'llm_provider': llm_provider,
|
||||
'llm_model': chosen_model,
|
||||
'llm_base_url': config.get('base_url'),
|
||||
'llm_api_key': api_key if api_key else None,
|
||||
'provider_name': chosen_provider
|
||||
}
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("✅ Configuration complete!")
|
||||
print("="*60)
|
||||
print(f"Provider: {chosen_provider}")
|
||||
print(f"Type: {llm_provider}")
|
||||
print(f"Model: {chosen_model}")
|
||||
print(f"Base URL: {config.get('base_url')}")
|
||||
if api_key:
|
||||
print(f"API Key: {'*' * (len(api_key) - 4) + api_key[-4:] if len(api_key) > 4 else '****'}")
|
||||
print("="*60 + "\n")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class UAVControlAgent:
|
||||
"""Intelligent agent for controlling UAVs using natural language"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str = "http://localhost:8000",
|
||||
uav_api_key: Optional[str] = None,
|
||||
llm_provider: str = "ollama",
|
||||
llm_model: str = "llama2",
|
||||
llm_api_key: Optional[str] = None,
|
||||
llm_base_url: Optional[str] = None,
|
||||
temperature: float = 0.1,
|
||||
verbose: bool = True,
|
||||
debug: bool = False
|
||||
):
|
||||
"""
|
||||
Initialize the UAV Control Agent
|
||||
|
||||
Args:
|
||||
base_url: Base URL of the UAV API server
|
||||
uav_api_key: API key for UAV server authentication (None = USER role, or provide SYSTEM/ADMIN key)
|
||||
llm_provider: LLM provider ('ollama', 'openai', 'openai-compatible')
|
||||
llm_model: Model name (e.g., 'llama2', 'gpt-4o-mini', 'deepseek-chat')
|
||||
llm_api_key: API key for LLM provider (required for openai/openai-compatible)
|
||||
llm_base_url: Custom base URL for LLM API (for openai-compatible providers)
|
||||
temperature: LLM temperature (lower = more deterministic)
|
||||
verbose: Enable verbose output for agent reasoning
|
||||
debug: Enable debug output for connection and setup info
|
||||
"""
|
||||
self.client = UAVAPIClient(base_url, api_key=uav_api_key)
|
||||
self.verbose = verbose
|
||||
self.debug = debug
|
||||
|
||||
if self.debug:
|
||||
print("\n" + "="*60)
|
||||
print("🔧 UAV Agent Initialization - Debug Mode")
|
||||
print("="*60)
|
||||
print(f"UAV API Server: {base_url}")
|
||||
print(f"LLM Provider: {llm_provider}")
|
||||
print(f"LLM Model: {llm_model}")
|
||||
print(f"Temperature: {temperature}")
|
||||
print(f"Verbose: {verbose}")
|
||||
print()
|
||||
|
||||
# Test UAV API connection
|
||||
if self.debug:
|
||||
print("🔌 Testing UAV API connection...")
|
||||
try:
|
||||
session = self.client.get_current_session()
|
||||
if self.debug:
|
||||
print(f"✅ Connected to UAV API")
|
||||
print(f" Session: {session.get('name', 'Unknown')}")
|
||||
print(f" Task: {session.get('task', 'Unknown')}")
|
||||
print()
|
||||
except Exception as e:
|
||||
if self.debug:
|
||||
print(f"⚠️ Warning: Could not connect to UAV API: {e}")
|
||||
print(f" Make sure the UAV server is running at {base_url}")
|
||||
print()
|
||||
|
||||
# Initialize LLM based on provider
|
||||
if self.debug:
|
||||
print(f"🤖 Initializing LLM provider: {llm_provider}")
|
||||
|
||||
if llm_provider == "ollama":
|
||||
if self.debug:
|
||||
print(f" Using Ollama with model: {llm_model}")
|
||||
print(f" Ollama URL: http://localhost:11434 (default)")
|
||||
|
||||
self.llm = ChatOllama(
|
||||
model=llm_model,
|
||||
temperature=temperature
|
||||
)
|
||||
|
||||
if self.debug:
|
||||
print(f"✅ Ollama LLM initialized")
|
||||
print()
|
||||
|
||||
elif llm_provider in ["openai", "openai-compatible"]:
|
||||
if not llm_api_key:
|
||||
raise ValueError(f"API key is required for {llm_provider} provider. Use --llm-api-key or set environment variable.")
|
||||
|
||||
# Determine base URL
|
||||
if llm_provider == "openai":
|
||||
final_base_url = llm_base_url or "https://api.openai.com/v1"
|
||||
provider_name = "OpenAI"
|
||||
else:
|
||||
if not llm_base_url:
|
||||
raise ValueError("llm_base_url is required for openai-compatible provider")
|
||||
final_base_url = llm_base_url
|
||||
provider_name = "OpenAI-Compatible API"
|
||||
|
||||
if self.debug:
|
||||
print(f" Provider: {provider_name}")
|
||||
print(f" Base URL: {final_base_url}")
|
||||
print(f" Model: {llm_model}")
|
||||
print(f" API Key: {'*' * (len(llm_api_key) - 4) + llm_api_key[-4:] if len(llm_api_key) > 4 else '****'}")
|
||||
|
||||
# Create LLM instance
|
||||
kwargs = {
|
||||
"model": llm_model,
|
||||
"temperature": temperature,
|
||||
"api_key": llm_api_key,
|
||||
"base_url": final_base_url
|
||||
}
|
||||
|
||||
self.llm = ChatOpenAI(**kwargs)
|
||||
|
||||
if self.debug:
|
||||
print(f"✅ {provider_name} LLM initialized")
|
||||
print()
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unknown LLM provider: {llm_provider}. "
|
||||
f"Use 'ollama', 'openai', or 'openai-compatible'"
|
||||
)
|
||||
|
||||
# Create tools using the new @tool decorator approach
|
||||
if self.debug:
|
||||
print("🔧 Creating UAV control tools...")
|
||||
self.tools = create_uav_tools(self.client)
|
||||
if self.debug:
|
||||
print(f"✅ Created {len(self.tools)} tools")
|
||||
print(f" Tools: {', '.join([tool.name for tool in self.tools[:5]])}...")
|
||||
print()
|
||||
|
||||
# Create prompt template
|
||||
if self.debug:
|
||||
print("📝 Creating agent prompt template...")
|
||||
self.prompt = self._create_prompt()
|
||||
if self.debug:
|
||||
print("✅ Prompt template created")
|
||||
print()
|
||||
|
||||
# Create ReAct agent
|
||||
if self.debug:
|
||||
print("🤖 Creating ReAct agent...")
|
||||
self.agent = create_react_agent(
|
||||
llm=self.llm,
|
||||
tools=self.tools,
|
||||
prompt=self.prompt
|
||||
)
|
||||
|
||||
if self.debug:
|
||||
print("✅ ReAct agent created")
|
||||
print()
|
||||
|
||||
# Create agent executor with improved error handling
|
||||
if self.debug:
|
||||
print("⚙️ Creating agent executor...")
|
||||
print(f" Max iterations: 20")
|
||||
print(f" Verbose mode: {verbose}")
|
||||
|
||||
# Custom error handler to help LLM fix formatting issues
|
||||
def handle_parsing_error(error) -> str:
|
||||
"""Provide helpful feedback when Action Input parsing fails"""
|
||||
return PARSING_ERROR_TEMPLATE.format(error=str(error))
|
||||
|
||||
self.agent_executor = AgentExecutor(
|
||||
agent=self.agent,
|
||||
tools=self.tools,
|
||||
verbose=verbose,
|
||||
handle_parsing_errors=handle_parsing_error,
|
||||
max_iterations=50, # Increased for complex tasks
|
||||
return_intermediate_steps=True,
|
||||
early_stopping_method="generate" # Better handling of completion
|
||||
)
|
||||
if self.debug:
|
||||
print("✅ Agent executor created")
|
||||
print()
|
||||
|
||||
# Session context
|
||||
if self.debug:
|
||||
print("🔄 Refreshing session context...")
|
||||
self.session_context = {}
|
||||
self.refresh_session_context()
|
||||
|
||||
if self.debug:
|
||||
print("="*60)
|
||||
print("✅ UAV Agent Initialization Complete!")
|
||||
print("="*60)
|
||||
print()
|
||||
|
||||
def _create_prompt(self) -> PromptTemplate:
|
||||
"""Create the agent prompt template"""
|
||||
prompt_template = PromptTemplate(
|
||||
template=AGENT_PROMPT,
|
||||
input_variables=["input", "agent_scratchpad"],
|
||||
partial_variables={
|
||||
"tools": "\n".join([
|
||||
f"- {tool.name}: {tool.description}"
|
||||
for tool in self.tools
|
||||
]),
|
||||
"tool_names": ", ".join([tool.name for tool in self.tools])
|
||||
}
|
||||
)
|
||||
return prompt_template
|
||||
|
||||
def refresh_session_context(self):
|
||||
"""Refresh session context information"""
|
||||
try:
|
||||
session = self.client.get_current_session()
|
||||
self.session_context = {
|
||||
'session_id': session.get('id'),
|
||||
'task_type': session.get('task'),
|
||||
'task_description': session.get('task_description'),
|
||||
'status': session.get('status')
|
||||
}
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"Warning: Could not refresh session context: {e}")
|
||||
|
||||
def get_session_summary(self) -> str:
|
||||
"""Get a summary of the current session"""
|
||||
try:
|
||||
session = self.client.get_current_session()
|
||||
progress = self.client.get_task_progress()
|
||||
drones = self.client.list_drones()
|
||||
|
||||
summary = f"""
|
||||
=== Current Session Summary ===
|
||||
Session: {session.get('name', 'Unknown')}
|
||||
Task: {session.get('task', 'Unknown')} - {session.get('task_description', '')}
|
||||
Status: {session.get('status', 'Unknown')}
|
||||
|
||||
Progress: {progress.get('progress_percentage', 0)}% ({progress.get('status_message', 'Unknown')})
|
||||
Completed: {progress.get('is_completed', False)}
|
||||
|
||||
Drones: {len(drones)} available
|
||||
"""
|
||||
for drone in drones:
|
||||
summary += f" - {drone.get('name')} ({drone.get('id')}): {drone.get('status')}, Battery: {drone.get('battery_level', 0):.1f}%\n"
|
||||
|
||||
return summary.strip()
|
||||
except Exception as e:
|
||||
return f"Error getting session summary: {e}"
|
||||
|
||||
def execute(self, command: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute a natural language command
|
||||
|
||||
Args:
|
||||
command: Natural language command from user
|
||||
|
||||
Returns:
|
||||
Dictionary with 'output', 'intermediate_steps', and 'success' keys
|
||||
"""
|
||||
if self.debug:
|
||||
print(f"\n{'='*60}")
|
||||
print(f"🎯 Executing Command")
|
||||
print(f"{'='*60}")
|
||||
print(f"Command: {command}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
try:
|
||||
if self.debug:
|
||||
print("🔄 Invoking agent executor...")
|
||||
|
||||
result = self.agent_executor.invoke({"input": command})
|
||||
|
||||
if self.debug:
|
||||
print(f"\n{'='*60}")
|
||||
print("✅ Command Execution Complete")
|
||||
print(f"{'='*60}")
|
||||
print(f"Success: True")
|
||||
print(f"Intermediate steps: {len(result.get('intermediate_steps', []))}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'output': result.get('output', ''),
|
||||
'intermediate_steps': result.get('intermediate_steps', [])
|
||||
}
|
||||
except Exception as e:
|
||||
if self.debug:
|
||||
print(f"\n{'='*60}")
|
||||
print("❌ Command Execution Failed")
|
||||
print(f"{'='*60}")
|
||||
print(f"Error: {str(e)}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
return {
|
||||
'success': False,
|
||||
'output': f"Error executing command: {str(e)}",
|
||||
'intermediate_steps': []
|
||||
}
|
||||
|
||||
def run_interactive(self):
|
||||
"""Run the agent in interactive mode"""
|
||||
print("\n" + "="*60)
|
||||
print("🚁 UAV Control Agent - Interactive Mode")
|
||||
print("="*60)
|
||||
print("\nType 'quit', 'exit', or 'q' to stop")
|
||||
print("Type 'status' to see session summary")
|
||||
print("Type 'help' for example commands\n")
|
||||
|
||||
# Show initial session summary
|
||||
print(self.get_session_summary())
|
||||
print("\n" + "-"*60 + "\n")
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = input("\n🎮 Command: ").strip()
|
||||
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
if user_input.lower() in ['quit', 'exit', 'q']:
|
||||
print("\n👋 Goodbye!")
|
||||
break
|
||||
|
||||
if user_input.lower() == 'status':
|
||||
print(self.get_session_summary())
|
||||
continue
|
||||
|
||||
if user_input.lower() == 'help':
|
||||
self._print_help()
|
||||
continue
|
||||
|
||||
# Execute command
|
||||
print("\n🤖 Processing...\n")
|
||||
result = self.execute(user_input)
|
||||
|
||||
if result['success']:
|
||||
print(f"\n✅ {result['output']}\n")
|
||||
else:
|
||||
print(f"\n❌ {result['output']}\n")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n👋 Goodbye!")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}\n")
|
||||
|
||||
def _print_help(self):
|
||||
"""Print example commands"""
|
||||
help_text = """
|
||||
Example Commands:
|
||||
==================
|
||||
|
||||
Information:
|
||||
- "What drones are available?"
|
||||
- "Show me the current mission status"
|
||||
- "What targets do I need to visit?"
|
||||
- "Check the weather conditions"
|
||||
- "What's the task progress?"
|
||||
|
||||
Basic Control:
|
||||
- "Take off drone-abc123 to 15 meters"
|
||||
- "Move drone-abc123 to coordinates x=100, y=50, z=20"
|
||||
- "Land drone-abc123"
|
||||
- "Return all drones home"
|
||||
|
||||
Mission Execution:
|
||||
- "Visit all targets with the first drone"
|
||||
- "Search the area with available drones"
|
||||
- "Complete the mission task"
|
||||
- "Patrol the assigned areas"
|
||||
|
||||
Safety:
|
||||
- "Check if there are obstacles between (0,0,10) and (100,100,10)"
|
||||
- "What's nearby drone-abc123?"
|
||||
- "Check battery levels"
|
||||
|
||||
Smart Commands:
|
||||
- "Take photos at all target locations"
|
||||
- "Charge any drones with low battery"
|
||||
- "Survey all targets and return home"
|
||||
"""
|
||||
print(help_text)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="UAV Control Agent - Natural Language Drone Control"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--base-url',
|
||||
default='http://localhost:8000',
|
||||
help='UAV API base URL'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--uav-api-key',
|
||||
default=None,
|
||||
help='API key for UAV server (defaults to USER role if not provided, or set UAV_API_KEY env var)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--llm-provider',
|
||||
default=None,
|
||||
choices=['ollama', 'openai', 'openai-compatible'],
|
||||
help='LLM provider (ollama, openai, or openai-compatible for DeepSeek, etc.)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--llm-model',
|
||||
default=None,
|
||||
help='LLM model name (e.g., llama2, gpt-4o-mini, deepseek-chat)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--llm-api-key',
|
||||
default=None,
|
||||
help='API key for LLM provider (or set via environment variable)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--llm-base-url',
|
||||
default=None,
|
||||
help='Custom base URL for LLM API (required for openai-compatible providers)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--temperature',
|
||||
type=float,
|
||||
default=0.1,
|
||||
help='LLM temperature (0.0-1.0)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--command', '-c',
|
||||
default=None,
|
||||
help='Single command to execute (non-interactive)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--quiet', '-q',
|
||||
action='store_true',
|
||||
help='Reduce verbosity'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--debug', '-d',
|
||||
action='store_true',
|
||||
help='Enable debug output for connection and setup info'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-prompt',
|
||||
action='store_true',
|
||||
help='Skip interactive provider/model selection (use command line args or defaults)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Determine if we should prompt for config
|
||||
should_prompt = (
|
||||
not args.no_prompt and
|
||||
not args.command and # Only prompt in interactive mode
|
||||
args.llm_provider is None and # No provider specified
|
||||
args.llm_model is None # No model specified
|
||||
)
|
||||
|
||||
# Get configuration from user prompt or command line
|
||||
if should_prompt:
|
||||
config = prompt_user_for_llm_config()
|
||||
if config:
|
||||
llm_provider = config.get('llm_provider', 'ollama')
|
||||
llm_model = config.get('llm_model', 'llama2')
|
||||
llm_base_url = config.get('llm_base_url')
|
||||
llm_api_key = config.get('llm_api_key')
|
||||
else:
|
||||
# Fallback to defaults
|
||||
llm_provider = 'ollama'
|
||||
llm_model = 'llama2'
|
||||
llm_base_url = None
|
||||
llm_api_key = None
|
||||
else:
|
||||
# Use command line arguments or defaults
|
||||
llm_provider = args.llm_provider or 'ollama'
|
||||
llm_model = args.llm_model or 'llama2'
|
||||
llm_base_url = args.llm_base_url
|
||||
llm_api_key = args.llm_api_key
|
||||
|
||||
# Get LLM API key from args or environment if not set
|
||||
if not llm_api_key:
|
||||
llm_api_key = os.getenv("OPENAI_API_KEY") or os.getenv("LLM_API_KEY")
|
||||
|
||||
# Get UAV API key from args or environment
|
||||
uav_api_key = args.uav_api_key or os.getenv("UAV_API_KEY")
|
||||
|
||||
# Create agent
|
||||
try:
|
||||
agent = UAVControlAgent(
|
||||
base_url=args.base_url,
|
||||
uav_api_key=uav_api_key,
|
||||
llm_provider=llm_provider,
|
||||
llm_model=llm_model,
|
||||
llm_api_key=llm_api_key,
|
||||
llm_base_url=llm_base_url,
|
||||
temperature=args.temperature,
|
||||
verbose=not args.quiet,
|
||||
debug=args.debug
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to create agent: {e}")
|
||||
print("\nMake sure:")
|
||||
print(" - Ollama is running (if using --llm-provider ollama)")
|
||||
print(" - OPENAI_API_KEY is set (if using --llm-provider openai)")
|
||||
print(" - UAV API server is accessible")
|
||||
return 1
|
||||
|
||||
if args.command:
|
||||
# Single command mode
|
||||
result = agent.execute(args.command)
|
||||
print(result['output'])
|
||||
return 0 if result['success'] else 1
|
||||
else:
|
||||
# Interactive mode
|
||||
agent.run_interactive()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
sys.exit(main())
|
||||
365
uav_api_client.py
Normal file
365
uav_api_client.py
Normal file
@@ -0,0 +1,365 @@
|
||||
"""
|
||||
UAV API Client
|
||||
Wrapper for the UAV Control System API to simplify drone operations
|
||||
"""
|
||||
import requests
|
||||
from typing import Dict, List, Any, Tuple, Optional
|
||||
|
||||
|
||||
class UAVAPIClient:
|
||||
"""Client for interacting with the UAV Control System API"""
|
||||
|
||||
def __init__(self, base_url: str = "http://localhost:8000", api_key: Optional[str] = None):
|
||||
"""
|
||||
Initialize UAV API Client
|
||||
|
||||
Args:
|
||||
base_url: Base URL of the UAV API server
|
||||
api_key: Optional API key for authentication (defaults to USER role if not provided)
|
||||
- None or empty: USER role (basic access)
|
||||
- Valid key: SYSTEM or ADMIN role (based on key)
|
||||
"""
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.api_key = api_key
|
||||
self.headers = {}
|
||||
if self.api_key:
|
||||
self.headers['X-API-Key'] = self.api_key
|
||||
|
||||
def _request(self, method: str, endpoint: str, **kwargs) -> Any:
|
||||
"""Make HTTP request to the API"""
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
|
||||
# Merge authentication headers with any provided headers
|
||||
headers = kwargs.pop('headers', {})
|
||||
headers.update(self.headers)
|
||||
|
||||
try:
|
||||
response = requests.request(method, url, headers=headers, **kwargs)
|
||||
response.raise_for_status()
|
||||
if response.status_code == 204:
|
||||
return None
|
||||
return response.json()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 401:
|
||||
raise Exception(f"Authentication failed: Invalid API key")
|
||||
elif e.response.status_code == 403:
|
||||
error_detail = e.response.json().get('detail', 'Access denied')
|
||||
raise Exception(f"Permission denied: {error_detail}")
|
||||
raise Exception(f"API request failed: {e}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise Exception(f"API request failed: {e}")
|
||||
|
||||
# Drone Operations
|
||||
def list_drones(self) -> List[Dict[str, Any]]:
|
||||
"""Get all drones in the current session"""
|
||||
return self._request('GET', '/drones')
|
||||
|
||||
def get_all_waypoints(self) -> List[Dict[str, Any]]:
|
||||
"""Get all waypoints in the current session"""
|
||||
return self._request('GET', '/targets/type/waypoint')
|
||||
|
||||
def get_drone_status(self, drone_id: str) -> Dict[str, Any]:
|
||||
"""Get detailed status of a specific drone"""
|
||||
return self._request('GET', f'/drones/{drone_id}')
|
||||
|
||||
def take_off(self, drone_id: str, altitude: float = 10.0) -> Dict[str, Any]:
|
||||
"""Command drone to take off to specified altitude"""
|
||||
return self._request('POST', f'/drones/{drone_id}/command/take_off',params={'altitude': altitude})
|
||||
|
||||
def land(self, drone_id: str) -> Dict[str, Any]:
|
||||
"""Command drone to land at current position"""
|
||||
return self._request('POST', f'/drones/{drone_id}/command/land')
|
||||
|
||||
def move_to(self, drone_id: str, x: float, y: float, z: float) -> Dict[str, Any]:
|
||||
"""Move drone to specific coordinates"""
|
||||
return self._request('POST', f'/drones/{drone_id}/command/move_to',
|
||||
params={'x': x, 'y': y, 'z': z})
|
||||
|
||||
def optimal_way_to(self, drone_id: str, x: float, y: float, z: float, min_safe_height: float = 0.5) -> List[Dict[str, float]]:
|
||||
"""
|
||||
计算到达目标点的最优路径(仅水平绕行)。
|
||||
"""
|
||||
# 1. 目标高度检查
|
||||
if z < min_safe_height:
|
||||
print(f"Error: Target altitude {z}m is too low.")
|
||||
return []
|
||||
|
||||
status = self.get_drone_status(drone_id)
|
||||
start_pos = status['position']
|
||||
start_coords = (start_pos['x'], start_pos['y'], start_pos['z'])
|
||||
end_coords = (x, y, z)
|
||||
|
||||
# 2. 起点高度检查
|
||||
if start_coords[2] < min_safe_height:
|
||||
print(f"Warning: Drone is currently below safe height!")
|
||||
|
||||
# 3. 执行递归搜索
|
||||
path_points = self._find_path_recursive(
|
||||
start_coords,
|
||||
end_coords,
|
||||
avoidance_radius=2.0, # 初始绕行半径
|
||||
depth=0,
|
||||
max_depth=4, # 最大递归深度
|
||||
min_safe_height=min_safe_height
|
||||
)
|
||||
|
||||
if path_points is None:
|
||||
print(f"Error: Unable to find a collision-free path.")
|
||||
return []
|
||||
|
||||
# 4. 格式化输出
|
||||
formatted_path = [{"x": p[0], "y": p[1], "z": p[2]} for p in path_points]
|
||||
for point in formatted_path:
|
||||
self.move_to(drone_id, **point)
|
||||
return formatted_path
|
||||
|
||||
# --- 递归核心 ---
|
||||
def _find_path_recursive(self, start: Tuple[float, float, float], end: Tuple[float, float, float],
|
||||
avoidance_radius: float, depth: int, max_depth: int,
|
||||
min_safe_height: float) -> Optional[List[Tuple[float, float, float]]]:
|
||||
sx, sy, sz = start
|
||||
ex, ey, ez = end
|
||||
|
||||
# 1. 检查直连是否有碰撞
|
||||
collision = self.check_path_collision(sx, sy, sz, ex, ey, ez)
|
||||
if not collision:
|
||||
return [end]
|
||||
|
||||
# 2. 达到最大深度则停止
|
||||
if depth >= max_depth:
|
||||
return None
|
||||
|
||||
# 3. 计算路径中点
|
||||
mid_point = ((sx + ex) / 2, (sy + ey) / 2, (sz + ez) / 2)
|
||||
|
||||
# 随着深度增加,减小绕行半径,进行更精细的搜索
|
||||
current_radius = avoidance_radius / (1 + 0.5 * depth)
|
||||
|
||||
# 4. 获取仅包含左右方向的候选点
|
||||
candidates = self._get_horizontal_avoidance_points(start, end, mid_point, current_radius)
|
||||
|
||||
# 5. 遍历候选点
|
||||
for candidate in candidates:
|
||||
# 过滤掉非法高度的点 (虽然水平偏移理论上不改变高度,但以防万一)
|
||||
if candidate[2] < min_safe_height:
|
||||
continue
|
||||
|
||||
# 递归处理:起点 -> 候选点
|
||||
path_first = self._find_path_recursive(start, candidate, avoidance_radius, depth + 1, max_depth, min_safe_height)
|
||||
|
||||
if path_first is not None:
|
||||
# 递归处理:候选点 -> 终点
|
||||
path_second = self._find_path_recursive(candidate, end, avoidance_radius, depth + 1, max_depth, min_safe_height)
|
||||
|
||||
if path_second is not None:
|
||||
# 路径拼接
|
||||
return path_first + path_second
|
||||
|
||||
# 所有左右尝试都失败
|
||||
return None
|
||||
|
||||
# --- 向量计算 (核心修改部分) ---
|
||||
def _get_horizontal_avoidance_points(self, start, end, mid, radius) -> List[Tuple[float, float, float]]:
|
||||
"""
|
||||
生成候选点:强制仅在水平面上进行左右偏移。
|
||||
"""
|
||||
# 1. 计算飞行方向向量 D = End - Start
|
||||
dx = end[0] - start[0]
|
||||
dy = end[1] - start[1]
|
||||
# dz 我们不关心,因为我们要在水平面找垂线
|
||||
|
||||
# 计算水平投影的长度
|
||||
dist_horizontal = (dx*dx + dy*dy)**0.5
|
||||
|
||||
rx, ry, rz = 0.0, 0.0, 0.0
|
||||
|
||||
# 2. 计算右向量 (Right Vector)
|
||||
if dist_horizontal == 0:
|
||||
# 特殊情况:垂直升降 (Start和End的x,y相同)
|
||||
# 此时"左右"没有绝对定义,我们任意选取 X 轴方向作为偏移方向
|
||||
rx, ry, rz = 1.0, 0.0, 0.0
|
||||
else:
|
||||
# 标准情况:利用 2D 向量旋转 90 度原理
|
||||
# 向量 (x, y) 顺时针旋转 90 度变为 (y, -x)
|
||||
# 归一化
|
||||
rx = dy / dist_horizontal
|
||||
ry = -dx / dist_horizontal
|
||||
rz = 0.0 # 强制 Z 轴分量为 0,保证水平
|
||||
|
||||
mx, my, mz = mid
|
||||
|
||||
# 3. 生成候选点:只生成 右(Right) 和 左(Left)
|
||||
# 注意:Right 是 (rx, ry),Left 是 (-rx, -ry)
|
||||
candidates = []
|
||||
|
||||
# 右侧点
|
||||
c1 = (mx + rx * radius, my + ry * radius, mz) # Z高度保持中点高度不变
|
||||
candidates.append(c1)
|
||||
|
||||
# 左侧点
|
||||
c2 = (mx - rx * radius, my - ry * radius, mz)
|
||||
candidates.append(c2)
|
||||
|
||||
return candidates
|
||||
|
||||
|
||||
def move_along_path(self, drone_id: str, waypoints: List[Dict[str, float]]) -> Dict[str, Any]:
|
||||
"""Move drone along a path of waypoints"""
|
||||
return self._request('POST', f'/drones/{drone_id}/command/move_along_path',
|
||||
json={'waypoints': waypoints})
|
||||
|
||||
def change_altitude(self, drone_id: str, altitude: float) -> Dict[str, Any]:
|
||||
"""Change drone altitude while maintaining X/Y position"""
|
||||
return self._request('POST', f'/drones/{drone_id}/command/change_altitude',
|
||||
params={'altitude': altitude})
|
||||
|
||||
def hover(self, drone_id: str, duration: Optional[float] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Command drone to hover at current position.
|
||||
|
||||
Args:
|
||||
drone_id: ID of the drone
|
||||
duration: Optional duration to hover in seconds
|
||||
"""
|
||||
params = {}
|
||||
if duration is not None:
|
||||
params['duration'] = duration
|
||||
return self._request('POST', f'/drones/{drone_id}/command/hover', params=params)
|
||||
|
||||
def rotate(self, drone_id: str, heading: float) -> Dict[str, Any]:
|
||||
"""Rotate drone to face specific direction (0-360 degrees)"""
|
||||
return self._request('POST', f'/drones/{drone_id}/command/rotate',
|
||||
params={'heading': heading})
|
||||
|
||||
def move_towards(self, drone_id: str, distance: float, heading: Optional[float] = None,
|
||||
dz: Optional[float] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Move drone a specific distance in a direction.
|
||||
|
||||
Args:
|
||||
drone_id: ID of the drone
|
||||
distance: Distance to move in meters
|
||||
heading: Optional heading direction (0-360). If None, uses current heading.
|
||||
dz: Optional vertical component (altitude change)
|
||||
"""
|
||||
params = {'distance': distance}
|
||||
if heading is not None:
|
||||
params['heading'] = heading
|
||||
if dz is not None:
|
||||
params['dz'] = dz
|
||||
return self._request('POST', f'/drones/{drone_id}/command/move_towards', params=params)
|
||||
|
||||
def return_home(self, drone_id: str) -> Dict[str, Any]:
|
||||
"""Command drone to return to home position"""
|
||||
return self._request('POST', f'/drones/{drone_id}/command/return_home')
|
||||
|
||||
def set_home(self, drone_id: str) -> Dict[str, Any]:
|
||||
"""Set current position as home position"""
|
||||
return self._request('POST', f'/drones/{drone_id}/command/set_home')
|
||||
|
||||
def calibrate(self, drone_id: str) -> Dict[str, Any]:
|
||||
"""Calibrate drone sensors"""
|
||||
return self._request('POST', f'/drones/{drone_id}/command/calibrate')
|
||||
|
||||
def charge(self, drone_id: str, charge_amount: float) -> Dict[str, Any]:
|
||||
"""Charge drone battery (when landed)"""
|
||||
return self._request('POST', f'/drones/{drone_id}/command/charge',
|
||||
params={'charge_amount': charge_amount})
|
||||
|
||||
def take_photo(self, drone_id: str) -> Dict[str, Any]:
|
||||
"""Take a photo with drone camera"""
|
||||
return self._request('POST', f'/drones/{drone_id}/command/take_photo')
|
||||
|
||||
def send_message(self, drone_id: str, target_drone_id: str, message: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Send a message to another drone.
|
||||
|
||||
Args:
|
||||
drone_id: ID of the sender drone
|
||||
target_drone_id: ID of the recipient drone
|
||||
message: Content of the message
|
||||
"""
|
||||
return self._request('POST', f'/drones/{drone_id}/command/send_message',
|
||||
params={'target_drone_id': target_drone_id, 'message': message})
|
||||
|
||||
def broadcast(self, drone_id: str, message: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Broadcast a message to all other drones.
|
||||
|
||||
Args:
|
||||
drone_id: ID of the sender drone
|
||||
message: Content of the message
|
||||
"""
|
||||
return self._request('POST', f'/drones/{drone_id}/command/broadcast',
|
||||
params={'message': message})
|
||||
|
||||
# Session Operations
|
||||
def get_current_session(self) -> Dict[str, Any]:
|
||||
"""Get information about current mission session"""
|
||||
return self._request('GET', '/sessions/current')
|
||||
|
||||
def get_session_data(self, session_id: str = 'current') -> Dict[str, Any]:
|
||||
"""Get all entities in a session (drones, targets, obstacles, environment)"""
|
||||
return self._request('GET', f'/sessions/{session_id}/data')
|
||||
|
||||
def get_task_progress(self, session_id: str = 'current') -> Dict[str, Any]:
|
||||
"""Get mission task completion progress"""
|
||||
return self._request('GET', f'/sessions/{session_id}/task-progress')
|
||||
|
||||
# Environment Operations
|
||||
def get_weather(self) -> Dict[str, Any]:
|
||||
"""Get current weather conditions"""
|
||||
return self._request('GET', '/environments/current')
|
||||
|
||||
def get_targets(self) -> List[Dict[str, Any]]:
|
||||
"""Get all targets in the session"""
|
||||
fixed = self._request('GET', '/targets/type/fixed')
|
||||
moving = self._request('GET', '/targets/type/moving')
|
||||
waypoint = self._request('GET', '/targets/type/waypoint')
|
||||
circle = self._request('GET', '/targets/type/circle')
|
||||
polygen = self._request('GET', '/targets/type/polygon')
|
||||
return fixed + moving + waypoint + circle + polygen
|
||||
|
||||
def get_waypoints(self) -> List[Dict[str, Any]]:
|
||||
"""Get all charging station waypoints"""
|
||||
return self._request('GET', '/targets/type/waypoint')
|
||||
|
||||
def get_nearest_waypoint(self, x: str, y: str, z: str) -> Dict[str, Any]:
|
||||
"""Get nearest charging station waypoint"""
|
||||
return self._request('GET', '/targets/waypoints/nearest',
|
||||
json={'x': x, 'y': y, 'z': z})
|
||||
|
||||
def get_obstacles(self) -> List[Dict[str, Any]]:
|
||||
"""Get all obstacles in the session"""
|
||||
point = self._request('GET', '/obstacles/type/point')
|
||||
circle = self._request('GET', '/obstacles/type/circle')
|
||||
polygon = self._request('GET', '/obstacles/type/polygon')
|
||||
ellipse = self._request('GET', '/obstacles/type/ellipse')
|
||||
return point + circle + polygon + ellipse
|
||||
|
||||
def get_nearby_entities(self, drone_id: str) -> Dict[str, Any]:
|
||||
"""Get entities near a drone (within perceived radius)"""
|
||||
return self._request('GET', f'/drones/{drone_id}/nearby')
|
||||
|
||||
# Safety Operations
|
||||
def check_point_collision(self, x: float, y: float, z: float,
|
||||
safety_margin: float = 0.0) -> Optional[Dict[str, Any]]:
|
||||
"""Check if a point collides with any obstacle"""
|
||||
result = self._request('POST', '/obstacles/collision/check',
|
||||
json={
|
||||
'point': {'x': x, 'y': y, 'z': z},
|
||||
'safety_margin': safety_margin
|
||||
})
|
||||
return result
|
||||
|
||||
def check_path_collision(self, start_x: float, start_y: float, start_z: float,
|
||||
end_x: float, end_y: float, end_z: float,
|
||||
safety_margin: float = 1.0) -> Optional[Dict[str, Any]]:
|
||||
"""Check if a path intersects any obstacle"""
|
||||
result = self._request('POST', '/obstacles/collision/path',
|
||||
json={
|
||||
'start': {'x': start_x, 'y': start_y, 'z': start_z},
|
||||
'end': {'x': end_x, 'y': end_y, 'z': end_z},
|
||||
'safety_margin': safety_margin
|
||||
})
|
||||
return result
|
||||
649
uav_langchain_tools.py
Normal file
649
uav_langchain_tools.py
Normal file
@@ -0,0 +1,649 @@
|
||||
"""
|
||||
LangChain Tools for UAV Control
|
||||
Wraps the UAV API client as LangChain tools using @tool decorator
|
||||
All tools accept JSON string input for consistent parameter handling
|
||||
"""
|
||||
from langchain.tools import tool
|
||||
from uav_api_client import UAVAPIClient
|
||||
import json
|
||||
|
||||
|
||||
def create_uav_tools(client: UAVAPIClient) -> list:
|
||||
"""
|
||||
Create all UAV control tools for LangChain agent using @tool decorator
|
||||
All tools that require parameters accept a JSON string input
|
||||
"""
|
||||
|
||||
# ========== Information Gathering Tools (No Parameters) ==========
|
||||
|
||||
@tool
|
||||
def list_drones() -> str:
|
||||
"""List all available drones in the current session with their status, battery level, and position.
|
||||
Use this to see what drones are available before trying to control them.
|
||||
|
||||
No input required."""
|
||||
try:
|
||||
drones = client.list_drones()
|
||||
return json.dumps(drones, indent=2)
|
||||
except Exception as e:
|
||||
return f"Error listing drones: {str(e)}"
|
||||
|
||||
@tool
|
||||
def get_session_info() -> str:
|
||||
"""Get current session information including task type, statistics, and status.
|
||||
Use this to understand what mission you need to complete.
|
||||
|
||||
No input required."""
|
||||
try:
|
||||
session = client.get_current_session()
|
||||
return json.dumps(session, indent=2)
|
||||
except Exception as e:
|
||||
return f"Error getting session info: {str(e)}"
|
||||
|
||||
@tool
|
||||
def get_session_data() -> str:
|
||||
"""Get all session data including drones, targets, and obstacles.
|
||||
Use this to understand the environment and plan your mission.
|
||||
|
||||
No input required."""
|
||||
try:
|
||||
session_data = client.get_session_data()
|
||||
return json.dumps(session_data, indent=2)
|
||||
except Exception as e:
|
||||
return f"Error getting session data: {str(e)}"
|
||||
|
||||
@tool
|
||||
def get_task_progress() -> str:
|
||||
"""Get mission task progress including completion percentage and status.
|
||||
Use this to track mission completion and see how close you are to finishing.
|
||||
|
||||
No input required."""
|
||||
try:
|
||||
progress = client.get_task_progress()
|
||||
return json.dumps(progress, indent=2)
|
||||
except Exception as e:
|
||||
return f"Error getting task progress: {str(e)}"
|
||||
|
||||
@tool
|
||||
def get_weather() -> str:
|
||||
"""Get current weather conditions including wind speed, visibility, and weather type.
|
||||
Check this before takeoff to ensure safe flying conditions.
|
||||
|
||||
No input required."""
|
||||
try:
|
||||
weather = client.get_weather()
|
||||
return json.dumps(weather, indent=2)
|
||||
except Exception as e:
|
||||
return f"Error getting weather: {str(e)}"
|
||||
|
||||
@tool
|
||||
def get_targets() -> str:
|
||||
"""Get all targets in the session including fixed, moving, waypoint, circle and polygon to search or patrol.
|
||||
Use this to see what targets you need to visit.
|
||||
|
||||
No input required."""
|
||||
try:
|
||||
targets = client.get_targets()
|
||||
return json.dumps(targets, indent=2)
|
||||
except Exception as e:
|
||||
return f"Error getting targets: {str(e)}"
|
||||
|
||||
@tool
|
||||
def get_all_waypoints() -> str:
|
||||
"""Get all waypoints in the session including coordinates and altitude.
|
||||
Use this to understand the where to charge that drones will follow.
|
||||
|
||||
No input required."""
|
||||
try:
|
||||
waypoints = client.get_all_waypoints()
|
||||
return json.dumps(waypoints, indent=2)
|
||||
except Exception as e:
|
||||
return f"Error getting waypoints: {str(e)}"
|
||||
|
||||
@tool
|
||||
def get_obstacles() -> str:
|
||||
"""Get all obstacles in the session that drones must avoid.
|
||||
Use this to understand what obstacles exist in the environment.
|
||||
|
||||
No input required."""
|
||||
try:
|
||||
obstacles = client.get_obstacles()
|
||||
return json.dumps(obstacles, indent=2)
|
||||
except Exception as e:
|
||||
return f"Error getting obstacles: {str(e)}"
|
||||
|
||||
|
||||
@tool
|
||||
def get_drone_status(input_json: str) -> str:
|
||||
"""Get detailed status of a specific drone including position, battery, heading, and visited targets.
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the drone (required)
|
||||
|
||||
Example: {{"drone_id": "drone-001"}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
|
||||
status = client.get_drone_status(drone_id)
|
||||
return json.dumps(status, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\"}}"
|
||||
except Exception as e:
|
||||
return f"Error getting drone status: {str(e)}"
|
||||
|
||||
@tool
|
||||
def get_nearby_entities(input_json: str) -> str:
|
||||
"""Get drones, targets, and obstacles near a specific drone (within its perception radius).
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the drone (required)
|
||||
|
||||
Example: {{"drone_id": "drone-001"}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
|
||||
nearby = client.get_nearby_entities(drone_id)
|
||||
return json.dumps(nearby, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\"}}"
|
||||
except Exception as e:
|
||||
return f"Error getting nearby entities: {str(e)}"
|
||||
|
||||
@tool
|
||||
def land(input_json: str) -> str:
|
||||
"""Command a drone to land at its current position.
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the drone (required)
|
||||
|
||||
Example: {{"drone_id": "drone-001"}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
|
||||
result = client.land(drone_id)
|
||||
return json.dumps(result, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\"}}"
|
||||
except Exception as e:
|
||||
return f"Error during landing: {str(e)}"
|
||||
|
||||
@tool
|
||||
def hover(input_json: str) -> str:
|
||||
"""Command a drone to hover at its current position.
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the drone (required)
|
||||
- duration: Optional duration in seconds to hover (optional)
|
||||
|
||||
Example: {{"drone_id": "drone-001", "duration": 5.0}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
duration = params.get('duration')
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
|
||||
result = client.hover(drone_id, duration)
|
||||
return json.dumps(result, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\"}}"
|
||||
except Exception as e:
|
||||
return f"Error hovering: {str(e)}"
|
||||
|
||||
@tool
|
||||
def return_home(input_json: str) -> str:
|
||||
"""Command a drone to return to its home position.
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the drone (required)
|
||||
|
||||
Example: {{"drone_id": "drone-001"}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
|
||||
result = client.return_home(drone_id)
|
||||
return json.dumps(result, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\"}}"
|
||||
except Exception as e:
|
||||
return f"Error returning home: {str(e)}"
|
||||
|
||||
@tool
|
||||
def set_home(input_json: str) -> str:
|
||||
"""Set the drone's current position as its new home position.
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the drone (required)
|
||||
|
||||
Example: {{"drone_id": "drone-001"}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
|
||||
result = client.set_home(drone_id)
|
||||
return json.dumps(result, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\"}}"
|
||||
except Exception as e:
|
||||
return f"Error setting home: {str(e)}"
|
||||
|
||||
@tool
|
||||
def calibrate(input_json: str) -> str:
|
||||
"""Calibrate the drone's sensors.
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the drone (required)
|
||||
|
||||
Example: {{"drone_id": "drone-001"}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
|
||||
result = client.calibrate(drone_id)
|
||||
return json.dumps(result, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\"}}"
|
||||
except Exception as e:
|
||||
return f"Error calibrating: {str(e)}"
|
||||
|
||||
@tool
|
||||
def take_photo(input_json: str) -> str:
|
||||
"""Command a drone to take a photo.
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the drone (required)
|
||||
|
||||
Example: {{"drone_id": "drone-001"}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
|
||||
result = client.take_photo(drone_id)
|
||||
return json.dumps(result, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\"}}"
|
||||
except Exception as e:
|
||||
return f"Error taking photo: {str(e)}"
|
||||
|
||||
# ========== Two Parameter Tools ==========
|
||||
|
||||
@tool
|
||||
def take_off(input_json: str) -> str:
|
||||
"""Command a drone to take off to a specified altitude.
|
||||
Drone must be on ground (idle or ready status).
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the drone (required)
|
||||
- altitude: Target altitude in meters (optional, default: 10.0)
|
||||
|
||||
Example: {{"drone_id": "drone-001", "altitude": 15.0}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
altitude = params.get('altitude', 10.0)
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
|
||||
result = client.take_off(drone_id, altitude)
|
||||
return json.dumps(result, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\", \"altitude\": 15.0}}"
|
||||
except Exception as e:
|
||||
return f"Error during takeoff: {str(e)}"
|
||||
|
||||
@tool
|
||||
def change_altitude(input_json: str) -> str:
|
||||
"""Change a drone's altitude while maintaining X/Y position.
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the drone (required)
|
||||
- altitude: Target altitude in meters (required)
|
||||
|
||||
Example: {{"drone_id": "drone-001", "altitude": 20.0}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
altitude = params.get('altitude')
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
if altitude is None:
|
||||
return "Error: altitude is required"
|
||||
|
||||
result = client.change_altitude(drone_id, altitude)
|
||||
return json.dumps(result, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\", \"altitude\": 20.0}}"
|
||||
except Exception as e:
|
||||
return f"Error changing altitude: {str(e)}"
|
||||
|
||||
@tool
|
||||
def rotate(input_json: str) -> str:
|
||||
"""Rotate a drone to face a specific direction.
|
||||
0=North, 90=East, 180=South, 270=West.
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the drone (required)
|
||||
- heading: Target heading in degrees 0-360 (required)
|
||||
|
||||
Example: {{"drone_id": "drone-001", "heading": 90.0}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
heading = params.get('heading')
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
if heading is None:
|
||||
return "Error: heading is required"
|
||||
|
||||
result = client.rotate(drone_id, heading)
|
||||
return json.dumps(result, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\", \"heading\": 90.0}}"
|
||||
except Exception as e:
|
||||
return f"Error rotating: {str(e)}"
|
||||
|
||||
@tool
|
||||
def send_message(input_json: str) -> str:
|
||||
"""Send a message from one drone to another.
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the sender drone (required)
|
||||
- target_drone_id: The ID of the recipient drone (required)
|
||||
- message: The message content (required)
|
||||
|
||||
Example: {{"drone_id": "drone-001", "target_drone_id": "drone-002", "message": "Hello"}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
target_drone_id = params.get('target_drone_id')
|
||||
message = params.get('message')
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
if not target_drone_id:
|
||||
return "Error: target_drone_id is required"
|
||||
if not message:
|
||||
return "Error: message is required"
|
||||
|
||||
result = client.send_message(drone_id, target_drone_id, message)
|
||||
return json.dumps(result, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\", \"target_drone_id\": \"drone-002\", \"message\": \"...\"}}"
|
||||
except Exception as e:
|
||||
return f"Error sending message: {str(e)}"
|
||||
|
||||
@tool
|
||||
def broadcast(input_json: str) -> str:
|
||||
"""Broadcast a message from one drone to all other drones.
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the sender drone (required)
|
||||
- message: The message content (required)
|
||||
|
||||
Example: {{"drone_id": "drone-001", "message": "Alert"}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
message = params.get('message')
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
if not message:
|
||||
return "Error: message is required"
|
||||
|
||||
result = client.broadcast(drone_id, message)
|
||||
return json.dumps(result, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\", \"message\": \"...\"}}"
|
||||
except Exception as e:
|
||||
return f"Error broadcasting: {str(e)}"
|
||||
|
||||
@tool
|
||||
def charge(input_json: str) -> str:
|
||||
"""Command a drone to charge its battery.
|
||||
Drone must be landed at a charging station.
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the drone (required)
|
||||
- charge_amount: Amount to charge in percent (required)
|
||||
|
||||
Example: {{"drone_id": "drone-001", "charge_amount": 25.0}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
charge_amount = params.get('charge_amount')
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
if charge_amount is None:
|
||||
return "Error: charge_amount is required"
|
||||
|
||||
result = client.charge(drone_id, charge_amount)
|
||||
return json.dumps(result, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\", \"charge_amount\": 25.0}}"
|
||||
except Exception as e:
|
||||
return f"Error charging: {str(e)}"
|
||||
|
||||
@tool
|
||||
def move_towards(input_json: str) -> str:
|
||||
"""Move a drone a specific distance in a direction.
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the drone (required)
|
||||
- distance: Distance to move in meters (required)
|
||||
- heading: Heading direction in degrees 0-360 (optional, default: current heading)
|
||||
- dz: Vertical component in meters (optional)
|
||||
|
||||
Example: {{"drone_id": "drone-001", "distance": 10.0, "heading": 90.0}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
distance = params.get('distance')
|
||||
heading = params.get('heading')
|
||||
dz = params.get('dz')
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
if distance is None:
|
||||
return "Error: distance is required"
|
||||
|
||||
result = client.move_towards(drone_id, distance, heading, dz)
|
||||
return json.dumps(result, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\", \"distance\": 10.0}}"
|
||||
except Exception as e:
|
||||
return f"Error moving towards: {str(e)}"
|
||||
|
||||
# @tool
|
||||
# def move_along_path(input_json: str) -> str:
|
||||
# """Move a drone along a path of waypoints.
|
||||
|
||||
# Input should be a JSON string with:
|
||||
# - drone_id: The ID of the drone (required)
|
||||
# - waypoints: List of points with x, y, z coordinates (required)
|
||||
|
||||
# Example: {{"drone_id": "drone-001", "waypoints": [{{"x": 10, "y": 10, "z": 10}}, {{"x": 20, "y": 20, "z": 10}}]}}
|
||||
# """
|
||||
# try:
|
||||
# params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
# drone_id = params.get('drone_id')
|
||||
# waypoints = params.get('waypoints')
|
||||
|
||||
# if not drone_id:
|
||||
# return "Error: drone_id is required"
|
||||
# if not waypoints:
|
||||
# return "Error: waypoints list is required"
|
||||
|
||||
# result = client.move_along_path(drone_id, waypoints)
|
||||
# return json.dumps(result, indent=2)
|
||||
# except json.JSONDecodeError as e:
|
||||
# return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\", \"waypoints\": [...]}}"
|
||||
# except Exception as e:
|
||||
# return f"Error moving along path: {str(e)}"
|
||||
|
||||
# ========== Multi-Parameter Tools ==========
|
||||
|
||||
@tool
|
||||
def get_nearest_waypoint(input_json: str) -> str:
|
||||
"""Get the nearest waypoint to a specific drone.
|
||||
Input should be a JSON string with:
|
||||
- x: The x-coordinate of the drone (required)
|
||||
- y: The y-coordinate of the drone (required)
|
||||
- z: The z-coordinate of the drone (required)
|
||||
|
||||
Example: {{"x": 0.0, "y": 0.0, "z": 0.0}}"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
x = params.get('x')
|
||||
y = params.get('y')
|
||||
z = params.get('z')
|
||||
|
||||
if x is None or y is None or z is None:
|
||||
return "Error: x, y, and z coordinates are required"
|
||||
nearest = client.get_nearest_waypoint(x, y, z)
|
||||
return json.dumps(nearest, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}}"
|
||||
except Exception as e:
|
||||
return f"Error getting nearest waypoint: {str(e)}"
|
||||
|
||||
@tool
|
||||
def move_to(input_json: str) -> str:
|
||||
"""Move a drone to specific 3D coordinates (x, y, z).
|
||||
Always check for collisions first using check_path_collision.
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the drone (required)
|
||||
- x: Target X coordinate in meters (required)
|
||||
- y: Target Y coordinate in meters (required)
|
||||
- z: Target Z coordinate (altitude) in meters (required)
|
||||
|
||||
Example: {{"drone_id": "drone-001", "x": 100.0, "y": 50.0, "z": 20.0}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
x = params.get('x')
|
||||
y = params.get('y')
|
||||
z = params.get('z')
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
if x is None or y is None or z is None:
|
||||
return "Error: x, y, and z coordinates are required"
|
||||
|
||||
result = client.move_to(drone_id, x, y, z)
|
||||
return json.dumps(result, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\", \"x\": 100.0, \"y\": 50.0, \"z\": 20.0}}"
|
||||
except Exception as e:
|
||||
return f"Error moving drone: {str(e)}"
|
||||
|
||||
@tool
|
||||
def optimal_way_to(input_json: str) -> str:
|
||||
"""Get the optimal path to a specific 3D coordinates (x, y, z).
|
||||
Always check for collisions first using check_path_collision.
|
||||
|
||||
Input should be a JSON string with:
|
||||
- drone_id: The ID of the drone (required)
|
||||
- x: Target X coordinate in meters (required)
|
||||
- y: Target Y coordinate in meters (required)
|
||||
- z: Target Z coordinate (altitude) in meters (required)
|
||||
|
||||
Example: {{"drone_id": "drone-001", "x": 100.0, "y": 50.0, "z": 20.0}}
|
||||
"""
|
||||
try:
|
||||
params = json.loads(input_json) if isinstance(input_json, str) else input_json
|
||||
drone_id = params.get('drone_id')
|
||||
x = params.get('x')
|
||||
y = params.get('y')
|
||||
z = params.get('z')
|
||||
|
||||
if not drone_id:
|
||||
return "Error: drone_id is required"
|
||||
if x is None or y is None or z is None:
|
||||
return "Error: x, y, and z coordinates are required"
|
||||
|
||||
result = client.optimal_way_to(drone_id, x, y, z)
|
||||
return json.dumps(result, indent=2)
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Error parsing JSON input: {str(e)}. Expected format: {{\"drone_id\": \"drone-001\", \"x\": 100.0, \"y\": 50.0, \"z\": 20.0}}"
|
||||
except Exception as e:
|
||||
return f"Error moving drone: {str(e)}"
|
||||
|
||||
|
||||
# Return all tools
|
||||
return [
|
||||
list_drones,
|
||||
get_drone_status,
|
||||
get_session_info,
|
||||
# get_session_data,
|
||||
get_task_progress,
|
||||
get_weather,
|
||||
# get_targets,
|
||||
get_obstacles,
|
||||
get_nearby_entities,
|
||||
take_off,
|
||||
land,
|
||||
move_to,
|
||||
# optimal_way_to,
|
||||
move_towards,
|
||||
change_altitude,
|
||||
hover,
|
||||
rotate,
|
||||
return_home,
|
||||
set_home,
|
||||
calibrate,
|
||||
take_photo,
|
||||
send_message,
|
||||
broadcast,
|
||||
charge,
|
||||
get_nearest_waypoint,
|
||||
get_all_waypoints,
|
||||
get_targets
|
||||
]
|
||||
Reference in New Issue
Block a user