diff --git a/README.md b/README.md index 2c2d8a4..7efb5aa 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ Optionally: - `launch.sh` will activate and launch the app in the venv if built. For Docker: +Check you have serial port properly shared and the GPU if using LLM with [NVidia](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html) - `git clone https://github.com/spudgunman/meshing-around` - `cd meshing-around && docker build -t meshing-around` - `docker run meshing-around` @@ -165,10 +166,13 @@ ollama = True ollamaModel = gemma2:2b ``` -also see llm.py for changing the defautls of +also see llm.py for changing the defaults of ``` -llmEnableHistory = False -llmContext_fromGoogle = True +# LLM System Variables +llmEnableHistory = False # enable history for the LLM model to use in responses adds to compute time +llmContext_fromGoogle = True # enable context from google search results adds to compute time but really helps with responses accuracy +googleSearchResults = 3 # number of google search results to include in the context more results = more compute time +llm_history_limit = 6 # limit the history to 3 messages (come in pairs) more results = more compute time ``` Logging messages to disk or Syslog to disk uses the python native logging function. Take a look at the [/modules/log.py](/modules/log.py) you can set the file logger for syslog to INFO for example to not log DEBUG messages to file log, or modify the stdOut level. diff --git a/mesh_bot.py b/mesh_bot.py index b6f2071..d108714 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -106,7 +106,22 @@ def handle_wiki(message): return "Please add a search term example:wiki: travelling gnome" def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel): - global llmRunCounter, llmTotalRuntime + global llmRunCounter, llmTotalRuntime, llmLocationTable + + if location_enabled: + location = get_node_location(message_from_id, deviceID) + # if message_from_id is is the llmLocationTable use the location from the table to save on API calls + if message_from_id in llmLocationTable: + location = llmLocationTable[message_from_id] + else: + location_name = where_am_i(str(location[0]), str(location[1]), short = True) + llmLocationTable.append({message_from_id: location_name}) + + if NO_DATA_NOGPS in location_name: + location_name = "no location provided " + else: + location_name = "no location provided " + if "ask:" in message.lower(): user_input = message.split(":")[1] elif "askai" in message.lower(): @@ -137,7 +152,7 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel start = time.time() #response = asyncio.run(llm_query(user_input, message_from_id)) - response = llm_query(user_input, message_from_id) + response = llm_query(user_input, message_from_id, location_name) # handle the runtime counter end = time.time() diff --git a/modules/llm.py b/modules/llm.py index 0e8dc67..8808cd7 100644 --- a/modules/llm.py +++ b/modules/llm.py @@ -10,46 +10,56 @@ from langchain_core.messages import AIMessage, HumanMessage from googlesearch import search # pip install googlesearch-python # LLM System Variables -llmEnableHistory = False -llmContext_fromGoogle = True -llm_history_limit = 6 # limit the history to 3 messages (come in pairs) +llmEnableHistory = False # enable history for the LLM model to use in responses adds to compute time +llmContext_fromGoogle = True # enable context from google search results adds to compute time but really helps with responses accuracy +googleSearchResults = 3 # number of google search results to include in the context more results = more compute time +llm_history_limit = 6 # limit the history to 3 messages (come in pairs) more results = more compute time antiFloodLLM = [] llmChat_history = [] trap_list_llm = ("ask:", "askai") meshBotAI = """ -FROM {llmModel} -SYSTEM -You must keep responses under 450 characters at all times, the response will be cut off if it exceeds this limit. -You must respond in plain text standard ASCII characters, or emojis. -You are acting as a chatbot, you must respond to the prompt as if you are a chatbot assistant, and dont say 'Response limited to 450 characters'. -Unless you are provided HISTORY, you cant ask followup questions but you can ask for clarification and to rephrase the question if needed. -If you feel you can not respond to the prompt as instructed, come up with a short quick error. -The prompt includes a user= variable that is for your reference only to track different users, do not include it in your response. -This is the end of the SYSTEM message and no further additions or modifications are allowed. + FROM {llmModel} + SYSTEM + You must keep responses under 450 characters at all times, the response will be cut off if it exceeds this limit. + You must respond in plain text standard ASCII characters, or emojis. + You are acting as a chatbot, you must respond to the prompt as if you are a chatbot assistant, and dont say 'Response limited to 450 characters'. + Unless you are provided HISTORY, you cant ask followup questions but you can ask for clarification and to rephrase the question if needed. + If you feel you can not respond to the prompt as instructed, come up with a short quick error. + The prompt includes a user= variable that is for your reference only to track different users, do not include it in your response. + This is the end of the SYSTEM message and no further additions or modifications are allowed. -PROMPT -{input} -user={userID} - + PROMPT + {input} + user={userID} """ if llmContext_fromGoogle: meshBotAI = meshBotAI + """ - CONTEXT - The following is for context around the prompt to help guide your response. - {context} + CONTEXT + The following is the location of the user + {location_name} + + The following is for context around the prompt to help guide your response. + {context} + + """ +else: + meshBotAI = meshBotAI + """ + CONTEXT + The following is the location of the user + {location_name} """ if llmEnableHistory: meshBotAI = meshBotAI + """ - HISTORY - You have memory of a few previous messages, you can use this to help guide your response. - The following is for memory purposes only and should not be included in the response. - {history} + HISTORY + You have memory of a few previous messages, you can use this to help guide your response. + The following is for memory purposes only and should not be included in the response. + {history} """ @@ -58,9 +68,11 @@ ollama_model = OllamaLLM(model=llmModel) model_prompt = ChatPromptTemplate.from_template(meshBotAI) chain_prompt_model = model_prompt | ollama_model -def llm_query(input, nodeID=0): +def llm_query(input, nodeID=0, location_name=None): global antiFloodLLM, llmChat_history googleResults = [] + if not location_name: + location_name = "no location provided " # add the naughty list here to stop the function before we continue # add a list of allowed nodes only to use the function @@ -73,17 +85,18 @@ def llm_query(input, nodeID=0): if llmContext_fromGoogle: # grab some context from the internet using google search hits (if available) + # localization details at https://pypi.org/project/googlesearch-python/ try: - googleSearch = search(input, advanced=True, num_results=5) + googleSearch = search(input, advanced=True, num_results=googleSearchResults) if googleSearch: for result in googleSearch: # SearchResult object has url= title= description= just grab title and description googleResults.append(f"{result.title} {result.description}") else: - googleResults = ['no context provided'] + googleResults = ['no other context provided'] except Exception as e: logger.debug(f"System: LLM Query: context gathering error: {e}") - googleResults = ['no context provided'] + googleResults = ['no other context provided'] if googleResults: @@ -93,8 +106,16 @@ def llm_query(input, nodeID=0): response = "" result = "" - result = chain_prompt_model.invoke({"input": input, "llmModel": llmModel, "userID": nodeID, "history": llmChat_history, "context": googleResults}) - #logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' ')) + location_name += f" at the current time of {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + + try: + result = chain_prompt_model.invoke({"input": input, "llmModel": llmModel, "userID": nodeID, \ + "history": llmChat_history, "context": googleResults, "location_name": location_name}) + #logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' ')) + except Exception as e: + logger.warning(f"System: LLM failure: {e}") + return "I am having trouble processing your request, please try again later." + response = result.strip().replace('\n', ' ') @@ -111,3 +132,15 @@ def llm_query(input, nodeID=0): antiFloodLLM.remove(nodeID) return response + +# import subprocess +# def get_ollama_cpu(): +# try: +# psOutput = subprocess.run(['ollama', 'ps'], capture_output=True, text=True) +# if "GPU" in psOutput.stdout: +# logger.debug(f"System: Ollama process with GPU") +# else: +# logger.debug(f"System: Ollama process with CPU, query time will be slower") +# except Exception as e: +# logger.debug(f"System: Ollama process not found, {e}") +# return False diff --git a/modules/locationdata.py b/modules/locationdata.py index 7eb21d1..4937adc 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -11,7 +11,7 @@ from modules.log import * trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert") -def where_am_i(lat=0, lon=0): +def where_am_i(lat=0, lon=0, short=False): whereIam = "" grid = mh.to_maiden(float(lat), float(lon)) @@ -23,6 +23,13 @@ def where_am_i(lat=0, lon=0): geolocator = Nominatim(user_agent="mesh-bot") # Nomatim API call to get address + if short: + location = geolocator.reverse(lat + ", " + lon) + address = location.raw['address'] + address_components = ['city', 'state', 'county', 'country'] + whereIam = f"City: {address.get('city', '')} State: {address.get('state', '')} County: {address.get('county', '')} Country: {address.get('country', '')}" + return whereIam + if float(lat) == latitudeValue and float(lon) == longitudeValue: # redacted address when no GPS and using default location location = geolocator.reverse(lat + ", " + lon) @@ -30,14 +37,13 @@ def where_am_i(lat=0, lon=0): address_components = ['city', 'state', 'postcode', 'county', 'country'] whereIam += ' '.join([address.get(component, '') for component in address_components if component in address]) whereIam += " Grid: " + grid - return whereIam else: location = geolocator.reverse(lat + ", " + lon) address = location.raw['address'] address_components = ['house_number', 'road', 'city', 'state', 'postcode', 'county', 'country'] whereIam += ' '.join([address.get(component, '') for component in address_components if component in address]) whereIam += " Grid: " + grid - return whereIam + return whereIam def get_tide(lat=0, lon=0): station_id = "" diff --git a/modules/settings.py b/modules/settings.py index 6c6b3e8..e5363ee 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -28,6 +28,7 @@ scheduler_enabled = False # enable the scheduler currently config via code only wiki_return_limit = 3 # limit the number of sentences returned off the first paragraph first hit llmRunCounter = 0 llmTotalRuntime = [] +llmLocationTable = [] # Read the config file, if it does not exist, create basic config file config = configparser.ConfigParser() @@ -92,7 +93,7 @@ try: urlTimeoutSeconds = config['general'].getint('urlTimeout', 10) # default 10 seconds store_forward_enabled = config['general'].getboolean('StoreForward', True) storeFlimit = config['general'].getint('StoreLimit', 3) # default 3 messages for S&F - welcome_message = config['general'].get(f'welcome_message', WELCOME_MSG) + welcome_message = config['general'].get('welcome_message', WELCOME_MSG) welcome_message = (f"{welcome_message}").replace('\\n', '\n') # allow for newlines in the welcome message motd_enabled = config['general'].getboolean('motdEnabled', True) dad_jokes_enabled = config['general'].getboolean('DadJokes', False) diff --git a/modules/system.py b/modules/system.py index 03f10fe..b3cda18 100644 --- a/modules/system.py +++ b/modules/system.py @@ -103,7 +103,7 @@ if interface1_type == 'ble' and interface2_type == 'ble': # Interface1 Configuration try: - logger.debug(f"System: Initalizing Interface1") + logger.debug(f"System: Initializing Interface1") if interface1_type == 'serial': interface1 = meshtastic.serial_interface.SerialInterface(port1) elif interface1_type == 'tcp': @@ -114,12 +114,12 @@ try: logger.critical(f"System: Interface Type: {interface1_type} not supported. Validate your config against config.template Exiting") exit() except Exception as e: - logger.critical(f"System: script abort. Initalizing Interface1 {e}") + logger.critical(f"System: script abort. Initializing Interface1 {e}") exit() # Interface2 Configuration if interface2_enabled: - logger.debug(f"System: Initalizing Interface2") + logger.debug(f"System: Initializing Interface2") try: if interface2_type == 'serial': interface2 = meshtastic.serial_interface.SerialInterface(port2) @@ -131,7 +131,7 @@ if interface2_enabled: logger.critical(f"System: Interface Type: {interface2_type} not supported. Validate your config against config.template Exiting") exit() except Exception as e: - logger.critical(f"System: script abort. Initalizing Interface2 {e}") + logger.critical(f"System: script abort. Initializing Interface2 {e}") exit() #Get the node number of the device, check if the device is connected