feat(serving): replace MLflow run logging with native trace spans
Convert ml-serving from isolated MLflow runs to nested traces using mlflow.start_span_no_context(). The recommend endpoint now emits a full span tree: recommend (CHAIN) → build_context (TOOL), agent:* (AGENT) ×N, llm_orchestrator (LLM). Compute and infer endpoints each emit a single span. Supporting changes: - mlflow-skinny>=3.1.0 added to requirements - MLflow configured with --serve-artifacts + mlflow-artifacts:/ default root for cross-container artifact proxy (spans now persist from ml-serving) - --allowed-hosts extended to include mlflow:5000 (SDK includes port in Host) - science_destiny slider wired through prompts.py and recommend endpoint - Config page exposes science/destiny slider (0=data-driven, 100=intuitive) - Tip page shows rationale inline on tap Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { getVapidPublicKey, subscribePush } from '@/lib/api';
|
||||
import { getVapidPublicKey, subscribePush, getOrchestatorPrefs, updateOrchestratorPref } from '@/lib/api';
|
||||
|
||||
type PushState = 'idle' | 'subscribed' | 'denied';
|
||||
|
||||
export default function ConfigPage() {
|
||||
const [pushState, setPushState] = useState<PushState>('idle');
|
||||
const [scienceDestiny, setScienceDestiny] = useState(50);
|
||||
const [prefSaving, setPrefSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getOrchestatorPrefs().then((prefs) => {
|
||||
if (typeof prefs.science_destiny === 'number') setScienceDestiny(prefs.science_destiny);
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const handleScienceDestinyChange = useCallback(async (value: number) => {
|
||||
setScienceDestiny(value);
|
||||
setPrefSaving(true);
|
||||
try { await updateOrchestratorPref('science_destiny', value); }
|
||||
finally { setPrefSaving(false); }
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof Notification !== 'undefined') {
|
||||
@@ -87,6 +102,41 @@ export default function ConfigPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Tip style */}
|
||||
<section style={{ marginBottom: '2.5rem' }}>
|
||||
<h3 style={{ fontSize: '0.75rem', letterSpacing: '0.12em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.35)', marginBottom: '1rem', fontWeight: 400 }}>
|
||||
Tip style
|
||||
</h3>
|
||||
<div style={{
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: '0.75rem',
|
||||
padding: '1.25rem 1.5rem',
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '0.875rem' }}>
|
||||
<span style={{ fontSize: '0.85rem', fontWeight: 500 }}>Science</span>
|
||||
<span style={{ fontSize: '0.7rem', color: 'rgba(255,255,255,0.25)' }}>
|
||||
{prefSaving ? 'saving…' : scienceDestiny === 50 ? 'balanced' : scienceDestiny < 50 ? 'data-driven' : 'intuitive'}
|
||||
</span>
|
||||
<span style={{ fontSize: '0.85rem', fontWeight: 500 }}>Destiny</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
value={scienceDestiny}
|
||||
onChange={(e) => handleScienceDestinyChange(Number(e.target.value))}
|
||||
style={{ width: '100%', accentColor: 'var(--white)', cursor: 'pointer' }}
|
||||
/>
|
||||
<div style={{ color: 'rgba(255,255,255,0.3)', fontSize: '0.7rem', marginTop: '0.75rem' }}>
|
||||
{scienceDestiny < 30
|
||||
? 'Tips lean on patterns and data'
|
||||
: scienceDestiny > 70
|
||||
? 'Tips lean on intuition and meaning'
|
||||
: 'Tips balance logic and intuition'}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Integrations */}
|
||||
<section>
|
||||
<h3 style={{ fontSize: '0.75rem', letterSpacing: '0.12em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.35)', marginBottom: '1rem', fontWeight: 400 }}>
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function TipPage() {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const holdTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [pressed, setPressed] = useState(false);
|
||||
const [showReasoning, setShowReasoning] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (state === 'loading' || state === 'done') {
|
||||
@@ -49,6 +50,7 @@ export default function TipPage() {
|
||||
return;
|
||||
}
|
||||
setTip(rec.tip);
|
||||
setShowReasoning(false);
|
||||
setState('tip');
|
||||
} catch (err: any) {
|
||||
console.error('[tip] loadTip error', err?.status, err?.message);
|
||||
@@ -235,6 +237,81 @@ export default function TipPage() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Reasoning overlay */}
|
||||
{showReasoning && tip?.rationale && (
|
||||
<div
|
||||
onClick={(e) => { e.stopPropagation(); setShowReasoning(false); }}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'center',
|
||||
zIndex: 20,
|
||||
padding: '0 0 5rem',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
background: 'rgba(20,20,20,0.96)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
borderRadius: '0.875rem',
|
||||
padding: '1.25rem 1.5rem',
|
||||
maxWidth: '360px',
|
||||
width: 'calc(100% - 3rem)',
|
||||
}}
|
||||
>
|
||||
<p style={{
|
||||
margin: 0,
|
||||
fontSize: '0.7rem',
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
color: 'rgba(255,255,255,0.3)',
|
||||
marginBottom: '0.625rem',
|
||||
}}>
|
||||
Why this tip
|
||||
</p>
|
||||
<p style={{
|
||||
margin: 0,
|
||||
fontSize: '0.9rem',
|
||||
fontWeight: 300,
|
||||
lineHeight: 1.5,
|
||||
color: 'rgba(255,255,255,0.75)',
|
||||
}}>
|
||||
{tip.rationale}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ? button — bottom left, shows reasoning */}
|
||||
{(state === 'tip' || state === 'actions') && tip?.rationale && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowReasoning((v) => !v); }}
|
||||
aria-label="Why this tip"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: '1.5rem',
|
||||
left: '1.5rem',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: showReasoning ? 'rgba(255,255,255,0.5)' : 'rgba(255,255,255,0.15)',
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 400,
|
||||
lineHeight: 1,
|
||||
padding: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 10,
|
||||
transition: 'color 0.2s ease',
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
?
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Settings gear — bottom right */}
|
||||
<a
|
||||
href="/config"
|
||||
|
||||
@@ -81,3 +81,15 @@ export async function unsubscribePush(endpoint: string) {
|
||||
body: JSON.stringify({ endpoint }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getOrchestatorPrefs(): Promise<Record<string, unknown>> {
|
||||
const data = await apiFetch<{ prefs: Record<string, Record<string, unknown>> }>('/profile');
|
||||
return data.prefs?.orchestrator ?? {};
|
||||
}
|
||||
|
||||
export async function updateOrchestratorPref(key: string, value: unknown) {
|
||||
return apiFetch<{ ok: boolean }>('/profile/prefs/orchestrator', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ [key]: value }),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user