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.
@@ -0,0 +1,35 @@
1
+ # -*- coding: utf8; -*-
2
+ # qpy 环境相关函数
3
+ import os
4
+ import random
5
+ import string
6
+
7
+ def create_temp_py_file(code, dir):
8
+ # 生成一个随机的文件名
9
+ random_filename = ''.join(random.choices(string.ascii_letters + string.digits, k=10)) + '.py'
10
+ temp_file_path = os.path.join(dir, random_filename)
11
+
12
+ # 写入代码到临时文件
13
+ with open(temp_file_path, 'w', encoding='utf-8') as temp_file:
14
+ temp_file.write(code)
15
+
16
+ return temp_file_path
17
+
18
+ def qpy_is_webapp(code):
19
+ # 读取前128个字节
20
+ header = code[:128]
21
+
22
+ # 将字节转换为字符串并检查是否包含特定内容
23
+ if '#qpy:webapp' in header:
24
+ return True
25
+ else:
26
+ return False
27
+
28
+ def qpy_exec(code, args):
29
+ try: # 在QPython环境跑的程序
30
+ import qpy, androidhelper
31
+ script = create_temp_py_file(code, qpy.tmp)
32
+ droid = androidhelper.Android()
33
+ droid.executeQPy(script)
34
+ except Exception as e: # 在非QPython环境跑的程序
35
+ exec(code, gs)
aipyapp/aipy/runner.py ADDED
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import sys
5
+ import json
6
+ import traceback
7
+ from io import StringIO
8
+ from importlib.util import find_spec
9
+
10
+ from term_image.image import from_file, from_url
11
+
12
+ from . import utils
13
+ from .qpython import qpy_is_webapp, qpy_exec
14
+ from .i18n import T
15
+ from .plugin import event_bus
16
+ from .interface import Runtime
17
+
18
+ INIT_IMPORTS = """
19
+ import os
20
+ import re
21
+ import sys
22
+ import json
23
+ import time
24
+ import random
25
+ import traceback
26
+ """
27
+
28
+ import signal
29
+
30
+ # 定义处理 Ctrl+C 的函数
31
+ def signal_handler(sig, frame):
32
+ raise KeyboardInterrupt
33
+
34
+ # 注册信号处理器
35
+ signal.signal(signal.SIGINT, signal_handler)
36
+
37
+
38
+ def is_json_serializable(obj):
39
+ try:
40
+ json.dumps(obj) # 尝试序列化对象
41
+ return True
42
+ except (TypeError, OverflowError):
43
+ return False
44
+
45
+ def diff_dicts(dict1, dict2):
46
+ diff = {}
47
+ for key, value in dict1.items():
48
+ if key not in dict2 or dict2[key] != value:
49
+ diff[key] = value
50
+ return diff
51
+
52
+ class Runner(Runtime):
53
+ def __init__(self, settings, console, *, envs=None):
54
+ self._console = console
55
+ self._settings = settings
56
+ self.env = envs or {}
57
+ self._auto_install = settings.get('auto_install')
58
+ self._auto_getenv = settings.get('auto_getenv')
59
+ self.clear()
60
+
61
+ def clear(self):
62
+ self.history = [{'env': self.env}]
63
+ self._globals = {'runtime': self, '__session__': {}, '__name__': '__main__', 'input': self.input, '__history__': self.history}
64
+ exec(INIT_IMPORTS, self._globals)
65
+
66
+ def __repr__(self):
67
+ return f"<Runner history={len(self.history)}, env={len(self.env)}>"
68
+
69
+ @property
70
+ def globals(self):
71
+ return self._globals
72
+
73
+ def __call__(self, code_str, blocks):
74
+ old_stdout, old_stderr = sys.stdout, sys.stderr
75
+ captured_stdout = StringIO()
76
+ captured_stderr = StringIO()
77
+ sys.stdout, sys.stderr = captured_stdout, captured_stderr
78
+ result = {}
79
+ env = self.env.copy()
80
+ session = self._globals['__session__'].copy()
81
+ gs = self._globals.copy()
82
+ gs['__result__'] = {}
83
+ gs['__code_blocks__'] = blocks
84
+ try:
85
+ if qpy_is_webapp(code_str): #如果是QPY的WEB APP
86
+ qpy_exec(code_str, gs)
87
+ else: # 否则正常运行
88
+ exec(code_str, gs)
89
+
90
+ if not gs['__result__']:
91
+ result['message'] = T("The operation completed successfully")
92
+ except (SystemExit, Exception) as e:
93
+ result['errstr'] = str(e)
94
+ result['traceback'] = traceback.format_exc()
95
+
96
+ except KeyboardInterrupt:
97
+ result['message'] = T("Execution was interrupted by the user")
98
+
99
+ finally:
100
+ sys.stdout = old_stdout
101
+ sys.stderr = old_stderr
102
+
103
+ s = captured_stdout.getvalue().strip()
104
+ if s: result['stdout'] = s if is_json_serializable(s) else '<filtered: cannot json-serialize>'
105
+ s = captured_stderr.getvalue().strip()
106
+ if s: result['stderr'] = s if is_json_serializable(s) else '<filtered: cannot json-serialize>'
107
+
108
+ vars = gs.get('__result__')
109
+ if vars:
110
+ result['__result__'] = self.filter_result(vars)
111
+
112
+ history = {'code': code_str, 'result': result}
113
+
114
+ diff = diff_dicts(env, self.env)
115
+ if diff:
116
+ history['env'] = diff
117
+ diff = diff_dicts(gs['__session__'], session)
118
+ if diff:
119
+ history['session'] = diff
120
+
121
+ self.history.append(history)
122
+ return result
123
+
124
+ @utils.restore_output
125
+ def install_packages(self, packages):
126
+ self._console.print(f"\n⚠️ LLM {T('ask_for_packages')}: {packages}")
127
+ need_install = []
128
+ for package in packages:
129
+ if not find_spec(package):
130
+ need_install.append(package)
131
+ if not need_install:
132
+ self._console.print(f"✅ {T('packages_exist')}")
133
+ return True
134
+ ok = utils.confirm(self._console, f"💬 {T('agree_packages')} 'y'> ", auto=self._auto_install)
135
+ if ok:
136
+ return utils.install_packages(self._console, need_install)
137
+
138
+ @utils.restore_output
139
+ def getenv(self, name, default=None, *, desc=None):
140
+ self._console.print(f"\n⚠️ LLM {T('ask_for_env', name)}: {desc}")
141
+ try:
142
+ value = self.env[name][0]
143
+ self._console.print(f"✅ {T('env_exist', name)}")
144
+ except KeyError:
145
+ if self._auto_getenv:
146
+ self._console.print(f"✅ {T('auto_confirm')}")
147
+ value = None
148
+ else:
149
+ value = self._console.input(f"💬 {T('input_env', name)}: ")
150
+ value = value.strip()
151
+ if value:
152
+ self.setenv(name, value, desc)
153
+ return value or default
154
+
155
+ @utils.restore_output
156
+ def display(self, path=None, url=None):
157
+ quiet = self._console.quiet
158
+ image = {'path': path, 'url': url}
159
+ event_bus.broadcast('display', image)
160
+ if not quiet:
161
+ image = from_file(path) if path else from_url(url)
162
+ image.draw()
163
+
164
+ @utils.restore_output
165
+ def input(self, prompt=''):
166
+ return self._console.input(prompt)
167
+
168
+ def setenv(self, name, value, desc):
169
+ self.env[name] = (value, desc)
170
+
171
+ def filter_result(self, vars):
172
+ if isinstance(vars, dict):
173
+ for key in vars.keys():
174
+ if key in self.env:
175
+ vars[key] = '<masked>'
176
+ else:
177
+ vars[key] = self.filter_result(vars[key])
178
+ elif isinstance(vars, list):
179
+ vars = [self.filter_result(v) for v in vars]
180
+ else:
181
+ vars = vars if is_json_serializable(vars) else '<filtered>'
182
+ return vars
aipyapp/aipy/task.py ADDED
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+ import re
6
+ import json
7
+ import uuid
8
+ import platform
9
+ from pathlib import Path
10
+ from datetime import date
11
+ from enum import Enum, auto
12
+
13
+ import requests
14
+ from rich.panel import Panel
15
+ from rich.align import Align
16
+ from rich.table import Table
17
+ from rich.syntax import Syntax
18
+ from rich.markdown import Markdown
19
+
20
+ from .i18n import T
21
+ from .plugin import event_bus
22
+ from .templates import CONSOLE_HTML_FORMAT
23
+ from .utils import get_safe_filename
24
+
25
+ CERT_PATH = Path('/tmp/aipy_client.crt')
26
+
27
+ class MsgType(Enum):
28
+ CODE = auto()
29
+ TEXT = auto()
30
+
31
+ class Task:
32
+ MAX_ROUNDS = 16
33
+
34
+ def __init__(self, instruction, *, system_prompt=None, max_rounds=None):
35
+ self.task_id = uuid.uuid4().hex
36
+ self.instruction = instruction
37
+ self.console = None
38
+ self.llm = None
39
+ self.runner = None
40
+ self.max_rounds = max_rounds
41
+ self.system_prompt = system_prompt
42
+ self.pattern = re.compile(
43
+ r"^(`{3,4})(\w+)\s+([\w\-\.]+)\n(.*?)^\1\s*$",
44
+ re.DOTALL | re.MULTILINE
45
+ )
46
+
47
+ def save(self, path):
48
+ self._console.save_html(path, clear=False, code_format=CONSOLE_HTML_FORMAT)
49
+
50
+ def save_html(self, path, task):
51
+ # 获取当前代码所在目录下的template.html作为模版文件
52
+ # 读取模版文件
53
+ # 将模版文件中的{{code}}替换为task转换为json
54
+ # 保存为新的html文件
55
+ template_path = Path(__file__).resolve().parent / 'template.html'
56
+ try:
57
+ with open(template_path, 'r') as f:
58
+ template_content = f.read()
59
+ except FileNotFoundError:
60
+ self.console.print(f"[red]Error: template.html not found at {template_path}[/red]")
61
+ return
62
+ except Exception as e:
63
+ self.console.print_exception()
64
+ return
65
+
66
+ if 'llm' in task and isinstance(task['llm'], list) and len(task['llm']) > 0:
67
+ if task['llm'][0]['role'] == 'system':
68
+ task['llm'].pop(0)
69
+
70
+ task_json = json.dumps(task, ensure_ascii=False)
71
+ html_content = template_content.replace('{{code}}', task_json)
72
+
73
+ try:
74
+ with open(path, 'w', encoding='utf-8') as f:
75
+ f.write(html_content)
76
+ self.console.print(f"[green]Task saved to {path}[/green]")
77
+ except Exception as e:
78
+ self.console.print_exception()
79
+ return
80
+
81
+ def done(self):
82
+ #import pdb;pdb.set_trace()
83
+ instruction = self.instruction
84
+ task = {'instruction': instruction}
85
+ task['llm'] = self.llm.history.json()
86
+ task['runner'] = self.runner.history
87
+ filename = get_safe_filename(instruction, extension='.json') or f"{self.task_id}.json"
88
+ try:
89
+ json.dump(task, open(filename, 'w'), ensure_ascii=False, indent=4)
90
+ except Exception as e:
91
+ self.console.print_exception()
92
+
93
+ filename = get_safe_filename(instruction) or f"{self.task_id}.html"
94
+ if hasattr(self.console, 'gui'):
95
+ # Only save new html in gui mode for now.
96
+ self.save_html(filename, task)
97
+ else:
98
+ self.console.save_html(filename, clear=True, code_format=CONSOLE_HTML_FORMAT)
99
+
100
+ self.llm.clear()
101
+ self.runner.clear()
102
+ self.task_id = None
103
+ self.instruction = None
104
+
105
+ def parse_reply(self, markdown):
106
+ """
107
+ 从LLM回复中匹配出需要执行的代码
108
+ """
109
+ code_blocks = {}
110
+ for match in self.pattern.finditer(markdown):
111
+ _, _, name, content = match.groups()
112
+ code_blocks[name] = content.rstrip('\n')
113
+
114
+ return code_blocks
115
+
116
+ def process_code_reply(self, blocks, llm=None):
117
+ """
118
+ 执行从LLM回复中匹配出的代码
119
+ """
120
+
121
+ event_bus('exec', blocks)
122
+ code_block = blocks['main']
123
+ self.box(f"[white] 🚀 {T('start_execute')} ", code_block, lang='python')
124
+ result = self.runner(code_block, blocks)
125
+ event_bus('result', result)
126
+ result = json.dumps(result, ensure_ascii=False, indent=4)
127
+ self.box(f"[green] ✅ {T('execute_result')} ", result, lang="json")
128
+ status = self.console.status(f"[dim white]{T('start_feedback')}...")
129
+ self.console.print(status)
130
+ feed_back = f"# {T('Initial mission')}\n{self.instruction}\n\n# {T('Code execution result feedback')}\n{result}"
131
+ feedback_response = self.llm(feed_back, name=llm)
132
+ return feedback_response
133
+
134
+ def box(self, title, content, align=None, lang=None):
135
+ if hasattr(self.console, 'gui'):
136
+ # Using Mocked console. Dont use Panel
137
+
138
+ if lang == 'json':
139
+ # Only print execute result.
140
+ self.console.print(f"\n{title}")
141
+ self.console.print(content)
142
+ return
143
+
144
+ if lang:
145
+ content = Syntax(content, lang, line_numbers=True, word_wrap=True)
146
+ if align:
147
+ content = Align(content, align=align)
148
+
149
+ self.console.print(Panel(content, title=title))
150
+
151
+ def print_summary(self):
152
+ history = self.llm.history
153
+ """
154
+ table = Table(title=T("Task Summary"), show_lines=True)
155
+
156
+ table.add_column(T("Round"), justify="center", style="bold cyan", no_wrap=True)
157
+ table.add_column(T("Time(s)"), justify="right")
158
+ table.add_column(T("In Tokens"), justify="right")
159
+ table.add_column(T("Out Tokens"), justify="right")
160
+ table.add_column(T("Total Tokens"), justify="right", style="bold magenta")
161
+
162
+ round = 1
163
+ for row in history.get_usage():
164
+ table.add_row(
165
+ str(round),
166
+ str(row["time"]),
167
+ str(row["input_tokens"]),
168
+ str(row["output_tokens"]),
169
+ str(row["total_tokens"]),
170
+ )
171
+ round += 1
172
+ self._console.print("\n")
173
+ self._console.print(table)
174
+ """
175
+ summary = history.get_summary()
176
+ if 'time' in summary:
177
+ summary = "| {rounds} | {time:.3f}s | Tokens: {input_tokens}/{output_tokens}/{total_tokens}".format(**summary)
178
+ else:
179
+ summary = ''
180
+ event_bus.broadcast('summary', summary)
181
+ self.console.print(f"\n⏹ [cyan]{T('end_instruction')} {summary}")
182
+
183
+ def build_user_prompt(self):
184
+ prompt = {'task': self.instruction}
185
+ prompt['python_version'] = platform.python_version()
186
+ prompt['platform'] = platform.platform()
187
+ prompt['today'] = date.today().isoformat()
188
+ prompt['work_dir'] = '工作目录为qpy.tmp,默认在qpy.tmp下创建文件'
189
+ if self.console.quiet:
190
+ prompt['matplotlib'] = "我现在用的是 matplotlib 的 Agg 后端,请默认用 plt.savefig() 保存图片后用 runtime.display() 显示,禁止使用 plt.show()"
191
+ prompt['wxPython'] = "你回复的Markdown 消息中,可以用 ![图片](图片路径) 的格式引用之前创建的图片,会显示在 wx.html2 的 WebView 中"
192
+ else:
193
+ prompt['TERM'] = os.environ.get('TERM')
194
+ prompt['LC_TERMINAL'] = os.environ.get('LC_TERMINAL')
195
+ return prompt
196
+
197
+ def run(self, instruction=None, *, llm=None, max_rounds=None):
198
+ """
199
+ 执行自动处理循环,直到 LLM 不再返回代码消息
200
+ """
201
+ self.box(f"[yellow] 💁 {T('start_instruction')} ", f'[red]{instruction or self.instruction}', align="center")
202
+ if not instruction:
203
+ prompt = self.build_user_prompt()
204
+ event_bus('task_start', prompt)
205
+ instruction = json.dumps(prompt, ensure_ascii=False)
206
+ system_prompt = self.system_prompt
207
+ else:
208
+ system_prompt = None
209
+ rounds = 1
210
+ max_rounds = max_rounds or self.max_rounds
211
+ if not max_rounds or max_rounds < 1:
212
+ max_rounds = self.MAX_ROUNDS
213
+ response = self.llm(instruction, system_prompt=system_prompt, name=llm)
214
+ while response and rounds <= max_rounds:
215
+ blocks = self.parse_reply(response)
216
+ if 'main' not in blocks:
217
+ break
218
+ rounds += 1
219
+ response = self.process_code_reply(blocks, llm)
220
+ if event_bus.is_stopped():
221
+ break
222
+ self.print_summary()
223
+ os.write(1, b'\a\a\a')
224
+
225
+ def chat(self, prompt):
226
+ system_prompt = None if self.llm.history else self.system_prompt
227
+ response, ok = self.llm(prompt, system_prompt=system_prompt)
228
+ self.console.print(Markdown(response))
229
+
230
+ def step(self):
231
+ response = self.llm.get_last_message()
232
+ if not response:
233
+ self.console.print(f"❌ {T('no_context')}")
234
+ return
235
+ self.process_reply(response)
236
+
237
+ def publish(self, title=None, author=None, verbose=True):
238
+ url = self.settings.get('publish.url')
239
+ cert = self.settings.get('publish.cert')
240
+ if not (url and cert) or self.settings.get('publish.disable'):
241
+ if verbose: self.console.print(f"[red]{T('publish_disabled')}")
242
+ return False
243
+ title = title or self.instruction
244
+ author = author or os.getlogin()
245
+ meta = {'author': author}
246
+ files = {'content': self.console.export_html(clear=False)}
247
+ data = {'title': title, 'metadata': json.dumps(meta)}
248
+
249
+ if not (CERT_PATH.exists() and CERT_PATH.stat().st_size > 0):
250
+ CERT_PATH.write_text(cert)
251
+
252
+ try:
253
+ response = requests.post(url, files=files, data=data, cert=str(CERT_PATH), verify=True)
254
+ except Exception as e:
255
+ self.console.print_exception(e)
256
+ return
257
+
258
+ status_code = response.status_code
259
+ if status_code in (200, 201):
260
+ if verbose: self.console.print(f"[green]{T('upload_success')}:", response.json())
261
+ return response.json()
262
+ else:
263
+ if verbose: self.console.print(f"[red]{T('upload_failed', status_code)}:", response.text)
264
+ return False
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+ from pathlib import Path
6
+
7
+ from .i18n import T
8
+ from .task import Task
9
+ from .llm import LLM
10
+ from .runner import Runner
11
+ from .plugin import PluginManager
12
+
13
+ lang = os.getenv('LANG')[:2] if os.getenv('LANG') else "en"
14
+ if lang == "zh":
15
+ from .prompt import SYSTEM_PROMPT
16
+ else:
17
+ from .prompt_en import SYSTEM_PROMPT
18
+
19
+ class TaskManager:
20
+ def __init__(self, settings, console):
21
+ self.settings = settings
22
+ self.console = console
23
+ self.task = None
24
+ self.envs = {}
25
+ self.config_files = settings._loaded_files
26
+ self.system_prompt = f"{settings.system_prompt}\n{SYSTEM_PROMPT}"
27
+ plugin_dir = settings.get('plugin_dir') or Path.cwd() / 'plugins'
28
+ self.plugin_manager = PluginManager(plugin_dir)
29
+ self.plugin_manager.load_plugins()
30
+ if settings.workdir:
31
+ workdir = Path.cwd() / settings.workdir
32
+ workdir.mkdir(parents=True, exist_ok=True)
33
+ os.chdir(workdir)
34
+ self._cwd = workdir
35
+ else:
36
+ self._cwd = Path.cwd()
37
+ self._init_environ()
38
+ self._init_api()
39
+ self.runner = Runner(settings, console, envs=self.envs)
40
+ self.llm = LLM(settings, console, system_prompt=self.system_prompt)
41
+
42
+ @property
43
+ def workdir(self):
44
+ return str(self._cwd)
45
+
46
+ def use(self, name):
47
+ ret = self.llm.use(name)
48
+ self.console.print('[green]Ok[/green]' if ret else '[red]Error[/red]')
49
+ return ret
50
+
51
+ def done(self):
52
+ if self.task:
53
+ self.task.done()
54
+ self.task = None
55
+
56
+ def save(self, path):
57
+ if self.task:
58
+ self.task.save(path)
59
+
60
+ def _init_environ(self):
61
+ envs = self.settings.get('environ', {})
62
+ for name, value in envs.items():
63
+ os.environ[name] = value
64
+
65
+ def _init_api(self):
66
+ api = self.settings.get('api')
67
+ if not api:
68
+ return
69
+ lines = [self.system_prompt]
70
+ for api_name, api_conf in api.items():
71
+ lines.append(f"## {api_name} API")
72
+ desc = api_conf.get('desc')
73
+ if desc:
74
+ lines.append(f"### API {T('description')}\n{desc}")
75
+
76
+ envs = api_conf.get('env')
77
+ if not envs:
78
+ continue
79
+
80
+ lines.append(f"### {T('env_description')}")
81
+ for name, (value, desc) in envs.items():
82
+ value = value.strip()
83
+ if not value:
84
+ continue
85
+ lines.append(f"- {name}: {desc}")
86
+ self.envs[name] = (value, desc)
87
+
88
+ self.system_prompt = "\n".join(lines)
89
+
90
+ def new_task(self, instruction, llm=None, max_rounds=None, system_prompt=None):
91
+ if llm and not self.llm.use(llm):
92
+ return None
93
+
94
+ system_prompt = system_prompt or self.system_prompt
95
+ max_rounds = max_rounds or self.settings.get('max_rounds')
96
+ task = Task(instruction, system_prompt=system_prompt, max_rounds=max_rounds)
97
+ task.console = self.console
98
+ task.llm = self.llm
99
+ task.runner = self.runner
100
+ return task
101
+
102
+ def __call__(self, instruction, llm=None, max_rounds=None, system_prompt=None):
103
+ if self.task:
104
+ self.task.run(instruction=instruction, llm=llm, max_rounds=max_rounds)
105
+ else:
106
+ task = self.new_task(instruction, llm=llm, max_rounds=max_rounds, system_prompt=system_prompt)
107
+ self.task = task
108
+ task.run()