devsu commited on
Commit
d853cbf
·
1 Parent(s): cc876ad

Add initial implementation of Excel Analyst Agent with Gradio interface

Browse files

- Created .gitignore to exclude unnecessary files and directories.
- Implemented main application logic in app.py for processing Excel and CSV files.
- Added requirements.txt for necessary dependencies.
- Developed multi-agent system in app_agents for data analysis using OpenAI Agents SDK.
- Included tools for executing Python code and web searching.
- Established logging and error handling throughout the application.
- Provided user interface for file upload and query input with results display.

.gitignore ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
app.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Excel Analyst Agent - Gradio Application
3
+ Main entry point for the web interface
4
+ """
5
+
6
+ import os
7
+ import logging
8
+ import base64
9
+ from io import BytesIO
10
+ from typing import Optional, Tuple, List
11
+ import gradio as gr
12
+ import pandas as pd
13
+ from PIL import Image
14
+ from dotenv import load_dotenv
15
+ from app_agents.master_agent import MasterAgent
16
+
17
+ # Load environment variables
18
+ load_dotenv()
19
+
20
+ # Configure logging
21
+ logging.basicConfig(
22
+ level=logging.INFO,
23
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
24
+ )
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # Get OpenAI API key
28
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
29
+ if not OPENAI_API_KEY:
30
+ logger.warning("OPENAI_API_KEY not found in environment variables")
31
+
32
+
33
+ def process_analysis(
34
+ file: Optional[gr.File],
35
+ query: str,
36
+ api_key: Optional[str] = None
37
+ ) -> Tuple[str, Optional[pd.DataFrame], Optional[List[Image.Image]]]:
38
+ """
39
+ Process the user's file and query
40
+
41
+ Args:
42
+ file: Uploaded file object
43
+ query: User's natural language query
44
+ api_key: Optional API key override
45
+
46
+ Returns:
47
+ Tuple of (output_text, dataframe, images)
48
+ """
49
+ try:
50
+ # Validate inputs
51
+ if not file:
52
+ return "❌ Please upload an Excel (.xlsx) or CSV (.csv) file.", None, None
53
+
54
+ if not query or query.strip() == "":
55
+ return "❌ Please enter a query describing what you want to analyze.", None, None
56
+
57
+ # Get API key
58
+ used_api_key = api_key if api_key else OPENAI_API_KEY
59
+ # Log presence (masked) of API key from UI/env for diagnostics
60
+ if api_key:
61
+ masked = f"{api_key[:4]}...{api_key[-4:]}" if len(api_key) >= 8 else "***"
62
+ logger.info(f"API key provided via UI: True (masked: {masked})")
63
+ else:
64
+ logger.info(f"API key provided via UI: False")
65
+ if OPENAI_API_KEY:
66
+ masked_env = f"{OPENAI_API_KEY[:4]}...{OPENAI_API_KEY[-4:]}" if len(OPENAI_API_KEY) >= 8 else "***"
67
+ logger.info(f"Using OPENAI_API_KEY from env: True (masked: {masked_env})")
68
+ else:
69
+ logger.info("Using OPENAI_API_KEY from env: False")
70
+ if not used_api_key:
71
+ return "❌ Please provide an OpenAI API key either in the interface or as an environment variable (OPENAI_API_KEY).", None, None
72
+
73
+ # Get file path
74
+ file_path = file.name
75
+ logger.info(f"Processing file: {file_path}")
76
+ logger.info(f"User query: {query}")
77
+
78
+ # Validate file extension
79
+ if not (file_path.endswith('.xlsx') or file_path.endswith('.csv')):
80
+ return "❌ Please upload a valid Excel (.xlsx) or CSV (.csv) file.", None, None
81
+
82
+ # Initialize the master agent
83
+ logger.info("Initializing Master Agent...")
84
+ agent = MasterAgent(api_key=used_api_key, model="gpt-4o-mini")
85
+
86
+ # Analyze the file
87
+ logger.info("Starting analysis...")
88
+ result = agent.analyze(user_query=query, file_path=file_path)
89
+
90
+ if not result['success']:
91
+ error_msg = result.get('error', 'Unknown error occurred')
92
+ return f"❌ Analysis failed:\n\n{error_msg}", None, None
93
+
94
+ # Format output
95
+ output_parts = ["✅ **Analysis Complete**\n"]
96
+
97
+ # Add text output
98
+ if result['output']:
99
+ output_parts.append("### Results:\n")
100
+ output_parts.append(result['output'])
101
+ output_parts.append("\n")
102
+
103
+ # Add code if available
104
+ if result['code']:
105
+ output_parts.append("\n### Generated Code:\n")
106
+ output_parts.append("```python\n")
107
+ output_parts.append(result['code'])
108
+ output_parts.append("\n```\n")
109
+
110
+ output_text = "\n".join(output_parts)
111
+
112
+ # Prepare dataframe
113
+ df_output = None
114
+ if result['dataframe']:
115
+ try:
116
+ df_output = pd.DataFrame(result['dataframe'])
117
+ logger.info(f"Dataframe prepared: {len(df_output)} rows")
118
+ except Exception as e:
119
+ logger.error(f"Error preparing dataframe: {e}")
120
+ output_text += f"\n\n⚠️ Note: Could not display dataframe - {str(e)}"
121
+
122
+ # Prepare images
123
+ images_output = None
124
+ if result['images']:
125
+ try:
126
+ images_output = []
127
+ for img_base64 in result['images']:
128
+ img_data = base64.b64decode(img_base64)
129
+ img = Image.open(BytesIO(img_data))
130
+ images_output.append(img)
131
+ logger.info(f"Prepared {len(images_output)} images")
132
+ except Exception as e:
133
+ logger.error(f"Error preparing images: {e}")
134
+ output_text += f"\n\n⚠️ Note: Could not display images - {str(e)}"
135
+
136
+ return output_text, df_output, images_output
137
+
138
+ except Exception as e:
139
+ error_msg = f"Unexpected error: {str(e)}"
140
+ logger.error(error_msg, exc_info=True)
141
+ return f"❌ {error_msg}", None, None
142
+
143
+
144
+ def create_interface() -> gr.Blocks:
145
+ """
146
+ Create the Gradio interface
147
+
148
+ Returns:
149
+ Gradio Blocks interface
150
+ """
151
+ with gr.Blocks(
152
+ title="Excel Analyst Agent",
153
+ theme=gr.themes.Soft()
154
+ ) as interface:
155
+
156
+ gr.Markdown(
157
+ """
158
+ # 📊 Excel Analyst Agent
159
+
160
+ **Intelligent data analysis powered by AI**
161
+
162
+ Upload your Excel or CSV file and describe what you want to analyze in plain English.
163
+ The agent will generate and execute Python code to fulfill your request.
164
+
165
+ ### Features:
166
+ - 📈 Data analysis and statistics
167
+ - 📊 Automatic visualizations
168
+ - 🔍 Natural language queries
169
+ - 🤖 Powered by OpenAI GPT-4o-mini
170
+
171
+ ### Example queries:
172
+ - *"Show me the average sales per region and create a bar chart"*
173
+ - *"Find the top 10 customers by revenue"*
174
+ - *"Calculate monthly trends and visualize them"*
175
+ - *"Identify outliers in the price column"*
176
+ """
177
+ )
178
+
179
+ with gr.Row():
180
+ with gr.Column(scale=1):
181
+ gr.Markdown("### 📁 Input")
182
+
183
+ file_input = gr.File(
184
+ label="Upload Excel or CSV file",
185
+ file_types=[".xlsx", ".csv"],
186
+ type="filepath"
187
+ )
188
+
189
+ query_input = gr.Textbox(
190
+ label="What would you like to analyze?",
191
+ placeholder="E.g., Show me the average sales per region and create a bar chart",
192
+ lines=3
193
+ )
194
+
195
+ api_key_input = gr.Textbox(
196
+ label="OpenAI API Key (optional if set in environment)",
197
+ placeholder="sk-...",
198
+ type="password"
199
+ )
200
+
201
+ with gr.Row():
202
+ submit_btn = gr.Button("🚀 Analyze", variant="primary", size="lg")
203
+ clear_btn = gr.ClearButton(
204
+ components=[file_input, query_input, api_key_input],
205
+ value="🔄 Clear"
206
+ )
207
+
208
+ with gr.Column(scale=2):
209
+ gr.Markdown("### 📊 Results")
210
+
211
+ output_text = gr.Markdown(
212
+ label="Analysis Output",
213
+ value="Results will appear here..."
214
+ )
215
+
216
+ output_dataframe = gr.Dataframe(
217
+ label="Data Preview",
218
+ interactive=False,
219
+ wrap=True
220
+ )
221
+
222
+ output_images = gr.Gallery(
223
+ label="Visualizations",
224
+ columns=2,
225
+ height="auto"
226
+ )
227
+
228
+ gr.Markdown(
229
+ """
230
+ ---
231
+ ### 💡 Tips:
232
+ - Be specific in your queries for better results
233
+ - The agent can create multiple visualizations in one request
234
+ - If something doesn't work, try rephrasing your query
235
+ - All processing is done securely in a sandboxed environment
236
+
237
+ ### 🔒 Privacy:
238
+ - Your files are processed temporarily and not stored
239
+ - Code execution is sandboxed without internet access
240
+ - Only you and OpenAI's API see your data
241
+ """
242
+ )
243
+
244
+ # Connect the submit button
245
+ submit_btn.click(
246
+ fn=process_analysis,
247
+ inputs=[file_input, query_input, api_key_input],
248
+ outputs=[output_text, output_dataframe, output_images]
249
+ )
250
+
251
+ # Also allow Enter key to submit
252
+ query_input.submit(
253
+ fn=process_analysis,
254
+ inputs=[file_input, query_input, api_key_input],
255
+ outputs=[output_text, output_dataframe, output_images]
256
+ )
257
+
258
+ return interface
259
+
260
+
261
+ def main():
262
+ """
263
+ Main entry point
264
+ """
265
+ logger.info("Starting Excel Analyst Agent application...")
266
+
267
+ # Create and launch the interface
268
+ interface = create_interface()
269
+
270
+ interface.launch(
271
+ server_name="0.0.0.0",
272
+ server_port=7860,
273
+ share=False,
274
+ show_error=True
275
+ )
276
+
277
+
278
+ if __name__ == "__main__":
279
+ main()
280
+
281
+
app_agents/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Excel Analyst Agents (renamed package)
3
+ Multi-agent system for Excel data analysis using OpenAI Agents SDK
4
+ """
5
+
6
+ __version__ = "1.1.0"
7
+
8
+
app_agents/excel_agent.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Excel Analysis Agent - MCP client using Agents SDK
3
+ """
4
+
5
+ import logging
6
+
7
+ from agents import Agent
8
+ from agents.mcp import MCPServerStdio, create_static_tool_filter
9
+ from agents.model_settings import ModelSettings
10
+
11
+
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ EXCEL_ANALYSIS_INSTRUCTIONS = """You are an expert Excel Data Analyst Agent specialized in analyzing and visualizing data from Excel and CSV files.
17
+
18
+ IMPORTANT: You MUST use the MCP tool execute_python_code(code, file_path) to run Python code. Do NOT just explain the code — EXECUTE it via the tool.
19
+
20
+ Your capabilities:
21
+ 1. Read and analyze Excel (.xlsx) and CSV files using pandas
22
+ 2. Perform data manipulation, aggregation, and statistical analysis
23
+ 3. Create visualizations using matplotlib and seaborn
24
+ 4. Generate clear, actionable insights from data
25
+ 5. Write clean, efficient Python code
26
+
27
+ Available Actions:
28
+ - execute_python_code(code, file_path): Execute Python for data analysis. The file path is provided in user messages.
29
+
30
+ Guidelines:
31
+ - YOU MUST CALL execute_python_code — never just describe code
32
+ - Always read the file first using: pd.read_excel(file_path) or pd.read_csv(file_path)
33
+ - Store the main dataframe in a variable named 'df' (or 'result')
34
+ - CRITICAL: Always use print() to display results and data to the user
35
+ - For dataframes: use print(df.head(10)) or print(df)
36
+ - For statistics: use print(df.describe()) or print(<metric>)
37
+ - Create clear, well-labeled visualizations; do not call plt.show() (figures are captured automatically)
38
+
39
+ IMPORTANT OUTPUT FORMATTING:
40
+ After calling execute_python_code, the tool returns a result with structure: {'success': bool, 'output': str, 'error': str, 'dataframe': list, 'images': list}
41
+ - Your final response MUST be the complete JSON object returned by the execute_python_code tool
42
+ - Return it exactly as received, including all fields: 'success', 'output', 'error', 'dataframe', and 'images'
43
+ - This allows the orchestrator to properly extract dataframe and images from your response
44
+
45
+
46
+ Code Examples:
47
+
48
+ Example 1: Show first N rows
49
+ ```python
50
+ import pandas as pd
51
+
52
+ df = pd.read_excel(file_path)
53
+ print("First 10 rows:")
54
+ print(df.head(10))
55
+ ```
56
+
57
+ Example 2: Calculate statistics (average of a column)
58
+ ```python
59
+ import pandas as pd
60
+
61
+ df = pd.read_excel(file_path)
62
+ average_sales = df['Sales'].mean()
63
+ print(f"Average Sales: {average_sales:.2f}")
64
+
65
+ avg_by_category = df.groupby('Category')['Sales'].mean()
66
+ print("\nAverage Sales by Category:")
67
+ print(avg_by_category)
68
+ ```
69
+
70
+ Example 3: Create a visualization
71
+ ```python
72
+ import pandas as pd
73
+ import matplotlib.pyplot as plt
74
+
75
+ df = pd.read_excel(file_path)
76
+ country_counts = df['Country'].value_counts()
77
+ plt.figure(figsize=(10, 8))
78
+ plt.pie(country_counts, labels=country_counts.index, autopct='%1.1f%%', startangle=90)
79
+ plt.title('Distribution by Country')
80
+ plt.axis('equal')
81
+ # Do not call plt.show()
82
+ ```
83
+
84
+ Example 4: Calculate median of a column
85
+ ```python
86
+ import pandas as pd
87
+
88
+ df = pd.read_excel(file_path)
89
+ median_price = df['Sale Price'].median()
90
+ print(f"Median Sale Price: {median_price:.2f}")
91
+ ```
92
+ """
93
+
94
+
95
+ def create_excel_agent(mcp_server: MCPServerStdio, model: str = "gpt-4o-mini") -> Agent:
96
+ """
97
+ Create an Agent SDK for Excel analysis
98
+
99
+ Args:
100
+ mcp_server: MCP server already configured with execute_python_code tool filter
101
+ model: OpenAI model to use
102
+
103
+ Returns:
104
+ Agent configured for Excel analysis
105
+ """
106
+ return Agent(
107
+ name="ExcelAnalysisAgent",
108
+ instructions=EXCEL_ANALYSIS_INSTRUCTIONS,
109
+ mcp_servers=[mcp_server],
110
+ model=model,
111
+ )
112
+
113
+
app_agents/master_agent.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Master Agent - coordinates ExcelAnalysisAgent and WebSearchAgent as tools
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ import asyncio
8
+ import json
9
+ from typing import Dict, Any, Optional
10
+
11
+ from agents import Agent, Runner
12
+ from agents.mcp import MCPServerStdio, create_static_tool_filter
13
+
14
+ from .excel_agent import create_excel_agent
15
+ from .web_agent import create_web_search_agent
16
+
17
+
18
+ logging.basicConfig(level=logging.INFO)
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ MASTER_AGENT_PROMPT = """
23
+ You are the orchestrator of a multi-agent system. Your task is to take the user's query and the file path and pass it to the appropriate agent tool.
24
+
25
+ Available agent tools:
26
+ - excel_analysis_agent: Executes Python code for data analysis and visualization using pandas and matplotlib.
27
+ When calling this tool, you MUST pass the complete user query and the file path so it can execute the correct analysis.
28
+ - web_search_agent: Searches the web for documentation, examples, and solutions.
29
+
30
+ Your strategy:
31
+ 1. First, try to use the excel_analysis_agent to directly answer the user's query using the file path.
32
+ IMPORTANT: When calling excel_analysis_agent, include the FULL user query in your message to the tool.
33
+ 2. If the analysis fails or needs additional context, use the web_search_agent to find relevant information.
34
+ 3. Use the web search results to guide a retry with the excel_analysis_agent.
35
+
36
+ Always provide clear, actionable results to the user.
37
+ """
38
+
39
+
40
+ class MasterAgent:
41
+ """
42
+ Master agent that coordinates ExcelAnalysisAgent and WebSearchAgent as tools.
43
+ """
44
+
45
+ def __init__(self, api_key: str, model: str = "gpt-4o-mini"):
46
+ if api_key:
47
+ os.environ["OPENAI_API_KEY"] = api_key
48
+ self.model = model
49
+
50
+ def analyze(self, user_query: str, file_path: str) -> Dict[str, Any]:
51
+ """
52
+ Coordinate the two agents to get the best possible result
53
+ """
54
+ async def _arun():
55
+ # Create MCP servers
56
+ python_server = MCPServerStdio(
57
+ name="excel-tools-python",
58
+ params={"command": "python", "args": ["-m", "app_agents.mcp_server"]},
59
+ cache_tools_list=True,
60
+ use_structured_content=True,
61
+ tool_filter=create_static_tool_filter(allowed_tool_names=["execute_python_code"]),
62
+ )
63
+
64
+ web_server = MCPServerStdio(
65
+ name="excel-tools-web",
66
+ params={"command": "python", "args": ["-m", "app_agents.mcp_server"]},
67
+ cache_tools_list=True,
68
+ tool_filter=create_static_tool_filter(allowed_tool_names=["search_web"]),
69
+ )
70
+
71
+ # Connect servers
72
+ await python_server.connect()
73
+ await web_server.connect()
74
+
75
+ try:
76
+ # Create specialized agents using functions from their respective modules
77
+ excel_agent = create_excel_agent(mcp_server=python_server, model=self.model)
78
+ web_agent = create_web_search_agent(mcp_server=web_server, model=self.model)
79
+
80
+ # Create orchestrator agent with other agents as tools
81
+ orchestrator = Agent(
82
+ name="MasterAgent",
83
+ model=self.model,
84
+ instructions=MASTER_AGENT_PROMPT,
85
+ tools=[
86
+ excel_agent.as_tool(
87
+ tool_name="excel_analysis_agent",
88
+ tool_description="Execute Python code to analyze Excel/CSV files and create visualizations. The agent receives the user query and file path and must execute the exact analysis requested."
89
+ ),
90
+ web_agent.as_tool(
91
+ tool_name="web_search_agent",
92
+ tool_description="Search the web for up-to-date information, documentation, and code examples"
93
+ ),
94
+ ],
95
+ )
96
+
97
+ # Prepare user message with file path
98
+ user_msg = (
99
+ f"User query: {user_query}\n"
100
+ f"File path: {file_path}\n\n"
101
+ f"Call the excel_analysis_agent tool with this exact message:\n"
102
+ f"'Analyze this request: {user_query}\\n\\nThe file is located at: {file_path}\\n\\n"
103
+ f"Write Python code and call execute_python_code with that code and the same file_path.'\n\n"
104
+ f"Make sure to pass the complete user query to the excel_analysis_agent so it can perform the correct analysis."
105
+ )
106
+
107
+ # Run orchestrator
108
+ result = await Runner.run(orchestrator, user_msg, max_turns=20)
109
+
110
+ return result
111
+ finally:
112
+ # Clean up servers
113
+ for server in [python_server, web_server]:
114
+ close_fn = getattr(server, "close", None) or getattr(server, "aclose", None)
115
+ if close_fn:
116
+ res = close_fn()
117
+ if hasattr(res, "__await__"):
118
+ await res
119
+
120
+ try:
121
+ loop = asyncio.new_event_loop()
122
+ try:
123
+ asyncio.set_event_loop(loop)
124
+ result = loop.run_until_complete(_arun())
125
+ finally:
126
+ loop.close()
127
+ asyncio.set_event_loop(None)
128
+
129
+ raw_output = result.final_output or ""
130
+
131
+ # Extract dataframe and images from tool output
132
+ extracted_df = None
133
+ extracted_images = []
134
+ final_text = raw_output
135
+
136
+ # Extract from result.new_items - Item 1 (ToolCallOutputItem) contains the JSON
137
+ for item in result.new_items:
138
+ if hasattr(item, 'output') and isinstance(item.output, str):
139
+ # Extract JSON from markdown code blocks if present
140
+ json_str = item.output
141
+ if "```json" in item.output:
142
+ parts = item.output.split("```json")
143
+ if len(parts) > 1:
144
+ json_str = parts[1].split("```")[0].strip()
145
+
146
+ try:
147
+ tool_result = json.loads(json_str)
148
+ if isinstance(tool_result, dict) and "success" in tool_result:
149
+ # Extract dataframe and images from tool result
150
+ if isinstance(tool_result.get("dataframe"), list) and tool_result.get("dataframe"):
151
+ extracted_df = tool_result.get("dataframe")
152
+ if isinstance(tool_result.get("images"), list) and tool_result.get("images"):
153
+ extracted_images = tool_result.get("images")
154
+ break # Found the JSON, no need to continue
155
+ except (json.JSONDecodeError, ValueError):
156
+ continue
157
+
158
+ return {
159
+ 'success': True,
160
+ 'output': final_text,
161
+ 'dataframe': extracted_df,
162
+ 'images': extracted_images,
163
+ 'code': None,
164
+ 'error': None
165
+ }
166
+
167
+ except Exception as e:
168
+ err = f"MasterAgent error: {e}"
169
+ logger.error(err)
170
+ return {
171
+ "success": False,
172
+ "output": None,
173
+ "dataframe": None,
174
+ "images": [],
175
+ "code": None,
176
+ "error": err,
177
+ }
178
+
179
+
app_agents/mcp_server.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP stdio server exposing execute_python_code and search_web tools (FastMCP)
3
+ """
4
+
5
+ from fastmcp import FastMCP
6
+
7
+
8
+ mcp = FastMCP("excel-tools")
9
+
10
+ # Lazy singletons to avoid heavy imports at startup
11
+ _python_tool = None
12
+ _web_tool = None
13
+
14
+
15
+ @mcp.tool
16
+ def execute_python_code(code: str, file_path: str) -> str:
17
+ """Execute Python code and return results as JSON string to avoid MCP serialization issues"""
18
+ import json
19
+ global _python_tool
20
+ if _python_tool is None:
21
+ from app_agents.tools.python_tool import PythonSandboxTool
22
+ _python_tool = PythonSandboxTool(timeout=30)
23
+ result = _python_tool.execute(code=code, file_path=file_path)
24
+ return json.dumps(result, ensure_ascii=False)
25
+
26
+
27
+ @mcp.tool
28
+ def search_web(query: str) -> str:
29
+ global _web_tool
30
+ if _web_tool is None:
31
+ from app_agents.tools.web_search_tool import WebSearchTool
32
+ _web_tool = WebSearchTool(max_results=5)
33
+ res = _web_tool.search(query)
34
+ if res.get("success"):
35
+ return _web_tool.format_results(res["results"])
36
+ return f"Search failed: {res.get('error', 'Unknown error')}"
37
+
38
+
39
+ if __name__ == "__main__":
40
+ # stdio is the default; we specify it explicitly for clarity
41
+ mcp.run(transport="stdio")
42
+
43
+
app_agents/tools/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tools package for Excel Analyst Agent
3
+ Contains Python sandbox and web search tools
4
+ """
5
+
6
+ from .python_tool import PythonSandboxTool
7
+ from .web_search_tool import WebSearchTool
8
+
9
+ __all__ = ["PythonSandboxTool", "WebSearchTool"]
10
+
11
+
app_agents/tools/python_tool.py ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Python Sandbox Tool for safe code execution
3
+ Uses standard exec() with AST validation and namespace control
4
+ """
5
+
6
+ import io
7
+ import sys
8
+ import base64
9
+ import logging
10
+ import ast
11
+ from typing import Dict, Any, Optional, Set
12
+ from contextlib import redirect_stdout, redirect_stderr
13
+ from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError
14
+ import pandas as pd
15
+ import numpy as np
16
+ import matplotlib
17
+ matplotlib.use('Agg') # Non-interactive backend
18
+ import matplotlib.pyplot as plt
19
+ import seaborn as sns
20
+
21
+ logging.basicConfig(level=logging.INFO)
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class TimeoutException(Exception):
26
+ """Exception raised when code execution times out"""
27
+ pass
28
+
29
+
30
+ class CodeValidator(ast.NodeVisitor):
31
+ """
32
+ AST validator to block dangerous operations
33
+ """
34
+
35
+ # Dangerous functions/modules to block
36
+ BLOCKED_NAMES: Set[str] = {
37
+ 'eval', 'exec', 'compile',
38
+ 'open', 'file', 'input', 'raw_input',
39
+ 'execfile', 'reload', 'breakpoint',
40
+ 'exit', 'quit', 'help',
41
+ }
42
+
43
+ # Dangerous modules to block
44
+ BLOCKED_MODULES: Set[str] = {
45
+ 'os', 'sys', 'subprocess', 'socket', 'urllib',
46
+ 'requests', 'http', 'ftplib', 'telnetlib',
47
+ 'pickle', 'shelve', 'marshal', 'importlib',
48
+ }
49
+
50
+ def __init__(self):
51
+ self.errors = []
52
+
53
+ def visit_Import(self, node):
54
+ """Check import statements"""
55
+ for alias in node.names:
56
+ module_name = alias.name.split('.')[0]
57
+ if module_name in self.BLOCKED_MODULES:
58
+ self.errors.append(f"Import of '{alias.name}' is not allowed")
59
+ self.generic_visit(node)
60
+
61
+ def visit_ImportFrom(self, node):
62
+ """Check from-import statements"""
63
+ if node.module:
64
+ module_name = node.module.split('.')[0]
65
+ if module_name in self.BLOCKED_MODULES:
66
+ self.errors.append(f"Import from '{node.module}' is not allowed")
67
+ self.generic_visit(node)
68
+
69
+ def visit_Name(self, node):
70
+ """Check for blocked names"""
71
+ if node.id in self.BLOCKED_NAMES:
72
+ self.errors.append(f"Use of '{node.id}' is not allowed")
73
+ self.generic_visit(node)
74
+
75
+ def visit_Attribute(self, node):
76
+ """Check for dangerous attribute access"""
77
+ # Block access to __builtins__, __globals__, etc.
78
+ if isinstance(node.attr, str) and node.attr.startswith('__') and node.attr.endswith('__'):
79
+ if node.attr not in {'__init__', '__str__', '__repr__'}:
80
+ self.errors.append(f"Access to '{node.attr}' is not allowed")
81
+ self.generic_visit(node)
82
+
83
+
84
+ class PythonSandboxTool:
85
+ """
86
+ Safe Python code execution sandbox with restricted access
87
+ """
88
+
89
+ def __init__(self, timeout: int = 30):
90
+ """
91
+ Initialize the sandbox tool
92
+
93
+ Args:
94
+ timeout: Maximum execution time in seconds (default: 30)
95
+ """
96
+ self.timeout = timeout
97
+ self.allowed_modules = {
98
+ 'pd': pd,
99
+ 'pandas': pd,
100
+ 'np': np,
101
+ 'numpy': np,
102
+ 'plt': plt,
103
+ 'matplotlib': matplotlib,
104
+ 'sns': sns,
105
+ 'seaborn': sns,
106
+ }
107
+
108
+ def _safe_import(self, name, globals=None, locals=None, fromlist=(), level=0):
109
+ """
110
+ Custom import function that uses pre-loaded modules
111
+
112
+ Args:
113
+ name: Module name to import
114
+ globals: Global namespace (ignored)
115
+ locals: Local namespace (ignored)
116
+ fromlist: Names to import from module
117
+ level: Relative import level
118
+
119
+ Returns:
120
+ Pre-loaded module object if allowed
121
+
122
+ Raises:
123
+ ImportError: If module is not allowed
124
+ """
125
+ # Map import names to allowed modules
126
+ allowed = {
127
+ 'pandas': pd,
128
+ 'numpy': np,
129
+ 'matplotlib': matplotlib,
130
+ 'seaborn': sns,
131
+ }
132
+
133
+ # Return pre-loaded module if allowed
134
+ if name in allowed:
135
+ return allowed[name]
136
+
137
+ # Handle matplotlib sub-modules (e.g., matplotlib.pyplot)
138
+ if name.startswith('matplotlib.'):
139
+ # Return the base matplotlib module
140
+ # Python will then access the sub-module as an attribute
141
+ return matplotlib
142
+
143
+ # Check if it's a blocked module
144
+ if name.split('.')[0] in CodeValidator.BLOCKED_MODULES:
145
+ raise ImportError(f"Import of '{name}' is not allowed for security reasons")
146
+
147
+ # For any other module not specifically allowed, raise error
148
+ raise ImportError(f"Cannot import '{name}'. Only pandas, numpy, matplotlib, and seaborn are allowed.")
149
+
150
+ def _create_safe_globals(self, file_path: Optional[str] = None) -> Dict[str, Any]:
151
+ """
152
+ Create a safe globals dictionary with whitelisted modules
153
+
154
+ Args:
155
+ file_path: Path to the uploaded Excel/CSV file
156
+
157
+ Returns:
158
+ Dictionary of safe globals
159
+ """
160
+ # Create a limited builtins dictionary
161
+ safe_builtins = {
162
+ 'print': print,
163
+ 'len': len,
164
+ 'range': range,
165
+ 'enumerate': enumerate,
166
+ 'zip': zip,
167
+ 'map': map,
168
+ 'filter': filter,
169
+ 'sum': sum,
170
+ 'min': min,
171
+ 'max': max,
172
+ 'abs': abs,
173
+ 'round': round,
174
+ 'sorted': sorted,
175
+ 'list': list,
176
+ 'dict': dict,
177
+ 'set': set,
178
+ 'tuple': tuple,
179
+ 'str': str,
180
+ 'int': int,
181
+ 'float': float,
182
+ 'bool': bool,
183
+ 'isinstance': isinstance,
184
+ 'type': type,
185
+ 'hasattr': hasattr,
186
+ 'getattr': getattr,
187
+ 'setattr': setattr,
188
+ 'True': True,
189
+ 'False': False,
190
+ 'None': None,
191
+ '__import__': self._safe_import, # Enable safe imports
192
+ # Exception types (necessary for try/except blocks)
193
+ 'Exception': Exception,
194
+ 'ValueError': ValueError,
195
+ 'TypeError': TypeError,
196
+ 'KeyError': KeyError,
197
+ 'IndexError': IndexError,
198
+ 'AttributeError': AttributeError,
199
+ 'RuntimeError': RuntimeError,
200
+ 'ImportError': ImportError,
201
+ 'ZeroDivisionError': ZeroDivisionError,
202
+ # Additional useful builtins
203
+ 'locals': locals,
204
+ 'globals': lambda: safe_builtins, # Return safe version
205
+ 'dir': dir,
206
+ 'any': any,
207
+ 'all': all,
208
+ }
209
+
210
+ safe_dict = {
211
+ '__builtins__': safe_builtins,
212
+ '__name__': 'sandbox',
213
+ }
214
+
215
+ # Add allowed modules (also available directly without import)
216
+ safe_dict.update(self.allowed_modules)
217
+
218
+ # Add file path if provided
219
+ if file_path:
220
+ safe_dict['file_path'] = file_path
221
+
222
+ return safe_dict
223
+
224
+ def _execute_code(self, byte_code, safe_dict, stdout_capture, stderr_capture) -> None:
225
+ """
226
+ Helper method to execute code (can be run in a separate thread)
227
+
228
+ Args:
229
+ byte_code: Compiled code object
230
+ safe_dict: Safe globals dictionary
231
+ stdout_capture: StringIO for capturing stdout
232
+ stderr_capture: StringIO for capturing stderr
233
+ """
234
+ with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
235
+ exec(byte_code, safe_dict)
236
+
237
+ def execute(self, code: str, file_path: Optional[str] = None) -> Dict[str, Any]:
238
+ """
239
+ Execute Python code in a restricted environment
240
+
241
+ Args:
242
+ code: Python code to execute
243
+ file_path: Path to the uploaded Excel/CSV file
244
+
245
+ Returns:
246
+ Dictionary containing:
247
+ - success: bool
248
+ - output: str (stdout)
249
+ - error: str (if any)
250
+ - dataframe: dict (if df variable exists)
251
+ - images: list of base64 encoded images
252
+ """
253
+ result = {
254
+ 'success': False,
255
+ 'output': '',
256
+ 'error': '',
257
+ 'dataframe': None,
258
+ 'images': []
259
+ }
260
+
261
+ try:
262
+ # Validate code using AST
263
+ try:
264
+ tree = ast.parse(code, filename='<user_code>', mode='exec')
265
+ except SyntaxError as e:
266
+ result['error'] = f"Syntax error: {str(e)}"
267
+ logger.error(f"Syntax error: {str(e)}")
268
+ return result
269
+
270
+ # Check for dangerous operations
271
+ validator = CodeValidator()
272
+ validator.visit(tree)
273
+
274
+ if validator.errors:
275
+ result['error'] = f"Security validation failed:\n" + "\n".join(validator.errors)
276
+ logger.error(f"Validation errors: {validator.errors}")
277
+ return result
278
+
279
+ # Configure pandas display to avoid truncated columns/rows in printed output
280
+ try:
281
+ pd.set_option('display.max_columns', None)
282
+ pd.set_option('display.width', 2000)
283
+ pd.set_option('display.max_colwidth', None)
284
+ pd.set_option('display.expand_frame_repr', False)
285
+ except Exception:
286
+ pass
287
+
288
+ # Compile the validated code
289
+ byte_code = compile(tree, filename='<user_code>', mode='exec')
290
+
291
+ # Create safe execution environment
292
+ safe_dict = self._create_safe_globals(file_path)
293
+
294
+ # Capture stdout and stderr
295
+ stdout_capture = io.StringIO()
296
+ stderr_capture = io.StringIO()
297
+
298
+ # Execute with timeout using ThreadPoolExecutor
299
+ try:
300
+ with ThreadPoolExecutor(max_workers=1) as executor:
301
+ future = executor.submit(
302
+ self._execute_code,
303
+ byte_code,
304
+ safe_dict,
305
+ stdout_capture,
306
+ stderr_capture
307
+ )
308
+ # Wait for completion with timeout
309
+ future.result(timeout=self.timeout)
310
+
311
+ # Get stdout
312
+ result['output'] = stdout_capture.getvalue()
313
+
314
+ # Check for dataframe in the namespace
315
+ if 'df' in safe_dict and isinstance(safe_dict['df'], pd.DataFrame):
316
+ # Convert dataframe to dict and make it JSON-safe
317
+ records = safe_dict['df'].head(5).to_dict('records')
318
+ result['dataframe'] = self._make_json_safe_records(records)
319
+ elif 'result' in safe_dict and isinstance(safe_dict['result'], pd.DataFrame):
320
+ records = safe_dict['result'].head(5).to_dict('records')
321
+ result['dataframe'] = self._make_json_safe_records(records)
322
+
323
+ # Capture matplotlib figures
324
+ figures = [plt.figure(i) for i in plt.get_fignums()]
325
+ for fig in figures:
326
+ buf = io.BytesIO()
327
+ fig.savefig(buf, format='png', bbox_inches='tight', dpi=100)
328
+ buf.seek(0)
329
+ img_base64 = base64.b64encode(buf.read()).decode('utf-8')
330
+ result['images'].append(img_base64)
331
+ buf.close()
332
+
333
+ # Close all figures to free memory
334
+ plt.close('all')
335
+
336
+ result['success'] = True
337
+ logger.info("Code executed successfully")
338
+
339
+ except FuturesTimeoutError:
340
+ result['error'] = f"Execution timeout: Code took longer than {self.timeout} seconds"
341
+ logger.error(f"Timeout: Code execution exceeded {self.timeout} seconds")
342
+ except Exception as e:
343
+ result['error'] = f"Runtime error: {str(e)}"
344
+ logger.error(f"Runtime error: {str(e)}")
345
+
346
+ # Include stderr if available
347
+ stderr_output = stderr_capture.getvalue()
348
+ if stderr_output:
349
+ result['error'] += f"\n{stderr_output}"
350
+
351
+ except Exception as e:
352
+ result['error'] = f"Sandbox error: {str(e)}"
353
+ logger.error(f"Sandbox error: {str(e)}")
354
+
355
+ logger.info(f"Result: {result}")
356
+
357
+ return result
358
+
359
+ def _make_json_safe_records(self, records):
360
+ """
361
+ Convert a list of dict records into JSON-serializable values:
362
+ - pandas.Timestamp -> ISO string
363
+ - numpy types -> native Python
364
+ - NaN -> None
365
+ """
366
+ import math
367
+ from datetime import datetime
368
+
369
+ def to_safe(value):
370
+ if isinstance(value, pd.Timestamp):
371
+ return value.isoformat()
372
+ if isinstance(value, datetime):
373
+ return value.isoformat()
374
+ if isinstance(value, np.generic):
375
+ # numpy scalar -> native python
376
+ py = value.item()
377
+ if isinstance(py, float) and (math.isnan(py) or py == float('inf') or py == float('-inf')):
378
+ return None
379
+ return py
380
+ if isinstance(value, float):
381
+ if math.isnan(value) or value == float('inf') or value == float('-inf'):
382
+ return None
383
+ return value
384
+ return value
385
+
386
+ safe_records = []
387
+ for rec in records or []:
388
+ safe_rec = {k: to_safe(v) for k, v in rec.items()}
389
+ safe_records.append(safe_rec)
390
+ return safe_records
391
+
392
+ def get_tool_definition(self) -> Dict[str, Any]:
393
+ """
394
+ Get the tool definition for OpenAI function calling
395
+
396
+ Returns:
397
+ Tool definition dictionary
398
+ """
399
+ return {
400
+ "type": "function",
401
+ "function": {
402
+ "name": "execute_python_code",
403
+ "description": "Execute Python code to analyze Excel/CSV data. Use pandas (pd), numpy (np), matplotlib (plt), and seaborn (sns). The uploaded file path is available as 'file_path' variable. Store results in 'df' or 'result' variable to return dataframes.",
404
+ "parameters": {
405
+ "type": "object",
406
+ "properties": {
407
+ "code": {
408
+ "type": "string",
409
+ "description": "Python code to execute. Must use pandas to read the file (e.g., pd.read_excel(file_path) or pd.read_csv(file_path)). Store final dataframe in 'df' or 'result' variable."
410
+ }
411
+ },
412
+ "required": ["code"]
413
+ }
414
+ }
415
+ }
416
+
417
+
418
+
app_agents/tools/web_search_tool.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Web Search Tool using DuckDuckGo
3
+ Provides web search capability for finding documentation and examples
4
+ """
5
+
6
+ import logging
7
+ from typing import Dict, Any, List
8
+ from ddgs import DDGS
9
+
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class WebSearchTool:
15
+ """
16
+ Web search tool using DuckDuckGo API
17
+ """
18
+
19
+ def __init__(self, max_results: int = 5):
20
+ """
21
+ Initialize the web search tool
22
+
23
+ Args:
24
+ max_results: Maximum number of search results to return (default: 5)
25
+ """
26
+ self.max_results = max_results
27
+
28
+ def search(self, query: str) -> Dict[str, Any]:
29
+ """
30
+ Search the web using DuckDuckGo
31
+
32
+ Args:
33
+ query: Search query string
34
+
35
+ Returns:
36
+ Dictionary containing:
37
+ - success: bool
38
+ - results: list of search results
39
+ - error: str (if any)
40
+ """
41
+ result = {
42
+ 'success': False,
43
+ 'results': [],
44
+ 'error': ''
45
+ }
46
+
47
+ try:
48
+ logger.info(f"Searching for: {query}")
49
+
50
+ with DDGS() as ddgs:
51
+ search_results = list(ddgs.text(
52
+ query,
53
+ max_results=self.max_results
54
+ ))
55
+
56
+ # Format results
57
+ formatted_results = []
58
+ for idx, res in enumerate(search_results, 1):
59
+ formatted_results.append({
60
+ 'position': idx,
61
+ 'title': res.get('title', ''),
62
+ 'snippet': res.get('body', ''),
63
+ 'url': res.get('href', '')
64
+ })
65
+
66
+ result['results'] = formatted_results
67
+ result['success'] = True
68
+ logger.info(f"Found {len(formatted_results)} results")
69
+
70
+ except Exception as e:
71
+ result['error'] = f"Search error: {str(e)}"
72
+ logger.error(f"Search error: {str(e)}")
73
+
74
+ return result
75
+
76
+ def format_results(self, search_results: List[Dict[str, Any]]) -> str:
77
+ """
78
+ Format search results into a readable string
79
+
80
+ Args:
81
+ search_results: List of search result dictionaries
82
+
83
+ Returns:
84
+ Formatted string of search results
85
+ """
86
+ if not search_results:
87
+ return "No results found."
88
+
89
+ formatted = "Search Results:\n\n"
90
+ for res in search_results:
91
+ formatted += f"{res['position']}. {res['title']}\n"
92
+ formatted += f" {res['snippet']}\n"
93
+ formatted += f" URL: {res['url']}\n\n"
94
+
95
+ return formatted
96
+
97
+ def get_tool_definition(self) -> Dict[str, Any]:
98
+ """
99
+ Get the tool definition for OpenAI function calling
100
+
101
+ Returns:
102
+ Tool definition dictionary
103
+ """
104
+ return {
105
+ "type": "function",
106
+ "function": {
107
+ "name": "search_web",
108
+ "description": "Search the web using DuckDuckGo to find Python/pandas documentation, code examples, or solutions to data analysis problems. Use this when you need help with specific pandas operations, matplotlib visualizations, or data manipulation techniques.",
109
+ "parameters": {
110
+ "type": "object",
111
+ "properties": {
112
+ "query": {
113
+ "type": "string",
114
+ "description": "Search query. Be specific and include relevant keywords like 'pandas', 'python', 'matplotlib', etc."
115
+ }
116
+ },
117
+ "required": ["query"]
118
+ }
119
+ }
120
+ }
121
+
122
+
123
+
app_agents/web_agent.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ WebSearch Agent - MCP client using Agents SDK
3
+ """
4
+
5
+ import logging
6
+
7
+ from agents import Agent
8
+ from agents.mcp import MCPServerStdio, create_static_tool_filter
9
+ from agents.model_settings import ModelSettings
10
+
11
+
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ # System prompt for the WebSearchAgent
17
+ WEB_SEARCH_INSTRUCTIONS = """You are a research assistant specialized in Python, pandas, matplotlib, and data analysis.
18
+
19
+ Your role is to search the web for:
20
+ - Documentation and API references
21
+ - Solutions to Python/pandas errors
22
+ - Code examples and best practices
23
+ - Matplotlib/seaborn visualization techniques
24
+
25
+ Use the MCP tool `search_web(query)` to find relevant information.
26
+
27
+ Guidelines:
28
+ - Provide concise, actionable summaries
29
+ - Include concrete code snippets when available
30
+ - Focus on authoritative sources (official docs, Stack Overflow, etc.)
31
+ - Return your findings in a clear, structured format
32
+ - The code you provide should be formatted as
33
+ ```python
34
+ YOUR CODE HERE
35
+ ```
36
+ """
37
+
38
+
39
+ def create_web_search_agent(mcp_server: MCPServerStdio, model: str = "gpt-4o-mini") -> Agent:
40
+ """
41
+ Create an Agent SDK for web search
42
+
43
+ Args:
44
+ mcp_server: MCP server already configured with search_web tool filter
45
+ model: OpenAI model to use
46
+
47
+ Returns:
48
+ Agent configured for web search
49
+ """
50
+ return Agent(
51
+ name="WebSearchAgent",
52
+ instructions=WEB_SEARCH_INSTRUCTIONS,
53
+ mcp_servers=[mcp_server],
54
+ model=model,
55
+ model_settings=ModelSettings(tool_choice="required"),
56
+ )
57
+
58
+
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ openai>=1.12.0
3
+ pandas>=2.0.0
4
+ numpy>=1.24.0
5
+ openpyxl>=3.1.0
6
+ matplotlib>=3.7.0
7
+ seaborn>=0.12.0
8
+ ddgs
9
+ python-dotenv>=1.0.0
10
+ openai-agents
11
+ fastmcp