| | 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() |
| |
|