UNPKG

@ts-dspy/core

Version:

Core library for building type-safe LLM applications with structured input/output signatures, automatic validation, and reasoning patterns within TypeScript

712 lines (687 loc) 28.4 kB
import 'reflect-metadata'; import * as fs from 'fs'; // Symbol keys for decorator metadata const INPUT_FIELDS = Symbol('inputFields'); const OUTPUT_FIELDS = Symbol('outputFields'); function InputField(config = {}) { return function (target, propertyKey) { // Handle both legacy and modern decorator contexts const key = typeof propertyKey === 'object' && propertyKey.name ? String(propertyKey.name) : String(propertyKey); if (!target.constructor[INPUT_FIELDS]) { target.constructor[INPUT_FIELDS] = {}; } target.constructor[INPUT_FIELDS][key] = { description: config.description || `Input field: ${key}`, prefix: config.prefix, type: config.type || 'string', required: config.required !== false }; }; } function OutputField(config = {}) { return function (target, propertyKey) { // Handle both legacy and modern decorator contexts const key = typeof propertyKey === 'object' && propertyKey.name ? String(propertyKey.name) : String(propertyKey); if (!target.constructor[OUTPUT_FIELDS]) { target.constructor[OUTPUT_FIELDS] = {}; } target.constructor[OUTPUT_FIELDS][key] = { description: config.description || `Output field: ${key}`, prefix: config.prefix, type: config.type || 'string', required: config.required !== false }; }; } class Signature { static getInputFields() { return this[INPUT_FIELDS] || {}; } static getOutputFields() { return this[OUTPUT_FIELDS] || {}; } static getPromptFormat() { const inputs = Object.keys(this.getInputFields()); const outputs = Object.keys(this.getOutputFields()); return `${inputs.join(', ')} -> ${outputs.join(', ')}`; } static parseStringSignature(signature) { const [inputPart, outputPart] = signature.split('->').map(s => s.trim()); const parseFields = (part) => { return part.split(',').map(field => { const trimmed = field.trim(); const [name, type] = trimmed.split(':').map(s => s.trim()); return { name, type: type || 'string' }; }); }; const inputFields = parseFields(inputPart); const outputFields = parseFields(outputPart); return { inputs: inputFields.map(f => f.name), outputs: outputFields.map(f => f.name), types: Object.assign(Object.assign({}, Object.fromEntries(inputFields.map(f => [f.name, f.type]))), Object.fromEntries(outputFields.map(f => [f.name, f.type]))) }; } } /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; class DSPyConfig { constructor() { this._cache = true; this._tracing = false; } static getInstance() { if (!DSPyConfig.instance) { DSPyConfig.instance = new DSPyConfig(); } return DSPyConfig.instance; } static configure(options) { const config = DSPyConfig.getInstance(); if (options.lm) config._defaultLM = options.lm; if (options.cache !== undefined) config._cache = options.cache; if (options.tracing !== undefined) config._tracing = options.tracing; } static getDefaultLM() { const config = DSPyConfig.getInstance(); if (!config._defaultLM) { throw new Error('No default language model configured. Call configure({ lm: ... }) first.'); } return config._defaultLM; } static isCacheEnabled() { return DSPyConfig.getInstance()._cache; } static isTracingEnabled() { return DSPyConfig.getInstance()._tracing; } } const configure = DSPyConfig.configure; const getDefaultLM = DSPyConfig.getDefaultLM; const isCacheEnabled = DSPyConfig.isCacheEnabled; const isTracingEnabled = DSPyConfig.isTracingEnabled; class Module { constructor(signature, lm) { this._compiled = false; this.signature = signature; this.lm = lm || getDefaultLM(); } // Make modules callable like functions __call__(inputs) { return __awaiter(this, void 0, void 0, function* () { return this.forward(inputs); }); } // Enable direct call syntax call(inputs) { return __awaiter(this, void 0, void 0, function* () { return this.forward(inputs); }); } save(path) { return __awaiter(this, void 0, void 0, function* () { const serialized = { type: this.constructor.name, signature: this.signature, compiled: this._compiled, // Add any module-specific state here }; yield fs.promises.writeFile(path, JSON.stringify(serialized, null, 2)); }); } static load(path) { return __awaiter(this, void 0, void 0, function* () { JSON.parse(yield fs.promises.readFile(path, 'utf-8')); // Implementation depends on module registry throw new Error('Module loading not implemented yet'); }); } get compiled() { return this._compiled; } } class Prediction { constructor(data, trace) { this._data = data; this.trace = trace; // Make all data properties accessible directly on the prediction Object.keys(data).forEach(key => { Object.defineProperty(this, key, { get: () => this._data[key], enumerable: true, configurable: true }); }); } get(key) { return this._data[key]; } toObject() { return Object.assign({}, this._data); } toString() { return JSON.stringify(this._data, null, 2); } } class Example { constructor(data) { this._data = Object.assign({}, data); // Make all data properties accessible directly Object.keys(data).forEach(key => { Object.defineProperty(this, key, { get: () => this._data[key], set: (value) => { this._data[key] = value; }, enumerable: true, configurable: true }); }); } get(key) { return this._data[key]; } set(key, value) { this._data[key] = value; Object.defineProperty(this, key, { get: () => this._data[key], set: (val) => { this._data[key] = val; }, enumerable: true, configurable: true }); } withInputs(...inputKeys) { const newExample = new Example(this._data); newExample._inputKeys = inputKeys; return newExample; } getInputs() { if (!this._inputKeys) { throw new Error('Input keys not specified. Use withInputs() first.'); } const inputs = {}; this._inputKeys.forEach(key => { inputs[key] = this._data[key]; }); return inputs; } getOutputs() { if (!this._inputKeys) { throw new Error('Input keys not specified. Use withInputs() first.'); } const outputs = {}; Object.keys(this._data).forEach(key => { if (!this._inputKeys.includes(key)) { outputs[key] = this._data[key]; } }); return outputs; } toObject() { return Object.assign({}, this._data); } } function buildPrompt(signature, inputs) { if (typeof signature === 'string') { return buildPromptFromString(signature, inputs); } else { return buildPromptFromClass(signature, inputs); } } function buildPromptFromString(signatureStr, inputs) { const parsed = Signature.parseStringSignature(signatureStr); let prompt = ''; // Add input fields for (const inputKey of parsed.inputs) { if (inputs[inputKey] !== undefined) { prompt += `${inputKey}: ${inputs[inputKey]}\n`; } } // Add output format - be more explicit about the format expected if (parsed.outputs.length === 1) { // For single output, make it clear the format expected const outputKey = parsed.outputs[0]; prompt += `\nProvide the ${outputKey} in this format:\n${outputKey}: [your response]`; } else { // For multiple outputs, be explicit about each field prompt += '\nProvide the following fields:\n'; for (const outputKey of parsed.outputs) { const typeInfo = parsed.types[outputKey] ? ` (${parsed.types[outputKey]})` : ''; prompt += `${outputKey}${typeInfo}: [your response]\n`; } } return prompt.trim(); } function buildPromptFromClass(signatureClass, inputs) { const inputFields = signatureClass.getInputFields(); const outputFields = signatureClass.getOutputFields(); let prompt = ''; // Add description if available if (signatureClass.description) { prompt += `${signatureClass.description}\n\n`; } // Add input fields Object.entries(inputFields).forEach(([key, config]) => { if (inputs[key] !== undefined) { const prefix = config.prefix || `${key}:`; prompt += `${prefix} ${inputs[key]}\n`; } }); // Add output format prompt += '\nProvide:\n'; Object.entries(outputFields).forEach(([key, config]) => { const desc = config.description ? ` (${config.description})` : ''; prompt += `${key}${desc}:\n`; }); return prompt.trim(); } function parseOutput(signature, rawOutput) { if (typeof signature === 'string') { return parseOutputFromString(signature, rawOutput); } else { return parseOutputFromClass(signature, rawOutput); } } function parseOutputFromString(signatureStr, rawOutput) { const parsed = Signature.parseStringSignature(signatureStr); const result = {}; for (const outputKey of parsed.outputs) { const value = extractFieldValue(rawOutput, outputKey, parsed.types[outputKey]); result[outputKey] = value; // Always set the field, even if null } return result; } function parseOutputFromClass(signatureClass, rawOutput) { const outputFields = signatureClass.getOutputFields(); const result = {}; Object.entries(outputFields).forEach(([key, config]) => { const value = extractFieldValue(rawOutput, key, config.type); result[key] = value; // Always set the field, even if null }); return result; } function extractFieldValue(text, fieldName, fieldType) { // Ensure text is a string if (typeof text !== 'string') { text = String(text); } // If the text is a simple value without field names, and we're looking for "answer", just return the text if (fieldName === 'answer' && !text.includes(':') && !text.includes('\n')) { // Simple single-line response without field names - treat as the answer let value = text.trim(); value = value.replace(/^\*+|\*+$/g, ''); // Remove asterisks value = value.replace(/^["']|["']$/g, ''); // Remove quotes if (value) { return convertValue(value, fieldType); } } // Try multiple patterns for field extraction - more flexible patterns const patterns = [ // Standard format: "fieldName: value" new RegExp(`${fieldName}\\s*:\\s*(.+?)(?=\\n\\s*\\w+\\s*:|$)`, 'is'), // Alternative format: "fieldName = value" or "fieldName value" new RegExp(`${fieldName}\\s*[=:]\\s*(.+?)(?=\\n|$)`, 'is'), // More flexible: field name followed by content new RegExp(`\\b${fieldName}\\b[:\\s=]*([^\\n]+)`, 'i'), // Try to find the value on the same line after the field name new RegExp(`${fieldName}[:\\s]*([^\\n]+?)(?=\\n|$)`, 'i'), // Very loose pattern - just look for anything after the field name new RegExp(`${fieldName}[^\\w]*([\\s\\S]*?)(?=\\n\\s*[A-Z]|$)`, 'i') ]; for (const pattern of patterns) { const match = text.match(pattern); if (match && match[1]) { let value = match[1].trim(); // Remove common artifacts value = value.replace(/^\*+|\*+$/g, ''); // Remove asterisks value = value.replace(/^["']|["']$/g, ''); // Remove quotes value = value.trim(); if (value) { // Type conversion based on field type return convertValue(value, fieldType); } } } return null; } function convertValue(value, type) { if (!type || type === 'string') { // Auto-detect JSON for untyped fields if ((value.startsWith('{') && value.endsWith('}')) || (value.startsWith('[') && value.endsWith(']'))) { try { return JSON.parse(value); } catch (_a) { return value; } } return value; } switch (type.toLowerCase()) { case 'number': case 'float': const num = parseFloat(value); return isNaN(num) ? value : num; case 'int': case 'integer': const int = parseInt(value); return isNaN(int) ? value : int; case 'boolean': case 'bool': return value.toLowerCase() === 'true' || value === '1'; case 'array': case 'list': try { return JSON.parse(value); } catch (_b) { // Try to split by common delimiters return value.split(/[,;\n]/).map(s => s.trim()).filter(s => s); } case 'object': case 'json': try { return JSON.parse(value); } catch (_c) { return value; } default: // Try to auto-detect and parse JSON if ((value.startsWith('{') && value.endsWith('}')) || (value.startsWith('[') && value.endsWith(']'))) { try { return JSON.parse(value); } catch (_d) { return value; } } return value; } } class Predict extends Module { constructor(signature, lm) { super(signature, lm); } forward(inputs) { return __awaiter(this, void 0, void 0, function* () { try { const prompt = this.buildPrompt(inputs); const rawOutput = yield this.lm.generate(prompt); const parsed = this.parseOutput(rawOutput); return new Prediction(parsed); } catch (error) { throw new Error(`Prediction failed: ${error instanceof Error ? error.message : String(error)}`); } }); } buildPrompt(inputs) { if (!this.signature) { throw new Error('No signature provided'); } return buildPrompt(this.signature, inputs); } parseOutput(rawOutput) { if (!this.signature) { throw new Error('No signature provided'); } return parseOutput(this.signature, rawOutput); } } class ChainOfThought extends Predict { forward(inputs) { return __awaiter(this, void 0, void 0, function* () { try { // Step 1: Generate reasoning const reasoningPrompt = this.buildReasoningPrompt(inputs); const reasoning = yield this.lm.generate(reasoningPrompt); // Step 2: Generate final answer with reasoning const finalPrompt = this.buildFinalPrompt(inputs, reasoning); const finalOutput = yield this.lm.generate(finalPrompt); // parseOutput returns Record<string, any>, cast to base output shape const parsed = this.parseOutput(finalOutput); // Combine parsed output with the reasoning const combinedOutput = Object.assign(Object.assign({}, parsed), { reasoning }); return new Prediction(combinedOutput); } catch (error) { throw new Error(`ChainOfThought failed: ${error instanceof Error ? error.message : String(error)}`); } }); } buildReasoningPrompt(inputs) { const basePrompt = this.buildPrompt(inputs); return `${basePrompt}\n\nLet's think step by step. Please provide your reasoning:`; } buildFinalPrompt(inputs, reasoning) { const basePrompt = this.buildPrompt(inputs); return `${basePrompt}\n\nReasoning: ${reasoning}\n\nBased on this reasoning, provide your final answer:`; } } class RespAct extends Module { constructor(signature, options) { super(signature); // Normalize tools to always have descriptions this.tools = {}; for (const [name, tool] of Object.entries(options.tools)) { if (typeof tool === 'function') { // Legacy support: function without description this.tools[name] = { description: `Tool: ${name}`, function: tool }; } else { // New format: tool with description this.tools[name] = tool; } } this.maxSteps = options.maxSteps || 6; } forward(inputs) { return __awaiter(this, void 0, void 0, function* () { let conversation = this.buildInitialPrompt(inputs); let finalAnswerData = {}; let previousToolCalls = []; // Track previous tool calls to avoid loops for (let step = 0; step < this.maxSteps; step++) { const response = yield this.lm.generate(conversation + '\n\nThought:'); conversation += `\n\nThought: ${response}`; // Check for tool usage FIRST (before checking for final answer) const toolCall = this.extractToolCall(response); if (toolCall) { // Check for repeated tool calls to prevent loops const toolCallKey = `${toolCall.tool}:${toolCall.input}`; if (previousToolCalls.includes(toolCallKey)) { console.warn('🔄 WARNING: Detected repeated tool call, encouraging final answer...'); conversation += `\n\nObservation: You have already made this tool call. Please move to the next step.`; continue; } previousToolCalls.push(toolCallKey); try { const observation = yield this.executeTool(toolCall.tool, toolCall.input); conversation += `\n\nObservation: ${observation}`; continue; // Continue to next step after tool execution } catch (error) { conversation += `\n\nObservation: Error - ${error}`; continue; // Continue even after error } } // Check for final answer only if no tool call was found if (response.toLowerCase().includes('final answer:')) { const rawAnswer = this.extractFinalAnswer(response); let parsedFromSignature = {}; try { parsedFromSignature = this.parseOutput(rawAnswer); } catch (error) { console.warn('Failed to parse output from signature, using raw answer:', error); parsedFromSignature = {}; } // For structured signatures, check if we have all required fields if (typeof this.signature !== 'string' && this.signature) { const outputFields = this.signature.getOutputFields(); const requiredFields = Object.keys(outputFields); const providedFields = Object.keys(parsedFromSignature).filter(key => parsedFromSignature[key] !== null && parsedFromSignature[key] !== undefined); // Temporarily relaxed validation - accept if we have at least some structured data if (providedFields.length === 0) { conversation += `\n\nObservation: Your Final Answer needs to include structured fields: ${requiredFields.join(', ')}. Please provide a Final Answer with the required format.`; continue; } } // Ensure we have meaningful output const parsedKeys = Object.keys(parsedFromSignature); const hasValidParsedData = parsedKeys.some(key => parsedFromSignature[key] !== null && parsedFromSignature[key] !== undefined); if (!hasValidParsedData || parsedKeys.length === 0) { // If parsing failed or returned empty, create answer from raw finalAnswerData = { answer: rawAnswer }; } else { // If we have parsed data but missing answer field, add it if (!('answer' in parsedFromSignature) && rawAnswer !== null) { finalAnswerData = Object.assign(Object.assign({}, parsedFromSignature), { answer: rawAnswer }); } else { finalAnswerData = parsedFromSignature; } } const combinedOutput = Object.assign(Object.assign({}, finalAnswerData), { steps: step + 1 }); return new Prediction(combinedOutput); } } throw new Error(`RespAct exceeded maximum steps (${this.maxSteps}) without finding answer`); }); } parseOutput(rawOutput) { if (!this.signature) { throw new Error('No signature provided for RespAct parsing'); } // Ensure rawOutput is a string for parsing let outputText; if (typeof rawOutput === 'string') { outputText = rawOutput; } else { // Convert non-string values to string for parsing outputText = JSON.stringify(rawOutput); } return parseOutput(this.signature, outputText); } buildInitialPrompt(inputs) { // Build tool descriptions const toolDescriptions = Object.entries(this.tools) .map(([name, tool]) => `- ${name}: ${tool.description}`) .join('\n'); // Check if we have a structured signature let outputFormatInstruction = ''; if (typeof this.signature !== 'string' && this.signature) { const outputFields = this.signature.getOutputFields(); const fieldNames = Object.keys(outputFields); if (fieldNames.length > 0) { outputFormatInstruction = `\n\nIMPORTANT: When providing your Final Answer, you MUST provide ALL the following fields in this EXACT format:\n\n`; fieldNames.forEach(field => { outputFormatInstruction += `${field}: [your response for ${field}]\n`; }); } } return `You have access to the following tools: ${toolDescriptions} Question: ${inputs.question || JSON.stringify(inputs)} IMPORTANT: You MUST use the available tools to solve this question. Do not attempt to answer directly without using tools when tools are available for the task. CRITICAL: Take ONE action at a time. After each action, you will receive an observation. Do NOT plan multiple actions in advance. Work systematically: 1. Gather all necessary data using tools 2. Perform calculations if needed 3. When you have ALL the information needed to answer the question completely, provide your Final Answer Use this EXACT format (DO NOT generate the Observation line - it will be provided automatically): Thought: [your reasoning about what to do next] Action: [tool name] Action Input: [input to the tool] After you receive the Observation, you can then decide your next action. When you have gathered ALL necessary information through tool usage, provide: Thought: [reasoning that you now have everything needed] Final Answer: [complete answer to the original question using all gathered information]${outputFormatInstruction} Begin! Remember: ONE action at a time, then decide if you need more information or can provide the final answer.`; } extractToolCall(response) { // Look for Action and Action Input patterns, handling multiline responses const actionMatch = response.match(/Action:\s*(.+?)(?=\n|$)/m); const inputMatch = response.match(/Action Input:\s*(.+?)(?=\n|$)/m); if (actionMatch && inputMatch) { return { tool: actionMatch[1].trim(), input: inputMatch[1].trim() }; } return null; } executeTool(toolName, input) { return __awaiter(this, void 0, void 0, function* () { if (!(toolName in this.tools)) { return `Error: Tool '${toolName}' not found. Available tools: ${Object.keys(this.tools).join(', ')}`; } try { const result = yield this.tools[toolName].function(input); return String(result); } catch (error) { return `Error executing ${toolName}: ${error}`; } }); } extractFinalAnswer(response) { const match = response.match(/Final Answer:\s*(.+?)$/m); if (match) { const answer = match[1].trim(); // Try to parse as number const num = Number(answer); if (!isNaN(num)) return num; // Try to parse as JSON try { return JSON.parse(answer); } catch (_a) { return answer; } } return null; } } export { ChainOfThought, Example, InputField, Module, OutputField, Predict, Prediction, RespAct, Signature, buildPrompt, configure, getDefaultLM, isCacheEnabled, isTracingEnabled, parseOutput }; //# sourceMappingURL=index.esm.js.map