diff --git a/apps/datascience_assistant/README.md b/apps/datascience_assistant/README.md new file mode 100644 index 000000000..961ce0d83 --- /dev/null +++ b/apps/datascience_assistant/README.md @@ -0,0 +1,26 @@ +# Data Science Assistant with Streamlit ⭐ +Data Science Assistant (hereinafter referred to as DS Assistant) is a Data Science Assistant developed based on the modelscope-agent framework, which can automatically perform exploratory Data analysis (EDA) in Data Science tasks according to user needs, Data preprocessing, feature engineering, model training, model evaluation and other steps are fully automated. + +Detailed information can be found in the [documentation](../../docs/source/agents/data_science_assistant.md). + +## Quick Start +Streamlit is a Python library that makes it easy to create and share beautiful, custom web apps for machine learning and data science. + +To run the DS Assistant in streamlit, you need to install the Streamlit library. You can install it using pip: +```bash +pip install streamlit streamlit-jupyter +``` +Then, you need to set +Then, you can run the DS Assistant using the following command: +```bash +streamlit run app.py +``` + +After running the command, a new tab will open in your default web browser with the DS Assistant running. +The following are screenshots of the DS Assistant running in the browser: + +![img_2.png](../../resources/data_science_assistant_streamlit_1.png) +you can view all of the codes and in streamlit +![img_3.png](../../resources/data_science_assistant_streamlit_2.png) +After you have finished using the DS Assistant, you can directly convert the running process to a pdf +![img_5.png](../../resources/data_science_assistant_streamlit_3.png) diff --git a/apps/datascience_assistant/app.py b/apps/datascience_assistant/app.py new file mode 100644 index 000000000..e590a1a66 --- /dev/null +++ b/apps/datascience_assistant/app.py @@ -0,0 +1,23 @@ +import os + +import streamlit as st +from modelscope_agent.agents.data_science_assistant import DataScienceAssistant +from modelscope_agent.tools.metagpt_tools.tool_recommend import \ + TypeMatchToolRecommender + +llm_config = { + 'model': 'qwen2-72b-instruct', + 'model_server': 'dashscope', +} +os.environ['DASHSCOPE_API_KEY'] = input( + 'Please input your dashscope api key: ') +data_science_assistant = DataScienceAssistant( + llm=llm_config, tool_recommender=TypeMatchToolRecommender(tools=[''])) +st.title('Data Science Assistant') +st.write( + 'This is a data science assistant that can help you with your data science tasks.' +) +st.write('Please input your request below and click the submit button.') +user_request = st.text_input('User Request') +if st.button('submit'): + data_science_assistant.run(user_request=user_request, streamlit=True) diff --git a/docs/source/index.rst b/docs/source/index.rst index baff8638a..d458e6c77 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -52,6 +52,14 @@ Modelscope-Agent DOCUMENTATION deployment/local_deploy.md +.. toctree:: + :maxdepth: 2 + :caption: Agents + + agents/data_science_assistant.md + + + Indices and tables ================== diff --git a/docs/source_en/index.rst b/docs/source_en/index.rst index 849089e6f..36d2677f6 100644 --- a/docs/source_en/index.rst +++ b/docs/source_en/index.rst @@ -50,6 +50,11 @@ Modelscope-Agent DOCUMENTATION use_cases/openAPI_for_agent.md deployment/local_deploy.md +.. toctree:: + :maxdepth: 2 + :caption: Agents + + agents/data_science_assistant.md Indices and tables diff --git a/modelscope_agent/agents/data_science_assistant.py b/modelscope_agent/agents/data_science_assistant.py index cb18ba93d..db8ad2472 100644 --- a/modelscope_agent/agents/data_science_assistant.py +++ b/modelscope_agent/agents/data_science_assistant.py @@ -1,5 +1,6 @@ # Implementation inspired by the paper "DATA INTERPRETER: AN LLM AGENT FOR DATA SCIENCE" import asyncio +import copy import os import time from datetime import datetime @@ -18,6 +19,12 @@ from modelscope_agent.utils.logger import agent_logger as logger from modelscope_agent.utils.utils import parse_code +try: + import streamlit as st # noqa + from nbconvert import HTMLExporter + from traitlets.config import Config +except Exception as e: + print(f'import error: {str(e)}, please install streamlit and nbconvert') PLAN_TEMPLATE = """ # Context: {context} @@ -28,9 +35,9 @@ - **feature engineering**: Only for creating new columns fo input data. - **model train**: Only for training model. - **model evaluate**: Only for evaluating model. +- **ocr**: Only for OCR tasks. - **other**: Any tasks not in the defined categories - # Task: Based on the context, write a simple plan or modify an existing plan of what you should do to achieve the goal. A plan \ consists of one to four tasks. @@ -226,14 +233,12 @@ these are the previous code blocks, which have been executed successfully in the previous jupyter notebook code blocks \ {previous_code_blocks} -Attention: your response should be one of the following: -- [your step by step thought], correct -- [your step by step thought], incorrect - +at the end of your thought, you need to give the final judgement with a new line( correct or incorrect). don't generate code , just give the reason why the code is correct or incorrect. ## Attention don't use the word 'incorrect' in your step by step thought. +your answer should be short and clear, don't need to be too long. """ CHECK_DATA_PROMPT = """ @@ -311,6 +316,7 @@ def __init__(self, self.code_interpreter = CodeInterpreter() self.plan = None self.total_token = 0 + self.streamlit = False def _update_plan(self, user_request: str, curr_plan: Plan = None) -> Plan: call_llm_success = False @@ -325,18 +331,26 @@ def _update_plan(self, user_request: str, curr_plan: Plan = None) -> Plan: }] while not call_llm_success and call_llm_count < 10: resp = self._call_llm(prompt=None, messages=messages, stop=None) + resp_streamlit = resp tasks_text = '' - for r in resp: - tasks_text += r + if self.streamlit: + st.write('#### Generate a plan based on the user request') + tasks_text = st.write_stream(resp_streamlit) + else: + for r in resp: + tasks_text += r if 'Error code' in tasks_text: call_llm_count += 1 time.sleep(10) else: call_llm_success = True + print('Tasks_text: ', tasks_text) tasks_text = parse_code(text=tasks_text, lang='json') + logger.info(f'tasks: {tasks_text}') tasks = json5.loads(tasks_text) tasks = [Task(**task) for task in tasks] + if curr_plan is None: new_plan = Plan(goal=user_request) new_plan.add_tasks(tasks=tasks) @@ -429,9 +443,8 @@ def _generate_code(self, code_counter: int, task: Task, else: # reflect the error and ask user to fix the code if self.tool_recommender: - tool_info = asyncio.run( - self.tool_recommender.get_recommended_tool_info( - plan=self.plan)) + tool_info = self.tool_recommender.get_recommended_tool_info( + plan=self.plan) prompt = CODE_USING_TOOLS_REFLECTION_TEMPLATE.format( instruction=task.instruction, task_guidance=TaskType.get_type(task.task_type).guidance, @@ -555,9 +568,6 @@ def _check_data(self): def _judge_code(self, task, previous_code_blocks, code, code_interpreter_resp): - success = True - failed_reason = '' - judge_prompt = JUDGE_TEMPLATE.format( instruction=task.instruction, previous_code_blocks=previous_code_blocks, @@ -578,13 +588,12 @@ def _judge_code(self, task, previous_code_blocks, code, self._get_total_tokens() if 'Error code' in judge_result: call_llm_count += 1 - time.sleep(10) + time.sleep(5) else: call_llm_success = True if not call_llm_success: raise Exception('call llm failed') logger.info(f'judge result for task{task.task_id}: \n {judge_result}') - if 'incorrect' in judge_result.split('\n')[-1]: success = False failed_reason = ( @@ -593,11 +602,17 @@ def _judge_code(self, task, previous_code_blocks, code, return success, failed_reason else: - return True, 'The code logic is correct' + return True, judge_result def _run(self, user_request, save: bool = True, **kwargs): before_time = time.time() try: + self.streamlit = kwargs.get('streamlit', False) + if self.streamlit: + st.write("""# DataScience Assistant """) + st.write("""### The user request is: \n""") + st.write(user_request) + print('streamlit: ', self.streamlit) self.plan = self._update_plan(user_request=user_request) jupyter_file_path = '' dir_name = '' @@ -610,7 +625,9 @@ def _run(self, user_request, save: bool = True, **kwargs): while self.plan.current_task_id: task = self.plan.task_map.get(self.plan.current_task_id) - # write_and_execute_code(self) + if self.streamlit: + st.write( + f"""### Task {task.task_id}: {task.instruction}\n""") logger.info( f'new task starts: task_{task.task_id} , instruction: {task.instruction}' ) @@ -622,7 +639,6 @@ def _run(self, user_request, save: bool = True, **kwargs): code_execute_success = False code_logic_success = False temp_code_interpreter = CodeInterpreter() - temp_code_interpreter.call( params=json.dumps({ 'code': @@ -633,26 +649,56 @@ def _run(self, user_request, save: bool = True, **kwargs): # generate code code = self._generate_code(code_counter, task, user_request) + code = '%matplotlib inline \n' + code code_execute_success, code_interpreter_resp = temp_code_interpreter.call( params=json.dumps({'code': code}), nb_mode=True, silent_mode=True) - # 删除临时 jupyter环境 - temp_code_interpreter.terminate() + if self.streamlit: + st.divider() + st_notebook = nbformat.v4.new_notebook() + st_notebook.cells = [ + temp_code_interpreter.nb.cells[-1] + ] + c = Config() + c.HTMLExporter.preprocessors = [ + 'nbconvert.preprocessors.ConvertFiguresPreprocessor' + ] + # create the new exporter using the custom config + html_exporter_with_figs = HTMLExporter(config=c) + (html, resources_with_fig + ) = html_exporter_with_figs.from_notebook_node( + st_notebook) + st.write( + 'We have generated the code for the current task') + st.html(html) judge_resp = '' if not code_execute_success: logger.error( f'code execution failed, task{task.task_id} code_counter{code_counter}:\n ' f'{code_interpreter_resp}') + if self.streamlit: + st.write( + 'The code execution failed. Now we will take a reflection and regenerate the code.' + ) else: logger.info( f'code execution success, task{task.task_id} code_counter{code_counter}:\n ' f'{code_interpreter_resp}') + if self.streamlit: + st.write( + 'The code execution is successful. Now we will ask the judge to check the code.' + ) code_logic_success, judge_resp = self._judge_code( task=task, previous_code_blocks=previous_code_blocks, code=code, code_interpreter_resp=code_interpreter_resp) + if self.streamlit: + st.write( + 'The judge has checked the code, here is the result.' + ) + st.write(judge_resp) success = code_execute_success and code_logic_success task.code_cells.append( CodeCell( @@ -663,6 +709,10 @@ def _run(self, user_request, save: bool = True, **kwargs): if success: self.code_interpreter.call( params=json.dumps({'code': code}), nb_mode=True) + if self.streamlit: + st.write( + 'The code is correct, we will move to the next task.' + ) task.code = code task.result = code_interpreter_resp code_counter += 1 @@ -699,6 +749,13 @@ def _run(self, user_request, save: bool = True, **kwargs): json.dumps(plan_dict, indent=4, cls=TaskEncoder)) except Exception as e: print(f'json write error: {str(e)}') + if self.streamlit: + st.divider() + st.write('### We have finished all the tasks! ') + st.balloons() + st.write( + f"""#### The total time cost is: {time_cost}\n #### The total token cost is: {total_token}""" + ) except Exception as e: logger.error(f'error: {e}') diff --git a/modelscope_agent/llm/__init__.py b/modelscope_agent/llm/__init__.py index c06ed0afc..2e036570d 100644 --- a/modelscope_agent/llm/__init__.py +++ b/modelscope_agent/llm/__init__.py @@ -17,6 +17,7 @@ def get_chat_model(model: str, model_server: str, **kwargs) -> BaseChatModel: """ model_type = re.split(r'[-/_]', model)[0] # parser qwen / gpt / ... registered_model_id = f'{model_server}_{model_type}' + if registered_model_id in LLM_REGISTRY: # specific model from specific source return LLM_REGISTRY[registered_model_id](model, model_server, **kwargs) elif model_server in LLM_REGISTRY: # specific source diff --git a/modelscope_agent/llm/openai.py b/modelscope_agent/llm/openai.py index 89f757e53..cfbacdffd 100644 --- a/modelscope_agent/llm/openai.py +++ b/modelscope_agent/llm/openai.py @@ -4,28 +4,55 @@ from modelscope_agent.llm.base import BaseChatModel, register_llm from modelscope_agent.utils.logger import agent_logger as logger from modelscope_agent.utils.retry import retry -from openai import OpenAI +from openai import AzureOpenAI, OpenAI @register_llm('openai') +@register_llm('azure_openai') class OpenAi(BaseChatModel): - def __init__(self, - model: str, - model_server: str, - is_chat: bool = True, - is_function_call: Optional[bool] = None, - support_stream: Optional[bool] = None, - **kwargs): + def __init__( + self, + model: str, + model_server: str, + is_chat: bool = True, + is_function_call: Optional[bool] = None, + support_stream: Optional[bool] = None, + **kwargs, + ): super().__init__(model, model_server, is_function_call) - default_api_base = os.getenv('OPENAI_API_BASE', - 'https://api.openai.com/v1') - api_base = kwargs.get('api_base', default_api_base).strip() - api_key = kwargs.get('api_key', - os.getenv('OPENAI_API_KEY', - default='EMPTY')).strip() - logger.info(f'client url {api_base}, client key: {api_key}') - self.client = OpenAI(api_key=api_key, base_url=api_base) + + self.is_azure = model_server.lower().startswith('azure') + if self.is_azure: + default_azure_endpoint = os.getenv( + 'AZURE_OPENAI_ENDPOINT', + 'https://docs-test-001.openai.azure.com/') + azure_endpoint = kwargs.get('azure_endpoint', + default_azure_endpoint).strip() + api_key = kwargs.get( + 'api_key', os.getenv('AZURE_OPENAI_API_KEY', + default='EMPTY')).strip() + api_version = kwargs.get('api_version', '2024-06-01').strip() + logger.info( + f'client url {azure_endpoint}, client key: {api_key}, client version: {api_version}' + ) + + self.client = AzureOpenAI( + azure_endpoint=azure_endpoint, + api_key=api_key, + api_version=api_version, + ) + else: + default_api_base = os.getenv('OPENAI_API_BASE', + 'https://api.openai.com/v1') + api_base = kwargs.get('api_base', default_api_base).strip() + api_key = kwargs.get('api_key', + os.getenv('OPENAI_API_KEY', + default='EMPTY')).strip() + logger.info(f'client url {api_base}, client key: {api_key}') + + self.client = OpenAI(api_key=api_key, base_url=api_base) + self.is_function_call = is_function_call self.is_chat = is_chat self.support_stream = support_stream @@ -38,21 +65,24 @@ def _chat_stream(self, logger.info( f'call openai api, model: {self.model}, messages: {str(messages)}, ' f'stop: {str(stop)}, stream: True, args: {str(kwargs)}') - stream_options = {'include_usage': True} + + if not self.is_azure: + kwargs['stream_options'] = {'include_usage': True} + response = self.client.chat.completions.create( model=self.model, messages=messages, stop=stop, stream=True, - stream_options=stream_options, **kwargs) + response = self.stat_last_call_token_info_stream(response) # TODO: error handling for chunk in response: # sometimes delta.content is None by vllm, we should not yield None - if len(chunk.choices) > 0 and hasattr( - chunk.choices[0].delta, - 'content') and chunk.choices[0].delta.content: + if (len(chunk.choices) > 0 + and hasattr(chunk.choices[0].delta, 'content') + and chunk.choices[0].delta.content): logger.info( f'call openai api success, output: {chunk.choices[0].delta.content}' ) @@ -93,12 +123,14 @@ def support_raw_prompt(self) -> bool: return not self.is_chat @retry(max_retries=3, delay_seconds=0.5) - def chat(self, - prompt: Optional[str] = None, - messages: Optional[List[Dict]] = None, - stop: Optional[List[str]] = None, - stream: bool = False, - **kwargs) -> Union[str, Iterator[str]]: + def chat( + self, + prompt: Optional[str] = None, + messages: Optional[List[Dict]] = None, + stop: Optional[List[str]] = None, + stream: bool = False, + **kwargs, + ) -> Union[str, Iterator[str]]: if 'uuid_str' in kwargs: kwargs.pop('uuid_str') @@ -150,7 +182,8 @@ def chat_with_functions(self, messages=messages, tools=functions, tool_choice='auto', - **kwargs) + **kwargs, + ) else: response = self.client.chat.completions.create( model=self.model, messages=messages, **kwargs) @@ -175,9 +208,9 @@ def _chat_stream(self, # TODO: error handling for chunk in response: # sometimes delta.content is None by vllm, we should not yield None - if len(chunk.choices) > 0 and hasattr( - chunk.choices[0].delta, - 'content') and chunk.choices[0].delta.content: + if (len(chunk.choices) > 0 + and hasattr(chunk.choices[0].delta, 'content') + and chunk.choices[0].delta.content): logger.info( f'call openai api success, output: {chunk.choices[0].delta.content}' ) diff --git a/modelscope_agent/tools/metagpt_tools/task_type.py b/modelscope_agent/tools/metagpt_tools/task_type.py index a34e734d9..e94d49d68 100644 --- a/modelscope_agent/tools/metagpt_tools/task_type.py +++ b/modelscope_agent/tools/metagpt_tools/task_type.py @@ -54,6 +54,13 @@ - Ensure that the evaluated data is same processed as the training data. - Use trained model from previous task result directly, do not mock or reload model yourself. """ +OCR_PROMPT = """ +The current task is about OCR, please note the following: +- you can follow the following code to get the OCR result: +from paddleocr import PaddleOCR +ocr = PaddleOCR(use_angle_cls=True, lang='en') +result = ocr.ocr('/path/to/the/pic', cls=True) # please replace the path with the real path +""" class TaskTypeDef(BaseModel): @@ -92,7 +99,8 @@ class TaskType(Enum): desc='Only for evaluating model.', guidance=MODEL_EVALUATE_PROMPT, ) - + OCR = TaskTypeDef( + name='ocr', desc='For performing OCR tasks', guidance=OCR_PROMPT) OTHER = TaskTypeDef( name='other', desc='Any tasks not in the defined categories') diff --git a/resources/data_science_assistant_streamlit_1.png b/resources/data_science_assistant_streamlit_1.png new file mode 100644 index 000000000..e98b56c4a Binary files /dev/null and b/resources/data_science_assistant_streamlit_1.png differ diff --git a/resources/data_science_assistant_streamlit_2.png b/resources/data_science_assistant_streamlit_2.png new file mode 100644 index 000000000..ad2c65d64 Binary files /dev/null and b/resources/data_science_assistant_streamlit_2.png differ diff --git a/resources/data_science_assistant_streamlit_3.png b/resources/data_science_assistant_streamlit_3.png new file mode 100644 index 000000000..d7b782fdc Binary files /dev/null and b/resources/data_science_assistant_streamlit_3.png differ