multiai 0.2__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.
multiai/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """init.py."""
2
+ from .multiai import Prompt, Provider
3
+ from .printlong import print_long
@@ -0,0 +1,28 @@
1
+ [system]
2
+ version = 0.2
3
+ description = A Python library for text-based AI interactions
4
+ url = https://sekika.github.io/multiai/
5
+
6
+ [model]
7
+ ai_provider = openai
8
+ openai = gpt-4o-mini
9
+ anthropic = claude-3-haiku-20240307
10
+ google = gemini-1.5-flash
11
+ perplexity = llama-3.1-sonar-small-128k-chat
12
+ mistral = mistral-large-latest
13
+
14
+ [default]
15
+ temperature = 0.7
16
+ max_requests = 5
17
+
18
+ [command]
19
+ blank_lines = 0
20
+ always_copy = no
21
+ always_log = no
22
+ log_file = chat-ai-DATE.md
23
+
24
+ [prompt]
25
+ color = blue
26
+ english = If the following sentence is English, revise the text to improve its readability and clarity in English. If not, translate into English. No need to explain. Just output the result English text.
27
+ factual = Do not hallucinate. Do not make up factual information.
28
+ url = Summarize following text very briefly.
multiai/entry.py ADDED
@@ -0,0 +1,145 @@
1
+ """
2
+ Entry point of multiai
3
+ """
4
+ import argparse
5
+ import configparser
6
+ import os
7
+ import readline
8
+ import subprocess
9
+ import sys
10
+ import webbrowser
11
+ from datetime import datetime
12
+ from multiai import Prompt, Provider
13
+
14
+ __all__ = [
15
+ "entry",
16
+ ]
17
+
18
+
19
+ def entry():
20
+ """
21
+ Entry point of multiai
22
+
23
+ to be invoked with ai command
24
+ """
25
+ client = Prompt()
26
+ # Load user setting from config file in the order of
27
+ # data/system.ini, ~/.multiai, .multiai
28
+ inifile = configparser.ConfigParser()
29
+ here = os.path.abspath(os.path.dirname(__file__))
30
+ inifile.read(os.path.join(here, 'data/system.ini'))
31
+ inifile.read(os.path.expanduser('~/.multiai'))
32
+ inifile.read('.multiai')
33
+ # Start reading [command] section of the config file
34
+ # blank_lines: blank lines required to finish input
35
+ client.blank_lines = inifile.getint('command', 'blank_lines')
36
+ # always_copy: always use -c option
37
+ always_copy = inifile.getboolean('command', 'always_copy')
38
+ # always_log: always use -l option
39
+ always_log = inifile.getboolean('command', 'always_copy')
40
+ # log_file: file name of the log file.
41
+ log_file = inifile.get('command', 'log_file')
42
+ log_file = os.path.expanduser(log_file)
43
+ log_file = log_file.replace('DATE', datetime.today().strftime('%Y%m%d'))
44
+ client.log_file = log_file
45
+ # user_agent: user agent when retrieving web data
46
+ client.user_agent = inifile.get('command', 'user_agent', fallback=None)
47
+ # [promot] section
48
+ prompt_english = inifile.get('prompt', 'english')
49
+ prompt_factual = inifile.get('prompt', 'factual')
50
+ prompt_url = inifile.get('prompt', 'url')
51
+ # Load commandline argument
52
+ parser = argparse.ArgumentParser(
53
+ description=f'multiai {client.version} - {client.description}')
54
+ parser.add_argument('prompt', nargs='*',
55
+ help='prompt for AI')
56
+ parser.add_argument('-d', '--document',
57
+ action='store_true', help='open document page and exit')
58
+ for provider in Provider:
59
+ name = provider.name.lower()
60
+ help = 'use ' + name
61
+ if client.ai_provider == provider:
62
+ help += ' (Default)'
63
+ parser.add_argument(
64
+ '-' + name.replace('m', '')[0],
65
+ '--' + name,
66
+ action='store_true',
67
+ help=help)
68
+ parser.add_argument('-m', '--model',
69
+ help='set model')
70
+ parser.add_argument('-t', '--temperature',
71
+ help=f'set temperature. 0 is deterministic. Default is {client.temperature}.')
72
+ parser.add_argument('-e', '--english',
73
+ action='store_true', help='correct if English, translate into English otherwise')
74
+ parser.add_argument('-f', '--factual',
75
+ action='store_true', help='factual information')
76
+ parser.add_argument('-u', '--url',
77
+ help='retrieve text from the URL')
78
+ if not always_copy:
79
+ parser.add_argument('-c', '--copy',
80
+ action='store_true', help='copy the latest answer')
81
+ if not always_log:
82
+ parser.add_argument('-l', '--log',
83
+ action='store_true', help='save log file as chatgpt.md')
84
+ args = parser.parse_args()
85
+ # Get prompt
86
+ prompt = ' '.join(args.prompt)
87
+ # -d option
88
+ if args.document:
89
+ webbrowser.open(client.url)
90
+ sys.exit()
91
+ # Set ai_provider, ai_providers and model
92
+ client.ai_providers = []
93
+ for provider in Provider:
94
+ if getattr(args, provider.name.lower()):
95
+ client.ai_provider = provider
96
+ client.ai_providers.append(provider)
97
+ if len(client.ai_providers) == 0:
98
+ client.ai_providers = [client.ai_provider]
99
+ if len(client.ai_providers) == 1:
100
+ default_model = 'model_' + client.ai_provider.name.lower()
101
+ if args.model:
102
+ setattr(client, default_model, args.model)
103
+ client.model = getattr(client, default_model, None)
104
+ # -t option
105
+ if args.temperature:
106
+ try:
107
+ client.temperature = float(args.temperature)
108
+ except ValueError:
109
+ print("Invalid 'temperature': should be a number.")
110
+ sys.exit(1)
111
+ if client.temperature < 0:
112
+ print("Invalid 'temperature': should be >=0.")
113
+ sys.exit(1)
114
+ # -c option
115
+ if always_copy:
116
+ args.copy = True
117
+ client.copy = args.copy
118
+ # -l option
119
+ if always_log:
120
+ args.log = True
121
+ client.log = args.log
122
+ if args.log:
123
+ if not os.path.exists(log_file):
124
+ with open(log_file, 'w') as file:
125
+ file.write("# AI chat log\n\n")
126
+ # -e and -f option
127
+ pre_prompt = ''
128
+ if args.english:
129
+ pre_prompt = prompt_english + '\n\n'
130
+ if args.factual:
131
+ pre_prompt = prompt_factual + '\n'
132
+ # -u option
133
+ if args.url:
134
+ text = client.retrieve_from_url(args.url)
135
+ if prompt == '':
136
+ prompt = prompt_url
137
+ print(f'{client.color(client.role)}> {prompt}\n\nText of {args.url}')
138
+ prompt_summary = f'{prompt_url}\n\nText of {args.url}'
139
+ prompt += '\n' + text
140
+ client.ask_print(prompt, prompt_summary=prompt_summary)
141
+ # Finished loading arguments and run
142
+ if prompt == '' or args.url:
143
+ client.interactive(pre_prompt=pre_prompt)
144
+ else:
145
+ client.ask_print(pre_prompt + prompt)
multiai/multiai.py ADDED
@@ -0,0 +1,540 @@
1
+ """
2
+ multiai - A Python library for text-based AI interactions
3
+ """
4
+ import anthropic
5
+ import configparser
6
+ import enum
7
+ import google.generativeai as genai
8
+ import json
9
+ import openai
10
+ import os
11
+ import mistralai
12
+ import PyPDF2
13
+ import pyperclip
14
+ import requests
15
+ import sys
16
+ import trafilatura
17
+ from io import BytesIO
18
+ from multiai.printlong import print_long
19
+
20
+ __all__ = [
21
+ "Prompt",
22
+ "Provider",
23
+ "ColorCode",
24
+ ]
25
+
26
+
27
+ class Prompt():
28
+ """
29
+ The Prompt main application.
30
+
31
+ Usage:
32
+ client = Prompt()
33
+ answer = client.ask(prompt)
34
+ """
35
+
36
+ def __init__(self):
37
+ # Values independent of system or user setting file
38
+ self.role = 'user'
39
+ # Anthropic requires max_tokens, so default value is given.
40
+ # It can be overwritten by max_tokens.
41
+ self.max_tokens_anthropic = 4096
42
+ # Load system default values from data/system.ini
43
+ inifile = configparser.ConfigParser()
44
+ here = os.path.abspath(os.path.dirname(__file__))
45
+ inifile.read(os.path.join(here, 'data/system.ini'))
46
+ self.version = inifile.get('system', 'version')
47
+ self.description = inifile.get('system', 'description')
48
+ self.url = inifile.get('system', 'url')
49
+ # Load user setting from config file in the order of
50
+ # ~/.multiai, .multai
51
+ # It overwrites the system default values
52
+ conf_file = os.path.expanduser('~/.multiai')
53
+ inifile.read(conf_file)
54
+ inifile.read('.multiai')
55
+ self.set_provider(inifile.get('model', 'ai_provider'))
56
+ for provider in Provider:
57
+ name = provider.name.lower()
58
+ model = inifile.get('model', name, fallback=None)
59
+ if model is None:
60
+ print(f'multiai system error: {name} not found in [model].')
61
+ sys.exit(1)
62
+ setattr(self, 'model_' + name, model)
63
+ self.temperature = inifile.getfloat('default', 'temperature')
64
+ self.max_requests = inifile.getint('default', 'max_requests')
65
+ prompt_color = inifile.get('prompt', 'color')
66
+ try:
67
+ self.prompt_color = ColorCode[prompt_color.upper()].value
68
+ except Exception:
69
+ print(f'Error in the settings file: color = {prompt_color}')
70
+ available_colors = [name.lower()
71
+ for name in ColorCode.__members__.keys()]
72
+ print(f'Available colors: {", ".join(available_colors)}')
73
+ sys.exit(1)
74
+ # No system default value is given from here.
75
+ # Default values are given by fallback values.
76
+ self.max_tokens = inifile.getint(
77
+ 'default', 'max_tokens', fallback=None)
78
+ for provider in Provider:
79
+ env = os.getenv(provider.name + '_API_KEY')
80
+ name = provider.name.lower()
81
+ key = name + '_api_key'
82
+ if env is None:
83
+ ini = inifile.get('api_key', name, fallback=None)
84
+ setattr(self, key, ini)
85
+ else:
86
+ setattr(self, key, env)
87
+ self.clear()
88
+
89
+ def set_provider(self, provider):
90
+ """
91
+ Set AI provider
92
+
93
+ :param provider: str
94
+ AI provider (case insensitive)
95
+ """
96
+ try:
97
+ self.ai_provider = Provider[provider.upper()]
98
+ except Exception:
99
+ print(f'AI provider "{provider}" is not available.')
100
+ sys.exit(1)
101
+
102
+ def set_model(self, provider, model):
103
+ """
104
+ Set model
105
+
106
+ :param prvider: str
107
+ AI provider (case insensitive)
108
+ :param model: str
109
+ AI model
110
+ """
111
+ self.set_provider(provider)
112
+ self.model = model
113
+ setattr(self, 'model_' + provider.lower(), model)
114
+
115
+ def clear(self):
116
+ """
117
+ Clear chat history.
118
+ """
119
+ self.openai_messages = []
120
+ self.anthropic_messages = []
121
+ self.google_chat = None
122
+ self.perplexity_messages = []
123
+ self.mistral_messages = []
124
+
125
+ def ask(self, prompt, request=1, verbose=False):
126
+ """
127
+ Ask a question to AI.
128
+
129
+ :param prompt: str
130
+ prompt to ask AI
131
+ :param request: int
132
+ numbers of repetitive request
133
+ :param verbose: boolean
134
+ show repeat process
135
+ :return: str
136
+ Answer from AI
137
+ """
138
+ self.message = [
139
+ {
140
+ "role": self.role,
141
+ "content": prompt,
142
+ }
143
+ ]
144
+ self.prompt = prompt
145
+ if request == 1:
146
+ self.prompt_continue = False
147
+ else:
148
+ self.prompt_continue = True
149
+ # For example, call ask_openai() for openai
150
+ func_name = 'ask_' + self.ai_provider.name.lower()
151
+ try:
152
+ func = getattr(self, func_name)
153
+ except AttributeError:
154
+ print(
155
+ f'multiai system error: {func_name}() function is not defined.')
156
+ sys.exit(1)
157
+ func()
158
+ # Error
159
+ if self.error:
160
+ return self.error_message
161
+ # Finish successfully
162
+ if self.finish_reason in ['stop', 'end_turn']:
163
+ return self.response
164
+ # Unexpected finish reason
165
+ if self.finish_reason not in ['length', 'max_tokens']:
166
+ self.response += '\n\nFinish reason: {self.finish_reason}'
167
+ return self.response
168
+ # Response not finished. Continue the request.
169
+ request += 1
170
+ if request > self.max_requests:
171
+ self.response += '\n\nFinished because of max_tokens and max_requests.'
172
+ return self.response
173
+ if verbose:
174
+ print(
175
+ f'{self.color("Repeating...")} max_requests = {self.max_requests}, requests = {request}\r',
176
+ end='')
177
+ response = self.response
178
+ answer = self.ask('continue', request=request, verbose=verbose)
179
+ if self.error:
180
+ return answer
181
+ return response + answer
182
+
183
+ def ask_print(self, prompt, prompt_summary=None):
184
+ """
185
+ Ask a question to AI and print, copy, log
186
+
187
+ :param prompt: str
188
+ prompt to ask AI
189
+ :param prompt_summary: str
190
+ prompt shortened for logging
191
+ """
192
+ print(f'{self.color("Please wait ......")}\r', end='')
193
+ if len(self.ai_providers) == 1:
194
+ answer = self.ask(prompt, verbose=True)
195
+ print(' ' * 50 + '\r', end='')
196
+ if self.error:
197
+ print(f'{self.color("Error message")}> {answer}')
198
+ sys.exit(1)
199
+ print(f'{self.color(self.model)}>')
200
+ if self.log:
201
+ if prompt_summary is not None:
202
+ prompt = prompt_summary
203
+ with open(self.log_file, mode='a') as f:
204
+ f.write(
205
+ f'### {self.role}:\n{prompt}\n### {self.model}:\n{answer}\n')
206
+ else:
207
+ answer = ''
208
+ if prompt_summary is None:
209
+ prompt_log = prompt
210
+ else:
211
+ prompt_log = prompt_summary
212
+ for provider in (self.ai_providers):
213
+ self.ai_provider = provider
214
+ single_answer = self.ask(prompt, verbose=True)
215
+ model = getattr(self, 'model_' + provider.name.lower(), None)
216
+ if self.error:
217
+ print(
218
+ f'{self.color("Error message from " + provider.name.lower())}> {single_answer}')
219
+ sys.exit(1)
220
+ answer += f'### {model}:\n{single_answer}\n\n'
221
+ answer = answer.strip()
222
+ if self.log:
223
+ with open(self.log_file, mode='a') as f:
224
+ f.write(
225
+ f'### {self.role}:\n{prompt_log}\n{answer}\n')
226
+ print(' ' * 50 + '\r', end='')
227
+ print_long(answer)
228
+ if self.copy:
229
+ pyperclip.copy(answer)
230
+
231
+ def interactive(self, pre_prompt=''):
232
+ """
233
+ Interactive mode
234
+
235
+ :param pre_prompt: str
236
+ pre-prompt to append before prompt
237
+ """
238
+ prompt = ''
239
+ blank = 0
240
+ b = self.blank_lines
241
+ if b > 0:
242
+ print(
243
+ f'\nInput {b} blank line{"s" if b > 1 else ""} to finish input.')
244
+ while True:
245
+ try:
246
+ if prompt == '':
247
+ line = input(f'{self.color(self.role)}> ')
248
+ else:
249
+ line = input()
250
+ except EOFError:
251
+ sys.exit()
252
+ except KeyboardInterrupt:
253
+ sys.exit()
254
+ if line == '':
255
+ if prompt == '':
256
+ print('Blank text entered. Enter "q" to quit.')
257
+ continue
258
+ blank += 1
259
+ if blank < self.blank_lines:
260
+ prompt += line + '\n'
261
+ else:
262
+ self.ask_print(pre_prompt + prompt.strip())
263
+ prompt = ''
264
+ blank = 0
265
+ elif prompt == '' and line in ['q', 'x', 'quit', 'exit']:
266
+ sys.exit()
267
+ else:
268
+ if self.blank_lines == 0:
269
+ self.ask_print(pre_prompt + line)
270
+ prompt = ''
271
+ else:
272
+ prompt += line + '\n'
273
+ blank = 0
274
+
275
+ def color(self, text):
276
+ """
277
+ Return colored text with color defined at self.prompt_color
278
+
279
+ :param text: str
280
+ Text
281
+ :return: str
282
+ Colored text
283
+ """
284
+ if sys.stdout.isatty():
285
+ return f'\033[{self.prompt_color}m{text}\033[0m'
286
+ else:
287
+ return text
288
+
289
+ def retrieve_from_url(self, url, verbose=True):
290
+ """
291
+ Retrieve text from URL.
292
+
293
+ When URL ends with ".pdf", PDF file is converted to text.
294
+
295
+ :param url: str
296
+ URL to retrieve data from
297
+ :param verbose: boolean
298
+ whether to print message
299
+ :return: str
300
+ Retrieved text
301
+ """
302
+ if verbose:
303
+ print('Retrieving ...\r', end='')
304
+ headers = {
305
+ 'User-Agent': self.user_agent if hasattr(self, 'user_agent') else None}
306
+ try:
307
+ response = requests.get(url, headers=headers)
308
+ except Exception as e:
309
+ if verbose:
310
+ print(e)
311
+ sys.exit(1)
312
+ if response.status_code != 200:
313
+ if verbose:
314
+ print(f'{response.status_code} - {response.reason}')
315
+ sys.exit(1)
316
+ if verbose:
317
+ print('Converting to text.\r', end='')
318
+ if url.lower().endswith('.pdf'):
319
+ with BytesIO(response.content) as pdf_file:
320
+ reader = PyPDF2.PdfReader(pdf_file)
321
+ text = ""
322
+ for page in range(len(reader.pages)):
323
+ text += reader.pages[page].extract_text()
324
+ else:
325
+ text = trafilatura.extract(response.text)
326
+ if text is None:
327
+ if verbose:
328
+ print(f'{url} could not be retrieved.')
329
+ sys.exit(1)
330
+ return text
331
+
332
+ # Implementations for each providers
333
+ def ask_openai(self):
334
+ """
335
+ Ask a question to OpenAI.
336
+ """
337
+ if self.openai_api_key is None:
338
+ self.error = True
339
+ self.error_message = 'API key for OpenAI is not set.'
340
+ return
341
+ openai.api_key = self.openai_api_key
342
+ if not self.prompt_continue:
343
+ self.openai_messages += self.message
344
+ try:
345
+ self.completion = openai.chat.completions.create(
346
+ messages=self.openai_messages,
347
+ model=self.model_openai,
348
+ temperature=self.temperature,
349
+ max_tokens=self.max_tokens
350
+ )
351
+ self.error = False
352
+ self.response = self.completion.choices[0].message.content.strip(
353
+ )
354
+ self.finish_reason = self.completion.choices[0].finish_reason
355
+ self.openai_messages += [{"role": "assistant",
356
+ "content": self.response}]
357
+ except openai.APIError as e:
358
+ self.error = True
359
+ try:
360
+ self.error_code = e.status_code
361
+ self.error_dict = e.body
362
+ self.error_type = f"Error {self.error_code}: {self.error_dict['code']}"
363
+ self.error_message = f"{self.error_type}\n{self.error_dict['message']}"
364
+ except Exception:
365
+ self.error_message = e
366
+
367
+ def ask_anthropic(self):
368
+ """
369
+ Ask a question to Anthropic.
370
+ """
371
+ if self.anthropic_api_key is None:
372
+ self.error = True
373
+ self.error_message = 'API key for Anthropic is not set.'
374
+ return
375
+ client = anthropic.Anthropic(api_key=self.anthropic_api_key)
376
+ self.anthropic_messages += self.message
377
+ try:
378
+ self.completion = client.messages.create(
379
+ messages=self.anthropic_messages,
380
+ model=self.model_anthropic,
381
+ temperature=self.temperature,
382
+ max_tokens=self.max_tokens if self.max_tokens else self.max_tokens_anthropic
383
+ )
384
+ self.error = False
385
+ self.response = self.completion.content[0].text.strip()
386
+ self.finish_reason = self.completion.stop_reason
387
+ self.anthropic_messages += [{"role": "assistant",
388
+ "content": self.response}]
389
+ except Exception as e:
390
+ self.error = True
391
+ try:
392
+ self.error_code = e.status_code
393
+ self.error_dict = e.body['error']
394
+ self.error_type = f"{self.error_code}: {self.error_dict['type']}"
395
+ self.error_message = f"{self.error_type}\n{self.error_dict['message']}"
396
+ except Exception:
397
+ self.error_message = e
398
+
399
+ def ask_google(self):
400
+ """
401
+ Ask a question to Google.
402
+ """
403
+ # Supress logging warnings of libraries
404
+ os.environ["GRPC_VERBOSITY"] = "ERROR"
405
+ os.environ["GLOG_minloglevel"] = "2"
406
+ if self.google_api_key is None:
407
+ self.error = True
408
+ self.error_message = 'API key for Google is not set.'
409
+ return
410
+ genai.configure(api_key=self.google_api_key)
411
+ model = genai.GenerativeModel(self.model_google)
412
+ if self.google_chat is None:
413
+ self.google_chat = model.start_chat(history=[])
414
+ config = genai.types.GenerationConfig(
415
+ temperature=self.temperature,
416
+ max_output_tokens=self.max_tokens)
417
+ try:
418
+ self.completion = self.google_chat.send_message(
419
+ self.prompt, generation_config=config)
420
+ self.error = False
421
+ self.response = self.completion.text.replace('•', '* ').strip()
422
+ self.finish_reason = self.completion.candidates[0].finish_reason.name.lower(
423
+ )
424
+ except Exception as e:
425
+ self.error = True
426
+ try:
427
+ self.error_message = e.message
428
+ except Exception:
429
+ self.error_message = e
430
+
431
+ def ask_perplexity(self):
432
+ """
433
+ Ask a question to perplexity.
434
+ """
435
+ if self.perplexity_api_key is None:
436
+ self.error = True
437
+ self.error_message = 'API key for Perplexity is not set.'
438
+ return
439
+ base_url = 'https://api.perplexity.ai'
440
+ client = openai.OpenAI(
441
+ api_key=self.perplexity_api_key,
442
+ base_url=base_url)
443
+ self.perplexity_messages += self.message
444
+ try:
445
+ self.completion = client.chat.completions.create(
446
+ messages=self.perplexity_messages,
447
+ model=self.model_perplexity,
448
+ temperature=self.temperature,
449
+ max_tokens=self.max_tokens
450
+ )
451
+ self.error = False
452
+ self.response = self.completion.choices[0].message.content.strip(
453
+ )
454
+ self.finish_reason = self.completion.choices[0].finish_reason
455
+ self.perplexity_messages += [{"role": "assistant",
456
+ "content": self.response}]
457
+ except openai.APIError as e:
458
+ self.error = True
459
+ try:
460
+ # print(f'e = {e.__dict__.keys()}')
461
+ # for key in e.__dict__.keys():
462
+ # print(f'e.{key} = {getattr(e, key)}')
463
+ message = trafilatura.extract(e.message)
464
+ self.error_message = message.splitlines()[0]
465
+ except Exception:
466
+ self.error_message = e
467
+
468
+ def ask_mistral(self):
469
+ """
470
+ Ask a question to mistral.
471
+ """
472
+ if self.mistral_api_key is None:
473
+ self.error = True
474
+ self.error_message = 'API key for Mistral is not set.'
475
+ return
476
+ client = mistralai.Mistral(api_key=self.mistral_api_key)
477
+ self.mistral_messages += self.message
478
+ try:
479
+ self.completion = client.chat.complete(
480
+ messages=self.mistral_messages,
481
+ model=self.model_mistral,
482
+ temperature=self.temperature,
483
+ max_tokens=self.max_tokens
484
+ )
485
+ self.error = False
486
+ self.response = self.completion.choices[0].message.content.strip(
487
+ )
488
+ self.finish_reason = self.completion.choices[0].finish_reason
489
+ self.mistral_messages += [{"role": "assistant",
490
+ "content": self.response}]
491
+ except mistralai.SDKError as e:
492
+ self.error = True
493
+ try:
494
+ self.error_code = e.status_code
495
+ self.error_dict = json.loads(e.body)
496
+ self.error_message = f"Error {self.error_code}: {self.error_dict['message']}"
497
+ except Exception:
498
+ self.error_message = e
499
+
500
+
501
+ class Provider(enum.Enum):
502
+ """
503
+ Provider is an Enum representing AI provider available at multiai.
504
+
505
+ To add a provider definition,
506
+ (1) Add the provider here. Note that the first letter should not
507
+ overwrap other command-line options
508
+ (2) Define ask_provider() function in Prompt class
509
+ (3) Update clear() function in Prompt class
510
+ (4) Define default model at system.ini
511
+ """
512
+ ANTHROPIC = enum.auto()
513
+ GOOGLE = enum.auto()
514
+ OPENAI = enum.auto()
515
+ PERPLEXITY = enum.auto()
516
+ MISTRAL = enum.auto()
517
+
518
+
519
+ class ColorCode(enum.Enum):
520
+ """
521
+ ColorCode is an Enum representing ANSI color codes.
522
+
523
+ Each member of this Enum corresponds to a specific color used in terminal output.
524
+ """
525
+ BLACK = 30
526
+ RED = 31
527
+ GREEN = 32
528
+ YELLOW = 33
529
+ BLUE = 34
530
+ MAGENTA = 35
531
+ CYAN = 36
532
+ WHITE = 37
533
+ BACK_BLACK = 40
534
+ BACK_RED = 41
535
+ BACK_GREEN = 42
536
+ BACK_YELLOW = 43
537
+ BACK_BLUE = 44
538
+ BACK_MAGENTA = 45
539
+ BACK_CYAN = 46
540
+ BACK_WHITE = 47
multiai/printlong.py ADDED
@@ -0,0 +1,83 @@
1
+ """
2
+ print_long - print long text with pypager
3
+ """
4
+ import shutil
5
+ import unicodedata
6
+ import pypager
7
+
8
+
9
+ def print_long(text):
10
+ """
11
+ Print long text with pypager.
12
+
13
+ When text fits in 1 page in a terminal, it uses print,
14
+ otherwise it uses pypager.
15
+
16
+ :param text: str
17
+ text to display
18
+ """
19
+ default_terminal_size = (80, 20)
20
+ terminal_size = shutil.get_terminal_size(default_terminal_size)
21
+ lines_per_page = terminal_size.lines - 1
22
+ terminal_width = terminal_size.columns
23
+ wrapped_lines = []
24
+ for line in text.split('\n'):
25
+ wrapped_lines.extend(wrap_text(line, terminal_width))
26
+ total_lines = len(wrapped_lines)
27
+ wrapped_text = '\n'.join(wrapped_lines)
28
+ if total_lines <= lines_per_page:
29
+ print(text)
30
+ else:
31
+ p = pypager.pager.Pager()
32
+ p.add_source(pypager.source.StringSource(wrapped_text))
33
+ p.run()
34
+
35
+
36
+ def calculate_display_width(text):
37
+ """
38
+ Calculate display width of a line.
39
+
40
+ :param text: str
41
+ text to count
42
+ """
43
+ width = 0
44
+ for char in text:
45
+ if unicodedata.east_asian_width(char) in 'WF':
46
+ width += 2
47
+ else:
48
+ width += 1
49
+ return width
50
+
51
+
52
+ def wrap_text(text, width):
53
+ """
54
+ Wrap text of a line.
55
+
56
+ :param text: str
57
+ text to wrap
58
+ :param width: int
59
+ display width
60
+ """
61
+ lines = []
62
+ current_line = ""
63
+ current_width = 0
64
+ for line in text.split('\n'):
65
+ if not line:
66
+ lines.append("")
67
+ continue
68
+ for char in line:
69
+ char_width = 2 if unicodedata.east_asian_width(char) in 'WF' else 1
70
+ if current_width + char_width > width:
71
+ lines.append(current_line)
72
+ current_line = char
73
+ current_width = char_width
74
+ else:
75
+ current_line += char
76
+ current_width += char_width
77
+ if current_line:
78
+ lines.append(current_line)
79
+ current_line = ""
80
+ current_width = 0
81
+ if current_line:
82
+ lines.append(current_line)
83
+ return lines
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Katsutoshi Seki (関 勝寿)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.1
2
+ Name: multiai
3
+ Version: 0.2
4
+ Summary: A Python library for text-based AI interactions
5
+ Home-page: https://sekika.github.io/multiai/
6
+ Author: Katsutoshi Seki
7
+ License: MIT
8
+ Keywords: AI
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Environment :: Console
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: License :: OSI Approved :: MIT License
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Operating System :: MacOS :: MacOS X
22
+ Classifier: Operating System :: Microsoft :: Windows :: Windows 10
23
+ Classifier: Operating System :: Microsoft :: Windows :: Windows 11
24
+ Classifier: Operating System :: POSIX
25
+ Classifier: Operating System :: POSIX :: BSD
26
+ Classifier: Operating System :: POSIX :: Linux
27
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
29
+ Classifier: Natural Language :: English
30
+ Requires-Python: >=3.10
31
+ Description-Content-Type: text/markdown
32
+ License-File: LICENSE
33
+ Requires-Dist: openai
34
+ Requires-Dist: anthropic
35
+ Requires-Dist: google-generativeai
36
+ Requires-Dist: requests
37
+ Requires-Dist: mistralai
38
+ Requires-Dist: pypager
39
+ Requires-Dist: pyperclip
40
+ Requires-Dist: PyPDF2
41
+ Requires-Dist: trafilatura
42
+
43
+ # multiai
44
+
45
+ `multiai` is a Python library and command-line tool designed to interact with text-based generative AI models from the following providers:
46
+
47
+ | AI Provider | Web Service | Models Available |
48
+ |--------------|------------------------------------|----------------------------------------------------------------|
49
+ | **OpenAI** | [ChatGPT](https://chat.openai.com/) | [GPT Models](https://platform.openai.com/docs/models) |
50
+ | **Anthropic**| [Claude](https://claude.ai/) | [Claude Models](https://docs.anthropic.com/en/docs/about-claude/models) |
51
+ | **Google** | [Gemini](https://gemini.google.com/)| [Gemini Models](https://ai.google.dev/gemini-api/docs/models/gemini) |
52
+ | **Perplexity** | [Perplexity](https://www.perplexity.ai/) | [Perplexity Models](https://docs.perplexity.ai/docs/model-cards) |
53
+ | **Mistral** | [Mistral](https://chat.mistral.ai/chat) | [Mistral Models](https://docs.mistral.ai/getting-started/models/) |
54
+
55
+ ## Key Features
56
+
57
+ - **Interactive Chat:** Communicate with AI directly from your terminal.
58
+ - **Multi-Line Input:** Supports multi-line prompts for complex queries.
59
+ - **Pager for Long Responses:** View lengthy responses conveniently using a pager.
60
+ - **Continuation Handling:** Automatically handle and request continuations if responses are cut off.
61
+ - **Automatic Chat Logging:** Automatically save your chat history for future reference.
62
+
63
+ ## Usage
64
+
65
+ Install `multiai`, then configure your API keys for your chosen AI providers as environment variables or in a user-setting file. Once that's done, you can start interacting with the AI.
66
+
67
+ - To send a simple query:
68
+
69
+ ```bash
70
+ ai hi
71
+ ```
72
+
73
+ You should see a response like:
74
+
75
+ ```bash
76
+ gpt-4o-mini>
77
+ Hello! How can I assist you today?
78
+ ```
79
+
80
+ - For an interactive session, enter interactive mode:
81
+
82
+ ```bash
83
+ ai
84
+ ```
85
+
86
+ In this mode, you can continue the conversation:
87
+
88
+ ```bash
89
+ user> hi
90
+ gpt-4o-mini>
91
+ Hello! How can I assist you today?
92
+ user> how are you
93
+ gpt-4o-mini>
94
+ I'm just a program, so I don't have feelings, but I'm here and ready to help you! How about you? How are you doing?
95
+ user>
96
+ ```
97
+
98
+ To see a list of all command-line options, use:
99
+
100
+ ```bash
101
+ ai -h
102
+ ```
103
+
104
+ For more detailed documentation, you can open the [manual](https://sekika.github.io/multiai/) in a web browser with:
105
+
106
+ ```bash
107
+ ai -d
108
+ ```
109
+
110
+ ## Using `multiai` as a Python Library
111
+
112
+ `multiai` can also be used as a Python library. Here’s a simple example:
113
+
114
+ ```python
115
+ import multiai
116
+
117
+ # Initialize the client
118
+ client = multiai.Prompt()
119
+ client.set_model('openai', 'gpt-4o') # Set model
120
+ client.temperature = 0.5 # Set temperature
121
+
122
+ # Send a prompt and get a response
123
+ answer = client.ask('hi')
124
+ print(answer)
125
+
126
+ # Continue the conversation with context
127
+ answer = client.ask('how are you')
128
+ print(answer)
129
+
130
+ # Clear the conversation context
131
+ client.clear()
132
+ ```
133
+
134
+ The manual includes the following sample codes:
135
+
136
+ - A script that translates a text file into English.
137
+ - A local chat app that allows you to easily select from various AI models provided by different providers and engage in conversations with them.
@@ -0,0 +1,11 @@
1
+ multiai/__init__.py,sha256=4-GlQ8EG6H-bo-MAtqRuEagzRVsbcTbYcypRg4QFb-o,87
2
+ multiai/entry.py,sha256=-HadVUo9TFs80moS1Z_-lwNbswuZAnbELF5xXjloHNg,5348
3
+ multiai/multiai.py,sha256=mOj5E1bHIuuE8B3aSE-4Nv410BovVTkuf25eBhOr274,18894
4
+ multiai/printlong.py,sha256=262_HUxTguI5oBjd7vbMdTqGQfub0ZX9KmNF_wwmPno,2085
5
+ multiai/data/system.ini,sha256=9kTZaOLY7SUdcGzeSlzE0BQfi-YqrMnoEr2V7YRoalA,785
6
+ multiai-0.2.dist-info/LICENSE,sha256=9KL7GR2OrW7sgnHc8IM48AAcQ6gTEstY3UQRpe3OC5A,1085
7
+ multiai-0.2.dist-info/METADATA,sha256=rJ5toCg-naptoJDAwx4C8ybKrcmSpfubMcuXA7SnPHI,4705
8
+ multiai-0.2.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
9
+ multiai-0.2.dist-info/entry_points.txt,sha256=gt2WpdEIKJEcBWGWyC0-bXGK0L7767qqSfu7NUEmr3Y,43
10
+ multiai-0.2.dist-info/top_level.txt,sha256=nIS7JbFZhSLFa-wpj8vjQ52SvxPEm9bAEYJxgV1gj68,8
11
+ multiai-0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (72.2.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ai = multiai.entry:entry
@@ -0,0 +1 @@
1
+ multiai