Run this notebook

Use Livebook to open this notebook and explore new ideas.

It is easy to get started, on your machine or the cloud.

Click below to open and run it in your Livebook at .

(or change your Livebook location)

<!-- livebook:{"persist_outputs":true} --> # Prisms Guide ```elixir Mix.install( [ {:lux, ">= 0.5.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]) ``` ## Overview <a href="https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2FSpectral-Finance%2Flux%2Fblob%2Fmain%2Flux%2Fguides%2Fprisms.livemd" style="display: none"> <img src="https://livebook.dev/badge/v1/blue.svg" alt="Run in Livebook" /> </a> 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: ```elixir 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 ``` <!-- livebook:{"output":true} --> ``` {:module, MyApp.Prisms.TextAnalysis, <<70, 79, 82, 49, 0, 0, 12, ...>>, {:handler, 2}} ``` ## Using Prisms Prisms can be used directly or composed into Beams: ```elixir # Direct usage {:ok, result} = MyApp.Prisms.TextAnalysis.run(%{ text: "Great product, highly recommended!", language: "en" }) result ``` <!-- livebook:{"output":true} --> ``` %{sentiment: "positive", confidence: 0.95, key_phrases: ["great", "awesome"]} ``` ## Prism Types ### Transformation Prisms Transform data from one format to another: ```elixir 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" }) ``` <!-- livebook:{"output":true} --> ``` {:ok, "{\"a\":1,\"b\":2}"} ``` ### Integration Prisms Connect to external services: ```elixir 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 ``` <!-- livebook:{"output":true} --> ``` {:module, MyApp.Prisms.EmailSender, <<70, 79, 82, 49, 0, 0, 12, ...>>, {:build_email, 1}} ``` ### Business Logic Prisms Implement business rules and workflows: ```elixir 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: %{} } }) ``` <!-- livebook:{"output":true} --> ``` {:ok, %{order_id: 1, processed_at: ~U[2025-02-08 18:22:11.925749Z], final_total: 2}} ``` ## Best Practices 1. **Input/Output Schemas** * Define clear, specific schemas * Document all properties * Use appropriate types and constraints * Include examples where helpful 2. **Error Handling** * Return `{:ok, result}` or `{:error, reason}` * Provide meaningful error messages * Handle all error cases * Use pattern matching for validation 3. **Context Usage** * Use context for cross-cutting concerns * Don't rely on global state * Pass necessary data through context * Keep context usage minimal 4. **Testing** * Test happy and error paths * Mock external dependencies * Test with various inputs * Test error conditions Example test: <!-- livebook:{"force_markdown":true} --> ```elixir 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 ``` ## Advanced Topics ### Composable Prisms Prisms can be composed together: ```elixir 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(%{}) ``` <!-- livebook:{"output":true} --> ``` {:ok, %{}} ``` ### Async Prisms Handle long-running operations: ```elixir 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(%{}) ``` <!-- livebook:{"output":true} --> ``` {: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: ```elixir 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" }) ``` <!-- livebook:{"output":true} --> ``` {:ok, %{"confidence" => 0.0, "sentiment" => "neutral"}} ``` ```elixir 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" }) ``` <!-- livebook:{"output":true} --> ``` {:ok, %{"is_valid" => true, "normalized_address" => "0xd3CdA913deB6f67967B99D67aCDFa1712C293601"}} ``` #### Pure Python Prisms You can define a prism entirely in Python. Extend the [Prism](https://github.com/Spectral-Finance/lux/blob/main/lux/priv/python/lux/prism.py) abstract class from `lux` package and implement `handler` and `new` methods: ```python from lux import Prism class SimplePrism(Prism): id='aaace2b1-8089-4d4e-b68e-2ce904da12f0' name='Simple Prism' description='A very simple prism that greets you' def handler(self, input, context): return { "success": True, "data": { "message": f'Hello, {input.get("name", "prism")}!', }, } @staticmethod def new(): return SimplePrism() ``` You can load Python prism using `Lux.Prism.view/1` with file path. Please keep in mind that your Python prism runs in the `venv` set up during initial setup. Only the packages listed in [`pyproject.toml`](../priv/python/pyproject.toml) are available to your Python prisms. While you access the prism with `Lux.Prism.view/1`, `SimplePrism` module is dynamically defined and available in Elixir runtime. <!-- livebook:{"force_markdown":true} --> ```elixir prism = Lux.Prism.view("path/to/prism.py") ``` You can also access the Python prism via dynamically defined module `SimplePrism` to Elixir: <!-- livebook:{"force_markdown":true} --> ```elixir SimplePrism.view() ``` In the agent module, use `{:python, "path/to/prism.py"}` tuple to indicate that the prism is implemented in Python. <!-- livebook:{"force_markdown":true} --> ```elixir defmodule MyApp.Agent do use Lux.Agent, prisms: [ {:python, "path/to/prism.py"} ], signal_handlers: [ {:python, "path/to/prism.py"} ] end ``` 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: 1. Always handle package imports explicitly 2. Use proper error handling for Python code execution 3. Keep Python code focused and modular 4. Leverage Python's scientific and ML libraries when appropriate 5. Use type hints and docstrings in Python code 6. Follow both Elixir and Python style guides Want to learn more about Python integration? Check out the [Python language support doc](language_support/python.livemd).
See source

Have you already installed Livebook?

If you already installed Livebook, you can configure the default Livebook location where you want to open notebooks.
Livebook up Checking status We can't reach this Livebook (but we saved your preference anyway)
Run notebook

Not yet? Install Livebook in just a minute

Livebook is open source, free, and ready to run anywhere.

Run on your machine

with Livebook Desktop

Run in the cloud

on select platforms

To run on Linux, Docker, embedded devices, or Elixir’s Mix, check our README.

PLATINUM SPONSORS
SPONSORS
Code navigation with go to definition of modules and functions Read More ×