feat: add "Multi-Turn Chat" learning recipe with structured conversation outputs
- Introduced "Multi-Turn Chat" recipe to generate structured user-assistant conversations with domain/topic-based goals and constraints. - Added `conversation.json` with model configuration, sampling strategies, and LLM prompts. - Updated UI nodes, layout, and graph rendering logic to support new recipe. - Enhanced `recipe-studio` fit view logic to improve editor layout responsiveness.
This commit is contained in:
parent
dec6b4b224
commit
ba000dc0f2
5 changed files with 278 additions and 4 deletions
|
|
@ -0,0 +1,236 @@
|
|||
{
|
||||
"recipe": {
|
||||
"model_providers": [
|
||||
{
|
||||
"name": "provider_1",
|
||||
"endpoint": "https://openrouter.ai/api/v1",
|
||||
"provider_type": "openai",
|
||||
"extra_headers": {},
|
||||
"extra_body": {}
|
||||
}
|
||||
],
|
||||
"mcp_providers": [],
|
||||
"model_configs": [
|
||||
{
|
||||
"alias": "model_1",
|
||||
"model": "mistralai/ministral-8b-2512",
|
||||
"provider": "provider_1",
|
||||
"inference_parameters": {
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 2048
|
||||
}
|
||||
}
|
||||
],
|
||||
"tool_configs": [],
|
||||
"columns": [
|
||||
{
|
||||
"column_type": "sampler",
|
||||
"name": "domain",
|
||||
"drop": true,
|
||||
"sampler_type": "category",
|
||||
"params": {
|
||||
"values": [
|
||||
"Tech Support",
|
||||
"Personal Finance",
|
||||
"Learning"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"column_type": "sampler",
|
||||
"name": "topic",
|
||||
"drop": true,
|
||||
"sampler_type": "subcategory",
|
||||
"params": {
|
||||
"category": "domain",
|
||||
"values": {
|
||||
"Tech Support": [
|
||||
"Wi-Fi keeps disconnecting",
|
||||
"Laptop running very slow",
|
||||
"Cannot install app update"
|
||||
],
|
||||
"Personal Finance": [
|
||||
"Monthly budget planning",
|
||||
"Credit card debt payoff",
|
||||
"Emergency fund setup"
|
||||
],
|
||||
"Learning": [
|
||||
"Exam study plan",
|
||||
"Learn Python basics",
|
||||
"Improve English writing"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"column_type": "sampler",
|
||||
"name": "conversation_length",
|
||||
"drop": true,
|
||||
"sampler_type": "category",
|
||||
"params": {
|
||||
"values": [
|
||||
"4",
|
||||
"6"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"column_type": "llm-text",
|
||||
"name": "user_goal",
|
||||
"drop": false,
|
||||
"model_alias": "model_1",
|
||||
"prompt": "Write one user goal for a chat assistant.\nDomain: {{ domain }}\nTopic: {{ topic }}\nConversation length target: {{ conversation_length }} messages total.\nRules:\n- 1 sentence.\n- Specific and practical.\n- Output only the goal text.",
|
||||
"system_prompt": "You write realistic user goals for assistant conversations.\n",
|
||||
"with_trace": "none"
|
||||
},
|
||||
{
|
||||
"column_type": "llm-structured",
|
||||
"name": "output_format",
|
||||
"drop": false,
|
||||
"model_alias": "model_1",
|
||||
"prompt": "Generate a realistic multi-turn conversation.\nUser goal:\n{{ user_goal }}\nConstraints:\n- Exactly {{ conversation_length }} messages total.\n- Alternate roles strictly: user, assistant, user, assistant...\n- First message must be user.\n- Last message must be assistant.\n- Keep responses grounded in {{ domain }} / {{ topic }}.\n- End naturally with resolution or clear next step.\n- No markdown, no extra keys.",
|
||||
"output_format": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"conversation": {
|
||||
"type": "array",
|
||||
"minItems": 4,
|
||||
"maxItems": 6,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user",
|
||||
"assistant"
|
||||
]
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"role",
|
||||
"content"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"conversation"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"processors": []
|
||||
},
|
||||
"run": {
|
||||
"rows": 5,
|
||||
"preview": true,
|
||||
"output_formats": [
|
||||
"jsonl"
|
||||
]
|
||||
},
|
||||
"ui": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "domain",
|
||||
"x": 0,
|
||||
"y": 140,
|
||||
"width": 400
|
||||
},
|
||||
{
|
||||
"id": "topic",
|
||||
"x": 0,
|
||||
"y": 280,
|
||||
"width": 400
|
||||
},
|
||||
{
|
||||
"id": "conversation_length",
|
||||
"x": 466.61510192672256,
|
||||
"y": 139.68271861864798,
|
||||
"width": 400
|
||||
},
|
||||
{
|
||||
"id": "user_goal",
|
||||
"x": 1.412158386197035,
|
||||
"y": 508.77123580445596,
|
||||
"width": 400
|
||||
},
|
||||
{
|
||||
"id": "output_format",
|
||||
"x": 1.1486983549970375,
|
||||
"y": 754.4221089431811,
|
||||
"width": 400
|
||||
},
|
||||
{
|
||||
"id": "provider_1",
|
||||
"x": -1056.848383841495,
|
||||
"y": 519.6373927070263,
|
||||
"width": 400
|
||||
},
|
||||
{
|
||||
"id": "model_1",
|
||||
"x": -543.7221365246206,
|
||||
"y": 488.2975724283656,
|
||||
"width": 400
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"from": "domain",
|
||||
"to": "topic",
|
||||
"type": "canvas",
|
||||
"source_handle": "data-out-bottom",
|
||||
"target_handle": "data-in-top"
|
||||
},
|
||||
{
|
||||
"from": "domain",
|
||||
"to": "conversation_length",
|
||||
"type": "canvas",
|
||||
"source_handle": "data-out",
|
||||
"target_handle": "data-in"
|
||||
},
|
||||
{
|
||||
"from": "topic",
|
||||
"to": "user_goal",
|
||||
"type": "canvas",
|
||||
"source_handle": "data-out-bottom",
|
||||
"target_handle": "data-in-top"
|
||||
},
|
||||
{
|
||||
"from": "user_goal",
|
||||
"to": "output_format",
|
||||
"type": "canvas",
|
||||
"source_handle": "data-out-bottom",
|
||||
"target_handle": "data-in-top"
|
||||
},
|
||||
{
|
||||
"from": "provider_1",
|
||||
"to": "model_1",
|
||||
"type": "semantic",
|
||||
"source_handle": "semantic-out",
|
||||
"target_handle": "semantic-in"
|
||||
},
|
||||
{
|
||||
"from": "model_1",
|
||||
"to": "user_goal",
|
||||
"type": "semantic",
|
||||
"source_handle": "semantic-out",
|
||||
"target_handle": "data-in"
|
||||
},
|
||||
{
|
||||
"from": "model_1",
|
||||
"to": "output_format",
|
||||
"type": "semantic",
|
||||
"source_handle": "semantic-out-bottom",
|
||||
"target_handle": "data-in"
|
||||
}
|
||||
],
|
||||
"layout_direction": "LR"
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ const instructionFromAnswerUrl = new URL(
|
|||
).href;
|
||||
const textToPythonUrl = new URL("./text-to-python.json", import.meta.url).href;
|
||||
const textToSqlUrl = new URL("./text-to-sql.json", import.meta.url).href;
|
||||
const conversationUrl = new URL("./conversation.json", import.meta.url).href;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||
|
|
@ -123,4 +124,11 @@ export const LEARNING_RECIPES: LearningRecipeDef[] = [
|
|||
"Generate SQL tasks and runnable SQL outputs with prompt-driven generation.",
|
||||
loadPayload: () => loadPayloadFromUrl(textToSqlUrl),
|
||||
},
|
||||
{
|
||||
id: "conversation",
|
||||
title: "Multi-Turn Chat",
|
||||
description:
|
||||
"Generate realistic user-assistant conversations with structured message output.",
|
||||
loadPayload: () => loadPayloadFromUrl(conversationUrl),
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -144,8 +144,8 @@ const TEMPLATE_CARDS: TemplateCard[] = [
|
|||
description:
|
||||
"Role-based multi-turn conversations for assistant behavior, memory, and response quality.",
|
||||
icon: AiChat02Icon,
|
||||
difficulty: "Advanced",
|
||||
learningBadges: ["LLM Text", "Conversation Design"],
|
||||
difficulty: "Easy",
|
||||
learningBadges: ["Structured LLM", "LLM Text"],
|
||||
surfaceClassName:
|
||||
"from-rose-500/15 via-pink-500/5 to-transparent",
|
||||
shineColor: [
|
||||
|
|
@ -153,6 +153,7 @@ const TEMPLATE_CARDS: TemplateCard[] = [
|
|||
"rgb(236 72 153 / 0.4)",
|
||||
"rgb(251 113 133 / 0.45)",
|
||||
],
|
||||
learningRecipeId: "conversation",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
type NodeTypes,
|
||||
Panel,
|
||||
ReactFlow,
|
||||
type ReactFlowInstance,
|
||||
} from "@xyflow/react";
|
||||
import {
|
||||
CookBookIcon,
|
||||
|
|
@ -101,6 +102,7 @@ export function RecipeStudioPage({
|
|||
activeConfigId,
|
||||
dialogOpen,
|
||||
layoutDirection,
|
||||
fitViewTick,
|
||||
onNodesChange,
|
||||
onEdgesChange,
|
||||
onConnect,
|
||||
|
|
@ -137,6 +139,7 @@ export function RecipeStudioPage({
|
|||
activeConfigId: state.activeConfigId,
|
||||
dialogOpen: state.dialogOpen,
|
||||
layoutDirection: state.layoutDirection,
|
||||
fitViewTick: state.fitViewTick,
|
||||
onNodesChange: state.onNodesChange,
|
||||
onEdgesChange: state.onEdgesChange,
|
||||
onConnect: state.onConnect,
|
||||
|
|
@ -169,6 +172,9 @@ export function RecipeStudioPage({
|
|||
const [activeView, setActiveView] = useState<RecipeStudioView>("editor");
|
||||
const [processorsOpen, setProcessorsOpen] = useState(false);
|
||||
const [interactive, setInteractive] = useState(true);
|
||||
const [reactFlowInstance, setReactFlowInstance] = useState<
|
||||
ReactFlowInstance<Node<RecipeNodeData | RecipeGraphAuxNodeData>, Edge> | null
|
||||
>(null);
|
||||
const handleExecutionStart = useCallback(() => {
|
||||
setActiveView("executions");
|
||||
}, []);
|
||||
|
|
@ -347,6 +353,24 @@ export function RecipeStudioPage({
|
|||
const runDialogLoading =
|
||||
runDialogKind === "preview" ? previewLoading : fullLoading;
|
||||
|
||||
useEffect(() => {
|
||||
if (!reactFlowInstance || activeView !== "editor" || fitViewTick === 0) {
|
||||
return;
|
||||
}
|
||||
let frame2 = 0;
|
||||
const frame1 = window.requestAnimationFrame(() => {
|
||||
frame2 = window.requestAnimationFrame(() => {
|
||||
reactFlowInstance.fitView({ duration: 250 });
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
window.cancelAnimationFrame(frame1);
|
||||
if (frame2) {
|
||||
window.cancelAnimationFrame(frame2);
|
||||
}
|
||||
};
|
||||
}, [activeView, fitViewTick, reactFlowInstance]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<main className="w-full px-6 py-8">
|
||||
|
|
@ -368,7 +392,8 @@ export function RecipeStudioPage({
|
|||
/>
|
||||
<div className="h-[75vh] w-full rounded-t-none">
|
||||
{activeView === "editor" ? (
|
||||
<ReactFlow
|
||||
<ReactFlow<Node<RecipeNodeData | RecipeGraphAuxNodeData>, Edge>
|
||||
onInit={setReactFlowInstance}
|
||||
nodes={displayGraph.nodes}
|
||||
edges={displayGraph.edges}
|
||||
nodeTypes={NODE_TYPES}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ type RecipeStudioState = {
|
|||
layoutDirection: LayoutDirection;
|
||||
nextId: number;
|
||||
nextY: number;
|
||||
fitViewTick: number;
|
||||
setSheetView: (view: SheetView) => void;
|
||||
setProcessors: (processors: RecipeProcessorConfig[]) => void;
|
||||
setDialogOpen: (open: boolean) => void;
|
||||
|
|
@ -103,6 +104,7 @@ const INITIAL_STATE = {
|
|||
layoutDirection: "LR",
|
||||
nextId: 3,
|
||||
nextY: 280,
|
||||
fitViewTick: 0,
|
||||
} satisfies Pick<
|
||||
RecipeStudioState,
|
||||
| "nodes"
|
||||
|
|
@ -118,6 +120,7 @@ const INITIAL_STATE = {
|
|||
| "layoutDirection"
|
||||
| "nextId"
|
||||
| "nextY"
|
||||
| "fitViewTick"
|
||||
>;
|
||||
|
||||
function buildAddedNodeState(
|
||||
|
|
@ -436,7 +439,7 @@ export const useRecipeStudioStore = create<RecipeStudioState>((set, get) => ({
|
|||
addExpressionNode: () =>
|
||||
set((state) => buildAddedNodeState(state, "expression", "expression")),
|
||||
loadRecipe: (snapshot) =>
|
||||
set(() => ({
|
||||
set((state) => ({
|
||||
configs: snapshot.configs,
|
||||
nodes: applyLayoutDirectionToNodes(
|
||||
snapshot.nodes,
|
||||
|
|
@ -454,6 +457,7 @@ export const useRecipeStudioStore = create<RecipeStudioState>((set, get) => ({
|
|||
activeConfigId: null,
|
||||
dialogOpen: false,
|
||||
sheetView: "root",
|
||||
fitViewTick: state.fitViewTick + 1,
|
||||
})),
|
||||
setAuxNodePosition: (id, position) =>
|
||||
set((state) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue