forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgoal.py
More file actions
283 lines (226 loc) · 8.99 KB
/
Copy pathgoal.py
File metadata and controls
283 lines (226 loc) · 8.99 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
"""Canonical goal contracts with released-client compatibility fields."""
from datetime import datetime
from enum import Enum
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from models.action_item import EvidenceRef
from models.task_intelligence import StableId
class GoalType(str, Enum):
boolean = 'boolean'
scale = 'scale'
numeric = 'numeric'
class GoalStatus(str, Enum):
background = 'background'
focused = 'focused'
paused = 'paused'
achieved = 'achieved'
abandoned = 'abandoned'
class GoalSource(str, Enum):
user = 'user'
ai_suggested = 'ai_suggested'
imported = 'imported'
class GoalRelationshipDisposition(str, Enum):
retain = 'retain'
detach = 'detach'
class GoalMetric(BaseModel):
model_config = ConfigDict(extra='forbid')
type: GoalType
current: float
target: float
min: Optional[float] = None
max: Optional[float] = None
unit: Optional[str] = Field(default=None, max_length=64)
@model_validator(mode='after')
def validate_bounds(self):
if self.min is not None and self.max is not None and self.min > self.max:
raise ValueError('metric min must not exceed max')
return self
class GoalCreate(BaseModel):
"""Canonical create shape with explicit compatibility for released request fields."""
model_config = ConfigDict(extra='forbid')
title: str = Field(min_length=1, max_length=500)
desired_outcome: Optional[str] = Field(default=None, max_length=2000)
why_it_matters: Optional[str] = Field(default=None, max_length=2000)
success_criteria: list[str] = Field(default_factory=list, max_length=20)
horizon_at: Optional[datetime] = None
status: GoalStatus = GoalStatus.background
metric: Optional[GoalMetric] = None
source: GoalSource = GoalSource.user
# Released request compatibility.
goal_type: Optional[GoalType] = None
target_value: Optional[float] = None
current_value: Optional[float] = None
min_value: Optional[float] = None
max_value: Optional[float] = None
unit: Optional[str] = Field(default=None, max_length=64)
@model_validator(mode='before')
@classmethod
def normalize_legacy_description(cls, value: object) -> object:
"""Promote the released `description` field without overriding its canonical replacement."""
if not isinstance(value, dict) or 'description' not in value:
return value
normalized = dict(value)
description = normalized.pop('description')
if normalized.get('desired_outcome') is None:
normalized['desired_outcome'] = description
return normalized
@field_validator('source', mode='before')
@classmethod
def normalize_legacy_source(cls, value: object) -> object:
"""Normalize source values emitted by released desktop clients."""
if not isinstance(value, str):
return value
legacy_sources = {
'ai': GoalSource.ai_suggested,
'onboarding_step_flow': GoalSource.user,
'onboarding_typed': GoalSource.user,
'onboarding_selected': GoalSource.user,
}
return legacy_sources.get(value, value)
@model_validator(mode='after')
def normalize_legacy_metric(self):
self.title = self.title.strip()
if not self.title:
raise ValueError('title cannot be blank')
if self.desired_outcome is None:
self.desired_outcome = self.title
if self.status == GoalStatus.focused:
raise ValueError('create the goal first, then focus it explicitly')
self.success_criteria = [criterion.strip() for criterion in self.success_criteria if criterion.strip()]
if self.metric is None and (self.target_value is not None or self.goal_type is not None):
self.metric = GoalMetric(
type=self.goal_type or GoalType.scale,
current=self.current_value if self.current_value is not None else 0,
target=self.target_value if self.target_value is not None else 0,
min=self.min_value,
max=self.max_value,
unit=self.unit,
)
return self
class GoalUpdate(BaseModel):
model_config = ConfigDict(extra='forbid')
title: Optional[str] = Field(default=None, min_length=1, max_length=500)
desired_outcome: Optional[str] = Field(default=None, max_length=2000)
why_it_matters: Optional[str] = Field(default=None, max_length=2000)
success_criteria: Optional[list[str]] = Field(default=None, max_length=20)
horizon_at: Optional[datetime] = None
metric: Optional[GoalMetric] = None
clear_metric: bool = False
# Released request compatibility.
target_value: Optional[float] = None
current_value: Optional[float] = None
min_value: Optional[float] = None
max_value: Optional[float] = None
unit: Optional[str] = Field(default=None, max_length=64)
@model_validator(mode='after')
def protect_required_fields(self):
for field_name in ('title', 'desired_outcome', 'success_criteria'):
if field_name in self.model_fields_set and getattr(self, field_name) is None:
raise ValueError(f'{field_name} cannot be null')
for field_name in ('target_value', 'current_value'):
if field_name in self.model_fields_set and getattr(self, field_name) is None:
raise ValueError(f'{field_name} cannot be null; use clear_metric to remove the metric')
if self.title is not None:
self.title = self.title.strip()
if not self.title:
raise ValueError('title cannot be blank')
if self.desired_outcome is not None:
self.desired_outcome = self.desired_outcome.strip()
if not self.desired_outcome:
raise ValueError('desired_outcome cannot be blank')
return self
class GoalFocusRequest(BaseModel):
model_config = ConfigDict(extra='forbid')
replacement_goal_id: Optional[StableId] = None
focus_rank: Optional[int] = Field(default=None, ge=0, le=4)
class GoalLifecycleRequest(BaseModel):
model_config = ConfigDict(extra='forbid')
status: GoalStatus
relationship_disposition: GoalRelationshipDisposition
@model_validator(mode='after')
def validate_terminal_status(self):
if self.status not in {GoalStatus.paused, GoalStatus.achieved, GoalStatus.abandoned}:
raise ValueError('goal lifecycle transition must pause or end the goal')
return self
class GoalProgressEventKind(str, Enum):
evidence = 'evidence'
metric_update = 'metric_update'
milestone = 'milestone'
status_change = 'status_change'
class GoalProgressEventCreate(BaseModel):
model_config = ConfigDict(extra='forbid')
kind: GoalProgressEventKind
summary: str = Field(min_length=1, max_length=1000)
evidence_refs: list[EvidenceRef] = Field(default_factory=list, max_length=50)
metric: Optional[GoalMetric] = None
class GoalProgressEvent(BaseModel):
model_config = ConfigDict(extra='forbid')
event_id: StableId
goal_id: StableId
sequence: int = Field(ge=1)
kind: GoalProgressEventKind
summary: str = Field(min_length=1, max_length=1000)
evidence_refs: list[EvidenceRef] = Field(default_factory=list, max_length=50)
metric: Optional[GoalMetric] = None
created_at: datetime
class GoalResponse(BaseModel):
"""Canonical response plus non-null aliases required by released clients."""
id: StableId
goal_id: StableId
title: str
desired_outcome: str
why_it_matters: Optional[str] = None
success_criteria: list[str] = Field(default_factory=list)
horizon_at: Optional[datetime] = None
status: GoalStatus
focus_rank: Optional[int] = None
metric: Optional[GoalMetric] = None
source: GoalSource
created_at: datetime
updated_at: datetime
ended_at: Optional[datetime] = None
latest_progress_sequence: int = 0
# Released response compatibility.
goal_type: str
target_value: float
current_value: float
min_value: float
max_value: float
unit: Optional[str] = None
is_active: bool
advice: Optional[str] = None
class GoalHistoryEntryResponse(BaseModel):
date: str
value: float
recorded_at: datetime
class GoalDeleteResponse(BaseModel):
success: bool
deleted_id: str
class GoalSuggestionResponse(BaseModel):
suggested_title: str
suggested_type: str
suggested_target: float
suggested_min: float = 0
suggested_max: float = 10
reasoning: str
class AdviceResponse(BaseModel):
advice: str
__all__ = [
'AdviceResponse',
'GoalCreate',
'GoalDeleteResponse',
'GoalFocusRequest',
'GoalHistoryEntryResponse',
'GoalLifecycleRequest',
'GoalMetric',
'GoalProgressEvent',
'GoalProgressEventCreate',
'GoalProgressEventKind',
'GoalRelationshipDisposition',
'GoalResponse',
'GoalSource',
'GoalStatus',
'GoalSuggestionResponse',
'GoalType',
'GoalUpdate',
]