diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 8072a30..c6ae9db 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 18d5687..0a046db 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 8f27c3c..e1d23ec 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/templates/core/chat.html b/core/templates/core/chat.html index f832463..8180408 100644 --- a/core/templates/core/chat.html +++ b/core/templates/core/chat.html @@ -113,6 +113,63 @@ } }); } + + const isGenerating = {{ selected_conversation.is_generating|yesno:"true,false" }}; + const conversationId = {{ selected_conversation.id|default:"null" }}; + + if (isGenerating && conversationId) { + const pollingInterval = setInterval(() => { + fetch(`/get_conversation_messages/${conversationId}/`) + .then(response => response.json()) + .then(data => { + const messagesContainer = document.getElementById('chat-messages'); + messagesContainer.innerHTML = ''; // Clear existing messages + data.messages.forEach(message => { + const messageEl = document.createElement('div'); + messageEl.classList.add('message', message.sender); + + const messageContentEl = document.createElement('div'); + messageContentEl.classList.add('message-content'); + + const authorEl = document.createElement('div'); + authorEl.classList.add('message-author'); + if (message.sender === 'user') { + authorEl.innerText = 'You'; + } else if (message.sender === 'ai') { + authorEl.innerText = 'AI'; + } else if (message.sender === 'system') { + authorEl.innerText = 'System'; + } else if (message.sender === 'ai_command') { + authorEl.innerText = 'AI Command'; + } + messageContentEl.appendChild(authorEl); + + if (message.sender === 'ai_command') { + const preEl = document.createElement('pre'); + const codeEl = document.createElement('code'); + codeEl.innerHTML = message.content.replace(/\n/g, '
'); + preEl.appendChild(codeEl); + messageContentEl.appendChild(preEl); + } else { + const pEl = document.createElement('p'); + pEl.innerHTML = message.content.replace(/\n/g, '
'); + messageContentEl.appendChild(pEl); + } + + messageEl.appendChild(messageContentEl); + messagesContainer.appendChild(messageEl); + }); + messagesContainer.scrollTop = messagesContainer.scrollHeight; + + if (!data.is_generating) { + clearInterval(pollingInterval); + if (loaderOverlay) { + loaderOverlay.style.display = 'none'; + } + } + }); + }, 2000); + } }); diff --git a/core/views.py b/core/views.py index 29c919f..a6ee8e2 100644 --- a/core/views.py +++ b/core/views.py @@ -3,6 +3,7 @@ from django.http import JsonResponse from django.views.decorators.http import require_POST import json import logging +import threading from .models import Article, TodoItem, Conversation, Message, Setting from .forms import TodoItemForm import time @@ -11,6 +12,7 @@ from ai.local_ai_api import LocalAIApi # Get an instance of a logger logger = logging.getLogger(__name__) + def index(request): if request.method == 'POST': form = TodoItemForm(request.POST) @@ -31,6 +33,7 @@ def index(request): } return render(request, "core/index.html", context) + def kanban_board(request): tasks = TodoItem.objects.all().order_by('created_at') tasks_by_status = { @@ -45,10 +48,12 @@ def kanban_board(request): } return render(request, "core/kanban.html", context) + def article_detail(request, article_id): article = Article.objects.get(pk=article_id) return render(request, "core/article_detail.html", {"article": article}) + @require_POST def update_task_status(request): try: @@ -76,11 +81,13 @@ def delete_task(request, task_id): return redirect(referer) return redirect('core:index') + @require_POST def cleanup_tasks(request): TodoItem.objects.all().delete() return redirect('core:index') + def execute_command(command_data): command_name = command_data.get('name') args = command_data.get('args', {}) @@ -145,40 +152,33 @@ def execute_command(command_data): logger.error(f"Error executing command '{command_name}': {e}", exc_info=True) return f"[SYSTEM] Error executing command '{command_name}': {e}" -def chat_view(request, conversation_id=None): - if request.method == 'POST': - if 'title' in request.POST: - title = request.POST.get('title', 'New Conversation') - conversation = Conversation.objects.create(title=title) - return redirect('core:chat_detail', conversation_id=conversation.id) - - elif 'text' in request.POST and conversation_id: - text = request.POST.get('text') - selected_conversation = get_object_or_404(Conversation, id=conversation_id) - - if text: - command_name = None # Initialize command_name - Message.objects.create(conversation=selected_conversation, content=text, sender='user') - - history = [] - for msg in selected_conversation.messages.order_by('created_at'): - role = msg.sender - if role == 'ai': - role = 'assistant' - elif role == 'system': - role = 'user' - history.append({"role": role, "content": msg.content}) - - try: - custom_instructions, created = Setting.objects.get_or_create( - key='custom_instructions', - defaults={'value': ''} - ) - custom_instructions_text = custom_instructions.value + '\n\n' if custom_instructions.value else '' - system_message = { - "role": "system", - "content": custom_instructions_text + '''You are a project management assistant. To communicate with the user, you MUST use the `send_message` command. +def run_ai_process_in_background(conversation_id): + """This function runs in a separate thread.""" + try: + conversation = get_object_or_404(Conversation, id=conversation_id) + conversation.is_generating = True + conversation.save() + + history = [] + for msg in conversation.messages.order_by('created_at'): + role = msg.sender + if role == 'ai': + role = 'assistant' + # User messages are already 'user', system messages are 'user' for the model + elif role == 'system': + role = 'user' + history.append({"role": role, "content": msg.content}) + + custom_instructions, _ = Setting.objects.get_or_create( + key='custom_instructions', + defaults={'value': ''} + ) + custom_instructions_text = custom_instructions.value + '\n\n' if custom_instructions.value else '' + + system_message = { + "role": "system", + "content": custom_instructions_text + '''You are a project management assistant. To communicate with the user, you MUST use the `send_message` command. **Commands must be in a specific JSON format.** Your response must be a JSON object with the following structure: @@ -253,86 +253,116 @@ def chat_view(request, conversation_id=None): ``` **IMPORTANT:** Do not wrap the JSON command in markdown backticks or any other text. The entire response must be the JSON object.''' - } + } - tasks = TodoItem.objects.all().order_by('created_at') - task_list_str = "\n".join([ - f"- ID {task.id}: {task.title} (Status: {task.get_status_display()}, Tags: {task.tags or 'None'})" for task in tasks - ]) + tasks = TodoItem.objects.all().order_by('created_at') + task_list_str = "\n".join([ + f"- ID {task.id}: {task.title} (Status: {task.get_status_display()}, Tags: {task.tags or 'None'})" for task in tasks + ]) + + tasks_context = { + "role": "system", + "content": f"Here is the current list of tasks:\n{task_list_str}" + } + + logger.info("Starting AI processing loop...") + + for i in range(7): # Loop up to 7 times + logger.info(f"AI loop iteration {i+1}") + + response = LocalAIApi.create_response({ + "input": [ + system_message, + tasks_context, + ] + history, + "text": {"format": {"type": "json_object"}}, + }) + + if not response.get("success"): + logger.error(f"AI API request failed. Full error: {response.get('error')}") + ai_text = "I couldn't process that. Please try again." + Message.objects.create(conversation=conversation, content=ai_text, sender='ai') + break + + ai_text = LocalAIApi.extract_text(response) + if not ai_text: + logger.warning("AI response was empty.") + ai_text = "I couldn't process that. Please try again." + Message.objects.create(conversation=conversation, content=ai_text, sender='ai') + break + + try: + command_json = json.loads(ai_text) + if 'command' in command_json: + command_name = command_json.get('command', {}).get('name') + command_result = execute_command(command_json['command']) - tasks_context = { - "role": "system", - "content": f"Here is the current list of tasks:\n{task_list_str}" - } - - logger.info("Starting AI processing loop...") + sender = 'ai' if command_name == 'send_message' else 'system' + Message.objects.create(conversation=conversation, content=command_result, sender=sender) - for i in range(7): # Loop up to 7 times - logger.info(f"AI loop iteration {i+1}") - - response = LocalAIApi.create_response({ - "input": [ - system_message, - tasks_context, - ] + history, - "text": {"format": {"type": "json_object"}}, - }) + if command_name == 'send_message': + break + + history.append({"role": "user", "content": command_result}) + else: + Message.objects.create(conversation=conversation, content=ai_text, sender='ai') + break + except (json.JSONDecodeError, TypeError): + Message.objects.create(conversation=conversation, content=ai_text, sender='ai') + break + else: + logger.warning("AI loop finished after 7 iterations without sending a message.") + final_message = "I seem to be stuck in a loop. Could you clarify what you'd like me to do?" + Message.objects.create(conversation=conversation, content=final_message, sender='ai') - if not response.get("success"): - logger.error(f"AI API request failed with status {response.get('status')}. Full error: {response.get('response')}") - ai_text = "I couldn't process that. Please try again." - Message.objects.create(conversation=selected_conversation, content=ai_text, sender='ai') - break - - logger.info(f"AI raw response: {response}") - ai_text = LocalAIApi.extract_text(response) - logger.info(f"Extracted AI text: {ai_text}") + except Exception as e: + logger.error(f"An unexpected error occurred in background AI process: {e}", exc_info=True) + try: + # Try to inform the user about the error + Message.objects.create(conversation_id=conversation_id, content=f"An internal error occurred: {str(e)}", sender='ai') + except Exception as e2: + logger.error(f"Could not even save the error message to the conversation: {e2}", exc_info=True) - if not ai_text: - logger.warning("AI response was empty.") - ai_text = "I couldn't process that. Please try again." - Message.objects.create(conversation=selected_conversation, content=ai_text, sender='ai') - break - - try: - command_json = json.loads(ai_text) - if 'command' in command_json: - command_name = command_json.get('command', {}).get('name') - command_result = execute_command(command_json['command']) - - sender = 'ai' if command_name == 'send_message' else 'system' - Message.objects.create(conversation=selected_conversation, content=command_result, sender=sender) - - if command_name == 'send_message': - break # Exit loop if send_message is called - - # Add system message with command result to history for next iteration - history.append({"role": "user", "content": command_result}) + finally: + # Ensure is_generating is always set to False + try: + conversation = Conversation.objects.get(id=conversation_id) + conversation.is_generating = False + conversation.save() + except Conversation.DoesNotExist: + logger.error(f"Conversation with ID {conversation_id} does not exist when trying to finalize background process.") + except Exception as e: + logger.error(f"Could not finalize background process for conversation {conversation_id}: {e}", exc_info=True) - else: - # If it's a JSON but not a command, save it as a message and break - Message.objects.create(conversation=selected_conversation, content=ai_text, sender='ai') - break - except (json.JSONDecodeError, TypeError): - # Not a JSON command, treat as a raw message and break - Message.objects.create(conversation=selected_conversation, content=ai_text, sender='ai') - break - else: - # This block executes if the loop completes without a 'break' - logger.warning("AI loop finished after 7 iterations without sending a message.") - final_message = "I seem to be stuck in a loop. Could you clarify what you'd like me to do?" - Message.objects.create(conversation=selected_conversation, content=final_message, sender='ai') - - except Exception as e: - logger.error(f"An unexpected error occurred: {e}", exc_info=True) - ai_text = f"An error occurred: {str(e)}" - Message.objects.create(conversation=selected_conversation, content=ai_text, sender='ai') - - return redirect('core:chat_detail', conversation_id=conversation_id) +def chat_view(request, conversation_id=None): + if request.method == 'POST': + # Create a new conversation + if 'title' in request.POST: + title = request.POST.get('title', 'New Conversation').strip() + if not title: + title = 'New Conversation' + conversation = Conversation.objects.create(title=title) + return redirect('core:chat_detail', conversation_id=conversation.id) + + # Send a message in an existing conversation + elif 'text' in request.POST and conversation_id: + text = request.POST.get('text').strip() + if text: + conversation = get_object_or_404(Conversation, id=conversation_id) + Message.objects.create(conversation=conversation, content=text, sender='user') + + # Start AI processing in a background thread + thread = threading.Thread(target=run_ai_process_in_background, args=(conversation_id,)) + thread.daemon = True + thread.start() + + return redirect('core:chat_detail', conversation_id=conversation_id) conversations = Conversation.objects.order_by('-created_at') - selected_conversation = get_object_or_404(Conversation, id=conversation_id) if conversation_id else None + selected_conversation = None + if conversation_id: + selected_conversation = get_object_or_404(Conversation, id=conversation_id) return render(request, 'core/chat.html', { 'conversation_list': conversations, @@ -340,13 +370,23 @@ def chat_view(request, conversation_id=None): 'timestamp': int(time.time()), }) + def conversation_list(request): conversations = Conversation.objects.order_by('-created_at') return render(request, 'core/conversation_list.html', {'conversation_list': conversations}) + +def get_conversation_messages(request, conversation_id): + conversation = get_object_or_404(Conversation, id=conversation_id) + messages = conversation.messages.order_by('created_at').values('sender', 'content', 'created_at') + return JsonResponse({ + 'messages': list(messages), + 'is_generating': conversation.is_generating + }) + + def settings_view(request): - # Get or create the custom_instructions setting - custom_instructions, created = Setting.objects.get_or_create( + custom_instructions, _ = Setting.objects.get_or_create( key='custom_instructions', defaults={'value': ''} ) @@ -358,4 +398,4 @@ def settings_view(request): return render(request, 'core/settings.html', { 'custom_instructions': custom_instructions - }) \ No newline at end of file + })