diff --git a/ai/__pycache__/local_ai_api.cpython-311.pyc b/ai/__pycache__/local_ai_api.cpython-311.pyc index fa9977d..2d4a4fb 100644 Binary files a/ai/__pycache__/local_ai_api.cpython-311.pyc and b/ai/__pycache__/local_ai_api.cpython-311.pyc differ diff --git a/ai/local_ai_api.py b/ai/local_ai_api.py index bcff732..1e72159 100644 --- a/ai/local_ai_api.py +++ b/ai/local_ai_api.py @@ -382,17 +382,11 @@ def _http_request(url: str, method: str, body: Optional[bytes], headers: Dict[st "data": decoded if decoded is not None else response_body, } - error_message = "AI proxy request failed" - if isinstance(decoded, dict): - error_message = decoded.get("error") or decoded.get("message") or error_message - elif response_body: - error_message = response_body - return { "success": False, "status": status, - "error": error_message, - "response": decoded if decoded is not None else response_body, + "error": "AI proxy request failed", + "data": decoded if decoded is not None else response_body, } diff --git a/assets/pasted-20251120-002324-c898244a.png b/assets/pasted-20251120-002324-c898244a.png new file mode 100644 index 0000000..5665a32 Binary files /dev/null and b/assets/pasted-20251120-002324-c898244a.png differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index fd34097..bdf2023 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 001b8c8..617e204 100644 --- a/config/settings.py +++ b/config/settings.py @@ -152,3 +152,17 @@ STATICFILES_DIRS = [ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' X_FRAME_OPTIONS = 'ALLOWALL' + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'INFO', + }, +} diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index f0c2919..cd02fcf 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index ef076c1..0580716 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 4722135..2726ef6 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/migrations/0005_alter_message_options_remove_message_is_from_user_and_more.py b/core/migrations/0005_alter_message_options_remove_message_is_from_user_and_more.py new file mode 100644 index 0000000..53bb4c7 --- /dev/null +++ b/core/migrations/0005_alter_message_options_remove_message_is_from_user_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.7 on 2025-11-19 23:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_conversation_message'), + ] + + operations = [ + migrations.AlterModelOptions( + name='message', + options={'ordering': ['created_at']}, + ), + migrations.RemoveField( + model_name='message', + name='is_from_user', + ), + migrations.AddField( + model_name='message', + name='sender', + field=models.CharField(choices=[('user', 'User'), ('ai', 'AI'), ('system', 'System'), ('ai_command', 'AI Command')], default='user', max_length=20), + ), + ] diff --git a/core/migrations/__pycache__/0005_alter_message_options_remove_message_is_from_user_and_more.cpython-311.pyc b/core/migrations/__pycache__/0005_alter_message_options_remove_message_is_from_user_and_more.cpython-311.pyc new file mode 100644 index 0000000..d711cc1 Binary files /dev/null and b/core/migrations/__pycache__/0005_alter_message_options_remove_message_is_from_user_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 424eaaf..b9cb4ae 100644 --- a/core/models.py +++ b/core/models.py @@ -35,8 +35,11 @@ class Conversation(models.Model): class Message(models.Model): conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name='messages') content = models.TextField() - is_from_user = models.BooleanField(default=True) + sender = models.CharField(max_length=20, choices=[('user', 'User'), ('ai', 'AI'), ('system', 'System'), ('ai_command', 'AI Command')], default='user') created_at = models.DateTimeField(auto_now_add=True) + class Meta: + ordering = ['created_at'] + def __str__(self): - return f"Message from {'User' if self.is_from_user else 'AI'} at {self.created_at}" \ No newline at end of file + return f"Message from {self.get_sender_display()} at {self.created_at}" \ No newline at end of file diff --git a/core/templates/core/chat.html b/core/templates/core/chat.html index e75bdf1..f832463 100644 --- a/core/templates/core/chat.html +++ b/core/templates/core/chat.html @@ -33,10 +33,24 @@
{% for message in selected_conversation.messages.all %} -
+
-
{% if message.is_from_user %}You{% else %}AI{% endif %}
-

{{ message.content|linebreaksbr }}

+
+ {% if message.sender == 'user' %} + You + {% elif message.sender == 'ai' %} + AI + {% elif message.sender == 'system' %} + System + {% elif message.sender == 'ai_command' %} + AI Command + {% endif %} +
+ {% if message.sender == 'ai_command' %} +
{{ message.content|linebreaksbr }}
+ {% else %} +

{{ message.content|linebreaksbr }}

+ {% endif %}
{% endfor %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index ed0a715..4dd0437 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -38,8 +38,12 @@
-
+

Your To-Do List

+
+ {% csrf_token %} + +
@@ -50,6 +54,7 @@ + @@ -66,6 +71,12 @@ + {% empty %} diff --git a/core/templates/core/kanban.html b/core/templates/core/kanban.html index fb200fc..a8faacb 100644 --- a/core/templates/core/kanban.html +++ b/core/templates/core/kanban.html @@ -25,6 +25,10 @@ {% for item in tasks %}
+
+ {% csrf_token %} + +
{{ item.title }}

{{ item.description|default:""|truncatewords:15 }}

{% if item.tags %} diff --git a/core/urls.py b/core/urls.py index 515081a..1a7b1a1 100644 --- a/core/urls.py +++ b/core/urls.py @@ -8,6 +8,8 @@ urlpatterns = [ path('kanban/', views.kanban_board, name='kanban'), path('article//', views.article_detail, name='article_detail'), path('update_task_status/', views.update_task_status, name='update_task_status'), + path('delete_task//', views.delete_task, name='delete_task'), path('chat/', views.chat_view, name='chat'), path('chat//', views.chat_view, name='chat_detail'), + path('cleanup_tasks/', views.cleanup_tasks, name='cleanup_tasks'), ] \ No newline at end of file diff --git a/core/views.py b/core/views.py index 667fdc5..1bc3e9a 100644 --- a/core/views.py +++ b/core/views.py @@ -2,17 +2,21 @@ 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('index') + return redirect('core:index') else: form = TodoItemForm() @@ -60,6 +64,87 @@ def update_task_status(request): 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: @@ -70,25 +155,103 @@ def chat_view(request, conversation_id=None): 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: - Message.objects.create(conversation=selected_conversation, content=text, is_from_user=True) + command_name = None # Initialize command_name + Message.objects.create(conversation=selected_conversation, content=text, sender='user') - # Construct the conversation history for the AI history = [] for msg in selected_conversation.messages.order_by('created_at'): - role = "user" if msg.is_from_user else "assistant" + 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 helpful assistant for a project management application. Your purpose is to assist users with their tasks and provide information about the application. The application manages articles and a to-do list." + "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 issue a command. +2. The system executes it. +3. The system returns a result message to you, like `[SYSTEM] Command 'add_task' executed successfully. New task ID: 5`. +4. You can then issue another command. + +**VERY IMPORTANT:** +- To talk to the user, you MUST use the `send_message` command. +- ONLY use other commands 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"- {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 = { @@ -96,29 +259,55 @@ def chat_view(request, conversation_id=None): "content": f"Here is the current list of tasks:\n{task_list_str}" } + logger.info("Sending request to AI...") response = LocalAIApi.create_response({ "input": [ system_message, tasks_context, - *history - ] + ] + history, + "text": {"format": {"type": "json_object"}}, }) - ai_text = LocalAIApi.extract_text(response) - if not ai_text: + + 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." - except Exception as e: - ai_text = f"An error occurred: {str(e)}" + Message.objects.create(conversation=selected_conversation, content=ai_text, sender='ai') + else: + logger.info(f"AI raw response: {response}") + ai_text = LocalAIApi.extract_text(response) + logger.info(f"Extracted AI text: {ai_text}") - Message.objects.create(conversation=selected_conversation, content=ai_text, is_from_user=False) - - # Re-fetch the conversation to include the new messages for rendering - selected_conversation = get_object_or_404(Conversation, id=conversation_id) + 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') + else: + ai_message_content = ai_text + sender = 'ai' + + 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']) + + ai_message_content = command_result + sender = 'ai' if command_name == 'send_message' else 'system' + except (json.JSONDecodeError, TypeError): + # Not a JSON command, treat as a raw message. + pass + + Message.objects.create(conversation=selected_conversation, content=ai_message_content, sender=sender) + + 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') - if conversation_id: - selected_conversation = get_object_or_404(Conversation, id=conversation_id) - else: - selected_conversation = None + selected_conversation = get_object_or_404(Conversation, id=conversation_id) if conversation_id else None return render(request, 'core/chat.html', { 'conversation_list': conversations, diff --git a/static/css/custom.css b/static/css/custom.css index 742ef2f..551c68f 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -198,4 +198,33 @@ body { @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } +} + +/* System and AI Command Messages */ +.message.system .message-content { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.message.ai_command .message-content { + background-color: #d1ecf1; + color: #0c5460; + border: 1px solid #bee5eb; +} + +.message.ai_command .message-content pre { + white-space: pre-wrap; + word-wrap: break-word; + margin: 0; +} + +.kanban-card .card-body { + position: relative; +} + +.delete-task-form { + position: absolute; + top: 10px; + right: 10px; } \ No newline at end of file diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 742ef2f..551c68f 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -198,4 +198,33 @@ body { @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } +} + +/* System and AI Command Messages */ +.message.system .message-content { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.message.ai_command .message-content { + background-color: #d1ecf1; + color: #0c5460; + border: 1px solid #bee5eb; +} + +.message.ai_command .message-content pre { + white-space: pre-wrap; + word-wrap: break-word; + margin: 0; +} + +.kanban-card .card-body { + position: relative; +} + +.delete-task-form { + position: absolute; + top: 10px; + right: 10px; } \ No newline at end of file
Tags Status CreatedActions
{{ item.get_status_display }} {{ item.created_at|date:"M d, Y" }} +
+ {% csrf_token %} + +
+