@loopstack/hitl-examples 0.1.1 → 0.2.1
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/README.md +38 -0
- package/dist/workflows/agent-ask-clarification/agent-ask-clarification-example.workflow.d.ts +1 -1
- package/dist/workflows/agent-ask-clarification/agent-ask-clarification-example.workflow.d.ts.map +1 -1
- package/dist/workflows/agent-ask-clarification/agent-ask-clarification-example.workflow.js +2 -2
- package/dist/workflows/agent-ask-clarification/agent-ask-clarification-example.workflow.js.map +1 -1
- package/dist/workflows/agent-ask-for-approval/agent-ask-for-approval-example.workflow.d.ts +1 -1
- package/dist/workflows/agent-ask-for-approval/agent-ask-for-approval-example.workflow.d.ts.map +1 -1
- package/dist/workflows/agent-ask-for-approval/agent-ask-for-approval-example.workflow.js +2 -2
- package/dist/workflows/agent-ask-for-approval/agent-ask-for-approval-example.workflow.js.map +1 -1
- package/dist/workflows/ask-user-confirm/ask-user-confirm-example.workflow.d.ts +1 -1
- package/dist/workflows/ask-user-confirm/ask-user-confirm-example.workflow.d.ts.map +1 -1
- package/dist/workflows/ask-user-confirm/ask-user-confirm-example.workflow.js +2 -2
- package/dist/workflows/ask-user-confirm/ask-user-confirm-example.workflow.js.map +1 -1
- package/dist/workflows/ask-user-options/ask-user-options-example.workflow.d.ts +1 -1
- package/dist/workflows/ask-user-options/ask-user-options-example.workflow.d.ts.map +1 -1
- package/dist/workflows/ask-user-options/ask-user-options-example.workflow.js +2 -2
- package/dist/workflows/ask-user-options/ask-user-options-example.workflow.js.map +1 -1
- package/dist/workflows/ask-user-text/ask-user-text-example.workflow.d.ts +1 -1
- package/dist/workflows/ask-user-text/ask-user-text-example.workflow.d.ts.map +1 -1
- package/dist/workflows/ask-user-text/ask-user-text-example.workflow.js +2 -2
- package/dist/workflows/ask-user-text/ask-user-text-example.workflow.js.map +1 -1
- package/dist/workflows/confirm-content/confirm-content-example.workflow.d.ts +1 -1
- package/dist/workflows/confirm-content/confirm-content-example.workflow.d.ts.map +1 -1
- package/dist/workflows/confirm-content/confirm-content-example.workflow.js +2 -2
- package/dist/workflows/confirm-content/confirm-content-example.workflow.js.map +1 -1
- package/dist/workflows/inline-form/feedback-form-document.d.ts +2 -0
- package/dist/workflows/inline-form/feedback-form-document.d.ts.map +1 -1
- package/dist/workflows/inline-form/feedback-form-document.js +2 -0
- package/dist/workflows/inline-form/feedback-form-document.js.map +1 -1
- package/dist/workflows/inline-form/feedback-form-document.yaml +3 -0
- package/dist/workflows/inline-form/inline-form-example.workflow.d.ts +1 -1
- package/dist/workflows/inline-form/inline-form-example.workflow.d.ts.map +1 -1
- package/dist/workflows/inline-form/inline-form-example.workflow.js +4 -4
- package/dist/workflows/inline-form/inline-form-example.workflow.js.map +1 -1
- package/dist/workflows/prompt-input-chat/prompt-input-chat-example.workflow.d.ts +5 -2
- package/dist/workflows/prompt-input-chat/prompt-input-chat-example.workflow.d.ts.map +1 -1
- package/dist/workflows/prompt-input-chat/prompt-input-chat-example.workflow.js +29 -16
- package/dist/workflows/prompt-input-chat/prompt-input-chat-example.workflow.js.map +1 -1
- package/package.json +4 -3
- package/src/workflows/agent-ask-clarification/__tests__/agent-ask-clarification-example.live.spec.ts +27 -0
- package/src/workflows/agent-ask-clarification/__tests__/agent-ask-clarification-example.spec.ts +86 -0
- package/src/workflows/agent-ask-clarification/agent-ask-clarification-example.workflow.ts +1 -1
- package/src/workflows/agent-ask-for-approval/__tests__/agent-ask-for-approval-example.live.spec.ts +27 -0
- package/src/workflows/agent-ask-for-approval/__tests__/agent-ask-for-approval-example.spec.ts +96 -0
- package/src/workflows/agent-ask-for-approval/agent-ask-for-approval-example.workflow.ts +1 -1
- package/src/workflows/ask-user-confirm/__tests__/ask-user-confirm-example.spec.ts +56 -0
- package/src/workflows/ask-user-confirm/ask-user-confirm-example.workflow.ts +1 -1
- package/src/workflows/ask-user-options/__tests__/ask-user-options-example.spec.ts +60 -0
- package/src/workflows/ask-user-options/ask-user-options-example.workflow.ts +1 -1
- package/src/workflows/ask-user-text/__tests__/ask-user-text-example.spec.ts +62 -0
- package/src/workflows/ask-user-text/ask-user-text-example.workflow.ts +1 -1
- package/src/workflows/confirm-content/__tests__/confirm-content-example.spec.ts +56 -0
- package/src/workflows/confirm-content/confirm-content-example.workflow.ts +1 -1
- package/src/workflows/inline-form/__tests__/inline-form-example.spec.ts +54 -0
- package/src/workflows/inline-form/feedback-form-document.ts +2 -0
- package/src/workflows/inline-form/feedback-form-document.yaml +3 -0
- package/src/workflows/inline-form/inline-form-example.workflow.ts +10 -3
- package/src/workflows/meeting-notes/__tests__/meeting-notes-example.live.spec.ts +38 -0
- package/src/workflows/meeting-notes/__tests__/meeting-notes-example.spec.ts +105 -0
- package/src/workflows/prompt-input-chat/__tests__/prompt-input-chat-example.spec.ts +83 -0
- package/src/workflows/prompt-input-chat/prompt-input-chat-example.workflow.ts +34 -12
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { LlmProviderModule } from '@loopstack/llm-provider-module';
|
|
3
|
+
import { type TestRun, coverage, replay, runWorkflow } from '@loopstack/testing';
|
|
4
|
+
import { HitlExamplesModule } from '../../../hitl-examples.module';
|
|
5
|
+
import { MeetingNotesExampleWorkflow } from '../meeting-notes-example.workflow';
|
|
6
|
+
|
|
7
|
+
const OPTIMIZED = {
|
|
8
|
+
date: '2025-01-01',
|
|
9
|
+
summary: 'Budget review and hiring decision.',
|
|
10
|
+
participants: ['Sarah', 'Anna'],
|
|
11
|
+
decisions: ['Cut costs', 'Hire for marketing'],
|
|
12
|
+
actionItems: ['Anna follows up on vendor pricing'],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** The scripted extraction turn in the shape `llm_generate_object` returns. */
|
|
16
|
+
const extraction = () =>
|
|
17
|
+
replay({
|
|
18
|
+
version: 3,
|
|
19
|
+
recordings: [
|
|
20
|
+
{
|
|
21
|
+
tool: 'llm_generate_object',
|
|
22
|
+
envelope: {
|
|
23
|
+
data: { data: OPTIMIZED, response: {} },
|
|
24
|
+
metadata: { provider: 'claude', model: 'claude-sonnet-4-6' },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('MeetingNotesExampleWorkflow', () => {
|
|
31
|
+
const runs: TestRun[] = [];
|
|
32
|
+
|
|
33
|
+
it('runs the full review flow: edit notes → scripted LLM extraction → confirm', async () => {
|
|
34
|
+
const run = await runWorkflow(
|
|
35
|
+
MeetingNotesExampleWorkflow,
|
|
36
|
+
{ inputText: 'budget: cut costs; anna follows up on vendor pricing' },
|
|
37
|
+
{
|
|
38
|
+
imports: [LlmProviderModule, HitlExamplesModule],
|
|
39
|
+
answers: {
|
|
40
|
+
userResponse: { text: 'Reviewed notes: cut costs, Anna follows up on vendor pricing.' },
|
|
41
|
+
confirm: OPTIMIZED,
|
|
42
|
+
},
|
|
43
|
+
replay: extraction(),
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
runs.push(run);
|
|
47
|
+
|
|
48
|
+
expect(run.error).toBeUndefined();
|
|
49
|
+
expect(run.status).toBe('completed');
|
|
50
|
+
expect(run.path).toEqual(['createForm', 'userResponse', 'optimizeNotes', 'confirm']);
|
|
51
|
+
expect(run.result).toEqual({ optimizedNotes: OPTIMIZED });
|
|
52
|
+
expect(run.document('final')).toEqual(OPTIMIZED);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('parks on the notes review form (first park)', async () => {
|
|
56
|
+
const run = await runWorkflow(MeetingNotesExampleWorkflow, undefined, {
|
|
57
|
+
imports: [LlmProviderModule, HitlExamplesModule],
|
|
58
|
+
});
|
|
59
|
+
runs.push(run);
|
|
60
|
+
|
|
61
|
+
expect(run.status).toBe('waiting');
|
|
62
|
+
expect(run.place).toBe('waiting_for_response');
|
|
63
|
+
|
|
64
|
+
const view = run.parkView();
|
|
65
|
+
expect(view).toMatchObject({
|
|
66
|
+
widget: 'form',
|
|
67
|
+
documentName: 'meeting_notes',
|
|
68
|
+
content: { text: expect.stringContaining('Unstructured Notes:') },
|
|
69
|
+
actions: ['Optimize Notes'],
|
|
70
|
+
defaultTransition: 'userResponse',
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('parks on the optimized-notes confirm form (second park)', async () => {
|
|
75
|
+
const run = await runWorkflow(
|
|
76
|
+
MeetingNotesExampleWorkflow,
|
|
77
|
+
{ inputText: 'budget: cut costs' },
|
|
78
|
+
{
|
|
79
|
+
imports: [LlmProviderModule, HitlExamplesModule],
|
|
80
|
+
// Answer the first park but not the second — the run advances through the LLM
|
|
81
|
+
// extraction and parks again on the confirmation form.
|
|
82
|
+
answers: { userResponse: { text: 'Reviewed notes.' } },
|
|
83
|
+
replay: extraction(),
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
runs.push(run);
|
|
87
|
+
|
|
88
|
+
expect(run.status).toBe('waiting');
|
|
89
|
+
expect(run.place).toBe('notes_optimized');
|
|
90
|
+
|
|
91
|
+
const view = run.parkView();
|
|
92
|
+
expect(view).toMatchObject({
|
|
93
|
+
widget: 'form',
|
|
94
|
+
documentName: 'optimized_notes',
|
|
95
|
+
actions: ['Confirm'],
|
|
96
|
+
defaultTransition: 'confirm',
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('covers every transition and both parks (coverage gate)', () => {
|
|
101
|
+
const cov = coverage(runs, MeetingNotesExampleWorkflow);
|
|
102
|
+
expect(cov.missingTransitions).toEqual([]);
|
|
103
|
+
expect(cov.missingParks).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { LlmProviderModule } from '@loopstack/llm-provider-module';
|
|
3
|
+
import { queue, replay, runWorkflow } from '@loopstack/testing';
|
|
4
|
+
import { HitlExamplesModule } from '../../../hitl-examples.module';
|
|
5
|
+
import { PromptInputChatExampleWorkflow } from '../prompt-input-chat-example.workflow';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A scripted assistant reply in the shape `llm_generate_text` returns — including the
|
|
9
|
+
* `documents` declaration the live tool emits, so replay materializes the assistant
|
|
10
|
+
* message as a conversation document exactly like a real call would.
|
|
11
|
+
*/
|
|
12
|
+
const assistantReply = (text: string) => {
|
|
13
|
+
const message = {
|
|
14
|
+
id: `msg_${text.length}`,
|
|
15
|
+
role: 'assistant',
|
|
16
|
+
text,
|
|
17
|
+
blocks: [{ type: 'text', text }],
|
|
18
|
+
stopReason: 'end_turn',
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
tool: 'llm_generate_text',
|
|
22
|
+
envelope: {
|
|
23
|
+
data: { message, response: {} },
|
|
24
|
+
metadata: { provider: 'claude', model: 'claude-sonnet-4-6' },
|
|
25
|
+
documents: [
|
|
26
|
+
{
|
|
27
|
+
documentName: 'llm_message',
|
|
28
|
+
content: message,
|
|
29
|
+
options: { meta: { response: {}, provider: 'claude' } },
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('PromptInputChatExampleWorkflow', () => {
|
|
37
|
+
it('drives two chat turns with queued messages and scripted LLM replies, then parks again', async () => {
|
|
38
|
+
const run = await runWorkflow(PromptInputChatExampleWorkflow, undefined, {
|
|
39
|
+
imports: [LlmProviderModule, HitlExamplesModule],
|
|
40
|
+
answers: { userMessage: queue('What is Loopstack?', 'And what about pricing?') },
|
|
41
|
+
replay: replay({
|
|
42
|
+
version: 3,
|
|
43
|
+
recordings: [assistantReply('Loopstack is a workflow framework.'), assistantReply('It is open source.')],
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(run.error).toBeUndefined();
|
|
48
|
+
// The chat loops back to waiting after each reply — two scripted turns, then parked again.
|
|
49
|
+
// Storing the message (userMessage) and generating the reply (generateReply) are separate
|
|
50
|
+
// transitions so the user's message is committed — and visible — before the LLM turn runs.
|
|
51
|
+
expect(run.status).toBe('waiting');
|
|
52
|
+
expect(run.place).toBe('waiting_for_user');
|
|
53
|
+
expect(run.path).toEqual(['greet', 'userMessage', 'generateReply', 'userMessage', 'generateReply']);
|
|
54
|
+
|
|
55
|
+
// Both sides of the conversation are documents: user messages from the workflow,
|
|
56
|
+
// assistant replies from the replayed envelopes' document declarations.
|
|
57
|
+
const texts = run.documents.map((d) => (d.content as { text?: string }).text ?? '');
|
|
58
|
+
expect(texts).toContain('What is Loopstack?');
|
|
59
|
+
expect(texts).toContain('Loopstack is a workflow framework.');
|
|
60
|
+
expect(texts).toContain('And what about pricing?');
|
|
61
|
+
expect(texts).toContain('It is open source.');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('parks on the greeting when no message is scripted', async () => {
|
|
65
|
+
const run = await runWorkflow(PromptInputChatExampleWorkflow, undefined, {
|
|
66
|
+
imports: [LlmProviderModule, HitlExamplesModule],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(run.status).toBe('waiting');
|
|
70
|
+
expect(run.path).toEqual(['greet']);
|
|
71
|
+
const texts = run.documents.map((d) => (d.content as { text?: string }).text ?? '');
|
|
72
|
+
expect(texts).toContain('Hi! Ask me anything.');
|
|
73
|
+
|
|
74
|
+
// The chat input the user would type into — a workflow-level `prompt-input` widget
|
|
75
|
+
// (from the workflow's own `widget:` config), active at the waiting_for_user place.
|
|
76
|
+
// The greeting message above is display content, not a prompt, so it isn't surfaced here.
|
|
77
|
+
expect(run.parkView()).toMatchObject({
|
|
78
|
+
widget: 'prompt-input',
|
|
79
|
+
options: { label: 'Send' },
|
|
80
|
+
defaultTransition: 'userMessage',
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -1,30 +1,52 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { BaseWorkflow,
|
|
2
|
+
import { BaseWorkflow, Transition, Workflow } from '@loopstack/common';
|
|
3
3
|
import type { TransitionInput } from '@loopstack/common';
|
|
4
|
+
import { LlmGenerateTextTool, LlmMessageDocument } from '@loopstack/llm-provider-module';
|
|
4
5
|
|
|
5
6
|
@Workflow({
|
|
6
7
|
title: 'HITL - Prompt Input Chat Example',
|
|
7
8
|
description:
|
|
8
|
-
'Predefined-workflow HITL via the prompt-input widget. The workflow loops: it waits for a user ' +
|
|
9
|
-
'message,
|
|
9
|
+
'Predefined-workflow HITL chat via the prompt-input widget. The workflow loops: it waits for a user ' +
|
|
10
|
+
'message, generates an LLM reply from the full conversation history, and re-enters the waiting state.',
|
|
10
11
|
widget: './prompt-input-chat-example.workflow.yaml',
|
|
11
12
|
})
|
|
12
13
|
export class PromptInputChatExampleWorkflow extends BaseWorkflow {
|
|
14
|
+
constructor(private readonly llmGenerateText: LlmGenerateTextTool) {
|
|
15
|
+
super();
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
@Transition({ to: 'waiting_for_user' })
|
|
14
|
-
async greet(
|
|
15
|
-
|
|
19
|
+
async greet() {
|
|
20
|
+
// Same document type as every other chat turn: renders like one and, being tagged
|
|
21
|
+
// 'message', joins the conversation history the provider builds below.
|
|
22
|
+
await this.documentStore.save(LlmMessageDocument, {
|
|
16
23
|
role: 'assistant',
|
|
17
|
-
text: 'Hi!
|
|
24
|
+
text: 'Hi! Ask me anything.',
|
|
18
25
|
});
|
|
19
26
|
}
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
// Storing the message and generating the reply are separate transitions on purpose:
|
|
29
|
+
// each transition commits its own transaction, so the user's message becomes visible
|
|
30
|
+
// in the UI immediately — before the (slow) LLM turn even starts.
|
|
31
|
+
@Transition({ from: 'waiting_for_user', to: 'generating_reply', wait: true, schema: z.string() })
|
|
22
32
|
async userMessage(state: Record<string, unknown>, input: TransitionInput<string>) {
|
|
23
|
-
|
|
24
|
-
await this.documentStore.save(
|
|
25
|
-
await this.documentStore.save(MessageDocument, { role: 'assistant', text: `You said: ${payload}` });
|
|
33
|
+
// Tagged 'message', so it becomes part of the conversation history below
|
|
34
|
+
await this.documentStore.save(LlmMessageDocument, { role: 'user', text: input.data });
|
|
26
35
|
}
|
|
27
36
|
|
|
28
|
-
@Transition({ from: '
|
|
29
|
-
|
|
37
|
+
@Transition({ from: 'generating_reply', to: 'waiting_for_user' })
|
|
38
|
+
async generateReply() {
|
|
39
|
+
// No prompt/messages args: the provider builds the conversation from all
|
|
40
|
+
// documents tagged 'message' and saves the assistant reply automatically
|
|
41
|
+
await this.llmGenerateText.call(
|
|
42
|
+
{},
|
|
43
|
+
{
|
|
44
|
+
config: {
|
|
45
|
+
provider: 'claude',
|
|
46
|
+
model: 'claude-sonnet-4-6',
|
|
47
|
+
system: 'You are a friendly, concise chat assistant.',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
}
|
|
30
52
|
}
|