My project started without thinking of integration with any GUI, but now I’m trying to implement a simple interface for the classes and functions I already have. My problem is that some of the functions that are called during the execution of my code received user inputs from the terminal, using Python’s input
. I tried to replicate that with a pop-up window in the modified tkinter I’m using (namely customtkinter) and it worked in my tests, however, when I integrated the code snippet in my project it stopped working as I was imagining.
My current solution is the following, this is my main class (App is the customtkinter class I’ve created for my main GUI, which is in the same file as this main, GraphBuilder and Chat are in their own .py files):
if __name__ == '__main__':
app = App()
graph = GraphBuilder(app, True).build()
chat = Chat(graph, 25)
app.set_chat(chat)
app.mainloop()
This is the way I’m defining the GraphBuilder (I’ve only included what I think is relevant for now, if more code is needed just let me know):
class GraphBuilder(ABC):
def __init__(self, app, debug):
self.debug = debug
self.app = app
# more code ....
def scenario_selector(self, state: GraphState) -> GraphState:
return agents.ScenarioIdentifier(chat_model, json_model, state, self.app, self.debug).execute()
# more code ....
def build(self) -> StateGraph:
workflow = StateGraph(GraphState)
# more code ...
And finally, this is the implementation of the pop-up window I’ve come up with, together with the implementation for ScenarioIdentifier:
class InputGetter(customtkinter.CTkToplevel):
def __init__(self, *args, **kwargs):
# Extract label, buttons, and callback from kwargs
label = kwargs.pop('label', None)
buttons = kwargs.pop('buttons', [])
self.callback = kwargs.pop('callback', None)
super().__init__(*args, **kwargs)
self.geometry("800x600")
# Create a canvas and a scrollbar
self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff")
self.scrollbar = customtkinter.CTkScrollbar(self, orientation="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.scrollbar.set)
self.scrollable_frame = customtkinter.CTkFrame(self.canvas)
self.scrollable_frame.bind(
"<Configure>",
lambda e: self.canvas.configure(
scrollregion=self.canvas.bbox("all")
)
)
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw", width=800)
self.canvas.pack(side="left", fill="both", expand=True)
self.scrollbar.pack(side="right", fill="y")
# Bind mouse scroll events
self.canvas.bind_all("<MouseWheel>", self._on_mouse_scroll)
self.canvas.bind_all("<Button-4>", self._on_mouse_scroll)
self.canvas.bind_all("<Button-5>", self._on_mouse_scroll)
# Configure the grid layout for the scrollable frame
self.scrollable_frame.grid_columnconfigure(0, weight=1)
# Add widgets to the scrollable frame
if label:
self.label = customtkinter.CTkLabel(self.scrollable_frame, text=label)
self.label.grid(row=0, column=0, padx=10, pady=10)
if buttons:
for i in range(len(buttons)):
button = customtkinter.CTkButton(self.scrollable_frame, text=buttons[i], command=lambda b=buttons[i]: self.button_clicked(b))
button.grid(row=i+1, column=0, padx=10, pady=10)
def _on_mouse_scroll(self, event):
if event.delta:
self.canvas.yview_scroll(-1 * (event.delta // 120), "units")
elif event.num == 4:
self.canvas.yview_scroll(-1, "units")
elif event.num == 5:
self.canvas.yview_scroll(1, "units")
def button_clicked(self, text) -> None:
if self.callback:
self.callback(text)
self.destroy()
class AgentBase(ABC):
def __init__(self, chat_model, json_model, state: GraphState, app, debug):
self.chat_model = chat_model
self.json_model = json_model
self.state = state
self.debug = debug
self.app = app
self.selected_value = None
def confirm_selection(self, selected_value):
self.selected_value = selected_value
@abstractmethod
def get_prompt_template(self) -> PromptTemplate:
pass
def execute(self) -> GraphState:
pass
class ScenarioIdentifier(AgentBase):
def get_prompt_template(self) -> PromptTemplate:
# irrelevant for now
def execute(self) -> GraphState:
# irrelevant for now
if identified_scenario == 'NOT_FOUND' or not(identified_scenario in scenarios):
kwargs = {
'label': 'No valid scenario was identified in the request, here are the available scenarios:',
'buttons': scenarios,
'callback': self.confirm_selection
}
input_getter = InputGetter(self.app, **kwargs)
if not(self.selected_value in scenarios):
identified_scenario = self.selected_value
else:
identified_scenario = 'NOT_FOUND'
if identified_scenario == 'NOT_FOUND':
message = 'No valid scenario was found'
valid = False
else:
message = f'Selected scenario for simulation: {identified_scenario}'
valid = True
print(message)
self.state['scenario'] = identified_scenario
self.state['selection_is_valid'] = valid
self.state['final_answer'] = message
return self.state
In my early tests with the pop-up window isolated everything worked fine (I tried with a button in my main GUI that would initialize the class and show the window), but with the class integrated in my code it freezes as soon as I get to the super().__init__(*args, **kwargs)
line. When I hit Ctrl + C twice, it unfreezes and the window loads (without the options, only the label) and the execution of my code resumes. I’m not get what exactly I’m doing wrong here, do anyone have a suggestion of how to make this work as expected (the code executes from the main GUI, and when the InputGetter is instantiated the execution freezes until there is a input)?