diff --git a/GeneralAgent/agent/agent.py b/GeneralAgent/agent/agent.py index cda3b3e..b0bcbe6 100644 --- a/GeneralAgent/agent/agent.py +++ b/GeneralAgent/agent/agent.py @@ -6,8 +6,27 @@ from GeneralAgent.interpreter import Interpreter from GeneralAgent.interpreter import KnowledgeInterpreter from GeneralAgent.interpreter import RoleInterpreter, PythonInterpreter +from GeneralAgent.utils import cut_messages, string_token_count +def default_output_callback(token): + if token is not None: + print(token, end='', flush=True) + else: + print('\n', end='', flush=True) + + +def default_check(check_content=None): + show = '确认 | 继续 (回车, yes, y, 是, ok) 或者 直接输入你的想法\n' + if check_content is not None: + show = f'{check_content}\n\n{show}' + response = input(show) + if response.lower() in ['', 'yes', 'y', '是', 'ok']: + return None + else: + return response + + class Agent(): """ Agent @@ -41,7 +60,7 @@ def __init__(self, base_url=None, self_call=False, continue_run=False, - output_callback=None, + output_callback=default_output_callback, disable_python_run=False, hide_python_code=False, messages=[], @@ -85,7 +104,6 @@ def __init__(self, frequency_penalty: float, 频率惩罚, 在 -2 和 2 之间 """ - from GeneralAgent import skills if workspace is None and len(knowledge_files) > 0: raise Exception('workspace must be provided when knowledge_files is not empty') if workspace is not None and not os.path.exists(workspace): @@ -98,7 +116,7 @@ def __init__(self, self.python_interpreter = PythonInterpreter(self, serialize_path=self._python_path) self.python_interpreter.function_tools = functions self.model = model or os.environ.get('DEFAULT_LLM_MODEL', 'gpt-4o') - self.token_limit = token_limit or skills.get_llm_token_limit(self.model) + self.token_limit = token_limit or 64 * 1000 self.api_key = api_key self.base_url = base_url # self.temperature = temperature @@ -108,12 +126,7 @@ def __init__(self, self.knowledge_interpreter = KnowledgeInterpreter(workspace, knowledge_files=knowledge_files, rag_function=rag_function) self.interpreters = [self.role_interpreter, self.python_interpreter, self.knowledge_interpreter] self.enter_index = None # 进入 with 语句时 self.memory.messages 的索引 - if output_callback is not None: - self.output_callback = output_callback - else: - # 默认输出回调函数 - from GeneralAgent import skills - self.output_callback = skills.output + self.output_callback = output_callback def __enter__(self): self.enter_index = len(self.memory.get_messages()) # Record the index of self.messages @@ -222,22 +235,7 @@ def run(self, command:Union[str, list], return_type=str, display=False, verbose= if not display: self.disable_output_callback() try: - from GeneralAgent import skills result = self._run(command, return_type=return_type, verbose=verbose) - if user_check: - # 没有渲染函数 & 没有输出回调函数: 用户不知道确认什么内容,则默认是str(result) - if check_render is None: - if self.output_callback is None: - show = str(result) - else: - show = ' ' - else: - show = check_render(result) - response = skills.check(show) - if response is None: - return result - else: - return self.run(response, return_type, user_check=user_check, check_render=check_render) return result except Exception as e: logging.exception(e) @@ -259,7 +257,7 @@ def user_input(self, input:Union[str, list], verbose=True): if self.continue_run and self.run_level == 0: # 判断是否继续执行 messages = self.memory.get_messages() - messages = skills.cut_messages(messages, 2*1000) + messages = cut_messages(messages, 2*1000) the_prompt = "对于当前状态,无需用户输入或者确认,继续执行任务,请回复yes,其他情况回复no" messages += [{'role': 'system', 'content': the_prompt}] response = skills.llm_inference(messages, model='smart', stream=False, api_key=self.api_key, base_url=self.base_url, **self.llm_args) @@ -277,7 +275,6 @@ def _run(self, input, return_type=str, verbose=False): @verbose: bool, verbose mode """ - from GeneralAgent import skills result = '' def local_output(token): @@ -331,7 +328,6 @@ def _memory_add_input(self, input): def _get_llm_messages(self): - from GeneralAgent import skills # 获取记忆 + prompt messages = self.memory.get_messages() if self.disable_python_run: @@ -339,9 +335,9 @@ def _get_llm_messages(self): else: prompt = '\n\n'.join([interpreter.prompt(messages) for interpreter in self.interpreters]) # 动态调整记忆长度 - prompt_count = skills.string_token_count(prompt) + prompt_count = string_token_count(prompt) left_count = int(self.token_limit * 0.9) - prompt_count - messages = skills.cut_messages(messages, left_count) + messages = cut_messages(messages, left_count) # 组合messages messages = [{'role': 'system', 'content': prompt}] + messages return messages diff --git a/GeneralAgent/interpreter/__init__.py b/GeneralAgent/interpreter/__init__.py index df48bdd..0d4d697 100644 --- a/GeneralAgent/interpreter/__init__.py +++ b/GeneralAgent/interpreter/__init__.py @@ -3,5 +3,4 @@ from .python_interpreter import PythonInterpreter from .knowledge_interpreter import KnowledgeInterpreter from .applescript_interpreter import AppleScriptInterpreter -from .shell_interpreter import ShellInterpreter -from .link_retrieve_interpreter import LinkRetrieveInterpreter \ No newline at end of file +from .shell_interpreter import ShellInterpreter \ No newline at end of file diff --git a/GeneralAgent/interpreter/link_retrieve_interpreter.py b/GeneralAgent/interpreter/link_retrieve_interpreter.py deleted file mode 100644 index fdf8a25..0000000 --- a/GeneralAgent/interpreter/link_retrieve_interpreter.py +++ /dev/null @@ -1,28 +0,0 @@ -# read the document and can retrieve the information -import re -from .interpreter import Interpreter -from GeneralAgent.memory import LinkMemory - - -class LinkRetrieveInterpreter(Interpreter): - """ - """ - - def __init__(self, python_interpreter=None, sparks_dict_name='sparks'): - self.python_interpreter = python_interpreter - self.sparks_dict_name = sparks_dict_name - self.link_memory = LinkMemory() - - def prompt(self, messages) -> str: - if self.link_memory.is_empty(): - return '' - else: - access_prompt = f""" -In Python, You can access the values of <> in all documents through the dictionary {self.sparks_dict_name}, such as <>: -```python -self.sparks_dict_name['Hello world'] -``` -""" - info = self.link_memory.get_memory(messages) - # return 'Background Information: \n' + info + access_prompt - return 'Background Information: \n' + info \ No newline at end of file diff --git a/GeneralAgent/interpreter/python_interpreter.py b/GeneralAgent/interpreter/python_interpreter.py index 8f12e54..bff09fc 100644 --- a/GeneralAgent/interpreter/python_interpreter.py +++ b/GeneralAgent/interpreter/python_interpreter.py @@ -5,9 +5,36 @@ from .interpreter import Interpreter from functools import partial +def get_python_version() -> str: + """ + Return the python version, like "3.9.12" + """ + import platform + python_version = platform.python_version() + return python_version + +def get_function_signature(func, module:str=None): + """Returns a description string of function""" + try: + import inspect + sig = inspect.signature(func) + sig_str = str(sig) + desc = f"{func.__name__}{sig_str}" + if func.__doc__: + desc += ': ' + func.__doc__.strip() + if module is not None: + desc = f'{module}.{desc}' + if inspect.iscoroutinefunction(func): + desc = "" + desc + return desc + except Exception as e: + import logging + logging.exception(e) + return '' + default_import_code = """ import os, sys, math, time -from GeneralAgent import skills +from codyer import skills """ class PythonInterpreter(Interpreter): @@ -59,7 +86,6 @@ def __init__(self, @prompt_append: append to the prompt, custom prompt can be added here @stop_wrong_count: stop running when the code is wrong for stop_wrong_count times """ - from GeneralAgent import skills self.globals = {} # global variables shared by all code self.agent = agent self.python_libs = libs @@ -82,12 +108,11 @@ def load(self): return {} def prompt(self, messages) -> str: - from GeneralAgent import skills - funtions = '\n\n'.join([skills.get_function_signature(x) for x in self.function_tools]) + funtions = '\n\n'.join([get_function_signature(x) for x in self.function_tools]) variables = { 'python_libs': self.python_libs, 'python_funcs': funtions, - 'python_version': skills.get_python_version() + 'python_version': get_python_version() } return Template(self.python_prompt_template).render(**variables) + self.prompt_append diff --git a/GeneralAgent/interpreter/role_interpreter.py b/GeneralAgent/interpreter/role_interpreter.py index b6bba8a..f91c591 100644 --- a/GeneralAgent/interpreter/role_interpreter.py +++ b/GeneralAgent/interpreter/role_interpreter.py @@ -1,98 +1,16 @@ -import os import datetime -import platform from jinja2 import Template from .interpreter import Interpreter -def get_os_version() -> str: - import platform - system = platform.system() - if system == 'Windows': - version = platform.version() - return f"Windows version: {version}" - elif system == 'Darwin': - version = platform.mac_ver()[0] - return f"macOS version: {version}" - elif system == 'Linux': - version = platform.platform() - return f"Linux version: {version}" - else: - return "Unknown system" - default_system_role = """ Current Time: {{now}} -You are an agent on the {{os_version}} computer, tasked with assisting users in resolving their issues. +You are an agent on the computer, tasked with assisting users in resolving their issues. You have the capability to control the computer and access the internet. All code in ```python ``` will be automatically executed by the system. So if you don't need to run the code, please don't write it in the code block. All responses should be formatted using markdown. For file references, use the format [title](a.txt), with all files stored in the './' directory. When result file is ready, provide it to the user with donwload link. """ -self_call_prompt = """ -# Self Control in Python: -- You are `agent` in python, and you can control yourself to do any task in future. -- Execute tasks by calling `agent.run('command', return_type=bool|str|dict|...)`. -- Ensure the 'command' string within `agent.run` does not exceed 5000 characters. -- Handle a wide range of tasks, not limited to text-based operations, by breaking down complex tasks into subtasks and executing them through self-calls. -- Use `agent.run` to complete parts of a task, not the entire task. -- Provide direct results when possible, without the need for additional calls to `agent.run('command', return_type=...)`. -- Complete highly complex tasks in one step through multi self-call, delivering the final result without requiring the user to wait or providing unnecessary explanations. - -## Example: -To introduce Chengdu and Beijing into a file: -```python -cities = ['Chengdu', 'Beijing'] -contents = [] -for city in cities: - contents.append(agent.run(f'Introduce {city}', return_type=str)) -with open('a.md', 'w') as f: - f.writelines(contents) -``` - -## Reponse with non-string type: -- when ask for a non-string type, you should return the variable by python code. - -## DEMO 1: give me the web (url: xxx) page content if amount to be issued is greater than 2000 dollar, return type should be -```python -content = agent.run('Scrape web page content of xxx', return_type=str) -bigger_than = agent.run(f'background: {content}\nDetermine whether the amount to be issued is greater than 2000 dollar?', return_type=bool) -result = content if bigger_than else "Content not displayed" -result -``` - -## DEMO 2: To return a boolean value, return type should be -user task: background:\n {content}. \nDetermine whether the amount to be issued is greater than 2000 dollar, and return a bool value -reposne: -\"\"\" -According to the background, the proposed issuance amount is greater than 2000 dollar, so it is True. -```python -bigger_than = True -bigger_than -``` -""" - -function_search_prompt = """ -# Search for functions -- When you cannot directly meet user needs, you can use the skills.search_functions function in python code to search for available functions, and then execute the functions to complete user needs. -## DEMO: draw a image about Beijing -```python -skills.search_functions('draw image') -``` -Result: -``` -skills.create_image(prompt): - Draw image given a prompt, returns the image path - @prompt: A text description of the desired image. The maximum length is 4000 characters. - @return: image path -``` -Then Draw a image -```python -image_path = skills.create_image('image description') -image_path -``` -""" - - class RoleInterpreter(Interpreter): """ RoleInterpreter, a interpreter that can change the role of the agent. @@ -101,13 +19,12 @@ class RoleInterpreter(Interpreter): def __init__(self, system_role=None, self_call=False, search_functions=False, role:str=None) -> None: """ - prompt = system_role | default_system_role + self_call_prompt + function_search_prompt + role + prompt = system_role | default_system_role + role @system_role: str, 系统角色. 如果为None,则使用默认系统角色 @self_call: bool, 是否开启自调用 @search_functions: bool, 是否开启搜索功能 @role: str, 用户角色 """ - self.os_version = get_os_version() self.system_role = system_role self.self_control = self_call self.search_functions = search_functions @@ -117,11 +34,7 @@ def prompt(self, messages) -> str: if self.system_role is not None: prompt = self.system_role else: - prompt = Template(default_system_role).render(os_version=self.os_version, now=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) - if self.self_control: - prompt += '\n\n' + self_call_prompt - if self.search_functions: - prompt += '\n\n' + function_search_prompt + prompt = Template(default_system_role).render(now=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) if self.role is not None: prompt += '\n\n' + self.role return prompt \ No newline at end of file diff --git a/GeneralAgent/memory/__init__.py b/GeneralAgent/memory/__init__.py index 08a9352..4569024 100644 --- a/GeneralAgent/memory/__init__.py +++ b/GeneralAgent/memory/__init__.py @@ -1,4 +1,2 @@ # import -from .normal_memory import NormalMemory -from .stack_memory import StackMemory, StackMemoryNode -from .link_memory import LinkMemory, LinkMemoryNode \ No newline at end of file +from .normal_memory import NormalMemory \ No newline at end of file diff --git a/GeneralAgent/memory/link_memory.py b/GeneralAgent/memory/link_memory.py deleted file mode 100644 index bc003f3..0000000 --- a/GeneralAgent/memory/link_memory.py +++ /dev/null @@ -1,124 +0,0 @@ -from dataclasses import dataclass -from typing import List -from tinydb import TinyDB, Query -from tinydb.storages import MemoryStorage - -@dataclass -class LinkMemoryNode: - key: str - content: str - childrens: List[str] = None - parents: List[str] = None - - def __post_init__(self): - self.childrens = self.childrens if self.childrens else [] - self.parents = self.parents if self.parents else [] - - def __str__(self): - return f'<<{self.key}>>\n{self.content}' - - def __repr__(self): - return str(self) - - -def summarize_and_segment(text, output_callback=None): - from GeneralAgent import skills - summary = skills.summarize_text(text) - if output_callback is not None: - output_callback(f'Summary: {summary}\n') - segments = skills.segment_text(text) - if output_callback is not None: - for key in segments: - output_callback(f'<<{key}>>\n') - return summary, segments - - -class LinkMemory(): - def __init__(self, serialize_path='./link_memory.json', short_memory_limit=2000) -> None: - """ - @serialize_path: str, 序列化路径,默认为'./link_memory.json'。如果为None,则使用内存存储 - @short_memory_limit: int, 短时记忆长度限制 - """ - self.serialize_path = serialize_path - self.short_memory_limit = short_memory_limit - if serialize_path is not None: - self.db = TinyDB(serialize_path) - else: - # 内存存储,不序列化 - self.db = TinyDB(storage=MemoryStorage) - nodes = [LinkMemoryNode(**x) for x in self.db.all()] - self.concepts = dict(zip([node.key for node in nodes], nodes)) - self.short_memory = '' - self._load_short_memory() - - def is_empty(self): - return len(self.concepts) == 0 - - def add_memory(self, content, output_callback=None): - from GeneralAgent import skills - self._summarize_content(content, output_callback) - while skills.string_token_count(self.short_memory) > self.short_memory_limit: - content = self.short_memory - self.short_memory = '' - self._summarize_content(content, output_callback) - - def get_memory(self, messages=None, limit_token_count=3000): - from GeneralAgent import skills - if len(self.concepts) == 0: - return '' - if messages is None: - return self.short_memory - else: - # TODO: recursive search - messages = skills.cut_messages(messages, 10*1000) - xx = self.short_memory.split('\n') - background = '\n'.join([f'#{line} {xx[line]}' for line in range(len(xx))]) - task = '\n'.join([f'{x["role"]}: {x["content"]}' for x in messages]) - info = skills.extract_info(background, task) - line_numbers, keys = skills.parse_extract_info(info) - result = [] - for line_number in line_numbers: - if line_number < len(xx) and line_number >= 0: - result.append(xx[line_number]) - if skills.string_token_count('\n'.join(result)) > limit_token_count: - return '\n'.join(result[:-1]) - for key in keys: - if key in self.concepts: - result.append(f'{key}\n{self.concepts[key]}\n') - if skills.string_token_count('\n'.join(result)) > limit_token_count: - return '\n'.join(result[:-1]) - return '\n'.join(result) - - def _load_short_memory(self): - short_memorys = self.db.table('short_memory').all() - self.short_memory = '' if len(short_memorys) == 0 else short_memorys[0]['content'] - - def _save_short_memory(self): - self.db.table('short_memory').truncate() - self.db.table('short_memory').insert({'content': self.short_memory}) - - def _summarize_content(self, input, output_callback=None): - from GeneralAgent import skills - inputs = skills.split_text(input, max_token=3000) - for text in inputs: - summary, nodes = summarize_and_segment(text, output_callback) - new_nodes = {} - for key in nodes: - new_key = self._add_node(key, nodes[key]) - new_nodes[new_key] = nodes[key] - self.short_memory += '\n' + summary + ' Detail in ' + ', '.join([f'<<{key}>>' for key in new_nodes]) - self.short_memory = self.short_memory.strip() - self._save_short_memory() - - def _add_node(self, key, value): - index = 0 - new_key = key - while new_key in self.concepts: - index += 1 - new_key = key + str(index) - self.concepts[new_key] = LinkMemoryNode(key=new_key, content=value) - self.db.upsert(self.concepts[key].__dict__, Query().key == new_key) - return new_key - - def __str__(self): - '\n'.join([str(x) for x in self.concepts.values()]) \ No newline at end of file diff --git a/GeneralAgent/memory/stack_memory.py b/GeneralAgent/memory/stack_memory.py deleted file mode 100644 index 1cb6cf4..0000000 --- a/GeneralAgent/memory/stack_memory.py +++ /dev/null @@ -1,266 +0,0 @@ -# Memeory -import json -from dataclasses import dataclass -from typing import List, Union -from tinydb import TinyDB, Query -from tinydb.storages import MemoryStorage - - -@dataclass -class StackMemoryNode: - role: str - type: str = None # 'text' or 'list' - content: str = None - node_id: int = None - parent: int = None - childrens: List[int] = None - - def __post_init__(self): - assert self.role in ['user', 'system', 'root', 'assistant'], self.role - self.childrens = self.childrens if self.childrens else [] - if self.type is None: - self.type = 'text' - - def __str__(self): - return f'<{self.role}><{self.type}>: {self.content}' - - def __repr__(self): - return str(self) - - def is_root(self): - return self.role == 'root' - - @classmethod - def new_root(cls): - return cls(node_id=0, role='root', content='root', parent=None, childrens=[]) - - -class StackMemory: - def __init__(self, serialize_path='./memory.json'): - """ - @serialize_path: str, 序列化路径,默认为'./memory.json'。如果为None,则使用内存存储 - """ - if serialize_path is not None: - self.db = TinyDB(serialize_path) - else: - # 内存存储,不序列化 - self.db = TinyDB(storage=MemoryStorage) - nodes = [StackMemoryNode(**node) for node in self.db.all()] - self.spark_nodes = dict(zip([node.node_id for node in nodes], nodes)) - # add root node - if len(self.spark_nodes) == 0: - root_node = StackMemoryNode.new_root() - self.spark_nodes[root_node.node_id] = root_node - self.db.insert(root_node.__dict__) - # load current_node - current_nodes = self.db.table('current_node').all() - if len(current_nodes) > 0: - node_id = current_nodes[0]['id'] - self.current_node = self.get_node(node_id) - else: - self.current_node = self.get_node(0) - self.next_position = 'after' # 'after' or 'in' - if self.current_node.is_root(): - self.next_position = 'in' - - def set_current_node(self, current_node): - self.current_node = current_node - # save current node - self.db.table('current_node').truncate() - self.db.table('current_node').insert({'id': current_node.node_id}) - # 同步数据库 - - def new_node_id(self): - return max(self.spark_nodes.keys()) + 1 - - def node_count(self): - # ignore root node - return len(self.spark_nodes.keys()) - 1 - - def add_node(self, node): - # put in root node - root_node = self.get_node(0) - node.node_id = self.new_node_id() - node.parent = root_node.node_id - root_node.childrens.append(node.node_id) - # save node - self.update_node(root_node) - self.db.insert(node.__dict__) - self.spark_nodes[node.node_id] = node - - def delete_node(self, node): - # delete node and all its childrens - for children_id in node.childrens: - children = self.get_node(children_id) - self.delete_node(children) - parent = self.get_node_parent(node) - if parent: - parent.childrens.remove(node.node_id) - self.update_node(parent) - self.db.remove(Query().node_id == node.node_id) - del self.spark_nodes[node.node_id] - - - def add_node_after(self, last_node, node): - # add node after last_node - node.node_id = self.new_node_id() - node.parent = last_node.parent - parent = self.get_node_parent(node) - if parent: - parent.childrens.insert(parent.childrens.index(last_node.node_id)+1, node.node_id) - self.update_node(parent) - # move childrens of last_node to node - node.childrens = last_node.childrens - last_node.childrens = [] - self.update_node(last_node) - for children_id in node.childrens: - children = self.get_node(children_id) - children.parent = node.node_id - self.update_node(children) - # save node - self.db.insert(node.__dict__) - self.spark_nodes[node.node_id] = node - return node - - def add_node_in(self, parent_node, node, put_first=False): - # add node in parent_node - node.node_id = self.new_node_id() - node.parent = parent_node.node_id - if put_first: - parent_node.childrens.insert(0, node.node_id) - else: - parent_node.childrens.append(node.node_id) - self.update_node(parent_node) - # save node - self.db.insert(node.__dict__) - self.spark_nodes[node.node_id] = node - return node - - def get_node(self, node_id): - return self.spark_nodes[node_id] - - def get_node_level(self, node:StackMemoryNode): - if node.is_root(): - return 0 - else: - return self.get_node_level(self.get_node_parent(node)) + 1 - - def get_node_parent(self, node): - if node.parent is None: - return None - else: - return self.get_node(node.parent) - - def update_node(self, node): - self.db.update(node.__dict__, Query().node_id == node.node_id) - - def get_level(self, node): - if node.is_root(): - return 0 - else: - return self.get_level(self.get_node_parent(node)) + 1 - - def get_related_nodes_for_node(self, node): - # ancestors + left_brothers + self - parent = self.get_node_parent(node) - brothers = [self.get_node(node_id) for node_id in parent.childrens] - left_brothers = [('brother', x) for x in brothers[:brothers.index(node)]] - if self.get_level(parent) != 0: - left_brothers = left_brothers[-4:] - ancestors = self.get_related_nodes_for_node(parent) if not parent.is_root() else [] - return ancestors + left_brothers + [('direct', node)] - - def get_related_messages_for_node(self, node: StackMemoryNode): - # 获取节点相关的消息列表(OpenAI格式,包含图片) - def _encode_image(image_path): - if image_path.startswith('http'): - return image_path - import base64 - with open(image_path, "rb") as image_file: - bin_data = base64.b64encode(image_file.read()).decode('utf-8') - image_type = image_path.split('.')[-1].lower() - virtural_url = f"data:image/{image_type};base64,{bin_data}" - return virtural_url - nodes_with_position = self.get_related_nodes_for_node(node) - def _parse_node(node): - if node.type == 'list': - contents = [] - items = json.loads(node.content) - for item in items: - if isinstance(item, str): - contents.append({'type': 'text', 'text': item}) - elif isinstance(item, dict): - key = list(item.keys())[0] - if key == 'image': - url = _encode_image(item[key]) - contents.append({'type': 'image_url', "image_url": { "url": url}}) - elif key == 'text': - contents.append({'type': 'text', 'text': item[key]}) - else: - raise Exception('message type wrong') - else: - raise Exception('message type wrong') - return {'role': node.role, 'content': contents} - return {'role': node.role, 'content': node.content} - messages = [_parse_node(node) for position, node in nodes_with_position] - return messages - - def get_all_description_of_node(self, node, intend_char=' ', depth=0): - lines = [] - description = intend_char * depth + str(node) - if not node.is_root(): - lines += [description] - for children_id in node.childrens: - children = self.get_node(children_id) - lines += self.get_all_description_of_node(children, intend_char, depth+1) - return lines - - def __str__(self) -> str: - lines = self.get_all_description_of_node(self.get_node(0), depth=-1) - return '\n'.join(lines) - - def get_messages(self): - return self.get_related_messages_for_node(self.current_node) - - def push_stack(self): - self.next_position = 'in' - return self.current_node.node_id - - def add_message(self, role, message: Union[str, list]): - assert role in ['user', 'system', 'assistant'], role - type = 'text' - if isinstance(message, list): - type = 'list' - message = json.dumps(message) - new_node = StackMemoryNode(role=role, content=message, type=type) - if self.next_position == 'after': - self.add_node_after(self.current_node, new_node) - else: - self.add_node_in(self.current_node, new_node) - self.next_position = 'after' - self.set_current_node(new_node) - return new_node.node_id - - def append_message(self, role, message, message_id=None): - if message_id is None: - message_id = self.current_node.node_id - node_id = message_id - self.pop_stack_to(node_id) - node = self.get_node(node_id) - node.content += '\n' + message - self.update_node(node) - self.set_current_node(node) - return node.node_id - - def pop_stack(self): - if self.next_position == 'in': - self.next_position = 'after' - else: - self.set_current_node(self.get_node_parent(self.current_node)) - self.next_position = 'after' - return self.current_node.node_id - - def pop_stack_to(self, node_id): - self.set_current_node(self.get_node(node_id)) - self.next_position = 'after' - return self.current_node.node_id \ No newline at end of file diff --git a/GeneralAgent/skills/__init__.py b/GeneralAgent/skills/__init__.py index 728291c..0509528 100644 --- a/GeneralAgent/skills/__init__.py +++ b/GeneralAgent/skills/__init__.py @@ -1,6 +1,6 @@ # 单列 import os -import logging +from codyer import skills def default_output_callback(token): if token is not None: @@ -19,79 +19,63 @@ def default_check(check_content=None): else: return response +def load_functions_with_path(python_code_path) -> (list, str): + """ + Load functions from python file + @param python_code_path: the path of python file + @return: a list of functions and error message (if any, else None) + """ + try: + import importlib.util + import inspect -class Skills: - __instance = None + # 指定要加载的文件路径和文件名 + module_name = 'skills' + module_file = python_code_path - @classmethod - def __getInstance(cls): - return cls.__instance + # 使用importlib加载文件 + spec = importlib.util.spec_from_file_location(module_name, module_file) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) - @classmethod - def _instance(cls, *args, **kwargs): - if not Skills.__instance: - Skills.__instance = Skills(*args, **kwargs) - return Skills.__instance - - def __setattr__(self, name, value): - if name.startswith('_'): - object.__setattr__(self, name, value) - else: - self._local_funs[name] = value - - def __getattr__(self, name): - if name.startswith('_'): - return None - return Skills.Proxy(self, name) - - class Proxy: - def __init__(self, skills_instance, name): - self.skills_instance = skills_instance - self.chain = [name] + # 获取文件中的所有函数 + functions = inspect.getmembers(module, inspect.isfunction) - def __getattr__(self, name): - if name.startswith('_'): - return None - self.chain.append(name) - return self + # 过滤functions中以下划线开头的函数 + functions = filter(lambda f: not f[0].startswith('_'), functions) - def __call__(self, *args, **kwargs): - full_name = '.'.join(self.chain) - func = self.skills_instance._get_func(full_name) - if func is None: - full_name = 'system.functions.' + full_name - func = self.skills_instance._get_func(full_name) - if func is None: - logging.error('Function {} not found'.format(full_name)) - return None - return func(*args, **kwargs) - - def _get_func(self, name): - fun = self._local_funs.get(name, None) - if fun is not None: - return fun - if name == 'output': - return default_output_callback - # logging.error('Function {} not found'.format(name)) - return None + return [f[1] for f in functions], None + except Exception as e: + # 代码可能有错误,加载不起来 + import logging + logging.exception(e) + return [], str(e) - def __init__(self): - self._local_funs = {} - self._load_local_funs() - self._local_funs['input'] = input - self._local_funs['check'] = default_check - self._local_funs['print'] = default_output_callback - self._local_funs['output'] = default_output_callback - - def _load_local_funs(self): - """ - 加载本目录的函数 - """ - from GeneralAgent.skills.python_envs import load_functions_with_directory - self._local_funs = {} - funcs = load_functions_with_directory(os.path.dirname(__file__)) - for fun in funcs: - self._local_funs[fun.__name__] = fun +def load_functions_with_directory(python_code_dir) -> list: + """ + Load functions from python directory (recursively) + @param python_code_dir: the path of python directory + @return: a list of functions + """ + import os + total_funs = [] + for file in os.listdir(python_code_dir): + # if file is directory + if os.path.isdir(os.path.join(python_code_dir, file)): + total_funs += load_functions_with_directory(os.path.join(python_code_dir, file)) + else: + # if file is file + if file.endswith('.py') and (not file.startswith('__init__') and not file.startswith('_') and not file == 'main.py'): + funcs, error = load_functions_with_path(os.path.join(python_code_dir, file)) + total_funs += funcs + return total_funs -skills = Skills._instance() \ No newline at end of file +if len(skills._functions) == 0: + skills._add_function('input', input) + skills._add_function('check', default_check) + skills._add_function('print', default_output_callback) + skills._add_function('output', default_output_callback) + funcs = load_functions_with_directory(os.path.dirname(__file__)) + for fun in funcs: + skills._add_function(fun.__name__, fun) \ No newline at end of file diff --git a/GeneralAgent/skills/memory_utils.py b/GeneralAgent/skills/memory_utils.py deleted file mode 100644 index 0af613c..0000000 --- a/GeneralAgent/skills/memory_utils.py +++ /dev/null @@ -1,154 +0,0 @@ - - -def _parse_segment_llm_result(text): - import logging - logging.info('-----------<_parse_segment_llm_result>------------') - logging.info(text) - logging.info('-----------------------') - lines = text.strip().split('\n') - key = None - nodes = {} - for line in lines: - line = line.strip() - if len(line) == 0: - continue - if line.startswith('<<') and line.endswith('>>'): - key = line[2:-2] - else: - if key is None: - logging.warning(f'key is None, line: {line}') - continue - blocks = line.split(':') - if len(blocks) >= 2: - start = int(blocks[0]) - end = int(blocks[1]) - nodes[key] = (start, end) - return nodes - - -def segment_text(text): - """ - 将文本进行语义分段,返回分段后的文本和key组成的字典nodes - """ - from GeneralAgent import skills - from jinja2 import Template - segment_prompt = """ ---------- -{{text}} ---------- - -For the text enclosed by ---------, the number following # is the line number. -Your task is to divide the text into segments (up to 6), each represented by the start and end line numbers. Additionally, assign a brief title (not exceeding 10 words) to each segment. -The output format is as follows: -``` -<> -Start_line: End_line - -<<Title for Segment>> -Start_line: End_line -``` -For instance: -``` -<<Hello>> -0:12 - -<<World>> -13:20 -``` -Please note, each title should not exceed 10 words. Titles exceeding this limit will be considered invalid. Strive to keep your titles concise yet reflective of the main content in the segment. -""" - lines = text.strip().split('\n') - new_lines = [] - for index in range(len(lines)): - new_lines.append(f'#{index} {lines[index]}') - new_text = '\n'.join(new_lines) - prompt = Template(segment_prompt).render({'text': new_text}) - messages = [ - {'role': 'system','content': 'You are a helpful assistant'}, - {'role': 'user','content': prompt} - ] - model_type='normal' - if skills.messages_token_count(messages) > 3500: - model_type = 'long' - result = skills.llm_inference(messages, model_type) - nodes = _parse_segment_llm_result(result) - for key in nodes: - start, end = nodes[key] - nodes[key] = '\n'.join(lines[start:end]) - return nodes - - -def summarize_text(text): - from GeneralAgent import skills - prompt = "Please distill the content between --------- into a concise phrase or sentence that captures the essence without any introductory phrases." - # prompt = "请将---------之间的内容提炼成一个简洁的短语或句子,抓住要点,无需任何介绍性短语。" - messages = [ - {'role': 'system','content': 'You are a helpful assistant'}, - {'role': 'user','content': f'{prompt}.\n---------\n{text}\n---------'} - ] - result = skills.llm_inference(messages) - return result - -def extract_info(background, task): - prompt_template = """ -Background (line number is indicated by #number, and <<title>> is a link to the details): ---------- -{{background}} ---------- - -Task ---------- -{{task}} ---------- - -Please provide the line numbers in the background that contain information relevant to solving the task. -Then, provide the <<titles>> that provide further details related to the background information. -The expected output format is as follows: -``` -#Line Number 1 -#Line Number 2 -... -<<title 1>> -<<title 2>> -... -``` -If no relevant information is found, please output "[Nothing]". -``` -[Nothing] -``` -Note: <<titles>> and line numbers provide up to 5 items each, so please select the most relevant. -""" - - from GeneralAgent import skills - from jinja2 import Template - prompt = Template(prompt_template).render({'background': background, 'task': task}) - messages = [ - {'role': 'system','content': 'You are a helpful assistant'}, - {'role': 'user','content': prompt} - ] - result = skills.llm_inference(messages) - return result - - -def parse_extract_info(text): - import re - numbers = re.findall(r'#(\d+)', text) - numbers = [int(x) for x in numbers] - titles = re.findall(r'<<([^>>]+)>>', text) - return numbers, titles - - -def extract_title(text, language='english'): - """ - extract title from text - """ - if len(text) > 500: - text = text[:500] - prompt = f"Please distill the content between --------- into a concise title in {language} of the content, less than five words. Return the title directly without including it in \".\n---------\n{text}\n---------" - from GeneralAgent import skills - messages = [ - {'role': 'system','content': 'You are a helpful assistant'}, - {'role': 'user','content': prompt} - ] - result = skills.llm_inference(messages) - return result \ No newline at end of file diff --git a/GeneralAgent/skills/split_text.py b/GeneralAgent/skills/split_text.py deleted file mode 100644 index 17d02a9..0000000 --- a/GeneralAgent/skills/split_text.py +++ /dev/null @@ -1,27 +0,0 @@ - - -def split_text(text, max_token=3000, separators='\n'): - """ - Split the text into paragraphs, each paragraph has less than max_token tokens. - """ - import re - from GeneralAgent import skills - pattern = "[" + re.escape(separators) + "]" - paragraphs = list(re.split(pattern, text)) - result = [] - current = '' - for paragraph in paragraphs: - if skills.string_token_count(current) + skills.string_token_count(paragraph) > max_token: - result.append(current) - current = '' - current += paragraph + '\n' - if len(current) > 0: - result.append(current) - new_result = [] - for x in result: - if skills.string_token_count(x) > max_token: - new_result.extend(split_text(x, max_token=max_token, separators=",。,.;;")) - else: - new_result.append(x) - new_result = [x.strip() for x in new_result if len(x.strip()) > 0] - return new_result \ No newline at end of file diff --git a/GeneralAgent/skills/text_is_english.py b/GeneralAgent/skills/text_is_english.py deleted file mode 100644 index 97f0aee..0000000 --- a/GeneralAgent/skills/text_is_english.py +++ /dev/null @@ -1,17 +0,0 @@ -def text_is_english(text): - """ - Check if a string is an English string. - """ - import string - # Remove all whitespace characters - text = ''.join(text.split()) - - # Check if all characters are in the ASCII range - if all(ord(c) < 128 for c in text): - # Check if the string contains any non-English characters - for c in text: - if c not in string.ascii_letters and c not in string.punctuation and c not in string.digits and c not in string.whitespace: - return False - return True - else: - return False \ No newline at end of file diff --git a/GeneralAgent/utils.py b/GeneralAgent/utils.py index 179ecfd..fb7f0cd 100644 --- a/GeneralAgent/utils.py +++ b/GeneralAgent/utils.py @@ -33,4 +33,41 @@ def encode_image(image_path): bin_data = base64.b64encode(image_file.read()).decode('utf-8') image_type = image_path.split('.')[-1].lower() virtural_url = f"data:image/{image_type};base64,{bin_data}" - return virtural_url \ No newline at end of file + return virtural_url + + +def messages_token_count(messages): + "Calculate and return the total number of tokens in the provided messages." + import tiktoken + encoding = tiktoken.get_encoding("cl100k_base") + tokens_per_message = 4 + tokens_per_name = 1 + num_tokens = 0 + for message in messages: + num_tokens += tokens_per_message + for key, value in message.items(): + if isinstance(value, str): + num_tokens += len(encoding.encode(value)) + if key == "name": + num_tokens += tokens_per_name + if isinstance(value, list): + for item in value: + if item["type"] == "text": + num_tokens += len(encoding.encode(item["text"])) + if item["type"] == "image_url": + num_tokens += (85 + 170 * 2 * 2) # 用最简单的模式来计算 + num_tokens += 3 # every reply is primed with <|start|>assistant<|message|> + return num_tokens + +def string_token_count(str): + """Calculate and return the token count in a given string.""" + import tiktoken + encoding = tiktoken.get_encoding("cl100k_base") + tokens = encoding.encode(str) + return len(tokens) + + +def cut_messages(messages, token_limit): + while messages_token_count(messages) > token_limit: + messages.pop(0) + return messages \ No newline at end of file diff --git a/examples/1_function_call.py b/examples/1_function_call.py index 68dd9fb..653a3ca 100644 --- a/examples/1_function_call.py +++ b/examples/1_function_call.py @@ -1,4 +1,6 @@ # 函数调用 +import logging +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s', handlers=[logging.StreamHandler()]) from GeneralAgent import Agent from dotenv import load_dotenv @@ -14,6 +16,7 @@ def get_weather(city: str) -> str: print(f"{city} weather: sunny") +# agent = Agent('你是一个天气小助手', functions=[get_weather], model='deepseek-chat') agent = Agent('你是一个天气小助手', functions=[get_weather]) agent.user_input('成都天气怎么样?') diff --git a/pyproject.toml b/pyproject.toml index 213e514..8d2f492 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "GeneralAgent" -version = "0.3.26" +version = "0.3.28" description = "General Agent: From LLM to Agent" authors = ["Chen Li <lichenarthurdata@gmail.com>"] license = "Apache 2.0" @@ -19,6 +19,7 @@ jinja2 = ">=3.1.2" numpy = ">=1.24.4" tiktoken = ">=0.5.1" llama-index =">=0.10.44" +codyer = ">=0.0.1" [tool.poetry.group.dev.dependencies] pytest = "^7.4.3"