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 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
@@ -4,8 +4,9 @@ export interface RLMStats {
4
4
  depth: number;
5
5
  }
6
6
  export interface RLMResult {
7
- result: string;
7
+ result: string | any;
8
8
  stats: RLMStats;
9
+ structured_result?: boolean;
9
10
  }
10
11
  export interface RLMConfig {
11
12
  recursive_model?: string;
@@ -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, '..', 'go', DEFAULT_BINARY_NAME), // Development
69
- path.join(__dirname, '..', 'bin', DEFAULT_BINARY_NAME), // NPM package
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: sanitizeConfig(rlmConfig)
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
@@ -1,3 +1,5 @@
1
1
  export { RLM } from './rlm';
2
2
  export { RLMConfig, RLMResult, RLMStats } from './bridge-interface';
3
3
  export { BridgeType } from './bridge-factory';
4
+ export { StructuredRLMResult } from './structured-types';
5
+ export { RLMAgentCoordinator } from './coordinator';
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
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -10,15 +10,23 @@ import (
10
10
  )
11
11
 
12
12
  type requestPayload struct {
13
- Model string `json:"model"`
14
- Query string `json:"query"`
15
- Context string `json:"context"`
16
- Config map[string]interface{} `json:"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 string `json:"result"`
21
- Stats rlm.RLMStats `json:"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
- result, stats, err := engine.Completion(req.Query, req.Context)
46
- if err != nil {
47
- fmt.Fprintln(os.Stderr, err)
48
- os.Exit(1)
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
- resp := responsePayload{
52
- Result: result,
53
- Stats: stats,
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
+ }
@@ -6,9 +6,33 @@ import (
6
6
  )
7
7
 
8
8
  type RLMStats struct {
9
- LlmCalls int `json:"llm_calls"`
10
- Iterations int `json:"iterations"`
11
- Depth int `json:"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": "3.0.1",
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",