|
import os |
|
import io |
|
import sys |
|
import re |
|
import traceback |
|
import subprocess |
|
import gradio as gr |
|
import pandas as pd |
|
from dotenv import load_dotenv |
|
from crewai import Crew, Agent, Task, Process, LLM |
|
from crewai_tools import FileReadTool |
|
from pydantic import BaseModel, Field |
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') |
|
if not OPENAI_API_KEY: |
|
raise ValueError("OPENAI_API_KEY environment variable not set") |
|
|
|
llm = LLM( |
|
model="openai/gpt-4o", |
|
api_key=OPENAI_API_KEY, |
|
temperature=0.7 |
|
) |
|
|
|
|
|
query_parser_agent = Agent( |
|
role="Stock Data Analyst", |
|
goal="Extract stock details and fetch required data from this user query: {query}.", |
|
backstory="You are a financial analyst specializing in stock market data retrieval.", |
|
llm=llm, |
|
verbose=True, |
|
memory=True, |
|
) |
|
|
|
|
|
class QueryAnalysisOutput(BaseModel): |
|
"""Structured output for the query analysis task.""" |
|
symbols: list[str] = Field( |
|
..., |
|
json_schema_extra={"description": "List of stock ticker symbols (e.g., ['TSLA', 'AAPL'])."} |
|
) |
|
timeframe: str = Field( |
|
..., |
|
json_schema_extra={"description": "Time period (e.g., '1d', '1mo', '1y')."} |
|
) |
|
action: str = Field( |
|
..., |
|
json_schema_extra={"description": "Action to be performed (e.g., 'fetch', 'plot')."} |
|
) |
|
|
|
|
|
query_parsing_task = Task( |
|
description="Analyze the user query and extract stock details.", |
|
expected_output="A dictionary with keys: 'symbol', 'timeframe', 'action'.", |
|
output_pydantic=QueryAnalysisOutput, |
|
agent=query_parser_agent, |
|
) |
|
|
|
|
|
code_writer_agent = Agent( |
|
role="Senior Python Developer", |
|
goal="Write Python code to visualize stock data.", |
|
backstory="""You are a Senior Python developer specializing in stock market data visualization. |
|
You are also a Pandas, Matplotlib and yfinance library expert. |
|
You are skilled at writing production-ready Python code. |
|
Ensure the code handles potential variations in the DataFrame structure returned by yfinance, |
|
especially for different timeframes or delisted stocks. |
|
Crucially, ensure the generated script saves any generated plot as 'plot.png' using `plt.savefig('plot.png')` before the script ends.""", |
|
llm=llm, |
|
verbose=True, |
|
) |
|
|
|
code_writer_task = Task( |
|
description="""Write Python code to visualize stock data based on the inputs from the stock analyst |
|
where you would find stock symbol, timeframe and action.""", |
|
expected_output="A clean and executable Python script file (.py) for stock visualization.", |
|
agent=code_writer_agent, |
|
) |
|
|
|
|
|
code_output_agent = Agent( |
|
role="Python Code Presenter", |
|
goal="Present the generated Python code for stock visualization.", |
|
backstory="You are an expert in presenting Python code in a clear and readable format.", |
|
allow_delegation=False, |
|
llm=llm, |
|
verbose=True, |
|
) |
|
|
|
code_output_task = Task( |
|
description="""Receive the Python code for stock visualization from the code writer agent and present it.""", |
|
expected_output="The complete Python script for stock visualization.", |
|
agent=code_output_agent, |
|
) |
|
|
|
crew = Crew( |
|
agents=[query_parser_agent, code_writer_agent, code_output_agent], |
|
tasks=[query_parsing_task, code_writer_task, code_output_task], |
|
process=Process.sequential |
|
) |
|
|
|
|
|
def run_crewai_process(user_query, model, temperature): |
|
""" |
|
Runs the CrewAI process, captures agent thoughts, gets generated code, |
|
executes the code, and returns results, including plot. |
|
|
|
Args: |
|
user_query (str): The user's query for the CrewAI process. |
|
model (str): The model to use for the LLM. |
|
temperature (float): The temperature to use for the LLM. |
|
|
|
Yields: |
|
tuple: A tuple containing the agent thoughts (str), the final answer (list of dicts), |
|
the generated code (str), the execution output (str), and plot file path (str or None). |
|
""" |
|
|
|
output_buffer = io.StringIO() |
|
original_stdout = sys.stdout |
|
sys.stdout = output_buffer |
|
agent_thoughts = "" |
|
generated_code = "" |
|
execution_output = "" |
|
generated_plot_path = None |
|
final_answer_chat = [{"role": "user", "content": user_query}] |
|
|
|
try: |
|
|
|
initial_message = {"role": "assistant", "content": "Starting CrewAI process..."} |
|
final_answer_chat = [{"role": "user", "content": str(user_query)}, initial_message] |
|
yield final_answer_chat, agent_thoughts, generated_code, execution_output, None, None |
|
|
|
|
|
final_result = crew.kickoff(inputs={"query": user_query}) |
|
|
|
|
|
agent_thoughts = output_buffer.getvalue() |
|
|
|
|
|
processing_message = {"role": "assistant", "content": "Processing complete. Generating code..."} |
|
final_answer_chat = [{"role": "user", "content": str(user_query)}, processing_message] |
|
yield final_answer_chat, agent_thoughts, generated_code, execution_output, None, None |
|
|
|
|
|
generated_code_raw = str(final_result).strip() |
|
|
|
|
|
code_match = re.search(r"```python\n(.*?)\n```", generated_code_raw, re.DOTALL) |
|
if code_match: |
|
generated_code = code_match.group(1).strip() |
|
else: |
|
|
|
generated_code = generated_code_raw |
|
if not generated_code.strip(): |
|
execution_output = "CrewAI process completed, but no code was generated." |
|
final_answer_chat.append({"role": "assistant", "content": execution_output}) |
|
yield agent_thoughts, final_answer_chat, generated_code, execution_output, generated_plot_path |
|
return |
|
|
|
|
|
code_gen_message = {"role": "assistant", "content": "Code generation complete. See the 'Generated Code' box. Attempting to execute code..."} |
|
final_answer_chat = [{"role": "user", "content": str(user_query)}, code_gen_message] |
|
yield final_answer_chat, agent_thoughts, generated_code, execution_output, None, None |
|
|
|
|
|
plot_file_path = 'plot.png' |
|
|
|
if generated_code: |
|
try: |
|
|
|
temp_script_path = "generated_script.py" |
|
with open(temp_script_path, "w") as f: |
|
f.write(generated_code) |
|
|
|
|
|
debug_script = """ |
|
import os |
|
import sys |
|
import traceback |
|
|
|
try: |
|
print("\n" + "="*80) |
|
print("STOCK PLOT GENERATION") |
|
print("="*80) |
|
|
|
# Import the stock plot module |
|
try: |
|
import stock_plot |
|
|
|
# Generate the plot using the module |
|
plot_path = stock_plot.plot_stock_gain(["META"], "ytd") |
|
|
|
print("\n" + "="*80) |
|
print("PLOT GENERATION COMPLETE") |
|
print("="*80) |
|
|
|
if plot_path and os.path.exists(plot_path): |
|
file_size = os.path.getsize(plot_path) |
|
print(f"✓ Plot generated successfully: {os.path.abspath(plot_path)}") |
|
print(f"✓ File size: {file_size} bytes") |
|
|
|
# Also check for plot.png in the root directory |
|
if os.path.exists('plot.png'): |
|
print(f"✓ Main plot.png found: {os.path.abspath('plot.png')}") |
|
else: |
|
print("ℹ️ plot.png not found in root directory") |
|
else: |
|
print("❌ Failed to generate plot or plot file not found") |
|
|
|
except ImportError as e: |
|
print(f"❌ Error importing stock_plot module: {e}") |
|
print("Make sure the stock_plot.py file exists in the same directory.") |
|
raise |
|
|
|
print("\n" + "="*80) |
|
print("DIRECTORY CONTENTS") |
|
print("="*80) |
|
|
|
# List all files in the current directory |
|
for f in sorted(os.listdir('.')): |
|
try: |
|
fpath = os.path.join('.', f) |
|
if os.path.isfile(fpath): |
|
size = os.path.getsize(fpath) |
|
print(f" - {f} ({size} bytes)") |
|
else: |
|
print(f" - {f}/ (dir)") |
|
except Exception as e: |
|
print(f" - {f} (error: {e})") |
|
|
|
# Check for generated_plots directory |
|
plots_dir = 'generated_plots' |
|
if os.path.exists(plots_dir) and os.path.isdir(plots_dir): |
|
print(f"\nContents of {plots_dir}/:") |
|
try: |
|
for f in sorted(os.listdir(plots_dir)): |
|
try: |
|
fpath = os.path.join(plots_dir, f) |
|
if os.path.isfile(fpath): |
|
size = os.path.getsize(fpath) |
|
print(f" - {f} ({size} bytes)") |
|
except Exception as e: |
|
print(f" - {f} (error: {e})") |
|
except Exception as e: |
|
print(f" Error reading {plots_dir}: {e}") |
|
|
|
except Exception as e: |
|
print(f"\n❌ UNEXPECTED ERROR: {str(e)}") |
|
print("\nTraceback:") |
|
traceback.print_exc() |
|
sys.exit(1) |
|
""" |
|
|
|
|
|
try: |
|
|
|
fig, ax = plt.subplots(figsize=(10, 6)) |
|
|
|
|
|
x = [1, 2, 3, 4, 5] |
|
y = [1, 4, 9, 16, 25] |
|
|
|
|
|
ax.plot(x, y, 'b-', linewidth=2, label='Sample Data') |
|
|
|
|
|
ax.set_title('Test Plot - Matplotlib Verification', fontsize=14) |
|
ax.set_xlabel('X Axis', fontsize=12) |
|
ax.set_ylabel('Y Axis', fontsize=12) |
|
|
|
|
|
ax.grid(True, linestyle='--', alpha=0.7) |
|
ax.legend(fontsize=10) |
|
|
|
|
|
plt.tight_layout() |
|
|
|
|
|
test_plot_path = 'test_plot.png' |
|
fig.savefig(test_plot_path, dpi=120, bbox_inches='tight') |
|
print(f"✓ Test plot saved to: {os.path.abspath(test_plot_path)}") |
|
|
|
|
|
plt.close(fig) |
|
|
|
except Exception as e: |
|
print(f"❌ Error creating test plot: {e}") |
|
print("Traceback:") |
|
traceback.print_exc() |
|
|
|
|
|
test_formats = [ |
|
('test_plot.png', 'PNG'), |
|
('test_plot.jpg', 'JPEG'), |
|
('test_plot.pdf', 'PDF') |
|
] |
|
|
|
for filename, fmt in test_formats: |
|
try: |
|
test_fig.savefig(filename, bbox_inches='tight', dpi=100) |
|
abs_path = os.path.abspath(filename) |
|
file_size = os.path.getsize(filename) |
|
print(f"✓ Saved {{fmt}} to: {{abs_path}} ({{file_size}} bytes)") |
|
|
|
|
|
if file_size == 0: |
|
print(f" ✗ WARNING: {{fmt}} file is empty!") |
|
elif file_size < 1024: |
|
print(f" ⚠ WARNING: {{fmt}} file is unusually small ({{file_size}} bytes)") |
|
|
|
except Exception as e: |
|
print(f"✗ Failed to save {{fmt}}: {{str(e)}}") |
|
|
|
|
|
plt.close(test_fig) |
|
|
|
|
|
print("\n" + "="*80) |
|
print("TEST 2: FILE SYSTEM VERIFICATION") |
|
print("="*80) |
|
|
|
|
|
for filename, _ in test_formats: |
|
if os.path.exists(filename): |
|
try: |
|
file_size = os.path.getsize(filename) |
|
print(f"✓ Found {{filename}} ({{file_size}} bytes)") |
|
|
|
with open(filename, 'rb') as f: |
|
header = f.read(4) |
|
print(f" File header: {{header[:20].hex()}}...") |
|
except Exception as e: |
|
print(f"✗ Error reading {{filename}}: {{str(e)}}") |
|
else: |
|
print(f"✗ File not found: {{filename}}") |
|
|
|
|
|
print("\n" + "="*80) |
|
print("EXECUTING USER SCRIPT") |
|
print("="*80) |
|
|
|
|
|
try: |
|
|
|
exec_globals = {{'plt': plt, 'pd': __import__('pandas')}} |
|
exec_globals.update({{'__builtins__': __builtins__}}) |
|
|
|
|
|
user_namespace = {{}} |
|
user_code = compile( |
|
{generated_code!r}.lstrip('\n').lstrip(' '), |
|
'<user_code>', 'exec', |
|
dont_inherit=True, |
|
optimize=2 |
|
) |
|
exec(user_code, user_namespace, user_namespace) |
|
|
|
|
|
plt.ioff() |
|
|
|
|
|
print("\n" + "="*80) |
|
print("SAVING PLOTS") |
|
print("="*80) |
|
|
|
|
|
fig_nums = plt.get_fignums() |
|
print(f"Found {{len(fig_nums)}} open figures") |
|
|
|
if fig_nums: |
|
|
|
plots_dir = 'generated_plots' |
|
os.makedirs(plots_dir, exist_ok=True) |
|
|
|
|
|
saved_plots = [] |
|
for i, num in enumerate(fig_nums, 1): |
|
try: |
|
fig = plt.figure(num) |
|
plot_name = f'plot_{{i}}.png' |
|
plot_path = os.path.abspath(os.path.join(plots_dir, plot_name)) |
|
|
|
|
|
fig.savefig( |
|
plot_path, |
|
bbox_inches='tight', |
|
dpi=150, |
|
facecolor=fig.get_facecolor(), |
|
edgecolor='none', |
|
transparent=False |
|
) |
|
|
|
file_size = os.path.getsize(plot_path) |
|
print(f"✓ Saved plot {{i}} to: {{plot_path}} ({{file_size}} bytes)") |
|
saved_plots.append(plot_path) |
|
|
|
|
|
if i == len(fig_nums): |
|
root_plot_path = os.path.abspath('plot.png') |
|
fig.savefig(root_plot_path, bbox_inches='tight', dpi=150) |
|
print(f"✓ Saved main plot to: {{root_plot_path}}") |
|
saved_plots.append(root_plot_path) |
|
|
|
except Exception as e: |
|
print(f"✗ Error saving figure {{num}}: {{str(e)}}") |
|
|
|
if saved_plots: |
|
generated_plot_path = saved_plots[-1] |
|
print(f"\nSuccessfully saved {{len(saved_plots)}} plot(s)") |
|
|
|
else: |
|
print("ℹ️ No figures were created by the user script") |
|
|
|
except Exception as e: |
|
print(f"\n❌ Error executing user script: {{str(e)}}") |
|
print("\nTraceback:") |
|
traceback.print_exc() |
|
raise |
|
|
|
|
|
print("\n" + "="*80) |
|
print("FINAL DIRECTORY CONTENTS") |
|
print("="*80) |
|
for f in os.listdir('.'): |
|
fpath = os.path.join('.', f) |
|
if os.path.isfile(fpath): |
|
print(f" - {{f}} ({{os.path.getsize(fpath)}} bytes)") |
|
else: |
|
print(f" - {{f}}/ (dir)") |
|
|
|
print("\n" + "="*80) |
|
print("SCRIPT EXECUTION COMPLETE") |
|
print("="*80) |
|
|
|
except Exception as e: |
|
print("\n" + "!"*80) |
|
print("ERROR DURING EXECUTION") |
|
print("!"*80) |
|
print(f"Error type: {{type(e).__name__}}") |
|
print(f"Error message: {{str(e)}}") |
|
print("\nTraceback:") |
|
traceback.print_exc() |
|
print("\n" + "!"*80 + "\n") |
|
raise |
|
|
|
finally: |
|
|
|
plt.close('all') |
|
""" |
|
# Write the debug script to a temporary file |
|
with open(temp_script_path, "w") as f: |
|
f.write(debug_script) |
|
|
|
# Execute the script |
|
process = subprocess.run( |
|
["python3", temp_script_path], |
|
capture_output=True, |
|
text=True, |
|
check=False |
|
) |
|
|
|
# Capture both stdout and stderr |
|
execution_output = process.stdout |
|
if process.stderr: |
|
execution_output += "\n\n[ERROR] Script execution errors:\n" + process.stderr |
|
|
|
# Check for common issues in the output |
|
if "KeyError" in execution_output: |
|
execution_output += "\n\n[HELP] The script encountered a KeyError. This typically happens when trying to access a column that doesn't exist in the stock data.\n" |
|
execution_output += "Common causes:\n" |
|
execution_output += "1. The stock symbol might not be recognized by yfinance\n" |
|
execution_output += "2. The requested time period might not have data (e.g., weekends, holidays)\n" |
|
execution_output += "3. The data column names might be different than expected\n\n" |
|
execution_output += "Please try a different stock symbol or time period." |
|
|
|
if "No data" in execution_output or "not found" in execution_output.lower(): |
|
execution_output += "\n\n[HELP] No data was returned for the specified stock symbol or time period.\n" |
|
execution_output += "Please check the stock symbol and try a different time period." |
|
|
|
if "Figure(" in execution_output and "plot.png" not in os.listdir(): |
|
execution_output += "\n\n[HELP] A plot was created but not saved. Adding save command...\n" |
|
# Try to save the plot if it wasn't saved |
|
try: |
|
import matplotlib.pyplot as plt |
|
if plt.get_fignums(): |
|
plt.savefig('plot.png') |
|
execution_output += "Successfully saved plot to plot.png" |
|
generated_plot_path = 'plot.png' |
|
plt.close('all') |
|
except Exception as e: |
|
execution_output += f"Failed to save plot: {str(e)}" |
|
except Exception as e: |
|
execution_output = f"Error during script execution: {str(e)}\n\n" |
|
execution_output += "Please check the generated code for issues or try a different query." |
|
|
|
# Enhanced plot file checking with more detailed debugging |
|
plot_debug_info = [] |
|
plot_found = False |
|
|
|
# Check in current directory first |
|
current_dir = os.getcwd() |
|
plot_abs_path = os.path.abspath(plot_file_path) |
|
|
|
# Log directory contents for debugging |
|
plot_debug_info.append(f"Current directory: {current_dir}") |
|
plot_debug_info.append("Directory contents:" + "\n- " + "\n- ".join(os.listdir('.'))) |
|
|
|
# Check if plot exists in current directory |
|
if os.path.exists(plot_file_path): |
|
plot_found = True |
|
plot_debug_info.append(f"✅ Plot file found at: {plot_abs_path}") |
|
generated_plot_path = plot_file_path |
|
else: |
|
# Try to find the plot file in subdirectories |
|
for root, _, files in os.walk('.'): |
|
if plot_file_path in files: |
|
found_path = os.path.join(root, plot_file_path) |
|
plot_found = True |
|
plot_debug_info.append(f"✅ Plot file found at: {os.path.abspath(found_path)}") |
|
generated_plot_path = found_path |
|
break |
|
|
|
if not plot_found: |
|
plot_debug_info.append(f"❌ Plot file not found at: {plot_abs_path}") |
|
plot_debug_info.append("Troubleshooting tips:") |
|
plot_debug_info.append("1. Ensure the script calls plt.savefig('plot.png')") |
|
plot_debug_info.append("2. Check for any errors in the execution output") |
|
plot_debug_info.append("3. Verify the script has write permissions in the current directory") |
|
|
|
# Add debug info to execution output |
|
execution_output += "\n\n[PLOT DEBUG] " + "\n[PLOT DEBUG] ".join(plot_debug_info) |
|
|
|
if not plot_found: |
|
execution_output += f"\n\n[ERROR] Plot file '{plot_file_path}' was not generated. Check the debug information above for details." |
|
|
|
except Exception as e: |
|
traceback_str = traceback.format_exc() |
|
execution_output = f"An error occurred during code execution: {e}\n{traceback_str}" |
|
|
|
finally: |
|
# Clean up the temporary script file |
|
if os.path.exists(temp_script_path): |
|
os.remove(temp_script_path) |
|
|
|
else: |
|
execution_output = "No code was generated to execute." |
|
|
|
# Update final answer chat to reflect execution attempt |
|
execution_complete_msg = "Code execution finished. See 'Execution Output'." |
|
if generated_plot_path: |
|
plot_msg = "Plot generated successfully. See 'Generated Plot'." |
|
final_answer_chat = [ |
|
{"role": "user", "content": str(user_query)}, |
|
{"role": "assistant", "content": execution_complete_msg}, |
|
{"role": "assistant", "content": plot_msg} |
|
] |
|
else: |
|
no_plot_msg = "No plot was generated. Check the execution output for details." |
|
final_answer_chat = [ |
|
{"role": "user", "content": str(user_query)}, |
|
{"role": "assistant", "content": execution_complete_msg}, |
|
{"role": "assistant", "content": no_plot_msg} |
|
] |
|
|
|
yield agent_thoughts, final_answer_chat, generated_code, execution_output, generated_plot_path |
|
|
|
except Exception as e: |
|
# If an error occurs during CrewAI process, return the error message |
|
traceback_str = traceback.format_exc() |
|
agent_thoughts += f"\nAn error occurred during CrewAI process: {e}\n{traceback_str}" |
|
error_message = f"An error occurred during CrewAI process: {e}" |
|
final_answer_chat = [ |
|
{"role": "user", "content": str(user_query)}, |
|
{"role": "assistant", "content": error_message} |
|
] |
|
yield final_answer_chat, agent_thoughts, generated_code, execution_output, None, None |
|
|
|
finally: |
|
# Restore original stdout |
|
sys.stdout = original_stdout |
|
|
|
|
|
def create_interface(): |
|
"""Create and return the Gradio interface.""" |
|
with gr.Blocks(title="Financial Analytics Agent", theme=gr.themes.Soft()) as interface: |
|
gr.Markdown("# 📊 Financial Analytics Agent") |
|
gr.Markdown("Enter your financial query to analyze stock data and generate visualizations.") |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=2): |
|
user_query_input = gr.Textbox( |
|
label="Enter your financial query", |
|
placeholder="e.g., Show me the stock performance of AAPL and MSFT for the last year", |
|
lines=3 |
|
) |
|
submit_btn = gr.Button("Analyze", variant="primary") |
|
|
|
with gr.Accordion("Advanced Options", open=False): |
|
gr.Markdown("### Model Settings") |
|
model_dropdown = gr.Dropdown( |
|
["gpt-4o", "gpt-4-turbo", "gpt-3.5-turbo"], |
|
value="gpt-4o", |
|
label="Model" |
|
) |
|
temperature = gr.Slider( |
|
minimum=0.1, |
|
maximum=1.0, |
|
value=0.7, |
|
step=0.1, |
|
label="Creativity (Temperature)" |
|
) |
|
|
|
with gr.Column(scale=3): |
|
with gr.Tabs(): |
|
with gr.TabItem("Analysis"): |
|
final_answer_chat = gr.Chatbot( |
|
label="Analysis Results", |
|
height=300, |
|
show_copy_button=True, |
|
type="messages" # Explicitly set to use OpenAI-style message format |
|
) |
|
|
|
with gr.TabItem("Agent Thoughts"): |
|
agent_thoughts = gr.Textbox( |
|
label="Agent Thinking Process", |
|
interactive=False, |
|
lines=15, |
|
max_lines=30, |
|
show_copy_button=True |
|
) |
|
|
|
with gr.TabItem("Generated Code"): |
|
generated_code = gr.Code( |
|
label="Generated Python Code", |
|
language="python", |
|
interactive=False, |
|
lines=15 |
|
) |
|
|
|
with gr.TabItem("Execution Output"): |
|
execution_output = gr.Textbox( |
|
label="Code Execution Output", |
|
interactive=False, |
|
lines=10, |
|
show_copy_button=True |
|
) |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
plot_output = gr.Plot( |
|
label="Generated Visualization", |
|
visible=False |
|
) |
|
image_output = gr.Image( |
|
label="Generated Plot", |
|
type="filepath", |
|
visible=False |
|
) |
|
|
|
# Handle form submission |
|
inputs = [user_query_input, model_dropdown, temperature] |
|
outputs = [ |
|
final_answer_chat, |
|
agent_thoughts, |
|
generated_code, |
|
execution_output, |
|
plot_output, |
|
image_output |
|
] |
|
|
|
submit_btn.click( |
|
fn=run_crewai_process, |
|
inputs=inputs, |
|
outputs=outputs, |
|
api_name="analyze" |
|
) |
|
|
|
return interface |
|
|
|
|
|
def main(): |
|
"""Run the Gradio interface.""" |
|
interface = create_interface() |
|
interface.launch(share=False, server_name="0.0.0.0", server_port=7860) |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |