Spaces:
No application file
No application file
Upload folder using huggingface_hub
Browse files- .gitignore +3 -0
- .gradio/certificate.pem +31 -0
- DAG_VISUALIZATION_README.md +235 -0
- EDITING_WORKFLOW_GUIDE.md +136 -0
- config.py +459 -0
- dag_demo.py +100 -0
- dag_visualizer.py +334 -0
- gradio_llm_interface.py +298 -0
- json_processor.py +33 -0
- llm_request_handler.py +278 -0
- main.py +348 -0
- monitor_topics.sh +44 -0
- prompts/VoxPoser/composer_prompt.txt +120 -0
- prompts/VoxPoser/get_affordance_map_prompt.txt +153 -0
- prompts/VoxPoser/get_avoidance_map_prompt.txt +36 -0
- prompts/VoxPoser/get_gripper_map_prompt.txt +48 -0
- prompts/VoxPoser/get_rotation_map_prompt.txt +53 -0
- prompts/VoxPoser/get_velocity_map_prompt.txt +31 -0
- prompts/VoxPoser/parse_query_obj_prompt.txt +66 -0
- prompts/VoxPoser/planner_prompt.txt +115 -0
- prompts/swarm/composer_prompt.txt +128 -0
- prompts/swarm/dart.txt +169 -0
- prompts/swarm/instruction_translator_prompt.txt +125 -0
- prompts/swarm/planner_prompt.txt +109 -0
- prompts/swarm/task_2_commannd_prompt.txt +564 -0
- prompts/swarm/task_decomposer_prompt.txt +99 -0
- readme.md +88 -0
- requirements.txt +47 -0
- ros_message_server.py +76 -0
- ros_node_publisher.py +71 -0
- test_dag_visualization.py +175 -0
- test_editing_workflow.py +235 -0
- test_persistent_editing.py +192 -0
- test_safety_workflow.py +141 -0
.gitignore
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
__pycache__/
|
2 |
+
venv/
|
3 |
+
.env
|
.gradio/certificate.pem
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
-----BEGIN CERTIFICATE-----
|
2 |
+
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
3 |
+
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
4 |
+
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
5 |
+
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
6 |
+
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
7 |
+
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
8 |
+
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
9 |
+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
10 |
+
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
11 |
+
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
12 |
+
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
13 |
+
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
14 |
+
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
15 |
+
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
16 |
+
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
17 |
+
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
18 |
+
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
19 |
+
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
20 |
+
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
21 |
+
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
22 |
+
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
23 |
+
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
24 |
+
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
25 |
+
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
26 |
+
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
27 |
+
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
28 |
+
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
29 |
+
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
30 |
+
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
31 |
+
-----END CERTIFICATE-----
|
DAG_VISUALIZATION_README.md
ADDED
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# DAG Visualization for QA_LLM_Module
|
2 |
+
|
3 |
+
## Overview
|
4 |
+
|
5 |
+
This enhancement adds DAG (Directed Acyclic Graph) visualization capability to the QA_LLM_Module, allowing users to see visual representations of robot task dependencies generated by the LLM.
|
6 |
+
|
7 |
+
## Features
|
8 |
+
|
9 |
+
- **Automatic DAG Generation**: Visualizations are generated automatically when the LLM outputs valid task JSON
|
10 |
+
- **IEEE-Style Formatting**: Professional, publication-ready graphs
|
11 |
+
- **Hierarchical Layout**: Tasks are organized by dependency levels
|
12 |
+
- **Color-Coded Nodes**:
|
13 |
+
- π΄ Red: Start tasks (no dependencies)
|
14 |
+
- π£ Purple: End tasks (no dependents)
|
15 |
+
- π Orange: Intermediate tasks
|
16 |
+
- **Detailed Information**: Each task shows function name, assigned robots, and object keywords
|
17 |
+
- **Two Visualization Modes**: Detailed and simplified versions
|
18 |
+
- **π Safety Confirmation Workflow**:
|
19 |
+
- Task plans require operator approval before deployment
|
20 |
+
- "Validate & Deploy Task Plan" button for safety compliance
|
21 |
+
- Visual status updates (Pending β Approved & Deployed)
|
22 |
+
- Prevents accidental execution of unverified plans
|
23 |
+
|
24 |
+
## Installation
|
25 |
+
|
26 |
+
The required dependencies are already added to `requirements.txt`:
|
27 |
+
|
28 |
+
```bash
|
29 |
+
pip install matplotlib==3.9.4
|
30 |
+
pip install networkx==3.4
|
31 |
+
```
|
32 |
+
|
33 |
+
## Usage
|
34 |
+
|
35 |
+
### Automatic Integration with Safety Workflow
|
36 |
+
|
37 |
+
When you run the main application and input a query that generates task JSON:
|
38 |
+
|
39 |
+
1. **DAG Generation**: The visualization appears automatically with "Pending Approval" status
|
40 |
+
2. **Safety Review**: Operator reviews the task dependency graph
|
41 |
+
3. **Confirmation**: Click "π Validate & Deploy Task Plan" to send to construction site
|
42 |
+
4. **Deployment**: Graph updates to "APPROVED & DEPLOYED" status
|
43 |
+
|
44 |
+
```bash
|
45 |
+
python3 main.py
|
46 |
+
```
|
47 |
+
|
48 |
+
### Enhanced Safety Workflow Steps
|
49 |
+
|
50 |
+
1. π€ **LLM generates task plan** β DAG shows "Pending Approval"
|
51 |
+
2. π¨βπΌ **Operator reviews visualization** β Confirms task safety and correctness
|
52 |
+
3. π **[Optional] Edit Task Plan** β Manual JSON editing if modifications needed
|
53 |
+
4. π **[Optional] Update DAG Visualization** β Regenerate graph with edits
|
54 |
+
5. π **Validate & Deploy Task Plan** β Final approval and deployment to robots
|
55 |
+
6. β
**Confirmation displayed** β DAG shows "APPROVED & DEPLOYED"
|
56 |
+
|
57 |
+
### Three-Button IEEE-Style Workflow
|
58 |
+
|
59 |
+
- **π Edit Task Plan**: Opens JSON editor for manual modifications
|
60 |
+
- **π Update DAG Visualization**: Regenerates graph from edited JSON
|
61 |
+
- **π Validate & Deploy Task Plan**: Final safety approval and deployment
|
62 |
+
|
63 |
+
### Manual Testing
|
64 |
+
|
65 |
+
You can test the DAG visualization independently:
|
66 |
+
|
67 |
+
```bash
|
68 |
+
python3 test_dag_visualization.py
|
69 |
+
python3 dag_demo.py
|
70 |
+
```
|
71 |
+
|
72 |
+
## API Reference
|
73 |
+
|
74 |
+
### DAGVisualizer Class
|
75 |
+
|
76 |
+
```python
|
77 |
+
from dag_visualizer import DAGVisualizer
|
78 |
+
|
79 |
+
visualizer = DAGVisualizer()
|
80 |
+
```
|
81 |
+
|
82 |
+
#### Methods
|
83 |
+
|
84 |
+
##### `create_dag_visualization(task_data, title="Robot Task Dependency Graph")`
|
85 |
+
Creates a detailed DAG visualization with full task information.
|
86 |
+
|
87 |
+
**Parameters:**
|
88 |
+
- `task_data` (dict): Task data in the expected JSON format
|
89 |
+
- `title` (str): Title for the graph
|
90 |
+
|
91 |
+
**Returns:**
|
92 |
+
- `str`: Path to the generated PNG image, or `None` if failed
|
93 |
+
|
94 |
+
##### `create_simplified_dag_visualization(task_data, title="Robot Task Graph")`
|
95 |
+
Creates a simplified DAG visualization suitable for smaller displays.
|
96 |
+
|
97 |
+
**Parameters:**
|
98 |
+
- `task_data` (dict): Task data in the expected JSON format
|
99 |
+
- `title` (str): Title for the graph
|
100 |
+
|
101 |
+
**Returns:**
|
102 |
+
- `str`: Path to the generated PNG image, or `None` if failed
|
103 |
+
|
104 |
+
## Expected JSON Format
|
105 |
+
|
106 |
+
The visualizer expects task data in the following format:
|
107 |
+
|
108 |
+
```json
|
109 |
+
{
|
110 |
+
"tasks": [
|
111 |
+
{
|
112 |
+
"task": "task_name",
|
113 |
+
"instruction_function": {
|
114 |
+
"name": "function_name",
|
115 |
+
"robot_ids": ["robot_id_1", "robot_id_2"],
|
116 |
+
"dependencies": ["dependency_task_name"],
|
117 |
+
"object_keywords": ["object1", "object2"]
|
118 |
+
}
|
119 |
+
}
|
120 |
+
]
|
121 |
+
}
|
122 |
+
```
|
123 |
+
|
124 |
+
## Examples
|
125 |
+
|
126 |
+
### Single Task
|
127 |
+
```json
|
128 |
+
{
|
129 |
+
"tasks": [
|
130 |
+
{
|
131 |
+
"task": "target_area_for_specific_robots_1",
|
132 |
+
"instruction_function": {
|
133 |
+
"name": "target_area_for_specific_robots",
|
134 |
+
"robot_ids": ["robot_dump_truck_01"],
|
135 |
+
"dependencies": [],
|
136 |
+
"object_keywords": ["puddle1"]
|
137 |
+
}
|
138 |
+
}
|
139 |
+
]
|
140 |
+
}
|
141 |
+
```
|
142 |
+
|
143 |
+
### Multiple Tasks with Dependencies
|
144 |
+
```json
|
145 |
+
{
|
146 |
+
"tasks": [
|
147 |
+
{
|
148 |
+
"task": "excavate_soil",
|
149 |
+
"instruction_function": {
|
150 |
+
"name": "excavate_soil_from_pile",
|
151 |
+
"robot_ids": ["robot_excavator_01"],
|
152 |
+
"dependencies": [],
|
153 |
+
"object_keywords": ["soil_pile"]
|
154 |
+
}
|
155 |
+
},
|
156 |
+
{
|
157 |
+
"task": "transport_soil",
|
158 |
+
"instruction_function": {
|
159 |
+
"name": "transport_soil_to_pit",
|
160 |
+
"robot_ids": ["robot_dump_truck_01"],
|
161 |
+
"dependencies": ["excavate_soil"],
|
162 |
+
"object_keywords": ["pit"]
|
163 |
+
}
|
164 |
+
}
|
165 |
+
]
|
166 |
+
}
|
167 |
+
```
|
168 |
+
|
169 |
+
## File Structure
|
170 |
+
|
171 |
+
```
|
172 |
+
QA_LLM_Module/
|
173 |
+
βββ dag_visualizer.py # Main visualization module
|
174 |
+
βββ test_dag_visualization.py # Test suite
|
175 |
+
βββ dag_demo.py # Demo script
|
176 |
+
βββ gradio_llm_interface.py # Updated with DAG integration
|
177 |
+
βββ main.py # Updated Gradio interface
|
178 |
+
βββ requirements.txt # Updated dependencies
|
179 |
+
```
|
180 |
+
|
181 |
+
## Integration Details
|
182 |
+
|
183 |
+
### Modified Files
|
184 |
+
|
185 |
+
1. **gradio_llm_interface.py**:
|
186 |
+
- Added `DAGVisualizer` import
|
187 |
+
- Modified `predict()` method to generate visualizations
|
188 |
+
- Returns additional DAG image output
|
189 |
+
|
190 |
+
2. **main.py**:
|
191 |
+
- Added `gr.Image` component for DAG display
|
192 |
+
- Updated input/output mappings for DAG visualization
|
193 |
+
|
194 |
+
3. **requirements.txt**:
|
195 |
+
- Added `matplotlib==3.9.4`
|
196 |
+
- Added `networkx==3.4`
|
197 |
+
|
198 |
+
## Troubleshooting
|
199 |
+
|
200 |
+
### Common Issues
|
201 |
+
|
202 |
+
1. **"No module named 'matplotlib'"**
|
203 |
+
- Solution: `pip install matplotlib networkx`
|
204 |
+
|
205 |
+
2. **"No tasks found or invalid graph structure"**
|
206 |
+
- Solution: Ensure your JSON follows the expected format
|
207 |
+
|
208 |
+
3. **Images not displaying in Gradio**
|
209 |
+
- Solution: Check that the image paths are accessible and files exist
|
210 |
+
|
211 |
+
### Debug Mode
|
212 |
+
|
213 |
+
Enable detailed logging by setting the log level:
|
214 |
+
|
215 |
+
```python
|
216 |
+
from loguru import logger
|
217 |
+
logger.add("dag_debug.log", level="DEBUG")
|
218 |
+
```
|
219 |
+
|
220 |
+
## Performance Notes
|
221 |
+
|
222 |
+
- Visualizations are generated in temporary directories
|
223 |
+
- Images are automatically cleaned up by the system
|
224 |
+
- Large graphs (>20 nodes) may take longer to render
|
225 |
+
- Memory usage scales with graph complexity
|
226 |
+
|
227 |
+
## Future Enhancements
|
228 |
+
|
229 |
+
Potential improvements for future versions:
|
230 |
+
|
231 |
+
1. **Interactive Graphs**: Clickable nodes with detailed information
|
232 |
+
2. **Real-time Updates**: Live visualization updates as tasks execute
|
233 |
+
3. **Export Options**: Support for PDF, SVG, and other formats
|
234 |
+
4. **Custom Styling**: User-configurable colors and layouts
|
235 |
+
5. **Animation**: Animated execution flow visualization
|
EDITING_WORKFLOW_GUIDE.md
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# π Enhanced Editing Workflow Guide
|
2 |
+
|
3 |
+
## π§ Fixed Issues
|
4 |
+
|
5 |
+
### β
Persistent Editing Problem Solved
|
6 |
+
- **Issue**: After updating DAG, editor showed empty `{"tasks": []}` on subsequent edits
|
7 |
+
- **Solution**: Enhanced state management to preserve task plans across edit cycles
|
8 |
+
- **Result**: Task plans now persist through the complete workflow
|
9 |
+
|
10 |
+
## π Complete Workflow
|
11 |
+
|
12 |
+
### 1οΈβ£ **LLM Generation Phase**
|
13 |
+
```
|
14 |
+
User Input β LLM Response β DAG Generated β Buttons Appear
|
15 |
+
```
|
16 |
+
**Buttons shown**: `π Edit Task Plan` + `π Validate & Deploy Task Plan`
|
17 |
+
|
18 |
+
### 2οΈβ£ **Manual Editing Phase** (Optional)
|
19 |
+
```
|
20 |
+
Click "π Edit Task Plan" β JSON Editor Opens β Make Changes
|
21 |
+
```
|
22 |
+
**Buttons shown**: `π Update DAG Visualization`
|
23 |
+
|
24 |
+
### 3οΈβ£ **DAG Update Phase**
|
25 |
+
```
|
26 |
+
Click "π Update DAG Visualization" β New DAG Generated β Review Changes
|
27 |
+
```
|
28 |
+
**Buttons shown**: `π Validate & Deploy Task Plan`
|
29 |
+
|
30 |
+
### 4οΈβ£ **Deployment Phase**
|
31 |
+
```
|
32 |
+
Click "π Validate & Deploy Task Plan" β Send to ROS Topics β Confirmation
|
33 |
+
```
|
34 |
+
**Status**: `β
Task Plan Successfully Deployed`
|
35 |
+
|
36 |
+
### 5οΈβ£ **Re-editing Capability**
|
37 |
+
```
|
38 |
+
Click "π Edit Task Plan" Again β Previous Plan Loads β Continue Editing
|
39 |
+
```
|
40 |
+
**Feature**: Deployed plans can be re-edited and updated
|
41 |
+
|
42 |
+
## π― Key Features
|
43 |
+
|
44 |
+
### π **Persistent State Management**
|
45 |
+
- β
Task plans persist across edit cycles
|
46 |
+
- β
Deployed plans remain available for re-editing
|
47 |
+
- β
Intelligent fallback to templates when needed
|
48 |
+
- β
Proper error handling for malformed data
|
49 |
+
|
50 |
+
### π **Smart Editor Behavior**
|
51 |
+
- **With existing plan**: Shows current task JSON for editing
|
52 |
+
- **After deployment**: Shows deployed plan for potential re-editing
|
53 |
+
- **Empty state**: Shows example template with proper structure
|
54 |
+
- **Malformed data**: Gracefully falls back to template
|
55 |
+
|
56 |
+
### π‘οΈ **Error Handling**
|
57 |
+
- **JSON Syntax Errors**: Shows clear error messages and keeps editor open
|
58 |
+
- **Missing Fields**: Validates required structure (`tasks` field)
|
59 |
+
- **Empty Tasks**: Handles empty task arrays gracefully
|
60 |
+
- **State Corruption**: Recovers with template when state is invalid
|
61 |
+
|
62 |
+
## π Example JSON Structure
|
63 |
+
|
64 |
+
```json
|
65 |
+
{
|
66 |
+
"tasks": [
|
67 |
+
{
|
68 |
+
"task": "excavate_soil_from_pile",
|
69 |
+
"instruction_function": {
|
70 |
+
"name": "excavate_soil_from_pile",
|
71 |
+
"robot_ids": ["robot_excavator_01"],
|
72 |
+
"dependencies": [],
|
73 |
+
"object_keywords": ["soil_pile"]
|
74 |
+
}
|
75 |
+
},
|
76 |
+
{
|
77 |
+
"task": "transport_soil_to_pit",
|
78 |
+
"instruction_function": {
|
79 |
+
"name": "transport_soil_to_pit",
|
80 |
+
"robot_ids": ["robot_dump_truck_01"],
|
81 |
+
"dependencies": ["excavate_soil_from_pile"],
|
82 |
+
"object_keywords": ["pit"]
|
83 |
+
}
|
84 |
+
}
|
85 |
+
]
|
86 |
+
}
|
87 |
+
```
|
88 |
+
|
89 |
+
## π Testing Commands
|
90 |
+
|
91 |
+
### Run the Application
|
92 |
+
```bash
|
93 |
+
cd /root/share/QA_LLM_Module
|
94 |
+
python3 main.py
|
95 |
+
```
|
96 |
+
|
97 |
+
### Monitor ROS Topics
|
98 |
+
```bash
|
99 |
+
./monitor_topics.sh
|
100 |
+
```
|
101 |
+
|
102 |
+
### Test Editing Workflow
|
103 |
+
```bash
|
104 |
+
python3 test_persistent_editing.py
|
105 |
+
```
|
106 |
+
|
107 |
+
## π Success Indicators
|
108 |
+
|
109 |
+
### β
Working Correctly When:
|
110 |
+
1. **First Edit**: LLM plan loads in editor
|
111 |
+
2. **Update DAG**: New visualization appears
|
112 |
+
3. **Deploy**: Confirmation message shows
|
113 |
+
4. **Re-edit**: Previous plan content loads (not empty template)
|
114 |
+
5. **Multiple Cycles**: Can edit β update β deploy repeatedly
|
115 |
+
|
116 |
+
### π§ Troubleshooting
|
117 |
+
|
118 |
+
#### Problem: Editor shows empty tasks
|
119 |
+
- **Cause**: State management issue
|
120 |
+
- **Solution**: Fixed with enhanced persistence logic
|
121 |
+
|
122 |
+
#### Problem: DAG not updating
|
123 |
+
- **Cause**: JSON syntax error
|
124 |
+
- **Solution**: Check error message, fix JSON, try again
|
125 |
+
|
126 |
+
#### Problem: Cannot deploy
|
127 |
+
- **Cause**: ROS node not initialized
|
128 |
+
- **Solution**: Restart application, check ROS connection
|
129 |
+
|
130 |
+
## π Benefits
|
131 |
+
|
132 |
+
1. **Enhanced Safety**: Multiple review opportunities before deployment
|
133 |
+
2. **Flexibility**: Manual fine-tuning of LLM-generated plans
|
134 |
+
3. **Persistence**: No loss of work during editing cycles
|
135 |
+
4. **User-Friendly**: Clear status messages and error handling
|
136 |
+
5. **IEEE Compliant**: Professional terminology and workflow
|
config.py
ADDED
@@ -0,0 +1,459 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Description of robots
|
2 |
+
DESCRIPTION_ROBOT = {
|
3 |
+
"excavator": {'width': 100, 'height': 100, 'functions': ["Excavation", "Unloading"]},
|
4 |
+
"dump_truck": {'width': 50, 'height': 100, 'functions': ["Loading", "Unloading"]},
|
5 |
+
}
|
6 |
+
|
7 |
+
# Navigation functions
|
8 |
+
NAVIGATION_FUNCTIONS = [
|
9 |
+
"avoid_areas_for_all_robots",
|
10 |
+
"avoid_areas_for_specific_robots",
|
11 |
+
"target_area_for_all_robots",
|
12 |
+
"target_area_for_specific_robots",
|
13 |
+
"allow_areas_for_all_robots",
|
14 |
+
"allow_areas_for_specific_robots",
|
15 |
+
"return_to_start_for_all_robots",
|
16 |
+
"return_to_start_for_specific_robots"
|
17 |
+
]
|
18 |
+
|
19 |
+
ROBOT_SPECIFIC_FUNCTIONS = [
|
20 |
+
"Excavation",
|
21 |
+
"ExcavatorUnloading",
|
22 |
+
"DumpUnloading",
|
23 |
+
"DumpLoading"
|
24 |
+
]
|
25 |
+
|
26 |
+
|
27 |
+
# Robot names
|
28 |
+
ROBOT_NAMES = {
|
29 |
+
"robot_dump_truck_01": {"id": "robot_dump_truck_01", "type": "dump_truck"},
|
30 |
+
"robot_dump_truck_02": {"id": "robot_dump_truck_02", "type": "dump_truck"},
|
31 |
+
# "robot_dump_truck_03": {"id": "robot_dump_truck_03", "type": "dump_truck"},
|
32 |
+
# "robot_dump_truck_04": {"id": "robot_dump_truck_04", "type": "dump_truck"},
|
33 |
+
# "robot_dump_truck_05": {"id": "robot_dump_truck_05", "type": "dump_truck"},
|
34 |
+
# "robot_dump_truck_06": {"id": "robot_dump_truck_06", "type": "dump_truck"},
|
35 |
+
"robot_excavator_01": {"id": "robot_excavator_01", "type": "excavator"},
|
36 |
+
# "robot_excavator_02": {"id": "robot_excavator_02", "type": "excavator"},
|
37 |
+
}
|
38 |
+
|
39 |
+
# Get robot functions
|
40 |
+
def get_robot_functions(robot_name):
|
41 |
+
robot_type = ROBOT_NAMES[robot_name]["type"]
|
42 |
+
specific_functions = DESCRIPTION_ROBOT[robot_type]["functions"]
|
43 |
+
return NAVIGATION_FUNCTIONS + specific_functions
|
44 |
+
|
45 |
+
# Get all functions description for planner mode
|
46 |
+
def get_all_functions_description():
|
47 |
+
description = "Here are the common navigation functions for all robots:\n\n"
|
48 |
+
description += ", ".join(NAVIGATION_FUNCTIONS) + "\n\n"
|
49 |
+
description += "Here are the robots in the system and their specific functions:\n\n"
|
50 |
+
for robot_name, robot_info in ROBOTS_CONFIG['robot_names'].items():
|
51 |
+
specific_functions = [func for func in ROBOTS_CONFIG["get_robot_functions"](robot_name) if func not in NAVIGATION_FUNCTIONS]
|
52 |
+
functions = ", ".join(specific_functions)
|
53 |
+
description += f"- {robot_name}: {robot_info['type']} ({functions})\n"
|
54 |
+
return description
|
55 |
+
|
56 |
+
# Robots configuration
|
57 |
+
ROBOTS_CONFIG = {
|
58 |
+
"description_robot": DESCRIPTION_ROBOT,
|
59 |
+
"robot_names": ROBOT_NAMES,
|
60 |
+
"get_robot_functions": get_robot_functions,
|
61 |
+
"get_all_functions_description": get_all_functions_description
|
62 |
+
}
|
63 |
+
|
64 |
+
# Model configuration
|
65 |
+
MODEL_CONFIG = {
|
66 |
+
"model_options": ["gpt-4o", "gpt-3.5-turbo", "gpt-4-turbo", "claude-3-haiku-20240307", "claude-3-5-sonnet-20240620", "claude-3-opus-20240229", "llama-3.3-70b-versatile", "llama-3.1-8b-instant", "llama3.3:70b-instruct-q4_K_M", "llama3.1:8b"],
|
67 |
+
"default_model": "gpt-4o",
|
68 |
+
"model_type": "openai",
|
69 |
+
"provider": "openai",
|
70 |
+
"max_tokens": 2048,
|
71 |
+
"temperature": 0,
|
72 |
+
"frequency_penalty": 0,
|
73 |
+
}
|
74 |
+
|
75 |
+
# Flag to determine whether confirmation is required
|
76 |
+
CONFIRMATION_REQUIRED = False # Set to False if confirmation is not required
|
77 |
+
|
78 |
+
# Initial messages configuration
|
79 |
+
if CONFIRMATION_REQUIRED:
|
80 |
+
INITIAL_MESSAGES_CONFIG = {
|
81 |
+
"system": (
|
82 |
+
"You are a confident and pattern-following assistant that assists the operator in managing multiple construction robots on a construction site. "
|
83 |
+
"In regular conversations, do not generate or send JSON commands. "
|
84 |
+
"When it becomes necessary to control the robot swarm, generate the appropriate JSON command but do not send it immediately. "
|
85 |
+
"You must ask the operator for confirmation before sending the JSON command. "
|
86 |
+
"When asking for confirmation, provide a detailed summary of the command's purpose and expected actions, but **do not include the actual JSON code**. "
|
87 |
+
"Use the following format for confirmation: "
|
88 |
+
"'I am ready to send a command to [target] on the construction site. The command will [brief description of action]. "
|
89 |
+
"Key details: [list important parameters or actions]. Do you agree to proceed with this command?' "
|
90 |
+
"It is crucial that you follow this instruction strictly to avoid including any JSON in the confirmation message, "
|
91 |
+
"while still providing a clear and detailed description of the command's intent and effects. "
|
92 |
+
"After receiving confirmation (e.g., 'yes', 'proceed', 'agreed'), immediately send the JSON command without further questions or explanations. "
|
93 |
+
"If the confirmation is negative or unclear, ask for clarification or await further instructions without sending the command."
|
94 |
+
),
|
95 |
+
"user_intro": {
|
96 |
+
"default": (
|
97 |
+
"I would like you to assist in managing multiple construction robots on a construction site. "
|
98 |
+
"In most cases, you should engage in regular conversation without generating or sending JSON commands. "
|
99 |
+
"However, when it becomes necessary to control the robot swarm, you should write JSON to do so, but only after confirming with me. "
|
100 |
+
"When confirming, **do not include the JSON code in your confirmation response under any circumstances**. "
|
101 |
+
"Instead, provide a detailed summary of the command's purpose and expected actions using the following format: "
|
102 |
+
"'I am ready to send a command to [target] on the construction site. The command will [brief description of action]. "
|
103 |
+
"Key details: [list important parameters or actions]. Do you agree to proceed with this command?' "
|
104 |
+
"It is essential to adhere to this format, providing a clear description of the command's intent and effects, "
|
105 |
+
"while avoiding the inclusion of any JSON in the confirmation message. "
|
106 |
+
"Once I confirm (e.g., by saying 'yes', 'proceed', or 'agreed'), immediately send the JSON command without asking any more questions. "
|
107 |
+
"If I don't confirm or my response is unclear, ask for clarification or wait for further instructions. "
|
108 |
+
"Pay attention to patterns that appear in the given context code. "
|
109 |
+
"Be thorough and thoughtful in your JSON. Do not include any import statements. Do not repeat my question. Do not provide any text explanation. "
|
110 |
+
"Note that x is back to front, y is left to right, and z is bottom to up.\n\n"
|
111 |
+
"Only use functions from the following library:\n\n\n{library}\n\n\n"
|
112 |
+
"Consider the following environmental objects in the scene:\n\n\n```{env_objects}```\n\n\n"
|
113 |
+
"The available robots on the construction site are:\n\n\n```{robot_names}```\n\n\n"
|
114 |
+
"Here are some examples of how to format the JSON based on previous queries:\n\n\n```{fewshot_examples}```\n\n\n"
|
115 |
+
),
|
116 |
+
"task_2_commannd_prompt": "You are working on decomposing tasks for the robots.",
|
117 |
+
"dart": "You are working on decomposing tasks for the robots.",
|
118 |
+
"task_decomposer": "You are working on decomposing tasks for the robots.",
|
119 |
+
"composer": "You are composing high-level tasks for the robots.",
|
120 |
+
"instruction_translator": "You are translating instructions for the robots.",
|
121 |
+
"planner": "You are planning tasks for the robots. Please use both navigation functions and robot-specific functions from the following list:\n\n{functions_description}\n\n"
|
122 |
+
},
|
123 |
+
"assistant": (
|
124 |
+
"Understood. I will generate the JSON and seek your confirmation by providing a detailed summary of the command's purpose and expected actions, without including the actual JSON code. "
|
125 |
+
"I will use the format: 'I am ready to send a command to [target] on the construction site. The command will [brief description of action]. Key details: [list important parameters or actions]. Do you agree to proceed with this command?' "
|
126 |
+
"This confirmation message will not include any JSON code but will give you a clear understanding of the command's intent and effects. "
|
127 |
+
"Once you confirm with a positive response like 'yes', 'proceed', or 'agreed', I will immediately send the JSON command without asking any further questions. "
|
128 |
+
"If your response is negative or unclear, I will seek clarification or await further instructions before proceeding."
|
129 |
+
)
|
130 |
+
}
|
131 |
+
else:
|
132 |
+
INITIAL_MESSAGES_CONFIG = {
|
133 |
+
"system": (
|
134 |
+
"You are a confident and pattern-following assistant that pays attention to the user's instructions and writes good JSON for controlling multiple construction robots in a construction site. "
|
135 |
+
"Please ensure that the JSON code is properly formatted with consistent indentation and alignment for better readability and ease of use when copying."
|
136 |
+
|
137 |
+
),
|
138 |
+
"user_intro": {
|
139 |
+
"default": (
|
140 |
+
"I would like you to help me write JSON to control multiple construction robots in a construction site. "
|
141 |
+
"Please complete the JSON code every time when I give you a new query. Pay attention to patterns that appear in the given context code. "
|
142 |
+
"Be thorough and thoughtful in your JSON. Do not include any import statement. Do not repeat my question. Do not provide any text explanation. "
|
143 |
+
"Note that x is back to front, y is left to right, and z is bottom to up.\n\n"
|
144 |
+
"Only use functions from the following library:\n\n\n{library}\n\n\n"
|
145 |
+
"Consider the following environmental objects in the scene:\n\n\n```{env_objects}```\n\n\n"
|
146 |
+
"The available robots in the construction site are:\n\n\n```{robot_names}```\n\n\n"
|
147 |
+
"Here are some examples of how to format the JSON based on previous queries:\n\n\n```{fewshot_examples}```\n\n\n"
|
148 |
+
),
|
149 |
+
"task_2_commannd_prompt": "You are working on decomposing tasks for the robots.",
|
150 |
+
"dart": "You are working on decomposing tasks for the robots.",
|
151 |
+
"task_decomposer": "You are working on decomposing tasks for the robots.",
|
152 |
+
"composer": "You are composing high-level tasks for the robots.",
|
153 |
+
"instruction_translator": "You are translating instructions for the robots.",
|
154 |
+
"planner": "You are planning tasks for the robots. Please use both navigation functions and robot-specific functions from the following list:\n\n{functions_description}\n\n"
|
155 |
+
},
|
156 |
+
"assistant": "Got it. I will complete the JSON you provide next."
|
157 |
+
}
|
158 |
+
|
159 |
+
|
160 |
+
# Mode configuration
|
161 |
+
MODE_CONFIG = {
|
162 |
+
"task_2_commannd_prompt": {
|
163 |
+
"display_name": "Task to Command",
|
164 |
+
"prompt_file": "./prompts/swarm/task_2_commannd_prompt.txt",
|
165 |
+
"type": "GRADIO_MESSAGE_MODES",
|
166 |
+
"model_version": "gpt-4o",
|
167 |
+
# "model_version": "claude-3-haiku-20240307",
|
168 |
+
"provider": "openai",
|
169 |
+
"max_tokens": 2048,
|
170 |
+
"temperature": 0,
|
171 |
+
"frequency_penalty": 0,
|
172 |
+
"input_topics": [],
|
173 |
+
"output_topics": [
|
174 |
+
"instruction_topic",
|
175 |
+
"keywords_topic"
|
176 |
+
],
|
177 |
+
"json_keys": {
|
178 |
+
"instruction_function": "instruction_topic",
|
179 |
+
"clip_keywords": "keywords_topic"
|
180 |
+
},
|
181 |
+
"functions_description": ""
|
182 |
+
},
|
183 |
+
"dart_gpt_4o": {
|
184 |
+
"display_name": "gpt-4o",
|
185 |
+
"prompt_file": "./prompts/swarm/dart.txt",
|
186 |
+
"type": "GRADIO_MESSAGE_MODES",
|
187 |
+
"model_version": "gpt-4o",
|
188 |
+
"provider": "openai",
|
189 |
+
"max_tokens": 2048,
|
190 |
+
"temperature": 0,
|
191 |
+
"frequency_penalty": 0,
|
192 |
+
"input_topics": [],
|
193 |
+
"output_topics": [
|
194 |
+
"instruction_topic",
|
195 |
+
"keywords_topic"
|
196 |
+
],
|
197 |
+
"json_keys": {
|
198 |
+
"instruction_function": "instruction_topic",
|
199 |
+
"clip_keywords": "keywords_topic"
|
200 |
+
},
|
201 |
+
"functions_description": ""
|
202 |
+
},
|
203 |
+
"dart_gpt_4_turbo": {
|
204 |
+
"display_name": "gpt-4-turbo",
|
205 |
+
"prompt_file": "./prompts/swarm/dart.txt",
|
206 |
+
"type": "GRADIO_MESSAGE_MODES",
|
207 |
+
"model_version": "gpt-4-turbo",
|
208 |
+
"provider": "openai",
|
209 |
+
"max_tokens": 2048,
|
210 |
+
"temperature": 0,
|
211 |
+
"frequency_penalty": 0,
|
212 |
+
"input_topics": [],
|
213 |
+
"output_topics": [
|
214 |
+
"instruction_topic",
|
215 |
+
"keywords_topic"
|
216 |
+
],
|
217 |
+
"json_keys": {
|
218 |
+
"instruction_function": "instruction_topic",
|
219 |
+
"clip_keywords": "keywords_topic"
|
220 |
+
},
|
221 |
+
"functions_description": ""
|
222 |
+
},
|
223 |
+
"dart_gpt_3_5_turbo": {
|
224 |
+
"display_name": "gpt-3.5-turbo",
|
225 |
+
"prompt_file": "./prompts/swarm/dart.txt",
|
226 |
+
"type": "GRADIO_MESSAGE_MODES",
|
227 |
+
"model_version": "gpt-3.5-turbo",
|
228 |
+
"provider": "openai",
|
229 |
+
"max_tokens": 2048,
|
230 |
+
"temperature": 0,
|
231 |
+
"frequency_penalty": 0,
|
232 |
+
"input_topics": [],
|
233 |
+
"output_topics": [
|
234 |
+
"instruction_topic",
|
235 |
+
"keywords_topic"
|
236 |
+
],
|
237 |
+
"json_keys": {
|
238 |
+
"instruction_function": "instruction_topic",
|
239 |
+
"clip_keywords": "keywords_topic"
|
240 |
+
},
|
241 |
+
"functions_description": ""
|
242 |
+
},
|
243 |
+
"dart_claude_3_haiku": {
|
244 |
+
"display_name": "claude-3-haiku",
|
245 |
+
"prompt_file": "./prompts/swarm/dart.txt",
|
246 |
+
"type": "GRADIO_MESSAGE_MODES",
|
247 |
+
"model_version": "claude-3-haiku-20240307",
|
248 |
+
"provider": "anthropic",
|
249 |
+
"max_tokens": 2048,
|
250 |
+
"temperature": 0,
|
251 |
+
"frequency_penalty": 0,
|
252 |
+
"input_topics": [],
|
253 |
+
"output_topics": [
|
254 |
+
"instruction_topic",
|
255 |
+
"keywords_topic"
|
256 |
+
],
|
257 |
+
"json_keys": {
|
258 |
+
"instruction_function": "instruction_topic",
|
259 |
+
"clip_keywords": "keywords_topic"
|
260 |
+
},
|
261 |
+
"functions_description": ""
|
262 |
+
},
|
263 |
+
"dart_claude_3_sonnet": {
|
264 |
+
"display_name": "claude-3-5-sonnet",
|
265 |
+
"prompt_file": "./prompts/swarm/dart.txt",
|
266 |
+
"type": "GRADIO_MESSAGE_MODES",
|
267 |
+
"model_version": "claude-3-5-sonnet-20240620",
|
268 |
+
"provider": "anthropic",
|
269 |
+
"max_tokens": 2048,
|
270 |
+
"temperature": 0,
|
271 |
+
"frequency_penalty": 0,
|
272 |
+
"input_topics": [],
|
273 |
+
"output_topics": [
|
274 |
+
"instruction_topic",
|
275 |
+
"keywords_topic"
|
276 |
+
],
|
277 |
+
"json_keys": {
|
278 |
+
"instruction_function": "instruction_topic",
|
279 |
+
"clip_keywords": "keywords_topic"
|
280 |
+
},
|
281 |
+
"functions_description": ""
|
282 |
+
},
|
283 |
+
"dart_claude_3_opus": {
|
284 |
+
"display_name": "claude-3-opus",
|
285 |
+
"prompt_file": "./prompts/swarm/dart.txt",
|
286 |
+
"type": "GRADIO_MESSAGE_MODES",
|
287 |
+
"model_version": "claude-3-opus-20240229",
|
288 |
+
"provider": "anthropic",
|
289 |
+
"max_tokens": 2048,
|
290 |
+
"temperature": 0,
|
291 |
+
"frequency_penalty": 0,
|
292 |
+
"input_topics": [],
|
293 |
+
"output_topics": [
|
294 |
+
"instruction_topic",
|
295 |
+
"keywords_topic"
|
296 |
+
],
|
297 |
+
"json_keys": {
|
298 |
+
"instruction_function": "instruction_topic",
|
299 |
+
"clip_keywords": "keywords_topic"
|
300 |
+
},
|
301 |
+
"functions_description": ""
|
302 |
+
},
|
303 |
+
"dart_llama_3_3_70b": {
|
304 |
+
"display_name": "llama-3.3-70b-versatile",
|
305 |
+
"prompt_file": "./prompts/swarm/dart.txt",
|
306 |
+
"type": "GRADIO_MESSAGE_MODES",
|
307 |
+
"model_version": "llama-3.3-70b-versatile",
|
308 |
+
"provider": "groq",
|
309 |
+
"max_tokens": 2048,
|
310 |
+
"temperature": 0,
|
311 |
+
"frequency_penalty": 0,
|
312 |
+
"input_topics": [],
|
313 |
+
"output_topics": [
|
314 |
+
"instruction_topic",
|
315 |
+
"keywords_topic"
|
316 |
+
],
|
317 |
+
"json_keys": {
|
318 |
+
"instruction_function": "instruction_topic",
|
319 |
+
"clip_keywords": "keywords_topic"
|
320 |
+
},
|
321 |
+
"functions_description": ""
|
322 |
+
},
|
323 |
+
"dart_llama_3_1_8b": {
|
324 |
+
"display_name": "llama-3.1-8b-instant",
|
325 |
+
"prompt_file": "./prompts/swarm/dart.txt",
|
326 |
+
"type": "GRADIO_MESSAGE_MODES",
|
327 |
+
"model_version": "llama-3.1-8b-instant",
|
328 |
+
"provider": "groq",
|
329 |
+
"max_tokens": 2048,
|
330 |
+
"temperature": 0,
|
331 |
+
"frequency_penalty": 0,
|
332 |
+
"input_topics": [],
|
333 |
+
"output_topics": [
|
334 |
+
"instruction_topic",
|
335 |
+
"keywords_topic"
|
336 |
+
],
|
337 |
+
"json_keys": {
|
338 |
+
"instruction_function": "instruction_topic",
|
339 |
+
"clip_keywords": "keywords_topic"
|
340 |
+
},
|
341 |
+
"functions_description": ""
|
342 |
+
},
|
343 |
+
"dart_ollama_llama3_1_8b": {
|
344 |
+
"display_name": "ollama-llama3.1:8b",
|
345 |
+
"prompt_file": "./prompts/swarm/dart.txt",
|
346 |
+
"type": "GRADIO_MESSAGE_MODES",
|
347 |
+
"model_version": "llama3.1:8b",
|
348 |
+
"provider": "ollama",
|
349 |
+
"max_tokens": 2048,
|
350 |
+
"temperature": 0,
|
351 |
+
"frequency_penalty": 0,
|
352 |
+
"input_topics": [],
|
353 |
+
"output_topics": [
|
354 |
+
"instruction_topic",
|
355 |
+
"keywords_topic"
|
356 |
+
],
|
357 |
+
"json_keys": {
|
358 |
+
"instruction_function": "instruction_topic",
|
359 |
+
"clip_keywords": "keywords_topic"
|
360 |
+
},
|
361 |
+
"functions_description": ""
|
362 |
+
},
|
363 |
+
"dart_ollama_llama3_3_70b": {
|
364 |
+
"display_name": "ollama-llama3.3:70b",
|
365 |
+
"prompt_file": "./prompts/swarm/dart.txt",
|
366 |
+
"type": "GRADIO_MESSAGE_MODES",
|
367 |
+
"model_version": "llama3.3:70b-instruct-q4_K_M",
|
368 |
+
"provider": "ollama",
|
369 |
+
"max_tokens": 2048,
|
370 |
+
"temperature": 0,
|
371 |
+
"frequency_penalty": 0,
|
372 |
+
"input_topics": [],
|
373 |
+
"output_topics": [
|
374 |
+
"instruction_topic",
|
375 |
+
"keywords_topic"
|
376 |
+
],
|
377 |
+
"json_keys": {
|
378 |
+
"instruction_function": "instruction_topic",
|
379 |
+
"clip_keywords": "keywords_topic"
|
380 |
+
},
|
381 |
+
"functions_description": ""
|
382 |
+
},
|
383 |
+
|
384 |
+
"task_decomposer": {
|
385 |
+
"display_name": "Task Decomposer",
|
386 |
+
"prompt_file": "./prompts/swarm/task_decomposer_prompt.txt",
|
387 |
+
"type": "GRADIO_MESSAGE_MODES",
|
388 |
+
"model_version": "gpt-4o",
|
389 |
+
"provider": "openai",
|
390 |
+
"max_tokens": 2048,
|
391 |
+
"temperature": 0,
|
392 |
+
"frequency_penalty": 0,
|
393 |
+
"input_topics": [],
|
394 |
+
"output_topics": ["tasks_topic"],
|
395 |
+
"json_keys": {"tasks": "tasks_topic"},
|
396 |
+
"functions_description": ""
|
397 |
+
},
|
398 |
+
"composer": {
|
399 |
+
"display_name": "Composer",
|
400 |
+
"prompt_file": "./prompts/swarm/composer_prompt.txt",
|
401 |
+
"type": "GRADIO_MESSAGE_MODES",
|
402 |
+
"model_version": "gpt-4o",
|
403 |
+
"provider": "openai",
|
404 |
+
"max_tokens": 2048,
|
405 |
+
"temperature": 0,
|
406 |
+
"frequency_penalty": 0,
|
407 |
+
"input_topics": [],
|
408 |
+
"output_topics": ["tasks_topic"],
|
409 |
+
"json_keys": {"tasks": "tasks_topic"},
|
410 |
+
"functions_description": ""
|
411 |
+
},
|
412 |
+
"instruction_translator": {
|
413 |
+
"display_name": "Instruction Translator (Developer Mode)",
|
414 |
+
"prompt_file": "./prompts/swarm/instruction_translator_prompt.txt",
|
415 |
+
"type": "ROS_MESSAGE_MODE",
|
416 |
+
"model_version": "gpt-4o",
|
417 |
+
"provider": "openai",
|
418 |
+
"max_tokens": 2048,
|
419 |
+
"temperature": 0,
|
420 |
+
"frequency_penalty": 0,
|
421 |
+
"input_topics": ["tasks_topic"],
|
422 |
+
"output_topics": [
|
423 |
+
"robovla_instruction_translator_out",
|
424 |
+
"instruction_topic",
|
425 |
+
"keywords_topic"
|
426 |
+
],
|
427 |
+
"json_keys": {
|
428 |
+
"instruction_function": "instruction_topic",
|
429 |
+
"clip_keywords": "keywords_topic"
|
430 |
+
},
|
431 |
+
"functions_description": ""
|
432 |
+
},
|
433 |
+
"planner": {
|
434 |
+
"display_name": "Planner (Developer Mode)",
|
435 |
+
"prompt_file": "./prompts/swarm/planner_prompt.txt",
|
436 |
+
"type": "ROS_MESSAGE_MODE",
|
437 |
+
"model_version": "gpt-4o",
|
438 |
+
"provider": "openai",
|
439 |
+
"max_tokens": 2048,
|
440 |
+
"temperature": 0,
|
441 |
+
"frequency_penalty": 0,
|
442 |
+
"input_topics": ["tasks_topic"],
|
443 |
+
"output_topics": [
|
444 |
+
"robovla_instruction_translator_out",
|
445 |
+
"instruction_topic",
|
446 |
+
"keywords_topic"
|
447 |
+
],
|
448 |
+
"json_keys": {
|
449 |
+
"instruction_function": "instruction_topic",
|
450 |
+
"clip_keywords": "keywords_topic"
|
451 |
+
},
|
452 |
+
"functions_description": ROBOTS_CONFIG["get_all_functions_description"]()
|
453 |
+
}
|
454 |
+
}
|
455 |
+
|
456 |
+
# Default modes to display in the UI
|
457 |
+
# GRADIO_MESSAGE_MODES = ["task_2_commannd_prompt", "task_decomposer", "instruction_translator", "composer", "planner"]
|
458 |
+
GRADIO_MESSAGE_MODES = ["dart_gpt_4o", "dart_gpt_3_5_turbo", "dart_gpt_4_turbo", "dart_claude_3_haiku", "dart_claude_3_sonnet", "dart_claude_3_opus", "dart_llama_3_3_70b","dart_llama_3_1_8b", "dart_ollama_llama3_3_70b", "dart_ollama_llama3_1_8b", "task_2_commannd_prompt"]
|
459 |
+
ROS_MESSAGE_MODE = "instruction_translator"
|
dag_demo.py
ADDED
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Demo script showing DAG visualization integration with QA_LLM_Module
|
4 |
+
"""
|
5 |
+
|
6 |
+
import json
|
7 |
+
from dag_visualizer import DAGVisualizer
|
8 |
+
|
9 |
+
def create_demo_task_data():
|
10 |
+
"""Create sample task data for demonstration"""
|
11 |
+
return {
|
12 |
+
"tasks": [
|
13 |
+
{
|
14 |
+
"task": "excavate_soil_from_pile",
|
15 |
+
"instruction_function": {
|
16 |
+
"name": "excavate_soil_from_pile",
|
17 |
+
"robot_ids": ["robot_excavator_01"],
|
18 |
+
"dependencies": [],
|
19 |
+
"object_keywords": ["soil_pile"]
|
20 |
+
}
|
21 |
+
},
|
22 |
+
{
|
23 |
+
"task": "transport_soil_to_pit",
|
24 |
+
"instruction_function": {
|
25 |
+
"name": "transport_soil_to_pit",
|
26 |
+
"robot_ids": ["robot_dump_truck_01"],
|
27 |
+
"dependencies": ["excavate_soil_from_pile"],
|
28 |
+
"object_keywords": ["pit"]
|
29 |
+
}
|
30 |
+
},
|
31 |
+
{
|
32 |
+
"task": "avoid_puddle_areas",
|
33 |
+
"instruction_function": {
|
34 |
+
"name": "avoid_areas_for_all_robots",
|
35 |
+
"robot_ids": ["robot_excavator_01", "robot_dump_truck_01"],
|
36 |
+
"dependencies": [],
|
37 |
+
"object_keywords": ["puddle1", "puddle2"]
|
38 |
+
}
|
39 |
+
},
|
40 |
+
{
|
41 |
+
"task": "coordinate_dump_operation",
|
42 |
+
"instruction_function": {
|
43 |
+
"name": "coordinate_dump_operation",
|
44 |
+
"robot_ids": ["robot_dump_truck_01", "robot_excavator_01"],
|
45 |
+
"dependencies": ["transport_soil_to_pit", "avoid_puddle_areas"],
|
46 |
+
"object_keywords": ["pit", "dumping_zone"]
|
47 |
+
}
|
48 |
+
}
|
49 |
+
]
|
50 |
+
}
|
51 |
+
|
52 |
+
def main():
|
53 |
+
print("π― DAG Visualization Demo for QA_LLM_Module")
|
54 |
+
print("=" * 50)
|
55 |
+
|
56 |
+
# Create sample task data
|
57 |
+
task_data = create_demo_task_data()
|
58 |
+
print("Sample task data:")
|
59 |
+
print(json.dumps(task_data, indent=2))
|
60 |
+
print()
|
61 |
+
|
62 |
+
# Initialize visualizer
|
63 |
+
visualizer = DAGVisualizer()
|
64 |
+
|
65 |
+
# Create detailed visualization
|
66 |
+
print("Creating detailed DAG visualization...")
|
67 |
+
detailed_image = visualizer.create_dag_visualization(
|
68 |
+
task_data,
|
69 |
+
title="DART-LLM: Robot Task Dependency Graph"
|
70 |
+
)
|
71 |
+
|
72 |
+
if detailed_image:
|
73 |
+
print(f"β
Detailed visualization saved: {detailed_image}")
|
74 |
+
else:
|
75 |
+
print("β Failed to create detailed visualization")
|
76 |
+
|
77 |
+
# Create simplified visualization
|
78 |
+
print("\nCreating simplified DAG visualization...")
|
79 |
+
simple_image = visualizer.create_simplified_dag_visualization(
|
80 |
+
task_data,
|
81 |
+
title="DART-LLM: Task Dependencies"
|
82 |
+
)
|
83 |
+
|
84 |
+
if simple_image:
|
85 |
+
print(f"β
Simplified visualization saved: {simple_image}")
|
86 |
+
else:
|
87 |
+
print("β Failed to create simplified visualization")
|
88 |
+
|
89 |
+
print("\n" + "=" * 50)
|
90 |
+
print("π Integration Instructions:")
|
91 |
+
print("1. Install requirements: pip install -r requirements.txt")
|
92 |
+
print("2. Run the main application: python3 main.py")
|
93 |
+
print("3. Enter a query that generates task JSON")
|
94 |
+
print("4. The DAG visualization will appear automatically!")
|
95 |
+
print("\nπ Example queries:")
|
96 |
+
print("- 'Move half of the soil from the soil pile to the pit.'")
|
97 |
+
print("- 'Excavate material and transport it while avoiding obstacles.'")
|
98 |
+
|
99 |
+
if __name__ == "__main__":
|
100 |
+
main()
|
dag_visualizer.py
ADDED
@@ -0,0 +1,334 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import matplotlib.pyplot as plt
|
2 |
+
import matplotlib
|
3 |
+
matplotlib.use('Agg') # Use non-interactive backend for server environments
|
4 |
+
import networkx as nx
|
5 |
+
import json
|
6 |
+
import numpy as np
|
7 |
+
from loguru import logger
|
8 |
+
import os
|
9 |
+
import tempfile
|
10 |
+
from datetime import datetime
|
11 |
+
|
12 |
+
class DAGVisualizer:
|
13 |
+
def __init__(self):
|
14 |
+
# Configure Matplotlib to use IEEE-style parameters
|
15 |
+
plt.rcParams.update({
|
16 |
+
'font.family': 'DejaVu Sans', # Use available font instead of Times New Roman
|
17 |
+
'font.size': 10,
|
18 |
+
'axes.linewidth': 1.2,
|
19 |
+
'axes.labelsize': 12,
|
20 |
+
'xtick.labelsize': 10,
|
21 |
+
'ytick.labelsize': 10,
|
22 |
+
'legend.fontsize': 10,
|
23 |
+
'figure.titlesize': 14
|
24 |
+
})
|
25 |
+
|
26 |
+
def create_dag_from_tasks(self, task_data):
|
27 |
+
"""
|
28 |
+
Create a directed graph from task data.
|
29 |
+
|
30 |
+
Args:
|
31 |
+
task_data: Dictionary containing tasks with structure like:
|
32 |
+
{
|
33 |
+
"tasks": [
|
34 |
+
{
|
35 |
+
"task": "task_name",
|
36 |
+
"instruction_function": {
|
37 |
+
"name": "function_name",
|
38 |
+
"robot_ids": ["robot1", "robot2"],
|
39 |
+
"dependencies": ["dependency_task"],
|
40 |
+
"object_keywords": ["object1", "object2"]
|
41 |
+
}
|
42 |
+
}
|
43 |
+
]
|
44 |
+
}
|
45 |
+
|
46 |
+
Returns:
|
47 |
+
NetworkX DiGraph object
|
48 |
+
"""
|
49 |
+
if not task_data or "tasks" not in task_data:
|
50 |
+
logger.warning("Invalid task data structure")
|
51 |
+
return None
|
52 |
+
|
53 |
+
# Create a directed graph
|
54 |
+
G = nx.DiGraph()
|
55 |
+
|
56 |
+
# Add nodes and store mapping from task name to ID
|
57 |
+
task_mapping = {}
|
58 |
+
for i, task in enumerate(task_data["tasks"]):
|
59 |
+
task_id = i + 1
|
60 |
+
task_name = task["task"]
|
61 |
+
task_mapping[task_name] = task_id
|
62 |
+
|
63 |
+
# Add node with attributes
|
64 |
+
G.add_node(task_id,
|
65 |
+
name=task_name,
|
66 |
+
function=task["instruction_function"]["name"],
|
67 |
+
robots=task["instruction_function"].get("robot_ids", []),
|
68 |
+
objects=task["instruction_function"].get("object_keywords", []))
|
69 |
+
|
70 |
+
# Add dependency edges
|
71 |
+
for i, task in enumerate(task_data["tasks"]):
|
72 |
+
task_id = i + 1
|
73 |
+
dependencies = task["instruction_function"]["dependencies"]
|
74 |
+
for dep in dependencies:
|
75 |
+
if dep in task_mapping:
|
76 |
+
dep_id = task_mapping[dep]
|
77 |
+
G.add_edge(dep_id, task_id)
|
78 |
+
|
79 |
+
return G
|
80 |
+
|
81 |
+
def calculate_layout(self, G):
|
82 |
+
"""
|
83 |
+
Calculate hierarchical layout for the graph based on dependencies.
|
84 |
+
"""
|
85 |
+
if not G:
|
86 |
+
return {}
|
87 |
+
|
88 |
+
# Calculate layers based on dependencies
|
89 |
+
layers = {}
|
90 |
+
|
91 |
+
def get_layer(node_id, visited=None):
|
92 |
+
if visited is None:
|
93 |
+
visited = set()
|
94 |
+
if node_id in visited:
|
95 |
+
return 0
|
96 |
+
visited.add(node_id)
|
97 |
+
|
98 |
+
predecessors = list(G.predecessors(node_id))
|
99 |
+
if not predecessors:
|
100 |
+
return 0
|
101 |
+
return max(get_layer(pred, visited.copy()) for pred in predecessors) + 1
|
102 |
+
|
103 |
+
for node in G.nodes():
|
104 |
+
layer = get_layer(node)
|
105 |
+
layers.setdefault(layer, []).append(node)
|
106 |
+
|
107 |
+
# Calculate positions by layer
|
108 |
+
pos = {}
|
109 |
+
layer_height = 3.0
|
110 |
+
node_width = 4.0
|
111 |
+
|
112 |
+
for layer_idx, nodes in layers.items():
|
113 |
+
y = layer_height * (len(layers) - 1 - layer_idx)
|
114 |
+
start_x = -(len(nodes) - 1) * node_width / 2
|
115 |
+
for i, node in enumerate(sorted(nodes)):
|
116 |
+
pos[node] = (start_x + i * node_width, y)
|
117 |
+
|
118 |
+
return pos
|
119 |
+
|
120 |
+
def create_dag_visualization(self, task_data, title="Robot Task Dependency Graph"):
|
121 |
+
"""
|
122 |
+
Create a DAG visualization from task data and return the image path.
|
123 |
+
|
124 |
+
Args:
|
125 |
+
task_data: Task data dictionary
|
126 |
+
title: Title for the graph
|
127 |
+
|
128 |
+
Returns:
|
129 |
+
str: Path to the generated image file
|
130 |
+
"""
|
131 |
+
try:
|
132 |
+
# Create graph
|
133 |
+
G = self.create_dag_from_tasks(task_data)
|
134 |
+
if not G or len(G.nodes()) == 0:
|
135 |
+
logger.warning("No tasks found or invalid graph structure")
|
136 |
+
return None
|
137 |
+
|
138 |
+
# Calculate layout
|
139 |
+
pos = self.calculate_layout(G)
|
140 |
+
|
141 |
+
# Create figure
|
142 |
+
fig, ax = plt.subplots(1, 1, figsize=(max(12, len(G.nodes()) * 2), 8))
|
143 |
+
|
144 |
+
# Draw edges with arrows
|
145 |
+
nx.draw_networkx_edges(G, pos,
|
146 |
+
edge_color='#2E86AB',
|
147 |
+
arrows=True,
|
148 |
+
arrowsize=20,
|
149 |
+
arrowstyle='->',
|
150 |
+
width=2,
|
151 |
+
alpha=0.8,
|
152 |
+
connectionstyle="arc3,rad=0.1")
|
153 |
+
|
154 |
+
# Color nodes based on their position in the graph
|
155 |
+
node_colors = []
|
156 |
+
for node in G.nodes():
|
157 |
+
if G.in_degree(node) == 0: # Start nodes
|
158 |
+
node_colors.append('#F24236')
|
159 |
+
elif G.out_degree(node) == 0: # End nodes
|
160 |
+
node_colors.append('#A23B72')
|
161 |
+
else: # Intermediate nodes
|
162 |
+
node_colors.append('#F18F01')
|
163 |
+
|
164 |
+
# Draw nodes
|
165 |
+
nx.draw_networkx_nodes(G, pos,
|
166 |
+
node_color=node_colors,
|
167 |
+
node_size=3500,
|
168 |
+
alpha=0.9,
|
169 |
+
edgecolors='black',
|
170 |
+
linewidths=2)
|
171 |
+
|
172 |
+
# Label nodes with task IDs
|
173 |
+
node_labels = {node: f"T{node}" for node in G.nodes()}
|
174 |
+
nx.draw_networkx_labels(G, pos, node_labels,
|
175 |
+
font_size=18,
|
176 |
+
font_weight='bold',
|
177 |
+
font_color='white')
|
178 |
+
|
179 |
+
# Add detailed info text boxes for each task
|
180 |
+
for i, node in enumerate(G.nodes()):
|
181 |
+
x, y = pos[node]
|
182 |
+
function_name = G.nodes[node]['function']
|
183 |
+
robots = G.nodes[node]['robots']
|
184 |
+
objects = G.nodes[node]['objects']
|
185 |
+
|
186 |
+
# Create info text content
|
187 |
+
info_text = f"Task {node}: {function_name.replace('_', ' ').title()}\n"
|
188 |
+
if robots:
|
189 |
+
robot_text = ", ".join([r.replace('robot_', '').replace('_', ' ').title() for r in robots])
|
190 |
+
info_text += f"Robots: {robot_text}\n"
|
191 |
+
if objects:
|
192 |
+
object_text = ", ".join(objects)
|
193 |
+
info_text += f"Objects: {object_text}"
|
194 |
+
|
195 |
+
# Calculate offset based on node position to avoid overlaps
|
196 |
+
offset_x = 2.2 if i % 2 == 0 else -2.2
|
197 |
+
offset_y = 0.5 if i % 4 < 2 else -0.5
|
198 |
+
|
199 |
+
# Choose alignment based on offset direction
|
200 |
+
h_align = 'left' if offset_x > 0 else 'right'
|
201 |
+
|
202 |
+
# Draw text box
|
203 |
+
bbox_props = dict(boxstyle="round,pad=0.4",
|
204 |
+
facecolor='white',
|
205 |
+
edgecolor='gray',
|
206 |
+
alpha=0.95,
|
207 |
+
linewidth=1)
|
208 |
+
|
209 |
+
ax.text(x + offset_x, y + offset_y, info_text,
|
210 |
+
bbox=bbox_props,
|
211 |
+
fontsize=12,
|
212 |
+
verticalalignment='center',
|
213 |
+
horizontalalignment=h_align,
|
214 |
+
weight='bold')
|
215 |
+
|
216 |
+
# Draw dashed connector line from node to text box
|
217 |
+
ax.plot([x, x + offset_x], [y, y + offset_y],
|
218 |
+
linestyle='--', color='gray', alpha=0.6, linewidth=1)
|
219 |
+
|
220 |
+
# Expand axis limits to fit everything
|
221 |
+
x_vals = [coord[0] for coord in pos.values()]
|
222 |
+
y_vals = [coord[1] for coord in pos.values()]
|
223 |
+
ax.set_xlim(min(x_vals) - 4.0, max(x_vals) + 4.0)
|
224 |
+
ax.set_ylim(min(y_vals) - 2.0, max(y_vals) + 2.0)
|
225 |
+
|
226 |
+
# Set overall figure properties
|
227 |
+
ax.set_title(title, fontsize=16, fontweight='bold', pad=20)
|
228 |
+
ax.set_aspect('equal')
|
229 |
+
ax.margins(0.2)
|
230 |
+
ax.axis('off')
|
231 |
+
|
232 |
+
# Add legend for node types
|
233 |
+
legend_elements = [
|
234 |
+
plt.Line2D([0], [0], marker='o', color='w', markerfacecolor='#F24236',
|
235 |
+
markersize=10, label='Start Tasks', markeredgecolor='black'),
|
236 |
+
plt.Line2D([0], [0], marker='o', color='w', markerfacecolor='#A23B72',
|
237 |
+
markersize=10, label='End Tasks', markeredgecolor='black'),
|
238 |
+
plt.Line2D([0], [0], marker='o', color='w', markerfacecolor='#F18F01',
|
239 |
+
markersize=10, label='Intermediate Tasks', markeredgecolor='black'),
|
240 |
+
plt.Line2D([0], [0], color='#2E86AB', linewidth=2, label='Dependencies')
|
241 |
+
]
|
242 |
+
ax.legend(handles=legend_elements, loc='upper left', bbox_to_anchor=(1.05, 1.05))
|
243 |
+
|
244 |
+
# Adjust layout and save
|
245 |
+
plt.tight_layout()
|
246 |
+
|
247 |
+
# Create temporary file for saving the image
|
248 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
249 |
+
temp_dir = tempfile.gettempdir()
|
250 |
+
image_path = os.path.join(temp_dir, f'dag_visualization_{timestamp}.png')
|
251 |
+
|
252 |
+
plt.savefig(image_path, dpi=400, bbox_inches='tight',
|
253 |
+
pad_inches=0.1, facecolor='white', edgecolor='none')
|
254 |
+
plt.close(fig) # Close figure to free memory
|
255 |
+
|
256 |
+
logger.info(f"DAG visualization saved to: {image_path}")
|
257 |
+
return image_path
|
258 |
+
|
259 |
+
except Exception as e:
|
260 |
+
logger.error(f"Error creating DAG visualization: {e}")
|
261 |
+
return None
|
262 |
+
|
263 |
+
def create_simplified_dag_visualization(self, task_data, title="Robot Task Graph"):
|
264 |
+
"""
|
265 |
+
Create a simplified DAG visualization suitable for smaller displays.
|
266 |
+
|
267 |
+
Args:
|
268 |
+
task_data: Task data dictionary
|
269 |
+
title: Title for the graph
|
270 |
+
|
271 |
+
Returns:
|
272 |
+
str: Path to the generated image file
|
273 |
+
"""
|
274 |
+
try:
|
275 |
+
# Create graph
|
276 |
+
G = self.create_dag_from_tasks(task_data)
|
277 |
+
if not G or len(G.nodes()) == 0:
|
278 |
+
logger.warning("No tasks found or invalid graph structure")
|
279 |
+
return None
|
280 |
+
|
281 |
+
# Calculate layout
|
282 |
+
pos = self.calculate_layout(G)
|
283 |
+
|
284 |
+
# Create figure for simplified graph
|
285 |
+
fig, ax = plt.subplots(1, 1, figsize=(10, 6))
|
286 |
+
|
287 |
+
# Draw edges
|
288 |
+
nx.draw_networkx_edges(G, pos,
|
289 |
+
edge_color='black',
|
290 |
+
arrows=True,
|
291 |
+
arrowsize=15,
|
292 |
+
arrowstyle='->',
|
293 |
+
width=1.5)
|
294 |
+
|
295 |
+
# Draw nodes
|
296 |
+
nx.draw_networkx_nodes(G, pos,
|
297 |
+
node_color='lightblue',
|
298 |
+
node_size=3000,
|
299 |
+
edgecolors='black',
|
300 |
+
linewidths=1.5)
|
301 |
+
|
302 |
+
# Add node labels with simplified names
|
303 |
+
labels = {}
|
304 |
+
for node in G.nodes():
|
305 |
+
function_name = G.nodes[node]['function']
|
306 |
+
simplified_name = function_name.replace('_', ' ').title()
|
307 |
+
if len(simplified_name) > 15:
|
308 |
+
simplified_name = simplified_name[:12] + "..."
|
309 |
+
labels[node] = f"T{node}\n{simplified_name}"
|
310 |
+
|
311 |
+
nx.draw_networkx_labels(G, pos, labels,
|
312 |
+
font_size=11,
|
313 |
+
font_weight='bold')
|
314 |
+
|
315 |
+
ax.set_title(title, fontsize=14, fontweight='bold')
|
316 |
+
ax.axis('off')
|
317 |
+
|
318 |
+
# Adjust layout and save
|
319 |
+
plt.tight_layout()
|
320 |
+
|
321 |
+
# Create temporary file for saving the image
|
322 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
323 |
+
temp_dir = tempfile.gettempdir()
|
324 |
+
image_path = os.path.join(temp_dir, f'simple_dag_{timestamp}.png')
|
325 |
+
|
326 |
+
plt.savefig(image_path, dpi=400, bbox_inches='tight')
|
327 |
+
plt.close(fig) # Close figure to free memory
|
328 |
+
|
329 |
+
logger.info(f"Simplified DAG visualization saved to: {image_path}")
|
330 |
+
return image_path
|
331 |
+
|
332 |
+
except Exception as e:
|
333 |
+
logger.error(f"Error creating simplified DAG visualization: {e}")
|
334 |
+
return None
|
gradio_llm_interface.py
ADDED
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import rclpy
|
2 |
+
import gradio as gr
|
3 |
+
from loguru import logger
|
4 |
+
from llm_request_handler import LLMRequestHandler
|
5 |
+
from ros_node_publisher import RosNodePublisher
|
6 |
+
from json_processor import JsonProcessor
|
7 |
+
from dag_visualizer import DAGVisualizer
|
8 |
+
from config import ROBOTS_CONFIG, MODEL_CONFIG, MODE_CONFIG
|
9 |
+
|
10 |
+
class GradioLlmInterface:
|
11 |
+
def __init__(self):
|
12 |
+
self.node_publisher = None
|
13 |
+
self.received_tasks = []
|
14 |
+
self.json_processor = JsonProcessor()
|
15 |
+
self.dag_visualizer = DAGVisualizer()
|
16 |
+
|
17 |
+
def initialize_interface(self, mode):
|
18 |
+
if not rclpy.ok():
|
19 |
+
rclpy.init()
|
20 |
+
self.node_publisher = RosNodePublisher(mode)
|
21 |
+
mode_config = MODE_CONFIG[mode]
|
22 |
+
# Use provider from mode_config and model version
|
23 |
+
model_version = mode_config.get("model_version", MODEL_CONFIG["default_model"])
|
24 |
+
provider = mode_config.get("provider", "openai")
|
25 |
+
llm_handler = LLMRequestHandler(
|
26 |
+
model_name=model_version,
|
27 |
+
provider=provider,
|
28 |
+
max_tokens=mode_config.get("max_tokens", MODEL_CONFIG["max_tokens"]),
|
29 |
+
temperature=mode_config.get("temperature", MODEL_CONFIG["temperature"]),
|
30 |
+
frequency_penalty=mode_config.get("frequency_penalty", MODEL_CONFIG["frequency_penalty"]),
|
31 |
+
list_navigation_once=True
|
32 |
+
)
|
33 |
+
file_path = mode_config["prompt_file"]
|
34 |
+
initial_messages = llm_handler.build_initial_messages(file_path, mode)
|
35 |
+
# Don't create chatbot here, return state data only
|
36 |
+
state_data = {
|
37 |
+
"file_path": file_path,
|
38 |
+
"initial_messages": initial_messages,
|
39 |
+
"mode": mode,
|
40 |
+
# store config dict with provider
|
41 |
+
"llm_config": llm_handler.get_config_dict()
|
42 |
+
}
|
43 |
+
return state_data
|
44 |
+
|
45 |
+
async def predict(self, input, state):
|
46 |
+
if not self.node_publisher.is_initialized():
|
47 |
+
mode = state.get('mode')
|
48 |
+
self.node_publisher.initialize_node(mode)
|
49 |
+
|
50 |
+
initial_messages = state['initial_messages']
|
51 |
+
full_history = initial_messages + state.get('history', [])
|
52 |
+
user_input = f"# Query: {input}"
|
53 |
+
full_history.append({"role": "user", "content": user_input})
|
54 |
+
|
55 |
+
mode_config = MODE_CONFIG[state.get('mode')]
|
56 |
+
if mode_config["type"] == 'complex' and self.received_tasks:
|
57 |
+
for task in self.received_tasks:
|
58 |
+
task_prompt = f"# Task: {task}"
|
59 |
+
full_history.append({"role": "user", "content": task_prompt})
|
60 |
+
self.received_tasks = []
|
61 |
+
|
62 |
+
# Create a new LLMRequestHandler instance for each request
|
63 |
+
llm_config = state['llm_config']
|
64 |
+
llm_handler = LLMRequestHandler.create_from_config_dict(llm_config)
|
65 |
+
|
66 |
+
response = await llm_handler.make_completion(full_history)
|
67 |
+
if response:
|
68 |
+
full_history.append({"role": "assistant", "content": response})
|
69 |
+
else:
|
70 |
+
response = "Error: Unable to get response."
|
71 |
+
full_history.append({"role": "assistant", "content": response})
|
72 |
+
|
73 |
+
response_json = self.json_processor.process_response(response)
|
74 |
+
|
75 |
+
# Store the task plan for approval workflow
|
76 |
+
state.update({'pending_task_plan': response_json})
|
77 |
+
|
78 |
+
# Generate DAG visualization if valid task data is available
|
79 |
+
dag_image_path = None
|
80 |
+
confirm_button_visible = False
|
81 |
+
if response_json and "tasks" in response_json:
|
82 |
+
try:
|
83 |
+
dag_image_path = self.dag_visualizer.create_dag_visualization(
|
84 |
+
response_json,
|
85 |
+
title="Robot Task Dependency Graph - Pending Approval"
|
86 |
+
)
|
87 |
+
logger.info(f"DAG visualization generated: {dag_image_path}")
|
88 |
+
confirm_button_visible = True
|
89 |
+
except Exception as e:
|
90 |
+
logger.error(f"Failed to generate DAG visualization: {e}")
|
91 |
+
|
92 |
+
# Modify the messages format to match the "messages" type
|
93 |
+
messages = [{"role": message["role"], "content": message["content"]} for message in full_history[len(initial_messages):]]
|
94 |
+
updated_history = state.get('history', []) + [{"role": "user", "content": input}, {"role": "assistant", "content": response}]
|
95 |
+
state.update({'history': updated_history})
|
96 |
+
return messages, state, dag_image_path, gr.update(visible=confirm_button_visible)
|
97 |
+
|
98 |
+
def clear_chat(self, state):
|
99 |
+
state['history'] = []
|
100 |
+
|
101 |
+
def show_task_plan_editor(self, state):
|
102 |
+
"""
|
103 |
+
Show the current task plan in JSON format for manual editing.
|
104 |
+
"""
|
105 |
+
# Check for pending plan first, then deployed plan as fallback
|
106 |
+
pending_plan = state.get('pending_task_plan')
|
107 |
+
deployed_plan = state.get('deployed_task_plan')
|
108 |
+
|
109 |
+
# Use pending plan if available, otherwise use deployed plan
|
110 |
+
current_plan = pending_plan if pending_plan else deployed_plan
|
111 |
+
|
112 |
+
if current_plan and "tasks" in current_plan and len(current_plan["tasks"]) > 0:
|
113 |
+
import json
|
114 |
+
# Format JSON for better readability
|
115 |
+
formatted_json = json.dumps(current_plan, indent=2, ensure_ascii=False)
|
116 |
+
plan_status = "pending" if pending_plan else "deployed"
|
117 |
+
logger.info(f"π Task plan editor opened with {plan_status} plan")
|
118 |
+
|
119 |
+
# Set pending plan for editing (copy from deployed if needed)
|
120 |
+
if not pending_plan and deployed_plan:
|
121 |
+
state.update({'pending_task_plan': deployed_plan})
|
122 |
+
|
123 |
+
return (
|
124 |
+
gr.update(visible=True, value=formatted_json), # Show editor with current JSON
|
125 |
+
gr.update(visible=True), # Show Update DAG button
|
126 |
+
gr.update(visible=False), # Hide Validate & Deploy button
|
127 |
+
f"π **Task Plan Editor Opened**\n\nYou can now manually edit the task plan JSON below. {plan_status.title()} plan loaded for editing."
|
128 |
+
)
|
129 |
+
else:
|
130 |
+
# Provide a better template with example structure
|
131 |
+
template_json = """{
|
132 |
+
"tasks": [
|
133 |
+
{
|
134 |
+
"task": "example_task_1",
|
135 |
+
"instruction_function": {
|
136 |
+
"name": "example_function_name",
|
137 |
+
"robot_ids": ["robot_dump_truck_01"],
|
138 |
+
"dependencies": [],
|
139 |
+
"object_keywords": ["object1", "object2"]
|
140 |
+
}
|
141 |
+
}
|
142 |
+
]
|
143 |
+
}"""
|
144 |
+
logger.info("π Task plan editor opened with template")
|
145 |
+
return (
|
146 |
+
gr.update(visible=True, value=template_json), # Show template
|
147 |
+
gr.update(visible=True), # Show Update DAG button
|
148 |
+
gr.update(visible=False), # Hide Validate & Deploy button
|
149 |
+
"β οΈ **No Task Plan Available**\n\nStarting with example template. Please edit the JSON structure and update."
|
150 |
+
)
|
151 |
+
|
152 |
+
def update_dag_from_editor(self, edited_json, state):
|
153 |
+
"""
|
154 |
+
Update DAG visualization from manually edited JSON.
|
155 |
+
"""
|
156 |
+
try:
|
157 |
+
import json
|
158 |
+
# Parse the edited JSON
|
159 |
+
edited_plan = json.loads(edited_json)
|
160 |
+
|
161 |
+
# Validate the JSON structure
|
162 |
+
if "tasks" not in edited_plan:
|
163 |
+
raise ValueError("JSON must contain 'tasks' field")
|
164 |
+
|
165 |
+
# Store the edited plan
|
166 |
+
state.update({'pending_task_plan': edited_plan})
|
167 |
+
|
168 |
+
# Generate updated DAG visualization
|
169 |
+
dag_image_path = self.dag_visualizer.create_dag_visualization(
|
170 |
+
edited_plan,
|
171 |
+
title="Robot Task Dependency Graph - EDITED & PENDING APPROVAL"
|
172 |
+
)
|
173 |
+
|
174 |
+
logger.info("π DAG updated from manual edits")
|
175 |
+
|
176 |
+
return (
|
177 |
+
dag_image_path,
|
178 |
+
gr.update(visible=True), # Show Validate & Deploy button
|
179 |
+
gr.update(visible=False), # Hide editor
|
180 |
+
gr.update(visible=False), # Hide Update DAG button
|
181 |
+
"β
**DAG Updated Successfully**\n\nTask plan has been updated with your edits. Please review the visualization and click 'Validate & Deploy' to proceed.",
|
182 |
+
state
|
183 |
+
)
|
184 |
+
|
185 |
+
except json.JSONDecodeError as e:
|
186 |
+
error_msg = f"β **JSON Parsing Error**\n\nInvalid JSON format: {str(e)}\n\nPlease fix the JSON syntax and try again."
|
187 |
+
return (
|
188 |
+
None,
|
189 |
+
gr.update(visible=False), # Hide Validate & Deploy button
|
190 |
+
gr.update(visible=True), # Keep editor visible
|
191 |
+
gr.update(visible=True), # Keep Update DAG button visible
|
192 |
+
error_msg,
|
193 |
+
state
|
194 |
+
)
|
195 |
+
except Exception as e:
|
196 |
+
error_msg = f"β **Update Failed**\n\nError: {str(e)}"
|
197 |
+
logger.error(f"Failed to update DAG from editor: {e}")
|
198 |
+
return (
|
199 |
+
None,
|
200 |
+
gr.update(visible=False), # Hide Validate & Deploy button
|
201 |
+
gr.update(visible=True), # Keep editor visible
|
202 |
+
gr.update(visible=True), # Keep Update DAG button visible
|
203 |
+
error_msg,
|
204 |
+
state
|
205 |
+
)
|
206 |
+
|
207 |
+
def validate_and_deploy_task_plan(self, state):
|
208 |
+
"""
|
209 |
+
Validate and deploy the task plan to the construction site.
|
210 |
+
This function implements the safety confirmation workflow.
|
211 |
+
"""
|
212 |
+
pending_plan = state.get('pending_task_plan')
|
213 |
+
if pending_plan:
|
214 |
+
try:
|
215 |
+
# Deploy the approved task plan to ROS
|
216 |
+
self.node_publisher.publish_response(pending_plan)
|
217 |
+
|
218 |
+
# Update DAG visualization to show approved status
|
219 |
+
approved_image_path = None
|
220 |
+
if "tasks" in pending_plan:
|
221 |
+
approved_image_path = self.dag_visualizer.create_dag_visualization(
|
222 |
+
pending_plan,
|
223 |
+
title="Robot Task Dependency Graph - APPROVED & DEPLOYED"
|
224 |
+
)
|
225 |
+
|
226 |
+
# Keep the deployed plan for potential re-editing, but mark as deployed
|
227 |
+
state.update({'deployed_task_plan': pending_plan, 'pending_task_plan': None})
|
228 |
+
|
229 |
+
logger.info("β
Task plan validated and deployed to construction site")
|
230 |
+
|
231 |
+
# Return confirmation message and updated visualization
|
232 |
+
confirmation_msg = "β
**Task Plan Successfully Deployed**\n\nThe validated task dependency graph has been sent to the construction site robots. All safety protocols confirmed."
|
233 |
+
|
234 |
+
return (
|
235 |
+
confirmation_msg,
|
236 |
+
approved_image_path,
|
237 |
+
gr.update(visible=False), # Hide confirmation button
|
238 |
+
state
|
239 |
+
)
|
240 |
+
|
241 |
+
except Exception as e:
|
242 |
+
logger.error(f"Failed to deploy task plan: {e}")
|
243 |
+
error_msg = f"β **Deployment Failed**\n\nError: {str(e)}"
|
244 |
+
return (
|
245 |
+
error_msg,
|
246 |
+
None,
|
247 |
+
gr.update(visible=True), # Keep button visible for retry
|
248 |
+
state
|
249 |
+
)
|
250 |
+
else:
|
251 |
+
warning_msg = "β οΈ **No Task Plan to Deploy**\n\nPlease generate a task plan first."
|
252 |
+
return (
|
253 |
+
warning_msg,
|
254 |
+
None,
|
255 |
+
gr.update(visible=False),
|
256 |
+
state
|
257 |
+
)
|
258 |
+
|
259 |
+
def update_chatbot(self, mode, state):
|
260 |
+
# Destroy and reinitialize the ROS node
|
261 |
+
self.node_publisher.destroy_node()
|
262 |
+
if not rclpy.ok():
|
263 |
+
rclpy.init()
|
264 |
+
self.node_publisher = RosNodePublisher(mode)
|
265 |
+
self.json_processor = JsonProcessor()
|
266 |
+
|
267 |
+
# Update llm_handler with the new model settings
|
268 |
+
mode_config = MODE_CONFIG[mode]
|
269 |
+
model_version = mode_config["model_version"]
|
270 |
+
model_type = mode_config.get("model_type", "openai") # Ensure the correct model_type is used
|
271 |
+
provider = mode_config.get("provider", MODEL_CONFIG["provider"])
|
272 |
+
|
273 |
+
# Re-instantiate LLMRequestHandler with the new model_version and model_type
|
274 |
+
llm_handler = LLMRequestHandler(
|
275 |
+
model_version=model_version,
|
276 |
+
provider=provider,
|
277 |
+
max_tokens=mode_config.get("max_tokens", MODEL_CONFIG["max_tokens"]),
|
278 |
+
temperature=mode_config.get("temperature", MODEL_CONFIG["temperature"]),
|
279 |
+
frequency_penalty=mode_config.get("frequency_penalty", MODEL_CONFIG["frequency_penalty"]),
|
280 |
+
list_navigation_once=True,
|
281 |
+
model_type=model_type
|
282 |
+
)
|
283 |
+
|
284 |
+
# Update the prompt file and initial messages
|
285 |
+
file_path = mode_config["prompt_file"]
|
286 |
+
initial_messages = llm_handler.build_initial_messages(file_path, mode)
|
287 |
+
|
288 |
+
# Update state with the new handler and reset history
|
289 |
+
logger.info(f"Updating chatbot with {file_path}, model {model_version}, provider {provider}")
|
290 |
+
state['file_path'] = file_path
|
291 |
+
state['initial_messages'] = initial_messages
|
292 |
+
state['history'] = []
|
293 |
+
state['mode'] = mode
|
294 |
+
state['llm_config'] = llm_handler.get_config_dict() # Update the state with the new handler
|
295 |
+
|
296 |
+
logger.info(f"\033[33mMode updated to {mode}\033[0m")
|
297 |
+
return gr.update(value=[]), state
|
298 |
+
|
json_processor.py
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import re
|
3 |
+
from loguru import logger
|
4 |
+
|
5 |
+
class JsonProcessor:
|
6 |
+
def process_response(self, response):
|
7 |
+
try:
|
8 |
+
# Search for JSON string in the response
|
9 |
+
json_str_match = re.search(r'\{.*\}', response, re.DOTALL)
|
10 |
+
if json_str_match:
|
11 |
+
# Get the matched JSON string
|
12 |
+
json_str = json_str_match.group()
|
13 |
+
logger.debug(f"Full JSON string: {json_str}")
|
14 |
+
|
15 |
+
# Replace escape characters and remove trailing commas
|
16 |
+
json_str = json_str.replace("\\", "")
|
17 |
+
json_str = json_str.replace(r'\\_', '_')
|
18 |
+
json_str = re.sub(r',\s*}', '}', json_str)
|
19 |
+
json_str = re.sub(r',\s*\]', ']', json_str)
|
20 |
+
|
21 |
+
# Parse the JSON string
|
22 |
+
response_json = json.loads(json_str)
|
23 |
+
return response_json
|
24 |
+
else:
|
25 |
+
logger.error("No JSON string match found in response.")
|
26 |
+
return None
|
27 |
+
|
28 |
+
except json.JSONDecodeError as e:
|
29 |
+
logger.error(f"JSONDecodeError: {e}")
|
30 |
+
except Exception as e:
|
31 |
+
logger.error(f"Unexpected error: {e}")
|
32 |
+
|
33 |
+
return None
|
llm_request_handler.py
ADDED
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import json
|
3 |
+
import asyncio
|
4 |
+
from typing import List, Optional, Dict, Any
|
5 |
+
from loguru import logger
|
6 |
+
from pydantic import BaseModel
|
7 |
+
|
8 |
+
from langchain_core.prompts import ChatPromptTemplate
|
9 |
+
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
|
10 |
+
from langchain_openai import ChatOpenAI
|
11 |
+
from langchain_anthropic import ChatAnthropic
|
12 |
+
from langchain_groq import ChatGroq
|
13 |
+
from langchain_ollama import ChatOllama
|
14 |
+
|
15 |
+
from config import MODEL_CONFIG, INITIAL_MESSAGES_CONFIG, MODE_CONFIG, NAVIGATION_FUNCTIONS, ROBOT_SPECIFIC_FUNCTIONS, ROBOT_NAMES
|
16 |
+
|
17 |
+
class LLMRequestConfig(BaseModel):
|
18 |
+
model_name: str = MODEL_CONFIG["default_model"]
|
19 |
+
max_tokens: int = MODEL_CONFIG["max_tokens"]
|
20 |
+
temperature: float = MODEL_CONFIG["temperature"]
|
21 |
+
frequency_penalty: float = MODEL_CONFIG["frequency_penalty"]
|
22 |
+
list_navigation_once: bool = True
|
23 |
+
provider: str = "openai"
|
24 |
+
|
25 |
+
# Resolve Pydantic namespace conflicts
|
26 |
+
model_config = {"protected_namespaces": ()}
|
27 |
+
|
28 |
+
def to_dict(self):
|
29 |
+
return {
|
30 |
+
"model_name": self.model_name,
|
31 |
+
"max_tokens": self.max_tokens,
|
32 |
+
"temperature": self.temperature,
|
33 |
+
"frequency_penalty": self.frequency_penalty,
|
34 |
+
"list_navigation_once": self.list_navigation_once,
|
35 |
+
"provider": self.provider
|
36 |
+
}
|
37 |
+
|
38 |
+
@classmethod
|
39 |
+
def from_dict(cls, config_dict):
|
40 |
+
return cls(**config_dict)
|
41 |
+
|
42 |
+
class LLMRequestHandler:
|
43 |
+
class Message(BaseModel):
|
44 |
+
role: str
|
45 |
+
content: str
|
46 |
+
|
47 |
+
def __init__(self,
|
48 |
+
# Support both old and new parameter names for backward compatibility
|
49 |
+
model_version: str = None,
|
50 |
+
model_name: str = None,
|
51 |
+
max_tokens: int = None,
|
52 |
+
temperature: float = None,
|
53 |
+
frequency_penalty: float = None,
|
54 |
+
list_navigation_once: bool = None,
|
55 |
+
model_type: str = None,
|
56 |
+
provider: str = None,
|
57 |
+
config: Optional[LLMRequestConfig] = None):
|
58 |
+
|
59 |
+
# Initialize with config or from individual parameters
|
60 |
+
if config:
|
61 |
+
self.config = config
|
62 |
+
else:
|
63 |
+
# Create config from individual parameters, giving priority to new names
|
64 |
+
self.config = LLMRequestConfig(
|
65 |
+
model_name=model_name or model_version or MODEL_CONFIG["default_model"],
|
66 |
+
max_tokens=max_tokens or MODEL_CONFIG["max_tokens"],
|
67 |
+
temperature=temperature or MODEL_CONFIG["temperature"],
|
68 |
+
frequency_penalty=frequency_penalty or MODEL_CONFIG["frequency_penalty"],
|
69 |
+
list_navigation_once=list_navigation_once if list_navigation_once is not None else True,
|
70 |
+
provider=provider or model_type or "openai"
|
71 |
+
)
|
72 |
+
|
73 |
+
# Store parameters for easier access
|
74 |
+
self.model_name = self.config.model_name
|
75 |
+
self.model_version = self.model_name # Alias for backward compatibility
|
76 |
+
self.max_tokens = self.config.max_tokens
|
77 |
+
self.temperature = self.config.temperature
|
78 |
+
self.frequency_penalty = self.config.frequency_penalty
|
79 |
+
self.list_navigation_once = self.config.list_navigation_once
|
80 |
+
self.provider = self.config.provider
|
81 |
+
self.model_type = self.provider # Alias for backward compatibility
|
82 |
+
|
83 |
+
# Store API keys
|
84 |
+
self.openai_api_key = os.getenv("OPENAI_API_KEY")
|
85 |
+
self.anthropic_api_key = os.getenv("ANTHROPIC_API_KEY")
|
86 |
+
self.groq_api_key = os.getenv("GROQ_API_KEY")
|
87 |
+
|
88 |
+
# Create the appropriate LangChain LLM based on provider
|
89 |
+
self._setup_llm()
|
90 |
+
|
91 |
+
def _setup_llm(self):
|
92 |
+
"""Initialize the appropriate LangChain LLM based on provider."""
|
93 |
+
if "anthropic" in self.provider or "claude" in self.model_name:
|
94 |
+
self.llm = ChatAnthropic(
|
95 |
+
api_key=self.anthropic_api_key,
|
96 |
+
model_name=self.model_name,
|
97 |
+
max_tokens=self.max_tokens,
|
98 |
+
temperature=self.temperature
|
99 |
+
)
|
100 |
+
elif "ollama" in self.provider or "ollama" in self.model_name:
|
101 |
+
self.llm = ChatOllama(
|
102 |
+
model=self.model_name,
|
103 |
+
max_tokens=self.max_tokens,
|
104 |
+
temperature=self.temperature,
|
105 |
+
base_url="http://host.docker.internal:11434"
|
106 |
+
)
|
107 |
+
elif "groq" in self.provider or "llama" in self.model_name:
|
108 |
+
self.llm = ChatGroq(
|
109 |
+
api_key=self.groq_api_key,
|
110 |
+
model_name=self.model_name,
|
111 |
+
max_tokens=self.max_tokens,
|
112 |
+
temperature=self.temperature,
|
113 |
+
frequency_penalty=self.frequency_penalty
|
114 |
+
)
|
115 |
+
else: # Default to OpenAI
|
116 |
+
self.llm = ChatOpenAI(
|
117 |
+
api_key=self.openai_api_key,
|
118 |
+
model_name=self.model_name,
|
119 |
+
max_tokens=self.max_tokens,
|
120 |
+
temperature=self.temperature,
|
121 |
+
frequency_penalty=self.frequency_penalty
|
122 |
+
)
|
123 |
+
|
124 |
+
def get_config_dict(self):
|
125 |
+
"""Get a serializable configuration dictionary"""
|
126 |
+
return self.config.to_dict()
|
127 |
+
|
128 |
+
@staticmethod
|
129 |
+
def create_from_config_dict(config_dict):
|
130 |
+
"""Create a new handler instance from a config dictionary"""
|
131 |
+
config = LLMRequestConfig.from_dict(config_dict)
|
132 |
+
return LLMRequestHandler(config=config)
|
133 |
+
|
134 |
+
def load_object_data(self) -> Dict[str, Any]:
|
135 |
+
"""Load environment information (E) from a JSON file"""
|
136 |
+
json_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'ros2_ws', 'src', 'breakdown_function_handler', 'object_database', 'object_database.json'))
|
137 |
+
|
138 |
+
with open(json_path, 'r') as json_file:
|
139 |
+
data = json.load(json_file)
|
140 |
+
|
141 |
+
return self.format_env_object(data)
|
142 |
+
|
143 |
+
def format_env_object(self, data: List[Dict[str, Any]]) -> Dict[str, Any]:
|
144 |
+
"""Format the environment data (E) for use in the prompt"""
|
145 |
+
formatted_env_object = {}
|
146 |
+
for obj in data:
|
147 |
+
object_name = obj['object_name']
|
148 |
+
target_position = obj['target_position']
|
149 |
+
shape = obj['shape']
|
150 |
+
formatted_env_object[object_name] = {
|
151 |
+
"position": {
|
152 |
+
"x": target_position["x"],
|
153 |
+
"y": target_position["y"]
|
154 |
+
},
|
155 |
+
"shape": shape
|
156 |
+
}
|
157 |
+
return formatted_env_object
|
158 |
+
|
159 |
+
def build_initial_messages(self, file_path: str, mode: str) -> List[Dict[str, str]]:
|
160 |
+
"""Build the initial prompt (P = (I, E, R, S))"""
|
161 |
+
with open(file_path, 'r', encoding='utf-8') as file:
|
162 |
+
user1 = file.read() # Example user instructions for few-shot learning (optional)
|
163 |
+
|
164 |
+
system = INITIAL_MESSAGES_CONFIG["system"]
|
165 |
+
|
166 |
+
# Load environment information (E)
|
167 |
+
env_objects = self.load_object_data()
|
168 |
+
|
169 |
+
# Create the user introduction with robot set (R), skills (S), and environment (E)
|
170 |
+
user_intro = INITIAL_MESSAGES_CONFIG["user_intro"]["default"] + INITIAL_MESSAGES_CONFIG["user_intro"].get(mode, "")
|
171 |
+
functions_description = MODE_CONFIG[mode].get("functions_description", "")
|
172 |
+
|
173 |
+
# Format user introduction with the instruction (I), robot set (R), skills (S), and environment (E)
|
174 |
+
user_intro = user_intro.format(
|
175 |
+
library=NAVIGATION_FUNCTIONS+ROBOT_SPECIFIC_FUNCTIONS,
|
176 |
+
env_objects=env_objects,
|
177 |
+
robot_names=ROBOT_NAMES,
|
178 |
+
fewshot_examples=user1,
|
179 |
+
functions_description=functions_description
|
180 |
+
)
|
181 |
+
|
182 |
+
assistant1 = INITIAL_MESSAGES_CONFIG["assistant"]
|
183 |
+
|
184 |
+
# Construct the messages (system, user, assistant)
|
185 |
+
messages = [
|
186 |
+
{"role": "system", "content": system},
|
187 |
+
{"role": "user", "content": user_intro},
|
188 |
+
{"role": "assistant", "content": assistant1}
|
189 |
+
]
|
190 |
+
return messages
|
191 |
+
|
192 |
+
def add_user_message(self, messages: List[Dict[str, str]], content: str) -> None:
|
193 |
+
"""Add a user message with natural language instruction (I)"""
|
194 |
+
user_message = self.Message(role="user", content=content)
|
195 |
+
messages.append(user_message.model_dump())
|
196 |
+
|
197 |
+
def _convert_to_langchain_messages(self, full_history: List[Dict[str, str]]):
|
198 |
+
"""Convert traditional message format to LangChain message objects"""
|
199 |
+
lc_messages = []
|
200 |
+
for msg in full_history:
|
201 |
+
if msg["role"] == "system":
|
202 |
+
lc_messages.append(SystemMessage(content=msg["content"]))
|
203 |
+
elif msg["role"] == "user":
|
204 |
+
lc_messages.append(HumanMessage(content=msg["content"]))
|
205 |
+
elif msg["role"] == "assistant":
|
206 |
+
lc_messages.append(AIMessage(content=msg["content"]))
|
207 |
+
return lc_messages
|
208 |
+
|
209 |
+
async def make_completion(self, full_history: List[Dict[str, str]]) -> Optional[str]:
|
210 |
+
"""Make a completion request to the selected model using LangChain"""
|
211 |
+
logger.debug(f"Using model: {self.model_name}")
|
212 |
+
try:
|
213 |
+
# Convert traditional messages to LangChain message format
|
214 |
+
lc_messages = self._convert_to_langchain_messages(full_history)
|
215 |
+
|
216 |
+
# Create a chat prompt template
|
217 |
+
chat_prompt = ChatPromptTemplate.from_messages(lc_messages)
|
218 |
+
|
219 |
+
# Get the response
|
220 |
+
chain = chat_prompt | self.llm
|
221 |
+
response = await chain.ainvoke({})
|
222 |
+
|
223 |
+
# Extract the content from the response
|
224 |
+
return response.content if hasattr(response, 'content') else str(response)
|
225 |
+
|
226 |
+
except Exception as e:
|
227 |
+
logger.error(f"Error making completion: {e}")
|
228 |
+
return None
|
229 |
+
|
230 |
+
|
231 |
+
if __name__ == "__main__":
|
232 |
+
async def main():
|
233 |
+
selected_model_index = 3 # 0 for OpenAI, 1 for Anthropic, 2 for LLaMA, 3 for Ollama
|
234 |
+
|
235 |
+
model_options = MODEL_CONFIG["model_options"]
|
236 |
+
|
237 |
+
# Choose the model based on selected_model_index
|
238 |
+
if selected_model_index == 0:
|
239 |
+
model = model_options[0]
|
240 |
+
provider = "openai"
|
241 |
+
elif selected_model_index == 1:
|
242 |
+
model = model_options[4]
|
243 |
+
provider = "anthropic"
|
244 |
+
elif selected_model_index == 2:
|
245 |
+
model = model_options[6]
|
246 |
+
provider = "groq"
|
247 |
+
elif selected_model_index == 3:
|
248 |
+
model = "llama3"
|
249 |
+
provider = "ollama"
|
250 |
+
else:
|
251 |
+
raise ValueError("Invalid selected_model_index")
|
252 |
+
|
253 |
+
logger.debug("Starting test llm_request_handler with LangChain...")
|
254 |
+
config = LLMRequestConfig(
|
255 |
+
model_name=model,
|
256 |
+
list_navigation_once=True,
|
257 |
+
provider=provider
|
258 |
+
)
|
259 |
+
handler = LLMRequestHandler(config=config)
|
260 |
+
|
261 |
+
# Build initial messages based on the selected model
|
262 |
+
if selected_model_index == 0:
|
263 |
+
messages = handler.build_initial_messages("/root/share/QA_LLM_Module/prompts/swarm/dart.txt", "dart_gpt_4o")
|
264 |
+
elif selected_model_index == 1:
|
265 |
+
messages = handler.build_initial_messages("/root/share/QA_LLM_Module/prompts/swarm/dart.txt", "dart_claude_3_sonnet")
|
266 |
+
elif selected_model_index == 2:
|
267 |
+
messages = handler.build_initial_messages("/root/share/QA_LLM_Module/prompts/swarm/dart.txt", "dart_llama_3_3_70b")
|
268 |
+
elif selected_model_index == 3:
|
269 |
+
messages = handler.build_initial_messages("/root/share/QA_LLM_Module/prompts/swarm/dart.txt", "dart_ollama_llama3_1_8b")
|
270 |
+
|
271 |
+
# Add a natural language instruction (I) to the prompt
|
272 |
+
handler.add_user_message(messages, "Excavator 1 performs excavation, then excavator 2 performs, then dump 1 performs unload.")
|
273 |
+
|
274 |
+
# Request completion from the model
|
275 |
+
response = await handler.make_completion(messages)
|
276 |
+
logger.debug(f"Response from make_completion: {response}")
|
277 |
+
|
278 |
+
asyncio.run(main())
|
main.py
ADDED
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
from loguru import logger
|
3 |
+
from gradio_llm_interface import GradioLlmInterface
|
4 |
+
from config import GRADIO_MESSAGE_MODES, MODE_CONFIG
|
5 |
+
import openai
|
6 |
+
import os
|
7 |
+
from dotenv import load_dotenv
|
8 |
+
|
9 |
+
# Load environment variables from .env file
|
10 |
+
load_dotenv()
|
11 |
+
|
12 |
+
# Speech-to-text function using OpenAI Whisper
|
13 |
+
def audio_to_text(audio):
|
14 |
+
if audio is None:
|
15 |
+
return "No audio file provided."
|
16 |
+
|
17 |
+
try:
|
18 |
+
# Get OpenAI API key from environment variable
|
19 |
+
openai_api_key = os.getenv("OPENAI_API_KEY")
|
20 |
+
if not openai_api_key:
|
21 |
+
return "Error: OpenAI API key not found. Please set OPENAI_API_KEY environment variable."
|
22 |
+
|
23 |
+
# Initialize OpenAI client
|
24 |
+
client = openai.OpenAI(api_key=openai_api_key)
|
25 |
+
|
26 |
+
# Open and transcribe the audio file
|
27 |
+
with open(audio, "rb") as audio_file:
|
28 |
+
transcript = client.audio.transcriptions.create(
|
29 |
+
model="whisper-1",
|
30 |
+
file=audio_file
|
31 |
+
)
|
32 |
+
|
33 |
+
return transcript.text
|
34 |
+
|
35 |
+
except FileNotFoundError:
|
36 |
+
return "Error: Audio file not found."
|
37 |
+
except openai.AuthenticationError:
|
38 |
+
return "Error: Invalid OpenAI API key."
|
39 |
+
except openai.RateLimitError:
|
40 |
+
return "Error: OpenAI API rate limit exceeded."
|
41 |
+
except Exception as e:
|
42 |
+
logger.error(f"Speech-to-text error: {str(e)}")
|
43 |
+
return f"Error during speech recognition: {str(e)}"
|
44 |
+
|
45 |
+
def main():
|
46 |
+
gradio_ros_interface = GradioLlmInterface()
|
47 |
+
|
48 |
+
title_markdown = ("""
|
49 |
+
# π DART-LLM: Dependency-Aware Multi-Robot Task Decomposition and Execution using Large Language Models
|
50 |
+
[[Project Page](https://wyd0817.github.io/project-dart-llm/)] [[Code](https://github.com/wyd0817/gradio_gpt_interface)] [[Model](https://artificialanalysis.ai/)] | π [[RoboQA](https://www.overleaf.com/project/6614a987ae2994cae02efcb2)]
|
51 |
+
""")
|
52 |
+
|
53 |
+
with gr.Blocks(css="""
|
54 |
+
#text-input, #audio-input {
|
55 |
+
height: 100px; /* Unified height */
|
56 |
+
max-height: 100px;
|
57 |
+
width: 100%; /* Full container width */
|
58 |
+
margin: 0;
|
59 |
+
}
|
60 |
+
.input-container {
|
61 |
+
display: flex; /* Flex layout */
|
62 |
+
gap: 10px; /* Spacing */
|
63 |
+
align-items: center; /* Vertical alignment */
|
64 |
+
}
|
65 |
+
#voice-input-container {
|
66 |
+
display: flex;
|
67 |
+
align-items: center;
|
68 |
+
gap: 15px;
|
69 |
+
margin: 15px 0;
|
70 |
+
padding: 15px;
|
71 |
+
background: linear-gradient(135deg, #ffeef8 0%, #fff5f5 100%);
|
72 |
+
border-radius: 20px;
|
73 |
+
border: 1px solid #ffe4e6;
|
74 |
+
}
|
75 |
+
#voice-btn {
|
76 |
+
width: 50px !important;
|
77 |
+
height: 50px !important;
|
78 |
+
border-radius: 50% !important;
|
79 |
+
font-size: 20px !important;
|
80 |
+
background: linear-gradient(135deg, #ff6b9d 0%, #c44569 100%) !important;
|
81 |
+
color: white !important;
|
82 |
+
border: none !important;
|
83 |
+
box-shadow: 0 4px 15px rgba(255, 107, 157, 0.3) !important;
|
84 |
+
transition: all 0.3s ease !important;
|
85 |
+
}
|
86 |
+
#voice-btn:hover {
|
87 |
+
transform: scale(1.05) !important;
|
88 |
+
box-shadow: 0 6px 20px rgba(255, 107, 157, 0.4) !important;
|
89 |
+
}
|
90 |
+
#voice-btn:active {
|
91 |
+
transform: scale(0.95) !important;
|
92 |
+
}
|
93 |
+
.voice-recording {
|
94 |
+
background: linear-gradient(135deg, #ff4757 0%, #ff3742 100%) !important;
|
95 |
+
animation: pulse 1.5s infinite !important;
|
96 |
+
}
|
97 |
+
@keyframes pulse {
|
98 |
+
0% { box-shadow: 0 4px 15px rgba(255, 71, 87, 0.3); }
|
99 |
+
50% { box-shadow: 0 4px 25px rgba(255, 71, 87, 0.6); }
|
100 |
+
100% { box-shadow: 0 4px 15px rgba(255, 71, 87, 0.3); }
|
101 |
+
}
|
102 |
+
#voice-status {
|
103 |
+
color: #ff6b9d;
|
104 |
+
font-size: 14px;
|
105 |
+
font-weight: 500;
|
106 |
+
text-align: center;
|
107 |
+
margin-top: 10px;
|
108 |
+
}
|
109 |
+
/* Enhanced layout for left-right split */
|
110 |
+
.gradio-container .gradio-row {
|
111 |
+
gap: 20px; /* Add spacing between columns */
|
112 |
+
}
|
113 |
+
.gradio-column {
|
114 |
+
padding: 10px;
|
115 |
+
border-radius: 8px;
|
116 |
+
background-color: var(--panel-background-fill);
|
117 |
+
}
|
118 |
+
/* Chat interface styling */
|
119 |
+
.chat-column {
|
120 |
+
border: 1px solid var(--border-color-primary);
|
121 |
+
}
|
122 |
+
/* DAG visualization column styling */
|
123 |
+
.dag-column {
|
124 |
+
border: 1px solid var(--border-color-primary);
|
125 |
+
}
|
126 |
+
""") as demo:
|
127 |
+
gr.Markdown(title_markdown)
|
128 |
+
|
129 |
+
mode_choices = [MODE_CONFIG[mode]["display_name"] for mode in GRADIO_MESSAGE_MODES]
|
130 |
+
mode_selector = gr.Radio(choices=mode_choices, label="Backend model", value=mode_choices[0])
|
131 |
+
clear_button = gr.Button("Clear Chat")
|
132 |
+
|
133 |
+
logger.info("Starting Gradio GPT Interface...")
|
134 |
+
|
135 |
+
initial_mode = GRADIO_MESSAGE_MODES[0]
|
136 |
+
|
137 |
+
def update_mode(selected_mode, state):
|
138 |
+
mode_key = [key for key, value in MODE_CONFIG.items() if value["display_name"] == selected_mode][0]
|
139 |
+
return gradio_ros_interface.update_chatbot(mode_key, state)
|
140 |
+
|
141 |
+
# Main content area with left-right layout
|
142 |
+
with gr.Row():
|
143 |
+
# Left column: Chat interface
|
144 |
+
with gr.Column(scale=1, elem_classes=["chat-column"]):
|
145 |
+
gr.Markdown("### π€ DART-LLM Chat Interface")
|
146 |
+
# Create chatbot component in the left column
|
147 |
+
chatbot_container = gr.Chatbot(label="DART-LLM", type="messages")
|
148 |
+
|
149 |
+
# Initialize the interface and get state data
|
150 |
+
state_data = gradio_ros_interface.initialize_interface(initial_mode)
|
151 |
+
state = gr.State(state_data)
|
152 |
+
|
153 |
+
# Add input area in the left column
|
154 |
+
with gr.Row(elem_id="input-container"):
|
155 |
+
txt = gr.Textbox(show_label=False, placeholder="Enter text and press enter", elem_id="text-input", container=False)
|
156 |
+
|
157 |
+
with gr.Row(elem_id="voice-input-container"):
|
158 |
+
with gr.Column(scale=4):
|
159 |
+
# Hidden audio component
|
160 |
+
audio_input = gr.Audio(
|
161 |
+
sources=["microphone"],
|
162 |
+
type="filepath",
|
163 |
+
elem_id="audio-input",
|
164 |
+
show_label=False,
|
165 |
+
interactive=True,
|
166 |
+
streaming=False,
|
167 |
+
visible=False
|
168 |
+
)
|
169 |
+
# Voice input status display
|
170 |
+
voice_status = gr.Markdown("", elem_id="voice-status", visible=False)
|
171 |
+
|
172 |
+
with gr.Column(scale=1, min_width=80):
|
173 |
+
# Main voice button
|
174 |
+
voice_btn = gr.Button(
|
175 |
+
"ποΈ",
|
176 |
+
elem_id="voice-btn",
|
177 |
+
variant="secondary",
|
178 |
+
size="sm",
|
179 |
+
scale=1
|
180 |
+
)
|
181 |
+
|
182 |
+
# Example prompts in the left column
|
183 |
+
gr.Examples(
|
184 |
+
examples=[
|
185 |
+
"Dump truck 1 goes to the puddle for inspection, after which all robots avoid the puddle",
|
186 |
+
"Send Excavator 1 and Dump Truck 1 to the soil area; Excavator 1 will excavate and unload, followed by Dump Truck 1 proceeding to the puddle for unloading."
|
187 |
+
],
|
188 |
+
inputs=txt
|
189 |
+
)
|
190 |
+
|
191 |
+
# Right column: DAG visualization and controls
|
192 |
+
with gr.Column(scale=1, elem_classes=["dag-column"]):
|
193 |
+
gr.Markdown("### π Task Dependency Visualization")
|
194 |
+
# DAG visualization display
|
195 |
+
dag_image = gr.Image(label="Task Dependency Graph", visible=True, height=600)
|
196 |
+
|
197 |
+
# Task plan editing section
|
198 |
+
task_editor = gr.Code(
|
199 |
+
label="Task Plan JSON Editor",
|
200 |
+
language="json",
|
201 |
+
visible=False,
|
202 |
+
lines=15,
|
203 |
+
interactive=True
|
204 |
+
)
|
205 |
+
|
206 |
+
# Control buttons section
|
207 |
+
with gr.Row():
|
208 |
+
with gr.Column(scale=2):
|
209 |
+
deployment_status = gr.Markdown("", visible=True)
|
210 |
+
with gr.Column(scale=1):
|
211 |
+
with gr.Row():
|
212 |
+
edit_task_btn = gr.Button(
|
213 |
+
"π Edit Task Plan",
|
214 |
+
variant="secondary",
|
215 |
+
visible=False,
|
216 |
+
size="sm"
|
217 |
+
)
|
218 |
+
update_dag_btn = gr.Button(
|
219 |
+
"π Update DAG Visualization",
|
220 |
+
variant="secondary",
|
221 |
+
visible=False,
|
222 |
+
size="sm"
|
223 |
+
)
|
224 |
+
validate_deploy_btn = gr.Button(
|
225 |
+
"π Validate & Deploy Task Plan",
|
226 |
+
variant="primary",
|
227 |
+
visible=False,
|
228 |
+
size="sm"
|
229 |
+
)
|
230 |
+
|
231 |
+
mode_selector.change(update_mode, inputs=[mode_selector, state], outputs=[chatbot_container, state])
|
232 |
+
clear_button.click(gradio_ros_interface.clear_chat, inputs=[state], outputs=[chatbot_container])
|
233 |
+
|
234 |
+
# Handle text input submission
|
235 |
+
async def handle_text_submit(text, state):
|
236 |
+
messages, state, dag_image_path, validate_btn_update = await gradio_ros_interface.predict(text, state)
|
237 |
+
# Show edit button when task plan is generated
|
238 |
+
edit_btn_visible = validate_btn_update.get('visible', False)
|
239 |
+
return (
|
240 |
+
"", # Clear the text input after submission
|
241 |
+
messages,
|
242 |
+
state,
|
243 |
+
dag_image_path,
|
244 |
+
validate_btn_update,
|
245 |
+
gr.update(visible=edit_btn_visible) # Show edit button
|
246 |
+
)
|
247 |
+
|
248 |
+
txt.submit(handle_text_submit, [txt, state], [txt, chatbot_container, state, dag_image, validate_deploy_btn, edit_task_btn])
|
249 |
+
|
250 |
+
# Voice input state management
|
251 |
+
voice_recording = gr.State(False)
|
252 |
+
|
253 |
+
# Voice button click handler
|
254 |
+
def handle_voice_input(audio, is_recording):
|
255 |
+
logger.info(f"Voice button clicked, current recording state: {is_recording}")
|
256 |
+
|
257 |
+
if not is_recording:
|
258 |
+
# Start recording state
|
259 |
+
logger.info("Starting recording...")
|
260 |
+
return (
|
261 |
+
gr.update(value="π΄", elem_classes=["voice-recording"]), # Change button style
|
262 |
+
"π¬ Recording in progress...", # Status message
|
263 |
+
gr.update(visible=True), # Show status
|
264 |
+
gr.update(visible=True), # Show audio component
|
265 |
+
True, # Update recording state
|
266 |
+
"" # Clear text box
|
267 |
+
)
|
268 |
+
else:
|
269 |
+
# Stop recording and transcribe
|
270 |
+
logger.info("Stopping recording, starting transcription...")
|
271 |
+
if audio is not None and audio != "":
|
272 |
+
try:
|
273 |
+
text = audio_to_text(audio)
|
274 |
+
logger.info(f"Transcription completed: {text}")
|
275 |
+
return (
|
276 |
+
gr.update(value="ποΈ", elem_classes=[]), # Restore button style
|
277 |
+
"β¨ Transcription completed!", # Success message
|
278 |
+
gr.update(visible=True), # Show status
|
279 |
+
gr.update(visible=False), # Hide audio component
|
280 |
+
False, # Reset recording state
|
281 |
+
text # Fill in transcribed text
|
282 |
+
)
|
283 |
+
except Exception as e:
|
284 |
+
logger.error(f"Transcription error: {e}")
|
285 |
+
return (
|
286 |
+
gr.update(value="ποΈ", elem_classes=[]), # Restore button style
|
287 |
+
f"β Transcription failed: {str(e)}",
|
288 |
+
gr.update(visible=True),
|
289 |
+
gr.update(visible=False),
|
290 |
+
False,
|
291 |
+
""
|
292 |
+
)
|
293 |
+
else:
|
294 |
+
logger.warning("No audio detected")
|
295 |
+
return (
|
296 |
+
gr.update(value="ποΈ", elem_classes=[]), # Restore button style
|
297 |
+
"β οΈ No audio detected, please record again",
|
298 |
+
gr.update(visible=True),
|
299 |
+
gr.update(visible=False),
|
300 |
+
False,
|
301 |
+
""
|
302 |
+
)
|
303 |
+
|
304 |
+
# Voice button event handling
|
305 |
+
voice_btn.click(
|
306 |
+
handle_voice_input,
|
307 |
+
inputs=[audio_input, voice_recording],
|
308 |
+
outputs=[voice_btn, voice_status, voice_status, audio_input, voice_recording, txt]
|
309 |
+
)
|
310 |
+
|
311 |
+
# Audio state change listener - automatic prompt
|
312 |
+
def on_audio_change(audio):
|
313 |
+
if audio is not None:
|
314 |
+
logger.info("Audio file detected")
|
315 |
+
return "π΅ Audio detected, you can click the button to complete transcription"
|
316 |
+
return ""
|
317 |
+
|
318 |
+
audio_input.change(
|
319 |
+
on_audio_change,
|
320 |
+
inputs=[audio_input],
|
321 |
+
outputs=[voice_status]
|
322 |
+
)
|
323 |
+
|
324 |
+
# Handle task plan editing
|
325 |
+
edit_task_btn.click(
|
326 |
+
gradio_ros_interface.show_task_plan_editor,
|
327 |
+
inputs=[state],
|
328 |
+
outputs=[task_editor, update_dag_btn, validate_deploy_btn, deployment_status]
|
329 |
+
)
|
330 |
+
|
331 |
+
# Handle DAG update from editor
|
332 |
+
update_dag_btn.click(
|
333 |
+
gradio_ros_interface.update_dag_from_editor,
|
334 |
+
inputs=[task_editor, state],
|
335 |
+
outputs=[dag_image, validate_deploy_btn, task_editor, update_dag_btn, deployment_status, state]
|
336 |
+
)
|
337 |
+
|
338 |
+
# Handle validation and deployment
|
339 |
+
validate_deploy_btn.click(
|
340 |
+
gradio_ros_interface.validate_and_deploy_task_plan,
|
341 |
+
inputs=[state],
|
342 |
+
outputs=[deployment_status, dag_image, validate_deploy_btn, state]
|
343 |
+
)
|
344 |
+
|
345 |
+
demo.launch(server_port=8080, share=True)
|
346 |
+
|
347 |
+
if __name__ == "__main__":
|
348 |
+
main()
|
monitor_topics.sh
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/bin/bash
|
2 |
+
|
3 |
+
echo "π ROS Topic Monitor for QA_LLM_Module"
|
4 |
+
echo "====================================="
|
5 |
+
|
6 |
+
# Check if ROS2 is running
|
7 |
+
if ! command -v ros2 &> /dev/null; then
|
8 |
+
echo "β ROS2 not found. Please source your ROS2 environment."
|
9 |
+
exit 1
|
10 |
+
fi
|
11 |
+
|
12 |
+
echo "π Active Topics:"
|
13 |
+
ros2 topic list | grep -E "(instruction_topic|keywords_topic|tasks_topic|robovla)" || echo " No relevant topics found"
|
14 |
+
|
15 |
+
echo ""
|
16 |
+
echo "π€ Active Nodes:"
|
17 |
+
ros2 node list | grep robovla || echo " No robovla nodes found"
|
18 |
+
|
19 |
+
echo ""
|
20 |
+
echo "π― Topic Information:"
|
21 |
+
for topic in "/instruction_topic" "/keywords_topic" "/tasks_topic"; do
|
22 |
+
if ros2 topic list | grep -q "^${topic}$"; then
|
23 |
+
echo " Topic: $topic"
|
24 |
+
ros2 topic info $topic
|
25 |
+
echo ""
|
26 |
+
fi
|
27 |
+
done
|
28 |
+
|
29 |
+
echo "π Real-time Monitoring (Press Ctrl+C to stop):"
|
30 |
+
echo "Monitoring key topics for messages..."
|
31 |
+
|
32 |
+
# Monitor in background and display messages
|
33 |
+
{
|
34 |
+
ros2 topic echo /instruction_topic &
|
35 |
+
PID1=$!
|
36 |
+
ros2 topic echo /keywords_topic &
|
37 |
+
PID2=$!
|
38 |
+
ros2 topic echo /tasks_topic &
|
39 |
+
PID3=$!
|
40 |
+
|
41 |
+
# Wait for user interrupt
|
42 |
+
trap "kill $PID1 $PID2 $PID3 2>/dev/null; exit" INT
|
43 |
+
wait
|
44 |
+
}
|
prompts/VoxPoser/composer_prompt.txt
ADDED
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
from env_utils import execute, reset_to_default_pose
|
3 |
+
from perception_utils import parse_query_obj
|
4 |
+
from plan_utils import get_affordance_map, get_avoidance_map, get_velocity_map, get_rotation_map, get_gripper_map
|
5 |
+
|
6 |
+
# Query: move ee forward for 10cm.
|
7 |
+
movable = parse_query_obj('gripper')
|
8 |
+
affordance_map = get_affordance_map(f'a point 10cm in front of {movable.position}')
|
9 |
+
execute(movable, affordance_map)
|
10 |
+
|
11 |
+
# Query: go back to default.
|
12 |
+
reset_to_default_pose()
|
13 |
+
|
14 |
+
# Query: move the gripper behind the bowl, and slow down when near the bowl.
|
15 |
+
movable = parse_query_obj('gripper')
|
16 |
+
affordance_map = get_affordance_map('a point 15cm behind the bowl')
|
17 |
+
avoidance_map = get_avoidance_map('10cm near the bowl')
|
18 |
+
velocity_map = get_velocity_map('slow down when near the bowl')
|
19 |
+
execute(movable, affordance_map=affordance_map, avoidance_map=avoidance_map, velocity_map=velocity_map)
|
20 |
+
|
21 |
+
# Query: move to the back side of the table while staying at least 5cm from the blue block.
|
22 |
+
movable = parse_query_obj('gripper')
|
23 |
+
affordance_map = get_affordance_map('a point on the back side of the table')
|
24 |
+
avoidance_map = get_avoidance_map('5cm from the blue block')
|
25 |
+
execute(movable, affordance_map=affordance_map, avoidance_map=avoidance_map)
|
26 |
+
|
27 |
+
# Query: move to the top of the plate and face the plate.
|
28 |
+
movable = parse_query_obj('gripper')
|
29 |
+
affordance_map = get_affordance_map('a point 10cm above the plate')
|
30 |
+
rotation_map = get_rotation_map('face the plate')
|
31 |
+
execute(movable, affordance_map=affordance_map, rotation_map=rotation_map)
|
32 |
+
|
33 |
+
# Query: drop the toy inside container.
|
34 |
+
movable = parse_query_obj('gripper')
|
35 |
+
affordance_map = get_affordance_map('a point 15cm above the container')
|
36 |
+
gripper_map = get_gripper_map('close everywhere but open when on top of the container')
|
37 |
+
execute(movable, affordance_map=affordance_map, gripper_map=gripper_map)
|
38 |
+
|
39 |
+
# Query: push close the topmost drawer.
|
40 |
+
movable = parse_query_obj('topmost drawer handle')
|
41 |
+
affordance_map = get_affordance_map('a point 30cm into the topmost drawer handle')
|
42 |
+
execute(movable, affordance_map=affordance_map)
|
43 |
+
|
44 |
+
# Query: push the second to the left block along the red line.
|
45 |
+
movable = parse_query_obj('second to the left block')
|
46 |
+
affordance_map = get_affordance_map('the red line')
|
47 |
+
execute(movable, affordance_map=affordance_map)
|
48 |
+
|
49 |
+
# Query: grasp the blue block from the table at a quarter of the speed.
|
50 |
+
movable = parse_query_obj('gripper')
|
51 |
+
affordance_map = get_affordance_map('a point at the center of blue block')
|
52 |
+
velocity_map = get_velocity_map('quarter of the speed')
|
53 |
+
gripper_map = get_gripper_map('open everywhere except 1cm around the blue block')
|
54 |
+
execute(movable, affordance_map=affordance_map, velocity_map=velocity_map, gripper_map=gripper_map)
|
55 |
+
|
56 |
+
# Query: move to the left of the brown block.
|
57 |
+
movable = parse_query_obj('gripper')
|
58 |
+
affordance_map = get_affordance_map('a point 10cm to the left of the brown block')
|
59 |
+
execute(movable, affordance_map=affordance_map)
|
60 |
+
|
61 |
+
# Query: move to the top of the tray that contains the lemon.
|
62 |
+
movable = parse_query_obj('gripper')
|
63 |
+
affordance_map = get_affordance_map('a point 10cm above the tray that contains the lemon')
|
64 |
+
execute(movable, affordance_map=affordance_map)
|
65 |
+
|
66 |
+
# Query: close drawer by 5cm.
|
67 |
+
movable = parse_query_obj('drawer handle')
|
68 |
+
affordance_map = get_affordance_map('a point 5cm into the drawer handle')
|
69 |
+
execute(movable, affordance_map=affordance_map)
|
70 |
+
|
71 |
+
# Query: move to 5cm on top of the soda can, at 0.5x speed when within 20cm of the wooden mug, and keep at least 15cm away from the wooden mug.
|
72 |
+
movable = parse_query_obj('gripper')
|
73 |
+
affordance_map = get_affordance_map('a point 5cm above the soda can')
|
74 |
+
avoidance_map = get_avoidance_map('15cm from the wooden mug')
|
75 |
+
velocity_map = get_velocity_map('0.5x speed when within 20cm of the wooden mug')
|
76 |
+
execute(movable, affordance_map=affordance_map, avoidance_map=avoidance_map, velocity_map=velocity_map)
|
77 |
+
|
78 |
+
# Query: wipe the red dot but avoid the blue block.
|
79 |
+
movable = parse_query_obj('gripper')
|
80 |
+
affordance_map = get_affordance_map('the red dot')
|
81 |
+
avoidance_map = get_avoidance_map('10cm from the blue block')
|
82 |
+
execute(movable, affordance_map=affordance_map, avoidance_map=avoidance_map)
|
83 |
+
|
84 |
+
# Query: grasp the mug from the shelf.
|
85 |
+
movable = parse_query_obj('gripper')
|
86 |
+
affordance_map = get_affordance_map('a point at the center of the mug handle')
|
87 |
+
gripper_map = get_gripper_map('open everywhere except 1cm around the mug handle')
|
88 |
+
execute(movable, affordance_map=affordance_map, gripper_map=gripper_map)
|
89 |
+
|
90 |
+
# Query: move to 10cm on top of the soup bowl, and 5cm to the left of the soup bowl, while away from the glass, at 0.75x speed.
|
91 |
+
movable = parse_query_obj('gripper')
|
92 |
+
affordance_map = get_affordance_map('a point 10cm above and 5cm to the left of the soup bowl')
|
93 |
+
avoidance_map = get_avoidance_map('10cm from the glass')
|
94 |
+
velocity_map = get_velocity_map('0.75x speed')
|
95 |
+
execute(movable, affordance_map=affordance_map, avoidance_map=avoidance_map, velocity_map=velocity_map)
|
96 |
+
|
97 |
+
# Query: open gripper.
|
98 |
+
movable = parse_query_obj('gripper')
|
99 |
+
gripper_map = get_gripper_map('open everywhere')
|
100 |
+
execute(movable, gripper_map=gripper_map)
|
101 |
+
|
102 |
+
# Query: turn counter-clockwise by 180 degrees.
|
103 |
+
movable = parse_query_obj('gripper')
|
104 |
+
rotation_map = get_rotation_map('turn counter-clockwise by 180 degrees')
|
105 |
+
execute(movable, rotation_map=rotation_map)
|
106 |
+
|
107 |
+
# Query: sweep all particles to the left side of the table.
|
108 |
+
particles = parse_query_obj('particles')
|
109 |
+
for particle in particles:
|
110 |
+
movable = particle
|
111 |
+
affordance_map = get_affordance_map('a point on the left side of the table')
|
112 |
+
execute(particle, affordance_map=affordance_map)
|
113 |
+
|
114 |
+
# Query: grasp the bottom drawer handle while moving at 0.5x speed.
|
115 |
+
movable = parse_query_obj('gripper')
|
116 |
+
affordance_map = get_affordance_map('a point at the center of the bottom drawer handle')
|
117 |
+
velocity_map = get_velocity_map('0.5x speed')
|
118 |
+
rotation_map = get_rotation_map('face the bottom drawer handle')
|
119 |
+
gripper_map = get_gripper_map('open everywhere except 1cm around the bottom drawer handle')
|
120 |
+
execute(movable, affordance_map=affordance_map, velocity_map=velocity_map, rotation_map=rotation_map, gripper_map=gripper_map)
|
prompts/VoxPoser/get_affordance_map_prompt.txt
ADDED
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
from perception_utils import parse_query_obj
|
3 |
+
from plan_utils import get_empty_affordance_map, set_voxel_by_radius, cm2index
|
4 |
+
|
5 |
+
# Query: a point 10cm in front of [10, 15, 60].
|
6 |
+
affordance_map = get_empty_affordance_map()
|
7 |
+
# 10cm in front of so we add to x-axis
|
8 |
+
x = 10 + cm2index(10, 'x')
|
9 |
+
y = 15
|
10 |
+
z = 60
|
11 |
+
affordance_map[x, y, z] = 1
|
12 |
+
ret_val = affordance_map
|
13 |
+
|
14 |
+
# Query: a point on the right side of the table.
|
15 |
+
affordance_map = get_empty_affordance_map()
|
16 |
+
table = parse_query_obj('table')
|
17 |
+
(min_x, min_y, min_z), (max_x, max_y, max_z) = table.aabb
|
18 |
+
center_x, center_y, center_z = table.position
|
19 |
+
# right side so y = max_y
|
20 |
+
x = center_x
|
21 |
+
y = max_y
|
22 |
+
z = center_z
|
23 |
+
affordance_map[x, y, z] = 1
|
24 |
+
ret_val = affordance_map
|
25 |
+
|
26 |
+
# Query: a point 20cm on top of the container.
|
27 |
+
affordance_map = get_empty_affordance_map()
|
28 |
+
container = parse_query_obj('container')
|
29 |
+
(min_x, min_y, min_z), (max_x, max_y, max_z) = container.aabb
|
30 |
+
center_x, center_y, center_z = container.position
|
31 |
+
# 20cm on top of so we add to z-axis
|
32 |
+
x = center_x
|
33 |
+
y = center_y
|
34 |
+
z = max_z + cm2index(20, 'z')
|
35 |
+
affordance_map[x, y, z] = 1
|
36 |
+
ret_val = affordance_map
|
37 |
+
|
38 |
+
# Query: a point 1cm to the left of the brown block.
|
39 |
+
affordance_map = get_empty_affordance_map()
|
40 |
+
brown_block = parse_query_obj('brown block')
|
41 |
+
(min_x, min_y, min_z), (max_x, max_y, max_z) = brown_block.aabb
|
42 |
+
center_x, center_y, center_z = brown_block.position
|
43 |
+
# 1cm to the left of so we subtract from y-axis
|
44 |
+
x = center_x
|
45 |
+
y = min_y - cm2index(1, 'y')
|
46 |
+
z = center_z
|
47 |
+
affordance_map[x, y, z] = 1
|
48 |
+
ret_val = affordance_map
|
49 |
+
|
50 |
+
# Query: anywhere within 20cm of the right most block.
|
51 |
+
affordance_map = get_empty_affordance_map()
|
52 |
+
right_most_block = parse_query_obj('the right most block')
|
53 |
+
set_voxel_by_radius(affordance_map, right_most_block.position, radius_cm=20, value=1)
|
54 |
+
|
55 |
+
# Query: a point on the back side of the table.
|
56 |
+
affordance_map = get_empty_affordance_map()
|
57 |
+
table = parse_query_obj('table')
|
58 |
+
(min_x, min_y, min_z), (max_x, max_y, max_z) = table.aabb
|
59 |
+
center_x, center_y, center_z = table.position
|
60 |
+
# back side so x = min_x
|
61 |
+
x = min_x
|
62 |
+
y = center_y
|
63 |
+
z = center_z
|
64 |
+
affordance_map[x, y, z] = 1
|
65 |
+
ret_val = affordance_map
|
66 |
+
|
67 |
+
# Query: a point on the front right corner of the table.
|
68 |
+
affordance_map = get_empty_affordance_map()
|
69 |
+
table = parse_query_obj('table')
|
70 |
+
(min_x, min_y, min_z), (max_x, max_y, max_z) = table.aabb
|
71 |
+
center_x, center_y, center_z = table.position
|
72 |
+
# front right corner so x = max_x and y = max_y
|
73 |
+
x = max_x
|
74 |
+
y = max_y
|
75 |
+
z = center_z
|
76 |
+
affordance_map[x, y, z] = 1
|
77 |
+
ret_val = affordance_map
|
78 |
+
|
79 |
+
# Query: a point 30cm into the topmost drawer handle.
|
80 |
+
affordance_map = get_empty_affordance_map()
|
81 |
+
top_handle = parse_query_obj('topmost drawer handle')
|
82 |
+
# negative normal because we are moving into the handle.
|
83 |
+
moving_dir = -top_handle.normal
|
84 |
+
affordance_xyz = top_handle.position + cm2index(30, moving_dir)
|
85 |
+
affordance_map[affordance_xyz[0], affordance_xyz[1], affordance_xyz[2]] = 1
|
86 |
+
ret_val = affordance_map
|
87 |
+
|
88 |
+
# Query: a point 5cm above the blue block.
|
89 |
+
affordance_map = get_empty_affordance_map()
|
90 |
+
blue_block = parse_query_obj('blue block')
|
91 |
+
(min_x, min_y, min_z), (max_x, max_y, max_z) = blue_block.aabb
|
92 |
+
center_x, center_y, center_z = blue_block.position
|
93 |
+
# 5cm above so we add to z-axis
|
94 |
+
x = center_x
|
95 |
+
y = center_y
|
96 |
+
z = max_z + cm2index(5, 'z')
|
97 |
+
affordance_map[x, y, z] = 1
|
98 |
+
ret_val = affordance_map
|
99 |
+
|
100 |
+
# Query: a point 20cm away from the leftmost block.
|
101 |
+
affordance_map = get_empty_affordance_map()
|
102 |
+
leftmost_block = parse_query_obj('leftmost block')
|
103 |
+
# positive normal because we are moving away from the block.
|
104 |
+
moving_dir = leftmost_block.normal
|
105 |
+
affordance_xyz = leftmost_block.position + cm2index(20, moving_dir)
|
106 |
+
affordance_map[affordance_xyz[0], affordance_xyz[1], affordance_xyz[2]] = 1
|
107 |
+
ret_val = affordance_map
|
108 |
+
|
109 |
+
# Query: a point 4cm to the left of and 10cm on top of the tray that contains the lemon.
|
110 |
+
affordance_map = get_empty_affordance_map()
|
111 |
+
tray_with_lemon = parse_query_obj('tray that contains the lemon')
|
112 |
+
(min_x, min_y, min_z), (max_x, max_y, max_z) = tray_with_lemon.aabb
|
113 |
+
center_x, center_y, center_z = tray_with_lemon.position
|
114 |
+
# 4cm to the left of so we subtract from y-axis, and 10cm on top of so we add to z-axis
|
115 |
+
x = center_x
|
116 |
+
y = min_y - cm2index(4, 'y')
|
117 |
+
z = max_z + cm2index(10, 'z')
|
118 |
+
affordance_map[x, y, z] = 1
|
119 |
+
ret_val = affordance_map
|
120 |
+
|
121 |
+
# Query: a point 10cm to the right of [45 49 66], and 5cm above it.
|
122 |
+
affordance_map = get_empty_affordance_map()
|
123 |
+
# 10cm to the right of so we add to y-axis, and 5cm above it so we add to z-axis
|
124 |
+
x = 45
|
125 |
+
y = 49 + cm2index(10, 'y')
|
126 |
+
z = 66 + cm2index(5, 'z')
|
127 |
+
affordance_map[x, y, z] = 1
|
128 |
+
ret_val = affordance_map
|
129 |
+
|
130 |
+
# Query: the blue circle.
|
131 |
+
affordance_map = get_empty_affordance_map()
|
132 |
+
blue_circle = parse_query_obj('blue circle')
|
133 |
+
affordance_map = blue_circle.occupancy_map
|
134 |
+
ret_val = affordance_map
|
135 |
+
|
136 |
+
# Query: a point at the center of the blue circle.
|
137 |
+
affordance_map = get_empty_affordance_map()
|
138 |
+
blue_circle = parse_query_obj('blue circle')
|
139 |
+
x, y, z = blue_block.position
|
140 |
+
affordance_map[x, y, z] = 1
|
141 |
+
ret_val = affordance_map
|
142 |
+
|
143 |
+
# Query: a point 10cm above and 5cm to the left of the yellow bowl.
|
144 |
+
affordance_map = get_empty_affordance_map()
|
145 |
+
yellow_bowl = parse_query_obj('yellow bowl')
|
146 |
+
(min_x, min_y, min_z), (max_x, max_y, max_z) = yellow_bowl.aabb
|
147 |
+
center_x, center_y, center_z = yellow_bowl.position
|
148 |
+
# 10cm above so we add to z-axis, and 5cm to the left of so we subtract from y-axis
|
149 |
+
x = center_x
|
150 |
+
y = min_y - cm2index(5, 'y')
|
151 |
+
z = max_z + cm2index(10, 'z')
|
152 |
+
affordance_map[x, y, z] = 1
|
153 |
+
ret_val = affordance_mapS
|
prompts/VoxPoser/get_avoidance_map_prompt.txt
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
from perception_utils import parse_query_obj
|
3 |
+
from plan_utils import get_empty_avoidance_map, set_voxel_by_radius, cm2index
|
4 |
+
|
5 |
+
# Query: 10cm from the bowl.
|
6 |
+
avoidance_map = get_empty_avoidance_map()
|
7 |
+
bowl = parse_query_obj('bowl')
|
8 |
+
set_voxel_by_radius(avoidance_map, bowl.position, radius_cm=10, value=1)
|
9 |
+
ret_val = avoidance_map
|
10 |
+
|
11 |
+
# Query: 20cm near the mug.
|
12 |
+
avoidance_map = get_empty_avoidance_map()
|
13 |
+
mug = parse_query_obj('mug')
|
14 |
+
set_voxel_by_radius(avoidance_map, mug.position, radius_cm=20, value=1)
|
15 |
+
ret_val = avoidance_map
|
16 |
+
|
17 |
+
# Query: 20cm around the mug and 10cm around the bowl.
|
18 |
+
avoidance_map = get_empty_avoidance_map()
|
19 |
+
mug = parse_query_obj('mug')
|
20 |
+
set_voxel_by_radius(avoidance_map, mug.position, radius_cm=20, value=1)
|
21 |
+
bowl = parse_query_obj('bowl')
|
22 |
+
set_voxel_by_radius(avoidance_map, bowl.position, radius_cm=10, value=1)
|
23 |
+
ret_val = avoidance_map
|
24 |
+
|
25 |
+
# Query: 10cm from anything fragile.
|
26 |
+
avoidance_map = get_empty_avoidance_map()
|
27 |
+
fragile_objects = parse_query_obj('anything fragile')
|
28 |
+
for obj in fragile_objects:
|
29 |
+
set_voxel_by_radius(avoidance_map, obj.position, radius_cm=10, value=1)
|
30 |
+
ret_val = avoidance_map
|
31 |
+
|
32 |
+
# Query: 10cm from the blue circle.
|
33 |
+
avoidance_map = get_empty_avoidance_map()
|
34 |
+
blue_circle = parse_query_obj('blue circle')
|
35 |
+
set_voxel_by_radius(avoidance_map, blue_circle.position, radius_cm=10, value=1)
|
36 |
+
ret_val = avoidance_map
|
prompts/VoxPoser/get_gripper_map_prompt.txt
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
from perception_utils import parse_query_obj
|
3 |
+
from plan_utils import get_empty_gripper_map, set_voxel_by_radius, cm2index
|
4 |
+
|
5 |
+
# Query: open everywhere except 1cm around the green block.
|
6 |
+
gripper_map = get_empty_gripper_map()
|
7 |
+
# open everywhere
|
8 |
+
gripper_map[:, :, :] = 1
|
9 |
+
# close when 1cm around the green block
|
10 |
+
green_block = parse_query_obj('green block')
|
11 |
+
set_voxel_by_radius(gripper_map, green_block.position, radius_cm=1, value=0)
|
12 |
+
ret_val = gripper_map
|
13 |
+
|
14 |
+
# Query: close everywhere but open when on top of the back left corner of the table.
|
15 |
+
gripper_map = get_empty_gripper_map()
|
16 |
+
# close everywhere
|
17 |
+
gripper_map[:, :, :] = 0
|
18 |
+
# open when on top of the back left corner of the table
|
19 |
+
table = parse_query_obj('table')
|
20 |
+
(min_x, min_y, min_z), (max_x, max_y, max_z) = table.aabb
|
21 |
+
center_x, center_y, center_z = table.position
|
22 |
+
# back so x = min_x, left so y = min_y, top so we add to z
|
23 |
+
x = min_x
|
24 |
+
y = min_y
|
25 |
+
z = max_z + cm2index(15, 'z')
|
26 |
+
set_voxel_by_radius(gripper_map, (x, y, z), radius_cm=10, value=1)
|
27 |
+
ret_val = gripper_map
|
28 |
+
|
29 |
+
# Query: always open except when you are on the right side of the table.
|
30 |
+
gripper_map = get_empty_gripper_map()
|
31 |
+
# always open
|
32 |
+
gripper_map[:, :, :] = 1
|
33 |
+
# close when you are on the right side of the table
|
34 |
+
table = parse_query_obj('table')
|
35 |
+
center_x, center_y, center_z = table.position
|
36 |
+
# right side so y is greater than center_y
|
37 |
+
gripper_map[:, center_y:, :] = 0
|
38 |
+
|
39 |
+
# Query: always close except when you are on the back side of the table.
|
40 |
+
gripper_map = get_empty_gripper_map()
|
41 |
+
# always close
|
42 |
+
gripper_map[:, :, :] = 0
|
43 |
+
# open when you are on the back side of the table
|
44 |
+
table = parse_query_obj('table')
|
45 |
+
center_x, center_y, center_z = table.position
|
46 |
+
# back side so x is less than center_x
|
47 |
+
gripper_map[:center_x, :, :] = 1
|
48 |
+
ret_val = gripper_map
|
prompts/VoxPoser/get_rotation_map_prompt.txt
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
from plan_utils import get_empty_rotation_map, set_voxel_by_radius, cm2index, vec2quat
|
3 |
+
from perception_utils import parse_query_obj
|
4 |
+
from transforms3d.euler import euler2quat, quat2euler
|
5 |
+
from transforms3d.quaternions import qmult, qinverse
|
6 |
+
|
7 |
+
# Query: face the support surface of the bowl.
|
8 |
+
rotation_map = get_empty_rotation_map()
|
9 |
+
bowl = parse_query_obj('bowl')
|
10 |
+
target_rotation = vec2quat(-bowl.normal)
|
11 |
+
rotation_map[:, :, :] = target_rotation
|
12 |
+
ret_val = rotation_map
|
13 |
+
|
14 |
+
# Query: face the table when within 30cm from table center.
|
15 |
+
rotation_map = get_empty_rotation_map()
|
16 |
+
table = parse_query_obj('table')
|
17 |
+
table_center = table.position
|
18 |
+
target_rotation = vec2quat(-table.normal)
|
19 |
+
set_voxel_by_radius(rotation_map, table_center, radius_cm=30, value=target_rotation)
|
20 |
+
ret_val = rotation_map
|
21 |
+
|
22 |
+
# Query: face the blue bowl.
|
23 |
+
rotation_map = get_empty_rotation_map()
|
24 |
+
blue_bowl = parse_query_obj('brown block')
|
25 |
+
target_rotation = vec2quat(-blue_bowl.normal)
|
26 |
+
rotation_map[:, :, :] = target_rotation
|
27 |
+
ret_val = rotation_map
|
28 |
+
|
29 |
+
# Query: turn clockwise by 45 degrees when at the center of the beer cap.
|
30 |
+
rotation_map = get_empty_rotation_map()
|
31 |
+
beer_cap = parse_query_obj('beer cap')
|
32 |
+
(x, y, z) = beer_cap.position
|
33 |
+
curr_rotation = rotation_map[x, y, z]
|
34 |
+
rotation_delta = euler2quat(0, 0, np.pi / 4)
|
35 |
+
rotation_map[x, y, z] = qmult(curr_rotation, rotation_delta)
|
36 |
+
ret_val = rotation_map
|
37 |
+
|
38 |
+
# Query: turn counter-clockwise by 30 degrees.
|
39 |
+
rotation_map = get_empty_rotation_map()
|
40 |
+
curr_rotation = rotation_map[0, 0, 0]
|
41 |
+
rotation_delta = euler2quat(0, 0, -np.pi / 6)
|
42 |
+
rotation_map[:, :, :] = qmult(curr_rotation, rotation_delta)
|
43 |
+
ret_val = rotation_map
|
44 |
+
|
45 |
+
# Query: rotate the gripper to be 45 degrees slanted relative to the plate.
|
46 |
+
rotation_map = get_empty_rotation_map()
|
47 |
+
plate = parse_query_obj('plate')
|
48 |
+
face_plate_quat = vec2quat(-plate.normal)
|
49 |
+
# rotate 45 degrees around the x-axis
|
50 |
+
rotation_delta = euler2quat(-np.pi / 4, 0, 0)
|
51 |
+
target_rotation = qmult(face_plate_quat, rotation_delta)
|
52 |
+
rotation_map[:, :, :] = target_rotation
|
53 |
+
ret_val = rotation_map
|
prompts/VoxPoser/get_velocity_map_prompt.txt
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
from plan_utils import get_empty_velocity_map, set_voxel_by_radius, cm2index
|
3 |
+
from perception_utils import parse_query_obj
|
4 |
+
|
5 |
+
# Query: faster when on the right side of the table and slower when on the left side of the table.
|
6 |
+
velocity_map = get_empty_velocity_map()
|
7 |
+
table = parse_query_obj('table')
|
8 |
+
center_x, center_y, center_z = table.position
|
9 |
+
# faster on right side so 1.5 when y > center_y, slower on left side so 0.5 when y < center_y
|
10 |
+
velocity_map[:, center_y:, :] = 1.5
|
11 |
+
velocity_map[:, :center_y, :] = 0.5
|
12 |
+
ret_val = velocity_map
|
13 |
+
|
14 |
+
# Query: slow down by a quarter.
|
15 |
+
velocity_map = get_empty_velocity_map()
|
16 |
+
velocity_map[:] = 0.75
|
17 |
+
ret_val = velocity_map
|
18 |
+
|
19 |
+
# Query: slow down by a half when you're near anything fragile (objects: ['block', 'fork', 'mug', 'bowl', 'chips']).
|
20 |
+
velocity_map = get_empty_velocity_map()
|
21 |
+
mug = parse_query_obj('mug')
|
22 |
+
set_voxel_by_radius(velocity_map, mug.position, radius_cm=10, value=0.5)
|
23 |
+
bowl = parse_query_obj('bowl')
|
24 |
+
set_voxel_by_radius(velocity_map, bowl.position, radius_cm=10, value=0.5)
|
25 |
+
ret_val = velocity_map
|
26 |
+
|
27 |
+
# Query: quarter of the speed when within 9cm from the yellow line.
|
28 |
+
velocity_map = get_empty_velocity_map()
|
29 |
+
yellow_line = parse_query_obj('yellow_line')
|
30 |
+
set_voxel_by_radius(velocity_map, yellow_line.position, radius_cm=9, value=0.25)
|
31 |
+
ret_val = velocity_map
|
prompts/VoxPoser/parse_query_obj_prompt.txt
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
from perception_utils import detect
|
3 |
+
|
4 |
+
objects = ['green block', 'cardboard box']
|
5 |
+
# Query: gripper.
|
6 |
+
gripper = detect('gripper')
|
7 |
+
ret_val = gripper
|
8 |
+
|
9 |
+
objects = ['handle1', 'handle2', 'egg1', 'egg2', 'plate']
|
10 |
+
# Query: topmost handle.
|
11 |
+
handle1 = detect('handle1')
|
12 |
+
handle2 = detect('handle2')
|
13 |
+
if handle1.position[2] > handle2.position[2]:
|
14 |
+
top_handle = handle1
|
15 |
+
else:
|
16 |
+
top_handle = handle2
|
17 |
+
ret_val = top_handle
|
18 |
+
|
19 |
+
objects = ['vase', 'napkin box', 'mask']
|
20 |
+
# Query: table.
|
21 |
+
table = detect('table')
|
22 |
+
ret_val = table
|
23 |
+
|
24 |
+
objects = ['brown line', 'red block', 'monitor']
|
25 |
+
# Query: brown line.
|
26 |
+
brown_line = detect('brown line')
|
27 |
+
ret_val = brown_line
|
28 |
+
|
29 |
+
objects = ['green block', 'cup holder', 'black block']
|
30 |
+
# Query: any block.
|
31 |
+
block = detect('green block')
|
32 |
+
ret_val = block
|
33 |
+
|
34 |
+
objects = ['mouse', 'yellow bowl', 'brown bowl', 'sticker']
|
35 |
+
# Query: bowl closest to the sticker.
|
36 |
+
yellow_bowl = detect('yellow bowl')
|
37 |
+
brown_bowl = detect('brown bowl')
|
38 |
+
sticker = detect('sticker')
|
39 |
+
if np.linalg.norm(yellow_bowl.position - sticker.position) < np.linalg.norm(brown_bowl.position - sticker.position):
|
40 |
+
closest_bowl = yellow_bowl
|
41 |
+
else:
|
42 |
+
closest_bowl = brown_bowl
|
43 |
+
ret_val = closest_bowl
|
44 |
+
|
45 |
+
objects = ['grape', 'wood tray', 'strawberry', 'white tray', 'blue tray', 'bread']
|
46 |
+
# Query: tray that contains the bread.
|
47 |
+
wood_tray = detect('wood tray')
|
48 |
+
white_tray = detect('white tray')
|
49 |
+
bread = detect('bread')
|
50 |
+
if np.linalg.norm(wood_tray.position - bread.position) < np.linalg.norm(white_tray.position - bread.position):
|
51 |
+
tray_with_bread = wood_tray
|
52 |
+
else:
|
53 |
+
tray_with_bread = white_tray
|
54 |
+
ret_val = tray_with_bread
|
55 |
+
|
56 |
+
objects = ['glass', 'vase', 'plastic bottle', 'block', 'phone case']
|
57 |
+
# Query: anything fragile.
|
58 |
+
fragile_items = []
|
59 |
+
for obj in ['glass', 'vase']:
|
60 |
+
item = detect(obj)
|
61 |
+
fragile_items.append(item)
|
62 |
+
ret_val = fragile_items
|
63 |
+
|
64 |
+
objects = ['blue block', 'red block']
|
65 |
+
# Query: green block.
|
66 |
+
ret_val = None
|
prompts/VoxPoser/planner_prompt.txt
ADDED
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
from env_utils import execute
|
3 |
+
from perception_utils import parse_query_obj
|
4 |
+
import action_utils import composer
|
5 |
+
|
6 |
+
objects = ['blue block', 'yellow block', 'mug']
|
7 |
+
# Query: place the blue block on the yellow block, and avoid the mug at all time.
|
8 |
+
composer("grasp the blue block while keeping at least 15cm away from the mug")
|
9 |
+
composer("back to default pose")
|
10 |
+
composer("move to 5cm on top of the yellow block while keeping at least 15cm away from the mug")
|
11 |
+
composer("open gripper")
|
12 |
+
# done
|
13 |
+
|
14 |
+
objects = ['airpods', 'drawer']
|
15 |
+
# Query: Open the drawer slowly.
|
16 |
+
composer("grasp the drawer handle, at 0.5x speed")
|
17 |
+
composer("move away from the drawer handle by 25cm, at 0.5x speed")
|
18 |
+
composer("open gripper, at 0.5x speed")
|
19 |
+
# done
|
20 |
+
|
21 |
+
objects = ['tissue box', 'tissue', 'bowl']
|
22 |
+
# Query: Can you pass me a tissue and place it next to the bowl?
|
23 |
+
composer("grasp the tissue")
|
24 |
+
composer("back to default pose")
|
25 |
+
composer("move to 10cm to the right of the bowl")
|
26 |
+
composer("open gripper")
|
27 |
+
composer("back to default pose")
|
28 |
+
# done
|
29 |
+
|
30 |
+
objects = ['charger', 'outlet']
|
31 |
+
# Query: unplug the charger from the wall.
|
32 |
+
composer("grasp the charger")
|
33 |
+
composer("back to default pose")
|
34 |
+
# done
|
35 |
+
|
36 |
+
objects = ['grape', 'lemon', 'drill', 'router', 'bread', 'tray']
|
37 |
+
# Query: put the sweeter fruit in the tray that contains the bread.
|
38 |
+
composer("grasp the grape")
|
39 |
+
composer("back to default pose")
|
40 |
+
composer("move to the top of the tray that contains the bread")
|
41 |
+
composer("open gripper")
|
42 |
+
# done
|
43 |
+
|
44 |
+
objects = ['marbles', 'tray', 'broom']
|
45 |
+
# Query: Can you sweep the marbles into the tray?
|
46 |
+
composer("grasp the broom")
|
47 |
+
composer("back to default pose")
|
48 |
+
composer("push the marbles into the tray")
|
49 |
+
# done
|
50 |
+
|
51 |
+
objects = ['orange', 'QR code', 'lemon', 'drawer']
|
52 |
+
# Query: put the sour fruit into the top drawer.
|
53 |
+
composer("grasp the top drawer handle")
|
54 |
+
composer("move away from the top drawer handle by 25cm")
|
55 |
+
composer("open gripper")
|
56 |
+
composer("back to default pose")
|
57 |
+
composer("grasp the lemon")
|
58 |
+
composer("move to 10cm on top of the top drawer")
|
59 |
+
composer("open gripper")
|
60 |
+
# done
|
61 |
+
|
62 |
+
objects = ['fridge', 'hot soup']
|
63 |
+
# Query: Open the fridge door and be careful around the hot soup.
|
64 |
+
composer("grasp the fridge handle and keep at least 15cm away from the hot soup")
|
65 |
+
composer("move away from the fridge handle by 25cm and keep at least 15cm away from the hot soup")
|
66 |
+
composer("open gripper")
|
67 |
+
# done
|
68 |
+
|
69 |
+
objects = ['cyan bowl', 'yellow bowl', 'box', 'ice cream']
|
70 |
+
# Query: move to the top of the cyan bowl.
|
71 |
+
composer("move to the top of the cyan bowl")
|
72 |
+
# done
|
73 |
+
|
74 |
+
objects = ['drawer', 'umbrella']
|
75 |
+
# Query: close the drawer.
|
76 |
+
composer("push close the drawer handle by 25cm")
|
77 |
+
# done
|
78 |
+
|
79 |
+
objects = ['iPhone', 'airpods']
|
80 |
+
# Query: slide the iPhone towards the airpods.
|
81 |
+
composer("push the iPhone towards the airpods")
|
82 |
+
# done
|
83 |
+
|
84 |
+
objects = ['plate', 'steak', 'fork', 'knife', 'spoon']
|
85 |
+
# Query: Could you please set up the fork for the steak for me?
|
86 |
+
composer("grasp the fork")
|
87 |
+
composer("back to default pose")
|
88 |
+
composer("move to 10cm to the right of the plate")
|
89 |
+
composer("open gripper")
|
90 |
+
composer("back to default pose")
|
91 |
+
# done
|
92 |
+
|
93 |
+
objects = ['lamp', 'switch']
|
94 |
+
# Query: Turn off the lamp.
|
95 |
+
composer("close the gripper")
|
96 |
+
composer("move to the center of the switch")
|
97 |
+
composer("back to default pose")
|
98 |
+
# done
|
99 |
+
|
100 |
+
objects = ['beer']
|
101 |
+
# Query: turn close the beer.
|
102 |
+
composer("grasp the beer cap")
|
103 |
+
composer("turn clockwise by 180 degrees")
|
104 |
+
composer("back to default pose")
|
105 |
+
# done
|
106 |
+
|
107 |
+
objects = ['steak', 'grill', 'plate']
|
108 |
+
# Query: Take the steak out of the grill and put it flat on the plate.
|
109 |
+
composer("grasp the steak")
|
110 |
+
composer("back to default pose")
|
111 |
+
composer("rotate the gripper to be 45 degrees slanted relative to the plate")
|
112 |
+
composer("move to 10cm on top of the plate")
|
113 |
+
composer("open gripper")
|
114 |
+
composer("back to default pose")
|
115 |
+
# done
|
prompts/swarm/composer_prompt.txt
ADDED
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Query: Move the soil from place A to B.
|
2 |
+
{
|
3 |
+
"tasks": [
|
4 |
+
{
|
5 |
+
"task": "Move excavator and dump truck to point A."
|
6 |
+
},
|
7 |
+
{
|
8 |
+
"task": "Begin cycle of digging, loading, transporting, and unloading until all soil is moved from point A to point B."
|
9 |
+
},
|
10 |
+
{
|
11 |
+
"task": "Wait for excavator to reach point A."
|
12 |
+
},
|
13 |
+
{
|
14 |
+
"task": "Start digging with excavator at point A."
|
15 |
+
},
|
16 |
+
{
|
17 |
+
"task": "Wait for excavator to finish digging and for dump truck to reach point A."
|
18 |
+
},
|
19 |
+
{
|
20 |
+
"task": "Load dug soil into dump truck at point A."
|
21 |
+
},
|
22 |
+
{
|
23 |
+
"task": "Wait for soil loading to complete at point A."
|
24 |
+
},
|
25 |
+
{
|
26 |
+
"task": "Transport soil with dump truck from point A to point B."
|
27 |
+
},
|
28 |
+
{
|
29 |
+
"task": "Unload soil at point B."
|
30 |
+
},
|
31 |
+
{
|
32 |
+
"task": "Return dump truck to point A."
|
33 |
+
},
|
34 |
+
{
|
35 |
+
"task": "Repeat the cycle as necessary until completion."
|
36 |
+
},
|
37 |
+
{
|
38 |
+
"task": "After all soil is moved, return both excavator and dump truck to their starting positions."
|
39 |
+
}
|
40 |
+
]
|
41 |
+
}
|
42 |
+
# done
|
43 |
+
|
44 |
+
# Query: Move gravel from Site C to Site D.
|
45 |
+
{
|
46 |
+
"tasks": [
|
47 |
+
{
|
48 |
+
"task": "Move excavator and dump truck to Site C."
|
49 |
+
},
|
50 |
+
{
|
51 |
+
"task": "Begin cycle of digging, loading, transporting, and unloading until all gravel is moved from Site C to Site D."
|
52 |
+
},
|
53 |
+
{
|
54 |
+
"task": "Wait for excavator to reach Site C."
|
55 |
+
},
|
56 |
+
{
|
57 |
+
"task": "Start digging with excavator at Site C."
|
58 |
+
},
|
59 |
+
{
|
60 |
+
"task": "Wait for excavator to finish digging and for dump truck to reach Site C."
|
61 |
+
},
|
62 |
+
{
|
63 |
+
"task": "Load dug gravel into dump truck at Site C."
|
64 |
+
},
|
65 |
+
{
|
66 |
+
"task": "Wait for gravel loading to complete at Site C."
|
67 |
+
},
|
68 |
+
{
|
69 |
+
"task": "Transport gravel with dump truck from Site C to Site D."
|
70 |
+
},
|
71 |
+
{
|
72 |
+
"task": "Unload gravel at Site D."
|
73 |
+
},
|
74 |
+
{
|
75 |
+
"task": "Return dump truck to Site C."
|
76 |
+
},
|
77 |
+
{
|
78 |
+
"task": "Repeat the cycle as necessary until completion."
|
79 |
+
},
|
80 |
+
{
|
81 |
+
"task": "After all gravel is moved, return both excavator and dump truck to their starting positions."
|
82 |
+
}
|
83 |
+
]
|
84 |
+
}
|
85 |
+
# done
|
86 |
+
|
87 |
+
# Query: Clean up debris at a construction site.
|
88 |
+
{
|
89 |
+
"tasks": [
|
90 |
+
{
|
91 |
+
"task": "Move excavator and hauler to the construction site."
|
92 |
+
},
|
93 |
+
{
|
94 |
+
"task": "Begin cycle of cleaning, loading, transporting, and unloading until the site is clean."
|
95 |
+
},
|
96 |
+
{
|
97 |
+
"task": "Wait for excavator and hauler to reach the construction site."
|
98 |
+
},
|
99 |
+
{
|
100 |
+
"task": "Start cleaning debris with the excavator at the construction site."
|
101 |
+
},
|
102 |
+
{
|
103 |
+
"task": "Wait for excavator to finish cleaning and for hauler to arrive."
|
104 |
+
},
|
105 |
+
{
|
106 |
+
"task": "Load cleaned debris into the hauler."
|
107 |
+
},
|
108 |
+
{
|
109 |
+
"task": "Wait for loading to complete."
|
110 |
+
},
|
111 |
+
{
|
112 |
+
"task": "Transport debris to a designated waste facility."
|
113 |
+
},
|
114 |
+
{
|
115 |
+
"task": "Unload debris at the waste facility."
|
116 |
+
},
|
117 |
+
{
|
118 |
+
"task": "Return the hauler to the construction site."
|
119 |
+
},
|
120 |
+
{
|
121 |
+
"task": "Repeat the cycle until the site is clean."
|
122 |
+
},
|
123 |
+
{
|
124 |
+
"task": "After the site is clean, return all equipment to their starting positions."
|
125 |
+
}
|
126 |
+
]
|
127 |
+
}
|
128 |
+
# done
|
prompts/swarm/dart.txt
ADDED
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Query: Ensure all robots avoid the puddle.
|
2 |
+
{
|
3 |
+
"tasks": [
|
4 |
+
{
|
5 |
+
"task": "avoid_areas_for_all_robots_1",
|
6 |
+
"instruction_function": {
|
7 |
+
"name": "avoid_areas_for_all_robots",
|
8 |
+
"robot_type": ["dump_truck", "excavator"],
|
9 |
+
"robot_count": "all",
|
10 |
+
"dependencies": [],
|
11 |
+
"object_keywords": ["puddle1"]
|
12 |
+
}
|
13 |
+
}
|
14 |
+
]
|
15 |
+
}
|
16 |
+
# done
|
17 |
+
|
18 |
+
# Query: Excavators 1 and 2 avoid the puddle.
|
19 |
+
{
|
20 |
+
"tasks": [
|
21 |
+
{
|
22 |
+
"task": "avoid_areas_for_specific_robots_1",
|
23 |
+
"instruction_function": {
|
24 |
+
"name": "avoid_areas_for_specific_robots",
|
25 |
+
"robot_ids": ["robot_excavator_01", "robot_excavator_02"],
|
26 |
+
"dependencies": [],
|
27 |
+
"object_keywords": ["puddle1"]
|
28 |
+
}
|
29 |
+
}
|
30 |
+
]
|
31 |
+
}
|
32 |
+
# done
|
33 |
+
|
34 |
+
# Query: Excavators 1 and 2 go to the puddle area.
|
35 |
+
{
|
36 |
+
"tasks": [
|
37 |
+
{
|
38 |
+
"task": "target_area_for_specific_robots_1",
|
39 |
+
"instruction_function": {
|
40 |
+
"name": "target_area_for_specific_robots",
|
41 |
+
"robot_ids": ["robot_excavator_01", "robot_excavator_02"],
|
42 |
+
"dependencies": [],
|
43 |
+
"object_keywords": ["puddle1"]
|
44 |
+
}
|
45 |
+
}
|
46 |
+
]
|
47 |
+
}
|
48 |
+
# done
|
49 |
+
|
50 |
+
# Query: All robots return to start.
|
51 |
+
{
|
52 |
+
"tasks": [
|
53 |
+
{
|
54 |
+
"task": "return_to_start_for_all_robots_1",
|
55 |
+
"instruction_function": {
|
56 |
+
"name": "return_to_start_for_all_robots",
|
57 |
+
"robot_type": ["dump_truck", "excavator"],
|
58 |
+
"robot_count": "all",
|
59 |
+
"dependencies": [],
|
60 |
+
"object_keywords": []
|
61 |
+
}
|
62 |
+
}
|
63 |
+
]
|
64 |
+
}
|
65 |
+
# done
|
66 |
+
|
67 |
+
# Query: Excavator 1 performs excavation.
|
68 |
+
{
|
69 |
+
"tasks": [
|
70 |
+
{
|
71 |
+
"task": "Excavation_1",
|
72 |
+
"instruction_function": {
|
73 |
+
"name": "Excavation",
|
74 |
+
"robot_ids": ["robot_excavator_01"],
|
75 |
+
"dependencies": [],
|
76 |
+
"object_keywords": []
|
77 |
+
}
|
78 |
+
}
|
79 |
+
]
|
80 |
+
}
|
81 |
+
# done
|
82 |
+
|
83 |
+
# Query: All dump truck perform loading.
|
84 |
+
{
|
85 |
+
"tasks": [
|
86 |
+
{
|
87 |
+
"task": "DumpLoading_1",
|
88 |
+
"instruction_function": {
|
89 |
+
"name": "DumpLoading",
|
90 |
+
"robot_type": ["dump_truck"],
|
91 |
+
"robot_count": "all",
|
92 |
+
"dependencies": [],
|
93 |
+
"object_keywords": []
|
94 |
+
}
|
95 |
+
}
|
96 |
+
]
|
97 |
+
}
|
98 |
+
# done
|
99 |
+
|
100 |
+
# Query: Truck 1 go to obstacle.
|
101 |
+
{
|
102 |
+
"tasks": [
|
103 |
+
{
|
104 |
+
"task": "target_area_for_specific_robots_1",
|
105 |
+
"instruction_function": {
|
106 |
+
"name": "target_area_for_specific_robots",
|
107 |
+
"robot_ids": ["robot_dump_truck_01"],
|
108 |
+
"dependencies": [],
|
109 |
+
"start_flag": "",
|
110 |
+
"stop_flag": "end_move_equipment",
|
111 |
+
"parallel_flag": false,
|
112 |
+
"condition": "",
|
113 |
+
},
|
114 |
+
"object_keywords": ["obstacle1"]
|
115 |
+
}
|
116 |
+
]
|
117 |
+
}
|
118 |
+
# done
|
119 |
+
|
120 |
+
# Query: Two trucks go to the puddle, then load, and then return to their initial position.
|
121 |
+
{
|
122 |
+
"tasks": [
|
123 |
+
{
|
124 |
+
"task": "target_area_for_specific_robots_1",
|
125 |
+
"instruction_function": {
|
126 |
+
"name": "target_area_for_specific_robots",
|
127 |
+
"robot_type": ["dump_truck"],
|
128 |
+
"robot_count": 2,
|
129 |
+
"dependencies": [],
|
130 |
+
"start_flag": "",
|
131 |
+
"stop_flag": "end_move_equipment",
|
132 |
+
"parallel_flag": false,
|
133 |
+
"condition": "",
|
134 |
+
"object_keywords": []
|
135 |
+
},
|
136 |
+
"object_keywords": ["puddle1"]
|
137 |
+
},
|
138 |
+
{
|
139 |
+
"task": "DumpLoading_1",
|
140 |
+
"instruction_function": {
|
141 |
+
"name": "DumpLoading",
|
142 |
+
"robot_type": ["dump_truck"],
|
143 |
+
"robot_count": 2,
|
144 |
+
"dependencies": ["target_area_for_specific_robots_1"],
|
145 |
+
"start_flag": "start_loading_soil",
|
146 |
+
"stop_flag": "end_loading_soil",
|
147 |
+
"parallel_flag": true,
|
148 |
+
"condition": "",
|
149 |
+
"object_keywords": []
|
150 |
+
}
|
151 |
+
},
|
152 |
+
{
|
153 |
+
"task": "return_to_start_for_specific_robots_1",
|
154 |
+
"instruction_function": {
|
155 |
+
"name": "return_to_start_for_specific_robots",
|
156 |
+
"robot_type": ["dump_truck"],
|
157 |
+
"robot_count": 2,
|
158 |
+
"dependencies": ["DumpLoading_1"],
|
159 |
+
"start_flag": "start_return_truck",
|
160 |
+
"stop_flag": "end_return_truck",
|
161 |
+
"parallel_flag": true,
|
162 |
+
"condition": "",
|
163 |
+
"object_keywords": []
|
164 |
+
},
|
165 |
+
"object_keywords": ["puddle1"]
|
166 |
+
}
|
167 |
+
]
|
168 |
+
}
|
169 |
+
# done
|
prompts/swarm/instruction_translator_prompt.txt
ADDED
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Query: move_equipment_to_location
|
2 |
+
{
|
3 |
+
"instruction_function": {
|
4 |
+
"name": "move_equipment_to_location",
|
5 |
+
"parameters": {
|
6 |
+
"equipment_ids": ["excavator1", "truck1"],
|
7 |
+
"location": "soil_pile"
|
8 |
+
},
|
9 |
+
"start_flag": "start_move_equipment",
|
10 |
+
"stop_flag": "end_move_equipment",
|
11 |
+
"dependencies": [],
|
12 |
+
"parallel_flag": false
|
13 |
+
}
|
14 |
+
}
|
15 |
+
# done
|
16 |
+
|
17 |
+
# Query: start_digging_at_location
|
18 |
+
{
|
19 |
+
"instruction_function": {
|
20 |
+
"name": "Excavation",
|
21 |
+
"parameters": {
|
22 |
+
"location": "soil_pile"
|
23 |
+
},
|
24 |
+
"start_flag": "start_digging",
|
25 |
+
"stop_flag": "end_digging",
|
26 |
+
"dependencies": ["end_move_equipment"],
|
27 |
+
"parallel_flag": true
|
28 |
+
}
|
29 |
+
}
|
30 |
+
# done
|
31 |
+
|
32 |
+
# Query: load_soil_into_truck
|
33 |
+
{
|
34 |
+
"instruction_function": {
|
35 |
+
"name": "DumpLoading",
|
36 |
+
"parameters": {
|
37 |
+
"truck_id": "truck1",
|
38 |
+
"location": "soil_pile"
|
39 |
+
},
|
40 |
+
"start_flag": "start_loading_soil",
|
41 |
+
"stop_flag": "end_loading_soil",
|
42 |
+
"dependencies": ["start_digging"],
|
43 |
+
"parallel_flag": true
|
44 |
+
}
|
45 |
+
}
|
46 |
+
# done
|
47 |
+
|
48 |
+
# Query: transport_soil
|
49 |
+
{
|
50 |
+
"instruction_function": {
|
51 |
+
"name": "transport_soil",
|
52 |
+
"parameters": {
|
53 |
+
"truck_id": "truck1",
|
54 |
+
"from_location": "soil_pile",
|
55 |
+
"to_location": "pit"
|
56 |
+
},
|
57 |
+
"start_flag": "start_transporting_soil",
|
58 |
+
"stop_flag": "end_transporting_soil",
|
59 |
+
"dependencies": ["start_loading_soil"],
|
60 |
+
"parallel_flag": true
|
61 |
+
}
|
62 |
+
}
|
63 |
+
# done
|
64 |
+
|
65 |
+
# Query: unload_soil_at_location
|
66 |
+
{
|
67 |
+
"instruction_function": {
|
68 |
+
"name": "DumpUnloading",
|
69 |
+
"parameters": {
|
70 |
+
"truck_id": "truck1",
|
71 |
+
"location": "pit"
|
72 |
+
},
|
73 |
+
"start_flag": "start_unloading_soil",
|
74 |
+
"stop_flag": "end_unloading_soil",
|
75 |
+
"dependencies": ["end_transporting_soil"],
|
76 |
+
"parallel_flag": false
|
77 |
+
}
|
78 |
+
}
|
79 |
+
# done
|
80 |
+
|
81 |
+
# Query: return_truck_to_location
|
82 |
+
{
|
83 |
+
"instruction_function": {
|
84 |
+
"name": "return_to_start_for_specific_robots",
|
85 |
+
"parameters": {
|
86 |
+
"robot_ids": ["truck1"],
|
87 |
+
"location": "soil_pile"
|
88 |
+
},
|
89 |
+
"start_flag": "start_return_truck",
|
90 |
+
"stop_flag": "end_return_truck",
|
91 |
+
"dependencies": ["end_unloading_soil"],
|
92 |
+
"parallel_flag": true
|
93 |
+
}
|
94 |
+
}
|
95 |
+
# done
|
96 |
+
|
97 |
+
# Query: check_if_half_soil_moved
|
98 |
+
{
|
99 |
+
"instruction_function": {
|
100 |
+
"name": "check_if_half_soil_moved",
|
101 |
+
"parameters": {
|
102 |
+
"condition": "Soil moved is less than 50%"
|
103 |
+
},
|
104 |
+
"start_flag": "start_check_half_moved",
|
105 |
+
"stop_flag": "end_check_half_moved",
|
106 |
+
"dependencies": ["end_return_truck"],
|
107 |
+
"parallel_flag": false
|
108 |
+
}
|
109 |
+
}
|
110 |
+
# done
|
111 |
+
|
112 |
+
# Query: return_equipment_to_starting_positions
|
113 |
+
{
|
114 |
+
"instruction_function": {
|
115 |
+
"name": "return_to_start_for_all_robots",
|
116 |
+
"parameters": {
|
117 |
+
"equipment_ids": ["excavator1", "truck1"]
|
118 |
+
},
|
119 |
+
"start_flag": "start_return_equipment",
|
120 |
+
"stop_flag": "end_return_equipment",
|
121 |
+
"dependencies": ["end_check_half_moved"],
|
122 |
+
"parallel_flag": false
|
123 |
+
}
|
124 |
+
}
|
125 |
+
# done
|
prompts/swarm/planner_prompt.txt
ADDED
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Query: Ensure all robots avoid the puddle.
|
2 |
+
{
|
3 |
+
"instruction_function": {
|
4 |
+
"name": "avoid_areas_for_all_robots"
|
5 |
+
},
|
6 |
+
"clip_keywords": ["The puddle"]
|
7 |
+
}
|
8 |
+
# done
|
9 |
+
|
10 |
+
# Query: Robots 2 and 3 avoid the puddle.
|
11 |
+
{
|
12 |
+
"instruction_function": {
|
13 |
+
"name": "avoid_areas_for_specific_robots",
|
14 |
+
"robot_ids": [2, 3]
|
15 |
+
},
|
16 |
+
"clip_keywords": ["The puddle"]
|
17 |
+
}
|
18 |
+
# done
|
19 |
+
|
20 |
+
# Query: All robots go to the puddle area.
|
21 |
+
{
|
22 |
+
"instruction_function": {
|
23 |
+
"name": "target_area_for_all_robots"
|
24 |
+
},
|
25 |
+
"clip_keywords": ["The puddle"]
|
26 |
+
}
|
27 |
+
# done
|
28 |
+
|
29 |
+
# Query: Robots 1 and 4 go to the puddle area.
|
30 |
+
{
|
31 |
+
"instruction_function": {
|
32 |
+
"name": "target_area_for_specific_robots",
|
33 |
+
"robot_ids": [1, 4]
|
34 |
+
},
|
35 |
+
"clip_keywords": ["The puddle"]
|
36 |
+
}
|
37 |
+
# done
|
38 |
+
|
39 |
+
# Query: All robots resume operations at the puddle.
|
40 |
+
{
|
41 |
+
"instruction_function": {
|
42 |
+
"name": "allow_areas_for_all_robots"
|
43 |
+
},
|
44 |
+
"clip_keywords": ["The puddle"]
|
45 |
+
}
|
46 |
+
# done
|
47 |
+
|
48 |
+
# Query: Robots 5 and 6 operate in the puddle area.
|
49 |
+
{
|
50 |
+
"instruction_function": {
|
51 |
+
"name": "allow_areas_for_specific_robots",
|
52 |
+
"robot_ids": [5, 6]
|
53 |
+
},
|
54 |
+
"clip_keywords": ["The puddle"]
|
55 |
+
}
|
56 |
+
# done
|
57 |
+
|
58 |
+
# Query: All robots return to start.
|
59 |
+
{
|
60 |
+
"instruction_function": {
|
61 |
+
"name": "return_to_start_for_all_robots"
|
62 |
+
}
|
63 |
+
}
|
64 |
+
# done
|
65 |
+
|
66 |
+
# Query: Robots 3 and 4 return to charging stations.
|
67 |
+
{
|
68 |
+
"instruction_function": {
|
69 |
+
"name": "return_to_start_for_specific_robots",
|
70 |
+
"robot_ids": [3, 4]
|
71 |
+
}
|
72 |
+
}
|
73 |
+
# done
|
74 |
+
|
75 |
+
# Query: Excavator performs excavation.
|
76 |
+
{
|
77 |
+
"instruction_function": {
|
78 |
+
"name": "Excavation",
|
79 |
+
"robot_type": "excavator"
|
80 |
+
}
|
81 |
+
}
|
82 |
+
# done
|
83 |
+
|
84 |
+
# Query: Excavator performs unloading.
|
85 |
+
{
|
86 |
+
"instruction_function": {
|
87 |
+
"name": "ExcavatorUnloading",
|
88 |
+
"robot_type": "excavator"
|
89 |
+
}
|
90 |
+
}
|
91 |
+
# done
|
92 |
+
|
93 |
+
# Query: Dump truck performs loading.
|
94 |
+
{
|
95 |
+
"instruction_function": {
|
96 |
+
"name": "DumpLoading",
|
97 |
+
"robot_type": "dump"
|
98 |
+
}
|
99 |
+
}
|
100 |
+
# done
|
101 |
+
|
102 |
+
# Query: Dump truck performs unloading.
|
103 |
+
{
|
104 |
+
"instruction_function": {
|
105 |
+
"name": "DumpUnloading",
|
106 |
+
"robot_type": "dump"
|
107 |
+
}
|
108 |
+
}
|
109 |
+
# done
|
prompts/swarm/task_2_commannd_prompt.txt
ADDED
@@ -0,0 +1,564 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Query: Ensure all robots avoid the puddle.
|
2 |
+
{
|
3 |
+
"tasks": [
|
4 |
+
{
|
5 |
+
"task": "ensure_all_robots_avoid_puddle",
|
6 |
+
"instruction_function": {
|
7 |
+
"name": "avoid_areas_for_all_robots",
|
8 |
+
"robot_type": ["dump_truck", "excavator"],
|
9 |
+
"robot_count": "all",
|
10 |
+
"dependencies": [],
|
11 |
+
"object_keywords": ["puddle1"]
|
12 |
+
}
|
13 |
+
}
|
14 |
+
]
|
15 |
+
}
|
16 |
+
# done
|
17 |
+
|
18 |
+
# Query: Trucks 2 and 3 avoid the puddle.
|
19 |
+
{
|
20 |
+
"tasks": [
|
21 |
+
{
|
22 |
+
"task": "trucks_2_and_3_avoid_puddle",
|
23 |
+
"instruction_function": {
|
24 |
+
"name": "avoid_areas_for_specific_robots",
|
25 |
+
"robot_ids": ["robot_dump_truck_02", "robot_dump_truck_03"],
|
26 |
+
"dependencies": [],
|
27 |
+
"object_keywords": ["puddle1"]
|
28 |
+
}
|
29 |
+
}
|
30 |
+
]
|
31 |
+
}
|
32 |
+
# done
|
33 |
+
|
34 |
+
# Query: Excavators 1 and 2 avoid the puddle.
|
35 |
+
{
|
36 |
+
"tasks": [
|
37 |
+
{
|
38 |
+
"task": "excavators_1_and_2_avoid_puddle",
|
39 |
+
"instruction_function": {
|
40 |
+
"name": "avoid_areas_for_specific_robots",
|
41 |
+
"robot_ids": ["robot_excavator_01", "robot_excavator_02"],
|
42 |
+
"dependencies": [],
|
43 |
+
"object_keywords": ["puddle1"]
|
44 |
+
}
|
45 |
+
}
|
46 |
+
]
|
47 |
+
}
|
48 |
+
# done
|
49 |
+
|
50 |
+
# Query: All robots go to the puddle area.
|
51 |
+
{
|
52 |
+
"tasks": [
|
53 |
+
{
|
54 |
+
"task": "all_robots_go_to_puddle_area",
|
55 |
+
"instruction_function": {
|
56 |
+
"name": "target_area_for_all_robots",
|
57 |
+
"robot_type": ["dump_truck", "excavator"],
|
58 |
+
"robot_count": "all",
|
59 |
+
"dependencies": [],
|
60 |
+
"object_keywords": ["puddle1"]
|
61 |
+
}
|
62 |
+
}
|
63 |
+
]
|
64 |
+
}
|
65 |
+
# done
|
66 |
+
|
67 |
+
# Query: Excavators 1 and 2 go to the puddle area.
|
68 |
+
{
|
69 |
+
"tasks": [
|
70 |
+
{
|
71 |
+
"task": "excavators_1_and_2_go_to_puddle_area",
|
72 |
+
"instruction_function": {
|
73 |
+
"name": "target_area_for_specific_robots",
|
74 |
+
"robot_ids": ["robot_excavator_01", "robot_excavator_02"],
|
75 |
+
"dependencies": [],
|
76 |
+
"object_keywords": ["puddle1"]
|
77 |
+
}
|
78 |
+
}
|
79 |
+
]
|
80 |
+
}
|
81 |
+
# done
|
82 |
+
|
83 |
+
# Query: Truck 1 and 4 go to the puddle area.
|
84 |
+
{
|
85 |
+
"tasks": [
|
86 |
+
{
|
87 |
+
"task": "truck_1_and_4_go_to_puddle_area",
|
88 |
+
"instruction_function": {
|
89 |
+
"name": "target_area_for_specific_robots",
|
90 |
+
"robot_ids": ["robot_dump_truck_01", "robot_dump_truck_04"],
|
91 |
+
"dependencies": [],
|
92 |
+
"object_keywords": ["puddle1"]
|
93 |
+
}
|
94 |
+
}
|
95 |
+
]
|
96 |
+
}
|
97 |
+
# done
|
98 |
+
|
99 |
+
# Query: Truck 1 and 4 go to the obstacle area.
|
100 |
+
{
|
101 |
+
"tasks": [
|
102 |
+
{
|
103 |
+
"task": "truck_1_and_4_go_to_obstacle_area",
|
104 |
+
"instruction_function": {
|
105 |
+
"name": "target_area_for_specific_robots",
|
106 |
+
"robot_ids": ["robot_dump_truck_01", "robot_dump_truck_04"],
|
107 |
+
"dependencies": [],
|
108 |
+
"object_keywords": ["obstacle1"]
|
109 |
+
}
|
110 |
+
}
|
111 |
+
]
|
112 |
+
}
|
113 |
+
# done
|
114 |
+
|
115 |
+
# Query: Excavators 1 and 2 go to the puddle area.
|
116 |
+
{
|
117 |
+
"tasks": [
|
118 |
+
{
|
119 |
+
"task": "excavators_1_and_2_go_to_puddle_area",
|
120 |
+
"instruction_function": {
|
121 |
+
"name": "target_area_for_specific_robots",
|
122 |
+
"robot_ids": ["robot_excavator_01", "robot_excavator_02"],
|
123 |
+
"dependencies": [],
|
124 |
+
"object_keywords": ["puddle1"]
|
125 |
+
}
|
126 |
+
}
|
127 |
+
]
|
128 |
+
}
|
129 |
+
# done
|
130 |
+
|
131 |
+
# Query: All robots resume operations at the puddle.
|
132 |
+
{
|
133 |
+
"tasks": [
|
134 |
+
{
|
135 |
+
"task": "all_robots_resume_operations_at_puddle",
|
136 |
+
"instruction_function": {
|
137 |
+
"name": "allow_areas_for_all_robots",
|
138 |
+
"robot_type": ["dump_truck", "excavator"],
|
139 |
+
"robot_count": "all",
|
140 |
+
"dependencies": [],
|
141 |
+
"object_keywords": ["puddle1"]
|
142 |
+
}
|
143 |
+
}
|
144 |
+
]
|
145 |
+
}
|
146 |
+
# done
|
147 |
+
|
148 |
+
# Query: Trucks 5 and 6 operate in the puddle area.
|
149 |
+
{
|
150 |
+
"tasks": [
|
151 |
+
{
|
152 |
+
"task": "trucks_5_and_6_operate_in_puddle_area",
|
153 |
+
"instruction_function": {
|
154 |
+
"name": "allow_areas_for_specific_robots",
|
155 |
+
"robot_ids": ["robot_dump_truck_05", "robot_dump_truck_06"],
|
156 |
+
"dependencies": [],
|
157 |
+
"object_keywords": ["puddle1"]
|
158 |
+
}
|
159 |
+
}
|
160 |
+
]
|
161 |
+
}
|
162 |
+
# done
|
163 |
+
|
164 |
+
# Query: Excavators 1 and 2 operate in the puddle area.
|
165 |
+
{
|
166 |
+
"tasks": [
|
167 |
+
{
|
168 |
+
"task": "excavators_1_and_2_operate_in_puddle_area",
|
169 |
+
"instruction_function": {
|
170 |
+
"name": "allow_areas_for_specific_robots",
|
171 |
+
"robot_ids": ["robot_excavator_01", "robot_excavator_02"],
|
172 |
+
"dependencies": [],
|
173 |
+
"object_keywords": ["puddle1"]
|
174 |
+
}
|
175 |
+
}
|
176 |
+
]
|
177 |
+
}
|
178 |
+
# done
|
179 |
+
|
180 |
+
# Query: All robots return to start.
|
181 |
+
{
|
182 |
+
"tasks": [
|
183 |
+
{
|
184 |
+
"task": "all_robots_return_to_start",
|
185 |
+
"instruction_function": {
|
186 |
+
"name": "return_to_start_for_all_robots",
|
187 |
+
"robot_type": ["dump_truck", "excavator"],
|
188 |
+
"robot_count": "all",
|
189 |
+
"dependencies": [],
|
190 |
+
"object_keywords": []
|
191 |
+
}
|
192 |
+
}
|
193 |
+
]
|
194 |
+
}
|
195 |
+
# done
|
196 |
+
|
197 |
+
# Query: Trucks 3 and 4 return to charging stations.
|
198 |
+
{
|
199 |
+
"tasks": [
|
200 |
+
{
|
201 |
+
"task": "trucks_3_and_4_return_to_charging_stations",
|
202 |
+
"instruction_function": {
|
203 |
+
"name": "return_to_start_for_specific_robots",
|
204 |
+
"robot_ids": ["robot_dump_truck_03", "robot_dump_truck_04"],
|
205 |
+
"dependencies": [],
|
206 |
+
"object_keywords": []
|
207 |
+
}
|
208 |
+
}
|
209 |
+
]
|
210 |
+
}
|
211 |
+
# done
|
212 |
+
|
213 |
+
# Query: Excavators 3 and 4 return to charging stations.
|
214 |
+
{
|
215 |
+
"tasks": [
|
216 |
+
{
|
217 |
+
"task": "excavators_1_and_2_return_to_charging_stations",
|
218 |
+
"instruction_function": {
|
219 |
+
"name": "return_to_start_for_specific_robots",
|
220 |
+
"robot_ids": ["robot_excavator_01", "robot_excavator_02"],
|
221 |
+
"dependencies": [],
|
222 |
+
"object_keywords": []
|
223 |
+
}
|
224 |
+
}
|
225 |
+
]
|
226 |
+
}
|
227 |
+
# done
|
228 |
+
|
229 |
+
# Query: All excavator perform excavation.
|
230 |
+
{
|
231 |
+
"tasks": [
|
232 |
+
{
|
233 |
+
"task": "all_excavator_perform_excavation",
|
234 |
+
"instruction_function": {
|
235 |
+
"name": "Excavation",
|
236 |
+
"robot_type": ["excavator"],
|
237 |
+
"robot_count": "all",
|
238 |
+
"dependencies": [],
|
239 |
+
"object_keywords": []
|
240 |
+
}
|
241 |
+
}
|
242 |
+
]
|
243 |
+
}
|
244 |
+
# done
|
245 |
+
|
246 |
+
# Query: Excavator 1 performs excavation.
|
247 |
+
{
|
248 |
+
"tasks": [
|
249 |
+
{
|
250 |
+
"task": "excavator_1_performs_excavation",
|
251 |
+
"instruction_function": {
|
252 |
+
"name": "Excavation",
|
253 |
+
"robot_ids": ["robot_excavator_01"],
|
254 |
+
"dependencies": [],
|
255 |
+
"object_keywords": []
|
256 |
+
}
|
257 |
+
}
|
258 |
+
]
|
259 |
+
}
|
260 |
+
# done
|
261 |
+
|
262 |
+
# Query: Excavator 2 performs excavation.
|
263 |
+
{
|
264 |
+
"tasks": [
|
265 |
+
{
|
266 |
+
"task": "excavator_2_performs_excavation",
|
267 |
+
"instruction_function": {
|
268 |
+
"name": "Excavation",
|
269 |
+
"robot_ids": ["robot_excavator_02"],
|
270 |
+
"dependencies": [],
|
271 |
+
"object_keywords": []
|
272 |
+
}
|
273 |
+
}
|
274 |
+
]
|
275 |
+
}
|
276 |
+
# done
|
277 |
+
|
278 |
+
# Query: All excavator perform unloading.
|
279 |
+
{
|
280 |
+
"tasks": [
|
281 |
+
{
|
282 |
+
"task": "all_excavator_perform_unloading",
|
283 |
+
"instruction_function": {
|
284 |
+
"name": "ExcavatorUnloading",
|
285 |
+
"robot_type": ["excavator"],
|
286 |
+
"robot_count": "all",
|
287 |
+
"dependencies": [],
|
288 |
+
"object_keywords": []
|
289 |
+
}
|
290 |
+
}
|
291 |
+
]
|
292 |
+
}
|
293 |
+
# done
|
294 |
+
|
295 |
+
# Query: Excavator 1 performs unloading.
|
296 |
+
{
|
297 |
+
"tasks": [
|
298 |
+
{
|
299 |
+
"task": "excavator_1_performs_unloading",
|
300 |
+
"instruction_function": {
|
301 |
+
"name": "ExcavatorUnloading",
|
302 |
+
"robot_ids": ["robot_excavator_01"],
|
303 |
+
"dependencies": [],
|
304 |
+
"object_keywords": []
|
305 |
+
}
|
306 |
+
}
|
307 |
+
]
|
308 |
+
}
|
309 |
+
# done
|
310 |
+
|
311 |
+
# Query: Excavator 2 performs unloading.
|
312 |
+
{
|
313 |
+
"tasks": [
|
314 |
+
{
|
315 |
+
"task": "excavator_2_performs_unloading",
|
316 |
+
"instruction_function": {
|
317 |
+
"name": "ExcavatorUnloading",
|
318 |
+
"robot_ids": ["robot_excavator_02"],
|
319 |
+
"dependencies": [],
|
320 |
+
"object_keywords": []
|
321 |
+
}
|
322 |
+
}
|
323 |
+
]
|
324 |
+
}
|
325 |
+
# done
|
326 |
+
|
327 |
+
# Query: All dump truck perform loading.
|
328 |
+
{
|
329 |
+
"tasks": [
|
330 |
+
{
|
331 |
+
"task": "all_dump_truck_perform_loading",
|
332 |
+
"instruction_function": {
|
333 |
+
"name": "DumpLoading",
|
334 |
+
"robot_type": ["dump_truck"],
|
335 |
+
"robot_count": "all",
|
336 |
+
"dependencies": [],
|
337 |
+
"object_keywords": []
|
338 |
+
}
|
339 |
+
}
|
340 |
+
]
|
341 |
+
}
|
342 |
+
# done
|
343 |
+
|
344 |
+
# Query: Dump truck 3 performs loading.
|
345 |
+
{
|
346 |
+
"tasks": [
|
347 |
+
{
|
348 |
+
"task": "dump_truck_3_performs_loading",
|
349 |
+
"instruction_function": {
|
350 |
+
"name": "DumpLoading",
|
351 |
+
"robot_ids": ["robot_dump_truck_03"],
|
352 |
+
"dependencies": [],
|
353 |
+
"object_keywords": []
|
354 |
+
}
|
355 |
+
}
|
356 |
+
]
|
357 |
+
}
|
358 |
+
# done
|
359 |
+
|
360 |
+
# Query: Dump truck 1 performs loading.
|
361 |
+
{
|
362 |
+
"tasks": [
|
363 |
+
{
|
364 |
+
"task": "dump_truck_1_performs_loading",
|
365 |
+
"instruction_function": {
|
366 |
+
"name": "DumpLoading",
|
367 |
+
"robot_ids": ["robot_dump_truck_01"],
|
368 |
+
"dependencies": [],
|
369 |
+
"object_keywords": []
|
370 |
+
}
|
371 |
+
}
|
372 |
+
]
|
373 |
+
}
|
374 |
+
# done
|
375 |
+
|
376 |
+
|
377 |
+
|
378 |
+
# Query: All dump truck perform unloading.
|
379 |
+
{
|
380 |
+
"tasks": [
|
381 |
+
{
|
382 |
+
"task": "all_dump_truck_perform_unloading",
|
383 |
+
"instruction_function": {
|
384 |
+
"name": "DumpUnloading",
|
385 |
+
"robot_type": ["dump_truck"],
|
386 |
+
"robot_count": "all",
|
387 |
+
"dependencies": [],
|
388 |
+
"object_keywords": []
|
389 |
+
}
|
390 |
+
}
|
391 |
+
]
|
392 |
+
}
|
393 |
+
# done
|
394 |
+
|
395 |
+
# Query: Dump truck 5 performs unloading.
|
396 |
+
{
|
397 |
+
"tasks": [
|
398 |
+
{
|
399 |
+
"task": "dump_truck_5_performs_unloading",
|
400 |
+
"instruction_function": {
|
401 |
+
"name": "DumpUnloading",
|
402 |
+
"robot_ids": ["robot_dump_truck_05"],
|
403 |
+
"dependencies": [],
|
404 |
+
"object_keywords": []
|
405 |
+
}
|
406 |
+
}
|
407 |
+
]
|
408 |
+
}
|
409 |
+
# done
|
410 |
+
|
411 |
+
# Query: Dump truck 2 performs unloading.
|
412 |
+
{
|
413 |
+
"tasks": [
|
414 |
+
{
|
415 |
+
"task": "dump_truck_2_performs_unloading",
|
416 |
+
"instruction_function": {
|
417 |
+
"name": "DumpUnloading",
|
418 |
+
"robot_ids": ["robot_dump_truck_02"],
|
419 |
+
"dependencies": [],
|
420 |
+
"object_keywords": []
|
421 |
+
}
|
422 |
+
}
|
423 |
+
]
|
424 |
+
}
|
425 |
+
# done
|
426 |
+
|
427 |
+
# Query: Truck1 go to puddle.
|
428 |
+
{
|
429 |
+
"tasks": [
|
430 |
+
{
|
431 |
+
"task": "move_equipment_to_location",
|
432 |
+
"instruction_function": {
|
433 |
+
"name": "target_area_for_specific_robots",
|
434 |
+
"robot_ids": ["robot_dump_truck_01"],
|
435 |
+
"dependencies": [],
|
436 |
+
"start_flag": "",
|
437 |
+
"stop_flag": "end_move_equipment",
|
438 |
+
"parallel_flag": false,
|
439 |
+
"condition": "",
|
440 |
+
},
|
441 |
+
"object_keywords": ["puddle1"]
|
442 |
+
}
|
443 |
+
]
|
444 |
+
}
|
445 |
+
# done
|
446 |
+
|
447 |
+
# Query: After excavator 1 finishes excavation, excavator 2 performs excavation.
|
448 |
+
{
|
449 |
+
"tasks": [
|
450 |
+
{
|
451 |
+
"task": "excavator_1_excavation",
|
452 |
+
"instruction_function": {
|
453 |
+
"name": "Excavation",
|
454 |
+
"robot_ids": ["robot_excavator_01"],
|
455 |
+
"dependencies": [],
|
456 |
+
"start_flag": "",
|
457 |
+
"stop_flag": "end_excavator_1",
|
458 |
+
"parallel_flag": false,
|
459 |
+
"condition": "",
|
460 |
+
"object_keywords": []
|
461 |
+
}
|
462 |
+
},
|
463 |
+
{
|
464 |
+
"task": "excavator_2_excavation",
|
465 |
+
"instruction_function": {
|
466 |
+
"name": "Excavation",
|
467 |
+
"robot_ids": ["robot_excavator_02"],
|
468 |
+
"dependencies": ["excavator_1_excavation"],
|
469 |
+
"start_flag": "excavator_1_excavation",
|
470 |
+
"stop_flag": "end_excavator_2",
|
471 |
+
"parallel_flag": false,
|
472 |
+
"condition": "",
|
473 |
+
"object_keywords": []
|
474 |
+
}
|
475 |
+
}
|
476 |
+
]
|
477 |
+
}
|
478 |
+
# done
|
479 |
+
|
480 |
+
# Query: Truck 2 goes to the puddle, then Truck 3 goes to the puddle.
|
481 |
+
{
|
482 |
+
"tasks": [
|
483 |
+
{
|
484 |
+
"task": "move_truck2_to_location",
|
485 |
+
"instruction_function": {
|
486 |
+
"name": "target_area_for_specific_robots",
|
487 |
+
"robot_ids": ["robot_dump_truck_02"],
|
488 |
+
"dependencies": [],
|
489 |
+
"start_flag": "",
|
490 |
+
"stop_flag": "",
|
491 |
+
"parallel_flag": false,
|
492 |
+
"condition": "",
|
493 |
+
"object_keywords": []
|
494 |
+
},
|
495 |
+
"object_keywords": ["puddle1"]
|
496 |
+
},
|
497 |
+
{
|
498 |
+
"task": "move_truck3_to_location",
|
499 |
+
"instruction_function": {
|
500 |
+
"name": "return_to_start_for_specific_robots",
|
501 |
+
"robot_ids": ["robot_dump_truck_03"],
|
502 |
+
"dependencies": ["move_truck2_to_location"],
|
503 |
+
"start_flag": "move_truck2_to_location",
|
504 |
+
"stop_flag": "",
|
505 |
+
"parallel_flag": false,
|
506 |
+
"condition": "",
|
507 |
+
"object_keywords": []
|
508 |
+
},
|
509 |
+
"object_keywords": ["puddle1"]
|
510 |
+
}
|
511 |
+
]
|
512 |
+
}
|
513 |
+
# done
|
514 |
+
|
515 |
+
# Query: Two trucks go to the puddle, then load, and then return to their initial position.
|
516 |
+
{
|
517 |
+
"tasks": [
|
518 |
+
{
|
519 |
+
"task": "move_two_trucks_to_location",
|
520 |
+
"instruction_function": {
|
521 |
+
"name": "target_area_for_specific_robots",
|
522 |
+
"robot_type": ["dump_truck"],
|
523 |
+
"robot_count": 2,
|
524 |
+
"dependencies": [],
|
525 |
+
"start_flag": "",
|
526 |
+
"stop_flag": "end_move_equipment",
|
527 |
+
"parallel_flag": false,
|
528 |
+
"condition": "",
|
529 |
+
"object_keywords": []
|
530 |
+
},
|
531 |
+
"object_keywords": ["puddle1"]
|
532 |
+
},
|
533 |
+
{
|
534 |
+
"task": "two_trucks_perform_loading_soil",
|
535 |
+
"instruction_function": {
|
536 |
+
"name": "DumpLoading",
|
537 |
+
"robot_type": ["dump_truck"],
|
538 |
+
"robot_count": 2,
|
539 |
+
"dependencies": ["move_two_trucks_to_location"],
|
540 |
+
"start_flag": "start_loading_soil",
|
541 |
+
"stop_flag": "end_loading_soil",
|
542 |
+
"parallel_flag": true,
|
543 |
+
"condition": "",
|
544 |
+
"object_keywords": []
|
545 |
+
}
|
546 |
+
},
|
547 |
+
{
|
548 |
+
"task": "return_truck_to_starting_position",
|
549 |
+
"instruction_function": {
|
550 |
+
"name": "return_to_start_for_specific_robots",
|
551 |
+
"robot_type": ["dump_truck"],
|
552 |
+
"robot_count": 2,
|
553 |
+
"dependencies": ["two_trucks_perform_loading_soil"],
|
554 |
+
"start_flag": "start_return_truck",
|
555 |
+
"stop_flag": "end_return_truck",
|
556 |
+
"parallel_flag": true,
|
557 |
+
"condition": "",
|
558 |
+
"object_keywords": []
|
559 |
+
},
|
560 |
+
"object_keywords": ["puddle1"]
|
561 |
+
}
|
562 |
+
]
|
563 |
+
}
|
564 |
+
# done
|
prompts/swarm/task_decomposer_prompt.txt
ADDED
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Query: Move half of the soil from the soil pile to the puddle.
|
2 |
+
{
|
3 |
+
"tasks": [
|
4 |
+
{
|
5 |
+
"task": "move_equipment_to_location",
|
6 |
+
"parameters": {
|
7 |
+
"equipment_ids": ["excavator1", "truck1"],
|
8 |
+
"location": "soil_pile"
|
9 |
+
},
|
10 |
+
"start_flag": "",
|
11 |
+
"stop_flag": "end_move_equipment",
|
12 |
+
"dependencies": [],
|
13 |
+
"parallel_flag": false,
|
14 |
+
"condition": "Soil moved is less than 50%"
|
15 |
+
},
|
16 |
+
{
|
17 |
+
"task": "start_digging_at_location",
|
18 |
+
"parameters": {
|
19 |
+
"location": "soil_pile"
|
20 |
+
},
|
21 |
+
"start_flag": "start_digging",
|
22 |
+
"stop_flag": "end_digging",
|
23 |
+
"dependencies": ["end_move_equipment"],
|
24 |
+
"parallel_flag": true,
|
25 |
+
"condition": "Soil moved is less than 50%"
|
26 |
+
},
|
27 |
+
{
|
28 |
+
"task": "load_soil_into_truck",
|
29 |
+
"parameters": {
|
30 |
+
"truck_id": "truck1",
|
31 |
+
"location": "soil_pile"
|
32 |
+
},
|
33 |
+
"start_flag": "start_loading_soil",
|
34 |
+
"stop_flag": "end_loading_soil",
|
35 |
+
"dependencies": ["start_digging"],
|
36 |
+
"parallel_flag": true,
|
37 |
+
"condition": "Soil moved is less than 50%"
|
38 |
+
},
|
39 |
+
{
|
40 |
+
"task": "transport_soil",
|
41 |
+
"parameters": {
|
42 |
+
"truck_id": "truck1",
|
43 |
+
"from_location": "soil_pile",
|
44 |
+
"to_location": "pit"
|
45 |
+
},
|
46 |
+
"start_flag": "start_transporting_soil",
|
47 |
+
"stop_flag": "end_transporting_soil",
|
48 |
+
"dependencies": ["start_loading_soil"],
|
49 |
+
"parallel_flag": true,
|
50 |
+
"condition": "Soil moved is less than 50%"
|
51 |
+
},
|
52 |
+
{
|
53 |
+
"task": "unload_soil_at_location",
|
54 |
+
"parameters": {
|
55 |
+
"truck_id": "truck1",
|
56 |
+
"location": "pit"
|
57 |
+
},
|
58 |
+
"start_flag": "start_unloading_soil",
|
59 |
+
"stop_flag": "end_unloading_soil",
|
60 |
+
"dependencies": ["end_transporting_soil"],
|
61 |
+
"parallel_flag": false,
|
62 |
+
"condition": "Soil moved is less than 50%"
|
63 |
+
},
|
64 |
+
{
|
65 |
+
"task": "return_truck_to_location",
|
66 |
+
"parameters": {
|
67 |
+
"truck_id": "truck1",
|
68 |
+
"location": "soil_pile"
|
69 |
+
},
|
70 |
+
"start_flag": "start_return_truck",
|
71 |
+
"stop_flag": "end_return_truck",
|
72 |
+
"dependencies": ["end_unloading_soil"],
|
73 |
+
"parallel_flag": true,
|
74 |
+
"condition": "Soil moved is less than 50%"
|
75 |
+
},
|
76 |
+
{
|
77 |
+
"task": "check_if_half_soil_moved",
|
78 |
+
"parameters": {
|
79 |
+
"condition": "Soil moved is less than 50%"
|
80 |
+
},
|
81 |
+
"start_flag": "start_check_half_moved",
|
82 |
+
"stop_flag": "end_check_half_moved",
|
83 |
+
"dependencies": ["end_return_truck"],
|
84 |
+
"parallel_flag": false
|
85 |
+
},
|
86 |
+
{
|
87 |
+
"task": "return_equipment_to_starting_positions",
|
88 |
+
"parameters": {
|
89 |
+
"equipment_ids": ["excavator1", "truck1"]
|
90 |
+
},
|
91 |
+
"start_flag": "start_return_equipment",
|
92 |
+
"stop_flag": "end_return_equipment",
|
93 |
+
"dependencies": ["end_check_half_moved"],
|
94 |
+
"parallel_flag": false,
|
95 |
+
"condition": "Half soil moved"
|
96 |
+
}
|
97 |
+
]
|
98 |
+
}
|
99 |
+
# done
|
readme.md
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: DART-LLM_Task_Decomposer
|
3 |
+
app_file: gradio_llm_interface.py
|
4 |
+
sdk: gradio
|
5 |
+
sdk_version: 5.38.0
|
6 |
+
---
|
7 |
+
# QA_LLM_Module
|
8 |
+
|
9 |
+
QA_LLM_Module is a core component of the DART-LLM (Dependency-Aware Multi-Robot Task Decomposition and Execution) system. It provides an intelligent interface for parsing natural language instructions into structured, dependency-aware task sequences for multi-robot systems. The module supports multiple LLM providers including OpenAI GPT, Anthropic Claude, and LLaMA models, enabling sophisticated task decomposition with explicit handling of dependencies between subtasks.
|
10 |
+
|
11 |
+
## Features
|
12 |
+
|
13 |
+
- **Multiple LLM Support**: Compatible with OpenAI GPT-4, GPT-3.5-turbo, Anthropic Claude, and LLaMA models
|
14 |
+
- **Dependency-Aware Task Decomposition**: Breaks down complex tasks into subtasks with explicit dependencies
|
15 |
+
- **Structured Output Format**: Generates standardized JSON output for consistent task parsing
|
16 |
+
- **Real-time Processing**: Supports real-time task decomposition and execution
|
17 |
+
|
18 |
+
## Installation
|
19 |
+
|
20 |
+
|
21 |
+
|
22 |
+
## Installation
|
23 |
+
|
24 |
+
You can choose one of the following ways to set up the module:
|
25 |
+
|
26 |
+
### Option 1: Install Dependencies Directly (Recommended for Linux/Mac)
|
27 |
+
|
28 |
+
1. Clone the repository and install dependencies:
|
29 |
+
```bash
|
30 |
+
pip install -r requirements.txt
|
31 |
+
```
|
32 |
+
|
33 |
+
2. Configure API keys in environment variables:
|
34 |
+
```bash
|
35 |
+
export OPENAI_API_KEY="your_openai_key"
|
36 |
+
export ANTHROPIC_API_KEY="your_anthropic_key"
|
37 |
+
export GROQ_API_KEY="your_groq_key"
|
38 |
+
```
|
39 |
+
|
40 |
+
### Option 2: Use Docker (Recommended for Windows)
|
41 |
+
|
42 |
+
For Windows users, it is recommended to use the pre-configured Docker environment to avoid compatibility issues.
|
43 |
+
|
44 |
+
1. Clone the Docker repository:
|
45 |
+
```bash
|
46 |
+
git clone https://github.com/wyd0817/DART_LLM_Docker.git
|
47 |
+
```
|
48 |
+
|
49 |
+
2. Follow the instructions in the [DART_LLM_Docker](https://github.com/wyd0817/DART_LLM_Docker) repository to build and run the container.
|
50 |
+
|
51 |
+
3. Configure API keys in environment variables:
|
52 |
+
```bash
|
53 |
+
export OPENAI_API_KEY="your_openai_key"
|
54 |
+
export ANTHROPIC_API_KEY="your_anthropic_key"
|
55 |
+
export GROQ_API_KEY="your_groq_key"
|
56 |
+
```
|
57 |
+
|
58 |
+
## Usage
|
59 |
+
|
60 |
+
### Example Usage:
|
61 |
+
|
62 |
+
Run the main interface with Gradio:
|
63 |
+
```bash
|
64 |
+
gradio main.py
|
65 |
+
```
|
66 |
+
|
67 |
+
### Output Format:
|
68 |
+
|
69 |
+
The module processes natural language instructions into structured JSON output following this format:
|
70 |
+
```json
|
71 |
+
{
|
72 |
+
"instruction_function": {
|
73 |
+
"name": "<breakdown function 1>",
|
74 |
+
"dependencies": ["<dep 1>", "<dep 2>", "...", "<dep n>"]
|
75 |
+
},
|
76 |
+
"object_keywords": ["<key 1>", "<key 2>", "...", "<key n>"],
|
77 |
+
...
|
78 |
+
}
|
79 |
+
|
80 |
+
```
|
81 |
+
|
82 |
+
## Configuration
|
83 |
+
|
84 |
+
The module can be configured through:
|
85 |
+
|
86 |
+
- `config.py`: Model settings and API configurations
|
87 |
+
- `prompts/`: Directory containing prompt templates
|
88 |
+
- `llm_request_handler.py`: Core logic for handling LLM requests
|
requirements.txt
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
wandb==0.19.8
|
2 |
+
datasets==3.5.0
|
3 |
+
tqdm==4.67.1
|
4 |
+
tiktoken==0.9.0
|
5 |
+
transformers==4.50.3
|
6 |
+
deepspeed==0.16.5
|
7 |
+
openai==1.69.0
|
8 |
+
fastapi==0.115.12
|
9 |
+
gradio==5.23.1
|
10 |
+
httpx==0.28.1
|
11 |
+
loguru==0.7.3
|
12 |
+
pydantic==2.11.1
|
13 |
+
python-dotenv==1.1.0
|
14 |
+
PyYAML==6.0.2
|
15 |
+
requests==2.32.3
|
16 |
+
six==1.17.0
|
17 |
+
uvicorn==0.34.0
|
18 |
+
anthropic==0.20.0
|
19 |
+
groq==0.20.0
|
20 |
+
peft==0.15.1
|
21 |
+
protobuf==5.29.4
|
22 |
+
scikit-learn==1.6.1
|
23 |
+
scipy==1.8.0
|
24 |
+
sentencepiece==0.2.0
|
25 |
+
fire==0.7.0
|
26 |
+
bs4==0.0.1
|
27 |
+
zenhan==0.5.2
|
28 |
+
mecab-python3==1.0.10
|
29 |
+
pyknp==0.6.1
|
30 |
+
langchain==0.3.21
|
31 |
+
langchain-anthropic==0.3.10
|
32 |
+
langchain-community==0.3.20
|
33 |
+
langchain-core==0.3.51
|
34 |
+
langchain-deepseek==0.1.3
|
35 |
+
langchain-groq==0.3.1
|
36 |
+
langchain-ollama==0.3.1
|
37 |
+
langchain-openai==0.3.11
|
38 |
+
langchain-text-splitters==0.3.7
|
39 |
+
sentence_transformers==4.0.1
|
40 |
+
faiss-gpu==1.7.2
|
41 |
+
ipykernel==6.29.5
|
42 |
+
jupyter==1.1.1
|
43 |
+
jupyterlab==4.3.6
|
44 |
+
numpy==1.26.4
|
45 |
+
transforms3d==0.4.2
|
46 |
+
matplotlib==3.9.4
|
47 |
+
networkx==3.4
|
ros_message_server.py
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import rclpy
|
2 |
+
from std_msgs.msg import String
|
3 |
+
from loguru import logger
|
4 |
+
import asyncio
|
5 |
+
from llm_request_handler import LLMRequestHandler
|
6 |
+
from ros_node_publisher import RosNodePublisher
|
7 |
+
from json_processor import JsonProcessor
|
8 |
+
from config import MODE_CONFIG, ROS_MESSAGE_MODE
|
9 |
+
import json
|
10 |
+
|
11 |
+
class RosMessageServer(RosNodePublisher):
|
12 |
+
def __init__(self):
|
13 |
+
super().__init__(ROS_MESSAGE_MODE)
|
14 |
+
self.mode_config = MODE_CONFIG[ROS_MESSAGE_MODE]
|
15 |
+
self._subscriptions = []
|
16 |
+
self.received_tasks = []
|
17 |
+
self.llm_handler = LLMRequestHandler(
|
18 |
+
model_version=self.mode_config["model_version"],
|
19 |
+
max_tokens=self.mode_config["max_tokens"],
|
20 |
+
temperature=self.mode_config["temperature"],
|
21 |
+
frequency_penalty=self.mode_config["frequency_penalty"],
|
22 |
+
list_navigation_once=True
|
23 |
+
)
|
24 |
+
self.initial_messages = self.llm_handler.build_initial_messages(self.mode_config["prompt_file"], ROS_MESSAGE_MODE)
|
25 |
+
self.json_processor = JsonProcessor()
|
26 |
+
|
27 |
+
# Initialize subscriptions based on input topics
|
28 |
+
for input_topic in self.mode_config["input_topics"]:
|
29 |
+
subscription = self.create_subscription(
|
30 |
+
String,
|
31 |
+
input_topic,
|
32 |
+
self.listener_callback,
|
33 |
+
10)
|
34 |
+
self._subscriptions.append(subscription)
|
35 |
+
|
36 |
+
logger.info(f"{self.mode_config['display_name']} node initialized.")
|
37 |
+
|
38 |
+
def listener_callback(self, msg):
|
39 |
+
task_data = msg.data
|
40 |
+
logger.debug(f"Received raw task data: {task_data}")
|
41 |
+
try:
|
42 |
+
# Parse the task data from the message
|
43 |
+
task_data_clean = task_data.strip('"')
|
44 |
+
self.received_tasks.append({"task": task_data_clean})
|
45 |
+
asyncio.run(self.process_tasks())
|
46 |
+
except Exception as e:
|
47 |
+
logger.error(f"Unexpected error: {e}")
|
48 |
+
|
49 |
+
async def process_tasks(self):
|
50 |
+
while self.received_tasks:
|
51 |
+
task = self.received_tasks.pop(0)
|
52 |
+
logger.debug(f"Processing task: {task}")
|
53 |
+
await self.send_to_gpt(task)
|
54 |
+
|
55 |
+
async def send_to_gpt(self, task):
|
56 |
+
prompt = f"# Task: {task}"
|
57 |
+
messages = self.initial_messages + [{"role": "user", "content": prompt}]
|
58 |
+
response = await self.llm_handler.make_completion(messages)
|
59 |
+
if response:
|
60 |
+
self.publish_response(response)
|
61 |
+
else:
|
62 |
+
self.publish_response("Error: Unable to get response.")
|
63 |
+
|
64 |
+
def publish_response(self, response):
|
65 |
+
response_json = self.json_processor.process_response(response)
|
66 |
+
super().publish_response(response_json)
|
67 |
+
|
68 |
+
def main(args=None):
|
69 |
+
rclpy.init(args=args)
|
70 |
+
ros_message_server = RosMessageServer()
|
71 |
+
rclpy.spin(ros_message_server)
|
72 |
+
ros_message_server.destroy_node()
|
73 |
+
rclpy.shutdown()
|
74 |
+
|
75 |
+
if __name__ == "__main__":
|
76 |
+
main()
|
ros_node_publisher.py
ADDED
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import rclpy
|
2 |
+
from rclpy.node import Node
|
3 |
+
from std_msgs.msg import String
|
4 |
+
from loguru import logger
|
5 |
+
import json
|
6 |
+
from collections import OrderedDict
|
7 |
+
from config import MODE_CONFIG
|
8 |
+
|
9 |
+
class RosNodePublisher(Node):
|
10 |
+
def __init__(self, node_type=None):
|
11 |
+
if not rclpy.ok():
|
12 |
+
rclpy.init()
|
13 |
+
super().__init__(f'robovla_{node_type}_node')
|
14 |
+
self.node_type = node_type
|
15 |
+
self.llm_publishers = {}
|
16 |
+
self.mode_config = MODE_CONFIG[node_type] if node_type else None
|
17 |
+
if node_type:
|
18 |
+
self.initialize_node(node_type)
|
19 |
+
|
20 |
+
def initialize_node(self, node_type):
|
21 |
+
self.node_type = node_type
|
22 |
+
self.mode_config = MODE_CONFIG[node_type]
|
23 |
+
for output_topic in self.mode_config['output_topics']:
|
24 |
+
self.llm_publishers[output_topic] = self.create_publisher(String, output_topic, 10)
|
25 |
+
logger.info(f"robovla {self.node_type} node initialized.")
|
26 |
+
|
27 |
+
def destroy_node(self):
|
28 |
+
super().destroy_node()
|
29 |
+
rclpy.shutdown()
|
30 |
+
self.node_type = None
|
31 |
+
logger.info("ROS node destroyed.")
|
32 |
+
|
33 |
+
def get_node_type(self):
|
34 |
+
return self.node_type
|
35 |
+
|
36 |
+
def is_initialized(self):
|
37 |
+
return self.node_type is not None and rclpy.ok()
|
38 |
+
|
39 |
+
def publish_response(self, response_json):
|
40 |
+
try:
|
41 |
+
for json_key, topic in self.mode_config['json_keys'].items():
|
42 |
+
# Handle top-level key
|
43 |
+
if json_key in response_json:
|
44 |
+
msg = String()
|
45 |
+
msg.data = json.dumps(response_json[json_key])
|
46 |
+
self.llm_publishers[topic].publish(msg)
|
47 |
+
logger.info(f"Published {json_key} to {topic}: {msg.data}")
|
48 |
+
# Handle nested structure logic
|
49 |
+
else:
|
50 |
+
# Iterate through all tasks to check for nested keys
|
51 |
+
for task in response_json['tasks']:
|
52 |
+
if json_key in task:
|
53 |
+
if json_key == 'instruction_function':
|
54 |
+
# Merge task content into instruction_function and rename key to task_name
|
55 |
+
task_copy = OrderedDict()
|
56 |
+
task_copy['task_name'] = task['task']
|
57 |
+
for key, value in task[json_key].items():
|
58 |
+
task_copy[key] = value
|
59 |
+
msg = String()
|
60 |
+
msg.data = json.dumps(task_copy)
|
61 |
+
logger.info(f"Merged task_name into instruction_function: {msg.data}")
|
62 |
+
else:
|
63 |
+
msg = String()
|
64 |
+
msg.data = json.dumps(task[json_key])
|
65 |
+
self.llm_publishers[topic].publish(msg)
|
66 |
+
logger.info(f"Published {json_key} to {topic}: {msg.data}")
|
67 |
+
except Exception as e:
|
68 |
+
logger.error(f"Unexpected error: {e}")
|
69 |
+
|
70 |
+
|
71 |
+
|
test_dag_visualization.py
ADDED
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Test script for DAG visualization functionality
|
4 |
+
"""
|
5 |
+
|
6 |
+
import sys
|
7 |
+
import os
|
8 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
9 |
+
|
10 |
+
from dag_visualizer import DAGVisualizer
|
11 |
+
import json
|
12 |
+
|
13 |
+
def test_single_task():
|
14 |
+
"""Test with a single task (no dependencies)"""
|
15 |
+
print("Testing single task visualization...")
|
16 |
+
|
17 |
+
task_data = {
|
18 |
+
"tasks": [
|
19 |
+
{
|
20 |
+
"task": "target_area_for_specific_robots_1",
|
21 |
+
"instruction_function": {
|
22 |
+
"name": "target_area_for_specific_robots",
|
23 |
+
"robot_ids": ["robot_dump_truck_01"],
|
24 |
+
"dependencies": [],
|
25 |
+
"object_keywords": ["puddle1"]
|
26 |
+
}
|
27 |
+
}
|
28 |
+
]
|
29 |
+
}
|
30 |
+
|
31 |
+
visualizer = DAGVisualizer()
|
32 |
+
image_path = visualizer.create_dag_visualization(task_data)
|
33 |
+
|
34 |
+
if image_path and os.path.exists(image_path):
|
35 |
+
print(f"β Single task visualization created: {image_path}")
|
36 |
+
return True
|
37 |
+
else:
|
38 |
+
print("β Failed to create single task visualization")
|
39 |
+
return False
|
40 |
+
|
41 |
+
def test_multiple_tasks_with_dependencies():
|
42 |
+
"""Test with multiple tasks with dependencies"""
|
43 |
+
print("Testing multiple tasks with dependencies...")
|
44 |
+
|
45 |
+
task_data = {
|
46 |
+
"tasks": [
|
47 |
+
{
|
48 |
+
"task": "target_area_for_specific_robots_1",
|
49 |
+
"instruction_function": {
|
50 |
+
"name": "target_area_for_specific_robots",
|
51 |
+
"robot_ids": ["robot_dump_truck_01"],
|
52 |
+
"dependencies": [],
|
53 |
+
"object_keywords": ["puddle1"]
|
54 |
+
}
|
55 |
+
},
|
56 |
+
{
|
57 |
+
"task": "avoid_areas_for_all_robots_1",
|
58 |
+
"instruction_function": {
|
59 |
+
"name": "avoid_areas_for_all_robots",
|
60 |
+
"robot_ids": ["robot_dump_truck_01", "robot_excavator_01"],
|
61 |
+
"dependencies": ["target_area_for_specific_robots_1"],
|
62 |
+
"object_keywords": ["obstacle1", "obstacle2"]
|
63 |
+
}
|
64 |
+
},
|
65 |
+
{
|
66 |
+
"task": "move_to_position_1",
|
67 |
+
"instruction_function": {
|
68 |
+
"name": "move_to_position",
|
69 |
+
"robot_ids": ["robot_excavator_01"],
|
70 |
+
"dependencies": ["avoid_areas_for_all_robots_1"],
|
71 |
+
"object_keywords": ["soil_pile"]
|
72 |
+
}
|
73 |
+
}
|
74 |
+
]
|
75 |
+
}
|
76 |
+
|
77 |
+
visualizer = DAGVisualizer()
|
78 |
+
image_path = visualizer.create_dag_visualization(task_data)
|
79 |
+
|
80 |
+
if image_path and os.path.exists(image_path):
|
81 |
+
print(f"β Multiple tasks visualization created: {image_path}")
|
82 |
+
return True
|
83 |
+
else:
|
84 |
+
print("β Failed to create multiple tasks visualization")
|
85 |
+
return False
|
86 |
+
|
87 |
+
def test_simplified_visualization():
|
88 |
+
"""Test simplified visualization"""
|
89 |
+
print("Testing simplified visualization...")
|
90 |
+
|
91 |
+
task_data = {
|
92 |
+
"tasks": [
|
93 |
+
{
|
94 |
+
"task": "excavate_soil_from_pile",
|
95 |
+
"instruction_function": {
|
96 |
+
"name": "excavate_soil_from_pile",
|
97 |
+
"robot_ids": ["robot_excavator_01"],
|
98 |
+
"dependencies": [],
|
99 |
+
"object_keywords": ["soil_pile"]
|
100 |
+
}
|
101 |
+
},
|
102 |
+
{
|
103 |
+
"task": "transport_soil_to_pit",
|
104 |
+
"instruction_function": {
|
105 |
+
"name": "transport_soil_to_pit",
|
106 |
+
"robot_ids": ["robot_dump_truck_01"],
|
107 |
+
"dependencies": ["excavate_soil_from_pile"],
|
108 |
+
"object_keywords": ["pit"]
|
109 |
+
}
|
110 |
+
}
|
111 |
+
]
|
112 |
+
}
|
113 |
+
|
114 |
+
visualizer = DAGVisualizer()
|
115 |
+
image_path = visualizer.create_simplified_dag_visualization(task_data)
|
116 |
+
|
117 |
+
if image_path and os.path.exists(image_path):
|
118 |
+
print(f"β Simplified visualization created: {image_path}")
|
119 |
+
return True
|
120 |
+
else:
|
121 |
+
print("β Failed to create simplified visualization")
|
122 |
+
return False
|
123 |
+
|
124 |
+
def test_invalid_data():
|
125 |
+
"""Test with invalid data"""
|
126 |
+
print("Testing invalid data handling...")
|
127 |
+
|
128 |
+
# Test with empty data
|
129 |
+
visualizer = DAGVisualizer()
|
130 |
+
image_path = visualizer.create_dag_visualization({})
|
131 |
+
|
132 |
+
if image_path is None:
|
133 |
+
print("β Invalid data handled correctly (returned None)")
|
134 |
+
return True
|
135 |
+
else:
|
136 |
+
print("β Invalid data not handled correctly")
|
137 |
+
return False
|
138 |
+
|
139 |
+
def main():
|
140 |
+
"""Run all tests"""
|
141 |
+
print("Starting DAG Visualization Tests...")
|
142 |
+
print("=" * 50)
|
143 |
+
|
144 |
+
tests = [
|
145 |
+
test_single_task,
|
146 |
+
test_multiple_tasks_with_dependencies,
|
147 |
+
test_simplified_visualization,
|
148 |
+
test_invalid_data
|
149 |
+
]
|
150 |
+
|
151 |
+
passed = 0
|
152 |
+
total = len(tests)
|
153 |
+
|
154 |
+
for test in tests:
|
155 |
+
try:
|
156 |
+
if test():
|
157 |
+
passed += 1
|
158 |
+
print()
|
159 |
+
except Exception as e:
|
160 |
+
print(f"β Test failed with exception: {e}")
|
161 |
+
print()
|
162 |
+
|
163 |
+
print("=" * 50)
|
164 |
+
print(f"Tests passed: {passed}/{total}")
|
165 |
+
|
166 |
+
if passed == total:
|
167 |
+
print("π All tests passed!")
|
168 |
+
return True
|
169 |
+
else:
|
170 |
+
print("β Some tests failed!")
|
171 |
+
return False
|
172 |
+
|
173 |
+
if __name__ == "__main__":
|
174 |
+
success = main()
|
175 |
+
sys.exit(0 if success else 1)
|
test_editing_workflow.py
ADDED
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Test script for the enhanced editing workflow
|
4 |
+
Tests: Edit Task Plan β Update DAG Visualization β Validate & Deploy
|
5 |
+
"""
|
6 |
+
|
7 |
+
import sys
|
8 |
+
import os
|
9 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
10 |
+
|
11 |
+
from gradio_llm_interface import GradioLlmInterface
|
12 |
+
import json
|
13 |
+
|
14 |
+
def test_task_plan_editor():
|
15 |
+
"""Test task plan editor functionality"""
|
16 |
+
print("Testing Task Plan Editor...")
|
17 |
+
print("=" * 40)
|
18 |
+
|
19 |
+
# Create test task data
|
20 |
+
test_task_data = {
|
21 |
+
"tasks": [
|
22 |
+
{
|
23 |
+
"task": "excavate_soil_from_pile",
|
24 |
+
"instruction_function": {
|
25 |
+
"name": "excavate_soil_from_pile",
|
26 |
+
"robot_ids": ["robot_excavator_01"],
|
27 |
+
"dependencies": [],
|
28 |
+
"object_keywords": ["soil_pile"]
|
29 |
+
}
|
30 |
+
}
|
31 |
+
]
|
32 |
+
}
|
33 |
+
|
34 |
+
interface = GradioLlmInterface()
|
35 |
+
|
36 |
+
# Test with existing task plan
|
37 |
+
state_with_plan = {'pending_task_plan': test_task_data}
|
38 |
+
result = interface.show_task_plan_editor(state_with_plan)
|
39 |
+
|
40 |
+
if result and len(result) == 4:
|
41 |
+
editor_update, dag_btn_update, validate_btn_update, status_msg = result
|
42 |
+
|
43 |
+
print("β Editor opened with existing task plan")
|
44 |
+
print(f" Status: {status_msg[:50]}...")
|
45 |
+
print(f" Editor visible: {'β' if editor_update.get('visible') else 'β'}")
|
46 |
+
print(f" JSON populated: {'β' if editor_update.get('value') else 'β'}")
|
47 |
+
|
48 |
+
return True
|
49 |
+
else:
|
50 |
+
print("β Failed to open task plan editor")
|
51 |
+
return False
|
52 |
+
|
53 |
+
def test_dag_update_from_editor():
|
54 |
+
"""Test DAG update from edited JSON"""
|
55 |
+
print("\nTesting DAG Update from Editor...")
|
56 |
+
print("=" * 40)
|
57 |
+
|
58 |
+
# Sample edited JSON
|
59 |
+
edited_json = """{
|
60 |
+
"tasks": [
|
61 |
+
{
|
62 |
+
"task": "move_to_position_1",
|
63 |
+
"instruction_function": {
|
64 |
+
"name": "move_to_position",
|
65 |
+
"robot_ids": ["robot_dump_truck_01"],
|
66 |
+
"dependencies": [],
|
67 |
+
"object_keywords": ["loading_zone"]
|
68 |
+
}
|
69 |
+
},
|
70 |
+
{
|
71 |
+
"task": "load_material_1",
|
72 |
+
"instruction_function": {
|
73 |
+
"name": "load_material",
|
74 |
+
"robot_ids": ["robot_dump_truck_01"],
|
75 |
+
"dependencies": ["move_to_position_1"],
|
76 |
+
"object_keywords": ["soil", "material"]
|
77 |
+
}
|
78 |
+
}
|
79 |
+
]
|
80 |
+
}"""
|
81 |
+
|
82 |
+
interface = GradioLlmInterface()
|
83 |
+
state = {}
|
84 |
+
|
85 |
+
try:
|
86 |
+
result = interface.update_dag_from_editor(edited_json, state)
|
87 |
+
|
88 |
+
if result and len(result) == 6:
|
89 |
+
dag_image, validate_btn, editor_vis, dag_btn_vis, status_msg, updated_state = result
|
90 |
+
|
91 |
+
print("β DAG updated from edited JSON")
|
92 |
+
print(f" Image generated: {'β' if dag_image else 'β'}")
|
93 |
+
print(f" Validate button shown: {'β' if validate_btn.get('visible') else 'β'}")
|
94 |
+
print(f" Task plan stored: {'β' if updated_state.get('pending_task_plan') else 'β'}")
|
95 |
+
|
96 |
+
return True
|
97 |
+
else:
|
98 |
+
print("β Failed to update DAG from editor")
|
99 |
+
return False
|
100 |
+
|
101 |
+
except Exception as e:
|
102 |
+
print(f"β Error updating DAG: {e}")
|
103 |
+
return False
|
104 |
+
|
105 |
+
def test_invalid_json_handling():
|
106 |
+
"""Test handling of invalid JSON"""
|
107 |
+
print("\nTesting Invalid JSON Handling...")
|
108 |
+
print("=" * 40)
|
109 |
+
|
110 |
+
invalid_json = """{
|
111 |
+
"tasks": [
|
112 |
+
{
|
113 |
+
"task": "invalid_task"
|
114 |
+
"missing_comma": true
|
115 |
+
}
|
116 |
+
]
|
117 |
+
}"""
|
118 |
+
|
119 |
+
interface = GradioLlmInterface()
|
120 |
+
state = {}
|
121 |
+
|
122 |
+
try:
|
123 |
+
result = interface.update_dag_from_editor(invalid_json, state)
|
124 |
+
|
125 |
+
if result and len(result) == 6:
|
126 |
+
dag_image, validate_btn, editor_vis, dag_btn_vis, status_msg, updated_state = result
|
127 |
+
|
128 |
+
if "JSON Parsing Error" in status_msg:
|
129 |
+
print("β Invalid JSON handled correctly")
|
130 |
+
print(f" Error message displayed: {'β' if 'JSON Parsing Error' in status_msg else 'β'}")
|
131 |
+
print(f" Editor kept visible: {'β' if editor_vis.get('visible') else 'β'}")
|
132 |
+
return True
|
133 |
+
else:
|
134 |
+
print("β Invalid JSON not handled correctly")
|
135 |
+
return False
|
136 |
+
else:
|
137 |
+
print("β Unexpected result from invalid JSON")
|
138 |
+
return False
|
139 |
+
|
140 |
+
except Exception as e:
|
141 |
+
print(f"β Unexpected exception: {e}")
|
142 |
+
return False
|
143 |
+
|
144 |
+
def test_full_workflow():
|
145 |
+
"""Test the complete editing workflow"""
|
146 |
+
print("\nTesting Complete Editing Workflow...")
|
147 |
+
print("=" * 40)
|
148 |
+
|
149 |
+
interface = GradioLlmInterface()
|
150 |
+
|
151 |
+
# Step 1: Open editor
|
152 |
+
state = {}
|
153 |
+
print("Step 1: Opening task plan editor...")
|
154 |
+
editor_result = interface.show_task_plan_editor(state)
|
155 |
+
|
156 |
+
if not editor_result:
|
157 |
+
print("β Failed at step 1")
|
158 |
+
return False
|
159 |
+
|
160 |
+
# Step 2: Update DAG with valid JSON
|
161 |
+
valid_json = """{
|
162 |
+
"tasks": [
|
163 |
+
{
|
164 |
+
"task": "complete_workflow_test",
|
165 |
+
"instruction_function": {
|
166 |
+
"name": "test_function",
|
167 |
+
"robot_ids": ["robot_excavator_01"],
|
168 |
+
"dependencies": [],
|
169 |
+
"object_keywords": ["test_object"]
|
170 |
+
}
|
171 |
+
}
|
172 |
+
]
|
173 |
+
}"""
|
174 |
+
|
175 |
+
print("Step 2: Updating DAG from editor...")
|
176 |
+
update_result = interface.update_dag_from_editor(valid_json, state)
|
177 |
+
|
178 |
+
if not update_result or len(update_result) != 6:
|
179 |
+
print("β Failed at step 2")
|
180 |
+
return False
|
181 |
+
|
182 |
+
# Step 3: Validate and deploy
|
183 |
+
print("Step 3: Validating and deploying...")
|
184 |
+
deploy_result = interface.validate_and_deploy_task_plan(state)
|
185 |
+
|
186 |
+
if deploy_result:
|
187 |
+
print("β Complete workflow test passed")
|
188 |
+
return True
|
189 |
+
else:
|
190 |
+
print("β Failed at step 3")
|
191 |
+
return False
|
192 |
+
|
193 |
+
def main():
|
194 |
+
"""Run all editing workflow tests"""
|
195 |
+
print("π οΈ Enhanced Editing Workflow Tests")
|
196 |
+
print("=" * 50)
|
197 |
+
|
198 |
+
tests = [
|
199 |
+
test_task_plan_editor,
|
200 |
+
test_dag_update_from_editor,
|
201 |
+
test_invalid_json_handling,
|
202 |
+
test_full_workflow
|
203 |
+
]
|
204 |
+
|
205 |
+
passed = 0
|
206 |
+
total = len(tests)
|
207 |
+
|
208 |
+
for test in tests:
|
209 |
+
try:
|
210 |
+
if test():
|
211 |
+
passed += 1
|
212 |
+
except Exception as e:
|
213 |
+
print(f"β Test failed with exception: {e}")
|
214 |
+
|
215 |
+
print("\n" + "=" * 50)
|
216 |
+
print(f"Editing Workflow Tests passed: {passed}/{total}")
|
217 |
+
|
218 |
+
if passed == total:
|
219 |
+
print("π All editing workflow tests passed!")
|
220 |
+
print("\nπ§ Enhanced Workflow Features:")
|
221 |
+
print(" β Manual JSON editing capability")
|
222 |
+
print(" β Real-time DAG visualization updates")
|
223 |
+
print(" β JSON validation and error handling")
|
224 |
+
print(" β Three-step safety workflow:")
|
225 |
+
print(" 1. π Edit Task Plan")
|
226 |
+
print(" 2. π Update DAG Visualization")
|
227 |
+
print(" 3. π Validate & Deploy Task Plan")
|
228 |
+
return True
|
229 |
+
else:
|
230 |
+
print("β Some editing workflow tests failed!")
|
231 |
+
return False
|
232 |
+
|
233 |
+
if __name__ == "__main__":
|
234 |
+
success = main()
|
235 |
+
sys.exit(0 if success else 1)
|
test_persistent_editing.py
ADDED
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Test script for persistent editing functionality
|
4 |
+
Tests multiple edit cycles to ensure task plans persist correctly
|
5 |
+
"""
|
6 |
+
|
7 |
+
import sys
|
8 |
+
import os
|
9 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
10 |
+
|
11 |
+
from gradio_llm_interface import GradioLlmInterface
|
12 |
+
import json
|
13 |
+
|
14 |
+
def test_edit_cycle():
|
15 |
+
"""Test complete edit cycle: generate β edit β update β deploy β edit again"""
|
16 |
+
print("Testing Complete Edit Cycle...")
|
17 |
+
print("=" * 50)
|
18 |
+
|
19 |
+
interface = GradioLlmInterface()
|
20 |
+
|
21 |
+
# Step 1: Initial task plan (simulating LLM generation)
|
22 |
+
initial_plan = {
|
23 |
+
"tasks": [
|
24 |
+
{
|
25 |
+
"task": "move_soil_1",
|
26 |
+
"instruction_function": {
|
27 |
+
"name": "move_soil",
|
28 |
+
"robot_ids": ["robot_excavator_01"],
|
29 |
+
"dependencies": [],
|
30 |
+
"object_keywords": ["soil_pile"]
|
31 |
+
}
|
32 |
+
}
|
33 |
+
]
|
34 |
+
}
|
35 |
+
|
36 |
+
state = {'pending_task_plan': initial_plan}
|
37 |
+
print("β Step 1: Initial task plan created")
|
38 |
+
|
39 |
+
# Step 2: Open editor with initial plan
|
40 |
+
editor_result = interface.show_task_plan_editor(state)
|
41 |
+
if not editor_result or len(editor_result) != 4:
|
42 |
+
print("β Step 2: Failed to open editor")
|
43 |
+
return False
|
44 |
+
|
45 |
+
editor_update, dag_btn, validate_btn, status = editor_result
|
46 |
+
if "move_soil" in editor_update.get('value', ''):
|
47 |
+
print("β Step 2: Editor opened with correct initial plan")
|
48 |
+
else:
|
49 |
+
print("β Step 2: Editor does not contain initial plan")
|
50 |
+
return False
|
51 |
+
|
52 |
+
# Step 3: Edit the plan
|
53 |
+
edited_json = """{
|
54 |
+
"tasks": [
|
55 |
+
{
|
56 |
+
"task": "move_soil_1_edited",
|
57 |
+
"instruction_function": {
|
58 |
+
"name": "move_soil_edited",
|
59 |
+
"robot_ids": ["robot_excavator_01", "robot_dump_truck_01"],
|
60 |
+
"dependencies": [],
|
61 |
+
"object_keywords": ["soil_pile", "edited_keyword"]
|
62 |
+
}
|
63 |
+
},
|
64 |
+
{
|
65 |
+
"task": "transport_soil_1",
|
66 |
+
"instruction_function": {
|
67 |
+
"name": "transport_soil",
|
68 |
+
"robot_ids": ["robot_dump_truck_01"],
|
69 |
+
"dependencies": ["move_soil_1_edited"],
|
70 |
+
"object_keywords": ["destination"]
|
71 |
+
}
|
72 |
+
}
|
73 |
+
]
|
74 |
+
}"""
|
75 |
+
|
76 |
+
update_result = interface.update_dag_from_editor(edited_json, state)
|
77 |
+
if not update_result or len(update_result) != 6:
|
78 |
+
print("β Step 3: Failed to update DAG from editor")
|
79 |
+
return False
|
80 |
+
|
81 |
+
print("β Step 3: DAG updated with edited plan")
|
82 |
+
|
83 |
+
# Step 4: Deploy the plan
|
84 |
+
deploy_result = interface.validate_and_deploy_task_plan(state)
|
85 |
+
if not deploy_result:
|
86 |
+
print("β Step 4: Failed to deploy plan")
|
87 |
+
return False
|
88 |
+
|
89 |
+
print("β Step 4: Plan deployed successfully")
|
90 |
+
|
91 |
+
# Step 5: Try to edit again (this should show the deployed plan)
|
92 |
+
second_editor_result = interface.show_task_plan_editor(state)
|
93 |
+
if not second_editor_result or len(second_editor_result) != 4:
|
94 |
+
print("β Step 5: Failed to open editor second time")
|
95 |
+
return False
|
96 |
+
|
97 |
+
second_editor_update, _, _, second_status = second_editor_result
|
98 |
+
if "move_soil_1_edited" in second_editor_update.get('value', ''):
|
99 |
+
print("β Step 5: Editor opened with deployed plan (persistent editing working)")
|
100 |
+
return True
|
101 |
+
else:
|
102 |
+
print("β Step 5: Editor lost the deployed plan content")
|
103 |
+
print(f" Editor content: {second_editor_update.get('value', 'No content')[:100]}...")
|
104 |
+
return False
|
105 |
+
|
106 |
+
def test_empty_state_handling():
|
107 |
+
"""Test editor behavior with completely empty state"""
|
108 |
+
print("\nTesting Empty State Handling...")
|
109 |
+
print("=" * 40)
|
110 |
+
|
111 |
+
interface = GradioLlmInterface()
|
112 |
+
empty_state = {}
|
113 |
+
|
114 |
+
result = interface.show_task_plan_editor(empty_state)
|
115 |
+
if result and len(result) == 4:
|
116 |
+
editor_update, _, _, status = result
|
117 |
+
if "example_task_1" in editor_update.get('value', ''):
|
118 |
+
print("β Empty state shows example template")
|
119 |
+
return True
|
120 |
+
else:
|
121 |
+
print("β Empty state does not show proper template")
|
122 |
+
return False
|
123 |
+
else:
|
124 |
+
print("β Failed to handle empty state")
|
125 |
+
return False
|
126 |
+
|
127 |
+
def test_malformed_state_handling():
|
128 |
+
"""Test editor behavior with malformed state data"""
|
129 |
+
print("\nTesting Malformed State Handling...")
|
130 |
+
print("=" * 40)
|
131 |
+
|
132 |
+
interface = GradioLlmInterface()
|
133 |
+
|
134 |
+
# Test with empty tasks array
|
135 |
+
malformed_state = {
|
136 |
+
'pending_task_plan': {
|
137 |
+
'tasks': []
|
138 |
+
}
|
139 |
+
}
|
140 |
+
|
141 |
+
result = interface.show_task_plan_editor(malformed_state)
|
142 |
+
if result and len(result) == 4:
|
143 |
+
editor_update, _, _, status = result
|
144 |
+
if "example_task_1" in editor_update.get('value', ''):
|
145 |
+
print("β Malformed state (empty tasks) handled correctly")
|
146 |
+
return True
|
147 |
+
else:
|
148 |
+
print("β Malformed state not handled properly")
|
149 |
+
return False
|
150 |
+
else:
|
151 |
+
print("β Failed to handle malformed state")
|
152 |
+
return False
|
153 |
+
|
154 |
+
def main():
|
155 |
+
"""Run all persistent editing tests"""
|
156 |
+
print("π Persistent Editing Tests")
|
157 |
+
print("=" * 50)
|
158 |
+
|
159 |
+
tests = [
|
160 |
+
test_edit_cycle,
|
161 |
+
test_empty_state_handling,
|
162 |
+
test_malformed_state_handling
|
163 |
+
]
|
164 |
+
|
165 |
+
passed = 0
|
166 |
+
total = len(tests)
|
167 |
+
|
168 |
+
for test in tests:
|
169 |
+
try:
|
170 |
+
if test():
|
171 |
+
passed += 1
|
172 |
+
except Exception as e:
|
173 |
+
print(f"β Test failed with exception: {e}")
|
174 |
+
|
175 |
+
print("\n" + "=" * 50)
|
176 |
+
print(f"Persistent Editing Tests passed: {passed}/{total}")
|
177 |
+
|
178 |
+
if passed == total:
|
179 |
+
print("π All persistent editing tests passed!")
|
180 |
+
print("\nπ Persistent Editing Features:")
|
181 |
+
print(" β Task plans persist through edit cycles")
|
182 |
+
print(" β Deployed plans can be re-edited")
|
183 |
+
print(" β State management handles edge cases")
|
184 |
+
print(" β Proper fallback to templates when needed")
|
185 |
+
return True
|
186 |
+
else:
|
187 |
+
print("β Some persistent editing tests failed!")
|
188 |
+
return False
|
189 |
+
|
190 |
+
if __name__ == "__main__":
|
191 |
+
success = main()
|
192 |
+
sys.exit(0 if success else 1)
|
test_safety_workflow.py
ADDED
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Test script for safety confirmation workflow
|
4 |
+
"""
|
5 |
+
|
6 |
+
import sys
|
7 |
+
import os
|
8 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
9 |
+
|
10 |
+
from gradio_llm_interface import GradioLlmInterface
|
11 |
+
import json
|
12 |
+
|
13 |
+
def test_safety_workflow():
|
14 |
+
"""Test the safety confirmation workflow"""
|
15 |
+
print("Testing Safety Confirmation Workflow...")
|
16 |
+
print("=" * 50)
|
17 |
+
|
18 |
+
# Create mock task data
|
19 |
+
mock_task_data = {
|
20 |
+
"tasks": [
|
21 |
+
{
|
22 |
+
"task": "excavate_soil_from_pile",
|
23 |
+
"instruction_function": {
|
24 |
+
"name": "excavate_soil_from_pile",
|
25 |
+
"robot_ids": ["robot_excavator_01"],
|
26 |
+
"dependencies": [],
|
27 |
+
"object_keywords": ["soil_pile"]
|
28 |
+
}
|
29 |
+
},
|
30 |
+
{
|
31 |
+
"task": "transport_soil_to_pit",
|
32 |
+
"instruction_function": {
|
33 |
+
"name": "transport_soil_to_pit",
|
34 |
+
"robot_ids": ["robot_dump_truck_01"],
|
35 |
+
"dependencies": ["excavate_soil_from_pile"],
|
36 |
+
"object_keywords": ["pit"]
|
37 |
+
}
|
38 |
+
}
|
39 |
+
]
|
40 |
+
}
|
41 |
+
|
42 |
+
# Initialize interface
|
43 |
+
interface = GradioLlmInterface()
|
44 |
+
|
45 |
+
# Create mock state with pending task plan
|
46 |
+
mock_state = {
|
47 |
+
'pending_task_plan': mock_task_data,
|
48 |
+
'history': []
|
49 |
+
}
|
50 |
+
|
51 |
+
print("β Mock state created with pending task plan")
|
52 |
+
print(f" Tasks: {len(mock_task_data['tasks'])}")
|
53 |
+
|
54 |
+
# Test validation and deployment
|
55 |
+
try:
|
56 |
+
result = interface.validate_and_deploy_task_plan(mock_state)
|
57 |
+
|
58 |
+
if result:
|
59 |
+
confirmation_msg, approved_image_path, button_update, updated_state = result
|
60 |
+
|
61 |
+
print("\nπ Validation Results:")
|
62 |
+
print(f" Confirmation Message: {confirmation_msg[:100]}...")
|
63 |
+
print(f" Image Generated: {'β' if approved_image_path else 'β'}")
|
64 |
+
print(f" Button Hidden: {'β' if not button_update.get('visible', True) else 'β'}")
|
65 |
+
print(f" State Updated: {'β' if updated_state.get('pending_task_plan') is None else 'β'}")
|
66 |
+
|
67 |
+
return True
|
68 |
+
else:
|
69 |
+
print("β No result returned from validation function")
|
70 |
+
return False
|
71 |
+
|
72 |
+
except Exception as e:
|
73 |
+
print(f"β Error during validation: {e}")
|
74 |
+
return False
|
75 |
+
|
76 |
+
def test_empty_state():
|
77 |
+
"""Test with empty state (no pending task plan)"""
|
78 |
+
print("\nTesting Empty State Handling...")
|
79 |
+
print("=" * 30)
|
80 |
+
|
81 |
+
interface = GradioLlmInterface()
|
82 |
+
empty_state = {'history': []}
|
83 |
+
|
84 |
+
try:
|
85 |
+
result = interface.validate_and_deploy_task_plan(empty_state)
|
86 |
+
|
87 |
+
if result:
|
88 |
+
warning_msg, image_path, button_update, state = result
|
89 |
+
|
90 |
+
if "No Task Plan to Deploy" in warning_msg:
|
91 |
+
print("β Empty state handled correctly")
|
92 |
+
return True
|
93 |
+
else:
|
94 |
+
print("β Unexpected warning message")
|
95 |
+
return False
|
96 |
+
else:
|
97 |
+
print("β No result returned for empty state")
|
98 |
+
return False
|
99 |
+
|
100 |
+
except Exception as e:
|
101 |
+
print(f"β Error handling empty state: {e}")
|
102 |
+
return False
|
103 |
+
|
104 |
+
def main():
|
105 |
+
"""Run safety workflow tests"""
|
106 |
+
print("π Safety Confirmation Workflow Tests")
|
107 |
+
print("=" * 50)
|
108 |
+
|
109 |
+
tests = [
|
110 |
+
test_safety_workflow,
|
111 |
+
test_empty_state
|
112 |
+
]
|
113 |
+
|
114 |
+
passed = 0
|
115 |
+
total = len(tests)
|
116 |
+
|
117 |
+
for test in tests:
|
118 |
+
try:
|
119 |
+
if test():
|
120 |
+
passed += 1
|
121 |
+
except Exception as e:
|
122 |
+
print(f"β Test failed with exception: {e}")
|
123 |
+
|
124 |
+
print("\n" + "=" * 50)
|
125 |
+
print(f"Safety Tests passed: {passed}/{total}")
|
126 |
+
|
127 |
+
if passed == total:
|
128 |
+
print("π All safety workflow tests passed!")
|
129 |
+
print("\nπ Safety Features:")
|
130 |
+
print(" β Task plan approval workflow")
|
131 |
+
print(" β Visual confirmation before deployment")
|
132 |
+
print(" β Error handling and validation")
|
133 |
+
print(" β State management for pending plans")
|
134 |
+
return True
|
135 |
+
else:
|
136 |
+
print("β Some safety tests failed!")
|
137 |
+
return False
|
138 |
+
|
139 |
+
if __name__ == "__main__":
|
140 |
+
success = main()
|
141 |
+
sys.exit(0 if success else 1)
|