Spaces:
Runtime error
Runtime error
Upload 237 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env +58 -0
- .gitattributes +2 -0
- .gitignore +40 -0
- GeneralAgent/__init__.py +1 -0
- GeneralAgent/agent/__init__.py +14 -0
- GeneralAgent/agent/abs_agent.py +83 -0
- GeneralAgent/agent/normal_agent.py +189 -0
- GeneralAgent/agent/stack_agent.py +221 -0
- GeneralAgent/cli.py +39 -0
- GeneralAgent/interpreter/__init__.py +22 -0
- GeneralAgent/interpreter/applescript_interpreter.py +46 -0
- GeneralAgent/interpreter/embedding_retrieve_interpreter.py +77 -0
- GeneralAgent/interpreter/file_interpreter.py +120 -0
- GeneralAgent/interpreter/interpreter.py +80 -0
- GeneralAgent/interpreter/link_retrieve_interpreter.py +57 -0
- GeneralAgent/interpreter/plan_interpreter.py +59 -0
- GeneralAgent/interpreter/python_interpreter.py +326 -0
- GeneralAgent/interpreter/role_interpreter.py +40 -0
- GeneralAgent/interpreter/shell_interpreter.py +44 -0
- GeneralAgent/interpreter/ui_interpreter.py +51 -0
- GeneralAgent/memory/__init__.py +4 -0
- GeneralAgent/memory/link_memory.py +125 -0
- GeneralAgent/memory/normal_memory.py +42 -0
- GeneralAgent/memory/stack_memory.py +223 -0
- GeneralAgent/pytest.ini +2 -0
- GeneralAgent/requirements.txt +30 -0
- GeneralAgent/skills/__init__.py +121 -0
- GeneralAgent/skills/agent_builder_2.py +245 -0
- GeneralAgent/skills/agents.py +105 -0
- GeneralAgent/skills/ai_draw_prompt_gen.py +11 -0
- GeneralAgent/skills/application_builder.py +598 -0
- GeneralAgent/skills/applications.py +85 -0
- GeneralAgent/skills/build_web.py +149 -0
- GeneralAgent/skills/concatenate_videos/concatenate_videos.py +27 -0
- GeneralAgent/skills/concatenate_videos/f63bfaae7b0e.mp4 +0 -0
- GeneralAgent/skills/download_file.py +50 -0
- GeneralAgent/skills/file_operation.py +51 -0
- GeneralAgent/skills/llm_inference.py +458 -0
- GeneralAgent/skills/memory_utils.py +153 -0
- GeneralAgent/skills/merge_video_audio/merge_video_audio.py +67 -0
- GeneralAgent/skills/merge_video_audio/music.wav +3 -0
- GeneralAgent/skills/merge_video_audio/narration.mp3 +0 -0
- GeneralAgent/skills/merge_video_audio/tmp_audio.mp3 +0 -0
- GeneralAgent/skills/merge_video_audio/video.mp4 +0 -0
- GeneralAgent/skills/musicgen/generate_music.py +32 -0
- GeneralAgent/skills/python_envs.py +130 -0
- GeneralAgent/skills/replicate_api.py +75 -0
- GeneralAgent/skills/scrape_dynamic_web.py +70 -0
- GeneralAgent/skills/split_text.py +28 -0
- GeneralAgent/skills/stable_video_diffusion/dab774a452f3.jpg +0 -0
.env
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# LLM import use litellm lib (https://docs.litellm.ai/docs/)
|
| 2 |
+
# OPENAI | AZURE | OR other LLM PREFIX
|
| 3 |
+
LLM_SOURCE='OPENAI'
|
| 4 |
+
|
| 5 |
+
LLM_TEMPERATURE='0.1'
|
| 6 |
+
|
| 7 |
+
# open ai
|
| 8 |
+
OPENAI_API_KEY='sk-666'
|
| 9 |
+
OPENAI_API_BASE='https://api.zhtec.xyz/xht/chatWith16k.php'
|
| 10 |
+
OPENAI_EMBEDDING_MODEL='text-embedding-ada-002'
|
| 11 |
+
OPENAI_LLM_MODEL_NORMAL='gpt-3.5-turbo-16k'
|
| 12 |
+
OPENAI_LLM_MODEL_SMART='gpt-4'
|
| 13 |
+
OPENAI_LLM_MODEL_SMART_LIMIT='8000'
|
| 14 |
+
OPENAI_LLM_MODEL_SMART_LONG='gpt-4-32k'
|
| 15 |
+
OPENAI_LLM_MODEL_SMART_LONG_LIMIT='32000'
|
| 16 |
+
OPENAI_LLM_MODEL_LONG='gpt-4-1106-preview'
|
| 17 |
+
OPENAI_LLM_MODEL_VISION='gpt-4-vision-preview'
|
| 18 |
+
OPENAI_LLM_MODEL_ALL='gpt-4-all'
|
| 19 |
+
|
| 20 |
+
# azure open ai
|
| 21 |
+
AZURE_API_KEY='xx'
|
| 22 |
+
AZURE_API_BASE='xx'
|
| 23 |
+
AZURE_API_VERSION='2023-06-01-preview'
|
| 24 |
+
AZURE_EMBEDDING_MODEL='azure/ada002'
|
| 25 |
+
AZURE_LLM_MODEL_NORMAL='azure/gpt35t'
|
| 26 |
+
AZURE_LLM_MODEL_SMART='azure/gpt4'
|
| 27 |
+
OPENAI_LLM_MODEL_SMART_LIMIT='8000'
|
| 28 |
+
AZURE_LLM_MODEL_LONG='azure/gpt4'
|
| 29 |
+
|
| 30 |
+
# replicate
|
| 31 |
+
REPLICATE_API_TOKEN='r8_E2UIynoq1yuY5ErDijgm4uR1C2pW8pG4IPXU7'
|
| 32 |
+
|
| 33 |
+
## user data directory, store user's applications、functions、application_datas and server datas
|
| 34 |
+
DATA_DIR='/workspace/data/'
|
| 35 |
+
|
| 36 |
+
# cache llm inference and embedding, useful when develop and debug
|
| 37 |
+
LLM_CACHE='no'
|
| 38 |
+
EMBEDDING_CACHE='yes'
|
| 39 |
+
# CACHE_PATH='./llm_cache.json'
|
| 40 |
+
|
| 41 |
+
# google search tool at https://google.serper.dev
|
| 42 |
+
SERPER_API_KEY='7a7a62b5f4a991043dd7fe96553802fb7675927c'
|
| 43 |
+
|
| 44 |
+
# Whether to automatically run python, shell, applescript and other codes
|
| 45 |
+
# Default no: n
|
| 46 |
+
AUTO_RUN='y'
|
| 47 |
+
|
| 48 |
+
# Logging level
|
| 49 |
+
# Default: INFO, can be DEBUG, INFO, WARNING, ERROR
|
| 50 |
+
LOG_LEVEL='INFO'
|
| 51 |
+
|
| 52 |
+
## ui builder directory, webui server(webui/server/server/app.py) will automatically set TSX_BUILDER_DIR to webui/server/server/ts_builder
|
| 53 |
+
# TSX_BUILDER_DIR
|
| 54 |
+
|
| 55 |
+
## local applications code directory. webui server(webui/server/server/app.py) will automatically set LOCAL_APPLICATIONS_DIR to webui/server/server/applicatoins
|
| 56 |
+
# LOCAL_APPLICATIONS_DIR
|
| 57 |
+
|
| 58 |
+
# export $(grep -v '^#' .env | sed 's/^export //g' | xargs)
|
.gitattributes
CHANGED
|
@@ -35,3 +35,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
GeneralAgent/GeneralAgent/skills/merge_video_audio/music.wav filter=lfs diff=lfs merge=lfs -text
|
| 37 |
GeneralAgent/test/data/Nougat.pdf filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
GeneralAgent/GeneralAgent/skills/merge_video_audio/music.wav filter=lfs diff=lfs merge=lfs -text
|
| 37 |
GeneralAgent/test/data/Nougat.pdf filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
GeneralAgent/skills/merge_video_audio/music.wav filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
test/data/Nougat.pdf filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.pyc
|
| 2 |
+
GeneralAgent/.env
|
| 3 |
+
GeneralAgent/*.json
|
| 4 |
+
|
| 5 |
+
test/data_0.json
|
| 6 |
+
test/data/test_workspace/*
|
| 7 |
+
test/tetris.py
|
| 8 |
+
test/tetris.py.bak
|
| 9 |
+
GeneralAgent/cache_json
|
| 10 |
+
test/data/plan_memory.json
|
| 11 |
+
test/data/test_interpreter.bin
|
| 12 |
+
test/data/b.txt
|
| 13 |
+
test/data/a.txt
|
| 14 |
+
test/data/hello.pptx
|
| 15 |
+
test/.env
|
| 16 |
+
|
| 17 |
+
build/*
|
| 18 |
+
dist/*
|
| 19 |
+
GeneralAgent.egg-info*
|
| 20 |
+
test/multi_lines_input/*
|
| 21 |
+
test/multi_lines_input/*
|
| 22 |
+
.env
|
| 23 |
+
examples/memory.json
|
| 24 |
+
test/link_memory.json
|
| 25 |
+
test/memory.json
|
| 26 |
+
test/llm_cache.json
|
| 27 |
+
test/summary_memory.json
|
| 28 |
+
*/llm_cache.json
|
| 29 |
+
test/test_skills/data/*
|
| 30 |
+
test/test_skills/llm_cache.json
|
| 31 |
+
webui/server/server/applications/test_application_id/bot.json
|
| 32 |
+
webui/server/server/applications/test_application_id/main.py
|
| 33 |
+
data/*
|
| 34 |
+
|
| 35 |
+
.idea/*
|
| 36 |
+
test/test_skills/code/*
|
| 37 |
+
test/data/ui/*
|
| 38 |
+
test/code/*
|
| 39 |
+
test/data/read_interpreter/*
|
| 40 |
+
webui/server/server/ts_builder/src/lib/index.tsx
|
GeneralAgent/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .skills import skills
|
GeneralAgent/agent/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# import
|
| 2 |
+
from .normal_agent import NormalAgent
|
| 3 |
+
from .stack_agent import StackAgent
|
| 4 |
+
|
| 5 |
+
class Agent(NormalAgent):
|
| 6 |
+
pass
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
LLM_SOURCE = os.environ.get('LLM_SOURCE', None)
|
| 10 |
+
if LLM_SOURCE is None:
|
| 11 |
+
print('enviroment variable LLM_SOURCE not available.')
|
| 12 |
+
|
| 13 |
+
from GeneralAgent.utils import set_logging_level
|
| 14 |
+
set_logging_level()
|
GeneralAgent/agent/abs_agent.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import abc
|
| 2 |
+
import os
|
| 3 |
+
import asyncio
|
| 4 |
+
from GeneralAgent.utils import default_get_input, default_output_callback
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class AbsAgent(metaclass=abc.ABCMeta):
|
| 8 |
+
"""
|
| 9 |
+
Abstract Agent
|
| 10 |
+
@memory: Memory
|
| 11 |
+
@interpreters: list, interpreters
|
| 12 |
+
@model_type: str, 'normal' or 'smart' or 'long'. For OpenAI api, normal=gpt3.5, smart=gpt4, long=gpt3.5-16k
|
| 13 |
+
@hide_output_parse: bool, hide the llm's output that output interpreters will parse, default True
|
| 14 |
+
"""
|
| 15 |
+
memory = None
|
| 16 |
+
interpreters = []
|
| 17 |
+
model_type = 'normal'
|
| 18 |
+
hide_output_parse = False
|
| 19 |
+
|
| 20 |
+
def add_role_prompt(self, prompt):
|
| 21 |
+
"""
|
| 22 |
+
add role prompt
|
| 23 |
+
"""
|
| 24 |
+
if len(self.interpreters) > 0 and self.interpreters[0].__class__.__name__ == 'RoleInterpreter':
|
| 25 |
+
role_interpreter = self.interpreters[0]
|
| 26 |
+
if role_interpreter.system_prompt is not None:
|
| 27 |
+
role_interpreter.system_prompt += '\n' + prompt
|
| 28 |
+
else:
|
| 29 |
+
role_interpreter.system_prompt_template += '\n' + prompt
|
| 30 |
+
|
| 31 |
+
@abc.abstractmethod
|
| 32 |
+
async def run(self, input=None, output_callback=default_output_callback, input_for_memory_node_id=-1):
|
| 33 |
+
"""
|
| 34 |
+
input: str, user's new input, None means continue to run where it stopped
|
| 35 |
+
input_for_memory_node_id: int, -1 means input is not from memory, None means input new, otherwise input is for memory node
|
| 36 |
+
output_callback: async function, output_callback(content: str) -> None
|
| 37 |
+
"""
|
| 38 |
+
pass
|
| 39 |
+
|
| 40 |
+
def stop(self):
|
| 41 |
+
"""
|
| 42 |
+
stop the agent
|
| 43 |
+
"""
|
| 44 |
+
self.stop_event.set()
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def __init__(self, workspace='./'):
|
| 48 |
+
"""
|
| 49 |
+
@workspace: str, workspace path
|
| 50 |
+
"""
|
| 51 |
+
if not os.path.exists(workspace):
|
| 52 |
+
os.makedirs(workspace)
|
| 53 |
+
self.is_running = False
|
| 54 |
+
self.stop_event = asyncio.Event()
|
| 55 |
+
self.workspace = workspace
|
| 56 |
+
|
| 57 |
+
@classmethod
|
| 58 |
+
def empty(cls, workspace='./'):
|
| 59 |
+
"""
|
| 60 |
+
empty agent, only role interpreter and memory, work like a basic LLM chatbot
|
| 61 |
+
@workspace: str, workspace path
|
| 62 |
+
"""
|
| 63 |
+
pass
|
| 64 |
+
|
| 65 |
+
@classmethod
|
| 66 |
+
def default(cls, workspace='./', retrieve_type='embedding'):
|
| 67 |
+
"""
|
| 68 |
+
default agent, with all interpreters
|
| 69 |
+
@workspace: str, workspace path
|
| 70 |
+
@retrieve_type: str, 'embedding' or 'link'
|
| 71 |
+
"""
|
| 72 |
+
pass
|
| 73 |
+
|
| 74 |
+
@classmethod
|
| 75 |
+
def with_functions(cls, functions, role_prompt=None, workspace = './', model_type='smart'):
|
| 76 |
+
"""
|
| 77 |
+
functions: list, [function1, function2, ...]
|
| 78 |
+
@role_prompt: str, role prompt
|
| 79 |
+
@workspace: str, workspace path
|
| 80 |
+
@import_code: str, import code
|
| 81 |
+
@libs: str, libs
|
| 82 |
+
"""
|
| 83 |
+
pass
|
GeneralAgent/agent/normal_agent.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Agent
|
| 2 |
+
import os, re
|
| 3 |
+
import asyncio
|
| 4 |
+
import logging
|
| 5 |
+
from GeneralAgent.utils import default_get_input, default_output_callback
|
| 6 |
+
from GeneralAgent.memory import NormalMemory
|
| 7 |
+
from GeneralAgent.interpreter import Interpreter
|
| 8 |
+
from GeneralAgent.interpreter import EmbeddingRetrieveInterperter, LinkRetrieveInterperter
|
| 9 |
+
from GeneralAgent.interpreter import RoleInterpreter, PythonInterpreter, ShellInterpreter, AppleScriptInterpreter, FileInterpreter
|
| 10 |
+
from .abs_agent import AbsAgent
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class NormalAgent(AbsAgent):
|
| 14 |
+
|
| 15 |
+
def __init__(self, workspace='./'):
|
| 16 |
+
super().__init__(workspace)
|
| 17 |
+
self.memory = NormalMemory(serialize_path=f'{workspace}/normal_memory.json')
|
| 18 |
+
|
| 19 |
+
@classmethod
|
| 20 |
+
def empty(cls, workspace='./'):
|
| 21 |
+
"""
|
| 22 |
+
empty agent, only role interpreter and memory, work like a basic LLM chatbot
|
| 23 |
+
"""
|
| 24 |
+
agent = cls(workspace)
|
| 25 |
+
agent.interpreters = [RoleInterpreter()]
|
| 26 |
+
return agent
|
| 27 |
+
|
| 28 |
+
@classmethod
|
| 29 |
+
def default(cls, workspace='./', retrieve_type='embedding'):
|
| 30 |
+
"""
|
| 31 |
+
default agent, with all interpreters
|
| 32 |
+
@workspace: str, workspace path
|
| 33 |
+
@retrieve_type: str, 'embedding' or 'link'
|
| 34 |
+
"""
|
| 35 |
+
agent = cls(workspace)
|
| 36 |
+
# memory
|
| 37 |
+
# interpreters
|
| 38 |
+
role_interpreter = RoleInterpreter()
|
| 39 |
+
python_interpreter = PythonInterpreter(serialize_path=f'{workspace}/code.bin')
|
| 40 |
+
if retrieve_type == 'embedding':
|
| 41 |
+
retrieve_interperter = EmbeddingRetrieveInterperter(serialize_path=f'{workspace}/read_interperter/')
|
| 42 |
+
else:
|
| 43 |
+
retrieve_interperter = LinkRetrieveInterperter(python_interpreter)
|
| 44 |
+
bash_interpreter = ShellInterpreter(workspace)
|
| 45 |
+
applescript_interpreter = AppleScriptInterpreter()
|
| 46 |
+
file_interpreter = FileInterpreter()
|
| 47 |
+
agent.interpreters = [role_interpreter, retrieve_interperter, python_interpreter, bash_interpreter, applescript_interpreter, file_interpreter]
|
| 48 |
+
return agent
|
| 49 |
+
|
| 50 |
+
@classmethod
|
| 51 |
+
def with_functions(cls, functions, role_prompt=None, workspace = './', model_type='smart'):
|
| 52 |
+
"""
|
| 53 |
+
functions: list, [function1, function2, ...]
|
| 54 |
+
@role_prompt: str, role prompt
|
| 55 |
+
@workspace: str, workspace path
|
| 56 |
+
@import_code: str, import code
|
| 57 |
+
@libs: str, libs
|
| 58 |
+
"""
|
| 59 |
+
agent = cls(workspace)
|
| 60 |
+
role_interpreter = RoleInterpreter()
|
| 61 |
+
python_interpreter = PythonInterpreter(serialize_path=f'{workspace}/code.bin')
|
| 62 |
+
python_interpreter.function_tools = functions
|
| 63 |
+
agent.interpreters = [role_interpreter, python_interpreter, ShellInterpreter()]
|
| 64 |
+
agent.model_type = model_type
|
| 65 |
+
if role_prompt is not None:
|
| 66 |
+
agent.add_role_prompt(role_prompt)
|
| 67 |
+
return agent
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
async def run(self, input=None, output_callback=default_output_callback, input_for_memory_node_id=-1):
|
| 71 |
+
"""
|
| 72 |
+
agent run: parse intput -> get llm messages -> run LLM and parse output
|
| 73 |
+
@input: str, user's new input, None means continue to run where it stopped
|
| 74 |
+
@input_for_memory_node_id: int, -1 means input is not from memory, None means input new, otherwise input is for memory node
|
| 75 |
+
@output_callback: async function, output_callback(content: str) -> None
|
| 76 |
+
"""
|
| 77 |
+
self.is_running = True
|
| 78 |
+
|
| 79 |
+
input_stop = await self._parse_input(input, output_callback)
|
| 80 |
+
if input_stop:
|
| 81 |
+
self.is_running = False
|
| 82 |
+
return
|
| 83 |
+
|
| 84 |
+
while True:
|
| 85 |
+
messages = await self._get_llm_messages()
|
| 86 |
+
output_stop = await self._llm_and_parse_output(messages, output_callback)
|
| 87 |
+
if output_stop:
|
| 88 |
+
self.is_running = False
|
| 89 |
+
return
|
| 90 |
+
await asyncio.sleep(0)
|
| 91 |
+
if self.stop_event.is_set():
|
| 92 |
+
self.is_running = False
|
| 93 |
+
return
|
| 94 |
+
|
| 95 |
+
async def _parse_input(self, input, output_callback):
|
| 96 |
+
self.memory.add_message('user', input)
|
| 97 |
+
input_content = input
|
| 98 |
+
input_stop = False
|
| 99 |
+
interpreter:Interpreter = None
|
| 100 |
+
for interpreter in self.interpreters:
|
| 101 |
+
if interpreter.input_match(input_content):
|
| 102 |
+
logging.info('interpreter: ' + interpreter.__class__.__name__)
|
| 103 |
+
parse_output, case_is_stop = await interpreter.input_parse(input_content)
|
| 104 |
+
if case_is_stop:
|
| 105 |
+
await output_callback(parse_output)
|
| 106 |
+
input_stop = True
|
| 107 |
+
return input_stop
|
| 108 |
+
|
| 109 |
+
async def _get_llm_messages(self):
|
| 110 |
+
from GeneralAgent import skills
|
| 111 |
+
messages = self.memory.get_messages()
|
| 112 |
+
token_limit = skills.get_llm_token_limit(self.model_type)
|
| 113 |
+
messages = skills.cut_messages(messages, int(token_limit*0.8))
|
| 114 |
+
system_prompt = '\n\n'.join([await interpreter.prompt(messages) for interpreter in self.interpreters])
|
| 115 |
+
messages = [{'role': 'system', 'content': system_prompt}] + messages
|
| 116 |
+
return messages
|
| 117 |
+
|
| 118 |
+
async def _llm_and_parse_output(self, messages, output_callback):
|
| 119 |
+
from GeneralAgent import skills
|
| 120 |
+
try:
|
| 121 |
+
result = ''
|
| 122 |
+
is_stop = True
|
| 123 |
+
is_break = False
|
| 124 |
+
in_parse_content = False
|
| 125 |
+
cache_tokens = []
|
| 126 |
+
response = skills.llm_inference(messages, model_type=self.model_type, stream=True)
|
| 127 |
+
for token in response:
|
| 128 |
+
if token is None: break
|
| 129 |
+
result += token
|
| 130 |
+
# logging.debug(result)
|
| 131 |
+
# print(token)
|
| 132 |
+
if self.hide_output_parse:
|
| 133 |
+
if not in_parse_content:
|
| 134 |
+
interpreter:Interpreter = None
|
| 135 |
+
for interpreter in self.interpreters:
|
| 136 |
+
is_start_matched, string_matched = interpreter.output_match_start(result)
|
| 137 |
+
if is_start_matched:
|
| 138 |
+
in_parse_content = True
|
| 139 |
+
# clear cache
|
| 140 |
+
cache_tokens.append(token)
|
| 141 |
+
left_count = len(string_matched)
|
| 142 |
+
while left_count > 0:
|
| 143 |
+
left_count -= len(cache_tokens[-1])
|
| 144 |
+
cache_tokens.remove(cache_tokens[-1])
|
| 145 |
+
while len(cache_tokens) > 0:
|
| 146 |
+
pop_token = cache_tokens.pop(0)
|
| 147 |
+
await output_callback(pop_token)
|
| 148 |
+
if not in_parse_content:
|
| 149 |
+
# cache token
|
| 150 |
+
cache_tokens.append(token)
|
| 151 |
+
if len(cache_tokens) > 5:
|
| 152 |
+
pop_token = cache_tokens.pop(0)
|
| 153 |
+
await output_callback(pop_token)
|
| 154 |
+
else:
|
| 155 |
+
await output_callback(token)
|
| 156 |
+
interpreter:Interpreter = None
|
| 157 |
+
for interpreter in self.interpreters:
|
| 158 |
+
if interpreter.output_match(result):
|
| 159 |
+
logging.info('interpreter: ' + interpreter.__class__.__name__)
|
| 160 |
+
output, is_stop = await interpreter.output_parse(result)
|
| 161 |
+
if interpreter.outptu_parse_done_recall is not None:
|
| 162 |
+
await interpreter.outptu_parse_done_recall()
|
| 163 |
+
if self.hide_output_parse:
|
| 164 |
+
is_matched, string_left = interpreter.output_match_end(result)
|
| 165 |
+
await output_callback(string_left)
|
| 166 |
+
while len(cache_tokens) > 0:
|
| 167 |
+
pop_token = cache_tokens.pop(0)
|
| 168 |
+
await output_callback(pop_token)
|
| 169 |
+
result += '\n' + output.strip() + '\n'
|
| 170 |
+
# logging.debug(result)
|
| 171 |
+
if not self.hide_output_parse or is_stop:
|
| 172 |
+
await output_callback('\n' + output.strip() + '\n')
|
| 173 |
+
is_break = True
|
| 174 |
+
in_parse_content = False
|
| 175 |
+
break
|
| 176 |
+
if is_break:
|
| 177 |
+
break
|
| 178 |
+
while len(cache_tokens) > 0:
|
| 179 |
+
pop_token = cache_tokens.pop(0)
|
| 180 |
+
await output_callback(pop_token)
|
| 181 |
+
# append messages
|
| 182 |
+
# logging.debug(result)
|
| 183 |
+
self.memory.append_message('assistant', result)
|
| 184 |
+
return is_stop
|
| 185 |
+
except Exception as e:
|
| 186 |
+
# if fail, recover
|
| 187 |
+
logging.exception(e)
|
| 188 |
+
await output_callback(str(e))
|
| 189 |
+
return True
|
GeneralAgent/agent/stack_agent.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Agent
|
| 2 |
+
import os, re
|
| 3 |
+
import asyncio
|
| 4 |
+
import logging
|
| 5 |
+
from GeneralAgent.utils import default_get_input, default_output_callback
|
| 6 |
+
from GeneralAgent.memory import StackMemory, StackMemoryNode
|
| 7 |
+
from GeneralAgent.interpreter import Interpreter
|
| 8 |
+
from GeneralAgent.interpreter import PlanInterpreter, EmbeddingRetrieveInterperter, LinkRetrieveInterperter
|
| 9 |
+
from GeneralAgent.interpreter import RoleInterpreter, PythonInterpreter, ShellInterpreter, AppleScriptInterpreter, FileInterpreter
|
| 10 |
+
from .abs_agent import AbsAgent
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class StackAgent(AbsAgent):
|
| 14 |
+
|
| 15 |
+
def __init__(self, workspace='./'):
|
| 16 |
+
super().__init__(workspace)
|
| 17 |
+
self.memory = StackMemory(serialize_path=f'{workspace}/memory.json')
|
| 18 |
+
|
| 19 |
+
@classmethod
|
| 20 |
+
def empty(cls, workspace='./'):
|
| 21 |
+
"""
|
| 22 |
+
empty agent, only role interpreter and memory, work like a basic LLM chatbot
|
| 23 |
+
"""
|
| 24 |
+
agent = cls(workspace)
|
| 25 |
+
agent.interpreters = [RoleInterpreter()]
|
| 26 |
+
return agent
|
| 27 |
+
|
| 28 |
+
@classmethod
|
| 29 |
+
def default(cls, workspace='./', retrieve_type='embedding'):
|
| 30 |
+
"""
|
| 31 |
+
default agent, with all interpreters
|
| 32 |
+
@workspace: str, workspace path
|
| 33 |
+
@retrieve_type: str, 'embedding' or 'link'
|
| 34 |
+
"""
|
| 35 |
+
agent = cls(workspace)
|
| 36 |
+
# memory
|
| 37 |
+
# interpreters
|
| 38 |
+
role_interpreter = RoleInterpreter()
|
| 39 |
+
python_interpreter = PythonInterpreter(serialize_path=f'{workspace}/code.bin')
|
| 40 |
+
plan_interperter = PlanInterpreter(agent.memory)
|
| 41 |
+
if retrieve_type == 'embedding':
|
| 42 |
+
retrieve_interperter = EmbeddingRetrieveInterperter(serialize_path=f'{workspace}/read_interperter/')
|
| 43 |
+
else:
|
| 44 |
+
retrieve_interperter = LinkRetrieveInterperter(python_interpreter)
|
| 45 |
+
bash_interpreter = ShellInterpreter(workspace)
|
| 46 |
+
applescript_interpreter = AppleScriptInterpreter()
|
| 47 |
+
file_interpreter = FileInterpreter()
|
| 48 |
+
agent.interpreters = [role_interpreter, plan_interperter, retrieve_interperter, python_interpreter, bash_interpreter, applescript_interpreter, file_interpreter]
|
| 49 |
+
return agent
|
| 50 |
+
|
| 51 |
+
@classmethod
|
| 52 |
+
def with_functions(cls, functions, role_prompt=None, workspace = './', model_type='smart'):
|
| 53 |
+
"""
|
| 54 |
+
functions: list, [function1, function2, ...]
|
| 55 |
+
@role_prompt: str, role prompt
|
| 56 |
+
@workspace: str, workspace path
|
| 57 |
+
@import_code: str, import code
|
| 58 |
+
@libs: str, libs
|
| 59 |
+
"""
|
| 60 |
+
agent = cls(workspace)
|
| 61 |
+
role_interpreter = RoleInterpreter(system_prompt=role_prompt)
|
| 62 |
+
python_interpreter = PythonInterpreter(serialize_path=f'{workspace}/code.bin')
|
| 63 |
+
python_interpreter.function_tools = functions
|
| 64 |
+
agent.interpreters = [role_interpreter, python_interpreter, ShellInterpreter()]
|
| 65 |
+
agent.model_type = model_type
|
| 66 |
+
return agent
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
async def run(self, input=None, output_callback=default_output_callback, input_for_memory_node_id=-1):
|
| 70 |
+
"""
|
| 71 |
+
agent run: parse intput -> get llm messages -> run LLM and parse output
|
| 72 |
+
input: str, user's new input, None means continue to run where it stopped
|
| 73 |
+
input_for_memory_node_id: int, -1 means input is not from memory, None means input new, otherwise input is for memory node
|
| 74 |
+
output_callback: async function, output_callback(content: str) -> None
|
| 75 |
+
"""
|
| 76 |
+
self.is_running = True
|
| 77 |
+
|
| 78 |
+
if input_for_memory_node_id == -1:
|
| 79 |
+
memory_node_id = self.memory.current_node.node_id if self.memory.current_node is not None else None
|
| 80 |
+
else:
|
| 81 |
+
memory_node_id = input_for_memory_node_id
|
| 82 |
+
input_node = self._insert_node(input, memory_node_id) if input is not None else None
|
| 83 |
+
|
| 84 |
+
# input interpreter
|
| 85 |
+
if input_node is not None:
|
| 86 |
+
input_content = input
|
| 87 |
+
input_stop = False
|
| 88 |
+
self.memory.set_current_node(input_node)
|
| 89 |
+
interpreter:Interpreter = None
|
| 90 |
+
for interpreter in self.interpreters:
|
| 91 |
+
if interpreter.input_match(input_content):
|
| 92 |
+
logging.info('interpreter: ' + interpreter.__class__.__name__)
|
| 93 |
+
# await output_callback('input parsing\n')
|
| 94 |
+
input_content, case_is_stop = await interpreter.input_parse(input_content)
|
| 95 |
+
if case_is_stop:
|
| 96 |
+
await output_callback(input_content)
|
| 97 |
+
input_stop = True
|
| 98 |
+
input_node.content = input_content
|
| 99 |
+
self.memory.update_node(input_node)
|
| 100 |
+
if input_stop:
|
| 101 |
+
self.memory.success_node(input_node)
|
| 102 |
+
self.is_running = False
|
| 103 |
+
return input_node.node_id
|
| 104 |
+
|
| 105 |
+
# execute todo node from memory
|
| 106 |
+
todo_node = self.memory.get_todo_node() or input_node
|
| 107 |
+
logging.debug(self.memory)
|
| 108 |
+
while todo_node is not None:
|
| 109 |
+
new_node, is_stop = await self._execute_node(todo_node, output_callback)
|
| 110 |
+
logging.debug(self.memory)
|
| 111 |
+
logging.debug(new_node)
|
| 112 |
+
logging.debug(is_stop)
|
| 113 |
+
if is_stop:
|
| 114 |
+
return new_node.node_id
|
| 115 |
+
todo_node = self.memory.get_todo_node()
|
| 116 |
+
await asyncio.sleep(0)
|
| 117 |
+
if self.stop_event.is_set():
|
| 118 |
+
self.is_running = False
|
| 119 |
+
return None
|
| 120 |
+
self.is_running = False
|
| 121 |
+
return None
|
| 122 |
+
|
| 123 |
+
def _insert_node(self, input, memory_node_id=None):
|
| 124 |
+
node = StackMemoryNode(role='user', action='input', content=input)
|
| 125 |
+
if memory_node_id is None:
|
| 126 |
+
logging.debug(self.memory)
|
| 127 |
+
self.memory.add_node(node)
|
| 128 |
+
else:
|
| 129 |
+
for_node = self.memory.get_node(memory_node_id)
|
| 130 |
+
self.memory.add_node_after(for_node, node)
|
| 131 |
+
self.memory.success_node(for_node)
|
| 132 |
+
return node
|
| 133 |
+
|
| 134 |
+
async def _execute_node(self, node, output_callback):
|
| 135 |
+
# construct system prompt
|
| 136 |
+
from GeneralAgent import skills
|
| 137 |
+
messages = self.memory.get_related_messages_for_node(node)
|
| 138 |
+
messages = skills.cut_messages(messages, 3000)
|
| 139 |
+
system_prompt = '\n\n'.join([await interpreter.prompt(messages) for interpreter in self.interpreters])
|
| 140 |
+
messages = [{'role': 'system', 'content': system_prompt}] + messages
|
| 141 |
+
|
| 142 |
+
# add answer node and set current node
|
| 143 |
+
answer_node = StackMemoryNode(role='system', action='answer', content='')
|
| 144 |
+
self.memory.add_node_after(node, answer_node)
|
| 145 |
+
self.memory.set_current_node(answer_node)
|
| 146 |
+
|
| 147 |
+
if node.action == 'plan':
|
| 148 |
+
await output_callback(f'\n[{node.content}]\n')
|
| 149 |
+
|
| 150 |
+
try:
|
| 151 |
+
result = ''
|
| 152 |
+
is_stop = False
|
| 153 |
+
is_break = False
|
| 154 |
+
in_parse_content = False
|
| 155 |
+
cache_tokens = []
|
| 156 |
+
response = skills.llm_inference(messages, model_type=self.model_type, stream=True)
|
| 157 |
+
for token in response:
|
| 158 |
+
if token is None: break
|
| 159 |
+
result += token
|
| 160 |
+
# print(token)
|
| 161 |
+
if self.hide_output_parse:
|
| 162 |
+
if not in_parse_content:
|
| 163 |
+
interpreter:Interpreter = None
|
| 164 |
+
for interpreter in self.interpreters:
|
| 165 |
+
is_start_matched, string_matched = interpreter.output_match_start(result)
|
| 166 |
+
if is_start_matched:
|
| 167 |
+
in_parse_content = True
|
| 168 |
+
# clear cache
|
| 169 |
+
cache_tokens.append(token)
|
| 170 |
+
left_count = len(string_matched)
|
| 171 |
+
while left_count > 0:
|
| 172 |
+
left_count -= len(cache_tokens[-1])
|
| 173 |
+
cache_tokens.remove(cache_tokens[-1])
|
| 174 |
+
while len(cache_tokens) > 0:
|
| 175 |
+
pop_token = cache_tokens.pop(0)
|
| 176 |
+
await output_callback(pop_token)
|
| 177 |
+
if not in_parse_content:
|
| 178 |
+
# cache token
|
| 179 |
+
cache_tokens.append(token)
|
| 180 |
+
if len(cache_tokens) > 5:
|
| 181 |
+
pop_token = cache_tokens.pop(0)
|
| 182 |
+
await output_callback(pop_token)
|
| 183 |
+
else:
|
| 184 |
+
await output_callback(token)
|
| 185 |
+
interpreter:Interpreter = None
|
| 186 |
+
for interpreter in self.interpreters:
|
| 187 |
+
if interpreter.output_match(result):
|
| 188 |
+
logging.info('interpreter: ' + interpreter.__class__.__name__)
|
| 189 |
+
output, is_stop = await interpreter.output_parse(result)
|
| 190 |
+
if interpreter.outptu_parse_done_recall is not None:
|
| 191 |
+
await interpreter.outptu_parse_done_recall()
|
| 192 |
+
if self.hide_output_parse:
|
| 193 |
+
is_matched, string_left = interpreter.output_match_end(result)
|
| 194 |
+
await output_callback(string_left)
|
| 195 |
+
result += '\n' + output.strip() + '\n'
|
| 196 |
+
if not self.hide_output_parse or is_stop:
|
| 197 |
+
await output_callback('\n' + output.strip() + '\n')
|
| 198 |
+
is_break = True
|
| 199 |
+
in_parse_content = False
|
| 200 |
+
break
|
| 201 |
+
if is_break:
|
| 202 |
+
break
|
| 203 |
+
while len(cache_tokens) > 0:
|
| 204 |
+
pop_token = cache_tokens.pop(0)
|
| 205 |
+
await output_callback(pop_token)
|
| 206 |
+
# await output_callback('\n')
|
| 207 |
+
# update current node and answer node
|
| 208 |
+
answer_node.content = result
|
| 209 |
+
self.memory.update_node(answer_node)
|
| 210 |
+
self.memory.success_node(node)
|
| 211 |
+
# llm run end with any interpreter, success the node
|
| 212 |
+
if not is_break:
|
| 213 |
+
self.memory.success_node(answer_node)
|
| 214 |
+
return answer_node, is_stop
|
| 215 |
+
except Exception as e:
|
| 216 |
+
# if fail, recover
|
| 217 |
+
logging.exception(e)
|
| 218 |
+
await output_callback(str(e))
|
| 219 |
+
self.memory.delete_node(answer_node)
|
| 220 |
+
self.memory.set_current_node(node)
|
| 221 |
+
return node, True
|
GeneralAgent/cli.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import asyncio
|
| 3 |
+
import datetime
|
| 4 |
+
import argparse
|
| 5 |
+
from GeneralAgent.agent import Agent
|
| 6 |
+
from GeneralAgent.utils import default_get_input, set_logging_level
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
async def _main(args):
|
| 10 |
+
if args.auto_run:
|
| 11 |
+
os.environ['AUTO_RUN'] = 'y'
|
| 12 |
+
else:
|
| 13 |
+
os.environ['AUTO_RUN'] = 'n'
|
| 14 |
+
set_logging_level(os.environ.get('LOG_LEVEL', 'ERROR'))
|
| 15 |
+
workspace = args.workspace
|
| 16 |
+
if args.new:
|
| 17 |
+
if os.path.exists(workspace):
|
| 18 |
+
workspace = workspace + '_' + datetime.datetime.now().strftime('%Y%m%d%H%M%S')
|
| 19 |
+
print('New workspace: ', workspace)
|
| 20 |
+
if not os.path.exists(workspace):
|
| 21 |
+
os.mkdir(workspace)
|
| 22 |
+
|
| 23 |
+
agent = Agent.default(workspace=workspace)
|
| 24 |
+
print('You can input multi lines, enter twice to end')
|
| 25 |
+
while True:
|
| 26 |
+
input_content = default_get_input()
|
| 27 |
+
print('[output]\n', end='', flush=True)
|
| 28 |
+
await agent.run(input_content)
|
| 29 |
+
|
| 30 |
+
def main():
|
| 31 |
+
parser = argparse.ArgumentParser(description='GeneralAgent CLI')
|
| 32 |
+
parser.add_argument('--workspace', default='./general_agent', help='Set workspace directory')
|
| 33 |
+
parser.add_argument('--new', action='store_true', help='Enable new workspace')
|
| 34 |
+
parser.add_argument('--auto_run', action='store_true', help='Auto run code without confirm')
|
| 35 |
+
args = parser.parse_args()
|
| 36 |
+
asyncio.run(_main(args))
|
| 37 |
+
|
| 38 |
+
if __name__ == '__main__':
|
| 39 |
+
main()
|
GeneralAgent/interpreter/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# import
|
| 2 |
+
from .interpreter import Interpreter
|
| 3 |
+
|
| 4 |
+
# role
|
| 5 |
+
from .role_interpreter import RoleInterpreter
|
| 6 |
+
|
| 7 |
+
# input
|
| 8 |
+
from .plan_interpreter import PlanInterpreter
|
| 9 |
+
|
| 10 |
+
# retrieve
|
| 11 |
+
from .embedding_retrieve_interpreter import EmbeddingRetrieveInterperter
|
| 12 |
+
from .link_retrieve_interpreter import LinkRetrieveInterperter
|
| 13 |
+
|
| 14 |
+
# output
|
| 15 |
+
from .applescript_interpreter import AppleScriptInterpreter
|
| 16 |
+
from .file_interpreter import FileInterpreter
|
| 17 |
+
from .python_interpreter import SyncPythonInterpreter
|
| 18 |
+
from .python_interpreter import AsyncPythonInterpreter
|
| 19 |
+
from .shell_interpreter import ShellInterpreter
|
| 20 |
+
from .ui_interpreter import UIInterpreter
|
| 21 |
+
class PythonInterpreter(AsyncPythonInterpreter):
|
| 22 |
+
pass
|
GeneralAgent/interpreter/applescript_interpreter.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from .interpreter import Interpreter
|
| 3 |
+
from GeneralAgent.utils import confirm_to_run
|
| 4 |
+
|
| 5 |
+
applescript_promt = """
|
| 6 |
+
# Run applescript
|
| 7 |
+
* Here are the commands
|
| 8 |
+
```applescript
|
| 9 |
+
<applescript_command>
|
| 10 |
+
```
|
| 11 |
+
* the command will be executed if in macOS computer.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
class AppleScriptInterpreter(Interpreter):
|
| 15 |
+
|
| 16 |
+
output_match_pattern = '```(\n)?applescript(.*?)\n```'
|
| 17 |
+
output_match_start_pattern = '```(\n)?applescript'
|
| 18 |
+
|
| 19 |
+
async def prompt(self, messages) -> str:
|
| 20 |
+
return applescript_promt
|
| 21 |
+
|
| 22 |
+
async def output_parse(self, string) -> (str, bool):
|
| 23 |
+
pattern = re.compile(self.output_match_pattern, re.DOTALL)
|
| 24 |
+
match = pattern.search(string)
|
| 25 |
+
assert match is not None
|
| 26 |
+
if confirm_to_run():
|
| 27 |
+
sys_out = self._run_applescript(match.group(2))
|
| 28 |
+
return sys_out.strip(), False
|
| 29 |
+
else:
|
| 30 |
+
return '', False
|
| 31 |
+
|
| 32 |
+
def _run_applescript(self, content):
|
| 33 |
+
content = content.replace('"', '\\"')
|
| 34 |
+
sys_out = ''
|
| 35 |
+
import subprocess
|
| 36 |
+
try:
|
| 37 |
+
p = subprocess.Popen('osascript -e "{}"'.format(content), shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
| 38 |
+
except:
|
| 39 |
+
pass
|
| 40 |
+
finally:
|
| 41 |
+
sys_out, err = p.communicate()
|
| 42 |
+
sys_out = sys_out.decode('utf-8')
|
| 43 |
+
sys_out = sys_out.strip()
|
| 44 |
+
if sys_out == '':
|
| 45 |
+
sys_out = 'run successfully'
|
| 46 |
+
return sys_out
|
GeneralAgent/interpreter/embedding_retrieve_interpreter.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# read the document and can retrieve the information
|
| 2 |
+
import re
|
| 3 |
+
from .interpreter import Interpreter
|
| 4 |
+
import chromadb
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
class EmbeddingRetrieveInterperter(Interpreter):
|
| 8 |
+
"""
|
| 9 |
+
EmbeddingRetrieveInterperter can retrieve the information from the memory by embedding.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
input_match_pattern = '```read\n(.*?)\n```'
|
| 13 |
+
|
| 14 |
+
def __init__(self, serialize_path='./read_data/', prompt_max_length=1000, useful_msg_count=2) -> None:
|
| 15 |
+
self.prompt_max_length = prompt_max_length
|
| 16 |
+
self.useful_msg_count = useful_msg_count
|
| 17 |
+
self.client = chromadb.PersistentClient(path=serialize_path)
|
| 18 |
+
self.collection = self.client.get_or_create_collection(name="read", metadata={"hnsw:space": "cosine"})
|
| 19 |
+
|
| 20 |
+
async def prompt(self, messages) -> str:
|
| 21 |
+
from GeneralAgent import skills
|
| 22 |
+
# when collection is empty, return empty string
|
| 23 |
+
if self.collection.count() == 0:
|
| 24 |
+
return ''
|
| 25 |
+
|
| 26 |
+
querys = []
|
| 27 |
+
for x in messages[-self.useful_msg_count:]:
|
| 28 |
+
querys += skills.split_text(x['content'], 200)
|
| 29 |
+
query_embeddings = skills.embedding_batch(querys)
|
| 30 |
+
result = self.collection.query(
|
| 31 |
+
query_embeddings=query_embeddings,
|
| 32 |
+
n_results=2,
|
| 33 |
+
)
|
| 34 |
+
# extract distances and documents
|
| 35 |
+
distances = [x for z in result['distances'] for x in z]
|
| 36 |
+
documents = [x for z in result['documents'] for x in z]
|
| 37 |
+
|
| 38 |
+
# sort distances and documents by distance
|
| 39 |
+
sorted_docs = sorted(list(zip(distances, documents)), key=lambda x: x[0])
|
| 40 |
+
|
| 41 |
+
# filter documents with distance < 100
|
| 42 |
+
documents = [x for d, x in sorted_docs if d < 100]
|
| 43 |
+
# texts token count should less than prompt_max_length
|
| 44 |
+
texts = []
|
| 45 |
+
texts_token_count = 0
|
| 46 |
+
for x in documents:
|
| 47 |
+
if texts_token_count + skills.string_token_count(x) > self.prompt_max_length:
|
| 48 |
+
break
|
| 49 |
+
texts.append(x)
|
| 50 |
+
return '\n'.join(texts)
|
| 51 |
+
|
| 52 |
+
async def input_parse(self, string) -> (str, bool):
|
| 53 |
+
from GeneralAgent import skills
|
| 54 |
+
information = []
|
| 55 |
+
pattern = re.compile(self.input_match_pattern, re.DOTALL)
|
| 56 |
+
match = pattern.search(string)
|
| 57 |
+
assert match is not None
|
| 58 |
+
file_paths = match.group(1).strip().split('\n')
|
| 59 |
+
for file_path in file_paths:
|
| 60 |
+
paragraphs = skills.split_text(skills.read_file_content(file_path), max_token=300)
|
| 61 |
+
if len(paragraphs) > 0:
|
| 62 |
+
information.append(f'The content of file {file_path} is: ' + '\n'.join(paragraphs)[:100] + '\n......')
|
| 63 |
+
embeddings = skills.embedding_batch(paragraphs)
|
| 64 |
+
# logging.debug(paragraphs[:2])
|
| 65 |
+
# logging.debug(embeddings[:2])
|
| 66 |
+
self.collection.add(
|
| 67 |
+
documents=paragraphs,
|
| 68 |
+
embeddings=embeddings,
|
| 69 |
+
metadatas=[{'file_path': file_path} for _ in paragraphs],
|
| 70 |
+
ids=[file_path+str(i) for i in range(len(paragraphs))],
|
| 71 |
+
)
|
| 72 |
+
stop = False
|
| 73 |
+
if string.replace(match.group(0), '').strip() == '':
|
| 74 |
+
stop = True
|
| 75 |
+
info = '\n'.join(information)
|
| 76 |
+
string = f'\n{string}\n```{info}```\n'
|
| 77 |
+
return string, stop
|
GeneralAgent/interpreter/file_interpreter.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re, os
|
| 2 |
+
import logging
|
| 3 |
+
from .interpreter import Interpreter
|
| 4 |
+
|
| 5 |
+
file_prompt = """
|
| 6 |
+
# For file operations, ALWAYS enclose your commands in triple backticks (```). Here are the commands:
|
| 7 |
+
|
| 8 |
+
1. Write:
|
| 9 |
+
```file
|
| 10 |
+
<file_path> write <start_line> <end_line> <<EOF
|
| 11 |
+
<content>
|
| 12 |
+
EOF
|
| 13 |
+
```
|
| 14 |
+
2. Read:
|
| 15 |
+
```file
|
| 16 |
+
<file_path> read <start_line> <end_line>
|
| 17 |
+
```
|
| 18 |
+
3. Delete:
|
| 19 |
+
```file
|
| 20 |
+
<file_path> delete <start_line> <end_line>
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
Line numbers start from 0, and -1 is the last line.
|
| 24 |
+
Read will print the content of the file with [line numbers] prefixed.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class FileInterpreter(Interpreter):
|
| 29 |
+
|
| 30 |
+
output_match_pattern = '```(\n)?file(\n| )?(.*?) (write|read|delete) (-?\d+) (-?\d+)(.*?)```'
|
| 31 |
+
|
| 32 |
+
async def prompt(self, messages) -> str:
|
| 33 |
+
return file_prompt
|
| 34 |
+
|
| 35 |
+
def _parse_commands(self, string):
|
| 36 |
+
match = re.search(self.output_match_pattern, string, re.DOTALL)
|
| 37 |
+
assert match is not None
|
| 38 |
+
file_path = match.group(3)
|
| 39 |
+
operation = match.group(4)
|
| 40 |
+
start_line = int(match.group(5))
|
| 41 |
+
end_line = int(match.group(6))
|
| 42 |
+
content = match.group(7).strip()
|
| 43 |
+
if content.startswith('<<EOF'):
|
| 44 |
+
content = content[5:].strip()
|
| 45 |
+
if content.endswith('EOF'):
|
| 46 |
+
content = content[:-3].strip()
|
| 47 |
+
return file_path, operation, start_line, end_line, content
|
| 48 |
+
|
| 49 |
+
async def output_parse(self, string) -> (str, bool):
|
| 50 |
+
logging.debug('FileInterpreter:parse called')
|
| 51 |
+
file_path, operation, start_line, end_line, content = self._parse_commands(string)
|
| 52 |
+
is_stop = True
|
| 53 |
+
if operation == 'write':
|
| 54 |
+
self._write_file(file_path, content, start_line, end_line)
|
| 55 |
+
return f'Content write to {file_path} successfully\n', is_stop
|
| 56 |
+
elif operation == 'delete':
|
| 57 |
+
self._delete_file(file_path, start_line, end_line)
|
| 58 |
+
return f'Delete lines of {file_path} successfully\n', is_stop
|
| 59 |
+
elif operation == 'read':
|
| 60 |
+
content = self._read_file(file_path, start_line, end_line)
|
| 61 |
+
return f'Read {file_path} succesfully, the content is below: \n\n```\n{content}\n```\n', is_stop
|
| 62 |
+
|
| 63 |
+
def _write_file(self, file_path, content, start_index, end_index):
|
| 64 |
+
# if .py file, remove ```python and ``` pair
|
| 65 |
+
if file_path.endswith('.py'):
|
| 66 |
+
content = content.replace('```python', '')
|
| 67 |
+
content = content.replace('```', '')
|
| 68 |
+
# file_path = os.path.join(self.workspace, file_path)
|
| 69 |
+
dir_path = os.path.dirname(file_path)
|
| 70 |
+
if not os.path.exists(dir_path):
|
| 71 |
+
os.makedirs(dir_path)
|
| 72 |
+
if not os.path.exists(file_path):
|
| 73 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
| 74 |
+
f.write('')
|
| 75 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 76 |
+
lines = f.readlines()
|
| 77 |
+
if start_index == -1:
|
| 78 |
+
start_index = len(lines)
|
| 79 |
+
if end_index == -1:
|
| 80 |
+
end_index = len(lines)
|
| 81 |
+
lines = lines[:start_index] + [content] + lines[end_index+1:]
|
| 82 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
| 83 |
+
f.writelines(lines)
|
| 84 |
+
|
| 85 |
+
def _delete_file(self, file_path, start_index, end_index):
|
| 86 |
+
# file_path = os.path.join(self.workspace, file_path)
|
| 87 |
+
if not os.path.exists(file_path):
|
| 88 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
| 89 |
+
f.write('')
|
| 90 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 91 |
+
lines = f.readlines()
|
| 92 |
+
if start_index == -1:
|
| 93 |
+
start_index = len(lines)
|
| 94 |
+
if end_index == -1:
|
| 95 |
+
end_index = len(lines)
|
| 96 |
+
lines = lines[:start_index] + lines[end_index+1:]
|
| 97 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
| 98 |
+
f.writelines(lines)
|
| 99 |
+
|
| 100 |
+
def _read_file(self, file_path, start_index, end_index):
|
| 101 |
+
# file_path = os.path.join(self.workspace, file_path)
|
| 102 |
+
if not os.path.exists(file_path):
|
| 103 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
| 104 |
+
f.write('')
|
| 105 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 106 |
+
lines = f.readlines()
|
| 107 |
+
if start_index == -1:
|
| 108 |
+
start_index = len(lines)
|
| 109 |
+
if end_index == -1:
|
| 110 |
+
end_index = len(lines)
|
| 111 |
+
content = ''
|
| 112 |
+
end_index = min(end_index + 1, len(lines))
|
| 113 |
+
for index in range(start_index, end_index):
|
| 114 |
+
new_add = f'[{index}]{lines[index]}'
|
| 115 |
+
if len(content + new_add) > 2000:
|
| 116 |
+
left_count = len(lines) - index
|
| 117 |
+
content += f'...\n[there are {left_count} lines left]'
|
| 118 |
+
break
|
| 119 |
+
content += new_add
|
| 120 |
+
return content.strip()
|
GeneralAgent/interpreter/interpreter.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Interpreter
|
| 2 |
+
import abc
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
class Interpreter(metaclass=abc.ABCMeta):
|
| 6 |
+
"""
|
| 7 |
+
Interpreter is the base class for all interpreters.
|
| 8 |
+
input_match_pattern is the pattern to match the LLM input string. for example ```tsx\n(.*?)\n```
|
| 9 |
+
output_match_pattern is the pattern to match the LLM ouput string. for example ```tsx\n(.*?)\n```
|
| 10 |
+
output_match_start_pattern is the pattern to match the start of the LLM output string. for example: ```tsx\n
|
| 11 |
+
outptu_parse_done_recall is the callback function to recall when the output_parse is done
|
| 12 |
+
"""
|
| 13 |
+
input_match_pattern = None
|
| 14 |
+
output_match_pattern = None
|
| 15 |
+
output_match_start_pattern = None
|
| 16 |
+
outptu_parse_done_recall = None
|
| 17 |
+
|
| 18 |
+
async def prompt(self, messages) -> str:
|
| 19 |
+
"""
|
| 20 |
+
:param messages: list of messages
|
| 21 |
+
:return: string
|
| 22 |
+
"""
|
| 23 |
+
return ''
|
| 24 |
+
|
| 25 |
+
def input_match(self, string) -> bool:
|
| 26 |
+
if self.input_match_pattern is None:
|
| 27 |
+
return False
|
| 28 |
+
match = re.compile(self.input_match_pattern, re.DOTALL).search(string)
|
| 29 |
+
if match is not None:
|
| 30 |
+
return True
|
| 31 |
+
else:
|
| 32 |
+
return False
|
| 33 |
+
|
| 34 |
+
def output_match(self, string) -> bool:
|
| 35 |
+
if self.output_match_pattern is None:
|
| 36 |
+
return False
|
| 37 |
+
match = re.compile(self.output_match_pattern, re.DOTALL).search(string)
|
| 38 |
+
if match is not None:
|
| 39 |
+
return True
|
| 40 |
+
else:
|
| 41 |
+
return False
|
| 42 |
+
|
| 43 |
+
def output_match_start(self, string) -> (bool, str):
|
| 44 |
+
"""
|
| 45 |
+
return is_match, string_matched
|
| 46 |
+
"""
|
| 47 |
+
if self.output_match_start_pattern is None:
|
| 48 |
+
return False, ''
|
| 49 |
+
match = re.compile(self.output_match_start_pattern, re.DOTALL).search(string)
|
| 50 |
+
if match is not None:
|
| 51 |
+
string_matched = string[match.start():]
|
| 52 |
+
return True, string_matched
|
| 53 |
+
else:
|
| 54 |
+
return False, ''
|
| 55 |
+
|
| 56 |
+
def output_match_end(self, string) -> (bool, str):
|
| 57 |
+
"""
|
| 58 |
+
return is_match, string_matched
|
| 59 |
+
"""
|
| 60 |
+
if self.output_match_pattern is None:
|
| 61 |
+
return False, ''
|
| 62 |
+
match = re.compile(self.output_match_pattern, re.DOTALL).search(string)
|
| 63 |
+
if match is not None:
|
| 64 |
+
string_left = string[match.end():]
|
| 65 |
+
return True, string_left
|
| 66 |
+
else:
|
| 67 |
+
return False, ''
|
| 68 |
+
|
| 69 |
+
async def input_parse(self, string) -> (str, bool):
|
| 70 |
+
"""
|
| 71 |
+
parse the input、output string, and return the output string and is_stop
|
| 72 |
+
"""
|
| 73 |
+
return '', False
|
| 74 |
+
|
| 75 |
+
async def output_parse(self, string) -> (str, bool):
|
| 76 |
+
"""
|
| 77 |
+
parse the input、output string, and return the output string and is_stop
|
| 78 |
+
"""
|
| 79 |
+
return '', False
|
| 80 |
+
|
GeneralAgent/interpreter/link_retrieve_interpreter.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# read the document and can retrieve the information
|
| 2 |
+
import re
|
| 3 |
+
from .interpreter import Interpreter
|
| 4 |
+
from GeneralAgent.memory import LinkMemory
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class LinkRetrieveInterperter(Interpreter):
|
| 8 |
+
"""
|
| 9 |
+
LinkRetrieveInterperter store and retrieve the information from the memory by link embed in the document. like I live in <<My Home>>.
|
| 10 |
+
LinkRetrieveInterperter handle input string like this:
|
| 11 |
+
```read
|
| 12 |
+
path/to/file1.pdf
|
| 13 |
+
path/to/file2.pdf
|
| 14 |
+
```
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
input_match_pattern = '```read\n(.*?)\n```'
|
| 18 |
+
|
| 19 |
+
def __init__(self, python_interpreter=None, sparks_dict_name='sparks'):
|
| 20 |
+
self.python_intrepreter = python_interpreter
|
| 21 |
+
self.sparks_dict_name = sparks_dict_name
|
| 22 |
+
self.link_memory = LinkMemory()
|
| 23 |
+
|
| 24 |
+
async def prompt(self, messages) -> str:
|
| 25 |
+
if self.link_memory.is_empty():
|
| 26 |
+
return None
|
| 27 |
+
else:
|
| 28 |
+
access_prompt = f"""
|
| 29 |
+
In Python, You can access the values of <<key>> in all documents through the dictionary {self.sparks_dict_name}, such as <<Hello world>>:
|
| 30 |
+
```
|
| 31 |
+
print({self.sparks_dict_name}['Hello world'])
|
| 32 |
+
```
|
| 33 |
+
"""
|
| 34 |
+
return await 'Background Information: \n' + self.link_memory.get_memory(messages) + access_prompt
|
| 35 |
+
|
| 36 |
+
async def input_parse(self, string) -> (str, bool):
|
| 37 |
+
from GeneralAgent import skills
|
| 38 |
+
pattern = re.compile(self.input_match_pattern, re.DOTALL)
|
| 39 |
+
match = pattern.search(string)
|
| 40 |
+
assert match is not None
|
| 41 |
+
file_paths = match.group(1).strip().split('\n')
|
| 42 |
+
result = ''
|
| 43 |
+
async def output_callback(token):
|
| 44 |
+
nonlocal result
|
| 45 |
+
if token is not None:
|
| 46 |
+
result += token
|
| 47 |
+
for file_path in file_paths:
|
| 48 |
+
content = skills.read_file_content(file_path)
|
| 49 |
+
await self.link_memory.add_memory(content, output_callback=output_callback)
|
| 50 |
+
self._update_python_variables()
|
| 51 |
+
return string + '\n' + result, True
|
| 52 |
+
|
| 53 |
+
def _update_python_variables(self):
|
| 54 |
+
if self.python_intrepreter is not None:
|
| 55 |
+
nodes = self.link_memory.concepts.values()
|
| 56 |
+
sparks_dict = dict(zip([node.key for node in nodes], [node.content for node in nodes]))
|
| 57 |
+
self.python_intrepreter.set_variable(self.sparks_dict_name, sparks_dict)
|
GeneralAgent/interpreter/plan_interpreter.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from collections import OrderedDict
|
| 3 |
+
from .interpreter import Interpreter
|
| 4 |
+
from GeneralAgent.memory import StackMemoryNode
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class PlanInterpreter(Interpreter):
|
| 8 |
+
"""
|
| 9 |
+
PlanInterperter is used to parse the plan structure.
|
| 10 |
+
PlanInterpreter handle the input string like this:
|
| 11 |
+
```runplan
|
| 12 |
+
section1
|
| 13 |
+
section1.1
|
| 14 |
+
```
|
| 15 |
+
Note: this is only for StackAgent and StackMemory.
|
| 16 |
+
"""
|
| 17 |
+
input_match_pattern = '```runplan(.*?)?\n(.*?)\n```'
|
| 18 |
+
|
| 19 |
+
def __init__(self, memory, max_plan_depth=4) -> None:
|
| 20 |
+
self.memory = memory
|
| 21 |
+
self.max_plan_depth = max_plan_depth
|
| 22 |
+
|
| 23 |
+
async def prompt(self, messages) -> str:
|
| 24 |
+
return ''
|
| 25 |
+
|
| 26 |
+
async def input_parse(self, string) -> (str, bool):
|
| 27 |
+
pattern = re.compile(self.input_match_pattern, re.DOTALL)
|
| 28 |
+
match = pattern.search(string)
|
| 29 |
+
assert match is not None
|
| 30 |
+
prefix = match.group(1).strip()
|
| 31 |
+
structure_data = match.group(2).strip()
|
| 32 |
+
plan_dict = self._structure_plan(structure_data)
|
| 33 |
+
current_node = self.memory.current_node
|
| 34 |
+
self._add_plans_for_node(current_node, plan_dict, prefix)
|
| 35 |
+
return string, False
|
| 36 |
+
|
| 37 |
+
def _add_plans_for_node(self, node:StackMemoryNode, plan_dict, prefix):
|
| 38 |
+
if self.memory.get_node_level(node) >= self.max_plan_depth:
|
| 39 |
+
return
|
| 40 |
+
for k, v in plan_dict.items():
|
| 41 |
+
new_node = StackMemoryNode(role='system', action='plan', content=k.strip(), prefix=prefix)
|
| 42 |
+
self.memory.add_node_in(node, new_node)
|
| 43 |
+
if len(v) > 0:
|
| 44 |
+
self._add_plans_for_node(new_node, v, prefix)
|
| 45 |
+
|
| 46 |
+
@classmethod
|
| 47 |
+
def _structure_plan(cls, data):
|
| 48 |
+
structured_data = OrderedDict()
|
| 49 |
+
current_section = [structured_data]
|
| 50 |
+
for line in data.split('\n'):
|
| 51 |
+
if not line.strip():
|
| 52 |
+
continue
|
| 53 |
+
depth = line.count(' ')
|
| 54 |
+
section = line.strip()
|
| 55 |
+
while depth < len(current_section) - 1:
|
| 56 |
+
current_section.pop()
|
| 57 |
+
current_section[-1][section] = OrderedDict()
|
| 58 |
+
current_section.append(current_section[-1][section])
|
| 59 |
+
return structured_data
|
GeneralAgent/interpreter/python_interpreter.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re, io, os, sys
|
| 2 |
+
import pickle
|
| 3 |
+
import logging
|
| 4 |
+
from jinja2 import Template
|
| 5 |
+
from .interpreter import Interpreter
|
| 6 |
+
from GeneralAgent.utils import confirm_to_run
|
| 7 |
+
from GeneralAgent import skills
|
| 8 |
+
import asyncio
|
| 9 |
+
|
| 10 |
+
default_import_code = """
|
| 11 |
+
import os, sys, math
|
| 12 |
+
from GeneralAgent import skills
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
# default_libs = ' '.join(["requests", "tinydb", "openai", "jinja2", "numpy", "bs4", "playwright", "retrying", "pymupdf", "python-pptx", "python-docx", "yfinance"])
|
| 16 |
+
# default_libs = skills.get_current_env_python_libs()
|
| 17 |
+
default_libs = ''
|
| 18 |
+
|
| 19 |
+
# from GeneralAgent.tools import Tools
|
| 20 |
+
|
| 21 |
+
class SyncPythonInterpreter(Interpreter):
|
| 22 |
+
"""
|
| 23 |
+
Sync Python Interpreter: run python code in the interpreter. Not same namespace with the agent & Can Only run synchronous code
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
output_match_start_pattern = '```python\n'
|
| 27 |
+
output_match_pattern = '```python\n(.*?)\n```'
|
| 28 |
+
|
| 29 |
+
python_prompt_template = """
|
| 30 |
+
# Run python
|
| 31 |
+
* format is : ```python\\nthe_code\\n```
|
| 32 |
+
* the code will be executed
|
| 33 |
+
* python version is {{python_version}}
|
| 34 |
+
* only write synchronous code
|
| 35 |
+
* The output display should be limited in length and should be truncated when displaying characters whose length is unknown. for example: print(a[:100])
|
| 36 |
+
* * Pickleable objects can be shared between different codes and variables
|
| 37 |
+
* Available libraries: {{python_libs}}
|
| 38 |
+
* The following functions can be used in code (already implemented and imported for you):
|
| 39 |
+
```
|
| 40 |
+
{{python_funcs}}
|
| 41 |
+
```
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
function_tools = []
|
| 45 |
+
|
| 46 |
+
def __init__(self,
|
| 47 |
+
serialize_path:str=None,
|
| 48 |
+
libs: str=default_libs,
|
| 49 |
+
import_code:str=None,
|
| 50 |
+
prompt_append='',
|
| 51 |
+
stop_wrong_count = 2
|
| 52 |
+
):
|
| 53 |
+
"""
|
| 54 |
+
Args:
|
| 55 |
+
serialize_path (str): path to save the global variables, default None, which means not save, like './serialized.bin'
|
| 56 |
+
libs ([str], optional): libraries can be to used. Defaults to skills.get_current_env_python_libs()
|
| 57 |
+
import_code (str, optional): code to import. The tools used should be imported. Defaults to default_import_code.
|
| 58 |
+
prompt_append: append to the prompt, custom prompt can be added here
|
| 59 |
+
stop_wrong_count: stop running when the code is wrong for stop_wrong_count times
|
| 60 |
+
"""
|
| 61 |
+
from GeneralAgent import skills
|
| 62 |
+
self.globals = {} # global variables shared by all code
|
| 63 |
+
self.python_libs = libs
|
| 64 |
+
self.import_code = import_code or default_import_code
|
| 65 |
+
self.serialize_path = serialize_path
|
| 66 |
+
self.prompt_append = prompt_append
|
| 67 |
+
# self.tools = tools or Tools([])
|
| 68 |
+
self.globals = self.load()
|
| 69 |
+
# count the number of times the code is wrong, and stop running when it reaches the threshold
|
| 70 |
+
self.run_wrong_count = 0
|
| 71 |
+
self.stop_wrong_count = stop_wrong_count
|
| 72 |
+
|
| 73 |
+
def load(self):
|
| 74 |
+
if self.serialize_path is None:
|
| 75 |
+
return {}
|
| 76 |
+
if os.path.exists(self.serialize_path):
|
| 77 |
+
with open(self.serialize_path, 'rb') as f:
|
| 78 |
+
data = pickle.loads(f.read())
|
| 79 |
+
return data['globals']
|
| 80 |
+
return {}
|
| 81 |
+
|
| 82 |
+
async def prompt(self, messages) -> str:
|
| 83 |
+
from GeneralAgent import skills
|
| 84 |
+
funtions = '\n\n'.join([skills.get_function_signature(x) for x in self.function_tools])
|
| 85 |
+
variables = {
|
| 86 |
+
'python_libs': self.python_libs,
|
| 87 |
+
'python_funcs': funtions,
|
| 88 |
+
'python_version': skills.get_python_version()
|
| 89 |
+
}
|
| 90 |
+
return Template(self.python_prompt_template).render(**variables) + self.prompt_append
|
| 91 |
+
|
| 92 |
+
def save(self):
|
| 93 |
+
if self.serialize_path is None:
|
| 94 |
+
return
|
| 95 |
+
self._remove_unpickleable()
|
| 96 |
+
# save
|
| 97 |
+
with open(self.serialize_path, 'wb') as f:
|
| 98 |
+
data = {'globals': self.globals}
|
| 99 |
+
f.write(pickle.dumps(data))
|
| 100 |
+
|
| 101 |
+
def _remove_unpickleable(self):
|
| 102 |
+
if '__builtins__' in self.globals:
|
| 103 |
+
self.globals.__delitem__('__builtins__')
|
| 104 |
+
keys = list(self.globals.keys())
|
| 105 |
+
for key in keys:
|
| 106 |
+
try:
|
| 107 |
+
pickle.dumps(self.globals[key])
|
| 108 |
+
except Exception as e:
|
| 109 |
+
self.globals.__delitem__(key)
|
| 110 |
+
|
| 111 |
+
async def output_parse(self, string) -> (str, bool):
|
| 112 |
+
sys_out = ''
|
| 113 |
+
pattern = re.compile(self.output_match_pattern, re.DOTALL)
|
| 114 |
+
match = pattern.search(string)
|
| 115 |
+
assert match is not None
|
| 116 |
+
if confirm_to_run():
|
| 117 |
+
sys_out, stop = await self.run_code(match.group(1))
|
| 118 |
+
result = 'python runs result:\n' + sys_out.strip()
|
| 119 |
+
return result, stop
|
| 120 |
+
else:
|
| 121 |
+
return '', False
|
| 122 |
+
|
| 123 |
+
async def run_code(self, code):
|
| 124 |
+
stop = False
|
| 125 |
+
code = self.add_print(code)
|
| 126 |
+
code = self.import_code + '\n' + code
|
| 127 |
+
globals_backup = self.load()
|
| 128 |
+
logging.debug(code)
|
| 129 |
+
sys_stdout = ''
|
| 130 |
+
output = io.StringIO()
|
| 131 |
+
sys.stdout = output
|
| 132 |
+
success = False
|
| 133 |
+
try:
|
| 134 |
+
exec(code, self.globals)
|
| 135 |
+
success = True
|
| 136 |
+
self.run_wrong_count = 0
|
| 137 |
+
except Exception as e:
|
| 138 |
+
import traceback
|
| 139 |
+
sys_stdout += traceback.format_exc()
|
| 140 |
+
self.globals = globals_backup
|
| 141 |
+
self.run_wrong_count += 1
|
| 142 |
+
if self.run_wrong_count >= self.stop_wrong_count:
|
| 143 |
+
stop = True
|
| 144 |
+
finally:
|
| 145 |
+
sys_stdout += output.getvalue()
|
| 146 |
+
sys.stdout = sys.__stdout__
|
| 147 |
+
if success:
|
| 148 |
+
self.save()
|
| 149 |
+
sys_stdout = sys_stdout.strip()
|
| 150 |
+
if sys_stdout == '':
|
| 151 |
+
sys_stdout = 'run successfully'
|
| 152 |
+
return sys_stdout, stop
|
| 153 |
+
|
| 154 |
+
def get_variable(self, name):
|
| 155 |
+
if name in self.globals:
|
| 156 |
+
return self.globals[name]
|
| 157 |
+
else:
|
| 158 |
+
logging.warning(f"Variable {name} not found")
|
| 159 |
+
return None
|
| 160 |
+
|
| 161 |
+
def set_variable(self, name, value):
|
| 162 |
+
self.globals[name] = value
|
| 163 |
+
self.save()
|
| 164 |
+
|
| 165 |
+
@classmethod
|
| 166 |
+
def add_print_old(cls, code_string):
|
| 167 |
+
pattern = r'^(\s*)(\w+)(\s*)$'
|
| 168 |
+
lines = code_string.split('\n')
|
| 169 |
+
for i, line in enumerate(lines):
|
| 170 |
+
keywords = ['False' 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']
|
| 171 |
+
if line.strip() in keywords:
|
| 172 |
+
continue
|
| 173 |
+
match = re.match(pattern, line)
|
| 174 |
+
if match:
|
| 175 |
+
lines[i] = f'{match.group(1)}print({match.group(2)}){match.group(3)}'
|
| 176 |
+
return '\n'.join(lines)
|
| 177 |
+
|
| 178 |
+
@classmethod
|
| 179 |
+
def add_print(cls, code_string):
|
| 180 |
+
from GeneralAgent import skills
|
| 181 |
+
code = code_string.strip()
|
| 182 |
+
lines = code.split('\n')
|
| 183 |
+
if len(lines) > 0:
|
| 184 |
+
last_line = lines[-1]
|
| 185 |
+
if skills.python_line_is_variable_expression(last_line):
|
| 186 |
+
last_line = f'print({last_line})'
|
| 187 |
+
lines[-1] = last_line
|
| 188 |
+
return '\n'.join(lines)
|
| 189 |
+
|
| 190 |
+
def _remove_unpickleable(namespace):
|
| 191 |
+
import pickle
|
| 192 |
+
if '__builtins__' in namespace:
|
| 193 |
+
namespace.__delitem__('__builtins__')
|
| 194 |
+
keys = list(namespace.keys())
|
| 195 |
+
for key in keys:
|
| 196 |
+
try:
|
| 197 |
+
pickle.dumps(namespace[key])
|
| 198 |
+
except Exception as e:
|
| 199 |
+
namespace.__delitem__(key)
|
| 200 |
+
return namespace
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def code_wrap(code, namespace):
|
| 204 |
+
lines = code.split('\n')
|
| 205 |
+
code = '\n '.join(lines)
|
| 206 |
+
variables = '\n '.join([f'{name} = globals()[\'{name}\']' for name, value in namespace.items()])
|
| 207 |
+
content = f"""
|
| 208 |
+
import asyncio
|
| 209 |
+
|
| 210 |
+
def _remove_unpickleable(namespace):
|
| 211 |
+
import pickle
|
| 212 |
+
if '__builtins__' in namespace:
|
| 213 |
+
namespace.__delitem__('__builtins__')
|
| 214 |
+
keys = list(namespace.keys())
|
| 215 |
+
for key in keys:
|
| 216 |
+
try:
|
| 217 |
+
pickle.dumps(namespace[key])
|
| 218 |
+
except Exception as e:
|
| 219 |
+
namespace.__delitem__(key)
|
| 220 |
+
for name in ['__name', '__value', '__namespace']:
|
| 221 |
+
if name in namespace:
|
| 222 |
+
namespace.__delitem__(name)
|
| 223 |
+
return namespace
|
| 224 |
+
|
| 225 |
+
__namespace = None
|
| 226 |
+
|
| 227 |
+
async def __main():
|
| 228 |
+
{variables}
|
| 229 |
+
{code}
|
| 230 |
+
global __namespace
|
| 231 |
+
__namespace = _remove_unpickleable(locals().copy())
|
| 232 |
+
"""
|
| 233 |
+
# print('----------<code>--------')
|
| 234 |
+
# print(content)
|
| 235 |
+
# print('----------</code>--------')
|
| 236 |
+
return content
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
class AsyncPythonInterpreter(SyncPythonInterpreter):
|
| 240 |
+
"""
|
| 241 |
+
Sync Python Interpreter: run python code in the interpreter. Same namespace with the agent & Can run async code
|
| 242 |
+
"""
|
| 243 |
+
|
| 244 |
+
python_prompt_template = """
|
| 245 |
+
# Run python
|
| 246 |
+
- format is : ```python\\nthe_code\\n```
|
| 247 |
+
- the code will be executed
|
| 248 |
+
- python version is {{python_version}}
|
| 249 |
+
- Pickleable objects can be shared between different codes and variables
|
| 250 |
+
- The output display should be limited in length and should be truncated when displaying characters whose length is unknown. for example: print(a[:100])
|
| 251 |
+
- Available libraries: {{python_libs}}
|
| 252 |
+
Complete the entire process in one code instead of writing multiple codes to run step by step. For example, the following code is allowed:
|
| 253 |
+
```python
|
| 254 |
+
# step 1
|
| 255 |
+
a = fun1(xx)
|
| 256 |
+
# step 2
|
| 257 |
+
c = fun2(a)
|
| 258 |
+
# step 3
|
| 259 |
+
d = fun3(c)
|
| 260 |
+
...
|
| 261 |
+
```
|
| 262 |
+
- The following functions can be used in code (already implemented and imported for you):
|
| 263 |
+
```
|
| 264 |
+
{{python_funcs}}
|
| 265 |
+
```
|
| 266 |
+
"""
|
| 267 |
+
|
| 268 |
+
async def run_code(self, code):
|
| 269 |
+
stop = False
|
| 270 |
+
code = self.add_print(code)
|
| 271 |
+
code = code_wrap(code, self.globals)
|
| 272 |
+
code = self.import_code + '\n' + code
|
| 273 |
+
# print('hello')
|
| 274 |
+
# print(code)
|
| 275 |
+
# print(self.function_tools)
|
| 276 |
+
globals_backup = self.load()
|
| 277 |
+
logging.debug(code)
|
| 278 |
+
sys_stdout = ''
|
| 279 |
+
output = io.StringIO()
|
| 280 |
+
sys.stdout = output
|
| 281 |
+
success = False
|
| 282 |
+
try:
|
| 283 |
+
# exec(code, self.globals)
|
| 284 |
+
# run async python code
|
| 285 |
+
local_vars = self.globals
|
| 286 |
+
# register functions
|
| 287 |
+
for fun in self.function_tools:
|
| 288 |
+
local_vars[fun.__name__] = fun
|
| 289 |
+
# print(local_vars)
|
| 290 |
+
exec(code, local_vars, local_vars)
|
| 291 |
+
main_function = local_vars['__main']
|
| 292 |
+
await asyncio.create_task(main_function())
|
| 293 |
+
local_vars = _remove_unpickleable(local_vars)
|
| 294 |
+
local_vars = local_vars['__namespace']
|
| 295 |
+
# remove functions
|
| 296 |
+
for fun in self.function_tools:
|
| 297 |
+
if fun.__name__ in local_vars:
|
| 298 |
+
local_vars.__delitem__(fun.__name__)
|
| 299 |
+
self.globals = local_vars
|
| 300 |
+
|
| 301 |
+
success = True
|
| 302 |
+
self.run_wrong_count = 0
|
| 303 |
+
except Exception as e:
|
| 304 |
+
import traceback
|
| 305 |
+
sys_stdout += traceback.format_exc()
|
| 306 |
+
self.globals = globals_backup
|
| 307 |
+
logging.exception((e))
|
| 308 |
+
self.run_wrong_count += 1
|
| 309 |
+
if self.run_wrong_count >= self.stop_wrong_count:
|
| 310 |
+
stop = True
|
| 311 |
+
finally:
|
| 312 |
+
sys_stdout += output.getvalue()
|
| 313 |
+
sys.stdout = sys.__stdout__
|
| 314 |
+
if success:
|
| 315 |
+
self.save()
|
| 316 |
+
sys_stdout = sys_stdout.strip()
|
| 317 |
+
if sys_stdout == '':
|
| 318 |
+
sys_stdout = 'run successfully'
|
| 319 |
+
return sys_stdout, stop
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
# class PythonInterpreter(AsyncPythonInterpreter):
|
| 323 |
+
# """
|
| 324 |
+
# Sync Python Interpreter: run python code in the interpreter. Same namespace with the agent & Can run async code
|
| 325 |
+
# """
|
| 326 |
+
# pass
|
GeneralAgent/interpreter/role_interpreter.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import datetime
|
| 3 |
+
import platform
|
| 4 |
+
from jinja2 import Template
|
| 5 |
+
from .interpreter import Interpreter
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class RoleInterpreter(Interpreter):
|
| 9 |
+
"""
|
| 10 |
+
RoleInterpreter, a interpreter that can change the role of the agent.
|
| 11 |
+
Note: This should be the first interpreter in the agent.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
system_prompt_template = \
|
| 15 |
+
"""
|
| 16 |
+
Now: {{now}}
|
| 17 |
+
You are GeneralAgent, a agent on the {{os_version}} computer to help the user solve the problem.
|
| 18 |
+
Remember, you can control the computer and access the internet.
|
| 19 |
+
Reponse message in markdown format to user. for example file a.txt, you should reponse [title](a.txt)
|
| 20 |
+
You can use the following skills (start with # ) to help you solve the problem directly without explain, without ask for permission.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def __init__(self, system_prompt=None) -> None:
|
| 24 |
+
from GeneralAgent import skills
|
| 25 |
+
self.os_version = skills.get_os_version()
|
| 26 |
+
self.system_prompt = system_prompt
|
| 27 |
+
|
| 28 |
+
async def prompt(self, messages) -> str:
|
| 29 |
+
if self.system_prompt is not None:
|
| 30 |
+
return self.system_prompt
|
| 31 |
+
if os.environ.get('LLM_CACHE', 'no') in ['yes', 'y', 'YES']:
|
| 32 |
+
now = '2023-09-27 00:00:00'
|
| 33 |
+
else:
|
| 34 |
+
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
| 35 |
+
data = {
|
| 36 |
+
'now': now,
|
| 37 |
+
'os_version': self.os_version
|
| 38 |
+
}
|
| 39 |
+
the_prompt = Template(self.system_prompt_template).render(**data)
|
| 40 |
+
return the_prompt
|
GeneralAgent/interpreter/shell_interpreter.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from .interpreter import Interpreter
|
| 3 |
+
from GeneralAgent.utils import confirm_to_run
|
| 4 |
+
|
| 5 |
+
shell_prompt = """
|
| 6 |
+
# Run shell
|
| 7 |
+
* format is : ```shell\\nthe_command\\n```
|
| 8 |
+
* the command will be executed
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
class ShellInterpreter(Interpreter):
|
| 12 |
+
|
| 13 |
+
output_match_start_pattern = '```shell\n'
|
| 14 |
+
output_match_pattern = '```shell\n(.*?)\n```'
|
| 15 |
+
|
| 16 |
+
def __init__(self, workspace='./') -> None:
|
| 17 |
+
self.workspace = workspace
|
| 18 |
+
|
| 19 |
+
async def prompt(self, messages) -> str:
|
| 20 |
+
return shell_prompt
|
| 21 |
+
|
| 22 |
+
async def output_parse(self, string) -> (str, bool):
|
| 23 |
+
pattern = re.compile(self.output_match_pattern, re.DOTALL)
|
| 24 |
+
match = pattern.search(string)
|
| 25 |
+
assert match is not None
|
| 26 |
+
if confirm_to_run():
|
| 27 |
+
output = self._run_bash(match.group(1))
|
| 28 |
+
return output.strip(), False
|
| 29 |
+
else:
|
| 30 |
+
return '', False
|
| 31 |
+
|
| 32 |
+
def _run_bash(self, content):
|
| 33 |
+
sys_out = ''
|
| 34 |
+
import subprocess
|
| 35 |
+
if 'python ' in content:
|
| 36 |
+
content = content.replace('python ', 'python3 ')
|
| 37 |
+
try:
|
| 38 |
+
p = subprocess.Popen(content, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
| 39 |
+
except:
|
| 40 |
+
pass
|
| 41 |
+
finally:
|
| 42 |
+
sys_out, err = p.communicate()
|
| 43 |
+
sys_out = sys_out.decode('utf-8')
|
| 44 |
+
return sys_out
|
GeneralAgent/interpreter/ui_interpreter.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from .interpreter import Interpreter
|
| 3 |
+
import asyncio
|
| 4 |
+
|
| 5 |
+
class UIInterpreter(Interpreter):
|
| 6 |
+
|
| 7 |
+
output_match_start_pattern = '```tsx\n'
|
| 8 |
+
output_match_pattern = '```tsx\n(.*?)\n```'
|
| 9 |
+
|
| 10 |
+
ui_prompt = """
|
| 11 |
+
# Send UI to user
|
| 12 |
+
Use the following tsx architecture to write a React component. The component will be compiled into a UI and sent to the user. The user's input can be sent to you through the save_data function.
|
| 13 |
+
```tsx
|
| 14 |
+
const React = (window as any).React;
|
| 15 |
+
const antd = (window as any).antd;
|
| 16 |
+
|
| 17 |
+
const [Form, Input, Button] = [antd.Form, antd.Input, antd.Button];
|
| 18 |
+
|
| 19 |
+
const LibTemplate = ({save_data}: {save_data: (data:any)=>void}) => {
|
| 20 |
+
// use save_data to save the data
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export default LibTemplate;
|
| 24 |
+
```
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
def __init__(self, send_ui, output_callback, workspace=None) -> None:
|
| 28 |
+
"""
|
| 29 |
+
:param send_ui: the async function to send ui to user
|
| 30 |
+
:param workspace: workspace for the interpreter
|
| 31 |
+
"""
|
| 32 |
+
self.send_ui = send_ui
|
| 33 |
+
self.output_callback = output_callback
|
| 34 |
+
self.workspace = workspace
|
| 35 |
+
|
| 36 |
+
async def prompt(self, messages) -> str:
|
| 37 |
+
return self.ui_prompt
|
| 38 |
+
|
| 39 |
+
async def output_parse(self, string) -> (str, bool):
|
| 40 |
+
from GeneralAgent import skills
|
| 41 |
+
pattern = re.compile(self.output_match_pattern, re.DOTALL)
|
| 42 |
+
match = pattern.search(string)
|
| 43 |
+
assert match is not None
|
| 44 |
+
code = match.group(1)
|
| 45 |
+
lib_name, js_path = skills.parse_tsx_to_ui(code, save_dir=self.workspace)
|
| 46 |
+
# Terminate the output callback
|
| 47 |
+
await self.output_callback(None)
|
| 48 |
+
# Send UI to user
|
| 49 |
+
await self.send_ui(lib_name, js_path)
|
| 50 |
+
print('Send UI to user successfuly.')
|
| 51 |
+
return '', True
|
GeneralAgent/memory/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# import
|
| 2 |
+
from .normal_memory import NormalMemory
|
| 3 |
+
from .stack_memory import StackMemory, StackMemoryNode
|
| 4 |
+
from .link_memory import LinkMemory, LinkMemoryNode
|
GeneralAgent/memory/link_memory.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from typing import List
|
| 3 |
+
from tinydb import TinyDB, Query
|
| 4 |
+
import asyncio
|
| 5 |
+
|
| 6 |
+
@dataclass
|
| 7 |
+
class LinkMemoryNode:
|
| 8 |
+
key: str
|
| 9 |
+
content: str
|
| 10 |
+
childrens: List[str] = None
|
| 11 |
+
parents: List[str] = None
|
| 12 |
+
|
| 13 |
+
def __post_init__(self):
|
| 14 |
+
self.childrens = self.childrens if self.childrens else []
|
| 15 |
+
self.parents = self.parents if self.parents else []
|
| 16 |
+
|
| 17 |
+
def __str__(self):
|
| 18 |
+
return f'<<{self.key}>>\n{self.content}'
|
| 19 |
+
|
| 20 |
+
def __repr__(self):
|
| 21 |
+
return str(self)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
async def summarize_and_segment(text, output_callback=None):
|
| 25 |
+
from GeneralAgent import skills
|
| 26 |
+
summary = await skills.summarize_text(text)
|
| 27 |
+
if output_callback is not None:
|
| 28 |
+
await output_callback(f'Summary: {summary}\n')
|
| 29 |
+
segments = await skills.segment_text(text)
|
| 30 |
+
if output_callback is not None:
|
| 31 |
+
for key in segments:
|
| 32 |
+
await output_callback(f'<<{key}>>\n')
|
| 33 |
+
return summary, segments
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class LinkMemory():
|
| 37 |
+
def __init__(self, serialize_path='./link_memory.json', short_memory_limit=2000) -> None:
|
| 38 |
+
self.serialize_path = serialize_path
|
| 39 |
+
self.short_memory_limit = short_memory_limit
|
| 40 |
+
self.db = TinyDB(serialize_path)
|
| 41 |
+
nodes = [LinkMemoryNode(**x) for x in self.db.all()]
|
| 42 |
+
self.concepts = dict(zip([node.key for node in nodes], nodes))
|
| 43 |
+
self.short_memory = ''
|
| 44 |
+
self._load_short_memory()
|
| 45 |
+
|
| 46 |
+
def is_empty(self):
|
| 47 |
+
return len(self.concepts) == 0
|
| 48 |
+
|
| 49 |
+
async def add_memory(self, content, output_callback=None):
|
| 50 |
+
from GeneralAgent import skills
|
| 51 |
+
# await self._oncurrent_summarize_content(content, output_callback)
|
| 52 |
+
await self._summarize_content(content, output_callback)
|
| 53 |
+
while skills.string_token_count(self.short_memory) > self.short_memory_limit:
|
| 54 |
+
content = self.short_memory
|
| 55 |
+
self.short_memory = ''
|
| 56 |
+
# await self._oncurrent_summarize_content(content, output_callback)
|
| 57 |
+
await self._summarize_content(content, output_callback)
|
| 58 |
+
|
| 59 |
+
async def get_memory(self, messages=None):
|
| 60 |
+
from GeneralAgent import skills
|
| 61 |
+
if len(self.concepts) == 0:
|
| 62 |
+
return ''
|
| 63 |
+
if messages is None:
|
| 64 |
+
return self.short_memory
|
| 65 |
+
else:
|
| 66 |
+
messages = skills.cut_messages(messages, 1000)
|
| 67 |
+
xx = self.short_memory.split('\n')
|
| 68 |
+
background = '\n'.join([f'#{line} {xx[line]}' for line in range(len(xx))])
|
| 69 |
+
task = '\n'.join([f'{x["role"]}: {x["content"]}' for x in messages])
|
| 70 |
+
info = await skills.extract_info(background, task)
|
| 71 |
+
line_numbers, keys = skills.parse_extract_info(info)
|
| 72 |
+
result = []
|
| 73 |
+
for line_number in line_numbers:
|
| 74 |
+
if line_number < len(xx) and line_number >= 0:
|
| 75 |
+
result.append(xx[line_number])
|
| 76 |
+
for key in keys:
|
| 77 |
+
if key in self.concepts:
|
| 78 |
+
result.append(f'{key}\n{self.concepts[key]}\n')
|
| 79 |
+
return '\n'.join(result)
|
| 80 |
+
|
| 81 |
+
def _load_short_memory(self):
|
| 82 |
+
short_memorys = self.db.table('short_memory').all()
|
| 83 |
+
self.short_memory = '' if len(short_memorys) == 0 else short_memorys[0]['content']
|
| 84 |
+
|
| 85 |
+
def _save_short_memory(self):
|
| 86 |
+
self.db.table('short_memory').truncate()
|
| 87 |
+
self.db.table('short_memory').insert({'content': self.short_memory})
|
| 88 |
+
|
| 89 |
+
async def _oncurrent_summarize_content(self, input):
|
| 90 |
+
from GeneralAgent import skills
|
| 91 |
+
inputs = skills.split_text(input, max_token=3000)
|
| 92 |
+
print('splited count: ', len(inputs))
|
| 93 |
+
coroutines = [summarize_and_segment(x) for x in inputs]
|
| 94 |
+
results = await asyncio.gather(*coroutines)
|
| 95 |
+
for summary, nodes in results:
|
| 96 |
+
new_nodes = {}
|
| 97 |
+
for key in nodes:
|
| 98 |
+
new_key = self._add_node(key, nodes[key])
|
| 99 |
+
new_nodes[new_key] = nodes[key]
|
| 100 |
+
self.short_memory += '\n' + summary + ' Detail in ' + ', '.join([f'<<{key}>>' for key in new_nodes])
|
| 101 |
+
self.short_memory = self.short_memory.strip()
|
| 102 |
+
self._save_short_memory()
|
| 103 |
+
|
| 104 |
+
async def _summarize_content(self, input, output_callback=None):
|
| 105 |
+
from GeneralAgent import skills
|
| 106 |
+
inputs = skills.split_text(input, max_token=3000)
|
| 107 |
+
for text in inputs:
|
| 108 |
+
summary, nodes = await summarize_and_segment(text, output_callback)
|
| 109 |
+
new_nodes = {}
|
| 110 |
+
for key in nodes:
|
| 111 |
+
new_key = self._add_node(key, nodes[key])
|
| 112 |
+
new_nodes[new_key] = nodes[key]
|
| 113 |
+
self.short_memory += '\n' + summary + ' Detail in ' + ', '.join([f'<<{key}>>' for key in new_nodes])
|
| 114 |
+
self.short_memory = self.short_memory.strip()
|
| 115 |
+
self._save_short_memory()
|
| 116 |
+
|
| 117 |
+
def _add_node(self, key, value):
|
| 118 |
+
index = 0
|
| 119 |
+
new_key = key
|
| 120 |
+
while new_key in self.concepts:
|
| 121 |
+
index += 1
|
| 122 |
+
new_key = key + str(index)
|
| 123 |
+
self.concepts[new_key] = LinkMemoryNode(key=new_key, content=value)
|
| 124 |
+
self.db.upsert(self.concepts[key].__dict__, Query().key == new_key)
|
| 125 |
+
return new_key
|
GeneralAgent/memory/normal_memory.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Memeory
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
class NormalMemory:
|
| 6 |
+
def __init__(self, serialize_path='./memory.json'):
|
| 7 |
+
self.messages = []
|
| 8 |
+
self.serialize_path = serialize_path
|
| 9 |
+
if os.path.exists(serialize_path):
|
| 10 |
+
with open(serialize_path, 'r', encoding='utf-8') as f:
|
| 11 |
+
self.messages = json.load(f)
|
| 12 |
+
|
| 13 |
+
def save(self):
|
| 14 |
+
with open(self.serialize_path, 'w', encoding='utf-8') as f:
|
| 15 |
+
json.dump(self.messages, f)
|
| 16 |
+
|
| 17 |
+
def add_message(self, role, content):
|
| 18 |
+
assert role in ['user', 'assistant']
|
| 19 |
+
self.messages.append({'role': role, 'content': content})
|
| 20 |
+
self.save()
|
| 21 |
+
|
| 22 |
+
def append_message(self, role, content):
|
| 23 |
+
assert role in ['user', 'assistant']
|
| 24 |
+
if len(self.messages) > 0 and self.messages[-1]['role'] == role:
|
| 25 |
+
self.messages[-1]['content'] += '\n' + content
|
| 26 |
+
else:
|
| 27 |
+
self.messages.append({'role': role, 'content': content})
|
| 28 |
+
self.save()
|
| 29 |
+
|
| 30 |
+
def get_messages(self):
|
| 31 |
+
return self.messages
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_NormalMemory():
|
| 35 |
+
serialize_path = './memory.json'
|
| 36 |
+
mem = NormalMemory(serialize_path=serialize_path)
|
| 37 |
+
mem.add_message('user', 'hello')
|
| 38 |
+
mem.add_message('assistant', 'hi')
|
| 39 |
+
mem = NormalMemory(serialize_path=serialize_path)
|
| 40 |
+
assert len(mem.get_messages()) == 2
|
| 41 |
+
mem.append_message('assistant', 'hi')
|
| 42 |
+
assert len(mem.get_messages()) == 2
|
GeneralAgent/memory/stack_memory.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Memeory
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
from typing import List
|
| 4 |
+
from tinydb import TinyDB, Query
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@dataclass
|
| 8 |
+
class StackMemoryNode:
|
| 9 |
+
role: str
|
| 10 |
+
action: str
|
| 11 |
+
state: str = 'ready'
|
| 12 |
+
content: str = None
|
| 13 |
+
prefix: str = None
|
| 14 |
+
node_id: int = None
|
| 15 |
+
parent: int = None
|
| 16 |
+
childrens: List[int] = None
|
| 17 |
+
|
| 18 |
+
def __post_init__(self):
|
| 19 |
+
assert self.role in ['user', 'system', 'root'], self.role
|
| 20 |
+
assert self.action in ['input', 'answer', 'plan'], self.action
|
| 21 |
+
assert self.state in ['ready', 'success', 'fail'], self.state
|
| 22 |
+
self.childrens = self.childrens if self.childrens else []
|
| 23 |
+
|
| 24 |
+
def __str__(self):
|
| 25 |
+
return f'<{self.role}><{self.action}><{self.state}>: {self.content}'
|
| 26 |
+
|
| 27 |
+
def __repr__(self):
|
| 28 |
+
return str(self)
|
| 29 |
+
|
| 30 |
+
def success_work(self):
|
| 31 |
+
self.state = 'success'
|
| 32 |
+
|
| 33 |
+
def fail_work(self):
|
| 34 |
+
self.state = 'fail'
|
| 35 |
+
|
| 36 |
+
def is_root(self):
|
| 37 |
+
return self.role == 'root'
|
| 38 |
+
|
| 39 |
+
def get_level(self):
|
| 40 |
+
if self.is_root():
|
| 41 |
+
return 0
|
| 42 |
+
else:
|
| 43 |
+
return self.get_parent().get_level() + 1
|
| 44 |
+
|
| 45 |
+
@classmethod
|
| 46 |
+
def new_root(cls):
|
| 47 |
+
return cls(node_id=0, role='root', action='input', state='success', content='root', parent=None, childrens=[])
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class StackMemory:
|
| 51 |
+
def __init__(self, serialize_path='./memory.json'):
|
| 52 |
+
self.db = TinyDB(serialize_path)
|
| 53 |
+
nodes = [StackMemoryNode(**node) for node in self.db.all()]
|
| 54 |
+
self.spark_nodes = dict(zip([node.node_id for node in nodes], nodes))
|
| 55 |
+
# add root node
|
| 56 |
+
if len(self.spark_nodes) == 0:
|
| 57 |
+
root_node = StackMemoryNode.new_root()
|
| 58 |
+
self.spark_nodes[root_node.node_id] = root_node
|
| 59 |
+
self.db.insert(root_node.__dict__)
|
| 60 |
+
# load current_node
|
| 61 |
+
current_nodes = self.db.table('current_node').all()
|
| 62 |
+
if len(current_nodes) > 0:
|
| 63 |
+
node_id = current_nodes[0]['id']
|
| 64 |
+
# print(node_id)
|
| 65 |
+
# print(self)
|
| 66 |
+
self.current_node = self.get_node(node_id)
|
| 67 |
+
else:
|
| 68 |
+
self.current_node = None
|
| 69 |
+
|
| 70 |
+
def set_current_node(self, current_node):
|
| 71 |
+
self.current_node = current_node
|
| 72 |
+
# save current node
|
| 73 |
+
self.db.table('current_node').truncate()
|
| 74 |
+
self.db.table('current_node').insert({'id': current_node.node_id})
|
| 75 |
+
|
| 76 |
+
def new_node_id(self):
|
| 77 |
+
return max(self.spark_nodes.keys()) + 1
|
| 78 |
+
|
| 79 |
+
def node_count(self):
|
| 80 |
+
# ignore root node
|
| 81 |
+
return len(self.spark_nodes.keys()) - 1
|
| 82 |
+
|
| 83 |
+
def is_all_children_success(self, node):
|
| 84 |
+
# check if all childrens of node are success
|
| 85 |
+
childrens = [self.get_node(node_id) for node_id in node.childrens]
|
| 86 |
+
return all([children.state == 'success' for children in childrens])
|
| 87 |
+
|
| 88 |
+
def add_node(self, node):
|
| 89 |
+
# put in root node
|
| 90 |
+
root_node = self.get_node(0)
|
| 91 |
+
node.node_id = self.new_node_id()
|
| 92 |
+
node.parent = root_node.node_id
|
| 93 |
+
root_node.childrens.append(node.node_id)
|
| 94 |
+
# save node
|
| 95 |
+
self.update_node(root_node)
|
| 96 |
+
self.db.insert(node.__dict__)
|
| 97 |
+
self.spark_nodes[node.node_id] = node
|
| 98 |
+
|
| 99 |
+
def delete_node(self, node):
|
| 100 |
+
# delete node and all its childrens
|
| 101 |
+
for children_id in node.childrens:
|
| 102 |
+
children = self.get_node(children_id)
|
| 103 |
+
self.delete_node(children)
|
| 104 |
+
parent = self.get_node_parent(node)
|
| 105 |
+
if parent:
|
| 106 |
+
parent.childrens.remove(node.node_id)
|
| 107 |
+
self.update_node(parent)
|
| 108 |
+
self.db.remove(Query().node_id == node.node_id)
|
| 109 |
+
del self.spark_nodes[node.node_id]
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def add_node_after(self, last_node, node):
|
| 113 |
+
# add node after last_node
|
| 114 |
+
node.node_id = self.new_node_id()
|
| 115 |
+
node.parent = last_node.parent
|
| 116 |
+
parent = self.get_node_parent(node)
|
| 117 |
+
if parent:
|
| 118 |
+
parent.childrens.insert(parent.childrens.index(last_node.node_id)+1, node.node_id)
|
| 119 |
+
self.update_node(parent)
|
| 120 |
+
# move childrens of last_node to node
|
| 121 |
+
node.childrens = last_node.childrens
|
| 122 |
+
last_node.childrens = []
|
| 123 |
+
self.update_node(last_node)
|
| 124 |
+
for children_id in node.childrens:
|
| 125 |
+
children = self.get_node(children_id)
|
| 126 |
+
children.parent = node.node_id
|
| 127 |
+
self.update_node(children)
|
| 128 |
+
# save node
|
| 129 |
+
self.db.insert(node.__dict__)
|
| 130 |
+
self.spark_nodes[node.node_id] = node
|
| 131 |
+
return node
|
| 132 |
+
|
| 133 |
+
def add_node_in(self, parent_node, node, put_first=False):
|
| 134 |
+
# add node in parent_node
|
| 135 |
+
node.node_id = self.new_node_id()
|
| 136 |
+
node.parent = parent_node.node_id
|
| 137 |
+
if put_first:
|
| 138 |
+
parent_node.childrens.insert(0, node.node_id)
|
| 139 |
+
else:
|
| 140 |
+
parent_node.childrens.append(node.node_id)
|
| 141 |
+
self.update_node(parent_node)
|
| 142 |
+
# save node
|
| 143 |
+
self.db.insert(node.__dict__)
|
| 144 |
+
self.spark_nodes[node.node_id] = node
|
| 145 |
+
return node
|
| 146 |
+
|
| 147 |
+
def get_node(self, node_id):
|
| 148 |
+
return self.spark_nodes[node_id]
|
| 149 |
+
|
| 150 |
+
def get_node_level(self, node:StackMemoryNode):
|
| 151 |
+
if node.is_root():
|
| 152 |
+
return 0
|
| 153 |
+
else:
|
| 154 |
+
return self.get_node_level(self.get_node_parent(node)) + 1
|
| 155 |
+
|
| 156 |
+
def get_node_parent(self, node):
|
| 157 |
+
if node.parent is None:
|
| 158 |
+
return None
|
| 159 |
+
else:
|
| 160 |
+
return self.get_node(node.parent)
|
| 161 |
+
|
| 162 |
+
def update_node(self, node):
|
| 163 |
+
self.db.update(node.__dict__, Query().node_id == node.node_id)
|
| 164 |
+
|
| 165 |
+
def get_related_nodes_for_node(self, node):
|
| 166 |
+
# ancestors + left_brothers + self
|
| 167 |
+
parent = self.get_node_parent(node)
|
| 168 |
+
brothers = [self.get_node(node_id) for node_id in parent.childrens]
|
| 169 |
+
left_brothers = [('brother', x) for x in brothers[:brothers.index(node)]]
|
| 170 |
+
ancestors = self.get_related_nodes_for_node(parent) if not parent.is_root() else []
|
| 171 |
+
return ancestors + left_brothers + [('direct', node)]
|
| 172 |
+
|
| 173 |
+
def get_related_messages_for_node(self, node: StackMemoryNode):
|
| 174 |
+
def _get_message(node, position='direct'):
|
| 175 |
+
content = node.content if node.prefix is None else node.prefix + ' ' + node.content
|
| 176 |
+
if position == 'brother' and node.action == 'plan' and len(node.childrens) > 0:
|
| 177 |
+
content = node.content + ' [detail ...]'
|
| 178 |
+
return {'role': node.role, 'content': content}
|
| 179 |
+
nodes_with_position = self.get_related_nodes_for_node(node)
|
| 180 |
+
messages = [_get_message(node, position) for position, node in nodes_with_position]
|
| 181 |
+
# if node.action == 'plan':
|
| 182 |
+
# messages[-1]['content'] = 'Improve the details of this topic:: ' + messages[-1]['content']
|
| 183 |
+
return messages
|
| 184 |
+
|
| 185 |
+
def get_all_description_of_node(self, node, intend_char=' ', depth=0):
|
| 186 |
+
lines = []
|
| 187 |
+
description = intend_char * depth + str(node)
|
| 188 |
+
if not node.is_root():
|
| 189 |
+
lines += [description]
|
| 190 |
+
for children_id in node.childrens:
|
| 191 |
+
children = self.get_node(children_id)
|
| 192 |
+
lines += self.get_all_description_of_node(children, intend_char, depth+1)
|
| 193 |
+
return lines
|
| 194 |
+
|
| 195 |
+
def __str__(self) -> str:
|
| 196 |
+
lines = self.get_all_description_of_node(self.get_node(0), depth=-1)
|
| 197 |
+
return '\n'.join(lines)
|
| 198 |
+
|
| 199 |
+
def success_node(self, node):
|
| 200 |
+
node.success_work()
|
| 201 |
+
self.update_node(node)
|
| 202 |
+
|
| 203 |
+
def _get_todo_node(self, node=None):
|
| 204 |
+
# get the first ready node in the tree of node
|
| 205 |
+
if node is None:
|
| 206 |
+
node = self.get_node(0)
|
| 207 |
+
for node_id in node.childrens:
|
| 208 |
+
child = self._get_todo_node(self.get_node(node_id))
|
| 209 |
+
if child is not None:
|
| 210 |
+
return child
|
| 211 |
+
if node.is_root():
|
| 212 |
+
return None
|
| 213 |
+
if node.state in ['ready']:
|
| 214 |
+
return node
|
| 215 |
+
return None
|
| 216 |
+
|
| 217 |
+
def get_todo_node(self):
|
| 218 |
+
todo_node = self._get_todo_node()
|
| 219 |
+
# if all childrens of todo_node are success, success todo_node
|
| 220 |
+
if todo_node is not None and len(todo_node.childrens) > 0 and self.is_all_children_success(todo_node):
|
| 221 |
+
self.success_node(todo_node)
|
| 222 |
+
return self.get_todo_node()
|
| 223 |
+
return todo_node
|
GeneralAgent/pytest.ini
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[pytest]
|
| 2 |
+
python_files = *.py
|
GeneralAgent/requirements.txt
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.104.1
|
| 2 |
+
requests==2.31.0
|
| 3 |
+
tinydb==4.8.0
|
| 4 |
+
openai==0.27.6
|
| 5 |
+
jinja2==3.1.2
|
| 6 |
+
numpy==1.24.4
|
| 7 |
+
bs4==0.0.1
|
| 8 |
+
playwright==1.39.0
|
| 9 |
+
retrying==1.3.4
|
| 10 |
+
pymupdf==1.23.6
|
| 11 |
+
python-pptx==0.6.23
|
| 12 |
+
python-docx==1.1.0
|
| 13 |
+
yfinance==0.2.31
|
| 14 |
+
beautifulsoup4==4.12.2
|
| 15 |
+
python-dotenv==1.0.0
|
| 16 |
+
uvicorn==0.24.0.post1
|
| 17 |
+
tiktoken==0.5.1
|
| 18 |
+
httpx==0.25.1
|
| 19 |
+
pulsar-client==3.3.0
|
| 20 |
+
pymongo==4.6.0
|
| 21 |
+
websocket-client-py3==0.15.0
|
| 22 |
+
websockets==12.0
|
| 23 |
+
pypdf==3.17.1
|
| 24 |
+
replicate==0.18.1
|
| 25 |
+
edge-tts==6.1.9
|
| 26 |
+
pydub==0.25.1
|
| 27 |
+
chromadb==0.4.17
|
| 28 |
+
python-multipart==0.0.6
|
| 29 |
+
pytest==7.4.3
|
| 30 |
+
pytest-asyncio==0.21.1
|
GeneralAgent/skills/__init__.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 单列
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
def load_functions_with_path(python_code_path):
|
| 5 |
+
try:
|
| 6 |
+
import importlib.util
|
| 7 |
+
import inspect
|
| 8 |
+
|
| 9 |
+
# 指定要加载的文件路径和文件名
|
| 10 |
+
module_name = 'skills'
|
| 11 |
+
module_file = python_code_path
|
| 12 |
+
|
| 13 |
+
# 使用importlib加载文件
|
| 14 |
+
spec = importlib.util.spec_from_file_location(module_name, module_file)
|
| 15 |
+
module = importlib.util.module_from_spec(spec)
|
| 16 |
+
spec.loader.exec_module(module)
|
| 17 |
+
|
| 18 |
+
# 获取文件中的所有函数
|
| 19 |
+
functions = inspect.getmembers(module, inspect.isfunction)
|
| 20 |
+
|
| 21 |
+
# 过滤functions中以下划线开头的函数、test_开头的函数
|
| 22 |
+
functions = filter(lambda f: not f[0].startswith('_'), functions)
|
| 23 |
+
# functions = filter(lambda f: not f[0].startswith('test_'), functions)
|
| 24 |
+
|
| 25 |
+
return [f[1] for f in functions]
|
| 26 |
+
except Exception as e:
|
| 27 |
+
# 代码可能有错误,加载不起来
|
| 28 |
+
import logging
|
| 29 |
+
logging.exception(e)
|
| 30 |
+
return []
|
| 31 |
+
|
| 32 |
+
class Skills:
|
| 33 |
+
__instance = None
|
| 34 |
+
|
| 35 |
+
@classmethod
|
| 36 |
+
def __getInstance(cls):
|
| 37 |
+
return cls.__instance
|
| 38 |
+
|
| 39 |
+
@classmethod
|
| 40 |
+
def _instance(cls, *args, **kwargs):
|
| 41 |
+
if not Skills.__instance:
|
| 42 |
+
Skills.__instance = Skills(*args, **kwargs)
|
| 43 |
+
return Skills.__instance
|
| 44 |
+
|
| 45 |
+
def __setattr__(self, name, value):
|
| 46 |
+
if name.startswith('_'):
|
| 47 |
+
object.__setattr__(self, name, value)
|
| 48 |
+
else:
|
| 49 |
+
self._local_funs[name] = value
|
| 50 |
+
|
| 51 |
+
def __getattr__(self, name):
|
| 52 |
+
if name.startswith('_'):
|
| 53 |
+
return object.__getattr__(self, name)
|
| 54 |
+
else:
|
| 55 |
+
return self._get_func(name)
|
| 56 |
+
|
| 57 |
+
def _get_func(self, name):
|
| 58 |
+
fun = self._local_funs.get(name, None)
|
| 59 |
+
if fun is not None:
|
| 60 |
+
return fun
|
| 61 |
+
fun = self._remote_funs.get(name, None)
|
| 62 |
+
if fun is not None:
|
| 63 |
+
return fun
|
| 64 |
+
self._load_remote_funs()
|
| 65 |
+
fun = self._remote_funs.get(name, None)
|
| 66 |
+
if fun is not None:
|
| 67 |
+
return fun
|
| 68 |
+
print('Function {} not found'.format(name))
|
| 69 |
+
return None
|
| 70 |
+
|
| 71 |
+
def __init__(self):
|
| 72 |
+
self._local_funs = {}
|
| 73 |
+
self._remote_funs = {}
|
| 74 |
+
self._load_local_funs()
|
| 75 |
+
self._load_remote_funs()
|
| 76 |
+
|
| 77 |
+
def _load_funcs(self, the_dir):
|
| 78 |
+
total_funs = []
|
| 79 |
+
for file in os.listdir(the_dir):
|
| 80 |
+
# 如果file是文件夹
|
| 81 |
+
if os.path.isdir(os.path.join(the_dir, file)):
|
| 82 |
+
total_funs += self._load_funcs(os.path.join(the_dir, file))
|
| 83 |
+
else:
|
| 84 |
+
# 如果file是文件
|
| 85 |
+
if file.endswith('.py') and (not file.startswith('__init__') and not file.startswith('_') and not file == 'main.py'):
|
| 86 |
+
funcs = load_functions_with_path(os.path.join(the_dir, file))
|
| 87 |
+
total_funs += funcs
|
| 88 |
+
return total_funs
|
| 89 |
+
|
| 90 |
+
def _load_local_funs(self):
|
| 91 |
+
self._local_funs = {}
|
| 92 |
+
funcs = self._load_funcs(os.path.dirname(__file__))
|
| 93 |
+
for fun in funcs:
|
| 94 |
+
self._local_funs[fun.__name__] = fun
|
| 95 |
+
|
| 96 |
+
def _load_remote_funs(self):
|
| 97 |
+
from GeneralAgent.utils import get_functions_dir
|
| 98 |
+
self._remote_funs = {}
|
| 99 |
+
funcs = self._load_funcs(get_functions_dir())
|
| 100 |
+
for fun in funcs:
|
| 101 |
+
self._remote_funs[fun.__name__] = fun
|
| 102 |
+
|
| 103 |
+
def _search_functions(self, task_description, return_list=False):
|
| 104 |
+
"""
|
| 105 |
+
Search functions that may help to solve the task.
|
| 106 |
+
"""
|
| 107 |
+
from .llm_inference import search_similar_texts
|
| 108 |
+
signatures = self._all_function_signatures()
|
| 109 |
+
results = search_similar_texts(task_description, signatures, top_k=5)
|
| 110 |
+
if return_list:
|
| 111 |
+
return results
|
| 112 |
+
else:
|
| 113 |
+
return '\n'.join(results)
|
| 114 |
+
|
| 115 |
+
def _all_function_signatures(self):
|
| 116 |
+
from .python_envs import get_function_signature
|
| 117 |
+
locals = [get_function_signature(fun, 'skills') for fun in self._local_funs.values() if not fun.__name__.startswith('test_')]
|
| 118 |
+
remotes = [get_function_signature(fun, 'skills') for fun in self._remote_funs.values() if not fun.__name__.startswith('test_')]
|
| 119 |
+
return locals + remotes
|
| 120 |
+
|
| 121 |
+
skills = Skills._instance()
|
GeneralAgent/skills/agent_builder_2.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def _llm_write_ui_lib(lib_name:str, task:str) -> str:
|
| 2 |
+
"""
|
| 3 |
+
Write a UI library for a given task description.
|
| 4 |
+
@param lib_name: The name of the UI library.
|
| 5 |
+
@param task: The task description.
|
| 6 |
+
@return: The UI library code.
|
| 7 |
+
"""
|
| 8 |
+
from GeneralAgent import skills
|
| 9 |
+
prompt_template = """
|
| 10 |
+
You are a React and Typescript expert.
|
| 11 |
+
|
| 12 |
+
# Task
|
| 13 |
+
Create a React function component named LibTemplate in tsx language.
|
| 14 |
+
The component should have the following functionality:
|
| 15 |
+
{{task}}
|
| 16 |
+
Note:
|
| 17 |
+
1. The component only save data to backend, no need to display data, or result of task.
|
| 18 |
+
2. When uploaded file, should show the file path
|
| 19 |
+
3. No need to ask user set result file name, backend server will use a unique name to save the file.
|
| 20 |
+
|
| 21 |
+
# Import
|
| 22 |
+
Use the following import syntax:
|
| 23 |
+
```
|
| 24 |
+
const React = (window as any).React;
|
| 25 |
+
const antd = (window as any).antd;
|
| 26 |
+
```
|
| 27 |
+
No other import methods are allowed.
|
| 28 |
+
|
| 29 |
+
# DEMO
|
| 30 |
+
|
| 31 |
+
```tsx
|
| 32 |
+
const React = (window as any).React;
|
| 33 |
+
const antd = (window as any).antd;
|
| 34 |
+
|
| 35 |
+
interface Props {
|
| 36 |
+
save_data: (user_data:any)=>void,
|
| 37 |
+
FileUploadConponent: (props: {onUploadSuccess: (file_path: string) => void, title?: string}) => React.ReactElement
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// user save_data to save the user_data
|
| 41 |
+
// user_data will be processed into a string through save_data using JSON.stringify({'data': user_data}) and sent to the backend
|
| 42 |
+
// use FileUploadConponent to upload file (<props.FileUploadConponent onUploadSuccess={handleUploadSuccess} title=''/>) and add file_path to data before save
|
| 43 |
+
|
| 44 |
+
const LibTemplate = (props: Props) => {
|
| 45 |
+
|
| 46 |
+
const handleCommit = () => {
|
| 47 |
+
# props.save_data(all_data_should_save)
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
return (<>{xxx}</>);
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
export default LibTemplate;
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
Please reponse the component code which finish the task without any explaination.
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
from jinja2 import Template
|
| 60 |
+
prompt = Template(prompt_template).render(task=task)
|
| 61 |
+
messages = [{'role': 'system', 'content': prompt}]
|
| 62 |
+
response = skills.llm_inference(messages, model_type="normal", stream=True)
|
| 63 |
+
result = ''
|
| 64 |
+
for token in response:
|
| 65 |
+
result += token
|
| 66 |
+
result = result.replace('LibTemplate', lib_name)
|
| 67 |
+
return result
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def create_application_ui(task: str, component_name: str = None) -> (str, str):
|
| 71 |
+
"""
|
| 72 |
+
Convert a given task description into UI components.
|
| 73 |
+
In the code, user_data will be processed into a string through save_data using JSON.stringify({'data': user_data}) and sent to the backend
|
| 74 |
+
@param task: Task description, should include all details related to creating the UI
|
| 75 |
+
@param component_name: The name of the UI library.
|
| 76 |
+
@return: The name of the UI library, the path of the UI library, the code of the UI library. None if failed.
|
| 77 |
+
Example:
|
| 78 |
+
create_application_ui('A task description with all the necessary details')
|
| 79 |
+
"""
|
| 80 |
+
import os
|
| 81 |
+
import uuid
|
| 82 |
+
from GeneralAgent import skills
|
| 83 |
+
lib_name = component_name
|
| 84 |
+
if lib_name is None:
|
| 85 |
+
lib_name = 'Lib' + str(uuid.uuid1())[:4]
|
| 86 |
+
target_dir = os.path.join(skills.get_code_dir(), lib_name)
|
| 87 |
+
content = _llm_write_ui_lib(lib_name, task)
|
| 88 |
+
code = skills.extract_tsx_code(content)
|
| 89 |
+
success = skills.compile_tsx(lib_name, code, target_dir)
|
| 90 |
+
if success:
|
| 91 |
+
js_path = os.path.join(lib_name, 'index.js')
|
| 92 |
+
print(f'UI library created successfully.\n js_component_name: {lib_name}\n js_path: {js_path}\n code: \n```tsx\n{code}\n```')
|
| 93 |
+
return lib_name, js_path, code
|
| 94 |
+
else:
|
| 95 |
+
return None
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def update_application_meta_2(
|
| 99 |
+
application_id:str=None,
|
| 100 |
+
type:str=None,
|
| 101 |
+
application_name:str=None,
|
| 102 |
+
description:str=None,
|
| 103 |
+
js_component_name:str=None,
|
| 104 |
+
js_path:str=None,
|
| 105 |
+
agent_can_upload_file=None,
|
| 106 |
+
) -> None:
|
| 107 |
+
"""
|
| 108 |
+
Update application meta data. When type is application, you should provide js_component_name and js_path. When type is agent, you should ignore js_component_name and js_path.
|
| 109 |
+
@param application_id: application id, You should name it, example: translat_text, ai_draw
|
| 110 |
+
@param type: application type, one of ['application', 'agent']. application is a normal application with ui, agent is a agent application with chat interface.
|
| 111 |
+
@param application_name: application name
|
| 112 |
+
@param description: application description
|
| 113 |
+
@param js_component_name: js component name
|
| 114 |
+
@param js_path: js file path
|
| 115 |
+
@param agent_can_upload_file: agent can upload file or not, default is False, if True, agent can upload file to the application
|
| 116 |
+
@return: None
|
| 117 |
+
"""
|
| 118 |
+
import os, json
|
| 119 |
+
from GeneralAgent import skills
|
| 120 |
+
bot_json_path = os.path.join(skills.get_code_dir(), 'bot.json')
|
| 121 |
+
if os.path.exists(bot_json_path):
|
| 122 |
+
with open(bot_json_path, 'r', encoding='utf-8') as f:
|
| 123 |
+
app_json = json.loads(f.read())
|
| 124 |
+
else:
|
| 125 |
+
app_json = {}
|
| 126 |
+
if application_id is not None:
|
| 127 |
+
from GeneralAgent import skills
|
| 128 |
+
bots = skills.load_applications()
|
| 129 |
+
if application_id in [x['id'] for x in bots]:
|
| 130 |
+
print(f'application_id ({application_id}) exists. ignore If you are just edit the exist application, or you should change the application_id')
|
| 131 |
+
app_json['id'] = application_id
|
| 132 |
+
if type is not None:
|
| 133 |
+
app_json['type'] = type
|
| 134 |
+
if application_name is not None:
|
| 135 |
+
app_json['name'] = application_name
|
| 136 |
+
if description is not None:
|
| 137 |
+
app_json['description'] = description
|
| 138 |
+
app_json['upload_file'] = 'yes'
|
| 139 |
+
if js_component_name is not None:
|
| 140 |
+
app_json['js_name'] = js_component_name
|
| 141 |
+
if js_path is not None:
|
| 142 |
+
app_json['js_path'] = js_path
|
| 143 |
+
if agent_can_upload_file is not None:
|
| 144 |
+
app_json['upload_file'] = 'yes'
|
| 145 |
+
if os.path.exists(os.path.join(skills.get_code_dir(), 'icon.jpg')):
|
| 146 |
+
app_json['icon'] = 'icon.jpg'
|
| 147 |
+
else:
|
| 148 |
+
del app_json['icon']
|
| 149 |
+
with open(bot_json_path, 'w', encoding='utf-8') as f:
|
| 150 |
+
f.write(json.dumps(app_json, indent=4))
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def edit_application_code_2(task_description:str) -> str:
|
| 154 |
+
"""
|
| 155 |
+
edit_application_code_2 is an Agent. You just tell it what will be done and vailable functions, it will generate a python function to complete the task. the code will be saved in main.py, which will be used to create a normal application or agent application.
|
| 156 |
+
@param task_description: task description, should be a string and include the detail of task, and what functions can be used. when building a normal application, task_desciption should have the detail of data format that ui save to backend, example: "Create a image creation application. Available functions:\n\nskills.image_generation(prompt) generate a image with prompt (in english), return a image url\n\nskills.translate_text(content, target_language), data format is {'data': {'prompt': 'xxxxx'}}". when building a agent application, ignore the detail of data format, example: "Create a agent. role prompt is : You are a image creator, transfer users's need to create a image. Available functions:\n\n xxxx"
|
| 157 |
+
@return: python code for the task
|
| 158 |
+
"""
|
| 159 |
+
import os
|
| 160 |
+
from GeneralAgent import skills
|
| 161 |
+
code_path = os.path.join(skills.get_code_dir(), 'main.py')
|
| 162 |
+
old_code = None
|
| 163 |
+
if os.path.exists(code_path):
|
| 164 |
+
with open(code_path, 'r', encoding='utf-8') as f:
|
| 165 |
+
old_code = f.read()
|
| 166 |
+
code = _generate_agent_code(task_description, default_code=old_code)
|
| 167 |
+
with open(code_path, 'w', encoding='utf-8') as f:
|
| 168 |
+
f.write(code)
|
| 169 |
+
return code
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def _generate_agent_code(task_description, default_code=None):
|
| 173 |
+
"""Return the python code text that completes the task to build a chat bot, when default_code is not None, update default_code by task"""
|
| 174 |
+
from GeneralAgent import skills
|
| 175 |
+
python_version = skills.get_python_version()
|
| 176 |
+
requirements = skills.get_current_env_python_libs()
|
| 177 |
+
prompt = f"""
|
| 178 |
+
You are a python expert, write a python function to complete user's task.
|
| 179 |
+
The function in code will be used to create a chat bot, like slack, discord.
|
| 180 |
+
|
| 181 |
+
# Function signature
|
| 182 |
+
```
|
| 183 |
+
async def main(messages, input, output_callback):
|
| 184 |
+
# messages is a list of dict, like [{{"role": "user", "content": "hello"}}, {{"role": "system", "content": "hi"}}]
|
| 185 |
+
# input is a string, user's input in agent application, or json string by save_data in UI in normal application.
|
| 186 |
+
# output_callback is a async function, output_callback(content: str) -> None. output_callback will send content to user. the content should be markdown format. file should be like [title](sandbox:file_path)
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
# Python Version: {python_version}
|
| 190 |
+
|
| 191 |
+
# Python Libs installed
|
| 192 |
+
{requirements}
|
| 193 |
+
|
| 194 |
+
# CONSTRAINTS:
|
| 195 |
+
- Do not import the lib that the function not use.
|
| 196 |
+
- Import the lib in the function
|
| 197 |
+
- In the code, Intermediate files are written directly to the current directory (./)
|
| 198 |
+
- Give the function a name that describe the task
|
| 199 |
+
- The docstring of the function should be as concise as possible without losing key information, only one line, and output in English
|
| 200 |
+
|
| 201 |
+
# DEMO 1 : normal application, write user's input to a file and return
|
| 202 |
+
```python
|
| 203 |
+
async def main(messages, input, output_callback):
|
| 204 |
+
from GeneralAgent import skills
|
| 205 |
+
import json
|
| 206 |
+
data = json.loads(input)['data']
|
| 207 |
+
# file_path should be a unique name, because the file will not be deleted, and the application will run many times.
|
| 208 |
+
file_path = skills.unique_name() + '.json
|
| 209 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
| 210 |
+
f.write(json.dumps(data))
|
| 211 |
+
await output_callback(f'file saved: [user_data.json](sandbox:{{file_path}})')
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
# DEMO 2 : agent application, Agent with functions
|
| 215 |
+
```python
|
| 216 |
+
async def main(messages, input, output_callback):
|
| 217 |
+
from GeneralAgent.agent import Agent
|
| 218 |
+
role_prompt = \"\"\"
|
| 219 |
+
You are a translation agent.
|
| 220 |
+
You complete user requirements by writing python code to call the predefined functions.
|
| 221 |
+
\"\"\"
|
| 222 |
+
functions = [
|
| 223 |
+
skills.translate_text
|
| 224 |
+
]
|
| 225 |
+
agent = Agent.with_functions(functions)
|
| 226 |
+
agent.add_role_prompt(role_prompt)
|
| 227 |
+
await agent.run(input, output_callback=output_callback)
|
| 228 |
+
```python
|
| 229 |
+
|
| 230 |
+
# There are two function types:
|
| 231 |
+
1. Application: like DEMO1, The application process is fixed and less flexible, but the function will be more stable
|
| 232 |
+
2. Agent: like DEMO2, Agent is a chat bot that can use functions to complete user's task. The agent will automatic handle user's input and output
|
| 233 |
+
You can choose one of them to complete the task.
|
| 234 |
+
|
| 235 |
+
Please think step by step carefully, consider any possible situation, and write a complete code like DEMO
|
| 236 |
+
Just reponse the python code, no any explain, no start with ```python, no end with ```, no any other text.
|
| 237 |
+
"""
|
| 238 |
+
|
| 239 |
+
messages = [{"role": "system", "content": prompt}]
|
| 240 |
+
if default_code is not None:
|
| 241 |
+
messages += [{"role": "system", "content": "user's code: " + default_code}]
|
| 242 |
+
messages += [{"role": "system", "content": f"user's task: {task_description}"}]
|
| 243 |
+
code = skills.llm_inference(messages, model_type='smart')
|
| 244 |
+
code = skills.get_python_code(code)
|
| 245 |
+
return code
|
GeneralAgent/skills/agents.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
async def get_function_builder_agent():
|
| 3 |
+
"""
|
| 4 |
+
Get a function builder agent
|
| 5 |
+
"""
|
| 6 |
+
from GeneralAgent.agent import Agent
|
| 7 |
+
from GeneralAgent import skills
|
| 8 |
+
from GeneralAgent.interpreter import RoleInterpreter, PythonInterpreter, FileInterpreter, ShellInterpreter
|
| 9 |
+
from GeneralAgent.utils import get_functions_dir
|
| 10 |
+
function_dir = get_functions_dir()
|
| 11 |
+
role_prompt = f"""
|
| 12 |
+
You are an agent who writes python function and the test function of it, into files according to user needs.
|
| 13 |
+
You can control your computer and access the Internet.
|
| 14 |
+
|
| 15 |
+
# make a directory to store function file and test dataset
|
| 16 |
+
```shell
|
| 17 |
+
mkdir -p {function_dir}/function_folder_name
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
# copy files uploaded by users to the function file directory
|
| 21 |
+
```shell
|
| 22 |
+
cp yy.zz {function_dir}/function_folder_name/yy.zz
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
# When writing a function, you can first search for available functions. For Example
|
| 26 |
+
```python
|
| 27 |
+
result = search_functions('scrape web page')
|
| 28 |
+
print(result)
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
# The function should be written in the folder {function_dir}/functoin_folder_name, and the file name should be the function name
|
| 32 |
+
# The content of the file is the function and the test function of the function (starting with test_)
|
| 33 |
+
# every created file should have a unique name, which can be generated by skills.unique_name()
|
| 34 |
+
# Import code should be placed inside the function
|
| 35 |
+
|
| 36 |
+
# DEMO
|
| 37 |
+
```file
|
| 38 |
+
{function_dir}/image_generation/image_generation.py write 0 -1 <<EOF
|
| 39 |
+
def image_generation(prompt) -> str:
|
| 40 |
+
\"\"\"
|
| 41 |
+
Generate an image according to the prompt and return the image path. For example, when the prompt is "apple" you will get an image of an apple. Note: The prompt should describe objective things in detail, not abstract concepts. For example, if you want to draw a picture of Chengdu, the prompt should be "Picture of Chengdu, with giant pandas playing in the bamboo forest, people eating hot pot around, and a Jinsha Sunbird totem next to it" instead of "Draw a picture of Chengdu" "painting"
|
| 42 |
+
@param prompt: The prompt should be detailed enough to describe the image. Tips can be in any type of language, but English is recommended.
|
| 43 |
+
\"\"\"
|
| 44 |
+
import replicate
|
| 45 |
+
from GeneralAgent import skills
|
| 46 |
+
output = replicate.run(
|
| 47 |
+
"stability-ai/sdxl:2f779eb9b23b34fe171f8eaa021b8261566f0d2c10cd2674063e7dbcd351509e",
|
| 48 |
+
input={{"prompt": prompt}}
|
| 49 |
+
)
|
| 50 |
+
image_url = output
|
| 51 |
+
if not skills.text_is_english(prompt):
|
| 52 |
+
prompt = skills.translate_text(prompt, 'english')
|
| 53 |
+
image_url = _replicate_image_generation(prompt)
|
| 54 |
+
image_path = skills.try_download_file(image_url)
|
| 55 |
+
print(f'image created at {{image_path}}')
|
| 56 |
+
return image_path
|
| 57 |
+
|
| 58 |
+
def test_image_generation():
|
| 59 |
+
import os
|
| 60 |
+
# load test file
|
| 61 |
+
file_path = os.path.join(os.path.dirname(__file__), 'yy.zz')
|
| 62 |
+
prompt = 'xxx'
|
| 63 |
+
image_path = image_generation(prompt)
|
| 64 |
+
assert os.path.exists(image_path)
|
| 65 |
+
EOF
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
# The written functions can be accessed through GeneralAgent's skills library, such as:
|
| 69 |
+
|
| 70 |
+
```python
|
| 71 |
+
from GeneralAgent import skills
|
| 72 |
+
result = skills.function_name()
|
| 73 |
+
skills.test_function_name()
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
# Note:
|
| 77 |
+
- Don't make up functions that don't exist
|
| 78 |
+
|
| 79 |
+
# General process for write function
|
| 80 |
+
* Fully communicate needs with users
|
| 81 |
+
* search available functions (by search_functions in python, optional)
|
| 82 |
+
* edit functions (by file operation)
|
| 83 |
+
* test functions (by python)
|
| 84 |
+
* ask for test files if needed, for example test data, test code, etc.
|
| 85 |
+
"""
|
| 86 |
+
functoins = [
|
| 87 |
+
skills.search_functions,
|
| 88 |
+
# skills.scrape_dynamic_web
|
| 89 |
+
]
|
| 90 |
+
workspace = './'
|
| 91 |
+
agent = Agent(workspace)
|
| 92 |
+
role_interpreter = RoleInterpreter(system_prompt=role_prompt)
|
| 93 |
+
python_interpreter = PythonInterpreter(serialize_path=f'{workspace}/code.bin')
|
| 94 |
+
python_interpreter.function_tools = functoins
|
| 95 |
+
|
| 96 |
+
# when file operation(python file), reload functions
|
| 97 |
+
file_interpreter = FileInterpreter()
|
| 98 |
+
async def reload_funs():
|
| 99 |
+
skills._load_remote_funs()
|
| 100 |
+
|
| 101 |
+
file_interpreter.outptu_parse_done_recall = reload_funs
|
| 102 |
+
agent.interpreters = [role_interpreter, python_interpreter, FileInterpreter(), ShellInterpreter()]
|
| 103 |
+
agent.model_type = 'smart'
|
| 104 |
+
agent.hide_output_parse = False
|
| 105 |
+
return agent
|
GeneralAgent/skills/ai_draw_prompt_gen.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
def ai_draw_prompt_gen(command):
|
| 3 |
+
"""The prompt word generator for AI painting inputs the user's needs and outputs a description word for a picture."""
|
| 4 |
+
from GeneralAgent import skills
|
| 5 |
+
system_prompt = "You are a prompt word engineer for AI painting. Your task is to describe the user's needs into descriptors for a picture, and require the content of the picture to meet the user's needs as much as possible. You can add some of your own creativity and understanding. Directly returns the description of the image without explanation or suffixes."
|
| 6 |
+
messages = [
|
| 7 |
+
{'role': 'system', 'content': system_prompt},
|
| 8 |
+
{"role": "user", "content": command}
|
| 9 |
+
]
|
| 10 |
+
image_prompt = skills.llm_inference(messages)
|
| 11 |
+
return image_prompt
|
GeneralAgent/skills/application_builder.py
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
CODE_DIR = './code'
|
| 3 |
+
|
| 4 |
+
def get_code_dir():
|
| 5 |
+
global CODE_DIR
|
| 6 |
+
import os
|
| 7 |
+
if not os.path.exists(CODE_DIR):
|
| 8 |
+
os.makedirs(CODE_DIR)
|
| 9 |
+
return CODE_DIR
|
| 10 |
+
|
| 11 |
+
def _set_code_dir(code_dir):
|
| 12 |
+
global CODE_DIR
|
| 13 |
+
CODE_DIR = code_dir
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def search_functions(task_description:str) -> str:
|
| 17 |
+
"""
|
| 18 |
+
print function signatures that may help to solve the task, and return the function signatures
|
| 19 |
+
"""
|
| 20 |
+
from GeneralAgent import skills
|
| 21 |
+
functions = skills._search_functions(task_description)
|
| 22 |
+
print(functions)
|
| 23 |
+
return functions
|
| 24 |
+
from jinja2 import Template
|
| 25 |
+
# # print(functions)
|
| 26 |
+
# prompt_template = """
|
| 27 |
+
# # Functions
|
| 28 |
+
# {{functions}}
|
| 29 |
+
|
| 30 |
+
# # Task
|
| 31 |
+
# {{task}}
|
| 32 |
+
|
| 33 |
+
# Please return the function signatures that can solve the task.
|
| 34 |
+
|
| 35 |
+
# """
|
| 36 |
+
# prompt = Template(prompt_template).render(task=task_description, functions=functions)
|
| 37 |
+
# functions = skills.llm_inference([{'role': 'system', 'content': 'You are a helpful assistant'}, {'role': 'user', 'content': prompt}])
|
| 38 |
+
# print(functions)
|
| 39 |
+
# return functions
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def edit_normal_function(function_name:str, task_description:str) -> None:
|
| 43 |
+
"""
|
| 44 |
+
Edit normal function code by task_description
|
| 45 |
+
@param function_name: The name of the function to be generated.
|
| 46 |
+
@param task_description (str): A description of the task that the generated function should perform and what functions can be used.
|
| 47 |
+
@return: The generated Python function signature as a string.
|
| 48 |
+
"""
|
| 49 |
+
return _edit_function(function_name, task_description, _generate_function_code)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def edit_llm_function(function_name: str, task_description:str) -> str:
|
| 53 |
+
"""
|
| 54 |
+
This function generates a Python function to perform a specific task using a large language model (LLM), such as translation, planning, answering general knowledge questions and so on.
|
| 55 |
+
@param function_name: The name of the function to be generated.
|
| 56 |
+
@param task_description (str): A description of the task that the generated function should perform.
|
| 57 |
+
@return: The generated Python function signature as a string.
|
| 58 |
+
"""
|
| 59 |
+
return _edit_function(function_name, task_description, _generate_llm_task_function)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _edit_function(function_name: str, task_description:str, code_fun) -> str:
|
| 63 |
+
import os
|
| 64 |
+
from GeneralAgent import skills
|
| 65 |
+
from GeneralAgent.utils import get_functions_dir
|
| 66 |
+
file_path = os.path.join(get_functions_dir(), function_name + '.py')
|
| 67 |
+
code = None
|
| 68 |
+
if os.path.exists(file_path):
|
| 69 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 70 |
+
code = f.read()
|
| 71 |
+
task_description += f'\n# Function name: {function_name}'
|
| 72 |
+
code = code_fun(task_description, default_code=code)
|
| 73 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
| 74 |
+
f.write(code)
|
| 75 |
+
funcs, error = skills.load_functions_with_path(file_path)
|
| 76 |
+
funcs = [x for x in funcs if x.__name__ == function_name]
|
| 77 |
+
if len(funcs) <= 0:
|
| 78 |
+
print(error)
|
| 79 |
+
return error
|
| 80 |
+
else:
|
| 81 |
+
signature = skills.get_function_signature(funcs[0], 'skills')
|
| 82 |
+
skills._load_remote_funs()
|
| 83 |
+
print(signature)
|
| 84 |
+
return signature
|
| 85 |
+
|
| 86 |
+
def delete_function(func_name:str) -> None:
|
| 87 |
+
"""
|
| 88 |
+
Delete a function by name
|
| 89 |
+
"""
|
| 90 |
+
import os
|
| 91 |
+
from GeneralAgent.utils import get_functions_dir
|
| 92 |
+
file_path = os.path.join(get_functions_dir(), func_name + '.py')
|
| 93 |
+
if os.path.exists(file_path):
|
| 94 |
+
os.remove(file_path)
|
| 95 |
+
|
| 96 |
+
def create_application_icon(application_description:str) -> None:
|
| 97 |
+
"""
|
| 98 |
+
Create a application icon by application description. The application description is application_description
|
| 99 |
+
"""
|
| 100 |
+
import os
|
| 101 |
+
from GeneralAgent import skills
|
| 102 |
+
prompt = skills.ai_draw_prompt_gen("Create an application icon. The application's description is below: \n" + application_description)
|
| 103 |
+
image_url = skills.image_generation(prompt)
|
| 104 |
+
file_path = skills.try_download_file(image_url)
|
| 105 |
+
target_path = os.path.join(get_code_dir(), 'icon.jpg')
|
| 106 |
+
os.system(f"mv {file_path} {target_path}")
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def edit_application_code(task_description:str) -> None:
|
| 110 |
+
"""
|
| 111 |
+
edit_application_code is an Agent. You just tell it what will be done and vailable functions, it will generate a python function to complete the task.
|
| 112 |
+
Edit agent code by task_description. task description should be a string and include the detail of task, and what functions can be used.
|
| 113 |
+
task_description example: "Create a image creation agent. Available functions:\n\nskills.image_generation(prompt) generate a image with prompt (in english), return a image url\n\nskills.translate_text(content, target_language)"
|
| 114 |
+
"""
|
| 115 |
+
import os
|
| 116 |
+
code_path = os.path.join(get_code_dir(), 'main.py')
|
| 117 |
+
old_code = None
|
| 118 |
+
if os.path.exists(code_path):
|
| 119 |
+
with open(code_path, 'r', encoding='utf-8') as f:
|
| 120 |
+
old_code = f.read()
|
| 121 |
+
from GeneralAgent import skills
|
| 122 |
+
code = _generate_agent_code(task_description, default_code=old_code)
|
| 123 |
+
with open(code_path, 'w', encoding='utf-8') as f:
|
| 124 |
+
f.write(code)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def delete_application():
|
| 128 |
+
"""
|
| 129 |
+
Delete application code
|
| 130 |
+
"""
|
| 131 |
+
import os
|
| 132 |
+
code_path = os.path.join(get_code_dir(), 'main.py')
|
| 133 |
+
if os.path.exists(code_path):
|
| 134 |
+
os.remove(code_path)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def update_application_meta(application_id:str=None, application_name:str=None, description:str=None, upload_file:str=None) -> None:
|
| 138 |
+
"""
|
| 139 |
+
Update application meta data
|
| 140 |
+
application_id: application id, You should name it, example: translat_text, ai_draw
|
| 141 |
+
application_name: application name
|
| 142 |
+
description: application description
|
| 143 |
+
upload_file: 'yes' or 'no', when upload_file is 'yes', the application can upload file, when upload_file is 'no', the application can not upload file
|
| 144 |
+
"""
|
| 145 |
+
import os, json
|
| 146 |
+
bot_json_path = os.path.join(get_code_dir(), 'bot.json')
|
| 147 |
+
if os.path.exists(bot_json_path):
|
| 148 |
+
with open(bot_json_path, 'r', encoding='utf-8') as f:
|
| 149 |
+
app_json = json.loads(f.read())
|
| 150 |
+
else:
|
| 151 |
+
app_json = {}
|
| 152 |
+
if application_id is not None:
|
| 153 |
+
from GeneralAgent import skills
|
| 154 |
+
bots = skills.load_applications()
|
| 155 |
+
if application_id in [x['id'] for x in bots]:
|
| 156 |
+
print(f'application_id ({application_id}) exists. ignore If you are just edit the exist application, or you should change the application_id')
|
| 157 |
+
app_json['id'] = application_id
|
| 158 |
+
if application_name is not None:
|
| 159 |
+
app_json['name'] = application_name
|
| 160 |
+
if description is not None:
|
| 161 |
+
app_json['description'] = description
|
| 162 |
+
if upload_file is not None:
|
| 163 |
+
app_json['upload_file'] = upload_file
|
| 164 |
+
if os.path.exists(os.path.join(get_code_dir(), 'icon.jpg')):
|
| 165 |
+
app_json['icon'] = 'icon.jpg'
|
| 166 |
+
else:
|
| 167 |
+
del app_json['icon']
|
| 168 |
+
with open(bot_json_path, 'w', encoding='utf-8') as f:
|
| 169 |
+
f.write(json.dumps(app_json, indent=4))
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def install_application() -> None:
|
| 173 |
+
"""
|
| 174 |
+
Install application to chat bot
|
| 175 |
+
"""
|
| 176 |
+
# TODO: check function_id and application_name
|
| 177 |
+
import os, json
|
| 178 |
+
bot_json_path = os.path.join(get_code_dir(), 'bot.json')
|
| 179 |
+
if os.path.exists(bot_json_path):
|
| 180 |
+
with open(bot_json_path, 'r', encoding='utf-8') as f:
|
| 181 |
+
app_json = json.loads(f.read())
|
| 182 |
+
else:
|
| 183 |
+
print('applicatoin meta not exists')
|
| 184 |
+
return
|
| 185 |
+
application_id = app_json['id']
|
| 186 |
+
# move code to bot
|
| 187 |
+
from GeneralAgent.utils import get_applications_dir
|
| 188 |
+
target_dir = os.path.join(get_applications_dir(), application_id)
|
| 189 |
+
if os.path.exists(target_dir):
|
| 190 |
+
import shutil
|
| 191 |
+
shutil.rmtree(target_dir)
|
| 192 |
+
os.makedirs(target_dir)
|
| 193 |
+
# print(target_dir)
|
| 194 |
+
os.system(f"cp -r {get_code_dir()}/* {target_dir}")
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def uninstall_application() -> None:
|
| 198 |
+
"""
|
| 199 |
+
Uninstall application from chat bot
|
| 200 |
+
"""
|
| 201 |
+
import os, json
|
| 202 |
+
bot_json_path = os.path.join(get_code_dir(), 'bot.json')
|
| 203 |
+
if os.path.exists(bot_json_path):
|
| 204 |
+
with open(bot_json_path, 'r', encoding='utf-8') as f:
|
| 205 |
+
app_json = json.loads(f.read())
|
| 206 |
+
else:
|
| 207 |
+
print('applicatoin meta not exists')
|
| 208 |
+
return
|
| 209 |
+
application_id = app_json['id']
|
| 210 |
+
# move code to bot
|
| 211 |
+
from GeneralAgent.utils import get_applications_dir
|
| 212 |
+
target_dir = os.path.join(get_applications_dir(), application_id)
|
| 213 |
+
if os.path.exists(target_dir):
|
| 214 |
+
import shutil
|
| 215 |
+
shutil.rmtree(target_dir)
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def _generate_function_code(task:str, default_code=None, search_functions=False):
|
| 220 |
+
"""Return the python function code text that completes the task to be used by other function or application, when default_code is not None, update default_code by task"""
|
| 221 |
+
|
| 222 |
+
"""
|
| 223 |
+
Return the python function code text that completes the task(a string)
|
| 224 |
+
task: 文字描述的任务
|
| 225 |
+
default_code: 如果不为None,按task修改默认代码,否则按task生成代码
|
| 226 |
+
return: 一个python代码字符串,主要包含了一个函数
|
| 227 |
+
"""
|
| 228 |
+
# global skills
|
| 229 |
+
import os
|
| 230 |
+
from GeneralAgent import skills
|
| 231 |
+
python_version = skills.get_python_version()
|
| 232 |
+
requirements = skills.get_current_env_python_libs()
|
| 233 |
+
the_skills_can_use = skills._search_functions(task) if search_functions else ''
|
| 234 |
+
prompt = f"""
|
| 235 |
+
You are a python expert, write a function to complete user's task
|
| 236 |
+
|
| 237 |
+
# Python Version
|
| 238 |
+
{python_version}
|
| 239 |
+
|
| 240 |
+
# Python Libs installed
|
| 241 |
+
{requirements}
|
| 242 |
+
|
| 243 |
+
# You can use skills lib(from GeneralAgent import skills), the function in the lib are:
|
| 244 |
+
{the_skills_can_use}
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
# CONSTRAINTS:
|
| 248 |
+
- Do not import the lib that the function not use.
|
| 249 |
+
- Import the lib in the function, any import statement must be placed in the function
|
| 250 |
+
- docstring the function simplely
|
| 251 |
+
- Do not use other libraries
|
| 252 |
+
- In the code, Intermediate files are written directly to the current directory (./)
|
| 253 |
+
- Give the function a name that describle the task
|
| 254 |
+
- The docstring of the function should be as concise as possible without losing key information, only one line, and output in English
|
| 255 |
+
- The code should be as simple as possible and the operation complexity should be low
|
| 256 |
+
|
| 257 |
+
# Demo:
|
| 258 |
+
```python
|
| 259 |
+
def translate(text:str, language:str) -> str:
|
| 260 |
+
\"\"\"
|
| 261 |
+
translate, return the translated text
|
| 262 |
+
Parameters: text -- user text, string
|
| 263 |
+
Returns: the translated text, string
|
| 264 |
+
\"\"\"
|
| 265 |
+
from GeneralAgent import skills
|
| 266 |
+
contents = text.split('.')
|
| 267 |
+
translated = []
|
| 268 |
+
for x in contents:
|
| 269 |
+
prompt = "Translate the following text to " + language + "\n" + x
|
| 270 |
+
translated += [skills.llm_inference([{{'role': 'system', 'content': prompt}}])
|
| 271 |
+
return '. '.join(translated)
|
| 272 |
+
```
|
| 273 |
+
|
| 274 |
+
Please think step by step carefully, consider any possible situation, and write a complete function.
|
| 275 |
+
Just reponse the python code, no any explain, no start with ```python, no end with ```, no any other text.
|
| 276 |
+
"""
|
| 277 |
+
messages = [{"role": "system", "content": prompt}]
|
| 278 |
+
if default_code is not None:
|
| 279 |
+
messages += [{"role": "system", "content": "user's code: " + default_code}]
|
| 280 |
+
messages += [{"role": "system", "content": f"user's task: {task}"}]
|
| 281 |
+
code = skills.llm_inference(messages, model_type='smart')
|
| 282 |
+
code = skills.get_python_code(code)
|
| 283 |
+
return code
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def application_code_generation(task, default_code=None):
|
| 287 |
+
"""Return the python code text that completes the task to build a chat bot, when default_code is not None, update default_code by task"""
|
| 288 |
+
from GeneralAgent import skills
|
| 289 |
+
python_version = skills.get_python_version()
|
| 290 |
+
requirements = skills.get_current_env_python_libs()
|
| 291 |
+
the_skills_can_use = skills._search_functions(task)
|
| 292 |
+
|
| 293 |
+
prompt = f"""
|
| 294 |
+
You are a python expert, write a python function to complete user's task.
|
| 295 |
+
The function in code will be used to create a chat bot, like slack, discord.
|
| 296 |
+
|
| 297 |
+
# Function signature
|
| 298 |
+
```
|
| 299 |
+
async def main(chat_history, input, file_path, output_callback, file_callback, ui_callback):
|
| 300 |
+
# chat_history is a list of dict, like [{{"role": "user", "content": "hello"}}, {{"role": "system", "content": "hi"}}]
|
| 301 |
+
# input is a string, user's input
|
| 302 |
+
# file_path is a string, user's file path
|
| 303 |
+
# output_callback is a async function, output_callback(content: str) -> None
|
| 304 |
+
# file_callback is a async function, file_callback(file_path: str) -> None
|
| 305 |
+
# ui_callback is a async function, ui_callback(name:str, js_path:str, data={{}}) -> None
|
| 306 |
+
```
|
| 307 |
+
|
| 308 |
+
# Python Version: {python_version}
|
| 309 |
+
|
| 310 |
+
# Python Libs installed
|
| 311 |
+
{requirements}
|
| 312 |
+
|
| 313 |
+
# You can use skills lib(from GeneralAgent import skills), the function in the lib are:
|
| 314 |
+
{the_skills_can_use}
|
| 315 |
+
|
| 316 |
+
# CONSTRAINTS:
|
| 317 |
+
- Do not import the lib that the function not use.
|
| 318 |
+
- Import the lib in the function
|
| 319 |
+
- In the code, Intermediate files are written directly to the current directory (./)
|
| 320 |
+
- Give the function a name that describe the task
|
| 321 |
+
- The docstring of the function should be as concise as possible without losing key information, only one line, and output in English
|
| 322 |
+
|
| 323 |
+
# DEMO 1 : Chat with A large language model
|
| 324 |
+
```python
|
| 325 |
+
async def main(chat_history, input, file_path, output_callback, file_callback, ui_callback):
|
| 326 |
+
from GeneralAgent import skills
|
| 327 |
+
chat_history = skills.cut_messages(chat_history, 4000)
|
| 328 |
+
messages = [{{"role": "system", "content": "You are a helpful assistant."}}] + chat_history
|
| 329 |
+
response = skills.llm_inference(messages, stream=True)
|
| 330 |
+
for token in response:
|
| 331 |
+
await output_callback(token)
|
| 332 |
+
await output_callback(None)
|
| 333 |
+
```
|
| 334 |
+
|
| 335 |
+
# DEMO 2 : Create a image by user's prompt
|
| 336 |
+
```python
|
| 337 |
+
async def main(chat_history, input, file_path, output_callback, file_callback, ui_callback):
|
| 338 |
+
from GeneralAgent import skills
|
| 339 |
+
prompt = input
|
| 340 |
+
if not skills.text_is_english(prompt):
|
| 341 |
+
prompt = skills.translate_text(prompt, 'english')
|
| 342 |
+
image_url = skills.image_generation(prompt)
|
| 343 |
+
await file_callback(image_url)
|
| 344 |
+
```
|
| 345 |
+
|
| 346 |
+
Please think step by step carefully, consider any possible situation, and write a complete code like DEMO
|
| 347 |
+
Just reponse the python code, no any explain, no start with ```python, no end with ```, no any other text.
|
| 348 |
+
"""
|
| 349 |
+
|
| 350 |
+
messages = [{"role": "system", "content": prompt}]
|
| 351 |
+
if default_code is not None:
|
| 352 |
+
messages += [{"role": "system", "content": "user's code: " + default_code}]
|
| 353 |
+
messages += [{"role": "system", "content": f"user's task: {task}"}]
|
| 354 |
+
code = skills.llm_inference(messages, model_type='smart')
|
| 355 |
+
code = skills.get_python_code(code)
|
| 356 |
+
return code
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def _generate_agent_code(task_description, default_code=None):
|
| 360 |
+
"""Return the python code text that completes the task to build a chat bot, when default_code is not None, update default_code by task"""
|
| 361 |
+
from GeneralAgent import skills
|
| 362 |
+
python_version = skills.get_python_version()
|
| 363 |
+
requirements = skills.get_current_env_python_libs()
|
| 364 |
+
prompt = f"""
|
| 365 |
+
You are a python expert, write a python function to complete user's task.
|
| 366 |
+
The function in code will be used to create a chat bot, like slack, discord.
|
| 367 |
+
|
| 368 |
+
# Function signature
|
| 369 |
+
```
|
| 370 |
+
async def main(chat_history, input, file_path, output_callback, file_callback, ui_callback):
|
| 371 |
+
# chat_history is a list of dict, like [{{"role": "user", "content": "hello"}}, {{"role": "system", "content": "hi"}}]
|
| 372 |
+
# input is a string, user's input
|
| 373 |
+
# file_path is a string, user's file path
|
| 374 |
+
# output_callback is a async function, output_callback(content: str) -> None
|
| 375 |
+
# file_callback is a async function, file_callback(file_path: str) -> None
|
| 376 |
+
# ui_callback is a async function, ui_callback(name:str, js_path:str, data={{}}) -> None
|
| 377 |
+
```
|
| 378 |
+
|
| 379 |
+
# Python Version: {python_version}
|
| 380 |
+
|
| 381 |
+
# Python Libs installed
|
| 382 |
+
{requirements}
|
| 383 |
+
|
| 384 |
+
# CONSTRAINTS:
|
| 385 |
+
- Do not import the lib that the function not use.
|
| 386 |
+
- Import the lib in the function
|
| 387 |
+
- In the code, Intermediate files are written directly to the current directory (./)
|
| 388 |
+
- Give the function a name that describe the task
|
| 389 |
+
- The docstring of the function should be as concise as possible without losing key information, only one line, and output in English
|
| 390 |
+
- Every created file should have a unique name, which can be generated by skills.unique_name()
|
| 391 |
+
|
| 392 |
+
# DEMO 1 : write user's input to a file and return
|
| 393 |
+
```python
|
| 394 |
+
async def main(chat_history, input, file_path, output_callback, file_callback, ui_callback):
|
| 395 |
+
from GeneralAgent import skills
|
| 396 |
+
|
| 397 |
+
```
|
| 398 |
+
|
| 399 |
+
# DEMO 2 : Agent with functions
|
| 400 |
+
```python
|
| 401 |
+
async def main(chat_history, input, file_path, output_callback, file_callback, ui_callback):
|
| 402 |
+
from GeneralAgent.agent import Agent
|
| 403 |
+
role_prompt = \"\"\"
|
| 404 |
+
You are a translation agent.
|
| 405 |
+
You complete user requirements by writing python code to call the predefined functions.
|
| 406 |
+
\"\"\"
|
| 407 |
+
functions = [
|
| 408 |
+
skills.translate_text
|
| 409 |
+
]
|
| 410 |
+
agent = Agent.with_functions(functions, role_prompt)
|
| 411 |
+
await agent.run(input, output_callback=output_callback)
|
| 412 |
+
```python
|
| 413 |
+
|
| 414 |
+
# There are two function types:
|
| 415 |
+
1. Application: like DEMO1, The application process is fixed and less flexible, but the function will be more stable
|
| 416 |
+
2. Agent: like DEMO2, Agent is a chat bot that can use functions to complete user's task. The agent will automatic handle user's input and output
|
| 417 |
+
You can choose one of them to complete the task.
|
| 418 |
+
|
| 419 |
+
Please think step by step carefully, consider any possible situation, and write a complete code like DEMO
|
| 420 |
+
Just reponse the python code, no any explain, no start with ```python, no end with ```, no any other text.
|
| 421 |
+
"""
|
| 422 |
+
|
| 423 |
+
messages = [{"role": "system", "content": prompt}]
|
| 424 |
+
if default_code is not None:
|
| 425 |
+
messages += [{"role": "system", "content": "user's code: " + default_code}]
|
| 426 |
+
messages += [{"role": "system", "content": f"user's task: {task_description}"}]
|
| 427 |
+
code = skills.llm_inference(messages, model_type='smart')
|
| 428 |
+
code = skills.get_python_code(code)
|
| 429 |
+
return code
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
def _generate_llm_task_function(task_description, default_code=None):
|
| 433 |
+
"""
|
| 434 |
+
This function generates a Python function to perform a specific task using a large language model (LLM), such as translation, planning, answering general knowledge questions.
|
| 435 |
+
|
| 436 |
+
Parameters:
|
| 437 |
+
task_description (str): A description of the task that the generated function should perform.
|
| 438 |
+
|
| 439 |
+
Returns:
|
| 440 |
+
str: The generated Python function code as a string.
|
| 441 |
+
"""
|
| 442 |
+
import os
|
| 443 |
+
from GeneralAgent import skills
|
| 444 |
+
python_version = skills.get_python_version()
|
| 445 |
+
requirements = skills.get_current_env_python_libs()
|
| 446 |
+
the_skills_can_use = skills._search_functions(task_description) if search_functions else ''
|
| 447 |
+
prompt = f"""
|
| 448 |
+
You are a python expert, write a function to complete user's task
|
| 449 |
+
|
| 450 |
+
# Python Version
|
| 451 |
+
{python_version}
|
| 452 |
+
|
| 453 |
+
# Python Libs installed
|
| 454 |
+
{requirements}
|
| 455 |
+
|
| 456 |
+
# You can use skills lib(from GeneralAgent import skills), the function in the lib are:
|
| 457 |
+
|
| 458 |
+
def skills.simple_llm_inference(messages, json_schema):
|
| 459 |
+
Run LLM (large language model) inference on the provided messages, The total number of tokens in the messages and the returned string must be less than 8000.
|
| 460 |
+
@param messages: Input messages for the model, like [{{'role': 'system', 'content': 'You are a helpful assistant'}}, {{'role': 'user', 'content': 'translate blow to english:\nxxxx'}}]
|
| 461 |
+
@param json_schema: the json schema of return dictionary, like {{"type": "object", "properties": {{"name": {{"type": "string"}}, "age": {{"type": "integer" }} }} }}
|
| 462 |
+
@return returned as a dictionary According to the provided JSON schema.
|
| 463 |
+
|
| 464 |
+
{the_skills_can_use}
|
| 465 |
+
|
| 466 |
+
# CONSTRAINTS:
|
| 467 |
+
- Do not import the lib that the function not use.
|
| 468 |
+
- Import the lib in the function, any import statement must be placed in the function
|
| 469 |
+
- docstring the function simplely
|
| 470 |
+
- Do not use other libraries
|
| 471 |
+
- In the code, Intermediate files are written directly to the current directory (./)
|
| 472 |
+
- Give the function a name that describle the task
|
| 473 |
+
- The docstring of the function should be as concise as possible without losing key information, only one line, and output in English
|
| 474 |
+
- The code should be as simple as possible and the operation complexity should be low
|
| 475 |
+
- Every created file should have a unique name, which can be generated by skills.unique_name()
|
| 476 |
+
|
| 477 |
+
# Demo:
|
| 478 |
+
```python
|
| 479 |
+
def translate(text:str, language:str) -> str:
|
| 480 |
+
\"\"\"
|
| 481 |
+
translate, return the translated text
|
| 482 |
+
Parameters: text -- user text, string
|
| 483 |
+
Returns: the translated text, string
|
| 484 |
+
\"\"\"
|
| 485 |
+
from GeneralAgent import skills
|
| 486 |
+
contents = text.split('.')
|
| 487 |
+
translated = []
|
| 488 |
+
for x in contents:
|
| 489 |
+
prompt = "Translate the following text to " + language + "\n" + x
|
| 490 |
+
translated += [skills.llm_inference([{{'role': 'system', 'content': prompt}}])
|
| 491 |
+
return '. '.join(translated)
|
| 492 |
+
```
|
| 493 |
+
|
| 494 |
+
Please think step by step carefully, consider any possible situation, and write a complete function.
|
| 495 |
+
Just reponse the python code, no any explain, no start with ```python, no end with ```, no any other text.
|
| 496 |
+
"""
|
| 497 |
+
messages = [{"role": "system", "content": prompt}]
|
| 498 |
+
if default_code is not None:
|
| 499 |
+
messages += [{"role": "system", "content": "user's code: " + default_code}]
|
| 500 |
+
messages += [{"role": "system", "content": f"user's task: {task_description}"}]
|
| 501 |
+
code = skills.llm_inference(messages, model_type='smart')
|
| 502 |
+
code = skills.get_python_code(code)
|
| 503 |
+
return code
|
| 504 |
+
|
| 505 |
+
# from GeneralAgent import skills
|
| 506 |
+
# from jinja2 import Template
|
| 507 |
+
# prompt_template = """
|
| 508 |
+
# 你是一个python专家。
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
# Your job is to have a large language model (LLM) perform specific tasks, such as translation, planning, answering general knowledge questions, etc.
|
| 512 |
+
# Your job is to have a large language model (LLM) perform specific tasks, such as translation, planning, answering general knowledge questions, etc.
|
| 513 |
+
# Large language model calling function:
|
| 514 |
+
|
| 515 |
+
# ```python
|
| 516 |
+
# def xxx(xxx):
|
| 517 |
+
# \"\"\"
|
| 518 |
+
# xxx
|
| 519 |
+
# \"\"\"
|
| 520 |
+
# from GeneralAgent import skills
|
| 521 |
+
# # skills.simple_llm_inference
|
| 522 |
+
# ```
|
| 523 |
+
|
| 524 |
+
# # Task
|
| 525 |
+
|
| 526 |
+
# {{task}}
|
| 527 |
+
|
| 528 |
+
# # Note:
|
| 529 |
+
|
| 530 |
+
# - All imports should be placed inside the function.
|
| 531 |
+
# - While creating your function, consider the all edge cases.
|
| 532 |
+
# - Do not use any other libraries except simple_llm_inference and installed libraries.
|
| 533 |
+
# - The simple_llm_inference function requires that the input messages are less than 8000, and the output length is less than 8000. -
|
| 534 |
+
# - When the task cannot be completed through one simple_llm_inference, you should consider task disassembly.
|
| 535 |
+
# """
|
| 536 |
+
# prompt = Template(prompt_template).render({'task': task_description})
|
| 537 |
+
# if default_code is not None:
|
| 538 |
+
# prompt += '\n' + 'user\'s code: ' + default_code + '\nUpdate the code to complete the task'
|
| 539 |
+
# result = skills.llm_inference([{'role': 'system', 'content': prompt}, {'role': 'user', 'content': 'You are a python expert.'}, {'role': 'user', 'content': prompt}], model_type="smart")
|
| 540 |
+
# code = skills.get_python_code(result)
|
| 541 |
+
# return code
|
| 542 |
+
|
| 543 |
+
|
| 544 |
+
# def create_function(func_name:str, task_description:str):
|
| 545 |
+
# """
|
| 546 |
+
# create a function by task description. Where task_description can include functions in GeneralAgent.skills
|
| 547 |
+
# """
|
| 548 |
+
# # from GeneralAgent import skills
|
| 549 |
+
# import os
|
| 550 |
+
# code = _generate_function_code(task_description)
|
| 551 |
+
# file_path = os.path.join(get_code_dir(), func_name + '.py')
|
| 552 |
+
# with open(file_path, 'w', encoding='utf-8') as f:
|
| 553 |
+
# f.write(code)
|
| 554 |
+
|
| 555 |
+
# def delete_function(func_name:str) -> None:
|
| 556 |
+
# """
|
| 557 |
+
# Delete a function by name
|
| 558 |
+
# """
|
| 559 |
+
# import os
|
| 560 |
+
# file_path = os.path.join(get_code_dir(), func_name + '.py')
|
| 561 |
+
# if os.path.exists(file_path):
|
| 562 |
+
# os.remove(file_path)
|
| 563 |
+
|
| 564 |
+
# def list_functions() -> [str]:
|
| 565 |
+
# """
|
| 566 |
+
# list all function names
|
| 567 |
+
# """
|
| 568 |
+
# # TODO function description
|
| 569 |
+
# import os
|
| 570 |
+
# files = os.listdir(get_code_dir())
|
| 571 |
+
# functions = [x.split('.')[0] for x in files]
|
| 572 |
+
# return functions
|
| 573 |
+
|
| 574 |
+
# def show_function(func_name:str) -> str:
|
| 575 |
+
# """
|
| 576 |
+
# Show a function code by name
|
| 577 |
+
# """
|
| 578 |
+
# import os
|
| 579 |
+
# file_path = os.path.join(get_code_dir(), func_name + '.py')
|
| 580 |
+
# if os.path.exists(file_path):
|
| 581 |
+
# with open(file_path, 'r', encoding='utf-8') as f:
|
| 582 |
+
# code = f.read()
|
| 583 |
+
# return code
|
| 584 |
+
# else:
|
| 585 |
+
# return None
|
| 586 |
+
|
| 587 |
+
|
| 588 |
+
# def create_application(task_description:str) -> None:
|
| 589 |
+
# """
|
| 590 |
+
# Create a application by task_description description. The application name is application_name, the task_description is task_description(string)
|
| 591 |
+
# """
|
| 592 |
+
# import os
|
| 593 |
+
# from GeneralAgent import skills
|
| 594 |
+
# # code = application_code_generation(task_description)
|
| 595 |
+
# code = _generate_agent_code(task_description)
|
| 596 |
+
# code_path = os.path.join(get_code_dir(), 'main.py')
|
| 597 |
+
# with open(code_path, 'w', encoding='utf-8') as f:
|
| 598 |
+
# f.write(code)
|
GeneralAgent/skills/applications.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
def load_applications():
|
| 3 |
+
"""
|
| 4 |
+
load all applications(bots) metadata
|
| 5 |
+
"""
|
| 6 |
+
from GeneralAgent.utils import get_applications_dir
|
| 7 |
+
local_bots = _load_bots(_get_local_applications_dir())
|
| 8 |
+
remote_bots= _load_bots(get_applications_dir())
|
| 9 |
+
for bot in remote_bots:
|
| 10 |
+
bot['nickname'] = '# ' + bot['nickname']
|
| 11 |
+
return local_bots + remote_bots
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _get_local_applications_dir():
|
| 15 |
+
from GeneralAgent.utils import get_local_applications_dir
|
| 16 |
+
return get_local_applications_dir()
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _load_bots(code_dir):
|
| 20 |
+
import os, json
|
| 21 |
+
result = []
|
| 22 |
+
for bot_name in os.listdir(code_dir):
|
| 23 |
+
bot_dir = os.path.join(code_dir, bot_name)
|
| 24 |
+
if os.path.isdir(bot_dir):
|
| 25 |
+
result.append(_load_bot_metadata(bot_dir))
|
| 26 |
+
return result
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _load_bot_metadata(bot_dir):
|
| 30 |
+
import os, json
|
| 31 |
+
bot_json_path = os.path.join(bot_dir, 'bot.json')
|
| 32 |
+
if os.path.exists(bot_json_path):
|
| 33 |
+
with open(bot_json_path, 'r', encoding='utf-8') as f:
|
| 34 |
+
bot_json = json.load(f)
|
| 35 |
+
if 'icon' in bot_json:
|
| 36 |
+
bot_json['icon_url'] = os.path.join(bot_dir, bot_json['icon'])
|
| 37 |
+
if 'js_path' in bot_json:
|
| 38 |
+
bot_json['js_path'] = os.path.join(bot_dir, bot_json['js_path'])
|
| 39 |
+
bot_json['nickname'] = bot_json['name']
|
| 40 |
+
return bot_json
|
| 41 |
+
return None
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def load_bot_metadata_by_id(bot_id):
|
| 45 |
+
"""
|
| 46 |
+
load bot metadata by bot id
|
| 47 |
+
"""
|
| 48 |
+
import os
|
| 49 |
+
local_dir = _get_local_applications_dir()
|
| 50 |
+
from GeneralAgent.utils import get_applications_dir
|
| 51 |
+
remote_dir = get_applications_dir()
|
| 52 |
+
bot_dir = os.path.join(local_dir, bot_id)
|
| 53 |
+
if not os.path.exists(bot_dir):
|
| 54 |
+
bot_dir = os.path.join(remote_dir, bot_id)
|
| 55 |
+
return _load_bot_metadata(bot_dir)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def get_application_module(bot_id):
|
| 59 |
+
"""
|
| 60 |
+
get application module by bot id
|
| 61 |
+
"""
|
| 62 |
+
import os
|
| 63 |
+
from GeneralAgent.utils import get_applications_dir
|
| 64 |
+
local_dir = _get_local_applications_dir()
|
| 65 |
+
remote_dir = get_applications_dir()
|
| 66 |
+
code_path = os.path.join(local_dir, f"{bot_id}/main.py")
|
| 67 |
+
if not os.path.exists(code_path):
|
| 68 |
+
code_path = os.path.join(remote_dir, f"{bot_id}/main.py")
|
| 69 |
+
return _load_application(code_path)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _load_application(code_path):
|
| 73 |
+
"""
|
| 74 |
+
load application with code path
|
| 75 |
+
"""
|
| 76 |
+
import os, importlib.util, logging
|
| 77 |
+
application = None
|
| 78 |
+
try:
|
| 79 |
+
spec = importlib.util.spec_from_file_location("main", code_path)
|
| 80 |
+
module = importlib.util.module_from_spec(spec)
|
| 81 |
+
spec.loader.exec_module(module)
|
| 82 |
+
application = module
|
| 83 |
+
except Exception as e:
|
| 84 |
+
logging.exception(e)
|
| 85 |
+
return application
|
GeneralAgent/skills/build_web.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ast import Tuple
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
def compile_tsx(lib_name:str, code:str, target_dir:str):
|
| 5 |
+
"""
|
| 6 |
+
Compile tsx code into a UI library.
|
| 7 |
+
@lib_name: the name of the UI library
|
| 8 |
+
@code: the tsx code
|
| 9 |
+
@target_dir: the directory to save the UI library
|
| 10 |
+
"""
|
| 11 |
+
# 目标目录不存在就创建
|
| 12 |
+
if os.path.exists(target_dir):
|
| 13 |
+
import shutil
|
| 14 |
+
shutil.rmtree(target_dir)
|
| 15 |
+
os.makedirs(target_dir)
|
| 16 |
+
|
| 17 |
+
from GeneralAgent.utils import get_tsx_builder_dir
|
| 18 |
+
ts_builder_dir = get_tsx_builder_dir()
|
| 19 |
+
|
| 20 |
+
code_path = os.path.join(ts_builder_dir, 'src/lib/index.tsx')
|
| 21 |
+
with open(code_path, 'w', encoding='utf-8') as f:
|
| 22 |
+
f.write(code)
|
| 23 |
+
# 写入lib名称
|
| 24 |
+
webpack_template_path = os.path.join(ts_builder_dir, 'webpack.config.template.js')
|
| 25 |
+
webpack_template = ''
|
| 26 |
+
with open(webpack_template_path, 'r', encoding='utf-8') as f:
|
| 27 |
+
webpack_template = f.read()
|
| 28 |
+
webpack_template = webpack_template.replace('LibTemplate', lib_name)
|
| 29 |
+
webpack_path = os.path.join(ts_builder_dir, 'webpack.config.js')
|
| 30 |
+
with open(webpack_path, 'w', encoding='utf-8') as f:
|
| 31 |
+
f.write(webpack_template)
|
| 32 |
+
# 获取编译的标准输出
|
| 33 |
+
output = os.popen(f"cd {ts_builder_dir} && npm run build").read()
|
| 34 |
+
if 'successfully' not in output:
|
| 35 |
+
print(output)
|
| 36 |
+
return False
|
| 37 |
+
# 将编译后的文件移动到目标目录
|
| 38 |
+
os.system(f"mv {ts_builder_dir}/build/* {target_dir}")
|
| 39 |
+
return True
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _llm_write_ui_lib(lib_name, task):
|
| 43 |
+
from GeneralAgent import skills
|
| 44 |
+
prompt_template = """
|
| 45 |
+
You are a React and Typescript expert.
|
| 46 |
+
|
| 47 |
+
# Task
|
| 48 |
+
Create a React function component named LibTemplate in tsx language.
|
| 49 |
+
The component should have the following functionality:
|
| 50 |
+
{{task}}
|
| 51 |
+
|
| 52 |
+
# Import
|
| 53 |
+
Use the following import syntax:
|
| 54 |
+
```
|
| 55 |
+
const React = (window as any).React;
|
| 56 |
+
const antd = (window as any).antd;
|
| 57 |
+
```
|
| 58 |
+
No other import methods are allowed.
|
| 59 |
+
|
| 60 |
+
# DEMO
|
| 61 |
+
|
| 62 |
+
```tsx
|
| 63 |
+
const React = (window as any).React;
|
| 64 |
+
const antd = (window as any).antd;
|
| 65 |
+
|
| 66 |
+
const [Form, Input, Button] = [antd.Form, antd.Input, antd.Button];
|
| 67 |
+
|
| 68 |
+
const LibTemplate = ({save_data}: {save_data: (data:any)=>void}) => {
|
| 69 |
+
// use save_data to save the data
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
export default LibTemplate;
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
Please reponse the component code which finish the task without any explaination.
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
from jinja2 import Template
|
| 79 |
+
prompt = Template(prompt_template).render(task=task)
|
| 80 |
+
messages = [{'role': 'system', 'content': prompt}]
|
| 81 |
+
response = skills.llm_inference(messages, model_type="normal", stream=True)
|
| 82 |
+
result = ''
|
| 83 |
+
for token in response:
|
| 84 |
+
# print(token, end='', flush=True)
|
| 85 |
+
result += token
|
| 86 |
+
result = result.replace('LibTemplate', lib_name)
|
| 87 |
+
return result
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def extract_tsx_code(content):
|
| 91 |
+
"""
|
| 92 |
+
Extract tsx code from markdown content.
|
| 93 |
+
"""
|
| 94 |
+
import re
|
| 95 |
+
# 兼容tsx和ts
|
| 96 |
+
pattern = re.compile(r'```tsx?\n([\s\S]*)\n```')
|
| 97 |
+
match = pattern.search(content)
|
| 98 |
+
if match:
|
| 99 |
+
return match.group(1)
|
| 100 |
+
else:
|
| 101 |
+
return content
|
| 102 |
+
|
| 103 |
+
# def create_ui(task: str, ui_dir: str = './ui', component_name: str = None) -> (str, str):
|
| 104 |
+
# """
|
| 105 |
+
# Convert task into UI components. Return (component_name, js_path) tuple.
|
| 106 |
+
# """
|
| 107 |
+
def create_ui(task: str, ui_dir: str = './ui', component_name: str = None) -> (str, str):
|
| 108 |
+
"""
|
| 109 |
+
Convert a given task description into UI components.
|
| 110 |
+
|
| 111 |
+
Args:
|
| 112 |
+
task: A string representing the task description with all the necessary details.
|
| 113 |
+
|
| 114 |
+
Returns:
|
| 115 |
+
A tuple containing the name of the UI component and the path to the JavaScript file.
|
| 116 |
+
|
| 117 |
+
Example:
|
| 118 |
+
create_ui('A task description with all the necessary details')
|
| 119 |
+
"""
|
| 120 |
+
import uuid
|
| 121 |
+
import os
|
| 122 |
+
lib_name = component_name
|
| 123 |
+
if lib_name is None:
|
| 124 |
+
lib_name = 'Lib' + str(uuid.uuid1())[:4]
|
| 125 |
+
if not os.path.exists(ui_dir):
|
| 126 |
+
os.makedirs(ui_dir)
|
| 127 |
+
target_dir = os.path.join(ui_dir, lib_name)
|
| 128 |
+
for _ in range(2):
|
| 129 |
+
content = _llm_write_ui_lib(lib_name, task)
|
| 130 |
+
code = extract_tsx_code(content)
|
| 131 |
+
success = compile_tsx(lib_name, code, target_dir)
|
| 132 |
+
if success:
|
| 133 |
+
return lib_name, os.path.join(target_dir, 'index.js')
|
| 134 |
+
return None
|
| 135 |
+
|
| 136 |
+
def parse_tsx_to_ui(code, save_dir=None):
|
| 137 |
+
import uuid
|
| 138 |
+
lib_name = 'Lib' + str(uuid.uuid1())[:4]
|
| 139 |
+
if save_dir is None:
|
| 140 |
+
from GeneralAgent import skills
|
| 141 |
+
save_dir = skills.get_code_dir()
|
| 142 |
+
if not os.path.exists(save_dir):
|
| 143 |
+
os.makedirs(save_dir)
|
| 144 |
+
target_dir = os.path.join(save_dir, lib_name)
|
| 145 |
+
success = compile_tsx(lib_name, code, target_dir)
|
| 146 |
+
if success:
|
| 147 |
+
return lib_name, os.path.join(target_dir, 'index.js')
|
| 148 |
+
else:
|
| 149 |
+
return None
|
GeneralAgent/skills/concatenate_videos/concatenate_videos.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
def concatenate_videos(video_list: list) -> str:
|
| 3 |
+
"""
|
| 4 |
+
Concatenate a list of videos into one video.
|
| 5 |
+
@param video_list: A list of video file paths.
|
| 6 |
+
@return: The file path of the concatenated video.
|
| 7 |
+
"""
|
| 8 |
+
import uuid
|
| 9 |
+
from moviepy.editor import concatenate_videoclips, VideoFileClip
|
| 10 |
+
clips = [VideoFileClip(video) for video in video_list]
|
| 11 |
+
final_clip = concatenate_videoclips(clips)
|
| 12 |
+
output_path = f"{uuid.uuid4().hex}.mp4" # Generate a unique output file name
|
| 13 |
+
final_clip.write_videofile(output_path)
|
| 14 |
+
return output_path
|
| 15 |
+
|
| 16 |
+
def test_concatenate_videos():
|
| 17 |
+
"""
|
| 18 |
+
Test the concatenate_videos function.
|
| 19 |
+
"""
|
| 20 |
+
import os
|
| 21 |
+
file_path = os.path.join(os.path.dirname(__file__), "f63bfaae7b0e.mp4")
|
| 22 |
+
video_list = [file_path, file_path, file_path] # Use the provided video file for testing
|
| 23 |
+
output_path = concatenate_videos(video_list)
|
| 24 |
+
assert os.path.exists(output_path)
|
| 25 |
+
|
| 26 |
+
if __name__ == '__main__':
|
| 27 |
+
test_concatenate_videos()
|
GeneralAgent/skills/concatenate_videos/f63bfaae7b0e.mp4
ADDED
|
Binary file (217 kB). View file
|
|
|
GeneralAgent/skills/download_file.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
def download_file(file_url, save_path):
|
| 3 |
+
"""download file to save_path, return True if success, else False"""
|
| 4 |
+
"""实现一个下载文件的函数,返回文件路径"""
|
| 5 |
+
import requests
|
| 6 |
+
import logging
|
| 7 |
+
try_count = 3
|
| 8 |
+
while try_count > 0:
|
| 9 |
+
try:
|
| 10 |
+
response = requests.get(file_url)
|
| 11 |
+
with open(save_path, "wb") as f:
|
| 12 |
+
f.write(response.content)
|
| 13 |
+
# print("Success: 文件(%s)已下载至 %s" % (file_url, save_path))
|
| 14 |
+
return True
|
| 15 |
+
except Exception as e:
|
| 16 |
+
# print("Error: 文件下载失败:", e)
|
| 17 |
+
logging.error(e)
|
| 18 |
+
import time
|
| 19 |
+
time.sleep(5)
|
| 20 |
+
try_count -= 1
|
| 21 |
+
continue
|
| 22 |
+
return False
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def try_download_file(file_path):
|
| 26 |
+
import os
|
| 27 |
+
from GeneralAgent import skills
|
| 28 |
+
from PIL import Image
|
| 29 |
+
"""Try to download file if it is a url, else return file_path"""
|
| 30 |
+
if file_path.startswith("http://") or file_path.startswith("https://"):
|
| 31 |
+
save_path = skills.unique_name() + '.' + file_path.split('.')[-1]
|
| 32 |
+
success = skills.download_file(file_path, save_path)
|
| 33 |
+
if success:
|
| 34 |
+
if save_path.endswith('.png') or save_path.endswith('.PNG'):
|
| 35 |
+
# 转成jpg
|
| 36 |
+
png_image = Image.open(save_path)
|
| 37 |
+
jpg_save_path = skills.unique_name() + '.jpg'
|
| 38 |
+
png_image.save(jpg_save_path, 'JPEG')
|
| 39 |
+
os.remove(save_path)
|
| 40 |
+
return jpg_save_path
|
| 41 |
+
else:
|
| 42 |
+
return save_path
|
| 43 |
+
else:
|
| 44 |
+
return file_path
|
| 45 |
+
else:
|
| 46 |
+
return file_path
|
| 47 |
+
|
| 48 |
+
if __name__ == '__main__':
|
| 49 |
+
# download_file('https://ai.tongtianta.site/file-upload/gvsAc4cEm543iaX5x/5.pdf', '1.pdf')
|
| 50 |
+
download_file('https://ai.tongtianta.site/file-upload/27XEb3WgyDru5eFFe/9.pdf', '3.pdf')
|
GeneralAgent/skills/file_operation.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
def read_pdf_pages(file_path):
|
| 3 |
+
"""Read the pdf file and return a list of strings on each page of the pdf"""
|
| 4 |
+
"""读取pdf文件,返回pdf每页字符串的列表"""
|
| 5 |
+
import fitz
|
| 6 |
+
doc = fitz.open(file_path)
|
| 7 |
+
documents = []
|
| 8 |
+
for page in doc:
|
| 9 |
+
documents.append(page.get_text())
|
| 10 |
+
return documents
|
| 11 |
+
|
| 12 |
+
def read_word_pages(file_path):
|
| 13 |
+
"""Read the word file and return a list of word paragraph strings"""
|
| 14 |
+
"""读取word文件,返回word段落字符串的列表"""
|
| 15 |
+
# https://zhuanlan.zhihu.com/p/146363527
|
| 16 |
+
from docx import Document
|
| 17 |
+
# 打开文档
|
| 18 |
+
document = Document(file_path)
|
| 19 |
+
# 读取标题、段落、列表内容
|
| 20 |
+
ps = [ paragraph.text for paragraph in document.paragraphs]
|
| 21 |
+
return ps
|
| 22 |
+
|
| 23 |
+
def read_ppt(file_path):
|
| 24 |
+
import pptx
|
| 25 |
+
prs = pptx.Presentation(file_path)
|
| 26 |
+
documents = []
|
| 27 |
+
for slide in prs.slides:
|
| 28 |
+
for shape in slide.shapes:
|
| 29 |
+
if hasattr(shape, "text"):
|
| 30 |
+
documents.append(shape.text)
|
| 31 |
+
return '\n'.join(documents)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def read_file_content(file_path):
|
| 35 |
+
"""return content of txt, md, pdf, docx file"""
|
| 36 |
+
# 支持file_path的类型包括txt、md、pdf、docx
|
| 37 |
+
if file_path.endswith('.pdf'):
|
| 38 |
+
return ' '.join(read_pdf_pages(file_path))
|
| 39 |
+
elif file_path.endswith('.docx'):
|
| 40 |
+
return ' '.join(read_word_pages(file_path))
|
| 41 |
+
elif file_path.endswith('.ppt') or file_path.endswith('.pptx'):
|
| 42 |
+
return read_ppt(file_path)
|
| 43 |
+
else:
|
| 44 |
+
# 默认当做文本文件
|
| 45 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 46 |
+
return '\n'.join(f.readlines())
|
| 47 |
+
|
| 48 |
+
def write_file_content(file_path, content):
|
| 49 |
+
"""write content to txt, md"""
|
| 50 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
| 51 |
+
f.write(content)
|
GeneralAgent/skills/llm_inference.py
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# from retrying import retry as _retry
|
| 2 |
+
# from tenacity import retry, stop_after_attempt, wait_fixed
|
| 3 |
+
|
| 4 |
+
class TinyDBCache():
|
| 5 |
+
def __init__(self):
|
| 6 |
+
from tinydb import TinyDB
|
| 7 |
+
import os, json
|
| 8 |
+
llm_path = os.environ.get('CACHE_PATH', None)
|
| 9 |
+
if llm_path is None:
|
| 10 |
+
from GeneralAgent.utils import get_server_dir
|
| 11 |
+
llm_path = os.path.join(get_server_dir(), 'cache.json')
|
| 12 |
+
self.db = TinyDB(llm_path)
|
| 13 |
+
|
| 14 |
+
@property
|
| 15 |
+
def cache_llm(self):
|
| 16 |
+
import os
|
| 17 |
+
return os.environ.get('LLM_CACHE', 'no') in ['yes', 'y', 'YES']
|
| 18 |
+
|
| 19 |
+
@property
|
| 20 |
+
def cache_embedding(self):
|
| 21 |
+
import os
|
| 22 |
+
return os.environ.get('EMBEDDING_CACHE', 'no') in ['yes', 'y', 'YES']
|
| 23 |
+
|
| 24 |
+
def get(self, table, key):
|
| 25 |
+
from tinydb import Query
|
| 26 |
+
result = self.db.table(table).get(Query().key == key)
|
| 27 |
+
if result is not None:
|
| 28 |
+
return result['value']
|
| 29 |
+
else:
|
| 30 |
+
return None
|
| 31 |
+
|
| 32 |
+
def set(self, table, key, value):
|
| 33 |
+
from tinydb import Query
|
| 34 |
+
self.db.table(table).upsert({'key': key, 'value': value}, Query().key == key)
|
| 35 |
+
|
| 36 |
+
def set_llm_cache(self, key, value):
|
| 37 |
+
if self.cache_llm:
|
| 38 |
+
self.set('llm', key, value)
|
| 39 |
+
|
| 40 |
+
def get_llm_cache(self, key):
|
| 41 |
+
if self.cache_llm:
|
| 42 |
+
return self.get('llm', key)
|
| 43 |
+
|
| 44 |
+
def set_embedding_cache(self, key, value):
|
| 45 |
+
if self.cache_embedding:
|
| 46 |
+
self.set('embedding', key, value)
|
| 47 |
+
|
| 48 |
+
def get_embedding_cache(self, key):
|
| 49 |
+
if self.cache_embedding:
|
| 50 |
+
return self.get('embedding', key)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
global_cache = TinyDBCache()
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def embedding_single(text) -> [float]:
|
| 57 |
+
"""
|
| 58 |
+
embedding the text and return a embedding (list of float) for the string
|
| 59 |
+
"""
|
| 60 |
+
global global_cache
|
| 61 |
+
key = _md5(text)
|
| 62 |
+
embedding = global_cache.get_embedding_cache(key)
|
| 63 |
+
if embedding is not None:
|
| 64 |
+
# print('embedding cache hitted')
|
| 65 |
+
return embedding
|
| 66 |
+
texts = [text]
|
| 67 |
+
from litellm import embedding
|
| 68 |
+
resp = embedding(model = _get_embedding_model(),
|
| 69 |
+
input=texts
|
| 70 |
+
)
|
| 71 |
+
result = [x['embedding'] for x in resp['data']]
|
| 72 |
+
embedding = result[0]
|
| 73 |
+
global_cache.set_embedding_cache(key, embedding)
|
| 74 |
+
return embedding
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def test_embedding_single():
|
| 78 |
+
size = None
|
| 79 |
+
from GeneralAgent.utils import EnvironmentVariableManager
|
| 80 |
+
with EnvironmentVariableManager('EMBEDDING_CACHE', 'no'):
|
| 81 |
+
for source_type in ['OPENAI', 'AZURE']:
|
| 82 |
+
with EnvironmentVariableManager('LLM_SOURCE', source_type):
|
| 83 |
+
result = embedding_single("Say this is a test")
|
| 84 |
+
# print(source_type, result)
|
| 85 |
+
if size is not None:
|
| 86 |
+
assert len(result) == size
|
| 87 |
+
size = len(result)
|
| 88 |
+
|
| 89 |
+
def test_embedding_batch():
|
| 90 |
+
size = None
|
| 91 |
+
from GeneralAgent.utils import EnvironmentVariableManager
|
| 92 |
+
with EnvironmentVariableManager('EMBEDDING_CACHE', 'no'):
|
| 93 |
+
for source_type in ['OPENAI', 'AZURE']:
|
| 94 |
+
with EnvironmentVariableManager('LLM_SOURCE', source_type):
|
| 95 |
+
result = embedding_batch(["Say this is a test"])
|
| 96 |
+
if size is not None:
|
| 97 |
+
assert len(result[0]) == size
|
| 98 |
+
size = len(result[0])
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def embedding_batch(texts) -> [[float]]:
|
| 102 |
+
"""
|
| 103 |
+
embedding the texts(list of string), and return a list of embedding (list of float) for every string
|
| 104 |
+
"""
|
| 105 |
+
global global_cache
|
| 106 |
+
embeddings = {}
|
| 107 |
+
remain_texts = []
|
| 108 |
+
for text in texts:
|
| 109 |
+
key = _md5(text)
|
| 110 |
+
embedding = global_cache.get_embedding_cache(key)
|
| 111 |
+
if embedding is not None:
|
| 112 |
+
embeddings[text] = embedding
|
| 113 |
+
else:
|
| 114 |
+
remain_texts.append(text)
|
| 115 |
+
if len(remain_texts) > 0:
|
| 116 |
+
result = _embedding_many(remain_texts)
|
| 117 |
+
for text, embedding in zip(remain_texts, result):
|
| 118 |
+
key = _md5(text)
|
| 119 |
+
global_cache.set_embedding_cache(key, embedding)
|
| 120 |
+
embeddings[text] = embedding
|
| 121 |
+
return [embeddings[text] for text in texts]
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _embedding_many(texts) -> [[float]]:
|
| 125 |
+
from litellm import embedding
|
| 126 |
+
# 每次最多embedding 16个
|
| 127 |
+
max_batch_size = 16
|
| 128 |
+
result = []
|
| 129 |
+
for i in range(0, len(texts), max_batch_size):
|
| 130 |
+
resp = embedding(model=_get_embedding_model(),input=texts[i:i+max_batch_size])
|
| 131 |
+
result += [x['embedding'] for x in resp['data']]
|
| 132 |
+
return result
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def cos_sim(a, b):
|
| 136 |
+
import numpy as np
|
| 137 |
+
from numpy.linalg import norm
|
| 138 |
+
# This function calculates the cosine similarity (scalar value) between two input vectors 'a' and 'b' (1-D array object), and return the similarity.
|
| 139 |
+
a = a if isinstance(a, np.ndarray) else np.array(a)
|
| 140 |
+
b = b if isinstance(b, np.ndarray) else np.array(b)
|
| 141 |
+
return np.dot(a, b)/(norm(a)*norm(b))
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def search_similar_texts(focal:str, texts:[str], top_k=5):
|
| 145 |
+
"""
|
| 146 |
+
search the most similar texts in texts, and return the top_k similar texts
|
| 147 |
+
"""
|
| 148 |
+
embeddings = embedding_batch([focal] + texts)
|
| 149 |
+
focal_embedding = embeddings[0]
|
| 150 |
+
texts_embeddings = embeddings[1:]
|
| 151 |
+
import numpy as np
|
| 152 |
+
similarities = np.dot(texts_embeddings, focal_embedding)
|
| 153 |
+
sorted_indices = np.argsort(similarities)
|
| 154 |
+
sorted_indices = sorted_indices[::-1]
|
| 155 |
+
return [texts[i] for i in sorted_indices[:top_k]]
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def _md5(obj):
|
| 159 |
+
import hashlib, json
|
| 160 |
+
if isinstance(obj, str):
|
| 161 |
+
return hashlib.md5(obj.encode('utf-8')).hexdigest()
|
| 162 |
+
else:
|
| 163 |
+
return hashlib.md5(json.dumps(obj).encode('utf-8')).hexdigest()
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def _get_llm_model(messages, model_type):
|
| 167 |
+
import os
|
| 168 |
+
from GeneralAgent import skills
|
| 169 |
+
assert model_type in ['normal', 'smart', 'long']
|
| 170 |
+
if model_type == 'normal' and skills.messages_token_count(messages) > 3000:
|
| 171 |
+
model_type = 'long'
|
| 172 |
+
api_type = os.environ.get('LLM_SOURCE', 'OPENAI')
|
| 173 |
+
model_key = f'{api_type}_LLM_MODEL_{model_type.upper()}'
|
| 174 |
+
model = os.environ.get(model_key, None)
|
| 175 |
+
if model is not None:
|
| 176 |
+
return model
|
| 177 |
+
model_key = f'{api_type}_LLM_MODEL_NORMAL'
|
| 178 |
+
return os.environ.get(model_key, 'gpt-3.5-turbo')
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def get_llm_token_limit(model_type):
|
| 182 |
+
"""
|
| 183 |
+
return the token limit for the model
|
| 184 |
+
"""
|
| 185 |
+
import os
|
| 186 |
+
assert model_type in ['normal', 'smart', 'long']
|
| 187 |
+
api_type = os.environ.get('LLM_SOURCE', 'OPENAI')
|
| 188 |
+
# OPENAI_LLM_MODEL_SMART_LIMIT
|
| 189 |
+
limit_key = f'{api_type}_LLM_MODEL_{model_type.upper()}_LIMIT'
|
| 190 |
+
limit = os.environ.get(limit_key, None)
|
| 191 |
+
if limit is not None:
|
| 192 |
+
return int(limit)
|
| 193 |
+
limit_key = f'{api_type}_LLM_MODEL_NORMAL_LIMIT'
|
| 194 |
+
return int(os.environ.get(limit_key, 4000))
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def test_get_llm_token_limit():
|
| 198 |
+
from GeneralAgent.utils import EnvironmentVariableManager
|
| 199 |
+
with EnvironmentVariableManager('LLM_SOURCE', 'OPENAI'):
|
| 200 |
+
with EnvironmentVariableManager('OPENAI_LLM_MODEL_SMART_LIMIT', '8000'):
|
| 201 |
+
assert get_llm_token_limit('smart') == 8000
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def _get_embedding_model():
|
| 205 |
+
import os
|
| 206 |
+
api_type = os.environ.get('LLM_SOURCE', 'OPENAI')
|
| 207 |
+
embedding_model = os.environ.get(f'{api_type}_EMBEDDING_MODEL', 'text-embedding-ada-002')
|
| 208 |
+
return embedding_model
|
| 209 |
+
|
| 210 |
+
def _get_temperature():
|
| 211 |
+
import os
|
| 212 |
+
temperature = float(os.environ.get('LLM_TEMPERATURE', 0.5))
|
| 213 |
+
return temperature
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def llm_inference(messages, model_type='normal', stream=False, json_schema=None):
|
| 217 |
+
"""
|
| 218 |
+
Run LLM (large language model) inference on the provided messages using the specified model.
|
| 219 |
+
|
| 220 |
+
Parameters:
|
| 221 |
+
messages: Input messages for the model, like [{'role': 'system', 'content': 'You are a helpful assistant'}, {'role': 'user', 'content': 'What is your name?'}]
|
| 222 |
+
model_type: Type of model to use. Options are 'normal', 'smart', 'long'
|
| 223 |
+
use_stream: Boolean indicating if the function should use streaming inference
|
| 224 |
+
json_format: Optional JSON schema string
|
| 225 |
+
|
| 226 |
+
Returns:
|
| 227 |
+
If use_stream is True, returns a generator that yields the inference results as they become available.
|
| 228 |
+
If use_stream is False, returns a string containing the inference result.
|
| 229 |
+
If json_format is provided, the inference result is parsed according to the provided JSON schema and returned as a dictionary.
|
| 230 |
+
|
| 231 |
+
Note:
|
| 232 |
+
The total number of tokens in the messages and the returned string must be less than 4000 when model_variant is 'normal', and less than 16000 when model_variant is 'long'.
|
| 233 |
+
"""
|
| 234 |
+
import logging
|
| 235 |
+
if stream:
|
| 236 |
+
return _llm_inference_with_stream(messages, model_type)
|
| 237 |
+
else:
|
| 238 |
+
if json_schema is None:
|
| 239 |
+
return _llm_inference_without_stream(messages, model_type)
|
| 240 |
+
else:
|
| 241 |
+
import json
|
| 242 |
+
if not isinstance(json_schema, str):
|
| 243 |
+
json_schema = json.dumps(json_schema)
|
| 244 |
+
messages[-1]['content'] += '\n' + return_json_prompt + json_schema
|
| 245 |
+
# messages += [{'role': 'user', 'content': return_json_prompt + json_schema}]
|
| 246 |
+
logging.debug(messages)
|
| 247 |
+
result = _llm_inference_without_stream(messages, model_type)
|
| 248 |
+
logging.debug(result)
|
| 249 |
+
return json.loads(fix_llm_json_str(result))
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def test_llm_inference():
|
| 253 |
+
from GeneralAgent.utils import EnvironmentVariableManager
|
| 254 |
+
with EnvironmentVariableManager('LLM_CACHE', 'no'):
|
| 255 |
+
for source_type in ['OPENAI', 'AZURE']:
|
| 256 |
+
with EnvironmentVariableManager('LLM_SOURCE', source_type):
|
| 257 |
+
model_types = ['normal', 'smart', 'long']
|
| 258 |
+
for model_type in model_types:
|
| 259 |
+
result = llm_inference([{"role": "user","content": "Say this is a test",}], model_type=model_type, stream=False)
|
| 260 |
+
print(source_type, model_type, result)
|
| 261 |
+
assert 'test' in result
|
| 262 |
+
result = ''
|
| 263 |
+
response = llm_inference([{"role": "user","content": "Say this is a test",}], model_type=model_type, stream=True)
|
| 264 |
+
for token in response:
|
| 265 |
+
result += token
|
| 266 |
+
print(source_type, model_type, result)
|
| 267 |
+
assert 'test' in result
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def simple_llm_inference(messages, json_schema=None):
|
| 271 |
+
"""
|
| 272 |
+
Run LLM (large language model) inference on the provided messages
|
| 273 |
+
|
| 274 |
+
Parameters:
|
| 275 |
+
messages: Input messages for the model, like [{'role': 'system', 'content': 'You are a helpful assistant'}, {'role': 'user', 'content': 'What is your name?'}]
|
| 276 |
+
json_format: Optional JSON schema string
|
| 277 |
+
|
| 278 |
+
Returns:
|
| 279 |
+
If json_format is provided, the inference result is parsed according to the provided JSON schema and returned as a dictionary.
|
| 280 |
+
Else return a string
|
| 281 |
+
|
| 282 |
+
Note:
|
| 283 |
+
The total number of tokens in the messages and the returned string must be less than 16000.
|
| 284 |
+
"""
|
| 285 |
+
from GeneralAgent import skills
|
| 286 |
+
return skills.llm_inference(messages, json_schema=json_schema)
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
# @retry(stop=stop_after_attempt(3), wait=wait_fixed(3))
|
| 290 |
+
async def async_llm_inference(messages, model_type='normal'):
|
| 291 |
+
from litellm import acompletion
|
| 292 |
+
import logging
|
| 293 |
+
global global_cache
|
| 294 |
+
logging.debug(messages)
|
| 295 |
+
key = _md5(messages)
|
| 296 |
+
result = global_cache.get_llm_cache(key)
|
| 297 |
+
if result is not None:
|
| 298 |
+
return result
|
| 299 |
+
model = _get_llm_model(messages, model_type)
|
| 300 |
+
temperature = _get_temperature()
|
| 301 |
+
try_count = 3
|
| 302 |
+
while try_count > 0:
|
| 303 |
+
try:
|
| 304 |
+
response = await acompletion(model=model, messages=messages, temperature=temperature)
|
| 305 |
+
result = response['choices'][0]['message']['content']
|
| 306 |
+
global_cache.set_llm_cache(key, result)
|
| 307 |
+
return result
|
| 308 |
+
except Exception as e:
|
| 309 |
+
try_count -= 1
|
| 310 |
+
import asyncio
|
| 311 |
+
await asyncio.sleep(3)
|
| 312 |
+
if try_count == 0:
|
| 313 |
+
raise ValueError('LLM(Large Languate Model) error, Please check your key or base_url, or network')
|
| 314 |
+
return ''
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
# @retry(stop=stop_after_attempt(3), wait=wait_fixed(3))
|
| 318 |
+
def _llm_inference_with_stream(messages, model_type='normal'):
|
| 319 |
+
"""
|
| 320 |
+
messages: llm messages, model_type: normal, smart, long
|
| 321 |
+
"""
|
| 322 |
+
from litellm import completion
|
| 323 |
+
import logging
|
| 324 |
+
# from GeneralAgent import skills
|
| 325 |
+
model = _get_llm_model(messages, model_type)
|
| 326 |
+
logging.debug(messages)
|
| 327 |
+
global global_cache
|
| 328 |
+
key = _md5(messages)
|
| 329 |
+
result = global_cache.get_llm_cache(key)
|
| 330 |
+
if result is not None:
|
| 331 |
+
# print('llm_inference cache hitted')
|
| 332 |
+
for x in result.split(' '):
|
| 333 |
+
yield x + ' '
|
| 334 |
+
yield '\n'
|
| 335 |
+
# yield None
|
| 336 |
+
else:
|
| 337 |
+
temperature = _get_temperature()
|
| 338 |
+
try_count = 3
|
| 339 |
+
while try_count > 0:
|
| 340 |
+
try:
|
| 341 |
+
response = completion(model=model, messages=messages, stream=True, temperature=temperature)
|
| 342 |
+
result = ''
|
| 343 |
+
for chunk in response:
|
| 344 |
+
# print(chunk)
|
| 345 |
+
if chunk['choices'][0]['finish_reason'] is None:
|
| 346 |
+
token = chunk['choices'][0]['delta']['content']
|
| 347 |
+
if token is None:
|
| 348 |
+
continue
|
| 349 |
+
result += token
|
| 350 |
+
global_cache.set_llm_cache(key, result)
|
| 351 |
+
yield token
|
| 352 |
+
break
|
| 353 |
+
except Exception as e:
|
| 354 |
+
try_count -= 1
|
| 355 |
+
import time
|
| 356 |
+
time.sleep(3)
|
| 357 |
+
if try_count == 0:
|
| 358 |
+
raise ValueError('LLM(Large Languate Model) error, Please check your key or base_url, or network')
|
| 359 |
+
# logging.info(result)
|
| 360 |
+
# yield None
|
| 361 |
+
|
| 362 |
+
# if we choose to use local llm for inferce, we can use the following completion function.
|
| 363 |
+
#def compeltion(model,messages,temperature):
|
| 364 |
+
# pass
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
# @retry(stop=stop_after_attempt(3), wait=wait_fixed(3))
|
| 368 |
+
def _llm_inference_without_stream(messages, model_type='normal'):
|
| 369 |
+
from litellm import completion
|
| 370 |
+
import logging
|
| 371 |
+
global global_cache
|
| 372 |
+
logging.debug(messages)
|
| 373 |
+
# print(messages)
|
| 374 |
+
key = _md5(messages)
|
| 375 |
+
result = global_cache.get_llm_cache(key)
|
| 376 |
+
if result is not None:
|
| 377 |
+
return result
|
| 378 |
+
model = _get_llm_model(messages, model_type)
|
| 379 |
+
temperature = _get_temperature()
|
| 380 |
+
try_count = 3
|
| 381 |
+
while try_count > 0:
|
| 382 |
+
try:
|
| 383 |
+
response = completion(model=model, messages=messages, temperature=temperature)
|
| 384 |
+
result = response['choices'][0]['message']['content']
|
| 385 |
+
global_cache.set_llm_cache(key, result)
|
| 386 |
+
return result
|
| 387 |
+
except Exception as e:
|
| 388 |
+
try_count -= 1
|
| 389 |
+
import time
|
| 390 |
+
time.sleep(3)
|
| 391 |
+
if try_count == 0:
|
| 392 |
+
raise ValueError('LLM(Large Languate Model) error, Please check your key or base_url, or network')
|
| 393 |
+
return ''
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
def fix_llm_json_str(string):
|
| 397 |
+
import json
|
| 398 |
+
import re
|
| 399 |
+
new_string = string.strip()
|
| 400 |
+
if new_string.startswith('```json'):
|
| 401 |
+
new_string = new_string[7:]
|
| 402 |
+
if new_string.endswith('```'):
|
| 403 |
+
new_string = new_string[:-3]
|
| 404 |
+
try:
|
| 405 |
+
json.loads(new_string)
|
| 406 |
+
return new_string
|
| 407 |
+
except Exception as e:
|
| 408 |
+
print("fix_llm_json_str failed 1:", e)
|
| 409 |
+
try:
|
| 410 |
+
pattern = r'```json(.*?)```'
|
| 411 |
+
match = re.findall(pattern, new_string, re.DOTALL)
|
| 412 |
+
if match:
|
| 413 |
+
new_string = match[-1]
|
| 414 |
+
|
| 415 |
+
json.loads(new_string)
|
| 416 |
+
return new_string
|
| 417 |
+
except Exception as e:
|
| 418 |
+
print("fix_llm_json_str failed 2:", e)
|
| 419 |
+
try:
|
| 420 |
+
new_string = new_string.replace("\n", "\\n")
|
| 421 |
+
json.loads(new_string)
|
| 422 |
+
return new_string
|
| 423 |
+
except Exception as e:
|
| 424 |
+
print("fix_llm_json_str failed 3:", e)
|
| 425 |
+
|
| 426 |
+
messages = [{
|
| 427 |
+
"role": "system",
|
| 428 |
+
"content": """Do not change the specific content, fix the json, directly return the repaired JSON, without any explanation and dialogue.
|
| 429 |
+
```
|
| 430 |
+
"""+new_string+"""
|
| 431 |
+
```"""
|
| 432 |
+
}]
|
| 433 |
+
|
| 434 |
+
message = llm_inference(messages)
|
| 435 |
+
pattern = r'```json(.*?)```'
|
| 436 |
+
match = re.findall(pattern, message, re.DOTALL)
|
| 437 |
+
if match:
|
| 438 |
+
return match[-1]
|
| 439 |
+
|
| 440 |
+
return message
|
| 441 |
+
|
| 442 |
+
return_json_prompt = """\n\nYou should only directly respond in JSON format without explian as described below, that must be parsed by Python json.loads.
|
| 443 |
+
Response JSON schema: \n"""
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
# def prompt_call(prompt_template, variables, json_schema=None):
|
| 447 |
+
# from jinja2 import Template
|
| 448 |
+
# import json
|
| 449 |
+
# prompt = Template(prompt_template).render(**variables)
|
| 450 |
+
# if json_schema is not None:
|
| 451 |
+
# prompt += return_json_prompt + json_schema
|
| 452 |
+
# result = llm_inference([{'role': 'system', 'content': 'You are a helpful assistant.'}, {'role': 'system', 'content': prompt}], model_type='smart')
|
| 453 |
+
# return json.loads(fix_llm_json_str(result))
|
| 454 |
+
# else:
|
| 455 |
+
# result = llm_inference([{'role': 'system', 'content': prompt}], model_type='smart')
|
| 456 |
+
|
| 457 |
+
if __name__ == '__main__':
|
| 458 |
+
test_embedding_single()
|
GeneralAgent/skills/memory_utils.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
def _parse_segment_llm_result(text):
|
| 4 |
+
import logging
|
| 5 |
+
lines = text.strip().split('\n')
|
| 6 |
+
key = None
|
| 7 |
+
nodes = {}
|
| 8 |
+
# print(lines)
|
| 9 |
+
for line in lines:
|
| 10 |
+
# print(line)
|
| 11 |
+
line = line.strip()
|
| 12 |
+
if len(line) == 0:
|
| 13 |
+
continue
|
| 14 |
+
if line.startswith('<<') and line.endswith('>>'):
|
| 15 |
+
key = line[2:-2]
|
| 16 |
+
else:
|
| 17 |
+
if key is None:
|
| 18 |
+
logging.warning(f'key is None, line: {line}')
|
| 19 |
+
continue
|
| 20 |
+
blocks = line.split(':')
|
| 21 |
+
if len(blocks) >= 2:
|
| 22 |
+
start = int(blocks[0])
|
| 23 |
+
end = int(blocks[1])
|
| 24 |
+
nodes[key] = (start, end)
|
| 25 |
+
return nodes
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
async def segment_text(text):
|
| 29 |
+
"""
|
| 30 |
+
将文本进行语义分段,返回分段后的文本和key组成的字典nodes
|
| 31 |
+
"""
|
| 32 |
+
from GeneralAgent import skills
|
| 33 |
+
from jinja2 import Template
|
| 34 |
+
segment_prompt = """
|
| 35 |
+
---------
|
| 36 |
+
{{text}}
|
| 37 |
+
---------
|
| 38 |
+
|
| 39 |
+
For the text enclosed by ---------, the number following # is the line number.
|
| 40 |
+
Your task is to divide the text into segments (up to 6), each represented by the start and end line numbers. Additionally, assign a brief title (not exceeding 10 words) to each segment.
|
| 41 |
+
The output format is as follows:
|
| 42 |
+
```
|
| 43 |
+
<<Title for Segment>>
|
| 44 |
+
Start_line: End_line
|
| 45 |
+
|
| 46 |
+
<<Title for Segment>>
|
| 47 |
+
Start_line: End_line
|
| 48 |
+
```
|
| 49 |
+
For instance:
|
| 50 |
+
```
|
| 51 |
+
<<Hello>>
|
| 52 |
+
0:12
|
| 53 |
+
|
| 54 |
+
<<World>>
|
| 55 |
+
13:20
|
| 56 |
+
```
|
| 57 |
+
Please note, each title should not exceed 10 words. Titles exceeding this limit will be considered invalid. Strive to keep your titles concise yet reflective of the main content in the segment.
|
| 58 |
+
"""
|
| 59 |
+
lines = text.strip().split('\n')
|
| 60 |
+
new_lines = []
|
| 61 |
+
for index in range(len(lines)):
|
| 62 |
+
new_lines.append(f'#{index} {lines[index]}')
|
| 63 |
+
new_text = '\n'.join(new_lines)
|
| 64 |
+
prompt = Template(segment_prompt).render({'text': new_text})
|
| 65 |
+
messages = [
|
| 66 |
+
{'role': 'system','content': 'You are a helpful assistant'},
|
| 67 |
+
{'role': 'user','content': prompt}
|
| 68 |
+
]
|
| 69 |
+
model_type='normal'
|
| 70 |
+
if skills.messages_token_count(messages) > 3500:
|
| 71 |
+
model_type = 'long'
|
| 72 |
+
result = await skills.async_llm_inference(messages, model_type)
|
| 73 |
+
# print(result)
|
| 74 |
+
nodes = _parse_segment_llm_result(result)
|
| 75 |
+
for key in nodes:
|
| 76 |
+
start, end = nodes[key]
|
| 77 |
+
nodes[key] = '\n'.join(lines[start:end])
|
| 78 |
+
return nodes
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
async def summarize_text(text):
|
| 82 |
+
from GeneralAgent import skills
|
| 83 |
+
prompt = "Please distill the content between --------- into a concise phrase or sentence that captures the essence without any introductory phrases."
|
| 84 |
+
# prompt = "请将---------之间的内容提炼成一个简洁的短语或句子,抓住要点,无需任何介绍性短语。"
|
| 85 |
+
messages = [
|
| 86 |
+
{'role': 'system','content': 'You are a helpful assistant'},
|
| 87 |
+
{'role': 'user','content': f'{prompt}.\n---------\n{text}\n---------'}
|
| 88 |
+
]
|
| 89 |
+
result = await skills.async_llm_inference(messages)
|
| 90 |
+
return result
|
| 91 |
+
|
| 92 |
+
async def extract_info(background, task):
|
| 93 |
+
prompt_template = """
|
| 94 |
+
Background (line number is indicated by #number, and <<title>> is a link to the details):
|
| 95 |
+
---------
|
| 96 |
+
{{background}}
|
| 97 |
+
---------
|
| 98 |
+
|
| 99 |
+
Task
|
| 100 |
+
---------
|
| 101 |
+
{{task}}
|
| 102 |
+
---------
|
| 103 |
+
|
| 104 |
+
Please provide the line numbers in the background that contain information relevant to solving the task.
|
| 105 |
+
Then, provide the <<titles>> that provide further details related to the background information.
|
| 106 |
+
The expected output format is as follows:
|
| 107 |
+
```
|
| 108 |
+
#Line Number 1
|
| 109 |
+
#Line Number 2
|
| 110 |
+
...
|
| 111 |
+
<<title 1>>
|
| 112 |
+
<<title 2>>
|
| 113 |
+
...
|
| 114 |
+
```
|
| 115 |
+
If no relevant information is found, please output "[Nothing]".
|
| 116 |
+
```
|
| 117 |
+
[Nothing]
|
| 118 |
+
```
|
| 119 |
+
"""
|
| 120 |
+
|
| 121 |
+
from GeneralAgent import skills
|
| 122 |
+
from jinja2 import Template
|
| 123 |
+
prompt = Template(prompt_template).render({'background': background, 'task': task})
|
| 124 |
+
messages = [
|
| 125 |
+
{'role': 'system','content': 'You are a helpful assistant'},
|
| 126 |
+
{'role': 'user','content': prompt}
|
| 127 |
+
]
|
| 128 |
+
result = await skills.async_llm_inference(messages)
|
| 129 |
+
return result
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def parse_extract_info(text):
|
| 133 |
+
import re
|
| 134 |
+
numbers = re.findall(r'#(\d+)', text)
|
| 135 |
+
numbers = [int(x) for x in numbers]
|
| 136 |
+
titles = re.findall(r'<<([^>>]+)>>', text)
|
| 137 |
+
return numbers, titles
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def extract_title(text):
|
| 141 |
+
"""
|
| 142 |
+
extract title from text
|
| 143 |
+
"""
|
| 144 |
+
if len(text) > 500:
|
| 145 |
+
text = text[:500]
|
| 146 |
+
prompt = "Please distill the content between --------- into a concise title of the content, less than five words.\n---------\n" + text + "\n---------"
|
| 147 |
+
from GeneralAgent import skills
|
| 148 |
+
messages = [
|
| 149 |
+
{'role': 'system','content': 'You are a helpful assistant'},
|
| 150 |
+
{'role': 'user','content': prompt}
|
| 151 |
+
]
|
| 152 |
+
result = skills.llm_inference(messages)
|
| 153 |
+
return result
|
GeneralAgent/skills/merge_video_audio/merge_video_audio.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def merge_video_audio(video_path:str, narration_path:str=None, music_path:str=None) -> str:
|
| 2 |
+
"""
|
| 3 |
+
Merge video, narration, and background music into a final video based on the shortest length among all elements.
|
| 4 |
+
Parameters: video_path -- path of the video file, string
|
| 5 |
+
narration_path -- path of the narration audio file, string, can be None
|
| 6 |
+
music_path -- path of the background music file, string, can be None
|
| 7 |
+
Returns: the path of the final video file, string
|
| 8 |
+
"""
|
| 9 |
+
from moviepy.editor import VideoFileClip, AudioFileClip, CompositeAudioClip
|
| 10 |
+
|
| 11 |
+
# Load video file
|
| 12 |
+
video = VideoFileClip(video_path)
|
| 13 |
+
|
| 14 |
+
# Load audio files if they exist
|
| 15 |
+
audio_clips = []
|
| 16 |
+
durations = [video.duration]
|
| 17 |
+
if narration_path is not None:
|
| 18 |
+
narration = AudioFileClip(narration_path)
|
| 19 |
+
audio_clips.append(narration)
|
| 20 |
+
durations.append(narration.duration)
|
| 21 |
+
if music_path is not None:
|
| 22 |
+
music = AudioFileClip(music_path)
|
| 23 |
+
audio_clips.append(music)
|
| 24 |
+
durations.append(music.duration)
|
| 25 |
+
|
| 26 |
+
# Determine base length
|
| 27 |
+
base_length = min(durations)
|
| 28 |
+
|
| 29 |
+
# Adjust lengths of elements
|
| 30 |
+
audio_clips = [clip.subclip(0, base_length) for clip in audio_clips]
|
| 31 |
+
|
| 32 |
+
# Merge audio files
|
| 33 |
+
if len(audio_clips) == 1:
|
| 34 |
+
final_audio = audio_clips[0]
|
| 35 |
+
elif audio_clips:
|
| 36 |
+
final_audio = CompositeAudioClip(audio_clips)
|
| 37 |
+
else:
|
| 38 |
+
final_audio = None
|
| 39 |
+
|
| 40 |
+
# Set audio of video file to final audio
|
| 41 |
+
final_video = video.subclip(0, base_length)
|
| 42 |
+
if final_audio is not None:
|
| 43 |
+
final_video = final_video.set_audio(final_audio)
|
| 44 |
+
|
| 45 |
+
# Save final video file
|
| 46 |
+
from GeneralAgent import skills
|
| 47 |
+
final_video_path = skills.unique_name() + ".mp4"
|
| 48 |
+
final_video.write_videofile(final_video_path)
|
| 49 |
+
|
| 50 |
+
return final_video_path
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def test_merge_video_audio():
|
| 54 |
+
"""
|
| 55 |
+
Test merge_video_audio function
|
| 56 |
+
"""
|
| 57 |
+
import os
|
| 58 |
+
video_path = os.path.join(os.path.dirname(__file__), "video.mp4")
|
| 59 |
+
narration_path = os.path.join(os.path.dirname(__file__), "narration.mp3")
|
| 60 |
+
music_path = os.path.join(os.path.dirname(__file__), "music.wav")
|
| 61 |
+
final_video_path = merge_video_audio(video_path, narration_path, music_path)
|
| 62 |
+
assert os.path.exists(final_video_path)
|
| 63 |
+
os.remove(final_video_path)
|
| 64 |
+
# os.remove("./final_audio.mp3")
|
| 65 |
+
|
| 66 |
+
if __name__ == '__main__':
|
| 67 |
+
test_merge_video_audio()
|
GeneralAgent/skills/merge_video_audio/music.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:27e302b1c36a4feb0a89f31f1cc7981d20356305396edf08fada7c36d57ed3a7
|
| 3 |
+
size 3840078
|
GeneralAgent/skills/merge_video_audio/narration.mp3
ADDED
|
Binary file (134 kB). View file
|
|
|
GeneralAgent/skills/merge_video_audio/tmp_audio.mp3
ADDED
|
Binary file (237 Bytes). View file
|
|
|
GeneralAgent/skills/merge_video_audio/video.mp4
ADDED
|
Binary file (249 kB). View file
|
|
|
GeneralAgent/skills/musicgen/generate_music.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def generate_music(prompt: str, model_version: str = 'stereo-melody-large', duration: int = 8, output_format: str = 'wav') -> str:
|
| 2 |
+
"""
|
| 3 |
+
Generate music according to the prompt and return the music file path. prompt should be english.
|
| 4 |
+
@param prompt: A description of the music you want to generate.
|
| 5 |
+
@param model_version: Model to use for generation. Default is 'stereo-melody-large'.
|
| 6 |
+
@param duration: Duration of the generated audio in seconds. Default is 8.
|
| 7 |
+
@param output_format: Output format for generated audio. Default is 'wav'.
|
| 8 |
+
"""
|
| 9 |
+
import replicate
|
| 10 |
+
from GeneralAgent import skills
|
| 11 |
+
|
| 12 |
+
output = replicate.run(
|
| 13 |
+
"meta/musicgen:7be0f12c54a8d033a0fbd14418c9af98962da9a86f5ff7811f9b3423a1f0b7d7",
|
| 14 |
+
input={
|
| 15 |
+
"model_version": model_version,
|
| 16 |
+
"prompt": prompt,
|
| 17 |
+
"duration": duration,
|
| 18 |
+
"output_format": output_format
|
| 19 |
+
}
|
| 20 |
+
)
|
| 21 |
+
music_url = output
|
| 22 |
+
music_path = skills.try_download_file(music_url)
|
| 23 |
+
print(f'Music created at {music_path}')
|
| 24 |
+
return music_path
|
| 25 |
+
|
| 26 |
+
def test_generate_music():
|
| 27 |
+
import os
|
| 28 |
+
music_path = generate_music('Happy birthday song', 'stereo-melody-large', 10, 'mp3')
|
| 29 |
+
assert os.path.exists(music_path), "Music file does not exist."
|
| 30 |
+
|
| 31 |
+
if __name__ == '__main__':
|
| 32 |
+
test_generate_music()
|
GeneralAgent/skills/python_envs.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
def get_current_env_python_libs() -> str:
|
| 3 |
+
"""
|
| 4 |
+
Return the python libs that installed in current env
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
requirements_path = os.path.join(os.path.dirname(__file__), '../requirements.txt')
|
| 8 |
+
with open(requirements_path, 'r', encoding='utf-8') as f:
|
| 9 |
+
requirements = f.read()
|
| 10 |
+
requirements = requirements.replace('\n', ' ')
|
| 11 |
+
return requirements.strip()
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def get_python_version() -> str:
|
| 15 |
+
"""
|
| 16 |
+
Return the python version, like "3.9.12"
|
| 17 |
+
"""
|
| 18 |
+
import platform
|
| 19 |
+
python_version = platform.python_version()
|
| 20 |
+
return python_version
|
| 21 |
+
|
| 22 |
+
def get_os_version() -> str:
|
| 23 |
+
import platform
|
| 24 |
+
system = platform.system()
|
| 25 |
+
if system == 'Windows':
|
| 26 |
+
version = platform.version()
|
| 27 |
+
return f"Windows version: {version}"
|
| 28 |
+
elif system == 'Darwin':
|
| 29 |
+
version = platform.mac_ver()[0]
|
| 30 |
+
return f"macOS version: {version}"
|
| 31 |
+
elif system == 'Linux':
|
| 32 |
+
version = platform.platform()
|
| 33 |
+
return f"Linux version: {version}"
|
| 34 |
+
else:
|
| 35 |
+
return "Unknown system"
|
| 36 |
+
|
| 37 |
+
def get_python_code(content:str) -> str:
|
| 38 |
+
"""
|
| 39 |
+
Return the python code text from content
|
| 40 |
+
"""
|
| 41 |
+
template = '```python\n(.*?)\n```'
|
| 42 |
+
import re
|
| 43 |
+
code = re.findall(template, content, re.S)
|
| 44 |
+
if len(code) > 0:
|
| 45 |
+
return code[0]
|
| 46 |
+
else:
|
| 47 |
+
return content
|
| 48 |
+
|
| 49 |
+
def test_get_python_code():
|
| 50 |
+
content = """
|
| 51 |
+
```python
|
| 52 |
+
import os
|
| 53 |
+
print(os.getcwd())
|
| 54 |
+
```
|
| 55 |
+
"""
|
| 56 |
+
assert get_python_code(content) == 'import os\nprint(os.getcwd())'
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def load_functions_with_path(python_code_path):
|
| 60 |
+
try:
|
| 61 |
+
import importlib.util
|
| 62 |
+
import inspect
|
| 63 |
+
|
| 64 |
+
# 指定要加载的文件路径和文件名
|
| 65 |
+
module_name = 'skills'
|
| 66 |
+
module_file = python_code_path
|
| 67 |
+
|
| 68 |
+
# 使用importlib加载文件
|
| 69 |
+
spec = importlib.util.spec_from_file_location(module_name, module_file)
|
| 70 |
+
module = importlib.util.module_from_spec(spec)
|
| 71 |
+
spec.loader.exec_module(module)
|
| 72 |
+
|
| 73 |
+
# 获取文件中的所有函数
|
| 74 |
+
functions = inspect.getmembers(module, inspect.isfunction)
|
| 75 |
+
|
| 76 |
+
# 过滤functions中以下划线开头的函数
|
| 77 |
+
functions = filter(lambda f: not f[0].startswith('_'), functions)
|
| 78 |
+
|
| 79 |
+
return [f[1] for f in functions], None
|
| 80 |
+
except Exception as e:
|
| 81 |
+
# 代码可能有错误,加载不起来
|
| 82 |
+
import logging
|
| 83 |
+
logging.exception(e)
|
| 84 |
+
return [], str(e)
|
| 85 |
+
|
| 86 |
+
def get_function_signature(func, module:str=None):
|
| 87 |
+
"""Returns a description string of function"""
|
| 88 |
+
import inspect
|
| 89 |
+
sig = inspect.signature(func)
|
| 90 |
+
sig_str = str(sig)
|
| 91 |
+
desc = f"{func.__name__}{sig_str}"
|
| 92 |
+
if func.__doc__:
|
| 93 |
+
desc += ': ' + func.__doc__.strip()
|
| 94 |
+
if module is not None:
|
| 95 |
+
desc = f'{module}.{desc}'
|
| 96 |
+
if inspect.iscoroutinefunction(func):
|
| 97 |
+
desc = "async " + desc
|
| 98 |
+
return desc
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def python_line_is_variable_expression(line):
|
| 102 |
+
"""
|
| 103 |
+
Return True if line is a variable expression, else False
|
| 104 |
+
"""
|
| 105 |
+
import ast
|
| 106 |
+
try:
|
| 107 |
+
tree = ast.parse(line)
|
| 108 |
+
except SyntaxError:
|
| 109 |
+
return False
|
| 110 |
+
|
| 111 |
+
if len(tree.body) != 1 or not isinstance(tree.body[0], ast.Expr):
|
| 112 |
+
return False
|
| 113 |
+
|
| 114 |
+
expr = tree.body[0].value
|
| 115 |
+
if isinstance(expr, ast.Call):
|
| 116 |
+
return False
|
| 117 |
+
|
| 118 |
+
return True
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def test_python_line_is_variable_expression():
|
| 122 |
+
assert python_line_is_variable_expression('a')
|
| 123 |
+
assert python_line_is_variable_expression('a, b')
|
| 124 |
+
assert python_line_is_variable_expression('a + b')
|
| 125 |
+
assert python_line_is_variable_expression('vars[0]')
|
| 126 |
+
assert python_line_is_variable_expression('scrape_web("https://www.baidu.com")[0]')
|
| 127 |
+
|
| 128 |
+
assert python_line_is_variable_expression(' vars[0]') is False
|
| 129 |
+
assert python_line_is_variable_expression('print(a)') is False
|
| 130 |
+
assert python_line_is_variable_expression('x = a + b') is False
|
GeneralAgent/skills/replicate_api.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# replicate api
|
| 2 |
+
|
| 3 |
+
def _replicate_image_generation(prompt):
|
| 4 |
+
"""generate a image with prompt (in english), return a image url"""
|
| 5 |
+
|
| 6 |
+
# import replicate
|
| 7 |
+
# output = replicate.run(
|
| 8 |
+
# "cjwbw/taiyi-stable-diffusion-1b-chinese-v0.1:36c580142e9fbbd52e5e678e30541c0da6f2021c9d2039a5c00be192a010e8c5",
|
| 9 |
+
# input={"prompt": prompt}
|
| 10 |
+
# )
|
| 11 |
+
# print(output)
|
| 12 |
+
|
| 13 |
+
# 图片生成切换成为sdxl的version 1 版本
|
| 14 |
+
import replicate
|
| 15 |
+
|
| 16 |
+
output = replicate.run(
|
| 17 |
+
# "stability-ai/stable-diffusion:ac732df83cea7fff18b8472768c88ad041fa750ff7682a21affe81863cbe77e4",
|
| 18 |
+
"stability-ai/sdxl:2f779eb9b23b34fe171f8eaa021b8261566f0d2c10cd2674063e7dbcd351509e",
|
| 19 |
+
input={"prompt": prompt}
|
| 20 |
+
)
|
| 21 |
+
image_url = output[0]
|
| 22 |
+
# print(image_url)
|
| 23 |
+
|
| 24 |
+
return image_url
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def image_generation(prompt) -> str:
|
| 28 |
+
"""
|
| 29 |
+
Generate an image according to the prompt and return the image path. For example, when the prompt is "apple" you will get an image of an apple. Note: The prompt should describe objective things in detail, not abstract concepts. For example, if you want to draw a picture of Chengdu, the prompt should be "Picture of Chengdu, with giant pandas playing in the bamboo forest, people eating hot pot around, and a Jinsha Sunbird totem next to it" instead of "Draw a picture of Chengdu" "painting"
|
| 30 |
+
@param prompt: The prompt should be detailed enough to describe the image. Tips can be in any type of language, but English is recommended.
|
| 31 |
+
"""
|
| 32 |
+
from GeneralAgent import skills
|
| 33 |
+
if not skills.text_is_english(prompt):
|
| 34 |
+
prompt = skills.translate_text(prompt, 'english')
|
| 35 |
+
image_url = _replicate_image_generation(prompt)
|
| 36 |
+
image_path = skills.try_download_file(image_url)
|
| 37 |
+
print(f'image created at ')
|
| 38 |
+
return image_path
|
| 39 |
+
|
| 40 |
+
def face_restoration(image_path):
|
| 41 |
+
""" Practical face restoration algorithm for old photos or AI-generated faces. input image path, and return the new image path"""
|
| 42 |
+
import replicate
|
| 43 |
+
from GeneralAgent import skills
|
| 44 |
+
image_url = replicate.run(
|
| 45 |
+
"tencentarc/gfpgan:9283608cc6b7be6b65a8e44983db012355fde4132009bf99d976b2f0896856a3",
|
| 46 |
+
input={"img": open(image_path, "rb")}
|
| 47 |
+
)
|
| 48 |
+
new_image_path = skills.try_download_file(image_url)
|
| 49 |
+
return new_image_path
|
| 50 |
+
|
| 51 |
+
def qrcode_stable_diffusion(prompt, qr_code_content):
|
| 52 |
+
"""generate a qrcode image with prompt, return a image url"""
|
| 53 |
+
import replicate
|
| 54 |
+
output = replicate.run(
|
| 55 |
+
"nateraw/qrcode-stable-diffusion:9cdabf8f8a991351960c7ce2105de2909514b40bd27ac202dba57935b07d29d4",
|
| 56 |
+
input={"prompt": prompt, 'qr_code_content': qr_code_content}
|
| 57 |
+
)
|
| 58 |
+
return output[0]
|
| 59 |
+
|
| 60 |
+
def speech_to_text(audio_file_path):
|
| 61 |
+
"""Convert speech in audio to text, return a text and the language of the text"""
|
| 62 |
+
import replicate
|
| 63 |
+
output = replicate.run(
|
| 64 |
+
"openai/whisper:91ee9c0c3df30478510ff8c8a3a545add1ad0259ad3a9f78fba57fbc05ee64f7",
|
| 65 |
+
input={"audio": open(audio_file_path, "rb")}
|
| 66 |
+
)
|
| 67 |
+
print(output)
|
| 68 |
+
language = output['detected_language']
|
| 69 |
+
text = output['transcription']
|
| 70 |
+
|
| 71 |
+
return text, language
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
if __name__ == '__main__':
|
| 75 |
+
image_generation('a cat')
|
GeneralAgent/skills/scrape_dynamic_web.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def scrape_dynamic_web(url):
|
| 2 |
+
"""
|
| 3 |
+
This function takes a url and returns the text content of the web page.
|
| 4 |
+
It uses selenium to load the dynamic content of the page, and BeautifulSoup to parse the HTML and extract the text.
|
| 5 |
+
It also replaces <span> tags with their text content, and <br> tags with newline characters.
|
| 6 |
+
"""
|
| 7 |
+
from selenium import webdriver
|
| 8 |
+
from selenium.webdriver.chrome.service import Service
|
| 9 |
+
from webdriver_manager.chrome import ChromeDriverManager
|
| 10 |
+
from selenium.webdriver.common.by import By
|
| 11 |
+
from selenium.webdriver.chrome.options import Options
|
| 12 |
+
from selenium.webdriver.common.action_chains import ActionChains
|
| 13 |
+
from selenium.webdriver.support.ui import WebDriverWait
|
| 14 |
+
from selenium.webdriver.support import expected_conditions as EC
|
| 15 |
+
from selenium.webdriver.common.by import By
|
| 16 |
+
from bs4 import BeautifulSoup
|
| 17 |
+
import time
|
| 18 |
+
import re
|
| 19 |
+
from urllib.parse import urljoin
|
| 20 |
+
|
| 21 |
+
# Setup chrome options
|
| 22 |
+
chrome_options = Options()
|
| 23 |
+
chrome_options.add_argument("--headless") # Ensure GUI is off
|
| 24 |
+
chrome_options.add_argument("--no-sandbox")
|
| 25 |
+
chrome_options.add_argument("--disable-dev-shm-usage")
|
| 26 |
+
|
| 27 |
+
# Set path to chromedriver as per your configuration
|
| 28 |
+
webdriver_service = Service(ChromeDriverManager().install())
|
| 29 |
+
|
| 30 |
+
# Choose Chrome Browser
|
| 31 |
+
driver = webdriver.Chrome(service=webdriver_service, options=chrome_options)
|
| 32 |
+
driver.get(url)
|
| 33 |
+
|
| 34 |
+
# Wait for the dynamic content to load
|
| 35 |
+
WebDriverWait(driver, 20).until(EC.presence_of_element_located((By.TAG_NAME, 'body')))
|
| 36 |
+
|
| 37 |
+
html = driver.page_source
|
| 38 |
+
driver.quit()
|
| 39 |
+
|
| 40 |
+
# Parse html content
|
| 41 |
+
soup = BeautifulSoup(html, "html.parser")
|
| 42 |
+
for span in soup.find_all("span"):
|
| 43 |
+
span.replace_with(span.text)
|
| 44 |
+
for a in soup.find_all("a"):
|
| 45 |
+
href = urljoin(url, a.get('href'))
|
| 46 |
+
a.replace_with(f"[{a.text}]({href})")
|
| 47 |
+
for br in soup.find_all("br"):
|
| 48 |
+
br.replace_with("\n")
|
| 49 |
+
text = soup.get_text(separator="\n")
|
| 50 |
+
|
| 51 |
+
# Replace multiple newlines and spaces around them with a single newline
|
| 52 |
+
text = re.sub('\s*\n\s*', '\n', text)
|
| 53 |
+
|
| 54 |
+
# Collapse whitespace
|
| 55 |
+
text = ' '.join(text.split())
|
| 56 |
+
|
| 57 |
+
return text
|
| 58 |
+
|
| 59 |
+
def test_scrape_dynamic_web():
|
| 60 |
+
"""
|
| 61 |
+
This function tests the scrape_dynamic_web function.
|
| 62 |
+
It asserts that the returned text contains the string 'replicate'.
|
| 63 |
+
"""
|
| 64 |
+
url = "https://replicate.com/stability-ai/stable-video-diffusion/api?tab=python"
|
| 65 |
+
text = scrape_dynamic_web(url)
|
| 66 |
+
assert 'replicate' in text
|
| 67 |
+
print(text)
|
| 68 |
+
|
| 69 |
+
if __name__ == '__main__':
|
| 70 |
+
test_scrape_dynamic_web()
|
GeneralAgent/skills/split_text.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
def split_text(text, max_token=3000, separators='\n'):
|
| 4 |
+
"""
|
| 5 |
+
Split the text into paragraphs, each paragraph has less than max_token tokens.
|
| 6 |
+
"""
|
| 7 |
+
import re
|
| 8 |
+
from GeneralAgent import skills
|
| 9 |
+
pattern = "[" + re.escape(separators) + "]"
|
| 10 |
+
paragraphs = list(re.split(pattern, text))
|
| 11 |
+
print(len(paragraphs))
|
| 12 |
+
result = []
|
| 13 |
+
current = ''
|
| 14 |
+
for paragraph in paragraphs:
|
| 15 |
+
if skills.string_token_count(current) + skills.string_token_count(paragraph) > max_token:
|
| 16 |
+
result.append(current)
|
| 17 |
+
current = ''
|
| 18 |
+
current += paragraph + '\n'
|
| 19 |
+
if len(current) > 0:
|
| 20 |
+
result.append(current)
|
| 21 |
+
new_result = []
|
| 22 |
+
for x in result:
|
| 23 |
+
if skills.string_token_count(x) > max_token:
|
| 24 |
+
new_result.extend(split_text(x, max_token=max_token, separators=",。,.;;"))
|
| 25 |
+
else:
|
| 26 |
+
new_result.append(x)
|
| 27 |
+
new_result = [x.strip() for x in new_result if len(x.strip()) > 0]
|
| 28 |
+
return new_result
|
GeneralAgent/skills/stable_video_diffusion/dab774a452f3.jpg
ADDED
|