Skip to content

Commit

Permalink
Added logic for handling a missing api key
Browse files Browse the repository at this point in the history
  • Loading branch information
dougollerenshaw committed Sep 19, 2024
1 parent c6cf97e commit af06a42
Show file tree
Hide file tree
Showing 3 changed files with 217 additions and 65 deletions.
105 changes: 93 additions & 12 deletions codeaide/logic/chat_handler.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import json
import os

from codeaide.utils.api_utils import parse_response, send_api_request
import sys
import re
import traceback
from codeaide.utils.api_utils import (
parse_response,
send_api_request,
get_api_client,
save_api_key,
MissingAPIKeyException,
)
from codeaide.utils.constants import MAX_RETRIES, MAX_TOKENS
from codeaide.utils.cost_tracker import CostTracker
from codeaide.utils.environment_manager import EnvironmentManager
Expand All @@ -18,17 +26,93 @@ def __init__(self):
self.env_manager = EnvironmentManager()
self.terminal_manager = TerminalManager()
self.latest_version = "0.0"
self.api_client = None
self.api_key_set = False
self.current_service = "anthropic" # Default service

def check_api_key(self):
if self.api_client is None:
self.api_client = get_api_client(self.current_service)

if self.api_client:
self.api_key_set = True
return True, None
else:
self.api_key_set = False
return False, self.get_api_key_instructions(self.current_service)

def get_api_key_instructions(self, service):
if service == "anthropic":
return (
"It looks like you haven't set up your Anthropic API key yet. "
"Here's how to get started:\n\n"
"1. Go to https://www.anthropic.com or https://console.anthropic.com to sign up or log in.\n"
"2. Navigate to your account settings or API section.\n"
"3. Generate a new API key.\n"
"4. Add some funds to your account to cover the cost of using the API (start with as little as $1).\n"
"5. Copy the API key and paste it here.\n\n"
"Once you've pasted your API key, I'll save it securely in a .env file in the root of your project. "
"This file is already in .gitignore, so it won't be shared if you push your code to a repository.\n\n"
"Please paste your Anthropic API key now:"
)
else:
return f"Please enter your API key for {service.capitalize()}:"

def validate_api_key(self, api_key):
# Remove leading/trailing whitespace and quotes
cleaned_key = api_key.strip().strip("'\"")

# Check if the API key follows a general pattern for API keys
pattern = r"^[a-zA-Z0-9_-]{32,}$"
if len(cleaned_key) < 32:
return False, "API key is too short (should be at least 32 characters)"
elif not re.match(pattern, cleaned_key):
return (
False,
"API key should only contain letters, numbers, underscores, and hyphens",
)
return True, ""

def handle_api_key_input(self, api_key):
cleaned_key = api_key.strip().strip("'\"") # Remove quotes and whitespace
is_valid, error_message = self.validate_api_key(cleaned_key)
if is_valid:
if save_api_key(self.current_service, cleaned_key):
# Try to get a new API client with the new key
self.api_client = get_api_client(self.current_service)
if self.api_client:
self.api_key_set = True
return True, "API key saved and verified successfully."
else:
return (
False,
"API key saved, but verification failed. Please check your key and try again.",
)
else:
return (
False,
"Failed to save the API key. Please check your permissions and try again.",
)
else:
return False, f"Invalid API key format: {error_message}. Please try again."

def process_input(self, user_input):
try:
if not self.api_key_set or self.api_client is None:
api_key_valid, message = self.check_api_key()
if not api_key_valid:
return {"type": "api_key_required", "message": message}

# At this point, we know we have a valid API client
self.conversation_history.append({"role": "user", "content": user_input})

for attempt in range(MAX_RETRIES):
version_info = f"\n\nThe latest code version was {self.latest_version}. If you're making minor changes to the previous code, increment the minor version (e.g., 1.0 to 1.1). If you're creating entirely new code, increment the major version (e.g., 1.1 to 2.0). Ensure the new version is higher than {self.latest_version}."
self.conversation_history[-1]["content"] += version_info

response = send_api_request(self.conversation_history, MAX_TOKENS)
print(f"Response (Attempt {attempt + 1}): {response}")
response = send_api_request(
self.api_client, self.conversation_history, MAX_TOKENS
)

if response is None:
if attempt == MAX_RETRIES - 1:
Expand All @@ -42,7 +126,7 @@ def process_input(self, user_input):

try:
parsed_response = parse_response(response)
if parsed_response[0] is None: # If parsing failed
if parsed_response[0] is None:
raise ValueError("Failed to parse JSON response")

(
Expand Down Expand Up @@ -88,8 +172,7 @@ def process_input(self, user_input):
else:
return {"type": "message", "message": text}

except (json.JSONDecodeError, ValueError) as e:
print(f"Error processing response (Attempt {attempt + 1}): {e}")
except (ValueError, json.JSONDecodeError) as e:
if attempt < MAX_RETRIES - 1:
error_prompt = f"\n\nThere was an error in your response: {e}. Please ensure you're using proper JSON formatting and incrementing the version number correctly. The latest version was {self.latest_version}, so the new version must be higher than this."
self.conversation_history[-1]["content"] += error_prompt
Expand All @@ -105,10 +188,10 @@ def process_input(self, user_input):
}

except Exception as e:
print("Unexpected error in process_input")
traceback.print_exc()
return {
"type": "error",
"message": f"An unexpected error occurred: {str(e)}",
"type": "internal_error",
"message": f"An unexpected error occurred: {str(e)}. Please check the console window for the full traceback.",
}

@staticmethod
Expand All @@ -125,14 +208,12 @@ def run_generated_code(self, filename, requirements):
)

activation_command = self.env_manager.get_activation_command()

new_packages = self.env_manager.install_requirements(req_path)

script_content = f"""
clear # Clear the terminal
echo "Activating environment..."
{activation_command}
"""

if new_packages:
Expand Down
84 changes: 49 additions & 35 deletions codeaide/ui/chat_window.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import signal
import sys
import traceback

from PyQt5.QtCore import Qt, QTimer, pyqtSignal
from PyQt5.QtGui import QColor, QFont
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtWidgets import (
QApplication,
QHBoxLayout,
Expand Down Expand Up @@ -41,46 +41,33 @@ def __init__(self, chat_handler):
self.chat_handler = chat_handler
self.cost_tracker = chat_handler.cost_tracker
self.code_popup = None
self.waiting_for_api_key = False
self.setup_ui()
self.add_to_chat("AI", INITIAL_MESSAGE)
self.check_api_key()

# Set up SIGINT handler
signal.signal(signal.SIGINT, self.sigint_handler)

# Allow CTRL+C to interrupt the Qt event loop
self.timer = QTimer()
self.timer.start(500) # Timeout in ms
self.timer.timeout.connect(lambda: None) # Let the interpreter run each 500 ms
self.timer.start(500)
self.timer.timeout.connect(lambda: None)

def setup_ui(self):
central_widget = QWidget(self)
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(5) # Space between main components
main_layout.setContentsMargins(8, 8, 8, 8) # Margins around the entire window
main_layout.setSpacing(5)
main_layout.setContentsMargins(8, 8, 8, 8)

# Chat display
self.chat_display = QTextEdit(self)
self.chat_display.setReadOnly(True)
self.chat_display.setStyleSheet(
f"""
background-color: {CHAT_WINDOW_BG};
color: {CHAT_WINDOW_FG};
border: 1px solid #ccc;
padding: 5px;
"""
f"background-color: {CHAT_WINDOW_BG}; color: {CHAT_WINDOW_FG}; border: 1px solid #ccc; padding: 5px;"
)
main_layout.addWidget(self.chat_display, stretch=3)

# Input area
self.input_text = QTextEdit(self)
self.input_text.setStyleSheet(
f"""
background-color: {CHAT_WINDOW_BG};
color: {USER_MESSAGE_COLOR};
border: 1px solid #ccc;
padding: 5px;
"""
f"background-color: {CHAT_WINDOW_BG}; color: {USER_MESSAGE_COLOR}; border: 1px solid #ccc; padding: 5px;"
)
self.input_text.setAcceptRichText(True)
self.input_text.setFont(general_utils.set_font(USER_FONT))
Expand All @@ -89,9 +76,8 @@ def setup_ui(self):
self.input_text.installEventFilter(self)
main_layout.addWidget(self.input_text, stretch=1)

# Button layout
button_layout = QHBoxLayout()
button_layout.setSpacing(5) # Space between buttons
button_layout.setSpacing(5)

self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.on_submit)
Expand Down Expand Up @@ -122,7 +108,6 @@ def eventFilter(self, obj, event):
def on_submit(self):
user_input = self.input_text.toPlainText().strip()
if user_input:
print(f"ChatWindow: Received submission: {user_input}")
self.add_to_chat("User", user_input)
self.input_text.clear()
self.display_thinking()
Expand All @@ -135,11 +120,9 @@ def on_modify(self):
self.input_text.ensureCursorVisible()

def add_to_chat(self, sender, message):
print(f"ChatWindow: Adding to chat - {sender}: {message}")
color = USER_MESSAGE_COLOR if sender == "User" else AI_MESSAGE_COLOR
font = USER_FONT if sender == "User" else AI_FONT
sender = AI_EMOJI if sender == "AI" else sender

html_message = general_utils.format_chat_message(sender, message, font, color)
self.chat_display.append(html_message + "<br>")
self.chat_display.ensureCursorVisible()
Expand All @@ -148,11 +131,42 @@ def display_thinking(self):
self.add_to_chat("AI", "Thinking... 🤔")

def process_input(self, user_input):
response = self.chat_handler.process_input(user_input)
QTimer.singleShot(100, lambda: self.handle_response(response))
try:
if self.waiting_for_api_key:
self.handle_api_key_input(user_input)
else:
response = self.chat_handler.process_input(user_input)
self.handle_response(response)
except Exception as e:
error_message = f"An unexpected error occurred: {str(e)}. Please check the console window for the full traceback."
self.add_to_chat("AI", error_message)
print("Unexpected error in ChatWindow process_input:", file=sys.stderr)
traceback.print_exc()
finally:
self.enable_ui_elements()

def handle_response(self, response):
def check_api_key(self):
api_key_valid, message = self.chat_handler.check_api_key()
if not api_key_valid:
self.add_to_chat("AI", message)
self.waiting_for_api_key = True
else:
self.waiting_for_api_key = False

def handle_api_key_input(self, api_key):
success, message = self.chat_handler.handle_api_key_input(api_key)
self.remove_thinking_messages()
if success:
self.waiting_for_api_key = False
self.add_to_chat(
"AI",
"Great! Your API key has been saved. What would you like to work on?",
)
else:
self.add_to_chat("AI", message)
self.enable_ui_elements()

def handle_response(self, response):
self.enable_ui_elements()

if response["type"] == "message":
Expand All @@ -170,10 +184,11 @@ def handle_response(self, response):
)
elif response["type"] == "code":
self.add_to_chat("AI", response["message"])
print("About to update or create code popup")
self.update_or_create_code_popup(response)
print("Code popup updated or created")
elif response["type"] == "error":
elif response["type"] in ["error", "internal_error"]:
self.add_to_chat("AI", response["message"])
elif response["type"] == "api_key_required":
self.waiting_for_api_key = True
self.add_to_chat("AI", response["message"])

def remove_thinking_messages(self):
Expand Down Expand Up @@ -238,5 +253,4 @@ def closeEvent(self, event):
super().closeEvent(event)

def sigint_handler(self, *args):
"""Handler for the SIGINT signal."""
QApplication.quit()
Loading

0 comments on commit af06a42

Please sign in to comment.