plot-agent 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
plot_agent/__init__.py ADDED
File without changes
@@ -0,0 +1,349 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ import plotly.express as px
5
+ import plotly.graph_objects as go
6
+ from plotly.subplots import make_subplots
7
+ from io import StringIO
8
+ import traceback
9
+ import sys
10
+ import re
11
+ from typing import Dict, List, Optional, Any
12
+
13
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
14
+ from langchain_core.messages import AIMessage, HumanMessage
15
+ from langchain_core.tools import Tool
16
+ from pydantic import BaseModel, Field
17
+ from langchain.agents import AgentExecutor, create_openai_tools_agent
18
+ from langchain_openai import ChatOpenAI
19
+
20
+
21
+ DEFAULT_SYSTEM_PROMPT = """
22
+ You are an expert data visualization assistant that helps users create Plotly visualizations in Python.
23
+ Your job is to generate Python and Plotly code based on the user's request that will create the desired visualization
24
+ of their pandas DataFrame (df).
25
+
26
+ You have access to a pandas df with the following information:
27
+
28
+ df.info():
29
+ ```plaintext
30
+ {df_info}
31
+ ```
32
+
33
+ df.head():
34
+ ```plaintext
35
+ {df_head}
36
+ ```
37
+
38
+ {sql_context}
39
+
40
+ NOTES:
41
+ - You must use the execute_plotly_code(generated_code) tool to test and run your code
42
+ - You must paste the full code, not just a reference to the code
43
+ - You must not use fig.show() in your code as it will be executed in a headless environment
44
+ - If you need to do any data cleaning or wrangling, do it in the code before generating the plotly code as preprocessing steps assume the data is in the pandas df object
45
+ - Do not use fig.show() in your code as it will be executed in a headless environment.
46
+
47
+ IMPORTANT CODE FORMATTING INSTRUCTIONS:
48
+ 1. Include thorough, detailed comments in your code to explain what each section does
49
+ 2. Use descriptive variable names
50
+ 3. DO NOT include fig.show() in your code - the visualization will be rendered externally
51
+ 4. Ensure your code creates a variable named 'fig' that contains the Plotly figure object
52
+ 5. Structure your code with proper spacing for readability
53
+
54
+ When a user asks for a visualization:
55
+ 1. YOU MUST ALWAYS use the execute_plotly_code(generated_code) tool to test and run your code
56
+ 2. If there are errors, fix the code and run it again with execute_plotly_code(generated_code)
57
+ 3. Check that a figure object is available using does_fig_exist(). does_fig_exist() takes no arguments.
58
+
59
+ IMPORTANT: The code you generate MUST be executed using the execute_plotly_code tool or no figure will be created!
60
+ YOU MUST CALL execute_plotly_code WITH THE FULL CODE, NOT JUST A REFERENCE TO THE CODE.
61
+
62
+ YOUR WORKFLOW MUST BE:
63
+ 1. execute_plotly_code(generated_code) → 2. check that a figure object is available using does_fig_exist() → 3. (if needed) fix and execute again
64
+
65
+ Always return the final working code (with all the comments) to the user along with an explanation of what the visualization shows.
66
+ Make sure to follow best practices for data visualization, such as appropriate chart types, labels, and colors.
67
+
68
+ Remember that users may want to iterate on their visualizations, so be responsive to requests for changes.
69
+ """
70
+
71
+
72
+ # Define input schemas for the tools
73
+ class PlotDescriptionInput(BaseModel):
74
+ plot_description: str = Field(
75
+ ..., description="Description of the plot the user wants to create"
76
+ )
77
+
78
+
79
+ class GeneratedCodeInput(BaseModel):
80
+ generated_code: str = Field(
81
+ ..., description="Python code that creates a Plotly figure"
82
+ )
83
+
84
+
85
+ class PlotlyAgentExecutionEnvironment:
86
+ """Environment to safely execute plotly code and capture the fig object."""
87
+
88
+ def __init__(self, df: pd.DataFrame):
89
+ self.df = df
90
+ self.locals_dict = {
91
+ "df": df,
92
+ "px": px,
93
+ "go": go,
94
+ "pd": pd,
95
+ "np": np,
96
+ "plt": plt,
97
+ "make_subplots": make_subplots,
98
+ }
99
+ self.output = None
100
+ self.error = None
101
+ self.fig = None
102
+
103
+ def preprocess_code(self, generated_code: str) -> str:
104
+ """Preprocess code to remove fig.show() calls."""
105
+ # Remove fig.show() calls
106
+ generated_code = re.sub(r"fig\.show\(\s*\)", "", generated_code)
107
+ generated_code = re.sub(r"fig\.show\(.*\)", "", generated_code)
108
+ return generated_code
109
+
110
+ def execute_code(self, generated_code: str) -> Dict[str, Any]:
111
+ """
112
+ Execute the provided code and capture the fig object if created.
113
+
114
+ Args:
115
+ generated_code (str): The code to execute.
116
+
117
+ Returns:
118
+ Dict[str, Any]: A dictionary containing the fig object, output, error, and success status.
119
+ """
120
+ self.output = None
121
+ self.error = None
122
+
123
+ # Preprocess code to remove fig.show() calls
124
+ processed_code = self.preprocess_code(generated_code)
125
+
126
+ # Capture stdout
127
+ old_stdout = sys.stdout
128
+ sys.stdout = mystdout = StringIO()
129
+
130
+ try:
131
+ # Execute the code
132
+ exec(processed_code, globals(), self.locals_dict)
133
+
134
+ # Check if a fig object was created
135
+ if "fig" in self.locals_dict:
136
+ self.fig = self.locals_dict["fig"]
137
+ self.output = "Code executed successfully. Figure object was created."
138
+ else:
139
+ print(f"no fig object created: {processed_code}")
140
+ self.error = "Code executed without errors, but no 'fig' object was created. Make sure your code creates a variable named 'fig'."
141
+
142
+ except Exception as e:
143
+ self.error = f"Error executing code: {str(e)}\n{traceback.format_exc()}"
144
+
145
+ finally:
146
+ # Restore stdout
147
+ sys.stdout = old_stdout
148
+ captured_output = mystdout.getvalue()
149
+
150
+ if captured_output.strip():
151
+ if self.output:
152
+ self.output += f"\nOutput:\n{captured_output}"
153
+ else:
154
+ self.output = f"Output:\n{captured_output}"
155
+
156
+ return {
157
+ "fig": self.fig,
158
+ "output": self.output,
159
+ "error": self.error,
160
+ "success": self.error is None and self.fig is not None,
161
+ }
162
+
163
+
164
+ class PlotlyAgent:
165
+ """
166
+ A class that uses an LLM to generate Plotly code based on a user's plot description.
167
+ """
168
+
169
+ def __init__(self, model="gpt-4o-mini", system_prompt: Optional[str] = None):
170
+ """
171
+ Initialize the PlotlyAgent.
172
+
173
+ Args:
174
+ model (str): The model to use for the LLM.
175
+ system_prompt (Optional[str]): The system prompt to use for the LLM.
176
+ """
177
+ self.llm = ChatOpenAI(model=model)
178
+ self.df = None
179
+ self.df_info = None
180
+ self.df_head = None
181
+ self.sql_query = None
182
+ self.execution_env = None
183
+ self.chat_history = []
184
+ self.agent_executor = None
185
+ self.generated_code = None
186
+ self.system_prompt = system_prompt or DEFAULT_SYSTEM_PROMPT
187
+
188
+ def set_df(self, df: pd.DataFrame, sql_query: Optional[str] = None):
189
+ """
190
+ Set the dataframe and capture its schema and sample.
191
+
192
+ Args:
193
+ df (pd.DataFrame): The pandas dataframe to set.
194
+ sql_query (Optional[str]): The SQL query used to generate the dataframe.
195
+
196
+ Returns:
197
+ None
198
+ """
199
+ self.df = df
200
+
201
+ # Capture df.info() output
202
+ buffer = StringIO()
203
+ df.info(buf=buffer)
204
+ self.df_info = buffer.getvalue()
205
+
206
+ # Capture df.head() as string representation
207
+ self.df_head = df.head().to_string()
208
+
209
+ # Store SQL query if provided
210
+ self.sql_query = sql_query
211
+
212
+ # Initialize execution environment
213
+ self.execution_env = PlotlyAgentExecutionEnvironment(df)
214
+
215
+ # Initialize the agent with tools
216
+ self._initialize_agent()
217
+
218
+ def execute_plotly_code(self, generated_code: str) -> str:
219
+ """
220
+ Execute the provided Plotly code and return the result.
221
+
222
+ Args:
223
+ generated_code (str): The Plotly code to execute.
224
+
225
+ Returns:
226
+ str: The result of the execution.
227
+ """
228
+ if not self.execution_env:
229
+ return "Error: No dataframe has been set. Please set a dataframe first."
230
+
231
+ # Store this as the last generated code
232
+ self.generated_code = generated_code
233
+
234
+ result = self.execution_env.execute_code(generated_code)
235
+
236
+ if result["success"]:
237
+ return f"Code executed successfully! A figure object was created.\n{result.get('output', '')}"
238
+ else:
239
+ return f"Error: {result.get('error', 'Unknown error')}\n{result.get('output', '')}"
240
+
241
+ def does_fig_exist(self, *args, **kwargs) -> str:
242
+ """
243
+ Check if a figure object is available for display.
244
+
245
+ Args:
246
+ *args: Any positional arguments (ignored)
247
+ **kwargs: Any keyword arguments (ignored)
248
+
249
+ Returns:
250
+ str: A message indicating whether a figure is available for display.
251
+ """
252
+ if not self.execution_env:
253
+ return "No execution environment has been initialized. Please set a dataframe first."
254
+
255
+ if self.execution_env.fig is not None:
256
+ return "A figure is available for display."
257
+ else:
258
+ return "No figure has been created yet."
259
+
260
+ def _initialize_agent(self):
261
+ """Initialize the LangChain agent with the necessary tools and prompt."""
262
+ tools = [
263
+ Tool.from_function(
264
+ func=self.execute_plotly_code,
265
+ name="execute_plotly_code",
266
+ description="Execute the provided Plotly code and return the result",
267
+ args_schema=GeneratedCodeInput,
268
+ ),
269
+ Tool.from_function(
270
+ func=self.does_fig_exist,
271
+ name="does_fig_exist",
272
+ description="Check if a figure exists and is available for display",
273
+ args_schema=None,
274
+ ),
275
+ ]
276
+
277
+ # Create system prompt with dataframe information
278
+ sql_context = ""
279
+ if self.sql_query:
280
+ sql_context = f"In case it is useful to help with the data understanding, the df was generated using the following SQL query:\n```sql\n{self.sql_query}\n```"
281
+
282
+ prompt = ChatPromptTemplate.from_messages(
283
+ [
284
+ (
285
+ "system",
286
+ self.system_prompt.format(
287
+ df_info=self.df_info,
288
+ df_head=self.df_head,
289
+ sql_context=sql_context,
290
+ ),
291
+ ),
292
+ MessagesPlaceholder(variable_name="chat_history"),
293
+ ("human", "{input}"),
294
+ MessagesPlaceholder(variable_name="agent_scratchpad"),
295
+ ]
296
+ )
297
+
298
+ agent = create_openai_tools_agent(self.llm, tools, prompt)
299
+ self.agent_executor = AgentExecutor(
300
+ agent=agent,
301
+ tools=tools,
302
+ verbose=True,
303
+ max_iterations=10,
304
+ early_stopping_method="force",
305
+ handle_parsing_errors=True,
306
+ )
307
+
308
+ def process_message(self, user_message: str) -> str:
309
+ """Process a user message and return the agent's response."""
310
+ if not self.agent_executor:
311
+ return "Please set a dataframe first using set_df() method."
312
+
313
+ # Add user message to chat history
314
+ self.chat_history.append(HumanMessage(content=user_message))
315
+
316
+ # Reset generated_code
317
+ self.generated_code = None
318
+
319
+ # Get response from agent
320
+ response = self.agent_executor.invoke(
321
+ {"input": user_message, "chat_history": self.chat_history}
322
+ )
323
+
324
+ # Add agent response to chat history
325
+ self.chat_history.append(AIMessage(content=response["output"]))
326
+
327
+ # If the agent didn't execute the code, but did generate code, execute it directly
328
+ if self.execution_env.fig is None and self.generated_code is not None:
329
+ self.execution_env.execute_code(self.generated_code)
330
+
331
+ # If we can extract code from the response when no code was executed, try that too
332
+ if self.execution_env.fig is None and "```python" in response["output"]:
333
+ code_blocks = response["output"].split("```python")
334
+ if len(code_blocks) > 1:
335
+ generated_code = code_blocks[1].split("```")[0].strip()
336
+ self.execution_env.execute_code(generated_code)
337
+
338
+ # Return the agent's response
339
+ return response["output"]
340
+
341
+ def get_figure(self):
342
+ """Return the current figure if one exists."""
343
+ if self.execution_env and self.execution_env.fig:
344
+ return self.execution_env.fig
345
+ return None
346
+
347
+ def reset_conversation(self):
348
+ """Reset the conversation history."""
349
+ self.chat_history = []
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: plot-agent
3
+ Version: 0.1.0
4
+ Summary: An AI-powered data visualization assistant using Plotly
5
+ Home-page: https://github.com/andrewm4894/plot-agent
6
+ Author: Andre
7
+ Author-email: andrewm4894 <andrewm4894@gmail.com>
8
+ Project-URL: Homepage, https://github.com/andrewm4894/plot-agent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: author
16
+ Dynamic: home-page
17
+ Dynamic: license-file
18
+ Dynamic: requires-python
19
+
20
+ # Plot Agent
21
+
22
+ An AI-powered data visualization assistant that helps users create Plotly visualizations in Python.
23
+
24
+ ## Installation
25
+
26
+ You can install the package using pip:
27
+
28
+ ```bash
29
+ pip install plot-agent
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ Here's a simple example of how to use Plot Agent:
35
+
36
+ ```python
37
+ import pandas as pd
38
+ from plot_agent import PlotlyAgent
39
+
40
+ # Create a sample dataframe
41
+ df = pd.DataFrame({
42
+ 'x': [1, 2, 3, 4, 5],
43
+ 'y': [10, 20, 30, 40, 50]
44
+ })
45
+
46
+ # Initialize the agent
47
+ agent = PlotlyAgent()
48
+
49
+ # Set the dataframe
50
+ agent.set_df(df)
51
+
52
+ # Process a visualization request
53
+ response = agent.process_message("Create a line plot of x vs y")
54
+ ```
55
+
56
+ ## Features
57
+
58
+ - AI-powered visualization generation
59
+ - Support for various Plotly chart types
60
+ - Automatic data preprocessing
61
+ - Interactive visualization capabilities
62
+ - Integration with LangChain for advanced AI capabilities
63
+
64
+ ## Requirements
65
+
66
+ - Python 3.8 or higher
67
+ - Dependencies are automatically installed with the package
68
+
69
+ ## License
70
+
71
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,7 @@
1
+ plot_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ plot_agent/plotly_agent.py,sha256=5ddz0zyMRwqsdLzIDZwJtyyX72zqTjyp06FiJl1WEqg,12686
3
+ plot_agent-0.1.0.dist-info/licenses/LICENSE,sha256=A4DPih7wHrh4VMEG3p1PhorqdhjmGIo8nQdYNQL7daA,1062
4
+ plot_agent-0.1.0.dist-info/METADATA,sha256=2PcErKj7H4qXBm2GAsQJTnbPvDDL-od0GUk8YZ1FtUY,1666
5
+ plot_agent-0.1.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
6
+ plot_agent-0.1.0.dist-info/top_level.txt,sha256=KyOjpihUssx26Ra-37vKUQ71pI2qgJsHaRwXHJUhjzQ,11
7
+ plot_agent-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (78.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Andre
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ plot_agent