YongdongWang commited on
Commit
92ef79b
Β·
verified Β·
1 Parent(s): af1ac25

Upload folder using huggingface_hub

Browse files
.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)