{"id":28605824,"url":"https://github.com/ash-project/ash_ai","last_synced_at":"2025-07-29T13:16:28.064Z","repository":{"id":255427254,"uuid":"849581912","full_name":"ash-project/ash_ai","owner":"ash-project","description":"Structured outputs, vectorization and tool calling for your Ash application","archived":false,"fork":false,"pushed_at":"2025-07-23T13:26:00.000Z","size":2078,"stargazers_count":114,"open_issues_count":5,"forks_count":33,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-07-26T18:52:10.548Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://ash-hq.org","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ash-project.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":["zachdaniel","jimsynz"]}},"created_at":"2024-08-29T21:20:18.000Z","updated_at":"2025-07-24T06:57:39.000Z","dependencies_parsed_at":"2024-08-29T23:44:31.829Z","dependency_job_id":"084e0fd6-05f3-44e0-957c-ab7b9b55dc40","html_url":"https://github.com/ash-project/ash_ai","commit_stats":null,"previous_names":["ash-project/ash_ai"],"tags_count":22,"template":false,"template_full_name":null,"purl":"pkg:github/ash-project/ash_ai","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ash-project%2Fash_ai","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ash-project%2Fash_ai/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ash-project%2Fash_ai/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ash-project%2Fash_ai/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ash-project","download_url":"https://codeload.github.com/ash-project/ash_ai/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ash-project%2Fash_ai/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267250638,"owners_count":24060007,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-26T02:00:08.937Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-06-11T19:02:00.709Z","updated_at":"2025-07-29T13:16:28.051Z","avatar_url":"https://github.com/ash-project.png","language":"Elixir","funding_links":["https://github.com/sponsors/zachdaniel","https://github.com/sponsors/jimsynz"],"categories":["High-Level Agent Frameworks","Elixir","Artificial Intelligence","Agent Frameworks","AI Related projects","Generative AI"],"sub_categories":["How to Join","Phoenix LiveView Components","LLM Tools"],"readme":"# Ash AI\n\u003cimg src=\"https://github.com/ash-project/ash_ai/blob/main/logos/ash_ai.png?raw=true\" alt=\"Logo\" width=\"300\"/\u003e\n\n[![DeepWiki](https://img.shields.io/badge/DeepWiki-ash--project%2Fash__ai-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/ash-project/ash_ai)\n\n\n## Installation\n\n\u003c!-- tabs-open --\u003e\n\n### With Igniter\n\nYou can install `AshAi` using igniter. For example:\n```sh\nmix igniter.install ash_ai\n```\n\n### Manually\n\nAdd `AshAi` to your list of dependencies:\n\n```elixir\ndef deps do\n  [\n    {:ash_ai, \"~\u003e 0.2\"}\n  ]\nend\n```\n\n\u003c!-- tabs-close --\u003e\n\n\n## MCP (Model Context Protocol) Server\n\nBoth the dev \u0026 production MCP servers can be installed with\n\n`mix ash_ai.gen.mcp`\n\n### Dev MCP Server\n\nTo install the dev MCP server, add the `AshAi.Mcp.Dev` plug to your\nendpoint module, in the `code_reloading?` block. By default the\nmcp server will be available under `http://localhost:4000/ash_ai/mcp`.\n\n\n\n```elixir\n  if code_reloading? do\n    socket \"/phoenix/live_reload/socket\", Phoenix.LiveReloader.Socket\n\n    plug AshAi.Mcp.Dev,\n      # see the note below on protocol versions below\n      protocol_version_statement: \"2024-11-05\",\n      otp_app: :your_app\n```\n\nWe are still experimenting to see what tools (if any) are useful while developing with agents.\n\n### Production MCP Server\n\nAshAi provides a pre-built MCP server that can be used to expose your tool definitions to an MCP client (typically some kind of IDE, or Claude Desktop for example).\n\nThe protocol version we implement is 2025-03-26. As of this writing, many tools have not yet been updated to support this version. You will generally need to use some kind of proxy until tools have been updated accordingly. We suggest this one, provided by tidewave. https://github.com/tidewave-ai/mcp_proxy_rust#installation\n\nHowever, as of the writing of this guide, it requires setting a previous protocol version as noted above.\n\n#### Roadmap\n\n- Implement OAuth2 flow with AshAuthentication (long term)\n- Implement support for more than just tools, i.e resources etc.\n- Implement sessions, and provide a session id context to tools (this code is just commented out, and can be uncommented, just needs timeout logic for inactive sesions)\n\n#### Installation\n\n##### Authentication\n\nWe don't currently support the OAuth2 flow out of the box with AshAi, but the goal is to eventually support this with AshAuthentication. You can always implement that yourself, but the quickest way to value is to use the new `api_key` strategy.\n\nIf you haven't installed `AshAuthentication` yet, install it like so: `mix igniter.install ash_authentication --auth-strategy api_key`.\nIf its already been installed, and you haven't set up API keys, use `mix ash_authentication.add_strategy api_key`.\n\nThen, create a separate pipeline for `:mcp`, and add the api key plug to it:\n\n```elixir\npipeline :mcp do\n  plug AshAuthentication.Strategy.ApiKey.Plug,\n    resource: YourApp.Accounts.User,\n    # Use `required?: false` to allow unauthenticated\n    # users to connect, for example if some tools\n    # are publicly accessible.\n    required?: false\nend\n```\n\n##### Add the MCP server to your router\n\n```elixir\nscope \"/mcp\" do\n  pipe_through :mcp\n\n  forward \"/\", AshAi.Mcp.Router,\n    tools: [\n      :list,\n      :of,\n      :tools\n    ],\n    # For many tools, you will need to set the `protocol_version_statement` to the older version.\n    protocol_version_statement: \"2024-11-05\",\n    otp_app: :my_app\nend\n```\n\n## `mix ash_ai.gen.chat`\n\nThis is a new and experimental tool to generate a chat feature for your Ash \u0026 Phoenix application. It is backed by `ash_oban` and `ash_postgres`, using `pub_sub` to stream messages to the client. This is primarily a tool to get started with chat features and is by no means intended to handle every case you can come up with.\n\nTo get started:\n```\nmix ash_ai.gen.chat --live\n```\n\nThe `--live` flag indicates that you wish to generate liveviews in addition to the chat resources.\n\nIt currently requires a `user` resource to exist. If your `user` resource is not called `\u003cYourApp\u003e.Accounts.User`, provide a custom user resource with the `--user`\nflag.\n\nTo try it out from scratch:\n\n```sh\nmix igniter.new my_app \\\n  --with phx.new \\\n  --install ash,ash_postgres,ash_phoenix \\\n  --install ash_authentication_phoenix,ash_oban \\\n  --install ash_ai@github:ash-project/ash_ai \\\n  --auth-strategy password\n```\n\nand then run:\n\n```sh\nmix ash_ai.gen.chat --live\n```\n\nYou can then start your server and visit `http://localhost:4000/chat` to see the chat feature in action. You will be prompted to register first and sign in the first time.\n\n## Expose actions as tool calls\n\n```elixir\ndefmodule MyApp.Blog do\n  use Ash.Domain, extensions: [AshAi]\n\n  tools do\n    tool :read_posts, MyApp.Blog.Post, :read\n    tool :create_post, MyApp.Blog.Post, :create\n    tool :publish_post, MyApp.Blog.Post, :publish\n    tool :read_comments, MyApp.Blog.Comment, :read\n  end\nend\n```\n\nExpose these actions as tools. When you call `AshAi.setup_ash_ai(chain, opts)`, or `AshAi.iex_chat/2`\nit will add those as tool calls to the agent.\n\n### Tool Data Access\n\n**Important**: Tools have different access levels for different operations:\n- **Filtering/Sorting/Aggregation**: Only public attributes (`public?: true`) can be used\n- **Arguments**: Only public action arguments are exposed\n- **Response data**: Public attributes are returned by default\n- **Loading data**: Use the `load` option to include relationships, calculations, or additional attributes (including private ones) in responses\n\nExample:\n```elixir\ntools do\n  # Returns only public attributes\n  tool :read_posts, MyApp.Blog.Post, :read\n  \n  # Returns public attributes AND loaded relationships/calculations\n  # Note: loaded fields can include private attributes\n  tool :read_posts_with_details, MyApp.Blog.Post, :read,\n    load: [:author, :comment_count, :internal_notes]\nend\n```\n\nKey distinction:\n- Private attributes cannot be used for filtering, sorting, or aggregation\n- Private attributes CAN be included in responses when using the `load` option\n- The `load` option is primarily for loading relationships and calculations, but also makes any loaded attributes (including private ones) visible\n\n### Tool Execution Callbacks\n\nMonitor tool execution in real-time by providing callbacks to `AshAi.setup_ash_ai/2`:\n\n```elixir\nchain\n|\u003e AshAi.setup_ash_ai(\n  actor: current_user,\n  on_tool_start: fn %AshAi.ToolStartEvent{} = event -\u003e\n    # event includes: tool_name, action, resource, arguments, actor, tenant\n    IO.puts(\"Starting #{event.tool_name}...\")\n  end,\n  on_tool_end: fn %AshAi.ToolEndEvent{} = event -\u003e\n    # event includes: tool_name, result ({:ok, ...} or {:error, ...})\n    IO.puts(\"Completed #{event.tool_name}\")\n  end\n)\n```\n\nThis is useful for showing progress indicators, logging, metrics collection, or debugging tool execution.\n\n## Prompt-backed actions\n\nThis allows defining an action, including input and output types, and delegating the\nimplementation to an LLM. We use structured outputs to ensure that it always returns\nthe correct data type. We also derive a default prompt from the action description and\naction inputs. See `AshAi.Actions.Prompt` for more information.\n\n```elixir\naction :analyze_sentiment, :atom do\n  constraints one_of: [:positive, :negative]\n\n  description \"\"\"\n  Analyzes the sentiment of a given piece of text to determine if it is overall positive or negative.\n  \"\"\"\n\n  argument :text, :string do\n    allow_nil? false\n    description \"The text for analysis\"\n  end\n\n  run prompt(\n    LangChain.ChatModels.ChatOpenAI.new!(%{ model: \"gpt-4o\"}),\n    # setting `tools: true` allows it to use all exposed tools in your app\n    tools: true\n    # alternatively you can restrict it to only a set of tools\n    # tools: [:list, :of, :tool, :names]\n    # provide an optional prompt, which is an EEx template\n     # prompt: \"Analyze the sentiment of the following text: \u003c%= @input.arguments.description %\u003e\",\n    # adapter: {Adapter, [some: :opt]}\n  )\nend\n```\n\n### Using Custom Types for Structured Outputs\n\nThe action's return type provides the JSON schema automatically. For complex structured outputs, you can use any Ash type:\n\n```elixir\n# Example using Ash.TypedStruct\ndefmodule JobListing do\n  use Ash.TypedStruct\n\n  typed_struct do\n    field :title, :string, allow_nil?: false\n    field :company, :string, allow_nil?: false\n    field :location, :string\n    field :requirements, {:array, :string}\n  end\nend\n\n# Use it as the return type for your action\naction :parse_job, JobListing do\n  argument :raw_content, :string, allow_nil?: false\n\n  run prompt(\n    LangChain.ChatModels.ChatOpenAI.new!(%{model: \"gpt-4o-mini\"}),\n    prompt: \"Parse this job listing: \u003c%= @input.arguments.raw_content %\u003e\",\n    tools: false\n  )\nend\n```\n\n## Adapters\n\nAdapters are used to determine how a given LLM fulfills a prompt-backed action. The adapter is guessed automatically from the model where possible.\nSee `AshAi.Actions.Prompt.Adapter` for more information.\n\n### Setting up LangChain\n\nFor any langchain models you use, you will need to configure them. See https://hexdocs.pm/langchain/ for more information.\n\nFor AshAI Specific changes to use different models:\n- [Google Gemini 2.5](/documentation/models/gemini.md)\n\n## Vectorization\n\nSee `AshPostgres` vector setup for required steps: https://hexdocs.pm/ash_postgres/AshPostgres.Extensions.Vector.html\n\nThis extension creates a vector search action, and provides a few different strategies for how to\nupdate the embeddings when needed.\n\n```elixir\n# in a resource\n\nvectorize do\n  full_text do\n    text(fn record -\u003e\n      \"\"\"\n      Name: #{record.name}\n      Biography: #{record.biography}\n      \"\"\"\n    end)\n\n    # When used_attributes are defined, embeddings will only be rebuilt when\n    # the listed attributes are changed in an update action.\n    used_attributes [:name, :biography]\n  end\n\n  strategy :after_action\n  attributes(name: :vectorized_name, biography: :vectorized_biography)\n\n  # See the section below on defining an embedding model\n  embedding_model MyApp.OpenAiEmbeddingModel\nend\n```\n\nIf you are using policies, add a bypass to allow us to update the vector embeddings:\n\n```elixir\nbypass action(:ash_ai_update_embeddings) do\n  authorize_if AshAi.Checks.ActorIsAshAi\nend\n```\n\n## Vectorization strategies\n\nCurrently there are three strategies to choose from:\n\n- `:after_action` (default) - The embeddings will be updated synchronously on after every create \u0026 update action.\n- `:ash_oban` - Embeddings will be updated asynchronously through an `ash_oban`-trigger when a record is created and updated.\n- `:manual` - The embeddings will not be automatically updated in any way.\n\n### `:after_action`\n\nWill add a global change on the resource, that will run a generated action named `:ash_ai_update_embeddings`\non every update that requires the embeddings to be rebuilt. The `:ash_ai_update_embeddings`-action will be run in the `after_transaction`-phase of any create action and update action that requires the embeddings to be rebuilt.\n\nThis will make your app incredibly slow, and is not recommended for any real production usage.\n\n### `:ash_oban`\n\nRequires the `ash_oban`-dependency to be installed, and that the resource in question uses it as an extension, like this:\n\n```elixir\ndefmodule MyApp.Artist do\n  use Ash.Resource, extensions: [AshAi, AshOban]\nend\n```\n\nJust like the `:after_action`-strategy, this strategy creates an `:ash_ai_update_embeddings` update-action, and adds a global change that will run an `ash_oban`-trigger (also in the `after_transaction`-phase) whenever embeddings need to be rebuilt.\n\nYou will to define this trigger yourself, and then reference it in the `vectorize`-section like this:\n\n```elixir\ndefmodule MyApp.Artist do\n  use Ash.Resource, extensions: [AshAi, AshOban]\n\n  vectorize do\n    full_text do\n      ...\n    end\n\n    strategy :ash_oban\n    ash_oban_trigger_name :my_vectorize_trigger (default name is :ash_ai_update_embeddings)\n    ...\n  end\n\n  oban do\n    triggers do\n      trigger :my_vectorize_trigger do\n        action :ash_ai_update_embeddings\n        worker_read_action :read\n        worker_module_name __MODULE__.AshOban.Worker.UpdateEmbeddings\n        scheduler_module_name __MODULE__.AshOban.Scheduler.UpdateEmbeddings\n        scheduler_cron nil\n        list_tenants MyApp.ListTenants\n      end\n    end\n  end\nend\n```\n\n### `:manual`\n\nWill not automatically update the embeddings in any way, but will by default generated an update action\nnamed `:ash_ai_update_embeddings` that can be run on demand. If needed, you can also disable the\ngeneration of this action like this:\n\n```elixir\nvectorize do\n  full_text do\n    ...\n  end\n\n  strategy :manual\n  define_update_action_for_manual_strategy? false\n  ...\nend\n```\n\n### Embedding Models\n\nEmbedding models are modules that are in charge of defining what the dimensions\nare of a given vector and how to generate one. This example uses `Req` to\ngenerate embeddings using `OpenAi`. To use it, you'd need to install `req`\n(`mix igniter.install req`).\n\n```elixir\ndefmodule Tunez.OpenAIEmbeddingModel do\n  use AshAi.EmbeddingModel\n\n  @impl true\n  def dimensions(_opts), do: 3072\n\n  @impl true\n  def generate(texts, _opts) do\n    api_key = System.fetch_env!(\"OPEN_AI_API_KEY\")\n\n    headers = [\n      {\"Authorization\", \"Bearer #{api_key}\"},\n      {\"Content-Type\", \"application/json\"}\n    ]\n\n    body = %{\n      \"input\" =\u003e texts,\n      \"model\" =\u003e \"text-embedding-3-large\"\n    }\n\n    response =\n      Req.post!(\"https://api.openai.com/v1/embeddings\",\n        json: body,\n        headers: headers\n      )\n\n    case response.status do\n      200 -\u003e\n        response.body[\"data\"]\n        |\u003e Enum.map(fn %{\"embedding\" =\u003e embedding} -\u003e embedding end)\n        |\u003e then(\u0026{:ok, \u00261})\n\n      _status -\u003e\n        {:error, response.body}\n    end\n  end\nend\n```\n\nOpts can be used to make embedding models that are dynamic depending on the resource, i.e\n\n```elixir\nembedding_model {MyApp.OpenAiEmbeddingModel, model: \"a-specific-model\"}\n```\n\nThose opts are available in the `_opts` argument to functions on your embedding model\n\n## Using the vectors\n\nYou can use expressions in filters and sorts like `vector_cosine_distance(full_text_vector, ^search_vector)`. For example:\n\n```elixir\nread :search do\n  argument :query, :string, allow_nil?: false\n\n  prepare before_action(fn query, context -\u003e\n    case YourEmbeddingModel.generate([query.arguments.query], []) do\n      {:ok, [search_vector]} -\u003e\n        Ash.Query.filter(\n          query,\n          vector_cosine_distance(full_text_vector, ^search_vector) \u003c 0.5\n        )\n        |\u003e Ash.Query.sort(\n          {calc(vector_cosine_distance(full_text_vector, ^search_vector),\n             type: :float\n           ), :asc}\n        )\n        |\u003e Ash.Query.limit(10)\n\n      {:error, error} -\u003e\n        {:error, error}\n    end\n  end)\nend\n```\n\n## Building a Vector Index\n\nIf your database stores more than ~10,000 vectors, you may see search performance degrade. You can ameliorate this by building an index on the vector column. Vector indices come at the expense of write speeds and higher resource usage. \n\nThe below example uses an `hnsw` index, which trades higher memory usage and vector build times for faster query speeds. An `ivfflat` index will have different settings, faster build times, lower memory usage, but slower query speeds. Do research and consider the tradeoffs for your use case. \n\n```elixir\n  postgres do\n    table \"embeddings\"\n    repo MyApp.Repo\n\n    custom_statements do\n      statement :vector_idx do\n        up \"CREATE INDEX vector_idx ON embeddings USING hnsw (vectorized_body vector_cosine_ops) WITH (m = 16, ef_construction = 64)\"\n        down \"DROP INDEX vector_idx;\"\n      end\n    end\n  end\n```\n\n\n# Roadmap\n\n- more action types, like:\n  - bulk updates\n  - bulk destroys\n  - bulk creates.\n\n# How to play with it\n\n1. Setup `LangChain`\n2. Modify a `LangChain` using `AshAi.setup_ash_ai/2` or use `AshAi.iex_chat` (see below)\n2. Run `iex -S mix` and then run `AshAi.iex_chat` to start chatting with your app.\n3. Build your own chat interface. See the implementation of `AshAi.iex_chat` to see how its done.\n\n## Contributing \n\n1. make sure to run `mix test.create \u0026\u0026 mix test.migrate` to set up locally\n1. ensure that `mix check` passes\n\n## Using AshAi.iex_chat\n\n```elixir\ndefmodule MyApp.ChatBot do\n  alias LangChain.Chains.LLMChain\n  alias LangChain.ChatModels.ChatOpenAI\n\n  def iex_chat(actor \\\\ nil) do\n    %{\n      llm: ChatOpenAI.new!(%{model: \"gpt-4o\", stream: true}),\n      verbose: true\n    }\n    |\u003e LLMChain.new!()\n    |\u003e AshAi.iex_chat(actor: actor, otp_app: :my_app)\n  end\nend\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fash-project%2Fash_ai","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fash-project%2Fash_ai","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fash-project%2Fash_ai/lists"}