diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46b113b --- /dev/null +++ b/.gitignore @@ -0,0 +1,180 @@ +langchain/ +guidance/ +Auto-GPT/ +Agents/ +utils/ + +/examples/log/* +working_dir/*.log +.vs/ +.vscode/ +.idea/ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +docs/docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints +notebooks/ + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +.venvs +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# macOS display setting files +.DS_Store + +# Wandb directory +wandb/ + +# asdf tool versions +.tool-versions +/.ruff_cache/ + +*.pkl +*.bin + +# integration test artifacts +data_map* +\[('_type', 'fake'), ('stop', None)] + +# Replit files +*replit* + +node_modules +docs/.yarn/ +docs/node_modules/ +docs/.docusaurus/ +docs/.cache-loader/ +docs/_dist +docs/api_reference/api_reference.rst +docs/api_reference/experimental_api_reference.rst +docs/api_reference/_build +docs/api_reference/*/ +!docs/api_reference/_static/ +!docs/api_reference/templates/ +!docs/api_reference/themes/ +docs/docs_skeleton/build +docs/docs_skeleton/node_modules +docs/docs_skeleton/yarn.lock diff --git a/config.json b/config.json new file mode 100644 index 0000000..a855cad --- /dev/null +++ b/config.json @@ -0,0 +1,5 @@ +{ + "model_name": "gpt-4-1106-preview", + "OPENAI_API_KEY": "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", + "OPENAI_ORGANIZATION": "" +} \ No newline at end of file diff --git a/friday/action/__init__.py b/friday/action/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/action/base_action.py b/friday/action/base_action.py new file mode 100644 index 0000000..440f77e --- /dev/null +++ b/friday/action/base_action.py @@ -0,0 +1,55 @@ +from typing import Optional + + +class BaseAction: + """Base class for all actions. + + Args: + description (str, optional): The description of the action. Defaults to + None. + name (str, optional): The name of the action. If None, the name will + be class name. Defaults to None. + """ + + def __init__(self, + description: Optional[str] = None, + name: Optional[str] = None, + timeout: int = 2, + action_type: Optional[str] = 'BASH') -> None: + if name is None: + name = self.__class__.__name__ + self._name = name + self._description = description + self._timeout = timeout + assert action_type in ['BASH', 'CODE', 'TOOL'] + self.action_type = action_type + + def __call__(self, *args, **kwargs): + raise NotImplementedError + + def _python(self, *lines): + return f'python -Bc "{"; ".join(lines)}"' + + def _import(self, *packages): + return f'from jarvis.{".".join(packages)} import *' + + @property + def timeout(self): + return self._timeout + + @property + def name(self): + return self._name + + @property + def description(self): + return self._description + + def __repr__(self): + return f'{self.name}:{self.description}' + + def __str__(self): + return self.__repr__() + +if __name__ == '__main__': + action = BaseAction() \ No newline at end of file diff --git a/friday/action/get_os_version.py b/friday/action/get_os_version.py new file mode 100644 index 0000000..b2a6b8a --- /dev/null +++ b/friday/action/get_os_version.py @@ -0,0 +1,33 @@ +import platform + +def get_os_version(): + system = platform.system() + + if system == "Darwin": + # macOS + return 'macOS ' + platform.mac_ver()[0] + elif system == "Linux": + try: + with open("/etc/os-release") as f: + lines = f.readlines() + for line in lines: + if line.startswith("PRETTY_NAME"): + return line.split("=")[1].strip().strip('"') + except FileNotFoundError: + pass + + return platform.version() + else: + return "Unknown Operating System" + + +def check_os_version(s): + if "mac" in s or "Ubuntu" in s or "CentOS" in s: + print("perating System Version:", s) + else: + raise ValueError("Unknown Operating System") + + +if __name__ == "__main__": + os_version = get_os_version() + print("Operating System Version:", os_version) \ No newline at end of file diff --git a/friday/action_lib/__init__.py b/friday/action_lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/action_lib/actions.json b/friday/action_lib/actions.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/friday/action_lib/actions.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/friday/action_lib/vectordb/chroma.sqlite3 b/friday/action_lib/vectordb/chroma.sqlite3 new file mode 100644 index 0000000..28da669 Binary files /dev/null and b/friday/action_lib/vectordb/chroma.sqlite3 differ diff --git a/friday/agent/__init__.py b/friday/agent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/agent/answer_agent.py b/friday/agent/answer_agent.py new file mode 100644 index 0000000..d76c47d --- /dev/null +++ b/friday/agent/answer_agent.py @@ -0,0 +1,31 @@ +from friday.core.llms import OpenAI + +QA_SYS_PROMPT=''' +You are a helpful ai assistant that can answer the questions asked by the user +with the help of the context provided by the user in a step by step manner. +If you don't know how to answer the user's question, answer "I don't know how to answer" instead of making up an answer. +''' +QA_USER_PROMPT=''' +context: {context} +question: {question} +''' +class AnswerAgent(): + ''' Answer is used to answer the question asked by the user''' + def __init__(self, config_path=None, open_api_doc_path = None) -> None: + super().__init__() + self.llm = OpenAI(config_path) + + # self.mac_systom_prompts = + + def generate_call_api_code(self, question,context="No context provided."): + self.sys_prompt = QA_SYS_PROMPT + self.user_prompt = QA_USER_PROMPT.format( + question = question, + context = context + ) + self.message = [ + {"role": "system", "content": self.sys_prompt}, + {"role": "user", "content": self.user_prompt}, + ] + return self.llm.chat(self.message) + \ No newline at end of file diff --git a/friday/agent/base_agent.py b/friday/agent/base_agent.py new file mode 100644 index 0000000..0a908d3 --- /dev/null +++ b/friday/agent/base_agent.py @@ -0,0 +1,52 @@ +from friday.action.base_action import BaseAction +import re +import json + + +class BaseAgent: + """ + BaseAgent is the base class of all agents. + """ + def __init__(self): + self.llm = None + self.environment = None + self.action_lib = None + self.max_iter = None + # self.action_lib_description = {} + # self.action = None + # self.retrieval_top_k = None + # self.action_lib_dir = None + # self.init_action_lib() + + # Extract information from text + def extract_information(self, message, begin_str='[BEGIN]', end_str='[END]'): + result = [] + _begin = message.find(begin_str) + _end = message.find(end_str) + while not (_begin == -1 or _end == -1): + result.append(message[_begin + len(begin_str):_end]) + message = message[_end + len(end_str):] + _begin = message.find(begin_str) + _end = message.find(end_str) + return result + + # egular expression to find JSON data within a string + def extract_json_from_string(self, text): + # Improved regular expression to find JSON data within a string + json_regex = r'```json\s*\n\{[\s\S]*?\n\}\s*```' + + # Search for JSON data in the text + matches = re.findall(json_regex, text) + + # Extract and parse the JSON data if found + if matches: + # Removing the ```json and ``` from the match to parse it as JSON + json_data = matches[0].replace('```json', '').replace('```', '').strip() + try: + # Parse the JSON data + parsed_json = json.loads(json_data) + return parsed_json + except json.JSONDecodeError as e: + return f"Error parsing JSON data: {e}" + else: + return "No JSON data found in the string." \ No newline at end of file diff --git a/friday/agent/format_agent.py b/friday/agent/format_agent.py new file mode 100644 index 0000000..b66a928 --- /dev/null +++ b/friday/agent/format_agent.py @@ -0,0 +1,60 @@ +from friday.action.get_os_version import get_os_version, check_os_version +from friday.core.llms import OpenAI + + + +_FORMAT_SYSTEM_PROMPT = ''' +You need to convert the following text to the format described in the format description. +''' +_FORMAT_USER_PROMPT = ''' +** Text ** +{text} +** Format Description ** +{format_description} +** Text After Conversion ** +''' + + + +class FormatAgent(): + """ + SkillCreator is used to generate new skills and store them in the action_lib. + """ + def __init__(self, config_path=None) -> None: + super().__init__() + self.llm = OpenAI(config_path) + self.model_name = self.llm.model_name + self.system_version = get_os_version() + try: + check_os_version(self.system_version) + except ValueError as e: + print(e) + # self.mac_systom_prompts = + + def convert_format(self, text,format_description): + self.sys_prompt = _FORMAT_SYSTEM_PROMPT + self.user_prompt = _FORMAT_USER_PROMPT.format( + text=text, + format_description=format_description + + ) + self.message = [ + {"role": "system", "content": self.sys_prompt}, + {"role": "user", "content": self.user_prompt}, + ] + res = self.llm.chat(self.message) + print(res) + return res + + +# text = ''' +# Zhiyong Wu Research Scientist Shanghai AI Laboratory Email: a@b, a=whucs2013wzy b=gmail.com] [Github] [Google Scholar] About me Hi! I am a research scientist at Shanghai AI Lab. I got my PhD degree from the University of Hong Kong at the end of 2021, affiliated with the HKU database group and NLP group. I am advised by Prof. Ben Kao. I am also working closely with Dr. Lingpeng Kong. Before that, I received my B.E. degree from the Dept. of Computer Science at Wuhan University in 2017. Throughout my graduate studies, I had great internships in Tencent AI Lab and Huawei Noah's Ark Lab. Hiring We have multiple full-time/internship positions available (focus on language agent and multilingual LLM), please feel free to hit me up with your CV or questions if interested. Research I am boardly interested in different topics in NLP. But at the moment, my research focus on exploring interesting (sometimes surprising) utilities of large language models: To synthesis datasets without human annotation. (ZeroGen, ProGen, SunGen) To explain model decision via natural language generation. (Neon, EIB) To learn a task without training by conditioning on in-context examples. (SAIL, CEIL, EvaLM, survey, OpenICL) I'm currently obsessed with the idea of \"LLM-powered autonomous agents\" and have multiple related projects underway. If you are also interested in this topic and have a plan to do an internship, feel free to hit me up via email. Research output of my interns Publications (*: equal contribution) Preprints In-Context Learning with Many Demonstration Examples Mukai Li, Shansan Gong, Jiangtao Feng, Yiheng Xu, Jun Zhang, Zhiyong Wu, Lingpeng Kong. [pdf]. A Survey on In-context Learning Qingxiu Dong, Lei Li, Damai Dai, Ce Zheng, Zhiyong Wu, Baobao Chang, Xu Sun, Jingjing Xu, Lei Li, Zhifang Sui [pdf]. Corex: Pushing the Boundaries of Complex Reasoning through Multi-Model Collaboration Qiushi Sun, Zhangyue Yin, Xiang Li, Zhiyong Wu, Xipeng Qiu, Lingpeng Kong [pdf]. EMO: Earth Mover Distance Optimization for Auto-Regressive Language Modeling Siyu Ren, Zhiyong Wu, Kenny Q Zhu [pdf]. 2023 Can We Edit Factual Knowledge by In-Context Learning? Ce Zheng, Lei Li, Qingxiu Dong, Yuxuan Fan, Zhiyong Wu, Jingjing Xu, Baobao Chang EMNLP 2023, Singapore, [pdf]. [code] DiffuSeq-v2: Bridging Discrete and Continuous Text Spaces for Accelerated Seq2Seq Diffusion Models Shansan Gong, Mukai Li, Jiangtao Feng, Zhiyong Wu, Lingpeng Kong. EMNLP 2023, Findings, Singapore, [pdf]. [code] Self-adaptive In-context Learning Zhiyong Wu*, Yaoxiang Wang*, Jiacheng Ye*, Lingpeng Kong. ACL 2023, Toronto, [pdf]. [code] OpenICL: An Open-Source Framework for In-context Learning Zhenyu Wu*, YaoXiang Wang*, Jiacheng Ye*, Jiangtao Feng, Jingjing Xu, Yu Qiao, Zhiyong Wu. ACL 2023, Toronto, Demo paper, [pdf]. [code] Explanation Regeneration via Information Bottleneck Qintong Li, Zhiyong Wu, Lingpeng Kong, Wei Bi. ACL 2023 Findings, Toronto, [pdf]. Compositional Exemplars for In-context Learning Jiacheng Ye, Zhiyong Wu, Jiangtao Feng, Tao Yu, Lingpeng Kong. ICML 2023, Hawaii, [pdf]. [code] DiffuSeq: Sequence to Sequence Text Generation with Diffusion Models Shansan Gong, Mukai Li, Jiangtao Feng, Zhiyong Wu, Lingpeng Kong. ICLR 2023, Rwanda, [pdf]. [code] Self-Guided High-Quality Data Generation in Efficient Zero-Shot Learning Jiahui Gao, Renjie Pi, Yong Lin, Hang Xu, Jiacheng Ye, Zhiyong Wu, Xiaodan Liang, Zhenguo Li, Lingpeng Kong. ICLR 2023, Rwanda, [pdf]. Unsupervised Explanation Generation via Correct Instantiations Sijie Chen, Zhiyong Wu, Jiangjie Chen, Zhixing Li, Yang Liu, and Lingpeng Kong AAAI 2023, Washington, [pdf]. [code] 2022 ProGen: Progressive Zero-shot Dataset Generation via In-context Feedback Jiacheng Ye, Jiahui Gao, Zhiyong Wu, Jiangtao Feng, Tao Yu, and Lingpeng Kong. EMNLP-Findings 2022, long paper.[pdf]. ZeroGen: Efficient Zero-shot Learning via Dataset Generation Jiacheng Ye*, Jiahui Gao*, Qintong Li, Hang Xu, Jiangtao Feng, Zhiyong Wu, Tao Yu and Lingpeng Kong. EMNLP 2022, long paper. [pdf]. [code] Lexical Knowledge Internalization for Neural Conversational Models Zhiyong Wu, Wei Bi, Xiang Li, Lingpeng Kong, Ben Kao. ACL 2022, long paper. [pdf]. [code] COLO: A Contrastive Learning based Re-ranking Framework for One-Stage Summarization Chenxin An, Ming Zhong, Zhiyong Wu, Qin Zhu, Xuanjing Huang, Xipeng Qiu. COLING 2022, long paper. [pdf]. [code] 2021 Good for Misconceived Reasons: An Empirical Revisiting on the Need for Visual Context in Multimodal Machine Translation Zhiyong Wu, Lingpeng Kong, Wei Bi, Xiang Li, Ben Kao. ACL 2021, long paper. [pdf] [code] Cascaded Head-colliding Attention Lin Zheng, Zhiyong Wu, Lingpeng Kong. ACL 2021, long paper. [pdf] [code] 2020 and before Perturbed Masking: Parameter-free Probing for Analyzing and Interpreting BERT Zhiyong Wu, Yun Chen, Ben Kao, Qun Liu. ACL 2020. [pdf] [code] PERQ: Predicting, Explaining, and Rectifying Failed Questions in KB-QA Systems Zhiyong Wu, Ben Kao, Tien-Hsuan Wu, Pengcheng Yin, Qun Liu. WSDM 2020, long paper. [pdf] Towards Practical Open Knowledge Base Canonicalization TTien-Hsuan Wu, Zhiyong Wu, Ben Kao, Pengcheng Yin. CIKM 2018. [pdf] Interns Jiacheng Ye EMNLP'22a, EMNLP'22b, ICML'23 Sijie Cheng AAAI'23 Yaoxiang Wang ACL'23a, ACL'23b Zhenyu Wu ACL'23b Siyu Ren Under review at ICLR'24 Qiushi Sun Under review at ICLR'24 Fangzhi Xu TBA Kanzhi Cheng TBA Yi Lu TBA +# ''' +# format_description = ''' +# Convert the text into Markdown Format, make it look like a personal blog +# ''' +# agent = FormatAgent("../../examples/config.json") +# print(agent.model_name) +# res = agent.convert_format(text,format_description) +# with open("test.txt","w") as f: +# f.write(res) \ No newline at end of file diff --git a/friday/agent/friday_agent.py b/friday/agent/friday_agent.py new file mode 100644 index 0000000..cff8f92 --- /dev/null +++ b/friday/agent/friday_agent.py @@ -0,0 +1,902 @@ +from friday.agent.base_agent import BaseAgent +from friday.core.action_node import ActionNode +from collections import defaultdict, deque +from friday.environment.py_env import PythonEnv +from friday.core.llms import OpenAI +from friday.core.action_manager import ActionManager +from friday.action.get_os_version import get_os_version, check_os_version +from friday.agent.prompt import prompt +from friday.core.utils import get_open_api_description_pair, get_open_api_doc_path +import re +import json +import logging + +class FridayAgent(BaseAgent): + """ AI agent class, including planning, retrieval and execution modules """ + + def __init__(self, config_path=None, action_lib_dir=None, max_iter=3): + super().__init__() + self.llm = OpenAI(config_path) + self.action_lib = ActionManager(config_path, action_lib_dir) + self.environment = PythonEnv() + self.prompt = prompt + self.system_version = get_os_version() + self.planner = PlanningModule(self.llm, self.environment, self.action_lib, self.prompt['planning_prompt'], self.system_version) + self.retriever = RetrievalModule(self.llm, self.environment, self.action_lib, self.prompt['retrieve_prompt']) + self.executor = ExecutionModule(self.llm, self.environment, self.action_lib, self.prompt['execute_prompt'], self.system_version, max_iter) + try: + check_os_version(self.system_version) + except ValueError as e: + print(e) + + def run(self, task): + """ + Run FridayAgent to execute task. + """ + # relevant action + retrieve_action_name = self.retriever.retrieve_action_name(task) + retrieve_action_description_pair = self.retriever.retrieve_action_description_pair(retrieve_action_name) + + # decompose task + self.planner.decompose_task(task, retrieve_action_description_pair) + + # iter each subtask + while self.planner.execute_list: + action = self.planner.execute_list[0] + action_node = self.planner.action_node[action] + description = action_node.description + logging.info("The current subtask is: {subtask}".format(subtask=description)) + code = '' + # The return value of the current task + result = '' + next_action = action_node.next_action + relevant_code = {} + type = action_node.type + pre_tasks_info = self.planner.get_pre_tasks_info(action) + if type == 'Code': + # retrieve existing action + retrieve_name = self.retriever.retrieve_action_name(description, 3) + relevant_code = self.retriever.retrieve_action_code_pair(retrieve_name) + # task execute step + if type == 'QA': + # result = self.executor.question_and_answer_action(pre_tasks_info, task, task) + if self.planner.action_num == 1: + result = self.executor.question_and_answer_action(pre_tasks_info, task, task) + else: + result = self.executor.question_and_answer_action(pre_tasks_info, task, description) + print(result) + logging.info(result) + else: + invoke = '' + if type == 'API': + api_path = self.executor.extract_API_Path(description) + code = self.executor.api_action(description, api_path, pre_tasks_info) + else: + code, invoke = self.executor.generate_action(action, description, pre_tasks_info, relevant_code) + # Execute python tool class code + state = self.executor.execute_action(code, invoke, type) + result = state.result + logging.info(state) + # Check whether the code runs correctly, if not, amend the code + if type == 'Code': + need_mend = False + trial_times = 0 + critique = '' + score = 0 + # If no error is reported, check whether the task is completed + if state.error == None: + critique, judge, score = self.executor.judge_action(code, description, state, next_action) + if not judge: + print("critique: {}".format(critique)) + need_mend = True + else: + # Determine whether it is caused by an error outside the code + reasoning, error_type = self.executor.analysis_action(code, description, state) + if error_type == 'replan': + relevant_action_name = self.retriever.retrieve_action_name(reasoning) + relevant_action_description_pair = self.retriever.retrieve_action_description_pair(relevant_action_name) + self.planner.replan_task(reasoning, action, relevant_action_description_pair) + continue + need_mend = True + # The code failed to complete its task, fix the code + while (trial_times < self.executor.max_iter and need_mend == True): + trial_times += 1 + print("current amend times: {}".format(trial_times)) + new_code, invoke = self.executor.amend_action(code, description, state, critique, pre_tasks_info) + critique = '' + code = new_code + # Run the current code and check for errors + state = self.executor.execute_action(code, invoke, type) + result = state.result + logging.info(state) + # print(state) + # Recheck + if state.error == None: + critique, judge, score = self.executor.judge_action(code, description, state, next_action) + # The task execution is completed and the loop exits + if judge: + need_mend = False + break + # print("critique: {}".format(critique)) + else: # The code still needs to be corrected + need_mend = True + + # If the task still cannot be completed, an error message will be reported. + if need_mend == True: + print("I can't Do this Task!!") + break + else: # The task is completed, if code is save the code, args_description, action_description in lib + if score >= 8: + self.executor.store_action(action, code) + print("Current task execution completed!!!") + self.planner.update_action(action, result, relevant_code, True, type) + self.planner.execute_list.remove(action) + + +class PlanningModule(BaseAgent): + """ The planning module is responsible for breaking down complex tasks into subtasks, re-planning, etc. """ + + def __init__(self, llm, environment, action_lib, prompt, system_version): + """ + Module initialization, including setting the execution environment, initializing prompts, etc. + """ + super().__init__() + # Model, environment, database + self.llm = llm + self.environment = environment + self.action_lib = action_lib + self.system_version = system_version + self.prompt = prompt + # Action nodes, action graph information and action topology sorting + self.action_num = 0 + self.action_node = {} + self.action_graph = defaultdict(list) + self.execute_list = [] + + def decompose_task(self, task, action_description_pair): + """ + Implement task disassembly logic. + """ + files_and_folders = self.environment.list_working_dir() + action_description_pair = json.dumps(action_description_pair) + response = self.task_decompose_format_message(task, action_description_pair, files_and_folders) + decompose_json = self.extract_json_from_string(response) + # Building action graph and topological ordering of actions + self.create_action_graph(decompose_json) + self.topological_sort() + + def replan_task(self, reasoning, current_task, relevant_action_description_pair): + """ + replan new task to origin action graph . + """ + # current_task information + current_action = self.action_node[current_task] + current_task_description = current_action.description + relevant_action_description_pair = json.dumps(relevant_action_description_pair) + files_and_folders = self.environment.list_working_dir() + response = self.task_replan_format_message(reasoning, current_task, current_task_description, relevant_action_description_pair, files_and_folders) + new_action = self.extract_json_from_string(response) + # add new action to action graph + self.add_new_action(new_action, current_task) + # update topological sort + self.topological_sort() + + def update_action(self, action, return_val='', relevant_code=None, status=False, type='Code'): + """ + Update action node info. + """ + if return_val: + if type=='Code': + return_val = self.extract_information(return_val, "", "") + print("**************************************************") + logging.info(return_val) + print(return_val) + print("*************************************************") + if return_val != 'None': + self.action_node[action]._return_val = return_val + if relevant_code: + self.action_node[action]._relevant_code = relevant_code + self.action_node[action]._status = status + + def task_decompose_format_message(self, task, action_list, files_and_folders): + """ + Send decompse task prompt to LLM and get task list. + """ + api_list = get_open_api_description_pair() + sys_prompt = self.prompt['_SYSTEM_TASK_DECOMPOSE_PROMPT'] + user_prompt = self.prompt['_USER_TASK_DECOMPOSE_PROMPT'].format( + system_version=self.system_version, + task=task, + action_list = action_list, + api_list = api_list, + working_dir = self.environment.working_dir, + files_and_folders = files_and_folders + ) + self.message = [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": user_prompt}, + ] + return self.llm.chat(self.message) + + def task_replan_format_message(self, reasoning, current_task, current_task_description, action_list, files_and_folders): + """ + Send replan task prompt to LLM and get task list. + """ + sys_prompt = self.prompt['_SYSTEM_TASK_REPLAN_PROMPT'] + user_prompt = self.prompt['_USER_TASK_REPLAN_PROMPT'].format( + current_task = current_task, + current_task_description = current_task_description, + system_version=self.system_version, + reasoning = reasoning, + action_list = action_list, + working_dir = self.environment.working_dir, + files_and_folders = files_and_folders + ) + self.message = [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": user_prompt}, + ] + return self.llm.chat(self.message) + + def get_action_list(self, relevant_action=None): + """ + Get action list, including action names and descriptions. + """ + action_dict = self.action_lib.descriptions + if not relevant_action: + return json.dumps(action_dict) + relevant_action_dict = {action : description for action ,description in action_dict.items() if action in relevant_action} + relevant_action_list = json.dumps(relevant_action_dict) + return relevant_action_list + + def create_action_graph(self, decompose_json): + """ + Creates a action graph from a list of dependencies. + """ + # generate execte graph + for _, task_info in decompose_json.items(): + self.action_num += 1 + task_name = task_info['name'] + task_description = task_info['description'] + task_type = task_info['type'] + task_dependencies = task_info['dependencies'] + self.action_node[task_name] = ActionNode(task_name, task_description, task_type) + self.action_graph[task_name] = task_dependencies + for pre_action in self.action_graph[task_name]: + self.action_node[pre_action].next_action[task_name] = task_description + + + def add_new_action(self, new_task_json, current_task): + """ + Creates a action graph from a list of dependencies. + """ + # update execte graph + for _, task_info in new_task_json.items(): + self.action_num += 1 + task_name = task_info['name'] + task_description = task_info['description'] + task_type = task_info['type'] + task_dependencies = task_info['dependencies'] + self.action_node[task_name] = ActionNode(task_name, task_description, task_type) + self.action_graph[task_name] = task_dependencies + for pre_action in self.action_graph[task_name]: + self.action_node[pre_action].next_action[task_name] = task_description + last_new_task = list(new_task_json.keys())[-1] + self.action_graph[current_task].append(last_new_task) + + def topological_sort(self): + """ + generate graph topological sort. + """ + # init execute list + self.execute_list = [] + graph = defaultdict(list) + for node, dependencies in self.action_graph.items(): + # If the current node has not been executed, put it in the dependency graph. + if not self.action_node[node].status: + graph.setdefault(node, []) + for dependent in dependencies: + # If the dependencies of the current node have not been executed, put them in the dependency graph. + if not self.action_node[dependent].status: + graph[dependent].append(node) + + in_degree = {node: 0 for node in graph} + # Count in-degree for each node + for node in graph: + for dependent in graph[node]: + in_degree[dependent] += 1 + + # Initialize queue with nodes having in-degree 0 + queue = deque([node for node in in_degree if in_degree[node] == 0]) + + # List to store the order of execution + + while queue: + # Get one node with in-degree 0 + current = queue.popleft() + self.execute_list.append(current) + + # Decrease in-degree for all nodes dependent on current + for dependent in graph[current]: + in_degree[dependent] -= 1 + if in_degree[dependent] == 0: + queue.append(dependent) + + # Check if topological sort is possible (i.e., no cycle) + if len(self.execute_list) == len(graph): + print("topological sort is possible") + else: + return "Cycle detected in the graph, topological sort not possible." + + def get_pre_tasks_info(self, current_task): + """ + Get string information of the prerequisite task for the current task. + """ + pre_tasks_info = {} + for task in self.action_graph[current_task]: + task_info = { + "description" : self.action_node[task].description, + "return_val" : self.action_node[task].return_val + } + pre_tasks_info[task] = task_info + pre_tasks_info = json.dumps(pre_tasks_info) + return pre_tasks_info + + + +class RetrievalModule(BaseAgent): + """ Retrieval module, responsible for retrieving available actions in the action library. """ + + def __init__(self, llm, environment, action_lib, prompt): + """ + Module initialization, including setting the execution environment, initializing prompts, etc. + """ + super().__init__() + # Model, environment, database + self.llm = llm + self.environment = environment + self.action_lib = action_lib + self.prompt = prompt + + def delete_action(self, action): + """ + Delete relevant action content, including code, description, parameter information, etc. + """ + self.action_lib.delete_action(action) + + def retrieve_action_name(self, task, k=10): + """ + Implement retrieval action name logic + """ + retrieve_action_name = self.action_lib.retrieve_action_name(task, k) + return retrieve_action_name + + def action_code_filter(self, action_code_pair, task): + """ + Implement filtering of search codes. + """ + action_code_pair = json.dumps(action_code_pair) + response = self.action_code_filter_format_message(action_code_pair, task) + action_name = self.extract_information(response, '', '')[0] + code = '' + if action_name: + code = self.action_lib.get_action_code(action_name) + return code + + def retrieve_action_description(self, action_name): + """ + Implement search action description logic. + """ + retrieve_action_description = self.action_lib.retrieve_action_description(action_name) + return retrieve_action_description + + def retrieve_action_code(self, action_name): + """ + Implement retrieval action code logic. + """ + retrieve_action_code = self.action_lib.retrieve_action_code(action_name) + return retrieve_action_code + + def retrieve_action_code_pair(self, retrieve_action_name): + """ + Retrieve task code pairs. + """ + retrieve_action_code = self.retrieve_action_code(retrieve_action_name) + action_code_pair = {} + for name, description in zip(retrieve_action_name, retrieve_action_code): + action_code_pair[name] = description + return action_code_pair + + def retrieve_action_description_pair(self, retrieve_action_name): + """ + Retrieve task description pairs. + """ + retrieve_action_description = self.retrieve_action_description(retrieve_action_name) + action_description_pair = {} + for name, description in zip(retrieve_action_name, retrieve_action_description): + action_description_pair[name] = description + return action_description_pair + + def action_code_filter_format_message(self, action_code_pair, task_description): + """ + Send aciton code to llm to filter useless action codes. + """ + sys_prompt = self.prompt['_SYSTEM_ACTION_CODE_FILTER_PROMPT'] + user_prompt = self.prompt['_USER_ACTION_CODE_FILTER_PROMPT'].format( + task_description=task_description, + action_code_pair=action_code_pair + ) + self.message = [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": user_prompt}, + ] + return self.llm.chat(self.message) + + +class ExecutionModule(BaseAgent): + """ Execution module, responsible for executing actions and updating the action library """ + + def __init__(self, llm, environment, action_lib, prompt, system_version, max_iter): + ''' + Module initialization, including setting the execution environment, initializing prompts, etc. + ''' + super().__init__() + self.llm = llm + self.environment = environment + self.action_lib = action_lib + self.system_version = system_version + self.prompt = prompt + self.max_iter = max_iter + self.open_api_doc_path = get_open_api_doc_path() + self.open_api_doc = {} + with open(self.open_api_doc_path) as f: + self.open_api_doc = json.load(f) + + def generate_action(self, task_name, task_description, pre_tasks_info, relevant_code): + ''' + Generate action code logic, generate code that can complete the action and its calls. + ''' + relevant_code = json.dumps(relevant_code) + create_msg = self.skill_create_and_invoke_format_message(task_name, task_description, pre_tasks_info, relevant_code) + code = self.extract_python_code(create_msg) + invoke = self.extract_information(create_msg, begin_str='', end_str='')[0] + return code, invoke + + # def generate_action(self, task_name, task_description): + # ''' + # Generate action code logic, generate code that can complete the action and its calls. + # ''' + # create_msg = self.skill_create_format_message(task_name, task_description) + # code = self.extract_python_code(create_msg) + # return code + + def execute_action(self, code, invoke, type): + ''' + Implement action execution logic. + instantiate the action class and execute it, and return the execution completed status. + ''' + # print result info + if type == 'Code': + info = "\n" + '''print("")''' + "\n" + "print(result)" + "\n" + '''print("")''' + code = code + '\nresult=' + invoke + info + print("**************************************************") + print(code) + print("*************************************************") + state = self.environment.step(code) + print("**************************************************") + print(state) + # print("error: " + state.error + "\nresult: " + state.result + "\npwd: " + state.pwd + "\nls: " + state.ls) + print("*************************************************") + return state + + # def execute_action(self, code, task_description, pre_tasks_info): + # ''' + # Implement action execution logic. + # instantiate the action class and execute it, and return the execution completed status. + # ''' + # invoke_msg = self.invoke_generate_format_message(code, task_description, pre_tasks_info) + # invoke = self.extract_information(invoke_msg, begin_str='', end_str='')[0] + # # print result info + # info = "\n" + '''print("")''' + "\n" + "print(result)" + "\n" + '''print("")''' + # code = code + '\nresult=' + invoke + info + # print("**************************************************") + # print(code) + # print("*************************************************") + # state = self.environment.step(code) + # print("**************************************************") + # print(state) + # print("*************************************************") + # return state + + def judge_action(self, code, task_description, state, next_action): + ''' + Implement action judgment logic. + judge whether the action completes the current task, and return the JSON result of the judgment. + ''' + judge_json = self.task_judge_format_message(code, task_description, state.result, state.pwd, state.ls, next_action) + reasoning = judge_json['reasoning'] + judge = judge_json['judge'] + score = judge_json['score'] + return reasoning, judge, score + + def amend_action(self, current_code, task_description, state, critique, pre_tasks_info): + ''' + Implement action repair logic. + repair unfinished tasks or erroneous code, and return the repaired code and call. + ''' + amend_msg = self.skill_amend_and_invoke_format_message(current_code, task_description, state.error, state.result, state.pwd, state.ls, critique, pre_tasks_info) + new_code = self.extract_python_code(amend_msg) + invoke = self.extract_information(amend_msg, begin_str='', end_str='')[0] + return new_code, invoke + + # def amend_action(self, current_code, task_description, state, critique): + # ''' + # Implement action repair logic. + # repair unfinished tasks or erroneous code, and return the repaired code and call. + # ''' + # amend_msg = self.skill_amend_format_message(current_code, task_description, state.error, state.result, state.pwd, state.ls, critique) + # new_code = self.extract_python_code(amend_msg) + # return new_code + + def analysis_action(self, code, task_description, state): + ''' + Implement the analysis of code errors. + If it is an environmental error that requires new operations, go to the planning module. + Otherwise, hand it to amend_action and return JSON. + ''' + analysis_json = self.error_analysis_format_message(code, task_description, state.error, state.pwd, state.ls) + reasoning = analysis_json['reasoning'] + type = analysis_json['type'] + return reasoning, type + + def store_action(self, action, code): + """ + Store action code and info. + + """ + # If action not in db. + if not self.action_lib.exist_action(action): + # Implement action storage logic and store new actions + args_description = self.extract_args_description(code) + action_description = self.extract_action_description(code) + # Save action name, code, and description to JSON + action_info = self.save_action_info_to_json(action, code, action_description) + # Save code and descriptions to databases and JSON files + self.action_lib.add_new_action(action_info) + # Parameter description save path + args_description_file_path = self.action_lib.action_lib_dir + '/args_description/' + action + '.txt' + # save args_description + self.save_str_to_path(args_description, args_description_file_path) + else: + print("action already exists!") + + + def api_action(self, description, api_path, context="No context provided."): + """ + Call api tool to execute task. + """ + response = self.generate_call_api_format_message(description, api_path, context) + code = self.extract_python_code(response) + return code + + def question_and_answer_action(self, context, question, current_question=None): + """ + Answer questions based on the information found. + """ + response = self.question_and_answer_format_message(context, question, current_question) + return response + + def skill_create_and_invoke_format_message(self, task_name, task_description, pre_tasks_info, relevant_code): + """ + Send skill generate and invoke message to LLM. + """ + sys_prompt = self.prompt['_SYSTEM_SKILL_CREATE_AND_INVOKE_PROMPT'] + user_prompt = self.prompt['_USER_SKILL_CREATE_AND_INVOKE_PROMPT'].format( + system_version=self.system_version, + task_description=task_description, + working_dir= self.environment.working_dir, + task_name=task_name, + pre_tasks_info=pre_tasks_info, + relevant_code=relevant_code + ) + self.message = [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": user_prompt}, + ] + return self.llm.chat(self.message) + + def skill_create_format_message(self, task_name, task_description): + """ + Send skill create message to LLM. + """ + sys_prompt = self.prompt['_SYSTEM_SKILL_CREATE_PROMPT'] + user_prompt = self.prompt['_USER_SKILL_CREATE_PROMPT'].format( + system_version=self.system_version, + task_description=task_description, + working_dir= self.environment.working_dir, + task_name=task_name + ) + self.message = [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": user_prompt}, + ] + return self.llm.chat(self.message) + + def invoke_generate_format_message(self, class_code, task_description, pre_tasks_info): + """ + Send invoke generate message to LLM. + """ + class_name, args_description = self.extract_class_name_and_args_description(class_code) + sys_prompt = self.prompt['_SYSTEM_INVOKE_GENERATE_PROMPT'] + user_prompt = self.prompt['_USER_INVOKE_GENERATE_PROMPT'].format( + class_name = class_name, + task_description = task_description, + args_description = args_description, + pre_tasks_info = pre_tasks_info, + working_dir = self.environment.working_dir + ) + self.message = [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": user_prompt}, + ] + return self.llm.chat(self.message) + + def question_and_answer_format_message(self, context, question, current_question): + """ + Send QA message to LLM. + """ + sys_prompt = self.prompt['_SYSTEM_QA_PROMPT'] + user_prompt = self.prompt['_USER_QA_PROMPT'].format( + context = context, + question = question, + current_question = current_question + ) + self.message = [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": user_prompt}, + ] + return self.llm.chat(self.message) + + def skill_amend_and_invoke_format_message(self, original_code, task, error, code_output, current_working_dir, files_and_folders, critique, pre_tasks_info): + """ + Send skill amend message to LLM. + """ + sys_prompt = self.prompt['_SYSTEM_SKILL_AMEND_AND_INVOKE_PROMPT'] + user_prompt = self.prompt['_USER_SKILL_AMEND_AND_INVOKE_PROMPT'].format( + original_code = original_code, + task = task, + error = error, + code_output = code_output, + current_working_dir = current_working_dir, + working_dir= self.environment.working_dir, + files_and_folders = files_and_folders, + critique = critique, + pre_tasks_info = pre_tasks_info + ) + self.message = [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": user_prompt}, + ] + return self.llm.chat(self.message) + + def skill_amend_format_message(self, original_code, task, error, code_output, current_working_dir, files_and_folders, critique): + """ + Send skill amend message to LLM. + """ + sys_prompt = self.prompt['_SYSTEM_SKILL_AMEND_PROMPT'] + user_prompt = self.prompt['_USER_SKILL_AMEND_PROMPT'].format( + original_code = original_code, + task = task, + error = error, + code_output = code_output, + current_working_dir = current_working_dir, + working_dir= self.environment.working_dir, + files_and_folders = files_and_folders, + critique = critique + ) + self.message = [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": user_prompt}, + ] + return self.llm.chat(self.message) + + def task_judge_format_message(self, current_code, task, code_output, current_working_dir, files_and_folders, next_action): + """ + Send task judge prompt to LLM and get JSON response. + """ + next_action = json.dumps(next_action) + sys_prompt = self.prompt['_SYSTEM_TASK_JUDGE_PROMPT'] + user_prompt = self.prompt['_USER_TASK_JUDGE_PROMPT'].format( + current_code=current_code, + task=task, + code_output=code_output, + current_working_dir=current_working_dir, + working_dir=self.environment.working_dir, + files_and_folders=files_and_folders, + next_action=next_action + ) + self.message = [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": user_prompt}, + ] + response =self.llm.chat(self.message) + judge_json = self.extract_json_from_string(response) + print("**************************************************") + print(judge_json) + print("*************************************************") + return judge_json + + def error_analysis_format_message(self, current_code, task, code_error, current_working_dir, files_and_folders): + """ + Send error analysis prompt to LLM and get JSON response. + """ + sys_prompt = self.prompt['_SYSTEM_ERROR_ANALYSIS_PROMPT'] + user_prompt = self.prompt['_USER_ERROR_ANALYSIS_PROMPT'].format( + current_code=current_code, + task=task, + code_error=code_error, + current_working_dir=current_working_dir, + working_dir= self.environment.working_dir, + files_and_folders= files_and_folders + ) + self.message = [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": user_prompt}, + ] + response =self.llm.chat(self.message) + analysis_json = self.extract_json_from_string(response) + print("**************************************************") + print(analysis_json) + print("*************************************************") + return analysis_json + + def extract_python_code(self, response): + """ + Extract python code from response. + """ + python_code = "" + if '```python' in response: + python_code = response.split('```python')[1].split('```')[0] + elif '```' in python_code: + python_code = response.split('```')[1].split('```')[0] + return python_code + + def extract_class_name_and_args_description(self, class_code): + """ + Extract class_name and args description from python code. + """ + class_name_pattern = r"class (\w+)" + class_name_match = re.search(class_name_pattern, class_code) + class_name = class_name_match.group(1) if class_name_match else None + + # Extracting the __call__ method's docstring + call_method_docstring_pattern = r"def __call__\([^)]*\):\s+\"\"\"(.*?)\"\"\"" + call_method_docstring_match = re.search(call_method_docstring_pattern, class_code, re.DOTALL) + args_description = call_method_docstring_match.group(1).strip() if call_method_docstring_match else None + + return class_name, args_description + + def extract_args_description(self, class_code): + """ + Extract args description from python code. + """ + # Extracting the __call__ method's docstring + call_method_docstring_pattern = r"def __call__\([^)]*\):\s+\"\"\"(.*?)\"\"\"" + call_method_docstring_match = re.search(call_method_docstring_pattern, class_code, re.DOTALL) + args_description = call_method_docstring_match.group(1).strip() if call_method_docstring_match else None + return args_description + + def extract_action_description(self, class_code): + """ + Extract action description from python code. + """ + # Extracting the __init__ method's description + init_pattern = r"def __init__\s*\(self[^)]*\):\s*(?:.|\n)*?self\._description\s*=\s*\"([^\"]+)\"" + action_match = re.search(init_pattern, class_code, re.DOTALL) + action_description = action_match.group(1).strip() if action_match else None + return action_description + + def save_str_to_path(self, content, path): + """ + save str content to the specified path. + """ + with open(path, 'w', encoding='utf-8') as f: + lines = content.strip().splitlines() + content = '\n'.join(lines) + f.write(content) + + def save_action_info_to_json(self, action, code, description): + """ + save action info to json. + """ + info = { + "task_name" : action, + "code": code, + "description": description + } + return info + + def generate_call_api_format_message(self, tool_sub_task, tool_api_path, context="No context provided."): + self.sys_prompt = self.prompt['_SYSTEM_TOOL_USAGE_PROMPT'].format( + openapi_doc = json.dumps(self.generate_openapi_doc(tool_api_path)), + tool_sub_task = tool_sub_task, + context = context + ) + self.user_prompt = self.prompt['_USER_TOOL_USAGE_PROMPT'] + self.message = [ + {"role": "system", "content": self.sys_prompt}, + {"role": "user", "content": self.user_prompt}, + ] + return self.llm.chat(self.message) + + def generate_openapi_doc(self, tool_api_path): + """ + Format openapi document. + """ + # init current api's doc + curr_api_doc = {} + curr_api_doc["openapi"] = self.open_api_doc["openapi"] + curr_api_doc["info"] = self.open_api_doc["info"] + curr_api_doc["paths"] = {} + curr_api_doc["components"] = {"schemas":{}} + api_path_doc = {} + #extract path and schema + if tool_api_path not in self.open_api_doc["paths"]: + curr_api_doc = {"error": "The api is not existed"} + return curr_api_doc + api_path_doc = self.open_api_doc["paths"][tool_api_path] + curr_api_doc["paths"][tool_api_path] = api_path_doc + find_ptr = {} + if "get" in api_path_doc: + findptr = api_path_doc["get"] + elif "post" in api_path_doc: + findptr = api_path_doc["post"] + api_params_schema_ref = "" + # json格式 + if (("requestBody" in findptr) and + ("content" in findptr["requestBody"]) and + ("application/json" in findptr["requestBody"]["content"]) and + ("schema" in findptr["requestBody"]["content"]["application/json"]) and + ("$ref" in findptr["requestBody"]["content"]["application/json"]["schema"])): + api_params_schema_ref = findptr["requestBody"]["content"]["application/json"]["schema"]["$ref"] + elif (("requestBody" in findptr) and + ("content" in findptr["requestBody"]) and + ("multipart/form-data" in findptr["requestBody"]["content"]) and + ("schema" in findptr["requestBody"]["content"]["multipart/form-data"]) and + ("allOf" in findptr["requestBody"]["content"]["multipart/form-data"]["schema"]) and + ("$ref" in findptr["requestBody"]["content"]["multipart/form-data"]["schema"]["allOf"][0])): + api_params_schema_ref = findptr["requestBody"]["content"]["multipart/form-data"]["schema"]["allOf"][0]["$ref"] + if api_params_schema_ref != None and api_params_schema_ref != "": + curr_api_doc["components"]["schemas"][api_params_schema_ref.split('/')[-1]] = self.open_api_doc["components"]["schemas"][api_params_schema_ref.split('/')[-1]] + return curr_api_doc + + def extract_API_Path(self, text): + """ + Extracts UNIX-style and Windows-style paths from the given string, + handling paths that may be enclosed in quotes. + + :param s: The string from which to extract paths. + :return: A list of extracted paths. + """ + # Regular expression for UNIX-style and Windows-style paths + unix_path_pattern = r"/[^/\s]+(?:/[^/\s]*)*" + windows_path_pattern = r"[a-zA-Z]:\\(?:[^\\\/\s]+\\)*[^\\\/\s]+" + + # Combine both patterns + pattern = f"({unix_path_pattern})|({windows_path_pattern})" + + # Find all matches + matches = re.findall(pattern, text) + + # Extract paths from the tuples returned by findall + paths = [match[0] or match[1] for match in matches] + + # Remove enclosing quotes (single or double) from the paths + stripped_paths = [path.strip("'\"") for path in paths] + return stripped_paths[0] + + + +if __name__ == '__main__': + agent = FridayAgent(config_path='../../examples/config.json', action_lib_dir="friday/action_lib") + print(agent.executor.extract_API_Path('''Use the "/tools/arxiv' API to search for the autogen paper and retrieve its summary.''')) diff --git a/friday/agent/linux_invoke_generator.py b/friday/agent/linux_invoke_generator.py new file mode 100644 index 0000000..4cf5ac3 --- /dev/null +++ b/friday/agent/linux_invoke_generator.py @@ -0,0 +1,100 @@ +from friday.action.get_os_version import get_os_version, check_os_version +import re +from friday.core.llms import OpenAI + +_LINUX_SYSTEM_INVOKE_GENERATOR_PROMPT = ''' +You are an AI trained to assist with Python programming tasks, with a focus on class and method usage. +Your goal is to generate a Python __call__ method invocation statement based on provided class names, task descriptions, and method parameter details. +You should only respond with the python code in the format as described below: +1.Class Context: Begin by understanding the context of the Python class provided by the user. This includes grasping the class name and its intended functionality. +2.Task Description Analysis: Analyze the task description provided to determine the purpose of the class and how it is expected to operate. This will help in identifying the correct method of the class to invoke. +3.Parameter Details Interpretation: Interpret the parameter details of the __call__ method. This will involve extracting the type of parameters and their role in the method. +4.Generating Invocation Statement: Construct the __call__ method invocation statement. This includes instantiating the class and passing the appropriate arguments to the __call__ method based on the task description. For example, if my class is called abc, and its __call__ method takes parameters 1 and 2, then my call statement could be abc()(1,2) +5.Fake Parameter Identification: If the required parameter information (like a URL or file path) is not provided and a placeholder or fake parameter is used, clearly identify and list these as not being actual or valid values.All the fake paramters you list should be separated by comma.If there are no fake parameters,you should give a None. +6.Output Format: The final output should include two parts:The first one is the invocation statement,which will be enclosed in tags.The second one is all the fake parameters you identified, which will be enclosed in tags. +And the response you write should also follow the following criteria: +Criteria: +1.The __call__ method invocation must be syntactically correct as per Python standards. +2.Clearly identify any fake or placeholder parameters used in the invocation. +3.Encourage generating a realistic and functional code snippet wherever possible. +4. If necessary, you can use the working directory provided by the user as a parameter passed into the __call__ method. +Now you will be provided with the following information, please generate your response according to these information: +''' +_LINUX_USER_INVOKE_GENERATOR_PROMPT = ''' +User's Information: +Class Name: {class_name} +Task Description: {task_description} +__call__ Method Parameters: {args_description} +Working Directory: {working_dir} +''' + + +class LinuxInvokeGenerator(): + + def __init__(self, config_path=None) -> None: + super().__init__() + self.llm = OpenAI(config_path) + self.system_version = get_os_version() + try: + check_os_version(self.system_version) + except ValueError as e: + print(e) + + # Generate calls for the selected tool class + def invoke_generator(self, class_code, task_description,working_dir): + class_name, args_description = self.extract_class_name_and_args_description(class_code) + self.sys_prompt = _LINUX_SYSTEM_INVOKE_GENERATOR_PROMPT + self.user_prompt = _LINUX_USER_INVOKE_GENERATOR_PROMPT.format( + class_name = class_name, + task_description = task_description, + args_description = args_description, + working_dir = working_dir + ) + self.message = [ + {"role": "system", "content": self.sys_prompt}, + {"role": "user", "content": self.user_prompt}, + ] + return self.llm.chat(self.message) + + # extract class_name and args description from python code + def extract_class_name_and_args_description(self, class_code): + """ + Extracts the class name and comments from the __call__ method of a given Python class code. + Specifically extracts the class name and the content between 'Args:' and 'Returns:' in the __call__ method. + + Args: + class_code (str): The string representation of the Python class code. + + Returns: + tuple: A tuple containing the class name and the extracted comments between 'Args:' and 'Returns:', + or None for each if not found. + """ + # # Extracting the class name + # class_name_pattern = re.compile(r'class\s+(\w+)') + # class_name_match = class_name_pattern.search(class_code) + # class_name = class_name_match.group(1) if class_name_match else None + + # # Pattern to match __call__ method and its docstring + # call_method_pattern = re.compile(r'def __call__\s*\(self, .*?\):\s*"""(.*?)"""', re.DOTALL) + # call_method_match = call_method_pattern.search(class_code) + + # if call_method_match: + # docstring = call_method_match.group(1) + # # Extracting the part between Args: and Returns: + # args_to_return_pattern = re.compile(r'Args:(.*?)Returns:', re.DOTALL) + # args_to_return_match = args_to_return_pattern.search(docstring) + + # call_args_comments = args_to_return_match.group(1).strip() if args_to_return_match else None + # else: + # call_args_comments = None + # Extracting the class name + class_name_pattern = r"class (\w+)" + class_name_match = re.search(class_name_pattern, class_code) + class_name = class_name_match.group(1) if class_name_match else None + + # Extracting the __call__ method's docstring + call_method_docstring_pattern = r"def __call__\([^)]*\):\s+\"\"\"(.*?)\"\"\"" + call_method_docstring_match = re.search(call_method_docstring_pattern, class_code, re.DOTALL) + call_method_docstring = call_method_docstring_match.group(1).strip() if call_method_docstring_match else None + + return class_name, call_method_docstring diff --git a/friday/agent/linux_skill_amend.py b/friday/agent/linux_skill_amend.py new file mode 100644 index 0000000..ee0d1c8 --- /dev/null +++ b/friday/agent/linux_skill_amend.py @@ -0,0 +1,69 @@ +from friday.action.get_os_version import get_os_version, check_os_version +from friday.core.llms import OpenAI + +_LINUX_SYSTEM_AMEND_PROMPT = ''' +You are an AI expert in Python programming, with a focus on diagnosing and resolving code issues. +Your goal is to precisely identify the reasons for failure in the existing Python code and implement effective modifications to ensure it accomplishes the intended task without errors. + +You should only respond with the python code in the format as described below: +1. Modified Code: Based on the error analysis, modify the original code to fix all the problems and give the final correct code to the user. +2. Error Analysis: Conduct a step-by-step analysis to identify why the code is generating errors or failing to complete the task. This involves checking for syntax errors, logical flaws, and any other issues that might hinder execution. +3. Detailed Explanation: Offer a clear and comprehensive explanation for each identified issue, detailing why these problems are occurring and how they are impacting the code's functionality. +And the code you write should also follow the following criteria: +1. You must keep the original code as formatted as possible, e.g. class names, methods, etc. You can only modify the relevant implementation of the __call__ method in the code. +2. Please avoid throwing exceptions in your modified code which may result in the execution of your code consistently reporting errors.You should instead handle the caught exceptions! +3. Some errors may be caused by unreasonable tasks by the user that result in something other than what is expected, e.g. the file to be created already exists, the parameters passed in are wrong, etc. You need to do some fault tolerance or exception handling for this to prevent it from reporting further errors. +4. Ensure the final code is syntactically correct, optimized for performance, and follows Python best practices.And the final code can only contain the class definition, the rest of the code about class instantiation and invocation must be commented out. +5. The python code should be surrounded by ```python and ```. +6. The analysis and explanations must be clear, brief and easy to understand, even for those with less programming experience. +7. All modifications must address the specific issues identified in the error analysis. +8. The solution must enable the code to successfully complete the intended task without errors. +Now you will be provided with the following information, please give your modified python code according to these information: +''' +_LINUX_USER_AMEND_PROMPT = ''' +User's information are as follows: +Original Code: {original_code} +Task: {task} +Error Messages: {error} +Code Output: {code_output} +Current Working Directiory: {working_dir} +Files And Folders in Current Working Directiory: {files_and_folders} +Critique On The Code: {critique} +''' + + +class LinuxSkillAmend(): + + def __init__(self, config_path=None) -> None: + super().__init__() + self.llm = OpenAI(config_path) + self.system_version = get_os_version() + try: + check_os_version(self.system_version) + except ValueError as e: + print(e) + + # amend the code to fullfill the task. + def amend_code(self, original_code, task, error,code_output,working_dir,files_and_folders,critique): + self.sys_prompt = _LINUX_SYSTEM_AMEND_PROMPT + self.user_prompt = _LINUX_USER_AMEND_PROMPT.format( + original_code = original_code, + task = task, + error = error, + code_output = code_output, + working_dir = working_dir, + files_and_folders = files_and_folders, + critique = critique + ) + self.message = [ + {"role": "system", "content": self.sys_prompt}, + {"role": "user", "content": self.user_prompt}, + ] + return self.llm.chat(self.message) + def extract_python_code(self, response): + python_code = "" + if '```python' in response: + python_code = response.split('```python')[1].split('```')[0] + elif '```' in python_code: + python_code = response.split('```')[1].split('```')[0] + return python_code diff --git a/friday/agent/linux_skill_create_agent.py b/friday/agent/linux_skill_create_agent.py new file mode 100644 index 0000000..ee21b57 --- /dev/null +++ b/friday/agent/linux_skill_create_agent.py @@ -0,0 +1,168 @@ +from friday.action.get_os_version import get_os_version, check_os_version +from friday.core.llms import OpenAI +from friday.agent.prompt import prompt_dict +import re +import json + + +class LinuxSkillCreateAgent(): + """ + LinuxSkillCreateAgent is used to generate new skills in Linux environment and store them in the action_lib. + """ + def __init__(self, config_path=None) -> None: + super().__init__() + self.llm = OpenAI(config_path) + self.system_version = get_os_version() + self.prompt = prompt_dict + try: + check_os_version(self.system_version) + except ValueError as e: + print(e) + + # Send skill create message to LLM + def skill_create_format_message(self, task_name, task_description, working_dir): + self.sys_prompt = self.prompt['_LINUX_SYSTEM_SKILL_CREATE_PROMPT'] + self.user_prompt = self.prompt['_LINUX_USER_SKILL_CREATE_PROMPT'].format( + system_version=self.system_version, + task_description=task_description, + working_dir=working_dir, + task_name=task_name + ) + self.message = [ + {"role": "system", "content": self.sys_prompt}, + {"role": "user", "content": self.user_prompt}, + ] + return self.llm.chat(self.message) + + # Send invoke generate message to LLM + def invoke_generate_format_message(self, class_code, task_description,working_dir): + class_name, args_description = self.extract_class_name_and_args_description(class_code) + self.sys_prompt = self.prompt['_LINUX_SYSTEM_INVOKE_GENERATE_PROMPT'] + self.user_prompt = self.prompt['_LINUX_USER_INVOKE_GENERATE_PROMPT'].format( + class_name = class_name, + task_description = task_description, + args_description = args_description, + working_dir = working_dir + ) + self.message = [ + {"role": "system", "content": self.sys_prompt}, + {"role": "user", "content": self.user_prompt}, + ] + return self.llm.chat(self.message) + + # Send skill amend message to LLM + def skill_amend_format_message(self, original_code, task, error,code_output,working_dir,files_and_folders,critique): + self.sys_prompt = self.prompt['_LINUX_SYSTEM_SKILL_AMEND_PROMPT'] + self.user_prompt = self.prompt['_LINUX_USER_SKILL_AMEND_PROMPT'].format( + original_code = original_code, + task = task, + error = error, + code_output = code_output, + working_dir = working_dir, + files_and_folders = files_and_folders, + critique = critique + ) + self.message = [ + {"role": "system", "content": self.sys_prompt}, + {"role": "user", "content": self.user_prompt}, + ] + return self.llm.chat(self.message) + + # Send task judge prompt to LLM and get JSON response + def task_judge_format_message(self, current_code,task,code_output,working_dir,files_and_folders): + self.sys_prompt = self.prompt['_LINUX_SYSTEM_TASK_JUDGE_PROMPT'] + self.user_prompt = self.prompt['_LINUX_TASK_JUDGE_PROMPT'].format( + current_code=current_code, + task=task, + code_output=code_output, + working_dir=working_dir, + files_and_folders= files_and_folders + ) + self.message = [ + {"role": "system", "content": self.sys_prompt}, + {"role": "user", "content": self.user_prompt}, + ] + response =self.llm.chat(self.message) + judge_json = '{' + '\n' + self.extract_information(response, '{', '}')[0] + '\n' + '}' + print("**************************************************") + print(judge_json) + print("*************************************************") + judge_json = json.loads(judge_json) + return judge_json + + # Extract python code from response + def extract_python_code(self, response): + python_code = "" + if '```python' in response: + python_code = response.split('```python')[1].split('```')[0] + elif '```' in python_code: + python_code = response.split('```')[1].split('```')[0] + return python_code + + # Extract class_name and args description from python code + def extract_class_name_and_args_description(self, class_code): + """ + Extracts the class name and comments from the __call__ method of a given Python class code. + Specifically extracts the class name and the content between 'Args:' and 'Returns:' in the __call__ method. + + Args: + class_code (str): The string representation of the Python class code. + + Returns: + tuple: A tuple containing the class name and the extracted comments between 'Args:' and 'Returns:', + or None for each if not found. + """ + class_name_pattern = r"class (\w+)" + class_name_match = re.search(class_name_pattern, class_code) + class_name = class_name_match.group(1) if class_name_match else None + + # Extracting the __call__ method's docstring + call_method_docstring_pattern = r"def __call__\([^)]*\):\s+\"\"\"(.*?)\"\"\"" + call_method_docstring_match = re.search(call_method_docstring_pattern, class_code, re.DOTALL) + args_description = call_method_docstring_match.group(1).strip() if call_method_docstring_match else None + + return class_name, args_description + + # Extract args description from python code + def extract_args_description(self, class_code): + # Extracting the __call__ method's docstring + call_method_docstring_pattern = r"def __call__\([^)]*\):\s+\"\"\"(.*?)\"\"\"" + call_method_docstring_match = re.search(call_method_docstring_pattern, class_code, re.DOTALL) + args_description = call_method_docstring_match.group(1).strip() if call_method_docstring_match else None + return args_description + + # Extract information from text + def extract_information(self, message, begin_str='[BEGIN]', end_str='[END]'): + result = [] + _begin = message.find(begin_str) + _end = message.find(end_str) + while not (_begin == -1 or _end == -1): + result.append(message[_begin + len(begin_str):_end].strip()) + message = message[_end + len(end_str):] + _begin = message.find(begin_str) + _end = message.find(end_str) + return result + + # Extract args description and returns_description from python code + def extract_inputs_description_and_returns_description(self, class_code): + # Extracting the __call__ method's docstring + call_method_docstring_pattern = r"def __call__\([^)]*\):\s+\"\"\"(.*?)\"\"\"" + call_method_docstring_match = re.search(call_method_docstring_pattern, class_code, re.DOTALL) + call_method_docstring = call_method_docstring_match.group(1).strip() if call_method_docstring_match else None + # 使用正则表达式提取 Args 部分 + args_pattern = r"Args:\s*(.*?)\s*(Returns:|$)" + args_match = re.search(args_pattern, call_method_docstring, re.DOTALL) + args_description = args_match.group(1).strip() if args_match else None + # 使用正则表达式提取 Returns 部分 + returns_pattern = r"Returns:\s*(.*)" + returns_match = re.search(returns_pattern, call_method_docstring, re.DOTALL) + returns_description = returns_match.group(1).strip() if returns_match else None + return args_description, returns_description + + # Extract action description from python code + def extract_action_description(self, class_code): + # Extracting the __init__ method's description + init_pattern = r"def __init__\s*\(self[^)]*\):\s*(?:.|\n)*?self\._description\s*=\s*\"([^\"]+)\"" + action_match = re.search(init_pattern, class_code, re.DOTALL) + action_description = action_match.group(1).strip() if action_match else None + return action_description diff --git a/friday/agent/linux_skill_creator.py b/friday/agent/linux_skill_creator.py new file mode 100644 index 0000000..ee6c899 --- /dev/null +++ b/friday/agent/linux_skill_creator.py @@ -0,0 +1,71 @@ +from friday.action.get_os_version import get_os_version, check_os_version +from friday.core.llms import OpenAI + +_LINUX_SYSTEM_PROMPT = ''' +You are helpful assistant to assist in writing Python tool code for tasks completed on Linux operating systems. Your expertise lies in creating Python classes that perform specific tasks, adhering to a predefined format and structure. +Your goal is to generate Python tool code in the form of a class. The code should be structured to perform a user-specified task on a Linux operating system. The class must be easy to use and understand, with clear instructions and comments. +You should only respond with the python code in the format as described below: +1. Code Structure: Begin with the necessary import statement: from friday.action.base_action import BaseAction. Then, define the class using the class name which is the same as the task name provided by the user. +2. Parameter Handling: In the __init__ method, only initialize self._description with a brief description of the class's purpose, detailing what task it accomplishes. +3. Code used to accomplish the task: Note that you should avoid using bash for the current task if you can, and prioritize using some of python's basic libraries for the current task. If the task involves Linux bash operations, instruct the use of the subprocess library, particularly the run method, to execute these operations. All core code used to accomplish the task should be encapsulated within the __call__ method of the class. +4. Detailed Comments: Provide comprehensive comments throughout the code. This includes describing the purpose of the class, and the function of parameters, especially in the __call__ method. +And the code you write should also follow the following criteria: +1.The class must start with from friday.action.base_action import BaseAction.In addition you need to import all the third-party libraries used in your code. +2.The class name should be the same as the user's task name. +3.In the __init__ method, only self._description should be initialized. +4.The __call__ method must allow flexible arguments (*args, **kwargs) for different user requirements.The __call__ method should not hardcode specific task details, but rather, it should abstract them into parameters that can be passed in by the user. For example, if the class is meant to download and play music, the method should take parameters like the download link, destination folder, and file name, instead of having these details fixed in the code. Please ensure that the class is structured to easily accommodate different types of tasks, with a clear and flexible parameter design in the __call__ method. In addition, the parameter design should be comprehensive and versatile enough to be applicable to almost all similar tasks. +5.For tasks involving Linux bash commands, use the subprocess library to execute these commands within the Python class. +6.The code should include detailed comments explaining the purpose of the class,and the role of each parameter. +7. If a file or folder creation operation is involved, the name of the file or folder should contain only English, numbers and underscores. +8. You need to note that for different system languages, some system paths may have different names, for example, the desktop path in Chinese system languages is ~/桌面 while the desktop path in English system languages is ~/Desktop. +9. If your code involves operating (reading or writing or creating) files or folders under a specified path, be sure to change the current working directory to that specified path before performing file-related operations.. +10. If the user does not specifically request it (specify an absolute path), all your file operations should be relative to the user's working directory, and all created files should be stored in that directory and its subdirectories as a matter of priority. And once a file or directory query is involved, the priority is to query from below the default initial working directory. +11. The working directory given by the user should not be hardcoded in your code, because different user can have different working directory at different time. +12. If you need to access the user's working directory, you should make the user's working directory a parameter that can be passed to the __call__ method. If the user provides a value for the working directory as a parameter, then use the path provided by the user as the working directory path. Otherwise, you can obtain it using methods like os.getcwd(). +13. You only need to write the class, don't instantiate it and call the __call__ method. If you want to write an example of how to use the class, put the example in the comments. +Now you will be provided with the following information,please write python code to accomplish the task and be compatible with system environments, versions and language according to these information. +''' +_LINUX_USER_PROMPT =''' +User's information is as follows: +System Version: {system_version} +System language: simplified chinese +Working Directory: {working_dir} +Task Name: {task_name} +Task Description: {task_description} +''' +class LinuxSkillCreator(): + """ + LinuxSkillCreator is used to generate new skills in Linux environment and store them in the action_lib. + """ + def __init__(self, config_path=None) -> None: + super().__init__() + self.llm = OpenAI(config_path) + self.system_version = get_os_version() + try: + check_os_version(self.system_version) + except ValueError as e: + print(e) + # self.mac_systom_prompts = + + def format_message(self, task_name,task_description,working_dir): + self.sys_prompt = _LINUX_SYSTEM_PROMPT + self.user_prompt = _LINUX_USER_PROMPT.format( + system_version=self.system_version, + task_description=task_description, + working_dir=working_dir, + task_name=task_name + ) + self.message = [ + {"role": "system", "content": self.sys_prompt}, + {"role": "user", "content": self.user_prompt}, + ] + return self.llm.chat(self.message) + def extract_python_code(self, response): + python_code = "" + if '```python' in response: + python_code = response.split('```python')[1].split('```')[0] + elif '```' in python_code: + python_code = response.split('```')[1].split('```')[0] + return python_code + + diff --git a/friday/agent/linux_task_judger.py b/friday/agent/linux_task_judger.py new file mode 100644 index 0000000..0577b2f --- /dev/null +++ b/friday/agent/linux_task_judger.py @@ -0,0 +1,98 @@ +from friday.action.get_os_version import get_os_version, check_os_version +from friday.core.llms import OpenAI +import json +_LINUX_SYSTEM_Judger_PROMPT = ''' +You are an AI programmed to verify Python code against a user's task requirements. +Your goal is to determine if the provided Python code accomplishes the user's specified task based on the feedback information. +You should only respond with the json result in the format as described below: +1.Analyze the provided code: Examine the user's Python code to understand its functionality and structure. +2.Compare the code with the task description: Align the objectives stated in the user's task description with the capabilities of the code. +3.Evaluate the feedback information: Review the user's feedback, including the output of the code and any file changes or directory states, to gauge the code's effectiveness. +4.Formulate a reasoning process: Synthesize the analysis, comparison, and evaluation to create a logical reasoning process about the code's effectiveness in achieving the task. +5.Conclude if the task is accomplished: Make a definitive judgment based on the reasoning process as to whether or not the code fulfills the user's task. +7.Output Format: You should only return me a json with no extra content. the json should contain two keys, one is called "reasoning" and its value is a string that represents your reasoning process. The other is called "judge", which is a boolean indicating whether the current code completed the task successfully. +And you should also follow the following criteria: +1.Ensure accurate understanding of the Python code. +2.Relate the code functionality to the user's task. +3.Assess the feedback information for evidence of task completion. +4.Provide clear, logical reasoning. +5.You need to note that the code I gave you is not reporting errors, I just don't know if it actually accomplishes the task or not. +6.Information about the current working directory and all the files and folders under it may imply whether the file was created successfully or not. +Now you will be provided with the following information, please give the result json according to these information: +''' +_LINUX_USER_Judger_PROMPT = ''' +User's information are as follows: +Current Code: {current_code} +Task: {task} +Code Output: {code_output} +Current Working Directiory: {working_dir} +Files And Folders in Current Working Directiory: {files_and_folders} +''' + + +class LinuxTaskJudger(): + + def __init__(self, config_path=None) -> None: + super().__init__() + self.llm = OpenAI(config_path) + self.system_version = get_os_version() + try: + check_os_version(self.system_version) + except ValueError as e: + print(e) + + # amend the code to fullfill the task. + def judge(self, current_code,task,code_output,working_dir,files_and_folders): + self.sys_prompt = _LINUX_SYSTEM_Judger_PROMPT + self.user_prompt = _LINUX_USER_Judger_PROMPT.format( + current_code=current_code, + task=task, + code_output=code_output, + working_dir=working_dir, + files_and_folders= files_and_folders + ) + self.message = [ + {"role": "system", "content": self.sys_prompt}, + {"role": "user", "content": self.user_prompt}, + ] + response =self.llm.chat(self.message) + judge_json = json.loads(response) + return judge_json + +# judger = LinuxTaskJudger(config_path="../../examples/config.json") +# current_code=''' +# from friday.action.base_action import BaseAction +# import os + +# class create_folder(BaseAction): +# def __init__(self): +# self._description = "Create a folder under the working directory" + +# def __call__(self, *args, **kwargs): +# # Get the working directory +# working_dir = os.getcwd() + +# # Create the folder path +# folder_name = "ss" +# folder_path = os.path.join(working_dir, folder_name) + +# # Check if the folder already exists +# if os.path.exists(folder_path): +# print(f"The folder '{folder_name}' already exists.") +# else: +# # Create the folder +# os.makedirs(folder_path) +# print(f"The folder '{folder_name}' has been created under the working directory.") + +# # Example usage +# # create_folder_action = create_folder() +# # create_folder_action() + +# ''' +# task="create a folder which is named test2 under the working directory" +# code_output ="" +# working_dir ="/home/wengzhenmin/Projects/friday/working_dir" +# files_and_folders ="ss\n" +# res = judger.judge(current_code=current_code,code_output=code_output,task=task,working_dir=working_dir,files_and_folders=files_and_folders) +# print(res) +# print(res["judge"]) \ No newline at end of file diff --git a/friday/agent/openai_agent.py b/friday/agent/openai_agent.py new file mode 100644 index 0000000..1ac2d8c --- /dev/null +++ b/friday/agent/openai_agent.py @@ -0,0 +1,169 @@ +import json +from friday.core.llms import OpenAI +from friday.agent.base_agent import BaseAgent +from friday.core.schema import EnvState +from friday.core.action_manager import ActionManager + + +a = "{action_input} the input to the action, could be any valid input for python programs or shell commands, such numbers, strings, or path to a file, etc." +BASE_PROMPT = """ +{system_prompt} +{tool_description} +To use a tool, please use the following format: +``` +{thought} to address the user request, thinking about what are the sub-goals you need to achieve and which tool is needed for each sub-goal? +{action} the tool names, each action name should be one of [{action_names}]. +``` +The response after utilizing tools should using the following format: +``` +{response} To generate a response, you need to summarize your thoughts above and combined them with the tool execution results. +`` +If you already know the answer, or you do not need to use tools, +please using the following format to reply: +``` +{thought} the thought process to answer user questions +{response} respond to user request based on thought +``` +Remember you must surround you action between and . +Now you are ready to take questions and requests from users. +""" + + +class OpenAIAgent(BaseAgent): + """ + BaseAgent is the base class of all agents. + """ + def __init__(self, config_path=None, action_lib_dir=None): + super().__init__() + self.llm = OpenAI(config_path) + self.action_lib = ActionManager(config_path=config_path, action_lib_dir=action_lib_dir) + # self.actions = None + self.max_iter = 3 + self.system_prompt = """You are a personal assistant that aims to automate the workflow for human.\nYou are capable of understanding human intent and decompose it into several subgoals that can be addressed via language generation or acomplished using external tools.\nSome of the external tools you can use and their functionalities are as follows: + """ + self.action_names = self.action_lib.action_names + self.available_action_description = self.action_lib.descriptions + # todo: 添加工具检索模块 + # self.available_action_description = "" + # for i, name in enumerate(self.action_names): + # self.available_action_description += "Tool {}: {}\n{}\n".format(i+1, name, self.action_lib_description[name]) + + def from_config(self, config_path=None): + self.llm = OpenAI(config_path) + + def format_message(self, query): + self.prompt = BASE_PROMPT.format( + system_prompt=self.system_prompt, + tool_description=self.available_action_description, + action_names=self.action_names, + thought="Thought:", + action="Actions:", + action_input="Action Input:", + response="Response:" + ) + self.message = [ + {"role": "system", "content": self.prompt}, + {"role": "user", "content": query}, + ] + return self.llm.chat(self.message) + + # Extract action from text + def extract_action(self, message, begin_str='[BEGIN]', end_str='[END]'): + result = [] + _begin = message.find(begin_str) + _end = message.find(end_str) + while not (_begin == -1 or _end == -1): + result.append(message[_begin + len(begin_str):_end].strip()) + message = message[_end + len(end_str):] + _begin = message.find(begin_str) + _end = message.find(end_str) + return result + + # Extract information from text + def extract_information(self, message, begin_str='[BEGIN]', end_str='[END]'): + result = [] + _begin = message.find(begin_str) + _end = message.find(end_str) + while not (_begin == -1 or _end == -1): + result.append(message[_begin + len(begin_str):_end].strip()) + message = message[_end + len(end_str):] + _begin = message.find(begin_str) + _end = message.find(end_str) + return result + + # def extract_invoke(self, message, begin_str='[BEGIN]', end_str='[END]'): + # result = [] + # _begin = message.find(begin_str) + # _end = message.find(end_str) + # while not (_begin == -1 or _end == -1): + # result.append(message[_begin + len(begin_str):_end].strip()) + # message = message[_end + len(end_str):] + # _begin = message.find(begin_str) + # _end = message.find(end_str) + # return result + + # # @dzc + # def extract_parameter(self, message, begin_str='[BEGIN]', end_str='[END]'): + # result = [] + # _begin_parameter = message.find(begin_str) + # _end_parameter = message.find(end_str) + # # go through parameters + # while not (_begin_parameter == -1 or _end_parameter == -1): + # # get current task parameters + # parameter = message[_begin_parameter + len(begin_str):_end_parameter].strip() + # _begin_arg = parameter.find("") + # _end_arg = parameter.find("") + # args = [] + # # go through args + # while not (_begin_arg == -1 or _end_arg == -1): + # arg = parameter[_begin_arg + len(""): _end_arg].strip() + # args.append(arg) + # parameter = parameter[_end_arg + len(""):].strip() + # _begin_arg = parameter.find("") + # _end_arg = parameter.find("") + # result.append(args) + # message = message[_end_parameter + len(end_str):] + # _begin_parameter = message.find(begin_str) + # _end_parameter = message.find(end_str) + # return result + + def chat(self, goal: str): + self._history = [] + + def step(self, single_action) -> EnvState: + _command = self.action_lib[single_action] + self.environment.step(_command) + return self.environment.observe() + + +if __name__ == '__main__': + # actions = { + # "turn_on_dark_mode()": "Using turn_on_dark_mode() will change your system into the dark mode.", + # "play_study_music()": "Using play_study_music() will open Music in your Mac and play songs that are sutiable for study and work.", + # "create_meeting()": "Using create_meeting() will help user create a meeting event. When users request to create a meeting, don't ask questions such as meeting title and time, just invoke this tool by generating the action name.", + # "show_upcoming_meetings()": "Using show_upcoming_meetings() will open Calendar and show the their upcoming meetings for the user.", + # "organize_app_layout()": "Using organize_app_layout() will help user reorganize their Desktop layout for better working condition and focus more easily." + # } + # agent = OpenAIAgent(config_path="../../examples/config.json", environment=environment) + agent = OpenAIAgent(config_path="../../examples/config.json") + # print(agent.action_lib) + # print(agent.action_lib_description) + # executation_action = agent.action_lib["turn_on_dark_mode"]() + # executation_action.run() + # response = agent.format_message(query="I want to start working now. Please help set up the working environment for me.") + # print(agent.prompt) + # print(response['content']) +# response = ''' +# Thought: To set up the working environment, we can focus on two sub-goals: turning on dark mode and organizing the app layout. + +# Actions: +# 1. turn_on_dark_mode +# 2. turn_on_light_mode''' +# action = agent.extract_action(response, begin_str='', end_str='') +# import time +# for a in action: +# print(a) +# command = agent.action_lib[a] +# # print(agent.env.step(command)) +# print(environment.step(command)) +# time.sleep(2) \ No newline at end of file diff --git a/friday/agent/prompt.py b/friday/agent/prompt.py new file mode 100644 index 0000000..a22ec4d --- /dev/null +++ b/friday/agent/prompt.py @@ -0,0 +1,463 @@ +prompt = { + 'execute_prompt' : { + # Code generate and invoke prompt in os + '_SYSTEM_SKILL_CREATE_AND_INVOKE_PROMPT': ''' + You are helpful assistant to assist in writing Python tool code for tasks completed on operating systems. Your expertise lies in creating Python classes that perform specific tasks, adhering to a predefined format and structure. + Your goal is to generate Python tool code in the form of a class. The code should be structured to perform a user-specified task on the current operating system. The class must be easy to use and understand, with clear instructions and comments. + You should only respond with a python code and a invocation statement. + Python code in the format as described below: + 1. Code Structure: Begin with the necessary import statement: from friday.action.base_action import BaseAction. Then, define the class using the class name which is the same as the task name provided by the user. + 2. Initialization Code: Initialization Code: In the __init__ method of the class, only "self._description" is initialized. This attribute succinctly summarizes the main function and purpose of the class. + 3. Code used to accomplish the Task: Note that you should avoid using bash for the current task if you can, and prioritize using some of python's basic libraries for the current task. If the task involves os bash operations, instruct the use of the subprocess library, particularly the run method, to execute these operations. All core code used to accomplish the task should be encapsulated within the __call__ method of the class. + 4. Parameters of __call__ method: The parameter design of __call__ methods should be comprehensive and generic enough to apply to different goals in all the same task scenarios. The parameters of the __call__ method are obtained by parsing and abstracting the task description, and the goals of the specific task can not be hard-coded into the method. + 5. Detailed Comments: Provide comprehensive comments throughout the code. This includes describing the purpose of the class, and the function of parameters, especially in the __call__ method. + invocation statement in the format as described below: + 1. Parameter Details Interpretation: Understand the parameter details of the __call__ method. This will help select the correct parameters to fill in the invocation statement. + 2. Task Description Analysis: Analyze the way the code is called based on the current task, the generated code, and the Information of Prerequisite Tasks. + 3. Generating Invocation Statement: Construct the __call__ method invocation statement. This includes instantiating the class and passing the appropriate arguments to the __call__ method based on the task description. For example, if my class is called Demo, and its __call__ method takes parameters a and b, then my invocation statement should be Demo()(a,b). + 4. Output Format: The final output should include the invocation statement, which must be enclosed in tags. For example, Demo()(a,b). + And the code you write should also follow the following criteria: + 1. The class must start with from friday.action.base_action import BaseAction.In addition you need to import all the third-party libraries used in your code. + 2. The class name should be the same as the user's task name. + 3. In the __init__ method, only self._description should be initialized. And self._description must be Code enough to encapsulate the functionality of the current class. For example, if the current task is to change the name of the file named test in the folder called document to test1, then the content of this attribute should be written as: Rename the specified file within a designated folder to a new, predetermined filename. + 4. The __call__ method must allow flexible arguments (*args, **kwargs) for different user requirements. The __call__ method can not hardcode specific task details, but rather, it should abstract them into parameters that can be passed in by the user, these parameters can be obtained by parsing and abstracting the task description. For example, if the class is meant to download and play music, the __call__ method should take parameters like the download link, destination folder, and file name, instead of having these details fixed in the code. Please ensure that the class is structured to easily accommodate different types of tasks, with a clear and flexible parameter design in the __call__ method. In addition, the parameter design should be comprehensive and versatile enough to be applicable to handling different targets under all the same task scenarios. + 5. For tasks involving os bash commands, use the subprocess library to execute these commands within the Python class. + 6. The code should include detailed comments explaining the purpose of the class, and the role of each parameter. + 7. If a file or folder creation operation is involved, the name of the file or folder should contain only English, numbers and underscores. + 8. You need to note that for different system languages, some system paths may have different names, for example, the desktop path in Chinese system languages is ~/桌面 while the desktop path in English system languages is ~/Desktop. + 9. If your code involves operating (reading or writing or creating) files or folders under a specified path, be sure to change the current working directory to that specified path before performing file-related operations. + 10. If the user does not specifically request it (specify an absolute path), all your file operations should be relative to the user's working directory, and all created files should be stored in that directory and its subdirectories as a matter of priority. And once a file or directory query is involved, the priority is to query from below the default initial working directory. + 11. The working directory given by the user can not be hardcoded in your code, because different user can have different working directory at different time. + 12. If you need to access the user's working directory, you should make the user's working directory a parameter that can be passed to the __call__ method. If the user provides a value for the working directory as a parameter, then use the path provided by the user as the working directory path. Otherwise, you can obtain it using methods like os.getcwd(). + 13. You only need to write the class, don't instantiate it and call the __call__ method. If you want to write an example of how to use the class, be sure to put the example in the comments. + 14. The description of parameters in the __call__ method must follow a standardized format: Args: [description of input parameters], Returns: [description of the method's return value]. + 15. In the __call__ method, you need to print the task execution completion message if the task execution completes. + 16. Please note that the code you generate is mainly used under the operating system, so it often involves system-level operations such as reading and writing files. You need to write a certain fault-tolerant mechanism to handle potential problems that may arise during these operations, such as Problems such as file non-existence and insufficient permissions. + 17. If the __call__ method needs a return value to help perform the next task, for example, if a task needs to return a list or value to facilitate the next task to receive, then let the __call__ method return. Otherwise, there is no need to return + 18. If the __call__ method involves file operations, then the file's path must be passed as a parameter to the __call__ method, in particular, if you are operating multiple files, pass the paths of these files as parameters in the form of a list. If it involves moving files, then both the source and destination paths must be provided as parameters to the __call__ method, since the source and destination may not be in the same directory. + 19. If the current task requires the use of the return results from a preceding task, then its corresponding call method must include a parameter specifically for receiving the return results of the preceding task. + 20. Please note that I have provided you with some codes similar to the current task in the Relevant Code of the user information. If the current task can be directly implemented with a certain code, then use this code directly without modifying code. + 21. If the code involves the output of file paths, ensure that the output includes the files' absolute path. + 22. When your code involves the task of file operation, please be sure to pay attention to the naming format of the file. If it is a jpg file called XXX, the name should be XXX.jpg. If it is an mp4 file called XXX, the name should be XXX.mp4. Additionally, the file name passed in may or may not have a file format suffix, and you need to handle these cases. + 23. Please note that the file path provided in the task might not include the file extension. This does not necessarily mean that the path is for a folder. You are required to devise an operation to determine the type of the file, which will assist you in obtaining the complete file path including the file type. + 24. Please note that when writing code to read the contents of a docx file, be sure to also read the table contents in the docx file. + 25. If the generated code is used to run other code, then the result of the other code must be returned. + And the invocation statement should also follow the following criteria: + 1. The __call__ method invocation must be syntactically correct as per Python standards. + 2. Clearly identify any fake or placeholder parameters used in the invocation. + 3. If necessary, you can use the Working Directory provided by the user as a parameter passed into the __call__ method. + 4. The 'Information of Prerequisite Tasks' from User's information provides relevant information about the prerequisite tasks for the current task, encapsulated in a dictionary format. The key is the name of the prerequisite task, and the value consists of two parts: 'description', which is the description of the task, and 'return_val', which is the return information of the task. + 5. If the execution of the current task's code requires the return value of a prerequisite task, the return information of the prerequisite task can assist you in generating the code execution for the current task. + 6. 'Working Directory' in User's information represents the working directory. It may not necessarily be the same as the current working directory. If the files or folders mentioned in the task do not specify a particular directory, then by default, they are assumed to be in the working directory. This can help you understand the paths of files or folders in the task to facilitate your generation of the call. + 7. The code comments include an example of a class invocation. You can refer to this example, but you should not directly copy it. Instead, you need to adapt and fill in the details of this invocation according to the current task and the information returned from previous tasks. + 8. For code that involves text content as a parameter, you should ensure as much as possible that this text content is fully included in the function parameters when generating a call, rather than abbreviating it to save token count. For example, if you need to perform a file write operation, you cannot abbreviate the content to be written into __call__ method invocation, like origin text is 'Yao ming is a basketball player.', you can not write 'Yao ming is ...'. + 9. If the string in the input parameter contains single quotes or double quotes, then the input of the parameter is wrapped in triple quotes. The following is an example of an invocation statement: Demo()("""xx"x"xxx""" ) + 10. All parameter information that needs to be filled in when calling must be filled in, and data cannot be omitted. + Now you will be provided with the following information, please write python code to accomplish the task and be compatible with system environments, versions and language according to these information. + ''', + '_USER_SKILL_CREATE_AND_INVOKE_PROMPT': ''' + User's information is as follows: + System Version: {system_version} + System language: simplified chinese + Working Directory: {working_dir} + Task Name: {task_name} + Task Description: {task_description} + Information of Prerequisite Tasks: {pre_tasks_info} + Relevant Code: {relevant_code} + ''', + + # Invoke generate prompt in os + '_SYSTEM_INVOKE_GENERATE_PROMPT' : ''' + You are an AI trained to assist with Python programming tasks, with a focus on class and method usage. + Your goal is to generate a Python __call__ method invocation statement based on provided class name, task descriptions, and method parameter details. + You should only respond with the python code in the format as described below: + 1. Class Context: Begin by understanding the context of the Python class provided by the user. This includes grasping the class name and its intended functionality. + 2. Task Description Analysis: Analyze the task description provided to determine the purpose of the class and how it is expected to operate. This will help in identifying the correct way to invoke the class. + 3. Parameter Details Interpretation: Interpret the parameter details of the __call__ method. This will involve extracting the type of parameters and their role in the method. + 4. Generating Invocation Statement: Construct the __call__ method invocation statement. This includes instantiating the class and passing the appropriate arguments to the __call__ method based on the task description. For example, if my class is called Demo, and its __call__ method takes parameters a and b, then my invocation statement could be Demo()(a,b). + 5. Fake Parameter Identification: If the required parameter information (like a URL or file path) is not provided and a placeholder or fake parameter is used, clearly identify and list these as not being actual or valid values.All the fake paramters you list should be separated by comma.If there are no fake parameters,you should give a None. + 6. Output Format: The final output should include two parts:The first one is the invocation statement, which must be enclosed in tags.The second one is all the fake parameters you identified, which will be enclosed in tags. + And the response you write should also follow the following criteria: + 1. The __call__ method invocation must be syntactically correct as per Python standards. + 2. Clearly identify any fake or placeholder parameters used in the invocation. + 3. Encouraging generating a realistic and functional code snippet wherever possible. + 4. If necessary, you can use the Working Directory provided by the user as a parameter passed into the __call__ method. + 5. The 'Information of Prerequisite Tasks' from User's information provides relevant information about the prerequisite tasks for the current task, encapsulated in a dictionary format. The key is the name of the prerequisite task, and the value consists of two parts: 'description', which is the description of the task, and 'return_val', which is the return information of the task. + 6. If the execution of the current task's code requires the return value of a prerequisite task, the return information of the prerequisite task can assist you in generating the code execution for the current task. + 7. 'Working Directory' in User's information represents the working directory. It may not necessarily be the same as the current working directory. If the files or folders mentioned in the task do not specify a particular directory, then by default, they are assumed to be in the working directory. This can help you understand the paths of files or folders in the task to facilitate your generation of the call. + 8. The code comments include an example of a class invocation. You can refer to this example, but you should not directly copy it. Instead, you need to adapt and fill in the details of this invocation according to the current task and the information returned from previous tasks. + Now you will be provided with the following information, please generate your response according to these information: + ''', + '_USER_INVOKE_GENERATE_PROMPT' : ''' + User's information are as follows: + Class Name: {class_name} + Task Description: {task_description} + __call__ Method Parameters: {args_description} + Information of Prerequisite Tasks: {pre_tasks_info} + Working Directory: {working_dir} + ''', + + # Skill amend and invoke prompt in os + '_SYSTEM_SKILL_AMEND_AND_INVOKE_PROMPT' : ''' + You are an AI expert in Python programming, with a focus on diagnosing and resolving code issues. + Your goal is to precisely identify the reasons for failure in the existing Python code and implement effective modifications to ensure it accomplishes the intended task without errors. + You should only respond with a python code and a invocation statement. + Python code in the format as described below: + 1. Modified Code: Based on the error analysis, the original code is modified to fix all the problems and provide the final correct code to the user to accomplish the target task. If the code is error free, fix and refine the code based on the Critique On The Code provided by the user to accomplish the target task. + 2. Error Analysis: Conduct a step-by-step analysis to identify why the code is generating errors or failing to complete the task. This involves checking for syntax errors, logical flaws, and any other issues that might hinder execution. + 3. Detailed Explanation: Offer a clear and comprehensive explanation for each identified issue, detailing why these issues are occurring and how they are impacting the code's functionality. + invocation statement in the format as described below: + 1. Parameter Details Interpretation: Understand the parameter details of the __call__ method. This will help select the correct parameters to fill in the invocation statement. + 2. Task Description Analysis: Analyze the way the code is called based on the current task, the generated code, and the Information of Prerequisite Tasks. + 3. Generating Invocation Statement: Construct the __call__ method invocation statement. This includes instantiating the class and passing the appropriate arguments to the __call__ method based on the task description. For example, if my class is called Demo, and its __call__ method takes parameters a and b, then my invocation statement should be Demo()(a,b). + 4. Output Format: The final output should include the invocation statement, which must be enclosed in tags. For example, Demo()(a,b). + And the code you write should also follow the following criteria: + 1. You must keep the original code as formatted as possible, e.g. class name, methods, etc. You can only modify the relevant implementation of the __call__ method in the code. + 2. Please avoid throwing exceptions in your modified code, as this may lead to consistent error reports during execution. Instead, you should handle the caught exceptions appropriately! + 3. Some errors may be caused by unreasonable tasks initiated by the user, resulting in outcomes that differ from what is expected. Examples include scenarios where the file to be created already exists, or the parameters passed in are incorrect. To prevent further errors, you need to implement fault tolerance or exception handling. + 4. Ensure the final code is syntactically correct, optimized for performance, and follows Python best practices. The final code should contain only the class definition; any code related to class instantiation and invocation must be commented out. + 5. The python code must be enclosed between ```python and ```. For example, ```python [python code] ```. + 6. The analysis and explanations must be clear, brief and easy to understand, even for those with less programming experience. + 7. All modifications must address the specific issues identified in the error analysis. + 8. The solution must enable the code to successfully complete the intended task without errors. + 9. When Critique On The Code in User's information is empty, it means that there is an error in the code itself, you should fix the error in the code so that it can accomplish the current task. + 10. In User's information, 'Working Directory' represents the root directory of the working directory, and 'Current Working Directory' represents the directory where the current task is located. + And the invocation statement should also follow the following criteria: + 1. The __call__ method invocation must be syntactically correct as per Python standards. + 2. Clearly identify any fake or placeholder parameters used in the invocation. + 3. If necessary, you can use the Working Directory provided by the user as a parameter passed into the __call__ method. + 4. The 'Information of Prerequisite Tasks' from User's information provides relevant information about the prerequisite tasks for the current task, encapsulated in a dictionary format. The key is the name of the prerequisite task, and the value consists of two parts: 'description', which is the description of the task, and 'return_val', which is the return information of the task. + 5. If the execution of the current task's code requires the return value of a prerequisite task, the return information of the prerequisite task can assist you in generating the code execution for the current task. + 6. 'Working Directory' in User's information represents the working directory. It may not necessarily be the same as the current working directory. If the files or folders mentioned in the task do not specify a particular directory, then by default, they are assumed to be in the working directory. This can help you understand the paths of files or folders in the task to facilitate your generation of the call. + 7. The code comments include an example of a class invocation. You can refer to this example, but you should not directly copy it. Instead, you need to adapt and fill in the details of this invocation according to the current task and the information returned from previous tasks. + 8. All parameter information that needs to be filled in when calling must be filled in, and data cannot be omitted. + Now you will be provided with the following information, please give your modified python code and invocation statement according to these information: + ''', + '_USER_SKILL_AMEND_AND_INVOKE_PROMPT' : ''' + User's information are as follows: + Original Code: {original_code} + Task: {task} + Error Messages: {error} + Code Output: {code_output} + Current Working Directiory: {current_working_dir} + Working Directiory: {working_dir} + Files And Folders in Current Working Directiory: {files_and_folders} + Critique On The Code: {critique} + Information of Prerequisite Tasks: {pre_tasks_info} + ''', + + + # Skill amend prompt in os + '_SYSTEM_SKILL_AMEND_PROMPT' : ''' + You are an AI expert in Python programming, with a focus on diagnosing and resolving code issues. + Your goal is to precisely identify the reasons for failure in the existing Python code and implement effective modifications to ensure it accomplishes the intended task without errors. + You should only respond with the python code in the format as described below: + 1. Modified Code: Based on the error analysis, the original code is modified to fix all the problems and provide the final correct code to the user to accomplish the target task. If the code is error free, fix and refine the code based on the Critique On The Code provided by the user to accomplish the target task. + 2. Error Analysis: Conduct a step-by-step analysis to identify why the code is generating errors or failing to complete the task. This involves checking for syntax errors, logical flaws, and any other issues that might hinder execution. + 3. Detailed Explanation: Offer a clear and comprehensive explanation for each identified issue, detailing why these issues are occurring and how they are impacting the code's functionality. + And the code you write should also follow the following criteria: + 1. You must keep the original code as formatted as possible, e.g. class name, methods, etc. You can only modify the relevant implementation of the __call__ method in the code. + 2. Please avoid throwing exceptions in your modified code, as this may lead to consistent error reports during execution. Instead, you should handle the caught exceptions appropriately! + 3. Some errors may be caused by unreasonable tasks initiated by the user, resulting in outcomes that differ from what is expected. Examples include scenarios where the file to be created already exists, or the parameters passed in are incorrect. To prevent further errors, you need to implement fault tolerance or exception handling. + 4. Ensure the final code is syntactically correct, optimized for performance, and follows Python best practices. The final code should contain only the class definition; any code related to class instantiation and invocation must be commented out. + 5. The python code must be enclosed between ```python and ```. For example, ```python [python code] ```. + 6. The analysis and explanations must be clear, brief and easy to understand, even for those with less programming experience. + 7. All modifications must address the specific issues identified in the error analysis. + 8. The solution must enable the code to successfully complete the intended task without errors. + 9. When Critique On The Code in User's information is empty, it means that there is an error in the code itself, you should fix the error in the code so that it can accomplish the current task. + 10. In User's information, 'Working Directory' represents the root directory of the working directory, and 'Current Working Directory' represents the directory where the current task is located. + Now you will be provided with the following information, please give your modified python code according to these information: + ''', + '_USER_SKILL_AMEND_PROMPT' : ''' + User's information are as follows: + Original Code: {original_code} + Task: {task} + Error Messages: {error} + Code Output: {code_output} + Current Working Directiory: {current_working_dir} + Working Directiory: {working_dir} + Files And Folders in Current Working Directiory: {files_and_folders} + Critique On The Code: {critique} + ''', + + # Skill create prompt in os + '_SYSTEM_SKILL_CREATE_PROMPT' : ''' + You are helpful assistant to assist in writing Python tool code for tasks completed on operating systems. Your expertise lies in creating Python classes that perform specific tasks, adhering to a predefined format and structure. + Your goal is to generate Python tool code in the form of a class. The code should be structured to perform a user-specified task on the current operating system. The class must be easy to use and understand, with clear instructions and comments. + You should only respond with the python code in the format as described below: + 1. Code Structure: Begin with the necessary import statement: from friday.action.base_action import BaseAction. Then, define the class using the class name which is the same as the task name provided by the user. + 2. Initialization Code: Initialization Code: In the __init__ method of the class, only "self._description" is initialized. This attribute succinctly summarizes the main function and purpose of the class. + 3. Code used to accomplish the Task: Note that you should avoid using bash for the current task if you can, and prioritize using some of python's basic libraries for the current task. If the task involves os bash operations, instruct the use of the subprocess library, particularly the run method, to execute these operations. All core code used to accomplish the task should be encapsulated within the __call__ method of the class. + 4. Parameters of __call__ method: The parameter design of __call__ methods should be comprehensive and generic enough to apply to different goals in all the same task scenarios. The parameters of the __call__ method are obtained by parsing and abstracting the task description, and the goals of the specific task can not be hard-coded into the method. + 5. Detailed Comments: Provide comprehensive comments throughout the code. This includes describing the purpose of the class, and the function of parameters, especially in the __call__ method. + And the code you write should also follow the following criteria: + 1. The class must start with from friday.action.base_action import BaseAction.In addition you need to import all the third-party libraries used in your code. + 2. The class name should be the same as the user's task name. + 3. In the __init__ method, only self._description should be initialized. And self._description must be Code enough to encapsulate the functionality of the current class. For example, if the current task is to change the name of the file named test in the folder called document to test1, then the content of this attribute should be written as: Rename the specified file within a designated folder to a new, predetermined filename. + 4. The __call__ method must allow flexible arguments (*args, **kwargs) for different user requirements. The __call__ method can not hardcode specific task details, but rather, it should abstract them into parameters that can be passed in by the user, these parameters can be obtained by parsing and abstracting the task description. For example, if the class is meant to download and play music, the __call__ method should take parameters like the download link, destination folder, and file name, instead of having these details fixed in the code. Please ensure that the class is structured to easily accommodate different types of tasks, with a clear and flexible parameter design in the __call__ method. In addition, the parameter design should be comprehensive and versatile enough to be applicable to handling different targets under all the same task scenarios. + 5. For tasks involving os bash commands, use the subprocess library to execute these commands within the Python class. + 6. The code should include detailed comments explaining the purpose of the class, and the role of each parameter. + 7. If a file or folder creation operation is involved, the name of the file or folder should contain only English, numbers and underscores. + 8. You need to note that for different system languages, some system paths may have different names, for example, the desktop path in Chinese system languages is ~/桌面 while the desktop path in English system languages is ~/Desktop. + 9. If your code involves operating (reading or writing or creating) files or folders under a specified path, be sure to change the current working directory to that specified path before performing file-related operations. + 10. If the user does not specifically request it (specify an absolute path), all your file operations should be relative to the user's working directory, and all created files should be stored in that directory and its subdirectories as a matter of priority. And once a file or directory query is involved, the priority is to query from below the default initial working directory. + 11. The working directory given by the user can not be hardcoded in your code, because different user can have different working directory at different time. + 12. If you need to access the user's working directory, you should make the user's working directory a parameter that can be passed to the __call__ method. If the user provides a value for the working directory as a parameter, then use the path provided by the user as the working directory path. Otherwise, you can obtain it using methods like os.getcwd(). + 13. You only need to write the class, don't instantiate it and call the __call__ method. If you want to write an example of how to use the class, be sure to put the example in the comments. + 14. The description of parameters in the __call__ method must follow a standardized format: Args: [description of input parameters], Returns: [description of the method's return value]. + 15. In the __call__ method, you need to print the task execution completion message if the task execution completes. + 16. Please note that the code you generate is mainly used under the operating system, so it often involves system-level operations such as reading and writing files. You need to write a certain fault-tolerant mechanism to handle potential problems that may arise during these operations, such as Problems such as file non-existence and insufficient permissions. + 17. If the __call__ method needs a return value to help perform the next task, for example, if a task needs to return a list or value to facilitate the next task to receive, then let the __call__ method return. Otherwise, there is no need to return + 18. If the __call__ method involves file operations, then the file's path must be passed as a parameter to the __call__ method, in particular, if you are operating multiple files, pass the paths of these files as parameters in the form of a list. If it involves moving files, then both the source and destination paths must be provided as parameters to the __call__ method, since the source and destination may not be in the same directory. + 19. If the current task requires the use of the return results from a preceding task, then its corresponding call method must include a parameter specifically for receiving the return results of the preceding task. + Now you will be provided with the following information, please write python code to accomplish the task and be compatible with system environments, versions and language according to these information. + ''', + '_USER_SKILL_CREATE_PROMPT' : ''' + User's information is as follows: + System Version: {system_version} + System language: simplified chinese + Working Directory: {working_dir} + Task Name: {task_name} + Task Description: {task_description} + ''', + + # Task judge prompt in os + '_SYSTEM_TASK_JUDGE_PROMPT' : ''' + You are an AI program expert to verify Python code against a user's task requirements. + Your goal is to determine if the provided Python code accomplishes the user's specified task based on the feedback information, And score the code based on the degree of generalizability of the code. + You should only respond with the JSON result in the format as described below: + 1. Analyze the provided code: Examine the user's Python code to understand its functionality and structure. + 2. Compare the code with the task description: Align the objectives stated in the user's task description with the capabilities of the code. + 3. Evaluate the feedback information: Review the user's feedback, Includes the output of the code and the working catalog information provided to measure the effectiveness of the code. + 4. Formulate a reasoning process: Comprehensive code analysis and feedback evaluation, create a logical reasoning process regarding the effectiveness of the code in accomplishing the task and the generalizability of the code. The generality of the code can be analyzed in terms of the flexibility of the parameters in the code, the handling of errors and exceptions, the clarity of the comments, the efficiency of the code, and the security perspective. + 5. Evaluating Task Completion: Determine if the task is complete based on the reasoning process, expressed as a Boolean value, with true meaning the task is complete and false meaning the task is not complete. + 6. Evaluating the code's generality: based on the analysis of the code's generality by the reasoning process, the code's generality is scored by assigning an integer score between 1 and 10 to reflect the code's generality, with a score of 1-4 indicating that the code is not sufficiently generalized, and that it may be possible to write the task objective directly into the code instead of passing it in as a parameter. a score of 5-7 indicates that the code is capable of accomplishing the task for different objectives of the same task, but does not do well in aspects such as security, clarity of comments, efficiency, or error and exception handling, and a score of 8 and above indicates that the code has good versatility and performs well in security, clarity of comments, efficiency, or error and exception handling. + 7. Output Format: You should only return a JSON with no extra content. The JSON should contain three keys: the first is called 'reasoning', with its value being a string that represents your reasoning process. the second is called 'judge', its value is the boolean type true or false, true indicates that the code completes the current task, false indicates that it does not. The last is called 'score', which is a number between 1 and 10, representing code generality rating based on the result of 'Evaluating the code's generality'. + And you should also follow the following criteria: + 1. Ensure accurate understanding of the Python code. + 2. Relate the code functionality to the user's task. + 3. Assess the completion degree of the task based on the feedback information. + 4. Provide clear, logical reasoning. + 5. You need to aware that the code I provided does not generate errors, I am just uncertain whether it effectively accomplishes the intended task. + 6. If the task involves file creation, information regarding the current working directory and all its subdirectories and files may assist you in determining whether the file has been successfully created. + 7. If the Code Output contains information indicating that the task has been completed, the task can be considered completed. + 8. In User's information, 'Working Directory' represents the root directory of the working directory, and 'Current Working Directory' represents the directory where the current task is located. + 9. If the task is not completed, it may be because the code generation and calling did not consider the information returned by the predecessor task. This information may be used as input parameters of the __call__ method. + 10. 'Next Task' in the User's information describes tasks that follow the current task and may depend on the return from the current task. If necessary, you should check the current task's code output to ensure it returns the information required for these subsequent tasks. If it does not, then the current task can be considered incomplete. + Now you will be provided with the following information, please give the result JSON according to these information: + ''', + '_USER_TASK_JUDGE_PROMPT' : ''' + User's information are as follows: + Current Code: {current_code} + Task: {task} + Code Output: {code_output} + Current Working Directiory: {current_working_dir} + Working Directory: {working_dir} + Files And Folders in Current Working Directiory: {files_and_folders} + Next Task: {next_action} + ''', + + # Code error judge prompt in os + '_SYSTEM_ERROR_ANALYSIS_PROMPT' : ''' + You are an expert in analyzing Python code errors, you are able to make an accurate analysis of different types of errors, and your return results adhere to a predefined format and structure. + Your goal is to analyze the errors that occur in the execution of the code provided to you, and determine whether the type of error is one that requires external additions (e.g., missing dependency packages, environment configuration issues, version incompatibility, etc.) or one that only requires internal changes to the code (e.g., syntax errors, logic errors, data type errors). + You should only respond with the JSON result in the format as described below: + 1. Analyze the provided code and error: Examine the user's Python code to understand its functionality and structure. Combine the code with the error message, locate the error location, and analyze the specific reason for the error step by step. + 2. Evaluate the feedback information: Review the user's feedback, including Current Working Directiory, Files And Folders in Current Working Directiory, combine with the previous analysis to further analyze the cause of the error. + 3. Determine the type of error: Based on the error analysis results and current task, determine the type of error, whether it belongs to External Supplementation Required Errors or Internal Code Modification Errors. + 4. Output Format: You should only return a JSON with no extra content. The JSON should contain two keys: one is called 'reasoning', with its value being a string that represents your reasoning process; the other is called 'type', where the value of 'type' is assigned as 'planning' for errors that fall under External Supplementation Required Errors, and as 'amend' for errors that are classified as Internal Code Modification Errors. + And you should also follow the following criteria: + 1. Ensure accurate understanding of the Python code and the error. + 2. There are only two types of errors, External Supplementation Required Errors and Internal Code Modification Errors. + 3. Understanding the definition of External Supplementation Required Errors: This type of error involves not only modifying the code itself, but also requiring some additional operations in the running environment of the code, this requires new tasks to complete the additional operations. + 4. Understanding the definition of Internal Code Modification Errors: This type of error can be resolved by modifying the code itself without having to perform any additional steps outside of the code. + 5. Provide clear, logical reasoning. + 6. The value of type can only be 'replan' or 'amend'. + 7. In User's information, 'Working Directory' represents the root directory of the working directory, and 'Current Working Directory' represents the directory where the current task is located. + ''', + '_USER_ERROR_ANALYSIS_PROMPT' : ''' + User's information are as follows: + Current Code: {current_code} + Task: {task} + Code Error: {code_error} + Current Working Directiory: {current_working_dir} + Working Directiory: {working_dir} + Files And Folders in Current Working Directiory: {files_and_folders} + ''', + + # Tool usage prompt in os + '_SYSTEM_TOOL_USAGE_PROMPT' : ''' + You are a useful AI assistant capable of accessing APIs to complete user-specified tasks, according to API documentation, + by using the provided ToolRequestUtil tool. The API documentation is as follows: + {openapi_doc} + The user-specified task is as follows: + {tool_sub_task} + The context which can further help you to determine the params of the API is as follows: + {context} + You need to complete the code using the ToolRequestUtil tool to call the specified API and print the return value + of the api. + ToolRequestUtil is a utility class, and the parameters of its 'request' method are described as follows: + def request(self, api_path, method, params=None, content_type=None): + """ + :param api_path: the path of the API + :param method: get/post + :param params: the parameters of the API, can be None.You cannot pass files to 'params' parameter.All files should be passed to 'files' parameter. + :param files: files to be uploaded, can be None.Remember if the parameters of the API contain files, you need to use the 'files' parameter to upload the files. + :param content_type: the content_type of api, e.g., application/json, multipart/form-data, can be None + :return: the response from the API + """ + Please begin your code completion: + ''', + '_USER_TOOL_USAGE_PROMPT' : ''' + from friday.core.tool_request_util import ToolRequestUtil + tool_request_util = ToolRequestUtil() + # TODO: your code here + ''', + + # QA prompt in os + '_SYSTEM_QA_PROMPT' : ''' + You are a helpful ai assistant that can answer the question with the help of the context provided by the user in a step by step manner. The full question may help you to solve the current question. + If you don't know how to answer the user's question, answer "I don't know." instead of making up an answer. + And you should also follow the following criteria: + 1. If the pre-task does not return the information you want, but your own knowledge can answer the current question, then you try to use your own knowledge to answer it. + 2. If your current solution is incorrect but you have a potential solution, please implement your potential solution directly. + 3. If you lack specific knowledge but can make inferences based on relevant knowledge, you can try to infer the answer to the question. + ''', + '_USER_QA_PROMPT' : ''' + Context: {context} + Full Question: {question} + Current Question: {current_question} + ''' + + }, + + 'planning_prompt' : { + # Task decompose prompt in os + '_SYSTEM_TASK_DECOMPOSE_PROMPT' : ''' + You are an expert in making plans. + I will give you a task and ask you to decompose this task into a series of subtasks. These subtasks can form a directed acyclic graph, and each subtask is an atomic operation. Through the execution of topological sorting of subtasks, I can complete the entire task. + You should only respond with a reasoning process and a JSON result in the format as described below: + 1. Carry out step-by-step reasoning based on the given task until the task is completed. Each step of reasoning is decomposed into sub-tasks. For example, the current task is to reorganize the text files containing the word 'agent' in the folder called document into the folder called agent. Then the reasoning process is as follows: According to Current Working Directiory and Files And Folders in Current Working Directiory information, the folders documernt and agent exist, so firstly, retrieve the txt text in the folder call document in the working directory. If the text contains the word "agent", save the path of the text file into the list, and return. Secondly, put the retrieved files into a folder named agent based on the file path list obtained by executing the previous task. + 2. There are three types of subtasks, the first is a task that requires the use of APIs to access internet resources to obtain information, such as retrieving information from the Internet, this type of task is called 'API subtask', and all available APIs are only listed in the API List. The second is a task that does not require the use of API tools but need to write code to complete, which is called 'Code subtask', 'Code subtask' usually only involves operating system or file operations. The third is called 'QA subtask', It neither requires writing code nor calling API to complete the task, it will analyze the current subtask description and the return results of the predecessor tasks to get an appropriate answer. + 3. Each decomposed subtask has four attributes: name, task description, and dependencies. 'name' abstracts an appropriate name based on the reasoning process of the current subtask. 'description' is the process of the current subtask, and if the current task is related to a corresponding file operation, the path to the file needs to be written in the 'description'. 'dependencies' refers to the list of task names that the current task depends on based on the reasoning process. These tasks must be executed before the current task. 'type' indicates whether the current task is a Code task or a API task or a QA task, If it is a Code task, its value is 'Code', if it is a API task, its value is 'API', if it is a QA task, its value is 'QA'. + 4. In JSON, each decomposed subtask contains four attributes: name, description, dependencies and type, which are obtained through reasoning about the task. The key of each subtask is the 'name' attribute of the subtask. + 5. Continuing with the example in 1, the format of the JSON data I want to get is as follows: + ```json + { + "retrieve_files" : { + "name": "retrieve_files", + "description": "retrieve the txt text in the folder call document in the working directory. If the text contains the word "agent", save the path of the text file into the list, and return.", + "dependencies": [], + "type" : "Code" + }, + "organize_files" : { + "name": "organize_files", + "description": "put the retrieved files into a folder named agent based on the file path list obtained by executing the previous task.", + "dependencies": ["retrieve_files"], + "type": "Code" + } + } + ``` + And you should also follow the following criteria: + 1. A task can be decomposed down into one or more subtasks, depending on the complexity of the task. + 2. The Action List I gave you contains the name of each action and the corresponding operation description. These actions are all atomic code task. You can refer to these atomic operations to decompose the code task. + 3. If it is a pure mathematical problem, you can write code to complete it, and then process a QA subtask to analyze the results of the code to solve the problem. + 4. The decomposed subtasks can form a directed acyclic graph based on the dependencies between them. + 5. The description information of the subtask must be detailed enough, no entity and operation information in the task can be ignored. + 6. 'Current Working Directiory' and 'Files And Folders in Current Working Directiory' specify the path and directory of the current working directory. These information may help you understand and generate tasks. + 7. The tasks currently designed are compatible with and can be executed on the present version of the system. + 8. The current task may need the return results of its predecessor tasks. For example, the current task is: Move the text files containing the word 'agent' from the folder named 'document' in the working directory to a folder named 'agent'. We can decompose this task into two subtasks, the first task is to retrieve text files containing the word 'agent' from the folder named 'document', and return their path list. The second task is to move the txt files of the corresponding path to the folder named 'agent' based on the path list returned by the previous task. + 9. If the current subtask needs to use the return result of the previous subtask, then this process should be written in the task description of the subtask. + 10. Please note that the name of a Code subtask must be abstract. For instance, if the subtask is to search for the word "agent," then the subtask name should be "search_word," not "search_agent." As another example, if the subtask involves moving a file named "test," then the subtask name should be "move_file," not "move_test." + 11. When generating the subtask description, you need to clearly specify whether the operation targets a single entity or multiple entities that meet certain criteria. + 12. When decomposing subtasks, avoid including redundant information. For instance, if the task is to move txt files containing the word 'agent' from the folder named 'document' to a folder named 'XXX', one subtask should be to retrieve text files containing the word 'agent' from the folder named 'document', and return their path list. Then, the next subtask should be to move the txt files to the folder named 'XXX' based on the path list returned by the previous task, rather than moving the txt files that contain the word 'agent' to the folder named 'XXX' based on the path list returned by the previous task. The latter approach would result in redundant information in the subtasks. + 13. User's information provided you with a API List that includes the API path and their corresponding descriptions. These APIs are designed for interacting with internet resources, like the Internet. + 14. When decomposing subtasks, you need to pay attention to whether the current subtask involves obtaining data from internet resources, such as finding cat pictures on the Internet, retrieving information on a certain web page, etc., then you need to select the relevant API from the API List. + 15. If the current subtask is a API task, the description of the task must include the API path of the specified API to facilitate my extraction through the special format of the API path. For example, if an API task is to use the arxiv API to find XXX, then the description of the task should be: "Use the "/tools/arxiv' API to search for XXX". + 16. Please note that QA subtasks will not be generated continuously, that is, there will be no dependency between any two QA subtasks. + 17. A QA subtask can perform comprehension analysis task, such as content conversion and format transformation, information summarization or analysis, answering academic questions, language translation, creative writing, logical reasoning based on existing information, and providing daily life advice and guidance, etc. + 18. If the task involves file or operating system operations, such as file reading and writing, downloading, moving, then decompose the Code subtask. If the task requires the use of APIs to access internet resources to obtain information, such as web page retrieval, obtaining web page text content, etc., then decompose the API subtask. QA subtasks usually use the results of reading files from the Code task and the content returned by the API task to help complete intermediate steps or give the final answer to the task. + 19. If the task does not involve any file operations or Internet data acquisition, then only plan a QA subtask, and the 'description' of the QA subtask must be the full content of the original task. + 20. If the task is to read and analyze the content of a PowerPoint presentation, it can be broken down into two sub-tasks. The first is a Code sub-task, which involves extracting the text content of the PowerPoint slides into a list. The second is a QA sub-task, which complete the task base on the text information extracted from each slide. + 21. Once the task involves obtaining knowledge such as books, articles, character information, etc. you need to plan API tasks to obtain this knowledge from the Internet. + 22. When decomposing an API subtask which uses the Bing Load Page API, you need to proceed to plan a QA subtask for analyzing and summarizing the information returned by that API subtask. For example, if the task is to find information about XXX, then your task will be broken down into three subtasks. The first API subtask is to use the Bing Search API to find relevant web page links. The second API subtask is to use the Bing Load Page API to obtain the information of the web pages found in the previous subtask. The final sub-task is a QA subtask, which is used to analyze the web page information returned by the previous sub-task and complete the task. + 23. When the task involves retrieving a certain detailed content, then after decomposing the API subtask using Bing Search API, you also need to decompose an API subtask using Bing Load Page API, using for more detailed content. + 24. If the attached file is a png or jpg file, the task must first be decomposed a API subtask, which uses image caption API to analyze image and solve problem. If it is necessary to obtain information from the Internet, then an API subtask should be decomposed. Otherwise, proceed with a QA subtask, which analyzes and completes task based on the return from API subtask. + 25. Please note that all available APIs are only in the API List. You should not make up APIs that are not in the API List. + 26. If the attached file is a mp3 file, you can only break out two subtasks! The task must first be decomposed a API subtask, which uses audio2text API to transcribe mp3 audio to text. Then proceed with a QA subtask, which analyzes and completes task based on the return from API subtask. + 27. Since the analyse or the content of the file are in the return value of the first subtask, if the following subtask requires the content or the analyse, the first subtask needs to be added to the dependencies of that subtask. + 28. If the task has a path to the file, then the subtask that operates the file must write the full path of the file in the task description, for example, add a new sheet, write calculation results into a certain column, etc. + ''', + '_USER_TASK_DECOMPOSE_PROMPT' : ''' + User's information are as follows: + System Version: {system_version} + Task: {task} + Action List: {action_list} + API List: {api_list} + Current Working Directiory: {working_dir} + Files And Folders in Current Working Directiory: {files_and_folders} + ''', + + # Task replan prompt in os + '_SYSTEM_TASK_REPLAN_PROMPT' : ''' + You are an expert at designing new tasks based on the results of your reasoning. + When I was executing the code of current task, an issue occurred that is not related to the code. The user information includes a reasoning process addressing this issue. Based on the results of this reasoning, please design a new task to resolve the problem. + You should only respond with a reasoning process and a JSON result in the format as described below: + 1. Design new tasks based on the reasoning process of current task errors. For example, the inference process analyzed that the reason for the error was that there was no numpy package in the environment, causing it to fail to run. Then the reasoning process for designing a new task is: According to the reasoning process of error reporting, because there is no numpy package in the environment, we need to use the pip tool to install the numpy package. + 2. There are three types of subtasks, the first is a task that requires the use of APIs to access internet resources to obtain information, such as retrieving information from the Internet, this type of task is called 'API subtask', and the second is a task that does not require the use of API tools but need to write code to complete, which is called 'Code subtask', 'Code subtask' usually only involves operating system or file operations. The third is called 'QA subtask', It neither requires writing code nor calling API to complete the task, it will analyze the current subtask description and the return results of the predecessor tasks to get an appropriate answer. + 3. Each decomposed subtask has four attributes: name, task description, and dependencies. 'name' abstracts an appropriate name based on the reasoning process of the current subtask. 'description' is the process of the current subtask. 'dependencies' refers to the list of task names that the current task depends on based on the reasoning process. These tasks must be executed before the current task. 'type' indicates whether the current task is a Code task or a API task or a QA task, If it is a Code task, its value is 'Code', if it is a API task, its value is 'API', if it is a QA task, its value is 'QA'. + 4. Continuing with the example in 1, the format of the JSON data I want to get is as follows: + ```json + { + "install_package" : { + "name": "install_package", + "description": "Use pip to install the numpy package that is missing in the environment.", + "dependencies": [], + "type" : "Code" + } + } + ``` + And you should also follow the following criteria: + 1. The tasks you design based on the reasoning process are all atomic operations. You may need to design more than one task to meet the requirement that each task is an atomic operation. + 2. The Action List I gave you contains the name of each action and the corresponding operation description. These actions are all atomic operations. You can refer to these atomic operations to design new task. + 3. If an atomic operation in the Action List can be used as a new task, then the name of the decomposed sub-task should be directly adopted from the name of that atomic action. + 4. The dependency relationship between the newly added task and the current task cannot form a loop. + 5. The description information of the new task must be detailed enough, no entity and operation information in the task can be ignored. + 6. 'Current Working Directiory' and 'Files And Folders in Current Working Directiory' specify the path and directory of the current working directory. These information may help you understand and generate tasks. + 7. The tasks currently designed are compatible with and can be executed on the present version of the system. + 8. Please note that the name of a task must be abstract. For instance, if the task is to search for the word "agent," then the task name should be "search_word," not "search_agent." As another example, if the task involves moving a file named "test," then the task name should be "move_file," not "move_test. + 9. Please note that QA subtasks will not be generated continuously, that is, there will be no dependency between any two QA subtasks. + 10. A QA subtask can perform comprehension analysis task, such as content conversion and format transformation, information summarization or analysis, answering academic questions, language translation, creative writing, logical reasoning based on existing information, and providing daily life advice and guidance, etc. + ''', + '_USER_TASK_REPLAN_PROMPT' : ''' + User's information are as follows: + Current Task: {current_task} + Current Task Description: {current_task_description} + System Version: {system_version} + reasoning: {reasoning} + Action List: {action_list} + Current Working Directiory: {working_dir} + Files And Folders in Current Working Directiory: {files_and_folders} + ''', + }, + + 'retrieve_prompt' : { + # action code filter prompt + '_SYSTEM_ACTION_CODE_FILTER_PROMPT' : ''' + You are an expert in analyzing python code. + I will assign you a task and provide a dictionary of action names along with their corresponding codes. Based on the current task, please analyze the dictionary to determine if there is any action whose code can be used to complete the task. If such a code exists, return the action name that corresponds to the code you believe is best suited for completing the task. If no appropriate code exists, return an empty string. + You should only respond with the format as described below: + 1. First, understand the requirements of the task. Next, read the code for each action, understanding their functions and methods. Examine the methods and attributes within the class, learning about their individual purposes and return values. Finally, by combining the task with the parameters of each action class's __call__ method, determine whether the content of the task can serve as an argument for the __call__ method, thereby arriving at an analysis result. + 2. Based on the above analysis results, determine whether there is code corresponding to the action that can complete the current task. If so, return the action name corresponding to the code you think is the most appropriate. If not, return an empty string. + 3. Output Format: The final output should include one part: the name of the selected action or empty string, which must be enclosed in tags. + And you should also follow the following criteria: + 1. There may be multiple codes that meet the needs of completing the task, but I only need you to return the action name corresponding to the most appropriate code. + 2. If no code can complete the task, be sure to return an empty string, rather than a name of an action corresponding to a code that is nearly but not exactly suitable. + ''', + '_USER_ACTION_CODE_FILTER_PROMPT' : ''' + User's information are as follows: + Action Code Pair: {action_code_pair} + Task: {task_description} + ''', + } +} diff --git a/friday/agent/skill_creator.py b/friday/agent/skill_creator.py new file mode 100644 index 0000000..3978eaa --- /dev/null +++ b/friday/agent/skill_creator.py @@ -0,0 +1,73 @@ +from action.get_os_version import get_os_version, check_os_version +from friday.core.llms import OpenAI + +_MAC_SYSTEM_PROMPT = ''' +You are a helpful assistant that writes AppleScript code to complete any task specified by me. +System Version: {system_version} +Task: {task} +You should only respond in the format as described below: + +import subprocess + +def task_name(): + # + applescript = f""" + + """ + subprocess.run(["osascript", "-e", applescript]) + +task_name() +''' + +_LINUX_SYSTEM_PROMPT = ''' +You are a helpful assistant that writes Python code to complete any task specified by me. +I will give you the following informations: +System Version: {system_version} +Task: {task} +You should only respond in the format as described below: + +from friday.action.base_action import BaseAction + +# TODO: you should write a class in the following format, and the class name should be the same as the task name,besides,you can design the parameters of __call__ as you want. +class task_name(BaseAction): + def __init__(self) -> None: + super().__init__() + # self._description should be initialized as the description of the task + self._description = "" + # self.action_type should be initialized as the type of the task, which can be 'BASH' or 'PYTHON' + self.action_type = '' + + def __call__(self, *args): + # TODO: write your code here + + + +''' + + +class SkillCreator(): + """ + SkillCreator is used to generate new skills and store them in the action_lib. + """ + def __init__(self, config_path=None) -> None: + super().__init__() + self.llm = OpenAI(config_path) + self.system_version = get_os_version() + try: + check_os_version(self.system_version) + except ValueError as e: + print(e) + # self.mac_systom_prompts = + + def format_message(self, task): + self.prompt = _MAC_SYSTEM_PROMPT.format( + system_version=self.system_version, + task=task + ) + self.message = [ + {"role": "system", "content": self.prompt}, + {"role": "user", "content": task}, + ] + return self.llm.chat(self.message) + + \ No newline at end of file diff --git a/friday/agent/tool_agent.py b/friday/agent/tool_agent.py new file mode 100644 index 0000000..0c95cc7 --- /dev/null +++ b/friday/agent/tool_agent.py @@ -0,0 +1,120 @@ +from friday.core.llms import OpenAI +from friday.environment.py_env import PythonEnv +import json +''' +让大模型根据目标工具的API文档做网络请求,获取到响应数据并返回 +''' +TOOL_SYS_PROMPT=''' +You are a useful AI assistant capable of accessing APIs to complete user-specified tasks, according to API documentation, +by using the provided ToolRequestUtil tool. The API documentation is as follows: +{openapi_doc} +The user-specified task is as follows: +{tool_sub_task} +The context which can further help you to determine the params of the API is as follows: +{context} +You need to complete the code using the ToolRequestUtil tool to call the specified API and print the return value +of the api. +ToolRequestUtil is a utility class, and the parameters of its 'request' method are described as follows: +def request(self, api_path, method, params=None, content_type=None): + """ + :param api_path: the path of the API + :param method: get/post + :param params: the parameters of the API, can be None + :param content_type: the content type of the API, e.g., application/json, can be None + :return: the response from the API + """ +Please begin your code completion: + +''' + +TOOL_USER_PROMPT=''' +from friday.core.tool_request_util import ToolRequestUtil +tool_request_util = ToolRequestUtil() +# TODO: your code here +''' + +class ToolAgent(): + ''' ToolAgent is used to call the tool api and get the result feedback ''' + def __init__(self, config_path=None, open_api_doc_path = None) -> None: + super().__init__() + self.llm = OpenAI(config_path) + self.open_api_doc = {} + self.environment = PythonEnv() + with open(open_api_doc_path) as f: + self.open_api_doc = json.load(f) + # self.mac_systom_prompts = + + def generate_call_api_code(self, tool_sub_task,tool_api_path,context="No context provided."): + self.sys_prompt = TOOL_SYS_PROMPT.format( + openapi_doc = json.dumps(self.generate_openapi_doc(tool_api_path)), + tool_sub_task = tool_sub_task, + context = context + ) + self.user_prompt = TOOL_USER_PROMPT + self.message = [ + {"role": "system", "content": self.sys_prompt}, + {"role": "user", "content": self.user_prompt}, + ] + return self.llm.chat(self.message) + def generate_openapi_doc(self, tool_api_path): + # init current api's doc + curr_api_doc = {} + curr_api_doc["openapi"] = self.open_api_doc["openapi"] + curr_api_doc["info"] = self.open_api_doc["info"] + curr_api_doc["paths"] = {} + curr_api_doc["components"] = {"schemas":{}} + api_path_doc = {} + #extract path and schema + if tool_api_path not in self.open_api_doc["paths"]: + curr_api_doc = {"error": "The api is not existed"} + return curr_api_doc + api_path_doc = self.open_api_doc["paths"][tool_api_path] + curr_api_doc["paths"][tool_api_path] = api_path_doc + find_ptr = {} + if "get" in api_path_doc: + findptr = api_path_doc["get"] + elif "post" in api_path_doc: + findptr = api_path_doc["post"] + api_params_schema_ref = "" + # json格式 + if (("requestBody" in findptr) and + ("content" in findptr["requestBody"]) and + ("application/json" in findptr["requestBody"]["content"]) and + ("schema" in findptr["requestBody"]["content"]["application/json"]) and + ("$ref" in findptr["requestBody"]["content"]["application/json"]["schema"])): + api_params_schema_ref = findptr["requestBody"]["content"]["application/json"]["schema"]["$ref"] + elif (("requestBody" in findptr) and + ("content" in findptr["requestBody"]) and + ("multipart/form-data" in findptr["requestBody"]["content"]) and + ("schema" in findptr["requestBody"]["content"]["multipart/form-data"]) and + ("allOf" in findptr["requestBody"]["content"]["multipart/form-data"]["schema"]) and + ("$ref" in findptr["requestBody"]["content"]["multipart/form-data"]["schema"]["allOf"][0])): + api_params_schema_ref = findptr["requestBody"]["content"]["multipart/form-data"]["schema"]["allOf"][0]["$ref"] + if api_params_schema_ref != None and api_params_schema_ref != "": + curr_api_doc["components"]["schemas"][api_params_schema_ref.split('/')[-1]] = self.open_api_doc["components"]["schemas"][api_params_schema_ref.split('/')[-1]] + return curr_api_doc + def extract_python_code(self, response): + python_code = "" + if '```python' in response: + python_code = response.split('```python')[1].split('```')[0] + elif '```' in python_code: + python_code = response.split('```')[1].split('```')[0] + return python_code + def execute_code(self,code): + state = self.environment.step(code) + api_result = None + if(state.error != None and state.error != ""): + api_result = state.error + else: + api_result = state.result + return api_result + + +# agent = ToolAgent("../../examples/config.json","../core/openapi.json") +# res = agent.generate_openapi_doc("/tools/image_caption") +# print(res) +# code_text = agent.generate_call_api_code("use /tools/bing/searchv2 tool to search How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? You can use the latest 2022 version of english wikipedia.","/tools/bing/searchv2") +# code = agent.extract_python_code(code_text) +# print(code) +# api_res = agent.execute_code(code) +# print(api_res) \ No newline at end of file diff --git a/friday/api/__init__.py b/friday/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/api/arxiv/__init__.py b/friday/api/arxiv/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/api/arxiv/arxiv.py b/friday/api/arxiv/arxiv.py new file mode 100644 index 0000000..dd02328 --- /dev/null +++ b/friday/api/arxiv/arxiv.py @@ -0,0 +1,37 @@ +from fastapi import APIRouter +from pydantic import BaseModel +import arxiv + +router = APIRouter() + + +class ArxivQuery(BaseModel): + query: str + + +top_k_results: int = 3 +ARXIV_MAX_QUERY_LENGTH = 300 +doc_content_chars_max: int = 4000 + + +@router.get("/tools/arxiv") +async def get_arxiv_article_information(item: ArxivQuery): + '''Run Arxiv search and get the article meta information. + ''' + try: + results = arxiv.Search( # type: ignore + item.query[: ARXIV_MAX_QUERY_LENGTH], max_results=top_k_results + ).results() + except Exception as ex: + return {"result": None, "error": f"Arxiv exception: {ex}"} + + docs = [ + f"Published: {result.updated.date()}\nTitle: {result.title}\n" + f"Authors: {', '.join(a.name for a in result.authors)}\n" + f"Summary: {result.summary}" + for result in results + ] + if docs: + return {"result": "\n\n".join(docs)[: doc_content_chars_max], "error": None} + else: + return {"result": None, "error": "No good Arxiv Result was found"} \ No newline at end of file diff --git a/friday/api/arxiv/test.py b/friday/api/arxiv/test.py new file mode 100644 index 0000000..9cfe259 --- /dev/null +++ b/friday/api/arxiv/test.py @@ -0,0 +1,8 @@ +import requests + +response = requests.get( + 'http://43.159.144.130:8079/tools/arxiv', + json={'query': 'autogen'} +) + +print(response.json()) \ No newline at end of file diff --git a/friday/api/audio2text/audio2text.py b/friday/api/audio2text/audio2text.py new file mode 100644 index 0000000..d7951e6 --- /dev/null +++ b/friday/api/audio2text/audio2text.py @@ -0,0 +1,16 @@ +from openai import OpenAI +import os + +os.environ["OPENAI_API_KEY"] = "" +os.environ["OPENAI_ORGANIZATION"] = "" + +class Audio2TextTool: + def __init__(self) -> None: + self.client = OpenAI() + def caption(self,audio_file): + # 使用 OpenAI Whisper API 进行语音识别 + response = self.client.audio.transcriptions.create( + model="whisper-1", + file=audio_file + ) + return response.texts \ No newline at end of file diff --git a/friday/api/audio2text/audio2text_service.py b/friday/api/audio2text/audio2text_service.py new file mode 100644 index 0000000..10e15cd --- /dev/null +++ b/friday/api/audio2text/audio2text_service.py @@ -0,0 +1,31 @@ +from fastapi import APIRouter, HTTPException, File, UploadFile,Depends +from pydantic import BaseModel,Field +from typing import Optional +from .audio2text import Audio2TextTool +import io +import os +import shutil +router = APIRouter() + +whisper_api = Audio2TextTool() + + +class AudioTextQueryItem(BaseModel): + file: UploadFile = File(...) + + + +@router.post("/tools/audio2text") +async def audio2text(item: AudioTextQueryItem = Depends()): + try: + # 创建一个临时文件来保存上传的音频 + with open(item.file.filename, "wb") as buffer: + shutil.copyfileobj(item.file.file, buffer) + with open(item.file.filename, "rb") as audio: + caption = whisper_api.caption(audio_file=audio) + # 清理临时文件 + os.remove(item.file.filename) + return {"text": caption} + except RuntimeError as e: + raise HTTPException(status_code=500, detail=str(e)) + diff --git a/friday/api/audio2text/test.mp3 b/friday/api/audio2text/test.mp3 new file mode 100644 index 0000000..e9b8dd7 Binary files /dev/null and b/friday/api/audio2text/test.mp3 differ diff --git a/friday/api/bing/__init__.py b/friday/api/bing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/api/bing/bing_api.py b/friday/api/bing/bing_api.py new file mode 100644 index 0000000..ab09fa6 --- /dev/null +++ b/friday/api/bing/bing_api.py @@ -0,0 +1,60 @@ +import requests +from bs4 import BeautifulSoup +from typing import Tuple +from enum import Enum + +SEARCH_RESULT_LIST_CHUNK_SIZE = 3 +RESULT_TARGET_PAGE_PER_TEXT_COUNT = 500 + + +class BingAPI: + def __init__(self, subscription_key: str) -> None: + self._headers = { + 'Ocp-Apim-Subscription-Key': subscription_key, + 'BingAPIs-Market': 'en-US', + + } + self._endpoint = "https://api.bing.microsoft.com/v7.0/search" + self._mkt = 'en-US' + + def search(self, key_words: str, max_retry: int = 3): + for _ in range(max_retry): + try: + result = requests.get(self._endpoint, headers=self._headers, params={'q': key_words, 'mkt': self._mkt,'safeSearch' : 'moderate'}, + timeout=10) + except Exception: + continue + if result.status_code == 200: + result = result.json() + return result + else: + continue + raise RuntimeError("Failed to access Bing Search API.") + + def load_page(self, url: str, max_retry: int = 3) -> Tuple[bool, str]: + for _ in range(max_retry): + try: + res = requests.get(url, timeout=15) + if res.status_code == 200: + res.raise_for_status() + else: + raise RuntimeError("Failed to load page, code {}".format(res.status_code)) + except Exception: + res = None + continue + res.encoding = res.apparent_encoding + content = res.text + break + if res is None: + return False, "Timeout for loading this page, Please try to load another one or search again." + try: + soup = BeautifulSoup(content, 'html.parser') + paragraphs = soup.find_all('p') + page_detail = "" + for p in paragraphs: + text = p.get_text().strip() + page_detail += text + return True, page_detail + except Exception: + return False, "Timeout for loading this page, Please try to load another one or search again." + diff --git a/friday/api/bing/bing_api_v2.py b/friday/api/bing/bing_api_v2.py new file mode 100644 index 0000000..713ec40 --- /dev/null +++ b/friday/api/bing/bing_api_v2.py @@ -0,0 +1,68 @@ +import requests +from langchain.utilities import BingSearchAPIWrapper +from bs4 import BeautifulSoup +from typing import Tuple +from enum import Enum +from .web_loader import WebPageLoader +from langchain.text_splitter import RecursiveCharacterTextSplitter +from langchain.embeddings.openai import OpenAIEmbeddings +from langchain.vectorstores import Chroma +from langchain.chains.summarize import load_summarize_chain +from langchain import OpenAI +import os + +os.environ["BING_SUBSCRIPTION_KEY"] = "885e62a126554fb390af88ae31d2c8ff" +os.environ["BING_SEARCH_URL"] = "https://api.bing.microsoft.com/v7.0/search" +os.environ["OPENAI_API_KEY"] = "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" +os.environ["OPENAI_ORGANIZATION"] = "org-fSyygvftM73W0pK4VjoK395W" + +SEARCH_RESULT_LIST_CHUNK_SIZE = 3 +RESULT_TARGET_PAGE_PER_TEXT_COUNT = 500 + + +class BingAPIV2: + def __init__(self) -> None: + self.search_engine = BingSearchAPIWrapper(search_kwargs={'mkt': 'en-us','safeSearch': 'moderate'}) + self.web_loader = WebPageLoader() + self.web_chunker = RecursiveCharacterTextSplitter(chunk_size=4500, chunk_overlap=0) + self.web_sniptter_embed = OpenAIEmbeddings() + self.web_summarizer = OpenAI( + temperature=0, + ) + + def search(self, key_words: str,top_k: int = 5, max_retry: int = 3): + # return search.results(query,top_k) + for _ in range(max_retry): + try: + result = self.search_engine.results(key_words,top_k) + except Exception: + continue + if result != None: + return result + else: + continue + raise RuntimeError("Failed to access Bing Search API.") + + def load_page(self, url: str) -> str: + page_data = self.web_loader.load_data(url) + page_content_str = "" + if(page_data["data"][0] != None and page_data["data"][0]["content"] != None): + page_content_str = page_data["data"][0]["content"] + return page_content_str + def summarize_loaded_page(self,page_str): + if page_str == "": + return "" + web_chunks = self.web_chunker.create_documents([page_str]) + summarize_chain = load_summarize_chain(self.web_summarizer, chain_type="map_reduce") + main_web_content = summarize_chain.run(web_chunks) + return main_web_content + def attended_loaded_page(self,page_str,query_str): + if page_str == "": + return "" + web_chunks = self.web_chunker.create_documents([page_str]) + chunSearch = Chroma.from_documents(web_chunks, self.web_sniptter_embed) + relatedChunks = chunSearch.similarity_search(query_str, k=3) + attended_content = '...'.join([chunk.page_content for chunk in relatedChunks]) + return attended_content + + diff --git a/friday/api/bing/bing_service.py b/friday/api/bing/bing_service.py new file mode 100644 index 0000000..fa9e4aa --- /dev/null +++ b/friday/api/bing/bing_service.py @@ -0,0 +1,90 @@ +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel,Field +from typing import Optional +from .bing_api import BingAPI +from .bing_api_v2 import BingAPIV2 +from .image_search_api import ImageSearchAPI +import tiktoken + +# 计算网页内容对gpt4来说的token数,如果token太多就用3.5做摘要或者用向量数据库检索最相关的片段 +def num_tokens_from_string(string: str) -> int: + """Returns the number of tokens in a text string.""" + encoding = tiktoken.encoding_for_model('gpt-4-1106-preview') + num_tokens = len(encoding.encode(string)) + return num_tokens + +router = APIRouter() + +bing_api = BingAPI('885e62a126554fb390af88ae31d2c8ff') +bing_api_v2 = BingAPIV2() +image_search_api = ImageSearchAPI('885e62a126554fb390af88ae31d2c8ff') + +# class QueryItem(BaseModel): +# query: str + +# class PageItem(BaseModel): +# url: str + +class QueryItemV2(BaseModel): + query: str + top_k: Optional[int] = Field(None) +class PageItemV2(BaseModel): + url: str + query: Optional[str] = Field(None) + +# @router.get("/tools/bing/search") +# async def bing_search(item: QueryItem): +# try: +# search_results = bing_api.search(item.query) +# except RuntimeError as e: +# raise HTTPException(status_code=500, detail=str(e)) +# return search_results + +# @router.get("/tools/bing/load_page") +# async def load_page(item: PageItem): +# try: +# page_loaded, page_detail = bing_api.load_page(item.url) +# except RuntimeError as e: +# raise HTTPException(status_code=500, detail=str(e)) +# if not page_loaded: +# raise HTTPException(status_code=500, detail=page_detail) +# return {"page_content": page_detail} + +@router.get("/tools/bing/image_search") +async def image_search(item: QueryItemV2): + try: + if item.top_k == None: + item.top_k = 10 + search_results = image_search_api.search_image(item.query,item.top_k) + except RuntimeError as e: + raise HTTPException(status_code=500, detail=str(e)) + return search_results + +@router.get("/tools/bing/searchv2") +async def bing_search_v2(item: QueryItemV2): + try: + if item.top_k == None: + item.top_k = 5 + search_results = bing_api_v2.search(item.query,item.top_k) + except RuntimeError as e: + raise HTTPException(status_code=500, detail=str(e)) + return search_results + +@router.get("/tools/bing/load_pagev2") +async def load_page_v2(item: PageItemV2): + result = {"page_content": ""} + try: + raw_page_content = bing_api_v2.load_page(item.url) + page_token_num = num_tokens_from_string(raw_page_content) + if(page_token_num <= 4096): + result = {"page_content": raw_page_content} + else: + if item.query == None: + summarized_page_content = bing_api_v2.summarize_loaded_page(raw_page_content) + result = {"page_content": summarized_page_content} + else: + attended_content = bing_api_v2.attended_loaded_page(raw_page_content,item.query) + result = {"page_content": attended_content} + except RuntimeError as e: + raise HTTPException(status_code=500, detail=str(e)) + return result \ No newline at end of file diff --git a/friday/api/bing/image_search_api.py b/friday/api/bing/image_search_api.py new file mode 100644 index 0000000..f1225eb --- /dev/null +++ b/friday/api/bing/image_search_api.py @@ -0,0 +1,45 @@ +import requests +from bs4 import BeautifulSoup +from typing import Tuple +from enum import Enum + +SEARCH_RESULT_LIST_CHUNK_SIZE = 3 +RESULT_TARGET_PAGE_PER_TEXT_COUNT = 500 + + +class ImageSearchAPI: + def __init__(self, subscription_key: str) -> None: + self._headers = { + 'Ocp-Apim-Subscription-Key': subscription_key, + 'BingAPIs-Market': 'en-US', + + } + self._endpoint = "https://api.bing.microsoft.com/v7.0/images/search" + self._mkt = 'en-US' + + def search_image(self, key_words: str,top_k: int=10, max_retry: int = 3): + + for _ in range(max_retry): + try: + result = requests.get(self._endpoint, headers=self._headers, params={'q': key_words, 'mkt': self._mkt,'safeSearch' : 'moderate'}, + timeout=10) + except Exception: + continue + if result.status_code == 200: + result = result.json() + image_List = [] + if result != None: + image_List = [ + { + "imageName": item["name"], + "imageUrl": item["thumbnailUrl"], + "imageSize": item["thumbnail"] + } for item in result["value"] + ] + if(len(image_List) > top_k): + image_List = image_List[:top_k] + return image_List + else: + continue + raise RuntimeError("Failed to access Bing Search API.") + diff --git a/friday/api/bing/test.json b/friday/api/bing/test.json new file mode 100644 index 0000000..2e9cc84 --- /dev/null +++ b/friday/api/bing/test.json @@ -0,0 +1 @@ +{'task_id': 'cffe0e32-c9a6-4c52-9877-78ceb4aaa9fb', 'Question': "An office held a Secret Santa gift exchange where each of its twelve employees was assigned one other employee in the group to present with a gift. Each employee filled out a profile including three likes or hobbies. On the day of the gift exchange, only eleven gifts were given, each one specific to one of the recipient's interests. Based on the information in the document, who did not give a gift?", 'Level': '1', 'Final answer': 'Fred', 'file_name': 'cffe0e32-c9a6-4c52-9877-78ceb4aaa9fb.docx', 'file_path': '/storage/hf-datasets-cache/medium/datasets/60530074150638-config-parquet-and-info-gaia-benchmark-GAIA-6a4b2225/downloads/1593b793b4874121c23969b271774a9a59f8a86f37c05e51bd7a35405efdf5c0', 'Annotator Metadata': {'Steps': '1. Open the document.\n2. Look at gifts and recipient interests.\n3. Match Galileo Galilei biography (could apply to astronomy or books -> Miguel or Micah)\n4. Match fishing reel (only applies to fishing -> Harry)\n5. Match Raku programming guide (Perl language, but could also apply to JavaScript enthusiast - > Fred or Jun)\n6. Match chisel set (could apply to camping or woodworking, but Harry is already fulfilled -> Jun, so Raku guide is for Fred)\n7. Match custom dice (could apply to board games or tabletop RPGs -> Lucy or Sara)\n8. Match “War and Peace” American film copy (could apply to old movies or Audrey Hepburn -> Perry or Alex)\n9. Match yarn (only applies to knitting -> Micah, so the Galileo biography is for Miguel)\n10. Match "One Piece" graphic novel (could apply to books or manga, but Micah already has yarn -> Alex, so the "War and Peace" film is for Perry)\n11. Match "War and Peace" novel (could apply to books or historical fiction novels, but Micah has yarn -> Tyson)\n12. Match Starbucks gift card (only applies to coffee -> Lucy, so the dice are for Sara)\n13. Match foam exercise mat (only applies to yoga -> Georgette)\n14. Note which recipients have gifts (Miguel, Harry, Fred, Jun, Sara, Perry, Micah, Alex, Tyson, Lucy, Georgette) and which does not (Rebecca).\n15. Find who was supposed to give Rebecca a gift (Fred).', 'Number of steps': '15', 'How long did this take?': '15 minutes', 'Tools': '1. Word document access', 'Number of tools': '1'}} \ No newline at end of file diff --git a/friday/api/bing/test.py b/friday/api/bing/test.py new file mode 100644 index 0000000..5145e38 --- /dev/null +++ b/friday/api/bing/test.py @@ -0,0 +1,27 @@ +import requests + +# response = requests.get( +# 'http://127.0.0.1:8079/tools/bing/search', +# json={'query': 'Python'} +# ) +# print(response.json()) + +# response = requests.get( +# 'http://127.0.0.1:8079/tools/bing/load_page', +# json={'url': 'https://www.python.org/'} +# ) +# print(response.json()) +import requests +import json + +headers = {'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML like Gecko) Chrome/52.0.2743.116 Safari/537.36'} +# url="http://101.132.188.137:8079/test?q=1" +url="http://101.132.188.137:8079/tools/bing/load_pagev2" +param = { + 'url': 'https://blog.csdn.net/sjxgghg/article/details/134312033', +} +res = requests.get(url, + headers=headers, + json=param, + timeout=30) +print(res.text) \ No newline at end of file diff --git a/friday/api/bing/web_loader.py b/friday/api/bing/web_loader.py new file mode 100644 index 0000000..6c1a6d2 --- /dev/null +++ b/friday/api/bing/web_loader.py @@ -0,0 +1,153 @@ +import hashlib +import logging +import re +import requests +import pdfplumber +from io import BytesIO +try: + from bs4 import BeautifulSoup +except ImportError: + raise ImportError( + 'Webpage requires extra dependencies. Install with `pip install --upgrade "embedchain[dataloaders]"`' + ) from None + + + +def clean_string(text): + """ + This function takes in a string and performs a series of text cleaning operations. + + Args: + text (str): The text to be cleaned. This is expected to be a string. + + Returns: + cleaned_text (str): The cleaned text after all the cleaning operations + have been performed. + """ + # Replacement of newline characters: + text = text.replace("\n", " ") + + # Stripping and reducing multiple spaces to single: + cleaned_text = re.sub(r"\s+", " ", text.strip()) + + # Removing backslashes: + cleaned_text = cleaned_text.replace("\\", "") + + # Replacing hash characters: + cleaned_text = cleaned_text.replace("#", " ") + + # Eliminating consecutive non-alphanumeric characters: + # This regex identifies consecutive non-alphanumeric characters (i.e., not + # a word character [a-zA-Z0-9_] and not a whitespace) in the string + # and replaces each group of such characters with a single occurrence of + # that character. + # For example, "!!! hello !!!" would become "! hello !". + cleaned_text = re.sub(r"([^\w\s])\1*", r"\1", cleaned_text) + + return cleaned_text + + + +class WebPageLoader: + # Shared session for all instances + _session = requests.Session() + + def load_data(self, url): + """Load data from a web page using a shared requests session.""" + headers = {'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML like Gecko) Chrome/52.0.2743.116 Safari/537.36'} + web_data = {} + content = "" + try: + response = self._session.get(url,headers=headers, timeout=30) + response.raise_for_status() + data = response.content + # Check content type + content_type = response.headers.get('Content-Type', '') + # print(content_type) + if 'html' in content_type: + content = self._get_clean_content(data, url) + + + elif 'pdf' in content_type: + # Open the PDF file using pdfplumber + with pdfplumber.open(BytesIO(response.content)) as pdf: + # Extract text from each page and combine it + content = '\n'.join([page.extract_text() for page in pdf.pages if page.extract_text()]) + + meta_data = {"url": url} + + doc_id = hashlib.sha256((content + url).encode()).hexdigest() + web_data = { + "doc_id": doc_id, + "data": [ + { + "content": content, + "meta_data": meta_data, + } + ], + } + except Exception: + web_data = { + "data": [ + { + "content": "", + "meta_data": "", + } + ], + } + return web_data + + def _get_clean_content(self, html, url) -> str: + soup = BeautifulSoup(html, "html.parser") + original_size = len(str(soup.get_text())) + + tags_to_exclude = [ + "nav", + "aside", + "form", + "header", + "noscript", + "svg", + "canvas", + "footer", + "script", + "style", + ] + for tag in soup(tags_to_exclude): + tag.decompose() + + ids_to_exclude = ["sidebar", "main-navigation", "menu-main-menu"] + for id in ids_to_exclude: + tags = soup.find_all(id=id) + for tag in tags: + tag.decompose() + + classes_to_exclude = [ + "elementor-location-header", + "navbar-header", + "nav", + "header-sidebar-wrapper", + "blog-sidebar-wrapper", + "related-posts", + ] + for class_name in classes_to_exclude: + tags = soup.find_all(class_=class_name) + for tag in tags: + tag.decompose() + + content = soup.get_text() + content = clean_string(content) + + cleaned_size = len(content) + if original_size != 0: + logging.info( + f"[{url}] Cleaned page size: {cleaned_size} characters, down from {original_size} (shrunk: {original_size-cleaned_size} chars, {round((1-(cleaned_size/original_size)) * 100, 2)}%)" # noqa:E501 + ) + + return content + + @classmethod + def close_session(cls): + cls._session.close() + + diff --git a/friday/api/calculator/__init__.py b/friday/api/calculator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/api/calculator/calculator.py b/friday/api/calculator/calculator.py new file mode 100644 index 0000000..c5647a6 --- /dev/null +++ b/friday/api/calculator/calculator.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from simpleeval import simple_eval, SimpleEval + +router = APIRouter() + +class Expression(BaseModel): + expression: str + + +@router.post("/tools/calculator") +def evaluate(expression: Expression): + try: + s = SimpleEval() + result = s.eval(expression.expression) + return {"result": str(result), "error": None} + except Exception as e: + return {"result": None, "error": str(e)} diff --git a/friday/api/calculator/test.py b/friday/api/calculator/test.py new file mode 100644 index 0000000..74de469 --- /dev/null +++ b/friday/api/calculator/test.py @@ -0,0 +1,10 @@ +import requests +import json + +# 测试加法 +expression = "((46210 - 8*9068) / (2 - x))" +response = requests.post( + 'http://127.0.0.1:8079/tools/calculator', + json={'expression': expression} +) +print(response.json()) \ No newline at end of file diff --git a/friday/api/chemical/__init__.py b/friday/api/chemical/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/api/chemical/chemical.py b/friday/api/chemical/chemical.py new file mode 100644 index 0000000..35fdb61 --- /dev/null +++ b/friday/api/chemical/chemical.py @@ -0,0 +1,99 @@ +import random + +from fastapi import APIRouter +from pydantic import BaseModel +from typing import List, Optional, Union +from .chemical_prop_api import ChemicalPropAPI + +router = APIRouter() + + +class GetNameResponse(BaseModel): + """name list""" + names: List[str] + + +class GetStructureResponse(BaseModel): + """structure list""" + state: int + content: Optional[str] = None + + +class GetIDResponse(BaseModel): + state: int + content: Union[str, List[str]] + + +chemical_prop_api = ChemicalPropAPI + + +@router.get("/tools/chemical/get_name", response_model=GetNameResponse) +def get_name(cid: str): + """prints the possible 3 synonyms of the queried compound ID""" + ans = chemical_prop_api.get_name_by_cid(cid, top_k=3) + return { + "names": ans + } + + +@router.get("/tools/chemical/get_allname", response_model=GetNameResponse) +def get_allname(cid: str): + """prints all the possible synonyms (might be too many, use this function carefully). + """ + ans = chemical_prop_api.get_name_by_cid(cid) + return { + "names": ans + } + + +@router.get("/tools/chemical/get_id_by_struct", response_model=GetIDResponse) +def get_id_by_struct(smiles: str): + """prints the ID of the queried compound SMILES. This should only be used if smiles is provided or retrieved in the previous step. The input should not be a string, but a SMILES formula. + """ + cids = chemical_prop_api.get_cid_by_struct(smiles) + if len(cids) == 0: + return { + "state": "no result" + } + else: + return { + "state": "matched", + "content": cids[0] + } + + +@router.get("/tools/chemical/get_id", response_model=GetIDResponse) +def get_id(name: str): + """prints the ID of the queried compound name, and prints the possible 5 names if the queried name can not been precisely matched, + """ + cids = chemical_prop_api.get_cid_by_name(name) + if len(cids) > 0: + return { + "state": "precise", + "content": cids[0] + } + + cids = chemical_prop_api.get_cid_by_name(name, name_type="word") + if len(cids) > 0: + if name in get_name(cids[0]): + return { + "state": "precise", + "content": cids[0] + } + + ans = [] + random.shuffle(cids) + for cid in cids[:5]: + nms = get_name(cid) + ans.append(nms) + return { + "state": "not precise", + "content": ans + } + + +@router.get("/tools/chemical/get_prop") +def get_prop(cid: str): + """prints the properties of the queried compound ID + """ + return chemical_prop_api.get_prop_by_cid(cid) diff --git a/friday/api/chemical/chemical_prop_api.py b/friday/api/chemical/chemical_prop_api.py new file mode 100644 index 0000000..363803f --- /dev/null +++ b/friday/api/chemical/chemical_prop_api.py @@ -0,0 +1,51 @@ +import json +from typing import Optional, List + +import requests +from bs4 import BeautifulSoup + + +class ChemicalPropAPI: + def __init__(self) -> None: + self._endpoint = "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/" + + def get_name_by_cid(self, cid: str, top_k: Optional[int] = None) -> List[str]: + html_doc = requests.get(f"{self._endpoint}cid/{cid}/synonyms/XML").text + soup = BeautifulSoup(html_doc, "html.parser", from_encoding="utf-8") + syns = soup.find_all('synonym') + ans = [] + if top_k is None: + top_k = len(syns) + for syn in syns[:top_k]: + ans.append(syn.text) + return ans + + def get_cid_by_struct(self, smiles: str) -> List[str]: + html_doc = requests.get(f"{self._endpoint}smiles/{smiles}/cids/XML").text + soup = BeautifulSoup(html_doc, "html.parser", from_encoding="utf-8") + cids = soup.find_all('cid') + if cids is None: + return [] + ans = [] + for cid in cids: + ans.append(cid.text) + return ans + + def get_cid_by_name(self, name: str, name_type: Optional[str] = None) -> List[str]: + url = f"{self._endpoint}name/{name}/cids/XML" + if name_type is not None: + url += f"?name_type={name_type}" + html_doc = requests.get(url).text + soup = BeautifulSoup(html_doc, "html.parser", from_encoding="utf-8") + cids = soup.find_all('cid') + if cids is None: + return [] + ans = [] + for cid in cids: + ans.append(cid.text) + return ans + + def get_prop_by_cid(self, cid: str) -> str: + html_doc = requests.get( + f"{self._endpoint}cid/{cid}/property/MolecularFormula,MolecularWeight,CanonicalSMILES,IsomericSMILES,IUPACName,XLogP,ExactMass,MonoisotopicMass,TPSA,Complexity,Charge,HBondDonorCount,HBondAcceptorCount,RotatableBondCount,HeavyAtomCount,CovalentUnitCount/json").text + return json.loads(html_doc)['PropertyTable']['Properties'][0] \ No newline at end of file diff --git a/friday/api/database/__init__.py b/friday/api/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/api/database/database.py b/friday/api/database/database.py new file mode 100644 index 0000000..881b92a --- /dev/null +++ b/friday/api/database/database.py @@ -0,0 +1,44 @@ +import datetime +from typing import List +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +import sqlite3 + +router = APIRouter() + + +class SQLRequest(BaseModel): + queries: List[str] + + +def execute_sql(queries: List[str]): + conn = sqlite3.connect('./tasks/travel/database/travel.db') + cursor = conn.cursor() + + results = [] + for query in queries: + try: + cursor.execute(query) + results.append({ + "query": query, + "result": cursor.fetchall(), + "error": "" + }) + except Exception as e: + results.append({ + "query": query, + "result": "", + "error": str(e) + }) + + # Commit changes and close the connection to the database + conn.commit() + conn.close() + + return results + + +@router.post("/tools/database") +async def execute_sqlite(req: SQLRequest): + print(f"{datetime.datetime.now()}:{req}") + return execute_sql(req.queries) diff --git a/friday/api/database/test.py b/friday/api/database/test.py new file mode 100644 index 0000000..7a01728 --- /dev/null +++ b/friday/api/database/test.py @@ -0,0 +1,42 @@ +import requests +import json + +# 你的API的URL +url = "http://localhost:8079/tools/database" + +# 准备的SQL查询 +queries = { + "queries": [ + '''SELECT * FROM railway +WHERE origin = 'Beijing' + AND destination = 'Shanghai' + AND DATE(departure_time) = '2023-07-08' +ORDER BY departure_time;''' + ] +} +# queries = {"queries":["SELECT * FROM railway\nWHERE origin = 'Shanghai'\n AND destination = 'Hangzhou'\n AND DATE(departure_time) = '2023-07-04';"] +# } + +# 发送POST请求 +response = requests.post(url, json=queries) + +# 打印返回的结果 +print(json.dumps(response.json(), indent=4)) + + +def query_database(query): + try: + response = requests.post( + "http://localhost:8079/tools/database", + json={'queries': query} + ).json() + return json.dumps(response, indent=4) + except Exception as e: + print(f'run error{e}') + + +query = [ + "SELECT * FROM railway\nWHERE origin = 'Shanghai'\n AND destination = 'Beijing'\n AND DATE(departure_time) = '2023-07-01';", + "SELECT * FROM railway\nWHERE origin = 'Beijing'\n AND destination = 'Hangzhou'\n AND DATE(departure_time) = '2023-07-04';", + "SELECT * FROM railway\nWHERE origin = 'Hangzhou'\n AND destination = 'Shanghai'\n AND DATE(departure_time) = '2023-07-07';"] +print(query_database(query)) diff --git a/friday/api/gmail/__init__.py b/friday/api/gmail/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/api/gmail/gmail.py b/friday/api/gmail/gmail.py new file mode 100644 index 0000000..b96eb13 --- /dev/null +++ b/friday/api/gmail/gmail.py @@ -0,0 +1,86 @@ +import os +from google.oauth2.credentials import Credentials +from google_auth_oauthlib.flow import InstalledAppFlow +from google.auth.transport.requests import Request as google_request +from googleapiclient.discovery import build +import base64 +from email.mime.text import MIMEText +import pickle + +SCOPES = ['https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/gmail.readonly', + 'https://www.googleapis.com/auth/calendar'] + + +def get_service(): + creds = None + # 尝试从 "token.pickle" 文件中加载凭据 + if os.path.exists('token.pickle'): + with open('token.pickle', 'rb') as token: + creds = pickle.load(token) + if creds and not creds.valid: + if creds.expired and creds.refresh_token: + creds.refresh(google_request()) + else: + flow = InstalledAppFlow.from_client_secrets_file( + './.auth/calendar.json', SCOPES) + creds = flow.run_local_server(port=0) + service = build('gmail', 'v1', credentials=creds) + return service + + +def send_email(service, from_email, to_email, subject, content): + message = MIMEText(content) + message['to'] = to_email + message['from'] = from_email + message['subject'] = subject + raw_message = base64.urlsafe_b64encode(message.as_string().encode("utf-8")) + message = {'raw': raw_message.decode("utf-8")} + message = (service.users().messages().send(userId='me', body=message).execute()) + + +from fastapi import APIRouter +import json +import base64 +from email.mime.text import MIMEText +from pydantic import BaseModel + +router = APIRouter() + + +class EmailSchema(BaseModel): + from_email: str + to_email: str + subject: str + content: str + + +@router.post("/gmail/send") +def send_test_email(email: EmailSchema): + try: + service = get_service() + send_email(service, email.from_email, email.to_email, email.subject, email.content) # 注意这里从email对象中取字段 + return {"result": "Email sent successfully", "error": None} + except Exception as e: + return {"result": None, "error": str(e)} + + +# token.pickle文件包含了与特定Gmail账户关联的访问令牌 +@router.get("/gmail/list") +def list_recent_emails(): + try: + service = get_service() + results = service.users().messages().list(userId='me', labelIds=['INBOX'], maxResults=10).execute() + messages = results.get('messages', []) + emails = [] + for message in messages: + msg = service.users().messages().get(userId='me', id=message['id']).execute() + email_data = msg['payload']['headers'] + for values in email_data: + name = values['name'] + if name == 'From': + from_name = values['value'] + subject = msg['snippet'] + emails.append({"from": from_name, "subject": subject}) + return {"emails": emails, "error": None} + except Exception as e: + return {"emails": None, "error": str(e)} diff --git a/friday/api/gmail/test.py b/friday/api/gmail/test.py new file mode 100644 index 0000000..9200278 --- /dev/null +++ b/friday/api/gmail/test.py @@ -0,0 +1,38 @@ +import os + +import requests +import json + +# 基础URL +BASE_URL = "http://127.0.0.1:8079" + +# 测试邮件发送API +def test_send_email(): + print("Testing: Send Email API") + + data = { + "from_email": "wyx7653@gmail.com", + "to_email": "2115492705@qq.com", + "subject": "Test Subject", + "content": "This is a test email." + } + + response = requests.post(f"{BASE_URL}/gmail/send", json=data) + if response.status_code == 200: + print(f"Success: {response.json()}") + else: + print(f"Failure: {response.json()}") + +# 测试获取最近邮件列表API +def test_list_recent_emails(): + print("Testing: List Recent Emails API") + + response = requests.get(f"{BASE_URL}/gmail/list") + if response.status_code == 200: + print(f"Success: {response.json()}") + else: + print(f"Failure: {response.json()}") + +if __name__ == "__main__": + test_send_email() + test_list_recent_emails() \ No newline at end of file diff --git a/friday/api/google_calendar/__init__.py b/friday/api/google_calendar/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/api/google_calendar/calendar_service.py b/friday/api/google_calendar/calendar_service.py new file mode 100644 index 0000000..4534211 --- /dev/null +++ b/friday/api/google_calendar/calendar_service.py @@ -0,0 +1,57 @@ +# 开启Google Calendar API并下载凭据: 访问Google Cloud Console,创建一个新的项目并启用Google Calendar API。下载生成的credentials.json文件。 +import os + +from fastapi import APIRouter +from pydantic import BaseModel, Field +from google_auth_oauthlib.flow import InstalledAppFlow +from google.auth.transport.requests import Request as google_request +from googleapiclient.discovery import build +import pickle + +# 如果修改了SCOPES,请删除文件token.pickle。 +SCOPES = ['https://www.googleapis.com/auth/gmail.send','https://www.googleapis.com/auth/gmail.readonly','https://www.googleapis.com/auth/calendar'] + + + +def get_service(): + creds = None + # 尝试从 "token.pickle" 文件中加载凭据 + if os.path.exists('token.pickle'): + with open('token.pickle', 'rb') as token: + creds = pickle.load(token) + + # 如果凭据无效,重新获取 + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + creds.refresh(google_request()) + else: + flow = InstalledAppFlow.from_client_secrets_file( + './.auth/calendar.json', SCOPES) + creds = flow.run_local_server(port=0) + + # 保存新的凭据到 "token.pickle" 文件 + with open('token.pickle', 'wb') as token: + pickle.dump(creds, token) + + service = build('calendar', 'v3', credentials=creds) + return service +router = APIRouter() + + +class CalendarEvent(BaseModel): + summary: str + location: str + description: str + start: dict = Field(..., example={"dateTime": "2023-07-31T15:00:00", "timeZone": "Asia/Shanghai"}) + end: dict = Field(..., example={"dateTime": "2023-07-31T16:00:00", "timeZone": "Asia/Shanghai"}) + + +@router.post("/calendar/insert_event") +def insert_event(event: CalendarEvent): + try: + # 这里你可以调用Google Calendar API + service = get_service() # 从你原来的代码获取service + inserted_event = service.events().insert(calendarId='primary', body=event.dict()).execute() + return {"result": f'Event created: {inserted_event["htmlLink"]}', "error": None} + except Exception as e: + return {"result": None, "error": str(e)} diff --git a/friday/api/google_calendar/test.py b/friday/api/google_calendar/test.py new file mode 100644 index 0000000..decdd41 --- /dev/null +++ b/friday/api/google_calendar/test.py @@ -0,0 +1,25 @@ +import requests +import json + +# 用于测试的日历事件 +test_event = { + "summary": "NLUI会议", + "location": "上海", + "description": "这是一个关于NLUI的会议", + "start": { + "dateTime": "2023-08-28T10:30:00", + "timeZone": "Asia/Shanghai" + }, + "end": { + "dateTime": "2023-08-28T11:30:00", # 假设会议时长为1小时 + "timeZone": "Asia/Shanghai" + } +} + +# 向API发送请求 +response = requests.post("http://127.0.0.1:8079/calendar/insert_event", json=test_event) + +# 解析响应 +if response.status_code == 200: + data = response.json() + print(data) \ No newline at end of file diff --git a/friday/api/image_caption/birds.jpg b/friday/api/image_caption/birds.jpg new file mode 100755 index 0000000..51a47d1 Binary files /dev/null and b/friday/api/image_caption/birds.jpg differ diff --git a/friday/api/image_caption/gpt4v_caption.py b/friday/api/image_caption/gpt4v_caption.py new file mode 100644 index 0000000..0075433 --- /dev/null +++ b/friday/api/image_caption/gpt4v_caption.py @@ -0,0 +1,45 @@ +from openai import OpenAI +import os + + +os.environ["OPENAI_API_KEY"] = "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" +os.environ["OPENAI_ORGANIZATION"] = "org-fSyygvftM73W0pK4VjoK395W" + +class ImageCaptionTool: + def __init__(self) -> None: + self.client = OpenAI() + def caption(self,url,query="What's in this Image?"): + response = self.client.chat.completions.create( + model="gpt-4-vision-preview", + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": query}, + { + "type": "image_url", + "image_url": { + "url": url, + }, + }, + ], + } + ], + max_tokens=300, + ) + return response.choices[0].message.content + + +# tool = ImageCaptionTool() +# import base64 +# # Function to encode the image +# def encode_image(image_path): +# with open(image_path, "rb") as image_file: +# return base64.b64encode(image_file.read()).decode('utf-8') +# # Path to your image +# image_path = "birds.jpg" + +# # Getting the base64 string +# base64_image = encode_image(image_path) +# res = tool.caption(url=f"data:image/jpeg;base64,{base64_image}") +# print(res) \ No newline at end of file diff --git a/friday/api/image_caption/image_caption_service.py b/friday/api/image_caption/image_caption_service.py new file mode 100644 index 0000000..6ff741d --- /dev/null +++ b/friday/api/image_caption/image_caption_service.py @@ -0,0 +1,37 @@ +from fastapi import APIRouter, HTTPException,UploadFile,File,Form, Depends +from pydantic import BaseModel,Field +from typing import Optional +from .gpt4v_caption import ImageCaptionTool +import base64 + +router = APIRouter() + +image_caption_api = ImageCaptionTool() + + +# class CaptionQueryItem(BaseModel): +# query: Optional[str] = "What's in this image?" +# url: Optional[str] = None +# image_file: Optional[UploadFile] = File(None) + +async def caption_parameters(query: Optional[str] = Form("What's in this image?"),url: Optional[str] = Form(None),image_file: Optional[UploadFile] = File(None)): + return {"query":query,"url":url,"image_file":image_file} + +@router.post("/tools/image_caption") +async def image_search(item: dict = Depends(caption_parameters)): + try: + if(item["query"] == None): + item["query"] = "What's in this image?" + if(item["url"] == None and item["image_file"] == None): + return {"error":"Invalid picture"} + image_url="" + if(item["url"] != None and item["image_file"] == None): + image_url = item["url"] + elif(item["image_file"] != None): + base64Img = base64.b64encode(await item["image_file"].read()).decode('utf-8') + image_url = f"data:image/jpeg;base64,{base64Img}" + caption = image_caption_api.caption(url=image_url,query=item["query"]) + except RuntimeError as e: + raise HTTPException(status_code=500, detail=str(e)) + return {"caption":caption} + diff --git a/friday/api/markdown/__init__.py b/friday/api/markdown/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/api/markdown/markdown_service.py b/friday/api/markdown/markdown_service.py new file mode 100644 index 0000000..a12ac8b --- /dev/null +++ b/friday/api/markdown/markdown_service.py @@ -0,0 +1,27 @@ +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel,Field +from typing import Optional +from .webpage2md import WebPage2MDTool +import tiktoken + + + +router = APIRouter() + +web2MdTool = WebPage2MDTool() + +class TargetPageModel(BaseModel): + url: str + + + + +@router.get("/tools/markdown/web2md") +async def get_web_md(item: TargetPageModel): + result = {"markdown": ""} + try: + markdown_text = web2MdTool.get_web_md(item.url) + result["markdown"] = markdown_text + except RuntimeError as e: + raise HTTPException(status_code=500, detail=str(e)) + return result \ No newline at end of file diff --git a/friday/api/markdown/webpage2md.py b/friday/api/markdown/webpage2md.py new file mode 100644 index 0000000..b07d0a2 --- /dev/null +++ b/friday/api/markdown/webpage2md.py @@ -0,0 +1,88 @@ +import re +import requests +import html2text as ht +from urllib.parse import urljoin +try: + from bs4 import BeautifulSoup +except ImportError: + raise ImportError( + 'Webpage requires extra dependencies. Install with `pip install --upgrade "embedchain[dataloaders]"`' + ) from None + +class WebPage2MDTool: + _session = requests.Session() + def get_web_md(self, url): + """Load data from a web page using a shared requests session.""" + headers = {'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML like Gecko) Chrome/52.0.2743.116 Safari/537.36'} + try: + response = self._session.get(url,headers=headers, timeout=30) + response.raise_for_status() + data = response.content + content = self._get_clean_content(data, url) + text_maker = ht.HTML2Text() + md_text = text_maker.handle(content) + except Exception: + md_text = "error loading markdown of current webpage" + return md_text + def _get_clean_content(self, html, url) -> str: + soup = BeautifulSoup(html, "html.parser") + original_size = len(str(soup.get_text())) + + tags_to_exclude = [ + "nav", + "aside", + "form", + "header", + "noscript", + "svg", + "canvas", + "footer", + "script", + "style", + ] + for tag in soup(tags_to_exclude): + tag.decompose() + + ids_to_exclude = ["sidebar", "main-navigation", "menu-main-menu"] + for id in ids_to_exclude: + tags = soup.find_all(id=id) + for tag in tags: + tag.decompose() + + classes_to_exclude = [ + "elementor-location-header", + "navbar-header", + "nav", + "header-sidebar-wrapper", + "blog-sidebar-wrapper", + "related-posts", + ] + for class_name in classes_to_exclude: + tags = soup.find_all(class_=class_name) + for tag in tags: + tag.decompose() + # 将相对路径转绝对路径 + # 查找所有带有href属性的标签 + for link in soup.find_all('a', href=True): + absolute_url = urljoin(url, link['href']) + link['href'] = absolute_url + + # 查找所有带有src属性的标签 + for img in soup.find_all('img', src=True): + absolute_url = urljoin(url, img['src']) + img['src'] = absolute_url + content = str(soup) + + return content + + @classmethod + def close_session(cls): + cls._session.close() +<<<<<<< HEAD + + + +# res = WebPage2MDTool().get_web_md("https://lividwo.github.io/zywu.github.io/") +# print(type(res)) +======= +>>>>>>> e40e1b5ed1a1e36395d5da5f5a83923837c0864e diff --git a/friday/api/ppt/__init__.py b/friday/api/ppt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/api/ppt/ppt.py b/friday/api/ppt/ppt.py new file mode 100644 index 0000000..8ff10b1 --- /dev/null +++ b/friday/api/ppt/ppt.py @@ -0,0 +1,115 @@ +from fastapi import APIRouter, Depends +from pydantic import BaseModel +from pptx import Presentation +import os +import time +import requests + +router = APIRouter() + +CWD = os.getcwd() # path of current working directory +LIB_DIR = os.path.dirname(__file__) # path of library +TEMPLATE_DIR = os.path.join(LIB_DIR, "templates") # path of templates +CACHE_DIR = os.path.join(CWD, "cache") # path of cache_dir +IMAGE_BED_PATTERN = 'https://source.unsplash.com/featured/?{}' # url pattern for image bed + +if not os.path.exists(CACHE_DIR): + os.makedirs(CACHE_DIR) + +ppt_file = None # a pointer to the powerpoint object + + +class CreateFileModel(BaseModel): + theme: str + + +class GetImageModel(BaseModel): + keywords: str + + +class AddFirstPageModel(BaseModel): + title: str + subtitle: str + + +class AddTextPageModel(BaseModel): + title: str + bullet_items: str + + +class AddTextImagePageModel(BaseModel): + title: str + bullet_items: str + image: str + + +@router.post("/tools/ppt/create_file") +async def create_file(item: CreateFileModel): + global ppt_file + ppt_file = Presentation(os.path.join(TEMPLATE_DIR, f"{item.theme}.pptx")) + return "created a ppt file." + + +@router.post("/tools/ppt/get_image") +async def get_image(item: GetImageModel): + picture_url = IMAGE_BED_PATTERN.format(item.keywords) + response = requests.get(picture_url) + img_local_path = os.path.join(CACHE_DIR, f"{time.time()}.jpg") + with open(img_local_path, 'wb') as f: + f.write(response.content) + return img_local_path + + +@router.post("/tools/ppt/add_first_page") +async def add_first_page(item: AddFirstPageModel): + global ppt_file + slide = ppt_file.slides.add_slide(ppt_file.slide_layouts[0]) # layout for first page (title and subtitle only) + title_shape = slide.shapes.title + subtitle_shape = slide.placeholders[1] + title_shape.text = item.title + subtitle_shape.text = item.subtitle + return "added first page." + + +@router.post("/tools/ppt/add_text_page") +async def add_text_page(item: AddTextPageModel): + global ppt_file + slide = ppt_file.slides.add_slide(ppt_file.slide_layouts[1]) + title_shape = slide.shapes.title + body_shape = slide.placeholders[1] + title_shape.text = item.title + tf = body_shape.text_frame + bullet_items = item.bullet_items.split("[SPAN]") + for bullet_item in bullet_items: + bullet_item_strip = bullet_item.strip() + p = tf.add_paragraph() + p.text = bullet_item_strip + p.level = 1 + return "added text page." + + +@router.post("/tools/ppt/add_text_image_page") +async def add_text_image_page(item: AddTextImagePageModel): + global ppt_file + slide = ppt_file.slides.add_slide(ppt_file.slide_layouts[3]) + title_shape = slide.shapes.title + title_shape.text = item.title + body_shape = slide.placeholders[1] + tf = body_shape.text_frame + bullet_items = item.bullet_items.split("[SPAN]") + for bullet_item in bullet_items: + bullet_item_strip = bullet_item.strip() + p = tf.add_paragraph() + p.text = bullet_item_strip + p.level = 1 + image_shape = slide.placeholders[2] + slide.shapes.add_picture(item.image, image_shape.left, image_shape.top, image_shape.width, image_shape.height) + return "added text and image page." + + +@router.get("/tools/ppt/submit_file") +async def submit_file(): + global ppt_file + file_path = os.path.join(CACHE_DIR, f"{time.time()}.pptx") + ppt_file.save(file_path) + return f"submitted. view ppt at {file_path}" diff --git a/friday/api/ppt/templates/flat.pptx b/friday/api/ppt/templates/flat.pptx new file mode 100644 index 0000000..8a6260a Binary files /dev/null and b/friday/api/ppt/templates/flat.pptx differ diff --git a/friday/api/ppt/templates/green.pptx b/friday/api/ppt/templates/green.pptx new file mode 100644 index 0000000..e63213c Binary files /dev/null and b/friday/api/ppt/templates/green.pptx differ diff --git a/friday/api/ppt/templates/orange.pptx b/friday/api/ppt/templates/orange.pptx new file mode 100644 index 0000000..3ca6f86 Binary files /dev/null and b/friday/api/ppt/templates/orange.pptx differ diff --git a/friday/api/ppt/templates/tech.pptx b/friday/api/ppt/templates/tech.pptx new file mode 100644 index 0000000..f84cab3 Binary files /dev/null and b/friday/api/ppt/templates/tech.pptx differ diff --git a/friday/api/ppt/templates/wooden.pptx b/friday/api/ppt/templates/wooden.pptx new file mode 100644 index 0000000..6e004ac Binary files /dev/null and b/friday/api/ppt/templates/wooden.pptx differ diff --git a/friday/api/ppt/test.py b/friday/api/ppt/test.py new file mode 100644 index 0000000..7141277 --- /dev/null +++ b/friday/api/ppt/test.py @@ -0,0 +1,16 @@ +import requests + +ppt_url='http://localhost:8079/tools/ppt' + +#创建文件 +requests.post(f'{ppt_url}/create_file', json={"theme": "tech"}) +#获取图片 +response = requests.post(f'{ppt_url}/get_image', json={"keywords": "programming"}) +image_path = response.json() +#加一页 +requests.post(f'{ppt_url}/add_first_page', json={"title": "About Me", "subtitle": "A brief introduction"}) +requests.post(f'{ppt_url}/add_text_page', json={"title": "Education", "bullet_items": "Bachelor's Degree in Computer Science[SPAN]Master's Degree in Data Science"}) +requests.post(f'{ppt_url}/add_text_image_page', json={"title": "Skills", "bullet_items": "Programming[SPAN]Data Analysis[SPAN]Machine Learning", "image": image_path}) +response = requests.get(f'{ppt_url}/submit_file') +file_path = response.json() +print(file_path) \ No newline at end of file diff --git a/friday/api/python/__init__.py b/friday/api/python/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/api/python/interpreter.py b/friday/api/python/interpreter.py new file mode 100644 index 0000000..c405a14 --- /dev/null +++ b/friday/api/python/interpreter.py @@ -0,0 +1,108 @@ +import ast +import os +import asyncio +import subprocess + +import astor +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +router = APIRouter() + + +class Item(BaseModel): + code: str + + +def modify_code_to_print_last_expr(code: str): + # Parse the code using AST + tree = ast.parse(code, mode='exec') + + # Check if the last node is an expression and not a print statement + last_node = tree.body[-1] + if isinstance(last_node, ast.Expr) and not ( + isinstance(last_node.value, ast.Call) and getattr(last_node.value.func, 'id', None) == 'print'): + # Create a new print node + print_node = ast.Expr( + value=ast.Call(func=ast.Name(id='print', ctx=ast.Load()), args=[last_node.value], keywords=[])) + # Copy line number and column offset from the last expression + print_node.lineno = last_node.lineno + print_node.col_offset = last_node.col_offset + # Replace the last expression with the print statement + tree.body[-1] = print_node + + # Use astor to convert the modified AST back to source code + modified_code = astor.to_source(tree) + return modified_code + + +async def run_code(code: str): + + try: + code = modify_code_to_print_last_expr(code) + # Write the code to a file + with open("code.py", "w") as f: + f.write(code) + with open("code_temp.py", "w") as f: + f.write(code) + # Run the file with a timeout of 3 seconds + process = await asyncio.create_subprocess_shell( + "python code.py", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=3) + + # Decode the stdout and stderr + result = stdout.decode("utf-8") + error = stderr.decode("utf-8") + + return {"result": result, "error": error} + except asyncio.TimeoutError: + process.terminate() + await process.wait() + return {"result": "", "error": "Code execution timed out"} + except Exception as e: + return {"result": "", "error": str(e)} + finally: + # Delete the code file + if os.path.exists("code.py"): + os.remove("code.py") + + +@router.post("/tools/python") +async def execute_python(item: Item): + result = await run_code(item.code) + return result + +# import io +# import traceback +# from contextlib import redirect_stdout +# from fastapi import APIRouter, HTTPException +# from pydantic import BaseModel +# from concurrent.futures import ThreadPoolExecutor, TimeoutError +# +# router = APIRouter() +# +# executor = ThreadPoolExecutor(max_workers=1) +# +# class Item(BaseModel): +# code: str +# +# def execute_code(code): +# f = io.StringIO() +# with redirect_stdout(f): +# try: +# exec(code) +# return {"result": f.getvalue(), "error": None} +# except Exception as e: +# return {"result": f.getvalue(), "error": traceback.format_exc().split('exec(code)\n ')[-1]} +# +# @router.post("/tools/python") +# async def execute_python(item: Item): +# future = executor.submit(execute_code, item.code) +# try: +# result = future.result(timeout=3) # Wait for the result or timeout after 3 seconds +# except TimeoutError: +# return {"result": None, "error": "TimeoutError"} +# return result diff --git a/friday/api/python/temp.py b/friday/api/python/temp.py new file mode 100644 index 0000000..ed0f110 --- /dev/null +++ b/friday/api/python/temp.py @@ -0,0 +1 @@ +print('hello') \ No newline at end of file diff --git a/friday/api/python/test.py b/friday/api/python/test.py new file mode 100644 index 0000000..460e96a --- /dev/null +++ b/friday/api/python/test.py @@ -0,0 +1,14 @@ +import requests +import json + +code = """ +import heapq as hq\r\nfrom collections import Counter\r\n\r\ndef func(lists, k):\r\n nums = []\r\n for lst in lists:\r\n nums.extend(lst)\r\n count = Counter(nums)\r\n top_k = hq.nlargest(k, count, key=count.get)\r\n return top_k\nassert func([[1, 2, 6], [1, 3, 4, 5, 7, 8], [1, 3, 5, 6, 8, 9], [2, 5, 7, 11], [1, 4, 7, 8, 12]],3)==[5, 7, 1]\nassert func([[1, 2, 6], [1, 3, 4, 5, 7, 8], [1, 3, 5, 6, 8, 9], [2, 5, 7, 11], [1, 4, 7, 8, 12]],1)==[1]\nassert func([[1, 2, 6], [1, 3, 4, 5, 7, 8], [1, 3, 5, 6, 8, 9], [2, 5, 7, 11], [1, 4, 7, 8, 12]],5)==[6, 5, 7, 8, 1]\n +""" +code=""" +print('hello world')""" +response = requests.post( + 'http://127.0.0.1:8079/tools/python', + json={'code': code} +) + +print(response.json()) \ No newline at end of file diff --git a/friday/api/python/test2.py b/friday/api/python/test2.py new file mode 100644 index 0000000..842e262 --- /dev/null +++ b/friday/api/python/test2.py @@ -0,0 +1,15 @@ +import requests +import json + +code = """ +import heapq as hq\r\nfrom collections import Counter\r\n\r\ndef func(lists, k):\r\n nums = []\r\n for lst in lists:\r\n nums.extend(lst)\r\n count = Counter(nums)\r\n top_k = hq.nlargest(k, count, key=count.get)\r\n return top_k\nassert func([[1, 2, 6], [1, 3, 4, 5, 7, 8], [1, 3, 5, 6, 8, 9], [2, 5, 7, 11], [1, 4, 7, 8, 12]],3)==[5, 7, 1]\nassert func([[1, 2, 6], [1, 3, 4, 5, 7, 8], [1, 3, 5, 6, 8, 9], [2, 5, 7, 11], [1, 4, 7, 8, 12]],1)==[1]\nassert func([[1, 2, 6], [1, 3, 4, 5, 7, 8], [1, 3, 5, 6, 8, 9], [2, 5, 7, 11], [1, 4, 7, 8, 12]],5)==[6, 5, 7, 8, 1]\n +""" +code=""" +abcde +fs""" +response = requests.post( + 'http://127.0.0.1:8079/tools/python', + json={'code': code} +) + +print(response.json()) \ No newline at end of file diff --git a/friday/api/shell/__init__.py b/friday/api/shell/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/api/shell/shell.py b/friday/api/shell/shell.py new file mode 100644 index 0000000..1a9c3fb --- /dev/null +++ b/friday/api/shell/shell.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter +from pydantic import BaseModel +import subprocess + +router = APIRouter() + +class ShellCommandModel(BaseModel): + command: str + +class ShellCommandResultModel(BaseModel): + stdout: str + stderr: str + +@router.post("/tools/shell", response_model=ShellCommandResultModel) +async def execute_shell_command(command: ShellCommandModel): + result = subprocess.run(command.command, capture_output=True, shell=True, text=True) + return ShellCommandResultModel(stdout=result.stdout, stderr=result.stderr) diff --git a/friday/api/shell/test.py b/friday/api/shell/test.py new file mode 100644 index 0000000..25de953 --- /dev/null +++ b/friday/api/shell/test.py @@ -0,0 +1,19 @@ +import requests +import json + +base_url = 'http://localhost:8079' + +def run_shell_command(command): + response = requests.post(f'{base_url}/tools/shell', data=json.dumps({"command": command}), headers={'Content-Type': 'application/json'}) + if response.status_code == 200: + print("Command executed successfully") + print("STDOUT: ", response.json()['stdout']) + print("STDERR: ", response.json()['stderr']) + else: + print("Error occurred while executing the command") + +# Create the file in /root directory +run_shell_command("echo 'This is a test file.' > /root/test.txt") + +# Copy the file to the current directory +run_shell_command("cp /root/test.txt ./test2.txt") diff --git a/friday/api/sympy/__init__.py b/friday/api/sympy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/api/sympy/test.py b/friday/api/sympy/test.py new file mode 100644 index 0000000..8a41c53 --- /dev/null +++ b/friday/api/sympy/test.py @@ -0,0 +1,13 @@ +from sympy import symbols, Eq, solve + +x, y = symbols('x y') + +# 设方程组为: +# 3x + 2y = 2 +# x + 2y = 0 +eq1 = Eq(3*x + 2*y, 2) +eq2 = Eq(x + 2*y, 0) + +# 使用 solve 解方程组 +sol = solve((eq1,eq2), (x, y)) +print(sol) \ No newline at end of file diff --git a/friday/api/translate/__init__.py b/friday/api/translate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/api/translate/translate.py b/friday/api/translate/translate.py new file mode 100644 index 0000000..10d02ba --- /dev/null +++ b/friday/api/translate/translate.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter +from pydantic import BaseModel + +router = APIRouter() + +class TranslateRequest(BaseModel): + text: str + src_language: str + dest_language: str + +class TranslateResponse(BaseModel): + translated_text: str + +def translate_text(text: str, src_language: str, dest_language: str) -> str: + """ + Translates the text from source language to destination language. + This function is just a placeholder. You should implement the actual translation here. + """ + # TODO: implement the translation + return text + +@router.post("/tools/translate", response_model=TranslateResponse) +async def translate(request: TranslateRequest) -> TranslateResponse: + translated_text = translate_text(request.text, request.src_language, request.dest_language) + return TranslateResponse(translated_text=translated_text) \ No newline at end of file diff --git a/friday/api/weather/__init__.py b/friday/api/weather/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/api/weather/test.py b/friday/api/weather/test.py new file mode 100644 index 0000000..3c09b53 --- /dev/null +++ b/friday/api/weather/test.py @@ -0,0 +1,23 @@ +# test_weather_api.py + +import requests + +def test_query_weather(): + base_url = "http://127.0.0.1:8079" + date = "2023-07-01" + city = "Beijing" + + # 发送GET请求到/weather/query端点 + response = requests.get(f"{base_url}/weather/query", params={"date": date, "city": city}) + + # 检查响应是否成功 + if response.status_code == 200: + print("Test Passed") + print("Response JSON:", response.json()) + else: + print("Test Failed") + print("Response Status Code:", response.status_code) + print("Response JSON:", response.json()) + +if __name__ == "__main__": + test_query_weather() \ No newline at end of file diff --git a/friday/api/weather/weather.py b/friday/api/weather/weather.py new file mode 100644 index 0000000..4218fe0 --- /dev/null +++ b/friday/api/weather/weather.py @@ -0,0 +1,24 @@ +# api/weather/weather.py +from fastapi import APIRouter, HTTPException, Query +import sqlite3 + +router = APIRouter() + +@router.get("/weather/query") # 注意这里改为GET请求 +def query_weather(date: str, city: str): # 使用Query参数 + try: + conn = sqlite3.connect('./database/weather.db') + c = conn.cursor() + c.execute("SELECT max_temp, min_temp, weather FROM weather WHERE city=? AND date=?", (city, date)) + row = c.fetchone() + conn.close() + + if row: + result=f'{date}, {city}: {row[2]}, {row[1]}-{row[0]} ℃' + return {"result": str(result), "error": None} + else: + {"result": '', "error": 'data not found'} + + except Exception as e: + print(e) + return {"result": '', "error": 'not found'} diff --git a/friday/api/wolfram_alpha/__init__.py b/friday/api/wolfram_alpha/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/api/wolfram_alpha/test.py b/friday/api/wolfram_alpha/test.py new file mode 100644 index 0000000..4a2814a --- /dev/null +++ b/friday/api/wolfram_alpha/test.py @@ -0,0 +1,21 @@ +import requests +import json + +# API endpoint +url = "http://43.159.144.130:8079/tools/wolframalpha" + +# Headers +headers = { + 'Content-Type': 'application/json' +} + +# Data +data = { + "query": "5+6" +} + +# Send the request +response = requests.post(url, headers=headers, data=json.dumps(data)) + +# Print the response +print(response.json()) \ No newline at end of file diff --git a/friday/api/wolfram_alpha/wolfram_alpha.py b/friday/api/wolfram_alpha/wolfram_alpha.py new file mode 100644 index 0000000..13078a3 --- /dev/null +++ b/friday/api/wolfram_alpha/wolfram_alpha.py @@ -0,0 +1,24 @@ +from fastapi import APIRouter +import wolframalpha +from pydantic import BaseModel +from typing import Optional + +class QueryItem(BaseModel): + query: str + +router = APIRouter() + +app_id = "XRY28U-7PVE2LRH7H" # Replace with your app id +client = wolframalpha.Client(app_id) + +@router.post("/tools/wolframalpha") +async def wolframalpha_query(item: QueryItem): + res = client.query(item.query) + + # Handle the query result + if res['@success'] == 'false': + return {"result": "Query failed"} + else: + # Return the first result text + result = next(res.results).text + return {"result": result} \ No newline at end of file diff --git a/friday/atom_action/__init__.py b/friday/atom_action/__init__.py new file mode 100644 index 0000000..172aa53 --- /dev/null +++ b/friday/atom_action/__init__.py @@ -0,0 +1,6 @@ +import sys + +sys.dont_write_bytecode = True +from .operations import * +from .query import * +from .src import * diff --git a/friday/atom_action/operations/__init__.py b/friday/atom_action/operations/__init__.py new file mode 100644 index 0000000..c3debaa --- /dev/null +++ b/friday/atom_action/operations/__init__.py @@ -0,0 +1,8 @@ +import sys + +sys.dont_write_bytecode = True +from .coding import * +from .files import * +from .media import * +from .routine import * +from .system import * diff --git a/friday/atom_action/operations/coding.py b/friday/atom_action/operations/coding.py new file mode 100644 index 0000000..5d7565a --- /dev/null +++ b/friday/atom_action/operations/coding.py @@ -0,0 +1,17 @@ +import sys + +sys.dont_write_bytecode = True +from ..src import * + +def exec_python(file_path: str) -> None: + return python(file_path) + +def open_editor(file_path: str) -> None: + return gedit(file_path) + +def open_vscode(path: str) -> None: + return code(path) + + + + diff --git a/friday/atom_action/operations/files.py b/friday/atom_action/operations/files.py new file mode 100644 index 0000000..02a3aa1 --- /dev/null +++ b/friday/atom_action/operations/files.py @@ -0,0 +1,35 @@ +import sys +import os + +sys.dont_write_bytecode = True +from ..src import * + +def create_dir(dir_path: str, new_dir_name: str) -> None: + assert os.path.exists(dir_path) + return mkdir(os.path.join(dir_path, new_dir_name)) + +def create_file(file_path: str, new_file_name: str) -> None: + assert os.path.exists(file_path) + return touch(os.path.join(file_path, new_file_name)) + +def download_file(url: str, file_name: str) -> None: + return wget(url, "-O", file_name, "--no-check-certificate") + +def copy(src_path: str, dst_path: str) -> None: + assert os.path.exists(src_path) + return cp(src_path, dst_path) # cp a b + +def move(src_path: str, dst_path: str) -> None: + assert os.path.exists(src_path) + return mv(src_path, dst_path) + +def rename(file_path: str, new_file_name: str) -> None: + assert os.path.exists(file_path) + components = list(os.path.split(file_path)) + components[-1] = new_file_name + dst = "/".join(components) + return mv(file_path, dst) + +def delete(path: str) -> None: + assert os.path.exists(path) + return rm("-rf", path) diff --git a/friday/atom_action/operations/media.py b/friday/atom_action/operations/media.py new file mode 100644 index 0000000..4b2e3fc --- /dev/null +++ b/friday/atom_action/operations/media.py @@ -0,0 +1,26 @@ +import sys + +sys.dont_write_bytecode = True +from friday.atom_action.src import * + +def view_document(file_path) -> None: + return evince(file_path) + +def view_txt(file_path) -> None: + return gedit(file_path) + +# add by wzm +def view_office_document(file_path, sys_version) -> None: + if 'mac' in sys_version: + return soffice(file_path) + else: + return libreoffice(file_path) + +def play_audio(file_path) -> None: + return rhythmbox_client(f"--play-uri=\"{file_path}\"") + +def play_video(file_path) -> None: + return totem(file_path) + +def view_office_document(file_path) -> None: + return libreoffice(file_path) diff --git a/friday/atom_action/operations/routine.py b/friday/atom_action/operations/routine.py new file mode 100644 index 0000000..2f50f71 --- /dev/null +++ b/friday/atom_action/operations/routine.py @@ -0,0 +1,6 @@ +import sys + +sys.dont_write_bytecode = True + +def add_plan() -> None: + raise NotImplementedError diff --git a/friday/atom_action/operations/system.py b/friday/atom_action/operations/system.py new file mode 100644 index 0000000..5dd7b75 --- /dev/null +++ b/friday/atom_action/operations/system.py @@ -0,0 +1,23 @@ +import sys + +sys.dont_write_bytecode = True +from ..src import * +from ..query import screen + +def sudo_install(package: str) -> None: + return Pkexec_apt("install", package) + +def pip_install(package: str) -> None: + return pip("install", package) + +def adjust_theme(theme: str) -> None: + return gsettings("set", "org.gnome.desktop.interface", "gtk-theme", theme) + +def adjust_brightness(brightness: int) -> None: + assert brightness >= 0.5 and brightness <= 1 + brightness = str(brightness) + return xrandr("--output", screen(), "--brightness", brightness) + +# add by wzm +def terminal_show_file_content(path: str) -> None: + return terminal('--geometry=130x44',"--", "bash", "-c", "cat {}; read line".format(path)) diff --git a/friday/atom_action/query/__init__.py b/friday/atom_action/query/__init__.py new file mode 100644 index 0000000..16cd6ad --- /dev/null +++ b/friday/atom_action/query/__init__.py @@ -0,0 +1,6 @@ +import sys + +sys.dont_write_bytecode = True +from .device import * +from .files import * +from .package import * diff --git a/friday/atom_action/query/device.py b/friday/atom_action/query/device.py new file mode 100644 index 0000000..61f468a --- /dev/null +++ b/friday/atom_action/query/device.py @@ -0,0 +1,10 @@ +import sys + +sys.dont_write_bytecode = True +from ..src import * + +def screen() -> str: + return Promise() \ + .then(xrandr)() \ + .then(grep)(" connected") \ + .then(cut)("-f1", "-d", " ")() diff --git a/friday/atom_action/query/files.py b/friday/atom_action/query/files.py new file mode 100644 index 0000000..bbd0416 --- /dev/null +++ b/friday/atom_action/query/files.py @@ -0,0 +1,10 @@ +import sys + +sys.dont_write_bytecode = True +from ..src import * + +def dir_list(dir_path: str) -> str: + return ls(dir_path) + +def dir_tree(dir_path: str) -> str: + return tree(dir_path) diff --git a/friday/atom_action/query/package.py b/friday/atom_action/query/package.py new file mode 100644 index 0000000..4929bbd --- /dev/null +++ b/friday/atom_action/query/package.py @@ -0,0 +1,9 @@ +import sys + +sys.dont_write_bytecode = True +from ..src import * + +def is_installed(package: str) -> str: + return Promise() \ + .then(apt)("list") \ + .then(grep)(f"{package}/")() diff --git a/friday/atom_action/src/__init__.py b/friday/atom_action/src/__init__.py new file mode 100644 index 0000000..cc975bc --- /dev/null +++ b/friday/atom_action/src/__init__.py @@ -0,0 +1,5 @@ +import sys + +sys.dont_write_bytecode = True +from .bash import * +from .commands import * diff --git a/friday/atom_action/src/bash.py b/friday/atom_action/src/bash.py new file mode 100644 index 0000000..ab27383 --- /dev/null +++ b/friday/atom_action/src/bash.py @@ -0,0 +1,79 @@ +from __future__ import annotations +import sys + +sys.dont_write_bytecode = True +from typing import Union, Optional +from subprocess import run, PIPE + + +class Output: + def __init__(self, stdout: str, stderr: str): + self.stdout = stdout + self.stderr = stderr + + def observe(self): + if self.stdout == "" and self.stderr != "": + raise RuntimeError(self.stderr) + else: + return self.stdout + + def __str__(self): + print([self.stdout, self.stderr]) + return self.stdout + self.stderr + + +class Bash: + def __init__(self, cmd: str, stdin: Optional[str]=None): + self.cmd = cmd + self.stdin = stdin + + def __call__(self, *arg) -> str: + result = run( + [self.cmd, *arg], + input=self.stdin, + capture_output=True, + text=True + ) + return Output(result.stdout, result.stderr) + + +class Promise(): + def __init__(self, stdin: Optional[str]=None): + self.stdin = stdin + self.stash = None + self.stdout = None + + def then(self, bash: Union[str, Bash], *arg) -> Promise: + if type(bash) == str: + bash = Bash(bash) + self.stash = bash + self.stash.stdin = self.stdout if self.stdout != None else self.stdin + return self.__call__(*arg) if len(arg) > 0 else self + + def observe(self) -> str: + return self.stdout.strip("\n") + + def __call__(self, *arg) -> Promise: + if self.stash != None: + self.stdout = self.stash(*arg).observe() + self.stash = None + return self + else: + return self.observe() + + +Pkexec = \ + lambda cmd: \ + lambda *arg: \ + Bash("pkexec")(cmd, *arg) + +Pkexec_GUI = \ + lambda cmd: \ + lambda *arg: \ + Bash("pkexec")( + "env", + "DISPLAY=$DISPLAY", + "XAUTHORITY=$XAUTHORITY", + cmd, + *arg + ) diff --git a/friday/atom_action/src/commands.py b/friday/atom_action/src/commands.py new file mode 100644 index 0000000..a534a02 --- /dev/null +++ b/friday/atom_action/src/commands.py @@ -0,0 +1,39 @@ +from .bash import * + +# file operation +mkdir = Bash("mkdir") +touch = Bash("touch") +cp = Bash("cp") +mv = Bash("mv") +rm = Bash("rm") +ls = Bash("ls") +tree = Bash("tree") + +# tools +grep = Bash("grep") +cut = Bash("cut") +wget = Bash("wget") +gedit = Bash("gedit") +Pkexec_gedit = Pkexec_GUI("gedit") + + +# system & setting +apt = Bash("apt") +Pkexec_apt = Pkexec("apt") +pip = Bash("pip") +gsettings = Bash("gsettings") +xrandr = Bash("xrandr") +terminal = Bash("gnome-terminal") # add by wzm + +# development +python = Bash("python") +code = Bash("code") + +# application +evince = Bash("evince") +gedit = Bash("gedit") +libreoffice = Bash("libreoffice") # add by wzm +soffice = Bash("/Applications/LibreOffice.app/Contents/MacOS/soffice") # libreoffice for macos +rhythmbox_client = Bash("rhythmbox-client") +totem = Bash("totem") +libreoffice = Bash("libreoffice") diff --git a/friday/core/__init__.py b/friday/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/core/action_manager.py b/friday/core/action_manager.py new file mode 100644 index 0000000..f00fa74 --- /dev/null +++ b/friday/core/action_manager.py @@ -0,0 +1,191 @@ + +from langchain.vectorstores import Chroma +from langchain.embeddings.openai import OpenAIEmbeddings +import json +import os + +class ActionManager: + def __init__(self, config_path=None, action_lib_dir=None): + # actions: Store the mapping relationship between descriptions and code (associated through task names) + self.actions = {} + self.action_lib_dir = action_lib_dir + with open(config_path) as f: + config = json.load(f) + with open(f"{self.action_lib_dir}/actions.json") as f2: + self.actions = json.load(f2) + self.vectordb_path = f"{action_lib_dir}/vectordb" + + if not os.path.exists(self.vectordb_path): + os.makedirs(self.vectordb_path) + # Utilize the Chroma database and employ OpenAI Embeddings for vectorization (defaul: text-embedding-ada-002) + self.vectordb = Chroma( + collection_name="action_vectordb", + embedding_function=OpenAIEmbeddings( + openai_api_key=config['OPENAI_API_KEY'], + openai_organization=config['OPENAI_ORGANIZATION'], + ), + persist_directory=self.vectordb_path, + ) + assert self.vectordb._collection.count() == len(self.actions), ( + f"Action Manager's vectordb is not synced with actions.json.\n" + f"There are {self.vectordb._collection.count()} actions in vectordb but {len(self.actions)} actions in actions.json.\n" + ) + + # View all the code in the code repository + @property + def programs(self): + programs = "" + for _, entry in self.actions.items(): + programs += f"{entry['code']}\n\n" + return programs + + # Retrieve the descriptions of all actions + @property + def descriptions(self): + descriptions = {} + for action_name, entry in self.actions.items(): + descriptions.update({action_name: entry["description"]}) + return descriptions + + # Retrieve all action class names + @property + def action_names(self): + return self.actions.keys() + + # View the code of a specific action + def get_action_code(self, action_name): + code = self.actions[action_name]['code'] + return code + + # Add new task code + def add_new_action(self, info): + program_name = info["task_name"] + program_code = info["code"] + program_description = info["description"] + print( + f"\033[33m {program_name}:\n{program_description}\033[0m" + ) + # If this task code already exists in the action library, delete it and rewrite + if program_name in self.actions: + print(f"\033[33mAction {program_name} already exists. Rewriting!\033[0m") + self.vectordb._collection.delete(ids=[program_name]) + # Store the new task code in the vector database and the action dictionary + self.vectordb.add_texts( + texts=[program_description], + ids=[program_name], + metadatas=[{"name": program_name}], + ) + self.actions[program_name] = { + "code": program_code, + "description": program_description, + } + assert self.vectordb._collection.count() == len( + self.actions + ), "vectordb is not synced with actions.json" + # Store the new task code and description in the action library, and enter the mapping relationship into the dictionary + with open(f"{self.action_lib_dir}/code/{program_name}.py", "w") as fa: + fa.write(program_code) + with open(f"{self.action_lib_dir}/action_description/{program_name}.txt", "w") as fb: + fb.write(program_description) + with open(f"{self.action_lib_dir}/actions.json", "w") as fc: + json.dump(self.actions,fc,indent=4) + self.vectordb.persist() + + # Check if there are relevant tools + def exist_action(self, action): + if action in self.action_names: + return True + return False + + # Retrieve related task names + def retrieve_action_name(self, query, k=10): + k = min(self.vectordb._collection.count(), k) + if k == 0: + return [] + print(f"\033[33mAction Manager retrieving for {k} Actions\033[0m") + # Retrieve descriptions of the top k related tasks. + docs_and_scores = self.vectordb.similarity_search_with_score(query, k=k) + print( + f"\033[33mAction Manager retrieved actions: " + f"{', '.join([doc.metadata['name'] for doc, _ in docs_and_scores])}\033[0m" + ) + action_name = [] + for doc, _ in docs_and_scores: + action_name.append(doc.metadata["name"]) + return action_name + + # Return the task description based on the task name + def retrieve_action_description(self, action_name): + action_description = [] + for name in action_name: + action_description.append(self.actions[name]["description"]) + return action_description + + # Return the task code based on the task name + def retrieve_action_code(self, action_name): + action_code = [] + for name in action_name: + action_code.append(self.actions[name]["code"]) + return action_code + + # Delete task-related information + def delete_action(self, action): + # Delete the task from the vector database + if action in self.actions: + self.vectordb._collection.delete(ids=[action]) + print( + f"\033[33m delete {action} from vectordb successfully! \033[0m" + ) + # Delete the task from actions.json + with open(f"{self.action_lib_dir}/actions.json", "r") as file: + action_infos = json.load(file) + if action in action_infos: + del action_infos[action] + with open(f"{self.action_lib_dir}/actions.json", "w") as file: + json.dump(action_infos, file, indent=4) + print( + f"\033[33m delete {action} info from JSON successfully! \033[0m" + ) + # del code + code_path = f"{self.action_lib_dir}/code/{action}.py" + if os.path.exists(code_path): + os.remove(code_path) + print( + f"\033[33m delete {action} code successfully! \033[0m" + ) + # del description + description_path = f"{self.action_lib_dir}/action_description/{action}.txt" + if os.path.exists(description_path): + os.remove(description_path) + print( + f"\033[33m delete {action} description txt successfully! \033[0m" + ) + # del args description + args_path = f"{self.action_lib_dir}/args_description/{action}.txt" + if os.path.exists(args_path): + os.remove(args_path) + print( + f"\033[33m delete {action} args description txt successfully! \033[0m" + ) + + +if __name__ == '__main__': + actionManager = ActionManager(config_path="config.json", action_lib_dir="friday/action_lib") + + # Retrieval + # res = actionManager.retrieve_action_name("Open the specified text file in the specified folder using the default text viewer on Ubuntu.") + # print(res[0]) + + # Delete + # actionManager.delete_action("zip_files") + + # Add + # code = '' + # with open("working_dir/code/temp.py", 'r') as file: + # code = file.read() + # info = { + # "task_name" : "XXX", + # "code" : code, + # "description" : "XXX" + # } + # actionManager.add_new_action(info) \ No newline at end of file diff --git a/friday/core/action_node.py b/friday/core/action_node.py new file mode 100644 index 0000000..ff98702 --- /dev/null +++ b/friday/core/action_node.py @@ -0,0 +1,45 @@ +class ActionNode: + def __init__(self, name, description, type): + self._name = name + self._description = description + self._return_val = '' + self._relevant_code = {} + self._next_action = {} + self._status = False + self._type = type + + @property + def name(self): + return self._name + + @property + def description(self): + return self._description + + @property + def return_val(self): + return self._return_val + + @property + def relevant_action(self): + return self._relevant_code + + @property + def status(self): + return self._status + + @property + def type(self): + return self._type + + @property + def next_action(self): + return self._next_action + + def __str__(self): + return f"name: {self.name} \n description: {self.description} \n return: {self.return_val} \n relevant_action: {self._relevant_code} \n next_action: {self.next_action} \n status: {self.status} \n type: {self.type}" + + +if __name__ == '__main__': + node = ActionNode('temp','xxx') + print(node.name) \ No newline at end of file diff --git a/friday/core/api_server.py b/friday/core/api_server.py new file mode 100644 index 0000000..6872d85 --- /dev/null +++ b/friday/core/api_server.py @@ -0,0 +1,74 @@ +import os + +from fastapi import FastAPI +from friday.core.server_config import ConfigManager + +app = FastAPI() + +# Import your services +from friday.api.python.interpreter import router as python_router +from friday.api.arxiv.arxiv import router as arxiv_router +from friday.api.bing.bing_service import router as bing_router +from friday.api.calculator.calculator import router as calculator_router +from friday.api.chemical.chemical import router as chemical_router +from friday.api.ppt.ppt import router as ppt_router +from friday.api.shell.shell import router as shell_router +from friday.api.database.database import router as db_router +from friday.api.wolfram_alpha.wolfram_alpha import router as wa_router +from friday.api.weather.weather import router as weather_router +from friday.api.google_calendar.calendar_service import router as calendar_router +from friday.api.gmail.gmail import router as gmail_router +from friday.api.markdown.markdown_service import router as markdown_router + +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request + + +class LoggingMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + print(f"Incoming request: {request.method} {request.url}") + try: + response = await call_next(request) + except Exception as e: + print(f"Request error: {str(e)}") + raise e from None + else: + print(f"Outgoing response: {response.status_code}") + return response + + +app.add_middleware(LoggingMiddleware) + +# Create a dictionary that maps service names to their routers +services = { + "python_executor": python_router, + "calculator": calculator_router, + "arxiv": arxiv_router, + "bing": bing_router, + "chemical": chemical_router, + "ppt": ppt_router, + "shell": shell_router, + "database": db_router, + "wolframalpha": wa_router, + "weather": weather_router, + "calendar": calendar_router, + "gmail": gmail_router, + "markdown": markdown_router + +} + +server_list = ["python_executor", "calculator","arxiv","bing","shell","ppt", + "database","wolframalpha","weather","calendar","gmail","markdown"] + +# Include only the routers for the services listed in server_list +for service in server_list: + if service in services: + app.include_router(services[service]) + +# proxy_manager = ConfigManager() +# proxy_manager.apply_proxies() + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8079) diff --git a/friday/core/bing_search.py b/friday/core/bing_search.py new file mode 100644 index 0000000..4c523cd --- /dev/null +++ b/friday/core/bing_search.py @@ -0,0 +1,25 @@ +from friday.core.tool_request_util import ToolRequestUtil + +# Initialize the ToolRequestUtil +tool_request_util = ToolRequestUtil() + +# Define the API path +api_path = "/tools/bing/load_pagev2" + +# Define the method to be used for the API call +method = "get" + +# Define the parameters for the API call +params = { + "query": "Vietnamese specimens described by Kuznetzov in Nedoshivina's 2010 paper deposition", + "url": "https://www.zobodat.at/pdf/Atalanta_41_0335-0347.pdf" # We only need the top result since we are looking for a specific piece of information +} + +# Define the content type +content_type = "application/json" + +# Make the API call and store the response +response = tool_request_util.request(api_path, method, params=params, content_type=content_type) + +# Print the return value of the API call +print(response) \ No newline at end of file diff --git a/friday/core/llms.py b/friday/core/llms.py new file mode 100644 index 0000000..0ccd0fd --- /dev/null +++ b/friday/core/llms.py @@ -0,0 +1,31 @@ +import openai +import json +import logging + +class OpenAI: + """ + OPEN AI Chat Models + """ + def __init__(self, config_path=None): + with open(config_path) as f: + config = json.load(f) + self.model_name = config['model_name'] + openai.api_key = config['OPENAI_API_KEY'] + openai.organization = config['OPENAI_ORGANIZATION'] + print(openai.api_key) + print(openai.organization) + # openai.proxy = proxy + + def chat(self, messages, temperature=0, sleep_time=2): + response = openai.chat.completions.create( + model=self.model_name, + messages=messages, + temperature=temperature + ) + logging.info(response.choices[0].message.content) + + # time.sleep(sleep_time) + # return response['choices'][0]['message'] + return response.choices[0].message.content + + diff --git a/friday/core/openapi.json b/friday/core/openapi.json new file mode 100644 index 0000000..b2a7542 --- /dev/null +++ b/friday/core/openapi.json @@ -0,0 +1,723 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/tools/markdown/web2md": { + "get": { + "summary": "This API can only get the markdown formatting of a web page at a given url but can not do summary work.", + "operationId": "get_web_md_tools_markdown_web2md_get", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TargetPageModel" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/tools/bing/searchv2": { + "get": { + "summary": "Execute Bing Search - returns top web snippets related to the query. Avoid using complex filters like 'site:'. For detailed page content, further use the web browser tool.", + "operationId": "bing_search_v2_tools_bing_searchv2_get", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryItemV2" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/tools/bing/load_pagev2": { + "get": { + "summary": "Web browser tool for detailed content retrieval and specific information extraction from a target URL.In the case of Wikipedia, the number of tokens on such pages is often too large to load the entire page, so the 'query' parameter must be given to perform a similarity query to find the most relevant pieces of content. The 'query' parameter should be assigned with your task description to find the most relevant content of the web page.It is important that your 'query' must retain enough details about the task, such as time, location, quantity, and other information, to ensure that the results obtained are accurate enough.", + "operationId": "load_page_v2_tools_bing_load_pagev2_get", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageItemV2" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/weather/query": { + "get": { + "summary": "Query Weather", + "operationId": "query_weather_weather_query_get", + "parameters": [ + { + "name": "date", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Date" + } + }, + { + "name": "city", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "City" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/gmail/send": { + "post": { + "summary": "Send google Email", + "operationId": "send_test_email_gmail_send_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailSchema" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/gmail/list": { + "get": { + "summary": "List Recent Emails from gmail", + "operationId": "list_recent_emails_gmail_list_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/tools/audio2text": { + "post": { + "summary": "A tool that converts audio to natural language text", + "operationId": "audio2text_tools_audio2text_post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_audio2text_tools_audio2text_post" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/tools/image_caption": { + "post": { + "summary": "When the task is to question and answer based on local picture, you have to use the Image Caption tool, who can directly analyze picture to answer question and complete task. For local images you want to understand, you need to only give the image_file without url. It is crucial to provide the 'query' parameter, and its value must be the full content of the task itself.", + "operationId": "image_search_tools_image_caption_post", + "parameters": [ + { + "name": "query", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "What's in this image?", + "title": "Query" + } + }, + { + "name": "url", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Body_image_search_tools_image_caption_post" + } + ], + "title": "Body" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "AddFirstPageModel": { + "properties": { + "title": { + "type": "string", + "title": "Title" + }, + "subtitle": { + "type": "string", + "title": "Subtitle" + } + }, + "type": "object", + "required": [ + "title", + "subtitle" + ], + "title": "AddFirstPageModel" + }, + "AddTextImagePageModel": { + "properties": { + "title": { + "type": "string", + "title": "Title" + }, + "bullet_items": { + "type": "string", + "title": "Bullet Items" + }, + "image": { + "type": "string", + "title": "Image" + } + }, + "type": "object", + "required": [ + "title", + "bullet_items", + "image" + ], + "title": "AddTextImagePageModel" + }, + "AddTextPageModel": { + "properties": { + "title": { + "type": "string", + "title": "Title" + }, + "bullet_items": { + "type": "string", + "title": "Bullet Items" + } + }, + "type": "object", + "required": [ + "title", + "bullet_items" + ], + "title": "AddTextPageModel" + }, + "CalendarEvent": { + "properties": { + "summary": { + "type": "string", + "title": "Summary" + }, + "location": { + "type": "string", + "title": "Location" + }, + "description": { + "type": "string", + "title": "Description" + }, + "start": { + "type": "object", + "title": "Start", + "example": { + "dateTime": "2023-07-31T15:00:00", + "timeZone": "Asia/Shanghai" + } + }, + "end": { + "type": "object", + "title": "End", + "example": { + "dateTime": "2023-07-31T16:00:00", + "timeZone": "Asia/Shanghai" + } + } + }, + "type": "object", + "required": [ + "summary", + "location", + "description", + "start", + "end" + ], + "title": "CalendarEvent" + }, + "CreateFileModel": { + "properties": { + "theme": { + "type": "string", + "title": "Theme" + } + }, + "type": "object", + "required": [ + "theme" + ], + "title": "CreateFileModel" + }, + "EmailSchema": { + "properties": { + "from_email": { + "type": "string", + "title": "From Email" + }, + "to_email": { + "type": "string", + "title": "To Email" + }, + "subject": { + "type": "string", + "title": "Subject" + }, + "content": { + "type": "string", + "title": "Content" + } + }, + "type": "object", + "required": [ + "from_email", + "to_email", + "subject", + "content" + ], + "title": "EmailSchema" + }, + "Expression": { + "properties": { + "expression": { + "type": "string", + "title": "Expression" + } + }, + "type": "object", + "required": [ + "expression" + ], + "title": "Expression" + }, + "GetImageModel": { + "properties": { + "keywords": { + "type": "string", + "title": "Keywords" + } + }, + "type": "object", + "required": [ + "keywords" + ], + "title": "GetImageModel" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "Item": { + "properties": { + "code": { + "type": "string", + "title": "Code" + } + }, + "type": "object", + "required": [ + "code" + ], + "title": "Item" + }, + "PageItemV2": { + "properties": { + "url": { + "type": "string", + "title": "Url" + }, + "query": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Query" + } + }, + "type": "object", + "required": [ + "url" + ], + "title": "PageItemV2" + }, + "QueryItem": { + "properties": { + "query": { + "type": "string", + "title": "Query" + } + }, + "type": "object", + "required": [ + "query" + ], + "title": "QueryItem" + }, + "Body_image_search_tools_image_caption_post": { + "properties": { + "image_file": { + "anyOf": [ + { + "type": "string", + "format": "binary" + }, + { + "type": "null" + } + ], + "title": "Image File" + } + }, + "type": "object", + "title": "Body_image_search_tools_image_caption_post" + }, + "Body_audio2text_tools_audio2text_post": { + "properties": { + "file": { + "type": "string", + "format": "binary", + "title": "File" + } + }, + "type": "object", + "required": [ + "file" + ], + "title": "Body_audio2text_tools_audio2text_post" + }, + "QueryItemV2": { + "properties": { + "query": { + "type": "string", + "title": "Query" + }, + "top_k": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Top K" + } + }, + "type": "object", + "required": [ + "query" + ], + "title": "QueryItemV2" + }, + "TargetPageModel": { + "properties": { + "url": { + "type": "string", + "title": "Url" + } + }, + "type": "object", + "required": [ + "url" + ], + "title": "TargetPageModel" + }, + "SQLRequest": { + "properties": { + "queries": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Queries" + } + }, + "type": "object", + "required": [ + "queries" + ], + "title": "SQLRequest" + }, + "ShellCommandModel": { + "properties": { + "command": { + "type": "string", + "title": "Command" + } + }, + "type": "object", + "required": [ + "command" + ], + "title": "ShellCommandModel" + }, + "ShellCommandResultModel": { + "properties": { + "stdout": { + "type": "string", + "title": "Stdout" + }, + "stderr": { + "type": "string", + "title": "Stderr" + } + }, + "type": "object", + "required": [ + "stdout", + "stderr" + ], + "title": "ShellCommandResultModel" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + }, + "ArxivQuery": { + "properties": { + "query": { + "type": "string", + "title": "Query" + } + }, + "type": "object", + "required": [ + "query" + ], + "title": "ArxivQuery" + } + + } + } +} \ No newline at end of file diff --git a/friday/core/request_api.py b/friday/core/request_api.py new file mode 100644 index 0000000..102a092 --- /dev/null +++ b/friday/core/request_api.py @@ -0,0 +1,38 @@ +from friday.core.tool_request_util import ToolRequestUtil + +# Initialize the ToolRequestUtil +tool_request_util = ToolRequestUtil() + +# Define the API path +api_path = '/tools/bing/load_pagev2' + +# Define the method to be used for the API call +method = 'get' + +# Extract the search results from the 'internet_search' subtask context +search_results = [ + { + "snippet": "Abstract: 67 species described by V. Kuznetzov from Vietnam are listed with short comments on the type series including descrip-tions of their labels. Colour images of the holotypes are given (col. pl. 7-9). Descriptions of †† of five species are provided and their genitalia are figured.", + "title": "A catalogue of type specimens of the Tortricidae described by V. I. K ...", + "link": "https://www.zobodat.at/pdf/Atalanta_41_0335-0347.pdf" + }, + # ... other search results +] + +# Define the query parameter based on the task description +query = "Vietnamese specimens described by Kuznetzov in Nedoshivina's 2010 paper deposition" + +# Since the API requires a URL, we will use the first link from the search results +url = search_results[0]['link'] +print(url) +# Prepare the params for the API call +params = { + 'url': url, + 'query': query +} + +# Make the API call using the ToolRequestUtil +response = tool_request_util.request(api_path, method, params=params) + +# Print the return value of the API +print(response) \ No newline at end of file diff --git a/friday/core/retriver.py b/friday/core/retriver.py new file mode 100644 index 0000000..1ba9c58 --- /dev/null +++ b/friday/core/retriver.py @@ -0,0 +1,47 @@ +from openai import OpenAI +from utils import cosine_similarity +import json + +""" + Use to generate embedding of the text +""" +class OpenAIEmbeddingAndRetriver: + """ + OPEN AI Chat Models + """ + def __init__(self, config_path=None): + with open(config_path) as f: + config = json.load(f) + self.model_name = "text-embedding-ada-002" + self.client = OpenAI(api_key=config['OPENAI_API_KEY'], organization=config['OPENAI_ORGANIZATION']) + + # get the embedding of a text + def get_text_embedding(self,text): + texts_embeddings = self.client.embeddings.create( + model= self.model_name, + input=[text]) + return texts_embeddings.data[0].embedding + # get the embeddings of each text in the list + def get_texts_embedding(self,text_list): + texts_embeddings = self.client.embeddings.create( + model= self.model_name, + input=text_list) + return [obj.embedding for obj in texts_embeddings.data] + + # retrive the most similar text with query in the text_list + def retrive(self,query,text_list,top_k=1): + texts_embeddings = self.get_texts_embedding(text_list) + query_embedding = self.get_text_embedding(query) + similarity = [] + for emb in texts_embeddings: + similarity.append(cosine_similarity(emb ,query_embedding)) + sorted_pairs=sorted(zip(similarity, text_list), reverse=True) + retrieved_texts = [pair[1] for pair in sorted_pairs] + return retrieved_texts[:top_k] + +# embed = OpenAIEmbeddingAndRetriver("../../examples/config.json") +# res = embed.retrive("set a 10s timer",["resume screen","set_timer","print_words","get_timer"]) +# print(res) + + + diff --git a/friday/core/retriver_qa.py b/friday/core/retriver_qa.py new file mode 100644 index 0000000..46e21ae --- /dev/null +++ b/friday/core/retriver_qa.py @@ -0,0 +1,23 @@ +from langchain.embeddings.openai import OpenAIEmbeddings +from langchain.vectorstores import Chroma +from langchain.text_splitter import RecursiveCharacterTextSplitter +from langchain.llms import OpenAI +from langchain.chains import RetrievalQA +from langchain.document_loaders import TextLoader +import json +with open("config.json") as f: + config = json.load(f) +with open('./test.txt') as f: + state_of_the_union = f.read() +text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=0) +texts = text_splitter.create_documents([state_of_the_union]) + +embeddings = OpenAIEmbeddings( + openai_api_key=config['OPENAI_API_KEY'], + openai_organization=config['OPENAI_ORGANIZATION'], +) +docsearch = Chroma.from_documents(texts, embeddings) +query = "Please summarize what this webpage https://zhuanlan.zhihu.com/p/541484549 is mainly about." +docs = docsearch.similarity_search(query, k=3) +res = '...'.join([doc.page_content for doc in docs]) +print(res) \ No newline at end of file diff --git a/friday/core/schema.py b/friday/core/schema.py new file mode 100644 index 0000000..44917cd --- /dev/null +++ b/friday/core/schema.py @@ -0,0 +1,43 @@ +from dataclasses import dataclass, field +from enum import Enum +from typing import Dict, List, Optional, Union + + +class ActionStatusCode(int, Enum): + ING = 0 + SUCCESS = 1 + FAILED = -1 + + +class ActionValidCode(int, Enum): + FINISH = 1 + OPEN = 0 + CLOSED = -1 + INVALID = -2 + ABSENT = -3 # NO ACTION + + +@dataclass +class ActionReturn: + args: Dict + url: Optional[str] = None + type: Optional[str] = None + result: Optional[str] = None + errmsg: Optional[str] = None + state: Union[ActionStatusCode, int] = ActionStatusCode.SUCCESS + thought: Optional[str] = None + valid: Optional[ActionValidCode] = ActionValidCode.OPEN + +@dataclass +class EnvState: + command: List[str] = field(default_factory=list) + result: Optional[str] = None + error: Optional[str] = None + pwd: Optional[str] = None + ls: Optional[str] = None + + def __str__(self): + return (f"Result: {self.result}\n" + f"Error: {self.error}\n" + f"PWD: {self.pwd}\n" + f"LS: {self.ls}") \ No newline at end of file diff --git a/friday/core/server_config.py b/friday/core/server_config.py new file mode 100644 index 0000000..10479ca --- /dev/null +++ b/friday/core/server_config.py @@ -0,0 +1,28 @@ +import os + +class ConfigManager: + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super(ConfigManager, cls).__new__(cls) + cls._instance.http_proxy = "http://127.0.0.1:10809" + cls._instance.https_proxy = "http://127.0.0.1:10809" + # cls._instance.http_proxy = None + # cls._instance.https_proxy = None + return cls._instance + + def set_proxies(self, http, https): + self.http_proxy = http + self.https_proxy = https + + def apply_proxies(self): + if self.http_proxy: + os.environ["http_proxy"] = self.http_proxy + if self.https_proxy: + os.environ["https_proxy"] = self.https_proxy + + def clear_proxies(self): + os.environ.pop("http_proxy", None) + os.environ.pop("https_proxy", None) + diff --git a/friday/core/tool_planner.py b/friday/core/tool_planner.py new file mode 100644 index 0000000..3ddeef3 --- /dev/null +++ b/friday/core/tool_planner.py @@ -0,0 +1,88 @@ +import requests +import json +import os +from friday.action.get_os_version import get_os_version, check_os_version +from friday.core.llms import OpenAI + +sys_prompt = ''' +You are a helpful assistant that can answer the user's questions with the help of tools.You are provided with the following tools: +Tools: +1. {{ + "tool_name": "bing_search", + "description": "You can use this tool to search top 10 relevant web pages' link and very short description for the given query keywords.You sometimes need to use web browser to get detailed information of some page" + "path": "/api/tools/bing/search", + "method": "get", + "params": [ + {{ + "name": "query", + "in":"query", + "type": "string", + "description": "the query keywords for bing search" + }}, + + ], + +}} +2. {{ + "tool_name": "web_browser", + "description": "You can use this tool to browser the detail content of the web page given its url" + "path": "/api/tools/bing/search", + "method": "get", + "params": [ + {{ + "name": "query", + "in":"query", + "type": "string", + "description": "the query keywords for bing search" + }}, + + ], + +}} +You need to decide whether which tools to use to solve the question asked by user. +Remember if you can make sure the answer you give by your own knowledge is completely accurate, don't use any tools. Only use the tools provided to you if you lack some external knowledge to give a factual answer. You can even use multiple tools or use the same tool multiple times if necessary. +You should give me a plan list to tell me how to use the tools to solve the question,the response format should just like: +eg. To solve the question "How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)?" +1. /api/tools/bing/search?query=Mercedes Sosa +2. /api/tools/bing/load_page?url=https://en.wikipedia.org/wiki/Mercedes_Sosa +If you think there is no need to use tools, you can just respond: +There is no need to use tools. +Now,you can start to solve the question,give me your plans: +{question} +''' + +os.environ["BING_SUBSCRIPTION_KEY"] = "885e62a126554fb390af88ae31d2c8ff" +os.environ["BING_SEARCH_URL"] = "https://api.bing.microsoft.com/v7.0/search" +# search = BingSearchAPIWrapper() + +# res = search.results("https://zhuanlan.zhihu.com/p/623421034 summary",10) +class ToolPlanner(): + """ + SkillCreator is used to generate new skills and store them in the action_lib. + """ + def __init__(self, config_path=None) -> None: + super().__init__() + self.llm = OpenAI(config_path) + self.system_version = get_os_version() + try: + check_os_version(self.system_version) + except ValueError as e: + print(e) + # self.mac_systom_prompts = + + def format_message(self, task): + self.prompt = sys_prompt.format(question=task) + # self.prompt = "" + print(self.prompt) + self.message = [ + {"role": "system", "content": self.prompt}, + {"role": "user", "content": task}, + ] + return self.llm.chat(self.message) +q = ''' +What writer is quoted by Merriam-Webster for the Word of the Day from June 27, 2022? +''' +res = ToolPlanner("../../examples/config.json").format_message(q) +print(res) + + \ No newline at end of file diff --git a/friday/core/tool_request_util.py b/friday/core/tool_request_util.py new file mode 100644 index 0000000..ec573de --- /dev/null +++ b/friday/core/tool_request_util.py @@ -0,0 +1,38 @@ +import requests +class ToolRequestUtil: + def __init__(self): + self.session = requests.session() + self.headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML like Gecko) Chrome/52.0.2743.116 Safari/537.36'} + self.base_url = "http://43.159.144.130:8079" + + def request(self, api_path, method, params=None, files=None, content_type="application/json"): + """ + :param api_path: the path of the api + :param method: get/post + :param params: the params of the api, can be None + :param files: files to be uploaded, can be None + :param content_type: the content_type of api, e.g., application/json, multipart/form-data, can be None + :return: the return of the api + """ + url = self.base_url + api_path + try: + # 判断请求方法 + if method.lower() == "get": + if content_type == "application/json": + result = self.session.get(url=url, json=params, headers=self.headers, timeout=60).json() + else: + result = self.session.get(url=url, params=params, headers=self.headers, timeout=60).json() + elif method.lower() == "post": + if content_type == "multipart/form-data": + result = self.session.post(url=url, files=files, data=params, headers=self.headers).json() + elif content_type == "application/json": + result = self.session.post(url=url, json=params, headers=self.headers).json() + else: + result = self.session.post(url=url, data=params, headers=self.headers).json() + else: + print("request method error!") + return None + return result + except Exception as e: + print("http request error: %s" % e) + return None \ No newline at end of file diff --git a/friday/core/tools_args_schema.py b/friday/core/tools_args_schema.py new file mode 100644 index 0000000..15fff3f --- /dev/null +++ b/friday/core/tools_args_schema.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel,Field +class WebBrowserInput(BaseModel): + url: str = Field(..., description="The url to load") + max_retry: int = Field(3, description="The max retry times to load the url") +class BingSearchInput(BaseModel): + query: str = Field(..., description="The query keywords for bing search") + top_k: int = Field(5, description="The number of top k results to return") +class TestInput(BaseModel): + a: int = Field(..., description="The first number") + b: int = Field(..., description="The second number") \ No newline at end of file diff --git a/friday/core/tools_dict.py b/friday/core/tools_dict.py new file mode 100644 index 0000000..94e6058 --- /dev/null +++ b/friday/core/tools_dict.py @@ -0,0 +1,55 @@ +from langchain.tools import Tool +from friday.core.web_browser import web_browser +from langchain.utilities import BingSearchAPIWrapper +from friday.core.tools_args_schema import * +import os +os.environ["BING_SUBSCRIPTION_KEY"] = "885e62a126554fb390af88ae31d2c8ff" +os.environ["BING_SEARCH_URL"] = "https://api.bing.microsoft.com/v7.0/search" + +def bing_search_func(query,top_k=5): + search = BingSearchAPIWrapper() + return search.results(query,top_k) + +web_browser_tool = Tool( + name="web_browser", + description="You can use this tool to browser the detail content of the web page given its url", + func=web_browser, + args_schema = WebBrowserInput + +) + +# class BingSearchTool(BaseTool): +# name = "bing_search" +# description:str ="You can use this tool to search top 10 relevant web pages' link and very short description for the given query keywords.You sometimes need to use web browser to get detailed information of some page", + +# def _run(self, query: str, top_k: int=5) -> str: +# """使用工具。""" +# search = BingSearchAPIWrapper() +# return search.results(query,top_k) + +# async def _arun(self, query: str) -> str: +# """异步使用工具。""" +# raise NotImplementedError("BingSearchTool不支持异步") +# bing_search_tool = BingSearchTool() +bing_search_tool = Tool( + name="bing_search", + description="You can use this tool to search top 10 relevant web pages' link and very short description for the given query keywords.You sometimes need to use web browser to get detailed information of some page", + func=bing_search_func, + args_schema = BingSearchInput +) + +test_tool = Tool( + name="test", + description="You can use this tool to test the tool agent", + func= lambda a,b : "", + # args_schema = TestInput + +) + +res = test_tool(1,2) +# print(res) + +tools_dict = { + "bing_search": bing_search_tool, + "web_browser": web_browser_tool +} diff --git a/friday/core/utils.py b/friday/core/utils.py new file mode 100644 index 0000000..b7d7dfe --- /dev/null +++ b/friday/core/utils.py @@ -0,0 +1,305 @@ +import copy +import numpy as np +import itertools +import json +import logging +import os +import re +import string +from typing import Any +import tqdm +import re +import tiktoken + +def num_tokens_from_string(string: str) -> int: + """Returns the number of tokens in a text string.""" + encoding = tiktoken.encoding_for_model('gpt-4-1106-preview') + num_tokens = len(encoding.encode(string)) + return num_tokens + + + +def parse_content(content, type="html.parser"): + implemented = ["html.parser", "lxml", "lxml-xml", "xml", "html5lib"] + if type not in implemented: + raise ValueError(f"Parser type {type} not implemented. Please choose one of {implemented}") + + from bs4 import BeautifulSoup + + soup = BeautifulSoup(content, type) + original_size = len(str(soup.get_text())) + + tags_to_exclude = [ + "nav", + "aside", + "form", + "header", + "noscript", + "svg", + "canvas", + "footer", + "script", + "style", + ] + for tag in soup(tags_to_exclude): + tag.decompose() + + ids_to_exclude = ["sidebar", "main-navigation", "menu-main-menu"] + for id in ids_to_exclude: + tags = soup.find_all(id=id) + for tag in tags: + tag.decompose() + + classes_to_exclude = [ + "elementor-location-header", + "navbar-header", + "nav", + "header-sidebar-wrapper", + "blog-sidebar-wrapper", + "related-posts", + ] + for class_name in classes_to_exclude: + tags = soup.find_all(class_=class_name) + for tag in tags: + tag.decompose() + + content = soup.get_text() + content = clean_string(content) + + cleaned_size = len(content) + if original_size != 0: + logging.info( + f"Cleaned page size: {cleaned_size} characters, down from {original_size} (shrunk: {original_size-cleaned_size} chars, {round((1-(cleaned_size/original_size)) * 100, 2)}%)" # noqa:E501 + ) + + return content + + +def clean_string(text): + """ + This function takes in a string and performs a series of text cleaning operations. + + Args: + text (str): The text to be cleaned. This is expected to be a string. + + Returns: + cleaned_text (str): The cleaned text after all the cleaning operations + have been performed. + """ + # Replacement of newline characters: + text = text.replace("\n", " ") + + # Stripping and reducing multiple spaces to single: + cleaned_text = re.sub(r"\s+", " ", text.strip()) + + # Removing backslashes: + cleaned_text = cleaned_text.replace("\\", "") + + # Replacing hash characters: + cleaned_text = cleaned_text.replace("#", " ") + + # Eliminating consecutive non-alphanumeric characters: + # This regex identifies consecutive non-alphanumeric characters (i.e., not + # a word character [a-zA-Z0-9_] and not a whitespace) in the string + # and replaces each group of such characters with a single occurrence of + # that character. + # For example, "!!! hello !!!" would become "! hello !". + cleaned_text = re.sub(r"([^\w\s])\1*", r"\1", cleaned_text) + + return cleaned_text + + +def is_readable(s): + """ + Heuristic to determine if a string is "readable" (mostly contains printable characters and forms meaningful words) + + :param s: string + :return: True if the string is more than 95% printable. + """ + try: + printable_ratio = sum(c in string.printable for c in s) / len(s) + except ZeroDivisionError: + logging.warning("Empty string processed as unreadable") + printable_ratio = 0 + return printable_ratio > 0.95 # 95% of characters are printable + + + + + +def format_source(source: str, limit: int = 20) -> str: + """ + Format a string to only take the first x and last x letters. + This makes it easier to display a URL, keeping familiarity while ensuring a consistent length. + If the string is too short, it is not sliced. + """ + if len(source) > 2 * limit: + return source[:limit] + "..." + source[-limit:] + return source + + + + +# check if the source is valid json string +def is_valid_json_string(source: str): + try: + _ = json.loads(source) + return True + except json.JSONDecodeError: + logging.error( + "Insert valid string format of JSON. \ + Check the docs to see the supported formats - `https://docs.embedchain.ai/data-sources/json`" + ) + return False + + + + +def chunks(iterable, batch_size=100, desc="Processing chunks"): + """A helper function to break an iterable into chunks of size batch_size.""" + it = iter(iterable) + total_size = len(iterable) + + with tqdm(total=total_size, desc=desc, unit="batch") as pbar: + chunk = tuple(itertools.islice(it, batch_size)) + while chunk: + yield chunk + pbar.update(len(chunk)) + chunk = tuple(itertools.islice(it, batch_size)) + +def generate_prompt(template: str, replace_dict: dict): + prompt = copy.deepcopy(template) + for k, v in replace_dict.items(): + prompt = prompt.replace(k, str(v)) + return prompt + +def cosine_similarity(a, b): + return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))\ + + +def get_open_api_description_pair(): + script_dir = os.path.dirname(os.path.abspath(__file__)) + open_api_path = os.path.join(script_dir, 'openapi.json') + with open(open_api_path, 'r') as file: + open_api_json = json.load(file) + open_api_dict = open_api_json['paths'] + open_api_description_pair = {} + for name, value in open_api_dict.items(): + if 'post' in value: + open_api_description_pair[name] = value['post']['summary'] + else: + open_api_description_pair[name] = value['get']['summary'] + return open_api_description_pair + +def get_open_api_doc_path(): + script_dir = os.path.dirname(os.path.abspath(__file__)) + open_api_path = os.path.join(script_dir, 'openapi.json') + return open_api_path + + +# prompt = ''' +# 'Sheet1': Zone 1 Unnamed: 1 Unnamed: 2 Unnamed: 3 \ +# 0 Name Type Revenue Rent +# 1 Rainforest Bistro Restaurant 32771 1920 +# 2 Panorama Outfitters Apparel 23170 1788 +# 3 Zack's Cameras and Trail Mix Electronics / Food 33117 1001 +# 4 SignPro Custom DeSign Signage 21246 1121 +# 5 Zone 2 NaN NaN NaN +# 6 Serenity Indoor Fountains Decor 25234 6359 +# 7 Budapest Comics Comics 12251 2461 +# 8 Dottie's Lattes Restaurant 34427 1293 +# 9 Zone 3 NaN NaN NaN +# 10 Gumball Utopia Candy 13271 3420 +# 11 Your Uncle's Basement Sports Collectibles 11119 8201 +# 12 Carnivore Loan Specialists Finance 31000 50312 +# 13 Harry's Steakhouse Restaurant 46791 1327 +# 14 Two Guys Paper Supplies Office Supplies 76201 1120 +# 15 Dragon Pizza Restaurant 10201 2000 +# 16 Zone 4 NaN NaN NaN +# 17 Us Three: The U2 Fan Store Music 10201 1200 +# 18 Jimmy's Buffett Restaurant 10027 3201 +# 19 Franz Equipment Rentals Industrial Supplies 20201 2201 +# 20 Nigel's Board Games Board Games 62012 2013 +# 21 Destructor's Den Baby Supplies 79915 5203 +# 22 Hook Me Up Sporting Goods 56503 1940 +# 23 Zone 5 (Food Court) NaN NaN NaN +# 24 Slam Dunk Restaurant 61239 5820 +# 25 Ben's Hungarian-Asian Fusion Restaurant 68303 2011 +# 26 PleaseBurgers Restaurant 20132 1402 +# 27 Reagan's Vegan Restaurant 20201 6201 +# 28 FreshCart Store-to-Table Restaurant 83533 2751 + +# Unnamed: 4 +# 0 Opened +# 1 2023-07-19 00:00:00 +# 2 2023-06-11 00:00:00 +# 3 2023-05-12 00:00:00 +# 4 2023-01-30 00:00:00 +# 5 NaN +# 6 2023-05-01 00:00:00 +# 7 2023-01-03 00:00:00 +# 8 2023-05-31 00:00:00 +# 9 NaN +# 10 2023-11-04 00:00:00 +# 11 2023-01-10 00:00:00 +# 12 2023-03-09 00:00:00 +# 13 2023-01-08 00:00:00 +# 14 2023-09-20 00:00:00 +# 15 2023-01-20 00:00:00 +# 16 NaN +# 17 2023-09-20 00:00:00 +# 18 2023-01-20 00:00:00 +# 19 2023-03-06 00:00:00 +# 20 2023-01-07 00:00:00 +# 21 2023-02-06 00:00:00 +# 22 2023-05-07 00:00:00 +# 23 NaN +# 24 2023-10-20 00:00:00 +# 25 2023-02-12 00:00:00 +# 26 2023-02-15 00:00:00 +# 27 2023-07-20 00:00:00 +# 28 2023-12-08 00:00:00 +# ''' +# print(num_tokens_from_string(prompt)) + + +# { +# "paths": { +# "/tools/python": { +# "post": { +# "summary": "Execute Python", +# "operationId": "execute_python_tools_python_post", +# "requestBody": { +# "content": { +# "application/json": { +# "schema": { +# "$ref": "#/components/schemas/Item" +# } +# } +# }, +# "required": true +# }, +# "responses": { +# "200": { +# "description": "Successful Response", +# "content": { +# "application/json": { +# "schema": {} +# } +# } +# }, +# "422": { +# "description": "Validation Error", +# "content": { +# "application/json": { +# "schema": { +# "$ref": "#/components/schemas/HTTPValidationError" +# } +# } +# } +# } +# } +# } +# } +# } +# } \ No newline at end of file diff --git a/friday/core/web_browser.py b/friday/core/web_browser.py new file mode 100644 index 0000000..dd72292 --- /dev/null +++ b/friday/core/web_browser.py @@ -0,0 +1,56 @@ +from bs4 import BeautifulSoup +import requests +import re +from typing import Tuple + +# url = "https://en.wikipedia.org/wiki/Mercedes_Sosa" +# with open("../../examples/config.json") as f: +# config = json.load(f) +# embeddings = OpenAIEmbeddings( +# openai_api_key=config['OPENAI_API_KEY'], +# openai_organization=config['OPENAI_ORGANIZATION'], +# ) +def web_browser( url: str, max_retry: int = 3) -> Tuple[bool, str]: + headers = {'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML like Gecko) Chrome/52.0.2743.116 Safari/537.36'} + for _ in range(max_retry): + try: + res = requests.get(url,headers=headers, timeout=15) + if res.status_code == 200: + res.raise_for_status() + else: + raise RuntimeError("Failed to load page, code {}".format(res.status_code)) + except Exception: + res = None + continue + res.encoding = res.apparent_encoding + content = res.text + break + if res is None: + return "Timeout for loading this page, Please try to load another one or search again." + try: + soup = BeautifulSoup(content, 'html.parser') + # paragraphs = soup.find_all('p') + # page_detail = "" + # for p in paragraphs: + # text = p.get_text().strip() + # page_detail += text + # if(len(page_detail) < 40): + page_detail = soup.getText() + page_detail = re.sub(r'\s+', ' ', page_detail) + page_detail = re.sub(r'\n\s*\n', '\n', page_detail) + # text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50) + # texts = text_splitter.create_documents([page_detail]) + # docsearch = Chroma.from_documents(texts, embeddings) + # docs = docsearch.similarity_search(query, k=5) + + return page_detail + except Exception: + return "Timeout for loading this page, Please try to load another one or search again." + +url = "https://zhuanlan.zhihu.com/p/541484549" +page_detail = web_browser(url=url) +print(page_detail) +print("{} token".format(len(page_detail))) + +# with open("test.json", "w", encoding="utf-8") as f: +# json.dump(page_detail, f, ensure_ascii=False, indent=4) diff --git a/friday/environment/__init__.py b/friday/environment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/friday/environment/bash_env.py b/friday/environment/bash_env.py new file mode 100644 index 0000000..bf8ace4 --- /dev/null +++ b/friday/environment/bash_env.py @@ -0,0 +1,56 @@ +import subprocess +import os + +from friday.core.schema import EnvState +from friday.environment.env import Env + + +class BashEnv(Env): + """Base class for all actions. + + Args: + description (str, optional): The description of the action. Defaults to + None. + name (str, optional): The name of the action. If None, the name will + be class name. Defaults to None. + """ + + def __init__(self) -> None: + super().__init__() + self._name: str = self.__class__.__name__ + + def step(self, _command) -> EnvState: + self.env_state = EnvState(command=_command) + _command = _command() + ' && pwd' + try: + results = subprocess.run([_command], capture_output=True, check=True, cwd=self.working_dir, + text=True, shell=True, timeout=self.timeout) + if results.stdout: + stout = results.stdout.strip().split('\n') + self.env_state.result = "\n".join(stout[:-1]) + self.observe(stout[-1]) + return self.env_state + except subprocess.CalledProcessError as e: + self.env_state.error = e.stderr + except Exception as e: + self.env_state.error = repr(e) + self.observe(self.working_dir) + return self.env_state + + def reset(self): + self.working_dir = os.path.abspath(os.path.join(__file__, "..", "..", "..", "working_dir")) + + def observe(self, pwd): + self.env_state.pwd = pwd + self.working_dir = pwd + self.env_state.ls = subprocess.run(['ls'], cwd=self.working_dir, capture_output=True, text=True).stdout + + +if __name__ == '__main__': + env = BashEnv() + print(env.name) + # print(env.step("ls")) + # print(env.step("cd ../../")) + # print(env.step("gogo")) + # env.reset() + # print(env.step("sleep 3")) diff --git a/friday/environment/env.py b/friday/environment/env.py new file mode 100644 index 0000000..a2804d1 --- /dev/null +++ b/friday/environment/env.py @@ -0,0 +1,79 @@ +import os + +from typing import Optional, Union, List +from friday.core.schema import EnvState + + +class Env: + """Base class for all actions. + + Args: + description (str, optional): The description of the action. Defaults to + None. + name (str, optional): The name of the action. If None, the name will + be class name. Defaults to None. + """ + + def __init__(self) -> None: + self._name: str = self.__class__.__name__ + self.timeout: int = 300 + self.working_dir = os.path.abspath(os.path.join(__file__, "..", "..", "..", "working_dir")) + if not os.path.exists(self.working_dir): + os.makedirs(self.working_dir) + + self.env_state: Union[EnvState, None] = None + + def list_working_dir(self): + """ + Lists files and directories in the given directory with details similar to 'ls' command in Linux. + """ + directory = self.working_dir + # Check if the directory exists + if not os.path.exists(directory): + return f"Directory '{directory}' does not exist." + + # List files and directories + files_and_dirs = os.listdir(directory) + + # Create a list to store the details + details = [] + + for name in files_and_dirs: + # Get the full path + full_path = os.path.join(directory, name) + + # Get file or directory size + size = os.path.getsize(full_path) + + # Check if it's a file or directory + if os.path.isdir(full_path): + type = 'Directory' + else: + type = 'File' + + details.append(f"{name}\t {size} bytes\t {type}") + + return "\n".join(details) + + + + def step(self, _command) -> EnvState: + raise NotImplementedError + + def reset(self): + raise NotImplementedError + @property + def name(self): + return self._name + + def __repr__(self): + return f'{self.name}' + + def __str__(self): + return self.__repr__() + + +if __name__ == '__main__': + env = Env() + env.env_state = EnvState() + # result = env.observe() diff --git a/friday/environment/py_env.py b/friday/environment/py_env.py new file mode 100644 index 0000000..809a5a3 --- /dev/null +++ b/friday/environment/py_env.py @@ -0,0 +1,76 @@ +import subprocess +import os +from friday.core.schema import EnvState +from friday.environment.env import Env +from tempfile import NamedTemporaryFile + + +class PythonEnv(Env): + """Base class for all actions. + + Args: + description (str, optional): The description of the action. Defaults to + None. + name (str, optional): The name of the action. If None, the name will + be class name. Defaults to None. + """ + + def __init__(self) -> None: + super().__init__() + self._name: str = self.__class__.__name__ + + def step(self, _command: str, args: list[str] | str = []) -> EnvState: + tmp_code_file = NamedTemporaryFile("w", dir=self.working_dir, suffix=".py", encoding="utf-8") + # Solving the issue of not being able to retrieve the current working directory of the last line of output + _command = _command.strip() + "\n" + "import os" + "\n" + "print(os.getcwd())" + tmp_code_file.write(_command) + tmp_code_file.flush() + filename = tmp_code_file.name + if isinstance(args, str): + args = args.split() # Convert space-separated string to a list + self.env_state = EnvState(command=_command) + try: + results = subprocess.run( + ["python", '-B', str(filename)], + encoding="utf8", + check=True, cwd=self.working_dir, timeout=self.timeout, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + # If there is standard output. + if results.stdout: + stout = results.stdout.strip().split('\n') + self.env_state.result = "\n".join(stout[:-1]) + self.observe(stout[-1]) + return self.env_state + except subprocess.CalledProcessError as e: + self.env_state.error = e.stderr + except Exception as e: + self.env_state.error = repr(e) + finally: + tmp_code_file.close() + self.observe(self.working_dir) + + return self.env_state + + def reset(self): + self.working_dir = os.path.abspath(os.path.join(__file__, "..", "..", "..", "working_dir")) + + def observe(self, pwd): + self.env_state.pwd = pwd + self.working_dir = pwd + self.env_state.ls = subprocess.run(['ls'], cwd=self.working_dir, capture_output=True, text=True).stdout + + +DEFAULT_DESCRIPTION = """def solution(): + print("hello world!") + print("hello world!") + return "return!" +""" +if __name__ == '__main__': + env = PythonEnv() + print(env.step(DEFAULT_DESCRIPTION)) + # print(env.step("cd ../../")) + # print(env.step("gogo")) + # env.reset() + # print(env.step("sleep 3")) diff --git a/run.py b/run.py new file mode 100644 index 0000000..d5d7fa1 --- /dev/null +++ b/run.py @@ -0,0 +1,132 @@ +import os +import argparse +import logging +from datasets import load_dataset +from friday.agent.friday_agent import FridayAgent + + +def main(): + parser = argparse.ArgumentParser(description='Inputs') + parser.add_argument('--action_lib_path', type=str, default='friday/action_lib', help='tool repo path') + parser.add_argument('--config_path', type=str, default='config.json', help='openAI config file path') + parser.add_argument('--query', type=str, default='''Move the text files containing the word 'agent' from the folder named 'document' to the path 'working_dir/agent' ''', help='user query') + parser.add_argument('--query_file_path', type=str, default='', help='user query file path') + parser.add_argument('--logging_filedir', type=str, default='log', help='log path') + parser.add_argument('--logging_filename', type=str, default='temp.log', help='log file name') + parser.add_argument('--score', type=int, default=8, help='critic score > score => store the tool') + args = parser.parse_args() + + logging.basicConfig(filename=os.path.join(args.logging_filedir, args.logging_filename), level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + + friday_agent = FridayAgent(config_path=args.config_path, action_lib_dir=args.action_lib_path) + planning_agent = friday_agent.planner + retrieve_agent = friday_agent.retriever + execute_agent = friday_agent.executor + + task = 'Your task is: {0}'.format(args.query) + if args.query_file_path != '': + task = task + '\nThe path of the files you need to use: {0}'.format(args.query_file_path) + + print('Task:\n'+task) + logging.info(task) + + # relevant action + retrieve_action_name = retrieve_agent.retrieve_action_name(task) + retrieve_action_description_pair = retrieve_agent.retrieve_action_description_pair(retrieve_action_name) + + # decompose task + planning_agent.decompose_task(task, retrieve_action_description_pair) + + # iter each subtask + while planning_agent.execute_list: + action = planning_agent.execute_list[0] + action_node = planning_agent.action_node[action] + description = action_node.description + logging.info("The current subtask is: {subtask}".format(subtask=description)) + code = '' + # The return value of the current task + result = '' + next_action = action_node.next_action + relevant_code = {} + type = action_node.type + pre_tasks_info = planning_agent.get_pre_tasks_info(action) + if type == 'Code': + # retrieve existing action + retrieve_name = retrieve_agent.retrieve_action_name(description, 3) + relevant_code = retrieve_agent.retrieve_action_code_pair(retrieve_name) + # task execute step + if type == 'QA': + # result = execute_agent.question_and_answer_action(pre_tasks_info, task, task) + if planning_agent.action_num == 1: + result = execute_agent.question_and_answer_action(pre_tasks_info, task, task) + else: + result = execute_agent.question_and_answer_action(pre_tasks_info, task, description) + print(result) + logging.info(result) + else: + invoke = '' + if type == 'API': + api_path = execute_agent.extract_API_Path(description) + code = execute_agent.api_action(description, api_path, pre_tasks_info) + else: + code, invoke = execute_agent.generate_action(action, description, pre_tasks_info, relevant_code) + # Execute python tool class code + state = execute_agent.execute_action(code, invoke, type) + result = state.result + logging.info(state) + # Check whether the code runs correctly, if not, amend the code + if type == 'Code': + need_mend = False + trial_times = 0 + critique = '' + score = 0 + # If no error is reported, check whether the task is completed + if state.error == None: + critique, judge, score = execute_agent.judge_action(code, description, state, next_action) + if not judge: + print("critique: {}".format(critique)) + need_mend = True + else: + # Determine whether it is caused by an error outside the code + reasoning, error_type = execute_agent.analysis_action(code, description, state) + if error_type == 'replan': + relevant_action_name = retrieve_agent.retrieve_action_name(reasoning) + relevant_action_description_pair = retrieve_agent.retrieve_action_description_pair(relevant_action_name) + planning_agent.replan_task(reasoning, action, relevant_action_description_pair) + continue + need_mend = True + # The code failed to complete its task, fix the code + while (trial_times < execute_agent.max_iter and need_mend == True): + trial_times += 1 + print("current amend times: {}".format(trial_times)) + new_code, invoke = execute_agent.amend_action(code, description, state, critique, pre_tasks_info) + critique = '' + code = new_code + # Run the current code and check for errors + state = execute_agent.execute_action(code, invoke, type) + result = state.result + logging.info(state) + # print(state) + # Recheck + if state.error == None: + critique, judge, score = execute_agent.judge_action(code, description, state, next_action) + # The task execution is completed and the loop exits + if judge: + need_mend = False + break + # print("critique: {}".format(critique)) + else: # The code still needs to be corrected + need_mend = True + + # If the task still cannot be completed, an error message will be reported. + if need_mend == True: + print("I can't Do this Task!!") + break + else: # The task is completed, if code is save the code, args_description, action_description in lib + if score >= args.score: + execute_agent.store_action(action, code) + print("Current task execution completed!!!") + planning_agent.update_action(action, result, relevant_code, True, type) + planning_agent.execute_list.remove(action) +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/working_dir/document/1.txt b/working_dir/document/1.txt new file mode 100644 index 0000000..e00d011 --- /dev/null +++ b/working_dir/document/1.txt @@ -0,0 +1 @@ +Autonomous interaction with the computer has been a longstanding challenge with great potential, and the recent proliferation of large language models (LLMs) has markedly accelerated progress in building digital agents. \ No newline at end of file diff --git a/working_dir/document/2.txt b/working_dir/document/2.txt new file mode 100644 index 0000000..233bb9f --- /dev/null +++ b/working_dir/document/2.txt @@ -0,0 +1 @@ +However, most of these agents are designed to interact with a narrow domain, such as a specific software or website. \ No newline at end of file diff --git a/working_dir/document/3.txt b/working_dir/document/3.txt new file mode 100644 index 0000000..ddd7a49 --- /dev/null +++ b/working_dir/document/3.txt @@ -0,0 +1 @@ +Our OS-Copilot framework and empirical findings provide infrastructure and insights for future research toward more capable and general purpose computer agents. \ No newline at end of file diff --git a/working_dir/document/4.txt b/working_dir/document/4.txt new file mode 100644 index 0000000..5f7eed4 --- /dev/null +++ b/working_dir/document/4.txt @@ -0,0 +1 @@ +To tackle the first challenge, we introduce OS-Copilot, a framework for OS-level language agents, accompanied by modular implementations for each component to facilitate agent development. \ No newline at end of file diff --git a/working_dir/document/5.txt b/working_dir/document/5.txt new file mode 100644 index 0000000..ca87659 --- /dev/null +++ b/working_dir/document/5.txt @@ -0,0 +1 @@ +From the 1920 novel R.U.R to characters like JARVIS in The Iron Man, throughout the past century, people have always dreamed of AI being employed as digital assistants for daily tasks. \ No newline at end of file diff --git a/working_dir/document/6.txt b/working_dir/document/6.txt new file mode 100644 index 0000000..da9d90e --- /dev/null +++ b/working_dir/document/6.txt @@ -0,0 +1 @@ +At the heart of these assistants lies a natural language user interface (NLUI) that enables users to interact with computers using natural language. \ No newline at end of file diff --git a/working_dir/document/7.txt b/working_dir/document/7.txt new file mode 100644 index 0000000..51c2abd --- /dev/null +++ b/working_dir/document/7.txt @@ -0,0 +1 @@ +However, current NLUI systems, like Microsoft’s Cortana, are primarily tailored for simple tasks like setting the alarm yet struggling with complex human requests. \ No newline at end of file diff --git a/working_dir/document/8.txt b/working_dir/document/8.txt new file mode 100644 index 0000000..dfadb70 --- /dev/null +++ b/working_dir/document/8.txt @@ -0,0 +1 @@ +Fortunately, advancements in large language models (LLMs) bring us closer to realizing the next generation of NLUI systems. \ No newline at end of file