Created
          April 24, 2025 15:43 
        
      - 
      
 - 
        
Save zblanco/c20b9f894624e2d91e35971799283c26 to your computer and use it in GitHub Desktop.  
Revisions
- 
        
zblanco created this gist
Apr 24, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,512 @@ # Runic - Demos ```elixir Mix.install([ {:kino, "~> 0.15.3"}, {:runic, github: "zblanco/runic", branch: "zw/map"} ]) ``` ## Graph Visualization ```elixir defmodule Kino.Cytoscape do use Kino.JS def new(g, options \\ []) def new(%Graph{} = graph, options) do graph |> to_edgelist_json() |> new(options(options)) end def new(edge_list, options) when is_list(options) do nodes = edge_list |> Enum.flat_map(fn %{data: edge} -> [ %{data: %{id: "#{edge.source}", name: "#{edge.source}"}}, %{data: %{id: "#{edge.target}", name: "#{edge.target}"}} ] end) |> Enum.uniq_by(& &1.data.id) new(nodes ++ edge_list, options(options)) end def new(graph, options) do Kino.JS.new(__MODULE__, Map.put(options, :elements, graph)) end defp options(:dag) do options = options([]) dag_layout = %{ name: "breadthfirst", fit: true, directed: true, avoidOverlap: true, spacingFactor: 1.0, grid: false, padding: 0, anime: true, circle: false, userZoomingEnabled: true } Map.put(options, :layout_options, dag_layout) end defp options(options) do cytoscape = options[:cytoscape] || %{ "zoomingEnabled" => true, "userZoomingEnabled" => true, "panningEnabled" => true, "userPanningEnabled" => true, "boxSelectionEnabled" => false } node_style = options[:node_style] || %{ "background-color" => "#ffffff", "font-size" => 14, "width" => 120, "shape" => "ellipse", "height" => 80, "text-wrap" => "wrap", "text-max-width" => 56, "color" => "#475569", "label" => "data(name)", "text-halign" => "center", "text-valign" => "center", "border-width" => 2, "border-color" => "#94a3b8", "border-opacity" => 0.5 } edge_style = options[:edge_style] || %{ "width" => 2, "font-size" => 12, "label" => "data(label)", "line-color" => "#94a3b8", "target-arrow-color" => "#94a3b8", "target-arrow-shape" => "triangle", "curve-style" => "bezier" } layout_options = options[:layout_options] || %{ name: "cose", fit: true, directed: true, avoidOverlap: true, spacingFactor: 1.0, grid: false, padding: 0, anime: true, circle: false, userZoomingEnabled: true } %{ cytoscape: cytoscape, node_style: node_style, edge_style: edge_style, layout_options: layout_options } end defp to_edgelist_json(graph) do vertices = graph |> Graph.vertices() |> Enum.map(fn v -> vertex_label = vertex_label(v) %{data: %{id: "#{vertex_label}", name: "#{vertex_label}"}} end) edges = graph |> Graph.edges() |> Enum.map(fn edge -> edge_label = edge_label(edge.label) v1 = vertex_label(edge.v1) v2 = vertex_label(edge.v2) %{ data: %{ id: "#{edge_label}-#{v1}#{v2}" |> :erlang.phash2(), label: "#{edge_label}", source: v1, target: v2 } } end) vertices ++ edges end defp edge_label(label), do: label defp vertex_label({_, _, _} = ast), do: Macro.to_string(ast) defp vertex_label(%Runic.Workflow.Root{}), do: :root defp vertex_label(%{name: name}) when not is_nil(name), do: name defp vertex_label(%{__struct__: struct} = vertex), do: "#{to_string(struct)}#{:erlang.phash2(vertex)}" defp vertex_label(otherwise), do: otherwise asset "main.js" do """ import "https://cdn.jsdelivr.net/npm/[email protected]/dist/cytoscape.min.js"; export function init(ctx, cyto_data) { ctx.root.innerHTML = `<div id='cyto' style='width: 896px; height: 400px;'></div>`; var cy = cytoscape({ ...cyto_data.cytoscape, ...{ container: document.getElementById('cyto'), elements: cyto_data.elements, style: [ { selector: 'node', style: cyto_data.node_style }, { selector: 'edge', style: cyto_data.edge_style } ] } }); cy.layout(cyto_data.layout_options).run(); } """ end end ``` ## Intro Runic is a tool for modeling your workflows as data that can be composed together at runtime. Runic components can be integrated into a Runic.Workflow and evaluated lazily in concurrent contexts. Runic Workflows are a decorated dataflow graph (a DAG - "directed acyclic graph") capable of modeling rules, pipelines, and state machines and more. Basic data flow dependencies such as in a pipeline are modeled as `%Step{}` structs (nodes/vertices) in the graph with directed edges (arrows) between steps. A step can be thought of as a simple input -> output lambda function. e.g. ## Examples ```elixir require Runic step = Runic.step(fn x -> x + 1 end, name: :add_one) ``` ```elixir workflow = Runic.workflow( name: "example pipeline workflow", steps: [ Runic.step(fn x -> x + 1 end, name: :add_one), Runic.step(fn x -> x * 2 end, name: :times_two), Runic.step(fn x -> x - 1 end, name: :minus_one) ] ) ``` ```elixir workflow.graph |> Kino.Cytoscape.new(:dag) ``` ```elixir alias Runic.Workflow wrk = workflow |> Workflow.react_until_satisfied(2) ``` ```elixir wrk |> Workflow.raw_productions() ``` ```elixir wrk |> Workflow.productions() ``` ```elixir serial_pipeline = Runic.workflow( name: "serial_pipeline", steps: [ {Runic.step(fn x -> x + 1 end, name: :add_one), [ {Runic.step(fn x -> x * 2 end, name: :times_two), [ Runic.step(fn x -> x - 1 end, name: :minus_one) ]} ]}, Runic.step(fn x -> x + 5 end, name: :add_five) ] ) ``` ```elixir serial_pipeline.graph |> Kino.Cytoscape.new(:dag) ``` ## Text Processing ```elixir defmodule TextProcessing do def tokenize(text) do text |> String.downcase() |> String.split(~r/[^[:alnum:]\-]/u, trim: true) end def count_words(list_of_words) do list_of_words |> Enum.reduce(Map.new(), fn word, map -> Map.update(map, word, 1, &(&1 + 1)) end) end def count_uniques(word_count) do Enum.count(word_count) end def first_word(list_of_words) do List.first(list_of_words) end def last_word(list_of_words) do List.last(list_of_words) end end ``` ```elixir import TextProcessing word_count = "anybody want a peanut?" |> tokenize() |> count_words() |> dbg() first_word = "anybody want a peanut?" |> tokenize() |> first_word() |> dbg() last_word = "anybody want a peanut?" |> tokenize() |> last_word() |> dbg() ``` ```elixir text_processing_workflow = Runic.workflow( name: "basic text processing example", steps: [ {Runic.step(&tokenize/1), [ {Runic.step(&count_words/1), [ Runic.step(&count_uniques/1) ]}, Runic.step(&first_word/1), Runic.step(&last_word/1) ]} ] ) ``` ```elixir text_processing_workflow.graph |> Kino.Cytoscape.new(:dag) ``` ```elixir text_processing_workflow |> Workflow.react_until_satisfied("anybody want a peanut?") |> Workflow.raw_productions() ``` ## Joins ```elixir join_with_many_dependencies = Runic.workflow( name: "workflow with joins", steps: [ {[Runic.step(fn num -> num * 2 end), Runic.step(fn num -> num * 3 end)], [ Runic.step(fn num_1, num_2 -> num_1 * num_2 end), Runic.step(fn num_1, num_2 -> num_1 + num_2 end), Runic.step(fn num_1, num_2 -> num_2 - num_1 end) ]} ] ) ``` ```elixir join_with_many_dependencies.graph |> Kino.Cytoscape.new(:dag) ``` ## Rules ```elixir rules_workflow = Runic.workflow( name: "a test workflow", rules: [ Runic.rule(fn :foo -> :bar end, name: "foobar"), Runic.rule( if: fn lhs -> lhs == :potato end, do: fn rhs -> rhs == :tomato end, name: "tomato when potato" ), Runic.rule( fn item when is_integer(item) and item > 41 and item < 43 -> "the answer to life the universe and everything" end, name: "what about the question?" ) ] ) ``` ```elixir rules_workflow.graph |> Kino.Cytoscape.new(:dag) ``` ## State Machines ```elixir state_machine = Runic.state_machine( name: "transitional_factor", init: 0, reducer: fn num, state when is_integer(num) and state >= 0 and state < 10 -> state + num * 1 num, state when is_integer(num) and state >= 10 and state < 20 -> state + num * 2 num, state when is_integer(num) and state >= 20 and state < 30 -> state + num * 3 _num, state -> state end ) |> Runic.transmute() ``` ```elixir state_machine.graph |> Kino.Cytoscape.new(:dag) ``` ## Map ```elixir map_expression = Runic.workflow( name: "map test", steps: [ {Runic.step(fn num -> Enum.map(0..3, &(&1 + num)) end), [ Runic.map( {Runic.step(fn num -> num * 2 end), [ Runic.step(fn num -> num + 1 end), Runic.step(fn num -> num + 4 end) ]} ) ]} ] ) ``` ```elixir map_expression.graph |> Kino.Cytoscape.new(:dag) ``` ## Reduce ```elixir reduce_example = Runic.workflow( name: "reduce test", steps: [ {Runic.step(fn num -> Enum.map(0..3, &(&1 + num)) end), [ {Runic.map(fn num -> num * 2 end), [ Runic.reduce([], fn num, acc -> [num | acc] end) ]} ]} ] ) ``` ```elixir reduce_example.graph |> Kino.Cytoscape.new(:dag) ``` ```elixir reduce_wrk = Runic.workflow( name: "reduce test", steps: [ {Runic.step(fn _ -> 0..3 end), [ Runic.map( {step(fn num -> num + 1 end), [ Runic.step(fn num -> num + 4 end), Runic.step(fn num -> num + 2 end, name: :plus2) ]}, name: "map" ) ]} ] ) reduce_wrk = Workflow.add( reduce_wrk, Runic.reduce(0, fn num, acc -> num + acc end, name: "reduce", map: "map"), to: :plus2 ) ``` ```elixir reduce_wrk |> Workflow.react_until_satisfied(:anything) |> Workflow.productions() ``` ```elixir reduce_wrk.graph |> Kino.Cytoscape.new(:dag) ```