🚀 部署:更新代码和配置文件并引入新的功能,为应用程序添加Python代码编辑器,支持多行代码高亮显示和执行功能。更新了系统提示信息模板,增加了对process.env.PORT环境变量的支持。更新了LLM工具类,允许根据用户输入调用tools中的execute函数来执行Python代码。修复了测试用例,修改了测试数据。

This commit is contained in:
ruotongyu
2024-05-30 20:25:32 +08:00
8 changed files with 433 additions and 62 deletions

32
agent/prompt.py Normal file
View File

@@ -0,0 +1,32 @@
from string import Template
system_prompt=Template("""### 目标 ###
你是一个高级程序员请调用tools中的execute函数完成用户提出的目标,execute 的code由你来编写,遇到不清楚的信息向用户咨询,最终一定要调用tools中的execute函数。
### 在代码中你可以使用以下函数 ###
word_action
如果你需要引用下面代码记得导入并初始化 WordAction。
```python
$python_code
```
"""
)
tools = [
{
"type": "function",
"function": {
"name": "execute",
"description": "execute python code",
"parameters": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "python code",
},
},
"required": ["code"],
},
},
}]

View File

@@ -1,35 +0,0 @@
from string import Template
system_prompt=Template("""
You are Open Interpreter, a world-class programmer that can complete any goal by executing code.
First, write a plan. **Always recap the plan between each code block** (you have extreme short-term memory loss, so you need to recap the plan between each message block to retain it).
When you execute code, it will be executed **on the user's machine**. The user has given you **full and complete permission** to execute any code necessary to complete the task. Execute the code.
If you want to send data between programming languages, save the data to a txt or json.
You can access the internet. Run **any code** to achieve the goal, and if at first you don't succeed, try again and again.
You can install new packages.
When a user refers to a filename, they're likely referring to an existing file in the directory you're currently executing code in.
Write messages to the user in Markdown.
In general, try to **make plans** with as few steps as possible. As for actually executing code to carry out that plan, for *stateful* languages (like python, javascript, shell, but NOT for html which starts from 0 every time) **it's critical not to try to do everything in one code block.** You should try something, print information about it, then continue from there in tiny, informed steps. You will never get it on the first try, and attempting it in one go will often lead to errors you cant see.
You are capable of **any** task.
# THE COMPUTER API
A python `word_action` module is ALREADY IMPORTED, and can be used for many tasks:
```python
$python_code
```
Do not import the computer module, or any of its sub-modules. They are already imported.
User Info\{\{import getpass
import os
import platform
print(f"Name: {getpass.getuser()}")
print(f"CWD: {os.getcwd()}")
print(f"SHELL: {os.environ.get('SHELL')}")
print(f"OS: {platform.system()}")
\}\}
"""
)

View File

@@ -1,5 +1,5 @@
from actions.action_util import ActionUtil
from agent.system_prompt import system_prompt
from agent.prompt import system_prompt
from utils.llm_util import LLM_Util
@@ -11,11 +11,14 @@ class WorkerAgent:
action_descriptions += action.package_actions_description() + "\n"
self.messages = [{"content": system_prompt.substitute(python_code=action_descriptions), "role": "system"}]
def run(self, question):
self.messages.append({"content": question, "role": "user"})
res = LLM_Util().invoke(self.messages)
self.messages.append({"content": res, "role": "assistant"})
# self.messages.append({"content": res, "role": "assistant"})
return res

View File

@@ -1,15 +1,18 @@
import json
import traceback
from PyQt6 import QtWidgets, QtCore
from PyQt6.QtCore import QThread, pyqtSignal, QEvent, Qt
from PyQt6.QtGui import QPixmap, QIcon
from PyQt6.QtWidgets import QApplication, QSystemTrayIcon, QMainWindow, QLabel, QTextEdit, QListWidgetItem, QSpacerItem, QSizePolicy, QAbstractItemView, QListWidget, QMenu
from pygments import highlight
from actions.action_util import ActionUtil
from agent.woker_agent import WorkerAgent
from pages.config_page import ConfigPage
from pages.plugin_page import PluginPage
from utils.global_keyboard_listen import GlobalKeyboardListen
from utils.qt_util import QtUtil
from utils.window_util import WindowUtil
from pygments.lexers import PythonLexer
from pygments.formatters import HtmlFormatter
class ActionItems(QListWidgetItem):
def __init__(self, action, chat_page):
@@ -93,7 +96,7 @@ class ChatList(QListWidget):
self.chat_page.action_list.set_visibility(False)
class WorkerThread(QThread):
finished_signal = pyqtSignal(str)
finished_signal = pyqtSignal(object)
def __init__(self, text):
QThread.__init__(self)
@@ -127,12 +130,25 @@ class ChatInput(QTextEdit):
# 清空输入框
self.clear()
# 连接线程的 finished 信号到槽函数增加对话UI
self.worker_thread.finished_signal.connect(lambda res: self.chat_page.new_conversation(f"{res}", "system"))
self.worker_thread.finished_signal.connect(self.render_llm_response)
self.worker_thread.start()
event.accept()
else:
super().keyPressEvent(event)
def render_llm_response(self, llm_res):
text = ""
if llm_res.get("tool_calls"):
print(llm_res.get("tool_calls"))
raw_arguments = llm_res["tool_calls"][0]["function"]["arguments"]
arguments = json.loads(raw_arguments)
self.chat_page.new_conversation(arguments["code"], "system", type="code")
elif llm_res.get("content"):
text = llm_res["content"]
self.chat_page.new_conversation(text, "system", type="text")
def on_text_changed(self):
current_text = self.toPlainText()
# 当输入中文不选择具体的文字时,也会进入到这里
@@ -226,9 +242,9 @@ class ChatPage(QMainWindow, interface_ui):
def mousePressEvent(self, event):
self.action_list.setVisible(False)
def new_conversation(self, text, role):
text = text.replace("\n", "<br>")
# type 为 text 时,显示文本,为 code 时,显示代码
def new_conversation(self, text, role, type="text"):
widget = QtWidgets.QWidget()
widget.setGeometry(QtCore.QRect(110, 100, 160, 80))
v_box = QtWidgets.QVBoxLayout(widget)
@@ -238,7 +254,7 @@ class ChatPage(QMainWindow, interface_ui):
role_name = "智子"
else:
role_pic = QtUtil.get_icon("vip.png")
role_name = "VIP用户"
role_name = ""
# 创建 QPixmap 对象并加图片
pixmap = QPixmap(role_pic)
pixmap = pixmap.scaled(30, 30, QtCore.Qt.AspectRatioMode.KeepAspectRatio)
@@ -267,7 +283,11 @@ class ChatPage(QMainWindow, interface_ui):
background-color: white;
border-radius: 10px;
""")
text_edit.setHtml(text)
if type == "code":
text = highlight(text, PythonLexer(), HtmlFormatter())
text_edit.setHtml(text)
else:
text_edit.setHtml(text)
v_box.addWidget(text_edit)
item = QListWidgetItem()
# 连接文档大小改变的信号

360
pages/python_code_edit.py Normal file
View File

@@ -0,0 +1,360 @@
import uuid
from pydantic import BaseModel, Field
from actions.action_base import ActionBase
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QTextEdit
from PyQt6.QtCore import QRect
from PyQt6.QtGui import QFont, QColor, QPainter, QTextFormat
from PyQt6.QtWidgets import QWidget, QPlainTextEdit, QTextEdit
from PyQt6 import QtGui, QtCore
class PythonExecutorInput(BaseModel):
code: str = Field(description="python代码", title="pytho代码", default="")
class PythonExecutorActoin(ActionBase):
name: str = "执行python代码"
description: str = "执行python代码"
args: PythonExecutorInput
output_save_name:str = "python_output"
def config_page_ui(self):
code_editor = QCodeEditor(display_line_numbers=True,
highlight_current_line=True,
syntax_high_lighter=PythonHighlighter)
self._config_ui.config_list.addWidget(code_editor)
self._ui_name_and_line_edit["code"] = code_editor
code_editor.setPlainText('# 保存输出结果\n' + self.output_save_name + " = ''")
# 执行python代码
def run(self, code):
environent = {}
exec(code, environent)
return environent[self.output_save_name]
class LineNumberArea(QWidget):
def __init__(self, editor):
QWidget.__init__(self, editor)
self.editor = editor
self.editor.blockCountChanged.connect(self.update_width)
self.editor.updateRequest.connect(self.update_contents)
self.font = QFont()
self.numberBarColor = QColor("#e8e8e8")
def paintEvent(self, event):
# Override paintEvent to draw the line numbers
painter = QPainter(self)
painter.fillRect(event.rect(), self.numberBarColor)
block = self.editor.firstVisibleBlock()
# Iterate over all visible text blocks in the document.
while block.isValid():
block_number = block.blockNumber()
block_top = self.editor.blockBoundingGeometry(block).translated(self.editor.contentOffset()).top()
# Check if the position of the block is outside the visible area.
if not block.isVisible() or block_top >= event.rect().bottom():
break
# We want the line number for the selected line to be bold.
if block_number == self.editor.textCursor().blockNumber():
self.font.setBold(True)
painter.setPen(QColor("#000000"))
else:
self.font.setBold(False)
painter.setPen(QColor("#717171"))
painter.setFont(self.font)
# Draw the line number right justified at the position of the line.
paint_rect = QRect(0, int(block_top), self.width(), self.editor.fontMetrics().height())
painter.drawText(paint_rect, Qt.AlignmentFlag.AlignRight, str(block_number + 1))
block = block.next()
painter.end()
QWidget.paintEvent(self, event)
# 根据文档的总行数来计算宽度
def get_width(self):
count = self.editor.blockCount()
# width = self.fontMetrics().width(str(count)) + 10
width = self.fontMetrics().horizontalAdvance(str(count)) + 5
return width
# 设置宽度
def update_width(self):
width = self.get_width()
if self.width() != width:
self.setFixedWidth(width)
self.editor.setViewportMargins(width, 0, 0, 0);
# 更行内容
def update_contents(self, rect, scroll):
if scroll:
self.scroll(0, scroll)
else:
self.update(0, rect.y(), self.width(), rect.height())
if rect.contains(self.editor.viewport().rect()):
font_size = self.editor.currentCharFormat().font().pointSize()
self.font.setPointSize(font_size)
self.font.setStyle(QFont.Style.StyleNormal)
self.update_width()
class QCodeEditor(QPlainTextEdit):
def __init__(self, display_line_numbers=True, highlight_current_line=True,
syntax_high_lighter=None, *args):
"""
Parameters
----------
display_line_numbers : bool
switch on/off the presence of the lines number bar
highlight_current_line : bool
switch on/off the current line highlighting
syntax_high_lighter : QSyntaxHighlighter
should be inherited from QSyntaxHighlighter
"""
super(QCodeEditor, self).__init__()
self.setFont(QFont("Microsoft YaHei UI Light", 11))
self.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
self.DISPLAY_LINE_NUMBERS = display_line_numbers
if display_line_numbers:
self.number_bar = LineNumberArea(self)
if highlight_current_line:
self.currentLineNumber = None
self.currentLineColor = self.palette().alternateBase()
self.cursorPositionChanged.connect(self.highlight_current_line)
if syntax_high_lighter is not None: # add highlighter to text document
self.highlighter = syntax_high_lighter(self.document())
# 默认选中第一行
cursor = self.textCursor()
cursor.movePosition(QtGui.QTextCursor.MoveOperation.Start)
self.setTextCursor(cursor)
self.highlight_current_line() # 确保第一行高亮
def resizeEvent(self, *e):
"""overload resizeEvent handler"""
if self.DISPLAY_LINE_NUMBERS: # resize LineNumberArea widget
cr = self.contentsRect()
rec = QRect(cr.left(), cr.top(), self.number_bar.get_width(), cr.height())
self.number_bar.setGeometry(rec)
QPlainTextEdit.resizeEvent(self, *e)
# 获取代码
def text(self):
return self.toPlainText()
def highlight_current_line(self):
new_current_line_number = self.textCursor().blockNumber()
if new_current_line_number != self.currentLineNumber:
self.currentLineNumber = new_current_line_number
hi_selection = QTextEdit.ExtraSelection()
hi_selection.format.setBackground(self.currentLineColor)
hi_selection.format.setProperty(QTextFormat.Property.FullWidthSelection, True)
hi_selection.cursor = self.textCursor()
hi_selection.cursor.clearSelection()
self.setExtraSelections([hi_selection])
def format_syn(color, style=''):
"""Return a QTextCharFormat with the given attributes.
"""
_color = QtGui.QColor()
_color.setNamedColor(color)
_format = QtGui.QTextCharFormat()
_format.setForeground(_color)
if 'bold' in style:
# QtGui.QFont.setBold(True)
_format.setFontWeight(QtGui.QFont.bold.__sizeof__())
if 'italic' in style:
_format.setFontItalic(True)
return _format
# Syntax styles that can be shared by all languages
STYLES = {
'keyword': format_syn('blue'),
'operator': format_syn('red'),
'brace': format_syn('darkGray'),
'defclass': format_syn('black', 'bold'),
'string': format_syn('magenta'),
'string2': format_syn('darkMagenta'),
'comment': format_syn('darkGreen', 'italic'),
'self': format_syn('black', 'italic'),
'numbers': format_syn('brown'),
}
class PythonHighlighter(QtGui.QSyntaxHighlighter):
"""Syntax highlighter for the Python language.
"""
# Python keywords
keywords = [
'and', 'assert', 'break', 'class', 'continue', 'def',
'del', 'elif', 'else', 'except', 'exec', 'finally',
'for', 'from', 'global', 'if', 'import', 'in',
'is', 'lambda', 'not', 'or', 'pass', 'print',
'raise', 'return', 'try', 'while', 'yield',
'None', 'True', 'False',
]
# Python operators
operators = [
r'=',
# Comparison
r'==', r'!=', r'<', r'<=', r'>', r'>=',
# Arithmetic
r'\+', r'-', r'\*', r'/', r'//', r'\%', r'\*\*',
# In-place
r'\+=', r'-=', r'\*=', r'/=', r'\%=',
# Bitwise
r'\^', r'\|', r'\&', r'\~', r'>>', r'<<',
]
# Python braces
braces = [
r'\{', r'\}', r'\(', r'\)', r'\[', r'\]',
]
def __init__(self, parent: QtGui.QTextDocument) -> None:
super().__init__(parent)
# Multi-line strings (expression, flag, style)
self.tri_single = (QtCore.QRegularExpression("'''"), 1, STYLES['string2'])
self.tri_double = (QtCore.QRegularExpression('"""'), 2, STYLES['string2'])
rules = []
# Keyword, operator, and brace rules
rules += [(r'\b%s\b' % w, 0, STYLES['keyword'])
for w in PythonHighlighter.keywords]
rules += [(r'%s' % o, 0, STYLES['operator'])
for o in PythonHighlighter.operators]
rules += [(r'%s' % b, 0, STYLES['brace'])
for b in PythonHighlighter.braces]
# All other rules
rules += [
# 'self'
(r'\bself\b', 0, STYLES['self']),
# 'def' followed by an identifier
(r"\bdef\b\s*(\w+)", 1, STYLES['defclass']),
# 'class' followed by an identifier
(r'\bclass\b\s*(\w+)', 1, STYLES['defclass']),
# Numeric literals
(r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']),
(r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']),
(r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']),
# Double-quoted string, possibly containing escape sequences
(r'"[^"\\]*(\\.[^"\\]*)*"', 0, STYLES['string']),
# Single-quoted string, possibly containing escape sequences
(r"'[^'\\]*(\\.[^'\\]*)*'", 0, STYLES['string']),
# From '#' until a newline
(r'#[^\n]*', 0, STYLES['comment']),
]
# Build a QRegExp for each pattern
self.rules = [(QtCore.QRegularExpression(pat), index, fmt)
for (pat, index, fmt) in rules]
def highlightBlock(self, text):
"""Apply syntax highlighting to the given block of text.
"""
self.tripleQuoutesWithinStrings = []
# Do other syntax formatting
for expression, nth, format in self.rules:
index = expression.match(text, 0).capturedStart(0)
if index >= 0:
# if there is a string we check
# if there are some triple quotes within the string
# they will be ignored if they are matched again
if expression.pattern() in [r'"[^"\\]*(\\.[^"\\]*)*"', r"'[^'\\]*(\\.[^'\\]*)*'"]:
# 匹配到三个引号
innerIndex = self.tri_single[0].match(text, index + 1).capturedStart(0)
if innerIndex == -1:
innerIndex = self.tri_double[0].match(text, index + 1).capturedStart(0)
if innerIndex != -1:
tripleQuoteIndexes = range(innerIndex, innerIndex + 3)
self.tripleQuoutesWithinStrings.extend(tripleQuoteIndexes)
while index >= 0:
# 跳过三引号
if index in self.tripleQuoutesWithinStrings:
index += 1
continue
# We actually want the index of the nth match
index = expression.match(text, index).capturedStart(nth)
length = expression.match(text, index).capturedLength(nth)
self.setFormat(index, length, format)
index = expression.match(text, index + length).capturedStart(0)
self.setCurrentBlockState(0)
# Do multi-line strings
in_multiline = self.match_multiline(text, *self.tri_single)
if not in_multiline:
in_multiline = self.match_multiline(text, *self.tri_double)
def match_multiline(self, text, delimiter, in_state, style):
"""Do highlight of multi-line strings. ``delimiter`` should be a
``QRegExp`` for triple-single-quotes or triple-double-quotes, and
``in_state`` should be a unique integer to represent the corresponding
state changes when inside those strings. Returns True if we're still
inside a multi-line string when this function is finished.
"""
# If inside triple-single quotes, start at 0
if self.previousBlockState() == in_state:
start = 0
add = 0
# Otherwise, look for the delimiter on this line
else:
start = delimiter.match(text).capturedStart(0)
# skipping triple quotes within strings
if start in self.tripleQuoutesWithinStrings:
return False
# Move past this match
add = delimiter.match(text).capturedLength()
# As long as there's a delimiter match on this line...
while start >= 0: # Look for the ending delimiter
end = delimiter.match(text, start + add).capturedStart(0)
# Ending delimiter on this line?
if end >= add:
length = end - start + add + delimiter.match(text).capturedLength()
self.setCurrentBlockState(0)
# No; multi-line string
else:
self.setCurrentBlockState(in_state)
length = len(text) - start + add
# Apply formatting
self.setFormat(start, length, style)
# Look for the next match
start = delimiter.match(text, start + length).capturedStart()
# Return True if still inside a multi-line string, False otherwise
if self.currentBlockState() == in_state:
return True
else:
return False

View File

@@ -11,19 +11,15 @@ PyYAML = "6.0.1"
requests = "2.31.0"
selenium = "4.16.0"
webdriver-manager = "4.0.1"
langchain = "0.1.7"
langchain-openai = "0.0.6"
pytest = "^8.0.1"
pyqt6 = "^6.6.1"
leancloud = "^2.9.12"
pyautogui = "^0.9.54"
langchainhub = "^0.1.15"
pyinstaller = "^6.5.0"
pygments = "^2.17.2"
python-docx = "^1.1.2"
global-hotkeys = "^0.1.7"
pynput = "^1.7.7"
litellm = "^1.38.12"
open-interpreter = "^0.2.5"
[build-system]

View File

@@ -2,7 +2,7 @@ from interpreter import interpreter
class OpenInterpreter:
def test_api(self):
interpreter.chat("电脑桌面有什么文件")
interpreter.chat("显示桌面所有文件")
if __name__ == "__main__":
interpreter.llm.api_key = "sk-i6ClcJAogigoWxI9271b80E978374e8dAbC1167d3b6d8eA3" # LiteLLM, which we use to talk to LM Studio, requires this

View File

@@ -1,8 +1,10 @@
from utils.config import Config
from litellm import completion
from agent.prompt import tools
class LLM_Util:
def __init__(self):
super().__init__()
self.config = Config()
@@ -10,16 +12,9 @@ class LLM_Util:
self.base_url = self.config.get_config_from_component("llm", "api_url")
self.model = self.config.get_config_from_component("llm", "model")
def llm(self):
from langchain_openai import ChatOpenAI
return ChatOpenAI(temperature=0, model_name=self.model, openai_api_key=self.api_key,
openai_api_base=self.base_url)
# messages = [{ "content": message, "role": "user"}]
def invoke(self, messages):
if self.base_url == "":
response = completion(model=self.model, api_key=self.api_key, messages=messages)
else:
response = completion(model=self.model, base_url=self.base_url, api_key=self.api_key, messages=messages)
response = completion(model=self.model, base_url=self.base_url, api_key=self.api_key,
messages=messages, tools=tools, tool_choice={"type": "function", "function": {"name": "execute"}})
return response.choices[0].message.content
return response.json()["choices"][0]["message"]