forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcandidate.py
More file actions
410 lines (331 loc) · 14.5 KB
/
Copy pathcandidate.py
File metadata and controls
410 lines (331 loc) · 14.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
"""Universal Candidate lifecycle contracts for task and workstream proposals."""
from datetime import datetime
from enum import Enum
from typing import Annotated, Any, Literal, Optional, Union
from pydantic import BaseModel, ConfigDict, Field, RootModel, model_validator
from pydantic.annotated_handlers import GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
from typing_extensions import TypeAliasType
from models.action_item import EvidenceRef, TaskChangePayload, TaskCreatePayload, TaskStatus
from models.task_intelligence import StableId, TaskWorkflowMode
class CandidateSubjectKind(str, Enum):
task = 'task'
workstream = 'workstream'
class CandidateAction(str, Enum):
create = 'create'
update = 'update'
complete = 'complete'
cancel = 'cancel'
supersede = 'supersede'
class CandidateStatus(str, Enum):
pending = 'pending'
accepted = 'accepted'
rejected = 'rejected'
expired = 'expired'
class WorkstreamProposal(BaseModel):
model_config = ConfigDict(extra='forbid')
title: str = Field(min_length=1, max_length=256)
objective: str = Field(min_length=1, max_length=2048)
anchor_task: TaskCreatePayload
class CandidateCompatibilityMetadata(BaseModel):
"""Released-client annotations retained outside the canonical task payload.
Staged-task clients historically supplied these fields for presentation and
ordering. They are carried on the Candidate envelope so the compatibility
projection remains lossless without weakening the strict task contract.
"""
model_config = ConfigDict(extra='forbid')
metadata: Optional[str] = None
category: Optional[str] = None
relevance_score: Optional[int] = Field(default=None, ge=0, le=1000)
class CandidateEnvelope(BaseModel):
model_config = ConfigDict(extra='forbid')
capture_confidence: float = Field(ge=0, le=1)
ownership_confidence: float = Field(ge=0, le=1)
goal_id: Optional[StableId] = None
workstream_id: Optional[StableId] = None
evidence_refs: list[EvidenceRef] = Field(min_length=1)
source_surface: str = Field(min_length=1, max_length=64)
compatibility: Optional[CandidateCompatibilityMetadata] = None
class TaskCreateCandidate(CandidateEnvelope):
subject_kind: Literal[CandidateSubjectKind.task] = CandidateSubjectKind.task
proposed_action: Literal[CandidateAction.create] = CandidateAction.create
task_change: TaskCreatePayload
class TaskMutationCandidate(CandidateEnvelope):
subject_kind: Literal[CandidateSubjectKind.task] = CandidateSubjectKind.task
task_id: StableId
task_change: TaskChangePayload
@model_validator(mode='after')
def validate_action_specific_change(self):
proposed_action = CandidateAction(getattr(self, 'proposed_action'))
required_status = {
CandidateAction.complete: TaskStatus.completed,
CandidateAction.cancel: TaskStatus.cancelled,
CandidateAction.supersede: TaskStatus.superseded,
}.get(proposed_action)
if required_status is not None and self.task_change.status != required_status:
raise ValueError(f'{proposed_action.value} Candidate requires status={required_status.value}')
if proposed_action == CandidateAction.supersede and self.task_change.superseded_by is None:
raise ValueError('supersede Candidate requires superseded_by')
return self
class TaskUpdateCandidate(TaskMutationCandidate):
proposed_action: Literal[CandidateAction.update] = CandidateAction.update
class TaskCompleteCandidate(TaskMutationCandidate):
proposed_action: Literal[CandidateAction.complete] = CandidateAction.complete
class TaskCancelCandidate(TaskMutationCandidate):
proposed_action: Literal[CandidateAction.cancel] = CandidateAction.cancel
class TaskSupersedeCandidate(TaskMutationCandidate):
proposed_action: Literal[CandidateAction.supersede] = CandidateAction.supersede
class WorkstreamCreateCandidate(CandidateEnvelope):
subject_kind: Literal[CandidateSubjectKind.workstream] = CandidateSubjectKind.workstream
proposed_action: Literal[CandidateAction.create] = CandidateAction.create
workstream_proposal: WorkstreamProposal
TaskCandidate = TypeAliasType(
'TaskCandidate',
Annotated[
Union[
TaskCreateCandidate,
TaskUpdateCandidate,
TaskCompleteCandidate,
TaskCancelCandidate,
TaskSupersedeCandidate,
],
Field(discriminator='proposed_action'),
],
)
CandidateCreateUnion = Annotated[
Union[TaskCandidate, WorkstreamCreateCandidate],
Field(discriminator='subject_kind'),
]
class CandidateCreate(RootModel[CandidateCreateUnion]):
"""Strict request union; each wire arm contains only fields valid for that action."""
@classmethod
def __get_pydantic_json_schema__(cls, core_schema: Any, handler: GetJsonSchemaHandler) -> JsonSchemaValue:
schema = handler(core_schema)
if 'anyOf' in schema:
schema['oneOf'] = schema.pop('anyOf')
return schema
@property
def subject_kind(self) -> CandidateSubjectKind:
return CandidateSubjectKind(self.root.subject_kind)
@property
def proposed_action(self) -> CandidateAction:
return CandidateAction(self.root.proposed_action)
@property
def task_id(self) -> Optional[StableId]:
return getattr(self.root, 'task_id', None)
@property
def task_change(self) -> Optional[TaskCreatePayload | TaskChangePayload]:
return getattr(self.root, 'task_change', None)
@property
def workstream_proposal(self) -> Optional[WorkstreamProposal]:
return getattr(self.root, 'workstream_proposal', None)
@property
def capture_confidence(self) -> float:
return self.root.capture_confidence
@property
def ownership_confidence(self) -> float:
return self.root.ownership_confidence
@property
def goal_id(self) -> Optional[StableId]:
return self.root.goal_id
@property
def workstream_id(self) -> Optional[StableId]:
return self.root.workstream_id
@property
def evidence_refs(self) -> list[EvidenceRef]:
return self.root.evidence_refs
@property
def source_surface(self) -> str:
return self.root.source_surface
@property
def compatibility(self) -> Optional[CandidateCompatibilityMetadata]:
return self.root.compatibility
class CandidateRecord(BaseModel):
model_config = ConfigDict(extra='forbid')
subject_kind: CandidateSubjectKind
proposed_action: CandidateAction
task_id: Optional[StableId] = None
task_change: Optional[TaskCreatePayload | TaskChangePayload] = None
workstream_proposal: Optional[WorkstreamProposal] = None
capture_confidence: float = Field(ge=0, le=1)
ownership_confidence: float = Field(ge=0, le=1)
goal_id: Optional[StableId] = None
workstream_id: Optional[StableId] = None
evidence_refs: list[EvidenceRef] = Field(min_length=1)
source_surface: str = Field(min_length=1, max_length=64)
compatibility: Optional[CandidateCompatibilityMetadata] = None
candidate_id: StableId
status: CandidateStatus = CandidateStatus.pending
account_generation: int = Field(ge=0)
idempotency_key: StableId
resolution_reason: Optional[str] = Field(default=None, max_length=64)
result_task_id: Optional[StableId] = None
result_workstream_id: Optional[StableId] = None
created_at: datetime
resolved_at: Optional[datetime] = None
# A pending suggestion the user never acts on dies on its own. Cleared on
# resolution: an accepted/rejected Candidate is the audit link to its task
# and must outlive the suggestion window.
expires_at: Optional[datetime] = None
@classmethod
def __get_pydantic_json_schema__(cls, core_schema: Any, handler: GetJsonSchemaHandler) -> JsonSchemaValue:
schema = handler(core_schema)
properties = schema['properties']
stable_id_schema = properties['task_id']['anyOf'][0]
task_create_ref = properties['task_change']['anyOf'][0]
task_change_ref = properties['task_change']['anyOf'][1]
workstream_ref = properties['workstream_proposal']['anyOf'][0]
def task_change_schema(*, status: Optional[TaskStatus] = None, require_superseded_by: bool = False):
constraints: dict[str, Any] = {}
if status is not None:
constraints.setdefault('properties', {})['status'] = {'const': status.value}
constraints.setdefault('required', []).append('status')
if require_superseded_by:
constraints.setdefault('required', []).append('superseded_by')
return {'allOf': [task_change_ref, constraints]} if constraints else task_change_ref
def task_arm(action: CandidateAction, change: dict[str, Any]):
return {
'properties': {
'subject_kind': {'const': CandidateSubjectKind.task.value},
'proposed_action': {'const': action.value},
'task_id': stable_id_schema if action != CandidateAction.create else {'type': 'null'},
'task_change': change,
'workstream_proposal': {'type': 'null'},
},
'required': ['task_change'] + (['task_id'] if action != CandidateAction.create else []),
}
schema['oneOf'] = [
task_arm(CandidateAction.create, task_create_ref),
task_arm(CandidateAction.update, task_change_schema()),
task_arm(CandidateAction.complete, task_change_schema(status=TaskStatus.completed)),
task_arm(CandidateAction.cancel, task_change_schema(status=TaskStatus.cancelled)),
task_arm(
CandidateAction.supersede,
task_change_schema(status=TaskStatus.superseded, require_superseded_by=True),
),
{
'properties': {
'subject_kind': {'const': CandidateSubjectKind.workstream.value},
'proposed_action': {'const': CandidateAction.create.value},
'task_id': {'type': 'null'},
'task_change': {'type': 'null'},
'workstream_proposal': workstream_ref,
},
'required': ['workstream_proposal'],
},
]
return schema
@model_validator(mode='before')
@classmethod
def validate_proposal_shape(cls, value: Any):
if not isinstance(value, dict):
return value
record_fields = {
'candidate_id',
'status',
'account_generation',
'idempotency_key',
'resolution_reason',
'result_task_id',
'result_workstream_id',
'created_at',
'resolved_at',
'expires_at',
}
proposal = CandidateCreate.model_validate(
{key: item for key, item in value.items() if key not in record_fields and item is not None}
)
normalized = dict(value)
normalized['subject_kind'] = proposal.subject_kind
normalized['proposed_action'] = proposal.proposed_action
normalized['task_id'] = proposal.task_id
normalized['task_change'] = proposal.task_change
normalized['workstream_proposal'] = proposal.workstream_proposal
return normalized
@model_validator(mode='after')
def validate_resolution(self):
if self.status == CandidateStatus.pending:
if self.resolved_at is not None or self.resolution_reason is not None:
raise ValueError('pending Candidate cannot have resolution metadata')
elif self.resolved_at is None:
raise ValueError('resolved Candidate requires resolved_at')
if self.status == CandidateStatus.accepted:
if self.subject_kind == CandidateSubjectKind.task and self.result_task_id is None:
raise ValueError('accepted task Candidate requires result_task_id')
if self.subject_kind == CandidateSubjectKind.workstream and self.result_workstream_id is None:
raise ValueError('accepted workstream Candidate requires result_workstream_id')
return self
def as_proposal(self) -> CandidateCreate:
record_fields = {
'candidate_id',
'status',
'account_generation',
'idempotency_key',
'resolution_reason',
'result_task_id',
'result_workstream_id',
'created_at',
'resolved_at',
'expires_at',
}
return CandidateCreate.model_validate(
{
key: value
for key, value in self.model_dump(mode='python').items()
if key not in record_fields and value is not None
}
)
@classmethod
def from_storage(cls, value: dict[str, Any]) -> 'CandidateRecord':
return cls.model_validate(value)
class CandidateListResponse(BaseModel):
candidates: list[CandidateRecord]
has_more: bool = False
class CandidateResolutionRequest(BaseModel):
model_config = ConfigDict(extra='forbid')
reason: Optional[str] = Field(default=None, max_length=64)
class CandidateResolutionReceipt(BaseModel):
model_config = ConfigDict(extra='forbid', frozen=True)
candidate_id: StableId
status: CandidateStatus
receipt_id: StableId
task_id: Optional[StableId] = None
workstream_id: Optional[StableId] = None
newly_resolved: bool
resolved_at: datetime
class CandidateMigrationReport(BaseModel):
model_config = ConfigDict(extra='forbid', frozen=True)
workflow_mode: TaskWorkflowMode
account_generation: int = Field(ge=0)
dry_run: bool
scanned: int = Field(ge=0)
created: int = Field(ge=0)
reconciled: int = Field(ge=0)
unchanged: int = Field(ge=0)
failed: int = Field(ge=0)
failure_ids: list[StableId]
checkpoint: Optional[StableId] = None
class CandidateMigrationRequest(BaseModel):
model_config = ConfigDict(extra='forbid')
after_id: Optional[StableId] = None
limit: int = Field(default=500, ge=1, le=500)
__all__ = [
'CandidateAction',
'CandidateCompatibilityMetadata',
'CandidateCreate',
'CandidateListResponse',
'CandidateMigrationReport',
'CandidateMigrationRequest',
'CandidateRecord',
'CandidateResolutionReceipt',
'CandidateResolutionRequest',
'CandidateStatus',
'CandidateSubjectKind',
'TaskCancelCandidate',
'TaskCompleteCandidate',
'TaskCreateCandidate',
'TaskSupersedeCandidate',
'TaskUpdateCandidate',
'WorkstreamCreateCandidate',
'WorkstreamProposal',
]