@minded-ai/mindedjs 2.0.14-beta-1 → 2.0.15-beta-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 (103) hide show
  1. package/dist/agent.js +17 -7
  2. package/dist/agent.js.map +1 -1
  3. package/dist/browserTask/executeBrowserTask.d.ts +2 -2
  4. package/dist/browserTask/executeBrowserTask.d.ts.map +1 -1
  5. package/dist/browserTask/executeBrowserTask.js +16 -5
  6. package/dist/browserTask/executeBrowserTask.js.map +1 -1
  7. package/dist/browserTask/executeBrowserTask.py +0 -1
  8. package/dist/browserTask/localBrowserTask.js +17 -7
  9. package/dist/browserTask/localBrowserTask.js.map +1 -1
  10. package/dist/checkpointer/checkpointSaverFactory.js +17 -7
  11. package/dist/checkpointer/checkpointSaverFactory.js.map +1 -1
  12. package/dist/cli/index.js +45 -14
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/cli/lambdaHandlerTemplate.d.ts.map +1 -1
  15. package/dist/cli/localOperatorSetup.d.ts +6 -0
  16. package/dist/cli/localOperatorSetup.d.ts.map +1 -0
  17. package/dist/cli/localOperatorSetup.js +310 -0
  18. package/dist/cli/localOperatorSetup.js.map +1 -0
  19. package/dist/edges/createDirectEdge.d.ts.map +1 -1
  20. package/dist/edges/createLogicalRouter.d.ts.map +1 -1
  21. package/dist/edges/createLogicalRouter.js +17 -7
  22. package/dist/edges/createLogicalRouter.js.map +1 -1
  23. package/dist/edges/createPromptRouter.d.ts.map +1 -1
  24. package/dist/edges/edgeFactory.d.ts.map +1 -1
  25. package/dist/index.js +17 -7
  26. package/dist/index.js.map +1 -1
  27. package/dist/interfaces/zendesk.js +17 -7
  28. package/dist/interfaces/zendesk.js.map +1 -1
  29. package/dist/internalTools/appActionRunnerTool.d.ts.map +1 -1
  30. package/dist/internalTools/appActionRunnerTool.js +17 -7
  31. package/dist/internalTools/appActionRunnerTool.js.map +1 -1
  32. package/dist/internalTools/documentExtraction/documentExtraction.js +17 -7
  33. package/dist/internalTools/documentExtraction/documentExtraction.js.map +1 -1
  34. package/dist/internalTools/libraryActionRunnerTool.d.ts.map +1 -1
  35. package/dist/internalTools/timer.js +17 -7
  36. package/dist/internalTools/timer.js.map +1 -1
  37. package/dist/internalTools/voice/escalateVoiceCall.d.ts.map +1 -1
  38. package/dist/internalTools/voice/escalateVoiceCall.js +17 -7
  39. package/dist/internalTools/voice/escalateVoiceCall.js.map +1 -1
  40. package/dist/internalTools/voice/retell.js +17 -7
  41. package/dist/internalTools/voice/retell.js.map +1 -1
  42. package/dist/internalTools/voice/sendPlaceholderMessage.js +17 -7
  43. package/dist/internalTools/voice/sendPlaceholderMessage.js.map +1 -1
  44. package/dist/interrupts/MindedInterruptSessionManager.js +17 -7
  45. package/dist/interrupts/MindedInterruptSessionManager.js.map +1 -1
  46. package/dist/interrupts/interruptSessionManagerFactory.js +17 -7
  47. package/dist/interrupts/interruptSessionManagerFactory.js.map +1 -1
  48. package/dist/llm/createLlmInstance.d.ts.map +1 -1
  49. package/dist/nodes/addAppToolNode.d.ts.map +1 -1
  50. package/dist/nodes/addBrowserTaskNode.d.ts.map +1 -1
  51. package/dist/nodes/addBrowserTaskNode.js.map +1 -1
  52. package/dist/nodes/addBrowserTaskRunNode.d.ts.map +1 -1
  53. package/dist/nodes/addHumanInTheLoopNode.d.ts.map +1 -1
  54. package/dist/nodes/addJumpToNode.d.ts.map +1 -1
  55. package/dist/nodes/addJunctionNode.d.ts.map +1 -1
  56. package/dist/nodes/addPromptNode.d.ts.map +1 -1
  57. package/dist/nodes/addRpaNode.d.ts.map +1 -1
  58. package/dist/nodes/addRpaNode.js +12 -1
  59. package/dist/nodes/addRpaNode.js.map +1 -1
  60. package/dist/nodes/addToolNode.d.ts.map +1 -1
  61. package/dist/nodes/addToolRunNode.d.ts.map +1 -1
  62. package/dist/nodes/addTriggerNode.d.ts.map +1 -1
  63. package/dist/nodes/compilePrompt.js +17 -7
  64. package/dist/nodes/compilePrompt.js.map +1 -1
  65. package/dist/nodes/nodeFactory.d.ts.map +1 -1
  66. package/dist/platform/mindedCheckpointSaver.js +17 -7
  67. package/dist/platform/mindedCheckpointSaver.js.map +1 -1
  68. package/dist/platform/mindedConnection.d.ts.map +1 -1
  69. package/dist/platform/mindedConnection.js +17 -7
  70. package/dist/platform/mindedConnection.js.map +1 -1
  71. package/dist/platform/piiGateway/gateway.js +17 -7
  72. package/dist/platform/piiGateway/gateway.js.map +1 -1
  73. package/dist/platform/utils/parseAttachments.d.ts.map +1 -1
  74. package/dist/platform/utils/tools.d.ts.map +1 -1
  75. package/dist/playbooks/playbooks.js +17 -7
  76. package/dist/playbooks/playbooks.js.map +1 -1
  77. package/dist/toolsLibrary/classifier.d.ts +12 -20
  78. package/dist/toolsLibrary/classifier.d.ts.map +1 -1
  79. package/dist/toolsLibrary/classifier.js +71 -109
  80. package/dist/toolsLibrary/classifier.js.map +1 -1
  81. package/dist/toolsLibrary/extraction.d.ts.map +1 -1
  82. package/dist/toolsLibrary/index.js +17 -7
  83. package/dist/toolsLibrary/index.js.map +1 -1
  84. package/dist/toolsLibrary/parseDocument.d.ts +3 -3
  85. package/dist/types/LLM.types.js.map +1 -1
  86. package/dist/types/LangGraph.types.d.ts.map +1 -1
  87. package/dist/utils/agentUtils.js +17 -7
  88. package/dist/utils/agentUtils.js.map +1 -1
  89. package/dist/utils/history.d.ts.map +1 -1
  90. package/dist/utils/logger.d.ts +2 -1
  91. package/dist/utils/logger.d.ts.map +1 -1
  92. package/dist/utils/wait.d.ts.map +1 -1
  93. package/dist/voice/voiceSession.js +17 -7
  94. package/dist/voice/voiceSession.js.map +1 -1
  95. package/package.json +1 -1
  96. package/src/browserTask/executeBrowserTask.py +0 -1
  97. package/src/browserTask/executeBrowserTask.ts +34 -20
  98. package/src/cli/index.ts +25 -7
  99. package/src/cli/localOperatorSetup.ts +308 -0
  100. package/src/nodes/addBrowserTaskNode.ts +2 -2
  101. package/src/nodes/addRpaNode.ts +13 -2
  102. package/src/toolsLibrary/classifier.ts +86 -114
  103. package/src/types/LLM.types.ts +2 -2
@@ -0,0 +1,308 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { execSync, spawn } from 'child_process';
4
+ import * as readline from 'readline';
5
+
6
+ const VENV_DIR = '.venv';
7
+ const SETUP_MARKER_FILE = '.minded-local-operator-setup';
8
+
9
+ interface SetupPackage {
10
+ name: string;
11
+ description: string;
12
+ command: string;
13
+ checkCommand?: string;
14
+ }
15
+
16
+ const REQUIRED_PACKAGES: SetupPackage[] = [
17
+ {
18
+ name: 'uv',
19
+ description: 'Python package manager for fast dependency installation',
20
+ command: 'curl -LsSf https://astral.sh/uv/install.sh | sh',
21
+ checkCommand: 'uv --version',
22
+ },
23
+ {
24
+ name: 'Python 3.12',
25
+ description: 'Python runtime for browser automation',
26
+ command: 'python3.12 --version',
27
+ checkCommand: 'python3.12 --version',
28
+ },
29
+ {
30
+ name: 'browser-use',
31
+ description: 'Browser automation library',
32
+ command: 'uv pip install browser-use',
33
+ },
34
+ {
35
+ name: 'Playwright Chromium',
36
+ description: 'Chromium browser for automation',
37
+ command: 'uvx playwright install chromium --with-deps',
38
+ },
39
+ ];
40
+
41
+ export function isLocalOperatorSetup(): boolean {
42
+ const setupMarkerPath = path.join(process.cwd(), SETUP_MARKER_FILE);
43
+ const venvPath = path.join(process.cwd(), VENV_DIR);
44
+
45
+ // Check if both the marker file and venv directory exist
46
+ return fs.existsSync(setupMarkerPath) && fs.existsSync(venvPath);
47
+ }
48
+
49
+ function checkCommand(command: string): boolean {
50
+ try {
51
+ execSync(command, { stdio: 'ignore' });
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ function promptUser(question: string): Promise<boolean> {
59
+ // Check if we're in an interactive terminal
60
+ if (!process.stdin.isTTY) {
61
+ console.warn('Non-interactive terminal detected, skipping prompt and cancelling installation');
62
+ return Promise.resolve(false);
63
+ }
64
+
65
+ return new Promise((resolve) => {
66
+ const rl = readline.createInterface({
67
+ input: process.stdin,
68
+ output: process.stdout,
69
+ });
70
+
71
+ // Set a timeout in case the prompt hangs
72
+ const timeout = setTimeout(() => {
73
+ console.warn('Prompt timeout (30s), cancelling installation');
74
+ rl.close();
75
+ resolve(false);
76
+ }, 30000); // 30 second timeout
77
+
78
+ console.log('Displaying prompt to user');
79
+
80
+ rl.question(question, (answer) => {
81
+ clearTimeout(timeout);
82
+ console.log(`User answered: "${answer}"`);
83
+
84
+ rl.close();
85
+
86
+ const confirmed = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
87
+ console.log(`Prompt result: ${confirmed}`);
88
+
89
+ resolve(confirmed);
90
+ });
91
+
92
+ // Handle potential readline errors
93
+ rl.on('error', (error) => {
94
+ console.error('Readline error, cancelling installation:', error.message);
95
+ clearTimeout(timeout);
96
+ rl.close();
97
+ resolve(false);
98
+ });
99
+ });
100
+ }
101
+
102
+ async function runCommand(command: string, description: string): Promise<void> {
103
+ return new Promise((resolve, reject) => {
104
+ console.log(`Installing ${description}...`);
105
+
106
+ const isComplexCommand = command.includes('|') || command.includes('&&');
107
+
108
+ if (isComplexCommand) {
109
+ // For complex commands with pipes or &&, use shell
110
+ const child = spawn('sh', ['-c', command], {
111
+ stdio: 'inherit',
112
+ shell: true,
113
+ });
114
+
115
+ child.on('error', (error) => {
116
+ console.error(`Failed to install ${description}:`, error.message);
117
+ reject(error);
118
+ });
119
+
120
+ child.on('close', (code) => {
121
+ if (code === 0) {
122
+ console.log(`✓ ${description} installed successfully`);
123
+ resolve();
124
+ } else {
125
+ reject(new Error(`Command exited with code ${code}`));
126
+ }
127
+ });
128
+ } else {
129
+ // For simple commands, use execSync
130
+ try {
131
+ execSync(command, { stdio: 'inherit' });
132
+ console.log(`✓ ${description} installed successfully`);
133
+ resolve();
134
+ } catch (error) {
135
+ console.error(`Failed to install ${description}:`, error);
136
+ reject(error);
137
+ }
138
+ }
139
+ });
140
+ }
141
+
142
+ export async function setupLocalOperator(skipPrompt: boolean = false): Promise<void> {
143
+ console.log('\n🤖 Minded Local Operator Setup\n');
144
+
145
+ // Check if already setup
146
+ if (isLocalOperatorSetup()) {
147
+ console.log('✓ Local operator is already set up');
148
+ return;
149
+ }
150
+
151
+ // Display what will be installed
152
+ console.log('The following packages will be installed for local browser automation:\n');
153
+ REQUIRED_PACKAGES.forEach((pkg, index) => {
154
+ console.log(` ${index + 1}. ${pkg.name} - ${pkg.description}`);
155
+ });
156
+ console.log('');
157
+
158
+ // Ask for confirmation unless skipped
159
+ if (!skipPrompt) {
160
+ console.log('Waiting for user confirmation...');
161
+ const confirmed = await promptUser('Do you want to proceed with the installation? (y/n): ');
162
+ if (!confirmed) {
163
+ console.log('Installation cancelled by user');
164
+ process.exit(0);
165
+ }
166
+ console.log('User confirmed, proceeding with installation');
167
+ } else {
168
+ console.log('Skipping confirmation prompt, proceeding with installation...');
169
+ }
170
+
171
+ console.log('\n📦 Starting installation...\n');
172
+
173
+ try {
174
+ // Check and install uv if needed
175
+ if (!checkCommand('uv --version')) {
176
+ console.log('Installing uv package manager...');
177
+ await runCommand('curl -LsSf https://astral.sh/uv/install.sh | sh', 'uv');
178
+
179
+ // Add uv to PATH for current session
180
+ const uvPath = path.join(process.env.HOME || '', '.cargo', 'bin');
181
+ process.env.PATH = `${uvPath}:${process.env.PATH}`;
182
+ } else {
183
+ console.log('✓ uv is already installed');
184
+ }
185
+
186
+ // Check Python 3.12
187
+ if (!checkCommand('python3.12 --version')) {
188
+ console.error('Python 3.12 is required but not found');
189
+ console.log('Please install Python 3.12 manually:');
190
+ console.log(' macOS: brew install python@3.12');
191
+ console.log(' Ubuntu/Debian: sudo apt install python3.12');
192
+ console.log(' Or visit: https://www.python.org/downloads/');
193
+ process.exit(1);
194
+ } else {
195
+ console.log('✓ Python 3.12 is available');
196
+ }
197
+
198
+ // Create virtual environment
199
+ console.log('Creating Python virtual environment...');
200
+ execSync(`uv venv --python 3.12 ${VENV_DIR}`, { stdio: 'inherit' });
201
+ console.log('✓ Virtual environment created');
202
+
203
+ // Install browser-use in the venv
204
+ console.log('Installing browser-use package...');
205
+ execSync(`uv pip install --python ${path.join(process.cwd(), VENV_DIR, 'bin', 'python')} browser-use`, {
206
+ stdio: 'inherit',
207
+ });
208
+ console.log('✓ browser-use installed');
209
+
210
+ // Install Playwright Chromium
211
+ console.log('Installing Playwright Chromium browser...');
212
+ execSync(`uvx playwright install chromium --with-deps`, { stdio: 'inherit' });
213
+ console.log('✓ Playwright Chromium installed');
214
+
215
+ // Create setup marker file
216
+ const setupMarkerPath = path.join(process.cwd(), SETUP_MARKER_FILE);
217
+ const setupInfo = {
218
+ version: '1.0.0',
219
+ setupDate: new Date().toISOString(),
220
+ packages: REQUIRED_PACKAGES.map((p) => p.name),
221
+ venvPath: VENV_DIR,
222
+ };
223
+ fs.writeFileSync(setupMarkerPath, JSON.stringify(setupInfo, null, 2));
224
+
225
+ // Add to .gitignore if not already there
226
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
227
+ if (fs.existsSync(gitignorePath)) {
228
+ const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
229
+ const itemsToIgnore = [VENV_DIR, SETUP_MARKER_FILE, 'downloads/'];
230
+ const linesToAdd: string[] = [];
231
+
232
+ itemsToIgnore.forEach((item) => {
233
+ if (!gitignoreContent.includes(item)) {
234
+ linesToAdd.push(item);
235
+ }
236
+ });
237
+
238
+ if (linesToAdd.length > 0) {
239
+ const newContent = gitignoreContent.trimEnd() + '\n\n# Minded Local Operator\n' + linesToAdd.join('\n') + '\n';
240
+ fs.writeFileSync(gitignorePath, newContent);
241
+ console.log('✓ Updated .gitignore');
242
+ }
243
+ }
244
+
245
+ // Add BROWSER_TASK_MODE to .env file
246
+ const envPath = path.join(process.cwd(), '.env');
247
+ const browserTaskModeVar = 'BROWSER_TASK_MODE=localRun';
248
+
249
+ if (fs.existsSync(envPath)) {
250
+ const envContent = fs.readFileSync(envPath, 'utf8');
251
+
252
+ // Check if BROWSER_TASK_MODE is already set
253
+ if (!envContent.includes('BROWSER_TASK_MODE')) {
254
+ // Append to existing .env file
255
+ const newEnvContent = envContent.trimEnd() + '\n' + browserTaskModeVar + '\n';
256
+ fs.writeFileSync(envPath, newEnvContent);
257
+ console.log('✓ Added BROWSER_TASK_MODE=localRun to .env');
258
+ } else if (!envContent.includes('BROWSER_TASK_MODE=localRun')) {
259
+ // BROWSER_TASK_MODE exists but with different value, update it
260
+ const updatedEnvContent = envContent.replace(/BROWSER_TASK_MODE=.*$/m, browserTaskModeVar);
261
+ fs.writeFileSync(envPath, updatedEnvContent);
262
+ console.log('✓ Updated BROWSER_TASK_MODE to localRun in .env');
263
+ } else {
264
+ console.log('✓ BROWSER_TASK_MODE=localRun already configured in .env');
265
+ }
266
+ } else {
267
+ // Create new .env file
268
+ fs.writeFileSync(envPath, browserTaskModeVar + '\n');
269
+ console.log('✓ Created .env file with BROWSER_TASK_MODE=localRun');
270
+ }
271
+
272
+ console.log('\n' + '='.repeat(50));
273
+ console.log('✅ Local operator setup completed successfully!');
274
+ console.log('='.repeat(50) + '\n');
275
+
276
+ console.log('You can now use browser automation features locally.');
277
+ console.log('The virtual environment will be activated automatically when needed.');
278
+ } catch (error) {
279
+ console.error('Setup failed:', error);
280
+
281
+ // Clean up partial installation
282
+ try {
283
+ if (fs.existsSync(path.join(process.cwd(), VENV_DIR))) {
284
+ fs.rmSync(path.join(process.cwd(), VENV_DIR), { recursive: true });
285
+ }
286
+ } catch {
287
+ // Ignore cleanup errors
288
+ }
289
+
290
+ process.exit(1);
291
+ }
292
+ }
293
+
294
+ export function validateLocalOperatorSetup(): void {
295
+ if (!isLocalOperatorSetup()) {
296
+ console.error('Local operator is not set up');
297
+ console.log('Please run: npx minded setup-local-operator');
298
+ throw new Error('Local operator not configured');
299
+ }
300
+ }
301
+
302
+ export function getVenvPath(): string {
303
+ return path.join(process.cwd(), VENV_DIR);
304
+ }
305
+
306
+ export function getVenvBinPath(): string {
307
+ return path.join(getVenvPath(), 'bin');
308
+ }
@@ -31,10 +31,10 @@ export const addBrowserTaskNode = async ({ graph, node, agent, llm }: AddBrowser
31
31
  const zodSchema = createZodSchemaFromFields(node.inputSchema);
32
32
 
33
33
  // Create langchain tool
34
- const tool = langchainTool(() => { }, {
34
+ const tool = langchainTool(() => {}, {
35
35
  name: 'browser-task',
36
36
  description: node.prompt,
37
- schema: zodSchema,
37
+ schema: zodSchema as any,
38
38
  });
39
39
 
40
40
  const combinedPlaybooks = combinePlaybooks(agent.playbooks);
@@ -12,6 +12,8 @@ import { LLMProviders } from '../types/LLM.types';
12
12
  import { AIMessage, ToolMessage } from '@langchain/core/messages';
13
13
  import { v4 as uuidv4 } from 'uuid';
14
14
  import { executeRpaStep } from './rpaStepsExecutor';
15
+ import { createBrowserSession } from '../browserTask/executeBrowserTask';
16
+ import { getConfig } from '../platform/config';
15
17
 
16
18
  type AddRpaNodeParams = {
17
19
  graph: PreCompiledGraph;
@@ -29,13 +31,22 @@ export const addRpaNode = async ({ graph, node, agent, llm }: AddRpaNodeParams)
29
31
 
30
32
  let browser: Browser | null = null;
31
33
  let page: Page | null = null;
34
+ const { browserTaskMode } = getConfig();
32
35
 
36
+ const session = await createBrowserSession({
37
+ sessionId: state.sessionId,
38
+ browserTaskMode,
39
+ });
40
+
41
+ if (!session.sessionId || !session.cdpUrl) {
42
+ throw new Error('Failed to create browser session: missing session details');
43
+ }
33
44
  // Create tool call for RPA execution
34
45
  const toolCallId = uuidv4();
35
46
  const aiMessageId = uuidv4();
36
-
47
+ state.cdpUrl = session.cdpUrl;
37
48
  // Get CDP URL from state
38
- const cdpUrl = state.cdpUrl;
49
+ const cdpUrl = session.cdpUrl;
39
50
 
40
51
  const toolCall = {
41
52
  id: toolCallId,
@@ -5,6 +5,15 @@ import { JsonOutputParser } from '@langchain/core/output_parsers';
5
5
  import { SystemMessage } from '@langchain/core/messages';
6
6
  import { BaseLanguageModel } from '@langchain/core/language_models/base';
7
7
 
8
+ // Type guard for checking if LLM supports structured output
9
+ interface StructuredOutputLLM extends BaseLanguageModel {
10
+ withStructuredOutput<T extends z.ZodType>(schema: T): BaseLanguageModel;
11
+ }
12
+
13
+ function supportsStructuredOutput(llm: BaseLanguageModel): llm is StructuredOutputLLM {
14
+ return 'withStructuredOutput' in llm && typeof (llm as any).withStructuredOutput === 'function';
15
+ }
16
+
8
17
  // Type definitions for classifier configuration
9
18
  export interface ClassDefinition {
10
19
  name: string;
@@ -14,53 +23,34 @@ export interface ClassDefinition {
14
23
  export interface ClassifierConfig {
15
24
  classes: ClassDefinition[];
16
25
  systemPrompt?: string;
17
- outputFormat?: 'json' | 'text';
18
- includeReason?: boolean;
19
- defaultClass?: string;
20
- defaultReason?: string;
26
+ defaultClass: string;
27
+ defaultReason: string;
21
28
  }
22
29
 
23
30
  export interface ClassificationResult {
24
31
  class: string;
25
- reason?: string;
26
- confidence?: number;
32
+ reason: string;
33
+ confidence: number;
27
34
  [key: string]: any; // Allow additional fields
28
35
  }
29
36
 
30
- // Default configuration
31
- const DEFAULT_CONFIG: Partial<ClassifierConfig> = {
32
- outputFormat: 'json',
33
- includeReason: true,
34
- defaultClass: 'unknown',
35
- defaultReason: 'Unable to determine classification',
36
- };
37
-
38
37
  /**
39
- * Validates the classification result
40
- * @param result The classification result to validate
41
- * @param config The classifier configuration
42
- * @returns True if the result is valid, false otherwise
38
+ * Create a dynamic Zod schema for classification result based on valid classes
43
39
  */
44
- function validateClassificationResult(result: ClassificationResult, config: ClassifierConfig): boolean {
45
- // Check if result exists and has a class property
46
- if (!result || !result.class) {
47
- return false;
48
- }
40
+ function createClassificationSchema(config: ClassifierConfig): z.ZodSchema {
41
+ const validClassNames = config.classes.map((c) => c.name);
49
42
 
50
- // Check if the class is one of the configured classes
51
- const validClasses = config.classes.map((c) => c.name);
52
- if (!validClasses.includes(result.class)) {
53
- return false;
54
- }
43
+ // Create a union type for valid class names
44
+ const classEnum = z.enum(validClassNames as [string, ...string[]]);
55
45
 
56
- // If we expect JSON format with confidence, validate it
57
- if (config.outputFormat === 'json' && result.confidence !== undefined) {
58
- if (typeof result.confidence !== 'number' || result.confidence < 0 || result.confidence > 1) {
59
- return false;
60
- }
61
- }
46
+ // Build the schema - always include all fields
47
+ const schemaShape: Record<string, z.ZodTypeAny> = {
48
+ class: classEnum.describe('The selected classification category'),
49
+ reason: z.string().describe('Explanation for the classification'),
50
+ confidence: z.number().min(0).max(1).describe('Confidence score between 0 and 1'),
51
+ };
62
52
 
63
- return true;
53
+ return z.object(schemaShape);
64
54
  }
65
55
 
66
56
  /**
@@ -71,65 +61,49 @@ function validateClassificationResult(result: ClassificationResult, config: Clas
71
61
  * @returns The classification result
72
62
  */
73
63
  export async function classify(content: string, config: ClassifierConfig, llm: BaseLanguageModel): Promise<ClassificationResult> {
74
- const mergedConfig = { ...DEFAULT_CONFIG, ...config };
75
64
  const MAX_RETRIES = 3;
76
65
  let lastError: Error | null = null;
77
- let lastInvalidResult: ClassificationResult | null = null;
66
+
67
+ // Check if LLM supports structured output upfront
68
+ if (!supportsStructuredOutput(llm)) {
69
+ throw new Error('LLM does not support structured output, which is required for classification');
70
+ }
78
71
 
79
72
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
80
73
  try {
81
74
  // Build the classification prompt
82
- const classesDescription = mergedConfig.classes.map((c) => `${c.name}: ${c.description}`).join('\n');
83
- const validClassNames = mergedConfig.classes.map((c) => c.name);
75
+ const classesDescription = config.classes.map((c) => `${c.name}: ${c.description}`).join('\n');
84
76
 
85
77
  const basePrompt =
86
- mergedConfig.systemPrompt ||
87
- 'You are a classifier. Your task is to classify the given content into one of the following categories:';
78
+ config.systemPrompt || 'You are a classifier. Your task is to classify the given content into one of the following categories:';
88
79
 
89
80
  let prompt = `${basePrompt}\n\n${classesDescription}\n\n`;
90
81
 
91
82
  // Add retry feedback if this is not the first attempt
92
- if (attempt > 1 && lastInvalidResult) {
93
- prompt += `\n⚠️ IMPORTANT: Your previous classification attempt was INVALID. `;
94
- if (lastInvalidResult.class && !validClassNames.includes(lastInvalidResult.class)) {
95
- prompt += `You selected "${lastInvalidResult.class}" which is NOT in the list of valid classes. `;
96
- } else if (!lastInvalidResult.class) {
97
- prompt += `You did not provide a class value. `;
98
- }
83
+ if (attempt > 1) {
84
+ const validClassNames = config.classes.map((c) => c.name);
85
+ prompt += `\n⚠️ IMPORTANT: Your previous classification attempt failed. `;
99
86
  prompt += `You MUST select ONLY from these valid classes:\n${validClassNames.join(', ')}\n\n`;
100
87
  }
101
88
 
102
- if (mergedConfig.outputFormat === 'json') {
103
- prompt += `You should output the result in the following JSON format: {
104
- "class": "<selected class name>",
105
- ${mergedConfig.includeReason ? '"reason": "<explanation for the classification>",' : ''}
106
- "confidence": <confidence score between 0 and 1>
107
- }.\nReturn JSON and nothing more.`;
108
- } else {
109
- prompt += 'Return only the class name.';
110
- }
89
+ // Create dynamic schema based on valid classes
90
+ const classificationSchema = createClassificationSchema(config);
111
91
 
92
+ // Add content to classify to the prompt
112
93
  prompt += `\n\nContent to classify:\n${content}`;
113
94
 
114
- // Make the classification request
115
- let result: ClassificationResult;
116
- if (mergedConfig.outputFormat === 'json') {
117
- const parser = new JsonOutputParser();
118
- const parsedResult = await llm.pipe(parser).invoke([new SystemMessage(prompt)]);
119
- result = parsedResult as ClassificationResult;
120
- } else {
121
- const llmResult = await llm.invoke([new SystemMessage(prompt)]);
122
- const classText = typeof llmResult.content === 'string' ? llmResult.content.trim() : '';
123
- result = { class: classText };
124
- }
95
+ // Use structured output for guaranteed compliance
96
+ const structuredLLM = llm.withStructuredOutput(classificationSchema);
97
+ const messages = [new SystemMessage(prompt)];
98
+ const result = (await structuredLLM.invoke(messages)) as ClassificationResult;
125
99
 
126
- // Validate the result
127
- if (!validateClassificationResult(result, mergedConfig)) {
128
- lastInvalidResult = result;
129
- throw new Error(`Invalid classification result: ${JSON.stringify(result)}`);
130
- }
100
+ // The result is already validated by withStructuredOutput
101
+ logger.debug({
102
+ message: 'Classification successful with structured output',
103
+ attempt,
104
+ class: result.class,
105
+ });
131
106
 
132
- // Success - return the valid result
133
107
  return result;
134
108
  } catch (err) {
135
109
  lastError = err as Error;
@@ -138,7 +112,6 @@ export async function classify(content: string, config: ClassifierConfig, llm: B
138
112
  attempt,
139
113
  maxRetries: MAX_RETRIES,
140
114
  error: lastError.message,
141
- invalidResult: lastInvalidResult,
142
115
  });
143
116
 
144
117
  // If this is not the last attempt, continue to retry
@@ -153,15 +126,14 @@ export async function classify(content: string, config: ClassifierConfig, llm: B
153
126
  message: 'Classification failed after max retries, using default class',
154
127
  maxRetries: MAX_RETRIES,
155
128
  lastError: lastError?.message,
156
- lastInvalidResult,
157
- defaultClass: mergedConfig.defaultClass,
158
- defaultReason: mergedConfig.defaultReason,
129
+ defaultClass: config.defaultClass,
130
+ defaultReason: config.defaultReason,
159
131
  });
160
132
 
161
133
  // Return default classification
162
134
  return {
163
- class: mergedConfig.defaultClass || 'unknown',
164
- reason: mergedConfig.defaultReason || `Classification failed after ${MAX_RETRIES} attempts: ${lastError?.message}`,
135
+ class: config.defaultClass,
136
+ reason: config.defaultReason,
165
137
  confidence: 0,
166
138
  };
167
139
  }
@@ -169,10 +141,10 @@ export async function classify(content: string, config: ClassifierConfig, llm: B
169
141
  /**
170
142
  * Create a classifier from a simple class list
171
143
  * @param classes Array of class names or [name, description] tuples
172
- * @param options Additional configuration options
144
+ * @param options Configuration options (includeReason, defaultClass, defaultReason are required)
173
145
  * @returns A configured classify function
174
146
  */
175
- export function createClassifier(classes: string[] | [string, string][], options?: Partial<ClassifierConfig>) {
147
+ export function createClassifier(classes: string[] | [string, string][], options: Omit<ClassifierConfig, 'classes'>) {
176
148
  const classDefinitions: ClassDefinition[] = classes.map((c) => {
177
149
  if (typeof c === 'string') {
178
150
  return { name: c, description: '' };
@@ -205,20 +177,18 @@ export const schema = z.object({
205
177
  .optional()
206
178
  .describe('Classes to classify into. Can be strings, [name, description] tuples, or objects with name and description'),
207
179
  systemPrompt: z.string().optional().describe('Custom system prompt for classification'),
208
- includeReason: z.boolean().optional().default(true).describe('Whether to include reasoning in the response'),
209
- outputFormat: z.enum(['json', 'text']).optional().default('json').describe('Output format for the classification'),
210
- defaultClass: z.string().optional().describe('Default class to use if classification fails'),
211
- defaultReason: z.string().optional().describe('Default reason to use if classification fails'),
180
+ defaultClass: z.string().describe('Default class to use if classification fails'),
181
+ defaultReason: z.string().describe('Default reason to use if classification fails'),
212
182
  });
213
183
 
214
184
  const classifierTool: Tool<typeof schema, any> = {
215
185
  name: 'minded-classifier',
216
186
  description:
217
- 'Classify content into predefined categories using AI. Supports custom classes, system prompts, and various output formats. Can be configured with default fallback values.',
187
+ 'Classify content into predefined categories using AI. Supports custom classes and system prompts. Uses structured output for guaranteed schema compliance. Requires default fallback values.',
218
188
  input: schema,
219
189
  isGlobal: false,
220
190
  execute: async ({ input, state, agent }) => {
221
- const { content, classes, systemPrompt, includeReason, outputFormat, defaultClass, defaultReason } = input;
191
+ const { content, classes, systemPrompt, defaultClass, defaultReason } = input;
222
192
 
223
193
  logger.info({
224
194
  message: 'Classifying content',
@@ -253,8 +223,6 @@ const classifierTool: Tool<typeof schema, any> = {
253
223
  const config: ClassifierConfig = {
254
224
  classes: classDefinitions,
255
225
  systemPrompt,
256
- includeReason,
257
- outputFormat,
258
226
  defaultClass,
259
227
  defaultReason,
260
228
  };
@@ -298,46 +266,50 @@ export async function multiClassify(
298
266
  config: ClassifierConfig & { maxClasses?: number },
299
267
  llm: BaseLanguageModel,
300
268
  ): Promise<ClassificationResult[]> {
301
- const mergedConfig = { ...DEFAULT_CONFIG, ...config };
302
- const maxClasses = mergedConfig.maxClasses || 3;
269
+ const maxClasses = config.maxClasses || 3;
270
+
271
+ // Check if LLM supports structured output
272
+ if (!supportsStructuredOutput(llm)) {
273
+ throw new Error('LLM does not support structured output, which is required for multi-classification');
274
+ }
303
275
 
304
276
  try {
305
- const classesDescription = mergedConfig.classes.map((c) => `${c.name}: ${c.description}`).join('\n');
277
+ const classesDescription = config.classes.map((c) => `${c.name}: ${c.description}`).join('\n');
278
+ const validClassNames = config.classes.map((c) => c.name);
306
279
 
307
- const basePrompt =
308
- mergedConfig.systemPrompt || 'You are a multi-label classifier. Select all applicable categories for the given content:';
280
+ const basePrompt = config.systemPrompt || 'You are a multi-label classifier. Select all applicable categories for the given content:';
309
281
 
310
282
  const prompt = `${basePrompt}\n\n${classesDescription}\n\n
311
- You should output the result as a JSON array of up to ${maxClasses} classifications, ordered by relevance:
312
- [
313
- {
314
- "class": "<class name>",
315
- ${mergedConfig.includeReason ? '"reason": "<explanation>",' : ''}
316
- "confidence": <confidence score between 0 and 1>
317
- },
318
- ...
319
- ]
320
- Return JSON and nothing more.
283
+ Select up to ${maxClasses} classifications, ordered by relevance.
321
284
 
322
285
  Content to classify:
323
286
  ${content}`;
324
287
 
325
- const parser = new JsonOutputParser();
326
- const result = await llm.pipe(parser).invoke([new SystemMessage(prompt)]);
288
+ // Create schema for array of classifications
289
+ const classEnum = z.enum(validClassNames as [string, ...string[]]);
290
+ const multiClassSchema = z
291
+ .array(
292
+ z.object({
293
+ class: classEnum.describe('The selected classification category'),
294
+ reason: z.string().describe('Explanation for the classification'),
295
+ confidence: z.number().min(0).max(1).describe('Confidence score between 0 and 1'),
296
+ }),
297
+ )
298
+ .min(1)
299
+ .max(maxClasses);
327
300
 
328
- if (Array.isArray(result)) {
329
- return result as ClassificationResult[];
330
- }
301
+ const structuredLLM = llm.withStructuredOutput(multiClassSchema);
302
+ const messages = [new SystemMessage(prompt)];
303
+ const result = (await structuredLLM.invoke(messages)) as ClassificationResult[];
331
304
 
332
- // If single result returned, wrap in array
333
- return [result as ClassificationResult];
305
+ return result;
334
306
  } catch (err) {
335
307
  logger.error({ message: 'Multi-classification failed', err });
336
308
 
337
309
  return [
338
310
  {
339
- class: mergedConfig.defaultClass || 'unknown',
340
- reason: mergedConfig.defaultReason || (err as Error).message,
311
+ class: config.defaultClass,
312
+ reason: config.defaultReason,
341
313
  confidence: 0,
342
314
  },
343
315
  ];