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