|
import os |
|
import asyncio |
|
import time |
|
import tkinter as tk |
|
from tkinter import scrolledtext, messagebox, filedialog |
|
import threading |
|
from dotenv import load_dotenv |
|
import openai |
|
|
|
|
|
load_dotenv() |
|
openai.api_key = os.getenv("OPENAI_API_KEY") |
|
ASSISTANT_ID = os.getenv("CODETTE_ASSISTANT_ID", "asst_xxx") |
|
|
|
class CodetteApp(tk.Tk): |
|
def __init__(self): |
|
super().__init__() |
|
self.title("Codette Universal Reasoning Assistant") |
|
self.geometry("900x600") |
|
self.configure(bg="#eef6f9") |
|
self.resizable(True, True) |
|
|
|
|
|
banner = tk.Label(self, text="Ask Codette", font=("Helvetica", 21, "bold"), |
|
bg="#3e75c3", fg="#fafafa", padx=10, pady=14) |
|
banner.pack(fill=tk.X) |
|
|
|
self._setup_controls() |
|
self._setup_output_box() |
|
self._setup_input_controls() |
|
self.chat_log = [] |
|
self.output_box.focus() |
|
self.append_chat("Welcome to Codette! 🧠\n(type your question and press Enter or 'Ask')", who="system") |
|
self.protocol("WM_DELETE_WINDOW", self.on_exit) |
|
|
|
def _setup_controls(self): |
|
btn_frame = tk.Frame(self, bg="#eef6f9") |
|
btn_frame.pack(anchor=tk.NE, pady=7, padx=10) |
|
tk.Button(btn_frame, text="Export Chat", command=self.export_chat, font=("Calibri", 11)).pack(side=tk.LEFT, padx=6) |
|
tk.Button(btn_frame, text="Clear", command=self.clear_all, font=("Calibri", 11)).pack(side=tk.LEFT, padx=6) |
|
tk.Button(btn_frame, text="Exit", command=self.on_exit, font=("Calibri", 11)).pack(side=tk.LEFT, padx=6) |
|
|
|
def _setup_output_box(self): |
|
self.output_frame = tk.Frame(self, bg="#eef6f9") |
|
self.output_frame.pack(expand=True, fill=tk.BOTH, padx=14, pady=2) |
|
self.output_box = scrolledtext.ScrolledText( |
|
self.output_frame, font=("Consolas", 13), bg="#fcfcfc", |
|
wrap=tk.WORD, state="disabled", padx=10, pady=8, |
|
borderwidth=2, relief=tk.GROOVE) |
|
self.output_box.pack(fill=tk.BOTH, expand=True) |
|
self.output_box.tag_config('user', foreground='#0d47a1', font=('Arial', 12, 'bold')) |
|
self.output_box.tag_config('ai', foreground='#357a38', font=('Arial', 12, 'italic')) |
|
self.output_box.tag_config('time', foreground='#ad1457', font=('Arial', 9, 'italic')) |
|
self.output_box.tag_config('system', foreground='#808080', font=('Arial', 10, 'italic')) |
|
|
|
def _setup_input_controls(self): |
|
user_frame = tk.Frame(self, bg="#eef6f9") |
|
user_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(1,10), padx=10) |
|
self.input_field = tk.Entry(user_frame, font=("Calibri", 15)) |
|
self.input_field.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10), ipady=6) |
|
self.input_field.bind("<Return>", lambda event: self.handle_ask()) |
|
tk.Button(user_frame, text="Ask", font=("Calibri", 13), bg="#357a38", fg="white", command=self.handle_ask).pack(side=tk.LEFT) |
|
self.input_field.focus() |
|
|
|
def append_chat(self, text, who="user", timestamp=None): |
|
self.output_box.configure(state='normal') |
|
if not timestamp: |
|
timestamp = time.strftime('%Y-%m-%d %H:%M:%S') |
|
if who == "user": |
|
self.output_box.insert(tk.END, f"[{timestamp}] You: ", ('user', 'time')) |
|
self.output_box.insert(tk.END, text + "\n", 'user') |
|
elif who == "ai": |
|
self.output_box.insert(tk.END, f"[{timestamp}] Codette: ", ('ai', 'time')) |
|
self.output_box.insert(tk.END, text + "\n\n", 'ai') |
|
elif who == "system": |
|
self.output_box.insert(tk.END, f"[{timestamp}] SYSTEM: {text}\n", 'system') |
|
self.output_box.see(tk.END) |
|
self.output_box.configure(state='disabled') |
|
self.chat_log.append((timestamp, who, text.strip())) |
|
|
|
def handle_ask(self): |
|
user_query = self.input_field.get().strip() |
|
if not user_query: |
|
messagebox.showwarning("Input Required", "Please enter your question.") |
|
return |
|
self.append_chat(user_query, 'user') |
|
self.input_field.delete(0, tk.END) |
|
self.input_field.focus() |
|
threading.Thread(target=self.fetch_codette, args=(user_query,), daemon=True).start() |
|
|
|
def fetch_codette(self, user_query): |
|
try: |
|
loop = asyncio.new_event_loop() |
|
asyncio.set_event_loop(loop) |
|
resp = loop.run_until_complete(assistant_thread_chat(user_query)) |
|
self.append_chat(resp, "ai") |
|
except Exception as e: |
|
self.append_chat(f"❗️Error: {e}", "system") |
|
|
|
def clear_all(self): |
|
self.output_box.configure(state='normal') |
|
self.output_box.delete('1.0', tk.END) |
|
self.output_box.configure(state='disabled') |
|
self.chat_log = [] |
|
self.append_chat("Chat cleared.", "system") |
|
self.input_field.focus() |
|
|
|
def export_chat(self): |
|
file_path = filedialog.asksaveasfilename( |
|
title="Export Chat", |
|
defaultextension=".txt", |
|
filetypes=[('Text files', '*.txt')] |
|
) |
|
if file_path: |
|
with open(file_path, "w", encoding="utf-8") as f: |
|
for (timestamp, who, text) in self.chat_log: |
|
label = "You:" if who == "user" else "Codette:" if who == "ai" else "SYSTEM:" |
|
f.write(f"[{timestamp}] {label} {text}\n") |
|
self.append_chat(f"Exported chat log to: {file_path}", "system") |
|
|
|
def on_exit(self): |
|
self.destroy() |
|
|
|
async def assistant_thread_chat(prompt: str) -> str: |
|
try: |
|
|
|
thread = await asyncio.to_thread(lambda: openai.beta.threads.create()) |
|
|
|
|
|
_ = await asyncio.to_thread( |
|
lambda: openai.beta.threads.messages.create( |
|
thread_id=thread.id, |
|
role="user", |
|
content=prompt |
|
) |
|
) |
|
|
|
|
|
run = await asyncio.to_thread( |
|
lambda: openai.beta.threads.runs.create( |
|
thread_id=thread.id, |
|
assistant_id=ASSISTANT_ID |
|
) |
|
) |
|
|
|
|
|
while run.status in ['queued', 'in_progress', 'cancelling']: |
|
await asyncio.sleep(1) |
|
run = await asyncio.to_thread( |
|
lambda: openai.beta.threads.runs.retrieve( |
|
thread_id=thread.id, |
|
run_id=run.id |
|
) |
|
) |
|
|
|
if run.status == "completed": |
|
messages = await asyncio.to_thread( |
|
lambda: openai.beta.threads.messages.list(thread_id=thread.id) |
|
) |
|
|
|
for msg in reversed(messages.data): |
|
if msg.role == "assistant": |
|
return msg.content + " 😊" |
|
return "[No assistant response found]" |
|
elif run.status == "requires_action": |
|
return "[ACTION REQUIRED: Tool/function call not yet implemented.]" |
|
else: |
|
return f"[ERROR: Run status {run.status}]" |
|
except Exception as e: |
|
return f"Sorry—Codette encountered an error: {e}" |
|
|
|
if __name__ == "__main__": |
|
app = CodetteApp() |
|
app.mainloop() |
|
|