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 from .models import Article, TodoItem, Conversation, Message 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 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: system_message = { "role": "system", "content": '''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(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 with status {response.get('status')}. Full error: {response.get('response')}") ai_text = "I couldn't process that. Please try again." Message.objects.create(conversation=selected_conversation, content=ai_text, sender='ai') break logger.info(f"AI raw response: {response}") ai_text = LocalAIApi.extract_text(response) logger.info(f"Extracted AI text: {ai_text}") if not ai_text: logger.warning("AI response was empty.") ai_text = "I couldn't process that. Please try again." Message.objects.create(conversation=selected_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=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): # 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: logger.error(f"An unexpected error occurred: {e}", exc_info=True) ai_text = f"An error occurred: {str(e)}" Message.objects.create(conversation=selected_conversation, content=ai_text, sender='ai') return redirect('core:chat_detail', conversation_id=conversation_id) conversations = Conversation.objects.order_by('-created_at') selected_conversation = get_object_or_404(Conversation, id=conversation_id) if conversation_id else None return render(request, 'core/chat.html', { 'conversation_list': conversations, 'selected_conversation': selected_conversation, 'timestamp': int(time.time()), })