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:
@@ -35,7 +35,7 @@ const AGENT_C = { ...MANIFEST_DEFAULTS, id: 'agent-c', required_consents: ['data
|
||||
beforeAll(async () => {
|
||||
await testDb.insert(users).values({
|
||||
id: 'u1', email: 'u@test.com', name: null, image: null, role: 'user',
|
||||
consentGiven: false, createdAt: NOW,
|
||||
createdAt: NOW,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ describe('POST /recommend integration', () => {
|
||||
});
|
||||
|
||||
// Intercept the /recommend body to inspect what agent_outputs were sent
|
||||
const origFetch = globalThis.fetch as ReturnType<typeof vi.fn>;
|
||||
const origFetch = globalThis.fetch as unknown as (url: string, init?: RequestInit) => Promise<Response>;
|
||||
const wrappedFetch = vi.fn().mockImplementation(async (url: string, init?: RequestInit) => {
|
||||
if (String(url).includes('/recommend') && init?.body) {
|
||||
const body = JSON.parse(init.body as string);
|
||||
|
||||
@@ -166,7 +166,7 @@ export async function computeAndStore(userId: string, agentId: string): Promise<
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_id: userId, tasks, profile, feedback_history: feedbackHistory, agent_prefs: agentPrefs }),
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
});
|
||||
|
||||
if (!mlResp.ok) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type Router as ExpressRouter, Router, Response } from 'express';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { logger } from '../logger.js';
|
||||
import { db } from '../db/index.js';
|
||||
import { integrationTokens, tipFeedback, tipViews, tipScores } from '../db/schema.js';
|
||||
import { integrationTokens, tipFeedback, tipViews, tipScores, userPreferences } from '../db/schema.js';
|
||||
import { eq, and, desc } from 'drizzle-orm';
|
||||
import { requireAuth, AuthenticatedRequest } from '../middleware/session.js';
|
||||
import { config } from '../config.js';
|
||||
@@ -52,6 +52,16 @@ interface OrchestratorResult {
|
||||
agentIds: string[];
|
||||
}
|
||||
|
||||
async function loadOrchestratorPref<T>(userId: string, key: string): Promise<T | undefined> {
|
||||
const rows = await db
|
||||
.select({ valueJson: userPreferences.valueJson })
|
||||
.from(userPreferences)
|
||||
.where(and(eq(userPreferences.userId, userId), eq(userPreferences.scope, 'orchestrator'), eq(userPreferences.key, key)))
|
||||
.limit(1);
|
||||
if (!rows.length) return undefined;
|
||||
try { return JSON.parse(rows[0].valueJson) as T; } catch { return undefined; }
|
||||
}
|
||||
|
||||
async function fetchOrchestratorTip(
|
||||
userId: string,
|
||||
signals: Signal[],
|
||||
@@ -59,9 +69,10 @@ async function fetchOrchestratorTip(
|
||||
dayOfWeek: number,
|
||||
traceparent?: string,
|
||||
): Promise<OrchestratorResult | null> {
|
||||
const [allAgentRows, eligibleIds] = await Promise.all([
|
||||
const [allAgentRows, eligibleIds, scienceDestiny] = await Promise.all([
|
||||
getActiveAgentOutputs(userId),
|
||||
getEligibleAgentIds(userId),
|
||||
loadOrchestratorPref<number>(userId, 'science_destiny'),
|
||||
]);
|
||||
const agentOutputs = allAgentRows
|
||||
.filter((r) => eligibleIds.has(r.agentId))
|
||||
@@ -78,7 +89,7 @@ async function fetchOrchestratorTip(
|
||||
const res = await fetch(`${config.ML_SERVING_URL}/recommend`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...(traceparent ? { traceparent } : {}) },
|
||||
body: JSON.stringify({ user_id: userId, agent_outputs: agentOutputs, tasks, hour_of_day: hour, day_of_week: dayOfWeek }),
|
||||
body: JSON.stringify({ user_id: userId, agent_outputs: agentOutputs, tasks, hour_of_day: hour, day_of_week: dayOfWeek, science_destiny: scienceDestiny ?? 50 }),
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
|
||||
@@ -68,14 +68,13 @@ async function runCycle(agentIds: string[]): Promise<void> {
|
||||
let failed = 0;
|
||||
|
||||
for (const userId of userIds) {
|
||||
const results = await Promise.allSettled(
|
||||
agentIds.map((agentId) => computeAndStore(userId, agentId)),
|
||||
);
|
||||
for (const r of results) {
|
||||
if (r.status === 'fulfilled') ok++;
|
||||
else {
|
||||
for (const agentId of agentIds) {
|
||||
try {
|
||||
await computeAndStore(userId, agentId);
|
||||
ok++;
|
||||
} catch (err: any) {
|
||||
failed++;
|
||||
logger.error({ err: r.reason, userId }, 'agent-scheduler: compute error');
|
||||
logger.error({ err, userId, agentId }, 'agent-scheduler: compute error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user