@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.
Files changed (61) hide show
  1. package/README.md +38 -0
  2. package/dist/workflows/agent-ask-clarification/agent-ask-clarification-example.workflow.d.ts +1 -1
  3. package/dist/workflows/agent-ask-clarification/agent-ask-clarification-example.workflow.d.ts.map +1 -1
  4. package/dist/workflows/agent-ask-clarification/agent-ask-clarification-example.workflow.js +2 -2
  5. package/dist/workflows/agent-ask-clarification/agent-ask-clarification-example.workflow.js.map +1 -1
  6. package/dist/workflows/agent-ask-for-approval/agent-ask-for-approval-example.workflow.d.ts +1 -1
  7. package/dist/workflows/agent-ask-for-approval/agent-ask-for-approval-example.workflow.d.ts.map +1 -1
  8. package/dist/workflows/agent-ask-for-approval/agent-ask-for-approval-example.workflow.js +2 -2
  9. package/dist/workflows/agent-ask-for-approval/agent-ask-for-approval-example.workflow.js.map +1 -1
  10. package/dist/workflows/ask-user-confirm/ask-user-confirm-example.workflow.d.ts +1 -1
  11. package/dist/workflows/ask-user-confirm/ask-user-confirm-example.workflow.d.ts.map +1 -1
  12. package/dist/workflows/ask-user-confirm/ask-user-confirm-example.workflow.js +2 -2
  13. package/dist/workflows/ask-user-confirm/ask-user-confirm-example.workflow.js.map +1 -1
  14. package/dist/workflows/ask-user-options/ask-user-options-example.workflow.d.ts +1 -1
  15. package/dist/workflows/ask-user-options/ask-user-options-example.workflow.d.ts.map +1 -1
  16. package/dist/workflows/ask-user-options/ask-user-options-example.workflow.js +2 -2
  17. package/dist/workflows/ask-user-options/ask-user-options-example.workflow.js.map +1 -1
  18. package/dist/workflows/ask-user-text/ask-user-text-example.workflow.d.ts +1 -1
  19. package/dist/workflows/ask-user-text/ask-user-text-example.workflow.d.ts.map +1 -1
  20. package/dist/workflows/ask-user-text/ask-user-text-example.workflow.js +2 -2
  21. package/dist/workflows/ask-user-text/ask-user-text-example.workflow.js.map +1 -1
  22. package/dist/workflows/confirm-content/confirm-content-example.workflow.d.ts +1 -1
  23. package/dist/workflows/confirm-content/confirm-content-example.workflow.d.ts.map +1 -1
  24. package/dist/workflows/confirm-content/confirm-content-example.workflow.js +2 -2
  25. package/dist/workflows/confirm-content/confirm-content-example.workflow.js.map +1 -1
  26. package/dist/workflows/inline-form/feedback-form-document.d.ts +2 -0
  27. package/dist/workflows/inline-form/feedback-form-document.d.ts.map +1 -1
  28. package/dist/workflows/inline-form/feedback-form-document.js +2 -0
  29. package/dist/workflows/inline-form/feedback-form-document.js.map +1 -1
  30. package/dist/workflows/inline-form/feedback-form-document.yaml +3 -0
  31. package/dist/workflows/inline-form/inline-form-example.workflow.d.ts +1 -1
  32. package/dist/workflows/inline-form/inline-form-example.workflow.d.ts.map +1 -1
  33. package/dist/workflows/inline-form/inline-form-example.workflow.js +4 -4
  34. package/dist/workflows/inline-form/inline-form-example.workflow.js.map +1 -1
  35. package/dist/workflows/prompt-input-chat/prompt-input-chat-example.workflow.d.ts +5 -2
  36. package/dist/workflows/prompt-input-chat/prompt-input-chat-example.workflow.d.ts.map +1 -1
  37. package/dist/workflows/prompt-input-chat/prompt-input-chat-example.workflow.js +29 -16
  38. package/dist/workflows/prompt-input-chat/prompt-input-chat-example.workflow.js.map +1 -1
  39. package/package.json +4 -3
  40. package/src/workflows/agent-ask-clarification/__tests__/agent-ask-clarification-example.live.spec.ts +27 -0
  41. package/src/workflows/agent-ask-clarification/__tests__/agent-ask-clarification-example.spec.ts +86 -0
  42. package/src/workflows/agent-ask-clarification/agent-ask-clarification-example.workflow.ts +1 -1
  43. package/src/workflows/agent-ask-for-approval/__tests__/agent-ask-for-approval-example.live.spec.ts +27 -0
  44. package/src/workflows/agent-ask-for-approval/__tests__/agent-ask-for-approval-example.spec.ts +96 -0
  45. package/src/workflows/agent-ask-for-approval/agent-ask-for-approval-example.workflow.ts +1 -1
  46. package/src/workflows/ask-user-confirm/__tests__/ask-user-confirm-example.spec.ts +56 -0
  47. package/src/workflows/ask-user-confirm/ask-user-confirm-example.workflow.ts +1 -1
  48. package/src/workflows/ask-user-options/__tests__/ask-user-options-example.spec.ts +60 -0
  49. package/src/workflows/ask-user-options/ask-user-options-example.workflow.ts +1 -1
  50. package/src/workflows/ask-user-text/__tests__/ask-user-text-example.spec.ts +62 -0
  51. package/src/workflows/ask-user-text/ask-user-text-example.workflow.ts +1 -1
  52. package/src/workflows/confirm-content/__tests__/confirm-content-example.spec.ts +56 -0
  53. package/src/workflows/confirm-content/confirm-content-example.workflow.ts +1 -1
  54. package/src/workflows/inline-form/__tests__/inline-form-example.spec.ts +54 -0
  55. package/src/workflows/inline-form/feedback-form-document.ts +2 -0
  56. package/src/workflows/inline-form/feedback-form-document.yaml +3 -0
  57. package/src/workflows/inline-form/inline-form-example.workflow.ts +10 -3
  58. package/src/workflows/meeting-notes/__tests__/meeting-notes-example.live.spec.ts +38 -0
  59. package/src/workflows/meeting-notes/__tests__/meeting-notes-example.spec.ts +105 -0
  60. package/src/workflows/prompt-input-chat/__tests__/prompt-input-chat-example.spec.ts +83 -0
  61. 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, MessageDocument, Transition, Workflow } from '@loopstack/common';
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, echoes back a reply, and re-enters the waiting state. No LLM and no sub-workflow involved.',
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(_state: Record<string, unknown>) {
15
- await this.documentStore.save(MessageDocument, {
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! Send me a message and I will echo it back.',
24
+ text: 'Hi! Ask me anything.',
18
25
  });
19
26
  }
20
27
 
21
- @Transition({ from: 'waiting_for_user', to: 'reply_sent', wait: true, schema: z.string() })
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
- const payload = input.data;
24
- await this.documentStore.save(MessageDocument, { role: 'user', text: payload });
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: 'reply_sent', to: 'waiting_for_user' })
29
- loop(_state: Record<string, unknown>) {}
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
  }