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,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
|