nexo-brain 4.1.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +13 -10
- package/bin/nexo-brain.js +34 -9
- package/bin/nexo.js +29 -2
- package/package.json +1 -1
- package/src/auto_update.py +80 -15
- package/src/cli.py +110 -0
- package/src/crons/manifest.json +24 -0
- package/src/crons/sync.py +27 -11
- package/src/dashboard/app.py +48 -3
- package/src/dashboard/templates/cortex.html +86 -27
- package/src/db/__init__.py +25 -0
- package/src/db/_goal_profiles.py +376 -0
- package/src/db/_outcomes.py +800 -0
- package/src/db/_protocol.py +239 -2
- package/src/db/_reminders.py +148 -2
- package/src/db/_schema.py +109 -0
- package/src/db/_skills.py +264 -8
- package/src/doctor/providers/runtime.py +48 -1
- package/src/plugins/cortex.py +702 -0
- package/src/plugins/episodic_memory.py +15 -2
- package/src/plugins/goal_engine.py +142 -0
- package/src/plugins/impact.py +29 -0
- package/src/plugins/outcomes.py +130 -0
- package/src/plugins/protocol.py +283 -0
- package/src/plugins/skills.py +19 -1
- package/src/plugins/update.py +39 -3
- package/src/scripts/deep-sleep/apply_findings.py +96 -3
- package/src/scripts/nexo-impact-scorer.py +117 -0
- package/src/scripts/nexo-outcome-checker.py +97 -0
- package/src/scripts/nexo-synthesis.py +81 -3
- package/src/skills_runtime.py +203 -0
- package/src/tools_drive.py +329 -32
- package/src/tools_reminders.py +3 -4
- package/src/tools_reminders_crud.py +15 -0
|
@@ -31,20 +31,20 @@
|
|
|
31
31
|
<!-- Stats row -->
|
|
32
32
|
<div class="grid grid-cols-4 gap-4 mb-6">
|
|
33
33
|
<div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
|
|
34
|
-
<p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">
|
|
34
|
+
<p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Alt Evaluations</p>
|
|
35
35
|
<p class="text-2xl font-display font-bold text-slate-100" id="stat-decisions">--</p>
|
|
36
36
|
</div>
|
|
37
37
|
<div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
|
|
38
|
-
<p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">
|
|
38
|
+
<p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Accept Rate</p>
|
|
39
39
|
<p class="text-2xl font-display font-bold text-nexo-400" id="stat-confidence">--</p>
|
|
40
40
|
</div>
|
|
41
41
|
<div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
|
|
42
|
-
<p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">
|
|
42
|
+
<p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Linked Outcomes</p>
|
|
43
43
|
<p class="text-2xl font-display font-bold text-slate-100" id="stat-logs">--</p>
|
|
44
44
|
</div>
|
|
45
45
|
<div class="bg-slate-900/50 border border-slate-800/50 rounded-xl p-5 card">
|
|
46
|
-
<p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">
|
|
47
|
-
<p class="text-2xl font-display font-bold text-
|
|
46
|
+
<p class="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-1">Override Rate</p>
|
|
47
|
+
<p class="text-2xl font-display font-bold text-amber-400" id="stat-success">--</p>
|
|
48
48
|
</div>
|
|
49
49
|
</div>
|
|
50
50
|
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
<span class="text-[10px] text-slate-600 font-mono" id="decisions-count">--</span>
|
|
78
78
|
</div>
|
|
79
79
|
<div id="decisions-list" class="space-y-3 max-h-[600px] overflow-y-auto pr-1">
|
|
80
|
-
<div class="py-12 text-center text-sm text-slate-500">Loading
|
|
80
|
+
<div class="py-12 text-center text-sm text-slate-500">Loading decision support...</div>
|
|
81
81
|
</div>
|
|
82
82
|
</div>
|
|
83
83
|
</div>
|
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
<script>
|
|
95
95
|
let allLogs = [];
|
|
96
96
|
let allDecisions = [];
|
|
97
|
+
let allEvaluations = [];
|
|
97
98
|
|
|
98
99
|
const modeColors = {
|
|
99
100
|
conservative: { bg: 'bg-blue-500/15', text: 'text-blue-400', border: 'border-blue-500/30' },
|
|
@@ -156,16 +157,69 @@
|
|
|
156
157
|
}).join('');
|
|
157
158
|
}
|
|
158
159
|
|
|
159
|
-
function renderDecisions(decisions) {
|
|
160
|
+
function renderDecisions(evaluations, decisions) {
|
|
160
161
|
const container = document.getElementById('decisions-list');
|
|
161
|
-
document.getElementById('decisions-count').textContent = `${decisions.length} decisions`;
|
|
162
|
+
document.getElementById('decisions-count').textContent = `${evaluations.length} evaluations / ${decisions.length} decisions`;
|
|
162
163
|
|
|
163
|
-
if (!decisions.length) {
|
|
164
|
-
container.innerHTML = '<div class="py-12 text-center text-sm text-slate-500">No
|
|
164
|
+
if (!evaluations.length && !decisions.length) {
|
|
165
|
+
container.innerHTML = '<div class="py-12 text-center text-sm text-slate-500">No decision support recorded</div>';
|
|
165
166
|
return;
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
|
|
169
|
+
const evaluationCards = evaluations.slice(0, 25).map((item, i) => {
|
|
170
|
+
const scores = Array.isArray(item.scores) ? item.scores : JSON.parse(item.scores || '[]');
|
|
171
|
+
const top = scores[0] || null;
|
|
172
|
+
const selectedChanged = item.selection_source === 'override';
|
|
173
|
+
return `
|
|
174
|
+
<div class="bg-slate-800/30 rounded-lg p-4 hover:bg-slate-800/50 transition-colors border border-transparent hover:border-slate-700/50 animate-fade-in" style="animation-delay: ${i * 35}ms">
|
|
175
|
+
<div class="flex items-center justify-between mb-2 gap-2">
|
|
176
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
177
|
+
${item.impact_level ? `<span class="inline-flex items-center px-2 py-0.5 rounded-md text-[10px] font-semibold bg-red-500/15 text-red-300 uppercase tracking-wider">${escapeHtml(item.impact_level)}</span>` : ''}
|
|
178
|
+
${item.area ? `<span class="inline-flex items-center px-2 py-0.5 rounded-md text-[10px] font-semibold bg-nexo-500/15 text-nexo-400 uppercase tracking-wider">${escapeHtml(item.area)}</span>` : ''}
|
|
179
|
+
</div>
|
|
180
|
+
<span class="text-[10px] text-slate-600">${relativeTime(item.created_at)}</span>
|
|
181
|
+
</div>
|
|
182
|
+
<p class="text-sm text-slate-200 mb-2">${escapeHtml(item.goal || '--')}</p>
|
|
183
|
+
<div class="grid grid-cols-2 gap-2 mb-2">
|
|
184
|
+
<div class="bg-slate-900/40 rounded-md p-2">
|
|
185
|
+
<p class="text-[10px] uppercase tracking-wider text-slate-500 mb-1">Recommended</p>
|
|
186
|
+
<p class="text-xs text-emerald-400 font-semibold">${escapeHtml(item.recommended_choice || '--')}</p>
|
|
187
|
+
</div>
|
|
188
|
+
<div class="bg-slate-900/40 rounded-md p-2">
|
|
189
|
+
<p class="text-[10px] uppercase tracking-wider text-slate-500 mb-1">Selected</p>
|
|
190
|
+
<p class="text-xs ${selectedChanged ? 'text-amber-400' : 'text-slate-200'} font-semibold">${escapeHtml(item.selected_choice || '--')}</p>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
${item.recommended_reasoning ? `<p class="text-xs text-slate-400 mb-2">${escapeHtml(item.recommended_reasoning)}</p>` : ''}
|
|
194
|
+
${item.goal_profile_id ? `
|
|
195
|
+
<div class="mt-2 bg-slate-900/40 rounded px-2 py-1.5">
|
|
196
|
+
<p class="text-[10px] text-slate-500 uppercase tracking-wider font-semibold mb-0.5">Goal Profile</p>
|
|
197
|
+
<p class="text-xs text-nexo-300 font-semibold">${escapeHtml(item.goal_profile_id)}</p>
|
|
198
|
+
${Array.isArray(item.goal_profile_labels) && item.goal_profile_labels.length ? `<p class="text-[10px] text-slate-400 mt-1">${escapeHtml(item.goal_profile_labels.join(' · '))}</p>` : ''}
|
|
199
|
+
</div>
|
|
200
|
+
` : ''}
|
|
201
|
+
${item.linked_outcome_id ? `
|
|
202
|
+
<div class="mt-2 text-[10px] text-slate-500">
|
|
203
|
+
outcome #${escapeHtml(String(item.linked_outcome_id))}
|
|
204
|
+
${item.linked_outcome?.status ? `· <span class="text-slate-300">${escapeHtml(String(item.linked_outcome.status))}</span>` : ''}
|
|
205
|
+
</div>
|
|
206
|
+
` : ''}
|
|
207
|
+
${selectedChanged && item.selection_reason ? `
|
|
208
|
+
<div class="mt-2 bg-amber-500/10 rounded px-2 py-1.5">
|
|
209
|
+
<p class="text-[10px] text-amber-400 font-semibold uppercase tracking-wider mb-0.5">Override</p>
|
|
210
|
+
<p class="text-xs text-amber-200/90">${escapeHtml(item.selection_reason)}</p>
|
|
211
|
+
</div>
|
|
212
|
+
` : ''}
|
|
213
|
+
${top ? `
|
|
214
|
+
<div class="mt-2 text-[10px] text-slate-500 font-mono">
|
|
215
|
+
top=${Number(top.total_score || 0).toFixed(2)} impact=${Number(top.impact || 0).toFixed(1)} success=${Number(top.success_probability || 0).toFixed(1)} risk=${Number(top.risk_level || 0).toFixed(1)}
|
|
216
|
+
</div>
|
|
217
|
+
` : ''}
|
|
218
|
+
</div>
|
|
219
|
+
`;
|
|
220
|
+
}).join('');
|
|
221
|
+
|
|
222
|
+
const decisionCards = decisions.slice(0, 20).map((d, i) => `
|
|
169
223
|
<div class="bg-slate-800/30 rounded-lg p-4 hover:bg-slate-800/50 transition-colors border border-transparent hover:border-slate-700/50 animate-fade-in" style="animation-delay: ${i * 40}ms">
|
|
170
224
|
<div class="flex items-center justify-between mb-2">
|
|
171
225
|
<div class="flex items-center gap-2">
|
|
@@ -190,6 +244,15 @@
|
|
|
190
244
|
</div>
|
|
191
245
|
</div>
|
|
192
246
|
`).join('');
|
|
247
|
+
|
|
248
|
+
const decisionSection = decisionCards ? `
|
|
249
|
+
<div class="pt-3 mt-3 border-t border-slate-800/80">
|
|
250
|
+
<p class="text-[10px] uppercase tracking-wider text-slate-500 font-semibold mb-3">Decision Log</p>
|
|
251
|
+
<div class="space-y-3">${decisionCards}</div>
|
|
252
|
+
</div>
|
|
253
|
+
` : '';
|
|
254
|
+
|
|
255
|
+
container.innerHTML = evaluationCards + decisionSection;
|
|
193
256
|
}
|
|
194
257
|
|
|
195
258
|
function renderConfidenceChart(decisions) {
|
|
@@ -217,9 +280,10 @@
|
|
|
217
280
|
|
|
218
281
|
const filteredLogs = modeFilter ? allLogs.filter(l => l.mode === modeFilter) : allLogs;
|
|
219
282
|
const filteredDecisions = domainFilter ? allDecisions.filter(d => d.domain === domainFilter) : allDecisions;
|
|
283
|
+
const filteredEvaluations = domainFilter ? allEvaluations.filter(e => e.area === domainFilter) : allEvaluations;
|
|
220
284
|
|
|
221
285
|
renderLogs(filteredLogs);
|
|
222
|
-
renderDecisions(filteredDecisions);
|
|
286
|
+
renderDecisions(filteredEvaluations, filteredDecisions);
|
|
223
287
|
}
|
|
224
288
|
|
|
225
289
|
async function loadData() {
|
|
@@ -228,29 +292,24 @@
|
|
|
228
292
|
|
|
229
293
|
allLogs = data.cortex_logs || [];
|
|
230
294
|
allDecisions = data.decisions || [];
|
|
295
|
+
allEvaluations = data.evaluations || [];
|
|
296
|
+
const summary = data.summary || {};
|
|
231
297
|
|
|
232
298
|
// Populate domain filter
|
|
233
|
-
const domains = [...new Set(
|
|
299
|
+
const domains = [...new Set([
|
|
300
|
+
...allDecisions.map(d => d.domain).filter(Boolean),
|
|
301
|
+
...allEvaluations.map(e => e.area).filter(Boolean),
|
|
302
|
+
])];
|
|
234
303
|
const domainSelect = document.getElementById('filter-domain');
|
|
235
304
|
const currentDomain = domainSelect.value;
|
|
236
305
|
domainSelect.innerHTML = '<option value="">All Domains</option>' +
|
|
237
306
|
domains.map(d => `<option value="${escapeHtml(d)}" ${d === currentDomain ? 'selected' : ''}>${escapeHtml(d)}</option>`).join('');
|
|
238
307
|
|
|
239
308
|
// Stats
|
|
240
|
-
document.getElementById('stat-decisions').textContent = formatNumber(
|
|
241
|
-
document.getElementById('stat-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const v = typeof d.confidence === 'number' ? (d.confidence > 1 ? d.confidence : d.confidence * 100) : parseFloat(d.confidence);
|
|
245
|
-
return v;
|
|
246
|
-
});
|
|
247
|
-
const avgConf = confidences.length ? (confidences.reduce((a, b) => a + b, 0) / confidences.length) : 0;
|
|
248
|
-
document.getElementById('stat-confidence').textContent = avgConf.toFixed(0) + '%';
|
|
249
|
-
|
|
250
|
-
const outcomes = allDecisions.filter(d => d.outcome);
|
|
251
|
-
const successes = outcomes.filter(d => d.outcome === 'success' || d.outcome === 'positive' || d.outcome === 'good');
|
|
252
|
-
const successRate = outcomes.length ? ((successes.length / outcomes.length) * 100) : 0;
|
|
253
|
-
document.getElementById('stat-success').textContent = successRate.toFixed(0) + '%';
|
|
309
|
+
document.getElementById('stat-decisions').textContent = formatNumber(allEvaluations.length);
|
|
310
|
+
document.getElementById('stat-confidence').textContent = `${Number(summary.recommendation_accept_rate || 0).toFixed(0)}%`;
|
|
311
|
+
document.getElementById('stat-logs').textContent = formatNumber(summary.linked_outcomes_total || 0);
|
|
312
|
+
document.getElementById('stat-success').textContent = `${Number(summary.override_rate || 0).toFixed(0)}%`;
|
|
254
313
|
|
|
255
314
|
renderConfidenceChart(allDecisions);
|
|
256
315
|
applyFilters();
|
package/src/db/__init__.py
CHANGED
|
@@ -52,6 +52,8 @@ _personal_scripts = _load_submodule("db._personal_scripts")
|
|
|
52
52
|
_skills = _load_submodule("db._skills")
|
|
53
53
|
_hot_context = _load_submodule("db._hot_context")
|
|
54
54
|
_drive = _load_submodule("db._drive")
|
|
55
|
+
_outcomes = _load_submodule("db._outcomes")
|
|
56
|
+
_goal_profiles = _load_submodule("db._goal_profiles")
|
|
55
57
|
|
|
56
58
|
# Core: connection, constants, init, utils
|
|
57
59
|
from db._core import (
|
|
@@ -88,6 +90,7 @@ from db._reminders import (
|
|
|
88
90
|
create_followup, update_followup, complete_followup, delete_followup,
|
|
89
91
|
restore_followup, add_followup_note, get_followups, get_followup, get_followup_history,
|
|
90
92
|
find_similar_followups,
|
|
93
|
+
compute_followup_impact, score_followup, score_active_followups,
|
|
91
94
|
add_item_history, get_item_history, validate_item_read_token,
|
|
92
95
|
)
|
|
93
96
|
|
|
@@ -147,6 +150,10 @@ from db._protocol import (
|
|
|
147
150
|
create_protocol_task, get_protocol_task, close_protocol_task,
|
|
148
151
|
create_protocol_debt, resolve_protocol_debts, list_protocol_debts,
|
|
149
152
|
protocol_compliance_summary,
|
|
153
|
+
create_cortex_evaluation, get_cortex_evaluation, list_cortex_evaluations,
|
|
154
|
+
cortex_evaluation_summary,
|
|
155
|
+
latest_cortex_evaluation_for_task, task_has_cortex_evaluation,
|
|
156
|
+
override_cortex_evaluation,
|
|
150
157
|
)
|
|
151
158
|
|
|
152
159
|
# Durable workflow runtime
|
|
@@ -185,6 +192,7 @@ from db._skills import (
|
|
|
185
192
|
validate_skill_params, render_command_template, sync_skill_directories,
|
|
186
193
|
import_skill_from_directory, approve_skill, collect_scriptable_skill_candidates,
|
|
187
194
|
collect_skill_improvement_candidates, materialize_personal_skill_definition,
|
|
195
|
+
get_skill_outcome_evidence, list_skill_outcome_reviews,
|
|
188
196
|
get_skill_health_report,
|
|
189
197
|
)
|
|
190
198
|
|
|
@@ -206,6 +214,23 @@ from db._hot_context import (
|
|
|
206
214
|
resolve_hot_context,
|
|
207
215
|
)
|
|
208
216
|
|
|
217
|
+
# Outcomes
|
|
218
|
+
from db._outcomes import (
|
|
219
|
+
VALID_METRIC_SOURCES as OUTCOME_METRIC_SOURCES,
|
|
220
|
+
VALID_TARGET_OPERATORS as OUTCOME_TARGET_OPERATORS,
|
|
221
|
+
create_outcome, get_outcome, list_outcomes,
|
|
222
|
+
cancel_outcome, evaluate_outcome, pending_outcomes_due,
|
|
223
|
+
find_pending_outcomes_by_action, set_linked_outcomes_met,
|
|
224
|
+
list_outcome_pattern_candidates, capture_outcome_pattern,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Goal Engine v1
|
|
228
|
+
from db._goal_profiles import (
|
|
229
|
+
DEFAULT_GOAL_PROFILES,
|
|
230
|
+
ensure_default_goal_profiles, get_goal_profile, list_goal_profiles,
|
|
231
|
+
upsert_goal_profile, resolve_goal_profile,
|
|
232
|
+
)
|
|
233
|
+
|
|
209
234
|
|
|
210
235
|
def get_db():
|
|
211
236
|
return _module("db._core").get_db()
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
"""Goal Engine v1 — explicit optimization profiles for durable goals and decisions."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from db._core import get_db
|
|
7
|
+
from db._workflow import get_workflow_goal
|
|
8
|
+
|
|
9
|
+
VALID_SCOPE_TYPES = {"default", "area", "task_type", "goal_id"}
|
|
10
|
+
VALID_STATUSES = {"active", "disabled"}
|
|
11
|
+
WEIGHT_KEYS = ("impact", "success", "risk", "somatic")
|
|
12
|
+
DEFAULT_WEIGHTS = {
|
|
13
|
+
"impact": 0.35,
|
|
14
|
+
"success": 0.30,
|
|
15
|
+
"risk": 0.20,
|
|
16
|
+
"somatic": 0.15,
|
|
17
|
+
}
|
|
18
|
+
DEFAULT_GOAL_PROFILES = (
|
|
19
|
+
{
|
|
20
|
+
"profile_id": "default_balanced",
|
|
21
|
+
"profile_name": "Balanced default",
|
|
22
|
+
"description": "Balancea impacto, exito, riesgo y huella somatica para decisiones generales.",
|
|
23
|
+
"scope_type": "default",
|
|
24
|
+
"scope_value": "",
|
|
25
|
+
"goal_labels": ["maximise_success", "minimise_risk", "preserve_trust"],
|
|
26
|
+
"weights": DEFAULT_WEIGHTS,
|
|
27
|
+
"status": "active",
|
|
28
|
+
"source": "system",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"profile_id": "release_safety",
|
|
32
|
+
"profile_name": "Release safety",
|
|
33
|
+
"description": "Favorece decisiones reversibles y verificadas en release, deploy y cambios publicos.",
|
|
34
|
+
"scope_type": "area",
|
|
35
|
+
"scope_value": "release",
|
|
36
|
+
"goal_labels": ["minimise_risk", "preserve_trust", "maximise_success"],
|
|
37
|
+
"weights": {
|
|
38
|
+
"impact": 0.24,
|
|
39
|
+
"success": 0.28,
|
|
40
|
+
"risk": 0.30,
|
|
41
|
+
"somatic": 0.18,
|
|
42
|
+
},
|
|
43
|
+
"status": "active",
|
|
44
|
+
"source": "system",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"profile_id": "customer_trust",
|
|
48
|
+
"profile_name": "Customer trust",
|
|
49
|
+
"description": "Favorece decisiones que preservan confianza y reducen friccion con clientes.",
|
|
50
|
+
"scope_type": "area",
|
|
51
|
+
"scope_value": "customer",
|
|
52
|
+
"goal_labels": ["preserve_trust", "maximise_success", "minimise_risk"],
|
|
53
|
+
"weights": {
|
|
54
|
+
"impact": 0.25,
|
|
55
|
+
"success": 0.31,
|
|
56
|
+
"risk": 0.26,
|
|
57
|
+
"somatic": 0.18,
|
|
58
|
+
},
|
|
59
|
+
"status": "active",
|
|
60
|
+
"source": "system",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"profile_id": "ops_efficiency",
|
|
64
|
+
"profile_name": "Operations efficiency",
|
|
65
|
+
"description": "Favorece throughput operativo manteniendo riesgo contenido en ejecucion rutinaria.",
|
|
66
|
+
"scope_type": "task_type",
|
|
67
|
+
"scope_value": "execute",
|
|
68
|
+
"goal_labels": ["maximise_efficiency", "maximise_success", "minimise_risk"],
|
|
69
|
+
"weights": {
|
|
70
|
+
"impact": 0.38,
|
|
71
|
+
"success": 0.28,
|
|
72
|
+
"risk": 0.20,
|
|
73
|
+
"somatic": 0.14,
|
|
74
|
+
},
|
|
75
|
+
"status": "active",
|
|
76
|
+
"source": "system",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"profile_id": "business_growth",
|
|
80
|
+
"profile_name": "Business growth",
|
|
81
|
+
"description": "Da mas peso a impacto y exito cuando el contexto busca crecimiento o revenue.",
|
|
82
|
+
"scope_type": "area",
|
|
83
|
+
"scope_value": "business",
|
|
84
|
+
"goal_labels": ["maximise_business_impact", "maximise_success"],
|
|
85
|
+
"weights": {
|
|
86
|
+
"impact": 0.56,
|
|
87
|
+
"success": 0.22,
|
|
88
|
+
"risk": 0.14,
|
|
89
|
+
"somatic": 0.08,
|
|
90
|
+
},
|
|
91
|
+
"status": "active",
|
|
92
|
+
"source": "system",
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _parse_json(value, default):
|
|
98
|
+
if value in (None, ""):
|
|
99
|
+
return default
|
|
100
|
+
if isinstance(value, (dict, list)):
|
|
101
|
+
return value
|
|
102
|
+
try:
|
|
103
|
+
return json.loads(value)
|
|
104
|
+
except Exception:
|
|
105
|
+
return default
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _normalize_goal_labels(labels) -> list[str]:
|
|
109
|
+
parsed = _parse_json(labels, labels if isinstance(labels, list) else [])
|
|
110
|
+
if not isinstance(parsed, list):
|
|
111
|
+
return []
|
|
112
|
+
seen: set[str] = set()
|
|
113
|
+
result: list[str] = []
|
|
114
|
+
for item in parsed:
|
|
115
|
+
clean = str(item or "").strip()
|
|
116
|
+
if not clean or clean in seen:
|
|
117
|
+
continue
|
|
118
|
+
seen.add(clean)
|
|
119
|
+
result.append(clean)
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _normalize_weights(weights) -> dict:
|
|
124
|
+
parsed = _parse_json(weights, weights if isinstance(weights, dict) else {})
|
|
125
|
+
if not isinstance(parsed, dict):
|
|
126
|
+
parsed = {}
|
|
127
|
+
collected: dict[str, float] = {}
|
|
128
|
+
for key in WEIGHT_KEYS:
|
|
129
|
+
try:
|
|
130
|
+
value = float(parsed.get(key, DEFAULT_WEIGHTS[key]))
|
|
131
|
+
except (TypeError, ValueError):
|
|
132
|
+
value = DEFAULT_WEIGHTS[key]
|
|
133
|
+
collected[key] = max(0.01, value)
|
|
134
|
+
total = sum(collected.values())
|
|
135
|
+
if total <= 0:
|
|
136
|
+
collected = dict(DEFAULT_WEIGHTS)
|
|
137
|
+
total = sum(collected.values())
|
|
138
|
+
return {key: round(collected[key] / total, 4) for key in WEIGHT_KEYS}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _row_to_goal_profile(row, *, resolved_by: str = "") -> dict | None:
|
|
142
|
+
if not row:
|
|
143
|
+
return None
|
|
144
|
+
profile = dict(row)
|
|
145
|
+
profile["goal_labels"] = _normalize_goal_labels(profile.get("goal_labels"))
|
|
146
|
+
profile["weights"] = _normalize_weights(profile.get("weights"))
|
|
147
|
+
if resolved_by:
|
|
148
|
+
profile["resolved_by"] = resolved_by
|
|
149
|
+
return profile
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def ensure_default_goal_profiles() -> None:
|
|
153
|
+
conn = get_db()
|
|
154
|
+
for profile in DEFAULT_GOAL_PROFILES:
|
|
155
|
+
conn.execute(
|
|
156
|
+
"""INSERT OR IGNORE INTO goal_profiles (
|
|
157
|
+
profile_id, profile_name, description, scope_type, scope_value,
|
|
158
|
+
goal_labels, weights, status, source
|
|
159
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
160
|
+
(
|
|
161
|
+
profile["profile_id"],
|
|
162
|
+
profile["profile_name"],
|
|
163
|
+
profile["description"],
|
|
164
|
+
profile["scope_type"],
|
|
165
|
+
profile["scope_value"],
|
|
166
|
+
json.dumps(profile["goal_labels"], ensure_ascii=False),
|
|
167
|
+
json.dumps(_normalize_weights(profile["weights"]), ensure_ascii=False),
|
|
168
|
+
profile["status"],
|
|
169
|
+
profile["source"],
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
conn.commit()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def get_goal_profile(profile_id: str) -> dict | None:
|
|
176
|
+
ensure_default_goal_profiles()
|
|
177
|
+
conn = get_db()
|
|
178
|
+
row = conn.execute(
|
|
179
|
+
"SELECT * FROM goal_profiles WHERE profile_id = ?",
|
|
180
|
+
((profile_id or "").strip(),),
|
|
181
|
+
).fetchone()
|
|
182
|
+
return _row_to_goal_profile(row)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def list_goal_profiles(*, scope_type: str = "", status: str = "active", limit: int = 50) -> list[dict]:
|
|
186
|
+
ensure_default_goal_profiles()
|
|
187
|
+
conn = get_db()
|
|
188
|
+
clauses = []
|
|
189
|
+
params: list[object] = []
|
|
190
|
+
clean_scope = (scope_type or "").strip()
|
|
191
|
+
clean_status = (status or "").strip()
|
|
192
|
+
if clean_scope:
|
|
193
|
+
clauses.append("scope_type = ?")
|
|
194
|
+
params.append(clean_scope)
|
|
195
|
+
if clean_status:
|
|
196
|
+
clauses.append("status = ?")
|
|
197
|
+
params.append(clean_status)
|
|
198
|
+
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
|
|
199
|
+
rows = conn.execute(
|
|
200
|
+
f"""SELECT * FROM goal_profiles
|
|
201
|
+
{where}
|
|
202
|
+
ORDER BY
|
|
203
|
+
CASE scope_type
|
|
204
|
+
WHEN 'default' THEN 0
|
|
205
|
+
WHEN 'area' THEN 1
|
|
206
|
+
WHEN 'task_type' THEN 2
|
|
207
|
+
WHEN 'goal_id' THEN 3
|
|
208
|
+
ELSE 9
|
|
209
|
+
END,
|
|
210
|
+
profile_id ASC
|
|
211
|
+
LIMIT ?""",
|
|
212
|
+
params + [max(1, int(limit))],
|
|
213
|
+
).fetchall()
|
|
214
|
+
return [_row_to_goal_profile(row) for row in rows if row]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def upsert_goal_profile(
|
|
218
|
+
*,
|
|
219
|
+
profile_id: str,
|
|
220
|
+
profile_name: str = "",
|
|
221
|
+
description: str = "",
|
|
222
|
+
scope_type: str = "default",
|
|
223
|
+
scope_value: str = "",
|
|
224
|
+
goal_labels=None,
|
|
225
|
+
weights=None,
|
|
226
|
+
status: str = "active",
|
|
227
|
+
source: str = "manual",
|
|
228
|
+
) -> dict:
|
|
229
|
+
ensure_default_goal_profiles()
|
|
230
|
+
clean_id = (profile_id or "").strip()
|
|
231
|
+
if not clean_id:
|
|
232
|
+
raise ValueError("profile_id is required")
|
|
233
|
+
clean_scope = (scope_type or "default").strip()
|
|
234
|
+
if clean_scope not in VALID_SCOPE_TYPES:
|
|
235
|
+
raise ValueError(f"scope_type must be one of: {', '.join(sorted(VALID_SCOPE_TYPES))}")
|
|
236
|
+
clean_status = (status or "active").strip().lower()
|
|
237
|
+
if clean_status not in VALID_STATUSES:
|
|
238
|
+
raise ValueError(f"status must be one of: {', '.join(sorted(VALID_STATUSES))}")
|
|
239
|
+
|
|
240
|
+
normalized_weights = _normalize_weights(weights)
|
|
241
|
+
normalized_labels = _normalize_goal_labels(goal_labels)
|
|
242
|
+
conn = get_db()
|
|
243
|
+
existing = conn.execute(
|
|
244
|
+
"SELECT * FROM goal_profiles WHERE profile_id = ?",
|
|
245
|
+
(clean_id,),
|
|
246
|
+
).fetchone()
|
|
247
|
+
if existing:
|
|
248
|
+
current = dict(existing)
|
|
249
|
+
conn.execute(
|
|
250
|
+
"""UPDATE goal_profiles
|
|
251
|
+
SET profile_name = ?,
|
|
252
|
+
description = ?,
|
|
253
|
+
scope_type = ?,
|
|
254
|
+
scope_value = ?,
|
|
255
|
+
goal_labels = ?,
|
|
256
|
+
weights = ?,
|
|
257
|
+
status = ?,
|
|
258
|
+
source = ?,
|
|
259
|
+
updated_at = datetime('now')
|
|
260
|
+
WHERE profile_id = ?""",
|
|
261
|
+
(
|
|
262
|
+
(profile_name or current.get("profile_name") or clean_id).strip(),
|
|
263
|
+
(description or current.get("description") or "").strip(),
|
|
264
|
+
clean_scope,
|
|
265
|
+
(scope_value or current.get("scope_value") or "").strip().lower(),
|
|
266
|
+
json.dumps(normalized_labels or _normalize_goal_labels(current.get("goal_labels")), ensure_ascii=False),
|
|
267
|
+
json.dumps(normalized_weights, ensure_ascii=False),
|
|
268
|
+
clean_status,
|
|
269
|
+
(source or current.get("source") or "manual").strip(),
|
|
270
|
+
clean_id,
|
|
271
|
+
),
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
conn.execute(
|
|
275
|
+
"""INSERT INTO goal_profiles (
|
|
276
|
+
profile_id, profile_name, description, scope_type, scope_value,
|
|
277
|
+
goal_labels, weights, status, source
|
|
278
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
279
|
+
(
|
|
280
|
+
clean_id,
|
|
281
|
+
(profile_name or clean_id).strip(),
|
|
282
|
+
(description or "").strip(),
|
|
283
|
+
clean_scope,
|
|
284
|
+
(scope_value or "").strip().lower(),
|
|
285
|
+
json.dumps(normalized_labels, ensure_ascii=False),
|
|
286
|
+
json.dumps(normalized_weights, ensure_ascii=False),
|
|
287
|
+
clean_status,
|
|
288
|
+
(source or "manual").strip(),
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
conn.commit()
|
|
292
|
+
return get_goal_profile(clean_id) or {}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def resolve_goal_profile(
|
|
296
|
+
*,
|
|
297
|
+
profile_id: str = "",
|
|
298
|
+
area: str = "",
|
|
299
|
+
task_type: str = "",
|
|
300
|
+
goal_id: str = "",
|
|
301
|
+
) -> dict:
|
|
302
|
+
ensure_default_goal_profiles()
|
|
303
|
+
conn = get_db()
|
|
304
|
+
explicit_id = (profile_id or "").strip()
|
|
305
|
+
if explicit_id:
|
|
306
|
+
explicit = get_goal_profile(explicit_id)
|
|
307
|
+
if not explicit:
|
|
308
|
+
raise ValueError(f"Unknown goal profile: {explicit_id}")
|
|
309
|
+
if explicit.get("status") != "active":
|
|
310
|
+
raise ValueError(f"Goal profile {explicit_id} is not active")
|
|
311
|
+
explicit["resolved_by"] = "explicit"
|
|
312
|
+
return explicit
|
|
313
|
+
|
|
314
|
+
clean_goal_id = (goal_id or "").strip()
|
|
315
|
+
if clean_goal_id:
|
|
316
|
+
workflow_goal = get_workflow_goal(clean_goal_id)
|
|
317
|
+
if workflow_goal:
|
|
318
|
+
shared_state = workflow_goal.get("shared_state") or {}
|
|
319
|
+
shared_profile_id = str(shared_state.get("goal_profile_id", "")).strip()
|
|
320
|
+
if shared_profile_id:
|
|
321
|
+
linked = get_goal_profile(shared_profile_id)
|
|
322
|
+
if linked and linked.get("status") == "active":
|
|
323
|
+
linked["resolved_by"] = "workflow_goal.shared_state"
|
|
324
|
+
return linked
|
|
325
|
+
row = conn.execute(
|
|
326
|
+
"""SELECT * FROM goal_profiles
|
|
327
|
+
WHERE scope_type = 'goal_id' AND scope_value = ? AND status = 'active'
|
|
328
|
+
ORDER BY updated_at DESC, profile_id ASC
|
|
329
|
+
LIMIT 1""",
|
|
330
|
+
(clean_goal_id,),
|
|
331
|
+
).fetchone()
|
|
332
|
+
if row:
|
|
333
|
+
return _row_to_goal_profile(row, resolved_by="goal_id") or {}
|
|
334
|
+
|
|
335
|
+
clean_area = (area or "").strip().lower()
|
|
336
|
+
if clean_area:
|
|
337
|
+
row = conn.execute(
|
|
338
|
+
"""SELECT * FROM goal_profiles
|
|
339
|
+
WHERE scope_type = 'area' AND scope_value = ? AND status = 'active'
|
|
340
|
+
ORDER BY updated_at DESC, profile_id ASC
|
|
341
|
+
LIMIT 1""",
|
|
342
|
+
(clean_area,),
|
|
343
|
+
).fetchone()
|
|
344
|
+
if row:
|
|
345
|
+
return _row_to_goal_profile(row, resolved_by="area") or {}
|
|
346
|
+
|
|
347
|
+
clean_type = (task_type or "").strip().lower()
|
|
348
|
+
if clean_type:
|
|
349
|
+
row = conn.execute(
|
|
350
|
+
"""SELECT * FROM goal_profiles
|
|
351
|
+
WHERE scope_type = 'task_type' AND scope_value = ? AND status = 'active'
|
|
352
|
+
ORDER BY updated_at DESC, profile_id ASC
|
|
353
|
+
LIMIT 1""",
|
|
354
|
+
(clean_type,),
|
|
355
|
+
).fetchone()
|
|
356
|
+
if row:
|
|
357
|
+
return _row_to_goal_profile(row, resolved_by="task_type") or {}
|
|
358
|
+
|
|
359
|
+
row = conn.execute(
|
|
360
|
+
"""SELECT * FROM goal_profiles
|
|
361
|
+
WHERE scope_type = 'default' AND status = 'active'
|
|
362
|
+
ORDER BY updated_at DESC, profile_id ASC
|
|
363
|
+
LIMIT 1"""
|
|
364
|
+
).fetchone()
|
|
365
|
+
return _row_to_goal_profile(row, resolved_by="default") or {
|
|
366
|
+
"profile_id": "default_balanced",
|
|
367
|
+
"profile_name": "Balanced default",
|
|
368
|
+
"description": DEFAULT_GOAL_PROFILES[0]["description"],
|
|
369
|
+
"scope_type": "default",
|
|
370
|
+
"scope_value": "",
|
|
371
|
+
"goal_labels": list(DEFAULT_GOAL_PROFILES[0]["goal_labels"]),
|
|
372
|
+
"weights": dict(DEFAULT_WEIGHTS),
|
|
373
|
+
"status": "active",
|
|
374
|
+
"source": "system",
|
|
375
|
+
"resolved_by": "fallback_default",
|
|
376
|
+
}
|