aipyapp-qpython 0.1.24.20__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.
aipyapp/gui.py ADDED
@@ -0,0 +1,409 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ import os
4
+ import subprocess
5
+ from enum import Enum, auto
6
+ from pathlib import Path
7
+ import importlib.resources as resources
8
+ import traceback
9
+ from typing import Any, Optional, Union
10
+ import threading
11
+ try:
12
+ import tkinter as tk
13
+ from tkinter import ttk
14
+ import tkinter.font as tkFont
15
+ import tkinter.scrolledtext as scrolledtext
16
+ except ImportError:
17
+ import sys
18
+ print("Python Tkinter package is not installed. Please install python-tk.")
19
+ if sys.platform == "darwin":
20
+ print("You can use brew to install it: brew install python-tk")
21
+ else:
22
+ raise
23
+ sys.exit(1)
24
+
25
+ from dynaconf import Dynaconf
26
+ from rich.console import Console,JustifyMethod, OverflowMethod
27
+ from prompt_toolkit import PromptSession
28
+ from prompt_toolkit.history import FileHistory
29
+ from prompt_toolkit.styles import Style
30
+ from prompt_toolkit.completion import WordCompleter
31
+
32
+ from . import __version__
33
+ from .aipy.config import ConfigManager
34
+ from .aipy import TaskManager
35
+ from .aipy.i18n import T,set_lang
36
+ #
37
+ __PACKAGE_NAME__ = "aipyapp"
38
+
39
+ def strip_rich_text_tags(text: str) -> str:
40
+ """
41
+ Removes common Rich text formatting tags from a string without using regex.
42
+
43
+ Args:
44
+ text: The input string containing Rich tags.
45
+
46
+ Returns:
47
+ The string with Rich tags removed.
48
+ """
49
+ # List of common colors and styles
50
+ colors = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"]
51
+ styles = ["bold", "italic", "underline", "blink", "reverse", "strike", "dim", "b", "i", "u", "s"]
52
+
53
+ tags_to_remove = []
54
+
55
+ # Simple color tags
56
+ for color in colors:
57
+ tags_to_remove.append(f"[{color}]")
58
+ tags_to_remove.append(f"[/{color}]")
59
+
60
+ # Simple style tags
61
+ for style in styles:
62
+ tags_to_remove.append(f"[{style}]")
63
+ tags_to_remove.append(f"[/{style}]")
64
+
65
+ # Combined style and color tags (e.g., [bold red])
66
+ for style in styles:
67
+ for color in colors:
68
+ tags_to_remove.append(f"[{style} {color}]")
69
+ tags_to_remove.append(f"[/{style} {color}]") # Although [/style color] is not standard, include for safety
70
+ tags_to_remove.append(f"[{color} {style}]") # Handle swapped order if necessary
71
+ tags_to_remove.append(f"[/{color} {style}]")
72
+
73
+ # Generic closing tag
74
+ tags_to_remove.append("[/]")
75
+
76
+ # Add specific known tags if needed
77
+ # tags_to_remove.extend(["[link]", "[/link]"]) # Example
78
+
79
+ cleaned_text = text
80
+ for tag in tags_to_remove:
81
+ cleaned_text = cleaned_text.replace(tag, "")
82
+
83
+ # Handle potential edge cases like incomplete tags or variations if necessary
84
+ # This simple replacement might leave artifacts if tags are nested complexly
85
+ # or if there are tags not covered in the lists.
86
+
87
+ return cleaned_text
88
+
89
+ class GUIConsole(Console):
90
+ def __init__(self, *args, **kwargs):
91
+ super().__init__(*args, **kwargs)
92
+ self.gui = None
93
+
94
+ def set_gui(self, gui):
95
+ self.gui = gui
96
+
97
+
98
+ def print(
99
+ self,
100
+ *objects: Any,
101
+ sep: str = " ",
102
+ end: str = "\n",
103
+ style: Optional[Union[str, Style]] = None,
104
+ justify: Optional[JustifyMethod] = None,
105
+ overflow: Optional[OverflowMethod] = None,
106
+ no_wrap: Optional[bool] = None,
107
+ emoji: Optional[bool] = None,
108
+ markup: Optional[bool] = None,
109
+ highlight: Optional[bool] = None,
110
+ width: Optional[int] = None,
111
+ height: Optional[int] = None,
112
+ crop: bool = True,
113
+ soft_wrap: Optional[bool] = None,
114
+ new_line_start: bool = False,
115
+ ) -> None:
116
+ """Print to the console and send the output to a GUI handler."""
117
+ message = ""
118
+ # if the first argument is a string, use it as the message
119
+ if len(objects) == 1 and isinstance(objects[0], str):
120
+ message = objects[0]
121
+ else:
122
+ # Otherwise, join all objects into a single string
123
+ #message = sep.join(str(_object) for _object in objects)
124
+ pass
125
+ # If the message is empty, return
126
+ if not message:
127
+ return
128
+ #print("message", message)
129
+
130
+ if self.gui:
131
+ message = strip_rich_text_tags(message)
132
+ self.gui.handle_ai_output(message)
133
+
134
+
135
+ def print_exception(self, *args, **kwargs):
136
+ super().print_exception(*args, **kwargs)
137
+ # Optionally, capture the formatted exception and display in the text_widget
138
+ # This requires capturing the output of super().print_exception
139
+ # and redirecting it to the text_widget. A full implementation is
140
+ # beyond the scope of this example.
141
+
142
+ class CommandType(Enum):
143
+ CMD_DONE = auto()
144
+ CMD_USE = auto()
145
+ CMD_EXIT = auto()
146
+ CMD_INVALID = auto()
147
+ CMD_TEXT = auto()
148
+
149
+ def parse_command(input_str, llms=set()):
150
+ lower = input_str.strip().lower()
151
+
152
+ if lower in ("/done", "done"):
153
+ return CommandType.CMD_DONE, None
154
+ if lower in ("/exit", "exit"):
155
+ return CommandType.CMD_EXIT, None
156
+ if lower in llms:
157
+ return CommandType.CMD_USE, input_str
158
+
159
+ if lower.startswith("/use "):
160
+ arg = input_str[5:].strip()
161
+ if arg in llms:
162
+ return CommandType.CMD_USE, arg
163
+ else:
164
+ return CommandType.CMD_INVALID, arg
165
+
166
+ if lower.startswith("use "):
167
+ arg = input_str[4:].strip()
168
+ if arg in llms:
169
+ return CommandType.CMD_USE, arg
170
+
171
+ return CommandType.CMD_TEXT, input_str
172
+
173
+ class AIAppGUI:
174
+ def __init__(self, tm, settings):
175
+ self.tm = tm
176
+ self.settings = settings
177
+
178
+ # init llm
179
+ self.names = tm.llm.names
180
+ self.task = None
181
+ completer = WordCompleter(['/use', 'use', '/done','done'] + list(self.names['enabled']), ignore_case=True)
182
+ self.history = FileHistory(str(Path.cwd() / settings.history))
183
+ self.session = PromptSession(history=self.history, completer=completer)
184
+ self.style_main = Style.from_dict({"prompt": "green"})
185
+ self.style_ai = Style.from_dict({"prompt": "cyan"})
186
+ # EOF
187
+
188
+ # GUI staff
189
+ self.root = tk.Tk()
190
+ self.root.title("aipyapp GUI")
191
+ # Maximize the window on startup
192
+ try:
193
+ # This works on Windows and some Linux environments
194
+ self.root.state('zoomed')
195
+ except tk.TclError:
196
+ # Fallback for macOS and other environments
197
+ # Get screen width and height
198
+ screen_width = self.root.winfo_screenwidth()
199
+ screen_height = self.root.winfo_screenheight()
200
+ # Set geometry to fill the screen (may include taskbar/menu bar space)
201
+ self.root.geometry(f"{screen_width}x{screen_height}+0+0")
202
+
203
+ # AI输出结果:
204
+ self.output_label = ttk.Label(self.root, text="AI 输出:")
205
+ self.output_label.grid(row=0, column=0, sticky="w")
206
+ self.output_text = scrolledtext.ScrolledText(self.root, width=60, height=20)
207
+ self.output_text.grid(row=1, column=0, padx=5, pady=5, sticky="nsew")
208
+
209
+ # 程序执行结果:
210
+ self.code_label = ttk.Label(self.root, text="代码执行结果:")
211
+ self.code_label.grid(row=0, column=1, sticky="w")
212
+ self.code_text = scrolledtext.ScrolledText(self.root, width=60, height=20)
213
+ self.code_text.grid(row=1, column=1, padx=5, pady=5, sticky="nsew")
214
+
215
+
216
+
217
+ self.input_label = ttk.Label(self.root, text="输入你的需求,然后点击提交:")
218
+ self.input_label.grid(row=2, column=0, sticky="w", padx=5, pady=2)
219
+ self.input_entry = tk.Text(self.root, width=60, height=5)
220
+ self.input_entry.grid(row=3, column=0, padx=5, pady=2, sticky="ew")
221
+
222
+ button_font = tkFont.Font(family="Arial", size=16) # 调整 size 以改变字体大小
223
+ style = ttk.Style()
224
+ style.configure("Submit.TButton", font=button_font, padding=(10, 20, 10, 20)) # 使用 TButton 样式
225
+ self.submit_button = ttk.Button(self.root, text="提交", command=self.submit_prompt, style="Submit.TButton")
226
+ self.submit_button.grid(row=3, column=1, padx=5, pady=5, ipady=10, sticky="w")
227
+
228
+ self.continue_button = ttk.Button(self.root, text="结束会话并继续", command=self.continue_session)
229
+ self.continue_button.grid(row=4, column=1, padx=5, pady=2, sticky="w")
230
+ self.end_button = ttk.Button(self.root, text="结束会话并退出", command=self.end_session)
231
+ self.end_button.grid(row=4, column=1, padx=60, pady=2, sticky="e")
232
+
233
+
234
+ self.open_work_button = ttk.Button(self.root, text="打开工作目录", command=self.open_work_dir)
235
+ self.open_work_button.grid(row=4, column=0, padx=5, pady=2, sticky="w")
236
+
237
+ #self.open_config_button = ttk.Button(self.root, text="打开配置文件", command=self.open_config_file)
238
+ #self.open_config_button.grid(row=4, column=0, padx=10, pady=2, sticky="e")
239
+
240
+ self.root.grid_columnconfigure(0, weight=1)
241
+ self.root.grid_columnconfigure(1, weight=1)
242
+ self.root.grid_rowconfigure(1, weight=1)
243
+
244
+ console.print(f"[bold cyan]🚀 [AIPyApp ({__version__}) on [[green]QPython[/green]][/bold cyan] ")
245
+ console.print(f"[bold cyan]🌐 http://github.com/qpython-android/aipyapp[/bold cyan]")
246
+ self.print_output(f"{T('default')}: {self.names['default']},{T('enabled')}: {' '.join(self.names['enabled'])}\n")
247
+
248
+ def open_work_dir(self):
249
+ #path = self.settings.workdir
250
+ path = Path.cwd()
251
+ if os.path.exists(path):
252
+ if os.name == 'nt': # Windows
253
+ os.startfile(path)
254
+ elif os.name == 'posix': # macOS or Linux
255
+ subprocess.Popen(['open', path]) # macOS
256
+ # For Linux, you might want to use 'xdg-open' instead of 'open'
257
+ # subprocess.Popen(['xdg-open', path])
258
+ else:
259
+ print(f"Directory not found: {path}")
260
+
261
+ def open_config_file(self):
262
+ path = self.settings.user_config_path
263
+ if os.path.exists(path):
264
+ if os.name == 'nt': # Windows
265
+ os.startfile(path)
266
+ elif os.name == 'posix': # macOS or Linux
267
+ subprocess.Popen(['open', path])
268
+ else:
269
+ print(f"File not found: {path}")
270
+
271
+
272
+ def handle_ai_output(self, output):
273
+ if output[0] == '{' and output[-1] == '}':
274
+ # output is json
275
+ self.print_code("\n" + output)
276
+ else:
277
+ self.print_output(output)
278
+
279
+ def print_code(self, code):
280
+ self.code_text.insert(tk.END, code)
281
+ self.code_text.see(tk.END)
282
+ self.code_text.update_idletasks() # 添加此行以强制刷新 GUI
283
+ self.code_text.update() # Force the GUI to refresh
284
+
285
+ def print_output(self, output):
286
+ self.output_text.insert(tk.END, output)
287
+ self.output_text.see(tk.END)
288
+ self.output_text.update_idletasks() # 添加此行以强制刷新 GUI
289
+ self.output_text.update() # Force the GUI to refresh
290
+
291
+ def parse_use_command(self, user_input, llms):
292
+ words = user_input.split()
293
+ if len(words) > 2:
294
+ return None
295
+ if words[0] in ('/use', 'use'):
296
+ return words[1] if len(words) > 1 else ''
297
+ return words[0] if len(words) == 1 and words[0] in llms else None
298
+
299
+ def submit_prompt(self):
300
+ user_input = self.input_entry.get("1.0", tk.END)
301
+
302
+ # check use command
303
+ if len(user_input) < 2:
304
+ return
305
+
306
+ cmd, arg = parse_command(user_input, self.names['enabled'])
307
+ if cmd == CommandType.CMD_TEXT:
308
+ if self.task:
309
+ # in task
310
+ self.run_task(self.task, arg)
311
+ else:
312
+ print("new task", arg)
313
+ task = self.tm.new_task(arg)
314
+ self.run_task(task)
315
+ self.task = task
316
+ elif cmd == CommandType.CMD_DONE:
317
+ self.continue_session()
318
+ self.print_output("\n task Done\n")
319
+ elif cmd == CommandType.CMD_USE:
320
+ ret = self.tm.llm.use(arg)
321
+ self.print_output(f'\nUsing {arg} ok\n' if ret else '\nUsing {arg} failed\n')
322
+ elif cmd == CommandType.CMD_INVALID:
323
+ self.print_output(f"\nInvalid command: {arg}")
324
+ elif cmd == CommandType.CMD_EXIT:
325
+ self.end_session()
326
+
327
+ # clear
328
+ self.input_entry.delete("1.0", tk.END)
329
+
330
+ def _run_task(self, task, instruction=None):
331
+ try:
332
+ task.run(instruction=instruction)
333
+ except (EOFError, KeyboardInterrupt):
334
+ pass
335
+ except Exception as e:
336
+ #self.console.print_exception()
337
+ pass
338
+
339
+ def run_task(self, task, instruction=None):
340
+ thread = threading.Thread(target=self._run_task, args=(task, instruction))
341
+ thread.start()
342
+
343
+
344
+ def end_session(self):
345
+ #try:
346
+ # self.tm.publish(verbose=False)
347
+ #except Exception as e:
348
+ # pass
349
+
350
+ try:
351
+ if self.task:
352
+ self.task.done()
353
+ self.task = None
354
+ except Exception as e:
355
+ #self.console.print_exception()
356
+ pass
357
+ self.root.destroy()
358
+
359
+ def continue_session(self):
360
+ #try:
361
+ # self.tm.publish(verbose=False)
362
+ #except Exception as e:
363
+ # pass
364
+
365
+ try:
366
+ if self.task:
367
+ self.task.done()
368
+ self.task = None
369
+ except Exception as e:
370
+ traceback.print_exc()
371
+ #self.console.print_exception()
372
+ pass
373
+ self.input_entry.delete("1.0", tk.END)
374
+ self.code_text.delete("1.0", tk.END)
375
+ self.output_text.delete("1.0", tk.END)
376
+
377
+ def run(self):
378
+ self.root.mainloop()
379
+
380
+ def main(args):
381
+
382
+ path = args.config if args.config else 'aipython.toml'
383
+ user_config_path = Path(path).resolve()
384
+ default_config_path = resources.files(__PACKAGE_NAME__) / "default.toml"
385
+ conf = ConfigManager(default_config_path, args.config)
386
+ conf.check_config()
387
+ settings = conf.get_config()
388
+
389
+ settings.user_config_path = user_config_path
390
+ # auto install package.
391
+ settings.auto_install = True
392
+ settings.auto_getenv = True
393
+ settings.lang="zh"
394
+ console = GUIConsole()
395
+
396
+ lang = settings.get('lang') or or (os.getenv('LANG')[:2] if os.getenv('LANG') else "en")
397
+ if lang: set_lang(lang)
398
+
399
+ try:
400
+ tm = TaskManager(settings, console=console)
401
+ except Exception as e:
402
+ #console.print_exception(e)
403
+ #console.print(f"[bold red]Error: {e}")
404
+ print(e)
405
+ return
406
+
407
+ gui = AIAppGUI(tm, settings) # Replace None with actual AI instance
408
+ console.set_gui(gui)
409
+ gui.run()
aipyapp/main.py ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ import os
4
+ import code
5
+ import builtins
6
+ from pathlib import Path
7
+ import importlib.resources as resources
8
+
9
+ from rich.console import Console
10
+ from prompt_toolkit import PromptSession
11
+ from prompt_toolkit.formatted_text import HTML
12
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
13
+ from prompt_toolkit.completion import WordCompleter
14
+ from prompt_toolkit.lexers import PygmentsLexer
15
+ from prompt_toolkit.history import FileHistory
16
+ from pygments.lexers.python import PythonLexer
17
+
18
+ from . import __version__
19
+ from .aipy import TaskManager
20
+ from .aipy.i18n import T, set_lang
21
+ from .aipy.config import ConfigManager
22
+
23
+ __PACKAGE_NAME__ = "aipyapp"
24
+
25
+ class PythonCompleter(WordCompleter):
26
+ def __init__(self, ai):
27
+ names = ['exit()']
28
+ names += [name for name in dir(builtins)]
29
+ names += [f"ai.{attr}" for attr in dir(ai) if not attr.startswith('_')]
30
+ super().__init__(names, ignore_case=True)
31
+
32
+ def get_default_config():
33
+ lang = os.getenv('LANG')[:2] if os.getenv('LANG') else "en"
34
+ conf_file = lang=="zh" and "default.toml" or "default_en.toml"
35
+ default_config_path = resources.files(__PACKAGE_NAME__) / conf_file
36
+ return str(default_config_path)
37
+
38
+ def main(args):
39
+ console = Console(record=True)
40
+ console.print(f"[bold cyan]🚀 [AIPyApp ({__version__}) on [green]QPython[/green]][/bold cyan] ")
41
+ console.print(f"[bold cyan]🌐 github.com/qpython-android/aipyapp[/bold cyan] ")
42
+ conf = ConfigManager(get_default_config(), args.config_dir)
43
+ conf.check_config()
44
+ settings = conf.get_config()
45
+
46
+ lang = settings.get('lang') or or (os.getenv('LANG')[:2] if os.getenv('LANG') else "en")
47
+ if lang: set_lang(lang)
48
+
49
+ try:
50
+ ai = TaskManager(settings, console=console)
51
+ except Exception as e:
52
+ console.print_exception(e)
53
+ return
54
+
55
+ if not ai.llm:
56
+ console.print(f"[bold red]{T('no_available_llm')}")
57
+ return
58
+
59
+ names = ai.llm.names
60
+ console.print(f"{T('banner1_python')}", style="green")
61
+ console.print(f"[cyan]{T('default')}: [green]{names['default']},[cyan]{T('enabled')}: [yellow]{' '.join(names['enabled'])}")
62
+
63
+ interp = code.InteractiveConsole({'ai': ai})
64
+
65
+ completer = PythonCompleter(ai)
66
+ lexer = PygmentsLexer(PythonLexer)
67
+ auto_suggest = AutoSuggestFromHistory()
68
+ history = FileHistory(str(Path.cwd() / settings.history))
69
+ session = PromptSession(history=history, completer=completer, lexer=lexer, auto_suggest=auto_suggest)
70
+ while True:
71
+ try:
72
+ user_input = session.prompt(HTML('<ansiblue>>> </ansiblue>'))
73
+ if user_input.strip() in {"exit()", "quit()"}:
74
+ break
75
+ interp.push(user_input)
76
+ except EOFError:
77
+ console.print("[bold yellow]Exiting...")
78
+ break
79
+ except Exception as e:
80
+ console.print(f"[bold red]Error: {e}")
aipyapp/publish.py ADDED
@@ -0,0 +1,60 @@
1
+ import sys
2
+ import os
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import requests
7
+ from bs4 import BeautifulSoup
8
+
9
+ API_URL = "https://ai.xxyy.eu.org/api/publish"
10
+ CERT_PATH = Path('client.crt')
11
+
12
+ def upload_article(api_url, html_file_path):
13
+ if not os.path.isfile(html_file_path):
14
+ print(f"错误: 文件 {html_file_path} 不存在")
15
+ return False
16
+
17
+ with open(html_file_path, 'r', encoding='utf-8') as file:
18
+ html_content = file.read()
19
+
20
+ soup = BeautifulSoup(html_content, 'html.parser')
21
+ title = soup.find('span', class_='r4').get_text()
22
+
23
+ try:
24
+ files = {
25
+ 'content': (html_file_path, html_content, 'text/html')
26
+ }
27
+ meta = {'author': os.getlogin()}
28
+ data = {
29
+ 'title': title,
30
+ 'metadata': json.dumps(meta)
31
+ }
32
+
33
+ response = requests.post(
34
+ api_url,
35
+ files=files,
36
+ data=data,
37
+ cert=str(CERT_PATH),
38
+ verify=True
39
+ )
40
+
41
+ if response.status_code == 201:
42
+ print("文章上传成功!")
43
+ print("访问URL:", response.json())
44
+ return True
45
+ else:
46
+ print(f"上传失败 (状态码: {response.status_code}):", response.text)
47
+ return False
48
+ except Exception as e:
49
+ print("发生错误:", str(e))
50
+ return False
51
+
52
+ if __name__ == "__main__":
53
+ if len(sys.argv) != 2:
54
+ print("用法: python publish.py 'path/to/article.html'")
55
+ sys.exit(1)
56
+
57
+ html_file = sys.argv[1]
58
+
59
+ print(f"正在上传文件: {html_file})")
60
+ upload_article(API_URL, html_file)