Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-07-22 09:42:03

0001 """MCP tools for append-only AI assessments — the platform mechanism.
0002 
0003 Registration, retrieval, and corun storage live here; what an assessment
0004 can be about is domain vocabulary, supplied through the subject-type
0005 registry below (the mechanism/policy split the action registry uses).
0006 The epicprod subject types register from ``swf_epicprod.ai_subjects``.
0007 """
0008 
0009 import logging
0010 
0011 from asgiref.sync import sync_to_async
0012 
0013 from ai.assessments import (
0014     CORUN_ASSESSMENT_SECTION,
0015     append_corun_page_group_id,
0016     corun_page_items,
0017 )
0018 from ai.corun_client import CorunAPIError, CorunClient, corun_configured
0019 from monitor_app.epicprod_logging import log_epicprod_action
0020 from monitor_app.mcp import mcp
0021 
0022 logger = logging.getLogger(__name__)
0023 
0024 
0025 # Subject-type registry. A domain registers each assessable subject type
0026 # once at import; unknown types still register unlinked (no local subject
0027 # pointer), so the mechanism never gates on the registry's contents.
0028 SUBJECT_TYPE_ALIASES = {}
0029 SUBJECT_RESOLVERS = {}
0030 
0031 
0032 def register_subject_type(canonical, resolver, aliases=()):
0033     """Register a subject type: ``resolver(subject_key, data)`` returns
0034     {target_obj, target_json_field, subject_key, subject_label,
0035     subject_url} or raises when the key resolves to no object."""
0036     SUBJECT_RESOLVERS[canonical] = resolver
0037     SUBJECT_TYPE_ALIASES[canonical] = canonical
0038     for alias in aliases:
0039         SUBJECT_TYPE_ALIASES[alias] = canonical
0040 
0041 
0042 def _canonical_subject_type(subject_type):
0043     key = str(subject_type or '').strip().lower()
0044     return SUBJECT_TYPE_ALIASES.get(key, str(subject_type or '').strip())
0045 
0046 
0047 def _resolve_subject(subject_type, subject_key, data):
0048     resolver = SUBJECT_RESOLVERS.get(subject_type)
0049     if resolver is not None:
0050         return resolver(subject_key, data)
0051     return {
0052         'target_obj': None,
0053         'target_json_field': '',
0054         'subject_key': str(subject_key).strip(),
0055         'subject_label': '',
0056         'subject_url': '',
0057     }
0058 
0059 
0060 def _register_ai_assessment_sync(
0061     subject_type,
0062     subject_key,
0063     assessment,
0064     username,
0065     ai,
0066     subject_label,
0067     subject_url,
0068     data,
0069     title='',
0070 ):
0071     canonical_type = _canonical_subject_type(subject_type)
0072     if not canonical_type:
0073         return {'success': False, 'error': 'subject_type is required'}
0074     if not str(subject_key or '').strip():
0075         return {'success': False, 'error': 'subject_key is required'}
0076     if not str(assessment or '').strip():
0077         return {'success': False, 'error': 'assessment is required'}
0078     if data is not None and not isinstance(data, dict):
0079         return {'success': False, 'error': 'data must be an object when provided'}
0080 
0081     payload_data = dict(data or {})
0082     payload_data.setdefault('registered_via', 'mcp')
0083     payload_data.setdefault('mcp_tool', 'epic_register_ai_assessment')
0084     try:
0085         resolved = _resolve_subject(canonical_type, subject_key, payload_data)
0086     except Exception as exc:
0087         logger.warning(
0088             'AI assessment subject resolution failed: type=%s key=%s error=%s',
0089             canonical_type, subject_key, exc,
0090         )
0091         return {
0092             'success': False,
0093             'error': f'No local subject found for {canonical_type}:{subject_key}',
0094             'subject_type': canonical_type,
0095             'subject_key': str(subject_key),
0096         }
0097 
0098     if not corun_configured():
0099         return {
0100             'success': False,
0101             'error': 'corun-ai config CORUN_BASE_URL and CORUN_API_TOKEN must be configured',
0102             'subject_type': canonical_type,
0103             'subject_key': resolved['subject_key'],
0104         }
0105 
0106     resolved_label = subject_label or resolved.get('subject_label') or ''
0107     resolved_url = subject_url or resolved.get('subject_url') or ''
0108     username_value = str(username or 'mcp').strip() or 'mcp'
0109     ai_value = str(ai or 'unknown').strip() or 'unknown'
0110     assessment_text = str(assessment).strip()
0111     report_title = (str(title).strip()
0112                     or f"AI assessment: {canonical_type} {resolved['subject_key']}")
0113     linked = bool(resolved.get('target_obj') and resolved.get('target_json_field'))
0114     page_data = {
0115         **payload_data,
0116         'artifact_type': 'ai_assessment',
0117         'source_system': 'swf-monitor',
0118         'ui_visible': False,
0119         'subject_type': canonical_type,
0120         'subject_key': resolved['subject_key'],
0121         'subject_label': resolved_label,
0122         'subject_url': resolved_url,
0123         'created_by_system': 'epicprod',
0124         'created_by_user': username_value,
0125         'ai': ai_value,
0126     }
0127     try:
0128         client = CorunClient()
0129         page = client.create_page(
0130             section=CORUN_ASSESSMENT_SECTION,
0131             title=report_title,
0132             content=assessment_text,
0133             data=page_data,
0134             tags=[
0135                 'epicprod',
0136                 'ai-assessment',
0137                 canonical_type.replace('_', '-'),
0138             ],
0139         )
0140     except CorunAPIError as exc:
0141         logger.warning(
0142             'corun AI assessment creation failed: type=%s key=%s error=%s',
0143             canonical_type, resolved['subject_key'], exc,
0144         )
0145         log_epicprod_action(
0146             'mcp', 'assessment_register',
0147             subject_type=canonical_type,
0148             subject_key=resolved['subject_key'],
0149             username=username_value,
0150             outcome='error',
0151             sublevel='normal',
0152             live_default=True,
0153             level=logging.ERROR,
0154             message=f'assessment registration failed: {exc}',
0155         )
0156         return {
0157             'success': False,
0158             'error': str(exc),
0159             'subject_type': canonical_type,
0160             'subject_key': resolved['subject_key'],
0161         }
0162 
0163     page_group_id = str(page.get('group_id') or '')
0164     if linked and page_group_id:
0165         append_corun_page_group_id(
0166             resolved['target_obj'],
0167             resolved['target_json_field'],
0168             page_group_id,
0169         )
0170     # A non-ok verdict raises the record to the live stream, so an
0171     # assessment calling for attention reaches operators without any
0172     # additional machinery (EPICPROD_ASSESSMENTS.md § Architecture).
0173     verdict = str(payload_data.get('verdict') or '').strip().lower()
0174     log_epicprod_action(
0175         'mcp', 'assessment_register',
0176         subject_type=canonical_type,
0177         subject_key=resolved['subject_key'],
0178         username=username_value,
0179         outcome='ok',
0180         sublevel='high' if verdict not in ('', 'ok') else 'normal',
0181         live_default=True,
0182         linked=linked,
0183         corun_page_group_id=page_group_id,
0184         report_title=report_title,
0185         report_path=(f'/ai/assessments/{page_group_id}/'
0186                      if page_group_id else ''),
0187         assessment_kind=str(payload_data.get('assessment_kind') or ''),
0188         narration=str(payload_data.get('narration') or '')[:600],
0189         **({'verdict': verdict} if verdict else {}),
0190     )
0191     return {
0192         'success': True,
0193         'storage': 'corun',
0194         'id': None,
0195         'corun_page_group_id': page_group_id,
0196         'corun_page_id': str(page.get('id') or ''),
0197         'subject_type': canonical_type,
0198         'subject_key': resolved['subject_key'],
0199         'subject_label': resolved_label,
0200         'subject_url': resolved_url,
0201         'username': username_value,
0202         'ai': ai_value,
0203         'created_at': page.get('created_at'),
0204         'linked': linked,
0205         'json_field': resolved.get('target_json_field') or '',
0206     }
0207 
0208 
0209 def _row_to_dict(row):
0210     data = row.data or {}
0211     return {
0212         'id': row.pk,
0213         'storage': 'legacy',
0214         'corun_page_group_id': '',
0215         'subject_type': row.subject_type,
0216         'subject_key': row.subject_key,
0217         'subject_label': row.subject_label,
0218         'subject_url': row.subject_url,
0219         'username': row.username,
0220         'ai': row.ai,
0221         'quality': data.get('quality') or '',
0222         'comment': data.get('comment') or '',
0223         'assessment': row.assessment,
0224         'data': data,
0225         'created_at': row.created_at.isoformat() if row.created_at else None,
0226     }
0227 
0228 
0229 def _corun_item_to_dict(item, page):
0230     data = page.get('data') if isinstance(page.get('data'), dict) else {}
0231     return {
0232         'id': None,
0233         'storage': 'corun',
0234         'corun_page_group_id': item.get('corun_page_group_id') or '',
0235         'subject_type': item.get('subject_type') or '',
0236         'subject_key': item.get('subject_key') or '',
0237         'subject_label': item.get('subject_label') or '',
0238         'subject_url': item.get('subject_url') or '',
0239         'username': item.get('username') or '',
0240         'ai': item.get('ai') or '',
0241         'quality': item.get('quality') or '',
0242         'comment': item.get('comment') or '',
0243         'assessment': item.get('assessment') or '',
0244         'data': data,
0245         'created_at': str(item.get('created_at') or page.get('created_at') or ''),
0246     }
0247 
0248 
0249 def _ordered_ai_content_ids(ids):
0250     if ids is None:
0251         return []
0252     if not isinstance(ids, list):
0253         raise ValueError('ids must be a list of AIContent ids')
0254 
0255     ordered_ids = []
0256     for raw in ids:
0257         try:
0258             item_id = int(raw)
0259         except (TypeError, ValueError):
0260             raise ValueError(f'invalid AIContent id: {raw!r}')
0261         if item_id <= 0:
0262             raise ValueError(f'invalid AIContent id: {raw!r}')
0263         if item_id not in ordered_ids:
0264             ordered_ids.append(item_id)
0265     return ordered_ids
0266 
0267 
0268 def _ordered_corun_page_group_ids(page_group_ids):
0269     if page_group_ids is None:
0270         return []
0271     if not isinstance(page_group_ids, list):
0272         raise ValueError('corun_page_group_ids must be a list of corun Page group ids')
0273     ordered_ids = []
0274     for raw in page_group_ids:
0275         value = str(raw or '').strip()
0276         if not value:
0277             raise ValueError(f'invalid corun Page group id: {raw!r}')
0278         if value not in ordered_ids:
0279             ordered_ids.append(value)
0280     return ordered_ids
0281 
0282 
0283 def _get_ai_content_sync(ids=None, corun_page_group_ids=None):
0284     try:
0285         ordered_ids = _ordered_ai_content_ids(ids)
0286         ordered_corun_ids = _ordered_corun_page_group_ids(corun_page_group_ids)
0287     except ValueError as exc:
0288         return {'success': False, 'error': str(exc)}
0289 
0290     if not ordered_ids and not ordered_corun_ids:
0291         return {
0292             'success': False,
0293             'error': 'ids or corun_page_group_ids must contain at least one id',
0294         }
0295 
0296     from monitor_app.models import AIContent
0297     rows = {
0298         row.pk: row
0299         for row in AIContent.objects.filter(pk__in=ordered_ids)
0300     }
0301     items = [_row_to_dict(rows[item_id]) for item_id in ordered_ids if item_id in rows]
0302     missing_ids = [item_id for item_id in ordered_ids if item_id not in rows]
0303 
0304     missing_corun_ids = []
0305     if ordered_corun_ids:
0306         if not corun_configured():
0307             return {
0308                 'success': False,
0309                 'error': 'corun-ai config CORUN_BASE_URL and CORUN_API_TOKEN must be configured',
0310             }
0311         client = CorunClient()
0312         for page_group_id in ordered_corun_ids:
0313             try:
0314                 page = client.get_page(page_group_id)
0315             except CorunAPIError:
0316                 missing_corun_ids.append(page_group_id)
0317                 continue
0318             normalized = corun_page_items([page])
0319             if normalized:
0320                 items.append(_corun_item_to_dict(normalized[0], page))
0321             else:
0322                 missing_corun_ids.append(page_group_id)
0323 
0324     return {
0325         'success': True,
0326         'count': len(items),
0327         'items': items,
0328         'missing_ids': missing_ids,
0329         'missing_corun_page_group_ids': missing_corun_ids,
0330     }
0331 
0332 
0333 @mcp.tool()
0334 async def epic_register_ai_assessment(
0335     subject_type: str,
0336     subject_key: str,
0337     assessment: str,
0338     username: str = 'mcp',
0339     ai: str = 'unknown',
0340     subject_label: str = '',
0341     subject_url: str = '',
0342     data: dict = None,
0343 ) -> dict:
0344     """
0345     Register an append-only AI assessment for an epicprod object.
0346 
0347     Known subject types are canonicalized and linked into the target object's
0348     JSON `corun_page_group_ids` pointer:
0349       - campaign_task: campaign/production task, keyed by composed name
0350       - panda_task: local PanDA-task association, keyed by JEDI task id or task name
0351       - panda_job: local production job record, keyed by pandaid
0352       - panda_queue: PanDA site/queue record, keyed by queue name; if
0353         the key is a site name, queue_name == site represents site-level content
0354       - campaign: production campaign, keyed by campaign name (e.g. 26.05.0);
0355         the natural subject for campaign-level reports and assessments
0356 
0357     Args:
0358         subject_type: Canonical subject type or alias.
0359         subject_key: Human-readable object key such as composed task name,
0360             JEDI task id, pandaid, queue name, or site name.
0361         assessment: Markdown assessment text. It is stored as a corun-ai Page.
0362         username: Human account or service account creating the assessment.
0363             Bot harnesses should pass `bot`, not a mutable bot deployment name.
0364         ai: Model or agent identifier. Bot harnesses should pass the exact
0365             model used to generate the assessment.
0366         subject_label: Optional display label override.
0367         subject_url: Optional monitor URL override.
0368         data: Optional structured metadata captured with the assessment. The
0369             server stamps `registered_via='mcp'` and
0370             `mcp_tool='epic_register_ai_assessment'` on stored metadata.
0371 
0372     Returns:
0373         Success status, corun-ai Page ids, canonical subject reference, created_at,
0374         and whether the target object JSON pointer was updated.
0375     """
0376     return await sync_to_async(_register_ai_assessment_sync)(
0377         subject_type=subject_type,
0378         subject_key=subject_key,
0379         assessment=assessment,
0380         username=username,
0381         ai=ai,
0382         subject_label=subject_label,
0383         subject_url=subject_url,
0384         data=data,
0385     )
0386 
0387 
0388 @mcp.tool()
0389 async def epic_get_ai_content(ids: list = None, corun_page_group_ids: list = None) -> dict:
0390     """
0391     Retrieve append-only epicprod AI assessment content.
0392 
0393     Detail tools that can have AI assessments return an `ai_content` block with
0394     this exact retrieval instruction:
0395 
0396         {
0397           "available": true,
0398           "count": 2,
0399           "ids": [],
0400           "corun_page_group_ids": ["..."],
0401           "retrieval": {
0402             "tool": "epic_get_ai_content",
0403             "arguments": {"corun_page_group_ids": ["..."]}
0404           }
0405         }
0406 
0407     Use the supplied `ids` and/or `corun_page_group_ids` directly. Do not
0408     reconstruct subject_type or subject_key from the parent object when a
0409     detail payload already includes this retrieval block.
0410 
0411     Args:
0412         ids: Optional list of legacy AIContent integer ids from an MCP detail payload's
0413             `ai_content.ids` or `ai_content.retrieval.arguments.ids`.
0414         corun_page_group_ids: Optional list of corun-ai Page group ids from
0415             `ai_content.corun_page_group_ids` or
0416             `ai_content.retrieval.arguments.corun_page_group_ids`.
0417 
0418     Returns:
0419         success, count, items in requested id order, and missing_ids for any
0420         ids that no longer resolve. Each item includes storage type, subject
0421         metadata, username, ai/model identifier, Markdown assessment text,
0422         structured data, and created_at.
0423     """
0424     return await sync_to_async(_get_ai_content_sync)(
0425         ids=ids,
0426         corun_page_group_ids=corun_page_group_ids,
0427     )