diff --git a/MAVProxy/modules/mavproxy_chat/__init__.py b/MAVProxy/modules/mavproxy_chat/__init__.py index 322ec5f273..95419a4de1 100644 --- a/MAVProxy/modules/mavproxy_chat/__init__.py +++ b/MAVProxy/modules/mavproxy_chat/__init__.py @@ -7,6 +7,8 @@ OpenAI Assistant API: https://platform.openai.com/docs/api-reference/assistants OpenAI Assistant Playground: https://platform.openai.com/playground MAVProxy chat wiki: https://ardupilot.org/mavproxy/docs/modules/chat.html + +AP_FLAKE8_CLEAN ''' from MAVProxy.modules.lib import mp_module @@ -16,6 +18,7 @@ from threading import Thread import time + class chat(mp_module.MPModule): def __init__(self, mpstate): @@ -110,6 +113,7 @@ def wait_for_command_ack(self, mav_cmd, timeout=1): del self.command_ack_waiting[mav_cmd] return False + # initialise module def init(mpstate): return chat(mpstate) diff --git a/MAVProxy/modules/mavproxy_chat/assistant_setup/assistant_file_checker.py b/MAVProxy/modules/mavproxy_chat/assistant_setup/assistant_file_checker.py index e684ddd250..0c43dd5528 100644 --- a/MAVProxy/modules/mavproxy_chat/assistant_setup/assistant_file_checker.py +++ b/MAVProxy/modules/mavproxy_chat/assistant_setup/assistant_file_checker.py @@ -7,31 +7,35 @@ OpenAI Assistant API: https://platform.openai.com/docs/api-reference/assistants OpenAI Assistant Playground: https://platform.openai.com/playground + +AP_FLAKE8_CLEAN ''' -import datetime, os +import datetime +import os try: from openai import OpenAI -except: +except Exception: print("chat: failed to import openai. See https://ardupilot.org/mavproxy/docs/modules/chat.html") exit() + # main function def main(openai_api_key=None, delete_unused=False): - + print("Starting Assistant File checker") - + # create connection object try: # if api key is provided, use it to create connection object if openai_api_key is not None: client = OpenAI(api_key=openai_api_key) else: - # if no api key is provided, attempt to create connection object without it - # user may have set the OPENAI_API_KEY environment variable - client = OpenAI() - except: + # if no api key is provided, attempt to create connection object without it + # user may have set the OPENAI_API_KEY environment variable + client = OpenAI() + except Exception: # if connection object creation fails, exit with error message print("assistant_file_checker: failed to connect to OpenAI. Perhaps the API key was incorrect?") exit() @@ -41,7 +45,7 @@ def main(openai_api_key=None, delete_unused=False): print("File list:") file_found = False for file in existing_files.data: - print(" id:" + file.id + " name:" + file.filename + " size:" + str(file.bytes) + " date:" + str(datetime.datetime.fromtimestamp(file.created_at))) + print(" id:" + file.id + " name:" + file.filename + " size:" + str(file.bytes) + " date:" + str(datetime.datetime.fromtimestamp(file.created_at))) # noqa file_found = True if not file_found: print(" no files found") @@ -64,12 +68,12 @@ def main(openai_api_key=None, delete_unused=False): for assistant_file in client.beta.assistants.files.list(assistant_id=assistant.id): file_assistant_count[assistant_file.id] = file_assistant_count.get(assistant_file.id, 0) + 1 filename_size_date = get_filename_size_date_from_id(existing_files, assistant_file.id) - print(" id:" + assistant_file.id + " name:" + filename_size_date[0] + " size:" + str(filename_size_date[1]) + " date:" + filename_size_date[2]) + print(" id:" + assistant_file.id + " name:" + filename_size_date[0] + " size:" + str(filename_size_date[1]) + " date:" + filename_size_date[2]) # noqa # display the list of files which are not used by any Assistants print_unused_files_header = True for existing_file in existing_files: - if not existing_file.id in file_assistant_count.keys(): + if existing_file.id not in file_assistant_count.keys(): # print header if this is the first unused file if print_unused_files_header: print("Files not used by any Assistants:") @@ -77,7 +81,7 @@ def main(openai_api_key=None, delete_unused=False): # print file attributes filename_size_date = get_filename_size_date_from_id(existing_files, existing_file.id) - print(" id:" + existing_file.id + " name:" + filename_size_date[0] + " size:" + str(filename_size_date[1]) + " date:" + filename_size_date[2]) + print(" id:" + existing_file.id + " name:" + filename_size_date[0] + " size:" + str(filename_size_date[1]) + " date:" + filename_size_date[2]) # noqa # delete the unused file if specified by the user if delete_unused: @@ -87,6 +91,7 @@ def main(openai_api_key=None, delete_unused=False): # print completion message print("Assistant File check complete") + # searches the files_list for the specified file id and retuns the filename, size and creation date (as a string) if found # returns None if not found def get_filename_size_date_from_id(files_list, file_id): @@ -95,9 +100,10 @@ def get_filename_size_date_from_id(files_list, file_id): return file.filename, file.bytes, str(datetime.datetime.fromtimestamp(file.created_at)) return "not found", "unknown", "unknown" + # call main function if this is run as standalone script if __name__ == "__main__": - + # parse command line arguments from argparse import ArgumentParser parser = ArgumentParser(description="MAVProxy Chat Module Assistant File Checker") @@ -105,7 +111,7 @@ def get_filename_size_date_from_id(files_list, file_id): parser.add_argument("--delete-unused", action='store_true', help="delete unused files") try: args = parser.parse_args() - except AttributeError as err: + except AttributeError: parser.print_help() os.sys.exit(0) main(openai_api_key=args.api_key, delete_unused=args.delete_unused) diff --git a/MAVProxy/modules/mavproxy_chat/assistant_setup/setup_assistant.py b/MAVProxy/modules/mavproxy_chat/assistant_setup/setup_assistant.py index 9a5f698706..d6ae171787 100644 --- a/MAVProxy/modules/mavproxy_chat/assistant_setup/setup_assistant.py +++ b/MAVProxy/modules/mavproxy_chat/assistant_setup/setup_assistant.py @@ -6,6 +6,8 @@ OpenAI Assistant API: https://platform.openai.com/docs/api-reference/assistants OpenAI Assistant Playground: https://platform.openai.com/playground + +AP_FLAKE8_CLEAN ''' import requests @@ -15,25 +17,26 @@ try: from openai import OpenAI -except: +except Exception: print("chat: failed to import openai. See https://ardupilot.org/mavproxy/docs/modules/chat.html") exit() + # main function def main(openai_api_key=None, assistant_name=None, model_name=None, upgrade=False): - + print("Starting assistant setup") - + # create connection object try: # if api key is provided, use it to create connection object if openai_api_key is not None: client = OpenAI(api_key=openai_api_key) else: - # if no api key is provided, attempt to create connection object without it - # user may have set the OPENAI_API_KEY environment variable - client = OpenAI() - except: + # if no api key is provided, attempt to create connection object without it + # user may have set the OPENAI_API_KEY environment variable + client = OpenAI() + except Exception: # if connection object creation fails, exit with error message print("setup_assistant: failed to connect to OpenAI. Perhaps the API key was incorrect?") exit() @@ -49,7 +52,7 @@ def main(openai_api_key=None, assistant_name=None, model_name=None, upgrade=Fals # check that assistant_instructions.txt file exists instructions_filename = os.path.join(os.getcwd(), "assistant_instructions.txt") if not os.path.isfile(instructions_filename): - print("setup_assistant: " + instructions_filename +" not found") + print("setup_assistant: " + instructions_filename + " not found") exit() # check that at least one text file exists. E.g. text files holding the flight mode number to name mappings @@ -72,7 +75,7 @@ def main(openai_api_key=None, assistant_name=None, model_name=None, upgrade=Fals function_object = json.load(function_file) function_tools.append(function_object) print("setup_assistant: parsed function file: " + os.path.basename(function_filename)) - except: + except Exception: print("setup_assistant: failed to parse json file: " + function_filename) exit() @@ -84,15 +87,15 @@ def main(openai_api_key=None, assistant_name=None, model_name=None, upgrade=Fals # download latest MAVLink files from ardupilot MAVLink repo, minimal.xml, common.xml and ardupilotmega.xml mavlink_filenames = ["minimal.xml", "common.xml", "ardupilotmega.xml"] for mavlink_filename in mavlink_filenames: - if not download_file("https://raw.githubusercontent.com/ArduPilot/mavlink/master/message_definitions/v1.0/" + mavlink_filename, mavlink_filename): + if not download_file("https://raw.githubusercontent.com/ArduPilot/mavlink/master/message_definitions/v1.0/" + mavlink_filename, mavlink_filename): # noqa exit() # download latest vehicle parameter definition files from ardupilot server paramdef_file_info = [ - {"url": "https://autotest.ardupilot.org/Parameters/ArduCopter/apm.pdef.xml", "filename": "copter_parameter_definitions.xml"}, - {"url": "https://autotest.ardupilot.org/Parameters/ArduPlane/apm.pdef.xml", "filename": "plane_parameter_definitions.xml"}, - {"url": "https://autotest.ardupilot.org/Parameters/APMrover2/apm.pdef.xml", "filename": "rover_parameter_definitions.xml"}, - {"url": "https://autotest.ardupilot.org/Parameters/ArduSub/apm.pdef.xml", "filename": "sub_parameter_definitions.xml"}] + {"url": "https://autotest.ardupilot.org/Parameters/ArduCopter/apm.pdef.xml", "filename": "copter_parameter_definitions.xml"}, # noqa + {"url": "https://autotest.ardupilot.org/Parameters/ArduPlane/apm.pdef.xml", "filename": "plane_parameter_definitions.xml"}, # noqa + {"url": "https://autotest.ardupilot.org/Parameters/APMrover2/apm.pdef.xml", "filename": "rover_parameter_definitions.xml"}, # noqa + {"url": "https://autotest.ardupilot.org/Parameters/ArduSub/apm.pdef.xml", "filename": "sub_parameter_definitions.xml"}] # noqa paramdef_filenames = [] for pdef_file_info in paramdef_file_info: if not download_file(pdef_file_info["url"], pdef_file_info["filename"]): @@ -124,7 +127,7 @@ def main(openai_api_key=None, assistant_name=None, model_name=None, upgrade=Fals try: instructions_content = open(instructions_filename, 'r').read() client.beta.assistants.update(assistant.id, instructions=instructions_content, tools=function_tools) - except: + except Exception: print("setup_assistant: failed to update assistant instructions") exit() @@ -136,7 +139,7 @@ def main(openai_api_key=None, assistant_name=None, model_name=None, upgrade=Fals try: # open local file as read-only file = open(filename, 'rb') - except: + except Exception: print("setup_assistant: failed to open file: " + filename) exit() @@ -155,7 +158,7 @@ def main(openai_api_key=None, assistant_name=None, model_name=None, upgrade=Fals try: client.files.delete(existing_file.id) print("setup_assistant: deleted existing file: " + filename) - except: + except Exception: print("setup_assistant: failed to delete file from OpenAI: " + filename) exit() @@ -165,14 +168,14 @@ def main(openai_api_key=None, assistant_name=None, model_name=None, upgrade=Fals uploaded_file = client.files.create(file=file, purpose="assistants") uploaded_file_ids.append(uploaded_file.id) print("setup_assistant: uploaded: " + filename) - except: + except Exception: print("setup_assistant: failed to upload file to OpenAI: " + filename) exit() # update assistant's accessible files try: client.beta.assistants.update(assistant.id, file_ids=uploaded_file_ids) - except: + except Exception: print("setup_assistant: failed to update assistant accessible files") exit() @@ -181,12 +184,13 @@ def main(openai_api_key=None, assistant_name=None, model_name=None, upgrade=Fals try: os.remove(mavlink_filename) print("setup_assistant: deleted local file: " + mavlink_filename) - except: + except Exception: print("setup_assistant: failed to delete file: " + mavlink_filename) # print completion message print("Assistant setup complete") + def download_file(url, filename): # download file from url to filename # return True if successful, False if not @@ -201,13 +205,14 @@ def download_file(url, filename): else: print("setup_assistant: failed to download file: " + url) return False - except: + except Exception: print("setup_assistant: failed to download file: " + url) return False + # call main function if this is run as standalone script if __name__ == "__main__": - + # parse command line arguments from argparse import ArgumentParser parser = ArgumentParser(description="MAVProxy AI chat module OpenAI Assistant setup script") diff --git a/MAVProxy/modules/mavproxy_chat/chat_openai.py b/MAVProxy/modules/mavproxy_chat/chat_openai.py index b9c422f360..18d57e4ffe 100644 --- a/MAVProxy/modules/mavproxy_chat/chat_openai.py +++ b/MAVProxy/modules/mavproxy_chat/chat_openai.py @@ -5,10 +5,13 @@ OpenAI Assistant API: https://platform.openai.com/docs/api-reference/assistants OpenAI Assistant Playground: https://platform.openai.com/playground MAVProxy chat wiki: https://ardupilot.org/mavproxy/docs/modules/chat.html + +AP_FLAKE8_CLEAN ''' from pymavlink import mavutil -import time, re +import time +import re from datetime import datetime from threading import Thread, Lock import json @@ -16,10 +19,11 @@ try: from openai import OpenAI -except: +except Exception: print("chat: failed to import openai. See https://ardupilot.org/mavproxy/docs/modules/chat.html") exit() + class chat_openai(): def __init__(self, mpstate, status_cb=None, wait_for_command_ack_fn=None): # keep reference to mpstate @@ -51,7 +55,7 @@ def check_connection(self): if self.client is None: try: self.client = OpenAI() - except: + except Exception: print("chat: failed to connect to OpenAI") return False @@ -90,7 +94,7 @@ def check_connection(self): # set the OpenAI API key def set_api_key(self, api_key_str): - self.client = OpenAI(api_key = api_key_str) + self.client = OpenAI(api_key=api_key_str) self.assistant = None self.assistant_thread = None @@ -126,7 +130,7 @@ def send_to_assistant(self, text): # wait for one second time.sleep(0.1) - # retrieve the run + # retrieve the run latest_run = self.client.beta.threads.runs.retrieve( thread_id=self.assistant_thread.id, run_id=self.run.id @@ -157,7 +161,9 @@ def send_to_assistant(self, text): self.send_status(status_message) # retrieve messages on the thread - reply_messages = self.client.beta.threads.messages.list(self.assistant_thread.id, order = "asc", after=input_message.id) + reply_messages = self.client.beta.threads.messages.list(self.assistant_thread.id, + order="asc", + after=input_message.id) if reply_messages is None: return "chat: failed to retrieve messages" @@ -228,7 +234,7 @@ def handle_function_call(self, run): # convert to json stage_str = "convert output to json" output = json.dumps(output) - except: + except Exception: error_message = str(func_name) + ": " + stage_str + " failed" print("chat: " + error_message) output = error_message @@ -242,12 +248,11 @@ def handle_function_call(self, run): # send function replies to assistant try: - run_reply = self.client.beta.threads.runs.submit_tool_outputs( + self.client.beta.threads.runs.submit_tool_outputs( thread_id=run.thread_id, run_id=run.id, - tool_outputs=tool_outputs - ) - except: + tool_outputs=tool_outputs) + except Exception: print("chat: error replying to function call") print(tool_outputs) @@ -278,7 +283,7 @@ def get_vehicle_type(self, arguments): mavutil.mavlink.MAV_TYPE_OCTOROTOR, mavutil.mavlink.MAV_TYPE_TRICOPTER, mavutil.mavlink.MAV_TYPE_DODECAROTOR]: - vehicle_type_str = "Copter" + vehicle_type_str = "Copter" if hearbeat_msg.type == mavutil.mavlink.MAV_TYPE_HELICOPTER: vehicle_type_str = "Heli" if hearbeat_msg.type == mavutil.mavlink.MAV_TYPE_ANTENNA_TRACKER: @@ -306,7 +311,7 @@ def get_mode_mapping(self, arguments): # prepare list of modes mode_list = [] mode_mapping = self.mpstate.master().mode_mapping() - + # handle request for all modes if mode_name is None and mode_number is None: for mname in mode_mapping: @@ -335,7 +340,7 @@ def get_vehicle_state(self, arguments): hearbeat_msg = self.mpstate.master().messages.get('HEARTBEAT', None) if hearbeat_msg is None: mode_number = 0 - print ("chat: get_vehicle_state: vehicle mode is unknown") + print("chat: get_vehicle_state: vehicle mode is unknown") else: mode_number = hearbeat_msg.custom_mode return { @@ -397,7 +402,10 @@ def send_mavlink_command_int(self, arguments): x = arguments.get("x", 0) y = arguments.get("y", 0) z = arguments.get("z", 0) - self.mpstate.master().mav.command_int_send(target_system, target_component, frame, command, current, autocontinue, param1, param2, param3, param4, x, y, z) + self.mpstate.master().mav.command_int_send(target_system, target_component, + frame, command, current, autocontinue, + param1, param2, param3, param4, + x, y, z) # wait for command ack mav_result = self.wait_for_command_ack_fn(command) @@ -441,7 +449,12 @@ def send_mavlink_set_position_target_global_int(self, arguments): afz = arguments.get("afz", 0) yaw = arguments.get("yaw", 0) yaw_rate = arguments.get("yaw_rate", 0) - self.mpstate.master().mav.set_position_target_global_int_send(time_boot_ms, target_system, target_component, coordinate_frame, type_mask, lat_int, lon_int, alt, vx, vy, vz, afx, afy, afz, yaw, yaw_rate) + self.mpstate.master().mav.set_position_target_global_int_send(time_boot_ms, target_system, target_component, + coordinate_frame, type_mask, + lat_int, lon_int, alt, + vx, vy, vz, + afx, afy, afz, + yaw, yaw_rate) return "set_position_target_global_int sent" # get a list of mavlink message names that can be retrieved using the get_mavlink_message function @@ -598,7 +611,7 @@ def delete_wakeup_timers(self, arguments): self.wakeup_schedule.remove(wakeup_timer) # return number deleted and remaining - return "delete_wakeup_timers: deleted " + str(num_timers_deleted) + " timers, " + str(len(self.wakeup_schedule)) + " remaining" + return "delete_wakeup_timers: deleted " + str(num_timers_deleted) + " timers, " + str(len(self.wakeup_schedule)) + " remaining" # noqa # check if any wakeup timers have expired and send messages if they have # this function never returns so it should be called from a new thread @@ -631,7 +644,7 @@ def wrap_latitude(self, latitude_deg): if latitude_deg < -90: return -(180 + latitude_deg) return latitude_deg - + # wrap longitude to range -180 to 180 def wrap_longitude(self, longitude_deg): if longitude_deg > 180: @@ -647,7 +660,7 @@ def send_status(self, status): # returns true if string contains regex characters def contains_regex(self, string): - regex_characters = ".^$*+?{}[]\|()" + regex_characters = ".^$*+?{}[]\\|()" for x in regex_characters: if string.count(x): return True diff --git a/MAVProxy/modules/mavproxy_chat/chat_voice_to_text.py b/MAVProxy/modules/mavproxy_chat/chat_voice_to_text.py index 66a90c9dc5..5718d4bc06 100644 --- a/MAVProxy/modules/mavproxy_chat/chat_voice_to_text.py +++ b/MAVProxy/modules/mavproxy_chat/chat_voice_to_text.py @@ -1,6 +1,8 @@ ''' AI Chat Module voice-to-text class Randy Mackay, December 2023 + +AP_FLAKE8_CLEAN ''' import time @@ -9,10 +11,11 @@ import pyaudio # install using, "sudo apt-get install python3-pyaudio" import wave # install with "pip3 install wave" from openai import OpenAI -except: +except Exception: print("chat: failed to import pyaudio, wave or openai. See https://ardupilot.org/mavproxy/docs/modules/chat.html") exit() + class chat_voice_to_text(): def __init__(self): # initialise OpenAI connection @@ -21,7 +24,7 @@ def __init__(self): # set the OpenAI API key def set_api_key(self, api_key_str): - self.client = OpenAI(api_key = api_key_str) + self.client = OpenAI(api_key=api_key_str) # check connection to OpenAI assistant and connect if necessary # returns True if connection is good, False if not @@ -30,7 +33,7 @@ def check_connection(self): if self.client is None: try: self.client = OpenAI() - except: + except Exception: print("chat: failed to connect to OpenAI") return False @@ -46,7 +49,7 @@ def record_audio(self): # Open stream try: stream = p.open(format=pyaudio.paInt16, channels=1, rate=44100, input=True, frames_per_buffer=1024) - except: + except Exception: print("chat: failed to connect to microphone") return None @@ -85,7 +88,7 @@ def convert_audio_to_text(self, audio_filename): # Process with Whisper audio_file = open(audio_filename, "rb") transcript = self.client.audio.transcriptions.create( - model="whisper-1", - file=audio_file, + model="whisper-1", + file=audio_file, response_format="text") return transcript diff --git a/MAVProxy/modules/mavproxy_chat/chat_window.py b/MAVProxy/modules/mavproxy_chat/chat_window.py index df5e790f45..f11e48d0ed 100644 --- a/MAVProxy/modules/mavproxy_chat/chat_window.py +++ b/MAVProxy/modules/mavproxy_chat/chat_window.py @@ -3,12 +3,15 @@ Randy Mackay, December 2023 Chat window for input and output of AI chat + +AP_FLAKE8_CLEAN ''' from MAVProxy.modules.lib.wx_loader import wx from MAVProxy.modules.mavproxy_chat import chat_openai, chat_voice_to_text from threading import Thread + class chat_window(): def __init__(self, mpstate, wait_for_command_ack_fn): # keep reference to mpstate @@ -35,7 +38,7 @@ def __init__(self, mpstate, wait_for_command_ack_fn): # add api key input window self.apikey_frame = wx.Frame(None, title="Input OpenAI API Key", size=(560, 50)) - self.apikey_text_input = wx.TextCtrl(self.apikey_frame, id=-1, pos=(10, 10), size=(450, -1), style = wx.TE_PROCESS_ENTER) + self.apikey_text_input = wx.TextCtrl(self.apikey_frame, id=-1, pos=(10, 10), size=(450, -1), style=wx.TE_PROCESS_ENTER) self.apikey_set_button = wx.Button(self.apikey_frame, id=-1, label="Set", pos=(470, 10), size=(75, 25)) self.apikey_frame.Bind(wx.EVT_BUTTON, self.apikey_set_button_click, self.apikey_set_button) self.apikey_frame.Bind(wx.EVT_TEXT_ENTER, self.apikey_set_button_click, self.apikey_text_input) @@ -48,17 +51,17 @@ def __init__(self, mpstate, wait_for_command_ack_fn): # add a record button self.record_button = wx.Button(self.frame, id=-1, label="Rec", size=(75, 25)) self.frame.Bind(wx.EVT_BUTTON, self.record_button_click, self.record_button) - self.horiz_sizer.Add(self.record_button, proportion = 0, flag = wx.ALIGN_TOP | wx.ALL, border = 5) + self.horiz_sizer.Add(self.record_button, proportion=0, flag=wx.ALIGN_TOP | wx.ALL, border=5) # add an input text box - self.text_input = wx.TextCtrl(self.frame, id=-1, value="", size=(450, -1), style = wx.TE_PROCESS_ENTER) + self.text_input = wx.TextCtrl(self.frame, id=-1, value="", size=(450, -1), style=wx.TE_PROCESS_ENTER) self.frame.Bind(wx.EVT_TEXT_ENTER, self.text_input_change, self.text_input) - self.horiz_sizer.Add(self.text_input, proportion = 1, flag = wx.ALIGN_TOP | wx.ALL, border = 5) + self.horiz_sizer.Add(self.text_input, proportion=1, flag=wx.ALIGN_TOP | wx.ALL, border=5) # add a send button self.send_button = wx.Button(self.frame, id=-1, label="Send", size=(75, 25)) self.frame.Bind(wx.EVT_BUTTON, self.send_button_click, self.send_button) - self.horiz_sizer.Add(self.send_button, proportion = 0, flag = wx.ALIGN_TOP | wx.ALL, border = 5) + self.horiz_sizer.Add(self.send_button, proportion=0, flag=wx.ALIGN_TOP | wx.ALL, border=5) # add a reply box and read-only text box self.text_reply = wx.TextCtrl(self.frame, id=-1, size=(600, 80), style=wx.TE_READONLY | wx.TE_MULTILINE | wx.TE_RICH)