Current section
Files
Jump to
Current section
Files
examples/llm_pipeline_orchestration.exs
#!/usr/bin/env elixir
# LLM Pipeline Orchestration Examples
# Demonstrates comprehensive patterns for LLM pipeline orchestration with Exdantic
defmodule LLMPipelineOrchestrationExamples do
@moduledoc """
Complete examples for LLM Pipeline Orchestration functionality in Exdantic.
This module demonstrates:
- Multi-step LLM validation pipelines
- Stage-based validation with error recovery
- Complex business logic validation chains
- Quality assessment integration
- Performance optimization patterns
- Error handling and retry logic
"""
# Example 1: Basic Pipeline Stage Definition
defmodule PipelineStage do
use Exdantic, define_struct: true
schema "Pipeline stage configuration" do
field :stage_name, :string, required: true
field :input_schema, :any, required: true # Schema module or runtime schema
field :output_schema, :any, required: true
field :llm_config, :map, default: %{}
field :validation_config, :map, default: %{}
field :retry_config, :map, default: %{max_retries: 3, backoff: :exponential}
model_validator :validate_stage_configuration
end
def validate_stage_configuration(input) do
# Validate that schemas are properly defined
cond do
is_nil(input.input_schema) ->
{:error, "Input schema cannot be nil"}
is_nil(input.output_schema) ->
{:error, "Output schema cannot be nil"}
true ->
{:ok, input}
end
end
end
# Example 2: Pipeline Definition
defmodule Pipeline do
use Exdantic, define_struct: true
schema "Multi-stage LLM pipeline" do
field :pipeline_id, :string, required: true
field :stages, {:array, PipelineStage}, min_items: 1
field :global_config, :map, default: %{}
field :error_handling, :string, choices: ["fail_fast", "continue", "retry_stage"], default: "fail_fast"
model_validator :validate_pipeline_flow
computed_field :stage_count, :integer, :count_stages
end
def validate_pipeline_flow(input) do
# Validate that output of stage N matches input of stage N+1
stage_pairs = Enum.zip(input.stages, tl(input.stages))
validation_errors = Enum.reduce(stage_pairs, [], fn {current_stage, next_stage}, errors ->
if schemas_compatible?(current_stage.output_schema, next_stage.input_schema) do
errors
else
error_msg = "Stage '#{current_stage.stage_name}' output incompatible with '#{next_stage.stage_name}' input"
[error_msg | errors]
end
end)
if validation_errors == [] do
{:ok, input}
else
{:error, Enum.join(validation_errors, "; ")}
end
end
def count_stages(input) do
{:ok, length(input.stages)}
end
defp schemas_compatible?(output_schema, input_schema) do
# Simple compatibility check - in practice, this would be more sophisticated
output_schema == input_schema or
(is_atom(output_schema) and is_atom(input_schema))
end
end
# Example 3: Input/Output Schemas for Different Stages
defmodule AnalysisInputSchema do
use Exdantic
schema "Analysis stage input" do
field :raw_data, :string, required: true, min_length: 10
field :analysis_type, :string, choices: ["sentiment", "entity", "summary", "classification"]
field :context, :string, optional: true
end
end
defmodule AnalysisOutputSchema do
use Exdantic
schema "Analysis stage output" do
field :analysis_result, :string, required: true, min_length: 10
field :confidence, :float, gteq: 0.0, lteq: 1.0
field :key_findings, {:array, :string}, optional: true
field :metadata, :map, default: %{}
end
end
defmodule SynthesisOutputSchema do
use Exdantic
schema "Synthesis stage output" do
field :synthesis, :string, required: true, min_length: 20
field :recommendations, {:array, :string}, min_items: 1
field :confidence, :float, gteq: 0.0, lteq: 1.0
field :supporting_evidence, {:array, :string}, optional: true
end
end
defmodule ValidationOutputSchema do
use Exdantic
schema "Final validation stage output" do
field :validation_result, :string, required: true
field :approved, :boolean, required: true
field :final_score, :float, gteq: 0.0, lteq: 1.0
field :quality_metrics, :map, default: %{}
end
end
# Example 4: Quality Assessment Schema
defmodule QualityMetrics do
use Exdantic, define_struct: true
schema "Quality assessment metrics" do
field :coherence_score, :float, gteq: 0.0, lteq: 1.0
field :relevance_score, :float, gteq: 0.0, lteq: 1.0
field :factual_accuracy, :float, gteq: 0.0, lteq: 1.0
field :completeness_score, :float, gteq: 0.0, lteq: 1.0
field :clarity_score, :float, gteq: 0.0, lteq: 1.0
computed_field :overall_quality, :float, :calculate_overall_quality
computed_field :quality_grade, :string, :assign_quality_grade
end
def calculate_overall_quality(input) do
scores = [
input.coherence_score,
input.relevance_score,
input.factual_accuracy,
input.completeness_score,
input.clarity_score
]
overall = Enum.sum(scores) / length(scores)
{:ok, overall}
end
def assign_quality_grade(input) do
grade = cond do
input.overall_quality >= 0.9 -> "A"
input.overall_quality >= 0.8 -> "B"
input.overall_quality >= 0.7 -> "C"
input.overall_quality >= 0.6 -> "D"
true -> "F"
end
{:ok, grade}
end
end
# Example 5: Pipeline Execution Engine
defmodule PipelineExecutor do
def execute_pipeline(pipeline_config, initial_input) do
config = Exdantic.Config.create(coercion: :safe, strict: true)
with {:ok, validated_pipeline} <- Exdantic.EnhancedValidator.validate(Pipeline, pipeline_config, config: config),
{:ok, final_result} <- execute_stages(validated_pipeline.stages, initial_input, validated_pipeline) do
{:ok, final_result}
else
{:error, reason} -> {:error, reason}
end
end
defp execute_stages(stages, input, pipeline) do
Enum.reduce_while(stages, {:ok, input}, fn stage, {:ok, current_input} ->
case execute_single_stage(stage, current_input, pipeline) do
{:ok, stage_output} ->
{:cont, {:ok, stage_output}}
{:error, reason} ->
case pipeline.error_handling do
"fail_fast" -> {:halt, {:error, {stage.stage_name, reason}}}
"continue" -> {:cont, {:ok, current_input}} # Skip failed stage
"retry_stage" -> handle_stage_retry(stage, current_input, pipeline)
end
end
end)
end
defp execute_single_stage(stage, input, _pipeline) do
validation_config = Exdantic.Config.create(
coercion: Map.get(stage.validation_config, :coercion, :safe),
strict: Map.get(stage.validation_config, :strict, true)
)
with {:ok, validated_input} <- validate_stage_input(stage, input, validation_config),
{:ok, llm_output} <- call_llm_for_stage(stage, validated_input),
{:ok, validated_output} <- validate_stage_output(stage, llm_output, validation_config) do
{:ok, validated_output}
else
{:error, reason} -> {:error, reason}
end
end
defp validate_stage_input(stage, input, config) do
Exdantic.EnhancedValidator.validate(stage.input_schema, input, config: config)
end
defp validate_stage_output(stage, output, config) do
Exdantic.EnhancedValidator.validate(stage.output_schema, output, config: config)
end
defp call_llm_for_stage(stage, validated_input) do
# Mock LLM call - in practice, this would call actual LLM APIs
case stage.stage_name do
"analysis" ->
{:ok, %{
"analysis_result" => "Comprehensive analysis of #{validated_input.analysis_type} completed",
"confidence" => 0.85,
"key_findings" => ["finding1", "finding2", "finding3"],
"metadata" => %{"processing_time" => 1.2, "model_used" => "gpt-4"}
}}
"synthesis" ->
{:ok, %{
"synthesis" => "Combined analysis shows positive trends with strong supporting evidence",
"recommendations" => ["action1", "action2", "action3"],
"confidence" => 0.9,
"supporting_evidence" => ["evidence1", "evidence2"]
}}
"validation" ->
{:ok, %{
"validation_result" => "All quality checks passed with high confidence",
"approved" => true,
"final_score" => 0.88,
"quality_metrics" => %{
"coherence" => 0.9,
"relevance" => 0.85,
"accuracy" => 0.92
}
}}
_ ->
{:error, "Unknown stage type: #{stage.stage_name}"}
end
end
defp handle_stage_retry(stage, input, pipeline) do
max_retries = get_in(stage.retry_config, [:max_retries]) || 3
attempt_stage_with_retries(stage, input, pipeline, max_retries)
end
defp attempt_stage_with_retries(stage, input, pipeline, retries_left) do
case execute_single_stage(stage, input, pipeline) do
{:ok, result} ->
{:cont, {:ok, result}}
{:error, _reason} when retries_left > 0 ->
# Implement backoff strategy
:timer.sleep(1000) # Simple linear backoff
attempt_stage_with_retries(stage, input, pipeline, retries_left - 1)
{:error, reason} ->
{:halt, {:error, {stage.stage_name, "Failed after retries: #{reason}"}}}
end
end
end
# Example 6: Quality Assessment Integration
defmodule QualityAssessor do
def assess_pipeline_quality(pipeline_result, original_input) do
# Simulate quality assessment
quality_data = %{
"coherence_score" => assess_coherence(pipeline_result),
"relevance_score" => assess_relevance(pipeline_result, original_input),
"factual_accuracy" => assess_accuracy(pipeline_result),
"completeness_score" => assess_completeness(pipeline_result),
"clarity_score" => assess_clarity(pipeline_result)
}
config = Exdantic.Config.create(coercion: :safe, strict: true)
Exdantic.EnhancedValidator.validate(QualityMetrics, quality_data, config: config)
end
defp assess_coherence(result) do
# Simple heuristic based on result structure
if is_map(result) and Map.has_key?(result, "synthesis") do
0.85 + :rand.uniform() * 0.1 # 0.85-0.95
else
0.7 + :rand.uniform() * 0.2 # 0.7-0.9
end
end
defp assess_relevance(result, _original_input) do
# Check if result addresses the input
if is_map(result) and Map.has_key?(result, "recommendations") do
0.8 + :rand.uniform() * 0.15 # 0.8-0.95
else
0.65 + :rand.uniform() * 0.25 # 0.65-0.9
end
end
defp assess_accuracy(result) do
# Check for factual consistency indicators
if is_map(result) and Map.get(result, "confidence", 0) > 0.8 do
0.88 + :rand.uniform() * 0.1 # 0.88-0.98
else
0.7 + :rand.uniform() * 0.2 # 0.7-0.9
end
end
defp assess_completeness(result) do
# Check if all expected fields are present
required_fields = ["validation_result", "approved", "final_score"]
present_fields = if is_map(result) do
Enum.count(required_fields, &Map.has_key?(result, &1))
else
0
end
present_fields / length(required_fields)
end
defp assess_clarity(result) do
# Simple length and structure check
if is_map(result) do
validation_result = Map.get(result, "validation_result", "")
if String.length(validation_result) > 20 do
0.8 + :rand.uniform() * 0.15 # 0.8-0.95
else
0.6 + :rand.uniform() * 0.3 # 0.6-0.9
end
else
0.5
end
end
end
def run do
IO.puts("=== LLM Pipeline Orchestration Examples ===\n")
# Example 1: Basic Pipeline Definition
basic_pipeline_definition()
# Example 2: Pipeline Execution
pipeline_execution()
# Example 3: Error Handling Strategies
error_handling_strategies()
# Example 4: Quality Assessment Integration
quality_assessment_integration()
# Example 5: Performance Analysis
performance_analysis()
# Example 6: Complex Multi-Stage Pipeline
complex_multi_stage_pipeline()
IO.puts("\n=== LLM Pipeline Orchestration Examples Complete ===")
end
defp basic_pipeline_definition do
IO.puts("1. Basic Pipeline Definition")
IO.puts("----------------------------")
# Define pipeline stages
analysis_stage = %{
"stage_name" => "analysis",
"input_schema" => AnalysisInputSchema,
"output_schema" => AnalysisOutputSchema,
"llm_config" => %{"model" => "gpt-4", "temperature" => 0.2},
"validation_config" => %{"strict" => true, "coercion" => :safe}
}
synthesis_stage = %{
"stage_name" => "synthesis",
"input_schema" => AnalysisOutputSchema,
"output_schema" => SynthesisOutputSchema,
"llm_config" => %{"model" => "gpt-4", "temperature" => 0.3}
}
validation_stage = %{
"stage_name" => "validation",
"input_schema" => SynthesisOutputSchema,
"output_schema" => ValidationOutputSchema,
"llm_config" => %{"model" => "gpt-3.5-turbo", "temperature" => 0.1}
}
# Define complete pipeline
pipeline_config = %{
"pipeline_id" => "market_analysis_pipeline",
"stages" => [analysis_stage, synthesis_stage, validation_stage],
"global_config" => %{"timeout" => 30000, "max_tokens" => 2000},
"error_handling" => "retry_stage"
}
IO.puts("Pipeline configuration:")
case Exdantic.EnhancedValidator.validate(Pipeline, pipeline_config) do
{:ok, validated_pipeline} ->
IO.puts(" ✅ Pipeline validated successfully")
IO.puts(" Pipeline ID: #{validated_pipeline.pipeline_id}")
IO.puts(" Stages: #{validated_pipeline.stage_count}")
IO.puts(" Error handling: #{validated_pipeline.error_handling}")
{:error, errors} ->
IO.puts(" ❌ Pipeline validation failed:")
Enum.each(errors, &IO.puts(" #{Exdantic.Error.format(&1)}"))
end
IO.puts("")
end
defp pipeline_execution do
IO.puts("2. Pipeline Execution")
IO.puts("---------------------")
# Create a valid pipeline
pipeline_config = create_sample_pipeline()
# Initial input data
initial_data = %{
"raw_data" => "Q4 financial reports show strong growth in renewable energy sector with 15% increase in revenue",
"analysis_type" => "sentiment",
"context" => "Market analysis for investment decisions"
}
IO.puts("Executing pipeline with input:")
IO.puts(" Raw data: #{initial_data["raw_data"]}")
IO.puts(" Analysis type: #{initial_data["analysis_type"]}")
case PipelineExecutor.execute_pipeline(pipeline_config, initial_data) do
{:ok, final_result} ->
IO.puts(" ✅ Pipeline completed successfully")
IO.puts(" Final result: #{inspect(final_result)}")
{:error, {stage_name, reason}} ->
IO.puts(" ❌ Pipeline failed at stage '#{stage_name}': #{reason}")
{:error, reason} ->
IO.puts(" ❌ Pipeline validation failed: #{inspect(reason)}")
end
IO.puts("")
end
defp error_handling_strategies do
IO.puts("3. Error Handling Strategies")
IO.puts("----------------------------")
# Test different error handling strategies
strategies = ["fail_fast", "continue", "retry_stage"]
Enum.each(strategies, fn strategy ->
IO.puts("Testing '#{strategy}' error handling:")
pipeline_config = create_sample_pipeline()
|> Map.put("error_handling", strategy)
# Simulate problematic input that might cause stage failures
problematic_input = %{
"raw_data" => "Short", # Too short, might cause validation errors
"analysis_type" => "invalid_type" # Invalid choice
}
case PipelineExecutor.execute_pipeline(pipeline_config, problematic_input) do
{:ok, result} ->
IO.puts(" ✅ Pipeline completed with strategy '#{strategy}'")
IO.puts(" Result: #{inspect(result)}")
{:error, {stage, reason}} ->
IO.puts(" ❌ Pipeline failed at stage '#{stage}' with strategy '#{strategy}': #{reason}")
{:error, reason} ->
IO.puts(" ❌ Pipeline error with strategy '#{strategy}': #{inspect(reason)}")
end
IO.puts("")
end)
end
defp quality_assessment_integration do
IO.puts("4. Quality Assessment Integration")
IO.puts("---------------------------------")
# Execute pipeline and assess quality
pipeline_config = create_sample_pipeline()
initial_data = %{
"raw_data" => "Comprehensive market analysis reveals positive trends in renewable energy adoption",
"analysis_type" => "summary",
"context" => "Investment strategy development"
}
case PipelineExecutor.execute_pipeline(pipeline_config, initial_data) do
{:ok, pipeline_result} ->
IO.puts("Pipeline execution completed, assessing quality...")
case QualityAssessor.assess_pipeline_quality(pipeline_result, initial_data) do
{:ok, quality_metrics} ->
IO.puts(" ✅ Quality assessment completed")
IO.puts(" Overall quality: #{quality_metrics.overall_quality}")
IO.puts(" Quality grade: #{quality_metrics.quality_grade}")
IO.puts(" Coherence: #{quality_metrics.coherence_score}")
IO.puts(" Relevance: #{quality_metrics.relevance_score}")
IO.puts(" Accuracy: #{quality_metrics.factual_accuracy}")
{:error, errors} ->
IO.puts(" ❌ Quality assessment failed:")
Enum.each(errors, &IO.puts(" #{Exdantic.Error.format(&1)}"))
end
{:error, reason} ->
IO.puts(" ❌ Pipeline failed, cannot assess quality: #{inspect(reason)}")
end
IO.puts("")
end
defp performance_analysis do
IO.puts("5. Performance Analysis")
IO.puts("-----------------------")
pipeline_config = create_sample_pipeline()
test_data = %{
"raw_data" => "Performance test data for pipeline execution timing analysis",
"analysis_type" => "classification",
"context" => "Performance benchmarking"
}
iterations = 5
IO.puts("Running performance analysis (#{iterations} iterations)...")
{total_time, results} = :timer.tc(fn ->
for i <- 1..iterations do
{time, result} = :timer.tc(fn ->
PipelineExecutor.execute_pipeline(pipeline_config, test_data)
end)
{i, time / 1000, result} # Convert to milliseconds
end
end)
successful_runs = Enum.count(results, fn {_, _, result} ->
match?({:ok, _}, result)
end)
execution_times = Enum.map(results, fn {_, time, _} -> time end)
avg_time = Enum.sum(execution_times) / length(execution_times)
min_time = Enum.min(execution_times)
max_time = Enum.max(execution_times)
IO.puts("Performance Results:")
IO.puts(" Total time: #{total_time / 1000} ms")
IO.puts(" Successful runs: #{successful_runs}/#{iterations}")
IO.puts(" Average execution time: #{Float.round(avg_time, 2)} ms")
IO.puts(" Min execution time: #{Float.round(min_time, 2)} ms")
IO.puts(" Max execution time: #{Float.round(max_time, 2)} ms")
IO.puts("")
end
defp complex_multi_stage_pipeline do
IO.puts("6. Complex Multi-Stage Pipeline")
IO.puts("-------------------------------")
# Create a more complex pipeline with additional stages
complex_pipeline = %{
"pipeline_id" => "complex_analysis_pipeline",
"stages" => [
%{
"stage_name" => "preprocessing",
"input_schema" => AnalysisInputSchema,
"output_schema" => AnalysisOutputSchema,
"llm_config" => %{"model" => "gpt-3.5-turbo", "temperature" => 0.1}
},
%{
"stage_name" => "analysis",
"input_schema" => AnalysisOutputSchema,
"output_schema" => AnalysisOutputSchema,
"llm_config" => %{"model" => "gpt-4", "temperature" => 0.2}
},
%{
"stage_name" => "synthesis",
"input_schema" => AnalysisOutputSchema,
"output_schema" => SynthesisOutputSchema,
"llm_config" => %{"model" => "gpt-4", "temperature" => 0.3}
},
%{
"stage_name" => "validation",
"input_schema" => SynthesisOutputSchema,
"output_schema" => ValidationOutputSchema,
"llm_config" => %{"model" => "gpt-4", "temperature" => 0.1}
}
],
"global_config" => %{
"timeout" => 60000,
"max_tokens" => 4000,
"retry_delay" => 2000
},
"error_handling" => "retry_stage"
}
complex_input = %{
"raw_data" => "Complex multi-dimensional analysis of market trends, consumer behavior, and economic indicators for strategic planning",
"analysis_type" => "summary",
"context" => "Strategic business planning and risk assessment"
}
IO.puts("Executing complex multi-stage pipeline...")
IO.puts(" Input: #{complex_input["raw_data"]}")
IO.puts(" Stages: #{length(complex_pipeline["stages"])}")
case PipelineExecutor.execute_pipeline(complex_pipeline, complex_input) do
{:ok, final_result} ->
IO.puts(" ✅ Complex pipeline completed successfully")
# Assess quality of complex pipeline result
case QualityAssessor.assess_pipeline_quality(final_result, complex_input) do
{:ok, quality_metrics} ->
IO.puts(" Final result quality: #{quality_metrics.quality_grade} (#{Float.round(quality_metrics.overall_quality, 3)})")
{:error, _} ->
IO.puts(" Quality assessment failed")
end
{:error, {stage, reason}} ->
IO.puts(" ❌ Complex pipeline failed at stage '#{stage}': #{reason}")
{:error, reason} ->
IO.puts(" ❌ Complex pipeline failed: #{inspect(reason)}")
end
IO.puts("")
end
# Helper function to create sample pipeline
defp create_sample_pipeline do
%{
"pipeline_id" => "sample_pipeline",
"stages" => [
%{
"stage_name" => "analysis",
"input_schema" => AnalysisInputSchema,
"output_schema" => AnalysisOutputSchema,
"llm_config" => %{"model" => "gpt-4", "temperature" => 0.2}
},
%{
"stage_name" => "synthesis",
"input_schema" => AnalysisOutputSchema,
"output_schema" => SynthesisOutputSchema,
"llm_config" => %{"model" => "gpt-4", "temperature" => 0.3}
},
%{
"stage_name" => "validation",
"input_schema" => SynthesisOutputSchema,
"output_schema" => ValidationOutputSchema,
"llm_config" => %{"model" => "gpt-3.5-turbo", "temperature" => 0.1}
}
],
"global_config" => %{"timeout" => 30000},
"error_handling" => "fail_fast"
}
end
end
# Run the examples
LLMPipelineOrchestrationExamples.run()