recursive-llm-ts 3.0.1 → 4.0.0
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 +101 -1
- package/bin/rlm-go +0 -0
- package/dist/bridge-interface.d.ts +2 -1
- package/dist/coordinator.d.ts +17 -0
- package/dist/coordinator.js +45 -0
- package/dist/go-bridge.js +8 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/rlm.d.ts +7 -0
- package/dist/rlm.js +76 -0
- package/dist/structured-types.d.ts +26 -0
- package/dist/structured-types.js +2 -0
- package/go/cmd/rlm/main.go +46 -14
- package/go/internal/rlm/structured.go +403 -0
- package/go/internal/rlm/types.go +30 -5
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -11,7 +11,8 @@ TypeScript/JavaScript package for [Recursive Language Models (RLM)](https://gith
|
|
|
11
11
|
💾 **3x Less Memory** - Efficient Go implementation
|
|
12
12
|
📦 **Single Binary** - Easy distribution and deployment
|
|
13
13
|
🔄 **Unbounded Context** - Process 10M+ tokens without degradation
|
|
14
|
-
🎯 **Provider Agnostic** - Works with OpenAI, Anthropic, Azure, Bedrock, local models
|
|
14
|
+
🎯 **Provider Agnostic** - Works with OpenAI, Anthropic, Azure, Bedrock, local models
|
|
15
|
+
🔍 **Structured Outputs** - Extract typed data with Zod schemas and parallel execution
|
|
15
16
|
|
|
16
17
|
## Installation
|
|
17
18
|
|
|
@@ -71,6 +72,83 @@ console.log(result.result);
|
|
|
71
72
|
console.log('Stats:', result.stats);
|
|
72
73
|
```
|
|
73
74
|
|
|
75
|
+
### Structured Outputs with Zod Schemas
|
|
76
|
+
|
|
77
|
+
Extract structured, typed data from any context using Zod schemas. Supports complex nested objects, arrays, enums, and automatic parallel execution for performance.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { RLM } from 'recursive-llm-ts';
|
|
81
|
+
import { z } from 'zod';
|
|
82
|
+
|
|
83
|
+
const rlm = new RLM('gpt-4o-mini', {
|
|
84
|
+
api_key: process.env.OPENAI_API_KEY
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Define your schema
|
|
88
|
+
const sentimentSchema = z.object({
|
|
89
|
+
sentimentValue: z.number().min(1).max(5),
|
|
90
|
+
sentimentExplanation: z.string(),
|
|
91
|
+
keyPhrases: z.array(z.object({
|
|
92
|
+
phrase: z.string(),
|
|
93
|
+
sentiment: z.number()
|
|
94
|
+
})),
|
|
95
|
+
topics: z.array(z.enum(['pricing', 'features', 'support', 'competition']))
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Extract structured data
|
|
99
|
+
const result = await rlm.structuredCompletion(
|
|
100
|
+
'Analyze the sentiment and extract key information',
|
|
101
|
+
callTranscript,
|
|
102
|
+
sentimentSchema
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// result.result is fully typed!
|
|
106
|
+
console.log(result.result.sentimentValue); // number
|
|
107
|
+
console.log(result.result.keyPhrases); // Array<{phrase: string, sentiment: number}>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Key Benefits:**
|
|
111
|
+
- ✅ **Type-safe** - Full TypeScript types from your Zod schema
|
|
112
|
+
- ✅ **Automatic validation** - Retries with error feedback if schema doesn't match
|
|
113
|
+
- ✅ **Parallel execution** - Complex schemas processed in parallel with goroutines (3-5x faster)
|
|
114
|
+
- ✅ **Deep nesting** - Supports arbitrarily nested objects and arrays
|
|
115
|
+
- ✅ **Enum support** - Validates enum values automatically
|
|
116
|
+
|
|
117
|
+
**Performance Options:**
|
|
118
|
+
```typescript
|
|
119
|
+
// Enable/disable parallel execution
|
|
120
|
+
const result = await rlm.structuredCompletion(
|
|
121
|
+
query,
|
|
122
|
+
context,
|
|
123
|
+
schema,
|
|
124
|
+
{
|
|
125
|
+
parallelExecution: true, // default: true for complex schemas
|
|
126
|
+
maxRetries: 3 // default: 3
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Agent Coordinator (Advanced)
|
|
132
|
+
|
|
133
|
+
For complex multi-field schemas, use the coordinator API:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import { RLMAgentCoordinator } from 'recursive-llm-ts';
|
|
137
|
+
|
|
138
|
+
const coordinator = new RLMAgentCoordinator(
|
|
139
|
+
'gpt-4o-mini',
|
|
140
|
+
{ api_key: process.env.OPENAI_API_KEY },
|
|
141
|
+
'auto',
|
|
142
|
+
{ parallelExecution: true }
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const result = await coordinator.processComplex(
|
|
146
|
+
'Extract comprehensive call analysis',
|
|
147
|
+
transcript,
|
|
148
|
+
complexSchema
|
|
149
|
+
);
|
|
150
|
+
```
|
|
151
|
+
|
|
74
152
|
### Bridge Selection
|
|
75
153
|
|
|
76
154
|
The package automatically uses the Go binary by default (if available). You can explicitly specify a bridge if needed:
|
|
@@ -123,6 +201,28 @@ Process a query with the given context using recursive language models.
|
|
|
123
201
|
**Returns:**
|
|
124
202
|
- `Promise<RLMResult>`: Result containing the answer and statistics
|
|
125
203
|
|
|
204
|
+
#### `structuredCompletion<T>(query: string, context: string, schema: ZodSchema<T>, options?): Promise<StructuredRLMResult<T>>`
|
|
205
|
+
|
|
206
|
+
Extract structured, typed data from context using a Zod schema.
|
|
207
|
+
|
|
208
|
+
**Parameters:**
|
|
209
|
+
- `query`: The extraction task to perform
|
|
210
|
+
- `context`: The context/document to process
|
|
211
|
+
- `schema`: Zod schema defining the output structure
|
|
212
|
+
- `options`: Optional configuration
|
|
213
|
+
- `parallelExecution?: boolean` - Enable parallel processing (default: true)
|
|
214
|
+
- `maxRetries?: number` - Max validation retries (default: 3)
|
|
215
|
+
|
|
216
|
+
**Returns:**
|
|
217
|
+
- `Promise<StructuredRLMResult<T>>`: Typed result matching your schema
|
|
218
|
+
|
|
219
|
+
**Example:**
|
|
220
|
+
```typescript
|
|
221
|
+
const schema = z.object({ score: z.number(), summary: z.string() });
|
|
222
|
+
const result = await rlm.structuredCompletion('Analyze', doc, schema);
|
|
223
|
+
// result.result is typed as { score: number, summary: string }
|
|
224
|
+
```
|
|
225
|
+
|
|
126
226
|
#### `cleanup(): Promise<void>`
|
|
127
227
|
|
|
128
228
|
Clean up the bridge and free resources.
|
package/bin/rlm-go
CHANGED
|
Binary file
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { RLMConfig } from './bridge-interface';
|
|
3
|
+
import { BridgeType } from './bridge-factory';
|
|
4
|
+
import { StructuredRLMResult, CoordinatorConfig } from './structured-types';
|
|
5
|
+
export declare class RLMAgentCoordinator {
|
|
6
|
+
private rlm;
|
|
7
|
+
private config;
|
|
8
|
+
constructor(model: string, rlmConfig?: RLMConfig, bridgeType?: BridgeType, coordinatorConfig?: CoordinatorConfig);
|
|
9
|
+
/**
|
|
10
|
+
* Process a complex query with structured output using schema decomposition
|
|
11
|
+
*/
|
|
12
|
+
processComplex<T>(query: string, context: string, schema: z.ZodSchema<T>): Promise<StructuredRLMResult<T>>;
|
|
13
|
+
/**
|
|
14
|
+
* Clean up resources
|
|
15
|
+
*/
|
|
16
|
+
cleanup(): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.RLMAgentCoordinator = void 0;
|
|
13
|
+
const rlm_1 = require("./rlm");
|
|
14
|
+
class RLMAgentCoordinator {
|
|
15
|
+
constructor(model, rlmConfig = {}, bridgeType = 'auto', coordinatorConfig = {}) {
|
|
16
|
+
var _a, _b, _c;
|
|
17
|
+
this.rlm = new rlm_1.RLM(model, rlmConfig, bridgeType);
|
|
18
|
+
this.config = {
|
|
19
|
+
parallelExecution: (_a = coordinatorConfig.parallelExecution) !== null && _a !== void 0 ? _a : true,
|
|
20
|
+
maxRetries: (_b = coordinatorConfig.maxRetries) !== null && _b !== void 0 ? _b : 3,
|
|
21
|
+
progressiveValidation: (_c = coordinatorConfig.progressiveValidation) !== null && _c !== void 0 ? _c : true
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Process a complex query with structured output using schema decomposition
|
|
26
|
+
*/
|
|
27
|
+
processComplex(query, context, schema) {
|
|
28
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
29
|
+
// Delegate to RLM which now handles everything in Go
|
|
30
|
+
return this.rlm.structuredCompletion(query, context, schema, {
|
|
31
|
+
maxRetries: this.config.maxRetries,
|
|
32
|
+
parallelExecution: this.config.parallelExecution
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Clean up resources
|
|
38
|
+
*/
|
|
39
|
+
cleanup() {
|
|
40
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
yield this.rlm.cleanup();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.RLMAgentCoordinator = RLMAgentCoordinator;
|
package/dist/go-bridge.js
CHANGED
|
@@ -57,7 +57,7 @@ exports.GoBridge = void 0;
|
|
|
57
57
|
const fs = __importStar(require("fs"));
|
|
58
58
|
const path = __importStar(require("path"));
|
|
59
59
|
const child_process_1 = require("child_process");
|
|
60
|
-
const DEFAULT_BINARY_NAME = process.platform === 'win32' ? 'rlm.exe' : 'rlm';
|
|
60
|
+
const DEFAULT_BINARY_NAME = process.platform === 'win32' ? 'rlm-go.exe' : 'rlm-go';
|
|
61
61
|
function resolveBinaryPath(rlmConfig) {
|
|
62
62
|
const configuredPath = rlmConfig.go_binary_path || process.env.RLM_GO_BINARY;
|
|
63
63
|
if (configuredPath) {
|
|
@@ -65,8 +65,8 @@ function resolveBinaryPath(rlmConfig) {
|
|
|
65
65
|
}
|
|
66
66
|
// Try multiple locations
|
|
67
67
|
const possiblePaths = [
|
|
68
|
-
path.join(__dirname, '..', '
|
|
69
|
-
path.join(__dirname, '..', '
|
|
68
|
+
path.join(__dirname, '..', 'bin', DEFAULT_BINARY_NAME), // NPM package (primary)
|
|
69
|
+
path.join(__dirname, '..', 'go', DEFAULT_BINARY_NAME), // Development fallback
|
|
70
70
|
];
|
|
71
71
|
for (const p of possiblePaths) {
|
|
72
72
|
if (fs.existsSync(p)) {
|
|
@@ -82,19 +82,21 @@ function assertBinaryExists(binaryPath) {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
function sanitizeConfig(config) {
|
|
85
|
-
const { pythonia_timeout, go_binary_path } = config, sanitized = __rest(config, ["pythonia_timeout", "go_binary_path"]);
|
|
86
|
-
return sanitized;
|
|
85
|
+
const { pythonia_timeout, go_binary_path, structured } = config, sanitized = __rest(config, ["pythonia_timeout", "go_binary_path", "structured"]);
|
|
86
|
+
return { config: sanitized, structured };
|
|
87
87
|
}
|
|
88
88
|
class GoBridge {
|
|
89
89
|
completion(model_1, query_1, context_1) {
|
|
90
90
|
return __awaiter(this, arguments, void 0, function* (model, query, context, rlmConfig = {}) {
|
|
91
91
|
const binaryPath = resolveBinaryPath(rlmConfig);
|
|
92
92
|
assertBinaryExists(binaryPath);
|
|
93
|
+
const { config, structured } = sanitizeConfig(rlmConfig);
|
|
93
94
|
const payload = JSON.stringify({
|
|
94
95
|
model,
|
|
95
96
|
query,
|
|
96
97
|
context,
|
|
97
|
-
config
|
|
98
|
+
config,
|
|
99
|
+
structured
|
|
98
100
|
});
|
|
99
101
|
return new Promise((resolve, reject) => {
|
|
100
102
|
const child = (0, child_process_1.spawn)(binaryPath, [], { stdio: ['pipe', 'pipe', 'pipe'] });
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RLM = void 0;
|
|
3
|
+
exports.RLMAgentCoordinator = exports.RLM = void 0;
|
|
4
4
|
var rlm_1 = require("./rlm");
|
|
5
5
|
Object.defineProperty(exports, "RLM", { enumerable: true, get: function () { return rlm_1.RLM; } });
|
|
6
|
+
var coordinator_1 = require("./coordinator");
|
|
7
|
+
Object.defineProperty(exports, "RLMAgentCoordinator", { enumerable: true, get: function () { return coordinator_1.RLMAgentCoordinator; } });
|
package/dist/rlm.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { RLMConfig, RLMResult } from './bridge-interface';
|
|
2
2
|
import { BridgeType } from './bridge-factory';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { StructuredRLMResult } from './structured-types';
|
|
3
5
|
export declare class RLM {
|
|
4
6
|
private bridge;
|
|
5
7
|
private model;
|
|
@@ -8,5 +10,10 @@ export declare class RLM {
|
|
|
8
10
|
constructor(model: string, rlmConfig?: RLMConfig, bridgeType?: BridgeType);
|
|
9
11
|
private ensureBridge;
|
|
10
12
|
completion(query: string, context: string): Promise<RLMResult>;
|
|
13
|
+
structuredCompletion<T>(query: string, context: string, schema: z.ZodSchema<T>, options?: {
|
|
14
|
+
maxRetries?: number;
|
|
15
|
+
parallelExecution?: boolean;
|
|
16
|
+
}): Promise<StructuredRLMResult<T>>;
|
|
17
|
+
private zodToJsonSchema;
|
|
11
18
|
cleanup(): Promise<void>;
|
|
12
19
|
}
|
package/dist/rlm.js
CHANGED
|
@@ -32,6 +32,82 @@ class RLM {
|
|
|
32
32
|
return bridge.completion(this.model, query, context, this.rlmConfig);
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
+
structuredCompletion(query_1, context_1, schema_1) {
|
|
36
|
+
return __awaiter(this, arguments, void 0, function* (query, context, schema, options = {}) {
|
|
37
|
+
var _a, _b;
|
|
38
|
+
const bridge = yield this.ensureBridge();
|
|
39
|
+
const jsonSchema = this.zodToJsonSchema(schema);
|
|
40
|
+
const structuredConfig = {
|
|
41
|
+
schema: jsonSchema,
|
|
42
|
+
parallelExecution: (_a = options.parallelExecution) !== null && _a !== void 0 ? _a : true,
|
|
43
|
+
maxRetries: (_b = options.maxRetries) !== null && _b !== void 0 ? _b : 3
|
|
44
|
+
};
|
|
45
|
+
const result = yield bridge.completion(this.model, query, context, Object.assign(Object.assign({}, this.rlmConfig), { structured: structuredConfig }));
|
|
46
|
+
// Validate result against Zod schema for type safety
|
|
47
|
+
const validated = schema.parse(result.result);
|
|
48
|
+
return {
|
|
49
|
+
result: validated,
|
|
50
|
+
stats: result.stats
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
zodToJsonSchema(schema) {
|
|
55
|
+
const def = schema._def;
|
|
56
|
+
// Check for object type by presence of shape
|
|
57
|
+
if (def.shape) {
|
|
58
|
+
const shape = def.shape;
|
|
59
|
+
const properties = {};
|
|
60
|
+
const required = [];
|
|
61
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
62
|
+
properties[key] = this.zodToJsonSchema(value);
|
|
63
|
+
if (!value.isOptional()) {
|
|
64
|
+
required.push(key);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties,
|
|
70
|
+
required: required.length > 0 ? required : undefined
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Check for array type - Zod arrays have an 'element' property (or 'type' in older versions)
|
|
74
|
+
if (def.type === 'array' && (def.element || def.type)) {
|
|
75
|
+
const itemSchema = def.element || def.type;
|
|
76
|
+
return {
|
|
77
|
+
type: 'array',
|
|
78
|
+
items: this.zodToJsonSchema(itemSchema)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Check for enum - Zod enums have a 'type' of 'enum' and 'entries' object
|
|
82
|
+
if (def.type === 'enum' && def.entries) {
|
|
83
|
+
return {
|
|
84
|
+
type: 'string',
|
|
85
|
+
enum: Object.keys(def.entries)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Check for legacy enum with values array
|
|
89
|
+
if (def.values && Array.isArray(def.values)) {
|
|
90
|
+
return {
|
|
91
|
+
type: 'string',
|
|
92
|
+
enum: def.values
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Check for optional/nullable
|
|
96
|
+
if (def.innerType) {
|
|
97
|
+
const inner = this.zodToJsonSchema(def.innerType);
|
|
98
|
+
return def.typeName === 'ZodNullable' ? Object.assign(Object.assign({}, inner), { nullable: true }) : inner;
|
|
99
|
+
}
|
|
100
|
+
// Detect primitive types
|
|
101
|
+
const defType = def.type;
|
|
102
|
+
if (defType === 'string')
|
|
103
|
+
return { type: 'string' };
|
|
104
|
+
if (defType === 'number')
|
|
105
|
+
return { type: 'number' };
|
|
106
|
+
if (defType === 'boolean')
|
|
107
|
+
return { type: 'boolean' };
|
|
108
|
+
// Default fallback
|
|
109
|
+
return { type: 'string' };
|
|
110
|
+
}
|
|
35
111
|
cleanup() {
|
|
36
112
|
return __awaiter(this, void 0, void 0, function* () {
|
|
37
113
|
if (this.bridge) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export interface StructuredRLMResult<T> {
|
|
3
|
+
result: T;
|
|
4
|
+
stats: {
|
|
5
|
+
llm_calls: number;
|
|
6
|
+
iterations: number;
|
|
7
|
+
depth: number;
|
|
8
|
+
parsing_retries?: number;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface SubTask {
|
|
12
|
+
id: string;
|
|
13
|
+
query: string;
|
|
14
|
+
schema: z.ZodSchema<any>;
|
|
15
|
+
dependencies: string[];
|
|
16
|
+
path: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface CoordinatorConfig {
|
|
19
|
+
parallelExecution?: boolean;
|
|
20
|
+
maxRetries?: number;
|
|
21
|
+
progressiveValidation?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface SchemaDecomposition {
|
|
24
|
+
subTasks: SubTask[];
|
|
25
|
+
dependencyGraph: Map<string, string[]>;
|
|
26
|
+
}
|
package/go/cmd/rlm/main.go
CHANGED
|
@@ -10,15 +10,23 @@ import (
|
|
|
10
10
|
)
|
|
11
11
|
|
|
12
12
|
type requestPayload struct {
|
|
13
|
-
Model
|
|
14
|
-
Query
|
|
15
|
-
Context
|
|
16
|
-
Config
|
|
13
|
+
Model string `json:"model"`
|
|
14
|
+
Query string `json:"query"`
|
|
15
|
+
Context string `json:"context"`
|
|
16
|
+
Config map[string]interface{} `json:"config"`
|
|
17
|
+
Structured *structuredRequest `json:"structured,omitempty"`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type structuredRequest struct {
|
|
21
|
+
Schema *rlm.JSONSchema `json:"schema"`
|
|
22
|
+
ParallelExecution bool `json:"parallelExecution"`
|
|
23
|
+
MaxRetries int `json:"maxRetries"`
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
type responsePayload struct {
|
|
20
|
-
Result
|
|
21
|
-
Stats
|
|
27
|
+
Result interface{} `json:"result"`
|
|
28
|
+
Stats rlm.RLMStats `json:"stats"`
|
|
29
|
+
StructuredResult bool `json:"structured_result,omitempty"`
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
func main() {
|
|
@@ -42,15 +50,39 @@ func main() {
|
|
|
42
50
|
config := rlm.ConfigFromMap(req.Config)
|
|
43
51
|
engine := rlm.New(req.Model, config)
|
|
44
52
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
var resp responsePayload
|
|
54
|
+
|
|
55
|
+
// Handle structured completion if requested
|
|
56
|
+
if req.Structured != nil {
|
|
57
|
+
structuredConfig := &rlm.StructuredConfig{
|
|
58
|
+
Schema: req.Structured.Schema,
|
|
59
|
+
ParallelExecution: req.Structured.ParallelExecution,
|
|
60
|
+
MaxRetries: req.Structured.MaxRetries,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
result, stats, err := engine.StructuredCompletion(req.Query, req.Context, structuredConfig)
|
|
64
|
+
if err != nil {
|
|
65
|
+
fmt.Fprintln(os.Stderr, err)
|
|
66
|
+
os.Exit(1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
resp = responsePayload{
|
|
70
|
+
Result: result,
|
|
71
|
+
Stats: stats,
|
|
72
|
+
StructuredResult: true,
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// Regular completion
|
|
76
|
+
result, stats, err := engine.Completion(req.Query, req.Context)
|
|
77
|
+
if err != nil {
|
|
78
|
+
fmt.Fprintln(os.Stderr, err)
|
|
79
|
+
os.Exit(1)
|
|
80
|
+
}
|
|
50
81
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
82
|
+
resp = responsePayload{
|
|
83
|
+
Result: result,
|
|
84
|
+
Stats: stats,
|
|
85
|
+
}
|
|
54
86
|
}
|
|
55
87
|
|
|
56
88
|
payload, err := json.Marshal(resp)
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
package rlm
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"regexp"
|
|
7
|
+
"strings"
|
|
8
|
+
"sync"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// StructuredCompletion executes a structured completion with schema validation
|
|
12
|
+
func (r *RLM) StructuredCompletion(query string, context string, config *StructuredConfig) (map[string]interface{}, RLMStats, error) {
|
|
13
|
+
if config == nil || config.Schema == nil {
|
|
14
|
+
return nil, RLMStats{}, fmt.Errorf("structured config and schema are required")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Set defaults
|
|
18
|
+
if config.MaxRetries == 0 {
|
|
19
|
+
config.MaxRetries = 3
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Decompose schema into sub-tasks
|
|
23
|
+
subTasks := decomposeSchema(config.Schema)
|
|
24
|
+
|
|
25
|
+
// If simple schema or parallel disabled, use direct method
|
|
26
|
+
if len(subTasks) <= 2 || !config.ParallelExecution {
|
|
27
|
+
return r.structuredCompletionDirect(query, context, config)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Execute with parallel goroutines
|
|
31
|
+
return r.structuredCompletionParallel(query, context, config, subTasks)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// structuredCompletionDirect performs a single structured completion
|
|
35
|
+
func (r *RLM) structuredCompletionDirect(query string, context string, config *StructuredConfig) (map[string]interface{}, RLMStats, error) {
|
|
36
|
+
schemaJSON, _ := json.Marshal(config.Schema)
|
|
37
|
+
|
|
38
|
+
// Build comprehensive prompt with context and schema
|
|
39
|
+
constraints := generateSchemaConstraints(config.Schema)
|
|
40
|
+
prompt := fmt.Sprintf(
|
|
41
|
+
"You are a data extraction assistant. Extract information from the context and return it as JSON.\n\n"+
|
|
42
|
+
"Context:\n%s\n\n"+
|
|
43
|
+
"Task: %s\n\n"+
|
|
44
|
+
"Required JSON Schema:\n%s\n\n"+
|
|
45
|
+
"%s"+
|
|
46
|
+
"CRITICAL INSTRUCTIONS:\n"+
|
|
47
|
+
"1. Return ONLY valid JSON - no explanations, no markdown, no code blocks\n"+
|
|
48
|
+
"2. The JSON must match the schema EXACTLY\n"+
|
|
49
|
+
"3. Include ALL required fields\n"+
|
|
50
|
+
"4. Use correct data types (strings in quotes, numbers without quotes, arrays in [], objects in {})\n"+
|
|
51
|
+
"5. For arrays, return actual JSON arrays [] not objects\n"+
|
|
52
|
+
"6. For enum fields, use ONLY the EXACT values listed - do not paraphrase or substitute\n"+
|
|
53
|
+
"7. Start your response directly with { or [ depending on the schema\n\n"+
|
|
54
|
+
"JSON Response:",
|
|
55
|
+
context, query, string(schemaJSON), constraints,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
var lastErr error
|
|
59
|
+
stats := RLMStats{Depth: r.currentDepth}
|
|
60
|
+
|
|
61
|
+
for attempt := 0; attempt < config.MaxRetries; attempt++ {
|
|
62
|
+
// Call LLM directly without REPL
|
|
63
|
+
messages := []Message{
|
|
64
|
+
{Role: "system", Content: "You are a data extraction assistant. Respond only with valid JSON objects."},
|
|
65
|
+
{Role: "user", Content: prompt},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
result, err := r.callLLM(messages)
|
|
69
|
+
stats.LlmCalls++
|
|
70
|
+
stats.Iterations++
|
|
71
|
+
|
|
72
|
+
if err != nil {
|
|
73
|
+
lastErr = err
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
parsed, err := parseAndValidateJSON(result, config.Schema)
|
|
78
|
+
if err != nil {
|
|
79
|
+
lastErr = err
|
|
80
|
+
if attempt < config.MaxRetries-1 {
|
|
81
|
+
// Retry with error feedback
|
|
82
|
+
prompt = fmt.Sprintf(
|
|
83
|
+
"%s\n\nPrevious attempt failed: %s\n"+
|
|
84
|
+
"Please fix the error and provide a valid JSON object.",
|
|
85
|
+
prompt, err.Error(),
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
stats.ParsingRetries = attempt
|
|
92
|
+
return parsed, stats, nil
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return nil, stats, fmt.Errorf("failed to get valid structured output after %d attempts: %v", config.MaxRetries, lastErr)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// structuredCompletionParallel executes sub-tasks in parallel
|
|
99
|
+
func (r *RLM) structuredCompletionParallel(query string, context string, config *StructuredConfig, subTasks []SubTask) (map[string]interface{}, RLMStats, error) {
|
|
100
|
+
results := make(map[string]interface{})
|
|
101
|
+
var resultsMutex sync.Mutex
|
|
102
|
+
|
|
103
|
+
var wg sync.WaitGroup
|
|
104
|
+
errChan := make(chan error, len(subTasks))
|
|
105
|
+
|
|
106
|
+
totalStats := RLMStats{}
|
|
107
|
+
var statsMutex sync.Mutex
|
|
108
|
+
|
|
109
|
+
for _, task := range subTasks {
|
|
110
|
+
wg.Add(1)
|
|
111
|
+
go func(t SubTask) {
|
|
112
|
+
defer wg.Done()
|
|
113
|
+
|
|
114
|
+
taskQuery := fmt.Sprintf("%s\n\nSpecific focus: %s", query, t.Query)
|
|
115
|
+
taskConfig := &StructuredConfig{
|
|
116
|
+
Schema: t.Schema,
|
|
117
|
+
ParallelExecution: false, // Disable nested parallelization
|
|
118
|
+
MaxRetries: config.MaxRetries,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
result, stats, err := r.structuredCompletionDirect(taskQuery, context, taskConfig)
|
|
122
|
+
if err != nil {
|
|
123
|
+
errChan <- fmt.Errorf("task %s failed: %w", t.ID, err)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
resultsMutex.Lock()
|
|
128
|
+
fieldName := strings.TrimPrefix(t.ID, "field_")
|
|
129
|
+
// If result has the __value__ wrapper (non-object type), unwrap it
|
|
130
|
+
if val, ok := result["__value__"]; ok {
|
|
131
|
+
results[fieldName] = val
|
|
132
|
+
} else {
|
|
133
|
+
results[fieldName] = result
|
|
134
|
+
}
|
|
135
|
+
resultsMutex.Unlock()
|
|
136
|
+
|
|
137
|
+
statsMutex.Lock()
|
|
138
|
+
totalStats.LlmCalls += stats.LlmCalls
|
|
139
|
+
totalStats.Iterations += stats.Iterations
|
|
140
|
+
if stats.Depth > totalStats.Depth {
|
|
141
|
+
totalStats.Depth = stats.Depth
|
|
142
|
+
}
|
|
143
|
+
totalStats.ParsingRetries += stats.ParsingRetries
|
|
144
|
+
statsMutex.Unlock()
|
|
145
|
+
}(task)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
wg.Wait()
|
|
149
|
+
close(errChan)
|
|
150
|
+
|
|
151
|
+
// Check for errors
|
|
152
|
+
if len(errChan) > 0 {
|
|
153
|
+
return nil, totalStats, <-errChan
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Validate merged result against full schema
|
|
157
|
+
if err := validateAgainstSchema(results, config.Schema); err != nil {
|
|
158
|
+
return nil, totalStats, fmt.Errorf("merged result validation failed: %w", err)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return results, totalStats, nil
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// decomposeSchema breaks down a schema into independent sub-tasks
|
|
165
|
+
func decomposeSchema(schema *JSONSchema) []SubTask {
|
|
166
|
+
var subTasks []SubTask
|
|
167
|
+
|
|
168
|
+
if schema.Type != "object" || schema.Properties == nil {
|
|
169
|
+
return subTasks
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for fieldName, fieldSchema := range schema.Properties {
|
|
173
|
+
taskID := fmt.Sprintf("field_%s", fieldName)
|
|
174
|
+
query := generateFieldQuery(fieldName, fieldSchema)
|
|
175
|
+
|
|
176
|
+
subTasks = append(subTasks, SubTask{
|
|
177
|
+
ID: taskID,
|
|
178
|
+
Query: query,
|
|
179
|
+
Schema: fieldSchema,
|
|
180
|
+
Dependencies: []string{},
|
|
181
|
+
Path: []string{fieldName},
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return subTasks
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// generateSchemaConstraints creates human-readable constraint descriptions
|
|
189
|
+
func generateSchemaConstraints(schema *JSONSchema) string {
|
|
190
|
+
var constraints []string
|
|
191
|
+
|
|
192
|
+
if schema.Type == "object" && schema.Properties != nil {
|
|
193
|
+
for fieldName, fieldSchema := range schema.Properties {
|
|
194
|
+
if fieldSchema.Type == "number" {
|
|
195
|
+
if strings.Contains(strings.ToLower(fieldName), "sentiment") {
|
|
196
|
+
constraints = append(constraints, fmt.Sprintf("- %s must be a number between 1 and 5 (inclusive)", fieldName))
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if fieldSchema.Enum != nil && len(fieldSchema.Enum) > 0 {
|
|
200
|
+
constraints = append(constraints, fmt.Sprintf("- %s must be EXACTLY one of these values: %s (use these exact strings, do not modify)", fieldName, strings.Join(fieldSchema.Enum, ", ")))
|
|
201
|
+
}
|
|
202
|
+
if fieldSchema.Type == "array" {
|
|
203
|
+
constraints = append(constraints, fmt.Sprintf("- %s must be a JSON array []", fieldName))
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check nested array items for constraints
|
|
209
|
+
if schema.Type == "array" && schema.Items != nil {
|
|
210
|
+
if schema.Items.Type == "object" && schema.Items.Properties != nil {
|
|
211
|
+
for fieldName, fieldSchema := range schema.Items.Properties {
|
|
212
|
+
if fieldSchema.Type == "number" && strings.Contains(strings.ToLower(fieldName), "sentiment") {
|
|
213
|
+
constraints = append(constraints, fmt.Sprintf("- Each item's %s must be between 1 and 5", fieldName))
|
|
214
|
+
}
|
|
215
|
+
if fieldSchema.Enum != nil && len(fieldSchema.Enum) > 0 {
|
|
216
|
+
constraints = append(constraints, fmt.Sprintf("- Each item's %s must be EXACTLY one of these values: %s (copy exactly, do not modify these strings)", fieldName, strings.Join(fieldSchema.Enum, ", ")))
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if len(constraints) > 0 {
|
|
223
|
+
return "CONSTRAINTS:\n" + strings.Join(constraints, "\n") + "\n\n"
|
|
224
|
+
}
|
|
225
|
+
return ""
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// generateFieldQuery creates a focused query for a specific field
|
|
229
|
+
func generateFieldQuery(fieldName string, schema *JSONSchema) string {
|
|
230
|
+
fieldQueries := map[string]string{
|
|
231
|
+
"sentiment": "Analyze the overall sentiment of this conversation. Provide a sentiment score from 1-5 and a detailed explanation.",
|
|
232
|
+
"sentimentValue": "What is the overall sentiment score (1-5) of this conversation?",
|
|
233
|
+
"sentimentExplanation": "Explain in 2-3 sentences why the conversation has this sentiment score.",
|
|
234
|
+
"phrases": "Extract key phrases that significantly impacted the sentiment, excluding neutral (3-value) phrases. For each phrase, include the sentiment value and the phrase itself (1 sentence).",
|
|
235
|
+
"keyMoments": "Identify key moments in the conversation such as churn mentions, personnel changes, competitive mentions, etc. For each moment, provide the phrase and categorize the type.",
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if query, exists := fieldQueries[fieldName]; exists {
|
|
239
|
+
return query
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return fmt.Sprintf("Extract the %s from the conversation.", fieldName)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// parseAndValidateJSON extracts JSON from response and validates against schema
|
|
246
|
+
func parseAndValidateJSON(result string, schema *JSONSchema) (map[string]interface{}, error) {
|
|
247
|
+
// Remove markdown code blocks if present
|
|
248
|
+
result = strings.TrimSpace(result)
|
|
249
|
+
if strings.HasPrefix(result, "```") {
|
|
250
|
+
// Extract content between ``` markers
|
|
251
|
+
lines := strings.Split(result, "\n")
|
|
252
|
+
if len(lines) > 2 {
|
|
253
|
+
// Remove first line (```json or ```) and last line (```)
|
|
254
|
+
result = strings.Join(lines[1:len(lines)-1], "\n")
|
|
255
|
+
result = strings.TrimSpace(result)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// For non-object schemas (arrays, primitives), handle special cases
|
|
260
|
+
if schema.Type != "object" {
|
|
261
|
+
// Try parsing as direct value first
|
|
262
|
+
var value interface{}
|
|
263
|
+
parseErr := json.Unmarshal([]byte(result), &value)
|
|
264
|
+
if parseErr == nil {
|
|
265
|
+
// Check if it's a map (LLM wrapped the value in an object)
|
|
266
|
+
if valueMap, ok := value.(map[string]interface{}); ok {
|
|
267
|
+
// If it's a single-key object, extract the value
|
|
268
|
+
if len(valueMap) == 1 {
|
|
269
|
+
for _, v := range valueMap {
|
|
270
|
+
value = v
|
|
271
|
+
break
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Validate the unwrapped value
|
|
277
|
+
if err := validateValue(value, schema); err != nil {
|
|
278
|
+
return nil, err
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Wrap in a map with a temp key
|
|
282
|
+
return map[string]interface{}{"__value__": value}, nil
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return nil, fmt.Errorf("failed to parse JSON: %v", parseErr)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Try to find the outermost JSON object
|
|
289
|
+
var parsed map[string]interface{}
|
|
290
|
+
|
|
291
|
+
// First, try to parse the entire trimmed string
|
|
292
|
+
if err := json.Unmarshal([]byte(result), &parsed); err == nil {
|
|
293
|
+
if err := validateAgainstSchema(parsed, schema); err != nil {
|
|
294
|
+
return nil, err
|
|
295
|
+
}
|
|
296
|
+
return parsed, nil
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// If that fails, try to extract JSON with regex
|
|
300
|
+
re := regexp.MustCompile(`\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
|
301
|
+
matches := re.FindAllString(result, -1)
|
|
302
|
+
|
|
303
|
+
if len(matches) == 0 {
|
|
304
|
+
return nil, fmt.Errorf("no JSON object found in response: %s", result)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Try each match until we find one that validates
|
|
308
|
+
for _, match := range matches {
|
|
309
|
+
var candidate map[string]interface{}
|
|
310
|
+
if err := json.Unmarshal([]byte(match), &candidate); err == nil {
|
|
311
|
+
if err := validateAgainstSchema(candidate, schema); err == nil {
|
|
312
|
+
return candidate, nil
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return nil, fmt.Errorf("no valid JSON object matching schema found in response")
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// validateAgainstSchema validates data against a JSON schema
|
|
321
|
+
func validateAgainstSchema(data map[string]interface{}, schema *JSONSchema) error {
|
|
322
|
+
if schema.Type != "object" {
|
|
323
|
+
return nil // Only validate object types for now
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check required fields
|
|
327
|
+
for _, required := range schema.Required {
|
|
328
|
+
if _, exists := data[required]; !exists {
|
|
329
|
+
return fmt.Errorf("missing required field: %s", required)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Validate properties
|
|
334
|
+
if schema.Properties != nil {
|
|
335
|
+
for key, fieldSchema := range schema.Properties {
|
|
336
|
+
value, exists := data[key]
|
|
337
|
+
if !exists && contains(schema.Required, key) {
|
|
338
|
+
return fmt.Errorf("missing required field: %s", key)
|
|
339
|
+
}
|
|
340
|
+
if exists {
|
|
341
|
+
if err := validateValue(value, fieldSchema); err != nil {
|
|
342
|
+
return fmt.Errorf("field %s: %w", key, err)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return nil
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// validateValue validates a value against a schema
|
|
352
|
+
func validateValue(value interface{}, schema *JSONSchema) error {
|
|
353
|
+
if value == nil && schema.Nullable {
|
|
354
|
+
return nil
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
switch schema.Type {
|
|
358
|
+
case "string":
|
|
359
|
+
if _, ok := value.(string); !ok {
|
|
360
|
+
return fmt.Errorf("expected string, got %T", value)
|
|
361
|
+
}
|
|
362
|
+
case "number":
|
|
363
|
+
switch value.(type) {
|
|
364
|
+
case float64, float32, int, int32, int64:
|
|
365
|
+
return nil
|
|
366
|
+
default:
|
|
367
|
+
return fmt.Errorf("expected number, got %T", value)
|
|
368
|
+
}
|
|
369
|
+
case "boolean":
|
|
370
|
+
if _, ok := value.(bool); !ok {
|
|
371
|
+
return fmt.Errorf("expected boolean, got %T", value)
|
|
372
|
+
}
|
|
373
|
+
case "array":
|
|
374
|
+
arr, ok := value.([]interface{})
|
|
375
|
+
if !ok {
|
|
376
|
+
return fmt.Errorf("expected array, got %T", value)
|
|
377
|
+
}
|
|
378
|
+
if schema.Items != nil {
|
|
379
|
+
for i, item := range arr {
|
|
380
|
+
if err := validateValue(item, schema.Items); err != nil {
|
|
381
|
+
return fmt.Errorf("array item %d: %w", i, err)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
case "object":
|
|
386
|
+
obj, ok := value.(map[string]interface{})
|
|
387
|
+
if !ok {
|
|
388
|
+
return fmt.Errorf("expected object, got %T", value)
|
|
389
|
+
}
|
|
390
|
+
return validateAgainstSchema(obj, schema)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return nil
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
func contains(arr []string, item string) bool {
|
|
397
|
+
for _, v := range arr {
|
|
398
|
+
if v == item {
|
|
399
|
+
return true
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return false
|
|
403
|
+
}
|
package/go/internal/rlm/types.go
CHANGED
|
@@ -6,9 +6,33 @@ import (
|
|
|
6
6
|
)
|
|
7
7
|
|
|
8
8
|
type RLMStats struct {
|
|
9
|
-
LlmCalls
|
|
10
|
-
Iterations
|
|
11
|
-
Depth
|
|
9
|
+
LlmCalls int `json:"llm_calls"`
|
|
10
|
+
Iterations int `json:"iterations"`
|
|
11
|
+
Depth int `json:"depth"`
|
|
12
|
+
ParsingRetries int `json:"parsing_retries,omitempty"`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type JSONSchema struct {
|
|
16
|
+
Type string `json:"type"`
|
|
17
|
+
Properties map[string]*JSONSchema `json:"properties,omitempty"`
|
|
18
|
+
Items *JSONSchema `json:"items,omitempty"`
|
|
19
|
+
Required []string `json:"required,omitempty"`
|
|
20
|
+
Enum []string `json:"enum,omitempty"`
|
|
21
|
+
Nullable bool `json:"nullable,omitempty"`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type SubTask struct {
|
|
25
|
+
ID string
|
|
26
|
+
Query string
|
|
27
|
+
Schema *JSONSchema
|
|
28
|
+
Dependencies []string
|
|
29
|
+
Path []string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type StructuredConfig struct {
|
|
33
|
+
Schema *JSONSchema
|
|
34
|
+
ParallelExecution bool
|
|
35
|
+
MaxRetries int
|
|
12
36
|
}
|
|
13
37
|
|
|
14
38
|
type Config struct {
|
|
@@ -20,6 +44,7 @@ type Config struct {
|
|
|
20
44
|
TimeoutSeconds int
|
|
21
45
|
Parallel bool // Enable parallel recursive calls with goroutines
|
|
22
46
|
UseMetacognitive bool // Enable step-by-step reasoning guidance in prompts
|
|
47
|
+
Structured *StructuredConfig
|
|
23
48
|
ExtraParams map[string]interface{}
|
|
24
49
|
}
|
|
25
50
|
|
|
@@ -62,8 +87,8 @@ func ConfigFromMap(config map[string]interface{}) Config {
|
|
|
62
87
|
if v, ok := value.(bool); ok {
|
|
63
88
|
parsed.UseMetacognitive = v
|
|
64
89
|
}
|
|
65
|
-
case "pythonia_timeout", "go_binary_path", "bridge":
|
|
66
|
-
// ignore bridge-only config
|
|
90
|
+
case "pythonia_timeout", "go_binary_path", "bridge", "structured":
|
|
91
|
+
// ignore bridge-only config and structured (handled separately)
|
|
67
92
|
default:
|
|
68
93
|
parsed.ExtraParams[key] = value
|
|
69
94
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "recursive-llm-ts",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "TypeScript bridge for recursive-llm: Recursive Language Models for unbounded context processing",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "TypeScript bridge for recursive-llm: Recursive Language Models for unbounded context processing with structured outputs",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -24,7 +24,11 @@
|
|
|
24
24
|
"recursive",
|
|
25
25
|
"context",
|
|
26
26
|
"nlp",
|
|
27
|
-
"language-model"
|
|
27
|
+
"language-model",
|
|
28
|
+
"structured-output",
|
|
29
|
+
"zod",
|
|
30
|
+
"schema",
|
|
31
|
+
"extraction"
|
|
28
32
|
],
|
|
29
33
|
"author": "",
|
|
30
34
|
"license": "MIT",
|
|
@@ -36,7 +40,9 @@
|
|
|
36
40
|
"url": "https://github.com/jbeck018/recursive-llm-ts/issues"
|
|
37
41
|
},
|
|
38
42
|
"homepage": "https://github.com/jbeck018/recursive-llm-ts#readme",
|
|
39
|
-
"dependencies": {
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"zod": "^4.3.6"
|
|
45
|
+
},
|
|
40
46
|
"devDependencies": {
|
|
41
47
|
"@types/node": "^20.11.19",
|
|
42
48
|
"dotenv": "^16.4.5",
|