Packages

Advanced schema definition and validation library for Elixir

Current section

Files

Jump to
exdantic examples llm_integration.exs
Raw

examples/llm_integration.exs

#!/usr/bin/env elixir
# LLM Integration Examples
# Demonstrates comprehensive patterns for AI/LLM integration with Exdantic
defmodule LLMIntegrationExamples do
@moduledoc """
Complete examples for LLM integration patterns with Exdantic.
This module demonstrates:
- Structured output validation for LLM responses
- DSPy signature patterns
- Provider-specific optimizations (OpenAI, Anthropic)
- Chain-of-thought validation
- Multi-agent coordination
- Quality assessment
"""
# Example 1: Basic LLM Output Validation
defmodule LLMResponseSchema do
use Exdantic, define_struct: true
schema "LLM structured output validation" do
field :reasoning, :string do
description("Step-by-step reasoning process")
min_length(20)
end
field :answer, :string do
required()
min_length(1)
description("Final answer to the question")
end
field :confidence, :float do
required()
gteq(0.0)
lteq(1.0)
description("Confidence score between 0 and 1")
end
field :sources, {:array, :string} do
optional()
description("List of sources used")
end
# Cross-field validation
model_validator :validate_confidence_reasoning
# Computed metrics
computed_field :reasoning_word_count, :integer, :count_reasoning_words
computed_field :answer_category, :string, :categorize_answer
end
def validate_confidence_reasoning(input) do
if input.confidence > 0.8 and String.length(input.reasoning) < 50 do
{:error, "High confidence answers must include detailed reasoning"}
else
{:ok, input}
end
end
def count_reasoning_words(input) do
word_count = input.reasoning |> String.split() |> length()
{:ok, word_count}
end
def categorize_answer(input) do
category = cond do
String.length(input.answer) < 20 -> "brief"
String.length(input.answer) < 100 -> "standard"
true -> "detailed"
end
{:ok, category}
end
end
# Example 2: DSPy Signature Implementation
defmodule DSPySignature do
def create_qa_signature do
# Input schema for question answering
input_fields = [
{:question, :string, [description: "The question to answer"]},
{:context, :string, [description: "Relevant context for answering"]}
]
# Output schema for structured response
output_fields = [
{:answer, :string, [required: true, min_length: 5]},
{:reasoning, :string, [required: true, min_length: 20]},
{:confidence, :float, [required: true, gteq: 0.0, lteq: 1.0]}
]
%{
input: Exdantic.Runtime.create_schema(input_fields, title: "QA Input"),
output: Exdantic.Runtime.create_schema(output_fields, title: "QA Output"),
instruction: "Answer the question based on the provided context"
}
end
def validate_input(signature, input_data) do
config = Exdantic.Config.create(strict: true, coercion: :safe)
Exdantic.Runtime.validate(input_data, signature.input, config: config)
end
def validate_output(signature, output_data) do
config = Exdantic.Config.create(strict: false, coercion: :safe)
Exdantic.Runtime.validate(output_data, signature.output, config: config)
end
def get_json_schema(signature, provider \\ :openai) do
schema = Exdantic.Runtime.to_json_schema(signature.output)
Exdantic.JsonSchema.Resolver.enforce_structured_output(schema,
provider: provider,
remove_unsupported: true
)
end
end
# Example 3: Chain of Thought Validation
defmodule ChainOfThoughtSchema do
use Exdantic, define_struct: true
schema "Chain of thought reasoning validation" do
field :question, :string, min_length: 5
field :steps, {:array, :map}, min_items: 1
field :final_answer, :string, min_length: 1
field :overall_confidence, :float, gteq: 0.0, lteq: 1.0
model_validator :validate_reasoning_chain
computed_field :step_count, :integer, :count_steps
computed_field :average_step_confidence, :float, :calculate_avg_confidence
end
def validate_reasoning_chain(input) do
# Validate each step has required fields
steps_valid = Enum.all?(input.steps, fn step ->
Map.has_key?(step, "reasoning") and
Map.has_key?(step, "conclusion") and
Map.has_key?(step, "confidence")
end)
if not steps_valid do
{:error, "All steps must have reasoning, conclusion, and confidence"}
else
# Validate confidence consistency
step_confidences = Enum.map(input.steps, &Map.get(&1, "confidence", 0.0))
avg_confidence = Enum.sum(step_confidences) / length(step_confidences)
if abs(input.overall_confidence - avg_confidence) > 0.3 do
{:error, "Overall confidence must align with step confidences"}
else
{:ok, input}
end
end
end
def count_steps(input) do
{:ok, length(input.steps)}
end
def calculate_avg_confidence(input) do
if length(input.steps) == 0 do
{:ok, 0.0}
else
confidences = Enum.map(input.steps, &Map.get(&1, "confidence", 0.0))
avg = Enum.sum(confidences) / length(confidences)
{:ok, avg}
end
end
end
# Example 4: OpenAI Function Calling
defmodule OpenAIFunctionIntegration do
defmodule WeatherQuerySchema do
use Exdantic
schema "Get weather information for a location" do
field :location, :string do
required()
description("City and state, e.g. San Francisco, CA")
end
field :unit, :string do
choices(["celsius", "fahrenheit"])
default("fahrenheit")
description("Temperature unit")
end
field :include_forecast, :boolean do
default(false)
description("Include 5-day forecast")
end
end
end
def create_openai_function(schema_module, opts \\ []) do
json_schema = Exdantic.JsonSchema.from_schema(schema_module)
# Optimize for OpenAI function calling
optimized_schema = Exdantic.JsonSchema.Resolver.enforce_structured_output(
json_schema,
provider: :openai,
remove_unsupported: true
)
%{
name: Keyword.get(opts, :name, schema_module |> Module.split() |> List.last()),
description: Keyword.get(opts, :description, optimized_schema["description"]),
parameters: optimized_schema
}
end
def validate_function_call(schema_module, arguments) do
config = Exdantic.Config.create(strict: false, coercion: :safe, extra: :forbid)
Exdantic.EnhancedValidator.validate(schema_module, arguments, config: config)
end
end
# Example 5: Multi-Agent Coordination
defmodule MultiAgentSchema do
use Exdantic, define_struct: true
schema "Multi-agent coordination validation" do
field :agent_id, :string, required: true
field :message_type, :string, choices: ["query", "response", "coordination", "error"]
field :content, :string, min_length: 1
field :timestamp, :string, format: ~r/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/
field :confidence, :float, gteq: 0.0, lteq: 1.0
field :metadata, :map, default: %{}
computed_field :message_length, :integer, :calculate_message_length
computed_field :urgency_level, :string, :assess_urgency
end
def calculate_message_length(input) do
{:ok, String.length(input.content)}
end
def assess_urgency(input) do
urgency = cond do
input.message_type == "error" -> "high"
input.confidence < 0.3 -> "high"
input.message_type == "coordination" -> "medium"
true -> "low"
end
{:ok, urgency}
end
end
# Example 6: Quality Assessment
defmodule QualityAssessment do
defmodule QualityMetrics do
use Exdantic, define_struct: true
schema do
field :coherence_score, :float, gteq: 0.0, lteq: 1.0
field :relevance_score, :float, gteq: 0.0, lteq: 1.0
field :clarity_score, :float, gteq: 0.0, lteq: 1.0
field :completeness_score, :float, gteq: 0.0, lteq: 1.0
computed_field :overall_quality, :float, :calculate_overall
computed_field :quality_grade, :string, :assign_grade
end
def calculate_overall(input) do
scores = [
input.coherence_score,
input.relevance_score,
input.clarity_score,
input.completeness_score
]
overall = Enum.sum(scores) / length(scores)
{:ok, overall}
end
def assign_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
def assess_llm_output(content, context \\ nil) do
# Simple quality assessment algorithms
quality_metrics = %{
"coherence_score" => assess_coherence(content),
"relevance_score" => assess_relevance(content, context),
"clarity_score" => assess_clarity(content),
"completeness_score" => assess_completeness(content)
}
config = Exdantic.Config.create(coercion: :safe, strict: false)
Exdantic.EnhancedValidator.validate(QualityMetrics, quality_metrics, config: config)
end
defp assess_coherence(content) do
# Simple coherence assessment based on sentence structure
sentences = String.split(content, ~r/[.!?]+/) |> Enum.filter(&(String.trim(&1) != ""))
if length(sentences) < 2, do: 0.8, else: 0.7 + min(length(sentences) * 0.1, 0.2)
end
defp assess_relevance(_content, context) do
if is_nil(context), do: 0.7, else: 0.8 # Simplified
end
defp assess_clarity(content) do
# Based on average sentence length
sentences = String.split(content, ~r/[.!?]+/) |> Enum.filter(&(String.trim(&1) != ""))
if length(sentences) == 0 do
0.0
else
avg_length = String.length(content) / length(sentences)
cond do
avg_length < 10 -> 0.6
avg_length < 25 -> 0.9
avg_length < 40 -> 0.7
true -> 0.5
end
end
end
defp assess_completeness(content) do
# Based on content length
length = String.length(content)
cond do
length < 50 -> 0.4
length < 200 -> 0.7
length < 500 -> 0.9
true -> 0.8
end
end
end
# Example 7: Dynamic Schema Generation for LLM Outputs
defmodule DynamicLLMSchemas do
def create_analysis_schema(analysis_type) do
base_fields = [
{:input_data, :string, [required: true, description: "The data analyzed"]},
{:analysis_result, :string, [required: true, min_length: 20]}
]
type_specific_fields = case analysis_type do
:sentiment ->
[{:sentiment_score, :float, [gteq: -1.0, lteq: 1.0]},
{:emotion, :string, [choices: ["positive", "negative", "neutral"]]}]
:classification ->
[{:category, :string, [required: true]},
{:subcategory, :string, [optional: true]},
{:classification_confidence, :float, [gteq: 0.0, lteq: 1.0]}]
:extraction ->
[{:entities, {:array, :string}, [optional: true]},
{:keywords, {:array, :string}, [min_items: 1]}]
_ ->
[]
end
all_fields = base_fields ++ type_specific_fields
Exdantic.Runtime.create_schema(all_fields,
title: "#{analysis_type |> Atom.to_string() |> String.capitalize()} Analysis",
description: "Schema for #{analysis_type} analysis output"
)
end
def validate_analysis_output(analysis_type, llm_response) do
schema = create_analysis_schema(analysis_type)
config = Exdantic.Config.create(coercion: :safe, strict: false)
Exdantic.Runtime.validate(llm_response, schema, config: config)
end
end
# Main demonstration function
def run_examples do
IO.puts("=== LLM Integration Examples ===\n")
# Example 1: Basic LLM Output Validation
IO.puts("1. Basic LLM Output Validation")
llm_response = %{
"reasoning" => "Based on the analysis of the data, the trend shows consistent growth",
"answer" => "The market is experiencing positive growth",
"confidence" => 0.85, # Float instead of string
"sources" => ["report1.pdf", "data.csv"]
}
config = Exdantic.Config.create(coercion: :safe, strict: false)
case Exdantic.EnhancedValidator.validate(LLMResponseSchema, llm_response, config: config) do
{:ok, validated} ->
IO.puts("✓ LLM response validated successfully")
IO.puts(" Answer: #{validated.answer}")
IO.puts(" Confidence: #{validated.confidence}")
IO.puts(" Word count: #{validated.reasoning_word_count}")
IO.puts(" Category: #{validated.answer_category}")
{:error, errors} ->
IO.puts("✗ Validation failed: #{inspect(errors)}")
end
IO.puts("")
# Example 2: DSPy Signature
IO.puts("2. DSPy Signature Pattern")
qa_signature = DSPySignature.create_qa_signature()
input_data = %{
question: "What is the capital of France?",
context: "France is a country in Europe. Paris is its capital city."
}
{:ok, validated_input} = DSPySignature.validate_input(qa_signature, input_data)
IO.puts("✓ DSPy input validated: #{validated_input.question}")
# Simulate LLM response
llm_output = %{
"answer" => "Paris",
"reasoning" => "Based on the context, Paris is explicitly mentioned as the capital of France.",
"confidence" => 0.95 # Float instead of string
}
case DSPySignature.validate_output(qa_signature, llm_output) do
{:ok, validated_output} ->
IO.puts("✓ DSPy output validated: #{validated_output.answer}")
# Generate JSON schema for LLM prompt
_json_schema = DSPySignature.get_json_schema(qa_signature, :openai)
IO.puts(" Generated OpenAI-compatible JSON schema")
{:error, errors} ->
IO.puts("✗ DSPy output validation failed: #{inspect(errors)}")
end
IO.puts("")
# Example 3: Chain of Thought
IO.puts("3. Chain of Thought Validation")
chain_data = %{
"question" => "What are the benefits of renewable energy?",
"steps" => [
%{
"reasoning" => "Renewable energy sources like solar and wind are sustainable",
"conclusion" => "They don't deplete natural resources",
"confidence" => 0.9
},
%{
"reasoning" => "These sources produce minimal greenhouse gas emissions",
"conclusion" => "They help combat climate change",
"confidence" => 0.85
}
],
"final_answer" => "Renewable energy is sustainable and environmentally friendly",
"overall_confidence" => 0.87 # Float instead of string
}
config = Exdantic.Config.create(coercion: :safe, strict: false)
case Exdantic.EnhancedValidator.validate(ChainOfThoughtSchema, chain_data, config: config) do
{:ok, validated_chain} ->
IO.puts("✓ Chain of thought validated")
IO.puts(" Steps: #{validated_chain.step_count}")
IO.puts(" Average confidence: #{validated_chain.average_step_confidence}")
{:error, errors} ->
IO.puts("✗ Chain validation failed: #{inspect(errors)}")
end
IO.puts("")
# Example 4: OpenAI Function Calling
IO.puts("4. OpenAI Function Calling")
function_def = OpenAIFunctionIntegration.create_openai_function(
OpenAIFunctionIntegration.WeatherQuerySchema,
name: "get_weather",
description: "Get current weather for a location"
)
IO.puts("✓ Generated OpenAI function definition:")
IO.puts(" Name: #{function_def.name}")
IO.puts(" Description: #{function_def.description}")
# Validate function call arguments
function_args = %{
"location" => "San Francisco, CA",
"unit" => "celsius",
"include_forecast" => true # Boolean instead of string
}
case OpenAIFunctionIntegration.validate_function_call(
OpenAIFunctionIntegration.WeatherQuerySchema,
function_args
) do
{:ok, validated_args} ->
IO.puts("✓ Function arguments validated")
IO.puts(" Location: #{validated_args.location}")
IO.puts(" Include forecast: #{validated_args.include_forecast}")
{:error, errors} ->
IO.puts("✗ Function argument validation failed: #{inspect(errors)}")
end
IO.puts("")
# Example 5: Multi-Agent Message
IO.puts("5. Multi-Agent Coordination")
agent_message = %{
"agent_id" => "agent_analyzer",
"message_type" => "response",
"content" => "Analysis complete: Market shows 15% growth in Q4",
"timestamp" => "2024-01-01T10:00:00Z",
"confidence" => 0.9,
"metadata" => %{"source" => "market_data", "priority" => "high"}
}
config = Exdantic.Config.create(coercion: :safe, strict: false)
case Exdantic.EnhancedValidator.validate(MultiAgentSchema, agent_message, config: config) do
{:ok, validated_message} ->
IO.puts("✓ Agent message validated")
IO.puts(" Agent: #{validated_message.agent_id}")
IO.puts(" Message length: #{validated_message.message_length}")
IO.puts(" Urgency: #{validated_message.urgency_level}")
{:error, errors} ->
IO.puts("✗ Agent message validation failed: #{inspect(errors)}")
end
IO.puts("")
# Example 6: Quality Assessment
IO.puts("6. LLM Output Quality Assessment")
content = "Climate change requires immediate action. The evidence shows rising temperatures and sea levels. Multiple solutions exist including renewable energy and policy changes."
case QualityAssessment.assess_llm_output(content, "Explain climate change solutions") do
{:ok, quality} ->
IO.puts("✓ Quality assessment completed")
IO.puts(" Overall quality: #{quality.overall_quality}")
IO.puts(" Grade: #{quality.quality_grade}")
IO.puts(" Coherence: #{quality.coherence_score}")
IO.puts(" Clarity: #{quality.clarity_score}")
{:error, errors} ->
IO.puts("✗ Quality assessment failed: #{inspect(errors)}")
end
IO.puts("")
# Example 7: Dynamic Schema Generation
IO.puts("7. Dynamic Schema Generation")
sentiment_response = %{
"input_data" => "I love this new product, it's amazing!",
"analysis_result" => "The text expresses strong positive sentiment with enthusiastic language",
"sentiment_score" => 0.8,
"emotion" => "positive"
}
case DynamicLLMSchemas.validate_analysis_output(:sentiment, sentiment_response) do
{:ok, validated_analysis} ->
IO.puts("✓ Sentiment analysis validated")
IO.puts(" Result: #{validated_analysis.analysis_result}")
IO.puts(" Sentiment score: #{validated_analysis.sentiment_score}")
IO.puts(" Emotion: #{validated_analysis.emotion}")
{:error, errors} ->
IO.puts("✗ Analysis validation failed: #{inspect(errors)}")
end
IO.puts("\n=== All LLM Integration Examples Completed ===")
end
end
# Run the examples
LLMIntegrationExamples.run_examples()