Added extra code actions for viewsets
This commit is contained in:
parent
7398ab4116
commit
623b46c691
1 changed files with 713 additions and 57 deletions
|
|
@ -1,49 +1,150 @@
|
|||
from django.db.models import Q
|
||||
import httpx
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN, HTTP_201_CREATED, HTTP_200_OK
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN
|
||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
from apps.onboarding.models import AgentConfig, OnboardingSession, AgentInteractionLog, OnboardingFlow
|
||||
from apps.onboarding.serializers import (
|
||||
AgentConfigSerializer,
|
||||
OnboardingSessionSerializer,
|
||||
AgentInteractionLogSerializer,
|
||||
OnboardingFlowSerializer
|
||||
)
|
||||
from apps.accounts.models import Organization, Role
|
||||
from apps.accounts.permissions import CanManageOrganization, can_manage_organization
|
||||
from apps.onboarding.models import AgentConfig, AgentInteractionLog, OnboardingFlow, OnboardingSession
|
||||
from apps.onboarding.serializers import AgentConfigSerializer, AgentInteractionLogSerializer, OnboardingFlowSerializer, OnboardingSessionSerializer
|
||||
|
||||
class OnboardingFlowViewSet(ModelViewSet):
|
||||
queryset = OnboardingFlow.objects.all()
|
||||
serializer_class = OnboardingFlowSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
lookup_field = 'uuid'
|
||||
|
||||
filterset_fields = {
|
||||
'role__uuid': ['exact'],
|
||||
}
|
||||
|
||||
def get_permissions(self):
|
||||
permissions = super().get_permissions()
|
||||
if self.action in ['update', 'partial_update', 'destroy']:
|
||||
return [*permissions, CanManageOrganization()]
|
||||
return permissions
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
return OnboardingFlow.objects.filter(
|
||||
queryset = OnboardingFlow.objects.filter(
|
||||
Q(role__organization__owner=user) |
|
||||
Q(role__organization__members=user)
|
||||
).distinct().order_by('-created_at')
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
organization_uuid = self.request.query_params.get('organization_uuid')
|
||||
if organization_uuid in (None, ''):
|
||||
organization_uuid = self.request.data.get('organization_uuid')
|
||||
if organization_uuid:
|
||||
queryset = queryset.filter(role__organization__uuid=organization_uuid)
|
||||
|
||||
role_uuid = self.request.query_params.get('role_uuid')
|
||||
if role_uuid in (None, ''):
|
||||
role_uuid = self.request.data.get('role_uuid')
|
||||
if role_uuid:
|
||||
queryset = queryset.filter(role__uuid=role_uuid)
|
||||
|
||||
return queryset
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
role_uuid = request.data.get('role_uuid')
|
||||
if not role_uuid:
|
||||
raise ValidationError({'role_uuid': 'role_uuid is required.'})
|
||||
|
||||
role = Role.objects.filter(uuid=role_uuid).first()
|
||||
if not role:
|
||||
raise NotFound('Role not found')
|
||||
if not can_manage_organization(request.user, role.organization):
|
||||
raise PermissionDenied('Forbidden')
|
||||
|
||||
is_active = self._parse_bool(request.data.get('is_active'), default=True, field_name='is_active')
|
||||
|
||||
flow = OnboardingFlow.objects.create(
|
||||
title=(request.data.get('title') or f'Onboarding Flow: {role.name}').strip(),
|
||||
role=role,
|
||||
structure=request.data.get('structure') or [],
|
||||
is_active=is_active,
|
||||
)
|
||||
|
||||
serializer = self.get_serializer(flow)
|
||||
return Response(serializer.data, status=HTTP_201_CREATED)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
flow = self.get_object()
|
||||
title = request.data.get('title')
|
||||
structure = request.data.get('structure')
|
||||
is_active = request.data.get('is_active')
|
||||
|
||||
if title is not None:
|
||||
flow.title = str(title).strip()
|
||||
if structure is not None:
|
||||
flow.structure = structure if isinstance(structure, list) else flow.structure
|
||||
if is_active is not None:
|
||||
flow.is_active = self._parse_bool(is_active, field_name='is_active')
|
||||
|
||||
flow.save(update_fields=['title', 'structure', 'is_active', 'updated_at'])
|
||||
serializer = self.get_serializer(flow)
|
||||
return Response(serializer.data, status=HTTP_200_OK)
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
flow = self.get_object()
|
||||
|
||||
if 'title' in request.data:
|
||||
flow.title = str(request.data.get('title') or '').strip()
|
||||
if 'structure' in request.data:
|
||||
structure = request.data.get('structure')
|
||||
if not isinstance(structure, list):
|
||||
raise ValidationError({'structure': 'structure must be a list'})
|
||||
flow.structure = structure
|
||||
if 'is_active' in request.data:
|
||||
flow.is_active = self._parse_bool(request.data.get('is_active'), field_name='is_active')
|
||||
|
||||
flow.save(update_fields=['title', 'structure', 'is_active', 'updated_at'])
|
||||
serializer = self.get_serializer(flow)
|
||||
return Response(serializer.data, status=HTTP_200_OK)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
flow = self.get_object()
|
||||
with transaction.atomic():
|
||||
OnboardingSession.objects.filter(role=flow.role).delete()
|
||||
self.perform_destroy(flow)
|
||||
|
||||
return Response(status=204)
|
||||
|
||||
def _parse_bool(self, value, *, default=None, field_name='value'):
|
||||
if value is None:
|
||||
if default is not None:
|
||||
return default
|
||||
raise ValidationError({field_name: f'{field_name} is required'})
|
||||
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
|
||||
if isinstance(value, int):
|
||||
if value in (0, 1):
|
||||
return bool(value)
|
||||
raise ValidationError({field_name: f'{field_name} must be a boolean value'})
|
||||
|
||||
if isinstance(value, str):
|
||||
normalized = value.strip().lower()
|
||||
if normalized in ['true', '1', 'yes', 'on']:
|
||||
return True
|
||||
if normalized in ['false', '0', 'no', 'off']:
|
||||
return False
|
||||
|
||||
raise ValidationError({field_name: f'{field_name} must be a boolean value'})
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='start-session')
|
||||
def start_session(self, request, uuid=None):
|
||||
flow = self.get_object()
|
||||
|
||||
if not request.user.is_manager and not flow.role.members.filter(id=request.user.id).exists():
|
||||
return Response(
|
||||
{'error': 'Join this role before starting onboarding.'},
|
||||
status=HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
session, created = OnboardingSession.objects.get_or_create(
|
||||
user=request.user,
|
||||
|
|
@ -73,18 +174,114 @@ class AgentConfigViewSet(ModelViewSet):
|
|||
serializer_class = AgentConfigSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
lookup_field = 'uuid'
|
||||
|
||||
def get_permissions(self):
|
||||
permissions = super().get_permissions()
|
||||
if self.action in ['update', 'partial_update', 'destroy']:
|
||||
return [*permissions, CanManageOrganization()]
|
||||
return permissions
|
||||
|
||||
filterset_fields = {
|
||||
'organization__uuid': ['exact'],
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
return AgentConfig.objects.filter(organization__members=self.request.user).distinct()
|
||||
queryset = AgentConfig.objects.filter(
|
||||
Q(organization__owner=self.request.user) | Q(organization__members=self.request.user)
|
||||
).distinct().order_by('-updated_at')
|
||||
|
||||
def perform_create(self, serializer):
|
||||
if not self.request.user.is_manager:
|
||||
return Response({'error': 'Forbidden'}, status=HTTP_403_FORBIDDEN)
|
||||
serializer.save()
|
||||
organization_uuid = self.request.query_params.get('organization_uuid')
|
||||
if organization_uuid in (None, ''):
|
||||
organization_uuid = self.request.data.get('organization_uuid')
|
||||
if organization_uuid:
|
||||
queryset = queryset.filter(organization__uuid=organization_uuid)
|
||||
|
||||
role_uuid = self.request.query_params.get('role_uuid')
|
||||
if role_uuid in (None, ''):
|
||||
role_uuid = self.request.data.get('role_uuid')
|
||||
if role_uuid:
|
||||
queryset = queryset.filter(role__uuid=role_uuid)
|
||||
|
||||
return queryset
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
organization_uuid = request.query_params.get('organization_uuid')
|
||||
if organization_uuid in (None, ''):
|
||||
organization_uuid = request.data.get('organization_uuid')
|
||||
if not organization_uuid:
|
||||
raise ValidationError({'organization_uuid': 'organization_uuid is required.'})
|
||||
|
||||
organization = Organization.objects.filter(uuid=organization_uuid).filter(
|
||||
Q(owner=request.user) | Q(members=request.user),
|
||||
).first()
|
||||
if not organization:
|
||||
raise NotFound('Organization not found')
|
||||
if not can_manage_organization(request.user, organization):
|
||||
raise PermissionDenied('Forbidden')
|
||||
|
||||
name = str(request.data.get('name') or '').strip()
|
||||
if not name:
|
||||
raise ValidationError({'name': 'Name is required.'})
|
||||
|
||||
agent_type = str(request.data.get('agent_type') or '').strip()
|
||||
if not agent_type:
|
||||
raise ValidationError({'agent_type': 'agent_type is required.'})
|
||||
|
||||
role_uuid = request.data.get('role_uuid')
|
||||
role = None
|
||||
if role_uuid:
|
||||
role = Role.objects.filter(uuid=role_uuid, organization=organization).first()
|
||||
if not role:
|
||||
raise NotFound('Role not found in this organization')
|
||||
|
||||
config = AgentConfig.objects.create(
|
||||
organization=organization,
|
||||
role=role,
|
||||
name=name,
|
||||
agent_type=agent_type,
|
||||
system_prompt=str(request.data.get('system_prompt') or ''),
|
||||
llm_config=request.data.get('llm_config') or {},
|
||||
tool_permissions=request.data.get('tool_permissions') or [],
|
||||
)
|
||||
|
||||
serializer = self.get_serializer(config)
|
||||
return Response(serializer.data, status=HTTP_201_CREATED)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
config = self.get_object()
|
||||
|
||||
updatable_fields = {
|
||||
'name': request.data.get('name'),
|
||||
'agent_type': request.data.get('agent_type'),
|
||||
'system_prompt': request.data.get('system_prompt'),
|
||||
'llm_config': request.data.get('llm_config'),
|
||||
'tool_permissions': request.data.get('tool_permissions'),
|
||||
}
|
||||
|
||||
for field, value in updatable_fields.items():
|
||||
if value is not None:
|
||||
setattr(config, field, value)
|
||||
|
||||
config.save(update_fields=['name', 'agent_type', 'system_prompt', 'llm_config', 'tool_permissions', 'updated_at'])
|
||||
serializer = self.get_serializer(config)
|
||||
return Response(serializer.data, status=HTTP_200_OK)
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
config = self.get_object()
|
||||
|
||||
if 'name' in request.data:
|
||||
config.name = request.data.get('name')
|
||||
if 'agent_type' in request.data:
|
||||
config.agent_type = request.data.get('agent_type')
|
||||
if 'system_prompt' in request.data:
|
||||
config.system_prompt = request.data.get('system_prompt')
|
||||
if 'llm_config' in request.data:
|
||||
config.llm_config = request.data.get('llm_config')
|
||||
if 'tool_permissions' in request.data:
|
||||
config.tool_permissions = request.data.get('tool_permissions')
|
||||
|
||||
config.save(update_fields=['name', 'agent_type', 'system_prompt', 'llm_config', 'tool_permissions', 'updated_at'])
|
||||
serializer = self.get_serializer(config)
|
||||
return Response(serializer.data, status=HTTP_200_OK)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
class OnboardingSessionViewSet(ModelViewSet):
|
||||
queryset = OnboardingSession.objects.all()
|
||||
|
|
@ -92,10 +289,6 @@ class OnboardingSessionViewSet(ModelViewSet):
|
|||
permission_classes = [IsAuthenticated]
|
||||
lookup_field = 'uuid'
|
||||
|
||||
filterset_fields = {
|
||||
'role__uuid': ['exact'],
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
if user.is_manager:
|
||||
|
|
@ -103,8 +296,351 @@ class OnboardingSessionViewSet(ModelViewSet):
|
|||
else:
|
||||
queryset = OnboardingSession.objects.filter(user=user)
|
||||
|
||||
organization_uuid = self.request.query_params.get('organization_uuid')
|
||||
if organization_uuid in (None, ''):
|
||||
organization_uuid = self.request.data.get('organization_uuid')
|
||||
if organization_uuid:
|
||||
queryset = queryset.filter(role__organization__uuid=organization_uuid)
|
||||
|
||||
role_uuid = self.request.query_params.get('role_uuid')
|
||||
if role_uuid in (None, ''):
|
||||
role_uuid = self.request.data.get('role_uuid')
|
||||
if role_uuid:
|
||||
queryset = queryset.filter(role__uuid=role_uuid)
|
||||
|
||||
status_value = self.request.query_params.get('status')
|
||||
if status_value:
|
||||
queryset = queryset.filter(status=status_value)
|
||||
|
||||
return queryset.order_by('-created_at')
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
return Response(
|
||||
{'error': 'Use onboarding-flow/<uuid>/start-session/ to create a session.'},
|
||||
status=HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def _has_attempt(self, value):
|
||||
if value is None:
|
||||
return False
|
||||
if isinstance(value, str):
|
||||
return bool(value.strip())
|
||||
if isinstance(value, (list, dict, tuple, set)):
|
||||
return len(value) > 0
|
||||
return True
|
||||
|
||||
def _get_flow_for_session(self, session):
|
||||
state = session.state or {}
|
||||
flow_uuid = state.get('flow_uuid')
|
||||
|
||||
flow = None
|
||||
if flow_uuid:
|
||||
flow = OnboardingFlow.objects.filter(uuid=flow_uuid, role=session.role).first()
|
||||
|
||||
if not flow:
|
||||
flow = OnboardingFlow.objects.filter(role=session.role, is_active=True).order_by('-updated_at').first()
|
||||
|
||||
return flow
|
||||
|
||||
def _get_page_from_flow(self, flow, page_uuid):
|
||||
pages = flow.structure if isinstance(flow.structure, list) else []
|
||||
page = next(
|
||||
(
|
||||
item for item in pages
|
||||
if isinstance(item, dict) and str(item.get('uuid')) == str(page_uuid)
|
||||
),
|
||||
None,
|
||||
)
|
||||
return page, pages
|
||||
|
||||
def _upsert_page_responses(self, session, page_uuid, responses):
|
||||
state = session.state or {}
|
||||
stored_responses = state.get('responses', {})
|
||||
if not isinstance(stored_responses, dict):
|
||||
stored_responses = {}
|
||||
|
||||
stored_responses[str(page_uuid)] = responses
|
||||
state['responses'] = stored_responses
|
||||
state['last_page_uuid'] = str(page_uuid)
|
||||
session.state = state
|
||||
session.save(update_fields=['state', 'updated_at'])
|
||||
|
||||
def _record_page_visit(self, session, page_uuid):
|
||||
state = session.state or {}
|
||||
visited_pages = state.get('visited_pages', [])
|
||||
if not isinstance(visited_pages, list):
|
||||
visited_pages = []
|
||||
|
||||
page_uuid_str = str(page_uuid)
|
||||
if page_uuid_str not in visited_pages:
|
||||
visited_pages.append(page_uuid_str)
|
||||
|
||||
state['visited_pages'] = visited_pages
|
||||
state['last_page_uuid'] = page_uuid_str
|
||||
session.state = state
|
||||
session.save(update_fields=['state', 'updated_at'])
|
||||
|
||||
def _mark_page_progress(self, session, page_uuid, total_pages, is_final_quiz=False):
|
||||
if is_final_quiz:
|
||||
return
|
||||
|
||||
state = session.state or {}
|
||||
completed_modules = state.get('completed_modules', [])
|
||||
if not isinstance(completed_modules, list):
|
||||
completed_modules = []
|
||||
|
||||
page_uuid_str = str(page_uuid)
|
||||
if page_uuid_str not in completed_modules:
|
||||
completed_modules.append(page_uuid_str)
|
||||
|
||||
state['completed_modules'] = completed_modules
|
||||
completed_count = len(completed_modules)
|
||||
state['progress_percentage'] = int(round((completed_count / total_pages) * 100)) if total_pages else 0
|
||||
|
||||
session.state = state
|
||||
session.save(update_fields=['state', 'updated_at'])
|
||||
|
||||
def _build_system_prompt(self, config):
|
||||
if not config:
|
||||
return "You are a helpful onboarding assistant."
|
||||
base_prompt = config.system_prompt or "You are a helpful onboarding assistant."
|
||||
permissions = config.tool_permissions or []
|
||||
if permissions:
|
||||
return f"{base_prompt}\n\nAllowed tools: {', '.join(str(p) for p in permissions)}"
|
||||
return base_prompt
|
||||
|
||||
def _get_knowledge_agent_config(self, session):
|
||||
role_specific = AgentConfig.objects.filter(
|
||||
role=session.role,
|
||||
agent_type='knowledge',
|
||||
).order_by('-updated_at').first()
|
||||
if role_specific:
|
||||
return role_specific
|
||||
|
||||
return AgentConfig.objects.filter(
|
||||
organization=session.role.organization,
|
||||
role__isnull=True,
|
||||
agent_type='knowledge',
|
||||
).order_by('-updated_at').first()
|
||||
|
||||
def _run_ka_help(self, session, page_title, page_body, user_message):
|
||||
config = self._get_knowledge_agent_config(session)
|
||||
fallback = (
|
||||
"I couldn't reach the knowledge model right now. "
|
||||
"Please try again, or clarify which part of this module is confusing and I can provide a shorter explanation."
|
||||
)
|
||||
|
||||
if not config:
|
||||
return fallback
|
||||
|
||||
prompt = (
|
||||
"Help the learner understand this onboarding page. Keep the explanation concise and practical. "
|
||||
"Use markdown with bullets when useful.\n\n"
|
||||
f"Role: {session.role.name}\n"
|
||||
f"Page Title: {page_title}\n"
|
||||
f"Page Body (excerpt): {str(page_body)[:2000]}\n"
|
||||
f"Learner question: {user_message}"
|
||||
)
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=60.0) as client:
|
||||
response = client.post(
|
||||
f"{settings.INFERENCE_URL}/v1/chat/completions",
|
||||
json={
|
||||
"model": (config.llm_config or {}).get("model_id", "meta-llama-3.1-8b"),
|
||||
"messages": [
|
||||
{"role": "system", "content": self._build_system_prompt(config)},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
res_json = response.json()
|
||||
content = res_json.get('choices', [{}])[0].get('message', {}).get('content')
|
||||
if isinstance(content, str) and content.strip():
|
||||
return content.strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return fallback
|
||||
|
||||
def _run_ka_page_revision(self, session, page_title, page_body, user_message):
|
||||
config = self._get_knowledge_agent_config(session)
|
||||
if not config:
|
||||
return None
|
||||
|
||||
prompt = (
|
||||
"Revise the onboarding page content by integrating the learner's clarification request directly into the main page text. "
|
||||
"Use the current page as the source of truth, preserve useful structure, and improve clarity and examples where needed. "
|
||||
"Do not append a separate 'Clarification' section. Return ONLY the fully revised markdown page body.\n\n"
|
||||
f"Role: {session.role.name}\n"
|
||||
f"Page Title: {page_title}\n"
|
||||
f"Learner clarification request: {user_message}\n\n"
|
||||
f"Current page markdown:\n{str(page_body)[:12000]}"
|
||||
)
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=60.0) as client:
|
||||
response = client.post(
|
||||
f"{settings.INFERENCE_URL}/v1/chat/completions",
|
||||
json={
|
||||
"model": (config.llm_config or {}).get("model_id", "meta-llama-3.1-8b"),
|
||||
"messages": [
|
||||
{"role": "system", "content": self._build_system_prompt(config)},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
res_json = response.json()
|
||||
content = res_json.get('choices', [{}])[0].get('message', {}).get('content')
|
||||
revised = str(content or '').strip()
|
||||
if revised:
|
||||
return revised
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def _append_page_help(self, session, page_uuid, user_message, assistant_message):
|
||||
state = session.state or {}
|
||||
page_help = state.get('page_help', {})
|
||||
if not isinstance(page_help, dict):
|
||||
page_help = {}
|
||||
|
||||
thread = page_help.get(str(page_uuid), [])
|
||||
if not isinstance(thread, list):
|
||||
thread = []
|
||||
|
||||
thread.append({
|
||||
'question': str(user_message),
|
||||
'answer': str(assistant_message),
|
||||
'timestamp': timezone.now().isoformat(),
|
||||
})
|
||||
|
||||
page_help[str(page_uuid)] = thread[-20:]
|
||||
state['page_help'] = page_help
|
||||
session.state = state
|
||||
session.save(update_fields=['state', 'updated_at'])
|
||||
|
||||
def _update_flow_page_body(self, session, page_uuid, new_body):
|
||||
flow = self._get_flow_for_session(session)
|
||||
if not flow:
|
||||
return False
|
||||
|
||||
structure = flow.structure if isinstance(flow.structure, list) else []
|
||||
updated = False
|
||||
for page in structure:
|
||||
if not isinstance(page, dict):
|
||||
continue
|
||||
if str(page.get('uuid')) != str(page_uuid):
|
||||
continue
|
||||
page['body'] = str(new_body)
|
||||
updated = True
|
||||
break
|
||||
|
||||
if not updated:
|
||||
return False
|
||||
|
||||
flow.structure = structure
|
||||
flow.save(update_fields=['structure', 'updated_at'])
|
||||
return True
|
||||
|
||||
def _evaluate_final_quiz(self, session):
|
||||
flow = self._get_flow_for_session(session)
|
||||
if not flow:
|
||||
return None, {'error': 'Onboarding flow not found for this session.'}
|
||||
|
||||
pages = flow.structure if isinstance(flow.structure, list) else []
|
||||
if not pages:
|
||||
return None, {'error': 'Onboarding flow has no pages.'}
|
||||
|
||||
quiz_page = None
|
||||
for page in pages:
|
||||
if not isinstance(page, dict):
|
||||
continue
|
||||
meta = page.get('meta') if isinstance(page.get('meta'), dict) else {}
|
||||
if str(meta.get('page_type') or '').strip() == 'final_quiz':
|
||||
quiz_page = page
|
||||
break
|
||||
|
||||
if quiz_page is None:
|
||||
quiz_page = pages[-1] if isinstance(pages[-1], dict) else None
|
||||
|
||||
if not isinstance(quiz_page, dict):
|
||||
return None, {'error': 'Final quiz page could not be identified.'}
|
||||
|
||||
quiz_fields = quiz_page.get('fields') if isinstance(quiz_page.get('fields'), list) else []
|
||||
if not quiz_fields:
|
||||
return None, {'error': 'Final quiz has no questions.'}
|
||||
|
||||
meta = quiz_page.get('meta') if isinstance(quiz_page.get('meta'), dict) else {}
|
||||
pass_mark = meta.get('pass_mark', 80)
|
||||
try:
|
||||
pass_mark = int(pass_mark)
|
||||
except Exception:
|
||||
pass_mark = 80
|
||||
|
||||
state = session.state or {}
|
||||
responses = state.get('responses', {})
|
||||
if not isinstance(responses, dict):
|
||||
responses = {}
|
||||
|
||||
page_uuid = str(quiz_page.get('uuid') or '')
|
||||
page_responses = responses.get(page_uuid, {})
|
||||
if not isinstance(page_responses, dict):
|
||||
page_responses = {}
|
||||
|
||||
required_count = 0
|
||||
missing_required_keys = []
|
||||
gradable_count = 0
|
||||
correct_count = 0
|
||||
|
||||
for field in quiz_fields:
|
||||
if not isinstance(field, dict):
|
||||
continue
|
||||
|
||||
key = str(field.get('key') or '').strip()
|
||||
if not key:
|
||||
continue
|
||||
|
||||
answer = page_responses.get(key)
|
||||
attempted = self._has_attempt(answer)
|
||||
|
||||
if bool(field.get('required', False)):
|
||||
required_count += 1
|
||||
if not attempted:
|
||||
missing_required_keys.append(key)
|
||||
|
||||
validation = field.get('validation') if isinstance(field.get('validation'), dict) else {}
|
||||
correct_option = validation.get('correct_option')
|
||||
if correct_option is None:
|
||||
continue
|
||||
|
||||
gradable_count += 1
|
||||
if attempted and str(answer) == str(correct_option):
|
||||
correct_count += 1
|
||||
|
||||
score_percentage = int(round((correct_count / gradable_count) * 100)) if gradable_count else 0
|
||||
passed = len(missing_required_keys) == 0 and gradable_count > 0 and score_percentage >= pass_mark
|
||||
|
||||
quiz_result = {
|
||||
'page_uuid': page_uuid,
|
||||
'pass_mark': pass_mark,
|
||||
'required_count': required_count,
|
||||
'missing_required_keys': missing_required_keys,
|
||||
'gradable_count': gradable_count,
|
||||
'correct_count': correct_count,
|
||||
'score_percentage': score_percentage,
|
||||
'passed': passed,
|
||||
}
|
||||
|
||||
state['final_quiz_result'] = quiz_result
|
||||
session.state = state
|
||||
session.save(update_fields=['state', 'updated_at'])
|
||||
|
||||
return quiz_result, None
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='interact')
|
||||
def interact(self, request, uuid=None):
|
||||
session = self.get_object()
|
||||
|
|
@ -115,37 +651,113 @@ class OnboardingSessionViewSet(ModelViewSet):
|
|||
if not user_message and not page_uuid:
|
||||
return Response({'error': 'Message or page_uuid is required'}, status=HTTP_400_BAD_REQUEST)
|
||||
|
||||
if page_uuid:
|
||||
self._record_page_visit(session, page_uuid)
|
||||
|
||||
if isinstance(responses, dict):
|
||||
state = session.state or {}
|
||||
stored_responses = state.get('responses', {})
|
||||
if not isinstance(stored_responses, dict):
|
||||
stored_responses = {}
|
||||
|
||||
flow = self._get_flow_for_session(session)
|
||||
total_pages = len(flow.structure) if flow and isinstance(flow.structure, list) else 0
|
||||
if page_uuid:
|
||||
stored_responses[str(page_uuid)] = responses
|
||||
self._upsert_page_responses(session, page_uuid, responses)
|
||||
is_final_quiz = False
|
||||
if flow:
|
||||
page, _ = self._get_page_from_flow(flow, page_uuid)
|
||||
if isinstance(page, dict):
|
||||
meta = page.get('meta') if isinstance(page.get('meta'), dict) else {}
|
||||
is_final_quiz = str(meta.get('page_type') or '').strip() == 'final_quiz'
|
||||
if total_pages:
|
||||
self._mark_page_progress(session, page_uuid, total_pages, is_final_quiz=is_final_quiz)
|
||||
else:
|
||||
state = session.state or {}
|
||||
stored_responses = state.get('responses', {})
|
||||
if not isinstance(stored_responses, dict):
|
||||
stored_responses = {}
|
||||
stored_responses.update(responses)
|
||||
state['responses'] = stored_responses
|
||||
session.state = state
|
||||
session.save(update_fields=['state', 'updated_at'])
|
||||
|
||||
state['responses'] = stored_responses
|
||||
if page_uuid:
|
||||
state['last_page_uuid'] = str(page_uuid)
|
||||
session.state = state
|
||||
session.save(update_fields=['state', 'updated_at'])
|
||||
|
||||
AgentInteractionLog.objects.create(
|
||||
session=session,
|
||||
sender_type='user',
|
||||
content=user_message or f'Submitted onboarding responses for page {page_uuid or "unknown"}',
|
||||
tool_call_metadata={'page_uuid': page_uuid, 'has_responses': isinstance(responses, dict)}
|
||||
)
|
||||
if user_message or isinstance(responses, dict):
|
||||
AgentInteractionLog.objects.create(
|
||||
session=session,
|
||||
sender_type='user',
|
||||
content=user_message or f'Submitted onboarding responses for page {page_uuid or "unknown"}',
|
||||
tool_call_metadata={'page_uuid': page_uuid, 'has_responses': isinstance(responses, dict)}
|
||||
)
|
||||
|
||||
|
||||
|
||||
return Response({
|
||||
'status': 'received',
|
||||
'session_state': session.state
|
||||
'session_state': session.state,
|
||||
})
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='ask-ka')
|
||||
def ask_ka(self, request, uuid=None):
|
||||
session = self.get_object()
|
||||
page_uuid = request.data.get('page_uuid')
|
||||
user_message = request.data.get('message')
|
||||
mode = request.data.get('mode', 'separate')
|
||||
|
||||
if not page_uuid or not user_message:
|
||||
return Response({'error': 'page_uuid and message are required.'}, status=HTTP_400_BAD_REQUEST)
|
||||
|
||||
flow = self._get_flow_for_session(session)
|
||||
if not flow:
|
||||
return Response({'error': 'Onboarding flow not found.'}, status=HTTP_400_BAD_REQUEST)
|
||||
|
||||
page, _ = self._get_page_from_flow(flow, page_uuid)
|
||||
if not isinstance(page, dict):
|
||||
return Response({'error': 'Page not found for this flow.'}, status=HTTP_400_BAD_REQUEST)
|
||||
|
||||
page_title = str(page.get('title') or 'Onboarding Page')
|
||||
page_body = str(page.get('body') or '')
|
||||
updated_page = False
|
||||
assistant_message = ''
|
||||
if str(mode) == 'update_page':
|
||||
revised_body = self._run_ka_page_revision(session, page_title, page_body, str(user_message))
|
||||
if revised_body:
|
||||
updated_page = self._update_flow_page_body(session, page_uuid, revised_body)
|
||||
if updated_page:
|
||||
assistant_message = (
|
||||
"Updated this page by integrating your clarification request into the core content. "
|
||||
"Please review the revised page text above."
|
||||
)
|
||||
|
||||
if not assistant_message:
|
||||
assistant_message = self._run_ka_help(session, page_title, page_body, str(user_message))
|
||||
|
||||
self._append_page_help(session, page_uuid, user_message, assistant_message)
|
||||
|
||||
AgentInteractionLog.objects.create(
|
||||
session=session,
|
||||
sender_type='user',
|
||||
content=str(user_message),
|
||||
tool_call_metadata={
|
||||
'action': 'ask_ka',
|
||||
'page_uuid': str(page_uuid),
|
||||
'mode': str(mode),
|
||||
},
|
||||
)
|
||||
AgentInteractionLog.objects.create(
|
||||
session=session,
|
||||
sender_type='ai',
|
||||
content=str(assistant_message),
|
||||
tool_call_metadata={
|
||||
'action': 'ask_ka_response',
|
||||
'page_uuid': str(page_uuid),
|
||||
'mode': str(mode),
|
||||
'updated_page': updated_page,
|
||||
},
|
||||
)
|
||||
|
||||
return Response({
|
||||
'status': 'ok',
|
||||
'answer': assistant_message,
|
||||
'updated_page': updated_page,
|
||||
'session_state': session.state,
|
||||
}, status=HTTP_200_OK)
|
||||
|
||||
@action(detail=True, methods=['get'], url_path='history')
|
||||
def history(self, request, uuid=None):
|
||||
session = self.get_object()
|
||||
|
|
@ -156,10 +768,33 @@ class OnboardingSessionViewSet(ModelViewSet):
|
|||
@action(detail=True, methods=['post'], url_path='complete')
|
||||
def complete(self, request, uuid=None):
|
||||
session = self.get_object()
|
||||
|
||||
quiz_result, quiz_error = self._evaluate_final_quiz(session)
|
||||
if quiz_error:
|
||||
return Response(quiz_error, status=HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not quiz_result.get('passed'):
|
||||
return Response({
|
||||
'error': 'Final quiz pass mark not met.',
|
||||
'quiz_result': quiz_result,
|
||||
}, status=HTTP_400_BAD_REQUEST)
|
||||
|
||||
state = session.state or {}
|
||||
completed_modules = state.get('completed_modules', [])
|
||||
if not isinstance(completed_modules, list):
|
||||
completed_modules = []
|
||||
if quiz_result.get('page_uuid') and quiz_result['page_uuid'] not in completed_modules:
|
||||
completed_modules.append(quiz_result['page_uuid'])
|
||||
state['completed_modules'] = completed_modules
|
||||
flow = self._get_flow_for_session(session)
|
||||
total_pages = len(flow.structure) if flow and isinstance(flow.structure, list) else 0
|
||||
state['progress_percentage'] = int(round((len(completed_modules) / total_pages) * 100)) if total_pages else 100
|
||||
session.state = state
|
||||
|
||||
session.status = 'completed'
|
||||
session.completed_at = timezone.now()
|
||||
session.save()
|
||||
return Response({'message': 'Session marked as completed'})
|
||||
session.save(update_fields=['status', 'completed_at', 'state', 'updated_at'])
|
||||
return Response({'message': 'Session marked as completed', 'quiz_result': quiz_result})
|
||||
|
||||
class AgentInteractionLogViewSet(ReadOnlyModelViewSet):
|
||||
queryset = AgentInteractionLog.objects.all()
|
||||
|
|
@ -167,13 +802,34 @@ class AgentInteractionLogViewSet(ReadOnlyModelViewSet):
|
|||
permission_classes = [IsAuthenticated]
|
||||
lookup_field = 'uuid'
|
||||
|
||||
filterset_fields = {
|
||||
'session__uuid': ['exact'],
|
||||
'session__role__uuid': ['exact'],
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
return AgentInteractionLog.objects.filter(
|
||||
Q(session__user=self.request.user) |
|
||||
Q(session__role__organization__owner=self.request.user)
|
||||
user = self.request.user
|
||||
manager_scope = Q()
|
||||
if user.is_manager:
|
||||
manager_scope = Q(session__role__organization__members=user)
|
||||
|
||||
queryset = AgentInteractionLog.objects.filter(
|
||||
Q(session__user=user) |
|
||||
Q(session__role__organization__owner=user) |
|
||||
manager_scope
|
||||
).distinct()
|
||||
|
||||
session_uuid = self.request.query_params.get('session_uuid')
|
||||
if session_uuid in (None, ''):
|
||||
session_uuid = self.request.data.get('session_uuid')
|
||||
if session_uuid:
|
||||
queryset = queryset.filter(session__uuid=session_uuid)
|
||||
|
||||
role_uuid = self.request.query_params.get('role_uuid')
|
||||
if role_uuid in (None, ''):
|
||||
role_uuid = self.request.data.get('role_uuid')
|
||||
if role_uuid:
|
||||
queryset = queryset.filter(session__role__uuid=role_uuid)
|
||||
|
||||
organization_uuid = self.request.query_params.get('organization_uuid')
|
||||
if organization_uuid in (None, ''):
|
||||
organization_uuid = self.request.data.get('organization_uuid')
|
||||
if organization_uuid:
|
||||
queryset = queryset.filter(session__role__organization__uuid=organization_uuid)
|
||||
|
||||
return queryset.order_by('created_at')
|
||||
|
|
|
|||
Loading…
Reference in a new issue