11# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework
22# All trademark and other rights reserved by their respective owners
3- # Copyright 2008-2022 Neongecko.com Inc.
3+ # Copyright 2008-2025 Neongecko.com Inc.
44# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds,
55# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo
66# BSD-3 License
2525# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
2626# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
2727# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
2829from enum import Enum
2930from threading import Thread
3031from time import time
3132
3233from lingua_franca .format import nice_duration
33- from ovos_bus_client import Message
34+ from ovos_bus_client . message import Message
3435from ovos_utils import classproperty
3536from ovos_utils .log import LOG
3637from ovos_utils .process_utils import RuntimeRequirements
37- # from ovos_workshop.skills.fallback import FallbackSkill
38- from neon_utils . skills . neon_fallback_skill import NeonFallbackSkill , NeonSkill
39- from neon_utils .message_utils import get_message_user
38+ from ovos_workshop .skills .fallback import FallbackSkill
39+ from ovos_workshop . decorators import intent_handler , fallback_handler
40+ from neon_utils .message_utils import get_message_user , dig_for_message
4041from neon_utils .user_utils import get_user_prefs
42+ from neon_utils .hana_utils import request_backend
4143from neon_mq_connector .utils .client_utils import send_mq_request
4244
43- from mycroft .skills .mycroft_skill .decorators import intent_handler
44-
4545
4646class LLM (Enum ):
4747 GPT = "Chat GPT"
4848 FASTCHAT = "FastChat"
4949
5050
51- class LLMSkill (NeonFallbackSkill ):
52- def __init__ (self , ** kwargs ):
53- NeonFallbackSkill .__init__ (self , ** kwargs )
51+ class LLMSkill (FallbackSkill ):
52+ def __init__ (self , * args , * *kwargs ):
53+ super () .__init__ (* args , ** kwargs )
5454 self .chat_history = dict ()
5555 self ._default_user = "local"
5656 self ._default_llm = LLM .FASTCHAT
5757 self .chatting = dict ()
58+ self .register_entity_file ("llm.entity" )
5859
5960 @classproperty
6061 def runtime_requirements (self ):
@@ -76,35 +77,36 @@ def chat_timeout_seconds(self):
7677 def fallback_enabled (self ):
7778 return self .settings .get ("fallback_enabled" , False )
7879
79- # TODO: Move to __init__ after ovos-workshop stable release
80- def initialize (self ):
81- self .register_entity_file ("llm.entity" )
82- # TODO: Resolve Padatious entity file handling bug
83- if self .fallback_enabled :
84- self .register_fallback (self .fallback_llm , 85 )
85-
80+ @fallback_handler (85 )
8681 def fallback_llm (self , message ):
82+ if not self .fallback_enabled :
83+ LOG .info ("LLM Fallback Disabled" )
84+ return False
8785 utterance = message .data ['utterance' ]
86+ LOG .info (f"Getting LLM response to: { utterance } " )
8887 user = get_message_user (message ) or self ._default_user
89- answer = self ._get_llm_response (utterance , user , self ._default_llm )
90- if not answer :
91- LOG .info (f"No fallback response" )
92- return False
93- self .speak (answer )
88+
89+ def _threaded_get_response (utt , usr ):
90+ answer = self ._get_llm_response (utt , usr , self ._default_llm )
91+ if not answer :
92+ LOG .info (f"No fallback response" )
93+ return
94+ self .speak (answer )
95+
96+ # TODO: Speak filler?
97+ Thread (target = _threaded_get_response , args = (utterance , user ), daemon = True ).start ()
9498 return True
9599
96100 @intent_handler ("enable_fallback.intent" )
97101 def handle_enable_fallback (self , message ):
98102 if not self .fallback_enabled :
99103 self .settings ['fallback_enabled' ] = True
100- self .register_fallback (self .fallback_llm , 85 )
101104 self .speak_dialog ("fallback_enabled" )
102105
103106 @intent_handler ("disable_fallback.intent" )
104107 def handle_disable_fallback (self , message ):
105108 if self .fallback_enabled :
106109 self .settings ['fallback_enabled' ] = False
107- self .remove_fallback (self .fallback_llm )
108110 self .speak_dialog ("fallback_disabled" )
109111
110112 @intent_handler ("ask_llm.intent" )
@@ -127,8 +129,7 @@ def handle_chat_with_llm(self, message):
127129 llm = self ._get_requested_llm (message )
128130 timeout_duration = nice_duration (self .chat_timeout_seconds )
129131 self .speak_dialog ("start_chat" , {"llm" : llm .value ,
130- "timeout" : timeout_duration },
131- private = True )
132+ "timeout" : timeout_duration })
132133 self ._reset_expiration (user , llm )
133134
134135 @intent_handler ("email_chat_history.intent" )
@@ -138,15 +139,15 @@ def handle_email_chat_history(self, message):
138139 email_addr = user_prefs ['email' ]
139140 if username not in self .chat_history :
140141 LOG .debug (f"No history for { username } " )
141- self .speak_dialog ("no_chat_history" , private = True )
142+ self .speak_dialog ("no_chat_history" )
142143 return
143144 if not email_addr :
144145 LOG .debug ("No email address" )
145146 # TODO: Capture Email address
146- self .speak_dialog ("no_email_address" , private = True )
147+ self .speak_dialog ("no_email_address" )
147148 return
148149 self .speak_dialog ("sending_chat_history" ,
149- {"email" : email_addr }, private = True )
150+ {"email" : email_addr })
150151 self ._send_email (username , email_addr )
151152
152153 def _send_email (self , username : str , email : str ):
@@ -155,8 +156,7 @@ def _send_email(self, username: str, email: str):
155156 for entry in history :
156157 formatted = entry [1 ].replace ('\n \n ' , '\n ' ).replace ('\n ' , '\n \t ...' )
157158 email_text += f"[{ entry [0 ]} ] { formatted } \n "
158- NeonSkill .send_email (self , "LLM Conversation" , email_text ,
159- email_addr = email )
159+ self .send_email ("LLM Conversation" , email_text , email_addr = email )
160160
161161 def _stop_chatting (self , message ):
162162 user = get_message_user (message ) or self ._default_user
@@ -174,16 +174,15 @@ def _get_llm_response(self, query: str, user: str, llm: LLM) -> str:
174174 :returns: Speakable response to the user's query
175175 """
176176 if llm == LLM .GPT :
177- queue = "chat_gpt_input "
177+ endpoint = "chatgpt "
178178 elif llm == LLM .FASTCHAT :
179- queue = "fastchat_input "
179+ endpoint = "fastchat "
180180 else :
181181 raise ValueError (f"Expected LLM, got: { llm } " )
182182 self .chat_history .setdefault (user , list ())
183- mq_resp = send_mq_request ("/llm" , {"query" : query ,
184- "history" : self .chat_history [user ]},
185- queue )
186- resp = mq_resp .get ("response" ) or ""
183+ resp = request_backend (f"/llm/{ endpoint } " , {"query" : query , "history" : self .chat_history [user ]})
184+
185+ resp = resp .get ("response" ) or ""
187186 if resp :
188187 username = "user" if user == self ._default_user else user
189188 self .chat_history [user ].append ((username , query ))
@@ -237,3 +236,38 @@ def _reset_expiration(self, user, llm):
237236 self .cancel_scheduled_event (event_name )
238237 self .schedule_event (self ._stop_chatting , self .chat_timeout_seconds ,
239238 {'user' : user }, event_name )
239+
240+ # TODO: copied from NeonSkill. This method should be moved to a standalone
241+ # utility
242+ def send_email (self , title , body , message = None , email_addr = None ,
243+ attachments = None ):
244+ """
245+ Send an email to the registered user's email.
246+ Method here for backwards compatibility with Mycroft skills.
247+ Email address priority: email_addr, user prefs from message,
248+ fallback to DeviceApi for Mycroft method
249+
250+ Arguments:
251+ title (str): Title of email
252+ body (str): HTML body of email. This supports
253+ simple HTML like bold and italics
254+ email_addr (str): Optional email address to send message to
255+ attachments (dict): Optional dict of file names to Base64 encoded files
256+ message (Message): Optional message to get email from
257+ """
258+ message = message or dig_for_message ()
259+ if not email_addr and message :
260+ email_addr = get_user_prefs (message )["user" ].get ("email" )
261+
262+ if email_addr and send_mq_request :
263+ LOG .info ("Send email via Neon Server" )
264+ request_data = {"recipient" : email_addr ,
265+ "subject" : title ,
266+ "body" : body ,
267+ "attachments" : attachments }
268+ data = send_mq_request ("/neon_emails" , request_data ,
269+ "neon_emails_input" )
270+ return data .get ("success" )
271+ else :
272+ LOG .warning ("Attempting to send email via Mycroft Backend" )
273+ super ().send_email (title , body )
0 commit comments