async AI job
This commit is contained in:
parent
1566d6c207
commit
4c1df17a46
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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>
|
||||||
|
|
||||||
|
|||||||
254
core/views.py
254
core/views.py
@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user