/ docs / v0.3.md
v0.3.md
  1  ## Oxyjen v0.3 Documentation
  2  
  3  **Version:** 0.3.0  
  4  **Status:** In Development  
  5  **Focus:** Structured Intelligence — Prompts, Schemas, Reliable Outputs  
  6  
  7  ---
  8  
  9  ## Overview
 10  
 11  Oxyjen v0.3 introduces structured intelligence to Java AI pipelines.
 12  
 13  Instead of unpredictable text, you can now enforce JSON schemas, validate model output, and automatically retry until the structure is correct.
 14  
 15  At the core:
 16  
 17  - Prompt templates for reusable prompts
 18  - JSON schemas for structured outputs
 19  - Automatic validation + retry
 20  - SchemaNode for graph integration
 21  
 22  ---
 23  
 24  ## What v0.3 Solves
 25  
 26  ### Before v0.3
 27  
 28  ```java
 29  String response = model.chat("Extract person info: John Smith, age 30");
 30  // Could be anything:
 31  // "John Smith is 30"
 32  // "{ name: John }"
 33  // invalid JSON
 34  // random formats
 35  ```
 36  ### With v0.3
 37  ```java
 38  JSONSchema schema = JSONSchema.object()
 39      .property("name", PropertySchema.string("Person name"))
 40      .property("age", PropertySchema.number("Person age"))
 41      .required("name", "age")
 42      .build();
 43  
 44  SchemaEnforcer enforcer = new SchemaEnforcer(model, schema);
 45  
 46  String json = enforcer.execute(
 47      "Extract person info: John Smith, age 30"
 48  );
 49  
 50  // Always valid JSON:
 51  // {"name":"John Smith","age":30}
 52  ```
 53  ---
 54  
 55  Core Concepts
 56  
 57  **Prompt Templates**
 58  - Prompt templates let you define prompts with variables and fill them at runtime.
 59  
 60  **Basic Usage**
 61  ```java
 62  PromptTemplate template = PromptTemplate.of(
 63      "Hello {{name}}, you are {{age}} years old."
 64  );
 65  
 66  String result = template.render(
 67      "name", "Alice",
 68      "age", 25
 69  );
 70  ```
 71  **Required / Optional Variables**
 72  ```java
 73  PromptTemplate template = PromptTemplate.of(
 74      "Hello {{name}}, role: {{role}}",
 75      Variable.required("name"),
 76      Variable.optional("role", "guest")
 77  );
 78  
 79  // Uses default role
 80  template.render("name", "Alice");
 81  
 82  // Override default
 83  template.render("name", "Bob", "role", "admin");
 84  ```
 85  Variables are provided at execution time, not on nodes.
 86  
 87  **JSON Schema**
 88  JSONSchema defines the structure your LLM output must follow.
 89  
 90  ***Creating a Schema***
 91  ```java
 92  JSONSchema schema = JSONSchema.object()
 93      .description("Person information")
 94      .property("name", PropertySchema.string("Full name"))
 95      .property("age", PropertySchema.number("Age"))
 96      .property("active", PropertySchema.bool("Is active"))
 97      .property("status", PropertySchema.enumOf(
 98          "Account status",
 99          "active", "inactive", "pending"
100      ))
101      .required("name", "age")
102      .build();
103  ```
104  **Supported types in v0.3:**
105  - string
106  - number
107  - boolean
108  - enum (string with fixed values)
109  
110  **Schema Validation**
111  Validate any JSON string against a schema.
112  ```java
113  SchemaValidator validator = new SchemaValidator(schema);
114  
115  ValidationResult result = validator.validate(json);
116  
117  if (!result.isValid()) {
118      System.out.println(result.formatErrors());
119  }
120  ```
121  ***ValidationResult exposes:***
122  - isValid()
123  - errors() → List<FieldError>
124  - formatErrors()
125  
126  ***Each FieldError includes:***
127  - fieldPath
128  - errorType
129  - expected
130  - received
131  - message
132  
133  **Schema Enforcement**
134  SchemaEnforcer automatically retries the model until valid JSON is produced (or retries are exhausted).
135  ```java
136  SchemaEnforcer enforcer =
137      new SchemaEnforcer(model, schema, 3);
138  
139  String json = enforcer.execute(
140      "Extract person info from: Alice Smith is 30"
141  );
142  ```
143  Internally:
144  
145  LLM -> validate -> retry with errors -> repeat
146  If all retries fail, SchemaException is thrown.
147  
148  **SchemaNode (Graph Integration)**
149  SchemaNode combines:
150  
151  - ChatModel
152  
153  - JSONSchema
154  
155  - SchemaEnforcer
156  
157  It acts as the boundary between text and structured data.
158  
159  Signature (v0.3):
160  
161  String → Map<String,Object>
162  Creating a SchemaNode:
163  ```java
164  SchemaNode node = SchemaNode.builder()
165      .model("gpt-4o-mini")
166      .schema(schema)
167      .memory("extractions")
168      .build();
169  ```
170  ---
171  
172  Public API (v0.3)
173  Prompts
174  - PromptTemplate.of(...)
175  - template.render(...)
176  - Variable.required(...)
177  - Variable.optional(...)
178  
179  Schema
180  - JSONSchema.object()
181  - PropertySchema.string(...)
182  - PropertySchema.number(...)
183  - PropertySchema.bool(...)
184  - PropertySchema.enumOf(...)
185  
186  Validation
187  - SchemaValidator.validate(...)
188  - ValidationResult
189  - FieldError
190  - Enforcement
191  - SchemaEnforcer.execute(...)
192  - SchemaException
193  
194  Graph
195  - SchemaNode.builder()...
196  - SchemaNode.process(...)
197  
198  ---
199  
200  **Migration from v0.2**
201  
202  v0.3 is backward compatible.
203  
204  You can still use:
205  
206  model.chat(...)
207  
208  ---
209  
210  **Mental Model**
211  
212  PromptTemplate = reusable prompt
213  
214  JSONSchema = output contract
215  
216  SchemaEnforcer = retry loop
217  
218  SchemaNode = graph boundary (text → data)
219  
220  ---
221  
222  ##Examples
223  
224  **Example 1**
225  
226  Customer Support Classifier
227  
228  Goal: Classify support tickets with structured outputs
229  
230  ```java
231  import io.oxyjen.core.*;
232  import io.oxyjen.llm.*;
233  import io.oxyjen.llm.prompts.*;
234  import io.oxyjen.llm.schema.*;
235  
236  public class SupportClassifier {
237  
238      public static void main(String[] args) {
239  
240          PromptTemplate prompt = PromptTemplate.of("""
241              You are a customer support ticket classifier for {{company_name}}.
242  
243              Classify this support ticket:
244              "{{ticket_message}}"
245  
246              Determine:
247              - Priority (low, medium, high, critical)
248              - Department (billing, technical, sales, general)
249              - Customer sentiment (happy, neutral, frustrated, angry)
250              - Brief summary
251  
252              Respond ONLY with valid JSON.
253              """,
254              Variable.required("company_name"),
255              Variable.required("ticket_message")
256          );
257  
258          JSONSchema schema = JSONSchema.object()
259              .property("priority", PropertySchema.enumOf(
260                  "Ticket priority",
261                  "low", "medium", "high", "critical"
262              ))
263              .property("department", PropertySchema.enumOf(
264                  "Handling department",
265                  "billing", "technical", "sales", "general"
266              ))
267              .property("sentiment", PropertySchema.enumOf(
268                  "Customer emotion",
269                  "happy", "neutral", "frustrated", "angry"
270              ))
271              .property("summary", PropertySchema.string("Brief issue summary"))
272              .required("priority", "department", "sentiment", "summary")
273              .build();
274  
275          SchemaNode classifyNode = SchemaNode.builder()
276              .model("gpt-4o-mini")
277              .schema(schema)
278              .memory("support-tickets")
279              .build();
280  
281          Graph pipeline = GraphBuilder.named("support-classifier")
282              .addNode(classifyNode)
283              .build();
284  
285          NodeContext ctx = new NodeContext();
286          Executor executor = new Executor();
287  
288          String ticket =
289              "I was charged twice for my subscription! I want a refund NOW!";
290  
291          // Render prompt 
292          String renderedPrompt =
293              prompt.render(
294                  "company_name", "Acme Corp",
295                  "ticket_message", ticket
296              );
297  
298          // Then send rendered prompt to SchemaNode
299          Map<String,Object> result =
300              executor.run(pipeline, renderedPrompt, ctx);
301  
302          System.out.println(result);
303      }
304  }
305  ```
306  
307  **Example 2**
308  
309  Data Extraction Pipeline
310  
311  Goal: Extract structured data from unstructured text.
312  
313  ```java
314  import io.oxyjen.core.*;
315  import io.oxyjen.llm.*;
316  import io.oxyjen.llm.schema.*;
317  
318  public class DataExtractor {
319      
320      public static void main(String[] args) {
321          // Define schema for person data
322          JSONSchema personSchema = JSONSchema.object()
323              .property("name", PropertySchema.string("Full name"))
324              .property("age", PropertySchema.number("Age in years"))
325              .property("email", PropertySchema.string("Email address"))
326              .property("phone", PropertySchema.string("Phone number"))
327              .property("city", PropertySchema.string("City of residence"))
328              .required("name")  // Only name is required
329              .build();
330          
331          // Create model with fallback
332          ChatModel model = LLMChain.builder()
333              .primary("gpt-4o")
334              .fallback("gpt-4o-mini")
335              .retry(3)
336              .timeout(Duration.ofSeconds(10))
337              .build();
338          
339          // Create enforcer
340          SchemaEnforcer enforcer = new SchemaEnforcer(model, personSchema);
341          
342          // Extract from different formats
343          String[] inputs = {
344              "John Smith is 35 years old, lives in NYC, email: john@example.com",
345              "Name: Alice Johnson, Age: 28, San Francisco, (415) 555-0123",
346              "Bob Williams, age 42, bob.w@company.com"
347          };
348          
349          for (String input : inputs) {
350              String json = enforcer.execute(
351                  "Extract person information from: " + input
352              );
353              System.out.println(json);
354          }
355          
356          /*
357          Output:
358          {"name": "John Smith", "age": 35, "email": "john@example.com", "city": "NYC"}
359          {"name": "Alice Johnson", "age": 28, "city": "San Francisco", "phone": "(415) 555-0123"}
360          {"name": "Bob Williams", "age": 42, "email": "bob.w@company.com"}
361          */
362      }
363  }
364  ```
365  
366  # Jitter and Retry Cap
367  
368  Jitter and Retry Cap are production-grade features that prevent cascading failures and unbounded waits when retrying failed LLM requests.
369  
370  ---
371  
372  ## The Problem
373  
374  ### Without Jitter: The Thundering Herd
375  
376  When multiple requests fail simultaneously (for example during an API outage or rate limit), they retry at the same time:
377  
378  Time 0s: 1000 requests fail simultaneously
379  Time 1s: 1000 requests retry simultaneously
380  Time 3s: 1000 requests retry simultaneously
381  Time 7s: 1000 requests retry simultaneously
382  
383  Result: synchronized retry waves keep hammering the API, preventing recovery.
384  
385  ---
386  
387  ### Without Retry Cap: Unbounded Waits
388  
389  Exponential backoff grows forever:
390  
391  Attempt 1: 1s
392  Attempt 2: 2s
393  Attempt 3: 4s
394  Attempt 4: 8s
395  Attempt 5: 16s
396  Attempt 6: 32s
397  Attempt 7: 64s
398  Attempt 8: 128s
399  
400  
401  Result:
402  
403  - users experience unacceptable delays  
404  - threads remain blocked  
405  - resources are wasted  
406  
407  ---
408  
409  ## The Solution
410  
411  ### Jitter: Randomize Retry Delays
412  
413  Add randomness to spread retries across time.
414  
415  With ±20% jitter:
416  
417  Time 0.8–1.2s: ~200 requests retry
418  Time 1.0–1.4s: ~200 requests retry
419  Time 1.2–1.6s: ~200 requests retry
420  Time 1.4–1.8s: ~200 requests retry
421  Time 1.6–2.0s: ~200 requests retry
422  
423  Result: smooth distributed load instead of synchronized spikes.
424  
425  ---
426  
427  ### Retry Cap: Bound Maximum Delay
428  
429  Cap exponential backoff at a reasonable maximum.
430  
431  With `maxBackoff = 10s`:
432  
433  Attempt 1: 1s
434  Attempt 2: 2s
435  Attempt 3: 4s
436  Attempt 4: 8s
437  Attempt 5: 10s (capped)
438  Attempt 6: 10s
439  Attempt 7: 10s
440  
441  Result: predictable wait times and better UX.
442  
443  ---
444  
445  ## Usage
446  
447  ### Recommended Defaults
448  
449  ```java
450  LLMChain chain = LLMChain.builder()
451      .primary("gpt-4")
452      .fallback("gpt-3.5-turbo")
453      .retry(5)
454      .exponentialBackoff()
455      .maxBackoff(Duration.ofSeconds(10))
456      .jitter(0.2)
457      .build();
458  ```
459  Behavior:
460  
461  exponential delays (~1s, ~2s, ~4s, ~8s, ~10s)
462  
463  each delay randomized ±20%
464  
465  never exceeds 10 seconds
466  
467  Production Configuration
468  ```java
469  LLMChain chain = LLMChain.builder()
470      .primary("gpt-4")
471      .fallback("claude-3-sonnet")
472      .fallback("gpt-3.5-turbo")
473      .retry(7)
474      .exponentialBackoff()
475      .maxBackoff(Duration.ofSeconds(15))
476      .jitter(0.3)
477      .timeout(Duration.ofSeconds(30))
478      .build();
479  ```
480  Use case: critical workloads with bounded waits and aggressive retries.
481  
482  Conservative Configuration
483  ```java
484  LLMChain chain = LLMChain.builder()
485      .primary("gpt-3.5-turbo")
486      .retry(3)
487      .exponentialBackoff()
488      .maxBackoff(Duration.ofSeconds(5))
489      .jitter(0.15)
490      .build();
491  ```
492  Use case: interactive applications.
493  
494  No Jitter or Cap (v0.2 Behavior)
495  ```java
496  LLMChain chain = LLMChain.builder()
497      .primary("gpt-4")
498      .retry(3)
499      .exponentialBackoff()
500      .build();
501  ```
502  Result: deterministic exponential backoff with no upper bound.
503  
504  Configuration Parameters
505  maxBackoff(Duration)
506  Sets maximum retry delay.
507  
508  Internal Algorithm
509  ```java
510  private long calculateBackoff(int attempt) {
511      Duration base = exponentialBackoff
512          ? Duration.ofSeconds(1L << (attempt - 1))
513          : Duration.ofSeconds(1);
514  
515      if (maxBackoff != null && base.compareTo(maxBackoff) > 0) {
516          base = maxBackoff;
517      }
518  
519      if (jitterFactor > 0) {
520          double multiplier = ThreadLocalRandom.current()
521              .nextDouble(1.0 - jitterFactor, 1.0 + jitterFactor);
522  
523          base = Duration.ofMillis((long) (base.toMillis() * multiplier));
524      }
525  
526      return base.toMillis();
527  }
528  ```
529  
530  Key points:
531  - exponential growth via 2^(attempt-1)
532  - cap applied first 
533  - jitter applied last
534  - ThreadLocalRandom used for efficiency
535  
536  ---