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
+ })