From 15521d13372b51f0a8ccc40e8d1a721bdee403db Mon Sep 17 00:00:00 2001 From: Zhicheng Zhang Date: Mon, 24 Jun 2024 14:35:37 +0800 Subject: [PATCH] Feat/tool service with git (#500) --- .dev_scripts/dockerci.sh | 2 +- .gitignore | 1 + apps/agentfabric/appBot.py | 27 +++-- apps/agentfabric/config_utils.py | 7 +- apps/agentfabric/requirements.txt | 2 +- apps/agentfabric/server.py | 60 +++++++++--- apps/agentfabric/server_utils.py | 22 +---- apps/agentfabric/user_core.py | 23 +++-- apps/agentfabric/version.py | 2 +- build.sh | 4 - docker/tool_node.dockerfile | 4 +- modelscope_agent/agent.py | 41 +++++--- modelscope_agent/agent_env_util.py | 2 +- modelscope_agent/agents/multi_role_play.py | 3 - modelscope_agent/agents/role_play.py | 3 - modelscope_agent/agents_registry.py | 2 +- modelscope_agent/constants.py | 2 + modelscope_agent/llm/dashscope.py | 4 - modelscope_agent/memory/memory_with_rag.py | 2 + .../multi_agents_utils/executors/ray.py | 9 +- modelscope_agent/rag/__init__.py | 5 + modelscope_agent/rag/knowledge.py | 3 - modelscope_agent/rag/llm.py | 1 - modelscope_agent/tools/__init__.py | 1 + modelscope_agent/tools/base.py | 75 +++++++++++--- .../code_interpreter/code_interpreter.py | 9 +- .../tools/modelscope_tools/pipeline_tool.py | 4 +- modelscope_agent/utils/git.py | 25 +++++ modelscope_agent/utils/nltk/stopwords.zip | Bin 0 -> 34276 bytes modelscope_agent/utils/nltk_utils.py | 15 +++ .../tool_manager_server/api.py | 92 ++++++++++++------ .../tool_manager_server/models.py | 2 + .../tool_manager_server/sandbox.py | 9 +- requirements.txt | 4 +- scripts/run_tool_node.sh | 57 +++++++++++ tests/utils/test_git_clone.py | 44 +++++++++ 36 files changed, 431 insertions(+), 137 deletions(-) delete mode 100644 build.sh create mode 100644 modelscope_agent/utils/git.py create mode 100644 modelscope_agent/utils/nltk/stopwords.zip create mode 100644 scripts/run_tool_node.sh create mode 100644 tests/utils/test_git_clone.py diff --git a/.dev_scripts/dockerci.sh b/.dev_scripts/dockerci.sh index 30822de01..e16fe10c7 100644 --- a/.dev_scripts/dockerci.sh +++ b/.dev_scripts/dockerci.sh @@ -15,7 +15,7 @@ pip install playwright playwright install --with-deps chromium # install package -pip install fastapi pydantic uvicorn docker sqlmodel +pip install fastapi pydantic uvicorn docker sqlmodel transformers ray # run ci pytest tests diff --git a/.gitignore b/.gitignore index 0e15057dd..78bf2763e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ MANIFEST *.manifest *.spec release.sh +build.sh *.html # Installer logs diff --git a/apps/agentfabric/appBot.py b/apps/agentfabric/appBot.py index b14c0137a..1336fffbd 100644 --- a/apps/agentfabric/appBot.py +++ b/apps/agentfabric/appBot.py @@ -11,6 +11,7 @@ import modelscope_studio as mgr from config_utils import get_avatar_image, get_ci_dir, parse_configuration from gradio_utils import format_cover_html +from modelscope_agent.constants import ApiNames from modelscope_agent.schemas import Message from modelscope_agent.utils.logger import agent_logger as logger from modelscope_studio.components.Chatbot.llm_thinking_presets import qwen @@ -42,10 +43,13 @@ def check_uuid(uuid_str): return uuid_str -def init_user(state): +def init_user(state, _user_token=None): try: + in_ms_studio = os.getenv('MODELSCOPE_ENVIRONMENT', 'None') == 'studio' seed = state.get('session_seed', random.randint(0, 1000000000)) - user_agent, user_memory = init_user_chatbot_agent(uuid_str) + # use tool api in ms studio + user_agent, user_memory = init_user_chatbot_agent( + uuid_str, use_tool_api=in_ms_studio, user_token=_user_token) user_agent.seed = seed state['user_agent'] = user_agent state['user_memory'] = user_memory @@ -72,6 +76,7 @@ def delete(state): # 创建 Gradio 界面 demo = gr.Blocks(css='assets/appBot.css', theme=customTheme) with demo: + user_token = gr.Textbox(label='modelscope_agent_tool_token', visible=False) gr.Markdown( '#
\N{fire} AgentFabric powered by Modelscope-agent [github star](https://github.com/modelscope/modelscope-agent/tree/main)
' # noqa E501 ) @@ -111,10 +116,16 @@ def delete(state): examples=suggests, inputs=[user_chatbot_input]) - def send_message(chatbot, input, _state): + def send_message(chatbot, input, _state, _user_token): # 将发送的消息添加到聊天历史 if 'user_agent' not in _state: - init_user(_state) + init_user(_state, _user_token) + + kwargs = { + name.lower(): os.getenv(value.value) + for name, value in ApiNames.__members__.items() + } + # 将发送的消息添加到聊天历史 _uuid_str = check_uuid(uuid_str) user_agent = _state['user_agent'] @@ -149,7 +160,9 @@ def send_message(chatbot, input, _state): input.text, history=history, ref_doc=ref_doc, - append_files=append_files): + append_files=append_files, + user_token=_user_token, + **kwargs): # important! do not change this response += frame @@ -178,10 +191,10 @@ def send_message(chatbot, input, _state): gr.on([user_chatbot_input.submit], fn=send_message, - inputs=[user_chatbot, user_chatbot_input, state], + inputs=[user_chatbot, user_chatbot_input, state, user_token], outputs=[user_chatbot, user_chatbot_input]) - demo.load(init_user, inputs=[state], outputs=[state]) + demo.load(init_user, inputs=[state, user_token], outputs=[state]) demo.queue() demo.launch(show_error=True, max_threads=10) diff --git a/apps/agentfabric/config_utils.py b/apps/agentfabric/config_utils.py index a5d07cab7..9929ad184 100644 --- a/apps/agentfabric/config_utils.py +++ b/apps/agentfabric/config_utils.py @@ -158,9 +158,14 @@ def parse_configuration(uuid_str=''): tools_info = builder_cfg.tools available_tool_list = [] for key, value in tools_info.items(): + if key in tool_cfg: + tool_cfg[key]['use'] = value['use'] + else: + # for tool hub only + if '/' in key: + tool_cfg[key] = value if value['use']: available_tool_list.append(key) - tool_cfg[key]['use'] = value['use'] openapi_plugin_file = get_user_openapi_plugin_cfg_file(uuid_str) plugin_cfg = {} diff --git a/apps/agentfabric/requirements.txt b/apps/agentfabric/requirements.txt index 4f8db879d..974962029 100644 --- a/apps/agentfabric/requirements.txt +++ b/apps/agentfabric/requirements.txt @@ -1,10 +1,10 @@ dashscope faiss-cpu gradio==4.36.1 +https://modelscope-agent.oss-cn-hangzhou.aliyuncs.com/releases/v0.6.2/modelscope_agent-0.6.2-py3-none-any.whl langchain markdown-cjk-spacing mdx_truly_sane_lists -modelscope-agent==0.4.1 modelscope_studio pymdown-extensions python-slugify diff --git a/apps/agentfabric/server.py b/apps/agentfabric/server.py index 7c6034a31..09ffc0a37 100644 --- a/apps/agentfabric/server.py +++ b/apps/agentfabric/server.py @@ -15,8 +15,10 @@ is_valid_plugin_configuration, parse_configuration, save_builder_configuration, save_plugin_configuration) -from flask import (Flask, Response, jsonify, make_response, request, +from flask import (Flask, Response, g, jsonify, make_response, request, send_from_directory) +from modelscope_agent.constants import (MODELSCOPE_AGENT_TOKEN_HEADER_NAME, + ApiNames) from modelscope_agent.schemas import Message from publish_util import (pop_user_info_from_config, prepare_agent_zip, reload_agent_dir) @@ -29,6 +31,28 @@ app.session_manager = SessionManager() +def get_auth_token(): + auth_header = request.headers.get('Authorization') + if auth_header and auth_header.startswith('Bearer '): + return auth_header[7:] # Slice off the 'Bearer ' prefix + return None + + +def get_modelscope_agent_token(): + token = None + # Check if authorization header is present + if MODELSCOPE_AGENT_TOKEN_HEADER_NAME in request.headers: + auth_header = request.headers[MODELSCOPE_AGENT_TOKEN_HEADER_NAME] + # Check if the value of the header starts with 'Bearer' + if auth_header.startswith('Bearer '): + # Extract the token part from 'Bearer token_value' + token = auth_header[7:] + else: + # Extract the token part from auth_header + token = auth_header + return token + + @app.before_request def set_request_id(): request_id = request.headers.get('X-Modelscope-Request-Id', 'unknown') @@ -94,8 +118,6 @@ def generate(): llm_result = frame.get('llm_text', '') exec_result = frame.get('exec_result', '') step_result = frame.get('step', '') - logger.info('frame, {}'.format( - str(frame).replace('\n', '\\n'))) if len(exec_result) != 0: if isinstance(exec_result, dict): exec_result = exec_result['result'] @@ -285,14 +307,6 @@ def save_builder_config(uuid_str): builder_config_str = request.form.get('builder_config') logger.info(f'builder_config: {builder_config_str}') builder_config = json.loads(builder_config_str) - if 'tools' in builder_config: - if 'code_interpreter' in builder_config['tools']: - return jsonify({ - 'success': False, - 'status': 404, - 'message': 'Using code_interpreter.', - 'request_id': request_id_var.get('') - }), 404 if 'knowledge' in builder_config: builder_config['knowledge'] = [ os.path.join(get_user_dir(uuid_str), os.path.basename(k)) @@ -367,6 +381,11 @@ def preview_publish_get_zip(uuid_str): @app.route('/preview/chat//', methods=['POST']) @with_request_id def preview_chat(uuid_str, session_str): + user_token = get_modelscope_agent_token() + if not user_token: + # If token is not found, return 401 Unauthorized response + return jsonify({'message': 'Token is missing!'}), 401 + logger.info(f'preview_chat: uuid_str_{uuid_str}_session_str_{session_str}') params_str = request.form.get('params') @@ -383,13 +402,18 @@ def preview_chat(uuid_str, session_str): file.save(file_path) file_paths.append(file_path) logger.info(f'/preview/chat/{uuid_str}/{session_str}: files: {file_paths}') + # Generating the kwargs dictionary + kwargs = { + name.lower(): os.getenv(value.value) + for name, value in ApiNames.__members__.items() + } def generate(): try: start_time = time.time() seed = random.randint(0, 1000000000) user_agent, user_memory = app.session_manager.get_user_bot( - uuid_str, session_str) + uuid_str, session_str, user_token=user_token) user_agent.seed = seed logger.info( f'get method: time consumed {time.time() - start_time}') @@ -422,12 +446,15 @@ def generate(): 'request_id': request_id_var.get('') }, ensure_ascii=False) + for frame in user_agent.run( input_content, history=history, ref_doc=ref_doc, append_files=file_paths, - uuid_str=uuid_str): + uuid_str=uuid_str, + user_token=user_token, + **kwargs): logger.info('frame, {}'.format( str(frame).replace('\n', '\\n'))) # important! do not change this @@ -486,8 +513,13 @@ def get_preview_chat_history(uuid_str, session_str): logger.info( f'get_preview_chat_history: uuid_str_{uuid_str}_session_str_{session_str}' ) + user_token = get_modelscope_agent_token() + if not user_token: + # If token is not found, return 401 Unauthorized response + return jsonify({'message': 'Token is missing!'}), 401 - _, user_memory = app.session_manager.get_user_bot(uuid_str, session_str) + _, user_memory = app.session_manager.get_user_bot( + uuid_str, session_str, user_token=user_token) return jsonify({ 'history': user_memory.get_history(), 'success': True, diff --git a/apps/agentfabric/server_utils.py b/apps/agentfabric/server_utils.py index 98e299ebe..1abc8e7fc 100644 --- a/apps/agentfabric/server_utils.py +++ b/apps/agentfabric/server_utils.py @@ -137,29 +137,17 @@ def get_user_bot( self, builder_id, session, - renew=False) -> Tuple[RolePlay, MemoryWithRetrievalKnowledge]: + renew=False, + user_token=None) -> Tuple[RolePlay, MemoryWithRetrievalKnowledge]: unique_id = builder_id + '_' + session user_agent = self.user_bots[unique_id] if renew or user_agent is None: logger.info(f'init_user_chatbot_agent: {builder_id} {session}') - # check code_interpreter builder_cfg, _, tool_cfg, _, _, _ = parse_configuration(builder_id) - if 'tools' in builder_cfg and 'code_interpreter' in builder_cfg[ - 'tools']: - if builder_cfg['tools']['code_interpreter'].get( - 'is_active', False - ) and builder_cfg['tools']['code_interpreter'].get( - 'use', False): - raise ValueError('Using code interpreter.') - if 'code_interpreter' in tool_cfg: - if tool_cfg['code_interpreter'].get( - 'is_active', - False) and tool_cfg['code_interpreter'].get( - 'use', False): - raise ValueError('Using code interpreter.') - - user_agent = init_user_chatbot_agent(builder_id, session) + + user_agent = init_user_chatbot_agent( + builder_id, session, use_tool_api=True, user_token=user_token) self.user_bots[unique_id] = user_agent return user_agent diff --git a/apps/agentfabric/user_core.py b/apps/agentfabric/user_core.py index 89d70def8..db2d3e680 100644 --- a/apps/agentfabric/user_core.py +++ b/apps/agentfabric/user_core.py @@ -12,7 +12,10 @@ # init user chatbot_agent -def init_user_chatbot_agent(uuid_str='', session='default'): +def init_user_chatbot_agent(uuid_str='', + session='default', + use_tool_api=False, + user_token=None): builder_cfg, model_cfg, tool_cfg, _, plugin_cfg, _ = parse_configuration( uuid_str) # set top_p and stop_words for role play @@ -21,17 +24,18 @@ def init_user_chatbot_agent(uuid_str='', session='default'): model_cfg[builder_cfg.model]['generate_cfg']['top_p'] = 0.5 model_cfg[builder_cfg.model]['generate_cfg']['stop'] = 'Observation' - # build model - logger.query_info( - uuid=uuid_str, - message=f'using model {builder_cfg.model}', - details={'model_config': model_cfg[builder_cfg.model]}) - # update function_list function_list = parse_tool_cfg(tool_cfg) function_list = add_openapi_plugin_to_additional_tool( plugin_cfg, function_list) + # build model + logger.query_info( + uuid=uuid_str, + message= + f'using model {builder_cfg.model} with tool {tool_cfg} and function list {function_list}', + details={'model_config': model_cfg[builder_cfg.model]}) + llm_config = copy.deepcopy(model_cfg[builder_cfg.model]) llm_config['model_server'] = llm_config.pop('type') instruction = { @@ -43,7 +47,10 @@ def init_user_chatbot_agent(uuid_str='', session='default'): function_list=function_list, llm=llm_config, instruction=instruction, - uuid_str=uuid_str) + uuid_str=uuid_str, + use_tool_api=use_tool_api, + user_token=user_token, + ) # build memory preview_history_dir = get_user_preview_history_dir(uuid_str, session) diff --git a/apps/agentfabric/version.py b/apps/agentfabric/version.py index c95a2a5da..d7cf31934 100644 --- a/apps/agentfabric/version.py +++ b/apps/agentfabric/version.py @@ -1 +1 @@ -__version__ = '0.2.1rc0' +__version__ = '0.3.0rc0' diff --git a/build.sh b/build.sh deleted file mode 100644 index 5e4c7c9cc..000000000 --- a/build.sh +++ /dev/null @@ -1,4 +0,0 @@ -tag=$(date +"%Y-%m-%d")-$(git rev-parse HEAD | cut -c1-6) -git apply ssrf.patch -sudo docker build . -f docker/dockerfile.agentfabric -t mshub-registry.cn-zhangjiakou.cr.aliyuncs.com/modelscope-repo/agent-fabric:${tag} -sudo docker push mshub-registry.cn-zhangjiakou.cr.aliyuncs.com/modelscope-repo/agent-fabric:${tag} diff --git a/docker/tool_node.dockerfile b/docker/tool_node.dockerfile index d75304341..bd4d26965 100644 --- a/docker/tool_node.dockerfile +++ b/docker/tool_node.dockerfile @@ -39,7 +39,9 @@ ENV BASE_TOOL_DIR /app/assets # install tool_node COPY modelscope_agent_servers /app/modelscope_agent_servers - +# start up script file +COPY scripts/run_tool_node.sh /app/run_tool_node.sh +RUN chmod +x /app/run_tool_node.sh #ENTRYPOINT exec uvicorn tool_service.tool_node.api:app --host 0.0.0.0 --port $PORT diff --git a/modelscope_agent/agent.py b/modelscope_agent/agent.py index 08d134b67..67a3f1237 100644 --- a/modelscope_agent/agent.py +++ b/modelscope_agent/agent.py @@ -1,3 +1,4 @@ +import os from abc import ABC, abstractmethod from typing import Dict, Iterator, List, Optional, Tuple, Union @@ -50,7 +51,7 @@ def __init__(self, self.function_map = {} if function_list: for function in function_list: - self._register_tool(function) + self._register_tool(function, **kwargs) self.storage_path = storage_path self.mem = None @@ -97,7 +98,8 @@ def _call_tool(self, tool_name: str, tool_args: str, **kwargs): def _register_tool(self, tool: Union[str, Dict], - tenant_id: str = 'default'): + tenant_id: str = 'default', + **kwargs): """ Instantiate the tool for the agent @@ -115,18 +117,28 @@ def _register_tool(self, if isinstance(tool, dict): tool_name = next(iter(tool)) tool_cfg = tool[tool_name] - if tool_name not in TOOL_REGISTRY: + if tool_name not in TOOL_REGISTRY and not self.use_tool_api: raise NotImplementedError if tool not in self.function_list: self.function_list.append(tool) - tool_class_with_tenant = TOOL_REGISTRY[tool_name] - - # adapt the TOOL_REGISTRY[tool_name] to origin tool class - - if isinstance(tool_class_with_tenant, BaseTool): - tool_class_with_tenant = {'class': TOOL_REGISTRY[tool_name]} - TOOL_REGISTRY[tool_name] = tool_class_with_tenant + try: + tool_class_with_tenant = TOOL_REGISTRY[tool_name] + + # adapt the TOOL_REGISTRY[tool_name] to origin tool class + if isinstance(tool_class_with_tenant, BaseTool): + tool_class_with_tenant = { + 'class': TOOL_REGISTRY[tool_name] + } + TOOL_REGISTRY[tool_name] = tool_class_with_tenant + + except KeyError as e: + print(e) + if not self.use_tool_api: + raise KeyError( + f'Tool {tool_name} is not registered in TOOL_REGISTRY, please register it first.' + ) + tool_class_with_tenant = {'class': ToolServiceProxy} # check if the tenant_id of tool instance or tool service are exists # TODO: change from use_tool_api=True to False, to get the tenant_id of the tool changes to @@ -137,7 +149,14 @@ def _register_tool(self, if self.use_tool_api: # get service proxy as tool instance, call method will call remote tool service tool_instance = ToolServiceProxy(tool_name, tool_cfg, - tenant_id) + tenant_id, **kwargs) + + # if the tool name is running in studio, remove the studio prefix from tool name + # TODO: it might cause duplicated name from different studio + in_ms_studio = os.getenv('MODELSCOPE_ENVIRONMENT', 'none') + if in_ms_studio == 'studio': + tool_name = tool_name.split('/')[-1] + else: # instantiation tool class as tool instance tool_instance = TOOL_REGISTRY[tool_name]['class'](tool_cfg) diff --git a/modelscope_agent/agent_env_util.py b/modelscope_agent/agent_env_util.py index ebce48517..2010b61ff 100644 --- a/modelscope_agent/agent_env_util.py +++ b/modelscope_agent/agent_env_util.py @@ -19,7 +19,7 @@ def __init__(self, use_history: bool = True, human_input_mode: Optional[str] = 'CLOSE', parse_env_prompt_function: Callable = None, - remote=True, + remote=False, **kwargs): """ Agent environment context mixin class to allow the agent to communicate with other agent, in the diff --git a/modelscope_agent/agents/multi_role_play.py b/modelscope_agent/agents/multi_role_play.py index 932ff9f62..1e8d8fca4 100644 --- a/modelscope_agent/agents/multi_role_play.py +++ b/modelscope_agent/agents/multi_role_play.py @@ -210,9 +210,6 @@ def _run(self, max_turn = 10 while True and max_turn > 0: - # print('=====one input planning_prompt======') - # print(planning_prompt) - # print('=============Answer=================') max_turn -= 1 if self.llm.support_function_calling(): output = self.llm.chat_with_functions( diff --git a/modelscope_agent/agents/role_play.py b/modelscope_agent/agents/role_play.py index f9a369980..58f8461a7 100644 --- a/modelscope_agent/agents/role_play.py +++ b/modelscope_agent/agents/role_play.py @@ -263,9 +263,6 @@ def _run(self, max_turn = 10 call_llm_count = 0 while True and max_turn > 0: - # print('=====one input planning_prompt======') - # print(planning_prompt) - # print('=============Answer=================') max_turn -= 1 call_llm_count += 1 if self.llm.support_function_calling(): diff --git a/modelscope_agent/agents_registry.py b/modelscope_agent/agents_registry.py index 6123ae35b..ef37f3233 100644 --- a/modelscope_agent/agents_registry.py +++ b/modelscope_agent/agents_registry.py @@ -6,7 +6,7 @@ class AgentRegistry: - def __init__(self, remote=True, **kwargs): + def __init__(self, remote=False, **kwargs): self._agents = {} self._agents_state = {} self.remote = remote diff --git a/modelscope_agent/constants.py b/modelscope_agent/constants.py index bd2629568..f1adf6c87 100644 --- a/modelscope_agent/constants.py +++ b/modelscope_agent/constants.py @@ -10,6 +10,7 @@ TASK_CENTER_NAME = 'task_center' DEFAULT_TOOL_MANAGER_SERVICE_URL = 'http://localhost:31511' DEFAULT_ASSISTANT_SERVICE_URL = 'http://localhost:31512' +MODELSCOPE_AGENT_TOKEN_HEADER_NAME = 'X-Modelscope-Agent-Token' class ApiNames(Enum): @@ -17,3 +18,4 @@ class ApiNames(Enum): modelscope_api_key = 'MODELSCOPE_API_TOKEN' amap_api_key = 'AMAP_TOKEN' bing_api_key = 'BING_SEARCH_V7_SUBSCRIPTION_KEY' + zhipu_api_key = 'ZHIPU_API_KEY' diff --git a/modelscope_agent/llm/dashscope.py b/modelscope_agent/llm/dashscope.py index 75cde4cd6..dc7b5696b 100644 --- a/modelscope_agent/llm/dashscope.py +++ b/modelscope_agent/llm/dashscope.py @@ -20,7 +20,6 @@ def stream_output(response, **kwargs): for trunk in response: if trunk.status_code == HTTPStatus.OK: # logging at the first frame for request_id, and the last frame for the whole output - print(trunk) if not text: logger.info( f'call dashscope generation api success, ' @@ -101,7 +100,6 @@ def _chat_stream(self, if kwargs.get('seed', None): generation_input['seed'] = kwargs.get('seed') response = dashscope.Generation.call(**generation_input) - print(response) response = self.stat_last_call_token_info(response) return stream_output(response, **kwargs) @@ -230,7 +228,6 @@ def build_multi_role_raw_prompt(self, messages: list): prompt = '' im_start = '<|im_start|>' im_end = '<|im_end|>' - print('build_raw_prompt', messages) if messages[0]['role'] == 'system': system_prompt = messages[0]['content'] else: @@ -264,7 +261,6 @@ def build_multi_role_raw_prompt(self, messages: list): user_content += f'<|im_start|>{cur_role.strip()}\n{cur_chat.strip()}<|im_end|>\n' prompt = f'{prompt}{user_content}<|im_start|>{cur_role_name}\n' - print('prompt: ', [prompt]) return prompt def _chat_stream(self, diff --git a/modelscope_agent/memory/memory_with_rag.py b/modelscope_agent/memory/memory_with_rag.py index 7e540a6b5..870daa470 100644 --- a/modelscope_agent/memory/memory_with_rag.py +++ b/modelscope_agent/memory/memory_with_rag.py @@ -52,6 +52,8 @@ def _run(self, query, files=url, **kwargs) # limit length if isinstance(summary_result, list): + if len(summary_result) == 0: + return '' single_max_token = int(max_token / len(summary_result)) concatenated_records = '\n'.join([ record[0:single_max_token - 1] for record in summary_result diff --git a/modelscope_agent/multi_agents_utils/executors/ray.py b/modelscope_agent/multi_agents_utils/executors/ray.py index 2034fc5c7..a6d476ab8 100644 --- a/modelscope_agent/multi_agents_utils/executors/ray.py +++ b/modelscope_agent/multi_agents_utils/executors/ray.py @@ -1,18 +1,25 @@ import logging from typing import Union -import ray from modelscope_agent.agents_registry import AgentRegistry from modelscope_agent.constants import USER_REQUIREMENT from modelscope_agent.environment.environment import Environment from modelscope_agent.schemas import Message from ray._raylet import ObjectRefGenerator +try: + import ray +except ImportError: + logging.error( + 'Ray is not installed, please install ray first by running `pip install ray>=2.9.4`' + ) + class RayTaskExecutor: @staticmethod def init_ray(): + if ray.is_initialized: ray.shutdown() ray.init(logging_level=logging.ERROR) diff --git a/modelscope_agent/rag/__init__.py b/modelscope_agent/rag/__init__.py index e69de29bb..8f1abc42d 100644 --- a/modelscope_agent/rag/__init__.py +++ b/modelscope_agent/rag/__init__.py @@ -0,0 +1,5 @@ +from modelscope_agent.utils.nltk_utils import install_nltk_data + +# install nltk data + +install_nltk_data() diff --git a/modelscope_agent/rag/knowledge.py b/modelscope_agent/rag/knowledge.py index 510d40a19..1450a6f1f 100644 --- a/modelscope_agent/rag/knowledge.py +++ b/modelscope_agent/rag/knowledge.py @@ -20,9 +20,6 @@ from modelscope_agent.llm.base import BaseChatModel from modelscope_agent.rag.emb import DashscopeEmbedding from modelscope_agent.rag.llm import ModelscopeAgentLLM -from modelscope_agent.utils.nltk_utils import install_nltk_data - -install_nltk_data() @dataclass diff --git a/modelscope_agent/rag/llm.py b/modelscope_agent/rag/llm.py index 59d37e3ca..b56d52154 100644 --- a/modelscope_agent/rag/llm.py +++ b/modelscope_agent/rag/llm.py @@ -131,7 +131,6 @@ def gen() -> ChatResponseGen: raw=r, ) - print(f'response: {response}') return gen() @llm_completion_callback() diff --git a/modelscope_agent/tools/__init__.py b/modelscope_agent/tools/__init__.py index 034a28fbd..545667ec9 100644 --- a/modelscope_agent/tools/__init__.py +++ b/modelscope_agent/tools/__init__.py @@ -1,6 +1,7 @@ import sys from ..utils import _LazyModule +from .contrib import * # noqa F403 _import_structure = { 'amap_weather': ['AMAPWeather'], diff --git a/modelscope_agent/tools/base.py b/modelscope_agent/tools/base.py index 9237f6290..b97493138 100644 --- a/modelscope_agent/tools/base.py +++ b/modelscope_agent/tools/base.py @@ -1,3 +1,4 @@ +import os import time from abc import ABC, abstractmethod from typing import Dict, List, Optional, Union @@ -5,7 +6,9 @@ import json import json5 import requests -from modelscope_agent.constants import DEFAULT_TOOL_MANAGER_SERVICE_URL +from modelscope_agent.constants import (DEFAULT_TOOL_MANAGER_SERVICE_URL, + MODELSCOPE_AGENT_TOKEN_HEADER_NAME) +from modelscope_agent.utils.logger import agent_logger as logger from modelscope_agent.utils.utils import has_chinese_chars # ast? @@ -161,7 +164,8 @@ def _verify_args(self, params: str) -> Union[str, dict]: """ try: params_json = json5.loads(params) - except Exception: + except Exception as e: + print(e) params = params.replace('\r', '\\r').replace('\n', '\\n') params_json = json5.loads(params) @@ -284,12 +288,15 @@ def has_chinese_chars_in_tools(_tools): class ToolServiceProxy(BaseTool): - def __init__( - self, - tool_name: str, - tool_cfg: dict, - tenant_id: str = 'default', - tool_service_manager_url: str = DEFAULT_TOOL_MANAGER_SERVICE_URL): + def __init__(self, + tool_name: str, + tool_cfg: dict, + tenant_id: str = 'default', + tool_service_manager_url: str = os.getenv( + 'TOOL_MANAGER_SERVICE_URL', + DEFAULT_TOOL_MANAGER_SERVICE_URL), + user_token: str = None, + **kwargs): """ Tool service proxy class Args: @@ -297,8 +304,11 @@ def __init__( tool_cfg: the configuration of tool tenant_id: the tenant id that the tool belongs to, defalut to 'default' tool_service_manager_url: the url of tool service manager, default to 'http://localhost:31511' + user_token: used to pass to the tool service manager to authenticate the user """ + self.tool_service_manager_url = tool_service_manager_url + self.user_token = user_token self.tool_name = tool_name self.tool_cfg = tool_cfg self.tenant_id = tenant_id @@ -337,13 +347,21 @@ def parse_service_response(response): def _register_tool(self): try: + service_token = os.getenv('TOOL_MANAGER_AUTH', '') + headers = { + 'Content-Type': 'application/json', + MODELSCOPE_AGENT_TOKEN_HEADER_NAME: self.user_token, + 'authorization': service_token + } + print(f'reach here create {headers}') response = requests.post( f'{self.tool_service_manager_url}/create_tool_service', json={ 'tool_name': self.tool_name, 'tenant_id': self.tenant_id, 'tool_cfg': self.tool_cfg - }) + }, + headers=headers) response.raise_for_status() result = ToolServiceProxy.parse_service_response(response) if 'status' not in result: @@ -356,17 +374,24 @@ def _register_tool(self): ) except Exception as e: raise RuntimeError( - f'Get error during registering tool from tool manager service with detail {e}' + f'Get error during registering tool from tool manager service with detail {e}.' ) def _check_tool_status(self): try: - response = requests.post( + service_token = os.getenv('TOOL_MANAGER_AUTH', '') + headers = { + 'Content-Type': 'application/json', + MODELSCOPE_AGENT_TOKEN_HEADER_NAME: self.user_token, + 'authorization': service_token + } + response = requests.get( f'{self.tool_service_manager_url}/check_tool_service_status', params={ 'tool_name': self.tool_name, 'tenant_id': self.tenant_id, - }) + }, + headers=headers) response.raise_for_status() result = ToolServiceProxy.parse_service_response(response) if 'status' not in result: @@ -381,12 +406,20 @@ def _check_tool_status(self): def _get_tool_info(self): try: + service_token = os.getenv('TOOL_MANAGER_AUTH', '') + headers = { + 'Content-Type': 'application/json', + MODELSCOPE_AGENT_TOKEN_HEADER_NAME: self.user_token, + 'authorization': service_token + } + logger.query_info(message=f'tool_info requests header {headers}') response = requests.post( f'{self.tool_service_manager_url}/tool_info', json={ 'tool_name': self.tool_name, 'tenant_id': self.tenant_id - }) + }, + headers=headers) response.raise_for_status() return ToolServiceProxy.parse_service_response(response) except Exception as e: @@ -395,6 +428,16 @@ def _get_tool_info(self): ) def call(self, params: str, **kwargs): + # ms_token + self.user_token = kwargs.get('user_token', self.user_token) + service_token = os.getenv('TOOL_MANAGER_AUTH', '') + headers = { + 'Content-Type': 'application/json', + MODELSCOPE_AGENT_TOKEN_HEADER_NAME: self.user_token, + 'authorization': service_token + } + logger.query_info(message=f'calling tool header {headers}') + try: # visit tool node to call tool response = requests.post( @@ -404,7 +447,11 @@ def call(self, params: str, **kwargs): 'tenant_id': self.tenant_id, 'params': params, 'kwargs': kwargs - }) + }, + headers=headers) + logger.query_info( + message=f'calling tool message {response.json()}') + response.raise_for_status() return ToolServiceProxy.parse_service_response(response) except Exception as e: diff --git a/modelscope_agent/tools/code_interpreter/code_interpreter.py b/modelscope_agent/tools/code_interpreter/code_interpreter.py index 9e55037f4..d5ab83d0f 100644 --- a/modelscope_agent/tools/code_interpreter/code_interpreter.py +++ b/modelscope_agent/tools/code_interpreter/code_interpreter.py @@ -18,6 +18,7 @@ from typing import Dict, Optional import json +import json5 import matplotlib import PIL.Image from jupyter_client import BlockingKernelClient @@ -50,7 +51,7 @@ class CodeInterpreter(BaseTool): should not be used the other code interpreter tool at the same time """ name = 'code_interpreter' - description = '代码解释器,可用于执行Python代码。 Enclose the code within triple backticks (`) at the beginning and end of the code.' # noqa E501 + description = '代码解释器,可用于执行Python代码。' # noqa E501 parameters = [{'name': 'code', 'type': 'string', 'description': '待执行的代码'}] def __init__(self, cfg={}): @@ -267,10 +268,10 @@ def _execute_code(self, kc: BlockingKernelClient, code: str) -> str: return result def call(self, params: str, timeout: Optional[int] = 30, **kwargs) -> str: - params = self._verify_args(params) - if isinstance(params, dict): + try: + params = json5.loads(params) code = params['code'] - else: + except Exception: code = extract_code(params) if not code.strip(): diff --git a/modelscope_agent/tools/modelscope_tools/pipeline_tool.py b/modelscope_agent/tools/modelscope_tools/pipeline_tool.py index af24f69d8..7833af5d5 100644 --- a/modelscope_agent/tools/modelscope_tools/pipeline_tool.py +++ b/modelscope_agent/tools/modelscope_tools/pipeline_tool.py @@ -64,7 +64,7 @@ def call(self, params: str, **kwargs) -> str: return self._remote_call(params, **kwargs) def _remote_call(self, params: dict, **kwargs): - data = json.dumps(params) + data = json.dumps(params, ensure_ascii=False) try: api_token = get_api_key(ApiNames.modelscope_api_key, **kwargs) except AssertionError: @@ -101,7 +101,7 @@ def _local_call(self, params: dict, **kwargs): try: self.setup() origin_result = self.pipeline(**kwargs) - return json.dumps(origin_result, default=str) + return json.dumps(origin_result, default=str, ensure_ascii=False) except RuntimeError as e: import traceback raise RuntimeError( diff --git a/modelscope_agent/utils/git.py b/modelscope_agent/utils/git.py new file mode 100644 index 000000000..79165a522 --- /dev/null +++ b/modelscope_agent/utils/git.py @@ -0,0 +1,25 @@ +import subprocess + + +def clone_git_repository(repo_url, branch_name, folder_name): + """ + Clone a git repository into a specified folder. + Args: + repo_url: user repository url + branch_name: branch name + folder_name: where should the repository be cloned + + Returns: + + """ + try: + subprocess.run( + ['git', 'clone', '-b', branch_name, repo_url, folder_name], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + print(f"Repository cloned successfully into '{folder_name}'.") + + except subprocess.CalledProcessError as e: + print(f'Error cloning repository: {e.stderr.decode()}') + raise RuntimeError(f'Repository cloning failed with e {e}') diff --git a/modelscope_agent/utils/nltk/stopwords.zip b/modelscope_agent/utils/nltk/stopwords.zip new file mode 100644 index 0000000000000000000000000000000000000000..3f1d91306dc4f9aca53d6639d887de2f5a82aebb GIT binary patch literal 34276 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 Optional[str]: # noqa E125 + if authorization: + schema, _, token = authorization.partition(' ') + if schema.lower() == 'bearer' and token: + # Assuming the token is a bearer token, return the token part + return token + elif token == '': + return authorization + + # If the schema is not bearer or there is no token, raise an exception + raise HTTPException(status_code=403, detail='Invalid authentication') + + +def get_user_token(authorization: Optional[str] = Header( + None, alias=MODELSCOPE_AGENT_TOKEN_HEADER_NAME)): # noqa E125 + if authorization: + # Assuming the token is a bearer token + schema, _, token = authorization.partition(' ') + if schema and token and schema.lower() == 'bearer': + return token + elif token == '': + return authorization + else: + return '' + + def start_docker_container_and_store_status(tool: ToolRegisterInfo, app_instance: FastAPI): with Session(engine) as session: @@ -199,7 +228,8 @@ async def root(): @app.post('/create_tool_service/') async def create_tool_service(tool_info: CreateTool, - background_tasks: BackgroundTasks): + background_tasks: BackgroundTasks, + auth_token: str = Depends(get_auth_token)): # todo: the tool name might be the repo dir for the tool, need to parse in this situation. tool_node_name = f'{tool_info.tool_name}_{tool_info.tenant_id}' tool_register_info = ToolRegisterInfo( @@ -209,6 +239,7 @@ async def create_tool_service(tool_info: CreateTool, tenant_id=tool_info.tenant_id, image=tool_info.tool_image, workspace_dir=os.getcwd(), + tool_url=tool_info.tool_url, ) background_tasks.add_task(start_docker_container_and_store_status, tool_register_info, app) @@ -222,11 +253,10 @@ async def create_tool_service(tool_info: CreateTool, return create_success_msg(output, request_id=request_id) -@app.post('/check_tool_service_status/') -async def check_tool_service_status( - tool_name: str, - tenant_id: str = 'default', -): +@app.get('/check_tool_service_status/') +async def check_tool_service_status(tool_name: str, + tenant_id: str = 'default', + auth_token: str = Depends(get_auth_token)): # todo: the tool name might be the repo dir for the tool, need to parse in this situation. tool_node_name = f'{tool_name}_{tenant_id}' request_id = str(uuid4()) @@ -250,21 +280,18 @@ async def check_tool_service_status( @app.post('/update_tool_service/') -async def update_tool_service( - tool_name: str, - background_tasks: BackgroundTasks, - tool_cfg: dict = {}, - tenant_id: str = 'default', - tool_image: str = 'modelscope-agent/tool-node:latest', -): - tool_node_name = f'{tool_name}_{tenant_id}' +async def update_tool_service(tool_info: CreateTool, + background_tasks: BackgroundTasks, + auth_token: str = Depends(get_auth_token)): + tool_node_name = f'{tool_info.tool_name}_{tool_info.tenant_id}' tool_register_info = ToolRegisterInfo( node_name=tool_node_name, - tool_name=tool_name, - config=tool_cfg, - tenant_id=tenant_id, - image=tool_image, + tool_name=tool_info.tool_name, + config=tool_info.tool_cfg, + tenant_id=tool_info.tenant_id, + image=tool_info.tool_image, workspace_dir=os.getcwd(), + tool_url=tool_info.tool_url, ) background_tasks.add_task(restart_docker_container_and_update_status, tool_register_info, app) @@ -281,7 +308,8 @@ async def update_tool_service( @app.post('/remove_tool/') async def deregister_tool(tool_name: str, background_tasks: BackgroundTasks, - tenant_id: str = 'default'): + tenant_id: str = 'default', + auth_token: str = Depends(get_auth_token)): tool_node_name = f'{tool_name}_{tenant_id}' tool_register = ToolRegisterInfo( node_name=tool_node_name, @@ -302,7 +330,8 @@ async def deregister_tool(tool_name: str, @app.get('/tools/', response_model=List[ToolInstance]) -async def list_tools(tenant_id: str = 'default'): +async def list_tools(tenant_id: str = 'default', + auth_token: str = Depends(get_auth_token)): with Session(engine) as session: statement = select(ToolInstance).where( ToolInstance.tenant_id == tenant_id) @@ -313,7 +342,9 @@ async def list_tools(tenant_id: str = 'default'): @app.post('/tool_info/') -async def get_tool_info(tool_input: ExecuteTool): +async def get_tool_info(tool_input: ExecuteTool, + user_token: str = Depends(get_user_token), + auth_token: str = Depends(get_auth_token)): # get tool instance request_id = str(uuid4()) @@ -333,7 +364,9 @@ async def get_tool_info(tool_input: ExecuteTool): tool_info_url = 'http://' + tool_instance.ip + ':' + str( tool_instance.port) + '/tool_info' response = requests.get( - tool_info_url, params={'request_id': request_id}) + tool_info_url, + params={'request_id': request_id}, + headers={'Authorization': f'Bearer {user_token}'}) response.raise_for_status() return create_success_msg( parse_service_response(response), request_id=request_id) @@ -347,7 +380,9 @@ async def get_tool_info(tool_input: ExecuteTool): @app.post('/execute_tool/') -async def execute_tool(tool_input: ExecuteTool): +async def execute_tool(tool_input: ExecuteTool, + user_token: str = Depends(get_user_token), + auth_token: str = Depends(get_auth_token)): request_id = str(uuid4()) @@ -378,7 +413,8 @@ async def execute_tool(tool_input: ExecuteTool): 'params': tool_input.params, 'kwargs': tool_input.kwargs, 'request_id': request_id - }) + }, + headers={'Authorization': f'Bearer {user_token}'}) response.raise_for_status() return create_success_msg( parse_service_response(response), request_id=request_id) @@ -387,8 +423,8 @@ async def execute_tool(tool_input: ExecuteTool): status_code=400, request_id=request_id, message= - f'Failed to execute tool for {tool_input.tool_name}_{tool_input.tenant_id}, with error {e}' - ) + f'Failed to execute tool for {tool_input.tool_name}_{tool_input.tenant_id}, ' + f'with error: {e} and origin error {response.message}') if __name__ == '__main__': diff --git a/modelscope_agent_servers/tool_manager_server/models.py b/modelscope_agent_servers/tool_manager_server/models.py index cffab1386..149b8efb2 100644 --- a/modelscope_agent_servers/tool_manager_server/models.py +++ b/modelscope_agent_servers/tool_manager_server/models.py @@ -26,6 +26,7 @@ class ToolRegisterInfo(BaseModel): tenant_id: str config: dict = {} port: Optional[int] = 31513 + tool_url: str = '' class CreateTool(BaseModel): @@ -33,6 +34,7 @@ class CreateTool(BaseModel): tenant_id: str = 'default' tool_cfg: dict = {} tool_image: str = 'modelscope-agent/tool-node:latest' + tool_url: str = '' class ExecuteTool(BaseModel): diff --git a/modelscope_agent_servers/tool_manager_server/sandbox.py b/modelscope_agent_servers/tool_manager_server/sandbox.py index 0093c614d..a279e3653 100644 --- a/modelscope_agent_servers/tool_manager_server/sandbox.py +++ b/modelscope_agent_servers/tool_manager_server/sandbox.py @@ -45,7 +45,7 @@ def init_docker_container(docker_client, tool: ToolRegisterInfo): container_port = f'{tool.port}/tcp' # inside container port(protocal could be tcp/udp) host_port = tool.port # host port port_bindings = {container_port: host_port} - command = f'uvicorn modelscope_agent_servers.tool_node_server.api:app --host 0.0.0.0 --port {tool.port}' + command = f'/bin/bash /app/run_tool_node.sh {tool.port}' container = docker_client.containers.run( tool.image, @@ -55,7 +55,10 @@ def init_docker_container(docker_client, tool: ToolRegisterInfo): name=tool.node_name, detach=True, ports=port_bindings, - environment={'PORT': 41111}) + environment={ + 'TOOL_OSS_URL': tool.tool_url, + 'TOOL_NAME': tool.tool_name + }) return container @@ -117,7 +120,7 @@ def start_docker_container(tool: ToolRegisterInfo): if elapsed > TIMEOUT: break # make configuration for class or copy remote github repo to docker container - inject_tool_info_to_container(container, tool) + # inject_tool_info_to_container(container, tool) return container except Exception as e: raise Exception( diff --git a/requirements.txt b/requirements.txt index b2de66382..667e68fdb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ langchain-experimental llama-index llama-index-readers-json llama-index-retrievers-bm25 -modelscope >=1.10.0,<=1.12.0 +modelscope >=1.15.0 moviepy openai opencv-python @@ -21,9 +21,7 @@ pydantic>=2.3.0 pytest pytest-mock python-dotenv -ray>=2.9.4 seaborn sentencepiece tiktoken -transformers unstructured diff --git a/scripts/run_tool_node.sh b/scripts/run_tool_node.sh new file mode 100644 index 000000000..c080940ac --- /dev/null +++ b/scripts/run_tool_node.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# get OSS_URL from ENV +OSS_URL=${TOOL_OSS_URL} +ZIP_FILE_NAME="new_tool.zip" +DESTINATION_FOLDER="/app/modelscope_agent/tools/contrib/new_tool" + +mkdir -p /app/assets +echo "{\"name\": \"${TOOL_NAME}\"}" > /app/assets/configuration.json + +# check if OSS_URL is empty, if empty them run a normal tool node server. +if [ -z "${OSS_URL}" ]; then + uvicorn modelscope_agent_servers.tool_node_server.api:app --host 0.0.0.0 --port "$1" +fi + +# Make sure the destination folder exists +mkdir -p "${DESTINATION_FOLDER}" + +# download the zip file +wget -O "${ZIP_FILE_NAME}" "${OSS_URL}" + +# check if download is successful +if [ $? -ne 0 ]; then + echo "Download failed." + exit 1 +else + echo "Downloaded ${ZIP_FILE_NAME} successfully." + + # unzip the downloaded file + unzip -o "${ZIP_FILE_NAME}" -d "${DESTINATION_FOLDER}" + for subfolder in "${DESTINATION_FOLDER}"/*; do + if [ -d "$subfolder" ]; then # Check if it's a directory + find "$subfolder" -type f -exec mv {} "${DESTINATION_FOLDER}"/ \; + # Optionally, remove the now-empty subdirectory + rmdir "$subfolder" + fi +done + echo "from .new_tool import *" >> /app/modelscope_agent/tools/contrib/__init__.py + + # check if extraction is successful + if [ $? -ne 0 ]; then + echo "Extraction failed." + exit 1 + else + echo "Extracted ${ZIP_FILE_NAME} into ${DESTINATION_FOLDER}." + + # clean up the downloaded zip file + rm "${ZIP_FILE_NAME}" + echo "Removed the downloaded zip file." + fi +fi + +# get config from ENV +TOOL_NAME=${TOOL_NAME} + +uvicorn modelscope_agent_servers.tool_node_server.api:app --host 0.0.0.0 --port "$1" +#sleep 90m diff --git a/tests/utils/test_git_clone.py b/tests/utils/test_git_clone.py new file mode 100644 index 000000000..ecc791182 --- /dev/null +++ b/tests/utils/test_git_clone.py @@ -0,0 +1,44 @@ +import os +import shutil +import tempfile + +import pytest +from modelscope_agent.utils.git import clone_git_repository + + +@pytest.fixture(scope='function') +def temp_dir(): + # create temp dir + temp_directory = tempfile.mkdtemp() + yield temp_directory + # delete temp dir after test + shutil.rmtree(temp_directory) + + +def test_clone_git_repository_success(temp_dir): + # use temp dir as folder name + repo_url = 'http://www.modelscope.cn/studios/zhicheng/zzc_tool_test.git' + branch_name = 'master' + folder_name = temp_dir + + # store the git to local dir + clone_git_repository(repo_url, branch_name, folder_name) + + # check if success + assert os.listdir( + folder_name) != [], 'Directory should not be empty after cloning' + + +def test_clone_git_repository_failed(temp_dir): + # use temp dir as folder name + repo_url = 'http://www.modelscope.cn/studios/zhicheng/zzc_tool_test1.git' + branch_name = 'master' + folder_name = temp_dir + + # store the git to local dir + with pytest.raises(RuntimeError): + clone_git_repository(repo_url, branch_name, folder_name) + + # check if error + assert os.listdir( + folder_name) == [], 'Directory should not be empty after cloning'