├── test ├── test_helper.exs └── app_test.exs ├── config └── config.exs ├── .formatter.exs ├── lib ├── services │ ├── vars.ex │ └── manager.ex ├── application.ex ├── adapters │ ├── openai.ex │ └── telegram.ex └── app.ex ├── README.md ├── mix.exs ├── .gitignore └── mix.lock /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :openai, http_options: [recv_timeout: 60_000] 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test/app_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AppTest do 2 | use ExUnit.Case 3 | doctest App 4 | 5 | test "greets the world" do 6 | assert App.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/services/vars.ex: -------------------------------------------------------------------------------- 1 | defmodule Services.Vars do 2 | def get_var(%{var: var}, vars) do 3 | Map.get(vars, var) 4 | end 5 | 6 | def get_var(%{literal: var}, _) do 7 | var 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/application.ex: -------------------------------------------------------------------------------- 1 | defmodule App.Application do 2 | use Application 3 | 4 | def start(_, _) do 5 | children = [ 6 | Services.Manager 7 | ] 8 | 9 | opts = [strategy: :one_for_one, name: App.Supervisor] 10 | Supervisor.start_link(children, opts) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # App 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `app` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:app, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at . 21 | 22 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule App.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :app, 7 | version: "0.1.0", 8 | elixir: "~> 1.18", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | mod: {App.Application, []}, 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | defp deps do 22 | [ 23 | {:telegram, github: "visciang/telegram", tag: "2.1.0"}, 24 | {:openai, "~> 0.6.2"} 25 | ] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | 19 | # Ignore package tarball (built via "mix hex.build"). 20 | app-*.tar 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | -------------------------------------------------------------------------------- /lib/adapters/openai.ex: -------------------------------------------------------------------------------- 1 | defmodule Adapters.Openai do 2 | alias Services.Vars 3 | 4 | def start(action, vars) do 5 | props = action.props 6 | token = Vars.get_var(props.token, vars) 7 | user_msg = Vars.get_var(props.user, vars) 8 | model = Vars.get_var(props.model, vars) 9 | 10 | {:ok, data} = 11 | OpenAI.chat_completion( 12 | [ 13 | model: model, 14 | messages: [ 15 | %{ 16 | role: "user", 17 | content: user_msg 18 | } 19 | ] 20 | ], 21 | %OpenAI.Config{ 22 | api_key: token 23 | } 24 | ) 25 | 26 | output = data |> Map.get(:choices) |> Enum.at(0) |> Map.get("message") |> Map.get("content") 27 | vars |> Map.put("#{action.id}::output", output) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/app.ex: -------------------------------------------------------------------------------- 1 | defmodule App do 2 | def runner do 3 | Services.Manager.start_workflow(data()) 4 | end 5 | 6 | def data do 7 | token = "" 8 | 9 | %{ 10 | id: "main", 11 | trigger: %{ 12 | id: "t1", 13 | type: "telegram:trigger", 14 | props: %{ 15 | token: token 16 | } 17 | }, 18 | actions: [ 19 | %{ 20 | id: "a1", 21 | type: "openai:text", 22 | props: %{ 23 | token: %{ 24 | literal: "" 25 | }, 26 | user: %{ 27 | var: "t1::text" 28 | }, 29 | model: %{ 30 | literal: "gpt-4.1-nano" 31 | } 32 | } 33 | }, 34 | %{ 35 | id: "a2", 36 | type: "telegram:send-text", 37 | props: %{ 38 | token: %{ 39 | literal: token 40 | }, 41 | text: %{ 42 | var: "a1::output" 43 | }, 44 | chat_id: %{ 45 | var: "t1::chat_id" 46 | } 47 | } 48 | } 49 | ] 50 | } 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/adapters/telegram.ex: -------------------------------------------------------------------------------- 1 | defmodule Adapters.Telegram do 2 | alias Services.Vars 3 | 4 | def start(id, trigger) do 5 | handle_updates(id, trigger.props.token, trigger.id, 0) 6 | end 7 | 8 | def handle_updates(id, token, trigger_id, offset) do 9 | case Telegram.Api.request(token, "getUpdates", offset: offset) do 10 | {:ok, updates} -> 11 | new_offset = 12 | Enum.reduce(updates, offset, fn update, acc_offset -> 13 | text = update |> Map.get("message") |> Map.get("text") 14 | chat_id = update |> Map.get("message") |> Map.get("chat") |> Map.get("id") 15 | 16 | vars = 17 | Map.put(%{}, "#{trigger_id}::text", text) 18 | |> Map.put("#{trigger_id}::chat_id", chat_id) 19 | 20 | Services.Manager.handle_actions(id, vars) 21 | max(update["update_id"] + 1, acc_offset) 22 | end) 23 | 24 | handle_updates(id, token, trigger_id, new_offset) 25 | 26 | {:error, _} -> 27 | nil 28 | :timer.sleep(1000) 29 | handle_updates(id, token, trigger_id, offset) 30 | end 31 | end 32 | 33 | def send_message(action, vars) do 34 | props = Map.get(action, :props) 35 | token = Vars.get_var(props.token, vars) 36 | text = Vars.get_var(props.text, vars) 37 | chat_id = Vars.get_var(props.chat_id, vars) 38 | Telegram.Api.request(token, "sendMessage", chat_id: chat_id, text: text) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/services/manager.ex: -------------------------------------------------------------------------------- 1 | defmodule Services.Manager do 2 | alias Adapters.Openai 3 | alias Adapters.Telegram 4 | use GenServer 5 | 6 | def start_link(_) do 7 | GenServer.start_link(__MODULE__, %{tasks: %{}, actions: %{}}, name: __MODULE__) 8 | end 9 | 10 | def start_workflow(workflow) do 11 | id = workflow |> Map.get(:id) 12 | trigger = workflow |> Map.get(:trigger) 13 | actions = workflow |> Map.get(:actions) 14 | 15 | task = 16 | Task.start(fn -> 17 | run_trigger(trigger, id) 18 | end) 19 | 20 | GenServer.cast(__MODULE__, {:new, id, task, actions}) 21 | end 22 | 23 | def handle_actions(id, vars) do 24 | actions = GenServer.call(__MODULE__, {:get_action, id}) 25 | actions |> Enum.reduce(vars, fn action, vars -> run_action(action, vars) end) 26 | end 27 | 28 | def list_actions do 29 | GenServer.call(__MODULE__, :list) |> Map.get(:actions) 30 | end 31 | 32 | @impl true 33 | def init(init_arg) do 34 | {:ok, init_arg} 35 | end 36 | 37 | @impl true 38 | def handle_call({:get_action, id}, _, state) do 39 | {:reply, state |> Map.get(:actions) |> Map.get(id), state} 40 | end 41 | 42 | @impl true 43 | def handle_call(:list, _, state) do 44 | {:reply, state, state} 45 | end 46 | 47 | @impl true 48 | def handle_cast({:new, id, task, actions}, state) do 49 | {:noreply, 50 | state 51 | |> Map.update!(:tasks, fn tasks -> 52 | tasks |> Map.put(id, task) 53 | end) 54 | |> Map.update!(:actions, fn a -> a |> Map.put(id, actions) end)} 55 | end 56 | 57 | def run_trigger(%{type: "telegram:trigger"} = trigger, id) do 58 | Telegram.start(id, trigger) 59 | end 60 | 61 | def run_action(%{type: "telegram:send-text"} = action, vars) do 62 | Telegram.send_message(action, vars) 63 | end 64 | 65 | def run_action(%{type: "openai:text"} = action, vars) do 66 | Openai.start(action, vars) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, 3 | "hackney": {:hex, :hackney, "1.24.1", "f5205a125bba6ed4587f9db3cc7c729d11316fa8f215d3e57ed1c067a9703fa9", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "f4a7392a0b53d8bbc3eb855bdcc919cd677358e65b2afd3840b5b3690c4c8a39"}, 4 | "httpoison": {:hex, :httpoison, "2.2.3", "a599d4b34004cc60678999445da53b5e653630651d4da3d14675fedc9dd34bd6", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fa0f2e3646d3762fdc73edb532104c8619c7636a6997d20af4003da6cfc53e53"}, 5 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 6 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 7 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 8 | "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, 9 | "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, 10 | "openai": {:hex, :openai, "0.6.2", "48ee0dc74f4d0327ebf78eaeeed8c3595e10eca88fd2a7cdd8b53473645078d6", [:mix], [{:httpoison, "~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "508c8c2937ef8627d111d9142ff9cc284d39cd0c9b8244339551ac5f5fe0e643"}, 11 | "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, 12 | "plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"}, 13 | "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, 14 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 15 | "telegram": {:git, "https://github.com/visciang/telegram.git", "2f768457d97c1b5c04f1fa5d95060c743e21aa50", [tag: "2.1.0"]}, 16 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 17 | "tesla": {:hex, :tesla, "1.14.2", "fb1d4f0538bbb8a842c1d94028c886727e345f09f5239395eea19f1baa3314a0", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "ad6ac070f1052c96384693b07811e8b1974d9e98b66f29e0691f99926daf6127"}, 18 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, 19 | } 20 | --------------------------------------------------------------------------------