diff --git a/hospexplorer/ask/admin.py b/hospexplorer/ask/admin.py index 8c38f3f..3910bbc 100644 --- a/hospexplorer/ask/admin.py +++ b/hospexplorer/ask/admin.py @@ -1,3 +1,16 @@ from django.contrib import admin +from ask.models import QARecord -# Register your models here. + +@admin.register(QARecord) +class QARecordAdmin(admin.ModelAdmin): + list_display = ["id", "user", "truncated_question", "question_timestamp", "answer_timestamp"] + list_filter = ["question_timestamp", "user"] + search_fields = ["question_text", "answer_text", "user__username"] + readonly_fields = ["question_timestamp", "answer_timestamp", "answer_raw_response"] + raw_id_fields = ["user"] + date_hierarchy = "question_timestamp" + + def truncated_question(self, obj): + return obj.question_text[:75] + "..." if len(obj.question_text) > 75 else obj.question_text + truncated_question.short_description = "Question" diff --git a/hospexplorer/ask/migrations/0001_initial.py b/hospexplorer/ask/migrations/0001_initial.py new file mode 100644 index 0000000..98d680a --- /dev/null +++ b/hospexplorer/ask/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# Generated by Django 6.0.1 on 2026-01-30 19:36 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='QARecord', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('question_text', models.TextField()), + ('question_timestamp', models.DateTimeField(auto_now_add=True)), + ('answer_text', models.TextField(blank=True, default='')), + ('answer_raw_response', models.JSONField(default=dict)), + ('answer_timestamp', models.DateTimeField(blank=True, null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='qa_records', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Q&A Record', + 'verbose_name_plural': 'Q&A Records', + 'ordering': ['-question_timestamp'], + 'indexes': [models.Index(fields=['user', '-question_timestamp'], name='ask_qarecor_user_id_f4353f_idx')], + }, + ), + ] diff --git a/hospexplorer/ask/migrations/0002_qarecord_is_error.py b/hospexplorer/ask/migrations/0002_qarecord_is_error.py new file mode 100644 index 0000000..10ddcac --- /dev/null +++ b/hospexplorer/ask/migrations/0002_qarecord_is_error.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-02-04 23:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ask', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='qarecord', + name='is_error', + field=models.BooleanField(default=False), + ), + ] diff --git a/hospexplorer/ask/models.py b/hospexplorer/ask/models.py index 71a8362..aa69d52 100644 --- a/hospexplorer/ask/models.py +++ b/hospexplorer/ask/models.py @@ -1,3 +1,32 @@ from django.db import models +from django.conf import settings -# Create your models here. + +class QARecord(models.Model): + """ + Stores a question-answer pair from user interactions with the LLM. + """ + # Question fields + question_text = models.TextField() + question_timestamp = models.DateTimeField(auto_now_add=True) + + # Answer fields + answer_text = models.TextField(blank=True, default="") + answer_raw_response = models.JSONField(default=dict) + answer_timestamp = models.DateTimeField(null=True, blank=True) + is_error = models.BooleanField(default=False) + + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="qa_records") + + class Meta: + ordering = ["-question_timestamp"] + verbose_name = "Q&A Record" + verbose_name_plural = "Q&A Records" + indexes = [ + models.Index(fields=["user", "-question_timestamp"]), + ] + + def __str__(self): + truncated = self.question_text[:50] + suffix = "..." if len(self.question_text) > 50 else "" + return f"{self.user.username}: {truncated}{suffix}" diff --git a/hospexplorer/ask/templates/_base.html b/hospexplorer/ask/templates/_base.html index 6665c53..16e5c75 100644 --- a/hospexplorer/ask/templates/_base.html +++ b/hospexplorer/ask/templates/_base.html @@ -26,6 +26,7 @@ {% if user.is_authenticated %}
Signed in as {{ user.username }}
+ Sign Out
{% endif %} @@ -57,11 +58,40 @@ -->
+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} {% block content %}{% endblock %}
+ + diff --git a/hospexplorer/ask/urls.py b/hospexplorer/ask/urls.py index 2a75452..aa9dcf1 100644 --- a/hospexplorer/ask/urls.py +++ b/hospexplorer/ask/urls.py @@ -7,4 +7,5 @@ path("", views.index, name="index"), path("mock", views.mock_response, name="mock-response"), path("query", views.query, name="query-llm"), + path("delete-history", views.delete_history, name="delete-history"), ] \ No newline at end of file diff --git a/hospexplorer/ask/views.py b/hospexplorer/ask/views.py index b948680..dc935ae 100644 --- a/hospexplorer/ask/views.py +++ b/hospexplorer/ask/views.py @@ -1,9 +1,15 @@ -from django.shortcuts import render +import logging +from django.shortcuts import render, redirect +from django.views.decorators.http import require_POST from django.http import JsonResponse from django.conf import settings from django.contrib.auth.decorators import login_required +from django.utils import timezone +from django.contrib import messages import ask.llm_connector +from ask.models import QARecord +logger = logging.getLogger(__name__) @login_required def index(request): @@ -12,17 +18,53 @@ def index(request): @login_required def mock_response(request): + """Returns a mock LLM response in the same format as the real server.""" return JsonResponse({ - "message": "Okay, the user wants a three-sentence bedtime story about a unicorn. Let's start by thinking about the key elements of a good bedtime story. They usually have a peaceful setting, a gentle conflict or quest, and a happy ending.\n\nFirst sentence needs to set the scene. Maybe a magical forest with a unicorn. Luna is a common unicorn name, sounds soft. Moonlight and stars could add a calming effect.\n\nSecond sentence should introduce a small problem or something the unicorn does. Healing powers are typical for unicorns. Maybe she finds an injured animal, like a fox. Using her horn to heal adds magic.\n\nThird sentence wraps it up with a happy ending. The fox recovers, they become friends, and the forest is peaceful. Emphasize safety and dreams to make it soothing for bedtime.\n\nCheck if it's exactly three sentences. Yes. Language is simple and comforting, suitable for a child. Avoid any scary elements. Make sure it flows smoothly and conveys warmth.\n\n\nUnder the shimmering moonlit sky, a silver-maned unicorn named Luna trotted through the enchanted forest, her hooves leaving trails of stardust. When she discovered a wounded fox whimpering beneath an ancient oak, she touched her glowing horn to its paw, weaving magic that healed the hurt. With the fox curled beside her, Luna rested on a bed of moss, her heart full as the forest whispered lullabies, ensuring all creatures drifted into dreams of peace." + "choices": [{ + "message": { + "content": "Under the shimmering moonlit sky, a silver-maned unicorn named Luna trotted through the enchanted forest, her hooves leaving trails of stardust. When she discovered a wounded fox whimpering beneath an ancient oak, she touched her glowing horn to its paw, weaving magic that healed the hurt. With the fox curled beside her, Luna rested on a bed of moss, her heart full as the forest whispered lullabies, ensuring all creatures drifted into dreams of peace." + } + }] }) @login_required def query(request): + query_text = request.GET.get("query", "") + record = QARecord.objects.create( + question_text=query_text, + user=request.user, + ) try: - llm_response = ask.llm_connector.query_llm(request.GET["query"]) - content = llm_response["choices"][0]["message"]["content"] - return JsonResponse({"message": content}) - except (KeyError, IndexError, TypeError) as e: - return JsonResponse({"error": f"Unexpected response from server: {e}"}, status=500) + llm_response = ask.llm_connector.query_llm(query_text) + + # Mock and real LLM use the same response format + if "choices" not in llm_response or not llm_response["choices"]: + raise ValueError("LLM response is missing structure") + answer_text = llm_response["choices"][0].get("message", {}).get("content", "") + + record.answer_text = answer_text + record.answer_raw_response = llm_response + record.answer_timestamp = timezone.now() + record.save() + + return JsonResponse({"message": answer_text}) + except (KeyError, IndexError, TypeError, ValueError) as e: + logger.exception("Unexpected response from server") + error_msg = f"Unexpected response from server: {e}" except Exception as e: - return JsonResponse({"error": f"Failed to connect to server: {e}"}, status=500) \ No newline at end of file + logger.exception("Failed to connect to server") + error_msg = f"Failed to connect to server: {e}" + + # The try block returns on success, so this only runs on error. + record.is_error = True + record.answer_text = error_msg + record.answer_timestamp = timezone.now() + record.save() + return JsonResponse({"error": error_msg}, status=500) + +@login_required +@require_POST +def delete_history(request): + request.user.qa_records.all().delete() + messages.success(request, "Question history deleted successfully!") + return redirect("ask:index") diff --git a/mockoon/llm-mock.json b/mockoon/llm-mock.json index f52caae..b31015a 100644 --- a/mockoon/llm-mock.json +++ b/mockoon/llm-mock.json @@ -17,7 +17,7 @@ "responses": [ { "uuid": "chat-completions-response", - "body": "{\n \"message\": \"This is a mock response from the local mockserver. Your query was received successfully.\"\n}", + "body": "{\n \"choices\": [{\n \"message\": {\n \"content\": \"Under the shimmering moonlit sky, a silver-maned unicorn named Luna trotted through the enchanted forest, her hooves leaving trails of stardust. When she discovered a wounded fox whimpering beneath an ancient oak, she touched her glowing horn to its paw, weaving magic that healed the hurt. With the fox curled beside her, Luna rested on a bed of moss, her heart full as the forest whispered lullabies, ensuring all creatures drifted into dreams of peace.\"\n }\n }]\n}", "latency": 0, "statusCode": 200, "label": "Success response",