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: def run_ai_process_in_background(conversation_id):
text = request.POST.get('text') """This function runs in a separate thread."""
selected_conversation = get_object_or_404(Conversation, id=conversation_id) try:
conversation = get_object_or_404(Conversation, id=conversation_id)
conversation.is_generating = True
conversation.save()
if text: history = []
command_name = None # Initialize command_name for msg in conversation.messages.order_by('created_at'):
Message.objects.create(conversation=selected_conversation, content=text, sender='user') 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})
history = [] custom_instructions, _ = Setting.objects.get_or_create(
for msg in selected_conversation.messages.order_by('created_at'): key='custom_instructions',
role = msg.sender defaults={'value': ''}
if role == 'ai': )
role = 'assistant' custom_instructions_text = custom_instructions.value + '\n\n' if custom_instructions.value else ''
elif role == 'system':
role = 'user'
history.append({"role": role, "content": msg.content})
try: system_message = {
custom_instructions, created = Setting.objects.get_or_create( "role": "system",
key='custom_instructions', "content": custom_instructions_text + '''You are a project management assistant. To communicate with the user, you MUST use the `send_message` command.
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 = { tasks_context = {
"role": "system", "role": "system",
"content": f"Here is the current list of tasks:\n{task_list_str}" "content": f"Here is the current list of tasks:\n{task_list_str}"
} }
logger.info("Starting AI processing loop...") logger.info("Starting AI processing loop...")
for i in range(7): # Loop up to 7 times for i in range(7): # Loop up to 7 times
logger.info(f"AI loop iteration {i+1}") logger.info(f"AI loop iteration {i+1}")
response = LocalAIApi.create_response({ response = LocalAIApi.create_response({
"input": [ "input": [
system_message, system_message,
tasks_context, tasks_context,
] + history, ] + history,
"text": {"format": {"type": "json_object"}}, "text": {"format": {"type": "json_object"}},
}) })
if not response.get("success"): if not response.get("success"):
logger.error(f"AI API request failed with status {response.get('status')}. Full error: {response.get('response')}") logger.error(f"AI API request failed. Full error: {response.get('error')}")
ai_text = "I couldn't process that. Please try again." ai_text = "I couldn't process that. Please try again."
Message.objects.create(conversation=selected_conversation, content=ai_text, sender='ai') Message.objects.create(conversation=conversation, content=ai_text, sender='ai')
break break
logger.info(f"AI raw response: {response}") ai_text = LocalAIApi.extract_text(response)
ai_text = LocalAIApi.extract_text(response) if not ai_text:
logger.info(f"Extracted AI text: {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
if not ai_text: try:
logger.warning("AI response was empty.") command_json = json.loads(ai_text)
ai_text = "I couldn't process that. Please try again." if 'command' in command_json:
Message.objects.create(conversation=selected_conversation, content=ai_text, sender='ai') command_name = command_json.get('command', {}).get('name')
break command_result = execute_command(command_json['command'])
try: sender = 'ai' if command_name == 'send_message' else 'system'
command_json = json.loads(ai_text) Message.objects.create(conversation=conversation, content=command_result, sender=sender)
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' if command_name == 'send_message':
Message.objects.create(conversation=selected_conversation, content=command_result, sender=sender) break
if command_name == 'send_message': history.append({"role": "user", "content": command_result})
break # Exit loop if send_message is called 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')
# Add system message with command result to history for next iteration except Exception as e:
history.append({"role": "user", "content": command_result}) 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)
else: finally:
# If it's a JSON but not a command, save it as a message and break # Ensure is_generating is always set to False
Message.objects.create(conversation=selected_conversation, content=ai_text, sender='ai') try:
break 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)
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: def chat_view(request, conversation_id=None):
logger.error(f"An unexpected error occurred: {e}", exc_info=True) if request.method == 'POST':
ai_text = f"An error occurred: {str(e)}" # Create a new conversation
Message.objects.create(conversation=selected_conversation, content=ai_text, sender='ai') 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)
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') 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': ''}
) )