Prisms Guide
Mix.install(
[
{:lux, "~> 0.4.0"},
{:xml_builder, "~> 2.3"},
{:csv, "~> 3.2"},
{:swoosh, "~> 1.17"}
],
start_applications: false
)
Mix.Task.run("setup", install_deps: false)
Application.put_env(:venomous, :snake_manager, %{
python_opts: [
module_paths: [
Lux.Python.module_path(),
Lux.Python.module_path(:deps)
],
python_executable: "python3"
]
})
Application.ensure_all_started([:lux, :ex_unit])
Overview
Prisms are modular units of functionality that can be composed into workflows. They provide a way to encapsulate business logic, transformations, and integrations into reusable components.
A Prism consists of:
- A unique identifier
- Input and output schemas
- A handler function
- Optional configuration and metadata
Creating a Prism
Here’s a basic example of a Prism:
defmodule MyApp.Prisms.TextAnalysis do
use Lux.Prism,
name: "Text Analysis",
description: "Analyzes text for sentiment and key phrases",
input_schema: %{
type: :object,
properties: %{
text: %{type: :string, description: "Text to analyze"},
language: %{type: :string, description: "ISO language code"}
},
required: ["text"]
},
output_schema: %{
type: :object,
properties: %{
sentiment: %{
type: :string,
enum: ["positive", "negative", "neutral"]
},
confidence: %{type: :number},
key_phrases: %{
type: :array,
items: %{type: :string}
}
},
required: ["sentiment", "confidence"]
}
def handler(%{text: ""}, _ctx), do: {:error, :empty_text}
def handler(_input, _ctx) do
# Implementation
{:ok, %{
sentiment: "positive",
confidence: 0.95,
key_phrases: ["great", "awesome"]
}}
end
end
{:module, MyApp.Prisms.TextAnalysis, <<70, 79, 82, 49, 0, 0, 12, ...>>, {:handler, 2}}
Using Prisms
Prisms can be used directly or composed into Beams:
# Direct usage
{:ok, result} = MyApp.Prisms.TextAnalysis.run(%{
text: "Great product, highly recommended!",
language: "en"
})
result
%{sentiment: "positive", confidence: 0.95, key_phrases: ["great", "awesome"]}
Prism Types
Transformation Prisms
Transform data from one format to another:
defmodule MyApp.Prisms.DataTransformation do
use Lux.Prism,
name: "Data Transformer",
description: "Transforms data between formats",
input_schema: %{
type: :object,
properties: %{
data: %{type: :object},
format: %{type: :string, enum: ["json", "xml", "csv"]}
}
}
def handler(%{data: data, format: format}, _ctx) do
case format do
"json" -> {:ok, Jason.encode!(data)}
"xml" -> {:ok, XmlBuilder.generate(data)}
"csv" -> {:ok, CSV.encode(data)}
end
end
end
MyApp.Prisms.DataTransformation.run(%{
data: %{a: 1, b: 2},
format: "json"
})
{:ok, "{\"a\":1,\"b\":2}"}
Integration Prisms
Connect to external services:
defmodule MyApp.Prisms.EmailSender do
use Lux.Prism,
name: "Email Sender",
description: "Sends emails via SMTP",
input_schema: %{
type: :object,
properties: %{
to: %{type: :string},
subject: %{type: :string},
body: %{type: :string}
},
required: ["to", "subject", "body"]
}
def handler(params, _ctx) do
case Swoosh.Mailer.deliver(build_email(params), []) do
{:ok, _} -> {:ok, %{sent: true}}
{:error, reason} -> {:error, reason}
end
end
defp build_email(%{to: to, subject: subject, body: body}) do
Swoosh.Email.new()
|> Swoosh.Email.to(to)
|> Swoosh.Email.subject(subject)
|> Swoosh.Email.text_body(body)
end
end
{:module, MyApp.Prisms.EmailSender, <<70, 79, 82, 49, 0, 0, 12, ...>>, {:build_email, 1}}
Business Logic Prisms
Implement business rules and workflows:
defmodule MyApp.Prisms.OrderProcessor do
use Lux.Prism,
name: "Order Processor",
description: "Processes orders with business rules",
input_schema: %{
type: :object,
properties: %{
order: %{
type: :object,
properties: %{
items: %{type: :array},
total: %{type: :number},
customer: %{type: :object}
}
}
}
}
def handler(%{order: order}, _ctx) do
with :ok <- validate_inventory(order.items),
:ok <- validate_payment(order.total),
{:ok, processed} <- apply_discounts(order) do
{:ok,
%{
order_id: generate_order_id(),
processed_at: DateTime.utc_now(),
final_total: processed.total
}}
end
end
defp validate_inventory(_), do: :ok
defp validate_payment(_), do: :ok
defp apply_discounts(order), do: {:ok, order}
defp generate_order_id(), do: 1
end
MyApp.Prisms.OrderProcessor.run(%{
order: %{
items: [1, 2],
total: 2,
customer: %{}
}
})
{:ok, %{order_id: 1, processed_at: ~U[2025-02-08 18:22:11.925749Z], final_total: 2}}
Best Practices
-
Input/Output Schemas
- Define clear, specific schemas
- Document all properties
- Use appropriate types and constraints
- Include examples where helpful
-
Error Handling
-
Return
{:ok, result}
or{:error, reason}
- Provide meaningful error messages
- Handle all error cases
- Use pattern matching for validation
-
Return
-
Context Usage
- Use context for cross-cutting concerns
- Don’t rely on global state
- Pass necessary data through context
- Keep context usage minimal
-
Testing
- Test happy and error paths
- Mock external dependencies
- Test with various inputs
- Test error conditions
Example test:
defmodule MyApp.Prisms.TextAnalysisTest do
use UnitCase, async: true
alias MyApp.Prisms.TextAnalysis
describe "run/2" do
test "analyzes positive text" do
{:ok, result} = TextAnalysis.run(%{
text: "Great product!",
language: "en"
})
assert result.sentiment == "positive"
assert result.confidence > 0.8
assert "great" in result.key_phrases
end
test "handles empty text" do
assert {:error, _} = TextAnalysis.run(%{
text: "",
language: "en"
})
end
end
end
ExUnit.run()
Running ExUnit with seed: 616293, max_cases: 40
..
Finished in 0.00 seconds (0.00s async, 0.00s sync)
2 tests, 0 failures
%{total: 2, failures: 0, excluded: 0, skipped: 0}
Advanced Topics
Composable Prisms
Prisms can be composed together:
defmodule MyApp.Prisms.Validator do
use Lux.Prism,
name: "Validator",
description: "Validate input"
def handler(input, _ctx), do: {:ok, input}
end
defmodule MyApp.Prisms.Enricher do
use Lux.Prism,
name: "Enricher",
description: "Enrich input"
def handler(input, _ctx), do: {:ok, input}
end
defmodule MyApp.Prisms.Processor do
use Lux.Prism,
name: "Processor",
description: "Process input"
def handler(input, _ctx), do: {:ok, input}
end
defmodule MyApp.Prisms.Pipeline do
use Lux.Prism,
name: "Processing Pipeline",
description: "Chains multiple prisms"
def handler(input, _ctx) do
with {:ok, validated} <- MyApp.Prisms.Validator.run(input),
{:ok, enriched} <- MyApp.Prisms.Enricher.run(validated),
{:ok, processed} <- MyApp.Prisms.Processor.run(enriched) do
{:ok, processed}
end
end
end
MyApp.Prisms.Pipeline.run(%{})
{:ok, %{}}
Async Prisms
Handle long-running operations:
defmodule MyApp.Prisms.AsyncProcessor do
use Lux.Prism,
name: "Async Processor",
description: "Handles async operations"
def handler(_input, _ctx) do
task = Task.async(fn ->
# Long running operation
Process.sleep(5000)
{:ok, %{result: "done"}}
end)
case Task.yield(task, :timer.seconds(10)) do
{:ok, result} -> result
nil ->
Task.shutdown(task)
{:error, :timeout}
end
end
end
MyApp.Prisms.AsyncProcessor.run(%{})
{:ok, %{result: "done"}}
Python Integration
Prisms can leverage Python code directly in their handlers using Lux.Python
. This is particularly useful for machine learning, data processing, or when you need to use Python libraries:
defmodule MyApp.Prisms.SentimentAnalyzer do
use Lux.Prism,
name: "Sentiment Analyzer",
description: "Analyzes text sentiment using Python's NLTK",
input_schema: %{
type: :object,
properties: %{
text: %{type: :string, description: "Text to analyze"},
language: %{type: :string, description: "ISO language code"}
},
required: ["text"]
},
output_schema: %{
type: :object,
properties: %{
sentiment: %{type: :string, enum: ["positive", "negative", "neutral"]},
confidence: %{type: :number, minimum: 0, maximum: 1}
},
required: ["sentiment", "confidence"]
}
require Lux.Python
import Lux.Python
def handler(%{text: text, language: lang}, _ctx) do
# Import required Python packages
{:ok, %{"success" => true}} = Lux.Python.import_package("nltk")
# Execute Python code with variable bindings
result = python variables: %{text: text, lang: lang} do
~PY"""
import nltk
from nltk.sentiment import SentimentIntensityAnalyzer
# Download required NLTK data if not already present
try:
nltk.data.find('vader_lexicon')
except LookupError:
nltk.download('vader_lexicon')
# Analyze sentiment
sia = SentimentIntensityAnalyzer()
scores = sia.polarity_scores(text)
# Convert scores to our format
compound = scores['compound']
if compound >= 0.05:
sentiment = "positive"
elif compound <= -0.05:
sentiment = "negative"
else:
sentiment = "neutral"
# Return result
{
"sentiment": sentiment,
"confidence": abs(compound)
}
"""
end
{:ok, result}
end
end
MyApp.Prisms.SentimentAnalyzer.run(%{
text: "hello",
language: "en"
})
{:ok, %{"confidence" => 0.0, "sentiment" => "neutral"}}
defmodule MyApp.Prisms.CryptoAddressValidator do
use Lux.Prism,
name: "Crypto Address Validator",
description: "Validates cryptocurrency addresses",
input_schema: %{
type: :object,
properties: %{
address: %{type: :string, description: "The cryptocurrency address"},
chain: %{type: :string, enum: ["ethereum", "bitcoin"], description: "Chain type"}
},
required: ["address", "chain"]
}
require Lux.Python
import Lux.Python
def handler(%{address: address, chain: "ethereum"}, _ctx) do
# Import required packages
{:ok, %{"success" => true}} = Lux.Python.import_package("web3")
{:ok, %{"success" => true}} = Lux.Python.import_package("eth_utils")
result = python variables: %{address: address} do
~PY"""
from eth_utils import is_address, to_checksum_address
checksum_address = to_checksum_address(address)
is_valid = is_address(checksum_address)
{"is_valid": is_valid, "normalized_address": checksum_address}
"""
end
{:ok, result}
end
end
MyApp.Prisms.CryptoAddressValidator.run(%{
address: "0xd3CdA913deB6f67967B99D67aCDFa1712C293601",
chain: "ethereum"
})
{:ok, %{"is_valid" => true, "normalized_address" => "0xd3CdA913deB6f67967B99D67aCDFa1712C293601"}}
The Python integration supports:
-
Direct Python code execution with
~PY
sigils - Variable binding between Elixir and Python
-
Package management with
import_package/1
- Error handling and timeouts
- Multi-line Python code with proper indentation
- Access to the full Python ecosystem
Best practices for Python integration:
- Always handle package imports explicitly
- Use proper error handling for Python code execution
- Keep Python code focused and modular
- Leverage Python’s scientific and ML libraries when appropriate
- Use type hints and docstrings in Python code
- Follow both Elixir and Python style guides