Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Phase tracking:
- [Phase 2 — Expand from GoF to enterprise patterns](docs/phase-2-enterprise-patterns.md)
- [Phase 3 — Modern Java implementation variants](docs/phase-3-modern-java-variants.md)
- [Phase 4 — Cloud-native + observability examples](docs/phase-4-cloud-native-observability.md)
- [Phase 5 — AI-era additions (high practical value)](docs/phase-5-ai-era-patterns.md)

---

Expand Down Expand Up @@ -109,6 +110,16 @@ Package: `src/main/java/org/example/cloudnative/observability`
- **Saga correlation IDs** (trace attributes and correlated execution)
- **Resilience fault-injection scenarios** (Testcontainers + WireMock)

### 10) AI application patterns
Package: `src/main/java/org/example/ai`

- **Prompt Builder pattern**
- **Retrieval pipeline (RAG-lite) orchestration**
- **Tool/Agent orchestration facade**
- **Fallback model strategy**
- **Policy & safety wrappers** (input validation, output filtering, PII masking)
- **Evaluation harness** (golden set + statistical scoring)

---

## Pattern index matrix
Expand Down
42 changes: 42 additions & 0 deletions docs/phase-5-ai-era-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Phase 5 — AI-era additions (high practical value)

This phase introduces practical AI application patterns that teams can use to structure prompt workflows, retrieval pipelines, model orchestration, policy enforcement, and repeatable evaluation.

## AI application patterns module

Package: `org.example.ai`

### 1) Prompt Builder pattern
- `PromptBuilder` composes reusable prompt templates with parameter placeholders.
- Keeps prompt assembly explicit and testable.

### 2) Retrieval pipeline (RAG-lite) orchestration
- `RetrievalPipeline` separates retrieval and reranking steps.
- Builds bounded context windows suitable for lightweight RAG flows.

### 3) Tool/Agent orchestration facade
- `ToolAgentFacade` exposes one entrypoint for tool calls and LLM-based responses.
- Supports a simple `tool:<name>:<input>` command route plus LLM answering path.

### 4) Fallback model strategy
- `FallbackModelStrategy` tries model clients in order.
- Moves to the next model when previous responses fail or return empty output.

### 5) Policy & safety wrappers
- `PolicyEnforcedLlmClient` decorates `LlmClient` with:
- input validation (`NonEmptyInputPolicy`, `MaxLengthInputPolicy`),
- PII masking (`PiiMaskingInputPolicy`),
- output filtering (`BasicOutputFilterPolicy`).

### 6) Evaluation harness
- `EvaluationHarness` provides:
- deterministic golden set checks (accuracy + failure diagnostics),
- statistical score aggregation (average/min/max).

## Validation

`AiApplicationPatternsTest` demonstrates end-to-end usage of:
- prompt + RAG-lite + facade orchestration,
- fallback model behavior,
- policy/safety wrappers,
- deterministic and statistical evaluation patterns.
56 changes: 56 additions & 0 deletions src/main/java/org/example/ai/EvaluationHarness.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.example.ai;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

public final class EvaluationHarness {

private EvaluationHarness() {
}

public record GoldenExample(String input, String expectedOutput) {
}

public record GoldenSetResult(double accuracy, List<String> failures) {
}

public record StatisticalResult(double averageScore, double minScore, double maxScore) {
}

public static GoldenSetResult evaluateGoldenSet(List<GoldenExample> examples, Function<String, String> model) {
int correct = 0;
List<String> failures = new ArrayList<>();

for (GoldenExample example : examples) {
String actual = model.apply(example.input());
if (example.expectedOutput().equals(actual)) {
correct++;
} else {
failures.add("input=" + example.input() + ", expected=" + example.expectedOutput() + ", actual=" + actual);
}
}

double accuracy = examples.isEmpty() ? 0.0 : (double) correct / examples.size();
return new GoldenSetResult(accuracy, failures);
}

public static StatisticalResult evaluateStatistically(List<String> inputs, Function<String, Double> scorer) {
if (inputs.isEmpty()) {
throw new IllegalArgumentException("inputs must not be empty");
}

double sum = 0.0;
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;

for (String input : inputs) {
double score = scorer.apply(input);
sum += score;
min = Math.min(min, score);
max = Math.max(max, score);
}

return new StatisticalResult(sum / inputs.size(), min, max);
}
}
32 changes: 32 additions & 0 deletions src/main/java/org/example/ai/FallbackModelStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.example.ai;

import java.util.List;

public class FallbackModelStrategy implements LlmClient {

private final List<LlmClient> models;

public FallbackModelStrategy(List<LlmClient> models) {
this.models = models;
}

@Override
public String complete(String prompt) {
RuntimeException lastError = null;
for (LlmClient model : models) {
try {
String response = model.complete(prompt);
if (response != null && !response.isBlank()) {
return response;
}
} catch (RuntimeException ex) {
lastError = ex;
}
}

if (lastError != null) {
throw lastError;
}
throw new IllegalStateException("No model produced a response");
}
}
5 changes: 5 additions & 0 deletions src/main/java/org/example/ai/LlmClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.example.ai;

public interface LlmClient {
String complete(String prompt);
}
91 changes: 91 additions & 0 deletions src/main/java/org/example/ai/PolicyEnforcedLlmClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package org.example.ai;

import java.util.List;
import java.util.regex.Pattern;

public class PolicyEnforcedLlmClient implements LlmClient {

public interface InputPolicy {
void validate(String input);
}

public interface OutputPolicy {
String filter(String output);
}

public static class NonEmptyInputPolicy implements InputPolicy {
@Override
public void validate(String input) {
if (input == null || input.isBlank()) {
throw new IllegalArgumentException("Prompt must not be blank");
}
}
}

public static class MaxLengthInputPolicy implements InputPolicy {
private final int maxLength;

public MaxLengthInputPolicy(int maxLength) {
this.maxLength = maxLength;
}

@Override
public void validate(String input) {
if (input.length() > maxLength) {
throw new IllegalArgumentException("Prompt exceeds max length " + maxLength);
}
}
}

public static class PiiMaskingInputPolicy implements InputPolicy {
private static final Pattern EMAIL = Pattern.compile("[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+");

@Override
public void validate(String input) {
// no-op for validation; mask is applied via sanitize
}

public String sanitize(String input) {
return EMAIL.matcher(input).replaceAll("[EMAIL_MASKED]");
}
}

public static class BasicOutputFilterPolicy implements OutputPolicy {
@Override
public String filter(String output) {
return output.replaceAll("(?i)password\\s*[:=]\\s*\\S+", "password:[REDACTED]");
}
}

private final LlmClient delegate;
private final List<InputPolicy> inputPolicies;
private final List<OutputPolicy> outputPolicies;
private final PiiMaskingInputPolicy piiMaskingInputPolicy;

public PolicyEnforcedLlmClient(
LlmClient delegate,
List<InputPolicy> inputPolicies,
List<OutputPolicy> outputPolicies,
PiiMaskingInputPolicy piiMaskingInputPolicy
) {
this.delegate = delegate;
this.inputPolicies = inputPolicies;
this.outputPolicies = outputPolicies;
this.piiMaskingInputPolicy = piiMaskingInputPolicy;
}

@Override
public String complete(String prompt) {
for (InputPolicy policy : inputPolicies) {
policy.validate(prompt);
}

String sanitizedPrompt = piiMaskingInputPolicy.sanitize(prompt);
String output = delegate.complete(sanitizedPrompt);
String filteredOutput = output;
for (OutputPolicy policy : outputPolicies) {
filteredOutput = policy.filter(filteredOutput);
}
return filteredOutput;
}
}
27 changes: 27 additions & 0 deletions src/main/java/org/example/ai/PromptBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.example.ai;

import java.util.HashMap;
import java.util.Map;

public class PromptBuilder {

private final String template;
private final Map<String, String> values = new HashMap<>();

public PromptBuilder(String template) {
this.template = template;
}

public PromptBuilder with(String key, String value) {
values.put(key, value);
return this;
}

public String build() {
String built = template;
for (Map.Entry<String, String> entry : values.entrySet()) {
built = built.replace("{{" + entry.getKey() + "}}", entry.getValue());
}
return built;
}
}
40 changes: 40 additions & 0 deletions src/main/java/org/example/ai/RetrievalPipeline.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.example.ai;

import java.util.List;
import java.util.stream.Collectors;

public class RetrievalPipeline {

public interface Retriever {
List<RetrievedDocument> retrieve(String query, int topK);
}

public interface Reranker {
List<RetrievedDocument> rerank(String query, List<RetrievedDocument> documents);
}

public record RetrievedDocument(String id, String content, double score) {
}

private final Retriever retriever;
private final Reranker reranker;

public RetrievalPipeline(Retriever retriever, Reranker reranker) {
this.retriever = retriever;
this.reranker = reranker;
}

public String buildContext(String query, int topK, int maxChars) {
List<RetrievedDocument> retrieved = retriever.retrieve(query, topK);
List<RetrievedDocument> reranked = reranker.rerank(query, retrieved);

String context = reranked.stream()
.map(RetrievedDocument::content)
.collect(Collectors.joining("\n---\n"));

if (context.length() <= maxChars) {
return context;
}
return context.substring(0, maxChars);
}
}
45 changes: 45 additions & 0 deletions src/main/java/org/example/ai/ToolAgentFacade.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.example.ai;

import java.util.Map;

public class ToolAgentFacade {

public interface Tool {
String execute(String input);
}

private final RetrievalPipeline retrievalPipeline;
private final LlmClient llmClient;
private final Map<String, Tool> tools;

public ToolAgentFacade(RetrievalPipeline retrievalPipeline, LlmClient llmClient, Map<String, Tool> tools) {
this.retrievalPipeline = retrievalPipeline;
this.llmClient = llmClient;
this.tools = tools;
}

public String answer(String userQuery) {
if (userQuery.startsWith("tool:")) {
return invokeTool(userQuery);
}

String context = retrievalPipeline.buildContext(userQuery, 3, 500);
String prompt = new PromptBuilder("Use context to answer.\\nContext:\\n{{context}}\\nQuestion: {{question}}")
.with("context", context)
.with("question", userQuery)
.build();
return llmClient.complete(prompt);
}

private String invokeTool(String query) {
String[] parts = query.split(":", 3);
if (parts.length < 3) {
throw new IllegalArgumentException("Tool query must be tool:<name>:<input>");
}
Tool tool = tools.get(parts[1]);
if (tool == null) {
throw new IllegalArgumentException("Unknown tool: " + parts[1]);
}
return tool.execute(parts[2]);
}
}
Loading
Loading