feat(consultation): let the jyotish agent drive skills and tools

This commit is contained in:
Jesse_Chen
2026-08-11 16:22:02 +08:00
parent 6d7a9a97be
commit 46cdc3bbf4
24 changed files with 1336 additions and 306 deletions
+231 -11
View File
@@ -4,6 +4,7 @@ import {
consultationWorkflowReceipt,
getGeneralJyotishAgent,
getJyotishAgent,
getLegacyJyotishAgent,
runConsultationWorkflow,
} from "@/mastra";
import { blocksPromptExtraction } from "@/lib/consult-safety";
@@ -18,6 +19,13 @@ import { resolveSessionLanguageModel } from "@/lib/model-catalog";
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import { streamTextResponse } from "@/lib/stream-text-response";
import { streamAgentResponse } from "@/lib/stream-agent-response";
import type { AgentExecutionReceipt, WorkflowReceipt } from "@/lib/consultation-agent-events";
import {
createConsultationAgentContext,
createConsultationRuntimeHooks,
createConsultationRuntimeState,
} from "@/mastra/consultation-tools";
import {
applyBirthTimeModeToWorkflowContext,
consultationBirthTimeModeSchema,
@@ -32,7 +40,7 @@ import {
import { z } from "zod";
export const runtime = "nodejs";
export const maxDuration = 60;
export const maxDuration = 120;
const chatRequestMetadataSchema = z.object({
requestId: z.string().uuid(),
@@ -117,6 +125,29 @@ function chinaCalendarDate(now: Date) {
return new Date(now.getTime() + 8 * 60 * 60 * 1000).toISOString().slice(0, 10);
}
type Usage = { inputTokens?: number; outputTokens?: number };
function mergeUsage(usages: Promise<Usage>[]): Promise<Usage> {
return Promise.all(usages).then((items) => items.reduce((total, item) => ({
inputTokens: (total.inputTokens ?? 0) + (item.inputTokens ?? 0),
outputTokens: (total.outputTokens ?? 0) + (item.outputTokens ?? 0),
}), {} as Usage));
}
function shouldUseAgenticRuntime(user: { id: string; app_metadata?: Record<string, unknown> }) {
const mode = process.env.CONSULTATION_AGENTIC_RUNTIME?.trim().toLowerCase() ?? "legacy";
if (mode === "enabled") return true;
if (mode !== "canary") return false;
const ids = new Set((process.env.CONSULTATION_AGENTIC_CANARY_USER_IDS ?? "")
.split(",").map((value) => value.trim()).filter(Boolean));
const roles = Array.isArray(user.app_metadata?.roles) ? user.app_metadata.roles : [];
return ids.has(user.id) || user.app_metadata?.role === "admin" || roles.includes("admin");
}
function workflowStatus(status: string | undefined): "ready" | "degraded" | "blocked" {
return status === "ready" || status === "degraded" ? status : "blocked";
}
export async function POST(request: Request) {
let supabase: Awaited<ReturnType<typeof createServerSupabaseClient>>;
let accounting: ReturnType<typeof createAdminSupabaseClient>;
@@ -228,7 +259,10 @@ export async function POST(request: Request) {
{ status: 400 },
);
}
let prepared;
type SelectedModel = NonNullable<Awaited<ReturnType<typeof resolveSessionLanguageModel>>>;
type ReservationResult = { success: boolean; credits: number | null; error_code: string | null };
type ModelSelection = Awaited<ReturnType<typeof reserveConsultationModel<SelectedModel, ReservationResult>>>;
let prepared: Awaited<ReturnType<typeof prepareConsultationRoute<ModelSelection>>>;
try {
prepared = await prepareConsultationRoute({
userId,
@@ -367,12 +401,8 @@ export async function POST(request: Request) {
rawTransformedText: string,
usage: Promise<{ inputTokens?: number; outputTokens?: number }>,
techniqueTruth: string,
workflowReceipt: {
route: string;
status: string;
preciseTiming: string;
missingLayers: readonly string[];
},
workflowReceipt: WorkflowReceipt,
agentExecutionReceipt?: AgentExecutionReceipt,
) {
try {
const reply = parseAgentReply(rawTransformedText, consultationTheme);
@@ -383,6 +413,7 @@ export async function POST(request: Request) {
suggestions: reply.suggestions,
techniqueTruth,
workflowReceipt,
...(agentExecutionReceipt ? { agentExecutionReceipt } : {}),
};
const actualUsage = await usagePayload(usage);
const completion = await retryDetachedSettlement(async () => {
@@ -414,10 +445,199 @@ export async function POST(request: Request) {
return settlement;
}
async function runAgenticConsultation(
consultationMode: ConsultationBirthTimeMode,
history: Array<{ role: "user" | "assistant"; text: string }>,
name: string,
) {
const state = createConsultationRuntimeState();
const hooks = createConsultationRuntimeHooks(state);
const usages: Promise<Usage>[] = [];
const agentStartedAt = Date.now();
let firstActivityMs = -1;
let firstTextMs = -1;
let logged = false;
const markFirstActivity = () => { if (firstActivityMs < 0) firstActivityMs = Date.now() - agentStartedAt; };
const markFirstText = () => { if (firstTextMs < 0) firstTextMs = Date.now() - agentStartedAt; };
const logRun = (finishReason: string, settlementResult: string) => {
if (logged) return;
logged = true;
console.info([
"[consult-agentic]",
`request_id=${requestId}`,
`run_id=${requestId}`,
`session_id=${sessionId}`,
`model_id=${selectedModel.id}`,
`skill_loaded=${state.jyotishSkillLoaded}`,
`skill_reference_read_count=${state.skillReferenceReadCount}`,
`consultation_tool_call_count=${state.consultationToolCallCount}`,
`consultation_tool_duration_ms=${state.consultationToolDurationMs ?? -1}`,
`time_to_first_activity_ms=${firstActivityMs}`,
`time_to_first_text_ms=${firstTextMs}`,
`total_duration_ms=${Date.now() - agentStartedAt}`,
`finish_reason=${finishReason}`,
`settlement_result=${settlementResult}`,
].join(" "));
};
const settleRun = async (action: () => Promise<void>, finishReason: string, settlementResult: string) => {
try {
await settle(action);
logRun(finishReason, settlementResult);
} catch (error) {
logRun("settlement_failed", "failed");
throw error;
}
};
const baseMessages = [
...history.map((message) => message.role === "user"
? { role: "user" as const, content: message.text }
: { role: "assistant" as const, content: message.text }),
{
role: "user" as const,
content: [
currentTimeContext(requestTime),
name ? `用户称呼:${name}` : "",
consultationMode === "general_no_birth_time"
? "当前是用户明确选择的无出生分钟一般咨询。不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。"
: "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。",
resolvedQuestion.modelQuestion,
].filter(Boolean).join("\n"),
},
];
const agentAbortSignal = AbortSignal.timeout(110_000);
const streamOptions = {
runId: requestId,
maxSteps: 6,
abortSignal: agentAbortSignal,
hooks,
};
const workflowReceipt: WorkflowReceipt = consultationMode === "general_no_birth_time"
? { route: "general-no-birth-time", status: "ready", preciseTiming: "blocked", missingLayers: ["birth-minute"] }
: { route: "pending", status: "blocked", preciseTiming: "blocked", missingLayers: [] };
if (consultationMode === "general_no_birth_time") {
state.workflowReceipt = workflowReceipt;
const agent = getGeneralJyotishAgent(selectedModel);
const result = await agent.stream(baseMessages, streamOptions);
usages.push(result.totalUsage);
const retry = async () => {
const retried = await agent.stream([
...baseMessages,
{ role: "user" as const, content: "运行合同不完整:请先调用 skill({ name: \"jyotish-vedic-astrology\" }) 加载方法,再回答问题。" },
], streamOptions);
usages.push(retried.totalUsage);
return retried.fullStream;
};
const executionReceipt = (): AgentExecutionReceipt => ({
runId: requestId,
runtime: "mastra-agentic",
skill: { name: "jyotish-vedic-astrology", loaded: state.jyotishSkillLoaded },
steps: state.steps,
workflow: workflowReceipt,
techniqueTruth: "not-applicable",
});
return streamAgentResponse({
runId: requestId,
requestId,
state,
stream: result.fullStream,
requireTool: false,
retry,
continueAfterDisconnect: true,
transformText: createBirthTimeModeOutputGuard(consultationMode, false),
toolStatus: () => "ready",
receipt: executionReceipt,
headers: { "x-jyotish-birth-time-mode": consultationMode },
onFirstActivity: markFirstActivity,
onFirstOutput: markFirstText,
onComplete: (output, agentExecutionReceipt) => settleRun(() => completeResponse(
output,
mergeUsage(usages),
"not-applicable",
workflowReceipt,
agentExecutionReceipt,
), "completed", "completed"),
onError: (error) => settleRun(
cancel,
error instanceof Error ? error.message : "failed",
"cancelled",
),
onCancel: () => settleRun(cancel, "cancelled", "cancelled"),
});
}
if (!prepared.serverChart) throw new Error("server_chart_truth_missing");
const agentContext = createConsultationAgentContext({
userId,
sessionId,
requestId,
consultationMode,
serverChart: prepared.serverChart,
abortSignal: agentAbortSignal,
state,
});
const agent = getJyotishAgent(selectedModel, agentContext);
const result = await agent.stream(baseMessages, streamOptions);
usages.push(result.totalUsage);
const retry = async () => {
const retried = await agent.stream([
...baseMessages,
{
role: "user" as const,
content: "运行合同不完整:请先加载 jyotish-vedic-astrology Skill,再调用 run-jyotish-consultation 完成服务器计算;不要在工具参数中添加出生资料。",
},
], streamOptions);
usages.push(retried.totalUsage);
return retried.fullStream;
};
const executionReceipt = (): AgentExecutionReceipt => ({
runId: requestId,
runtime: "mastra-agentic",
skill: { name: "jyotish-vedic-astrology", loaded: state.jyotishSkillLoaded },
steps: state.steps,
workflow: state.workflowReceipt ?? workflowReceipt,
techniqueTruth: state.techniqueTruth ?? "unknown",
});
return streamAgentResponse({
runId: requestId,
requestId,
state,
stream: result.fullStream,
requireTool: true,
retry,
continueAfterDisconnect: true,
transformText: (text) => createBirthTimeModeOutputGuard(
consultationMode,
state.workflowReceipt?.preciseTiming === "allowed",
)(text),
toolStatus: () => workflowStatus(state.workflowReceipt?.status),
receipt: executionReceipt,
headers: { "x-jyotish-birth-time-mode": consultationMode },
onFirstActivity: markFirstActivity,
onFirstOutput: markFirstText,
onComplete: (output, agentExecutionReceipt) => settleRun(() => completeResponse(
output,
mergeUsage(usages),
state.techniqueTruth ?? "unknown",
state.workflowReceipt ?? workflowReceipt,
agentExecutionReceipt,
), "completed", "completed"),
onError: (error) => settleRun(
cancel,
error instanceof Error ? error.message : "failed",
"cancelled",
),
onCancel: () => settleRun(cancel, "cancelled", "cancelled"),
});
}
try {
const { history } = parsed.data;
const name = prepared.serverChart?.name ?? parsed.data.name;
const consultationMode: ConsultationBirthTimeMode = prepared.consultationMode;
if (shouldUseAgenticRuntime(user)) {
return await runAgenticConsultation(consultationMode, history, name);
}
if (!shouldRunBirthChartWorkflow(consultationMode)) {
const result = await getGeneralJyotishAgent(selectedModel).stream([
{
@@ -430,12 +650,12 @@ export async function POST(request: Request) {
].filter(Boolean).join("\n"),
},
]);
const workflowReceipt = {
const workflowReceipt: WorkflowReceipt = {
route: "general-no-birth-time",
status: "ready",
preciseTiming: "blocked",
missingLayers: ["birth-minute"],
} as const;
};
const settleErrored = (emitted: boolean, output: string) => settle(
emitted
? () => completeResponse(
@@ -485,7 +705,7 @@ export async function POST(request: Request) {
);
const workflowReceipt = consultationWorkflowReceipt(workflowContext);
const result = await getJyotishAgent(selectedModel, workflowContext).stream([
const result = await getLegacyJyotishAgent(selectedModel, workflowContext).stream([
...history.map((message) => message.role === "user"
? { role: "user" as const, content: message.text }
: { role: "assistant" as const, content: message.text }),