py-isaac 0.1.0__tar.gz

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.
@@ -0,0 +1,17 @@
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
File without changes
@@ -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
+ )
@@ -0,0 +1,4 @@
1
+ def main():
2
+ from isaac.command import run_loop
3
+
4
+ run_loop()
@@ -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="")
@@ -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"
@@ -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 = []
@@ -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
@@ -0,0 +1,3 @@
1
+ name = "py-isaac"
2
+ version = "0.1.0"
3
+ github = "https://github.com/n1teshy/%s" % (name,)
@@ -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()
@@ -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()
@@ -0,0 +1,5 @@
1
+ import threading
2
+
3
+ event_mute = threading.Event()
4
+ speech_lock = threading.Lock()
5
+ stdout_lock = threading.Lock()
@@ -0,0 +1,7 @@
1
+ RED = "\033[31m"
2
+ GREEN = "\033[32m"
3
+
4
+ BRIGHT = "\033[97m"
5
+ BOLD_BRIGHT = "\033[1;97m"
6
+
7
+ RESET = "\033[0m"
@@ -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): ...
@@ -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,17 @@
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
@@ -0,0 +1,21 @@
1
+ README.md
2
+ setup.py
3
+ isaac/__init__.py
4
+ isaac/cli.py
5
+ isaac/command.py
6
+ isaac/constants.py
7
+ isaac/globals.py
8
+ isaac/lang_models.py
9
+ isaac/meta.py
10
+ isaac/settings.py
11
+ isaac/speech.py
12
+ isaac/sync.py
13
+ isaac/theme.py
14
+ isaac/types.py
15
+ isaac/utils.py
16
+ py_isaac.egg-info/PKG-INFO
17
+ py_isaac.egg-info/SOURCES.txt
18
+ py_isaac.egg-info/dependency_links.txt
19
+ py_isaac.egg-info/entry_points.txt
20
+ py_isaac.egg-info/requires.txt
21
+ py_isaac.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ py-isaac = isaac.cli:main
@@ -0,0 +1,7 @@
1
+ yapper-tts
2
+ py-listener
3
+ psutil
4
+ rich
5
+
6
+ [:sys_platform == "win32"]
7
+ pyreadline3
@@ -0,0 +1 @@
1
+ isaac
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,46 @@
1
+ from pathlib import Path
2
+
3
+ from setuptools import find_packages, setup
4
+
5
+ ROOT = Path(__file__).parent.resolve()
6
+ meta = {}
7
+ exec((ROOT / "isaac/meta.py").read_text(), meta)
8
+ NAME, VERSION, GITHUB = meta["name"], meta["version"], meta["github"]
9
+ long_description = (ROOT / "README.md").read_text()
10
+
11
+
12
+ setup(
13
+ name=NAME,
14
+ version=VERSION,
15
+ author="Nitesh Yadav",
16
+ author_email="nitesh.txt@gmail.com",
17
+ description="Cooles locally run AI assistant",
18
+ long_description=long_description,
19
+ long_description_content_type="text/markdown",
20
+ url=GITHUB,
21
+ packages=find_packages(),
22
+ install_requires=[
23
+ "yapper-tts",
24
+ "pyreadline3; sys_platform == 'win32'",
25
+ "py-listener",
26
+ "psutil",
27
+ "rich"
28
+ ],
29
+ include_package_data=True,
30
+ classifiers=[
31
+ "Intended Audience :: Developers",
32
+ "Programming Language :: Python",
33
+ "Programming Language :: Python :: 3",
34
+ "License :: OSI Approved :: MIT License",
35
+ "Operating System :: OS Independent",
36
+ ],
37
+ entry_points={
38
+ "console_scripts": [
39
+ f"{NAME}=isaac.cli:main",
40
+ ],
41
+ },
42
+ platforms="Posix; Windows",
43
+ keywords=["AI assistant", "local assistant"],
44
+ python_requires=">=3.9",
45
+ license="MIT",
46
+ )