├── chatgpt_ros ├── resource │ └── chatgpt_ros ├── chatgpt_ros │ ├── __init__.py │ ├── chatgpt_action_client.py │ └── chatgpt_action_server.py ├── setup.cfg ├── package.xml ├── setup.py └── test │ ├── test_copyright.py │ ├── test_pep257.py │ └── test_flake8.py ├── chatgpt_ros_interfaces ├── msg │ └── .gitkeep ├── srv │ ├── SetPrompt.srv │ └── AddExample.srv ├── action │ └── Chat.action ├── package.xml └── CMakeLists.txt ├── chatgpt_ros_turtlesim ├── resource │ └── chatgpt_ros_turtlesim ├── chatgpt_ros_turtlesim │ ├── __init__.py │ └── chat_turtle.py ├── setup.cfg ├── launch │ └── demo.py ├── package.xml ├── setup.py └── test │ ├── test_copyright.py │ ├── test_pep257.py │ └── test_flake8.py ├── LICENSE ├── README.md └── .gitignore /chatgpt_ros/resource/chatgpt_ros: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chatgpt_ros/chatgpt_ros/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chatgpt_ros_interfaces/msg/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chatgpt_ros_turtlesim/resource/chatgpt_ros_turtlesim: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chatgpt_ros_turtlesim/chatgpt_ros_turtlesim/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chatgpt_ros_interfaces/srv/SetPrompt.srv: -------------------------------------------------------------------------------- 1 | string prompt 2 | --- -------------------------------------------------------------------------------- /chatgpt_ros_interfaces/srv/AddExample.srv: -------------------------------------------------------------------------------- 1 | string user 2 | string assistant 3 | --- -------------------------------------------------------------------------------- /chatgpt_ros_interfaces/action/Chat.action: -------------------------------------------------------------------------------- 1 | string messages 2 | --- 3 | string content 4 | --- 5 | string delta -------------------------------------------------------------------------------- /chatgpt_ros/setup.cfg: -------------------------------------------------------------------------------- 1 | [develop] 2 | script-dir=$base/lib/chatgpt_ros 3 | [install] 4 | install-scripts=$base/lib/chatgpt_ros 5 | -------------------------------------------------------------------------------- /chatgpt_ros_turtlesim/setup.cfg: -------------------------------------------------------------------------------- 1 | [develop] 2 | script-dir=$base/lib/chatgpt_ros_turtlesim 3 | [install] 4 | install-scripts=$base/lib/chatgpt_ros_turtlesim 5 | -------------------------------------------------------------------------------- /chatgpt_ros_turtlesim/launch/demo.py: -------------------------------------------------------------------------------- 1 | from launch import LaunchDescription 2 | from launch_ros.actions import Node 3 | 4 | def generate_launch_description(): 5 | return LaunchDescription([ 6 | Node( 7 | package='chatgpt_ros', 8 | namespace='/', 9 | executable='chatgpt_action_server', 10 | name='chatgpt_action_server' 11 | ), 12 | Node( 13 | package='turtlesim', 14 | namespace='/', 15 | executable='turtlesim_node', 16 | name='sim' 17 | ), 18 | Node( 19 | package='chatgpt_ros_turtlesim', 20 | namespace='/', 21 | executable='chat_turtle', 22 | name='chat_turtle', 23 | prefix="gnome-terminal --" 24 | ) 25 | ]) -------------------------------------------------------------------------------- /chatgpt_ros_turtlesim/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | chatgpt_ros_turtlesim 5 | 0.0.0 6 | TODO: Package description 7 | hy 8 | TODO: License declaration 9 | 10 | rclpy 11 | ros2launch 12 | chatgpt_ros_interfaces 13 | 14 | ament_copyright 15 | ament_flake8 16 | ament_pep257 17 | python3-pytest 18 | 19 | 20 | ament_python 21 | 22 | 23 | -------------------------------------------------------------------------------- /chatgpt_ros_turtlesim/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | package_name = 'chatgpt_ros_turtlesim' 4 | 5 | setup( 6 | name=package_name, 7 | version='0.0.0', 8 | packages=[package_name], 9 | data_files=[ 10 | ('share/ament_index/resource_index/packages', 11 | ['resource/' + package_name]), 12 | ('share/' + package_name, ['package.xml']), 13 | ('share/' + package_name, ['launch/demo.py']), 14 | ], 15 | install_requires=['setuptools'], 16 | zip_safe=True, 17 | maintainer='hy', 18 | maintainer_email='810130242@qq.com', 19 | description='TODO: Package description', 20 | license='TODO: License declaration', 21 | tests_require=['pytest'], 22 | entry_points={ 23 | 'console_scripts': [ 24 | 'chat_turtle = chatgpt_ros_turtlesim.chat_turtle:main', 25 | ], 26 | }, 27 | ) 28 | -------------------------------------------------------------------------------- /chatgpt_ros/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | chatgpt_ros 5 | 0.0.0 6 | TODO: Package description 7 | hy 8 | TODO: License declaration 9 | 10 | rclpy 11 | std_msgs 12 | std_srvs 13 | chatgpt_ros_interfaces 14 | 15 | ament_copyright 16 | ament_flake8 17 | ament_pep257 18 | python3-pytest 19 | 20 | 21 | ament_python 22 | 23 | 24 | -------------------------------------------------------------------------------- /chatgpt_ros/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | package_name = 'chatgpt_ros' 4 | 5 | setup( 6 | name=package_name, 7 | version='0.0.0', 8 | packages=[package_name], 9 | data_files=[ 10 | ('share/ament_index/resource_index/packages', 11 | ['resource/' + package_name]), 12 | ('share/' + package_name, ['package.xml']) 13 | ], 14 | install_requires=['setuptools'], 15 | zip_safe=True, 16 | maintainer='hy', 17 | maintainer_email='810130242@qq.com', 18 | description='TODO: Package description', 19 | license='TODO: License declaration', 20 | tests_require=['pytest'], 21 | entry_points={ 22 | 'console_scripts': [ 23 | 'chatgpt_action_server = chatgpt_ros.chatgpt_action_server:main', 24 | 'chatgpt_action_client = chatgpt_ros.chatgpt_action_client:main' 25 | ], 26 | }, 27 | ) 28 | -------------------------------------------------------------------------------- /chatgpt_ros_interfaces/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | chatgpt_ros_interfaces 5 | 0.0.0 6 | TODO: Package description 7 | hy 8 | TODO: License declaration 9 | 10 | ament_cmake 11 | 12 | ament_lint_auto 13 | ament_lint_common 14 | 15 | rosidl_default_generators 16 | rosidl_default_runtime 17 | rosidl_interface_packages 18 | 19 | 20 | ament_cmake 21 | 22 | 23 | -------------------------------------------------------------------------------- /chatgpt_ros/test/test_copyright.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_copyright.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.copyright 20 | @pytest.mark.linter 21 | def test_copyright(): 22 | rc = main(argv=['.', 'test']) 23 | assert rc == 0, 'Found errors' 24 | -------------------------------------------------------------------------------- /chatgpt_ros/test/test_pep257.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_pep257.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.linter 20 | @pytest.mark.pep257 21 | def test_pep257(): 22 | rc = main(argv=['.', 'test']) 23 | assert rc == 0, 'Found code style errors / warnings' 24 | -------------------------------------------------------------------------------- /chatgpt_ros_turtlesim/test/test_copyright.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_copyright.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.copyright 20 | @pytest.mark.linter 21 | def test_copyright(): 22 | rc = main(argv=['.', 'test']) 23 | assert rc == 0, 'Found errors' 24 | -------------------------------------------------------------------------------- /chatgpt_ros_turtlesim/test/test_pep257.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_pep257.main import main 16 | import pytest 17 | 18 | 19 | @pytest.mark.linter 20 | @pytest.mark.pep257 21 | def test_pep257(): 22 | rc = main(argv=['.', 'test']) 23 | assert rc == 0, 'Found code style errors / warnings' 24 | -------------------------------------------------------------------------------- /chatgpt_ros/test/test_flake8.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_flake8.main import main_with_errors 16 | import pytest 17 | 18 | 19 | @pytest.mark.flake8 20 | @pytest.mark.linter 21 | def test_flake8(): 22 | rc, errors = main_with_errors(argv=[]) 23 | assert rc == 0, \ 24 | 'Found %d code style errors / warnings:\n' % len(errors) + \ 25 | '\n'.join(errors) 26 | -------------------------------------------------------------------------------- /chatgpt_ros_turtlesim/test/test_flake8.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Open Source Robotics Foundation, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from ament_flake8.main import main_with_errors 16 | import pytest 17 | 18 | 19 | @pytest.mark.flake8 20 | @pytest.mark.linter 21 | def test_flake8(): 22 | rc, errors = main_with_errors(argv=[]) 23 | assert rc == 0, \ 24 | 'Found %d code style errors / warnings:\n' % len(errors) + \ 25 | '\n'.join(errors) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 交叉坐标的星辰 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /chatgpt_ros_interfaces/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(chatgpt_ros_interfaces) 3 | 4 | # Default to C99 5 | if(NOT CMAKE_C_STANDARD) 6 | set(CMAKE_C_STANDARD 99) 7 | endif() 8 | 9 | # Default to C++14 10 | if(NOT CMAKE_CXX_STANDARD) 11 | set(CMAKE_CXX_STANDARD 14) 12 | endif() 13 | 14 | if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") 15 | add_compile_options(-Wall -Wextra -Wpedantic) 16 | endif() 17 | 18 | # find dependencies 19 | find_package(ament_cmake REQUIRED) 20 | # uncomment the following section in order to fill in 21 | # further dependencies manually. 22 | # find_package( REQUIRED) 23 | 24 | find_package(geometry_msgs REQUIRED) 25 | find_package(rosidl_default_generators REQUIRED) 26 | 27 | rosidl_generate_interfaces(${PROJECT_NAME} 28 | "action/Chat.action" 29 | "srv/SetPrompt.srv" 30 | "srv/AddExample.srv" 31 | ) 32 | 33 | if(BUILD_TESTING) 34 | find_package(ament_lint_auto REQUIRED) 35 | # the following line skips the linter which checks for copyrights 36 | # uncomment the line when a copyright and license is not present in all source files 37 | #set(ament_cmake_copyright_FOUND TRUE) 38 | # the following line skips cpplint (only works in a git repo) 39 | # uncomment the line when this package is not in a git repo 40 | #set(ament_cmake_cpplint_FOUND TRUE) 41 | ament_lint_auto_find_test_dependencies() 42 | endif() 43 | 44 | ament_package() 45 | -------------------------------------------------------------------------------- /chatgpt_ros/chatgpt_ros/chatgpt_action_client.py: -------------------------------------------------------------------------------- 1 | import rclpy 2 | from rclpy.action import ActionClient 3 | from rclpy.node import Node 4 | 5 | from chatgpt_ros_interfaces.action import Chat 6 | 7 | 8 | class ChatActionClient(Node): 9 | 10 | def __init__(self): 11 | super().__init__('chatgpt_action_client') 12 | self._action_client = ActionClient(self, Chat, 'chat') 13 | 14 | def send_goal(self, messages): 15 | goal_msg = Chat.Goal() 16 | goal_msg.messages = messages 17 | 18 | self._action_client.wait_for_server() 19 | 20 | self._send_goal_future = self._action_client.send_goal_async(goal_msg, feedback_callback=self.feedback_callback) 21 | 22 | self._send_goal_future.add_done_callback(self.goal_response_callback) 23 | 24 | def goal_response_callback(self, future): 25 | goal_handle = future.result() 26 | if not goal_handle.accepted: 27 | return 28 | 29 | self._get_result_future = goal_handle.get_result_async() 30 | self._get_result_future.add_done_callback(self.get_result_callback) 31 | 32 | def get_result_callback(self, future): 33 | print() 34 | self.send_goal(input("Input: ")) 35 | 36 | def feedback_callback(self, feedback_msg): 37 | feedback = feedback_msg.feedback 38 | print(feedback.delta, end='', flush=True) 39 | 40 | 41 | def main(args=None): 42 | rclpy.init(args=args) 43 | 44 | action_client = ChatActionClient() 45 | 46 | action_client.send_goal(input("Input: ")) 47 | 48 | rclpy.spin(action_client) 49 | 50 | 51 | if __name__ == '__main__': 52 | main() 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chatgpt_ros 2 | 3 | https://user-images.githubusercontent.com/59406481/230091094-198598c8-d4c2-4cd6-ad87-e34df3f56664.mp4 4 | 5 | ## Quick Start 6 | 7 | First you need to get your api key from openai and set it as an environment variable. 8 | 9 | ``` 10 | export OPENAI_API_KEY= 11 | ``` 12 | 13 | After that you can start the action server and thus establish communication with chatgpt. 14 | 15 | ``` 16 | ros2 run chatgpt_ros chatgpt_action_server 17 | ``` 18 | 19 | This command starts a node named `/chatgpt_action_server`, which provides the following services and actions. 20 | 21 | Services: 22 | - /reset (std_srvs/srv/Empty): Reset chat history. 23 | - /set_prompt (chatgpt_ros_interfaces/srv/SetPrompt): Set system prompt. 24 | - /add_example (chatgpt_ros_interfaces/srv/AddExample): Add example. 25 | 26 | Actions: 27 | - /chat (chatgpt_ros_interfaces/action/Chat): Chat with ChatGPT. 28 | 29 | This is the description of the `/chat` action. 30 | 31 | ``` 32 | string messages 33 | --- 34 | string content 35 | --- 36 | string delta 37 | ``` 38 | 39 | The goal (messages) of the chat action is the user's input. The response from chatgpt will keep coming back from feedback (delta). When the chat ends, a full reply (content) is returned. 40 | 41 | You can start an action client to test the `/chat` action. 42 | 43 | ``` 44 | ros2 run chatgpt_ros chatgpt_action_client 45 | ``` 46 | 47 | ## Turtlesim Demo 48 | 49 | We also provide a demo on how to control the turtlesim in ros using chatgpt. You can start the demo with the following command: 50 | 51 | ``` 52 | ros2 launch chatgpt_ros_turtlesim demo.py 53 | ``` 54 | 55 | This will pop up a separate command window for entering commands to control the turtle. You can instruct the turtles to draw graphics through natural language. 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /chatgpt_ros/chatgpt_ros/chatgpt_action_server.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import json 4 | import urllib.request 5 | import rclpy 6 | from rclpy.action import ActionServer 7 | from rclpy.node import Node 8 | 9 | from chatgpt_ros_interfaces.action import Chat 10 | from chatgpt_ros_interfaces.srv import SetPrompt, AddExample 11 | from std_srvs.srv import Empty 12 | 13 | 14 | class ChatBot: 15 | """ 16 | Chatbot class for conversation. 17 | """ 18 | 19 | def __init__(self, api_key, api_base, prompt=None): 20 | self.api_key = api_key 21 | self.api_base = api_base 22 | if prompt is None: 23 | current_date = datetime.datetime.now().strftime('%Y-%m-%d') 24 | self.prompt = f"You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible. Knowledge cutoff: 2021-09-01, Current date: {current_date}." 25 | else: 26 | self.prompt = prompt 27 | self.reset() 28 | 29 | def _parse_stream(self, s): 30 | """Parse server-sent events""" 31 | for line in s: 32 | line = line.decode('utf-8') 33 | if line.startswith(':'): 34 | continue 35 | if line.strip() == "data: [DONE]": 36 | return None 37 | if line.startswith("data: "): 38 | yield json.loads(line[len("data: "):]) 39 | 40 | def _request(self): 41 | """Send request to openai""" 42 | req = urllib.request.Request( 43 | url=f'{self.api_base}/chat/completions', 44 | data=json.dumps({ 45 | "model": "gpt-3.5-turbo", 46 | "temperature": 0, 47 | "stream": True, 48 | "messages": self.messages 49 | }).encode('utf-8'), 50 | headers={ 51 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36", 52 | "Content-Type": "application/json", 53 | "Authorization": f"Bearer {self.api_key}" 54 | } 55 | ) 56 | return self._parse_stream(urllib.request.urlopen(req)) 57 | 58 | def chat(self, messages): 59 | """ 60 | Chat with chatgpt model. 61 | :param content: User input 62 | :return: A content generator 63 | """ 64 | self.messages.append({"role": "user", "content": messages}) 65 | 66 | collected_messages = [] 67 | for chunk in self._request(): 68 | chunk_message = chunk['choices'][0]['delta'] 69 | collected_messages.append(chunk_message) 70 | yield chunk_message.get('content', '') 71 | 72 | full_reply_role = ''.join([m.get('role', '') for m in collected_messages]) 73 | full_reply_content = ''.join([m.get('content', '') for m in collected_messages]) 74 | self.messages.append({"role": full_reply_role, "content": full_reply_content}) 75 | 76 | def get_latest_content(self): 77 | return self.messages[-1]['content'] 78 | 79 | def reset(self): 80 | self.messages = [ 81 | { 82 | "role": "system", 83 | "content": self.prompt 84 | } 85 | ] 86 | 87 | 88 | class ChatActionServer(Node): 89 | def __init__(self): 90 | # initialize node 91 | super().__init__('chatgpt_action_server') 92 | 93 | # load openai api key 94 | api_key = os.getenv("OPENAI_API_KEY") 95 | api_base = os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1") 96 | assert api_key is not None, "OPENAI_API_KEY is not found in environment variables." 97 | 98 | # create chatbot object 99 | self._chatbot = ChatBot(api_key, api_base) 100 | 101 | # create ros action server 102 | self._action_server = ActionServer(self, Chat, 'chat', self._execute_callback) 103 | 104 | # create service 105 | self._reset_service = self.create_service(Empty, 'reset', self._reset_callback) 106 | self._set_prompt_service = self.create_service(SetPrompt, 'set_prompt', self._set_prompt_callback) 107 | self._add_example_service = self.create_service(AddExample, 'add_example', self._add_example_callback) 108 | 109 | def _execute_callback(self, goal_handle): 110 | messages = goal_handle.request.messages 111 | self.get_logger().info(f'Chat goal received: {messages}') 112 | 113 | # stream chat result 114 | feedback_msg = Chat.Feedback() 115 | for res in self._chatbot.chat(messages): 116 | feedback_msg.delta = res 117 | goal_handle.publish_feedback(feedback_msg) 118 | 119 | # chat finished 120 | goal_handle.succeed() 121 | 122 | # return total chat reply 123 | result = Chat.Result() 124 | result.content = self._chatbot.get_latest_content() 125 | self.get_logger().info('Chat finished.') 126 | return result 127 | 128 | def _reset_callback(self, request, response): 129 | """Reset chat history""" 130 | self._chatbot.reset() 131 | return response 132 | 133 | def _set_prompt_callback(self, request, response): 134 | """Set system prompt""" 135 | self._chatbot.prompt = request.prompt 136 | self._chatbot.reset() 137 | self.get_logger().info(f'Chat prompt changed: {self._chatbot.prompt}') 138 | return response 139 | 140 | def _add_example_callback(self, request, response): 141 | """Add example""" 142 | self._chatbot.messages.append({"role": "user", "content": request.user}) 143 | self._chatbot.messages.append({"role": "assistant", "content": request.assistant}) 144 | return response 145 | 146 | 147 | def main(args=None): 148 | rclpy.init(args=args) 149 | 150 | chat_action_server = ChatActionServer() 151 | 152 | rclpy.spin(chat_action_server) 153 | 154 | 155 | if __name__ == '__main__': 156 | main() 157 | -------------------------------------------------------------------------------- /chatgpt_ros_turtlesim/chatgpt_ros_turtlesim/chat_turtle.py: -------------------------------------------------------------------------------- 1 | import rclpy 2 | from rclpy.action import ActionClient 3 | from rclpy.node import Node 4 | from rclpy.executors import SingleThreadedExecutor 5 | import threading 6 | import re 7 | import math 8 | 9 | from chatgpt_ros_interfaces.action import Chat 10 | from chatgpt_ros_interfaces.srv import SetPrompt, AddExample 11 | from turtlesim.srv import SetPen, TeleportRelative, TeleportAbsolute 12 | from turtlesim.msg import Pose 13 | from std_srvs.srv import Empty 14 | 15 | 16 | system_prompt = """You are an assistant helping me with the simulator for robots. 17 | When I ask you to do something, you are supposed to give me Python code that is needed to achieve that task using simulator and then an explanation of what that code does. 18 | You are only allowed to use the functions I have defined for you. 19 | You are not to use any other hypothetical functions that you think might exist. 20 | You can use simple Python functions from libraries such as math and numpy.""" 21 | 22 | user_prompt = """Here are some functions you can use to command the robot. 23 | 24 | turtlebot.clear() - Delete the turtle's drawings from the screen. Do not move turtle. State and position of the turtle as well as drawings of other turtles are not affected. 25 | turtlebot.forward(distance) - Move the turtle forward by the specified distance, in the direction the turtle is headed. 26 | turtlebot.backward(distance) - Move the turtle backward by distance, opposite to the direction the turtle is headed. Do not change the turtle's heading. 27 | turtlebot.right(angle) - Turn turtle right by degrees. 28 | turtlebot.left(angle) - Turn turtle left by degrees. 29 | turtlebot.setposition(x, y) - Move turtle to an absolute position. If the pen is down, draw line. Do not change the turtle's orientation. 30 | turtlebot.setheading(to_angle) - Set the orientation of the turtle to to_angle. 31 | turtlebot.home() - Move turtle to the origin - coordinates (0,0) - and set its heading to its start-orientation. 32 | turtlebot.pendown() - Pull the pen down - drawing when moving. 33 | turtlebot.penup() - Pull the pen up - no drawing when moving. 34 | turtlebot.get_position() - Return the turtle's current location (x,y). 35 | turtlebot.get_heading() - Return the turtle's current heading. 36 | 37 | A few useful things: 38 | Instead of moveToPositionAsync() or moveToZAsync(), you should use the function setposition() that I have defined for you. 39 | This is a two-dimensional environment, and setposition() accepts only 2 parameters. 40 | Note that the robot is initially in the center of a square area of size 10*10, and you are not allowed to let the robot touch the boundary of the area.""" 41 | 42 | examples = [ 43 | { 44 | "user": "move 2 units forward", 45 | "assistant": """```python 46 | turtlebot.forward(10) 47 | ``` 48 | This code uses the `forward()` function to move the robot to a new position that is 2 units before the current position.""" 49 | } 50 | ] 51 | 52 | 53 | class TurtleBot: 54 | def __init__(self, node, turtle_namespace='/turtle1'): 55 | self._node = node 56 | self._set_pen_cli = node.create_client(SetPen, turtle_namespace + "/set_pen") 57 | self._teleport_relative_cli = node.create_client(TeleportRelative, turtle_namespace + "/teleport_relative") 58 | self._teleport_absolute_cli = node.create_client(TeleportAbsolute, turtle_namespace + "/teleport_absolute") 59 | self._clear_cli = node.create_client(Empty, "/clear") 60 | self._pose_sub = node.create_subscription(Pose, turtle_namespace + "/pose", self._pose_callback, 1) 61 | self._origin = [5.544444561004639, 5.544444561004639, 0.0] 62 | self._pose = [0.0, 0.0, 0.0] 63 | 64 | self.home() 65 | 66 | def _pose_callback(self, msg): 67 | self._pose[0] = msg.x - self._origin[0] 68 | self._pose[1] = msg.y - self._origin[1] 69 | self._pose[2] = msg.theta - - self._origin[2] 70 | 71 | def _degree_to_radian(self, angle): 72 | return angle * math.pi / 180.0 73 | 74 | def clear(self): 75 | """Delete the turtle's drawings from the screen. Do not move turtle. State and position of the turtle as well as drawings of other turtles are not affected. 76 | """ 77 | rclpy.spin_until_future_complete(self._node, self._clear_cli.call_async(Empty.Request())) 78 | 79 | def forward(self, distance): 80 | """Move the turtle forward by the specified distance, in the direction the turtle is headed. 81 | 82 | Args: 83 | distance (integer or float): a number 84 | """ 85 | req = TeleportRelative.Request() 86 | req.linear = float(distance) 87 | rclpy.spin_until_future_complete(self._node, self._teleport_relative_cli.call_async(req)) 88 | 89 | 90 | def backward(self, distance): 91 | """Move the turtle backward by distance, opposite to the direction the turtle is headed. Do not change the turtle's heading. 92 | 93 | Args: 94 | distance (integer or float): a number 95 | """ 96 | req = TeleportRelative.Request() 97 | req.linear = -float(distance) 98 | rclpy.spin_until_future_complete(self._node, self._teleport_relative_cli.call_async(req)) 99 | 100 | def right(self, angle): 101 | """Turn turtle right by degrees. 102 | 103 | Args: 104 | angle (integer or float): a number 105 | """ 106 | req = TeleportRelative.Request() 107 | req.angular = -self._degree_to_radian(angle) 108 | rclpy.spin_until_future_complete(self._node, self._teleport_relative_cli.call_async(req)) 109 | 110 | def left(self, angle): 111 | """Turn turtle left by degrees. 112 | 113 | Args: 114 | angle (integer or float): a number 115 | """ 116 | req = TeleportRelative.Request() 117 | req.angular = self._degree_to_radian(angle) 118 | rclpy.spin_until_future_complete(self._node, self._teleport_relative_cli.call_async(req)) 119 | 120 | def setposition(self, x, y): 121 | """Move turtle to an absolute position. If the pen is down, draw line. Do not change the turtle's orientation. 122 | 123 | Args: 124 | x (integer or float): a number 125 | y (integer or float): a number 126 | """ 127 | req = TeleportAbsolute.Request() 128 | req.x = float(x) + self._origin[0] 129 | req.y = float(y) + self._origin[1] 130 | req.theta = self._pose[2] + self._origin[2] 131 | rclpy.spin_until_future_complete(self._node, self._teleport_absolute_cli.call_async(req)) 132 | 133 | def setheading(self, to_angle): 134 | """Set the orientation of the turtle to to_angle. 135 | 136 | Args: 137 | to_angle (integer or float): a number 138 | """ 139 | req = TeleportAbsolute.Request() 140 | req.x = self._pose[0] + self._origin[0] 141 | req.y = self._pose[1] + self._origin[1] 142 | req.theta = to_angle + self._origin[2] 143 | rclpy.spin_until_future_complete(self._node, self._teleport_absolute_cli.call_async(req)) 144 | 145 | def home(self): 146 | """Move turtle to the origin - coordinates (5.0, 5.0) - and set its heading to its start-orientation. 147 | """ 148 | req = TeleportAbsolute.Request() 149 | req.x = self._origin[0] 150 | req.y = self._origin[1] 151 | req.theta = self._origin[2] 152 | rclpy.spin_until_future_complete(self._node, self._teleport_absolute_cli.call_async(req)) 153 | 154 | def pendown(self): 155 | """Pull the pen down - drawing when moving. 156 | """ 157 | req = SetPen.Request() 158 | req.r = 255 159 | req.g = 255 160 | req.b = 255 161 | req.width = 2 162 | req.off = 0 163 | rclpy.spin_until_future_complete(self._node, self._set_pen_cli.call_async(req)) 164 | 165 | def penup(self): 166 | """Pull the pen up - no drawing when moving. 167 | """ 168 | req = SetPen.Request() 169 | req.r = 255 170 | req.g = 255 171 | req.b = 255 172 | req.width = 2 173 | req.off = 1 174 | rclpy.spin_until_future_complete(self._node, self._set_pen_cli.call_async(req)) 175 | 176 | def get_position(self): 177 | """Return the turtle's current location (x,y). 178 | 179 | Returns: 180 | list[float]: a list of numbers 181 | """ 182 | return self._pose[:2] 183 | 184 | def get_heading(self): 185 | """Return the turtle's current heading. 186 | 187 | Returns: 188 | float: a number 189 | """ 190 | return self._pose[2] 191 | 192 | 193 | class ChatTurlteClient(Node): 194 | def __init__(self): 195 | super().__init__('chat_action_client') 196 | self._action_client = ActionClient(self, Chat, 'chat') 197 | self._set_prompt_cli = self.create_client(SetPrompt, "set_prompt") 198 | self._add_example_cli = self.create_client(AddExample, "add_example") 199 | 200 | def initialize(self): 201 | req = SetPrompt.Request() 202 | req.prompt = system_prompt 203 | self._set_prompt_cli.wait_for_service() 204 | set_prompt_future = self._set_prompt_cli.call_async(req) 205 | rclpy.spin_until_future_complete(self, set_prompt_future) 206 | 207 | for ex in examples: 208 | req = AddExample.Request() 209 | req.user = ex['user'] 210 | req.assistant = ex['assistant'] 211 | self._add_example_cli.wait_for_service() 212 | add_example_future = self._add_example_cli.call_async(req) 213 | rclpy.spin_until_future_complete(self, add_example_future) 214 | 215 | self.send_goal(user_prompt, slient=True) 216 | 217 | def send_goal(self, messages, slient=False): 218 | goal_msg = Chat.Goal() 219 | goal_msg.messages = messages 220 | 221 | self._action_client.wait_for_server() 222 | if slient: 223 | send_goal_future = self._action_client.send_goal_async(goal_msg) 224 | else: 225 | send_goal_future = self._action_client.send_goal_async(goal_msg, feedback_callback=self.feedback_callback) 226 | rclpy.spin_until_future_complete(self, send_goal_future) 227 | 228 | get_result_future = send_goal_future.result().get_result_async() 229 | rclpy.spin_until_future_complete(self, get_result_future) 230 | if not slient: 231 | print() 232 | 233 | return get_result_future.result().result.content 234 | 235 | def feedback_callback(self, feedback_msg): 236 | feedback = feedback_msg.feedback 237 | print(feedback.delta, end='', flush=True) 238 | 239 | 240 | def extract_python_code(content): 241 | code_block_regex = re.compile(r"```(.*?)```", re.DOTALL) 242 | code_blocks = code_block_regex.findall(content) 243 | if code_blocks: 244 | full_code = "\n".join(code_blocks) 245 | 246 | if full_code.startswith("python"): 247 | full_code = full_code[7:] 248 | 249 | return full_code 250 | else: 251 | return None 252 | 253 | 254 | def interact_function(node): 255 | turtlebot = TurtleBot(node) 256 | while rclpy.ok(): 257 | user_input = input('user: ') 258 | reply = node.send_goal(user_input) 259 | code = extract_python_code(reply) 260 | if code is not None: 261 | try: 262 | exec(code) 263 | except Exception as e: 264 | node.send_goal(str(e)) 265 | 266 | 267 | def main(args=None): 268 | rclpy.init(args=args) 269 | 270 | node = ChatTurlteClient() 271 | node.initialize() 272 | 273 | interact_thread = threading.Thread(target=interact_function, args=(node,)) 274 | interact_thread.run() 275 | 276 | executer = SingleThreadedExecutor() 277 | executer.add_node(node) 278 | executer.spin() 279 | 280 | 281 | if __name__ == '__main__': 282 | main() 283 | --------------------------------------------------------------------------------