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/__init__.py +1 -0
- aipyapp/__main__.py +36 -0
- aipyapp/aipy/__init__.py +7 -0
- aipyapp/aipy/config.py +105 -0
- aipyapp/aipy/i18n.py +212 -0
- aipyapp/aipy/interface.py +26 -0
- aipyapp/aipy/llm.py +540 -0
- aipyapp/aipy/plugin.py +87 -0
- aipyapp/aipy/prompt.py +684 -0
- aipyapp/aipy/prompt.zh.py +512 -0
- aipyapp/aipy/prompt_en.py +688 -0
- aipyapp/aipy/qpython.py +35 -0
- aipyapp/aipy/runner.py +182 -0
- aipyapp/aipy/task.py +264 -0
- aipyapp/aipy/taskmgr.py +108 -0
- aipyapp/aipy/template.html +290 -0
- aipyapp/aipy/templates.py +54 -0
- aipyapp/aipy/utils.py +76 -0
- aipyapp/default.toml +97 -0
- aipyapp/default_en.toml +97 -0
- aipyapp/gui.py +409 -0
- aipyapp/main.py +80 -0
- aipyapp/publish.py +60 -0
- aipyapp/saas.py +167 -0
- aipyapp/wxgui.py +367 -0
- aipyapp_qpython-0.1.24.20.dist-info/METADATA +44 -0
- aipyapp_qpython-0.1.24.20.dist-info/RECORD +29 -0
- aipyapp_qpython-0.1.24.20.dist-info/WHEEL +5 -0
- aipyapp_qpython-0.1.24.20.dist-info/top_level.txt +1 -0
aipyapp/aipy/qpython.py
ADDED
|
@@ -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
|
aipyapp/aipy/taskmgr.py
ADDED
|
@@ -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()
|