Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-07-21 08:46:24

0001 """AI app REST surface: propose / decide / delete for the proposal list.
0002 
0003 Thin peers over ``ai.services`` — the same auth stack as the PCS API
0004 (tunnel, session, token). The propose verb is the only mutation surface AI
0005 clients get; decide demands an authenticated human.
0006 """
0007 from rest_framework import status
0008 from rest_framework.authentication import (
0009     SessionAuthentication, TokenAuthentication,
0010 )
0011 from rest_framework.permissions import IsAuthenticated
0012 from rest_framework.response import Response
0013 from rest_framework.views import APIView
0014 
0015 from monitor_app.middleware import TunnelAuthentication
0016 from pcs.services import ServiceError
0017 
0018 from . import services
0019 
0020 
0021 class _AiApiView(APIView):
0022     authentication_classes = [TunnelAuthentication, SessionAuthentication,
0023                               TokenAuthentication]
0024     permission_classes = [IsAuthenticated]
0025 
0026 
0027 class ProposalProposeView(_AiApiView):
0028     def post(self, request):
0029         """Create AI propagation proposals (AI_PROPOSALS.md).
0030 
0031         Body: ``names``, ``state``, ``comment`` (required), ``replaced_by``,
0032         ``proposer``, ``scan_version``, ``batch_id``.
0033         """
0034         try:
0035             result = services.propose_propagation(
0036                 request.data.get('names') or [],
0037                 request.data.get('state'),
0038                 request.data.get('comment'),
0039                 replaced_by=request.data.get('replaced_by', ''),
0040                 proposer=request.data.get('proposer', ''),
0041                 scan_version=request.data.get('scan_version', 1),
0042                 batch_id=request.data.get('batch_id', ''),
0043                 created_by=request.user.username,
0044             )
0045         except ServiceError as e:
0046             return Response({'detail': e.detail}, status=e.status)
0047         return Response(result, status=status.HTTP_200_OK)
0048 
0049 
0050 class ProposalDecideView(_AiApiView):
0051     def post(self, request):
0052         """Approve or deny pending AI proposals.
0053 
0054         Body: ``names`` and/or ``ids``, ``decision`` ('approve' | 'deny'),
0055         ``quality`` (optional: wrong | poor | ok | good), ``filter``
0056         (optional audit record of the selecting filter).
0057         """
0058         try:
0059             result = services.proposal_decide(
0060                 request.data.get('names') or [],
0061                 request.data.get('decision'),
0062                 decided_by=request.user.username,
0063                 quality=request.data.get('quality', ''),
0064                 filter_state=request.data.get('filter', ''),
0065                 proposal_ids=request.data.get('ids') or [],
0066             )
0067         except ServiceError as e:
0068             return Response({'detail': e.detail}, status=e.status)
0069         except (TypeError, ValueError):
0070             return Response({'detail': 'ids must be integers'},
0071                             status=status.HTTP_400_BAD_REQUEST)
0072         return Response(result, status=status.HTTP_200_OK)
0073 
0074 
0075 class ProposalUndoView(_AiApiView):
0076     def post(self, request):
0077         """Undo executed AI proposals — the compensating action.
0078 
0079         Body: ``names`` and/or ``ids`` (selection mirrors decide).
0080         Restores the prior state captured at propose time through the
0081         same executor, origin-stamped ``undo``; expired offers (record
0082         moved on) are counted in ``moved``, never silently skipped.
0083         """
0084         try:
0085             result = services.proposal_undo(
0086                 request.data.get('names') or [],
0087                 undone_by=request.user.username,
0088                 proposal_ids=request.data.get('ids') or [],
0089             )
0090         except ServiceError as e:
0091             return Response({'detail': e.detail}, status=e.status)
0092         except (TypeError, ValueError):
0093             return Response({'detail': 'ids must be integers'},
0094                             status=status.HTTP_400_BAD_REQUEST)
0095         return Response(result, status=status.HTTP_200_OK)
0096 
0097 
0098 class NarrativeSaveView(_AiApiView):
0099     def post(self, request):
0100         """Save an edited narrative as a new corun-ai version.
0101 
0102         Body: ``group_id``, ``content``. Title, status, tags, and data are
0103         carried over from the current version — editing revises content;
0104         publication (status change) is a separate deliberate act.
0105         """
0106         from monitor_app.epicprod_logging import log_epicprod_action
0107 
0108         from .corun_client import CorunAPIError, CorunClient
0109 
0110         group_id = (request.data.get('group_id') or '').strip()
0111         content = request.data.get('content') or ''
0112         if not group_id or not content.strip():
0113             return Response({'detail': 'group_id and content are required'},
0114                             status=status.HTTP_400_BAD_REQUEST)
0115         try:
0116             client = CorunClient()
0117             current = client.get_page(group_id)
0118             data = dict(current.get('data') or {})
0119             data['author'] = request.user.username
0120             data['ui_visible'] = True
0121             result = client.create_version(
0122                 group_id,
0123                 content=content,
0124                 title=current.get('title', ''),
0125                 status=current.get('status', 'draft'),
0126                 data=data,
0127                 tags=current.get('tags') or [],
0128             )
0129         except CorunAPIError as exc:
0130             return Response({'detail': str(exc)},
0131                             status=status.HTTP_502_BAD_GATEWAY)
0132         name = (current.get('data') or {}).get('name', current.get('title', ''))
0133         log_epicprod_action(
0134             'web', 'narrative_edited',
0135             subject_type='narrative',
0136             subject_key=name,
0137             username=request.user.username,
0138             sublevel='normal', live_default=False,
0139             message=f'narrative {name} revised to version {result.get("version")}',
0140             version=result.get('version'),
0141         )
0142         return Response({'version': result.get('version')},
0143                         status=status.HTTP_200_OK)
0144 
0145 
0146 class NarrativeCommentView(_AiApiView):
0147     def post(self, request):
0148         """Post a comment on a narrative — the non-intrusive contribution
0149         path beside direct editing. Body: ``group_id``, ``content``."""
0150         from monitor_app.epicprod_logging import log_epicprod_action
0151 
0152         from .corun_client import CorunAPIError, CorunClient
0153 
0154         group_id = (request.data.get('group_id') or '').strip()
0155         content = (request.data.get('content') or '').strip()
0156         if not group_id or not content:
0157             return Response({'detail': 'group_id and content are required'},
0158                             status=status.HTTP_400_BAD_REQUEST)
0159         try:
0160             client = CorunClient()
0161             current = client.get_page(group_id)
0162             client.create_comment(group_id, content=content,
0163                                   data={'author': request.user.username})
0164         except CorunAPIError as exc:
0165             return Response({'detail': str(exc)},
0166                             status=status.HTTP_502_BAD_GATEWAY)
0167         name = (current.get('data') or {}).get('name', current.get('title', ''))
0168         log_epicprod_action(
0169             'web', 'narrative_commented',
0170             subject_type='narrative',
0171             subject_key=name,
0172             username=request.user.username,
0173             sublevel='normal', live_default=True,
0174             message=f'comment on narrative {name}',
0175         )
0176         return Response({'ok': True}, status=status.HTTP_200_OK)
0177 
0178 
0179 class ProposalDeleteView(_AiApiView):
0180     def post(self, request):
0181         """Delete AI proposal list rows (operator housekeeping). Body: ids."""
0182         try:
0183             result = services.proposal_delete(
0184                 request.data.get('ids') or [],
0185                 deleted_by=request.user.username,
0186             )
0187         except ServiceError as e:
0188             return Response({'detail': e.detail}, status=e.status)
0189         except (TypeError, ValueError):
0190             return Response({'detail': 'ids must be integers'},
0191                             status=status.HTTP_400_BAD_REQUEST)
0192         return Response(result, status=status.HTTP_200_OK)