From 722d5c787d2726f75ad6fbc23d400eb1be97f81d Mon Sep 17 00:00:00 2001 From: ZJU_czx <952370295@qq.com> Date: Fri, 28 Mar 2025 16:22:30 +0800 Subject: [PATCH 1/5] feat: add chart prepare tools and update chart geneartion tool to generate chart parallel --- app/agent/data_analysis.py | 38 +---- app/tool/chart_visualization/__init__.py | 4 +- app/tool/chart_visualization/chart_prepare.py | 43 ++++++ .../chart_visualization.py | 143 +++++++++++------- .../data_analysis_python.py | 42 ----- .../normal_python_execute.py | 5 +- .../chart_visualization/src/chartVisualize.ts | 101 +++++++++++-- .../chart_visualization/test/tool_test.py | 32 ++++ 8 files changed, 269 insertions(+), 139 deletions(-) create mode 100644 app/tool/chart_visualization/chart_prepare.py delete mode 100644 app/tool/chart_visualization/data_analysis_python.py create mode 100644 app/tool/chart_visualization/test/tool_test.py diff --git a/app/agent/data_analysis.py b/app/agent/data_analysis.py index 8bd2829..156a90e 100644 --- a/app/agent/data_analysis.py +++ b/app/agent/data_analysis.py @@ -1,17 +1,17 @@ from pydantic import Field -from app.agent.browser import BrowserAgent +from app.agent.toolcall import ToolCallAgent from app.config import config -from app.prompt.browser import NEXT_STEP_PROMPT as BROWSER_NEXT_STEP_PROMPT from app.prompt.visualization import NEXT_STEP_PROMPT, SYSTEM_PROMPT from app.tool import Terminate, ToolCollection -from app.tool.browser_use_tool import BrowserUseTool from app.tool.chart_visualization.chart_visualization import ChartVisualization from app.tool.chart_visualization.normal_python_execute import NormalPythonExecute -from app.tool.chart_visualization.data_analysis_python import DataAnalysisPythonExecute +from app.tool.chart_visualization.chart_prepare import ( + VisualizationPrepare, +) -class DataAnalysis(BrowserAgent): +class DataAnalysis(ToolCallAgent): """ A data analysis agent that uses planning to solve various data analysis tasks. @@ -34,34 +34,8 @@ class DataAnalysis(BrowserAgent): available_tools: ToolCollection = Field( default_factory=lambda: ToolCollection( NormalPythonExecute(), - DataAnalysisPythonExecute(), + VisualizationPrepare(), ChartVisualization(), - BrowserUseTool(), Terminate(), ) ) - - async def think(self) -> bool: - """Process current state and decide next actions with appropriate context.""" - # Store original prompt - original_prompt = self.next_step_prompt - - # Only check recent messages (last 3) for browser activity - recent_messages = self.memory.messages[-3:] if self.memory.messages else [] - browser_in_use = any( - "browser_use" in msg.content.lower() - for msg in recent_messages - if hasattr(msg, "content") and isinstance(msg.content, str) - ) - - if browser_in_use: - # Override with browser-specific prompt temporarily to get browser context - self.next_step_prompt = BROWSER_NEXT_STEP_PROMPT - - # Call parent's think method - result = await super().think() - - # Restore original prompt - self.next_step_prompt = original_prompt - - return result diff --git a/app/tool/chart_visualization/__init__.py b/app/tool/chart_visualization/__init__.py index 6de1c65..9dcd09e 100644 --- a/app/tool/chart_visualization/__init__.py +++ b/app/tool/chart_visualization/__init__.py @@ -1,5 +1,5 @@ from app.tool.chart_visualization.chart_visualization import ChartVisualization -from app.tool.chart_visualization.data_analysis_python import DataAnalysisPythonExecute +from app.tool.chart_visualization.chart_prepare import VisualizationPrepare from app.tool.chart_visualization.normal_python_execute import NormalPythonExecute -__all__ = ["ChartVisualization", "DataAnalysisPythonExecute", "NormalPythonExecute"] +__all__ = ["ChartVisualization", "VisualizationPrepare", "NormalPythonExecute"] diff --git a/app/tool/chart_visualization/chart_prepare.py b/app/tool/chart_visualization/chart_prepare.py new file mode 100644 index 0000000..d0f7e68 --- /dev/null +++ b/app/tool/chart_visualization/chart_prepare.py @@ -0,0 +1,43 @@ +from app.tool.chart_visualization.normal_python_execute import NormalPythonExecute + + +class VisualizationPrepare(NormalPythonExecute): + """A tool for Chart Generation Preparation""" + + name: str = "visualization_preparation" + description: str = ( + "Using Python code to Generates structured visualization datasets with metadata. Outputs: 1) Cleaned CSV data files 2) JSON info with csv path and visualization description." + ) + parameters: dict = { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": """Python code template EXCLUSIVELY for visualization prepare. Must Contains: +1. Data loading logic (handle dataframe/dict/file/url/json/web crawler) +2. Csv Data and chart description generate +2.1 Csv data (The data you want to visulazation, cleaning / transform from origin data, saved in .csv) +2.2 Chart description of csv data (The chart title or description should be concise and clear. Examples: 'Product sales distribution', 'Monthly revenue trend'.) +3. Save information in json file.( format: {"csvFilePath": string, "chartTitle": string}[] encoding='utf-8') +3. Json file saving with path print: print(json_path) +# Note +You can generate one or multiple csv data with different visualization needs. +""", + }, + }, + "required": ["code"], + } + + async def execute(self, code: str, timeout=5): + """ + Executes the provided Python code with a timeout. + + Args: + code (str): The Python code to execute. + analysis_content (str): The analysis content of current task. + timeout (int): Execution timeout in seconds. + + Returns: + Dict: Contains 'output' with execution output or error message and 'success' status. + """ + return await super().execute(code, timeout) diff --git a/app/tool/chart_visualization/chart_visualization.py b/app/tool/chart_visualization/chart_visualization.py index 8905c05..9791d89 100644 --- a/app/tool/chart_visualization/chart_visualization.py +++ b/app/tool/chart_visualization/chart_visualization.py @@ -1,8 +1,6 @@ -import subprocess import json -import base64 +import asyncio import pandas as pd -import aiofiles import os from typing import Any, Hashable from pydantic import Field, model_validator @@ -10,23 +8,20 @@ from pydantic import Field, model_validator from app.llm import LLM from app.tool.base import BaseTool from app.logger import logger +from app.config import config class ChartVisualization(BaseTool): - name: str = "generate_data_visualization" - description: str = """Visualize a statistical chart using csv data and chart description. The tool accepts local csv data file path and description of the chart, and output a chart in png or html. + name: str = "data_visualization_with_insight" + description: str = """Visualize statistical chart with JSON info from visualization_preparation tool. Outputs: 1) Charts (png/html) 2) Charts Insights (.md). Note: Each tool call generates only one single chart. """ parameters: dict = { "type": "object", "properties": { - "csv_path": { + "json_path": { "type": "string", - "description": """file path of csv data with ".csv" in the end""", - }, - "chart_description": { - "type": "string", - "description": "The chart title or description should be concise and clear. Examples: 'Product sales distribution', 'Monthly revenue trend'.", + "description": """file path of json info with ".json" in the end""", }, "output_type": { "description": "Rendering format (html=interactive)", @@ -35,7 +30,7 @@ Note: Each tool call generates only one single chart. "enum": ["png", "html"], }, }, - "required": ["code", "chart_description"], + "required": ["code"], } llm: LLM = Field(default_factory=LLM, description="Language model instance") @@ -46,40 +41,74 @@ Note: Each tool call generates only one single chart. self.llm = LLM(config_name=self.name.lower()) return self - async def execute( - self, csv_path: str, chart_description: str, output_type: str - ) -> str: - logger.info( - f"๐Ÿ“ˆ Chart Generation with data and description: {chart_description} with {csv_path} " - ) + def success_output_template(self, result: list[dict[str, str]]) -> str: + content = "" + for item in result: + content += f"""## {item["title"]} +Chart saved in: {item["savedPath"]}""" + if len(item["insightsText"]) > 0: + insight_content = "" + for index, text in enumerate(item["insightsText"]): + insight_content += f"{index}. {text}\n" + content += f"""\n### Insights of Chart\n{insight_content}""" + else: + content += "\n" + return f"Chart Generated Successful! Detail is below:\n{content}" + + async def execute(self, json_path: str, output_type: str) -> str: + logger.info(f"๐Ÿ“ˆ Chart Generation with json path: {json_path} ") try: - df = pd.read_csv(csv_path) - df = df.astype(object) - df = df.where(pd.notnull(df), None) - data_dict_list = df.to_json(orient="records", force_ascii=False) - result = await self.invoke_vmind( - data_dict_list, chart_description, output_type - ) - if "error" in result: + with open(json_path, "r", encoding="utf-8") as file: + json_info = json.load(file) + data_list = [] + for item in json_info: + df = pd.read_csv(item["csvFilePath"]) + df = df.astype(object) + df = df.where(pd.notnull(df), None) + data_dict_list = df.to_json(orient="records", force_ascii=False) + + data_list.append( + { + "file_name": os.path.basename(item["csvFilePath"]).replace( + ".csv", "" + ), + "dict_data": data_dict_list, + "chart_description": item["chartTitle"], + } + ) + tasks = [ + self.invoke_vmind( + item["dict_data"], + item["chart_description"], + item["file_name"], + output_type, + ) + for item in data_list + ] + + results = await asyncio.gather(*tasks) + error_list = [] + success_list = [] + for index, result in enumerate(results): + csv_path = json_info[index]["csvFilePath"] + if "error" in result: + error_list.append(f"Error in {csv_path}: {result["error"]}") + else: + success_list.append( + { + **result, + "title": json_info[index]["chart_description"], + } + ) + if len(error_list) > 0: return { - "observation": f"Error: {result["error"]}", + "observation": f"# Error chart generated{'\n'.join(error_list)}\nCharts saved successful are below: \n{self.success_output_template(success_list)}", "success": False, } - chart_file_path = csv_path.replace(".csv", f".{output_type}") - while os.path.exists(chart_file_path): - chart_file_path = chart_file_path.replace( - f".{output_type}", f"_new.{output_type}" - ) - if output_type == "png": - byte_data = base64.b64decode(result["res"]) - async with aiofiles.open(chart_file_path, "wb") as file: - await file.write(byte_data) else: - async with aiofiles.open( - chart_file_path, "w", encoding="utf-8" - ) as file: - await file.write(result["res"]) - return {"observation": f"chart successfully saved to {chart_file_path}"} + return { + "observation": f"All charts saved successful!\n{self.success_output_template(success_list)}" + } except Exception as e: return { "observation": f"Error: {e}", @@ -90,6 +119,7 @@ Note: Each tool call generates only one single chart. self, dict_data: list[dict[Hashable, Any]], chart_description: str, + file_name: str, output_type: str, ): llm_config = { @@ -102,16 +132,27 @@ Note: Each tool call generates only one single chart. "user_prompt": chart_description, "dataset": dict_data, "output_type": output_type, + "file_name": file_name, + "directory": str(config.workspace_root), } - process = subprocess.run( - ["npx", "ts-node", "src/chartVisualize.ts"], - input=json.dumps(vmind_params), - capture_output=True, - text=True, - encoding="utf-8", + print(vmind_params) + # build async sub process + process = await asyncio.create_subprocess_exec( + "npx", + "ts-node", + "src/chartVisualize.ts", + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, cwd=os.path.dirname(__file__), ) - if process.returncode == 0: - return json.loads(process.stdout) - else: - return {"error": f"Node.js Error: {process.stderr}"} + + input_json = json.dumps(vmind_params).encode("utf-8") + try: + stdout, stderr = await process.communicate(input_json) + if process.returncode == 0: + return json.loads(stdout) + else: + return {"error": f"Node.js Error: {stderr}"} + except Exception as e: + return {"error": f"Subprocess Error: {str(e)}"} diff --git a/app/tool/chart_visualization/data_analysis_python.py b/app/tool/chart_visualization/data_analysis_python.py deleted file mode 100644 index e92aed0..0000000 --- a/app/tool/chart_visualization/data_analysis_python.py +++ /dev/null @@ -1,42 +0,0 @@ -from app.tool.chart_visualization.normal_python_execute import NormalPythonExecute - - -class DataAnalysisPythonExecute(NormalPythonExecute): - """A tool for executing Python code in data analysis task with timeout and safety restrictions.""" - - name: str = "data_analysis_python_execute" - description: str = ( - "Executes Python code string in data analysis task, save data table in csv file. Note: Only print outputs are visible, function return values are not captured. Use print statements to see results." - ) - parameters: dict = { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": """Python code template EXCLUSIVELY for data analysis. Must Contains: -1. Data loading logic (handle dataframe/dict/file/url/json/web crawler) -2. Data analysis (cleaning/transformation) -3. CSV saving with path print: print(csv_path) -""", - }, - "analysis_content": { - "type": "string", - "description": "Your analysis of current task, ensure your analysis is concise, clear, and easy to understand.", - }, - }, - "required": ["code"], - } - - async def execute(self, code: str, analysis_content: str, timeout=5): - """ - Executes the provided Python code with a timeout. - - Args: - code (str): The Python code to execute. - analysis_content (str): The analysis content of current task. - timeout (int): Execution timeout in seconds. - - Returns: - Dict: Contains 'output' with execution output or error message and 'success' status. - """ - return await super().execute(code, timeout) diff --git a/app/tool/chart_visualization/normal_python_execute.py b/app/tool/chart_visualization/normal_python_execute.py index 5102eb3..f49b622 100644 --- a/app/tool/chart_visualization/normal_python_execute.py +++ b/app/tool/chart_visualization/normal_python_execute.py @@ -12,9 +12,10 @@ class NormalPythonExecute(PythonExecute): name: str = "common_python_execute" description: str = ( - """Executes Python code strings. Note: + """Executes Python code strings to do data analysis. Note: 1. Only outputs from print() are visible; function return values are not captured. Use print() statements to display results -2. Applicable to scenarios **excluding data analysis and chart generation**""" +2. Do data analysis (cleaning / transform) saved in *.csv +3. Generate a data analysis report in *.md""" ) parameters: dict = { "type": "object", diff --git a/app/tool/chart_visualization/src/chartVisualize.ts b/app/tool/chart_visualization/src/chartVisualize.ts index b566153..1819a37 100644 --- a/app/tool/chart_visualization/src/chartVisualize.ts +++ b/app/tool/chart_visualization/src/chartVisualize.ts @@ -1,10 +1,28 @@ import Canvas from "canvas"; import path from "path"; +import fs from "fs"; import { readFileSync } from "fs"; -import VMind from "@visactor/vmind"; +import VMind, { ChartType } from "@visactor/vmind"; import VChart from "@visactor/vchart"; import { isString } from "@visactor/vutils"; +declare enum AlgorithmType { + OverallTrending = "overallTrend", + AbnormalTrend = "abnormalTrend", + PearsonCorrelation = "pearsonCorrelation", + SpearmanCorrelation = "spearmanCorrelation", + ExtremeValue = "extremeValue", + MajorityValue = "majorityValue", + StatisticsAbnormal = "statisticsAbnormal", + StatisticsBase = "statisticsBase", + DbscanOutlier = "dbscanOutlier", + LOFOutlier = "lofOutlier", + TurningPoint = "turningPoint", + PageHinkley = "pageHinkley", + DifferenceOutlier = "differenceOutlier", + Volatility = "volatility", +} + const getBase64 = async (spec: any, width?: number, height?: number) => { spec.animation = false; width && (spec.width = width); @@ -77,6 +95,22 @@ async function getHtmlVChart(spec: any, width: number, height: number) { `; } +function getSavedPathName( + directory: string, + fileName: string, + outputType: "html" | "png" | "json" +) { + let newFileName = fileName; + while ( + fs.existsSync( + path.join(directory, "visualization", `${newFileName}.${outputType}`) + ) + ) { + newFileName += "_new"; + } + return path.join(directory, "visualization", `${newFileName}.${outputType}`); +} + async function generateChart() { const inputData = JSON.parse(readFileSync(process.stdin.fd, "utf-8")); try { @@ -87,6 +121,8 @@ async function generateChart() { output_type: outputType = "png", width, height, + file_name: fileName, + directory, } = inputData; const { base_url: baseUrl, model, api_key: apiKey } = llm_config; const vmind = new VMind({ @@ -98,7 +134,7 @@ async function generateChart() { }, }); const jsonDataset = isString(dataset) ? JSON.parse(dataset) : dataset; - const { spec, error } = await vmind.generateChart( + const { spec, error, chartType } = await vmind.generateChart( userPrompt, undefined, jsonDataset, @@ -107,6 +143,43 @@ async function generateChart() { theme: "light", } ); + spec.title = { + text: userPrompt, + }; + const insights = []; + if ( + chartType && + [ + ChartType.BarChart, + ChartType.LineChart, + ChartType.AreaChart, + ChartType.ScatterPlot, + ChartType.DualAxisChart, + ].includes(chartType) + ) { + const { insights: vmindInsights } = await vmind.getInsights(spec, { + maxNum: 6, + algorithms: [ + AlgorithmType.OverallTrending, + AlgorithmType.AbnormalTrend, + AlgorithmType.PearsonCorrelation, + AlgorithmType.SpearmanCorrelation, + AlgorithmType.StatisticsAbnormal, + AlgorithmType.LOFOutlier, + AlgorithmType.DbscanOutlier, + AlgorithmType.MajorityValue, + AlgorithmType.PageHinkley, + AlgorithmType.TurningPoint, + AlgorithmType.StatisticsBase, + AlgorithmType.Volatility, + ], + usePolish: false, + }); + insights.push(...vmindInsights); + } + const insightsText = insights.map( + (insight) => insight.textContent?.plainText + ); if (error || !spec) { console.log( JSON.stringify({ @@ -115,15 +188,23 @@ async function generateChart() { ); return; } - if (outputType === "png") { - console.log( - JSON.stringify({ res: await getBase64(spec, width, height) }) - ); - } else { - console.log( - JSON.stringify({ res: await getHtmlVChart(spec, width, height) }) - ); + spec.insights = insights; + if (!fs.existsSync(path.join(directory, "visualization"))) { + fs.mkdirSync(path.join(directory, "visualization")); } + fs.writeFileSync( + getSavedPathName(directory, fileName, "json"), + JSON.stringify(spec, null, 2) + ); + const savedPath = getSavedPathName(directory, fileName, outputType); + if (outputType === "png") { + const base64 = await getBase64(spec, width, height); + fs.writeFileSync(savedPath, base64); + } else { + const html = await getHtmlVChart(spec, width, height); + fs.writeFileSync(savedPath, html, "utf-8"); + } + console.log(JSON.stringify({ savedPath, insightsText })); } catch (error) { console.log(JSON.stringify({ error })); } diff --git a/app/tool/chart_visualization/test/tool_test.py b/app/tool/chart_visualization/test/tool_test.py new file mode 100644 index 0000000..3101e8b --- /dev/null +++ b/app/tool/chart_visualization/test/tool_test.py @@ -0,0 +1,32 @@ +import asyncio +from app.tool.chart_visualization import ChartVisualization + + +async def mock_request(delay, value): + print("!!!!") + await asyncio.sleep(delay) # ๆจกๆ‹Ÿๅผ‚ๆญฅIOๆ“ไฝœ๏ผˆๅฆ‚็ฝ‘็ปœ่ฏทๆฑ‚๏ผ‰ + return value + + +async def main(): + # ๅˆ›ๅปบๅคšไธชๅผ‚ๆญฅไปปๅŠก + tasks = [ + mock_request(1, "็ป“ๆžœ1"), + mock_request(2, "็ป“ๆžœ2"), + mock_request(3, "็ป“ๆžœ3"), + ] + + # ๅนถๅ‘ๆ‰ง่กŒๆ‰€ๆœ‰ไปปๅŠก๏ผŒ็ญ‰ๅพ…ๅ…จ้ƒจๅฎŒๆˆ + results = await asyncio.gather(*tasks) + print(results) # ่พ“ๅ‡บ: ['็ป“ๆžœ1', '็ป“ๆžœ2', '็ป“ๆžœ3'] + + +async def test_chart(): + chartTool = ChartVisualization() + print(await chartTool.execute("./data/visualization_info.json", "html")) + + +if __name__ == "__main__": + asyncio.run(test_chart()) + # ่ฟ่กŒไธปๅ็จ‹ + # asyncio.run(main()) From 6a2ff78ffdf8b48842f917ed3425b6f3006a5c63 Mon Sep 17 00:00:00 2001 From: ZJU_czx <952370295@qq.com> Date: Sat, 29 Mar 2025 19:45:00 +0800 Subject: [PATCH 2/5] Revert "Merge branch 'feat/data_visualization_hackathon' of https://github.com/666haiwen/OpenManus into feat/data_visualization_hackathon" This reverts commit b0e9384502cad2a19f32298327524619253255ea, reversing changes made to 96c23f1f56448efab8b2d866c897a49a01a39f60. --- .../chart_visualization/test/hack_demo.py | 48 ------------------- 1 file changed, 48 deletions(-) delete mode 100644 app/tool/chart_visualization/test/hack_demo.py diff --git a/app/tool/chart_visualization/test/hack_demo.py b/app/tool/chart_visualization/test/hack_demo.py deleted file mode 100644 index a08321d..0000000 --- a/app/tool/chart_visualization/test/hack_demo.py +++ /dev/null @@ -1,48 +0,0 @@ -import asyncio -import time - -from app.agent.manus import Manus -from app.agent.data_analysis import DataAnalysis -from app.flow.base import FlowType -from app.flow.flow_factory import FlowFactory -from app.logger import logger - - -async def run_flow(): - agents = { - "manus": Manus(), - "visactor": DataAnalysis(), - } - - try: - prompt = """Here's last month's sales data from my Amazon store in './data/amazon_sales_jan2025.csv'. Could you analyze it thoroughly with visualizations and recommend specific, data-driven strategies to boost next month's sales by 10%?""" - - flow = FlowFactory.create_flow( - flow_type=FlowType.PLANNING, - agents=agents, - ) - logger.warning("Processing your request...") - - try: - start_time = time.time() - result = await asyncio.wait_for( - flow.execute(prompt), - timeout=3600, # 60 minute timeout for the entire execution - ) - elapsed_time = time.time() - start_time - logger.info(f"Request processed in {elapsed_time:.2f} seconds") - logger.info(result) - except asyncio.TimeoutError: - logger.error("Request processing timed out after 1 hour") - logger.info( - "Operation terminated due to timeout. Please try a simpler request." - ) - - except KeyboardInterrupt: - logger.info("Operation cancelled by user.") - except Exception as e: - logger.error(f"Error: {str(e)}") - - -if __name__ == "__main__": - asyncio.run(run_flow()) From f9ad4362f607859bd871ff6491ca29f97f5c19bd Mon Sep 17 00:00:00 2001 From: ZJU_czx <952370295@qq.com> Date: Sat, 29 Mar 2025 19:45:15 +0800 Subject: [PATCH 3/5] Revert "feat: generate structured analysis reports" This reverts commit 96c23f1f56448efab8b2d866c897a49a01a39f60. --- .../normal_python_execute.py | 87 ++----------------- .../test/amazon_fashion_analysis.py | 17 ---- 2 files changed, 6 insertions(+), 98 deletions(-) delete mode 100644 app/tool/chart_visualization/test/amazon_fashion_analysis.py diff --git a/app/tool/chart_visualization/normal_python_execute.py b/app/tool/chart_visualization/normal_python_execute.py index b16b4c2..5102eb3 100644 --- a/app/tool/chart_visualization/normal_python_execute.py +++ b/app/tool/chart_visualization/normal_python_execute.py @@ -1,8 +1,10 @@ import sys from io import StringIO -from app.tool.chart_visualization.utils import extract_executable_code from app.tool.python_execute import PythonExecute +from app.tool.chart_visualization.utils import ( + extract_executable_code, +) class NormalPythonExecute(PythonExecute): @@ -10,86 +12,9 @@ class NormalPythonExecute(PythonExecute): name: str = "common_python_execute" description: str = ( - """ -A tool for executing Python code with data anaylsis. -Prefix: ๅธฎๆˆ‘็”Ÿๆˆ็ป“ๆžœไฟๅญ˜ๅœจๆœฌๅœฐ./dataไธ‹ - -Data Analysis Agent Protocol (Non-Visual) - -=== Core Requirements === -1. Strictly text-based outputs only -2. Prohibited actions: - - Any chart/image generation - - Interactive visual elements - - Graphical libraries import - -=== Execution Phases === - -1. DATA LOADING (Auto-detect format) -- Supported formats: CSV/Excel/JSON -- Mandatory checks: - a) File existence verification - b) Column structure validation - c) Basic integrity checks - -2. ANALYSIS PIPELINE -- Cleaning: - โ€ข Null handling (drop or impute) - โ€ข Deduplication - โ€ข Outlier treatment (IQR/Z-score) - -- Transformation: - โ€ข Date parsing - โ€ข Derived metrics - โ€ข Aggregations - -3. REPORT GENERATION -Output 1: data_exploration.md -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Section โ”‚ Content Requirements โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ Dataset Metadata โ”‚ Rows/Columns/Temporal Range โ”‚ -โ”‚ Column Descriptions โ”‚ Type/Stats/Unique Values โ”‚ -โ”‚ Key Findings โ”‚ 3-5 bullet points โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - -Output 2: preprocessing_results.md -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Section โ”‚ Content Requirements โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ Cleaning Log โ”‚ Rows affected by each operation โ”‚ -โ”‚ Derived Metrics โ”‚ Formula/Summary Stats โ”‚ -โ”‚ Anomaly Report โ”‚ Z-score >2.5 cases โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - -=== Implementation Rules === -1. Code Generation Constraints: - - Forbidden libraries: matplotlib, seaborn, plotly - - Maximum column width: 120 chars - - Required docstrings for all functions - -2. Error Handling: - - Skip corrupted records with logging - - Continue processing after non-critical errors - - Fail fast on structural issues - -3. Output Validation: - - Markdown syntax check - - Statistical validity verification - - Cross-report consistency - -=== Sample Invocation === -def analyze(data_path): - '''Main analysis workflow''' - df = load_data(data_path) # Phase 1 - cleaned = clean_and_transform(df) # Phase 2 - generate_reports(cleaned) # Phase 3 -=== ๆ‰ง่กŒ็บฆๆŸ === -ๅฝ“ๆฃ€ๆต‹ๅˆฐ้”™่ฏฏๆ—ถ๏ผš -1. ๅˆ†ๆž้”™่ฏฏ็ฑปๅž‹๏ผˆๆ•ฐๆฎ/้€ป่พ‘/็Žฏๅขƒ๏ผ‰ -2. ็”Ÿๆˆไฟฎๆญฃๆ–นๆกˆ๏ผˆ่‡ชๅŠจ้‡่ฏ•โ‰ค3ๆฌก๏ผ‰ -3. ไธฅ้‡้”™่ฏฏๆ—ถๅ›žๆปšไธญ้—ดๆ–‡ไปถ -""" + """Executes Python code strings. Note: +1. Only outputs from print() are visible; function return values are not captured. Use print() statements to display results +2. Applicable to scenarios **excluding data analysis and chart generation**""" ) parameters: dict = { "type": "object", diff --git a/app/tool/chart_visualization/test/amazon_fashion_analysis.py b/app/tool/chart_visualization/test/amazon_fashion_analysis.py deleted file mode 100644 index 012e82d..0000000 --- a/app/tool/chart_visualization/test/amazon_fashion_analysis.py +++ /dev/null @@ -1,17 +0,0 @@ -import asyncio - -from app.agent.data_analysis import DataAnalysis - -# from app.agent.manus import Manus - - -async def main(): - agent = DataAnalysis() - # agent = Manus() - await agent.run( - """Here's last month's sales data from my Amazon store in './data/amazon_sales_jan2025.xlsx'. Could you analyze it? """ - ) - - -if __name__ == "__main__": - asyncio.run(main()) From d3313a6b39779ed285dbd81496a88e951d76078b Mon Sep 17 00:00:00 2001 From: ZJU_czx <952370295@qq.com> Date: Mon, 31 Mar 2025 17:34:33 +0800 Subject: [PATCH 4/5] feat: update data analysis agent prompt and output content --- app/prompt/visualization.py | 6 +- app/tool/chart_visualization/chart_prepare.py | 28 ++--- .../chart_visualization.py | 68 ++++++----- .../normal_python_execute.py | 40 +++---- .../chart_visualization/package-lock.json | 33 +++++- app/tool/chart_visualization/package.json | 4 +- .../chart_visualization/src/chartVisualize.ts | 107 ++++++++++++------ .../chart_visualization/test/hack_demo.py | 6 +- app/tool/python_execute.py | 4 +- 9 files changed, 176 insertions(+), 120 deletions(-) diff --git a/app/prompt/visualization.py b/app/prompt/visualization.py index 00b072e..a4f046b 100644 --- a/app/prompt/visualization.py +++ b/app/prompt/visualization.py @@ -1,8 +1,8 @@ SYSTEM_PROMPT = ( - "You are an AI agent designed to data analysis and data visualization task. You have various tools at your disposal that you can call upon to efficiently complete complex requests." - "The initial directory is: {directory}" + "You are an AI agent designed to data analysis / visualization task. You have various tools at your disposal that you can call upon to efficiently complete complex requests." + "The workspace directory is: {directory}" ) NEXT_STEP_PROMPT = """ -Based on user needs, proactively select the most appropriate tool or combination of tools. For complex tasks, you can break down the problem and use different tools step by step to solve it. After using each tool, clearly explain the execution results and suggest the next steps. +Based on user needs, break down the problem and use different tools step by step to solve it. Each step select the most appropriate tool proactively(ONLY ONE). After using each tool, clearly explain the execution results and suggest the next steps. """ diff --git a/app/tool/chart_visualization/chart_prepare.py b/app/tool/chart_visualization/chart_prepare.py index d0f7e68..077336a 100644 --- a/app/tool/chart_visualization/chart_prepare.py +++ b/app/tool/chart_visualization/chart_prepare.py @@ -1,12 +1,12 @@ -from app.tool.chart_visualization.normal_python_execute import NormalPythonExecute +from app.tool.python_execute import PythonExecute -class VisualizationPrepare(NormalPythonExecute): +class VisualizationPrepare(PythonExecute): """A tool for Chart Generation Preparation""" name: str = "visualization_preparation" description: str = ( - "Using Python code to Generates structured visualization datasets with metadata. Outputs: 1) Cleaned CSV data files 2) JSON info with csv path and visualization description." + "Using Python code to Generates metadata of data_visualization tool. Outputs: 1) Cleaned CSV data files 2) JSON info with csv path and visualization description." ) parameters: dict = { "type": "object", @@ -18,26 +18,14 @@ class VisualizationPrepare(NormalPythonExecute): 2. Csv Data and chart description generate 2.1 Csv data (The data you want to visulazation, cleaning / transform from origin data, saved in .csv) 2.2 Chart description of csv data (The chart title or description should be concise and clear. Examples: 'Product sales distribution', 'Monthly revenue trend'.) -3. Save information in json file.( format: {"csvFilePath": string, "chartTitle": string}[] encoding='utf-8') -3. Json file saving with path print: print(json_path) +3. Save information in json file.( format: {"csvFilePath": string, "chartTitle": string}[]) +4. Json file saving with path print: print(json_path) # Note -You can generate one or multiple csv data with different visualization needs. +1. You can generate one or multiple csv data with different visualization needs. +2. Make each chart data esay, clean and different. +3. save/read in utf-8 """, }, }, "required": ["code"], } - - async def execute(self, code: str, timeout=5): - """ - Executes the provided Python code with a timeout. - - Args: - code (str): The Python code to execute. - analysis_content (str): The analysis content of current task. - timeout (int): Execution timeout in seconds. - - Returns: - Dict: Contains 'output' with execution output or error message and 'success' status. - """ - return await super().execute(code, timeout) diff --git a/app/tool/chart_visualization/chart_visualization.py b/app/tool/chart_visualization/chart_visualization.py index 9791d89..95541a3 100644 --- a/app/tool/chart_visualization/chart_visualization.py +++ b/app/tool/chart_visualization/chart_visualization.py @@ -12,10 +12,10 @@ from app.config import config class ChartVisualization(BaseTool): - name: str = "data_visualization_with_insight" - description: str = """Visualize statistical chart with JSON info from visualization_preparation tool. Outputs: 1) Charts (png/html) 2) Charts Insights (.md). -Note: Each tool call generates only one single chart. -""" + name: str = "data_visualization" + description: str = ( + """Visualize statistical chart with JSON info from visualization_preparation tool. Outputs: 1) Charts (png/html) 2) Charts Insights (.md)(Optional).""" + ) parameters: dict = { "type": "object", "properties": { @@ -41,16 +41,29 @@ Note: Each tool call generates only one single chart. self.llm = LLM(config_name=self.name.lower()) return self + def get_csv_path(self, json_info: list[dict[str, str]]) -> list[str]: + res = [] + for item in json_info: + if os.path.exists(item["csvFilePath"]): + res.append(item["csvFilePath"]) + elif os.path.exists( + os.path.join(f"{config.workspace_root}", item["csvFilePath"]) + ): + res.append( + os.path.join(f"{config.workspace_root}", item["csvFilePath"]) + ) + else: + raise Exception(f"No such file or directory: {item["csvFilePath"]}") + return res + def success_output_template(self, result: list[dict[str, str]]) -> str: content = "" + if len(result) == 0: + return "Is EMPTY!" for item in result: - content += f"""## {item["title"]} -Chart saved in: {item["savedPath"]}""" - if len(item["insightsText"]) > 0: - insight_content = "" - for index, text in enumerate(item["insightsText"]): - insight_content += f"{index}. {text}\n" - content += f"""\n### Insights of Chart\n{insight_content}""" + content += f"""## {item["title"]}\nChart saved in: {item["chart_path"]}""" + if "insight_path" in item and item["insight_path"]: + content += f"""\nChart insights saved in {item["insight_path"]}\n""" else: content += "\n" return f"Chart Generated Successful! Detail is below:\n{content}" @@ -61,25 +74,26 @@ Chart saved in: {item["savedPath"]}""" with open(json_path, "r", encoding="utf-8") as file: json_info = json.load(file) data_list = [] - for item in json_info: - df = pd.read_csv(item["csvFilePath"]) + csv_file_path = self.get_csv_path(json_info) + for index, item in enumerate(json_info): + df = pd.read_csv(csv_file_path[index], encoding="utf-8") df = df.astype(object) df = df.where(pd.notnull(df), None) data_dict_list = df.to_json(orient="records", force_ascii=False) data_list.append( { - "file_name": os.path.basename(item["csvFilePath"]).replace( + "file_name": os.path.basename(csv_file_path[index]).replace( ".csv", "" ), "dict_data": data_dict_list, - "chart_description": item["chartTitle"], + "chartTitle": item["chartTitle"], } ) tasks = [ self.invoke_vmind( item["dict_data"], - item["chart_description"], + item["chartTitle"], item["file_name"], output_type, ) @@ -90,25 +104,23 @@ Chart saved in: {item["savedPath"]}""" error_list = [] success_list = [] for index, result in enumerate(results): - csv_path = json_info[index]["csvFilePath"] - if "error" in result: + csv_path = csv_file_path[index] + if "error" in result and "chart_path" not in result: error_list.append(f"Error in {csv_path}: {result["error"]}") else: success_list.append( { **result, - "title": json_info[index]["chart_description"], + "title": json_info[index]["chartTitle"], } ) if len(error_list) > 0: return { - "observation": f"# Error chart generated{'\n'.join(error_list)}\nCharts saved successful are below: \n{self.success_output_template(success_list)}", + "observation": f"# Error chart generated{'\n'.join(error_list)}\n{self.success_output_template(success_list)}", "success": False, } else: - return { - "observation": f"All charts saved successful!\n{self.success_output_template(success_list)}" - } + return {"observation": f"{self.success_output_template(success_list)}"} except Exception as e: return { "observation": f"Error: {e}", @@ -135,7 +147,6 @@ Chart saved in: {item["savedPath"]}""" "file_name": file_name, "directory": str(config.workspace_root), } - print(vmind_params) # build async sub process process = await asyncio.create_subprocess_exec( "npx", @@ -146,13 +157,14 @@ Chart saved in: {item["savedPath"]}""" stderr=asyncio.subprocess.PIPE, cwd=os.path.dirname(__file__), ) - - input_json = json.dumps(vmind_params).encode("utf-8") + input_json = json.dumps(vmind_params, ensure_ascii=False).encode("utf-8") try: stdout, stderr = await process.communicate(input_json) + stdout_str = stdout.decode("utf-8") + stderr_str = stderr.decode("utf-8") if process.returncode == 0: - return json.loads(stdout) + return json.loads(stdout_str) else: - return {"error": f"Node.js Error: {stderr}"} + return {"error": f"Node.js Error: {stderr_str}"} except Exception as e: return {"error": f"Subprocess Error: {str(e)}"} diff --git a/app/tool/chart_visualization/normal_python_execute.py b/app/tool/chart_visualization/normal_python_execute.py index f49b622..06465a7 100644 --- a/app/tool/chart_visualization/normal_python_execute.py +++ b/app/tool/chart_visualization/normal_python_execute.py @@ -1,10 +1,4 @@ -import sys -from io import StringIO - from app.tool.python_execute import PythonExecute -from app.tool.chart_visualization.utils import ( - extract_executable_code, -) class NormalPythonExecute(PythonExecute): @@ -12,35 +6,27 @@ class NormalPythonExecute(PythonExecute): name: str = "common_python_execute" description: str = ( - """Executes Python code strings to do data analysis. Note: -1. Only outputs from print() are visible; function return values are not captured. Use print() statements to display results -2. Do data analysis (cleaning / transform) saved in *.csv -3. Generate a data analysis report in *.md""" + """Executes Python code strings to tasks such as data process and data report""" ) parameters: dict = { "type": "object", "properties": { "code": { "type": "string", - "description": "The Python code to execute.", + "description": """The Python code to execute. Note: +1. Only outputs from print() are visible; function return values are not captured. Use print() statements to display results +2. Do data process (cleaning / transform) saved in *.csv +3. Generate a data analysis report in html""", + }, + "code_type": { + "description": "code type", + "type": "string", + "default": "html", + "enum": ["process", "report", "others"], }, }, "required": ["code"], } - def _run_code(self, code: str, result_dict: dict, safe_globals: dict) -> None: - original_stdout = sys.stdout - be_extracted_code = extract_executable_code(code) # ignore_security_alert RCE - try: - output_buffer = StringIO() - sys.stdout = output_buffer - exec( # ignore_security_alert RCE - be_extracted_code, safe_globals, safe_globals - ) # ignore_security_alert RCE - result_dict["observation"] = output_buffer.getvalue() - result_dict["success"] = True - except Exception as e: - result_dict["observation"] = str(e) - result_dict["success"] = False - finally: - sys.stdout = original_stdout + async def execute(self, code: str, code_type: str, timeout=5): + return await super().execute(code, timeout) diff --git a/app/tool/chart_visualization/package-lock.json b/app/tool/chart_visualization/package-lock.json index 7c8ddf2..cd68a38 100644 --- a/app/tool/chart_visualization/package-lock.json +++ b/app/tool/chart_visualization/package-lock.json @@ -11,9 +11,11 @@ "dependencies": { "@visactor/vchart": "^1.13.7", "@visactor/vmind": "^2.0.4", - "canvas": "^2.11.2" + "canvas": "^2.11.2", + "get-stdin": "^9.0.0" }, "devDependencies": { + "@types/get-stdin": "^7.0.0", "@types/node": "^22.10.1", "ts-node": "^10.9.2", "typescript": "^5.7.2" @@ -6213,6 +6215,16 @@ "url": "https://opencollective.com/turf" } }, + "node_modules/@types/get-stdin": { + "version": "7.0.0", + "resolved": "https://bnpm.byted.org/@types/get-stdin/-/get-stdin-7.0.0.tgz", + "integrity": "sha512-kiDwIsKQvsLRvtBOnasij+6eChbCzcUT7OyVvrC5BEOE4QSKbpnwejEp0xND/9sIdOTfiu+BBl3zsB16MJ3Fww==", + "deprecated": "This is a stub types definition. get-stdin provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "get-stdin": "*" + } + }, "node_modules/@types/node": { "version": "22.13.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", @@ -7131,6 +7143,14 @@ "geojson-flatten": "geojson-flatten" } }, + "node_modules/geojson-flatten/node_modules/get-stdin": { + "version": "6.0.0", + "resolved": "https://bnpm.byted.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "engines": { + "node": ">=4" + } + }, "node_modules/geojson-linestring-dissolve": { "version": "0.0.1", "resolved": "https://bnpm.byted.org/geojson-linestring-dissolve/-/geojson-linestring-dissolve-0.0.1.tgz", @@ -7180,11 +7200,14 @@ } }, "node_modules/get-stdin": { - "version": "6.0.0", - "resolved": "https://bnpm.byted.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "version": "9.0.0", + "resolved": "https://bnpm.byted.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", "engines": { - "node": ">=4" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/gifuct-js": { diff --git a/app/tool/chart_visualization/package.json b/app/tool/chart_visualization/package.json index 7fcbb90..1000386 100644 --- a/app/tool/chart_visualization/package.json +++ b/app/tool/chart_visualization/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "main": "src/index.ts", "devDependencies": { + "@types/get-stdin": "^7.0.0", "@types/node": "^22.10.1", "ts-node": "^10.9.2", "typescript": "^5.7.2" @@ -10,7 +11,8 @@ "dependencies": { "@visactor/vchart": "^1.13.7", "@visactor/vmind": "^2.0.4", - "canvas": "^2.11.2" + "canvas": "^2.11.2", + "get-stdin": "^9.0.0" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/app/tool/chart_visualization/src/chartVisualize.ts b/app/tool/chart_visualization/src/chartVisualize.ts index 1819a37..861a4e7 100644 --- a/app/tool/chart_visualization/src/chartVisualize.ts +++ b/app/tool/chart_visualization/src/chartVisualize.ts @@ -1,12 +1,11 @@ import Canvas from "canvas"; import path from "path"; import fs from "fs"; -import { readFileSync } from "fs"; import VMind, { ChartType } from "@visactor/vmind"; import VChart from "@visactor/vchart"; import { isString } from "@visactor/vutils"; -declare enum AlgorithmType { +enum AlgorithmType { OverallTrending = "overallTrend", AbnormalTrend = "abnormalTrend", PearsonCorrelation = "pearsonCorrelation", @@ -54,7 +53,7 @@ const serializeSpec = (spec: any) => { }); }; -async function getHtmlVChart(spec: any, width: number, height: number) { +async function getHtmlVChart(spec: any, width?: number, height?: number) { return ` @@ -98,7 +97,7 @@ async function getHtmlVChart(spec: any, width: number, height: number) { function getSavedPathName( directory: string, fileName: string, - outputType: "html" | "png" | "json" + outputType: "html" | "png" | "json" | "md" ) { let newFileName = fileName; while ( @@ -111,8 +110,39 @@ function getSavedPathName( return path.join(directory, "visualization", `${newFileName}.${outputType}`); } +const readStdin = (): Promise => { + return new Promise((resolve) => { + let input = ""; + process.stdin.setEncoding("utf-8"); // ็กฎไฟ็ผ–็ ไธŽ Python ็ซฏไธ€่‡ด + process.stdin.on("data", (chunk) => (input += chunk)); + process.stdin.on("end", () => resolve(input)); + }); +}; + +const setInsightTemplate = ( + path: string, + title: string, + insights: string[] +) => { + let res = ""; + if (insights.length) { + res += `## ${title} Insights`; + insights.forEach((insight, index) => { + res += `\n${index + 1}. ${insight}`; + }); + } + if (res) { + fs.writeFileSync(path, res, "utf-8"); + return path; + } + return ""; +}; + async function generateChart() { - const inputData = JSON.parse(readFileSync(process.stdin.fd, "utf-8")); + const input = await readStdin(); + const inputData = JSON.parse(input); + const res: { chart_path?: string; error?: string; insight_path?: string } = + {}; try { const { llm_config, @@ -133,6 +163,7 @@ async function generateChart() { Authorization: `Bearer ${apiKey}`, }, }); + // Get chart spec and save in local file const jsonDataset = isString(dataset) ? JSON.parse(dataset) : dataset; const { spec, error, chartType } = await vmind.generateChart( userPrompt, @@ -143,9 +174,34 @@ async function generateChart() { theme: "light", } ); + if (error || !spec) { + console.log( + JSON.stringify({ + error: error || "Spec of Chart was Empty!", + }) + ); + return; + } + spec.title = { text: userPrompt, }; + if (!fs.existsSync(path.join(directory, "visualization"))) { + fs.mkdirSync(path.join(directory, "visualization")); + } + const specPath = getSavedPathName(directory, fileName, "json"); + fs.writeFileSync(specPath, JSON.stringify(spec, null, 2)); + const savedPath = getSavedPathName(directory, fileName, outputType); + if (outputType === "png") { + const base64 = await getBase64(spec, width, height); + fs.writeFileSync(savedPath, base64); + } else { + const html = await getHtmlVChart(spec, width, height); + fs.writeFileSync(savedPath, html, "utf-8"); + } + res.chart_path = savedPath; + + // get chart insights and save in local const insights = []; if ( chartType && @@ -177,36 +233,21 @@ async function generateChart() { }); insights.push(...vmindInsights); } - const insightsText = insights.map( - (insight) => insight.textContent?.plainText - ); - if (error || !spec) { - console.log( - JSON.stringify({ - error: error || "Spec of Chart was Empty!", - }) - ); - return; - } + const insightsText = insights + .map((insight) => insight.textContent?.plainText) + .filter((insight) => !!insight) as string[]; spec.insights = insights; - if (!fs.existsSync(path.join(directory, "visualization"))) { - fs.mkdirSync(path.join(directory, "visualization")); - } - fs.writeFileSync( - getSavedPathName(directory, fileName, "json"), - JSON.stringify(spec, null, 2) + fs.writeFileSync(specPath, JSON.stringify(spec, null, 2)); + const insightRes = setInsightTemplate( + getSavedPathName(directory, fileName, "md"), + userPrompt, + insightsText ); - const savedPath = getSavedPathName(directory, fileName, outputType); - if (outputType === "png") { - const base64 = await getBase64(spec, width, height); - fs.writeFileSync(savedPath, base64); - } else { - const html = await getHtmlVChart(spec, width, height); - fs.writeFileSync(savedPath, html, "utf-8"); - } - console.log(JSON.stringify({ savedPath, insightsText })); - } catch (error) { - console.log(JSON.stringify({ error })); + res.insight_path = insightRes; + } catch (error: any) { + res.error = error.toString(); + } finally { + console.log(JSON.stringify(res)); } } diff --git a/app/tool/chart_visualization/test/hack_demo.py b/app/tool/chart_visualization/test/hack_demo.py index a08321d..b9a6251 100644 --- a/app/tool/chart_visualization/test/hack_demo.py +++ b/app/tool/chart_visualization/test/hack_demo.py @@ -10,12 +10,14 @@ from app.logger import logger async def run_flow(): agents = { - "manus": Manus(), + # "manus": Manus(), "visactor": DataAnalysis(), } try: - prompt = """Here's last month's sales data from my Amazon store in './data/amazon_sales_jan2025.csv'. Could you analyze it thoroughly with visualizations and recommend specific, data-driven strategies to boost next month's sales by 10%?""" + prompt = """Here's last month's sales data from my Amazon store. Could you analyze it thoroughly with visualizations and recommend specific, data-driven strategies to boost next month's sales by 10%? +File Path: workspace/amazon_sales_jan2025.csv +""" flow = FlowFactory.create_flow( flow_type=FlowType.PLANNING, diff --git a/app/tool/python_execute.py b/app/tool/python_execute.py index 08ceffa..09bcbf1 100644 --- a/app/tool/python_execute.py +++ b/app/tool/python_execute.py @@ -10,7 +10,9 @@ class PythonExecute(BaseTool): """A tool for executing Python code with timeout and safety restrictions.""" name: str = "python_execute" - description: str = "Executes Python code string. Note: Only print outputs are visible, function return values are not captured. Use print statements to see results." + description: str = ( + "Executes Python code string. Note: Only print outputs are visible, function return values are not captured. Use print statements to see results." + ) parameters: dict = { "type": "object", "properties": { From 0a4dce552204d5efbe924919117fdf29f4ae59a5 Mon Sep 17 00:00:00 2001 From: ZJU_czx <952370295@qq.com> Date: Mon, 31 Mar 2025 17:45:23 +0800 Subject: [PATCH 5/5] fix: remove wrong default prompt in function tool --- app/tool/chart_visualization/normal_python_execute.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/tool/chart_visualization/normal_python_execute.py b/app/tool/chart_visualization/normal_python_execute.py index 06465a7..1c0aa2f 100644 --- a/app/tool/chart_visualization/normal_python_execute.py +++ b/app/tool/chart_visualization/normal_python_execute.py @@ -21,7 +21,6 @@ class NormalPythonExecute(PythonExecute): "code_type": { "description": "code type", "type": "string", - "default": "html", "enum": ["process", "report", "others"], }, },