py-isaac 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- isaac/__init__.py +38 -0
- isaac/cli.py +4 -0
- isaac/command.py +336 -0
- isaac/constants.py +79 -0
- isaac/globals.py +8 -0
- isaac/lang_models.py +168 -0
- isaac/meta.py +3 -0
- isaac/settings.py +275 -0
- isaac/speech.py +41 -0
- isaac/sync.py +5 -0
- isaac/theme.py +7 -0
- isaac/types.py +38 -0
- isaac/utils.py +168 -0
- py_isaac-0.1.0.dist-info/METADATA +23 -0
- py_isaac-0.1.0.dist-info/RECORD +18 -0
- py_isaac-0.1.0.dist-info/WHEEL +5 -0
- py_isaac-0.1.0.dist-info/entry_points.txt +2 -0
- py_isaac-0.1.0.dist-info/top_level.txt +1 -0
isaac/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from yapper import PiperVoiceUS
|
|
5
|
+
|
|
6
|
+
import isaac.constants as c
|
|
7
|
+
|
|
8
|
+
if not os.path.isdir(c.APP_DIR):
|
|
9
|
+
os.mkdir(c.APP_DIR)
|
|
10
|
+
|
|
11
|
+
if not os.path.isfile(c.FILE_SETTINGS):
|
|
12
|
+
with open(c.FILE_SETTINGS, "w", encoding="utf-8") as f:
|
|
13
|
+
json.dump(
|
|
14
|
+
{
|
|
15
|
+
c.STNG_FLD_GROQ: {
|
|
16
|
+
c.STNG_FLD_KEY: None,
|
|
17
|
+
c.STNG_FLD_MODEL: None,
|
|
18
|
+
},
|
|
19
|
+
c.STNG_FLD_GEMINI: {
|
|
20
|
+
c.STNG_FLD_KEY: None,
|
|
21
|
+
c.STNG_FLD_MODEL: None,
|
|
22
|
+
},
|
|
23
|
+
c.STNG_FLD_SPEECH: {
|
|
24
|
+
c.STNG_FLD_IS_ENABLED: False,
|
|
25
|
+
c.STNG_FLD_PIPER_VOICE: PiperVoiceUS.HFC_FEMALE.value,
|
|
26
|
+
},
|
|
27
|
+
c.STNG_FLD_HEARING: {
|
|
28
|
+
c.STNG_FLD_IS_ENABLED: False,
|
|
29
|
+
c.STNG_FLD_WHISPER_SIZE: "auto",
|
|
30
|
+
},
|
|
31
|
+
c.STNG_FLD_RSPNS_GENERATOR: None,
|
|
32
|
+
c.STNG_FLD_SYS_MESSAGE: None,
|
|
33
|
+
c.STNG_FLD_CONTEXT_ENABLED: True,
|
|
34
|
+
c.STNG_FLD_SHELL: c.FILE_SHELL,
|
|
35
|
+
},
|
|
36
|
+
f,
|
|
37
|
+
indent=2,
|
|
38
|
+
)
|
isaac/cli.py
ADDED
isaac/command.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
try:
|
|
2
|
+
import readline
|
|
3
|
+
except ImportError:
|
|
4
|
+
import pyreadline3 as readline
|
|
5
|
+
import shlex
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import platform
|
|
9
|
+
import subprocess
|
|
10
|
+
import isaac.constants as c
|
|
11
|
+
import isaac.globals as glb
|
|
12
|
+
import psutil
|
|
13
|
+
import os
|
|
14
|
+
import isaac.lang_models as lang_models
|
|
15
|
+
import isaac.speech as speech
|
|
16
|
+
from isaac.settings import Settings
|
|
17
|
+
from isaac.theme import BOLD_BRIGHT, BRIGHT, RESET
|
|
18
|
+
from isaac.utils import clear, handle_lm_response, write, label_switch
|
|
19
|
+
from difflib import SequenceMatcher
|
|
20
|
+
|
|
21
|
+
CMD_SELECT = ":select"
|
|
22
|
+
CMD_TOGGLE = ":toggle"
|
|
23
|
+
CMD_KEY = ":key"
|
|
24
|
+
CMD_INSTRUCT = ":instruct"
|
|
25
|
+
CMD_STATUS = ":status"
|
|
26
|
+
CMD_MUTE = ":mute"
|
|
27
|
+
CMD_CMD = ":cmd"
|
|
28
|
+
CMD_COMMANDS = ":commands"
|
|
29
|
+
CMD_CLEAR = ":clear"
|
|
30
|
+
CMD_EXIT = ":exit"
|
|
31
|
+
|
|
32
|
+
SELECTABLE_LM_PROVIDER = "lm_provider"
|
|
33
|
+
SELECTABLE_VOICE = "voice"
|
|
34
|
+
SELECTABLE_LANG_MODEL = "lm"
|
|
35
|
+
SELECTABLE_WHISPER_MODEL = "whisper"
|
|
36
|
+
|
|
37
|
+
TOGGLABLE_SPEECH = "speech"
|
|
38
|
+
TOGGLABLE_HEARING = "hearing"
|
|
39
|
+
TOGGLABLE_CONTEXT = "context"
|
|
40
|
+
|
|
41
|
+
commands = {
|
|
42
|
+
CMD_SELECT,
|
|
43
|
+
CMD_TOGGLE,
|
|
44
|
+
CMD_KEY,
|
|
45
|
+
CMD_INSTRUCT,
|
|
46
|
+
CMD_STATUS,
|
|
47
|
+
CMD_MUTE,
|
|
48
|
+
CMD_CMD,
|
|
49
|
+
CMD_COMMANDS,
|
|
50
|
+
CMD_CLEAR,
|
|
51
|
+
CMD_EXIT,
|
|
52
|
+
}
|
|
53
|
+
selectables = [
|
|
54
|
+
SELECTABLE_LM_PROVIDER,
|
|
55
|
+
SELECTABLE_LANG_MODEL,
|
|
56
|
+
SELECTABLE_VOICE,
|
|
57
|
+
SELECTABLE_WHISPER_MODEL,
|
|
58
|
+
]
|
|
59
|
+
togglables = [TOGGLABLE_SPEECH, TOGGLABLE_HEARING, TOGGLABLE_CONTEXT]
|
|
60
|
+
command_args = {CMD_SELECT: selectables, CMD_TOGGLE: togglables}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def print_welcome():
|
|
64
|
+
"""Prints the welcome message on the screen."""
|
|
65
|
+
banner = """
|
|
66
|
+
%s ___ ____ _ _ ____
|
|
67
|
+
|_ _| / ___| / \ / \ / ___|
|
|
68
|
+
| | \___ \ / _ \ / _ \ | |
|
|
69
|
+
| | _ ___) | / ___ \ _ / ___ \ | |___
|
|
70
|
+
|___(_)____(_)_/ \_(_)_/ \_(_)____|%s"""
|
|
71
|
+
message = banner + " type %s%s%s to see commands.\n"
|
|
72
|
+
message = message % (BRIGHT, RESET, BOLD_BRIGHT, CMD_COMMANDS, RESET)
|
|
73
|
+
print(message)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_command(text: str) -> bool:
|
|
77
|
+
"""
|
|
78
|
+
prints if the given text is a command, i.e. if it starts with a colon(:).
|
|
79
|
+
"""
|
|
80
|
+
return text.startswith(":")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def command_exists(word: str) -> bool:
|
|
84
|
+
"""
|
|
85
|
+
checks if the given word is one of the valid commands defined in the file.
|
|
86
|
+
"""
|
|
87
|
+
return word in commands
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def handle_misspell(word: str) -> str:
|
|
91
|
+
"""prints the most similar command to word."""
|
|
92
|
+
|
|
93
|
+
# penalizes command matches that don't share the first letter
|
|
94
|
+
def key(c: str):
|
|
95
|
+
return SequenceMatcher(None, word, c).ratio() * (
|
|
96
|
+
0.8 if c[:2] != word[:2] else 1
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
similar = max(commands, key=key)
|
|
100
|
+
write(f"command not found, did you mean '{similar}'?")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def command_completer(text: str, state: int) -> Optional[str]:
|
|
104
|
+
"""handles auto-completion of commands and their arguments."""
|
|
105
|
+
text = readline.get_line_buffer().lstrip()
|
|
106
|
+
if not is_command(text):
|
|
107
|
+
return
|
|
108
|
+
words = shlex.split(text)
|
|
109
|
+
if len(words) > 2:
|
|
110
|
+
return
|
|
111
|
+
command = words[0]
|
|
112
|
+
if not command_exists(command):
|
|
113
|
+
if len(words) == 1:
|
|
114
|
+
options = [cmd for cmd in commands if cmd.startswith(command)]
|
|
115
|
+
return options[state] if state < len(options) else None
|
|
116
|
+
return None
|
|
117
|
+
if command not in command_args:
|
|
118
|
+
return
|
|
119
|
+
arg = words[1] if len(words) == 2 else ""
|
|
120
|
+
options = [option for option in command_args[command] if option.startswith(arg)]
|
|
121
|
+
return options[state] if state < len(options) else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def handle_select(args: list[str]):
|
|
125
|
+
"""handles the ':select' command."""
|
|
126
|
+
if len(args) > 1:
|
|
127
|
+
write(":select only takes one argument")
|
|
128
|
+
return
|
|
129
|
+
elif len(args) == 0:
|
|
130
|
+
write(":select needs an argument")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
arg = args[0]
|
|
134
|
+
if arg not in selectables:
|
|
135
|
+
write(f"invalid argument {arg}, must be one of {selectables}")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if arg == SELECTABLE_LM_PROVIDER:
|
|
139
|
+
glb.settings.select_lm_provider()
|
|
140
|
+
elif arg == SELECTABLE_LANG_MODEL:
|
|
141
|
+
glb.settings.select_lm()
|
|
142
|
+
elif arg == SELECTABLE_VOICE:
|
|
143
|
+
glb.settings.select_voice()
|
|
144
|
+
else:
|
|
145
|
+
glb.settings.select_whisper_size()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def handle_toggle(args: list[str]):
|
|
149
|
+
"""handles the ':toggle' command."""
|
|
150
|
+
if len(args) > 1:
|
|
151
|
+
write(":toggle only takes one argument")
|
|
152
|
+
return
|
|
153
|
+
elif len(args) == 0:
|
|
154
|
+
write(":toggle needs an argument")
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
arg = args[0]
|
|
158
|
+
if arg not in togglables:
|
|
159
|
+
write(f"invalid argument {arg}, argument must be one of {togglables}")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
if arg == TOGGLABLE_SPEECH:
|
|
163
|
+
glb.settings.toggle_speech()
|
|
164
|
+
elif arg == TOGGLABLE_CONTEXT:
|
|
165
|
+
glb.settings.toggle_context()
|
|
166
|
+
else:
|
|
167
|
+
glb.settings.toggle_hearing()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def set_sys_msg(args: list[str]):
|
|
171
|
+
"""sets the system message if it is valid."""
|
|
172
|
+
if len(args) == 0:
|
|
173
|
+
write(f"{CMD_INSTRUCT} needs an argument")
|
|
174
|
+
return
|
|
175
|
+
if len(args) > 1:
|
|
176
|
+
write(f"{CMD_INSTRUCT} only takes one argument")
|
|
177
|
+
return
|
|
178
|
+
message = args[0]
|
|
179
|
+
if len(message.strip()) == 0:
|
|
180
|
+
write("instruction must not be empty")
|
|
181
|
+
return
|
|
182
|
+
glb.settings.set_sys_msg(message)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def handle_cmd():
|
|
186
|
+
"""handles the `:cmd` command, i.e. launches the appropriate shell."""
|
|
187
|
+
try:
|
|
188
|
+
if glb.listener is not None:
|
|
189
|
+
glb.listener.pause()
|
|
190
|
+
if platform.system() == "Windows":
|
|
191
|
+
subprocess.run(["powershell.exe"])
|
|
192
|
+
else:
|
|
193
|
+
subprocess.run(["/bin/sh"])
|
|
194
|
+
finally:
|
|
195
|
+
if glb.listener is not None:
|
|
196
|
+
glb.listener.resume()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def display_status():
|
|
200
|
+
"""Displays the user's preferences, token cost, and memory consumption."""
|
|
201
|
+
settings = glb.settings
|
|
202
|
+
lang_model = (
|
|
203
|
+
glb.settings.groq_model
|
|
204
|
+
if settings.response_generator == c.RSPNS_GNRTR_GROQ
|
|
205
|
+
else glb.settings.gemini_model
|
|
206
|
+
)
|
|
207
|
+
total_mem = psutil.virtual_memory().total
|
|
208
|
+
parent = psutil.Process(os.getpid())
|
|
209
|
+
consumed_mem = parent.memory_info().rss
|
|
210
|
+
for child in parent.children(recursive=True):
|
|
211
|
+
consumed_mem += child.memory_info().rss
|
|
212
|
+
mem_per = consumed_mem / total_mem * 100
|
|
213
|
+
|
|
214
|
+
lines = [
|
|
215
|
+
"%slanguage model:%s" % (BOLD_BRIGHT, RESET),
|
|
216
|
+
" %sprovider%s: %s" % (BRIGHT, RESET, settings.response_generator),
|
|
217
|
+
" %smodel:%s %s" % (BRIGHT, RESET, lang_model),
|
|
218
|
+
" %sinstruction:%s %s" % (BRIGHT, RESET, settings.system_message or "null"),
|
|
219
|
+
" %scontext:%s %s" % (BRIGHT, RESET, label_switch(settings.context_enabled)),
|
|
220
|
+
"%sspeech:%s" % (BOLD_BRIGHT, RESET),
|
|
221
|
+
" %sstatus:%s %s" % (BRIGHT, RESET, label_switch(settings.speech_enabled)),
|
|
222
|
+
" %svoice:%s %s" % (BRIGHT, RESET, glb.settings.piper_voice),
|
|
223
|
+
"%shearing:%s" % (BOLD_BRIGHT, RESET),
|
|
224
|
+
" %sstatus:%s %s" % (BRIGHT, RESET, label_switch(settings.hearing_enabled)),
|
|
225
|
+
" %smodel:%s %s" % (BRIGHT, RESET, settings.whisper_size or "null"),
|
|
226
|
+
"%sconsumption:%s" % (BOLD_BRIGHT, RESET),
|
|
227
|
+
" %sprompt tokens:%s %s" % (BRIGHT, RESET, settings.prompt_tokens),
|
|
228
|
+
" %scompletion tokens:%s %s" % (BRIGHT, RESET, settings.completion_tokens),
|
|
229
|
+
" %smemory:%s %.2f%%" % (BRIGHT, RESET, mem_per),
|
|
230
|
+
]
|
|
231
|
+
write("\n".join(lines))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def display_commands():
|
|
235
|
+
"""Displays the available commands."""
|
|
236
|
+
lines = [
|
|
237
|
+
"%s%s%s to turn features on or off" % (BOLD_BRIGHT, CMD_TOGGLE, RESET),
|
|
238
|
+
" %s%s %s%s to toggle the assistant's speech"
|
|
239
|
+
% (BRIGHT, CMD_TOGGLE, TOGGLABLE_SPEECH, RESET),
|
|
240
|
+
(
|
|
241
|
+
" %s%s %s%s to toggle the use of conversation"
|
|
242
|
+
" history for coherent responses"
|
|
243
|
+
)
|
|
244
|
+
% (BRIGHT, CMD_TOGGLE, TOGGLABLE_CONTEXT, RESET),
|
|
245
|
+
" %s%s %s%s to toggle the assistant's ability to hear you"
|
|
246
|
+
% (BRIGHT, CMD_TOGGLE, TOGGLABLE_HEARING, RESET),
|
|
247
|
+
"%s%s%s for selecting from available models and voices"
|
|
248
|
+
% (BOLD_BRIGHT, CMD_SELECT, RESET),
|
|
249
|
+
" %s%s %s%s to select the language model provider"
|
|
250
|
+
% (BRIGHT, CMD_SELECT, SELECTABLE_LM_PROVIDER, RESET),
|
|
251
|
+
" %s%s %s%s to select the model for generating responses"
|
|
252
|
+
% (BRIGHT, CMD_SELECT, SELECTABLE_LANG_MODEL, RESET),
|
|
253
|
+
" %s%s %s%s to select the assistant's voice"
|
|
254
|
+
% (BRIGHT, CMD_SELECT, SELECTABLE_VOICE, RESET),
|
|
255
|
+
" %s%s %s%s to select the model interpreting speech"
|
|
256
|
+
% (BRIGHT, CMD_SELECT, SELECTABLE_WHISPER_MODEL, RESET),
|
|
257
|
+
"%s%s%s to set the LLM API key for the selected provider"
|
|
258
|
+
% (BOLD_BRIGHT, CMD_KEY, RESET),
|
|
259
|
+
"%s%s%s to instruct the model to behave a certain way"
|
|
260
|
+
% (BOLD_BRIGHT, CMD_INSTRUCT, RESET),
|
|
261
|
+
"%s%s%s to display status and settings" % (BOLD_BRIGHT, CMD_STATUS, RESET),
|
|
262
|
+
"%s%s%s to mute the assistant" % (BOLD_BRIGHT, CMD_MUTE, RESET),
|
|
263
|
+
"%s%s%s to launch a shell session" % (BOLD_BRIGHT, CMD_CMD, RESET),
|
|
264
|
+
"%s%s%s to print this help message" % (BOLD_BRIGHT, CMD_COMMANDS, RESET),
|
|
265
|
+
"%s%s%s for clearing the console" % (BOLD_BRIGHT, CMD_CLEAR, RESET),
|
|
266
|
+
"%s%s%s to exit" % (BOLD_BRIGHT, CMD_EXIT, RESET),
|
|
267
|
+
]
|
|
268
|
+
write("\n".join(lines))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def handle_command(words: list[str]):
|
|
272
|
+
"""handles words as command."""
|
|
273
|
+
command = words[0]
|
|
274
|
+
|
|
275
|
+
if command == CMD_SELECT:
|
|
276
|
+
handle_select(words[1:])
|
|
277
|
+
elif command == CMD_TOGGLE:
|
|
278
|
+
handle_toggle(words[1:])
|
|
279
|
+
elif command == CMD_KEY:
|
|
280
|
+
glb.settings.set_key()
|
|
281
|
+
elif command == CMD_INSTRUCT:
|
|
282
|
+
set_sys_msg(words[1:])
|
|
283
|
+
elif command == CMD_STATUS:
|
|
284
|
+
display_status()
|
|
285
|
+
elif command == CMD_MUTE:
|
|
286
|
+
speech.mute()
|
|
287
|
+
elif command == CMD_CMD:
|
|
288
|
+
handle_cmd()
|
|
289
|
+
elif command == CMD_COMMANDS:
|
|
290
|
+
display_commands()
|
|
291
|
+
elif command == CMD_CLEAR:
|
|
292
|
+
clear()
|
|
293
|
+
elif command == CMD_EXIT:
|
|
294
|
+
glb.settings.dump_to_cache()
|
|
295
|
+
if glb.listener is not None:
|
|
296
|
+
glb.settings.disable_hearing()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def get_proc_count():
|
|
300
|
+
import psutil
|
|
301
|
+
|
|
302
|
+
return sum(1 for _ in psutil.Process(os.getpid()).children(recursive=True)) + 1
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def run_loop():
|
|
306
|
+
"""starts the REPL loop."""
|
|
307
|
+
readline.set_completer(command_completer)
|
|
308
|
+
readline.parse_and_bind("tab: complete")
|
|
309
|
+
|
|
310
|
+
glb.settings = Settings()
|
|
311
|
+
glb.settings.enact()
|
|
312
|
+
clear()
|
|
313
|
+
print_welcome()
|
|
314
|
+
|
|
315
|
+
while True:
|
|
316
|
+
try:
|
|
317
|
+
query = input(">> ")
|
|
318
|
+
if len(query.strip()) == 0:
|
|
319
|
+
continue
|
|
320
|
+
if is_command(query):
|
|
321
|
+
try:
|
|
322
|
+
words = shlex.split(query)
|
|
323
|
+
if command_exists(words[0]):
|
|
324
|
+
handle_command(words)
|
|
325
|
+
if words[0] == CMD_EXIT:
|
|
326
|
+
break
|
|
327
|
+
elif words[0] == ":procs":
|
|
328
|
+
print(get_proc_count())
|
|
329
|
+
else:
|
|
330
|
+
handle_misspell(words[0])
|
|
331
|
+
except ValueError:
|
|
332
|
+
write("invalid command")
|
|
333
|
+
else:
|
|
334
|
+
handle_lm_response(lang_models.ask(query))
|
|
335
|
+
except KeyboardInterrupt:
|
|
336
|
+
write("\r", end="")
|
isaac/constants.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import os.path as path
|
|
4
|
+
|
|
5
|
+
import isaac.meta as meta
|
|
6
|
+
|
|
7
|
+
# Gemini API fields
|
|
8
|
+
GMNI_FLD_SYS_INST = "system_instruction"
|
|
9
|
+
GMNI_FLD_CONTENT = "content"
|
|
10
|
+
GMNI_FLD_ROLE = "role"
|
|
11
|
+
GMNI_FLD_PARTS = "parts"
|
|
12
|
+
GMNI_FLD_CONTENTS = "contents"
|
|
13
|
+
GMNI_FLD_CANDIDATES = "candidates"
|
|
14
|
+
GMNI_FLD_TEXT = "text"
|
|
15
|
+
GMNI_FLD_USAGE = "usageMetadata"
|
|
16
|
+
GMNI_FLD_ERROR = "error"
|
|
17
|
+
|
|
18
|
+
GMNI_USG_PROMPT = "promptTokenCount"
|
|
19
|
+
GMNI_USG_COMPLETION = "candidatesTokenCount"
|
|
20
|
+
|
|
21
|
+
GMNI_ROLE_USER = "user"
|
|
22
|
+
GMNI_ROLE_MODEL = "model"
|
|
23
|
+
|
|
24
|
+
# Groq API fields
|
|
25
|
+
GROQ_FLD_ROLE = "role"
|
|
26
|
+
GROQ_FLD_CHOICES = "choices"
|
|
27
|
+
GROQ_FLD_MESSAGE = "message"
|
|
28
|
+
GROQ_FLD_CONTENT = "content"
|
|
29
|
+
GROQ_FLD_MESSAGES = "messages"
|
|
30
|
+
GROQ_FLD_MODEL = "model"
|
|
31
|
+
GROQ_FLD_USAGE = "usage"
|
|
32
|
+
GROQ_FLD_ERROR = "error"
|
|
33
|
+
GROQ_FLD_ERROR_CODE = "code"
|
|
34
|
+
|
|
35
|
+
GROQ_USG_PROMPT = "prompt_tokens"
|
|
36
|
+
GROQ_USG_COMPLETION = "completion_tokens"
|
|
37
|
+
|
|
38
|
+
GROQ_ROLE_SYSTEM = "system"
|
|
39
|
+
GROQ_ROLE_USER = "user"
|
|
40
|
+
GROQ_ROLE_ASSISTANT = "assistant"
|
|
41
|
+
|
|
42
|
+
# Settings fields
|
|
43
|
+
STNG_FLD_GROQ = "groq"
|
|
44
|
+
STNG_FLD_GEMINI = "gemini"
|
|
45
|
+
STNG_FLD_KEY = "key"
|
|
46
|
+
STNG_FLD_MODEL = "model"
|
|
47
|
+
STNG_FLD_HEARING = "hearing"
|
|
48
|
+
STNG_FLD_WHISPER_SIZE = "whisperSize"
|
|
49
|
+
STNG_FLD_SPEECH = "speech"
|
|
50
|
+
STNG_FLD_PIPER_VOICE = "piperVoice"
|
|
51
|
+
STNG_FLD_IS_ENABLED = "isEnabled"
|
|
52
|
+
STNG_FLD_RSPNS_GENERATOR = "responseGenerator"
|
|
53
|
+
STNG_FLD_SYS_MESSAGE = "systemMessage"
|
|
54
|
+
STNG_FLD_CONTEXT_ENABLED = "contextEnabled"
|
|
55
|
+
STNG_FLD_SHELL = "shell"
|
|
56
|
+
|
|
57
|
+
# Response generator APIs
|
|
58
|
+
RSPNS_GNRTR_GROQ = "groq"
|
|
59
|
+
RSPNS_GNRTR_GEMINI = "gemini"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if os.name == "nt":
|
|
63
|
+
APP_DIR = path.join(os.getenv("APPDATA"), meta.name)
|
|
64
|
+
elif os.name == "posix":
|
|
65
|
+
home = path.expanduser("~")
|
|
66
|
+
if os.uname().sysname == "Darwin":
|
|
67
|
+
APP_DIR = path.join(home, "Library/Application Support", meta.name)
|
|
68
|
+
else:
|
|
69
|
+
APP_DIR = path.join(home, ".config", meta.name)
|
|
70
|
+
else:
|
|
71
|
+
print("your system is not supported")
|
|
72
|
+
sys.exit()
|
|
73
|
+
|
|
74
|
+
# File names
|
|
75
|
+
FILE_SETTINGS = path.join(APP_DIR, "settings.json")
|
|
76
|
+
FILE_SHELL = "powershell.exe" if os.name == "nt" else "/bin/sh"
|
|
77
|
+
|
|
78
|
+
# messages
|
|
79
|
+
MSG_LANG_MODEL_ERROR = "could not process that query, something went wrong"
|
isaac/globals.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from isaac.types import SettingsInterface, SpeakerInteface, ListenerInterface
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
settings: Optional[SettingsInterface] = None
|
|
6
|
+
speaker: Optional[SpeakerInteface] = None
|
|
7
|
+
listener: Optional[ListenerInterface] = None
|
|
8
|
+
past_exchanges = []
|
isaac/lang_models.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from typing import List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
import isaac.constants as c
|
|
6
|
+
import isaac.globals as glb
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def append_history(query: str, answer: str):
|
|
12
|
+
glb.past_exchanges.append((query, answer))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ask_gemini(
|
|
16
|
+
model: str,
|
|
17
|
+
api_key: str,
|
|
18
|
+
query: str,
|
|
19
|
+
sys_msg: Optional[str] = None,
|
|
20
|
+
past_exchanges: Optional[List[Tuple[str, str]]] = None,
|
|
21
|
+
) -> str:
|
|
22
|
+
"""
|
|
23
|
+
queries the selected `Gemini` model and returns its repsonse as a string.
|
|
24
|
+
"""
|
|
25
|
+
base = "https://generativelanguage.googleapis.com/v1beta/models"
|
|
26
|
+
url = f"{base}/{model}:generateContent?key={api_key}"
|
|
27
|
+
data = {}
|
|
28
|
+
if sys_msg is not None:
|
|
29
|
+
data[c.GMNI_FLD_SYS_INST] = {c.GMNI_FLD_PARTS: {c.GMNI_FLD_TEXT: sys_msg}}
|
|
30
|
+
contents = []
|
|
31
|
+
if past_exchanges is not None:
|
|
32
|
+
for exchange in past_exchanges:
|
|
33
|
+
contents.append(
|
|
34
|
+
{
|
|
35
|
+
c.GMNI_FLD_ROLE: c.GMNI_ROLE_USER,
|
|
36
|
+
c.GMNI_FLD_PARTS: [{c.GMNI_FLD_TEXT: exchange[0]}],
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
contents.append(
|
|
40
|
+
{
|
|
41
|
+
c.GMNI_FLD_ROLE: c.GMNI_ROLE_MODEL,
|
|
42
|
+
c.GMNI_FLD_PARTS: [{c.GMNI_FLD_TEXT: exchange[1]}],
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
contents.append(
|
|
46
|
+
{
|
|
47
|
+
c.GMNI_FLD_ROLE: c.GMNI_ROLE_USER,
|
|
48
|
+
c.GMNI_FLD_PARTS: [{c.GMNI_FLD_TEXT: query}],
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
data[c.GMNI_FLD_CONTENTS] = contents
|
|
52
|
+
response = requests.post(url, json=data).json()
|
|
53
|
+
if c.GMNI_FLD_ERROR in response:
|
|
54
|
+
return c.MSG_LANG_MODEL_ERROR
|
|
55
|
+
update_token_cost(
|
|
56
|
+
response[c.GMNI_FLD_USAGE][c.GMNI_USG_PROMPT],
|
|
57
|
+
response[c.GMNI_FLD_USAGE][c.GMNI_USG_COMPLETION],
|
|
58
|
+
)
|
|
59
|
+
content = response[c.GMNI_FLD_CANDIDATES][0][c.GMNI_FLD_CONTENT]
|
|
60
|
+
return content[c.GMNI_FLD_PARTS][0][c.GMNI_FLD_TEXT]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def ask_groq(
|
|
64
|
+
model: str,
|
|
65
|
+
api_key: str,
|
|
66
|
+
query: str,
|
|
67
|
+
sys_msg: Optional[str] = None,
|
|
68
|
+
past_exhanges: Optional[List[Tuple[str, str]]] = None,
|
|
69
|
+
) -> str:
|
|
70
|
+
"""
|
|
71
|
+
queries the selected `Groq` model and returns its repsonse as a string.
|
|
72
|
+
"""
|
|
73
|
+
url = "https://api.groq.com/openai/v1/chat/completions"
|
|
74
|
+
headers = {
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
"Authorization": f"Bearer {api_key}",
|
|
77
|
+
"User-Agent": "curl/7.68.0",
|
|
78
|
+
}
|
|
79
|
+
messages = []
|
|
80
|
+
if sys_msg is not None:
|
|
81
|
+
messages.append(
|
|
82
|
+
{c.GROQ_FLD_ROLE: c.GROQ_ROLE_SYSTEM, c.GROQ_FLD_CONTENT: sys_msg}
|
|
83
|
+
)
|
|
84
|
+
if past_exhanges is not None:
|
|
85
|
+
for exchange in past_exhanges:
|
|
86
|
+
messages.append(
|
|
87
|
+
{
|
|
88
|
+
c.GROQ_FLD_ROLE: c.GROQ_ROLE_USER,
|
|
89
|
+
c.GROQ_FLD_CONTENT: exchange[0],
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
messages.append(
|
|
93
|
+
{
|
|
94
|
+
c.GROQ_FLD_ROLE: c.GROQ_ROLE_ASSISTANT,
|
|
95
|
+
c.GROQ_FLD_CONTENT: exchange[1],
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
messages.append({c.GROQ_FLD_ROLE: c.GROQ_ROLE_USER, c.GROQ_FLD_CONTENT: query})
|
|
99
|
+
data = {c.GROQ_FLD_MESSAGES: messages, c.GROQ_FLD_MODEL: model}
|
|
100
|
+
response = requests.post(url, json=data, headers=headers).json()
|
|
101
|
+
if c.GROQ_FLD_ERROR in response:
|
|
102
|
+
if response[c.GROQ_FLD_ERROR].get(c.GROQ_FLD_ERROR_CODE) == "model_decommissioned":
|
|
103
|
+
return (f"the model '{model}' has been decommissioned"
|
|
104
|
+
", please use another Groq model.")
|
|
105
|
+
return c.MSG_LANG_MODEL_ERROR
|
|
106
|
+
update_token_cost(
|
|
107
|
+
response[c.GROQ_FLD_USAGE][c.GROQ_USG_PROMPT],
|
|
108
|
+
response[c.GROQ_FLD_USAGE][c.GROQ_USG_COMPLETION],
|
|
109
|
+
)
|
|
110
|
+
return response[c.GROQ_FLD_CHOICES][0][c.GROQ_FLD_MESSAGE][c.GROQ_FLD_CONTENT]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def ask(query: str) -> Tuple[bool, str]:
|
|
114
|
+
"""
|
|
115
|
+
queries the selected language model and returns response as a string,
|
|
116
|
+
sends chat context if context is enabled.
|
|
117
|
+
"""
|
|
118
|
+
if glb.settings.response_generator is None:
|
|
119
|
+
glb.settings.select_lm_provider()
|
|
120
|
+
if glb.settings.lang_model is None:
|
|
121
|
+
glb.settings.select_lm()
|
|
122
|
+
|
|
123
|
+
exchanges = glb.past_exchanges if glb.settings.context_enabled else None
|
|
124
|
+
if glb.settings.response_generator == c.RSPNS_GNRTR_GEMINI:
|
|
125
|
+
response = ask_gemini(
|
|
126
|
+
glb.settings.gemini_model,
|
|
127
|
+
glb.settings.gemini_key,
|
|
128
|
+
query,
|
|
129
|
+
glb.settings.system_message,
|
|
130
|
+
exchanges,
|
|
131
|
+
)
|
|
132
|
+
else:
|
|
133
|
+
response = ask_groq(
|
|
134
|
+
glb.settings.groq_model,
|
|
135
|
+
glb.settings.groq_key,
|
|
136
|
+
query,
|
|
137
|
+
glb.settings.system_message,
|
|
138
|
+
exchanges,
|
|
139
|
+
)
|
|
140
|
+
post_ask(query, response)
|
|
141
|
+
return response
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def post_ask(query: str, answer: str):
|
|
145
|
+
"""
|
|
146
|
+
stores query-response exchange to send as context with later queries,
|
|
147
|
+
if the response contains code, only keeps code as response, if response
|
|
148
|
+
contains plain text, response is truncated.
|
|
149
|
+
"""
|
|
150
|
+
max_answer_words = 30
|
|
151
|
+
max_exchanges = 5
|
|
152
|
+
code_blocks = re.findall(r"```.*?```", answer, re.DOTALL)
|
|
153
|
+
|
|
154
|
+
if len(code_blocks) > 0:
|
|
155
|
+
answer = "\n\n".join(code_blocks)
|
|
156
|
+
else:
|
|
157
|
+
parts = answer.split()
|
|
158
|
+
if len(parts) > max_answer_words:
|
|
159
|
+
answer = " ".join(parts[:max_answer_words]) + "..."
|
|
160
|
+
glb.past_exchanges.append((query, answer))
|
|
161
|
+
if len(glb.past_exchanges) > max_exchanges:
|
|
162
|
+
glb.past_exchanges = glb.past_exchanges[-max_exchanges:]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def update_token_cost(prompt: int, completion: int):
|
|
166
|
+
"""updates the token cost to keep track of token expenditure."""
|
|
167
|
+
glb.settings.prompt_tokens += prompt
|
|
168
|
+
glb.settings.completion_tokens += completion
|
isaac/meta.py
ADDED
isaac/settings.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from yapper import GeminiModel, GroqModel, PiperVoiceUK, PiperVoiceUS
|
|
5
|
+
|
|
6
|
+
import isaac.constants as c
|
|
7
|
+
from isaac.utils import (
|
|
8
|
+
select_from,
|
|
9
|
+
write,
|
|
10
|
+
get_piper_voice_enum,
|
|
11
|
+
handle_lm_response,
|
|
12
|
+
safe_input,
|
|
13
|
+
)
|
|
14
|
+
from isaac.types import SettingsInterface
|
|
15
|
+
import isaac.speech as speech
|
|
16
|
+
from yapper import PiperSpeaker
|
|
17
|
+
import isaac.globals as glb
|
|
18
|
+
import isaac.lang_models as lm
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
whisper_options = [
|
|
22
|
+
"tiny",
|
|
23
|
+
"tiny.en",
|
|
24
|
+
"base",
|
|
25
|
+
"base.en",
|
|
26
|
+
"small",
|
|
27
|
+
"small.en",
|
|
28
|
+
"medium",
|
|
29
|
+
"medium",
|
|
30
|
+
"turbo",
|
|
31
|
+
"large",
|
|
32
|
+
"auto",
|
|
33
|
+
]
|
|
34
|
+
whisper_option_details = [
|
|
35
|
+
"~1GB",
|
|
36
|
+
"~1GB, english only",
|
|
37
|
+
"~1GB",
|
|
38
|
+
"~1GB, english only",
|
|
39
|
+
"~2GB",
|
|
40
|
+
"~2GB, english only",
|
|
41
|
+
"~5GB",
|
|
42
|
+
"~5GB, english only",
|
|
43
|
+
"~6GB",
|
|
44
|
+
"~10GB",
|
|
45
|
+
"select based on resource availability",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Settings(SettingsInterface):
|
|
50
|
+
"""
|
|
51
|
+
stores the user's preferences, loads settings from the default settings
|
|
52
|
+
file on initialization and can dump back to the same file.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self):
|
|
56
|
+
cache = json.load(open(c.FILE_SETTINGS, encoding="utf-8"))
|
|
57
|
+
self.groq_key = cache[c.STNG_FLD_GROQ][c.STNG_FLD_KEY]
|
|
58
|
+
self.groq_model = cache[c.STNG_FLD_GROQ][c.STNG_FLD_MODEL]
|
|
59
|
+
self.gemini_key = cache[c.STNG_FLD_GEMINI][c.STNG_FLD_KEY]
|
|
60
|
+
self.gemini_model = cache[c.STNG_FLD_GEMINI][c.STNG_FLD_MODEL]
|
|
61
|
+
self.hearing_enabled = cache[c.STNG_FLD_HEARING][c.STNG_FLD_IS_ENABLED]
|
|
62
|
+
self.whisper_size = cache[c.STNG_FLD_HEARING][c.STNG_FLD_WHISPER_SIZE]
|
|
63
|
+
self.speech_enabled = cache[c.STNG_FLD_SPEECH][c.STNG_FLD_IS_ENABLED]
|
|
64
|
+
self.piper_voice = cache[c.STNG_FLD_SPEECH][c.STNG_FLD_PIPER_VOICE]
|
|
65
|
+
self.response_generator = cache[c.STNG_FLD_RSPNS_GENERATOR]
|
|
66
|
+
self.system_message = cache[c.STNG_FLD_SYS_MESSAGE]
|
|
67
|
+
self.context_enabled = cache[c.STNG_FLD_CONTEXT_ENABLED]
|
|
68
|
+
self.shell = cache[c.STNG_FLD_SHELL]
|
|
69
|
+
self.prompt_tokens = 0
|
|
70
|
+
self.completion_tokens = 0
|
|
71
|
+
if self.response_generator is None:
|
|
72
|
+
self.select_lm_provider()
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def lang_model(self):
|
|
76
|
+
if self.response_generator == c.RSPNS_GNRTR_GEMINI:
|
|
77
|
+
return self.gemini_model
|
|
78
|
+
elif self.response_generator == c.RSPNS_GNRTR_GROQ:
|
|
79
|
+
return self.groq_model
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
def dump_to_cache(self):
|
|
83
|
+
"""dumps settings to the default settings file."""
|
|
84
|
+
cache = {}
|
|
85
|
+
cache[c.STNG_FLD_GROQ] = {
|
|
86
|
+
c.STNG_FLD_KEY: self.groq_key,
|
|
87
|
+
c.STNG_FLD_MODEL: self.groq_model,
|
|
88
|
+
}
|
|
89
|
+
cache[c.STNG_FLD_GEMINI] = {
|
|
90
|
+
c.STNG_FLD_KEY: self.gemini_key,
|
|
91
|
+
c.STNG_FLD_MODEL: self.gemini_model,
|
|
92
|
+
}
|
|
93
|
+
cache[c.STNG_FLD_SPEECH] = {
|
|
94
|
+
c.STNG_FLD_IS_ENABLED: self.speech_enabled,
|
|
95
|
+
c.STNG_FLD_PIPER_VOICE: self.piper_voice,
|
|
96
|
+
}
|
|
97
|
+
cache[c.STNG_FLD_HEARING] = {
|
|
98
|
+
c.STNG_FLD_IS_ENABLED: self.hearing_enabled,
|
|
99
|
+
c.STNG_FLD_WHISPER_SIZE: self.whisper_size,
|
|
100
|
+
}
|
|
101
|
+
cache[c.STNG_FLD_RSPNS_GENERATOR] = self.response_generator
|
|
102
|
+
cache[c.STNG_FLD_SYS_MESSAGE] = self.system_message
|
|
103
|
+
cache[c.STNG_FLD_CONTEXT_ENABLED] = self.context_enabled
|
|
104
|
+
cache[c.STNG_FLD_SHELL] = self.shell
|
|
105
|
+
json.dump(cache, open(c.FILE_SETTINGS, "w", encoding="utf-8"), indent=2)
|
|
106
|
+
|
|
107
|
+
def select_lm_provider(self):
|
|
108
|
+
"""
|
|
109
|
+
lets the user select a language model provider,
|
|
110
|
+
currently from `Groq` and `Gemini`.
|
|
111
|
+
"""
|
|
112
|
+
options = [c.RSPNS_GNRTR_GEMINI, c.RSPNS_GNRTR_GROQ]
|
|
113
|
+
idx = select_from(
|
|
114
|
+
options, prompt="select an LLM API provider", allow_none=False
|
|
115
|
+
)
|
|
116
|
+
if idx != -1:
|
|
117
|
+
self.response_generator = options[idx]
|
|
118
|
+
if self.lang_model is None:
|
|
119
|
+
self.select_lm()
|
|
120
|
+
|
|
121
|
+
def select_lm(self):
|
|
122
|
+
"""
|
|
123
|
+
lets the user select a language model to be used for answering
|
|
124
|
+
queries.
|
|
125
|
+
"""
|
|
126
|
+
if self.response_generator is None:
|
|
127
|
+
self.select_lm_provider()
|
|
128
|
+
provider = self.response_generator
|
|
129
|
+
if provider is None:
|
|
130
|
+
return
|
|
131
|
+
options = (
|
|
132
|
+
[model.value for model in GroqModel]
|
|
133
|
+
if self.response_generator == c.RSPNS_GNRTR_GROQ
|
|
134
|
+
else list(GeminiModel)
|
|
135
|
+
)
|
|
136
|
+
idx = select_from(
|
|
137
|
+
options, prompt="please select a language model", allow_none=False
|
|
138
|
+
)
|
|
139
|
+
if idx == -1:
|
|
140
|
+
return
|
|
141
|
+
if provider == c.RSPNS_GNRTR_GEMINI:
|
|
142
|
+
self.gemini_model = options[idx]
|
|
143
|
+
else:
|
|
144
|
+
self.groq_model = options[idx]
|
|
145
|
+
|
|
146
|
+
if (
|
|
147
|
+
self.response_generator == c.RSPNS_GNRTR_GROQ
|
|
148
|
+
and self.groq_key is None
|
|
149
|
+
or self.response_generator == c.RSPNS_GNRTR_GEMINI
|
|
150
|
+
and self.gemini_key is None
|
|
151
|
+
):
|
|
152
|
+
self.set_key()
|
|
153
|
+
|
|
154
|
+
def set_key(self):
|
|
155
|
+
"""sets the key for the currently selected language model provider."""
|
|
156
|
+
if self.response_generator is None:
|
|
157
|
+
self.select_lm()
|
|
158
|
+
key = safe_input(f"please enter your {self.response_generator} key: ").strip()
|
|
159
|
+
if self.response_generator == c.RSPNS_GNRTR_GROQ:
|
|
160
|
+
self.groq_key = key
|
|
161
|
+
else:
|
|
162
|
+
self.gemini_key = key
|
|
163
|
+
write()
|
|
164
|
+
|
|
165
|
+
def set_sys_msg(self, message: str):
|
|
166
|
+
"""
|
|
167
|
+
sets the system message to be used for querying the language
|
|
168
|
+
model.
|
|
169
|
+
"""
|
|
170
|
+
self.system_message = message
|
|
171
|
+
|
|
172
|
+
def toggle_context(self):
|
|
173
|
+
"""
|
|
174
|
+
toggles the use of context to help the model come up with coherent
|
|
175
|
+
responses.
|
|
176
|
+
"""
|
|
177
|
+
self.context_enabled = not self.context_enabled
|
|
178
|
+
|
|
179
|
+
def enable_speech(self):
|
|
180
|
+
"""
|
|
181
|
+
enables speech for the assistant so it can both prints and speaks its
|
|
182
|
+
response.
|
|
183
|
+
"""
|
|
184
|
+
glb.speaker = PiperSpeaker(
|
|
185
|
+
get_piper_voice_enum(self.piper_voice), show_progress=False
|
|
186
|
+
)
|
|
187
|
+
self.speech_enabled = True
|
|
188
|
+
|
|
189
|
+
def disable_speech(self):
|
|
190
|
+
"""disables speech for the assistant."""
|
|
191
|
+
glb.speaker = None
|
|
192
|
+
self.speech_enabled = False
|
|
193
|
+
|
|
194
|
+
def select_voice(self):
|
|
195
|
+
"""
|
|
196
|
+
lets the user select a piper voice for the assistant to speak with
|
|
197
|
+
"""
|
|
198
|
+
voices = [voice.value for voice in PiperVoiceUS] + [
|
|
199
|
+
voice.value for voice in PiperVoiceUK
|
|
200
|
+
]
|
|
201
|
+
idx = select_from(voices, prompt="select a voice")
|
|
202
|
+
if idx == -1:
|
|
203
|
+
return
|
|
204
|
+
if self.speech_enabled and self.piper_voice != voices[idx]:
|
|
205
|
+
glb.speaker = PiperSpeaker(
|
|
206
|
+
get_piper_voice_enum(voices[idx]), show_progress=False
|
|
207
|
+
)
|
|
208
|
+
self.piper_voice = voices[idx]
|
|
209
|
+
|
|
210
|
+
def toggle_speech(self):
|
|
211
|
+
"""toggles the assistant's ability to speak."""
|
|
212
|
+
if self.speech_enabled:
|
|
213
|
+
self.disable_speech()
|
|
214
|
+
else:
|
|
215
|
+
self.enable_speech()
|
|
216
|
+
|
|
217
|
+
def select_whisper_size(self):
|
|
218
|
+
"""
|
|
219
|
+
lets the user select the size of the whisper model used for converting
|
|
220
|
+
user's speech to text.
|
|
221
|
+
"""
|
|
222
|
+
display_options = [
|
|
223
|
+
f"{whisper_options[idx]} ({whisper_option_details[idx]})"
|
|
224
|
+
for idx in range(len(whisper_options))
|
|
225
|
+
]
|
|
226
|
+
idx = select_from(
|
|
227
|
+
display_options,
|
|
228
|
+
prompt="please select a whisper model, large model means better accuracy",
|
|
229
|
+
)
|
|
230
|
+
self.whisper_size = whisper_options[idx]
|
|
231
|
+
|
|
232
|
+
def enable_hearing(self):
|
|
233
|
+
"""enables the assistant to hear user with py-listener."""
|
|
234
|
+
from listener import Listener
|
|
235
|
+
|
|
236
|
+
def handle_speech(query: list[str]):
|
|
237
|
+
# print voice transcription on the console so
|
|
238
|
+
# the user may know what the assistant interprets
|
|
239
|
+
query = " ".join(query).strip()
|
|
240
|
+
if re.search("\w+", query) is None:
|
|
241
|
+
return
|
|
242
|
+
write(re.sub(r"\n+", ";", query))
|
|
243
|
+
handle_lm_response(lm.ask(query))
|
|
244
|
+
write(">> ", end="")
|
|
245
|
+
|
|
246
|
+
glb.listener = Listener(
|
|
247
|
+
time_window=3,
|
|
248
|
+
speech_handler=handle_speech,
|
|
249
|
+
on_speech_start=speech.mute,
|
|
250
|
+
whisper_size=self.whisper_size,
|
|
251
|
+
en_only=True,
|
|
252
|
+
# show_download=False
|
|
253
|
+
)
|
|
254
|
+
glb.listener.listen()
|
|
255
|
+
self.hearing_enabled = True
|
|
256
|
+
|
|
257
|
+
def disable_hearing(self):
|
|
258
|
+
"""stops listening to user."""
|
|
259
|
+
glb.listener.close()
|
|
260
|
+
glb.listener = None
|
|
261
|
+
self.hearing_enabled = False
|
|
262
|
+
|
|
263
|
+
def toggle_hearing(self):
|
|
264
|
+
"""toggles assistant's ability to hear user's speech."""
|
|
265
|
+
if self.hearing_enabled:
|
|
266
|
+
self.disable_hearing()
|
|
267
|
+
else:
|
|
268
|
+
self.enable_hearing()
|
|
269
|
+
|
|
270
|
+
def enact(self):
|
|
271
|
+
"""brings the settings into action."""
|
|
272
|
+
if self.speech_enabled:
|
|
273
|
+
self.enable_speech()
|
|
274
|
+
if self.hearing_enabled:
|
|
275
|
+
self.enable_hearing()
|
isaac/speech.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import isaac.globals as glb
|
|
7
|
+
import isaac.sync as sync
|
|
8
|
+
|
|
9
|
+
os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1"
|
|
10
|
+
import pygame
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def mute():
|
|
14
|
+
"""
|
|
15
|
+
sets the `event_mute` event that signals the speaker thread to stop
|
|
16
|
+
speaking.
|
|
17
|
+
"""
|
|
18
|
+
sync.event_mute.set()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def say(text: str):
|
|
22
|
+
"""Speaks the given text in a separate thread."""
|
|
23
|
+
|
|
24
|
+
def say_in_thread(text: str):
|
|
25
|
+
sync.event_mute.clear()
|
|
26
|
+
with sync.speech_lock:
|
|
27
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as temp:
|
|
28
|
+
file = temp.name
|
|
29
|
+
try:
|
|
30
|
+
glb.speaker.text_to_wave(text, file)
|
|
31
|
+
sound = pygame.mixer.Sound(file)
|
|
32
|
+
sound.play()
|
|
33
|
+
while pygame.mixer.get_busy():
|
|
34
|
+
if sync.event_mute.is_set():
|
|
35
|
+
sound.stop()
|
|
36
|
+
break
|
|
37
|
+
pygame.time.wait(100)
|
|
38
|
+
finally:
|
|
39
|
+
os.remove(file)
|
|
40
|
+
|
|
41
|
+
threading.Thread(target=say_in_thread, args=(text,)).start()
|
isaac/sync.py
ADDED
isaac/theme.py
ADDED
isaac/types.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Optional, Literal
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SettingsInterface(ABC):
|
|
6
|
+
groq_key: Optional[str]
|
|
7
|
+
groq_model: str
|
|
8
|
+
gemini_key: Optional[str]
|
|
9
|
+
gemini_model: str
|
|
10
|
+
hearing_enabled: bool
|
|
11
|
+
whisper_size: str
|
|
12
|
+
speech_enabled: bool
|
|
13
|
+
piper_voice: str
|
|
14
|
+
response_generator: Literal["gemini", "groq"]
|
|
15
|
+
system_message = Optional[str]
|
|
16
|
+
context_enabled: bool
|
|
17
|
+
shell: str
|
|
18
|
+
prompt_tokens: int
|
|
19
|
+
completion_tokens: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SpeakerInteface(ABC):
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def text_to_wave(self, text: str, file: str): ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ListenerInterface(ABC):
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def listen(self): ...
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def close(self): ...
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def pause(self): ...
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def resume(self): ...
|
isaac/utils.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import re
|
|
4
|
+
from typing import Optional, Union
|
|
5
|
+
|
|
6
|
+
from rich.console import Console, ConsoleOptions
|
|
7
|
+
from rich.markdown import Markdown
|
|
8
|
+
from rich.segment import Segment
|
|
9
|
+
from yapper import PiperVoiceUK, PiperVoiceUS
|
|
10
|
+
|
|
11
|
+
import isaac.globals as glb
|
|
12
|
+
import isaac.speech as speech
|
|
13
|
+
import isaac.sync as sync
|
|
14
|
+
import isaac.theme as theme
|
|
15
|
+
|
|
16
|
+
rich_console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def clear():
|
|
20
|
+
"""
|
|
21
|
+
clears the screen.
|
|
22
|
+
"""
|
|
23
|
+
os.system("cls" if platform.system() == "Windows" else "clear")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def write(*args, **kwargs):
|
|
27
|
+
"""
|
|
28
|
+
prints the given text on the screen with a lock that prevents
|
|
29
|
+
the listener thread from interrupting it.
|
|
30
|
+
"""
|
|
31
|
+
with sync.stdout_lock:
|
|
32
|
+
print(*args, **kwargs, flush=True)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def label_switch(switch: bool):
|
|
36
|
+
"""
|
|
37
|
+
return a string that prints the given boolean switch on the screen
|
|
38
|
+
in color.
|
|
39
|
+
"""
|
|
40
|
+
label = "on" if switch else "off"
|
|
41
|
+
pre = theme.GREEN if switch else theme.RED
|
|
42
|
+
label = pre + label + theme.RESET
|
|
43
|
+
return label
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_piper_voice_enum(str_voice: str) -> Union[PiperVoiceUS, PiperVoiceUK]:
|
|
47
|
+
"""
|
|
48
|
+
takes stringified name of a piper voice and return the matching attribute
|
|
49
|
+
from enum `PiperVoiceUS` or `PiperVoiceUK`.
|
|
50
|
+
"""
|
|
51
|
+
for voice_enum in list(PiperVoiceUS) + list(PiperVoiceUK):
|
|
52
|
+
if voice_enum.value == str_voice:
|
|
53
|
+
return voice_enum
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def safe_input(message: str) -> str:
|
|
57
|
+
try:
|
|
58
|
+
if glb.listener is not None:
|
|
59
|
+
glb.listener.pause()
|
|
60
|
+
with sync.stdout_lock:
|
|
61
|
+
return input(message)
|
|
62
|
+
finally:
|
|
63
|
+
if glb.listener is not None:
|
|
64
|
+
glb.listener.resume()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def select_from(
|
|
68
|
+
options: list[str],
|
|
69
|
+
prompt: Optional[str] = None,
|
|
70
|
+
allow_none: bool = True,
|
|
71
|
+
none_label: str = "none",
|
|
72
|
+
) -> int:
|
|
73
|
+
"""
|
|
74
|
+
Displays indexed options on the screen and lets the user select
|
|
75
|
+
one by typing its index.
|
|
76
|
+
"""
|
|
77
|
+
if allow_none:
|
|
78
|
+
options += [none_label]
|
|
79
|
+
options = [f"{idx}: {option}" for idx, option in enumerate(options)]
|
|
80
|
+
if prompt is not None:
|
|
81
|
+
write(theme.BOLD_BRIGHT + prompt + theme.RESET)
|
|
82
|
+
write("\n".join(options))
|
|
83
|
+
while True:
|
|
84
|
+
idx = safe_input("select by typing the index: ").strip()
|
|
85
|
+
try:
|
|
86
|
+
idx = int(idx)
|
|
87
|
+
if idx >= len(options):
|
|
88
|
+
raise ValueError()
|
|
89
|
+
except ValueError:
|
|
90
|
+
write("invalid option")
|
|
91
|
+
continue
|
|
92
|
+
if allow_none and idx == len(options) - 1:
|
|
93
|
+
return -1
|
|
94
|
+
write()
|
|
95
|
+
return idx
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def normalize_md(text: str) -> str:
|
|
99
|
+
"""
|
|
100
|
+
Detects code blocks in markdown text and replaces the code with
|
|
101
|
+
"you can see the code on the screen", replacs 'code' with specific
|
|
102
|
+
language if it can be detected.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def replacer(match):
|
|
106
|
+
match = re.search("```(\w+).*", text)
|
|
107
|
+
repl = match.group(1) if match else "the code"
|
|
108
|
+
return f"you can see {repl} on the screen. "
|
|
109
|
+
|
|
110
|
+
text = re.sub(r"```.*?```", replacer, text, flags=re.DOTALL)
|
|
111
|
+
text = re.sub(r"#+ ", "", text)
|
|
112
|
+
text = re.sub(r"\*\*(.*?)\*\*", r"\1", text)
|
|
113
|
+
text = re.sub(r"\*(.*?)\*", r"\1", text)
|
|
114
|
+
return text
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def handle_lm_response(response: str):
|
|
118
|
+
"""
|
|
119
|
+
prints the response on the screen, normalizes and speaks it.
|
|
120
|
+
"""
|
|
121
|
+
with sync.stdout_lock:
|
|
122
|
+
rich_console.print(CustomMarkdown(response))
|
|
123
|
+
if glb.settings.speech_enabled:
|
|
124
|
+
speech.say(normalize_md(response))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class CustomMarkdown(Markdown):
|
|
128
|
+
"""
|
|
129
|
+
rich.markdown.Markdown prepends a leading space character before lines of
|
|
130
|
+
code, that makes the code clearer to read but it becomes difficult to just
|
|
131
|
+
copy it from the console and paste because of the leading space, this
|
|
132
|
+
class detects those leading spaces, and moves them with sequence of spaces
|
|
133
|
+
or before the next newline character, makes code easy to copy and paste.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions):
|
|
137
|
+
rows: list[list[Segment]] = [[]]
|
|
138
|
+
bg_style = None
|
|
139
|
+
for seg in super().__rich_console__(console, options):
|
|
140
|
+
rows[-1].append(seg)
|
|
141
|
+
if (
|
|
142
|
+
bg_style is None
|
|
143
|
+
and seg.style
|
|
144
|
+
and seg.style.bgcolor
|
|
145
|
+
and seg.text.strip(" ") == ""
|
|
146
|
+
):
|
|
147
|
+
bg_style = seg.style
|
|
148
|
+
if seg.text.endswith("\n"):
|
|
149
|
+
rows.append([])
|
|
150
|
+
|
|
151
|
+
rows = rows[:-1] if len(rows[-1]) == 0 else rows
|
|
152
|
+
|
|
153
|
+
for row_idx in range(len(rows)):
|
|
154
|
+
row = rows[row_idx]
|
|
155
|
+
if (
|
|
156
|
+
not row[0].text.startswith(" ")
|
|
157
|
+
or row[0].text.count(" ") == options.max_width
|
|
158
|
+
):
|
|
159
|
+
continue
|
|
160
|
+
if row[0].text == " ":
|
|
161
|
+
rows[row_idx] = row = row[1:]
|
|
162
|
+
else:
|
|
163
|
+
segment = Segment(row[0].text[1:], row[0].style, row[0].control)
|
|
164
|
+
rows[row_idx][0] = row[0] = segment
|
|
165
|
+
rows[row_idx] = row[:-1] + [Segment(" ", style=bg_style)] + [row[-1]]
|
|
166
|
+
|
|
167
|
+
for segment in [s for row in rows for s in row]:
|
|
168
|
+
yield segment
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: py-isaac
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cooles locally run AI assistant
|
|
5
|
+
Home-page: https://github.com/n1teshy/py-isaac
|
|
6
|
+
Author: Nitesh Yadav
|
|
7
|
+
Author-email: nitesh.txt@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: AI assistant,local assistant
|
|
10
|
+
Platform: Posix; Windows
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: yapper-tts
|
|
19
|
+
Requires-Dist: py-listener
|
|
20
|
+
Requires-Dist: psutil
|
|
21
|
+
Requires-Dist: rich
|
|
22
|
+
Requires-Dist: pyreadline3; sys_platform == "win32"
|
|
23
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
isaac/__init__.py,sha256=SECpAtki4bUUcPSQGNlp4RRhGGObET-DY4lH0T7KA6g,1155
|
|
2
|
+
isaac/cli.py,sha256=OtEzrGzURT3IOz5sVL1OmQG341XA12ubt3s0rrtLkpE,67
|
|
3
|
+
isaac/command.py,sha256=Zj7n7yx_NfNidl50aabVU78h7Midg3cIL1Fmzjjtfow,10847
|
|
4
|
+
isaac/constants.py,sha256=ay5FqBKKVtnPpns05EHaggTzyyFCqoHwX6GyL8OGkp4,2035
|
|
5
|
+
isaac/globals.py,sha256=-mftYWEGP3iw-yOnTnPN6O9gR5LvMprJDDGA-TTa7kI,260
|
|
6
|
+
isaac/lang_models.py,sha256=lFTR0qLKUO6cJifqs2NMTP46IH_yZsRniHvGTgzhtYo,5550
|
|
7
|
+
isaac/meta.py,sha256=47Z714NbVkVnpKnxja4CGreFG2gVPM-hOwOxkva_4fM,86
|
|
8
|
+
isaac/settings.py,sha256=-IBi62Q1kLG2icjuL1I_Fc1NIGfCRV3Gy4dtsmorWu4,8878
|
|
9
|
+
isaac/speech.py,sha256=2Anjy2R9vqAeRIZQsYNutn1dFakk98C5Bp_z5rXaCr4,1065
|
|
10
|
+
isaac/sync.py,sha256=GB1gCk5n5aGbZ4viavHLR5nzHR1FEAWQ5ZiCDZwW8gQ,111
|
|
11
|
+
isaac/theme.py,sha256=c8V6sP0upZk2UVvyb54apJhCU3LIW304ReIYnWRXYPg,103
|
|
12
|
+
isaac/types.py,sha256=lTGIIoZhm09RiuGZ4YJZ_lM8x0e2mB1OpyMw6SnzDcQ,796
|
|
13
|
+
isaac/utils.py,sha256=_iJBEqzb0rmVK-IZ9SVst8iTbsYhJe-lmFH6g4L7PCA,5066
|
|
14
|
+
py_isaac-0.1.0.dist-info/METADATA,sha256=51Ac6dLcdGgEhSy5pAHlsKWHpjQJ4fxfeFDfNmQl5dM,740
|
|
15
|
+
py_isaac-0.1.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
16
|
+
py_isaac-0.1.0.dist-info/entry_points.txt,sha256=KgxpEWNAg3CorJxXXHMPytyJsVEIaXo9Cx_81471JwM,44
|
|
17
|
+
py_isaac-0.1.0.dist-info/top_level.txt,sha256=8a8FgzwJHUaYkAYAYrDA4d8e2svPKRpmgy_nddysmzQ,6
|
|
18
|
+
py_isaac-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
isaac
|