async AI job

This commit is contained in:
Flatlogic Bot 2025-11-20 19:22:48 +00:00
parent 1566d6c207
commit 4c1df17a46
5 changed files with 204 additions and 107 deletions

View File

@ -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, '<br>');
preEl.appendChild(codeEl);
messageContentEl.appendChild(preEl);
} else {
const pEl = document.createElement('p');
pEl.innerHTML = message.content.replace(/\n/g, '<br>');
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);
}
}); });
</script> </script>

View File

@ -3,6 +3,7 @@ from django.http import JsonResponse
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
import json import json
import logging import logging
import threading
from .models import Article, TodoItem, Conversation, Message, Setting from .models import Article, TodoItem, Conversation, Message, Setting
from .forms import TodoItemForm from .forms import TodoItemForm
import time import time
@ -11,6 +12,7 @@ from ai.local_ai_api import LocalAIApi
# Get an instance of a logger # Get an instance of a logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def index(request): def index(request):
if request.method == 'POST': if request.method == 'POST':
form = TodoItemForm(request.POST) form = TodoItemForm(request.POST)
@ -31,6 +33,7 @@ def index(request):
} }
return render(request, "core/index.html", context) return render(request, "core/index.html", context)
def kanban_board(request): def kanban_board(request):
tasks = TodoItem.objects.all().order_by('created_at') tasks = TodoItem.objects.all().order_by('created_at')
tasks_by_status = { tasks_by_status = {
@ -45,10 +48,12 @@ def kanban_board(request):
} }
return render(request, "core/kanban.html", context) return render(request, "core/kanban.html", context)
def article_detail(request, article_id): def article_detail(request, article_id):
article = Article.objects.get(pk=article_id) article = Article.objects.get(pk=article_id)
return render(request, "core/article_detail.html", {"article": article}) return render(request, "core/article_detail.html", {"article": article})
@require_POST @require_POST
def update_task_status(request): def update_task_status(request):
try: try:
@ -76,11 +81,13 @@ def delete_task(request, task_id):
return redirect(referer) return redirect(referer)
return redirect('core:index') return redirect('core:index')
@require_POST @require_POST
def cleanup_tasks(request): def cleanup_tasks(request):
TodoItem.objects.all().delete() TodoItem.objects.all().delete()
return redirect('core:index') return redirect('core:index')
def execute_command(command_data): def execute_command(command_data):
command_name = command_data.get('name') command_name = command_data.get('name')
args = command_data.get('args', {}) 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) logger.error(f"Error executing command '{command_name}': {e}", exc_info=True)
return f"[SYSTEM] Error executing command '{command_name}': {e}" 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 = { def run_ai_process_in_background(conversation_id):
"role": "system", """This function runs in a separate thread."""
"content": custom_instructions_text + '''You are a project management assistant. To communicate with the user, you MUST use the `send_message` command. 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: **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.''' **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') tasks = TodoItem.objects.all().order_by('created_at')
task_list_str = "\n".join([ 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 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 = { sender = 'ai' if command_name == 'send_message' else 'system'
"role": "system", Message.objects.create(conversation=conversation, content=command_result, sender=sender)
"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 if command_name == 'send_message':
logger.info(f"AI loop iteration {i+1}") break
response = LocalAIApi.create_response({ history.append({"role": "user", "content": command_result})
"input": [ else:
system_message, Message.objects.create(conversation=conversation, content=ai_text, sender='ai')
tasks_context, break
] + history, except (json.JSONDecodeError, TypeError):
"text": {"format": {"type": "json_object"}}, 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"): except Exception as e:
logger.error(f"AI API request failed with status {response.get('status')}. Full error: {response.get('response')}") logger.error(f"An unexpected error occurred in background AI process: {e}", exc_info=True)
ai_text = "I couldn't process that. Please try again." try:
Message.objects.create(conversation=selected_conversation, content=ai_text, sender='ai') # Try to inform the user about the error
break Message.objects.create(conversation_id=conversation_id, content=f"An internal error occurred: {str(e)}", sender='ai')
except Exception as e2:
logger.info(f"AI raw response: {response}") logger.error(f"Could not even save the error message to the conversation: {e2}", exc_info=True)
ai_text = LocalAIApi.extract_text(response)
logger.info(f"Extracted AI text: {ai_text}")
if not ai_text: finally:
logger.warning("AI response was empty.") # Ensure is_generating is always set to False
ai_text = "I couldn't process that. Please try again." try:
Message.objects.create(conversation=selected_conversation, content=ai_text, sender='ai') conversation = Conversation.objects.get(id=conversation_id)
break conversation.is_generating = False
conversation.save()
try: except Conversation.DoesNotExist:
command_json = json.loads(ai_text) logger.error(f"Conversation with ID {conversation_id} does not exist when trying to finalize background process.")
if 'command' in command_json: except Exception as e:
command_name = command_json.get('command', {}).get('name') logger.error(f"Could not finalize background process for conversation {conversation_id}: {e}", exc_info=True)
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})
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): def chat_view(request, conversation_id=None):
# Not a JSON command, treat as a raw message and break if request.method == 'POST':
Message.objects.create(conversation=selected_conversation, content=ai_text, sender='ai') # Create a new conversation
break if 'title' in request.POST:
else: title = request.POST.get('title', 'New Conversation').strip()
# This block executes if the loop completes without a 'break' if not title:
logger.warning("AI loop finished after 7 iterations without sending a message.") title = 'New Conversation'
final_message = "I seem to be stuck in a loop. Could you clarify what you'd like me to do?" conversation = Conversation.objects.create(title=title)
Message.objects.create(conversation=selected_conversation, content=final_message, sender='ai') return redirect('core:chat_detail', conversation_id=conversation.id)
except Exception as e: # Send a message in an existing conversation
logger.error(f"An unexpected error occurred: {e}", exc_info=True) elif 'text' in request.POST and conversation_id:
ai_text = f"An error occurred: {str(e)}" text = request.POST.get('text').strip()
Message.objects.create(conversation=selected_conversation, content=ai_text, sender='ai') if text:
conversation = get_object_or_404(Conversation, id=conversation_id)
return redirect('core:chat_detail', 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') 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', { return render(request, 'core/chat.html', {
'conversation_list': conversations, 'conversation_list': conversations,
@ -340,13 +370,23 @@ def chat_view(request, conversation_id=None):
'timestamp': int(time.time()), 'timestamp': int(time.time()),
}) })
def conversation_list(request): def conversation_list(request):
conversations = Conversation.objects.order_by('-created_at') conversations = Conversation.objects.order_by('-created_at')
return render(request, 'core/conversation_list.html', {'conversation_list': conversations}) 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): def settings_view(request):
# Get or create the custom_instructions setting custom_instructions, _ = Setting.objects.get_or_create(
custom_instructions, created = Setting.objects.get_or_create(
key='custom_instructions', key='custom_instructions',
defaults={'value': ''} defaults={'value': ''}
) )
@ -358,4 +398,4 @@ def settings_view(request):
return render(request, 'core/settings.html', { return render(request, 'core/settings.html', {
'custom_instructions': custom_instructions 'custom_instructions': custom_instructions
}) })