Back to home page

EIC code displayed by LXR

 
 

    


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

0001 """AI subsystem data models.
0002 
0003 The AI app owns the human-in-the-loop automation machinery
0004 (AI_PROPOSALS.md): proposals and their decisions. Executors — the
0005 services that actually mutate domain state — stay in their domain apps
0006 (e.g. ``pcs.services.dataset_propagation_set``); this app proposes,
0007 reviews, and dispatches to them.
0008 """
0009 from django.db import models
0010 
0011 # Per-category ref prefix behind the short human-referencable proposal
0012 # identifier ('cp-123'), used identically on the web list and the bot/MCP
0013 # surface. Every proposal category declares its prefix here
0014 # (AI_PROPOSALS.md, "Adding a proposal category").
0015 ACTION_REF_PREFIXES = {
0016     'propagation': 'cp',  # campaign propagation
0017 }
0018 
0019 
0020 class Proposal(models.Model):
0021     """AI (or rule-based) proposal of a concrete action, pending or decided —
0022     the canonical record behind the AI proposal list (AI_PROPOSALS.md).
0023 
0024     A proposal is a frozen executable payload: the action identifier plus
0025     the exact validated arguments of the call it wants. Terminal rows are
0026     retained — the AI proposal list is where AI-proposed activity and its
0027     human decisions remain visible and queryable (per-proposer track
0028     records, approval and wrong-rates). Records the proposal targets carry a
0029     render projection in their metadata, written and cleared by the same
0030     services that write these rows; this table is the truth.
0031     """
0032     EXECUTOR_CHOICES = [('service', 'service'), ('agent_message', 'agent_message')]
0033     # proposed -> executed | denied | withdrawn | stale; undoing an
0034     # executed proposal returns it to proposed — the approve and undo
0035     # live as origin-stamped events, not as row state.
0036     # approved_pending_execution is reserved for agent_message executors.
0037     STATUS_CHOICES = [
0038         ('proposed', 'proposed'), ('approved_pending_execution',
0039         'approved_pending_execution'), ('executed', 'executed'),
0040         ('denied', 'denied'), ('withdrawn', 'withdrawn'), ('stale', 'stale'),
0041     ]
0042     # One shared review vocabulary with AI assessments, worst to best.
0043     QUALITY_CHOICES = [('wrong', 'wrong'), ('poor', 'poor'), ('ok', 'ok'),
0044                        ('good', 'good')]
0045 
0046     action = models.CharField(max_length=40)
0047     subject_type = models.CharField(max_length=40)
0048     subject_key = models.CharField(max_length=255, db_index=True)
0049     # The other side of a relation subject (e.g. the task of a
0050     # questionnaire-task match); empty for single-record subjects.
0051     counterpart_key = models.CharField(max_length=255, blank=True, default='')
0052     payload = models.JSONField(default=dict)
0053     comment = models.TextField()
0054     confidence = models.CharField(max_length=16, blank=True, default='')
0055     proposer = models.CharField(max_length=100)
0056     scan_version = models.IntegerField(default=1)
0057     batch_id = models.CharField(max_length=100, blank=True, default='',
0058                                 db_index=True)
0059     executor = models.CharField(max_length=20, choices=EXECUTOR_CHOICES,
0060                                 default='service')
0061     # Deterministic decide-time guard, e.g. {'prev_state': 'continue'}.
0062     precondition = models.JSONField(default=dict)
0063     input_hash = models.CharField(max_length=40, db_index=True)
0064     status = models.CharField(max_length=30, choices=STATUS_CHOICES,
0065                               default='proposed', db_index=True)
0066     # Optional decision quality tag ('wrong' is the one-tap miscalibration
0067     # signal; it weighs against the proposer's track record).
0068     quality = models.CharField(max_length=10, choices=QUALITY_CHOICES,
0069                                blank=True, default='')
0070     created_by = models.CharField(max_length=100)
0071     created_at = models.DateTimeField(auto_now_add=True)
0072     decided_by = models.CharField(max_length=100, blank=True, default='')
0073     decided_at = models.DateTimeField(null=True, blank=True)
0074     # AppLog row id of the origin-stamped execution event.
0075     executed_log_id = models.BigIntegerField(null=True, blank=True)
0076     # Trace of the most recent undo (the row returns to proposed; the
0077     # origin-stamped events are the canonical record).
0078     undone_by = models.CharField(max_length=100, blank=True, default='')
0079     undone_at = models.DateTimeField(null=True, blank=True)
0080     undone_log_id = models.BigIntegerField(null=True, blank=True)
0081 
0082     class Meta:
0083         db_table = 'ai_proposal'
0084         ordering = ['-created_at']
0085         indexes = [
0086             models.Index(fields=['action', 'subject_key', 'status']),
0087         ]
0088 
0089     @property
0090     def ref(self):
0091         """The short reference humans use across surfaces, e.g. 'cp-123':
0092         category prefix + row id. The prefix doubles as corroboration — a
0093         decide call whose prefix mismatches the row's category is refused."""
0094         return f"{ACTION_REF_PREFIXES.get(self.action, 'ap')}-{self.pk}"
0095 
0096     def __str__(self):
0097         return f'{self.action} {self.subject_key} [{self.status}]'