407 lines
16 KiB
Python
407 lines
16 KiB
Python
from django.shortcuts import render, redirect, get_object_or_404
|
|
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
|
|
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)
|
|
if form.is_valid():
|
|
form.save()
|
|
return redirect('core:index')
|
|
else:
|
|
form = TodoItemForm()
|
|
|
|
todo_list = TodoItem.objects.all().order_by('-created_at')
|
|
articles = Article.objects.all()
|
|
|
|
context = {
|
|
'articles': articles,
|
|
'todo_list': todo_list,
|
|
'form': form,
|
|
'timestamp': int(time.time()),
|
|
}
|
|
return render(request, "core/index.html", context)
|
|
|
|
|
|
def kanban_board(request):
|
|
tasks = TodoItem.objects.all().order_by('created_at')
|
|
tasks_by_status = {
|
|
status_value: list(filter(lambda t: t.status == status_value, tasks))
|
|
for status_value, status_display in TodoItem.STATUS_CHOICES
|
|
}
|
|
|
|
context = {
|
|
'tasks_by_status': tasks_by_status,
|
|
'status_choices': TodoItem.STATUS_CHOICES,
|
|
'timestamp': int(time.time()),
|
|
}
|
|
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:
|
|
data = json.loads(request.body)
|
|
task_id = data.get('task_id')
|
|
new_status = data.get('new_status')
|
|
|
|
task = get_object_or_404(TodoItem, id=task_id)
|
|
task.status = new_status
|
|
task.save()
|
|
|
|
return JsonResponse({'success': True})
|
|
except (json.JSONDecodeError, TypeError, ValueError) as e:
|
|
return JsonResponse({'success': False, 'error': str(e)}, status=400)
|
|
|
|
|
|
@require_POST
|
|
def delete_task(request, task_id):
|
|
task = get_object_or_404(TodoItem, id=task_id)
|
|
task.delete()
|
|
|
|
# Redirect to the previous page or a default URL
|
|
referer = request.META.get('HTTP_REFERER')
|
|
if referer:
|
|
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', {})
|
|
logger.info(f"Executing command: {command_name} with args: {args}")
|
|
|
|
try:
|
|
if command_name == 'send_message':
|
|
message = args.get('message')
|
|
if not message:
|
|
logger.error("Command 'send_message' failed: 'message' is a required argument.")
|
|
return "[SYSTEM] Error: 'message' is a required argument for send_message."
|
|
return message
|
|
|
|
elif command_name == 'add_task':
|
|
title = args.get('title')
|
|
if not title:
|
|
logger.error("Command 'add_task' failed: 'title' is a required argument.")
|
|
return "[SYSTEM] Error: 'title' is a required argument for add_task."
|
|
|
|
new_task = TodoItem.objects.create(
|
|
title=title,
|
|
description=args.get('description', ''),
|
|
status=args.get('status', 'todo')
|
|
)
|
|
logger.info(f"Command 'add_task' executed successfully. New task ID: {new_task.id}")
|
|
return f"[SYSTEM] Command 'add_task' executed successfully. New task ID: {new_task.id}"
|
|
|
|
elif command_name == 'edit_task':
|
|
task_id = args.get('task_id')
|
|
if not task_id:
|
|
logger.error("Command 'edit_task' failed: 'task_id' is a required argument.")
|
|
return "[SYSTEM] Error: 'task_id' is a required argument for edit_task."
|
|
|
|
task = get_object_or_404(TodoItem, id=task_id)
|
|
|
|
if 'title' in args:
|
|
task.title = args['title']
|
|
if 'description' in args:
|
|
task.description = args['description']
|
|
if 'status' in args:
|
|
task.status = args['status']
|
|
|
|
task.save()
|
|
logger.info(f"Command 'edit_task' for task ID {task_id} executed successfully.")
|
|
return f"[SYSTEM] Command 'edit_task' for task ID {task_id} executed successfully."
|
|
|
|
elif command_name == 'delete_task':
|
|
task_id = args.get('task_id')
|
|
if not task_id:
|
|
logger.error("Command 'delete_task' failed: 'task_id' is a required argument.")
|
|
return "[SYSTEM] Error: 'task_id' is a required argument for delete_task."
|
|
|
|
task = get_object_or_404(TodoItem, id=task_id)
|
|
task.delete()
|
|
logger.info(f"Command 'delete_task' for task ID {task_id} executed successfully.")
|
|
return f"[SYSTEM] Command 'delete_task' for task ID {task_id} executed successfully."
|
|
|
|
else:
|
|
logger.warning(f"Unknown command received: '{command_name}'")
|
|
return f"[SYSTEM] Error: Unknown command '{command_name}'."
|
|
except Exception as e:
|
|
logger.error(f"Error executing command '{command_name}': {e}", exc_info=True)
|
|
return f"[SYSTEM] Error executing command '{command_name}': {e}"
|
|
|
|
|
|
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:
|
|
|
|
```json
|
|
{
|
|
"command": {
|
|
"name": "command_name",
|
|
"args": {
|
|
"arg1": "value1",
|
|
"arg2": "value2"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Available Commands:**
|
|
|
|
* `send_message`: Sends a message to the user. **USE THIS FOR ALL CONVERSATIONAL RESPONSES.**
|
|
* `args`:
|
|
* `message` (string, required): The message to send to the user.
|
|
* `add_task`: Adds a new task.
|
|
* `args`:
|
|
* `title` (string, required): The title of the task.
|
|
* `description` (string, optional): The description of the task.
|
|
* `status` (string, optional, default: 'todo'): The status of the task. Can be 'todo', 'inprogress', 'done', 'blocked'.
|
|
* `edit_task`: Edits an existing task.
|
|
* `args`:
|
|
* `task_id` (integer, required): The ID of the task to edit.
|
|
* `title` (string, optional): The new title.
|
|
* `description` (string, optional): The new description.
|
|
* `status` (string, optional): The new status.
|
|
* `delete_task`: Deletes a task.
|
|
* `args`:
|
|
* `task_id` (integer, required): The ID of the task to delete.
|
|
|
|
**Execution Loop:**
|
|
|
|
1. You can issue a series of commands to be executed sequentially.
|
|
2. The system executes each command and provides a result.
|
|
3. The loop will stop if you call `send_message` or after a maximum of 7 iterations.
|
|
|
|
**VERY IMPORTANT:**
|
|
- To talk to the user, you MUST use the `send_message` command. This command will STOP the execution loop.
|
|
- If you need to perform multiple actions (e.g., add a task and then comment on it), issue the action commands (`add_task`, `edit_task`, etc.) *before* using `send_message`.
|
|
- ONLY use commands other than `send_message` if the user explicitly asks you to `add`, `edit`, or `delete` tasks.
|
|
|
|
**Examples:**
|
|
|
|
* **User:** "Hi, how are you?"
|
|
* **Correct AI Response:**
|
|
```json
|
|
{
|
|
"command": {
|
|
"name": "send_message",
|
|
"args": {
|
|
"message": "I'm doing great, thanks for asking! How can I help you with your tasks today?"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
* **User:** "add a new task to buy milk"
|
|
* **Correct AI Response:**
|
|
```json
|
|
{
|
|
"command": {
|
|
"name": "add_task",
|
|
"args": {
|
|
"title": "buy milk"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**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_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(31): # 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'])
|
|
|
|
sender = 'ai' if command_name == 'send_message' else 'system'
|
|
Message.objects.create(conversation=conversation, content=command_result, sender=sender)
|
|
|
|
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')
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
|
|
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')
|
|
|
|
# Set is_generating to True
|
|
conversation.is_generating = True
|
|
conversation.save()
|
|
|
|
# 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 JsonResponse({'status': 'success', 'conversation_id': conversation_id})
|
|
return JsonResponse({'status': 'error', 'message': 'Text is required.'}, status=400)
|
|
|
|
conversations = Conversation.objects.order_by('-created_at')
|
|
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,
|
|
'selected_conversation': selected_conversation,
|
|
'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):
|
|
custom_instructions, _ = Setting.objects.get_or_create(
|
|
key='custom_instructions',
|
|
defaults={'value': ''}
|
|
)
|
|
|
|
if request.method == 'POST':
|
|
custom_instructions.value = request.POST.get('custom_instructions', '')
|
|
custom_instructions.save()
|
|
return redirect('core:settings')
|
|
|
|
return render(request, 'core/settings.html', {
|
|
'custom_instructions': custom_instructions
|
|
})
|